@@ -5,7 +5,9 @@ package terraform
55
66import (
77 "context"
8+ "sync/atomic"
89 "testing"
10+ "time"
911
1012 "github.com/google/go-cmp/cmp"
1113 "github.com/hashicorp/hcl/v2"
@@ -17,6 +19,7 @@ import (
1719 "github.com/hashicorp/terraform/internal/states"
1820 "github.com/hashicorp/terraform/internal/tfdiags"
1921 "github.com/zclconf/go-cty/cty"
22+ "google.golang.org/protobuf/testing/protocmp"
2023)
2124
2225func TestContext2Apply_PolicyEvaluation_Full (t * testing.T ) {
@@ -96,7 +99,7 @@ func TestContext2Apply_PolicyEvaluation_Full(t *testing.T) {
9699 planPolicyClient := policy .NewTestMockClient (t )
97100
98101 // The expected values to be sent for policy evaluation.
99- expected := map [string ]cty.Value {
102+ expectedPlan := map [string ]cty.Value {
100103 "test_resource" : cty .ObjectVal (map [string ]cty.Value {
101104 "value" : cty .NullVal (cty .String ),
102105 "sensitive_value" : cty .StringVal ("foo" ),
@@ -106,25 +109,27 @@ func TestContext2Apply_PolicyEvaluation_Full(t *testing.T) {
106109 "sensitive_value" : cty .NilVal ,
107110 }),
108111 }
112+ actualPlan := make (map [string ]cty.Value )
109113
110114 planPolicyClient .EvaluateFn = func (ctx context.Context , req policy.EvaluationRequest [* proto.ResourceMetadata ]) policy.EvaluationResponse {
111- var actual cty.Value
115+ var actualVal cty.Value
112116 attrs := req .Attrs
113117 target := req .Target
114118 if ! attrs .IsNull () {
115119 mp := attrs .AsValueMap ()
116- actual = cty .ObjectVal (map [string ]cty.Value {
120+ actualVal = cty .ObjectVal (map [string ]cty.Value {
117121 "value" : mp ["value" ],
118122 "sensitive_value" : mp ["sensitive_value" ],
119123 })
120124 }
121-
122- if diff := cmp .Diff (actual , expected [target ], cmp .Comparer (cty .Value .RawEquals )); diff != "" {
123- t .Errorf ("Unexpected diff (-got +want):\n %s" , diff )
124- }
125-
125+ actualPlan [target ] = actualVal
126126 return policy.EvaluationResponse {Overall : policy .AllowResult }
127127 }
128+ t .Cleanup (func () {
129+ if diff := cmp .Diff (actualPlan , expectedPlan , cmp .Comparer (cty .Value .RawEquals )); diff != "" {
130+ t .Errorf ("Unexpected diff (-got +want):\n %s" , diff )
131+ }
132+ })
128133
129134 ctx , diags := NewContext (& ContextOpts {
130135 Providers : map [addrs.Provider ]providers.Factory {
@@ -145,7 +150,7 @@ func TestContext2Apply_PolicyEvaluation_Full(t *testing.T) {
145150 applyPolicyClient := policy .NewTestMockClient (t )
146151
147152 // The expected values to be sent for policy evaluation.
148- expected = map [string ]cty.Value {
153+ expectedApply : = map [string ]cty.Value {
149154 "test_resource" : cty .ObjectVal (map [string ]cty.Value {
150155 "value" : cty .NullVal (cty .String ),
151156 "sensitive_value" : cty .StringVal ("foo" ),
@@ -155,6 +160,8 @@ func TestContext2Apply_PolicyEvaluation_Full(t *testing.T) {
155160 "sensitive_value" : cty .NilVal ,
156161 }),
157162 }
163+ actualApply := make (map [string ]cty.Value )
164+
158165 applyPolicyClient .EvaluateFn = func (ctx context.Context , req policy.EvaluationRequest [* proto.ResourceMetadata ]) policy.EvaluationResponse {
159166 var actual cty.Value
160167 attrs := req .Attrs
@@ -166,15 +173,18 @@ func TestContext2Apply_PolicyEvaluation_Full(t *testing.T) {
166173 "sensitive_value" : mp ["sensitive_value" ],
167174 })
168175 }
169-
170- if diff := cmp .Diff (actual , expected [target ], cmp .Comparer (cty .Value .RawEquals )); diff != "" {
171- t .Errorf ("Unexpected diff (-got +want):\n %s" , diff )
172- }
176+ actualApply [target ] = actual
173177
174178 // this return does not actually do anything
175179 return policy.EvaluationResponse {Overall : policy .AllowResult }
176180 }
177181
182+ t .Cleanup (func () {
183+ if diff := cmp .Diff (actualApply , expectedApply , cmp .Comparer (cty .Value .RawEquals )); diff != "" {
184+ t .Errorf ("Unexpected diff (-got +want):\n %s" , diff )
185+ }
186+ })
187+
178188 _ , diags = ctx .Apply (plan , mod , & ApplyOpts {
179189 PolicyClient : applyPolicyClient ,
180190 })
@@ -385,6 +395,257 @@ func TestContext2Apply_PolicyEvaluationError(t *testing.T) {
385395 }
386396}
387397
398+ func TestContext2Apply_PolicyEvaluation_NoResourceAfterPolicy (t * testing.T ) {
399+ // This verifies that no resource instance node is run after policy evaluation
400+ mainConfig := `
401+ terraform {
402+ required_providers {
403+ test = {
404+ source = "hashicorp/test"
405+ version = "1.0.0"
406+ }
407+ }
408+ }
409+
410+ resource "test_instance" "test" {
411+ count = 2
412+ value = tostring(count.index)
413+ }
414+ `
415+
416+ policyConfig := `
417+ resource_policy "test_instance" "policy_name" {
418+ enforce {
419+ condition = true
420+ }
421+ }
422+ `
423+
424+ mod := testModuleInline (t , map [string ]string {
425+ "main.tf" : mainConfig ,
426+ "main.tfpolicy.hcl" : policyConfig ,
427+ })
428+
429+ providerAddr := addrs .NewDefaultProvider ("test" )
430+ provider := testProvider ("test" )
431+
432+ var policyRan atomic.Bool
433+ var applyCalls atomic.Int32
434+
435+ provider .ApplyResourceChangeFn = func (req providers.ApplyResourceChangeRequest ) (resp providers.ApplyResourceChangeResponse ) {
436+ callNum := applyCalls .Add (1 )
437+ if callNum == 2 {
438+ time .Sleep (150 * time .Millisecond )
439+ }
440+
441+ if policyRan .Load () {
442+ t .Fatalf ("resource apply for %s ran after policy evaluation" , req .TypeName )
443+ }
444+
445+ newState := req .PlannedState .AsValueMap ()
446+ newState ["id" ] = cty .StringVal (req .PlannedState .GetAttr ("value" ).AsString ())
447+ newState ["type" ] = cty .StringVal (req .TypeName )
448+ newState ["unknown" ] = cty .StringVal ("known" )
449+ resp .NewState = cty .ObjectVal (newState )
450+ return resp
451+ }
452+
453+ ctx , diags := NewContext (& ContextOpts {
454+ Providers : map [addrs.Provider ]providers.Factory {
455+ providerAddr : testProviderFuncFixed (provider ),
456+ },
457+ Parallelism : 4 ,
458+ })
459+ tfdiags .AssertNoDiagnostics (t , diags )
460+
461+ plan , diags := ctx .Plan (mod , states .NewState (), & PlanOpts {
462+ Mode : plans .NormalMode ,
463+ SetVariables : testInputValuesUnset (mod .Module .Variables ),
464+ })
465+ tfdiags .AssertNoDiagnostics (t , diags )
466+
467+ applyPolicyClient := policy .NewTestMockClient (t )
468+ applyPolicyClient .EvaluateFn = func (ctx context.Context , req policy.EvaluationRequest [* proto.ResourceMetadata ]) policy.EvaluationResponse {
469+ policyRan .Store (true )
470+
471+ if diff := cmp .Diff (req .Meta , & proto.ResourceMetadata {
472+ Type : "test_instance" ,
473+ ProviderType : "test" ,
474+ Operation : proto .Operation_CREATE ,
475+ }, protocmp .Transform ()); diff != "" {
476+ t .Errorf ("Invalid resource metadata: %s" , diff )
477+ }
478+
479+ return policy.EvaluationResponse {Overall : policy .AllowResult }
480+ }
481+
482+ resultState , diags := ctx .Apply (plan , mod , & ApplyOpts {
483+ PolicyClient : applyPolicyClient ,
484+ })
485+ tfdiags .AssertNoDiagnostics (t , diags )
486+
487+ if ! applyPolicyClient .EvaluateCalled {
488+ t .Fatal ("expected policy evaluation to be called during apply" )
489+ }
490+
491+ remainingAddrs := resultState .AllManagedResourceInstanceObjectAddrs ()
492+ if len (remainingAddrs ) != 2 {
493+ t .Fatalf ("expected 2 managed resources in the state after apply, got %d: %v" , len (remainingAddrs ), remainingAddrs )
494+ }
495+ }
496+
497+ func TestContext2Apply_PolicyEvaluation_ChangedResourceCount (t * testing.T ) {
498+ cases := []struct {
499+ name string
500+ state * states.State
501+ configBody string
502+ expectTarget string
503+ expectOp proto.Operation
504+ expectCalls int
505+ expectFinalAttr cty.Value
506+ }{
507+ {
508+ name : "create" ,
509+ state : states .NewState (),
510+ configBody : `
511+ resource "test_resource" "test" {
512+ sensitive_value = "foo"
513+ }
514+ ` ,
515+ expectTarget : "test_resource" ,
516+ expectOp : proto .Operation_CREATE ,
517+ expectCalls : 1 ,
518+ expectFinalAttr : cty .ObjectVal (map [string ]cty.Value {
519+ "id" : cty .StringVal ("created" ),
520+ "sensitive_value" : cty .StringVal ("foo" ),
521+ }),
522+ },
523+ {
524+ name : "update" ,
525+ state : states .BuildState (func (ss * states.SyncState ) {
526+ ss .SetResourceInstanceCurrent (
527+ mustResourceInstanceAddr ("test_resource.test" ),
528+ & states.ResourceInstanceObjectSrc {
529+ Status : states .ObjectReady ,
530+ AttrsJSON : []byte (`{"id":"existing","type":"test_resource","sensitive_value":"before"}` ),
531+ },
532+ mustProviderConfig (`provider["registry.terraform.io/hashicorp/test"]` ),
533+ )
534+ }),
535+ configBody : `
536+ resource "test_resource" "test" {
537+ sensitive_value = "after"
538+ }
539+ ` ,
540+ expectTarget : "test_resource" ,
541+ expectOp : proto .Operation_UPDATE ,
542+ expectCalls : 1 ,
543+ expectFinalAttr : cty .ObjectVal (map [string ]cty.Value {
544+ "id" : cty .StringVal ("existing" ),
545+ "sensitive_value" : cty .StringVal ("after" ),
546+ }),
547+ },
548+ }
549+
550+ for _ , tc := range cases {
551+ t .Run (tc .name , func (t * testing.T ) {
552+ mainConfig := `
553+ terraform {
554+ required_providers {
555+ test = {
556+ source = "hashicorp/test"
557+ version = "1.0.0"
558+ }
559+ }
560+ }
561+ ` + tc .configBody
562+
563+ policyConfig := `
564+ resource_policy "test_resource" "policy_name" {
565+ enforce {
566+ condition = true
567+ }
568+ }
569+ `
570+ mod := testModuleInline (t , map [string ]string {
571+ "main.tf" : mainConfig ,
572+ "main.tfpolicy.hcl" : policyConfig ,
573+ })
574+
575+ providerAddr := addrs .NewDefaultProvider ("test" )
576+ provider := testProvider ("test" )
577+ provider .ApplyResourceChangeFn = func (req providers.ApplyResourceChangeRequest ) (resp providers.ApplyResourceChangeResponse ) {
578+ cfg := req .Config .AsValueMap ()
579+ if req .TypeName == "test_resource" {
580+ if id , ok := cfg ["id" ]; ok && ! id .IsNull () && id .IsKnown () {
581+ cfg ["id" ] = id
582+ } else if tc .name == "create" {
583+ cfg ["id" ] = cty .StringVal ("created" )
584+ } else {
585+ cfg ["id" ] = cty .StringVal ("existing" )
586+ }
587+ }
588+ resp .NewState = cty .ObjectVal (cfg )
589+ return resp
590+ }
591+
592+ ctx , diags := NewContext (& ContextOpts {
593+ Providers : map [addrs.Provider ]providers.Factory {
594+ providerAddr : testProviderFuncFixed (provider ),
595+ },
596+ Parallelism : 1 ,
597+ })
598+ tfdiags .AssertNoDiagnostics (t , diags )
599+
600+ planPolicyClient := policy .NewTestMockClient (t )
601+ plan , diags := ctx .Plan (mod , tc .state , & PlanOpts {
602+ Mode : plans .NormalMode ,
603+ SetVariables : testInputValuesUnset (mod .Module .Variables ),
604+ PolicyClient : planPolicyClient ,
605+ })
606+ tfdiags .AssertNoDiagnostics (t , diags )
607+
608+ applyPolicyClient := policy .NewTestMockClient (t )
609+ var called int
610+ applyPolicyClient .EvaluateFn = func (ctx context.Context , req policy.EvaluationRequest [* proto.ResourceMetadata ]) policy.EvaluationResponse {
611+ called ++
612+ if req .Target != tc .expectTarget {
613+ t .Fatalf ("expected target %s, got %s" , tc .expectTarget , req .Target )
614+ }
615+ if diff := cmp .Diff (req .Meta , & proto.ResourceMetadata {
616+ Type : tc .expectTarget ,
617+ ProviderType : "test" ,
618+ Operation : tc .expectOp ,
619+ }, protocmp .Transform ()); diff != "" {
620+ t .Fatalf ("unexpected resource metadata (-got +want):\n %s" , diff )
621+ }
622+
623+ actualAttrs := req .Attrs
624+ if ! actualAttrs .IsNull () {
625+ mp := actualAttrs .AsValueMap ()
626+ actualAttrs = cty .ObjectVal (map [string ]cty.Value {
627+ "id" : mp ["id" ],
628+ "sensitive_value" : mp ["sensitive_value" ],
629+ })
630+ }
631+ if diff := cmp .Diff (actualAttrs , tc .expectFinalAttr , cmp .Comparer (cty .Value .RawEquals )); diff != "" {
632+ t .Fatalf ("unexpected attrs (-got +want):\n %s" , diff )
633+ }
634+ return policy.EvaluationResponse {Overall : policy .AllowResult }
635+ }
636+
637+ _ , diags = ctx .Apply (plan , mod , & ApplyOpts {
638+ PolicyClient : applyPolicyClient ,
639+ })
640+ tfdiags .AssertNoDiagnostics (t , diags )
641+
642+ if called != tc .expectCalls {
643+ t .Fatalf ("expected %d policy evaluation call(s), got %d" , tc .expectCalls , called )
644+ }
645+ })
646+ }
647+ }
648+
388649func TestContext2Apply_PolicyEvaluation_Destroy (t * testing.T ) {
389650 mainConfig := `
390651 terraform {
0 commit comments