From 5b266dd5ca739b1438e2093aa644d3f02fcf1015 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Mon, 18 Oct 2021 15:08:35 -0700 Subject: [PATCH] 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. --- commands.go | 6 - internal/command/add.go | 369 ------ internal/command/add_test.go | 685 ----------- internal/command/arguments/add.go | 110 -- internal/command/arguments/add_test.go | 146 --- internal/command/command_test.go | 8 - internal/command/testdata/add/basic/main.tf | 14 - internal/command/testdata/add/module/main.tf | 17 - .../testdata/add/module/module/main.tf | 9 - internal/command/views/add.go | 562 --------- internal/command/views/add_test.go | 1018 ----------------- website/docs/cli/commands/add.html.md | 81 -- website/docs/cli/commands/index.html.md | 1 - website/layouts/docs.erb | 8 - 14 files changed, 3034 deletions(-) delete mode 100644 internal/command/add.go delete mode 100644 internal/command/add_test.go delete mode 100644 internal/command/arguments/add.go delete mode 100644 internal/command/arguments/add_test.go delete mode 100644 internal/command/testdata/add/basic/main.tf delete mode 100644 internal/command/testdata/add/module/main.tf delete mode 100644 internal/command/testdata/add/module/module/main.tf delete mode 100644 internal/command/views/add.go delete mode 100644 internal/command/views/add_test.go delete mode 100644 website/docs/cli/commands/add.html.md diff --git a/commands.go b/commands.go index 2c1cb90ee..41c39066d 100644 --- a/commands.go +++ b/commands.go @@ -109,12 +109,6 @@ func initCommands( // that to match. Commands = map[string]cli.CommandFactory{ - "add": func() (cli.Command, error) { - return &command.AddCommand{ - Meta: meta, - }, nil - }, - "apply": func() (cli.Command, error) { return &command.ApplyCommand{ Meta: meta, diff --git a/internal/command/add.go b/internal/command/add.go deleted file mode 100644 index 69f91b773..000000000 --- a/internal/command/add.go +++ /dev/null @@ -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 -} diff --git a/internal/command/add_test.go b/internal/command/add_test.go deleted file mode 100644 index 7afe5e754..000000000 --- a/internal/command/add_test.go +++ /dev/null @@ -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") - } -} diff --git a/internal/command/arguments/add.go b/internal/command/arguments/add.go deleted file mode 100644 index 18fa59f1b..000000000 --- a/internal/command/arguments/add.go +++ /dev/null @@ -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 -} diff --git a/internal/command/arguments/add_test.go b/internal/command/arguments/add_test.go deleted file mode 100644 index bc63255cd..000000000 --- a/internal/command/arguments/add_test.go +++ /dev/null @@ -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 -} diff --git a/internal/command/command_test.go b/internal/command/command_test.go index 4b6fcf311..5fab71f1f 100644 --- a/internal/command/command_test.go +++ b/internal/command/command_test.go @@ -1003,14 +1003,6 @@ func mustResourceAddr(s string) addrs.ConfigResource { return addr.Config() } -func mustProviderConfig(s string) addrs.AbsProviderConfig { - p, diags := addrs.ParseAbsProviderConfigStr(s) - if diags.HasErrors() { - panic(diags.Err()) - } - return p -} - // This map from provider type name to namespace is used by the fake registry // when called via LookupLegacyProvider. Providers not in this map will return // a 404 Not Found error. diff --git a/internal/command/testdata/add/basic/main.tf b/internal/command/testdata/add/basic/main.tf deleted file mode 100644 index ec661dbd9..000000000 --- a/internal/command/testdata/add/basic/main.tf +++ /dev/null @@ -1,14 +0,0 @@ -terraform { - required_providers { - test = { - source = "hashicorp/test" - } - othertest = { - source = "happycorp/test" - } - } -} - -resource "test_instance" "exists" { - // I exist! -} \ No newline at end of file diff --git a/internal/command/testdata/add/module/main.tf b/internal/command/testdata/add/module/main.tf deleted file mode 100644 index 11fca9923..000000000 --- a/internal/command/testdata/add/module/main.tf +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/internal/command/testdata/add/module/module/main.tf b/internal/command/testdata/add/module/module/main.tf deleted file mode 100644 index 55210f20f..000000000 --- a/internal/command/testdata/add/module/module/main.tf +++ /dev/null @@ -1,9 +0,0 @@ -terraform { - required_providers { - test = { - source = "hashicorp/test" - } - } -} - -resource "test_instance" "exists" {} \ No newline at end of file diff --git a/internal/command/views/add.go b/internal/command/views/add.go deleted file mode 100644 index c009fb463..000000000 --- a/internal/command/views/add.go +++ /dev/null @@ -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 -} diff --git a/internal/command/views/add_test.go b/internal/command/views/add_test.go deleted file mode 100644 index c0986e4da..000000000 --- a/internal/command/views/add_test.go +++ /dev/null @@ -1,1018 +0,0 @@ -package views - -import ( - "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/lang/marks" - "github.com/hashicorp/terraform/internal/terminal" - "github.com/zclconf/go-cty/cty" -) - -// The output is tested in greater detail in other tests; this suite focuses on -// details specific to the Resource function. -func TestAddResource(t *testing.T) { - t.Run("config only", func(t *testing.T) { - streams, done := terminal.StreamsForTesting(t) - v := addHuman{view: NewView(streams), optional: true} - err := v.Resource( - mustResourceInstanceAddr("test_instance.foo"), - addTestSchemaSensitive(configschema.NestingSingle), - addrs.NewDefaultLocalProviderConfig("mytest"), cty.NilVal, - ) - if err != nil { - t.Fatal(err.Error()) - } - - 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" "foo" { - provider = mytest - - ami = null # OPTIONAL string - disks = { # OPTIONAL object - mount_point = null # OPTIONAL string - size = null # OPTIONAL string - } - id = null # OPTIONAL string - root_block_device { # OPTIONAL block - volume_type = null # OPTIONAL string - } -} -` - output := done(t) - if output.Stdout() != expected { - t.Errorf("wrong result: %s", cmp.Diff(expected, output.Stdout())) - } - }) - - t.Run("from state", func(t *testing.T) { - streams, done := terminal.StreamsForTesting(t) - v := addHuman{view: NewView(streams), optional: true} - val := cty.ObjectVal(map[string]cty.Value{ - "ami": cty.StringVal("ami-123456789"), - "disks": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/mnt/foo"), - "size": cty.StringVal("50GB"), - }), - }) - - err := v.Resource( - mustResourceInstanceAddr("test_instance.foo"), - addTestSchemaSensitive(configschema.NestingSingle), - addrs.NewDefaultLocalProviderConfig("mytest"), val, - ) - if err != nil { - t.Fatal(err.Error()) - } - - 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" "foo" { - provider = mytest - - ami = "ami-123456789" - disks = {} # sensitive - id = null -} -` - output := done(t) - if output.Stdout() != expected { - t.Errorf("wrong result: %s", cmp.Diff(expected, output.Stdout())) - } - }) - -} - -func TestAdd_writeConfigAttributes(t *testing.T) { - tests := map[string]struct { - attrs map[string]*configschema.Attribute - expected string - }{ - "empty returns nil": { - map[string]*configschema.Attribute{}, - "", - }, - "attributes": { - map[string]*configschema.Attribute{ - "ami": { - Type: cty.Number, - Required: true, - }, - "boot_disk": { - Type: cty.String, - Optional: true, - }, - "password": { - Type: cty.String, - Optional: true, - Sensitive: true, // sensitivity is ignored when printing blank templates - }, - }, - `ami = null # REQUIRED number -boot_disk = null # OPTIONAL string -password = null # OPTIONAL string -`, - }, - "attributes with nested types": { - map[string]*configschema.Attribute{ - "ami": { - Type: cty.Number, - Required: true, - }, - "disks": { - NestedType: &configschema.Object{ - Nesting: configschema.NestingSingle, - Attributes: map[string]*configschema.Attribute{ - "size": { - Type: cty.Number, - Optional: true, - }, - "mount_point": { - Type: cty.String, - Required: true, - }, - }, - }, - Optional: true, - }, - }, - `ami = null # REQUIRED number -disks = { # OPTIONAL object - mount_point = null # REQUIRED string - size = null # OPTIONAL number -} -`, - }, - } - - v := addHuman{optional: true} - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - var buf strings.Builder - if err := v.writeConfigAttributes(&buf, test.attrs, 0); err != nil { - t.Errorf("unexpected error") - } - if buf.String() != test.expected { - t.Errorf("wrong result: %s", cmp.Diff(test.expected, buf.String())) - } - }) - } -} - -func TestAdd_writeConfigAttributesFromExisting(t *testing.T) { - attrs := map[string]*configschema.Attribute{ - "ami": { - Type: cty.Number, - Required: true, - }, - "boot_disk": { - Type: cty.String, - Optional: true, - }, - "password": { - Type: cty.String, - Optional: true, - Sensitive: true, - }, - "disks": { - NestedType: &configschema.Object{ - Nesting: configschema.NestingSingle, - Attributes: map[string]*configschema.Attribute{ - "size": { - Type: cty.Number, - Optional: true, - }, - "mount_point": { - Type: cty.String, - Required: true, - }, - }, - }, - Optional: true, - }, - } - - tests := map[string]struct { - attrs map[string]*configschema.Attribute - val cty.Value - expected string - }{ - "empty returns nil": { - map[string]*configschema.Attribute{}, - cty.NilVal, - "", - }, - "mixed attributes": { - attrs, - cty.ObjectVal(map[string]cty.Value{ - "ami": cty.NumberIntVal(123456), - "boot_disk": cty.NullVal(cty.String), - "password": cty.StringVal("i am secret"), - "disks": cty.ObjectVal(map[string]cty.Value{ - "size": cty.NumberIntVal(50), - "mount_point": cty.NullVal(cty.String), - }), - }), - `ami = 123456 -boot_disk = null -disks = { - mount_point = null - size = 50 -} -password = null # sensitive -`, - }, - } - - v := addHuman{optional: true} - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - var buf strings.Builder - if err := v.writeConfigAttributesFromExisting(&buf, test.val, test.attrs, 0); err != nil { - t.Errorf("unexpected error") - } - if buf.String() != test.expected { - t.Errorf("wrong result: %s", cmp.Diff(test.expected, buf.String())) - } - }) - } -} - -func TestAdd_writeConfigBlocks(t *testing.T) { - t.Run("NestingSingle", func(t *testing.T) { - v := addHuman{optional: true} - schema := addTestSchema(configschema.NestingSingle) - var buf strings.Builder - v.writeConfigBlocks(&buf, schema.BlockTypes, 0) - - expected := `network_rules { # REQUIRED block - ip_address = null # OPTIONAL string -} -root_block_device { # OPTIONAL block - volume_type = null # OPTIONAL string -} -` - - if !cmp.Equal(buf.String(), expected) { - t.Errorf("wrong output:\n%s", cmp.Diff(expected, buf.String())) - } - }) - - t.Run("NestingList", func(t *testing.T) { - v := addHuman{optional: true} - schema := addTestSchema(configschema.NestingList) - var buf strings.Builder - v.writeConfigBlocks(&buf, schema.BlockTypes, 0) - - expected := `network_rules { # REQUIRED block - ip_address = null # OPTIONAL string -} -root_block_device { # OPTIONAL block - volume_type = null # OPTIONAL string -} -` - - if !cmp.Equal(buf.String(), expected) { - t.Fatalf("wrong output:\n%s", cmp.Diff(expected, buf.String())) - } - }) - - t.Run("NestingSet", func(t *testing.T) { - v := addHuman{optional: true} - schema := addTestSchema(configschema.NestingSet) - var buf strings.Builder - v.writeConfigBlocks(&buf, schema.BlockTypes, 0) - - expected := `network_rules { # REQUIRED block - ip_address = null # OPTIONAL string -} -root_block_device { # OPTIONAL block - volume_type = null # OPTIONAL string -} -` - - if !cmp.Equal(buf.String(), expected) { - t.Fatalf("wrong output:\n%s", cmp.Diff(expected, buf.String())) - } - }) - - t.Run("NestingMap", func(t *testing.T) { - v := addHuman{optional: true} - schema := addTestSchema(configschema.NestingMap) - var buf strings.Builder - v.writeConfigBlocks(&buf, schema.BlockTypes, 0) - - expected := `network_rules "key" { # REQUIRED block - ip_address = null # OPTIONAL string -} -root_block_device "key" { # OPTIONAL block - volume_type = null # OPTIONAL string -} -` - - if !cmp.Equal(buf.String(), expected) { - t.Fatalf("wrong output:\n%s", cmp.Diff(expected, buf.String())) - } - }) -} - -func TestAdd_writeConfigBlocksFromExisting(t *testing.T) { - - t.Run("NestingSingle", func(t *testing.T) { - v := addHuman{optional: true} - val := cty.ObjectVal(map[string]cty.Value{ - "root_block_device": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("foo"), - }), - }) - schema := addTestSchema(configschema.NestingSingle) - var buf strings.Builder - v.writeConfigBlocksFromExisting(&buf, val, schema.BlockTypes, 0) - - expected := `root_block_device { - volume_type = "foo" -} -` - - if !cmp.Equal(buf.String(), expected) { - t.Errorf("wrong output:\n%s", cmp.Diff(expected, buf.String())) - } - }) - - t.Run("NestingSingle_marked_attr", func(t *testing.T) { - v := addHuman{optional: true} - val := cty.ObjectVal(map[string]cty.Value{ - "root_block_device": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("foo").Mark(marks.Sensitive), - }), - }) - schema := addTestSchema(configschema.NestingSingle) - var buf strings.Builder - v.writeConfigBlocksFromExisting(&buf, val, schema.BlockTypes, 0) - - expected := `root_block_device { - volume_type = null # sensitive -} -` - - if !cmp.Equal(buf.String(), expected) { - t.Errorf("wrong output:\n%s", cmp.Diff(expected, buf.String())) - } - }) - - t.Run("NestingSingle_entirely_marked", func(t *testing.T) { - v := addHuman{optional: true} - val := cty.ObjectVal(map[string]cty.Value{ - "root_block_device": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("foo"), - }), - }).Mark(marks.Sensitive) - schema := addTestSchema(configschema.NestingSingle) - var buf strings.Builder - v.writeConfigBlocksFromExisting(&buf, val, schema.BlockTypes, 0) - - expected := `root_block_device {} # sensitive -` - - if !cmp.Equal(buf.String(), expected) { - t.Errorf("wrong output:\n%s", cmp.Diff(expected, buf.String())) - } - }) - - t.Run("NestingList", func(t *testing.T) { - v := addHuman{optional: true} - val := cty.ObjectVal(map[string]cty.Value{ - "root_block_device": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("foo"), - }), - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("bar"), - }), - }), - }) - schema := addTestSchema(configschema.NestingList) - var buf strings.Builder - v.writeConfigBlocksFromExisting(&buf, val, schema.BlockTypes, 0) - - expected := `root_block_device { - volume_type = "foo" -} -root_block_device { - volume_type = "bar" -} -` - - if !cmp.Equal(buf.String(), expected) { - t.Fatalf("wrong output:\n%s", cmp.Diff(expected, buf.String())) - } - }) - - t.Run("NestingList_marked_attr", func(t *testing.T) { - v := addHuman{optional: true} - val := cty.ObjectVal(map[string]cty.Value{ - "root_block_device": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("foo").Mark(marks.Sensitive), - }), - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("bar"), - }), - }), - }) - schema := addTestSchema(configschema.NestingList) - var buf strings.Builder - v.writeConfigBlocksFromExisting(&buf, val, schema.BlockTypes, 0) - - expected := `root_block_device { - volume_type = null # sensitive -} -root_block_device { - volume_type = "bar" -} -` - - if !cmp.Equal(buf.String(), expected) { - t.Fatalf("wrong output:\n%s", cmp.Diff(expected, buf.String())) - } - }) - - t.Run("NestingList_entirely_marked", func(t *testing.T) { - v := addHuman{optional: true} - val := cty.ObjectVal(map[string]cty.Value{ - "root_block_device": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("foo"), - }), - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("bar"), - }), - }).Mark(marks.Sensitive), - }) - schema := addTestSchema(configschema.NestingList) - var buf strings.Builder - v.writeConfigBlocksFromExisting(&buf, val, schema.BlockTypes, 0) - - expected := `root_block_device {} # sensitive -` - - if !cmp.Equal(buf.String(), expected) { - t.Fatalf("wrong output:\n%s", cmp.Diff(expected, buf.String())) - } - }) - - t.Run("NestingSet", func(t *testing.T) { - v := addHuman{optional: true} - val := cty.ObjectVal(map[string]cty.Value{ - "root_block_device": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("foo"), - }), - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("bar"), - }), - }), - }) - schema := addTestSchema(configschema.NestingSet) - var buf strings.Builder - v.writeConfigBlocksFromExisting(&buf, val, schema.BlockTypes, 0) - - expected := `root_block_device { - volume_type = "bar" -} -root_block_device { - volume_type = "foo" -} -` - - if !cmp.Equal(buf.String(), expected) { - t.Fatalf("wrong output:\n%s", cmp.Diff(expected, buf.String())) - } - }) - - t.Run("NestingSet_marked", func(t *testing.T) { - v := addHuman{optional: true} - // In cty.Sets, the entire set ends up marked if any element is marked. - val := cty.ObjectVal(map[string]cty.Value{ - "root_block_device": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("foo"), - }), - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("bar"), - }), - }).Mark(marks.Sensitive), - }) - schema := addTestSchema(configschema.NestingSet) - var buf strings.Builder - v.writeConfigBlocksFromExisting(&buf, val, schema.BlockTypes, 0) - - // When the entire set of blocks is sensitive, we only print one block. - expected := `root_block_device {} # sensitive -` - - if !cmp.Equal(buf.String(), expected) { - t.Fatalf("wrong output:\n%s", cmp.Diff(expected, buf.String())) - } - }) - - t.Run("NestingMap", func(t *testing.T) { - v := addHuman{optional: true} - val := cty.ObjectVal(map[string]cty.Value{ - "root_block_device": cty.MapVal(map[string]cty.Value{ - "1": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("foo"), - }), - "2": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("bar"), - }), - }), - }) - schema := addTestSchema(configschema.NestingMap) - var buf strings.Builder - v.writeConfigBlocksFromExisting(&buf, val, schema.BlockTypes, 0) - - expected := `root_block_device "1" { - volume_type = "foo" -} -root_block_device "2" { - volume_type = "bar" -} -` - - if !cmp.Equal(buf.String(), expected) { - t.Fatalf("wrong output:\n%s", cmp.Diff(expected, buf.String())) - } - }) - - t.Run("NestingMap_marked", func(t *testing.T) { - v := addHuman{optional: true} - val := cty.ObjectVal(map[string]cty.Value{ - "root_block_device": cty.MapVal(map[string]cty.Value{ - "1": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("foo").Mark(marks.Sensitive), - }), - "2": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("bar"), - }), - }), - }) - schema := addTestSchema(configschema.NestingMap) - var buf strings.Builder - v.writeConfigBlocksFromExisting(&buf, val, schema.BlockTypes, 0) - - expected := `root_block_device "1" { - volume_type = null # sensitive -} -root_block_device "2" { - volume_type = "bar" -} -` - - if !cmp.Equal(buf.String(), expected) { - t.Fatalf("wrong output:\n%s", cmp.Diff(expected, buf.String())) - } - }) - - t.Run("NestingMap_entirely_marked", func(t *testing.T) { - v := addHuman{optional: true} - val := cty.ObjectVal(map[string]cty.Value{ - "root_block_device": cty.MapVal(map[string]cty.Value{ - "1": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("foo"), - }), - "2": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("bar"), - }), - }).Mark(marks.Sensitive), - }) - schema := addTestSchema(configschema.NestingMap) - var buf strings.Builder - v.writeConfigBlocksFromExisting(&buf, val, schema.BlockTypes, 0) - - expected := `root_block_device {} # sensitive -` - - if !cmp.Equal(buf.String(), expected) { - t.Fatalf("wrong output:\n%s", cmp.Diff(expected, buf.String())) - } - }) - - t.Run("NestingMap_marked_elem", func(t *testing.T) { - v := addHuman{optional: true} - val := cty.ObjectVal(map[string]cty.Value{ - "root_block_device": cty.MapVal(map[string]cty.Value{ - "1": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("foo"), - }), - "2": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("bar"), - }).Mark(marks.Sensitive), - }), - }) - schema := addTestSchema(configschema.NestingMap) - var buf strings.Builder - v.writeConfigBlocksFromExisting(&buf, val, schema.BlockTypes, 0) - - expected := `root_block_device "1" { - volume_type = "foo" -} -root_block_device "2" {} # sensitive -` - - if !cmp.Equal(buf.String(), expected) { - t.Fatalf("wrong output:\n%s", cmp.Diff(expected, buf.String())) - } - }) -} - -func TestAdd_writeConfigNestedTypeAttribute(t *testing.T) { - t.Run("NestingSingle", func(t *testing.T) { - v := addHuman{optional: true} - schema := addTestSchema(configschema.NestingSingle) - var buf strings.Builder - v.writeConfigNestedTypeAttribute(&buf, "disks", schema.Attributes["disks"], 0) - - expected := `disks = { # OPTIONAL object - mount_point = null # OPTIONAL string - size = null # OPTIONAL string -} -` - - if !cmp.Equal(buf.String(), expected) { - t.Fatalf("wrong output:\n%s", cmp.Diff(expected, buf.String())) - } - }) - - t.Run("NestingList", func(t *testing.T) { - v := addHuman{optional: true} - schema := addTestSchema(configschema.NestingList) - var buf strings.Builder - v.writeConfigNestedTypeAttribute(&buf, "disks", schema.Attributes["disks"], 0) - - expected := `disks = [{ # OPTIONAL list of object - mount_point = null # OPTIONAL string - size = null # OPTIONAL string -}] -` - - if !cmp.Equal(buf.String(), expected) { - t.Fatalf("wrong output:\n%s", cmp.Diff(expected, buf.String())) - } - }) - - t.Run("NestingSet", func(t *testing.T) { - v := addHuman{optional: true} - schema := addTestSchema(configschema.NestingSet) - var buf strings.Builder - v.writeConfigNestedTypeAttribute(&buf, "disks", schema.Attributes["disks"], 0) - - expected := `disks = [{ # OPTIONAL set of object - mount_point = null # OPTIONAL string - size = null # OPTIONAL string -}] -` - - if !cmp.Equal(buf.String(), expected) { - t.Fatalf("wrong output:\n%s", cmp.Diff(expected, buf.String())) - } - }) - - t.Run("NestingMap", func(t *testing.T) { - v := addHuman{optional: true} - schema := addTestSchema(configschema.NestingMap) - var buf strings.Builder - v.writeConfigNestedTypeAttribute(&buf, "disks", schema.Attributes["disks"], 0) - - expected := `disks = { # OPTIONAL map of object - key = { - mount_point = null # OPTIONAL string - size = null # OPTIONAL string - } -} -` - - if !cmp.Equal(buf.String(), expected) { - t.Fatalf("wrong output:\n%s", cmp.Diff(expected, buf.String())) - } - }) -} - -func TestAdd_WriteConfigNestedTypeAttributeFromExisting(t *testing.T) { - t.Run("NestingSingle", func(t *testing.T) { - v := addHuman{optional: true} - val := cty.ObjectVal(map[string]cty.Value{ - "disks": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/mnt/foo"), - "size": cty.StringVal("50GB"), - }), - }) - schema := addTestSchema(configschema.NestingSingle) - var buf strings.Builder - v.writeConfigNestedTypeAttributeFromExisting(&buf, "disks", schema.Attributes["disks"], val, 0) - - expected := `disks = { - mount_point = "/mnt/foo" - size = "50GB" -} -` - - if !cmp.Equal(buf.String(), expected) { - t.Fatalf("wrong output:\n%s", cmp.Diff(expected, buf.String())) - } - }) - - t.Run("NestingSingle_sensitive", func(t *testing.T) { - v := addHuman{optional: true} - val := cty.ObjectVal(map[string]cty.Value{ - "disks": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/mnt/foo"), - "size": cty.StringVal("50GB"), - }), - }) - schema := addTestSchemaSensitive(configschema.NestingSingle) - var buf strings.Builder - v.writeConfigNestedTypeAttributeFromExisting(&buf, "disks", schema.Attributes["disks"], val, 0) - - expected := `disks = {} # sensitive -` - - if !cmp.Equal(buf.String(), expected) { - t.Fatalf("wrong output:\n%s", cmp.Diff(expected, buf.String())) - } - }) - - t.Run("NestingList", func(t *testing.T) { - v := addHuman{optional: true} - val := cty.ObjectVal(map[string]cty.Value{ - "disks": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/mnt/foo"), - "size": cty.StringVal("50GB"), - }), - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/mnt/bar"), - "size": cty.StringVal("250GB"), - }), - }), - }) - - schema := addTestSchema(configschema.NestingList) - var buf strings.Builder - v.writeConfigNestedTypeAttributeFromExisting(&buf, "disks", schema.Attributes["disks"], val, 0) - - expected := `disks = [ - { - mount_point = "/mnt/foo" - size = "50GB" - }, - { - mount_point = "/mnt/bar" - size = "250GB" - }, -] -` - - if !cmp.Equal(buf.String(), expected) { - t.Fatalf("wrong output:\n%s", cmp.Diff(expected, buf.String())) - } - }) - - t.Run("NestingList - marked", func(t *testing.T) { - v := addHuman{optional: true} - val := cty.ObjectVal(map[string]cty.Value{ - "disks": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/mnt/foo"), - "size": cty.StringVal("50GB").Mark(marks.Sensitive), - }), - // This is an odd example, where the entire element is marked. - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/mnt/bar"), - "size": cty.StringVal("250GB"), - }).Mark(marks.Sensitive), - }), - }) - - schema := addTestSchema(configschema.NestingList) - var buf strings.Builder - v.writeConfigNestedTypeAttributeFromExisting(&buf, "disks", schema.Attributes["disks"], val, 0) - - expected := `disks = [ - { - mount_point = "/mnt/foo" - size = null # sensitive - }, - {}, # sensitive -] -` - - if !cmp.Equal(buf.String(), expected) { - t.Fatalf("wrong output:\n%s", cmp.Diff(expected, buf.String())) - } - }) - - t.Run("NestingList - entirely marked", func(t *testing.T) { - v := addHuman{optional: true} - val := cty.ObjectVal(map[string]cty.Value{ - "disks": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/mnt/foo"), - "size": cty.StringVal("50GB"), - }), - // This is an odd example, where the entire element is marked. - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/mnt/bar"), - "size": cty.StringVal("250GB"), - }), - }), - }).Mark(marks.Sensitive) - - schema := addTestSchema(configschema.NestingList) - var buf strings.Builder - v.writeConfigNestedTypeAttributeFromExisting(&buf, "disks", schema.Attributes["disks"], val, 0) - - expected := `disks = [] # sensitive -` - - if !cmp.Equal(buf.String(), expected) { - t.Fatalf("wrong output:\n%s", cmp.Diff(expected, buf.String())) - } - }) - - t.Run("NestingMap", func(t *testing.T) { - v := addHuman{optional: true} - val := cty.ObjectVal(map[string]cty.Value{ - "disks": cty.MapVal(map[string]cty.Value{ - "foo": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/mnt/foo"), - "size": cty.StringVal("50GB"), - }), - "bar": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/mnt/bar"), - "size": cty.StringVal("250GB"), - }), - }), - }) - schema := addTestSchema(configschema.NestingMap) - var buf strings.Builder - v.writeConfigNestedTypeAttributeFromExisting(&buf, "disks", schema.Attributes["disks"], val, 0) - - expected := `disks = { - bar = { - mount_point = "/mnt/bar" - size = "250GB" - } - foo = { - mount_point = "/mnt/foo" - size = "50GB" - } -} -` - - if !cmp.Equal(buf.String(), expected) { - t.Fatalf("wrong output:\n%s", cmp.Diff(expected, buf.String())) - } - }) - - t.Run("NestingMap - marked", func(t *testing.T) { - v := addHuman{optional: true} - val := cty.ObjectVal(map[string]cty.Value{ - "disks": cty.MapVal(map[string]cty.Value{ - "foo": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/mnt/foo"), - "size": cty.StringVal("50GB").Mark(marks.Sensitive), - }), - "bar": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/mnt/bar"), - "size": cty.StringVal("250GB"), - }).Mark(marks.Sensitive), - }), - }) - schema := addTestSchema(configschema.NestingMap) - var buf strings.Builder - v.writeConfigNestedTypeAttributeFromExisting(&buf, "disks", schema.Attributes["disks"], val, 0) - - expected := `disks = { - bar = {} # sensitive - foo = { - mount_point = "/mnt/foo" - size = null # sensitive - } -} -` - - if !cmp.Equal(buf.String(), expected) { - t.Fatalf("wrong output:\n%s", cmp.Diff(expected, buf.String())) - } - }) -} - -func addTestSchema(nesting configschema.NestingMode) *configschema.Block { - return &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - // Attributes which are neither optional nor required should not print. - "uuid": {Type: cty.String, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "disks": { - NestedType: &configschema.Object{ - Attributes: map[string]*configschema.Attribute{ - "mount_point": {Type: cty.String, Optional: true}, - "size": {Type: cty.String, Optional: true}, - }, - Nesting: nesting, - }, - }, - }, - BlockTypes: map[string]*configschema.NestedBlock{ - "root_block_device": { - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "volume_type": { - Type: cty.String, - Optional: true, - Computed: true, - }, - }, - }, - Nesting: nesting, - }, - "network_rules": { - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "ip_address": { - Type: cty.String, - Optional: true, - Computed: true, - }, - }, - }, - Nesting: nesting, - MinItems: 1, - }, - }, - } -} - -// addTestSchemaSensitive returns a schema with a sensitive NestedType and a -// NestedBlock with sensitive attributes. -func addTestSchemaSensitive(nesting configschema.NestingMode) *configschema.Block { - return &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - // Attributes which are neither optional nor required should not print. - "uuid": {Type: cty.String, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "disks": { - NestedType: &configschema.Object{ - Attributes: map[string]*configschema.Attribute{ - "mount_point": {Type: cty.String, Optional: true}, - "size": {Type: cty.String, Optional: true}, - }, - Nesting: nesting, - }, - Sensitive: true, - }, - }, - BlockTypes: map[string]*configschema.NestedBlock{ - "root_block_device": { - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "volume_type": { - Type: cty.String, - Optional: true, - Computed: true, - Sensitive: true, - }, - }, - }, - Nesting: nesting, - }, - }, - } -} - -func mustResourceInstanceAddr(s string) addrs.AbsResourceInstance { - addr, diags := addrs.ParseAbsResourceInstanceStr(s) - if diags.HasErrors() { - panic(diags.Err()) - } - return addr -} diff --git a/website/docs/cli/commands/add.html.md b/website/docs/cli/commands/add.html.md deleted file mode 100644 index 73364f06d..000000000 --- a/website/docs/cli/commands/add.html.md +++ /dev/null @@ -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. diff --git a/website/docs/cli/commands/index.html.md b/website/docs/cli/commands/index.html.md index 1b01d60d1..6fb5919a7 100644 --- a/website/docs/cli/commands/index.html.md +++ b/website/docs/cli/commands/index.html.md @@ -39,7 +39,6 @@ Main commands: destroy Destroy previously-created infrastructure All other commands: - add Generate a resource configuration template console Try Terraform expressions at an interactive command prompt fmt Reformat your configuration in the standard style force-unlock Release a stuck lock on the current workspace diff --git a/website/layouts/docs.erb b/website/layouts/docs.erb index da4f0f6c9..602a490e5 100644 --- a/website/layouts/docs.erb +++ b/website/layouts/docs.erb @@ -74,10 +74,6 @@ Overview -
  • - add -
  • -
  • console
  • @@ -361,10 +357,6 @@ Alphabetical List of Commands