Files
tf_provider/internal/provider/pgadmin_resource.go
T

360 lines
11 KiB
Go

package provider
import (
"context"
"fmt"
"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/path"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
)
var _ resource.Resource = &PgAdminResource{}
var _ resource.ResourceWithImportState = &PgAdminResource{}
type PgAdminResource struct {
client *NubesClient
}
func NewPgAdminResource() resource.Resource {
return &PgAdminResource{}
}
type PgAdminResourceModel struct {
ID types.String `tfsdk:"id"`
Domain types.String `tfsdk:"domain"`
ResourceRealm types.String `tfsdk:"resource_realm"`
Cpu types.Int64 `tfsdk:"cpu"`
Memory types.Int64 `tfsdk:"memory"`
Disk types.Int64 `tfsdk:"disk"`
Email types.String `tfsdk:"email"`
Password types.String `tfsdk:"password"`
DeletionProtection types.Bool `tfsdk:"deletion_protection"`
}
func (r *PgAdminResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_pgadmin"
}
func (r *PgAdminResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Manages a pgAdmin instance.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"domain": schema.StringAttribute{
Required: true,
Description: "Domain name (param 164)",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"resource_realm": schema.StringAttribute{
Required: true,
Description: "Deployment platform (param 169). E.g. k8s-3.ext.nubes.ru",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"cpu": schema.Int64Attribute{
Optional: true,
Computed: true,
Default: int64default.StaticInt64(200),
Description: "CPU quota in milicores (param 165)",
},
"memory": schema.Int64Attribute{
Optional: true,
Computed: true,
Default: int64default.StaticInt64(256),
Description: "Memory quota in MB (param 166)",
},
"disk": schema.Int64Attribute{
Optional: true,
Computed: true,
Default: int64default.StaticInt64(1),
Description: "Disk size in GB (param 167)",
},
"email": schema.StringAttribute{
Required: true,
Description: "Login email (param 170)",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"password": schema.StringAttribute{
Required: true,
Sensitive: true,
Description: "Login password (param 171)",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"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),
},
},
}
}
func (r *PgAdminResource) 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 *PgAdminResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan PgAdminResourceModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Creating pgAdmin resource...")
// Params based on pg_admin.har analysis
params := []InstanceParam{
{SvcOperationCfsParamId: 164, ParamValue: plan.Domain.ValueString()},
{SvcOperationCfsParamId: 169, ParamValue: plan.ResourceRealm.ValueString()},
{SvcOperationCfsParamId: 165, ParamValue: fmt.Sprintf("%d", plan.Cpu.ValueInt64())},
{SvcOperationCfsParamId: 166, ParamValue: fmt.Sprintf("%d", plan.Memory.ValueInt64())},
{SvcOperationCfsParamId: 167, ParamValue: fmt.Sprintf("%d", plan.Disk.ValueInt64())},
{SvcOperationCfsParamId: 170, ParamValue: plan.Email.ValueString()},
{SvcOperationCfsParamId: 171, ParamValue: plan.Password.ValueString()},
}
serviceId := 96
svcOperationId, err := r.client.GetOperationId(ctx, serviceId, "create")
if err != nil {
resp.Diagnostics.AddError("Failed to get create operation ID for pgAdmin", err.Error())
return
}
instanceUid, opUid, err := r.client.CreateInstance(ctx, plan.Domain.ValueString(), serviceId, svcOperationId, params)
if err != nil {
resp.Diagnostics.AddError("Error creating pgAdmin", err.Error())
return
}
err = r.client.WaitForOperation(ctx, opUid)
if err != nil {
resp.Diagnostics.AddError("Error waiting for pgAdmin ready", err.Error())
return
}
plan.ID = types.StringValue(instanceUid)
// plan.DeletionProtection = types.BoolValue(true)
diags = resp.State.Set(ctx, plan)
resp.Diagnostics.Append(diags...)
}
func (r *PgAdminResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state PgAdminResourceModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
instanceUid := state.ID.ValueString()
// Fetch current state
inst, err := r.client.GetInstanceState(ctx, instanceUid)
if err != nil {
// If 404, remove from state
if strings.Contains(err.Error(), "status 404") {
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError("Error reading pgAdmin state", err.Error())
return
}
// Map params
if inst.State != nil && inst.State.Params != nil {
for k, v := range inst.State.Params {
valStr := fmt.Sprintf("%v", v)
// Log for debugging
tflog.Info(ctx, fmt.Sprintf("PGAdmin Param: %s = %s", k, valStr))
switch k {
case "resourceCPU":
val, _ := strconv.ParseInt(valStr, 10, 64)
state.Cpu = types.Int64Value(val)
case "resourceMemory":
val, _ := strconv.ParseInt(valStr, 10, 64)
state.Memory = types.Int64Value(val)
case "resourceDisk":
val, _ := strconv.ParseInt(valStr, 10, 64)
state.Disk = types.Int64Value(val)
case "domain":
state.Domain = types.StringValue(valStr)
case "resourceRealm":
state.ResourceRealm = types.StringValue(valStr)
case "login":
state.Email = types.StringValue(valStr)
}
}
}
diags = resp.State.Set(ctx, &state)
resp.Diagnostics.Append(diags...)
}
func (r *PgAdminResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan, state PgAdminResourceModel
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 pgAdmin resource...")
// Find operation ID for 'modify'
// Based on HAR/Screenshots, modify uses standard "modify" operation
opId, err := r.client.GetOperationIdForInstance(ctx, state.ID.ValueString(), "modify")
if err != nil {
resp.Diagnostics.AddError("Failed to find update operation", err.Error())
return
}
// Payload for OP creation
opPayload := map[string]interface{}{
"instanceUid": state.ID.ValueString(),
"svcOperationId": opId,
"operation": "modify",
}
// Create request
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
}
// 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
}
// Create params list from plan
// We assume standard param names "resourceCPU", "resourceMemory", "resourceDisk" based on screenshot
// But we use mapping to be safe if IDs differ from Create
type Modification struct {
Name string
Val string
}
mods := []Modification{
{"resourceCPU", fmt.Sprintf("%d", plan.Cpu.ValueInt64())},
{"resourceMemory", fmt.Sprintf("%d", plan.Memory.ValueInt64())},
{"resourceDisk", fmt.Sprintf("%d", plan.Disk.ValueInt64())},
}
params := []InstanceParam{}
for _, m := range mods {
if id, ok := paramMapping[m.Name]; ok {
params = append(params, InstanceParam{SvcOperationCfsParamId: id, ParamValue: m.Val})
} else {
tflog.Warn(ctx, fmt.Sprintf("Param %s not found in modify operation", m.Name))
}
}
// If no params (e.g. only email changed but modify doesn't support it?), skip
if len(params) == 0 {
tflog.Warn(ctx, "No matching parameters found for modify operation. Skipping.")
} else {
// Run op
_, _, err = r.client.RunInstanceOperation(ctx, opUid, params)
if err != nil {
resp.Diagnostics.AddError("Error running modify operation", err.Error())
return
}
// Wait
err = r.client.WaitForOperation(ctx, opUid)
if err != nil {
resp.Diagnostics.AddError("Error waiting for modification", err.Error())
return
}
}
diags = resp.State.Set(ctx, plan)
resp.Diagnostics.Append(diags...)
}
func (r *PgAdminResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state PgAdminResourceModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// User requested NO DELETE for pgAdmin (suspend/protection).
// We will simply remove it from Terraform state without calling API delete.
if state.DeletionProtection.ValueBool() {
tflog.Warn(ctx, "Deletion Protection is ENABLED for PgAdmin. Resource will remain active in Nubes Cloud. Manual cleanup required.")
return
}
tflog.Info(ctx, "Deletion Protection is DISABLED for PgAdmin. Triggering 'suspend'...")
instanceId := state.ID.ValueString()
// Выполняем операцию suspend
err := r.client.RunAction(ctx, instanceId, "suspend")
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to suspend PgAdmin: %s", err))
return
}
tflog.Info(ctx, "PgAdmin suspended successfully. It will be permanently deleted from the cloud in 14 days.")
}
func (r *PgAdminResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}