mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-30 10:47:14 -06:00
1f57b7a8bd
`terraform add`: `-out` option append to existing config & optionally check resource existance
370 lines
10 KiB
Go
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
|
|
}
|