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 }