Feat/separate functions (#19)

* chore: work save

* fix: refactor flavors

* fix: refactor pg user and database

* fix: refactor flavor parameters

* fix: refactor tf script

* chore: work save

* chore: work save

* chore: work save

---------

Co-authored-by: Marcel S. Henselin <marcel.henselin@stackit.cloud>
This commit is contained in:
Marcel S. Henselin 2026-01-13 12:19:12 +01:00 committed by GitHub
parent 910551f09d
commit 0150fea302
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
54 changed files with 6010 additions and 2826 deletions

View file

@ -100,25 +100,11 @@ func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques
"backup_schedule": schema.StringAttribute{
Computed: true,
},
"flavor": schema.SingleNestedAttribute{
"retention_days": schema.Int64Attribute{
Computed: true,
},
"flavor_id": schema.StringAttribute{
Computed: true,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
},
"description": schema.StringAttribute{
Computed: true,
},
"cpu": schema.Int64Attribute{
Computed: true,
},
"ram": schema.Int64Attribute{
Computed: true,
},
"node_type": schema.StringAttribute{
Computed: true,
},
},
},
"replicas": schema.Int64Attribute{
Computed: true,
@ -226,31 +212,6 @@ func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques
ctx = core.LogResponse(ctx)
var flavor = &flavorModel{}
if instanceResp != nil && instanceResp.FlavorId != nil {
flavor.Id = types.StringValue(*instanceResp.FlavorId)
}
if !model.Flavor.IsNull() && !model.Flavor.IsUnknown() {
diags = model.Flavor.As(ctx, flavor, basetypes.ObjectAsOptions{})
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
err := getFlavorModelById(ctx, r.client, &model, flavor)
if err != nil {
resp.Diagnostics.AddError(err.Error(), err.Error())
return
}
diags = model.Flavor.As(ctx, flavor, basetypes.ObjectAsOptions{})
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
var storage = &storageModel{}
if !model.Storage.IsNull() && !model.Storage.IsUnknown() {
diags = model.Storage.As(ctx, storage, basetypes.ObjectAsOptions{})
@ -278,7 +239,7 @@ func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques
}
}
err = mapFields(ctx, instanceResp, &model, flavor, storage, encryption, network, region)
err = mapFields(ctx, r.client, instanceResp, &model, storage, encryption, network, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err))
return

View file

@ -4,7 +4,6 @@ import (
"context"
"fmt"
"math"
"strings"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/types"
@ -20,9 +19,9 @@ type postgresflexClient interface {
func mapFields(
ctx context.Context,
client postgresflexClient,
resp *postgresflex.GetInstanceResponse,
model *Model,
flavor *flavorModel,
storage *storageModel,
encryption *encryptionModel,
network *networkModel,
@ -80,19 +79,11 @@ func mapFields(
return fmt.Errorf("creating network (acl list): %w", core.DiagsToError(diags))
}
var routerAddress string
if instance.Network.RouterAddress != nil {
routerAddress = *instance.Network.RouterAddress
diags.AddWarning("field missing while mapping fields", "router_address was empty in API response")
}
if instance.Network.InstanceAddress == nil {
return fmt.Errorf("creating network: no instance address returned")
}
networkValues = map[string]attr.Value{
"acl": aclList,
"access_scope": types.StringValue(string(*instance.Network.AccessScope)),
"instance_address": types.StringValue(*instance.Network.InstanceAddress),
"router_address": types.StringValue(routerAddress),
"access_scope": types.StringPointerValue((*string)(instance.Network.AccessScope)),
"instance_address": types.StringPointerValue(instance.Network.InstanceAddress),
"router_address": types.StringPointerValue(instance.Network.RouterAddress),
}
}
networkObject, diags := types.ObjectValue(networkTypes, networkValues)
@ -100,48 +91,6 @@ func mapFields(
return fmt.Errorf("creating network: %w", core.DiagsToError(diags))
}
var flavorValues map[string]attr.Value
if instance.FlavorId == nil || *instance.FlavorId == "" {
return fmt.Errorf("instance has no flavor id")
}
if !flavor.Id.IsUnknown() && !flavor.Id.IsNull() {
if *instance.FlavorId != flavor.Id.ValueString() {
return fmt.Errorf("instance has different flavor id %s - %s", *instance.FlavorId, flavor.Id.ValueString())
}
}
if model.Flavor.IsNull() || model.Flavor.IsUnknown() {
var nodeType string
if flavor.NodeType.IsUnknown() || flavor.NodeType.IsNull() {
if instance.Replicas == nil {
return fmt.Errorf("instance has no replicas setting")
}
switch *instance.Replicas {
case 1:
nodeType = "Single"
case 3:
nodeType = "Replicas"
default:
return fmt.Errorf("could not determine replicas settings")
}
} else {
nodeType = flavor.NodeType.ValueString()
}
flavorValues = map[string]attr.Value{
"id": flavor.Id,
"description": flavor.Description,
"cpu": flavor.CPU,
"ram": flavor.RAM,
"node_type": types.StringValue(nodeType),
}
} else {
flavorValues = model.Flavor.Attributes()
}
flavorObject, diags := types.ObjectValue(flavorTypes, flavorValues)
if diags.HasError() {
return fmt.Errorf("creating flavor: %w", core.DiagsToError(diags))
}
var storageValues map[string]attr.Value
if instance.Storage == nil {
storageValues = map[string]attr.Value{
@ -167,10 +116,8 @@ func mapFields(
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, instanceId)
model.InstanceId = types.StringValue(instanceId)
model.Name = types.StringPointerValue(instance.Name)
model.Network = networkObject
model.BackupSchedule = types.StringPointerValue(instance.BackupSchedule)
model.Flavor = flavorObject
// TODO - verify working
model.FlavorId = types.StringPointerValue(instance.FlavorId)
model.Replicas = types.Int64Value(int64(*instance.Replicas))
model.Storage = storageObject
model.Version = types.StringPointerValue(instance.Version)
@ -182,7 +129,6 @@ func mapFields(
func toCreatePayload(
model *Model,
flavor *flavorModel,
storage *storageModel,
enc *encryptionModel,
net *networkModel,
@ -190,9 +136,6 @@ func toCreatePayload(
if model == nil {
return nil, fmt.Errorf("nil model")
}
if flavor == nil {
return nil, fmt.Errorf("nil flavor")
}
if storage == nil {
return nil, fmt.Errorf("nil storage")
}
@ -239,24 +182,11 @@ func toCreatePayload(
}
}
if model.Replicas.IsNull() || model.Replicas.IsUnknown() {
if !flavor.NodeType.IsNull() && !flavor.NodeType.IsUnknown() {
switch strings.ToLower(flavor.NodeType.ValueString()) {
case "single":
replVal = int32(1)
case "replica":
replVal = int32(3)
default:
return nil, fmt.Errorf("flavor has invalid replica attribute")
}
}
}
return &postgresflex.CreateInstanceRequestPayload{
Acl: &aclElements,
BackupSchedule: conversion.StringValueToPointer(model.BackupSchedule),
Encryption: encryptionPayload,
FlavorId: conversion.StringValueToPointer(flavor.Id),
FlavorId: conversion.StringValueToPointer(model.FlavorId),
Name: conversion.StringValueToPointer(model.Name),
Network: networkPayload,
Replicas: postgresflex.CreateInstanceRequestPayloadGetReplicasAttributeType(&replVal),
@ -266,13 +196,14 @@ func toCreatePayload(
}, nil
}
func toUpdatePayload(model *Model, flavor *flavorModel, storage *storageModel, _ *networkModel) (*postgresflex.UpdateInstancePartiallyRequestPayload, error) {
func toUpdatePayload(
model *Model,
storage *storageModel,
_ *networkModel,
) (*postgresflex.UpdateInstancePartiallyRequestPayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
}
if flavor == nil {
return nil, fmt.Errorf("nil flavor")
}
if storage == nil {
return nil, fmt.Errorf("nil storage")
}
@ -282,7 +213,7 @@ func toUpdatePayload(model *Model, flavor *flavorModel, storage *storageModel, _
// Items: &acl,
// },
BackupSchedule: conversion.StringValueToPointer(model.BackupSchedule),
FlavorId: conversion.StringValueToPointer(flavor.Id),
FlavorId: conversion.StringValueToPointer(model.FlavorId),
Name: conversion.StringValueToPointer(model.Name),
// Replicas: conversion.Int64ValueToPointer(model.Replicas),
Storage: &postgresflex.StorageUpdate{
@ -291,187 +222,3 @@ func toUpdatePayload(model *Model, flavor *flavorModel, storage *storageModel, _
Version: conversion.StringValueToPointer(model.Version),
}, nil
}
func loadFlavorId(ctx context.Context, client postgresflexClient, model *Model, flavor *flavorModel, storage *storageModel) error {
if model == nil {
return fmt.Errorf("nil model")
}
if flavor == nil {
return fmt.Errorf("nil flavor")
}
cpu := flavor.CPU.ValueInt64()
if cpu == 0 {
return fmt.Errorf("nil CPU")
}
ram := flavor.RAM.ValueInt64()
if ram == 0 {
return fmt.Errorf("nil RAM")
}
nodeType := flavor.NodeType.ValueString()
if nodeType == "" {
if model.Replicas.IsNull() || model.Replicas.IsUnknown() {
return fmt.Errorf("nil NodeType")
}
switch model.Replicas.ValueInt64() {
case 1:
nodeType = "Single"
case 3:
nodeType = "Replica"
default:
return fmt.Errorf("unknown Replicas value: %d", model.Replicas.ValueInt64())
}
}
storageClass := conversion.StringValueToPointer(storage.Class)
if storageClass == nil {
return fmt.Errorf("nil StorageClass")
}
storageSize := conversion.Int64ValueToPointer(storage.Size)
if storageSize == nil {
return fmt.Errorf("nil StorageSize")
}
projectId := model.ProjectId.ValueString()
region := model.Region.ValueString()
flavorList, err := getAllFlavors(ctx, client, projectId, region)
if err != nil {
return err
}
avl := ""
foundFlavorCount := 0
var foundFlavors []string
for _, f := range flavorList {
if f.Id == nil || f.Cpu == nil || f.Memory == nil {
continue
}
if !strings.EqualFold(*f.NodeType, nodeType) {
continue
}
if *f.Cpu == cpu && *f.Memory == ram {
var useSc *postgresflex.FlavorStorageClassesStorageClass
for _, sc := range *f.StorageClasses {
if *sc.Class != *storageClass {
continue
}
if *storageSize < *f.MinGB || *storageSize > *f.MaxGB {
return fmt.Errorf("storage size %d out of bounds (min: %d - max: %d)", *storageSize, *f.MinGB, *f.MaxGB)
}
useSc = &sc
}
if useSc == nil {
return fmt.Errorf("no storage class found for %s", *storageClass)
}
flavor.Id = types.StringValue(*f.Id)
flavor.Description = types.StringValue(*f.Description)
foundFlavors = append(foundFlavors, fmt.Sprintf("%s (%d/%d - %s)", *f.Id, *f.Cpu, *f.Memory, *f.NodeType))
foundFlavorCount++
}
for _, cls := range *f.StorageClasses {
avl = fmt.Sprintf("%s\n- %d CPU, %d GB RAM, storage %s (min: %d - max: %d)", avl, *f.Cpu, *f.Memory, *cls.Class, *f.MinGB, *f.MaxGB)
}
}
if foundFlavorCount > 1 {
return fmt.Errorf(
"number of flavors returned: %d\nmultiple flavors found: %d flavors\n %s",
len(flavorList),
foundFlavorCount,
strings.Join(foundFlavors, "\n "),
)
}
if flavor.Id.ValueString() == "" {
return fmt.Errorf("couldn't find flavor, available specs are:%s", avl)
}
return nil
}
func getAllFlavors(ctx context.Context, client postgresflexClient, projectId, region string) ([]postgresflex.ListFlavors, error) {
if projectId == "" || region == "" {
return nil, fmt.Errorf("listing postgresflex flavors: projectId and region are required")
}
var flavorList []postgresflex.ListFlavors
page := int64(1)
size := int64(10)
sort := postgresflex.FLAVORSORT_INDEX_ASC
counter := 0
for {
res, err := client.GetFlavorsRequestExecute(ctx, projectId, region, &page, &size, &sort)
if err != nil {
return nil, fmt.Errorf("listing postgresflex flavors: %w", err)
}
if res.Flavors == nil {
return nil, fmt.Errorf("finding flavors for project %s", projectId)
}
pagination := res.GetPagination()
flavors := res.GetFlavors()
flavorList = append(flavorList, flavors...)
if *pagination.TotalRows < int64(len(flavorList)) {
return nil, fmt.Errorf("total rows is smaller than current accumulated list - that should not happen")
}
if *pagination.TotalRows == int64(len(flavorList)) {
break
}
page++
if page > *pagination.TotalPages {
break
}
// implement a breakpoint
counter++
if counter > 1000 {
panic("too many pagination results")
}
}
return flavorList, nil
}
func getFlavorModelById(ctx context.Context, client postgresflexClient, model *Model, flavor *flavorModel) error {
if model == nil {
return fmt.Errorf("nil model")
}
if flavor == nil {
return fmt.Errorf("nil flavor")
}
id := conversion.StringValueToPointer(flavor.Id)
if id == nil {
return fmt.Errorf("nil flavor ID")
}
flavor.Id = types.StringValue("")
projectId := model.ProjectId.ValueString()
region := model.Region.ValueString()
flavorList, err := getAllFlavors(ctx, client, projectId, region)
if err != nil {
return err
}
avl := ""
for _, f := range flavorList {
if f.Id == nil || f.Cpu == nil || f.Memory == nil {
continue
}
if *f.Id == *id {
flavor.Id = types.StringValue(*f.Id)
flavor.Description = types.StringValue(*f.Description)
flavor.CPU = types.Int64Value(*f.Cpu)
flavor.RAM = types.Int64Value(*f.Memory)
flavor.NodeType = types.StringValue(*f.NodeType)
break
}
avl = fmt.Sprintf("%s\n- %d CPU, %d GB RAM", avl, *f.Cpu, *f.Memory)
}
if flavor.Id.ValueString() == "" {
return fmt.Errorf("couldn't find flavor, available specs are: %s", avl)
}
return nil
}

View file

@ -3,11 +3,7 @@ package postgresflexalpha
import (
"context"
"fmt"
"reflect"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
postgresflex "github.com/mhenselin/terraform-provider-stackitprivatepreview/pkg/postgresflexalpha"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
)
@ -490,327 +486,303 @@ func (c postgresFlexClientMocked) GetFlavorsRequestExecute(
return &res, nil
}
func Test_getAllFlavors(t *testing.T) {
type args struct {
projectId string
region string
}
tests := []struct {
name string
args args
firstItem int
lastItem int
want []postgresflex.ListFlavors
wantErr bool
}{
{
name: "find exactly one flavor",
args: args{
projectId: "project",
region: "region",
},
firstItem: 0,
lastItem: 0,
want: []postgresflex.ListFlavors{
testFlavorToResponseFlavor(responseList[0]),
},
wantErr: false,
},
{
name: "get exactly 1 page flavors",
args: args{
projectId: "project",
region: "region",
},
firstItem: 0,
lastItem: 9,
want: testFlavorListToResponseFlavorList(responseList[0:10]),
wantErr: false,
},
{
name: "get exactly 20 flavors",
args: args{
projectId: "project",
region: "region",
},
firstItem: 0,
lastItem: 20,
// 0 indexed therefore we want :21
want: testFlavorListToResponseFlavorList(responseList[0:21]),
wantErr: false,
},
{
name: "get all flavors",
args: args{
projectId: "project",
region: "region",
},
firstItem: 0,
lastItem: len(responseList),
want: testFlavorListToResponseFlavorList(responseList),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
first := tt.firstItem
if first > len(responseList)-1 {
first = len(responseList) - 1
}
last := tt.lastItem
if last > len(responseList)-1 {
last = len(responseList) - 1
}
mockClient := postgresFlexClientMocked{
returnError: tt.wantErr,
firstItem: first,
lastItem: last,
}
got, err := getAllFlavors(context.TODO(), mockClient, tt.args.projectId, tt.args.region)
if (err != nil) != tt.wantErr {
t.Errorf("getAllFlavors() error = %v, wantErr %v", err, tt.wantErr)
return
}
//func Test_getAllFlavors(t *testing.T) {
// type args struct {
// projectId string
// region string
// }
// tests := []struct {
// name string
// args args
// firstItem int
// lastItem int
// want []postgresflex.ListFlavors
// wantErr bool
// }{
// {
// name: "find exactly one flavor",
// args: args{
// projectId: "project",
// region: "region",
// },
// firstItem: 0,
// lastItem: 0,
// want: []postgresflex.ListFlavors{
// testFlavorToResponseFlavor(responseList[0]),
// },
// wantErr: false,
// },
// {
// name: "get exactly 1 page flavors",
// args: args{
// projectId: "project",
// region: "region",
// },
// firstItem: 0,
// lastItem: 9,
// want: testFlavorListToResponseFlavorList(responseList[0:10]),
// wantErr: false,
// },
// {
// name: "get exactly 20 flavors",
// args: args{
// projectId: "project",
// region: "region",
// },
// firstItem: 0,
// lastItem: 20,
// // 0 indexed therefore we want :21
// want: testFlavorListToResponseFlavorList(responseList[0:21]),
// wantErr: false,
// },
// {
// name: "get all flavors",
// args: args{
// projectId: "project",
// region: "region",
// },
// firstItem: 0,
// lastItem: len(responseList),
// want: testFlavorListToResponseFlavorList(responseList),
// wantErr: false,
// },
// }
// for _, tt := range tests {
// t.Run(tt.name, func(t *testing.T) {
// first := tt.firstItem
// if first > len(responseList)-1 {
// first = len(responseList) - 1
// }
// last := tt.lastItem
// if last > len(responseList)-1 {
// last = len(responseList) - 1
// }
// mockClient := postgresFlexClientMocked{
// returnError: tt.wantErr,
// firstItem: first,
// lastItem: last,
// }
// got, err := getAllFlavors(context.TODO(), mockClient, tt.args.projectId, tt.args.region)
// if (err != nil) != tt.wantErr {
// t.Errorf("getAllFlavors() error = %v, wantErr %v", err, tt.wantErr)
// return
// }
//
// if diff := cmp.Diff(tt.want, got); diff != "" {
// t.Errorf("mismatch (-want +got):\n%s", diff)
// }
//
// if !reflect.DeepEqual(got, tt.want) {
// t.Errorf("getAllFlavors() got = %v, want %v", got, tt.want)
// }
// })
// }
//}
if diff := cmp.Diff(tt.want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("getAllFlavors() got = %v, want %v", got, tt.want)
}
})
}
}
func Test_loadFlavorId(t *testing.T) {
type args struct {
ctx context.Context
model *Model
flavor *flavorModel
storage *storageModel
}
tests := []struct {
name string
args args
firstItem int
lastItem int
want []postgresflex.ListFlavors
wantErr bool
}{
{
name: "find a single flavor",
args: args{
ctx: context.Background(),
model: &Model{
ProjectId: basetypes.NewStringValue("project"),
Region: basetypes.NewStringValue("region"),
},
flavor: &flavorModel{
CPU: basetypes.NewInt64Value(1),
RAM: basetypes.NewInt64Value(1),
NodeType: basetypes.NewStringValue("Single"),
},
storage: &storageModel{
Class: basetypes.NewStringValue("sc1"),
Size: basetypes.NewInt64Value(100),
},
},
firstItem: 0,
lastItem: 3,
want: []postgresflex.ListFlavors{
testFlavorToResponseFlavor(responseList[0]),
},
wantErr: false,
},
{
name: "find a single flavor by replicas option",
args: args{
ctx: context.Background(),
model: &Model{
ProjectId: basetypes.NewStringValue("project"),
Region: basetypes.NewStringValue("region"),
Replicas: basetypes.NewInt64Value(1),
},
flavor: &flavorModel{
CPU: basetypes.NewInt64Value(1),
RAM: basetypes.NewInt64Value(1),
},
storage: &storageModel{
Class: basetypes.NewStringValue("sc1"),
Size: basetypes.NewInt64Value(100),
},
},
firstItem: 0,
lastItem: 3,
want: []postgresflex.ListFlavors{
testFlavorToResponseFlavor(responseList[0]),
},
wantErr: false,
},
{
name: "fail finding find a single flavor by replicas option",
args: args{
ctx: context.Background(),
model: &Model{
ProjectId: basetypes.NewStringValue("project"),
Region: basetypes.NewStringValue("region"),
Replicas: basetypes.NewInt64Value(1),
},
flavor: &flavorModel{
CPU: basetypes.NewInt64Value(1),
RAM: basetypes.NewInt64Value(1),
},
storage: &storageModel{
Class: basetypes.NewStringValue("sc1"),
Size: basetypes.NewInt64Value(100),
},
},
firstItem: 13,
lastItem: 23,
want: []postgresflex.ListFlavors{},
wantErr: true,
},
{
name: "find a replicas flavor lower case",
args: args{
ctx: context.Background(),
model: &Model{
ProjectId: basetypes.NewStringValue("project"),
Region: basetypes.NewStringValue("region"),
},
flavor: &flavorModel{
CPU: basetypes.NewInt64Value(1),
RAM: basetypes.NewInt64Value(1),
NodeType: basetypes.NewStringValue("replica"),
},
storage: &storageModel{
Class: basetypes.NewStringValue("sc1"),
Size: basetypes.NewInt64Value(100),
},
},
firstItem: 0,
lastItem: len(responseList) - 1,
want: []postgresflex.ListFlavors{
testFlavorToResponseFlavor(responseList[16]),
},
wantErr: false,
},
{
name: "find a replicas flavor CamelCase",
args: args{
ctx: context.Background(),
model: &Model{
ProjectId: basetypes.NewStringValue("project"),
Region: basetypes.NewStringValue("region"),
},
flavor: &flavorModel{
CPU: basetypes.NewInt64Value(1),
RAM: basetypes.NewInt64Value(1),
NodeType: basetypes.NewStringValue("Replica"),
},
storage: &storageModel{
Class: basetypes.NewStringValue("sc1"),
Size: basetypes.NewInt64Value(100),
},
},
firstItem: 0,
lastItem: len(responseList) - 1,
want: []postgresflex.ListFlavors{
testFlavorToResponseFlavor(responseList[16]),
},
wantErr: false,
},
{
name: "find a replicas flavor by replicas option",
args: args{
ctx: context.Background(),
model: &Model{
ProjectId: basetypes.NewStringValue("project"),
Region: basetypes.NewStringValue("region"),
Replicas: basetypes.NewInt64Value(3),
},
flavor: &flavorModel{
CPU: basetypes.NewInt64Value(1),
RAM: basetypes.NewInt64Value(1),
},
storage: &storageModel{
Class: basetypes.NewStringValue("sc1"),
Size: basetypes.NewInt64Value(100),
},
},
firstItem: 0,
lastItem: len(responseList) - 1,
want: []postgresflex.ListFlavors{
testFlavorToResponseFlavor(responseList[16]),
},
wantErr: false,
},
{
name: "fail finding a replica flavor",
args: args{
ctx: context.Background(),
model: &Model{
ProjectId: basetypes.NewStringValue("project"),
Region: basetypes.NewStringValue("region"),
Replicas: basetypes.NewInt64Value(3),
},
flavor: &flavorModel{
CPU: basetypes.NewInt64Value(1),
RAM: basetypes.NewInt64Value(1),
},
storage: &storageModel{
Class: basetypes.NewStringValue("sc1"),
Size: basetypes.NewInt64Value(100),
},
},
firstItem: 0,
lastItem: 10,
want: []postgresflex.ListFlavors{},
wantErr: true,
},
{
name: "no flavor found error",
args: args{
ctx: context.Background(),
model: &Model{
ProjectId: basetypes.NewStringValue("project"),
Region: basetypes.NewStringValue("region"),
},
flavor: &flavorModel{
CPU: basetypes.NewInt64Value(10),
RAM: basetypes.NewInt64Value(1000),
NodeType: basetypes.NewStringValue("Single"),
},
storage: &storageModel{
Class: basetypes.NewStringValue("sc1"),
Size: basetypes.NewInt64Value(100),
},
},
firstItem: 0,
lastItem: 3,
want: []postgresflex.ListFlavors{},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
first := tt.firstItem
if first > len(responseList)-1 {
first = len(responseList) - 1
}
last := tt.lastItem
if last > len(responseList)-1 {
last = len(responseList) - 1
}
mockClient := postgresFlexClientMocked{
returnError: tt.wantErr,
firstItem: first,
lastItem: last,
}
if err := loadFlavorId(tt.args.ctx, mockClient, tt.args.model, tt.args.flavor, tt.args.storage); (err != nil) != tt.wantErr {
t.Errorf("loadFlavorId() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
//func Test_loadFlavorId(t *testing.T) {
// type args struct {
// ctx context.Context
// model *Model
// storage *storageModel
// }
// tests := []struct {
// name string
// args args
// firstItem int
// lastItem int
// want []postgresflex.ListFlavors
// wantErr bool
// }{
// {
// name: "find a single flavor",
// args: args{
// ctx: context.Background(),
// model: &Model{
// ProjectId: basetypes.NewStringValue("project"),
// Region: basetypes.NewStringValue("region"),
// },
// storage: &storageModel{
// Class: basetypes.NewStringValue("sc1"),
// Size: basetypes.NewInt64Value(100),
// },
// },
// firstItem: 0,
// lastItem: 3,
// want: []postgresflex.ListFlavors{
// testFlavorToResponseFlavor(responseList[0]),
// },
// wantErr: false,
// },
// {
// name: "find a single flavor by replicas option",
// args: args{
// ctx: context.Background(),
// model: &Model{
// ProjectId: basetypes.NewStringValue("project"),
// Region: basetypes.NewStringValue("region"),
// Replicas: basetypes.NewInt64Value(1),
// },
// storage: &storageModel{
// Class: basetypes.NewStringValue("sc1"),
// Size: basetypes.NewInt64Value(100),
// },
// },
// firstItem: 0,
// lastItem: 3,
// want: []postgresflex.ListFlavors{
// testFlavorToResponseFlavor(responseList[0]),
// },
// wantErr: false,
// },
// {
// name: "fail finding find a single flavor by replicas option",
// args: args{
// ctx: context.Background(),
// model: &Model{
// ProjectId: basetypes.NewStringValue("project"),
// Region: basetypes.NewStringValue("region"),
// Replicas: basetypes.NewInt64Value(1),
// },
// storage: &storageModel{
// Class: basetypes.NewStringValue("sc1"),
// Size: basetypes.NewInt64Value(100),
// },
// },
// firstItem: 13,
// lastItem: 23,
// want: []postgresflex.ListFlavors{},
// wantErr: true,
// },
// {
// name: "find a replicas flavor lower case",
// args: args{
// ctx: context.Background(),
// model: &Model{
// ProjectId: basetypes.NewStringValue("project"),
// Region: basetypes.NewStringValue("region"),
// },
// storage: &storageModel{
// Class: basetypes.NewStringValue("sc1"),
// Size: basetypes.NewInt64Value(100),
// },
// },
// firstItem: 0,
// lastItem: len(responseList) - 1,
// want: []postgresflex.ListFlavors{
// testFlavorToResponseFlavor(responseList[16]),
// },
// wantErr: false,
// },
// {
// name: "find a replicas flavor CamelCase",
// args: args{
// ctx: context.Background(),
// model: &Model{
// ProjectId: basetypes.NewStringValue("project"),
// Region: basetypes.NewStringValue("region"),
// },
// storage: &storageModel{
// Class: basetypes.NewStringValue("sc1"),
// Size: basetypes.NewInt64Value(100),
// },
// },
// firstItem: 0,
// lastItem: len(responseList) - 1,
// want: []postgresflex.ListFlavors{
// testFlavorToResponseFlavor(responseList[16]),
// },
// wantErr: false,
// },
// {
// name: "find a replicas flavor by replicas option",
// args: args{
// ctx: context.Background(),
// model: &Model{
// ProjectId: basetypes.NewStringValue("project"),
// Region: basetypes.NewStringValue("region"),
// Replicas: basetypes.NewInt64Value(3),
// },
// flavor: &flavorModel{
// CPU: basetypes.NewInt64Value(1),
// RAM: basetypes.NewInt64Value(1),
// },
// storage: &storageModel{
// Class: basetypes.NewStringValue("sc1"),
// Size: basetypes.NewInt64Value(100),
// },
// },
// firstItem: 0,
// lastItem: len(responseList) - 1,
// want: []postgresflex.ListFlavors{
// testFlavorToResponseFlavor(responseList[16]),
// },
// wantErr: false,
// },
// {
// name: "fail finding a replica flavor",
// args: args{
// ctx: context.Background(),
// model: &Model{
// ProjectId: basetypes.NewStringValue("project"),
// Region: basetypes.NewStringValue("region"),
// Replicas: basetypes.NewInt64Value(3),
// },
// flavor: &flavorModel{
// CPU: basetypes.NewInt64Value(1),
// RAM: basetypes.NewInt64Value(1),
// },
// storage: &storageModel{
// Class: basetypes.NewStringValue("sc1"),
// Size: basetypes.NewInt64Value(100),
// },
// },
// firstItem: 0,
// lastItem: 10,
// want: []postgresflex.ListFlavors{},
// wantErr: true,
// },
// {
// name: "no flavor found error",
// args: args{
// ctx: context.Background(),
// model: &Model{
// ProjectId: basetypes.NewStringValue("project"),
// Region: basetypes.NewStringValue("region"),
// },
// flavor: &flavorModel{
// CPU: basetypes.NewInt64Value(10),
// RAM: basetypes.NewInt64Value(1000),
// NodeType: basetypes.NewStringValue("Single"),
// },
// storage: &storageModel{
// Class: basetypes.NewStringValue("sc1"),
// Size: basetypes.NewInt64Value(100),
// },
// },
// firstItem: 0,
// lastItem: 3,
// want: []postgresflex.ListFlavors{},
// wantErr: true,
// },
// }
// for _, tt := range tests {
// t.Run(tt.name, func(t *testing.T) {
// first := tt.firstItem
// if first > len(responseList)-1 {
// first = len(responseList) - 1
// }
// last := tt.lastItem
// if last > len(responseList)-1 {
// last = len(responseList) - 1
// }
// mockClient := postgresFlexClientMocked{
// returnError: tt.wantErr,
// firstItem: first,
// lastItem: last,
// }
// if err := loadFlavorId(tt.args.ctx, mockClient, tt.args.model, tt.args.flavor, tt.args.storage); (err != nil) != tt.wantErr {
// t.Errorf("loadFlavorId() error = %v, wantErr %v", err, tt.wantErr)
// }
// })
// }
//}

View file

@ -12,7 +12,7 @@ type Model struct {
ProjectId types.String `tfsdk:"project_id"`
Name types.String `tfsdk:"name"`
BackupSchedule types.String `tfsdk:"backup_schedule"`
Flavor types.Object `tfsdk:"flavor"`
FlavorId types.String `tfsdk:"flavor_id"`
Replicas types.Int64 `tfsdk:"replicas"`
RetentionDays types.Int64 `tfsdk:"retention_days"`
Storage types.Object `tfsdk:"storage"`
@ -22,9 +22,11 @@ type Model struct {
Network types.Object `tfsdk:"network"`
}
type IdentityModel struct {
ID types.String `tfsdk:"id"`
}
//type IdentityModel struct {
// InstanceId types.String `tfsdk:"instance_id"`
// Region types.String `tfsdk:"region"`
// ProjectId types.String `tfsdk:"project_id"`
//}
type encryptionModel struct {
KeyRingId types.String `tfsdk:"keyring_id"`
@ -54,24 +56,6 @@ var networkTypes = map[string]attr.Type{
"router_address": basetypes.StringType{},
}
// Struct corresponding to Model.Flavor
type flavorModel struct {
Id types.String `tfsdk:"id"`
Description types.String `tfsdk:"description"`
CPU types.Int64 `tfsdk:"cpu"`
RAM types.Int64 `tfsdk:"ram"`
NodeType types.String `tfsdk:"node_type"`
}
// Types corresponding to flavorModel
var flavorTypes = map[string]attr.Type{
"id": basetypes.StringType{},
"description": basetypes.StringType{},
"cpu": basetypes.Int64Type{},
"ram": basetypes.Int64Type{},
"node_type": basetypes.StringType{},
}
// Struct corresponding to Model.Storage
type storageModel struct {
Class types.String `tfsdk:"class"`

View file

@ -6,16 +6,13 @@ import (
"net/http"
"regexp"
"strings"
"time"
"github.com/hashicorp/terraform-plugin-framework/resource/identityschema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
postgresflex "github.com/mhenselin/terraform-provider-stackitprivatepreview/pkg/postgresflexalpha"
"github.com/mhenselin/terraform-provider-stackitprivatepreview/pkg/postgresflexalpha/wait"
postgresflexUtils "github.com/mhenselin/terraform-provider-stackitprivatepreview/stackit/internal/services/postgresflexalpha/utils"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
@ -42,7 +39,7 @@ var (
_ resource.ResourceWithImportState = &instanceResource{}
_ resource.ResourceWithModifyPlan = &instanceResource{}
_ resource.ResourceWithValidateConfig = &instanceResource{}
_ resource.ResourceWithIdentity = &instanceResource{}
//_ resource.ResourceWithIdentity = &instanceResource{}
)
// NewInstanceResource is a helper function to simplify the provider implementation.
@ -128,35 +125,30 @@ func (r *instanceResource) Configure(ctx context.Context, req resource.Configure
// Schema defines the schema for the resource.
func (r *instanceResource) Schema(_ context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
descriptions := map[string]string{
"main": "Postgres Flex instance resource schema. Must have a `region` specified in the provider configuration.",
"id": "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`instance_id`\".",
"instance_id": "ID of the PostgresFlex instance.",
"project_id": "STACKIT project ID to which the instance is associated.",
"name": "Instance name.",
"backup_schedule": "The schedule for on what time and how often the database backup will be created. The schedule is written as a cron schedule.",
"retention_days": "The days of the retention period.",
"flavor": "The block that defines the flavor data.",
"flavor_id": "The ID of the flavor.",
"flavor_description": "The flavor detailed flavor name.",
"flavor_cpu": "The CPU count of the flavor.",
"flavor_ram": "The RAM count of the flavor.",
"flavor_node_type": "The node type of the flavor. (Single or Replicas)",
"replicas": "The number of replicas.",
"storage": "The block of the storage configuration.",
"storage_class": "The storage class used.",
"storage_size": "The disk size of the storage.",
"region": "The resource region. If not defined, the provider region is used.",
"version": "The database version used.",
"encryption": "The encryption block.",
"keyring_id": "KeyRing ID of the encryption key.",
"key_id": "Key ID of the encryption key.",
"key_version": "Key version of the encryption key.",
"service_account": "The service account ID of the service account.",
"network": "The network block configuration.",
"access_scope": "The access scope. (Either SNA or PUBLIC)",
"acl": "The Access Control List (ACL) for the PostgresFlex instance.",
"instance_address": "The returned instance address.",
"router_address": "The returned router address.",
"main": "Postgres Flex instance resource schema. Must have a `region` specified in the provider configuration.",
"id": "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`instance_id`\".",
"instance_id": "ID of the PostgresFlex instance.",
"project_id": "STACKIT project ID to which the instance is associated.",
"name": "Instance name.",
"backup_schedule": "The schedule for on what time and how often the database backup will be created. The schedule is written as a cron schedule.",
"retention_days": "The days of the retention period.",
"flavor_id": "The ID of the flavor.",
"replicas": "The number of replicas.",
"storage": "The block of the storage configuration.",
"storage_class": "The storage class used.",
"storage_size": "The disk size of the storage.",
"region": "The resource region. If not defined, the provider region is used.",
"version": "The database version used.",
"encryption": "The encryption block.",
"keyring_id": "KeyRing ID of the encryption key.",
"key_id": "Key ID of the encryption key.",
"key_version": "Key version of the encryption key.",
"service_account": "The service account ID of the service account.",
"network": "The network block configuration.",
"access_scope": "The access scope. (Either SNA or PUBLIC)",
"acl": "The Access Control List (ACL) for the PostgresFlex instance.",
"instance_address": "The returned instance address.",
"router_address": "The returned router address.",
}
resp.Schema = schema.Schema{
@ -210,45 +202,8 @@ func (r *instanceResource) Schema(_ context.Context, req resource.SchemaRequest,
Description: descriptions["retention_days"],
Required: true,
},
"flavor": schema.SingleNestedAttribute{
Required: true,
Description: descriptions["flavor"],
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: descriptions["flavor_id"],
Computed: true,
Optional: true,
PlanModifiers: []planmodifier.String{
UseStateForUnknownIfFlavorUnchanged(req),
stringplanmodifier.RequiresReplace(),
},
},
"description": schema.StringAttribute{
Computed: true,
Description: descriptions["flavor_description"],
PlanModifiers: []planmodifier.String{
UseStateForUnknownIfFlavorUnchanged(req),
},
},
"cpu": schema.Int64Attribute{
Description: descriptions["flavor_cpu"],
Required: true,
},
"ram": schema.Int64Attribute{
Description: descriptions["flavor_ram"],
Required: true,
},
"node_type": schema.StringAttribute{
Description: descriptions["flavor_node_type"],
Computed: true,
Optional: true,
PlanModifiers: []planmodifier.String{
// TODO @mhenselin anschauen
UseStateForUnknownIfFlavorUnchanged(req),
stringplanmodifier.RequiresReplace(),
},
},
},
"flavor_id": schema.StringAttribute{
Required: true,
},
"replicas": schema.Int64Attribute{
Required: true,
@ -390,15 +345,21 @@ func (r *instanceResource) Schema(_ context.Context, req resource.SchemaRequest,
}
}
func (r *instanceResource) IdentitySchema(_ context.Context, _ resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) {
resp.IdentitySchema = identityschema.Schema{
Attributes: map[string]identityschema.Attribute{
"id": identityschema.StringAttribute{
RequiredForImport: true, // must be set during import by the practitioner
},
},
}
}
//func (r *instanceResource) IdentitySchema(_ context.Context, _ resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) {
// resp.IdentitySchema = identityschema.Schema{
// Attributes: map[string]identityschema.Attribute{
// "project_id": identityschema.StringAttribute{
// RequiredForImport: true, // must be set during import by the practitioner
// },
// "region": identityschema.StringAttribute{
// RequiredForImport: true, // must be set during import by the practitioner
// },
// "instance_id": identityschema.StringAttribute{
// RequiredForImport: true, // must be set during import by the practitioner
// },
// },
// }
//}
// Create creates the resource and sets the initial Terraform state.
func (r *instanceResource) Create(
@ -406,7 +367,6 @@ func (r *instanceResource) Create(
req resource.CreateRequest,
resp *resource.CreateResponse,
) { // nolint:gocritic // function signature required by Terraform
// Retrieve values from plan
var model Model
diags := req.Plan.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
@ -430,42 +390,6 @@ func (r *instanceResource) Create(
}
}
var flavor = &flavorModel{}
if !model.Flavor.IsNull() && !model.Flavor.IsUnknown() {
diags = model.Flavor.As(ctx, flavor, basetypes.ObjectAsOptions{})
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
err := loadFlavorId(ctx, r.client, &model, flavor, storage)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Loading flavor ID: %v", err))
return
}
}
if flavor.Id.IsNull() || flavor.Id.IsUnknown() {
err := loadFlavorId(ctx, r.client, &model, flavor, storage)
if err != nil {
resp.Diagnostics.AddError(err.Error(), err.Error())
return
}
flavorValues := map[string]attr.Value{
"id": flavor.Id,
"description": flavor.Description,
"cpu": flavor.CPU,
"ram": flavor.RAM,
"node_type": flavor.NodeType,
}
var flavorObject basetypes.ObjectValue
flavorObject, diags = types.ObjectValue(flavorTypes, flavorValues)
resp.Diagnostics.Append(diags...)
if diags.HasError() {
return
}
model.Flavor = flavorObject
}
var encryption = &encryptionModel{}
if !model.Encryption.IsNull() && !model.Encryption.IsUnknown() {
diags = model.Encryption.As(ctx, encryption, basetypes.ObjectAsOptions{})
@ -494,7 +418,7 @@ func (r *instanceResource) Create(
}
// Generate API request body from model
payload, err := toCreatePayload(&model, flavor, storage, encryption, network)
payload, err := toCreatePayload(&model, storage, encryption, network)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Creating API payload: %v", err))
return
@ -516,11 +440,13 @@ func (r *instanceResource) Create(
return
}
// Set data returned by API in identity
identity := IdentityModel{
ID: utils.BuildInternalTerraformId(projectId, region, instanceId),
}
resp.Diagnostics.Append(resp.Identity.Set(ctx, identity)...)
//// Set data returned by API in identity
//identity := IdentityModel{
// InstanceId: types.StringValue(instanceId),
// Region: types.StringValue(region),
// ProjectId: types.StringValue(projectId),
//}
//resp.Diagnostics.Append(resp.Identity.Set(ctx, identity)...)
waitResp, err := wait.CreateInstanceWaitHandler(ctx, r.client, projectId, region, instanceId).WaitWithContext(ctx)
if err != nil {
@ -529,7 +455,7 @@ func (r *instanceResource) Create(
}
// Map response body to schema
err = mapFields(ctx, waitResp, &model, flavor, storage, encryption, network, region)
err = mapFields(ctx, r.client, waitResp, &model, storage, encryption, network, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Processing API payload: %v", err))
return
@ -552,12 +478,12 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r
return
}
// Read identity data
var identityData IdentityModel
resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...)
if resp.Diagnostics.HasError() {
return
}
//// Read identity data
//var identityData IdentityModel
//resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...)
//if resp.Diagnostics.HasError() {
// return
//}
ctx = core.InitProviderContext(ctx)
@ -568,35 +494,27 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r
ctx = tflog.SetField(ctx, "instance_id", instanceId)
ctx = tflog.SetField(ctx, "region", region)
var flavor = &flavorModel{}
if !model.Flavor.IsNull() && !model.Flavor.IsUnknown() {
diags = model.Flavor.As(ctx, flavor, basetypes.ObjectAsOptions{})
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
var storage = &storageModel{}
var storage = storageModel{}
if !model.Storage.IsNull() && !model.Storage.IsUnknown() {
diags = model.Storage.As(ctx, storage, basetypes.ObjectAsOptions{})
diags = model.Storage.As(ctx, &storage, basetypes.ObjectAsOptions{})
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
var network = &networkModel{}
var network = networkModel{}
if !model.Network.IsNull() && !model.Network.IsUnknown() {
diags = model.Network.As(ctx, network, basetypes.ObjectAsOptions{})
diags = model.Network.As(ctx, &network, basetypes.ObjectAsOptions{})
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
var encryption = &encryptionModel{}
var encryption = encryptionModel{}
if !model.Encryption.IsNull() && !model.Encryption.IsUnknown() {
diags = model.Encryption.As(ctx, encryption, basetypes.ObjectAsOptions{})
diags = model.Encryption.As(ctx, &encryption, basetypes.ObjectAsOptions{})
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
@ -617,7 +535,7 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r
ctx = core.LogResponse(ctx)
// Map response body to schema
err = mapFields(ctx, instanceResp, &model, flavor, storage, encryption, network, region)
err = mapFields(ctx, r.client, instanceResp, &model, &storage, &encryption, &network, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err))
return
@ -629,18 +547,19 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r
return
}
identityData.ID = model.Id
resp.Diagnostics.Append(resp.Identity.Set(ctx, identityData)...)
if resp.Diagnostics.HasError() {
return
}
//identityData.InstanceId = model.InstanceId
//identityData.Region = model.Region
//identityData.ProjectId = model.ProjectId
//resp.Diagnostics.Append(resp.Identity.Set(ctx, identityData)...)
//if resp.Diagnostics.HasError() {
// return
//}
tflog.Info(ctx, "Postgres Flex instance read")
}
// Update updates the resource and sets the updated Terraform state on success.
func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
// Retrieve values from plan
var model Model
diags := req.Plan.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
@ -676,20 +595,6 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques
}
}
var flavor = &flavorModel{}
if !model.Flavor.IsNull() && !model.Flavor.IsUnknown() {
diags = model.Flavor.As(ctx, flavor, basetypes.ObjectAsOptions{})
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
err := loadFlavorId(ctx, r.client, &model, flavor, storage)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Loading flavor ID: %v", err))
return
}
}
var network = &networkModel{}
if !model.Network.IsNull() && !model.Network.IsUnknown() {
diags = model.Network.As(ctx, network, basetypes.ObjectAsOptions{})
@ -709,7 +614,7 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques
}
// Generate API request body from model
payload, err := toUpdatePayload(&model, flavor, storage, network)
payload, err := toUpdatePayload(&model, storage, network)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Creating API payload: %v", err))
return
@ -730,7 +635,7 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques
}
// Map response body to schema
err = mapFields(ctx, waitResp, &model, flavor, storage, encryption, network, region)
err = mapFields(ctx, r.client, waitResp, &model, storage, encryption, network, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Processing API payload: %v", err))
return
@ -745,7 +650,6 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques
// Delete deletes the resource and removes the Terraform state on success.
func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform
// Retrieve values from state
var model Model
diags := req.State.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
@ -771,11 +675,22 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques
ctx = core.LogResponse(ctx)
_, err = wait.DeleteInstanceWaitHandler(ctx, r.client, projectId, region, instanceId).SetTimeout(45 * time.Minute).WaitWithContext(ctx)
//_, err = wait.DeleteInstanceWaitHandler(ctx, r.client, projectId, region, instanceId).SetTimeout(45 * time.Minute).WaitWithContext(ctx)
//if err != nil {
// core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Instance deletion waiting: %v", err))
// return
//}
_, err = r.client.GetInstanceRequest(ctx, projectId, region, instanceId).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Instance deletion waiting: %v", err))
return
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
if ok && oapiErr.StatusCode != http.StatusNotFound {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", err.Error())
return
}
}
resp.State.RemoveResource(ctx)
tflog.Info(ctx, "Postgres Flex instance deleted")
}

View file

@ -32,7 +32,6 @@ func TestMapFields(t *testing.T) {
description string
state Model
input *postgresflex.GetInstanceResponse
flavor *flavorModel
storage *storageModel
encryption *encryptionModel
network *networkModel
@ -51,9 +50,6 @@ func TestMapFields(t *testing.T) {
FlavorId: utils.Ptr("flavor_id"),
Replicas: postgresflex.GetInstanceResponseGetReplicasAttributeType(utils.Ptr(int32(1))),
},
&flavorModel{
NodeType: types.StringValue("Single"),
},
&storageModel{},
&encryptionModel{},
&networkModel{
@ -67,16 +63,10 @@ func TestMapFields(t *testing.T) {
InstanceId: types.StringValue("iid"),
ProjectId: types.StringValue("pid"),
Name: types.StringNull(),
FlavorId: types.StringValue("flavor_id"),
//ACL: types.ListNull(types.StringType),
BackupSchedule: types.StringNull(),
Flavor: types.ObjectValueMust(flavorTypes, map[string]attr.Value{
"id": types.StringNull(),
"description": types.StringNull(),
"cpu": types.Int64Null(),
"ram": types.Int64Null(),
"node_type": types.StringValue("Single"),
}),
Replicas: types.Int64Value(1),
Replicas: types.Int64Value(1),
Encryption: types.ObjectValueMust(encryptionTypes, map[string]attr.Value{
"keyring_id": types.StringNull(),
"key_id": types.StringNull(),
@ -171,7 +161,6 @@ func TestMapFields(t *testing.T) {
ProjectId: types.StringValue("pid"),
},
nil,
&flavorModel{},
&storageModel{},
&encryptionModel{},
&networkModel{},
@ -186,7 +175,6 @@ func TestMapFields(t *testing.T) {
ProjectId: types.StringValue("pid"),
},
&postgresflex.GetInstanceResponse{},
&flavorModel{},
&storageModel{},
&encryptionModel{},
&networkModel{},
@ -197,7 +185,21 @@ func TestMapFields(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := mapFields(context.Background(), tt.input, &tt.state, tt.flavor, tt.storage, tt.encryption, tt.network, tt.region)
client := postgresFlexClientMocked{
returnError: false,
firstItem: 0,
lastItem: 0,
}
err := mapFields(
context.Background(),
client,
tt.input,
&tt.state,
tt.storage,
tt.encryption,
tt.network,
tt.region,
)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
@ -219,7 +221,6 @@ func TestToCreatePayload(t *testing.T) {
description string
input *Model
inputAcl []string
inputFlavor *flavorModel
inputStorage *storageModel
inputEncryption *encryptionModel
inputNetwork *networkModel
@ -232,34 +233,6 @@ func TestToCreatePayload(t *testing.T) {
Replicas: types.Int64Value(1),
},
[]string{},
&flavorModel{},
&storageModel{},
&encryptionModel{},
&networkModel{
ACL: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("0.0.0.0/0"),
}),
},
&postgresflex.CreateInstanceRequestPayload{
Acl: &[]string{"0.0.0.0/0"},
Storage: postgresflex.CreateInstanceRequestPayloadGetStorageAttributeType(&postgresflex.Storage{}),
Encryption: &postgresflex.InstanceEncryption{},
Network: &postgresflex.InstanceNetwork{
Acl: &[]string{"0.0.0.0/0"},
},
Replicas: postgresflex.CreateInstanceRequestPayloadGetReplicasAttributeType(utils.Ptr(int32(1))),
},
true,
},
{
"use flavor node_type instead of replicas",
&Model{},
[]string{
"0.0.0.0/0",
},
&flavorModel{
NodeType: types.StringValue("Single"),
},
&storageModel{},
&encryptionModel{},
&networkModel{
@ -282,7 +255,6 @@ func TestToCreatePayload(t *testing.T) {
"nil_model",
nil,
[]string{},
&flavorModel{},
&storageModel{},
&encryptionModel{},
&networkModel{},
@ -293,7 +265,6 @@ func TestToCreatePayload(t *testing.T) {
"nil_acl",
&Model{},
nil,
&flavorModel{},
&storageModel{},
&encryptionModel{},
&networkModel{},
@ -304,7 +275,6 @@ func TestToCreatePayload(t *testing.T) {
"nil_flavor",
&Model{},
[]string{},
nil,
&storageModel{},
&encryptionModel{},
&networkModel{},
@ -315,7 +285,6 @@ func TestToCreatePayload(t *testing.T) {
"nil_storage",
&Model{},
[]string{},
&flavorModel{},
nil,
&encryptionModel{},
&networkModel{},
@ -325,7 +294,7 @@ func TestToCreatePayload(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
output, err := toCreatePayload(tt.input, tt.inputFlavor, tt.inputStorage, tt.inputEncryption, tt.inputNetwork)
output, err := toCreatePayload(tt.input, tt.inputStorage, tt.inputEncryption, tt.inputNetwork)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}

View file

@ -1,85 +0,0 @@
package postgresflexalpha
import (
"context"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
)
type useStateForUnknownIfFlavorUnchangedModifier struct {
Req resource.SchemaRequest
}
// UseStateForUnknownIfFlavorUnchanged returns a plan modifier similar to UseStateForUnknown
// if the RAM and CPU values are not changed in the plan. Otherwise, the plan modifier does nothing.
func UseStateForUnknownIfFlavorUnchanged(req resource.SchemaRequest) planmodifier.String {
return useStateForUnknownIfFlavorUnchangedModifier{
Req: req,
}
}
func (m useStateForUnknownIfFlavorUnchangedModifier) Description(context.Context) string {
return "UseStateForUnknownIfFlavorUnchanged returns a plan modifier similar to UseStateForUnknown if the RAM and CPU values are not changed in the plan. Otherwise, the plan modifier does nothing."
}
func (m useStateForUnknownIfFlavorUnchangedModifier) MarkdownDescription(ctx context.Context) string {
return m.Description(ctx)
}
func (m useStateForUnknownIfFlavorUnchangedModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { // nolint:gocritic // function signature required by Terraform
// Do nothing if there is no state value.
if req.StateValue.IsNull() {
return
}
// Do nothing if there is a known planned value.
if !req.PlanValue.IsUnknown() {
return
}
// Do nothing if there is an unknown configuration value, otherwise interpolation gets messed up.
if req.ConfigValue.IsUnknown() {
return
}
// The above checks are taken from the UseStateForUnknown plan modifier implementation
// (https://github.com/hashicorp/terraform-plugin-framework/blob/main/resource/schema/stringplanmodifier/use_state_for_unknown.go#L38)
var stateModel Model
diags := req.State.Get(ctx, &stateModel)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
var stateFlavor = &flavorModel{}
if !stateModel.Flavor.IsNull() && !stateModel.Flavor.IsUnknown() {
diags = stateModel.Flavor.As(ctx, stateFlavor, basetypes.ObjectAsOptions{})
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
var planModel Model
diags = req.Plan.Get(ctx, &planModel)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
var planFlavor = &flavorModel{}
if !planModel.Flavor.IsNull() && !planModel.Flavor.IsUnknown() {
diags = planModel.Flavor.As(ctx, planFlavor, basetypes.ObjectAsOptions{})
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
if planFlavor.CPU == stateFlavor.CPU && planFlavor.RAM == stateFlavor.RAM {
resp.PlanValue = req.StateValue
}
}