925 lines
30 KiB
Go
925 lines
30 KiB
Go
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")
|
||
}
|