diff --git a/01_generate_yamls.sh b/01_generate_yamls.sh index 530dac6..a3e228d 100644 --- a/01_generate_yamls.sh +++ b/01_generate_yamls.sh @@ -47,8 +47,13 @@ sid = "${sid}" endpoint = "${API_ENDPOINT}" token = "${NUBES_API_TOKEN}" -params = urllib.parse.urlencode({"endpoint": f"/services/{sid}"}) -url = f"{endpoint}?{params}" +# 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}") diff --git a/HISTORY/2026-07-02_api_gateway_migration.md b/HISTORY/2026-07-02_api_gateway_migration.md new file mode 100644 index 0000000..7f8883a --- /dev/null +++ b/HISTORY/2026-07-02_api_gateway_migration.md @@ -0,0 +1,98 @@ +# 2026-07-02 — Миграция с deck-api на API Gateway (lk-api-gateway) + +## Контекст + +Старый API: `https://deck-api-{stand}.ngcloud.ru/api/v1/index.cfm?endpoint=/services/123` +Новый API Gateway: `https://lk-api-gateway-{stand}.ngcloud.ru/api/v1/svc/services/123` + +Ответы JSON идентичны. Различается только формат URL: +- Старый: proxy через `index.cfm?endpoint=` +- Новый: прямые REST-пути + +Цель: перевести TEST-стенд на новый Gateway, сохранив совместимость со старым для dev/prod. + +## Архитектурное решение + +Авто-детект режима по наличию `index.cfm` в URL: +- Есть `index.cfm` → старый proxy (`?endpoint=`) +- Нет `index.cfm` → новый REST (прямая конкатенация пути) + +Один env-параметр `NUBES_API_ENDPOINT` управляет всем. + +## Изменения (11 файлов) + +### 1. Генератор YAML: `universal_rebuild/tools/service_spec_gen/generate_service_spec.go` +- `normalizeAPIEndpoint()` — убрано принудительное добавление `/index.cfm`, теперь только trim trailing slash +- `getViaProxy()` → `callAPI()` — авто-детект: если `index.cfm` в URL → `?endpoint=`, иначе → прямая конкатенация `{base}{path}` +- Добавлен метод `isProxyAPI()` для детекта +- Обновлён комментарий с документированием двух режимов + +### 2. Ядро провайдера: `universal_rebuild/internal/core/client.go` +- Добавлены `isProxyAPI()` и `buildURL(path string) string` — единая точка конструирования URL +- Заменены все 4 места с хардкодом `?endpoint=`: + - `FindExistingInstances` (пагинированный поиск, строка ~515) + - `GetInstanceState` (строка ~590) + - `GetInstanceStateRaw` (строка ~637) + - `doRequest` (POST/PUT/GET, строки ~838-853) + +### 3. Оркестратор YAML: `devops/01_generate_yamls.sh` +- Убрана принудительная нормализация `if [[ "$API_ENDPOINT" != */index.cfm ]]` → автоматическая +- Python-скрипт: авто-детект `"index.cfm" in endpoint` → `?endpoint=` vs прямой путь + +### 4. Генератор документации: `devops/02_generate_resources_and_docs_v2.sh` +- Добавлена передача `-api-endpoint "$DOCS_API_ENDPOINT"` в `docs_template_gen_v2` +- Нормализация: если нет ни `index.cfm`, ни `/svc` → дописать `/index.cfm` (обратная совместимость) + +### 5. Публикация доков: `devops/04_build_and_publish_docs.sh` +- `normalize_api_endpoint()` — убрано принудительное `/index.cfm`, только trim + +### 6. Корневой скрипт: `01_generate_yamls.sh` (корень) +- Python-скрипт: такой же авто-детект, как в devops/01 + +### 7-8. Шаблоны документации: `docs_template_gen_v2/main.go` + `docs_template_gen/main.go` +- Добавлен флаг `-api-endpoint` (default: старый deck-api) +- Все хардкоды `api_endpoint = "https://deck-api...index.cfm"` заменены на `fmt.Sprintf` +- Проброшено через `writeResourceDocs` → `buildExamplePage` → `exampleBlock` → `buildSubresourceExamplePage` + +### 9-10. Провайдеры: `internal/provider/provider.go` + `universal_rebuild/internal/provider/provider.go` +- Добавлен `TODO(api-gateway)` комментарий над дефолтным `apiEndpoint` +- Сам дефолт не менялся (runtime провайдера теперь использует `core.buildURL()`) + +### 11. Профиль TEST: `devops/profiles/test/profile.env` +- `NUBES_API_ENDPOINT` → `https://lk-api-gateway-test.ngcloud.ru/api/v1/svc` + +### Бонус: версия в бинарник +- `devops/build-provider.sh`: `go build` теперь с `-ldflags "-X main.version=${VERSION}"` — версия из профиля вшивается в бинарник (раньше всегда была из main.go) + +## Анализ пайплайна (попутно) + +Полный пайплайн: `services_list.txt` → `01_generate_yamls.sh` → `02_generate_resources_and_docs_v2.sh` → `03_build_and_upload_provider.sh` → `04_build_and_publish_docs.sh`. + +Найденные проблемы (не исправлялись, зафиксированы): +- `main.go` Address = `nubes-test` (только для test, для prod нужно `nubes`) +- Нет сборки под `darwin/arm64` (Apple Silicon) +- Корневой `01_generate_yamls.sh` ссылается на несуществующий `service_params_gen` +- Кросс-стадийная валидация отсутствует (сбои 01 не видны в 02) + +## Оставшиеся `deck-api` в коде + +Только как **значения по умолчанию** (переопределяются через `NUBES_API_ENDPOINT`): +- `service_spec_gen/generate_service_spec.go:150` — `getenvDefault("NUBES_API_ENDPOINT", "https://deck-api...")` +- `devops/01_generate_yamls.sh:108` — `${NUBES_API_ENDPOINT:-https://deck-api...}` +- `devops/02_generate_resources_and_docs_v2.sh:95` — аналогично +- `devops/04_build_and_publish_docs.sh:13,87` — аналогично +- `docs_template_gen_v2/main.go:91` — flag default +- `docs_template_gen/main.go:81` — flag default +- `internal/provider/provider.go:105` + `universal_rebuild/...` — с TODO + +Хардкодов в теле функций/примеров больше нет. + +## Для перехода dev/prod + +Достаточно поменять одну строку в `profile.env`: +``` +NUBES_API_ENDPOINT="https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc" # dev +NUBES_API_ENDPOINT="https://lk-api-gateway.ngcloud.ru/api/v1/svc" # prod +``` + +Всё остальное — авто-детект. diff --git a/devops/01_generate_yamls.sh b/devops/01_generate_yamls.sh index 52ac253..fd16b5d 100755 --- a/devops/01_generate_yamls.sh +++ b/devops/01_generate_yamls.sh @@ -106,10 +106,8 @@ if [[ ! -f "$SERVICES_FILE" ]]; then fi API_ENDPOINT="${NUBES_API_ENDPOINT:-https://deck-api.ngcloud.ru/api/v1/index.cfm}" -# Generator expects proxy-style endpoint (index.cfm?endpoint=/...). -if [[ "$API_ENDPOINT" != */index.cfm ]]; then - API_ENDPOINT="${API_ENDPOINT%/}/index.cfm" -fi +# Auto-detect API style: if endpoint contains "index.cfm" → legacy proxy (?endpoint=), +# otherwise → new REST gateway (direct paths). No forced /index.cfm normalization. GENERATED_DIR="${PROFILE_DIR}/generated" YAML_OUTPUT_DIR_DEFAULT="${GENERATED_DIR}/resources_yaml" @@ -179,8 +177,12 @@ endpoint = "${API_ENDPOINT}" token = "${NUBES_API_TOKEN}" max_retries = 3 -params = urllib.parse.urlencode({"endpoint": f"/services/{sid}"}) -url = f"{endpoint}?{params}" +# Auto-detect API style: "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}" for attempt in range(1, max_retries + 1): try: diff --git a/devops/02_generate_resources_and_docs_v2.sh b/devops/02_generate_resources_and_docs_v2.sh index d777e15..94046e9 100755 --- a/devops/02_generate_resources_and_docs_v2.sh +++ b/devops/02_generate_resources_and_docs_v2.sh @@ -89,9 +89,20 @@ cp -R "$TMP_GEN_DIR/." "$GO_OUTPUT_DIR/" echo "Generating docs via template generator..." mkdir -p "$DOCS_DIR" + +# Determine API endpoint for doc examples. +# Use NUBES_API_ENDPOINT from profile.env, fall back to production default. +DOCS_API_ENDPOINT="${NUBES_API_ENDPOINT:-https://deck-api.ngcloud.ru/api/v1/index.cfm}" +# If endpoint looks like new REST gateway (no index.cfm), keep as-is. +# If it's old-style without index.cfm, append it for backward compat in docs. +if [[ "$DOCS_API_ENDPOINT" != *"/index.cfm"* ]] && [[ "$DOCS_API_ENDPOINT" != *"/svc"* ]]; then + DOCS_API_ENDPOINT="${DOCS_API_ENDPOINT%/}/index.cfm" +fi + go run ./tools/docs_template_gen_v2 \ -resources "$RESOURCES_YAML_DIR" \ -docs "$DOCS_DIR" \ - -services "$SERVICES_LIST_PATH" + -services "$SERVICES_LIST_PATH" \ + -api-endpoint "$DOCS_API_ENDPOINT" echo "Resources and docs generated in template format." diff --git a/devops/04_build_and_publish_docs.sh b/devops/04_build_and_publish_docs.sh index 4dcd811..c992991 100755 --- a/devops/04_build_and_publish_docs.sh +++ b/devops/04_build_and_publish_docs.sh @@ -13,11 +13,9 @@ normalize_api_endpoint() { echo "https://deck-api.ngcloud.ru/api/v1/index.cfm" return fi - if [[ "$endpoint" == */index.cfm ]]; then - echo "$endpoint" - return - fi - echo "${endpoint}/index.cfm" + # Keep as-is: both legacy proxy (index.cfm) and new REST gateway are valid. + # No forced /index.cfm normalization — the downstream tools auto-detect. + echo "$endpoint" } if [[ "${1:-}" == "--profile" ]]; then diff --git a/devops/build-provider.sh b/devops/build-provider.sh index f8d2a5e..b9387ee 100755 --- a/devops/build-provider.sh +++ b/devops/build-provider.sh @@ -77,7 +77,7 @@ build_and_zip() { fi echo "Building for ${os}/${arch}..." - (cd "$PROVIDER_DIR" && CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" go build -o "${BUILD_DIR}/${binary}" .) + (cd "$PROVIDER_DIR" && CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" go build -ldflags "-X main.version=${VERSION}" -o "${BUILD_DIR}/${binary}" .) (cd "$BUILD_DIR" && python3 -m zipfile -c "terraform-provider-nubes_${VERSION}_${os}_${arch}.zip" "$binary") rm -f "${BUILD_DIR:?}/${binary}" diff --git a/devops/profiles/test/profile.env b/devops/profiles/test/profile.env index aab35aa..ce1e8ca 100644 --- a/devops/profiles/test/profile.env +++ b/devops/profiles/test/profile.env @@ -1,5 +1,5 @@ # Stand profile: TEST -NUBES_API_ENDPOINT="https://deck-api-test.ngcloud.ru/api/v1" +NUBES_API_ENDPOINT="https://lk-api-gateway-test.ngcloud.ru/api/v1/svc" TOKEN_FILE="secrets/test.token" # Release versions diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 07d757d..276c13f 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -100,6 +100,8 @@ func (p *NubesProvider) Configure(ctx context.Context, req provider.ConfigureReq } // 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 := "" diff --git a/universal_rebuild/internal/core/client.go b/universal_rebuild/internal/core/client.go index 84c6a72..5589ace 100644 --- a/universal_rebuild/internal/core/client.go +++ b/universal_rebuild/internal/core/client.go @@ -29,6 +29,31 @@ type UniversalClient struct { LogLevel string } +// isProxyAPI returns true if ApiEndpoint uses legacy ?endpoint= proxy pattern (contains "index.cfm"). +func (c *UniversalClient) isProxyAPI() bool { + return strings.Contains(c.ApiEndpoint, "index.cfm") +} + +// buildURL constructs a full URL from the base endpoint + path. +// Legacy proxy: index.cfm?endpoint=/instances&page=1 +// New REST gateway: /api/v1/svc/instances?page=1 +func (c *UniversalClient) buildURL(path string) string { + if c.isProxyAPI() { + endpointPath := path + extraQuery := "" + if idx := strings.Index(path, "?"); idx >= 0 { + endpointPath = path[:idx] + extraQuery = path[idx+1:] + } + rawQuery := "endpoint=" + endpointPath + if extraQuery != "" { + rawQuery += "&" + extraQuery + } + return c.ApiEndpoint + "?" + rawQuery + } + return c.ApiEndpoint + path +} + // ctxKeyLogLevel — ключ для переопределения LogLevel на уровне ресурса через context.WithValue. type ctxKeyLogLevelType struct{} @@ -512,7 +537,7 @@ func (c *UniversalClient) FindInstanceByDisplayName(ctx context.Context, service if len(found) == 0 { page := 1 for { - reqURL := fmt.Sprintf("%s?endpoint=/instances&page=%d&size=100", c.ApiEndpoint, page) + reqURL := c.buildURL(fmt.Sprintf("/instances?page=%d&size=100", page)) req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil) if err != nil { return nil, err @@ -587,7 +612,7 @@ func (c *UniversalClient) FindInstanceByDisplayName(ctx context.Context, service } func (c *UniversalClient) GetInstanceState(ctx context.Context, instanceUid string) (*InstanceStateResponse, error) { - url := fmt.Sprintf("%s?endpoint=/instances/%s", c.ApiEndpoint, instanceUid) + url := c.buildURL(fmt.Sprintf("/instances/%s", instanceUid)) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, err @@ -634,7 +659,7 @@ func isInstanceDeleted(state *InstanceStateResponse) bool { // GetInstanceStateRaw получает состояние инстанса БЕЗ валидации статуса. // Используется для проверки ref-параметров: нужно читать даже deleted/suspended инстансы. func (c *UniversalClient) GetInstanceStateRaw(ctx context.Context, instanceUid string) (*InstanceStateResponse, error) { - reqURL := fmt.Sprintf("%s?endpoint=/instances/%s", c.ApiEndpoint, instanceUid) + reqURL := c.buildURL(fmt.Sprintf("/instances/%s", instanceUid)) req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil) if err != nil { return nil, err @@ -835,24 +860,12 @@ func (c *UniversalClient) doRequest(ctx context.Context, method, path string, pa body = bytes.NewBuffer(b) } - req, err := http.NewRequestWithContext(ctx, method, c.ApiEndpoint, body) + reqURL := c.buildURL(path) + req, err := http.NewRequestWithContext(ctx, method, reqURL, body) if err != nil { return nil, nil, err } - // ?endpoint= формат — без url.Values.Encode (сохраняет / как /) - endpointPath := path - extraQuery := "" - if idx := strings.Index(path, "?"); idx >= 0 { - endpointPath = path[:idx] - extraQuery = path[idx+1:] - } - rawQuery := "endpoint=" + endpointPath - if extraQuery != "" { - rawQuery += "&" + extraQuery - } - req.URL.RawQuery = rawQuery - // Форсируем новое TCP-соединение: DDoS-Guard может блочить POST на keep-alive req.Close = true diff --git a/universal_rebuild/internal/provider/provider.go b/universal_rebuild/internal/provider/provider.go index 99c1007..be70a3f 100644 --- a/universal_rebuild/internal/provider/provider.go +++ b/universal_rebuild/internal/provider/provider.go @@ -92,6 +92,8 @@ func (p *NubesProvider) Configure(ctx context.Context, req provider.ConfigureReq return } + // 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 := "" diff --git a/universal_rebuild/tools/docs_template_gen/main.go b/universal_rebuild/tools/docs_template_gen/main.go index cb20519..118b7da 100644 --- a/universal_rebuild/tools/docs_template_gen/main.go +++ b/universal_rebuild/tools/docs_template_gen/main.go @@ -78,6 +78,7 @@ func main() { servicesListFlag := flag.String("services", "", "Path to devops/config/services_list.txt") excludeFlag := flag.String("exclude", "clickhouse", "Comma-separated resource names to skip") versionFlag := flag.String("version", "", "Provider version for example block") + apiEndpointFlag := flag.String("api-endpoint", "https://deck-api.ngcloud.ru/api/v1/index.cfm", "API endpoint for example block") flag.Parse() root := detectRoot() @@ -89,6 +90,7 @@ func main() { if version == "" { version = detectVersion(filepath.Join(root, "universal_rebuild", "main.go")) } + apiEndpoint := *apiEndpointFlag servicesOrder := loadServicesList(servicesList) specs := loadSpecs(resourcesDir, servicesOrder) @@ -105,7 +107,7 @@ func main() { if excludeSet[spec.Name] { continue } - writeResourceDocs(docsDir, spec, version) + writeResourceDocs(docsDir, spec, version, apiEndpoint) } } @@ -222,12 +224,12 @@ func loadSpecs(dir string, ordered []ServiceMeta) []ServiceSpec { return specs } -func writeResourceDocs(docsDir string, spec ServiceSpec, version string) { +func writeResourceDocs(docsDir string, spec ServiceSpec, version string, apiEndpoint string) { base := spec.Name nav := fmt.Sprintf("[Manual](%s.md) | [Create params](%s_params_create.md) | [Modify params](%s_params_modify.md) | [Output params](%s_outputs.md) | [Operations](%s_ops.md) | [Example](%s_example.md)", base, base, base, base, base, base) writeFile(filepath.Join(docsDir, base+".md"), buildManualPage(spec, nav)) - writeFile(filepath.Join(docsDir, base+"_example.md"), buildExamplePage(spec, nav, version)) + writeFile(filepath.Join(docsDir, base+"_example.md"), buildExamplePage(spec, nav, version, apiEndpoint)) writeFile(filepath.Join(docsDir, base+"_params_create.md"), buildCreateParamsPage(spec, nav)) writeFile(filepath.Join(docsDir, base+"_params_modify.md"), buildModifyParamsPage(spec, nav)) writeFile(filepath.Join(docsDir, base+"_outputs.md"), buildOutputsPage(spec, nav)) @@ -263,12 +265,12 @@ func buildManualPage(spec ServiceSpec, nav string) string { return b.String() } -func buildExamplePage(spec ServiceSpec, nav, version string) string { +func buildExamplePage(spec ServiceSpec, nav, version string, apiEndpoint string) string { var b strings.Builder b.WriteString(buildHeader(spec, nav)) b.WriteString(fmt.Sprintf("## Copy-ready manifest (`%s_main.tf`)\n\n", spec.Name)) b.WriteString("
")
- b.WriteString(exampleBlock(spec, version))
+ b.WriteString(exampleBlock(spec, version, apiEndpoint))
b.WriteString("")
@@ -285,7 +287,7 @@ func buildExamplePage(spec ServiceSpec, nav, version string) string {
return b.String()
}
-func exampleBlock(spec ServiceSpec, version string) string {
+func exampleBlock(spec ServiceSpec, version string, apiEndpoint string) string {
createParams := findParams(spec.Operations, "create")
requiredParams, defaultParams := splitParams(createParams)
@@ -301,7 +303,7 @@ func exampleBlock(spec ServiceSpec, version string) string {
b.WriteString("# Токен доступа api_token к Nubes API — замените на реальный.\n")
b.WriteString("provider \"nubes\" {\n")
b.WriteString(" api_token = \"token_***\"\n")
- b.WriteString(" api_endpoint = \"https://deck-api.ngcloud.ru/api/v1/index.cfm\"\n")
+ b.WriteString(fmt.Sprintf(" api_endpoint = \"%s\"\n", apiEndpoint))
b.WriteString("}\n")
b.WriteString(fmt.Sprintf("resource \"nubes_%s\" \"baza\" {\n", spec.Name))
diff --git a/universal_rebuild/tools/docs_template_gen_v2/main.go b/universal_rebuild/tools/docs_template_gen_v2/main.go
index 2f92f8e..63fad11 100644
--- a/universal_rebuild/tools/docs_template_gen_v2/main.go
+++ b/universal_rebuild/tools/docs_template_gen_v2/main.go
@@ -88,6 +88,7 @@ func main() {
servicesListFlag := flag.String("services", "", "Path to devops/config/services_list.txt")
excludeFlag := flag.String("exclude", "", "Comma-separated resource names to skip (e.g. clickhouse)")
versionFlag := flag.String("version", "", "Provider version for example block")
+ apiEndpointFlag := flag.String("api-endpoint", "https://deck-api.ngcloud.ru/api/v1/index.cfm", "API endpoint for example block")
flag.Parse()
root := detectRoot()
@@ -100,6 +101,7 @@ func main() {
if version == "" {
version = detectVersion(filepath.Join(root, "universal_rebuild", "main.go"))
}
+ apiEndpoint := *apiEndpointFlag
servicesOrder := loadServicesList(servicesList)
specs := loadSpecs(resourcesDir, servicesOrder)
@@ -118,7 +120,7 @@ func main() {
if excludeSet[spec.Name] {
continue
}
- writeResourceDocs(docsDir, spec, version)
+ writeResourceDocs(docsDir, spec, version, apiEndpoint)
processedSpecs = append(processedSpecs, spec)
}
generateIndexMD(docsDir, processedSpecs)
@@ -296,12 +298,12 @@ func collectSubresources(spec ServiceSpec) []string {
return out
}
-func writeResourceDocs(docsDir string, spec ServiceSpec, version string) {
+func writeResourceDocs(docsDir string, spec ServiceSpec, version string, apiEndpoint string) {
base := spec.Name
nav := fmt.Sprintf("[Manual](%s.md) | [Create params](%s_params_create.md) | [Modify params](%s_params_modify.md) | [Output params](%s_outputs.md) | [Operations](%s_ops.md) | [Example](%s_example.md)", base, base, base, base, base, base)
writeFile(filepath.Join(docsDir, base+".md"), buildManualPage(spec, nav))
- writeFile(filepath.Join(docsDir, base+"_example.md"), buildExamplePage(spec, nav, version))
+ writeFile(filepath.Join(docsDir, base+"_example.md"), buildExamplePage(spec, nav, version, apiEndpoint))
writeFile(filepath.Join(docsDir, base+"_params_create.md"), buildCreateParamsPage(spec, nav))
writeFile(filepath.Join(docsDir, base+"_params_modify.md"), buildModifyParamsPage(spec, nav))
writeFile(filepath.Join(docsDir, base+"_outputs.md"), buildOutputsPage(spec, nav))
@@ -314,7 +316,7 @@ func writeResourceDocs(docsDir string, spec ServiceSpec, version string) {
srNav := fmt.Sprintf("[%s (основной)](%s.md) | [Operations](%s_ops.md) | [%s](%s.md) | [Example](%s_example.md)",
spec.ServiceDisplayName, base, base, capitalize(srName), srBase, srBase)
writeFile(filepath.Join(docsDir, srBase+".md"), buildSubresourcePage(spec, srName, srNav))
- writeFile(filepath.Join(docsDir, srBase+"_example.md"), buildSubresourceExamplePage(spec, srName, srNav, version))
+ writeFile(filepath.Join(docsDir, srBase+"_example.md"), buildSubresourceExamplePage(spec, srName, srNav, version, apiEndpoint))
}
}
@@ -347,12 +349,12 @@ func buildManualPage(spec ServiceSpec, nav string) string {
return b.String()
}
-func buildExamplePage(spec ServiceSpec, nav, version string) string {
+func buildExamplePage(spec ServiceSpec, nav, version string, apiEndpoint string) string {
var b strings.Builder
b.WriteString(buildHeader(spec, nav))
b.WriteString(fmt.Sprintf("## Copy-ready manifest (`%s_main.tf`)\n\n", spec.Name))
b.WriteString("```hcl\n")
- b.WriteString(exampleBlock(spec, version))
+ b.WriteString(exampleBlock(spec, version, apiEndpoint))
b.WriteString("```\n\n")
b.WriteString("## Outputs usage\n\n")
b.WriteString("```hcl\n")
@@ -369,7 +371,7 @@ func buildExamplePage(spec ServiceSpec, nav, version string) string {
return b.String()
}
-func exampleBlock(spec ServiceSpec, version string) string {
+func exampleBlock(spec ServiceSpec, version string, apiEndpoint string) string {
createParams := findParams(spec.Operations, "create")
requiredParams, defaultParams := splitParams(createParams)
@@ -385,7 +387,7 @@ func exampleBlock(spec ServiceSpec, version string) string {
b.WriteString("# \u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 api_token \u043a Nubes API \u2014 \u0437\u0430\u043c\u0435\u043d\u0438\u0442\u0435 \u043d\u0430 \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u0439.\n")
b.WriteString("provider \"nubes\" {\n")
b.WriteString(" api_token = \"token_***\"\n")
- b.WriteString(" api_endpoint = \"https://deck-api.ngcloud.ru/api/v1/index.cfm\"\n")
+ b.WriteString(fmt.Sprintf(" api_endpoint = \"%s\"\n", apiEndpoint))
b.WriteString("}\n")
b.WriteString(fmt.Sprintf("resource \"nubes_%s\" \"baza\" {\n", spec.Name))
@@ -1047,7 +1049,7 @@ func buildSubresourcePage(spec ServiceSpec, srName string, nav string) string {
}
// buildSubresourceExamplePage генерирует страницу с HCL-примером для subresource.
-func buildSubresourceExamplePage(spec ServiceSpec, srName string, nav string, version string) string {
+func buildSubresourceExamplePage(spec ServiceSpec, srName string, nav string, version string, apiEndpoint string) string {
var b strings.Builder
srResourceName := fmt.Sprintf("nubes_%s_%s", spec.Name, slug(srName))
b.WriteString(fmt.Sprintf("# Resource %s — Example\n\n", srResourceName))
@@ -1068,7 +1070,7 @@ func buildSubresourceExamplePage(spec ServiceSpec, srName string, nav string, ve
b.WriteString("provider \"nubes\" {\n")
b.WriteString(" api_token = \"token_***\"\n")
- b.WriteString(" api_endpoint = \"https://deck-api.ngcloud.ru/api/v1/index.cfm\"\n")
+ b.WriteString(fmt.Sprintf(" api_endpoint = \"%s\"\n", apiEndpoint))
b.WriteString("}\n\n")
// Основной ресурс (родитель) — ссылка
diff --git a/universal_rebuild/tools/service_spec_gen/generate_service_spec.go b/universal_rebuild/tools/service_spec_gen/generate_service_spec.go
index acd92f2..3e409d3 100644
--- a/universal_rebuild/tools/service_spec_gen/generate_service_spec.go
+++ b/universal_rebuild/tools/service_spec_gen/generate_service_spec.go
@@ -27,6 +27,9 @@ import (
// Env:
// - NUBES_API_TOKEN (preferred) or TOKEN_FILE
// - NUBES_API_ENDPOINT (default: https://deck-api.ngcloud.ru/api/v1/index.cfm)
+// Supports two modes, auto-detected:
+// - Proxy (legacy): contains "index.cfm" → ?endpoint=/services/123
+// - REST (new API Gateway): no "index.cfm" → /api/v1/svc/services/123
// - NUBES_SERVICE_ID (optional for single service)
// - NUBES_SERVICE_NAME (optional override for single service)
// - NUBES_SERVICES_FILE (default: devops/config/services_list.txt)
@@ -142,7 +145,8 @@ type serviceRef struct {
}
func loadConfig() (config, error) {
- // API endpoint defaults to production; token is required.
+ // API endpoint defaults to production (legacy proxy); token is required.
+ // New API Gateway (REST) endpoints are also supported — auto-detected by presence of "index.cfm".
apiEndpoint := getenvDefault("NUBES_API_ENDPOINT", "https://deck-api.ngcloud.ru/api/v1/index.cfm")
apiEndpoint = normalizeAPIEndpoint(apiEndpoint)
apiToken, err := loadToken()
@@ -372,7 +376,7 @@ func (c *apiClient) getService(serviceID int) (serviceInfo, error) {
// /services/{id} returns display names, MAN, and a list of operations.
endpoint := fmt.Sprintf("/services/%d", serviceID)
var res serviceResponse
- if err := c.getViaProxy(endpoint, &res); err != nil {
+ if err := c.callAPI(endpoint, &res); err != nil {
return serviceInfo{}, err
}
return res.Service, nil
@@ -382,13 +386,13 @@ func (c *apiClient) getServiceOperation(svcOperationId int) (serviceOperationInf
// /serviceOperation/{id} returns params and MAN for a single operation.
endpoint := fmt.Sprintf("/serviceOperation/%d", svcOperationId)
var res serviceOperationResponse
- if err := c.getViaProxy(endpoint, &res); err != nil {
+ if err := c.callAPI(endpoint, &res); err != nil {
return serviceOperationInfo{}, err
}
return res.ServiceOperation, nil
}
-func (c *apiClient) getViaProxy(endpoint string, out interface{}) error {
+func (c *apiClient) callAPI(endpoint string, out interface{}) error {
const maxRetries = 3
baseDelay := 2 * time.Second
@@ -398,13 +402,18 @@ func (c *apiClient) getViaProxy(endpoint string, out interface{}) error {
time.Sleep(baseDelay * time.Duration(1<<(attempt-1)))
}
- req, err := http.NewRequest("GET", c.endpoint, nil)
+ var reqURL string
+ if c.isProxyAPI() {
+ // Legacy proxy: index.cfm?endpoint=/services/123
+ reqURL = c.endpoint + "?endpoint=" + endpoint
+ } else {
+ // New REST gateway: /api/v1/svc/services/123
+ reqURL = c.endpoint + endpoint
+ }
+ req, err := http.NewRequest("GET", reqURL, nil)
if err != nil {
return err
}
- q := req.URL.Query()
- q.Set("endpoint", endpoint)
- req.URL.RawQuery = q.Encode()
if c.token != "" {
req.Header.Set("Authorization", "Bearer "+c.token)
}
@@ -441,7 +450,7 @@ func (c *apiClient) getViaProxy(endpoint string, out interface{}) error {
return json.Unmarshal(body, out)
}
- return fmt.Errorf("getViaProxy failed after %d retries: %w", maxRetries, lastErr)
+ return fmt.Errorf("callAPI failed after %d retries: %w", maxRetries, lastErr)
}
func (c *apiClient) collectOperations(ops []operationInfo) ([]OperationSpec, bool, bool, bool, error) {
@@ -725,13 +734,13 @@ func collapseUnderscores(value string) string {
}
func normalizeAPIEndpoint(raw string) string {
- // Accept base API URL with or without index.cfm; normalize to index.cfm.
- endpoint := strings.TrimSpace(raw)
- if endpoint == "" {
- return endpoint
- }
- if strings.HasSuffix(endpoint, "/index.cfm") {
- return endpoint
- }
- return strings.TrimRight(endpoint, "/") + "/index.cfm"
+ // Accept both legacy proxy (index.cfm) and new REST gateway endpoints.
+ // Just trim trailing slash — no forced /index.cfm appending.
+ // The callAPI method auto-detects proxy vs REST mode by checking for "index.cfm".
+ return strings.TrimRight(strings.TrimSpace(raw), "/")
+}
+
+// isProxyAPI returns true if the endpoint uses the legacy ?endpoint= proxy pattern.
+func (c *apiClient) isProxyAPI() bool {
+ return strings.Contains(c.endpoint, "index.cfm")
}