Files
tf_provider/internal/core/client.go
T

1278 lines
40 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package core
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
"github.com/hashicorp/terraform-plugin-log/tflog"
)
func debugLog(format string, args ...interface{}) {
f, err := os.OpenFile("/home/naeel/terra/debug_nubes.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return
}
defer f.Close()
fmt.Fprintf(f, time.Now().Format(time.RFC3339)+": "+format+"\n", args...)
}
// UniversalClient handles Nubes API logic
type UniversalClient struct {
HttpClient *http.Client
ApiEndpoint string
ApiToken string
ProviderVersion string
LogLevel string
}
// Request models
type genericInstanceReq struct {
ServiceId int `json:"serviceId"`
DisplayName string `json:"displayName"`
Descr string `json:"descr"`
}
type genericOpReq struct {
InstanceUid string `json:"instanceUid"`
Operation string `json:"operation"`
}
type genericParamReq struct {
InstanceOperationUid string `json:"instanceOperationUid"`
SvcOperationCfsParamId int `json:"svcOperationCfsParamId"`
ParamValue string `json:"paramValue"`
}
// Deprecated: Use CreateGenericInstanceUniversalV5 instead.
// This method is kept for backward compatibility and will be removed in v3.0.
func (c *UniversalClient) CreateGenericInstance(ctx context.Context, serviceId int, displayName string, params map[int]string) (string, error) {
// 1. Create Placeholder (POST /instances)
tflog.Debug(ctx, fmt.Sprintf("Creating generic instance placeholder for service %d", serviceId))
instPayload := genericInstanceReq{
ServiceId: serviceId,
DisplayName: displayName,
Descr: "Created via Terraform Universal Provider",
}
instResp, instHeaders, err := c.doRequest(ctx, "POST", "/instances", instPayload)
if err != nil {
return "", err
}
debugLog("Step 1 Resp Body: %s", string(instResp))
debugLog("Step 1 Resp Headers: %v", instHeaders)
var instanceUid string
// Try Location header first
if loc := instHeaders.Get("Location"); loc != "" {
// Location usually looks like "./3A021B21-..."
instanceUid = strings.TrimPrefix(loc, "./")
}
// If header didn't give us ID, try body
if instanceUid == "" {
var instResult struct {
InstanceUid string `json:"instanceUid"`
}
if err := json.Unmarshal(instResp, &instResult); err == nil && instResult.InstanceUid != "" {
instanceUid = instResult.InstanceUid
} else {
// Fallback: maybe the body IS the ID (string)?
var justId string
if err2 := json.Unmarshal(instResp, &justId); err2 == nil && justId != "" {
instanceUid = justId
}
}
}
if instanceUid == "" {
return "", fmt.Errorf("could not extract instanceUid from response (Header: %s, Body: %s)", instHeaders.Get("Location"), string(instResp))
}
// 2. Init Operation (POST /instanceOperations)
tflog.Debug(ctx, fmt.Sprintf("Initializing create operation for %s", instanceUid))
opPayload := genericOpReq{
InstanceUid: instanceUid,
Operation: "create",
}
opResp, opHeaders, err := c.doRequest(ctx, "POST", "/instanceOperations", opPayload)
if err != nil {
return "", err
}
debugLog("Step 2 Resp Body: %s", string(opResp))
debugLog("Step 2 Resp Headers: %v", opHeaders)
var opUid string
if loc := opHeaders.Get("Location"); loc != "" {
opUid = strings.TrimPrefix(loc, "./")
}
if opUid == "" {
var opResult struct {
InstanceOperationUid string `json:"instanceOperationUid"`
}
if err := json.Unmarshal(opResp, &opResult); err == nil && opResult.InstanceOperationUid != "" {
opUid = opResult.InstanceOperationUid
} else {
var justId string
if err2 := json.Unmarshal(opResp, &justId); err2 == nil && justId != "" {
opUid = justId
}
}
}
if opUid == "" {
return "", fmt.Errorf("failed to extract instanceOperationUid from response (Header: %s, Body: %s)", opHeaders.Get("Location"), string(opResp))
}
// 3. Send Parameters (POST /instanceOperationCfsParams)
for paramId, value := range params {
tflog.Debug(ctx, fmt.Sprintf("Sending param %d = %s", paramId, value))
pPayload := genericParamReq{
InstanceOperationUid: opUid,
SvcOperationCfsParamId: paramId,
ParamValue: value,
}
_, _, err := c.doRequest(ctx, "POST", "/instanceOperationCfsParams", pPayload)
if err != nil {
debugLog("Step 3 Failed for param %d. Error: %v", paramId, err)
return "", fmt.Errorf("failed to set param %d: %w", paramId, err)
}
debugLog("Step 3 OK for param %d", paramId)
}
// 4. Validate (GET /validate-cfs)
tflog.Debug(ctx, fmt.Sprintf("Validating operation %s", opUid))
_, _, err = c.doRequest(ctx, "GET", fmt.Sprintf("/instanceOperations/%s/validate-cfs", opUid), nil)
if err != nil {
return "", fmt.Errorf("validation failed: %w", err)
}
// 5. Execute (POST /run)
tflog.Debug(ctx, fmt.Sprintf("Executing operation %s", opUid))
_, _, err = c.doRequest(ctx, "POST", fmt.Sprintf("/instanceOperations/%s/run", opUid), map[string]interface{}{})
if err != nil {
return "", fmt.Errorf("execution failed: %w", err)
}
// 6. Wait for completion (Simple Polling)
tflog.Info(ctx, "Waiting for resource creation...")
time.Sleep(5 * time.Second)
return instanceUid, nil
}
func (c *UniversalClient) doRequest(ctx context.Context, method, path string, payload interface{}) ([]byte, http.Header, error) {
// User-Agent: браузерный, чтобы пройти DDoS-Guard (см. docs/ops/API_TOKENS.md).
const userAgent = "Mozilla/5.0 (compatible; Terraform-Provider-Nubes)"
const maxRetries = 3
baseDelay := 2 * time.Second
var lastErr error
for attempt := 0; attempt <= maxRetries; attempt++ {
if attempt > 0 {
delay := baseDelay * time.Duration(1<<(attempt-1))
select {
case <-time.After(delay):
case <-ctx.Done():
return nil, nil, ctx.Err()
}
}
var body io.Reader
if payload != nil {
b, err := json.Marshal(payload)
if err != nil {
return nil, nil, err
}
body = bytes.NewBuffer(b)
}
req, err := http.NewRequestWithContext(ctx, method, c.ApiEndpoint+path, body)
if err != nil {
return nil, nil, err
}
req.Close = true
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", userAgent)
if c.ApiToken != "" {
req.Header.Set("Authorization", "Bearer "+c.ApiToken)
}
resp, err := c.HttpClient.Do(req)
if err != nil {
lastErr = err
if attempt < maxRetries {
continue
}
return nil, nil, err
}
respBody, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
lastErr = err
if attempt < maxRetries {
continue
}
return nil, nil, err
}
if resp.StatusCode >= 400 {
err := fmt.Errorf("API error %d: %s", resp.StatusCode, string(respBody))
if attempt < maxRetries && method == "GET" && isRetryable(resp.StatusCode) {
lastErr = err
continue
}
return nil, nil, err
}
return respBody, resp.Header, nil
}
return nil, nil, fmt.Errorf("doRequest failed after %d retries: %w", maxRetries, lastErr)
}
func isRetryable(statusCode int) bool {
return statusCode == http.StatusTooManyRequests ||
statusCode == http.StatusServiceUnavailable ||
statusCode == http.StatusBadGateway ||
statusCode == http.StatusGatewayTimeout
}
// ===== UNIVERSAL FLOW (APPEND-ONLY) =====
type universalOpResponse struct {
InstanceOperation universalOperation `json:"instanceOperation"`
}
type universalOperation struct {
CfsParams []universalCfsParam `json:"cfsParams"`
}
type universalCfsParam struct {
SvcOperationCfsParamId int `json:"svcOperationCfsParamId"`
ParamValue *string `json:"paramValue"`
DefaultValue *string `json:"defaultValue"`
DataType string `json:"dataType"`
}
// Deprecated: Use CreateGenericInstanceUniversalV5 instead.
// This method is kept for backward compatibility and will be removed in v3.0.
// CreateGenericInstanceUniversal реализует универсальный Nubes Flow:
// instances -> instanceOperations -> get cfsParams -> submit params -> validate -> run
func (c *UniversalClient) CreateGenericInstanceUniversal(ctx context.Context, serviceId int, displayName string, params map[int]string) (string, error) {
// 1. Create Placeholder (POST /instances)
instPayload := genericInstanceReq{
ServiceId: serviceId,
DisplayName: displayName,
Descr: "Created via Terraform Universal Provider",
}
instResp, instHeaders, err := c.doRequest(ctx, "POST", "/instances", instPayload)
if err != nil {
return "", err
}
var instanceUid string
if loc := instHeaders.Get("Location"); loc != "" {
instanceUid = strings.TrimPrefix(loc, "./")
}
if instanceUid == "" {
var instResult struct {
InstanceUid string `json:"instanceUid"`
}
if err := json.Unmarshal(instResp, &instResult); err == nil && instResult.InstanceUid != "" {
instanceUid = instResult.InstanceUid
} else {
var justId string
if err2 := json.Unmarshal(instResp, &justId); err2 == nil && justId != "" {
instanceUid = justId
}
}
}
if instanceUid == "" {
return "", fmt.Errorf("could not extract instanceUid from response (Header: %s, Body: %s)", instHeaders.Get("Location"), string(instResp))
}
// 2. Init Operation (POST /instanceOperations)
opPayload := genericOpReq{
InstanceUid: instanceUid,
Operation: "create",
}
opResp, opHeaders, err := c.doRequest(ctx, "POST", "/instanceOperations", opPayload)
if err != nil {
return "", err
}
var opUid string
if loc := opHeaders.Get("Location"); loc != "" {
opUid = strings.TrimPrefix(loc, "./")
}
if opUid == "" {
var opResult struct {
InstanceOperationUid string `json:"instanceOperationUid"`
}
if err := json.Unmarshal(opResp, &opResult); err == nil && opResult.InstanceOperationUid != "" {
opUid = opResult.InstanceOperationUid
} else {
var justId string
if err2 := json.Unmarshal(opResp, &justId); err2 == nil && justId != "" {
opUid = justId
}
}
}
if opUid == "" {
return "", fmt.Errorf("failed to extract instanceOperationUid from response (Header: %s, Body: %s)", opHeaders.Get("Location"), string(opResp))
}
// 3. Get operation details (cfsParams)
opDetailsResp, _, err := c.doRequest(ctx, "GET", fmt.Sprintf("/instanceOperations/%s?fields=cfsParams", opUid), nil)
if err != nil {
return "", fmt.Errorf("failed to get operation details: %w", err)
}
var opDetails universalOpResponse
if err := json.Unmarshal(opDetailsResp, &opDetails); err != nil {
return "", fmt.Errorf("failed to parse operation details: %w", err)
}
// 4. Submit explicit params
sent := make(map[int]bool)
for paramId, value := range params {
pPayload := genericParamReq{
InstanceOperationUid: opUid,
SvcOperationCfsParamId: paramId,
ParamValue: value,
}
_, _, err := c.doRequest(ctx, "POST", "/instanceOperationCfsParams", pPayload)
if err != nil {
return "", fmt.Errorf("failed to set param %d: %w", paramId, err)
}
sent[paramId] = true
}
// 5. Submit defaults for missing params
for _, param := range opDetails.InstanceOperation.CfsParams {
if sent[param.SvcOperationCfsParamId] {
continue
}
val := ""
if param.ParamValue != nil {
val = *param.ParamValue
} else if param.DefaultValue != nil {
val = *param.DefaultValue
}
val = normalizeUniversalValue(val, param.DataType)
pPayload := genericParamReq{
InstanceOperationUid: opUid,
SvcOperationCfsParamId: param.SvcOperationCfsParamId,
ParamValue: val,
}
_, _, err := c.doRequest(ctx, "POST", "/instanceOperationCfsParams", pPayload)
if err != nil {
return "", fmt.Errorf("failed to submit default param %d: %w", param.SvcOperationCfsParamId, err)
}
}
// 6. Validate (GET /validate-cfs)
_, _, err = c.doRequest(ctx, "GET", fmt.Sprintf("/instanceOperations/%s/validate-cfs", opUid), nil)
if err != nil {
return "", fmt.Errorf("validation failed: %w", err)
}
// 7. Execute (POST /run)
_, _, err = c.doRequest(ctx, "POST", fmt.Sprintf("/instanceOperations/%s/run", opUid), map[string]interface{}{})
if err != nil {
return "", fmt.Errorf("execution failed: %w", err)
}
// 8. Wait for completion (Simple Polling)
time.Sleep(5 * time.Second)
return instanceUid, nil
}
func normalizeUniversalValue(val string, dataType string) string {
if val != "" {
return val
}
switch strings.ToLower(dataType) {
case "map", "json":
return "{}"
case "array", "list":
return "[]"
default:
return val
}
}
// ===== UNIVERSAL FLOW V2 (APPEND-ONLY) =====
type universalOpResponseV2 struct {
InstanceOperation universalOperationV2 `json:"instanceOperation"`
}
type universalOperationV2 struct {
CfsParams []universalCfsParamV2 `json:"cfsParams"`
}
type universalCfsParamV2 struct {
SvcOperationCfsParamId int `json:"svcOperationCfsParamId"`
ParamValue *string `json:"paramValue"`
DefaultValue *string `json:"defaultValue"`
DataType string `json:"dataType"`
Name string `json:"name"`
Code string `json:"code"`
SvcOperationCfsParam string `json:"svcOperationCfsParam"`
}
// Deprecated: Use CreateGenericInstanceUniversalV5 instead.
// This method is kept for backward compatibility and will be removed in v3.0.
// CreateGenericInstanceUniversalV2 - расширенная нормализация значений
// (map/json/list/array) с учетом name/code/svcOperationCfsParam
func (c *UniversalClient) CreateGenericInstanceUniversalV2(ctx context.Context, serviceId int, displayName string, params map[int]string) (string, error) {
// 1. Create Placeholder (POST /instances)
instPayload := genericInstanceReq{
ServiceId: serviceId,
DisplayName: displayName,
Descr: "Created via Terraform Universal Provider",
}
instResp, instHeaders, err := c.doRequest(ctx, "POST", "/instances", instPayload)
if err != nil {
return "", err
}
var instanceUid string
if loc := instHeaders.Get("Location"); loc != "" {
instanceUid = strings.TrimPrefix(loc, "./")
}
if instanceUid == "" {
var instResult struct {
InstanceUid string `json:"instanceUid"`
}
if err := json.Unmarshal(instResp, &instResult); err == nil && instResult.InstanceUid != "" {
instanceUid = instResult.InstanceUid
} else {
var justId string
if err2 := json.Unmarshal(instResp, &justId); err2 == nil && justId != "" {
instanceUid = justId
}
}
}
if instanceUid == "" {
return "", fmt.Errorf("could not extract instanceUid from response (Header: %s, Body: %s)", instHeaders.Get("Location"), string(instResp))
}
// 2. Init Operation (POST /instanceOperations)
opPayload := genericOpReq{
InstanceUid: instanceUid,
Operation: "create",
}
opResp, opHeaders, err := c.doRequest(ctx, "POST", "/instanceOperations", opPayload)
if err != nil {
return "", err
}
var opUid string
if loc := opHeaders.Get("Location"); loc != "" {
opUid = strings.TrimPrefix(loc, "./")
}
if opUid == "" {
var opResult struct {
InstanceOperationUid string `json:"instanceOperationUid"`
}
if err := json.Unmarshal(opResp, &opResult); err == nil && opResult.InstanceOperationUid != "" {
opUid = opResult.InstanceOperationUid
} else {
var justId string
if err2 := json.Unmarshal(opResp, &justId); err2 == nil && justId != "" {
opUid = justId
}
}
}
if opUid == "" {
return "", fmt.Errorf("failed to extract instanceOperationUid from response (Header: %s, Body: %s)", opHeaders.Get("Location"), string(opResp))
}
// 3. Get operation details (cfsParams)
opDetailsResp, _, err := c.doRequest(ctx, "GET", fmt.Sprintf("/instanceOperations/%s?fields=cfsParams", opUid), nil)
if err != nil {
return "", fmt.Errorf("failed to get operation details: %w", err)
}
var opDetails universalOpResponseV2
if err := json.Unmarshal(opDetailsResp, &opDetails); err != nil {
return "", fmt.Errorf("failed to parse operation details: %w", err)
}
// 4. Submit explicit params
sent := make(map[int]bool)
for paramId, value := range params {
pPayload := genericParamReq{
InstanceOperationUid: opUid,
SvcOperationCfsParamId: paramId,
ParamValue: value,
}
_, _, err := c.doRequest(ctx, "POST", "/instanceOperationCfsParams", pPayload)
if err != nil {
return "", fmt.Errorf("failed to set param %d: %w", paramId, err)
}
sent[paramId] = true
}
// 5. Submit defaults for missing params
for _, param := range opDetails.InstanceOperation.CfsParams {
if sent[param.SvcOperationCfsParamId] {
continue
}
val := ""
if param.ParamValue != nil {
val = *param.ParamValue
} else if param.DefaultValue != nil {
val = *param.DefaultValue
}
val = normalizeUniversalValueV2(val, param)
pPayload := genericParamReq{
InstanceOperationUid: opUid,
SvcOperationCfsParamId: param.SvcOperationCfsParamId,
ParamValue: val,
}
_, _, err := c.doRequest(ctx, "POST", "/instanceOperationCfsParams", pPayload)
if err != nil {
return "", fmt.Errorf("failed to submit default param %d: %w", param.SvcOperationCfsParamId, err)
}
}
// 6. Validate (GET /validate-cfs)
_, _, err = c.doRequest(ctx, "GET", fmt.Sprintf("/instanceOperations/%s/validate-cfs", opUid), nil)
if err != nil {
return "", fmt.Errorf("validation failed: %w", err)
}
// 7. Execute (POST /run)
_, _, err = c.doRequest(ctx, "POST", fmt.Sprintf("/instanceOperations/%s/run", opUid), map[string]interface{}{})
if err != nil {
return "", fmt.Errorf("execution failed: %w", err)
}
// 8. Wait for completion (Simple Polling)
time.Sleep(5 * time.Second)
return instanceUid, nil
}
func normalizeUniversalValueV2(val string, param universalCfsParamV2) string {
if val != "" {
return val
}
dataType := strings.ToLower(param.DataType)
nameHint := strings.ToLower(param.Name + " " + param.Code + " " + param.SvcOperationCfsParam)
if dataType == "map" || dataType == "json" || strings.Contains(nameHint, "map") || strings.Contains(nameHint, "json") {
return "{}"
}
if dataType == "array" || dataType == "list" || strings.Contains(nameHint, "array") || strings.Contains(nameHint, "list") {
return "[]"
}
return val
}
// ===== UNIVERSAL FLOW V3 (APPEND-ONLY) =====
type universalOpResponseV3 struct {
InstanceOperation universalOperationV3 `json:"instanceOperation"`
}
type universalOperationV3 struct {
CfsParams []universalCfsParamV3 `json:"cfsParams"`
}
type universalCfsParamV3 struct {
SvcOperationCfsParamId int `json:"svcOperationCfsParamId"`
ParamValue *string `json:"paramValue"`
DefaultValue *string `json:"defaultValue"`
DataType string `json:"dataType"`
Name string `json:"name"`
Code string `json:"code"`
Label string `json:"label"`
SvcOperationCfsParam string `json:"svcOperationCfsParam"`
}
// Deprecated: Use CreateGenericInstanceUniversalV5 instead.
// This method is kept for backward compatibility and will be removed in v3.0.
// CreateGenericInstanceUniversalV3 - расширенная нормализация (label/name/code)
func (c *UniversalClient) CreateGenericInstanceUniversalV3(ctx context.Context, serviceId int, displayName string, params map[int]string) (string, error) {
// 1. Create Placeholder (POST /instances)
instPayload := genericInstanceReq{
ServiceId: serviceId,
DisplayName: displayName,
Descr: "Created via Terraform Universal Provider",
}
instResp, instHeaders, err := c.doRequest(ctx, "POST", "/instances", instPayload)
if err != nil {
return "", err
}
var instanceUid string
if loc := instHeaders.Get("Location"); loc != "" {
instanceUid = strings.TrimPrefix(loc, "./")
}
if instanceUid == "" {
var instResult struct {
InstanceUid string `json:"instanceUid"`
}
if err := json.Unmarshal(instResp, &instResult); err == nil && instResult.InstanceUid != "" {
instanceUid = instResult.InstanceUid
} else {
var justId string
if err2 := json.Unmarshal(instResp, &justId); err2 == nil && justId != "" {
instanceUid = justId
}
}
}
if instanceUid == "" {
return "", fmt.Errorf("could not extract instanceUid from response (Header: %s, Body: %s)", instHeaders.Get("Location"), string(instResp))
}
// 2. Init Operation (POST /instanceOperations)
opPayload := genericOpReq{
InstanceUid: instanceUid,
Operation: "create",
}
opResp, opHeaders, err := c.doRequest(ctx, "POST", "/instanceOperations", opPayload)
if err != nil {
return "", err
}
var opUid string
if loc := opHeaders.Get("Location"); loc != "" {
opUid = strings.TrimPrefix(loc, "./")
}
if opUid == "" {
var opResult struct {
InstanceOperationUid string `json:"instanceOperationUid"`
}
if err := json.Unmarshal(opResp, &opResult); err == nil && opResult.InstanceOperationUid != "" {
opUid = opResult.InstanceOperationUid
} else {
var justId string
if err2 := json.Unmarshal(opResp, &justId); err2 == nil && justId != "" {
opUid = justId
}
}
}
if opUid == "" {
return "", fmt.Errorf("failed to extract instanceOperationUid from response (Header: %s, Body: %s)", opHeaders.Get("Location"), string(opResp))
}
// 3. Get operation details (cfsParams)
opDetailsResp, _, err := c.doRequest(ctx, "GET", fmt.Sprintf("/instanceOperations/%s?fields=cfsParams", opUid), nil)
if err != nil {
return "", fmt.Errorf("failed to get operation details: %w", err)
}
var opDetails universalOpResponseV3
if err := json.Unmarshal(opDetailsResp, &opDetails); err != nil {
return "", fmt.Errorf("failed to parse operation details: %w", err)
}
// Debug log of cfsParams (for type mismatch analysis)
for _, p := range opDetails.InstanceOperation.CfsParams {
debugLog("CFS Param: id=%d dataType=%s name=%s code=%s label=%s svcParam=%s", p.SvcOperationCfsParamId, p.DataType, p.Name, p.Code, p.Label, p.SvcOperationCfsParam)
}
// 4. Submit explicit params
sent := make(map[int]bool)
for paramId, value := range params {
pPayload := genericParamReq{
InstanceOperationUid: opUid,
SvcOperationCfsParamId: paramId,
ParamValue: value,
}
_, _, err := c.doRequest(ctx, "POST", "/instanceOperationCfsParams", pPayload)
if err != nil {
return "", fmt.Errorf("failed to set param %d: %w", paramId, err)
}
sent[paramId] = true
}
// 5. Submit defaults for missing params
for _, param := range opDetails.InstanceOperation.CfsParams {
if sent[param.SvcOperationCfsParamId] {
continue
}
val := ""
if param.ParamValue != nil {
val = *param.ParamValue
} else if param.DefaultValue != nil {
val = *param.DefaultValue
}
val = normalizeUniversalValueV3(val, param)
pPayload := genericParamReq{
InstanceOperationUid: opUid,
SvcOperationCfsParamId: param.SvcOperationCfsParamId,
ParamValue: val,
}
_, _, err := c.doRequest(ctx, "POST", "/instanceOperationCfsParams", pPayload)
if err != nil {
return "", fmt.Errorf("failed to submit default param %d: %w", param.SvcOperationCfsParamId, err)
}
}
// 6. Validate (GET /validate-cfs)
_, _, err = c.doRequest(ctx, "GET", fmt.Sprintf("/instanceOperations/%s/validate-cfs", opUid), nil)
if err != nil {
return "", fmt.Errorf("validation failed: %w", err)
}
// 7. Execute (POST /run)
_, _, err = c.doRequest(ctx, "POST", fmt.Sprintf("/instanceOperations/%s/run", opUid), map[string]interface{}{})
if err != nil {
return "", fmt.Errorf("execution failed: %w", err)
}
// 8. Wait for completion (Simple Polling)
time.Sleep(5 * time.Second)
return instanceUid, nil
}
func normalizeUniversalValueV3(val string, param universalCfsParamV3) string {
if val != "" {
return val
}
dataType := strings.ToLower(param.DataType)
nameHint := strings.ToLower(param.Name + " " + param.Code + " " + param.Label + " " + param.SvcOperationCfsParam)
if dataType == "map" || dataType == "json" || strings.Contains(nameHint, "map") || strings.Contains(nameHint, "json") {
return "{}"
}
if dataType == "array" || dataType == "list" || strings.Contains(nameHint, "array") || strings.Contains(nameHint, "list") {
return "[]"
}
return val
}
// ===== UNIVERSAL FLOW V4 (APPEND-ONLY) =====
// Deprecated: Use CreateGenericInstanceUniversalV5 instead.
// This method is kept for backward compatibility and will be removed in v3.0.
// CreateGenericInstanceUniversalV4 - нормализация с trim + расширенный разбор dataType
func (c *UniversalClient) CreateGenericInstanceUniversalV4(ctx context.Context, serviceId int, displayName string, params map[int]string) (string, error) {
// 1. Create Placeholder (POST /instances)
instPayload := genericInstanceReq{
ServiceId: serviceId,
DisplayName: displayName,
Descr: "Created via Terraform Universal Provider",
}
instResp, instHeaders, err := c.doRequest(ctx, "POST", "/instances", instPayload)
if err != nil {
return "", err
}
var instanceUid string
if loc := instHeaders.Get("Location"); loc != "" {
instanceUid = strings.TrimPrefix(loc, "./")
}
if instanceUid == "" {
var instResult struct {
InstanceUid string `json:"instanceUid"`
}
if err := json.Unmarshal(instResp, &instResult); err == nil && instResult.InstanceUid != "" {
instanceUid = instResult.InstanceUid
} else {
var justId string
if err2 := json.Unmarshal(instResp, &justId); err2 == nil && justId != "" {
instanceUid = justId
}
}
}
if instanceUid == "" {
return "", fmt.Errorf("could not extract instanceUid from response (Header: %s, Body: %s)", instHeaders.Get("Location"), string(instResp))
}
// 2. Init Operation (POST /instanceOperations)
opPayload := genericOpReq{
InstanceUid: instanceUid,
Operation: "create",
}
opResp, opHeaders, err := c.doRequest(ctx, "POST", "/instanceOperations", opPayload)
if err != nil {
return "", err
}
var opUid string
if loc := opHeaders.Get("Location"); loc != "" {
opUid = strings.TrimPrefix(loc, "./")
}
if opUid == "" {
var opResult struct {
InstanceOperationUid string `json:"instanceOperationUid"`
}
if err := json.Unmarshal(opResp, &opResult); err == nil && opResult.InstanceOperationUid != "" {
opUid = opResult.InstanceOperationUid
} else {
var justId string
if err2 := json.Unmarshal(opResp, &justId); err2 == nil && justId != "" {
opUid = justId
}
}
}
if opUid == "" {
return "", fmt.Errorf("failed to extract instanceOperationUid from response (Header: %s, Body: %s)", opHeaders.Get("Location"), string(opResp))
}
// 3. Get operation details (cfsParams)
opDetailsResp, _, err := c.doRequest(ctx, "GET", fmt.Sprintf("/instanceOperations/%s?fields=cfsParams", opUid), nil)
if err != nil {
return "", fmt.Errorf("failed to get operation details: %w", err)
}
var opDetails universalOpResponseV3
if err := json.Unmarshal(opDetailsResp, &opDetails); err != nil {
return "", fmt.Errorf("failed to parse operation details: %w", err)
}
// Debug log of cfsParams (for type mismatch analysis)
for _, p := range opDetails.InstanceOperation.CfsParams {
debugLog("CFS Param: id=%d dataType=%s name=%s code=%s label=%s svcParam=%s", p.SvcOperationCfsParamId, p.DataType, p.Name, p.Code, p.Label, p.SvcOperationCfsParam)
}
// 4. Submit explicit params
sent := make(map[int]bool)
for paramId, value := range params {
pPayload := genericParamReq{
InstanceOperationUid: opUid,
SvcOperationCfsParamId: paramId,
ParamValue: value,
}
_, _, err := c.doRequest(ctx, "POST", "/instanceOperationCfsParams", pPayload)
if err != nil {
return "", fmt.Errorf("failed to set param %d: %w", paramId, err)
}
sent[paramId] = true
}
// 5. Submit defaults for missing params
for _, param := range opDetails.InstanceOperation.CfsParams {
if sent[param.SvcOperationCfsParamId] {
continue
}
val := ""
if param.ParamValue != nil {
val = *param.ParamValue
} else if param.DefaultValue != nil {
val = *param.DefaultValue
}
val = normalizeUniversalValueV4(val, param)
pPayload := genericParamReq{
InstanceOperationUid: opUid,
SvcOperationCfsParamId: param.SvcOperationCfsParamId,
ParamValue: val,
}
_, _, err := c.doRequest(ctx, "POST", "/instanceOperationCfsParams", pPayload)
if err != nil {
return "", fmt.Errorf("failed to submit default param %d: %w", param.SvcOperationCfsParamId, err)
}
}
// 6. Validate (GET /validate-cfs)
_, _, err = c.doRequest(ctx, "GET", fmt.Sprintf("/instanceOperations/%s/validate-cfs", opUid), nil)
if err != nil {
return "", fmt.Errorf("validation failed: %w", err)
}
// 7. Execute (POST /run)
_, _, err = c.doRequest(ctx, "POST", fmt.Sprintf("/instanceOperations/%s/run", opUid), map[string]interface{}{})
if err != nil {
return "", fmt.Errorf("execution failed: %w", err)
}
// 8. Wait for completion (Simple Polling)
time.Sleep(5 * time.Second)
return instanceUid, nil
}
func normalizeUniversalValueV4(val string, param universalCfsParamV3) string {
trimmed := strings.TrimSpace(val)
if trimmed != "" {
return trimmed
}
dataType := strings.ToLower(param.DataType)
nameHint := strings.ToLower(param.Name + " " + param.Code + " " + param.Label + " " + param.SvcOperationCfsParam)
if strings.Contains(dataType, "array") || strings.Contains(nameHint, "array") || strings.Contains(nameHint, "list") {
return "[]"
}
if strings.Contains(dataType, "map") || strings.Contains(dataType, "json") || strings.Contains(nameHint, "map") || strings.Contains(nameHint, "json") {
return "{}"
}
return trimmed
}
// ===== UNIVERSAL FLOW V5 (APPEND-ONLY) =====
// CreateGenericInstanceUniversalV5 - добавляет логирование отправляемых значений
func (c *UniversalClient) CreateGenericInstanceUniversalV5(ctx context.Context, serviceId int, displayName string, params map[int]string) (string, error) {
// 1. Create Placeholder (POST /instances)
instPayload := genericInstanceReq{
ServiceId: serviceId,
DisplayName: displayName,
Descr: "Created via Terraform Universal Provider",
}
instResp, instHeaders, err := c.doRequest(ctx, "POST", "/instances", instPayload)
if err != nil {
return "", err
}
var instanceUid string
if loc := instHeaders.Get("Location"); loc != "" {
instanceUid = strings.TrimPrefix(loc, "./")
}
if instanceUid == "" {
var instResult struct {
InstanceUid string `json:"instanceUid"`
}
if err := json.Unmarshal(instResp, &instResult); err == nil && instResult.InstanceUid != "" {
instanceUid = instResult.InstanceUid
} else {
var justId string
if err2 := json.Unmarshal(instResp, &justId); err2 == nil && justId != "" {
instanceUid = justId
}
}
}
if instanceUid == "" {
return "", fmt.Errorf("could not extract instanceUid from response (Header: %s, Body: %s)", instHeaders.Get("Location"), string(instResp))
}
// 2. Init Operation (POST /instanceOperations)
opPayload := genericOpReq{
InstanceUid: instanceUid,
Operation: "create",
}
opResp, opHeaders, err := c.doRequest(ctx, "POST", "/instanceOperations", opPayload)
if err != nil {
return "", err
}
var opUid string
if loc := opHeaders.Get("Location"); loc != "" {
opUid = strings.TrimPrefix(loc, "./")
}
if opUid == "" {
var opResult struct {
InstanceOperationUid string `json:"instanceOperationUid"`
}
if err := json.Unmarshal(opResp, &opResult); err == nil && opResult.InstanceOperationUid != "" {
opUid = opResult.InstanceOperationUid
} else {
var justId string
if err2 := json.Unmarshal(opResp, &justId); err2 == nil && justId != "" {
opUid = justId
}
}
}
if opUid == "" {
return "", fmt.Errorf("failed to extract instanceOperationUid from response (Header: %s, Body: %s)", opHeaders.Get("Location"), string(opResp))
}
// 3. Get operation details (cfsParams)
opDetailsResp, _, err := c.doRequest(ctx, "GET", fmt.Sprintf("/instanceOperations/%s?fields=cfsParams", opUid), nil)
if err != nil {
return "", fmt.Errorf("failed to get operation details: %w", err)
}
var opDetails universalOpResponseV3
if err := json.Unmarshal(opDetailsResp, &opDetails); err != nil {
return "", fmt.Errorf("failed to parse operation details: %w", err)
}
// Debug log of cfsParams (for type mismatch analysis)
for _, p := range opDetails.InstanceOperation.CfsParams {
debugLog("CFS Param: id=%d dataType=%s name=%s code=%s label=%s svcParam=%s", p.SvcOperationCfsParamId, p.DataType, p.Name, p.Code, p.Label, p.SvcOperationCfsParam)
}
// 4. Submit explicit params
sent := make(map[int]bool)
for paramId, value := range params {
pPayload := genericParamReq{
InstanceOperationUid: opUid,
SvcOperationCfsParamId: paramId,
ParamValue: value,
}
_, _, err := c.doRequest(ctx, "POST", "/instanceOperationCfsParams", pPayload)
if err != nil {
return "", fmt.Errorf("failed to set param %d: %w", paramId, err)
}
sent[paramId] = true
}
// 5. Submit defaults for missing params
for _, param := range opDetails.InstanceOperation.CfsParams {
if sent[param.SvcOperationCfsParamId] {
continue
}
val := ""
if param.ParamValue != nil {
val = *param.ParamValue
} else if param.DefaultValue != nil {
val = *param.DefaultValue
}
normalized := normalizeUniversalValueV5(val, param)
debugLog("CFS Send: id=%d raw=%q normalized=%q", param.SvcOperationCfsParamId, val, normalized)
pPayload := genericParamReq{
InstanceOperationUid: opUid,
SvcOperationCfsParamId: param.SvcOperationCfsParamId,
ParamValue: normalized,
}
_, _, err := c.doRequest(ctx, "POST", "/instanceOperationCfsParams", pPayload)
if err != nil {
return "", fmt.Errorf("failed to submit default param %d: %w", param.SvcOperationCfsParamId, err)
}
}
// 6. Validate (GET /validate-cfs)
_, _, err = c.doRequest(ctx, "GET", fmt.Sprintf("/instanceOperations/%s/validate-cfs", opUid), nil)
if err != nil {
return "", fmt.Errorf("validation failed: %w", err)
}
// 7. Execute (POST /run)
_, _, err = c.doRequest(ctx, "POST", fmt.Sprintf("/instanceOperations/%s/run", opUid), map[string]interface{}{})
if err != nil {
return "", fmt.Errorf("execution failed: %w", err)
}
// 8. Wait for completion (Simple Polling)
time.Sleep(5 * time.Second)
return instanceUid, nil
}
func normalizeUniversalValueV5(val string, param universalCfsParamV3) string {
trimmed := strings.TrimSpace(val)
if strings.EqualFold(trimmed, "null") {
trimmed = ""
}
if trimmed != "" {
return trimmed
}
dataType := strings.ToLower(param.DataType)
nameHint := strings.ToLower(param.Name + " " + param.Code + " " + param.Label + " " + param.SvcOperationCfsParam)
if strings.Contains(dataType, "array") || strings.Contains(nameHint, "array") || strings.Contains(nameHint, "list") {
return "[]"
}
if strings.Contains(dataType, "map") || strings.Contains(dataType, "json") || strings.Contains(nameHint, "map") || strings.Contains(nameHint, "json") {
return "{}"
}
return trimmed
}
// ===== UNIVERSAL FLOW V6 (APPEND-ONLY) =====
// CreateGenericInstanceUniversalV6 - обработка raw="\"\"" для map/json/array/list
func (c *UniversalClient) CreateGenericInstanceUniversalV6(ctx context.Context, serviceId int, displayName string, params map[int]string) (string, error) {
// 1. Create Placeholder (POST /instances)
instPayload := genericInstanceReq{
ServiceId: serviceId,
DisplayName: displayName,
Descr: "Created via Terraform Universal Provider",
}
instResp, instHeaders, err := c.doRequest(ctx, "POST", "/instances", instPayload)
if err != nil {
return "", err
}
var instanceUid string
if loc := instHeaders.Get("Location"); loc != "" {
instanceUid = strings.TrimPrefix(loc, "./")
}
if instanceUid == "" {
var instResult struct {
InstanceUid string `json:"instanceUid"`
}
if err := json.Unmarshal(instResp, &instResult); err == nil && instResult.InstanceUid != "" {
instanceUid = instResult.InstanceUid
} else {
var justId string
if err2 := json.Unmarshal(instResp, &justId); err2 == nil && justId != "" {
instanceUid = justId
}
}
}
if instanceUid == "" {
return "", fmt.Errorf("could not extract instanceUid from response (Header: %s, Body: %s)", instHeaders.Get("Location"), string(instResp))
}
// 2. Init Operation (POST /instanceOperations)
opPayload := genericOpReq{
InstanceUid: instanceUid,
Operation: "create",
}
opResp, opHeaders, err := c.doRequest(ctx, "POST", "/instanceOperations", opPayload)
if err != nil {
return "", err
}
var opUid string
if loc := opHeaders.Get("Location"); loc != "" {
opUid = strings.TrimPrefix(loc, "./")
}
if opUid == "" {
var opResult struct {
InstanceOperationUid string `json:"instanceOperationUid"`
}
if err := json.Unmarshal(opResp, &opResult); err == nil && opResult.InstanceOperationUid != "" {
opUid = opResult.InstanceOperationUid
} else {
var justId string
if err2 := json.Unmarshal(opResp, &justId); err2 == nil && justId != "" {
opUid = justId
}
}
}
if opUid == "" {
return "", fmt.Errorf("failed to extract instanceOperationUid from response (Header: %s, Body: %s)", opHeaders.Get("Location"), string(opResp))
}
// 3. Get operation details (cfsParams)
opDetailsResp, _, err := c.doRequest(ctx, "GET", fmt.Sprintf("/instanceOperations/%s?fields=cfsParams", opUid), nil)
if err != nil {
return "", fmt.Errorf("failed to get operation details: %w", err)
}
var opDetails universalOpResponseV3
if err := json.Unmarshal(opDetailsResp, &opDetails); err != nil {
return "", fmt.Errorf("failed to parse operation details: %w", err)
}
for _, p := range opDetails.InstanceOperation.CfsParams {
debugLog("CFS Param: id=%d dataType=%s name=%s code=%s label=%s svcParam=%s", p.SvcOperationCfsParamId, p.DataType, p.Name, p.Code, p.Label, p.SvcOperationCfsParam)
}
// 4. Submit explicit params
sent := make(map[int]bool)
for paramId, value := range params {
pPayload := genericParamReq{
InstanceOperationUid: opUid,
SvcOperationCfsParamId: paramId,
ParamValue: value,
}
_, _, err := c.doRequest(ctx, "POST", "/instanceOperationCfsParams", pPayload)
if err != nil {
return "", fmt.Errorf("failed to set param %d: %w", paramId, err)
}
sent[paramId] = true
}
// 5. Submit defaults for missing params
for _, param := range opDetails.InstanceOperation.CfsParams {
if sent[param.SvcOperationCfsParamId] {
continue
}
val := ""
if param.ParamValue != nil {
val = *param.ParamValue
} else if param.DefaultValue != nil {
val = *param.DefaultValue
}
normalized := normalizeUniversalValueV6(val, param)
debugLog("CFS Send: id=%d raw=%q normalized=%q", param.SvcOperationCfsParamId, val, normalized)
pPayload := genericParamReq{
InstanceOperationUid: opUid,
SvcOperationCfsParamId: param.SvcOperationCfsParamId,
ParamValue: normalized,
}
_, _, err := c.doRequest(ctx, "POST", "/instanceOperationCfsParams", pPayload)
if err != nil {
return "", fmt.Errorf("failed to submit default param %d: %w", param.SvcOperationCfsParamId, err)
}
}
// 6. Validate (GET /validate-cfs)
_, _, err = c.doRequest(ctx, "GET", fmt.Sprintf("/instanceOperations/%s/validate-cfs", opUid), nil)
if err != nil {
return "", fmt.Errorf("validation failed: %w", err)
}
// 7. Execute (POST /run)
_, _, err = c.doRequest(ctx, "POST", fmt.Sprintf("/instanceOperations/%s/run", opUid), map[string]interface{}{})
if err != nil {
return "", fmt.Errorf("execution failed: %w", err)
}
// 8. Wait for completion (Simple Polling)
time.Sleep(5 * time.Second)
return instanceUid, nil
}
func normalizeUniversalValueV6(val string, param universalCfsParamV3) string {
trimmed := strings.TrimSpace(val)
if strings.EqualFold(trimmed, "null") {
trimmed = ""
}
// Handle JSON-encoded empty string: ""
if trimmed == "\"\"" {
trimmed = ""
}
if trimmed != "" {
return trimmed
}
dataType := strings.ToLower(param.DataType)
nameHint := strings.ToLower(param.Name + " " + param.Code + " " + param.Label + " " + param.SvcOperationCfsParam)
if strings.Contains(dataType, "array") || strings.Contains(nameHint, "array") || strings.Contains(nameHint, "list") {
return "[]"
}
if strings.Contains(dataType, "map") || strings.Contains(dataType, "json") || strings.Contains(nameHint, "map") || strings.Contains(nameHint, "json") {
return "{}"
}
return trimmed
}