179 lines
5.7 KiB
Go
179 lines
5.7 KiB
Go
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
|
|
}
|