Fix VDC flow and FullPipe example

This commit is contained in:
Repinoid
2026-09-21 13:02:25 +03:00
parent 7d446977a5
commit 1ec6a0fedc
8 changed files with 196 additions and 59 deletions
+2 -42
View File
@@ -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')
}
+77
View File
@@ -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 }