Merge pull request #9809 from hashicorp/f-import-provider

command/import: allow configuration from files
This commit is contained in:
Mitchell Hashimoto 2016-11-09 15:13:39 -08:00 committed by GitHub
commit ec55cecc70
14 changed files with 360 additions and 18 deletions

View File

@ -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.

View File

@ -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()

View File

@ -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

View File

@ -0,0 +1,3 @@
provider "test" {
foo = "bar"
}

View File

@ -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.

View File

@ -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 {

View File

@ -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(),
}

View File

@ -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{

View File

@ -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{},
}

View File

@ -0,0 +1,7 @@
provider "aws" {
foo = "${aws_instance.foo.bar}"
}
resource "aws_instance" "foo" {
bar = "value"
}

View File

@ -0,0 +1,5 @@
variable "foo" {}
provider "aws" {
foo = "${var.foo}"
}

View File

@ -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
}

View File

@ -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

View File

@ -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)