package provider import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" "github.com/hashicorp/terraform-plugin-log/tflog" ) // Helper struct for parameters type InstanceParam struct { SvcOperationCfsParamId int ParamValue string } type InstanceOperationRequest struct { Action string `json:"action"` Params interface{} `json:"params"` } // Implement methods for NubesClient defined in provider.go func (c *NubesClient) GetOperationId(ctx context.Context, serviceId int, opName string) (int, error) { // Logic to fetch operation ID if needed. // Based on HAR, we might not strictly need this if we pass "operation": "create" // But let's assume we return a dummy or look it up. // For now, return 0 as placeholder or implement lookup if API supports it. return 0, nil } func (c *NubesClient) CreateInstance(ctx context.Context, displayName string, serviceId int, svcOperationId int, params []InstanceParam) (string, string, error) { // 1. Create Instance Placeholder // Payload based on pg_admin.har: {"serviceId":96,"displayName":"...","descr":""} payload := map[string]interface{}{ "serviceId": serviceId, "displayName": displayName, "descr": "", } instanceUid, err := c.postInstance(ctx, payload) if err != nil { return "", "", fmt.Errorf("failed to create instance placeholder: %w", err) } tflog.Info(ctx, fmt.Sprintf("Created Instance Placeholder: %s", instanceUid)) // 2. Create Operation opPayload := map[string]interface{}{ "instanceUid": instanceUid, "operation": "create", } // If svcOperationId is valid (>0), maybe we use it? HAR just said "operation":"create". // We'll stick to "operation":"create". opUid, err := c.postIgnoreResponse(ctx, "/instanceOperations", opPayload, true) if err != nil { return "", "", fmt.Errorf("failed to create operation: %w", err) } tflog.Info(ctx, fmt.Sprintf("Created Operation: %s", opUid)) // 3. Get operation details with parameters opPath := fmt.Sprintf("/instanceOperations/%s?fields=cfsParams", opUid) var opDetails GetOperationResponse if err := c.get(ctx, opPath, &opDetails); err != nil { return "", "", fmt.Errorf("failed to get operation details: %w", err) } // 3a. Submit parameters // Strategy: // 1. Send all explicitly provided 'params' (from Terraform config/resource) // 2. Iterate server-provided defaults (opDetails) for anything we missed and send defaults sentParams := make(map[int]bool) // Phase 1: Send explicit overrides for _, p := range params { valToSend := p.ParamValue // Simple normalization for empty values if needed if valToSend == "" { // Some fields might reject empty string? For now send as is // or apply the map/list fix if we knew the type. // But for explicit params, we assume caller knows best. } paramPayload := map[string]interface{}{ "instanceOperationUid": opUid, "svcOperationCfsParamId": p.SvcOperationCfsParamId, "paramValue": valToSend, } _, err := c.postIgnoreResponse(ctx, "/instanceOperationCfsParams", paramPayload, false) if err != nil { return "", "", fmt.Errorf("failed to submit explicit param %d: %w", p.SvcOperationCfsParamId, err) } sentParams[p.SvcOperationCfsParamId] = true } // Phase 2: Fill in defaults from Server Metadata (if not already sent) for _, param := range opDetails.InstanceOperation.CfsParams { if _, sent := sentParams[param.SvcOperationCfsParamId]; sent { continue // Already sent in Phase 1 } valToSend := "" if param.ParamValue != nil { valToSend = *param.ParamValue } else if param.DefaultValue != nil { valToSend = *param.DefaultValue } // Fix specific data type formatting (from tubulus example) if valToSend == "" { if param.DataType == "map" || param.DataType == "json" { valToSend = "{}" } else if param.DataType == "array" || param.DataType == "list" { valToSend = "[]" } } paramPayload := map[string]interface{}{ "instanceOperationUid": opUid, "svcOperationCfsParamId": param.SvcOperationCfsParamId, "paramValue": valToSend, } _, err := c.postIgnoreResponse(ctx, "/instanceOperationCfsParams", paramPayload, false) if err != nil { return "", "", fmt.Errorf("failed to submit default param %d: %w", param.SvcOperationCfsParamId, err) } } // 4. Validate (Optional, seen in S3 HAR) validateUrl := fmt.Sprintf("/instanceOperations/%s/validate-cfs", opUid) tflog.Info(ctx, fmt.Sprintf("Validating operation: %s", validateUrl)) _ = c.get(ctx, validateUrl, nil) // 5. Run Operation // HAR: POST .../instanceOperations/{ids}/run runUrl := fmt.Sprintf("/instanceOperations/%s/run", opUid) tflog.Info(ctx, fmt.Sprintf("Running operation: %s", runUrl)) _, err = c.postIgnoreResponse(ctx, runUrl, map[string]interface{}{}, false) if err != nil { return "", "", fmt.Errorf("failed to run operation: %w", err) } return instanceUid, opUid, nil } // RunInstanceOperation is a helper to just fill params and run an existing OP UID func (c *NubesClient) RunInstanceOperation(ctx context.Context, opUid string, params []InstanceParam) (string, string, error) { // Add params for _, param := range params { valToSend := param.ParamValue // Simple normalization if valToSend == "" { valToSend = "pass" // Default fallback if needed, or empty } paramPayload := map[string]interface{}{ "instanceOperationUid": opUid, "svcOperationCfsParamId": param.SvcOperationCfsParamId, "paramValue": valToSend, } _, err := c.postIgnoreResponse(ctx, "/instanceOperationCfsParams", paramPayload, false) if err != nil { return "", "", fmt.Errorf("failed to add param %d: %w", param.SvcOperationCfsParamId, err) } } // Validate validateUrl := fmt.Sprintf("/instanceOperations/%s/validate-cfs", opUid) _ = c.get(ctx, validateUrl, nil) // Run runUrl := fmt.Sprintf("/instanceOperations/%s/run", opUid) _, err := c.postIgnoreResponse(ctx, runUrl, map[string]interface{}{}, false) if err != nil { return "", "", fmt.Errorf("failed to run operation: %w", err) } return "", opUid, nil } func (c *NubesClient) WaitForInstanceStatus(ctx context.Context, instanceUid string, targetStatus string) error { timeout := time.After(15 * time.Minute) ticker := time.NewTicker(10 * time.Second) defer ticker.Stop() for { select { case <-ctx.Done(): return ctx.Err() case <-timeout: return fmt.Errorf("timeout waiting for instance %s to reach status %s", instanceUid, targetStatus) case <-ticker.C: inst, err := c.GetInstanceState(ctx, instanceUid) if err != nil { tflog.Warn(ctx, fmt.Sprintf("Error checking state for %s: %s", instanceUid, err)) continue } state := inst.ExplainedStatus tflog.Info(ctx, fmt.Sprintf("Instance %s current status: %s (target: %s, InProgress: %v)", instanceUid, state, targetStatus, inst.OperationIsInProgress)) if strings.EqualFold(state, targetStatus) { return nil } lowerState := strings.ToLower(state) if strings.Contains(lowerState, "error") || strings.Contains(lowerState, "failed") { return fmt.Errorf("instance %s in error state: %s", instanceUid, state) } if !inst.OperationIsInProgress && !inst.OperationIsPending { if strings.EqualFold(state, targetStatus) { return nil } return fmt.Errorf("operation finished but target status %s not reached (current: %s)", targetStatus, state) } } } } // ttyWriter открывает /dev/tty для прямого вывода в терминал пользователя, // минуя перехват stderr terraform'ом. Fallback на os.Stderr. func ttyWriter() *os.File { if f, err := os.OpenFile("/dev/tty", os.O_WRONLY, 0); err == nil { return f } return os.Stderr } func (c *NubesClient) WaitForOperation(ctx context.Context, opUid string) error { timeout := time.After(15 * time.Minute) ticker := time.NewTicker(10 * time.Second) defer ticker.Stop() // отслеживаем уже напечатанные этапы чтобы не дублировать printedStages := make(map[string]bool) for { select { case <-ctx.Done(): return ctx.Err() case <-timeout: return fmt.Errorf("timeout waiting for operation %s", opUid) case <-ticker.C: op, err := c.GetInstanceOperation(ctx, opUid) if err != nil { tflog.Warn(ctx, fmt.Sprintf("Error checking operation %s: %s", opUid, err)) continue } // Печатаем завершённые этапы tty := ttyWriter() defer tty.Close() for _, stage := range op.Stages { if printedStages[stage.InstanceOperationStageUid] { continue } if stage.DtFinish == nil || *stage.DtFinish == "" { continue } printedStages[stage.InstanceOperationStageUid] = true status := "OK " if !stage.IsSuccessful { status = "FAIL" } fmt.Fprintf(tty, " [%s] %s — %.1f sec\n", status, stage.Stage, stage.Duration) if stage.StageMsg != nil && *stage.StageMsg != "" { fmt.Fprintf(tty, " %s\n", *stage.StageMsg) } } // Операция завершена if !op.IsInProgress && !op.IsPending && op.DtFinish != nil && *op.DtFinish != "" { if op.IsSuccessful != nil && *op.IsSuccessful { if op.Duration != nil { fmt.Fprintf(tty, " [DONE] operation completed in %.1f sec\n", *op.Duration) } return nil } // Собираем детали ошибки из упавших этапов var failedStages []string for _, stage := range op.Stages { if !stage.IsSuccessful && stage.DtFinish != nil { msg := stage.Stage if stage.StageMsg != nil && *stage.StageMsg != "" { msg += ": " + *stage.StageMsg } failedStages = append(failedStages, msg) } } errMsg := "operation failed" if op.ErrorLog != nil && *op.ErrorLog != "" { errMsg = *op.ErrorLog } if len(failedStages) > 0 { errMsg += " | failed stages: " + strings.Join(failedStages, "; ") } return fmt.Errorf("%s", errMsg) } // Ранний выход если ErrorLog появился раньше dtFinish if op.ErrorLog != nil && *op.ErrorLog != "" { return fmt.Errorf("operation failed (early error): %s", *op.ErrorLog) } } } } func (c *NubesClient) get(ctx context.Context, path string, target interface{}) error { url := fmt.Sprintf("%s%s", c.ApiEndpoint, path) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return err } if c.ApiToken != "" { req.Header.Set("Authorization", "Bearer "+c.ApiToken) } resp, err := c.HttpClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return fmt.Errorf("GET %s failed with status %d: %s", path, resp.StatusCode, string(body)) } if target != nil { return json.NewDecoder(resp.Body).Decode(target) } return nil } func (c *NubesClient) WaitForInstanceReady(ctx context.Context, instanceUid string) error { // Set timeout timeout := time.After(15 * time.Minute) ticker := time.NewTicker(10 * time.Second) defer ticker.Stop() for { select { case <-ctx.Done(): return ctx.Err() case <-timeout: return fmt.Errorf("timeout waiting for instance %s", instanceUid) case <-ticker.C: // check status inst, err := c.GetInstanceState(ctx, instanceUid) if err != nil { tflog.Warn(ctx, fmt.Sprintf("Error checking state for %s: %s", instanceUid, err)) continue } state := inst.ExplainedStatus tflog.Info(ctx, fmt.Sprintf("Instance %s status: %s (InProgress: %v)", instanceUid, state, inst.OperationIsInProgress)) // Statuses from HAR: "running", "Active", "deployed", "Deployed" if strings.EqualFold(state, "Active") || strings.EqualFold(state, "Running") || strings.EqualFold(state, "Deployed") { return nil } // Detect failure patterns in explainedStatus lowerState := strings.ToLower(state) if strings.Contains(lowerState, "error") || strings.Contains(lowerState, "failed") || strings.Contains(lowerState, "не удалось") || strings.Contains(lowerState, "не заполнена") { return fmt.Errorf("instance %s end status: %s", instanceUid, state) } // If operation finished but we didn't reach success state if !inst.OperationIsInProgress && !inst.OperationIsPending { // Second check for success, sometimes status updates slightly after op finishes if strings.EqualFold(state, "Active") || strings.EqualFold(state, "Running") || strings.EqualFold(state, "Deployed") { return nil } // If still not success, and not in a known transitioning state (like "Processing", "Creating") if !strings.EqualFold(state, "Creating") && !strings.EqualFold(state, "Processing") && state != "" { return fmt.Errorf("operation finished but instance %s is in unexpected state: %s", instanceUid, state) } } } } } func (c *NubesClient) DeleteInstance(ctx context.Context, instanceUid string, opId int) error { // Create delete operation opPayload := map[string]interface{}{ "instanceUid": instanceUid, "operation": "delete", } opUid, err := c.postIgnoreResponse(ctx, "/instanceOperations", opPayload, true) if err != nil { return err } // Run It runUrl := fmt.Sprintf("/instanceOperations/%s/run", opUid) _, err = c.postIgnoreResponse(ctx, runUrl, map[string]interface{}{}, false) return err } // Helpers func (c *NubesClient) postInstance(ctx context.Context, payload interface{}) (string, error) { url := fmt.Sprintf("%s/instances", c.ApiEndpoint) data, err := json.Marshal(payload) if err != nil { return "", err } req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(data)) if err != nil { return "", err } req.Header.Set("Content-Type", "application/json") if c.ApiToken != "" { req.Header.Set("Authorization", "Bearer "+c.ApiToken) } resp, err := c.HttpClient.Do(req) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode != 201 && resp.StatusCode != 200 { body, _ := io.ReadAll(resp.Body) return "", fmt.Errorf("status %d: %s", resp.StatusCode, string(body)) } // Try to get ID from Location header loc := resp.Header.Get("Location") if loc != "" { parts := strings.Split(loc, "/") if len(parts) > 0 { return parts[len(parts)-1], nil } } // Try body var res map[string]interface{} body, _ := io.ReadAll(resp.Body) if len(body) > 0 { if err := json.Unmarshal(body, &res); err == nil { if uid, ok := res["instanceUid"].(string); ok { return uid, nil } } } return "", fmt.Errorf("could not extract instanceUid from response") } func (c *NubesClient) postIgnoreResponse(ctx context.Context, path string, payload interface{}, returnLocationId bool) (string, error) { url := fmt.Sprintf("%s%s", c.ApiEndpoint, path) var reqBody io.Reader if payload != nil { data, err := json.Marshal(payload) if err != nil { return "", err } reqBody = bytes.NewBuffer(data) } req, err := http.NewRequestWithContext(ctx, "POST", url, reqBody) if err != nil { return "", err } req.Header.Set("Content-Type", "application/json") if c.ApiToken != "" { req.Header.Set("Authorization", "Bearer "+c.ApiToken) } resp, err := c.HttpClient.Do(req) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode != 201 && resp.StatusCode != 200 { body, _ := io.ReadAll(resp.Body) return "", fmt.Errorf("status %d: %s", resp.StatusCode, string(body)) } if returnLocationId { // Extract ID from Location header .../something/{id} loc := resp.Header.Get("Location") if loc != "" { parts := strings.Split(loc, "/") return parts[len(parts)-1], nil } // Fallback to body scan if needed var res map[string]interface{} body, _ := io.ReadAll(resp.Body) if len(body) > 0 { if err := json.Unmarshal(body, &res); err == nil { // Try common ID fields if id, ok := res["instanceOperationUid"].(string); ok { return id, nil } } } } return "", nil } type ApiOperation struct { SvcOperationId int `json:"svcOperationId"` Operation string `json:"operation"` } type InstanceStateData struct { Params map[string]interface{} `json:"params"` } type InstanceStateResponse struct { InstanceUid string `json:"instanceUid"` ExplainedStatus string `json:"explainedStatus"` OperationIsInProgress bool `json:"operationIsInProgress"` OperationIsPending bool `json:"operationIsPending"` AvailableOperations []ApiOperation `json:"availableOperations"` State *InstanceStateData `json:"state"` } func (c *NubesClient) GetInstanceState(ctx context.Context, instanceUid string) (*InstanceStateResponse, error) { url := fmt.Sprintf("%s/instances/%s", c.ApiEndpoint, instanceUid) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, err } if c.ApiToken != "" { req.Header.Set("Authorization", "Bearer "+c.ApiToken) } resp, err := c.HttpClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != 200 { return nil, fmt.Errorf("status %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 } // Post is a helper for generic API calls, used by postgres_resource func (c *NubesClient) Post(path string, payload interface{}, target interface{}) error { url := fmt.Sprintf("%s%s", c.ApiEndpoint, path) var reqBody io.Reader if payload != nil { data, err := json.Marshal(payload) if err != nil { return err } reqBody = bytes.NewBuffer(data) } req, err := http.NewRequestWithContext(context.Background(), "POST", url, reqBody) if err != nil { return err } req.Header.Set("Content-Type", "application/json") if c.ApiToken != "" { req.Header.Set("Authorization", "Bearer "+c.ApiToken) } resp, err := c.HttpClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != 200 && resp.StatusCode != 201 { body, _ := io.ReadAll(resp.Body) return fmt.Errorf("status %d: %s", resp.StatusCode, string(body)) } if target != nil { return json.NewDecoder(resp.Body).Decode(target) } return nil } func (c *NubesClient) GetInstanceStateDetails(ctx context.Context, instanceUid string) (map[string]interface{}, error) { url := fmt.Sprintf("%s/instances/%s", c.ApiEndpoint, instanceUid) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, err } if c.ApiToken != "" { req.Header.Set("Authorization", "Bearer "+c.ApiToken) } resp, err := c.HttpClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != 200 { return nil, fmt.Errorf("status %d", resp.StatusCode) } var res map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { return nil, err } if inst, ok := res["instance"].(map[string]interface{}); ok { if state, ok := inst["state"].(map[string]interface{}); ok { return state, nil } } return nil, fmt.Errorf("could not find instance.state in response") } func (c *NubesClient) GetOperationIdForInstance(ctx context.Context, instanceUid string, opName string) (int, error) { url := fmt.Sprintf("%s/instances/%s", c.ApiEndpoint, instanceUid) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return 0, err } if c.ApiToken != "" { req.Header.Set("Authorization", "Bearer "+c.ApiToken) } resp, err := c.HttpClient.Do(req) if err != nil { return 0, err } defer resp.Body.Close() if resp.StatusCode != 200 { return 0, fmt.Errorf("status %d", resp.StatusCode) } var res map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { return 0, err } inst, ok := res["instance"].(map[string]interface{}) if !ok { return 0, fmt.Errorf("instance field missing") } availableOps, ok := inst["availableOperations"].([]interface{}) if !ok { return 0, fmt.Errorf("availableOperations missing or empty") } for _, item := range availableOps { opMap, ok := item.(map[string]interface{}) if !ok { continue } var name string var id int if n, ok := opMap["name"].(string); ok { name = n } if n, ok := opMap["operation"].(string); ok { name = n } if i, ok := opMap["svcOperationId"].(float64); ok { id = int(i) } if svcOp, ok := opMap["svcOperation"].(map[string]interface{}); ok { if n, ok := svcOp["name"].(string); ok { name = n } if idFloat, ok := svcOp["id"].(float64); ok { id = int(idFloat) } } if strings.EqualFold(name, opName) { return id, nil } } return 0, fmt.Errorf("operation '%s' not found in available operations", opName) } type GetOperationResponse struct { InstanceOperation OperationResponse `json:"instanceOperation"` } // OperationResponse представляет собой полное состояние операции из Deck API. // Все поля получены путем анализа HAR файлов (dummy.har, vdc.har) и документации платформы. type OperationResponse struct { InstanceOperationUid string `json:"instanceOperationUid"` // Уникальный идентификатор операции (UUID) InstanceUid string `json:"instanceUid"` // Идентификатор инстанса, над которым идет работа Operation string `json:"operation"` // Имя операции (create, modify, suspend, и т.д.) IsInProgress bool `json:"isInProgress"` // true, если Jenkins-джоб выполняется в данный момент IsPending bool `json:"isPending"` // true, если операция стоит в очереди (ждем слота в Jenkins) IsSuccessful *bool `json:"isSuccessful"` // "Зеленая галка" платформы. Появляется ПОСЛЕ dtFinish. DtFinish *string `json:"dtFinish"` // Штамп времени окончания (ГГГГ-ММ-ДД...). Не null = ОПЕРАЦИЯ ЗАВЕРШЕНА. DtCreated *string `json:"dtCreated"` // Время создания записи об операции DtUpdated *string `json:"dtUpdated"` // Время последнего обновления записи DtSubmit *string `json:"dtSubmit"` // Когда кнопка была нажата (или API вызван) DtStart *string `json:"dtStart"` // Когда реально начался Jenkins-джоб SubmitResult *string `json:"submitResult"` // HTTP-код первичной регистрации (обычно "201") Duration *float64 `json:"duration"` // Общее время выполнения в секундах ErrorLog *string `json:"errorLog"` // Текст ошибки, если операция упала UpdaterId *int `json:"updaterId"` // ID пользователя, запустившего операцию UpdaterLogin *string `json:"updaterLogin"` // Логин инициатора UpdaterShortname *string `json:"updaterShortname"` // Инициалы инициатора (например, "Н. Ф.") DisplayName *string `json:"displayName"` // Имя инстанса на момент операции ServiceId *int `json:"serviceId"` // ID сервиса (1 - Болванка, 96 - PG Admin и т.д.) Svc *string `json:"svc"` // Текстовое имя сервиса SvcOperationId *int `json:"svcOperationId"` // Внутренний ID операции в каталоге Man *string `json:"man"` // Мануал/описание операции (иногда содержит Markdown) CfsParams []CfsParam `json:"cfsParams"` // Список всех параметров (конфигурация) Stages []ApiStage `json:"stages"` // Этапы выполнения джоба (подготовка, секреты...) State any `json:"state"` // Результирующее состояние (выходные данные джоба) } // ApiStage представляет этап выполнения операции в Jenkins type ApiStage struct { InstanceOperationStageUid string `json:"instanceOperationStageUid"` Stage string `json:"stage"` // Название (например, "Подготовка среды") IsSuccessful bool `json:"isSuccessful"` // Успех конкретного этапа DtStart *string `json:"dtStart"` DtFinish *string `json:"dtFinish"` Duration float64 `json:"duration"` StageMsg *string `json:"stageMsg"` // Лог этапа (часто JSON в строке) } type CfsParam struct { InstanceOperationCfsParamUid string `json:"instanceOperationCfsParamUid"` SvcOperationCfsParamId int `json:"svcOperationCfsParamId"` ParamValue *string `json:"paramValue"` DefaultValue *string `json:"defaultValue"` DataType string `json:"dataType"` Name string `json:"name"` Code string `json:"code"` SvcOperationCfsParam string `json:"svcOperationCfsParam"` } func (c *NubesClient) GetInstanceOperation(ctx context.Context, opUid string) (*OperationResponse, error) { // ВАЖНО: Список полей максимально расширен на основе HAR (dummy.har, vdc.har). // Эти поля позволяют видеть полную картину происходящего на платформе. fields := "instanceOperationUid,instanceUid,state,stages,isSuccessful,dtCreated,dtUpdated,dtStart,dtFinish,operation,svcOperationId,svc,displayName,submitResult,duration,errorLog,updaterShortname,man,isInProgress,isPending,dtSubmit" url := fmt.Sprintf("%s/instanceOperations/%s?fields=%s", c.ApiEndpoint, opUid, fields) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, err } if c.ApiToken != "" { req.Header.Set("Authorization", "Bearer "+c.ApiToken) } resp, err := c.HttpClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) if resp.StatusCode != 200 { return nil, fmt.Errorf("status %d: %s", resp.StatusCode, string(body)) } var res GetOperationResponse if err := json.Unmarshal(body, &res); err != nil { return nil, fmt.Errorf("json unmarshal failed: %w, body: %s", err, string(body)) } return &res.InstanceOperation, nil } func (c *NubesClient) RunAction(ctx context.Context, instanceUid string, action string) error { state, err := c.GetInstanceState(ctx, instanceUid) if err != nil { return err } var opId int for _, op := range state.AvailableOperations { if op.Operation == action { opId = op.SvcOperationId break } } if opId == 0 { return fmt.Errorf("action %s not available for instance %s", action, instanceUid) } // 1. Create Op 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) } // 2. Run Op runUrl := fmt.Sprintf("/instanceOperations/%s/run", opUid) _, err = c.postIgnoreResponse(ctx, runUrl, map[string]interface{}{}, false) return err } type InstanceSummary struct { InstanceUid string `json:"instanceUid"` DisplayName string `json:"displayName"` ServiceId int `json:"serviceId"` Svc string `json:"svc"` } type InstancesListResponse struct { Results []InstanceSummary `json:"results"` } func (c *NubesClient) GetInstances(ctx context.Context) ([]InstanceSummary, error) { var allInstances []InstanceSummary page := 1 for { url := fmt.Sprintf("%s/instances?page=%d&size=100", c.ApiEndpoint, page) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, err } if c.ApiToken != "" { req.Header.Set("Authorization", "Bearer "+c.ApiToken) } resp, err := c.HttpClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != 200 { return nil, fmt.Errorf("status %d", resp.StatusCode) } var res InstancesListResponse if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { return nil, err } if len(res.Results) == 0 { break } allInstances = append(allInstances, res.Results...) page++ // Safety break to prevent infinite loops if API behaves weirdly if page > 100 { break } } return allInstances, nil } func (c *NubesClient) GetInstanceFull(ctx context.Context, instanceUid string) (map[string]interface{}, error) { url := fmt.Sprintf("%s/instances/%s", c.ApiEndpoint, instanceUid) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, err } if c.ApiToken != "" { req.Header.Set("Authorization", "Bearer "+c.ApiToken) } resp, err := c.HttpClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != 200 { return nil, fmt.Errorf("status %d", resp.StatusCode) } var res map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { return nil, err } if inst, ok := res["instance"].(map[string]interface{}); ok { return inst, nil } return nil, fmt.Errorf("instance field missing in response") }