opentofu/internal/command/add.go
Kristin Laemmert 583859e510
commands: terraform add (#28874)
* command: new command, terraform add, generates resource templates

terraform add ADDRESS generates a resource configuration template with all required (and optionally optional) attributes set to null. This can optionally also pre-populate nonsesitive attributes with values from an existing resource of the same type in state (sensitive vals will be populated with null and a comment indicating sensitivity)

* website: terraform add documentation
2021-06-17 12:08:37 -04:00

330 lines
8.8 KiB
Go

package command
import (
"fmt"
"os"
"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
}
// 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
ctx, _, ctxDiags := local.Context(opReq)
diags = diags.Append(ctxDiags)
if ctxDiags.HasErrors() {
view.Diagnostics(diags)
return 1
}
// 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 = ctx.Config().Module
} else {
// This is weird, but users can potentially specify non-existant module names
cfg := ctx.Config().Root.Descendent(args.Addr.Module.Module())
if cfg != nil {
module = cfg.Module
}
}
if module == nil {
// It's fine if the module doesn't actually exist; we don't need to check if the resource exists.
} else {
if rs, ok := 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
}
}
// Get the schemas from the context
schemas := ctx.Schemas()
// 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
var moreDiags tfdiags.Diagnostics
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))
if diags.HasErrors() {
c.View.Diagnostics(diags)
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,
the template will be displayed in the terminal.
Options:
-from-state=true Fill the template with values from an existing resource.
Defaults to false.
-out=string Write the template to a file. If the file already
exists, the template will be appended to the file.
-optional=true Include optional attributes. Defaults to false.
-provider=provider Override the configured provider for the resource. Conflicts
with -from-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
}