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 specifies which output format to use: human, JSON, or "raw".
|
||||||
ViewType ViewType
|
ViewType ViewType
|
||||||
|
|
||||||
|
Vars *Vars
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseValidate processes CLI arguments, returning a Validate value and errors.
|
// 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
|
var diags tfdiags.Diagnostics
|
||||||
validate := &Validate{
|
validate := &Validate{
|
||||||
Path: ".",
|
Path: ".",
|
||||||
|
Vars: &Vars{},
|
||||||
}
|
}
|
||||||
|
|
||||||
var jsonOutput bool
|
var jsonOutput bool
|
||||||
cmdFlags := defaultFlagSet("validate")
|
cmdFlags := extendedFlagSet("validate", nil, nil, validate.Vars)
|
||||||
cmdFlags.BoolVar(&jsonOutput, "json", false, "json")
|
cmdFlags.BoolVar(&jsonOutput, "json", false, "json")
|
||||||
cmdFlags.StringVar(&validate.TestDirectory, "test-directory", "tests", "test-directory")
|
cmdFlags.StringVar(&validate.TestDirectory, "test-directory", "tests", "test-directory")
|
||||||
cmdFlags.BoolVar(&validate.NoTests, "no-tests", false, "no-tests")
|
cmdFlags.BoolVar(&validate.NoTests, "no-tests", false, "no-tests")
|
||||||
|
@ -68,6 +68,7 @@ func TestParseValidate_valid(t *testing.T) {
|
|||||||
if len(diags) > 0 {
|
if len(diags) > 0 {
|
||||||
t.Fatalf("unexpected diags: %v", diags)
|
t.Fatalf("unexpected diags: %v", diags)
|
||||||
}
|
}
|
||||||
|
got.Vars = nil
|
||||||
if *got != *tc.want {
|
if *got != *tc.want {
|
||||||
t.Fatalf("unexpected result\n got: %#v\nwant: %#v", 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 {
|
for name, tc := range testCases {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
got, gotDiags := ParseValidate(tc.args)
|
got, gotDiags := ParseValidate(tc.args)
|
||||||
|
got.Vars = nil
|
||||||
if *got != *tc.want {
|
if *got != *tc.want {
|
||||||
t.Fatalf("unexpected result\n got: %#v\nwant: %#v", 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
|
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
|
// This test covers the scenario where a user migrates an existing project
|
||||||
// to having encryption enabled, uses it, then migrates back to encryption
|
// to having encryption enabled, uses it, then migrates back to encryption
|
||||||
// disabled
|
// disabled
|
||||||
|
@ -28,18 +28,104 @@ func TestStaticPlanVariables(t *testing.T) {
|
|||||||
modVar := "-var=src=./mod"
|
modVar := "-var=src=./mod"
|
||||||
planfile := "static.plan"
|
planfile := "static.plan"
|
||||||
|
|
||||||
// Init without static variable
|
modErr := "module.mod.source depends on var.src which is not available"
|
||||||
run("init").Failure()
|
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()
|
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
|
// Plan with static variable
|
||||||
run("plan", stateVar, modVar, "-out="+planfile).Success()
|
run("plan", stateVar, modVar, "-out="+planfile).Success()
|
||||||
|
|
||||||
// Show plan without static variable (embedded)
|
// Show plan without static variable (embedded)
|
||||||
run("show", planfile).Success()
|
run("show", planfile).Success()
|
||||||
|
|
||||||
|
// Check graph (without plan)
|
||||||
|
run("graph", "-plan="+planfile).Success()
|
||||||
|
|
||||||
// Apply plan without static variable (embedded)
|
// Apply plan without static variable (embedded)
|
||||||
run("apply", planfile).Success()
|
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)
|
args = c.Meta.process(args)
|
||||||
cmdFlags := c.Meta.defaultFlagSet("get")
|
cmdFlags := c.Meta.defaultFlagSet("get")
|
||||||
|
c.Meta.varFlagSet(cmdFlags)
|
||||||
cmdFlags.BoolVar(&update, "update", false, "update")
|
cmdFlags.BoolVar(&update, "update", false, "update")
|
||||||
cmdFlags.StringVar(&testsDirectory, "test-directory", "tests", "test-directory")
|
cmdFlags.StringVar(&testsDirectory, "test-directory", "tests", "test-directory")
|
||||||
cmdFlags.BoolVar(&c.outputInJSON, "json", false, "json")
|
cmdFlags.BoolVar(&c.outputInJSON, "json", false, "json")
|
||||||
|
@ -77,6 +77,38 @@ func (c *GraphCommand) Run(args []string) int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load the backend
|
||||||
|
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)
|
backendConfig, backendDiags := c.loadBackendConfig(configPath)
|
||||||
diags = diags.Append(backendDiags)
|
diags = diags.Append(backendDiags)
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
@ -84,8 +116,7 @@ func (c *GraphCommand) Run(args []string) int {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the backend
|
b, backendDiags = c.Backend(&BackendOpts{
|
||||||
b, backendDiags := c.Backend(&BackendOpts{
|
|
||||||
Config: backendConfig,
|
Config: backendConfig,
|
||||||
}, enc.State())
|
}, enc.State())
|
||||||
diags = diags.Append(backendDiags)
|
diags = diags.Append(backendDiags)
|
||||||
@ -93,6 +124,7 @@ func (c *GraphCommand) Run(args []string) int {
|
|||||||
c.showDiagnostics(diags)
|
c.showDiagnostics(diags)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// We require a local backend
|
// We require a local backend
|
||||||
local, ok := b.(backend.Local)
|
local, ok := b.(backend.Local)
|
||||||
@ -111,6 +143,16 @@ func (c *GraphCommand) Run(args []string) int {
|
|||||||
opReq.ConfigLoader, err = c.initConfigLoader()
|
opReq.ConfigLoader, err = c.initConfigLoader()
|
||||||
opReq.PlanFile = planFile
|
opReq.PlanFile = planFile
|
||||||
opReq.AllowUnsetVariables = true
|
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 {
|
if err != nil {
|
||||||
diags = diags.Append(err)
|
diags = diags.Append(err)
|
||||||
c.showDiagnostics(diags)
|
c.showDiagnostics(diags)
|
||||||
|
@ -127,15 +127,16 @@ func TestGraph_plan(t *testing.T) {
|
|||||||
Module: addrs.RootModule,
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
plan.Backend = plans.Backend{
|
plan.Backend = plans.Backend{
|
||||||
// Doesn't actually matter since we aren't going to activate the backend
|
Type: "local",
|
||||||
// for this command anyway, but we need something here for the plan
|
|
||||||
// file writer to succeed.
|
|
||||||
Type: "placeholder",
|
|
||||||
Config: emptyConfig,
|
Config: emptyConfig,
|
||||||
}
|
}
|
||||||
_, configSnap := testModuleWithSnapshot(t, "graph")
|
_, configSnap := testModuleWithSnapshot(t, "graph")
|
||||||
|
@ -95,6 +95,14 @@ func (c *ProvidersSchemaCommand) Run(args []string) int {
|
|||||||
opReq := c.Operation(b, arguments.ViewJSON, enc)
|
opReq := c.Operation(b, arguments.ViewJSON, enc)
|
||||||
opReq.ConfigDir = cwd
|
opReq.ConfigDir = cwd
|
||||||
opReq.ConfigLoader, err = c.initConfigLoader()
|
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
|
opReq.AllowUnsetVariables = true
|
||||||
if err != nil {
|
if err != nil {
|
||||||
diags = diags.Append(err)
|
diags = diags.Append(err)
|
||||||
|
@ -20,6 +20,7 @@ import (
|
|||||||
"github.com/opentofu/opentofu/internal/command/jsonstate"
|
"github.com/opentofu/opentofu/internal/command/jsonstate"
|
||||||
"github.com/opentofu/opentofu/internal/states"
|
"github.com/opentofu/opentofu/internal/states"
|
||||||
"github.com/opentofu/opentofu/internal/states/statefile"
|
"github.com/opentofu/opentofu/internal/states/statefile"
|
||||||
|
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||||
"github.com/opentofu/opentofu/internal/tofumigrate"
|
"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 := c.Operation(b, arguments.ViewHuman, enc)
|
||||||
opReq.AllowUnsetVariables = true
|
opReq.AllowUnsetVariables = true
|
||||||
opReq.ConfigDir = cwd
|
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()
|
opReq.ConfigLoader, err = c.initConfigLoader()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -56,6 +56,9 @@ func (c *ValidateCommand) Run(rawArgs []string) int {
|
|||||||
return view.Results(diags)
|
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)
|
validateDiags := c.validate(dir, args.TestDirectory, args.NoTests)
|
||||||
diags = diags.Append(validateDiags)
|
diags = diags.Append(validateDiags)
|
||||||
|
|
||||||
@ -68,6 +71,23 @@ func (c *ValidateCommand) Run(rawArgs []string) int {
|
|||||||
return view.Results(diags)
|
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 {
|
func (c *ValidateCommand) validate(dir, testDir string, noTests bool) tfdiags.Diagnostics {
|
||||||
var diags tfdiags.Diagnostics
|
var diags tfdiags.Diagnostics
|
||||||
var cfg *configs.Config
|
var cfg *configs.Config
|
||||||
|
Loading…
Reference in New Issue
Block a user