1047 lines
36 KiB
Go
1047 lines
36 KiB
Go
package provider
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
|
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
|
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
|
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
|
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
|
"github.com/hashicorp/terraform-plugin-log/tflog"
|
|
)
|
|
|
|
var _ resource.Resource = &PostgresResource{}
|
|
var _ resource.ResourceWithImportState = &PostgresResource{}
|
|
var _ resource.ResourceWithModifyPlan = &PostgresResource{}
|
|
|
|
type PostgresResource struct {
|
|
client *NubesClient
|
|
}
|
|
|
|
var uuidLikeRegex = regexp.MustCompile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
|
|
|
|
func isUUIDLike(value string) bool {
|
|
trimmed := strings.TrimSpace(value)
|
|
if trimmed == "" {
|
|
return false
|
|
}
|
|
return uuidLikeRegex.MatchString(trimmed)
|
|
}
|
|
|
|
func (r *PostgresResource) resolveS3Uid(ctx context.Context, value string) (string, error) {
|
|
trimmed := strings.TrimSpace(value)
|
|
if trimmed == "" {
|
|
return "", nil
|
|
}
|
|
if isUUIDLike(trimmed) {
|
|
return trimmed, nil
|
|
}
|
|
|
|
return "", fmt.Errorf("s3_uid must be a UUID, got: %s", trimmed)
|
|
}
|
|
|
|
func NewPostgresResource() resource.Resource {
|
|
return &PostgresResource{}
|
|
}
|
|
|
|
type PostgresResourceModel struct {
|
|
ID types.String `tfsdk:"id"`
|
|
Name types.String `tfsdk:"name"`
|
|
ResourceRealm types.String `tfsdk:"resource_realm"`
|
|
S3Uid types.String `tfsdk:"s3_uid"`
|
|
Cpu types.Int64 `tfsdk:"cpu"`
|
|
Ram types.Int64 `tfsdk:"ram"`
|
|
DiskSize types.Int64 `tfsdk:"disk_size"`
|
|
NodesCount types.Int64 `tfsdk:"nodes_count"`
|
|
Version types.String `tfsdk:"version"`
|
|
BackupSchedule types.String `tfsdk:"backup_schedule"`
|
|
BackupRetention types.Int64 `tfsdk:"backup_retention"`
|
|
Parameters types.String `tfsdk:"parameters"`
|
|
EnablePgPoolerMaster types.Bool `tfsdk:"enable_pgpooler_master"`
|
|
EnablePgPoolerSlave types.Bool `tfsdk:"enable_pgpooler_slave"`
|
|
AllowNoSSL types.Bool `tfsdk:"allow_no_ssl"`
|
|
AutoScale types.Bool `tfsdk:"auto_scale"`
|
|
AutoScalePercentage types.Int64 `tfsdk:"auto_scale_percentage"`
|
|
AutoScaleTechWindow types.Int64 `tfsdk:"auto_scale_tech_window"`
|
|
AutoScaleQuotaGb types.Int64 `tfsdk:"auto_scale_quota_gb"`
|
|
EnableExternalMaster types.Bool `tfsdk:"enable_external_master"`
|
|
EnableExternalSlave types.Bool `tfsdk:"enable_external_slave"`
|
|
IpSpaceMaster types.String `tfsdk:"ip_space_master"`
|
|
IpSpaceSlave types.String `tfsdk:"ip_space_slave"`
|
|
DeletionProtection types.Bool `tfsdk:"deletion_protection"`
|
|
|
|
// Computed
|
|
AdminUser types.String `tfsdk:"admin_user"`
|
|
AdminPassword types.String `tfsdk:"admin_password"`
|
|
StandbyUser types.String `tfsdk:"standby_user"`
|
|
StandbyPassword types.String `tfsdk:"standby_password"`
|
|
InternalHost types.String `tfsdk:"internal_host"`
|
|
VaultUserPath types.String `tfsdk:"vault_user_path"`
|
|
VaultUrl types.String `tfsdk:"vault_url"`
|
|
MonitoringUrl types.String `tfsdk:"monitoring_url"`
|
|
InternalConnectMaster types.String `tfsdk:"internal_connect_master"`
|
|
InternalConnectSlave types.String `tfsdk:"internal_connect_slave"`
|
|
ExternalConnectMasterIp types.String `tfsdk:"external_connect_master_ip"`
|
|
ExternalConnectMasterFqdn types.String `tfsdk:"external_connect_master_fqdn"`
|
|
ExternalConnectSlaveIp types.String `tfsdk:"external_connect_slave_ip"`
|
|
ExternalConnectSlaveFqdn types.String `tfsdk:"external_connect_slave_fqdn"`
|
|
}
|
|
|
|
func (r *PostgresResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
|
resp.TypeName = req.ProviderTypeName + "_postgres"
|
|
}
|
|
|
|
func (r *PostgresResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
|
|
resp.Schema = schema.Schema{
|
|
Description: "Manages a Postgres cluster.",
|
|
Attributes: map[string]schema.Attribute{
|
|
"id": schema.StringAttribute{
|
|
Computed: true,
|
|
PlanModifiers: []planmodifier.String{
|
|
stringplanmodifier.UseStateForUnknown(),
|
|
},
|
|
},
|
|
"name": schema.StringAttribute{
|
|
Required: true,
|
|
Description: "Name of the Postgres instance",
|
|
PlanModifiers: []planmodifier.String{
|
|
stringplanmodifier.RequiresReplace(),
|
|
},
|
|
},
|
|
"resource_realm": schema.StringAttribute{
|
|
Required: true,
|
|
Description: "Deployment platform (param 102). Example: k8s-3.ext.nubes.ru",
|
|
PlanModifiers: []planmodifier.String{
|
|
stringplanmodifier.RequiresReplace(),
|
|
},
|
|
},
|
|
"s3_uid": schema.StringAttribute{
|
|
Required: true,
|
|
Description: "UID Корневой услуги S3 (Param 23). Только UUID, без displayName. Пример: 6d6061cb-b0c1-44b9-8969-a70f08fe673c",
|
|
Validators: []validator.String{
|
|
UUIDLike(),
|
|
},
|
|
PlanModifiers: []planmodifier.String{
|
|
stringplanmodifier.RequiresReplace(),
|
|
},
|
|
},
|
|
"cpu": schema.Int64Attribute{
|
|
Required: true,
|
|
Description: "CPU in millicores (param 82)",
|
|
},
|
|
"ram": schema.Int64Attribute{
|
|
Required: true,
|
|
Description: "Memory in MB (param 81)",
|
|
},
|
|
"disk_size": schema.Int64Attribute{
|
|
Required: true,
|
|
Description: "Disk size in GB (param 83)",
|
|
},
|
|
"nodes_count": schema.Int64Attribute{
|
|
Optional: true,
|
|
Computed: true,
|
|
Default: int64default.StaticInt64(1),
|
|
Description: "Number of instances (param 80)",
|
|
},
|
|
"version": schema.StringAttribute{
|
|
Optional: true,
|
|
Computed: true,
|
|
Default: stringdefault.StaticString("17"),
|
|
Description: "Postgres Version (param 310)",
|
|
},
|
|
"backup_schedule": schema.StringAttribute{
|
|
Optional: true,
|
|
Computed: true,
|
|
Default: stringdefault.StaticString("0 0 * * *"),
|
|
Description: "Backup Schedule (param 265)",
|
|
},
|
|
"backup_retention": schema.Int64Attribute{
|
|
Optional: true,
|
|
Computed: true,
|
|
Default: int64default.StaticInt64(7),
|
|
Description: "Backup Retention Days (param 266)",
|
|
},
|
|
"parameters": schema.StringAttribute{
|
|
Optional: true,
|
|
Computed: true,
|
|
Default: stringdefault.StaticString("{}"),
|
|
Description: "Postgres config as JSON string (param 311)",
|
|
},
|
|
"enable_pgpooler_master": schema.BoolAttribute{
|
|
Optional: true,
|
|
Computed: true,
|
|
Default: booldefault.StaticBool(false),
|
|
Description: "Enable pgPooler for Master (param 312)",
|
|
},
|
|
"enable_pgpooler_slave": schema.BoolAttribute{
|
|
Optional: true,
|
|
Computed: true,
|
|
Default: booldefault.StaticBool(false),
|
|
Description: "Enable pgPooler for Slave (param 313)",
|
|
},
|
|
"allow_no_ssl": schema.BoolAttribute{
|
|
Optional: true,
|
|
Computed: true,
|
|
Default: booldefault.StaticBool(false),
|
|
Description: "Allow No SSL (param 314)",
|
|
},
|
|
"auto_scale": schema.BoolAttribute{
|
|
Optional: true,
|
|
Computed: true,
|
|
Default: booldefault.StaticBool(false),
|
|
Description: "Enable AutoScale (param 329)",
|
|
},
|
|
"auto_scale_percentage": schema.Int64Attribute{
|
|
Optional: true,
|
|
Computed: true,
|
|
Default: int64default.StaticInt64(10),
|
|
Description: "AutoScale Percentage (param 330)",
|
|
},
|
|
"auto_scale_tech_window": schema.Int64Attribute{
|
|
Optional: true,
|
|
Computed: true,
|
|
Default: int64default.StaticInt64(0),
|
|
Description: "AutoScale Tech Window (param 331)",
|
|
},
|
|
"auto_scale_quota_gb": schema.Int64Attribute{
|
|
Optional: true,
|
|
Computed: true,
|
|
Default: int64default.StaticInt64(1),
|
|
Description: "AutoScale Quota GB (param 339)",
|
|
},
|
|
"enable_external_master": schema.BoolAttribute{
|
|
Optional: true,
|
|
Computed: true,
|
|
Default: booldefault.StaticBool(false),
|
|
Description: "Need External IP for Master (param 145)",
|
|
},
|
|
"enable_external_slave": schema.BoolAttribute{
|
|
Optional: true,
|
|
Computed: true,
|
|
Default: booldefault.StaticBool(false),
|
|
Description: "Need External IP for Slave (param 146)",
|
|
},
|
|
"ip_space_master": schema.StringAttribute{
|
|
Optional: true,
|
|
Description: "IP Space Name for Master (param 337)",
|
|
},
|
|
"ip_space_slave": schema.StringAttribute{
|
|
Optional: true,
|
|
Description: "IP Space Name for Slave (param 338)",
|
|
},
|
|
"deletion_protection": schema.BoolAttribute{
|
|
MarkdownDescription: "If true, the resource will only be removed from Terraform state upon destroy, but will remain in the cloud. If false, destroy will trigger 'suspend' in Nubes.",
|
|
Optional: true,
|
|
Computed: true,
|
|
Default: booldefault.StaticBool(true),
|
|
},
|
|
|
|
// Output
|
|
"admin_user": schema.StringAttribute{
|
|
Computed: true,
|
|
},
|
|
"admin_password": schema.StringAttribute{
|
|
Computed: true,
|
|
Sensitive: true,
|
|
},
|
|
"standby_user": schema.StringAttribute{
|
|
Computed: true,
|
|
},
|
|
"standby_password": schema.StringAttribute{
|
|
Computed: true,
|
|
Sensitive: true,
|
|
},
|
|
"internal_host": schema.StringAttribute{
|
|
Computed: true,
|
|
},
|
|
"vault_user_path": schema.StringAttribute{
|
|
Computed: true,
|
|
},
|
|
"vault_url": schema.StringAttribute{
|
|
Computed: true,
|
|
},
|
|
"monitoring_url": schema.StringAttribute{
|
|
Computed: true,
|
|
},
|
|
"internal_connect_master": schema.StringAttribute{
|
|
Computed: true,
|
|
},
|
|
"internal_connect_slave": schema.StringAttribute{
|
|
Computed: true,
|
|
},
|
|
"external_connect_master_ip": schema.StringAttribute{
|
|
Computed: true,
|
|
},
|
|
"external_connect_master_fqdn": schema.StringAttribute{
|
|
Computed: true,
|
|
},
|
|
"external_connect_slave_ip": schema.StringAttribute{
|
|
Computed: true,
|
|
},
|
|
"external_connect_slave_fqdn": schema.StringAttribute{
|
|
Computed: true,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func (r *PostgresResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
|
if req.ProviderData == nil {
|
|
return
|
|
}
|
|
client, ok := req.ProviderData.(*NubesClient)
|
|
if !ok {
|
|
resp.Diagnostics.AddError("Unexpected Resource Configure Type", fmt.Sprintf("Expected *NubesClient, got: %T", req.ProviderData))
|
|
return
|
|
}
|
|
r.client = client
|
|
}
|
|
|
|
func (r *PostgresResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
|
var plan PostgresResourceModel
|
|
diags := req.Plan.Get(ctx, &plan)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
tflog.Info(ctx, "Creating Postgres resource...")
|
|
|
|
// Attempt to resume suspended instance if exists
|
|
tflog.Info(ctx, "Checking for suspended instances to resume...")
|
|
var resumedInstanceUid string
|
|
instances, err := r.client.GetInstances(ctx)
|
|
if err != nil {
|
|
tflog.Warn(ctx, fmt.Sprintf("Failed to list instances: %s", err))
|
|
} else {
|
|
tflog.Info(ctx, fmt.Sprintf("Scanned %d instances.", len(instances)))
|
|
for _, inst := range instances {
|
|
if inst.DisplayName == plan.Name.ValueString() && inst.ServiceId == 90 {
|
|
tflog.Info(ctx, fmt.Sprintf("Found matching instance name: %s (%s)", inst.DisplayName, inst.InstanceUid))
|
|
details, err := r.client.GetInstanceStateDetails(ctx, inst.InstanceUid)
|
|
if err == nil {
|
|
isSuspended, _ := details["isSuspended"].(bool)
|
|
isDeleted, _ := details["isDeleted"].(bool)
|
|
|
|
tflog.Info(ctx, fmt.Sprintf("Instance status isSuspended: %v, isDeleted: %v", isSuspended, isDeleted))
|
|
|
|
if isSuspended {
|
|
tflog.Info(ctx, fmt.Sprintf("Found suspended instance %s. Resuming...", inst.InstanceUid))
|
|
if err := r.client.RunAction(ctx, inst.InstanceUid, "resume"); err != nil {
|
|
resp.Diagnostics.AddError("Resume Failed", err.Error())
|
|
return
|
|
}
|
|
// Postgres goes to 'running' state, not 'active'
|
|
if err := r.client.WaitForInstanceStatus(ctx, inst.InstanceUid, "running"); err != nil {
|
|
resp.Diagnostics.AddError("Wait for Resume Failed", err.Error())
|
|
return
|
|
}
|
|
resumedInstanceUid = inst.InstanceUid
|
|
} else if !isDeleted {
|
|
tflog.Info(ctx, fmt.Sprintf("Instance %s is already active. Adopting...", inst.InstanceUid))
|
|
resumedInstanceUid = inst.InstanceUid
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
resolvedS3Uid, err := r.resolveS3Uid(ctx, plan.S3Uid.ValueString())
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("Failed to resolve s3_uid", err.Error())
|
|
return
|
|
}
|
|
if resolvedS3Uid != "" {
|
|
plan.S3Uid = types.StringValue(resolvedS3Uid)
|
|
}
|
|
|
|
params := []InstanceParam{
|
|
{SvcOperationCfsParamId: 102, ParamValue: plan.ResourceRealm.ValueString()},
|
|
{SvcOperationCfsParamId: 23, ParamValue: plan.S3Uid.ValueString()},
|
|
{SvcOperationCfsParamId: 82, ParamValue: fmt.Sprintf("%d", plan.Cpu.ValueInt64())},
|
|
{SvcOperationCfsParamId: 81, ParamValue: fmt.Sprintf("%d", plan.Ram.ValueInt64())},
|
|
{SvcOperationCfsParamId: 83, ParamValue: fmt.Sprintf("%d", plan.DiskSize.ValueInt64())},
|
|
{SvcOperationCfsParamId: 80, ParamValue: fmt.Sprintf("%d", plan.NodesCount.ValueInt64())},
|
|
{SvcOperationCfsParamId: 266, ParamValue: fmt.Sprintf("%d", plan.BackupRetention.ValueInt64())},
|
|
{SvcOperationCfsParamId: 265, ParamValue: plan.BackupSchedule.ValueString()},
|
|
{SvcOperationCfsParamId: 310, ParamValue: plan.Version.ValueString()},
|
|
{SvcOperationCfsParamId: 311, ParamValue: plan.Parameters.ValueString()},
|
|
{SvcOperationCfsParamId: 312, ParamValue: strconv.FormatBool(plan.EnablePgPoolerMaster.ValueBool())},
|
|
{SvcOperationCfsParamId: 313, ParamValue: strconv.FormatBool(plan.EnablePgPoolerSlave.ValueBool())},
|
|
{SvcOperationCfsParamId: 314, ParamValue: strconv.FormatBool(plan.AllowNoSSL.ValueBool())},
|
|
{SvcOperationCfsParamId: 329, ParamValue: strconv.FormatBool(plan.AutoScale.ValueBool())},
|
|
{SvcOperationCfsParamId: 330, ParamValue: fmt.Sprintf("%d", plan.AutoScalePercentage.ValueInt64())},
|
|
{SvcOperationCfsParamId: 331, ParamValue: fmt.Sprintf("%d", plan.AutoScaleTechWindow.ValueInt64())},
|
|
{SvcOperationCfsParamId: 339, ParamValue: fmt.Sprintf("%d", plan.AutoScaleQuotaGb.ValueInt64())},
|
|
{SvcOperationCfsParamId: 145, ParamValue: strconv.FormatBool(plan.EnableExternalMaster.ValueBool())},
|
|
{SvcOperationCfsParamId: 146, ParamValue: strconv.FormatBool(plan.EnableExternalSlave.ValueBool())},
|
|
}
|
|
|
|
if !plan.IpSpaceMaster.IsNull() {
|
|
params = append(params, InstanceParam{SvcOperationCfsParamId: 337, ParamValue: plan.IpSpaceMaster.ValueString()})
|
|
} else {
|
|
params = append(params, InstanceParam{SvcOperationCfsParamId: 337, ParamValue: ""})
|
|
}
|
|
if !plan.IpSpaceSlave.IsNull() {
|
|
params = append(params, InstanceParam{SvcOperationCfsParamId: 338, ParamValue: plan.IpSpaceSlave.ValueString()})
|
|
} else {
|
|
params = append(params, InstanceParam{SvcOperationCfsParamId: 338, ParamValue: ""})
|
|
}
|
|
|
|
serviceId := 90
|
|
var instanceUid string
|
|
|
|
if resumedInstanceUid != "" {
|
|
instanceUid = resumedInstanceUid
|
|
} else {
|
|
svcOperationId, err := r.client.GetOperationId(ctx, serviceId, "create")
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("Failed to get create operation ID", err.Error())
|
|
return
|
|
}
|
|
|
|
displayName := plan.Name.ValueString()
|
|
var opUid string
|
|
instanceUid, opUid, err = r.client.CreateInstance(ctx, displayName, serviceId, svcOperationId, params)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("Error creating Postgres", err.Error())
|
|
return
|
|
}
|
|
|
|
// Wait for operation success
|
|
err = r.client.WaitForOperation(ctx, opUid)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("Error waiting for Postgres creation", err.Error())
|
|
return
|
|
}
|
|
}
|
|
|
|
plan.ID = types.StringValue(instanceUid)
|
|
|
|
// Initialize computed fields to avoid "Unknown" errors if API doesn't return them
|
|
plan.AdminUser = types.StringNull()
|
|
plan.AdminPassword = types.StringNull()
|
|
plan.StandbyUser = types.StringNull()
|
|
plan.StandbyPassword = types.StringNull()
|
|
plan.InternalHost = types.StringNull()
|
|
plan.VaultUserPath = types.StringNull()
|
|
plan.VaultUrl = types.StringNull()
|
|
plan.MonitoringUrl = types.StringNull()
|
|
plan.InternalConnectMaster = types.StringNull()
|
|
plan.InternalConnectSlave = types.StringNull()
|
|
plan.ExternalConnectMasterIp = types.StringNull()
|
|
plan.ExternalConnectMasterFqdn = types.StringNull()
|
|
plan.ExternalConnectSlaveIp = types.StringNull()
|
|
plan.ExternalConnectSlaveFqdn = types.StringNull()
|
|
|
|
// Fetch details to populate Computed fields
|
|
stateData, err := r.client.GetInstanceStateDetails(ctx, instanceUid)
|
|
if err != nil {
|
|
resp.Diagnostics.AddWarning("Failed to populate computed fields", err.Error())
|
|
} else {
|
|
// Vault
|
|
if vault, ok := stateData["vault"].(map[string]interface{}); ok {
|
|
if val, ok := vault["userPath"]; ok {
|
|
plan.VaultUserPath = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
if val, ok := vault["url"]; ok {
|
|
plan.VaultUrl = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
}
|
|
|
|
// Out (Connect details)
|
|
if out, ok := stateData["out"].(map[string]interface{}); ok {
|
|
// Credentials
|
|
if val, ok := out["adminUser"]; ok {
|
|
plan.AdminUser = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
if val, ok := out["adminPass"]; ok {
|
|
plan.AdminPassword = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
if val, ok := out["standbyUser"]; ok {
|
|
plan.StandbyUser = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
if val, ok := out["standbyPass"]; ok {
|
|
plan.StandbyPassword = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
|
|
// Monitoring
|
|
if monitoring, ok := out["monitoring"].(map[string]interface{}); ok {
|
|
if val, ok := monitoring["allDashboards"]; ok {
|
|
plan.MonitoringUrl = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
}
|
|
|
|
// Internal Connect
|
|
if internalConnect, ok := out["internalConnect"].(map[string]interface{}); ok {
|
|
if val, ok := internalConnect["master"]; ok && val != "" {
|
|
plan.InternalConnectMaster = types.StringValue(fmt.Sprintf("%v", val))
|
|
plan.InternalHost = types.StringValue(fmt.Sprintf("%v", val)) // Sync with master for now
|
|
}
|
|
if val, ok := internalConnect["slave"]; ok && val != "" {
|
|
plan.InternalConnectSlave = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
}
|
|
|
|
// External Connect
|
|
if externalConnect, ok := out["externalConnect"].(map[string]interface{}); ok {
|
|
if master, ok := externalConnect["master"].(map[string]interface{}); ok {
|
|
if val, ok := master["ip"]; ok && val != "" {
|
|
plan.ExternalConnectMasterIp = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
if val, ok := master["fqdn"]; ok && val != "" {
|
|
plan.ExternalConnectMasterFqdn = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
}
|
|
if slave, ok := externalConnect["slave"].(map[string]interface{}); ok {
|
|
if val, ok := slave["ip"]; ok && val != "" {
|
|
plan.ExternalConnectSlaveIp = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
if val, ok := slave["fqdn"]; ok && val != "" {
|
|
plan.ExternalConnectSlaveFqdn = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
diags = resp.State.Set(ctx, plan)
|
|
resp.Diagnostics.Append(diags...)
|
|
}
|
|
|
|
func (r *PostgresResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
|
var state PostgresResourceModel
|
|
diags := req.State.Get(ctx, &state)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
if !state.S3Uid.IsNull() && !state.S3Uid.IsUnknown() {
|
|
resolvedS3Uid, err := r.resolveS3Uid(ctx, state.S3Uid.ValueString())
|
|
if err == nil && resolvedS3Uid != "" {
|
|
state.S3Uid = types.StringValue(resolvedS3Uid)
|
|
} else if err != nil {
|
|
resp.Diagnostics.AddWarning("Failed to resolve s3_uid", err.Error())
|
|
}
|
|
}
|
|
|
|
stateData, err := r.client.GetInstanceStateDetails(ctx, state.ID.ValueString())
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("Error reading Postgres", err.Error())
|
|
return
|
|
}
|
|
|
|
// Update calculated fields
|
|
// Vault
|
|
if vault, ok := stateData["vault"].(map[string]interface{}); ok {
|
|
if val, ok := vault["userPath"]; ok {
|
|
state.VaultUserPath = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
if val, ok := vault["url"]; ok {
|
|
state.VaultUrl = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
}
|
|
|
|
// Out
|
|
if out, ok := stateData["out"].(map[string]interface{}); ok {
|
|
// Credentials
|
|
if val, ok := out["adminUser"]; ok {
|
|
state.AdminUser = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
if val, ok := out["adminPass"]; ok {
|
|
state.AdminPassword = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
if val, ok := out["standbyUser"]; ok {
|
|
state.StandbyUser = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
if val, ok := out["standbyPass"]; ok {
|
|
state.StandbyPassword = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
|
|
// Monitoring
|
|
if monitoring, ok := out["monitoring"].(map[string]interface{}); ok {
|
|
if val, ok := monitoring["allDashboards"]; ok {
|
|
state.MonitoringUrl = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
}
|
|
|
|
// Internal Connect
|
|
if internalConnect, ok := out["internalConnect"].(map[string]interface{}); ok {
|
|
if val, ok := internalConnect["master"]; ok && val != "" {
|
|
state.InternalConnectMaster = types.StringValue(fmt.Sprintf("%v", val))
|
|
state.InternalHost = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
if val, ok := internalConnect["slave"]; ok && val != "" {
|
|
state.InternalConnectSlave = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
}
|
|
|
|
// External Connect
|
|
if externalConnect, ok := out["externalConnect"].(map[string]interface{}); ok {
|
|
if master, ok := externalConnect["master"].(map[string]interface{}); ok {
|
|
if val, ok := master["ip"]; ok && val != "" {
|
|
state.ExternalConnectMasterIp = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
if val, ok := master["fqdn"]; ok && val != "" {
|
|
state.ExternalConnectMasterFqdn = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
}
|
|
if slave, ok := externalConnect["slave"].(map[string]interface{}); ok {
|
|
if val, ok := slave["ip"]; ok && val != "" {
|
|
state.ExternalConnectSlaveIp = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
if val, ok := slave["fqdn"]; ok && val != "" {
|
|
state.ExternalConnectSlaveFqdn = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
diags = resp.State.Set(ctx, &state)
|
|
resp.Diagnostics.Append(diags...)
|
|
}
|
|
|
|
func (r *PostgresResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
|
var plan, state PostgresResourceModel
|
|
diags := req.Plan.Get(ctx, &plan)
|
|
resp.Diagnostics.Append(diags...)
|
|
diags = req.State.Get(ctx, &state)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
tflog.Info(ctx, "Updating Postgres resource...")
|
|
|
|
// Validation: Verify Disk Size reduction
|
|
if plan.DiskSize.ValueInt64() < state.DiskSize.ValueInt64() {
|
|
resp.Diagnostics.AddError(
|
|
"Invalid Disk Upgrade",
|
|
fmt.Sprintf("Decreasing disk size is not supported (Current: %d GB, Requested: %d GB)", state.DiskSize.ValueInt64(), plan.DiskSize.ValueInt64()),
|
|
)
|
|
return
|
|
}
|
|
|
|
// Find operation ID for 'modify'
|
|
opId, err := r.client.GetOperationIdForInstance(ctx, state.ID.ValueString(), "modify")
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("Failed to find update operation", err.Error())
|
|
return
|
|
}
|
|
|
|
// Create Operation
|
|
opPayload := map[string]interface{}{
|
|
"instanceUid": state.ID.ValueString(),
|
|
"svcOperationId": opId,
|
|
"operation": "modify",
|
|
}
|
|
|
|
var opUid string
|
|
opUid, err = r.client.postIgnoreResponse(ctx, "/instanceOperations", opPayload, true)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("Failed to create update operation", err.Error())
|
|
return
|
|
}
|
|
if opUid == "" {
|
|
resp.Diagnostics.AddError("Failed to get operation UID", "Empty UID returned")
|
|
return
|
|
}
|
|
|
|
// Fetch Op details to get correct CFS Param IDs for 'modify'
|
|
opDetails, err := r.client.GetInstanceOperation(ctx, opUid)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("Failed to fetch operation details", err.Error())
|
|
return
|
|
}
|
|
|
|
paramMapping := make(map[string]int)
|
|
for _, p := range opDetails.CfsParams {
|
|
paramMapping[p.SvcOperationCfsParam] = p.SvcOperationCfsParamId
|
|
}
|
|
|
|
type NamedParam struct {
|
|
Name string
|
|
Value string
|
|
}
|
|
namedParams := []NamedParam{
|
|
{"resourceCPU", fmt.Sprintf("%d", plan.Cpu.ValueInt64())},
|
|
{"resourceMemory", fmt.Sprintf("%d", plan.Ram.ValueInt64())},
|
|
{"resourceDisk", fmt.Sprintf("%d", plan.DiskSize.ValueInt64())},
|
|
{"resourceInstances", fmt.Sprintf("%d", plan.NodesCount.ValueInt64())},
|
|
{"ext_BACKUP_NUM_TO_RETAIN", fmt.Sprintf("%d", plan.BackupRetention.ValueInt64())},
|
|
{"ext_BACKUP_SCHEDULE", plan.BackupSchedule.ValueString()},
|
|
{"appVersion", plan.Version.ValueString()},
|
|
{"jsonParameters", plan.Parameters.ValueString()},
|
|
{"enablePgPoolerMaster", strconv.FormatBool(plan.EnablePgPoolerMaster.ValueBool())},
|
|
{"enablePgPoolerSlave", strconv.FormatBool(plan.EnablePgPoolerSlave.ValueBool())},
|
|
{"allowNoSSL", strconv.FormatBool(plan.AllowNoSSL.ValueBool())},
|
|
{"autoScale", strconv.FormatBool(plan.AutoScale.ValueBool())},
|
|
{"autoScalePercentage", fmt.Sprintf("%d", plan.AutoScalePercentage.ValueInt64())},
|
|
{"autoScaleTechWindow", fmt.Sprintf("%d", plan.AutoScaleTechWindow.ValueInt64())},
|
|
{"autoScaleQuotaGb", fmt.Sprintf("%d", plan.AutoScaleQuotaGb.ValueInt64())},
|
|
{"needExternalAddressMaster", strconv.FormatBool(plan.EnableExternalMaster.ValueBool())},
|
|
{"needExternalAddressSlave", strconv.FormatBool(plan.EnableExternalSlave.ValueBool())},
|
|
}
|
|
|
|
if !plan.IpSpaceMaster.IsNull() {
|
|
namedParams = append(namedParams, NamedParam{"ipSpaceNameMaster", plan.IpSpaceMaster.ValueString()})
|
|
} else {
|
|
namedParams = append(namedParams, NamedParam{"ipSpaceNameMaster", ""})
|
|
}
|
|
if !plan.IpSpaceSlave.IsNull() {
|
|
namedParams = append(namedParams, NamedParam{"ipSpaceNameSlave", plan.IpSpaceSlave.ValueString()})
|
|
} else {
|
|
namedParams = append(namedParams, NamedParam{"ipSpaceNameSlave", ""})
|
|
}
|
|
|
|
// Add Params using dynamic mapping
|
|
for _, np := range namedParams {
|
|
id, ok := paramMapping[np.Name]
|
|
if !ok {
|
|
tflog.Debug(ctx, fmt.Sprintf("Param %s not supported for this operation, skipping", np.Name))
|
|
continue
|
|
}
|
|
|
|
paramPayload := map[string]interface{}{
|
|
"instanceOperationUid": opUid,
|
|
"svcOperationCfsParamId": id,
|
|
"paramValue": np.Value,
|
|
}
|
|
if _, err := r.client.postIgnoreResponse(ctx, "/instanceOperationCfsParams", paramPayload, false); err != nil {
|
|
resp.Diagnostics.AddError(fmt.Sprintf("Failed to add param %s (%d)", np.Name, id), err.Error())
|
|
return
|
|
}
|
|
}
|
|
|
|
// Run
|
|
runUrl := fmt.Sprintf("/instanceOperations/%s/run", opUid)
|
|
if _, err := r.client.postIgnoreResponse(ctx, runUrl, map[string]interface{}{}, false); err != nil {
|
|
resp.Diagnostics.AddError("Failed to run update", err.Error())
|
|
return
|
|
}
|
|
|
|
// Wait for operation success
|
|
err = r.client.WaitForOperation(ctx, opUid)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("Error waiting for Postgres update", err.Error())
|
|
return
|
|
}
|
|
|
|
// Fetch details to populate Computed fields
|
|
stateData, err := r.client.GetInstanceStateDetails(ctx, state.ID.ValueString())
|
|
if err != nil {
|
|
tflog.Warn(ctx, fmt.Sprintf("Failed to refresh state after update: %s", err))
|
|
}
|
|
|
|
// Initialize all computed fields to avoid 'unknown value' errors after apply
|
|
plan.AdminUser = types.StringNull()
|
|
plan.AdminPassword = types.StringNull()
|
|
plan.StandbyUser = types.StringNull()
|
|
plan.StandbyPassword = types.StringNull()
|
|
plan.VaultUserPath = types.StringNull()
|
|
plan.VaultUrl = types.StringNull()
|
|
plan.MonitoringUrl = types.StringNull()
|
|
plan.InternalConnectMaster = types.StringNull()
|
|
plan.InternalHost = types.StringNull()
|
|
plan.InternalConnectSlave = types.StringNull()
|
|
plan.ExternalConnectMasterIp = types.StringNull()
|
|
plan.ExternalConnectMasterFqdn = types.StringNull()
|
|
plan.ExternalConnectSlaveIp = types.StringNull()
|
|
plan.ExternalConnectSlaveFqdn = types.StringNull()
|
|
|
|
if err == nil {
|
|
// Vault
|
|
if vault, ok := stateData["vault"].(map[string]interface{}); ok {
|
|
if val, ok := vault["userPath"]; ok {
|
|
plan.VaultUserPath = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
if val, ok := vault["url"]; ok {
|
|
plan.VaultUrl = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
}
|
|
|
|
// Out (Connect details)
|
|
if out, ok := stateData["out"].(map[string]interface{}); ok {
|
|
// Credentials
|
|
if val, ok := out["adminUser"]; ok {
|
|
plan.AdminUser = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
if val, ok := out["adminPass"]; ok {
|
|
plan.AdminPassword = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
if val, ok := out["standbyUser"]; ok {
|
|
plan.StandbyUser = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
if val, ok := out["standbyPass"]; ok {
|
|
plan.StandbyPassword = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
|
|
// Monitoring
|
|
if monitoring, ok := out["monitoring"].(map[string]interface{}); ok {
|
|
if val, ok := monitoring["allDashboards"]; ok {
|
|
plan.MonitoringUrl = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
}
|
|
|
|
// Internal Connect
|
|
if internalConnect, ok := out["internalConnect"].(map[string]interface{}); ok {
|
|
if val, ok := internalConnect["master"]; ok && val != "" {
|
|
plan.InternalConnectMaster = types.StringValue(fmt.Sprintf("%v", val))
|
|
plan.InternalHost = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
if val, ok := internalConnect["slave"]; ok && val != "" {
|
|
plan.InternalConnectSlave = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
}
|
|
|
|
// External Connect
|
|
if externalConnect, ok := out["externalConnect"].(map[string]interface{}); ok {
|
|
if master, ok := externalConnect["master"].(map[string]interface{}); ok {
|
|
if val, ok := master["ip"]; ok && val != "" {
|
|
plan.ExternalConnectMasterIp = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
if val, ok := master["fqdn"]; ok && val != "" {
|
|
plan.ExternalConnectMasterFqdn = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
}
|
|
if slave, ok := externalConnect["slave"].(map[string]interface{}); ok {
|
|
if val, ok := slave["ip"]; ok && val != "" {
|
|
plan.ExternalConnectSlaveIp = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
if val, ok := slave["fqdn"]; ok && val != "" {
|
|
plan.ExternalConnectSlaveFqdn = types.StringValue(fmt.Sprintf("%v", val))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// fix: plan.ID is Computed (empty in plan), must preserve existing ID from state
|
|
plan.ID = state.ID
|
|
|
|
diags = resp.State.Set(ctx, plan)
|
|
resp.Diagnostics.Append(diags...)
|
|
}
|
|
|
|
func (r *PostgresResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
|
|
if r.client == nil {
|
|
return
|
|
}
|
|
var plan PostgresResourceModel
|
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
if plan.S3Uid.IsNull() || plan.S3Uid.IsUnknown() {
|
|
return
|
|
}
|
|
|
|
resolvedS3Uid, err := r.resolveS3Uid(ctx, plan.S3Uid.ValueString())
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("Invalid s3_uid", err.Error())
|
|
return
|
|
}
|
|
if resolvedS3Uid == "" || resolvedS3Uid == plan.S3Uid.ValueString() {
|
|
return
|
|
}
|
|
|
|
plan.S3Uid = types.StringValue(resolvedS3Uid)
|
|
resp.Diagnostics.Append(resp.Plan.Set(ctx, &plan)...)
|
|
}
|
|
|
|
func (r *PostgresResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
|
var data PostgresResourceModel
|
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
if data.DeletionProtection.ValueBool() {
|
|
tflog.Warn(ctx, "Deletion Protection is ENABLED for Postgres. Resource will remain active in Nubes Cloud. Manual cleanup required.")
|
|
return
|
|
}
|
|
|
|
tflog.Info(ctx, "Deletion Protection is DISABLED for Postgres. Triggering 'suspend'...")
|
|
|
|
instanceId := data.ID.ValueString()
|
|
|
|
// Выполняем операцию suspend
|
|
// Для Postgres используем тот же механизм Instance Run что и для VDC
|
|
err := r.triggerSuspend(ctx, instanceId)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to suspend Postgres: %s", err))
|
|
return
|
|
}
|
|
|
|
tflog.Info(ctx, "Postgres suspended successfully. It will be permanently deleted from the cloud in 14 days.")
|
|
}
|
|
|
|
func (r *PostgresResource) triggerSuspend(ctx context.Context, instanceId string) error {
|
|
// Выполняем операцию suspend через новый унифицированный метод
|
|
err := r.client.RunAction(ctx, instanceId, "suspend")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Ждем пока статус изменится на suspended
|
|
return r.client.WaitForInstanceStatus(ctx, instanceId, "suspended")
|
|
}
|
|
|
|
func (r *PostgresResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
|
// Retrieve the full instance data
|
|
inst, err := r.client.GetInstanceFull(ctx, req.ID)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("Failed to fetch instance for import", err.Error())
|
|
return
|
|
}
|
|
|
|
var state PostgresResourceModel
|
|
state.ID = types.StringValue(req.ID)
|
|
|
|
// Top-level fields
|
|
if v, ok := inst["displayName"].(string); ok {
|
|
state.Name = types.StringValue(v)
|
|
}
|
|
if v, ok := inst["resourceRealm"].(string); ok {
|
|
state.ResourceRealm = types.StringValue(v)
|
|
}
|
|
|
|
// State params
|
|
if stateMap, ok := inst["state"].(map[string]interface{}); ok {
|
|
if params, ok := stateMap["params"].(map[string]interface{}); ok {
|
|
|
|
// String or Int handling
|
|
toInt64 := func(v interface{}) int64 {
|
|
switch val := v.(type) {
|
|
case float64:
|
|
return int64(val)
|
|
case int:
|
|
return int64(val)
|
|
case string:
|
|
if i, err := strconv.ParseInt(val, 10, 64); err == nil {
|
|
return i
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
if v, ok := params["resourceCPU"]; ok {
|
|
state.Cpu = types.Int64Value(toInt64(v))
|
|
}
|
|
if v, ok := params["resourceMemory"]; ok {
|
|
state.Ram = types.Int64Value(toInt64(v))
|
|
}
|
|
if v, ok := params["resourceDisk"]; ok {
|
|
state.DiskSize = types.Int64Value(toInt64(v))
|
|
}
|
|
if v, ok := params["resourceInstances"]; ok {
|
|
state.NodesCount = types.Int64Value(toInt64(v))
|
|
}
|
|
|
|
if v, ok := params["s3Uid"]; ok {
|
|
raw := fmt.Sprintf("%v", v)
|
|
resolvedS3Uid, err := r.resolveS3Uid(ctx, raw)
|
|
if err != nil {
|
|
resp.Diagnostics.AddWarning("Failed to resolve s3_uid", err.Error())
|
|
state.S3Uid = types.StringValue(raw)
|
|
} else if resolvedS3Uid != "" {
|
|
state.S3Uid = types.StringValue(resolvedS3Uid)
|
|
}
|
|
}
|
|
if v, ok := params["appVersion"]; ok {
|
|
state.Version = types.StringValue(fmt.Sprintf("%v", v))
|
|
}
|
|
|
|
if v, ok := params["ext_BACKUP_SCHEDULE"]; ok {
|
|
state.BackupSchedule = types.StringValue(fmt.Sprintf("%v", v))
|
|
}
|
|
if v, ok := params["ext_BACKUP_NUM_TO_RETAIN"]; ok {
|
|
state.BackupRetention = types.Int64Value(toInt64(v))
|
|
}
|
|
|
|
// Booleans
|
|
toBool := func(v interface{}) bool {
|
|
switch val := v.(type) {
|
|
case bool:
|
|
return val
|
|
case string:
|
|
return val == "true"
|
|
}
|
|
return false
|
|
}
|
|
|
|
if v, ok := params["enablePgPoolerMaster"]; ok {
|
|
state.EnablePgPoolerMaster = types.BoolValue(toBool(v))
|
|
}
|
|
if v, ok := params["enablePgPoolerSlave"]; ok {
|
|
state.EnablePgPoolerSlave = types.BoolValue(toBool(v))
|
|
}
|
|
if v, ok := params["allowNoSSL"]; ok {
|
|
state.AllowNoSSL = types.BoolValue(toBool(v))
|
|
}
|
|
if v, ok := params["autoScale"]; ok {
|
|
state.AutoScale = types.BoolValue(toBool(v))
|
|
}
|
|
if v, ok := params["needExternalAddressMaster"]; ok {
|
|
state.EnableExternalMaster = types.BoolValue(toBool(v))
|
|
}
|
|
if v, ok := params["needExternalAddressSlave"]; ok {
|
|
state.EnableExternalSlave = types.BoolValue(toBool(v))
|
|
}
|
|
|
|
if v, ok := params["ipSpaceNameMaster"]; ok {
|
|
if s := fmt.Sprintf("%v", v); s != "" {
|
|
state.IpSpaceMaster = types.StringValue(s)
|
|
}
|
|
}
|
|
if v, ok := params["ipSpaceNameSlave"]; ok {
|
|
if s := fmt.Sprintf("%v", v); s != "" {
|
|
state.IpSpaceSlave = types.StringValue(s)
|
|
}
|
|
}
|
|
|
|
// Autoscale details
|
|
if v, ok := params["autoScalePercentage"]; ok {
|
|
state.AutoScalePercentage = types.Int64Value(toInt64(v))
|
|
}
|
|
if v, ok := params["autoScaleTechWindow"]; ok {
|
|
state.AutoScaleTechWindow = types.Int64Value(toInt64(v))
|
|
}
|
|
if v, ok := params["autoScaleQuotaGb"]; ok {
|
|
state.AutoScaleQuotaGb = types.Int64Value(toInt64(v))
|
|
}
|
|
|
|
// JSON Parameters (map -> json string)
|
|
// Нормализуем через json.Compact чтобы совпадало с планом (jsonencode тоже compact)
|
|
if v, ok := params["jsonParameters"]; ok {
|
|
if jsonBytes, err := json.Marshal(v); err == nil {
|
|
var buf bytes.Buffer
|
|
if err2 := json.Compact(&buf, jsonBytes); err2 == nil {
|
|
state.Parameters = types.StringValue(buf.String())
|
|
} else {
|
|
state.Parameters = types.StringValue(string(jsonBytes))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
state.DeletionProtection = types.BoolValue(true)
|
|
|
|
diags := resp.State.Set(ctx, &state)
|
|
resp.Diagnostics.Append(diags...)
|
|
}
|