mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
Fix tofu validate
with static variables (#1788)
Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
This commit is contained in:
parent
5f1509d8c7
commit
0a53bab15d
@ -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")
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user