Compare commits
60
Commits
721c3fcfab
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25f339bc8a | ||
|
|
5742457cc0 | ||
|
|
6b6252c42b | ||
|
|
70b937ea2f | ||
|
|
bc1af681aa | ||
|
|
318b847ede | ||
|
|
7d762387e7 | ||
|
|
0d7b8c5310 | ||
|
|
2675495eef | ||
|
|
034096d16d | ||
|
|
15cd369ec3 | ||
|
|
2fd9ef8aba | ||
|
|
ef22e78d8a | ||
|
|
a366f47eff | ||
|
|
6791e8fb5f | ||
|
|
ff38352bfc | ||
|
|
dc837e8e1a | ||
|
|
223b0ccdca | ||
|
|
ea725bd8b9 | ||
|
|
48009583a6 | ||
|
|
e72eb75d10 | ||
|
|
8d25de9e79 | ||
|
|
f8d64948c8 | ||
|
|
e20c22e0cb | ||
|
|
dbaadccc50 | ||
|
|
c808a3b345 | ||
|
|
aaf87d966b | ||
|
|
10520670a5 | ||
|
|
208d97e2ce | ||
|
|
03fff05117 | ||
|
|
621280a530 | ||
|
|
c9d73450b0 | ||
|
|
5fd64b68d0 | ||
|
|
4bdf03a531 | ||
|
|
eaff056d9c | ||
|
|
77de8cece6 | ||
|
|
cab606b90e | ||
|
|
c29df2173f | ||
|
|
ba3887fa69 | ||
|
|
40aef879e4 | ||
|
|
22c6c83a0f | ||
|
|
0de72e09f0 | ||
|
|
3df93ad07f | ||
|
|
57abb7bfa4 | ||
|
|
72d5771ffc | ||
|
|
d98f6036d1 | ||
|
|
1a90c7737d | ||
|
|
b8adeb6582 | ||
|
|
5a2a5e7487 | ||
|
|
1611f7afa8 | ||
|
|
418b5645e5 | ||
|
|
5bd197f031 | ||
|
|
bd5de0cead | ||
|
|
c3b82cf074 | ||
|
|
9590005914 | ||
|
|
caa55d9ff8 | ||
|
|
d76418303a | ||
|
|
6196a0119a | ||
|
|
e25ef02a1a | ||
|
|
807dfde287 |
@@ -1,16 +1,31 @@
|
|||||||
НИКАКОЙ САМОДЕЙТЕЛЬНОСТИ !!! делать ТОЛЬКО ТО НА ЧТО ПОЛУЧЕНО РАЗРЕШЕНИЕ !!!!
|
# Правила работы в этом репозитории
|
||||||
НИКАКИХ ДОГАДОК !!! ЕСТь сомнения - СПРОСИ !!!
|
|
||||||
НИКОГДА НЕ ДЕЛАЙ ПРЕДПОЛОЖЕНИЙ !!!
|
## Разрешения и самодеятельность
|
||||||
ВСЕГДА СПРАШИВАЙ, ЕСЛИ НЕ УВЕРЕН !!!
|
|
||||||
НИКОГДА НЕ ИГНОРИРУЙ ПОЛУЧЕННЫЕ ИНСТРУКЦИИ !!!
|
- **Никакой самодеятельности**: делать только то, на что получено разрешение.
|
||||||
ВСЕГДА ПОДТВЕРЖДАЙ ПОЛУЧЕННЫЕ ИНСТРУКЦИИ !!!
|
- Полученные инструкции **не игнорировать**: соблюдать их и подтверждать.
|
||||||
НИКОГДА НЕ ИЗМЕНЯЙ ИНСТРУКЦИИ БЕЗ РАЗРЕШЕНИЯ !!!
|
- Соблюдать порядок и последовательность инструкций.
|
||||||
ВСЕГДА СОБЛЮДАЙ ПОРЯДОК И ПОСЛЕДОВАТЕЛЬНОСТЬ В ИНСТРУКЦИЯХ !!!
|
- Не превышать свои полномочия.
|
||||||
НИКОГДА НЕ ПРЕВЫШАЙ СВОИ ПОЛНОМОЧИЯ !!!
|
- Соблюдать безопасность и конфиденциальность.
|
||||||
ВСЕГДА СОБЛЮДАЙ БЕЗОПАСНОСТЬ И КОНФИДЕНЦИАЛЬНОСТЬ !!!
|
- Не изменять инструкции без разрешения.
|
||||||
НИКОГДА НЕ ПЕРЕДАВАЙ СЕКРЕТЫ ИНТЕРНЕТУ БЕЗ РАЗРЕШЕНИЯ !!!
|
- Не вызывать другие агенты без разрешения.
|
||||||
НЕ ВЫЗЫВАТЬ ДРУГИЕ АГЕНТЫ БЕЗ РАЗРЕШЕНИЯ !!!
|
- Не передавать секреты в интернет без разрешения.
|
||||||
коммитить после каждой правки, чтобы зафиксировать текущее состояние и избежать потери изменений. Использовать осмысленные сообщения коммитов, отражающие суть изменений.
|
|
||||||
ВСЕГДА СОХРАНЯТЬ РЕЗЕРВНЫЕ КОПИИ ВАЖНЫХ ФАЙЛОВ ПЕРЕД ВНЕСЕНИЕМ ИЗМЕНЕНИЙ.
|
## Сомнения и вопросы
|
||||||
НИКОГДА НЕ ПОЛАГАЙСЯ НА ПАМЯТЬ — ВСЕГДА ПРОВЕРЯЙ АКТУАЛЬНОСТЬ ИНСТРУКЦИЙ.
|
|
||||||
ВСЕГДА СОБЛЮДАЙ ИНСТРУКЦИИ, ДАВАЙТЕ ПОДТВЕРЖДЕНИЯ И НЕ ДЕЛАЙТЕ САМОСТОЯТЕЛЬНЫХ ИЗМЕНЕНИЙ.
|
- **Никаких догадок**: есть сомнения — спроси.
|
||||||
|
- Никогда не делать предположений и не действовать по догадкам.
|
||||||
|
- Всегда спрашивать, если не уверен.
|
||||||
|
- Если уверенности в распоряжении нет на 100 % — остановиться, спросить снова и подтвердить, что имел в виду пользователь. Не гадать.
|
||||||
|
- Перепроверять всё несколько раз.
|
||||||
|
|
||||||
|
## Коммиты и бэкапы
|
||||||
|
|
||||||
|
- Коммитить после каждой правки — чтобы зафиксировать текущее состояние и не потерять изменения.
|
||||||
|
- Сообщения коммитов — осмысленные, отражающие суть изменений.
|
||||||
|
- Всегда сохранять резервные копии важных файлов перед внесением изменений.
|
||||||
|
|
||||||
|
## Общий принцип
|
||||||
|
|
||||||
|
- Соблюдать инструкции, давать подтверждения и не делать самостоятельных изменений.
|
||||||
|
- Не полагаться на память — всегда проверять актуальность инструкций.
|
||||||
@@ -6,6 +6,9 @@
|
|||||||
.terraform.lock.hcl
|
.terraform.lock.hcl
|
||||||
|
|
||||||
# === Generated files (NOT code — regenerate from API) ===
|
# === Generated files (NOT code — regenerate from API) ===
|
||||||
|
# ВАЖНО: provider/internal/provider/operation_timeouts.json — НЕ артефакт.
|
||||||
|
# Это дефолтный конфиг таймаутов для go build/go test (см. operation_timeouts_embed.go),
|
||||||
|
# поэтому он намеренно отслеживается git. Профильные значения — в TOOLS/config/<profile>/.
|
||||||
provider/resources_yaml/
|
provider/resources_yaml/
|
||||||
provider/internal/resources_gen/
|
provider/internal/resources_gen/
|
||||||
|
|
||||||
@@ -30,6 +33,9 @@ provider/generated/
|
|||||||
*.exe
|
*.exe
|
||||||
*.test
|
*.test
|
||||||
*.out
|
*.out
|
||||||
|
# Локально собранный провайдер под dev_overrides (см. TMP/terraformrc.dev)
|
||||||
|
TMP/devbin/
|
||||||
|
terraform-provider-nubes
|
||||||
|
|
||||||
# === Build artifacts (generated by devops scripts) ===
|
# === Build artifacts (generated by devops scripts) ===
|
||||||
devops/profiles/*/generated/
|
devops/profiles/*/generated/
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
resource "nubes_vc_nsxt" "edge" {
|
||||||
|
resource_name = var.nsxt_resource_name
|
||||||
|
|
||||||
|
# Тип родительской услуги: "vdc" (нужен vdc_uid) или "vdcGroup" (нужен vdc_group_uid)
|
||||||
|
vdc_type = var.nsxt_vdc_type
|
||||||
|
|
||||||
|
# refSvc-поле: принимает UUID или имя. Здесь берём UID созданного VDC,
|
||||||
|
# чтобы Edge гарантированно создавался после vDC.
|
||||||
|
vdc_uid = nubes_vc_vdc.vdc.id
|
||||||
|
|
||||||
|
need_enable_avi = var.nsxt_need_enable_avi
|
||||||
|
virtual_services_count = var.nsxt_virtual_services_count
|
||||||
|
|
||||||
|
# routed-сеть, которую разворачивает Edge (SingleNestedAttribute -> объект)
|
||||||
|
routed_net_configuration = {
|
||||||
|
ip_addr_pool = var.nsxt_ip_addr_pool
|
||||||
|
main_dns = var.nsxt_main_dns
|
||||||
|
second_dns = var.nsxt_second_dns
|
||||||
|
}
|
||||||
|
|
||||||
|
# «Заморозка»: destroy НЕ удаляет эдж (у платформы для эджа нет операции suspend),
|
||||||
|
# а только убирает его из состояния. Для полного удаления — keep_on_destroy = false.
|
||||||
|
keep_on_destroy = true
|
||||||
|
|
||||||
|
# Повторный apply усыновляет уже работающий эдж, а не падает с
|
||||||
|
# «РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ (RUNNING)».
|
||||||
|
adopt_existing_on_create = true
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Ресурсы-модификаторы (операции modify, которых нет в create-схеме ресурсов)
|
||||||
|
#
|
||||||
|
# Порядок строго такой:
|
||||||
|
# орга (создана вручную в ЛК)
|
||||||
|
# -> nubes_vc_vdc.vdc
|
||||||
|
# -> nubes_vc_nsxt.edge
|
||||||
|
# -> nubes_vc_org_ip_allocation (выделение внешних IP на орге)
|
||||||
|
# -> nubes_vc_nsxt_snat (SNAT на эдже этим ipSpace)
|
||||||
|
#
|
||||||
|
# Почему аллокация ПОСЛЕ эджа: платформа строит список ipSpace из состояния
|
||||||
|
# `job.vcd.networkProvider` / `job.vcd.providerGateway`, то есть требует уже
|
||||||
|
# созданный vDC и Edge. Иначе modify на орге падает
|
||||||
|
# («Can't cast Complex Object Type Struct to String»).
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# 1. Внешние IP на организации (modify: vIPConfigure, массив перезаписывается целиком)
|
||||||
|
resource "nubes_vc_org_ip_allocation" "org_ip" {
|
||||||
|
organization = var.organization
|
||||||
|
|
||||||
|
vip_configure = jsonencode([
|
||||||
|
{
|
||||||
|
name = var.ip_space_name
|
||||||
|
count = var.ip_count
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
# true = «заморозка»: destroy не трогает квоту внешних IP (кластер Штурвала держит
|
||||||
|
# адреса, опустить count ниже занятых платформа не даёт). Для полного удаления — false
|
||||||
|
# (и только после удаления кластера).
|
||||||
|
keep_on_destroy = true
|
||||||
|
|
||||||
|
depends_on = [nubes_vc_nsxt.edge]
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. SNAT на эдже (modify: ipSpaceName)
|
||||||
|
resource "nubes_vc_nsxt_snat" "snat" {
|
||||||
|
nsxt_uid = nubes_vc_nsxt.edge.id
|
||||||
|
ip_space_name = var.ip_space_name
|
||||||
|
|
||||||
|
# true = «заморозка»: destroy не выключает SNAT на эдже. Для полного удаления — false.
|
||||||
|
keep_on_destroy = true
|
||||||
|
|
||||||
|
# ipSpace должен быть уже выделен на организации
|
||||||
|
depends_on = [nubes_vc_org_ip_allocation.org_ip]
|
||||||
|
}
|
||||||
|
|
||||||
|
output "allocated_org_ip" {
|
||||||
|
description = "Выделено внешних IP на организации"
|
||||||
|
value = {
|
||||||
|
organization = var.organization
|
||||||
|
ip_space_name = var.ip_space_name
|
||||||
|
ip_count = var.ip_count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "snat_ip_space" {
|
||||||
|
description = "ipSpace, включённый как SNAT на эдже"
|
||||||
|
value = nubes_vc_nsxt_snat.snat.ip_space_name
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
output "vdc_id" {
|
||||||
|
description = "UID созданного VDC"
|
||||||
|
value = nubes_vc_vdc.vdc.id
|
||||||
|
}
|
||||||
|
|
||||||
|
output "vdc_name" {
|
||||||
|
description = "Имя VDC"
|
||||||
|
value = nubes_vc_vdc.vdc.resource_name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "vdc_state_params" {
|
||||||
|
description = "Параметры состояния VDC из API"
|
||||||
|
value = nubes_vc_vdc.vdc.state_params
|
||||||
|
}
|
||||||
|
|
||||||
|
output "nsxt_id" {
|
||||||
|
description = "UID созданного Edge (vc_nsxt)"
|
||||||
|
value = nubes_vc_nsxt.edge.id
|
||||||
|
}
|
||||||
|
|
||||||
|
output "nsxt_name" {
|
||||||
|
description = "Имя Edge (vc_nsxt)"
|
||||||
|
value = nubes_vc_nsxt.edge.resource_name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "nsxt_state_params" {
|
||||||
|
description = "Параметры состояния Edge (vc_nsxt) из API"
|
||||||
|
value = nubes_vc_nsxt.edge.state_params
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
provider "nubes" {
|
||||||
|
api_token = var.api_token
|
||||||
|
api_endpoint = var.api_endpoint
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Kubernetes кластер Штурвал — сервис 150, ресурс nubes_k8s_sthutrval_cluster
|
||||||
|
# (НЕ 148 «Менеджмент Kubernetes кластер Штурвал» — это другой сервис)
|
||||||
|
#
|
||||||
|
# Всё, что относится к Штурвалу, лежит ТОЛЬКО в этом файле: переменные, их
|
||||||
|
# значения по умолчанию и сам ресурс. Чтобы выключить Штурвал — удалить файл
|
||||||
|
# или закомментировать ресурс.
|
||||||
|
#
|
||||||
|
# Порядок (чек-лист из инструкции на услугу в ЛК):
|
||||||
|
# 1) Организация в Cloud Director — создана вручную в ЛК
|
||||||
|
# 2) nubes_vc_vdc.vdc — есть
|
||||||
|
# 3) nubes_vc_nsxt.edge — есть, обязательно ALB + AVI VS >= 3
|
||||||
|
# 4) внешние адреса в организации — суммарно >= 3 (nubes_vc_org_ip_allocation)
|
||||||
|
# 5) SNAT на Edge — nubes_vc_nsxt_snat
|
||||||
|
# 6) Kubernetes кластер Штурвал — этот ресурс
|
||||||
|
#
|
||||||
|
# Минимальные требования к кластеру: мастер-нод >= 1, воркер-нод >= 1,
|
||||||
|
# 4 vCPU / 8 GB RAM / 50 GB диска на ноду.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# --- Переменные Штурвала ---
|
||||||
|
|
||||||
|
variable "shturval_resource_name" {
|
||||||
|
type = string
|
||||||
|
default = "shturval-dev1"
|
||||||
|
description = "Имя услуги «Kubernetes кластер Штурвал» в ЛК"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cluster_name" {
|
||||||
|
type = string
|
||||||
|
default = "shturval-dev-01"
|
||||||
|
description = "Имя кластера внутри Штурвала"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_app_version" {
|
||||||
|
type = string
|
||||||
|
default = "2.14.0"
|
||||||
|
description = "Версия Штурвала (значение по умолчанию платформы — 2.14.0)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cp_sizing_policy" {
|
||||||
|
type = string
|
||||||
|
default = "TKG 4CPU 8RAM"
|
||||||
|
description = "Политика размера control plane: 4 vCPU / 8 GB (минимум по инструкции). Должна существовать в ресурсной платформе vDC — список политик берётся из услуги «Виртуальный датацентр»"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cp_sizing_disk" {
|
||||||
|
type = number
|
||||||
|
default = 50
|
||||||
|
description = "Диск control plane, ГБ (минимум 50)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cp_count" {
|
||||||
|
type = number
|
||||||
|
default = 1
|
||||||
|
description = "Количество мастер-нод: 1, 3 или 5"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_group_name" {
|
||||||
|
type = string
|
||||||
|
default = "workers-shturval-dev"
|
||||||
|
description = "Имя группы воркеров (уникальное в кластере; допустимы строчные латинские буквы, цифры и дефис)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_sizing_policy" {
|
||||||
|
type = string
|
||||||
|
default = "TKG 4CPU 8RAM"
|
||||||
|
description = "Политика размера воркеров: 4 vCPU / 8 GB (минимум по инструкции)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_sizing_disk" {
|
||||||
|
type = number
|
||||||
|
default = 50
|
||||||
|
description = "Диск воркеров, ГБ (минимум 50)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_count" {
|
||||||
|
type = number
|
||||||
|
default = 1
|
||||||
|
description = "Количество воркер-нод (минимум 1)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Значения, которые собираются из переменных ---
|
||||||
|
|
||||||
|
locals {
|
||||||
|
# Группы воркеров передаются JSON-строкой ВНУТРЬ услуги как есть, поэтому ключи
|
||||||
|
# должны быть ровно такими, как в манифесте услуги 150: groupName, sizingPolicy,
|
||||||
|
# sizingDisk, count, autoscale, labelDeck.
|
||||||
|
# ВНИМАНИЕ: в сгенерированном примере провайдера (docs → Example) ключи показаны
|
||||||
|
# в snake_case — это ошибка генератора, платформа на них падает с
|
||||||
|
# «Cannot invoke method split() on null object» (не находит groupName → null).
|
||||||
|
shturval_worker_config = jsonencode([
|
||||||
|
{
|
||||||
|
groupName = var.shturval_worker_group_name
|
||||||
|
sizingPolicy = var.shturval_worker_sizing_policy
|
||||||
|
sizingDisk = var.shturval_worker_sizing_disk
|
||||||
|
count = var.shturval_worker_count
|
||||||
|
autoscale = false # автоскейл выключен
|
||||||
|
labelDeck = true # разрешить разворачивать услуги из ЛК на этих нодах
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Ресурс Штурвала ---
|
||||||
|
|
||||||
|
resource "nubes_k8s_sthutrval_cluster" "shturval" {
|
||||||
|
resource_name = var.shturval_resource_name
|
||||||
|
|
||||||
|
# Кластер Штурвала уже существует (инстанс «shturval-dev») и в проде не
|
||||||
|
# удаляется неделями, поэтому ресурс должен УСЫНОВИТЬ существующий инстанс,
|
||||||
|
# а не падать с «РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ (SUSPEND)».
|
||||||
|
# Проверка/adopt выполняются в Create на apply (в plan будет «will be created»).
|
||||||
|
adopt_existing_on_create = true
|
||||||
|
|
||||||
|
# «Заморозка»: destroy приостанавливает кластер (suspend), а не удаляет.
|
||||||
|
# Следующий apply усыновит его и разморозит (resume).
|
||||||
|
suspend_on_destroy = true
|
||||||
|
|
||||||
|
# Штурвал создаётся долго (десятки минут) — поднимаем таймаут ожидания,
|
||||||
|
# иначе провайдер сдаётся на дефолтных 600 с.
|
||||||
|
operation_timeout = "60m"
|
||||||
|
|
||||||
|
startup_configuration = {
|
||||||
|
# vDC и Edge из этого же конфига (обязательные поля)
|
||||||
|
vdc_uid = nubes_vc_vdc.vdc.id
|
||||||
|
nsxt_uid = nubes_vc_nsxt.edge.id
|
||||||
|
|
||||||
|
cluster_name = var.shturval_cluster_name
|
||||||
|
|
||||||
|
# Дополнительные возможности кластера (в ЛК — галочки при создании)
|
||||||
|
ex_logging = true # логи в Loki (без него логи услуг не видны в ЛК)
|
||||||
|
ex_monitoring = true # метрики в VictoriaMetrics (без него метрик в ЛК нет)
|
||||||
|
ex_local_csi = true
|
||||||
|
ex_vip = true
|
||||||
|
ex_update = true
|
||||||
|
ex_ingress = true
|
||||||
|
ex_named_csi = true
|
||||||
|
}
|
||||||
|
|
||||||
|
cluster_configuration = {
|
||||||
|
app_version = var.shturval_app_version
|
||||||
|
}
|
||||||
|
|
||||||
|
control_plane_configuration = {
|
||||||
|
sizing_policy = var.shturval_cp_sizing_policy
|
||||||
|
sizing_disk = var.shturval_cp_sizing_disk
|
||||||
|
count = var.shturval_cp_count
|
||||||
|
}
|
||||||
|
|
||||||
|
worker_configuration = local.shturval_worker_config
|
||||||
|
|
||||||
|
access_configuration = {
|
||||||
|
need_external_address_api = true # внешний адрес для Kubernetes API (false недопустим)
|
||||||
|
access_ip_list_api = jsonencode([]) # пусто = доступ всем
|
||||||
|
need_external_address_ingress = true # внешний адрес для Ingress
|
||||||
|
access_ip_list_ingress = jsonencode([]) # пусто = доступ всем
|
||||||
|
}
|
||||||
|
|
||||||
|
# Кластер поднимается только после готовой сети: vDC -> Edge -> внешние IP -> SNAT
|
||||||
|
depends_on = [nubes_vc_nsxt_snat.snat]
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
api_token = "ВАШ_ТОКЕН_ИЗ_ЛК"
|
||||||
|
|
||||||
|
# Имя или UUID организации:
|
||||||
|
organization = "kontra"
|
||||||
|
|
||||||
|
vdc_resource_name = "fullpipe-vdc"
|
||||||
|
vdc_network_provider = "snb1"
|
||||||
|
vdc_provider_vdc = "Intel Broadwell 2.4"
|
||||||
|
vdc_cpu_allocated = 8
|
||||||
|
vdc_cpu_guaranteed = 0
|
||||||
|
vdc_mem_allocated = 32
|
||||||
|
|
||||||
|
vdc_storage_config = "[{\"name\":\"SATA\",\"size\":\"200\"}]"
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
variable "api_token" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
description = "API-токен Nubes"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "api_endpoint" {
|
||||||
|
type = string
|
||||||
|
default = "https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc"
|
||||||
|
description = "API Gateway URL"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Имя (display_name, напр. "kontora") ИЛИ UUID организации из ЛК
|
||||||
|
variable "organization" {
|
||||||
|
type = string
|
||||||
|
description = "Имя или UUID организации (vc_org)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Модификаторы (IP на орге + SNAT на эдже) ---
|
||||||
|
|
||||||
|
variable "ip_space_name" {
|
||||||
|
type = string
|
||||||
|
description = "Имя ipSpace, доступное организации (смотреть в ЛК, напр. internet-ipv4-v1)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ip_count" {
|
||||||
|
type = string
|
||||||
|
default = "3"
|
||||||
|
description = "Сколько внешних IP выделить на организации (count — строка)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vdc_resource_name" {
|
||||||
|
type = string
|
||||||
|
default = "fullpipe-vdc"
|
||||||
|
description = "Имя VDC"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vdc_network_provider" {
|
||||||
|
type = string
|
||||||
|
default = null
|
||||||
|
description = "Сетевой провайдер. Заполнить значением из текущей страницы ЛК"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vdc_provider_vdc" {
|
||||||
|
type = string
|
||||||
|
default = null
|
||||||
|
description = "Provider VDC. Заполнить значением из текущей страницы ЛК"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vdc_cpu_allocated" {
|
||||||
|
type = number
|
||||||
|
default = 8
|
||||||
|
description = "vCPU (шт.)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vdc_cpu_guaranteed" {
|
||||||
|
type = number
|
||||||
|
default = 0
|
||||||
|
description = "Резервирование vCPU (%, допустимо: 0, 50, 80)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vdc_mem_allocated" {
|
||||||
|
type = number
|
||||||
|
default = 32
|
||||||
|
description = "RAM (GB)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vdc_storage_config" {
|
||||||
|
type = string
|
||||||
|
default = "[{\"name\":\"SATA\",\"size\":\"200\"}]"
|
||||||
|
description = "Дисковое хранилище (JSON-массив, size в GB). Имя политики должно существовать в ресурсном пуле (например, SATA, SSD)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- vc_nsxt (Сетевой шлюз периметра / Edge) ---
|
||||||
|
|
||||||
|
variable "nsxt_resource_name" {
|
||||||
|
type = string
|
||||||
|
default = "fullpipe-edge"
|
||||||
|
description = "Имя Edge (vc_nsxt)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "nsxt_vdc_type" {
|
||||||
|
type = string
|
||||||
|
default = "vdc"
|
||||||
|
description = "Тип родительской услуги: vdc или vdcGroup"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "nsxt_need_enable_avi" {
|
||||||
|
type = bool
|
||||||
|
default = true
|
||||||
|
description = "Включить AVI Load Balancer (ALB)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "nsxt_virtual_services_count" {
|
||||||
|
type = number
|
||||||
|
default = 3
|
||||||
|
description = "Кол-во виртуальных сервисов на AVI (1..4; Штурвал: ≥ 3)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "nsxt_ip_addr_pool" {
|
||||||
|
type = string
|
||||||
|
default = "10.10.102.0/24"
|
||||||
|
description = "Адресный пул routed-сети (маска /24 обязательна)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "nsxt_main_dns" {
|
||||||
|
type = string
|
||||||
|
default = "81.22.46.22"
|
||||||
|
description = "Основной DNS"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "nsxt_second_dns" {
|
||||||
|
type = string
|
||||||
|
default = "185.247.187.77"
|
||||||
|
description = "Второй DNS"
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
resource "nubes_vc_vdc" "vdc" {
|
||||||
|
resource_name = var.vdc_resource_name
|
||||||
|
|
||||||
|
# Организация: имя из ЛК ("kontora") или точный UUID
|
||||||
|
organization_uid = var.organization
|
||||||
|
|
||||||
|
network_provider = var.vdc_network_provider
|
||||||
|
provider_vdc = var.vdc_provider_vdc
|
||||||
|
|
||||||
|
cpu_allocated = var.vdc_cpu_allocated
|
||||||
|
cpu_guaranteed = var.vdc_cpu_guaranteed
|
||||||
|
mem_allocated = var.vdc_mem_allocated
|
||||||
|
|
||||||
|
# JSON-массив дисковых политик (size в GB)
|
||||||
|
storage_config = var.vdc_storage_config
|
||||||
|
|
||||||
|
suspend_on_destroy = true
|
||||||
|
|
||||||
|
adopt_existing_on_create = true
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.5.0"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
nubes = {
|
||||||
|
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes"
|
||||||
|
version = "2.0.23"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Виртуальная машина внутри vApp — услуги 26 (vApp) и 28 (ВМ)
|
||||||
|
#
|
||||||
|
# Всё, что относится к ВМ, лежит ТОЛЬКО в этом файле: переменные, их значения
|
||||||
|
# по умолчанию, оба ресурса и выводы. Чтобы выключить ВМ — удалить или
|
||||||
|
# закомментировать этот файл (по аналогии с shturval.tf).
|
||||||
|
#
|
||||||
|
# Место в цепочке:
|
||||||
|
# орга (вручную в ЛК) → vDC (21) → Edge (22) → внешние IP → SNAT
|
||||||
|
# → [ vApp (26) → ВМ (28) ] → Штурвал (150)
|
||||||
|
#
|
||||||
|
# Зависимости (из манифестов услуг, сгенерированные ресурсы):
|
||||||
|
# vApp (26) — nubes_vapp: требует vdc_uid (21) и nsxt_uid (22)
|
||||||
|
# ВМ (28) — nubes_vc_vm_v3: требует vapp_uid (26)
|
||||||
|
#
|
||||||
|
# Внешний доступ: ВМ публикуется за общим SNAT эджа (same_snat = false), для
|
||||||
|
# этого ipSpace должен быть выделен на организации и включён как SNAT
|
||||||
|
# (см. modifiers.tf). Нужен ВЫДЕЛЕННЫЙ внешний адрес — same_snat = true.
|
||||||
|
# ipSpace для ВМ берём тот же, что у SNAT (var.ip_space_name).
|
||||||
|
#
|
||||||
|
# Режим destroy: у обеих услуг операция delete требует предварительного
|
||||||
|
# suspend, поэтому по умолчанию suspend_on_destroy = true («заморозка»).
|
||||||
|
# Полное удаление vApp возможно только через 14 дней после suspend.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# --- Переменные vApp ---
|
||||||
|
|
||||||
|
variable "vapp_resource_name" {
|
||||||
|
type = string
|
||||||
|
default = "fullpipe-vapp"
|
||||||
|
description = "Имя услуги «Виртуальный каталог ВМ (vApp)» в ЛК"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vapp_name" {
|
||||||
|
type = string
|
||||||
|
default = "fullpipe-vapp-01"
|
||||||
|
description = "Имя vApp. Маска ^[a-z0-9][a-z0-9.-]{3,61}[a-z0-9]$, уникально в организации; участвует в DNS-имени ВМ. НЕ оставлять дефолтом платформы."
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Переменные ВМ ---
|
||||||
|
|
||||||
|
variable "vm_resource_name" {
|
||||||
|
type = string
|
||||||
|
default = "fullpipe-vm-01"
|
||||||
|
description = "Имя услуги «Виртуальная машина» в ЛК"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vm_name" {
|
||||||
|
type = string
|
||||||
|
default = "web01"
|
||||||
|
description = "Имя ВМ. Маска ^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$. Определяет имя NSX-T IP Set: {vapp_name}-{vm_name}"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vm_image" {
|
||||||
|
type = string
|
||||||
|
default = "Ubuntu_22-20G"
|
||||||
|
description = "Образ ОС. Доступные значения: RockyLinux_9-16G-cloudinit, Ubuntu_22-20G, Debian_13-20G. Не изменяется после создания"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vm_cpu" {
|
||||||
|
type = number
|
||||||
|
default = 2
|
||||||
|
description = "vCPU (1..64), шт"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vm_ram" {
|
||||||
|
type = number
|
||||||
|
default = 2
|
||||||
|
description = "RAM (1..256), GB"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vm_disk" {
|
||||||
|
type = number
|
||||||
|
default = 20
|
||||||
|
description = "Дополнительный диск, GB (основной диск зависит от образа)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vm_user_login" {
|
||||||
|
type = string
|
||||||
|
default = "ubuntu"
|
||||||
|
description = "Учётка SSH. Не изменяется после создания"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vm_user_public_key" {
|
||||||
|
type = string
|
||||||
|
description = "Публичная часть SSH-ключа в формате OpenSSH. Не изменяется после создания. Значение — в terraform.tfvars"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vm_access_port_list" {
|
||||||
|
type = list(object({
|
||||||
|
port = string
|
||||||
|
type = string
|
||||||
|
}))
|
||||||
|
default = [
|
||||||
|
{ port = "22", type = "tcp" }
|
||||||
|
]
|
||||||
|
description = "Белый список портов для доступа извне; type: tcp | udp | all"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vm_access_ip_list" {
|
||||||
|
type = list(string)
|
||||||
|
default = ["0.0.0.0/0"]
|
||||||
|
description = "Белый список адресов, которым разрешён доступ к ВМ. Требует выделенного внешнего IP"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vm_same_snat" {
|
||||||
|
type = bool
|
||||||
|
default = false
|
||||||
|
description = "false — публикация за общим SNAT эджа; true — за выделенным внешним IP услуги"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- vApp (услуга 26) ---
|
||||||
|
|
||||||
|
resource "nubes_vapp" "vapp" {
|
||||||
|
resource_name = var.vapp_resource_name
|
||||||
|
vapp_name = var.vapp_name
|
||||||
|
|
||||||
|
vdc_uid = nubes_vc_vdc.vdc.id # ref 21 — вычислительная инфраструктура
|
||||||
|
nsxt_uid = nubes_vc_nsxt.edge.id # ref 22 — сеть/маршрутизация
|
||||||
|
|
||||||
|
# «Заморозка»: destroy переводит vApp в suspend (delete требует suspend).
|
||||||
|
suspend_on_destroy = true
|
||||||
|
|
||||||
|
# Повторный apply усыновляет существующий vApp, а не падает с
|
||||||
|
# «РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ».
|
||||||
|
adopt_existing_on_create = true
|
||||||
|
|
||||||
|
# vApp требует готовую сеть (ipSpace на организации + SNAT на эдже).
|
||||||
|
depends_on = [nubes_vc_nsxt_snat.snat]
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- ВМ (услуга 28) ---
|
||||||
|
|
||||||
|
resource "nubes_vc_vm_v3" "vm" {
|
||||||
|
resource_name = var.vm_resource_name
|
||||||
|
vm_name = var.vm_name
|
||||||
|
|
||||||
|
vapp_uid = nubes_vapp.vapp.id # ref 26 — ВМ размещается в vApp
|
||||||
|
image_vm = var.vm_image
|
||||||
|
|
||||||
|
vm_cpu = var.vm_cpu
|
||||||
|
vm_ram = var.vm_ram
|
||||||
|
vm_disk = var.vm_disk
|
||||||
|
|
||||||
|
user_login = var.vm_user_login
|
||||||
|
user_public_key = var.vm_user_public_key
|
||||||
|
|
||||||
|
# Внешний доступ
|
||||||
|
ip_space_name = var.ip_space_name # тот же ipSpace, что у SNAT эджа
|
||||||
|
same_snat = var.vm_same_snat
|
||||||
|
access_port_list = jsonencode(var.vm_access_port_list)
|
||||||
|
access_ip_list = jsonencode(var.vm_access_ip_list)
|
||||||
|
|
||||||
|
# «Заморозка»: destroy переводит ВМ в suspend.
|
||||||
|
suspend_on_destroy = true
|
||||||
|
|
||||||
|
adopt_existing_on_create = true
|
||||||
|
|
||||||
|
# ВМ создаётся платформой долго — поднимаем таймаут ожидания.
|
||||||
|
operation_timeout = "15m"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Выводы ---
|
||||||
|
|
||||||
|
output "vapp_id" {
|
||||||
|
description = "UID созданного vApp (услуга 26)"
|
||||||
|
value = nubes_vapp.vapp.id
|
||||||
|
}
|
||||||
|
|
||||||
|
output "vapp_name" {
|
||||||
|
description = "Имя vApp"
|
||||||
|
value = nubes_vapp.vapp.vapp_name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "vm_id" {
|
||||||
|
description = "UID созданной ВМ (услуга 28)"
|
||||||
|
value = nubes_vc_vm_v3.vm.id
|
||||||
|
}
|
||||||
|
|
||||||
|
output "vm_state_flat" {
|
||||||
|
description = "Плоский state ВМ — IP-адреса, статус и т.д."
|
||||||
|
value = nubes_vc_vm_v3.vm.state_out_flat
|
||||||
|
}
|
||||||
@@ -17,4 +17,12 @@ resource "nubes_vc_nsxt" "edge" {
|
|||||||
main_dns = var.nsxt_main_dns
|
main_dns = var.nsxt_main_dns
|
||||||
second_dns = var.nsxt_second_dns
|
second_dns = var.nsxt_second_dns
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# «Заморозка»: destroy НЕ удаляет эдж (у платформы для эджа нет операции suspend),
|
||||||
|
# а только убирает его из состояния. Для полного удаления — keep_on_destroy = false.
|
||||||
|
keep_on_destroy = true
|
||||||
|
|
||||||
|
# Повторный apply усыновляет уже работающий эдж, а не падает с
|
||||||
|
# «РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ (RUNNING)».
|
||||||
|
adopt_existing_on_create = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
# 1. Внешние IP на организации (modify: vIPConfigure, массив перезаписывается целиком)
|
# 1. Внешние IP на организации (modify: vIPConfigure, массив перезаписывается целиком)
|
||||||
resource "nubes_vc_org_ip_allocation" "org_ip" {
|
resource "nubes_vc_org_ip_allocation" "org_ip" {
|
||||||
org_uid = var.org_uid
|
organization = var.organization
|
||||||
|
|
||||||
vip_configure = jsonencode([
|
vip_configure = jsonencode([
|
||||||
{
|
{
|
||||||
@@ -25,8 +25,10 @@ resource "nubes_vc_org_ip_allocation" "org_ip" {
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
# false = при destroy отправить обратный modify с count=0 (квота обнулится)
|
# true = «заморозка»: destroy не трогает квоту внешних IP (кластер Штурвала держит
|
||||||
keep_on_destroy = false
|
# адреса, опустить count ниже занятых платформа не даёт). Для полного удаления — false
|
||||||
|
# (и только после удаления кластера).
|
||||||
|
keep_on_destroy = true
|
||||||
|
|
||||||
depends_on = [nubes_vc_nsxt.edge]
|
depends_on = [nubes_vc_nsxt.edge]
|
||||||
}
|
}
|
||||||
@@ -36,7 +38,8 @@ resource "nubes_vc_nsxt_snat" "snat" {
|
|||||||
nsxt_uid = nubes_vc_nsxt.edge.id
|
nsxt_uid = nubes_vc_nsxt.edge.id
|
||||||
ip_space_name = var.ip_space_name
|
ip_space_name = var.ip_space_name
|
||||||
|
|
||||||
keep_on_destroy = false
|
# true = «заморозка»: destroy не выключает SNAT на эдже. Для полного удаления — false.
|
||||||
|
keep_on_destroy = true
|
||||||
|
|
||||||
# ipSpace должен быть уже выделен на организации
|
# ipSpace должен быть уже выделен на организации
|
||||||
depends_on = [nubes_vc_org_ip_allocation.org_ip]
|
depends_on = [nubes_vc_org_ip_allocation.org_ip]
|
||||||
@@ -45,7 +48,7 @@ resource "nubes_vc_nsxt_snat" "snat" {
|
|||||||
output "allocated_org_ip" {
|
output "allocated_org_ip" {
|
||||||
description = "Выделено внешних IP на организации"
|
description = "Выделено внешних IP на организации"
|
||||||
value = {
|
value = {
|
||||||
org_uid = var.org_uid
|
organization = var.organization
|
||||||
ip_space_name = var.ip_space_name
|
ip_space_name = var.ip_space_name
|
||||||
ip_count = var.ip_count
|
ip_count = var.ip_count
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Kubernetes кластер Штурвал — сервис 150, ресурс nubes_k8s_sthutrval_cluster
|
||||||
|
# (НЕ 148 «Менеджмент Kubernetes кластер Штурвал» — это другой сервис)
|
||||||
|
#
|
||||||
|
# Всё, что относится к Штурвалу, лежит ТОЛЬКО в этом файле: переменные, их
|
||||||
|
# значения по умолчанию и сам ресурс. Чтобы выключить Штурвал — удалить файл
|
||||||
|
# или закомментировать ресурс.
|
||||||
|
#
|
||||||
|
# Порядок (чек-лист из инструкции на услугу в ЛК):
|
||||||
|
# 1) Организация в Cloud Director — создана вручную в ЛК
|
||||||
|
# 2) nubes_vc_vdc.vdc — есть
|
||||||
|
# 3) nubes_vc_nsxt.edge — есть, обязательно ALB + AVI VS >= 3
|
||||||
|
# 4) внешние адреса в организации — суммарно >= 3 (nubes_vc_org_ip_allocation)
|
||||||
|
# 5) SNAT на Edge — nubes_vc_nsxt_snat
|
||||||
|
# 6) Kubernetes кластер Штурвал — этот ресурс
|
||||||
|
#
|
||||||
|
# Минимальные требования к кластеру: мастер-нод >= 1, воркер-нод >= 1,
|
||||||
|
# 4 vCPU / 8 GB RAM / 50 GB диска на ноду.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# --- Переменные Штурвала ---
|
||||||
|
|
||||||
|
variable "shturval_resource_name" {
|
||||||
|
type = string
|
||||||
|
default = "shturval-dev"
|
||||||
|
description = "Имя услуги «Kubernetes кластер Штурвал» в ЛК"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cluster_name" {
|
||||||
|
type = string
|
||||||
|
default = "shturval-dev-00"
|
||||||
|
description = "Имя кластера внутри Штурвала"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_app_version" {
|
||||||
|
type = string
|
||||||
|
default = "2.14.0"
|
||||||
|
description = "Версия Штурвала (значение по умолчанию платформы — 2.14.0)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cp_sizing_policy" {
|
||||||
|
type = string
|
||||||
|
default = "TKG 4CPU 8RAM"
|
||||||
|
description = "Политика размера control plane: 4 vCPU / 8 GB (минимум по инструкции). Должна существовать в ресурсной платформе vDC — список политик берётся из услуги «Виртуальный датацентр»"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cp_sizing_disk" {
|
||||||
|
type = number
|
||||||
|
default = 50
|
||||||
|
description = "Диск control plane, ГБ (минимум 50)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cp_count" {
|
||||||
|
type = number
|
||||||
|
default = 1
|
||||||
|
description = "Количество мастер-нод: 1, 3 или 5"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_group_name" {
|
||||||
|
type = string
|
||||||
|
default = "workers-shturval-dev"
|
||||||
|
description = "Имя группы воркеров (уникальное в кластере; допустимы строчные латинские буквы, цифры и дефис)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_sizing_policy" {
|
||||||
|
type = string
|
||||||
|
default = "TKG 4CPU 8RAM"
|
||||||
|
description = "Политика размера воркеров: 4 vCPU / 8 GB (минимум по инструкции)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_sizing_disk" {
|
||||||
|
type = number
|
||||||
|
default = 50
|
||||||
|
description = "Диск воркеров, ГБ (минимум 50)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_count" {
|
||||||
|
type = number
|
||||||
|
default = 1
|
||||||
|
description = "Количество воркер-нод (минимум 1)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Значения, которые собираются из переменных ---
|
||||||
|
|
||||||
|
locals {
|
||||||
|
# Группы воркеров передаются JSON-строкой ВНУТРЬ услуги как есть, поэтому ключи
|
||||||
|
# должны быть ровно такими, как в манифесте услуги 150: groupName, sizingPolicy,
|
||||||
|
# sizingDisk, count, autoscale, labelDeck.
|
||||||
|
# ВНИМАНИЕ: в сгенерированном примере провайдера (docs → Example) ключи показаны
|
||||||
|
# в snake_case — это ошибка генератора, платформа на них падает с
|
||||||
|
# «Cannot invoke method split() on null object» (не находит groupName → null).
|
||||||
|
shturval_worker_config = jsonencode([
|
||||||
|
{
|
||||||
|
groupName = var.shturval_worker_group_name
|
||||||
|
sizingPolicy = var.shturval_worker_sizing_policy
|
||||||
|
sizingDisk = var.shturval_worker_sizing_disk
|
||||||
|
count = var.shturval_worker_count
|
||||||
|
autoscale = false # автоскейл выключен
|
||||||
|
labelDeck = true # разрешить разворачивать услуги из ЛК на этих нодах
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Ресурс Штурвала ---
|
||||||
|
|
||||||
|
resource "nubes_k8s_sthutrval_cluster" "shturval" {
|
||||||
|
resource_name = var.shturval_resource_name
|
||||||
|
|
||||||
|
# Кластер Штурвала уже существует (инстанс «shturval-dev») и в проде не
|
||||||
|
# удаляется неделями, поэтому ресурс должен УСЫНОВИТЬ существующий инстанс,
|
||||||
|
# а не падать с «РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ (SUSPEND)».
|
||||||
|
# Проверка/adopt выполняются в Create на apply (в plan будет «will be created»).
|
||||||
|
adopt_existing_on_create = true
|
||||||
|
|
||||||
|
# «Заморозка»: destroy приостанавливает кластер (suspend), а не удаляет.
|
||||||
|
# Следующий apply усыновит его и разморозит (resume).
|
||||||
|
suspend_on_destroy = true
|
||||||
|
|
||||||
|
# Штурвал создаётся долго (десятки минут) — поднимаем таймаут ожидания,
|
||||||
|
# иначе провайдер сдаётся на дефолтных 600 с.
|
||||||
|
operation_timeout = "60m"
|
||||||
|
|
||||||
|
startup_configuration = {
|
||||||
|
# vDC и Edge из этого же конфига (обязательные поля)
|
||||||
|
vdc_uid = nubes_vc_vdc.vdc.id
|
||||||
|
nsxt_uid = nubes_vc_nsxt.edge.id
|
||||||
|
|
||||||
|
cluster_name = var.shturval_cluster_name
|
||||||
|
|
||||||
|
# Дополнительные возможности кластера (в ЛК — галочки при создании)
|
||||||
|
ex_logging = true # логи в Loki (без него логи услуг не видны в ЛК)
|
||||||
|
ex_monitoring = true # метрики в VictoriaMetrics (без него метрик в ЛК нет)
|
||||||
|
ex_local_csi = true
|
||||||
|
ex_vip = true
|
||||||
|
ex_update = true
|
||||||
|
ex_ingress = true
|
||||||
|
ex_named_csi = true
|
||||||
|
}
|
||||||
|
|
||||||
|
cluster_configuration = {
|
||||||
|
app_version = var.shturval_app_version
|
||||||
|
}
|
||||||
|
|
||||||
|
control_plane_configuration = {
|
||||||
|
sizing_policy = var.shturval_cp_sizing_policy
|
||||||
|
sizing_disk = var.shturval_cp_sizing_disk
|
||||||
|
count = var.shturval_cp_count
|
||||||
|
}
|
||||||
|
|
||||||
|
worker_configuration = local.shturval_worker_config
|
||||||
|
|
||||||
|
access_configuration = {
|
||||||
|
need_external_address_api = true # внешний адрес для Kubernetes API (false недопустим)
|
||||||
|
access_ip_list_api = jsonencode([]) # пусто = доступ всем
|
||||||
|
need_external_address_ingress = true # внешний адрес для Ingress
|
||||||
|
access_ip_list_ingress = jsonencode([]) # пусто = доступ всем
|
||||||
|
}
|
||||||
|
|
||||||
|
# Кластер поднимается только после готовой сети: vDC -> Edge -> внешние IP -> SNAT
|
||||||
|
depends_on = [nubes_vc_nsxt_snat.snat]
|
||||||
|
}
|
||||||
@@ -16,12 +16,6 @@ variable "organization" {
|
|||||||
description = "Имя или UUID организации (vc_org)"
|
description = "Имя или UUID организации (vc_org)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# UUID той же организации — нужен ресурсам-модификаторам (они адресуются строго по uid)
|
|
||||||
variable "org_uid" {
|
|
||||||
type = string
|
|
||||||
description = "UUID организации (vc_org) для nubes_vc_org_ip_allocation"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Модификаторы (IP на орге + SNAT на эдже) ---
|
# --- Модификаторы (IP на орге + SNAT на эдже) ---
|
||||||
|
|
||||||
variable "ip_space_name" {
|
variable "ip_space_name" {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ terraform {
|
|||||||
required_providers {
|
required_providers {
|
||||||
nubes = {
|
nubes = {
|
||||||
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes"
|
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes"
|
||||||
version = "2.0.19"
|
version = "2.0.23"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# 2026-09-24 — Штурвал dev-00: диагностика, adopt и дизайн «freeze on destroy»
|
||||||
|
|
||||||
|
Краткая запись по дню. Разбор с источниками (файл:строка, ответы API, логи) —
|
||||||
|
`NOTES/30_analysis/SHTURVAL_DEV00_DIAG_AND_FREEZE_DESIGN_2026-09-24.md`,
|
||||||
|
резюме для продолжения — `NOTES/40_chat_summaries/CHAT_RESUME_2026-09-24_shturval_freeze.md`.
|
||||||
|
|
||||||
|
## Изменения в репозитории
|
||||||
|
|
||||||
|
| Что | Файл | Коммит |
|
||||||
|
|---|---|---|
|
||||||
|
| `adopt_existing_on_create = true` для кластера Штурвала (иначе apply падал на существующем suspended-инстансе) | `DEV_STAND/FullPipe/shturval.tf` | `57abb7b` |
|
||||||
|
| Документация сессии (диагностика + дизайн freeze) | `NOTES/30_analysis/…`, `NOTES/40_chat_summaries/…` | `3df93ad` |
|
||||||
|
| Универсальный третий режим destroy `keep_on_destroy` (`state_only`) для всех instance-ресурсов + предупреждения в `Delete` | `TOOLS/resource-generator/{types.go,loader.go,templates/instance.go}` | `22c6c83` |
|
||||||
|
| Режим «заморозки» в конфиге стенда: `keep_on_destroy=true` (эдж/SNAT/квота IP), adopt для эджа, явный `suspend_on_destroy` у кластера | `DEV_STAND/FullPipe/{edge.tf,modifiers.tf,shturval.tf}` | `40aef87` |
|
||||||
|
| Релиз dev-провайдера `2.0.22` (три платформы + SHA256SUMS/подпись, залито в реестр) | `VERSIONS.md` | `c29df21` |
|
||||||
|
|
||||||
|
## Баг после заморозки: регистр UUID внутри JSON (исправлен)
|
||||||
|
|
||||||
|
- Первый `apply` после freeze упал: `required params mismatch … startupConfiguration` — `nsxtUid` в плане
|
||||||
|
(`2c37fed1-…`, lowercase из пересозданного эджа) против `2C37FED1-…` (UPPERCASE) в живом инстансе.
|
||||||
|
- Причина: регистр UUID нормализовался в 5 местах (отправка в API, одиночные значения, create-only сравнение,
|
||||||
|
state), но **внутри JSON** — нет; adopt приостановленного инстанса сравнивает параметр целиком как JSON.
|
||||||
|
- Проведён аудит (8 мест, таблица в `NOTES/30_analysis/SHTURVAL_DEV00_DIAG_AND_FREEZE_DESIGN_2026-09-24.md` §5.3).
|
||||||
|
- Фикс: `jsonutil.LowercaseUUIDsInText` + нормализация строк внутри JSON (закрывает adopt-suspended, modifier-compare,
|
||||||
|
state_refresh, диагностику), UUID-подстроки в `JsonNormalize()`; тесты в `jsonutil` и `resources_core`.
|
||||||
|
- Открыто: ref-параметр внутри JSON не валидируется при adopt; регистр ключей в `lookupLiveParam`.
|
||||||
|
|
||||||
|
## Проверка цикла на живом стенде
|
||||||
|
|
||||||
|
- `terraform destroy` (провайдер `2.0.22`): `0 added, 0 changed, 5 destroyed`, ошибок нет.
|
||||||
|
Кластер и vDC ушли в `suspend`, эдж остался `running` с `ipSpaceName=internet-ipv4-v1`, квота IP — `count=3`,
|
||||||
|
state пуст. Предупреждения: «заморожен, а не удалён» ×2 (кластер, vDC), «оставлен как есть» (эдж),
|
||||||
|
«Аллокация IP не снималась» (квота), «SNAT не выключался».
|
||||||
|
- Обратный ход (`apply` → adopt + `resume`) — следующий шаг, запускает пользователь.
|
||||||
|
|
||||||
|
Бэкап перед правкой: `TMP/backup_2026-09-24/shturval.tf.before-adopt`.
|
||||||
|
|
||||||
|
## Итоги диагностики кластера `shturval-dev-00`
|
||||||
|
|
||||||
|
- Кластер здоров: 2 ноды Ready (k8s v1.35.1, платформа 2.14.0), `shturvalserviceconfigs` 41/41 `ready`,
|
||||||
|
`nodeconfigitems` 4/4, endpoints есть у всех 35 сервисов.
|
||||||
|
- Единственный «мусор» — 4 подвисших пода `kube-system/shturval-init-job` (3 Error + 1 Unknown) при
|
||||||
|
`Complete 1/1` у Job. Причина: webhook-и Штурвала недоступны, пока Cilium не поднял сеть
|
||||||
|
(`connect: operation not permitted`). Самоочистка по `ttlSecondsAfterFinished: 86400` (~25.09 14:31 UTC).
|
||||||
|
- Счётчики ЛК расшифрованы: `Pods` = готовые/всего (без Completed), «Системные сервисы» = число сервисов в режиме
|
||||||
|
`auto` (17/24 во время установки → 24/24), «Ingress» — домен-шаблон, «Конфигурация узлов» — NodeConfigItems.
|
||||||
|
|
||||||
|
## Итоги разбора destroy
|
||||||
|
|
||||||
|
- `nubes_vc_org_ip_allocation` при `keep_on_destroy = false` отправляет `count=0` и падает, если квота занята
|
||||||
|
(2 адреса держит кластер: `.146` API, `.148` ingress; `suspend` их не освобождает).
|
||||||
|
- Упавший destroy оставляет «рваное» состояние: SNAT снят, edge/vDC/квота — нет.
|
||||||
|
- `adopt_existing_on_create = true` решает восстановление: apply усыновил инстанс `94627ff4-…` и сам сделал
|
||||||
|
`resume`; SNAT восстановлен (`internet-ipv4-v1`). Проверено на живом стенде.
|
||||||
|
|
||||||
|
## Принятое направление (дизайн)
|
||||||
|
|
||||||
|
Три режима destroy в одной общей логике: `delete` (дефолт), `suspend` (где сервис умеет),
|
||||||
|
`keep` → `state_only` (эдж, SNAT, квота IP). Реализация — через генератор
|
||||||
|
(`TOOLS/resource-generator`), без ручных правок `resources_gen/`. Дефолты провайдера остаются разрушающими,
|
||||||
|
freeze включается явно в `.tf` стенда; в `Delete` обязательны предупреждения («заморожено», «оставлено как есть»).
|
||||||
|
Полный teardown — только явный opt-out и в порядке: кластер → `count=0` → SNAT → эдж → vDC.
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# 2026-09-25 — Штурвал в примере `fullpipe_chain` + страница документации
|
||||||
|
|
||||||
|
## Что сделано
|
||||||
|
|
||||||
|
Примеры (`tf_examples`, отдельный репозиторий `https://gitea.services.ngcloud.ru/Nail/tf_examples.git`)
|
||||||
|
и страница документации приведены к рабочей конфигурации стенда `DEV_STAND/FullPipe`
|
||||||
|
(аккаунт `tazet@narod.ru`) — теперь цепочка полная: **vDC → Edge → внешние IP → SNAT → Штурвал**.
|
||||||
|
|
||||||
|
| Файл | Изменение |
|
||||||
|
|---|---|
|
||||||
|
| `tf_examples/fullpipe_chain/shturval.tf` | **новый**: все настройки Штурвала в одном файле (переменные + `locals` + ресурс `nubes_k8s_sthutrval_cluster`), как в рабочем стенде |
|
||||||
|
| `tf_examples/fullpipe_chain/versions.tf` | провайдер `2.0.21` → `2.0.23` (последняя dev) |
|
||||||
|
| `tf_examples/fullpipe_chain/edge.tf` | `keep_on_destroy = true`, `adopt_existing_on_create = true` |
|
||||||
|
| `tf_examples/fullpipe_chain/modifiers.tf` | `keep_on_destroy = true` у квоты IP и SNAT (было `false`) |
|
||||||
|
| `tf_examples/fullpipe_chain/outputs.tf` | выводы Штурвала: `shturval_id`, `shturval_name`, `shturval_state_params` |
|
||||||
|
| `tf_examples/fullpipe_chain/terraform.tfvars.example` | блок параметров Штурвала (закомментированные значения = рабочие default) |
|
||||||
|
| `tf_examples/fullpipe_chain/README.md`, `tf_examples/README.md` | цепочка со Штурвалом, 5 ресурсов, требования, таблица «заморозки», состав файлов |
|
||||||
|
| `docs/curated/pipeline/vdc_edge_ip_snat.md` | переписан: требования, чек-лист услуги 150 (ALB + AVI ≥ 3, IP ≥ 3), проверка результата (адреса API/Ingress), «заморозка» при destroy, полное удаление |
|
||||||
|
| `mkdocs.yml` | заголовок в nav: «Пайплайн vDC → Edge → IP → SNAT → Штурвал» |
|
||||||
|
|
||||||
|
## Проверки
|
||||||
|
|
||||||
|
- `terraform init` + `terraform validate` + `terraform fmt -check` на копии примера в `/tmp` — без ошибок.
|
||||||
|
- `terraform plan` (копия в `/tmp`, организация `kontra`, токен `secrets/dev.token`) — `5 to add, 0 change, 0 destroy`, ошибок нет.
|
||||||
|
- Копия для проверки делалась в `/tmp`, **не** в `tf_examples/`: там нет `.gitignore` для `.terraform/`, и служебные файлы уехали бы в публичный репозиторий.
|
||||||
|
|
||||||
|
## Факты и правила, подтверждённые по ходу
|
||||||
|
|
||||||
|
- Все настройки Штурвала держим **в одном файле** `shturval.tf` (переменные + ресурс): чтобы выключить Штурвал — удалить файл.
|
||||||
|
- Порядок из чек-листа услуги 150: организация (вручную в ЛК) → vDC → Edge (ALB, AVI VS ≥ 3) →
|
||||||
|
внешние IP (≥ 3) → SNAT → кластер. Минимум ноды: 1 + 1 по 4 vCPU / 8 ГБ / 50 ГБ.
|
||||||
|
- `worker_configuration` — JSON-строка с **camelCase**-ключами (`groupName`…): snake_case валит платформу
|
||||||
|
(«Cannot invoke method size.split() on null object»).
|
||||||
|
- «Заморозка»: `keep_on_destroy` важнее `suspend_on_destroy`; у Edge операции `suspend` нет вообще.
|
||||||
|
- Публикация доков: `TOOLS/scripts/04_build_and_publish_docs.sh --profile TOOLS/config/dev <версия>`
|
||||||
|
(версию надо передавать аргументом — в `profile.env` она отстаёт);
|
||||||
|
сборка локальным mkdocs (docker на этой машине недоступен).
|
||||||
|
|
||||||
|
## Ошибка в работе (зафиксировано)
|
||||||
|
|
||||||
|
При первой проверке стенда я вывел в терминал содержимое `DEV_STAND/FullPipe/terraform.tfvars` —
|
||||||
|
файл содержит живой `api_token`. В git файл не попадает (`*.tfvars` в `.gitignore`), но токен оказался
|
||||||
|
в логе вывода. Правило: секреты из `.tfvars` не печатать, сравнивать по хешу/маскировать.
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# VPN transit via VM 213 and Vultr
|
||||||
|
|
||||||
|
Date: 2026-09-27 to 2026-09-28
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Provide access from Russian residential/mobile networks to services restricted by Russian network filtering, while retaining the existing foreign egress on Vultr.
|
||||||
|
|
||||||
|
## Verified network facts
|
||||||
|
|
||||||
|
- Test host `3060`: `46.39.251.163`, connection from Khimki / Iskratelecom.
|
||||||
|
- Transit VM `213`: `5.172.178.213`, public egress observed as `5.172.178.65`; hosted in NUBES data centre.
|
||||||
|
- Vultr addresses: primary `95.179.252.111`; secondary `104.238.177.67`.
|
||||||
|
- `3060 -> 213`: ICMP approximately 3 ms, 0% loss.
|
||||||
|
- `213 -> Vultr`: ICMP approximately 34 ms, 0% loss; HTTPS response returned in about 0.07-0.11 s.
|
||||||
|
- Direct `213 -> Vultr` test file transfer: 10 MiB in 1.59 s, about 6.27 MiB/s / 50.2 Mbit/s.
|
||||||
|
- Direct `3060 -> Vultr` test file transfer timed out / was throttled.
|
||||||
|
- Direct `213 -> OVH proof endpoint`: 10 MiB in 1.18 s, about 8.5 MiB/s.
|
||||||
|
- Direct access from `213` to YouTube and Telegram failed with `HTTP=000` and timeout/SSL errors, while OVH and Google returned HTTP 200. Therefore a foreign egress remains required for those services.
|
||||||
|
|
||||||
|
## Persistent changes on VM 213
|
||||||
|
|
||||||
|
- Created backup:
|
||||||
|
- `/etc/nginx/sites-available/check.kube5s.ru.bak_vpn`
|
||||||
|
- Modified:
|
||||||
|
- `/etc/nginx/sites-available/check.kube5s.ru`
|
||||||
|
- Added an Nginx `/ws` reverse-proxy location with:
|
||||||
|
- upstream `https://95.179.252.111:443`
|
||||||
|
- SNI `vipien.kube5s.ru`
|
||||||
|
- upstream Host header `vipien.kube5s.ru`
|
||||||
|
- WebSocket upgrade headers
|
||||||
|
- 3600-second proxy timeouts
|
||||||
|
- Ran `nginx -t` successfully and reloaded Nginx.
|
||||||
|
- Existing unrelated Nginx warnings about duplicate `contracts.kube5s.ru` server names remained.
|
||||||
|
|
||||||
|
## Persistent/previously existing changes on Vultr
|
||||||
|
|
||||||
|
The following configuration was read or used during validation:
|
||||||
|
|
||||||
|
- `/etc/nginx/conf.d/vipien.conf`: TLS/WebSocket endpoint for `vipien.kube5s.ru`.
|
||||||
|
- `/etc/v2ray-agent/xray/conf/08_VLESS_ws_inbound.json`: VLESS WebSocket inbound on `127.0.0.1:10086`, path `/ws`.
|
||||||
|
- `/etc/systemd/system/hysteria-server.service`: Hysteria service was stopped and disabled; it was not changed in this work.
|
||||||
|
- Xray service was confirmed active.
|
||||||
|
- Nginx service was confirmed active.
|
||||||
|
- Cloudflared tunnel configuration was inspected earlier, but it is not used by the final working route.
|
||||||
|
- A temporary 10 MiB test file was created on Vultr and removed after testing.
|
||||||
|
|
||||||
|
## Temporary files on test VM 3060
|
||||||
|
|
||||||
|
The following temporary client files were created under `/tmp/xray-test/` for validation and are not repository files:
|
||||||
|
|
||||||
|
- `client-cf.json`
|
||||||
|
- `client-213.json`
|
||||||
|
- `client-directip.json`
|
||||||
|
- temporary log/test artifacts where applicable
|
||||||
|
|
||||||
|
The files contained test Xray client configurations. They were used only to verify the route from `3060`; no permanent system service was installed there.
|
||||||
|
|
||||||
|
## Final tested route
|
||||||
|
|
||||||
|
`client in Russia -> 5.172.178.213:443 -> Nginx WebSocket proxy -> 95.179.252.111:443 -> Xray -> Internet`
|
||||||
|
|
||||||
|
Final test from `3060` through the route:
|
||||||
|
|
||||||
|
- observed outbound IP: `95.179.252.111`
|
||||||
|
- 10 MiB OVH download: 1.76-1.91 s
|
||||||
|
- measured speed: approximately 5.5-6.0 MiB/s
|
||||||
|
|
||||||
|
## Final client parameters
|
||||||
|
|
||||||
|
- Address: `5.172.178.213`
|
||||||
|
- Port: `443`
|
||||||
|
- UUID: existing UUID used by the Vultr Xray inbound
|
||||||
|
- TLS SNI: `check.kube5s.ru`
|
||||||
|
- WebSocket path: `/ws`
|
||||||
|
- WebSocket Host: `vipien.kube5s.ru`
|
||||||
|
|
||||||
|
The final direct-IP test used Xray 26.3.27. The client-side `allowInsecure` option was not used because this Xray version reports that the option was removed.
|
||||||
|
|
||||||
|
## Secondary Vultr IP
|
||||||
|
|
||||||
|
Before removal, the Nginx upstream on VM 213 was switched from `104.238.177.67` to `95.179.252.111`. A post-switch end-to-end test succeeded, with outbound IP `95.179.252.111` and approximately 6.0 MiB/s.
|
||||||
|
|
||||||
|
No Vultr IP deletion was performed in this work. The secondary address was only confirmed as no longer referenced by the transit configuration.
|
||||||
|
|
||||||
|
## Scope audit
|
||||||
|
|
||||||
|
- No repository source/configuration files were edited before this record.
|
||||||
|
- `git status` was clean before this documentation file was created.
|
||||||
|
- This documentation file is the only workspace file created by the current documentation action.
|
||||||
|
- Server-side files were changed on VM 213 and earlier on Vultr; temporary test files were also created on VM 3060.
|
||||||
|
- No commit was created for this record.
|
||||||
|
|
||||||
|
## Important limitations
|
||||||
|
|
||||||
|
The measurements prove the route worked at test time. They do not guarantee permanent availability: NUBES, Vultr, upstream providers, or network filtering policy can change independently.
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# ПЛАН: живой прогон цепочки на DEV_STAND/FullPipe (2026-09-24)
|
||||||
|
|
||||||
|
> Стенд: dev, орга **`organ`** (`57eeacd1-dc7f-4a52-b903-7e5f7d3c1164`, realm `sandbox.nubes.ru`, тип `saas`,
|
||||||
|
> CD-имя `WZ01325-saas`). Провайдер `2.0.19` (`terraform init -upgrade` уже сделан, `validate` — Success).
|
||||||
|
> **`apply`/`destroy` запускает только пользователь.**
|
||||||
|
|
||||||
|
## 0. Что уже готово
|
||||||
|
|
||||||
|
- Ресурсы `nubes_vc_org_ip_allocation` (modify `vIPConfigure`) и `nubes_vc_nsxt_snat` (modify `ipSpaceName`) —
|
||||||
|
в провайдере, собраны в `2.0.19`, залиты в `nubes-dev`, есть unit-тесты канонизации.
|
||||||
|
- Конфиг стенда: `DEV_STAND/FullPipe/` — `vdc.tf`, `edge.tf`, `modifiers.tf` (аллокация после эджа, затем SNAT),
|
||||||
|
`organization = "organ"` + `org_uid`.
|
||||||
|
- Орга создана вручную (в tf её нет) — по решению пользователя.
|
||||||
|
|
||||||
|
## 1. Цель прогона
|
||||||
|
|
||||||
|
Проверить **одним `apply`**: `vdc → edge → IP на орге → SNAT`, затем чистый повторный `plan` и корректный
|
||||||
|
`destroy`. Это первый живой прогон обоих новых ресурсов: CRUD до сих пор не проверялся.
|
||||||
|
|
||||||
|
## 2. Перед прогоном (проверить значения)
|
||||||
|
|
||||||
|
1. `vdc_network_provider` (`snb1`), `vdc_provider_vdc` (`Intel Broadwell 2.4`), `vdc_storage_config` (`SATA`) —
|
||||||
|
убедиться в ЛК, что доступны для орги `organ` (значения брались из ЛК для прежней орги).
|
||||||
|
2. `ip_space_name` — сначала может быть недоступен: **список ipSpace в ЛК падает** (`Can't cast Complex Object
|
||||||
|
Type Struct to String`), пока нет vDC/эджа. Брать имя из прежних HAR: `internet-ipv4-v1`.
|
||||||
|
3. `ip_count` — `"3"` (строка).
|
||||||
|
|
||||||
|
## 3. Шаги прогона (пользователь)
|
||||||
|
|
||||||
|
| # | Команда | Ожидаемый результат |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | `terraform plan` | создание: `nubes_vc_vdc.vdc` → `nubes_vc_nsxt.edge` → `nubes_vc_org_ip_allocation.org_ip` → `nubes_vc_nsxt_snat.snat`; порядка не меньше |
|
||||||
|
| 2 | `terraform apply` | всё создаётся за один проход |
|
||||||
|
| 3 | `terraform plan` (повторно) | **пустой** — главный тест канонизации (иначе вечный diff) |
|
||||||
|
| 4 | проверить API (см. §4) | `vIPConfigure` и `ipSpaceName` в live-состоянии |
|
||||||
|
| 5 | изменить `ip_count` 3 → 2, `plan`+`apply` | меняется только аллокация, state сходится |
|
||||||
|
| 6 | `terraform destroy` | порядок `snat (no-needed)` → `org_ip (count=0)` → `edge` → `vdc`; орги не касается |
|
||||||
|
|
||||||
|
## 4. Что проверять и чем
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TOK=$(tr -d '\n' < secrets/narodDEV.token) # токен орги organ
|
||||||
|
# состояние орги
|
||||||
|
curl -s -H "Authorization: Bearer $TOK" 'https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc/instances/<org_uid>'
|
||||||
|
# состояние эджа
|
||||||
|
curl -s -H "Authorization: Bearer $TOK" 'https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc/instances/<nsxt_uid>'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Гипотезы, которые прогон подтверждает/опровергает:**
|
||||||
|
|
||||||
|
1. **Имена live-ключей**: `state.params.vIPConfigure` (орга) и `state.params.ipSpaceName` (эдж) — взяты из HAR,
|
||||||
|
кодом не проверены. Если Read вернёт не то → увидим дрейф/пустое значение.
|
||||||
|
2. **Частичный payload не затирает остальное**: SNAT-модификация шлёт только `372`; `needEnableAVI`
|
||||||
|
и `virtualServicesCount` должны остаться прежними (`true` / `1`), т.к. досылаются из live
|
||||||
|
(`core/operation_run_bycode.go`). Проверить в состоянии эджа до/после.
|
||||||
|
3. **Один `apply`** проходит целиком без второго прогона (ради этого и делались ресурсы).
|
||||||
|
4. **Нет вечного diff** после apply (канонизация `vip_configure`).
|
||||||
|
5. **`Required` + пустое live** не даёт ошибок (лечение из ревью).
|
||||||
|
|
||||||
|
## 5. Точки отказа и что делать
|
||||||
|
|
||||||
|
| Симптом | Вероятная причина | Действие |
|
||||||
|
|---|---|---|
|
||||||
|
| аллокация падает `Can't cast ... Struct to String` | платформа ещё не видит `job.vcd.networkProvider`/`providerGateway` (эдж/VDC не в состоянии) | проверить порядок и фактическое состояние эджа; при необходимости — пауза/повторный `apply` |
|
||||||
|
| `Provider produced inconsistent result after apply` на `vdc`/`edge` | read-back перекрыл план (известный класс дефектов) | записать в NOTES, разбирать отдельно (это уже не про наши ресурсы) |
|
||||||
|
| повторный `plan` не пустой | порядок ключей/формат не сошлись | сверить, что вернул live, с `formatVipConfigure` |
|
||||||
|
| SNAT не включился | `372` не доехал / неверное имя ipSpace | проверить `state.params.ipSpaceName` эджа и лог операции |
|
||||||
|
| `destroy` падает | обратный modify на живой/мёртвый родитель | смотреть тексты диагностик ресурсов (мы развели: ошибка API ≠ «родителя нет») |
|
||||||
|
|
||||||
|
## 6. После прогона
|
||||||
|
|
||||||
|
1. Отчёт в `NOTES/30_analysis/` — что прошло, что упало, с HAR/логами.
|
||||||
|
2. Обновить память репозитория (подтверждённые факты вместо гипотез).
|
||||||
|
3. Если найдутся баги — отдельные коммиты + при необходимости новый релиз провайдера.
|
||||||
|
4. Публикация документации (`04_build_and_publish_docs.sh`) — отдельной командой.
|
||||||
|
|
||||||
|
**Не входит в этот прогон:** кластер Штурвал (`nubes_k8s_shturval_cluster`) — отдельным шагом, после того как
|
||||||
|
SNAT подтверждён.
|
||||||
@@ -808,14 +808,24 @@ func (m jsonNormalizePlanModifier) PlanModifyString(_ context.Context, req planm
|
|||||||
(только destroy) — **задокументировать** в описании атрибута.
|
(только destroy) — **задокументировать** в описании атрибута.
|
||||||
- Раздел 3 (риски живой платформы) без прогона не закрывается — остаётся открытым.
|
- Раздел 3 (риски живой платформы) без прогона не закрывается — остаётся открытым.
|
||||||
|
|
||||||
## Требуется сделать (по итогам ревью) — ВЫПОЛНЕНО (коммиты `ba6c4f5`, `4b497e6`, `1236c59`)
|
## Итог по ревью: что сделано и где ревью ошиблось
|
||||||
|
|
||||||
1. ✅ Заменить `JsonNormalize()` на канонизирующий plan-modifier (`parse → formatVipConfigure`) —
|
**⚠️ Совет Opus (вариант «б», канонизация в plan-modifier) — НЕВЕРЕН.** Plan-modifier не имеет права
|
||||||
закрывает баг порядка ключей (в т.ч. для `jsonencode`).
|
менять значение пользовательского атрибута: Terraform отвечает
|
||||||
2. ✅ Убрать запись `null` в `Required`-атрибуты (`vip_configure`, `ip_space_name`) — при пустом live
|
`Provider produced invalid plan: planned value does not match config value`.
|
||||||
сохраняется текущее значение state (проверка «что отправили — то и в state»).
|
Это правило описано в нашем же сгенерированном коде (`22_vc_nsxt_resource.go`, комментарий в `ModifyPlan`).
|
||||||
3. ✅ `Delete`: ошибки API → `AddError`; warning оставлен только для отсутствующего родителя.
|
Проверено живым `terraform plan` 2026-09-24 (ошибка воспроизведена).
|
||||||
4. ✅ `setSnat`: вместо тихой подмены — валидация пустой строки.
|
|
||||||
|
Правильное решение (коммит `807dfde`):
|
||||||
|
- plan-modifier удалён полностью (`JsonNormalize` тоже снят — он компактит, то есть тоже менял бы значение);
|
||||||
|
- в `Read` — смысловое сравнение `vipAllocationsEqual`: если смысл совпал (порядок ключей/формат не важны),
|
||||||
|
значение пользователя НЕ переписывается; пишется только реальный дрейф.
|
||||||
|
|
||||||
|
**Выполнено корректно:**
|
||||||
|
1. ✅ Убран plan-modifier, менявший пользовательское значение; сравнение — смысловое (коммит `807dfde`).
|
||||||
|
2. ✅ `null` в `Required`-атрибуты не пишется — при пустом live сохраняется текущее значение state.
|
||||||
|
3. ✅ `Delete`: ошибки API → `AddError`; warning только для отсутствующего родителя.
|
||||||
|
4. ✅ `setSnat`: валидация пустой строки вместо тихой подмены на `no-needed`.
|
||||||
5. ✅ Задокументировано: «снять всё» через `vip_configure` нельзя, только `destroy`.
|
5. ✅ Задокументировано: «снять всё» через `vip_configure` нельзя, только `destroy`.
|
||||||
6. ✅ Поправлены/добавлены тесты канонизации (`jsonencode`-форма, пробелы, `[{}]`, невалидный JSON).
|
6. ✅ Тесты: смысловое сравнение (порядок ключей, разный count/имя, пустая аллокация).
|
||||||
7. ⏳ Новый релиз провайдера (2.0.19) с повторной заливкой в `nubes-dev`.
|
7. ⚠️ Релиз `2.0.19` залит, но **содержит сломанный plan-modifier** — для работы из реестра нужен `2.0.20`.
|
||||||
|
|||||||
@@ -0,0 +1,307 @@
|
|||||||
|
# Штурвал dev-00: диагностика, destroy-засада с квотой IP и дизайн «freeze on destroy» (2026-09-24)
|
||||||
|
|
||||||
|
> Источники: `kubectl` из локали (контекст `tazet@narod.ru@shturval-dev-00`), API ЛК dev
|
||||||
|
> (`https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc`, токен `secrets/narodDEV.token`), код провайдера
|
||||||
|
> (`provider/`), генератор (`TOOLS/resource-generator/`), конфиг стенда `DEV_STAND/FullPipe/`.
|
||||||
|
> Все выводы — только из этих источников; где не проверено, отмечено «не проверено».
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Стенд и кластер
|
||||||
|
|
||||||
|
- Услуга **150 «Kubernetes кластер Штурвал»**, инстанс `shturval-dev`, uid `94627ff4-33a5-48f2-aca1-695741e0b6a2`.
|
||||||
|
Создан 24.09.2026 17:18:01, операция `create` завершена 17:36:58 (`isSuccessful=true`, `errorLog=null`).
|
||||||
|
- `state.out`: `webUrl=https://k8s.ngcloud.ru/clusters/shturval-dev-00/dashboard`,
|
||||||
|
`kubernetesApiAddress=185.247.187.146`, `ingressAddress=185.247.187.148`.
|
||||||
|
- Параметры: `clusterName=shturval-dev-00`, `vdcUid=d0937335-…` (`fullpipe-vdc`),
|
||||||
|
`nsxtUid=2C37FED1-E8F8-4A84-8434-7851C7C8B5D6` (эдж `fullpipe-edge`), `appVersion=2.14.0`,
|
||||||
|
`exLogging/exMonitoring/exLocalCsi/exVip/exUpdate/exIngress/exNamedCsi = true`, CP 1× `TKG 4CPU 8RAM` / 50 ГБ,
|
||||||
|
workers 1× `TKG 4CPU 8RAM` / 50 ГБ (`workers-shturval-dev`, labelDeck=true).
|
||||||
|
- kubeconfig: сервер `https://185.247.187.146:6443`; client v1.34.1, server v1.35.1; узлы 2× Ready
|
||||||
|
(control-plane + worker), Ubuntu 24.04.5, containerd 2.2.1.
|
||||||
|
- Организация `organ` (uid `57eeacd1-dc7f-4a52-b903-7e5f7d3c1164`, CD-имя `WZ01325-saas`, realm `sandbox.nubes.ru`).
|
||||||
|
|
||||||
|
### Состояние кластера (снимок 17:52 MSK)
|
||||||
|
|
||||||
|
- Подов 49 (готовых 44). Не-Running остались только подвисшие поды установщика:
|
||||||
|
`shturval-init-job-489mk`, `-98n4k`, `-vkwk7` (Error), `-l8457` (Unknown); рядом `-9587k` (Completed).
|
||||||
|
- Job `kube-system/shturval-init-job`: label `shturval.tech/init`, **без ownerReferences**,
|
||||||
|
`backoffLimit: 10`, `ttlSecondsAfterFinished: 86400`, nodeSelector `control-plane`,
|
||||||
|
образ `r.shturval.tech/shturval-install:2.14.0`, `/scripts/run.sh`. Итог: `failed: 4`, `succeeded: 1`,
|
||||||
|
завершён 14:31:46 UTC (17:31 MSK) → поды удалятся сами ~25.09 14:31 UTC.
|
||||||
|
- Причина падений (лог пода): `UPGRADE FAILED: failed to create resource: conversion webhook for
|
||||||
|
ops.shturval.tech/v1beta1, Kind=ShturvalRepoConfig failed: Post
|
||||||
|
"https://shturval-services-webhook-service.shturval-services-system.svc:443/convert?timeout=30s":
|
||||||
|
dial tcp 10.97.147.91:443: connect: operation not permitted`. То же в логе оператора:
|
||||||
|
`failed calling webhook "vshturvalupdate.kb.io" … connect: operation not permitted` — до готовности Cilium
|
||||||
|
webhook-и недоступны. Следующая попытка прошла (релиз `shturval-services.v3`), всё поднялось.
|
||||||
|
- Остальное зелёное: `shturvalserviceconfigs` 41/41 `ready=true` (24 в режиме `auto`, 17 в `absent`),
|
||||||
|
`nodeconfigitems` 4/4 `ready=true`, `nodeconfigs` 2/2, у всех 35 сервисов есть endpoints,
|
||||||
|
IngressClass `nginx` 1, ingress-controller 1/1.
|
||||||
|
|
||||||
|
### UI-счётчики (расшифровка)
|
||||||
|
|
||||||
|
| Колонка | Что это на самом деле |
|
||||||
|
|---|---|
|
||||||
|
| `Pods 44/48` | готовые/всего поды (48 = все поды минус `Completed`) |
|
||||||
|
| «Системные сервисы» | число `ShturvalServiceConfig` в режиме `auto`: 17/24 во время установки → 24/24 |
|
||||||
|
| «Конфигурация узлов» 4/4 | `nodeconfigitems.node.shturval.tech` `ready=true` |
|
||||||
|
| «Ingress» | домен `*.shturval-dev-00.ip-185-247-187-148.shturval.link`, **не** счётчик |
|
||||||
|
| ⚠️ на «Pods» | ровно 4 подвисших пода `shturval-init-job` |
|
||||||
|
|
||||||
|
Статус «Работает с ошибками» в первом снимке (31/53 подов) был снят во время установки; после догрузки — «Работает».
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Что DevOps может сделать с мусором init-job
|
||||||
|
|
||||||
|
Ответ на вопрос «что выставить в настройках деплоя Штурвала»:
|
||||||
|
|
||||||
|
- В услуге 150 и в конфиге стенда таких ручек **нет** (есть только `vdc_uid`/`nsxt_uid`, `cluster_name`,
|
||||||
|
галочки `ex_*`, sizing/count, внешние адреса). `backoffLimit` и `ttlSecondsAfterFinished` зашиты
|
||||||
|
в манифест установщика платформы.
|
||||||
|
- Поэтому вариантов два: подождать самоочистку по `ttlSecondsAfterFinished: 86400`, либо тикет в команду
|
||||||
|
Штурвала: уменьшить TTL и/или не запускать установку компонентов до готовности Cilium
|
||||||
|
(иначе снова `operation not permitted` на webhook-ах).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Destroy стенда и засада с квотой IP
|
||||||
|
|
||||||
|
Порядок destroy: `nubes_vc_nsxt_snat.snat` → `nubes_vc_org_ip_allocation.org_ip` → `nubes_vc_nsxt.edge` → `nubes_vc_vdc.vdc`.
|
||||||
|
|
||||||
|
- `snat` удалился успешно (2m27s), отправив modify `ipSpaceName = "no-needed"` (warning «SNAT выключен»).
|
||||||
|
- `org_ip_allocation` упал:
|
||||||
|
`Error: Ошибка клиента — операция D9FB606D-C86E-4D30-A58D-44282C4508AE завершилась с ошибкой:
|
||||||
|
Кол-во зантяых Ip в тенанте 'WZ01325-saas': 2. Невозможно выставить параметр count ниже этого параметра`.
|
||||||
|
- Причина в коде: `Delete` аллокации при `keep_on_destroy = false` отправляет обратный modify
|
||||||
|
`vIPConfigure = [{"name":"internet-ipv4-v1","count":"0"}]`
|
||||||
|
(`provider/internal/resources_core/org_ip_allocation_resource.go:258-274`); при `keep_on_destroy = true`
|
||||||
|
ничего не отправляется (строки 212-215). В стенде сейчас `keep_on_destroy = false`
|
||||||
|
(`DEV_STAND/FullPipe/modifiers.tf:39` для квоты, `:29` для SNAT).
|
||||||
|
- Кто держит 2 адреса: инстанс кластера Штурвала — `.146` (API) и `.148` (ingress). `suspend` адреса
|
||||||
|
**не** освобождает (suspend выполнен 17:59:59 MSK успешно, `isDeleted=false`, `uptime=0`, адреса в `state.out` остались).
|
||||||
|
- Последствие упавшего destroy: прерван, до edge/vDC дело не дошло; в state остались `vdc`, `edge`,
|
||||||
|
`org_ip_allocation`, а `snat` уже удалён — «рваное» состояние.
|
||||||
|
|
||||||
|
### Метаданные платформы (проверено через API ЛК)
|
||||||
|
|
||||||
|
- Обязательные заголовки: `Authorization: Bearer <secrets/narodDEV.token>`,
|
||||||
|
браузерный `User-Agent`, `Referer: https://deck-dev.ngcloud.ru/` — без них DDoS-Guard отдаёт `403 Forbidden`.
|
||||||
|
- Эндпоинты: `GET /instances?page=1&size=200`, `GET /instances/{uid}`; параметры — в
|
||||||
|
`instance.state.params` (верхнеуровневый `instance.params = null`), статус — `explainedStatus`.
|
||||||
|
- `availableOperations`: кластер — `delete, modify, suspend, resume, reconcile, create_user, delete_user`;
|
||||||
|
vDC — `delete, modify, suspend, resume, reconcile`; **эдж — `delete, modify, reconcile` (suspend отсутствует)**.
|
||||||
|
- `dependencies`/`dependentInstances`: у кластера и эджа пусто; у `fullpipe-vdc` в зависимых три инстанса
|
||||||
|
`fullpipe-edge` (`b289beb8…`, `86a01033…`, `2c37fed1…`) — рабочий только `2c37fed1…`, два других сироты
|
||||||
|
от прошлых прогонов. Кластера в зависимых нет → платформа не блокирует удаление эджа/квоты при живом кластере.
|
||||||
|
- vDC (услуга 21) по инструкции удаляется только через 14 дней после `suspend`; при живых Edge/vApp/VM/кластере
|
||||||
|
удаление — через поддержку.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. `adopt_existing_on_create` — как усыновление реально работает
|
||||||
|
|
||||||
|
- Кластера не было в state, а ресурс есть в `shturval.tf` → план показывал `will be created`. Это **не**
|
||||||
|
доказательство отсутствия adopt: проверка/adopt выполняются в `Create` на apply
|
||||||
|
(`provider/internal/resources_gen/150_k8s_sthutrval_cluster_resource.go:211`).
|
||||||
|
- Дефолт adopt — `false` (там же, строка 136). При существующем инстансе:
|
||||||
|
`running` либо `suspended` без adopt → hard error «РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ (…RUNNING/SUSPEND)»
|
||||||
|
(`provider/internal/resources_core/resource_diagnostics_required.go:241-259`). Дубль при этом не создаётся.
|
||||||
|
- Исправление: добавлен `adopt_existing_on_create = true` в `DEV_STAND/FullPipe/shturval.tf`
|
||||||
|
(коммит `57abb7b`; бэкап `TMP/backup_2026-09-24/shturval.tf.before-adopt`).
|
||||||
|
- Результат apply (проверено): state получил `id=94627ff4-…`; провайдер сам выполнил `resume`
|
||||||
|
(18:26:30, success) — инстанс `running`, `isSuspended=false`; кластер жив (2 ноды Ready);
|
||||||
|
`nubes_vc_nsxt_snat.snat` в state (`internet-ipv4-v1`), live эдж `ipSpaceName=internet-ipv4-v1`
|
||||||
|
(modify 18:23:12, success). План после apply: действий по ресурсам нет, только дрейф state
|
||||||
|
(`edge.state_params.ipSpaceName`: `no-needed` → `internet-ipv4-v1`) и `Changes to Outputs`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Дизайн «freeze on destroy» (решение)
|
||||||
|
|
||||||
|
**Требование заказчика:** пользователь стенда не должен ничего делать руками и не должен звать DevOps.
|
||||||
|
`destroy` не удаляет, а «замораживает»: кластер → `suspend`, vDC → `suspend`, эдж → оставить как есть,
|
||||||
|
SNAT → не выключать, квота IP → не трогать. Следующий `apply` возвращает всё в работу.
|
||||||
|
|
||||||
|
**Что уже есть в ядре:**
|
||||||
|
|
||||||
|
- `resources_core/crud.go:64-96` — `DeleteResourceWithTimeout(..., destroyBehavior, ...)`, режимы
|
||||||
|
`state_only`/`detach` (ничего не делаем, ресурс забывается) и `suspend` (шлём операцию `suspend`).
|
||||||
|
- `crud.go:162-214` — adopt на create: для `StateSuspended` при `resumeIfExists` шлёт `resume` и ждёт готовности.
|
||||||
|
- `nubes_k8s_sthutrval_cluster` и `nubes_vc_vdc` — `suspend_on_destroy` (default `true`) + `adopt_existing_on_create`.
|
||||||
|
- `nubes_vc_nsxt_snat` и `nubes_vc_org_ip_allocation` — `keep_on_destroy` (в стенде `false`).
|
||||||
|
- `nubes_vc_nsxt` (эдж) — только `adopt_existing_on_create`; в `provider/resources_yaml/22_vc_nsxt.yaml`
|
||||||
|
нет операции `suspend` → генератор ставит `deleteMode := "delete"`
|
||||||
|
(`TOOLS/resource-generator/internal/templates/instance.go:546-552`), т.е. эдж удаляется по-настоящему.
|
||||||
|
|
||||||
|
**Решение:** три режима в одной общей логике — `delete` (дефолт), `suspend` (где сервис умеет),
|
||||||
|
`keep` → `state_only` (эдж, SNAT, IP-квота). Дефолты провайдера остаются разрушающими; freeze включается
|
||||||
|
явно в `.tf` стенда. Обязательны предупреждения в выводе destroy («заморожено (suspend)», «оставлен как есть:
|
||||||
|
эдж», «квота IP не изменена») — иначе freeze выглядит как успешное удаление.
|
||||||
|
|
||||||
|
**Реализация — только через генератор (ручные правки `resources_gen/` затрёт регенерация):**
|
||||||
|
|
||||||
|
1. `TOOLS/resource-generator/internal/types/types.go` — в `LifecycleSpec` добавить
|
||||||
|
`KeepOnDestroyDefault *bool \`yaml:"keep_on_destroy_default"\``, в `GenResource` — `KeepOnDestroy bool`.
|
||||||
|
2. `TOOLS/resource-generator/internal/loader/loader.go` — читать новый ключ (дефолт `false`),
|
||||||
|
как сейчас читается `suspend_on_destroy_default` (строки ~114-118).
|
||||||
|
3. `TOOLS/resource-generator/internal/templates/instance.go` — эмитить атрибут `keep_on_destroy`
|
||||||
|
(Optional+Computed, дефолт из YAML) в schema и модель; в `Delete` собирать режим:
|
||||||
|
`suspend` → `state_only` (keep) → `delete`.
|
||||||
|
4. `provider/resources_yaml/22_vc_nsxt.yaml` — `keep_on_destroy_default: false`.
|
||||||
|
5. Регенерация + проверка воспроизводимости (`TOOLS/scripts/10_yaml_stability_run.sh`) → сборка/релиз.
|
||||||
|
|
||||||
|
`resources_core`-ресурсы (SNAT, квота IP) менять не нужно — флаг там уже есть.
|
||||||
|
|
||||||
|
**Конфиг стенда для freeze:**
|
||||||
|
|
||||||
|
| Файл / ресурс | Сейчас | Надо |
|
||||||
|
|---|---|---|
|
||||||
|
| `modifiers.tf` → `nubes_vc_org_ip_allocation.org_ip` | `keep_on_destroy = false` (:39) | `true` |
|
||||||
|
| `modifiers.tf` → `nubes_vc_nsxt_snat.snat` | `keep_on_destroy = false` (:29) | `true` |
|
||||||
|
| `edge.tf` → `nubes_vc_nsxt.edge` | атрибутов нет | `keep_on_destroy = true` + `adopt_existing_on_create = true` |
|
||||||
|
| `shturval.tf` → `nubes_k8s_sthutrval_cluster.shturval` | `adopt=true`, `suspend_on_destroy` дефолт | `adopt=true` (есть) + `suspend_on_destroy = true` явно |
|
||||||
|
| `vdc.tf` → `nubes_vc_vdc.vdc` | `suspend_on_destroy = true`, `adopt = true` (:17,19) | без изменений |
|
||||||
|
|
||||||
|
**Что будет при destroy в режиме freeze:** из state ресурсы уйдут, но в облаке ничего не изменится —
|
||||||
|
SNAT останется включённым (Delete при `keep=true` печатает «SNAT не выключался», `nsxt_snat_resource.go:185-190`),
|
||||||
|
квота IP — `count=3`, эдж — running, vDC и кластер — suspended. При следующем `apply` ресурсы создадутся заново
|
||||||
|
и усыновят живые объекты (`suspend` → `resume`, `running` → просто UID), SNAT/квота отправят те же значения → no-op.
|
||||||
|
|
||||||
|
**Полный teardown** — только явный opt-out (`keep_on_destroy=false` / `suspend_on_destroy=false`) и в порядке:
|
||||||
|
кластер → `count=0` → SNAT → эдж → vDC. Иначе `count` ниже занятых не опустить, а удаление эджа оставит кластер
|
||||||
|
без внешнего API/ingress.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5.1. Реализовано (вечер 24.09)
|
||||||
|
|
||||||
|
**Генератор (коммит `22c6c83`):**
|
||||||
|
|
||||||
|
- `TOOLS/resource-generator/internal/types/types.go` — в `ServiceSpec.Lifecycle` добавлен `KeepOnDestroyDefault *bool`
|
||||||
|
(`yaml:"keep_on_destroy_default"`), в `GenResource` — `KeepOnDestroy bool`.
|
||||||
|
- `TOOLS/resource-generator/internal/loader/loader.go` — читает ключ из YAML (дефолт `false`) и передаёт в генератор.
|
||||||
|
- `TOOLS/resource-generator/internal/templates/instance.go` — атрибут `keep_on_destroy` (Optional+Computed, дефолт из YAML)
|
||||||
|
во всех instance-ресурсах; в `Delete` режим выбирается так: `keep_on_destroy` → `state_only` (приоритет),
|
||||||
|
иначе `suspend_on_destroy` (где сервис умеет) → `suspend`, иначе `delete`; после успешного удаления печатаются
|
||||||
|
предупреждения «Ресурс заморожен, а не удалён» / «Ресурс оставлен как есть, а не удалён».
|
||||||
|
- Флаг получили **все 40 instance-ресурсов** (проверено: `grep -l keep_on_destroy generated/dev/go/*_resource.go`).
|
||||||
|
Subresource-ресурсы (пользователи/БД/бэкапы) — без него: другой шаблон, у них нет своего suspend.
|
||||||
|
|
||||||
|
**YAML-спеки не правим:** `01_generate_yamls.sh` перезаписывает `generated/<stand>/resources_yaml/*.yaml` из API,
|
||||||
|
поэтому ручной ключ там не живёт. Ключ `keep_on_destroy_default` поддержан, но не используется:
|
||||||
|
дефолт `false` берётся из нулевого значения Go.
|
||||||
|
|
||||||
|
**Проверка:**
|
||||||
|
|
||||||
|
- `02_generate_resources_and_docs_v2.sh --profile TOOLS/config/dev` → `dev-materialize.sh dev` →
|
||||||
|
`go build ./...` в `provider/` — OK, `go test ./...` — OK.
|
||||||
|
- Локальная проверка конфига без релиза: собран свой бинарь в `TMP/devbin/`, `dev_overrides` —
|
||||||
|
`TMP/terraformrc.dev`; `TF_CLI_CONFIG_FILE=TMP/terraformrc.dev terraform validate` — Success,
|
||||||
|
`terraform plan` — `0 to add, 5 to change, 0 to destroy`, у ресурсов меняется только новый
|
||||||
|
атрибут (`keep_on_destroy = false -> true` у квоты, `+ keep_on_destroy = false` у vDC/кластера/эджа/SNAT) плюс
|
||||||
|
пересчёт outputs.
|
||||||
|
|
||||||
|
**Конфиг стенда (коммит `40aef87`):** `modifiers.tf` — `keep_on_destroy = true` у квоты IP (`:31`) и SNAT (`:42`);
|
||||||
|
`edge.tf` — `keep_on_destroy = true` (`:23`) + `adopt_existing_on_create = true` (`:27`);
|
||||||
|
`shturval.tf` — явные `adopt_existing_on_create = true` (`:113`) и `suspend_on_destroy = true` (`:117`);
|
||||||
|
у vDC в `vdc.tf:17,19` оба флага уже были.
|
||||||
|
|
||||||
|
**Релиз выполнен:** `03_build_and_upload_provider.sh --profile TOOLS/config/dev 2.0.22` → три платформы (linux/darwin/windows amd64) + `SHA256SUMS`/`.sig` залиты, версия видна в реестре (проверено `GET /v1/providers/nubes-dev/nubes/versions` → `2.0.22`); `VERSIONS.md` обновлён (коммит `c29df21`).
|
||||||
|
|
||||||
|
**Что осталось:** проверить цикл на живом стенде: `destroy` = заморозка (кластер/vDC → suspend, эдж/SNAT/квота IP → state_only с предупреждениями) и `apply` = разморозка (adopt + resume). `apply`/`destroy` запускает только пользователь.
|
||||||
|
|
||||||
|
**Состояние на 19:4x:** пин в `DEV_STAND/FullPipe/versions.tf` поднят до `2.0.22`, `terraform plan` → «No changes» (дрейф по `edge.state_params.ipSpaceName` ушёл после apply SNAT). Флаги в state: кластер — `adopt=true`, `suspend_on_destroy=true`, `keep=false`; vDC — то же; эдж — `keep=true`, `adopt=true`; SNAT — `keep=true`; квота IP — `keep=true`. Кластер жив: 2 ноды Ready, подов 49 (готовых 44 — те же 4 мусорных пода init-job).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5.2. Проверено вживую: `destroy` = заморозка (24.09, вечер)
|
||||||
|
|
||||||
|
`terraform destroy` на `DEV_STAND/FullPipe` (провайдер `2.0.22`) — «Apply complete! Resources: 0 added, 0 changed, 5 destroyed», ошибок нет. Предупреждения вывода:
|
||||||
|
|
||||||
|
- `SNAT не выключался` — `keep_on_destroy = true`: `ipSpaceName` шлюза оставлен без изменений (NSXT-логика, `nsxt_snat_resource.go`);
|
||||||
|
- `Ресурс оставлен как есть, а не удалён` — эдж (`vc_nsxt`, service_id=22) не менялся в облаке;
|
||||||
|
- `Аллокация IP не снималась` — квота внешних IP организации оставлена без изменений;
|
||||||
|
- `Ресурс заморожен, а не удалён` (×2) — vDC (`vc_vdc`, 21) и кластер (`k8s_sthutrval_cluster`, 150) переведены в `suspend`.
|
||||||
|
|
||||||
|
Состояние после destroy (проверено kubectl + API ЛК):
|
||||||
|
|
||||||
|
| Объект | Статус |
|
||||||
|
|---|---|
|
||||||
|
| `terraform state list` | пусто (все 5 ресурсов убраны из стейта) |
|
||||||
|
| Кластер `94627ff4…` | `suspended`, `isSuspended=true`, не удалён |
|
||||||
|
| vDC `d0937335…` | `suspended`, `isSuspended=true`, не удалён |
|
||||||
|
| Эдж `2c37fed1…` | `running`, `ipSpaceName=internet-ipv4-v1` (SNAT включён) |
|
||||||
|
| Орга `57eeacd1…` | `running`, `vIPConfigure=[{name:internet-ipv4-v1,count:3}]` (квота не тронута) |
|
||||||
|
| Кластерный API `.146:6443` | TCP принимается эджем, но k8s не отвечает (`connection reset by peer`) — ВМ кластера спят |
|
||||||
|
| Ingress `.148:443` | открыт (эдж/AVI живут) |
|
||||||
|
|
||||||
|
Осталось проверить обратный ход: `terraform apply` должен усыновить те же инстансы (`adopt_existing_on_create=true`)
|
||||||
|
и разморозить их (`resume`) — запускает пользователь.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5.3. Баг: регистр UUID внутри JSON (первый `apply` после заморозки)
|
||||||
|
|
||||||
|
**Симптом.** `apply` после destroy (провайдер `2.0.22`) упал:
|
||||||
|
`Error: Ошибка клиента … required params mismatch for resource_name shturval-dev: startupConfiguration
|
||||||
|
(plan={…"nsxtUid":"2c37fed1-…"}, actual={…"nsxtUid":"2C37FED1-…"})`.
|
||||||
|
Эдж после пересоздания вернул UUID в lowercase, а в живом инстансе кластера тот же UUID лежит в UPPERCASE.
|
||||||
|
|
||||||
|
**Почему вылезло именно сейчас.** Регистр ранее учли в пяти местах — `core/refsvc.go:20` (lowercase при отправке),
|
||||||
|
`core/refsvc_resolve.go:28-29`, `resources_core/params_compare.go` (`normalizeCompareValue` — одиночные значения),
|
||||||
|
шаблон `instance.go:204` (`strings.EqualFold` для create-only), плюс восстановление регистра в state.
|
||||||
|
Ни одно из них не смотрит **внутрь JSON**, а adopt **приостановленного** инстанса сравнивает параметр целиком как JSON
|
||||||
|
(`RequiredParamsMismatch` → `paramsEquivalent` → `JSONStringsEquivalent` → `normalizeJSONScalarsToStrings`,
|
||||||
|
где было `case string: return val`). У Штурвала ref-параметры упакованы в JSON (`startupConfiguration`),
|
||||||
|
а путь adopt-suspended задействован впервые.
|
||||||
|
|
||||||
|
**Аудит: где ещё может вылезти.**
|
||||||
|
|
||||||
|
| # | Место | Что ломает |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | `resources_core/required_params_compare.go:94` | adopt suspended — hard error (сегодняшний кейс) |
|
||||||
|
| 2 | `core/modifier_compare.go:47,53` | ложное «не равно» → лишний `modify` при каждом apply (сейчас спит: у `org_ip_allocation` UUID внутри `vip_configure` нет) |
|
||||||
|
| 3 | `resources_core/state_refresh.go:150` | сохранение планового JSON при эквивалентности → в стейт уедет регистр API |
|
||||||
|
| 4 | `resources_core/resource_diagnostics_required.go:104` | та же `RequiredParamsMismatch` в create-диагностике |
|
||||||
|
| 5 | `resources_core/params_compare.go` (`ParamsMatchForResume`) | одиночный UUID ок, JSON — та же дыра (в сгенерированном коде не вызывается) |
|
||||||
|
| 6 | `resources_core/json_planmodifier.go` (`JsonNormalize`) | только `json.Compact` → для user-facing JSON-атрибутов с UUID риск вечного diff |
|
||||||
|
| 7 | `resources_core/ref_validation.go` (`ValidateRefParamsOnAdopt`) | ref-параметр, зашитый внутрь JSON, не проверяется вовсе → чужой инстанс не отловится (открыто) |
|
||||||
|
| 8 | `core/operation_run.go:151`, `operation_run_bycode.go:125` (`lookupLiveParam`) | подстановка live-значений по ключам; при другом регистре ключа молча не сработает (надо проверить, открыто) |
|
||||||
|
|
||||||
|
**Фикс (коммит — см. ниже).**
|
||||||
|
|
||||||
|
- `internal/core/jsonutil/jsonutil.go`: добавлен `LowercaseUUIDsInText` (regex по UUID-подстроке) и строковые значения
|
||||||
|
внутри JSON теперь нормализуются (`normalizeJSONScalarsToStrings`, `case string`) — закрывает пункты 1–5 сразу.
|
||||||
|
- `internal/resources_core/json_planmodifier.go`: `JsonNormalize()` после `json.Compact` приводит UUID-подстроки
|
||||||
|
к lowercase (типы и порядок ключей НЕ меняются) — закрывает пункт 6.
|
||||||
|
- Тесты: `internal/core/jsonutil/jsonutil_test.go` (UUID внутри вложенного JSON, регистр, разные UUID, числа/bool,
|
||||||
|
текст без UUID), `internal/resources_core/params_compare_test.go` (`paramsEquivalent` на реальном `startupConfiguration`).
|
||||||
|
|
||||||
|
**Открыто (7–8):** валидация ref-параметров внутри JSON и регистр ключей в `lookupLiveParam` — отдельная задача
|
||||||
|
(требует решения, что делать при mismatch, и живой проверки).
|
||||||
|
|
||||||
|
**Релиз:** `2.0.23` собран и залит в dev-реестр (`03_build_and_upload_provider.sh`), версия видна в реестре;
|
||||||
|
`VERSIONS.md` обновлён. После него нужно повторить `apply` на стенде (усыновление + `resume`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Мои ошибки в этой сессии (обязательно к фиксации)
|
||||||
|
|
||||||
|
1. Сказал, что apply «либо даст ошибку, либо создаст дубль кластера» — **неверно**: будет hard error
|
||||||
|
«РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ», дубль не создаётся (проверено в коде).
|
||||||
|
2. Интерпретировал `will be created` в плане как доказательство отсутствия adopt — adopt работает в `Create`, не в plan.
|
||||||
|
3. Перепутал колонки UI: «17/24» — это «Системные сервисы», а «Ingress» — домен-шаблон, а не счётчик.
|
||||||
|
4. Предлагал ручные обходы (`terraform state rm`, `removed`-блок, `-target`) там, где требуется автоматический
|
||||||
|
freeze флагами — пользователь это отклонил.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Открытые вопросы / тикет в платформу
|
||||||
|
|
||||||
|
1. Job установщика: Failed-поды живут сутки (`ttlSecondsAfterFinished: 86400`), `backoffLimit: 10`,
|
||||||
|
очистки нет; установка компонентов идёт до готовности Cilium → EPERM на webhook-ах.
|
||||||
|
2. Эдж: в `availableOperations` нет `suspend` → «заморозить» его платформенно невозможно, только «не трогать».
|
||||||
|
3. Квота IP: `count` нельзя опустить ниже занятых, штатного API «занято N» нет — только текст ошибки.
|
||||||
|
4. vDC: полное удаление только через 14 дней после `suspend`; при живых сущностях — через поддержку.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# CHAT RESUME — Штурвал dev-00: диагностика + дизайн «freeze on destroy» (2026-09-24)
|
||||||
|
|
||||||
|
> Полная версия с источниками: `NOTES/30_analysis/SHTURVAL_DEV00_DIAG_AND_FREEZE_DESIGN_2026-09-24.md`
|
||||||
|
|
||||||
|
## Что сделано
|
||||||
|
|
||||||
|
1. **Проверка кластера из локали** (контекст `tazet@narod.ru@shturval-dev-00`, API `185.247.187.146:6443`):
|
||||||
|
всё зелёное — 2 ноды Ready, `shturvalserviceconfigs` 41/41 `ready`, `nodeconfigitems` 4/4,
|
||||||
|
у всех 35 сервисов есть endpoints. Остался только «мусор»: 4 подвисших пода `kube-system/shturval-init-job`
|
||||||
|
(3 Error + 1 Unknown) — Job уже `Complete 1/1`, поды уйдут сами по `ttlSecondsAfterFinished: 86400`
|
||||||
|
(~25.09 14:31 UTC). Причина падений — webhook-вызовы до готовности Cilium: `connect: operation not permitted`.
|
||||||
|
2. **Расшифрованы счётчики ЛК**: `Pods 44/48` = готовые/всего (48 = поды без Completed); «Системные сервисы»
|
||||||
|
= число сервисов в режиме `auto` (17/24 → 24/24 после установки); «Ingress» = домен, не счётчик;
|
||||||
|
⚠️ на «Pods» = те 4 подвисших пода.
|
||||||
|
3. **Разобрана ошибка destroy**: `nubes_vc_org_ip_allocation` шлёт `count=0`, платформа не даёт опустить
|
||||||
|
`count` ниже занятых (2 адреса держит кластер: `.146` API и `.148` ingress; `suspend` адреса не освобождает).
|
||||||
|
Destroy прервался на аллокации, SNAT успел сняться → «рваное» состояние.
|
||||||
|
4. **Adopt починен**: добавлен `adopt_existing_on_create = true` в `DEV_STAND/FullPipe/shturval.tf`
|
||||||
|
(коммит `57abb7b`). Apply усыновил существующий инстанс `94627ff4-…` и сам сделал `resume` (18:26:30) —
|
||||||
|
кластер снова running, SNAT восстановлен (`internet-ipv4-v1`, modify 18:23:12).
|
||||||
|
5. **Решение по дизайну** (Опус + наше): три режима destroy в одной логике — `delete` (дефолт),
|
||||||
|
`suspend` (где сервис умеет), `keep` → `state_only` (эдж, SNAT, квота IP). Реализация — **через генератор**
|
||||||
|
(`TOOLS/resource-generator`: types/loader/templates + `keep_on_destroy_default` в YAML), не ручными правками
|
||||||
|
`resources_gen/`. Дефолты провайдера остаются разрушающими; freeze включается явно в `.tf` стенда.
|
||||||
|
|
||||||
|
## Что осталось сделать (по команде)
|
||||||
|
|
||||||
|
1. Правка генератора: `keep_on_destroy` для инстанс-ресурсов (эдж в первую очередь) + предупреждения в `Delete`.
|
||||||
|
2. `DEV_STAND/FullPipe`: `keep_on_destroy = true` в `modifiers.tf` (:29 snat, :39 квота) и в `edge.tf`
|
||||||
|
(+ `adopt_existing_on_create = true`), кластеру — явный `suspend_on_destroy = true`.
|
||||||
|
3. Регенерация + проверка воспроизводимости `10_yaml_stability_run.sh`, сборка/релиз провайдера.
|
||||||
|
4. Тикет в платформу: TTL/очистка Failed-подов установщика, отсутствие `suspend` у эджа, `count` ниже занятых.
|
||||||
|
|
||||||
|
## Полезное для воспроизведения
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# состояние кластера
|
||||||
|
kubectl get nodes; kubectl get pods -A | grep -v -E "Running|Completed"
|
||||||
|
kubectl -n kube-system get job shturval-init-job -o json | jq '.spec.backoffLimit,.spec.ttlSecondsAfterFinished,.status'
|
||||||
|
# API ЛК dev (нужны User-Agent и Referer, иначе 403)
|
||||||
|
TOK=$(tr -d '\n' < secrets/narodDEV.token)
|
||||||
|
curl -s -H "Authorization: Bearer $TOK" -H "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36" \
|
||||||
|
-H "Referer: https://deck-dev.ngcloud.ru/" \
|
||||||
|
'https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc/instances/94627ff4-33a5-48f2-aca1-695741e0b6a2'
|
||||||
|
```
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
# CHAT RESUME — Штурвал + «freeze on destroy»: состояние на 2026-09-25
|
||||||
|
|
||||||
|
> Цель файла: начать новый чат **без уточняющих вопросов** — здесь всё, что сделано, где живёт
|
||||||
|
> документация, что в каком состоянии и что делать дальше.
|
||||||
|
|
||||||
|
## 0. Где что лежит (точки входа)
|
||||||
|
|
||||||
|
| Что | Путь |
|
||||||
|
|---|---|
|
||||||
|
| Полный разбор сессии (диагностика, пайплайн, аудит регистра UUID) | `NOTES/30_analysis/SHTURVAL_DEV00_DIAG_AND_FREEZE_DESIGN_2026-09-24.md` |
|
||||||
|
| Страница для пользователя/DevOps: поведение и отличия от канонического Terraform | `docs/30_registry/guides/provider-behavior.md` |
|
||||||
|
| Регистр UUID (все нерабочие подходы + кейс UUID внутри JSON, §10) | `docs/60_strategy/terraform_case_sensitivity_fix.md` |
|
||||||
|
| Запись дня с коммитами и результатами | `HISTORY/2026-09-24_shturval_dev00_adopt_and_freeze_design.md` |
|
||||||
|
| Память репозитория (факты, шпаргалки, уроки) | `/memories/repo/shturval-destroy-freeze.md` |
|
||||||
|
| Версии провайдеров по стендам | `VERSIONS.md` |
|
||||||
|
|
||||||
|
Репозиторий: `/home/naeel/TF/tf_provider`, ветка `master`, remote `origin`
|
||||||
|
(`https://gitea.services.ngcloud.ru/terraform/tf_provider.git`). Снапшот состояния — ветка
|
||||||
|
`snapshot/2026-09-25-shturval-freeze-state`.
|
||||||
|
|
||||||
|
## 1. Что уже сделано
|
||||||
|
|
||||||
|
1. **Диагностика стенда `shturval-dev-00`** (услуга 150, инстанс `shturval-dev`, uid `94627ff4-…`):
|
||||||
|
кластер здоров (2 ноды Ready, 41/41 Shturval-сервисов `ready`, NodeConfigItems 4/4, у всех сервисов
|
||||||
|
есть endpoints). Единственный мусор — 4 подвисших пода `kube-system/shturval-init-job`
|
||||||
|
(3 Error + 1 Unknown при `Complete 1/1`), причина — webhook-и Штурвала недоступны до готовности Cilium
|
||||||
|
(`connect: operation not permitted`); самоочистка по `ttlSecondsAfterFinished: 86400` (≈25.09 14:31 UTC).
|
||||||
|
2. **Расшифрованы счётчики ЛК/Штурвала**: `Pods X/Y` = готовые/всего (без `Completed`);
|
||||||
|
«Системные сервисы» = число сервисов в режиме `auto`; «Конфигурация узлов» = NodeConfigItems;
|
||||||
|
«Ingress» = домен-шаблон, не счётчик.
|
||||||
|
3. **Разобран провал `destroy`**: `nubes_vc_org_ip_allocation` отправлял `count=0`, платформа не даёт
|
||||||
|
опустить `count` ниже занятых адресов — их держит кластер (`.146` API и `.148` ingress), `suspend`
|
||||||
|
адреса не освобождает.
|
||||||
|
4. **Усыновление исправлено**: в `DEV_STAND/FullPipe/shturval.tf` добавлен `adopt_existing_on_create = true`
|
||||||
|
(коммит `57abb7b`); проверено вживую — apply усыновил существующий инстанс и сам сделал `resume`.
|
||||||
|
5. **Реализован третий режим destroy** (коммит `22c6c83`, генератор, универсально для всех instance-ресурсов):
|
||||||
|
`keep_on_destroy` → `state_only` (приоритет), иначе `suspend_on_destroy` → `suspend`, иначе `delete`;
|
||||||
|
в `Delete` добавлены предупреждения «Ресурс заморожен, а не удалён» / «оставлен как есть».
|
||||||
|
6. **Конфиг стенда переведён в режим «заморозки»** (коммит `40aef87`): `keep_on_destroy = true` у квоты IP,
|
||||||
|
SNAT и эджа; `adopt_existing_on_create = true` и `suspend_on_destroy = true` у кластера; у vDC оба флага
|
||||||
|
уже стояли.
|
||||||
|
7. **Проверен цикл `destroy` = заморозка** на живом стенде (`2.0.22`): `0 added, 0 changed, 5 destroyed`,
|
||||||
|
ошибок нет; кластер и vDC → `suspended`, эдж `running` с `ipSpaceName=internet-ipv4-v1`, квота IP `count=3`,
|
||||||
|
state пуст; в выводе — 5 предупреждений.
|
||||||
|
8. **Найден и исправлен баг регистра UUID внутри JSON** (коммит `621280a`, релиз `2.0.23`): первый `apply`
|
||||||
|
после заморозки падал на `required params mismatch … startupConfiguration` (`2c37fed1-…` в плане против
|
||||||
|
`2C37FED1-…` в живом инстансе). Проведён аудит 8 мест (см. §10 в `terraform_case_sensitivity_fix.md`),
|
||||||
|
добавлены `jsonutil.LowercaseUUIDsInText` и нормализация строк внутри JSON, `JsonNormalize()` приводит
|
||||||
|
UUID-подстроки к lowercase; покрыто тестами (`jsonutil_test.go`, `params_compare_test.go`), `go test ./...` зелёный.
|
||||||
|
9. **Документация**: страница `docs/30_registry/guides/provider-behavior.md`, обновлённый §10 в
|
||||||
|
`terraform_case_sensitivity_fix.md`, записи в `NOTES`/`HISTORY`, память репозитория.
|
||||||
|
|
||||||
|
## 2. Текущее состояние (на момент записи)
|
||||||
|
|
||||||
|
- `DEV_STAND/FullPipe`: **`terraform state list` пуст** (после проверочного `destroy`).
|
||||||
|
- В облаке: кластер `shturval-dev-00` — `suspended`; vDC — `suspended`; эдж — `running`
|
||||||
|
(`ipSpaceName = internet-ipv4-v1`, SNAT включён); квота IP организации — `count = 3`.
|
||||||
|
- Кластер «спит»: `.146:6443` TCP принимается эджем, но k8s не отвечает (`connection reset by peer`);
|
||||||
|
`.148:443` открыт (эдж/AVI живут).
|
||||||
|
- Версия провайдера: в реестре dev — **`2.0.23`**; пины `versions.tf` в `DEV_STAND/FullPipe` и
|
||||||
|
`DEV_STAND/FPipeGmail` — `2.0.23`.
|
||||||
|
- Новый (не проверенный вживую) стенд пользователя: `DEV_STAND/FPipeGmail/`.
|
||||||
|
|
||||||
|
## 3. Что делать дальше
|
||||||
|
|
||||||
|
1. **Проверить обратный ход** (запускает только пользователь): `terraform apply` в `DEV_STAND/FullPipe`.
|
||||||
|
Ожидание: `adopt` по имени + `resume` для кластера и vDC; эдж/SNAT/квота — no-op; затем `plan` = `No changes`.
|
||||||
|
Проверки: `terraform state list`, `terraform state show`, статусы инстансов в API ЛК, `kubectl get nodes`
|
||||||
|
(снова Ready), поды `44/48` (+ мусор `shturval-init-job`, уйдёт сам).
|
||||||
|
2. **Доку при необходимости**: добавить страницу `provider-behavior.md` в `nav` (`mkdocs.yml`) и запустить
|
||||||
|
`TOOLS/scripts/04_build_and_publish_docs.sh` — **не запускать без прямой команды**.
|
||||||
|
3. **Открытые техдолги:**
|
||||||
|
- ref-параметр внутри JSON **не валидируется** при adopt (`resources_core/ref_validation.go`);
|
||||||
|
- регистр ключей в `lookupLiveParam` (`core/operation_run.go`, `operation_run_bycode.go`) — требует живой проверки;
|
||||||
|
- тикет в платформу: TTL/очистка Failed-подов установщика + порядок установки компонентов до готовности Cilium;
|
||||||
|
- тикет в платформу: у Эджа нет операции `suspend` (в `availableOperations` только `delete/modify/reconcile`).
|
||||||
|
4. **Полный teardown** — осознанно: `keep_on_destroy = false` и `suspend_on_destroy = false`, порядок
|
||||||
|
кластер → `count=0` → SNAT → эдж → vDC (для vDC действует правило «14 дней после suspend»).
|
||||||
|
|
||||||
|
## 4. Шпаргалка: режимы destroy
|
||||||
|
|
||||||
|
| Ресурс | Флаг | Поведение при destroy | Предупреждение |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `nubes_k8s_sthutrval_cluster` | `suspend_on_destroy = true` | `suspend` | «Ресурс заморожен, а не удалён» |
|
||||||
|
| `nubes_vc_vdc` | `suspend_on_destroy = true` | `suspend` | то же |
|
||||||
|
| `nubes_vc_nsxt` (эдж) | `keep_on_destroy = true` | не трогается | «Ресурс оставлен как есть, а не удалён» |
|
||||||
|
| `nubes_vc_nsxt_snat` | `keep_on_destroy = true` | не трогается | «SNAT не выключался» |
|
||||||
|
| `nubes_vc_org_ip_allocation` | `keep_on_destroy = true` | не трогается | «Аллокация IP не снималась» |
|
||||||
|
|
||||||
|
Приоритет: `keep_on_destroy` > `suspend_on_destroy` > обычное удаление. Дефолты провайдера — разрушающие;
|
||||||
|
«заморозка» включается в `.tf` стенда.
|
||||||
|
|
||||||
|
## 5. Факты платформы (проверено)
|
||||||
|
|
||||||
|
- 1 кластер Штурвала = 1 vDC; vDC удаляется только через 14 дней после `suspend`.
|
||||||
|
- Квоту IP нельзя опустить ниже занятых адресов; адреса кластера `suspend` не освобождает.
|
||||||
|
- У эджа нет `suspend`; удаление эджа при живых зависимых (vApp/VM/кластер) недопустимо.
|
||||||
|
- API ЛК: `GET /api/v1/svc/instances/{uid}`, состояние — `instance.state.params`, статус — `explainedStatus`,
|
||||||
|
операции — `availableOperations`; ошибки операции — в теле (`isSuccessful=false`, `errorLog`), HTTP 200/201.
|
||||||
|
- Обязательные заголовки API ЛК: браузерный `User-Agent`, `Referer: https://deck-dev.ngcloud.ru/`,
|
||||||
|
`Authorization: Bearer <secrets/narodDEV.token>` — иначе `403`.
|
||||||
|
- Регистр UUID: облако отдаёт один и тот же UUID в разных регистрах → сравнивать всегда без учёта регистра
|
||||||
|
(в т.ч. **внутри JSON**).
|
||||||
|
|
||||||
|
## 6. Релизы
|
||||||
|
|
||||||
|
- Схема: prod `1.*`, dev `2.*`, test `3.*`; актуальная dev — `2.0.23` (`VERSIONS.md`).
|
||||||
|
- Релиз: `TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev <версия>`
|
||||||
|
(сам прогоняет `01` + `02`, собирает, подписывает, заливает, обновить `VERSIONS.md` вручную).
|
||||||
|
- Локальная проверка без релиза: `go build -o TMP/devbin/terraform-provider-nubes .` в `provider/`
|
||||||
|
и `TF_CLI_CONFIG_FILE=TMP/terraformrc.dev terraform validate|plan` (dev_overrides).
|
||||||
|
- YAML-спеки (`generated/*/resources_yaml/`) **не редактировать руками** — `01_generate_yamls.sh` перезапишет их из API.
|
||||||
|
|
||||||
|
## 7. Правила работы (для нового чата)
|
||||||
|
|
||||||
|
- `terraform apply` / `destroy` — только пользователь. Мне доступны `init/plan/validate/show/state show`.
|
||||||
|
- Никаких правок, коммитов, релизов и публикаций без прямой команды; после каждой правки — коммит.
|
||||||
|
- Перед правками важных файлов — бэкап в `TMP/backup_<дата>/`.
|
||||||
|
- Не «улучшать» соседние стенды/сервисы без команды (scope creep запрещён).
|
||||||
|
|
||||||
|
## 8. Быстрые команды
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# состояние стенда
|
||||||
|
cd DEV_STAND/FullPipe && terraform state list && terraform plan
|
||||||
|
terraform state show nubes_k8s_sthutrval_cluster.shturval | grep -E "id|adopt|suspend|keep"
|
||||||
|
|
||||||
|
# кластер
|
||||||
|
kubectl get nodes
|
||||||
|
kubectl get pods -A --no-headers | awk '{split($3,a,"/"); tot++; if($4=="Running"&&a[1]==a[2]) ok++} END{print tot, ok}'
|
||||||
|
kubectl get pods -A | grep -v -E "Running|Completed"
|
||||||
|
|
||||||
|
# API ЛК (dev)
|
||||||
|
TOK=$(tr -d '\n' < secrets/narodDEV.token)
|
||||||
|
curl -s -H "Authorization: Bearer $TOK" \
|
||||||
|
-H "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36" \
|
||||||
|
-H "Referer: https://deck-dev.ngcloud.ru/" \
|
||||||
|
'https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc/instances/94627ff4-33a5-48f2-aca1-695741e0b6a2'
|
||||||
|
|
||||||
|
# реестр провайдера (dev)
|
||||||
|
curl -s https://tf-registry.containerk8s.services.ngcloud.ru/v1/providers/nubes-dev/nubes/versions
|
||||||
|
```
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
# CHAT RESUME — добавляем ВМ в пайплайн (состояние на 2026-09-27)
|
||||||
|
|
||||||
|
> Цель файла: новый чат стартует **без уточняющих вопросов** — здесь всё, что уже сделано,
|
||||||
|
> что где лежит, что проверено на живых стендах и что решать по ВМ.
|
||||||
|
> Репозиторий: `/home/naeel/TF/tf_provider`, ветка `master`.
|
||||||
|
> Remote: `https://gitea.services.ngcloud.ru/terraform/tf_provider.git`.
|
||||||
|
> Репозиторий примеров: `tf_examples/` (отдельный git) → `https://gitea.services.ngcloud.ru/Nail/tf_examples.git`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Где что лежит (точки входа)
|
||||||
|
|
||||||
|
| Тема | Путь |
|
||||||
|
|---|---|
|
||||||
|
| Предыдущее резюме (freeze/adopt, состояние Штурвала) | `NOTES/40_chat_summaries/CHAT_RESUME_2026-09-25_shturval_freeze_state.md` |
|
||||||
|
| Разбор диагностики + дизайн «заморозки» | `NOTES/30_analysis/SHTURVAL_DEV00_DIAG_AND_FREEZE_DESIGN_2026-09-24.md` |
|
||||||
|
| Зависимости сервисов DEV + чек-листы Штурвал/ВМ | `docs/60_strategy/dev_stand_service_dependencies.md` |
|
||||||
|
| Диаграмма зависимостей + генератор | `docs/diagrams/README.md`, `docs/diagrams/infra_services_flow.mmd`, `render_infra_diagram.py` |
|
||||||
|
| Страница для пользователя: поведение провайдера | `docs/30_registry/guides/provider-behavior.md` |
|
||||||
|
| Страница-инструкция пайплайна | `docs/curated/pipeline/vdc_edge_ip_snat.md` |
|
||||||
|
| Пример в репе примеров | `tf_examples/fullpipe_chain/` (в т.ч. `shturval.tf`) |
|
||||||
|
| Регламент публикации доков | `DOCS_PIPELINE/README.md`, `TOOLS/scripts/04_build_and_publish_docs.sh`, `scripts/publish-docs.sh` |
|
||||||
|
| Инструкции «как сделать руками» | `HOW_TO/README.md` (индекс), `HOW_TO/howitwasdone.md`, `HOW_TO/DEVOPS_BUILD_PIPELINE.md` |
|
||||||
|
| Версии провайдеров по стендам (источник правды) | `VERSIONS.md` |
|
||||||
|
| Память (репозиторий) | `/memories/repo/shturval-destroy-freeze.md`, `/memories/repo/registry-versions.md` |
|
||||||
|
| **ВМ: старые материалы** | `docs/20_discovery/vm_service.md`, `vm-ui-verification.md`, `vm_provisioning_failures.md`, `vapp-service.md`, `docs/40_analysis/vm_creation_failure_analysis.md`, `docs/50_history/04_vm_hang_fix_and_500_error.md`, `docs/50_history/12_vm_resource_hardening.md`, `docs/50_history/23_vapp_uid_inconsistency_displayname_resolve_bug.md` |
|
||||||
|
| **ВМ: старое резюме (ЛЕГАСИ)** | `NOTES/40_chat_summaries/CHAT_RESUME_PLAN_VM.md` — другой репозиторий (`/home/naeel/terra`), ресурс `nubes_vm`, пути неактуальны; только как история |
|
||||||
|
| **ВМ: спеки услуг** | `generated/dev/resources_yaml/26_vapp.yaml` (услуга 26, vApp), `generated/dev/resources_yaml/28_vc_vm_v3.yaml` (услуга 28, ВМ) |
|
||||||
|
| Референс от платформы (Cloud Director) | каталог `!/` в корне: `vmware_org.tf`, `vdc.tf`, `network.tf.tmpl` (цепочка ipSpace → providerGateway → providerVdc) |
|
||||||
|
| Стенды | `DEV_STAND/FullPipe/` (проверенный, аккаунт `tazet@narod.ru`), `DEV_STAND/FPipeGmail/` (аккаунт `tazetdinovn@gmail.com`, организация `kontra`) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Что уже сделано (только факты, по коммитам)
|
||||||
|
|
||||||
|
**21–24.09 — ресурсы-модификаторы и релизы 2.0.18 → 2.0.23**
|
||||||
|
- `nubes_vc_org_ip_allocation` (modify `vIPConfigure`) и `nubes_vc_nsxt_snat` (modify `ipSpaceName`, обратный `no-needed`) — генератор + код + тесты (`22cf259`, `80d82a1`, `73a7459`, `4b497e6`, `ba6c4f5`, `1236c59`).
|
||||||
|
- Ресурс аллокации принимает **имя** организации (резолв в UUID) — `5bd197f`.
|
||||||
|
- Убран plan-modifier, менявший пользовательское значение (Terraform: planned value must match config) — `807dfde`.
|
||||||
|
|
||||||
|
**24.09 — Штурвал: adopt + «заморозка»**
|
||||||
|
- Всё про Штурвал собрано в один файл `shturval.tf` (`b8adeb6`); правильный сервис — **150**, не 148 (`1a90c77`, `d98f603`).
|
||||||
|
- `worker_configuration` — JSON с **camelCase** (`groupName`, `sizingPolicy`, `sizingDisk`, `labelDeck`) — snake_case валит платформу (`72d5771`).
|
||||||
|
- `adopt_existing_on_create = true` для кластера (`57abb7b`).
|
||||||
|
- Реализован **третий режим destroy** `keep_on_destroy` → `state_only` для всех instance-ресурсов + предупреждения в Delete (`22c6c83`), релиз **2.0.22** (`c29df21`).
|
||||||
|
- Конфиг стенда переведён в «заморозку» (`40aef87`): квота IP/SNAT/эдж — `keep_on_destroy = true`, кластер — `suspend_on_destroy = true`, у vDC — `suspend` + adopt.
|
||||||
|
- Живая проверка `destroy` = заморозка: `5 destroyed`, кластер и vDC → `suspended`, эдж/SNAT/квота не тронуты (`c9d7345`).
|
||||||
|
|
||||||
|
**24.09 — фикс регистра UUID, релиз 2.0.23**
|
||||||
|
- Adopt suspended-инстанса падал на регистре UUID внутри JSON (`startupConfiguration`: план `2c37fed1-…` против живого `2C37FED1-…`). Добавлены `jsonutil.LowercaseUUIDsInText`, нормализация строк внутри JSON, `JsonNormalize()` приводит UUID-подстроки к lowercase; покрыто тестами (`621280a`), релиз **2.0.23** (`208d97e`, `03fff05`).
|
||||||
|
|
||||||
|
**24–25.09 — документация**
|
||||||
|
- Новая страница «Как работает провайдер Nubes: поведение и отличия от канонического Terraform» (`aaf87d9`, читаемость `c808a3b`): §1 суть, §2 ресурсы, §3 отличия от Terraform, §4 CRUD, §5 delete/suspend/state_only, **§6 конкретный пайплайн стенда vDC → Edge → IP → SNAT → Штурвал**, §7 регистр UUID, §8 чек-лист DevOps, §9 FAQ, §10 ссылки.
|
||||||
|
- Страница пайплайна переписана (`f8d6494`): требования, чек-лист услуги 150 (ALB + AVI ≥ 3, внешних IP ≥ 3), проверка результата (адреса API/Ingress), таблица «заморозки» при destroy, полное удаление, состав файлов.
|
||||||
|
- `provider-behavior.md` добавлена в навигацию `mkdocs.yml` («Руководства → Как работает провайдер (отличия от Terraform)») и связана ссылкой со страницей пайплайна (`8d25de9`).
|
||||||
|
|
||||||
|
**25.09 — стенд второго пользователя `FPipeGmail`**
|
||||||
|
- Организация `kontra` (аккаунт `tazetdinovn@gmail.com`, компания `naeel_test`, ClientID `WZ03709`), токен — `secrets/dev.token`; в `.tfvars` было закомментировано устаревшее `kontora` (`e20c22e`).
|
||||||
|
- Имена Штурвала сделаны свои: `shturval-dev1` / `shturval-dev-01` — занятое имя кластера (даже созданное другим пользователем) платформа не принимает (`e72eb75`).
|
||||||
|
- Проверено на 25.09: `terraform init` (провайдер 2.0.23), `validate` — ок, `plan` = `5 to add, 0 change, 0 destroy`. **apply не запускался** (запускает только пользователь).
|
||||||
|
|
||||||
|
**25.09 — репозиторий примеров `tf_examples`**
|
||||||
|
- `fullpipe_chain`: добавлен `shturval.tf` (переменные + locals + ресурс, самодостаточно), пин провайдера `2.0.21 → 2.0.23`, флаги «заморозки» как в рабочем стенде (эдж/SNAT/квота `keep_on_destroy = true`, vDC `suspend` + adopt), выводы Штурвала, блок параметров в `terraform.tfvars.example`, переписаны README примера и корневой (`21618a7`); добавлено предупреждение про уникальность имени кластера (`435a685`).
|
||||||
|
- Всё запушено: последний коммит примеров — `435a685`.
|
||||||
|
|
||||||
|
**26.09 (не в этой сессии, но уже в `master` — чтобы не переспрашивать)**
|
||||||
|
- Диаграмма зависимостей сервисов: `docs/diagrams/*` (`ea725bd`, `ff38352`, `6791e8f`), генератор `render_infra_diagram.py`, README с инструкцией перегенерации.
|
||||||
|
- Сверка цепочек зависимостей с YAML + чек-листы Штурвала и ВМ: `docs/60_strategy/dev_stand_service_dependencies.md` (`dc837e8`), перенос артефактов диаграмм + гайд по созданию vApp (`a366f47`).
|
||||||
|
- Закоммичены бэкапы от 25.09: `TMP/backup_2026-09-25/**` (`4800958`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Текущее состояние (проверено 2026-09-27)
|
||||||
|
|
||||||
|
**Terraform**
|
||||||
|
- `DEV_STAND/FullPipe`: `terraform state list` — **пусто**.
|
||||||
|
- `DEV_STAND/FPipeGmail`: `terraform state list` — **пусто**; при этом конфиг валиден, `plan` = `5 to add` (по состоянию на 25.09).
|
||||||
|
- Terraform `1.9.5`; `go build ./...` и `go test ./...` — чисто (проверено 25.09).
|
||||||
|
|
||||||
|
**Облако, аккаунт `tazet@narod.ru` (токен `secrets/narodDEV.token`)**
|
||||||
|
|
||||||
|
| Услуга | Имя | UID | Статус |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 150 Штурвал | `shturval-dev` | `94627ff4-33a5-48f2-aca1-695741e0b6a2` | `suspended` |
|
||||||
|
| 21 vDC | `fullpipe-vdc` | `d0937335-276b-475b-baa4-d8e6d16bad51` | `suspended` |
|
||||||
|
| 22 Edge | `fullpipe-edge` | `2c37fed1-e8f8-4a84-8434-7851c7c8b5d6` | `running`, SNAT `internet-ipv4-v1` |
|
||||||
|
| 19 Организация | `organ` | `57eeacd1-dc7f-4a52-b903-7e5f7d3c1164` | `running`, `vIPConfigure` = `[{internet-ipv4-v1, count: 3}]` |
|
||||||
|
|
||||||
|
Всего в аккаунте 13 инстансов, в том числе: `dummy-5`/`dummy-6` (1), `External ip` и `vIp` (25, `running`), `A-record` (111, `running`), инстансы mgmt-кластера (21/22, `deleted`). Из 3 инстансов `fullpipe-edge` — 2 `deleted` (история), 1 `running` — не пугаться.
|
||||||
|
|
||||||
|
**Облако, аккаунт `tazetdinovn@gmail.com` (токен `secrets/dev.token`, срок до 2026-12-27)**
|
||||||
|
- Организация `kontra` (`df5ec5f2-5f5d-4c82-952c-3dffa91c61d3`, услуга 19): `running`, `isTrial`, создана 2026-09-24 08:20, `vIPConfigure` = `count: 3`, realm `sandbox.nubes.ru`.
|
||||||
|
- В этом аккаунте **уже созданы объекты стенда FPipeGmail**: `fullpipe-vdc` (21) — `suspended`, `fullpipe-edge` (22) — `running`, `shturval-dev1` (150) — `suspended` (локальный `state` при этом пуст → следующий `apply` усыновит их по имени и разморозит). Всего 12 инстансов, плюс `kontra` (19), S3-бакеты (13), `s3naeeldev` (12), `mariamdb` (115), `vc_v`/`vc_vdc-1`/`tedj` — `deleted`.
|
||||||
|
- ⛔ **Исправление ранее ложного наблюдения** (в первой версии этого резюме было «список отдаёт 0 инстансов»): это была **ошибка парсинга ответа с моей стороны** — API отдаёт список в ключе `results`, а не `instances`. Оба аккаунта отдают данные корректно (narod — 13, gmail — 12). Никакой аномалии платформы нет.
|
||||||
|
|
||||||
|
**Провайдер / реестр / доки**
|
||||||
|
- DEV в реестре: **`2.0.23`** (последняя), пины `2.0.23` в `DEV_STAND/FullPipe/versions.tf`, `DEV_STAND/FPipeGmail/versions.tf`, `tf_examples/fullpipe_chain/versions.tf`, `.terraform.lock.hcl` FullPipe.
|
||||||
|
- PROD `1.0.0`, TEST `3.0.0` (новая схема: prod `1.*`, dev `2.*`, test `3.*`).
|
||||||
|
- Доки dev опубликованы (`2.0.23`) и зеркало на ВМ обновлено; живые URL:
|
||||||
|
- https://tf-docs.nodejsk8s.dev.nubes.ru/nubes-dev/curated/pipeline/vdc_edge_ip_snat/ (в меню: «Проверенные примеры»)
|
||||||
|
- https://tf-docs.nodejsk8s.dev.nubes.ru/nubes-dev/30_registry/guides/provider-behavior/ (в меню: «Руководства»)
|
||||||
|
|
||||||
|
**Git**
|
||||||
|
- Основной репо: `master` = `origin/master` = `e72eb75`; примеры: `435a685` = `origin/master`.
|
||||||
|
- В истории уже лежат коммиты от 26.09 (диаграммы, чек-листы, бэкапы) — см. §1.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Что делать дальше: **добавляем ВМ в пайплайн** (главная цель нового чата)
|
||||||
|
|
||||||
|
**Целевая цепочка (текущая + ВМ):**
|
||||||
|
организация (руками в ЛК) → vDC (21) → Edge (22) → внешние IP (modify) → SNAT (modify) → **[vApp (26) → ВМ (28)]** → Штурвал (150)
|
||||||
|
|
||||||
|
**Подтверждённые факты (сверено с YAML, коммит `dc837e8`)**
|
||||||
|
- D-06: vApp (26) → Edge (22), параметр `nsxtUid` — vApp использует Edge для сети.
|
||||||
|
- D-07: vApp (26) → vDC (21), параметр `vdcUid`.
|
||||||
|
- D-08: ВМ (28) → vApp (26), параметр `vappUid` — ВМ размещается в vApp.
|
||||||
|
- ВМ можно создавать в **уже существующих** vApp / vDC / Edge — ссылками `vappUid` / `vdcUid` / `nsxtUid`.
|
||||||
|
- Ресурсы (CPU/RAM/Storage) ВМ: `vmCpu`, `vmRam`, `vmDisk` (доп. диск); квоты выделяются на уровне vDC (21) и услуги ВМ (28).
|
||||||
|
- Чек-лист создания ВМ (28) по create-параметрам, обязательные: `vappUid`, `vmName`, `vmCpu`, `vmRam`, `accessPortList`, `imageVm`, `userLogin`, `userPublicKey`, `ipSpaceName`. Полный текст — `docs/60_strategy/dev_stand_service_dependencies.md` (§«Чек-лист создания ВМ (28)»).
|
||||||
|
- Спеки ресурсов: `generated/dev/resources_yaml/26_vapp.yaml`, `28_vc_vm_v3.yaml` (**руками не править** — перезапишет `01_generate_yamls.sh`).
|
||||||
|
- Имена ресурсов в провайдере: `nubes_vc_vapp`, `nubes_vc_vm_v3` (услуга 148 «Менеджмент Kubernetes Штурвал» — **не** то, что нужно; для кластера только 150).
|
||||||
|
|
||||||
|
**Известные проблемы ВМ (читать до работ)**
|
||||||
|
- `docs/20_discovery/vm_provisioning_failures.md` — падения на создании, разбор payload Terraform vs ручного запроса.
|
||||||
|
- `docs/40_analysis/vm_creation_failure_analysis.md`, `docs/50_history/04_vm_hang_fix_and_500_error.md` — зависание на FW и 500 («String[] → GUID»).
|
||||||
|
- `docs/50_history/12_vm_resource_hardening.md` — правки по надёжности ресурса ВМ.
|
||||||
|
- `docs/50_history/23_vapp_uid_inconsistency_displayname_resolve_bug.md` — резолв по имени мог вернуть **удалённый** инстанс (лечится фильтром `isDeleted=false`); проверить, что фикс жив в 2.0.23.
|
||||||
|
|
||||||
|
**Что уже выяснено фактами (проверено 27.09 — чтобы не гадать)**
|
||||||
|
- vApp (26), обязательные параметры create: `vappName`, `vdcUid`, `nsxtUid`. Операции: `create`, `delete`, `suspend`, `resume`; в YAML — `suspend_on_destroy_default: true`. Из инструкции услуги: `delete` требует **предварительного `suspend`**, полное удаление — через 14 дней. Отдельных параметров сети/подсети/gateway у vApp нет — сеть даёт Edge (`nsxtUid`).
|
||||||
|
- ВМ (28), обязательные параметры create: `vappUid`, `vmName`, `vmCpu`, `vmRam`, `ipSpaceName`, `imageVm`, `userLogin`, `userPublicKey`, `accessPortList` (`vmDisk` — дополнительный диск, не обязателен). Операции: `create`, `delete`, `modify`, `reconcile`, `redeploy`, `resume`, `suspend`; в YAML — `suspend_on_destroy_default: true`. В modify-наборе есть параметр `sameSnat` (разобраться, что даёт).
|
||||||
|
- `ipSpaceName` у ВМ — «Public IP setting», допускает значение `no-needed` (без публичного адреса). Если ВМ нужен внешний IP, квоту организации придётся увеличивать: сейчас `count = 3` и все три держит кластер Штурвала, а `suspend` адреса не освобождает.
|
||||||
|
- Соответствие кодов и ID параметров ВМ (`docs/20_discovery/vm_service.md`): `vappUid` 407, `vmName` 408, `vmCpu` 409/493, `vmRam` 410/494, `vmDisk` 411/495, `ipSpaceName` 412/496, `accessIpList` 413/497, `imageVm` 414, `cloudInit` 415, `userLogin` 416, `userPublicKey` 417, `accessPortList` 448/498, `needAddZabbixTemplate` 449/499.
|
||||||
|
- **Список образов (`imageVm`) в репозитории отсутствует**: в документации зафиксирован только пример `Ubuntu_22-20G`; HAR с созданием ВМ (`vmOK.har`, `vmsuspendresumemodify.har`) были в старом репо `/home/naeel/terra` — в этом репо в `HAR/` их нет (там `org*`, `vdc`, `edge_`, `ipSpace0`, `pgmodify`, `dummy`, `globak`). Значит: смотреть список в ЛК либо снять свежий HAR.
|
||||||
|
- Живых инстансов vApp (26) и ВМ (28) сейчас **нет ни в одном аккаунте** → `availableOperations` по ним снять не с чего, проверять после первого создания.
|
||||||
|
- Наличие `suspend` у обоих сервисов означает, что режим «заморозка» (`suspend_on_destroy` / `keep_on_destroy`) у них применим — как у кластера и vDC.
|
||||||
|
|
||||||
|
**Что предстоит решить (НЕ выбрано, требует решения пользователя)**
|
||||||
|
1. Где создавать vApp: в `DEV_STAND/FullPipe` (проверенный стенд, аккаунт `narod`) или отдельным файлом-аналогом `shturval.tf` (например `vm.tf`) — по сложившейся конвенции «всё про сервис в одном файле».
|
||||||
|
2. Очерёдность: ВМ до Штурвала или после (влияет и на внешние адреса, и на время apply).
|
||||||
|
3. `imageVm` — откуда брать список образов (в текущих заметках не зафиксировано) → проверять в ЛК/HAR.
|
||||||
|
4. `ipSpaceName` для ВМ — тот же `internet-ipv4-v1` или другой; хватит ли квоты (сейчас `count = 3`, занято кластером).
|
||||||
|
5. `userPublicKey` — какой ключ использовать (в репо есть `secrets/id_ed25519.pub`).
|
||||||
|
6. Режим destroy для vApp и ВМ (есть ли `suspend` у 26/28 — проверить `availableOperations` на живом инстансе перед выбором флагов).
|
||||||
|
7. Обновить после работ: `docs/curated/pipeline/vdc_edge_ip_snat.md` (или новую страницу), `tf_examples/fullpipe_chain` (файл ВМ + README + выводы), `docs/60_strategy/dev_stand_service_dependencies.md`, диаграмму `docs/diagrams/*` (перегенерировать `render_infra_diagram.py`).
|
||||||
|
|
||||||
|
**Правило порядка (из инструкций ЛК):** платформа строит список ipSpace из состояния `job.vcd.networkProvider` / `providerGateway`, то есть аллокация IP и SNAT идут **после** готовых vDC и Edge; иначе modify падает с «Can't cast Complex Object Type Struct to String».
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Факты платформы (проверено, не перепроверять)
|
||||||
|
|
||||||
|
- 1 кластер Штурвала = 1 vDC; vDC удаляется только через **14 дней** после `suspend`.
|
||||||
|
- Квоту IP нельзя опустить ниже занятых адресов; `suspend` кластера адреса **не** освобождает.
|
||||||
|
- У Edge операции `suspend` нет вообще: доступны `delete`, `modify`, `reconcile`.
|
||||||
|
- Доступные операции: организация (19) — `delete`, `suspend`, `resume`, `modify`, `reconcile`, `create_user`, `delete_user`; vDC (21) — `delete`, `modify`, `suspend`, `resume`, `reconcile`; Штурвал (150) — то же + `create_user`, `delete_user`.
|
||||||
|
- API ЛК (dev): `GET /api/v1/svc/instances/{uid}`; поля: `explainedStatus`, `state.isSuspended`, `state.isDeleted`, `state.params`, `availableOperations`; ошибки операций приходят **в теле** (`isSuccessful=false`, `errorLog`) при HTTP 200/201.
|
||||||
|
- Обязательные заголовки: браузерный `User-Agent`, `Referer: https://deck-dev.ngcloud.ru/`, `Authorization: Bearer <токен>`; иначе 403.
|
||||||
|
- Регистр UUID: облако отдаёт один и тот же UUID в разных регистрах → сравнивать **без учёта регистра, в том числе внутри JSON** (учтено в 2.0.23).
|
||||||
|
- Имя кластера Штурвала должно быть **уникальным**: занятое (даже другим пользователем) не подойдёт; имя услуги и имя кластера — свои у каждого пользователя.
|
||||||
|
- `plan` не видит существующие объекты: adopt/конфликт проверяются в `Create` (в плане всегда «will be created»).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Шпаргалка: режимы destroy («заморозка»)
|
||||||
|
|
||||||
|
| Ресурс | Флаг в стенде | Поведение при destroy | Предупреждение |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `nubes_k8s_sthutrval_cluster` | `suspend_on_destroy = true` | `suspend` | «Ресурс заморожен, а не удалён» |
|
||||||
|
| `nubes_vc_vdc` | `suspend_on_destroy = true` | `suspend` | то же |
|
||||||
|
| `nubes_vc_nsxt` (эдж) | `keep_on_destroy = true` | не трогается | «Ресурс оставлен как есть, а не удалён» |
|
||||||
|
| `nubes_vc_nsxt_snat` | `keep_on_destroy = true` | не трогается | «SNAT не выключался» |
|
||||||
|
| `nubes_vc_org_ip_allocation` | `keep_on_destroy = true` | не трогается | «Аллокация IP не снималась» |
|
||||||
|
|
||||||
|
Приоритет: `keep_on_destroy` > `suspend_on_destroy` > обычное удаление. Дефолты генератора: `suspend_on_destroy = true` (для сервисов с операцией `suspend`), `keep_on_destroy = false`, `adopt_existing_on_create = false`. Обратный ход: `apply` усыновляет объект по имени и делает `resume`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Релизы и публикация (команды)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Релиз провайдера (dev)
|
||||||
|
TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev <версия>
|
||||||
|
# → сам прогоняет 01 (YAML) + 02 (Go/доки), собирает, подписывает, заливает в реестр
|
||||||
|
# → VERSIONS.md обновить вручную
|
||||||
|
|
||||||
|
# Локальная проверка без релиза
|
||||||
|
cd provider && go build -o ../TMP/devbin/terraform-provider-nubes .
|
||||||
|
TF_CLI_CONFIG_FILE=TMP/terraformrc.dev terraform validate|plan # dev_overrides
|
||||||
|
|
||||||
|
# Документация: собрать и залить в S3
|
||||||
|
TOOLS/scripts/04_build_and_publish_docs.sh --profile TOOLS/config/dev 2.0.23
|
||||||
|
# ⚠️ версию передавать АРГУМЕНТОМ: в TOOLS/config/dev/profile.env всё ещё VERSION="2.0.17"
|
||||||
|
# docker недоступен → сборка идёт локальным mkdocs 1.6.1 + mkdocs-material
|
||||||
|
|
||||||
|
# Довезти до домена (сейчас обязательно руками: с ВМ нет маршрута до S3)
|
||||||
|
tar -C site -cf - . | ssh vps 'rm -rf ~/tmp-docs-site && mkdir -p ~/tmp-docs-site && tar -C ~/tmp-docs-site -xf -'
|
||||||
|
ssh vps 'rsync -a --delete ~/tmp-docs-site/ /var/www/tf-docs/nubes-dev/'
|
||||||
|
# vps = naeel@5.172.178.213 (алиас в ~/.ssh/config, ключ ~/.ssh/naeel_vm_id_ed25519)
|
||||||
|
# путь из secrets/HOW_TO_SSH.md устарел (файла /home/naeel/remote_dev/... нет)
|
||||||
|
|
||||||
|
# Примеры
|
||||||
|
cd tf_examples && git add -A && git commit -m "..." && git push origin master
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверка доков: `curl -s -o /dev/null -w "%{http_code}" <URL>` — должно быть 200; содержимое локальной сборки лежит в `site/` (каталог в `.gitignore`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Правила работы (не нарушать)
|
||||||
|
|
||||||
|
- `terraform apply` и `destroy` — **только пользователь**. Мне доступны `init/plan/validate/show/state list/state show`.
|
||||||
|
- Никаких правок, коммитов, релизов и публикаций без прямой команды; команда непонятна — **спросить, не гадать**.
|
||||||
|
- После каждой правки — коммит с осмысленным сообщением; перед правкой важных файлов — бэкап в `TMP/backup_<дата>/`.
|
||||||
|
- Секреты (`terraform.tfvars`, токены) **не печатать** в терминал и не коммитить (`*.tfvars` в `.gitignore`); сравнивать по хешу/маскировать.
|
||||||
|
- Не расширять область задач (scope creep), не «улучшать» соседние стенды и репозитории.
|
||||||
|
- Не зацикливаться: два вызова инструмента с близкими аргументами без результата — стоп и доклад.
|
||||||
|
- Проверка примера — на копии в `/tmp`, не в `tf_examples/` (там нет gitignore для `.terraform/`, служебные файлы уехали бы в публичный репозиторий).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Быстрые команды
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# состояние стендов
|
||||||
|
cd DEV_STAND/FullPipe && terraform state list && terraform plan
|
||||||
|
cd DEV_STAND/FPipeGmail && terraform state list && terraform plan
|
||||||
|
|
||||||
|
# кластер (когда running)
|
||||||
|
kubectl config current-context # tazet@narod.ru@shturval-dev-00
|
||||||
|
kubectl get nodes
|
||||||
|
kubectl get pods -A | grep -v -E "Running|Completed"
|
||||||
|
|
||||||
|
# API ЛК (dev), народ-аккаунт
|
||||||
|
TOK=$(tr -d '\n' < secrets/narodDEV.token)
|
||||||
|
curl -s -H "Authorization: Bearer $TOK" \
|
||||||
|
-H "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36" \
|
||||||
|
-H 'Referer: https://deck-dev.ngcloud.ru/' \
|
||||||
|
'https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc/instances?page=1&size=200'
|
||||||
|
|
||||||
|
# реестр провайдера (dev)
|
||||||
|
curl -s https://tf-registry.containerk8s.services.ngcloud.ru/v1/providers/nubes-dev/nubes/versions
|
||||||
|
|
||||||
|
# услуги каталога: 19 орга, 21 vDC, 22 Edge/SNAT, 26 vApp, 28 ВМ, 150 Штурвал, 148 — не то
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Открытые вопросы и техдолги
|
||||||
|
|
||||||
|
1. Ref-параметр **внутри JSON** не валидируется при adopt — `provider/internal/resources_core/ref_validation.go`.
|
||||||
|
2. Регистр ключей в `lookupLiveParam` — `provider/internal/core/operation_cfs.go:38` (в прежних заметках был указан неверный файл); нужна живая проверка.
|
||||||
|
3. Тикеты в платформу: TTL/очистка Failed-подов установщика Штурвала (порядок установки компонентов до готовности Cilium); у Edge нет операции `suspend`.
|
||||||
|
4. `TOOLS/config/dev/profile.env`: `VERSION="2.0.17"` — конфиг отстаёт от реестра (`2.0.23`); правка конфига без команды не делалась.
|
||||||
|
5. `DEV_STAND/FPipeGmail`: локальный `state` пуст, но объекты в облаке **уже есть** (`fullpipe-vdc` suspended, `fullpipe-edge` running, `shturval-dev1` suspended) — при применении Terraform их усыновит.
|
||||||
|
6. `DEV_STAND/FPipeGmail` — применять или нет (сейчас `plan` = 5 ресурсов, `state` пуст) — решение за пользователем.
|
||||||
@@ -7,7 +7,10 @@
|
|||||||
|
|
||||||
| Файл | Тема | Статус |
|
| Файл | Тема | Статус |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `CHAT_RESUME_IAC_2026-09-24.md` | **Текущая линия: IaC + `modify` (Штурвал).** Задача, позиции участников (Георгий/Дмитрий/Виталий), 10 подтверждённых фактов, почему не работает простое, каноничное решение, развилка A–E, **исправленные ошибки**, карта файлов, открытые вопросы | ✅ **Актуально — начинать отсюда** |
|
| `CHAT_RESUME_2026-09-27_vm_in_pipeline.md` | **Актуальная линия: добавляем ВМ (услуги 26 vApp + 28 ВМ) в пайплайн.** Всё сделанное (freeze/adopt, 2.0.19→2.0.23, пример, доки), состояние стендов и облака на 27.09, точки входа по ВМ, известные проблемы, развилки | ✅ **Актуально — начинать отсюда** |
|
||||||
|
| `CHAT_RESUME_2026-09-25_shturval_freeze_state.md` | Состояние Штурвал/freeze на 25.09: диагностика стенда, режимы destroy, релиз 2.0.23, схема публикаций | ⚠️ История (всё вошло в резюме от 27.09) |
|
||||||
|
| `CHAT_RESUME_2026-09-24_shturval_freeze.md` | Adopt кластера + дизайн freeze-on-destroy | ⚠️ История |
|
||||||
|
| `CHAT_RESUME_IAC_2026-09-24.md` | Линия IaC + `modify` (Штурвал): факты, развилка A–E, карта файлов | ⚠️ История |
|
||||||
| `CHAT_RESUME_2026-09-22.md` | Предыдущая сессия (дата в имени) | ⚠️ История |
|
| `CHAT_RESUME_2026-09-22.md` | Предыдущая сессия (дата в имени) | ⚠️ История |
|
||||||
| `CHAT_RESUME_2026-09-21.md` | Предыдущая сессия | ⚠️ История |
|
| `CHAT_RESUME_2026-09-21.md` | Предыдущая сессия | ⚠️ История |
|
||||||
| `CHAT_RESUME_2026-09-20.md` | Предыдущая сессия | ⚠️ История |
|
| `CHAT_RESUME_2026-09-20.md` | Предыдущая сессия | ⚠️ История |
|
||||||
@@ -17,8 +20,8 @@
|
|||||||
|
|
||||||
## Как пользоваться
|
## Как пользоваться
|
||||||
|
|
||||||
1. Открыть `CHAT_RESUME_IAC_2026-09-24.md` — это сводка всей линии по IaC.
|
1. Открыть `CHAT_RESUME_2026-09-27_vm_in_pipeline.md` — это сводка всей текущей линии (и всё, что уже сделано ранее).
|
||||||
2. Внутри него есть ссылки на детальные документы (`../30_analysis/*`, `../20_prompts/*`).
|
2. Внутри него есть ссылки на детальные документы (`../30_analysis/*`, `../10_plans/*`, `../20_prompts/*`).
|
||||||
3. Устаревшие рестюме **не удалять** — они фиксируют состояние на свою дату (полезно для хронологии).
|
3. Устаревшие рестюме **не удалять** — они фиксируют состояние на свою дату (полезно для хронологии).
|
||||||
|
|
||||||
## Важно про даты
|
## Важно про даты
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
name: vc_nsxt
|
||||||
|
service_id: 22
|
||||||
|
service_display_name: Сетевой шлюз периметра (Edge)
|
||||||
|
service_short_name: vc_nsxt
|
||||||
|
service_man: '# Инструкция по развертыванию Сетевой шлюз периметра (Edge) через платформу<br/><br/>## 1. Общая информация<br/><br/>Сервис **Сетевой шлюз периметра (Edge)** предназначен для создания периметрового сетевого шлюза в рамках одного виртуального датацентра (vDC) или группы виртуальных датацентров (groupvDC). <br/>При создании автоматически разворачивается routed-сеть с адресным пространством `10.10.102.0/24`. <br/>Сервис обеспечивает сетевую изоляцию, маршрутизацию, а также может включать функциональность балансировщика нагрузки AVI (ALB) для последующей интеграции, включая поддержку кластеров Штурвал.<br/><br/>### Доступные операции<br/><br/>**create** — Создание нового Edge с привязкой к vDC или groupvDC и развёртыванием routed-сети. <br/>**delete** — Удаление ранее созданного Edge. Недоступно при наличии зависимых услуг (vApp, VM, Кластер Штурвал). <br/>**modify** — Изменение параметров и сетевых настроек существующего Edge.<br/><br/>---<br/><br/>## 2. Параметры развертывания<br/><br/>Ниже приведены параметры операции **create**.<br/><br/>### Тип родительской услуги<br/>Определяет контекст размещения Edge. <br/>Допустимые значения: `vdc`, `groupvdc`. <br/>Использование:<br/>- При выборе `vdc` обязателен параметр **UUID VDC**.<br/>- При выборе `groupvdc` обязателен параметр **UUID Группы VDC**.<br/><br/>### UUID VDC<br/>Идентификатор виртуального датацентра. <br/>Необходимо предварительно создать vDC через услугу «Виртуальный датацентр (vDC)». <br/>Указывается только при выборе родительского типа `vdc`.<br/><br/>### UUID Группы VDC<br/>Идентификатор группы виртуальных датацентров. <br/>Создаётся через услугу «Группа виртуальных датацентров (groupvDC)». <br/>Используется при выборе родительского типа `groupvdc`.<br/><br/>### Включить ALB<br/>Флаг активации AVI Load Balancer в Cloud Director. <br/>Пример значения: `true`/`false`. <br/>Нужен для развёртывания кластера Штурвал и возможности создания Virtual Services.<br/><br/>### Наименование segroup<br/>Имя сервиса групп (segroup) на уровне ресурсной платформы. <br/>Обязательно при включённом ALB. <br/>Определяет группу, в которой будут выделяться пулы AVI для Virtual Services.<br/><br/>### virtualServicesCount<br/>Количество резервируемых виртуальных сервисов (Virtual Services) на AVI. <br/>Типичное значение: 1–10 в зависимости от нагрузки.<br/><br/>---<br/><br/>## 3. Рекомендованные характеристики<br/><br/>### Тестовое окружение (Test)<br/><br/>- Тип родительской услуги: `vdc` <br/>- virtualServicesCount: 1–2 <br/>- ALB: выключен по умолчанию (включать только при необходимости тестирования Штурвала) <br/>- segroup: задаётся только при включённом ALB <br/>- VDC гарантии CPU/RAM: минимальные (поскольку изменить их после создания нельзя)<br/><br/>### Промышленное окружение (Production)<br/><br/>- Тип родительской услуги: `groupvdc` при работе с распределёнными нагрузками или `vdc` для локализованных проектов <br/>- virtualServicesCount: 3–10, исходя из предполагаемого количества публикаций <br/>- ALB: включён, если требуется высокая доступность сервисов или используется кластер Штурвал <br/>- segroup: рекомендуется использовать выделенную группу под проект <br/>- VDC гарантии CPU/RAM: повышенные, с учётом того, что изменить их после создания невозможно без пересоздания vDC<br/><br/>---<br/><br/>## 4. Выходные параметры<br/><br/>### Имя Edge<br/>Уникальное название созданного сетевого шлюза, используется для ссылок и дальнейших операций.<br/><br/>### Имя routed-сети<br/>Автоматически созданная routed-сеть с подсетью `10.10.102.0/24`, используется для подключения ресурсов.<br/><br/>### Параметры Edge<br/>Набор параметров, указанных пользователем при создании: тип услуги, ALB, segroup, virtualServicesCount и др. <br/>Используются в операциях modify и для анализа состояния сервиса.<br/><br/>---<br/><br/>## 5. Дополнительная информация<br/><br/>- В рамках сервиса **один раз при создании задаётся гарантированная доля CPU/RAM vDC**. Изменить её невозможно — требуется пересоздание vDC. <br/>- Удаление Edge недоступно при наличии зависимых ресурсов (vApp, VM, кластер Штурвал). <br/>- При использовании ALB необходимо корректно указывать segroup, чтобы обеспечить корректное выделение пулов AVI. <br/>- При выборе режима `groupvdc` следует учитывать распределение нагрузки между несколькими vDC. <br/>- SNAT для всех ресурсов внутри Edge можно активировать через операцию modify (параметры выделения VIP и ipSpace).<br/><br/>'
|
||||||
|
lifecycle:
|
||||||
|
suspend_on_destroy_default: false
|
||||||
|
adopt_existing_on_create_default: false
|
||||||
|
outputs:
|
||||||
|
params:
|
||||||
|
- code: state_params
|
||||||
|
type: map
|
||||||
|
- code: state_out
|
||||||
|
type: map
|
||||||
|
- code: state_params_flat
|
||||||
|
type: map
|
||||||
|
- code: state_out_flat
|
||||||
|
type: map
|
||||||
|
- code: vault_secrets
|
||||||
|
type: map
|
||||||
|
sensitive: true
|
||||||
|
- code: vault_url
|
||||||
|
type: string
|
||||||
|
- code: vault_user_path
|
||||||
|
type: string
|
||||||
|
- code: vault_fields
|
||||||
|
type: list
|
||||||
|
operations:
|
||||||
|
- name: create
|
||||||
|
id: 10
|
||||||
|
kind: instance
|
||||||
|
action: create
|
||||||
|
params:
|
||||||
|
- id: 8
|
||||||
|
code: vdcUid
|
||||||
|
data_type: string
|
||||||
|
required: false
|
||||||
|
ref_svc_id: 21
|
||||||
|
descr: UUID Услуги `Виртуальный датацентр (vDC)`
|
||||||
|
man: 'Необходимо, если выбран тип подключаемой VDC: `vdc`'
|
||||||
|
sort: 10
|
||||||
|
- id: 340
|
||||||
|
code: needEnableAVI
|
||||||
|
data_type: boolean
|
||||||
|
required: true
|
||||||
|
default: "false"
|
||||||
|
value_list:
|
||||||
|
- "false"
|
||||||
|
- "true"
|
||||||
|
descr: Включение Load Balancer
|
||||||
|
man: Параметры `Наименование segroup`, `Кол-во VS на AVI` необходимо также указать
|
||||||
|
sort: 30
|
||||||
|
is_modifiable: true
|
||||||
|
- id: 341
|
||||||
|
code: virtualServicesCount
|
||||||
|
data_type: integer > 0
|
||||||
|
required: false
|
||||||
|
default: "1"
|
||||||
|
maxvalue: 4
|
||||||
|
minvalue: 1
|
||||||
|
descr: Кол-во виртуальных сервисов, которые **резервируются** на AVI
|
||||||
|
man: Во избежании коллапса пока выделяется до 4<br/>Необходимо указывать, если включён параметр `Включить ALB`
|
||||||
|
sort: 50
|
||||||
|
is_modifiable: true
|
||||||
|
- id: 621
|
||||||
|
code: vdcType
|
||||||
|
data_type: string
|
||||||
|
required: true
|
||||||
|
default: vdc
|
||||||
|
value_list:
|
||||||
|
- vdc
|
||||||
|
- vdcGroup
|
||||||
|
descr: Тип родительской услуги
|
||||||
|
man: При выборе vdc обеспечивает сетевую доступность только в рамках этого vdc<br/>При выборе groupvdc обеспечивает сетевую доступность между всеми vdc, которые включены в groupvdc
|
||||||
|
sort: 0
|
||||||
|
- id: 622
|
||||||
|
code: vdcGroupUid
|
||||||
|
data_type: string
|
||||||
|
required: false
|
||||||
|
ref_svc_id: 29
|
||||||
|
descr: UUID Услуги `Группа виртуальных датацентров (groupvDC)`
|
||||||
|
man: 'Необходимо, если выбран тип подключаемой VDC: `groupvdc`'
|
||||||
|
sort: 20
|
||||||
|
- id: 825
|
||||||
|
code: qosProfile
|
||||||
|
required: false
|
||||||
|
descr: Параметр пока не работает<br/>Должен возвращать поле из ресурсной платформы типа vc из .vcd.hardware.gatewayQoSProfiles<br/><br/>Как по идее должен работать.<br/>Нужно заполнить или CFS vdcUid, или vdcGroupUid<br/>Идеально конечно проверять `if (params.vdcType == "vdc" && params.vdcUid != "")` или `if (params.vdcType == "vdcGroup" && params.vdcGroupUid != "")`<br/><br/>Далее надо пойти по стейту vdc -> org -> resPlatform, взять gatewayQoSProfiles<br/>Или пойти по стейту vdcgroup -> vdc -> org -> resPlatform, взять gatewayQoSProfiles<br/><br/>Регулярку могу написать (Виталя)
|
||||||
|
man: if (vdcUid != '") {<br/> наборфункций1<br/>} <br/><br/>elif (vdcGroupUid != "") {<br/> наборфункций2<br/>}<br/><br/>else {<br/> return "Необходимо выбрать vdc или vdcgroup"<br/>}
|
||||||
|
sort: 60
|
||||||
|
depends_on: vdcUid,vdcGroupUid
|
||||||
|
is_modifiable: true
|
||||||
|
- id: 1110
|
||||||
|
code: routedNetConfiguration
|
||||||
|
data_type: map-fixed
|
||||||
|
required: true
|
||||||
|
sort: 70
|
||||||
|
is_modifiable: true
|
||||||
|
sub_params:
|
||||||
|
- id: 649
|
||||||
|
code: ipAddrPool
|
||||||
|
data_type: string
|
||||||
|
required: true
|
||||||
|
default: 10.10.102.0/24
|
||||||
|
regex: ^((25[0-4]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?|0)\.){3}0/24$
|
||||||
|
man: '**Параметр не изменяемый**<br/>Адрессный пул, которая будет присвоена routed-сети. Описывается как (10.10.10.0/24). Маска 24 обязательна. (.1) - шлюз. (.2-.254) - Под адресацию для ВМ'
|
||||||
|
is_modifiable: false
|
||||||
|
- id: 650
|
||||||
|
code: mainDns
|
||||||
|
data_type: string
|
||||||
|
required: true
|
||||||
|
default: 81.22.46.22
|
||||||
|
regex: ^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?|0)\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?|0)$
|
||||||
|
man: Подставляется автоматически в resolv.conf при создании виртуальных машин
|
||||||
|
is_modifiable: false
|
||||||
|
- id: 651
|
||||||
|
code: secondDns
|
||||||
|
data_type: string
|
||||||
|
required: true
|
||||||
|
default: 185.247.187.77
|
||||||
|
regex: ^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?|0)\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?|0)$
|
||||||
|
man: Подставляется автоматически в resolv.conf при создании виртуальных машин
|
||||||
|
is_modifiable: false
|
||||||
|
- name: delete
|
||||||
|
id: 25
|
||||||
|
kind: instance
|
||||||
|
action: delete
|
||||||
|
man: Невозможно удалить если routed-сеть в Edge связана с:<br/>- vApp<br/>- VM<br/>- Kubernetes-кластер Штурвал<br/><br/>Запустить операцию возможно, но будет ошибка
|
||||||
|
params: []
|
||||||
|
- name: modify
|
||||||
|
id: 111
|
||||||
|
kind: instance
|
||||||
|
action: modify
|
||||||
|
man: Для создания SNAT Правила убедитесь, что в организации есть свободные IP<br/>Редактировать кол-во свободных IP можно в услуге `Организация в Cloud Director` -> `modify`
|
||||||
|
params:
|
||||||
|
- id: 368
|
||||||
|
code: needEnableAVI
|
||||||
|
data_type: boolean
|
||||||
|
required: false
|
||||||
|
value_list:
|
||||||
|
- "false"
|
||||||
|
- "true"
|
||||||
|
descr: Включение Load Balancer
|
||||||
|
man: Параметры `Наименование segroup`, `Кол-во VS на AVI` необходимо также указать
|
||||||
|
sort: 10
|
||||||
|
- id: 369
|
||||||
|
code: virtualServicesCount
|
||||||
|
data_type: integer > 0
|
||||||
|
required: false
|
||||||
|
maxvalue: 4
|
||||||
|
minvalue: 1
|
||||||
|
descr: Кол-во виртуальных сервисов, которые выделяются на AVI
|
||||||
|
man: Во избежании коллапса пока выделяется до 4<br/>Необходимо указывать, если включён параметр `Включить ALB`
|
||||||
|
sort: 30
|
||||||
|
- id: 372
|
||||||
|
code: ipSpaceName
|
||||||
|
data_type: string
|
||||||
|
required: false
|
||||||
|
descr: Имя ip Space для внешнего IP
|
||||||
|
man: Необходимо указывать, если включён параметр `Выделить VIP для SNAT`
|
||||||
|
sort: 50
|
||||||
|
- id: 856
|
||||||
|
code: qosProfile
|
||||||
|
data_type: string
|
||||||
|
required: false
|
||||||
|
sort: 40
|
||||||
|
- id: 1112
|
||||||
|
code: routedNetConfiguration
|
||||||
|
data_type: map-fixed
|
||||||
|
required: true
|
||||||
|
sort: 70
|
||||||
|
sub_params:
|
||||||
|
- id: 652
|
||||||
|
code: ipAddrPool
|
||||||
|
data_type: string
|
||||||
|
required: true
|
||||||
|
default: ""
|
||||||
|
regex: ^((25[0-4]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?|0)\.){3}0/24$
|
||||||
|
man: '**Параметр не изменяемый**<br/>Адрессный пул, которая будет присвоена routed-сети. Описывается как (10.10.10.0/24). Маска 24 обязательна. (.1) - шлюз. (.2-.254) - Под адресацию для ВМ'
|
||||||
|
is_modifiable: false
|
||||||
|
- id: 653
|
||||||
|
code: mainDns
|
||||||
|
data_type: string
|
||||||
|
required: true
|
||||||
|
default: ""
|
||||||
|
regex: ^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?|0)\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?|0)$
|
||||||
|
man: Подставляется автоматически в resolv.conf при создании виртуальных машин
|
||||||
|
is_modifiable: false
|
||||||
|
- id: 654
|
||||||
|
code: secondDns
|
||||||
|
data_type: string
|
||||||
|
required: true
|
||||||
|
default: ""
|
||||||
|
regex: ^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?|0)\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?|0)$
|
||||||
|
man: Подставляется автоматически в resolv.conf при создании виртуальных машин
|
||||||
|
is_modifiable: false
|
||||||
|
- name: reconcile
|
||||||
|
id: 252
|
||||||
|
kind: action
|
||||||
|
action: reconcile
|
||||||
|
params: []
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
resource "nubes_vc_nsxt" "edge" {
|
||||||
|
resource_name = var.nsxt_resource_name
|
||||||
|
|
||||||
|
# Тип родительской услуги: "vdc" (нужен vdc_uid) или "vdcGroup" (нужен vdc_group_uid)
|
||||||
|
vdc_type = var.nsxt_vdc_type
|
||||||
|
|
||||||
|
# refSvc-поле: принимает UUID или имя. Здесь берём UID созданного VDC,
|
||||||
|
# чтобы Edge гарантированно создавался после vDC.
|
||||||
|
vdc_uid = nubes_vc_vdc.vdc.id
|
||||||
|
|
||||||
|
need_enable_avi = var.nsxt_need_enable_avi
|
||||||
|
virtual_services_count = var.nsxt_virtual_services_count
|
||||||
|
|
||||||
|
# routed-сеть, которую разворачивает Edge (SingleNestedAttribute -> объект)
|
||||||
|
routed_net_configuration = {
|
||||||
|
ip_addr_pool = var.nsxt_ip_addr_pool
|
||||||
|
main_dns = var.nsxt_main_dns
|
||||||
|
second_dns = var.nsxt_second_dns
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,583 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
const Instance = `package resources_gen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
{{- if .NeedsStringsImport }}
|
||||||
|
"strings"
|
||||||
|
{{- end }}
|
||||||
|
{{- if .NeedsFmtImport }}
|
||||||
|
"fmt"
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
"terraform-provider-nubes/internal/core"
|
||||||
|
"terraform-provider-nubes/internal/resources_core"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
|
||||||
|
{{- if .NeedsInt64Default }}
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
|
||||||
|
{{- end }}
|
||||||
|
{{- if .NeedsStringDefault }}
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
|
||||||
|
{{- end }}
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||||
|
{{- if .NeedsBoolUseStateForUnknown }}
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
|
||||||
|
{{- end }}
|
||||||
|
{{- if .NeedsInt64UseStateForUnknown }}
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
|
||||||
|
{{- end }}
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Code generated by TOOLS/resource-generator. DO NOT EDIT.
|
||||||
|
// Service: {{.Name}}
|
||||||
|
// Service ID: {{.ServiceID}}
|
||||||
|
|
||||||
|
var _ resource.Resource = &{{ToCamel .Name}}Resource{}
|
||||||
|
var _ resource.ResourceWithModifyPlan = &{{ToCamel .Name}}Resource{}
|
||||||
|
var _ resource.ResourceWithImportState = &{{ToCamel .Name}}Resource{}
|
||||||
|
|
||||||
|
type {{ToCamel .Name}}Resource struct {
|
||||||
|
client *core.UniversalClient
|
||||||
|
}
|
||||||
|
|
||||||
|
{{- range .SchemaParams }}
|
||||||
|
{{- if (IsNested .) }}
|
||||||
|
// {{NestedModelName $.Name .Code}} — вложенная модель для map-fixed параметра {{.Code}}.
|
||||||
|
type {{NestedModelName $.Name .Code}} struct {
|
||||||
|
{{- range .SubParams }}
|
||||||
|
{{ToCamel .Code}} {{ParamType .}} ` + "`" + `tfsdk:"{{ToSnake .Code}}" json:"{{.Code}}"` + "`" + `
|
||||||
|
{{- end }}
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
type {{ToCamel .Name}}Model struct {
|
||||||
|
ID types.String ` + "`" + `tfsdk:"id"` + "`" + `
|
||||||
|
ResourceName types.String ` + "`" + `tfsdk:"resource_name"` + "`" + `
|
||||||
|
OperationTimeout types.String ` + "`" + `tfsdk:"operation_timeout"` + "`" + `
|
||||||
|
LogLevel types.String ` + "`" + `tfsdk:"log_level"` + "`" + `
|
||||||
|
{{- if .HasRedeploy }}
|
||||||
|
// --- Redeploy support (ARCHITECTURE.md) ---
|
||||||
|
// git_revision: при изменении вызывает redeploy вместо modify.
|
||||||
|
// Опциональное поле — если не задано, modify работает как обычно.
|
||||||
|
GitRevision types.String ` + "`" + `tfsdk:"git_revision"` + "`" + `
|
||||||
|
{{- end }}
|
||||||
|
{{- range .SchemaParams }}
|
||||||
|
{{- if (IsNested .) }}
|
||||||
|
{{ToCamel .Code}} {{NestedTfType . $.Name}} ` + "`" + `tfsdk:"{{ToSnake .Code}}"` + "`" + `
|
||||||
|
{{- else }}
|
||||||
|
{{ToCamel .Code}} {{ParamType .}} ` + "`" + `tfsdk:"{{ToSnake .Code}}"` + "`" + `
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .SupportsSuspendDestroy }}
|
||||||
|
SuspendOnDestroy types.Bool ` + "`" + `tfsdk:"suspend_on_destroy"` + "`" + `
|
||||||
|
{{- end }}
|
||||||
|
AdoptExistingOnCreate types.Bool ` + "`" + `tfsdk:"adopt_existing_on_create"` + "`" + `
|
||||||
|
{{- range .OutputParams }}
|
||||||
|
{{ToCamel .Code}} {{OutputType .}} ` + "`" + `tfsdk:"{{ToSnake .Code}}"` + "`" + `
|
||||||
|
{{- end }}
|
||||||
|
}
|
||||||
|
|
||||||
|
func New{{ToCamel .Name}}Resource() resource.Resource {
|
||||||
|
return &{{ToCamel .Name}}Resource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *{{ToCamel .Name}}Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_{{.Name}}"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *{{ToCamel .Name}}Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||||
|
attrs := map[string]schema.Attribute{
|
||||||
|
"id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}},
|
||||||
|
"resource_name": schema.StringAttribute{Required: true},
|
||||||
|
"operation_timeout": schema.StringAttribute{Optional: true},
|
||||||
|
"log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."},
|
||||||
|
{{- range .SchemaParams }}
|
||||||
|
{{- if (IsNested .) }}
|
||||||
|
"{{ToSnake .Code}}": {{NestedSchemaBlock .}}
|
||||||
|
{{- range .SubParams }}
|
||||||
|
"{{ToSnake .Code}}": schema.{{SubSchemaType .}}Attribute{
|
||||||
|
{{- if .Default }}Optional: true, Computed: true, Default: {{SubDefaultExpr .}},{{else if .Required}}Required: true,{{else}}Optional: true,{{end}}
|
||||||
|
{{- if ne (ParamDescription .) "" }}MarkdownDescription: {{ParamDescription .}},{{end}}
|
||||||
|
},
|
||||||
|
{{- end }}
|
||||||
|
{{NestedSchemaEnd .}}
|
||||||
|
{{- else }}
|
||||||
|
"{{ToSnake .Code}}": schema.{{if eq (ParamType .) "types.Bool"}}Bool{{else if eq (ParamType .) "types.Int64"}}Int64{{else}}String{{end}}Attribute{
|
||||||
|
{{- if and .Required (eq (ParamDefaultExpr .) "") (eq .RefSvcId 0) }}Required: true,{{else}}Optional: true,{{end}}
|
||||||
|
{{- if ne (ParamDefaultExpr .) "" }}Computed: true, Default: {{ParamDefaultExpr .}},{{- else if or .IsJson (ShouldBeOptionalComputed .) }}Computed: true,{{- end }}
|
||||||
|
{{- if ne (ParamDescription .) "" }}MarkdownDescription: {{ParamDescription .}},{{end}}
|
||||||
|
{{- if .Sensitive }}Sensitive: true,{{end}}
|
||||||
|
{{- if .IsJson }}PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()},{{- else if ShouldUseStateForUnknown . }}PlanModifiers: []planmodifier.{{if eq (ParamType .) "types.Bool"}}Bool{boolplanmodifier.UseStateForUnknown()}{{else if eq (ParamType .) "types.Int64"}}Int64{int64planmodifier.UseStateForUnknown()}{{else}}String{stringplanmodifier.UseStateForUnknown()}{{end}},{{- end }}
|
||||||
|
},
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .HasRedeploy }}
|
||||||
|
// --- Redeploy support (ARCHITECTURE.md) ---
|
||||||
|
// При изменении git_revision вызывается redeploy вместо modify.
|
||||||
|
// Если не задано — modify работает без изменений.
|
||||||
|
"git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."},
|
||||||
|
{{- end }}
|
||||||
|
{{- if .SupportsSuspendDestroy }}
|
||||||
|
"suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool({{.SuspendOnDestroy}})},
|
||||||
|
{{- end }}
|
||||||
|
"adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool({{.AdoptExistingOnCreate}})},
|
||||||
|
{{- range .OutputParams }}
|
||||||
|
{{- if or (OutputIsMap .) (OutputIsList .) }}
|
||||||
|
"{{ToSnake .Code}}": schema.{{if OutputIsMap .}}Map{{else}}List{{end}}Attribute{Computed: true, ElementType: types.StringType{{if OutputSensitive .}}, Sensitive: true{{end}}},
|
||||||
|
{{- else }}
|
||||||
|
"{{ToSnake .Code}}": schema.StringAttribute{Computed: true{{if OutputSensitive .}}, Sensitive: true{{end}}},
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Schema = schema.Schema{Attributes: attrs}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *{{ToCamel .Name}}Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
|
||||||
|
if r.client == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var config *{{ToCamel .Name}}Model
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if config == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var state *{{ToCamel .Name}}Model
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Destroy-план (plan == null): create-time проверку «уже существует / adopt»
|
||||||
|
// запускать нельзя — удаление не валидируется через существование инстанса.
|
||||||
|
if req.Plan.Raw.IsNull() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
|
||||||
|
var plan {{ToCamel .Name}}Model
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
{{- if .HasRefSvcParams }}
|
||||||
|
// FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
|
||||||
|
// Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
|
||||||
|
// Resolve (displayName → UUID или uppercase → lowercase) нужен только для
|
||||||
|
// API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
|
||||||
|
// "Provider produced invalid plan: planned value does not match config value".
|
||||||
|
{{- end }}
|
||||||
|
if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
|
||||||
|
if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
|
||||||
|
resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{{- range .CreateOnlyParams }}
|
||||||
|
{{- if not (IsNested .) }}
|
||||||
|
if !plan.{{ToCamel .Code}}.IsNull() && !plan.{{ToCamel .Code}}.IsUnknown() && !state.{{ToCamel .Code}}.IsNull() && !state.{{ToCamel .Code}}.IsUnknown() {
|
||||||
|
{{- if eq (ParamType .) "types.Bool" }}
|
||||||
|
if plan.{{ToCamel .Code}}.ValueBool() != state.{{ToCamel .Code}}.ValueBool() {
|
||||||
|
resp.Diagnostics.AddError("Нельзя изменить {{ToSnake .Code}}", "Параметр задается при создании и не может быть изменен.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
{{- else if eq (ParamType .) "types.Int64" }}
|
||||||
|
if plan.{{ToCamel .Code}}.ValueInt64() != state.{{ToCamel .Code}}.ValueInt64() {
|
||||||
|
resp.Diagnostics.AddError("Нельзя изменить {{ToSnake .Code}}", "Параметр задается при создании и не может быть изменен.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
{{- else }}
|
||||||
|
// FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
|
||||||
|
// в lowercase, пользователь мог написать upper/mixed. Это одно и то же
|
||||||
|
// значение, менять его нельзя только если оно реально другое.
|
||||||
|
if !strings.EqualFold(plan.{{ToCamel .Code}}.ValueString(), state.{{ToCamel .Code}}.ValueString()) {
|
||||||
|
resp.Diagnostics.AddError("Нельзя изменить {{ToSnake .Code}}", "Параметр задается при создании и не может быть изменен.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
{{- range .SchemaParams }}
|
||||||
|
{{- if and .Required (eq (ParamDefaultExpr .) "") (not (IsNested .)) }}
|
||||||
|
if config.{{ToCamel .Code}}.IsNull() {
|
||||||
|
resp.Diagnostics.AddError("Missing required attribute", "{{ToSnake .Code}} is required.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if config.{{ToCamel .Code}}.IsUnknown() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
// ⛔ Create-time проверка существования/усыновления здесь СОЗНАТЕЛЬНО НЕ вызывается.
|
||||||
|
//
|
||||||
|
// Причина: при tainted-ресурсе Terraform планирует ЗАМЕНУ (destroy+create), и
|
||||||
|
// create-узел замены приходит в ModifyPlan с prior state = null — ровно как у
|
||||||
|
// нового ресурса. Отличить «замену» от «создания» на этом уровне невозможно,
|
||||||
|
// поэтому проверка «РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ» ложно срабатывала на
|
||||||
|
// ещё не удалённый инстанс и блокировала plan/destroy.
|
||||||
|
//
|
||||||
|
// Проверка осталась в Create (CreateExistingResourceDiagnosticsWithDomainAndServices):
|
||||||
|
// на apply она выполняется ПОСЛЕ удаления старого инстанса, поэтому конфликта уже нет.
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *{{ToCamel .Name}}Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||||
|
var data {{ToCamel .Name}}Model
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
{{- range .SchemaParams }}
|
||||||
|
{{- if and .Required (eq (ParamDefaultExpr .) "") (not (IsNested .)) }}
|
||||||
|
if data.{{ToCamel .Code}}.IsNull() || data.{{ToCamel .Code}}.IsUnknown() {
|
||||||
|
resp.Diagnostics.AddError("Missing required attribute", "{{ToSnake .Code}} is required.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .HasRefSvcParams }}
|
||||||
|
{{- range .CreateParams }}
|
||||||
|
{{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }}
|
||||||
|
// refSvc-поле резолвим только для API-запроса.
|
||||||
|
// config не перезаписываем: пользовательский display name или UUID должен
|
||||||
|
// пройти в state ровно в том виде, в котором его передал Terraform.
|
||||||
|
resolved{{ToCamel .Code}} := data.{{ToCamel .Code}}.ValueString()
|
||||||
|
if !data.{{ToCamel .Code}}.IsNull() && !data.{{ToCamel .Code}}.IsUnknown() {
|
||||||
|
var err error
|
||||||
|
resolved{{ToCamel .Code}}, err = r.client.ResolveRefSvcParamValue(ctx, {{.RefSvcId}}, data.{{ToCamel .Code}}.ValueString())
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
resourceName := data.ResourceName.ValueString()
|
||||||
|
desiredDomain := ""
|
||||||
|
{{- if .HasDomainParam }}
|
||||||
|
if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
|
||||||
|
desiredDomain = data.Domain.ValueString()
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
domainServiceIDs := []int{ {{- range .DomainServiceIDs }}{{.}}, {{- end }} }
|
||||||
|
resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, {{.ServiceID}}, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs, {{.SupportsSuspendDestroy}})...)
|
||||||
|
// ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
|
||||||
|
// not created, конфликт) сайд-эффект create не должен выполняться.
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
params := map[int]string{
|
||||||
|
{{- range .CreateParams }}
|
||||||
|
{{- if not (IsNested .) }}
|
||||||
|
{{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }}
|
||||||
|
{{.ID}}: resolved{{ToCamel .Code}},
|
||||||
|
{{- else }}
|
||||||
|
{{.ID}}: {{ParamFormat . (printf "data.%s" (ToCamel .Code))}},
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
}
|
||||||
|
{{- range .CreateParams }}
|
||||||
|
{{- if (IsNested .) }}
|
||||||
|
if data.{{ToCamel .Code}} != nil {
|
||||||
|
params[{{.ID}}] = {{NestedJSONExpr . "data"}}
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
operationTimeout := ""
|
||||||
|
if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
|
||||||
|
operationTimeout = data.OperationTimeout.ValueString()
|
||||||
|
}
|
||||||
|
if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
|
||||||
|
ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
|
||||||
|
}
|
||||||
|
id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, {{.ServiceID}}, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data.ID = types.StringValue(id)
|
||||||
|
|
||||||
|
state, diags := resources_core.RefreshResourceState(ctx, r.client, id, {{.ServiceID}}, data, []resources_core.StateField{
|
||||||
|
{{- range .OutputParams }}
|
||||||
|
{Code: "{{.Code}}"},
|
||||||
|
{{- end }}
|
||||||
|
}, []resources_core.InputField{
|
||||||
|
{{- range .SchemaParams }}
|
||||||
|
{{- if eq .RefSvcId 0 }}
|
||||||
|
{Code: "{{.Code}}", Field: "{{ToCamel .Code}}", Type: "{{.Type}}"},
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
})
|
||||||
|
resp.Diagnostics.Append(diags...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *{{ToCamel .Name}}Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||||
|
var state {{ToCamel .Name}}Model
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if state.ID.IsNull() || state.ID.IsUnknown() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.client != nil {
|
||||||
|
remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if remove {
|
||||||
|
resp.State.RemoveResource(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), {{.ServiceID}}, state, []resources_core.StateField{
|
||||||
|
{{- range .OutputParams }}
|
||||||
|
{Code: "{{.Code}}"},
|
||||||
|
{{- end }}
|
||||||
|
}, []resources_core.InputField{
|
||||||
|
{{- range .SchemaParams }}
|
||||||
|
{{- if eq .RefSvcId 0 }}
|
||||||
|
{Code: "{{.Code}}", Field: "{{ToCamel .Code}}", Type: "{{.Type}}"},
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
})
|
||||||
|
resp.Diagnostics.Append(diags...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *{{ToCamel .Name}}Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||||
|
var plan {{ToCamel .Name}}Model
|
||||||
|
var state {{ToCamel .Name}}Model
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
instanceID := state.ID
|
||||||
|
if instanceID.IsNull() || instanceID.IsUnknown() {
|
||||||
|
instanceID = plan.ID
|
||||||
|
}
|
||||||
|
if instanceID.IsNull() || instanceID.IsUnknown() {
|
||||||
|
resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hasServiceParamChanges := false
|
||||||
|
{{- range .ModifyParams }}
|
||||||
|
{{- if not (IsNested .) }}
|
||||||
|
if !hasServiceParamChanges {
|
||||||
|
if plan.{{ToCamel .Code}}.IsNull() != state.{{ToCamel .Code}}.IsNull() || plan.{{ToCamel .Code}}.IsUnknown() != state.{{ToCamel .Code}}.IsUnknown() {
|
||||||
|
hasServiceParamChanges = true
|
||||||
|
} else if !plan.{{ToCamel .Code}}.IsNull() && !plan.{{ToCamel .Code}}.IsUnknown() && !state.{{ToCamel .Code}}.IsNull() && !state.{{ToCamel .Code}}.IsUnknown() {
|
||||||
|
{{- if eq (ParamType .) "types.Bool" }}
|
||||||
|
if plan.{{ToCamel .Code}}.ValueBool() != state.{{ToCamel .Code}}.ValueBool() {
|
||||||
|
hasServiceParamChanges = true
|
||||||
|
}
|
||||||
|
{{- else if eq (ParamType .) "types.Int64" }}
|
||||||
|
if plan.{{ToCamel .Code}}.ValueInt64() != state.{{ToCamel .Code}}.ValueInt64() {
|
||||||
|
hasServiceParamChanges = true
|
||||||
|
}
|
||||||
|
{{- else }}
|
||||||
|
if !strings.EqualFold(plan.{{ToCamel .Code}}.ValueString(), state.{{ToCamel .Code}}.ValueString()) {
|
||||||
|
hasServiceParamChanges = true
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- if .HasRedeploy }}
|
||||||
|
// --- Redeploy support (ARCHITECTURE.md) ---
|
||||||
|
// Проверяем изменился ли git_revision.
|
||||||
|
// Если да — вызываем redeploy вместо modify.
|
||||||
|
// git_revision = "" (не задано) → обычный modify.
|
||||||
|
redeployRequested := false
|
||||||
|
if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() {
|
||||||
|
if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() {
|
||||||
|
redeployRequested = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
if !hasServiceParamChanges{{if .HasRedeploy}} && !redeployRequested{{end}} {
|
||||||
|
// Сервисные параметры не изменились → modify НЕ вызываем.
|
||||||
|
//
|
||||||
|
// НО read-back поля (state_params*, state_out*, vault_*) обязаны быть
|
||||||
|
// перечитаны из инстанса. Раньше здесь слепо копировались значения из
|
||||||
|
// старого state, из-за чего tfstate хранил устаревшее значение:
|
||||||
|
// пример — state_params["needEnableAVI"]="false", когда на платформе уже
|
||||||
|
// true (модификатор включил ALB). Это давало ложный дрейф
|
||||||
|
// «Objects have changed outside of Terraform» на каждом plan.
|
||||||
|
plan.ID = instanceID
|
||||||
|
refreshed, refreshDiags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), {{.ServiceID}}, plan, []resources_core.StateField{
|
||||||
|
{{- range .OutputParams }}
|
||||||
|
{Code: "{{.Code}}"},
|
||||||
|
{{- end }}
|
||||||
|
}, []resources_core.InputField{
|
||||||
|
{{- range .SchemaParams }}
|
||||||
|
{{- if eq .RefSvcId 0 }}
|
||||||
|
{Code: "{{.Code}}", Field: "{{ToCamel .Code}}", Type: "{{.Type}}"},
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
})
|
||||||
|
resp.Diagnostics.Append(refreshDiags...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &refreshed)...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
operationTimeout := ""
|
||||||
|
if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() {
|
||||||
|
operationTimeout = plan.OperationTimeout.ValueString()
|
||||||
|
}
|
||||||
|
if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() {
|
||||||
|
ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString())
|
||||||
|
}
|
||||||
|
|
||||||
|
{{- if .HasRedeploy }}
|
||||||
|
// --- Redeploy support (ARCHITECTURE.md) ---
|
||||||
|
// 1. Modify — только если изменились параметры сервиса
|
||||||
|
if hasServiceParamChanges {
|
||||||
|
{{- end }}
|
||||||
|
params := map[int]string{
|
||||||
|
{{- range .ModifyParams }}
|
||||||
|
{{- if not (IsNested .) }}
|
||||||
|
{{.ID}}: {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}},
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
}
|
||||||
|
{{- range .ModifyParams }}
|
||||||
|
{{- if (IsNested .) }}
|
||||||
|
if plan.{{ToCamel .Code}} != nil {
|
||||||
|
params[{{.ID}}] = {{NestedJSONExpr . "plan"}}
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
{{- if .HasRedeploy }}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Redeploy — только если изменился git_revision
|
||||||
|
if redeployRequested {
|
||||||
|
redeployParams := map[int]string{}
|
||||||
|
{{- range .RedeployParams }}
|
||||||
|
{{- if (IsNested .) }}
|
||||||
|
if plan.{{ToCamel .Code}} != nil {
|
||||||
|
redeployParams[{{.ID}}] = {{NestedJSONExpr . "plan"}}
|
||||||
|
}
|
||||||
|
{{- else }}
|
||||||
|
redeployParams[{{.ID}}] = {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout, redeployParams); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
plan.ID = instanceID
|
||||||
|
state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), {{.ServiceID}}, plan, []resources_core.StateField{
|
||||||
|
{{- range .OutputParams }}
|
||||||
|
{Code: "{{.Code}}"},
|
||||||
|
{{- end }}
|
||||||
|
}, []resources_core.InputField{
|
||||||
|
{{- range .SchemaParams }}
|
||||||
|
{{- if eq .RefSvcId 0 }}
|
||||||
|
{Code: "{{.Code}}", Field: "{{ToCamel .Code}}", Type: "{{.Type}}"},
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
})
|
||||||
|
resp.Diagnostics.Append(diags...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *{{ToCamel .Name}}Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||||
|
var state {{ToCamel .Name}}Model
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if state.ID.IsNull() || state.ID.IsUnknown() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
{{- if .SupportsSuspendDestroy }}
|
||||||
|
deleteMode := "state_only"
|
||||||
|
if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
|
||||||
|
deleteMode = "suspend"
|
||||||
|
}
|
||||||
|
{{- else }}
|
||||||
|
deleteMode := "delete"
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
operationTimeout := ""
|
||||||
|
if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() {
|
||||||
|
operationTimeout = state.OperationTimeout.ValueString()
|
||||||
|
}
|
||||||
|
if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() {
|
||||||
|
ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString())
|
||||||
|
}
|
||||||
|
if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *{{ToCamel .Name}}Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||||
|
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *{{ToCamel .Name}}Resource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client, ok := req.ProviderData.(*core.UniversalClient)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError("Error", "Invalid client type")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.client = client
|
||||||
|
}
|
||||||
|
`
|
||||||
@@ -0,0 +1,473 @@
|
|||||||
|
// Package loader — загрузка YAML-спеков и построение GenResource/GenSubresource/GenAction.
|
||||||
|
//
|
||||||
|
// LoadSpecs — главная функция. Для каждого YAML:
|
||||||
|
// 1. Парсит операции (create/modify/delete/suspend/resume/...)
|
||||||
|
// 2. Классифицирует их на instance/subresource/action
|
||||||
|
// 3. Сливает параметры, вычисляет CreateOnly и ForceNew
|
||||||
|
// 4. Строит GenResource/GenSubresource/GenAction
|
||||||
|
// 5. Сортирует результат по имени сервиса
|
||||||
|
//
|
||||||
|
// ValidateSpec — fail-fast валидация (паника при неизвестном Kind).
|
||||||
|
package loader
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"resource-generator/internal/params"
|
||||||
|
"resource-generator/internal/types"
|
||||||
|
"tf-tools/lib"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadSpecs загружает все YAML-спеки из директории и строит модели всех ресурсов.
|
||||||
|
func LoadSpecs(dir string) ([]types.GenResource, []types.GenSubresource, []types.GenAction, []types.GenModifier, error) {
|
||||||
|
var services []types.GenResource
|
||||||
|
var subs []types.GenSubresource
|
||||||
|
var actions []types.GenAction
|
||||||
|
var modifiers []types.GenModifier
|
||||||
|
domainServiceIDsSet := map[int]struct{}{}
|
||||||
|
walkErr := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if d.IsDir() || !strings.HasSuffix(d.Name(), ".yaml") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var spec types.ServiceSpec
|
||||||
|
if err := yaml.Unmarshal(b, &spec); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// P1.4: fail-fast на неизвестных kind'ах и отсутствующих обязательных полях.
|
||||||
|
if err := ValidateSpec(path, &spec); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
createParams := []types.Param{}
|
||||||
|
modifyParams := []types.Param{}
|
||||||
|
supportsSuspendDestroy := false
|
||||||
|
for _, op := range spec.Operations {
|
||||||
|
if op.Kind == "modifier" {
|
||||||
|
name := strings.TrimSpace(op.Modifier)
|
||||||
|
if name == "" {
|
||||||
|
name = strings.TrimSpace(op.Action)
|
||||||
|
}
|
||||||
|
modifier := types.GenModifier{
|
||||||
|
ServiceName: spec.Name,
|
||||||
|
ServiceID: spec.ServiceID,
|
||||||
|
ModifierName: name,
|
||||||
|
OperationName: op.Action,
|
||||||
|
Params: ConvertParams(op.Params),
|
||||||
|
DeleteStrategy: normalizeDeleteStrategy(op.DeleteStrategy),
|
||||||
|
Idempotency: normalizeIdempotency(op.Idempotency),
|
||||||
|
}
|
||||||
|
modifier.DeleteParams = convertDeleteParams(op.DeleteParams)
|
||||||
|
modifier.SchemaParams = modifier.Params
|
||||||
|
for idx := range modifier.SchemaParams {
|
||||||
|
if modifier.SchemaParams[idx].HasSubParams {
|
||||||
|
modifier.SchemaParams[idx].IsJson = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
params.Analyze(&modifier.UsesBool, &modifier.UsesInt64, &modifier.UsesString, &modifier.HasDefaults, &modifier.NeedsBoolDefault, &modifier.NeedsInt64Default, &modifier.NeedsStringDefault, modifier.SchemaParams)
|
||||||
|
modifier.NeedsJsonPlanMod = params.AnalyzeJsonPlanMod(modifier.SchemaParams)
|
||||||
|
modifiers = append(modifiers, modifier)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if op.Kind != "instance" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch op.Action {
|
||||||
|
case "create":
|
||||||
|
createParams = ConvertParams(op.Params)
|
||||||
|
case "modify":
|
||||||
|
modifyParams = ConvertParams(op.Params)
|
||||||
|
case "suspend":
|
||||||
|
supportsSuspendDestroy = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
schemaParams := params.Merge(createParams, modifyParams)
|
||||||
|
createParams = params.AlignParamTypes(createParams, schemaParams)
|
||||||
|
modifyParams = params.AlignParamTypes(modifyParams, schemaParams)
|
||||||
|
createOnly := params.ComputeCreateOnly(createParams, modifyParams)
|
||||||
|
schemaParams = params.MarkCreateOnly(schemaParams, createOnly)
|
||||||
|
hasDomainParam := false
|
||||||
|
for _, param := range schemaParams {
|
||||||
|
if strings.EqualFold(strings.TrimSpace(param.Code), "domain") {
|
||||||
|
hasDomainParam = true
|
||||||
|
domainServiceIDsSet[spec.ServiceID] = struct{}{}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adoptExistingOnCreate := false
|
||||||
|
|
||||||
|
suspendOnDestroy := true
|
||||||
|
if spec.Lifecycle.SuspendOnDestroyDefault != nil {
|
||||||
|
suspendOnDestroy = *spec.Lifecycle.SuspendOnDestroyDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
gr := types.GenResource{
|
||||||
|
Name: spec.Name,
|
||||||
|
ServiceID: spec.ServiceID,
|
||||||
|
CreateParams: createParams,
|
||||||
|
ModifyParams: modifyParams,
|
||||||
|
SchemaParams: schemaParams,
|
||||||
|
CreateOnlyParams: params.FilterCreateOnly(schemaParams),
|
||||||
|
CreateOnlyRequiredParams: params.FilterCreateOnlyRequired(schemaParams),
|
||||||
|
OutputParams: NormalizeOutputParams(spec.Outputs.Params),
|
||||||
|
HasRefSvcParams: HasRefSvcParams(schemaParams),
|
||||||
|
SupportsSuspendDestroy: supportsSuspendDestroy,
|
||||||
|
SuspendOnDestroy: suspendOnDestroy,
|
||||||
|
AdoptExistingOnCreate: adoptExistingOnCreate,
|
||||||
|
HasDomainParam: hasDomainParam,
|
||||||
|
}
|
||||||
|
params.Analyze(&gr.UsesBool, &gr.UsesInt64, &gr.UsesString, &gr.HasDefaults, &gr.NeedsBoolDefault, &gr.NeedsInt64Default, &gr.NeedsStringDefault, gr.SchemaParams)
|
||||||
|
gr.NeedsBoolUseStateForUnknown, gr.NeedsInt64UseStateForUnknown = analyzeUseStateForUnknown(gr.SchemaParams)
|
||||||
|
// Анализируем nested sub-params для default-импортов
|
||||||
|
if params.AnalyzeNestedDefaults(&gr.NeedsBoolDefault, &gr.NeedsInt64Default, &gr.NeedsStringDefault, gr.SchemaParams) {
|
||||||
|
gr.NeedsFmtImport = true
|
||||||
|
}
|
||||||
|
gr.NeedsJsonPlanMod = params.AnalyzeJsonPlanMod(gr.SchemaParams)
|
||||||
|
// Строковый import нужен только для обычных строковых сравнений и
|
||||||
|
// обработок в шаблоне. Старый restore-хук для refSvc больше не
|
||||||
|
// генерируется, поэтому отдельный флаг под него не нужен.
|
||||||
|
gr.NeedsStringsImport = params.AnalyzeNeedsStrings(gr.ModifyParams) || params.AnalyzeNeedsStrings(gr.CreateOnlyRequiredParams) || params.AnalyzeNeedsStrings(gr.CreateOnlyParams)
|
||||||
|
gr.NeedsFmtImport = params.HasNestedParams(gr.SchemaParams)
|
||||||
|
|
||||||
|
// --- Action processing (ДО append, чтобы HasRedeploy попал в слайс) ---
|
||||||
|
for _, op := range spec.Operations {
|
||||||
|
if op.Kind != "action" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch op.Action {
|
||||||
|
case "redeploy":
|
||||||
|
gr.HasRedeploy = true
|
||||||
|
gr.RedeployParams = ConvertParams(op.Params)
|
||||||
|
case "restart", "recovery", "reconcile":
|
||||||
|
// Исключены
|
||||||
|
default:
|
||||||
|
act := types.GenAction{
|
||||||
|
ServiceName: spec.Name,
|
||||||
|
ServiceID: spec.ServiceID,
|
||||||
|
ActionName: op.Action,
|
||||||
|
OperationName: op.Name,
|
||||||
|
Params: ConvertParams(op.Params),
|
||||||
|
}
|
||||||
|
act.SchemaParams = act.Params
|
||||||
|
params.Analyze(&act.UsesBool, &act.UsesInt64, &act.UsesString, &act.HasDefaults, &act.NeedsBoolDefault, &act.NeedsInt64Default, &act.NeedsStringDefault, act.SchemaParams)
|
||||||
|
act.NeedsJsonPlanMod = params.AnalyzeJsonPlanMod(act.SchemaParams)
|
||||||
|
act.NeedsStringsImport = params.AnalyzeNeedsStrings(act.SchemaParams)
|
||||||
|
actions = append(actions, act)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
services = append(services, gr)
|
||||||
|
|
||||||
|
srByName := map[string]*types.GenSubresource{}
|
||||||
|
for _, op := range spec.Operations {
|
||||||
|
if op.Kind != "subresource" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := strings.TrimSpace(op.Subresource)
|
||||||
|
if name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sr := srByName[name]
|
||||||
|
if sr == nil {
|
||||||
|
sr = &types.GenSubresource{ServiceName: spec.Name, ServiceID: spec.ServiceID, SubName: name}
|
||||||
|
srByName[name] = sr
|
||||||
|
}
|
||||||
|
switch op.Action {
|
||||||
|
case "create":
|
||||||
|
sr.CreateOpName = op.Name
|
||||||
|
sr.CreateParams = ConvertParams(op.Params)
|
||||||
|
case "modify":
|
||||||
|
sr.ModifyOpName = op.Name
|
||||||
|
sr.ModifyParams = ConvertParams(op.Params)
|
||||||
|
case "delete":
|
||||||
|
sr.DeleteOpName = op.Name
|
||||||
|
sr.DeleteParams = ConvertParams(op.Params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, sr := range srByName {
|
||||||
|
sr.SchemaParams = params.Merge(sr.CreateParams, sr.ModifyParams, sr.DeleteParams)
|
||||||
|
sr.CreateParams = params.AlignParamTypes(sr.CreateParams, sr.SchemaParams)
|
||||||
|
sr.ModifyParams = params.AlignParamTypes(sr.ModifyParams, sr.SchemaParams)
|
||||||
|
sr.DeleteParams = params.AlignParamTypes(sr.DeleteParams, sr.SchemaParams)
|
||||||
|
createOnly := params.ComputeCreateOnly(sr.CreateParams, sr.ModifyParams)
|
||||||
|
sr.SchemaParams = params.MarkCreateOnly(sr.SchemaParams, createOnly)
|
||||||
|
forceNewCodes := params.BuildSubresourceForceNewCodes(sr, createOnly)
|
||||||
|
sr.SchemaParams = params.MarkForceNew(sr.SchemaParams, forceNewCodes)
|
||||||
|
sr.IdentityParams = params.FilterForceNew(sr.SchemaParams)
|
||||||
|
if len(sr.IdentityParams) == 0 {
|
||||||
|
sr.IdentityParams = sr.SchemaParams
|
||||||
|
}
|
||||||
|
sr.HasRefSvcParams = HasRefSvcParams(sr.SchemaParams)
|
||||||
|
params.Analyze(&sr.UsesBool, &sr.UsesInt64, &sr.UsesString, &sr.HasDefaults, &sr.NeedsBoolDefault, &sr.NeedsInt64Default, &sr.NeedsStringDefault, sr.SchemaParams)
|
||||||
|
params.AnalyzePlanModifiers(&sr.NeedsBoolPlanMod, &sr.NeedsInt64PlanMod, &sr.NeedsStringPlanMod, sr.SchemaParams)
|
||||||
|
sr.NeedsJsonPlanMod = params.AnalyzeJsonPlanMod(sr.SchemaParams)
|
||||||
|
sr.NeedsStringsImport = params.AnalyzeNeedsStrings(sr.ModifyParams)
|
||||||
|
subs = append(subs, *sr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if walkErr != nil {
|
||||||
|
return nil, nil, nil, nil, walkErr
|
||||||
|
}
|
||||||
|
|
||||||
|
domainServiceIDs := make([]int, 0, len(domainServiceIDsSet))
|
||||||
|
for serviceID := range domainServiceIDsSet {
|
||||||
|
domainServiceIDs = append(domainServiceIDs, serviceID)
|
||||||
|
}
|
||||||
|
sort.Ints(domainServiceIDs)
|
||||||
|
for idx := range services {
|
||||||
|
if len(domainServiceIDs) == 0 {
|
||||||
|
services[idx].DomainServiceIDs = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
services[idx].DomainServiceIDs = append([]int(nil), domainServiceIDs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(services, func(i, j int) bool { return services[i].Name < services[j].Name })
|
||||||
|
sort.Slice(subs, func(i, j int) bool {
|
||||||
|
if subs[i].ServiceName == subs[j].ServiceName {
|
||||||
|
return subs[i].SubName < subs[j].SubName
|
||||||
|
}
|
||||||
|
return subs[i].ServiceName < subs[j].ServiceName
|
||||||
|
})
|
||||||
|
sort.Slice(actions, func(i, j int) bool {
|
||||||
|
if actions[i].ServiceName == actions[j].ServiceName {
|
||||||
|
return actions[i].ActionName < actions[j].ActionName
|
||||||
|
}
|
||||||
|
return actions[i].ServiceName < actions[j].ServiceName
|
||||||
|
})
|
||||||
|
sort.Slice(modifiers, func(i, j int) bool {
|
||||||
|
if modifiers[i].ServiceName == modifiers[j].ServiceName {
|
||||||
|
return modifiers[i].ModifierName < modifiers[j].ModifierName
|
||||||
|
}
|
||||||
|
return modifiers[i].ServiceName < modifiers[j].ServiceName
|
||||||
|
})
|
||||||
|
|
||||||
|
return services, subs, actions, modifiers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertParams конвертирует []ParamSpec → []Param с нормализацией типов.
|
||||||
|
func ConvertParams(params []types.ParamSpec) []types.Param {
|
||||||
|
out := make([]types.Param, 0, len(params))
|
||||||
|
for _, p := range params {
|
||||||
|
typeName := NormalizeParamType(p.DataType)
|
||||||
|
defVal := NormalizeDefault(p.Default)
|
||||||
|
ref := 0
|
||||||
|
if p.RefSvcID != nil {
|
||||||
|
ref = *p.RefSvcID
|
||||||
|
}
|
||||||
|
isNested := len(p.SubParams) > 0
|
||||||
|
// Рекурсивно конвертируем SubParams для map-fixed/array-map-fixed.
|
||||||
|
subParams := make([]types.Param, 0, len(p.SubParams))
|
||||||
|
for _, sp := range p.SubParams {
|
||||||
|
sub := ConvertParams([]types.ParamSpec{sp})
|
||||||
|
if len(sub) > 0 {
|
||||||
|
subParams = append(subParams, sub[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out = append(out, types.Param{
|
||||||
|
ID: p.ID,
|
||||||
|
Code: p.Code,
|
||||||
|
Type: typeName,
|
||||||
|
Required: p.Required,
|
||||||
|
Default: defVal,
|
||||||
|
RefSvcId: ref,
|
||||||
|
Descr: strings.TrimSpace(p.Descr),
|
||||||
|
Man: strings.TrimSpace(p.Man),
|
||||||
|
Sensitive: p.IsSensitive,
|
||||||
|
IsJson: strings.EqualFold(strings.TrimSpace(p.DataType), "json"),
|
||||||
|
HasSubParams: isNested,
|
||||||
|
SubParams: subParams,
|
||||||
|
IsModifiable: p.IsModifiable,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizeParamType нормализует строку data_type → bool | int64 | string | map-fixed | array-map-fixed.
|
||||||
|
func NormalizeParamType(value string) string {
|
||||||
|
v := strings.ToLower(strings.TrimSpace(value))
|
||||||
|
if v == "map-fixed" || v == "array-map-fixed" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
if strings.Contains(v, "bool") {
|
||||||
|
return "bool"
|
||||||
|
}
|
||||||
|
if strings.Contains(v, "int") || strings.Contains(v, "number") {
|
||||||
|
return "int64"
|
||||||
|
}
|
||||||
|
return "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizeDefault нормализует значение по умолчанию в строку.
|
||||||
|
func NormalizeDefault(value interface{}) string {
|
||||||
|
if value == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
switch t := value.(type) {
|
||||||
|
case string:
|
||||||
|
return strings.TrimSpace(t)
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%v", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizeOutputParams нормализует выходные параметры.
|
||||||
|
func NormalizeOutputParams(params []types.OutputParam) []types.OutputParam {
|
||||||
|
if len(params) == 0 {
|
||||||
|
return []types.OutputParam{}
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasRefSvcParams проверяет наличие ref_svc-параметров.
|
||||||
|
func HasRefSvcParams(params []types.Param) bool {
|
||||||
|
for _, p := range params {
|
||||||
|
if p.RefSvcId > 0 && strings.EqualFold(strings.TrimSpace(p.Type), "string") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// KnownKinds — допустимые значения Kind операций.
|
||||||
|
var KnownKinds = map[string]bool{
|
||||||
|
"instance": true,
|
||||||
|
"subresource": true,
|
||||||
|
"action": true,
|
||||||
|
"modifier": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateSpec проверяет YAML-спек на обязательные поля и неизвестные kinds.
|
||||||
|
// P1.4: fail-fast — паника при неизвестном kind вместо тихого игнорирования.
|
||||||
|
func ValidateSpec(path string, spec *types.ServiceSpec) error {
|
||||||
|
if spec.Name == "" {
|
||||||
|
return fmt.Errorf("missing required field: name")
|
||||||
|
}
|
||||||
|
if spec.ServiceID <= 0 {
|
||||||
|
return fmt.Errorf("missing required field: service_id")
|
||||||
|
}
|
||||||
|
for i, op := range spec.Operations {
|
||||||
|
if op.Kind == "" {
|
||||||
|
return fmt.Errorf("operation[%d] %q: missing required field: kind", i, op.Name)
|
||||||
|
}
|
||||||
|
if !KnownKinds[op.Kind] {
|
||||||
|
return fmt.Errorf("operation[%d] %q: unknown kind %q (valid: instance, subresource, action, modifier)", i, op.Name, op.Kind)
|
||||||
|
}
|
||||||
|
if op.Action == "" {
|
||||||
|
return fmt.Errorf("operation[%d] %q (kind=%s): missing required field: action", i, op.Name, op.Kind)
|
||||||
|
}
|
||||||
|
if op.Kind == "subresource" && op.Subresource == "" {
|
||||||
|
return fmt.Errorf("operation[%d] %q (kind=subresource): missing required field: subresource", i, op.Name)
|
||||||
|
}
|
||||||
|
if op.Kind == "modifier" && strings.TrimSpace(op.Action) == "" {
|
||||||
|
return fmt.Errorf("operation[%d] %q (kind=modifier): missing required field: action", i, op.Name)
|
||||||
|
}
|
||||||
|
if op.Kind == "modifier" {
|
||||||
|
if err := validateModifierOperation(i, &op); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalizeDeleteStrategy возвращает каноническое значение delete_strategy.
|
||||||
|
// Пусто → noop_warn; неизвестное — как есть (валидация в ValidateSpec отклонит раньше).
|
||||||
|
func normalizeDeleteStrategy(raw string) string {
|
||||||
|
v := strings.ToLower(strings.TrimSpace(raw))
|
||||||
|
if v == "" {
|
||||||
|
return "noop_warn"
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalizeIdempotency возвращает каноническое значение idempotency.
|
||||||
|
// Пусто → none.
|
||||||
|
func normalizeIdempotency(raw string) string {
|
||||||
|
v := strings.ToLower(strings.TrimSpace(raw))
|
||||||
|
if v == "" {
|
||||||
|
return "none"
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertDeleteParams переносит lib.DeleteParam → types.DeleteParam (wire-строки).
|
||||||
|
func convertDeleteParams(raw []lib.DeleteParam) []types.DeleteParam {
|
||||||
|
if len(raw) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]types.DeleteParam, 0, len(raw))
|
||||||
|
for _, p := range raw {
|
||||||
|
out = append(out, types.DeleteParam{Code: strings.TrimSpace(p.Code), Value: p.Value})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateModifierOperation — fail-fast для полей modifier-операции.
|
||||||
|
func validateModifierOperation(i int, op *lib.OperationSpec) error {
|
||||||
|
ds := normalizeDeleteStrategy(op.DeleteStrategy)
|
||||||
|
switch ds {
|
||||||
|
case "noop_warn", "inverse", "error":
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("operation[%d] %q (kind=modifier): unknown delete_strategy %q (valid: noop_warn, inverse, error)", i, op.Name, op.DeleteStrategy)
|
||||||
|
}
|
||||||
|
|
||||||
|
idem := normalizeIdempotency(op.Idempotency)
|
||||||
|
switch idem {
|
||||||
|
case "none", "check_before_run":
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("operation[%d] %q (kind=modifier): unknown idempotency %q (valid: none, check_before_run)", i, op.Name, op.Idempotency)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ds == "inverse" && len(op.DeleteParams) == 0 {
|
||||||
|
return fmt.Errorf("operation[%d] %q (kind=modifier): delete_strategy=inverse требует delete_params", i, op.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// каждый delete_params.code обязан существовать среди params (по lower-code)
|
||||||
|
paramCodes := make(map[string]struct{}, len(op.Params))
|
||||||
|
for _, p := range op.Params {
|
||||||
|
paramCodes[strings.ToLower(strings.TrimSpace(p.Code))] = struct{}{}
|
||||||
|
}
|
||||||
|
for _, dp := range op.DeleteParams {
|
||||||
|
if _, ok := paramCodes[strings.ToLower(strings.TrimSpace(dp.Code))]; !ok {
|
||||||
|
return fmt.Errorf("operation[%d] %q (kind=modifier): delete_params.code %q отсутствует в params", i, op.Name, dp.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// analyzeUseStateForUnknown определяет, нужны ли импорты boolplanmodifier/int64planmodifier:
|
||||||
|
// есть ли среди SchemaParams скалярный Optional+Computed (без Default) параметр
|
||||||
|
// соответствующего типа, для которого генерируется UseStateForUnknown().
|
||||||
|
func analyzeUseStateForUnknown(schemaParams []types.Param) (needsBool, needsInt64 bool) {
|
||||||
|
for _, p := range schemaParams {
|
||||||
|
if p.IsJson || p.Required || p.RefSvcId != 0 || p.Default != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch strings.ToLower(p.Type) {
|
||||||
|
case "bool":
|
||||||
|
needsBool = true
|
||||||
|
case "int", "int64", "number":
|
||||||
|
needsInt64 = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return needsBool, needsInt64
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Ресурсы-модификаторы (операции modify, которых нет в create-схеме ресурсов)
|
||||||
|
#
|
||||||
|
# Порядок строго такой:
|
||||||
|
# орга (создана вручную в ЛК)
|
||||||
|
# -> nubes_vc_vdc.vdc
|
||||||
|
# -> nubes_vc_nsxt.edge
|
||||||
|
# -> nubes_vc_org_ip_allocation (выделение внешних IP на орге)
|
||||||
|
# -> nubes_vc_nsxt_snat (SNAT на эдже этим ipSpace)
|
||||||
|
#
|
||||||
|
# Почему аллокация ПОСЛЕ эджа: платформа строит список ipSpace из состояния
|
||||||
|
# `job.vcd.networkProvider` / `job.vcd.providerGateway`, то есть требует уже
|
||||||
|
# созданный vDC и Edge. Иначе modify на орге падает
|
||||||
|
# («Can't cast Complex Object Type Struct to String»).
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# 1. Внешние IP на организации (modify: vIPConfigure, массив перезаписывается целиком)
|
||||||
|
resource "nubes_vc_org_ip_allocation" "org_ip" {
|
||||||
|
organization = var.organization
|
||||||
|
|
||||||
|
vip_configure = jsonencode([
|
||||||
|
{
|
||||||
|
name = var.ip_space_name
|
||||||
|
count = var.ip_count
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
# false = при destroy отправить обратный modify с count=0 (квота обнулится)
|
||||||
|
keep_on_destroy = false
|
||||||
|
|
||||||
|
depends_on = [nubes_vc_nsxt.edge]
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. SNAT на эдже (modify: ipSpaceName)
|
||||||
|
resource "nubes_vc_nsxt_snat" "snat" {
|
||||||
|
nsxt_uid = nubes_vc_nsxt.edge.id
|
||||||
|
ip_space_name = var.ip_space_name
|
||||||
|
|
||||||
|
keep_on_destroy = false
|
||||||
|
|
||||||
|
# ipSpace должен быть уже выделен на организации
|
||||||
|
depends_on = [nubes_vc_org_ip_allocation.org_ip]
|
||||||
|
}
|
||||||
|
|
||||||
|
output "allocated_org_ip" {
|
||||||
|
description = "Выделено внешних IP на организации"
|
||||||
|
value = {
|
||||||
|
organization = var.organization
|
||||||
|
ip_space_name = var.ip_space_name
|
||||||
|
ip_count = var.ip_count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "snat_ip_space" {
|
||||||
|
description = "ipSpace, включённый как SNAT на эдже"
|
||||||
|
value = nubes_vc_nsxt_snat.snat.ip_space_name
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Kubernetes кластер Штурвал — сервис 150, ресурс nubes_k8s_sthutrval_cluster
|
||||||
|
# (НЕ 148 «Менеджмент Kubernetes кластер Штурвал» — это другой сервис)
|
||||||
|
#
|
||||||
|
# Всё, что относится к Штурвалу, лежит ТОЛЬКО в этом файле: переменные, их
|
||||||
|
# значения по умолчанию и сам ресурс. Чтобы выключить Штурвал — удалить файл
|
||||||
|
# или закомментировать ресурс.
|
||||||
|
#
|
||||||
|
# Порядок (чек-лист из инструкции на услугу в ЛК):
|
||||||
|
# 1) Организация в Cloud Director — создана вручную в ЛК
|
||||||
|
# 2) nubes_vc_vdc.vdc — есть
|
||||||
|
# 3) nubes_vc_nsxt.edge — есть, обязательно ALB + AVI VS >= 3
|
||||||
|
# 4) внешние адреса в организации — суммарно >= 3 (nubes_vc_org_ip_allocation)
|
||||||
|
# 5) SNAT на Edge — nubes_vc_nsxt_snat
|
||||||
|
# 6) Kubernetes кластер Штурвал — этот ресурс
|
||||||
|
#
|
||||||
|
# Минимальные требования к кластеру: мастер-нод >= 1, воркер-нод >= 1,
|
||||||
|
# 4 vCPU / 8 GB RAM / 50 GB диска на ноду.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# --- Переменные Штурвала ---
|
||||||
|
|
||||||
|
variable "shturval_resource_name" {
|
||||||
|
type = string
|
||||||
|
default = "shturval-dev"
|
||||||
|
description = "Имя услуги «Kubernetes кластер Штурвал» в ЛК"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cluster_name" {
|
||||||
|
type = string
|
||||||
|
default = "shturval-dev-00"
|
||||||
|
description = "Имя кластера внутри Штурвала"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_app_version" {
|
||||||
|
type = string
|
||||||
|
default = "2.14.0"
|
||||||
|
description = "Версия Штурвала (значение по умолчанию платформы — 2.14.0)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cp_sizing_policy" {
|
||||||
|
type = string
|
||||||
|
default = "TKG 4CPU 8RAM"
|
||||||
|
description = "Политика размера control plane: 4 vCPU / 8 GB (минимум по инструкции). Должна существовать в ресурсной платформе vDC — список политик берётся из услуги «Виртуальный датацентр»"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cp_sizing_disk" {
|
||||||
|
type = number
|
||||||
|
default = 50
|
||||||
|
description = "Диск control plane, ГБ (минимум 50)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cp_count" {
|
||||||
|
type = number
|
||||||
|
default = 1
|
||||||
|
description = "Количество мастер-нод: 1, 3 или 5"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_group_name" {
|
||||||
|
type = string
|
||||||
|
default = "workers-shturval-dev"
|
||||||
|
description = "Имя группы воркеров (уникальное в кластере; допустимы строчные латинские буквы, цифры и дефис)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_sizing_policy" {
|
||||||
|
type = string
|
||||||
|
default = "TKG 4CPU 8RAM"
|
||||||
|
description = "Политика размера воркеров: 4 vCPU / 8 GB (минимум по инструкции)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_sizing_disk" {
|
||||||
|
type = number
|
||||||
|
default = 50
|
||||||
|
description = "Диск воркеров, ГБ (минимум 50)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_count" {
|
||||||
|
type = number
|
||||||
|
default = 1
|
||||||
|
description = "Количество воркер-нод (минимум 1)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Значения, которые собираются из переменных ---
|
||||||
|
|
||||||
|
locals {
|
||||||
|
# Группы воркеров передаются JSON-строкой ВНУТРЬ услуги как есть, поэтому ключи
|
||||||
|
# должны быть ровно такими, как в манифесте услуги 150: groupName, sizingPolicy,
|
||||||
|
# sizingDisk, count, autoscale, labelDeck.
|
||||||
|
# ВНИМАНИЕ: в сгенерированном примере провайдера (docs → Example) ключи показаны
|
||||||
|
# в snake_case — это ошибка генератора, платформа на них падает с
|
||||||
|
# «Cannot invoke method split() on null object» (не находит groupName → null).
|
||||||
|
shturval_worker_config = jsonencode([
|
||||||
|
{
|
||||||
|
groupName = var.shturval_worker_group_name
|
||||||
|
sizingPolicy = var.shturval_worker_sizing_policy
|
||||||
|
sizingDisk = var.shturval_worker_sizing_disk
|
||||||
|
count = var.shturval_worker_count
|
||||||
|
autoscale = false # автоскейл выключен
|
||||||
|
labelDeck = true # разрешить разворачивать услуги из ЛК на этих нодах
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Ресурс Штурвала ---
|
||||||
|
|
||||||
|
resource "nubes_k8s_sthutrval_cluster" "shturval" {
|
||||||
|
resource_name = var.shturval_resource_name
|
||||||
|
|
||||||
|
# Кластер Штурвала уже существует (инстанс «shturval-dev») и в проде не
|
||||||
|
# удаляется неделями, поэтому ресурс должен УСЫНОВИТЬ существующий инстанс,
|
||||||
|
# а не падать с «РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ (SUSPEND)».
|
||||||
|
# Проверка/adopt выполняются в Create на apply (в plan будет «will be created»).
|
||||||
|
adopt_existing_on_create = true
|
||||||
|
|
||||||
|
# Штурвал создаётся долго (десятки минут) — поднимаем таймаут ожидания,
|
||||||
|
# иначе провайдер сдаётся на дефолтных 600 с.
|
||||||
|
operation_timeout = "60m"
|
||||||
|
|
||||||
|
startup_configuration = {
|
||||||
|
# vDC и Edge из этого же конфига (обязательные поля)
|
||||||
|
vdc_uid = nubes_vc_vdc.vdc.id
|
||||||
|
nsxt_uid = nubes_vc_nsxt.edge.id
|
||||||
|
|
||||||
|
cluster_name = var.shturval_cluster_name
|
||||||
|
|
||||||
|
# Дополнительные возможности кластера (в ЛК — галочки при создании)
|
||||||
|
ex_logging = true # логи в Loki (без него логи услуг не видны в ЛК)
|
||||||
|
ex_monitoring = true # метрики в VictoriaMetrics (без него метрик в ЛК нет)
|
||||||
|
ex_local_csi = true
|
||||||
|
ex_vip = true
|
||||||
|
ex_update = true
|
||||||
|
ex_ingress = true
|
||||||
|
ex_named_csi = true
|
||||||
|
}
|
||||||
|
|
||||||
|
cluster_configuration = {
|
||||||
|
app_version = var.shturval_app_version
|
||||||
|
}
|
||||||
|
|
||||||
|
control_plane_configuration = {
|
||||||
|
sizing_policy = var.shturval_cp_sizing_policy
|
||||||
|
sizing_disk = var.shturval_cp_sizing_disk
|
||||||
|
count = var.shturval_cp_count
|
||||||
|
}
|
||||||
|
|
||||||
|
worker_configuration = local.shturval_worker_config
|
||||||
|
|
||||||
|
access_configuration = {
|
||||||
|
need_external_address_api = true # внешний адрес для Kubernetes API (false недопустим)
|
||||||
|
access_ip_list_api = jsonencode([]) # пусто = доступ всем
|
||||||
|
need_external_address_ingress = true # внешний адрес для Ingress
|
||||||
|
access_ip_list_ingress = jsonencode([]) # пусто = доступ всем
|
||||||
|
}
|
||||||
|
|
||||||
|
# Кластер поднимается только после готовой сети: vDC -> Edge -> внешние IP -> SNAT
|
||||||
|
depends_on = [nubes_vc_nsxt_snat.snat]
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
// Package types — структуры данных для генератора Terraform-провайдера.
|
||||||
|
//
|
||||||
|
// Моделирует три вида ресурсов:
|
||||||
|
// - GenResource — основной CRUD инстанса (nubes_{service})
|
||||||
|
// - GenSubresource — подресурсы (nubes_{service}_{user}, ...)
|
||||||
|
// - GenAction — действия (почти не используется, redeploy встроен в GenResource.HasRedeploy)
|
||||||
|
//
|
||||||
|
// Правила обработки action'ов из ARCHITECTURE.md:
|
||||||
|
// - redeploy → HasRedeploy=true, поле git_revision в основном ресурсе
|
||||||
|
// - restart/recovery/reconcile → исключены (ручные, только UI)
|
||||||
|
// - всё остальное → отдельный GenAction
|
||||||
|
package types
|
||||||
|
|
||||||
|
import "tf-tools/lib"
|
||||||
|
|
||||||
|
// ─── Алиасы к lib (общий YAML-контракт) ─────────────────────────────────────
|
||||||
|
|
||||||
|
type OutputParam = lib.OutputParam
|
||||||
|
type OperationSpec = lib.OperationSpec
|
||||||
|
type ParamSpec = lib.ParamSpec
|
||||||
|
|
||||||
|
// ─── Входные структуры (из YAML) — локальное определение ────────────────────
|
||||||
|
|
||||||
|
// ServiceSpec — локальное определение (отличается вложенными типами от lib).
|
||||||
|
// Использует *bool для Lifecycle (совместимость с YAML-парсингом).
|
||||||
|
type ServiceSpec struct {
|
||||||
|
Name string `yaml:"name"` // snake_case имя (postgres, s3bucket, ...)
|
||||||
|
ServiceID int `yaml:"service_id"` // числовой ID сервиса в Nubes
|
||||||
|
Outputs struct {
|
||||||
|
Params []OutputParam `yaml:"params"`
|
||||||
|
} `yaml:"outputs"`
|
||||||
|
Lifecycle struct {
|
||||||
|
SuspendOnDestroyDefault *bool `yaml:"suspend_on_destroy_default"`
|
||||||
|
AdoptExistingOnCreateDefault *bool `yaml:"adopt_existing_on_create_default"`
|
||||||
|
} `yaml:"lifecycle"`
|
||||||
|
Operations []OperationSpec `yaml:"operations"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Param — нормализованный параметр (после конвертации из ParamSpec).
|
||||||
|
type Param struct {
|
||||||
|
ID int
|
||||||
|
Code string
|
||||||
|
Type string
|
||||||
|
Required bool
|
||||||
|
Default string
|
||||||
|
RefSvcId int
|
||||||
|
Descr string
|
||||||
|
Man string
|
||||||
|
Sensitive bool
|
||||||
|
CreateOnly bool
|
||||||
|
ForceNew bool
|
||||||
|
// IsModifiable — флаг is_modifiable из API (можно менять в UI → можно менять в Terraform).
|
||||||
|
// nil означает «не указано» (как правило, изменяемо).
|
||||||
|
IsModifiable *bool
|
||||||
|
// IsJson — атрибут является JSON-строкой (data_type: json в YAML-спеке).
|
||||||
|
// При IsJson=true в схему добавляется JsonNormalize план-модификатор,
|
||||||
|
// чтобы план и API-ответ (компактный JSON) всегда совпадали.
|
||||||
|
IsJson bool
|
||||||
|
// HasSubParams — параметр имеет подполя (map-fixed или array-map-fixed).
|
||||||
|
HasSubParams bool
|
||||||
|
// SubParams — подполя для map-fixed/array-map-fixed параметров.
|
||||||
|
SubParams []Param
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenResource — ресурс инстанса (nubes_{service}).
|
||||||
|
type GenResource struct {
|
||||||
|
Name string
|
||||||
|
ServiceID int
|
||||||
|
CreateParams []Param
|
||||||
|
ModifyParams []Param
|
||||||
|
SchemaParams []Param
|
||||||
|
CreateOnlyParams []Param
|
||||||
|
CreateOnlyRequiredParams []Param
|
||||||
|
OutputParams []OutputParam
|
||||||
|
HasRefSvcParams bool
|
||||||
|
SupportsSuspendDestroy bool
|
||||||
|
SuspendOnDestroy bool
|
||||||
|
AdoptExistingOnCreate bool
|
||||||
|
UsesBool bool
|
||||||
|
UsesInt64 bool
|
||||||
|
UsesString bool
|
||||||
|
HasDefaults bool
|
||||||
|
NeedsBoolDefault bool
|
||||||
|
NeedsInt64Default bool
|
||||||
|
NeedsStringDefault bool
|
||||||
|
NeedsBoolUseStateForUnknown bool
|
||||||
|
NeedsInt64UseStateForUnknown bool
|
||||||
|
HasDomainParam bool
|
||||||
|
DomainServiceIDs []int
|
||||||
|
// HasRedeploy: сервис поддерживает redeploy (пересборка из git).
|
||||||
|
// Если true — в основной ресурс добавляется поле git_revision.
|
||||||
|
// При изменении git_revision вызывается redeploy вместо modify.
|
||||||
|
// Правило из ARCHITECTURE.md: только redeploy включается как inline action.
|
||||||
|
// restart, recovery, reconcile — исключены (ручные операции, только UI).
|
||||||
|
HasRedeploy bool
|
||||||
|
// RedeployParams — параметры redeploy-операции (если есть).
|
||||||
|
RedeployParams []Param
|
||||||
|
// NeedsJsonPlanMod: хотя бы один атрибут имеет data_type: json.
|
||||||
|
// Управляет добавлением импорта planmodifier в сгенерированный файл.
|
||||||
|
NeedsJsonPlanMod bool
|
||||||
|
// NeedsStringsImport: есть строковые ModifyParams (нужен EqualFold в Update).
|
||||||
|
NeedsStringsImport bool
|
||||||
|
// NeedsFmtImport: есть nested (map-fixed) параметры, нужен fmt.Sprintf.
|
||||||
|
NeedsFmtImport bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenSubresource — подресурс (nubes_{service}_{sub}).
|
||||||
|
type GenSubresource struct {
|
||||||
|
ServiceName string
|
||||||
|
ServiceID int
|
||||||
|
SubName string
|
||||||
|
CreateOpName string
|
||||||
|
ModifyOpName string
|
||||||
|
DeleteOpName string
|
||||||
|
CreateParams []Param
|
||||||
|
ModifyParams []Param
|
||||||
|
DeleteParams []Param
|
||||||
|
SchemaParams []Param
|
||||||
|
IdentityParams []Param
|
||||||
|
HasRefSvcParams bool
|
||||||
|
UsesBool bool
|
||||||
|
UsesInt64 bool
|
||||||
|
UsesString bool
|
||||||
|
HasDefaults bool
|
||||||
|
NeedsBoolDefault bool
|
||||||
|
NeedsInt64Default bool
|
||||||
|
NeedsStringDefault bool
|
||||||
|
NeedsBoolPlanMod bool
|
||||||
|
NeedsInt64PlanMod bool
|
||||||
|
NeedsStringPlanMod bool
|
||||||
|
NeedsJsonPlanMod bool
|
||||||
|
NeedsStringsImport bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenAction — action-ресурс (почти не используется, redeploy встроен в GenResource).
|
||||||
|
type GenAction struct {
|
||||||
|
ServiceName string
|
||||||
|
ServiceID int
|
||||||
|
ActionName string
|
||||||
|
OperationName string
|
||||||
|
Params []Param
|
||||||
|
SchemaParams []Param
|
||||||
|
UsesBool bool
|
||||||
|
UsesInt64 bool
|
||||||
|
UsesString bool
|
||||||
|
HasDefaults bool
|
||||||
|
NeedsBoolDefault bool
|
||||||
|
NeedsInt64Default bool
|
||||||
|
NeedsStringDefault bool
|
||||||
|
NeedsJsonPlanMod bool
|
||||||
|
NeedsStringsImport bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenModifier — отдельный ресурс для отложенной parent-level modify операции.
|
||||||
|
// Delete намеренно не содержит rollback: API-контракт обратного payload не подтверждён.
|
||||||
|
type GenModifier struct {
|
||||||
|
ServiceName string
|
||||||
|
ServiceID int
|
||||||
|
ModifierName string
|
||||||
|
OperationName string
|
||||||
|
Params []Param
|
||||||
|
SchemaParams []Param
|
||||||
|
// DeleteStrategy — noop_warn | inverse | error (нормализовано из YAML, пусто → noop_warn).
|
||||||
|
DeleteStrategy string
|
||||||
|
// Idempotency — none | check_before_run (нормализовано из YAML, пусто → none).
|
||||||
|
Idempotency string
|
||||||
|
// DeleteParams — обратные значения (wire-строки), только при DeleteStrategy==inverse.
|
||||||
|
DeleteParams []DeleteParam
|
||||||
|
UsesBool bool
|
||||||
|
UsesInt64 bool
|
||||||
|
UsesString bool
|
||||||
|
HasDefaults bool
|
||||||
|
NeedsBoolDefault bool
|
||||||
|
NeedsInt64Default bool
|
||||||
|
NeedsStringDefault bool
|
||||||
|
NeedsJsonPlanMod bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteParam — обратное значение параметра inverse-Delete (code → wire-строка).
|
||||||
|
type DeleteParam struct {
|
||||||
|
Code string
|
||||||
|
Value string
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Kubernetes кластер Штурвал — сервис 150, ресурс nubes_k8s_sthutrval_cluster
|
||||||
|
# (НЕ 148 «Менеджмент Kubernetes кластер Штурвал» — это другой сервис)
|
||||||
|
#
|
||||||
|
# Всё, что относится к Штурвалу, лежит ТОЛЬКО в этом файле: переменные, их
|
||||||
|
# значения по умолчанию и сам ресурс. Чтобы выключить Штурвал — удалить файл
|
||||||
|
# или закомментировать ресурс.
|
||||||
|
#
|
||||||
|
# Порядок (чек-лист из инструкции на услугу в ЛК):
|
||||||
|
# 1) Организация в Cloud Director — создана вручную в ЛК
|
||||||
|
# 2) nubes_vc_vdc.vdc — есть
|
||||||
|
# 3) nubes_vc_nsxt.edge — есть, обязательно ALB + AVI VS >= 3
|
||||||
|
# 4) внешние адреса в организации — суммарно >= 3 (nubes_vc_org_ip_allocation)
|
||||||
|
# 5) SNAT на Edge — nubes_vc_nsxt_snat
|
||||||
|
# 6) Kubernetes кластер Штурвал — этот ресурс
|
||||||
|
#
|
||||||
|
# Минимальные требования к кластеру: мастер-нод >= 1, воркер-нод >= 1,
|
||||||
|
# 4 vCPU / 8 GB RAM / 50 GB диска на ноду.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# --- Переменные Штурвала ---
|
||||||
|
|
||||||
|
variable "shturval_resource_name" {
|
||||||
|
type = string
|
||||||
|
default = "shturval-dev"
|
||||||
|
description = "Имя услуги «Kubernetes кластер Штурвал» в ЛК"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cluster_name" {
|
||||||
|
type = string
|
||||||
|
default = "shturval-dev-00"
|
||||||
|
description = "Имя кластера внутри Штурвала"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_app_version" {
|
||||||
|
type = string
|
||||||
|
default = "2.14.0"
|
||||||
|
description = "Версия Штурвала (значение по умолчанию платформы — 2.14.0)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cp_sizing_policy" {
|
||||||
|
type = string
|
||||||
|
default = "TKG 4CPU 8RAM"
|
||||||
|
description = "Политика размера control plane: 4 vCPU / 8 GB (минимум по инструкции). Должна существовать в ресурсной платформе vDC — список политик берётся из услуги «Виртуальный датацентр»"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cp_sizing_disk" {
|
||||||
|
type = number
|
||||||
|
default = 50
|
||||||
|
description = "Диск control plane, ГБ (минимум 50)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cp_count" {
|
||||||
|
type = number
|
||||||
|
default = 1
|
||||||
|
description = "Количество мастер-нод: 1, 3 или 5"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_group_name" {
|
||||||
|
type = string
|
||||||
|
default = "workers-shturval-dev"
|
||||||
|
description = "Имя группы воркеров (уникальное в кластере; допустимы строчные латинские буквы, цифры и дефис)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_sizing_policy" {
|
||||||
|
type = string
|
||||||
|
default = "TKG 4CPU 8RAM"
|
||||||
|
description = "Политика размера воркеров: 4 vCPU / 8 GB (минимум по инструкции)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_sizing_disk" {
|
||||||
|
type = number
|
||||||
|
default = 50
|
||||||
|
description = "Диск воркеров, ГБ (минимум 50)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_count" {
|
||||||
|
type = number
|
||||||
|
default = 1
|
||||||
|
description = "Количество воркер-нод (минимум 1)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Значения, которые собираются из переменных ---
|
||||||
|
|
||||||
|
locals {
|
||||||
|
# Группы воркеров передаются JSON-строкой ВНУТРЬ услуги как есть, поэтому ключи
|
||||||
|
# должны быть ровно такими, как в манифесте услуги 150: groupName, sizingPolicy,
|
||||||
|
# sizingDisk, count, autoscale, labelDeck.
|
||||||
|
# ВНИМАНИЕ: в сгенерированном примере провайдера (docs → Example) ключи показаны
|
||||||
|
# в snake_case — это ошибка генератора, платформа на них падает с
|
||||||
|
# «Cannot invoke method split() on null object» (не находит groupName → null).
|
||||||
|
shturval_worker_config = jsonencode([
|
||||||
|
{
|
||||||
|
groupName = var.shturval_worker_group_name
|
||||||
|
sizingPolicy = var.shturval_worker_sizing_policy
|
||||||
|
sizingDisk = var.shturval_worker_sizing_disk
|
||||||
|
count = var.shturval_worker_count
|
||||||
|
autoscale = false # автоскейл выключен
|
||||||
|
labelDeck = true # разрешить разворачивать услуги из ЛК на этих нодах
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Ресурс Штурвала ---
|
||||||
|
|
||||||
|
resource "nubes_k8s_sthutrval_cluster" "shturval" {
|
||||||
|
resource_name = var.shturval_resource_name
|
||||||
|
|
||||||
|
# Штурвал создаётся долго (десятки минут) — поднимаем таймаут ожидания,
|
||||||
|
# иначе провайдер сдаётся на дефолтных 600 с.
|
||||||
|
operation_timeout = "60m"
|
||||||
|
|
||||||
|
startup_configuration = {
|
||||||
|
# vDC и Edge из этого же конфига (обязательные поля)
|
||||||
|
vdc_uid = nubes_vc_vdc.vdc.id
|
||||||
|
nsxt_uid = nubes_vc_nsxt.edge.id
|
||||||
|
|
||||||
|
cluster_name = var.shturval_cluster_name
|
||||||
|
|
||||||
|
# Дополнительные возможности кластера (в ЛК — галочки при создании)
|
||||||
|
ex_logging = true # логи в Loki (без него логи услуг не видны в ЛК)
|
||||||
|
ex_monitoring = true # метрики в VictoriaMetrics (без него метрик в ЛК нет)
|
||||||
|
ex_local_csi = true
|
||||||
|
ex_vip = true
|
||||||
|
ex_update = true
|
||||||
|
ex_ingress = true
|
||||||
|
ex_named_csi = true
|
||||||
|
}
|
||||||
|
|
||||||
|
cluster_configuration = {
|
||||||
|
app_version = var.shturval_app_version
|
||||||
|
}
|
||||||
|
|
||||||
|
control_plane_configuration = {
|
||||||
|
sizing_policy = var.shturval_cp_sizing_policy
|
||||||
|
sizing_disk = var.shturval_cp_sizing_disk
|
||||||
|
count = var.shturval_cp_count
|
||||||
|
}
|
||||||
|
|
||||||
|
worker_configuration = local.shturval_worker_config
|
||||||
|
|
||||||
|
access_configuration = {
|
||||||
|
need_external_address_api = true # внешний адрес для Kubernetes API (false недопустим)
|
||||||
|
access_ip_list_api = jsonencode([]) # пусто = доступ всем
|
||||||
|
need_external_address_ingress = true # внешний адрес для Ingress
|
||||||
|
access_ip_list_ingress = jsonencode([]) # пусто = доступ всем
|
||||||
|
}
|
||||||
|
|
||||||
|
# Кластер поднимается только после готовой сети: vDC -> Edge -> внешние IP -> SNAT
|
||||||
|
depends_on = [nubes_vc_nsxt_snat.snat]
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
resource "nubes_vc_nsxt" "edge" {
|
||||||
|
resource_name = var.nsxt_resource_name
|
||||||
|
|
||||||
|
# Тип родительской услуги: "vdc" (нужен vdc_uid) или "vdcGroup" (нужен vdc_group_uid)
|
||||||
|
vdc_type = var.nsxt_vdc_type
|
||||||
|
|
||||||
|
# refSvc-поле: принимает UUID или имя. Здесь берём UID созданного VDC,
|
||||||
|
# чтобы Edge гарантированно создавался после vDC.
|
||||||
|
vdc_uid = nubes_vc_vdc.vdc.id
|
||||||
|
|
||||||
|
need_enable_avi = var.nsxt_need_enable_avi
|
||||||
|
virtual_services_count = var.nsxt_virtual_services_count
|
||||||
|
|
||||||
|
# routed-сеть, которую разворачивает Edge (SingleNestedAttribute -> объект)
|
||||||
|
routed_net_configuration = {
|
||||||
|
ip_addr_pool = var.nsxt_ip_addr_pool
|
||||||
|
main_dns = var.nsxt_main_dns
|
||||||
|
second_dns = var.nsxt_second_dns
|
||||||
|
}
|
||||||
|
|
||||||
|
# «Заморозка»: destroy НЕ удаляет эдж (у платформы для эджа нет операции suspend),
|
||||||
|
# а только убирает его из состояния. Для полного удаления — keep_on_destroy = false.
|
||||||
|
keep_on_destroy = true
|
||||||
|
|
||||||
|
# Повторный apply усыновляет уже работающий эдж, а не падает с
|
||||||
|
# «РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ (RUNNING)».
|
||||||
|
adopt_existing_on_create = true
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Ресурсы-модификаторы (операции modify, которых нет в create-схеме ресурсов)
|
||||||
|
#
|
||||||
|
# Порядок строго такой:
|
||||||
|
# орга (создана вручную в ЛК)
|
||||||
|
# -> nubes_vc_vdc.vdc
|
||||||
|
# -> nubes_vc_nsxt.edge
|
||||||
|
# -> nubes_vc_org_ip_allocation (выделение внешних IP на орге)
|
||||||
|
# -> nubes_vc_nsxt_snat (SNAT на эдже этим ipSpace)
|
||||||
|
#
|
||||||
|
# Почему аллокация ПОСЛЕ эджа: платформа строит список ipSpace из состояния
|
||||||
|
# `job.vcd.networkProvider` / `job.vcd.providerGateway`, то есть требует уже
|
||||||
|
# созданный vDC и Edge. Иначе modify на орге падает
|
||||||
|
# («Can't cast Complex Object Type Struct to String»).
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# 1. Внешние IP на организации (modify: vIPConfigure, массив перезаписывается целиком)
|
||||||
|
resource "nubes_vc_org_ip_allocation" "org_ip" {
|
||||||
|
organization = var.organization
|
||||||
|
|
||||||
|
vip_configure = jsonencode([
|
||||||
|
{
|
||||||
|
name = var.ip_space_name
|
||||||
|
count = var.ip_count
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
# true = «заморозка»: destroy не трогает квоту внешних IP (кластер Штурвала держит
|
||||||
|
# адреса, опустить count ниже занятых платформа не даёт). Для полного удаления — false
|
||||||
|
# (и только после удаления кластера).
|
||||||
|
keep_on_destroy = true
|
||||||
|
|
||||||
|
depends_on = [nubes_vc_nsxt.edge]
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. SNAT на эдже (modify: ipSpaceName)
|
||||||
|
resource "nubes_vc_nsxt_snat" "snat" {
|
||||||
|
nsxt_uid = nubes_vc_nsxt.edge.id
|
||||||
|
ip_space_name = var.ip_space_name
|
||||||
|
|
||||||
|
# true = «заморозка»: destroy не выключает SNAT на эдже. Для полного удаления — false.
|
||||||
|
keep_on_destroy = true
|
||||||
|
|
||||||
|
# ipSpace должен быть уже выделен на организации
|
||||||
|
depends_on = [nubes_vc_org_ip_allocation.org_ip]
|
||||||
|
}
|
||||||
|
|
||||||
|
output "allocated_org_ip" {
|
||||||
|
description = "Выделено внешних IP на организации"
|
||||||
|
value = {
|
||||||
|
organization = var.organization
|
||||||
|
ip_space_name = var.ip_space_name
|
||||||
|
ip_count = var.ip_count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "snat_ip_space" {
|
||||||
|
description = "ipSpace, включённый как SNAT на эдже"
|
||||||
|
value = nubes_vc_nsxt_snat.snat.ip_space_name
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
output "vdc_id" {
|
||||||
|
description = "UID созданного VDC"
|
||||||
|
value = nubes_vc_vdc.vdc.id
|
||||||
|
}
|
||||||
|
|
||||||
|
output "vdc_name" {
|
||||||
|
description = "Имя VDC"
|
||||||
|
value = nubes_vc_vdc.vdc.resource_name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "vdc_state_params" {
|
||||||
|
description = "Параметры состояния VDC из API"
|
||||||
|
value = nubes_vc_vdc.vdc.state_params
|
||||||
|
}
|
||||||
|
|
||||||
|
output "nsxt_id" {
|
||||||
|
description = "UID созданного Edge (vc_nsxt)"
|
||||||
|
value = nubes_vc_nsxt.edge.id
|
||||||
|
}
|
||||||
|
|
||||||
|
output "nsxt_name" {
|
||||||
|
description = "Имя Edge (vc_nsxt)"
|
||||||
|
value = nubes_vc_nsxt.edge.resource_name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "nsxt_state_params" {
|
||||||
|
description = "Параметры состояния Edge (vc_nsxt) из API"
|
||||||
|
value = nubes_vc_nsxt.edge.state_params
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
provider "nubes" {
|
||||||
|
api_token = var.api_token
|
||||||
|
api_endpoint = var.api_endpoint
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Kubernetes кластер Штурвал — сервис 150, ресурс nubes_k8s_sthutrval_cluster
|
||||||
|
# (НЕ 148 «Менеджмент Kubernetes кластер Штурвал» — это другой сервис)
|
||||||
|
#
|
||||||
|
# Всё, что относится к Штурвалу, лежит ТОЛЬКО в этом файле: переменные, их
|
||||||
|
# значения по умолчанию и сам ресурс. Чтобы выключить Штурвал — удалить файл
|
||||||
|
# или закомментировать ресурс.
|
||||||
|
#
|
||||||
|
# Порядок (чек-лист из инструкции на услугу в ЛК):
|
||||||
|
# 1) Организация в Cloud Director — создана вручную в ЛК
|
||||||
|
# 2) nubes_vc_vdc.vdc — есть
|
||||||
|
# 3) nubes_vc_nsxt.edge — есть, обязательно ALB + AVI VS >= 3
|
||||||
|
# 4) внешние адреса в организации — суммарно >= 3 (nubes_vc_org_ip_allocation)
|
||||||
|
# 5) SNAT на Edge — nubes_vc_nsxt_snat
|
||||||
|
# 6) Kubernetes кластер Штурвал — этот ресурс
|
||||||
|
#
|
||||||
|
# Минимальные требования к кластеру: мастер-нод >= 1, воркер-нод >= 1,
|
||||||
|
# 4 vCPU / 8 GB RAM / 50 GB диска на ноду.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# --- Переменные Штурвала ---
|
||||||
|
|
||||||
|
variable "shturval_resource_name" {
|
||||||
|
type = string
|
||||||
|
default = "shturval-dev"
|
||||||
|
description = "Имя услуги «Kubernetes кластер Штурвал» в ЛК"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cluster_name" {
|
||||||
|
type = string
|
||||||
|
default = "shturval-dev-00"
|
||||||
|
description = "Имя кластера внутри Штурвала"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_app_version" {
|
||||||
|
type = string
|
||||||
|
default = "2.14.0"
|
||||||
|
description = "Версия Штурвала (значение по умолчанию платформы — 2.14.0)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cp_sizing_policy" {
|
||||||
|
type = string
|
||||||
|
default = "TKG 4CPU 8RAM"
|
||||||
|
description = "Политика размера control plane: 4 vCPU / 8 GB (минимум по инструкции). Должна существовать в ресурсной платформе vDC — список политик берётся из услуги «Виртуальный датацентр»"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cp_sizing_disk" {
|
||||||
|
type = number
|
||||||
|
default = 50
|
||||||
|
description = "Диск control plane, ГБ (минимум 50)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cp_count" {
|
||||||
|
type = number
|
||||||
|
default = 1
|
||||||
|
description = "Количество мастер-нод: 1, 3 или 5"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_group_name" {
|
||||||
|
type = string
|
||||||
|
default = "workers-shturval-dev"
|
||||||
|
description = "Имя группы воркеров (уникальное в кластере; допустимы строчные латинские буквы, цифры и дефис)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_sizing_policy" {
|
||||||
|
type = string
|
||||||
|
default = "TKG 4CPU 8RAM"
|
||||||
|
description = "Политика размера воркеров: 4 vCPU / 8 GB (минимум по инструкции)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_sizing_disk" {
|
||||||
|
type = number
|
||||||
|
default = 50
|
||||||
|
description = "Диск воркеров, ГБ (минимум 50)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_count" {
|
||||||
|
type = number
|
||||||
|
default = 1
|
||||||
|
description = "Количество воркер-нод (минимум 1)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Значения, которые собираются из переменных ---
|
||||||
|
|
||||||
|
locals {
|
||||||
|
# Группы воркеров передаются JSON-строкой ВНУТРЬ услуги как есть, поэтому ключи
|
||||||
|
# должны быть ровно такими, как в манифесте услуги 150: groupName, sizingPolicy,
|
||||||
|
# sizingDisk, count, autoscale, labelDeck.
|
||||||
|
# ВНИМАНИЕ: в сгенерированном примере провайдера (docs → Example) ключи показаны
|
||||||
|
# в snake_case — это ошибка генератора, платформа на них падает с
|
||||||
|
# «Cannot invoke method split() on null object» (не находит groupName → null).
|
||||||
|
shturval_worker_config = jsonencode([
|
||||||
|
{
|
||||||
|
groupName = var.shturval_worker_group_name
|
||||||
|
sizingPolicy = var.shturval_worker_sizing_policy
|
||||||
|
sizingDisk = var.shturval_worker_sizing_disk
|
||||||
|
count = var.shturval_worker_count
|
||||||
|
autoscale = false # автоскейл выключен
|
||||||
|
labelDeck = true # разрешить разворачивать услуги из ЛК на этих нодах
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Ресурс Штурвала ---
|
||||||
|
|
||||||
|
resource "nubes_k8s_sthutrval_cluster" "shturval" {
|
||||||
|
resource_name = var.shturval_resource_name
|
||||||
|
|
||||||
|
# Кластер Штурвала уже существует (инстанс «shturval-dev») и в проде не
|
||||||
|
# удаляется неделями, поэтому ресурс должен УСЫНОВИТЬ существующий инстанс,
|
||||||
|
# а не падать с «РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ (SUSPEND)».
|
||||||
|
# Проверка/adopt выполняются в Create на apply (в plan будет «will be created»).
|
||||||
|
adopt_existing_on_create = true
|
||||||
|
|
||||||
|
# «Заморозка»: destroy приостанавливает кластер (suspend), а не удаляет.
|
||||||
|
# Следующий apply усыновит его и разморозит (resume).
|
||||||
|
suspend_on_destroy = true
|
||||||
|
|
||||||
|
# Штурвал создаётся долго (десятки минут) — поднимаем таймаут ожидания,
|
||||||
|
# иначе провайдер сдаётся на дефолтных 600 с.
|
||||||
|
operation_timeout = "60m"
|
||||||
|
|
||||||
|
startup_configuration = {
|
||||||
|
# vDC и Edge из этого же конфига (обязательные поля)
|
||||||
|
vdc_uid = nubes_vc_vdc.vdc.id
|
||||||
|
nsxt_uid = nubes_vc_nsxt.edge.id
|
||||||
|
|
||||||
|
cluster_name = var.shturval_cluster_name
|
||||||
|
|
||||||
|
# Дополнительные возможности кластера (в ЛК — галочки при создании)
|
||||||
|
ex_logging = true # логи в Loki (без него логи услуг не видны в ЛК)
|
||||||
|
ex_monitoring = true # метрики в VictoriaMetrics (без него метрик в ЛК нет)
|
||||||
|
ex_local_csi = true
|
||||||
|
ex_vip = true
|
||||||
|
ex_update = true
|
||||||
|
ex_ingress = true
|
||||||
|
ex_named_csi = true
|
||||||
|
}
|
||||||
|
|
||||||
|
cluster_configuration = {
|
||||||
|
app_version = var.shturval_app_version
|
||||||
|
}
|
||||||
|
|
||||||
|
control_plane_configuration = {
|
||||||
|
sizing_policy = var.shturval_cp_sizing_policy
|
||||||
|
sizing_disk = var.shturval_cp_sizing_disk
|
||||||
|
count = var.shturval_cp_count
|
||||||
|
}
|
||||||
|
|
||||||
|
worker_configuration = local.shturval_worker_config
|
||||||
|
|
||||||
|
access_configuration = {
|
||||||
|
need_external_address_api = true # внешний адрес для Kubernetes API (false недопустим)
|
||||||
|
access_ip_list_api = jsonencode([]) # пусто = доступ всем
|
||||||
|
need_external_address_ingress = true # внешний адрес для Ingress
|
||||||
|
access_ip_list_ingress = jsonencode([]) # пусто = доступ всем
|
||||||
|
}
|
||||||
|
|
||||||
|
# Кластер поднимается только после готовой сети: vDC -> Edge -> внешние IP -> SNAT
|
||||||
|
depends_on = [nubes_vc_nsxt_snat.snat]
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
api_token = "ВАШ_ТОКЕН_ИЗ_ЛК"
|
||||||
|
|
||||||
|
# Имя или UUID организации:
|
||||||
|
organization = "kontora"
|
||||||
|
|
||||||
|
vdc_resource_name = "fullpipe-vdc"
|
||||||
|
vdc_network_provider = "snb1"
|
||||||
|
vdc_provider_vdc = "Intel Broadwell 2.4"
|
||||||
|
vdc_cpu_allocated = 8
|
||||||
|
vdc_cpu_guaranteed = 0
|
||||||
|
vdc_mem_allocated = 32
|
||||||
|
|
||||||
|
vdc_storage_config = "[{\"name\":\"SATA\",\"size\":\"200\"}]"
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
variable "api_token" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
description = "API-токен Nubes"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "api_endpoint" {
|
||||||
|
type = string
|
||||||
|
default = "https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc"
|
||||||
|
description = "API Gateway URL"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Имя (display_name, напр. "kontora") ИЛИ UUID организации из ЛК
|
||||||
|
variable "organization" {
|
||||||
|
type = string
|
||||||
|
description = "Имя или UUID организации (vc_org)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Модификаторы (IP на орге + SNAT на эдже) ---
|
||||||
|
|
||||||
|
variable "ip_space_name" {
|
||||||
|
type = string
|
||||||
|
description = "Имя ipSpace, доступное организации (смотреть в ЛК, напр. internet-ipv4-v1)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ip_count" {
|
||||||
|
type = string
|
||||||
|
default = "3"
|
||||||
|
description = "Сколько внешних IP выделить на организации (count — строка)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vdc_resource_name" {
|
||||||
|
type = string
|
||||||
|
default = "fullpipe-vdc"
|
||||||
|
description = "Имя VDC"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vdc_network_provider" {
|
||||||
|
type = string
|
||||||
|
default = null
|
||||||
|
description = "Сетевой провайдер. Заполнить значением из текущей страницы ЛК"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vdc_provider_vdc" {
|
||||||
|
type = string
|
||||||
|
default = null
|
||||||
|
description = "Provider VDC. Заполнить значением из текущей страницы ЛК"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vdc_cpu_allocated" {
|
||||||
|
type = number
|
||||||
|
default = 8
|
||||||
|
description = "vCPU (шт.)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vdc_cpu_guaranteed" {
|
||||||
|
type = number
|
||||||
|
default = 0
|
||||||
|
description = "Резервирование vCPU (%, допустимо: 0, 50, 80)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vdc_mem_allocated" {
|
||||||
|
type = number
|
||||||
|
default = 32
|
||||||
|
description = "RAM (GB)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vdc_storage_config" {
|
||||||
|
type = string
|
||||||
|
default = "[{\"name\":\"SATA\",\"size\":\"200\"}]"
|
||||||
|
description = "Дисковое хранилище (JSON-массив, size в GB). Имя политики должно существовать в ресурсном пуле (например, SATA, SSD)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- vc_nsxt (Сетевой шлюз периметра / Edge) ---
|
||||||
|
|
||||||
|
variable "nsxt_resource_name" {
|
||||||
|
type = string
|
||||||
|
default = "fullpipe-edge"
|
||||||
|
description = "Имя Edge (vc_nsxt)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "nsxt_vdc_type" {
|
||||||
|
type = string
|
||||||
|
default = "vdc"
|
||||||
|
description = "Тип родительской услуги: vdc или vdcGroup"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "nsxt_need_enable_avi" {
|
||||||
|
type = bool
|
||||||
|
default = true
|
||||||
|
description = "Включить AVI Load Balancer (ALB)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "nsxt_virtual_services_count" {
|
||||||
|
type = number
|
||||||
|
default = 3
|
||||||
|
description = "Кол-во виртуальных сервисов на AVI (1..4; Штурвал: ≥ 3)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "nsxt_ip_addr_pool" {
|
||||||
|
type = string
|
||||||
|
default = "10.10.102.0/24"
|
||||||
|
description = "Адресный пул routed-сети (маска /24 обязательна)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "nsxt_main_dns" {
|
||||||
|
type = string
|
||||||
|
default = "81.22.46.22"
|
||||||
|
description = "Основной DNS"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "nsxt_second_dns" {
|
||||||
|
type = string
|
||||||
|
default = "185.247.187.77"
|
||||||
|
description = "Второй DNS"
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
resource "nubes_vc_vdc" "vdc" {
|
||||||
|
resource_name = var.vdc_resource_name
|
||||||
|
|
||||||
|
# Организация: имя из ЛК ("kontora") или точный UUID
|
||||||
|
organization_uid = var.organization
|
||||||
|
|
||||||
|
network_provider = var.vdc_network_provider
|
||||||
|
provider_vdc = var.vdc_provider_vdc
|
||||||
|
|
||||||
|
cpu_allocated = var.vdc_cpu_allocated
|
||||||
|
cpu_guaranteed = var.vdc_cpu_guaranteed
|
||||||
|
mem_allocated = var.vdc_mem_allocated
|
||||||
|
|
||||||
|
# JSON-массив дисковых политик (size в GB)
|
||||||
|
storage_config = var.vdc_storage_config
|
||||||
|
|
||||||
|
suspend_on_destroy = true
|
||||||
|
|
||||||
|
adopt_existing_on_create = true
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.5.0"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
nubes = {
|
||||||
|
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes"
|
||||||
|
version = "2.0.23"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# vDC → Edge → внешние IP → SNAT
|
||||||
|
|
||||||
|
Пример поднимает сетевую основу в Nubes:
|
||||||
|
|
||||||
|
- виртуальный датацентр (vDC);
|
||||||
|
- сетевой шлюз периметра (Edge);
|
||||||
|
- внешние IP на организации;
|
||||||
|
- SNAT на шлюзе.
|
||||||
|
|
||||||
|
Кластер Штурвал сюда **не входит** — он разворачивается долго, отдельным шагом.
|
||||||
|
Организацию создайте заранее в ЛК: Terraform её не создаёт и не удаляет.
|
||||||
|
|
||||||
|
**Перед началом:** нужен установленный Terraform 1.5 или новее. Провайдер скачается сам
|
||||||
|
при `terraform init` — отдельно ставить ничего не нужно.
|
||||||
|
|
||||||
|
## 1. Скачайте пример
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://gitea.services.ngcloud.ru/Nail/tf_examples.git
|
||||||
|
cd tf_examples/fullpipe_chain
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Возьмите значения в ЛК
|
||||||
|
|
||||||
|
| Значение | Где взять | Пример |
|
||||||
|
|---|---|---|
|
||||||
|
| `api_token` | ЛК → Профиль → Токены → «Технический» | `eyJhbGciOi...` |
|
||||||
|
| `organization` | ЛК → услуга «Организация в Cloud Director» → название услуги | `organ` |
|
||||||
|
| `vdc_network_provider` | ЛК → создание vDC → «Сетевой провайдер» | `snb1` |
|
||||||
|
| `vdc_provider_vdc` | ЛК → создание vDC → «Provider VDC» | `Intel Broadwell 2.4` |
|
||||||
|
| `vdc_storage_config` | ЛК → создание vDC → доступные дисковые политики | `SATA` |
|
||||||
|
| `ip_space_name` | ЛК → карточка организации → внешние IP | `internet-ipv4-v1` |
|
||||||
|
|
||||||
|
Остальные значения уже подставлены в `terraform.tfvars.example` — меняйте, если нужно.
|
||||||
|
|
||||||
|
## 3. Заполните terraform.tfvars
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp terraform.tfvars.example terraform.tfvars
|
||||||
|
nano terraform.tfvars # подставьте значения из шага 2
|
||||||
|
```
|
||||||
|
|
||||||
|
Полный список параметров — в `variables.tf`, у каждой переменной есть описание.
|
||||||
|
|
||||||
|
## 4. Выполните команды
|
||||||
|
|
||||||
|
```bash
|
||||||
|
terraform init # один раз — скачает провайдер
|
||||||
|
terraform plan # покажет, что будет создано (4 ресурса), ничего не меняет
|
||||||
|
terraform apply # создаст (подтвердить: yes)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Проверьте результат
|
||||||
|
|
||||||
|
```bash
|
||||||
|
terraform output # UUID и имена созданных услуг
|
||||||
|
```
|
||||||
|
|
||||||
|
И в ЛК: появились vDC и Edge, на организации выделены внешние IP, на шлюзе включён SNAT.
|
||||||
|
Повторный `terraform plan` должен показать `No changes`.
|
||||||
|
|
||||||
|
## 6. Удаление
|
||||||
|
|
||||||
|
```bash
|
||||||
|
terraform destroy
|
||||||
|
```
|
||||||
|
|
||||||
|
Порядок обратный: SNAT выключается, квота внешних IP обнуляется, затем удаляется шлюз,
|
||||||
|
а vDC приостанавливается (данные сохраняются).
|
||||||
|
**Организация не удаляется.**
|
||||||
|
|
||||||
|
## Состав файлов
|
||||||
|
|
||||||
|
| Файл | Что делает |
|
||||||
|
|---|---|
|
||||||
|
| `versions.tf` | версия Terraform и провайдера Nubes |
|
||||||
|
| `provider.tf` | подключение к API (токен, адрес) |
|
||||||
|
| `variables.tf` | все параметры с описанием |
|
||||||
|
| `vdc.tf` | виртуальный датацентр |
|
||||||
|
| `edge.tf` | сетевой шлюз периметра (Edge) |
|
||||||
|
| `modifiers.tf` | внешние IP на организации + SNAT на шлюзе |
|
||||||
|
| `outputs.tf` | UUID и имена созданных услуг |
|
||||||
|
| `terraform.tfvars.example` | шаблон значений (копируется в `terraform.tfvars`) |
|
||||||
|
|
||||||
|
`terraform.tfvars` с токеном никому не передавайте и не коммитьте в git.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Сетевой шлюз периметра (Edge) — создаётся внутри vDC, объявленного в vdc.tf.
|
||||||
|
resource "nubes_vc_nsxt" "edge" {
|
||||||
|
# имя услуги в ЛК (любое, удобное вам)
|
||||||
|
resource_name = var.nsxt_resource_name
|
||||||
|
|
||||||
|
# родительская услуга: vdc (нужен vdc_uid) или vdcGroup
|
||||||
|
vdc_type = var.nsxt_vdc_type
|
||||||
|
|
||||||
|
# ссылка на vDC: сначала создаётся vDC, потом шлюз
|
||||||
|
vdc_uid = nubes_vc_vdc.vdc.id
|
||||||
|
|
||||||
|
# балансировщик (ALB): включён; виртуальных сервисов 1..4,
|
||||||
|
# для кластера Штурвал нужно не меньше 3
|
||||||
|
need_enable_avi = var.nsxt_need_enable_avi
|
||||||
|
virtual_services_count = var.nsxt_virtual_services_count
|
||||||
|
|
||||||
|
# сеть шлюза: пул адресов (маска /24 обязательна) и DNS для машин
|
||||||
|
routed_net_configuration = {
|
||||||
|
ip_addr_pool = var.nsxt_ip_addr_pool
|
||||||
|
main_dns = var.nsxt_main_dns
|
||||||
|
second_dns = var.nsxt_second_dns
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Ресурсы, которые выполняют операции modify над уже созданными услугами.
|
||||||
|
#
|
||||||
|
# Порядок строго такой:
|
||||||
|
# организация (создаётся заранее в ЛК)
|
||||||
|
# -> nubes_vc_vdc.vdc
|
||||||
|
# -> nubes_vc_nsxt.edge
|
||||||
|
# -> nubes_vc_org_ip_allocation (внешние IP на организации)
|
||||||
|
# -> nubes_vc_nsxt_snat (SNAT на шлюзе этим ipSpace)
|
||||||
|
#
|
||||||
|
# Аллокация IP идёт ПОСЛЕ шлюза: платформа строит список доступных ipSpace
|
||||||
|
# только когда vDC и Edge уже созданы. Если поменять порядок — modify упадёт
|
||||||
|
# с ошибкой «Can't cast Complex Object Type Struct to String».
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# 1. Внешние IP на организации.
|
||||||
|
resource "nubes_vc_org_ip_allocation" "org_ip" {
|
||||||
|
# та же организация, что у vDC: название услуги из ЛК или её UUID
|
||||||
|
organization = var.organization
|
||||||
|
|
||||||
|
# сколько IP выделить: name — имя ipSpace, count — количество (строкой).
|
||||||
|
# массив передаётся целиком, поэтому старые значения перезаписываются
|
||||||
|
vip_configure = jsonencode([
|
||||||
|
{
|
||||||
|
name = var.ip_space_name
|
||||||
|
count = var.ip_count
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
# при destroy: false — квота обнуляется (count=0); true — оставить как есть
|
||||||
|
keep_on_destroy = false
|
||||||
|
|
||||||
|
depends_on = [nubes_vc_nsxt.edge]
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. SNAT на шлюзе: внешние адреса для машин.
|
||||||
|
resource "nubes_vc_nsxt_snat" "snat" {
|
||||||
|
# UUID шлюза, созданного выше
|
||||||
|
nsxt_uid = nubes_vc_nsxt.edge.id
|
||||||
|
ip_space_name = var.ip_space_name
|
||||||
|
|
||||||
|
# при destroy: false — SNAT выключается (no-needed); true — оставить как есть
|
||||||
|
keep_on_destroy = false
|
||||||
|
|
||||||
|
# ipSpace должен быть уже выделен на организации
|
||||||
|
depends_on = [nubes_vc_org_ip_allocation.org_ip]
|
||||||
|
}
|
||||||
|
|
||||||
|
output "allocated_org_ip" {
|
||||||
|
description = "Внешние IP, выделенные на организации"
|
||||||
|
value = {
|
||||||
|
organization = var.organization
|
||||||
|
ip_space_name = var.ip_space_name
|
||||||
|
ip_count = var.ip_count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "snat_ip_space" {
|
||||||
|
description = "ipSpace, включённый как SNAT на шлюзе"
|
||||||
|
value = nubes_vc_nsxt_snat.snat.ip_space_name
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Значения, нужные для дальнейшей работы (например, чтобы привязать к шлюзу
|
||||||
|
# кластер Штурвал или другие услуги).
|
||||||
|
|
||||||
|
output "vdc_id" {
|
||||||
|
description = "UUID созданного vDC"
|
||||||
|
value = nubes_vc_vdc.vdc.id
|
||||||
|
}
|
||||||
|
|
||||||
|
output "vdc_name" {
|
||||||
|
description = "Имя vDC"
|
||||||
|
value = nubes_vc_vdc.vdc.resource_name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "nsxt_id" {
|
||||||
|
description = "UUID созданного Edge (сетевого шлюза периметра)"
|
||||||
|
value = nubes_vc_nsxt.edge.id
|
||||||
|
}
|
||||||
|
|
||||||
|
output "nsxt_name" {
|
||||||
|
description = "Имя Edge"
|
||||||
|
value = nubes_vc_nsxt.edge.resource_name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "vdc_state_params" {
|
||||||
|
description = "Параметры vDC из API (что реально создалось)"
|
||||||
|
value = nubes_vc_vdc.vdc.state_params
|
||||||
|
}
|
||||||
|
|
||||||
|
output "nsxt_state_params" {
|
||||||
|
description = "Параметры Edge из API (что реально создалось)"
|
||||||
|
value = nubes_vc_nsxt.edge.state_params
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Подключение к API Nubes.
|
||||||
|
# Токен и адрес API берутся из terraform.tfvars (переменные api_token, api_endpoint).
|
||||||
|
provider "nubes" {
|
||||||
|
api_token = var.api_token
|
||||||
|
api_endpoint = var.api_endpoint
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Шаблон значений для fullpipe_chain.
|
||||||
|
#
|
||||||
|
# 1. Скопируйте файл: cp terraform.tfvars.example terraform.tfvars
|
||||||
|
# 2. Заполните значения ниже (откуда брать — в комментариях и в README.md).
|
||||||
|
# 3. terraform.tfvars НЕ публикуйте и не коммитьте: в нём токен доступа.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# ЛК → Профиль → Токены → «Технический»
|
||||||
|
api_token = "eyJhbGciOi..."
|
||||||
|
|
||||||
|
# Название услуги «Организация в Cloud Director» в ЛК (например: organ).
|
||||||
|
# Организацию создайте заранее в ЛК — Terraform её не создаёт и не удаляет.
|
||||||
|
organization = "organ"
|
||||||
|
|
||||||
|
# Имя ipSpace для внешнего IP. Смотреть в ЛК в карточке организации.
|
||||||
|
ip_space_name = "internet-ipv4-v1"
|
||||||
|
# Сколько внешних IP выделить (строкой).
|
||||||
|
ip_count = "3"
|
||||||
|
|
||||||
|
# --- vDC (виртуальный датацентр) ---
|
||||||
|
|
||||||
|
# Имя vDC в ЛК — любое.
|
||||||
|
vdc_resource_name = "fullpipe-vdc"
|
||||||
|
# ЛК → создание vDC → «Сетевой провайдер»
|
||||||
|
vdc_network_provider = "snb1"
|
||||||
|
# ЛК → создание vDC → «Provider VDC»
|
||||||
|
vdc_provider_vdc = "Intel Broadwell 2.4"
|
||||||
|
# vCPU, шт.
|
||||||
|
vdc_cpu_allocated = 8
|
||||||
|
# Резервирование vCPU, %: допустимы 0, 50 или 80
|
||||||
|
vdc_cpu_guaranteed = 0
|
||||||
|
# RAM, ГБ
|
||||||
|
vdc_mem_allocated = 32
|
||||||
|
# Дисковая политика: JSON-массив, name — имя политики из ЛК, size — размер в ГБ.
|
||||||
|
# Пример: политика SATA, 200 ГБ.
|
||||||
|
vdc_storage_config = "[{\"name\":\"SATA\",\"size\":\"200\"}]"
|
||||||
|
|
||||||
|
# --- Edge (сетевой шлюз периметра) ---
|
||||||
|
|
||||||
|
# Имя Edge в ЛК — любое.
|
||||||
|
nsxt_resource_name = "fullpipe-edge"
|
||||||
|
# Родительская услуга: vdc или vdcGroup.
|
||||||
|
nsxt_vdc_type = "vdc"
|
||||||
|
# Балансировщик AVI (ALB).
|
||||||
|
nsxt_need_enable_avi = true
|
||||||
|
# Виртуальных сервисов AVI: 1..4. Для кластера Штурвал нужно не меньше 3.
|
||||||
|
nsxt_virtual_services_count = 3
|
||||||
|
# Адресный пул routed-сети шлюза. Маска /24 обязательна.
|
||||||
|
nsxt_ip_addr_pool = "10.10.102.0/24"
|
||||||
|
# DNS для машин за шлюзом.
|
||||||
|
nsxt_main_dns = "81.22.46.22"
|
||||||
|
nsxt_second_dns = "185.247.187.77"
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Переменные. Все значения задаются в terraform.tfvars.
|
||||||
|
# Шаблон файла: terraform.tfvars.example (скопируйте в terraform.tfvars).
|
||||||
|
# Где взять каждое значение — в описании переменной и в README.md.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
variable "api_token" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
description = "Токен API. ЛК → Профиль → Токены → «Технический». Никому не передавайте: токен даёт полный доступ к услугам."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "api_endpoint" {
|
||||||
|
type = string
|
||||||
|
default = "https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc"
|
||||||
|
description = "Адрес API. Менять не нужно."
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Организация ---
|
||||||
|
|
||||||
|
variable "organization" {
|
||||||
|
type = string
|
||||||
|
description = "Название услуги «Организация в Cloud Director» в ЛК (например: organ). Можно указать и её UUID. Организация создаётся заранее в ЛК — Terraform её не создаёт."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ip_space_name" {
|
||||||
|
type = string
|
||||||
|
description = "Имя ipSpace для внешнего IP. Смотреть в ЛК в карточке организации (например: internet-ipv4-v1)."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ip_count" {
|
||||||
|
type = string
|
||||||
|
default = "3"
|
||||||
|
description = "Сколько внешних IP выделить на организации. Строкой — так его отдаёт платформа."
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- vDC (виртуальный датацентр) ---
|
||||||
|
|
||||||
|
variable "vdc_resource_name" {
|
||||||
|
type = string
|
||||||
|
default = "fullpipe-vdc"
|
||||||
|
description = "Имя vDC в ЛК. Любое, удобное вам."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vdc_network_provider" {
|
||||||
|
type = string
|
||||||
|
default = null
|
||||||
|
description = "Сетевой провайдер vDC. Взять в ЛК на странице создания vDC (например: snb1)."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vdc_provider_vdc" {
|
||||||
|
type = string
|
||||||
|
default = null
|
||||||
|
description = "Provider VDC. Взять в ЛК на странице создания vDC (например: Intel Broadwell 2.4)."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vdc_cpu_allocated" {
|
||||||
|
type = number
|
||||||
|
default = 8
|
||||||
|
description = "vCPU, шт."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vdc_cpu_guaranteed" {
|
||||||
|
type = number
|
||||||
|
default = 0
|
||||||
|
description = "Резервирование vCPU, %. Допустимы только 0, 50 или 80."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vdc_mem_allocated" {
|
||||||
|
type = number
|
||||||
|
default = 32
|
||||||
|
description = "RAM, ГБ."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vdc_storage_config" {
|
||||||
|
type = string
|
||||||
|
default = "[{\"name\":\"SATA\",\"size\":\"200\"}]"
|
||||||
|
description = "Дисковая политика: JSON-массив, name — имя политики из ЛК, size — размер в ГБ (строкой). Имя политики должно существовать в ресурсном пуле (например: SATA, SSD)."
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Edge (сетевой шлюз периметра, vc_nsxt) ---
|
||||||
|
|
||||||
|
variable "nsxt_resource_name" {
|
||||||
|
type = string
|
||||||
|
default = "fullpipe-edge"
|
||||||
|
description = "Имя Edge в ЛК. Любое, удобное вам."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "nsxt_vdc_type" {
|
||||||
|
type = string
|
||||||
|
default = "vdc"
|
||||||
|
description = "Тип родительской услуги: vdc (нужен vdc_uid) или vdcGroup."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "nsxt_need_enable_avi" {
|
||||||
|
type = bool
|
||||||
|
default = true
|
||||||
|
description = "Включить балансировщик AVI (ALB)."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "nsxt_virtual_services_count" {
|
||||||
|
type = number
|
||||||
|
default = 3
|
||||||
|
description = "Количество виртуальных сервисов на AVI: 1..4. Для кластера Штурвал нужно не меньше 3."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "nsxt_ip_addr_pool" {
|
||||||
|
type = string
|
||||||
|
default = "10.10.102.0/24"
|
||||||
|
description = "Адресный пул routed-сети шлюза. Маска /24 обязательна."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "nsxt_main_dns" {
|
||||||
|
type = string
|
||||||
|
default = "81.22.46.22"
|
||||||
|
description = "Основной DNS для машин за шлюзом."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "nsxt_second_dns" {
|
||||||
|
type = string
|
||||||
|
default = "185.247.187.77"
|
||||||
|
description = "Второй DNS для машин за шлюзом."
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Виртуальный датацентр (vDC) — создаётся внутри организации из terraform.tfvars.
|
||||||
|
resource "nubes_vc_vdc" "vdc" {
|
||||||
|
# имя услуги в ЛК (любое, удобное вам)
|
||||||
|
resource_name = var.vdc_resource_name
|
||||||
|
|
||||||
|
# организация, к которой привязан vDC: название услуги из ЛК или её UUID
|
||||||
|
organization_uid = var.organization
|
||||||
|
|
||||||
|
# значения из ЛК со страницы создания vDC
|
||||||
|
network_provider = var.vdc_network_provider
|
||||||
|
provider_vdc = var.vdc_provider_vdc
|
||||||
|
|
||||||
|
# квоты: vCPU (шт.), резервирование vCPU (%), RAM (ГБ)
|
||||||
|
# для cpu_guaranteed допустимы только 0, 50 или 80
|
||||||
|
cpu_allocated = var.vdc_cpu_allocated
|
||||||
|
cpu_guaranteed = var.vdc_cpu_guaranteed
|
||||||
|
mem_allocated = var.vdc_mem_allocated
|
||||||
|
|
||||||
|
# дисковые политики: JSON-массив, name — имя политики из ЛК, size — размер в ГБ
|
||||||
|
storage_config = var.vdc_storage_config
|
||||||
|
|
||||||
|
# при destroy не удалять vDC, а приостановить (данные сохраняются)
|
||||||
|
suspend_on_destroy = true
|
||||||
|
|
||||||
|
# если услуга с таким именем уже есть — подключиться к ней, а не падать с ошибкой
|
||||||
|
adopt_existing_on_create = true
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Версия Terraform и провайдера Nubes.
|
||||||
|
# Провайдер скачается сам при `terraform init` — вручную ставить ничего не нужно.
|
||||||
|
terraform {
|
||||||
|
required_version = ">= 1.5.0"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
nubes = {
|
||||||
|
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes"
|
||||||
|
version = "2.0.21"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
# Как развернуть vDC, Edge, внешние IP и SNAT
|
||||||
|
|
||||||
|
Пошаговая инструкция. Готовые файлы примера — в репозитории `tf_examples`, папка `fullpipe_chain`.
|
||||||
|
|
||||||
|
Что получится в итоге:
|
||||||
|
|
||||||
|
- виртуальный датацентр (vDC);
|
||||||
|
- сетевой шлюз периметра (Edge);
|
||||||
|
- внешние IP на организации;
|
||||||
|
- SNAT на шлюзе.
|
||||||
|
|
||||||
|
Кластер Штурвал в эту инструкцию не входит — он разворачивается долго, отдельным шагом.
|
||||||
|
|
||||||
|
**Организацию создайте заранее в ЛК** — Terraform её не создаёт и не удаляет.
|
||||||
|
|
||||||
|
## 1. Скачайте пример
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://gitea.services.ngcloud.ru/Nail/tf_examples.git
|
||||||
|
cd tf_examples/fullpipe_chain
|
||||||
|
```
|
||||||
|
|
||||||
|
Понадобится Terraform 1.5 или новее. Провайдер скачается сам при `terraform init`.
|
||||||
|
|
||||||
|
## 2. Возьмите значения в ЛК
|
||||||
|
|
||||||
|
| Значение | Где взять | Пример |
|
||||||
|
|---|---|---|
|
||||||
|
| `api_token` | ЛК → Профиль → Токены → «Технический» | `eyJhbGciOi...` |
|
||||||
|
| `organization` | ЛК → услуга «Организация в Cloud Director» → название услуги | `organ` |
|
||||||
|
| `vdc_network_provider` | ЛК → создание vDC → «Сетевой провайдер» | `snb1` |
|
||||||
|
| `vdc_provider_vdc` | ЛК → создание vDC → «Provider VDC» | `Intel Broadwell 2.4` |
|
||||||
|
| `vdc_storage_config` | ЛК → создание vDC → доступные дисковые политики | `SATA` |
|
||||||
|
| `ip_space_name` | ЛК → карточка организации → внешние IP | `internet-ipv4-v1` |
|
||||||
|
|
||||||
|
## 3. Заполните значения
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp terraform.tfvars.example terraform.tfvars
|
||||||
|
nano terraform.tfvars
|
||||||
|
```
|
||||||
|
|
||||||
|
Файл `terraform.tfvars` выглядит так:
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
# Токен из ЛК
|
||||||
|
api_token = "eyJhbGciOi..."
|
||||||
|
|
||||||
|
# Организация из ЛК (создана заранее)
|
||||||
|
organization = "organ"
|
||||||
|
|
||||||
|
# Внешние IP
|
||||||
|
ip_space_name = "internet-ipv4-v1"
|
||||||
|
ip_count = "3"
|
||||||
|
|
||||||
|
# vDC
|
||||||
|
vdc_resource_name = "fullpipe-vdc" # имя услуги в ЛК, любое
|
||||||
|
vdc_network_provider = "snb1" # ЛК → создание vDC → «Сетевой провайдер»
|
||||||
|
vdc_provider_vdc = "Intel Broadwell 2.4" # ЛК → создание vDC → «Provider VDC»
|
||||||
|
vdc_cpu_allocated = 8 # vCPU, шт.
|
||||||
|
vdc_cpu_guaranteed = 0 # резервирование vCPU, %: 0, 50 или 80
|
||||||
|
vdc_mem_allocated = 32 # RAM, ГБ
|
||||||
|
vdc_storage_config = "[{\"name\":\"SATA\",\"size\":\"200\"}]" # политика и размер, ГБ
|
||||||
|
|
||||||
|
# Edge (сетевой шлюз периметра)
|
||||||
|
nsxt_resource_name = "fullpipe-edge" # имя услуги в ЛК, любое
|
||||||
|
nsxt_vdc_type = "vdc" # родитель: vdc или vdcGroup
|
||||||
|
nsxt_need_enable_avi = true # балансировщик AVI (ALB)
|
||||||
|
nsxt_virtual_services_count = 3 # виртуальных сервисов AVI: 1..4 (Штурвал: не меньше 3)
|
||||||
|
nsxt_ip_addr_pool = "10.10.102.0/24" # пул адресов routed-сети, маска /24
|
||||||
|
nsxt_main_dns = "81.22.46.22" # основной DNS
|
||||||
|
nsxt_second_dns = "185.247.187.77" # второй DNS
|
||||||
|
```
|
||||||
|
|
||||||
|
Описание всех параметров — в `variables.tf`. `terraform.tfvars` с токеном никому не передавайте
|
||||||
|
и не коммитьте в git.
|
||||||
|
|
||||||
|
## 4. Выполните команды
|
||||||
|
|
||||||
|
```bash
|
||||||
|
terraform init # один раз — скачает провайдер
|
||||||
|
terraform plan # покажет, что будет создано: 4 ресурса
|
||||||
|
terraform apply # создаст (подтвердить: yes)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Проверьте результат
|
||||||
|
|
||||||
|
```bash
|
||||||
|
terraform output # UUID и имена созданных услуг
|
||||||
|
```
|
||||||
|
|
||||||
|
И в ЛК: появились vDC и Edge, на организации выделены внешние IP, на шлюзе включён SNAT.
|
||||||
|
Повторный `terraform plan` должен показать `No changes`.
|
||||||
|
|
||||||
|
## 6. Удаление
|
||||||
|
|
||||||
|
```bash
|
||||||
|
terraform destroy
|
||||||
|
```
|
||||||
|
|
||||||
|
Порядок обратный: SNAT выключается, квота внешних IP обнуляется, затем удаляется шлюз,
|
||||||
|
а vDC приостанавливается (данные сохраняются). **Организация не удаляется.**
|
||||||
|
|
||||||
|
## Файлы примера
|
||||||
|
|
||||||
|
| Файл | Что делает |
|
||||||
|
|---|---|
|
||||||
|
| `versions.tf` | версия Terraform и провайдера Nubes |
|
||||||
|
| `provider.tf` | подключение к API (токен, адрес) |
|
||||||
|
| `variables.tf` | все параметры с описанием |
|
||||||
|
| `vdc.tf` | виртуальный датацентр |
|
||||||
|
| `edge.tf` | сетевой шлюз периметра (Edge) |
|
||||||
|
| `modifiers.tf` | внешние IP на организации + SNAT на шлюзе |
|
||||||
|
| `outputs.tf` | UUID и имена созданных услуг |
|
||||||
|
| `terraform.tfvars.example` | шаблон значений (копируется в `terraform.tfvars`) |
|
||||||
|
|
||||||
|
Подробнее про два последних ресурса — на странице
|
||||||
|
[«Ресурсы-модификаторы (IP организации, SNAT)»](../modifiers/org_ip_and_snat.md).
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Примеры Terraform для Nubes
|
||||||
|
|
||||||
|
> ## Внимание: рабочая папка одна
|
||||||
|
>
|
||||||
|
> Для работы используйте **только** [`fullpipe_chain`](fullpipe_chain/) — это пример
|
||||||
|
> «vDC → Edge → внешние IP → SNAT». Он прогнан на живом стенде (создание и удаление)
|
||||||
|
> и снабжён пошаговой инструкцией.
|
||||||
|
>
|
||||||
|
> Остальные папки репозитория — черновики: могут быть сырыми, содержать неактуальные
|
||||||
|
> версии провайдера и работать неправильно. **Не используйте их.**
|
||||||
|
|
||||||
|
Пошаговая инструкция: <https://tf-docs.nodejsk8s.dev.nubes.ru/nubes-dev/curated/pipeline/vdc_edge_ip_snat/>
|
||||||
|
|
||||||
|
## fullpipe_chain — что использовать
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://gitea.services.ngcloud.ru/Nail/tf_examples.git
|
||||||
|
cd tf_examples/fullpipe_chain
|
||||||
|
cp terraform.tfvars.example terraform.tfvars # заполнить значениями из ЛК
|
||||||
|
terraform init
|
||||||
|
terraform plan # покажет 4 ресурса, ничего не меняет
|
||||||
|
terraform apply # подтвердить: yes
|
||||||
|
```
|
||||||
|
|
||||||
|
Подробности и список значений — в [README примера](fullpipe_chain/README.md).
|
||||||
|
|
||||||
|
Организацию создайте заранее в ЛК: Terraform её не создаёт и не удаляет.
|
||||||
|
|
||||||
|
## Остальные папки — не использовать
|
||||||
|
|
||||||
|
`CRUD`, `modify_resources`, `SHTURVAL_MGMT`, `iot-rmq-demo` — черновики от прошлых работ.
|
||||||
|
Могут быть сырыми и не поддерживаются. Берите только `fullpipe_chain`.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
resource "nubes_vc_nsxt" "edge" {
|
||||||
|
resource_name = var.nsxt_resource_name
|
||||||
|
|
||||||
|
# Тип родительской услуги: "vdc" (нужен vdc_uid) или "vdcGroup" (нужен vdc_group_uid)
|
||||||
|
vdc_type = var.nsxt_vdc_type
|
||||||
|
|
||||||
|
# refSvc-поле: принимает UUID или имя. Здесь берём UID созданного VDC,
|
||||||
|
# чтобы Edge гарантированно создавался после vDC.
|
||||||
|
vdc_uid = nubes_vc_vdc.vdc.id
|
||||||
|
|
||||||
|
need_enable_avi = var.nsxt_need_enable_avi
|
||||||
|
virtual_services_count = var.nsxt_virtual_services_count
|
||||||
|
|
||||||
|
# routed-сеть, которую разворачивает Edge (SingleNestedAttribute -> объект)
|
||||||
|
routed_net_configuration = {
|
||||||
|
ip_addr_pool = var.nsxt_ip_addr_pool
|
||||||
|
main_dns = var.nsxt_main_dns
|
||||||
|
second_dns = var.nsxt_second_dns
|
||||||
|
}
|
||||||
|
|
||||||
|
# «Заморозка»: destroy НЕ удаляет эдж (у платформы для эджа нет операции suspend),
|
||||||
|
# а только убирает его из состояния. Для полного удаления — keep_on_destroy = false.
|
||||||
|
keep_on_destroy = true
|
||||||
|
|
||||||
|
# Повторный apply усыновляет уже работающий эдж, а не падает с
|
||||||
|
# «РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ (RUNNING)».
|
||||||
|
adopt_existing_on_create = true
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Ресурсы-модификаторы (операции modify, которых нет в create-схеме ресурсов)
|
||||||
|
#
|
||||||
|
# Порядок строго такой:
|
||||||
|
# орга (создана вручную в ЛК)
|
||||||
|
# -> nubes_vc_vdc.vdc
|
||||||
|
# -> nubes_vc_nsxt.edge
|
||||||
|
# -> nubes_vc_org_ip_allocation (выделение внешних IP на орге)
|
||||||
|
# -> nubes_vc_nsxt_snat (SNAT на эдже этим ipSpace)
|
||||||
|
#
|
||||||
|
# Почему аллокация ПОСЛЕ эджа: платформа строит список ipSpace из состояния
|
||||||
|
# `job.vcd.networkProvider` / `job.vcd.providerGateway`, то есть требует уже
|
||||||
|
# созданный vDC и Edge. Иначе modify на орге падает
|
||||||
|
# («Can't cast Complex Object Type Struct to String»).
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# 1. Внешние IP на организации (modify: vIPConfigure, массив перезаписывается целиком)
|
||||||
|
resource "nubes_vc_org_ip_allocation" "org_ip" {
|
||||||
|
organization = var.organization
|
||||||
|
|
||||||
|
vip_configure = jsonencode([
|
||||||
|
{
|
||||||
|
name = var.ip_space_name
|
||||||
|
count = var.ip_count
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
# true = «заморозка»: destroy не трогает квоту внешних IP (кластер Штурвала держит
|
||||||
|
# адреса, опустить count ниже занятых платформа не даёт). Для полного удаления — false
|
||||||
|
# (и только после удаления кластера).
|
||||||
|
keep_on_destroy = true
|
||||||
|
|
||||||
|
depends_on = [nubes_vc_nsxt.edge]
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. SNAT на эдже (modify: ipSpaceName)
|
||||||
|
resource "nubes_vc_nsxt_snat" "snat" {
|
||||||
|
nsxt_uid = nubes_vc_nsxt.edge.id
|
||||||
|
ip_space_name = var.ip_space_name
|
||||||
|
|
||||||
|
# true = «заморозка»: destroy не выключает SNAT на эдже. Для полного удаления — false.
|
||||||
|
keep_on_destroy = true
|
||||||
|
|
||||||
|
# ipSpace должен быть уже выделен на организации
|
||||||
|
depends_on = [nubes_vc_org_ip_allocation.org_ip]
|
||||||
|
}
|
||||||
|
|
||||||
|
output "allocated_org_ip" {
|
||||||
|
description = "Выделено внешних IP на организации"
|
||||||
|
value = {
|
||||||
|
organization = var.organization
|
||||||
|
ip_space_name = var.ip_space_name
|
||||||
|
ip_count = var.ip_count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "snat_ip_space" {
|
||||||
|
description = "ipSpace, включённый как SNAT на эдже"
|
||||||
|
value = nubes_vc_nsxt_snat.snat.ip_space_name
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
output "vdc_id" {
|
||||||
|
description = "UID созданного VDC"
|
||||||
|
value = nubes_vc_vdc.vdc.id
|
||||||
|
}
|
||||||
|
|
||||||
|
output "vdc_name" {
|
||||||
|
description = "Имя VDC"
|
||||||
|
value = nubes_vc_vdc.vdc.resource_name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "vdc_state_params" {
|
||||||
|
description = "Параметры состояния VDC из API"
|
||||||
|
value = nubes_vc_vdc.vdc.state_params
|
||||||
|
}
|
||||||
|
|
||||||
|
output "nsxt_id" {
|
||||||
|
description = "UID созданного Edge (vc_nsxt)"
|
||||||
|
value = nubes_vc_nsxt.edge.id
|
||||||
|
}
|
||||||
|
|
||||||
|
output "nsxt_name" {
|
||||||
|
description = "Имя Edge (vc_nsxt)"
|
||||||
|
value = nubes_vc_nsxt.edge.resource_name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "nsxt_state_params" {
|
||||||
|
description = "Параметры состояния Edge (vc_nsxt) из API"
|
||||||
|
value = nubes_vc_nsxt.edge.state_params
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
provider "nubes" {
|
||||||
|
api_token = var.api_token
|
||||||
|
api_endpoint = var.api_endpoint
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Kubernetes кластер Штурвал — сервис 150, ресурс nubes_k8s_sthutrval_cluster
|
||||||
|
# (НЕ 148 «Менеджмент Kubernetes кластер Штурвал» — это другой сервис)
|
||||||
|
#
|
||||||
|
# Всё, что относится к Штурвалу, лежит ТОЛЬКО в этом файле: переменные, их
|
||||||
|
# значения по умолчанию и сам ресурс. Чтобы выключить Штурвал — удалить файл
|
||||||
|
# или закомментировать ресурс.
|
||||||
|
#
|
||||||
|
# Порядок (чек-лист из инструкции на услугу в ЛК):
|
||||||
|
# 1) Организация в Cloud Director — создана вручную в ЛК
|
||||||
|
# 2) nubes_vc_vdc.vdc — есть
|
||||||
|
# 3) nubes_vc_nsxt.edge — есть, обязательно ALB + AVI VS >= 3
|
||||||
|
# 4) внешние адреса в организации — суммарно >= 3 (nubes_vc_org_ip_allocation)
|
||||||
|
# 5) SNAT на Edge — nubes_vc_nsxt_snat
|
||||||
|
# 6) Kubernetes кластер Штурвал — этот ресурс
|
||||||
|
#
|
||||||
|
# Минимальные требования к кластеру: мастер-нод >= 1, воркер-нод >= 1,
|
||||||
|
# 4 vCPU / 8 GB RAM / 50 GB диска на ноду.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# --- Переменные Штурвала ---
|
||||||
|
|
||||||
|
variable "shturval_resource_name" {
|
||||||
|
type = string
|
||||||
|
default = "shturval-dev1"
|
||||||
|
description = "Имя услуги «Kubernetes кластер Штурвал» в ЛК"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cluster_name" {
|
||||||
|
type = string
|
||||||
|
default = "shturval-dev-01"
|
||||||
|
description = "Имя кластера внутри Штурвала"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_app_version" {
|
||||||
|
type = string
|
||||||
|
default = "2.14.0"
|
||||||
|
description = "Версия Штурвала (значение по умолчанию платформы — 2.14.0)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cp_sizing_policy" {
|
||||||
|
type = string
|
||||||
|
default = "TKG 4CPU 8RAM"
|
||||||
|
description = "Политика размера control plane: 4 vCPU / 8 GB (минимум по инструкции). Должна существовать в ресурсной платформе vDC — список политик берётся из услуги «Виртуальный датацентр»"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cp_sizing_disk" {
|
||||||
|
type = number
|
||||||
|
default = 50
|
||||||
|
description = "Диск control plane, ГБ (минимум 50)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cp_count" {
|
||||||
|
type = number
|
||||||
|
default = 1
|
||||||
|
description = "Количество мастер-нод: 1, 3 или 5"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_group_name" {
|
||||||
|
type = string
|
||||||
|
default = "workers-shturval-dev"
|
||||||
|
description = "Имя группы воркеров (уникальное в кластере; допустимы строчные латинские буквы, цифры и дефис)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_sizing_policy" {
|
||||||
|
type = string
|
||||||
|
default = "TKG 4CPU 8RAM"
|
||||||
|
description = "Политика размера воркеров: 4 vCPU / 8 GB (минимум по инструкции)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_sizing_disk" {
|
||||||
|
type = number
|
||||||
|
default = 50
|
||||||
|
description = "Диск воркеров, ГБ (минимум 50)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_count" {
|
||||||
|
type = number
|
||||||
|
default = 1
|
||||||
|
description = "Количество воркер-нод (минимум 1)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Значения, которые собираются из переменных ---
|
||||||
|
|
||||||
|
locals {
|
||||||
|
# Группы воркеров передаются JSON-строкой ВНУТРЬ услуги как есть, поэтому ключи
|
||||||
|
# должны быть ровно такими, как в манифесте услуги 150: groupName, sizingPolicy,
|
||||||
|
# sizingDisk, count, autoscale, labelDeck.
|
||||||
|
# ВНИМАНИЕ: в сгенерированном примере провайдера (docs → Example) ключи показаны
|
||||||
|
# в snake_case — это ошибка генератора, платформа на них падает с
|
||||||
|
# «Cannot invoke method split() on null object» (не находит groupName → null).
|
||||||
|
shturval_worker_config = jsonencode([
|
||||||
|
{
|
||||||
|
groupName = var.shturval_worker_group_name
|
||||||
|
sizingPolicy = var.shturval_worker_sizing_policy
|
||||||
|
sizingDisk = var.shturval_worker_sizing_disk
|
||||||
|
count = var.shturval_worker_count
|
||||||
|
autoscale = false # автоскейл выключен
|
||||||
|
labelDeck = true # разрешить разворачивать услуги из ЛК на этих нодах
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Ресурс Штурвала ---
|
||||||
|
|
||||||
|
resource "nubes_k8s_sthutrval_cluster" "shturval" {
|
||||||
|
resource_name = var.shturval_resource_name
|
||||||
|
|
||||||
|
# Кластер Штурвала уже существует (инстанс «shturval-dev») и в проде не
|
||||||
|
# удаляется неделями, поэтому ресурс должен УСЫНОВИТЬ существующий инстанс,
|
||||||
|
# а не падать с «РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ (SUSPEND)».
|
||||||
|
# Проверка/adopt выполняются в Create на apply (в plan будет «will be created»).
|
||||||
|
adopt_existing_on_create = true
|
||||||
|
|
||||||
|
# «Заморозка»: destroy приостанавливает кластер (suspend), а не удаляет.
|
||||||
|
# Следующий apply усыновит его и разморозит (resume).
|
||||||
|
suspend_on_destroy = true
|
||||||
|
|
||||||
|
# Штурвал создаётся долго (десятки минут) — поднимаем таймаут ожидания,
|
||||||
|
# иначе провайдер сдаётся на дефолтных 600 с.
|
||||||
|
operation_timeout = "60m"
|
||||||
|
|
||||||
|
startup_configuration = {
|
||||||
|
# vDC и Edge из этого же конфига (обязательные поля)
|
||||||
|
vdc_uid = nubes_vc_vdc.vdc.id
|
||||||
|
nsxt_uid = nubes_vc_nsxt.edge.id
|
||||||
|
|
||||||
|
cluster_name = var.shturval_cluster_name
|
||||||
|
|
||||||
|
# Дополнительные возможности кластера (в ЛК — галочки при создании)
|
||||||
|
ex_logging = true # логи в Loki (без него логи услуг не видны в ЛК)
|
||||||
|
ex_monitoring = true # метрики в VictoriaMetrics (без него метрик в ЛК нет)
|
||||||
|
ex_local_csi = true
|
||||||
|
ex_vip = true
|
||||||
|
ex_update = true
|
||||||
|
ex_ingress = true
|
||||||
|
ex_named_csi = true
|
||||||
|
}
|
||||||
|
|
||||||
|
cluster_configuration = {
|
||||||
|
app_version = var.shturval_app_version
|
||||||
|
}
|
||||||
|
|
||||||
|
control_plane_configuration = {
|
||||||
|
sizing_policy = var.shturval_cp_sizing_policy
|
||||||
|
sizing_disk = var.shturval_cp_sizing_disk
|
||||||
|
count = var.shturval_cp_count
|
||||||
|
}
|
||||||
|
|
||||||
|
worker_configuration = local.shturval_worker_config
|
||||||
|
|
||||||
|
access_configuration = {
|
||||||
|
need_external_address_api = true # внешний адрес для Kubernetes API (false недопустим)
|
||||||
|
access_ip_list_api = jsonencode([]) # пусто = доступ всем
|
||||||
|
need_external_address_ingress = true # внешний адрес для Ingress
|
||||||
|
access_ip_list_ingress = jsonencode([]) # пусто = доступ всем
|
||||||
|
}
|
||||||
|
|
||||||
|
# Кластер поднимается только после готовой сети: vDC -> Edge -> внешние IP -> SNAT
|
||||||
|
depends_on = [nubes_vc_nsxt_snat.snat]
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
variable "api_token" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
description = "API-токен Nubes"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "api_endpoint" {
|
||||||
|
type = string
|
||||||
|
default = "https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc"
|
||||||
|
description = "API Gateway URL"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Имя (display_name, напр. "kontora") ИЛИ UUID организации из ЛК
|
||||||
|
variable "organization" {
|
||||||
|
type = string
|
||||||
|
description = "Имя или UUID организации (vc_org)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Модификаторы (IP на орге + SNAT на эдже) ---
|
||||||
|
|
||||||
|
variable "ip_space_name" {
|
||||||
|
type = string
|
||||||
|
description = "Имя ipSpace, доступное организации (смотреть в ЛК, напр. internet-ipv4-v1)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ip_count" {
|
||||||
|
type = string
|
||||||
|
default = "3"
|
||||||
|
description = "Сколько внешних IP выделить на организации (count — строка)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vdc_resource_name" {
|
||||||
|
type = string
|
||||||
|
default = "fullpipe-vdc"
|
||||||
|
description = "Имя VDC"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vdc_network_provider" {
|
||||||
|
type = string
|
||||||
|
default = null
|
||||||
|
description = "Сетевой провайдер. Заполнить значением из текущей страницы ЛК"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vdc_provider_vdc" {
|
||||||
|
type = string
|
||||||
|
default = null
|
||||||
|
description = "Provider VDC. Заполнить значением из текущей страницы ЛК"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vdc_cpu_allocated" {
|
||||||
|
type = number
|
||||||
|
default = 8
|
||||||
|
description = "vCPU (шт.)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vdc_cpu_guaranteed" {
|
||||||
|
type = number
|
||||||
|
default = 0
|
||||||
|
description = "Резервирование vCPU (%, допустимо: 0, 50, 80)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vdc_mem_allocated" {
|
||||||
|
type = number
|
||||||
|
default = 32
|
||||||
|
description = "RAM (GB)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vdc_storage_config" {
|
||||||
|
type = string
|
||||||
|
default = "[{\"name\":\"SATA\",\"size\":\"200\"}]"
|
||||||
|
description = "Дисковое хранилище (JSON-массив, size в GB). Имя политики должно существовать в ресурсном пуле (например, SATA, SSD)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- vc_nsxt (Сетевой шлюз периметра / Edge) ---
|
||||||
|
|
||||||
|
variable "nsxt_resource_name" {
|
||||||
|
type = string
|
||||||
|
default = "fullpipe-edge"
|
||||||
|
description = "Имя Edge (vc_nsxt)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "nsxt_vdc_type" {
|
||||||
|
type = string
|
||||||
|
default = "vdc"
|
||||||
|
description = "Тип родительской услуги: vdc или vdcGroup"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "nsxt_need_enable_avi" {
|
||||||
|
type = bool
|
||||||
|
default = true
|
||||||
|
description = "Включить AVI Load Balancer (ALB)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "nsxt_virtual_services_count" {
|
||||||
|
type = number
|
||||||
|
default = 3
|
||||||
|
description = "Кол-во виртуальных сервисов на AVI (1..4; Штурвал: ≥ 3)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "nsxt_ip_addr_pool" {
|
||||||
|
type = string
|
||||||
|
default = "10.10.102.0/24"
|
||||||
|
description = "Адресный пул routed-сети (маска /24 обязательна)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "nsxt_main_dns" {
|
||||||
|
type = string
|
||||||
|
default = "81.22.46.22"
|
||||||
|
description = "Основной DNS"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "nsxt_second_dns" {
|
||||||
|
type = string
|
||||||
|
default = "185.247.187.77"
|
||||||
|
description = "Второй DNS"
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
resource "nubes_vc_vdc" "vdc" {
|
||||||
|
resource_name = var.vdc_resource_name
|
||||||
|
|
||||||
|
# Организация: имя из ЛК ("kontora") или точный UUID
|
||||||
|
organization_uid = var.organization
|
||||||
|
|
||||||
|
network_provider = var.vdc_network_provider
|
||||||
|
provider_vdc = var.vdc_provider_vdc
|
||||||
|
|
||||||
|
cpu_allocated = var.vdc_cpu_allocated
|
||||||
|
cpu_guaranteed = var.vdc_cpu_guaranteed
|
||||||
|
mem_allocated = var.vdc_mem_allocated
|
||||||
|
|
||||||
|
# JSON-массив дисковых политик (size в GB)
|
||||||
|
storage_config = var.vdc_storage_config
|
||||||
|
|
||||||
|
suspend_on_destroy = true
|
||||||
|
|
||||||
|
adopt_existing_on_create = true
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.5.0"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
nubes = {
|
||||||
|
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes"
|
||||||
|
version = "2.0.23"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
НИКАКОЙ САМОДЕЙТЕЛЬНОСТИ !!! делать ТОЛЬКО ТО НА ЧТО ПОЛУЧЕНО РАЗРЕШЕНИЕ !!!!
|
||||||
|
НИКАКИХ ДОГАДОК !!! ЕСТь сомнения - СПРОСИ !!!
|
||||||
|
НИКОГДА НЕ ДЕЛАЙ ПРЕДПОЛОЖЕНИЙ !!!
|
||||||
|
ВСЕГДА СПРАШИВАЙ, ЕСЛИ НЕ УВЕРЕН !!!
|
||||||
|
НИКОГДА НЕ ИГНОРИРУЙ ПОЛУЧЕННЫЕ ИНСТРУКЦИИ !!!
|
||||||
|
ВСЕГДА ПОДТВЕРЖДАЙ ПОЛУЧЕННЫЕ ИНСТРУКЦИИ !!!
|
||||||
|
НИКОГДА НЕ ИЗМЕНЯЙ ИНСТРУКЦИИ БЕЗ РАЗРЕШЕНИЯ !!!
|
||||||
|
ВСЕГДА СОБЛЮДАЙ ПОРЯДОК И ПОСЛЕДОВАТЕЛЬНОСТЬ В ИНСТРУКЦИЯХ !!!
|
||||||
|
НИКОГДА НЕ ПРЕВЫШАЙ СВОИ ПОЛНОМОЧИЯ !!!
|
||||||
|
ВСЕГДА СОБЛЮДАЙ БЕЗОПАСНОСТЬ И КОНФИДЕНЦИАЛЬНОСТЬ !!!
|
||||||
|
НИКОГДА НЕ ПЕРЕДАВАЙ СЕКРЕТЫ ИНТЕРНЕТУ БЕЗ РАЗРЕШЕНИЯ !!!
|
||||||
|
НЕ ВЫЗЫВАТЬ ДРУГИЕ АГЕНТЫ БЕЗ РАЗРЕШЕНИЯ !!!
|
||||||
|
коммитить после каждой правки, чтобы зафиксировать текущее состояние и избежать потери изменений. Использовать осмысленные сообщения коммитов, отражающие суть изменений.
|
||||||
|
ВСЕГДА СОХРАНЯТЬ РЕЗЕРВНЫЕ КОПИИ ВАЖНЫХ ФАЙЛОВ ПЕРЕД ВНЕСЕНИЕМ ИЗМЕНЕНИЙ.
|
||||||
|
НИКОГДА НЕ ПОЛАГАЙСЯ НА ПАМЯТЬ — ВСЕГДА ПРОВЕРЯЙ АКТУАЛЬНОСТЬ ИНСТРУКЦИЙ.
|
||||||
|
ВСЕГДА СОБЛЮДАЙ ИНСТРУКЦИИ, ДАВАЙТЕ ПОДТВЕРЖДЕНИЯ И НЕ ДЕЛАЙТЕ САМОСТОЯТЕЛЬНЫХ ИЗМЕНЕНИЙ.
|
||||||
|
Если не на 100% уверен в распоряжениях - СПРОСИ СНОВА И ПОДТВЕРДИ. НЕ ГАДАЙ ЧТО Я ИМЛ ВВИДУ !!!!
|
||||||
|
ЕСЛИ В ЧЁМ совменваешься - ОСТАНОВИСЬ И СПРОСИ !!!! НИКОГДА не действуй по догадкам и преположенгиям !!! ПЕРЕПРОВЕРЯЙ ВСЁ НЕСКОЛЬКО РАЗ !!!
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
provider_installation {
|
||||||
|
dev_overrides {
|
||||||
|
"tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes" = "/home/naeel/TF/tf_provider/TMP/devbin"
|
||||||
|
}
|
||||||
|
direct {}
|
||||||
|
}
|
||||||
@@ -116,6 +116,11 @@ func LoadSpecs(dir string) ([]types.GenResource, []types.GenSubresource, []types
|
|||||||
suspendOnDestroy = *spec.Lifecycle.SuspendOnDestroyDefault
|
suspendOnDestroy = *spec.Lifecycle.SuspendOnDestroyDefault
|
||||||
}
|
}
|
||||||
|
|
||||||
|
keepOnDestroy := false
|
||||||
|
if spec.Lifecycle.KeepOnDestroyDefault != nil {
|
||||||
|
keepOnDestroy = *spec.Lifecycle.KeepOnDestroyDefault
|
||||||
|
}
|
||||||
|
|
||||||
gr := types.GenResource{
|
gr := types.GenResource{
|
||||||
Name: spec.Name,
|
Name: spec.Name,
|
||||||
ServiceID: spec.ServiceID,
|
ServiceID: spec.ServiceID,
|
||||||
@@ -128,6 +133,7 @@ func LoadSpecs(dir string) ([]types.GenResource, []types.GenSubresource, []types
|
|||||||
HasRefSvcParams: HasRefSvcParams(schemaParams),
|
HasRefSvcParams: HasRefSvcParams(schemaParams),
|
||||||
SupportsSuspendDestroy: supportsSuspendDestroy,
|
SupportsSuspendDestroy: supportsSuspendDestroy,
|
||||||
SuspendOnDestroy: suspendOnDestroy,
|
SuspendOnDestroy: suspendOnDestroy,
|
||||||
|
KeepOnDestroy: keepOnDestroy,
|
||||||
AdoptExistingOnCreate: adoptExistingOnCreate,
|
AdoptExistingOnCreate: adoptExistingOnCreate,
|
||||||
HasDomainParam: hasDomainParam,
|
HasDomainParam: hasDomainParam,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ type {{ToCamel .Name}}Model struct {
|
|||||||
{{- if .SupportsSuspendDestroy }}
|
{{- if .SupportsSuspendDestroy }}
|
||||||
SuspendOnDestroy types.Bool ` + "`" + `tfsdk:"suspend_on_destroy"` + "`" + `
|
SuspendOnDestroy types.Bool ` + "`" + `tfsdk:"suspend_on_destroy"` + "`" + `
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
KeepOnDestroy types.Bool ` + "`" + `tfsdk:"keep_on_destroy"` + "`" + `
|
||||||
AdoptExistingOnCreate types.Bool ` + "`" + `tfsdk:"adopt_existing_on_create"` + "`" + `
|
AdoptExistingOnCreate types.Bool ` + "`" + `tfsdk:"adopt_existing_on_create"` + "`" + `
|
||||||
{{- range .OutputParams }}
|
{{- range .OutputParams }}
|
||||||
{{ToCamel .Code}} {{OutputType .}} ` + "`" + `tfsdk:"{{ToSnake .Code}}"` + "`" + `
|
{{ToCamel .Code}} {{OutputType .}} ` + "`" + `tfsdk:"{{ToSnake .Code}}"` + "`" + `
|
||||||
@@ -128,6 +129,7 @@ func (r *{{ToCamel .Name}}Resource) Schema(ctx context.Context, req resource.Sch
|
|||||||
{{- if .SupportsSuspendDestroy }}
|
{{- if .SupportsSuspendDestroy }}
|
||||||
"suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool({{.SuspendOnDestroy}})},
|
"suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool({{.SuspendOnDestroy}})},
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
"keep_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool({{.KeepOnDestroy}}), MarkdownDescription: "Режим state_only при destroy: ресурс не удаляется и не меняется в облаке, только убирается из состояния. Имеет приоритет над suspend_on_destroy. false = обычное удаление."},
|
||||||
"adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool({{.AdoptExistingOnCreate}})},
|
"adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool({{.AdoptExistingOnCreate}})},
|
||||||
{{- range .OutputParams }}
|
{{- range .OutputParams }}
|
||||||
{{- if or (OutputIsMap .) (OutputIsList .) }}
|
{{- if or (OutputIsMap .) (OutputIsList .) }}
|
||||||
@@ -543,14 +545,15 @@ func (r *{{ToCamel .Name}}Resource) Delete(ctx context.Context, req resource.Del
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteMode := "delete"
|
||||||
{{- if .SupportsSuspendDestroy }}
|
{{- if .SupportsSuspendDestroy }}
|
||||||
deleteMode := "state_only"
|
|
||||||
if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
|
if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
|
||||||
deleteMode = "suspend"
|
deleteMode = "suspend"
|
||||||
}
|
}
|
||||||
{{- else }}
|
|
||||||
deleteMode := "delete"
|
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
if !state.KeepOnDestroy.IsNull() && !state.KeepOnDestroy.IsUnknown() && state.KeepOnDestroy.ValueBool() {
|
||||||
|
deleteMode = "state_only"
|
||||||
|
}
|
||||||
|
|
||||||
operationTimeout := ""
|
operationTimeout := ""
|
||||||
if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() {
|
if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() {
|
||||||
@@ -563,6 +566,19 @@ func (r *{{ToCamel .Name}}Resource) Delete(ctx context.Context, req resource.Del
|
|||||||
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch deleteMode {
|
||||||
|
case "suspend":
|
||||||
|
resp.Diagnostics.AddWarning(
|
||||||
|
"Ресурс заморожен, а не удалён",
|
||||||
|
"destroy: {{.Name}} (service_id={{.ServiceID}}) переведён в suspend, удаление не выполнялось. Для полного удаления выставьте suspend_on_destroy = false.",
|
||||||
|
)
|
||||||
|
case "state_only":
|
||||||
|
resp.Diagnostics.AddWarning(
|
||||||
|
"Ресурс оставлен как есть, а не удалён",
|
||||||
|
"destroy: {{.Name}} (service_id={{.ServiceID}}) не изменялся в облаке и только убран из состояния (keep_on_destroy = true). Для полного удаления выставьте keep_on_destroy = false.",
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *{{ToCamel .Name}}Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
func (r *{{ToCamel .Name}}Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ type ServiceSpec struct {
|
|||||||
Lifecycle struct {
|
Lifecycle struct {
|
||||||
SuspendOnDestroyDefault *bool `yaml:"suspend_on_destroy_default"`
|
SuspendOnDestroyDefault *bool `yaml:"suspend_on_destroy_default"`
|
||||||
AdoptExistingOnCreateDefault *bool `yaml:"adopt_existing_on_create_default"`
|
AdoptExistingOnCreateDefault *bool `yaml:"adopt_existing_on_create_default"`
|
||||||
|
// KeepOnDestroyDefault: дефолт ресурса для режима state_only (ничего не менять в облаке).
|
||||||
|
// Нужен там, где сервис не умеет suspend (например, Эдж) — иначе destroy удаляет объект.
|
||||||
|
KeepOnDestroyDefault *bool `yaml:"keep_on_destroy_default"`
|
||||||
} `yaml:"lifecycle"`
|
} `yaml:"lifecycle"`
|
||||||
Operations []OperationSpec `yaml:"operations"`
|
Operations []OperationSpec `yaml:"operations"`
|
||||||
}
|
}
|
||||||
@@ -75,18 +78,21 @@ type GenResource struct {
|
|||||||
HasRefSvcParams bool
|
HasRefSvcParams bool
|
||||||
SupportsSuspendDestroy bool
|
SupportsSuspendDestroy bool
|
||||||
SuspendOnDestroy bool
|
SuspendOnDestroy bool
|
||||||
AdoptExistingOnCreate bool
|
// KeepOnDestroy: destroy только убирает ресурс из состояния (state_only),
|
||||||
UsesBool bool
|
// ничего не меняя в облаке. Приоритетнее suspend_on_destroy.
|
||||||
UsesInt64 bool
|
KeepOnDestroy bool
|
||||||
UsesString bool
|
AdoptExistingOnCreate bool
|
||||||
HasDefaults bool
|
UsesBool bool
|
||||||
NeedsBoolDefault bool
|
UsesInt64 bool
|
||||||
NeedsInt64Default bool
|
UsesString bool
|
||||||
NeedsStringDefault bool
|
HasDefaults bool
|
||||||
NeedsBoolUseStateForUnknown bool
|
NeedsBoolDefault bool
|
||||||
NeedsInt64UseStateForUnknown bool
|
NeedsInt64Default bool
|
||||||
HasDomainParam bool
|
NeedsStringDefault bool
|
||||||
DomainServiceIDs []int
|
NeedsBoolUseStateForUnknown bool
|
||||||
|
NeedsInt64UseStateForUnknown bool
|
||||||
|
HasDomainParam bool
|
||||||
|
DomainServiceIDs []int
|
||||||
// HasRedeploy: сервис поддерживает redeploy (пересборка из git).
|
// HasRedeploy: сервис поддерживает redeploy (пересборка из git).
|
||||||
// Если true — в основной ресурс добавляется поле git_revision.
|
// Если true — в основной ресурс добавляется поле git_revision.
|
||||||
// При изменении git_revision вызывается redeploy вместо modify.
|
// При изменении git_revision вызывается redeploy вместо modify.
|
||||||
@@ -154,12 +160,12 @@ type GenAction struct {
|
|||||||
// GenModifier — отдельный ресурс для отложенной parent-level modify операции.
|
// GenModifier — отдельный ресурс для отложенной parent-level modify операции.
|
||||||
// Delete намеренно не содержит rollback: API-контракт обратного payload не подтверждён.
|
// Delete намеренно не содержит rollback: API-контракт обратного payload не подтверждён.
|
||||||
type GenModifier struct {
|
type GenModifier struct {
|
||||||
ServiceName string
|
ServiceName string
|
||||||
ServiceID int
|
ServiceID int
|
||||||
ModifierName string
|
ModifierName string
|
||||||
OperationName string
|
OperationName string
|
||||||
Params []Param
|
Params []Param
|
||||||
SchemaParams []Param
|
SchemaParams []Param
|
||||||
// DeleteStrategy — noop_warn | inverse | error (нормализовано из YAML, пусто → noop_warn).
|
// DeleteStrategy — noop_warn | inverse | error (нормализовано из YAML, пусто → noop_warn).
|
||||||
DeleteStrategy string
|
DeleteStrategy string
|
||||||
// Idempotency — none | check_before_run (нормализовано из YAML, пусто → none).
|
// Idempotency — none | check_before_run (нормализовано из YAML, пусто → none).
|
||||||
|
|||||||
@@ -164,6 +164,13 @@ if [[ -n "${MKDOCS_DOCS_DIR:-}" ]]; then
|
|||||||
mkdir -p "${MKDOCS_DOCS_DIR}/curated"
|
mkdir -p "${MKDOCS_DOCS_DIR}/curated"
|
||||||
cp -r "${ROOT_DIR}/docs/curated/"* "${MKDOCS_DOCS_DIR}/curated/" 2>/dev/null || true
|
cp -r "${ROOT_DIR}/docs/curated/"* "${MKDOCS_DOCS_DIR}/curated/" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
# Картинки схемы зависимостей (docs/diagrams/*.svg|png) — чтобы их можно было
|
||||||
|
# показывать на опубликованных страницах. Исходники (.mmd, .py) не копируются.
|
||||||
|
if [[ -d "${ROOT_DIR}/docs/diagrams" ]]; then
|
||||||
|
mkdir -p "${MKDOCS_DOCS_DIR}/diagrams"
|
||||||
|
cp "${ROOT_DIR}/docs/diagrams/"*.svg "${MKDOCS_DOCS_DIR}/diagrams/" 2>/dev/null || true
|
||||||
|
cp "${ROOT_DIR}/docs/diagrams/"*.png "${MKDOCS_DOCS_DIR}/diagrams/" 2>/dev/null || true
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Per-стенд подстановка во все скопированные Markdown-файлы.
|
# Per-стенд подстановка во все скопированные Markdown-файлы.
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
| Стенд | Namespace | Версия | Дата заливки |
|
| Стенд | Namespace | Версия | Дата заливки |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| PROD | `nubes` | `1.0.0` | 2026-09-03 | (новая нумерация) |
|
| PROD | `nubes` | `1.0.0` | 2026-09-03 | (новая нумерация) |
|
||||||
| DEV | `nubes-dev` | `2.0.19` | 2026-09-24 | (fix: канонизация `vip_configure` — баг порядка ключей `jsonencode`; запрет `null` в Required-атрибутах; ошибки API в Delete → error) |
|
| DEV | `nubes-dev` | `2.0.23` | 2026-09-24 | (fix: UUID внутри JSON нормализуется к lowercase при сравнении (adopt suspended-инстанса падал на регистре `nsxtUid`) + `JsonNormalize` приводит UUID-подстроки; тесты) |
|
||||||
| TEST | `nubes-test` | `3.0.0` | 2026-09-03 | (новая нумерация) |
|
| TEST | `nubes-test` | `3.0.0` | 2026-09-03 | (новая нумерация) |
|
||||||
|
|
||||||
## Как проверить
|
## Как проверить
|
||||||
|
|||||||
@@ -0,0 +1,222 @@
|
|||||||
|
# Как работает провайдер Nubes: поведение и отличия от канонического Terraform
|
||||||
|
|
||||||
|
> Страница для тех, кто уже работал с Terraform и хочет понять, чего ожидать от провайдера Nubes,
|
||||||
|
> и для DevOps, которым важно знать, что реально произойдёт в облаке при `plan`, `apply` и `destroy`.
|
||||||
|
> Здесь описан наблюдаемый контракт (что уже проверено на живых стендах) и ограничения платформы.
|
||||||
|
|
||||||
|
## 1. Суть в одном абзаце
|
||||||
|
|
||||||
|
Провайдер Nubes — это не плагин к гипервизору, а **обёртка над API личного кабинета**: каждый Terraform-ресурс
|
||||||
|
соответствует *инстансу услуги* в облаке, а `create` / `update` / `delete` транслируются в **асинхронные операции**
|
||||||
|
платформы (`create`, `modify`, `suspend`, `resume`, `delete`). Отсюда и все отличия от привычного Terraform:
|
||||||
|
|
||||||
|
- операции **долгие** — провайдер ждёт их завершения (поллинг);
|
||||||
|
- меняется **уже созданный объект**, а не создаётся заново;
|
||||||
|
- ошибки приходят **внутри тела ответа**, а не HTTP-кодом;
|
||||||
|
- ресурс ищется **по имени**, а не только по `id`;
|
||||||
|
- удаление может быть **«мягким»** — объект остаётся в облаке.
|
||||||
|
|
||||||
|
## 2. Какие ресурсы за что отвечают
|
||||||
|
|
||||||
|
| Услуга облака | Ресурс Terraform | Что делает |
|
||||||
|
|---|---|---|
|
||||||
|
| Организация в Cloud Director (19) | `nubes_vc_org_ip_allocation` | **модификатор**: меняет квоту внешних IP в организации (саму организацию создают в ЛК, Terraform её не трогает) |
|
||||||
|
| Виртуальный датацентр (21) | `nubes_vc_vdc` | создаёт vDC с ресурсами (CPU/RAM/Storage) |
|
||||||
|
| Сетевой шлюз периметра (22) | `nubes_vc_nsxt` | создаёт Edge (routed-сеть, ALB/AVI) |
|
||||||
|
| Сетевой шлюз периметра (22) | `nubes_vc_nsxt_snat` | **модификатор**: включает/переключает SNAT (`ipSpaceName`) на существующем Edge |
|
||||||
|
| Виртуальный каталог ВМ (26) | `nubes_vapp` | создаёт vApp (каталог для ВМ) на существующих vDC и Edge |
|
||||||
|
| Виртуальная машина (28) | `nubes_vc_vm_v3` | создаёт ВМ внутри vApp (CPU/RAM/диск, образ, SSH-ключ, внешний IP) |
|
||||||
|
| Kubernetes кластер Штурвал (150) | `nubes_k8s_sthutrval_cluster` | создаёт кластер (control plane + группы воркеров) |
|
||||||
|
|
||||||
|
Общее правило. Ресурс, у которого есть свой «объект в облаке», **создаёт** этот объект. А то, что платформа
|
||||||
|
меняет операцией `modify` (квота IP, включение SNAT, настройки шлюза), собрано в отдельные ресурсы-
|
||||||
|
**модификаторы**: они не создают ничего нового, а правят уже существующий объект.
|
||||||
|
|
||||||
|
## 3. Отличия от «учебного» Terraform
|
||||||
|
|
||||||
|
| Ожидание по канону | Как в Nubes | Причина |
|
||||||
|
|---|---|---|
|
||||||
|
| `create` = один вызов API | Создание инстанса — **последовательность**: `POST /instances` → `POST /instanceOperations` → параметры (`instanceOperationCfsParams`) → `run` → поллинг до `dtFinish` | Так устроен API платформы |
|
||||||
|
| `id` — произвольная строка | `id` = UUID инстанса в облаке; провайдер читает его из ответа платформы | Состояние живёт в облаке, не в Terraform |
|
||||||
|
| `update` = замена при несовместимых параметрах | `update` = операция **`modify`** над тем же инстансом; часть параметров — **create-only** (менять нельзя → ошибка на этапе plan) | Платформа не пересоздаёт объекты «из коробки» |
|
||||||
|
| есть `data sources` для поиска существующего | Поиск/ссылки — через **ref-параметры**: можно указать UUID **или имя** инстанса | Экономит data sources, но требует дисциплины в значениях |
|
||||||
|
| `import` — явная команда | Плюс к `import` есть **авто-усыновление** `adopt_existing_on_create = true`: ресурс сам находит инстанс **по имени** | В проде объекты живут неделями, и пересоздавать их нельзя |
|
||||||
|
| `plan` показывает, что ресурс уже существует | **Нет**: проверка «инстанс с таким именем уже есть / adopt» выполняется в `Create`, то есть на `apply` | Иначе ломается `destroy` и работа с tainted-ресурсами |
|
||||||
|
| `delete` удаляет | `delete` может быть `delete` / `suspend` / `state_only` — см. §5 | У платформы не всё удаляется, а часть объектов удалять нельзя, пока жив потребитель |
|
||||||
|
| ошибка приходит HTTP-кодом | Ошибка операции — **в теле**: `isSuccessful=false` + `errorLog`, при успешном HTTP-коде | API платформы отвечает 200/201 почти всегда |
|
||||||
|
| операции быстрые | Операции асинхронные и долгие (Штурвал — десятки минут) → параметр `operation_timeout`; одновременные операции на одном инстансе не поддерживаются | Наследство платформы (внутри — CFS-оркестратор) |
|
||||||
|
| план обязан совпадать с config | Тоже, но: **ref-параметры в плане остаются как написаны** (`name` не подменяется на `uuid`), а уже перед вызовом API провайдер сам разберётся, имя это или UUID | Иначе Terraform упадёт с «Provider produced invalid plan» |
|
||||||
|
| параметры сравниваются как строки | JSON-параметры сравниваются **канонично**: порядок ключей и пробелы не важны, скаляры приводятся к строке, UUID — без учёта регистра | API возвращает JSON в своём виде, иначе будет «вечный diff» |
|
||||||
|
| идентичность — по `id` | Дополнительно: инстанс ищется по `resource_name` (displayName) → **переименование = новый ресурс** | Имя задаётся при создании и не меняется |
|
||||||
|
|
||||||
|
### Что из этого следует на практике
|
||||||
|
|
||||||
|
- `terraform plan` **не может** проверить, что «такой объект уже есть»: он это покажет как `will be created`,
|
||||||
|
а разбираться будет `apply`.
|
||||||
|
- `terraform apply` на существующей инфраструктуре — это нормальный сценарий (adopt), если включён
|
||||||
|
`adopt_existing_on_create`; без флага вы получите явную ошибку-конфликт, а не дубль ресурса.
|
||||||
|
- Долгие операции требуют терпения: смотрите `operation_timeout`, не запускайте вторую операцию по тому же объекту.
|
||||||
|
|
||||||
|
## 4. Создание, чтение, изменение, удаление
|
||||||
|
|
||||||
|
**Создание.** Провайдер формирует параметры операции, отправляет её и ждёт завершения. Параметры, зависящие
|
||||||
|
от других ресурсов (например `vdc_uid`, `nsxt_uid`), передаются как ref-значения: UUID или имя.
|
||||||
|
|
||||||
|
**Чтение (refresh).** Значения `state_params` / `state_out` приходят из облака: так в state попадают
|
||||||
|
адреса (`kubernetesApiAddress`, `ingressAddress`), имена, текущие параметры.
|
||||||
|
|
||||||
|
**Изменение.** Если параметры поменялись, вызывается `modify` (для модификаторов — обратный `modify` при удалении).
|
||||||
|
Параметры, помеченные как create-only, изменить нельзя — провайдер скажет об этом до обращения к API.
|
||||||
|
|
||||||
|
**Удаление.** См. следующий раздел — это самое неочевидное место.
|
||||||
|
|
||||||
|
## 5. Удаление: `delete`, `suspend` и «оставить как есть»
|
||||||
|
|
||||||
|
У платформы три разных исхода, и провайдер умеет все три. Управляется флагами:
|
||||||
|
|
||||||
|
| Флаг | Где применим | Поведение при `destroy` | Предупреждение в выводе |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `suspend_on_destroy = true` | кластер Штурвала, vDC, vApp, ВМ (по умолчанию `true`) | объект **приостанавливается**, не удаляется | «Ресурс заморожен, а не удалён» |
|
||||||
|
| `keep_on_destroy = true` | Edge, SNAT, квота IP (по умолчанию `false`) | объект **не трогается в облаке**, только убирается из state | «Оставлен как есть» (для SNAT — «SNAT не выключался», для квоты IP — «Аллокация IP не снималась») |
|
||||||
|
| оба `false` | любой | обычное удаление | — |
|
||||||
|
|
||||||
|
Если выставлены оба флага, победит `keep_on_destroy` (объект просто не тронут). По умолчанию провайдер
|
||||||
|
удаляет по-настоящему (`keep_on_destroy = false`), а «заморозку» или «оставить как есть» включают явно
|
||||||
|
в конфигурации стенда.
|
||||||
|
|
||||||
|
### Почему Edge остаётся `running`
|
||||||
|
|
||||||
|
Edge **физически не умеет `suspend`**: в списке доступных операций шлюза есть только `delete`, `modify`,
|
||||||
|
`reconcile`. Усыпить его платформа не даёт, поэтому единственный корректный способ «не удалять шлюз» —
|
||||||
|
не трогать его: `keep_on_destroy = true`. Побочный эффект — шлюз продолжает работать и тарифицироваться,
|
||||||
|
и через него продолжают публиковаться внешние адреса (API кластера и ingress). Именно поэтому удаление Edge
|
||||||
|
при живом кластере Штурвала (или других зависимых объектах: vApp, VM) считается недопустимым.
|
||||||
|
|
||||||
|
### Почему SNAT остаётся включённым, а квота IP не обнуляется
|
||||||
|
|
||||||
|
- SNAT-модификатор с `keep_on_destroy = true` **не отправляет** обратный `modify` с `ipSpaceName = "no-needed"`,
|
||||||
|
то есть SNAT для виртуальных машин остаётся в прежнем состоянии.
|
||||||
|
- Квота внешних IP: уменьшать `count` **ниже фактически занятых адресов платформа не разрешает**
|
||||||
|
(ошибка вида «Кол-во занятых Ip … Невозможно выставить параметр count ниже этого параметра»).
|
||||||
|
Адреса держит кластер Штурвала (API + ingress), и `suspend` кластера их **не освобождает**;
|
||||||
|
если у ВМ есть внешний адрес, он тоже держится инстансом ВМ — даже остановленной.
|
||||||
|
Поэтому при «заморозке» квота не изменяется вовсе, а реальное освобождение адресов возможно только после
|
||||||
|
удаления кластера — отдельным шагом.
|
||||||
|
|
||||||
|
### Почему кластер и vDC уходят в `suspend`
|
||||||
|
|
||||||
|
Для них `suspend` поддерживается платформой, и это самый близкий к «выключить» вариант: ВМ останавливаются,
|
||||||
|
объекты не удаляются, адреса и конфигурация сохраняются. Обратный ход делает `apply`:
|
||||||
|
|
||||||
|
| Что | При `destroy` (заморозка) | При следующем `apply` |
|
||||||
|
|---|---|---|
|
||||||
|
| Кластер Штурвала | `suspend` | adopt по имени + `resume` |
|
||||||
|
| vDC | `suspend` | adopt по имени + `resume` |
|
||||||
|
| vApp | `suspend` | adopt по имени + `resume` |
|
||||||
|
| ВМ | `suspend` | adopt по имени + `resume` |
|
||||||
|
| Edge | не трогается (`running`) | adopt (инстанс уже работает) |
|
||||||
|
| SNAT | не трогается (включён) | повторный `modify` теми же значениями (фактически no-op) |
|
||||||
|
| Квота IP | не трогается | `modify` с тем же `count` (no-op) |
|
||||||
|
|
||||||
|
Итого цикл «`destroy` → `apply`» на стенде выглядит как «усыпить → разбудить», а не «снести → поднять заново».
|
||||||
|
Полностью удалить такой стенд можно только явным отказом от заморозки (`keep_on_destroy = false` /
|
||||||
|
`suspend_on_destroy = false`) и в правильном порядке (см. §6).
|
||||||
|
|
||||||
|
## 6. Конкретный пайплайн стенда: vDC → Edge → внешние IP → SNAT → vApp → ВМ → Штурвал
|
||||||
|
|
||||||
|
Порядок создания и зависимости:
|
||||||
|
|
||||||
|
1. Организация — **вручную в ЛК** (Terraform её не создаёт).
|
||||||
|
2. `nubes_vc_vdc` — виртуальный датацентр.
|
||||||
|
3. `nubes_vc_nsxt` — Edge. Обязательно включать балансировщик (параметр `need_enable_avi = true`)
|
||||||
|
и задать не меньше 3 виртуальных сервисов (`virtual_services_count >= 3`) — это нужно кластеру Штурвала.
|
||||||
|
4. `nubes_vc_org_ip_allocation` — внешние адреса в организации (минимум 3 по инструкции услуги Штурвала,
|
||||||
|
плюс ещё один, если у ВМ будет внешний адрес).
|
||||||
|
5. `nubes_vc_nsxt_snat` — SNAT на Edge (по `depends_on` после аллокации адресов).
|
||||||
|
6. `nubes_vapp` — vApp: привязка к vDC (`vdc_uid`) и Edge (`nsxt_uid`); создаётся после SNAT,
|
||||||
|
чтобы сеть была уже готова.
|
||||||
|
7. `nubes_vc_vm_v3` — ВМ внутри vApp (`vapp_uid`): образ, CPU/RAM/диск, учётка и SSH-ключ, порты FW.
|
||||||
|
8. `nubes_k8s_sthutrval_cluster` — кластер Штурвала (по `depends_on` после SNAT: нодам нужен выход в интернет).
|
||||||
|
|
||||||
|
При удалении Terraform идёт в обратном порядке. Ограничения платформы, которые встречаются на этом пути:
|
||||||
|
|
||||||
|
- **1 кластер Штурвала = 1 vDC** (действующее ограничение услуги).
|
||||||
|
- vDC удаляется только **через 14 дней после `suspend`**; при живых Edge/vApp/VM/кластере — через поддержку.
|
||||||
|
- У vApp удаление требует **предварительного `suspend`**, полное удаление — тоже через 14 дней.
|
||||||
|
- У vApp и ВМ есть `suspend` и `resume`; у Edge операции `suspend` нет вообще.
|
||||||
|
- **ВМ не включится, если в vDC не осталось vCPU**: Штурвал при настройках по умолчанию занимает всё
|
||||||
|
(2 ноды × 4 vCPU при `vdc_cpu_allocated = 8`). Платформа отвечает общей ошибкой
|
||||||
|
`[400:VALIDATION] Unable to perform this action` на этапе power-on, инстанс остаётся в `not created`.
|
||||||
|
- Создание ВМ идёт дольше остальных ресурсов (customization + power-on) — задавайте `operation_timeout = "15m"`.
|
||||||
|
- Edge не удаляется при живых зависимых объектах (по инструкции услуги).
|
||||||
|
- Квота IP не опускается ниже занятых адресов (см. §5).
|
||||||
|
|
||||||
|
Как проверить, что конфигурация корректна: `terraform plan` после `apply` должен говорить
|
||||||
|
`No changes`. Если появляется стабильный diff — сверяйте его с `terraform state show` и с тем,
|
||||||
|
что реально показывается в личном кабинете.
|
||||||
|
|
||||||
|
## 7. Регистр UUID и канонизация значений
|
||||||
|
|
||||||
|
UUID в облаке не имеет «правильного» регистра: один и тот же идентификатор может прийти как `2c37fed1-…`
|
||||||
|
и как `2C37FED1-…`. Поэтому провайдер сравнивает UUID **без учёта регистра** — в том числе внутри JSON-параметров
|
||||||
|
(например, `startupConfiguration` кластера Штурвала). Подробный разбор проблемы и всех мест, где она может
|
||||||
|
проявиться, — в `60_strategy/terraform_case_sensitivity_fix.md` (внутренний документ).
|
||||||
|
|
||||||
|
Для JSON-параметров сравнение также игнорирует порядок ключей и пробелы, а скаляры приводит к строковому виду —
|
||||||
|
это нужно, чтобы `plan` не показывал «изменение» там, где API вернул то же значение в другом формате.
|
||||||
|
|
||||||
|
## 8. Чек-лист DevOps
|
||||||
|
|
||||||
|
- Пин версии провайдера привязан к стенду: **prod `1.*`, dev `2.*`, test `3.*`**; после смены версии —
|
||||||
|
`terraform init -upgrade`.
|
||||||
|
- Перед `apply` и после — смотрите `plan`; ожидаемый финальный результат — `No changes`.
|
||||||
|
- **Читайте предупреждения `destroy`**: «заморожен», «оставлен как есть», «SNAT не выключался» — это не косметика,
|
||||||
|
а отчёт о том, что объекты продолжают жить и тарифицироваться.
|
||||||
|
- Не удаляйте объекты стенда руками в ЛК между `destroy` и `apply`: авто-усыновление ищет их по имени, и на удалённом
|
||||||
|
объекте упрётся в состояние `not created`.
|
||||||
|
- Долгие операции: задавайте `operation_timeout` (для Штурвала — `60m`), не запускайте параллельные операции
|
||||||
|
по одному объекту.
|
||||||
|
- Проверяя состояние через API ЛК, помните про обязательные заголовки (браузерный `User-Agent` и `Referer`) —
|
||||||
|
иначе отдаёт `403`.
|
||||||
|
|
||||||
|
## 9. FAQ
|
||||||
|
|
||||||
|
**Почему `plan` пишет `will be created`, если объект уже есть?**
|
||||||
|
Потому что проверка существования и усыновление выполняются на `apply` (в `Create`). В state ресурса нет → план
|
||||||
|
честно планирует создание. Решение — `adopt_existing_on_create = true`.
|
||||||
|
|
||||||
|
**Почему `destroy` сказал `5 destroyed`, а в облаке всё живо?**
|
||||||
|
Сработала «заморозка»: кластер и vDC приостановлены, Edge, SNAT и квота IP не изменялись. Terraform «удалил»
|
||||||
|
ресурсы только из своего состояния. Смотрите предупреждения в выводе.
|
||||||
|
|
||||||
|
**Почему IP не освободились?**
|
||||||
|
Квота не может стать меньше числа занятых адресов, а их держит кластер. Пока кластер существует (даже в `suspend`),
|
||||||
|
платформа не даст уменьшить `count`.
|
||||||
|
|
||||||
|
**Почему Edge нельзя приостановить?**
|
||||||
|
У платформы для шлюза нет операции `suspend` — только `delete`, `modify`, `reconcile`.
|
||||||
|
|
||||||
|
**Почему после `apply` кластер ожил сам?**
|
||||||
|
Сработало усыновление: ресурс нашёл инстанс по имени (`adopt_existing_on_create`), увидел статус `suspended`
|
||||||
|
и выполнил `resume`.
|
||||||
|
|
||||||
|
**State пуст, а объекты в облаке есть. Что делать?**
|
||||||
|
Это ожидаемый результат «заморозки». `terraform apply` в том же каталоге усыновит объекты обратно.
|
||||||
|
|
||||||
|
**Кто-то удалил объект в ЛК. Что будет?**
|
||||||
|
Усыновление не найдёт его и провайдер завершится ошибкой с указанием статуса (`not created`) — нужно либо создать
|
||||||
|
объект заново, либо разбираться с конфигурацией.
|
||||||
|
|
||||||
|
**А можно всё-таки удалить стенд полностью?**
|
||||||
|
Да, но осознанно: выставить `keep_on_destroy = false` и `suspend_on_destroy = false`, затем удалять в порядке
|
||||||
|
кластер → квота IP → SNAT → Edge → vDC, при необходимости — через поддержку (для vDC действует правило 14 дней).
|
||||||
|
|
||||||
|
## 10. Куда смотреть дальше
|
||||||
|
|
||||||
|
- [Как развернуть цепочку: vDC → Edge → внешние IP → SNAT → vApp → ВМ → Штурвал](https://tf-docs.nodejsk8s.dev.nubes.ru/nubes-dev/curated/pipeline/vdc_edge_ip_snat/) —
|
||||||
|
пошаговая инструкция по всей цепочке вашего стенда.
|
||||||
|
- `curated/modifiers/org_ip_and_snat.md` — ресурсы-модификаторы.
|
||||||
|
- `30_registry/guides/terraform-structure.md` — структура манифестов.
|
||||||
|
- Внутренние документы (не публикуются): `60_strategy/provider_philosophy.md`,
|
||||||
|
`60_strategy/modifier_resources_ideology_and_specification.md`,
|
||||||
|
`60_strategy/adopt_ref_validation.md`, `60_strategy/terraform_case_sensitivity_fix.md`.
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
# Зависимости облачных сервисов каталога DEV
|
||||||
|
|
||||||
|
Промежуточная инвентаризация для будущего общего графа. Источник — YAML-контракты
|
||||||
|
в `generated/dev/resources_yaml`.
|
||||||
|
|
||||||
|
Вершина — сервис каталога. Направление будущей стрелки: `потребитель -> зависимость`.
|
||||||
|
В таблицу зависимостей включены только связи, явно подтверждённые полем
|
||||||
|
`ref_svc_id` или тем же контрактом сервиса.
|
||||||
|
|
||||||
|
## Все сервисы каталога
|
||||||
|
|
||||||
|
| ID | Сервис | YAML |
|
||||||
|
|---:|---|---|
|
||||||
|
| 1 | Болванка | `1_dummy.yaml` |
|
||||||
|
| 2 | Темплейт k8s | `2_template.yaml` |
|
||||||
|
| 12 | S3 Object Storage | `12_s3.yaml` |
|
||||||
|
| 13 | S3 бакет | `13_s3bucket.yaml` |
|
||||||
|
| 19 | Организация в Cloud Director | `19_vc_org.yaml` |
|
||||||
|
| 21 | Виртуальный датацентр (vDC) | `21_vc_vdc.yaml` |
|
||||||
|
| 22 | Сетевой шлюз периметра (Edge) | `22_vc_nsxt.yaml` |
|
||||||
|
| 25 | Публичные IP адреса | `25_vcexternalip.yaml` |
|
||||||
|
| 26 | Виртуальный каталог ВМ (vApp) | `26_vapp.yaml` |
|
||||||
|
| 28 | Виртуальная машина | `28_vc_vm_v3.yaml` |
|
||||||
|
| 29 | Группа датацентров | `29_vc_vdc_group.yaml` |
|
||||||
|
| 50 | Nextcloud | `50_nextcloud.yaml` |
|
||||||
|
| 81 | Apache Superset | `81_superset.yaml` |
|
||||||
|
| 82 | Container Registry | `82_harbor.yaml` |
|
||||||
|
| 86 | Velero | `86_k8s_velero.yaml` |
|
||||||
|
| 87 | Valkey | `87_k8svalkey.yaml` |
|
||||||
|
| 88 | k8sZitiController | `88_k8s_ziti_controller.yaml` |
|
||||||
|
| 89 | Web-сервер с фреймворком Flask | `89_flask.yaml` |
|
||||||
|
| 90 | PostgreSQL | `90_postgres.yaml` |
|
||||||
|
| 91 | Redis | `91_redis.yaml` |
|
||||||
|
| 92 | MongoDB | `92_mongodb.yaml` |
|
||||||
|
| 93 | RabbitMQ | `93_rabbitmq.yaml` |
|
||||||
|
| 94 | Lucee | `94_lucee.yaml` |
|
||||||
|
| 95 | NodeJS | `95_nodejs.yaml` |
|
||||||
|
| 96 | pgAdmin | `96_pgadmin.yaml` |
|
||||||
|
| 97 | NodeRed | `97_nodered.yaml` |
|
||||||
|
| 98 | Простой HTTP контейнер | `98_http.yaml` |
|
||||||
|
| 99 | Gitea | `99_gitea.yaml` |
|
||||||
|
| 109 | Управление DNS | `109_zones_v2.yaml` |
|
||||||
|
| 111 | DNS запись | `111_dnsrecord.yaml` |
|
||||||
|
| 115 | Mariadb | `115_mariadb.yaml` |
|
||||||
|
| 116 | ApacheKafka | `116_kafka.yaml` |
|
||||||
|
| 119 | Akhq | `119_akhq.yaml` |
|
||||||
|
| 120 | ClickHouse | `120_clickhouse.yaml` |
|
||||||
|
| 148 | Менеджмент Kubernetes кластер Штурвал | `148_vc_mgmt_sthutrval_cluster.yaml` |
|
||||||
|
| 149 | VALO Cloud | `149_valo_tenant.yaml` |
|
||||||
|
| 150 | Kubernetes кластер Штурвал | `150_k8s_sthutrval_cluster.yaml` |
|
||||||
|
| 151 | Vault | `151_k8s_openbao.yaml` |
|
||||||
|
| 153 | Nifi | `153_nifi.yaml` |
|
||||||
|
| 163 | ML Улей | `163_llm_ai.yaml` |
|
||||||
|
|
||||||
|
## Подтверждённые зависимости
|
||||||
|
|
||||||
|
| ID | Потребитель | Зависимость | Поле связи | Смысл связи |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| D-01 | DNS запись (111) | Управление DNS (109) | `zoneUid` | запись создаётся в DNS-зоне |
|
||||||
|
| D-02 | S3 бакет (13) | S3 Object Storage (12) | `s3UserUid` | бакет создаётся для корневой S3-услуги |
|
||||||
|
| D-03 | Виртуальный датацентр (21) | Организация в Cloud Director (19) | `organizationUid` | vDC принадлежит организации |
|
||||||
|
| D-04 | Сетевой шлюз периметра (22) | Виртуальный датацентр (21) | `vdcUid` | Edge привязывается к vDC |
|
||||||
|
| D-05 | Сетевой шлюз периметра (22) | Группа датацентров (29) | `vdcGroupUid` | альтернативная привязка Edge к группе vDC (vdcType=vdcGroup) |
|
||||||
|
| D-06 | Виртуальный каталог ВМ (26) | Сетевой шлюз периметра (22) | `nsxtUid` | vApp использует Edge для сети |
|
||||||
|
| D-07 | Виртуальный каталог ВМ (26) | Виртуальный датацентр (21) | `vdcUid` | vApp размещается в vDC |
|
||||||
|
| D-08 | Виртуальная машина (28) | Виртуальный каталог ВМ (26) | `vappUid` | VM размещается в vApp |
|
||||||
|
| D-09 | Группа датацентров (29) | Виртуальный датацентр (21) | `vdcUid` | группа объединяет vDC (create и add_vdc) |
|
||||||
|
| D-10 | Akhq (119) | ApacheKafka (116) | `kafkaUid` | UI подключается к Kafka-кластеру |
|
||||||
|
| D-11 | Container Registry (82) | S3 Object Storage (12) | `s3Uid` | registry использует S3 для хранения |
|
||||||
|
| D-12 | Gitea (99) | PostgreSQL (90) | `psqlUid` | Gitea использует PostgreSQL |
|
||||||
|
|
||||||
|
## Переиспользование существующих инстансов
|
||||||
|
|
||||||
|
Зависимости (кроме Организации) являются ссылками на **существующие инстансы** через UUID:
|
||||||
|
|
||||||
|
- Новую ВМ (28) можно создать в уже существующем vApp, vDC, Edge — ссылкой `vappUid`/`vdcUid`/`nsxtUid`.
|
||||||
|
- Edge (22) можно переиспользовать, если он привязан к нужному vDC или groupvDC.
|
||||||
|
- Для Штурвала (150) переиспользуемый Edge должен иметь включённый ALB и `virtualServicesCount >= 3`.
|
||||||
|
- Внешние IP резервируются только после: Организация + vDC + Каталог (vApp) + Edge (по `25_vcexternalip.yaml`, `service_man`).
|
||||||
|
|
||||||
|
## Где выделяются ресурсы (CPU / RAM / Storage / IP)
|
||||||
|
|
||||||
|
| Элемент | Где задаётся | Параметр(ы) |
|
||||||
|
|---|---|---|
|
||||||
|
| Пул CPU/RAM/Storage организации | vDC (21) create/modify | `cpuAllocated`, `memAllocated`, `storageConfig` (+ разовый `cpuGuaranteed` 0/50/80) |
|
||||||
|
| Запрос ресурсов нод Штурвала | Кластер Штурвал (150) | `controlPlaneConfiguration.sizingPolicy/sizingDisk/count`, `workerConfiguration…` (политики из vDC) |
|
||||||
|
| Запрос ресурсов ВМ | Виртуальная машина (28) | `vmCpu`, `vmRam`, `vmDisk` (доп. диск) |
|
||||||
|
| Внешние IP | vcOrg (19) `modify` (`vIPConfigure`) или `vcexternalip` (25) | `ipSpaceName`, квота адресов |
|
||||||
|
| SNAT-правило | Edge (22) `modify` | `ipSpaceName` |
|
||||||
|
| DNAT/SNAT-правила адреса | Публичные IP (25) | `dnatCreate`/`snatCreate`, `internalAddrAccess`, `internalPortAccess` |
|
||||||
|
|
||||||
|
## Чек-лист создания Штурвала (150) — по `service_man`
|
||||||
|
|
||||||
|
1. Организация (19) — create.
|
||||||
|
2. vDC (21) — create, выделить ресурсы под все ноды.
|
||||||
|
3. Edge (22) — create, включить ALB и `virtualServicesCount >= 3`.
|
||||||
|
4. Внешние адреса в организации — vcOrg `modify`, суммарно 3 адреса.
|
||||||
|
5. SNAT — Edge `modify`, выбрать `ipSpaceName`.
|
||||||
|
6. Кластер Штурвал (150) — create (требует `vdcUid` и `nsxtUid`).
|
||||||
|
|
||||||
|
## Чек-лист создания ВМ (28) — по create-параметрам
|
||||||
|
|
||||||
|
1. vDC (21) — существующий (`vdcUid`).
|
||||||
|
2. Edge (22) — существующий (`nsxtUid`).
|
||||||
|
3. vApp (26) — существующий (`vappUid`), привязан к vDC и Edge.
|
||||||
|
4. ВМ (28) — create; обязательные: `vappUid`, `vmName`, `vmCpu`, `vmRam`, `accessPortList`, `imageVm`, `userLogin`, `userPublicKey`, `ipSpaceName`.
|
||||||
|
|
||||||
|
## Правила для будущего графа
|
||||||
|
|
||||||
|
1. Рисовать стрелку только по подтверждённой связи сервиса каталога.
|
||||||
|
2. `ref_svc_id` считать прямой зависимостью; `descr` использовать для подписи
|
||||||
|
стрелки и проверки смысла.
|
||||||
|
3. Не считать общей платформой, DNS-полем или внешним IP автоматическую связь
|
||||||
|
между сервисами без ссылки на конкретный сервисный ID.
|
||||||
|
4. Поля без `ref_svc_id` вынести в отдельный список кандидатов на проверку, но не
|
||||||
|
смешивать с подтверждёнными рёбрами.
|
||||||
@@ -239,5 +239,59 @@ gr.NeedsStringsImport = hasRestoreCasingParams(gr.SchemaParams) || analyzeNeedsS
|
|||||||
|
|
||||||
## 9. Версия
|
## 9. Версия
|
||||||
|
|
||||||
Фикс введён в версии провайдера **5.0.46**.
|
Фикс введён в версии провайдера **5.0.46** (старая линия; актуальные линии — prod `1.*`, dev `2.*`, test `3.*`, см. §10).
|
||||||
Сгенерированные ресурсы пересозданы после изменения генератора.
|
Сгенерированные ресурсы пересозданы после изменения генератора.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Обновление 2026-09-24: UUID внутри JSON (важно)
|
||||||
|
|
||||||
|
**Что уточнилось.** Посылка «API всегда возвращает lowercase» **неверна**. На живом стенде (Shturval dev-00)
|
||||||
|
один и тот же UUID приходил в разных регистрах: `vdcUid` — `d0937335-…` (lowercase), а `nsxtUid` кластера —
|
||||||
|
`2C37FED1-…` (UPPERCASE). Значит ориентироваться на «API нормализует» нельзя: сравнивать нужно всегда
|
||||||
|
без учёта регистра.
|
||||||
|
|
||||||
|
**Где вылезло.** Первый `apply` после «заморозки» стенда упал на усыновлении приостановленного кластера:
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: required params mismatch for resource_name shturval-dev: startupConfiguration
|
||||||
|
(plan={… "nsxtUid":"2c37fed1-…" }, actual={… "nsxtUid":"2C37FED1-…" }).
|
||||||
|
```
|
||||||
|
|
||||||
|
**Почему предыдущие пять фиксов не помогли.** Они закрывали: отправку в API (`core/refsvc.go`,
|
||||||
|
`core/refsvc_resolve.go`), сравнение **одиночных** значений (`resources_core/params_compare.go`,
|
||||||
|
`normalizeCompareValue`), сравнение create-only атрибутов (`strings.EqualFold` в шаблоне генератора)
|
||||||
|
и восстановление регистра в state. Ни один из них не смотрит **внутрь JSON**, а adopt приостановленного
|
||||||
|
инстанса сравнивает параметр целиком как JSON:
|
||||||
|
`RequiredParamsMismatch` → `paramsEquivalent` → `JSONStringsEquivalent` → `jsonutil.normalizeJSONScalarsToStrings`,
|
||||||
|
где строки возвращались как есть (`case string: return val`). У Штурвала ref-параметры упакованы в JSON
|
||||||
|
(`startupConfiguration`), а путь adopt-suspended задействован впервые.
|
||||||
|
|
||||||
|
**Аудит: где регистр UUID может вылезти (проверено 24.09).**
|
||||||
|
|
||||||
|
| # | Место | Что ломает |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | `resources_core/required_params_compare.go` (`paramsEquivalent` → `JSONStringsEquivalent`) | adopt приостановленного инстанса — hard error (этот кейс) |
|
||||||
|
| 2 | `core/modifier_compare.go` | ложное «параметр изменился» → лишний `modify` на каждом apply |
|
||||||
|
| 3 | `resources_core/state_refresh.go` (сохранение планового JSON) | в state уедет регистр API вместо значения из config |
|
||||||
|
| 4 | `resources_core/resource_diagnostics_required.go` (create-диагностика) | та же `RequiredParamsMismatch` |
|
||||||
|
| 5 | `resources_core/params_compare.go` (`ParamsMatchForResume`) | одиночный UUID ок, JSON — та же дыра |
|
||||||
|
| 6 | `resources_core/json_planmodifier.go` (`JsonNormalize()`) | для JSON-атрибутов с UUID внутри — риск вечного diff |
|
||||||
|
| 7 | `resources_core/ref_validation.go` (`ValidateRefParamsOnAdopt`) | ref-параметр, зашитый внутрь JSON, **не проверяется вообще** (открыто) |
|
||||||
|
| 8 | `core/operation_run*.go` (`lookupLiveParam`) | подстановка live-значений по ключам: при другом регистре ключа может молча не сработать (открыто, требует живой проверки) |
|
||||||
|
|
||||||
|
**Фикс (провайдер `2.0.23`, dev).**
|
||||||
|
|
||||||
|
- `internal/core/jsonutil/jsonutil.go`: добавлен `LowercaseUUIDsInText` (UUID-подстрока → lowercase), и строковые
|
||||||
|
значения **внутри JSON** теперь нормализуются в `normalizeJSONScalarsToStrings` — закрывает пункты 1–5.
|
||||||
|
- `internal/resources_core/json_planmodifier.go`: `JsonNormalize()` после `json.Compact` приводит UUID-подстроки
|
||||||
|
к lowercase (типы и порядок ключей **не** меняются — план обязан совпадать с config) — закрывает пункт 6.
|
||||||
|
- Тесты: `internal/core/jsonutil/jsonutil_test.go`, `internal/resources_core/params_compare_test.go`
|
||||||
|
(в т.ч. на реальном `startupConfiguration` кластера).
|
||||||
|
|
||||||
|
**Дополнение к алгоритму диагностики (§8):**
|
||||||
|
|
||||||
|
7. Если расхождение — внутри JSON-параметра, проверьте именно UUID-подстроки и их регистр (не только одиночные
|
||||||
|
значения); смотрите `jsonutil.LowercaseUUIDsInText`.
|
||||||
|
8. Не «лечите» это нормализацией плана целиком (скаляры → строки, сортировка ключей): для атрибутов из config
|
||||||
|
допустимо менять только регистр UUID-подстрок, иначе Terraform ругнётся на несоответствие плана конфигу.
|
||||||
|
|||||||
@@ -1,75 +1,77 @@
|
|||||||
# Ресурсы-модификаторы: внешние IP организации и SNAT на шлюзе
|
# Ресурсы-модификаторы: внешние IP организации и SNAT на шлюзе
|
||||||
|
|
||||||
> ⚠️ Живым прогоном пока **не проверено**. Описаны ресурсы и порядок применения; выводы о поведении
|
Два ресурса работают с **уже существующими** услугами:
|
||||||
> сделаны по коду провайдера и HAR-записям ЛК.
|
|
||||||
|
|
||||||
Два ресурса выполняют операции `modify` над **уже существующими** услугами. Организация Cloud Director
|
- `nubes_vc_org_ip_allocation` — выделяет внешние IP на организации;
|
||||||
и сетевой шлюз периметра создаются один раз **вручную в ЛК** и адресуются по UUID.
|
- `nubes_vc_nsxt_snat` — включает SNAT на сетевом шлюзе периметра.
|
||||||
|
|
||||||
## `nubes_vc_org_ip_allocation`
|
Эти параметры нельзя задать при создании услуги обычным ресурсом, поэтому они вынесены в отдельные
|
||||||
|
ресурсы: так вся цепочка поднимается одним `apply` и в правильном порядке.
|
||||||
|
|
||||||
Выделяет внешние IP на организации (операция `modify`, параметр `vIPConfigure`).
|
Оба ресурса проверены живым прогоном: создание, проверка состояния в ЛК и удаление.
|
||||||
|
|
||||||
|
Полный рабочий пример (vDC + Edge + IP + SNAT + vApp + ВМ + Штурвал) — на странице
|
||||||
|
[«Как развернуть цепочку: vDC → Edge → внешние IP → SNAT → vApp → ВМ → Штурвал»](../pipeline/vdc_edge_ip_snat.md),
|
||||||
|
файлы — в репозитории `tf_examples`, папка `fullpipe_chain`.
|
||||||
|
|
||||||
|
## `nubes_vc_org_ip_allocation` — внешние IP на организации
|
||||||
|
|
||||||
| Атрибут | Тип | Описание |
|
| Атрибут | Тип | Описание |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `org_uid` | string, обязательный | UUID услуги «Организация в Cloud Director» |
|
| `organization` | string, обязательный | Организация: название услуги из ЛК или её UUID |
|
||||||
| `vip_configure` | string (JSON), обязательный | Массив аллокаций: `[{"name":"internet-ipv4-v1","count":"3"}]`. `count` — строка |
|
| `vip_configure` | string (JSON), обязательный | Сколько IP выделить: `[{"name":"internet-ipv4-v1","count":"3"}]`. `count` — строкой. Для Штурвала нужно не меньше 3 адресов, ещё один — если у ВМ будет внешний адрес |
|
||||||
| `keep_on_destroy` | bool, по умолчанию `false` | Не снимать квоту при `destroy` |
|
| `keep_on_destroy` | bool, по умолчанию `false` | `false` — при удалении квота обнуляется; `true` — остаётся как есть |
|
||||||
|
|
||||||
Порядок ключей и форматирование не важны — значение канонизируется при планировании
|
|
||||||
(важно потому, что `jsonencode` сортирует ключи по алфавиту).
|
|
||||||
|
|
||||||
Снять аллокацию через `vip_configure` **нельзя** (пустой массив отклоняется): для этого удали ресурс —
|
|
||||||
тогда отправится обратный `modify` с `count = "0"`.
|
|
||||||
|
|
||||||
```hcl
|
```hcl
|
||||||
resource "nubes_vc_org_ip_allocation" "this" {
|
resource "nubes_vc_org_ip_allocation" "this" {
|
||||||
org_uid = var.org_uid
|
organization = "organ" # название услуги из ЛК
|
||||||
|
|
||||||
vip_configure = jsonencode([
|
vip_configure = jsonencode([
|
||||||
{ name = "internet-ipv4-v1", count = "3" }
|
{ name = "internet-ipv4-v1", count = "3" }
|
||||||
])
|
])
|
||||||
|
|
||||||
|
keep_on_destroy = false
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Операция имеет **replace-семантику**: массив перезаписывается целиком, поэтому ресурс владеет всей
|
Как это работает:
|
||||||
аллокацией организации, а не отдельным элементом.
|
|
||||||
|
|
||||||
При `destroy` по умолчанию отправляется обратный `modify` с `count = "0"` — квота обнуляется.
|
- массив `vip_configure` записывается **целиком**, поэтому ресурс владеет всей аллокацией организации:
|
||||||
|
если нужно поменять количество — меняйте значение и делайте `apply`;
|
||||||
|
- снять аллокацию через пустой массив нельзя — для этого удалите ресурс
|
||||||
|
(`terraform destroy -target=nubes_vc_org_ip_allocation.this`), тогда отправится запрос с `count = "0"`;
|
||||||
|
- **считайте потребителей адресов**: кластер Штурвал держит 3 адреса (API и Ingress), а ВМ с внешним
|
||||||
|
адресом (`ip_space_name = "internet-ipv4-v1"`) занимает ещё один — итого `count = "4"`.
|
||||||
|
Адреса освобождаются только после удаления потребителя: `suspend` их не снимает.
|
||||||
|
|
||||||
## `nubes_vc_nsxt_snat`
|
## `nubes_vc_nsxt_snat` — SNAT на шлюзе
|
||||||
|
|
||||||
Включает или выключает SNAT на сетевом шлюзе периметра (операция `modify`, параметр `ipSpaceName`).
|
|
||||||
|
|
||||||
| Атрибут | Тип | Описание |
|
| Атрибут | Тип | Описание |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `nsxt_uid` | string, обязательный | UUID услуги «Сетевой шлюз периметра (Edge)» |
|
| `nsxt_uid` | string, обязательный | UUID услуги «Сетевой шлюз периметра (Edge)» |
|
||||||
| `ip_space_name` | string, обязательный | Имя ipSpace для внешнего IP; `no-needed` выключает SNAT |
|
| `ip_space_name` | string, обязательный | Имя ipSpace для внешнего IP; значение `no-needed` выключает SNAT |
|
||||||
| `keep_on_destroy` | bool, по умолчанию `false` | Не выключать SNAT при `destroy` |
|
| `keep_on_destroy` | bool, по умолчанию `false` | `false` — при удалении SNAT выключается; `true` — остаётся как есть |
|
||||||
|
|
||||||
```hcl
|
```hcl
|
||||||
resource "nubes_vc_nsxt_snat" "this" {
|
resource "nubes_vc_nsxt_snat" "this" {
|
||||||
nsxt_uid = var.nsxt_uid
|
nsxt_uid = nubes_vc_nsxt.edge.id # UUID шлюза из этого же конфига
|
||||||
ip_space_name = "internet-ipv4-v1"
|
ip_space_name = "internet-ipv4-v1"
|
||||||
|
|
||||||
depends_on = [nubes_vc_org_ip_allocation.this]
|
depends_on = [nubes_vc_org_ip_allocation.this]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Имя ipSpace должно быть предварительно выделено на организации, поэтому SNAT-ресурс **должен зависеть**
|
Если шлюз создаётся в этом же конфиге, UUID искать вручную не нужно — используйте
|
||||||
от `nubes_vc_org_ip_allocation`. При `destroy` отправляется `ipSpaceName = "no-needed"`.
|
`nubes_vc_nsxt.edge.id`. Если шлюз уже существует, UUID виден в карточке услуги в ЛК.
|
||||||
|
|
||||||
## Зачем отдельные ресурсы
|
Имя ipSpace должно быть сначала выделено на организации, поэтому у ресурса есть `depends_on`
|
||||||
|
на аллокацию IP.
|
||||||
В схемах ресурсов-инстансов (`nubes_vc_org`, `nubes_vc_nsxt`) эти параметры присутствуют только в
|
|
||||||
операции `modify` — `create` их не отправляет. Отдельные ресурсы позволяют собрать цепочку
|
|
||||||
**в одном `apply`** и выстроить корректный порядок зависимостей.
|
|
||||||
|
|
||||||
## Импорт
|
## Импорт
|
||||||
|
|
||||||
Оба ресурса импортируются по UUID родительской услуги:
|
Если услуга уже настроена (например, в ЛК вручную), ресурс можно подключить к Terraform:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
terraform import nubes_vc_org_ip_allocation.this <org_uid>
|
terraform import nubes_vc_org_ip_allocation.this organ # организация: имя или UUID
|
||||||
terraform import nubes_vc_nsxt_snat.this <nsxt_uid>
|
terraform import nubes_vc_nsxt_snat.this <uuid услуги шлюза> # UUID из карточки услуги в ЛК
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -0,0 +1,236 @@
|
|||||||
|
# Как развернуть цепочку: vDC → Edge → внешние IP → SNAT → vApp → ВМ → Штурвал
|
||||||
|
|
||||||
|
Пошаговая инструкция. Готовые файлы примера — в репозитории `tf_examples`, папка `fullpipe_chain`.
|
||||||
|
|
||||||
|
Что получится в итоге:
|
||||||
|
|
||||||
|
- виртуальный датацентр (vDC);
|
||||||
|
- сетевой шлюз периметра (Edge) с балансировщиком AVI;
|
||||||
|
- внешние IP на организации;
|
||||||
|
- SNAT на шлюзе;
|
||||||
|
- виртуальный каталог (vApp) и виртуальная машина (ВМ) внутри него — с доступом по SSH;
|
||||||
|
- Kubernetes-кластер Штурвал (сервис 150) на этой сети.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Организацию создайте заранее в ЛК** — Terraform её не создаёт и не удаляет.
|
||||||
|
Кластер Штурвал создаётся десятки минут, поэтому провайдер ждёт его до часу
|
||||||
|
(`operation_timeout = "60m"`); ВМ тоже создаётся небыстро (у неё отдельный таймаут `15m`),
|
||||||
|
остальные ресурсы — минуты.
|
||||||
|
|
||||||
|
## Что нужно перед началом
|
||||||
|
|
||||||
|
| Требование | Зачем | Где смотреть |
|
||||||
|
|---|---|---|
|
||||||
|
| Terraform 1.5 или новее | работает провайдер | `terraform version` |
|
||||||
|
| Токен API | доступ к ЛК | ЛК → Профиль → Токены → «Технический» |
|
||||||
|
| Организация в Cloud Director | всё создаётся внутри неё | услуга «Организация в Cloud Director» |
|
||||||
|
| Edge с балансировщиком AVI | без ALB кластер Штурвал не поднимется | `nsxt_need_enable_avi = true`, `nsxt_virtual_services_count >= 3` |
|
||||||
|
| Внешние IP: не меньше 3 + 1 под ВМ | адрес Kubernetes API, адрес Ingress, адрес ВМ и запас | `ip_count = "4"` |
|
||||||
|
| SNAT на Edge | выход в интернет для машин кластера и для ВМ | ресурс `nubes_vc_nsxt_snat` |
|
||||||
|
| Публичный SSH-ключ | вход на ВМ по SSH | `~/.ssh/id_ed25519.pub` → `vm_user_public_key` |
|
||||||
|
| Запас vCPU в vDC | Штурвал занимает 8 vCPU из 8 при настройках по умолчанию, и тогда ВМ не включится | `vdc_cpu_allocated` |
|
||||||
|
|
||||||
|
Порядок из чек-листа услуги 150 (именно так связаны ресурсы в примере):
|
||||||
|
организация → vDC → Edge (ALB, AVI ≥ 3) → внешние IP (≥ 3) → SNAT → **vApp → ВМ** → кластер Штурвал.
|
||||||
|
Минимум для кластера: 1 мастер-нода и 1 воркер-нода по 4 vCPU / 8 ГБ RAM / 50 ГБ диска;
|
||||||
|
минимум для ВМ: 1 vCPU / 1 ГБ RAM (в примере — 2 vCPU / 2 ГБ).
|
||||||
|
|
||||||
|
## 1. Скачайте пример
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://gitea.services.ngcloud.ru/Nail/tf_examples.git
|
||||||
|
cd tf_examples/fullpipe_chain
|
||||||
|
```
|
||||||
|
|
||||||
|
Понадобится Terraform 1.5 или новее. Провайдер скачается сам при `terraform init`.
|
||||||
|
|
||||||
|
## 2. Возьмите значения в ЛК
|
||||||
|
|
||||||
|
| Значение | Где взять | Пример |
|
||||||
|
|---|---|---|
|
||||||
|
| `api_token` | ЛК → Профиль → Токены → «Технический» | `eyJhbGciOi...` |
|
||||||
|
| `organization` | ЛК → услуга «Организация в Cloud Director» → название услуги | `organ` |
|
||||||
|
| `vdc_network_provider` | ЛК → создание vDC → «Сетевой провайдер» | `snb1` |
|
||||||
|
| `vdc_provider_vdc` | ЛК → создание vDC → «Provider VDC» | `Intel Broadwell 2.4` |
|
||||||
|
| `vdc_storage_config` | ЛК → создание vDC → доступные дисковые политики | `SATA` |
|
||||||
|
| `ip_space_name` | ЛК → карточка организации → внешние IP | `internet-ipv4-v1` |
|
||||||
|
|
||||||
|
## 3. Заполните значения
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp terraform.tfvars.example terraform.tfvars
|
||||||
|
nano terraform.tfvars
|
||||||
|
```
|
||||||
|
|
||||||
|
Файл `terraform.tfvars` выглядит так:
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
# Токен из ЛК
|
||||||
|
api_token = "eyJhbGciOi..."
|
||||||
|
|
||||||
|
# Организация из ЛК (создана заранее)
|
||||||
|
organization = "organ"
|
||||||
|
|
||||||
|
# Внешние IP
|
||||||
|
ip_space_name = "internet-ipv4-v1"
|
||||||
|
ip_count = "4" # 3 под Штурвал + 1 под ВМ
|
||||||
|
|
||||||
|
# vDC
|
||||||
|
vdc_resource_name = "fullpipe-vdc" # имя услуги в ЛК, любое
|
||||||
|
vdc_network_provider = "snb1" # ЛК → создание vDC → «Сетевой провайдер»
|
||||||
|
vdc_provider_vdc = "Intel Broadwell 2.4" # ЛК → создание vDC → «Provider VDC»
|
||||||
|
vdc_cpu_allocated = 12 # 8 под Штурвал + 2 под ВМ + 2 запаса
|
||||||
|
vdc_cpu_guaranteed = 0 # резервирование vCPU, %: 0, 50 или 80
|
||||||
|
vdc_mem_allocated = 32 # RAM, ГБ
|
||||||
|
vdc_storage_config = "[{\"name\":\"SATA\",\"size\":\"200\"}]" # политика и размер, ГБ
|
||||||
|
|
||||||
|
# Edge (сетевой шлюз периметра)
|
||||||
|
nsxt_resource_name = "fullpipe-edge" # имя услуги в ЛК, любое
|
||||||
|
nsxt_vdc_type = "vdc" # родитель: vdc или vdcGroup
|
||||||
|
nsxt_need_enable_avi = true # балансировщик AVI (ALB)
|
||||||
|
nsxt_virtual_services_count = 3 # виртуальных сервисов AVI: 1..4 (Штурвал: не меньше 3)
|
||||||
|
nsxt_ip_addr_pool = "10.10.102.0/24" # пул адресов routed-сети, маска /24
|
||||||
|
nsxt_main_dns = "81.22.46.22" # основной DNS
|
||||||
|
nsxt_second_dns = "185.247.187.77" # второй DNS
|
||||||
|
|
||||||
|
# vApp и ВМ
|
||||||
|
vapp_name = "fullpipe-vapp-01" # имя vApp: маска ^[a-z0-9][a-z0-9.-]{3,61}[a-z0-9]$, уникально в организации
|
||||||
|
vm_name = "web01" # имя ВМ: задаёт имя IP Set {vapp_name}-{vm_name}
|
||||||
|
vm_image = "Ubuntu_22-20G" # RockyLinux_9-16G-cloudinit | Ubuntu_22-20G | Debian_13-20G
|
||||||
|
vm_cpu = 2 # vCPU, 1..64
|
||||||
|
vm_ram = 2 # RAM, ГБ, 1..256
|
||||||
|
vm_disk = 20 # дополнительный диск, ГБ
|
||||||
|
vm_user_login = "ubuntu" # учётка SSH
|
||||||
|
vm_user_public_key = "ssh-ed25519 AAAA..." # содержимое ~/.ssh/id_ed25519.pub
|
||||||
|
vm_same_snat = false # false — за общим SNAT шлюза
|
||||||
|
```
|
||||||
|
|
||||||
|
Описание всех параметров — в `variables.tf`. `terraform.tfvars` с токеном никому не передавайте
|
||||||
|
и не коммитьте в git.
|
||||||
|
|
||||||
|
## 4. Выполните команды
|
||||||
|
|
||||||
|
```bash
|
||||||
|
terraform init # один раз — скачает провайдер
|
||||||
|
terraform plan # покажет, что будет создано: 7 ресурсов (vDC, Edge, IP, SNAT, vApp, ВМ, Штурвал)
|
||||||
|
terraform apply # создаст (подтвердить: yes)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Проверьте результат
|
||||||
|
|
||||||
|
```bash
|
||||||
|
terraform output # UUID и имена созданных услуг
|
||||||
|
```
|
||||||
|
|
||||||
|
И в ЛК: появились vDC, Edge, vApp с ВМ и кластер Штурвал, на организации выделены внешние IP,
|
||||||
|
на шлюзе включён SNAT. Повторный `terraform plan` должен показать `No changes`.
|
||||||
|
|
||||||
|
Адрес ВМ виден в `terraform output vm_state_flat` — поля `externalConnect` (внешний IP),
|
||||||
|
`internalConnect` (адрес в routed-сети) и `fqdn`: `ssh <vm_user_login>@<externalConnect>`.
|
||||||
|
|
||||||
|
Кластер создаётся десятки минут — провайдер ждёт его (`operation_timeout = "60m"`),
|
||||||
|
в ЛК он появится со статусом `running`. Адреса Kubernetes API и Ingress — в
|
||||||
|
`terraform output shturval_state_params` (поля `kubernetesApiAddress` и `ingressAddress`).
|
||||||
|
|
||||||
|
## 6. vApp и ВМ
|
||||||
|
|
||||||
|
Виртуальная машина живёт **внутри vApp**: vApp привязывается к vDC (`vdc_uid`) и к Edge (`nsxt_uid`),
|
||||||
|
а ВМ — к vApp (`vapp_uid`) и получает адрес в routed-сети шлюза. Своей сети у vApp нет.
|
||||||
|
|
||||||
|
| Ресурс | Услуга | Ключевые параметры |
|
||||||
|
|---|---|---|
|
||||||
|
| `nubes_vapp` | 26 | `vapp_name` (уникальное в организации, маска DNS-имени), `vdc_uid`, `nsxt_uid` |
|
||||||
|
| `nubes_vc_vm_v3` | 28 | `vapp_uid`, `vm_name`, `vm_cpu`, `vm_ram`, `vm_disk`, `image_vm`, `user_login`, `user_public_key`, `ip_space_name`, `access_port_list` |
|
||||||
|
|
||||||
|
### Параметры ВМ
|
||||||
|
|
||||||
|
| Параметр | Значения |
|
||||||
|
|---|---|
|
||||||
|
| `image_vm` | `RockyLinux_9-16G-cloudinit`, `Ubuntu_22-20G`, `Debian_13-20G` |
|
||||||
|
| `vm_cpu` / `vm_ram` | 1..64 шт. / 1..256 ГБ |
|
||||||
|
| `vm_disk` | дополнительный диск, ГБ (основной диск зависит от образа) |
|
||||||
|
| `user_login` | учётка SSH (по умолчанию `myuser`) |
|
||||||
|
| `access_port_list` | `[{ "port": "22", "type": "tcp" }]`; `type` — `tcp`, `udp` или `all` |
|
||||||
|
| `access_ip_list` | белый список адресов; по умолчанию `["0.0.0.0/0"]` |
|
||||||
|
|
||||||
|
`image_vm`, `vm_name`, `user_login`, `user_public_key` и `vapp_uid` — **create-only** (менять нельзя).
|
||||||
|
`vm_cpu`, `vm_ram`, `vm_disk`, `access_port_list`, `ip_space_name` меняются операцией `modify`.
|
||||||
|
|
||||||
|
### Внешний доступ
|
||||||
|
|
||||||
|
| `ip_space_name` | Что это значит |
|
||||||
|
|---|---|
|
||||||
|
| `internet-ipv4-v1` | у ВМ есть внешний адрес: в `vm_state_flat` появятся `externalConnect` и `fqdn` |
|
||||||
|
| `no-needed` | внешнего адреса нет, доступ только изнутри |
|
||||||
|
|
||||||
|
Адрес берётся из квоты организации, поэтому при внешнем доступе нужен **4-й адрес** (`ip_count = "4"`).
|
||||||
|
При `same_snat = false` (по умолчанию) ВМ публикуется за общим SNAT шлюза.
|
||||||
|
|
||||||
|
Пример результата на живом стенде:
|
||||||
|
|
||||||
|
```
|
||||||
|
vm_state_flat = {
|
||||||
|
"externalConnect" = "185.247.187.235"
|
||||||
|
"internalConnect" = "10.10.102.4"
|
||||||
|
"fqdn" = "web01.85ea682f-...dev.nubes.ru"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Что проверять при создании ВМ
|
||||||
|
|
||||||
|
1. **Запас vCPU в vDC.** Штурвал при настройках по умолчанию занимает 8 vCPU из 8 (2 ноды × «TKG 4CPU 8RAM»).
|
||||||
|
Тогда ВМ не включится: платформа отдаёт `[400:VALIDATION] Unable to perform this action` на этапе
|
||||||
|
power-on, а инстанс остаётся в статусе `not created`. Лечится увеличением `vdc_cpu_allocated`.
|
||||||
|
2. **Квота IP.** `ip_count ≥ 4`, иначе внешний адрес для ВМ не выделится.
|
||||||
|
3. **Ключ.** Публичный ключ кладите в `terraform.tfvars`, не в репозиторий.
|
||||||
|
4. **Время.** ВМ создаётся и включается с customization (запись учётки и ключа) — на живом стенде
|
||||||
|
операция шла больше 15 минут. В примере стоит `operation_timeout = "15m"`; при медленной платформе
|
||||||
|
увеличьте его. Симптом таймаута — «операция … не завершилась за установленный таймаут».
|
||||||
|
5. **Проверка.** `ssh <vm_user_login>@<externalConnect>` и `terraform plan` → `No changes`.
|
||||||
|
|
||||||
|
## 7. Удаление: «заморозка» вместо удаления
|
||||||
|
|
||||||
|
```bash
|
||||||
|
terraform destroy
|
||||||
|
```
|
||||||
|
|
||||||
|
По умолчанию пример повторяет рабочую конфигурацию — при `destroy` объекты **не удаляются**:
|
||||||
|
|
||||||
|
| Ресурс | Что делает `destroy` | Флаг в примере |
|
||||||
|
|---|---|---|
|
||||||
|
| Кластер Штурвал | `suspend`: выключается, данные и адреса сохраняются | `suspend_on_destroy = true` |
|
||||||
|
| vDC | `suspend` | `suspend_on_destroy = true` |
|
||||||
|
| Edge | не трогается: у эджа нет операции `suspend` | `keep_on_destroy = true` |
|
||||||
|
| SNAT | не выключается | `keep_on_destroy = true` |
|
||||||
|
| Квота внешних IP | не меняется: адреса держит кластер Штурвала | `keep_on_destroy = true` |
|
||||||
|
| vApp | `suspend`: выключаются все ВМ внутри каталога | `suspend_on_destroy = true` |
|
||||||
|
| ВМ | `suspend` | `suspend_on_destroy = true` |
|
||||||
|
|
||||||
|
Приоритет флагов: `keep_on_destroy` важнее `suspend_on_destroy`. Следующий `apply`
|
||||||
|
усыновит объекты по имени и разморозит кластер и vDC
|
||||||
|
(`adopt_existing_on_create = true` → `resume`). **Организация не удаляется никогда.**
|
||||||
|
|
||||||
|
Полное удаление — осознанно: поставьте `keep_on_destroy = false` и
|
||||||
|
`suspend_on_destroy = false` и удаляйте по порядку: кластер → ВМ → vApp → квота IP (`count = 0`) →
|
||||||
|
SNAT → Edge → vDC. Квоту нельзя опустить ниже занятых адресов, поэтому — только
|
||||||
|
после удаления кластера и ВМ; vApp удаляется лишь после `suspend` (полное удаление — через 14 дней),
|
||||||
|
а vDC — через 14 дней после `suspend`.
|
||||||
|
|
||||||
|
## Файлы примера
|
||||||
|
|
||||||
|
| Файл | Что делает |
|
||||||
|
|---|---|
|
||||||
|
| `versions.tf` | версия Terraform и провайдера Nubes |
|
||||||
|
| `provider.tf` | подключение к API (токен, адрес) |
|
||||||
|
| `variables.tf` | все параметры с описанием |
|
||||||
|
| `vdc.tf` | виртуальный датацентр |
|
||||||
|
| `edge.tf` | сетевой шлюз периметра (Edge) |
|
||||||
|
| `modifiers.tf` | внешние IP на организации + SNAT на шлюзе |
|
||||||
|
| `vm.tf` | vApp и ВМ: переменные vApp/ВМ, оба ресурса и выводы (отключается удалением файла) |
|
||||||
|
| `shturval.tf` | Kubernetes-кластер Штурвал: переменные Штурвала, группы воркеров и сам ресурс |
|
||||||
|
| `outputs.tf` | UUID и имена созданных услуг (vDC, Edge, vApp, ВМ, Штурвал) |
|
||||||
|
| `terraform.tfvars.example` | шаблон значений (копируется в `terraform.tfvars`) |
|
||||||
|
|
||||||
|
Подробнее про два последних ресурса — на странице
|
||||||
|
[«Ресурсы-модификаторы (IP организации, SNAT)»](../modifiers/org_ip_and_snat.md).
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
# Схема зависимостей облачных сервисов (docs/diagrams)
|
||||||
|
|
||||||
|
Папка содержит все артефакты схемы: исходники, отрендеренные изображения и
|
||||||
|
генератор. Здесь же описан процесс создания и результаты сверки с контрактами.
|
||||||
|
|
||||||
|
## 1. Состав папки
|
||||||
|
|
||||||
|
| Файл | Что это | Чем создаётся |
|
||||||
|
|---|---|---|
|
||||||
|
| `infra_services_flow.mmd` | исходник Mermaid-версии схемы | правится вручную |
|
||||||
|
| `infra_services_flow.svg` | вектор Mermaid-версии | рендер через `mermaid.ink` |
|
||||||
|
| `infra_services_flow.png` | растр Mermaid-версии | рендер через `mermaid.ink` |
|
||||||
|
| `render_infra_diagram.py` | генератор стилизованной схемы (PNG + SVG) | чистый Python + Pillow |
|
||||||
|
| `infra_services_diagram.png` | стилизованная схема, растр 1600×720 (рендер 2x + LANCZOS) | `render_infra_diagram.py` |
|
||||||
|
| `infra_services_diagram.svg` | стилизованная схема, вектор | `render_infra_diagram.py` |
|
||||||
|
|
||||||
|
Две версии существуют одновременно: Mermaid — для правок текстом и встраивания
|
||||||
|
в Markdown; стилизованная — как готовая картинка для показа.
|
||||||
|
|
||||||
|
## 2. Источник данных
|
||||||
|
|
||||||
|
Единственный источник фактов — YAML-контракты сервисов:
|
||||||
|
|
||||||
|
```
|
||||||
|
generated/dev/resources_yaml/*.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Из них берутся:
|
||||||
|
|
||||||
|
- `service_id`, `service_display_name` — идентичность сервиса;
|
||||||
|
- `operations[].params[].ref_svc_id` — **прямая зависимость** от другого сервиса;
|
||||||
|
- `operations[].params[].depends_on` — зависимость порядка внутри операции;
|
||||||
|
- `value_list`, `default`, `man`, `descr` — значения на блоках (CPU, сети и т.п.);
|
||||||
|
- `service_man` — чек-листы и порядок действий, заданные самой платформой.
|
||||||
|
|
||||||
|
Terraform-файлы `DEV_STAND/*` **не используются как источник** для этой схемы —
|
||||||
|
они лишь один из примеров потребления.
|
||||||
|
|
||||||
|
## 3. Как перегенерировать
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/naeel/TF/tf_provider
|
||||||
|
python3 docs/diagrams/render_infra_diagram.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт пишет `infra_services_diagram.png` и `.svg` **в свою же папку**
|
||||||
|
(`docs/diagrams/`), путь вычисляется от расположения файла.
|
||||||
|
|
||||||
|
Требования: `python3` с `Pillow` и шрифты DejaVu
|
||||||
|
(`/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf`, `...-Bold.ttf`).
|
||||||
|
Сторонние библиотеки для SVG не нужны — SVG формируется текстом.
|
||||||
|
|
||||||
|
### Публикация картинок на сайте
|
||||||
|
|
||||||
|
Скрипт `TOOLS/scripts/04_build_and_publish_docs.sh` копирует **только картинки**
|
||||||
|
(`docs/diagrams/*.svg`, `*.png`) в публикуемый `docs_dir` → `diagrams/`: исходники (`.mmd`, `.py`)
|
||||||
|
на сайт не попадают. Поэтому картинку можно вставлять в опубликованные страницы относительной
|
||||||
|
ссылкой, например со страницы `docs/curated/pipeline/vdc_edge_ip_snat.md`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|

|
||||||
|
```
|
||||||
|
|
||||||
|
Изменения в `docs/diagrams/` попадают на сайт только при следующей публикации документации.
|
||||||
|
|
||||||
|
Для Mermaid-версии нужен рендерер (`mermaid.ink`, `mmdc` или предпросмотр
|
||||||
|
Markdown в VS Code); в этом репозитории использовался `mermaid.ink` через
|
||||||
|
`python3` + `urllib`.
|
||||||
|
|
||||||
|
## 4. Структура картинки
|
||||||
|
|
||||||
|
Пять колонок = этапы создания, слева направо:
|
||||||
|
|
||||||
|
1. **Тенант** — Организация в Cloud Director (19)
|
||||||
|
2. **Пул ресурсов** — Виртуальный датацентр (21)
|
||||||
|
3. **Сетевой периметр** — Сетевой шлюз периметра / Edge (22)
|
||||||
|
4. **Среда запуска** — Каталог ВМ / vApp (26) + Внешние IP (vcOrg modify / 25)
|
||||||
|
5. **Потребители** — Виртуальная машина (28) и Kubernetes кластер Штурвал (150)
|
||||||
|
|
||||||
|
### Обозначения
|
||||||
|
|
||||||
|
- **Сплошная стрелка** — обязательная зависимость, ссылка на UUID существующего
|
||||||
|
инстанса (`ref_svc_id`).
|
||||||
|
- **Пунктирная стрелка** — выделение ресурса, операция `modify` или предусловие.
|
||||||
|
- Направление стрелки всегда **от зависимости к потребителю**: `Org → vDC`,
|
||||||
|
`vApp → VM`. То есть стрелка отвечает на вопрос «что должно существовать до».
|
||||||
|
- Цвет колонки кодирует этап, цвет рамки блока — группу (тенант / ресурсы / сеть /
|
||||||
|
потребители / адреса). Легенда — в правом нижнем углу картинки.
|
||||||
|
|
||||||
|
### Бейджи точек выделения ресурсов
|
||||||
|
|
||||||
|
| Бейдж | Где на схеме | Что означает |
|
||||||
|
|---|---|---|
|
||||||
|
| «здесь выделяются CPU / RAM / Диск» | блок vDC (21) | `cpuAllocated`, `memAllocated`, `storageConfig` |
|
||||||
|
| «SNAT настраивается операцией modify» | блок Edge (22) | SNAT — не сервис, а `modify` у Edge |
|
||||||
|
| «здесь выделяются IP» | блок «Внешние IP» | выделение адресов на организации |
|
||||||
|
| «требует ALB + VS ≥ 3» | блок Штурвала (150) | обязательное условие из чек-листа |
|
||||||
|
|
||||||
|
## 5. Результат сверки с YAML
|
||||||
|
|
||||||
|
Проверено построчно (коммит `6791e8f`, дополнение — связь `vApp → Внешние IP`).
|
||||||
|
|
||||||
|
| Утверждение на схеме | Источник |
|
||||||
|
|---|---|
|
||||||
|
| Организация: типы `iaas / saas` | `19_vc_org.yaml:47-53` |
|
||||||
|
| Выделение IP через `vcOrg modify (vIPConfigure)` | `19_vc_org.yaml:115` |
|
||||||
|
| Для внешних IP нужны vDC и Edge | `19_vc_org.yaml:112` (`man`) |
|
||||||
|
| vDC: `cpuGuaranteed: 0% / 50% / 80%` | `21_vc_vdc.yaml:71-78` |
|
||||||
|
| vDC: `cpuAllocated`, `memAllocated`, `storageConfig` | `21_vc_vdc.yaml:49,83,93` |
|
||||||
|
| Edge: AVI Load Balancer | `22_vc_nsxt.yaml` (`needEnableAVI`) |
|
||||||
|
| Edge: routed-сеть `10.10.102.0/24` | `22_vc_nsxt.yaml:5` |
|
||||||
|
| Edge: SNAT настраивается операцией `modify` | `22_vc_nsxt.yaml` (`modify → ipSpaceName`) |
|
||||||
|
| vApp → vDC (`vdcUid`), Edge (`nsxtUid`) | `26_vapp.yaml:38,56` |
|
||||||
|
| ВМ → vApp (`vappUid`), `vmCpu · vmRam · vmDisk` | `28_vc_vm_v3.yaml:39,49,59,69` |
|
||||||
|
| Штурвал: `vdcUid`, `nsxtUid`; `sizingPolicy · sizingDisk · count` | `150_k8s_sthutrval_cluster.yaml:41,47,132,141,148` |
|
||||||
|
| Публичные IP: NAT/DNAT через сервис 25 | `25_vcexternalip.yaml:41,57,100` |
|
||||||
|
| **Публичные IP требуют «Каталог» (vApp)** | `25_vcexternalip.yaml:5` (`service_man`) |
|
||||||
|
| Штурвал: обязательны ALB и VS ≥ 3; суммарно 3 внешних адреса | `150_k8s_sthutrval_cluster.yaml` (`service_man`, чек-лист) |
|
||||||
|
|
||||||
|
## 6. Известные ограничения
|
||||||
|
|
||||||
|
1. **Переиспользование инстансов** («vDC и Edge могут быть переиспользованы»)
|
||||||
|
не написано в YAML буквально — это следствие семантики ссылок на UUID
|
||||||
|
существующего инстанса. Уверенность средняя, не высокая.
|
||||||
|
2. Блок «Внешние IP» объединяет два механизма: выделение адресов на организации
|
||||||
|
(`vcOrg modify`) и правила NAT у сервиса 25 (`vcexternalip`). Они подписаны
|
||||||
|
раздельно внутри блока, но нарисованы одной вершиной.
|
||||||
|
3. Остальные 33 сервиса каталога на схеме не показаны — отрисованы только
|
||||||
|
инфраструктурные цепочки и их прямые потребители.
|
||||||
|
4. Схема — компоновка по контрактам, а не результат наблюдения за развёртыванием.
|
||||||
|
Фактический порядок и доступность нужно подтверждать отдельно.
|
||||||
|
|
||||||
|
## 7. История
|
||||||
|
|
||||||
|
| Дата | Изменение |
|
||||||
|
|---|---|
|
||||||
|
| 2026-09-26 | Создана Mermaid-версия (`infra_services_flow.*`) |
|
||||||
|
| 2026-09-26 | Исправлена связь «вычислительный пул»: от vDC, а не от внешних IP |
|
||||||
|
| 2026-09-26 | Добавлен генератор стилизованной схемы (`render_infra_diagram.py`) |
|
||||||
|
| 2026-09-26 | Убрана дублирующая подпись `modify: SNAT` у блока Edge |
|
||||||
|
| 2026-09-26 | Сверка с YAML; добавлена связь `vApp → Внешние IP` — предусловие сервиса 25 |
|
||||||
|
| 2026-09-26 | Все артефакты перенесены в `docs/diagrams/` |
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 260 KiB |
@@ -0,0 +1,145 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="720" viewBox="0 0 1600 720" font-family="DejaVu Sans, Verdana, sans-serif">
|
||||||
|
<rect width="1600" height="720" fill="#FFFFFF"/>
|
||||||
|
<rect x="16" y="8" width="270" height="552" rx="12" fill="#E8F3FB"/>
|
||||||
|
<rect x="16" y="14" width="270" height="44" rx="8" fill="#1565C0"/>
|
||||||
|
<text x="151.0" y="25" font-size="15" fill="#FFFFFF" font-weight="bold" text-anchor="middle" dominant-baseline="hanging">Шаг 1. Тенант</text>
|
||||||
|
<text x="151.0" y="68" font-size="12" fill="#7A8794" text-anchor="middle" dominant-baseline="hanging">базовый уровень изоляции</text>
|
||||||
|
<rect x="340" y="8" width="270" height="552" rx="12" fill="#FDF1E3"/>
|
||||||
|
<rect x="340" y="14" width="270" height="44" rx="8" fill="#E65100"/>
|
||||||
|
<text x="475.0" y="25" font-size="15" fill="#FFFFFF" font-weight="bold" text-anchor="middle" dominant-baseline="hanging">Шаг 2. Пул ресурсов</text>
|
||||||
|
<text x="475.0" y="68" font-size="12" fill="#7A8794" text-anchor="middle" dominant-baseline="hanging">CPU / RAM / Storage</text>
|
||||||
|
<rect x="664" y="8" width="270" height="552" rx="12" fill="#EEEAF8"/>
|
||||||
|
<rect x="664" y="14" width="270" height="44" rx="8" fill="#4527A0"/>
|
||||||
|
<text x="799.0" y="25" font-size="15" fill="#FFFFFF" font-weight="bold" text-anchor="middle" dominant-baseline="hanging">Шаг 3. Сетевой периметр</text>
|
||||||
|
<text x="799.0" y="68" font-size="12" fill="#7A8794" text-anchor="middle" dominant-baseline="hanging">маршрутизация и балансировка</text>
|
||||||
|
<rect x="988" y="8" width="270" height="552" rx="12" fill="#FDF4E3"/>
|
||||||
|
<rect x="988" y="14" width="270" height="44" rx="8" fill="#EF6C00"/>
|
||||||
|
<text x="1123.0" y="25" font-size="15" fill="#FFFFFF" font-weight="bold" text-anchor="middle" dominant-baseline="hanging">Шаг 4. Среда запуска</text>
|
||||||
|
<text x="1123.0" y="68" font-size="12" fill="#7A8794" text-anchor="middle" dominant-baseline="hanging">контейнер и внешние адреса</text>
|
||||||
|
<rect x="1312" y="8" width="270" height="552" rx="12" fill="#E9F5EA"/>
|
||||||
|
<rect x="1312" y="14" width="270" height="44" rx="8" fill="#2E7D32"/>
|
||||||
|
<text x="1447.0" y="25" font-size="15" fill="#FFFFFF" font-weight="bold" text-anchor="middle" dominant-baseline="hanging">Шаг 5. Потребители</text>
|
||||||
|
<text x="1447.0" y="68" font-size="12" fill="#7A8794" text-anchor="middle" dominant-baseline="hanging">что разворачивается в итоге</text>
|
||||||
|
<rect x="31" y="150" width="240" height="150" rx="10" fill="#FFFFFF" stroke="#0288D1" stroke-width="2"/>
|
||||||
|
<rect x="43" y="162" width="22" height="22" rx="5" fill="#0288D1"/>
|
||||||
|
<text x="73" y="162" font-size="15" fill="#1A1A1A" font-weight="bold" dominant-baseline="hanging">Организация</text>
|
||||||
|
<text x="73" y="182" font-size="15" fill="#1A1A1A" font-weight="bold" dominant-baseline="hanging">в Cloud Director</text>
|
||||||
|
<text x="73" y="202" font-size="11" fill="#5F6B76" dominant-baseline="hanging">Сервис 19 · vcOrg</text>
|
||||||
|
<circle cx="51" cy="231" r="2.4" fill="#0288D1"/>
|
||||||
|
<text x="61" y="224" font-size="10" fill="#2E3A45" dominant-baseline="hanging">Типы: iaas / saas</text>
|
||||||
|
<rect x="355" y="130" width="240" height="230" rx="10" fill="#FFF3E0" stroke="#F57C00" stroke-width="2"/>
|
||||||
|
<rect x="367" y="142" width="22" height="22" rx="5" fill="#F57C00"/>
|
||||||
|
<text x="397" y="142" font-size="15" fill="#1A1A1A" font-weight="bold" dominant-baseline="hanging">Виртуальный</text>
|
||||||
|
<text x="397" y="162" font-size="15" fill="#1A1A1A" font-weight="bold" dominant-baseline="hanging">датацентр (vDC)</text>
|
||||||
|
<text x="397" y="182" font-size="11" fill="#5F6B76" dominant-baseline="hanging">Сервис 21 · vcVdc</text>
|
||||||
|
<circle cx="375" cy="211" r="2.4" fill="#F57C00"/>
|
||||||
|
<text x="385" y="204" font-size="10" fill="#2E3A45" dominant-baseline="hanging">cpuGuaranteed: 0% / 50% / 80%</text>
|
||||||
|
<circle cx="375" cy="231" r="2.4" fill="#F57C00"/>
|
||||||
|
<text x="385" y="224" font-size="10" fill="#2E3A45" dominant-baseline="hanging">cpuAllocated, memAllocated</text>
|
||||||
|
<circle cx="375" cy="251" r="2.4" fill="#F57C00"/>
|
||||||
|
<text x="385" y="244" font-size="10" fill="#2E3A45" dominant-baseline="hanging">storageConfig</text>
|
||||||
|
<rect x="367" y="318" width="216" height="30" rx="6" fill="#FFFFFF" stroke="#F57C00" stroke-width="1"/>
|
||||||
|
<text x="475.0" y="328" font-size="10" fill="#F57C00" text-anchor="middle" dominant-baseline="hanging">здесь выделяются CPU / RAM / Диск</text>
|
||||||
|
<rect x="679" y="120" width="240" height="200" rx="10" fill="#EDE7F6" stroke="#5E35B1" stroke-width="2"/>
|
||||||
|
<rect x="691" y="132" width="22" height="22" rx="5" fill="#5E35B1"/>
|
||||||
|
<text x="721" y="132" font-size="15" fill="#1A1A1A" font-weight="bold" dominant-baseline="hanging">Сетевой шлюз</text>
|
||||||
|
<text x="721" y="152" font-size="15" fill="#1A1A1A" font-weight="bold" dominant-baseline="hanging">периметра (Edge)</text>
|
||||||
|
<text x="721" y="172" font-size="11" fill="#5F6B76" dominant-baseline="hanging">Сервис 22 · vcNsxt</text>
|
||||||
|
<circle cx="699" cy="201" r="2.4" fill="#5E35B1"/>
|
||||||
|
<text x="709" y="194" font-size="10" fill="#2E3A45" dominant-baseline="hanging">AVI Load Balancer</text>
|
||||||
|
<circle cx="699" cy="221" r="2.4" fill="#5E35B1"/>
|
||||||
|
<text x="709" y="214" font-size="10" fill="#2E3A45" dominant-baseline="hanging">routed-сеть 10.10.102.0/24</text>
|
||||||
|
<rect x="691" y="278" width="216" height="30" rx="6" fill="#FFFFFF" stroke="#5E35B1" stroke-width="1"/>
|
||||||
|
<text x="799.0" y="288" font-size="10" fill="#5E35B1" text-anchor="middle" dominant-baseline="hanging">SNAT настраивается операцией modify</text>
|
||||||
|
<rect x="1003" y="100" width="240" height="110" rx="10" fill="#FFF3E0" stroke="#F57C00" stroke-width="2"/>
|
||||||
|
<rect x="1015" y="112" width="22" height="22" rx="5" fill="#F57C00"/>
|
||||||
|
<text x="1045" y="112" font-size="15" fill="#1A1A1A" font-weight="bold" dominant-baseline="hanging">Каталог ВМ (vApp)</text>
|
||||||
|
<text x="1045" y="132" font-size="11" fill="#5F6B76" dominant-baseline="hanging">Сервис 26 · vapp</text>
|
||||||
|
<circle cx="1023" cy="161" r="2.4" fill="#F57C00"/>
|
||||||
|
<text x="1033" y="154" font-size="10" fill="#2E3A45" dominant-baseline="hanging">логический контейнер для ВМ</text>
|
||||||
|
<rect x="1003" y="245" width="180" height="150" rx="10" fill="#FAFAFA" stroke="#9E9E9E" stroke-width="2" stroke-dasharray="9 6"/>
|
||||||
|
<rect x="1015" y="257" width="22" height="22" rx="5" fill="#9E9E9E"/>
|
||||||
|
<text x="1045" y="257" font-size="15" fill="#1A1A1A" font-weight="bold" dominant-baseline="hanging">Внешние IP</text>
|
||||||
|
<text x="1045" y="277" font-size="11" fill="#5F6B76" dominant-baseline="hanging">Сервис 25</text>
|
||||||
|
<circle cx="1023" cy="306" r="2.4" fill="#9E9E9E"/>
|
||||||
|
<text x="1033" y="299" font-size="10" fill="#2E3A45" dominant-baseline="hanging">выделение: vcOrg</text>
|
||||||
|
<circle cx="1023" cy="326" r="2.4" fill="#9E9E9E"/>
|
||||||
|
<text x="1033" y="319" font-size="10" fill="#2E3A45" dominant-baseline="hanging">NAT: сервис 25</text>
|
||||||
|
<circle cx="1023" cy="346" r="2.4" fill="#9E9E9E"/>
|
||||||
|
<text x="1033" y="339" font-size="10" fill="#2E3A45" dominant-baseline="hanging">квота IP (ipSpace)</text>
|
||||||
|
<rect x="1015" y="353" width="156" height="30" rx="6" fill="#FFFFFF" stroke="#9E9E9E" stroke-width="1"/>
|
||||||
|
<text x="1093.0" y="363" font-size="10" fill="#9E9E9E" text-anchor="middle" dominant-baseline="hanging">здесь выделяются IP</text>
|
||||||
|
<rect x="1327" y="100" width="240" height="120" rx="10" fill="#E8F5E9" stroke="#2E7D32" stroke-width="2"/>
|
||||||
|
<rect x="1339" y="112" width="22" height="22" rx="5" fill="#2E7D32"/>
|
||||||
|
<text x="1369" y="112" font-size="15" fill="#1A1A1A" font-weight="bold" dominant-baseline="hanging">Виртуальная машина</text>
|
||||||
|
<text x="1369" y="132" font-size="11" fill="#5F6B76" dominant-baseline="hanging">Сервис 28 · vc_vm_v3</text>
|
||||||
|
<circle cx="1347" cy="161" r="2.4" fill="#2E7D32"/>
|
||||||
|
<text x="1357" y="154" font-size="10" fill="#2E3A45" dominant-baseline="hanging">vmCpu · vmRam · vmDisk</text>
|
||||||
|
<rect x="1327" y="255" width="240" height="170" rx="10" fill="#E8F5E9" stroke="#2E7D32" stroke-width="2"/>
|
||||||
|
<rect x="1339" y="267" width="22" height="22" rx="5" fill="#2E7D32"/>
|
||||||
|
<text x="1369" y="267" font-size="15" fill="#1A1A1A" font-weight="bold" dominant-baseline="hanging">Kubernetes кластер</text>
|
||||||
|
<text x="1369" y="287" font-size="15" fill="#1A1A1A" font-weight="bold" dominant-baseline="hanging">Штурвал</text>
|
||||||
|
<text x="1369" y="307" font-size="11" fill="#5F6B76" dominant-baseline="hanging">Сервис 150</text>
|
||||||
|
<circle cx="1347" cy="336" r="2.4" fill="#2E7D32"/>
|
||||||
|
<text x="1357" y="329" font-size="10" fill="#2E3A45" dominant-baseline="hanging">sizingPolicy · sizingDisk</text>
|
||||||
|
<circle cx="1347" cy="356" r="2.4" fill="#2E7D32"/>
|
||||||
|
<text x="1357" y="349" font-size="10" fill="#2E3A45" dominant-baseline="hanging">· count</text>
|
||||||
|
<rect x="1339" y="383" width="216" height="30" rx="6" fill="#FFFFFF" stroke="#2E7D32" stroke-width="1"/>
|
||||||
|
<text x="1447.0" y="393" font-size="10" fill="#2E7D32" text-anchor="middle" dominant-baseline="hanging">требует ALB + VS ≥ 3</text>
|
||||||
|
<path d="M271 225 L355 225" fill="none" stroke="#455A64" stroke-width="2" stroke-linejoin="round"/>
|
||||||
|
<polygon points="355,225 346.0,228.78 346.0,221.22" fill="#455A64"/>
|
||||||
|
<text x="313" y="262" font-size="10" fill="#455A64" text-anchor="middle" dominant-baseline="middle" transform="rotate(-90 313 262)">organizationUid</text>
|
||||||
|
<path d="M595 200 L679 200" fill="none" stroke="#455A64" stroke-width="2" stroke-linejoin="round"/>
|
||||||
|
<polygon points="679,200 670.0,203.78 670.0,196.22" fill="#455A64"/>
|
||||||
|
<text x="637" y="186" font-size="10" fill="#455A64" dominant-baseline="hanging">vdcUid</text>
|
||||||
|
<path d="M500 130 L500 92 L961 92 L961 140 L1003 140" fill="none" stroke="#455A64" stroke-width="2" stroke-linejoin="round"/>
|
||||||
|
<polygon points="1003,140 994.0,143.78 994.0,136.22" fill="#455A64"/>
|
||||||
|
<text x="706" y="96" font-size="10" fill="#455A64" dominant-baseline="hanging">vdcUid</text>
|
||||||
|
<path d="M919 180 L1003 180" fill="none" stroke="#455A64" stroke-width="2" stroke-linejoin="round"/>
|
||||||
|
<polygon points="1003,180 994.0,183.78 994.0,176.22" fill="#455A64"/>
|
||||||
|
<text x="961" y="166" font-size="10" fill="#455A64" dominant-baseline="hanging">nsxtUid</text>
|
||||||
|
<path d="M1243 140 L1327 140" fill="none" stroke="#455A64" stroke-width="2" stroke-linejoin="round"/>
|
||||||
|
<polygon points="1327,140 1318.0,143.78 1318.0,136.22" fill="#455A64"/>
|
||||||
|
<text x="1285" y="126" font-size="10" fill="#455A64" dominant-baseline="hanging">vappUid</text>
|
||||||
|
<path d="M500 360 L500 470 L1447 470 L1447 425" fill="none" stroke="#455A64" stroke-width="2" stroke-linejoin="round"/>
|
||||||
|
<polygon points="1447,425 1450.78,434.0 1443.22,434.0" fill="#455A64"/>
|
||||||
|
<text x="1180" y="456" font-size="10" fill="#455A64" dominant-baseline="hanging">vdcUid</text>
|
||||||
|
<path d="M799 320 L799 505 L1467 505 L1467 425" fill="none" stroke="#455A64" stroke-width="2" stroke-linejoin="round"/>
|
||||||
|
<polygon points="1467,425 1470.78,434.0 1463.22,434.0" fill="#455A64"/>
|
||||||
|
<text x="1180" y="491" font-size="10" fill="#455A64" dominant-baseline="hanging">nsxtUid</text>
|
||||||
|
<path d="M151 300 L151 540 L1050 540 L1050 395" fill="none" stroke="#7A8794" stroke-width="2" stroke-dasharray="9 6" stroke-linejoin="round"/>
|
||||||
|
<polygon points="1050,395 1053.78,404.0 1046.22,404.0" fill="#7A8794"/>
|
||||||
|
<text x="161" y="526" font-size="10" fill="#7A8794" dominant-baseline="hanging">modify: vIPConfigure</text>
|
||||||
|
<path d="M870 320 L870 400 L1140 400 L1140 395" fill="none" stroke="#7A8794" stroke-width="2" stroke-dasharray="9 6" stroke-linejoin="round"/>
|
||||||
|
<polygon points="1140,395 1143.78,404.0 1136.22,404.0" fill="#7A8794"/>
|
||||||
|
<text x="880" y="386" font-size="10" fill="#7A8794" dominant-baseline="hanging">modify: SNAT</text>
|
||||||
|
<path d="M1050 210 L1050 245" fill="none" stroke="#7A8794" stroke-width="2" stroke-dasharray="9 6" stroke-linejoin="round"/>
|
||||||
|
<polygon points="1050,245 1046.22,236.0 1053.78,236.0" fill="#7A8794"/>
|
||||||
|
<text x="1060" y="218" font-size="10" fill="#7A8794" dominant-baseline="hanging">требует Каталог</text>
|
||||||
|
<path d="M1183 270 L1255 270 L1255 160 L1327 160" fill="none" stroke="#7A8794" stroke-width="2" stroke-dasharray="9 6" stroke-linejoin="round"/>
|
||||||
|
<polygon points="1327,160 1318.0,163.78 1318.0,156.22" fill="#7A8794"/>
|
||||||
|
<text x="1187" y="222" font-size="10" fill="#7A8794" dominant-baseline="hanging">внешний IP</text>
|
||||||
|
<path d="M1183 350 L1327 350" fill="none" stroke="#C62828" stroke-width="2" stroke-dasharray="9 6" stroke-linejoin="round"/>
|
||||||
|
<polygon points="1327,350 1318.0,353.78 1318.0,346.22" fill="#C62828"/>
|
||||||
|
<text x="1187" y="330" font-size="10" fill="#C62828" dominant-baseline="hanging">≥ 3 адреса</text>
|
||||||
|
<text x="24" y="590" font-size="14" fill="#2E3A45" dominant-baseline="hanging">> Порядок создания: Организация → vDC → Edge → (vApp + внешние IP) → ВМ / Штурвал.</text>
|
||||||
|
<text x="24" y="616" font-size="14" fill="#2E3A45" dominant-baseline="hanging">> SNAT — не отдельный сервис, а операция modify у Edge.</text>
|
||||||
|
<text x="24" y="642" font-size="14" fill="#2E3A45" dominant-baseline="hanging">> vDC и Edge могут быть переиспользованы существующие.</text>
|
||||||
|
<path d="M980 606 L1040 606" fill="none" stroke="#455A64" stroke-width="2" stroke-linejoin="round"/>
|
||||||
|
<polygon points="1040,606 1031.0,609.78 1031.0,602.22" fill="#455A64"/>
|
||||||
|
<text x="1052" y="597" font-size="11" fill="#2E3A45" dominant-baseline="hanging">Сплошная линия — обязательная зависимость</text>
|
||||||
|
<text x="1052" y="613" font-size="10" fill="#7A8794" dominant-baseline="hanging">(ссылка на UUID существующего инстанса)</text>
|
||||||
|
<path d="M980 650 L1040 650" fill="none" stroke="#7A8794" stroke-width="2" stroke-dasharray="9 6" stroke-linejoin="round"/>
|
||||||
|
<polygon points="1040,650 1031.0,653.78 1031.0,646.22" fill="#7A8794"/>
|
||||||
|
<text x="1052" y="641" font-size="11" fill="#2E3A45" dominant-baseline="hanging">Пунктир — выделение ресурса / modify / предусловие</text>
|
||||||
|
<rect x="980" y="684" width="12" height="12" rx="3" fill="#1565C0"/>
|
||||||
|
<text x="996" y="682" font-size="10" fill="#2E3A45" dominant-baseline="hanging">Тенант</text>
|
||||||
|
<rect x="1038" y="684" width="12" height="12" rx="3" fill="#F57C00"/>
|
||||||
|
<text x="1054" y="682" font-size="10" fill="#2E3A45" dominant-baseline="hanging">Ресурсы</text>
|
||||||
|
<rect x="1102" y="684" width="12" height="12" rx="3" fill="#5E35B1"/>
|
||||||
|
<text x="1118" y="682" font-size="10" fill="#2E3A45" dominant-baseline="hanging">Сеть</text>
|
||||||
|
<rect x="1147" y="684" width="12" height="12" rx="3" fill="#2E7D32"/>
|
||||||
|
<text x="1163" y="682" font-size="10" fill="#2E3A45" dominant-baseline="hanging">Потребители</text>
|
||||||
|
<rect x="1237" y="684" width="12" height="12" rx="3" fill="#9E9E9E"/>
|
||||||
|
<text x="1253" y="682" font-size="10" fill="#2E3A45" dominant-baseline="hanging">Адреса</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,43 @@
|
|||||||
|
flowchart LR
|
||||||
|
classDef base fill:#e1f5fe,stroke:#0288d1,stroke-width:2px;
|
||||||
|
classDef net fill:#ede7f6,stroke:#5e35b1,stroke-width:2px;
|
||||||
|
classDef compute fill:#fff3e0,stroke:#f57c00,stroke-width:2px;
|
||||||
|
classDef target fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px;
|
||||||
|
classDef note fill:#fafafa,stroke:#9e9e9e,stroke-width:1px,stroke-dasharray: 4 4;
|
||||||
|
|
||||||
|
subgraph S1 ["Шаг 1. Тенант"]
|
||||||
|
ORG["<b>Организация (vcOrg: 19)</b><br/>• Тип: iaas / saas"]:::base
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph S2 ["Шаг 2. Пул ресурсов"]
|
||||||
|
VDC["<b>Виртуальный ДЦ (vcVdc: 21)</b><br/>• cpuGuaranteed: 0% / 50% / 80%<br/>• Пул: cpuAllocated, memAllocated, storage"]:::compute
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph S3 ["Шаг 3. Сетевой периметр"]
|
||||||
|
EDGE["<b>Шлюз Edge (vcNsxt: 22)</b><br/>• AVI Load Balancer<br/>• routed-сеть 10.10.102.0/24"]:::net
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph S4 ["Шаг 4. Среда запуска и адреса"]
|
||||||
|
VAPP["<b>Каталог vApp (26)</b><br/>(контейнер для ВМ)"]:::compute
|
||||||
|
IP["<b>Внешние IP</b><br/>• vcOrg modify / 25<br/>• квота адресов (ipSpace)"]:::note
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph S5 ["Шаг 5. Потребители"]
|
||||||
|
VM["<b>Виртуальная машина (28)</b><br/>• vmCpu, vmRam, vmDisk"]:::target
|
||||||
|
SHTURVAL["<b>Кластер Штурвал (150)</b><br/>• sizingPolicy, sizingDisk, count"]:::target
|
||||||
|
end
|
||||||
|
|
||||||
|
ORG -->|"organizationUid"| VDC
|
||||||
|
VDC -->|"vdcUid"| EDGE
|
||||||
|
ORG -.->|"modify: vIPConfigure"| IP
|
||||||
|
EDGE -.->|"modify: SNAT (ipSpaceName)"| IP
|
||||||
|
|
||||||
|
VDC -->|"vdcUid"| VAPP
|
||||||
|
EDGE -->|"nsxtUid"| VAPP
|
||||||
|
|
||||||
|
VAPP -->|"vappUid"| VM
|
||||||
|
IP -.->|"внешний IP"| VM
|
||||||
|
|
||||||
|
VDC -->|"vdcUid"| SHTURVAL
|
||||||
|
EDGE -->|"nsxtUid"| SHTURVAL
|
||||||
|
IP -.->|"внешний IP (>= 3)"| SHTURVAL
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 89 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg id="mermaid-svg" width="100%" xmlns="http://www.w3.org/2000/svg" class="flowchart" style="max-width: 2338.671875px;" viewBox="0 0 2338.671875 552" role="graphics-document document" aria-roledescription="flowchart-v2" xmlns:xlink="http://www.w3.org/1999/xlink"><style xmlns="http://www.w3.org/1999/xhtml">@import url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css");</style><style>#mermaid-svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg .error-icon{fill:#552222;}#mermaid-svg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg .marker{fill:#333333;stroke:#333333;}#mermaid-svg .marker.cross{stroke:#333333;}#mermaid-svg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg p{margin:0;}#mermaid-svg .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg .cluster-label text{fill:#333;}#mermaid-svg .cluster-label span{color:#333;}#mermaid-svg .cluster-label span p{background-color:transparent;}#mermaid-svg .label text,#mermaid-svg span{fill:#333;color:#333;}#mermaid-svg .node rect,#mermaid-svg .node circle,#mermaid-svg .node ellipse,#mermaid-svg .node polygon,#mermaid-svg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg .rough-node .label text,#mermaid-svg .node .label text,#mermaid-svg .image-shape .label,#mermaid-svg .icon-shape .label{text-anchor:middle;}#mermaid-svg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg .rough-node .label,#mermaid-svg .node .label,#mermaid-svg .image-shape .label,#mermaid-svg .icon-shape .label{text-align:center;}#mermaid-svg .node.clickable{cursor:pointer;}#mermaid-svg .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg .arrowheadPath{fill:#333333;}#mermaid-svg .edgePath .path{stroke:#333333;stroke-width:1px;}#mermaid-svg .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg .cluster text{fill:#333;}#mermaid-svg .cluster span{color:#333;}#mermaid-svg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg rect.text{fill:none;stroke-width:0;}#mermaid-svg .icon-shape,#mermaid-svg .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg .icon-shape p,#mermaid-svg .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg .icon-shape .label rect,#mermaid-svg .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg .node .neo-node{stroke:#9370DB;}#mermaid-svg [data-look="neo"].node rect,#mermaid-svg [data-look="neo"].cluster rect,#mermaid-svg [data-look="neo"].node polygon{stroke:#9370DB;filter:drop-shadow(1px 2px 2px rgba(185, 185, 185, 1));}#mermaid-svg [data-look="neo"].swimlane.cluster rect{filter:none;}#mermaid-svg [data-look="neo"].node path{stroke:#9370DB;stroke-width:1px;}#mermaid-svg [data-look="neo"].node .outer-path{filter:drop-shadow(1px 2px 2px rgba(185, 185, 185, 1));}#mermaid-svg [data-look="neo"].node .neo-line path{stroke:#9370DB;filter:none;}#mermaid-svg [data-look="neo"].node circle{stroke:#9370DB;filter:drop-shadow(1px 2px 2px rgba(185, 185, 185, 1));}#mermaid-svg [data-look="neo"].node circle .state-start{fill:#000000;}#mermaid-svg [data-look="neo"].icon-shape .icon{fill:#9370DB;filter:drop-shadow(1px 2px 2px rgba(185, 185, 185, 1));}#mermaid-svg [data-look="neo"].icon-shape .icon-neo path{stroke:#9370DB;filter:drop-shadow(1px 2px 2px rgba(185, 185, 185, 1));}#mermaid-svg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg .base>*{fill:rgb(225, 245, 254)!important;stroke:rgb(2, 136, 209)!important;stroke-width:2px!important;}#mermaid-svg .base span{fill:rgb(225, 245, 254)!important;stroke:rgb(2, 136, 209)!important;stroke-width:2px!important;}#mermaid-svg .net>*{fill:rgb(237, 231, 246)!important;stroke:rgb(94, 53, 177)!important;stroke-width:2px!important;}#mermaid-svg .net span{fill:rgb(237, 231, 246)!important;stroke:rgb(94, 53, 177)!important;stroke-width:2px!important;}#mermaid-svg .compute>*{fill:rgb(255, 243, 224)!important;stroke:rgb(245, 124, 0)!important;stroke-width:2px!important;}#mermaid-svg .compute span{fill:rgb(255, 243, 224)!important;stroke:rgb(245, 124, 0)!important;stroke-width:2px!important;}#mermaid-svg .target>*{fill:rgb(232, 245, 233)!important;stroke:rgb(46, 125, 50)!important;stroke-width:2px!important;}#mermaid-svg .target span{fill:rgb(232, 245, 233)!important;stroke:rgb(46, 125, 50)!important;stroke-width:2px!important;}#mermaid-svg .note>*{fill:rgb(250, 250, 250)!important;stroke:rgb(158, 158, 158)!important;stroke-width:1px!important;stroke-dasharray:4,4!important;}#mermaid-svg .note span{fill:rgb(250, 250, 250)!important;stroke:rgb(158, 158, 158)!important;stroke-width:1px!important;stroke-dasharray:4,4!important;}</style><g><marker id="mermaid-svg_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="mermaid-svg_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="mermaid-svg_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="mermaid-svg_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="mermaid-svg_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="mermaid-svg_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="mermaid-svg_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="mermaid-svg_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="mermaid-svg_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="mermaid-svg_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="mermaid-svg_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="mermaid-svg_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"><g class="cluster " id="mermaid-svg-S5" data-look="classic"><rect style="" x="2020.671875" y="60" width="310" height="449"/><g class="cluster-label " transform="translate(2101.4140625, 60)"><foreignObject width="148.515625" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5;"><span class="nodeLabel "><p>Шаг 5. Потребители</p></span></div></foreignObject></g></g><g class="cluster " id="mermaid-svg-S4" data-look="classic"><rect style="" x="1539.546875" y="37" width="298.9375" height="322"/><g class="cluster-label " transform="translate(1573.4921875, 37)"><foreignObject width="231.046875" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5;"><span class="nodeLabel "><p>Шаг 4. Среда запуска и адреса</p></span></div></foreignObject></g></g><g class="cluster " id="mermaid-svg-S3" data-look="classic"><rect style="" x="979.546875" y="98" width="310" height="276"/><g class="cluster-label " transform="translate(1039.9453125, 98)"><foreignObject width="189.203125" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5;"><span class="nodeLabel "><p>Шаг 3. Сетевой периметр</p></span></div></foreignObject></g></g><g class="cluster " id="mermaid-svg-S2" data-look="classic"><rect style="" x="471.03125" y="178" width="310" height="366"/><g class="cluster-label " transform="translate(549.703125, 178)"><foreignObject width="152.65625" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5;"><span class="nodeLabel "><p>Шаг 2. Пул ресурсов</p></span></div></foreignObject></g></g><g class="cluster " id="mermaid-svg-S1" data-look="classic"><rect style="" x="8" y="8" width="301.84375" height="403"/><g class="cluster-label " transform="translate(107.265625, 8)"><foreignObject width="103.3125" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5;"><span class="nodeLabel "><p>Шаг 1. Тенант</p></span></div></foreignObject></g></g></g><g class="edgePaths"><path d="M201.574,265L219.619,281.5C237.664,298,273.754,331,305.231,347.5C336.708,364,363.573,364,390.438,364C417.302,364,444.167,364,461.099,364C478.031,364,485.031,364,488.531,364L492.031,364" id="mermaid-svg-L_ORG_VDC_0" class=" edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_ORG_VDC_0" data-points="W3sieCI6MjAxLjU3MzcwOTIzOTEzMDQ0LCJ5IjoyNjV9LHsieCI6MzA5Ljg0Mzc1LCJ5IjozNjR9LHsieCI6MzkwLjQzNzUsInkiOjM2NH0seyJ4Ijo0NzEuMDMxMjUsInkiOjM2NH0seyJ4Ijo0OTYuMDMxMjUsInkiOjM2NH1d" data-look="classic" marker-end="url(#mermaid-svg_flowchart-v2-pointEnd)"/><path d="M740.311,277L747.098,271.833C753.884,266.667,767.458,256.333,790.787,251.167C814.117,246,847.203,246,880.289,246C913.375,246,946.461,246,966.504,246C986.547,246,993.547,246,997.047,246L1000.547,246" id="mermaid-svg-L_VDC_EDGE_0" class=" edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_VDC_EDGE_0" data-points="W3sieCI6NzQwLjMxMDkxMTAxNjk0OTEsInkiOjI3N30seyJ4Ijo3ODEuMDMxMjUsInkiOjI0Nn0seyJ4Ijo4ODAuMjg5MDYyNSwieSI6MjQ2fSx7IngiOjk3OS41NDY4NzUsInkiOjI0Nn0seyJ4IjoxMDA0LjU0Njg3NSwieSI6MjQ2fV0=" data-look="classic" marker-end="url(#mermaid-svg_flowchart-v2-pointEnd)"/><path d="M195.481,187L214.541,166.667C233.602,146.333,271.723,105.667,304.216,85.333C336.708,65,363.573,65,390.438,65C417.302,65,444.167,65,483.432,65C522.698,65,574.365,65,626.031,65C677.698,65,729.365,65,771.741,65C814.117,65,847.203,65,880.289,65C913.375,65,946.461,65,988.837,65C1031.214,65,1082.88,65,1134.547,65C1186.214,65,1237.88,65,1284.547,78.333C1331.214,91.667,1372.88,118.333,1414.547,131.667C1456.214,145,1497.88,145,1522.221,144.484C1546.561,143.968,1553.575,142.935,1557.082,142.419L1560.59,141.903" id="mermaid-svg-L_ORG_IP_0" class=" edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_ORG_IP_0" data-points="W3sieCI6MTk1LjQ4MDU5MDA2MjExMTgsInkiOjE4N30seyJ4IjozMDkuODQzNzUsInkiOjY1fSx7IngiOjM5MC40Mzc1LCJ5Ijo2NX0seyJ4Ijo0NzEuMDMxMjUsInkiOjY1fSx7IngiOjYyNi4wMzEyNSwieSI6NjV9LHsieCI6NzgxLjAzMTI1LCJ5Ijo2NX0seyJ4Ijo4ODAuMjg5MDYyNSwieSI6NjV9LHsieCI6OTc5LjU0Njg3NSwieSI6NjV9LHsieCI6MTEzNC41NDY4NzUsInkiOjY1fSx7IngiOjEyODkuNTQ2ODc1LCJ5Ijo2NX0seyJ4IjoxNDE0LjU0Njg3NSwieSI6MTQ1fSx7IngiOjE1MzkuNTQ2ODc1LCJ5IjoxNDV9LHsieCI6MTU2NC41NDY4NzUsInkiOjE0MS4zMjAzMDEwNjYyNzY0fV0=" data-look="classic" marker-end="url(#mermaid-svg_flowchart-v2-pointEnd)"/><path d="M1258.154,183L1263.387,180.333C1268.619,177.667,1279.083,172.333,1305.148,158.667C1331.214,145,1372.88,123,1414.547,112C1456.214,101,1497.88,101,1522.221,101.516C1546.561,102.032,1553.575,103.065,1557.082,103.581L1560.59,104.097" id="mermaid-svg-L_EDGE_IP_0" class=" edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_EDGE_IP_0" data-points="W3sieCI6MTI1OC4xNTQ0Njk5MzY3MDksInkiOjE4M30seyJ4IjoxMjg5LjU0Njg3NSwieSI6MTY3fSx7IngiOjE0MTQuNTQ2ODc1LCJ5IjoxMDF9LHsieCI6MTUzOS41NDY4NzUsInkiOjEwMX0seyJ4IjoxNTY0LjU0Njg3NSwieSI6MTA0LjY3OTY5ODkzMzcyMzZ9XQ==" data-look="classic" marker-end="url(#mermaid-svg_flowchart-v2-pointEnd)"/><path d="M756.031,399.226L760.198,400.355C764.365,401.484,772.698,403.742,793.408,404.871C814.117,406,847.203,406,880.289,406C913.375,406,946.461,406,988.837,406C1031.214,406,1082.88,406,1134.547,406C1186.214,406,1237.88,406,1284.547,390.333C1331.214,374.667,1372.88,343.333,1414.547,327.667C1456.214,312,1497.88,312,1525.684,310.741C1553.487,309.482,1567.428,306.964,1574.398,305.704L1581.368,304.445" id="mermaid-svg-L_VDC_VAPP_0" class=" edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_VDC_VAPP_0" data-points="W3sieCI6NzU2LjAzMTI1LCJ5IjozOTkuMjI1ODA2NDUxNjEyOX0seyJ4Ijo3ODEuMDMxMjUsInkiOjQwNn0seyJ4Ijo4ODAuMjg5MDYyNSwieSI6NDA2fSx7IngiOjk3OS41NDY4NzUsInkiOjQwNn0seyJ4IjoxMTM0LjU0Njg3NSwieSI6NDA2fSx7IngiOjEyODkuNTQ2ODc1LCJ5Ijo0MDZ9LHsieCI6MTQxNC41NDY4NzUsInkiOjMxMn0seyJ4IjoxNTM5LjU0Njg3NSwieSI6MzEyfSx7IngiOjE1ODUuMzA0Njg3NSwieSI6MzAzLjczNDMxOTQ2NDc3MTF9XQ==" data-look="classic" marker-end="url(#mermaid-svg_flowchart-v2-pointEnd)"/><path d="M1264.547,224.194L1268.714,223.495C1272.88,222.796,1281.214,221.398,1306.214,220.699C1331.214,220,1372.88,220,1414.547,220C1456.214,220,1497.88,220,1528.067,224.067C1558.253,228.135,1576.96,236.27,1586.313,240.337L1595.666,244.405" id="mermaid-svg-L_EDGE_VAPP_0" class=" edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_EDGE_VAPP_0" data-points="W3sieCI6MTI2NC41NDY4NzUsInkiOjIyNC4xOTM1NDgzODcwOTY3N30seyJ4IjoxMjg5LjU0Njg3NSwieSI6MjIwfSx7IngiOjE0MTQuNTQ2ODc1LCJ5IjoyMjB9LHsieCI6MTUzOS41NDY4NzUsInkiOjIyMH0seyJ4IjoxNTk5LjMzNDM3NSwieSI6MjQ2fV0=" data-look="classic" marker-end="url(#mermaid-svg_flowchart-v2-pointEnd)"/><path d="M1792.727,285L1800.353,285C1807.979,285,1823.232,285,1846.04,285C1868.849,285,1899.214,285,1929.578,285C1959.943,285,1990.307,285,2019.627,274.237C2048.947,263.474,2077.222,241.949,2091.36,231.186L2105.498,220.423" id="mermaid-svg-L_VAPP_VM_0" class=" edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_VAPP_VM_0" data-points="W3sieCI6MTc5Mi43MjY1NjI1LCJ5IjoyODV9LHsieCI6MTgzOC40ODQzNzUsInkiOjI4NX0seyJ4IjoxOTI5LjU3ODEyNSwieSI6Mjg1fSx7IngiOjIwMjAuNjcxODc1LCJ5IjoyODV9LHsieCI6MjEwOC42ODAzNDk1NzYyNzEsInkiOjIxOH1d" data-look="classic" marker-end="url(#mermaid-svg_flowchart-v2-pointEnd)"/><path d="M1813.484,123L1817.651,123C1821.818,123,1830.151,123,1849.5,123C1868.849,123,1899.214,123,1929.578,123C1959.943,123,1990.307,123,2009.015,124.001C2027.723,125.001,2034.773,127.003,2038.299,128.004L2041.824,129.004" id="mermaid-svg-L_IP_VM_0" class=" edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_IP_VM_0" data-points="W3sieCI6MTgxMy40ODQzNzUsInkiOjEyM30seyJ4IjoxODM4LjQ4NDM3NSwieSI6MTIzfSx7IngiOjE5MjkuNTc4MTI1LCJ5IjoxMjN9LHsieCI6MjAyMC42NzE4NzUsInkiOjEyM30seyJ4IjoyMDQ1LjY3MTg3NSwieSI6MTMwLjA5Njc3NDE5MzU0ODM4fV0=" data-look="classic" marker-end="url(#mermaid-svg_flowchart-v2-pointEnd)"/><path d="M756.031,441.161L760.198,443.634C764.365,446.108,772.698,451.054,793.408,453.527C814.117,456,847.203,456,880.289,456C913.375,456,946.461,456,988.837,456C1031.214,456,1082.88,456,1134.547,456C1186.214,456,1237.88,456,1284.547,456C1331.214,456,1372.88,456,1414.547,456C1456.214,456,1497.88,456,1543.625,456C1589.37,456,1639.193,456,1689.016,456C1738.839,456,1788.661,456,1828.755,456C1868.849,456,1899.214,456,1929.578,456C1959.943,456,1990.307,456,2011.337,453.434C2032.368,450.869,2044.063,445.738,2049.911,443.172L2055.759,440.607" id="mermaid-svg-L_VDC_SHTURVAL_0" class=" edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_VDC_SHTURVAL_0" data-points="W3sieCI6NzU2LjAzMTI1LCJ5Ijo0NDEuMTYxMjkwMzIyNTgwNn0seyJ4Ijo3ODEuMDMxMjUsInkiOjQ1Nn0seyJ4Ijo4ODAuMjg5MDYyNSwieSI6NDU2fSx7IngiOjk3OS41NDY4NzUsInkiOjQ1Nn0seyJ4IjoxMTM0LjU0Njg3NSwieSI6NDU2fSx7IngiOjEyODkuNTQ2ODc1LCJ5Ijo0NTZ9LHsieCI6MTQxNC41NDY4NzUsInkiOjQ1Nn0seyJ4IjoxNTM5LjU0Njg3NSwieSI6NDU2fSx7IngiOjE2ODkuMDE1NjI1LCJ5Ijo0NTZ9LHsieCI6MTgzOC40ODQzNzUsInkiOjQ1Nn0seyJ4IjoxOTI5LjU3ODEyNSwieSI6NDU2fSx7IngiOjIwMjAuNjcxODc1LCJ5Ijo0NTZ9LHsieCI6MjA1OS40MjE4NzUsInkiOjQzOX1d" data-look="classic" marker-end="url(#mermaid-svg_flowchart-v2-pointEnd)"/><path d="M1264.547,303.871L1268.714,305.726C1272.88,307.581,1281.214,311.29,1306.214,325.812C1331.214,340.333,1372.88,365.667,1414.547,378.333C1456.214,391,1497.88,391,1543.625,391C1589.37,391,1639.193,391,1689.016,391C1738.839,391,1788.661,391,1828.755,391C1868.849,391,1899.214,391,1929.578,391C1959.943,391,1990.307,391,2008.99,390.932C2027.672,390.865,2034.672,390.729,2038.172,390.661L2041.673,390.594" id="mermaid-svg-L_EDGE_SHTURVAL_0" class=" edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_EDGE_SHTURVAL_0" data-points="W3sieCI6MTI2NC41NDY4NzUsInkiOjMwMy44NzA5Njc3NDE5MzU1fSx7IngiOjEyODkuNTQ2ODc1LCJ5IjozMTV9LHsieCI6MTQxNC41NDY4NzUsInkiOjM5MX0seyJ4IjoxNTM5LjU0Njg3NSwieSI6MzkxfSx7IngiOjE2ODkuMDE1NjI1LCJ5IjozOTF9LHsieCI6MTgzOC40ODQzNzUsInkiOjM5MX0seyJ4IjoxOTI5LjU3ODEyNSwieSI6MzkxfSx7IngiOjIwMjAuNjcxODc1LCJ5IjozOTF9LHsieCI6MjA0NS42NzE4NzUsInkiOjM5MC41MTYxMjkwMzIyNTgwNX1d" data-look="classic" marker-end="url(#mermaid-svg_flowchart-v2-pointEnd)"/><path d="M1725.143,174L1744.033,200.667C1762.924,227.333,1800.704,280.667,1834.776,307.333C1868.849,334,1899.214,334,1929.578,334C1959.943,334,1990.307,334,2009.027,335.232C2027.746,336.465,2034.82,338.929,2038.357,340.161L2041.895,341.394" id="mermaid-svg-L_IP_SHTURVAL_0" class=" edge-thickness-normal edge-pattern-dotted edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_IP_SHTURVAL_0" data-points="W3sieCI6MTcyNS4xNDMxNDI3NzI1MTIsInkiOjE3NH0seyJ4IjoxODM4LjQ4NDM3NSwieSI6MzM0fSx7IngiOjE5MjkuNTc4MTI1LCJ5IjozMzR9LHsieCI6MjAyMC42NzE4NzUsInkiOjMzNH0seyJ4IjoyMDQ1LjY3MTg3NSwieSI6MzQyLjcwOTY3NzQxOTM1NDh9XQ==" data-look="classic" marker-end="url(#mermaid-svg_flowchart-v2-pointEnd)"/></g><g class="edgeLabels"><g class="edgeLabel" transform="translate(390.4375, 364)"><g class="label" data-id="L_ORG_VDC_0" transform="translate(-55.59375, -12)"><foreignObject width="111.1875" height="24"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel "><p>organizationUid</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(880.2890625, 246)"><g class="label" data-id="L_VDC_EDGE_0" transform="translate(-24.453125, -12)"><foreignObject width="48.90625" height="24"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel "><p>vdcUid</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(880.2890625, 65)"><g class="label" data-id="L_ORG_IP_0" transform="translate(-74.2578125, -12)"><foreignObject width="148.515625" height="24"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel "><p>modify: vIPConfigure</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(1414.546875, 101)"><g class="label" data-id="L_EDGE_IP_0" transform="translate(-100, -24)"><foreignObject width="200" height="48"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="edgeLabel "><p>modify: SNAT (ipSpaceName)</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(1134.546875, 406)"><g class="label" data-id="L_VDC_VAPP_0" transform="translate(-24.453125, -12)"><foreignObject width="48.90625" height="24"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel "><p>vdcUid</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(1414.546875, 220)"><g class="label" data-id="L_EDGE_VAPP_0" transform="translate(-26.6796875, -12)"><foreignObject width="53.359375" height="24"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel "><p>nsxtUid</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(1929.578125, 285)"><g class="label" data-id="L_VAPP_VM_0" transform="translate(-29.3515625, -12)"><foreignObject width="58.703125" height="24"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel "><p>vappUid</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(1929.578125, 123)"><g class="label" data-id="L_IP_VM_0" transform="translate(-42.671875, -12)"><foreignObject width="85.34375" height="24"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel "><p>внешний IP</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(1414.546875, 456)"><g class="label" data-id="L_VDC_SHTURVAL_0" transform="translate(-24.453125, -12)"><foreignObject width="48.90625" height="24"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel "><p>vdcUid</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(1689.015625, 391)"><g class="label" data-id="L_EDGE_SHTURVAL_0" transform="translate(-26.6796875, -12)"><foreignObject width="53.359375" height="24"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel "><p>nsxtUid</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(1929.578125, 334)"><g class="label" data-id="L_IP_SHTURVAL_0" transform="translate(-66.09375, -12)"><foreignObject width="132.1875" height="24"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel "><p>внешний IP (>= 3)</p></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default base " id="mermaid-svg-flowchart-ORG-0" data-look="classic" transform="translate(158.921875, 226)"><rect class="basic label-container" style="fill:#e1f5fe !important;stroke:#0288d1 !important;stroke-width:2px !important" x="-125.921875" y="-39" width="251.84375" height="78"/><g class="label" style="" transform="translate(-95.921875, -24)"><rect/><foreignObject width="191.84375" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel "><p><b>Организация (vcOrg: 19)</b><br />• Тип: iaas / saas</p></span></div></foreignObject></g></g><g class="node default compute " id="mermaid-svg-flowchart-VDC-1" data-look="classic" transform="translate(626.03125, 364)"><rect class="basic label-container" style="fill:#fff3e0 !important;stroke:#f57c00 !important;stroke-width:2px !important" x="-130" y="-87" width="260" height="174"/><g class="label" style="" transform="translate(-100, -72)"><rect/><foreignObject width="200" height="144"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel "><p><b>Виртуальный ДЦ (vcVdc: 21)</b><br />• cpuGuaranteed: 0% / 50% / 80%<br />• Пул: cpuAllocated, memAllocated, storage</p></span></div></foreignObject></g></g><g class="node default net " id="mermaid-svg-flowchart-EDGE-2" data-look="classic" transform="translate(1134.546875, 246)"><rect class="basic label-container" style="fill:#ede7f6 !important;stroke:#5e35b1 !important;stroke-width:2px !important" x="-130" y="-63" width="260" height="126"/><g class="label" style="" transform="translate(-100, -48)"><rect/><foreignObject width="200" height="96"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel "><p><b>Шлюз Edge (vcNsxt: 22)</b><br />• AVI Load Balancer<br />• routed-сеть 10.10.102.0/24</p></span></div></foreignObject></g></g><g class="node default compute " id="mermaid-svg-flowchart-VAPP-3" data-look="classic" transform="translate(1689.015625, 285)"><rect class="basic label-container" style="fill:#fff3e0 !important;stroke:#f57c00 !important;stroke-width:2px !important" x="-103.7109375" y="-39" width="207.421875" height="78"/><g class="label" style="" transform="translate(-73.7109375, -24)"><rect/><foreignObject width="147.421875" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel "><p><b>Каталог vApp (26)</b><br />(контейнер для ВМ)</p></span></div></foreignObject></g></g><g class="node default note " id="mermaid-svg-flowchart-IP-4" data-look="classic" transform="translate(1689.015625, 123)"><rect class="basic label-container" style="fill:#fafafa !important;stroke:#9e9e9e !important;stroke-width:1px !important;stroke-dasharray:4 4 !important" x="-124.46875" y="-51" width="248.9375" height="102"/><g class="label" style="" transform="translate(-94.46875, -36)"><rect/><foreignObject width="188.9375" height="72"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel "><p><b>Внешние IP</b><br />• vcOrg modify / 25<br />• квота адресов (ipSpace)</p></span></div></foreignObject></g></g><g class="node default target " id="mermaid-svg-flowchart-VM-5" data-look="classic" transform="translate(2175.671875, 167)"><rect class="basic label-container" style="fill:#e8f5e9 !important;stroke:#2e7d32 !important;stroke-width:2px !important" x="-130" y="-51" width="260" height="102"/><g class="label" style="" transform="translate(-100, -36)"><rect/><foreignObject width="200" height="72"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel "><p><b>Виртуальная машина (28)</b><br />• vmCpu, vmRam, vmDisk</p></span></div></foreignObject></g></g><g class="node default target " id="mermaid-svg-flowchart-SHTURVAL-6" data-look="classic" transform="translate(2175.671875, 388)"><rect class="basic label-container" style="fill:#e8f5e9 !important;stroke:#2e7d32 !important;stroke-width:2px !important" x="-130" y="-51" width="260" height="102"/><g class="label" style="" transform="translate(-100, -36)"><rect/><foreignObject width="200" height="72"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel "><p><b>Кластер Штурвал (150)</b><br />• sizingPolicy, sizingDisk, count</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="mermaid-svg-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="mermaid-svg-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs></svg>
|
||||||
|
After Width: | Height: | Size: 32 KiB |
@@ -0,0 +1,303 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Генератор схемы зависимостей облачных сервисов (PNG + SVG).
|
||||||
|
|
||||||
|
Содержание подтверждено YAML-контрактами generated/dev/resources_yaml.
|
||||||
|
PNG рисуется в 2x и уменьшается (сглаживание), SVG — вектор.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
|
W, H = 1600, 720
|
||||||
|
SS = 2 # supersample
|
||||||
|
|
||||||
|
FONT_R = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
|
||||||
|
FONT_B = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
|
||||||
|
|
||||||
|
DARK = "#1A1A1A"
|
||||||
|
GRAY = "#5F6B76"
|
||||||
|
MUTED = "#7A8794"
|
||||||
|
TEXT = "#2E3A45"
|
||||||
|
RED = "#C62828"
|
||||||
|
LINE = "#455A64"
|
||||||
|
|
||||||
|
COLS = [
|
||||||
|
(16, "Шаг 1. Тенант", "базовый уровень изоляции", "#1565C0", "#E8F3FB"),
|
||||||
|
(340, "Шаг 2. Пул ресурсов", "CPU / RAM / Storage", "#E65100", "#FDF1E3"),
|
||||||
|
(664, "Шаг 3. Сетевой периметр", "маршрутизация и балансировка", "#4527A0", "#EEEAF8"),
|
||||||
|
(988, "Шаг 4. Среда запуска", "контейнер и внешние адреса", "#EF6C00", "#FDF4E3"),
|
||||||
|
(1312, "Шаг 5. Потребители", "что разворачивается в итоге", "#2E7D32", "#E9F5EA"),
|
||||||
|
]
|
||||||
|
COL_W = 270
|
||||||
|
|
||||||
|
# блоки: (x, y, w, h, заголовок-строки, подпись, строки, бейдж, рамка, фон, пунктир)
|
||||||
|
BLOCKS = [
|
||||||
|
(31, 150, 240, 150, ["Организация", "в Cloud Director"], "Сервис 19 · vcOrg",
|
||||||
|
["Типы: iaas / saas"], None, "#0288D1", "#FFFFFF", False),
|
||||||
|
(355, 130, 240, 230, ["Виртуальный", "датацентр (vDC)"], "Сервис 21 · vcVdc",
|
||||||
|
["cpuGuaranteed: 0% / 50% / 80%", "cpuAllocated, memAllocated", "storageConfig"],
|
||||||
|
"здесь выделяются CPU / RAM / Диск", "#F57C00", "#FFF3E0", False),
|
||||||
|
(679, 120, 240, 200, ["Сетевой шлюз", "периметра (Edge)"], "Сервис 22 · vcNsxt",
|
||||||
|
["AVI Load Balancer", "routed-сеть 10.10.102.0/24"],
|
||||||
|
"SNAT настраивается операцией modify", "#5E35B1", "#EDE7F6", False),
|
||||||
|
(1003, 100, 240, 110, ["Каталог ВМ (vApp)"], "Сервис 26 · vapp",
|
||||||
|
["логический контейнер для ВМ"], None, "#F57C00", "#FFF3E0", False),
|
||||||
|
(1003, 245, 180, 150, ["Внешние IP"], "Сервис 25",
|
||||||
|
["выделение: vcOrg", "NAT: сервис 25", "квота IP (ipSpace)"],
|
||||||
|
"здесь выделяются IP", "#9E9E9E", "#FAFAFA", True),
|
||||||
|
(1327, 100, 240, 120, ["Виртуальная машина"], "Сервис 28 · vc_vm_v3",
|
||||||
|
["vmCpu · vmRam · vmDisk"], None, "#2E7D32", "#E8F5E9", False),
|
||||||
|
(1327, 255, 240, 170, ["Kubernetes кластер", "Штурвал"], "Сервис 150",
|
||||||
|
["sizingPolicy · sizingDisk", "· count"], "требует ALB + VS ≥ 3",
|
||||||
|
"#2E7D32", "#E8F5E9", False),
|
||||||
|
]
|
||||||
|
|
||||||
|
# связи: (точки, цвет, толщина, пунктир, подпись, позиция подписи, вертикальная)
|
||||||
|
EDGES = [
|
||||||
|
([(271, 225), (355, 225)], LINE, 2, False, "organizationUid", (313, 262), True),
|
||||||
|
([(595, 200), (679, 200)], LINE, 2, False, "vdcUid", (637, 186), False),
|
||||||
|
([(500, 130), (500, 92), (961, 92), (961, 140), (1003, 140)], LINE, 2, False,
|
||||||
|
"vdcUid", (706, 96), False),
|
||||||
|
([(919, 180), (1003, 180)], LINE, 2, False, "nsxtUid", (961, 166), False),
|
||||||
|
([(1243, 140), (1327, 140)], LINE, 2, False, "vappUid", (1285, 126), False),
|
||||||
|
([(500, 360), (500, 470), (1447, 470), (1447, 425)], LINE, 2, False,
|
||||||
|
"vdcUid", (1180, 456), False),
|
||||||
|
([(799, 320), (799, 505), (1467, 505), (1467, 425)], LINE, 2, False,
|
||||||
|
"nsxtUid", (1180, 491), False),
|
||||||
|
([(151, 300), (151, 540), (1050, 540), (1050, 395)], MUTED, 2, True,
|
||||||
|
"modify: vIPConfigure", (161, 526), False),
|
||||||
|
([(870, 320), (870, 400), (1140, 400), (1140, 395)], MUTED, 2, True,
|
||||||
|
"modify: SNAT", (880, 386), False),
|
||||||
|
([(1050, 210), (1050, 245)], MUTED, 2, True, "требует Каталог", (1060, 218), False),
|
||||||
|
([(1183, 270), (1255, 270), (1255, 160), (1327, 160)], MUTED, 2, True,
|
||||||
|
"внешний IP", (1187, 222), False),
|
||||||
|
([(1183, 350), (1327, 350)], RED, 2, True, "≥ 3 адреса", (1187, 330), False),
|
||||||
|
]
|
||||||
|
|
||||||
|
CAPTION = [
|
||||||
|
"Порядок создания: Организация → vDC → Edge → (vApp + внешние IP) → ВМ / Штурвал.",
|
||||||
|
"SNAT — не отдельный сервис, а операция modify у Edge.",
|
||||||
|
"vDC и Edge могут быть переиспользованы существующие.",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def op_rect(x, y, w, h, fill, outline, lw=2, r=10, dash=False):
|
||||||
|
return ("rect", (x, y, w, h, fill, outline, lw, r, dash))
|
||||||
|
|
||||||
|
|
||||||
|
def op_text(x, y, s, size, color, bold=False, anchor="lt", vertical=False):
|
||||||
|
return ("text", (x, y, s, size, color, bold, anchor, vertical))
|
||||||
|
|
||||||
|
|
||||||
|
def op_path(pts, color, lw, dash):
|
||||||
|
return ("path", (pts, color, lw, dash))
|
||||||
|
|
||||||
|
|
||||||
|
def op_dot(x, y, r, color):
|
||||||
|
return ("dot", (x, y, r, color))
|
||||||
|
|
||||||
|
|
||||||
|
def build():
|
||||||
|
ops = []
|
||||||
|
for cx, title, sub, accent, tint in COLS:
|
||||||
|
ops.append(op_rect(cx, 8, COL_W, 552, tint, "#FFFFFF", 0, 12))
|
||||||
|
ops.append(op_rect(cx, 14, COL_W, 44, accent, accent, 0, 8))
|
||||||
|
ops.append(op_text(cx + COL_W / 2, 25, title, 15, "#FFFFFF", True, "mt"))
|
||||||
|
ops.append(op_text(cx + COL_W / 2, 68, sub, 12, MUTED, False, "mt"))
|
||||||
|
|
||||||
|
for x, y, w, h, titles, sub, lines, badge, border, fill, dashed in BLOCKS:
|
||||||
|
ops.append(op_rect(x, y, w, h, fill, border, 2, 10, dashed))
|
||||||
|
ops.append(op_rect(x + 12, y + 12, 22, 22, border, border, 0, 5))
|
||||||
|
tx = x + 42
|
||||||
|
ty = y + 12
|
||||||
|
for i, t in enumerate(titles):
|
||||||
|
ops.append(op_text(tx, ty + i * 20, t, 15, DARK, True))
|
||||||
|
sy = ty + len(titles) * 20
|
||||||
|
ops.append(op_text(tx, sy, sub, 11, GRAY))
|
||||||
|
ly = sy + 22
|
||||||
|
for ln in lines:
|
||||||
|
ops.append(op_dot(x + 20, ly + 7, 2.4, border))
|
||||||
|
ops.append(op_text(x + 30, ly, ln, 10, TEXT))
|
||||||
|
ly += 20
|
||||||
|
if badge:
|
||||||
|
ops.append(op_rect(x + 12, y + h - 42, w - 24, 30, "#FFFFFF", border, 1, 6))
|
||||||
|
ops.append(op_text(x + w / 2, y + h - 32, badge, 10, border, False, "mt"))
|
||||||
|
|
||||||
|
for pts, color, lw, dash, label, pos, vertical in EDGES:
|
||||||
|
ops.append(op_path(pts, color, lw, dash))
|
||||||
|
ops.append(op_text(pos[0], pos[1], label, 10, color, False,
|
||||||
|
"lt" if not vertical else "lt", vertical))
|
||||||
|
|
||||||
|
for i, s in enumerate(CAPTION):
|
||||||
|
ops.append(op_text(24, 590 + i * 26, "> " + s, 14, TEXT))
|
||||||
|
|
||||||
|
ops.append(op_path([(980, 606), (1040, 606)], LINE, 2, False))
|
||||||
|
ops.append(op_text(1052, 597, "Сплошная линия — обязательная зависимость", 11, TEXT))
|
||||||
|
ops.append(op_text(1052, 613, "(ссылка на UUID существующего инстанса)", 10, MUTED))
|
||||||
|
ops.append(op_path([(980, 650), (1040, 650)], MUTED, 2, True))
|
||||||
|
ops.append(op_text(1052, 641, "Пунктир — выделение ресурса / modify / предусловие", 11, TEXT))
|
||||||
|
|
||||||
|
swatches = [("#1565C0", "Тенант"), ("#F57C00", "Ресурсы"), ("#5E35B1", "Сеть"),
|
||||||
|
("#2E7D32", "Потребители"), ("#9E9E9E", "Адреса")]
|
||||||
|
sx = 980
|
||||||
|
for color, name in swatches:
|
||||||
|
ops.append(op_rect(sx, 684, 12, 12, color, color, 0, 3))
|
||||||
|
ops.append(op_text(sx + 16, 682, name, 10, TEXT))
|
||||||
|
sx += 20 + int(len(name) * 6.4)
|
||||||
|
return ops
|
||||||
|
|
||||||
|
|
||||||
|
def accent_of(border):
|
||||||
|
return border
|
||||||
|
|
||||||
|
|
||||||
|
def dash_segments(p1, p2, on=9, off=6):
|
||||||
|
(x1, y1), (x2, y2) = p1, p2
|
||||||
|
segs, total = [], ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
|
||||||
|
if total == 0:
|
||||||
|
return segs
|
||||||
|
ux, uy = (x2 - x1) / total, (y2 - y1) / total
|
||||||
|
pos = 0.0
|
||||||
|
while pos < total:
|
||||||
|
end = min(pos + on, total)
|
||||||
|
segs.append(((x1 + ux * pos, y1 + uy * pos), (x1 + ux * end, y1 + uy * end)))
|
||||||
|
pos = end + off
|
||||||
|
return segs
|
||||||
|
|
||||||
|
|
||||||
|
def arrow_head(d, tip, tail, color, size=9):
|
||||||
|
(tx, ty), (px, py) = tip, tail
|
||||||
|
dx, dy = tx - px, ty - py
|
||||||
|
n = (dx * dx + dy * dy) ** 0.5 or 1
|
||||||
|
ux, uy = dx / n, dy / n
|
||||||
|
bx, by = tx - ux * size, ty - uy * size
|
||||||
|
ox, oy = -uy * size * 0.42, ux * size * 0.42
|
||||||
|
d.polygon([(tx, ty), (bx + ox, by + oy), (bx - ox, by - oy)], fill=color)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_polyline(d, pts, color, lw, dash):
|
||||||
|
for a, b in zip(pts, pts[1:]):
|
||||||
|
segs = dash_segments(a, b) if dash else [(a, b)]
|
||||||
|
for s in segs:
|
||||||
|
d.line([s[0], s[1]], fill=color, width=lw)
|
||||||
|
if len(pts) >= 2:
|
||||||
|
tail = pts[-2]
|
||||||
|
tip = pts[-1]
|
||||||
|
dx, dy = tip[0] - tail[0], tip[1] - tail[1]
|
||||||
|
n = (dx * dx + dy * dy) ** 0.5 or 1
|
||||||
|
shrink = 9
|
||||||
|
tip2 = (tip[0] - dx / n * shrink, tip[1] - dy / n * shrink)
|
||||||
|
d.line([tail, tip2], fill=color, width=lw)
|
||||||
|
arrow_head(d, tip, tail, color)
|
||||||
|
|
||||||
|
|
||||||
|
def render_png(ops, path):
|
||||||
|
img = Image.new("RGB", (W * SS, H * SS), "#FFFFFF")
|
||||||
|
d = ImageDraw.Draw(img)
|
||||||
|
fonts = {}
|
||||||
|
|
||||||
|
def fnt(size, bold):
|
||||||
|
key = (size, bold)
|
||||||
|
if key not in fonts:
|
||||||
|
fonts[key] = ImageFont.truetype(FONT_B if bold else FONT_R, size * SS)
|
||||||
|
return fonts[key]
|
||||||
|
|
||||||
|
for kind, a in ops:
|
||||||
|
if kind == "rect":
|
||||||
|
x, y, w, h, fill, outline, lw, r, dashed = a
|
||||||
|
box = [x * SS, y * SS, (x + w) * SS, (y + h) * SS]
|
||||||
|
if dashed:
|
||||||
|
d.rounded_rectangle(box, radius=r * SS, fill=fill)
|
||||||
|
for a1, b1 in [((x, y), (x + w, y)), ((x + w, y), (x + w, y + h)),
|
||||||
|
((x + w, y + h), (x, y + h)), ((x, y + h), (x, y))]:
|
||||||
|
for s in dash_segments((a1[0] * SS, a1[1] * SS), (b1[0] * SS, b1[1] * SS),
|
||||||
|
on=8 * SS, off=6 * SS):
|
||||||
|
d.line([s[0], s[1]], fill=outline, width=max(1, lw * SS))
|
||||||
|
else:
|
||||||
|
d.rounded_rectangle(box, radius=r * SS, fill=fill,
|
||||||
|
outline=outline if lw else None,
|
||||||
|
width=max(1, lw * SS) if lw else 0)
|
||||||
|
elif kind == "text":
|
||||||
|
x, y, s, size, color, bold, anchor, vertical = a
|
||||||
|
f = fnt(size, bold)
|
||||||
|
if vertical:
|
||||||
|
bbox = f.getbbox(s)
|
||||||
|
tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
||||||
|
tmp = Image.new("RGBA", (tw + 4, th + 4), (0, 0, 0, 0))
|
||||||
|
ImageDraw.Draw(tmp).text((-bbox[0] + 2, -bbox[1] + 2), s, font=f, fill=color)
|
||||||
|
tmp = tmp.rotate(90, expand=True)
|
||||||
|
img.paste(tmp, (int(x * SS - tmp.width / 2), int(y * SS - tmp.height / 2)), tmp)
|
||||||
|
else:
|
||||||
|
d.text((x * SS, y * SS), s, font=f, fill=color, anchor=anchor)
|
||||||
|
elif kind == "path":
|
||||||
|
pts, color, lw, dash = a
|
||||||
|
draw_polyline(d, [(px * SS, py * SS) for px, py in pts], color,
|
||||||
|
max(1, lw * SS), dash)
|
||||||
|
elif kind == "dot":
|
||||||
|
x, y, r, color = a
|
||||||
|
d.ellipse([(x - r) * SS, (y - r) * SS, (x + r) * SS, (y + r) * SS], fill=color)
|
||||||
|
|
||||||
|
img = img.resize((W, H), Image.LANCZOS)
|
||||||
|
img.save(path)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def render_svg(ops, path):
|
||||||
|
out = [f'<svg xmlns="http://www.w3.org/2000/svg" width="{W}" height="{H}" '
|
||||||
|
f'viewBox="0 0 {W} {H}" font-family="DejaVu Sans, Verdana, sans-serif">',
|
||||||
|
f'<rect width="{W}" height="{H}" fill="#FFFFFF"/>']
|
||||||
|
|
||||||
|
def esc(s):
|
||||||
|
return s.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||||
|
|
||||||
|
for kind, a in ops:
|
||||||
|
if kind == "rect":
|
||||||
|
x, y, w, h, fill, outline, lw, r, dashed = a
|
||||||
|
stroke = f' stroke="{outline}" stroke-width="{lw}"' if lw else ""
|
||||||
|
dash = ' stroke-dasharray="9 6"' if dashed else ""
|
||||||
|
out.append(f'<rect x="{x}" y="{y}" width="{w}" height="{h}" rx="{r}" '
|
||||||
|
f'fill="{fill}"{stroke}{dash}/>')
|
||||||
|
elif kind == "text":
|
||||||
|
x, y, s, size, color, bold, anchor, vertical = a
|
||||||
|
weight = ' font-weight="bold"' if bold else ""
|
||||||
|
if vertical:
|
||||||
|
ta = ' text-anchor="middle" dominant-baseline="middle"'
|
||||||
|
transform = f' transform="rotate(-90 {x} {y})"'
|
||||||
|
else:
|
||||||
|
ta = (' text-anchor="middle" dominant-baseline="hanging"' if anchor == "mt"
|
||||||
|
else ' dominant-baseline="hanging"')
|
||||||
|
transform = ""
|
||||||
|
out.append(f'<text x="{x}" y="{y}" font-size="{size}" fill="{color}"'
|
||||||
|
f'{weight}{ta}{transform}>{esc(s)}</text>')
|
||||||
|
elif kind == "path":
|
||||||
|
pts, color, lw, dash = a
|
||||||
|
d = " ".join(("M" if i == 0 else "L") + f"{p[0]} {p[1]}"
|
||||||
|
for i, p in enumerate(pts))
|
||||||
|
dash = ' stroke-dasharray="9 6"' if dash else ""
|
||||||
|
out.append(f'<path d="{d}" fill="none" stroke="{color}" '
|
||||||
|
f'stroke-width="{lw}"{dash} stroke-linejoin="round"/>')
|
||||||
|
(tx, ty), (px, py) = pts[-1], pts[-2]
|
||||||
|
dx, dy = tx - px, ty - py
|
||||||
|
n = (dx * dx + dy * dy) ** 0.5 or 1
|
||||||
|
ux, uy = dx / n, dy / n
|
||||||
|
s_ = 9
|
||||||
|
bx, by = tx - ux * s_, ty - uy * s_
|
||||||
|
ox, oy = -uy * s_ * 0.42, ux * s_ * 0.42
|
||||||
|
out.append(f'<polygon points="{tx},{ty} {bx + ox},{by + oy} '
|
||||||
|
f'{bx - ox},{by - oy}" fill="{color}"/>')
|
||||||
|
elif kind == "dot":
|
||||||
|
x, y, r, color = a
|
||||||
|
out.append(f'<circle cx="{x}" cy="{y}" r="{r}" fill="{color}"/>')
|
||||||
|
|
||||||
|
out.append("</svg>")
|
||||||
|
with open(path, "w", encoding="utf-8") as fh:
|
||||||
|
fh.write("\n".join(out))
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Файлы кладём рядом со скриптом: docs/diagrams/
|
||||||
|
base = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
ops = build()
|
||||||
|
print(render_png(ops, os.path.join(base, "infra_services_diagram.png")))
|
||||||
|
print(render_svg(ops, os.path.join(base, "infra_services_diagram.svg")))
|
||||||
@@ -56,8 +56,10 @@ nav:
|
|||||||
- Руководства:
|
- Руководства:
|
||||||
- Быстрый старт: 30_registry/guides/getting-started.md
|
- Быстрый старт: 30_registry/guides/getting-started.md
|
||||||
- Основы Terraform: 30_registry/guides/terraform-basics.md
|
- Основы Terraform: 30_registry/guides/terraform-basics.md
|
||||||
|
- Как работает провайдер (отличия от Terraform): 30_registry/guides/provider-behavior.md
|
||||||
- Структура манифестов: 30_registry/guides/terraform-structure.md
|
- Структура манифестов: 30_registry/guides/terraform-structure.md
|
||||||
- Глоссарий: 30_registry/guides/glossary.md
|
- Глоссарий: 30_registry/guides/glossary.md
|
||||||
- Проверенные примеры:
|
- Проверенные примеры:
|
||||||
- PostgreSQL: curated/postgres/pg_user_db.md
|
- PostgreSQL: curated/postgres/pg_user_db.md
|
||||||
|
- Пайплайн vDC → Edge → IP → SNAT → vApp → ВМ → Штурвал: curated/pipeline/vdc_edge_ip_snat.md
|
||||||
- Ресурсы-модификаторы (IP организации, SNAT): curated/modifiers/org_ip_and_snat.md
|
- Ресурсы-модификаторы (IP организации, SNAT): curated/modifiers/org_ip_and_snat.md
|
||||||
|
|||||||
@@ -8,11 +8,28 @@ package jsonutil
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// uuidAnywhereRegex — UUID-подобная подстрока внутри произвольного текста
|
||||||
|
// (например, внутри JSON-строки). Регистр не учитывается.
|
||||||
|
var uuidAnywhereRegex = regexp.MustCompile(`(?i)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`)
|
||||||
|
|
||||||
|
// LowercaseUUIDsInText приводит все UUID-подобные подстроки к lowercase.
|
||||||
|
//
|
||||||
|
// Зачем: облако отдаёт один и тот же UUID то в lowercase (vdcUid), то в UPPERCASE
|
||||||
|
// (nsxtUid у кластера Штурвала), а пользователь может написать в config любой
|
||||||
|
// регистр. Сравнение plan vs actual обязано игнорировать регистр UUID.
|
||||||
|
func LowercaseUUIDsInText(value string) string {
|
||||||
|
if !uuidAnywhereRegex.MatchString(value) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return uuidAnywhereRegex.ReplaceAllStringFunc(value, strings.ToLower)
|
||||||
|
}
|
||||||
|
|
||||||
// JSONStringsEquivalent compares JSON strings ignoring whitespace and map key order.
|
// JSONStringsEquivalent compares JSON strings ignoring whitespace and map key order.
|
||||||
func JSONStringsEquivalent(a, b string) bool {
|
func JSONStringsEquivalent(a, b string) bool {
|
||||||
na, oka := normalizeJSONIfPossible(a)
|
na, oka := normalizeJSONIfPossible(a)
|
||||||
@@ -107,7 +124,7 @@ func normalizeJSONScalarsToStrings(v interface{}) interface{} {
|
|||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
case string:
|
case string:
|
||||||
return val
|
return LowercaseUUIDsInText(val)
|
||||||
case float64:
|
case float64:
|
||||||
return strconv.FormatFloat(val, 'g', -1, 64)
|
return strconv.FormatFloat(val, 'g', -1, 64)
|
||||||
case bool:
|
case bool:
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package jsonutil
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// FIX(uuid-case) регрессия: облако отдаёт один и тот же UUID то в lowercase (vdcUid),
|
||||||
|
// то в UPPERCASE (nsxtUid у кластера Штурвала). Сравнение plan vs actual должно
|
||||||
|
// игнорировать регистр UUID — и для одиночного значения, и внутри JSON.
|
||||||
|
func TestJSONStringsEquivalentUUIDCase(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
a string
|
||||||
|
b string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "uuid внутри JSON: разный регистр — эквивалентно",
|
||||||
|
a: `{"nsxtUid":"2c37fed1-e8f8-4a84-8434-7851c7c8b5d6","clusterName":"shturval-dev-00"}`,
|
||||||
|
b: `{"clusterName":"shturval-dev-00","nsxtUid":"2C37FED1-E8F8-4A84-8434-7851C7C8B5D6"}`,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "вложенный JSON и смешанный регистр",
|
||||||
|
a: `{"a":{"b":["2C37FED1-E8F8-4A84-8434-7851C7C8B5D6"]}}`,
|
||||||
|
b: `{"a":{"b":["2c37fed1-e8f8-4a84-8434-7851c7c8b5d6"]}}`,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "разные UUID — не эквивалентно",
|
||||||
|
a: `{"nsxtUid":"2c37fed1-e8f8-4a84-8434-7851c7c8b5d6"}`,
|
||||||
|
b: `{"nsxtUid":"86a01033-adac-424f-8296-aa10a001d6ed"}`,
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "разные значения не-UUID — не эквивалентно",
|
||||||
|
a: `{"clusterName":"shturval-dev-00"}`,
|
||||||
|
b: `{"clusterName":"shturval-dev-01"}`,
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "числа и bool остаются различимыми",
|
||||||
|
a: `{"exVip":true,"count":3}`,
|
||||||
|
b: `{"exVip":true,"count":"3"}`,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if got := JSONStringsEquivalent(tc.a, tc.b); got != tc.want {
|
||||||
|
t.Fatalf("JSONStringsEquivalent(%s, %s) = %v, want %v", tc.a, tc.b, got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLowercaseUUIDsInText(t *testing.T) {
|
||||||
|
in := `{"nsxtUid":"2C37FED1-E8F8-4A84-8434-7851C7C8B5D6","name":"internet-ipv4-v1"}`
|
||||||
|
want := `{"nsxtUid":"2c37fed1-e8f8-4a84-8434-7851c7c8b5d6","name":"internet-ipv4-v1"}`
|
||||||
|
if got := LowercaseUUIDsInText(in); got != want {
|
||||||
|
t.Fatalf("LowercaseUUIDsInText() = %s, want %s", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Текст без UUID не меняется (в т.ч. имя ipSpace с дефисами и цифрами).
|
||||||
|
plain := `{"ipSpaceName":"internet-ipv4-v1","qosProfile":"QoS-100Mbit"}`
|
||||||
|
if got := LowercaseUUIDsInText(plain); got != plain {
|
||||||
|
t.Fatalf("LowercaseUUIDsInText() изменил текст без UUID: %s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,5 +2,15 @@ package provider
|
|||||||
|
|
||||||
import _ "embed"
|
import _ "embed"
|
||||||
|
|
||||||
|
// operation_timeouts.json — ДЕФОЛТНЫЙ конфиг таймаутов операций для сборки из
|
||||||
|
// исходников (без него падает go build / go test в provider/).
|
||||||
|
//
|
||||||
|
// Это НЕ сгенерированный артефакт и он СОЗНАТЕЛЬНО лежит в git:
|
||||||
|
// при релизе скрипт TOOLS/scripts/03_build_and_upload_provider.sh копирует
|
||||||
|
// провайдер во временный каталог и ПОДМЕНЯЕТ там этот файл профильным
|
||||||
|
// (TOOLS/config/<profile>/operation_timeouts.json) — рабочее дерево не меняется.
|
||||||
|
//
|
||||||
|
// Менять значения для релиза нужно в TOOLS/config/<profile>/, а не здесь.
|
||||||
|
//
|
||||||
//go:embed operation_timeouts.json
|
//go:embed operation_timeouts.json
|
||||||
var operationTimeoutsConfigEmbedded []byte
|
var operationTimeoutsConfigEmbedded []byte
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import (
|
|||||||
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
|
||||||
|
"terraform-provider-nubes/internal/core/jsonutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
// jsonNormalizePlanModifier реализует planmodifier.String.
|
// jsonNormalizePlanModifier реализует planmodifier.String.
|
||||||
@@ -49,5 +51,7 @@ func (m jsonNormalizePlanModifier) PlanModifyString(_ context.Context, req planm
|
|||||||
// Невалидный JSON — оставляем значение без изменений, не прерываем планирование.
|
// Невалидный JSON — оставляем значение без изменений, не прерываем планирование.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp.PlanValue = types.StringValue(buf.String())
|
// FIX(uuid-case): UUID-подстроки — к lowercase, чтобы план не расходился с
|
||||||
|
// состоянием только из-за регистра UUID (облако отдаёт их в разных регистрах).
|
||||||
|
resp.PlanValue = types.StringValue(jsonutil.LowercaseUUIDsInText(buf.String()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ type OrgIpAllocationResource struct {
|
|||||||
|
|
||||||
type OrgIpAllocationModel struct {
|
type OrgIpAllocationModel struct {
|
||||||
ID types.String `tfsdk:"id"`
|
ID types.String `tfsdk:"id"`
|
||||||
OrgUID types.String `tfsdk:"org_uid"`
|
Organization types.String `tfsdk:"organization"`
|
||||||
VIPConfigure types.String `tfsdk:"vip_configure"`
|
VIPConfigure types.String `tfsdk:"vip_configure"`
|
||||||
KeepOnDestroy types.Bool `tfsdk:"keep_on_destroy"`
|
KeepOnDestroy types.Bool `tfsdk:"keep_on_destroy"`
|
||||||
}
|
}
|
||||||
@@ -59,7 +59,7 @@ func (r *OrgIpAllocationResource) Metadata(ctx context.Context, req resource.Met
|
|||||||
func (r *OrgIpAllocationResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
|
func (r *OrgIpAllocationResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||||
resp.Schema = schema.Schema{
|
resp.Schema = schema.Schema{
|
||||||
MarkdownDescription: "Аллокация внешних IP (vIPConfigure) на существующей организации Cloud Director. " +
|
MarkdownDescription: "Аллокация внешних IP (vIPConfigure) на существующей организации Cloud Director. " +
|
||||||
"Организация создаётся вручную в ЛК, ресурс адресует её по `org_uid`. " +
|
"Организация создаётся вручную в ЛК, в конфиге указывается её имя или UUID. " +
|
||||||
"Операция имеет replace-семантику: массив перезаписывается целиком.",
|
"Операция имеет replace-семантику: массив перезаписывается целиком.",
|
||||||
Attributes: map[string]schema.Attribute{
|
Attributes: map[string]schema.Attribute{
|
||||||
"id": schema.StringAttribute{
|
"id": schema.StringAttribute{
|
||||||
@@ -68,9 +68,10 @@ func (r *OrgIpAllocationResource) Schema(ctx context.Context, req resource.Schem
|
|||||||
stringplanmodifier.UseStateForUnknown(),
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"org_uid": schema.StringAttribute{
|
"organization": schema.StringAttribute{
|
||||||
Required: true,
|
Required: true,
|
||||||
MarkdownDescription: "UUID существующей услуги «Организация в Cloud Director».",
|
MarkdownDescription: "Организация, на которой выделяются внешние IP: имя из ЛК (например `organ`) " +
|
||||||
|
"или её UUID.",
|
||||||
PlanModifiers: []planmodifier.String{
|
PlanModifiers: []planmodifier.String{
|
||||||
stringplanmodifier.RequiresReplace(),
|
stringplanmodifier.RequiresReplace(),
|
||||||
},
|
},
|
||||||
@@ -79,11 +80,8 @@ func (r *OrgIpAllocationResource) Schema(ctx context.Context, req resource.Schem
|
|||||||
Required: true,
|
Required: true,
|
||||||
MarkdownDescription: "JSON-массив аллокаций: `[{\"name\":\"internet-ipv4-v1\",\"count\":\"3\"}]`. " +
|
MarkdownDescription: "JSON-массив аллокаций: `[{\"name\":\"internet-ipv4-v1\",\"count\":\"3\"}]`. " +
|
||||||
"Значение перезаписывает текущую аллокацию целиком. `count` — строка. " +
|
"Значение перезаписывает текущую аллокацию целиком. `count` — строка. " +
|
||||||
"Порядок ключей и форматирование не важны — значение канонизируется при планировании. " +
|
"Порядок ключей и форматирование не важны (сравнение смысловое). " +
|
||||||
"Снять аллокацию (`[]`) через этот атрибут **нельзя** — только удалением ресурса (`destroy`).",
|
"Снять аллокацию (`[]`) через этот атрибут **нельзя** — только удалением ресурса (`destroy`).",
|
||||||
PlanModifiers: []planmodifier.String{
|
|
||||||
vipConfigureCanonical(),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"keep_on_destroy": schema.BoolAttribute{
|
"keep_on_destroy": schema.BoolAttribute{
|
||||||
Optional: true,
|
Optional: true,
|
||||||
@@ -103,12 +101,18 @@ func (r *OrgIpAllocationResource) Create(ctx context.Context, req resource.Creat
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.applyAllocation(ctx, plan.OrgUID, plan.VIPConfigure); err != nil {
|
orgUID, err := r.resolveOrganizationUID(ctx, plan.Organization)
|
||||||
|
if err != nil {
|
||||||
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
plan.ID = types.StringValue(strings.TrimSpace(plan.OrgUID.ValueString()))
|
if err := r.applyAllocation(ctx, orgUID, plan.VIPConfigure); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
plan.ID = types.StringValue(orgUID)
|
||||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,12 +123,18 @@ func (r *OrgIpAllocationResource) Update(ctx context.Context, req resource.Updat
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.applyAllocation(ctx, plan.OrgUID, plan.VIPConfigure); err != nil {
|
orgUID, err := r.resolveOrganizationUID(ctx, plan.Organization)
|
||||||
|
if err != nil {
|
||||||
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
plan.ID = types.StringValue(strings.TrimSpace(plan.OrgUID.ValueString()))
|
if err := r.applyAllocation(ctx, orgUID, plan.VIPConfigure); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
plan.ID = types.StringValue(orgUID)
|
||||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,8 +145,13 @@ func (r *OrgIpAllocationResource) Read(ctx context.Context, req resource.ReadReq
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
orgUID := strings.TrimSpace(state.OrgUID.ValueString())
|
if strings.TrimSpace(state.Organization.ValueString()) == "" || r.client == nil {
|
||||||
if orgUID == "" || r.client == nil {
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
orgUID, err := r.resolveOrganizationUID(ctx, state.Organization)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,18 +172,19 @@ func (r *OrgIpAllocationResource) Read(ctx context.Context, req resource.ReadReq
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ВАЖНО: в Required-атрибут нельзя писать null — после apply state обязан совпасть с планом,
|
// Атрибут принадлежит пользователю: НЕ переписываем его, если смысл совпал — иначе Terraform
|
||||||
// иначе Terraform вернёт "Provider produced inconsistent result after apply". Если платформа
|
// увидит расхождение config vs state и покажет ложный дрейф (jsonencode отдаёт ключи по алфавиту).
|
||||||
// ещё не вернула значение (у свежей орги `vIPConfigure: [{}]`), оставляем текущее значение state.
|
// Писать null в Required-атрибут тоже нельзя (это даёт "Provider produced inconsistent result").
|
||||||
raw, ok := live["vIPConfigure"]
|
raw, ok := live["vIPConfigure"]
|
||||||
if ok {
|
if ok {
|
||||||
items, parseErr := parseVipConfigure(raw)
|
liveItems, parseErr := parseVipConfigure(raw)
|
||||||
if parseErr != nil {
|
if parseErr != nil {
|
||||||
resp.Diagnostics.AddError("Ошибка чтения состояния", parseErr.Error())
|
resp.Diagnostics.AddError("Ошибка чтения состояния", parseErr.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(items) > 0 {
|
stateItems, _ := parseVipConfigure(state.VIPConfigure.ValueString())
|
||||||
state.VIPConfigure = types.StringValue(formatVipConfigure(items))
|
if !vipAllocationsEqual(liveItems, stateItems) {
|
||||||
|
state.VIPConfigure = types.StringValue(formatVipConfigure(liveItems))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,8 +199,13 @@ func (r *OrgIpAllocationResource) Delete(ctx context.Context, req resource.Delet
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
orgUID := strings.TrimSpace(state.OrgUID.ValueString())
|
if strings.TrimSpace(state.Organization.ValueString()) == "" || r.client == nil {
|
||||||
if orgUID == "" || r.client == nil {
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
orgUID, err := r.resolveOrganizationUID(ctx, state.Organization)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,14 +290,37 @@ func (r *OrgIpAllocationResource) Configure(_ context.Context, req resource.Conf
|
|||||||
func (r *OrgIpAllocationResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
func (r *OrgIpAllocationResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||||
uid := strings.TrimSpace(req.ID)
|
uid := strings.TrimSpace(req.ID)
|
||||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), uid)...)
|
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), uid)...)
|
||||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("org_uid"), uid)...)
|
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("organization"), uid)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveOrganizationUID принимает имя организации из ЛК или её UUID и возвращает UUID.
|
||||||
|
// Резолв делает клиент — тем же путём, что сгенерированный nubes_vc_vdc
|
||||||
|
// (core.ResolveRefSvcParamValue, сравн. 21_vc_vdc_resource.go).
|
||||||
|
func (r *OrgIpAllocationResource) resolveOrganizationUID(ctx context.Context, organization types.String) (string, error) {
|
||||||
|
if r.client == nil {
|
||||||
|
return "", fmt.Errorf("клиент не инициализирован")
|
||||||
|
}
|
||||||
|
raw := strings.TrimSpace(organization.ValueString())
|
||||||
|
if raw == "" {
|
||||||
|
return "", fmt.Errorf("organization обязателен")
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved, err := r.client.ResolveRefSvcParamValue(ctx, 19, raw)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("не удалось определить организацию %q: %w", raw, err)
|
||||||
|
}
|
||||||
|
resolved = strings.TrimSpace(resolved)
|
||||||
|
if resolved == "" {
|
||||||
|
return "", fmt.Errorf("организация %q не найдена", raw)
|
||||||
|
}
|
||||||
|
return resolved, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// applyAllocation отправляет modify с массивом vIPConfigure целиком.
|
// applyAllocation отправляет modify с массивом vIPConfigure целиком.
|
||||||
func (r *OrgIpAllocationResource) applyAllocation(ctx context.Context, orgUID types.String, vipConfigure types.String) error {
|
func (r *OrgIpAllocationResource) applyAllocation(ctx context.Context, orgUID string, vipConfigure types.String) error {
|
||||||
uid := strings.TrimSpace(orgUID.ValueString())
|
uid := strings.TrimSpace(orgUID)
|
||||||
if uid == "" {
|
if uid == "" {
|
||||||
return fmt.Errorf("org_uid обязателен")
|
return fmt.Errorf("organization обязателен")
|
||||||
}
|
}
|
||||||
if r.client == nil {
|
if r.client == nil {
|
||||||
return fmt.Errorf("клиент не инициализирован")
|
return fmt.Errorf("клиент не инициализирован")
|
||||||
@@ -349,36 +393,21 @@ func formatVipConfigure(items []vipAllocation) string {
|
|||||||
return "[" + strings.Join(parts, ",") + "]"
|
return "[" + strings.Join(parts, ",") + "]"
|
||||||
}
|
}
|
||||||
|
|
||||||
// canonicalVipConfigure приводит пользовательский ввод к каноническому виду.
|
// vipAllocationsEqual сравнивает аллокации по СМЫСЛУ: порядок элементов и формат не важны.
|
||||||
// Нужен потому, что Terraform `jsonencode` сортирует ключи по алфавиту (`count` раньше `name`),
|
// Имена ipSpace в рамках организации уникальны, поэтому сравнение идёт по имени.
|
||||||
// а API/HAR дают порядок `name,count`: без канонизации план и Read расходятся → вечный diff.
|
func vipAllocationsEqual(a, b []vipAllocation) bool {
|
||||||
// Невалидный JSON возвращаем как есть — содержательную ошибку выдаст apply.
|
if len(a) != len(b) {
|
||||||
func canonicalVipConfigure(raw string) string {
|
return false
|
||||||
items, err := parseVipConfigure(raw)
|
|
||||||
if err != nil {
|
|
||||||
return raw
|
|
||||||
}
|
}
|
||||||
return formatVipConfigure(items)
|
byName := make(map[string]string, len(b))
|
||||||
}
|
for _, item := range b {
|
||||||
|
byName[item.Name] = item.Count
|
||||||
// vipConfigureCanonical — plan modifier для атрибута vip_configure.
|
|
||||||
type vipConfigureCanonicalPlanModifier struct{}
|
|
||||||
|
|
||||||
func vipConfigureCanonical() planmodifier.String {
|
|
||||||
return vipConfigureCanonicalPlanModifier{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m vipConfigureCanonicalPlanModifier) Description(_ context.Context) string {
|
|
||||||
return "Приводит JSON-массив vIPConfigure к каноническому виду (чтобы план совпадал с результатом Read)."
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m vipConfigureCanonicalPlanModifier) MarkdownDescription(ctx context.Context) string {
|
|
||||||
return m.Description(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m vipConfigureCanonicalPlanModifier) PlanModifyString(_ context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) {
|
|
||||||
if req.PlanValue.IsNull() || req.PlanValue.IsUnknown() {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
resp.PlanValue = types.StringValue(canonicalVipConfigure(req.PlanValue.ValueString()))
|
for _, item := range a {
|
||||||
|
count, ok := byName[item.Name]
|
||||||
|
if !ok || count != item.Count {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,32 +51,36 @@ func TestFormatVipConfigure_Canonical(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Канонизация пользовательского ввода: terraform jsonencode сортирует ключи по алфавиту
|
// Сравнение смысловое: `jsonencode` сортирует ключи по алфавиту (count раньше name),
|
||||||
// (count раньше name), а канон у нас — name,count. Без канонизации план ≠ state → вечный diff
|
// но для нас это то же самое значение — переписывать state нельзя (иначе ложный дрейф).
|
||||||
// (баг воспроизведён через `terraform console`, см. NOTES/20_prompts/prompt_for_opus_review_modify_resources_2026-09-24.md).
|
func TestVipAllocationsEqual_OrderInsensitive(t *testing.T) {
|
||||||
func TestCanonicalVipConfigure_NormalizesJsonencodeForm(t *testing.T) {
|
a, err := parseVipConfigure(`[{"name":"internet-ipv4-v1","count":"3"}]`)
|
||||||
raw := `[{"count":"3","name":"internet-ipv4-v1"}]` // так отдаёт jsonencode
|
if err != nil {
|
||||||
want := `[{"name":"internet-ipv4-v1","count":"3"}]`
|
t.Fatalf("неожиданная ошибка: %v", err)
|
||||||
if got := canonicalVipConfigure(raw); got != want {
|
}
|
||||||
t.Fatalf("получено %q, ожидалось %q", got, want)
|
b, err := parseVipConfigure(`[{"count":"3","name":"internet-ipv4-v1"}]`) // так отдаёт jsonencode
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("неожиданная ошибка: %v", err)
|
||||||
|
}
|
||||||
|
if !vipAllocationsEqual(a, b) {
|
||||||
|
t.Fatal("значения должны считаться равными несмотря на порядок ключей")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCanonicalVipConfigure_CompactsAndDropsEmptyElements(t *testing.T) {
|
func TestVipAllocationsEqual_Differences(t *testing.T) {
|
||||||
raw := `[ { "count" : "4" , "name" : "internet-ipv4-v1" }, {} ]`
|
base, _ := parseVipConfigure(`[{"name":"internet-ipv4-v1","count":"3"}]`)
|
||||||
want := `[{"name":"internet-ipv4-v1","count":"4"}]`
|
otherCount, _ := parseVipConfigure(`[{"name":"internet-ipv4-v1","count":"2"}]`)
|
||||||
if got := canonicalVipConfigure(raw); got != want {
|
otherName, _ := parseVipConfigure(`[{"name":"internet-antiddos-v1","count":"3"}]`)
|
||||||
t.Fatalf("получено %q, ожидалось %q", got, want)
|
empty, _ := parseVipConfigure(`[{}]`)
|
||||||
}
|
|
||||||
if got := canonicalVipConfigure(`[{}]`); got != "[]" {
|
|
||||||
t.Fatalf("для [{}] ожидалось \"[]\", получено %q", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCanonicalVipConfigure_InvalidJSONLeftAsIs(t *testing.T) {
|
if vipAllocationsEqual(base, otherCount) {
|
||||||
raw := `{not json`
|
t.Fatal("разный count должен считаться разными значениями")
|
||||||
if got := canonicalVipConfigure(raw); got != raw {
|
}
|
||||||
t.Fatalf("невалидный JSON должен остаться как есть: %q → %q", raw, got)
|
if vipAllocationsEqual(base, otherName) {
|
||||||
|
t.Fatal("разное имя ipSpace должно считаться разными значениями")
|
||||||
|
}
|
||||||
|
if vipAllocationsEqual(base, empty) {
|
||||||
|
t.Fatal("пустая аллокация должна отличаться от непустой")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package resources_core
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// FIX(uuid-case): регрессия today's bug — adopt приостановленного инстанса Штурвала
|
||||||
|
// падал с "required params mismatch ... startupConfiguration", потому что UUID
|
||||||
|
// внутри JSON сравнивался с учётом регистра (plan — lowercase из config,
|
||||||
|
// actual — UPPERCASE от API). Теперь UUID внутри JSON эквивалентны.
|
||||||
|
func TestParamsEquivalentUUIDCaseInsideJSON(t *testing.T) {
|
||||||
|
plan := `{"nsxtUid":"2c37fed1-e8f8-4a84-8434-7851c7c8b5d6","vdcUid":"d0937335-276b-475b-baa4-d8e6d16bad51","clusterName":"shturval-dev-00","exVip":true}`
|
||||||
|
actual := `{"clusterName":"shturval-dev-00","exVip":true,"nsxtUid":"2C37FED1-E8F8-4A84-8434-7851C7C8B5D6","vdcUid":"d0937335-276b-475b-baa4-d8e6d16bad51"}`
|
||||||
|
|
||||||
|
if !paramsEquivalent(plan, actual) {
|
||||||
|
t.Fatalf("paramsEquivalent() = false, ожидалось true: регистр UUID внутри JSON не должен влиять")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Реально другой nsxtUid — по-прежнему не эквивалентно.
|
||||||
|
other := `{"clusterName":"shturval-dev-00","exVip":true,"nsxtUid":"86a01033-adac-424f-8296-aa10a001d6ed","vdcUid":"d0937335-276b-475b-baa4-d8e6d16bad51"}`
|
||||||
|
if paramsEquivalent(plan, other) {
|
||||||
|
t.Fatalf("paramsEquivalent() = true для другого nsxtUid — неверно")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user