command: Remove the experimental "terraform add" command

We introduced this experiment to gather feedback, and the feedback we saw
led to us deciding to do another round of design work before we move
forward with something to meet this use-case.

In addition to being experimental, this has only been included in alpha
releases so far, and so on both counts it is not protected by the
Terraform v1.0 Compatibility Promises.
This commit is contained in:
Martin Atkins 2021-10-18 15:08:35 -07:00
parent cdd5ee6fb3
commit 5b266dd5ca
14 changed files with 0 additions and 3034 deletions

View File

@ -109,12 +109,6 @@ func initCommands(
// that to match. // that to match.
Commands = map[string]cli.CommandFactory{ Commands = map[string]cli.CommandFactory{
"add": func() (cli.Command, error) {
return &command.AddCommand{
Meta: meta,
}, nil
},
"apply": func() (cli.Command, error) { "apply": func() (cli.Command, error) {
return &command.ApplyCommand{ return &command.ApplyCommand{
Meta: meta, Meta: meta,

View File

@ -1,369 +0,0 @@
package command
import (
"fmt"
"os"
"path/filepath"
"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
}
// In case the output configuration path is specified, we should ensure the
// target resource address doesn't exist in the module tree indicated by
// the existing configuration files.
if args.OutPath != "" {
// Ensure the directory to the path exists and is accessible.
outDir := filepath.Dir(args.OutPath)
if _, err := os.Stat(outDir); os.IsNotExist(err) {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"The out path doesn't exist or is not accessible",
err.Error(),
))
view.Diagnostics(diags)
return 1
}
config, loadDiags := c.loadConfig(outDir)
diags = diags.Append(loadDiags)
if diags.HasErrors() {
view.Diagnostics(diags)
return 1
}
if config != nil && config.Module != nil {
if rs, ok := config.Module.ManagedResources[args.Addr.ContainingResource().Config().String()]; ok {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Resource already in configuration",
Detail: fmt.Sprintf("The resource %s is already in this configuration at %s. Resource names must be unique per type in each module.", args.Addr, rs.DeclRange),
Subject: &rs.DeclRange,
})
c.View.Diagnostics(diags)
return 1
}
}
}
// Check for user-supplied plugin path
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
lr, _, ctxDiags := local.LocalRun(opReq)
diags = diags.Append(ctxDiags)
if ctxDiags.HasErrors() {
view.Diagnostics(diags)
return 1
}
// Successfully creating the context can result in a lock, so ensure we release it
defer func() {
diags := opReq.StateLocker.Unlock()
if diags.HasErrors() {
c.showDiagnostics(diags)
}
}()
// 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 = lr.Config.Module
} else {
// This is weird, but users can potentially specify non-existant module names
cfg := lr.Config.Root.Descendent(args.Addr.Module.Module())
if cfg != nil {
module = cfg.Module
}
}
// Get the schemas from the context
schemas, moreDiags := lr.Core.Schemas(lr.Config, lr.InputState)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
view.Diagnostics(diags)
return 1
}
// 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
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))
c.View.Diagnostics(diags)
if diags.HasErrors() {
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, Terraform
will write the result to standard output.
Options:
-from-state Fill the template with values from an existing resource
instance tracked in the state. By default, Terraform will
emit only placeholder values based on the resource type.
-out=string Write the template to a file, instead of to standard
output.
-optional Include optional arguments. By default, the result will
include only required arguments.
-provider=provider Override the provider configuration for the resource,
using the absolute provider configuration address syntax.
This is incompatible with -from-state, because in that
case Terraform will use the provider configuration already
selected in the 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

@ -1,685 +0,0 @@
package command
import (
"fmt"
"os"
"path/filepath"
"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 := `# NOTE: The "terraform add" command is currently experimental and offers only a
# starting point for your resource configuration, with some limitations.
#
# The behavior of this command may change in future based on feedback, possibly
# in incompatible ways. We don't recommend building automation around this
# command at this time. If you have feedback about this command, please open
# a feature request issue in the Terraform GitHub repository.
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 := `# NOTE: The "terraform add" command is currently experimental and offers only a
# starting point for your resource configuration, with some limitations.
#
# The behavior of this command may change in future based on feedback, possibly
# in incompatible ways. We don't recommend building automation around this
# command at this time. If you have feedback about this command, please open
# a feature request issue in the Terraform GitHub repository.
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("basic to existing file", func(t *testing.T) {
view, done := testView(t)
c := &AddCommand{
Meta: Meta{
testingOverrides: overrides,
View: view,
},
}
outPath := "add.tf"
args := []string{fmt.Sprintf("-out=%s", outPath), "test_instance.new"}
c.Run(args)
args = []string{fmt.Sprintf("-out=%s", outPath), "test_instance.new2"}
code := c.Run(args)
output := done(t)
if code != 0 {
fmt.Println(output.Stderr())
t.Fatalf("wrong exit status. Got %d, want 0", code)
}
expected := `# NOTE: The "terraform add" command is currently experimental and offers only a
# starting point for your resource configuration, with some limitations.
#
# The behavior of this command may change in future based on feedback, possibly
# in incompatible ways. We don't recommend building automation around this
# command at this time. If you have feedback about this command, please open
# a feature request issue in the Terraform GitHub repository.
resource "test_instance" "new" {
value = null # REQUIRED string
}
# NOTE: The "terraform add" command is currently experimental and offers only a
# starting point for your resource configuration, with some limitations.
#
# The behavior of this command may change in future based on feedback, possibly
# in incompatible ways. We don't recommend building automation around this
# command at this time. If you have feedback about this command, please open
# a feature request issue in the Terraform GitHub repository.
resource "test_instance" "new2" {
value = null # REQUIRED string
}
`
result, err := os.ReadFile(outPath)
if err != nil {
t.Fatalf("error reading result file %s: %s", outPath, err.Error())
}
// While the entire directory will get removed once the whole test suite
// is done, we remove this lest it gets in the way of another (not yet
// written) test.
os.Remove(outPath)
if !cmp.Equal(expected, string(result)) {
t.Fatalf("wrong output:\n%s", cmp.Diff(expected, string(result)))
}
})
t.Run("optionals", func(t *testing.T) {
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 := `# NOTE: The "terraform add" command is currently experimental and offers only a
# starting point for your resource configuration, with some limitations.
#
# The behavior of this command may change in future based on feedback, possibly
# in incompatible ways. We don't recommend building automation around this
# command at this time. If you have feedback about this command, please open
# a feature request issue in the Terraform GitHub repository.
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 := `# NOTE: The "terraform add" command is currently experimental and offers only a
# starting point for your resource configuration, with some limitations.
#
# The behavior of this command may change in future based on feedback, possibly
# in incompatible ways. We don't recommend building automation around this
# command at this time. If you have feedback about this command, please open
# a feature request issue in the Terraform GitHub repository.
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,
},
}
outPath := "add.tf"
args := []string{fmt.Sprintf("-out=%s", outPath), "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("output existing resource to stdout", func(t *testing.T) {
view, done := testView(t)
c := &AddCommand{
Meta: Meta{
testingOverrides: overrides,
View: view,
},
}
args := []string{"test_instance.exists"}
code := c.Run(args)
output := done(t)
if code != 0 {
fmt.Println(output.Stderr())
t.Fatalf("wrong exit status. Got %d, want 0", code)
}
expected := `# NOTE: The "terraform add" command is currently experimental and offers only a
# starting point for your resource configuration, with some limitations.
#
# The behavior of this command may change in future based on feedback, possibly
# in incompatible ways. We don't recommend building automation around this
# command at this time. If you have feedback about this command, please open
# a feature request issue in the Terraform GitHub repository.
resource "test_instance" "exists" {
value = null # REQUIRED string
}
`
if !cmp.Equal(output.Stdout(), expected) {
t.Fatalf("wrong output:\n%s", cmp.Diff(expected, output.Stdout()))
}
})
t.Run("provider not in configuration", func(t *testing.T) {
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 := `# NOTE: The "terraform add" command is currently experimental and offers only a
# starting point for your resource configuration, with some limitations.
#
# The behavior of this command may change in future based on feedback, possibly
# in incompatible ways. We don't recommend building automation around this
# command at this time. If you have feedback about this command, please open
# a feature request issue in the Terraform GitHub repository.
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 := `# NOTE: The "terraform add" command is currently experimental and offers only a
# starting point for your resource configuration, with some limitations.
#
# The behavior of this command may change in future based on feedback, possibly
# in incompatible ways. We don't recommend building automation around this
# command at this time. If you have feedback about this command, please open
# a feature request issue in the Terraform GitHub repository.
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 := `# NOTE: The "terraform add" command is currently experimental and offers only a
# starting point for your resource configuration, with some limitations.
#
# The behavior of this command may change in future based on feedback, possibly
# in incompatible ways. We don't recommend building automation around this
# command at this time. If you have feedback about this command, please open
# a feature request issue in the Terraform GitHub repository.
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 := `# NOTE: The "terraform add" command is currently experimental and offers only a
# starting point for your resource configuration, with some limitations.
#
# The behavior of this command may change in future based on feedback, possibly
# in incompatible ways. We don't recommend building automation around this
# command at this time. If you have feedback about this command, please open
# a feature request issue in the Terraform GitHub repository.
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 := `# NOTE: The "terraform add" command is currently experimental and offers only a
# starting point for your resource configuration, with some limitations.
#
# The behavior of this command may change in future based on feedback, possibly
# in incompatible ways. We don't recommend building automation around this
# command at this time. If you have feedback about this command, please open
# a feature request issue in the Terraform GitHub repository.
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()))
}
if _, err := os.Stat(filepath.Join(td, ".terraform.tfstate.lock.info")); !os.IsNotExist(err) {
t.Fatal("state left locked after add")
}
}

