Merge pull request #29235 from magodo/terraform_add_output_append

`terraform add`: `-out` option append to existing config & optionally check resource existance
This commit is contained in:
Alisdair McDiarmid 2021-09-17 11:19:55 -04:00 committed by GitHub
commit 1f57b7a8bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 112 additions and 17 deletions

View File

@ -3,6 +3,7 @@ package command
import ( import (
"fmt" "fmt"
"os" "os"
"path/filepath"
"strings" "strings"
"github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2"
@ -33,6 +34,43 @@ func (c *AddCommand) Run(rawArgs []string) int {
return 1 return 1
} }
// In case the output configuration path is specified, we should ensure the
// target resource address doesn't exist in the module tree indicated by
// the existing configuration files.
if args.OutPath != "" {
// Ensure the directory to the path exists and is accessible.
outDir := filepath.Dir(args.OutPath)
if _, err := os.Stat(outDir); os.IsNotExist(err) {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"The out path doesn't exist or is not accessible",
err.Error(),
))
view.Diagnostics(diags)
return 1
}
config, loadDiags := c.loadConfig(outDir)
diags = diags.Append(loadDiags)
if diags.HasErrors() {
view.Diagnostics(diags)
return 1
}
if config != nil && config.Module != nil {
if rs, ok := config.Module.ManagedResources[args.Addr.ContainingResource().Config().String()]; ok {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Resource already in configuration",
Detail: fmt.Sprintf("The resource %s is already in this configuration at %s. Resource names must be unique per type in each module.", args.Addr, rs.DeclRange),
Subject: &rs.DeclRange,
})
c.View.Diagnostics(diags)
return 1
}
}
}
// Check for user-supplied plugin path // Check for user-supplied plugin path
var err error var err error
if c.pluginPath, err = c.loadPluginPath(); err != nil { if c.pluginPath, err = c.loadPluginPath(); err != nil {
@ -127,21 +165,6 @@ func (c *AddCommand) Run(rawArgs []string) int {
} }
} }
if module == nil {
// It's fine if the module doesn't actually exist; we don't need to check if the resource exists.
} else {
if rs, ok := module.ManagedResources[args.Addr.ContainingResource().Config().String()]; ok {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Resource already in configuration",
Detail: fmt.Sprintf("The resource %s is already in this configuration at %s. Resource names must be unique per type in each module.", args.Addr, rs.DeclRange),
Subject: &rs.DeclRange,
})
c.View.Diagnostics(diags)
return 1
}
}
// Get the schemas from the context // Get the schemas from the context
schemas, moreDiags := lr.Core.Schemas(lr.Config, lr.InputState) schemas, moreDiags := lr.Core.Schemas(lr.Config, lr.InputState)
diags = diags.Append(moreDiags) diags = diags.Append(moreDiags)

View File

@ -118,6 +118,45 @@ resource "test_instance" "new" {
} }
}) })
t.Run("basic to existing file", func(t *testing.T) {
view, done := testView(t)
c := &AddCommand{
Meta: Meta{
testingOverrides: overrides,
View: view,
},
}
outPath := "add.tf"
args := []string{fmt.Sprintf("-out=%s", outPath), "test_instance.new"}
c.Run(args)
args = []string{fmt.Sprintf("-out=%s", outPath), "test_instance.new2"}
code := c.Run(args)
output := done(t)
if code != 0 {
fmt.Println(output.Stderr())
t.Fatalf("wrong exit status. Got %d, want 0", code)
}
expected := `resource "test_instance" "new" {
value = null # REQUIRED string
}
resource "test_instance" "new2" {
value = null # REQUIRED string
}
`
result, err := os.ReadFile(outPath)
if err != nil {
t.Fatalf("error reading result file %s: %s", outPath, err.Error())
}
// While the entire directory will get removed once the whole test suite
// is done, we remove this lest it gets in the way of another (not yet
// written) test.
os.Remove(outPath)
if !cmp.Equal(expected, string(result)) {
t.Fatalf("wrong output:\n%s", cmp.Diff(expected, string(result)))
}
})
t.Run("optionals", func(t *testing.T) { t.Run("optionals", func(t *testing.T) {
view, done := testView(t) view, done := testView(t)
c := &AddCommand{ c := &AddCommand{
@ -194,7 +233,8 @@ resource "test_instance" "new" {
View: view, View: view,
}, },
} }
args := []string{"test_instance.exists"} outPath := "add.tf"
args := []string{fmt.Sprintf("-out=%s", outPath), "test_instance.exists"}
code := c.Run(args) code := c.Run(args)
if code != 1 { if code != 1 {
t.Fatalf("wrong exit status. Got %d, want 0", code) t.Fatalf("wrong exit status. Got %d, want 0", code)
@ -206,6 +246,31 @@ resource "test_instance" "new" {
} }
}) })
t.Run("output existing resource to stdout", func(t *testing.T) {
view, done := testView(t)
c := &AddCommand{
Meta: Meta{
testingOverrides: overrides,
View: view,
},
}
args := []string{"test_instance.exists"}
code := c.Run(args)
output := done(t)
if code != 0 {
fmt.Println(output.Stderr())
t.Fatalf("wrong exit status. Got %d, want 0", code)
}
expected := `resource "test_instance" "exists" {
value = null # REQUIRED string
}
`
if !cmp.Equal(output.Stdout(), expected) {
t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout()))
}
})
t.Run("provider not in configuration", func(t *testing.T) { t.Run("provider not in configuration", func(t *testing.T) {
view, done := testView(t) view, done := testView(t)
c := &AddCommand{ c := &AddCommand{

View File

@ -84,7 +84,14 @@ func (v *addHuman) Resource(addr addrs.AbsResourceInstance, schema *configschema
} else { } else {
// The Println call above adds this final newline automatically; we add it manually here. // The Println call above adds this final newline automatically; we add it manually here.
formatted = append(formatted, '\n') formatted = append(formatted, '\n')
return os.WriteFile(v.outPath, formatted, 0600)
f, err := os.OpenFile(v.outPath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
return err
}
defer f.Close()
_, err = f.Write(formatted)
return err
} }
} }