Skip to content

Consider Automatic Reification of List/Map/Object/Set Value Types for Zero Values #716

Open
@bflad

Description

@bflad

Module version

v1.2.0

Use-cases

When a framework List/Map/Object/Set value type is present in a data model, e.g. the types.Object below

type ExampleResourceModel struct {
	ID                    types.String `tfsdk:"id"`
	Other                 types.String `tfsdk:"other"`
	SingleNestedAttribute types.Object `tfsdk:"single_nested_attribute"`
}

The zero-value for the value types implicitly cannot contain the correct underlying type implementation for the associated schema, e.g. for this example:

func (r *ExampleResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
	resp.Schema = schema.Schema{
		Attributes: map[string]schema.Attribute{
			"id": schema.StringAttribute{
				Computed: true,
				PlanModifiers: []planmodifier.String{
					stringplanmodifier.UseStateForUnknown(),
				},
			},
			"other": schema.StringAttribute{
				Optional: true,
				Computed: true,
				PlanModifiers: []planmodifier.String{
					stringplanmodifier.UseStateForUnknown(),
				},
			},
			"single_nested_attribute": schema.SingleNestedAttribute{
				Attributes: map[string]schema.Attribute{
					"nested_attribute": schema.StringAttribute{
						Optional: true,
					},
				},
				Optional: true,
			},
		},
	}
}

The types.Object zero-value does not contain the underlying nested_attribute object attribute name or its type.

It is possible for provider developers to run into situations where the zero-value is unknowingly being incorrectly used.

For example with CRUD logic:

func (r ExampleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
	var plan ExampleResourceModel

	resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)

	if resp.Diagnostics.HasError() {
		return
	}

	// API logic, etc.

	// Consider calling plan.XXX = YYY for Computed values instead of
	// this approach of instantiating a whole new model.
	state := ExampleResourceModel{
		ID:                    plan.ID,
		Other:                 plan.Other,
		// Missing SingleNestedAttribute
	}

	resp.Diagnostics.Append(resp.State.Get(ctx, &state)...)
}

For example with ImportState logic:

func (r ExampleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
	var data ExampleResourceModel

	data.ID = types.StringValue(req.ID)
	data.Other = data.ID
	// data.SingleNestedAttribute remains the types.Object zero-value

	resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

#715 should help somewhat with troubleshooting here, where an error is raised:

Error: Value Conversion Error

An unexpected error was encountered while verifying an attribute value
matched its expected type to prevent unexpected behavior or panics. This is
always an error in the provider. Please report the following to the provider
developer:

Expected type: types.ObjectType["nested_attribute":basetypes.StringType]
Value type: types.ObjectType[]
Path: single_nested_attribute

But this error is generic due to the generic nature of the shared reflection logic and it might not be clear what the next steps for resolving the issue are in this situation.

Attempted Solutions

Ideally, the provider code is fixed to either use the framework-provided request or response data to fetch the information. In CRUD logic, it may look like:

func (r ExampleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
	var plan ExampleResourceModel

	resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)

	if resp.Diagnostics.HasError() {
		return
	}

	// API logic, etc.

	// Consider calling plan.XXX = YYY for Computed values instead of
	// this approach of instantiating a whole new model.
	state := ExampleResourceModel{
		ID:                    plan.ID,
		Other:                 plan.Other,
		SingleNestedAttribute: types.ObjectNull(plan.SingleNestedAttribute.AttributeTypes(ctx)),
	}

	resp.Diagnostics.Append(resp.State.Get(ctx, &state)...)
}

In the awkward case of ImportState:

func (r ExampleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
	var data ExampleResourceModel
	var obj types.Object // can be avoided with awkward resp.State.Get(ctx, &data) instead

	data.ID = types.StringValue(req.ID)
	data.Other = data.ID

	resp.Diagnostics.Append(resp.State.GetAttribute(ctx, path.Root("single_nested_attribute"), &obj)...)

	if resp.Diagnostics.HasError() {
	 	return
	}

	data.SingleNestedAttribute = types.ObjectNull(obj.AttributeTypes(ctx))

	resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

Another valid fix would be using SetAttribute() over Set() so the value is not being overwritten with the zero-value.

func (r ExampleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
	id := types.StringValue(req.ID)
	resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...)
	resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("other"), id)...)
}

Proposal

One option here would be attempting to detect problematic zero-value value types when Set() is being called. The prior value should contain all the correct type information and a value state, so the framework could use that information to "ignore" the zero-value errantly being set. The underlying implementation to enable this would likely be quite complex.

It would be debatable whether a warning log or diagnostic should be raised if the framework does implement this behavior. The diagnostic is more ideal, however there is a decent chance it would be encountered by practitioners instead of developers. Developers may not notice a log though.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestreflectionIssues and PRs about the reflection subsystem used to convert between attr.Values and Go values.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions