From 1ec6a0fedcd38c8998437337baed98e639c18279 Mon Sep 17 00:00:00 2001 From: Repinoid Date: Mon, 21 Sep 2026 13:02:25 +0300 Subject: [PATCH] Fix VDC flow and FullPipe example --- DEV_STAND/FullPipe/terraform.tfvars.example | 4 +- TOOLS/config/dev/profile.env | 2 +- TOOLS/config/dev/services_list.txt | 13 +-- TOOLS/scripts/build-provider.sh | 2 +- VERSIONS.md | 2 +- docs/CHAT_RESUME_2026-09-21.md | 111 ++++++++++++++++++++ provider/internal/core/client.go | 44 +------- provider/internal/core/client_test.go | 77 ++++++++++++++ 8 files changed, 196 insertions(+), 59 deletions(-) create mode 100644 docs/CHAT_RESUME_2026-09-21.md diff --git a/DEV_STAND/FullPipe/terraform.tfvars.example b/DEV_STAND/FullPipe/terraform.tfvars.example index aa583d5..ef1a073 100644 --- a/DEV_STAND/FullPipe/terraform.tfvars.example +++ b/DEV_STAND/FullPipe/terraform.tfvars.example @@ -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 diff --git a/TOOLS/config/dev/profile.env b/TOOLS/config/dev/profile.env index e496756..7a5034a 100644 --- a/TOOLS/config/dev/profile.env +++ b/TOOLS/config/dev/profile.env @@ -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" diff --git a/TOOLS/config/dev/services_list.txt b/TOOLS/config/dev/services_list.txt index 5a4bd06..610a421 100644 --- a/TOOLS/config/dev/services_list.txt +++ b/TOOLS/config/dev/services_list.txt @@ -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 diff --git a/TOOLS/scripts/build-provider.sh b/TOOLS/scripts/build-provider.sh index eef8704..41a714e 100755 --- a/TOOLS/scripts/build-provider.sh +++ b/TOOLS/scripts/build-provider.sh @@ -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}" diff --git a/VERSIONS.md b/VERSIONS.md index 708ed62..7e24636 100644 --- a/VERSIONS.md +++ b/VERSIONS.md @@ -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 | (новая нумерация) | ## Как проверить diff --git a/docs/CHAT_RESUME_2026-09-21.md b/docs/CHAT_RESUME_2026-09-21.md new file mode 100644 index 0000000..a14d72f --- /dev/null +++ b/docs/CHAT_RESUME_2026-09-21.md @@ -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 и получен ответ, который подтвердил направление правки. diff --git a/provider/internal/core/client.go b/provider/internal/core/client.go index 94c1986..9d6721c 100644 --- a/provider/internal/core/client.go +++ b/provider/internal/core/client.go @@ -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') } diff --git a/provider/internal/core/client_test.go b/provider/internal/core/client_test.go index 4ead441..1a7638f 100644 --- a/provider/internal/core/client_test.go +++ b/provider/internal/core/client_test.go @@ -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 }