-
Notifications
You must be signed in to change notification settings - Fork 43
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
feat: type check values based on pulumi schema #1800
Conversation
This adds an initial type checking implementation to `pkg/tfbridge`. The actually type checker is pretty generic and only has a dependency on `pulumi/pulumi` so it would be possible to pull this out into a separate library as a general type checker. **Feature Support** - [X] required properties - [X] property names - [X] `string` types - [X] `number` types - [X] `object` types - [X] `array` types - [X] `null` types - [X] `bool` types - [X] `output` types - [X] `secret` types - [ ] `asset` types - [ ] `archive` types - [ ] `resourceReference` types - [ ] String Enums - [ ] Number Enums - *It knows about enums, but it does not validate enum values since enums are not exhaustive* Along with the type checker it will also build an error message that contains the path to the property. re #1328
3c5e839
to
0201d50
Compare
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1800 +/- ##
==========================================
+ Coverage 59.99% 60.14% +0.15%
==========================================
Files 327 328 +1
Lines 43976 44206 +230
==========================================
+ Hits 26382 26587 +205
- Misses 16102 16122 +20
- Partials 1492 1497 +5 ☔ View full report in Codecov by Sentry. |
pkg/tfbridge/provider.go
Outdated
if p.pulumiSchema != nil { | ||
var schema pschema.PackageSpec | ||
if err := json.Unmarshal(p.pulumiSchema, &schema); err != nil { | ||
return nil, err | ||
} |
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 don't think we can afford to unmarshal the schema on each Check
. Let's do this only on the first check, maybe with sync.OnceValue
.
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.
Done!
Co-authored-by: Ian Wahbe <ian@wahbe.com>
Round of suggestions here: #1878 - happy to take some time to talk through that tomorrow. |
// elementTypeSpec := typeSpec.AdditionalProperties | ||
if propertyValue.IsObject() { | ||
if typeSpec.AdditionalProperties == nil { | ||
// Unknown item type so nothing more to check |
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.
The binder actually assumes this would be a stringly typed map[string]string. Could go either way here.
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.
Are you saying that if Type: "object", AdditionalProperties: nil
then the assumption is that the type is Type: "object", AdditionalProperties: { Type: "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.
Yes! Last I checked that seemed to be literally what the binder code was saying. It is not easy to tell though if that's intentional or "let's assume some type here". I guess I prefer your version here for now - no type specified so we won't check.
failures := []TypeFailure{} | ||
for _, propertyKey := range objectValue.StableKeys() { | ||
pb := append(propertyPath, string(propertyKey)) | ||
failure := v.validatePropertyValue(objectValue[propertyKey], *typeSpec.AdditionalProperties, pb) |
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 looks right.
pkg/tfbridge/validate_input_types.go
Outdated
return nil | ||
} | ||
// if there is a ref to another type, get that and then validate | ||
if typeSpec.AdditionalProperties.Ref != "" { |
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.
You don't need this if clause at all, I think. What's going to happen is that v.validatePropertyValue on *typeSpec.AdditionalProperties is going to take full care of this.
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.
There is one scenario where this is needed (example below). If we don't include this block then we end up comparing the objectStringProp
type to the pkg:index/type:ObjectStringType
.
input like this:
"prop": map[string]interface{}{
"objectStringProp": "foo",
}
And a spec like this
map[string]pschema.ComplexTypeSpec{
"pkg:index/type:ObjectStringType": {
ObjectTypeSpec: pschema.ObjectTypeSpec{
Type: "object",
Properties: map[string]pschema.PropertySpec{
"objectStringProp": {
TypeSpec: pschema.TypeSpec{
Type: "string",
},
},
},
},
},
"pkg:index/type:ObjectDoubleNestedObjectType": {
ObjectTypeSpec: pschema.ObjectTypeSpec{
Type: "object",
Properties: map[string]pschema.PropertySpec{
"prop": {
TypeSpec: pschema.TypeSpec{
Type: "object",
// not using ref to test arbitrary object keys
AdditionalProperties: &pschema.TypeSpec{
Type: "object",
Ref: "#/types/pkg:index/type:ObjectStringType",
},
},
},
},
},
},
}
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 see it's under test, checking up on it, hm..
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.
Removing this block makes TestValidateInputType_arrays/object_double_nested_object_type_success fail, I've instrumented that test case and I think it's actually not asserting the right behavior, with this block removed I think the validator correctly fails on that test case. Step by step:
=== RUN TestValidateInputType_arrays/object_double_nested_object_type_success
# validating against array of objects
validatePropertyValue {[{map[prop:{map[objectStringProp:{foo}]}]}]} object_double_nested_object_type_success4 {
"type": "array",
"items": {
"$ref": "#/types/pkg:index/type:ObjectDoubleNestedObjectType"
}
}
# validating first element against an object
validatePropertyValue {map[prop:{map[objectStringProp:{foo}]}]} object_double_nested_object_type_success4[0] {
"$ref": "#/types/pkg:index/type:ObjectDoubleNestedObjectType"
}
# down to prop attribute of the object, validating against a map of objects
validatePropertyValue {map[objectStringProp:{foo}]} object_double_nested_object_type_success4[0].prop {
"type": "object",
"additionalProperties": {
"type": "object",
"$ref": "#/types/pkg:index/type:ObjectStringType"
}
}
# down objectStringProp map key, validating "foo" string against an object type
validatePropertyValue {foo} object_double_nested_object_type_success4[0].prop.objectStringProp {
"type": "object",
"$ref": "#/types/pkg:index/type:ObjectStringType"
}
# this should fail as it does
validate_input_types_test.go:1552: 0 failures, got 1: [{expected object type, got string type object_double_nested_object_type_success4[0].prop.objectStringProp}]
--- FAIL: TestValidateInputType_arrays (0.00s)
--- FAIL: TestValidateInputType_arrays/object_double_nested_object_type_success4 (0.00s)
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.
E.g. for the double nested object type, TypeScript would have to supply something like this:
{"prop": {"key1": {"objectStringProp": "A"}, "key2": {"objectStringProp": "B"}}}
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.
We can always check by generating a TypeScript SDK for a schema that looks like that.
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.
Ok I remove that block
pkg/tfbridge/validate_input_types.go
Outdated
// we have found the correct type | ||
return nil | ||
} | ||
failures = append(failures, failure...) |
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.
Merging all these failures will be unreadable. Perhaps just ignore them all for now and emit one generic failure that "matched none of the union type members"
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 took a stab at trying to return a better single error message, let me know what you think.
pkg/tfbridge/validate_input_types.go
Outdated
|
||
// default type | ||
if typeSpec.Type != "" { | ||
typeSpec.OneOf = append(typeSpec.OneOf, pschema.TypeSpec{Type: typeSpec.Type}) |
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 always worry this mutation would leak outside the scope of this func, but looks like it doesn't perhaps because we have our own struct copy.
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 don't quite understand what this is doing. Why is typeSpec.Type
appended to OneOf
?
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 took that logic from https://github.com/pulumi/pulumi/blob/477a54b3de081a4927df3038c7b2266d599d5f3c/pkg/codegen/schema/bind.go#L849-L854
Not sure exactly why, but it looks like a "default" type can be stored in the Type
field.
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 strange encoding I don't quite fully understand how it's interpreted either, would need to dig in.
Looking for examples in the wild.
Looks like for all examples I could find, these are enums that can also be strings. So the encoding is specifying that .type is string ("default"), but then also providing string as one of the alternatives, and the enum type as the other alternative.
) | ||
} | ||
|
||
if typeSpec.OneOf != 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.
This looks right
remove required property validation better oneof error messages
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 looks pretty good to me. Before we roll out a change like this, can you create a branch that has PULUMI_ERROR_TYPE_CHECKER
default to on, then run all of our downstream tests. If our downstream tests are valid and the type checker is valid, then they should all pass.
pkg/tfbridge/validate_input_types.go
Outdated
stableKeys := propertyMap.StableKeys() | ||
failures := []TypeFailure{} | ||
|
||
// TODO: handle required properties. Deferring for now |
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.
Let's make sure that all TODO's have an issue baked in:
// TODO: handle required properties. Deferring for now | |
// TODO[pulumi/pulumi-terraform-bridge#123]: handle required properties. Deferring for now |
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.
Done!
pkg/tfbridge/provider.go
Outdated
logger := GetLogger(ctx) | ||
// for now we are just going to log warnings if there are failures. | ||
// over time we may want to turn these into actual errors | ||
_, validateShouldError := os.LookupEnv("PULUMI_ERROR_TYPE_CHECKER") |
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.
PULUMI_ERROR_TYPE_CHECKER=false
should not be equivalent to PULUMI_ERROR_TYPE_CHECKER=true
.
_, validateShouldError := os.LookupEnv("PULUMI_ERROR_TYPE_CHECKER") | |
validateShouldError := "github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil".IsTruthy(os.Getenv("PULUMI_ERROR_TYPE_CHECKER")) |
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.
Updated!
pkg/tfbridge/validate_input_types.go
Outdated
} | ||
failures = append(failures, failure...) | ||
} | ||
// try to find the best failure message |
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.
Yeah IDK how this will play in practice I think it's just hard to get right. Looks like in practice mostly we use this unit feature for enums that can be string or one of the enum values. I think on the wire they'll all be strings then. I think it's really best leave it for a separate PR, or if we'r doing something here do something basic just fail in place, or not handle unions at all.
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.
ok i'll remove the OneOf
checking and leave a TODO.
remove oneof implementation remove additionalproperties.ref
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 LGTM to me if we can pass a version of this that panics instead of warning and have all downstream tests pass with that (sorry for being so paranoid). Code looks good but we've historically had some trouble with this codebase behaving safely.
Thanks @corymhall for all the work here. |
Ran this branch through the downstream tests and nothing failed |
These tests were added in #1800 but seem to have been misplaced in the pf folder - the tests are for the SDKV2. The ~same tests are already present in the pkg folder under https://github.com/pulumi/pulumi-terraform-bridge/blob/18db7c57211af0eb7dfa26a70b145edc4d33a88f/pkg/tfbridge/tests/provider_test.go#L4
This adds an initial type checking implementation to
pkg/tfbridge
. The actually type checker is pretty generic and only has a dependency onpulumi/pulumi
so it would be possible to pull this out into a separate library as a general type checker.Feature Support
string
typesnumber
typesobject
typesarray
typesnull
typesbool
typesoutput
typessecret
typesasset
typesarchive
typesresourceReference
typesAlong with the type checker it will also build an error message that contains the path to the property.
Todo
pf/tfbridge
(can be done in a later pr)- By default this will only warn users, but can override with an env var
re #1328