-
Notifications
You must be signed in to change notification settings - Fork 42
/
validate_input_types.go
276 lines (255 loc) · 8.64 KB
/
validate_input_types.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
package tfbridge
import (
"fmt"
"strings"
pschema "github.com/pulumi/pulumi/pkg/v3/codegen/schema"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
)
type PulumiInputValidator struct {
// The resource URN that we are validating
urn resource.URN
// The pulumi schema of the package
schema pschema.PackageSpec
}
type TypeFailure struct {
// The Reason for the type failure
Reason string
// The path to the property that failed
ResourcePath string
}
// NewInputValidator creates a new input validator for a given resource and
// package schema
func NewInputValidator(urn resource.URN, schema pschema.PackageSpec) *PulumiInputValidator {
return &PulumiInputValidator{
urn: urn,
schema: schema,
}
}
// validatePropertyValue is the main function for validating a PropertyMap against the Pulumi Schema. It is a
// recursive function that will validate nested types and arrays. Returns a list of type failures if any are found.
func (v *PulumiInputValidator) validatePropertyMap(
propertyMap resource.PropertyMap,
propertyTypes map[string]pschema.PropertySpec,
propertyPath resource.PropertyPath,
) []TypeFailure {
stableKeys := propertyMap.StableKeys()
failures := []TypeFailure{}
// TODO[pulumi/pulumi-terrafor-bridge#1892]: handle required properties. Deferring for now
// because properties can be filled in later and we don't want to
// fail too aggressively
for _, objectKey := range stableKeys {
objectValue := propertyMap[objectKey]
objType, knownType := propertyTypes[string(objectKey)]
if !knownType {
// permit extraneous properties to flow through
continue
}
subPropertyPath := append(propertyPath, string(objectKey))
failure := v.validatePropertyValue(objectValue, objType.TypeSpec, subPropertyPath)
if failure != nil {
failures = append(failures, failure...)
}
}
return failures
}
// validatePropertyValue is the main function for validating a PropertyValue against the Pulumi Schema. It is a
// recursive function that will validate nested types and arrays. Returns a list of type failures if any are found.
func (v *PulumiInputValidator) validatePropertyValue(
propertyValue resource.PropertyValue,
typeSpec pschema.TypeSpec,
propertyPath resource.PropertyPath,
) []TypeFailure {
// don't type check
// - resource references (not yet)
// - assets (not yet)
// - archives (not yet)
// - computed values (they are allowed for any type)
// - unknown output values (similar to computed)
// - nulls (they are allowed for any type, and for missing required properties see validatePropertyMap)
if propertyValue.IsResourceReference() ||
propertyValue.IsAsset() ||
propertyValue.IsArchive() ||
propertyValue.IsComputed() ||
propertyValue.IsNull() ||
(propertyValue.IsOutput() && !propertyValue.OutputValue().Known) {
return nil
}
// for known first-class outputs simply validate their known value
if propertyValue.IsOutput() && propertyValue.OutputValue().Known {
elementValue := propertyValue.OutputValue().Element
return v.validatePropertyValue(elementValue, typeSpec, propertyPath)
}
// for secrets validate their inner value
if propertyValue.IsSecret() {
elementValue := propertyValue.SecretValue().Element
return v.validatePropertyValue(elementValue, typeSpec, propertyPath)
}
// now we are going to switch on the desired type
//
// a good reference here for the semantics of the TypeSpec is
// https://github.com/pulumi/pulumi/blob/master/pkg/codegen/schema/bind.go#L881
//
// this code is not using the binder functionality as that would require a plugin loader to resolve references,
// which is not desirable for the provider runtime
//
// possibly this could be done in the future with a way to still use the bind code while resolving references to
// unchecked types as using the bound representation would make the code cleaner
//
// for now the code simply follows bindSpec
if typeSpec.Ref != "" {
objType := v.getType(typeSpec.Ref)
// Refusing to validate unknown or unresolved type.
if objType == nil {
return nil
}
if !propertyValue.IsObject() {
return []TypeFailure{{
ResourcePath: propertyPath.String(),
Reason: fmt.Sprintf(
"expected object type, got %s type",
propertyValue.TypeString(),
),
}}
}
return v.validatePropertyMap(
propertyValue.ObjectValue(),
objType.Properties,
propertyPath,
)
}
if typeSpec.OneOf != nil {
// TODO[pulumi/pulumi-terrafor-bridge#1891]: handle OneOf types
// bindTypeSpecOneOf provides a good hint of how to interpret these:
//
// https://github.com/pulumi/pulumi/blob/master/pkg/codegen/schema/bind.go#L842
//
// Specifically it defines the defaultType, discriminator, mapping and elements.
return nil
}
switch typeSpec.Type {
case "bool":
// The bridge permits coalescing strings to booleans, hence skip strings.
if !propertyValue.IsBool() && !propertyValue.IsString() {
return []TypeFailure{{
ResourcePath: propertyPath.String(),
Reason: fmt.Sprintf(
"expected boolean type, got %s type",
propertyValue.TypeString(),
),
}}
}
return nil
case "integer", "number":
// The bridge permits coalescing strings to numbers, hence skip strings.
if !propertyValue.IsNumber() && !propertyValue.IsString() {
return []TypeFailure{{
ResourcePath: propertyPath.String(),
Reason: fmt.Sprintf(
"expected number type, got %s type",
propertyValue.TypeString(),
),
}}
}
return nil
case "string":
// The bridge permits coalescing numbers and booleans to strings, hence skip these.
if !propertyValue.IsString() && !propertyValue.IsNumber() && !propertyValue.IsBool() {
return []TypeFailure{{
ResourcePath: propertyPath.String(),
Reason: fmt.Sprintf(
"expected string type, got %s type",
propertyValue.TypeString(),
),
}}
}
return nil
case "array":
if propertyValue.IsArray() {
if typeSpec.Items == nil {
// Unknown item type so nothing more to check.
return nil
}
// Check every item against the array element type.
failures := []TypeFailure{}
for idx, arrayValue := range propertyValue.ArrayValue() {
pb := append(propertyPath, idx)
failure := v.validatePropertyValue(arrayValue, *typeSpec.Items, pb)
if failure != nil {
failures = append(failures, failure...)
}
}
return failures
}
return []TypeFailure{{
ResourcePath: propertyPath.String(),
Reason: fmt.Sprintf(
"expected array type, got %s type",
propertyValue.TypeString(),
),
}}
case "object":
// This is not really an object but a map type with some element type, which is assumed to be string if
// unspecified. Check accordingly. This should be very similar to the "array" case.
//
// elementTypeSpec := typeSpec.AdditionalProperties
if propertyValue.IsObject() {
if typeSpec.AdditionalProperties == nil {
// Unknown item type so nothing more to check
return nil
}
objectValue := propertyValue.ObjectValue()
failures := []TypeFailure{}
for _, propertyKey := range objectValue.StableKeys() {
pb := append(propertyPath, string(propertyKey))
failure := v.validatePropertyValue(objectValue[propertyKey], *typeSpec.AdditionalProperties, pb)
if failure != nil {
failures = append(failures, failure...)
}
}
return failures
}
return []TypeFailure{{
ResourcePath: propertyPath.String(),
Reason: fmt.Sprintf(
"expected object type, got %s type",
propertyValue.TypeString(),
),
}}
default:
// Unrecognized type, assume no errors.
return nil
}
}
// getType gets a type definition from a schema reference. Currently it only supports types from the same schema that
// are object types. It does not support enum types, foreign type references, special references such as
// "pulumi.json#/Archive", references to resources or providers or anything else.
func (v *PulumiInputValidator) getType(typeRef string) *pschema.ObjectTypeSpec {
if strings.HasPrefix(typeRef, "#/types/") {
ref := strings.TrimPrefix(typeRef, "#/types/")
if typeSpec, ok := v.schema.Types[ref]; ok {
// Exclude enum types here.
if len(typeSpec.Enum) == 0 {
return &typeSpec.ObjectTypeSpec
}
}
}
return nil
}
// ValidateInputs will validate a set of inputs against the pulumi schema. It will
// return a list of type failures if any are found
func (v *PulumiInputValidator) ValidateInputs(resourceToken tokens.Type, inputs resource.PropertyMap) *[]TypeFailure {
resourceSpec, knownResourceSpec := v.schema.Resources[string(resourceToken)]
if !knownResourceSpec {
return nil
}
failures := v.validatePropertyMap(
inputs,
resourceSpec.InputProperties,
resource.PropertyPath{},
)
if len(failures) > 0 {
return &failures
}
return nil
}