github.com/jaredpalmer/terraform@v1.1.0-alpha20210908.0.20210911170307-88705c943a03/internal/command/add.go (about)

     1  package command
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"strings"
     7  
     8  	"github.com/hashicorp/hcl/v2"
     9  	"github.com/hashicorp/terraform/internal/addrs"
    10  	"github.com/hashicorp/terraform/internal/backend"
    11  	"github.com/hashicorp/terraform/internal/command/arguments"
    12  	"github.com/hashicorp/terraform/internal/command/views"
    13  	"github.com/hashicorp/terraform/internal/configs"
    14  	"github.com/hashicorp/terraform/internal/states"
    15  	"github.com/hashicorp/terraform/internal/tfdiags"
    16  	"github.com/zclconf/go-cty/cty"
    17  )
    18  
    19  // AddCommand is a Command implementation that generates resource configuration templates.
    20  type AddCommand struct {
    21  	Meta
    22  }
    23  
    24  func (c *AddCommand) Run(rawArgs []string) int {
    25  	// Parse and apply global view arguments
    26  	common, rawArgs := arguments.ParseView(rawArgs)
    27  	c.View.Configure(common)
    28  
    29  	args, diags := arguments.ParseAdd(rawArgs)
    30  	view := views.NewAdd(args.ViewType, c.View, args)
    31  	if diags.HasErrors() {
    32  		view.Diagnostics(diags)
    33  		return 1
    34  	}
    35  
    36  	// Check for user-supplied plugin path
    37  	var err error
    38  	if c.pluginPath, err = c.loadPluginPath(); err != nil {
    39  		diags = diags.Append(tfdiags.Sourceless(
    40  			tfdiags.Error,
    41  			"Error loading plugin path",
    42  			err.Error(),
    43  		))
    44  		view.Diagnostics(diags)
    45  		return 1
    46  	}
    47  
    48  	// Apply the state arguments to the meta object here because they are later
    49  	// used when initializing the backend.
    50  	c.Meta.applyStateArguments(args.State)
    51  
    52  	// Load the backend
    53  	b, backendDiags := c.Backend(nil)
    54  	diags = diags.Append(backendDiags)
    55  	if backendDiags.HasErrors() {
    56  		view.Diagnostics(diags)
    57  		return 1
    58  	}
    59  
    60  	// We require a local backend
    61  	local, ok := b.(backend.Local)
    62  	if !ok {
    63  		diags = diags.Append(tfdiags.Sourceless(
    64  			tfdiags.Error,
    65  			"Unsupported backend",
    66  			ErrUnsupportedLocalOp,
    67  		))
    68  		view.Diagnostics(diags)
    69  		return 1
    70  	}
    71  
    72  	// This is a read-only command (until -import is implemented)
    73  	c.ignoreRemoteBackendVersionConflict(b)
    74  
    75  	cwd, err := os.Getwd()
    76  	if err != nil {
    77  		diags = diags.Append(tfdiags.Sourceless(
    78  			tfdiags.Error,
    79  			"Error determining current working directory",
    80  			err.Error(),
    81  		))
    82  		view.Diagnostics(diags)
    83  		return 1
    84  	}
    85  
    86  	// Build the operation
    87  	opReq := c.Operation(b)
    88  	opReq.AllowUnsetVariables = true
    89  	opReq.ConfigDir = cwd
    90  	opReq.ConfigLoader, err = c.initConfigLoader()
    91  	if err != nil {
    92  		diags = diags.Append(tfdiags.Sourceless(
    93  			tfdiags.Error,
    94  			"Error initializing config loader",
    95  			err.Error(),
    96  		))
    97  		view.Diagnostics(diags)
    98  		return 1
    99  	}
   100  
   101  	// Get the context
   102  	lr, _, ctxDiags := local.LocalRun(opReq)
   103  	diags = diags.Append(ctxDiags)
   104  	if ctxDiags.HasErrors() {
   105  		view.Diagnostics(diags)
   106  		return 1
   107  	}
   108  
   109  	// Successfully creating the context can result in a lock, so ensure we release it
   110  	defer func() {
   111  		diags := opReq.StateLocker.Unlock()
   112  		if diags.HasErrors() {
   113  			c.showDiagnostics(diags)
   114  		}
   115  	}()
   116  
   117  	// load the configuration to verify that the resource address doesn't
   118  	// already exist in the config.
   119  	var module *configs.Module
   120  	if args.Addr.Module.IsRoot() {
   121  		module = lr.Config.Module
   122  	} else {
   123  		// This is weird, but users can potentially specify non-existant module names
   124  		cfg := lr.Config.Root.Descendent(args.Addr.Module.Module())
   125  		if cfg != nil {
   126  			module = cfg.Module
   127  		}
   128  	}
   129  
   130  	if module == nil {
   131  		// It's fine if the module doesn't actually exist; we don't need to check if the resource exists.
   132  	} else {
   133  		if rs, ok := module.ManagedResources[args.Addr.ContainingResource().Config().String()]; ok {
   134  			diags = diags.Append(&hcl.Diagnostic{
   135  				Severity: hcl.DiagError,
   136  				Summary:  "Resource already in configuration",
   137  				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),
   138  				Subject:  &rs.DeclRange,
   139  			})
   140  			c.View.Diagnostics(diags)
   141  			return 1
   142  		}
   143  	}
   144  
   145  	// Get the schemas from the context
   146  	schemas, moreDiags := lr.Core.Schemas(lr.Config, lr.InputState)
   147  	diags = diags.Append(moreDiags)
   148  	if moreDiags.HasErrors() {
   149  		view.Diagnostics(diags)
   150  		return 1
   151  	}
   152  
   153  	// Determine the correct provider config address. The provider-related
   154  	// variables may get updated below
   155  	absProviderConfig := args.Provider
   156  	var providerLocalName string
   157  	rs := args.Addr.Resource.Resource
   158  
   159  	// If we are getting the values from state, get the AbsProviderConfig
   160  	// directly from state as well.
   161  	var resource *states.Resource
   162  	if args.FromState {
   163  		resource, moreDiags = c.getResource(b, args.Addr.ContainingResource())
   164  		if moreDiags.HasErrors() {
   165  			diags = diags.Append(moreDiags)
   166  			c.View.Diagnostics(diags)
   167  			return 1
   168  		}
   169  		absProviderConfig = &resource.ProviderConfig
   170  	}
   171  
   172  	if absProviderConfig == nil {
   173  		ip := rs.ImpliedProvider()
   174  		if module != nil {
   175  			provider := module.ImpliedProviderForUnqualifiedType(ip)
   176  			providerLocalName = module.LocalNameForProvider(provider)
   177  			absProviderConfig = &addrs.AbsProviderConfig{
   178  				Provider: provider,
   179  				Module:   args.Addr.Module.Module(),
   180  			}
   181  		} else {
   182  			// lacking any configuration to query, we'll go with a default provider.
   183  			absProviderConfig = &addrs.AbsProviderConfig{
   184  				Provider: addrs.NewDefaultProvider(ip),
   185  			}
   186  			providerLocalName = ip
   187  		}
   188  	} else {
   189  		if module != nil {
   190  			providerLocalName = module.LocalNameForProvider(absProviderConfig.Provider)
   191  		} else {
   192  			providerLocalName = absProviderConfig.Provider.Type
   193  		}
   194  	}
   195  
   196  	localProviderConfig := addrs.LocalProviderConfig{
   197  		LocalName: providerLocalName,
   198  		Alias:     absProviderConfig.Alias,
   199  	}
   200  
   201  	// Get the schemas from the context
   202  	if _, exists := schemas.Providers[absProviderConfig.Provider]; !exists {
   203  		diags = diags.Append(tfdiags.Sourceless(
   204  			tfdiags.Error,
   205  			"Missing schema for provider",
   206  			fmt.Sprintf("No schema found for provider %s. Please verify that this provider exists in the configuration.", absProviderConfig.Provider.String()),
   207  		))
   208  		c.View.Diagnostics(diags)
   209  		return 1
   210  	}
   211  
   212  	// Get the schema for the resource
   213  	schema, schemaVersion := schemas.ResourceTypeConfig(absProviderConfig.Provider, rs.Mode, rs.Type)
   214  	if schema == nil {
   215  		diags = diags.Append(tfdiags.Sourceless(
   216  			tfdiags.Error,
   217  			"Missing resource schema from provider",
   218  			fmt.Sprintf("No resource schema found for %s.", rs.Type),
   219  		))
   220  		c.View.Diagnostics(diags)
   221  		return 1
   222  	}
   223  
   224  	stateVal := cty.NilVal
   225  	// Now that we have the schema, we can decode the previously-acquired resource state
   226  	if args.FromState {
   227  		ri := resource.Instance(args.Addr.Resource.Key)
   228  		if ri.Current == nil {
   229  			diags = diags.Append(tfdiags.Sourceless(
   230  				tfdiags.Error,
   231  				"No state for resource",
   232  				fmt.Sprintf("There is no state found for the resource %s, so add cannot populate values.", rs.String()),
   233  			))
   234  			c.View.Diagnostics(diags)
   235  			return 1
   236  		}
   237  
   238  		rio, err := ri.Current.Decode(schema.ImpliedType())
   239  		if err != nil {
   240  			diags = diags.Append(tfdiags.Sourceless(
   241  				tfdiags.Error,
   242  				"Error decoding state",
   243  				fmt.Sprintf("Error decoding state for resource %s: %s", rs.String(), err.Error()),
   244  			))
   245  			c.View.Diagnostics(diags)
   246  			return 1
   247  		}
   248  
   249  		if ri.Current.SchemaVersion != schemaVersion {
   250  			diags = diags.Append(tfdiags.Sourceless(
   251  				tfdiags.Error,
   252  				"Schema version mismatch",
   253  				fmt.Sprintf("schema version %d for %s in state does not match version %d from the provider", ri.Current.SchemaVersion, rs.String(), schemaVersion),
   254  			))
   255  			c.View.Diagnostics(diags)
   256  			return 1
   257  		}
   258  
   259  		stateVal = rio.Value
   260  	}
   261  
   262  	diags = diags.Append(view.Resource(args.Addr, schema, localProviderConfig, stateVal))
   263  	c.View.Diagnostics(diags)
   264  	if diags.HasErrors() {
   265  		return 1
   266  	}
   267  	return 0
   268  }
   269  
   270  func (c *AddCommand) Help() string {
   271  	helpText := `
   272  Usage: terraform [global options] add [options] ADDRESS
   273  
   274    Generates a blank resource template. With no additional options, Terraform
   275    will write the result to standard output.
   276  
   277  Options:
   278  
   279    -from-state         Fill the template with values from an existing resource
   280                        instance tracked in the state. By default, Terraform will
   281                        emit only placeholder values based on the resource type.
   282  
   283    -out=string         Write the template to a file, instead of to standard
   284                        output.
   285  
   286    -optional           Include optional arguments. By default, the result will
   287                        include only required arguments.
   288  
   289    -provider=provider  Override the provider configuration for the resource,
   290                        using the absolute provider configuration address syntax.
   291  
   292                        This is incompatible with -from-state, because in that
   293                        case Terraform will use the provider configuration already
   294                        selected in the state.
   295  `
   296  	return strings.TrimSpace(helpText)
   297  }
   298  
   299  func (c *AddCommand) Synopsis() string {
   300  	return "Generate a resource configuration template"
   301  }
   302  
   303  func (c *AddCommand) getResource(b backend.Enhanced, addr addrs.AbsResource) (*states.Resource, tfdiags.Diagnostics) {
   304  	var diags tfdiags.Diagnostics
   305  	// Get the state
   306  	env, err := c.Workspace()
   307  	if err != nil {
   308  		diags = diags.Append(tfdiags.Sourceless(
   309  			tfdiags.Error,
   310  			"Error selecting workspace",
   311  			err.Error(),
   312  		))
   313  		return nil, diags
   314  	}
   315  
   316  	stateMgr, err := b.StateMgr(env)
   317  	if err != nil {
   318  		diags = diags.Append(tfdiags.Sourceless(
   319  			tfdiags.Error,
   320  			"Error loading state",
   321  			fmt.Sprintf(errStateLoadingState, err),
   322  		))
   323  		return nil, diags
   324  	}
   325  
   326  	if err := stateMgr.RefreshState(); err != nil {
   327  		diags = diags.Append(tfdiags.Sourceless(
   328  			tfdiags.Error,
   329  			"Error refreshing state",
   330  			err.Error(),
   331  		))
   332  		return nil, diags
   333  	}
   334  
   335  	state := stateMgr.State()
   336  	if state == nil {
   337  		diags = diags.Append(tfdiags.Sourceless(
   338  			tfdiags.Error,
   339  			"No state",
   340  			"There is no state found for the current workspace, so add cannot populate values.",
   341  		))
   342  		return nil, diags
   343  	}
   344  
   345  	return state.Resource(addr), nil
   346  }