Files
tf_provider/TOOLS/yaml-generator/internal/client/client.go
T
“Naeel” 46e847a3c1 docs: add detailed package-level comments to all TOOLS/ and provider/
TOOLS:
- yaml-generator: types (API↔YAML), client (proxy/REST + retry)
- resource-generator: types (3 resource kinds), loader (pipeline),
  writers (generated files), params (merge/CreateOnly/ForceNew)
- docs-generator, ops-generator — already documented

provider:
- main.go: entrypoint + build instructions
- core: API client (GetInstance, RunOperation, GetInstanceState)
- provider: provider registration (New, Resources, Schema)
- resources_core: shared CRUD, state refresh, params, validation
2026-07-05 10:24:43 +04:00

235 lines
6.3 KiB
Go

// Package client — HTTP-клиент для API Nubes.
//
// Поддерживает два режима API, автоопределяемых по URL:
// - Legacy proxy: содержит "index.cfm" → ?endpoint=/services/123
// - REST Gateway: без "index.cfm" → /api/v1/svc/services/123
//
// Ретраи: до 3 попыток с экспоненциальной задержкой (2s, 4s, 8s).
// Ретраятся: сетевые ошибки, 429 (rate limit), 5xx.
//
// Все методы возвращают типы из пакета types.
package client
import (
"encoding/json"
"fmt"
"io"
"net/http"
"sort"
"strings"
"time"
"yaml-generator/internal/normalize"
"yaml-generator/internal/types"
)
// Client — HTTP-клиент для Nubes API.
type Client struct {
endpoint string
token string
httpClient *http.Client
}
// New создаёт новый API-клиент.
func New(endpoint string, token string) *Client {
return &Client{
endpoint: endpoint,
token: token,
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
}
}
// GetService получает метаданные сервиса.
func (c *Client) GetService(serviceID int) (types.ServiceInfo, error) {
endpoint := fmt.Sprintf("/services/%d", serviceID)
var res types.ServiceResponse
if err := c.callAPI(endpoint, &res); err != nil {
return types.ServiceInfo{}, err
}
return res.Service, nil
}
// GetServiceOperation получает детали операции (параметры + MAN).
func (c *Client) GetServiceOperation(svcOperationID int) (types.ServiceOperationInfo, error) {
endpoint := fmt.Sprintf("/serviceOperation/%d", svcOperationID)
var res types.ServiceOperationResponse
if err := c.callAPI(endpoint, &res); err != nil {
return types.ServiceOperationInfo{}, err
}
return res.ServiceOperation, nil
}
func (c *Client) callAPI(endpoint string, out interface{}) error {
const maxRetries = 3
baseDelay := 2 * time.Second
var lastErr error
for attempt := 0; attempt <= maxRetries; attempt++ {
if attempt > 0 {
time.Sleep(baseDelay * time.Duration(1<<(attempt-1)))
}
var reqURL string
if c.isProxyAPI() {
reqURL = c.endpoint + "?endpoint=" + endpoint
} else {
reqURL = c.endpoint + endpoint
}
req, err := http.NewRequest("GET", reqURL, nil)
if err != nil {
return err
}
if c.token != "" {
req.Header.Set("Authorization", "Bearer "+c.token)
}
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; Terraform-Provider-Nubes/Generator)")
resp, err := c.httpClient.Do(req)
if err != nil {
lastErr = err
if attempt < maxRetries {
continue
}
return err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
lastErr = readErr
if attempt < maxRetries {
continue
}
return readErr
}
if resp.StatusCode != 200 {
err := fmt.Errorf("status %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
if attempt < maxRetries && (resp.StatusCode == 429 || resp.StatusCode >= 500) {
lastErr = err
continue
}
return err
}
return json.Unmarshal(body, out)
}
return fmt.Errorf("callAPI failed after %d retries: %w", maxRetries, lastErr)
}
func (c *Client) isProxyAPI() bool {
return strings.Contains(c.endpoint, "index.cfm")
}
// CollectOperations собирает и классифицирует все операции сервиса.
func (c *Client) CollectOperations(ops []types.OperationInfo) ([]types.OperationSpec, bool, bool, bool, error) {
result := make([]types.OperationSpec, 0, len(ops))
hasSuspend := false
hasResume := false
hasDelete := false
for _, op := range ops {
opName := normalize.OperationName(op.Operation)
if opName == "" {
continue
}
opInfo, err := c.GetServiceOperation(op.SvcOperationID)
if err != nil {
return nil, false, false, false, err
}
params := make([]types.ParamSpec, 0, len(opInfo.CfsParams))
for _, p := range opInfo.CfsParams {
params = append(params, types.ParamSpec{
ID: p.ID,
Code: p.Code,
DataType: strings.TrimSpace(p.DataType),
Required: p.IsRequired,
Default: normalizeDefault(p.DefaultValue),
ValueList: normalizeValueList(p.ValueList),
RefSvcID: p.RefSvcID,
Func: strings.TrimSpace(p.Func),
Regex: strings.TrimSpace(p.Regex),
UniqueScope: strings.TrimSpace(p.UniqueScope),
MaxLength: p.MaxLength,
MinLength: p.MinLength,
MaxValue: normalizeDefault(p.MaxValue),
MinValue: normalizeDefault(p.MinValue),
Descr: strings.TrimSpace(p.Descr),
Man: strings.TrimSpace(p.Man),
Sort: p.Sort,
DependsOn: p.DependsOnCfsParams,
IsModifiable: p.IsModifiable,
IsSensitive: p.IsSensitive,
})
}
sort.Slice(params, func(i, j int) bool { return params[i].ID < params[j].ID })
kind, action, subresource := classifyOperation(opName)
if kind == "instance" && action == "suspend" {
hasSuspend = true
}
if kind == "instance" && action == "resume" {
hasResume = true
}
if kind == "instance" && action == "delete" {
hasDelete = true
}
result = append(result, types.OperationSpec{
Name: opName,
ID: opInfo.SvcOperationID,
Kind: kind,
Action: action,
Subresource: subresource,
Man: strings.TrimSpace(opInfo.Man),
Params: params,
})
}
sort.Slice(result, func(i, j int) bool {
if result[i].Name == result[j].Name {
return result[i].ID < result[j].ID
}
return result[i].Name < result[j].Name
})
return result, hasSuspend, hasResume, hasDelete, nil
}
func classifyOperation(name string) (string, string, string) {
n := strings.ToLower(strings.TrimSpace(name))
if n == "create" || n == "modify" || n == "delete" || n == "suspend" || n == "resume" {
return "instance", n, ""
}
if idx := strings.Index(n, "_"); idx > 0 {
verb := n[:idx]
sub := n[idx+1:]
if sub != "" {
return "subresource", verb, sub
}
}
return "action", n, ""
}
func normalizeValueList(values []interface{}) []string {
if len(values) == 0 {
return nil
}
out := make([]string, 0, len(values))
for _, v := range values {
out = append(out, fmt.Sprintf("%v", v))
}
return out
}
func normalizeDefault(value interface{}) interface{} {
if value == nil {
return nil
}
switch t := value.(type) {
case string:
return strings.TrimSpace(t)
default:
return value
}
}