Fix tofu validate with static variables (#1788)

Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
This commit is contained in:
Christian Mesh 2024-07-11 10:16:20 -04:00 committed by GitHub
parent 5f1509d8c7
commit 0a53bab15d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 201 additions and 23 deletions

View File

@ -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")

View File

@ -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)
}

View File

@ -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

View File

@ -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")
}

View File

@ -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")

View File

@ -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)

View File

@ -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")

View File

@ -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)

View File

@ -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 {

View File

@ -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