feat: initial copy of v0.1.0
This commit is contained in:
parent
4cc801a7f3
commit
7d4cbb6b08
538 changed files with 63361 additions and 55213 deletions
|
|
@ -10,7 +10,8 @@ import (
|
|||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
||||
|
||||
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/core"
|
||||
)
|
||||
|
||||
type attributeGetter interface {
|
||||
|
|
|
|||
229
stackit/internal/utils/planModifiers.go
Normal file
229
stackit/internal/utils/planModifiers.go
Normal file
|
|
@ -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
|
||||
}
|
||||
224
stackit/internal/utils/planModifiers_test.go
Normal file
224
stackit/internal/utils/planModifiers_test.go
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
"github.com/stackitcloud/stackit-sdk-go/core/utils"
|
||||
)
|
||||
|
||||
func TestReadModifiersConfig(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
content []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid yaml",
|
||||
content: []byte(`
|
||||
fields:
|
||||
- name: 'id'
|
||||
modifiers:
|
||||
- 'UseStateForUnknown'
|
||||
`),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid yaml",
|
||||
content: []byte(`invalid: yaml: :`),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(
|
||||
tc.name, func(t *testing.T) {
|
||||
_, err := ReadModifiersConfig(tc.content)
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Errorf("ReadModifiersConfig() error = %v, wantErr %v", err, tc.wantErr)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddPlanModifiersToResourceSchema(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
fields *Fields
|
||||
sch *schema.Schema
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "full coverage - all types and nested structures",
|
||||
fields: &Fields{
|
||||
Fields: []*Field{
|
||||
{
|
||||
Name: "string_attr",
|
||||
Modifiers: []*string{utils.Ptr("RequiresReplace"), utils.Ptr("UseStateForUnknown")},
|
||||
},
|
||||
{Name: "bool_attr", Modifiers: []*string{utils.Ptr("RequiresReplace")}},
|
||||
{Name: "int_attr", Modifiers: []*string{utils.Ptr("UseStateForUnknown")}},
|
||||
{Name: "list_attr", Modifiers: []*string{utils.Ptr("RequiresReplace")}},
|
||||
{Name: "Nested.sub_string", Modifiers: []*string{utils.Ptr("RequiresReplace")}},
|
||||
},
|
||||
},
|
||||
sch: &schema.Schema{
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"StringAttr": schema.StringAttribute{},
|
||||
"BoolAttr": schema.BoolAttribute{},
|
||||
"IntAttr": schema.Int64Attribute{},
|
||||
"ListAttr": schema.ListAttribute{},
|
||||
"Nested": schema.SingleNestedAttribute{
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"SubString": schema.StringAttribute{},
|
||||
},
|
||||
},
|
||||
"Unsupported": schema.MapAttribute{ElementType: types.StringType}, // Triggers default/warn case
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "validation error - invalid modifier",
|
||||
fields: &Fields{
|
||||
Fields: []*Field{
|
||||
{Name: "id", Modifiers: []*string{utils.Ptr("InvalidModifier")}},
|
||||
},
|
||||
},
|
||||
sch: &schema.Schema{
|
||||
Attributes: map[string]schema.Attribute{"id": schema.StringAttribute{}},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "validation error - empty modifier",
|
||||
fields: &Fields{
|
||||
Fields: []*Field{
|
||||
{Name: "id", Modifiers: []*string{utils.Ptr("")}},
|
||||
},
|
||||
},
|
||||
sch: &schema.Schema{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "nil fields - should return nil",
|
||||
fields: nil,
|
||||
sch: &schema.Schema{},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(
|
||||
tc.name, func(t *testing.T) {
|
||||
err := AddPlanModifiersToResourceSchema(tc.fields, tc.sch)
|
||||
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Fatalf("AddPlanModifiersToResourceSchema() error = %v, wantErr %v", err, tc.wantErr)
|
||||
}
|
||||
|
||||
if !tc.wantErr && tc.name == "full coverage - all types and nested structures" {
|
||||
// Check StringAttr
|
||||
if sAttr, ok := tc.sch.Attributes["StringAttr"].(schema.StringAttribute); ok {
|
||||
if len(sAttr.PlanModifiers) != 2 {
|
||||
t.Errorf("StringAttr: expected 2 modifiers, got %d", len(sAttr.PlanModifiers))
|
||||
}
|
||||
}
|
||||
|
||||
// Check Nested Sub-Attribute
|
||||
if nested, ok := tc.sch.Attributes["Nested"].(schema.SingleNestedAttribute); ok {
|
||||
if subAttr, ok := nested.Attributes["SubString"].(schema.StringAttribute); ok {
|
||||
if len(subAttr.PlanModifiers) != 1 {
|
||||
// Dies schlug vorher fehl, weil der Prefix "Nested" statt "nested" war
|
||||
t.Errorf("Nested SubString: expected 1 modifier, got %d", len(subAttr.PlanModifiers))
|
||||
}
|
||||
} else {
|
||||
t.Error("SubString attribute not found in Nested")
|
||||
}
|
||||
} else {
|
||||
t.Error("Nested attribute not found")
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFieldListToMap(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
fields *Fields
|
||||
want map[string][]*string
|
||||
}{
|
||||
{
|
||||
name: "convert list to map",
|
||||
fields: &Fields{
|
||||
Fields: []*Field{
|
||||
{Name: "test", Modifiers: []*string{utils.Ptr("mod")}},
|
||||
},
|
||||
},
|
||||
want: map[string][]*string{
|
||||
"test": {utils.Ptr("mod")},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nil fields",
|
||||
fields: nil,
|
||||
want: map[string][]*string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(
|
||||
tc.name, func(t *testing.T) {
|
||||
got := fieldListToMap(tc.fields)
|
||||
if diff := cmp.Diff(tc.want, got); diff != "" {
|
||||
t.Errorf("fieldListToMap() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleTypeMismatches(t *testing.T) {
|
||||
modifiers := []*string{utils.Ptr("RequiresReplace")}
|
||||
|
||||
t.Run(
|
||||
"bool type mismatch", func(t *testing.T) {
|
||||
_, err := handleBoolPlanModifiers(schema.StringAttribute{}, modifiers)
|
||||
if err == nil {
|
||||
t.Error("expected error for type mismatch in handleBoolPlanModifiers")
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
t.Run(
|
||||
"string type mismatch", func(t *testing.T) {
|
||||
_, err := handleStringPlanModifiers(schema.BoolAttribute{}, modifiers)
|
||||
if err == nil {
|
||||
t.Error("expected error for type mismatch in handleStringPlanModifiers")
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
t.Run(
|
||||
"int64 type mismatch", func(t *testing.T) {
|
||||
_, err := handleInt64PlanModifiers(schema.StringAttribute{}, modifiers)
|
||||
if err == nil {
|
||||
t.Error("expected error for type mismatch in handleInt64PlanModifiers")
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
t.Run(
|
||||
"list type mismatch", func(t *testing.T) {
|
||||
_, err := handleListPlanModifiers(schema.StringAttribute{}, modifiers)
|
||||
if err == nil {
|
||||
t.Error("expected error for type mismatch in handleListPlanModifiers")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -8,7 +8,8 @@ import (
|
|||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
||||
|
||||
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/core"
|
||||
)
|
||||
|
||||
// AdaptRegion rewrites the region of a terraform plan
|
||||
|
|
|
|||
12
stackit/internal/utils/strings.go
Normal file
12
stackit/internal/utils/strings.go
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
package utils
|
||||
|
||||
func RemoveQuotes(src string) string {
|
||||
var res string
|
||||
if src != "" && src[0] == '"' {
|
||||
res = src[1:]
|
||||
}
|
||||
if res != "" && res[len(res)-1] == '"' {
|
||||
res = res[:len(res)-1]
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
|
@ -20,7 +20,8 @@ import (
|
|||
"github.com/hashicorp/terraform-plugin-log/tflog"
|
||||
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
|
||||
"github.com/stackitcloud/stackit-sdk-go/core/utils"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
||||
|
||||
"tf-provider.git.onstackit.cloud/stackit-dev-tools/terraform-provider-stackitprivatepreview/stackit/internal/core"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue