478 lines
15 KiB
Go
478 lines
15 KiB
Go
package postgresflexalpha
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/terraform-plugin-framework/attr"
|
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
|
postgresflex "github.com/mhenselin/terraform-provider-stackitprivatepreview/pkg/postgresflexalpha"
|
|
"github.com/mhenselin/terraform-provider-stackitprivatepreview/stackit/internal/conversion"
|
|
"github.com/mhenselin/terraform-provider-stackitprivatepreview/stackit/internal/core"
|
|
"github.com/mhenselin/terraform-provider-stackitprivatepreview/stackit/internal/utils"
|
|
)
|
|
|
|
type postgresflexClient interface {
|
|
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,
|
|
encryption *encryptionModel,
|
|
network *networkModel,
|
|
region string,
|
|
) error {
|
|
if resp == nil {
|
|
return fmt.Errorf("response input is nil")
|
|
}
|
|
if model == nil {
|
|
return fmt.Errorf("model input is nil")
|
|
}
|
|
instance := resp
|
|
|
|
var instanceId string
|
|
if model.InstanceId.ValueString() != "" {
|
|
instanceId = model.InstanceId.ValueString()
|
|
} else if instance.Id != nil {
|
|
instanceId = *instance.Id
|
|
} else {
|
|
return fmt.Errorf("instance id not present")
|
|
}
|
|
|
|
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 {
|
|
networkValues = map[string]attr.Value{
|
|
"acl": network.ACL,
|
|
"access_scope": network.AccessScope,
|
|
"instance_address": network.InstanceAddress,
|
|
"router_address": network.RouterAddress,
|
|
}
|
|
} else {
|
|
aclList, diags := types.ListValueFrom(ctx, types.StringType, *instance.Network.Acl)
|
|
if diags.HasError() {
|
|
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),
|
|
}
|
|
}
|
|
networkObject, diags := types.ObjectValue(networkTypes, networkValues)
|
|
if diags.HasError() {
|
|
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{
|
|
"class": storage.Class,
|
|
"size": storage.Size,
|
|
}
|
|
} else {
|
|
storageValues = map[string]attr.Value{
|
|
"class": types.StringValue(*instance.Storage.PerformanceClass),
|
|
"size": types.Int64PointerValue(instance.Storage.Size),
|
|
}
|
|
}
|
|
storageObject, diags := types.ObjectValue(storageTypes, storageValues)
|
|
if diags.HasError() {
|
|
return fmt.Errorf("creating storage: %w", core.DiagsToError(diags))
|
|
}
|
|
|
|
if instance.Replicas == nil {
|
|
diags.AddError("error mapping fields", "replicas is nil")
|
|
return fmt.Errorf("replicas is nil")
|
|
}
|
|
|
|
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.Replicas = types.Int64Value(int64(*instance.Replicas))
|
|
model.Storage = storageObject
|
|
model.Version = types.StringPointerValue(instance.Version)
|
|
model.Region = types.StringValue(region)
|
|
model.Encryption = encryptionObject
|
|
model.Network = networkObject
|
|
return nil
|
|
}
|
|
|
|
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 flavor == nil {
|
|
return nil, fmt.Errorf("nil flavor")
|
|
}
|
|
if storage == nil {
|
|
return nil, fmt.Errorf("nil storage")
|
|
}
|
|
|
|
var replVal int32
|
|
if !model.Replicas.IsNull() && !model.Replicas.IsUnknown() {
|
|
if model.Replicas.ValueInt64() > math.MaxInt32 {
|
|
return nil, fmt.Errorf("replica count too big: %d", model.Replicas.ValueInt64())
|
|
}
|
|
replVal = int32(model.Replicas.ValueInt64()) // nolint:gosec // check is performed above
|
|
}
|
|
|
|
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))
|
|
}
|
|
}
|
|
|
|
if len(aclElements) < 1 {
|
|
return nil, fmt.Errorf("no acl elements found")
|
|
}
|
|
|
|
networkPayload := &postgresflex.CreateInstanceRequestPayloadGetNetworkArgType{}
|
|
if net != nil {
|
|
networkPayload = &postgresflex.CreateInstanceRequestPayloadGetNetworkArgType{
|
|
AccessScope: postgresflex.InstanceNetworkGetAccessScopeAttributeType(conversion.StringValueToPointer(net.AccessScope)),
|
|
Acl: &aclElements,
|
|
}
|
|
}
|
|
|
|
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{
|
|
BackupSchedule: conversion.StringValueToPointer(model.BackupSchedule),
|
|
Encryption: encryptionPayload,
|
|
FlavorId: conversion.StringValueToPointer(flavor.Id),
|
|
Name: conversion.StringValueToPointer(model.Name),
|
|
Network: networkPayload,
|
|
Replicas: postgresflex.CreateInstanceRequestPayloadGetReplicasAttributeType(&replVal),
|
|
RetentionDays: conversion.Int64ValueToPointer(model.RetentionDays),
|
|
Storage: storagePayload,
|
|
Version: conversion.StringValueToPointer(model.Version),
|
|
}, nil
|
|
}
|
|
|
|
func toUpdatePayload(model *Model, flavor *flavorModel, 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")
|
|
}
|
|
|
|
return &postgresflex.UpdateInstancePartiallyRequestPayload{
|
|
// Acl: postgresflexalpha.UpdateInstancePartiallyRequestPayloadGetAclAttributeType{
|
|
// Items: &acl,
|
|
// },
|
|
BackupSchedule: conversion.StringValueToPointer(model.BackupSchedule),
|
|
FlavorId: conversion.StringValueToPointer(flavor.Id),
|
|
Name: conversion.StringValueToPointer(model.Name),
|
|
// Replicas: conversion.Int64ValueToPointer(model.Replicas),
|
|
Storage: &postgresflex.StorageUpdate{
|
|
Size: conversion.Int64ValueToPointer(storage.Size),
|
|
},
|
|
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\n",
|
|
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()
|
|
for _, flavor := range flavors {
|
|
flavorList = append(flavorList, flavor)
|
|
}
|
|
|
|
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
|
|
}
|