-
Notifications
You must be signed in to change notification settings - Fork 95
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
Conversation
…r/metaschema` packages
…ider/schema`, and `provider/metaschema`
…ted_attribute_validation_test.go
…PC to `ValidateResourceConfig` RPC
…State`, `UpgradeResourceState`, and `MoveResourceState` RPCs
# Conflicts: # go.mod # go.sum
There was a problem hiding this 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 😆 )
// 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()), | ||
) | ||
} |
There was a problem hiding this comment.
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
}
There was a problem hiding this comment.
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.", | |||
) | |||
} | |||
|
There was a problem hiding this comment.
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)
// 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 | ||
} | ||
} | ||
|
There was a problem hiding this comment.
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
// 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 | ||
} |
There was a problem hiding this comment.
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
// 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 | ||
} |
There was a problem hiding this comment.
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
)
if attribute.IsWriteOnly() && !val.IsNull() { | ||
logging.FrameworkDebug(ctx, "Nullifying write-only attribute in the newState") | ||
|
||
return tftypes.NewValue(newValueType, nil), nil | ||
} |
There was a problem hiding this comment.
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 🥳
"dynamic-underlying-string-nil-computed": schema.DynamicAttribute{ | ||
WriteOnly: true, | ||
}, |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- 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. |
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.