Skip to content

Commit

Permalink
[Internal] Add maxItem=1 validator for object types in plugin framewo…
Browse files Browse the repository at this point in the history
…rk schema (#4094)

## Changes
<!-- Summary of your changes that are easy to understand -->
- Added `object` tag for object types in tfsdk struct
- Added `maxItem=1` validator for object types in the plugin framework
schema

## Tests
<!-- 
How is this tested? Please see the checklist below and also describe any
other relevant tests
-->

- [x] `make test` run locally
- [x] relevant change in `docs/` folder
- [x] covered with integration tests in `internal/acceptance`
- [x] relevant acceptance tests are passing
- [x] using Go SDK
  • Loading branch information
edwardfeng-db authored Oct 11, 2024
1 parent e1c683f commit 4a70e64
Show file tree
Hide file tree
Showing 23 changed files with 797 additions and 774 deletions.
2 changes: 1 addition & 1 deletion .codegen/model.go.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ type {{.PascalName}} struct {
{{end}}

{{- define "field-tag" -}}
{{if .IsJson}}tfsdk:"{{if and (ne .Entity.Terraform nil) (ne .Entity.Terraform.Alias "") }}{{.Entity.Terraform.Alias}}{{else}}{{.Name}}{{end}}" tf:"{{if not .Required}}optional{{end}}"{{else}}tfsdk:"-"{{end -}}
{{if .IsJson}}tfsdk:"{{if and (ne .Entity.Terraform nil) (ne .Entity.Terraform.Alias "") }}{{.Entity.Terraform.Alias}}{{else}}{{.Name}}{{end}}" tf:"{{- $first := true -}}{{- if not .Required -}}{{- if not $first -}},{{end}}optional{{- $first = false -}}{{- end -}}{{- if .Entity.IsObject -}}{{- if not $first -}},{{end}}object{{- $first = false -}}{{- end -}}"{{else}}tfsdk:"-"{{end -}}
{{- end -}}

{{- define "type" -}}
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ require (
github.com/hashicorp/logutils v1.0.0 // indirect
github.com/hashicorp/terraform-exec v0.21.0 // indirect
github.com/hashicorp/terraform-json v0.22.1 // indirect
github.com/hashicorp/terraform-plugin-framework-validators v0.13.0 // indirect
github.com/hashicorp/terraform-registry-address v0.2.3 // indirect
github.com/hashicorp/terraform-svchost v0.1.1 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ github.com/hashicorp/terraform-json v0.22.1 h1:xft84GZR0QzjPVWs4lRUwvTcPnegqlyS7
github.com/hashicorp/terraform-json v0.22.1/go.mod h1:JbWSQCLFSXFFhg42T7l9iJwdGXBYV8fmmD6o/ML4p3A=
github.com/hashicorp/terraform-plugin-framework v1.11.0 h1:M7+9zBArexHFXDx/pKTxjE6n/2UCXY6b8FIq9ZYhwfE=
github.com/hashicorp/terraform-plugin-framework v1.11.0/go.mod h1:qBXLDn69kM97NNVi/MQ9qgd1uWWsVftGSnygYG1tImM=
github.com/hashicorp/terraform-plugin-framework-validators v0.13.0 h1:bxZfGo9DIUoLLtHMElsu+zwqI4IsMZQBRRy4iLzZJ8E=
github.com/hashicorp/terraform-plugin-framework-validators v0.13.0/go.mod h1:wGeI02gEhj9nPANU62F2jCaHjXulejm/X+af4PdZaNo=
github.com/hashicorp/terraform-plugin-go v0.23.0 h1:AALVuU1gD1kPb48aPQUjug9Ir/125t+AAurhqphJ2Co=
github.com/hashicorp/terraform-plugin-go v0.23.0/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ=
github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0=
Expand Down
15 changes: 12 additions & 3 deletions internal/providers/pluginfw/tfschema/customizable_schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ import (
)

type TestTfSdk struct {
Description types.String `tfsdk:"description" tf:""`
Nested *NestedTfSdk `tfsdk:"nested" tf:"optional"`
Map map[string]types.String `tfsdk:"map" tf:"optional"`
Description types.String `tfsdk:"description" tf:""`
Nested *NestedTfSdk `tfsdk:"nested" tf:"optional"`
NestedSliceObject []NestedTfSdk `tfsdk:"nested_slice_object" tf:"optional,object"`
Map map[string]types.String `tfsdk:"map" tf:"optional"`
}

type NestedTfSdk struct {
Expand Down Expand Up @@ -121,3 +122,11 @@ func TestCustomizeSchemaAddPlanModifier(t *testing.T) {

assert.True(t, len(scm.Attributes["description"].(schema.StringAttribute).PlanModifiers) == 1)
}

func TestCustomizeSchemaObjectTypeValidatorAdded(t *testing.T) {
scm := ResourceStructToSchema(TestTfSdk{}, func(c CustomizableSchema) CustomizableSchema {
return c
})

assert.True(t, len(scm.Blocks["nested_slice_object"].(schema.ListNestedBlock).Validators) == 1)
}
119 changes: 65 additions & 54 deletions internal/providers/pluginfw/tfschema/struct_to_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,19 @@ import (

"github.com/databricks/terraform-provider-databricks/common"
"github.com/databricks/terraform-provider-databricks/internal/tfreflect"
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
dataschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
)

type structTag struct {
optional bool
computed bool
singleObject bool
}

func typeToSchema(v reflect.Value) NestedBlockObject {
scmAttr := map[string]AttributeBuilder{}
scmBlock := map[string]BlockBuilder{}
Expand All @@ -30,8 +38,7 @@ func typeToSchema(v reflect.Value) NestedBlockObject {
if fieldName == "-" {
continue
}
isOptional := fieldIsOptional(typeField)
isComputed := fieldIsComputed(typeField)
structTag := getStructTag(typeField)
kind := typeField.Type.Kind()
value := field.Value
typeFieldType := typeField.Type
Expand All @@ -52,42 +59,47 @@ func typeToSchema(v reflect.Value) NestedBlockObject {
case reflect.TypeOf(types.Bool{}):
scmAttr[fieldName] = ListAttributeBuilder{
ElementType: types.BoolType,
Optional: isOptional,
Required: !isOptional,
Computed: isComputed,
Optional: structTag.optional,
Required: !structTag.optional,
Computed: structTag.computed,
}
case reflect.TypeOf(types.Int64{}):
scmAttr[fieldName] = ListAttributeBuilder{
ElementType: types.Int64Type,
Optional: isOptional,
Required: !isOptional,
Computed: isComputed,
Optional: structTag.optional,
Required: !structTag.optional,
Computed: structTag.computed,
}
case reflect.TypeOf(types.Float64{}):
scmAttr[fieldName] = ListAttributeBuilder{
ElementType: types.Float64Type,
Optional: isOptional,
Required: !isOptional,
Computed: isComputed,
Optional: structTag.optional,
Required: !structTag.optional,
Computed: structTag.computed,
}
case reflect.TypeOf(types.String{}):
scmAttr[fieldName] = ListAttributeBuilder{
ElementType: types.StringType,
Optional: isOptional,
Required: !isOptional,
Computed: isComputed,
Optional: structTag.optional,
Required: !structTag.optional,
Computed: structTag.computed,
}
default:
// Nested struct
nestedScm := typeToSchema(reflect.New(elemType).Elem())
var validators []validator.List
if structTag.singleObject {
validators = append(validators, listvalidator.SizeAtMost(1))
}
scmBlock[fieldName] = ListNestedBlockBuilder{
NestedObject: NestedBlockObject{
Attributes: nestedScm.Attributes,
Blocks: nestedScm.Blocks,
},
Optional: isOptional,
Required: !isOptional,
Computed: isComputed,
Optional: structTag.optional,
Required: !structTag.optional,
Computed: structTag.computed,
Validators: validators,
}
}
} else if kind == reflect.Map {
Expand All @@ -102,30 +114,30 @@ func typeToSchema(v reflect.Value) NestedBlockObject {
case reflect.TypeOf(types.Bool{}):
scmAttr[fieldName] = MapAttributeBuilder{
ElementType: types.BoolType,
Optional: isOptional,
Required: !isOptional,
Computed: isComputed,
Optional: structTag.optional,
Required: !structTag.optional,
Computed: structTag.computed,
}
case reflect.TypeOf(types.Int64{}):
scmAttr[fieldName] = MapAttributeBuilder{
ElementType: types.Int64Type,
Optional: isOptional,
Required: !isOptional,
Computed: isComputed,
Optional: structTag.optional,
Required: !structTag.optional,
Computed: structTag.computed,
}
case reflect.TypeOf(types.Float64{}):
scmAttr[fieldName] = MapAttributeBuilder{
ElementType: types.Float64Type,
Optional: isOptional,
Required: !isOptional,
Computed: isComputed,
Optional: structTag.optional,
Required: !structTag.optional,
Computed: structTag.computed,
}
case reflect.TypeOf(types.String{}):
scmAttr[fieldName] = MapAttributeBuilder{
ElementType: types.StringType,
Optional: isOptional,
Required: !isOptional,
Computed: isComputed,
Optional: structTag.optional,
Required: !structTag.optional,
Computed: structTag.computed,
}
default:
// Nested struct
Expand All @@ -134,36 +146,36 @@ func typeToSchema(v reflect.Value) NestedBlockObject {
NestedObject: NestedAttributeObject{
Attributes: nestedScm.Attributes,
},
Optional: isOptional,
Required: !isOptional,
Computed: isComputed,
Optional: structTag.optional,
Required: !structTag.optional,
Computed: structTag.computed,
}
}
} else if kind == reflect.Struct {
switch value.Interface().(type) {
case types.Bool:
scmAttr[fieldName] = BoolAttributeBuilder{
Optional: isOptional,
Required: !isOptional,
Computed: isComputed,
Optional: structTag.optional,
Required: !structTag.optional,
Computed: structTag.computed,
}
case types.Int64:
scmAttr[fieldName] = Int64AttributeBuilder{
Optional: isOptional,
Required: !isOptional,
Computed: isComputed,
Optional: structTag.optional,
Required: !structTag.optional,
Computed: structTag.computed,
}
case types.Float64:
scmAttr[fieldName] = Float64AttributeBuilder{
Optional: isOptional,
Required: !isOptional,
Computed: isComputed,
Optional: structTag.optional,
Required: !structTag.optional,
Computed: structTag.computed,
}
case types.String:
scmAttr[fieldName] = StringAttributeBuilder{
Optional: isOptional,
Required: !isOptional,
Computed: isComputed,
Optional: structTag.optional,
Required: !structTag.optional,
Computed: structTag.computed,
}
case types.List:
panic(fmt.Errorf("types.List should never be used in tfsdk structs. %s", common.TerraformBugErrorMessage))
Expand All @@ -176,9 +188,9 @@ func typeToSchema(v reflect.Value) NestedBlockObject {
nestedScm := typeToSchema(sv)
scmBlock[fieldName] = ListNestedBlockBuilder{
NestedObject: nestedScm,
Optional: isOptional,
Required: !isOptional,
Computed: isComputed,
Optional: structTag.optional,
Required: !structTag.optional,
Computed: structTag.computed,
}
}
} else {
Expand All @@ -188,14 +200,13 @@ func typeToSchema(v reflect.Value) NestedBlockObject {
return NestedBlockObject{Attributes: scmAttr, Blocks: scmBlock}
}

func fieldIsComputed(field reflect.StructField) bool {
func getStructTag(field reflect.StructField) structTag {
tagValue := field.Tag.Get("tf")
return strings.Contains(tagValue, "computed")
}

func fieldIsOptional(field reflect.StructField) bool {
tagValue := field.Tag.Get("tf")
return strings.Contains(tagValue, "optional")
return structTag{
optional: strings.Contains(tagValue, "optional"),
computed: strings.Contains(tagValue, "computed"),
singleObject: strings.Contains(tagValue, "object"),
}
}

// ResourceStructToSchema builds a resource schema from a tfsdk struct, with custoimzations applied.
Expand Down
20 changes: 10 additions & 10 deletions internal/service/apps_tf/model.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 4a70e64

Please sign in to comment.