opentofu/internal/command/add.go
Alisdair McDiarmid 1f57b7a8bd
Merge pull request #29235 from magodo/terraform_add_output_append
`terraform add`: `-out` option append to existing config & optionally check resource existance
2021-09-17 11:19:55 -04:00

370 lines
10 KiB
Go

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
}