diff --git a/.changes/v1.17/NEW FEATURES-20260810-122653.yaml b/.changes/v1.17/NEW FEATURES-20260810-122653.yaml new file mode 100644 index 000000000000..3a6e948715f0 --- /dev/null +++ b/.changes/v1.17/NEW FEATURES-20260810-122653.yaml @@ -0,0 +1,5 @@ +kind: NEW FEATURES +body: 'policy: Terraform Policy is now generally available. The `-policies` flag for `plan`, `apply`, and `init` no longer requires the `-allow-experimental-features` flag.' +time: 2026-08-10T12:26:53.000000+00:00 +custom: + Issue: "38970" diff --git a/internal/command/apply.go b/internal/command/apply.go index 7acf88e966b7..ed3a9b84d289 100644 --- a/internal/command/apply.go +++ b/internal/command/apply.go @@ -162,7 +162,7 @@ func (c *ApplyCommand) Run(rawArgs []string) int { } func (c *ApplyCommand) Validate(args *arguments.Apply) (diags tfdiags.Diagnostics) { - return diags.Append(validatePolicyPaths(args.PolicyPaths, c.AllowExperimentalFeatures)) + return diags.Append(validatePolicyPaths(args.PolicyPaths)) } func (c *ApplyCommand) LoadPlanFile(path string) (*planfile.WrappedPlanFile, tfdiags.Diagnostics) { diff --git a/internal/command/apply_policy_test.go b/internal/command/apply_policy_test.go index 92d5486a3476..59566d5bdacf 100644 --- a/internal/command/apply_policy_test.go +++ b/internal/command/apply_policy_test.go @@ -39,9 +39,8 @@ func TestApply_WithPolicyDiagnosticsJSON(t *testing.T) { overrides.PolicyClient = policyClient c := &ApplyCommand{ Meta: Meta{ - testingOverrides: overrides, - View: view, - AllowExperimentalFeatures: true, + testingOverrides: overrides, + View: view, }, } resp := policy.EvaluationFromProtoResponse( @@ -176,9 +175,8 @@ func TestApply_PolicyResultsJSON_WithSavedPlan(t *testing.T) { planOverrides.PolicyClient = policy.NewTestMockClient(t) planCmd := &PlanCommand{ Meta: Meta{ - testingOverrides: planOverrides, - View: planView, - AllowExperimentalFeatures: true, + testingOverrides: planOverrides, + View: planView, }, } if code := planCmd.Run([]string{"-policies", td, "-no-color", "-out=planfile"}); code != 0 { @@ -193,9 +191,8 @@ func TestApply_PolicyResultsJSON_WithSavedPlan(t *testing.T) { applyOverrides.PolicyClient = applyPolicyClient applyCmd := &ApplyCommand{ Meta: Meta{ - testingOverrides: applyOverrides, - View: applyView, - AllowExperimentalFeatures: true, + testingOverrides: applyOverrides, + View: applyView, }, } @@ -314,9 +311,8 @@ func TestApply_WithPolicyClientStopAfterApply(t *testing.T) { overrides.PolicyClient = policyClient c := &ApplyCommand{ Meta: Meta{ - testingOverrides: overrides, - View: view, - AllowExperimentalFeatures: true, + testingOverrides: overrides, + View: view, }, } @@ -357,9 +353,8 @@ func TestApply_WithPlanPolicyDiagnosticsJSON(t *testing.T) { overrides.PolicyClient = policyClient c := &ApplyCommand{ Meta: Meta{ - testingOverrides: overrides, - View: view, - AllowExperimentalFeatures: true, + testingOverrides: overrides, + View: view, }, } p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { diff --git a/internal/command/command_test.go b/internal/command/command_test.go index db29b4ae1c30..eaec10073b48 100644 --- a/internal/command/command_test.go +++ b/internal/command/command_test.go @@ -249,102 +249,6 @@ func testPlanFile(t *testing.T, configSnap *configload.Snapshot, state *states.S return testPlanFileMatchState(t, configSnap, state, plan, statemgr.SnapshotMeta{}) } -func TestPlan_PoliciesRequireExperimentalFeatures(t *testing.T) { - td := testPolicyFixtureDir(t) - - p := planFixtureProvider() - view, done := testView(t) - c := &PlanCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(p), - View: view, - }, - } - - code := c.Run([]string{"-policies", td, "-no-color"}) - output := done(t) - if code != 1 { - t.Fatalf("expected exit code 1, got %d\n\n%s", code, output.All()) - } - if got := output.Stderr(); !strings.Contains(got, "The -policies flag is only valid in experimental builds of Terraform.") { - t.Fatalf("expected policy experiment gating diagnostic, got: %s", got) - } - if strings.Contains(output.All(), "Failed to connect to policy engine") { - t.Fatalf("policy engine should not be initialized when experiments are disabled: %s", output.All()) - } -} - -func TestApply_PoliciesRequireExperimentalFeatures(t *testing.T) { - td := testPolicyFixtureDir(t) - - p := planFixtureProvider() - view, done := testView(t) - c := &ApplyCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(p), - View: view, - }, - } - - code := c.Run([]string{"-policies", td, "-no-color", "-auto-approve"}) - output := done(t) - if code != 1 { - t.Fatalf("expected exit code 1, got %d\n\n%s", code, output.All()) - } - if got := output.Stderr(); !strings.Contains(got, "The -policies flag is only valid in experimental builds of Terraform.") { - t.Fatalf("expected policy experiment gating diagnostic, got: %s", got) - } - if strings.Contains(output.All(), "Failed to connect to policy engine") { - t.Fatalf("policy engine should not be initialized when experiments are disabled: %s", output.All()) - } -} - -func TestInit_PoliciesRequireExperimentalFeatures(t *testing.T) { - td := testPolicyFixtureDir(t) - - view, done := testView(t) - c := &InitCommand{ - Meta: Meta{ - Ui: new(cli.MockUi), - View: view, - }, - } - - code := c.Run([]string{"-policies", td, "-no-color"}) - output := done(t) - if code != 1 { - t.Fatalf("expected exit code 1, got %d\n\n%s", code, output.All()) - } - if got := output.Stderr(); !strings.Contains(got, "The -policies flag is only valid in experimental builds of Terraform.") { - t.Fatalf("expected policy experiment gating diagnostic, got: %s", got) - } -} - -func TestQuery_PoliciesRequireExperimentalFeatures(t *testing.T) { - td := testPolicyFixtureDir(t) - - p := queryFixtureProvider() - view, done := testView(t) - c := &QueryCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(p), - View: view, - }, - } - - code := c.Run([]string{"-policies", td, "-no-color"}) - output := done(t) - if code != 1 { - t.Fatalf("expected exit code 1, got %d\n\n%s", code, output.All()) - } - if got := output.Stderr(); !strings.Contains(got, "The -policies flag is only valid in experimental builds of Terraform.") { - t.Fatalf("expected policy experiment gating diagnostic, got: %s", got) - } - if strings.Contains(output.All(), "Failed to connect to policy engine") { - t.Fatalf("policy engine should not be initialized when experiments are disabled: %s", output.All()) - } -} - func testPlanFileMatchState(t *testing.T, configSnap *configload.Snapshot, state *states.State, plan *plans.Plan, stateMeta statemgr.SnapshotMeta) string { t.Helper() diff --git a/internal/command/init.go b/internal/command/init.go index c56db136cc5f..04abd1a4ab58 100644 --- a/internal/command/init.go +++ b/internal/command/init.go @@ -299,7 +299,7 @@ func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, ini } func (c *InitCommand) Validate(args *arguments.Init) (diags tfdiags.Diagnostics) { - diags = diags.Append(validatePolicyPaths(args.PolicyPaths, c.AllowExperimentalFeatures)) + diags = diags.Append(validatePolicyPaths(args.PolicyPaths)) return diags } diff --git a/internal/command/meta_policy.go b/internal/command/meta_policy.go index ed68af3d2341..6955a7dfdb96 100644 --- a/internal/command/meta_policy.go +++ b/internal/command/meta_policy.go @@ -29,10 +29,6 @@ func (c *Meta) PolicyClient(ctx context.Context, policyPaths []string, ent *poli client.Stop() } } - if !c.AllowExperimentalFeatures { - log.Printf("[DEBUG] Policies are not supported without experiments enabled, skipping policy client setup") - return client, nil, closer - } if len(policyPaths) == 0 { log.Printf("[DEBUG] No policy paths configured, skipping policy client setup") return client, nil, closer diff --git a/internal/command/plan.go b/internal/command/plan.go index 879bc577436d..b9cc37ab1c88 100644 --- a/internal/command/plan.go +++ b/internal/command/plan.go @@ -134,7 +134,7 @@ func (c *PlanCommand) Run(rawArgs []string) int { } func (c *PlanCommand) Validate(args *arguments.Plan) (diags tfdiags.Diagnostics) { - return diags.Append(validatePolicyPaths(args.PolicyPaths, c.AllowExperimentalFeatures)) + return diags.Append(validatePolicyPaths(args.PolicyPaths)) } func (c *PlanCommand) PrepareBackend(args *arguments.State, viewType arguments.ViewType) (backendrun.OperationsBackend, tfdiags.Diagnostics) { diff --git a/internal/command/plan_policy_test.go b/internal/command/plan_policy_test.go index c34f55aee165..a140c456ec8e 100644 --- a/internal/command/plan_policy_test.go +++ b/internal/command/plan_policy_test.go @@ -40,9 +40,8 @@ func TestPlan_WithPolicy(t *testing.T) { overrides.PolicyClient = policyClient c := &PlanCommand{ Meta: Meta{ - testingOverrides: overrides, - View: view, - AllowExperimentalFeatures: true, + testingOverrides: overrides, + View: view, }, } resp := policy.EvaluationFromProtoResponse( @@ -158,9 +157,8 @@ func TestPlan_WithPolicyDiagnosticsJSON(t *testing.T) { overrides.PolicyClient = policyClient c := &PlanCommand{ Meta: Meta{ - testingOverrides: overrides, - View: view, - AllowExperimentalFeatures: true, + testingOverrides: overrides, + View: view, }, } resp := policy.EvaluationFromProtoResponse( @@ -355,9 +353,8 @@ func TestPlan_WithPolicyUnknown(t *testing.T) { overrides.PolicyClient = policyClient c := &PlanCommand{ Meta: Meta{ - testingOverrides: overrides, - View: view, - AllowExperimentalFeatures: true, + testingOverrides: overrides, + View: view, }, } @@ -482,10 +479,9 @@ func TestPlan_WithPolicySuccessInfo(t *testing.T) { policyClient := policy.NewTestMockClient(t) overrides.PolicyClient = policyClient meta := Meta{ - testingOverrides: overrides, - View: view, - ProviderSource: providerSource, - AllowExperimentalFeatures: true, + testingOverrides: overrides, + View: view, + ProviderSource: providerSource, } init := &InitCommand{ @@ -674,9 +670,8 @@ func TestPlan_WithPolicySuccessInfoJSON(t *testing.T) { overrides.PolicyClient = policyClient c := &PlanCommand{ Meta: Meta{ - testingOverrides: overrides, - View: view, - AllowExperimentalFeatures: true, + testingOverrides: overrides, + View: view, }, } @@ -859,9 +854,8 @@ func TestPlan_Policy_Destroy(t *testing.T) { c := &PlanCommand{ Meta: Meta{ - testingOverrides: overrides, - View: view, - AllowExperimentalFeatures: true, + testingOverrides: overrides, + View: view, }, } @@ -948,9 +942,8 @@ func TestPlan_WithPolicyClientStopAfterPlan(t *testing.T) { overrides.PolicyClient = policyClient c := &PlanCommand{ Meta: Meta{ - testingOverrides: overrides, - View: view, - AllowExperimentalFeatures: true, + testingOverrides: overrides, + View: view, }, } @@ -991,9 +984,8 @@ func TestPlan_WithPolicySetupFailure(t *testing.T) { // diagnostics from attempting to connect to the policy engine. c := &PlanCommand{ Meta: Meta{ - testingOverrides: overrides, - View: view, - AllowExperimentalFeatures: true, + testingOverrides: overrides, + View: view, }, } @@ -1073,9 +1065,8 @@ func TestPlan_WithPolicySetupFailureJSON(t *testing.T) { // diagnostics from attempting to connect to the policy engine. c := &PlanCommand{ Meta: Meta{ - testingOverrides: overrides, - View: view, - AllowExperimentalFeatures: true, + testingOverrides: overrides, + View: view, }, } diff --git a/internal/command/policy.go b/internal/command/policy.go index d626d469c17e..78eeb3f239a3 100644 --- a/internal/command/policy.go +++ b/internal/command/policy.go @@ -10,15 +10,7 @@ import ( "github.com/hashicorp/terraform/internal/tfdiags" ) -func validatePolicyPaths(policyPaths []string, experimental bool) (diags tfdiags.Diagnostics) { - if !experimental && len(policyPaths) > 0 { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Failed to parse command-line flags", - "The -policies flag is only valid in experimental builds of Terraform.", - )) - } - +func validatePolicyPaths(policyPaths []string) (diags tfdiags.Diagnostics) { for _, path := range policyPaths { if _, err := os.Stat(path); err != nil { if os.IsNotExist(err) { diff --git a/internal/command/policy_test.go b/internal/command/policy_test.go index cb5b0697f3e6..3b2310b81af8 100644 --- a/internal/command/policy_test.go +++ b/internal/command/policy_test.go @@ -44,18 +44,6 @@ func TestValidatePolicyPaths(t *testing.T) { ), }, }, - { - name: "existing path, experiments disallowed", - path: existingPath, - allowExperiments: false, - want: tfdiags.Diagnostics{ - tfdiags.Sourceless( - tfdiags.Error, - "Failed to parse command-line flags", - "The -policies flag is only valid in experimental builds of Terraform.", - ), - }, - }, } for _, tc := range tests { diff --git a/internal/command/query.go b/internal/command/query.go index 27364e9c9161..d79fe469fbe9 100644 --- a/internal/command/query.go +++ b/internal/command/query.go @@ -170,8 +170,7 @@ func (c *QueryCommand) Run(rawArgs []string) int { } func (c *QueryCommand) Validate(args *arguments.Query) (diags tfdiags.Diagnostics) { - // validatePolicyPaths call ejects early if -policies flag was passed for non-experimental builds - return diags.Append(validatePolicyPaths(args.PolicyPaths, c.AllowExperimentalFeatures)) + return diags.Append(validatePolicyPaths(args.PolicyPaths)) } func (c *QueryCommand) PrepareBackend(args *arguments.State, viewType arguments.ViewType) (backendrun.OperationsBackend, tfdiags.Diagnostics) { diff --git a/internal/command/query_test.go b/internal/command/query_test.go index 3bc3ca6d7b66..4c4f0488f5d0 100644 --- a/internal/command/query_test.go +++ b/internal/command/query_test.go @@ -341,18 +341,6 @@ func TestQueryCommand_Validate(t *testing.T) { ), }, }, - { - name: "experiments disallowed", - policyPaths: []string{td}, - allowExperiments: false, - wantDiags: tfdiags.Diagnostics{ - tfdiags.Sourceless( - tfdiags.Error, - "Failed to parse command-line flags", - "The -policies flag is only valid in experimental builds of Terraform.", - ), - }, - }, } for _, tc := range tests {