fix: add missing functions

This commit is contained in:
Marcel_Henselin 2025-12-22 17:02:38 +01:00
parent b08b32ef1d
commit 76b61859da
11 changed files with 940 additions and 214 deletions

View file

@ -0,0 +1,502 @@
package postgresflex
import (
"context"
"fmt"
"strings"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)
var _ basetypes.ObjectTypable = FlavorType{}
type FlavorType struct {
basetypes.ObjectType
}
func (t FlavorType) Equal(o attr.Type) bool {
other, ok := o.(FlavorType)
if !ok {
return false
}
return t.ObjectType.Equal(other.ObjectType)
}
func (t FlavorType) String() string {
return "FlavorType"
}
func (t FlavorType) ValueFromObject(ctx context.Context, in basetypes.ObjectValue) (basetypes.ObjectValuable, diag.Diagnostics) {
var diags diag.Diagnostics
attributes := in.Attributes()
cpuAttribute, ok := attributes["cpu"]
if !ok {
diags.AddError(
"Attribute Missing",
`cpu is missing from object`)
return nil, diags
}
cpuVal, ok := cpuAttribute.(basetypes.Int64Value)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`cpu expected to be basetypes.Int64Value, was: %T`, cpuAttribute))
}
descriptionAttribute, ok := attributes["description"]
if !ok {
diags.AddError(
"Attribute Missing",
`description is missing from object`)
return nil, diags
}
descriptionVal, ok := descriptionAttribute.(basetypes.StringValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`description expected to be basetypes.StringValue, was: %T`, descriptionAttribute))
}
idAttribute, ok := attributes["id"]
if !ok {
diags.AddError(
"Attribute Missing",
`id is missing from object`)
return nil, diags
}
idVal, ok := idAttribute.(basetypes.StringValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`id expected to be basetypes.StringValue, was: %T`, idAttribute))
}
memoryAttribute, ok := attributes["memory"]
if !ok {
diags.AddError(
"Attribute Missing",
`memory is missing from object`)
return nil, diags
}
ramVal, ok := memoryAttribute.(basetypes.Int64Value)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`memory expected to be basetypes.Int64Value, was: %T`, memoryAttribute))
}
if diags.HasError() {
return nil, diags
}
return FlavorValue{
Cpu: cpuVal,
Description: descriptionVal,
Id: idVal,
Ram: ramVal,
state: attr.ValueStateKnown,
}, diags
}
func NewFlavorValueNull() FlavorValue {
return FlavorValue{
state: attr.ValueStateNull,
}
}
func NewFlavorValueUnknown() FlavorValue {
return FlavorValue{
state: attr.ValueStateUnknown,
}
}
func NewFlavorValue(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) (FlavorValue, diag.Diagnostics) {
var diags diag.Diagnostics
// Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/521
ctx := context.Background()
for name, attributeType := range attributeTypes {
attribute, ok := attributes[name]
if !ok {
diags.AddError(
"Missing FlavorValue Attribute Value",
"While creating a FlavorValue value, a missing attribute value was detected. "+
"A FlavorValue must contain values for all attributes, even if null or unknown. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
fmt.Sprintf("FlavorValue Attribute Name (%s) Expected Type: %s", name, attributeType.String()),
)
continue
}
if !attributeType.Equal(attribute.Type(ctx)) {
diags.AddError(
"Invalid FlavorValue Attribute Type",
"While creating a FlavorValue value, an invalid attribute value was detected. "+
"A FlavorValue must use a matching attribute type for the value. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
fmt.Sprintf("FlavorValue Attribute Name (%s) Expected Type: %s\n", name, attributeType.String())+
fmt.Sprintf("FlavorValue Attribute Name (%s) Given Type: %s", name, attribute.Type(ctx)),
)
}
}
for name := range attributes {
_, ok := attributeTypes[name]
if !ok {
diags.AddError(
"Extra FlavorValue Attribute Value",
"While creating a FlavorValue value, an extra attribute value was detected. "+
"A FlavorValue must not contain values beyond the expected attribute types. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
fmt.Sprintf("Extra FlavorValue Attribute Name: %s", name),
)
}
}
if diags.HasError() {
return NewFlavorValueUnknown(), diags
}
cpuAttribute, ok := attributes["cpu"]
if !ok {
diags.AddError(
"Attribute Missing",
`cpu is missing from object`)
return NewFlavorValueUnknown(), diags
}
cpuVal, ok := cpuAttribute.(basetypes.Int64Value)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`cpu expected to be basetypes.Int64Value, was: %T`, cpuAttribute))
}
descriptionAttribute, ok := attributes["description"]
if !ok {
diags.AddError(
"Attribute Missing",
`description is missing from object`)
return NewFlavorValueUnknown(), diags
}
descriptionVal, ok := descriptionAttribute.(basetypes.StringValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`description expected to be basetypes.StringValue, was: %T`, descriptionAttribute))
}
idAttribute, ok := attributes["id"]
if !ok {
diags.AddError(
"Attribute Missing",
`id is missing from object`)
return NewFlavorValueUnknown(), diags
}
idVal, ok := idAttribute.(basetypes.StringValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`id expected to be basetypes.StringValue, was: %T`, idAttribute))
}
memoryAttribute, ok := attributes["memory"]
if !ok {
diags.AddError(
"Attribute Missing",
`memory is missing from object`)
return NewFlavorValueUnknown(), diags
}
memoryVal, ok := memoryAttribute.(basetypes.Int64Value)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`memory expected to be basetypes.Int64Value, was: %T`, memoryAttribute))
}
if diags.HasError() {
return NewFlavorValueUnknown(), diags
}
return FlavorValue{
Cpu: cpuVal,
Description: descriptionVal,
Id: idVal,
Ram: memoryVal,
state: attr.ValueStateKnown,
}, diags
}
func NewFlavorValueMust(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) FlavorValue {
object, diags := NewFlavorValue(attributeTypes, attributes)
if diags.HasError() {
// This could potentially be added to the diag package.
diagsStrings := make([]string, 0, len(diags))
for _, diagnostic := range diags {
diagsStrings = append(diagsStrings, fmt.Sprintf(
"%s | %s | %s",
diagnostic.Severity(),
diagnostic.Summary(),
diagnostic.Detail()))
}
panic("NewFlavorValueMust received error(s): " + strings.Join(diagsStrings, "\n"))
}
return object
}
func (t FlavorType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
if in.Type() == nil {
return NewFlavorValueNull(), nil
}
if !in.Type().Equal(t.TerraformType(ctx)) {
return nil, fmt.Errorf("expected %s, got %s", t.TerraformType(ctx), in.Type())
}
if !in.IsKnown() {
return NewFlavorValueUnknown(), nil
}
if in.IsNull() {
return NewFlavorValueNull(), nil
}
attributes := map[string]attr.Value{}
val := map[string]tftypes.Value{}
err := in.As(&val)
if err != nil {
return nil, err
}
for k, v := range val {
a, err := t.AttrTypes[k].ValueFromTerraform(ctx, v)
if err != nil {
return nil, err
}
attributes[k] = a
}
return NewFlavorValueMust(FlavorValue{}.AttributeTypes(ctx), attributes), nil
}
func (t FlavorType) ValueType(ctx context.Context) attr.Value {
return FlavorValue{}
}
var _ basetypes.ObjectValuable = FlavorValue{}
type FlavorValue struct {
Cpu basetypes.Int64Value `tfsdk:"cpu"`
Description basetypes.StringValue `tfsdk:"description"`
Id basetypes.StringValue `tfsdk:"id"`
Ram basetypes.Int64Value `tfsdk:"ram"`
state attr.ValueState
}
func (v FlavorValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) {
attrTypes := make(map[string]tftypes.Type, 4)
var val tftypes.Value
var err error
attrTypes["cpu"] = basetypes.Int64Type{}.TerraformType(ctx)
attrTypes["description"] = basetypes.StringType{}.TerraformType(ctx)
attrTypes["id"] = basetypes.StringType{}.TerraformType(ctx)
attrTypes["memory"] = basetypes.Int64Type{}.TerraformType(ctx)
objectType := tftypes.Object{AttributeTypes: attrTypes}
switch v.state {
case attr.ValueStateKnown:
vals := make(map[string]tftypes.Value, 4)
val, err = v.Cpu.ToTerraformValue(ctx)
if err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
vals["cpu"] = val
val, err = v.Description.ToTerraformValue(ctx)
if err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
vals["description"] = val
val, err = v.Id.ToTerraformValue(ctx)
if err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
vals["id"] = val
val, err = v.Ram.ToTerraformValue(ctx)
if err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
vals["memory"] = val
if err := tftypes.ValidateValue(objectType, vals); err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
return tftypes.NewValue(objectType, vals), nil
case attr.ValueStateNull:
return tftypes.NewValue(objectType, nil), nil
case attr.ValueStateUnknown:
return tftypes.NewValue(objectType, tftypes.UnknownValue), nil
default:
panic(fmt.Sprintf("unhandled Object state in ToTerraformValue: %s", v.state))
}
}
func (v FlavorValue) IsNull() bool {
return v.state == attr.ValueStateNull
}
func (v FlavorValue) IsUnknown() bool {
return v.state == attr.ValueStateUnknown
}
func (v FlavorValue) String() string {
return "FlavorValue"
}
func (v FlavorValue) ToObjectValue(ctx context.Context) (basetypes.ObjectValue, diag.Diagnostics) {
var diags diag.Diagnostics
attributeTypes := map[string]attr.Type{
"cpu": basetypes.Int64Type{},
"description": basetypes.StringType{},
"id": basetypes.StringType{},
"memory": basetypes.Int64Type{},
}
if v.IsNull() {
return types.ObjectNull(attributeTypes), diags
}
if v.IsUnknown() {
return types.ObjectUnknown(attributeTypes), diags
}
objVal, diags := types.ObjectValue(
attributeTypes,
map[string]attr.Value{
"cpu": v.Cpu,
"description": v.Description,
"id": v.Id,
"memory": v.Ram,
})
return objVal, diags
}
func (v FlavorValue) Equal(o attr.Value) bool {
other, ok := o.(FlavorValue)
if !ok {
return false
}
if v.state != other.state {
return false
}
if v.state != attr.ValueStateKnown {
return true
}
if !v.Cpu.Equal(other.Cpu) {
return false
}
if !v.Description.Equal(other.Description) {
return false
}
if !v.Id.Equal(other.Id) {
return false
}
if !v.Ram.Equal(other.Ram) {
return false
}
return true
}
func (v FlavorValue) Type(ctx context.Context) attr.Type {
return FlavorType{
basetypes.ObjectType{
AttrTypes: v.AttributeTypes(ctx),
},
}
}
func (v FlavorValue) AttributeTypes(ctx context.Context) map[string]attr.Type {
return map[string]attr.Type{
"cpu": basetypes.Int64Type{},
"description": basetypes.StringType{},
"id": basetypes.StringType{},
"memory": basetypes.Int64Type{},
}
}

View file

@ -60,7 +60,7 @@ func (r *instanceDataSource) Configure(ctx context.Context, req datasource.Confi
}
// Schema defines the schema for the data source.
func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
func (r *instanceDataSource) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
descriptions := map[string]string{
"main": "Postgres Flex instance data source schema. Must have a `region` specified in the provider configuration.",
"id": "Terraform's internal data source. ID. It is structured as \"`project_id`,`region`,`instance_id`\".",
@ -98,11 +98,6 @@ func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques
Description: descriptions["name"],
Computed: true,
},
"acl": schema.ListAttribute{
Description: descriptions["acl"],
ElementType: types.StringType,
Computed: true,
},
"backup_schedule": schema.StringAttribute{
Computed: true,
},
@ -121,7 +116,15 @@ func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques
"ram": schema.Int64Attribute{
Computed: true,
},
"node_type": schema.StringAttribute{
Computed: true,
},
},
//CustomType: postgresflex.FlavorType{
// ObjectType: types.ObjectType{
// AttrTypes: postgresflex.FlavorValue{}.AttributeTypes(ctx),
// },
//},
},
"replicas": schema.Int64Attribute{
Computed: true,
@ -146,7 +149,7 @@ func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques
Description: descriptions["region"],
},
"encryption": schema.SingleNestedAttribute{
Required: true,
Computed: true,
Attributes: map[string]schema.Attribute{
"key_id": schema.StringAttribute{
Description: descriptions["key_id"],
@ -236,13 +239,30 @@ func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques
}
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{})
@ -261,7 +281,16 @@ func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques
}
}
err = mapFields(ctx, instanceResp, &model, flavor, storage, network, region)
var encryption = &encryptionModel{}
if !(model.Encryption.IsNull() || model.Encryption.IsUnknown()) {
diags = model.Encryption.As(ctx, encryption, basetypes.ObjectAsOptions{})
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
err = mapFields(ctx, instanceResp, &model, flavor, 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

@ -3,6 +3,7 @@ package postgresflexalpha
import (
"context"
"fmt"
"strings"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/types"
@ -13,10 +14,19 @@ import (
)
type postgresflexClient interface {
GetFlavorsRequestExecute(ctx context.Context, projectId string, region string) (*postgresflex.GetFlavorsResponse, error)
GetFlavorsRequestExecute(ctx context.Context, projectId string, region string, page, size *int64, sort *postgresflex.FlavorSort) (*postgresflex.GetFlavorsResponse, error)
}
func mapFields(ctx context.Context, resp *postgresflex.GetInstanceResponse, model *Model, flavor *flavorModel, storage *storageModel, network *networkModel, region string) error {
func mapFields(
ctx context.Context,
resp *postgresflex.GetInstanceResponse,
model *Model,
flavor *flavorModel,
storage *storageModel,
encryption *encryptionModel,
network *networkModel,
region string,
) error {
if resp == nil {
return fmt.Errorf("response input is nil")
}
@ -34,28 +44,26 @@ func mapFields(ctx context.Context, resp *postgresflex.GetInstanceResponse, mode
return fmt.Errorf("instance id not present")
}
/*
var aclList basetypes.ListValue
var diags diag.Diagnostics
if instance.Acl == nil {
aclList = types.ListNull(types.StringType)
} else {
respACL := *instance.Acl
modelACL, err := utils.ListValuetoStringSlice(model.ACL)
if err != nil {
return err
}
reconciledACL := utils.ReconcileStringSlices(modelACL, respACL)
aclList, diags = types.ListValueFrom(ctx, types.StringType, reconciledACL)
if diags.HasError() {
return fmt.Errorf("mapping ACL: %w", core.DiagsToError(diags))
}
var encryptionValues map[string]attr.Value
if instance.Encryption == nil {
encryptionValues = map[string]attr.Value{
"keyring_id": encryption.KeyRingId,
"key_id": encryption.KeyId,
"key_version": encryption.KeyVersion,
"service_account": encryption.ServiceAccount,
}
*/
} else {
encryptionValues = map[string]attr.Value{
"keyring_id": types.StringValue(*instance.Encryption.KekKeyRingId),
"key_id": types.StringValue(*instance.Encryption.KekKeyId),
"key_version": types.StringValue(*instance.Encryption.KekKeyVersion),
"service_account": types.StringValue(*instance.Encryption.ServiceAccount),
}
}
encryptionObject, diags := types.ObjectValue(encryptionTypes, encryptionValues)
if diags.HasError() {
return fmt.Errorf("creating encryption: %w", core.DiagsToError(diags))
}
var networkValues map[string]attr.Value
if instance.Network == nil {
@ -93,21 +101,25 @@ func mapFields(ctx context.Context, resp *postgresflex.GetInstanceResponse, mode
var flavorValues map[string]attr.Value
if instance.FlavorId == nil {
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() {
flavorValues = map[string]attr.Value{
"id": flavor.Id,
"description": flavor.Description,
"cpu": flavor.CPU,
"ram": flavor.RAM,
"node_type": flavor.NodeType,
}
} else {
// TODO @mhenselin
// flavorValues = map[string]attr.Value{
// "id": types.StringValue(*instance.FlavorId),
// "description": types.StringValue(*instance.FlavorId.Description),
// "cpu": types.Int64PointerValue(instance.FlavorId.Cpu),
// "ram": types.Int64PointerValue(instance.FlavorId.Memory),
// }
flavorValues = model.Flavor.Attributes()
}
flavorObject, diags := types.ObjectValue(flavorTypes, flavorValues)
if diags.HasError() {
return fmt.Errorf("creating flavor: %w", core.DiagsToError(diags))
@ -133,7 +145,6 @@ func mapFields(ctx context.Context, resp *postgresflex.GetInstanceResponse, mode
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, instanceId)
model.InstanceId = types.StringValue(instanceId)
model.Name = types.StringPointerValue(instance.Name)
// model.ACL = aclList
model.Network = networkObject
model.BackupSchedule = types.StringPointerValue(instance.BackupSchedule)
model.Flavor = flavorObject
@ -142,18 +153,15 @@ func mapFields(ctx context.Context, resp *postgresflex.GetInstanceResponse, mode
model.Storage = storageObject
model.Version = types.StringPointerValue(instance.Version)
model.Region = types.StringValue(region)
//model.Encryption = types.ObjectValue()
//model.Network = networkModel
model.Encryption = encryptionObject
model.Network = networkObject
return nil
}
func toCreatePayload(model *Model, acl []string, flavor *flavorModel, storage *storageModel, enc *encryptionModel, net *networkModel) (*postgresflex.CreateInstanceRequestPayload, error) {
func toCreatePayload(model *Model, flavor *flavorModel, storage *storageModel, enc *encryptionModel, net *networkModel) (*postgresflex.CreateInstanceRequestPayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
}
if acl == nil {
return nil, fmt.Errorf("nil acl")
}
if flavor == nil {
return nil, fmt.Errorf("nil flavor")
}
@ -162,46 +170,57 @@ func toCreatePayload(model *Model, acl []string, flavor *flavorModel, storage *s
}
replVal := int32(model.Replicas.ValueInt64())
storagePayload := &postgresflex.CreateInstanceRequestPayloadGetStorageArgType{
PerformanceClass: conversion.StringValueToPointer(storage.Class),
Size: conversion.Int64ValueToPointer(storage.Size),
}
encryptionPayload := &postgresflex.CreateInstanceRequestPayloadGetEncryptionArgType{}
if enc != nil {
encryptionPayload.KekKeyId = conversion.StringValueToPointer(enc.KeyId)
encryptionPayload.KekKeyVersion = conversion.StringValueToPointer(enc.KeyVersion)
encryptionPayload.KekKeyRingId = conversion.StringValueToPointer(enc.KeyRingId)
encryptionPayload.ServiceAccount = conversion.StringValueToPointer(enc.ServiceAccount)
}
var aclElements []string
if net != nil && !(net.ACL.IsNull() || net.ACL.IsUnknown()) {
aclElements = make([]string, 0, len(net.ACL.Elements()))
diags := net.ACL.ElementsAs(context.TODO(), &aclElements, false)
if diags.HasError() {
return nil, fmt.Errorf("creating network: %w", core.DiagsToError(diags))
}
}
networkPayload := &postgresflex.CreateInstanceRequestPayloadGetNetworkArgType{}
if net != nil {
networkPayload = &postgresflex.CreateInstanceRequestPayloadGetNetworkArgType{
AccessScope: postgresflex.InstanceNetworkGetAccessScopeAttributeType(conversion.StringValueToPointer(net.AccessScope)),
Acl: &aclElements,
}
}
return &postgresflex.CreateInstanceRequestPayload{
// TODO - verify working
//Acl: &[]string{
// strings.Join(acl, ","),
//},
BackupSchedule: conversion.StringValueToPointer(model.BackupSchedule),
FlavorId: conversion.StringValueToPointer(flavor.Id),
Name: conversion.StringValueToPointer(model.Name),
// TODO - verify working
Replicas: postgresflex.CreateInstanceRequestPayloadGetReplicasAttributeType(&replVal),
// TODO - verify working
Storage: postgresflex.CreateInstanceRequestPayloadGetStorageAttributeType(&postgresflex.Storage{
PerformanceClass: conversion.StringValueToPointer(storage.Class),
Size: conversion.Int64ValueToPointer(storage.Size),
}),
Version: conversion.StringValueToPointer(model.Version),
// TODO - verify working
Encryption: postgresflex.CreateInstanceRequestPayloadGetEncryptionAttributeType(
&postgresflex.InstanceEncryption{
KekKeyId: conversion.StringValueToPointer(enc.KeyId), // model.Encryption.Attributes(),
KekKeyRingId: conversion.StringValueToPointer(enc.KeyRingId),
KekKeyVersion: conversion.StringValueToPointer(enc.KeyVersion),
ServiceAccount: conversion.StringValueToPointer(enc.ServiceAccount),
},
),
Network: &postgresflex.InstanceNetwork{
AccessScope: postgresflex.InstanceNetworkGetAccessScopeAttributeType(
conversion.StringValueToPointer(net.AccessScope),
),
},
Replicas: postgresflex.CreateInstanceRequestPayloadGetReplicasAttributeType(&replVal),
Storage: storagePayload,
Version: conversion.StringValueToPointer(model.Version),
Encryption: encryptionPayload,
Network: networkPayload,
}, nil
}
func toUpdatePayload(model *Model, acl []string, flavor *flavorModel, storage *storageModel) (*postgresflex.UpdateInstancePartiallyRequestPayload, error) {
func toUpdatePayload(model *Model, flavor *flavorModel, storage *storageModel, network *networkModel) (*postgresflex.UpdateInstancePartiallyRequestPayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
}
if acl == nil {
return nil, fmt.Errorf("nil acl")
}
// if acl == nil {
// return nil, fmt.Errorf("nil acl")
// }
if flavor == nil {
return nil, fmt.Errorf("nil flavor")
}
@ -224,7 +243,7 @@ func toUpdatePayload(model *Model, acl []string, flavor *flavorModel, storage *s
}, nil
}
func loadFlavorId(ctx context.Context, client postgresflexClient, model *Model, flavor *flavorModel) error {
func loadFlavorId(ctx context.Context, client postgresflexClient, model *Model, flavor *flavorModel, storage *storageModel) error {
if model == nil {
return fmt.Errorf("nil model")
}
@ -239,28 +258,61 @@ func loadFlavorId(ctx context.Context, client postgresflexClient, model *Model,
if ram == nil {
return fmt.Errorf("nil RAM")
}
nodeType := conversion.StringValueToPointer(flavor.NodeType)
if nodeType == nil {
return fmt.Errorf("nil NodeType")
}
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()
res, err := client.GetFlavorsRequestExecute(ctx, projectId, region)
flavorList, err := getAllFlavors(ctx, client, projectId, region)
if err != nil {
return fmt.Errorf("listing postgresflex flavors: %w", err)
return err
}
avl := ""
if res.Flavors == nil {
return fmt.Errorf("finding flavors for project %s", projectId)
}
for _, f := range *res.Flavors {
foundFlavorCount := 0
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)
break
foundFlavorCount++
}
avl = fmt.Sprintf("%s\n- %d CPU, %d GB RAM", avl, *f.Cpu, *f.Memory)
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("multiple flavors found: %d flavors", foundFlavorCount)
}
if flavor.Id.ValueString() == "" {
return fmt.Errorf("couldn't find flavor, available specs are:%s", avl)
@ -268,3 +320,75 @@ func loadFlavorId(ctx context.Context, client postgresflexClient, model *Model,
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)
for {
sort := postgresflex.FLAVORSORT_INDEX_ASC
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()
flavorList = append(flavorList, *res.Flavors...)
if *pagination.TotalRows == int64(len(flavorList)) {
break
}
page++
}
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

@ -8,14 +8,14 @@ import (
"strings"
"time"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier"
//postgresflex "github.com/mhenselin/terraform-provider-stackitprivatepreview/pkg/postgresflexalpha"
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"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/hashicorp/terraform-plugin-log/tflog"
@ -46,7 +46,6 @@ type Model struct {
InstanceId types.String `tfsdk:"instance_id"`
ProjectId types.String `tfsdk:"project_id"`
Name types.String `tfsdk:"name"`
ACL types.List `tfsdk:"acl"`
BackupSchedule types.String `tfsdk:"backup_schedule"`
Flavor types.Object `tfsdk:"flavor"`
Replicas types.Int64 `tfsdk:"replicas"`
@ -91,6 +90,7 @@ type flavorModel struct {
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
@ -99,6 +99,7 @@ var flavorTypes = map[string]attr.Type{
"description": basetypes.StringType{},
"cpu": basetypes.Int64Type{},
"ram": basetypes.Int64Type{},
"node_type": basetypes.StringType{},
}
// Struct corresponding to Model.Storage
@ -178,17 +179,18 @@ 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.",
"acl": "The Access Control List (ACL) for the PostgresFlex instance.",
"region": "The resource region. If not defined, the provider region is used.",
"encryption": "The encryption block.",
"key_id": "Key ID of the encryption key.",
// TODO @mhenselin - do the rest
"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.",
"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.",
"acl": "The Access Control List (ACL) for the PostgresFlex instance.",
"region": "The resource region. If not defined, the provider region is used.",
"encryption": "The encryption block.",
"key_id": "Key ID of the encryption key.",
}
// TODO @mhenselin - do the rest
resp.Schema = schema.Schema{
Description: descriptions["main"],
@ -234,11 +236,6 @@ func (r *instanceResource) Schema(_ context.Context, req resource.SchemaRequest,
),
},
},
"acl": schema.ListAttribute{
Description: descriptions["acl"],
ElementType: types.StringType,
Required: true,
},
"backup_schedule": schema.StringAttribute{
Required: true,
},
@ -247,8 +244,10 @@ func (r *instanceResource) Schema(_ context.Context, req resource.SchemaRequest,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
Optional: true,
PlanModifiers: []planmodifier.String{
UseStateForUnknownIfFlavorUnchanged(req),
stringplanmodifier.RequiresReplace(),
},
},
"description": schema.StringAttribute{
@ -350,14 +349,19 @@ func (r *instanceResource) Schema(_ context.Context, req resource.SchemaRequest,
Required: true,
Attributes: map[string]schema.Attribute{
"access_scope": schema.StringAttribute{
Default: stringdefault.StaticString(
"PUBLIC",
),
Description: descriptions["access_scope"],
Required: true,
Computed: true,
Optional: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
stringplanmodifier.UseStateForUnknown(),
},
Validators: []validator.String{
validate.NoSeparator(),
stringvalidator.OneOf("SNA", "PUBLIC"),
},
},
"acl": schema.ListAttribute{
@ -371,6 +375,7 @@ func (r *instanceResource) Schema(_ context.Context, req resource.SchemaRequest,
"instance_address": schema.StringAttribute{
Description: descriptions["instance_address"],
Computed: true,
Optional: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
@ -378,6 +383,7 @@ func (r *instanceResource) Schema(_ context.Context, req resource.SchemaRequest,
"router_address": schema.StringAttribute{
Description: descriptions["router_address"],
Computed: true,
Optional: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
@ -409,27 +415,6 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
var acl []string
if !(model.ACL.IsNull() || model.ACL.IsUnknown()) {
diags = model.ACL.ElementsAs(ctx, &acl, false)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
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)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Loading flavor ID: %v", err))
return
}
}
var storage = &storageModel{}
if !(model.Storage.IsNull() || model.Storage.IsUnknown()) {
diags = model.Storage.As(ctx, storage, basetypes.ObjectAsOptions{})
@ -439,6 +424,42 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques
}
}
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{})
@ -457,8 +478,17 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques
}
}
var acl []string
if !(network.ACL.IsNull() || network.ACL.IsUnknown()) {
diags = network.ACL.ElementsAs(ctx, &acl, false)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
// Generate API request body from model
payload, err := toCreatePayload(&model, acl, flavor, storage, encryption, network)
payload, err := toCreatePayload(&model, flavor, storage, encryption, network)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Creating API payload: %v", err))
return
@ -487,7 +517,7 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques
}
// Map response body to schema
err = mapFields(ctx, waitResp, &model, flavor, storage, network, region)
err = mapFields(ctx, waitResp, &model, flavor, storage, encryption, network, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Processing API payload: %v", err))
return
@ -545,6 +575,15 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r
}
}
var encryption = &encryptionModel{}
if !(model.Encryption.IsNull() || model.Encryption.IsUnknown()) {
diags = model.Encryption.As(ctx, encryption, basetypes.ObjectAsOptions{})
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
instanceResp, err := r.client.GetInstanceRequest(ctx, projectId, region, instanceId).Execute()
if err != nil {
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
@ -564,7 +603,7 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r
}
// Map response body to schema
err = mapFields(ctx, instanceResp, &model, flavor, storage, network, region)
err = mapFields(ctx, instanceResp, &model, flavor, storage, encryption, network, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err))
return
@ -597,27 +636,15 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques
ctx = tflog.SetField(ctx, "instance_id", instanceId)
ctx = tflog.SetField(ctx, "region", region)
var acl []string
if !(model.ACL.IsNull() || model.ACL.IsUnknown()) {
diags = model.ACL.ElementsAs(ctx, &acl, false)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
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)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Loading flavor ID: %v", err))
return
}
}
//var acl []string
//if !(model.ACL.IsNull() || model.ACL.IsUnknown()) {
// diags = model.ACL.ElementsAs(ctx, &acl, false)
// 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{})
@ -627,8 +654,40 @@ 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{})
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
var encryption = &encryptionModel{}
if !(model.Encryption.IsNull() || model.Encryption.IsUnknown()) {
diags = model.Encryption.As(ctx, encryption, basetypes.ObjectAsOptions{})
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
// Generate API request body from model
payload, err := toUpdatePayload(&model, acl, flavor, storage)
payload, err := toUpdatePayload(&model, flavor, storage, network)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Creating API payload: %v", err))
return
@ -648,17 +707,8 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques
return
}
var network = &networkModel{}
if !(model.Network.IsNull() || model.Network.IsUnknown()) {
diags = model.Network.As(ctx, network, basetypes.ObjectAsOptions{})
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
// Map response body to schema
err = mapFields(ctx, waitResp, &model, flavor, storage, network, region)
err = mapFields(ctx, waitResp, &model, flavor, storage, encryption, network, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Processing API payload: %v", err))
return

View file

@ -87,7 +87,7 @@ func TestMapFields(t *testing.T) {
// Cpu: utils.Ptr(int64(12)),
// Description: utils.Ptr("description"),
// Id: utils.Ptr("flavor_id"),
// Memory: utils.Ptr(int64(34)),
// Ram: utils.Ptr(int64(34)),
//},
Id: utils.Ptr("iid"),
Name: utils.Ptr("name"),
@ -633,7 +633,7 @@ func TestToCreatePayload(t *testing.T) {
// Id: utils.Ptr("fid-1"),
// Cpu: utils.Ptr(int64(2)),
// Description: utils.Ptr("description"),
// Memory: utils.Ptr(int64(8)),
// Ram: utils.Ptr(int64(8)),
// },
// },
// },
@ -658,13 +658,13 @@ func TestToCreatePayload(t *testing.T) {
// Id: utils.Ptr("fid-1"),
// Cpu: utils.Ptr(int64(2)),
// Description: utils.Ptr("description"),
// Memory: utils.Ptr(int64(8)),
// Ram: utils.Ptr(int64(8)),
// },
// {
// Id: utils.Ptr("fid-2"),
// Cpu: utils.Ptr(int64(1)),
// Description: utils.Ptr("description"),
// Memory: utils.Ptr(int64(4)),
// Ram: utils.Ptr(int64(4)),
// },
// },
// },
@ -689,13 +689,13 @@ func TestToCreatePayload(t *testing.T) {
// Id: utils.Ptr("fid-1"),
// Cpu: utils.Ptr(int64(1)),
// Description: utils.Ptr("description"),
// Memory: utils.Ptr(int64(8)),
// Ram: utils.Ptr(int64(8)),
// },
// {
// Id: utils.Ptr("fid-2"),
// Cpu: utils.Ptr(int64(1)),
// Description: utils.Ptr("description"),
// Memory: utils.Ptr(int64(4)),
// Ram: utils.Ptr(int64(4)),
// },
// },
// },