terraform-provider-stackitp.../stackit/internal/utils/planModifiers.go

229 lines
5.6 KiB
Go

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
}