add: internal package (provider, core, registrykeys)
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,178 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RunInstanceOperationUniversal runs an available operation (modify/suspend/delete/resume) if possible.
|
||||
func (c *UniversalClient) RunInstanceOperationUniversal(ctx context.Context, instanceUid string, action string, params map[int]string) error {
|
||||
state, err := c.GetInstanceState(ctx, instanceUid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var opId int
|
||||
for _, op := range state.AvailableOperations {
|
||||
if strings.EqualFold(op.Operation, action) {
|
||||
opId = op.SvcOperationId
|
||||
break
|
||||
}
|
||||
}
|
||||
if opId == 0 {
|
||||
return fmt.Errorf("action %s not available for instance %s", action, instanceUid)
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"instanceUid": instanceUid,
|
||||
"svcOperationId": opId,
|
||||
"operation": action,
|
||||
}
|
||||
|
||||
opUid, err := c.postIgnoreResponse(ctx, "/instanceOperations", payload, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create %s operation: %w", action, err)
|
||||
}
|
||||
if opUid == "" {
|
||||
return fmt.Errorf("failed to get operation UID for %s", action)
|
||||
}
|
||||
|
||||
for paramId, value := range params {
|
||||
pPayload := genericParamReq{
|
||||
InstanceOperationUid: opUid,
|
||||
SvcOperationCfsParamId: paramId,
|
||||
ParamValue: value,
|
||||
}
|
||||
_, _, err := c.doRequest(ctx, "POST", "/instanceOperationCfsParams", pPayload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set param %d: %w", paramId, err)
|
||||
}
|
||||
}
|
||||
|
||||
_, _, err = c.doRequest(ctx, "POST", fmt.Sprintf("/instanceOperations/%s/run", opUid), map[string]interface{}{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.waitForOperationFinish(ctx, opUid, defaultOperationTimeout)
|
||||
}
|
||||
|
||||
const defaultOperationTimeout = 30 * time.Minute
|
||||
|
||||
type operationStatusResponse struct {
|
||||
InstanceOperation struct {
|
||||
DtFinish *string `json:"dtFinish"`
|
||||
IsSuccessful *bool `json:"isSuccessful"`
|
||||
ErrorLog *string `json:"errorLog"`
|
||||
IsInProgress bool `json:"isInProgress"`
|
||||
IsPending bool `json:"isPending"`
|
||||
} `json:"instanceOperation"`
|
||||
}
|
||||
|
||||
func (c *UniversalClient) waitForOperationFinish(ctx context.Context, opUid string, timeout time.Duration) error {
|
||||
deadline := time.Now().Add(timeout)
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("operation %s cancelled", opUid)
|
||||
case <-ticker.C:
|
||||
if time.Now().After(deadline) {
|
||||
return fmt.Errorf("timeout waiting for operation %s to finish", opUid)
|
||||
}
|
||||
|
||||
respBody, _, err := c.doRequest(ctx, "GET", fmt.Sprintf("/instanceOperations/%s", opUid), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check operation %s status: %w", opUid, err)
|
||||
}
|
||||
|
||||
var status operationStatusResponse
|
||||
if err := json.Unmarshal(respBody, &status); err != nil {
|
||||
return fmt.Errorf("failed to parse operation %s status: %w", opUid, err)
|
||||
}
|
||||
|
||||
if status.InstanceOperation.DtFinish != nil && strings.TrimSpace(*status.InstanceOperation.DtFinish) != "" {
|
||||
if status.InstanceOperation.IsSuccessful != nil && !*status.InstanceOperation.IsSuccessful {
|
||||
if status.InstanceOperation.ErrorLog != nil && strings.TrimSpace(*status.InstanceOperation.ErrorLog) != "" {
|
||||
return fmt.Errorf("operation %s failed: %s", opUid, *status.InstanceOperation.ErrorLog)
|
||||
}
|
||||
return fmt.Errorf("operation %s failed", opUid)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *UniversalClient) postIgnoreResponse(ctx context.Context, path string, payload interface{}, returnLocation bool) (string, error) {
|
||||
respBody, headers, err := c.doRequest(ctx, "POST", path, payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if returnLocation {
|
||||
if loc := headers.Get("Location"); loc != "" {
|
||||
return extractUIDFromLocation(loc), nil
|
||||
}
|
||||
}
|
||||
|
||||
var justId string
|
||||
if err := json.Unmarshal(respBody, &justId); err == nil && justId != "" {
|
||||
return justId, nil
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func extractUIDFromLocation(loc string) string {
|
||||
if loc == "" {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimPrefix(loc, "./")
|
||||
}
|
||||
Reference in New Issue
Block a user