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:
parent
cdd5ee6fb3
commit
5b266dd5ca
|
@ -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,
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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.
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
terraform {
|
|
||||||
required_providers {
|
|
||||||
test = {
|
|
||||||
source = "hashicorp/test"
|
|
||||||
}
|
|
||||||
othertest = {
|
|
||||||
source = "happycorp/test"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "test_instance" "exists" {
|
|
||||||
// I exist!
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
terraform {
|
|
||||||
required_providers {
|
|
||||||
test = {
|
|
||||||
source = "hashicorp/test"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "test_instance" "exists" {}
|
|
|
@ -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
|
@ -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.
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue