Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Internal] Use Attributes by default for List Objects #4315

Merged
merged 18 commits into from
Dec 12, 2024
10 changes: 9 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,18 @@ We are migrating the resource from SDKv2 to Plugin Framework provider and hence
6. Create a PR and send it for review.

### Migrating resource to plugin framework
Ideally there shouldn't be any behaviour change when migrating a resource or data source to either Go SDk or Plugin Framework.
There must not be any behaviour change or schema change when migrating a resource or data source to either Go SDK or Plugin Framework.
- Please make sure there are no breaking differences due to changes in schema by running: `make diff-schema`.
- Integration tests shouldn't require any major changes.

By default, `ResourceStructToSchema` will convert a `types.List` field to a `ListAttribute` or `ListNestedAttribute`. For resources or data sources migrated from the SDKv2, `ListNestedBlock` must be used for such fields. To do this, call `cs.ConfigureAsSdkV2Compatible()` in the `ResourceStructToSchema` callback:
```go
resp.Schema = tfschema.ResourceStructToSchema(ctx, Resource{}, func(c tfschema.CustomizableSchema) tfschema.CustomizableSchema {
cs.ConfigureAsSdkV2Compatible()
// Add any additional configuration here
return cs
})
```

### Code Organization
Each resource and data source should be defined in package `internal/providers/plugnifw/products/<resource>`, e.g.: `internal/providers/plugnifw/products/volume` package will contain both resource, data sources and other utils specific to volumes. Tests (both unit and integration tests) will also remain in this package.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ func (r *LibraryResource) Metadata(ctx context.Context, req resource.MetadataReq

func (r *LibraryResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
attrs, blocks := tfschema.ResourceStructToSchemaMap(ctx, LibraryExtended{}, func(c tfschema.CustomizableSchema) tfschema.CustomizableSchema {
c.ConfigureAsSdkV2Compatible()
for field, attribute := range c.ToNestedBlockObject().Attributes {
switch attribute.(type) {
case tfschema.StringAttributeBuilder:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ func (r *QualityMonitorResource) Metadata(ctx context.Context, req resource.Meta

func (r *QualityMonitorResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
attrs, blocks := tfschema.ResourceStructToSchemaMap(ctx, MonitorInfoExtended{}, func(c tfschema.CustomizableSchema) tfschema.CustomizableSchema {
c.ConfigureAsSdkV2Compatible()
c.SetRequired("assets_dir")
c.SetRequired("output_schema_name")
c.SetReadOnly("monitor_version")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ func (r *ShareResource) Metadata(ctx context.Context, req resource.MetadataReque

func (r *ShareResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
attrs, blocks := tfschema.ResourceStructToSchemaMap(ctx, ShareInfoExtended{}, func(c tfschema.CustomizableSchema) tfschema.CustomizableSchema {
c.ConfigureAsSdkV2Compatible()
c.SetRequired("name")

c.AddPlanModifier(stringplanmodifier.RequiresReplace(), "name") // ForceNew
Expand Down
34 changes: 31 additions & 3 deletions internal/providers/pluginfw/tfschema/attribute_converter.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,34 @@
package tfschema

type BlockToAttributeConverter interface {
// ConvertBlockToAttribute converts a contained block to its corresponding attribute type.
ConvertBlockToAttribute(string) BaseSchemaBuilder
// Blockable is an interface that can be implemented by an AttributeBuilder to convert it to a BlockBuilder.
type Blockable interface {
// ToBlock converts the AttributeBuilder to a BlockBuilder.
ToBlock() BlockBuilder
}

// convertAttributesToBlocks converts all attributes implementing the Blockable interface to blocks, returning
// a new NestedBlockObject with the converted attributes and the original blocks.
func convertAttributesToBlocks(attributes map[string]AttributeBuilder, blocks map[string]BlockBuilder) NestedBlockObject {
newAttributes := make(map[string]AttributeBuilder)
newBlocks := make(map[string]BlockBuilder)
for name, attr := range attributes {
if lnab, ok := attr.(Blockable); ok {
newBlocks[name] = lnab.ToBlock()
} else {
newAttributes[name] = attr
}
}
for name, block := range blocks {
newBlocks[name] = block
}
if len(newAttributes) == 0 {
newAttributes = nil
}
if len(newBlocks) == 0 {
newBlocks = nil
}
return NestedBlockObject{
Attributes: newAttributes,
Blocks: newBlocks,
}
}
32 changes: 6 additions & 26 deletions internal/providers/pluginfw/tfschema/customizable_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,32 +202,12 @@ func (s *CustomizableSchema) SetReadOnly(path ...string) *CustomizableSchema {
return s
}

// ConvertToAttribute converts the last element of the path from a block to an attribute.
// It panics if the path is empty, if the path does not exist in the schema, or if the path
// points to an attribute, not a block.
func (s *CustomizableSchema) ConvertToAttribute(path ...string) *CustomizableSchema {
if len(path) == 0 {
panic(fmt.Errorf("ConvertToAttribute called on root schema. %s", common.TerraformBugErrorMessage))
}
field := path[len(path)-1]

cb := func(attr BaseSchemaBuilder) BaseSchemaBuilder {
switch a := attr.(type) {
case BlockToAttributeConverter:
return a.ConvertBlockToAttribute(field)
default:
panic(fmt.Errorf("ConvertToAttribute called on invalid attribute type: %s. %s", reflect.TypeOf(attr).String(), common.TerraformBugErrorMessage))
}
}

// We have to go only as far as the second-to-last entry, since we need to change the parent schema
// by moving the last entry from a block to an attribute.
if len(path) == 1 {
s.attr = cb(s.attr)
} else {
navigateSchemaWithCallback(&s.attr, cb, path[0:len(path)-1]...)
}

// ConfigureAsSdkV2Compatible modifies the underlying schema to be compatible with SDKv2. This method must
// be called on all resources that were originally implemented using the SDKv2 and are migrated to the plugin
// framework.
func (s *CustomizableSchema) ConfigureAsSdkV2Compatible() *CustomizableSchema {
nbo := s.attr.(SingleNestedBlockBuilder).NestedObject
s.attr = SingleNestedBlockBuilder{NestedObject: convertAttributesToBlocks(nbo.Attributes, nbo.Blocks)}
return s
}

Expand Down
113 changes: 30 additions & 83 deletions internal/providers/pluginfw/tfschema/customizable_schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func TestCustomizeSchemaSetRequired(t *testing.T) {
return c
})

assert.True(t, scm.Blocks["nested"].(schema.ListNestedBlock).NestedObject.Attributes["enabled"].IsRequired())
assert.True(t, scm.Attributes["nested"].(schema.ListNestedAttribute).NestedObject.Attributes["enabled"].IsRequired())
}

func TestCustomizeSchemaSetOptional(t *testing.T) {
Expand All @@ -93,7 +93,7 @@ func TestCustomizeSchemaSetSensitive(t *testing.T) {
return c
})

assert.True(t, scm.Blocks["nested"].(schema.ListNestedBlock).NestedObject.Attributes["name"].IsSensitive())
assert.True(t, scm.Attributes["nested"].(schema.ListNestedAttribute).NestedObject.Attributes["name"].IsSensitive())
}

func TestCustomizeSchemaSetDeprecated(t *testing.T) {
Expand Down Expand Up @@ -127,7 +127,7 @@ func (testTfSdkListNestedAttribute) GetComplexFieldTypes(context.Context) map[st

func TestCustomizeSchemaSetReadOnly_RecursivelySetsFieldsOfListNestedAttributes(t *testing.T) {
scm := ResourceStructToSchema(context.Background(), testTfSdkListNestedAttribute{}, func(c CustomizableSchema) CustomizableSchema {
c.ConvertToAttribute("list").SetReadOnly("list")
c.SetReadOnly("list")
return c
})
for _, field := range []string{"name", "enabled"} {
Expand Down Expand Up @@ -160,12 +160,13 @@ func TestCustomizeSchemaObjectTypeValidatorAdded(t *testing.T) {
return c
})

assert.True(t, len(scm.Blocks["nested_slice_object"].(schema.ListNestedBlock).Validators) == 1)
assert.True(t, len(scm.Attributes["nested_slice_object"].(schema.ListNestedAttribute).Validators) == 1)
}

func TestCustomizeSchema_SetRequired_PanicOnBlock(t *testing.T) {
assert.Panics(t, func() {
_ = ResourceStructToSchema(context.Background(), TestTfSdk{}, func(c CustomizableSchema) CustomizableSchema {
c.ConfigureAsSdkV2Compatible()
c.SetRequired("nested")
return c
})
Expand All @@ -175,6 +176,7 @@ func TestCustomizeSchema_SetRequired_PanicOnBlock(t *testing.T) {
func TestCustomizeSchema_SetOptional_PanicOnBlock(t *testing.T) {
assert.Panics(t, func() {
_ = ResourceStructToSchema(context.Background(), TestTfSdk{}, func(c CustomizableSchema) CustomizableSchema {
c.ConfigureAsSdkV2Compatible()
c.SetOptional("nested")
return c
})
Expand All @@ -184,6 +186,7 @@ func TestCustomizeSchema_SetOptional_PanicOnBlock(t *testing.T) {
func TestCustomizeSchema_SetSensitive_PanicOnBlock(t *testing.T) {
assert.Panics(t, func() {
_ = ResourceStructToSchema(context.Background(), TestTfSdk{}, func(c CustomizableSchema) CustomizableSchema {
c.ConfigureAsSdkV2Compatible()
c.SetSensitive("nested")
return c
})
Expand All @@ -193,6 +196,7 @@ func TestCustomizeSchema_SetSensitive_PanicOnBlock(t *testing.T) {
func TestCustomizeSchema_SetReadOnly_PanicOnBlock(t *testing.T) {
assert.Panics(t, func() {
_ = ResourceStructToSchema(context.Background(), TestTfSdk{}, func(c CustomizableSchema) CustomizableSchema {
c.ConfigureAsSdkV2Compatible()
c.SetReadOnly("nested")
return c
})
Expand All @@ -202,6 +206,7 @@ func TestCustomizeSchema_SetReadOnly_PanicOnBlock(t *testing.T) {
func TestCustomizeSchema_SetComputed_PanicOnBlock(t *testing.T) {
assert.Panics(t, func() {
_ = ResourceStructToSchema(context.Background(), TestTfSdk{}, func(c CustomizableSchema) CustomizableSchema {
c.ConfigureAsSdkV2Compatible()
c.SetComputed("nested")
return c
})
Expand Down Expand Up @@ -258,22 +263,21 @@ func (m mockValidator) ValidateObject(context.Context, validator.ObjectRequest,
var _ validator.List = mockValidator{}
var _ validator.Object = mockValidator{}

func TestCustomizeSchema_ConvertToAttribute(t *testing.T) {
func TestCustomizeSchema_ConfigureAsSdkV2Compatible(t *testing.T) {
v := mockValidator{}
pm := mockPlanModifier{}
testCases := []struct {
name string
baseSchema NestedBlockObject
path []string
want NestedBlockObject
expectPanic bool
}{
{
name: "ListNestedBlock",
name: "ListNestedAttribute",
baseSchema: NestedBlockObject{
Blocks: map[string]BlockBuilder{
"list": ListNestedBlockBuilder{
NestedObject: NestedBlockObject{
Attributes: map[string]AttributeBuilder{
"list": ListNestedAttributeBuilder{
NestedObject: NestedAttributeObject{
Attributes: map[string]AttributeBuilder{
"attr": StringAttributeBuilder{},
},
Expand All @@ -284,11 +288,10 @@ func TestCustomizeSchema_ConvertToAttribute(t *testing.T) {
},
},
},
path: []string{"list"},
want: NestedBlockObject{
Attributes: map[string]AttributeBuilder{
"list": ListNestedAttributeBuilder{
NestedObject: NestedAttributeObject{
Blocks: map[string]BlockBuilder{
"list": ListNestedBlockBuilder{
NestedObject: NestedBlockObject{
Attributes: map[string]AttributeBuilder{
"attr": StringAttributeBuilder{},
},
Expand All @@ -301,14 +304,14 @@ func TestCustomizeSchema_ConvertToAttribute(t *testing.T) {
},
},
{
name: "ListNestedBlock/CalledOnInnerBlock",
name: "ListNestedAttribute/RecursiveBlocks",
baseSchema: NestedBlockObject{
Blocks: map[string]BlockBuilder{
"list": ListNestedBlockBuilder{
NestedObject: NestedBlockObject{
Blocks: map[string]BlockBuilder{
"nested_block": ListNestedBlockBuilder{
NestedObject: NestedBlockObject{
Attributes: map[string]AttributeBuilder{
"list": ListNestedAttributeBuilder{
NestedObject: NestedAttributeObject{
Attributes: map[string]AttributeBuilder{
"nested_block": ListNestedAttributeBuilder{
NestedObject: NestedAttributeObject{
Attributes: map[string]AttributeBuilder{
"attr": StringAttributeBuilder{},
},
Expand All @@ -319,14 +322,13 @@ func TestCustomizeSchema_ConvertToAttribute(t *testing.T) {
},
},
},
path: []string{"list", "nested_block"},
want: NestedBlockObject{
Blocks: map[string]BlockBuilder{
"list": ListNestedBlockBuilder{
NestedObject: NestedBlockObject{
Attributes: map[string]AttributeBuilder{
"nested_block": ListNestedAttributeBuilder{
NestedObject: NestedAttributeObject{
Blocks: map[string]BlockBuilder{
"nested_block": ListNestedBlockBuilder{
NestedObject: NestedBlockObject{
Attributes: map[string]AttributeBuilder{
"attr": StringAttributeBuilder{},
},
Expand All @@ -339,23 +341,8 @@ func TestCustomizeSchema_ConvertToAttribute(t *testing.T) {
},
},
{
name: "SingleNestedBlock",
name: "SingleNestedBlock/Panics",
baseSchema: NestedBlockObject{
Blocks: map[string]BlockBuilder{
"single": SingleNestedBlockBuilder{
NestedObject: NestedBlockObject{
Attributes: map[string]AttributeBuilder{
"attr": StringAttributeBuilder{},
},
},
DeprecationMessage: "deprecated",
Validators: []validator.Object{v},
PlanModifiers: []planmodifier.Object{pm},
},
},
},
path: []string{"single"},
want: NestedBlockObject{
Attributes: map[string]AttributeBuilder{
"single": SingleNestedAttributeBuilder{
Attributes: map[string]AttributeBuilder{
Expand All @@ -367,57 +354,17 @@ func TestCustomizeSchema_ConvertToAttribute(t *testing.T) {
},
},
},
},
{
name: "SingleNestedBlock/RecursiveBlocks",
baseSchema: NestedBlockObject{
Blocks: map[string]BlockBuilder{
"single": SingleNestedBlockBuilder{
NestedObject: NestedBlockObject{
Blocks: map[string]BlockBuilder{
"nested_block": ListNestedBlockBuilder{
NestedObject: NestedBlockObject{
Attributes: map[string]AttributeBuilder{
"attr": StringAttributeBuilder{},
},
},
},
},
},
},
},
},
path: []string{"single"},
want: NestedBlockObject{
Attributes: map[string]AttributeBuilder{
"single": SingleNestedAttributeBuilder{
Attributes: map[string]AttributeBuilder{
"nested_block": ListNestedAttributeBuilder{
NestedObject: NestedAttributeObject{
Attributes: map[string]AttributeBuilder{
"attr": StringAttributeBuilder{},
},
},
},
},
},
},
},
},
{
name: "PanicOnEmptyPath",
path: nil,
expectPanic: true,
},
}
for _, c := range testCases {
t.Run(c.name, func(t *testing.T) {
if c.expectPanic {
assert.Panics(t, func() {
ConstructCustomizableSchema(c.baseSchema).ConvertToAttribute(c.path...)
ConstructCustomizableSchema(c.baseSchema).ConfigureAsSdkV2Compatible()
})
} else {
got := ConstructCustomizableSchema(c.baseSchema).ConvertToAttribute(c.path...)
got := ConstructCustomizableSchema(c.baseSchema).ConfigureAsSdkV2Compatible()
assert.Equal(t, c.want, got.attr.(SingleNestedBlockBuilder).NestedObject)
}
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,12 @@ func (a ListNestedAttributeBuilder) AddPlanModifier(v planmodifier.List) BaseSch
a.PlanModifiers = append(a.PlanModifiers, v)
return a
}

func (a ListNestedAttributeBuilder) ToBlock() BlockBuilder {
return ListNestedBlockBuilder{
NestedObject: convertAttributesToBlocks(a.NestedObject.Attributes, nil),
DeprecationMessage: a.DeprecationMessage,
Validators: a.Validators,
PlanModifiers: a.PlanModifiers,
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package tfschema

import (
"fmt"

"github.com/databricks/terraform-provider-databricks/common"
dataschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
Expand Down Expand Up @@ -102,3 +105,7 @@ func (a SingleNestedAttributeBuilder) AddPlanModifier(v planmodifier.Object) Att
a.PlanModifiers = append(a.PlanModifiers, v)
return a
}

func (a SingleNestedAttributeBuilder) ToBlock() BlockBuilder {
panic(fmt.Errorf("ToBlock() called on SingleNestedAttributeBuilder. This means that the corresponding field is a types.Object, which should never happen for legacy resources. %s", common.TerraformBugErrorMessage))
}
Loading
Loading