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:
commit
1f57b7a8bd
|
@ -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)
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue