commands: `terraform add` (#28874)
* command: new command, terraform add, generates resource templates terraform add ADDRESS generates a resource configuration template with all required (and optionally optional) attributes set to null. This can optionally also pre-populate nonsesitive attributes with values from an existing resource of the same type in state (sensitive vals will be populated with null and a comment indicating sensitivity) * website: terraform add documentation
This commit is contained in:
parent
d6e9739b26
commit
583859e510
|
@ -110,6 +110,12 @@ func initCommands(
|
|||
// that to match.
|
||||
|
||||
Commands = map[string]cli.CommandFactory{
|
||||
"add": func() (cli.Command, error) {
|
||||
return &command.AddCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"apply": func() (cli.Command, error) {
|
||||
return &command.ApplyCommand{
|
||||
Meta: meta,
|
||||
|
|
|
@ -0,0 +1,329 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/backend"
|
||||
"github.com/hashicorp/terraform/internal/command/arguments"
|
||||
"github.com/hashicorp/terraform/internal/command/views"
|
||||
"github.com/hashicorp/terraform/internal/configs"
|
||||
"github.com/hashicorp/terraform/internal/states"
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// AddCommand is a Command implementation that generates resource configuration templates.
|
||||
type AddCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (c *AddCommand) Run(rawArgs []string) int {
|
||||
// Parse and apply global view arguments
|
||||
common, rawArgs := arguments.ParseView(rawArgs)
|
||||
c.View.Configure(common)
|
||||
|
||||
args, diags := arguments.ParseAdd(rawArgs)
|
||||
view := views.NewAdd(args.ViewType, c.View, args)
|
||||
if diags.HasErrors() {
|
||||
view.Diagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
|
||||
// Check for user-supplied plugin path
|
||||
var err error
|
||||
if c.pluginPath, err = c.loadPluginPath(); err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Error loading plugin path",
|
||||
err.Error(),
|
||||
))
|
||||
view.Diagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
|
||||
// Apply the state arguments to the meta object here because they are later
|
||||
// used when initializing the backend.
|
||||
c.Meta.applyStateArguments(args.State)
|
||||
|
||||
// Load the backend
|
||||
b, backendDiags := c.Backend(nil)
|
||||
diags = diags.Append(backendDiags)
|
||||
if backendDiags.HasErrors() {
|
||||
view.Diagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
|
||||
// We require a local backend
|
||||
local, ok := b.(backend.Local)
|
||||
if !ok {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Unsupported backend",
|
||||
ErrUnsupportedLocalOp,
|
||||
))
|
||||
view.Diagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
|
||||
// This is a read-only command (until -import is implemented)
|
||||
c.ignoreRemoteBackendVersionConflict(b)
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Error determining current working directory",
|
||||
err.Error(),
|
||||
))
|
||||
view.Diagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
|
||||
// Build the operation
|
||||
opReq := c.Operation(b)
|
||||
opReq.AllowUnsetVariables = true
|
||||
opReq.ConfigDir = cwd
|
||||
opReq.ConfigLoader, err = c.initConfigLoader()
|
||||
if err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Error initializing config loader",
|
||||
err.Error(),
|
||||
))
|
||||
view.Diagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
|
||||
// Get the context
|
||||
ctx, _, ctxDiags := local.Context(opReq)
|
||||
diags = diags.Append(ctxDiags)
|
||||
if ctxDiags.HasErrors() {
|
||||
view.Diagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
|
||||
// load the configuration to verify that the resource address doesn't
|
||||
// already exist in the config.
|
||||
var module *configs.Module
|
||||
if args.Addr.Module.IsRoot() {
|
||||
module = ctx.Config().Module
|
||||
} else {
|
||||
// This is weird, but users can potentially specify non-existant module names
|
||||
cfg := ctx.Config().Root.Descendent(args.Addr.Module.Module())
|
||||
if cfg != nil {
|
||||
module = cfg.Module
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
schemas := ctx.Schemas()
|
||||
|
||||
// Determine the correct provider config address. The provider-related
|
||||
// variables may get updated below
|
||||
absProviderConfig := args.Provider
|
||||
var providerLocalName string
|
||||
rs := args.Addr.Resource.Resource
|
||||
|
||||
// If we are getting the values from state, get the AbsProviderConfig
|
||||
// directly from state as well.
|
||||
var resource *states.Resource
|
||||
var moreDiags tfdiags.Diagnostics
|
||||
if args.FromState {
|
||||
resource, moreDiags = c.getResource(b, args.Addr.ContainingResource())
|
||||
if moreDiags.HasErrors() {
|
||||
diags = diags.Append(moreDiags)
|
||||
c.View.Diagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
absProviderConfig = &resource.ProviderConfig
|
||||
}
|
||||
|
||||
if absProviderConfig == nil {
|
||||
ip := rs.ImpliedProvider()
|
||||
if module != nil {
|
||||
provider := module.ImpliedProviderForUnqualifiedType(ip)
|
||||
providerLocalName = module.LocalNameForProvider(provider)
|
||||
absProviderConfig = &addrs.AbsProviderConfig{
|
||||
Provider: provider,
|
||||
Module: args.Addr.Module.Module(),
|
||||
}
|
||||
} else {
|
||||
// lacking any configuration to query, we'll go with a default provider.
|
||||
absProviderConfig = &addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewDefaultProvider(ip),
|
||||
}
|
||||
providerLocalName = ip
|
||||
}
|
||||
} else {
|
||||
if module != nil {
|
||||
providerLocalName = module.LocalNameForProvider(absProviderConfig.Provider)
|
||||
} else {
|
||||
providerLocalName = absProviderConfig.Provider.Type
|
||||
}
|
||||
}
|
||||
|
||||
localProviderConfig := addrs.LocalProviderConfig{
|
||||
LocalName: providerLocalName,
|
||||
Alias: absProviderConfig.Alias,
|
||||
}
|
||||
|
||||
// Get the schemas from the context
|
||||
if _, exists := schemas.Providers[absProviderConfig.Provider]; !exists {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Missing schema for provider",
|
||||
fmt.Sprintf("No schema found for provider %s. Please verify that this provider exists in the configuration.", absProviderConfig.Provider.String()),
|
||||
))
|
||||
c.View.Diagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
|
||||
// Get the schema for the resource
|
||||
schema, schemaVersion := schemas.ResourceTypeConfig(absProviderConfig.Provider, rs.Mode, rs.Type)
|
||||
if schema == nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Missing resource schema from provider",
|
||||
fmt.Sprintf("No resource schema found for %s.", rs.Type),
|
||||
))
|
||||
c.View.Diagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
|
||||
stateVal := cty.NilVal
|
||||
// Now that we have the schema, we can decode the previously-acquired resource state
|
||||
if args.FromState {
|
||||
ri := resource.Instance(args.Addr.Resource.Key)
|
||||
if ri.Current == nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"No state for resource",
|
||||
fmt.Sprintf("There is no state found for the resource %s, so add cannot populate values.", rs.String()),
|
||||
))
|
||||
c.View.Diagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
|
||||
rio, err := ri.Current.Decode(schema.ImpliedType())
|
||||
if err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Error decoding state",
|
||||
fmt.Sprintf("Error decoding state for resource %s: %s", rs.String(), err.Error()),
|
||||
))
|
||||
c.View.Diagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
|
||||
if ri.Current.SchemaVersion != schemaVersion {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Schema version mismatch",
|
||||
fmt.Sprintf("schema version %d for %s in state does not match version %d from the provider", ri.Current.SchemaVersion, rs.String(), schemaVersion),
|
||||
))
|
||||
c.View.Diagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
|
||||
stateVal = rio.Value
|
||||
}
|
||||
|
||||
diags = diags.Append(view.Resource(args.Addr, schema, localProviderConfig, stateVal))
|
||||
if diags.HasErrors() {
|
||||
c.View.Diagnostics(diags)
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *AddCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: terraform [global options] add [options] ADDRESS
|
||||
|
||||
Generates a blank resource template. With no additional options,
|
||||
the template will be displayed in the terminal.
|
||||
|
||||
Options:
|
||||
|
||||
-from-state=true Fill the template with values from an existing resource.
|
||||
Defaults to false.
|
||||
|
||||
-out=string Write the template to a file. If the file already
|
||||
exists, the template will be appended to the file.
|
||||
|
||||
-optional=true Include optional attributes. Defaults to false.
|
||||
|
||||
-provider=provider Override the configured provider for the resource. Conflicts
|
||||
with -from-state
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *AddCommand) Synopsis() string {
|
||||
return "Generate a resource configuration template"
|
||||
}
|
||||
|
||||
func (c *AddCommand) getResource(b backend.Enhanced, addr addrs.AbsResource) (*states.Resource, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
// Get the state
|
||||
env, err := c.Workspace()
|
||||
if err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Error selecting workspace",
|
||||
err.Error(),
|
||||
))
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
stateMgr, err := b.StateMgr(env)
|
||||
if err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Error loading state",
|
||||
fmt.Sprintf(errStateLoadingState, err),
|
||||
))
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
if err := stateMgr.RefreshState(); err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Error refreshing state",
|
||||
err.Error(),
|
||||
))
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
state := stateMgr.State()
|
||||
if state == nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"No state",
|
||||
"There is no state found for the current workspace, so add cannot populate values.",
|
||||
))
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
return state.Resource(addr), nil
|
||||
}
|
|
@ -0,0 +1,531 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/configs/configschema"
|
||||
"github.com/hashicorp/terraform/internal/providers"
|
||||
"github.com/hashicorp/terraform/internal/states"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// simple test cases with a simple resource schema
|
||||
func TestAdd_basic(t *testing.T) {
|
||||
td := tempDir(t)
|
||||
testCopyDir(t, testFixturePath("add/basic"), td)
|
||||
defer os.RemoveAll(td)
|
||||
defer testChdir(t, td)()
|
||||
|
||||
p := testProvider()
|
||||
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
|
||||
ResourceTypes: map[string]providers.Schema{
|
||||
"test_instance": {
|
||||
Block: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {Type: cty.String, Optional: true, Computed: true},
|
||||
"ami": {Type: cty.String, Optional: true, Description: "the ami to use"},
|
||||
"value": {Type: cty.String, Required: true, Description: "a value of a thing"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
overrides := &testingOverrides{
|
||||
Providers: map[addrs.Provider]providers.Factory{
|
||||
addrs.NewDefaultProvider("test"): providers.FactoryFixed(p),
|
||||
addrs.NewProvider("registry.terraform.io", "happycorp", "test"): providers.FactoryFixed(p),
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("basic", func(t *testing.T) {
|
||||
view, done := testView(t)
|
||||
c := &AddCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: overrides,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
args := []string{"test_instance.new"}
|
||||
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
|
||||
}
|
||||
`
|
||||
|
||||
if !cmp.Equal(output.Stdout(), expected) {
|
||||
t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout()))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("basic to 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"}
|
||||
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
|
||||
}
|
||||
`
|
||||
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) {
|
||||
view, done := testView(t)
|
||||
c := &AddCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: overrides,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
args := []string{"-optional", "test_instance.new"}
|
||||
code := c.Run(args)
|
||||
if code != 0 {
|
||||
t.Fatalf("wrong exit status. Got %d, want 0", code)
|
||||
}
|
||||
output := done(t)
|
||||
expected := `resource "test_instance" "new" {
|
||||
ami = null # OPTIONAL string
|
||||
id = null # OPTIONAL string
|
||||
value = null # REQUIRED string
|
||||
}
|
||||
`
|
||||
|
||||
if !cmp.Equal(output.Stdout(), expected) {
|
||||
t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout()))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("alternate provider for resource", func(t *testing.T) {
|
||||
view, done := testView(t)
|
||||
c := &AddCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: overrides,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
args := []string{"-provider=provider[\"registry.terraform.io/happycorp/test\"].alias", "test_instance.new"}
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
if code != 0 {
|
||||
t.Fatalf("wrong exit status. Got %d, want 0", code)
|
||||
}
|
||||
|
||||
// The provider happycorp/test has a localname "othertest" in the provider configuration.
|
||||
expected := `resource "test_instance" "new" {
|
||||
provider = othertest.alias
|
||||
value = null # REQUIRED string
|
||||
}
|
||||
`
|
||||
|
||||
if !cmp.Equal(output.Stdout(), expected) {
|
||||
t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout()))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("resource exists error", 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)
|
||||
if code != 1 {
|
||||
t.Fatalf("wrong exit status. Got %d, want 0", code)
|
||||
}
|
||||
|
||||
output := done(t)
|
||||
if !strings.Contains(output.Stderr(), "The resource test_instance.exists is already in this configuration") {
|
||||
t.Fatalf("missing expected error message: %s", output.Stderr())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("provider not in configuration", func(t *testing.T) {
|
||||
view, done := testView(t)
|
||||
c := &AddCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: overrides,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
args := []string{"toast_instance.new"}
|
||||
code := c.Run(args)
|
||||
if code != 1 {
|
||||
t.Fatalf("wrong exit status. Got %d, want 0", code)
|
||||
}
|
||||
|
||||
output := done(t)
|
||||
if !strings.Contains(output.Stderr(), "No schema found for provider registry.terraform.io/hashicorp/toast.") {
|
||||
t.Fatalf("missing expected error message: %s", output.Stderr())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no schema for resource", func(t *testing.T) {
|
||||
view, done := testView(t)
|
||||
c := &AddCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: overrides,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
args := []string{"test_pet.meow"}
|
||||
code := c.Run(args)
|
||||
if code != 1 {
|
||||
t.Fatalf("wrong exit status. Got %d, want 0", code)
|
||||
}
|
||||
|
||||
output := done(t)
|
||||
if !strings.Contains(output.Stderr(), "No resource schema found for test_pet.") {
|
||||
t.Fatalf("missing expected error message: %s", output.Stderr())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAdd(t *testing.T) {
|
||||
td := tempDir(t)
|
||||
testCopyDir(t, testFixturePath("add/module"), td)
|
||||
defer os.RemoveAll(td)
|
||||
defer testChdir(t, td)()
|
||||
|
||||
// a simple hashicorp/test provider, and a more complex happycorp/test provider
|
||||
p := testProvider()
|
||||
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
|
||||
ResourceTypes: map[string]providers.Schema{
|
||||
"test_instance": {
|
||||
Block: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {Type: cty.String, Required: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
happycorp := testProvider()
|
||||
happycorp.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
|
||||
ResourceTypes: map[string]providers.Schema{
|
||||
"test_instance": {
|
||||
Block: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {Type: cty.String, Optional: true, Computed: true},
|
||||
"ami": {Type: cty.String, Optional: true, Description: "the ami to use"},
|
||||
"value": {Type: cty.String, Required: true, Description: "a value of a thing"},
|
||||
"disks": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingList,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"size": {Type: cty.String, Optional: true},
|
||||
"mount_point": {Type: cty.String, Required: true},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"network_interface": {
|
||||
Nesting: configschema.NestingList,
|
||||
MinItems: 1,
|
||||
Block: configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"device_index": {Type: cty.String, Optional: true},
|
||||
"description": {Type: cty.String, Optional: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
providerSource, psClose := newMockProviderSource(t, map[string][]string{
|
||||
"registry.terraform.io/happycorp/test": {"1.0.0"},
|
||||
"registry.terraform.io/hashicorp/test": {"1.0.0"},
|
||||
})
|
||||
defer psClose()
|
||||
|
||||
overrides := &testingOverrides{
|
||||
Providers: map[addrs.Provider]providers.Factory{
|
||||
addrs.NewProvider("registry.terraform.io", "happycorp", "test"): providers.FactoryFixed(happycorp),
|
||||
addrs.NewDefaultProvider("test"): providers.FactoryFixed(p),
|
||||
},
|
||||
}
|
||||
|
||||
// the test fixture uses a module, so we need to run init.
|
||||
m := Meta{
|
||||
testingOverrides: overrides,
|
||||
ProviderSource: providerSource,
|
||||
Ui: new(cli.MockUi),
|
||||
}
|
||||
|
||||
init := &InitCommand{
|
||||
Meta: m,
|
||||
}
|
||||
|
||||
code := init.Run([]string{})
|
||||
if code != 0 {
|
||||
t.Fatal("init failed")
|
||||
}
|
||||
|
||||
t.Run("optional", func(t *testing.T) {
|
||||
view, done := testView(t)
|
||||
c := &AddCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: overrides,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
args := []string{"-optional", "test_instance.new"}
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
if code != 0 {
|
||||
t.Fatalf("wrong exit status. Got %d, want 0", code)
|
||||
}
|
||||
|
||||
expected := `resource "test_instance" "new" {
|
||||
ami = null # OPTIONAL string
|
||||
disks = [{ # OPTIONAL list of object
|
||||
mount_point = null # REQUIRED string
|
||||
size = null # OPTIONAL string
|
||||
}]
|
||||
id = null # OPTIONAL string
|
||||
value = null # REQUIRED string
|
||||
network_interface { # REQUIRED block
|
||||
description = null # OPTIONAL string
|
||||
device_index = null # OPTIONAL string
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
if !cmp.Equal(output.Stdout(), expected) {
|
||||
t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout()))
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
t.Run("chooses correct provider for root module", func(t *testing.T) {
|
||||
// in the root module of this test fixture, "test" is the local name for "happycorp/test"
|
||||
view, done := testView(t)
|
||||
c := &AddCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: overrides,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
args := []string{"test_instance.new"}
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
if code != 0 {
|
||||
t.Fatalf("wrong exit status. Got %d, want 0", code)
|
||||
}
|
||||
|
||||
expected := `resource "test_instance" "new" {
|
||||
value = null # REQUIRED string
|
||||
network_interface { # REQUIRED block
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
if !cmp.Equal(output.Stdout(), expected) {
|
||||
t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout()))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("chooses correct provider for child module", func(t *testing.T) {
|
||||
// in the child module of this test fixture, "test" is a default "hashicorp/test" provider
|
||||
view, done := testView(t)
|
||||
c := &AddCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: overrides,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
args := []string{"module.child.test_instance.new"}
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
if code != 0 {
|
||||
t.Fatalf("wrong exit status. Got %d, want 0", code)
|
||||
}
|
||||
|
||||
expected := `resource "test_instance" "new" {
|
||||
id = null # REQUIRED string
|
||||
}
|
||||
`
|
||||
|
||||
if !cmp.Equal(output.Stdout(), expected) {
|
||||
t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout()))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("chooses correct provider for an unknown module", func(t *testing.T) {
|
||||
// it's weird but ok to use a new/unknown module name; terraform will
|
||||
// fall back on default providers (unless a -provider argument is
|
||||
// supplied)
|
||||
view, done := testView(t)
|
||||
c := &AddCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: overrides,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
args := []string{"module.madeup.test_instance.new"}
|
||||
code := c.Run(args)
|
||||
output := done(t)
|
||||
if code != 0 {
|
||||
t.Fatalf("wrong exit status. Got %d, want 0", code)
|
||||
}
|
||||
|
||||
expected := `resource "test_instance" "new" {
|
||||
id = null # REQUIRED string
|
||||
}
|
||||
`
|
||||
|
||||
if !cmp.Equal(output.Stdout(), expected) {
|
||||
t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAdd_from_state(t *testing.T) {
|
||||
td := tempDir(t)
|
||||
testCopyDir(t, testFixturePath("add/basic"), td)
|
||||
defer os.RemoveAll(td)
|
||||
defer testChdir(t, td)()
|
||||
|
||||
// write some state
|
||||
testState := states.BuildState(func(s *states.SyncState) {
|
||||
s.SetResourceInstanceCurrent(
|
||||
addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "test_instance",
|
||||
Name: "new",
|
||||
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
||||
&states.ResourceInstanceObjectSrc{
|
||||
AttrsJSON: []byte("{\"id\":\"bar\",\"ami\":\"ami-123456\",\"disks\":[{\"mount_point\":\"diska\",\"size\":null}],\"value\":\"bloop\"}"),
|
||||
Status: states.ObjectReady,
|
||||
Dependencies: []addrs.ConfigResource{},
|
||||
},
|
||||
mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
|
||||
)
|
||||
})
|
||||
f, err := os.Create("terraform.tfstate")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temporary state file: %s", err)
|
||||
}
|
||||
defer f.Close()
|
||||
err = writeStateForTesting(testState, f)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write state file: %s", err)
|
||||
}
|
||||
|
||||
p := testProvider()
|
||||
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
|
||||
ResourceTypes: map[string]providers.Schema{
|
||||
"test_instance": {
|
||||
Block: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {Type: cty.String, Optional: true, Computed: true},
|
||||
"ami": {Type: cty.String, Optional: true, Description: "the ami to use"},
|
||||
"value": {Type: cty.String, Required: true, Description: "a value of a thing"},
|
||||
"disks": {
|
||||
NestedType: &configschema.Object{
|
||||
Nesting: configschema.NestingList,
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"size": {Type: cty.String, Optional: true},
|
||||
"mount_point": {Type: cty.String, Required: true},
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
BlockTypes: map[string]*configschema.NestedBlock{
|
||||
"network_interface": {
|
||||
Nesting: configschema.NestingList,
|
||||
MinItems: 1,
|
||||
Block: configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"device_index": {Type: cty.String, Optional: true},
|
||||
"description": {Type: cty.String, Optional: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
overrides := &testingOverrides{
|
||||
Providers: map[addrs.Provider]providers.Factory{
|
||||
addrs.NewDefaultProvider("test"): providers.FactoryFixed(p),
|
||||
addrs.NewProvider("registry.terraform.io", "happycorp", "test"): providers.FactoryFixed(p),
|
||||
},
|
||||
}
|
||||
view, done := testView(t)
|
||||
c := &AddCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: overrides,
|
||||
View: view,
|
||||
},
|
||||
}
|
||||
|
||||
args := []string{"-from-state", "test_instance.new"}
|
||||
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" {
|
||||
ami = "ami-123456"
|
||||
disks = [
|
||||
{
|
||||
mount_point = "diska"
|
||||
size = null
|
||||
},
|
||||
]
|
||||
id = "bar"
|
||||
value = "bloop"
|
||||
}
|
||||
`
|
||||
|
||||
if !cmp.Equal(output.Stdout(), expected) {
|
||||
t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout()))
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
package arguments
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
)
|
||||
|
||||
// Add represents the command-line arguments for the Add command.
|
||||
type Add struct {
|
||||
// Addr specifies which resource to generate configuration for.
|
||||
Addr addrs.AbsResourceInstance
|
||||
|
||||
// FromState specifies that the configuration should be populated with
|
||||
// values from state.
|
||||
FromState bool
|
||||
|
||||
// OutPath contains an optional path to store the generated configuration.
|
||||
OutPath string
|
||||
|
||||
// Optional specifies whether or not to include optional attributes in the
|
||||
// generated configuration. Defaults to false.
|
||||
Optional bool
|
||||
|
||||
// Provider specifies the provider for the target.
|
||||
Provider *addrs.AbsProviderConfig
|
||||
|
||||
// State from the common extended flags.
|
||||
State *State
|
||||
|
||||
// ViewType specifies which output format to use. ViewHuman is currently the
|
||||
// only supported view type.
|
||||
ViewType ViewType
|
||||
}
|
||||
|
||||
func ParseAdd(args []string) (*Add, tfdiags.Diagnostics) {
|
||||
add := &Add{State: &State{}, ViewType: ViewHuman}
|
||||
|
||||
var diags tfdiags.Diagnostics
|
||||
var provider string
|
||||
|
||||
cmdFlags := extendedFlagSet("add", add.State, nil, nil)
|
||||
cmdFlags.BoolVar(&add.FromState, "from-state", false, "fill attribute values from a resource already managed by terraform")
|
||||
cmdFlags.BoolVar(&add.Optional, "optional", false, "include optional attributes")
|
||||
cmdFlags.StringVar(&add.OutPath, "out", "", "out")
|
||||
cmdFlags.StringVar(&provider, "provider", "", "provider")
|
||||
|
||||
if err := cmdFlags.Parse(args); err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Failed to parse command-line flags",
|
||||
err.Error(),
|
||||
))
|
||||
return add, diags
|
||||
}
|
||||
|
||||
args = cmdFlags.Args()
|
||||
if len(args) != 1 {
|
||||
//var adj string
|
||||
adj := "few"
|
||||
if len(args) > 1 {
|
||||
adj = "many"
|
||||
}
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
fmt.Sprintf("Too %s command line arguments", adj),
|
||||
"Expected exactly one positional argument, giving the address of the resource to generate configuration for.",
|
||||
))
|
||||
return add, diags
|
||||
}
|
||||
|
||||
// parse address from the argument
|
||||
addr, addrDiags := addrs.ParseAbsResourceInstanceStr(args[0])
|
||||
if addrDiags.HasErrors() {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
fmt.Sprintf("Error parsing resource address: %s", args[0]),
|
||||
"This command requires that the address argument specifies one resource instance.",
|
||||
))
|
||||
return add, diags
|
||||
}
|
||||
add.Addr = addr
|
||||
|
||||
if provider != "" {
|
||||
if add.FromState {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Incompatible command-line options",
|
||||
"Cannot use both -from-state and -provider. The provider will be determined from the resource's state.",
|
||||
))
|
||||
return add, diags
|
||||
}
|
||||
|
||||
absProvider, providerDiags := addrs.ParseAbsProviderConfigStr(provider)
|
||||
if providerDiags.HasErrors() {
|
||||
// The diagnostics returned from ParseAbsProviderConfigStr are
|
||||
// not always clear, so we wrap them in a single customized diagnostic.
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
fmt.Sprintf("Invalid provider string: %s", provider),
|
||||
providerDiags.Err().Error(),
|
||||
))
|
||||
return add, diags
|
||||
}
|
||||
add.Provider = &absProvider
|
||||
}
|
||||
|
||||
return add, diags
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
package arguments
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
)
|
||||
|
||||
func TestParseAdd(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
args []string
|
||||
want *Add
|
||||
wantError string
|
||||
}{
|
||||
"defaults": {
|
||||
[]string{"test_foo.bar"},
|
||||
&Add{
|
||||
Addr: mustResourceInstanceAddr("test_foo.bar"),
|
||||
State: &State{Lock: true},
|
||||
ViewType: ViewHuman,
|
||||
},
|
||||
``,
|
||||
},
|
||||
"some flags": {
|
||||
[]string{"-optional=true", "test_foo.bar"},
|
||||
&Add{
|
||||
Addr: mustResourceInstanceAddr("test_foo.bar"),
|
||||
State: &State{Lock: true},
|
||||
Optional: true,
|
||||
ViewType: ViewHuman,
|
||||
},
|
||||
``,
|
||||
},
|
||||
"-from-state": {
|
||||
[]string{"-from-state", "module.foo.test_foo.baz"},
|
||||
&Add{
|
||||
Addr: mustResourceInstanceAddr("module.foo.test_foo.baz"),
|
||||
State: &State{Lock: true},
|
||||
ViewType: ViewHuman,
|
||||
FromState: true,
|
||||
},
|
||||
``,
|
||||
},
|
||||
"-provider": {
|
||||
[]string{"-provider=provider[\"example.com/happycorp/test\"]", "test_foo.bar"},
|
||||
&Add{
|
||||
Addr: mustResourceInstanceAddr("test_foo.bar"),
|
||||
State: &State{Lock: true},
|
||||
ViewType: ViewHuman,
|
||||
Provider: &addrs.AbsProviderConfig{
|
||||
Provider: addrs.NewProvider("example.com", "happycorp", "test"),
|
||||
},
|
||||
},
|
||||
``,
|
||||
},
|
||||
"state options from extended flag set": {
|
||||
[]string{"-state=local.tfstate", "test_foo.bar"},
|
||||
&Add{
|
||||
Addr: mustResourceInstanceAddr("test_foo.bar"),
|
||||
State: &State{Lock: true, StatePath: "local.tfstate"},
|
||||
ViewType: ViewHuman,
|
||||
},
|
||||
``,
|
||||
},
|
||||
|
||||
// Error cases
|
||||
"missing required argument": {
|
||||
nil,
|
||||
&Add{
|
||||
ViewType: ViewHuman,
|
||||
State: &State{Lock: true},
|
||||
},
|
||||
`Too few command line arguments`,
|
||||
},
|
||||
"too many arguments": {
|
||||
[]string{"-from-state", "resource_foo.bar", "module.foo.resource_foo.baz"},
|
||||
&Add{
|
||||
ViewType: ViewHuman,
|
||||
State: &State{Lock: true},
|
||||
FromState: true,
|
||||
},
|
||||
`Too many command line arguments`,
|
||||
},
|
||||
"invalid target address": {
|
||||
[]string{"definitely-not_a-VALID-resource"},
|
||||
&Add{
|
||||
ViewType: ViewHuman,
|
||||
State: &State{Lock: true},
|
||||
},
|
||||
`Error parsing resource address: definitely-not_a-VALID-resource`,
|
||||
},
|
||||
"invalid provider flag": {
|
||||
[]string{"-provider=/this/isn't/quite/correct", "resource_foo.bar"},
|
||||
&Add{
|
||||
Addr: mustResourceInstanceAddr("resource_foo.bar"),
|
||||
ViewType: ViewHuman,
|
||||
State: &State{Lock: true},
|
||||
},
|
||||
`Invalid provider string: /this/isn't/quite/correct`,
|
||||
},
|
||||
"incompatible options": {
|
||||
[]string{"-from-state", "-provider=provider[\"example.com/happycorp/test\"]", "test_compute.bar"},
|
||||
&Add{ViewType: ViewHuman,
|
||||
Addr: mustResourceInstanceAddr("test_compute.bar"),
|
||||
State: &State{Lock: true},
|
||||
FromState: true,
|
||||
},
|
||||
`Incompatible command-line options`,
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, diags := ParseAdd(test.args)
|
||||
if test.wantError != "" {
|
||||
if len(diags) != 1 {
|
||||
t.Fatalf("got %d diagnostics; want exactly 1\n", len(diags))
|
||||
}
|
||||
if diags[0].Severity() != tfdiags.Error {
|
||||
t.Fatalf("got a warning; want an error\n%s", diags.ErrWithWarnings())
|
||||
}
|
||||
if desc := diags[0].Description(); desc.Summary != test.wantError {
|
||||
t.Fatalf("wrong error\ngot: %s\nwant: %s", desc.Summary, test.wantError)
|
||||
}
|
||||
} else {
|
||||
if len(diags) != 0 {
|
||||
t.Fatalf("got %d diagnostics; want none\n%s", len(diags), diags.Err().Error())
|
||||
}
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(test.want, got); diff != "" {
|
||||
t.Errorf("unexpected result\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mustResourceInstanceAddr(s string) addrs.AbsResourceInstance {
|
||||
addr, diags := addrs.ParseAbsResourceInstanceStr(s)
|
||||
if diags.HasErrors() {
|
||||
panic(diags.Err())
|
||||
}
|
||||
return addr
|
||||
}
|
|
@ -935,6 +935,14 @@ func mustResourceAddr(s string) addrs.ConfigResource {
|
|||
return addr.Config()
|
||||
}
|
||||
|
||||
func mustProviderConfig(s string) addrs.AbsProviderConfig {
|
||||
p, diags := addrs.ParseAbsProviderConfigStr(s)
|
||||
if diags.HasErrors() {
|
||||
panic(diags.Err())
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// This map from provider type name to namespace is used by the fake registry
|
||||
// when called via LookupLegacyProvider. Providers not in this map will return
|
||||
// a 404 Not Found error.
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
terraform {
|
||||
required_providers {
|
||||
test = {
|
||||
source = "hashicorp/test"
|
||||
}
|
||||
othertest = {
|
||||
source = "happycorp/test"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "test_instance" "exists" {
|
||||
// I exist!
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
terraform {
|
||||
required_providers {
|
||||
// This is deliberately odd, so we can test that the correct happycorp
|
||||
// provider is selected for any test_ resource added for this module
|
||||
test = {
|
||||
source = "happycorp/test"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "test_instance" "exists" {
|
||||
// I exist!
|
||||
}
|
||||
|
||||
module "child" {
|
||||
source = "./module"
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
terraform {
|
||||
required_providers {
|
||||
test = {
|
||||
source = "hashicorp/test"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "test_instance" "exists" {}
|
|
@ -0,0 +1,545 @@
|
|||
package views
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/hcl/v2/hclwrite"
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/command/arguments"
|
||||
"github.com/hashicorp/terraform/internal/configs/configschema"
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// Add is the view interface for the "terraform add" command.
|
||||
type Add interface {
|
||||
Resource(addrs.AbsResourceInstance, *configschema.Block, addrs.LocalProviderConfig, cty.Value) error
|
||||
Diagnostics(tfdiags.Diagnostics)
|
||||
}
|
||||
|
||||
// NewAdd returns an initialized Validate implementation. At this time,
|
||||
// ViewHuman is the only implemented view type.
|
||||
func NewAdd(vt arguments.ViewType, view *View, args *arguments.Add) Add {
|
||||
return &addHuman{
|
||||
view: view,
|
||||
optional: args.Optional,
|
||||
outPath: args.OutPath,
|
||||
}
|
||||
}
|
||||
|
||||
type addHuman struct {
|
||||
view *View
|
||||
optional bool
|
||||
outPath string
|
||||
}
|
||||
|
||||
func (v *addHuman) Resource(addr addrs.AbsResourceInstance, schema *configschema.Block, pc addrs.LocalProviderConfig, stateVal cty.Value) error {
|
||||
var buf strings.Builder
|
||||
buf.WriteString(fmt.Sprintf("resource %q %q {\n", addr.Resource.Resource.Type, addr.Resource.Resource.Name))
|
||||
|
||||
if pc.LocalName != addr.Resource.Resource.ImpliedProvider() || pc.Alias != "" {
|
||||
buf.WriteString(strings.Repeat(" ", 2))
|
||||
buf.WriteString(fmt.Sprintf("provider = %s\n", pc.StringCompact()))
|
||||
}
|
||||
|
||||
if stateVal.RawEquals(cty.NilVal) {
|
||||
if err := v.writeConfigAttributes(&buf, schema.Attributes, 2); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := v.writeConfigBlocks(&buf, schema.BlockTypes, 2); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := v.writeConfigAttributesFromExisting(&buf, stateVal, schema.Attributes, 2); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := v.writeConfigBlocksFromExisting(&buf, stateVal, schema.BlockTypes, 2); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
buf.WriteString("}")
|
||||
|
||||
// The output better be valid HCL which can be parsed and formatted.
|
||||
formatted := hclwrite.Format([]byte(buf.String()))
|
||||
|
||||
var err error
|
||||
if v.outPath == "" {
|
||||
_, err = v.view.streams.Println(string(formatted))
|
||||
return err
|
||||
} else {
|
||||
// The Println call above adds this final newline automatically; we add it manually here.
|
||||
formatted = append(formatted, '\n')
|
||||
return os.WriteFile(v.outPath, formatted, 0600)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *addHuman) Diagnostics(diags tfdiags.Diagnostics) {
|
||||
v.view.Diagnostics(diags)
|
||||
}
|
||||
|
||||
func (v *addHuman) writeConfigAttributes(buf *strings.Builder, attrs map[string]*configschema.Attribute, indent int) error {
|
||||
if len(attrs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get a list of sorted attribute names so the output will be consistent between runs.
|
||||
keys := make([]string, 0, len(attrs))
|
||||
for k := range attrs {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for i := range keys {
|
||||
name := keys[i]
|
||||
attrS := attrs[name]
|
||||
if attrS.NestedType != nil {
|
||||
if err := v.writeConfigNestedTypeAttribute(buf, name, attrS, indent); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if attrS.Required {
|
||||
buf.WriteString(strings.Repeat(" ", indent))
|
||||
buf.WriteString(fmt.Sprintf("%s = ", name))
|
||||
tok := hclwrite.TokensForValue(attrS.EmptyValue())
|
||||
if _, err := tok.WriteTo(buf); err != nil {
|
||||
return err
|
||||
}
|
||||
writeAttrTypeConstraint(buf, attrS)
|
||||
} else if attrS.Optional && v.optional {
|
||||
buf.WriteString(strings.Repeat(" ", indent))
|
||||
buf.WriteString(fmt.Sprintf("%s = ", name))
|
||||
tok := hclwrite.TokensForValue(attrS.EmptyValue())
|
||||
if _, err := tok.WriteTo(buf); err != nil {
|
||||
return err
|
||||
}
|
||||
writeAttrTypeConstraint(buf, attrS)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *addHuman) writeConfigAttributesFromExisting(buf *strings.Builder, stateVal cty.Value, attrs map[string]*configschema.Attribute, indent int) error {
|
||||
if len(attrs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get a list of sorted attribute names so the output will be consistent between runs.
|
||||
keys := make([]string, 0, len(attrs))
|
||||
for k := range attrs {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for i := range keys {
|
||||
name := keys[i]
|
||||
attrS := attrs[name]
|
||||
if attrS.NestedType != nil {
|
||||
if err := v.writeConfigNestedTypeAttributeFromExisting(buf, name, attrS, stateVal, indent); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Exclude computed-only attributes
|
||||
if attrS.Required || attrS.Optional {
|
||||
buf.WriteString(strings.Repeat(" ", indent))
|
||||
buf.WriteString(fmt.Sprintf("%s = ", name))
|
||||
|
||||
var val cty.Value
|
||||
if stateVal.Type().HasAttribute(name) {
|
||||
val = stateVal.GetAttr(name)
|
||||
} else {
|
||||
val = attrS.EmptyValue()
|
||||
}
|
||||
if attrS.Sensitive || val.IsMarked() {
|
||||
buf.WriteString("null # sensitive")
|
||||
} else {
|
||||
tok := hclwrite.TokensForValue(val)
|
||||
if _, err := tok.WriteTo(buf); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *addHuman) writeConfigBlocks(buf *strings.Builder, blocks map[string]*configschema.NestedBlock, indent int) error {
|
||||
if len(blocks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get a list of sorted block names so the output will be consistent between runs.
|
||||
names := make([]string, 0, len(blocks))
|
||||
for k := range blocks {
|
||||
names = append(names, k)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
for i := range names {
|
||||
name := names[i]
|
||||
blockS := blocks[name]
|
||||
if err := v.writeConfigNestedBlock(buf, name, blockS, indent); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *addHuman) writeConfigNestedBlock(buf *strings.Builder, name string, schema *configschema.NestedBlock, indent int) error {
|
||||
if !v.optional && schema.MinItems == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch schema.Nesting {
|
||||
case configschema.NestingSingle, configschema.NestingGroup:
|
||||
buf.WriteString(strings.Repeat(" ", indent))
|
||||
buf.WriteString(fmt.Sprintf("%s {", name))
|
||||
writeBlockTypeConstraint(buf, schema)
|
||||
if err := v.writeConfigAttributes(buf, schema.Attributes, indent+2); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := v.writeConfigBlocks(buf, schema.BlockTypes, indent+2); err != nil {
|
||||
return err
|
||||
}
|
||||
buf.WriteString("}\n")
|
||||
return nil
|
||||
case configschema.NestingList, configschema.NestingSet:
|
||||
buf.WriteString(strings.Repeat(" ", indent))
|
||||
buf.WriteString(fmt.Sprintf("%s {", name))
|
||||
writeBlockTypeConstraint(buf, schema)
|
||||
if err := v.writeConfigAttributes(buf, schema.Attributes, indent+2); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := v.writeConfigBlocks(buf, schema.BlockTypes, indent+2); err != nil {
|
||||
return err
|
||||
}
|
||||
buf.WriteString("}\n")
|
||||
return nil
|
||||
case configschema.NestingMap:
|
||||
buf.WriteString(strings.Repeat(" ", indent))
|
||||
// we use an arbitrary placeholder key (block label) "key"
|
||||
buf.WriteString(fmt.Sprintf("%s \"key\" {", name))
|
||||
writeBlockTypeConstraint(buf, schema)
|
||||
if err := v.writeConfigAttributes(buf, schema.Attributes, indent+2); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := v.writeConfigBlocks(buf, schema.BlockTypes, indent+2); err != nil {
|
||||
return err
|
||||
}
|
||||
buf.WriteString(strings.Repeat(" ", indent))
|
||||
buf.WriteString("}\n")
|
||||
return nil
|
||||
default:
|
||||
// This should not happen, the above should be exhaustive.
|
||||
return fmt.Errorf("unsupported NestingMode %s", schema.Nesting.String())
|
||||
}
|
||||
}
|
||||
|
||||
func (v *addHuman) writeConfigNestedTypeAttribute(buf *strings.Builder, name string, schema *configschema.Attribute, indent int) error {
|
||||
if schema.Required == false && v.optional == false {
|
||||
return nil
|
||||
}
|
||||
|
||||
buf.WriteString(strings.Repeat(" ", indent))
|
||||
buf.WriteString(fmt.Sprintf("%s = ", name))
|
||||
|
||||
switch schema.NestedType.Nesting {
|
||||
case configschema.NestingSingle:
|
||||
buf.WriteString("{")
|
||||
writeAttrTypeConstraint(buf, schema)
|
||||
if err := v.writeConfigAttributes(buf, schema.NestedType.Attributes, indent+2); err != nil {
|
||||
return err
|
||||
}
|
||||
buf.WriteString(strings.Repeat(" ", indent))
|
||||
buf.WriteString("}\n")
|
||||
return nil
|
||||
case configschema.NestingList, configschema.NestingSet:
|
||||
buf.WriteString("[{")
|
||||
writeAttrTypeConstraint(buf, schema)
|
||||
if err := v.writeConfigAttributes(buf, schema.NestedType.Attributes, indent+2); err != nil {
|
||||
return err
|
||||
}
|
||||
buf.WriteString(strings.Repeat(" ", indent))
|
||||
buf.WriteString("}]\n")
|
||||
return nil
|
||||
case configschema.NestingMap:
|
||||
buf.WriteString("{")
|
||||
writeAttrTypeConstraint(buf, schema)
|
||||
buf.WriteString(strings.Repeat(" ", indent+2))
|
||||
// we use an arbitrary placeholder key "key"
|
||||
buf.WriteString("key = {\n")
|
||||
if err := v.writeConfigAttributes(buf, schema.NestedType.Attributes, indent+4); err != nil {
|
||||
return err
|
||||
}
|
||||
buf.WriteString(strings.Repeat(" ", indent+2))
|
||||
buf.WriteString("}\n")
|
||||
buf.WriteString(strings.Repeat(" ", indent))
|
||||
buf.WriteString("}\n")
|
||||
return nil
|
||||
default:
|
||||
// This should not happen, the above should be exhaustive.
|
||||
return fmt.Errorf("unsupported NestingMode %s", schema.NestedType.Nesting.String())
|
||||
}
|
||||
}
|
||||
|
||||
func (v *addHuman) writeConfigBlocksFromExisting(buf *strings.Builder, stateVal cty.Value, blocks map[string]*configschema.NestedBlock, indent int) error {
|
||||
if len(blocks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get a list of sorted block names so the output will be consistent between runs.
|
||||
names := make([]string, 0, len(blocks))
|
||||
for k := range blocks {
|
||||
names = append(names, k)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
for _, name := range names {
|
||||
blockS := blocks[name]
|
||||
// This shouldn't happen in real usage; state always has all values (set
|
||||
// to null as needed), but it protects against panics in tests (and any
|
||||
// really weird and unlikely cases).
|
||||
if !stateVal.Type().HasAttribute(name) {
|
||||
continue
|
||||
}
|
||||
blockVal := stateVal.GetAttr(name)
|
||||
if err := v.writeConfigNestedBlockFromExisting(buf, name, blockS, blockVal, indent); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *addHuman) writeConfigNestedTypeAttributeFromExisting(buf *strings.Builder, name string, schema *configschema.Attribute, stateVal cty.Value, indent int) error {
|
||||
switch schema.NestedType.Nesting {
|
||||
case configschema.NestingSingle:
|
||||
if schema.Sensitive || stateVal.IsMarked() {
|
||||
buf.WriteString(strings.Repeat(" ", indent))
|
||||
buf.WriteString(fmt.Sprintf("%s = {} # sensitive\n", name))
|
||||
return nil
|
||||
}
|
||||
buf.WriteString(strings.Repeat(" ", indent))
|
||||
buf.WriteString(fmt.Sprintf("%s = {\n", name))
|
||||
|
||||
// This shouldn't happen in real usage; state always has all values (set
|
||||
// to null as needed), but it protects against panics in tests (and any
|
||||
// really weird and unlikely cases).
|
||||
if !stateVal.Type().HasAttribute(name) {
|
||||
return nil
|
||||
}
|
||||
nestedVal := stateVal.GetAttr(name)
|
||||
if err := v.writeConfigAttributesFromExisting(buf, nestedVal, schema.NestedType.Attributes, indent+2); err != nil {
|
||||
return err
|
||||
}
|
||||
buf.WriteString("}\n")
|
||||
return nil
|
||||
|
||||
case configschema.NestingList, configschema.NestingSet:
|
||||
buf.WriteString(strings.Repeat(" ", indent))
|
||||
buf.WriteString(fmt.Sprintf("%s = [", name))
|
||||
|
||||
if schema.Sensitive || stateVal.IsMarked() {
|
||||
buf.WriteString("] # sensitive\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
buf.WriteString("\n")
|
||||
|
||||
listVals := ctyCollectionValues(stateVal.GetAttr(name))
|
||||
for i := range listVals {
|
||||
buf.WriteString(strings.Repeat(" ", indent+2))
|
||||
|
||||
// The entire element is marked.
|
||||
if listVals[i].IsMarked() {
|
||||
buf.WriteString("{}, # sensitive\n")
|
||||
continue
|
||||
}
|
||||
|
||||
buf.WriteString("{\n")
|
||||
if err := v.writeConfigAttributesFromExisting(buf, listVals[i], schema.NestedType.Attributes, indent+4); err != nil {
|
||||
return err
|
||||
}
|
||||
buf.WriteString(strings.Repeat(" ", indent+2))
|
||||
buf.WriteString("},\n")
|
||||
}
|
||||
buf.WriteString(strings.Repeat(" ", indent))
|
||||
buf.WriteString("]\n")
|
||||
return nil
|
||||
|
||||
case configschema.NestingMap:
|
||||
buf.WriteString(strings.Repeat(" ", indent))
|
||||
buf.WriteString(fmt.Sprintf("%s = {", name))
|
||||
|
||||
if schema.Sensitive || stateVal.IsMarked() {
|
||||
buf.WriteString(" } # sensitive\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
buf.WriteString("\n")
|
||||
|
||||
vals := stateVal.GetAttr(name).AsValueMap()
|
||||
keys := make([]string, 0, len(vals))
|
||||
for key := range vals {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
buf.WriteString(strings.Repeat(" ", indent+2))
|
||||
buf.WriteString(fmt.Sprintf("%s = {", key))
|
||||
|
||||
// This entire value is marked
|
||||
if vals[key].IsMarked() {
|
||||
buf.WriteString("} # sensitive\n")
|
||||
continue
|
||||
}
|
||||
|
||||
buf.WriteString("\n")
|
||||
if err := v.writeConfigAttributesFromExisting(buf, vals[key], schema.NestedType.Attributes, indent+4); err != nil {
|
||||
return err
|
||||
}
|
||||
buf.WriteString(strings.Repeat(" ", indent+2))
|
||||
buf.WriteString("}\n")
|
||||
}
|
||||
buf.WriteString(strings.Repeat(" ", indent))
|
||||
buf.WriteString("}\n")
|
||||
return nil
|
||||
|
||||
default:
|
||||
// This should not happen, the above should be exhaustive.
|
||||
return fmt.Errorf("unsupported NestingMode %s", schema.NestedType.Nesting.String())
|
||||
}
|
||||
}
|
||||
|
||||
func (v *addHuman) writeConfigNestedBlockFromExisting(buf *strings.Builder, name string, schema *configschema.NestedBlock, stateVal cty.Value, indent int) error {
|
||||
switch schema.Nesting {
|
||||
case configschema.NestingSingle, configschema.NestingGroup:
|
||||
buf.WriteString(strings.Repeat(" ", indent))
|
||||
buf.WriteString(fmt.Sprintf("%s {", name))
|
||||
|
||||
// If the entire value is marked, don't print any nested attributes
|
||||
if stateVal.IsMarked() {
|
||||
buf.WriteString("} # sensitive\n")
|
||||
return nil
|
||||
}
|
||||
buf.WriteString("\n")
|
||||
if err := v.writeConfigAttributesFromExisting(buf, stateVal, schema.Attributes, indent+2); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := v.writeConfigBlocksFromExisting(buf, stateVal, schema.BlockTypes, indent+2); err != nil {
|
||||
return err
|
||||
}
|
||||
buf.WriteString("}\n")
|
||||
return nil
|
||||
case configschema.NestingList, configschema.NestingSet:
|
||||
if stateVal.IsMarked() {
|
||||
buf.WriteString(strings.Repeat(" ", indent))
|
||||
buf.WriteString(fmt.Sprintf("%s {} # sensitive\n", name))
|
||||
return nil
|
||||
}
|
||||
listVals := ctyCollectionValues(stateVal)
|
||||
for i := range listVals {
|
||||
buf.WriteString(strings.Repeat(" ", indent))
|
||||
buf.WriteString(fmt.Sprintf("%s {\n", name))
|
||||
if err := v.writeConfigAttributesFromExisting(buf, listVals[i], schema.Attributes, indent+2); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := v.writeConfigBlocksFromExisting(buf, listVals[i], schema.BlockTypes, indent+2); err != nil {
|
||||
return err
|
||||
}
|
||||
buf.WriteString("}\n")
|
||||
}
|
||||
return nil
|
||||
case configschema.NestingMap:
|
||||
// If the entire value is marked, don't print any nested attributes
|
||||
if stateVal.IsMarked() {
|
||||
buf.WriteString(fmt.Sprintf("%s {} # sensitive\n", name))
|
||||
return nil
|
||||
}
|
||||
|
||||
vals := stateVal.AsValueMap()
|
||||
keys := make([]string, 0, len(vals))
|
||||
for key := range vals {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
buf.WriteString(strings.Repeat(" ", indent))
|
||||
buf.WriteString(fmt.Sprintf("%s %q {", name, key))
|
||||
// This entire map element is marked
|
||||
if vals[key].IsMarked() {
|
||||
buf.WriteString("} # sensitive\n")
|
||||
return nil
|
||||
}
|
||||
buf.WriteString("\n")
|
||||
|
||||
if err := v.writeConfigAttributesFromExisting(buf, vals[key], schema.Attributes, indent+2); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := v.writeConfigBlocksFromExisting(buf, vals[key], schema.BlockTypes, indent+2); err != nil {
|
||||
return err
|
||||
}
|
||||
buf.WriteString(strings.Repeat(" ", indent))
|
||||
buf.WriteString("}\n")
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
// This should not happen, the above should be exhaustive.
|
||||
return fmt.Errorf("unsupported NestingMode %s", schema.Nesting.String())
|
||||
}
|
||||
}
|
||||
|
||||
func writeAttrTypeConstraint(buf *strings.Builder, schema *configschema.Attribute) {
|
||||
if schema.Required {
|
||||
buf.WriteString(" # REQUIRED ")
|
||||
} else {
|
||||
buf.WriteString(" # OPTIONAL ")
|
||||
}
|
||||
|
||||
if schema.NestedType != nil {
|
||||
buf.WriteString(fmt.Sprintf("%s\n", schema.NestedType.ImpliedType().FriendlyName()))
|
||||
} else {
|
||||
buf.WriteString(fmt.Sprintf("%s\n", schema.Type.FriendlyName()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func writeBlockTypeConstraint(buf *strings.Builder, schema *configschema.NestedBlock) {
|
||||
if schema.MinItems > 0 {
|
||||
buf.WriteString(" # REQUIRED block\n")
|
||||
} else {
|
||||
buf.WriteString(" # OPTIONAL block\n")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// copied from command/format/diff
|
||||
func ctyCollectionValues(val cty.Value) []cty.Value {
|
||||
if !val.IsKnown() || val.IsNull() {
|
||||
return nil
|
||||
}
|
||||
|
||||
var len int
|
||||
if val.IsMarked() {
|
||||
val, _ = val.Unmark()
|
||||
len = val.LengthInt()
|
||||
} else {
|
||||
len = val.LengthInt()
|
||||
}
|
||||
|
||||
ret := make([]cty.Value, 0, len)
|
||||
for it := val.ElementIterator(); it.Next(); {
|
||||
_, value := it.Element()
|
||||
ret = append(ret, value)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
layout: "docs"
|
||||
page_title: "Command: add"
|
||||
sidebar_current: "docs-commands-add"
|
||||
description: |-
|
||||
The `terraform add` command generates resource configuration templates.
|
||||
---
|
||||
|
||||
# Command: add
|
||||
|
||||
The `terraform add` command generates a resource configuration template with
|
||||
`null` placeholder values for all attributes, unless the `-from-state` flag is
|
||||
used. By default, the template only includes required resource attributes; the
|
||||
`-optional` flag tells Terraform to also include any optional attributes.
|
||||
|
||||
When `terraform add` used with the `-from-state` will _not_ print sensitive
|
||||
values. You can use `terraform show ADDRESS` to see all values, including
|
||||
sensitive values, recorded in state for the given resource address.
|
||||
|
||||
## Usage
|
||||
|
||||
Usage: `terraform add [options] ADDRESS`
|
||||
|
||||
This command requires an address that points to a resource which does not
|
||||
already exist in the configuration. Addresses are in
|
||||
[resource addressing format](/docs/cli/state/resource-addressing.html).
|
||||
|
||||
This command accepts the following options:
|
||||
|
||||
`-from-state` - populate the template with values from a resource
|
||||
already in state. Sensitive values are redacted.
|
||||
|
||||
`-optional` - include optional attributes in the template.
|
||||
|
||||
`-out=FILENAME` - writes the template to the given filename. If the file already
|
||||
exists, the template will be added to the end of the file.
|
||||
|
||||
`-provider=provider` - override the configured provider for the resource. By
|
||||
default, Terraform will use the configured provider for the given resource type,
|
||||
and that is the best behavior in most cases.
|
|
@ -40,6 +40,7 @@ Main commands:
|
|||
destroy Destroy previously-created infrastructure
|
||||
|
||||
All other commands:
|
||||
add Generate a resource configuration template
|
||||
console Try Terraform expressions at an interactive command prompt
|
||||
fmt Reformat your configuration in the standard style
|
||||
force-unlock Release a stuck lock on the current workspace
|
||||
|
|
|
@ -74,6 +74,10 @@
|
|||
<a href="/docs/cli/code/index.html">Overview</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/docs/cli/commands/add.html"><code>add</code></a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/docs/cli/commands/console.html"><code>console</code></a>
|
||||
</li>
|
||||
|
@ -357,6 +361,10 @@
|
|||
<a href="#">Alphabetical List of Commands</a>
|
||||
<ul class="nav">
|
||||
|
||||
<li>
|
||||
<a href="/docs/cli/commands/add.html"><code>add</code></a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="/docs/cli/commands/apply.html"><code>apply</code></a>
|
||||
</li>
|
||||
|
|
Loading…
Reference in New Issue