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 }