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