From e9901c6309fbbe4623a4183dcd836c32b8937e3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sun, 5 Jul 2026 07:51:23 +0400 Subject: [PATCH] v5.0.58: retry on 401 from Gateway, bump test profile version --- TEST_STAND/PG/main.tf | 2 +- TEST_STAND/POSTGRES/main.tf | 2 +- TEST_STAND/buck0/main.tf | 2 +- devops/profiles/test/profile.env | 6 +- universal_rebuild/internal/core/client.go | 145 ++++++++++++++-------- 5 files changed, 99 insertions(+), 58 deletions(-) diff --git a/TEST_STAND/PG/main.tf b/TEST_STAND/PG/main.tf index e0432f8..4c77ef2 100644 --- a/TEST_STAND/PG/main.tf +++ b/TEST_STAND/PG/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { nubes = { source = "terra.k8c.ru/nubes-test/nubes" - version = "5.0.57" + version = "5.0.58" } } } diff --git a/TEST_STAND/POSTGRES/main.tf b/TEST_STAND/POSTGRES/main.tf index f01c370..ec1027f 100644 --- a/TEST_STAND/POSTGRES/main.tf +++ b/TEST_STAND/POSTGRES/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { nubes = { source = "terra.k8c.ru/nubes-test/nubes" - version = "5.0.57" + version = "5.0.58" } } } diff --git a/TEST_STAND/buck0/main.tf b/TEST_STAND/buck0/main.tf index 5e0ddd3..deb7672 100644 --- a/TEST_STAND/buck0/main.tf +++ b/TEST_STAND/buck0/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { nubes = { source = "terra.k8c.ru/nubes-test/nubes" - version = "5.0.57" + version = "5.0.58" } } } diff --git a/devops/profiles/test/profile.env b/devops/profiles/test/profile.env index ce1e8ca..478e4ef 100644 --- a/devops/profiles/test/profile.env +++ b/devops/profiles/test/profile.env @@ -3,9 +3,9 @@ NUBES_API_ENDPOINT="https://lk-api-gateway-test.ngcloud.ru/api/v1/svc" TOKEN_FILE="secrets/test.token" # Release versions -RELEASE_VERSION="5.0.57" -PROVIDER_VERSION="5.0.57" -DOCS_VERSION="5.0.57" +RELEASE_VERSION="5.0.58" +PROVIDER_VERSION="5.0.58" +DOCS_VERSION="5.0.58" # Docs generation — ONLY from docs_gen// (never from docs/) DOCS_GEN_DIR="universal_rebuild/docs_gen/test" diff --git a/universal_rebuild/internal/core/client.go b/universal_rebuild/internal/core/client.go index 5589ace..006a9a7 100644 --- a/universal_rebuild/internal/core/client.go +++ b/universal_rebuild/internal/core/client.go @@ -612,38 +612,58 @@ func (c *UniversalClient) FindInstanceByDisplayName(ctx context.Context, service } func (c *UniversalClient) GetInstanceState(ctx context.Context, instanceUid string) (*InstanceStateResponse, error) { - url := c.buildURL(fmt.Sprintf("/instances/%s", instanceUid)) - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) - if err != nil { - return nil, err - } - req.Header.Set("User-Agent", userAgent) - if c.ApiToken != "" { - req.Header.Set("Authorization", "Bearer "+c.ApiToken) - } + const maxRetries = 3 + baseDelay := 2 * time.Second - resp, err := c.HttpClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() + var lastErr error + for attempt := 0; attempt <= maxRetries; attempt++ { + if attempt > 0 { + select { + case <-time.After(baseDelay * time.Duration(1<<(attempt-1))): + case <-ctx.Done(): + return nil, ctx.Err() + } + } - if resp.StatusCode != 200 { - return nil, fmt.Errorf("HTTP статус %d", resp.StatusCode) - } + url := c.buildURL(fmt.Sprintf("/instances/%s", instanceUid)) + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, err + } + req.Header.Set("User-Agent", userAgent) + if c.ApiToken != "" { + req.Header.Set("Authorization", "Bearer "+c.ApiToken) + } - var res struct { - Instance InstanceStateResponse `json:"instance"` - } - if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { - return nil, err - } + resp, err := c.HttpClient.Do(req) + if err != nil { + lastErr = err + continue + } + defer resp.Body.Close() - if err := validateInstanceStatus(&res.Instance); err != nil { - return nil, err - } + if resp.StatusCode == 401 { + lastErr = fmt.Errorf("HTTP статус %d", resp.StatusCode) + continue + } + if resp.StatusCode != 200 { + return nil, fmt.Errorf("HTTP статус %d", resp.StatusCode) + } - return &res.Instance, nil + var res struct { + Instance InstanceStateResponse `json:"instance"` + } + if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { + return nil, err + } + + if err := validateInstanceStatus(&res.Instance); err != nil { + return nil, err + } + + return &res.Instance, nil + } + return nil, fmt.Errorf("GetInstanceState failed after %d retries: %w", maxRetries, lastErr) } func isInstanceDeleted(state *InstanceStateResponse) bool { @@ -659,34 +679,54 @@ func isInstanceDeleted(state *InstanceStateResponse) bool { // GetInstanceStateRaw получает состояние инстанса БЕЗ валидации статуса. // Используется для проверки ref-параметров: нужно читать даже deleted/suspended инстансы. func (c *UniversalClient) GetInstanceStateRaw(ctx context.Context, instanceUid string) (*InstanceStateResponse, error) { - reqURL := c.buildURL(fmt.Sprintf("/instances/%s", instanceUid)) - req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil) - if err != nil { - return nil, err - } - req.Header.Set("User-Agent", userAgent) - if c.ApiToken != "" { - req.Header.Set("Authorization", "Bearer "+c.ApiToken) - } + const maxRetries = 3 + baseDelay := 2 * time.Second - resp, err := c.HttpClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() + var lastErr error + for attempt := 0; attempt <= maxRetries; attempt++ { + if attempt > 0 { + select { + case <-time.After(baseDelay * time.Duration(1<<(attempt-1))): + case <-ctx.Done(): + return nil, ctx.Err() + } + } - if resp.StatusCode != 200 { - return nil, fmt.Errorf("HTTP статус %d", resp.StatusCode) - } + reqURL := c.buildURL(fmt.Sprintf("/instances/%s", instanceUid)) + req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil) + if err != nil { + return nil, err + } + req.Header.Set("User-Agent", userAgent) + if c.ApiToken != "" { + req.Header.Set("Authorization", "Bearer "+c.ApiToken) + } - var res struct { - Instance InstanceStateResponse `json:"instance"` - } - if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { - return nil, err - } + resp, err := c.HttpClient.Do(req) + if err != nil { + lastErr = err + continue + } + defer resp.Body.Close() - return &res.Instance, nil + if resp.StatusCode == 401 { + lastErr = fmt.Errorf("HTTP статус %d", resp.StatusCode) + continue + } + if resp.StatusCode != 200 { + return nil, fmt.Errorf("HTTP статус %d", resp.StatusCode) + } + + var res struct { + Instance InstanceStateResponse `json:"instance"` + } + if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { + return nil, err + } + + return &res.Instance, nil + } + return nil, fmt.Errorf("GetInstanceStateRaw failed after %d retries: %w", maxRetries, lastErr) } // ===== UNIVERSAL OPERATION WAIT (APPEND-ONLY) ===== @@ -932,7 +972,8 @@ func isRetryable(statusCode int) bool { return statusCode == http.StatusTooManyRequests || // 429 statusCode == http.StatusServiceUnavailable || // 503 statusCode == http.StatusBadGateway || // 502 - statusCode == http.StatusGatewayTimeout // 504 + statusCode == http.StatusGatewayTimeout || // 504 + statusCode == http.StatusUnauthorized // 401 — Gateway иногда отбрасывает валидный JWT } func (c *UniversalClient) postIgnoreResponse(ctx context.Context, path string, payload interface{}, returnLocation bool) (string, error) {