add: universal client core
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNormalizeValue_EmptyString(t *testing.T) {
|
||||
param := universalCfsParam{DataType: "string"}
|
||||
result := normalizeUniversalValueV6("", param)
|
||||
if result != "" {
|
||||
t.Errorf("expected empty string, got: %q", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeValue_NullString(t *testing.T) {
|
||||
param := universalCfsParam{DataType: "string"}
|
||||
result := normalizeUniversalValueV6("null", param)
|
||||
if result != "" {
|
||||
t.Errorf("expected empty string for 'null' input, got: %q", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeValue_MapType(t *testing.T) {
|
||||
param := universalCfsParam{DataType: "map"}
|
||||
result := normalizeUniversalValueV6("", param)
|
||||
if result != "{}" {
|
||||
t.Errorf("expected '{}' for map dataType, got: %q", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeValue_ArrayType(t *testing.T) {
|
||||
param := universalCfsParam{DataType: "array"}
|
||||
result := normalizeUniversalValueV6("", param)
|
||||
if result != "[]" {
|
||||
t.Errorf("expected '[]' for array dataType, got: %q", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeValue_TrimSpace(t *testing.T) {
|
||||
param := universalCfsParam{DataType: "string"}
|
||||
result := normalizeUniversalValueV6(" hello ", param)
|
||||
if result != "hello" {
|
||||
t.Errorf("expected 'hello' after trim, got: %q", result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type InstanceStateDetails struct {
|
||||
Params map[string]string
|
||||
Out map[string]string
|
||||
RawParams map[string]interface{}
|
||||
RawOut map[string]interface{}
|
||||
Vault InstanceVaultMeta
|
||||
}
|
||||
|
||||
type InstanceVaultMeta struct {
|
||||
Url string
|
||||
UserPath string
|
||||
Fields []string
|
||||
}
|
||||
|
||||
// GetInstanceStateDetails returns instance.state params/out and vault metadata.
|
||||
func (c *UniversalClient) GetInstanceStateDetails(ctx context.Context, instanceUid string) (*InstanceStateDetails, error) {
|
||||
if strings.TrimSpace(instanceUid) == "" {
|
||||
return nil, fmt.Errorf("missing instance uid")
|
||||
}
|
||||
|
||||
respBody, _, err := c.doRequest(ctx, "GET", fmt.Sprintf("/instances/%s", instanceUid), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var res map[string]interface{}
|
||||
if err := json.Unmarshal(respBody, &res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
inst, ok := res["instance"].(map[string]interface{})
|
||||
if !ok {
|
||||
return &InstanceStateDetails{Params: map[string]string{}, Out: map[string]string{}}, nil
|
||||
}
|
||||
|
||||
state, _ := inst["state"].(map[string]interface{})
|
||||
if state == nil {
|
||||
return &InstanceStateDetails{Params: map[string]string{}, Out: map[string]string{}}, nil
|
||||
}
|
||||
|
||||
paramsRaw := extractStateRawMap(state["params"])
|
||||
outRaw := extractStateRawMap(state["out"])
|
||||
params := extractStateStringMap(paramsRaw)
|
||||
out := extractStateStringMap(outRaw)
|
||||
vault := extractVaultMeta(state["vault"])
|
||||
|
||||
return &InstanceStateDetails{
|
||||
Params: params,
|
||||
Out: out,
|
||||
RawParams: paramsRaw,
|
||||
RawOut: outRaw,
|
||||
Vault: vault,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetInstanceVaultSecrets fetches all named secrets from the vault endpoint.
|
||||
func (c *UniversalClient) GetInstanceVaultSecrets(ctx context.Context, instanceUid string, fields []string) (map[string]string, error) {
|
||||
secrets := make(map[string]string)
|
||||
if strings.TrimSpace(instanceUid) == "" || len(fields) == 0 {
|
||||
return secrets, nil
|
||||
}
|
||||
|
||||
var errs []string
|
||||
for _, field := range fields {
|
||||
name := strings.TrimSpace(field)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
value, err := c.GetInstanceVaultSecret(ctx, instanceUid, name)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Sprintf("%s: %v", name, err))
|
||||
continue
|
||||
}
|
||||
secrets[name] = value
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return secrets, fmt.Errorf(strings.Join(errs, "; "))
|
||||
}
|
||||
return secrets, nil
|
||||
}
|
||||
|
||||
// GetInstanceVaultSecret fetches a single secret value by name.
|
||||
func (c *UniversalClient) GetInstanceVaultSecret(ctx context.Context, instanceUid string, secretName string) (string, error) {
|
||||
if strings.TrimSpace(instanceUid) == "" {
|
||||
return "", fmt.Errorf("missing instance uid")
|
||||
}
|
||||
if strings.TrimSpace(secretName) == "" {
|
||||
return "", fmt.Errorf("missing secret name")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/instances/%s/vault/%s", instanceUid, url.PathEscape(secretName))
|
||||
respBody, _, err := c.doRequest(ctx, "GET", path, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return extractVaultSecretValue(respBody)
|
||||
}
|
||||
|
||||
func extractStateRawMap(value interface{}) map[string]interface{} {
|
||||
raw, _ := value.(map[string]interface{})
|
||||
if raw == nil {
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
func extractStateStringMap(raw map[string]interface{}) map[string]string {
|
||||
if raw == nil {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
out := make(map[string]string, len(raw))
|
||||
for key, val := range raw {
|
||||
out[key] = normalizeInstanceParamValue(val)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func extractVaultMeta(value interface{}) InstanceVaultMeta {
|
||||
raw, _ := value.(map[string]interface{})
|
||||
if raw == nil {
|
||||
return InstanceVaultMeta{}
|
||||
}
|
||||
|
||||
return InstanceVaultMeta{
|
||||
Url: normalizeInstanceParamValue(raw["url"]),
|
||||
UserPath: normalizeInstanceParamValue(raw["userPath"]),
|
||||
Fields: extractStringSlice(raw["fields"]),
|
||||
}
|
||||
}
|
||||
|
||||
func extractStringSlice(value interface{}) []string {
|
||||
list, _ := value.([]interface{})
|
||||
if list == nil {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
out := make([]string, 0, len(list))
|
||||
for _, item := range list {
|
||||
val := strings.TrimSpace(normalizeInstanceParamValue(item))
|
||||
if val == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, val)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func extractVaultSecretValue(body []byte) (string, error) {
|
||||
var decoded interface{}
|
||||
if err := json.Unmarshal(body, &decoded); err != nil {
|
||||
return strings.TrimSpace(string(body)), nil
|
||||
}
|
||||
|
||||
switch v := decoded.(type) {
|
||||
case string:
|
||||
return strings.TrimSpace(v), nil
|
||||
case map[string]interface{}:
|
||||
if value, ok := extractSecretFromMap(v); ok {
|
||||
return value, nil
|
||||
}
|
||||
b, _ := json.Marshal(v)
|
||||
return strings.TrimSpace(string(b)), nil
|
||||
default:
|
||||
b, _ := json.Marshal(v)
|
||||
return strings.TrimSpace(string(b)), nil
|
||||
}
|
||||
}
|
||||
|
||||
func extractSecretFromMap(raw map[string]interface{}) (string, bool) {
|
||||
for _, key := range []string{"value", "secret", "password", "token"} {
|
||||
if value, ok := raw[key]; ok {
|
||||
out := strings.TrimSpace(normalizeInstanceParamValue(value))
|
||||
if out != "" {
|
||||
return out, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if data, ok := raw["data"].(map[string]interface{}); ok {
|
||||
if value, ok := extractSecretFromMap(data); ok {
|
||||
return value, true
|
||||
}
|
||||
}
|
||||
if record, ok := raw["record"].(map[string]interface{}); ok {
|
||||
if value, ok := extractSecretFromMap(record); ok {
|
||||
return value, true
|
||||
}
|
||||
}
|
||||
|
||||
if len(raw) == 1 {
|
||||
for _, value := range raw {
|
||||
out := strings.TrimSpace(normalizeInstanceParamValue(value))
|
||||
if out != "" {
|
||||
return out, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetInstanceStateParams returns instance.state.params as a map of string values.
|
||||
func (c *UniversalClient) GetInstanceStateParams(ctx context.Context, instanceUid string) (map[string]string, error) {
|
||||
if strings.TrimSpace(instanceUid) == "" {
|
||||
return nil, fmt.Errorf("missing instance uid")
|
||||
}
|
||||
|
||||
respBody, _, err := c.doRequest(ctx, "GET", fmt.Sprintf("/instances/%s", instanceUid), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var res map[string]interface{}
|
||||
if err := json.Unmarshal(respBody, &res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
inst, ok := res["instance"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("instance field missing in response")
|
||||
}
|
||||
|
||||
state, _ := inst["state"].(map[string]interface{})
|
||||
if state == nil {
|
||||
return map[string]string{}, nil
|
||||
}
|
||||
|
||||
paramsRaw, _ := state["params"].(map[string]interface{})
|
||||
if paramsRaw == nil {
|
||||
return map[string]string{}, nil
|
||||
}
|
||||
|
||||
params := make(map[string]string, len(paramsRaw))
|
||||
for key, val := range paramsRaw {
|
||||
params[key] = normalizeInstanceParamValue(val)
|
||||
}
|
||||
|
||||
return params, nil
|
||||
}
|
||||
|
||||
func normalizeInstanceParamValue(value interface{}) string {
|
||||
switch v := value.(type) {
|
||||
case nil:
|
||||
return ""
|
||||
case string:
|
||||
return strings.TrimSpace(v)
|
||||
case bool:
|
||||
if v {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
case float64:
|
||||
if math.Mod(v, 1) == 0 {
|
||||
return strconv.FormatInt(int64(v), 10)
|
||||
}
|
||||
return strconv.FormatFloat(v, 'f', -1, 64)
|
||||
case json.Number:
|
||||
return v.String()
|
||||
default:
|
||||
b, err := json.Marshal(v)
|
||||
if err == nil {
|
||||
return strings.TrimSpace(string(b))
|
||||
}
|
||||
return fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type operationTimeoutOverrideKey struct{}
|
||||
|
||||
// WithOperationTimeout attaches optional per-request operation timeout override to context.
|
||||
// Empty value means no override and falls back to configured defaults.
|
||||
func WithOperationTimeout(ctx context.Context, raw string) (context.Context, error) {
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
if trimmed == "" {
|
||||
return ctx, nil
|
||||
}
|
||||
d, err := time.ParseDuration(trimmed)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("некорректный operation_timeout %q: %w", raw, err)
|
||||
}
|
||||
if d <= 0 {
|
||||
return nil, fmt.Errorf("некорректный operation_timeout %q: значение должно быть больше 0", raw)
|
||||
}
|
||||
return context.WithValue(ctx, operationTimeoutOverrideKey{}, d), nil
|
||||
}
|
||||
|
||||
func operationTimeoutOverrideFromContext(ctx context.Context) (time.Duration, bool) {
|
||||
if ctx == nil {
|
||||
return 0, false
|
||||
}
|
||||
v := ctx.Value(operationTimeoutOverrideKey{})
|
||||
d, ok := v.(time.Duration)
|
||||
if !ok || d <= 0 {
|
||||
return 0, false
|
||||
}
|
||||
return d, true
|
||||
}
|
||||
|
||||
func (c *UniversalClient) operationTimeoutForContext(ctx context.Context, serviceID int, action string) time.Duration {
|
||||
if d, ok := operationTimeoutOverrideFromContext(ctx); ok {
|
||||
return d
|
||||
}
|
||||
return c.operationTimeoutFor(serviceID, action)
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultOperationTimeoutValue = 30 * time.Minute
|
||||
defaultIdleTimeoutValue = 30 * time.Minute
|
||||
)
|
||||
|
||||
type OperationTimeouts struct {
|
||||
DefaultTimeout time.Duration
|
||||
IdleTimeout time.Duration
|
||||
ByOperationName map[string]time.Duration
|
||||
ByServiceOperation map[int]map[string]time.Duration
|
||||
}
|
||||
|
||||
type operationTimeoutsConfig struct {
|
||||
DefaultTimeout string `json:"default_timeout"`
|
||||
IdleTimeout string `json:"idle_timeout"`
|
||||
Operations map[string]string `json:"operations"`
|
||||
Overrides map[string]string `json:"overrides"`
|
||||
}
|
||||
|
||||
func DefaultOperationTimeouts() *OperationTimeouts {
|
||||
return &OperationTimeouts{
|
||||
DefaultTimeout: defaultOperationTimeoutValue,
|
||||
IdleTimeout: defaultIdleTimeoutValue,
|
||||
ByOperationName: map[string]time.Duration{
|
||||
"create": 30 * time.Minute,
|
||||
"modify": 30 * time.Minute,
|
||||
"resume": 30 * time.Minute,
|
||||
"suspend": 30 * time.Minute,
|
||||
"delete_user": 10 * time.Minute,
|
||||
"create_user": 10 * time.Minute,
|
||||
"delete_database": 10 * time.Minute,
|
||||
"create_database": 10 * time.Minute,
|
||||
},
|
||||
ByServiceOperation: map[int]map[string]time.Duration{},
|
||||
}
|
||||
}
|
||||
|
||||
func LoadOperationTimeoutsFromBytes(raw []byte) (*OperationTimeouts, error) {
|
||||
var cfg operationTimeoutsConfig
|
||||
if err := json.Unmarshal(raw, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse operation timeouts config: %w", err)
|
||||
}
|
||||
|
||||
result := DefaultOperationTimeouts()
|
||||
|
||||
if strings.TrimSpace(cfg.DefaultTimeout) != "" {
|
||||
d, err := time.ParseDuration(strings.TrimSpace(cfg.DefaultTimeout))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid default_timeout value %q: %w", cfg.DefaultTimeout, err)
|
||||
}
|
||||
if d <= 0 {
|
||||
return nil, fmt.Errorf("invalid default_timeout value %q: must be > 0", cfg.DefaultTimeout)
|
||||
}
|
||||
result.DefaultTimeout = d
|
||||
}
|
||||
|
||||
if strings.TrimSpace(cfg.IdleTimeout) != "" {
|
||||
d, err := time.ParseDuration(strings.TrimSpace(cfg.IdleTimeout))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid idle_timeout value %q: %w", cfg.IdleTimeout, err)
|
||||
}
|
||||
if d <= 0 {
|
||||
return nil, fmt.Errorf("invalid idle_timeout value %q: must be > 0", cfg.IdleTimeout)
|
||||
}
|
||||
result.IdleTimeout = d
|
||||
}
|
||||
|
||||
for operation, timeoutRaw := range cfg.Operations {
|
||||
operationKey := strings.ToLower(strings.TrimSpace(operation))
|
||||
if operationKey == "" {
|
||||
continue
|
||||
}
|
||||
d, err := time.ParseDuration(strings.TrimSpace(timeoutRaw))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid timeout for operation %q: %w", operation, err)
|
||||
}
|
||||
if d <= 0 {
|
||||
return nil, fmt.Errorf("invalid timeout for operation %q: must be > 0", operation)
|
||||
}
|
||||
result.ByOperationName[operationKey] = d
|
||||
}
|
||||
|
||||
for overrideKey, timeoutRaw := range cfg.Overrides {
|
||||
serviceID, operationKey, err := parseServiceOperationOverrideKey(overrideKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d, err := time.ParseDuration(strings.TrimSpace(timeoutRaw))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid timeout for override %q: %w", overrideKey, err)
|
||||
}
|
||||
if d <= 0 {
|
||||
return nil, fmt.Errorf("invalid timeout for override %q: must be > 0", overrideKey)
|
||||
}
|
||||
if _, ok := result.ByServiceOperation[serviceID]; !ok {
|
||||
result.ByServiceOperation[serviceID] = map[string]time.Duration{}
|
||||
}
|
||||
result.ByServiceOperation[serviceID][operationKey] = d
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func parseServiceOperationOverrideKey(raw string) (int, string, error) {
|
||||
key := strings.ToLower(strings.TrimSpace(raw))
|
||||
parts := strings.Split(key, ".")
|
||||
if len(parts) != 3 || parts[0] != "services" || strings.TrimSpace(parts[1]) == "" || strings.TrimSpace(parts[2]) == "" {
|
||||
return 0, "", fmt.Errorf("invalid override key %q: expected services.<serviceId>.<operation>", raw)
|
||||
}
|
||||
serviceID, err := strconv.Atoi(parts[1])
|
||||
if err != nil || serviceID <= 0 {
|
||||
return 0, "", fmt.Errorf("invalid override key %q: serviceId must be positive integer", raw)
|
||||
}
|
||||
return serviceID, parts[2], nil
|
||||
}
|
||||
|
||||
func (c *UniversalClient) operationTimeoutFor(serviceID int, action string) time.Duration {
|
||||
if c == nil || c.OperationTimeouts == nil {
|
||||
return defaultOperationTimeoutValue
|
||||
}
|
||||
key := strings.ToLower(strings.TrimSpace(action))
|
||||
if serviceID > 0 && key != "" {
|
||||
if serviceOverrides, ok := c.OperationTimeouts.ByServiceOperation[serviceID]; ok {
|
||||
if d, ok := serviceOverrides[key]; ok && d > 0 {
|
||||
return d
|
||||
}
|
||||
}
|
||||
}
|
||||
if key != "" {
|
||||
if d, ok := c.OperationTimeouts.ByOperationName[key]; ok && d > 0 {
|
||||
return d
|
||||
}
|
||||
}
|
||||
if c.OperationTimeouts.DefaultTimeout > 0 {
|
||||
return c.OperationTimeouts.DefaultTimeout
|
||||
}
|
||||
return defaultOperationTimeoutValue
|
||||
}
|
||||
|
||||
func (c *UniversalClient) idleTimeoutFor(serviceID int) time.Duration {
|
||||
if c == nil || c.OperationTimeouts == nil {
|
||||
return defaultIdleTimeoutValue
|
||||
}
|
||||
if serviceID > 0 {
|
||||
if serviceOverrides, ok := c.OperationTimeouts.ByServiceOperation[serviceID]; ok {
|
||||
if d, ok := serviceOverrides["idle"]; ok && d > 0 {
|
||||
return d
|
||||
}
|
||||
}
|
||||
}
|
||||
if c.OperationTimeouts.IdleTimeout > 0 {
|
||||
return c.OperationTimeouts.IdleTimeout
|
||||
}
|
||||
if c.OperationTimeouts.DefaultTimeout > 0 {
|
||||
return c.OperationTimeouts.DefaultTimeout
|
||||
}
|
||||
return defaultIdleTimeoutValue
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type RefServiceInstanceOption struct {
|
||||
InstanceUID string
|
||||
DisplayName string
|
||||
}
|
||||
|
||||
// ResolveRefSvcParamValue normalizes a refSvcId parameter to a UUID if possible.
|
||||
// If value already looks like a UUID, it is returned as-is.
|
||||
func (c *UniversalClient) ResolveRefSvcParamValue(ctx context.Context, refSvcId int, value string) (string, error) {
|
||||
if c == nil {
|
||||
return "", nil
|
||||
}
|
||||
if refSvcId <= 0 {
|
||||
return value, nil
|
||||
}
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" {
|
||||
return "", nil
|
||||
}
|
||||
// FIX(uuid-case): UUID-like values are normalised to lowercase so that
|
||||
// plan (user input may be uppercase) and API response (always lowercase)
|
||||
// produce identical strings and do not cause Terraform"s plan/state
|
||||
// inconsistency error.
|
||||
if isUUIDLike(trimmed) {
|
||||
return strings.ToLower(trimmed), nil
|
||||
}
|
||||
uid, err := c.findInstanceUidByDisplayNameRefSvc(ctx, refSvcId, trimmed)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return uid, nil
|
||||
}
|
||||
|
||||
// ResolveRefSvcParamDisplayName maps UUID values back to display_name when possible.
|
||||
func (c *UniversalClient) ResolveRefSvcParamDisplayName(ctx context.Context, refSvcId int, value string) (string, error) {
|
||||
if c == nil {
|
||||
return "", nil
|
||||
}
|
||||
if refSvcId <= 0 {
|
||||
return value, nil
|
||||
}
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" {
|
||||
return "", nil
|
||||
}
|
||||
if !isUUIDLike(trimmed) {
|
||||
return trimmed, nil
|
||||
}
|
||||
// Prefer direct lookup by instance UID to avoid paging the full instances list.
|
||||
displayName, err := c.getInstanceDisplayNameByUidRefSvc(ctx, refSvcId, trimmed)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if strings.TrimSpace(displayName) == "" {
|
||||
// Fallback to list scan if the direct lookup did not match by serviceId.
|
||||
displayName, err = c.findInstanceDisplayNameByUidRefSvc(ctx, refSvcId, trimmed)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if strings.TrimSpace(displayName) == "" {
|
||||
return trimmed, nil
|
||||
}
|
||||
}
|
||||
return displayName, nil
|
||||
}
|
||||
|
||||
// ListRefServiceInstances returns active instance options for a referenced service.
|
||||
func (c *UniversalClient) ListRefServiceInstances(ctx context.Context, serviceId int) ([]RefServiceInstanceOption, error) {
|
||||
if c == nil || serviceId <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var out []RefServiceInstanceOption
|
||||
page := 1
|
||||
for {
|
||||
// isDeleted=false — API-уровень фильтрации, не тратим трафик на удалённые инстансы
|
||||
path := fmt.Sprintf("/instances?page=%d&size=100&isDeleted=false", page)
|
||||
respBody, _, err := c.doRequest(ctx, "GET", path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var res struct {
|
||||
Results []struct {
|
||||
InstanceUid string `json:"instanceUid"`
|
||||
DisplayName string `json:"displayName"`
|
||||
ServiceId int `json:"serviceId"`
|
||||
IsDeleted bool `json:"isDeleted"`
|
||||
ExplainedStatus string `json:"explainedStatus"`
|
||||
} `json:"results"`
|
||||
}
|
||||
if err := json.Unmarshal(respBody, &res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(res.Results) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
for _, item := range res.Results {
|
||||
if item.ServiceId != serviceId {
|
||||
continue
|
||||
}
|
||||
// показываем только running инстансы — deleted/not_created не пригодны к использованию
|
||||
if item.IsDeleted || !strings.EqualFold(strings.TrimSpace(item.ExplainedStatus), "running") {
|
||||
continue
|
||||
}
|
||||
uid := strings.TrimSpace(item.InstanceUid)
|
||||
name := strings.TrimSpace(item.DisplayName)
|
||||
if uid == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, RefServiceInstanceOption{InstanceUID: uid, DisplayName: name})
|
||||
}
|
||||
|
||||
page++
|
||||
if page > 100 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ListAvailableResourceRealms returns realm names available for the target service.
|
||||
func (c *UniversalClient) ListAvailableResourceRealms(ctx context.Context, serviceId int) ([]string, error) {
|
||||
if c == nil || serviceId <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
respBody, _, err := c.doRequest(ctx, "GET", fmt.Sprintf("/resourceRealms/available?svcId=%d", serviceId), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Results json.RawMessage `json:"results"`
|
||||
}
|
||||
if err := json.Unmarshal(respBody, &payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parseArray := func(raw json.RawMessage) []string {
|
||||
var values []string
|
||||
_ = json.Unmarshal(raw, &values)
|
||||
return values
|
||||
}
|
||||
|
||||
parseString := func(raw json.RawMessage) []string {
|
||||
var single string
|
||||
if err := json.Unmarshal(raw, &single); err != nil {
|
||||
return nil
|
||||
}
|
||||
single = strings.TrimSpace(single)
|
||||
if single == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(single, ",")
|
||||
result := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
trimmed := strings.TrimSpace(part)
|
||||
if trimmed != "" {
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
}
|
||||
if len(result) == 0 {
|
||||
result = append(result, single)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
values := parseArray(payload.Results)
|
||||
if len(values) == 0 {
|
||||
values = parseString(payload.Results)
|
||||
}
|
||||
|
||||
out := make([]string, 0, len(values))
|
||||
seen := map[string]struct{}{}
|
||||
for _, v := range values {
|
||||
trimmed := strings.TrimSpace(v)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[trimmed]; ok {
|
||||
continue
|
||||
}
|
||||
seen[trimmed] = struct{}{}
|
||||
out = append(out, trimmed)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
Reference in New Issue
Block a user