View File

@ -1,110 +0,0 @@
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

@ -1,146 +0,0 @@
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

@ -1003,14 +1003,6 @@ func mustResourceAddr(s string) addrs.ConfigResource {
return addr.Config() 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 // 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 // when called via LookupLegacyProvider. Providers not in this map will return
// a 404 Not Found error. // a 404 Not Found error.

View File

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

View File

@ -1,17 +0,0 @@
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

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

View File

@ -1,562 +0,0 @@
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/lang/marks"
"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(`# NOTE: The "terraform add" command is currently experimental and offers only a
# starting point for your resource configuration, with some limitations.
#
# The behavior of this command may change in future based on feedback, possibly
# in incompatible ways. We don't recommend building automation around this
# command at this time. If you have feedback about this command, please open
# a feature request issue in the Terraform GitHub repository.
`)
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\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')
f, err := os.OpenFile(v.outPath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
return err
}
defer f.Close()
_, err = f.Write(formatted)
return err
}
}
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.HasMark(marks.Sensitive) {
buf.WriteString("null # sensitive")
} else {
val, _ = val.Unmark()
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 && !v.optional {
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.HasMark(marks.Sensitive) {
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.HasMark(marks.Sensitive) {
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].HasMark(marks.Sensitive) {
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.HasMark(marks.Sensitive) {
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].HasMark(marks.Sensitive) {
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.HasMark(marks.Sensitive) {
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.HasMark(marks.Sensitive) {
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.HasMark(marks.Sensitive) {
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].HasMark(marks.Sensitive) {
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()))
}
}
func writeBlockTypeConstraint(buf *strings.Builder, schema *configschema.NestedBlock) {
if schema.MinItems > 0 {
buf.WriteString(" # REQUIRED block\n")
} else {
buf.WriteString(" # OPTIONAL block\n")
}
}
// 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

@ -1,81 +0,0 @@
---
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 starting point for the configuration
of a particular resource.
~> **Warning:** This command is currently experimental. Its exact behavior and
command line arguments are likely to change in future releases based on
feedback. We don't recommend building automation around the current design of
this command, but it's safe to use directly in a development environment
setting.
By default, Terraform will include only the subset of arguments that are marked
by the provider as being required, and will use `null` as a placeholder for
their values. You can then replace `null` with suitable expressions in order
to make the arguments valid.
If you use the `-optional` option then Terraform will also include arguments
that the provider declares as optional. You can then either write a suitable
expression for each argument or remove the arguments you wish to leave unset.
If you use the `-from-state` option then Terraform will instead generate a
configuration containing expressions which will produce the same values as
the corresponding resource instance object already tracked in the Terraform
state, if for example you've previously imported the object using
[`terraform import`](import.html).
-> **Note:** If you use `-from-state`, the result will not include expressions
for any values which are marked as sensitive in the state. If you want to
see those, you can inspect the state data directly using
`terraform state show 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` - Fill the template with values from an existing resource
instance already tracked in the state. By default, Terraform will emit only
placeholder values based on the resource type.
* `-optional` - Include optional arguments. By default, the result will
include only required arguments.
* `-out=FILENAME` - Write the template to a file, instead of to standard
output.
* `-provider=provider` - Override the provider configuration for the resource,
using the absolute provider configuration address syntax.
Absolute provider configuration syntax uses the full source address of
the provider, rather than a local name declared in the relevant module.
For example, to select the aliased provider configuration "us-east-1"
of the official AWS provider, use:
```
-provider='provider["hashicorp/aws"].us-east-1'
```
or, if you are using the Windows command prompt, use Windows-style escaping
for the quotes inside the address:
```
-provider=provider[\"hashicorp/aws\"].us-east-1
```
This is incompatible with `-from-state`, because in that case Terraform
will use the provider configuration already selected in the state, which
is the provider configuration that most recently managed the object.

View File

@ -39,7 +39,6 @@ Main commands:
destroy Destroy previously-created infrastructure destroy Destroy previously-created infrastructure
All other commands: All other commands:
add Generate a resource configuration template
console Try Terraform expressions at an interactive command prompt console Try Terraform expressions at an interactive command prompt
fmt Reformat your configuration in the standard style fmt Reformat your configuration in the standard style
force-unlock Release a stuck lock on the current workspace force-unlock Release a stuck lock on the current workspace

View File

@ -74,10 +74,6 @@
<a href="/docs/cli/code/index.html">Overview</a> <a href="/docs/cli/code/index.html">Overview</a>
</li> </li>
<li>
<a href="/docs/cli/commands/add.html"><code>add</code></a>
</li>
<li> <li>
<a href="/docs/cli/commands/console.html"><code>console</code></a> <a href="/docs/cli/commands/console.html"><code>console</code></a>
</li> </li>
@ -361,10 +357,6 @@
<a href="#">Alphabetical List of Commands</a> <a href="#">Alphabetical List of Commands</a>
<ul class="nav"> <ul class="nav">
<li>
<a href="/docs/cli/commands/add.html"><code>add</code></a>
</li>
<li> <li>
<a href="/docs/cli/commands/apply.html"><code>apply</code></a> <a href="/docs/cli/commands/apply.html"><code>apply</code></a>
</li> </li>