Merge pull request #9809 from hashicorp/f-import-provider
command/import: allow configuration from files
This commit is contained in:
commit
ec55cecc70
|
@ -3,6 +3,7 @@ package command
|
|||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
|
@ -15,13 +16,22 @@ type ImportCommand struct {
|
|||
}
|
||||
|
||||
func (c *ImportCommand) Run(args []string) int {
|
||||
// Get the pwd since its our default -config flag value
|
||||
pwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
var configPath string
|
||||
args = c.Meta.process(args, true)
|
||||
|
||||
cmdFlags := c.Meta.flagSet("import")
|
||||
cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
|
||||
cmdFlags.IntVar(&c.Meta.parallelism, "parallelism", 0, "parallelism")
|
||||
cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path")
|
||||
cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path")
|
||||
cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path")
|
||||
cmdFlags.StringVar(&configPath, "config", pwd, "path")
|
||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||
if err := cmdFlags.Parse(args); err != nil {
|
||||
return 1
|
||||
|
@ -36,6 +46,8 @@ func (c *ImportCommand) Run(args []string) int {
|
|||
|
||||
// Build the context based on the arguments given
|
||||
ctx, _, err := c.Context(contextOpts{
|
||||
Path: configPath,
|
||||
PathEmptyOk: true,
|
||||
StatePath: c.Meta.statePath,
|
||||
Parallelism: c.Meta.parallelism,
|
||||
})
|
||||
|
@ -111,6 +123,11 @@ Options:
|
|||
modifying. Defaults to the "-state-out" path with
|
||||
".backup" extension. Set to "-" to disable backup.
|
||||
|
||||
-config=path Path to a directory of Terraform configuration files
|
||||
to use to configure the provider. Defaults to pwd.
|
||||
If no config files are present, they must be provided
|
||||
via the input prompts or env vars.
|
||||
|
||||
-input=true Ask for input for variables if not directly set.
|
||||
|
||||
-no-color If specified, output won't contain any color.
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
|
@ -45,6 +46,119 @@ func TestImport(t *testing.T) {
|
|||
testStateOutput(t, statePath, testImportStr)
|
||||
}
|
||||
|
||||
func TestImport_providerConfig(t *testing.T) {
|
||||
defer testChdir(t, testFixturePath("import-provider"))()
|
||||
|
||||
statePath := testTempFile(t)
|
||||
|
||||
p := testProvider()
|
||||
ui := new(cli.MockUi)
|
||||
c := &ImportCommand{
|
||||
Meta: Meta{
|
||||
ContextOpts: testCtxConfig(p),
|
||||
Ui: ui,
|
||||
},
|
||||
}
|
||||
|
||||
p.ImportStateFn = nil
|
||||
p.ImportStateReturn = []*terraform.InstanceState{
|
||||
&terraform.InstanceState{
|
||||
ID: "yay",
|
||||
Ephemeral: terraform.EphemeralState{
|
||||
Type: "test_instance",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
configured := false
|
||||
p.ConfigureFn = func(c *terraform.ResourceConfig) error {
|
||||
configured = true
|
||||
|
||||
if v, ok := c.Get("foo"); !ok || v.(string) != "bar" {
|
||||
return fmt.Errorf("bad value: %#v", v)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"-state", statePath,
|
||||
"test_instance.foo",
|
||||
"bar",
|
||||
}
|
||||
if code := c.Run(args); code != 0 {
|
||||
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||
}
|
||||
|
||||
// Verify that we were called
|
||||
if !configured {
|
||||
t.Fatal("Configure should be called")
|
||||
}
|
||||
|
||||
if !p.ImportStateCalled {
|
||||
t.Fatal("ImportState should be called")
|
||||
}
|
||||
|
||||
testStateOutput(t, statePath, testImportStr)
|
||||
}
|
||||
|
||||
func TestImport_providerConfigDisable(t *testing.T) {
|
||||
defer testChdir(t, testFixturePath("import-provider"))()
|
||||
|
||||
statePath := testTempFile(t)
|
||||
|
||||
p := testProvider()
|
||||
ui := new(cli.MockUi)
|
||||
c := &ImportCommand{
|
||||
Meta: Meta{
|
||||
ContextOpts: testCtxConfig(p),
|
||||
Ui: ui,
|
||||
},
|
||||
}
|
||||
|
||||
p.ImportStateFn = nil
|
||||
p.ImportStateReturn = []*terraform.InstanceState{
|
||||
&terraform.InstanceState{
|
||||
ID: "yay",
|
||||
Ephemeral: terraform.EphemeralState{
|
||||
Type: "test_instance",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
configured := false
|
||||
p.ConfigureFn = func(c *terraform.ResourceConfig) error {
|
||||
configured = true
|
||||
|
||||
if v, ok := c.Get("foo"); ok {
|
||||
return fmt.Errorf("bad value: %#v", v)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"-state", statePath,
|
||||
"-config", "",
|
||||
"test_instance.foo",
|
||||
"bar",
|
||||
}
|
||||
if code := c.Run(args); code != 0 {
|
||||
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||
}
|
||||
|
||||
// Verify that we were called
|
||||
if !configured {
|
||||
t.Fatal("Configure should be called")
|
||||
}
|
||||
|
||||
if !p.ImportStateCalled {
|
||||
t.Fatal("ImportState should be called")
|
||||
}
|
||||
|
||||
testStateOutput(t, statePath, testImportStr)
|
||||
}
|
||||
|
||||
/*
|
||||
func TestRefresh_badState(t *testing.T) {
|
||||
p := testProvider()
|
||||
|
|
|
@ -5,11 +5,14 @@ import (
|
|||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
"github.com/hashicorp/go-getter"
|
||||
"github.com/hashicorp/terraform/config"
|
||||
"github.com/hashicorp/terraform/config/module"
|
||||
"github.com/hashicorp/terraform/helper/experiment"
|
||||
"github.com/hashicorp/terraform/state"
|
||||
|
@ -163,6 +166,17 @@ func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) {
|
|||
var mod *module.Tree
|
||||
if copts.Path != "" {
|
||||
mod, err = module.NewTreeModule("", copts.Path)
|
||||
|
||||
// Check for the error where we have no config files but
|
||||
// allow that. If that happens, clear the error.
|
||||
if errwrap.ContainsType(err, new(config.ErrNoConfigsFound)) &&
|
||||
copts.PathEmptyOk {
|
||||
log.Printf(
|
||||
"[WARN] Empty configuration dir, ignoring: %s", copts.Path)
|
||||
err = nil
|
||||
mod = module.NewEmptyTree()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("Error loading config: %s", err)
|
||||
}
|
||||
|
@ -495,7 +509,11 @@ func (m *Meta) outputShadowError(err error, output bool) bool {
|
|||
// contextOpts are the options used to load a context from a command.
|
||||
type contextOpts struct {
|
||||
// Path to the directory where the root module is.
|
||||
Path string
|
||||
//
|
||||
// PathEmptyOk, when set, will allow paths that have no Terraform
|
||||
// configurations. The result in that case will be an empty module.
|
||||
Path string
|
||||
PathEmptyOk bool
|
||||
|
||||
// StatePath is the path to the state file. If this is empty, then
|
||||
// no state will be loaded. It is also okay for this to be a path to
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
provider "test" {
|
||||
foo = "bar"
|
||||
}
|
|
@ -12,6 +12,18 @@ import (
|
|||
"github.com/hashicorp/hcl"
|
||||
)
|
||||
|
||||
// ErrNoConfigsFound is the error returned by LoadDir if no
|
||||
// Terraform configuration files were found in the given directory.
|
||||
type ErrNoConfigsFound struct {
|
||||
Dir string
|
||||
}
|
||||
|
||||
func (e ErrNoConfigsFound) Error() string {
|
||||
return fmt.Sprintf(
|
||||
"No Terraform configuration files found in directory: %s",
|
||||
e.Dir)
|
||||
}
|
||||
|
||||
// LoadJSON loads a single Terraform configuration from a given JSON document.
|
||||
//
|
||||
// The document must be a complete Terraform configuration. This function will
|
||||
|
@ -69,9 +81,7 @@ func LoadDir(root string) (*Config, error) {
|
|||
return nil, err
|
||||
}
|
||||
if len(files) == 0 {
|
||||
return nil, fmt.Errorf(
|
||||
"No Terraform configuration files found in directory: %s",
|
||||
root)
|
||||
return nil, &ErrNoConfigsFound{Dir: root}
|
||||
}
|
||||
|
||||
// Determine the absolute path to the directory.
|
||||
|
|
|
@ -8,6 +8,10 @@ import (
|
|||
"testing"
|
||||
)
|
||||
|
||||
func TestErrNoConfigsFound_impl(t *testing.T) {
|
||||
var _ error = new(ErrNoConfigsFound)
|
||||
}
|
||||
|
||||
func TestIsEmptyDir(t *testing.T) {
|
||||
val, err := IsEmptyDir(fixtureDir)
|
||||
if err != nil {
|
||||
|
|
|
@ -43,10 +43,17 @@ func (c *Context) Import(opts *ImportOpts) (*State, error) {
|
|||
// Copy our own state
|
||||
c.state = c.state.DeepCopy()
|
||||
|
||||
// If no module is given, default to the module configured with
|
||||
// the Context.
|
||||
module := opts.Module
|
||||
if module == nil {
|
||||
module = c.module
|
||||
}
|
||||
|
||||
// Initialize our graph builder
|
||||
builder := &ImportGraphBuilder{
|
||||
ImportTargets: opts.Targets,
|
||||
Module: opts.Module,
|
||||
Module: module,
|
||||
Providers: c.components.ResourceProviders(),
|
||||
}
|
||||
|
||||
|
|
|
@ -209,6 +209,91 @@ func TestContextImport_moduleProvider(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// Test that import will interpolate provider configuration and use
|
||||
// that configuration for import.
|
||||
func TestContextImport_providerVarConfig(t *testing.T) {
|
||||
p := testProvider("aws")
|
||||
ctx := testContext2(t, &ContextOpts{
|
||||
Module: testModule(t, "import-provider-vars"),
|
||||
Providers: map[string]ResourceProviderFactory{
|
||||
"aws": testProviderFuncFixed(p),
|
||||
},
|
||||
Variables: map[string]interface{}{
|
||||
"foo": "bar",
|
||||
},
|
||||
})
|
||||
|
||||
configured := false
|
||||
p.ConfigureFn = func(c *ResourceConfig) error {
|
||||
configured = true
|
||||
|
||||
if v, ok := c.Get("foo"); !ok || v.(string) != "bar" {
|
||||
return fmt.Errorf("bad value: %#v", v)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
p.ImportStateReturn = []*InstanceState{
|
||||
&InstanceState{
|
||||
ID: "foo",
|
||||
Ephemeral: EphemeralState{Type: "aws_instance"},
|
||||
},
|
||||
}
|
||||
|
||||
state, err := ctx.Import(&ImportOpts{
|
||||
Targets: []*ImportTarget{
|
||||
&ImportTarget{
|
||||
Addr: "aws_instance.foo",
|
||||
ID: "bar",
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if !configured {
|
||||
t.Fatal("didn't configure provider")
|
||||
}
|
||||
|
||||
actual := strings.TrimSpace(state.String())
|
||||
expected := strings.TrimSpace(testImportStr)
|
||||
if actual != expected {
|
||||
t.Fatalf("bad: \n%s", actual)
|
||||
}
|
||||
}
|
||||
|
||||
// Test that provider configs can't reference resources.
|
||||
func TestContextImport_providerNonVarConfig(t *testing.T) {
|
||||
p := testProvider("aws")
|
||||
ctx := testContext2(t, &ContextOpts{
|
||||
Module: testModule(t, "import-provider-non-vars"),
|
||||
Providers: map[string]ResourceProviderFactory{
|
||||
"aws": testProviderFuncFixed(p),
|
||||
},
|
||||
})
|
||||
|
||||
p.ImportStateReturn = []*InstanceState{
|
||||
&InstanceState{
|
||||
ID: "foo",
|
||||
Ephemeral: EphemeralState{Type: "aws_instance"},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := ctx.Import(&ImportOpts{
|
||||
Targets: []*ImportTarget{
|
||||
&ImportTarget{
|
||||
Addr: "aws_instance.foo",
|
||||
ID: "bar",
|
||||
},
|
||||
},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("should error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextImport_refresh(t *testing.T) {
|
||||
p := testProvider("aws")
|
||||
ctx := testContext2(t, &ContextOpts{
|
||||
|
|
|
@ -36,6 +36,14 @@ func (b *ImportGraphBuilder) Steps() []GraphTransformer {
|
|||
mod = module.NewEmptyTree()
|
||||
}
|
||||
|
||||
// Custom factory for creating providers.
|
||||
providerFactory := func(name string, path []string) GraphNodeProvider {
|
||||
return &NodeApplyableProvider{
|
||||
NameValue: name,
|
||||
PathValue: path,
|
||||
}
|
||||
}
|
||||
|
||||
steps := []GraphTransformer{
|
||||
// Create all our resources from the configuration and state
|
||||
&ConfigTransformerOld{Module: mod},
|
||||
|
@ -44,17 +52,18 @@ func (b *ImportGraphBuilder) Steps() []GraphTransformer {
|
|||
&ImportStateTransformer{Targets: b.ImportTargets},
|
||||
|
||||
// Provider-related transformations
|
||||
&MissingProviderTransformer{Providers: b.Providers},
|
||||
&MissingProviderTransformer{Providers: b.Providers, Factory: providerFactory},
|
||||
&ProviderTransformer{},
|
||||
&DisableProviderTransformerOld{},
|
||||
&PruneProviderTransformer{},
|
||||
&AttachProviderConfigTransformer{Module: mod},
|
||||
|
||||
// This validates that the providers only depend on variables
|
||||
&ImportProviderValidateTransformer{},
|
||||
|
||||
// Single root
|
||||
&RootTransformer{},
|
||||
|
||||
// Insert nodes to close opened plugin connections
|
||||
&CloseProviderTransformer{},
|
||||
|
||||
// Optimize
|
||||
&TransitiveReductionTransformer{},
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
provider "aws" {
|
||||
foo = "${aws_instance.foo.bar}"
|
||||
}
|
||||
|
||||
resource "aws_instance" "foo" {
|
||||
bar = "value"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
variable "foo" {}
|
||||
|
||||
provider "aws" {
|
||||
foo = "${var.foo}"
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package terraform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ImportProviderValidateTransformer is a GraphTransformer that goes through
|
||||
// the providers in the graph and validates that they only depend on variables.
|
||||
type ImportProviderValidateTransformer struct{}
|
||||
|
||||
func (t *ImportProviderValidateTransformer) Transform(g *Graph) error {
|
||||
for _, v := range g.Vertices() {
|
||||
// We only care about providers
|
||||
pv, ok := v.(GraphNodeProvider)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// We only care about providers that reference things
|
||||
rn, ok := pv.(GraphNodeReferencer)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, ref := range rn.References() {
|
||||
if !strings.HasPrefix(ref, "var.") {
|
||||
return fmt.Errorf(
|
||||
"Provider %q depends on non-var %q. Providers for import can currently\n"+
|
||||
"only depend on variables or must be hardcoded. You can stop import\n"+
|
||||
"from loading configurations by specifying `-config=\"\"`.",
|
||||
pv.ProviderName(), ref)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -35,6 +35,11 @@ The command-line flags are all optional. The list of available flags are:
|
|||
the `-state-out` path with the ".backup" extension. Set to "-" to disable
|
||||
backups.
|
||||
|
||||
* `-config=path` - Path to directory of Terraform configuration files that
|
||||
configure the provider for import. This defaults to your working directory.
|
||||
If this directory contains no Terraform configuration files, the provider
|
||||
must be configured via manual input or environmental variables.
|
||||
|
||||
* `-input=true` - Whether to ask for input for provider configuration.
|
||||
|
||||
* `-state=path` - The path to read and save state files (unless state-out is
|
||||
|
@ -46,12 +51,35 @@ The command-line flags are all optional. The list of available flags are:
|
|||
|
||||
## Provider Configuration
|
||||
|
||||
To access the provider that the resource is being imported from, Terraform
|
||||
will ask you for access credentials. If you don't want to be asked for input,
|
||||
verify that all environment variables for your provider are set.
|
||||
Terraform will attempt to load configuration files that configure the
|
||||
provider being used for import. If no configuration files are present or
|
||||
no configuration for that specific provider is present, Terraform will
|
||||
prompt you for access credentials. You may also specify environmental variables
|
||||
to configure the provider.
|
||||
|
||||
The import command cannot read provider configuration from a Terraform
|
||||
configuration file.
|
||||
The only limitation Terraform has when reading the configuration files
|
||||
is that the import provider configurations must not depend on non-variable
|
||||
inputs. For example, a provider configuration cannot depend on a data
|
||||
source.
|
||||
|
||||
As a working example, if you're importing AWS resources and you have a
|
||||
configuration file with the contents below, then Terraform will configure
|
||||
the AWS provider with this file.
|
||||
|
||||
```
|
||||
variable "access_key" {}
|
||||
variable "secret_key" {}
|
||||
|
||||
provider "aws" {
|
||||
access_key = "${var.access_key}"
|
||||
secret_key = "${var.secret_key}"
|
||||
}
|
||||
```
|
||||
|
||||
You can force Terraform to explicitly not load your configuration by
|
||||
specifying `-config=""` (empty string). This is useful in situations where
|
||||
you want to manually configure the provider because your configuration
|
||||
may not be valid.
|
||||
|
||||
## Example: AWS Instance
|
||||
|
||||
|
|
|
@ -22,9 +22,6 @@ $ terraform import aws_instance.bar i-abcd1234
|
|||
...
|
||||
```
|
||||
|
||||
~> **Note:** In order to import resources, the provider should be configured with environment variables.
|
||||
We currently do not support passing credentials directly to the provider.
|
||||
|
||||
The above command imports an AWS instance with the given ID to the
|
||||
address `aws_instance.bar`. You can also import resources into modules.
|
||||
See the [resource addressing](/docs/internals/resource-addressing.html)
|
||||
|
|
Loading…
Reference in New Issue