Skip to content

Commit 94142f5

Browse files
committed
add schema and config validators
1 parent 0ddd14e commit 94142f5

File tree

8 files changed

+390
-14
lines changed

8 files changed

+390
-14
lines changed

internal/configvalidator/conflicting.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,21 @@ func (v ConflictingValidator) Validate(ctx context.Context, config tfsdk.Config)
7676
continue
7777
}
7878

79-
// Value must not be null or unknown to trigger validation error
80-
if value.IsNull() || value.IsUnknown() {
79+
// Value must not be null or fully unknown to trigger validation error
80+
if value.IsNull() {
81+
continue
82+
}
83+
84+
if value.IsUnknown() {
85+
// If the unknown value will eventually be not null, we add it to the
86+
// configured paths to potentially trigger a validation error
87+
val, ok := value.(attr.ValueWithNotNullRefinement)
88+
if ok {
89+
if _, notNull := val.NotNullRefinement(); notNull {
90+
configuredPaths.Append(matchedPath)
91+
}
92+
}
93+
8194
continue
8295
}
8396

internal/configvalidator/conflicting_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
1818
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
1919
"github.com/hashicorp/terraform-plugin-go/tftypes"
20+
tfrefinement "github.com/hashicorp/terraform-plugin-go/tftypes/refinement"
2021

2122
"github.com/hashicorp/terraform-plugin-framework-validators/internal/configvalidator"
2223
)
@@ -329,6 +330,52 @@ func TestConflictingValidatorValidate(t *testing.T) {
329330
},
330331
expected: nil,
331332
},
333+
"two-matching-path-expression-one-notnull-unknown-one-value": {
334+
validator: configvalidator.ConflictingValidator{
335+
PathExpressions: path.Expressions{
336+
path.MatchRoot("test1"),
337+
path.MatchRoot("test2"),
338+
},
339+
},
340+
config: tfsdk.Config{
341+
Schema: schema.Schema{
342+
Attributes: map[string]schema.Attribute{
343+
"test1": schema.StringAttribute{
344+
Optional: true,
345+
},
346+
"test2": schema.StringAttribute{
347+
Optional: true,
348+
},
349+
"other": schema.StringAttribute{
350+
Optional: true,
351+
},
352+
},
353+
},
354+
Raw: tftypes.NewValue(
355+
tftypes.Object{
356+
AttributeTypes: map[string]tftypes.Type{
357+
"test1": tftypes.String,
358+
"test2": tftypes.String,
359+
"other": tftypes.String,
360+
},
361+
},
362+
map[string]tftypes.Value{
363+
"test1": tftypes.NewValue(tftypes.String, tftypes.UnknownValue).Refine(tfrefinement.Refinements{
364+
tfrefinement.KeyNullness: tfrefinement.NewNullness(false),
365+
}),
366+
"test2": tftypes.NewValue(tftypes.String, "test-value"),
367+
"other": tftypes.NewValue(tftypes.String, "test-value"),
368+
},
369+
),
370+
},
371+
expected: diag.Diagnostics{
372+
diag.NewAttributeErrorDiagnostic(
373+
path.Root("test1"),
374+
"Invalid Attribute Combination",
375+
"These attributes cannot be configured together: [test1,test2]",
376+
),
377+
},
378+
},
332379
"two-matching-path-expression-two-null": {
333380
validator: configvalidator.ConflictingValidator{
334381
PathExpressions: path.Expressions{
@@ -405,6 +452,54 @@ func TestConflictingValidatorValidate(t *testing.T) {
405452
},
406453
expected: nil,
407454
},
455+
"two-matching-path-expression-two-notnull-unknown": {
456+
validator: configvalidator.ConflictingValidator{
457+
PathExpressions: path.Expressions{
458+
path.MatchRoot("test1"),
459+
path.MatchRoot("test2"),
460+
},
461+
},
462+
config: tfsdk.Config{
463+
Schema: schema.Schema{
464+
Attributes: map[string]schema.Attribute{
465+
"test1": schema.StringAttribute{
466+
Optional: true,
467+
},
468+
"test2": schema.StringAttribute{
469+
Optional: true,
470+
},
471+
"other": schema.StringAttribute{
472+
Optional: true,
473+
},
474+
},
475+
},
476+
Raw: tftypes.NewValue(
477+
tftypes.Object{
478+
AttributeTypes: map[string]tftypes.Type{
479+
"test1": tftypes.String,
480+
"test2": tftypes.String,
481+
"other": tftypes.String,
482+
},
483+
},
484+
map[string]tftypes.Value{
485+
"test1": tftypes.NewValue(tftypes.String, tftypes.UnknownValue).Refine(tfrefinement.Refinements{
486+
tfrefinement.KeyNullness: tfrefinement.NewNullness(false),
487+
}),
488+
"test2": tftypes.NewValue(tftypes.String, tftypes.UnknownValue).Refine(tfrefinement.Refinements{
489+
tfrefinement.KeyNullness: tfrefinement.NewNullness(false),
490+
}),
491+
"other": tftypes.NewValue(tftypes.String, "test-value"),
492+
},
493+
),
494+
},
495+
expected: diag.Diagnostics{
496+
diag.NewAttributeErrorDiagnostic(
497+
path.Root("test1"),
498+
"Invalid Attribute Combination",
499+
"These attributes cannot be configured together: [test1,test2]",
500+
),
501+
},
502+
},
408503
"two-matching-path-expression-two-value": {
409504
validator: configvalidator.ConflictingValidator{
410505
PathExpressions: path.Expressions{

internal/configvalidator/exactly_one_of.go

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,17 +76,27 @@ func (v ExactlyOneOfValidator) Validate(ctx context.Context, config tfsdk.Config
7676
continue
7777
}
7878

79-
// If value is unknown, it may be null or a value, so we cannot
80-
// know if the validator should succeed or not. Collect the path
81-
// path so we use it to skip the validation later and continue to
82-
// collect all path matching diagnostics.
83-
if value.IsUnknown() {
84-
unknownPaths.Append(matchedPath)
79+
// If value is null, move onto the next one.
80+
if value.IsNull() {
8581
continue
8682
}
8783

88-
// If value is null, move onto the next one.
89-
if value.IsNull() {
84+
if value.IsUnknown() {
85+
// If the unknown value will eventually be not null, we add it to the
86+
// configured paths to potentially trigger a validation error
87+
val, ok := value.(attr.ValueWithNotNullRefinement)
88+
if ok {
89+
if _, notNull := val.NotNullRefinement(); notNull {
90+
configuredPaths.Append(matchedPath)
91+
continue
92+
}
93+
}
94+
95+
// If value is fully unknown, it may be null or a value, so we cannot
96+
// know if the validator should succeed or not. Collect the path
97+
// path so we use it to skip the validation later and continue to
98+
// collect all path matching diagnostics.
99+
unknownPaths.Append(matchedPath)
90100
continue
91101
}
92102

internal/configvalidator/exactly_one_of_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
1818
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
1919
"github.com/hashicorp/terraform-plugin-go/tftypes"
20+
tfrefinement "github.com/hashicorp/terraform-plugin-go/tftypes/refinement"
2021

2122
"github.com/hashicorp/terraform-plugin-framework-validators/internal/configvalidator"
2223
)
@@ -344,6 +345,52 @@ func TestExactlyOneOfValidatorValidate(t *testing.T) {
344345
},
345346
expected: nil,
346347
},
348+
"two-matching-path-expression-one-notnull-unknown-one-value": {
349+
validator: configvalidator.ExactlyOneOfValidator{
350+
PathExpressions: path.Expressions{
351+
path.MatchRoot("test1"),
352+
path.MatchRoot("test2"),
353+
},
354+
},
355+
config: tfsdk.Config{
356+
Schema: schema.Schema{
357+
Attributes: map[string]schema.Attribute{
358+
"test1": schema.StringAttribute{
359+
Optional: true,
360+
},
361+
"test2": schema.StringAttribute{
362+
Optional: true,
363+
},
364+
"other": schema.StringAttribute{
365+
Optional: true,
366+
},
367+
},
368+
},
369+
Raw: tftypes.NewValue(
370+
tftypes.Object{
371+
AttributeTypes: map[string]tftypes.Type{
372+
"test1": tftypes.String,
373+
"test2": tftypes.String,
374+
"other": tftypes.String,
375+
},
376+
},
377+
map[string]tftypes.Value{
378+
"test1": tftypes.NewValue(tftypes.String, tftypes.UnknownValue).Refine(tfrefinement.Refinements{
379+
tfrefinement.KeyNullness: tfrefinement.NewNullness(false),
380+
}),
381+
"test2": tftypes.NewValue(tftypes.String, "test-value"),
382+
"other": tftypes.NewValue(tftypes.String, "test-value"),
383+
},
384+
),
385+
},
386+
expected: diag.Diagnostics{
387+
diag.NewAttributeErrorDiagnostic(
388+
path.Root("test1"),
389+
"Invalid Attribute Combination",
390+
"Exactly one of these attributes must be configured: [test1,test2]",
391+
),
392+
},
393+
},
347394
"two-matching-path-expression-two-null": {
348395
validator: configvalidator.ExactlyOneOfValidator{
349396
PathExpressions: path.Expressions{
@@ -425,6 +472,54 @@ func TestExactlyOneOfValidatorValidate(t *testing.T) {
425472
},
426473
expected: nil,
427474
},
475+
"two-matching-path-expression-two-notnull-unknown": {
476+
validator: configvalidator.ExactlyOneOfValidator{
477+
PathExpressions: path.Expressions{
478+
path.MatchRoot("test1"),
479+
path.MatchRoot("test2"),
480+
},
481+
},
482+
config: tfsdk.Config{
483+
Schema: schema.Schema{
484+
Attributes: map[string]schema.Attribute{
485+
"test1": schema.StringAttribute{
486+
Optional: true,
487+
},
488+
"test2": schema.StringAttribute{
489+
Optional: true,
490+
},
491+
"other": schema.StringAttribute{
492+
Optional: true,
493+
},
494+
},
495+
},
496+
Raw: tftypes.NewValue(
497+
tftypes.Object{
498+
AttributeTypes: map[string]tftypes.Type{
499+
"test1": tftypes.String,
500+
"test2": tftypes.String,
501+
"other": tftypes.String,
502+
},
503+
},
504+
map[string]tftypes.Value{
505+
"test1": tftypes.NewValue(tftypes.String, tftypes.UnknownValue).Refine(tfrefinement.Refinements{
506+
tfrefinement.KeyNullness: tfrefinement.NewNullness(false),
507+
}),
508+
"test2": tftypes.NewValue(tftypes.String, tftypes.UnknownValue).Refine(tfrefinement.Refinements{
509+
tfrefinement.KeyNullness: tfrefinement.NewNullness(false),
510+
}),
511+
"other": tftypes.NewValue(tftypes.String, "test-value"),
512+
},
513+
),
514+
},
515+
expected: diag.Diagnostics{
516+
diag.NewAttributeErrorDiagnostic(
517+
path.Root("test1"),
518+
"Invalid Attribute Combination",
519+
"Exactly one of these attributes must be configured: [test1,test2]",
520+
),
521+
},
522+
},
428523
"two-matching-path-expression-two-value": {
429524
validator: configvalidator.ExactlyOneOfValidator{
430525
PathExpressions: path.Expressions{

internal/schemavalidator/conflicts_with.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,21 @@ func (av ConflictsWithValidator) Validate(ctx context.Context, req ConflictsWith
8989
continue
9090
}
9191

92-
// Delay validation until all involved attribute have a known value
92+
// If value is fully unknown, delay validation until all involved attributes have a known value
9393
if mpVal.IsUnknown() {
94+
// If the unknown value will eventually be not null, we can add the diagnostic and continue looping
95+
val, ok := mpVal.(attr.ValueWithNotNullRefinement)
96+
if ok {
97+
if _, notNull := val.NotNullRefinement(); notNull {
98+
res.Diagnostics.Append(validatordiag.InvalidAttributeCombinationDiagnostic(
99+
req.Path,
100+
fmt.Sprintf("Attribute %q cannot be specified when %q is specified", mp, req.Path),
101+
))
102+
103+
continue
104+
}
105+
}
106+
94107
return
95108
}
96109

internal/schemavalidator/conflicts_with_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
1313
"github.com/hashicorp/terraform-plugin-framework/types"
1414
"github.com/hashicorp/terraform-plugin-go/tftypes"
15+
tfrefinement "github.com/hashicorp/terraform-plugin-go/tftypes/refinement"
1516

1617
"github.com/hashicorp/terraform-plugin-framework-validators/internal/schemavalidator"
1718
)
@@ -204,6 +205,42 @@ func TestConflictsWithValidatorValidate(t *testing.T) {
204205
},
205206
//expErrors: 2,
206207
},
208+
"error_notnull-unknowns": {
209+
req: schemavalidator.ConflictsWithValidatorRequest{
210+
ConfigValue: types.StringValue("bar value"),
211+
Path: path.Root("bar"),
212+
PathExpression: path.MatchRoot("bar"),
213+
Config: tfsdk.Config{
214+
Schema: schema.Schema{
215+
Attributes: map[string]schema.Attribute{
216+
"foo": schema.Int64Attribute{},
217+
"bar": schema.StringAttribute{},
218+
"baz": schema.Int64Attribute{},
219+
},
220+
},
221+
Raw: tftypes.NewValue(tftypes.Object{
222+
AttributeTypes: map[string]tftypes.Type{
223+
"foo": tftypes.Number,
224+
"bar": tftypes.String,
225+
"baz": tftypes.Number,
226+
},
227+
}, map[string]tftypes.Value{
228+
"foo": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue).Refine(tfrefinement.Refinements{
229+
tfrefinement.KeyNullness: tfrefinement.NewNullness(false),
230+
}),
231+
"bar": tftypes.NewValue(tftypes.String, "bar value"),
232+
"baz": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue).Refine(tfrefinement.Refinements{
233+
tfrefinement.KeyNullness: tfrefinement.NewNullness(false),
234+
}),
235+
}),
236+
},
237+
},
238+
in: path.Expressions{
239+
path.MatchRoot("foo"),
240+
path.MatchRoot("baz"),
241+
},
242+
expErrors: 2,
243+
},
207244
"matches-no-attribute-in-schema": {
208245
req: schemavalidator.ConflictsWithValidatorRequest{
209246
ConfigValue: types.StringValue("bar value"),

0 commit comments

Comments
 (0)