add: internal package (provider, core, registrykeys)

This commit is contained in:
“Naeel”
2026-06-30 15:42:35 +04:00
parent eb2db782f3
commit 7f3fbd5041
25 changed files with 10796 additions and 0 deletions
+924
View File
@@ -0,0 +1,924 @@
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")
}