command: Fix backend config override validation

When loading a backend config override file, init was doing two things
wrong:

- First, if the file failed to parse, we accidentally didn't return,
  which caused a panic due to the parsed body being nil;
- Secondly, we were overzealous with the validation of the file,
  allowing only attributes. While most backend configs are attributes
  only, the enhanced remote backend body also contains a `workspaces`
  block, which we need to support here.

This commit fixes the first bug with an early return and adds test cases
for missing file and intentionally-blank filename (to clear the config).

We also add a schema validation for the backend block, based on the
backend schema itself. This requires constructing an HCL body schema so
that we can call `Content` and check for diagnostic errors.

The result is more useful errors when an invalid backend config override
file is used, while also supporting the enhanced remote backend config
fully.

Does not include tests specific to the remote backend, because the
mocking involved to allow the backend to fully initialize is too
involved to be worth it.
This commit is contained in:
Alisdair McDiarmid 2020-08-21 16:10:06 -04:00
parent e63093edc8
commit 677aabc767
4 changed files with 88 additions and 14 deletions

View File

@ -856,16 +856,31 @@ func (c *InitCommand) backendConfigOverrideBody(flags rawFlags, schema *configsc
// The value is interpreted as a filename. // The value is interpreted as a filename.
newBody, fileDiags := c.loadHCLFile(item.Value) newBody, fileDiags := c.loadHCLFile(item.Value)
diags = diags.Append(fileDiags) diags = diags.Append(fileDiags)
// Verify that the file contains only key-values pairs, and not a if fileDiags.HasErrors() {
// full backend config block. JustAttributes() will return an error continue
// if blocks are found }
_, attrDiags := newBody.JustAttributes() // Generate an HCL body schema for the backend block.
if attrDiags.HasErrors() { var bodySchema hcl.BodySchema
diags = diags.Append(tfdiags.Sourceless( for name, attr := range schema.Attributes {
tfdiags.Error, bodySchema.Attributes = append(bodySchema.Attributes, hcl.AttributeSchema{
"Invalid backend configuration file", Name: name,
fmt.Sprintf("The backend configuration file %q given on the command line must contain key-value pairs only, and not configuration blocks.", item.Value), Required: attr.Required,
)) })
}
for name, block := range schema.BlockTypes {
var labelNames []string
if block.Nesting == configschema.NestingMap {
labelNames = append(labelNames, "key")
}
bodySchema.Blocks = append(bodySchema.Blocks, hcl.BlockHeaderSchema{
Type: name,
LabelNames: labelNames,
})
}
// Verify that the file body matches the expected backend schema.
_, schemaDiags := newBody.Content(&bodySchema)
diags = diags.Append(schemaDiags)
if schemaDiags.HasErrors() {
continue continue
} }
flushVals() // deal with any accumulated individual values first flushVals() // deal with any accumulated individual values first

View File

@ -355,8 +355,8 @@ func TestInit_backendConfigFile(t *testing.T) {
} }
}) })
// the backend config file must be a set of key-value pairs and not a full backend {} block // the backend config file must not be a full terraform block
t.Run("invalid-config-file", func(t *testing.T) { t.Run("full-backend-config-file", func(t *testing.T) {
ui := new(cli.MockUi) ui := new(cli.MockUi)
c := &InitCommand{ c := &InitCommand{
Meta: Meta{ Meta: Meta{
@ -368,10 +368,67 @@ func TestInit_backendConfigFile(t *testing.T) {
if code := c.Run(args); code != 1 { if code := c.Run(args); code != 1 {
t.Fatalf("expected error, got success\n") t.Fatalf("expected error, got success\n")
} }
if !strings.Contains(ui.ErrorWriter.String(), "Invalid backend configuration file") { if !strings.Contains(ui.ErrorWriter.String(), "Unsupported block type") {
t.Fatalf("wrong error: %s", ui.ErrorWriter) t.Fatalf("wrong error: %s", ui.ErrorWriter)
} }
}) })
// the backend config file must match the schema for the backend
t.Run("invalid-config-file", func(t *testing.T) {
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
args := []string{"-backend-config", "invalid.config"}
if code := c.Run(args); code != 1 {
t.Fatalf("expected error, got success\n")
}
if !strings.Contains(ui.ErrorWriter.String(), "Unsupported argument") {
t.Fatalf("wrong error: %s", ui.ErrorWriter)
}
})
// missing file is an error
t.Run("missing-config-file", func(t *testing.T) {
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
args := []string{"-backend-config", "missing.config"}
if code := c.Run(args); code != 1 {
t.Fatalf("expected error, got success\n")
}
if !strings.Contains(ui.ErrorWriter.String(), "Failed to read file") {
t.Fatalf("wrong error: %s", ui.ErrorWriter)
}
})
// blank filename clears the backend config
t.Run("blank-config-file", func(t *testing.T) {
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
args := []string{"-backend-config="}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
// Read our saved backend config and verify the backend config is empty
state := testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename))
if got, want := normalizeJSON(t, state.Backend.ConfigRaw), `{"path":null,"workspace_dir":null}`; got != want {
t.Errorf("wrong config\ngot: %s\nwant: %s", got, want)
}
})
} }
func TestInit_backendConfigFilePowershellConfusion(t *testing.T) { func TestInit_backendConfigFilePowershellConfusion(t *testing.T) {

View File

@ -1,5 +1,5 @@
// the -backend-config flag on init cannot be used to point to a "full" backend // the -backend-config flag on init cannot be used to point to a "full" backend
// block, only key-value pairs (like terraform.tfvars) // block
terraform { terraform {
backend "local" { backend "local" {
path = "hello" path = "hello"

View File

@ -0,0 +1,2 @@
path = "hello"
foo = "bar"