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