Fix VDC flow and FullPipe example

This commit is contained in:
Repinoid
2026-09-21 13:02:25 +03:00
parent 7d446977a5
commit 1ec6a0fedc
8 changed files with 196 additions and 59 deletions
+2 -2
View File
@@ -4,8 +4,8 @@ api_token = "ВАШ_ТОКЕН_ИЗ_ЛК"
organization = "kontora"
vdc_resource_name = "fullpipe-vdc"
# vdc_network_provider = "<взять из текущей страницы ЛК>"
# vdc_provider_vdc = "<взять из текущей страницы ЛК>"
vdc_network_provider = "snb1"
vdc_provider_vdc = "Intel Broadwell 2.4"
vdc_cpu_allocated = 8
vdc_cpu_guaranteed = 0
vdc_mem_allocated = 32
+1 -1
View File
@@ -4,7 +4,7 @@ TOKEN_FILE="secrets/dev.token"
# Release versions
# Version
VERSION="2.0.2"
VERSION="2.0.3"
NAMESPACE="nubes-dev"
PROVIDER_NAME="nubes"
+1 -12
View File
@@ -6,17 +6,12 @@
12 s3 # S3 Object Storage
13 s3bucket # S3 бакет
19 vcOrg # Организация в Cloud Director
# 20 vcOrgSaas # Организация [DEPRECATED] — нет в UI
21 vc_vdc # Виртуальный датацентр (vDC)
22 vc_nsxt # Сетевой шлюз периметра (Edge)
# 23 vc_vm # VM в Cloud Director (старый формат) (vc_vm) — нет в UI
# 24 vcNat # DEPRECATED Правила маршрутизации для VM (vc_nat)
25 vcexternalip # Публичные IP адреса
26 vapp # Виртуальный каталог ВМ (vApp)
# 27 vc_vm_v2 # VM в Cloud Director (vc_vmV2) — нет в DEV UI
28 vc_vm_v3 # Виртуальная машина
29 vcVdcGroup # Группа датацентров
# 32 vmpostgre # vc_vm_postgresql_std
50 nextcloud # Nextcloud
81 superset # Apache Superset
82 harbor # Container Registry
@@ -34,13 +29,8 @@
97 nodered # NodeRed
98 http # Простой HTTP контейнер
99 gitea # Gitea
# 100 openwhisk # Serverless Openwhisk — нет в UI
109 zonesV2 # Управление DNS
# 110 dnszone # DNS зона — нет в UI
111 dnsrecord # DNS запись
# 112 tenant # Тенант в Grafana — нет в UI
# 113 vcComplex # Быстрый старт — нет в UI
# 114 GiteaComplex # Комплексная услуга по созданию gitea — нет в UI
115 mariadb # Mariadb
116 kafka # ApacheKafka
# 117 nifi # Nifi
@@ -50,6 +40,5 @@
149 valoTenant # VALO Cloud
150 k8sSthutrvalCluster # Kubernetes кластер Штурвал
151 k8sOpenbao # Vault
# 153 nifi # Nifi (DEV)
153 nifi # Nifi (DEV)
163 llmAi # LLM
# 175 k8sGo # Go — нет в UI
+1 -1
View File
@@ -33,7 +33,7 @@ PROVIDER_DIR="${PROVIDER_DIR:-${ROOT_DIR}/provider}"
BUILD_DIR="${BUILD_DIR:-${ROOT_DIR}/artifacts/provider_build}"
GPG_KEY_FILE="${GPG_KEY_FILE:-${ROOT_DIR}/secrets/private_key.asc}"
VERSION="${1:-2.0.2}"
VERSION="${1:-2.0.3}"
REGISTRY_HOSTNAME="${REGISTRY_HOSTNAME:-tf-registry.containerk8s.services.ngcloud.ru}"
NAMESPACE="${NAMESPACE:-nubes}"
PROVIDER_NAME="${PROVIDER_NAME:-nubes}"
+1 -1
View File
@@ -5,7 +5,7 @@
| Стенд | Namespace | Версия | Дата заливки |
|---|---|---|---|
| PROD | `nubes` | `1.0.0` | 2026-09-03 | (новая нумерация) |
| DEV | `nubes-dev` | `2.0.2` | 2026-09-21 | (graceful fallback для cfsParams при 500) |
| DEV | `nubes-dev` | `2.0.3` | 2026-09-21 | (graceful fallback для cfsParams при 500) |
| TEST | `nubes-test` | `3.0.0` | 2026-09-03 | (новая нумерация) |
## Как проверить
+111
View File
@@ -0,0 +1,111 @@
# Резюме сессии: VDC create-flow, диалог с Opus и правка fallback (2026-09-21)
## 1. Контекст задачи
- Цель: довести до рабочего состояния создание `nubes_vc_vdc` в `FullPipe`.
- Симптом: при создании VDC провайдер падал на `GET /instanceOperations/{opUid}?fields=cfsParams` с HTTP 500.
- В ходе разбора было подтверждено, что обычные сервисы через тот же провайдер работали, а VDC попадал в отдельную проблемную ветку backend-обработки.
---
## 2. Диагностика проблемы
### 2.1. Что ломалось
- В `provider/internal/core/client.go` create-flow делал `GET /instanceOperations/{opUid}?fields=cfsParams` сразу после создания операции.
- Для VDC этот запрос приводил к ошибке backend’а:
- `Invalid call of the function [getResourceRealmConfig]`
- `Cannot cast Object type [Struct] to a value of type [string]`
- источник ошибки: `/app/api/v1/resources/instance_operation_cfs_param.cfc`
- Причина по отчету: для `vc_vdc` вычислялся динамический `descr` у `storageConfig.name`, и backend падал на `resourceRealm`, который в DEV хранится как `Struct`, а не `string`.
### 2.2. Почему обычные сервисы не ломались
- На обычных сервисах этот `GET` либо не попадал в проблемный backend-код, либо не требовал вычисления `resourceRealm`.
- Для VDC в YAML есть специфическая зависимость:
- `generated/dev/resources_yaml/21_vc_vdc.yaml`
- `storageConfig.name` содержит вычисляемый `descr` с `getResourceRealmConfig(...resourceRealm...)`.
- Для обычных сервисов, например `vapp` и `postgres`, такого вычисляемого `resourceRealm`-контекста нет.
### 2.3. Почему `hasUnresolvedParams` мешал
- Эвристика проверяла **все** строковые параметры, а не только параметры с `ref_svc_id`.
- Для VDC это ломало fallback на обычных литералах вроде:
- `providerVdc = fast-2.8`
- `networkProvider = default`
- Эти значения не UUID и не JSON, но и резолвить их не нужно.
- В результате при падении GET провайдер вместо продолжения переходил в ошибку.
---
## 3. Диалог с Opus
### 3.1. Что просили у Opus
- Проверить только:
- `provider/internal/core/client.go`
- `docs/DEBUG_REPORT_VC_VDC_500.md`
- `generated/dev/resources_yaml/21_vc_vdc.yaml`
- `generated/dev/resources_yaml/26_vapp.yaml`
- `generated/dev/resources_yaml/90_postgres.yaml`
- Вопросы к Opus были узкими:
1. почему обычные сервисы работали, а VDC начал падать на `GET ?fields=cfsParams`
2. есть ли в VDC специфическая структура или зависимость, которой нет у обычных сервисов
3. является ли `hasUnresolvedParams` неверной эвристикой именно в этом месте
4. что именно надо исправить
### 3.2. Что ответил Opus по сути
- Root cause — не данные Terraform и не сами строки `fast-2.8` / `default`, а backend-ошибка на `GET /instanceOperations/{opUid}?fields=cfsParams` именно для VDC.
- VDC отличается от обычных сервисов тем, что в его YAML есть динамический `descr` для `storageConfig.name`, который тянет `resourceRealm`.
- `hasUnresolvedParams` была признана лишней и хрупкой эвристикой: она может ломать fallback на обычных строках.
- Итоговое решение Opus: при ошибке GET идти дальше по браузерному flow, без условий по всем строковым параметрам.
### 3.3. Дополнительные уточнения в диалоге
- Был отдельный спор по формулировке про Lucee / ColdFusion backend.
- В итоге было зафиксировано, что этот термин — не отдельная гипотеза, а просто обозначение backend-слоя, который уже фигурировал в отчетах и traceback’ах.
- Opus также подтвердил, что для VDC этот GET нужен только как вспомогательный шаг для `resolveRefSvcParamValues`, а не как обязательный бизнес-этап.
---
## 4. Что изменили в коде
### 4.1. `provider/internal/core/client.go`
- В `CreateGenericInstanceUniversalV6` удалён gate по `hasUnresolvedParams`.
- Теперь логика такая:
- если `GET /instanceOperations/{opUid}?fields=cfsParams` успешен — парсим и резолвим `ref_svc_id`
- если GET падает — просто продолжаем POST’ить параметры, а потом идём в `validate-cfs` и `run`
- Функция `hasUnresolvedParams` удалена полностью.
- После удаления была убрана осиротевшая документационная строка, оставшаяся над `isHexDigit`.
### 4.2. `provider/internal/core/client_test.go`
- Добавлен тест:
- `TestCreateGenericInstanceUniversalV6_ContinuesWhenOpDetailsGETFails`
- Тест моделирует:
- `POST /instances`
- `POST /instanceOperations`
- `GET /instanceOperations/{opUid}?fields=cfsParams` → 500
- `POST /instanceOperationCfsParams`
- `GET /instanceOperations/{opUid}/validate-cfs`
- `POST /instanceOperations/{opUid}/run`
- финальный `GET /instances/{uid}`
- Проверка теста:
- create-flow завершился успешно
- все 7 параметров были отправлены с ожидаемыми значениями
- polling по операции был ровно один раз
---
## 5. Проверка после правки
- `cd /home/naeel/TF/tf_provider/provider && go test ./internal/core` — успешно.
- После ревью был пойман и исправлен только косметический хвост:
- старый комментарий над `isHexDigit`, оставшийся после удаления `hasUnresolvedParams`.
- После этого пакет `internal/core` снова прошёл тесты.
---
## 6. Вывод по итогам сессии
- Проблема была не в обычных сервисах как таковых, а в специфике VDC-данных и backend-пути, который срабатывал на `GET ?fields=cfsParams`.
- `hasUnresolvedParams` была неверной эвристикой именно в create-flow VDC и ломала рабочий fallback.
- Правильное поведение: если GET падает, не гадать по строковым параметрам, а продолжать browser-like flow через POST параметров, validate и run.
---
## 7. Что дальше
- Следующий этап — уже не правка логики, а публикация и стендовая проверка при необходимости.
- DEV-релиз `2.0.3` успешно собран и загружен в registry `nubes-dev` через `TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev`.
- Перед этим уже был подготовлен короткий запрос на ревью для Opus и получен ответ, который подтвердил направление правки.
+2 -42
View File
@@ -272,15 +272,10 @@ func (c *UniversalClient) CreateGenericInstanceUniversalV6(ctx context.Context,
opDetailsResp, _, err := c.doRequest(ctx, "GET", fmt.Sprintf("/instanceOperations/%s?fields=cfsParams", opUid), nil)
var opDetails universalOpResponse
hasOpDetails := false
if err != nil {
if hasUnresolvedParams(params) {
return "", fmt.Errorf("не удалось получить детали операции: %w", err)
}
} else if jsonErr := json.Unmarshal(opDetailsResp, &opDetails); jsonErr != nil {
if hasUnresolvedParams(params) {
if err == nil {
if jsonErr := json.Unmarshal(opDetailsResp, &opDetails); jsonErr != nil {
return "", fmt.Errorf("не удалось разобрать детали операции: %w", jsonErr)
}
} else {
hasOpDetails = true
params, err = c.resolveRefSvcParamValues(ctx, opDetails.InstanceOperation.CfsParams, params)
if err != nil {
@@ -1359,41 +1354,6 @@ func isUUIDLike(value string) bool {
return true
}
// hasUnresolvedParams возвращает true, если хотя бы одно значение параметров похоже на
// неразрешённое имя вместо UUID. Значения map-fixed/json/array, числа и булевы пропускаются.
func hasUnresolvedParams(params map[int]string) bool {
for _, val := range params {
trimmed := strings.TrimSpace(val)
if trimmed == "" || trimmed == "{}" || trimmed == "[]" {
continue
}
if strings.HasPrefix(trimmed, "{") || strings.HasPrefix(trimmed, "[") {
// json/map-fixed/array не являются именами одиночных инстансов
continue
}
if trimmed == "true" || trimmed == "false" {
continue
}
isNum := true
for _, r := range trimmed {
if r < '0' || r > '9' {
isNum = false
break
}
}
if isNum {
continue
}
if isUUIDLike(trimmed) {
continue
}
// Обнаружена не-числовая строка, не JSON и не UUID (потенциальное имя).
// Если GET ?fields=cfsParams упал, такое имя невозможно разрешить в UUID.
return true
}
return false
}
func isHexDigit(r rune) bool {
return (r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F')
}
+77
View File
@@ -3,6 +3,7 @@ package core
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
@@ -231,4 +232,80 @@ func TestWaitForOperationFinish_Timeout(t *testing.T) {
}
}
func TestCreateGenericInstanceUniversalV6_ContinuesWhenOpDetailsGETFails(t *testing.T) {
var postedParams = make(map[int]string)
var pollCount atomic.Int32
c, cleanup := makeTestClient(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPost && r.URL.Path == "/instances":
w.Header().Set("Location", "./inst-1")
w.WriteHeader(http.StatusCreated)
_, _ = w.Write([]byte(`{"instanceUid":"inst-1"}`))
case r.Method == http.MethodPost && r.URL.Path == "/instanceOperations":
w.Header().Set("Location", "./op-1")
w.WriteHeader(http.StatusCreated)
_, _ = w.Write([]byte(`{"instanceOperationUid":"op-1"}`))
case r.Method == http.MethodGet && r.URL.Path == "/instanceOperations/op-1" && strings.Contains(r.URL.RawQuery, "fields=cfsParams"):
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(`{"message":"boom"}`))
case r.Method == http.MethodPost && r.URL.Path == "/instanceOperationCfsParams":
var req genericParamReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
t.Fatalf("failed to decode param request: %v", err)
}
postedParams[req.SvcOperationCfsParamId] = req.ParamValue
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{}`))
case r.Method == http.MethodGet && r.URL.Path == "/instanceOperations/op-1/validate-cfs":
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"ok":true}`))
case r.Method == http.MethodPost && r.URL.Path == "/instanceOperations/op-1/run":
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{}`))
case r.Method == http.MethodGet && r.URL.Path == "/instanceOperations/op-1" && strings.Contains(r.URL.RawQuery, "fields=dtFinish"):
if pollCount.Add(1) != 1 {
t.Fatalf("unexpected extra polling request: %s", r.URL.String())
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"instanceOperation":{"dtFinish":"2026-01-01T00:00:03Z","isSuccessful":true,"errorLog":null,"isInProgress":false,"isPending":false,"duration":1.5,"stages":[{"instanceOperationStageUid":"1","stage":"1. Validation","isSuccessful":true,"dtFinish":"2026-01-01T00:00:01Z","duration":1.0}]}}`))
case r.Method == http.MethodGet && r.URL.Path == "/instances/inst-1":
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"instance":{"instanceUid":"inst-1","explainedStatus":"running","isDeleted":false,"operationIsPending":false,"operationIsInProgress":false}}`))
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.String())
}
})
defer cleanup()
c.PollInterval = 10 * time.Millisecond
c.RetryBaseDelay = 10 * time.Millisecond
params := map[int]string{
30: "c3f81e4b-7a2b-4e1d-9e0a-123456789abc",
335: "fast-2.8",
361: `[{"name":"fast","size":200}]`,
397: "0",
557: "8",
558: "32",
746: "default",
}
if _, err := c.CreateGenericInstanceUniversalV6(context.Background(), 21, "f12vdc-2", params); err != nil {
t.Fatalf("expected create to succeed, got error: %v", err)
}
if len(postedParams) != len(params) {
t.Fatalf("expected %d posted params, got %d", len(params), len(postedParams))
}
for id, value := range params {
if got := postedParams[id]; got != value {
t.Fatalf("param %d mismatch: got %q, want %q", id, got, value)
}
}
if pollCount.Load() != 1 {
t.Fatalf("expected exactly 1 polling request, got %d", pollCount.Load())
}
}
func strPtr(s string) *string { return &s }