Fix VDC flow and FullPipe example
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 | (новая нумерация) |
|
||||
|
||||
## Как проверить
|
||||
|
||||
@@ -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 и получен ответ, который подтвердил направление правки.
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user