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

Resource schema-based support for static default values #668

Closed
bendbennett opened this issue Feb 10, 2023 · 1 comment · Fixed by #674
Closed

Resource schema-based support for static default values #668

bendbennett opened this issue Feb 10, 2023 · 1 comment · Fixed by #674
Labels
enhancement New feature or request
Milestone

Comments

@bendbennett
Copy link
Contributor

bendbennett commented Feb 10, 2023

Module version

v1.1.1

Use-cases

Provider developers have been desiring (#285, #516) the framework support default value plan modifications more easily. Any schema-based or resource-based plan modification for default values must be manually implemented by provider developers currently. The prior SDK supported Default and DefaultFunc fields for this use case.

Attempted Solutions

Provider developers have been using attribute-based or resource-level plan modifiers with varying degrees of success owing to the fact that when Terraform constructs the proposed new state for a computed attribute with a null configuration, it will copy the prior state. If the attribute met the following criteria, the framework will either not show an expected plan to fix the value or propose a plan missing the unknown value marking for other attributes:

  • Attribute is optional and computed.
  • Attribute has a default value, potentially with an associated plan modifier.
  • Attribute is previously configured to a value that differs from the default value, then the configuration is removed.

Provider developers do not have a straightforward methodology for handling this situation, given all provider-defined logic occurs after the unknown marking (#183, #456, #615).

Proposal

Add resource schema-based support for static default values to the framework. This default value handling would occur during the PlanResourceChange RPC and modify the Terraform-provided proposed new state value before the framework performs its check between the proposed new state and prior state to automatically mark computed attributes with null configuration as unknown. The framework would handle the semantics of only consulting the default value handling when the configuration is null.

Similar to the existing type-specific plan modifier support in schema attributes, the default value handling would require implementations to use new typed interfaces. These interfaces would require description methods for future documentation generation needs and a logical method. The framework will implement common use case default value handlers, such as static default value implementations for each type. Provider developers could create their own custom default value implementations to suit more advanced use cases.

Existing resource create and update logic would require no updates assuming it is correctly consulting plan data already, which would have existing bugs if they were not. Existing resource read logic would require no updates unless the default value is not returned in the refreshed state, which would have already required special logic or raised perpetual plan differences. Existing resource configure, delete, import, and upgrade state logic is unaffected.

Existing schema-based plan modifiers that are attempting to handle default values should migrate to the new default value handling to simplify implementations and prevent the unexpected plan differences mentioned in the Background section. This will be denoted in the CHANGELOG for this feature.

An example resource attribute implementation after this change would look like:

"example_attr": schema.StringAttribute{
  // ... other fields ...
  Default: stringdefault.StaticValue("hello"),
},

Default Value Interfaces

Introduce a new resource/schema/default package. It will implement the interfaces, request, and response types for framework-defined and provider-defined implementations, similar to the resource/schema/planmodifier package.

For example:

type Describer interface {
  Description(context.Context) string
  MarkdownDescription(context.Context) string
}

type String interface {
	Describer

	DefaultString(context.Context, StringRequest, *StringResponse)
}

type StringRequest struct {
  Path path.Path
}

type StringResponse struct {
  Diagnostics diag.Diagnostics
  PlanValue types.String
}

Framework-Defined Default Value Implementations

Introduce new resource/schema/{TYPE}default packages, similar to the existing resource/schema/{TYPE}planmodifier packages. These will implement the framework-defined default value handlers, such as static string value default handling. These will be included in the framework, rather than split out like validators in the terraform-plugin-framework-validators repository, as they should be rather trivial in implementation and therefore have less versioning concerns once implemented.

For the initial implementation, only "static value" handlers will be introduced. As other use cases are decided for inclusion in the framework based on ecosystem desire, they can be incrementally introduced.

For example:

func StaticValue(default string) default.String {
	return staticValueDefault{
		default: default,
	}
}

type staticValueDefault struct{
  default string
}

func (d staticValueDefault) Description(_ context.Context) string {
	return "value defaults to "+d.default
}

func (d staticValueDefault) MarkdownDescription(_ context.Context) string {
	return "value defaults to `"+d.default+"`"
}

func (d staticValueDefault) DefaultString(_ context.Context, req default.StringRequest, resp *default.StringResponse) {
  resp.PlanValue = types.StringValue(d.default)
}

Framework Schema Interface

In the internal/fwschema/fwxschema package, new type-specific interfaces for default value handling would be introduced.

For example:

type AttributeWithStringDefaultValue interface {
	fwschema.Attribute

	DefaultValue() default.String
}

Resource Attribute Interface

In the resource/schema package, existing attribute implementation types would be updated to check for the new fwxschema interface, implement a new Default field, and implement the DefaultValue method to satisfy the fwxschema interface.

For example:

var (
	_ Attribute                                  = StringAttribute{}
	_ fwxschema.AttributeWithStringDefaultValue  = StringAttribute{} // new
	_ fwxschema.AttributeWithStringPlanModifiers = StringAttribute{}
	_ fwxschema.AttributeWithStringValidators    = StringAttribute{}
)

type StringAttribute struct {
	// ... existing fields ...
	Default default.String
}

func (a StringAttribute) DefaultValue() {
	return a.Default
}

Framework Schema Data

In the internal/fwschemadata package, the Data type can implement a new transform method which walks the schema and applies schema defined default values when the source Data type contains a null value at the same path.

For example:

func (d *Data) TransformPlanDefaults(ctx context.Context, config Data) diag.Diagnostics {
	// Errors are handled as richer diag.Diagnostics instead.
	d.TerraformValue, _ = tftypes.Transform(d.TerraformValue, func(tfTypePath *tftypes.AttributePath, tfTypeValue tftypes.Value) (tftypes.Value, error) {
		// ...
	})
}

Framework Server

In the internal/fwserver package, the PlanResourceChange method logic can be updated to call the new fwschemadata transform method on the proposed new state data before its checked against the prior state. The existing unknown marking transformer will need to be updated to skip over attributes that declare the new default value handling, so it does not undo what the default value handling just transformed.

For example:

func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChangeRequest, resp *PlanResourceChangeResponse) {
	// ... existing logic ...

	resp.PlannedState = planToState(*req.ProposedNewState)

	// not accurate at the moment, but as pseudo-code
	resp.PlannedState.TransformPlanDefaults(ctx, req.Config)

	if !resp.PlannedState.Raw.IsNull() && !resp.PlannedState.Raw.Equal(req.PriorState.Raw) {

	// ... existing logic ...
}

References

@bendbennett bendbennett added the enhancement New feature or request label Feb 10, 2023
@bendbennett bendbennett added this to the v1.2.0 milestone Feb 10, 2023
bendbennett added a commit that referenced this issue Feb 16, 2023
bendbennett added a commit that referenced this issue Mar 6, 2023
bendbennett added a commit that referenced this issue Mar 7, 2023
bendbennett added a commit that referenced this issue Mar 7, 2023
bendbennett added a commit that referenced this issue Mar 10, 2023
bendbennett added a commit that referenced this issue Mar 20, 2023
bendbennett added a commit that referenced this issue Mar 20, 2023
* Adding Default Value interfaces (#668)

* Fixing docs (#668)

* Adding framework-defined default value interfaces (#668)

* Add framework schema interface (#668)

* Add resource attribute interface (#668)

* Adding TransformPlanDefaults for BoolAttribute (#668)

* Modifying TransformDefaults and incorporating into PlanResourceChange (#668)

* Add handling for float64, int64, number and string defaults in TransformDefaults (#668)

* Add handling to prevent MarkComputedNilsAsUnknown overwriting default values (#668)

* Add handling for list, map, object and set (#668)

* Add handling for list, map, object and set in PlanResourceChange (#668)

* Adding test coverage for list nested attribute (#668)

* Refactoring func name to {TYPE}DefaultValue() (#668)

* Removing TransformDefaults() from tfsdk.State so as not to expose it in the public API (#668)

* Updating documentation for Default on resource schema attributes (#668)

* Adding Default to remaining resource schema attributes (#668)

* Adding further test coverage for TransformDefaults() for list, map, set, single nested attributes (#668)

* Adding further test coverage for PlanResourceChange() for map, set and single nested attributes (#668)

* Removing erroneous character (#668)

* Updating tests to use Computed everywhere (#668)

* Adding schema validation to only permit default values on computed attributes (#668)

* Adding docs for default values (#668)

* Further updates for docs (#668)

* Apply suggestions from code review

Co-authored-by: Brian Flad <bflad417@gmail.com>

* Updates following code review (#668)

* Apply suggestions from code review

Co-authored-by: Brian Flad <bflad417@gmail.com>

* Renaming test functions for consistency (#668)

* Fixing link (#668)

---------

Co-authored-by: Brian Flad <bflad417@gmail.com>
@github-actions
Copy link

I'm going to lock this issue because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active issues.
If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Apr 20, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant