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:
Kristin Laemmert 2021-06-17 12:08:37 -04:00 committed by GitHub
parent d6e9739b26
commit 583859e510
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 2765 additions and 0 deletions

View File

@ -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,

329
internal/command/add.go Normal file
View File

@ -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
}

View File

@ -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()))
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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.

View File

@ -0,0 +1,14 @@
terraform {
required_providers {
test = {
source = "hashicorp/test"
}
othertest = {
source = "happycorp/test"
}
}
}
resource "test_instance" "exists" {
// I exist!
}

View File

@ -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"
}

View File

@ -0,0 +1,9 @@
terraform {
required_providers {
test = {
source = "hashicorp/test"
}
}
}
resource "test_instance" "exists" {}

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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>