diff --git a/internal/command/arguments/validate.go b/internal/command/arguments/validate.go index 49d00ce692..5140965281 100644 --- a/internal/command/arguments/validate.go +++ b/internal/command/arguments/validate.go @@ -26,6 +26,8 @@ type Validate struct { // ViewType specifies which output format to use: human, JSON, or "raw". ViewType ViewType + + Vars *Vars } // ParseValidate processes CLI arguments, returning a Validate value and errors. @@ -35,10 +37,11 @@ func ParseValidate(args []string) (*Validate, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics validate := &Validate{ Path: ".", + Vars: &Vars{}, } var jsonOutput bool - cmdFlags := defaultFlagSet("validate") + cmdFlags := extendedFlagSet("validate", nil, nil, validate.Vars) cmdFlags.BoolVar(&jsonOutput, "json", false, "json") cmdFlags.StringVar(&validate.TestDirectory, "test-directory", "tests", "test-directory") cmdFlags.BoolVar(&validate.NoTests, "no-tests", false, "no-tests") diff --git a/internal/command/arguments/validate_test.go b/internal/command/arguments/validate_test.go index e016100fd5..6134f1b3e0 100644 --- a/internal/command/arguments/validate_test.go +++ b/internal/command/arguments/validate_test.go @@ -68,6 +68,7 @@ func TestParseValidate_valid(t *testing.T) { if len(diags) > 0 { t.Fatalf("unexpected diags: %v", diags) } + got.Vars = nil if *got != *tc.want { t.Fatalf("unexpected result\n got: %#v\nwant: %#v", got, tc.want) } @@ -116,6 +117,7 @@ func TestParseValidate_invalid(t *testing.T) { for name, tc := range testCases { t.Run(name, func(t *testing.T) { got, gotDiags := ParseValidate(tc.args) + got.Vars = nil if *got != *tc.want { t.Fatalf("unexpected result\n got: %#v\nwant: %#v", got, tc.want) } diff --git a/internal/command/e2etest/encryption_test.go b/internal/command/e2etest/encryption_test.go index 72fdf4f047..d7a9ecff6b 100644 --- a/internal/command/e2etest/encryption_test.go +++ b/internal/command/e2etest/encryption_test.go @@ -53,6 +53,14 @@ func (r tofuResult) StderrContains(msg string) tofuResult { return r } +func (r tofuResult) Contains(msg string) tofuResult { + if !strings.Contains(r.stdout, msg) { + debug.PrintStack() + r.t.Fatalf("expected output %q:\n%s", msg, r.stdout) + } + return r +} + // This test covers the scenario where a user migrates an existing project // to having encryption enabled, uses it, then migrates back to encryption // disabled diff --git a/internal/command/e2etest/static_plan_test.go b/internal/command/e2etest/static_plan_test.go index 09be28ebb7..4a6753d457 100644 --- a/internal/command/e2etest/static_plan_test.go +++ b/internal/command/e2etest/static_plan_test.go @@ -28,18 +28,104 @@ func TestStaticPlanVariables(t *testing.T) { modVar := "-var=src=./mod" planfile := "static.plan" - // Init without static variable - run("init").Failure() + modErr := "module.mod.source depends on var.src which is not available" + backendErr := "backend.local depends on var.state_path which is not available" - // Init with static variable + // Init + run("init").Failure().StderrContains(modErr) run("init", stateVar, modVar).Success() + // Get + run("get").Failure().StderrContains(modErr) + run("get", stateVar, modVar).Success() + + // Validate + run("validate").Failure().StderrContains(modErr) + run("validate", stateVar, modVar).Success() + + // Providers + run("providers").Failure().StderrContains(modErr) + run("providers", stateVar, modVar).Success() + run("providers", "lock").Failure().StderrContains(modErr) + run("providers", "lock", stateVar, modVar).Success() + run("providers", "mirror", "./tempproviders").Failure().StderrContains(modErr) + run("providers", "mirror", stateVar, modVar, "./tempproviders").Failure().StderrContains("Could not scan the output directory to get package metadata for the JSON") + run("providers", "schema", "-json").Failure().StderrContains(backendErr) + run("providers", "schema", "-json", stateVar, modVar).Success() + + // Check console init (early exits due to stdin setup) + run("console").Failure().StderrContains(backendErr) + run("console", stateVar, modVar).Success() + + // Check graph (without plan) + run("graph").Failure().StderrContains(backendErr) + run("graph", stateVar, modVar).Success() + // Plan with static variable run("plan", stateVar, modVar, "-out="+planfile).Success() // Show plan without static variable (embedded) run("show", planfile).Success() + // Check graph (without plan) + run("graph", "-plan="+planfile).Success() + // Apply plan without static variable (embedded) run("apply", planfile).Success() + + // Force Unlock + run("force-unlock", "ident").Failure().StderrContains(backendErr) + run("force-unlock", stateVar, modVar, "ident").Failure().StderrContains("Local state cannot be unlocked by another process") + + // Output values + run("output").Failure().StderrContains(backendErr) + run("output", stateVar, modVar).Success().Contains(`out = "placeholder"`) + + // Refresh + run("refresh").Failure().StderrContains(backendErr) + run("refresh", stateVar, modVar).Success().Contains("There are currently no remote objects tracked in the state") + + // Import + run("import", "resource.addr", "id").Failure().StderrContains(modErr) + run("import", stateVar, modVar, "resource.addr", "id").Failure().StderrContains("Before importing this resource, please create its configuration in the root module.") + + // Taint + run("taint", "resource.addr").Failure().StderrContains(modErr) + run("taint", stateVar, modVar, "resource.addr").Failure().StderrContains("There is no resource instance in the state with the address resource.addr.") + run("untaint", "resource.addr").Failure().StderrContains(backendErr) + run("untaint", stateVar, modVar, "resource.addr").Failure().StderrContains("There is no resource instance in the state with the address resource.addr.") + + // State + run("state", "list").Failure().StderrContains(backendErr) + run("state", "list", stateVar, modVar).Success() + run("state", "mv", "foo.bar", "foo.baz").Failure().StderrContains(modErr) + run("state", "mv", stateVar, modVar, "foo.bar", "foo.baz").Failure().StderrContains("Cannot move foo.bar: does not match anything in the current state.") + run("state", "pull").Failure().StderrContains(modErr) + run("state", "pull", stateVar, modVar).Success().Contains(`"outputs":{"out":{"value":"placeholder","type":"string"}}`) + run("state", "push", statePath).Failure().StderrContains(modErr) + run("state", "push", stateVar, modVar, statePath).Success() + run("state", "replace-provider", "foo", "bar").Failure().StderrContains(modErr) + run("state", "replace-provider", stateVar, modVar, "foo", "bar").Success().Contains("No matching resources found.") + run("state", "rm", "foo.bar").Failure().StderrContains(modErr) + run("state", "rm", stateVar, modVar, "foo.bar").Failure().StderrContains("No matching objects found.") + run("state", "show", "out").Failure().StderrContains(backendErr) + run("state", "show", stateVar, modVar, "invalid.resource").Failure().StderrContains("No instance found for the given address!") + + // Workspace + run("workspace", "list").Failure().StderrContains(backendErr) + run("workspace", "list", stateVar, modVar).Success().Contains(`default`) + run("workspace", "new", "foo").Failure().StderrContains(backendErr) + run("workspace", "new", stateVar, modVar, "foo").Success().Contains(`foo`) + run("workspace", "select", "default").Failure().StderrContains(backendErr) + run("workspace", "select", stateVar, modVar, "default").Success().Contains(`default`) + run("workspace", "delete", "foo").Failure().StderrContains(backendErr) + run("workspace", "delete", stateVar, modVar, "foo").Success().Contains(`foo`) + + // Test + run("test").Failure().StderrContains(modErr) + run("test", stateVar, modVar).Success().Contains(`Success!`) + + // Destroy + run("destroy", "-auto-approve").Failure().StderrContains(backendErr) + run("destroy", stateVar, modVar, "-auto-approve").Success().Contains("You can apply this plan to save these new output values") } diff --git a/internal/command/get.go b/internal/command/get.go index b3799a5450..d1259a2ce0 100644 --- a/internal/command/get.go +++ b/internal/command/get.go @@ -26,6 +26,7 @@ func (c *GetCommand) Run(args []string) int { args = c.Meta.process(args) cmdFlags := c.Meta.defaultFlagSet("get") + c.Meta.varFlagSet(cmdFlags) cmdFlags.BoolVar(&update, "update", false, "update") cmdFlags.StringVar(&testsDirectory, "test-directory", "tests", "test-directory") cmdFlags.BoolVar(&c.outputInJSON, "json", false, "json") diff --git a/internal/command/graph.go b/internal/command/graph.go index 3e09bb173c..3cad4ec6fc 100644 --- a/internal/command/graph.go +++ b/internal/command/graph.go @@ -77,21 +77,53 @@ func (c *GraphCommand) Run(args []string) int { } } - backendConfig, backendDiags := c.loadBackendConfig(configPath) - diags = diags.Append(backendDiags) - if diags.HasErrors() { - c.showDiagnostics(diags) - return 1 - } - // Load the backend - b, backendDiags := c.Backend(&BackendOpts{ - Config: backendConfig, - }, enc.State()) - diags = diags.Append(backendDiags) - if backendDiags.HasErrors() { - c.showDiagnostics(diags) - return 1 + var b backend.Enhanced + //nolint: nestif // This is inspired by apply:PrepareBackend + if lp, ok := planFile.Local(); ok { + plan, planErr := lp.ReadPlan() + if planErr != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to read plan from plan file", + fmt.Sprintf("Cannot read the plan from the given plan file: %s.", planErr), + )) + c.showDiagnostics(diags) + return 1 + } + if plan.Backend.Config == nil { + // Should never happen; always indicates a bug in the creation of the plan file + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to read plan from plan file", + "The given plan file does not have a valid backend configuration. This is a bug in the OpenTofu command that generated this plan file.", + )) + c.showDiagnostics(diags) + return 1 + } + var backendDiags tfdiags.Diagnostics + b, backendDiags = c.BackendForLocalPlan(plan.Backend, enc.State()) + diags = diags.Append(backendDiags) + if backendDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + } else { + backendConfig, backendDiags := c.loadBackendConfig(configPath) + diags = diags.Append(backendDiags) + if diags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + + b, backendDiags = c.Backend(&BackendOpts{ + Config: backendConfig, + }, enc.State()) + diags = diags.Append(backendDiags) + if backendDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } } // We require a local backend @@ -111,6 +143,16 @@ func (c *GraphCommand) Run(args []string) int { opReq.ConfigLoader, err = c.initConfigLoader() opReq.PlanFile = planFile opReq.AllowUnsetVariables = true + + // Inject information required for static evaluation + var callDiags tfdiags.Diagnostics + opReq.RootCall, callDiags = c.rootModuleCall(opReq.ConfigDir) + diags = diags.Append(callDiags) + if callDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + if err != nil { diags = diags.Append(err) c.showDiagnostics(diags) diff --git a/internal/command/graph_test.go b/internal/command/graph_test.go index 6fe38012bf..6f4b1d442d 100644 --- a/internal/command/graph_test.go +++ b/internal/command/graph_test.go @@ -127,15 +127,16 @@ func TestGraph_plan(t *testing.T) { Module: addrs.RootModule, }, }) - emptyConfig, err := plans.NewDynamicValue(cty.EmptyObjectVal, cty.EmptyObject) + beConfig := cty.ObjectVal(map[string]cty.Value{ + "path": cty.NilVal, + "workspace_dir": cty.NilVal, + }) + emptyConfig, err := plans.NewDynamicValue(beConfig, beConfig.Type()) if err != nil { t.Fatal(err) } plan.Backend = plans.Backend{ - // Doesn't actually matter since we aren't going to activate the backend - // for this command anyway, but we need something here for the plan - // file writer to succeed. - Type: "placeholder", + Type: "local", Config: emptyConfig, } _, configSnap := testModuleWithSnapshot(t, "graph") diff --git a/internal/command/providers_schema.go b/internal/command/providers_schema.go index ede52a0c07..776a3999f1 100644 --- a/internal/command/providers_schema.go +++ b/internal/command/providers_schema.go @@ -95,6 +95,14 @@ func (c *ProvidersSchemaCommand) Run(args []string) int { opReq := c.Operation(b, arguments.ViewJSON, enc) opReq.ConfigDir = cwd opReq.ConfigLoader, err = c.initConfigLoader() + var callDiags tfdiags.Diagnostics + opReq.RootCall, callDiags = c.rootModuleCall(opReq.ConfigDir) + diags = diags.Append(callDiags) + if callDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + opReq.AllowUnsetVariables = true if err != nil { diags = diags.Append(err) diff --git a/internal/command/state_show.go b/internal/command/state_show.go index 747f6aa855..5c466eb5bc 100644 --- a/internal/command/state_show.go +++ b/internal/command/state_show.go @@ -20,6 +20,7 @@ import ( "github.com/opentofu/opentofu/internal/command/jsonstate" "github.com/opentofu/opentofu/internal/states" "github.com/opentofu/opentofu/internal/states/statefile" + "github.com/opentofu/opentofu/internal/tfdiags" "github.com/opentofu/opentofu/internal/tofumigrate" ) @@ -93,6 +94,12 @@ func (c *StateShowCommand) Run(args []string) int { opReq := c.Operation(b, arguments.ViewHuman, enc) opReq.AllowUnsetVariables = true opReq.ConfigDir = cwd + var callDiags tfdiags.Diagnostics + opReq.RootCall, callDiags = c.rootModuleCall(opReq.ConfigDir) + if callDiags.HasErrors() { + c.showDiagnostics(callDiags) + return 1 + } opReq.ConfigLoader, err = c.initConfigLoader() if err != nil { diff --git a/internal/command/validate.go b/internal/command/validate.go index 887202af32..5159bbb77e 100644 --- a/internal/command/validate.go +++ b/internal/command/validate.go @@ -56,6 +56,9 @@ func (c *ValidateCommand) Run(rawArgs []string) int { return view.Results(diags) } + // Inject variables from args into meta for static evaluation + c.GatherVariables(args.Vars) + validateDiags := c.validate(dir, args.TestDirectory, args.NoTests) diags = diags.Append(validateDiags) @@ -68,6 +71,23 @@ func (c *ValidateCommand) Run(rawArgs []string) int { return view.Results(diags) } +func (c *ValidateCommand) GatherVariables(args *arguments.Vars) { + // FIXME the arguments package currently trivially gathers variable related + // arguments in a heterogenous slice, in order to minimize the number of + // code paths gathering variables during the transition to this structure. + // Once all commands that gather variables have been converted to this + // structure, we could move the variable gathering code to the arguments + // package directly, removing this shim layer. + + varArgs := args.All() + items := make([]rawFlag, len(varArgs)) + for i := range varArgs { + items[i].Name = varArgs[i].Name + items[i].Value = varArgs[i].Value + } + c.Meta.variableArgs = rawFlags{items: &items} +} + func (c *ValidateCommand) validate(dir, testDir string, noTests bool) tfdiags.Diagnostics { var diags tfdiags.Diagnostics var cfg *configs.Config