Fix provider review findings
This commit is contained in:
@@ -19,6 +19,7 @@ import (
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -90,6 +91,25 @@ var ctxKeyLogLevel = ctxKeyLogLevelType{}
|
||||
// Go-http-client по умолчанию блокируется фильтром ddos-guard.
|
||||
const userAgent = "Mozilla/5.0"
|
||||
|
||||
func debugLogPath(fileName string) string {
|
||||
if dir := strings.TrimSpace(os.Getenv("NUBES_DEBUG_DIR")); dir != "" {
|
||||
return filepath.Join(dir, fileName)
|
||||
}
|
||||
return filepath.Join(os.TempDir(), fileName)
|
||||
}
|
||||
|
||||
func isHTTPDebugEnabled() bool {
|
||||
return os.Getenv("NUBES_DEBUG_HTTP") == "1"
|
||||
}
|
||||
|
||||
func sanitizeAuthHeader(h http.Header) http.Header {
|
||||
cloned := h.Clone()
|
||||
if cloned.Get("Authorization") != "" {
|
||||
cloned.Set("Authorization", "Bearer [REDACTED]")
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
// CtxWithLogLevel возвращает ctx с переопределённым уровнем логирования.
|
||||
func CtxWithLogLevel(ctx context.Context, level string) context.Context {
|
||||
return context.WithValue(ctx, ctxKeyLogLevel, level)
|
||||
@@ -621,7 +641,10 @@ type InstanceStateResponse struct {
|
||||
// Если найдено больше одного non-deleted инстанса — возвращает ошибку.
|
||||
func (c *UniversalClient) FindInstanceByDisplayName(ctx context.Context, serviceId int, displayName string) (*InstanceStateResponse, error) {
|
||||
debug := func(format string, args ...interface{}) {
|
||||
f, err := os.OpenFile("/tmp/nubes_find_debug.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if !isHTTPDebugEnabled() {
|
||||
return
|
||||
}
|
||||
f, err := os.OpenFile(debugLogPath("nubes_find_debug.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -684,6 +707,8 @@ func (c *UniversalClient) FindInstanceByDisplayName(ctx context.Context, service
|
||||
// Если быстрый поиск не дал результатов — пагинированный fallback
|
||||
if len(found) == 0 {
|
||||
debug("[FIND-DEBUG] search path failed, entering fallback (serviceId=%d, name=%q)", serviceId, displayName)
|
||||
const maxFallbackPages = 100
|
||||
hitPageLimit := false
|
||||
page := 1
|
||||
for {
|
||||
reqURL := c.buildURL(fmt.Sprintf("/instances?page=%d&size=100", page))
|
||||
@@ -753,10 +778,15 @@ func (c *UniversalClient) FindInstanceByDisplayName(ctx context.Context, service
|
||||
}
|
||||
|
||||
page++
|
||||
if page > 100 {
|
||||
if page > maxFallbackPages {
|
||||
hitPageLimit = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if hitPageLimit && len(found) == 0 {
|
||||
return nil, fmt.Errorf("поиск инстанса по display_name достиг лимита пагинации (%d страниц); сузьте фильтр или повторите с более точными параметрами", maxFallbackPages)
|
||||
}
|
||||
}
|
||||
|
||||
if len(found) == 0 {
|
||||
@@ -778,58 +808,14 @@ func (c *UniversalClient) FindInstanceByDisplayName(ctx context.Context, service
|
||||
}
|
||||
|
||||
func (c *UniversalClient) GetInstanceState(ctx context.Context, instanceUid string) (*InstanceStateResponse, error) {
|
||||
const maxRetries = 3
|
||||
baseDelay := 2 * time.Second
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
resp, err := c.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if err := validateInstanceStatus(&res.Instance); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &res.Instance, nil
|
||||
state, err := c.getInstanceStateWithRetry(ctx, instanceUid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, fmt.Errorf("GetInstanceState failed after %d retries: %w", maxRetries, lastErr)
|
||||
if err := validateInstanceStatus(state); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return state, nil
|
||||
}
|
||||
|
||||
func isInstanceDeleted(state *InstanceStateResponse) bool {
|
||||
@@ -845,54 +831,23 @@ func isInstanceDeleted(state *InstanceStateResponse) bool {
|
||||
// GetInstanceStateRaw получает состояние инстанса БЕЗ валидации статуса.
|
||||
// Используется для проверки ref-параметров: нужно читать даже deleted/suspended инстансы.
|
||||
func (c *UniversalClient) GetInstanceStateRaw(ctx context.Context, instanceUid string) (*InstanceStateResponse, error) {
|
||||
const maxRetries = 3
|
||||
baseDelay := 2 * time.Second
|
||||
return c.getInstanceStateWithRetry(ctx, instanceUid)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
resp, err := c.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
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
|
||||
func (c *UniversalClient) getInstanceStateWithRetry(ctx context.Context, instanceUid string) (*InstanceStateResponse, error) {
|
||||
respBody, _, err := c.doRequest(ctx, "GET", fmt.Sprintf("/instances/%s", instanceUid), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, fmt.Errorf("GetInstanceStateRaw failed after %d retries: %w", maxRetries, lastErr)
|
||||
|
||||
var res struct {
|
||||
Instance InstanceStateResponse `json:"instance"`
|
||||
}
|
||||
if err := json.Unmarshal(respBody, &res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &res.Instance, nil
|
||||
}
|
||||
|
||||
// ===== UNIVERSAL OPERATION WAIT (APPEND-ONLY) =====
|
||||
@@ -1096,14 +1051,16 @@ func (c *UniversalClient) doRequest(ctx context.Context, method, path string, pa
|
||||
}
|
||||
|
||||
// DEBUG
|
||||
if os.Getenv("NUBES_DEBUG_HTTP") == "1" {
|
||||
dump, _ := httputil.DumpRequestOut(req, body != nil)
|
||||
if isHTTPDebugEnabled() {
|
||||
reqForDump := req.Clone(req.Context())
|
||||
reqForDump.Header = sanitizeAuthHeader(req.Header)
|
||||
dump, _ := httputil.DumpRequestOut(reqForDump, body != nil)
|
||||
fmt.Fprintf(os.Stderr, "\n>>> REQ %s %s\n%s\n", method, path, dump)
|
||||
}
|
||||
|
||||
// DEBUG в файл
|
||||
if os.Getenv("NUBES_DEBUG_HTTP") == "1" {
|
||||
f, _ := os.OpenFile("/tmp/nubes_debug.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if isHTTPDebugEnabled() {
|
||||
f, _ := os.OpenFile(debugLogPath("nubes_debug.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
||||
if f != nil {
|
||||
fmt.Fprintf(f, ">>> %s %s\n", method, req.URL.String())
|
||||
f.Close()
|
||||
@@ -1112,8 +1069,8 @@ func (c *UniversalClient) doRequest(ctx context.Context, method, path string, pa
|
||||
resp, err := c.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
// Сетевые ошибки — retry (кроме последней попытки)
|
||||
if attempt < maxRetries {
|
||||
// Сетевые ошибки: retry только для идемпотентного GET.
|
||||
if attempt < maxRetries && method == "GET" {
|
||||
continue
|
||||
}
|
||||
return nil, nil, err
|
||||
@@ -1149,8 +1106,7 @@ func isRetryable(statusCode int) bool {
|
||||
return statusCode == http.StatusTooManyRequests || // 429
|
||||
statusCode == http.StatusServiceUnavailable || // 503
|
||||
statusCode == http.StatusBadGateway || // 502
|
||||
statusCode == http.StatusGatewayTimeout || // 504
|
||||
statusCode == http.StatusUnauthorized // 401 — Gateway иногда отбрасывает валидный JWT
|
||||
statusCode == http.StatusGatewayTimeout // 504
|
||||
}
|
||||
|
||||
func (c *UniversalClient) postIgnoreResponse(ctx context.Context, path string, payload interface{}, returnLocation bool) (string, error) {
|
||||
@@ -1197,7 +1153,7 @@ func extractUIDFromLocation(loc string) string {
|
||||
// Однако в state UUID должен сохраняться в том регистре, который написал пользователь
|
||||
// (иначе plan != state → "Provider produced inconsistent result after apply").
|
||||
// Восстановление регистра делается в Create/Read/Update generated resource, см. шаблон
|
||||
// instanceTemplate в tools/gen_v2/generate_resources_v2.go (блоки "Restore user-provided casing").
|
||||
// instanceTemplate в TOOLS/resource-generator/internal/templates/instance.go (блоки "Restore user-provided casing").
|
||||
|
||||
func (c *UniversalClient) resolveRefSvcParamValues(ctx context.Context, opParams []universalCfsParam, params map[int]string) (map[int]string, error) {
|
||||
if len(params) == 0 || len(opParams) == 0 {
|
||||
|
||||
Reference in New Issue
Block a user