package core import ( "context" "encoding/json" "fmt" "strings" ) type ApiOperation struct { SvcOperationId int `json:"svcOperationId"` Operation string `json:"operation"` } type InstanceStateResponse struct { InstanceUid string `json:"instanceUid"` ExplainedStatus string `json:"explainedStatus"` IsDeleted bool `json:"isDeleted"` OperationIsInProgress bool `json:"operationIsInProgress"` OperationIsPending bool `json:"operationIsPending"` AvailableOperations []ApiOperation `json:"availableOperations"` } // FindInstanceByDisplayName finds an instance by display_name for a given serviceId. // Если найдено больше одного non-deleted инстанса — возвращает ошибку. func (c *UniversalClient) FindInstanceByDisplayName(ctx context.Context, serviceId int, displayName string) (*InstanceStateResponse, error) { all, err := c.FindAllInstancesByDisplayName(ctx, serviceId, displayName) if err != nil { return nil, err } if len(all) == 0 { return nil, nil } if len(all) == 1 { return &all[0], nil } // Множественные non-deleted инстансы: пробуем выбрать единственный running/suspended var nonDeleted []InstanceStateResponse for _, inst := range all { if !inst.IsDeleted { nonDeleted = append(nonDeleted, inst) } } if len(nonDeleted) == 0 { return nil, nil } if len(nonDeleted) == 1 { return &nonDeleted[0], nil } // Больше одного — формируем понятное сообщение details := make([]string, 0, len(nonDeleted)) for _, inst := range nonDeleted { details = append(details, fmt.Sprintf(" - %s (статус: %s)", inst.InstanceUid, inst.ExplainedStatus)) } return nil, fmt.Errorf( "обнаружено %d инстансов с именем '%s' (serviceId=%d):\n%s\nНевозможно определить какой adopt-ить. Удалите лишние через ЛК (Управление облаком) или укажите конкретный UUID через 'terraform import'", len(nonDeleted), displayName, serviceId, strings.Join(details, "\n"), ) } func (c *UniversalClient) GetInstanceState(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 } var res struct { Instance InstanceStateResponse `json:"instance"` } if err := json.Unmarshal(respBody, &res); err != nil { return nil, err } if err := validateInstanceStatus(&res.Instance); err != nil { return nil, err } return &res.Instance, nil } // GetInstanceStateRaw получает состояние инстанса БЕЗ валидации статуса. // Используется для проверки ref-параметров: нужно читать даже deleted/suspended инстансы. func (c *UniversalClient) GetInstanceStateRaw(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 } var res struct { Instance InstanceStateResponse `json:"instance"` } if err := json.Unmarshal(respBody, &res); err != nil { return nil, err } return &res.Instance, nil } // FindAllInstancesByDisplayName находит ВСЕ non-deleted инстансы по display_name и serviceId. // Возвращает slice, чтобы caller мог обработать дубликаты. func (c *UniversalClient) FindAllInstancesByDisplayName(ctx context.Context, serviceId int, displayName string) ([]InstanceStateResponse, error) { var found []InstanceStateResponse page := 1 for { path := fmt.Sprintf("/instances?page=%d&size=100", page) respBody, _, err := c.doRequest(ctx, "GET", path, nil) if err != nil { return nil, err } var res struct { Results []struct { InstanceUid string `json:"instanceUid"` DisplayName string `json:"displayName"` ServiceId int `json:"serviceId"` } `json:"results"` } if err := json.Unmarshal(respBody, &res); err != nil { return nil, err } if len(res.Results) == 0 { break } for _, item := range res.Results { if item.ServiceId == serviceId && strings.EqualFold(item.DisplayName, displayName) { state, err := c.GetInstanceStateRaw(ctx, item.InstanceUid) if err != nil { return nil, err } if state != nil && !isInstanceDeleted(state) { found = append(found, *state) } } } page++ if page > 100 { break } } return found, nil } func isInstanceDeleted(state *InstanceStateResponse) bool { if state == nil { return false } if state.IsDeleted { return true } return strings.EqualFold(strings.TrimSpace(state.ExplainedStatus), "deleted") } func validateInstanceStatus(state *InstanceStateResponse) error { if state == nil { return fmt.Errorf("missing instance state") } if state.IsDeleted { return fmt.Errorf("instance %s is deleted", state.InstanceUid) } if state.OperationIsPending || state.OperationIsInProgress { return fmt.Errorf("instance %s not ready: operation pending", state.InstanceUid) } status := strings.ToLower(strings.TrimSpace(state.ExplainedStatus)) if strings.Contains(status, "not created") { return fmt.Errorf("instance %s not created", state.InstanceUid) } if strings.Contains(status, "pending") { return fmt.Errorf("instance %s pending", state.InstanceUid) } if strings.Contains(status, "failed") || strings.Contains(status, "error") { return fmt.Errorf("instance %s failed: %s", state.InstanceUid, state.ExplainedStatus) } return nil }