chore: remove 18 junk files + stale root internal/ (old provider code)

Deleted: check_ops.py, s.sh, instance_fca3f912.json, name_check.txt,
prompt_for_ds_pro.md, prompt_for_opus48.md, state_keys_*.json,
state_keys_progress.txt, state_vault_*.json, svcs.json, services.*,
REPO_CONTENTS.md, REPO_INVENTORY.md, 01_generate_yamls.sh (duplicate),
internal/ (old provider, migrated to universal_rebuild/)
This commit is contained in:
“Naeel”
2026-07-05 09:37:40 +04:00
parent 3a003b28b3
commit d9673f7b17
42 changed files with 0 additions and 13557 deletions
-83
View File
@@ -1,83 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="${ROOT_DIR:-${SCRIPT_DIR}}"
PROVIDER_DIR="${ROOT_DIR}/universal_rebuild"
SERVICES_FILE="${SERVICES_FILE:-${ROOT_DIR}/devops/config/services_list.txt}"
TOKEN_FILE="${TOKEN_FILE:-}"
NUBES_API_TOKEN="${NUBES_API_TOKEN:-}"
if [[ -z "$NUBES_API_TOKEN" ]]; then
if [[ -z "$TOKEN_FILE" ]]; then
TOKEN_FILE=$(ls -t /home/naeel/terra/*.token 2>/dev/null | head -n 1 || true)
fi
if [[ -n "$TOKEN_FILE" && -f "$TOKEN_FILE" ]]; then
NUBES_API_TOKEN=$(cat "$TOKEN_FILE")
fi
fi
if [[ -z "$NUBES_API_TOKEN" ]]; then
echo "Error: NUBES_API_TOKEN or TOKEN_FILE is required" >&2
exit 2
fi
if [[ ! -f "$SERVICES_FILE" ]]; then
echo "Error: services file not found: $SERVICES_FILE" >&2
exit 2
fi
API_ENDPOINT="${NUBES_API_ENDPOINT:-https://deck-api.ngcloud.ru/api/v1/index.cfm}"
while IFS= read -r sid; do
sid="${sid//[$'\t'\r' ']}"
if [[ -z "$sid" ]]; then
continue
fi
if [[ "$sid" == \#* ]]; then
continue
fi
svc_name=$(python3 - <<PY
import json
import urllib.parse
import urllib.request
sid = "${sid}"
endpoint = "${API_ENDPOINT}"
token = "${NUBES_API_TOKEN}"
# Auto-detect: "index.cfm" → legacy proxy, otherwise → REST gateway.
if "index.cfm" in endpoint:
params = urllib.parse.urlencode({"endpoint": f"/services/{sid}"})
url = f"{endpoint}?{params}"
else:
url = f"{endpoint}/services/{sid}"
req = urllib.request.Request(url)
if token:
req.add_header("Authorization", f"Bearer {token}")
with urllib.request.urlopen(req) as resp:
data = json.loads(resp.read().decode("utf-8"))
svc = data.get("svc", {})
name = svc.get("svcShort") or svc.get("name") or svc.get("title") or f"service_{sid}"
print(name)
PY
)
echo "Generating YAML for ${sid} (${svc_name})"
(
cd "$PROVIDER_DIR"
NUBES_API_TOKEN="$NUBES_API_TOKEN" \
NUBES_SERVICE_ID="$sid" \
NUBES_SERVICE_NAME="$svc_name" \
NUBES_OUTPUT="${PROVIDER_DIR}/resources_yaml/${svc_name}.yaml" \
NUBES_INSTRUCTION_OUTPUT="/dev/null" \
go run ./tools/service_params_gen/main.go
)
done < "$SERVICES_FILE"
echo "Done. YAML files are in ${PROVIDER_DIR}/resources_yaml"
-102
View File
@@ -1,102 +0,0 @@
# Repository Contents
Updated: 2026-03-01
Purpose: single-file map of repository structure for quick onboarding and orientation.
## How to use this file
- Read this file at the start of a new chat/session to understand repo structure quickly.
- Re-read this file when context is unclear, requirements conflict, or there are navigation uncertainties.
- For active build/deploy work, treat `devops/` as the primary operational source of truth.
## Mandatory policy for new chats
- Для `instance`-ресурсов с операциями `suspend/resume` обязательный источник правил: `docs/60_strategy/provider_philosophy.md` (разделы 7-9).
- Применять только каноничные флаги: `adopt_existing_on_create` (default `false`) и `suspend_on_destroy` (default `true`).
- Логика apply/destroy/modify должна следовать status-matrix из стратегии (`deleted`, `suspend`, `running`, `not created`, `creating`).
- Упоминания `resume_if_exists` и `delete_mode` считать legacy и не использовать как норматив для новой реализации.
## Build/Deploy priority
- Основной источник инструкций для сборки и деплоя: `devops/`.
- При любых задачах публикации, генерации, сборки и релизов сначала проверять:
- `devops/README.md`
- `devops/ARCHITECTURE.md`
- `devops/01_generate_yamls.sh`
- `devops/02_generate_resources_and_docs_template_v2.sh`
- `devops/03_build_and_upload_provider.sh`
---
## Top-level summary
- `docs/`**Единый корень документации** (overview, discovery, registry, analysis, history, strategy, API).
- `tools/har/` — Scripts used to analyze HAR files and extract parameters/stages.
- `har/` — Collected HAR files captured from browser/API traffic (raw evidence files).
- `artifacts/` — JSON artifacts produced by live API queries and intermediate outputs.
- `internal/provider/` — Terraform provider implementation (resources and data-sources).
- `operator/`, `k8s/` — Operator manifests and Kubernetes overlays.
- `examples/` & `tests/` — Usage examples and test scenarios for the provider.
---
## Docs publication (CI) & Cloud S3 Storage
- **Cloud Storage:** The project uses Nubes Cloud S3 (`s3.msk-1.ngcloud.ru`) with `S3_*` variables.
- **Infrastructure:** Storage is external S3; Kubernetes resources are S3-only.
- **Docs Location:** Published static HTML is stored in the `terraform-registry` bucket.
- **Path Pattern:** `/docs/nubes/nubes/<version>/` (Proxy logic handles mapping to S3 keys).
- **Branding:** Documentation uses MkDocs Material with Nubes "Strict" style (Logo-only header, Blue/Indigo palette).
- **CI Secrets:** `S3_ENDPOINT`, `S3_ACCESS_KEY`, `S3_SECRET_KEY`, `S3_BUCKET`, `REGISTRY_HOSTNAME`.
- **History & Discovery:** See `docs/history/09_s3_migration_and_branded_docs.md`, `docs/discovery/s3-storage-and-docs-architecture.md` and `docs/discovery/documentation_build_and_deploy.md` for full context on this transition and build processes.
---
## docs/
- `docs/README.md` — Главный индекс документации (быстрые ссылки на критические темы).
- `docs/00_overview/` — Обзор, архитектура, чеклисты.
- `docs/20_discovery/` — Discovery-материалы по сервисам.
- `docs/30_registry/` — Реестр и ресурсы.
- `docs/40_analysis/` — Анализ, форензика, инциденты.
- `docs/50_history/` — История изменений.
- `docs/60_strategy/` — Стратегия и принципы.
- `docs/70_api/` — API-материалы и дампы.
- `docs/70_api/api_analysis/` — API reverse-engineering outputs and machine-generated API documentation.
## tools/har/
- `analyze_f12_har.py` — Script to parse `f12vmbad.har` and extract operation timelines.
- `analyze_faststart.py` — Analysis helper for `faststart.har`.
- `extract_params.py` — Extracts `instanceOperationCfsParams` bodies and maps them to svc IDs.
- `compare_har.py` — Diffing tool for HAR files (stages/headers summary).
- `check_op.py` / `check_status.py` — Helpers to re-query API operation status.
## har/
- `f12vmbad.har`, `f12vmbad1.har`, `faststart.har`, `edge.har`, `vm.har`, etc. — Raw HAR files. **Large and sensitive**; don't commit new raw HARs to repo without consent. Use `tools/har/*` scripts to parse.
## artifacts/
- `edge_instance.json`, `service_22_detail.json`, `instances_list.json` — Live API JSON dumps captured during investigation. Use them to craft provider schemas.
## internal/provider/
- `*resource.go`, `*_data_source.go` — Provider implementations for resources (VM, VDC, Edge, VApp, Postgres, S3bucket, etc.). Important files:
- `vm_resource.go` — VM lifecycle logic & polling
- `edge_resource.go` — Edge discovery/resolution
- `provider.go` — Provider schema and configuration
## examples/ & tests/
- `examples/*` — Terraform example configs for different deployment patterns (full-stack, infra-only, quick-start).
- `tests/*` — Automated test scenarios (lifecycle, postgres modify tests, s3 tests). Use to validate changes.
## operator/ & k8s/
- Operator code and Kubernetes manifests to run the Registry/Operator. Contains build artifacts in `operator/bin/`.
## scripts/
- `scripts/deploy-dev.sh` — Dev deployment helpers.
---
## Recommended reading order for agents
1. Read `REPO_CONTENTS.md` at chat start (and re-read when unclear context appears).
2. For build/deploy/generation tasks: read `devops/README.md` and relevant scripts in `devops/` first.
3. Read `docs/60_strategy/provider_philosophy.md` (обязательные правила поведения провайдера и агентов; сначала разделы 7-9).
4. Read `docs/README.md` (главный индекс).
5. Read `docs/70_api/api_analysis/API_DOCUMENTATION_SUMMARY.md` to understand API endpoints.
6. Read `docs/20_discovery/*` relevant to the target service (e.g., `edge-service.md`).
7. Inspect `artifacts/*` JSON examples and `har/*` only via `tools/har/*` scripts.
---
If you want, I can add more granular mapping (function names, exported symbols) per source file, or generate a Markdown tree with intra-file links. Tell me the level of detail you want: "brief", "detailed", or "code-level".
-126
View File
@@ -1,126 +0,0 @@
# REPO INVENTORY — 01.07.2026
Полная карта репозитория `/home/naeel/tf_provider`: структура, версии, профили, публикации.
---
## 1. Директории
| Путь | Назначение | Файлы |
|------|-----------|-------|
| `/` | Корень Go-модуля `terraform-provider-mycloud` (Legacy провайдер) | `main.go`, `go.mod`, `go.sum` |
| `internal/` | Исходный код Legacy провайдера (ручные ресурсы) | 25 `.go` файлов |
| `internal/core/` | Ядро Legacy: HTTP-клиент, lookup инстансов, операции | 3 файла |
| `internal/generated/` | Сгенерированные ресурсы болванки (3 файла) | 3 `.go` |
| `internal/provider/` | 12 ручных ресурсов + дата-сорсы | 17 `.go` |
| `internal/registrykeys/` | Пусто | — |
| `universal_rebuild/` | **Universal провайдер** (главный, генерируемый) | Go-модуль |
| `universal_rebuild/internal/core/` | Универсальное ядро: клиент, операции, реф-резолв | 10 файлов |
| `universal_rebuild/internal/provider/` | Provider entrypoint + таймауты | 3 файла |
| `universal_rebuild/internal/resources_core/` | CRUD-логика, state, params | 22 файла |
| `universal_rebuild/internal/resources_gen/` | **Сгенерированные ресурсы** (82 файла) | 82 `.go` |
| `universal_rebuild/resources_yaml/` | **YAML-спеки** (42 файла) — source of truth | 42 `.yaml` |
| `universal_rebuild/tools/` | Инструменты генерации | 5 тулов |
| `universal_rebuild/tools/service_spec_gen/` | API → YAML (основной генератор) | 1 `.go` |
| `universal_rebuild/tools/gen_v2/` | YAML → Go (основной генератор) | 1 `.go` |
| `universal_rebuild/tools/docs_template_gen_v2/` | YAML → Документация | 1 `.go` |
| `universal_rebuild/tools/docs_template_gen/` | Docs gen v1 | 1 `.go` |
| `universal_rebuild/tools/ops_docs_gen/` | Документация операций | 1 `.go` |
| `devops/` | DevOps скрипты и CI | ~20 файлов |
| `devops/profiles/prod/` | **PROD профиль** (версия 2.1.23) | profile.env, services_list, timeouts |
| `devops/profiles/test/` | **TEST профиль** (версия 5.0.55) | profile.env, services_list, timeouts |
| `devops/profiles/dev/` | **DEV профиль** (версия 3.0.1) | profile.env, services_list, timeouts |
| `devops/config/` | Мастер-список сервисов + таймауты | 2 файла |
| `devops/ci/` | GitLab CI пайплайн | 1 файл |
| `docs/` | **MkDocs документация** | ~600+ MD файлов |
| `docs/30_registry/resources/` | **Реестр ресурсов** (документация по каждому сервису) | ~500+ |
| `docs/50_history/` | История разработки (24 спринта) | 24 `.md` |
| `docs/ops/` | Операционные документы (runbook, troubleshooting) | 9 `.md` |
| `cloud-dashboard/` | FastAPI дашборд мониторинга | Python + K8s |
| `HAR/` | HTTP-трассировки API (8 файлов) | 8 `.har` |
| `HISTORY/OPUS/` | История работы с AI-ассистентом Opus | 7 `.md` |
| `PROD_STAND/` | Terraform-конфиги продакшен-стенда | ~20 файлов |
| `TEST_STAND/` | Terraform-конфиги тестового стенда | ~15 файлов |
| `RABBIT/` | Terraform-конфиги RabbitMQ | 2 `.tf` |
| `tools/` | Вспомогательные утилиты | ~15 файлов |
| `secrets/` | Токены и ключи (в gitignore) | 6 файлов |
| `scripts/` | Скрипты деплоя | 5 файлов |
| `charts/` | Пусто (под Helm-чарты) | — |
| `.github/` | GitHub Actions + Copilot инструкции | 4 файла |
---
## 2. Версии — полная матрица
### 2.1. Код
| Компонент | Путь | Версия |
|-----------|------|--------|
| **Universal провайдер** | `universal_rebuild/main.go` | **5.0.56** |
| **Legacy провайдер** | `main.go` | **5.0.52** |
Оба кода указывают на один registry-адрес `terra.k8c.ru/nubes/nubes`.
### 2.2. Профили (devops/profiles/)
| Стенд | Файл | RELEASE_VERSION | PROVIDER_VERSION | DOCS_VERSION |
|-------|------|-----------------|------------------|--------------|
| **PROD** | `prod/profile.env` | **2.1.23** | **2.1.23** | **2.1.23** |
| **TEST** | `test/profile.env` | **5.0.55** | **5.0.55** | **5.0.55** |
| **DEV** | `dev/profile.env` | **3.0.1** | **3.0.1** | **3.0.1** |
### 2.3. Registry (terra.k8c.ru — опубликованное)
| Линейка | Количество | Диапазон | Последняя |
|---------|-----------|----------|-----------|
| **2.x** (Legacy) | 35 версий | 2.0.0 → **2.12.13** | 2.12.13 |
| **5.x** (Universal) | 52 версии | 5.0.1 → **5.0.55** | 5.0.55 |
**Всего на регистре: 87 версий.**
### 2.4. Документация (опубликованная)
| Версия | Статус |
|--------|--------|
| `2.0.0` | ✅ 200 |
| `2.1.2` | ✅ 200 |
| `2.1.23` | ✅ 200 |
| `5.0.1` | ✅ 200 |
| `5.0.51` | ✅ 200 |
| `5.0.55` | ❌ 404 |
| `5.0.56` | ❌ 404 |
**Проблема:** документация для 5.x публиковалась не для всех версий. 5.0.1 и 5.0.51 есть, 5.0.55 — нет.
---
## 3. Что не синхронизировано
### Разрыв между кодом и профилями
| Что | Код | PROD профиль | Registry | Разрыв |
|-----|-----|-------------|----------|--------|
| Universal | 5.0.56 | 5.0.55 (test) | 5.0.55 | +1 |
| Legacy | 5.0.52 | 2.1.23 (prod) | 2.12.13 | -? |
- **Universal код (5.0.56) новее registry-версии (5.0.55).** Наши правки P0-P2 + регенерация 88 ресурсов не опубликованы.
- **PROD профиль (2.1.23) использует Legacy.** Universal (5.x) развёрнут только на TEST.
- **DEV профиль (3.0.1)** — непонятная версия, не совпадает ни с кодом, ни с registry.
### Разрыв между услугами в PROD и TEST
| Файл | PROD | TEST |
|------|------|------|
| `services_list.txt` | 27 активных | 31 активный |
| Отличия | Нет: `vcOrgSaas`, `vcexternalip`, `superset`, `k8sZitiController`, ... | Есть все |
PROD использует меньше сервисов (старый список), TEST — полный.
---
## 4. Итого
1. Universal (5.0.56) — самая свежая версия кода, но **не опубликована**
2. PROD стоит на Legacy 2.1.23 — старом ручном провайдере
3. Документация для 5.x публикуется нерегулярно (дыры)
4. DEV профиль (3.0.1) — не используется, версия ничему не соответствует
-57
View File
@@ -1,57 +0,0 @@
#!/usr/bin/env python3
# Проверка операций на инстансе pg-test-02 (710C1C21)
import json, urllib.request
TFVARS = "/home/naeel/terra/sless/examples/PG_TEST/terraform.tfvars"
INSTANCE_UID = "710C1C21-B9E8-4973-8DE4-EAA796ADDA00"
API = "https://deck-api-test.ngcloud.ru/api/v1/index.cfm"
# Читаем токен из tfvars
token = ""
for line in open(TFVARS):
if "api_token" in line:
token = line.split('"')[1]
break
def get(path):
req = urllib.request.Request(f"{API}{path}", headers={"Authorization": f"Bearer {token}"})
return json.loads(urllib.request.urlopen(req, timeout=10).read())
# Операции на инстансе
print("=== Операции на инстансе ===")
ops = get(f"/instanceOperations?instanceUid={INSTANCE_UID}")
items = ops if isinstance(ops, list) else ops.get("data", [])
for i in items[:10]:
print(f" uid={i.get('uid')} op={i.get('svcOperation')} status={i.get('status')} created={i.get('createdAt')}")
# Статус инстанса
print("\n=== Статус инстанса ===")
inst = get(f"/instances/{INSTANCE_UID}")
print(f" name={inst.get('name') or inst.get('displayName')} state={inst.get('state') or inst.get('instanceState')} status={inst.get('status') or inst.get('instanceStatus')}")
# Все ключи инстанса для диагностики
print(" DEBUG keys:", list(inst.keys())[:15])
# Поиск по имени через search API (как делает провайдер)
print("\n=== Поиск pg-test-02 через search API ===")
search_res = get("/instances?fields=instanceUid,displayName,serviceId&page=1&pageSize=100&search=pg-test-02&isAuxiliary=false&isDeleted=false")
results = search_res if isinstance(search_res, list) else search_res.get("results", search_res.get("data", []))
for i in results:
uid = i.get("instanceUid") or i.get("uid")
print(f" uid={uid} name={i.get('displayName') or i.get('name')} svcId={i.get('serviceId')} state={i.get('state')}")
# Пагинированный список всех инстансов (первая страница)
print("\n=== Все инстансы (страница 1, size=100) ===")
all_inst = get("/instances?page=1&size=100")
all_items = all_inst if isinstance(all_inst, list) else all_inst.get("data", all_inst.get("results", []))
pg_found = 0
for i in all_items:
name = i.get("displayName") or i.get("name") or ""
if "pg-test" in name.lower():
uid = i.get("instanceUid") or i.get("uid")
print(f" uid={uid} name={name} state={i.get('state')} status={i.get('status')}")
pg_found += 1
if pg_found == 0:
print(f" pg-test инстансов не найдено (всего в ответе: {len(all_items)} записей)")
# Печатаем первые 3 для отладки структуры
for i in all_items[:3]:
print(f" DEBUG: keys={list(i.keys())[:8]}")
-1
View File
@@ -1 +0,0 @@
{"queryDurationMs":134,"instance":{"instanceUid":"fca3f912-c32f-49f9-9cba-40fbaefced63","displayName":"lucy1","serviceId":94,"descr":"Created via Terraform Universal Provider","specificationItemId":1705,"isAuxiliary":false,"instanceConfigDtCreated":"2026-02-13T16:06:51.978+0300","instanceConfigDtUpdated":"2026-02-13T16:06:51.978+0300","contractId":4,"specificationId":4,"specification":"2","quantity":1,"price":null,"svc":"Lucee","code":null,"man":null,"updaterId":7594,"updaterLogin":"tazet@narod.ru","updaterShortname":" Н. Ф.","resourceRealm":"k8s-3.ext.nubes.ru","operationIsInProgress":false,"operationIsPending":false,"uptime":0,"explainedStatus":"running","resourceRealmCnt":2,"isCreated":true,"isDeleted":false,"state":{"instanceStateUid":"1f8e98d0-a2ad-4188-b965-a8ec874cc34b","version":15,"creatorId":7594,"dtState":"2026-02-14T09:40:10.817+0300","isTest":true,"instanceOperationUid":"9c1554b8-5023-4aef-85b1-21f6afc6260a","out":{"monitoring":{"base":{"url":"https://grafana.ngcloud.ru/d/fca3f912-c32f-49f9-9cba-40fbaefced63-def?orgId=28"},"loki":{"url":"https://grafana.ngcloud.ru/d/fca3f912-c32f-49f9-9cba-40fbaefced63-log?orgId=28"},"allDashboards":"https://grafana.ngcloud.ru/dashboards/f/fca3f912-c32f-49f9-9cba-40fbaefced63?orgId=28"},"connectionUrl":"https://web03.luceek8s.services.ngcloud.ru"},"params":{"domain":"web03","gitPath":"https://github.com/xahys/testlucee","jsonEnv":{"PGHOST":"postgresqlk8s-master.a71094c1-b0ed-4bfb-a264-37b41d279567.svc.k8s-3.ext.nubes.ru","PGPORT":"5432","PGUSER":"postgres","PGSSLMODE":"require","PGPASSWORD":"TMGMCN5GYzx8gj1GiqA4rmf19HEJKgQXSUHcN17hJNnbyZmicfhU5AfFpLQyF22I"},"appVersion":"5.4","healthPath":"","resourceCPU":300,"resourceRealm":"k8s-3.ext.nubes.ru","resourceMemory":512,"resourceInstances":1},"vault":{"url":"https://vault.lk.adl.nubes.ru/v1/deck-tool-prod/data/clients/luceek8s/fca3f912-c32f-49f9-9cba-40fbaefced63?version=15","fields":[],"userPath":"luceek8s/fca3f912-c32f-49f9-9cba-40fbaefced63"},"isDeleted":false,"isSuspended":false},"operations":[{"instanceOperationUid":"db0be830-f61d-46a6-8663-6280ace602c9","instanceStateUid":null,"stateVersion":null,"operation":"modify","resourceRealmId":null,"resourceRealm":null,"dtSubmit":"2026-02-14T09:40:47.389+0300","submitResult":"201","dtStart":"2026-02-14T09:40:47.389+0300","dtFinish":"2026-02-14T09:43:37.337+0300","duration":169.948,"secondsPassed":365.3,"isSuccessful":false,"dtCreated":"2026-02-14T09:40:34.638+0300","errorLog":null,"note":null,"updaterLogin":"tazet@narod.ru","updaterShortname":" Н. Ф.","isInProgress":false,"isPending":false},{"instanceOperationUid":"9c1554b8-5023-4aef-85b1-21f6afc6260a","instanceStateUid":"1f8e98d0-a2ad-4188-b965-a8ec874cc34b","stateVersion":15,"operation":"modify","resourceRealmId":null,"resourceRealm":null,"dtSubmit":"2026-02-14T09:38:52.735+0300","submitResult":"201","dtStart":"2026-02-14T09:38:52.735+0300","dtFinish":"2026-02-14T09:40:12.280+0300","duration":79.545,"secondsPassed":570.3,"isSuccessful":true,"dtCreated":"2026-02-14T09:38:40.357+0300","errorLog":null,"note":null,"updaterLogin":"tazet@narod.ru","updaterShortname":" Н. Ф.","isInProgress":false,"isPending":false},{"instanceOperationUid":"1e36b5ea-7a54-452e-8ebd-70f0c6aa82e8","instanceStateUid":"c301c5bc-2abd-44e0-a59a-34fc81da4b56","stateVersion":14,"operation":"modify","resourceRealmId":null,"resourceRealm":null,"dtSubmit":"2026-02-14T09:34:26.821+0300","submitResult":"201","dtStart":"2026-02-14T09:34:26.821+0300","dtFinish":"2026-02-14T09:35:40.839+0300","duration":74.018,"secondsPassed":841.8,"isSuccessful":true,"dtCreated":"2026-02-14T09:34:12.549+0300","errorLog":null,"note":null,"updaterLogin":"tazet@narod.ru","updaterShortname":" Н. Ф.","isInProgress":false,"isPending":false},{"instanceOperationUid":"c0c60270-3100-4b8b-abe6-356955c65d6d","instanceStateUid":"6644063b-22cb-4a68-b9b2-c0e2ba2539ee","stateVersion":13,"operation":"modify","resourceRealmId":null,"resourceRealm":null,"dtSubmit":"2026-02-14T08:39:06.299+0300","submitResult":"201","dtStart":"2026-02-14T08:39:06.299+0300","dtFinish":"2026-02-14T08:39:42.265+0300","duration":35.966,"secondsPassed":4200.3,"isSuccessful":true,"dtCreated":"2026-02-13T21:29:58.266+0300","errorLog":null,"note":null,"updaterLogin":"tazet@narod.ru","updaterShortname":" Н. Ф.","isInProgress":false,"isPending":false},{"instanceOperationUid":"97b6d67b-9476-4c7f-8d77-bfc30aab050f","instanceStateUid":"48c9307a-a73d-4b0b-8ad9-51f6a7aaabdf","stateVersion":12,"operation":"modify","resourceRealmId":null,"resourceRealm":null,"dtSubmit":"2026-02-13T21:29:04.474+0300","submitResult":"201","dtStart":"2026-02-13T21:29:04.473+0300","dtFinish":"2026-02-13T21:29:36.965+0300","duration":32.492,"secondsPassed":44405.6,"isSuccessful":true,"dtCreated":"2026-02-13T21:28:54.690+0300","errorLog":null,"note":null,"updaterLogin":"tazet@narod.ru","updaterShortname":" Н. Ф.","isInProgress":false,"isPending":false},{"instanceOperationUid":"6b6e7072-1b4d-4367-8123-e09dc0a63d5f","instanceStateUid":"5e5d9464-b204-4852-84ed-542c2d52b9f9","stateVersion":11,"operation":"modify","resourceRealmId":null,"resourceRealm":null,"dtSubmit":"2026-02-13T19:17:08.914+0300","submitResult":"201","dtStart":"2026-02-13T19:17:08.914+0300","dtFinish":"2026-02-13T19:17:42.381+0300","duration":33.467,"secondsPassed":52320.2,"isSuccessful":true,"dtCreated":"2026-02-13T19:17:01.113+0300","errorLog":null,"note":null,"updaterLogin":"tazet@narod.ru","updaterShortname":" Н. Ф.","isInProgress":false,"isPending":false},{"instanceOperationUid":"5c5272be-c392-4a39-b0fb-c1aa8bb63907","instanceStateUid":"b1a508a9-bd3c-4720-809e-3617663f16c4","stateVersion":10,"operation":"modify","resourceRealmId":null,"resourceRealm":null,"dtSubmit":"2026-02-13T19:15:30.638+0300","submitResult":"201","dtStart":"2026-02-13T19:15:30.638+0300","dtFinish":"2026-02-13T19:16:03.443+0300","duration":32.805,"secondsPassed":52419.2,"isSuccessful":true,"dtCreated":"2026-02-13T19:15:23.401+0300","errorLog":null,"note":null,"updaterLogin":"tazet@narod.ru","updaterShortname":" Н. Ф.","isInProgress":false,"isPending":false},{"instanceOperationUid":"63e98213-0e7a-4fbb-a8b8-2e2375488bd4","instanceStateUid":"af7973a4-5486-4765-886c-187db17fbbc6","stateVersion":9,"operation":"modify","resourceRealmId":null,"resourceRealm":null,"dtSubmit":"2026-02-13T19:13:18.592+0300","submitResult":"201","dtStart":"2026-02-13T19:13:18.592+0300","dtFinish":"2026-02-13T19:13:52.565+0300","duration":33.973,"secondsPassed":52550,"isSuccessful":true,"dtCreated":"2026-02-13T18:53:38.157+0300","errorLog":null,"note":null,"updaterLogin":"tazet@narod.ru","updaterShortname":" Н. Ф.","isInProgress":false,"isPending":false},{"instanceOperationUid":"41767567-e747-4028-83f2-e02d492c434d","instanceStateUid":"2aa22fa6-c143-46b9-86db-c2112aae2f17","stateVersion":8,"operation":"modify","resourceRealmId":null,"resourceRealm":null,"dtSubmit":"2026-02-13T18:51:20.635+0300","submitResult":"201","dtStart":"2026-02-13T18:51:20.635+0300","dtFinish":"2026-02-13T18:52:41.425+0300","duration":80.79,"secondsPassed":53821.2,"isSuccessful":true,"dtCreated":"2026-02-13T18:51:08.062+0300","errorLog":null,"note":null,"updaterLogin":"tazet@narod.ru","updaterShortname":" Н. Ф.","isInProgress":false,"isPending":false},{"instanceOperationUid":"e53d43be-171b-4540-babc-5b9403e7a982","instanceStateUid":"c79eb58d-7fe6-4442-bec5-8c54dc470ad8","stateVersion":7,"operation":"modify","resourceRealmId":null,"resourceRealm":null,"dtSubmit":"2026-02-13T18:43:52.574+0300","submitResult":"201","dtStart":"2026-02-13T18:43:52.573+0300","dtFinish":"2026-02-13T18:44:27.973+0300","duration":35.4,"secondsPassed":54314.6,"isSuccessful":true,"dtCreated":"2026-02-13T18:43:44.492+0300","errorLog":null,"note":null,"updaterLogin":"tazet@narod.ru","updaterShortname":" Н. Ф.","isInProgress":false,"isPending":false},{"instanceOperationUid":"9a11e25d-6dc1-447c-b8c0-22b172455d62","instanceStateUid":"4eeac614-9497-455e-b4c8-6b6e06a62cf7","stateVersion":6,"operation":"modify","resourceRealmId":null,"resourceRealm":null,"dtSubmit":"2026-02-13T18:41:17.999+0300","submitResult":"201","dtStart":"2026-02-13T18:41:17.999+0300","dtFinish":"2026-02-13T18:42:59.165+0300","duration":101.166,"secondsPassed":54403.4,"isSuccessful":true,"dtCreated":"2026-02-13T18:41:08.248+0300","errorLog":null,"note":null,"updaterLogin":"tazet@narod.ru","updaterShortname":" Н. Ф.","isInProgress":false,"isPending":false},{"instanceOperationUid":"50a3f864-97fc-4e45-b92b-b9537646e6a7","instanceStateUid":"9629e035-02fb-45a4-8896-000184f5a020","stateVersion":5,"operation":"modify","resourceRealmId":null,"resourceRealm":null,"dtSubmit":"2026-02-13T18:32:36.412+0300","submitResult":"201","dtStart":"2026-02-13T18:32:36.412+0300","dtFinish":"2026-02-13T18:33:10.212+0300","duration":33.8,"secondsPassed":54992.4,"isSuccessful":true,"dtCreated":"2026-02-13T18:32:20.849+0300","errorLog":null,"note":null,"updaterLogin":"tazet@narod.ru","updaterShortname":" Н. Ф.","isInProgress":false,"isPending":false},{"instanceOperationUid":"be06ab86-1a48-4c13-b4b2-4e3ed95d7da4","instanceStateUid":"f544dc74-d4ad-4380-90f3-1bf1d3d70110","stateVersion":4,"operation":"modify","resourceRealmId":null,"resourceRealm":null,"dtSubmit":"2026-02-13T18:28:20.245+0300","submitResult":"201","dtStart":"2026-02-13T18:28:20.244+0300","dtFinish":"2026-02-13T18:28:53.651+0300","duration":33.407,"secondsPassed":55248.9,"isSuccessful":true,"dtCreated":"2026-02-13T18:28:11.225+0300","errorLog":null,"note":null,"updaterLogin":"tazet@narod.ru","updaterShortname":" Н. Ф.","isInProgress":false,"isPending":false},{"instanceOperationUid":"e40443e0-0891-41b1-8965-978eed5ba6d3","instanceStateUid":null,"stateVersion":null,"operation":"modify","resourceRealmId":null,"resourceRealm":null,"dtSubmit":"2026-02-13T18:14:36.753+0300","submitResult":"201","dtStart":"2026-02-13T18:14:36.753+0300","dtFinish":"2026-02-13T18:25:40.744+0300","duration":663.991,"secondsPassed":55441.9,"isSuccessful":false,"dtCreated":"2026-02-13T18:14:28.365+0300","errorLog":"Приложение не запустилось. Производится полный откат установки. Ошибка: jlib.k8s [correctReplicaActive] | ERROR | Под(ы) не работают: 'luceek8s' (Deployment).","note":null,"updaterLogin":"tazet@narod.ru","updaterShortname":" Н. Ф.","isInProgress":false,"isPending":false},{"instanceOperationUid":"75146b18-4b58-4672-ae8e-96f51fd86912","instanceStateUid":"40b24238-b1b2-425b-8ad1-51d0ef56a14e","stateVersion":3,"operation":"modify","resourceRealmId":null,"resourceRealm":null,"dtSubmit":"2026-02-13T18:11:55.353+0300","submitResult":"201","dtStart":"2026-02-13T18:11:55.353+0300","dtFinish":"2026-02-13T18:12:33.597+0300","duration":38.244,"secondsPassed":56229,"isSuccessful":true,"dtCreated":"2026-02-13T18:11:45.024+0300","errorLog":null,"note":null,"updaterLogin":"tazet@narod.ru","updaterShortname":" Н. Ф.","isInProgress":false,"isPending":false},{"instanceOperationUid":"cdc73ec4-51b6-4ef3-9e8d-7e9bccd27340","instanceStateUid":"c4ae70b9-a35b-4b26-84b9-b62514613475","stateVersion":2,"operation":"modify","resourceRealmId":null,"resourceRealm":null,"dtSubmit":"2026-02-13T17:59:53.784+0300","submitResult":"201","dtStart":"2026-02-13T17:59:53.783+0300","dtFinish":"2026-02-13T18:00:29.370+0300","duration":35.587,"secondsPassed":56953.2,"isSuccessful":true,"dtCreated":"2026-02-13T17:59:46.242+0300","errorLog":null,"note":null,"updaterLogin":"tazet@narod.ru","updaterShortname":" Н. Ф.","isInProgress":false,"isPending":false},{"instanceOperationUid":"04563ada-d2a4-40cb-9019-c79906cfc361","instanceStateUid":"c2e2c108-9241-4f7e-8f8d-75c12f578375","stateVersion":1,"operation":"create","resourceRealmId":null,"resourceRealm":null,"dtSubmit":"2026-02-13T17:56:45.645+0300","submitResult":"201","dtStart":"2026-02-13T17:56:45.645+0300","dtFinish":"2026-02-13T17:58:41.256+0300","duration":115.611,"secondsPassed":57061.3,"isSuccessful":true,"dtCreated":"2026-02-13T16:06:52.171+0300","errorLog":null,"note":null,"updaterLogin":"tazet@narod.ru","updaterShortname":" Н. Ф.","isInProgress":false,"isPending":false}],"availableOperations":[{"svcOperationId":50,"operation":"delete","instanceOperationUid":null},{"svcOperationId":53,"operation":"redeploy","instanceOperationUid":null},{"svcOperationId":55,"operation":"modify","instanceOperationUid":null},{"svcOperationId":95,"operation":"resume","instanceOperationUid":null},{"svcOperationId":96,"operation":"suspend","instanceOperationUid":null},{"svcOperationId":163,"operation":"restart","instanceOperationUid":null}],"dependentInstances":[],"dependencies":[]},"runDurationMs":136}
File diff suppressed because it is too large Load Diff
-178
View File
@@ -1,178 +0,0 @@
package core
import (
"context"
"encoding/json"
"fmt"
"strings"
)
type ApiOperation struct {
SvcOperationId int `json:"svcOperationId"`
Operation string `json:"operation"`
}
type InstanceStateResponse struct {
InstanceUid string `json:"instanceUid"`
ExplainedStatus string `json:"explainedStatus"`
IsDeleted bool `json:"isDeleted"`
OperationIsInProgress bool `json:"operationIsInProgress"`
OperationIsPending bool `json:"operationIsPending"`
AvailableOperations []ApiOperation `json:"availableOperations"`
}
// FindInstanceByDisplayName finds an instance by display_name for a given serviceId.
// Если найдено больше одного non-deleted инстанса — возвращает ошибку.
func (c *UniversalClient) FindInstanceByDisplayName(ctx context.Context, serviceId int, displayName string) (*InstanceStateResponse, error) {
all, err := c.FindAllInstancesByDisplayName(ctx, serviceId, displayName)
if err != nil {
return nil, err
}
if len(all) == 0 {
return nil, nil
}
if len(all) == 1 {
return &all[0], nil
}
// Множественные non-deleted инстансы: пробуем выбрать единственный running/suspended
var nonDeleted []InstanceStateResponse
for _, inst := range all {
if !inst.IsDeleted {
nonDeleted = append(nonDeleted, inst)
}
}
if len(nonDeleted) == 0 {
return nil, nil
}
if len(nonDeleted) == 1 {
return &nonDeleted[0], nil
}
// Больше одного — формируем понятное сообщение
details := make([]string, 0, len(nonDeleted))
for _, inst := range nonDeleted {
details = append(details, fmt.Sprintf(" - %s (статус: %s)", inst.InstanceUid, inst.ExplainedStatus))
}
return nil, fmt.Errorf(
"обнаружено %d инстансов с именем '%s' (serviceId=%d):\n%s\nНевозможно определить какой adopt-ить. Удалите лишние через ЛК (Управление облаком) или укажите конкретный UUID через 'terraform import'",
len(nonDeleted), displayName, serviceId, strings.Join(details, "\n"),
)
}
func (c *UniversalClient) GetInstanceState(ctx context.Context, instanceUid string) (*InstanceStateResponse, error) {
respBody, _, err := c.doRequest(ctx, "GET", fmt.Sprintf("/instances/%s", instanceUid), nil)
if err != nil {
return nil, err
}
var res struct {
Instance InstanceStateResponse `json:"instance"`
}
if err := json.Unmarshal(respBody, &res); err != nil {
return nil, err
}
if err := validateInstanceStatus(&res.Instance); err != nil {
return nil, err
}
return &res.Instance, nil
}
// GetInstanceStateRaw получает состояние инстанса БЕЗ валидации статуса.
// Используется для проверки ref-параметров: нужно читать даже deleted/suspended инстансы.
func (c *UniversalClient) GetInstanceStateRaw(ctx context.Context, instanceUid string) (*InstanceStateResponse, error) {
respBody, _, err := c.doRequest(ctx, "GET", fmt.Sprintf("/instances/%s", instanceUid), nil)
if err != nil {
return nil, err
}
var res struct {
Instance InstanceStateResponse `json:"instance"`
}
if err := json.Unmarshal(respBody, &res); err != nil {
return nil, err
}
return &res.Instance, nil
}
// FindAllInstancesByDisplayName находит ВСЕ non-deleted инстансы по display_name и serviceId.
// Возвращает slice, чтобы caller мог обработать дубликаты.
func (c *UniversalClient) FindAllInstancesByDisplayName(ctx context.Context, serviceId int, displayName string) ([]InstanceStateResponse, error) {
var found []InstanceStateResponse
page := 1
for {
path := fmt.Sprintf("/instances?page=%d&size=100", page)
respBody, _, err := c.doRequest(ctx, "GET", path, nil)
if err != nil {
return nil, err
}
var res struct {
Results []struct {
InstanceUid string `json:"instanceUid"`
DisplayName string `json:"displayName"`
ServiceId int `json:"serviceId"`
} `json:"results"`
}
if err := json.Unmarshal(respBody, &res); err != nil {
return nil, err
}
if len(res.Results) == 0 {
break
}
for _, item := range res.Results {
if item.ServiceId == serviceId && strings.EqualFold(item.DisplayName, displayName) {
state, err := c.GetInstanceStateRaw(ctx, item.InstanceUid)
if err != nil {
return nil, err
}
if state != nil && !isInstanceDeleted(state) {
found = append(found, *state)
}
}
}
page++
if page > 100 {
break
}
}
return found, nil
}
func isInstanceDeleted(state *InstanceStateResponse) bool {
if state == nil {
return false
}
if state.IsDeleted {
return true
}
return strings.EqualFold(strings.TrimSpace(state.ExplainedStatus), "deleted")
}
func validateInstanceStatus(state *InstanceStateResponse) error {
if state == nil {
return fmt.Errorf("missing instance state")
}
if state.IsDeleted {
return fmt.Errorf("instance %s is deleted", state.InstanceUid)
}
if state.OperationIsPending || state.OperationIsInProgress {
return fmt.Errorf("instance %s not ready: operation pending", state.InstanceUid)
}
status := strings.ToLower(strings.TrimSpace(state.ExplainedStatus))
if strings.Contains(status, "not created") {
return fmt.Errorf("instance %s not created", state.InstanceUid)
}
if strings.Contains(status, "pending") {
return fmt.Errorf("instance %s pending", state.InstanceUid)
}
if strings.Contains(status, "failed") || strings.Contains(status, "error") {
return fmt.Errorf("instance %s failed: %s", state.InstanceUid, state.ExplainedStatus)
}
return nil
}
-137
View File
@@ -1,137 +0,0 @@
package core
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
)
// RunInstanceOperationUniversal runs an available operation (modify/suspend/delete/resume) if possible.
func (c *UniversalClient) RunInstanceOperationUniversal(ctx context.Context, instanceUid string, action string, params map[int]string) error {
state, err := c.GetInstanceState(ctx, instanceUid)
if err != nil {
return err
}
var opId int
for _, op := range state.AvailableOperations {
if strings.EqualFold(op.Operation, action) {
opId = op.SvcOperationId
break
}
}
if opId == 0 {
return fmt.Errorf("action %s not available for instance %s", action, instanceUid)
}
payload := map[string]interface{}{
"instanceUid": instanceUid,
"svcOperationId": opId,
"operation": action,
}
opUid, err := c.postIgnoreResponse(ctx, "/instanceOperations", payload, true)
if err != nil {
return fmt.Errorf("failed to create %s operation: %w", action, err)
}
if opUid == "" {
return fmt.Errorf("failed to get operation UID for %s", action)
}
for paramId, value := range params {
pPayload := genericParamReq{
InstanceOperationUid: opUid,
SvcOperationCfsParamId: paramId,
ParamValue: value,
}
_, _, err := c.doRequest(ctx, "POST", "/instanceOperationCfsParams", pPayload)
if err != nil {
return fmt.Errorf("failed to set param %d: %w", paramId, err)
}
}
_, _, err = c.doRequest(ctx, "POST", fmt.Sprintf("/instanceOperations/%s/run", opUid), map[string]interface{}{})
if err != nil {
return err
}
return c.waitForOperationFinish(ctx, opUid, defaultOperationTimeout)
}
const defaultOperationTimeout = 30 * time.Minute
type operationStatusResponse struct {
InstanceOperation struct {
DtFinish *string `json:"dtFinish"`
IsSuccessful *bool `json:"isSuccessful"`
ErrorLog *string `json:"errorLog"`
IsInProgress bool `json:"isInProgress"`
IsPending bool `json:"isPending"`
} `json:"instanceOperation"`
}
func (c *UniversalClient) waitForOperationFinish(ctx context.Context, opUid string, timeout time.Duration) error {
deadline := time.Now().Add(timeout)
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return fmt.Errorf("operation %s cancelled", opUid)
case <-ticker.C:
if time.Now().After(deadline) {
return fmt.Errorf("timeout waiting for operation %s to finish", opUid)
}
respBody, _, err := c.doRequest(ctx, "GET", fmt.Sprintf("/instanceOperations/%s", opUid), nil)
if err != nil {
return fmt.Errorf("failed to check operation %s status: %w", opUid, err)
}
var status operationStatusResponse
if err := json.Unmarshal(respBody, &status); err != nil {
return fmt.Errorf("failed to parse operation %s status: %w", opUid, err)
}
if status.InstanceOperation.DtFinish != nil && strings.TrimSpace(*status.InstanceOperation.DtFinish) != "" {
if status.InstanceOperation.IsSuccessful != nil && !*status.InstanceOperation.IsSuccessful {
if status.InstanceOperation.ErrorLog != nil && strings.TrimSpace(*status.InstanceOperation.ErrorLog) != "" {
return fmt.Errorf("operation %s failed: %s", opUid, *status.InstanceOperation.ErrorLog)
}
return fmt.Errorf("operation %s failed", opUid)
}
return nil
}
}
}
}
func (c *UniversalClient) postIgnoreResponse(ctx context.Context, path string, payload interface{}, returnLocation bool) (string, error) {
respBody, headers, err := c.doRequest(ctx, "POST", path, payload)
if err != nil {
return "", err
}
if returnLocation {
if loc := headers.Get("Location"); loc != "" {
return extractUIDFromLocation(loc), nil
}
}
var justId string
if err := json.Unmarshal(respBody, &justId); err == nil && justId != "" {
return justId, nil
}
return "", nil
}
func extractUIDFromLocation(loc string) string {
if loc == "" {
return ""
}
return strings.TrimPrefix(loc, "./")
}
-112
View File
@@ -1,112 +0,0 @@
package generated
import (
"context"
"strconv"
"terraform-provider-mycloud/internal/core"
// "terraform-provider-mycloud/internal/provider" REMOVED CYCLE
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
// ========= RESOURCE DEFINITION (Auto-Generated) =========
// Service: Bolvanka (Dummy)
// Service ID: 1
var _ resource.Resource = &BolvankaResource{}
func NewBolvankaResource() resource.Resource {
return &BolvankaResource{}
}
type BolvankaResource struct {
client *core.UniversalClient
}
type BolvankaResourceModel struct {
ID types.String `tfsdk:"id"`
DisplayName types.String `tfsdk:"display_name"`
// Params
DurationMs types.Int64 `tfsdk:"duration_ms"` // ID 198
FailAtStart types.Bool `tfsdk:"fail_at_start"` // ID 199
}
func (r *BolvankaResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_bolvanka"
}
func (r *BolvankaResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{Computed: true},
"display_name": schema.StringAttribute{Required: true},
"duration_ms": schema.Int64Attribute{
Required: true,
MarkdownDescription: "Sleep duration in ms (Param 198)",
},
"fail_at_start": schema.BoolAttribute{
Optional: true,
MarkdownDescription: "Fail immediately (Param 199)",
},
},
}
}
func (r *BolvankaResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data BolvankaResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// === MAP PARAMETERS (The Stringifier Logic) ===
params := make(map[int]string)
// 198: durationMs (int -> string)
params[198] = strconv.FormatInt(data.DurationMs.ValueInt64(), 10)
// 199: failAtStart (bool -> string)
valRef := "false"
if data.FailAtStart.ValueBool() {
valRef = "true"
}
params[199] = valRef
// Call Core
id, err := r.client.CreateGenericInstance(ctx, 1, data.DisplayName.ValueString(), params)
if err != nil {
resp.Diagnostics.AddError("Client Error", err.Error())
return
}
data.ID = types.StringValue(id)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *BolvankaResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
// Implemented as shim for now
}
func (r *BolvankaResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
}
func (r *BolvankaResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
}
func (r *BolvankaResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
// Assuming Provider passes the correct *UniversalClient or compatible interface
client, ok := req.ProviderData.(*core.UniversalClient)
if !ok {
resp.Diagnostics.AddError("Error", "Wrong client type expected *core.UniversalClient")
return
}
r.client = client
}
@@ -1,110 +0,0 @@
package generated
import (
"context"
"strconv"
"terraform-provider-mycloud/internal/core"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
// ========= RESOURCE DEFINITION (Auto-Generated, Universal Flow) =========
// Service: Bolvanka (Dummy)
// Service ID: 1
var _ resource.Resource = &BolvankaUniversalResource{}
func NewBolvankaUniversalResource() resource.Resource {
return &BolvankaUniversalResource{}
}
type BolvankaUniversalResource struct {
client *core.UniversalClient
}
type BolvankaUniversalResourceModel struct {
ID types.String `tfsdk:"id"`
DisplayName types.String `tfsdk:"display_name"`
// Params
DurationMs types.Int64 `tfsdk:"duration_ms"` // ID 198
FailAtStart types.Bool `tfsdk:"fail_at_start"` // ID 199
}
func (r *BolvankaUniversalResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_bolvanka_universal"
}
func (r *BolvankaUniversalResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{Computed: true},
"display_name": schema.StringAttribute{Required: true},
"duration_ms": schema.Int64Attribute{
Required: true,
MarkdownDescription: "Sleep duration in ms (Param 198)",
},
"fail_at_start": schema.BoolAttribute{
Optional: true,
MarkdownDescription: "Fail immediately (Param 199)",
},
},
}
}
func (r *BolvankaUniversalResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data BolvankaUniversalResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// === MAP PARAMETERS (The Stringifier Logic) ===
params := make(map[int]string)
// 198: durationMs (int -> string)
params[198] = strconv.FormatInt(data.DurationMs.ValueInt64(), 10)
// 199: failAtStart (bool -> string)
valRef := "false"
if data.FailAtStart.ValueBool() {
valRef = "true"
}
params[199] = valRef
// Call Core (Universal Flow V6)
id, err := r.client.CreateGenericInstanceUniversalV6(ctx, 1, data.DisplayName.ValueString(), params)
if err != nil {
resp.Diagnostics.AddError("Client Error", err.Error())
return
}
data.ID = types.StringValue(id)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *BolvankaUniversalResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
// Implemented as shim for now
}
func (r *BolvankaUniversalResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
}
func (r *BolvankaUniversalResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
}
func (r *BolvankaUniversalResource) 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", "Wrong client type expected *core.UniversalClient")
return
}
r.client = client
}
@@ -1,192 +0,0 @@
package generated
import (
"context"
"strconv"
"strings"
"terraform-provider-mycloud/internal/core"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
"github.com/hashicorp/terraform-plugin-framework/types"
)
// ========= RESOURCE DEFINITION (Auto-Generated, Universal Flow + Lifecycle) =========
// Service: Bolvanka (Dummy)
// Service ID: 1
var _ resource.Resource = &BolvankaUniversalLifecycleResource{}
func NewBolvankaUniversalLifecycleResource() resource.Resource {
return &BolvankaUniversalLifecycleResource{}
}
type BolvankaUniversalLifecycleResource struct {
client *core.UniversalClient
}
type BolvankaUniversalLifecycleModel struct {
ID types.String `tfsdk:"id"`
DisplayName types.String `tfsdk:"display_name"`
DurationMs types.Int64 `tfsdk:"duration_ms"`
FailAtStart types.Bool `tfsdk:"fail_at_start"`
DeleteMode types.String `tfsdk:"delete_mode"`
ResumeIfExists types.Bool `tfsdk:"resume_if_exists"`
}
func (r *BolvankaUniversalLifecycleResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_bolvanka_universal_lifecycle"
}
func (r *BolvankaUniversalLifecycleResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
},
"display_name": schema.StringAttribute{
Required: true,
},
"duration_ms": schema.Int64Attribute{
Required: true,
MarkdownDescription: "Sleep duration in ms (Param 198)",
},
"fail_at_start": schema.BoolAttribute{
Optional: true,
MarkdownDescription: "Fail immediately (Param 199)",
},
"delete_mode": schema.StringAttribute{
Optional: true,
Computed: true,
Default: stringdefault.StaticString("state_only"),
MarkdownDescription: "Deletion mode: delete | suspend | state_only",
},
"resume_if_exists": schema.BoolAttribute{
Optional: true,
Computed: true,
Default: booldefault.StaticBool(true),
MarkdownDescription: "If true, attempt resume/adopt when instance already exists",
},
},
}
}
func (r *BolvankaUniversalLifecycleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data BolvankaUniversalLifecycleModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
displayName := data.DisplayName.ValueString()
// If resume_if_exists: try to find existing instance by display_name
if data.ResumeIfExists.ValueBool() {
existing, err := r.client.FindInstanceByDisplayName(ctx, 1, displayName)
if err != nil {
resp.Diagnostics.AddError("Client Error", err.Error())
return
}
if existing != nil {
status := strings.ToLower(existing.ExplainedStatus)
if strings.Contains(status, "suspend") || strings.Contains(status, "suspended") {
if err := r.client.RunInstanceOperationUniversal(ctx, existing.InstanceUid, "resume", nil); err != nil {
resp.Diagnostics.AddError("Client Error", err.Error())
return
}
}
data.ID = types.StringValue(existing.InstanceUid)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
return
}
}
params := map[int]string{
198: strconv.FormatInt(data.DurationMs.ValueInt64(), 10),
199: boolToString(data.FailAtStart.ValueBool()),
}
id, err := r.client.CreateGenericInstanceUniversalV6(ctx, 1, displayName, params)
if err != nil {
resp.Diagnostics.AddError("Client Error", err.Error())
return
}
data.ID = types.StringValue(id)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *BolvankaUniversalLifecycleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
// Implemented as shim for now
}
func (r *BolvankaUniversalLifecycleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data BolvankaUniversalLifecycleModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
params := map[int]string{
198: strconv.FormatInt(data.DurationMs.ValueInt64(), 10),
199: boolToString(data.FailAtStart.ValueBool()),
}
if err := r.client.RunInstanceOperationUniversal(ctx, data.ID.ValueString(), "modify", params); err != nil {
resp.Diagnostics.AddError("Client Error", err.Error())
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *BolvankaUniversalLifecycleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state BolvankaUniversalLifecycleModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
mode := strings.ToLower(state.DeleteMode.ValueString())
if mode == "" {
mode = "state_only"
}
switch mode {
case "state_only":
return
case "suspend", "delete":
if err := r.client.RunInstanceOperationUniversal(ctx, state.ID.ValueString(), mode, nil); err != nil {
resp.Diagnostics.AddError("Client Error", err.Error())
return
}
default:
resp.Diagnostics.AddError("Invalid delete_mode", "Use: delete | suspend | state_only")
return
}
}
func (r *BolvankaUniversalLifecycleResource) 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", "Wrong client type expected *core.UniversalClient")
return
}
r.client = client
}
func boolToString(v bool) string {
if v {
return "true"
}
return "false"
}
-132
View File
@@ -1,132 +0,0 @@
# Internal Provider — Code-level Index & Developer Guide
Generated: 2026-01-27
Purpose: Quick reference for developers and Copilot agents to understand the internal Terraform provider implementation (`internal/provider/`). **Read this file before making changes to provider code.**
---
## Overview ✨
This folder contains the implementation of the Terraform provider `nubes` (resources and data-sources) and the HTTP client used to interact with the Taffy/Deck API.
Primary responsibilities:
- Define provider schema and configuration (`provider.go`).
- Implement resources (`*_resource.go`) and data-sources (`*_data_source.go`).
- Encapsulate HTTP interactions in `client_impl.go` (NubesClient) and helper types.
- Implement polling/wait logic and operation submission patterns used across resources.
---
## Files (short mapping)
- `provider.go` — Provider factory, Metadata, Schema, and lists of Resources/DataSources.
- `client_impl.go``NubesClient` implementation: request helpers, polling (`WaitForInstanceReady`, `WaitForOperation`), instance state retrieval, and operation submission.
- `edge_resource.go` / `edge_data_source.go` — Edge Gateway resource and data-source implementations.
- `vm_resource.go` — VM resource implementation (create/read/update/delete/import and operation waiting, `waitForVMOperationAndInstanceStatus`).
- `vapp_resource.go` / `vapp_data_source.go` — vApp resource and data-source.
- `vdc_resource.go` / `vdc_data_source.go` — vDC resource and data-source.
- `organization_resource.go` — Organization resource implementation.
- `postgres_resource.go` / `pgadmin_resource.go` — Postgres and PgAdmin resources.
- `s3bucket_resource.go` — S3 bucket resource.
- `quick_start_resource.go` — QuickStart resource (helper for full-stack installs).
- `tubulus_resource.go` / `tubulus_ai.go` — Tubulus resource (AI integrations). Contains `askGemini` usage.
---
## Key Patterns & Conventions 🔧
1. Resource lifecycle methods follow Terraform SDK conventions: `Metadata`, `Schema`, `Configure`, `Create`, `Read`, `Update`, `Delete`, `ImportState`.
2. Creation pattern:
- Submit a create operation (via `NubesClient` helpers) -> wait for operation state -> wait for instance readiness (if applicable) -> apply post-creation steps (VIP, DNS, FW).
3. Operation polling:
- Each resource implements `waitForOperationAndInstanceStatus` style helpers (e.g., VM uses specialized `waitForVMOperationAndInstanceStatus`) that poll Deck API for operation completion using `GetInstanceOperation` / `GetInstanceState`.
4. Parameter submission:
- Before `Create`, resources collect a model (e.g., `VMResourceModel`) and call `submitVMOperationParams` / `submitOperationParams` which uses `instanceOperationCfsParams` mapping to Deck's `svcOperationCfsParamId`.
5. Error handling:
- If operation stage `dtFinish` is `null` or the stage is stuck on `[PROCESS]`, functions return a timeout/error and propagate to Terraform user.
---
## Important Types & Functions (by file)
### provider.go
- `New(version string) func() provider.Provider` — Provider factory used by Terraform to instantiate provider.
- `NubesProvider` (type) — implements provider hooks: `Metadata`, `Schema`, `Configure`.
- `Resources()` and `DataSources()` — lists of registered resources and data-sources.
### client_impl.go
- `type NubesClient struct` — HTTP client wrapper; holds base URL, token provider, logger.
- `GetOperationId(ctx, serviceId, opName)` — Resolve operation numeric ID by name.
- `CreateInstance(ctx, displayName, serviceId, svcOperationId, params)` — Creates instance and returns UIDs.
- `WaitForInstanceReady`, `WaitForOperation` — Polling helpers.
- `GetInstanceStateDetails`, `GetInstanceOperation` — low-level getters for state & operation details.
- `Post`, `postInstance` helpers — handle posting and optionally returning Location/ID from `Location` header.
### vm_resource.go
- `NewVMResource()` — resource constructor.
- `VMResource` and `VMResourceModel` — model for user-specified parameters and mapping.
- `Create` — builds request model, submits params, runs operation (watching for `Firewall` stage and others), and treats partial success carefully.
- `waitForVMOperationAndInstanceStatus` — VM-specific wait logic (parses stages and handles FW timeouts).
### edge_resource.go & edge_data_source.go
- `NewEdgeResource()`, `NewEdgeDataSource()` — constructors.
- `EdgeResourceModel`, `readInstance`, `submitOperationParams`, `waitForOperationAndInstanceStatus` — same patterns applied for Edge resource.
### tubulus_resource.go & tubulus_ai.go
- Integrates AI flows (Gemini) with resource flow.
- `askGemini` — helper which calls AI integration for instruction parsing.
---
## How to build the provider locally (developer workflow) ⚙️
1. Build the provider binary:
```bash
# At repo root
go build -o terraform-provider-nubes ./
```
2. Make it available to Terraform for local testing:
```bash
# Option A: plugin dir
mkdir -p ~/.terraform.d/plugins/local/terraform-provider-nubes
cp terraform-provider-nubes ~/.terraform.d/plugins/local/terraform-provider-nubes/
# Option B: use plugin-dir during init
terraform init -plugin-dir=./ (not recommended if you have mixed plugins)
```
3. Run an example config:
```bash
cd examples/quick_start
terraform init
terraform apply -var="token=<YOUR_TOKEN>" -auto-approve
```
4. Useful commands during development:
- `go vet`, `golangci-lint run` (if configured), `go test ./...`.
- Use `tools/har/*` scripts to replay HAR-based scenarios when testing resource behavior.
> Note: Examples might perform destructive operations against live environment — prefer dev account and `-auto-approve` only when you expect the run.
---
## Acceptance / Integration Tests
- Use `tests/` scenarios to validate create/modify/delete flows.
- Manual acceptance: run an example on a dev account, inspect `instances` and `operations` via Deck API.
- Test idempotency: apply the same config twice and ensure `plan` shows no changes.
---
## Conventions for Agents and Contributors
- ALWAYS read `/home/naeel/terra/REPO_CONTENTS.md` and this file before making changes to provider code.
- When adding a resource:
- Add a new `*_resource.go` and a `*_data_source.go` if discovery is required.
- Implement `Create` / `Read` / `Update` / `Delete` / import if supported.
- Add tests in `tests/` and examples in `examples/`.
---
## Next steps I can take ✅
- Generate a function-level index (per-file exported functions and short signatures) as a machine-readable YAML/JSON for other agents.
- Add `internal/provider/DEVELOPMENT.md` with a checklist and `make` targets for build and acceptance test steps.
If you want the function-level JSON index and a `DEVELOPMENT.md`, say "code-level" and I'll create them and commit to the repo.
-924
View File
@@ -1,924 +0,0 @@
package provider
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
"github.com/hashicorp/terraform-plugin-log/tflog"
)
// Helper struct for parameters
type InstanceParam struct {
SvcOperationCfsParamId int
ParamValue string
}
type InstanceOperationRequest struct {
Action string `json:"action"`
Params interface{} `json:"params"`
}
// Implement methods for NubesClient defined in provider.go
func (c *NubesClient) GetOperationId(ctx context.Context, serviceId int, opName string) (int, error) {
// Logic to fetch operation ID if needed.
// Based on HAR, we might not strictly need this if we pass "operation": "create"
// But let's assume we return a dummy or look it up.
// For now, return 0 as placeholder or implement lookup if API supports it.
return 0, nil
}
func (c *NubesClient) CreateInstance(ctx context.Context, displayName string, serviceId int, svcOperationId int, params []InstanceParam) (string, string, error) {
// 1. Create Instance Placeholder
// Payload based on pg_admin.har: {"serviceId":96,"displayName":"...","descr":""}
payload := map[string]interface{}{
"serviceId": serviceId,
"displayName": displayName,
"descr": "",
}
instanceUid, err := c.postInstance(ctx, payload)
if err != nil {
return "", "", fmt.Errorf("failed to create instance placeholder: %w", err)
}
tflog.Info(ctx, fmt.Sprintf("Created Instance Placeholder: %s", instanceUid))
// 2. Create Operation
opPayload := map[string]interface{}{
"instanceUid": instanceUid,
"operation": "create",
}
// If svcOperationId is valid (>0), maybe we use it? HAR just said "operation":"create".
// We'll stick to "operation":"create".
opUid, err := c.postIgnoreResponse(ctx, "/instanceOperations", opPayload, true)
if err != nil {
return "", "", fmt.Errorf("failed to create operation: %w", err)
}
tflog.Info(ctx, fmt.Sprintf("Created Operation: %s", opUid))
// 3. Get operation details with parameters
opPath := fmt.Sprintf("/instanceOperations/%s?fields=cfsParams", opUid)
var opDetails GetOperationResponse
if err := c.get(ctx, opPath, &opDetails); err != nil {
return "", "", fmt.Errorf("failed to get operation details: %w", err)
}
// 3a. Submit parameters
// Strategy:
// 1. Send all explicitly provided 'params' (from Terraform config/resource)
// 2. Iterate server-provided defaults (opDetails) for anything we missed and send defaults
sentParams := make(map[int]bool)
// Phase 1: Send explicit overrides
for _, p := range params {
valToSend := p.ParamValue
// Simple normalization for empty values if needed
if valToSend == "" {
// Some fields might reject empty string? For now send as is
// or apply the map/list fix if we knew the type.
// But for explicit params, we assume caller knows best.
}
paramPayload := map[string]interface{}{
"instanceOperationUid": opUid,
"svcOperationCfsParamId": p.SvcOperationCfsParamId,
"paramValue": valToSend,
}
_, err := c.postIgnoreResponse(ctx, "/instanceOperationCfsParams", paramPayload, false)
if err != nil {
return "", "", fmt.Errorf("failed to submit explicit param %d: %w", p.SvcOperationCfsParamId, err)
}
sentParams[p.SvcOperationCfsParamId] = true
}
// Phase 2: Fill in defaults from Server Metadata (if not already sent)
for _, param := range opDetails.InstanceOperation.CfsParams {
if _, sent := sentParams[param.SvcOperationCfsParamId]; sent {
continue // Already sent in Phase 1
}
valToSend := ""
if param.ParamValue != nil {
valToSend = *param.ParamValue
} else if param.DefaultValue != nil {
valToSend = *param.DefaultValue
}
// Fix specific data type formatting (from tubulus example)
if valToSend == "" {
if param.DataType == "map" || param.DataType == "json" {
valToSend = "{}"
} else if param.DataType == "array" || param.DataType == "list" {
valToSend = "[]"
}
}
paramPayload := map[string]interface{}{
"instanceOperationUid": opUid,
"svcOperationCfsParamId": param.SvcOperationCfsParamId,
"paramValue": valToSend,
}
_, err := c.postIgnoreResponse(ctx, "/instanceOperationCfsParams", paramPayload, false)
if err != nil {
return "", "", fmt.Errorf("failed to submit default param %d: %w", param.SvcOperationCfsParamId, err)
}
}
// 4. Validate (Optional, seen in S3 HAR)
validateUrl := fmt.Sprintf("/instanceOperations/%s/validate-cfs", opUid)
tflog.Info(ctx, fmt.Sprintf("Validating operation: %s", validateUrl))
_ = c.get(ctx, validateUrl, nil)
// 5. Run Operation
// HAR: POST .../instanceOperations/{ids}/run
runUrl := fmt.Sprintf("/instanceOperations/%s/run", opUid)
tflog.Info(ctx, fmt.Sprintf("Running operation: %s", runUrl))
_, err = c.postIgnoreResponse(ctx, runUrl, map[string]interface{}{}, false)
if err != nil {
return "", "", fmt.Errorf("failed to run operation: %w", err)
}
return instanceUid, opUid, nil
}
// RunInstanceOperation is a helper to just fill params and run an existing OP UID
func (c *NubesClient) RunInstanceOperation(ctx context.Context, opUid string, params []InstanceParam) (string, string, error) {
// Add params
for _, param := range params {
valToSend := param.ParamValue
// Simple normalization
if valToSend == "" {
valToSend = "pass" // Default fallback if needed, or empty
}
paramPayload := map[string]interface{}{
"instanceOperationUid": opUid,
"svcOperationCfsParamId": param.SvcOperationCfsParamId,
"paramValue": valToSend,
}
_, err := c.postIgnoreResponse(ctx, "/instanceOperationCfsParams", paramPayload, false)
if err != nil {
return "", "", fmt.Errorf("failed to add param %d: %w", param.SvcOperationCfsParamId, err)
}
}
// Validate
validateUrl := fmt.Sprintf("/instanceOperations/%s/validate-cfs", opUid)
_ = c.get(ctx, validateUrl, nil)
// Run
runUrl := fmt.Sprintf("/instanceOperations/%s/run", opUid)
_, err := c.postIgnoreResponse(ctx, runUrl, map[string]interface{}{}, false)
if err != nil {
return "", "", fmt.Errorf("failed to run operation: %w", err)
}
return "", opUid, nil
}
func (c *NubesClient) WaitForInstanceStatus(ctx context.Context, instanceUid string, targetStatus string) error {
timeout := time.After(15 * time.Minute)
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-timeout:
return fmt.Errorf("timeout waiting for instance %s to reach status %s", instanceUid, targetStatus)
case <-ticker.C:
inst, err := c.GetInstanceState(ctx, instanceUid)
if err != nil {
tflog.Warn(ctx, fmt.Sprintf("Error checking state for %s: %s", instanceUid, err))
continue
}
state := inst.ExplainedStatus
tflog.Info(ctx, fmt.Sprintf("Instance %s current status: %s (target: %s, InProgress: %v)", instanceUid, state, targetStatus, inst.OperationIsInProgress))
if strings.EqualFold(state, targetStatus) {
return nil
}
lowerState := strings.ToLower(state)
if strings.Contains(lowerState, "error") || strings.Contains(lowerState, "failed") {
return fmt.Errorf("instance %s in error state: %s", instanceUid, state)
}
if !inst.OperationIsInProgress && !inst.OperationIsPending {
if strings.EqualFold(state, targetStatus) {
return nil
}
return fmt.Errorf("operation finished but target status %s not reached (current: %s)", targetStatus, state)
}
}
}
}
// ttyWriter открывает /dev/tty для прямого вывода в терминал пользователя,
// минуя перехват stderr terraform'ом. Fallback на os.Stderr.
func ttyWriter() *os.File {
if f, err := os.OpenFile("/dev/tty", os.O_WRONLY, 0); err == nil {
return f
}
return os.Stderr
}
func (c *NubesClient) WaitForOperation(ctx context.Context, opUid string) error {
timeout := time.After(15 * time.Minute)
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
// отслеживаем уже напечатанные этапы чтобы не дублировать
printedStages := make(map[string]bool)
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-timeout:
return fmt.Errorf("timeout waiting for operation %s", opUid)
case <-ticker.C:
op, err := c.GetInstanceOperation(ctx, opUid)
if err != nil {
tflog.Warn(ctx, fmt.Sprintf("Error checking operation %s: %s", opUid, err))
continue
}
// Печатаем завершённые этапы
tty := ttyWriter()
defer tty.Close()
for _, stage := range op.Stages {
if printedStages[stage.InstanceOperationStageUid] {
continue
}
if stage.DtFinish == nil || *stage.DtFinish == "" {
continue
}
printedStages[stage.InstanceOperationStageUid] = true
status := "OK "
if !stage.IsSuccessful {
status = "FAIL"
}
fmt.Fprintf(tty, " [%s] %s — %.1f sec\n", status, stage.Stage, stage.Duration)
if stage.StageMsg != nil && *stage.StageMsg != "" {
fmt.Fprintf(tty, " %s\n", *stage.StageMsg)
}
}
// Операция завершена
if !op.IsInProgress && !op.IsPending && op.DtFinish != nil && *op.DtFinish != "" {
if op.IsSuccessful != nil && *op.IsSuccessful {
if op.Duration != nil {
fmt.Fprintf(tty, " [DONE] operation completed in %.1f sec\n", *op.Duration)
}
return nil
}
// Собираем детали ошибки из упавших этапов
var failedStages []string
for _, stage := range op.Stages {
if !stage.IsSuccessful && stage.DtFinish != nil {
msg := stage.Stage
if stage.StageMsg != nil && *stage.StageMsg != "" {
msg += ": " + *stage.StageMsg
}
failedStages = append(failedStages, msg)
}
}
errMsg := "operation failed"
if op.ErrorLog != nil && *op.ErrorLog != "" {
errMsg = *op.ErrorLog
}
if len(failedStages) > 0 {
errMsg += " | failed stages: " + strings.Join(failedStages, "; ")
}
return fmt.Errorf("%s", errMsg)
}
// Ранний выход если ErrorLog появился раньше dtFinish
if op.ErrorLog != nil && *op.ErrorLog != "" {
return fmt.Errorf("operation failed (early error): %s", *op.ErrorLog)
}
}
}
}
func (c *NubesClient) get(ctx context.Context, path string, target interface{}) error {
url := fmt.Sprintf("%s%s", c.ApiEndpoint, path)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return err
}
if c.ApiToken != "" {
req.Header.Set("Authorization", "Bearer "+c.ApiToken)
}
resp, err := c.HttpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("GET %s failed with status %d: %s", path, resp.StatusCode, string(body))
}
if target != nil {
return json.NewDecoder(resp.Body).Decode(target)
}
return nil
}
func (c *NubesClient) WaitForInstanceReady(ctx context.Context, instanceUid string) error {
// Set timeout
timeout := time.After(15 * time.Minute)
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-timeout:
return fmt.Errorf("timeout waiting for instance %s", instanceUid)
case <-ticker.C:
// check status
inst, err := c.GetInstanceState(ctx, instanceUid)
if err != nil {
tflog.Warn(ctx, fmt.Sprintf("Error checking state for %s: %s", instanceUid, err))
continue
}
state := inst.ExplainedStatus
tflog.Info(ctx, fmt.Sprintf("Instance %s status: %s (InProgress: %v)", instanceUid, state, inst.OperationIsInProgress))
// Statuses from HAR: "running", "Active", "deployed", "Deployed"
if strings.EqualFold(state, "Active") || strings.EqualFold(state, "Running") || strings.EqualFold(state, "Deployed") {
return nil
}
// Detect failure patterns in explainedStatus
lowerState := strings.ToLower(state)
if strings.Contains(lowerState, "error") ||
strings.Contains(lowerState, "failed") ||
strings.Contains(lowerState, "не удалось") ||
strings.Contains(lowerState, "не заполнена") {
return fmt.Errorf("instance %s end status: %s", instanceUid, state)
}
// If operation finished but we didn't reach success state
if !inst.OperationIsInProgress && !inst.OperationIsPending {
// Second check for success, sometimes status updates slightly after op finishes
if strings.EqualFold(state, "Active") || strings.EqualFold(state, "Running") || strings.EqualFold(state, "Deployed") {
return nil
}
// If still not success, and not in a known transitioning state (like "Processing", "Creating")
if !strings.EqualFold(state, "Creating") && !strings.EqualFold(state, "Processing") && state != "" {
return fmt.Errorf("operation finished but instance %s is in unexpected state: %s", instanceUid, state)
}
}
}
}
}
func (c *NubesClient) DeleteInstance(ctx context.Context, instanceUid string, opId int) error {
// Create delete operation
opPayload := map[string]interface{}{
"instanceUid": instanceUid,
"operation": "delete",
}
opUid, err := c.postIgnoreResponse(ctx, "/instanceOperations", opPayload, true)
if err != nil {
return err
}
// Run It
runUrl := fmt.Sprintf("/instanceOperations/%s/run", opUid)
_, err = c.postIgnoreResponse(ctx, runUrl, map[string]interface{}{}, false)
return err
}
// Helpers
func (c *NubesClient) postInstance(ctx context.Context, payload interface{}) (string, error) {
url := fmt.Sprintf("%s/instances", c.ApiEndpoint)
data, err := json.Marshal(payload)
if err != nil {
return "", err
}
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(data))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
if c.ApiToken != "" {
req.Header.Set("Authorization", "Bearer "+c.ApiToken)
}
resp, err := c.HttpClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != 201 && resp.StatusCode != 200 {
body, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("status %d: %s", resp.StatusCode, string(body))
}
// Try to get ID from Location header
loc := resp.Header.Get("Location")
if loc != "" {
parts := strings.Split(loc, "/")
if len(parts) > 0 {
return parts[len(parts)-1], nil
}
}
// Try body
var res map[string]interface{}
body, _ := io.ReadAll(resp.Body)
if len(body) > 0 {
if err := json.Unmarshal(body, &res); err == nil {
if uid, ok := res["instanceUid"].(string); ok {
return uid, nil
}
}
}
return "", fmt.Errorf("could not extract instanceUid from response")
}
func (c *NubesClient) postIgnoreResponse(ctx context.Context, path string, payload interface{}, returnLocationId bool) (string, error) {
url := fmt.Sprintf("%s%s", c.ApiEndpoint, path)
var reqBody io.Reader
if payload != nil {
data, err := json.Marshal(payload)
if err != nil {
return "", err
}
reqBody = bytes.NewBuffer(data)
}
req, err := http.NewRequestWithContext(ctx, "POST", url, reqBody)
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
if c.ApiToken != "" {
req.Header.Set("Authorization", "Bearer "+c.ApiToken)
}
resp, err := c.HttpClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != 201 && resp.StatusCode != 200 {
body, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("status %d: %s", resp.StatusCode, string(body))
}
if returnLocationId {
// Extract ID from Location header .../something/{id}
loc := resp.Header.Get("Location")
if loc != "" {
parts := strings.Split(loc, "/")
return parts[len(parts)-1], nil
}
// Fallback to body scan if needed
var res map[string]interface{}
body, _ := io.ReadAll(resp.Body)
if len(body) > 0 {
if err := json.Unmarshal(body, &res); err == nil {
// Try common ID fields
if id, ok := res["instanceOperationUid"].(string); ok {
return id, nil
}
}
}
}
return "", nil
}
type ApiOperation struct {
SvcOperationId int `json:"svcOperationId"`
Operation string `json:"operation"`
}
type InstanceStateData struct {
Params map[string]interface{} `json:"params"`
}
type InstanceStateResponse struct {
InstanceUid string `json:"instanceUid"`
ExplainedStatus string `json:"explainedStatus"`
OperationIsInProgress bool `json:"operationIsInProgress"`
OperationIsPending bool `json:"operationIsPending"`
AvailableOperations []ApiOperation `json:"availableOperations"`
State *InstanceStateData `json:"state"`
}
func (c *NubesClient) GetInstanceState(ctx context.Context, instanceUid string) (*InstanceStateResponse, error) {
url := fmt.Sprintf("%s/instances/%s", c.ApiEndpoint, instanceUid)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
if c.ApiToken != "" {
req.Header.Set("Authorization", "Bearer "+c.ApiToken)
}
resp, err := c.HttpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("status %d", resp.StatusCode)
}
var res struct {
Instance InstanceStateResponse `json:"instance"`
}
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
return nil, err
}
return &res.Instance, nil
}
// Post is a helper for generic API calls, used by postgres_resource
func (c *NubesClient) Post(path string, payload interface{}, target interface{}) error {
url := fmt.Sprintf("%s%s", c.ApiEndpoint, path)
var reqBody io.Reader
if payload != nil {
data, err := json.Marshal(payload)
if err != nil {
return err
}
reqBody = bytes.NewBuffer(data)
}
req, err := http.NewRequestWithContext(context.Background(), "POST", url, reqBody)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
if c.ApiToken != "" {
req.Header.Set("Authorization", "Bearer "+c.ApiToken)
}
resp, err := c.HttpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 && resp.StatusCode != 201 {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("status %d: %s", resp.StatusCode, string(body))
}
if target != nil {
return json.NewDecoder(resp.Body).Decode(target)
}
return nil
}
func (c *NubesClient) GetInstanceStateDetails(ctx context.Context, instanceUid string) (map[string]interface{}, error) {
url := fmt.Sprintf("%s/instances/%s", c.ApiEndpoint, instanceUid)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
if c.ApiToken != "" {
req.Header.Set("Authorization", "Bearer "+c.ApiToken)
}
resp, err := c.HttpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("status %d", resp.StatusCode)
}
var res map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
return nil, err
}
if inst, ok := res["instance"].(map[string]interface{}); ok {
if state, ok := inst["state"].(map[string]interface{}); ok {
return state, nil
}
}
return nil, fmt.Errorf("could not find instance.state in response")
}
func (c *NubesClient) GetOperationIdForInstance(ctx context.Context, instanceUid string, opName string) (int, error) {
url := fmt.Sprintf("%s/instances/%s", c.ApiEndpoint, instanceUid)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return 0, err
}
if c.ApiToken != "" {
req.Header.Set("Authorization", "Bearer "+c.ApiToken)
}
resp, err := c.HttpClient.Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return 0, fmt.Errorf("status %d", resp.StatusCode)
}
var res map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
return 0, err
}
inst, ok := res["instance"].(map[string]interface{})
if !ok {
return 0, fmt.Errorf("instance field missing")
}
availableOps, ok := inst["availableOperations"].([]interface{})
if !ok {
return 0, fmt.Errorf("availableOperations missing or empty")
}
for _, item := range availableOps {
opMap, ok := item.(map[string]interface{})
if !ok {
continue
}
var name string
var id int
if n, ok := opMap["name"].(string); ok {
name = n
}
if n, ok := opMap["operation"].(string); ok {
name = n
}
if i, ok := opMap["svcOperationId"].(float64); ok {
id = int(i)
}
if svcOp, ok := opMap["svcOperation"].(map[string]interface{}); ok {
if n, ok := svcOp["name"].(string); ok {
name = n
}
if idFloat, ok := svcOp["id"].(float64); ok {
id = int(idFloat)
}
}
if strings.EqualFold(name, opName) {
return id, nil
}
}
return 0, fmt.Errorf("operation '%s' not found in available operations", opName)
}
type GetOperationResponse struct {
InstanceOperation OperationResponse `json:"instanceOperation"`
}
// OperationResponse представляет собой полное состояние операции из Deck API.
// Все поля получены путем анализа HAR файлов (dummy.har, vdc.har) и документации платформы.
type OperationResponse struct {
InstanceOperationUid string `json:"instanceOperationUid"` // Уникальный идентификатор операции (UUID)
InstanceUid string `json:"instanceUid"` // Идентификатор инстанса, над которым идет работа
Operation string `json:"operation"` // Имя операции (create, modify, suspend, и т.д.)
IsInProgress bool `json:"isInProgress"` // true, если Jenkins-джоб выполняется в данный момент
IsPending bool `json:"isPending"` // true, если операция стоит в очереди (ждем слота в Jenkins)
IsSuccessful *bool `json:"isSuccessful"` // "Зеленая галка" платформы. Появляется ПОСЛЕ dtFinish.
DtFinish *string `json:"dtFinish"` // Штамп времени окончания (ГГГГ-ММ-ДД...). Не null = ОПЕРАЦИЯ ЗАВЕРШЕНА.
DtCreated *string `json:"dtCreated"` // Время создания записи об операции
DtUpdated *string `json:"dtUpdated"` // Время последнего обновления записи
DtSubmit *string `json:"dtSubmit"` // Когда кнопка была нажата (или API вызван)
DtStart *string `json:"dtStart"` // Когда реально начался Jenkins-джоб
SubmitResult *string `json:"submitResult"` // HTTP-код первичной регистрации (обычно "201")
Duration *float64 `json:"duration"` // Общее время выполнения в секундах
ErrorLog *string `json:"errorLog"` // Текст ошибки, если операция упала
UpdaterId *int `json:"updaterId"` // ID пользователя, запустившего операцию
UpdaterLogin *string `json:"updaterLogin"` // Логин инициатора
UpdaterShortname *string `json:"updaterShortname"` // Инициалы инициатора (например, "Н. Ф.")
DisplayName *string `json:"displayName"` // Имя инстанса на момент операции
ServiceId *int `json:"serviceId"` // ID сервиса (1 - Болванка, 96 - PG Admin и т.д.)
Svc *string `json:"svc"` // Текстовое имя сервиса
SvcOperationId *int `json:"svcOperationId"` // Внутренний ID операции в каталоге
Man *string `json:"man"` // Мануал/описание операции (иногда содержит Markdown)
CfsParams []CfsParam `json:"cfsParams"` // Список всех параметров (конфигурация)
Stages []ApiStage `json:"stages"` // Этапы выполнения джоба (подготовка, секреты...)
State any `json:"state"` // Результирующее состояние (выходные данные джоба)
}
// ApiStage представляет этап выполнения операции в Jenkins
type ApiStage struct {
InstanceOperationStageUid string `json:"instanceOperationStageUid"`
Stage string `json:"stage"` // Название (например, "Подготовка среды")
IsSuccessful bool `json:"isSuccessful"` // Успех конкретного этапа
DtStart *string `json:"dtStart"`
DtFinish *string `json:"dtFinish"`
Duration float64 `json:"duration"`
StageMsg *string `json:"stageMsg"` // Лог этапа (часто JSON в строке)
}
type CfsParam struct {
InstanceOperationCfsParamUid string `json:"instanceOperationCfsParamUid"`
SvcOperationCfsParamId int `json:"svcOperationCfsParamId"`
ParamValue *string `json:"paramValue"`
DefaultValue *string `json:"defaultValue"`
DataType string `json:"dataType"`
Name string `json:"name"`
Code string `json:"code"`
SvcOperationCfsParam string `json:"svcOperationCfsParam"`
}
func (c *NubesClient) GetInstanceOperation(ctx context.Context, opUid string) (*OperationResponse, error) {
// ВАЖНО: Список полей максимально расширен на основе HAR (dummy.har, vdc.har).
// Эти поля позволяют видеть полную картину происходящего на платформе.
fields := "instanceOperationUid,instanceUid,state,stages,isSuccessful,dtCreated,dtUpdated,dtStart,dtFinish,operation,svcOperationId,svc,displayName,submitResult,duration,errorLog,updaterShortname,man,isInProgress,isPending,dtSubmit"
url := fmt.Sprintf("%s/instanceOperations/%s?fields=%s", c.ApiEndpoint, opUid, fields)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
if c.ApiToken != "" {
req.Header.Set("Authorization", "Bearer "+c.ApiToken)
}
resp, err := c.HttpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != 200 {
return nil, fmt.Errorf("status %d: %s", resp.StatusCode, string(body))
}
var res GetOperationResponse
if err := json.Unmarshal(body, &res); err != nil {
return nil, fmt.Errorf("json unmarshal failed: %w, body: %s", err, string(body))
}
return &res.InstanceOperation, nil
}
func (c *NubesClient) RunAction(ctx context.Context, instanceUid string, action string) error {
state, err := c.GetInstanceState(ctx, instanceUid)
if err != nil {
return err
}
var opId int
for _, op := range state.AvailableOperations {
if op.Operation == action {
opId = op.SvcOperationId
break
}
}
if opId == 0 {
return fmt.Errorf("action %s not available for instance %s", action, instanceUid)
}
// 1. Create Op
payload := map[string]interface{}{
"instanceUid": instanceUid,
"svcOperationId": opId,
"operation": action,
}
opUid, err := c.postIgnoreResponse(ctx, "/instanceOperations", payload, true)
if err != nil {
return fmt.Errorf("failed to create %s operation: %w", action, err)
}
if opUid == "" {
return fmt.Errorf("failed to get operation UID for %s", action)
}
// 2. Run Op
runUrl := fmt.Sprintf("/instanceOperations/%s/run", opUid)
_, err = c.postIgnoreResponse(ctx, runUrl, map[string]interface{}{}, false)
return err
}
type InstanceSummary struct {
InstanceUid string `json:"instanceUid"`
DisplayName string `json:"displayName"`
ServiceId int `json:"serviceId"`
Svc string `json:"svc"`
}
type InstancesListResponse struct {
Results []InstanceSummary `json:"results"`
}
func (c *NubesClient) GetInstances(ctx context.Context) ([]InstanceSummary, error) {
var allInstances []InstanceSummary
page := 1
for {
url := fmt.Sprintf("%s/instances?page=%d&size=100", c.ApiEndpoint, page)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
if c.ApiToken != "" {
req.Header.Set("Authorization", "Bearer "+c.ApiToken)
}
resp, err := c.HttpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("status %d", resp.StatusCode)
}
var res InstancesListResponse
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
return nil, err
}
if len(res.Results) == 0 {
break
}
allInstances = append(allInstances, res.Results...)
page++
// Safety break to prevent infinite loops if API behaves weirdly
if page > 100 {
break
}
}
return allInstances, nil
}
func (c *NubesClient) GetInstanceFull(ctx context.Context, instanceUid string) (map[string]interface{}, error) {
url := fmt.Sprintf("%s/instances/%s", c.ApiEndpoint, instanceUid)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
if c.ApiToken != "" {
req.Header.Set("Authorization", "Bearer "+c.ApiToken)
}
resp, err := c.HttpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("status %d", resp.StatusCode)
}
var res map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
return nil, err
}
if inst, ok := res["instance"].(map[string]interface{}); ok {
return inst, nil
}
return nil, fmt.Errorf("instance field missing in response")
}
-144
View File
@@ -1,144 +0,0 @@
package provider
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var _ datasource.DataSource = &EdgeDataSource{}
func NewEdgeDataSource() datasource.DataSource {
return &EdgeDataSource{}
}
type EdgeDataSource struct {
client *NubesClient
}
type EdgeDataSourceModel struct {
ID types.String `tfsdk:"id"`
DisplayName types.String `tfsdk:"display_name"`
Status types.String `tfsdk:"status"`
Description types.String `tfsdk:"description"`
}
func (d *EdgeDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_edge"
}
func (d *EdgeDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Data source для получения информации о существующем Edge Gateway по имени",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
MarkdownDescription: "UUID Edge Gateway",
Computed: true,
},
"display_name": schema.StringAttribute{
MarkdownDescription: "Имя Edge Gateway для поиска",
Required: true,
},
"status": schema.StringAttribute{
MarkdownDescription: "Статус Edge Gateway",
Computed: true,
},
"description": schema.StringAttribute{
MarkdownDescription: "Описание Edge Gateway",
Computed: true,
},
},
}
}
func (d *EdgeDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*NubesClient)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *NubesClient, got: %T", req.ProviderData),
)
return
}
d.client = client
}
func (d *EdgeDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data EdgeDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
httpReq, err := http.NewRequestWithContext(ctx, "GET", d.client.ApiEndpoint+"/instances", nil)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create request: %s", err))
return
}
httpReq.Header.Set("Authorization", "Bearer "+strings.TrimSpace(d.client.ApiToken))
httpReq.Header.Set("Content-Type", "application/json")
httpResp, err := d.client.HttpClient.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read instances: %s", err))
return
}
defer httpResp.Body.Close()
body, err := io.ReadAll(httpResp.Body)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read response: %s", err))
return
}
if httpResp.StatusCode != http.StatusOK {
resp.Diagnostics.AddError("API Error", fmt.Sprintf("GET /instances returned %d: %s", httpResp.StatusCode, string(body)))
return
}
var instancesResp struct {
Results []struct {
ID string `json:"id"`
DisplayName string `json:"name"`
Status string `json:"status"`
Description string `json:"desc"`
Service string `json:"svc"`
} `json:"results"`
}
if err := json.Unmarshal(body, &instancesResp); err != nil {
resp.Diagnostics.AddError("Parse Error", fmt.Sprintf("Unable to parse response: %s", err))
return
}
searchName := data.DisplayName.ValueString()
for _, instance := range instancesResp.Results {
if instance.Service == "Сетевой шлюз периметра (Edge)" && instance.DisplayName == searchName {
data.ID = types.StringValue(instance.ID)
data.Status = types.StringValue(instance.Status)
data.Description = types.StringValue(instance.Description)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
return
}
}
resp.Diagnostics.AddError(
"Edge Gateway Not Found",
fmt.Sprintf("Edge Gateway с именем '%s' не найден. Проверьте имя или создайте новый Edge.", searchName),
)
}
-674
View File
@@ -1,674 +0,0 @@
package provider
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"time"
"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/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var _ resource.Resource = &EdgeResource{}
var _ resource.ResourceWithImportState = &EdgeResource{}
func NewEdgeResource() resource.Resource {
return &EdgeResource{}
}
type EdgeResource struct {
client *NubesClient
}
type EdgeResourceModel struct {
ID types.String `tfsdk:"id"`
DisplayName types.String `tfsdk:"display_name"`
Description types.String `tfsdk:"description"`
VdcUID types.String `tfsdk:"vdc_uid"`
EdgeType types.String `tfsdk:"edge_type"`
EdgeCount types.Int64 `tfsdk:"edge_count"`
EnableAdvanced types.Bool `tfsdk:"enable_advanced"`
ExternalNetwork types.String `tfsdk:"external_network"`
Status types.String `tfsdk:"status"`
}
func (r *EdgeResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_edge"
}
func (r *EdgeResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Nubes Edge Gateway resource",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Edge identifier (UUID)",
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"display_name": schema.StringAttribute{
MarkdownDescription: "Edge display name",
Required: true,
},
"description": schema.StringAttribute{
MarkdownDescription: "Edge description",
Optional: true,
},
"vdc_uid": schema.StringAttribute{
MarkdownDescription: "VDC UUID (parameter ID 8)",
Required: true,
},
"edge_type": schema.StringAttribute{
MarkdownDescription: "Edge type: vdc (parameter ID 621)",
Optional: true,
Computed: true,
},
"edge_count": schema.Int64Attribute{
MarkdownDescription: "Number of Edge Gateways (parameter ID 341)",
Optional: true,
Computed: true,
},
"enable_advanced": schema.BoolAttribute{
MarkdownDescription: "Enable advanced features (parameter ID 340)",
Optional: true,
Computed: true,
},
"external_network": schema.StringAttribute{
MarkdownDescription: "External network name (parameter ID 367)",
Optional: true,
},
"status": schema.StringAttribute{
MarkdownDescription: "Current status of the Edge",
Computed: true,
},
},
}
}
func (r *EdgeResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*NubesClient)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *NubesClient, got: %T", req.ProviderData),
)
return
}
r.client = client
}
func (r *EdgeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data EdgeResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Set defaults
if data.EdgeType.IsNull() || data.EdgeType.IsUnknown() {
data.EdgeType = types.StringValue("vdc")
}
if data.EdgeCount.IsNull() || data.EdgeCount.IsUnknown() {
data.EdgeCount = types.Int64Value(1)
}
if data.EnableAdvanced.IsNull() || data.EnableAdvanced.IsUnknown() {
data.EnableAdvanced = types.BoolValue(false)
}
// Step 1: Create instance
createReq := CreateInstanceRequest{
ServiceId: 22, // Edge service ID
DisplayName: data.DisplayName.ValueString(),
Descr: data.Description.ValueString(),
}
jsonData, err := json.Marshal(createReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to marshal request: %s", err))
return
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", r.client.ApiEndpoint+"/instances", bytes.NewBuffer(jsonData))
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create request: %s", err))
return
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create instance: %s", err))
return
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(httpResp.Body)
resp.Diagnostics.AddError(
"API Error",
fmt.Sprintf("Create instance failed with status %d: %s", httpResp.StatusCode, string(body)),
)
return
}
location := httpResp.Header.Get("Location")
if location == "" {
resp.Diagnostics.AddError("API Error", "No Location header in response")
return
}
instanceId := location[2:]
data.ID = types.StringValue(instanceId)
// Step 2: Create operation
operationReq := CreateOperationRequest{
InstanceUid: instanceId,
Operation: "create",
}
jsonData, err = json.Marshal(operationReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to marshal operation request: %s", err))
return
}
httpReq, err = http.NewRequestWithContext(ctx, "POST", r.client.ApiEndpoint+"/instanceOperations", bytes.NewBuffer(jsonData))
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create operation request: %s", err))
return
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err = r.client.HttpClient.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create operation: %s", err))
return
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(httpResp.Body)
resp.Diagnostics.AddError(
"API Error",
fmt.Sprintf("Create operation failed with status %d: %s", httpResp.StatusCode, string(body)),
)
return
}
location = httpResp.Header.Get("Location")
if location == "" {
resp.Diagnostics.AddError("API Error", "No Location header in operation response")
return
}
operationId := location[2:]
// Step 3: Submit operation parameters and run
if err := r.submitOperationParams(ctx, operationId, data); err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to submit operation parameters: %s", err))
return
}
// Step 4: Wait for operation completion and instance running status
if err := r.waitForOperationAndInstanceStatus(ctx, operationId, instanceId, 10*time.Minute); err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Instance failed to reach running status: %s", err))
return
}
readData, err := r.readInstance(ctx, instanceId)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read instance after creation: %s", err))
return
}
data.Status = types.StringValue(readData.Status)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *EdgeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data EdgeResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
instanceResp, err := r.readInstance(ctx, data.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read instance: %s", err))
return
}
data.Status = types.StringValue(instanceResp.Status)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *EdgeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data EdgeResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
operationReq := CreateOperationRequest{
InstanceUid: data.ID.ValueString(),
Operation: "modify",
}
jsonData, err := json.Marshal(operationReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to marshal operation request: %s", err))
return
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", r.client.ApiEndpoint+"/instanceOperations", bytes.NewBuffer(jsonData))
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create operation request: %s", err))
return
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update instance: %s", err))
return
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(httpResp.Body)
resp.Diagnostics.AddError(
"API Error",
fmt.Sprintf("Modify operation failed with status %d: %s", httpResp.StatusCode, string(body)),
)
return
}
location := httpResp.Header.Get("Location")
if location != "" {
operationId := location[2:]
if err := r.submitOperationParams(ctx, operationId, data); err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to submit operation parameters: %s", err))
return
}
}
time.Sleep(5 * time.Second)
readData, err := r.readInstance(ctx, data.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read instance after update: %s", err))
return
}
data.Status = types.StringValue(readData.Status)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *EdgeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data EdgeResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
operationReq := CreateOperationRequest{
InstanceUid: data.ID.ValueString(),
Operation: "delete",
}
jsonData, err := json.Marshal(operationReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to marshal operation request: %s", err))
return
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", r.client.ApiEndpoint+"/instanceOperations", bytes.NewBuffer(jsonData))
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create operation request: %s", err))
return
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete instance: %s", err))
return
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusCreated && httpResp.StatusCode != http.StatusNotFound {
body, _ := io.ReadAll(httpResp.Body)
resp.Diagnostics.AddError(
"API Error",
fmt.Sprintf("Delete operation failed with status %d: %s", httpResp.StatusCode, string(body)),
)
return
}
location := httpResp.Header.Get("Location")
if location != "" {
operationId := location[2:]
if err := r.submitOperationParams(ctx, operationId, data); err != nil {
resp.Diagnostics.AddWarning("Client Warning", fmt.Sprintf("Unable to submit operation parameters: %s", err))
}
}
}
func (r *EdgeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}
func (r *EdgeResource) readInstance(ctx context.Context, id string) (*InstanceResponse, error) {
httpReq, err := http.NewRequestWithContext(ctx, "GET", r.client.ApiEndpoint+"/instances/"+id, nil)
if err != nil {
return nil, err
}
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
return nil, err
}
defer httpResp.Body.Close()
body, err := io.ReadAll(httpResp.Body)
if err != nil {
return nil, err
}
if httpResp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("read request failed with status %d: %s", httpResp.StatusCode, string(body))
}
var instanceResp InstanceResponse
if err := json.Unmarshal(body, &instanceResp); err != nil {
return nil, err
}
return &instanceResp, nil
}
func (r *EdgeResource) submitOperationParams(ctx context.Context, operationUid string, data EdgeResourceModel) error {
// Get operation details
httpReq, err := http.NewRequestWithContext(ctx, "GET",
r.client.ApiEndpoint+"/instanceOperations/"+operationUid+"?fields=cfsParams", nil)
if err != nil {
return fmt.Errorf("unable to create request: %s", err)
}
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
return fmt.Errorf("unable to get operation: %s", err)
}
defer httpResp.Body.Close()
body, err := io.ReadAll(httpResp.Body)
if err != nil {
return fmt.Errorf("unable to read response: %s", err)
}
if httpResp.StatusCode != http.StatusOK {
return fmt.Errorf("get operation failed with status %d: %s", httpResp.StatusCode, string(body))
}
var getResp GetOperationResponse
if err := json.Unmarshal(body, &getResp); err != nil {
return fmt.Errorf("unable to unmarshal response: %s", err)
}
operationResp := getResp.InstanceOperation
// Submit each parameter
for _, param := range operationResp.CfsParams {
valToSend := ""
// Map Edge parameters by ID
switch param.SvcOperationCfsParamId {
case 8: // vdcUid
if !data.VdcUID.IsNull() && !data.VdcUID.IsUnknown() {
valToSend = data.VdcUID.ValueString()
}
case 340: // enableAdvanced
if !data.EnableAdvanced.IsNull() && !data.EnableAdvanced.IsUnknown() {
valToSend = fmt.Sprintf("%t", data.EnableAdvanced.ValueBool())
}
case 341: // edgeCount
if !data.EdgeCount.IsNull() && !data.EdgeCount.IsUnknown() {
valToSend = fmt.Sprintf("%d", data.EdgeCount.ValueInt64())
}
case 367: // externalNetwork
if !data.ExternalNetwork.IsNull() && !data.ExternalNetwork.IsUnknown() {
valToSend = data.ExternalNetwork.ValueString()
}
case 621: // edgeType
if !data.EdgeType.IsNull() && !data.EdgeType.IsUnknown() {
valToSend = data.EdgeType.ValueString()
}
case 622: // unknown optional parameter
// Leave empty or use default
default:
// Use existing or default value for unknown parameters
if param.ParamValue != nil {
valToSend = *param.ParamValue
} else if param.DefaultValue != nil {
valToSend = *param.DefaultValue
}
}
// Fix specific data type formatting
if valToSend == "" || valToSend == "\"\"" {
if param.DataType == "map" || param.DataType == "json" {
valToSend = "{}"
} else if param.DataType == "array" || param.DataType == "list" {
valToSend = "[]"
}
}
paramReq := CreateCfsParamRequest{
InstanceOperationUid: operationUid,
SvcOperationCfsParamId: param.SvcOperationCfsParamId,
ParamValue: valToSend,
}
jsonData, err := json.Marshal(paramReq)
if err != nil {
return fmt.Errorf("unable to marshal param request: %s", err)
}
httpReq, err = http.NewRequestWithContext(ctx, "POST",
r.client.ApiEndpoint+"/instanceOperationCfsParams", bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("unable to create param request: %s", err)
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err = r.client.HttpClient.Do(httpReq)
if err != nil {
return fmt.Errorf("unable to submit parameter: %s", err)
}
respBody, _ := io.ReadAll(httpResp.Body)
httpResp.Body.Close()
if httpResp.StatusCode != http.StatusCreated &&
httpResp.StatusCode != http.StatusOK &&
httpResp.StatusCode != http.StatusNoContent {
return fmt.Errorf("submit parameter id %d failed with status %d: %s",
param.SvcOperationCfsParamId, httpResp.StatusCode, string(respBody))
}
}
// Run the operation
runReq, err := http.NewRequestWithContext(ctx, "POST",
r.client.ApiEndpoint+"/instanceOperations/"+operationUid+"/run", bytes.NewBuffer([]byte("{}")))
if err != nil {
return fmt.Errorf("unable to create run request: %s", err)
}
runReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
runReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
runResp, err := r.client.HttpClient.Do(runReq)
if err != nil {
return fmt.Errorf("unable to run operation: %s", err)
}
defer runResp.Body.Close()
if runResp.StatusCode != http.StatusOK &&
runResp.StatusCode != http.StatusNoContent &&
runResp.StatusCode != http.StatusCreated {
runBody, _ := io.ReadAll(runResp.Body)
return fmt.Errorf("run operation failed with status %d: %s",
runResp.StatusCode, string(runBody))
}
return nil
}
func (r *EdgeResource) waitForOperationAndInstanceStatus(ctx context.Context, operationId string, instanceId string, timeout time.Duration) error {
deadline := time.Now().Add(timeout)
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
log.Printf("[DEBUG] Starting two-stage polling: operationId=%s, instanceId=%s, timeout=%v", operationId, instanceId, timeout)
// Шаг 1: Ждём завершения операции
operationLoop:
for {
select {
case <-ctx.Done():
return fmt.Errorf("context cancelled")
case <-ticker.C:
if time.Now().After(deadline) {
return fmt.Errorf("timeout waiting for operation to complete")
}
log.Printf("[DEBUG] Polling operation status: operationId=%s", operationId)
// Проверяем статус операции
httpReq, err := http.NewRequestWithContext(ctx, "GET",
r.client.ApiEndpoint+"/instanceOperations/"+operationId, nil)
if err != nil {
return fmt.Errorf("failed to create request: %s", err)
}
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
return fmt.Errorf("failed to check operation status: %s", err)
}
body, _ := io.ReadAll(httpResp.Body)
httpResp.Body.Close()
if httpResp.StatusCode != http.StatusOK {
return fmt.Errorf("get operation failed with status %d", httpResp.StatusCode)
}
var opResp struct {
InstanceOperation struct {
IsInProgress bool `json:"isInProgress"`
IsPending bool `json:"isPending"`
} `json:"instanceOperation"`
}
if err := json.Unmarshal(body, &opResp); err != nil {
return fmt.Errorf("failed to parse operation response: %s", err)
}
log.Printf("[DEBUG] Operation status: isInProgress=%v, isPending=%v", opResp.InstanceOperation.IsInProgress, opResp.InstanceOperation.IsPending)
// Операция завершена когда isInProgress=false И isPending=false
if !opResp.InstanceOperation.IsInProgress && !opResp.InstanceOperation.IsPending {
log.Printf("[DEBUG] Operation completed, moving to instance status check")
break operationLoop
}
}
}
// Шаг 2: Проверяем статус instance
ticker2 := time.NewTicker(5 * time.Second)
defer ticker2.Stop()
log.Printf("[DEBUG] Starting instance status polling")
for {
select {
case <-ctx.Done():
return fmt.Errorf("context cancelled")
case <-ticker2.C:
if time.Now().After(deadline) {
return fmt.Errorf("timeout waiting for instance to become running")
}
log.Printf("[DEBUG] Polling instance status: instanceId=%s", instanceId)
instance, err := r.readInstance(ctx, instanceId)
if err != nil {
return fmt.Errorf("failed to check instance status: %s", err)
}
if instance.Status == "running" {
return nil
}
if instance.Status == "error" || instance.Status == "failed" {
return fmt.Errorf("instance entered error state: %s", instance.Status)
}
// Continue waiting for other statuses
}
}
}
-517
View File
@@ -1,517 +0,0 @@
package provider
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var _ resource.Resource = &OrganizationResource{}
func NewOrganizationResource() resource.Resource {
return &OrganizationResource{}
}
type OrganizationResource struct {
client *NubesClient
}
type OrganizationResourceModel struct {
ID types.String `tfsdk:"id"`
DisplayName types.String `tfsdk:"display_name"`
Description types.String `tfsdk:"description"`
Platform types.String `tfsdk:"platform"`
OrganizationType types.String `tfsdk:"organization_type"`
ResourceRealm types.String `tfsdk:"resource_realm"`
Status types.String `tfsdk:"status"`
}
func (r *OrganizationResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_organization"
}
func (r *OrganizationResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Cloud Director Organization resource - корневая сущность для управления облачной инфраструктурой",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
MarkdownDescription: "UUID организации",
},
"display_name": schema.StringAttribute{
Required: true,
MarkdownDescription: "Отображаемое имя организации",
},
"description": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Описание организации",
},
"platform": schema.StringAttribute{
Required: true,
MarkdownDescription: "Платформа для развертывания (например: ngcloud.ru)",
},
"organization_type": schema.StringAttribute{
Required: true,
MarkdownDescription: "Тип организации: iaas (с доступом к Cloud Director) или saas (управляется Nubes)",
},
"resource_realm": schema.StringAttribute{
Optional: true,
Computed: true,
MarkdownDescription: "Realm ресурса (по умолчанию: vcd)",
},
"status": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Текущий статус организации",
},
},
}
}
func (r *OrganizationResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*NubesClient)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *NubesClient, got: %T", req.ProviderData),
)
return
}
r.client = client
}
func (r *OrganizationResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data OrganizationResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Set default resource_realm if not provided
if data.ResourceRealm.IsNull() || data.ResourceRealm.IsUnknown() {
data.ResourceRealm = types.StringValue("vcd")
}
// Step 1: Create instance
createReq := CreateInstanceRequest{
ServiceId: 19, // Cloud Director Organization
DisplayName: data.DisplayName.ValueString(),
Descr: data.Description.ValueString(),
}
jsonData, err := json.Marshal(createReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to marshal create request: %s", err))
return
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", r.client.ApiEndpoint+"/instances", bytes.NewBuffer(jsonData))
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create request: %s", err))
return
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create instance: %s", err))
return
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(httpResp.Body)
resp.Diagnostics.AddError(
"API Error",
fmt.Sprintf("Create instance failed with status %d: %s", httpResp.StatusCode, string(body)),
)
return
}
// Extract instance ID from Location header
location := httpResp.Header.Get("Location")
if location == "" {
resp.Diagnostics.AddError("API Error", "No Location header in response")
return
}
instanceId := location[2:] // Remove "./"
data.ID = types.StringValue(instanceId)
// Step 2: Create operation
operationReq := CreateOperationRequest{
InstanceUid: instanceId,
Operation: "create",
}
jsonData, err = json.Marshal(operationReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to marshal operation request: %s", err))
return
}
httpReq, err = http.NewRequestWithContext(ctx, "POST", r.client.ApiEndpoint+"/instanceOperations", bytes.NewBuffer(jsonData))
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create operation request: %s", err))
return
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err = r.client.HttpClient.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create operation: %s", err))
return
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(httpResp.Body)
resp.Diagnostics.AddError(
"API Error",
fmt.Sprintf("Create operation failed with status %d: %s", httpResp.StatusCode, string(body)),
)
return
}
// Extract operation ID from Location header
location = httpResp.Header.Get("Location")
if location == "" {
resp.Diagnostics.AddError("API Error", "No Location header in operation response")
return
}
operationId := location[2:] // Remove "./"
// Step 3: Submit operation parameters
if err := r.submitOrganizationParams(ctx, operationId, data); err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to submit operation parameters: %s", err))
return
}
// Step 4: Wait for completion and read status
time.Sleep(5 * time.Second)
readData, err := r.readInstance(ctx, instanceId)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read instance after creation: %s", err))
return
}
data.Status = types.StringValue(readData.Status)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *OrganizationResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data OrganizationResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
instanceResp, err := r.readInstance(ctx, data.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read instance: %s", err))
return
}
data.Status = types.StringValue(instanceResp.Status)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *OrganizationResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data OrganizationResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Trigger modify operation
operationReq := CreateOperationRequest{
InstanceUid: data.ID.ValueString(),
Operation: "modify",
}
jsonData, err := json.Marshal(operationReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to marshal operation request: %s", err))
return
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", r.client.ApiEndpoint+"/instanceOperations", bytes.NewBuffer(jsonData))
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create operation request: %s", err))
return
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update instance: %s", err))
return
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(httpResp.Body)
resp.Diagnostics.AddError(
"API Error",
fmt.Sprintf("Modify operation failed with status %d: %s", httpResp.StatusCode, string(body)),
)
return
}
// Extract operation ID and submit parameters
location := httpResp.Header.Get("Location")
if location != "" {
operationId := location[2:]
if err := r.submitOrganizationParams(ctx, operationId, data); err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to submit operation parameters: %s", err))
return
}
}
time.Sleep(5 * time.Second)
readData, err := r.readInstance(ctx, data.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read instance after update: %s", err))
return
}
data.Status = types.StringValue(readData.Status)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *OrganizationResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data OrganizationResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// First suspend the organization
suspendReq := CreateOperationRequest{
InstanceUid: data.ID.ValueString(),
Operation: "suspend",
}
jsonData, err := json.Marshal(suspendReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to marshal suspend request: %s", err))
return
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", r.client.ApiEndpoint+"/instanceOperations", bytes.NewBuffer(jsonData))
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create suspend request: %s", err))
return
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to suspend organization: %s", err))
return
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(httpResp.Body)
resp.Diagnostics.AddError(
"API Error",
fmt.Sprintf("Suspend operation failed with status %d: %s", httpResp.StatusCode, string(body)),
)
return
}
// Note: Actual deletion requires 14 days wait after suspend
// For now we just suspend and remove from state
resp.Diagnostics.AddWarning(
"Organization Suspended",
"Organization has been suspended. Actual deletion requires 14 days wait period and must be performed manually.",
)
}
func (r *OrganizationResource) readInstance(ctx context.Context, instanceId string) (*InstanceResponse, error) {
httpReq, err := http.NewRequestWithContext(ctx, "GET", r.client.ApiEndpoint+"/instances/"+instanceId, nil)
if err != nil {
return nil, err
}
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
return nil, err
}
defer httpResp.Body.Close()
body, err := io.ReadAll(httpResp.Body)
if err != nil {
return nil, err
}
if httpResp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("read request failed with status %d: %s", httpResp.StatusCode, string(body))
}
var instanceResp InstanceResponse
if err := json.Unmarshal(body, &instanceResp); err != nil {
return nil, err
}
return &instanceResp, nil
}
func (r *OrganizationResource) submitOrganizationParams(ctx context.Context, operationUid string, data OrganizationResourceModel) error {
// Step 1: Get operation details with parameters
httpReq, err := http.NewRequestWithContext(ctx, "GET",
r.client.ApiEndpoint+"/instanceOperations/"+operationUid+"?fields=cfsParams", nil)
if err != nil {
return fmt.Errorf("unable to create request: %s", err)
}
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
return fmt.Errorf("unable to get operation: %s", err)
}
defer httpResp.Body.Close()
body, err := io.ReadAll(httpResp.Body)
if err != nil {
return fmt.Errorf("unable to read response: %s", err)
}
if httpResp.StatusCode != http.StatusOK {
return fmt.Errorf("get operation failed with status %d: %s", httpResp.StatusCode, string(body))
}
var getResp GetOperationResponse
if err := json.Unmarshal(body, &getResp); err != nil {
return fmt.Errorf("unable to unmarshal response: %s", err)
}
operationResp := getResp.InstanceOperation
// Step 2: Submit each parameter
for _, param := range operationResp.CfsParams {
valToSend := ""
// Map parameters by ID
switch param.SvcOperationCfsParamId {
case 418: // platform
if !data.Platform.IsNull() && !data.Platform.IsUnknown() {
valToSend = data.Platform.ValueString()
}
case 556: // organizationType
if !data.OrganizationType.IsNull() && !data.OrganizationType.IsUnknown() {
valToSend = data.OrganizationType.ValueString()
}
default:
// Use existing or default value
if param.ParamValue != nil {
valToSend = *param.ParamValue
} else if param.DefaultValue != nil {
valToSend = *param.DefaultValue
}
}
paramReq := CreateCfsParamRequest{
InstanceOperationUid: operationUid,
SvcOperationCfsParamId: param.SvcOperationCfsParamId,
ParamValue: valToSend,
}
jsonData, err := json.Marshal(paramReq)
if err != nil {
return fmt.Errorf("unable to marshal param request: %s", err)
}
httpReq, err = http.NewRequestWithContext(ctx, "POST",
r.client.ApiEndpoint+"/instanceOperationCfsParams", bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("unable to create param request: %s", err)
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err = r.client.HttpClient.Do(httpReq)
if err != nil {
return fmt.Errorf("unable to submit param: %s", err)
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(httpResp.Body)
return fmt.Errorf("submit parameter id %d failed with status %d: %s",
param.SvcOperationCfsParamId, httpResp.StatusCode, string(body))
}
}
// Step 3: Run the operation
httpReq, err = http.NewRequestWithContext(ctx, "POST",
r.client.ApiEndpoint+"/instanceOperations/"+operationUid+"/run", bytes.NewBuffer([]byte("{}")))
if err != nil {
return fmt.Errorf("unable to create run request: %s", err)
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err = r.client.HttpClient.Do(httpReq)
if err != nil {
return fmt.Errorf("unable to run operation: %s", err)
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusOK && httpResp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(httpResp.Body)
return fmt.Errorf("run operation failed with status %d: %s", httpResp.StatusCode, string(body))
}
return nil
}
-359
View File
@@ -1,359 +0,0 @@
package provider
import (
"context"
"fmt"
"strconv"
"strings"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
)
var _ resource.Resource = &PgAdminResource{}
var _ resource.ResourceWithImportState = &PgAdminResource{}
type PgAdminResource struct {
client *NubesClient
}
func NewPgAdminResource() resource.Resource {
return &PgAdminResource{}
}
type PgAdminResourceModel struct {
ID types.String `tfsdk:"id"`
Domain types.String `tfsdk:"domain"`
ResourceRealm types.String `tfsdk:"resource_realm"`
Cpu types.Int64 `tfsdk:"cpu"`
Memory types.Int64 `tfsdk:"memory"`
Disk types.Int64 `tfsdk:"disk"`
Email types.String `tfsdk:"email"`
Password types.String `tfsdk:"password"`
DeletionProtection types.Bool `tfsdk:"deletion_protection"`
}
func (r *PgAdminResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_pgadmin"
}
func (r *PgAdminResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Manages a pgAdmin instance.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"domain": schema.StringAttribute{
Required: true,
Description: "Domain name (param 164)",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"resource_realm": schema.StringAttribute{
Required: true,
Description: "Deployment platform (param 169). E.g. k8s-3.ext.nubes.ru",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"cpu": schema.Int64Attribute{
Optional: true,
Computed: true,
Default: int64default.StaticInt64(200),
Description: "CPU quota in milicores (param 165)",
},
"memory": schema.Int64Attribute{
Optional: true,
Computed: true,
Default: int64default.StaticInt64(256),
Description: "Memory quota in MB (param 166)",
},
"disk": schema.Int64Attribute{
Optional: true,
Computed: true,
Default: int64default.StaticInt64(1),
Description: "Disk size in GB (param 167)",
},
"email": schema.StringAttribute{
Required: true,
Description: "Login email (param 170)",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"password": schema.StringAttribute{
Required: true,
Sensitive: true,
Description: "Login password (param 171)",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"deletion_protection": schema.BoolAttribute{
MarkdownDescription: "If true, the resource will only be removed from Terraform state upon destroy, but will remain in the cloud. If false, destroy will trigger 'suspend' in Nubes.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(true),
},
},
}
}
func (r *PgAdminResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*NubesClient)
if !ok {
resp.Diagnostics.AddError("Unexpected Resource Configure Type", fmt.Sprintf("Expected *NubesClient, got: %T", req.ProviderData))
return
}
r.client = client
}
func (r *PgAdminResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan PgAdminResourceModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Creating pgAdmin resource...")
// Params based on pg_admin.har analysis
params := []InstanceParam{
{SvcOperationCfsParamId: 164, ParamValue: plan.Domain.ValueString()},
{SvcOperationCfsParamId: 169, ParamValue: plan.ResourceRealm.ValueString()},
{SvcOperationCfsParamId: 165, ParamValue: fmt.Sprintf("%d", plan.Cpu.ValueInt64())},
{SvcOperationCfsParamId: 166, ParamValue: fmt.Sprintf("%d", plan.Memory.ValueInt64())},
{SvcOperationCfsParamId: 167, ParamValue: fmt.Sprintf("%d", plan.Disk.ValueInt64())},
{SvcOperationCfsParamId: 170, ParamValue: plan.Email.ValueString()},
{SvcOperationCfsParamId: 171, ParamValue: plan.Password.ValueString()},
}
serviceId := 96
svcOperationId, err := r.client.GetOperationId(ctx, serviceId, "create")
if err != nil {
resp.Diagnostics.AddError("Failed to get create operation ID for pgAdmin", err.Error())
return
}
instanceUid, opUid, err := r.client.CreateInstance(ctx, plan.Domain.ValueString(), serviceId, svcOperationId, params)
if err != nil {
resp.Diagnostics.AddError("Error creating pgAdmin", err.Error())
return
}
err = r.client.WaitForOperation(ctx, opUid)
if err != nil {
resp.Diagnostics.AddError("Error waiting for pgAdmin ready", err.Error())
return
}
plan.ID = types.StringValue(instanceUid)
// plan.DeletionProtection = types.BoolValue(true)
diags = resp.State.Set(ctx, plan)
resp.Diagnostics.Append(diags...)
}
func (r *PgAdminResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state PgAdminResourceModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
instanceUid := state.ID.ValueString()
// Fetch current state
inst, err := r.client.GetInstanceState(ctx, instanceUid)
if err != nil {
// If 404, remove from state
if strings.Contains(err.Error(), "status 404") {
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError("Error reading pgAdmin state", err.Error())
return
}
// Map params
if inst.State != nil && inst.State.Params != nil {
for k, v := range inst.State.Params {
valStr := fmt.Sprintf("%v", v)
// Log for debugging
tflog.Info(ctx, fmt.Sprintf("PGAdmin Param: %s = %s", k, valStr))
switch k {
case "resourceCPU":
val, _ := strconv.ParseInt(valStr, 10, 64)
state.Cpu = types.Int64Value(val)
case "resourceMemory":
val, _ := strconv.ParseInt(valStr, 10, 64)
state.Memory = types.Int64Value(val)
case "resourceDisk":
val, _ := strconv.ParseInt(valStr, 10, 64)
state.Disk = types.Int64Value(val)
case "domain":
state.Domain = types.StringValue(valStr)
case "resourceRealm":
state.ResourceRealm = types.StringValue(valStr)
case "login":
state.Email = types.StringValue(valStr)
}
}
}
diags = resp.State.Set(ctx, &state)
resp.Diagnostics.Append(diags...)
}
func (r *PgAdminResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan, state PgAdminResourceModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
diags = req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Updating pgAdmin resource...")
// Find operation ID for 'modify'
// Based on HAR/Screenshots, modify uses standard "modify" operation
opId, err := r.client.GetOperationIdForInstance(ctx, state.ID.ValueString(), "modify")
if err != nil {
resp.Diagnostics.AddError("Failed to find update operation", err.Error())
return
}
// Payload for OP creation
opPayload := map[string]interface{}{
"instanceUid": state.ID.ValueString(),
"svcOperationId": opId,
"operation": "modify",
}
// Create request
var opUid string
opUid, err = r.client.postIgnoreResponse(ctx, "/instanceOperations", opPayload, true)
if err != nil {
resp.Diagnostics.AddError("Failed to create update operation", err.Error())
return
}
// Fetch Op details to get correct CFS Param IDs for 'modify'
opDetails, err := r.client.GetInstanceOperation(ctx, opUid)
if err != nil {
resp.Diagnostics.AddError("Failed to fetch operation details", err.Error())
return
}
paramMapping := make(map[string]int)
for _, p := range opDetails.CfsParams {
paramMapping[p.SvcOperationCfsParam] = p.SvcOperationCfsParamId
}
// Create params list from plan
// We assume standard param names "resourceCPU", "resourceMemory", "resourceDisk" based on screenshot
// But we use mapping to be safe if IDs differ from Create
type Modification struct {
Name string
Val string
}
mods := []Modification{
{"resourceCPU", fmt.Sprintf("%d", plan.Cpu.ValueInt64())},
{"resourceMemory", fmt.Sprintf("%d", plan.Memory.ValueInt64())},
{"resourceDisk", fmt.Sprintf("%d", plan.Disk.ValueInt64())},
}
params := []InstanceParam{}
for _, m := range mods {
if id, ok := paramMapping[m.Name]; ok {
params = append(params, InstanceParam{SvcOperationCfsParamId: id, ParamValue: m.Val})
} else {
tflog.Warn(ctx, fmt.Sprintf("Param %s not found in modify operation", m.Name))
}
}
// If no params (e.g. only email changed but modify doesn't support it?), skip
if len(params) == 0 {
tflog.Warn(ctx, "No matching parameters found for modify operation. Skipping.")
} else {
// Run op
_, _, err = r.client.RunInstanceOperation(ctx, opUid, params)
if err != nil {
resp.Diagnostics.AddError("Error running modify operation", err.Error())
return
}
// Wait
err = r.client.WaitForOperation(ctx, opUid)
if err != nil {
resp.Diagnostics.AddError("Error waiting for modification", err.Error())
return
}
}
diags = resp.State.Set(ctx, plan)
resp.Diagnostics.Append(diags...)
}
func (r *PgAdminResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state PgAdminResourceModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// User requested NO DELETE for pgAdmin (suspend/protection).
// We will simply remove it from Terraform state without calling API delete.
if state.DeletionProtection.ValueBool() {
tflog.Warn(ctx, "Deletion Protection is ENABLED for PgAdmin. Resource will remain active in Nubes Cloud. Manual cleanup required.")
return
}
tflog.Info(ctx, "Deletion Protection is DISABLED for PgAdmin. Triggering 'suspend'...")
instanceId := state.ID.ValueString()
// Выполняем операцию suspend
err := r.client.RunAction(ctx, instanceId, "suspend")
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to suspend PgAdmin: %s", err))
return
}
tflog.Info(ctx, "PgAdmin suspended successfully. It will be permanently deleted from the cloud in 14 days.")
}
func (r *PgAdminResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}
File diff suppressed because it is too large Load Diff
-199
View File
@@ -1,199 +0,0 @@
package provider
import (
"context"
"crypto/tls"
"net/http"
"os"
"strings"
"time"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
"terraform-provider-mycloud/internal/core"
"terraform-provider-mycloud/internal/generated"
)
var _ provider.Provider = &NubesProvider{}
type NubesProvider struct {
version string
}
// NubesProviderModel — конфигурация провайдера из HCL-блока provider {}.
// Каждое поле соответствует атрибуту в Schema() ниже.
// Поля Optional: если не указаны — берутся из env-переменных или defaults.
type NubesProviderModel struct {
ApiEndpoint types.String `tfsdk:"api_endpoint"`
ApiToken types.String `tfsdk:"api_token"`
// Insecure отключает проверку TLS-сертификата сервера.
// НЕ использовать в продакшн! Только для dev-стендов с самоподписанным сертом.
// Может быть задан также через env NUBES_INSECURE=true.
Insecure types.Bool `tfsdk:"insecure"`
// LogLevel задаёт уровень вывода этапов операций: "none" (default) | "info" | "debug".
// Может быть переопределён на уровне ресурса через атрибут log_level ресурса.
LogLevel types.String `tfsdk:"log_level"`
}
type NubesClient struct {
HttpClient *http.Client
ApiEndpoint string
ApiToken string
}
func New(version string) func() provider.Provider {
return func() provider.Provider {
return &NubesProvider{
version: version,
}
}
}
func (p *NubesProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) {
resp.TypeName = "nubes"
resp.Version = p.version
}
func (p *NubesProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"api_endpoint": schema.StringAttribute{
MarkdownDescription: "API Gateway endpoint for Nubes Cloud",
Optional: true,
},
"api_token": schema.StringAttribute{
MarkdownDescription: "API authentication token",
Optional: true,
Sensitive: true,
},
// insecure: отключает проверку TLS-сертификата Nubes API.
// По умолчанию false — сертификат проверяется (безопасно).
// Устанавливать true только на dev-стенде с самоподписанным сертом.
// Альтернатива: env NUBES_INSECURE=true (не требует правки .tf файлов).
"insecure": schema.BoolAttribute{
MarkdownDescription: "Disable TLS certificate verification. Use only for dev environments with self-signed certs. Can also be set via NUBES_INSECURE env var.",
Optional: true,
},
// log_level: уровень вывода этапов операций во время terraform apply/destroy.
// "none" (default) — не выводить ничего.
// "info" — выводить строку на каждый этап: [OK ] Валидация — 74.8 sec
// "debug" — как info + детали каждого этапа без timestamp-мусора.
// Может быть переопределён на уровне ресурса через атрибут log_level.
"log_level": schema.StringAttribute{
MarkdownDescription: "Operation stages log level: none (default), info, debug.",
Optional: true,
},
},
}
}
func (p *NubesProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var config NubesProviderModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}
// Default values
// TODO(api-gateway): default is legacy proxy (index.cfm?endpoint=).
// core.UniversalClient still uses ?endpoint= pattern — needs migration to REST paths.
apiEndpoint := "https://deck-api.ngcloud.ru/api/v1/index.cfm"
apiToken := ""
if !config.ApiEndpoint.IsNull() {
apiEndpoint = config.ApiEndpoint.ValueString()
}
if !config.ApiToken.IsNull() {
apiToken = strings.TrimSpace(config.ApiToken.ValueString())
} else {
// Try to get from environment
if token := os.Getenv("NUBES_API_TOKEN"); token != "" {
apiToken = strings.TrimSpace(token)
} else {
// Try to read from ~/.nubes_token file
homeDir, err := os.UserHomeDir()
if err == nil {
tokenFile := homeDir + "/.nubes_token"
if data, err := os.ReadFile(tokenFile); err == nil {
apiToken = strings.TrimSpace(string(data))
}
}
}
}
// --- TLS: определяем нужно ли пропустить проверку сертификата ---
// Приоритет: config.insecure > env NUBES_INSECURE > false (безопасный default).
// Nubes Cloud API использует валидный TLS-сертификат от доверенного CA,
// поэтому в продакшн InsecureSkipVerify должен быть false.
// true оставлен только для совместимости с dev-стендами без нормального сертификата.
insecureSkipVerify := false
if !config.Insecure.IsNull() && !config.Insecure.IsUnknown() {
// Явно задано в provider {} блоке HCL
insecureSkipVerify = config.Insecure.ValueBool()
} else if os.Getenv("NUBES_INSECURE") == "true" {
// Задано через переменную окружения (удобно для CI/CD без правки .tf файлов)
insecureSkipVerify = true
}
// Custom transport based on DefaultTransport.
// Клонируем DefaultTransport чтобы сохранить все системные настройки (proxy, timeouts).
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.TLSHandshakeTimeout = 60 * time.Second
// Force HTTP/1.1: Nubes API не поддерживает HTTP/2, принудительно отключаем.
// MinVersion TLS 1.2 — минимально безопасная версия TLS.
transport.TLSClientConfig = &tls.Config{
InsecureSkipVerify: insecureSkipVerify, // false в prod, true только для dev
NextProtos: []string{"http/1.1"},
MinVersion: tls.VersionTLS12,
}
transport.ForceAttemptHTTP2 = false
// Use Core Universal Client
client := &core.UniversalClient{
HttpClient: &http.Client{
Transport: transport,
Timeout: 300 * time.Second,
},
ApiEndpoint: apiEndpoint,
ApiToken: apiToken,
LogLevel: config.LogLevel.ValueString(),
}
resp.DataSourceData = client
resp.ResourceData = client
}
func (p *NubesProvider) Resources(ctx context.Context) []func() resource.Resource {
return []func() resource.Resource{
NewTubulusResource,
NewOrganizationResource,
NewVMResource,
NewVDCResource,
NewEdgeResource,
NewVAppResource,
NewQuickStartResource,
NewPostgresResource,
NewS3BucketResource,
NewPgAdminResource,
// Generated Resources
generated.NewBolvankaResource,
generated.NewBolvankaUniversalResource,
generated.NewBolvankaUniversalLifecycleResource,
}
}
func (p *NubesProvider) DataSources(ctx context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{
NewVDCDataSource,
NewEdgeDataSource,
NewVAppDataSource,
NewServiceInstanceDataSource,
}
}
-491
View File
@@ -1,491 +0,0 @@
package provider
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"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/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var _ resource.Resource = &QuickStartResource{}
var _ resource.ResourceWithImportState = &QuickStartResource{}
func NewQuickStartResource() resource.Resource {
return &QuickStartResource{}
}
type QuickStartResource struct {
client *NubesClient
}
type QuickStartResourceModel struct {
ID types.String `tfsdk:"id"`
DisplayName types.String `tfsdk:"display_name"`
Description types.String `tfsdk:"description"`
Status types.String `tfsdk:"status"`
// Параметры из HAR файла
OrgDomain types.String `tfsdk:"org_domain"` // 332: ngcloud.ru
VAppName types.String `tfsdk:"vapp_name"` // 334: vappa
ProviderVDC types.String `tfsdk:"provider_vdc"` // 358: sandbox-v1cl1-pvdc
EdgeCount types.Int64 `tfsdk:"edge_count"` // 359: 1
ExternalNetwork types.String `tfsdk:"external_network"` // 360: internet-ipv4-v1
StorageProfiles types.String `tfsdk:"storage_profiles"` // 399: [{"name": "", "size": 1024}]
NetworkPool types.String `tfsdk:"network_pool"` // 400: nsxt-sandbox-geneve-np
CPUAllocationPercent types.Int64 `tfsdk:"cpu_allocation_percent"` // 401: 20
RAMAllocationPercent types.Int64 `tfsdk:"ram_allocation_percent"` // 402: 20
ServiceEngineGroup types.String `tfsdk:"service_engine_group"` // 403: SEGROUP-SANDBOX-CL1-SHARED-01
ThinProvisioning types.Bool `tfsdk:"thin_provisioning"` // 404: true
FastProvisioning types.Bool `tfsdk:"fast_provisioning"` // 405: false
VDCNetworkQuota types.Int64 `tfsdk:"vdc_network_quota"` // 566: 1
CPUQuotaMhz types.Int64 `tfsdk:"cpu_quota_mhz"` // 567: 10
RAMQuotaGb types.Int64 `tfsdk:"ram_quota_gb"` // 568: 1
}
func (r *QuickStartResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_quick_start"
}
func (r *QuickStartResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Nubes Quick Start - создаёт полное окружение (Org + VDC + Edge + vApp)",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Quick Start identifier (UUID)",
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"display_name": schema.StringAttribute{
MarkdownDescription: "Display name",
Required: true,
},
"description": schema.StringAttribute{
MarkdownDescription: "Description",
Optional: true,
},
"status": schema.StringAttribute{
MarkdownDescription: "Current status",
Computed: true,
},
"org_domain": schema.StringAttribute{
MarkdownDescription: "Organization domain (например: ngcloud.ru)",
Required: true,
},
"vapp_name": schema.StringAttribute{
MarkdownDescription: "vApp name",
Required: true,
},
"provider_vdc": schema.StringAttribute{
MarkdownDescription: "Provider VDC name",
Required: true,
},
"edge_count": schema.Int64Attribute{
MarkdownDescription: "Number of Edge gateways",
Required: true,
},
"external_network": schema.StringAttribute{
MarkdownDescription: "External network name",
Required: true,
},
"storage_profiles": schema.StringAttribute{
MarkdownDescription: "Storage profiles JSON",
Required: true,
},
"network_pool": schema.StringAttribute{
MarkdownDescription: "Network pool name",
Required: true,
},
"cpu_allocation_percent": schema.Int64Attribute{
MarkdownDescription: "CPU allocation percent",
Required: true,
},
"ram_allocation_percent": schema.Int64Attribute{
MarkdownDescription: "RAM allocation percent",
Required: true,
},
"service_engine_group": schema.StringAttribute{
MarkdownDescription: "Service Engine Group",
Required: true,
},
"thin_provisioning": schema.BoolAttribute{
MarkdownDescription: "Enable thin provisioning",
Required: true,
},
"fast_provisioning": schema.BoolAttribute{
MarkdownDescription: "Enable fast provisioning",
Required: true,
},
"vdc_network_quota": schema.Int64Attribute{
MarkdownDescription: "VDC network quota",
Required: true,
},
"cpu_quota_mhz": schema.Int64Attribute{
MarkdownDescription: "CPU quota in MHz",
Required: true,
},
"ram_quota_gb": schema.Int64Attribute{
MarkdownDescription: "RAM quota in GB",
Required: true,
},
},
}
}
func (r *QuickStartResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*NubesClient)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *NubesClient, got: %T", req.ProviderData),
)
return
}
r.client = client
}
func (r *QuickStartResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data QuickStartResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Step 1: Create instance
instanceReq := CreateInstanceRequest{
ServiceId: 113, // Quick Start service ID
DisplayName: data.DisplayName.ValueString(),
Descr: data.Description.ValueString(),
}
jsonData, err := json.Marshal(instanceReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to marshal instance request: %s", err))
return
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", r.client.ApiEndpoint+"/instances", bytes.NewBuffer(jsonData))
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create instance request: %s", err))
return
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create instance: %s", err))
return
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(httpResp.Body)
resp.Diagnostics.AddError("API Error", fmt.Sprintf("Create instance failed with status %d: %s", httpResp.StatusCode, string(body)))
return
}
location := httpResp.Header.Get("Location")
if location == "" {
resp.Diagnostics.AddError("API Error", "No Location header in response")
return
}
instanceId := location[2:]
data.ID = types.StringValue(instanceId)
// Step 2: Create operation
operationReq := CreateOperationRequest{
InstanceUid: instanceId,
Operation: "create",
}
jsonData, err = json.Marshal(operationReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to marshal operation request: %s", err))
return
}
httpReq, err = http.NewRequestWithContext(ctx, "POST", r.client.ApiEndpoint+"/instanceOperations", bytes.NewBuffer(jsonData))
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create operation request: %s", err))
return
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err = r.client.HttpClient.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create operation: %s", err))
return
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(httpResp.Body)
resp.Diagnostics.AddError("API Error", fmt.Sprintf("Create operation failed with status %d: %s", httpResp.StatusCode, string(body)))
return
}
location = httpResp.Header.Get("Location")
if location == "" {
resp.Diagnostics.AddError("API Error", "No Location header in operation response")
return
}
operationId := location[2:]
// Step 3: Submit all parameters
params := map[int]string{
332: data.OrgDomain.ValueString(),
334: data.VAppName.ValueString(),
358: data.ProviderVDC.ValueString(),
359: fmt.Sprintf("%d", data.EdgeCount.ValueInt64()),
360: data.ExternalNetwork.ValueString(),
399: data.StorageProfiles.ValueString(),
400: data.NetworkPool.ValueString(),
401: fmt.Sprintf("%d", data.CPUAllocationPercent.ValueInt64()),
402: fmt.Sprintf("%d", data.RAMAllocationPercent.ValueInt64()),
403: data.ServiceEngineGroup.ValueString(),
404: fmt.Sprintf("%t", data.ThinProvisioning.ValueBool()),
405: fmt.Sprintf("%t", data.FastProvisioning.ValueBool()),
566: fmt.Sprintf("%d", data.VDCNetworkQuota.ValueInt64()),
567: fmt.Sprintf("%d", data.CPUQuotaMhz.ValueInt64()),
568: fmt.Sprintf("%d", data.RAMQuotaGb.ValueInt64()),
}
for paramId, paramValue := range params {
paramReq := CreateCfsParamRequest{
InstanceOperationUid: operationId,
SvcOperationCfsParamId: paramId,
ParamValue: paramValue,
}
jsonData, err = json.Marshal(paramReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to marshal param request: %s", err))
return
}
httpReq, err = http.NewRequestWithContext(ctx, "POST", r.client.ApiEndpoint+"/instanceOperationCfsParams", bytes.NewBuffer(jsonData))
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create param request: %s", err))
return
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err = r.client.HttpClient.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to submit parameter: %s", err))
return
}
respBody, _ := io.ReadAll(httpResp.Body)
httpResp.Body.Close()
if httpResp.StatusCode != http.StatusCreated && httpResp.StatusCode != http.StatusOK {
resp.Diagnostics.AddError("API Error", fmt.Sprintf("Submit parameter %d failed with status %d: %s", paramId, httpResp.StatusCode, string(respBody)))
return
}
}
// Step 4: Run operation
httpReq, err = http.NewRequestWithContext(ctx, "POST", r.client.ApiEndpoint+"/instanceOperations/"+operationId+"/run", bytes.NewBuffer([]byte("{}")))
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create run request: %s", err))
return
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err = r.client.HttpClient.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to run operation: %s", err))
return
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusOK && httpResp.StatusCode != http.StatusCreated && httpResp.StatusCode != http.StatusAccepted {
body, _ := io.ReadAll(httpResp.Body)
resp.Diagnostics.AddError("API Error", fmt.Sprintf("Run operation failed with status %d: %s", httpResp.StatusCode, string(body)))
return
}
// Wait and read status
time.Sleep(5 * time.Second)
readData, err := r.readInstance(ctx, instanceId)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read instance after creation: %s", err))
return
}
data.Status = types.StringValue(readData.Status)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *QuickStartResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data QuickStartResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
instanceResp, err := r.readInstance(ctx, data.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read instance: %s", err))
return
}
data.Status = types.StringValue(instanceResp.Status)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *QuickStartResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
resp.Diagnostics.AddError("Not Supported", "Quick Start resource cannot be updated")
}
func (r *QuickStartResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data QuickStartResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
operationReq := CreateOperationRequest{
InstanceUid: data.ID.ValueString(),
Operation: "delete",
}
jsonData, err := json.Marshal(operationReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to marshal operation request: %s", err))
return
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", r.client.ApiEndpoint+"/instanceOperations", bytes.NewBuffer(jsonData))
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create operation request: %s", err))
return
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create delete operation: %s", err))
return
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(httpResp.Body)
resp.Diagnostics.AddError("API Error", fmt.Sprintf("Create delete operation failed with status %d: %s", httpResp.StatusCode, string(body)))
return
}
location := httpResp.Header.Get("Location")
if location == "" {
resp.Diagnostics.AddError("API Error", "No Location header in operation response")
return
}
operationId := location[2:]
// Run delete operation
httpReq, err = http.NewRequestWithContext(ctx, "POST", r.client.ApiEndpoint+"/instanceOperations/"+operationId+"/run", bytes.NewBuffer([]byte("{}")))
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create run request: %s", err))
return
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err = r.client.HttpClient.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to run delete operation: %s", err))
return
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(httpResp.Body)
resp.Diagnostics.AddError("API Error", fmt.Sprintf("Run delete operation failed with status %d: %s", httpResp.StatusCode, string(body)))
return
}
}
func (r *QuickStartResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}
func (r *QuickStartResource) readInstance(ctx context.Context, instanceId string) (*InstanceResponse, error) {
httpReq, err := http.NewRequestWithContext(ctx, "GET", r.client.ApiEndpoint+"/instances/"+instanceId, nil)
if err != nil {
return nil, err
}
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
return nil, err
}
defer httpResp.Body.Close()
body, err := io.ReadAll(httpResp.Body)
if err != nil {
return nil, err
}
if httpResp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("read request failed with status %d: %s", httpResp.StatusCode, string(body))
}
var instanceResp InstanceResponse
if err := json.Unmarshal(body, &instanceResp); err != nil {
return nil, err
}
return &instanceResp, nil
}
-255
View File
@@ -1,255 +0,0 @@
package provider
// [НЕ ИЗМЕНЯТЬ !!!!]
// Данный ресурс реализует паттерн "Nubes Flow" для S3 бакета.
// ВАЖНО: Операция Update (изменение) для S3 бакетов в API Nubes отсутствует.
// Любое изменение атрибутов в Terraform приведет к пересозданию ресурса (RequiresReplace).
import (
"context"
"fmt"
"strconv"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
)
var _ resource.Resource = &S3BucketResource{}
type S3BucketResource struct {
client *NubesClient
}
func NewS3BucketResource() resource.Resource {
return &S3BucketResource{}
}
type S3BucketResourceModel struct {
ID types.String `tfsdk:"id"`
S3UserUid types.String `tfsdk:"s3_user_uid"`
BucketName types.String `tfsdk:"bucket_name"`
MaxSize types.Int64 `tfsdk:"max_size"`
ReadAll types.Bool `tfsdk:"read_all"`
ListAll types.Bool `tfsdk:"list_all"`
CorsAll types.Bool `tfsdk:"cors_all"`
Placement types.String `tfsdk:"placement"`
}
func (r *S3BucketResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_s3_bucket"
}
func (r *S3BucketResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Manages an S3 Bucket resource.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"s3_user_uid": schema.StringAttribute{
Required: true,
Description: "UID Корневой услуги S3 (Param 124). Это UUID экземпляра услуги S3 Object Storage (svcId 12). Например: 6d6061cb-b0c1-44b9-8969-a70f08fe673c",
Validators: []validator.String{
UUIDLike(),
},
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"bucket_name": schema.StringAttribute{
Required: true,
Description: "The name of the bucket (param 125)",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"max_size": schema.Int64Attribute{
Optional: true,
Computed: true,
Default: int64default.StaticInt64(-1),
Description: "Max size of the bucket, -1 for unlimited (param 126)",
PlanModifiers: []planmodifier.Int64{
int64planmodifier.RequiresReplace(),
},
},
"read_all": schema.BoolAttribute{
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false),
Description: "Enable read all (param 127)",
PlanModifiers: []planmodifier.Bool{
boolplanmodifier.RequiresReplace(),
},
},
"list_all": schema.BoolAttribute{
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false),
Description: "Enable list all (param 128)",
PlanModifiers: []planmodifier.Bool{
boolplanmodifier.RequiresReplace(),
},
},
"cors_all": schema.BoolAttribute{
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false),
Description: "Enable CORS all (param 129)",
PlanModifiers: []planmodifier.Bool{
boolplanmodifier.RequiresReplace(),
},
},
"placement": schema.StringAttribute{
Optional: true,
Computed: true,
Default: stringdefault.StaticString("HOT"),
Description: "Placement strategy (HOT/COLD) (param 130)",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
},
}
}
func (r *S3BucketResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*NubesClient)
if !ok {
resp.Diagnostics.AddError("Unexpected Resource Configure Type", fmt.Sprintf("Expected *NubesClient, got: %T. Please report this issue to the provider developers.", req.ProviderData))
return
}
r.client = client
}
func (r *S3BucketResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan S3BucketResourceModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Creating S3 Bucket resource...")
// 1. Подготовка параметров для создания.
// Каждый параметр соответствует svcOperationCfsParamId из API.
// Эти ID фиксированы для услуги S3 (ServiceId 13).
params := []InstanceParam{
{SvcOperationCfsParamId: 124, ParamValue: plan.S3UserUid.ValueString()},
{SvcOperationCfsParamId: 125, ParamValue: plan.BucketName.ValueString()},
{SvcOperationCfsParamId: 126, ParamValue: fmt.Sprintf("%d", plan.MaxSize.ValueInt64())},
{SvcOperationCfsParamId: 127, ParamValue: strconv.FormatBool(plan.ReadAll.ValueBool())},
{SvcOperationCfsParamId: 128, ParamValue: strconv.FormatBool(plan.ListAll.ValueBool())},
{SvcOperationCfsParamId: 129, ParamValue: strconv.FormatBool(plan.CorsAll.ValueBool())},
{SvcOperationCfsParamId: 130, ParamValue: plan.Placement.ValueString()},
}
// Service ID 13 для S3 (бакеты)
serviceId := 13
// Динамический поиск ID операции "create".
// В Nubes для каждого сервиса свой набор ID операций.
svcOperationId, err := r.client.GetOperationId(ctx, serviceId, "create")
if err != nil {
resp.Diagnostics.AddError("Failed to get create operation ID", err.Error())
return
}
// CreateInstance выполняет полный цикл "Nubes Flow":
// 1. POST /instances (создание объекта)
// 2. POST /instanceOperations (инициализация визарда)
// 3. GET /instanceOperations?fields=cfsParams (получение списка ожидаемых параметров)
// 4. POST /instanceOperationCfsParams (синхронизация значений)
// 5. POST /run {BODY: {}} (запуск выполнения)
instanceUid, opUid, err := r.client.CreateInstance(ctx, plan.BucketName.ValueString(), serviceId, svcOperationId, params)
if err != nil {
resp.Diagnostics.AddError("Error creating S3 bucket", err.Error())
return
}
// Ожидание завершения операции.
err = r.client.WaitForOperation(ctx, opUid)
if err != nil {
resp.Diagnostics.AddError("Error waiting for S3 bucket ready", err.Error())
return
}
plan.ID = types.StringValue(instanceUid)
diags = resp.State.Set(ctx, plan)
resp.Diagnostics.Append(diags...)
}
func (r *S3BucketResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state S3BucketResourceModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Implement Read if necessary. For now, just maintain state.
// Ideally, fetch instance details and update state.
// We can use r.client.GetInstance(ctx, state.ID.ValueString()) to check if it exists or is deleted.
/*
instance, err := r.client.GetInstance(ctx, state.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Error reading S3 bucket", err.Error())
return
}
if instance == nil || instance.IsDeleted {
resp.State.RemoveResource(ctx)
return
}
// Update fields if needed
*/
diags = resp.State.Set(ctx, &state)
resp.Diagnostics.Append(diags...)
}
func (r *S3BucketResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
// [НЕ ИЗМЕНЯТЬ !!!!]
// API Nubes не поддерживает операцию Modify для S3 бакетов.
// Этот метод оставлен пустым, так как RequiresReplace в схеме должен предотвращать его вызов для критичных полей.
}
func (r *S3BucketResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state S3BucketResourceModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Assume delete operation exists
// Get "delete" operation ID
serviceId := 13
svcOperationId, err := r.client.GetOperationId(ctx, serviceId, "delete")
if err != nil {
resp.Diagnostics.AddError("Failed to get delete operation ID", err.Error())
return
}
err = r.client.DeleteInstance(ctx, state.ID.ValueString(), svcOperationId)
if err != nil {
resp.Diagnostics.AddError("Error deleting S3 bucket", err.Error())
return
}
}
@@ -1,120 +0,0 @@
package provider
import (
"context"
"fmt"
"strings"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var _ datasource.DataSource = &ServiceInstanceDataSource{}
func NewServiceInstanceDataSource() datasource.DataSource {
return &ServiceInstanceDataSource{}
}
type ServiceInstanceDataSource struct {
client *NubesClient
}
type ServiceInstanceDataSourceModel struct {
ID types.String `tfsdk:"id"`
DisplayName types.String `tfsdk:"display_name"`
ServiceId types.Int64 `tfsdk:"service_id"`
ServiceName types.String `tfsdk:"service_name"`
}
func (d *ServiceInstanceDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_service_instance"
}
func (d *ServiceInstanceDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Data source для получения UUID сервиса (Instance) по его имени (DisplayName). Позволяет использовать human-readable имена (например 's3-111805') вместо UUID.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
MarkdownDescription: "UUID найденного сервиса (instanceUid)",
Computed: true,
},
"display_name": schema.StringAttribute{
MarkdownDescription: "Имя (DisplayName) для поиска. Например 's3-111805'",
Required: true,
},
"service_id": schema.Int64Attribute{
MarkdownDescription: "ID типа сервиса для дополнительной фильтрации (например 12 для S3). Опционально.",
Optional: true,
},
"service_name": schema.StringAttribute{
MarkdownDescription: "Название типа сервиса (например 'S3 Object Storage')",
Computed: true,
},
},
}
}
func (d *ServiceInstanceDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*NubesClient)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *NubesClient, got: %T", req.ProviderData),
)
return
}
d.client = client
}
func (d *ServiceInstanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data ServiceInstanceDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
displayName := data.DisplayName.ValueString()
instances, err := d.client.GetInstances(ctx)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to fetch instances list: %s", err))
return
}
var foundInstance *InstanceSummary
// Поиск по DisplayName
for _, inst := range instances {
if strings.TrimSpace(inst.DisplayName) == strings.TrimSpace(displayName) {
// Если задан service_id, проверяем и его
if !data.ServiceId.IsNull() && !data.ServiceId.IsUnknown() {
if int64(inst.ServiceId) != data.ServiceId.ValueInt64() {
continue
}
}
foundInstance = &inst
break
}
}
if foundInstance == nil {
resp.Diagnostics.AddError(
"Instance Not Found",
fmt.Sprintf("Could not find instance with DisplayName '%s'", displayName),
)
return
}
data.ID = types.StringValue(foundInstance.InstanceUid)
data.ServiceName = types.StringValue(foundInstance.Svc)
data.ServiceId = types.Int64Value(int64(foundInstance.ServiceId))
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
-133
View File
@@ -1,133 +0,0 @@
package provider
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"strings"
"github.com/google/generative-ai-go/genai"
"google.golang.org/api/option"
)
type AIConfig struct {
DurationMs *int64 `json:"duration_ms,omitempty"`
FailAtStart *bool `json:"fail_at_start,omitempty"`
FailInProgress *bool `json:"fail_in_progress,omitempty"`
WhereFail *int64 `json:"where_fail,omitempty"`
BodyMessage *string `json:"body_message,omitempty"`
ResourceRealm *string `json:"resource_realm,omitempty"`
MapExample *string `json:"map_example,omitempty"`
JsonExample *string `json:"json_example,omitempty"`
YamlExample *string `json:"yaml_example,omitempty"`
}
func (r *TubulusResource) askGemini(ctx context.Context, instruction string) (*AIConfig, error) {
// Приоритет: переменная окружения → файл → ошибка
apiKey := os.Getenv("GEMINI_API_KEY")
if apiKey == "" {
// Пробуем прочитать из файла (для локальной разработки)
keyBytes, err := os.ReadFile("gemini_api_key.txt")
if err == nil {
apiKey = strings.TrimSpace(string(keyBytes))
}
}
if apiKey == "" {
return nil, fmt.Errorf("GEMINI_API_KEY not found (set env var or create gemini_api_key.txt)")
}
client, err := genai.NewClient(ctx, option.WithAPIKey(apiKey))
if err != nil {
return nil, err
}
defer client.Close()
model := client.GenerativeModel("gemini-2.0-flash")
// Настройка модели: заставляем её возвращать строго JSON
model.ResponseMIMEType = "application/json"
systemPrompt := `Ты — эксперт по инфраструктуре и помощник для Terraform провайдера Nubes.
Твоя задача — распарсить пожелания пользователя (даже самые простые и неточные) и превратить их в JSON-конфигурацию для тестового ресурса Tubulus.
=== ПОЛЯ JSON (ВСЕ обязательны!) ===
1. "duration_ms" (integer): Сколько миллисекунд работает ресурс.
Примеры: "5 секунд" → 5000, "минута" → 60000, "быстро" → 1000, "долго" → 120000, "очень долго" → 300000
Дефолт если не указано: 5000
2. "fail_at_start" (boolean): Сломаться ли сразу при запуске?
Примеры: "сломай сразу" → true, "упади в начале" → true
Дефолт: false
3. "fail_in_progress" (boolean): Сломаться ли в процессе работы?
Примеры: "упади в середине" → true, "сломай в процессе" → true, "упадет на втором этапе" → true
Дефолт: false
4. "where_fail" (integer): На каком этапе сломаться? ТОЛЬКО [0, 1, 2, 3]
0 = не ломается
1 = подготовка (prepare)
2 = заполнение данных (data_fill) — ДЕФОЛТ если fail_in_progress=true
3 = после записи в Vault
Примеры: "первый этап" → 1, "второй этап" → 2, "последний" → 3
Дефолт: 0, но если fail_in_progress=true, то 2
5. "body_message" (string): Текст для записи в Vault (как секрет/пароль).
Примеры: "напиши hello" → "hello", "секрет 123" → "секрет 123", "привет мир" → "привет мир"
Если НЕ указан текст явно — оставь null (не "ai_generated")
Дефолт: null
6. "resource_realm" (string): Окружение. Всегда "dummy".
Дефолт: "dummy"
=== ВАЖНЫЕ ПРАВИЛА ===
• Возвращай СТРОГО JSON с ВСЕМИ 6 полями
• Понимай разговорный язык: "сделай быстро" = duration_ms: 1000, "пусть долго работает" = duration_ms: 120000
• Если пользователь написал текст для Vault ("напиши X", "положи Y") — используй его в body_message, иначе null
• Ответ БЕЗ пояснений, только {"duration_ms": ..., "fail_at_start": ..., ...}
=== ПРИМЕРЫ ===
Запрос: "сделай быстро"
{"duration_ms": 1000, "fail_at_start": false, "fail_in_progress": false, "where_fail": 0, "body_message": null, "resource_realm": "dummy"}
Запрос: "пусть работает минуту и напиши привет"
{"duration_ms": 60000, "fail_at_start": false, "fail_in_progress": false, "where_fail": 0, "body_message": "привет", "resource_realm": "dummy"}
Запрос: "сломай на втором этапе"
{"duration_ms": 5000, "fail_at_start": false, "fail_in_progress": true, "where_fail": 2, "body_message": null, "resource_realm": "dummy"}
Инструкция: ` + instruction
resp, err := model.GenerateContent(ctx, genai.Text(systemPrompt))
if err != nil {
return nil, err
}
if len(resp.Candidates) == 0 || len(resp.Candidates[0].Content.Parts) == 0 {
return nil, fmt.Errorf("gemini returned no content")
}
part := resp.Candidates[0].Content.Parts[0]
text, ok := part.(genai.Text)
if !ok {
return nil, fmt.Errorf("gemini returned non-text content")
}
log.Printf("[DEBUG] Gemini response: %s", string(text))
// Some Gemini versions return an array with one object when constrained to JSON
// Let's try to unmarshal as object first, then as array if it fails
var config AIConfig
if err := json.Unmarshal([]byte(text), &config); err != nil {
var configs []AIConfig
if errArray := json.Unmarshal([]byte(text), &configs); errArray == nil && len(configs) > 0 {
config = configs[0]
} else {
return nil, fmt.Errorf("failed to unmarshal gemini response: %w", err)
}
}
return &config, nil
}
File diff suppressed because it is too large Load Diff
-155
View File
@@ -1,155 +0,0 @@
package provider
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
)
// stringOneOfValidator validates that a string value is one of the allowed values.
type stringOneOfValidator struct {
values []string
}
func (v stringOneOfValidator) Description(ctx context.Context) string {
return fmt.Sprintf("value must be one of: %v", v.values)
}
func (v stringOneOfValidator) MarkdownDescription(ctx context.Context) string {
return fmt.Sprintf("value must be one of: %v", v.values)
}
func (v stringOneOfValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
return
}
value := req.ConfigValue.ValueString()
for _, allowed := range v.values {
if value == allowed {
return
}
}
resp.Diagnostics.AddAttributeError(
req.Path,
"Invalid value",
fmt.Sprintf("Value must be one of %v, got: %s", v.values, value),
)
}
func StringOneOf(values ...string) validator.String {
return stringOneOfValidator{
values: values,
}
}
// validJSONArrayValidator проверяет, что строка является валидным JSON массивом
type validJSONArrayValidator struct{}
func (v validJSONArrayValidator) Description(ctx context.Context) string {
return "value must be a valid JSON array (e.g., [\"item1\", \"item2\"])"
}
func (v validJSONArrayValidator) MarkdownDescription(ctx context.Context) string {
return "value must be a valid JSON array (e.g., `[\"item1\", \"item2\"]`)"
}
func (v validJSONArrayValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
return
}
value := req.ConfigValue.ValueString()
if value == "" {
// Пустая строка не валидна - должен быть либо null, либо JSON массив
resp.Diagnostics.AddAttributeError(
req.Path,
"Invalid JSON Array",
"Empty string is not a valid JSON array. Use jsonencode([...]) or omit the attribute.",
)
return
}
var arr []interface{}
if err := json.Unmarshal([]byte(value), &arr); err != nil {
resp.Diagnostics.AddAttributeError(
req.Path,
"Invalid JSON Array",
fmt.Sprintf("Value must be a valid JSON array: %s", err),
)
}
}
func ValidJSONArray() validator.String {
return validJSONArrayValidator{}
}
// validJSONArrayOrEmptyValidator проверяет JSON массив или разрешает пустую строку
// Используется для accessIpList где "" означает "доступ отовсюду"
type validJSONArrayOrEmptyValidator struct{}
func (v validJSONArrayOrEmptyValidator) Description(ctx context.Context) string {
return "value must be a valid JSON array or empty string (empty = access from anywhere)"
}
func (v validJSONArrayOrEmptyValidator) MarkdownDescription(ctx context.Context) string {
return "value must be a valid JSON array or empty string (empty = access from anywhere)"
}
func (v validJSONArrayOrEmptyValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
return
}
value := req.ConfigValue.ValueString()
if value == "" {
// Пустая строка разрешена - означает доступ отовсюду
return
}
var arr []interface{}
if err := json.Unmarshal([]byte(value), &arr); err != nil {
resp.Diagnostics.AddAttributeError(
req.Path,
"Invalid JSON Array",
fmt.Sprintf("Value must be a valid JSON array or empty string: %s", err),
)
}
}
func ValidJSONArrayOrEmpty() validator.String {
return validJSONArrayOrEmptyValidator{}
}
type uuidLikeValidator struct{}
func (v uuidLikeValidator) Description(ctx context.Context) string {
return "value must be a UUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)"
}
func (v uuidLikeValidator) MarkdownDescription(ctx context.Context) string {
return "value must be a UUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)"
}
func (v uuidLikeValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
return
}
value := strings.TrimSpace(req.ConfigValue.ValueString())
if value == "" || !uuidLikeRegex.MatchString(value) {
resp.Diagnostics.AddAttributeError(
req.Path,
"Invalid UUID",
fmt.Sprintf("Value must be a UUID, got: %q", req.ConfigValue.ValueString()),
)
}
}
func UUIDLike() validator.String {
return uuidLikeValidator{}
}
-144
View File
@@ -1,144 +0,0 @@
package provider
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var _ datasource.DataSource = &VAppDataSource{}
func NewVAppDataSource() datasource.DataSource {
return &VAppDataSource{}
}
type VAppDataSource struct {
client *NubesClient
}
type VAppDataSourceModel struct {
ID types.String `tfsdk:"id"`
DisplayName types.String `tfsdk:"display_name"`
Status types.String `tfsdk:"status"`
Description types.String `tfsdk:"description"`
}
func (d *VAppDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_vapp"
}
func (d *VAppDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Data source для получения информации о существующем vApp по имени",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
MarkdownDescription: "UUID vApp",
Computed: true,
},
"display_name": schema.StringAttribute{
MarkdownDescription: "Имя vApp для поиска",
Required: true,
},
"status": schema.StringAttribute{
MarkdownDescription: "Статус vApp",
Computed: true,
},
"description": schema.StringAttribute{
MarkdownDescription: "Описание vApp",
Computed: true,
},
},
}
}
func (d *VAppDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*NubesClient)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *NubesClient, got: %T", req.ProviderData),
)
return
}
d.client = client
}
func (d *VAppDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data VAppDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
httpReq, err := http.NewRequestWithContext(ctx, "GET", d.client.ApiEndpoint+"/instances", nil)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create request: %s", err))
return
}
httpReq.Header.Set("Authorization", "Bearer "+strings.TrimSpace(d.client.ApiToken))
httpReq.Header.Set("Content-Type", "application/json")
httpResp, err := d.client.HttpClient.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read instances: %s", err))
return
}
defer httpResp.Body.Close()
body, err := io.ReadAll(httpResp.Body)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read response: %s", err))
return
}
if httpResp.StatusCode != http.StatusOK {
resp.Diagnostics.AddError("API Error", fmt.Sprintf("GET /instances returned %d: %s", httpResp.StatusCode, string(body)))
return
}
var instancesResp struct {
Results []struct {
ID string `json:"id"`
DisplayName string `json:"name"`
Status string `json:"status"`
Description string `json:"desc"`
Service string `json:"svc"`
} `json:"results"`
}
if err := json.Unmarshal(body, &instancesResp); err != nil {
resp.Diagnostics.AddError("Parse Error", fmt.Sprintf("Unable to parse response: %s", err))
return
}
searchName := data.DisplayName.ValueString()
for _, instance := range instancesResp.Results {
if instance.Service == "Виртуальное приложение (vApp)" && instance.DisplayName == searchName {
data.ID = types.StringValue(instance.ID)
data.Status = types.StringValue(instance.Status)
data.Description = types.StringValue(instance.Description)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
return
}
}
resp.Diagnostics.AddError(
"vApp Not Found",
fmt.Sprintf("vApp с именем '%s' не найден. Проверьте имя или создайте новый vApp.", searchName),
)
}
-639
View File
@@ -1,639 +0,0 @@
package provider
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"time"
"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/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var _ resource.Resource = &VAppResource{}
var _ resource.ResourceWithImportState = &VAppResource{}
func NewVAppResource() resource.Resource {
return &VAppResource{}
}
type VAppResource struct {
client *NubesClient
}
type VAppResourceModel struct {
ID types.String `tfsdk:"id"`
DisplayName types.String `tfsdk:"display_name"`
Description types.String `tfsdk:"description"`
EdgeUID types.String `tfsdk:"edge_uid"`
VAppName types.String `tfsdk:"vapp_name"`
VdcUID types.String `tfsdk:"vdc_uid"`
Status types.String `tfsdk:"status"`
}
func (r *VAppResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_vapp"
}
func (r *VAppResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Nubes vApp (Virtual Application Catalog) resource",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
MarkdownDescription: "vApp identifier (UUID)",
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"display_name": schema.StringAttribute{
MarkdownDescription: "vApp display name",
Required: true,
},
"description": schema.StringAttribute{
MarkdownDescription: "vApp description",
Optional: true,
},
"edge_uid": schema.StringAttribute{
MarkdownDescription: "Edge Gateway UUID (parameter ID 190)",
Required: true,
},
"vapp_name": schema.StringAttribute{
MarkdownDescription: "vApp name in Cloud Director (parameter ID 191)",
Required: true,
},
"vdc_uid": schema.StringAttribute{
MarkdownDescription: "VDC UUID (parameter ID 623)",
Required: true,
},
"status": schema.StringAttribute{
MarkdownDescription: "Current status of the vApp",
Computed: true,
},
},
}
}
func (r *VAppResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*NubesClient)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *NubesClient, got: %T", req.ProviderData),
)
return
}
r.client = client
}
func (r *VAppResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data VAppResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Step 1: Create instance
createReq := CreateInstanceRequest{
ServiceId: 26, // VApp service ID
DisplayName: data.DisplayName.ValueString(),
Descr: data.Description.ValueString(),
}
jsonData, err := json.Marshal(createReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to marshal request: %s", err))
return
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", r.client.ApiEndpoint+"/instances", bytes.NewBuffer(jsonData))
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create request: %s", err))
return
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create instance: %s", err))
return
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(httpResp.Body)
resp.Diagnostics.AddError(
"API Error",
fmt.Sprintf("Create instance failed with status %d: %s", httpResp.StatusCode, string(body)),
)
return
}
location := httpResp.Header.Get("Location")
if location == "" {
resp.Diagnostics.AddError("API Error", "No Location header in response")
return
}
instanceId := location[2:]
data.ID = types.StringValue(instanceId)
// Step 2: Create operation
operationReq := CreateOperationRequest{
InstanceUid: instanceId,
Operation: "create",
}
jsonData, err = json.Marshal(operationReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to marshal operation request: %s", err))
return
}
httpReq, err = http.NewRequestWithContext(ctx, "POST", r.client.ApiEndpoint+"/instanceOperations", bytes.NewBuffer(jsonData))
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create operation request: %s", err))
return
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err = r.client.HttpClient.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create operation: %s", err))
return
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(httpResp.Body)
resp.Diagnostics.AddError(
"API Error",
fmt.Sprintf("Create operation failed with status %d: %s", httpResp.StatusCode, string(body)),
)
return
}
location = httpResp.Header.Get("Location")
if location == "" {
resp.Diagnostics.AddError("API Error", "No Location header in operation response")
return
}
operationId := location[2:]
// Step 3: Submit operation parameters and run
if err := r.submitOperationParams(ctx, operationId, data); err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to submit operation parameters: %s", err))
return
}
// Step 4: Wait for operation completion and instance running status
if err := r.waitForOperationAndInstanceStatus(ctx, operationId, instanceId, 10*time.Minute); err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Instance failed to reach running status: %s", err))
return
}
readData, err := r.readInstance(ctx, instanceId)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read instance after creation: %s", err))
return
}
data.Status = types.StringValue(readData.Status)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *VAppResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data VAppResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
instanceResp, err := r.readInstance(ctx, data.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read instance: %s", err))
return
}
data.Status = types.StringValue(instanceResp.Status)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *VAppResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data VAppResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
operationReq := CreateOperationRequest{
InstanceUid: data.ID.ValueString(),
Operation: "modify",
}
jsonData, err := json.Marshal(operationReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to marshal operation request: %s", err))
return
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", r.client.ApiEndpoint+"/instanceOperations", bytes.NewBuffer(jsonData))
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create operation request: %s", err))
return
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update instance: %s", err))
return
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(httpResp.Body)
resp.Diagnostics.AddError(
"API Error",
fmt.Sprintf("Modify operation failed with status %d: %s", httpResp.StatusCode, string(body)),
)
return
}
location := httpResp.Header.Get("Location")
if location != "" {
operationId := location[2:]
if err := r.submitOperationParams(ctx, operationId, data); err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to submit operation parameters: %s", err))
return
}
}
time.Sleep(5 * time.Second)
readData, err := r.readInstance(ctx, data.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read instance after update: %s", err))
return
}
data.Status = types.StringValue(readData.Status)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *VAppResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data VAppResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
operationReq := CreateOperationRequest{
InstanceUid: data.ID.ValueString(),
Operation: "delete",
}
jsonData, err := json.Marshal(operationReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to marshal operation request: %s", err))
return
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", r.client.ApiEndpoint+"/instanceOperations", bytes.NewBuffer(jsonData))
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create operation request: %s", err))
return
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete instance: %s", err))
return
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusCreated && httpResp.StatusCode != http.StatusNotFound {
body, _ := io.ReadAll(httpResp.Body)
resp.Diagnostics.AddError(
"API Error",
fmt.Sprintf("Delete operation failed with status %d: %s", httpResp.StatusCode, string(body)),
)
return
}
location := httpResp.Header.Get("Location")
if location != "" {
operationId := location[2:]
if err := r.submitOperationParams(ctx, operationId, data); err != nil {
resp.Diagnostics.AddWarning("Client Warning", fmt.Sprintf("Unable to submit operation parameters: %s", err))
}
}
}
func (r *VAppResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}
func (r *VAppResource) readInstance(ctx context.Context, id string) (*InstanceResponse, error) {
httpReq, err := http.NewRequestWithContext(ctx, "GET", r.client.ApiEndpoint+"/instances/"+id, nil)
if err != nil {
return nil, err
}
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
return nil, err
}
defer httpResp.Body.Close()
body, err := io.ReadAll(httpResp.Body)
if err != nil {
return nil, err
}
if httpResp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("read request failed with status %d: %s", httpResp.StatusCode, string(body))
}
var instanceResp InstanceResponse
if err := json.Unmarshal(body, &instanceResp); err != nil {
return nil, err
}
return &instanceResp, nil
}
func (r *VAppResource) submitOperationParams(ctx context.Context, operationUid string, data VAppResourceModel) error {
// Get operation details
httpReq, err := http.NewRequestWithContext(ctx, "GET",
r.client.ApiEndpoint+"/instanceOperations/"+operationUid+"?fields=cfsParams", nil)
if err != nil {
return fmt.Errorf("unable to create request: %s", err)
}
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
return fmt.Errorf("unable to get operation: %s", err)
}
defer httpResp.Body.Close()
body, err := io.ReadAll(httpResp.Body)
if err != nil {
return fmt.Errorf("unable to read response: %s", err)
}
if httpResp.StatusCode != http.StatusOK {
return fmt.Errorf("get operation failed with status %d: %s", httpResp.StatusCode, string(body))
}
var getResp GetOperationResponse
if err := json.Unmarshal(body, &getResp); err != nil {
return fmt.Errorf("unable to unmarshal response: %s", err)
}
operationResp := getResp.InstanceOperation
// Submit each parameter
for _, param := range operationResp.CfsParams {
valToSend := ""
// Map vApp parameters by ID
switch param.SvcOperationCfsParamId {
case 190: // edgeUid
if !data.EdgeUID.IsNull() && !data.EdgeUID.IsUnknown() {
valToSend = data.EdgeUID.ValueString()
}
case 191: // vappName
if !data.VAppName.IsNull() && !data.VAppName.IsUnknown() {
valToSend = data.VAppName.ValueString()
}
case 623: // vdcUid
if !data.VdcUID.IsNull() && !data.VdcUID.IsUnknown() {
valToSend = data.VdcUID.ValueString()
}
default:
// Use existing or default value for unknown parameters
if param.ParamValue != nil {
valToSend = *param.ParamValue
} else if param.DefaultValue != nil {
valToSend = *param.DefaultValue
}
}
// Fix specific data type formatting
if valToSend == "" || valToSend == "\"\"" {
if param.DataType == "map" || param.DataType == "json" {
valToSend = "{}"
} else if param.DataType == "array" || param.DataType == "list" {
valToSend = "[]"
}
}
paramReq := CreateCfsParamRequest{
InstanceOperationUid: operationUid,
SvcOperationCfsParamId: param.SvcOperationCfsParamId,
ParamValue: valToSend,
}
jsonData, err := json.Marshal(paramReq)
if err != nil {
return fmt.Errorf("unable to marshal param request: %s", err)
}
httpReq, err = http.NewRequestWithContext(ctx, "POST",
r.client.ApiEndpoint+"/instanceOperationCfsParams", bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("unable to create param request: %s", err)
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err = r.client.HttpClient.Do(httpReq)
if err != nil {
return fmt.Errorf("unable to submit parameter: %s", err)
}
respBody, _ := io.ReadAll(httpResp.Body)
httpResp.Body.Close()
if httpResp.StatusCode != http.StatusCreated &&
httpResp.StatusCode != http.StatusOK &&
httpResp.StatusCode != http.StatusNoContent {
return fmt.Errorf("submit parameter id %d failed with status %d: %s",
param.SvcOperationCfsParamId, httpResp.StatusCode, string(respBody))
}
}
// Run the operation
runReq, err := http.NewRequestWithContext(ctx, "POST",
r.client.ApiEndpoint+"/instanceOperations/"+operationUid+"/run", bytes.NewBuffer([]byte("{}")))
if err != nil {
return fmt.Errorf("unable to create run request: %s", err)
}
runReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
runReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
runResp, err := r.client.HttpClient.Do(runReq)
if err != nil {
return fmt.Errorf("unable to run operation: %s", err)
}
defer runResp.Body.Close()
if runResp.StatusCode != http.StatusOK &&
runResp.StatusCode != http.StatusNoContent &&
runResp.StatusCode != http.StatusCreated {
runBody, _ := io.ReadAll(runResp.Body)
return fmt.Errorf("run operation failed with status %d: %s",
runResp.StatusCode, string(runBody))
}
return nil
}
func (r *VAppResource) waitForOperationAndInstanceStatus(ctx context.Context, operationId string, instanceId string, timeout time.Duration) error {
deadline := time.Now().Add(timeout)
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
log.Printf("[DEBUG] Starting two-stage polling: operationId=%s, instanceId=%s, timeout=%v", operationId, instanceId, timeout)
// Шаг 1: Ждём завершения операции
operationLoop:
for {
select {
case <-ctx.Done():
return fmt.Errorf("context cancelled")
case <-ticker.C:
if time.Now().After(deadline) {
return fmt.Errorf("timeout waiting for operation to complete")
}
log.Printf("[DEBUG] Polling operation status: operationId=%s", operationId)
// Проверяем статус операции
httpReq, err := http.NewRequestWithContext(ctx, "GET",
r.client.ApiEndpoint+"/instanceOperations/"+operationId, nil)
if err != nil {
return fmt.Errorf("failed to create request: %s", err)
}
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
return fmt.Errorf("failed to check operation status: %s", err)
}
body, _ := io.ReadAll(httpResp.Body)
httpResp.Body.Close()
if httpResp.StatusCode != http.StatusOK {
return fmt.Errorf("get operation failed with status %d", httpResp.StatusCode)
}
var opResp struct {
InstanceOperation struct {
IsInProgress bool `json:"isInProgress"`
IsPending bool `json:"isPending"`
} `json:"instanceOperation"`
}
if err := json.Unmarshal(body, &opResp); err != nil {
return fmt.Errorf("failed to parse operation response: %s", err)
}
log.Printf("[DEBUG] Operation status: isInProgress=%v, isPending=%v", opResp.InstanceOperation.IsInProgress, opResp.InstanceOperation.IsPending)
// Операция завершена когда isInProgress=false И isPending=false
if !opResp.InstanceOperation.IsInProgress && !opResp.InstanceOperation.IsPending {
log.Printf("[DEBUG] Operation completed, moving to instance status check")
break operationLoop
}
}
}
// Шаг 2: Проверяем статус instance
ticker2 := time.NewTicker(5 * time.Second)
defer ticker2.Stop()
log.Printf("[DEBUG] Starting instance status polling")
for {
select {
case <-ctx.Done():
return fmt.Errorf("context cancelled")
case <-ticker2.C:
if time.Now().After(deadline) {
return fmt.Errorf("timeout waiting for instance to become running")
}
log.Printf("[DEBUG] Polling instance status: instanceId=%s", instanceId)
instance, err := r.readInstance(ctx, instanceId)
if err != nil {
return fmt.Errorf("failed to check instance status: %s", err)
}
if instance.Status == "running" {
return nil
}
if instance.Status == "error" || instance.Status == "failed" {
return fmt.Errorf("instance entered error state: %s", instance.Status)
}
}
}
}
-146
View File
@@ -1,146 +0,0 @@
package provider
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var _ datasource.DataSource = &VDCDataSource{}
func NewVDCDataSource() datasource.DataSource {
return &VDCDataSource{}
}
type VDCDataSource struct {
client *NubesClient
}
type VDCDataSourceModel struct {
ID types.String `tfsdk:"id"`
DisplayName types.String `tfsdk:"display_name"`
Status types.String `tfsdk:"status"`
Description types.String `tfsdk:"description"`
}
func (d *VDCDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_vdc"
}
func (d *VDCDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Data source для получения информации о существующем VDC по имени",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
MarkdownDescription: "UUID VDC",
Computed: true,
},
"display_name": schema.StringAttribute{
MarkdownDescription: "Имя VDC для поиска",
Required: true,
},
"status": schema.StringAttribute{
MarkdownDescription: "Статус VDC (running, suspended, etc.)",
Computed: true,
},
"description": schema.StringAttribute{
MarkdownDescription: "Описание VDC",
Computed: true,
},
},
}
}
func (d *VDCDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*NubesClient)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *NubesClient, got: %T", req.ProviderData),
)
return
}
d.client = client
}
func (d *VDCDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data VDCDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Поиск VDC по имени
httpReq, err := http.NewRequestWithContext(ctx, "GET", d.client.ApiEndpoint+"/instances", nil)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create request: %s", err))
return
}
httpReq.Header.Set("Authorization", "Bearer "+strings.TrimSpace(d.client.ApiToken))
httpReq.Header.Set("Content-Type", "application/json")
httpResp, err := d.client.HttpClient.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read instances: %s", err))
return
}
defer httpResp.Body.Close()
body, err := io.ReadAll(httpResp.Body)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read response: %s", err))
return
}
if httpResp.StatusCode != http.StatusOK {
resp.Diagnostics.AddError("API Error", fmt.Sprintf("GET /instances returned %d: %s", httpResp.StatusCode, string(body)))
return
}
var instancesResp struct {
Results []struct {
ID string `json:"id"`
DisplayName string `json:"name"`
Status string `json:"status"`
Description string `json:"desc"`
Service string `json:"svc"`
} `json:"results"`
}
if err := json.Unmarshal(body, &instancesResp); err != nil {
resp.Diagnostics.AddError("Parse Error", fmt.Sprintf("Unable to parse response: %s", err))
return
}
// Поиск VDC с указанным именем
searchName := data.DisplayName.ValueString()
for _, instance := range instancesResp.Results {
if instance.Service == "Виртуальный датацентр (vDC)" && instance.DisplayName == searchName {
data.ID = types.StringValue(instance.ID)
data.Status = types.StringValue(instance.Status)
data.Description = types.StringValue(instance.Description)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
return
}
}
resp.Diagnostics.AddError(
"VDC Not Found",
fmt.Sprintf("VDC с именем '%s' не найден. Проверьте имя или создайте новый VDC.", searchName),
)
}
-766
View File
@@ -1,766 +0,0 @@
package provider
// [НЕ ИЗМЕНЯТЬ !!!!]
// Данный ресурс реализует паттерн "Nubes Flow" для Virtual Data Center (VDC).
// ВАЖНО: VDC является фундаментальным ресурсом инфраструктуры.
//
// ЛОГИКА УДАЛЕНИЯ (Delete):
// Автоматическое удаление через API ОТКЛЮЧЕНО для защиты от случайной потери данных.
// При вызове 'terraform destroy' ресурс просто УДАЛЯЕТСЯ ИЗ СТЕЙТА Terraform, но остается в облаке.
// Для реального удаления юзер должен вручную перевести инстанс в 'suspend' через UI и дождаться удаления (14 дней).
//
// ЛОГИКА ИЗМЕНЕНИЯ (Modify):
// Если ресурс находится в стейте, выполнение 'terraform apply' вызовет метод Update,
// который запустит операцию 'modify' для обновления параметров (квот и т.д.).
// ВНИМАНИЕ: Если вы уже выполнили 'destroy' (удалили из стейта), но ресурс остался в облаке,
// то для его изменения через Terraform вам придется сначала выполнить 'terraform import'.
//
// TODO: Протестировать VDC 'modify' позже.
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"time"
"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"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
)
var _ resource.Resource = &VDCResource{}
var _ resource.ResourceWithImportState = &VDCResource{}
func NewVDCResource() resource.Resource {
return &VDCResource{}
}
type VDCResource struct {
client *NubesClient
}
type VDCResourceModel struct {
ID types.String `tfsdk:"id"`
DisplayName types.String `tfsdk:"display_name"`
Description types.String `tfsdk:"description"`
OrganizationUID types.String `tfsdk:"organization_uid"`
ProviderVDC types.String `tfsdk:"provider_vdc"`
StorageProfiles types.String `tfsdk:"storage_profiles"`
NetworkPool types.String `tfsdk:"network_pool"`
CpuAllocationPct types.Int64 `tfsdk:"cpu_allocation_pct"`
RamAllocationPct types.Int64 `tfsdk:"ram_allocation_pct"`
CpuQuota types.Int64 `tfsdk:"cpu_quota"`
RamQuota types.Int64 `tfsdk:"ram_quota"`
DeletionProtection types.Bool `tfsdk:"deletion_protection"`
Status types.String `tfsdk:"status"`
Timeouts timeouts.Value `tfsdk:"timeouts"`
}
func (r *VDCResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_vdc"
}
func (r *VDCResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Nubes VDC (Virtual Data Center) resource",
Blocks: map[string]schema.Block{
"timeouts": timeouts.Block(ctx, timeouts.Opts{
Create: true,
Update: true,
Delete: true,
}),
},
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
MarkdownDescription: "VDC identifier (UUID)",
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"display_name": schema.StringAttribute{
MarkdownDescription: "VDC display name",
Required: true,
},
"description": schema.StringAttribute{
MarkdownDescription: "VDC description",
Optional: true,
},
"organization_uid": schema.StringAttribute{
MarkdownDescription: "Organization UUID (parameter ID 30)",
Required: true,
},
"provider_vdc": schema.StringAttribute{
MarkdownDescription: "Provider VDC name in Cloud Director (parameter ID 335)",
Required: true,
},
"storage_profiles": schema.StringAttribute{
MarkdownDescription: "Storage profiles JSON array, e.g. [{\"name\":\"vsan\",\"size\":\"100\"}] (parameter ID 361)",
Required: true,
},
"network_pool": schema.StringAttribute{
MarkdownDescription: "Network Pool name in Cloud Director (parameter ID 366)",
Required: true,
},
"cpu_allocation_pct": schema.Int64Attribute{
MarkdownDescription: "CPU allocation percentage (parameter ID 397)",
Required: true,
},
"ram_allocation_pct": schema.Int64Attribute{
MarkdownDescription: "RAM allocation percentage (parameter ID 398)",
Required: true,
},
"cpu_quota": schema.Int64Attribute{
MarkdownDescription: "CPU quota (parameter ID 557)",
Optional: true,
},
"ram_quota": schema.Int64Attribute{
MarkdownDescription: "RAM quota (parameter ID 558)",
Optional: true,
},
"deletion_protection": schema.BoolAttribute{
MarkdownDescription: "If true, the resource will only be removed from Terraform state upon destroy, but will remain in the cloud. If false, destroy will trigger 'suspend' in Nubes.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(true),
},
"status": schema.StringAttribute{
MarkdownDescription: "Current status of the VDC",
Computed: true,
},
},
}
}
func (r *VDCResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*NubesClient)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *NubesClient, got: %T", req.ProviderData),
)
return
}
r.client = client
}
func (r *VDCResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
// [ЛОГИКА VDC FLOW]
// Ресурс VDC создается по стандартному 7-шаговому алгоритму Nubes.
// Особое внимание уделяется параметрам квот (CPU/RAM) и сетевым пулам.
var data VDCResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Apply timeout
createTimeout, diags := data.Timeouts.Create(ctx, 3*time.Minute)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
ctx, cancel := context.WithTimeout(ctx, createTimeout)
defer cancel()
// Step 1: Create instance
createReq := CreateInstanceRequest{
ServiceId: 21, // VDC service ID
DisplayName: data.DisplayName.ValueString(),
Descr: data.Description.ValueString(),
}
jsonData, err := json.Marshal(createReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to marshal request: %s", err))
return
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", r.client.ApiEndpoint+"/instances", bytes.NewBuffer(jsonData))
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create request: %s", err))
return
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create instance: %s", err))
return
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(httpResp.Body)
resp.Diagnostics.AddError(
"API Error",
fmt.Sprintf("Create instance failed with status %d: %s", httpResp.StatusCode, string(body)),
)
return
}
location := httpResp.Header.Get("Location")
if location == "" {
resp.Diagnostics.AddError("API Error", "No Location header in response")
return
}
instanceId := location[2:]
data.ID = types.StringValue(instanceId)
// Step 2: Create operation
// Deck API требует явного создания операции 'create' для инстанса.
// Это переводит инстанс в состояние "wizard", где можно настраивать параметры.
operationReq := CreateOperationRequest{
InstanceUid: instanceId,
Operation: "create",
}
jsonData, err = json.Marshal(operationReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to marshal operation request: %s", err))
return
}
httpReq, err = http.NewRequestWithContext(ctx, "POST", r.client.ApiEndpoint+"/instanceOperations", bytes.NewBuffer(jsonData))
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create operation request: %s", err))
return
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err = r.client.HttpClient.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create operation: %s", err))
return
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(httpResp.Body)
resp.Diagnostics.AddError(
"API Error",
fmt.Sprintf("Create operation failed with status %d: %s", httpResp.StatusCode, string(body)),
)
return
}
location = httpResp.Header.Get("Location")
if location == "" {
resp.Diagnostics.AddError("API Error", "No Location header in operation response")
return
}
operationId := location[2:]
// Step 3: Submit operation parameters and run
if err := r.submitOperationParams(ctx, operationId, data); err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to submit operation parameters: %s", err))
return
}
// Step 4: Wait for operation to complete, then check instance status
if err := r.waitForOperationAndInstanceStatus(ctx, operationId, instanceId, 10*time.Minute); err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Instance failed to reach running status: %s", err))
return
}
readData, err := r.readInstance(ctx, instanceId)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read instance after creation: %s", err))
return
}
data.Status = types.StringValue(readData.Status)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *VDCResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data VDCResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
instanceResp, err := r.readInstance(ctx, data.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read instance: %s", err))
return
}
data.Status = types.StringValue(instanceResp.Status)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *VDCResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
// [НЕ ИЗМЕНЯТЬ !!!!]
// VDC поддерживает изменение параметров через операцию 'modify'.
// Это позволяет обновлять квоты CPU, RAM и другие параметры без пересоздания ресурса.
var data VDCResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
operationReq := CreateOperationRequest{
InstanceUid: data.ID.ValueString(),
Operation: "modify",
}
jsonData, err := json.Marshal(operationReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to marshal operation request: %s", err))
return
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", r.client.ApiEndpoint+"/instanceOperations", bytes.NewBuffer(jsonData))
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create operation request: %s", err))
return
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update instance: %s", err))
return
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(httpResp.Body)
resp.Diagnostics.AddError(
"API Error",
fmt.Sprintf("Modify operation failed with status %d: %s", httpResp.StatusCode, string(body)),
)
return
}
location := httpResp.Header.Get("Location")
if location != "" {
operationId := location[2:]
if err := r.submitOperationParams(ctx, operationId, data); err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to submit operation parameters: %s", err))
return
}
}
time.Sleep(5 * time.Second)
readData, err := r.readInstance(ctx, data.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read instance after update: %s", err))
return
}
data.Status = types.StringValue(readData.Status)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *VDCResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
// ЛОГИКА УДАЛЕНИЯ VDC:
// В зависимости от флага deletion_protection:
// 1. Если true (по умолчанию): Только удаляем из стейта. VDC остается работать.
// 2. Если false: Вызываем операцию 'suspend' через API.
// Доступа к VDC больше не будет, данные сохраняются 14 дней, затем авто-удаление.
var data VDCResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
if data.DeletionProtection.ValueBool() {
tflog.Warn(ctx, "Deletion Protection is ENABLED. VDC will remain active in Nubes Cloud. Manual cleanup required.")
return
}
tflog.Info(ctx, "Deletion Protection is DISABLED. Triggering 'suspend' for VDC...")
instanceId := data.ID.ValueString()
// Выполняем операцию suspend
err := r.triggerOperation(ctx, instanceId, "suspend")
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to suspend VDC: %s", err))
return
}
tflog.Info(ctx, "VDC suspended successfully. It will be permanently deleted from the cloud in 14 days.")
}
func (r *VDCResource) triggerOperation(ctx context.Context, instanceId string, operationName string) error {
opReq := InstanceOperationRequest{
Action: operationName,
Params: struct{}{},
}
jsonData, err := json.Marshal(opReq)
if err != nil {
return fmt.Errorf("unable to marshal request: %s", err)
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", r.client.ApiEndpoint+"/instances/"+instanceId+"/run", bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("unable to create request: %s", err)
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
return fmt.Errorf("unable to trigger operation: %s", err)
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusOK && httpResp.StatusCode != http.StatusAccepted {
body, _ := io.ReadAll(httpResp.Body)
return fmt.Errorf("operation failed with status %d: %s", httpResp.StatusCode, string(body))
}
// Ждем пока статус изменится на целевой (например, suspended)
targetStatus := "running"
if operationName == "suspend" {
targetStatus = "suspended"
}
return r.client.WaitForInstanceStatus(ctx, instanceId, targetStatus)
}
func (r *VDCResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}
func (r *VDCResource) readInstance(ctx context.Context, id string) (*InstanceResponse, error) {
httpReq, err := http.NewRequestWithContext(ctx, "GET", r.client.ApiEndpoint+"/instances/"+id, nil)
if err != nil {
return nil, err
}
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
return nil, err
}
defer httpResp.Body.Close()
body, err := io.ReadAll(httpResp.Body)
if err != nil {
return nil, err
}
if httpResp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("read request failed with status %d: %s", httpResp.StatusCode, string(body))
}
var instanceResp InstanceResponse
if err := json.Unmarshal(body, &instanceResp); err != nil {
return nil, err
}
return &instanceResp, nil
}
func (r *VDCResource) submitOperationParams(ctx context.Context, operationUid string, data VDCResourceModel) error {
// [ЛОГИКА СИНХРОНИЗАЦИИ ПАРАМЕТРОВ]
// 1. Получаем список параметров операции (GET /instanceOperations/{uid}?fields=cfsParams).
// 2. Для каждого параметра из списка находим значение в Terraform или используем дефолт.
// 3. Отправляем значение обратно в API (POST /instanceOperationCfsParams).
// 4. После синхронизации всех параметров вызываем RUN.
// Get operation details
httpReq, err := http.NewRequestWithContext(ctx, "GET",
r.client.ApiEndpoint+"/instanceOperations/"+operationUid+"?fields=cfsParams", nil)
if err != nil {
return fmt.Errorf("unable to create request: %s", err)
}
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
return fmt.Errorf("unable to get operation: %s", err)
}
defer httpResp.Body.Close()
body, err := io.ReadAll(httpResp.Body)
if err != nil {
return fmt.Errorf("unable to read response: %s", err)
}
if httpResp.StatusCode != http.StatusOK {
return fmt.Errorf("get operation failed with status %d: %s", httpResp.StatusCode, string(body))
}
var getResp GetOperationResponse
if err := json.Unmarshal(body, &getResp); err != nil {
return fmt.Errorf("unable to unmarshal response: %s", err)
}
operationResp := getResp.InstanceOperation
// Submit each parameter
for _, param := range operationResp.CfsParams {
valToSend := ""
// Map VDC parameters by ID
switch param.SvcOperationCfsParamId {
case 30: // organizationUid
if !data.OrganizationUID.IsNull() && !data.OrganizationUID.IsUnknown() {
valToSend = data.OrganizationUID.ValueString()
}
case 335: // providerVdc
if !data.ProviderVDC.IsNull() && !data.ProviderVDC.IsUnknown() {
valToSend = data.ProviderVDC.ValueString()
}
case 361: // storageProfiles (JSON)
if !data.StorageProfiles.IsNull() && !data.StorageProfiles.IsUnknown() {
valToSend = data.StorageProfiles.ValueString()
}
case 366: // networkPool
if !data.NetworkPool.IsNull() && !data.NetworkPool.IsUnknown() {
valToSend = data.NetworkPool.ValueString()
}
case 397: // cpuAllocationPct
if !data.CpuAllocationPct.IsNull() && !data.CpuAllocationPct.IsUnknown() {
valToSend = fmt.Sprintf("%d", data.CpuAllocationPct.ValueInt64())
}
case 398: // ramAllocationPct
if !data.RamAllocationPct.IsNull() && !data.RamAllocationPct.IsUnknown() {
valToSend = fmt.Sprintf("%d", data.RamAllocationPct.ValueInt64())
}
case 557: // cpuQuota
if !data.CpuQuota.IsNull() && !data.CpuQuota.IsUnknown() {
valToSend = fmt.Sprintf("%d", data.CpuQuota.ValueInt64())
}
case 558: // ramQuota
if !data.RamQuota.IsNull() && !data.RamQuota.IsUnknown() {
valToSend = fmt.Sprintf("%d", data.RamQuota.ValueInt64())
}
default:
// Use existing or default value for unknown parameters
if param.ParamValue != nil {
valToSend = *param.ParamValue
} else if param.DefaultValue != nil {
valToSend = *param.DefaultValue
}
}
// Fix specific data type formatting
if valToSend == "" || valToSend == "\"\"" {
if param.DataType == "map" || param.DataType == "json" {
valToSend = "{}"
} else if param.DataType == "array" || param.DataType == "list" {
valToSend = "[]"
}
}
paramReq := CreateCfsParamRequest{
InstanceOperationUid: operationUid,
SvcOperationCfsParamId: param.SvcOperationCfsParamId,
ParamValue: valToSend,
}
jsonData, err := json.Marshal(paramReq)
if err != nil {
return fmt.Errorf("unable to marshal param request: %s", err)
}
httpReq, err = http.NewRequestWithContext(ctx, "POST",
r.client.ApiEndpoint+"/instanceOperationCfsParams", bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("unable to create param request: %s", err)
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err = r.client.HttpClient.Do(httpReq)
if err != nil {
return fmt.Errorf("unable to submit parameter: %s", err)
}
respBody, _ := io.ReadAll(httpResp.Body)
httpResp.Body.Close()
if httpResp.StatusCode != http.StatusCreated &&
httpResp.StatusCode != http.StatusOK &&
httpResp.StatusCode != http.StatusNoContent {
return fmt.Errorf("submit parameter id %d failed with status %d: %s",
param.SvcOperationCfsParamId, httpResp.StatusCode, string(respBody))
}
}
// Run the operation
runReq, err := http.NewRequestWithContext(ctx, "POST",
r.client.ApiEndpoint+"/instanceOperations/"+operationUid+"/run", bytes.NewBuffer([]byte("{}")))
if err != nil {
return fmt.Errorf("unable to create run request: %s", err)
}
runReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
runReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
runResp, err := r.client.HttpClient.Do(runReq)
if err != nil {
return fmt.Errorf("unable to run operation: %s", err)
}
defer runResp.Body.Close()
if runResp.StatusCode != http.StatusOK &&
runResp.StatusCode != http.StatusNoContent &&
runResp.StatusCode != http.StatusCreated {
runBody, _ := io.ReadAll(runResp.Body)
return fmt.Errorf("run operation failed with status %d: %s",
runResp.StatusCode, string(runBody))
}
return nil
}
func (r *VDCResource) waitForOperationAndInstanceStatus(ctx context.Context, operationId string, instanceId string, timeout time.Duration) error {
deadline := time.Now().Add(timeout)
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
log.Printf("[DEBUG] Starting two-stage polling: operationId=%s, instanceId=%s, timeout=%v", operationId, instanceId, timeout)
// Шаг 1: Ждём завершения операции
operationLoop:
for {
select {
case <-ctx.Done():
return fmt.Errorf("context cancelled")
case <-ticker.C:
if time.Now().After(deadline) {
return fmt.Errorf("timeout waiting for operation to complete")
}
log.Printf("[DEBUG] Polling operation status: operationId=%s", operationId)
// Проверяем статус операции
httpReq, err := http.NewRequestWithContext(ctx, "GET",
r.client.ApiEndpoint+"/instanceOperations/"+operationId, nil)
if err != nil {
return fmt.Errorf("failed to create request: %s", err)
}
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
return fmt.Errorf("failed to check operation status: %s", err)
}
body, _ := io.ReadAll(httpResp.Body)
httpResp.Body.Close()
if httpResp.StatusCode != http.StatusOK {
return fmt.Errorf("get operation failed with status %d", httpResp.StatusCode)
}
var opResp struct {
InstanceOperation struct {
IsInProgress bool `json:"isInProgress"`
IsPending bool `json:"isPending"`
} `json:"instanceOperation"`
}
if err := json.Unmarshal(body, &opResp); err != nil {
return fmt.Errorf("failed to parse operation response: %s", err)
}
log.Printf("[DEBUG] Operation status: isInProgress=%v, isPending=%v", opResp.InstanceOperation.IsInProgress, opResp.InstanceOperation.IsPending)
// Операция завершена когда isInProgress=false И isPending=false
if !opResp.InstanceOperation.IsInProgress && !opResp.InstanceOperation.IsPending {
log.Printf("[DEBUG] Operation completed, moving to instance status check")
break operationLoop
}
}
}
// Шаг 2: Проверяем статус instance
ticker2 := time.NewTicker(5 * time.Second)
defer ticker2.Stop()
log.Printf("[DEBUG] Starting instance status polling")
for {
select {
case <-ctx.Done():
return fmt.Errorf("context cancelled")
case <-ticker2.C:
if time.Now().After(deadline) {
return fmt.Errorf("timeout waiting for instance to become running")
}
log.Printf("[DEBUG] Polling instance status: instanceId=%s", instanceId)
instance, err := r.readInstance(ctx, instanceId)
if err != nil {
return fmt.Errorf("failed to check instance status: %s", err)
}
log.Printf("[DEBUG] Instance status: %s", instance.Status)
if instance.Status == "running" {
log.Printf("[DEBUG] Instance is running, success!")
return nil
}
if instance.Status == "error" || instance.Status == "failed" {
return fmt.Errorf("instance entered error state: %s", instance.Status)
}
}
}
}
-939
View File
@@ -1,939 +0,0 @@
package provider
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
"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"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
)
var _ resource.Resource = &VMResource{}
var _ resource.ResourceWithImportState = &VMResource{}
func NewVMResource() resource.Resource {
return &VMResource{}
}
type VMResource struct {
client *NubesClient
}
type VMResourceModel struct {
ID types.String `tfsdk:"id"`
DisplayName types.String `tfsdk:"display_name"`
Description types.String `tfsdk:"description"`
Status types.String `tfsdk:"status"`
VappUid types.String `tfsdk:"vapp_uid"`
VmName types.String `tfsdk:"vm_name"`
VmCpu types.Int64 `tfsdk:"vm_cpu"`
VmRam types.Int64 `tfsdk:"vm_ram"`
AccessPortList types.String `tfsdk:"access_port_list"`
ImageVm types.String `tfsdk:"image_vm"`
UserLogin types.String `tfsdk:"user_login"`
UserPublicKey types.String `tfsdk:"user_public_key"`
NeedAddZabbixTemplate types.Bool `tfsdk:"need_add_zabbix_template"`
AccessIpList types.String `tfsdk:"access_ip_list"`
VmDisk types.Int64 `tfsdk:"vm_disk"`
IpSpaceName types.String `tfsdk:"ip_space_name"`
CloudInit types.String `tfsdk:"cloud_init"`
ResourceRealm types.String `tfsdk:"resource_realm"`
DeletionProtection types.Bool `tfsdk:"deletion_protection"`
}
func (r *VMResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_vm_instance"
}
func (r *VMResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Nubes VM Instance resource",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Instance identifier (UUID)",
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"display_name": schema.StringAttribute{
MarkdownDescription: "Instance display name",
Required: true,
},
"description": schema.StringAttribute{
MarkdownDescription: "Instance description",
Optional: true,
},
"status": schema.StringAttribute{
MarkdownDescription: "Current status of the instance",
Computed: true,
},
"vapp_uid": schema.StringAttribute{
MarkdownDescription: "UUID vApp, в которой будет создаваться виртуальная машина",
Required: true,
},
"vm_name": schema.StringAttribute{
MarkdownDescription: "Уникальное имя виртуальной машины",
Required: true,
},
"vm_cpu": schema.Int64Attribute{
MarkdownDescription: "Количество ядер процессора. Должно быть больше 0",
Required: true,
Validators: []validator.Int64{
int64validator.AtLeast(1),
},
},
"vm_ram": schema.Int64Attribute{
MarkdownDescription: "Объем оперативной памяти в гигабайтах. Должно быть больше 0",
Required: true,
Validators: []validator.Int64{
int64validator.AtLeast(1),
},
},
"access_port_list": schema.StringAttribute{
MarkdownDescription: "Белый список портов для доступа к виртуальной машине извне в формате JSON. Пример: jsonencode([\"22\", \"80\"])",
Required: true,
Validators: []validator.String{
ValidJSONArray(),
},
},
"image_vm": schema.StringAttribute{
MarkdownDescription: "Образ операционной системы для развёртывания (RockyLinux_9-16G-cloudinit, Ubuntu_22-20G, Debian_13-20G)",
Required: true,
},
"user_login": schema.StringAttribute{
MarkdownDescription: "Логин пользователя для SSH-доступа",
Required: true,
},
"user_public_key": schema.StringAttribute{
MarkdownDescription: "Публичный SSH-ключ пользователя в формате OpenSSH",
Required: true,
},
"need_add_zabbix_template": schema.BoolAttribute{
MarkdownDescription: "Значение истина\\ложь, определяющее будет ли добавлен хост в zabbix",
Required: true,
},
"access_ip_list": schema.StringAttribute{
MarkdownDescription: "Белый список IP-адресов для доступа к виртуальной машине в формате JSON. Пример: jsonencode([\"1.2.3.4\", \"10.0.0.0/8\"]). Пустая строка или не указан = доступ отовсюду",
Optional: true,
Computed: true,
Validators: []validator.String{
ValidJSONArrayOrEmpty(),
},
},
"vm_disk": schema.Int64Attribute{
MarkdownDescription: "Размер дополнительного диска в гигабайтах. Должно быть больше 0",
Optional: true,
Validators: []validator.Int64{
int64validator.AtLeast(1),
},
},
"ip_space_name": schema.StringAttribute{
MarkdownDescription: "Внешний IP-адрес. Если не требуется — укажите значение 'no-needed'",
Optional: true,
},
"cloud_init": schema.StringAttribute{
MarkdownDescription: "YAML-скрипт для кастомизации системы через cloud-init",
Optional: true,
},
"resource_realm": schema.StringAttribute{
MarkdownDescription: "Платформа ресурса (например, vcd, openstack)",
Optional: true,
},
"deletion_protection": schema.BoolAttribute{
MarkdownDescription: "Если true, при удалении ресурса из Terraform он просто удаляется из стейта. Если false, отправляется команда suspend (карантин 14 дней).",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(true),
},
},
}
}
func (r *VMResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*NubesClient)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *NubesClient, got: %T", req.ProviderData),
)
return
}
r.client = client
}
func (r *VMResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data VMResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Get service operation ID for 'create'
serviceId := 28 // VM service
var instanceUid string
svcOperationId, err := r.client.GetOperationId(ctx, serviceId, "create")
if err != nil {
resp.Diagnostics.AddError("Failed to get create operation ID", err.Error())
return
}
// Prepare parameters
params := r.prepareVMParams(&data)
// Use client.CreateInstance like Postgres does
displayName := data.DisplayName.ValueString()
var opUid string
instanceUid, opUid, err = r.client.CreateInstance(ctx, displayName, serviceId, svcOperationId, params)
if err != nil {
resp.Diagnostics.AddError("Error creating VM", err.Error())
return
}
// Wait for operation success (like Postgres)
err = r.client.WaitForOperation(ctx, opUid)
if err != nil {
resp.Diagnostics.AddError("Error waiting for VM creation", err.Error())
return
}
// Wait for instance to be running (like Postgres does)
err = r.client.WaitForInstanceStatus(ctx, instanceUid, "running")
if err != nil {
resp.Diagnostics.AddError("Error waiting for VM running status", err.Error())
return
}
data.ID = types.StringValue(instanceUid)
// Read final state
readData, err := r.readInstance(ctx, instanceUid)
if err != nil {
resp.Diagnostics.AddWarning("Unable to read instance after creation", err.Error())
} else {
data.Status = types.StringValue(readData.Status)
}
// Fix "Provider returned invalid result object" for Computed fields
// AccessIpList is Computed + Optional. If it was not provided, we sent "", so we must set it to ""
if data.AccessIpList.IsNull() || data.AccessIpList.IsUnknown() {
data.AccessIpList = types.StringValue("")
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
// prepareVMParams собирает параметры для VM (выделено в отдельную функцию для переиспользования)
func (r *VMResource) prepareVMParams(data *VMResourceModel) []InstanceParam {
params := []InstanceParam{}
// Hardcoded parameter mapping (от HAR анализа)
paramMapping := map[string]int{
"vappUid": 407, // vApp UUID
"vmName": 408, // VM name
"vmCpu": 409, // CPU count
"vmRam": 410, // RAM in GB
"vmDisk": 411, // Additional disk in GB
"ipSpaceName": 412, // IP space name
"accessIpList": 413, // JSON array of allowed IPs (or empty string for all)
"imageVm": 414, // OS image name
"cloudInit": 415, // Cloud-init script
"userLogin": 416, // SSH username
"userPublicKey": 417, // SSH public key
"accessPortList": 448, // JSON array of ports
"needAddZabbixTemplate": 449, // Boolean for Zabbix monitoring
}
// Собираем все параметры в том же порядке что и UI (по возрастанию ID)
// vappUid (407)
if !data.VappUid.IsNull() && !data.VappUid.IsUnknown() {
params = append(params, InstanceParam{SvcOperationCfsParamId: paramMapping["vappUid"], ParamValue: data.VappUid.ValueString()})
}
// vmName (408)
if !data.VmName.IsNull() && !data.VmName.IsUnknown() {
params = append(params, InstanceParam{SvcOperationCfsParamId: paramMapping["vmName"], ParamValue: data.VmName.ValueString()})
}
// vmCpu (409)
if !data.VmCpu.IsNull() && !data.VmCpu.IsUnknown() {
params = append(params, InstanceParam{SvcOperationCfsParamId: paramMapping["vmCpu"], ParamValue: fmt.Sprintf("%d", data.VmCpu.ValueInt64())})
}
// vmRam (410)
if !data.VmRam.IsNull() && !data.VmRam.IsUnknown() {
params = append(params, InstanceParam{SvcOperationCfsParamId: paramMapping["vmRam"], ParamValue: fmt.Sprintf("%d", data.VmRam.ValueInt64())})
}
// vmDisk (411) - ВСЕГДА отправляем (пустая строка если не задан)
if !data.VmDisk.IsNull() && !data.VmDisk.IsUnknown() && data.VmDisk.ValueInt64() > 0 {
params = append(params, InstanceParam{SvcOperationCfsParamId: paramMapping["vmDisk"], ParamValue: fmt.Sprintf("%d", data.VmDisk.ValueInt64())})
} else {
params = append(params, InstanceParam{SvcOperationCfsParamId: paramMapping["vmDisk"], ParamValue: ""})
}
// ipSpaceName (412)
if !data.IpSpaceName.IsNull() && !data.IpSpaceName.IsUnknown() {
params = append(params, InstanceParam{SvcOperationCfsParamId: paramMapping["ipSpaceName"], ParamValue: data.IpSpaceName.ValueString()})
}
// accessIpList (413) - ВСЕГДА отправляем (пустая строка если не задан)
if !data.AccessIpList.IsNull() && !data.AccessIpList.IsUnknown() {
params = append(params, InstanceParam{SvcOperationCfsParamId: paramMapping["accessIpList"], ParamValue: data.AccessIpList.ValueString()})
} else {
params = append(params, InstanceParam{SvcOperationCfsParamId: paramMapping["accessIpList"], ParamValue: ""})
}
// imageVm (414)
if !data.ImageVm.IsNull() && !data.ImageVm.IsUnknown() {
params = append(params, InstanceParam{SvcOperationCfsParamId: paramMapping["imageVm"], ParamValue: data.ImageVm.ValueString()})
}
// cloudInit (415) - ВСЕГДА отправляем (пустая строка если не задан)
if !data.CloudInit.IsNull() && !data.CloudInit.IsUnknown() {
params = append(params, InstanceParam{SvcOperationCfsParamId: paramMapping["cloudInit"], ParamValue: data.CloudInit.ValueString()})
} else {
params = append(params, InstanceParam{SvcOperationCfsParamId: paramMapping["cloudInit"], ParamValue: ""})
}
// userLogin (416)
if !data.UserLogin.IsNull() && !data.UserLogin.IsUnknown() {
params = append(params, InstanceParam{SvcOperationCfsParamId: paramMapping["userLogin"], ParamValue: data.UserLogin.ValueString()})
}
// userPublicKey (417)
if !data.UserPublicKey.IsNull() && !data.UserPublicKey.IsUnknown() {
params = append(params, InstanceParam{SvcOperationCfsParamId: paramMapping["userPublicKey"], ParamValue: data.UserPublicKey.ValueString()})
}
// accessPortList (448)
if !data.AccessPortList.IsNull() && !data.AccessPortList.IsUnknown() {
params = append(params, InstanceParam{SvcOperationCfsParamId: paramMapping["accessPortList"], ParamValue: data.AccessPortList.ValueString()})
}
// needAddZabbixTemplate (449) - ВСЕГДА отправляем "true" (как в UI)
params = append(params, InstanceParam{SvcOperationCfsParamId: paramMapping["needAddZabbixTemplate"], ParamValue: "true"})
return params
}
func (r *VMResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data VMResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
instanceResp, err := r.readInstance(ctx, data.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read instance: %s", err))
return
}
data.Status = types.StringValue(instanceResp.Status)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *VMResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan VMResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
var state VMResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
// Trigger modify operation
operationReq := CreateOperationRequest{
InstanceUid: plan.ID.ValueString(),
Operation: "modify",
}
jsonData, err := json.Marshal(operationReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to marshal operation request: %s", err))
return
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", r.client.ApiEndpoint+"/instanceOperations", bytes.NewBuffer(jsonData))
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create operation request: %s", err))
return
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update instance: %s", err))
return
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(httpResp.Body)
resp.Diagnostics.AddError(
"API Error",
fmt.Sprintf("Modify operation failed with status %d: %s", httpResp.StatusCode, string(body)),
)
return
}
// Extract operation ID and submit parameters
location := httpResp.Header.Get("Location")
if location != "" {
operationId := location[2:]
// Wait for backend to possibly populate params
tflog.Info(ctx, "Waiting 5 seconds for operation parameters to initialize...")
time.Sleep(5 * time.Second)
// Discovery parameters for modify to debug 400 error
tflog.Info(ctx, fmt.Sprintf("Discovering parameters for operation %s", operationId))
op, err := r.client.GetInstanceOperation(ctx, operationId)
if err == nil {
for _, p := range op.CfsParams {
tflog.Info(ctx, fmt.Sprintf("Allowed Param: %s (ID: %d)", p.SvcOperationCfsParam, p.SvcOperationCfsParamId))
}
}
if err := r.submitVMOperationParams(ctx, operationId, plan, state); err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to submit operation parameters: %s", err))
return
}
}
// Read updated state
time.Sleep(5 * time.Second)
readData, err := r.readInstance(ctx, plan.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read instance after update: %s", err))
return
}
plan.Status = types.StringValue(readData.Status)
// Fix "Provider returned invalid result object" for Computed fields
if plan.AccessIpList.IsNull() || plan.AccessIpList.IsUnknown() {
plan.AccessIpList = types.StringValue("")
}
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}
func (r *VMResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data VMResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Deletion Logic:
// If DeletionProtection is TRUE (default) -> Remove from state, do not touch cloud resource.
// If DeletionProtection is FALSE -> Send 'suspend' operation (quarantine) + remove from state.
if !data.DeletionProtection.IsUnknown() && data.DeletionProtection.ValueBool() {
tflog.Info(ctx, "DeletionProtection is enabled. Resource will be removed from state but kept in cloud.")
return
}
tflog.Info(ctx, "DeletionProtection is disabled. Initiating 'suspend' operation (quarantine).")
// Trigger suspend operation (instead of delete)
operationReq := CreateOperationRequest{
InstanceUid: data.ID.ValueString(),
Operation: "suspend",
}
jsonData, err := json.Marshal(operationReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to marshal operation request: %s", err))
return
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", r.client.ApiEndpoint+"/instanceOperations", bytes.NewBuffer(jsonData))
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create operation request: %s", err))
return
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to suspend instance: %s", err))
return
}
defer httpResp.Body.Close()
if httpResp.StatusCode != http.StatusCreated && httpResp.StatusCode != http.StatusNotFound {
body, _ := io.ReadAll(httpResp.Body)
resp.Diagnostics.AddError(
"API Error",
fmt.Sprintf("Suspend operation failed with status %d: %s", httpResp.StatusCode, string(body)),
)
return
}
// Extract operation ID and run operation
location := httpResp.Header.Get("Location")
if location != "" {
operationId := location[2:]
// For suspend, we typically don't need parameters. Just Run.
tflog.Info(ctx, fmt.Sprintf("Running suspend operation %s", operationId))
runReq, err := http.NewRequestWithContext(ctx, "POST",
r.client.ApiEndpoint+"/instanceOperations/"+operationId+"/run", bytes.NewBuffer([]byte("{}")))
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create run request: %s", err))
return
}
runReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
runReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
runResp, err := r.client.HttpClient.Do(runReq)
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to run run request: %s", err))
return
}
defer runResp.Body.Close()
if runResp.StatusCode != http.StatusOK && runResp.StatusCode != http.StatusNoContent && runResp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(runResp.Body)
resp.Diagnostics.AddWarning("Client Warning", fmt.Sprintf("Run suspend failed with status %d: %s", runResp.StatusCode, string(body)))
}
}
}
func (r *VMResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}
func (r *VMResource) readInstance(ctx context.Context, id string) (*InstanceResponse, error) {
httpReq, err := http.NewRequestWithContext(ctx, "GET", r.client.ApiEndpoint+"/instances/"+id, nil)
if err != nil {
return nil, err
}
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
return nil, err
}
defer httpResp.Body.Close()
body, err := io.ReadAll(httpResp.Body)
if err != nil {
return nil, err
}
if httpResp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("read request failed with status %d: %s", httpResp.StatusCode, string(body))
}
var response struct {
Instance InstanceResponse `json:"instance"`
}
if err := json.Unmarshal(body, &response); err != nil {
tflog.Error(ctx, fmt.Sprintf("Failed to unmarshal instance details: %s, Body: %s", err, string(body)))
return nil, err
}
tflog.Info(ctx, fmt.Sprintf("Read Instance %s: Status='%s', ExplainedStatus='%s'", id, response.Instance.Status, response.Instance.Status))
return &response.Instance, nil
}
func (r *VMResource) submitVMOperationParams(ctx context.Context, operationUid string, plan VMResourceModel, state VMResourceModel) error {
// Step 1: Get operation details with parameters to build dynamic mapping
op, err := r.client.GetInstanceOperation(ctx, operationUid)
if err != nil {
return fmt.Errorf("unable to get operation parameters: %s", err)
}
// Step 2: Build parameter mapping dynamically
type ParamInfo struct {
Id int
Uid string
CurrentValue string
}
paramMapping := make(map[string]ParamInfo)
for _, p := range op.CfsParams {
// Map param name (e.g. "vmCpu") to info (ID + InstanceParamUID)
curVal := ""
if p.ParamValue != nil {
curVal = *p.ParamValue
}
paramMapping[p.SvcOperationCfsParam] = ParamInfo{
Id: p.SvcOperationCfsParamId,
Uid: p.InstanceOperationCfsParamUid,
CurrentValue: curVal,
}
}
tflog.Info(ctx, fmt.Sprintf("Built dynamic parameter mapping for operation %s: %v", operationUid, paramMapping))
// Step 3: Build named parameters list
type NamedParam struct {
Name string
Value string // Must be strictly string
}
namedParams := []NamedParam{}
// Add ALL potential parameters. The mapping check will filter out those not applicable to the current operation.
// vmName
/*
if !plan.VmName.IsNull() && !plan.VmName.IsUnknown() {
// Only send if changed
if !plan.VmName.Equal(state.VmName) {
namedParams = append(namedParams, NamedParam{"vmName", plan.VmName.ValueString()})
}
}
*/
// vmCpu
if !plan.VmCpu.IsNull() && !plan.VmCpu.IsUnknown() {
if !plan.VmCpu.Equal(state.VmCpu) {
namedParams = append(namedParams, NamedParam{"vmCpu", fmt.Sprintf("%d", plan.VmCpu.ValueInt64())})
}
}
// vmRam
/*
if !plan.VmRam.IsNull() && !plan.VmRam.IsUnknown() {
if !plan.VmRam.Equal(state.VmRam) {
namedParams = append(namedParams, NamedParam{"vmRam", fmt.Sprintf("%d", plan.VmRam.ValueInt64())})
}
}
// vmDisk
if !plan.VmDisk.IsNull() && !plan.VmDisk.IsUnknown() {
val := plan.VmDisk.ValueInt64()
if val > 0 && !plan.VmDisk.Equal(state.VmDisk) {
namedParams = append(namedParams, NamedParam{"vmDisk", fmt.Sprintf("%d", val)})
}
}
// ipSpaceName
if !plan.IpSpaceName.IsNull() && !plan.IpSpaceName.IsUnknown() {
if !plan.IpSpaceName.Equal(state.IpSpaceName) {
namedParams = append(namedParams, NamedParam{"ipSpaceName", plan.IpSpaceName.ValueString()})
}
}
*/
// accessIpList
/*
if !plan.AccessIpList.IsNull() && !plan.AccessIpList.IsUnknown() {
val := plan.AccessIpList.ValueString()
// If ipSpaceName is "no-needed", SKIP sending this parameter
isNoNeeded := !plan.IpSpaceName.IsNull() && plan.IpSpaceName.ValueString() == "no-needed"
if !isNoNeeded {
if !plan.AccessIpList.Equal(state.AccessIpList) {
namedParams = append(namedParams, NamedParam{"accessIpList", val})
}
}
}
// accessPortList
if !plan.AccessPortList.IsNull() && !plan.AccessPortList.IsUnknown() {
val := plan.AccessPortList.ValueString()
// If ipSpaceName is "no-needed", SKIP sending this parameter
isNoNeeded := !plan.IpSpaceName.IsNull() && plan.IpSpaceName.ValueString() == "no-needed"
if !isNoNeeded {
if !plan.AccessPortList.Equal(state.AccessPortList) {
namedParams = append(namedParams, NamedParam{"accessPortList", val})
}
}
}
*/
// needAddZabbixTemplate
// Skip sending to avoid duplicates unless we really know it changed.
// Since we don't track it in state (usually), we omit it.
// Create-only params (will be filtered out for Modify automatically but good to skip anyway)
// Generally they shouldn't change in Modify.
// imageVm, cloudInit, userLogin, userPublicKey -> usually ForceNew?
// If they are not ForceNew, check change.
// Assuming ForceNew behavior for image/user details, but if they are Updateable:
/*
if !plan.ImageVm.IsNull() && !plan.ImageVm.IsUnknown() && !plan.ImageVm.Equal(state.ImageVm) {
namedParams = append(namedParams, NamedParam{"imageVm", plan.ImageVm.ValueString()})
}
if !plan.CloudInit.IsNull() && !plan.CloudInit.IsUnknown() && !plan.CloudInit.Equal(state.CloudInit) {
namedParams = append(namedParams, NamedParam{"cloudInit", plan.CloudInit.ValueString()})
}
if !plan.UserLogin.IsNull() && !plan.UserLogin.IsUnknown() && !plan.UserLogin.Equal(state.UserLogin) {
namedParams = append(namedParams, NamedParam{"userLogin", plan.UserLogin.ValueString()})
}
if !plan.UserPublicKey.IsNull() && !plan.UserPublicKey.IsUnknown() && !plan.UserPublicKey.Equal(state.UserPublicKey) {
namedParams = append(namedParams, NamedParam{"userPublicKey", plan.UserPublicKey.ValueString()})
}
*/
// Step 4: Submit parameters
if len(namedParams) == 0 {
tflog.Info(ctx, "No parameters to submit. Skipping parameter submission.")
return nil
}
// Submit parameters
for i := len(namedParams) - 1; i >= 0; i-- {
np := namedParams[i]
info, ok := paramMapping[np.Name]
if !ok {
// Parameter not allowed in this operation
continue
}
// Check if value is unchanged
if np.Value == info.CurrentValue {
tflog.Info(ctx, fmt.Sprintf("Skipping parameter %s (ID %d) as value '%s' is unchanged", np.Name, info.Id, np.Value))
continue
}
tflog.Info(ctx, fmt.Sprintf("Submitting parameter: %s (ID %d) = %v [ParamUID: %s]", np.Name, info.Id, np.Value, info.Uid))
var method string
var payload map[string]interface{}
if info.Uid != "" {
// Update existing parameter -> PUT
method = "PUT"
payload = map[string]interface{}{
"instanceOperationCfsParamUid": info.Uid,
"svcOperationCfsParamId": info.Id,
"instanceOperationUid": operationUid,
"paramValue": np.Value,
}
} else {
// Create new parameter -> POST
method = "POST"
payload = map[string]interface{}{
"instanceOperationUid": operationUid,
"svcOperationCfsParamId": info.Id,
"paramValue": np.Value,
}
}
jsonData, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("unable to marshal param request: %s", err)
}
tflog.Info(ctx, fmt.Sprintf("Param Request Payload (%s): %s", method, string(jsonData)))
url := r.client.ApiEndpoint + "/instanceOperationCfsParams"
httpReq, err := http.NewRequestWithContext(ctx, method, url, bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("unable to create param request: %s", err)
}
httpReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
return fmt.Errorf("unable to submit parameter: %s", err)
}
respBody, _ := io.ReadAll(httpResp.Body)
httpResp.Body.Close()
if httpResp.StatusCode != http.StatusCreated &&
httpResp.StatusCode != http.StatusOK &&
httpResp.StatusCode != http.StatusNoContent {
return fmt.Errorf("submit parameter %s (id %d) failed with status %d: %s",
np.Name, info.Id, httpResp.StatusCode, string(respBody))
}
}
// Step 5: Run the operation
runReq, err := http.NewRequestWithContext(ctx, "POST",
r.client.ApiEndpoint+"/instanceOperations/"+operationUid+"/run", bytes.NewBuffer([]byte("{}")))
if err != nil {
return fmt.Errorf("unable to create run request: %s", err)
}
runReq.Header.Set("Content-Type", "application/json")
if r.client.ApiToken != "" {
runReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
runResp, err := r.client.HttpClient.Do(runReq)
if err != nil {
return fmt.Errorf("unable to run operation: %s", err)
}
defer runResp.Body.Close()
if runResp.StatusCode != http.StatusOK &&
runResp.StatusCode != http.StatusNoContent &&
runResp.StatusCode != http.StatusCreated {
runBody, _ := io.ReadAll(runResp.Body)
return fmt.Errorf("run operation failed with status %d: %s",
runResp.StatusCode, string(runBody))
}
// Step 6: Wait for operation completion
tflog.Info(ctx, fmt.Sprintf("Waiting for operation %s to complete...", operationUid))
if err := r.client.WaitForOperation(ctx, operationUid); err != nil {
return fmt.Errorf("wait for operation %s failed: %s", operationUid, err)
}
return nil
}
type VMOperationStage struct {
Name string `json:"stage"`
IsSuccessful bool `json:"isSuccessful"`
}
type VMOperation struct {
IsInProgress bool `json:"isInProgress"`
IsPending bool `json:"isPending"`
IsSuccessful *bool `json:"isSuccessful"`
DtFinish *string `json:"dtFinish"`
ErrorLog *string `json:"errorLog"`
Stages []VMOperationStage `json:"stages"`
}
type GetVMOperationResponse struct {
InstanceOperation VMOperation `json:"instanceOperation"`
}
func (r *VMResource) waitForVMOperationAndInstanceStatus(ctx context.Context, operationId string, instanceId string, timeout time.Duration) error {
deadline := time.Now().Add(timeout)
// Phase 1: Wait for operation completion
opTicker := time.NewTicker(5 * time.Second)
defer opTicker.Stop()
for {
select {
case <-ctx.Done():
return fmt.Errorf("context cancelled")
case <-opTicker.C:
if time.Now().After(deadline) {
return fmt.Errorf("timeout waiting for operation %s", operationId)
}
// Check operation status
httpReq, err := http.NewRequestWithContext(ctx, "GET", r.client.ApiEndpoint+"/instanceOperations/"+operationId, nil)
if err != nil {
return fmt.Errorf("failed to create request: %s", err)
}
if r.client.ApiToken != "" {
httpReq.Header.Set("Authorization", "Bearer "+r.client.ApiToken)
}
httpResp, err := r.client.HttpClient.Do(httpReq)
if err != nil {
return fmt.Errorf("failed to check operation status: %s", err)
}
body, _ := io.ReadAll(httpResp.Body)
httpResp.Body.Close()
if httpResp.StatusCode != http.StatusOK {
return fmt.Errorf("check operation failed: %d", httpResp.StatusCode)
}
var opResp GetVMOperationResponse
if err := json.Unmarshal(body, &opResp); err != nil {
return fmt.Errorf("failed to parse operation response: %s", err)
}
op := opResp.InstanceOperation
// Detect Failure
if op.IsSuccessful != nil && !*op.IsSuccessful {
// Scan stages for detail
failedStage := "unknown"
for _, stage := range op.Stages {
if !stage.IsSuccessful {
failedStage = stage.Name
}
}
return fmt.Errorf("operation failed at stage '%s' (isSuccessful=false)", failedStage)
}
// Detect Success (как в client_impl.go WaitForOperation)
if !op.IsInProgress && !op.IsPending && op.DtFinish != nil && *op.DtFinish != "" {
if op.IsSuccessful != nil && *op.IsSuccessful {
// Operation finished successfully
// Move to Phase 2
goto Phase2
}
// Если не successful но finished - это ошибка (уже обработана выше)
}
// Still running, continue waiting...
}
}
Phase2:
// Phase 2: Wait for Instance to be Running
instTicker := time.NewTicker(10 * time.Second)
defer instTicker.Stop()
for {
select {
case <-ctx.Done():
return fmt.Errorf("context cancelled")
case <-instTicker.C:
if time.Now().After(deadline) {
return fmt.Errorf("timeout waiting for instance running state")
}
instance, err := r.readInstance(ctx, instanceId)
if err != nil {
return fmt.Errorf("failed to check instance: %s", err)
}
if instance.Status == "running" {
return nil
}
if instance.Status == "error" || instance.Status == "failed" {
return fmt.Errorf("instance entered error state: %s", instance.Status)
}
}
}
}
-18
View File
@@ -1,18 +0,0 @@
tf-s3-test-01 NOT_FOUND >> /home/naeel/terra/name_check.txt
else
echo tf-s3-test-01 FOUND
tf-edge-test-01 NOT_FOUND >> /home/naeel/terra/name_check.txt
else
echo tf-edge-test-01 FOUND
tf-redis-test-01 NOT_FOUND >> /home/naeel/terra/name_check.txt
else
echo tf-redis-test-01 FOUND
tf-dns-zone-01 NOT_FOUND >> /home/naeel/terra/name_check.txt
else
echo tf-dns-zone-01 FOUND
tf-dns-record-01 NOT_FOUND >> /home/naeel/terra/name_check.txt
else
echo tf-dns-record-01 FOUND
tf-grafana-tenant-01 NOT_FOUND >> /home/naeel/terra/name_check.txt
else
echo tf-grafana-tenant-01 FOUND
-226
View File
@@ -1,226 +0,0 @@
# Запрос на глубокий анализ — Terraform Provider for Nubes Cloud
## Контекст
Проект — **Terraform Provider** для облачной платформы **Nubes Cloud** (Taffy/Deck API).
Провайдер написан на Go с использованием `terraform-plugin-framework` (SDK v2).
**Версия:** 5.0.52
**Тип:** Universal Provider — один core-движок, ресурсы генерируются из YAML-спек.
---
## Структура проекта
### Папки (корень: `/home/naeel/tf_provider/`)
```
internal/
├── core/ # Универсальный клиент API (client.go, instance_ops.go, instance_lookup.go)
├── provider/ # Реализация провайдера и НЕ-сгенерированных ресурсов (VM, Edge, VDC, VApp, Postgres, S3 и др.)
├── generated/ # Сгенерированные bolvan-ресурсы
└── registrykeys/ # Ключи для GPG-подписи
universal_rebuild/ # Новая универсальная архитектура (ядро, генераторы, YAML-спеки)
├── internal/
│ ├── core/ # Универсальный клиент (client.go) — v6 API flow
│ ├── provider/ # Конфиг провайдера
│ ├── resources_core/ # CRUD-логика (crud.go), хелперы, валидация, реф-параметры
│ └── resources_gen/ # АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЕ ресурсы (~100 файлов)
├── tools/
│ ├── gen/ # Go-генератор (читает YAML → генерирует ресурсы + registry.go)
│ ├── gen_v2/ # Версия 2 генератора
│ ├── service_params_gen/ # YAML-генератор (API → YAML)
│ ├── docs_template_gen/ # Генератор документации
│ └── service_spec_gen/ # Генератор спек
└── resources_yaml/ # YAML-спеки (сейчас только embed.go — встраиваются в бинарник)
devops/ # Скрипты сборки и деплоя
├── config/
│ ├── services_list.txt # Список всех сервисов (30+ строк)
│ └── operation_timeouts.json # Таймауты операций по сервисам
├── 01_generate_yamls.sh # Шаг 1: YAML из API
├── 02_generate_resources_and_docs*.sh # Шаг 2: генерация Go + docs
└── 03_build_and_upload_provider.sh # Шаг 3: сборка + публикация
docs/ # Документация
├── 00_overview/ # Обзор
├── 20_discovery/ # Discovery сервисов
├── 30_registry/ # Реестр ресурсов
├── 40_analysis/ # Анализы и форензика
├── 50_history/ # История (23 файла — пошагово весь процесс разработки)
├── 60_strategy/ # Стратегия и философия провайдера
├── 70_api/ # API-документация и дампы
└── help/ # Справка
```
---
## Что уже сделано и известно
### Архитектура API (Taffy/Deck)
**Базовый URL:** `https://deck-api.ngcloud.ru/api/v1/index.cfm`
**Жизненный цикл операции:**
1. `POST /instances` — создать placeholder инстанса → получаем `instanceUid`
2. `POST /instanceOperations` — создать операцию (create/modify/suspend/resume/delete) → `instanceOperationUid`
3. `GET /instanceOperations/{uid}?fields=cfsParams` — получить схему параметров
4. `POST /instanceOperationCfsParams` — отправить значения параметров (каждый параметр по `svcOperationCfsParamId`)
5. `GET /instanceOperations/{uid}/validate-cfs` — валидация
6. `POST /instanceOperations/{uid}/run` — запуск операции
7. `GET /instanceOperations/{uid}` — поллинг до `dtFinish` (критерий завершения)
**Получение состояния инстанса:** `GET /instances/{instanceUid}``explainedStatus`, `isDeleted`, `availableOperations`
**Получение списка инстансов:** `GET /instances?page=N&size=100`
### Список сервисов (30+)
Сервисы с service_id от 1 до 150:
- Инфраструктурные: vcOrg(19), vc_vdc(21), vc_nsxt(22), vc_vm(23/27/28), vapp(26), vcexternalip(25), vcVdcGroup(29)
- Базы данных: postgres(90), mariadb(115), mongodb(92), redis(91), clickhouse(120), vmpostgre(32), kafka(116)
- Хранилища: s3(12), s3bucket(13)
- Приложения: nextcloud(50), superset(81), harbor(82), flask(89), lucee(94), nodejs(95), pgadmin(96), nodered(97), http(98), gitea(99), rabbitmq(93), nifi(117), openwhisk(100)
- DNS: dnszone(110), dnsrecord(111)
- K8s: k8s_sthutrval_cluster(150)
- Прочее: dummy(1), template(2), tenant(112), vcComplex(113), GiteaComplex(114), akhq(119), valoTenant(149)
### Lifecycle-логика (для сервисов с suspend/resume)
- **Флаги:** `adopt_existing_on_create` (default: false), `suspend_on_destroy` (default: true)
- **Apply/Create:** проверка облака по `resource_name` → статус-матрица принятия решений
- **Destroy:** по умолчанию suspend, при `suspend_on_destroy=false` — только state
- **Modify:** через `RunInstanceOperationUniversal` с params по `svcOperationCfsParamId`
- **Replace:** запрещён для suspend-сервисов
### Проблемы, известные из истории
1. **Case-sensitivity имён** (docs/60_strategy/terraform_case_sensitivity_fix.md)
2. **Ref-параметры и валидация на adopt** (docs/60_strategy/adopt_ref_validation.md)
3. **VApp UID inconsistency / displayName resolve bug** (история 23)
4. **Subresource generation** — не все подресурсы корректно генерируются
5. **Params normalisation** — CamelCase → snake_case может ломаться
6. **Postgres immutables** — некоторые параметры нельзя менять после создания (ForceNew)
7. **Operation timeouts** — разные сервисы требуют разных таймаутов
8. **Deleted instances** — определение deleted через `explainedStatus` и `isDeleted`
9. **Валидация статусов**`not created`, `pending`, `failed` должны блокировать adopt
10. **ResourceAlreadyExists diagnostics** — обязательны два варианта в сообщении
### Архитектура universal_rebuild (ключевые особенности)
- **Ядро (core)**: UniversalClient — универсальный клиент, `CreateGenericInstanceUniversalV6`, `RunInstanceOperationUniversal`, `FindInstanceByDisplayName`
- **Resources_core**: `CreateResource`, `UpdateResource`, `DeleteResource`, `RunOperationByCode` — обёртки CRUD с timeout override
- **Resources_gen**: ~100 автогенерированных файлов ресурсов (все сервисы)
- **Генераторы**: `service_params_gen` → YAML, `gen/gen_v2` → Go код, `docs_template_gen` → документация
- **YAML-спеки**: сейчас встраиваются через embed.go, но есть YAML-генератор
### Генерация (devops pipeline)
1. `01_generate_yamls.sh` — получает YAML из API для всех сервисов из `services_list.txt`
2. `02_generate_resources_and_docs*.sh` — запускает Go-генератор + docs-генератор
3. `03_build_and_upload_provider.sh` — сборка под 3 ОС + GPG-подпись + загрузка в S3
4. `04_build_and_publish_docs.sh` — сборка mkdocs + публикация в S3
### Документация
- MkDocs Material theme
- Документация генерируется из тех же YAML-спек
- Публикуется в S3 bucket `terraform-registry`
---
## Что НУЖНО изучить подробно
### 1. API: неизвестные эндпойнты и возможности
- Есть ли другие эндпойнты, кроме задокументированных?
- Как работает `/services/{svcId}/default` (deprecated — что вместо)?
- Есть ли batch-операции?
- Пагинация: какие лимиты? Есть ли курсор?
- Rate limiting: есть ли? Какие лимиты?
- Есть ли эндпойнт для массового получения всех инстансов с параметрами?
### 2. Параметры сервисов (svcOperationCfsParamId)
Для каждого сервиса:
- Полный список `cfsParams` (все операции: create, modify, suspend, resume, delete, action)
- Какие параметры required, какие optional, какие read-only (outputs)
- Какие параметры имеют `refSvcId` (ссылки на другие сервисы)
- Какие параметры имеют `valueList` (выпадающие списки)
- DataType: какие бывают (string, boolean, list, uuid, int, float, json?)
- Default values — откуда берутся?
- Какие параметры immutable (create-only)
- Какие параметры зависят от других (dependency chain)
### 3. Статусы и состояния инстансов
- Все возможные значения `explainedStatus`
- Все возможные значения `isDeleted`
- Матрица переходов (state machine):
- `creating``running`
- `running``suspend``running`
- `running``deleted`
- `failed` → ?
- `not created` → ?
- Что происходит при `pending` / `operation in progress`?
- Как долго длятся типичные операции?
### 4. Операции (kinds и их семантика)
- `kind: instance` — полный CRUD
- `kind: subresource` — операции внутри инстанса (create_user, delete_database и т.д.)
- `kind: action` — одноразовые операции (restart, redeploy, recovery)
- Для каждого сервиса: какие subresource и action операции доступны?
- Как subresource идентифицируются? (через parent instanceUid? свой instanceUid?)
- Есть ли операции, которые не вписываются в схему?
### 5. Схема YAML-спек
- Нужно восстановить формат YAML для примера одного-двух сервисов (dummy, postgres, s3bucket)
- Какие поля обязательны? Какие опциональны?
- Как описываются operations с kind-ами?
- Как описываются timeout-ы?
- Как описываются outputs?
- Как описываются service_man (руководство пользователя)?
- Как описываются lifecycle-правила (suspend_on_destroy, adopt_existing_on_create)?
### 6. Ошибки и проблемы продакшена
- Все известные баги из истории (23 файла в docs/50_history/)
- Какие ресурсы сейчас не работают? (VApp UID, Postgres immutables и т.д.)
- Какие тесты падают или неполны?
- Есть ли проблемы с GPG-ключами / реестром?
- Есть ли проблемы с S3-публикацией?
### 7. Тестовое покрытие
- Какие тесты написаны в `universal_rebuild/internal/resources_core/`?
- Есть ли интеграционные тесты с реальным API?
- Какие тесты нужны, но отсутствуют?
- Есть ли моки для API?
### 8. Безопасность
- Как управляются токены? Какой механизм refresh?
- Есть ли поддержка разных эндпойнтов (prod/dev)?
- Как обрабатываются чувствительные данные (пароли БД, ключи)?
- Есть ли валидация входящих параметров на injection?
### 9. Производительность
- Сколько времени занимает типичный apply?
- Есть ли узкие места в поллинге?
- Оптимальные интервалы поллинга для разных операций?
- Есть ли кэширование?
### 10. Документация (MkDocs)
- Как генерируется документация из YAML?
- Как работает шаблонизация?
- Что надо исправить/дополнить?
---
## Формат ответа
Пожалуйста, предоставь детальный анализ по каждому из 10 пунктов. Для каждого пункта:
1. **Текущее понимание** (что уже известно из контекста)
2. **Пробелы в знаниях** (чего не хватает)
3. **Гипотезы** (что можно предположить на основе имеющихся данных)
4. **Что нужно сделать** (конкретные шаги: какие файлы прочитать, какие API вызвать, какие тесты запустить)
5. **Риски** (какие проблемы могут возникнуть)
Особое внимание удели:
- Формату YAML-спек (п.5) — это основа всей генерации
- Полному списку cfsParams для топ-10 сервисов (п.2)
- Матрице состояний (п.3) — критично для lifecycle-логики
- Ошибкам из истории (п.6) — чтобы не повторять
Если для какого-то пункта информации в предоставленном контексте недостаточно — укажи это и предложи, где искать недостающие данные (какие файлы прочитать, какие curl-запросы выполнить).
**Важно:** Ничего НЕ ДЕЛАЙ в файловой системе. Никаких изменений кода, файлов, конфигов. Только анализ.
-289
View File
@@ -1,289 +0,0 @@
# Промпт для Opus 4.8: Анализ Terraform Provider для Nubes Cloud
> **Цель:** глубокий анализ архитектуры, генерационного пайплайна, API-взаимодействия и проблем проекта. Не просто описать, а найти слабые места, предложить улучшения, оценить риски.
> **Инструкция:** читай файлы по мере необходимости, не пытайся прочитать всё сразу. Ниже — карта проекта с указанием что где лежит и на что обратить внимание.
---
## 1. Общая архитектура — два провайдера в одном репозитории
| Компонент | Путь | Версия | Registry | Характер |
|-----------|------|--------|----------|----------|
| **Legacy** | `/main.go`, `internal/provider/` | 5.0.52 | `registry.terraform.io/nubes/nubes` | Ручной код, 13 ресурсов |
| **Universal Rebuild** | `universal_rebuild/main.go`, `universal_rebuild/...` | 5.0.51 | `terra.k8c.ru/nubes/nubes` | Полностью генерируемый, ~50 ресурсов |
Оба используют `terraform-plugin-framework`. Legacy — ручной, Universal — продукт генерационного конвейера. Legacy всё ещё жив, Universal — целевой.
### Ключевые файлы для понимания архитектуры:
- `/home/naeel/tf_provider/devops/ARCHITECTURE.md` — архитектурные принципы (YAML как source of truth, никаких ручных правок сгенерированного кода)
- `/home/naeel/tf_provider/docs/CODEBASE_ANALYSIS_AND_ROADMAP.md` — полный анализ кодовой базы от 13.03.2026
- `/home/naeel/tf_provider/docs/MIGRATION_PLAN_FOR_AGENT.md` — план миграции в Managed K8s
**Вопросы к Opus:**
- Насколько оправдано существование двух провайдеров? Когда и как Legacy должен быть выведен?
- Есть ли архитектурные проблемы в разделении `internal/` vs `universal_rebuild/internal/`?
---
## 2. Генерационный конвейер (CRITICAL PATH)
### Полный пайплайн:
```
API Nubes (deck-api.ngcloud.ru)
[Шаг 1] devops/01_generate_yamls.sh
│ └─ запускает: universal_rebuild/tools/service_spec_gen/generate_service_spec.go
│ └─ читает: devops/config/services_list.txt (30+ сервисов)
│ └─ для каждого сервиса: GET /services/{id} → получает операции → GET детали каждой операции → cfsParams
│ └─ пишет: {id}_{name}.yaml в resources_yaml/
[Шаг 2] devops/02_generate_resources_and_docs_v2.sh
│ ├─ Go-генератор: universal_rebuild/tools/gen_v2/generate_resources_v2.go
│ │ └─ читает YAML → классифицирует операции (instance/subresource/action) → генерирует .go файлы
│ │ └─ пишет: internal/resources_gen/*.go (50+ файлов) + registry.go
│ │
│ └─ Доку-генератор: universal_rebuild/tools/docs_template_gen_v2/
│ └─ читает YAML → генерирует .md документацию
[Шаг 3] devops/03_build_and_upload_provider.sh
│ └─ сборка под linux/windows/darwin → GPG-подпись → S3
[Шаг 4] devops/04_build_and_publish_docs.sh
└─ mkdocs build → S3
```
### Ключевые файлы генераторов:
- **service_spec_gen** (API→YAML): `/home/naeel/tf_provider/universal_rebuild/tools/service_spec_gen/generate_service_spec.go`
- **gen_v2** (YAML→Go): `/home/naeel/tf_provider/universal_rebuild/tools/gen_v2/generate_resources_v2.go`
- **services_list.txt**: `/home/naeel/tf_provider/devops/config/services_list.txt` — 30+ сервисов (dummy, s3, postgres, kafka, clickhouse, k8s, ...)
- **operation_timeouts.json**: `/home/naeel/tf_provider/devops/config/operation_timeouts.json`
### Профили (стенды):
- `/home/naeel/tf_provider/devops/profiles/test/`, `prod/`, `dev/`
- Каждый профиль содержит: `profile.env`, `services_list.txt`, `operation_timeouts.json`, `generated/`
**Вопросы к Opus:**
- Насколько надёжен конвейер? Что произойдёт если API изменит формат ответа?
- Достаточно ли валидации на каждом шаге? Нет ли риска генерации битого кода?
- Почему генераторы на Go, а не на Python (учитывая что скрипты на bash)?
- Как обрабатываются ошибки API (retry, rate limiting)? См. `service_spec_gen` — есть `ATTEMPTS=3`, `REQUEST_DELAY=0.5`
- Комментирование сервисов в `services_list.txt` — это единственный способ исключить сервис? Не приведёт ли к рассинхрону?
---
## 3. Core: UniversalClient и API-взаимодействие
### Файл: `/home/naeel/tf_provider/universal_rebuild/internal/core/client.go`
**API Flow V6 (create):**
1. `POST /instances` → получаем instanceUid
2. `POST /instanceOperations` (operation="create") → получаем instanceOperationUid
3. `GET /instanceOperations/{uid}?fields=cfsParams` → получаем параметры с дефолтами
4. `resolveRefSvcParamValues` — разрешение ref-параметров (UUID других сервисов)
5. Для каждого параметра: `POST /instanceOperationCfsParams`
6. Валидация + запуск операции
**Методы:**
- `CreateGenericInstanceUniversalV6` — полный create flow
- `FindInstanceByDisplayName` — поиск по displayName с фильтрацией deleted/дубликатов
- `GetInstanceState` / `GetInstanceStateRaw` — чтение состояния
- `RunInstanceOperationUniversal` / `RunInstanceOperationUniversalWithDefaults` — modify/suspend/resume/delete
- `RunInstanceOperationUniversalByCode` — операции по коду (для action/subresource)
### HTTP-транспорт: `/home/naeel/tf_provider/universal_rebuild/internal/provider/provider.go`
- Force HTTP/1.1 (API не поддерживает HTTP/2)
- InsecureSkipVerify опционально (для dev-стендов)
- Timeout 300s
- TLS 1.2 minimum
### Поддержка core:
- `/home/naeel/tf_provider/universal_rebuild/internal/core/instance_params.go` — маппинг параметров
- `/home/naeel/tf_provider/universal_rebuild/internal/core/instance_outputs.go` — чтение outputs
- `/home/naeel/tf_provider/universal_rebuild/internal/core/refsvc_resolve.go` — разрешение ref_svc_id
- `/home/naeel/tf_provider/universal_rebuild/internal/core/operation_timeouts.go` — таймауты операций
**Вопросы к Opus:**
- API Flow V6 — есть ли проблемы с идемпотентностью? Что при обрыве на шаге 4 или 5?
- `doRequest` внутри — есть ли retry logic? Как обрабатываются 429/503?
- Почему HTTP/1.1 принудительно? Это ограничение API или обход бага?
- 300s timeout на всём HTTP-клиенте — не мало ли для длинных операций (создание VM может идти 10+ минут)?
---
## 4. CRUD-логика и Lifecycle
### Файл: `/home/naeel/tf_provider/universal_rebuild/internal/resources_core/crud.go`
**CreateResource:**
1. `FindInstanceByDisplayName` — поиск существующего
2. Если найден → `adoptExistingInstanceOnCreate`:
- Проверка `operation_in_progress`/`operation_pending`
- Если `adopt_existing_on_create=false` → ошибка
- Статус `not_created` → ошибка
- Статус `running` → валидация ref-параметров → adopt (возврат UUID)
- Статус `suspended` → проверка required-params → resume → валидация ref → adopt
3. Если не найден → `CreateGenericInstanceUniversalV6`
**DeleteResource:**
- `suspend` → вызов suspend
- `state_only`/`detach` → только удаление из state
**UpdateResource:**
- `RunInstanceOperationUniversalWithDefaults("modify")`
### Поддерживающие файлы в `resources_core/` (все в `/home/naeel/tf_provider/universal_rebuild/internal/resources_core/`):
- `ref_validation.go` — валидация ref-параметров при adopt
- `required_params.go` + `required_params_compare.go` — проверка обязательных параметров
- `params_compare.go` — сравнение параметров для detect changes
- `params_mapping.go` + `params_ref_mapping.go` — маппинг параметров из/в API
- `params_validation_mapping.go` — маппинг для plan validation
- `state_refresh.go` — RefreshResourceState (чтение state после apply)
- `outputs.go` — FetchInstanceOutputs
- `operation_ids.go` — маппинг operation ID → имя
- `json_normalize.go` + `json_planmodifier.go` — нормализация JSON-параметров
- `uuid_planmodifier.go` — план-модификатор для UUID
- `domain_collision.go` — проверка коллизий доменов
- `resource_diagnostics.go` + `resource_diagnostics_required.go` — форматирование диагностик
- `subresource_guard.go` — защита подресурсов
- `crud_test.go` — тесты
**Вопросы к Opus:**
- `adoptExistingInstanceOnCreate` — не слишком ли сложная логика? 100+ строк, много ветвлений. Есть ли риск не покрытых кейсов?
- Что происходит при concurrent apply из двух разных Terraform-окружений?
- `state_only` destroy — не остаются ли orphaned ресурсы в облаке?
- Как определяется `suspend_on_destroy` по умолчанию? Где логика `hasSuspend → suspendOnDestroyDefault`?
---
## 5. Генерация Go-кода из YAML — КРИТИЧЕСКИ
### Файл: `/home/naeel/tf_provider/universal_rebuild/tools/gen_v2/generate_resources_v2.go`
**На входе:** YAML-спек каждого сервиса
**На выходе:** 3 типа Go-ресурсов:
- **Instance** (kind=instance) — CRUD + lifecycle (suspend/resume)
- **Subresource** (kind=subresource) — CRUD для подобъектов (users, databases, topics)
- **Action** (kind=action) — одноразовые операции (restart, redeploy, reconcile)
**Логика генератора:**
1. Парсит все YAML → `ServiceSpec` (name, service_id, outputs, lifecycle, operations)
2. Для instance: собирает create/modify params → computeCreateOnly → mergeParams → генерирует `.go`
3. Для subresource: группирует по subresource name → create/modify/delete params
4. Для action: каждый action → отдельный ресурс с trigger-полем
5. Генерирует `registry.go``AllResources()` со всеми New* функциями
### Пример сгенерированного файла: `/home/naeel/tf_provider/universal_rebuild/internal/resources_gen/90_postgres_resource.go`
**Вопросы к Opus:**
- Шаблоны генерятся через `text/template` — где сами шаблоны? Они вшиты в `gen_v2` как константы? Насколько они поддерживаемы?
- Что произойдёт если в YAML появится новый kind операции, не известный генератору?
- `createOnly` параметры — как определяется что параметр immutable? Из API или эвристика?
- JSON-параметры (`data_type: json`) — как обрабатываются? Есть `IsJson` + `JsonNormalize` план-модификатор. Это надёжно?
- Для subresource — как определяется identity (какие параметры идентифицируют ресурс)?
---
## 6. Известные проблемы (из 23 файлов истории)
Архив: `/home/naeel/tf_provider/docs/50_history/` (23 файла, от `00_system_mechanics.md` до `23_vapp_uid_inconsistency_displayname_resolve_bug.md`)
### Ключевые баги (прочитай эти файлы):
- **23** — `vapp_uid_inconsistency`: `FindInstanceByDisplayName` возвращает deleted инстанс (нет фильтра isDeleted)
- **22** — `adopt_ref_validation`: ref-параметры не валидировались при adopt → ссылки на deleted ресурсы
- **21** — `plan_validation_ref_svc_filter`: план показывал deleted/suspended инстансы в ref-списках
- **16** — `create_only_params`: create-only параметры не блокировались при modify
- **13** — `universal_flow_param_normalization`: нормализация параметров между API и Terraform
- **06** — `postgres_update_immutable_params`: immutable параметры при модификации
- **05** — `polling_fixes`: проблемы с поллингом длительных операций
- **04** — `vm_hang_fix_and_500_error`: 500 ошибка при модификации VM (см. также DEBUG_REPORT_VM_FIX.md)
### Другие важные документы:
- `/home/naeel/tf_provider/docs/INSTANCE_STATES.md` — полная матрица состояний инстансов
- `/home/naeel/tf_provider/docs/STATE_TRANSITIONS.md` — все переходы состояний (NOT_CREATED, CREATING, RUNNING, SUSPENDED, DELETED, ...)
- `/home/naeel/tf_provider/docs/DEBUG_REPORT_VM_FIX.md` — отчёт о попытке исправить 500 при modify VM
**Вопросы к Opus:**
- Какие баги из истории до сих пор актуальны (не исправлены)?
- Есть ли системные проблемы, которые порождают целые классы багов (например, отсутствие фильтрации deleted инстансов)?
- Насколько полна матрица состояний? Все ли переходы обрабатываются?
- 500 ошибка при modify VM — правильно ли был сделан анализ? Может быть проблема в API, а не в провайдере?
---
## 7. HAR-трассировки — источник правды об API
Файлы в `/home/naeel/tf_provider/HAR/`:
- `OK.har`, `goodmodify.har`, `baddelete.har` — реальные HTTP-трассировки
- `deck.ngcloud.ru.har`, `deck.ngcloud1.ru.har` — полные сессии
- `keycloak.nubes.ru.har`, `lucee.har`, `pguser.har` — специфичные сервисы
**Вопросы к Opus:**
- HAR-файлы — ценный источник для верификации API-контракта. Используются ли они в тестах?
- Можно ли автоматизировать проверку что провайдер соответствует реальному API на основе HAR?
---
## 8. Тестирование
### Где тесты:
- `/home/naeel/tf_provider/universal_rebuild/internal/resources_core/crud_test.go`
- `/home/naeel/tf_provider/universal_rebuild/internal/core/client_test.go`
**Вопросы к Opus:**
- Достаточно ли тестов? Какие пробелы?
- Как тестировать сгенерированные ресурсы без реального API?
- Нужны ли integration-тесты против реального API (с мок-сервером или тестовым стендом)?
---
## 9. Безопасность
- Токен аутентификации: env `NUBES_API_TOKEN`, файл `~/.nubes_token`, или в HCL (`api_token` — sensitive)
- GPG-ключ для подписи провайдера: `/home/naeel/tf_provider/secrets/private_key.asc`
- Токены: `/home/naeel/tf_provider/secrets/dev.token`, `prod.token`, `test.token`
- InsecureSkipVerify для dev-стендов
**Вопросы к Opus:**
- Есть ли риск утечки токена через логи/диагностики?
- `Sensitive: true` на api_token — достаточно ли этого?
- GPG-ключ в репозитории — это нормально? (в `.gitignore` ли он?)
---
## 10. Что нужно оценить Opus (итоговый список)
1. **Архитектура:** два провайдера → стратегия перехода на Universal
2. **Конвейер:** надёжность, валидация, обработка ошибок API
3. **Генератор кода:** поддерживаемость шаблонов, расширяемость
4. **Core/API:** retry logic, таймауты, идемпотентность, HTTP/1.1 vs HTTP/2
5. **Lifecycle:** полнота coverage состояний, краевые кейзы adopt/resume
6. **Баги:** какие системные, какие ещё не исправлены
7. **Тесты:** пробелы в покрытии
8. **Безопасность:** токены, GPG, secrets management
9. **Производительность:** поллинг, параллельные запросы, кэширование
10. **Документация:** насколько генерируемые доки соответствуют реальности
### На что обратить ОСОБОЕ внимание:
- `adoptExistingInstanceOnCreate` в `crud.go` — самая сложная функция, много ветвлений
- `FindInstanceByDisplayName` — была переписана, но всё ещё есть риск не найти/найти не тот
- Генератор `gen_v2` — если сломается, сломается ВЕСЬ провайдер
- Immutable параметры — как определяется неизменяемость? Из API или из YAML?
- Конкурентный доступ — что если два `terraform apply` одновременно?
---
## Инструкция Opus
1. Начни с чтения `devops/ARCHITECTURE.md` и `docs/CODEBASE_ANALYSIS_AND_ROADMAP.md`
2. Затем прочитай ключевые файлы конвейера: `service_spec_gen/generate_service_spec.go` и `gen_v2/generate_resources_v2.go`
3. Затем core: `client.go` и `crud.go`
4. Затем историю проблем: выборочно 2-3 последних файла из `docs/50_history/`
5. После этого — дай развёрнутый анализ по ВСЕМ 10 пунктам выше
6. В анализе на каждый пункт: текущее состояние → что хорошо → что плохо → конкретные предложения → риски
**НЕ читай все 50+ сгенерированных файлов подряд — они генерируются и не содержат уникальной логики.**
-16
View File
@@ -1,16 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
VM_HOST="naeel@5.172.178.213"
VM_PATH="/home/naeel/tf_provider/TEST_STAND"
SSH_KEY="$HOME/.ssh/naeel_vm_id_ed25519"
SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=10"
rsync -avz --delete \
--exclude '.terraform/' \
--exclude '*.tfstate' \
--exclude '*.tfstate.*' \
--exclude '.terraform.lock.hcl' \
-e "ssh -i $SSH_KEY $SSH_OPTS" \
/home/naeel/tf_provider/TEST_STAND/ \
"${VM_HOST}:${VM_PATH}/"
-24
View File
@@ -1,24 +0,0 @@
1 dummy
114 GiteaComplex
115 mariadb
116 kafka
117 nifi
12 s3
13 s3bucket
21 vc_vdc
22 vc_nsxt
25 vcexternalip
26 vapp
28 vc_vm_v3
81 superset
82 harbor
89 flask
90 postgres
91 redis
92 mongodb
93 rabbitmq
94 lucee
95 nodejs
96 pgadmin
97 nodered
99 gitea
1 1 dummy
2 114 GiteaComplex
3 115 mariadb
4 116 kafka
5 117 nifi
6 12 s3
7 13 s3bucket
8 21 vc_vdc
9 22 vc_nsxt
10 25 vcexternalip
11 26 vapp
12 28 vc_vm_v3
13 81 superset
14 82 harbor
15 89 flask
16 90 postgres
17 91 redis
18 92 mongodb
19 93 rabbitmq
20 94 lucee
21 95 nodejs
22 96 pgadmin
23 97 nodered
24 99 gitea
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
-58
View File
@@ -1,58 +0,0 @@
{
"index": 30,
"params": {
"150": [
"accessIpListAPI",
"accessIpListIngress",
"appVersion",
"clusterName",
"controlPlaneCount",
"controlPlaneSizingDisk",
"controlPlaneSizingPolicy",
"needExternalAddressAPI",
"needExternalAddressIngress",
"nsxtUid",
"resourceRealm",
"workersCount",
"workersSizingDisk",
"workersSizingPolicy"
],
"116": [
"ipSpaceNameMaster",
"needExternalAddressMaster",
"resourceCPU",
"resourceDisk",
"resourceInstances",
"resourceMemory",
"resourceRealm"
],
"115": [],
"113": [],
"111": [
"recordInput",
"recordName",
"recordTTL",
"recordType",
"zoneName",
"zoneUid"
]
},
"out": {
"150": [
"ingressAddress",
"kubernetesApiAddress",
"webUrl"
],
"116": [
"externalConnect",
"internalConnect",
"monitoring"
],
"115": [],
"113": [],
"111": [
"dnsServers",
"record"
]
}
}
-1
View File
@@ -1 +0,0 @@
processed 25/313
-22
View File
@@ -1,22 +0,0 @@
{
"index": 28,
"found": {
"82": [
"adminPass"
],
"90": [
"adminPass",
"adminUser",
"standbyPass",
"standbyUser"
],
"93": [
"adminPass",
"adminUser"
],
"99": [
"adminPass",
"adminUser"
]
}
}
-30
View File
@@ -1,30 +0,0 @@
{
"150": "b1c54787-8729-4264-811a-24b2936fc304",
"116": "bd1d689e-bce2-4792-9f1f-73cd1d6c7de7",
"115": "2eb2dc97-6658-4b36-b266-ef5698400158",
"113": "29f85c8e-430b-4b12-a72a-b5da70424966",
"111": "c0e5b324-fecb-4acf-9e21-2de0e7ba08a7",
"110": "f17cfa3e-20df-4e31-8d03-b5743f468e31",
"99": "e15e66e1-c613-4230-8f5e-263804224b0f",
"98": "870c2fa3-5fe9-43eb-985f-7c6d8979968e",
"97": "a70021b9-902d-40e9-bffa-d41ede78b841",
"96": "ef20352e-855c-4b41-a74e-e7f95a2f4f17",
"95": "46436960-1400-468c-83d2-35894cdff415",
"94": "b3a75608-d980-448d-9ea3-bb924357d994",
"93": "873b1a10-593c-4efc-bbdb-00b8daf3fda3",
"92": "3ec2d96d-a897-45ce-83ac-e7de3de01894",
"91": "e8f16cae-3668-4b8c-8119-6209d5a87fd7",
"90": "ad0a2410-fba7-4b0a-9e6a-53b158884b1a",
"89": "b31737f3-b792-4a43-83a9-fdd6452eb768",
"82": "55c4914e-7bc5-4bcc-bc71-2444025bc6fc",
"50": "4ca0482e-3a5a-4648-9a39-4a9995c970a9",
"28": "e3ff4b3a-1236-4df7-ad88-c66142733bdb",
"26": "464eb4a0-62dc-42cd-af2b-4305db3c8d1b",
"25": "b4683403-8858-4bda-b043-1fc6ddd28b7e",
"22": "ba6d6e55-0189-40a2-a989-ff3151a7e7bc",
"21": "7ecf6b01-7493-4c83-af5d-9fb19385f904",
"19": "e5375174-36ec-4512-bba9-b56f9eeba0bd",
"13": "ba0dbcf7-8c89-4ded-b180-482643846d85",
"12": "6efc7c08-68fd-47b5-b33b-5d2f7e6d094a",
"1": "30124241-644c-4925-b525-a8c981a44023"
}
-411
View File
@@ -1,411 +0,0 @@
{
"1": {
"svc": "Болванка",
"ops": [
"create",
"delete",
"modify",
"redeploy",
"resume",
"suspend"
]
},
"2": {
"svc": "Темплейт k8s",
"ops": [
"create",
"delete",
"modify",
"resume",
"suspend"
]
},
"12": {
"svc": "S3 Object Storage",
"ops": [
"create",
"create_sub_user",
"delete",
"delete_sub_user",
"modify",
"resume",
"suspend"
]
},
"13": {
"svc": "S3 бакет",
"ops": [
"create",
"delete"
]
},
"19": {
"svc": "Организация в Cloud Director",
"ops": [
"create",
"delete",
"modify",
"resume",
"suspend"
]
},
"20": {
"svc": "Организация [DEPRECATED]",
"ops": [
"create",
"delete",
"resume",
"suspend"
]
},
"21": {
"svc": "Виртуальный датацентр (vDC)",
"ops": [
"create",
"delete",
"modify",
"resume",
"suspend"
]
},
"22": {
"svc": "Сетевой шлюз периметра (Edge)",
"ops": [
"create",
"delete",
"modify"
]
},
"23": {
"svc": "VM в Cloud Director (старый формат) (vc_vm)",
"ops": [
"create",
"delete",
"modify",
"resume",
"suspend"
]
},
"24": {
"svc": "DEPRECATED Правила маршрутизации для VM (vc_nat)",
"ops": []
},
"25": {
"svc": "Публичные IP адреса",
"ops": [
"create",
"delete",
"modify"
]
},
"26": {
"svc": "Виртуальный каталог ВМ (vApp)",
"ops": [
"create",
"delete",
"resume",
"suspend"
]
},
"27": {
"svc": "VM в Cloud Director (vc_vmV2)",
"ops": [
"create",
"delete",
"modify",
"resume",
"suspend"
]
},
"28": {
"svc": "Виртуальная машина",
"ops": [
"create",
"delete",
"modify",
"redeploy",
"resume",
"suspend"
]
},
"29": {
"svc": "Группа датацентров",
"ops": [
"add_vdc",
"create",
"delete",
"remove_vdc"
]
},
"32": {
"svc": "vc_vm_postgresql_std",
"ops": [
"create",
"delete"
]
},
"50": {
"svc": "Nextcloud",
"ops": [
"create",
"delete",
"modify",
"restart",
"resume",
"suspend"
]
},
"81": {
"svc": "Apache Superset",
"ops": [
"create",
"delete"
]
},
"82": {
"svc": "Container Registry",
"ops": [
"create",
"delete"
]
},
"88": {
"svc": "k8sZitiController",
"ops": [
"create",
"delete"
]
},
"89": {
"svc": "Flask",
"ops": [
"create",
"delete",
"modify",
"resume",
"suspend"
]
},
"90": {
"svc": "PostgreSQL",
"ops": [
"create",
"delete",
"modify",
"recovery",
"restart",
"resume",
"suspend"
]
},
"91": {
"svc": "Redis",
"ops": [
"create",
"delete"
]
},
"92": {
"svc": "MongoDB",
"ops": [
"create",
"create_user",
"delete",
"delete_user",
"modify_user",
"resume",
"suspend"
]
},
"93": {
"svc": "RabbitMQ",
"ops": [
"create",
"delete",
"modify",
"resume",
"suspend"
]
},
"94": {
"svc": "Lucee",
"ops": [
"create",
"delete",
"modify",
"redeploy",
"restart",
"resume",
"suspend"
]
},
"95": {
"svc": "NodeJS",
"ops": [
"create",
"delete",
"modify",
"redeploy",
"resume",
"suspend"
]
},
"96": {
"svc": "pgAdmin",
"ops": [
"create",
"delete",
"modify",
"redeploy",
"resume",
"suspend"
]
},
"97": {
"svc": "NodeRed",
"ops": [
"create",
"delete"
]
},
"98": {
"svc": "Простой HTTP контейнер",
"ops": [
"create",
"delete",
"modify",
"redeploy",
"resume",
"suspend"
]
},
"99": {
"svc": "Gitea",
"ops": [
"create",
"delete",
"modify",
"resume",
"suspend"
]
},
"100": {
"svc": "Serverless Openwhisk (openwhisk_tenant)",
"ops": [
"create",
"delete"
]
},
"110": {
"svc": "DNS зона",
"ops": [
"create",
"delete"
]
},
"111": {
"svc": "DNS запись",
"ops": [
"create",
"delete",
"modify"
]
},
"112": {
"svc": "Тенант в Grafana",
"ops": [
"create",
"delete",
"modify"
]
},
"113": {
"svc": "Быстрый старт",
"ops": [
"create"
]
},
"114": {
"svc": "Комплексная услуга по созданию gitea",
"ops": []
},
"115": {
"svc": "Mariadb",
"ops": [
"create",
"delete",
"modify",
"resume",
"suspend"
]
},
"116": {
"svc": "ApacheKafka",
"ops": [
"create",
"create_topic",
"create_user",
"delete",
"delete_topic",
"delete_user",
"modify",
"modify_topic",
"modify_user",
"resume",
"suspend"
]
},
"117": {
"svc": "Nifi",
"ops": [
"create",
"delete",
"modify"
]
},
"119": {
"svc": "Akhq",
"ops": [
"create",
"delete",
"modify",
"resume",
"suspend"
]
},
"120": {
"svc": "ClickHouse",
"ops": [
"create",
"create_database",
"create_user",
"delete",
"delete_database",
"delete_user",
"modify",
"resume",
"suspend"
]
},
"149": {
"svc": "VALO Cloud",
"ops": [
"create",
"delete",
"modify",
"resume",
"suspend"
]
},
"150": {
"svc": "Kubernetes кластер Штурвал",
"ops": [
"create",
"delete",
"modify",
"resume",
"suspend",
"update_secrets"
]
}
}