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

Add support for write only attributes #1044

Merged
merged 62 commits into from
Feb 10, 2025
Merged

Conversation

SBGoods
Copy link
Contributor

@SBGoods SBGoods commented Oct 2, 2024

Adds support for new write-only attribute feature in Terraform v1.11 or higher. Write-only attributes are managed resource attributes whose values are not saved to the Terraform plan or state artifacts and can accept ephemeral values.

A provider can declare an attribute as write-only in the schema using the WriteOnly field. Write-only attributes values are only available in the configuration, and the prior state, planned state, and final state values for write-only attributes should always be null.

@SBGoods SBGoods changed the title [DRAFT] Add support for write only attributes Add support for write only attributes Feb 6, 2025
@SBGoods SBGoods marked this pull request as ready for review February 6, 2025 16:41
@SBGoods SBGoods requested review from a team as code owners February 6, 2025 16:41
@SBGoods SBGoods requested a review from nandereck February 6, 2025 16:41
@SBGoods SBGoods added this to the v1.14.0 milestone Feb 6, 2025
@austinvalle austinvalle added the enhancement New feature or request label Feb 7, 2025
Copy link
Member

@austinvalle austinvalle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work! I left some comments on things that should probably be addressed before we release, but I think they are all small enough that they could be addressed in a follow-up PR (since this one is getting pretty big 😆 )

