api-gateway: migrate YAML gen + provider core from deck-api (?endpoint=) to lk-api-gateway (REST paths)
- service_spec_gen: callAPI() auto-detects proxy vs REST mode by index.cfm presence
- core/client.go: buildURL() replaces all 4 ?endpoint= harcoded sites
- build-provider.sh: -ldflags -X main.version=${VERSION} for correct binary version
- 01_generate_yamls.sh (devops + root): Python auto-detect proxy vs REST
- 02_generate_resources_and_docs_v2.sh: pass -api-endpoint to doc generators
- 04_build_and_publish_docs.sh: normalize_api_endpoint without forced /index.cfm
- docs_template_gen + v2: -api-endpoint flag, hardcoded deck-api replaced
- provider.go (both): TODO(api-gateway) comments
- profiles/test/profile.env: NUBES_API_ENDPOINT -> lk-api-gateway-test
- HISTORY: 2026-07-02_api_gateway_migration.md
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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 := ""
|
||||
|
||||
|
||||
@@ -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("<div class=\"copy-ready-code\"><pre><code>")
|
||||
b.WriteString(exampleBlock(spec, version))
|
||||
b.WriteString(exampleBlock(spec, version, apiEndpoint))
|
||||
b.WriteString("</code></pre></div>\n\n")
|
||||
b.WriteString("## Outputs usage\n\n")
|
||||
b.WriteString("<div class=\"copy-ready-code\"><pre><code>")
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
// Основной ресурс (родитель) — ссылка
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user