Files
tf_provider/internal/provider/client_impl.go
T

925 lines
30 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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")
}