Comment on lines +132 to +143
// If the client doesn't support write-only attributes (first supported in Terraform v1.11.0), then we raise an early validation error
// to avoid a confusing data consistency error when the provider attempts to return "null" for a write-only attribute in the planned/final state.
//
// Write-only attributes can only be successfully used with a supporting client, so the only option for a practitoner to utilize a write-only attribute
// is to upgrade their Terraform CLI version to v1.11.0 or later.
if !req.ClientCapabilities.WriteOnlyAttributesAllowed && a.IsWriteOnly() && !attributeConfig.IsNull() {
resp.Diagnostics.AddAttributeError(
req.AttributePath,
"WriteOnly Attribute Not Allowed",
fmt.Sprintf("The resource contains a non-null value for WriteOnly attribute %s. Write-only attributes are only supported in Terraform 1.11 and later.", req.AttributePath.String()),
)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disclaimer that I believe this would be best resolved in a follow-up PR, since this PR is already pretty long

I believe this can be bypassed by dynamic write-only attributes and need some more extensive logic for checking the underlying value as well 😞
https://developer.hashicorp.com/terraform/plugin/framework/handling-data/dynamic-data#handling-underlying-null-and-unknown-values

I believe with this current logic, you would not observe an error < Terraform 1.11 for the following config

resource "examplecloud_thing" "example" {
  example_attribute_wo = var.null_string
}

variable "null_string" {
  type    = string
  default = null
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also looking at the logic above this, I believe the other behaviors would have that same problem

@@ -338,6 +339,18 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange
"Ensure all resource plan modifiers do not attempt to change resource plan data from being a null value if the request plan is a null value.",
)
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the conditional right above this should probably return (after adding the diagnostic)

Comment on lines +516 to +576
// RequiredWriteOnlyNilsAttributePaths returns a tftypes.Walk() function
// that populates reqWriteOnlyNilsPaths with the paths of Required and WriteOnly
// attributes that have a null value.
func RequiredWriteOnlyNilsAttributePaths(ctx context.Context, schema fwschema.Schema, reqWriteOnlyNilsPaths *path.Paths) func(path *tftypes.AttributePath, value tftypes.Value) (bool, error) {
return func(attrPath *tftypes.AttributePath, value tftypes.Value) (bool, error) {
// we are only modifying attributes, not the entire resource
if len(attrPath.Steps()) < 1 {
return true, nil
}

ctx = logging.FrameworkWithAttributePath(ctx, attrPath.String())

attribute, err := schema.AttributeAtTerraformPath(ctx, attrPath)

if err != nil {
if errors.Is(err, fwschema.ErrPathInsideAtomicAttribute) {
// atomic attributes can be nested block objects that contain child writeOnly attributes
return true, nil
}

if errors.Is(err, fwschema.ErrPathIsBlock) {
// blocks do not have the writeOnly field but can contain child writeOnly attributes
return true, nil
}

if errors.Is(err, fwschema.ErrPathInsideDynamicAttribute) {
return false, nil
}

logging.FrameworkError(ctx, "couldn't find attribute in resource schema")
return false, fmt.Errorf("couldn't find attribute in resource schema: %w", err)
}

if attribute.IsWriteOnly() {
if attribute.IsRequired() && value.IsNull() {
fwPath, diags := fromtftypes.AttributePath(ctx, attrPath, schema)
if diags.HasError() {
for _, diagErr := range diags.Errors() {
logging.FrameworkError(ctx,
"Error converting tftypes.AttributePath to path.Path",
map[string]any{
logging.KeyError: diagErr.Detail(),
},
)
}

return false, fmt.Errorf("couldn't convert tftypes.AttributePath to path.Path")
}
reqWriteOnlyNilsPaths.Append(fwPath)

// if the value is nil, there is no need to continue walking
return false, nil
}
// check for any writeOnly child attributes
return true, nil
}

return false, nil
}
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this function can be deleted? I don't see a reference to it anymore

Comment on lines +267 to +276
// If the write-only nullification results in a null state, then this is a provider error
if upgradeResourceStateResponse.State.Raw.Type() == nil || upgradeResourceStateResponse.State.Raw.IsNull() {
resp.Diagnostics.AddError(
"Missing Upgraded Resource State",
fmt.Sprintf("After attempting a resource state upgrade to version %d, the provider did not return any state data. ", req.Version)+
"Preventing the unexpected loss of resource state data. "+
"This is always an issue with the Terraform Provider and should be reported to the provider developer.",
)
return
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this conditional branch possible? I see we check it right above the NullifyWriteOnlyAttributes call, which implies that it's possible for NullifyWriteOnlyAttributes to return a nil type or null state when given a non-null state

Comment on lines +61 to +67
// If the attribute is dynamic set the new value type to DynamicPseudoType
// instead of the underlying concrete type
// TODO: verify if this is the correct behavior once Terraform Core implementation is complete
_, isDynamic := attribute.GetType().(basetypes.DynamicTypable)
if isDynamic {
newValueType = tftypes.DynamicPseudoType
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did we verify this TODO? My initial thought is that a dynamic write-only attribute should be nullified completely, including the type.

Which means that I don't think you need this conditional (since attribute.GetType().TerraformType(ctx) should return DynamicPseudoType for a DynamicAttribute)

Comment on lines +69 to +73
if attribute.IsWriteOnly() && !val.IsNull() {
logging.FrameworkDebug(ctx, "Nullifying write-only attribute in the newState")

return tftypes.NewValue(newValueType, nil), nil
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note: This behavior technically suffers from a comment I made above about underlying nulls for dynamic data, however, I think we always want to wipe out the type as well with nullification, so this should be correct.

So for a DynamicAttribute, this should get nullified:

Old: tftypes.NewValue(tftypes.String, nil)
New, after nullified: tftypes.NewValue(tftypes.DynamicPseudoType, nil)

Looks like you already have tests that confirm this 🥳

Comment on lines +42 to +44
"dynamic-underlying-string-nil-computed": schema.DynamicAttribute{
WriteOnly: true,
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: This isn't a valid schema, so not sure we should include it in the test like this

- Pair write-only arguments with a configuration attribute (required or optional) to “trigger” the use of the write-only argument
- For example, a `password_wo` write-only argument can be paired with a configured `password_wo_version` attribute. When the `password_wo_version` is modified, the provider will send the `password_wo` value to the API.
- Use a keepers attribute (which is used in the [Random Provider](https://registry.terraform.io/providers/hashicorp/random/latest/docs#resource-keepers)) that will take in arbitrary key-pair values. Whenever there is a change to the `keepers` attribute, the provider will use the write-only argument value.
- Use the resource's [private state] to store secure hashes of write-only argument values, the provider will then use the hash to determine if a write-only argument value has changed in later Terraform runs.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Use the resource's [private state] to store secure hashes of write-only argument values, the provider will then use the hash to determine if a write-only argument value has changed in later Terraform runs.
- Use the resource's [private state](/terraform/plugin/framework/resources/private-state) to store secure hashes of write-only argument values, the provider will then use the hash to determine if a write-only argument value has changed in later Terraform runs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants