| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package command | |
| import ( | |
| "context" | |
| "os" | |
| "path/filepath" | |
| "slices" | |
| "strings" | |
| "sync/atomic" | |
| "testing" | |
| "github.com/google/go-cmp/cmp" | |
| "github.com/zclconf/go-cty/cty" | |
| "google.golang.org/protobuf/testing/protocmp" | |
| "github.com/hashicorp/terraform/internal/policy" | |
| "github.com/hashicorp/terraform/internal/policy/proto" | |
| ) | |
| func TestInit_WithModulePolicy(t *testing.T) { | |
| cases := []struct { | |
| name string | |
| policy *policy.EvaluationResponse | |
| expected int | |
| }{ | |
| { | |
| name: "unknown module policy", | |
| // This unknown evaluation should still lead to success of the init operation. | |
| policy: &policy.EvaluationResponse{Overall: policy.UnknownResult}, | |
| expected: 0, | |
| }, | |
| { | |
| name: "advisory deny module policy", | |
| // Advisory policies may return deny without error diagnostics and should | |
| // not block init. | |
| policy: &policy.EvaluationResponse{Overall: policy.DenyResult}, | |
| expected: 0, | |
| }, | |
| { | |
| name: "success module policy", | |
| policy: &policy.EvaluationResponse{Overall: policy.AllowResult}, | |
| expected: 0, | |
| }, | |
| { | |
| name: "errored module policy", | |
| policy: &policy.EvaluationResponse{ | |
| Overall: policy.DenyResult, | |
| Diagnostics: policy.Diagnostics{ | |
| policy.NewErrorDiagnostic("test error", "test error detail", policy.DenyResult), | |
| }, | |
| }, | |
| expected: 1, | |
| }, | |
| } | |
| for _, tc := range cases { | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("dynamic-module-sources/local-source-with-variable"), td) | |
| t.Chdir(td) | |
| ui := testUiWrapped(t) | |
| view, done := testView(t) | |
| overrides := metaOverridesForProvider(testProvider()) | |
| policyClient := policy.NewTestMockClient(t) | |
| t.Run(tc.name, func(t *testing.T) { | |
| policyClient.EvaluateModuleResponse = tc.policy | |
| overrides.PolicyClient = policyClient | |
| c := &InitCommand{ | |
| Meta: Meta{ | |
| testingOverrides: overrides, | |
| Ui: ui, | |
| View: view, | |
| AllowExperimentalFeatures: true, | |
| }, | |
| } | |
| code := c.Run([]string{"-policies", td, "-var", "module_name=example"}) | |
| output := done(t) | |
| if code != tc.expected { | |
| t.Fatalf("got exit status %d; want %d\nstderr:\n%s\n\nstdout:\n%s", code, tc.expected, output.Stderr(), output.Stdout()) | |
| } | |
| if !policyClient.EvaluateModuleCalled { | |
| t.Fatal("expected EvaluateModule to be called") | |
| } | |
| if got, want := policyClient.EvaluateModuleRequest.Target, "./modules/example"; got != want { | |
| t.Fatalf("wrong module policy target\ngot: %q\nwant: %q", got, want) | |
| } | |
| if !policyClient.EvaluateModuleCalled { | |
| t.Fatal("expected EvaluateModule to be called") | |
| } | |
| if got, want := policyClient.EvaluateModuleRequest.Target, "./modules/example"; got != want { | |
| t.Fatalf("wrong module policy target\ngot: %q\nwant: %q", got, want) | |
| } | |
| if policyClient.EvaluateModuleRequest.Meta == nil { | |
| t.Fatal("expected module metadata to be set") | |
| } | |
| if got, want := policyClient.EvaluateModuleRequest.Meta.Address, "module.example"; got != want { | |
| t.Fatalf("wrong module address\ngot: %q\nwant: %q", got, want) | |
| } | |
| if got, want := policyClient.EvaluateModuleRequest.Meta.Version, ""; got != want { | |
| t.Fatalf("wrong module version\ngot: %q\nwant: %q", got, want) | |
| } | |
| }) | |
| } | |
| } | |
| func TestInit_WithPolicyClientStopAfterInit(t *testing.T) { | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("dynamic-module-sources/local-source-with-variable"), td) | |
| t.Chdir(td) | |
| ui := testUiWrapped(t) | |
| view, done := testView(t) | |
| overrides := metaOverridesForProvider(testProvider()) | |
| policyClient := policy.NewTestMockClient(t) | |
| var stopCalled atomic.Bool | |
| var policyEvaluated atomic.Bool | |
| policyClient.StopFn = func() { | |
| stopCalled.Store(true) | |
| } | |
| policyClient.EvaluateModuleFn = func(ctx context.Context, req policy.EvaluationRequest[*proto.PolicyEvaluateModuleRequest_ModuleMetadata]) policy.EvaluationResponse { | |
| policyEvaluated.Store(true) | |
| if stopCalled.Load() { | |
| t.Fatal("policy client Stop was called before init finished") | |
| } | |
| return policy.EvaluationResponse{Overall: policy.AllowResult} | |
| } | |
| overrides.PolicyClient = policyClient | |
| c := &InitCommand{ | |
| Meta: Meta{ | |
| testingOverrides: overrides, | |
| Ui: ui, | |
| View: view, | |
| AllowExperimentalFeatures: true, | |
| }, | |
| } | |
| code := c.Run([]string{"-policies", td, "-var", "module_name=example"}) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("got exit status %d; want 0\nstderr:\n%s\n\nstdout:\n%s", code, output.Stderr(), output.Stdout()) | |
| } | |
| if !policyEvaluated.Load() { | |
| t.Fatal("expected module policy evaluation to be called during init") | |
| } | |
| if !stopCalled.Load() { | |
| t.Fatal("expected policy client Stop to be called after init") | |
| } | |
| } | |
| func TestInit_WithModulePolicy_AlreadyInstalled(t *testing.T) { | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("dynamic-module-sources/add-version-constraint"), td) | |
| t.Chdir(td) | |
| if err := os.WriteFile(filepath.Join(td, ".terraform/modules/modules.json"), []byte(`{ | |
| "Modules": [ | |
| { | |
| "Key": "", | |
| "Source": "", | |
| "Dir": "" | |
| }, | |
| { | |
| "Key": "child", | |
| "Source": "registry.terraform.io/hashicorp/module-installer-acctest/aws", | |
| "Version": "0.0.1", | |
| "Dir": ".terraform/modules/child" | |
| } | |
| ] | |
| }`), 0644); err != nil { | |
| t.Fatal(err) | |
| } | |
| ui := testUiWrapped(t) | |
| view, done := testView(t) | |
| overrides := metaOverridesForProvider(testProvider()) | |
| policyClient := policy.NewTestMockClient(t) | |
| policyClient.EvaluateModuleFn = func(ctx context.Context, req policy.EvaluationRequest[*proto.PolicyEvaluateModuleRequest_ModuleMetadata]) policy.EvaluationResponse { | |
| if got, want := req.Meta.Version, "0.0.1"; got != want { | |
| t.Fatalf("wrong module version\ngot: %q\nwant: %q", got, want) | |
| } | |
| return policy.EvaluationResponse{ | |
| Overall: policy.DenyResult, | |
| Diagnostics: policy.Diagnostics{ | |
| policy.NewErrorDiagnostic( | |
| "module policy denied", | |
| "module policy blocked installation", | |
| policy.DenyResult, | |
| ), | |
| }, | |
| } | |
| } | |
| overs := overrides | |
| overs.PolicyClient = policyClient | |
| c := &InitCommand{ | |
| Meta: Meta{ | |
| testingOverrides: overs, | |
| Ui: ui, | |
| View: view, | |
| AllowExperimentalFeatures: true, | |
| }, | |
| } | |
| code := c.Run([]string{"-policies", td, "-no-color"}) | |
| output := done(t) | |
| if code != 1 { | |
| t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, output.Stderr(), output.Stdout()) | |
| } | |
| if !policyClient.EvaluateModuleCalled { | |
| t.Fatal("expected EvaluateModule to be called for already-installed module") | |
| } | |
| expected := ` | |
| Error: module policy denied | |
| on add-version-constraint.tf line 7: | |
| 7: module "child" { | |
| module policy blocked installation | |
| Error: Policy evaluation failed | |
| Module download blocked due to policy violations. Please review other | |
| diagnostics for details. | |
| ` | |
| if diff := cmp.Diff(expected, output.Stderr()); diff != "" { | |
| t.Fatalf("unexpected stderr:\n%s", diff) | |
| } | |
| } | |
| func TestInit_WithPolicySetupFailureJSON(t *testing.T) { | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("init-nested-provider-requirements"), td) | |
| t.Chdir(td) | |
| providerSource := newMockProviderSource(t, map[string][]string{ | |
| "hashicorp/test": {"1.2.3", "1.2.4"}, | |
| "tf.example.com/awesomecorp/happycloud": {"1.0.0"}, | |
| "hashicorp/null": {"2.0.1"}, | |
| "hashicorp/grandchild": {"1.0.0"}, | |
| }) | |
| ui := testUiWrapped(t) | |
| view, done := testView(t) | |
| c := &InitCommand{ | |
| Meta: Meta{ | |
| Ui: ui, | |
| View: view, | |
| ProviderSource: providerSource, | |
| AllowExperimentalFeatures: true, | |
| }, | |
| } | |
| code := c.Run([]string{"-policies", td, "-no-color", "-json"}) | |
| output := done(t) | |
| if code != 1 { | |
| t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, output.Stderr(), output.Stdout()) | |
| } | |
| expected := `{"@level":"info","@message":"Terraform 1.15.0-dev","@module":"terraform.ui","terraform":"1.15.0-dev","type":"version","ui":"1.3"} | |
| {"@level":"info","@message":"Initializing the backend...","@module":"terraform.ui","message_code":"initializing_backend_message","type":"init_output"} | |
| {"@level":"error","@message":"Error: Failed to connect to policy engine","@module":"terraform.ui","@policy":"true","policy_diagnostic":{"severity":"error","summary":"Failed to connect to policy engine","detail":"Failed to connect to policy engine: failed to connect to plugin: exec: \"tfpolicy-plugin\": executable file not found in $PATH."},"policy_metadata":{},"result":"SetupErrorResult","type":"policy_diagnostic"}` | |
| checkGoldenReferenceStr(t, output, expected) | |
| } | |
| func TestInit_WithModulePolicyDiagnostics(t *testing.T) { | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("dynamic-module-sources/local-source-with-variable"), td) | |
| t.Chdir(td) | |
| ui := testUiWrapped(t) | |
| view, done := testView(t) | |
| overrides := metaOverridesForProvider(testProvider()) | |
| policyClient := policy.NewTestMockClient(t) | |
| policyClient.EvaluateModuleResponse = &policy.EvaluationResponse{ | |
| Overall: policy.DenyResult, | |
| Diagnostics: policy.Diagnostics{ | |
| policy.DiagsFromProto([]*proto.Diagnostic{ | |
| { | |
| Severity: proto.Severity_ERROR, | |
| Summary: "module policy denied", | |
| Result: &proto.DiagnosticResult{ | |
| Result: proto.EvaluateResult_DENY_EVALUATE_RESULT, | |
| }, | |
| }, | |
| }, &policy.Policy{ | |
| Address: "module_policy.example", | |
| Filename: "policy_file.tfpolicy.hcl", | |
| EnforcementLevel: "mandatory", | |
| Result: policy.DenyResult, | |
| })[0], | |
| }, | |
| Policies: []*policy.Policy{ | |
| { | |
| Address: "module_policy.example", | |
| Filename: "policy_file.tfpolicy.hcl", | |
| EnforcementLevel: "mandatory", | |
| Result: policy.DenyResult, | |
| }, | |
| }, | |
| } | |
| overrides.PolicyClient = policyClient | |
| c := &InitCommand{ | |
| Meta: Meta{ | |
| testingOverrides: overrides, | |
| Ui: ui, | |
| View: view, | |
| AllowExperimentalFeatures: true, | |
| }, | |
| } | |
| code := c.Run([]string{"-policies", td, "-var", "module_name=example", "-no-color"}) | |
| output := done(t) | |
| if code != 1 { | |
| t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, output.Stderr(), output.Stdout()) | |
| } | |
| if !policyClient.EvaluateModuleCalled { | |
| t.Fatal("expected EvaluateModule to be called") | |
| } | |
| stderr := output.Stderr() | |
| expected := ` | |
| Error: module policy denied | |
| on main.tf line 6: | |
| 6: module "example" { | |
| Error: Policy evaluation failed | |
| Module download blocked due to policy violations. Please review other | |
| diagnostics for details. | |
| ` | |
| if diff := cmp.Diff(expected, stderr); diff != "" { | |
| t.Fatalf("unexpected stderr:\n%s", diff) | |
| } | |
| } | |
| func TestInit_WithNestedModulePolicyDiagnostics(t *testing.T) { | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("init-nested-provider-requirements"), td) | |
| t.Chdir(td) | |
| ui := testUiWrapped(t) | |
| view, done := testView(t) | |
| // assert modules are evaluated correctly | |
| expected := map[string]policy.EvaluateResult{ | |
| "./grandchild": policy.DenyResult, | |
| "./modules/child": policy.AllowResult, | |
| } | |
| actual := map[string]policy.EvaluateResult{} | |
| overrides := metaOverridesForProvider(testProvider()) | |
| policyClient := policy.NewTestMockClient(t) | |
| policyClient.EvaluateModuleFn = func(ctx context.Context, req policy.EvaluationRequest[*proto.PolicyEvaluateModuleRequest_ModuleMetadata]) policy.EvaluationResponse { | |
| if req.Target != "./grandchild" { | |
| actual[req.Target] = policy.AllowResult | |
| return policy.EvaluationResponse{Overall: policy.AllowResult} | |
| } | |
| actual[req.Target] = policy.DenyResult | |
| return policy.EvaluationResponse{ | |
| Overall: policy.DenyResult, | |
| Diagnostics: policy.Diagnostics{ | |
| policy.DiagsFromProto([]*proto.Diagnostic{ | |
| { | |
| Severity: proto.Severity_ERROR, | |
| Summary: "nested module policy denied", | |
| Result: &proto.DiagnosticResult{ | |
| Result: proto.EvaluateResult_DENY_EVALUATE_RESULT, | |
| }, | |
| }, | |
| }, &policy.Policy{ | |
| Address: "module_policy.nested", | |
| Filename: "policy_file.tfpolicy.hcl", | |
| EnforcementLevel: "mandatory", | |
| Result: policy.DenyResult, | |
| })[0], | |
| }, | |
| } | |
| } | |
| overrides.PolicyClient = policyClient | |
| c := &InitCommand{ | |
| Meta: Meta{ | |
| testingOverrides: overrides, | |
| Ui: ui, | |
| View: view, | |
| AllowExperimentalFeatures: true, | |
| }, | |
| } | |
| code := c.Run([]string{"-policies", td, "-no-color"}) | |
| output := done(t) | |
| if code != 1 { | |
| t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, output.Stderr(), output.Stdout()) | |
| } | |
| if !policyClient.EvaluateModuleCalled { | |
| t.Fatal("expected EvaluateModule to be called") | |
| } | |
| if diff := cmp.Diff(expected, actual); diff != "" { | |
| t.Fatalf("unexpected module policy evaluation results:\n%s", diff) | |
| } | |
| stderr := output.Stderr() | |
| expectedStderr := ` | |
| Error: nested module policy denied | |
| on modules/child/main.tf line 12: | |
| 12: module "nested" { | |
| Error: Policy evaluation failed | |
| Module download blocked due to policy violations. Please review other | |
| diagnostics for details. | |
| ` | |
| if diff := cmp.Diff(expectedStderr, stderr); diff != "" { | |
| t.Fatalf("unexpected stderr:\n%s\nexpected:\n%s\n diff:\n%s", stderr, expectedStderr, diff) | |
| } | |
| } | |
| func TestInit_WithModulePolicyJSON(t *testing.T) { | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("dynamic-module-sources/local-source-with-variable"), td) | |
| t.Chdir(td) | |
| ui := testUiWrapped(t) | |
| view, done := testView(t) | |
| overrides := metaOverridesForProvider(testProvider()) | |
| policyClient := policy.NewTestMockClient(t) | |
| resp := policy.EvaluationFromProtoResponse( | |
| proto.EvaluateResult_DENY_EVALUATE_RESULT, | |
| []*proto.PolicyEvaluationDetail{ | |
| { | |
| Address: "module_policy.example", | |
| Result: proto.EvaluateResult_DENY_EVALUATE_RESULT, | |
| File: "policy_file.tfpolicy.hcl", | |
| PolicySetEnforcement: "mandatory", | |
| Diagnostics: []*proto.Diagnostic{ | |
| { | |
| Severity: proto.Severity_ERROR, | |
| Summary: "module policy denied", | |
| Result: &proto.DiagnosticResult{ | |
| Result: proto.EvaluateResult_DENY_EVALUATE_RESULT, | |
| }, | |
| }, | |
| }, | |
| DefRange: &proto.Range{ | |
| Filename: "policy_set.policy.hcl", | |
| }, | |
| }, | |
| }, | |
| ) | |
| policyClient.EvaluateModuleResponse = &resp | |
| overrides.PolicyClient = policyClient | |
| c := &InitCommand{ | |
| Meta: Meta{ | |
| testingOverrides: overrides, | |
| Ui: ui, | |
| View: view, | |
| AllowExperimentalFeatures: true, | |
| }, | |
| } | |
| code := c.Run([]string{"-policies", td, "-var", "module_name=example", "-no-color", "-json"}) | |
| output := done(t) | |
| if code != 1 { | |
| t.Fatalf("got exit status %d; want 0\nstderr:\n%s\n\nstdout:\n%s", code, output.Stderr(), output.Stdout()) | |
| } | |
| if !policyClient.EvaluateModuleCalled { | |
| t.Fatal("expected EvaluateModule to be called") | |
| } | |
| expected := `{"@level":"info","@message":"Terraform 1.15.0-dev","@module":"terraform.ui","terraform":"1.15.0-dev","type":"version","ui":"1.3"} | |
| {"@level":"info","@message":"Initializing the backend...","@module":"terraform.ui","message_code":"initializing_backend_message","type":"init_output"} | |
| {"@level":"info","@message":"Initializing modules...","@module":"terraform.ui","message_code":"initializing_modules_message","type":"init_output"} | |
| {"@level":"error","@message":"Error: module policy denied","@module":"terraform.ui","@policy":"true","policy_diagnostic":{"severity":"error","summary":"module policy denied","detail":"","range":{"filename":"main.tf","start":{"line":6,"column":1,"byte":60},"end":{"line":6,"column":17,"byte":76}},"snippet":{"context":null,"code":"module \"example\" {","start_line":6,"highlight_start_offset":0,"highlight_end_offset":16,"values":[]}},"policy_metadata":{"policy_set_path":"policy_file.tfpolicy.hcl","policy_name":"module_policy.example","file_name":"policy_set.policy.hcl","enforcement_level":"mandatory"},"result":"DenyResult","target_address":"module.example","type":"policy_diagnostic"} | |
| {"@level":"info","@message":"Policy Result","@module":"terraform.ui","@policy":"true","target_address":"module.example","policy_address":"module_policy.example","policy_metadata":{"policy_set_path":"policy_file.tfpolicy.hcl","policy_name":"module_policy.example","file_name":"policy_set.policy.hcl","enforcement_level":"mandatory"},"result":"DenyResult","type":"policy_result"} | |
| {"@level":"error","@message":"Error: Policy evaluation failed","@module":"terraform.ui","diagnostic":{"severity":"error","summary":"Policy evaluation failed","detail":"Module download blocked due to policy violations. Please review other diagnostics for details."},"type":"diagnostic"}` | |
| checkGoldenReferenceStr(t, output, expected) | |
| } | |
| func TestInit_WithProviderPolicy(t *testing.T) { | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("init-nested-provider-requirements"), td) | |
| t.Chdir(td) | |
| providerSource := newMockProviderSource(t, map[string][]string{ | |
| "hashicorp/test": {"1.2.3", "1.2.4"}, | |
| "tf.example.com/awesomecorp/happycloud": {"1.0.0"}, | |
| "hashicorp/null": {"2.0.1"}, | |
| "hashicorp/grandchild": {"1.0.0"}, | |
| }) | |
| ui := testUiWrapped(t) | |
| view, done := testView(t) | |
| overrides := metaOverridesForProvider(testProvider()) | |
| policyClient := policy.NewTestMockClient(t) | |
| actualTargets := []string{} | |
| expectedTargets := []string{"test", "happycloud", "null", "grandchild"} | |
| policyClient.EvaluateProviderFn = func(ctx context.Context, req policy.EvaluationRequest[*proto.PolicyEvaluateProviderRequest_ProviderMetadata]) policy.EvaluationResponse { | |
| actualTargets = append(actualTargets, req.Target) | |
| var expected *proto.PolicyEvaluateProviderRequest_ProviderMetadata | |
| switch req.Target { | |
| case "test": | |
| expected = &proto.PolicyEvaluateProviderRequest_ProviderMetadata{ | |
| Name: "test", | |
| Namespace: "hashicorp", | |
| Source: "registry.terraform.io/hashicorp/test", | |
| Version: "1.2.3", | |
| } | |
| case "happycloud": | |
| expected = &proto.PolicyEvaluateProviderRequest_ProviderMetadata{ | |
| Name: "happycloud", | |
| Namespace: "awesomecorp", | |
| Source: "tf.example.com/awesomecorp/happycloud", | |
| Version: "1.0.0", | |
| } | |
| case "null": | |
| expected = &proto.PolicyEvaluateProviderRequest_ProviderMetadata{ | |
| Name: "null", | |
| Namespace: "hashicorp", | |
| Source: "registry.terraform.io/hashicorp/null", | |
| Version: "2.0.1", | |
| } | |
| case "grandchild": | |
| expected = &proto.PolicyEvaluateProviderRequest_ProviderMetadata{ | |
| Name: "grandchild", | |
| Namespace: "hashicorp", | |
| Source: "registry.terraform.io/hashicorp/grandchild", | |
| Version: "1.0.0", | |
| } | |
| } | |
| if diff := cmp.Diff(expected, req.Meta, protocmp.Transform(), cmp.Comparer(cty.Value.RawEquals)); diff != "" { | |
| t.Fatalf("wrong provider metadata\ngot: %s\nwant: %v", diff, expected) | |
| } | |
| t.Cleanup(func() { | |
| if !slices.Contains(expectedTargets, req.Target) { | |
| t.Errorf("expected target %q to be in %v", req.Target, expectedTargets) | |
| } | |
| }) | |
| if req.Target == "test" { | |
| // This unknown evaluation should still lead to success of the init operation. | |
| return policy.EvaluationResponse{Overall: policy.UnknownResult} | |
| } | |
| if req.Target == "happycloud" { | |
| // Advisory policies may return deny without error diagnostics and should | |
| // not block init. | |
| return policy.EvaluationResponse{Overall: policy.DenyResult} | |
| } | |
| return policy.EvaluationResponse{Overall: policy.AllowResult} | |
| } | |
| overrides.PolicyClient = policyClient | |
| c := &InitCommand{ | |
| Meta: Meta{ | |
| testingOverrides: overrides, | |
| Ui: ui, | |
| View: view, | |
| ProviderSource: providerSource, | |
| AllowExperimentalFeatures: true, | |
| }, | |
| } | |
| code := c.Run([]string{"-policies", td}) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("got exit status %d; want 0\nstderr:\n%s\n\nstdout:\n%s", code, output.Stderr(), output.Stdout()) | |
| } | |
| if !policyClient.EvaluateProviderCalled { | |
| t.Fatal("expected EvaluateProvider to be called") | |
| } | |
| } | |
| func TestInit_WithProviderPolicyDiagnostics(t *testing.T) { | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("init-nested-provider-requirements"), td) | |
| t.Chdir(td) | |
| providerSource := newMockProviderSource(t, map[string][]string{ | |
| "hashicorp/test": {"1.2.3", "1.2.4"}, | |
| "tf.example.com/awesomecorp/happycloud": {"1.0.0"}, | |
| "hashicorp/null": {"2.0.1"}, | |
| "hashicorp/grandchild": {"1.0.0"}, | |
| }) | |
| ui := testUiWrapped(t) | |
| view, done := testView(t) | |
| overrides := metaOverridesForProvider(testProvider()) | |
| policyClient := policy.NewTestMockClient(t) | |
| expectedTargets := []string{"test", "happycloud", "null", "grandchild"} | |
| actualTargets := []string{} | |
| policyClient.EvaluateProviderFn = func(ctx context.Context, req policy.EvaluationRequest[*proto.PolicyEvaluateProviderRequest_ProviderMetadata]) policy.EvaluationResponse { | |
| actualTargets = append(actualTargets, req.Target) | |
| return policy.EvaluationFromProtoResponse( | |
| proto.EvaluateResult_DENY_EVALUATE_RESULT, | |
| []*proto.PolicyEvaluationDetail{ | |
| { | |
| Address: "provider_policy." + req.Target, | |
| Result: proto.EvaluateResult_DENY_EVALUATE_RESULT, | |
| File: "policy_file.tfpolicy.hcl", | |
| PolicySetEnforcement: "mandatory", | |
| Diagnostics: []*proto.Diagnostic{ | |
| { | |
| Severity: proto.Severity_ERROR, | |
| Summary: req.Target + " provider policy denied", | |
| Result: &proto.DiagnosticResult{ | |
| Result: proto.EvaluateResult_DENY_EVALUATE_RESULT, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| ) | |
| } | |
| overs := overrides | |
| overs.PolicyClient = policyClient | |
| c := &InitCommand{ | |
| Meta: Meta{ | |
| testingOverrides: overs, | |
| Ui: ui, | |
| View: view, | |
| ProviderSource: providerSource, | |
| AllowExperimentalFeatures: true, | |
| }, | |
| } | |
| code := c.Run([]string{"-policies", td, "-no-color"}) | |
| output := done(t) | |
| if code != 1 { | |
| t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, output.Stderr(), output.Stdout()) | |
| } | |
| if !policyClient.EvaluateProviderCalled { | |
| t.Fatal("expected EvaluateProvider to be called") | |
| } | |
| if got, want := len(actualTargets), len(expectedTargets); got != want { | |
| t.Fatalf("wrong number of evaluated providers\ngot: %d (%v)\nwant: %d", got, actualTargets, want) | |
| } | |
| for _, target := range expectedTargets { | |
| if !slices.Contains(actualTargets, target) { | |
| t.Fatalf("missing provider evaluation for %q in %v", target, actualTargets) | |
| } | |
| } | |
| stderr := output.Stderr() | |
| for _, target := range expectedTargets { | |
| if !strings.Contains(stderr, "Error: "+target+" provider policy denied") { | |
| t.Fatalf("missing policy diagnostic for %q in stderr:\n%s", target, stderr) | |
| } | |
| } | |
| if !strings.Contains(stderr, "Provider download blocked due to policy violations. Please review other diagnostics for details.") { | |
| t.Fatalf("missing provider policy error in stderr:\n%s", stderr) | |
| } | |
| } | |
| func TestInit_WithProviderPolicyJSON(t *testing.T) { | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("init-nested-provider-requirements"), td) | |
| t.Chdir(td) | |
| providerSource := newMockProviderSource(t, map[string][]string{ | |
| "hashicorp/test": {"1.2.3", "1.2.4"}, | |
| "awesomecorp/happycloud": {"1.0.0"}, | |
| "hashicorp/null": {"2.0.1"}, | |
| "hashicorp/grandchild": {"1.0.0"}, | |
| }) | |
| ui := testUiWrapped(t) | |
| view, done := testView(t) | |
| overrides := metaOverridesForProvider(testProvider()) | |
| policyClient := policy.NewTestMockClient(t) | |
| resp := policy.EvaluationFromProtoResponse( | |
| proto.EvaluateResult_DENY_EVALUATE_RESULT, | |
| []*proto.PolicyEvaluationDetail{ | |
| { | |
| Address: "provider_policy.example", | |
| Result: proto.EvaluateResult_DENY_EVALUATE_RESULT, | |
| File: "policy_file.tfpolicy.hcl", | |
| PolicySetEnforcement: "mandatory", | |
| Diagnostics: []*proto.Diagnostic{ | |
| { | |
| Severity: proto.Severity_ERROR, | |
| Summary: "provider policy denied", | |
| Result: &proto.DiagnosticResult{ | |
| Result: proto.EvaluateResult_DENY_EVALUATE_RESULT, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| ) | |
| policyClient.EvaluateProviderResponse = &resp | |
| overrides.PolicyClient = policyClient | |
| c := &InitCommand{ | |
| Meta: Meta{ | |
| testingOverrides: overrides, | |
| Ui: ui, | |
| View: view, | |
| ProviderSource: providerSource, | |
| AllowExperimentalFeatures: true, | |
| }, | |
| } | |
| code := c.Run([]string{"-policies", td, "-no-color", "-json"}) | |
| output := done(t) | |
| if code != 1 { | |
| t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, output.Stderr(), output.Stdout()) | |
| } | |
| if !policyClient.EvaluateProviderCalled { | |
| t.Fatal("expected EvaluateProvider to be called") | |
| } | |
| allOutput := strings.SplitSeq(output.Stdout(), "\n") | |
| var foundPolicyDiagnostic bool | |
| for line := range allOutput { | |
| if strings.Contains(line, `"type":"policy_diagnostic"`) { | |
| foundPolicyDiagnostic = strings.Contains(line, "provider policy denied") | |
| } | |
| } | |
| if !foundPolicyDiagnostic { | |
| t.Fatal("expected diagnostic output") | |
| } | |
| } |