Skip to content

Commit 62c84e3

Browse files
dbanckdsa0x
andauthored
Validate identity to match identity schema (#36904)
* Validate identity to match identity schema * validate on new create too and add tests * validate schema nesting type * validate required attrs * remove need for extra fn --------- Co-authored-by: Samsondeen Dare <[email protected]>
1 parent 1bc7d22 commit 62c84e3

File tree

4 files changed

+109
-7
lines changed

4 files changed

+109
-7
lines changed

internal/terraform/context_apply2_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2224,6 +2224,7 @@ import {
22242224
Required: true,
22252225
},
22262226
},
2227+
Nesting: configschema.NestingSingle,
22272228
},
22282229
},
22292230
})

internal/terraform/context_apply_identity_test.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ func TestContext2Apply_identity(t *testing.T) {
2222
prevRunState *states.State
2323
requiresReplace []cty.Path
2424
plannedIdentity cty.Value
25+
appliedIdentity cty.Value
2526

26-
expectedIdentity cty.Value
27+
expectedIdentity cty.Value
28+
expectDiagnostics tfdiags.Diagnostics
2729
}{
2830
"create": {
2931
plannedIdentity: cty.ObjectVal(map[string]cty.Value{
@@ -33,6 +35,17 @@ func TestContext2Apply_identity(t *testing.T) {
3335
"id": cty.StringVal("foo"),
3436
}),
3537
},
38+
"create - invalid applied identity schema": {
39+
plannedIdentity: cty.ObjectVal(map[string]cty.Value{
40+
"id": cty.StringVal("foo"),
41+
}),
42+
appliedIdentity: cty.ObjectVal(map[string]cty.Value{
43+
"id": cty.BoolVal(false),
44+
}),
45+
expectDiagnostics: tfdiags.Diagnostics{
46+
tfdiags.Sourceless(tfdiags.Error, "Provider produced an identity that doesn't match the schema", "Provider \"registry.terraform.io/hashicorp/test\" returned an identity for test_resource.test that doesn't match the identity schema: .id: string required, but received bool. \n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker."),
47+
},
48+
},
3649

3750
"update": {
3851
prevRunState: states.BuildState(func(s *states.SyncState) {
@@ -165,10 +178,23 @@ func TestContext2Apply_identity(t *testing.T) {
165178
}
166179
}
167180

181+
if !tc.appliedIdentity.IsNull() {
182+
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
183+
resp := providers.ApplyResourceChangeResponse{}
184+
resp.NewState = req.PlannedState
185+
resp.NewIdentity = tc.appliedIdentity
186+
return resp
187+
}
188+
}
189+
168190
plan, diags := ctx.Plan(m, tc.prevRunState, &PlanOpts{Mode: tc.mode})
169191
tfdiags.AssertNoDiagnostics(t, diags)
170192

171193
state, diags := ctx.Apply(plan, m, nil)
194+
if tc.expectDiagnostics.HasErrors() {
195+
tfdiags.AssertDiagnosticsMatch(t, diags, tc.expectDiagnostics)
196+
return
197+
}
172198
tfdiags.AssertNoDiagnostics(t, diags)
173199

174200
if !tc.expectedIdentity.IsNull() {

internal/terraform/context_plan_identity_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,22 @@ func TestContext2Plan_resource_identity_plan(t *testing.T) {
437437
"id": cty.StringVal("foo"),
438438
}),
439439
},
440+
"create - invalid planned identity schema": {
441+
plannedIdentity: cty.ObjectVal(map[string]cty.Value{
442+
"id": cty.BoolVal(false),
443+
}),
444+
expectDiagnostics: tfdiags.Diagnostics{
445+
tfdiags.Sourceless(tfdiags.Error, "Provider produced an identity that doesn't match the schema", "Provider \"registry.terraform.io/hashicorp/test\" returned an identity for test_resource.test that doesn't match the identity schema: .id: string required, but received bool. \n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker."),
446+
},
447+
},
448+
"create - null planned identity schema": {
449+
plannedIdentity: cty.ObjectVal(map[string]cty.Value{
450+
"id": cty.NullVal(cty.String),
451+
}),
452+
expectDiagnostics: tfdiags.Diagnostics{
453+
tfdiags.Sourceless(tfdiags.Error, "Provider produced an identity that doesn't match the schema", "Provider \"registry.terraform.io/hashicorp/test\" returned an identity for test_resource.test that doesn't match the identity schema: attributes \"id\" are required and must not be null. \n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker."),
454+
},
455+
},
440456
"update": {
441457
prevRunState: states.BuildState(func(s *states.SyncState) {
442458
s.SetResourceInstanceCurrent(

internal/terraform/node_resource_abstract_instance.go

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,17 @@ func (n *NodeAbstractResourceInstance) planDestroy(ctx EvalContext, currentState
396396
return noop, deferred, nil
397397
}
398398

399+
_, providerSchema, err := getProvider(ctx, n.ResolvedProvider)
400+
if err != nil {
401+
return plan, deferred, diags.Append(err)
402+
}
403+
schema := providerSchema.SchemaForResourceAddr(n.Addr.Resource.Resource)
404+
if schema.Body == nil {
405+
// Should be caught during validation, so we don't bother with a pretty error here
406+
diags = diags.Append(fmt.Errorf("provider does not support resource type %q", n.Addr.Resource.Resource.Type))
407+
return nil, deferred, diags
408+
}
409+
399410
// If we are in a context where we forget instead of destroying, we can
400411
// just return the forget change without consulting the provider.
401412
if ctx.Forget() {
@@ -453,7 +464,7 @@ func (n *NodeAbstractResourceInstance) planDestroy(ctx EvalContext, currentState
453464
if !resp.PlannedIdentity.IsNull() {
454465
// Destroying is an operation where we allow identity changes.
455466
diags = diags.Append(n.validateIdentityKnown(resp.PlannedIdentity))
456-
diags = diags.Append(n.validateIdentity(resp.PlannedIdentity))
467+
diags = diags.Append(n.validateIdentity(resp.PlannedIdentity, providerSchema.Provider.Identity))
457468
}
458469

459470
// We may not have a config for all destroys, but we want to reference
@@ -659,7 +670,7 @@ func (n *NodeAbstractResourceInstance) refresh(ctx EvalContext, deposedKey state
659670

660671
if !resp.Identity.IsNull() {
661672
diags = diags.Append(n.validateIdentityKnown(resp.Identity))
662-
diags = diags.Append(n.validateIdentity(resp.Identity))
673+
diags = diags.Append(n.validateIdentity(resp.Identity, schema.Identity))
663674
diags = diags.Append(n.validateIdentityDidNotChange(state, resp.Identity))
664675
}
665676
if resp.Deferred != nil {
@@ -1117,7 +1128,7 @@ func (n *NodeAbstractResourceInstance) plan(
11171128
}
11181129
}
11191130

1120-
diags = diags.Append(n.validateIdentity(plannedIdentity))
1131+
diags = diags.Append(n.validateIdentity(plannedIdentity, schema.Identity))
11211132
}
11221133
if diags.HasErrors() {
11231134
return nil, nil, deferred, keyData, diags
@@ -1179,7 +1190,7 @@ func (n *NodeAbstractResourceInstance) plan(
11791190

11801191
if !resp.PlannedIdentity.IsNull() {
11811192
// On replace the identity is allowed to change and be unknown.
1182-
diags = diags.Append(n.validateIdentity(resp.PlannedIdentity))
1193+
diags = diags.Append(n.validateIdentity(resp.PlannedIdentity, schema.Identity))
11831194
}
11841195
}
11851196
// We need to tread carefully here, since if there are any warnings
@@ -2636,7 +2647,7 @@ func (n *NodeAbstractResourceInstance) apply(
26362647

26372648
if !resp.NewIdentity.IsNull() {
26382649
diags = diags.Append(n.validateIdentityKnown(resp.NewIdentity))
2639-
diags = diags.Append(n.validateIdentity(resp.NewIdentity))
2650+
diags = diags.Append(n.validateIdentity(resp.NewIdentity, schema.Identity))
26402651
if !change.Action.IsReplace() {
26412652
diags = diags.Append(n.validateIdentityDidNotChange(state, resp.NewIdentity))
26422653
}
@@ -2920,7 +2931,7 @@ func (n *NodeAbstractResourceInstance) validateIdentityDidNotChange(state *state
29202931
return diags
29212932
}
29222933

2923-
func (n *NodeAbstractResourceInstance) validateIdentity(newIdentity cty.Value) (diags tfdiags.Diagnostics) {
2934+
func (n *NodeAbstractResourceInstance) validateIdentity(newIdentity cty.Value, identitySchema *configschema.Object) (diags tfdiags.Diagnostics) {
29242935
if _, marks := newIdentity.UnmarkDeep(); len(marks) > 0 {
29252936
diags = diags.Append(tfdiags.Sourceless(
29262937
tfdiags.Error,
@@ -2932,6 +2943,54 @@ func (n *NodeAbstractResourceInstance) validateIdentity(newIdentity cty.Value) (
29322943
))
29332944
}
29342945

2946+
// The identity schema is always a single object, so we can check the
2947+
// nesting type here.
2948+
if identitySchema.Nesting != configschema.NestingSingle {
2949+
diags = diags.Append(tfdiags.Sourceless(
2950+
tfdiags.Error,
2951+
"Provider produced invalid identity",
2952+
fmt.Sprintf(
2953+
"Provider %q returned an identity with a nesting type of %s for %s. \n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.",
2954+
n.ResolvedProvider.Provider, identitySchema.Nesting, n.Addr,
2955+
),
2956+
))
2957+
}
2958+
2959+
newType := newIdentity.Type()
2960+
currentType := identitySchema.ImpliedType()
2961+
if errs := newType.TestConformance(currentType); len(errs) > 0 {
2962+
for _, err := range errs {
2963+
diags = diags.Append(tfdiags.Sourceless(
2964+
tfdiags.Error,
2965+
"Provider produced an identity that doesn't match the schema",
2966+
fmt.Sprintf(
2967+
"Provider %q returned an identity for %s that doesn't match the identity schema: %s. \n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.",
2968+
n.ResolvedProvider.Provider, n.Addr, tfdiags.FormatError(err),
2969+
),
2970+
))
2971+
}
2972+
return diags
2973+
}
2974+
2975+
// Check for required attributes
2976+
names := make([]string, 0, len(identitySchema.Attributes))
2977+
for name, attrS := range identitySchema.Attributes {
2978+
if attrS.Required && newIdentity.GetAttr(name).IsNull() {
2979+
names = append(names, name)
2980+
}
2981+
}
2982+
if len(names) > 0 {
2983+
diags = diags.Append(tfdiags.Sourceless(
2984+
tfdiags.Error,
2985+
"Provider produced an identity that doesn't match the schema",
2986+
fmt.Sprintf(
2987+
"Provider %q returned an identity for %s that doesn't match the identity schema: attributes %q are required and must not be null. \n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.",
2988+
n.ResolvedProvider.Provider, n.Addr,
2989+
strings.Join(names, ", "),
2990+
),
2991+
))
2992+
}
2993+
29352994
return diags
29362995
}
29372996

0 commit comments

Comments
 (0)