feat: plan modifiers handling
This commit is contained in:
parent
c9c6dd1852
commit
7c4ca25791
13 changed files with 428 additions and 555 deletions
|
|
@ -0,0 +1,11 @@
|
|||
fields:
|
||||
- name: 'backup_schedule'
|
||||
modifiers:
|
||||
- 'UseStateForUnknown'
|
||||
- 'RequiresReplace'
|
||||
- name: 'encryption.kek_key_id'
|
||||
modifiers:
|
||||
- 'RequiresReplace'
|
||||
- name: 'network.acl'
|
||||
modifiers:
|
||||
- 'RequiresReplace'
|
||||
|
|
@ -2,6 +2,7 @@ package postgresflexalpha
|
|||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
|
|
@ -9,6 +10,7 @@ import (
|
|||
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/identityschema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
"github.com/hashicorp/terraform-plugin-log/tflog"
|
||||
postgresflex "github.com/mhenselin/terraform-provider-stackitprivatepreview/pkg/postgresflexalpha"
|
||||
|
|
@ -28,7 +30,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.
|
||||
|
|
@ -42,6 +44,12 @@ type instanceResource struct {
|
|||
providerData core.ProviderData
|
||||
}
|
||||
|
||||
type InstanceResourceIdentityModel struct {
|
||||
ProjectID types.String `tfsdk:"project_id"`
|
||||
Region types.String `tfsdk:"region"`
|
||||
InstanceID types.String `tfsdk:"instance_id"`
|
||||
}
|
||||
|
||||
func (r *instanceResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
|
||||
var data postgresflexalpha.InstanceModel
|
||||
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||
|
|
@ -115,9 +123,44 @@ func (r *instanceResource) Configure(
|
|||
tflog.Info(ctx, "Postgres Flex instance client configured")
|
||||
}
|
||||
|
||||
/*
|
||||
until tfplugingen framework can handle plan modifiers, we use a function to add them
|
||||
*/
|
||||
|
||||
//go:embed planModifiers.yaml
|
||||
var modifiersFileByte []byte
|
||||
|
||||
// Schema defines the schema for the resource.
|
||||
func (r *instanceResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = postgresflexalpha.InstanceResourceSchema(ctx)
|
||||
schema := postgresflexalpha.InstanceResourceSchema(ctx)
|
||||
fields, err := postgresflexUtils.ReadModifiersConfig(modifiersFileByte)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("error during read modifiers config file", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = postgresflexUtils.AddPlanModifiersToResourceSchema(fields, &schema)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("error adding plan modifiers", err.Error())
|
||||
return
|
||||
}
|
||||
resp.Schema = schema
|
||||
}
|
||||
|
||||
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, // can be defaulted by the provider configuration
|
||||
},
|
||||
"instance_id": identityschema.StringAttribute{
|
||||
RequiredForImport: true, // can be defaulted by the provider configuration
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Create creates the resource and sets the initial Terraform state.
|
||||
|
|
@ -158,7 +201,7 @@ func (r *instanceResource) Create(
|
|||
// Create new instance
|
||||
createResp, err := r.client.CreateInstanceRequest(ctx, projectId, region).CreateInstanceRequestPayload(payload).Execute()
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API: %v", err))
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "error creating instance", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -172,6 +215,17 @@ func (r *instanceResource) Create(
|
|||
return
|
||||
}
|
||||
|
||||
// Set data returned by API in identity
|
||||
identity := InstanceResourceIdentityModel{
|
||||
ProjectID: types.StringValue(projectId),
|
||||
Region: types.StringValue(region),
|
||||
InstanceID: types.StringValue(instanceId),
|
||||
}
|
||||
resp.Diagnostics.Append(resp.Identity.Set(ctx, identity)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
waitResp, err := wait.CreateInstanceWaitHandler(ctx, r.client, projectId, region, instanceId).WaitWithContext(ctx)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Wait handler error: %v", err))
|
||||
|
|
@ -225,6 +279,7 @@ func modelToCreateInstancePayload(netAcl []string, model postgresflexalpha.Insta
|
|||
|
||||
// Read refreshes the Terraform state with the latest data.
|
||||
func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
|
||||
functionError := "read instance failed"
|
||||
var model postgresflexalpha.InstanceModel
|
||||
diags := req.State.Get(ctx, &model)
|
||||
resp.Diagnostics.Append(diags...)
|
||||
|
|
@ -232,6 +287,13 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r
|
|||
return
|
||||
}
|
||||
|
||||
// Read identity data
|
||||
var identityData InstanceResourceIdentityModel
|
||||
resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx = core.InitProviderContext(ctx)
|
||||
|
||||
projectId := model.ProjectId.ValueString()
|
||||
|
|
@ -248,15 +310,32 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r
|
|||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", err.Error())
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, functionError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ctx = core.LogResponse(ctx)
|
||||
|
||||
respInstanceID, ok := instanceResp.GetIdOk()
|
||||
if !ok {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, functionError, "response provided no ID")
|
||||
return
|
||||
}
|
||||
if !model.InstanceId.IsUnknown() && !model.InstanceId.IsNull() {
|
||||
if respInstanceID != instanceId {
|
||||
core.LogAndAddError(
|
||||
ctx,
|
||||
&resp.Diagnostics,
|
||||
functionError,
|
||||
"ID in response did not match ID in state",
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = mapGetInstanceResponseToModel(ctx, &model, instanceResp)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err))
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, functionError, fmt.Sprintf("Processing API payload: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -266,6 +345,17 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r
|
|||
return
|
||||
}
|
||||
|
||||
// Set data returned by API in identity
|
||||
identity := InstanceResourceIdentityModel{
|
||||
ProjectID: types.StringValue(projectId),
|
||||
Region: types.StringValue(region),
|
||||
InstanceID: types.StringValue(instanceId),
|
||||
}
|
||||
resp.Diagnostics.Append(resp.Identity.Set(ctx, identity)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
tflog.Info(ctx, "Postgres Flex instance read")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,229 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"reflect"
|
||||
"slices"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
||||
"github.com/iancoleman/strcase"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Field struct {
|
||||
Name string `yaml:"name"`
|
||||
Modifiers []*string `yaml:"modifiers"`
|
||||
}
|
||||
|
||||
type Fields struct {
|
||||
Fields []*Field `yaml:"fields"`
|
||||
}
|
||||
|
||||
var validModifiers = []string{
|
||||
"UseStateForUnknown",
|
||||
"RequiresReplace",
|
||||
}
|
||||
|
||||
func ReadModifiersConfig(content []byte) (*Fields, error) {
|
||||
var fields Fields
|
||||
err := yaml.Unmarshal(content, &fields)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &fields, nil
|
||||
}
|
||||
|
||||
func AddPlanModifiersToResourceSchema(fields *Fields, s *schema.Schema) error {
|
||||
err := validateFields(fields)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resAttr, err := handleAttributes("", s.Attributes, fields)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Attributes = resAttr
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleAttributes(prefix string, attributes map[string]schema.Attribute, fields *Fields) (map[string]schema.Attribute, error) {
|
||||
fieldMap := fieldListToMap(fields)
|
||||
for attrName, attrValue := range attributes {
|
||||
attrNameSnake := strcase.ToSnake(attrName)
|
||||
if prefix != "" {
|
||||
attrNameSnake = prefix + "." + attrNameSnake
|
||||
}
|
||||
switch reflect.TypeOf(attrValue).String() {
|
||||
case "schema.BoolAttribute":
|
||||
if field, ok := fieldMap[attrNameSnake]; ok {
|
||||
res, err := handleBoolPlanModifiers(attrValue, field)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
attributes[attrName] = res
|
||||
}
|
||||
case "schema.Int64Attribute":
|
||||
if field, ok := fieldMap[attrNameSnake]; ok {
|
||||
res, err := handleInt64PlanModifiers(attrValue, field)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
attributes[attrName] = res
|
||||
}
|
||||
case "schema.StringAttribute":
|
||||
if field, ok := fieldMap[attrNameSnake]; ok {
|
||||
res, err := handleStringPlanModifiers(attrValue, field)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
attributes[attrName] = res
|
||||
}
|
||||
case "schema.ListAttribute":
|
||||
if field, ok := fieldMap[attrNameSnake]; ok {
|
||||
res, err := handleListPlanModifiers(attrValue, field)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
attributes[attrName] = res
|
||||
}
|
||||
case "schema.SingleNestedAttribute":
|
||||
nested, ok := attrValue.(schema.SingleNestedAttribute)
|
||||
if !ok {
|
||||
if _, ok := attrValue.(interface {
|
||||
GetAttributes() map[string]schema.Attribute
|
||||
}); ok {
|
||||
return nil, fmt.Errorf("unsupported type for single nested attribute")
|
||||
}
|
||||
}
|
||||
|
||||
res, err := handleAttributes(attrName, nested.Attributes, fields)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nested.Attributes = res
|
||||
attributes[attrName] = nested
|
||||
default:
|
||||
slog.Warn("type currently not supported", "type", reflect.TypeOf(attrValue).String())
|
||||
}
|
||||
}
|
||||
return attributes, nil
|
||||
}
|
||||
|
||||
func handleBoolPlanModifiers(
|
||||
attr schema.Attribute,
|
||||
fields []*string,
|
||||
) (schema.Attribute, error) {
|
||||
a, ok := attr.(schema.BoolAttribute)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("field is not a string attribute")
|
||||
}
|
||||
|
||||
for _, v := range fields {
|
||||
switch *v {
|
||||
case "RequiresReplace":
|
||||
a.PlanModifiers = append(a.PlanModifiers, boolplanmodifier.RequiresReplace())
|
||||
case "UseStateForUnknown":
|
||||
a.PlanModifiers = append(a.PlanModifiers, boolplanmodifier.UseStateForUnknown())
|
||||
}
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func handleStringPlanModifiers(
|
||||
attr schema.Attribute,
|
||||
fields []*string,
|
||||
) (schema.Attribute, error) {
|
||||
a, ok := attr.(schema.StringAttribute)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("field is not a string attribute")
|
||||
}
|
||||
|
||||
for _, v := range fields {
|
||||
switch *v {
|
||||
case "RequiresReplace":
|
||||
a.PlanModifiers = append(a.PlanModifiers, stringplanmodifier.RequiresReplace())
|
||||
case "UseStateForUnknown":
|
||||
a.PlanModifiers = append(a.PlanModifiers, stringplanmodifier.UseStateForUnknown())
|
||||
}
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func handleInt64PlanModifiers(
|
||||
attr schema.Attribute,
|
||||
fields []*string,
|
||||
) (schema.Attribute, error) {
|
||||
a, ok := attr.(schema.Int64Attribute)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("field is not a string attribute")
|
||||
}
|
||||
|
||||
for _, v := range fields {
|
||||
switch *v {
|
||||
case "RequiresReplace":
|
||||
a.PlanModifiers = append(a.PlanModifiers, int64planmodifier.RequiresReplace())
|
||||
case "UseStateForUnknown":
|
||||
a.PlanModifiers = append(a.PlanModifiers, int64planmodifier.UseStateForUnknown())
|
||||
}
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func handleListPlanModifiers(
|
||||
attr schema.Attribute,
|
||||
fields []*string,
|
||||
) (schema.Attribute, error) {
|
||||
a, ok := attr.(schema.ListAttribute)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("field is not a string attribute")
|
||||
}
|
||||
|
||||
for _, v := range fields {
|
||||
switch *v {
|
||||
case "RequiresReplace":
|
||||
a.PlanModifiers = append(a.PlanModifiers, listplanmodifier.RequiresReplace())
|
||||
case "UseStateForUnknown":
|
||||
a.PlanModifiers = append(a.PlanModifiers, listplanmodifier.UseStateForUnknown())
|
||||
}
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func validateFields(fields *Fields) error {
|
||||
if fields == nil {
|
||||
return nil
|
||||
}
|
||||
for _, field := range fields.Fields {
|
||||
for _, modifier := range field.Modifiers {
|
||||
if *modifier == "" {
|
||||
return fmt.Errorf("modifier %s is required", *modifier)
|
||||
}
|
||||
if !slices.Contains(validModifiers, *modifier) {
|
||||
return fmt.Errorf("modifier %s is invalid", *modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fieldListToMap(fields *Fields) map[string][]*string {
|
||||
res := make(map[string][]*string)
|
||||
|
||||
if fields != nil {
|
||||
for _, field := range fields.Fields {
|
||||
res[field.Name] = field.Modifiers
|
||||
}
|
||||
} else {
|
||||
slog.Warn("no fields available")
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
|
@ -53,8 +53,9 @@ func CreateInstanceWaitHandler(
|
|||
) *wait.AsyncActionHandler[postgresflex.GetInstanceResponse] {
|
||||
instanceCreated := false
|
||||
var instanceGetResponse *postgresflex.GetInstanceResponse
|
||||
maxWait := time.Minute * 30
|
||||
maxWait := time.Minute * 45
|
||||
startTime := time.Now()
|
||||
extendedTimeout := 0
|
||||
|
||||
handler := wait.New(
|
||||
func() (waitFinished bool, response *postgresflex.GetInstanceResponse, err error) {
|
||||
|
|
@ -88,11 +89,32 @@ func CreateInstanceWaitHandler(
|
|||
instanceId,
|
||||
),
|
||||
)
|
||||
if extendedTimeout < 3 {
|
||||
maxWait = maxWait + time.Minute*5
|
||||
extendedTimeout = extendedTimeout + 1
|
||||
if s.Network == nil || s.Network.InstanceAddress == nil {
|
||||
tflog.Warn(ctx, "Waiting for instance_address")
|
||||
return false, nil, nil
|
||||
}
|
||||
if s.Network.RouterAddress == nil {
|
||||
tflog.Warn(ctx, "Waiting for router_address")
|
||||
return false, nil, nil
|
||||
}
|
||||
if s.Status == nil {
|
||||
tflog.Warn(ctx, "Waiting for status")
|
||||
return false, nil, nil
|
||||
}
|
||||
if s.IsDeletable == nil {
|
||||
tflog.Warn(ctx, "Waiting for is_deletable")
|
||||
return false, nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
instanceCreated = true
|
||||
instanceGetResponse = s
|
||||
case InstanceStateSuccess:
|
||||
if s.Network == nil || s.Network.InstanceAddress == nil {
|
||||
tflog.Info(ctx, "Waiting for instance_address")
|
||||
tflog.Warn(ctx, "Waiting for instance_address")
|
||||
return false, nil, nil
|
||||
}
|
||||
if s.Network.RouterAddress == nil {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue