mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-02 12:17:39 -06:00
43d753e727
We're aware of several quirks of this command's current design, which result from some existing architectural limitations that we can't address immediately. However, we do still want to make this command available in its current capacity as an incremental improvement, so as a compromise we'll document it as experimental. Our intent here is to exclude it from the Terraform 1.0 Compatibility Promises so that we can have the space to continue to improve the design as other parts of the overall Terraform system gain new capabilities. We don't currently have any concrete plan for this command to be stabilized and subject to compatibility promises. That decision will follow from ongoing discussions with other teams whose systems may need to change in order to support the final design of "terraform add".
335 lines
9.2 KiB
Go
335 lines
9.2 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))
|
|
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
|
|
}
|