-
Notifications
You must be signed in to change notification settings - Fork 94
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 type-based provider-defined function parameter validation #968
Conversation
…eter-type-validation
…cError and add switch to handle validation.ValidateableParameter interface
…lidateableParameter interface
…validation.ValidateableParameter interface
…o avoid import cycle
…eter-type-validation
…idateableAttribute assertions
…dateableAttribute assertions
…alueWithValidateAttributeWarning
…dle ValidateableAttribute assertions
…omFloat(), FromBigFloat(), and FromBigInt() functions to handle ValidateableAttribute assertions
… handle ValidateableAttribute assertions
…andle ValidateableAttribute assertions
…handle ValidateableAttribute assertions
…eter-type-validation
…nction parameters in ArgumentsData() function
…ge to xfwfunction package Parameter() function * xfwfunction package is purely to avoid import cycles
…nd validation.ValidateableParameter
…eter-type-validation
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.
Looks great! I left some general thoughts around package structure and some of the questions you asked.
} | ||
|
||
switch t := res.(type) { | ||
case xattr.ValidateableAttribute: |
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.
A consequence of the reflect
package's interface, but I wonder if there will be general confusion around "which validation logic" is being called and when.
For example, in the case of a custom type only being used on a function parameter, it might not implement this ValidateableAttribute
interface. When the original ArgumentData
is being built from tfplugin-go, it would call the ValidateableParameter
interface, but when calling (ArgumentData).Get
or (ResultData).Set
it would be using the ValidateableAttribute
interface (via the reflection) and therefore not get called.
I wonder how big of a refactor it would be to completely remove validation from the reflect
package and make it the responsibility of the caller to validate the reflection value is valid. So (ArgumentData).Get
, (ResultData).Set
, (Data).Get
, (Data).Set
, etc. would need to perform this validation.
Or perhaps we should be attempting to add the parameter validation into the reflect
logic, although we'll likely run into import cycles there because of FuncError
, so maybe if we don't do anything here it's just a documentation problem?
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.
For example, in the case of a custom type only being used on a function parameter, it might not implement this ValidateableAttribute interface. When the original ArgumentData is being built from tfplugin-go, it would call the ValidateableParameter interface, but when calling (ArgumentData).Get or (ResultData).Set it would be using the ValidateableAttribute interface (via the reflection) and therefore not get called.
If a custom type that is only being used as a function parameter implements ValidateableParameter
but does not implement ValidateableAttribute
, the call to ValidateParameter()
will occur during the CallFunction
RPC when fromproto5/6.ArgumentsData()
is called. The call to fromproto5/6.ArgumentsData()
occurs, prior to calling (ArgumentData).Get()
or (ResultData).Set()
, the latter of which are typically called within the Run()
method of the provider-defined function. Does this answer your concern?
I wonder how big of a refactor it would be to completely remove validation from the reflect package and make it the responsibility of the caller to validate the reflection value is valid. So (ArgumentData).Get, (ResultData).Set, (Data).Get, (Data).Set, etc. would need to perform this validation.
I chatted with Brian about this out-of-band, as I was also wondering about the embedded validation within reflection. I think this is worthy of further investigation but I believe would be best suited as a follow-up issue/discussion.
Or perhaps we should be attempting to add the parameter validation into the reflect logic, although we'll likely run into import cycles there because of FuncError, so maybe if we don't do anything here it's just a documentation problem?
In terms of the documentation, is the concern that it may not be clear under which circumstances to implement ValidateableParameter
vs ValidateableAttribute
? There's some updates to the docs in this PR to try and make this clear, but let me know if you think this should be expanded.
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.
Definitely agree about any refactoring removing validation from reflection should be a separate issue 👍🏻
If a custom type that is only being used as a function parameter implements
ValidateableParameter
but does not implementValidateableAttribute
, the call toValidateParameter()
will occur during theCallFunction
RPC whenfromproto5/6.ArgumentsData()
is called. The call tofromproto5/6.ArgumentsData()
occurs, prior to calling(ArgumentData).Get()
or(ResultData).Set()
, the latter of which are typically called within theRun()
method of the provider-defined function. Does this answer your concern?
That answers the (ArgumentData).Get()
portion, but is there an equivalent for the (ResultData).Set()
direction?
For example sake, let's say that iptypes.IPv4AddressType
only implements ValidateableParameter
func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) {
resp.Definition = function.Definition{
Parameters: []function.Parameter{
function.StringParameter{
Name: "ip_address",
CustomType: iptypes.IPv4AddressType{},
},
},
Return: function.StringReturn{
CustomType: iptypes.IPv4AddressType{},
},
}
}
func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) {
var arg string
resp.Error = req.Arguments.Get(ctx, &arg)
resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, "not an IP address"))
}
I think based on what you described, the req.Arguments.Get
would be protected implicitly because of the fromproto
logic already calling ValidateableParameter
, but the resp.Result.Set
wouldn't call validation because the custom type doesn't implement ValidateableAttribute
.
If that's the case, maybe a Go doc comment could reflect that ValidateableAttribute
is used during reflection? This does feel like enough of an "edge" that it shouldn't be a regular problem (I would expect provider developers using custom types to not use reflection for setting?)
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.
This is an interesting case! If we want to explicitly validate values that are being passed to Result.Set
it seems that maybe neither ValidateableParameter
nor ValidateableAttribute
are appropriate interfaces, as the value is neither a parameter nor an attribute.
One option might be to consider a Validateable
interface that operates solely on the value. Perhaps something along the following lines:
type Validateable interface {
Validate(context.Context, ValidateRequest, *ValidateResponse)
}
type ValidateRequest struct{}
type ValidateResponse struct {
Error *FuncError
}
A custom type that was to be used for validating a provider-defined function parameter, and the result from the function could be defined as follows:
func (v CustomStringValue) ValidateParameter(ctx context.Context, req function.ValidateParameterRequest, resp *function.ValidateParameterResponse) {
if v.IsNull() || v.IsUnknown() {
return
}
if !v.isValid(v.ValueString()) {
resp.Error = function.NewArgumentFuncError(
req.Position,
fmt.Sprintf("Invalid String Value: value %q length does not equal %d", v.ValueString(), 10))
}
}
func (v CustomStringValue) Validate(ctx context.Context, req function.ValidateRequest, resp *function.ValidateResponse) {
if v.IsNull() || v.IsUnknown() {
return
}
if !v.isValid(v.ValueString()) {
resp.Error = function.NewFuncError(
fmt.Sprintf("Invalid String Value: value %q length does not equal %d", v.ValueString(), 10))
}
}
func (v CustomStringValue) isValid(in string) bool {
return len(in) == 10
}
The function definition could then be specified as:
func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) {
resp.Definition = function.Definition{
Parameters: []function.Parameter{
function.StringParameter{
CustomType: CustomStringType{},
Name: "arg0",
},
},
Return: function.StringReturn{
CustomType: CustomStringType{},
},
}
}
The validation of the result value could then be handled by modifying the ResultData.Set
method:
func (d *ResultData) Set(ctx context.Context, value any) *FuncError {
reflectValue, reflectDiags := fwreflect.FromValue(ctx, d.value.Type(ctx), value, path.Empty())
funcErr := FuncErrorFromDiags(ctx, reflectDiags)
if funcErr != nil {
return funcErr
}
if v, ok := reflectValue.(Validateable); ok {
resp := ValidateResponse{}
v.Validate(ctx, ValidateRequest{}, &resp)
if resp.Error != nil {
return resp.Error
}
}
d.value = reflectValue
return nil
}
Thoughts?
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.
In the case of something like Validateable
, I'd assume it would end up just returning a plain error
🤔. I'm not 100% sure adding a new interface just for validation in reflection is worth it, but I think the general problem has been shown here.
I.e. the ValidateableAttribute
interface is being used for more than just attributes. Perhaps something that we can't avoid? Something we don't necessarily need to avoid? 🤷🏻
Would be interested to hear the rest of the teams thoughts on this, although I don't think it's really a showstopper
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.
This is a tough problem, I'm reluctant to expose a new interface just for reflection which is more of an implementation detail than it is a general Terraform provider development concept like parameter, attribute, or value. I think documenting that the ValidatebleAttribute
is being used in reflection logic in the Go doc is good enough for now and we can tackle this issue later own when we look into decoupling validation from the reflection logic.
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.
Thanks for the feedback. Ok, so I've amended the Go doc for ValidateableAttribute
as follows:
// ValidateableAttribute defines an interface for validating an attribute value.
// The ValidateAttribute method is called implicitly by the framework when value
// types from Terraform are converted into framework types.
type ValidateableAttribute interface {
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.
LGTM 👍🏻
) | ||
|
||
// Float64Typable extends attr.Type for float64 types. | ||
// Implement this interface to create a custom Float64Type type. | ||
type Float64Typable interface { | ||
//nolint:staticcheck // xattr.TypeWithValidate is deprecated, but we still need to support it. |
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.
For the types still implementing the legacy validation (float64, int64, list, set, map), should we create a follow-up issue to add this validation implementations for both ValidateableParameter
and ValidateableAttribute
?
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 was wondering about this. For float64 and int64 I was thinking that this may not be necessary as the calls to <Float64|Int64>Type.ValueFromTerraform
ensure that the tftypes.Value
supplied can be represented as 64-bit floating point, and integer values, respectively. The constructors on the corresponding value types only accept 64-bit floating point, and integer values too (e.g., NewFloat64Value
). So it doesn't seem that there is a way to generate a float64 or int64 value type that would have a value that couldn't be represented as a 64-bit floating point, or integer value, respectively.
I'll create a follow-up issue for list, map, set. Let me know if you think float64 and int64 should be included and we can discuss this further.
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.
Ah! That makes sense to me 👍🏻
…mentsData() function
…bleAttribute and <Type>ValuableWithValidateableParameter
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 Ben! 🚀 🎸
} | ||
|
||
switch t := res.(type) { | ||
case xattr.ValidateableAttribute: |
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.
Definitely agree about any refactoring removing validation from reflection should be a separate issue 👍🏻
If a custom type that is only being used as a function parameter implements
ValidateableParameter
but does not implementValidateableAttribute
, the call toValidateParameter()
will occur during theCallFunction
RPC whenfromproto5/6.ArgumentsData()
is called. The call tofromproto5/6.ArgumentsData()
occurs, prior to calling(ArgumentData).Get()
or(ResultData).Set()
, the latter of which are typically called within theRun()
method of the provider-defined function. Does this answer your concern?
That answers the (ArgumentData).Get()
portion, but is there an equivalent for the (ResultData).Set()
direction?
For example sake, let's say that iptypes.IPv4AddressType
only implements ValidateableParameter
func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) {
resp.Definition = function.Definition{
Parameters: []function.Parameter{
function.StringParameter{
Name: "ip_address",
CustomType: iptypes.IPv4AddressType{},
},
},
Return: function.StringReturn{
CustomType: iptypes.IPv4AddressType{},
},
}
}
func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) {
var arg string
resp.Error = req.Arguments.Get(ctx, &arg)
resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, "not an IP address"))
}
I think based on what you described, the req.Arguments.Get
would be protected implicitly because of the fromproto
logic already calling ValidateableParameter
, but the resp.Result.Set
wouldn't call validation because the custom type doesn't implement ValidateableAttribute
.
If that's the case, maybe a Go doc comment could reflect that ValidateableAttribute
is used during reflection? This does feel like enough of an "edge" that it shouldn't be a regular problem (I would expect provider developers using custom types to not use reflection for setting?)
) | ||
|
||
// Float64Typable extends attr.Type for float64 types. | ||
// Implement this interface to create a custom Float64Type type. | ||
type Float64Typable interface { | ||
//nolint:staticcheck // xattr.TypeWithValidate is deprecated, but we still need to support it. |
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.
Ah! That makes sense to me 👍🏻
Co-authored-by: Austin Valle <austinvalle@gmail.com>
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 job, Ben!
…plicit calling of ValidateAttribute
I'm going to lock this pull request because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active contributions. |
Closes: #589
Closes: #893
Background
Provider-defined functions can accept parameters, or arguments as input. There is an opportunity to provide validation of such parameters in an analogous manner to the validation of values supplied in configuration for attributes, by implementing parameter-based and type-based validation for provider-defined function parameters.
This PR is concerned with the addition of type-based validation, which enables provider developers using custom value types to implement parameter validation.
Validation Package
This PR adds type-based provider-defined function parameter validation through the introduction of a
validation
package with a newValidateableParameter
interface.Deprecation of
xattr.TypeWithValidate
This PR also introduces a new
ValidateableAttribute
interface, and deprecates thexattr.TypeWithValidate
interface. Provider developers who are using custom value types that will be used in the context of both attribute validation and parameter validation would need to implement both interfaces on the custom value type, but could share the validation logic to reduce duplication.