Merge pull request #15208 from hashicorp/0.10-plugins

0.10 Core/Provider split work
This commit is contained in:
James Bardin 2017-06-09 17:21:57 -04:00 committed by GitHub
commit 73139ba6aa
161 changed files with 16913 additions and 2822 deletions

View File

@ -47,14 +47,13 @@ func TestLocalProvider(t *testing.T, b *Local, name string) *terraform.MockResou
if b.ContextOpts == nil {
b.ContextOpts = &terraform.ContextOpts{}
}
if b.ContextOpts.Providers == nil {
b.ContextOpts.Providers = make(map[string]terraform.ResourceProviderFactory)
}
// Setup our provider
b.ContextOpts.Providers[name] = func() (terraform.ResourceProvider, error) {
return p, nil
}
b.ContextOpts.ProviderResolver = terraform.ResourceProviderResolverFixed(
map[string]terraform.ResourceProviderFactory{
name: terraform.ResourceProviderFactoryFixed(p),
},
)
return p
}

View File

@ -12,6 +12,11 @@ func testResource() *schema.Resource {
Read: testResourceRead,
Update: testResourceUpdate,
Delete: testResourceDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"required": {
Type: schema.TypeString,

View File

@ -132,10 +132,15 @@ func (c *ApplyCommand) Run(args []string) int {
}
*/
var conf *config.Config
if mod != nil {
conf = mod.Config()
}
// Load the backend
b, err := c.Backend(&BackendOpts{
ConfigPath: configPath,
Plan: plan,
Config: conf,
Plan: plan,
})
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))

View File

@ -33,8 +33,8 @@ func TestApply_destroy(t *testing.T) {
c := &ApplyCommand{
Destroy: true,
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -123,8 +123,8 @@ func TestApply_destroyLockedState(t *testing.T) {
c := &ApplyCommand{
Destroy: true,
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -155,8 +155,8 @@ func TestApply_destroyPlan(t *testing.T) {
c := &ApplyCommand{
Destroy: true,
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -199,8 +199,8 @@ func TestApply_destroyTargeted(t *testing.T) {
c := &ApplyCommand{
Destroy: true,
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}

View File

@ -28,8 +28,8 @@ func TestApply(t *testing.T) {
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -74,8 +74,8 @@ func TestApply_lockedState(t *testing.T) {
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -112,8 +112,8 @@ func TestApply_lockedStateWait(t *testing.T) {
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -188,8 +188,8 @@ func TestApply_parallelism(t *testing.T) {
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(provider),
Ui: ui,
testingOverrides: metaOverridesForProvider(provider),
Ui: ui,
},
}
@ -241,8 +241,8 @@ func TestApply_configInvalid(t *testing.T) {
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -276,8 +276,8 @@ func TestApply_defaultState(t *testing.T) {
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -314,8 +314,8 @@ func TestApply_error(t *testing.T) {
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -403,8 +403,8 @@ func TestApply_init(t *testing.T) {
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -460,8 +460,8 @@ func TestApply_input(t *testing.T) {
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -495,8 +495,8 @@ func TestApply_inputPartial(t *testing.T) {
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -535,8 +535,8 @@ func TestApply_noArgs(t *testing.T) {
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -584,8 +584,8 @@ func TestApply_plan(t *testing.T) {
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -630,8 +630,8 @@ func TestApply_plan_backup(t *testing.T) {
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -672,8 +672,8 @@ func TestApply_plan_noBackup(t *testing.T) {
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -729,8 +729,8 @@ func TestApply_plan_remoteState(t *testing.T) {
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -782,8 +782,8 @@ func TestApply_planWithVarFile(t *testing.T) {
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -824,8 +824,8 @@ func TestApply_planVars(t *testing.T) {
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -855,12 +855,10 @@ func TestApply_planNoModuleFiles(t *testing.T) {
Module: testModule(t, "apply-plan-no-module"),
})
contextOpts := testCtxConfig(p)
apply := &ApplyCommand{
Meta: Meta{
ContextOpts: contextOpts,
Ui: new(cli.MockUi),
testingOverrides: metaOverridesForProvider(p),
Ui: new(cli.MockUi),
},
}
args := []string{
@ -895,8 +893,8 @@ func TestApply_refresh(t *testing.T) {
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -961,8 +959,8 @@ func TestApply_shutdown(t *testing.T) {
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
ShutdownCh: shutdownCh,
@ -1072,8 +1070,8 @@ func TestApply_state(t *testing.T) {
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -1145,8 +1143,8 @@ func TestApply_stateNoExist(t *testing.T) {
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -1164,8 +1162,8 @@ func TestApply_sensitiveOutput(t *testing.T) {
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -1198,8 +1196,8 @@ func TestApply_stateFuture(t *testing.T) {
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -1239,8 +1237,8 @@ func TestApply_statePast(t *testing.T) {
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -1260,8 +1258,8 @@ func TestApply_vars(t *testing.T) {
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -1303,8 +1301,8 @@ func TestApply_varFile(t *testing.T) {
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -1356,8 +1354,8 @@ func TestApply_varFileDefault(t *testing.T) {
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -1408,8 +1406,8 @@ func TestApply_varFileDefaultJSON(t *testing.T) {
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -1471,8 +1469,8 @@ func TestApply_backup(t *testing.T) {
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -1540,8 +1538,8 @@ func TestApply_disableBackup(t *testing.T) {
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -1608,8 +1606,8 @@ func TestApply_terraformEnv(t *testing.T) {
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -1663,8 +1661,8 @@ func TestApply_terraformEnvNonDefault(t *testing.T) {
ui := new(cli.MockUi)
c := &ApplyCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}

View File

@ -69,23 +69,27 @@ func testFixturePath(name string) string {
return filepath.Join(fixtureDir, name)
}
func testCtxConfig(p terraform.ResourceProvider) *terraform.ContextOpts {
return &terraform.ContextOpts{
Providers: map[string]terraform.ResourceProviderFactory{
"test": func() (terraform.ResourceProvider, error) {
return p, nil
func metaOverridesForProvider(p terraform.ResourceProvider) *testingOverrides {
return &testingOverrides{
ProviderResolver: terraform.ResourceProviderResolverFixed(
map[string]terraform.ResourceProviderFactory{
"test": func() (terraform.ResourceProvider, error) {
return p, nil
},
},
},
),
}
}
func testCtxConfigWithShell(p terraform.ResourceProvider, pr terraform.ResourceProvisioner) *terraform.ContextOpts {
return &terraform.ContextOpts{
Providers: map[string]terraform.ResourceProviderFactory{
"test": func() (terraform.ResourceProvider, error) {
return p, nil
func metaOverridesForProviderAndProvisioner(p terraform.ResourceProvider, pr terraform.ResourceProvisioner) *testingOverrides {
return &testingOverrides{
ProviderResolver: terraform.ResourceProviderResolverFixed(
map[string]terraform.ResourceProviderFactory{
"test": func() (terraform.ResourceProvider, error) {
return p, nil
},
},
},
),
Provisioners: map[string]terraform.ResourceProvisionerFactory{
"shell": func() (terraform.ResourceProvisioner, error) {
return pr, nil

View File

@ -6,6 +6,7 @@ import (
"strings"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/helper/wrappedstreams"
"github.com/hashicorp/terraform/repl"
@ -43,8 +44,16 @@ func (c *ConsoleCommand) Run(args []string) int {
return 1
}
var conf *config.Config
if mod != nil {
conf = mod.Config()
}
// Load the backend
b, err := c.Backend(&BackendOpts{ConfigPath: configPath})
b, err := c.Backend(&BackendOpts{
Config: conf,
})
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
return 1

View File

@ -25,8 +25,8 @@ func TestConsole_basic(t *testing.T) {
ui := new(cli.MockUi)
c := &ConsoleCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -61,8 +61,8 @@ func TestConsole_tfvars(t *testing.T) {
ui := new(cli.MockUi)
c := &ConsoleCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}

View File

@ -30,8 +30,8 @@ func TestDebugJSON2Dot(t *testing.T) {
ui := new(cli.MockUi)
c := &DebugJSON2DotCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}

View File

@ -43,8 +43,17 @@ func (c *EnvDeleteCommand) Run(args []string) int {
return 1
}
cfg, err := c.Config(configPath)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err))
return 1
}
// Load the backend
b, err := c.Backend(&BackendOpts{ConfigPath: configPath})
b, err := c.Backend(&BackendOpts{
Config: cfg,
})
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
return 1

View File

@ -26,8 +26,17 @@ func (c *EnvListCommand) Run(args []string) int {
return 1
}
cfg, err := c.Config(configPath)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err))
return 1
}
// Load the backend
b, err := c.Backend(&BackendOpts{ConfigPath: configPath})
b, err := c.Backend(&BackendOpts{
Config: cfg,
})
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
return 1

View File

@ -46,8 +46,16 @@ func (c *EnvNewCommand) Run(args []string) int {
return 1
}
conf, err := c.Config(configPath)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err))
}
// Load the backend
b, err := c.Backend(&BackendOpts{ConfigPath: configPath})
b, err := c.Backend(&BackendOpts{
Config: conf,
})
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
return 1

View File

@ -31,8 +31,17 @@ func (c *EnvSelectCommand) Run(args []string) int {
return 1
}
conf, err := c.Config(configPath)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err))
return 1
}
// Load the backend
b, err := c.Backend(&BackendOpts{ConfigPath: configPath})
b, err := c.Backend(&BackendOpts{
Config: conf,
})
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
return 1

View File

@ -22,8 +22,8 @@ func TestFmt_errorReporting(t *testing.T) {
ui := new(cli.MockUi)
c := &FmtCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -43,8 +43,8 @@ func TestFmt_tooManyArgs(t *testing.T) {
ui := new(cli.MockUi)
c := &FmtCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -82,8 +82,8 @@ func TestFmt_workingDirectory(t *testing.T) {
ui := new(cli.MockUi)
c := &FmtCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -108,8 +108,8 @@ func TestFmt_directoryArg(t *testing.T) {
ui := new(cli.MockUi)
c := &FmtCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -131,8 +131,8 @@ func TestFmt_stdinArg(t *testing.T) {
ui := new(cli.MockUi)
c := &FmtCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
input: input,
}
@ -158,8 +158,8 @@ func TestFmt_nonDefaultOptions(t *testing.T) {
ui := new(cli.MockUi)
c := &FmtCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}

View File

@ -15,9 +15,9 @@ func TestGet(t *testing.T) {
ui := new(cli.MockUi)
c := &GetCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
dataDir: tempDir(t),
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
dataDir: tempDir(t),
},
}
@ -41,9 +41,9 @@ func TestGet_multipleArgs(t *testing.T) {
ui := new(cli.MockUi)
c := &GetCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
dataDir: tempDir(t),
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
dataDir: tempDir(t),
},
}
@ -69,9 +69,9 @@ func TestGet_noArgs(t *testing.T) {
ui := new(cli.MockUi)
c := &GetCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
dataDir: tempDir(t),
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
dataDir: tempDir(t),
},
}
@ -96,9 +96,9 @@ func TestGet_update(t *testing.T) {
ui := new(cli.MockUi)
c := &GetCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
dataDir: tempDir(t),
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
dataDir: tempDir(t),
},
}

View File

@ -6,6 +6,7 @@ import (
"strings"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/dag"
"github.com/hashicorp/terraform/terraform"
@ -62,10 +63,15 @@ func (c *GraphCommand) Run(args []string) int {
}
}
var conf *config.Config
if mod != nil {
conf = mod.Config()
}
// Load the backend
b, err := c.Backend(&BackendOpts{
ConfigPath: configPath,
Plan: plan,
Config: conf,
Plan: plan,
})
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))

View File

@ -16,8 +16,8 @@ func TestGraph(t *testing.T) {
ui := new(cli.MockUi)
c := &GraphCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -38,8 +38,8 @@ func TestGraph_multipleArgs(t *testing.T) {
ui := new(cli.MockUi)
c := &GraphCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -65,8 +65,8 @@ func TestGraph_noArgs(t *testing.T) {
ui := new(cli.MockUi)
c := &GraphCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -105,8 +105,8 @@ func TestGraph_plan(t *testing.T) {
ui := new(cli.MockUi)
c := &GraphCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}

View File

@ -7,6 +7,7 @@ import (
"strings"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/terraform"
)
@ -49,6 +50,23 @@ func (c *ImportCommand) Run(args []string) int {
return 1
}
// Validate the provided resource address for syntax
addr, err := terraform.ParseResourceAddress(args[0])
if err != nil {
c.Ui.Error(fmt.Sprintf(importCommandInvalidAddressFmt, err))
return 1
}
if !addr.HasResourceSpec() {
// module.foo target isn't allowed for import
c.Ui.Error(importCommandMissingResourceSpecMsg)
return 1
}
if addr.Mode != config.ManagedResourceMode {
// can't import to a data resource address
c.Ui.Error(importCommandResourceModeMsg)
return 1
}
// Load the module
var mod *module.Tree
if configPath != "" {
@ -60,8 +78,44 @@ func (c *ImportCommand) Run(args []string) int {
}
}
// Verify that the given address points to something that exists in config.
// This is to reduce the risk that a typo in the resource address will
// import something that Terraform will want to immediately destroy on
// the next plan, and generally acts as a reassurance of user intent.
targetMod := mod.Child(addr.Path)
if targetMod == nil {
modulePath := addr.WholeModuleAddress().String()
if modulePath == "" {
c.Ui.Error(importCommandMissingConfigMsg)
} else {
c.Ui.Error(fmt.Sprintf(importCommandMissingModuleFmt, modulePath))
}
return 1
}
rcs := targetMod.Config().Resources
var rc *config.Resource
for _, thisRc := range rcs {
if addr.MatchesConfig(targetMod, thisRc) {
rc = thisRc
break
}
}
if rc == nil {
modulePath := addr.WholeModuleAddress().String()
if modulePath == "" {
modulePath = "the root module"
}
c.Ui.Error(fmt.Sprintf(
importCommandMissingResourceFmt,
addr, modulePath, addr.Type, addr.Name,
))
return 1
}
// Load the backend
b, err := c.Backend(&BackendOpts{ConfigPath: configPath})
b, err := c.Backend(&BackendOpts{
Config: mod.Config(),
})
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
return 1
@ -113,13 +167,7 @@ func (c *ImportCommand) Run(args []string) int {
return 1
}
c.Ui.Output(c.Colorize().Color(fmt.Sprintf(
"[reset][green]\n" +
"Import success! The resources imported are shown above. These are\n" +
"now in your Terraform state. Import does not currently generate\n" +
"configuration, so you must do this next. If you do not create configuration\n" +
"for the above resources, then the next `terraform plan` will mark\n" +
"them for destruction.")))
c.Ui.Output(c.Colorize().Color("[reset][green]\n" + importCommandSuccessMsg))
return 0
}
@ -196,3 +244,51 @@ Options:
func (c *ImportCommand) Synopsis() string {
return "Import existing infrastructure into Terraform"
}
const importCommandInvalidAddressFmt = `Error: %s
For information on valid syntax, see:
https://www.terraform.io/docs/internals/resource-addressing.html
`
const importCommandMissingResourceSpecMsg = `Error: resource address must include a full resource spec
For information on valid syntax, see:
https://www.terraform.io/docs/internals/resource-addressing.html
`
const importCommandResourceModeMsg = `Error: resource address must refer to a managed resource.
Data resources cannot be imported.
`
const importCommandMissingConfigMsg = `Error: no configuration files in this directory.
"terraform import" can only be run in a Terraform configuration directory.
Create one or more .tf files in this directory to import here.
`
const importCommandMissingModuleFmt = `Error: %s does not exist in the configuration.
Please add the configuration for the module before importing resources into it.
`
const importCommandMissingResourceFmt = `Error: resource address %q does not exist in the configuration.
Before importing this resource, please create its configuration in %s. For example:
resource %q %q {
# (resource arguments)
}
`
const importCommandSuccessMsg = `Import successful!
The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.
Import does not generate configuration, so the next step is to ensure that
the resource configurations match the current (or desired) state of the
imported resources. You can use the output from "terraform plan" to verify that
the configuration is correct and complete.
`

View File

@ -2,6 +2,7 @@ package command
import (
"fmt"
"strings"
"testing"
"github.com/hashicorp/terraform/terraform"
@ -9,14 +10,16 @@ import (
)
func TestImport(t *testing.T) {
defer testChdir(t, testFixturePath("import-provider-implicit"))()
statePath := testTempFile(t)
p := testProvider()
ui := new(cli.MockUi)
c := &ImportCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -55,8 +58,8 @@ func TestImport_providerConfig(t *testing.T) {
ui := new(cli.MockUi)
c := &ImportCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -102,63 +105,6 @@ func TestImport_providerConfig(t *testing.T) {
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 TestImport_providerConfigWithVar(t *testing.T) {
defer testChdir(t, testFixturePath("import-provider-var"))()
@ -168,8 +114,8 @@ func TestImport_providerConfigWithVar(t *testing.T) {
ui := new(cli.MockUi)
c := &ImportCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -225,8 +171,8 @@ func TestImport_providerConfigWithVarDefault(t *testing.T) {
ui := new(cli.MockUi)
c := &ImportCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -281,8 +227,8 @@ func TestImport_providerConfigWithVarFile(t *testing.T) {
ui := new(cli.MockUi)
c := &ImportCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -329,700 +275,17 @@ func TestImport_providerConfigWithVarFile(t *testing.T) {
testStateOutput(t, statePath, testImportStr)
}
/*
func TestRefresh_badState(t *testing.T) {
p := testProvider()
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{
"-state", "i-should-not-exist-ever",
testFixturePath("refresh"),
}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
}
func TestRefresh_cwd(t *testing.T) {
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("err: %s", err)
}
if err := os.Chdir(testFixturePath("refresh")); err != nil {
t.Fatalf("err: %s", err)
}
defer os.Chdir(cwd)
state := testState()
statePath := testStateFile(t, state)
p := testProvider()
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
p.RefreshFn = nil
p.RefreshReturn = &terraform.InstanceState{ID: "yes"}
args := []string{
"-state", statePath,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
if !p.RefreshCalled {
t.Fatal("refresh should be called")
}
f, err := os.Open(statePath)
if err != nil {
t.Fatalf("err: %s", err)
}
newState, err := terraform.ReadState(f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(newState.String())
expected := strings.TrimSpace(testRefreshCwdStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
func TestRefresh_defaultState(t *testing.T) {
originalState := testState()
// Write the state file in a temporary directory with the
// default filename.
td, err := ioutil.TempDir("", "tf")
if err != nil {
t.Fatalf("err: %s", err)
}
statePath := filepath.Join(td, DefaultStateFilename)
f, err := os.Create(statePath)
if err != nil {
t.Fatalf("err: %s", err)
}
err = terraform.WriteState(originalState, f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
// Change to that directory
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("err: %s", err)
}
if err := os.Chdir(filepath.Dir(statePath)); err != nil {
t.Fatalf("err: %s", err)
}
defer os.Chdir(cwd)
p := testProvider()
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
p.RefreshFn = nil
p.RefreshReturn = &terraform.InstanceState{ID: "yes"}
args := []string{
testFixturePath("refresh"),
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
if !p.RefreshCalled {
t.Fatal("refresh should be called")
}
f, err = os.Open(statePath)
if err != nil {
t.Fatalf("err: %s", err)
}
newState, err := terraform.ReadState(f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
actual := newState.RootModule().Resources["test_instance.foo"].Primary
expected := p.RefreshReturn
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
f, err = os.Open(statePath + DefaultBackupExtension)
if err != nil {
t.Fatalf("err: %s", err)
}
backupState, err := terraform.ReadState(f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
actual = backupState.RootModule().Resources["test_instance.foo"].Primary
expected = originalState.RootModule().Resources["test_instance.foo"].Primary
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
}
func TestRefresh_futureState(t *testing.T) {
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("err: %s", err)
}
if err := os.Chdir(testFixturePath("refresh")); err != nil {
t.Fatalf("err: %s", err)
}
defer os.Chdir(cwd)
state := testState()
state.TFVersion = "99.99.99"
statePath := testStateFile(t, state)
p := testProvider()
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{
"-state", statePath,
}
if code := c.Run(args); code == 0 {
t.Fatal("should fail")
}
if p.RefreshCalled {
t.Fatal("refresh should not be called")
}
f, err := os.Open(statePath)
if err != nil {
t.Fatalf("err: %s", err)
}
newState, err := terraform.ReadState(f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(newState.String())
expected := strings.TrimSpace(state.String())
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
func TestRefresh_pastState(t *testing.T) {
state := testState()
state.TFVersion = "0.1.0"
statePath := testStateFile(t, state)
p := testProvider()
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
p.RefreshFn = nil
p.RefreshReturn = &terraform.InstanceState{ID: "yes"}
args := []string{
"-state", statePath,
testFixturePath("refresh"),
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
if !p.RefreshCalled {
t.Fatal("refresh should be called")
}
f, err := os.Open(statePath)
if err != nil {
t.Fatalf("err: %s", err)
}
newState, err := terraform.ReadState(f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(newState.String())
expected := strings.TrimSpace(testRefreshStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
if newState.TFVersion != terraform.Version {
t.Fatalf("bad:\n\n%s", newState.TFVersion)
}
}
func TestRefresh_outPath(t *testing.T) {
state := testState()
statePath := testStateFile(t, state)
// Output path
outf, err := ioutil.TempFile("", "tf")
if err != nil {
t.Fatalf("err: %s", err)
}
outPath := outf.Name()
outf.Close()
os.Remove(outPath)
p := testProvider()
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
p.RefreshFn = nil
p.RefreshReturn = &terraform.InstanceState{ID: "yes"}
args := []string{
"-state", statePath,
"-state-out", outPath,
testFixturePath("refresh"),
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
f, err := os.Open(statePath)
if err != nil {
t.Fatalf("err: %s", err)
}
newState, err := terraform.ReadState(f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
if !reflect.DeepEqual(newState, state) {
t.Fatalf("bad: %#v", newState)
}
f, err = os.Open(outPath)
if err != nil {
t.Fatalf("err: %s", err)
}
newState, err = terraform.ReadState(f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
actual := newState.RootModule().Resources["test_instance.foo"].Primary
expected := p.RefreshReturn
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
f, err = os.Open(outPath + DefaultBackupExtension)
if err != nil {
t.Fatalf("err: %s", err)
}
backupState, err := terraform.ReadState(f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
actualStr := strings.TrimSpace(backupState.String())
expectedStr := strings.TrimSpace(state.String())
if actualStr != expectedStr {
t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr)
}
}
func TestRefresh_var(t *testing.T) {
state := testState()
statePath := testStateFile(t, state)
p := testProvider()
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{
"-var", "foo=bar",
"-state", statePath,
testFixturePath("refresh-var"),
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
if !p.ConfigureCalled {
t.Fatal("configure should be called")
}
if p.ConfigureConfig.Config["value"].(string) != "bar" {
t.Fatalf("bad: %#v", p.ConfigureConfig.Config)
}
}
func TestRefresh_varFile(t *testing.T) {
state := testState()
statePath := testStateFile(t, state)
p := testProvider()
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
varFilePath := testTempFile(t)
if err := ioutil.WriteFile(varFilePath, []byte(refreshVarFile), 0644); err != nil {
t.Fatalf("err: %s", err)
}
args := []string{
"-var-file", varFilePath,
"-state", statePath,
testFixturePath("refresh-var"),
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
if !p.ConfigureCalled {
t.Fatal("configure should be called")
}
if p.ConfigureConfig.Config["value"].(string) != "bar" {
t.Fatalf("bad: %#v", p.ConfigureConfig.Config)
}
}
func TestRefresh_varFileDefault(t *testing.T) {
state := testState()
statePath := testStateFile(t, state)
p := testProvider()
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
varFileDir := testTempDir(t)
varFilePath := filepath.Join(varFileDir, "terraform.tfvars")
if err := ioutil.WriteFile(varFilePath, []byte(refreshVarFile), 0644); err != nil {
t.Fatalf("err: %s", err)
}
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("err: %s", err)
}
if err := os.Chdir(varFileDir); err != nil {
t.Fatalf("err: %s", err)
}
defer os.Chdir(cwd)
args := []string{
"-state", statePath,
testFixturePath("refresh-var"),
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
if !p.ConfigureCalled {
t.Fatal("configure should be called")
}
if p.ConfigureConfig.Config["value"].(string) != "bar" {
t.Fatalf("bad: %#v", p.ConfigureConfig.Config)
}
}
func TestRefresh_varsUnset(t *testing.T) {
// Disable test mode so input would be asked
test = false
defer func() { test = true }()
defaultInputReader = bytes.NewBufferString("bar\n")
state := testState()
statePath := testStateFile(t, state)
p := testProvider()
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{
"-state", statePath,
testFixturePath("refresh-unset-var"),
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
}
func TestRefresh_backup(t *testing.T) {
state := testState()
statePath := testStateFile(t, state)
// Output path
outf, err := ioutil.TempFile("", "tf")
if err != nil {
t.Fatalf("err: %s", err)
}
outPath := outf.Name()
outf.Close()
os.Remove(outPath)
// Backup path
backupf, err := ioutil.TempFile("", "tf")
if err != nil {
t.Fatalf("err: %s", err)
}
backupPath := backupf.Name()
backupf.Close()
os.Remove(backupPath)
p := testProvider()
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
p.RefreshFn = nil
p.RefreshReturn = &terraform.InstanceState{ID: "yes"}
args := []string{
"-state", statePath,
"-state-out", outPath,
"-backup", backupPath,
testFixturePath("refresh"),
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
f, err := os.Open(statePath)
if err != nil {
t.Fatalf("err: %s", err)
}
newState, err := terraform.ReadState(f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
if !reflect.DeepEqual(newState, state) {
t.Fatalf("bad: %#v", newState)
}
f, err = os.Open(outPath)
if err != nil {
t.Fatalf("err: %s", err)
}
newState, err = terraform.ReadState(f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
actual := newState.RootModule().Resources["test_instance.foo"].Primary
expected := p.RefreshReturn
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
f, err = os.Open(backupPath)
if err != nil {
t.Fatalf("err: %s", err)
}
backupState, err := terraform.ReadState(f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
actualStr := strings.TrimSpace(backupState.String())
expectedStr := strings.TrimSpace(state.String())
if actualStr != expectedStr {
t.Fatalf("bad:\n\n%s\n\n%s", actualStr, expectedStr)
}
}
func TestRefresh_disableBackup(t *testing.T) {
state := testState()
statePath := testStateFile(t, state)
// Output path
outf, err := ioutil.TempFile("", "tf")
if err != nil {
t.Fatalf("err: %s", err)
}
outPath := outf.Name()
outf.Close()
os.Remove(outPath)
p := testProvider()
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
p.RefreshFn = nil
p.RefreshReturn = &terraform.InstanceState{ID: "yes"}
args := []string{
"-state", statePath,
"-state-out", outPath,
"-backup", "-",
testFixturePath("refresh"),
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
f, err := os.Open(statePath)
if err != nil {
t.Fatalf("err: %s", err)
}
newState, err := terraform.ReadState(f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
if !reflect.DeepEqual(newState, state) {
t.Fatalf("bad: %#v", newState)
}
f, err = os.Open(outPath)
if err != nil {
t.Fatalf("err: %s", err)
}
newState, err = terraform.ReadState(f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
actual := newState.RootModule().Resources["test_instance.foo"].Primary
expected := p.RefreshReturn
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
// Ensure there is no backup
_, err = os.Stat(outPath + DefaultBackupExtension)
if err == nil || !os.IsNotExist(err) {
t.Fatalf("backup should not exist")
}
}
func TestRefresh_displaysOutputs(t *testing.T) {
state := testState()
statePath := testStateFile(t, state)
p := testProvider()
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
},
}
args := []string{
"-state", statePath,
testFixturePath("refresh-output"),
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
// Test that outputs were displayed
outputValue := "foo.example.com"
actual := ui.OutputWriter.String()
if !strings.Contains(actual, outputValue) {
t.Fatalf("Expected:\n%s\n\nTo include: %q", actual, outputValue)
}
}
*/
func TestImport_customProvider(t *testing.T) {
defer testChdir(t, testFixturePath("import-provider-aliased"))()
statePath := testTempFile(t)
p := testProvider()
ui := new(cli.MockUi)
c := &ImportCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -1053,6 +316,156 @@ func TestImport_customProvider(t *testing.T) {
testStateOutput(t, statePath, testImportCustomProviderStr)
}
func TestImport_missingResourceConfig(t *testing.T) {
defer testChdir(t, testFixturePath("import-missing-resource-config"))()
statePath := testTempFile(t)
p := testProvider()
ui := new(cli.MockUi)
c := &ImportCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
args := []string{
"-state", statePath,
"test_instance.foo",
"bar",
}
code := c.Run(args)
if code != 1 {
t.Fatalf("import succeeded; expected failure")
}
msg := ui.ErrorWriter.String()
if want := `resource address "test_instance.foo" does not exist`; !strings.Contains(msg, want) {
t.Errorf("incorrect message\nwant substring: %s\ngot:\n%s", want, msg)
}
}
func TestImport_missingModuleConfig(t *testing.T) {
defer testChdir(t, testFixturePath("import-missing-resource-config"))()
statePath := testTempFile(t)
p := testProvider()
ui := new(cli.MockUi)
c := &ImportCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
args := []string{
"-state", statePath,
"module.baz.test_instance.foo",
"bar",
}
code := c.Run(args)
if code != 1 {
t.Fatalf("import succeeded; expected failure")
}
msg := ui.ErrorWriter.String()
if want := `module.baz does not exist in the configuration`; !strings.Contains(msg, want) {
t.Errorf("incorrect message\nwant substring: %s\ngot:\n%s", want, msg)
}
}
func TestImport_dataResource(t *testing.T) {
defer testChdir(t, testFixturePath("import-missing-resource-config"))()
statePath := testTempFile(t)
p := testProvider()
ui := new(cli.MockUi)
c := &ImportCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
args := []string{
"-state", statePath,
"data.test_data_source.foo",
"bar",
}
code := c.Run(args)
if code != 1 {
t.Fatalf("import succeeded; expected failure")
}
msg := ui.ErrorWriter.String()
if want := `resource address must refer to a managed resource`; !strings.Contains(msg, want) {
t.Errorf("incorrect message\nwant substring: %s\ngot:\n%s", want, msg)
}
}
func TestImport_invalidResourceAddr(t *testing.T) {
defer testChdir(t, testFixturePath("import-missing-resource-config"))()
statePath := testTempFile(t)
p := testProvider()
ui := new(cli.MockUi)
c := &ImportCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
args := []string{
"-state", statePath,
"bananas",
"bar",
}
code := c.Run(args)
if code != 1 {
t.Fatalf("import succeeded; expected failure")
}
msg := ui.ErrorWriter.String()
if want := `invalid resource address "bananas"`; !strings.Contains(msg, want) {
t.Errorf("incorrect message\nwant substring: %s\ngot:\n%s", want, msg)
}
}
func TestImport_targetIsModule(t *testing.T) {
defer testChdir(t, testFixturePath("import-missing-resource-config"))()
statePath := testTempFile(t)
p := testProvider()
ui := new(cli.MockUi)
c := &ImportCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
args := []string{
"-state", statePath,
"module.foo",
"bar",
}
code := c.Run(args)
if code != 1 {
t.Fatalf("import succeeded; expected failure")
}
msg := ui.ErrorWriter.String()
if want := `resource address must include a full resource spec`; !strings.Contains(msg, want) {
t.Errorf("incorrect message\nwant substring: %s\ngot:\n%s", want, msg)
}
}
const testImportStr = `
test_instance.foo:
ID = yay

View File

@ -2,24 +2,37 @@ package command
import (
"fmt"
"log"
"os"
"path/filepath"
"sort"
"strings"
"github.com/hashicorp/go-getter"
getter "github.com/hashicorp/go-getter"
multierror "github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/helper/variables"
"github.com/hashicorp/terraform/plugin"
"github.com/hashicorp/terraform/plugin/discovery"
"github.com/hashicorp/terraform/terraform"
)
// InitCommand is a Command implementation that takes a Terraform
// module and clones it to the working directory.
type InitCommand struct {
Meta
// getProvider fetches providers that aren't found locally, and unpacks
// them into the dst directory.
// This uses discovery.GetProvider by default, but it provided here as a
// way to mock fetching providers for tests.
getProvider func(dst, provider string, req discovery.Constraints, protoVersion uint) error
}
func (c *InitCommand) Run(args []string) int {
var flagBackend, flagGet bool
var flagBackend, flagGet, flagGetPlugins bool
var flagConfigExtra map[string]interface{}
args = c.Meta.process(args, false)
@ -27,6 +40,7 @@ func (c *InitCommand) Run(args []string) int {
cmdFlags.BoolVar(&flagBackend, "backend", true, "")
cmdFlags.Var((*variables.FlagAny)(&flagConfigExtra), "backend-config", "")
cmdFlags.BoolVar(&flagGet, "get", true, "")
cmdFlags.BoolVar(&flagGetPlugins, "get-plugins", true, "")
cmdFlags.BoolVar(&c.forceInitCopy, "force-copy", false, "suppress prompts about copying state data")
cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock state")
cmdFlags.DurationVar(&c.Meta.stateLockTimeout, "lock-timeout", 0, "lock timeout")
@ -37,6 +51,11 @@ func (c *InitCommand) Run(args []string) int {
return 1
}
// set getProvider if we don't have a test version already
if c.getProvider == nil {
c.getProvider = discovery.GetProvider
}
// Validate the arg count
args = cmdFlags.Args()
if len(args) > 2 {
@ -103,14 +122,12 @@ func (c *InitCommand) Run(args []string) int {
return 0
}
var back backend.Backend
// If we're performing a get or loading the backend, then we perform
// some extra tasks.
if flagGet || flagBackend {
// Load the configuration in this directory so that we can know
// if we have anything to get or any backend to configure. We do
// this to improve the UX. Practically, we could call the functions
// below without checking this to the same effect.
conf, err := config.LoadDir(path)
conf, err := c.Config(path)
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error loading configuration: %s", err))
@ -129,10 +146,12 @@ func (c *InitCommand) Run(args []string) int {
"Error downloading modules: %s", err))
return 1
}
}
// If we're requesting backend configuration and configure it
if flagBackend {
// If we're requesting backend configuration or looking for required
// plugins, load the backend
if flagBackend || flagGetPlugins {
header = true
// Only output that we're initializing a backend if we have
@ -145,17 +164,44 @@ func (c *InitCommand) Run(args []string) int {
}
opts := &BackendOpts{
ConfigPath: path,
Config: conf,
ConfigExtra: flagConfigExtra,
Init: true,
}
if _, err := c.Backend(opts); err != nil {
if back, err = c.Backend(opts); err != nil {
c.Ui.Error(err.Error())
return 1
}
}
}
// Now that we have loaded all modules, check the module tree for missing providers
if flagGetPlugins {
sMgr, err := back.State(c.Env())
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error loading state: %s", err))
return 1
}
if err := sMgr.RefreshState(); err != nil {
c.Ui.Error(fmt.Sprintf(
"Error refreshing state: %s", err))
return 1
}
c.Ui.Output(c.Colorize().Color(
"[reset][bold]Initializing provider plugins...",
))
err = c.getProviders(path, sMgr.State())
if err != nil {
// this function provides its own output
log.Printf("[ERROR] %s", err)
return 1
}
}
// If we outputted information, then we need to output a newline
// so that our success message is nicely spaced out from prior text.
if header {
@ -167,6 +213,95 @@ func (c *InitCommand) Run(args []string) int {
return 0
}
// Load the complete module tree, and fetch any missing providers.
// This method outputs its own Ui.
func (c *InitCommand) getProviders(path string, state *terraform.State) error {
mod, err := c.Module(path)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error getting plugins: %s", err))
return err
}
if err := mod.Validate(); err != nil {
c.Ui.Error(fmt.Sprintf("Error getting plugins: %s", err))
return err
}
available := c.providerPluginSet()
requirements := terraform.ModuleTreeDependencies(mod, state).AllPluginRequirements()
missing := c.missingPlugins(available, requirements)
dst := c.pluginDir()
var errs error
for provider, reqd := range missing {
c.Ui.Output(fmt.Sprintf("- downloading plugin for provider %q...", provider))
err := c.getProvider(dst, provider, reqd.Versions, plugin.Handshake.ProtocolVersion)
if err != nil {
c.Ui.Error(fmt.Sprintf(errProviderNotFound, err, provider, reqd.Versions))
errs = multierror.Append(errs, err)
}
}
if errs != nil {
return errs
}
// With all the providers downloaded, we'll generate our lock file
// that ensures the provider binaries remain unchanged until we init
// again. If anything changes, other commands that use providers will
// fail with an error instructing the user to re-run this command.
available = c.providerPluginSet() // re-discover to see newly-installed plugins
chosen := choosePlugins(available, requirements)
digests := map[string][]byte{}
for name, meta := range chosen {
digest, err := meta.SHA256()
if err != nil {
c.Ui.Error(fmt.Sprintf("failed to read provider plugin %s: %s", meta.Path, err))
return err
}
digests[name] = digest
}
err = c.providerPluginsLock().Write(digests)
if err != nil {
c.Ui.Error(fmt.Sprintf("failed to save provider manifest: %s", err))
return err
}
// If any providers have "floating" versions (completely unconstrained)
// we'll suggest the user constrain with a pessimistic constraint to
// avoid implicitly adopting a later major release.
constraintSuggestions := make(map[string]discovery.ConstraintStr)
for name, meta := range chosen {
req := requirements[name]
if req == nil {
// should never happen, but we don't want to crash here, so we'll
// be cautious.
continue
}
if req.Versions.Unconstrained() {
// meta.Version.MustParse is safe here because our "chosen" metas
// were already filtered for validity of versions.
constraintSuggestions[name] = meta.Version.MustParse().MinorUpgradeConstraintStr()
}
}
if len(constraintSuggestions) != 0 {
names := make([]string, 0, len(constraintSuggestions))
for name := range constraintSuggestions {
names = append(names, name)
}
sort.Strings(names)
c.Ui.Output(outputInitProvidersUnconstrained)
for _, name := range names {
c.Ui.Output(fmt.Sprintf("* provider.%s: version = %q", name, constraintSuggestions[name]))
}
}
return nil
}
func (c *InitCommand) copySource(dst, src, pwd string) error {
// Verify the directory is empty
if empty, err := config.IsEmptyDir(dst); err != nil {
@ -189,7 +324,7 @@ func (c *InitCommand) Help() string {
helpText := `
Usage: terraform init [options] [SOURCE] [PATH]
Initialize a new or existing Terraform environment by creating
Initialize a new or existing Terraform working directory by creating
initial files, loading any remote state, downloading modules, etc.
This is the first command that should be run for any new or existing
@ -198,9 +333,9 @@ Usage: terraform init [options] [SOURCE] [PATH]
control.
This command is always safe to run multiple times. Though subsequent runs
may give errors, this command will never blow away your environment or state.
Even so, if you have important information, please back it up prior to
running this command just in case.
may give errors, this command will never delete your configuration or
state. Even so, if you have important information, please back it up prior
to running this command, just in case.
If no arguments are given, the configuration in this working directory
is initialized.
@ -215,7 +350,7 @@ Usage: terraform init [options] [SOURCE] [PATH]
Options:
-backend=true Configure the backend for this environment.
-backend=true Configure the backend for this configuration.
-backend-config=path This can be either a path to an HCL file with key/value
assignments (same format as terraform.tfvars) or a
@ -230,6 +365,8 @@ Options:
-get=true Download any modules for this configuration.
-get-plugins=true Download any missing plugins for this configuration.
-input=true Ask for input if necessary. If false, will error if
input was required.
@ -271,6 +408,28 @@ any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your environment. If you forget, other
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
`
const outputInitProvidersUnconstrained = `
The following providers do not have any version constraints in configuration,
so the latest version was installed.
To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version = "..." constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.
`
const errProviderNotFound = `
[reset][red]%[1]s
[reset][bold][red]Error: Satisfying %[2]q, provider not found
[reset][red]A version of the %[2]q provider that satisfies all version
constraints could not be found. The requested version
constraints are shown below.
%[2]s = %[3]q[reset]
`

View File

@ -1,13 +1,16 @@
package command
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/hashicorp/terraform/helper/copy"
"github.com/hashicorp/terraform/plugin/discovery"
"github.com/mitchellh/cli"
)
@ -17,8 +20,8 @@ func TestInit(t *testing.T) {
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -54,8 +57,8 @@ func TestInit_cwd(t *testing.T) {
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -81,8 +84,8 @@ func TestInit_empty(t *testing.T) {
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -96,8 +99,8 @@ func TestInit_multipleArgs(t *testing.T) {
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -134,8 +137,8 @@ func TestInit_dstInSrc(t *testing.T) {
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -162,8 +165,8 @@ func TestInit_get(t *testing.T) {
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -189,8 +192,8 @@ func TestInit_copyGet(t *testing.T) {
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -222,8 +225,8 @@ func TestInit_backend(t *testing.T) {
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -248,8 +251,8 @@ func TestInit_backendUnset(t *testing.T) {
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -273,8 +276,8 @@ func TestInit_backendUnset(t *testing.T) {
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -301,8 +304,8 @@ func TestInit_backendConfigFile(t *testing.T) {
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -333,8 +336,8 @@ func TestInit_backendConfigFileChange(t *testing.T) {
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -360,8 +363,8 @@ func TestInit_backendConfigKV(t *testing.T) {
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -387,8 +390,8 @@ func TestInit_copyBackendDst(t *testing.T) {
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -426,8 +429,8 @@ func TestInit_backendReinitWithExtra(t *testing.T) {
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -474,8 +477,8 @@ func TestInit_backendReinitConfigToExtra(t *testing.T) {
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -518,8 +521,8 @@ func TestInit_inputFalse(t *testing.T) {
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -534,6 +537,170 @@ func TestInit_inputFalse(t *testing.T) {
}
}
func TestInit_getProvider(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
copy.CopyDir(testFixturePath("init-get-providers"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
getter := &mockGetProvider{
Providers: map[string][]string{
// looking for an exact version
"exact": []string{"1.2.3"},
// config requires >= 2.3.3
"greater_than": []string{"2.3.4", "2.3.3", "2.3.0"},
// config specifies
"between": []string{"3.4.5", "2.3.4", "1.2.3"},
},
}
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
getProvider: getter.GetProvider,
}
args := []string{}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
// check that we got the providers for our config
exactPath := filepath.Join(c.pluginDir(), getter.FileName("exact", "1.2.3"))
if _, err := os.Stat(exactPath); os.IsNotExist(err) {
t.Fatal("provider 'exact' not downloaded")
}
greaterThanPath := filepath.Join(c.pluginDir(), getter.FileName("greater_than", "2.3.4"))
if _, err := os.Stat(greaterThanPath); os.IsNotExist(err) {
t.Fatal("provider 'greater_than' not downloaded")
}
betweenPath := filepath.Join(c.pluginDir(), getter.FileName("between", "2.3.4"))
if _, err := os.Stat(betweenPath); os.IsNotExist(err) {
t.Fatal("provider 'between' not downloaded")
}
}
func TestInit_getProviderMissing(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
copy.CopyDir(testFixturePath("init-get-providers"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
getter := &mockGetProvider{
Providers: map[string][]string{
// looking for exact version 1.2.3
"exact": []string{"1.2.4"},
// config requires >= 2.3.3
"greater_than": []string{"2.3.4", "2.3.3", "2.3.0"},
// config specifies
"between": []string{"3.4.5", "2.3.4", "1.2.3"},
},
}
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
getProvider: getter.GetProvider,
}
args := []string{}
if code := c.Run(args); code == 0 {
t.Fatalf("expceted error, got output: \n%s", ui.OutputWriter.String())
}
if !strings.Contains(ui.ErrorWriter.String(), "no suitable version for provider") {
t.Fatalf("unexpected error output: %s", ui.ErrorWriter)
}
}
func TestInit_getProviderHaveLegacyVersion(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
copy.CopyDir(testFixturePath("init-providers-lock"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
if err := ioutil.WriteFile("terraform-provider-test", []byte("provider bin"), 0755); err != nil {
t.Fatal(err)
}
// provider test has a version constraint in the config, which should
// trigger the getProvider error below.
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
getProvider: func(dst, provider string, req discovery.Constraints, protoVersion uint) error {
return fmt.Errorf("EXPECTED PROVIDER ERROR %s", provider)
},
}
args := []string{}
if code := c.Run(args); code == 0 {
t.Fatalf("expceted error, got output: \n%s", ui.OutputWriter.String())
}
if !strings.Contains(ui.ErrorWriter.String(), "EXPECTED PROVIDER ERROR test") {
t.Fatalf("unexpected error output: %s", ui.ErrorWriter)
}
}
func TestInit_providerLockFile(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
copy.CopyDir(testFixturePath("init-provider-lock-file"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
getter := &mockGetProvider{
Providers: map[string][]string{
"test": []string{"1.2.3"},
},
}
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
getProvider: getter.GetProvider,
}
args := []string{}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
providersLockFile := fmt.Sprintf(
".terraform/plugins/%s_%s/lock.json",
runtime.GOOS, runtime.GOARCH,
)
buf, err := ioutil.ReadFile(providersLockFile)
if err != nil {
t.Fatalf("failed to read providers lock file %s: %s", providersLockFile, err)
}
// The hash in here is for the empty files that mockGetProvider produces
wantLockFile := strings.TrimSpace(`
{
"test": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}
`)
if string(buf) != wantLockFile {
t.Errorf("wrong provider lock file contents\ngot: %s\nwant: %s", buf, wantLockFile)
}
}
/*
func TestInit_remoteState(t *testing.T) {
tmp, cwd := testCwd(t)
@ -546,7 +713,7 @@ func TestInit_remoteState(t *testing.T) {
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -582,7 +749,7 @@ func TestInit_remoteStateSubdir(t *testing.T) {
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -626,7 +793,7 @@ func TestInit_remoteStateWithLocal(t *testing.T) {
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -664,7 +831,7 @@ func TestInit_remoteStateWithRemote(t *testing.T) {
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}

View File

@ -32,9 +32,10 @@ type Meta struct {
// command with a Meta field. These are expected to be set externally
// (not from within the command itself).
Color bool // True if output should be colored
ContextOpts *terraform.ContextOpts // Opts copied to initialize
Ui cli.Ui // Ui for output
Color bool // True if output should be colored
GlobalPluginDirs []string // Additional paths to search for plugins
PluginOverrides *PluginOverrides // legacy overrides from .terraformrc file
Ui cli.Ui // Ui for output
// ExtraHooks are extra hooks to add to the context.
ExtraHooks []terraform.Hook
@ -46,6 +47,9 @@ type Meta struct {
// Modify the data directory location. Defaults to DefaultDataDir
dataDir string
// Override certain behavior for tests within this package
testingOverrides *testingOverrides
//----------------------------------------------------------
// Private: do not set these
//----------------------------------------------------------
@ -109,6 +113,16 @@ type Meta struct {
reconfigure bool
}
type PluginOverrides struct {
Providers map[string]string
Provisioners map[string]string
}
type testingOverrides struct {
ProviderResolver terraform.ResourceProviderResolver
Provisioners map[string]terraform.ResourceProvisionerFactory
}
// initStatePaths is used to initialize the default values for
// statePath, stateOutPath, and backupPath
func (m *Meta) initStatePaths() {
@ -199,14 +213,7 @@ func (m *Meta) StdinPiped() bool {
// context with the settings from this Meta.
func (m *Meta) contextOpts() *terraform.ContextOpts {
var opts terraform.ContextOpts
if v := m.ContextOpts; v != nil {
opts = *v
}
opts.Hooks = []terraform.Hook{m.uiHook(), &terraform.DebugHook{}}
if m.ContextOpts != nil {
opts.Hooks = append(opts.Hooks, m.ContextOpts.Hooks...)
}
opts.Hooks = append(opts.Hooks, m.ExtraHooks...)
vs := make(map[string]interface{})
@ -226,6 +233,19 @@ func (m *Meta) contextOpts() *terraform.ContextOpts {
opts.Parallelism = m.parallelism
opts.Shadow = m.shadow
// If testingOverrides are set, we'll skip the plugin discovery process
// and just work with what we've been given, thus allowing the tests
// to provide mock providers and provisioners.
if m.testingOverrides != nil {
opts.ProviderResolver = m.testingOverrides.ProviderResolver
opts.Provisioners = m.testingOverrides.Provisioners
} else {
opts.ProviderResolver = m.providerResolver()
opts.Provisioners = m.provisionerFactories()
}
opts.ProviderSHA256s = m.providerPluginsLock().Read()
opts.Meta = &terraform.ContextMeta{
Env: m.Env(),
}

View File

@ -9,11 +9,9 @@ import (
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/hcl"
"github.com/hashicorp/terraform/backend"
@ -29,9 +27,9 @@ import (
// BackendOpts are the options used to initialize a backend.Backend.
type BackendOpts struct {
// ConfigPath is a path to a file or directory containing the backend
// configuration (declaration).
ConfigPath string
// Module is the root module from which we will extract the terraform and
// backend configuration.
Config *config.Config
// ConfigFile is a path to a file that contains configuration that
// is merged directly into the backend configuration when loaded
@ -178,71 +176,34 @@ func (m *Meta) Operation() *backend.Operation {
// backendConfig returns the local configuration for the backend
func (m *Meta) backendConfig(opts *BackendOpts) (*config.Backend, error) {
// If no explicit path was given then it is okay for there to be
// no backend configuration found.
emptyOk := opts.ConfigPath == ""
// Determine the path to the configuration.
path := opts.ConfigPath
// If we had no path set, it is an error. We can't initialize unset
if path == "" {
path = "."
}
// Expand the path
if !filepath.IsAbs(path) {
var err error
path, err = filepath.Abs(path)
if opts.Config == nil {
// check if the config was missing, or just not required
conf, err := m.Config(".")
if err != nil {
return nil, fmt.Errorf(
"Error expanding path to backend config %q: %s", path, err)
return nil, err
}
}
log.Printf("[DEBUG] command: loading backend config file: %s", path)
// We first need to determine if we're loading a file or a directory.
fi, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) && emptyOk {
log.Printf(
"[INFO] command: backend config not found, returning nil: %s",
path)
if conf == nil {
log.Println("[INFO] command: no config, returning nil")
return nil, nil
}
return nil, err
log.Println("[WARNING] BackendOpts.Config not set, but config found")
opts.Config = conf
}
var f func(string) (*config.Config, error) = config.LoadFile
if fi.IsDir() {
f = config.LoadDir
}
// Load the configuration
c, err := f(path)
if err != nil {
// Check for the error where we have no config files and return nil
// as the configuration type.
if errwrap.ContainsType(err, new(config.ErrNoConfigsFound)) {
log.Printf(
"[INFO] command: backend config not found, returning nil: %s",
path)
return nil, nil
}
return nil, err
}
c := opts.Config
// If there is no Terraform configuration block, no backend config
if c.Terraform == nil {
log.Println("[INFO] command: empty terraform config, returning nil")
return nil, nil
}
// Get the configuration for the backend itself.
backend := c.Terraform.Backend
if backend == nil {
log.Println("[INFO] command: empty backend config, returning nil")
return nil, nil
}

View File

@ -2,7 +2,9 @@ package command
import (
"fmt"
"log"
"os"
"path/filepath"
"strconv"
"github.com/hashicorp/errwrap"
@ -51,6 +53,66 @@ func (m *Meta) Module(path string) (*module.Tree, error) {
return mod, nil
}
// Config loads the root config for the path specified. Path may be a directory
// or file. The absence of configuration is not an error and returns a nil Config.
func (m *Meta) Config(path string) (*config.Config, error) {
// If no explicit path was given then it is okay for there to be
// no backend configuration found.
emptyOk := path == ""
// If we had no path set, it is an error. We can't initialize unset
if path == "" {
path = "."
}
// Expand the path
if !filepath.IsAbs(path) {
var err error
path, err = filepath.Abs(path)
if err != nil {
return nil, fmt.Errorf(
"Error expanding path to backend config %q: %s", path, err)
}
}
log.Printf("[DEBUG] command: loading backend config file: %s", path)
// We first need to determine if we're loading a file or a directory.
fi, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) && emptyOk {
log.Printf(
"[INFO] command: backend config not found, returning nil: %s",
path)
return nil, nil
}
return nil, err
}
var f func(string) (*config.Config, error) = config.LoadFile
if fi.IsDir() {
f = config.LoadDir
}
// Load the configuration
c, err := f(path)
if err != nil {
// Check for the error where we have no config files and return nil
// as the configuration type.
if errwrap.ContainsType(err, new(config.ErrNoConfigsFound)) {
log.Printf(
"[INFO] command: backend config not found, returning nil: %s",
path)
return nil, nil
}
return nil, err
}
return c, nil
}
// Plan returns the plan for the given path.
//
// This only has an effect if the path itself looks like a plan.

View File

@ -31,8 +31,8 @@ func TestOutput(t *testing.T) {
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -79,8 +79,8 @@ func TestModuleOutput(t *testing.T) {
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -129,8 +129,8 @@ func TestModuleOutputs(t *testing.T) {
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -177,8 +177,8 @@ func TestOutput_nestedListAndMap(t *testing.T) {
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -216,8 +216,8 @@ func TestOutput_json(t *testing.T) {
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -256,8 +256,8 @@ func TestMissingModuleOutput(t *testing.T) {
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -292,8 +292,8 @@ func TestOutput_badVar(t *testing.T) {
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -330,8 +330,8 @@ func TestOutput_blank(t *testing.T) {
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -355,8 +355,8 @@ func TestOutput_manyArgs(t *testing.T) {
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -373,8 +373,8 @@ func TestOutput_noArgs(t *testing.T) {
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -391,8 +391,8 @@ func TestOutput_noState(t *testing.T) {
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -420,8 +420,8 @@ func TestOutput_noVars(t *testing.T) {
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -480,8 +480,8 @@ func TestOutput_stateDefault(t *testing.T) {
ui := new(cli.MockUi)
c := &OutputCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}

View File

@ -6,6 +6,7 @@ import (
"strings"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/module"
)
@ -68,10 +69,14 @@ func (c *PlanCommand) Run(args []string) int {
}
}
var conf *config.Config
if mod != nil {
conf = mod.Config()
}
// Load the backend
b, err := c.Backend(&BackendOpts{
ConfigPath: configPath,
Plan: plan,
Config: conf,
Plan: plan,
})
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))

View File

@ -28,8 +28,8 @@ func TestPlan(t *testing.T) {
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -61,8 +61,8 @@ func TestPlan_lockedState(t *testing.T) {
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -89,8 +89,8 @@ func TestPlan_plan(t *testing.T) {
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -128,8 +128,8 @@ func TestPlan_destroy(t *testing.T) {
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -165,8 +165,8 @@ func TestPlan_noState(t *testing.T) {
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -205,8 +205,8 @@ func TestPlan_outPath(t *testing.T) {
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -262,8 +262,8 @@ func TestPlan_outPathNoChange(t *testing.T) {
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -318,8 +318,8 @@ func TestPlan_outBackend(t *testing.T) {
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -382,8 +382,8 @@ func TestPlan_outBackendLegacy(t *testing.T) {
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -412,8 +412,8 @@ func TestPlan_refresh(t *testing.T) {
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -450,8 +450,8 @@ func TestPlan_state(t *testing.T) {
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -506,8 +506,8 @@ func TestPlan_stateDefault(t *testing.T) {
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -535,8 +535,8 @@ func TestPlan_stateFuture(t *testing.T) {
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -576,8 +576,8 @@ func TestPlan_statePast(t *testing.T) {
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -608,8 +608,8 @@ func TestPlan_validate(t *testing.T) {
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -632,8 +632,8 @@ func TestPlan_vars(t *testing.T) {
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -676,8 +676,8 @@ func TestPlan_varsUnset(t *testing.T) {
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -702,8 +702,8 @@ func TestPlan_varFile(t *testing.T) {
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -752,8 +752,8 @@ func TestPlan_varFileDefault(t *testing.T) {
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -795,8 +795,8 @@ func TestPlan_detailedExitcode(t *testing.T) {
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -820,8 +820,8 @@ func TestPlan_detailedExitcode_emptyDiff(t *testing.T) {
ui := new(cli.MockUi)
c := &PlanCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}

234
command/plugins.go Normal file
View File

@ -0,0 +1,234 @@
package command
import (
"fmt"
"log"
"os/exec"
"path/filepath"
"runtime"
"strings"
plugin "github.com/hashicorp/go-plugin"
tfplugin "github.com/hashicorp/terraform/plugin"
"github.com/hashicorp/terraform/plugin/discovery"
"github.com/hashicorp/terraform/terraform"
"github.com/kardianos/osext"
)
// multiVersionProviderResolver is an implementation of
// terraform.ResourceProviderResolver that matches the given version constraints
// against a set of versioned provider plugins to find the newest version of
// each that satisfies the given constraints.
type multiVersionProviderResolver struct {
Available discovery.PluginMetaSet
}
func choosePlugins(avail discovery.PluginMetaSet, reqd discovery.PluginRequirements) map[string]discovery.PluginMeta {
candidates := avail.ConstrainVersions(reqd)
ret := map[string]discovery.PluginMeta{}
for name, metas := range candidates {
if len(metas) == 0 {
continue
}
ret[name] = metas.Newest()
}
return ret
}
func (r *multiVersionProviderResolver) ResolveProviders(
reqd discovery.PluginRequirements,
) (map[string]terraform.ResourceProviderFactory, []error) {
factories := make(map[string]terraform.ResourceProviderFactory, len(reqd))
var errs []error
chosen := choosePlugins(r.Available, reqd)
for name := range reqd {
if newest, available := chosen[name]; available {
digest, err := newest.SHA256()
if err != nil {
errs = append(errs, fmt.Errorf("provider.%s: failed to load plugin to verify its signature: %s", name, err))
continue
}
if !reqd[name].AcceptsSHA256(digest) {
// This generic error message is intended to avoid troubling
// users with implementation details. The main useful point
// here is that they need to run "terraform init" to
// fix this, which is covered by the UI code reporting these
// error messages.
errs = append(errs, fmt.Errorf("provider.%s: installed but not yet initialized", name))
continue
}
client := tfplugin.Client(newest)
factories[name] = providerFactory(client)
} else {
errs = append(errs, fmt.Errorf("provider.%s: no suitable version installed", name))
}
}
return factories, errs
}
// the default location for automatically installed plugins
func (m *Meta) pluginDir() string {
return filepath.Join(m.DataDir(), "plugins", fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH))
}
// pluginDirs return a list of directories to search for plugins.
//
// Earlier entries in this slice get priority over later when multiple copies
// of the same plugin version are found, but newer versions always override
// older versions where both satisfy the provider version constraints.
func (m *Meta) pluginDirs() []string {
// When searching the following directories, earlier entries get precedence
// if the same plugin version is found twice, but newer versions will
// always get preference below regardless of where they are coming from.
// TODO: Add auto-install dir, default vendor dir and optional override
// vendor dir(s).
dirs := []string{"."}
// Look in the same directory as the Terraform executable.
// If found, this replaces what we found in the config path.
exePath, err := osext.Executable()
if err != nil {
log.Printf("[ERROR] Error discovering exe directory: %s", err)
} else {
dirs = append(dirs, filepath.Dir(exePath))
}
dirs = append(dirs, m.pluginDir())
dirs = append(dirs, m.GlobalPluginDirs...)
return dirs
}
// providerPluginSet returns the set of valid providers that were discovered in
// the defined search paths.
func (m *Meta) providerPluginSet() discovery.PluginMetaSet {
plugins := discovery.FindPlugins("provider", m.pluginDirs())
plugins, _ = plugins.ValidateVersions()
for p := range plugins {
log.Printf("[DEBUG] found valid plugin: %q", p.Name)
}
return plugins
}
func (m *Meta) providerResolver() terraform.ResourceProviderResolver {
return &multiVersionProviderResolver{
Available: m.providerPluginSet(),
}
}
// filter the requirements returning only the providers that we can't resolve
func (m *Meta) missingPlugins(avail discovery.PluginMetaSet, reqd discovery.PluginRequirements) discovery.PluginRequirements {
missing := make(discovery.PluginRequirements)
for n, r := range reqd {
log.Printf("[DEBUG] plugin requirements: %q=%q", n, r.Versions)
}
candidates := avail.ConstrainVersions(reqd)
for name, versionSet := range reqd {
if metas := candidates[name]; metas.Count() == 0 {
missing[name] = versionSet
}
}
return missing
}
func (m *Meta) provisionerFactories() map[string]terraform.ResourceProvisionerFactory {
dirs := m.pluginDirs()
plugins := discovery.FindPlugins("provisioner", dirs)
plugins, _ = plugins.ValidateVersions()
// For now our goal is to just find the latest version of each plugin
// we have on the system. All provisioners should be at version 0.0.0
// currently, so there should actually only be one instance of each plugin
// name here, even though the discovery interface forces us to pretend
// that might not be true.
factories := make(map[string]terraform.ResourceProvisionerFactory)
// Wire up the internal provisioners first. These might be overridden
// by discovered provisioners below.
for name := range InternalProvisioners {
client, err := internalPluginClient("provisioner", name)
if err != nil {
log.Printf("[WARN] failed to build command line for internal plugin %q: %s", name, err)
continue
}
factories[name] = provisionerFactory(client)
}
byName := plugins.ByName()
for name, metas := range byName {
// Since we validated versions above and we partitioned the sets
// by name, we're guaranteed that the metas in our set all have
// valid versions and that there's at least one meta.
newest := metas.Newest()
client := tfplugin.Client(newest)
factories[name] = provisionerFactory(client)
}
return factories
}
func internalPluginClient(kind, name string) (*plugin.Client, error) {
cmdLine, err := BuildPluginCommandString(kind, name)
if err != nil {
return nil, err
}
// See the docstring for BuildPluginCommandString for why we need to do
// this split here.
cmdArgv := strings.Split(cmdLine, TFSPACE)
cfg := &plugin.ClientConfig{
Cmd: exec.Command(cmdArgv[0], cmdArgv[1:]...),
HandshakeConfig: tfplugin.Handshake,
Managed: true,
Plugins: tfplugin.PluginMap,
}
return plugin.NewClient(cfg), nil
}
func providerFactory(client *plugin.Client) terraform.ResourceProviderFactory {
return func() (terraform.ResourceProvider, error) {
// Request the RPC client so we can get the provider
// so we can build the actual RPC-implemented provider.
rpcClient, err := client.Client()
if err != nil {
return nil, err
}
raw, err := rpcClient.Dispense(tfplugin.ProviderPluginName)
if err != nil {
return nil, err
}
return raw.(terraform.ResourceProvider), nil
}
}
func provisionerFactory(client *plugin.Client) terraform.ResourceProvisionerFactory {
return func() (terraform.ResourceProvisioner, error) {
// Request the RPC client so we can get the provisioner
// so we can build the actual RPC-implemented provisioner.
rpcClient, err := client.Client()
if err != nil {
return nil, err
}
raw, err := rpcClient.Dispense(tfplugin.ProvisionerPluginName)
if err != nil {
return nil, err
}
return raw.(terraform.ResourceProvisioner), nil
}
}

86
command/plugins_lock.go Normal file
View File

@ -0,0 +1,86 @@
package command
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
)
func (m *Meta) providerPluginsLock() *pluginSHA256LockFile {
return &pluginSHA256LockFile{
Filename: filepath.Join(m.pluginDir(), "lock.json"),
}
}
type pluginSHA256LockFile struct {
Filename string
}
// Read loads the lock information from the file and returns it. If the file
// cannot be read, an empty map is returned to indicate that _no_ providers
// are acceptable, since the user must run "terraform init" to lock some
// providers before a context can be created.
func (pf *pluginSHA256LockFile) Read() map[string][]byte {
// Returning an empty map is different than nil because it causes
// us to reject all plugins as uninitialized, rather than applying no
// constraints at all.
//
// We don't surface any specific errors here because we want it to all
// roll up into our more-user-friendly error that appears when plugin
// constraint verification fails during context creation.
digests := make(map[string][]byte)
buf, err := ioutil.ReadFile(pf.Filename)
if err != nil {
// This is expected if the user runs any context-using command before
// running "terraform init".
log.Printf("[INFO] Failed to read plugin lock file %s: %s", pf.Filename, err)
return digests
}
var strDigests map[string]string
err = json.Unmarshal(buf, &strDigests)
if err != nil {
// This should never happen unless the user directly edits the file.
log.Printf("[WARNING] Plugin lock file %s failed to parse as JSON: %s", pf.Filename, err)
return digests
}
for name, strDigest := range strDigests {
var digest []byte
_, err := fmt.Sscanf(strDigest, "%x", &digest)
if err == nil {
digests[name] = digest
} else {
// This should never happen unless the user directly edits the file.
log.Printf("[WARNING] Plugin lock file %s has invalid digest for %q", pf.Filename, name)
}
}
return digests
}
// Write persists lock information to disk, where it will be retrieved by
// future calls to Read. This entirely replaces any previous lock information,
// so the given map must be comprehensive.
func (pf *pluginSHA256LockFile) Write(digests map[string][]byte) error {
strDigests := map[string]string{}
for name, digest := range digests {
strDigests[name] = fmt.Sprintf("%x", digest)
}
buf, err := json.MarshalIndent(strDigests, "", " ")
if err != nil {
// should never happen
return fmt.Errorf("failed to serialize plugin lock as JSON: %s", err)
}
os.MkdirAll(
filepath.Dir(pf.Filename), os.ModePerm,
) // ignore error since WriteFile below will generate a better one anyway
return ioutil.WriteFile(pf.Filename, buf, os.ModePerm)
}

View File

@ -0,0 +1,40 @@
package command
import (
"io/ioutil"
"reflect"
"testing"
)
func TestPluginSHA256LockFile(t *testing.T) {
f, err := ioutil.TempFile("", "tf-pluginsha1lockfile-test-")
if err != nil {
t.Fatalf("failed to create temporary file: %s", err)
}
f.Close()
//defer os.Remove(f.Name())
t.Logf("working in %s", f.Name())
plf := &pluginSHA256LockFile{
Filename: f.Name(),
}
// Initially the file is invalid, so we should get an empty map.
digests := plf.Read()
if !reflect.DeepEqual(digests, map[string][]byte{}) {
t.Errorf("wrong initial content %#v; want empty map", digests)
}
digests = map[string][]byte{
"test": []byte("hello world"),
}
err = plf.Write(digests)
if err != nil {
t.Fatalf("failed to write lock file: %s", err)
}
got := plf.Read()
if !reflect.DeepEqual(got, digests) {
t.Errorf("wrong content %#v after write; want %#v", got, digests)
}
}

56
command/plugins_test.go Normal file
View File

@ -0,0 +1,56 @@
package command
import (
"fmt"
"os"
"path/filepath"
"github.com/hashicorp/terraform/plugin/discovery"
)
// mockGetProvider providers a GetProvider method for testing automatic
// provider downloads
type mockGetProvider struct {
// A map of provider names to available versions.
// The tests expect the versions to be in order from newest to oldest.
Providers map[string][]string
}
func (m mockGetProvider) FileName(provider, version string) string {
return fmt.Sprintf("terraform-provider-%s_v%s_x4", provider, version)
}
// GetProvider will check the Providers map to see if it can find a suitable
// version, and put an empty file in the dst directory.
func (m mockGetProvider) GetProvider(dst, provider string, req discovery.Constraints, protoVersion uint) error {
versions := m.Providers[provider]
if len(versions) == 0 {
return fmt.Errorf("provider %q not found", provider)
}
err := os.MkdirAll(dst, 0755)
if err != nil {
return fmt.Errorf("error creating plugins directory: %s", err)
}
for _, v := range versions {
version, err := discovery.VersionStr(v).Parse()
if err != nil {
panic(err)
}
if req.Allows(version) {
// provider filename
name := m.FileName(provider, v)
path := filepath.Join(dst, name)
f, err := os.Create(path)
if err != nil {
return fmt.Errorf("error fetching provider: %s", err)
}
f.Close()
return nil
}
}
return fmt.Errorf("no suitable version for provider %q found with constraints %s", provider, req)
}

127
command/providers.go Normal file
View File

@ -0,0 +1,127 @@
package command
import (
"fmt"
"sort"
"github.com/hashicorp/terraform/moduledeps"
"github.com/hashicorp/terraform/terraform"
"github.com/xlab/treeprint"
)
// ProvidersCommand is a Command implementation that prints out information
// about the providers used in the current configuration/state.
type ProvidersCommand struct {
Meta
}
func (c *ProvidersCommand) Help() string {
return providersCommandHelp
}
func (c *ProvidersCommand) Synopsis() string {
return "Prints a tree of the providers used in the configuration"
}
func (c *ProvidersCommand) Run(args []string) int {
cmdFlags := c.Meta.flagSet("providers")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
}
configPath, err := ModulePath(cmdFlags.Args())
if err != nil {
c.Ui.Error(err.Error())
return 1
}
// Load the config
root, err := c.Module(configPath)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err))
return 1
}
// Validate the config (to ensure the version constraints are valid)
err = root.Validate()
if err != nil {
c.Ui.Error(err.Error())
return 1
}
// Load the backend
b, err := c.Backend(&BackendOpts{
Config: root.Config(),
})
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
return 1
}
// Get the state
env := c.Env()
state, err := b.State(env)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
return 1
}
if err := state.RefreshState(); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
return 1
}
s := state.State()
depTree := terraform.ModuleTreeDependencies(root, s)
depTree.SortDescendents()
printRoot := treeprint.New()
providersCommandPopulateTreeNode(printRoot, depTree)
c.Ui.Output(printRoot.String())
return 0
}
func providersCommandPopulateTreeNode(node treeprint.Tree, deps *moduledeps.Module) {
names := make([]string, 0, len(deps.Providers))
for name := range deps.Providers {
names = append(names, string(name))
}
sort.Strings(names)
for _, name := range names {
dep := deps.Providers[moduledeps.ProviderInstance(name)]
versionsStr := dep.Constraints.String()
if versionsStr != "" {
versionsStr = " " + versionsStr
}
var reasonStr string
switch dep.Reason {
case moduledeps.ProviderDependencyInherited:
reasonStr = " (inherited)"
case moduledeps.ProviderDependencyFromState:
reasonStr = " (from state)"
}
node.AddNode(fmt.Sprintf("provider.%s%s%s", name, versionsStr, reasonStr))
}
for _, child := range deps.Children {
childNode := node.AddBranch(fmt.Sprintf("module.%s", child.Name))
providersCommandPopulateTreeNode(childNode, child)
}
}
const providersCommandHelp = `
Usage: terraform providers [dir]
Prints out a tree of modules in the referenced configuration annotated with
their provider requirements.
This provides an overview of all of the provider requirements across all
referenced modules, as an aid to understanding why particular provider
plugins are needed and why particular versions are selected.
`

43
command/providers_test.go Normal file
View File

@ -0,0 +1,43 @@
package command
import (
"os"
"strings"
"testing"
"github.com/mitchellh/cli"
)
func TestProviders(t *testing.T) {
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("err: %s", err)
}
if err := os.Chdir(testFixturePath("providers")); err != nil {
t.Fatalf("err: %s", err)
}
defer os.Chdir(cwd)
ui := new(cli.MockUi)
c := &ProvidersCommand{
Meta: Meta{
Ui: ui,
},
}
args := []string{}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
output := ui.OutputWriter.String()
if !strings.Contains(output, "provider.foo") {
t.Errorf("output missing provider.foo\n\n%s", output)
}
if !strings.Contains(output, "provider.bar") {
t.Errorf("output missing provider.bar\n\n%s", output)
}
if !strings.Contains(output, "provider.baz") {
t.Errorf("output missing provider.baz\n\n%s", output)
}
}

View File

@ -11,6 +11,7 @@ import (
"github.com/hashicorp/atlas-go/archive"
"github.com/hashicorp/atlas-go/v1"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/terraform"
)
@ -98,9 +99,14 @@ func (c *PushCommand) Run(args []string) int {
return 1
}
var conf *config.Config
if mod != nil {
conf = mod.Config()
}
// Load the backend
b, err := c.Backend(&BackendOpts{
ConfigPath: configPath,
Config: conf,
})
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))

View File

@ -4,10 +4,12 @@ import (
"archive/tar"
"bytes"
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"runtime"
"sort"
"strings"
"testing"
@ -40,8 +42,8 @@ func TestPush_good(t *testing.T) {
ui := new(cli.MockUi)
c := &PushCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
client: client,
@ -101,8 +103,8 @@ func TestPush_goodBackendInit(t *testing.T) {
ui = new(cli.MockUi)
c := &PushCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
client: client,
@ -121,6 +123,9 @@ func TestPush_goodBackendInit(t *testing.T) {
// Expected weird behavior, doesn't affect unpackaging
".terraform/",
".terraform/",
".terraform/plugins/",
fmt.Sprintf(".terraform/plugins/%s_%s/", runtime.GOOS, runtime.GOARCH),
fmt.Sprintf(".terraform/plugins/%s_%s/lock.json", runtime.GOOS, runtime.GOARCH),
".terraform/terraform.tfstate",
".terraform/terraform.tfstate",
"main.tf",
@ -148,8 +153,8 @@ func TestPush_noUploadModules(t *testing.T) {
ui := new(cli.MockUi)
c := &PushCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
client: client,
@ -168,8 +173,8 @@ func TestPush_noUploadModules(t *testing.T) {
ui := new(cli.MockUi)
c := &GetCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -238,8 +243,8 @@ func TestPush_input(t *testing.T) {
ui := new(cli.MockUi)
c := &PushCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
client: client,
@ -297,8 +302,8 @@ func TestPush_inputPartial(t *testing.T) {
ui := new(cli.MockUi)
c := &PushCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
client: client,
@ -367,8 +372,8 @@ func TestPush_localOverride(t *testing.T) {
ui := new(cli.MockUi)
c := &PushCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
client: client,
@ -446,8 +451,8 @@ func TestPush_remoteOverride(t *testing.T) {
ui := new(cli.MockUi)
c := &PushCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
client: client,
@ -537,8 +542,8 @@ func TestPush_preferAtlas(t *testing.T) {
ui := new(cli.MockUi)
c := &PushCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
client: client,
@ -613,8 +618,8 @@ func TestPush_tfvars(t *testing.T) {
ui := new(cli.MockUi)
c := &PushCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
client: client,
@ -688,8 +693,8 @@ func TestPush_name(t *testing.T) {
ui := new(cli.MockUi)
c := &PushCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
client: client,
@ -716,8 +721,8 @@ func TestPush_noState(t *testing.T) {
ui := new(cli.MockUi)
c := &PushCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -801,8 +806,8 @@ func TestPush_plan(t *testing.T) {
ui := new(cli.MockUi)
c := &PushCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}

View File

@ -6,6 +6,7 @@ import (
"strings"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/terraform"
)
@ -43,8 +44,15 @@ func (c *RefreshCommand) Run(args []string) int {
return 1
}
var conf *config.Config
if mod != nil {
conf = mod.Config()
}
// Load the backend
b, err := c.Backend(&BackendOpts{ConfigPath: configPath})
b, err := c.Backend(&BackendOpts{
Config: conf,
})
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))
return 1

View File

@ -22,8 +22,8 @@ func TestRefresh(t *testing.T) {
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -71,8 +71,8 @@ func TestRefresh_empty(t *testing.T) {
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -105,8 +105,8 @@ func TestRefresh_lockedState(t *testing.T) {
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -145,8 +145,8 @@ func TestRefresh_cwd(t *testing.T) {
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -217,8 +217,8 @@ func TestRefresh_defaultState(t *testing.T) {
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -290,8 +290,8 @@ func TestRefresh_futureState(t *testing.T) {
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -333,8 +333,8 @@ func TestRefresh_pastState(t *testing.T) {
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -392,8 +392,8 @@ func TestRefresh_outPath(t *testing.T) {
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -467,8 +467,8 @@ func TestRefresh_var(t *testing.T) {
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -497,8 +497,8 @@ func TestRefresh_varFile(t *testing.T) {
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -532,8 +532,8 @@ func TestRefresh_varFileDefault(t *testing.T) {
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -582,8 +582,8 @@ func TestRefresh_varsUnset(t *testing.T) {
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -622,8 +622,8 @@ func TestRefresh_backup(t *testing.T) {
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -707,8 +707,8 @@ func TestRefresh_disableBackup(t *testing.T) {
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -776,8 +776,8 @@ func TestRefresh_displaysOutputs(t *testing.T) {
ui := new(cli.MockUi)
c := &RefreshCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}

View File

@ -16,8 +16,8 @@ func TestShow(t *testing.T) {
ui := new(cli.MockUi)
c := &ShowCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -61,8 +61,8 @@ func TestShow_noArgs(t *testing.T) {
ui := new(cli.MockUi)
c := &ShowCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -93,8 +93,8 @@ func TestShow_noArgsNoState(t *testing.T) {
ui := new(cli.MockUi)
c := &ShowCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -112,8 +112,8 @@ func TestShow_plan(t *testing.T) {
ui := new(cli.MockUi)
c := &ShowCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -138,8 +138,8 @@ func TestShow_noArgsRemoteState(t *testing.T) {
ui := new(cli.MockUi)
c := &ShowCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}
@ -162,8 +162,8 @@ func TestShow_state(t *testing.T) {
ui := new(cli.MockUi)
c := &ShowCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
testingOverrides: metaOverridesForProvider(testProvider()),
Ui: ui,
},
}

View File

@ -17,8 +17,8 @@ func TestStateList(t *testing.T) {
ui := new(cli.MockUi)
c := &StateListCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -48,8 +48,8 @@ func TestStateList_backendState(t *testing.T) {
ui := new(cli.MockUi)
c := &StateListCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -74,8 +74,8 @@ func TestStateList_noState(t *testing.T) {
ui := new(cli.MockUi)
c := &StateListCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}

View File

@ -47,8 +47,8 @@ func TestStateMv(t *testing.T) {
ui := new(cli.MockUi)
c := &StateMvCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -114,8 +114,8 @@ func TestStateMv_backupExplicit(t *testing.T) {
ui := new(cli.MockUi)
c := &StateMvCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -169,8 +169,8 @@ func TestStateMv_stateOutNew(t *testing.T) {
ui := new(cli.MockUi)
c := &StateMvCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -241,8 +241,8 @@ func TestStateMv_stateOutExisting(t *testing.T) {
ui := new(cli.MockUi)
c := &StateMvCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -282,8 +282,8 @@ func TestStateMv_noState(t *testing.T) {
ui := new(cli.MockUi)
c := &StateMvCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -343,8 +343,8 @@ func TestStateMv_stateOutNew_count(t *testing.T) {
ui := new(cli.MockUi)
c := &StateMvCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -521,8 +521,8 @@ func TestStateMv_stateOutNew_largeCount(t *testing.T) {
ui := new(cli.MockUi)
c := &StateMvCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -602,8 +602,8 @@ func TestStateMv_stateOutNew_nestedModule(t *testing.T) {
ui := new(cli.MockUi)
c := &StateMvCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}

View File

@ -21,8 +21,8 @@ func TestStatePull(t *testing.T) {
ui := new(cli.MockUi)
c := &StatePullCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -46,8 +46,8 @@ func TestStatePull_noState(t *testing.T) {
ui := new(cli.MockUi)
c := &StatePullCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}

View File

@ -23,8 +23,8 @@ func TestStatePush_empty(t *testing.T) {
ui := new(cli.MockUi)
c := &StatePushCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -52,8 +52,8 @@ func TestStatePush_replaceMatch(t *testing.T) {
ui := new(cli.MockUi)
c := &StatePushCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -88,8 +88,8 @@ func TestStatePush_replaceMatchStdin(t *testing.T) {
ui := new(cli.MockUi)
c := &StatePushCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -117,8 +117,8 @@ func TestStatePush_lineageMismatch(t *testing.T) {
ui := new(cli.MockUi)
c := &StatePushCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -146,8 +146,8 @@ func TestStatePush_serialNewer(t *testing.T) {
ui := new(cli.MockUi)
c := &StatePushCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -175,8 +175,8 @@ func TestStatePush_serialOlder(t *testing.T) {
ui := new(cli.MockUi)
c := &StatePushCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}

View File

@ -47,8 +47,8 @@ func TestStateRm(t *testing.T) {
ui := new(cli.MockUi)
c := &StateRmCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -113,8 +113,8 @@ func TestStateRm_backupExplicit(t *testing.T) {
ui := new(cli.MockUi)
c := &StateRmCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -147,8 +147,8 @@ func TestStateRm_noState(t *testing.T) {
ui := new(cli.MockUi)
c := &StateRmCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}

View File

@ -35,8 +35,8 @@ func TestStateShow(t *testing.T) {
ui := new(cli.MockUi)
c := &StateShowCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -93,8 +93,8 @@ func TestStateShow_multi(t *testing.T) {
ui := new(cli.MockUi)
c := &StateShowCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -115,8 +115,8 @@ func TestStateShow_noState(t *testing.T) {
ui := new(cli.MockUi)
c := &StateShowCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -135,8 +135,8 @@ func TestStateShow_emptyState(t *testing.T) {
ui := new(cli.MockUi)
c := &StateShowCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}
@ -164,8 +164,8 @@ func TestStateShow_emptyStateWithModule(t *testing.T) {
ui := new(cli.MockUi)
c := &StateShowCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}

View File

@ -0,0 +1,5 @@
provider "test" {
}
# No resource block present, so import fails

View File

@ -0,0 +1,8 @@
provider "test" {
foo = "bar"
alias = "alias"
}
resource "test_instance" "foo" {
}

View File

@ -0,0 +1,4 @@
# Declaring this resource implies that we depend on the
# "test" provider, making it available for import.
resource "test_instance" "foo" {
}

View File

@ -3,3 +3,6 @@ variable "foo" {}
provider "test" {
foo = "${var.foo}"
}
resource "test_instance" "foo" {
}

View File

@ -3,3 +3,6 @@ variable "foo" {}
provider "test" {
foo = "${var.foo}"
}
resource "test_instance" "foo" {
}

View File

@ -3,3 +3,6 @@ variable "foo" {}
provider "test" {
foo = "${var.foo}"
}
resource "test_instance" "foo" {
}

View File

@ -1,3 +1,6 @@
provider "test" {
foo = "bar"
}
resource "test_instance" "foo" {
}

View File

@ -0,0 +1,11 @@
provider "exact" {
version = "1.2.3"
}
provider "greater_than" {
version = ">= 2.3.3"
}
provider "between" {
version = "> 1.0.0 , < 3.0.0"
}

View File

@ -0,0 +1,3 @@
provider "test" {
version = "1.2.3"
}

View File

@ -0,0 +1,3 @@
provider "test" {
version = "1.2.3"
}

View File

@ -0,0 +1,11 @@
provider "foo" {
}
resource "bar_instance" "test" {
}
provider "baz" {
version = "1.2.0"
}

View File

@ -1,4 +1,4 @@
resource "aws_instance" "foo" {}
resource "test_instance" "foo" {}
atlas {
name = "foo"

View File

@ -43,9 +43,15 @@ func (c *UnlockCommand) Run(args []string) int {
return 1
}
conf, err := c.Config(configPath)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load root config module: %s", err))
return 1
}
// Load the backend
b, err := c.Backend(&BackendOpts{
ConfigPath: configPath,
Config: conf,
})
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load backend: %s", err))

View File

@ -35,8 +35,8 @@ func TestUnlock(t *testing.T) {
ui := new(cli.MockUi)
c := &UnlockCommand{
Meta: Meta{
ContextOpts: testCtxConfig(p),
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
Ui: ui,
},
}

View File

@ -30,9 +30,10 @@ func init() {
}
meta := command.Meta{
Color: true,
ContextOpts: &ContextOpts,
Ui: Ui,
Color: true,
GlobalPluginDirs: globalPluginDirs(),
PluginOverrides: &PluginOverrides,
Ui: Ui,
}
// The command list is included in the terraform -help
@ -148,6 +149,12 @@ func init() {
}, nil
},
"providers": func() (cli.Command, error) {
return &command.ProvidersCommand{
Meta: meta,
}, nil
},
"push": func() (cli.Command, error) {
return &command.PushCommand{
Meta: meta,

265
config.go
View File

@ -6,17 +6,9 @@ import (
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/hcl"
"github.com/hashicorp/terraform/command"
tfplugin "github.com/hashicorp/terraform/plugin"
"github.com/hashicorp/terraform/terraform"
"github.com/kardianos/osext"
"github.com/mitchellh/cli"
)
// Config is the structure of the configuration for the Terraform CLI.
@ -35,8 +27,9 @@ type Config struct {
// can be overridden by user configurations.
var BuiltinConfig Config
// ContextOpts are the global ContextOpts we use to initialize the CLI.
var ContextOpts terraform.ContextOpts
// PluginOverrides are paths that override discovered plugins, set from
// the config file.
var PluginOverrides command.PluginOverrides
// ConfigFile returns the default path to the configuration file.
//
@ -85,88 +78,6 @@ func LoadConfig(path string) (*Config, error) {
return &result, nil
}
// Discover plugins located on disk, and fall back on plugins baked into the
// Terraform binary.
//
// We look in the following places for plugins:
//
// 1. Terraform configuration path
// 2. Path where Terraform is installed
// 3. Path where Terraform is invoked
//
// Whichever file is discoverd LAST wins.
//
// Finally, we look at the list of plugins compiled into Terraform. If any of
// them has not been found on disk we use the internal version. This allows
// users to add / replace plugins without recompiling the main binary.
func (c *Config) Discover(ui cli.Ui) error {
// Look in ~/.terraform.d/plugins/
dir, err := ConfigDir()
if err != nil {
log.Printf("[ERR] Error loading config directory: %s", err)
} else {
if err := c.discover(filepath.Join(dir, "plugins")); err != nil {
return err
}
}
// Next, look in the same directory as the Terraform executable, usually
// /usr/local/bin. If found, this replaces what we found in the config path.
exePath, err := osext.Executable()
if err != nil {
log.Printf("[ERR] Error loading exe directory: %s", err)
} else {
if err := c.discover(filepath.Dir(exePath)); err != nil {
return err
}
}
// Finally look in the cwd (where we are invoke Terraform). If found, this
// replaces anything we found in the config / install paths.
if err := c.discover("."); err != nil {
return err
}
// Finally, if we have a plugin compiled into Terraform and we didn't find
// a replacement on disk, we'll just use the internal version. Only do this
// from the main process, or the log output will break the plugin handshake.
if os.Getenv("TF_PLUGIN_MAGIC_COOKIE") == "" {
for name, _ := range command.InternalProviders {
if path, found := c.Providers[name]; found {
// Allow these warnings to be suppressed via TF_PLUGIN_DEV=1 or similar
if os.Getenv("TF_PLUGIN_DEV") == "" {
ui.Warn(fmt.Sprintf("[WARN] %s overrides an internal plugin for %s-provider.\n"+
" If you did not expect to see this message you will need to remove the old plugin.\n"+
" See https://www.terraform.io/docs/internals/internal-plugins.html", path, name))
}
} else {
cmd, err := command.BuildPluginCommandString("provider", name)
if err != nil {
return err
}
c.Providers[name] = cmd
}
}
for name, _ := range command.InternalProvisioners {
if path, found := c.Provisioners[name]; found {
if os.Getenv("TF_PLUGIN_DEV") == "" {
ui.Warn(fmt.Sprintf("[WARN] %s overrides an internal plugin for %s-provisioner.\n"+
" If you did not expect to see this message you will need to remove the old plugin.\n"+
" See https://www.terraform.io/docs/internals/internal-plugins.html", path, name))
}
} else {
cmd, err := command.BuildPluginCommandString("provisioner", name)
if err != nil {
return err
}
c.Provisioners[name] = cmd
}
}
}
return nil
}
// Merge merges two configurations and returns a third entirely
// new configuration with the two merged.
func (c1 *Config) Merge(c2 *Config) *Config {
@ -196,173 +107,3 @@ func (c1 *Config) Merge(c2 *Config) *Config {
return &result
}
func (c *Config) discover(path string) error {
var err error
if !filepath.IsAbs(path) {
path, err = filepath.Abs(path)
if err != nil {
return err
}
}
err = c.discoverSingle(
filepath.Join(path, "terraform-provider-*"), &c.Providers)
if err != nil {
return err
}
err = c.discoverSingle(
filepath.Join(path, "terraform-provisioner-*"), &c.Provisioners)
if err != nil {
return err
}
return nil
}
func (c *Config) discoverSingle(glob string, m *map[string]string) error {
matches, err := filepath.Glob(glob)
if err != nil {
return err
}
if *m == nil {
*m = make(map[string]string)
}
for _, match := range matches {
file := filepath.Base(match)
// If the filename has a ".", trim up to there
if idx := strings.Index(file, "."); idx >= 0 {
file = file[:idx]
}
// Look for foo-bar-baz. The plugin name is "baz"
parts := strings.SplitN(file, "-", 3)
if len(parts) != 3 {
continue
}
log.Printf("[DEBUG] Discovered plugin: %s = %s", parts[2], match)
(*m)[parts[2]] = match
}
return nil
}
// ProviderFactories returns the mapping of prefixes to
// ResourceProviderFactory that can be used to instantiate a
// binary-based plugin.
func (c *Config) ProviderFactories() map[string]terraform.ResourceProviderFactory {
result := make(map[string]terraform.ResourceProviderFactory)
for k, v := range c.Providers {
result[k] = c.providerFactory(v)
}
return result
}
func (c *Config) providerFactory(path string) terraform.ResourceProviderFactory {
// Build the plugin client configuration and init the plugin
var config plugin.ClientConfig
config.Cmd = pluginCmd(path)
config.HandshakeConfig = tfplugin.Handshake
config.Managed = true
config.Plugins = tfplugin.PluginMap
client := plugin.NewClient(&config)
return func() (terraform.ResourceProvider, error) {
// Request the RPC client so we can get the provider
// so we can build the actual RPC-implemented provider.
rpcClient, err := client.Client()
if err != nil {
return nil, err
}
raw, err := rpcClient.Dispense(tfplugin.ProviderPluginName)
if err != nil {
return nil, err
}
return raw.(terraform.ResourceProvider), nil
}
}
// ProvisionerFactories returns the mapping of prefixes to
// ResourceProvisionerFactory that can be used to instantiate a
// binary-based plugin.
func (c *Config) ProvisionerFactories() map[string]terraform.ResourceProvisionerFactory {
result := make(map[string]terraform.ResourceProvisionerFactory)
for k, v := range c.Provisioners {
result[k] = c.provisionerFactory(v)
}
return result
}
func (c *Config) provisionerFactory(path string) terraform.ResourceProvisionerFactory {
// Build the plugin client configuration and init the plugin
var config plugin.ClientConfig
config.HandshakeConfig = tfplugin.Handshake
config.Cmd = pluginCmd(path)
config.Managed = true
config.Plugins = tfplugin.PluginMap
client := plugin.NewClient(&config)
return func() (terraform.ResourceProvisioner, error) {
rpcClient, err := client.Client()
if err != nil {
return nil, err
}
raw, err := rpcClient.Dispense(tfplugin.ProvisionerPluginName)
if err != nil {
return nil, err
}
return raw.(terraform.ResourceProvisioner), nil
}
}
func pluginCmd(path string) *exec.Cmd {
cmdPath := ""
// If the path doesn't contain a separator, look in the same
// directory as the Terraform executable first.
if !strings.ContainsRune(path, os.PathSeparator) {
exePath, err := osext.Executable()
if err == nil {
temp := filepath.Join(
filepath.Dir(exePath),
filepath.Base(path))
if _, err := os.Stat(temp); err == nil {
cmdPath = temp
}
}
// If we still haven't found the executable, look for it
// in the PATH.
if v, err := exec.LookPath(path); err == nil {
cmdPath = v
}
}
// No plugin binary found, so try to use an internal plugin.
if strings.Contains(path, command.TFSPACE) {
parts := strings.Split(path, command.TFSPACE)
return exec.Command(parts[0], parts[1:]...)
}
// If we still don't have a path, then just set it to the original
// given path.
if cmdPath == "" {
cmdPath = path
}
// Build the command to execute the plugin
return exec.Command(cmdPath)
}

View File

@ -12,6 +12,7 @@ import (
"github.com/hashicorp/hil"
"github.com/hashicorp/hil/ast"
"github.com/hashicorp/terraform/helper/hilmapstructure"
"github.com/hashicorp/terraform/plugin/discovery"
"github.com/mitchellh/reflectwalk"
)
@ -64,6 +65,7 @@ type Module struct {
type ProviderConfig struct {
Name string
Alias string
Version string
RawConfig *RawConfig
}
@ -238,6 +240,33 @@ func (r *Resource) Id() string {
}
}
// ProviderFullName returns the full name of the provider for this resource,
// which may either be specified explicitly using the "provider" meta-argument
// or implied by the prefix on the resource type name.
func (r *Resource) ProviderFullName() string {
return ResourceProviderFullName(r.Type, r.Provider)
}
// ResourceProviderFullName returns the full (dependable) name of the
// provider for a hypothetical resource with the given resource type and
// explicit provider string. If the explicit provider string is empty then
// the provider name is inferred from the resource type name.
func ResourceProviderFullName(resourceType, explicitProvider string) string {
if explicitProvider != "" {
return explicitProvider
}
idx := strings.IndexRune(resourceType, '_')
if idx == -1 {
// If no underscores, the resource name is assumed to be
// also the provider name, e.g. if the provider exposes
// only a single resource of each type.
return resourceType
}
return resourceType[:idx]
}
// Validate does some basic semantic checking of the configuration.
func (c *Config) Validate() error {
if c == nil {
@ -349,7 +378,8 @@ func (c *Config) Validate() error {
}
}
// Check that providers aren't declared multiple times.
// Check that providers aren't declared multiple times and that their
// version constraints, where present, are syntactically valid.
providerSet := make(map[string]struct{})
for _, p := range c.ProviderConfigs {
name := p.FullName()
@ -360,6 +390,16 @@ func (c *Config) Validate() error {
continue
}
if p.Version != "" {
_, err := discovery.ConstraintStr(p.Version).Parse()
if err != nil {
errs = append(errs, fmt.Errorf(
"provider.%s: invalid version constraint %q: %s",
name, p.Version, err,
))
}
}
providerSet[name] = struct{}{}
}

View File

@ -207,6 +207,18 @@ func TestConfigValidate_table(t *testing.T) {
false,
"",
},
{
"provider with valid version constraint",
"provider-version",
false,
"",
},
{
"provider with invalid version constraint",
"provider-version-invalid",
true,
"invalid version constraint",
},
}
for i, tc := range cases {
@ -614,6 +626,13 @@ func TestConfigValidate_varModuleInvalid(t *testing.T) {
}
}
func TestConfigValidate_varProviderVersionInvalid(t *testing.T) {
c := testConfig(t, "validate-provider-version-invalid")
if err := c.Validate(); err == nil {
t.Fatal("should not be valid")
}
}
func TestNameRegexp(t *testing.T) {
cases := []struct {
Input string
@ -673,3 +692,74 @@ func TestConfigDataCount(t *testing.T) {
t.Fatal("count key still exists in RawConfig")
}
}
func TestConfigProviderVersion(t *testing.T) {
c := testConfig(t, "provider-version")
if len(c.ProviderConfigs) != 1 {
t.Fatal("expected 1 provider")
}
p := c.ProviderConfigs[0]
if p.Name != "aws" {
t.Fatalf("expected provider name 'aws', got %q", p.Name)
}
if p.Version != "0.0.1" {
t.Fatalf("expected providers version '0.0.1', got %q", p.Version)
}
if _, ok := p.RawConfig.Raw["version"]; ok {
t.Fatal("'version' should not exist in raw config")
}
}
func TestResourceProviderFullName(t *testing.T) {
type testCase struct {
ResourceName string
Alias string
Expected string
}
tests := []testCase{
{
// If no alias is provided, the first underscore-separated segment
// is assumed to be the provider name.
ResourceName: "aws_thing",
Alias: "",
Expected: "aws",
},
{
// If we have more than one underscore then it's the first one that we'll use.
ResourceName: "aws_thingy_thing",
Alias: "",
Expected: "aws",
},
{
// A provider can export a resource whose name is just the bare provider name,
// e.g. because the provider only has one resource and so any additional
// parts would be redundant.
ResourceName: "external",
Alias: "",
Expected: "external",
},
{
// Alias always overrides the default extraction of the name
ResourceName: "aws_thing",
Alias: "tls.baz",
Expected: "tls.baz",
},
}
for _, test := range tests {
got := ResourceProviderFullName(test.ResourceName, test.Alias)
if got != test.Expected {
t.Errorf(
"(%q, %q) produced %q; want %q",
test.ResourceName, test.Alias,
got,
test.Expected,
)
}
}
}

View File

@ -562,6 +562,7 @@ func loadProvidersHcl(list *ast.ObjectList) ([]*ProviderConfig, error) {
}
delete(config, "alias")
delete(config, "version")
rawConfig, err := NewRawConfig(config)
if err != nil {
@ -583,9 +584,22 @@ func loadProvidersHcl(list *ast.ObjectList) ([]*ProviderConfig, error) {
}
}
// If we have a version field then extract it
var version string
if a := listVal.Filter("version"); len(a.Items) > 0 {
err := hcl.DecodeObject(&version, a.Items[0].Val)
if err != nil {
return nil, fmt.Errorf(
"Error reading version for provider[%s]: %s",
n,
err)
}
}
result = append(result, &ProviderConfig{
Name: n,
Alias: alias,
Version: version,
RawConfig: rawConfig,
})
}

View File

@ -92,6 +92,25 @@ func (t *Tree) Children() map[string]*Tree {
return t.children
}
// DeepEach calls the provided callback for the receiver and then all of
// its descendents in the tree, allowing an operation to be performed on
// all modules in the tree.
//
// Parents will be visited before their children but otherwise the order is
// not defined.
func (t *Tree) DeepEach(cb func(*Tree)) {
t.lock.RLock()
defer t.lock.RUnlock()
t.deepEach(cb)
}
func (t *Tree) deepEach(cb func(*Tree)) {
cb(t)
for _, c := range t.children {
c.deepEach(cb)
}
}
// Loaded says whether or not this tree has been loaded or not yet.
func (t *Tree) Loaded() bool {
t.lock.RLock()

103
config/providers.go Normal file
View File

@ -0,0 +1,103 @@
package config
import "github.com/blang/semver"
// ProviderVersionConstraint presents a constraint for a particular
// provider, identified by its full name.
type ProviderVersionConstraint struct {
Constraint string
ProviderType string
}
// ProviderVersionConstraints is a map from provider full name to its associated
// ProviderVersionConstraint, as produced by Config.RequiredProviders.
type ProviderVersionConstraints map[string]ProviderVersionConstraint
// RequiredProviders returns the ProviderVersionConstraints for this
// module.
//
// This includes both providers that are explicitly requested by provider
// blocks and those that are used implicitly by instantiating one of their
// resource types. In the latter case, the returned semver Range will
// accept any version of the provider.
func (c *Config) RequiredProviders() ProviderVersionConstraints {
ret := make(ProviderVersionConstraints, len(c.ProviderConfigs))
configs := c.ProviderConfigsByFullName()
// In order to find the *implied* dependencies (those without explicit
// "provider" blocks) we need to walk over all of the resources and
// cross-reference with the provider configs.
for _, rc := range c.Resources {
providerName := rc.ProviderFullName()
var providerType string
// Default to (effectively) no constraint whatsoever, but we might
// override if there's an explicit constraint in config.
constraint := ">=0.0.0"
config, ok := configs[providerName]
if ok {
if config.Version != "" {
constraint = config.Version
}
providerType = config.Name
} else {
providerType = providerName
}
ret[providerName] = ProviderVersionConstraint{
ProviderType: providerType,
Constraint: constraint,
}
}
return ret
}
// RequiredRanges returns a semver.Range for each distinct provider type in
// the constraint map. If the same provider type appears more than once
// (e.g. because aliases are in use) then their respective constraints are
// combined such that they must *all* apply.
//
// The result of this method can be passed to the
// PluginMetaSet.ConstrainVersions method within the plugin/discovery
// package in order to filter down the available plugins to those which
// satisfy the given constraints.
//
// This function will panic if any of the constraints within cannot be
// parsed as semver ranges. This is guaranteed to never happen for a
// constraint set that was built from a configuration that passed validation.
func (cons ProviderVersionConstraints) RequiredRanges() map[string]semver.Range {
ret := make(map[string]semver.Range, len(cons))
for _, con := range cons {
spec := semver.MustParseRange(con.Constraint)
if existing, exists := ret[con.ProviderType]; exists {
ret[con.ProviderType] = existing.AND(spec)
} else {
ret[con.ProviderType] = spec
}
}
return ret
}
// ProviderConfigsByFullName returns a map from provider full names (as
// returned by ProviderConfig.FullName()) to the corresponding provider
// configs.
//
// This function returns no new information than what's already in
// c.ProviderConfigs, but returns it in a more convenient shape. If there
// is more than one provider config with the same full name then the result
// is undefined, but that is guaranteed not to happen for any config that
// has passed validation.
func (c *Config) ProviderConfigsByFullName() map[string]*ProviderConfig {
ret := make(map[string]*ProviderConfig, len(c.ProviderConfigs))
for _, pc := range c.ProviderConfigs {
ret[pc.FullName()] = pc
}
return ret
}

View File

@ -0,0 +1,5 @@
provider "aws" {
version = "bananas"
a = "a"
b = "b"
}

View File

@ -0,0 +1,6 @@
provider "aws" {
version = "0.0.1"
a = "a"
b = "b"
}

View File

@ -0,0 +1,3 @@
provider "test" {
version = "bananas!"
}

View File

@ -383,11 +383,11 @@ func Test(t TestT, c TestCase) {
c.PreCheck()
}
ctxProviders, err := testProviderFactories(c)
providerResolver, err := testProviderResolver(c)
if err != nil {
t.Fatal(err)
}
opts := terraform.ContextOpts{Providers: ctxProviders}
opts := terraform.ContextOpts{ProviderResolver: providerResolver}
// A single state variable to track the lifecycle, starting with no state
var state *terraform.State
@ -400,15 +400,18 @@ func Test(t TestT, c TestCase) {
var err error
log.Printf("[WARN] Test: Executing step %d", i)
// Determine the test mode to execute
if step.Config != "" {
state, err = testStepConfig(opts, state, step)
} else if step.ImportState {
state, err = testStepImportState(opts, state, step)
} else {
if step.Config == "" && !step.ImportState {
err = fmt.Errorf(
"unknown test mode for step. Please see TestStep docs\n\n%#v",
step)
} else {
if step.ImportState {
// Can optionally set step.Config in addition to
// step.ImportState, to provide config for the import.
state, err = testStepImportState(opts, state, step)
} else {
state, err = testStepConfig(opts, state, step)
}
}
// If there was an error, exit
@ -496,16 +499,17 @@ func Test(t TestT, c TestCase) {
}
}
// testProviderFactories is a helper to build the ResourceProviderFactory map
// testProviderResolver is a helper to build a ResourceProviderResolver
// with pre instantiated ResourceProviders, so that we can reset them for the
// test, while only calling the factory function once.
// Any errors are stored so that they can be returned by the factory in
// terraform to match non-test behavior.
func testProviderFactories(c TestCase) (map[string]terraform.ResourceProviderFactory, error) {
ctxProviders := c.ProviderFactories // make(map[string]terraform.ResourceProviderFactory)
func testProviderResolver(c TestCase) (terraform.ResourceProviderResolver, error) {
ctxProviders := c.ProviderFactories
if ctxProviders == nil {
ctxProviders = make(map[string]terraform.ResourceProviderFactory)
}
// add any fixed providers
for k, p := range c.Providers {
ctxProviders[k] = terraform.ResourceProviderFactoryFixed(p)
@ -527,7 +531,7 @@ func testProviderFactories(c TestCase) (map[string]terraform.ResourceProviderFac
}
}
return ctxProviders, nil
return terraform.ResourceProviderResolverFixed(ctxProviders), nil
}
// UnitTest is a helper to force the acceptance testing harness to run in the

View File

@ -40,6 +40,7 @@ func TestTest_importState(t *testing.T) {
Steps: []TestStep{
TestStep{
Config: testConfigStrProvider,
ResourceName: "test_instance.foo",
ImportState: true,
ImportStateId: "foo",
@ -89,6 +90,7 @@ func TestTest_importStateFail(t *testing.T) {
Steps: []TestStep{
TestStep{
Config: testConfigStrProvider,
ResourceName: "test_instance.foo",
ImportState: true,
ImportStateId: "foo",
@ -163,6 +165,7 @@ func TestTest_importStateDetectId(t *testing.T) {
Config: testConfigStr,
},
TestStep{
Config: testConfigStr,
ResourceName: "test_instance.foo",
ImportState: true,
ImportStateCheck: checkFn,
@ -236,6 +239,7 @@ func TestTest_importStateIdPrefix(t *testing.T) {
Config: testConfigStr,
},
{
Config: testConfigStr,
ResourceName: "test_instance.foo",
ImportState: true,
ImportStateCheck: checkFn,
@ -309,6 +313,7 @@ func TestTest_importStateVerify(t *testing.T) {
Config: testConfigStr,
},
TestStep{
Config: testConfigStr,
ResourceName: "test_instance.foo",
ImportState: true,
ImportStateVerify: true,
@ -371,6 +376,7 @@ func TestTest_importStateVerifyFail(t *testing.T) {
Config: testConfigStr,
},
TestStep{
Config: testConfigStr,
ResourceName: "test_instance.foo",
ImportState: true,
ImportStateVerify: true,

View File

@ -619,10 +619,6 @@ func testProvider() *terraform.MockResourceProvider {
return mp
}
const testConfigStr = `
resource "test_instance" "foo" {}
`
func TestTest_Main(t *testing.T) {
flag.Parse()
if *flagSweep == "" {
@ -777,3 +773,11 @@ func TestTest_Main(t *testing.T) {
func mockSweeperFunc(s string) error {
return nil
}
const testConfigStr = `
resource "test_instance" "foo" {}
`
const testConfigStrProvider = `
provider "test" {}
`

10
main.go
View File

@ -108,10 +108,6 @@ func wrappedMain() int {
// Load the configuration
config := BuiltinConfig
if err := config.Discover(Ui); err != nil {
Ui.Error(fmt.Sprintf("Error discovering plugins: %s", err))
return 1
}
// Load the configuration file if we have one, that can be used to
// define extra providers and provisioners.
@ -185,9 +181,9 @@ func wrappedMain() int {
HelpWriter: os.Stdout,
}
// Initialize the TFConfig settings for the commands...
ContextOpts.Providers = config.ProviderFactories()
ContextOpts.Provisioners = config.ProvisionerFactories()
// Pass in the overriding plugin paths from config
PluginOverrides.Providers = config.Providers
PluginOverrides.Provisioners = config.Provisioners
exitCode, err := cliRunner.Run()
if err != nil {

View File

@ -0,0 +1,43 @@
package moduledeps
import (
"github.com/hashicorp/terraform/plugin/discovery"
)
// Providers describes a set of provider dependencies for a given module.
//
// Each named provider instance can have one version constraint.
type Providers map[ProviderInstance]ProviderDependency
// ProviderDependency describes the dependency for a particular provider
// instance, including both the set of allowed versions and the reason for
// the dependency.
type ProviderDependency struct {
Constraints discovery.Constraints
Reason ProviderDependencyReason
}
// ProviderDependencyReason is an enumeration of reasons why a dependency might be
// present.
type ProviderDependencyReason int
const (
// ProviderDependencyExplicit means that there is an explicit "provider"
// block in the configuration for this module.
ProviderDependencyExplicit ProviderDependencyReason = iota
// ProviderDependencyImplicit means that there is no explicit "provider"
// block but there is at least one resource that uses this provider.
ProviderDependencyImplicit
// ProviderDependencyInherited is a special case of
// ProviderDependencyImplicit where a parent module has defined a
// configuration for the provider that has been inherited by at least one
// resource in this module.
ProviderDependencyInherited
// ProviderDependencyFromState means that this provider is not currently
// referenced by configuration at all, but some existing instances in
// the state still depend on it.
ProviderDependencyFromState
)

7
moduledeps/doc.go Normal file
View File

@ -0,0 +1,7 @@
// Package moduledeps contains types that can be used to describe the
// providers required for all of the modules in a module tree.
//
// It does not itself contain the functionality for populating such
// data structures; that's in Terraform core, since this package intentionally
// does not depend on terraform core to avoid package dependency cycles.
package moduledeps

204
moduledeps/module.go Normal file
View File

@ -0,0 +1,204 @@
package moduledeps
import (
"sort"
"strings"
"github.com/hashicorp/terraform/plugin/discovery"
)
// Module represents the dependencies of a single module, as well being
// a node in a tree of such structures representing the dependencies of
// an entire configuration.
type Module struct {
Name string
Providers Providers
Children []*Module
}
// WalkFunc is a callback type for use with Module.WalkTree
type WalkFunc func(path []string, parent *Module, current *Module) error
// WalkTree calls the given callback once for the receiver and then
// once for each descendent, in an order such that parents are called
// before their children and siblings are called in the order they
// appear in the Children slice.
//
// When calling the callback, parent will be nil for the first call
// for the receiving module, and then set to the direct parent of
// each module for the subsequent calls.
//
// The path given to the callback is valid only until the callback
// returns, after which it will be mutated and reused. Callbacks must
// therefore copy the path slice if they wish to retain it.
//
// If the given callback returns an error, the walk will be aborted at
// that point and that error returned to the caller.
//
// This function is not thread-safe for concurrent modifications of the
// data structure, so it's the caller's responsibility to arrange for that
// should it be needed.
//
// It is safe for a callback to modify the descendents of the "current"
// module, including the ordering of the Children slice itself, but the
// callback MUST NOT modify the parent module.
func (m *Module) WalkTree(cb WalkFunc) error {
return walkModuleTree(make([]string, 0, 1), nil, m, cb)
}
func walkModuleTree(path []string, parent *Module, current *Module, cb WalkFunc) error {
path = append(path, current.Name)
err := cb(path, parent, current)
if err != nil {
return err
}
for _, child := range current.Children {
err := walkModuleTree(path, current, child, cb)
if err != nil {
return err
}
}
return nil
}
// SortChildren sorts the Children slice into lexicographic order by
// name, in-place.
//
// This is primarily useful prior to calling WalkTree so that the walk
// will proceed in a consistent order.
func (m *Module) SortChildren() {
sort.Sort(sortModules{m.Children})
}
// SortDescendents is a convenience wrapper for calling SortChildren on
// the receiver and all of its descendent modules.
func (m *Module) SortDescendents() {
m.WalkTree(func(path []string, parent *Module, current *Module) error {
current.SortChildren()
return nil
})
}
type sortModules struct {
modules []*Module
}
func (s sortModules) Len() int {
return len(s.modules)
}
func (s sortModules) Less(i, j int) bool {
cmp := strings.Compare(s.modules[i].Name, s.modules[j].Name)
return cmp < 0
}
func (s sortModules) Swap(i, j int) {
s.modules[i], s.modules[j] = s.modules[j], s.modules[i]
}
// PluginRequirements produces a PluginRequirements structure that can
// be used with discovery.PluginMetaSet.ConstrainVersions to identify
// suitable plugins to satisfy the module's provider dependencies.
//
// This method only considers the direct requirements of the receiver.
// Use AllPluginRequirements to flatten the dependencies for the
// entire tree of modules.
//
// Requirements returned by this method include only version constraints,
// and apply no particular SHA256 hash constraint.
func (m *Module) PluginRequirements() discovery.PluginRequirements {
ret := make(discovery.PluginRequirements)
for inst, dep := range m.Providers {
// m.Providers is keyed on provider names, such as "aws.foo".
// a PluginRequirements wants keys to be provider *types*, such
// as "aws". If there are multiple aliases for the same
// provider then we will flatten them into a single requirement
// by combining their constraint sets.
pty := inst.Type()
if existing, exists := ret[pty]; exists {
ret[pty].Versions = existing.Versions.Append(dep.Constraints)
} else {
ret[pty] = &discovery.PluginConstraints{
Versions: dep.Constraints,
}
}
}
return ret
}
// AllPluginRequirements calls PluginRequirements for the receiver and all
// of its descendents, and merges the result into a single PluginRequirements
// structure that would satisfy all of the modules together.
//
// Requirements returned by this method include only version constraints,
// and apply no particular SHA256 hash constraint.
func (m *Module) AllPluginRequirements() discovery.PluginRequirements {
var ret discovery.PluginRequirements
m.WalkTree(func(path []string, parent *Module, current *Module) error {
ret = ret.Merge(current.PluginRequirements())
return nil
})
return ret
}
// Equal returns true if the receiver is the root of an identical tree
// to the other given Module. This is a deep comparison that considers
// the equality of all downstream modules too.
//
// The children are considered to be ordered, so callers may wish to use
// SortDescendents first to normalize the order of the slices of child nodes.
//
// The implementation of this function is not optimized since it is provided
// primarily for use in tests.
func (m *Module) Equal(other *Module) bool {
// take care of nils first
if m == nil && other == nil {
return true
} else if (m == nil && other != nil) || (m != nil && other == nil) {
return false
}
if m.Name != other.Name {
return false
}
if len(m.Providers) != len(other.Providers) {
return false
}
if len(m.Children) != len(other.Children) {
return false
}
// Can't use reflect.DeepEqual on this provider structure because
// the nested Constraints objects contain function pointers that
// never compare as equal. So we'll need to walk it the long way.
for inst, dep := range m.Providers {
if _, exists := other.Providers[inst]; !exists {
return false
}
if dep.Reason != other.Providers[inst].Reason {
return false
}
// Constraints are not too easy to compare robustly, so
// we'll just use their string representations as a proxy
// for now.
if dep.Constraints.String() != other.Providers[inst].Constraints.String() {
return false
}
}
// Above we already checked that we have the same number of children
// in each module, so now we just need to check that they are
// recursively equal.
for i := range m.Children {
if !m.Children[i].Equal(other.Children[i]) {
return false
}
}
// If we fall out here then they are equal
return true
}

216
moduledeps/module_test.go Normal file
View File

@ -0,0 +1,216 @@
package moduledeps
import (
"fmt"
"reflect"
"testing"
"github.com/hashicorp/terraform/plugin/discovery"
)
func TestModuleWalkTree(t *testing.T) {
type walkStep struct {
Path []string
ParentName string
}
tests := []struct {
Root *Module
WalkOrder []walkStep
}{
{
&Module{
Name: "root",
Children: nil,
},
[]walkStep{
{
Path: []string{"root"},
ParentName: "",
},
},
},
{
&Module{
Name: "root",
Children: []*Module{
{
Name: "child",
},
},
},
[]walkStep{
{
Path: []string{"root"},
ParentName: "",
},
{
Path: []string{"root", "child"},
ParentName: "root",
},
},
},
{
&Module{
Name: "root",
Children: []*Module{
{
Name: "child",
Children: []*Module{
{
Name: "grandchild",
},
},
},
},
},
[]walkStep{
{
Path: []string{"root"},
ParentName: "",
},
{
Path: []string{"root", "child"},
ParentName: "root",
},
{
Path: []string{"root", "child", "grandchild"},
ParentName: "child",
},
},
},
{
&Module{
Name: "root",
Children: []*Module{
{
Name: "child1",
Children: []*Module{
{
Name: "grandchild1",
},
},
},
{
Name: "child2",
Children: []*Module{
{
Name: "grandchild2",
},
},
},
},
},
[]walkStep{
{
Path: []string{"root"},
ParentName: "",
},
{
Path: []string{"root", "child1"},
ParentName: "root",
},
{
Path: []string{"root", "child1", "grandchild1"},
ParentName: "child1",
},
{
Path: []string{"root", "child2"},
ParentName: "root",
},
{
Path: []string{"root", "child2", "grandchild2"},
ParentName: "child2",
},
},
},
}
for i, test := range tests {
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
wo := test.WalkOrder
test.Root.WalkTree(func(path []string, parent *Module, current *Module) error {
if len(wo) == 0 {
t.Fatalf("ran out of walk steps while expecting one for %#v", path)
}
step := wo[0]
wo = wo[1:]
if got, want := path, step.Path; !reflect.DeepEqual(got, want) {
t.Errorf("wrong path %#v; want %#v", got, want)
}
parentName := ""
if parent != nil {
parentName = parent.Name
}
if got, want := parentName, step.ParentName; got != want {
t.Errorf("wrong parent name %q; want %q", got, want)
}
if got, want := current.Name, path[len(path)-1]; got != want {
t.Errorf("mismatching current.Name %q and final path element %q", got, want)
}
return nil
})
})
}
}
func TestModuleSortChildren(t *testing.T) {
m := &Module{
Name: "root",
Children: []*Module{
{
Name: "apple",
},
{
Name: "zebra",
},
{
Name: "xylophone",
},
{
Name: "pig",
},
},
}
m.SortChildren()
want := []string{"apple", "pig", "xylophone", "zebra"}
var got []string
for _, c := range m.Children {
got = append(got, c.Name)
}
if !reflect.DeepEqual(want, got) {
t.Errorf("wrong order %#v; want %#v", want, got)
}
}
func TestModulePluginRequirements(t *testing.T) {
m := &Module{
Name: "root",
Providers: Providers{
"foo": ProviderDependency{
Constraints: discovery.ConstraintStr(">=1.0.0").MustParse(),
},
"foo.bar": ProviderDependency{
Constraints: discovery.ConstraintStr(">=2.0.0").MustParse(),
},
"baz": ProviderDependency{
Constraints: discovery.ConstraintStr(">=3.0.0").MustParse(),
},
},
}
reqd := m.PluginRequirements()
if len(reqd) != 2 {
t.Errorf("wrong number of elements in %#v; want 2", reqd)
}
if got, want := reqd["foo"].Versions.String(), ">=1.0.0,>=2.0.0"; got != want {
t.Errorf("wrong combination of versions for 'foo' %q; want %q", got, want)
}
if got, want := reqd["baz"].Versions.String(), ">=3.0.0"; got != want {
t.Errorf("wrong combination of versions for 'baz' %q; want %q", got, want)
}
}

30
moduledeps/provider.go Normal file
View File

@ -0,0 +1,30 @@
package moduledeps
import (
"strings"
)
// ProviderInstance describes a particular provider instance by its full name,
// like "null" or "aws.foo".
type ProviderInstance string
// Type returns the provider type of this instance. For example, for an instance
// named "aws.foo" the type is "aws".
func (p ProviderInstance) Type() string {
t := string(p)
if dotPos := strings.Index(t, "."); dotPos != -1 {
t = t[:dotPos]
}
return t
}
// Alias returns the alias of this provider, if any. An instance named "aws.foo"
// has the alias "foo", while an instance named just "docker" has no alias,
// so the empty string would be returned.
func (p ProviderInstance) Alias() string {
t := string(p)
if dotPos := strings.Index(t, "."); dotPos != -1 {
return t[dotPos+1:]
}
return ""
}

View File

@ -0,0 +1,36 @@
package moduledeps
import (
"testing"
)
func TestProviderInstance(t *testing.T) {
tests := []struct {
Name string
WantType string
WantAlias string
}{
{
Name: "aws",
WantType: "aws",
WantAlias: "",
},
{
Name: "aws.foo",
WantType: "aws",
WantAlias: "foo",
},
}
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
inst := ProviderInstance(test.Name)
if got, want := inst.Type(), test.WantType; got != want {
t.Errorf("got type %q; want %q", got, want)
}
if got, want := inst.Alias(), test.WantAlias; got != want {
t.Errorf("got alias %q; want %q", got, want)
}
})
}
}

24
plugin/client.go Normal file
View File

@ -0,0 +1,24 @@
package plugin
import (
"os/exec"
plugin "github.com/hashicorp/go-plugin"
"github.com/hashicorp/terraform/plugin/discovery"
)
// ClientConfig returns a configuration object that can be used to instantiate
// a client for the plugin described by the given metadata.
func ClientConfig(m discovery.PluginMeta) *plugin.ClientConfig {
return &plugin.ClientConfig{
Cmd: exec.Command(m.Path),
HandshakeConfig: Handshake,
Managed: true,
Plugins: PluginMap,
}
}
// Client returns a plugin client for the plugin described by the given metadata.
func Client(m discovery.PluginMeta) *plugin.Client {
return plugin.NewClient(ClientConfig(m))
}

183
plugin/discovery/find.go Normal file
View File

@ -0,0 +1,183 @@
package discovery
import (
"io/ioutil"
"log"
"path/filepath"
"runtime"
"strings"
)
const machineName = runtime.GOOS + "_" + runtime.GOARCH
// FindPlugins looks in the given directories for files whose filenames
// suggest that they are plugins of the given kind (e.g. "provider") and
// returns a PluginMetaSet representing the discovered potential-plugins.
//
// Currently this supports two different naming schemes. The current
// standard naming scheme is a subdirectory called $GOOS-$GOARCH containing
// files named terraform-$KIND-$NAME-V$VERSION. The legacy naming scheme is
// files directly in the given directory whose names are like
// terraform-$KIND-$NAME.
//
// Only one plugin will be returned for each unique plugin (name, version)
// pair, with preference given to files found in earlier directories.
//
// This is a convenience wrapper around FindPluginPaths and ResolvePluginsPaths.
func FindPlugins(kind string, dirs []string) PluginMetaSet {
return ResolvePluginPaths(FindPluginPaths(kind, dirs))
}
// FindPluginPaths looks in the given directories for files whose filenames
// suggest that they are plugins of the given kind (e.g. "provider").
//
// The return value is a list of absolute paths that appear to refer to
// plugins in the given directories, based only on what can be inferred
// from the naming scheme. The paths returned are ordered such that files
// in later dirs appear after files in earlier dirs in the given directory
// list. Within the same directory plugins are returned in a consistent but
// undefined order.
func FindPluginPaths(kind string, dirs []string) []string {
// This is just a thin wrapper around findPluginPaths so that we can
// use the latter in tests with a fake machineName so we can use our
// test fixtures.
return findPluginPaths(kind, machineName, dirs)
}
func findPluginPaths(kind string, machineName string, dirs []string) []string {
prefix := "terraform-" + kind + "-"
ret := make([]string, 0, len(dirs))
for _, baseDir := range dirs {
baseItems, err := ioutil.ReadDir(baseDir)
if err != nil {
// Ignore missing dirs, non-dirs, etc
continue
}
log.Printf("[DEBUG] checking for plugins in %q", baseDir)
for _, item := range baseItems {
fullName := item.Name()
if fullName == machineName && item.Mode().IsDir() {
// Current-style $GOOS-$GOARCH directory prefix
machineDir := filepath.Join(baseDir, machineName)
machineItems, err := ioutil.ReadDir(machineDir)
if err != nil {
continue
}
log.Printf("[DEBUG] checking for plugins in %q", machineDir)
for _, item := range machineItems {
fullName := item.Name()
if !strings.HasPrefix(fullName, prefix) {
continue
}
// New-style paths must have a version segment in filename
if !strings.Contains(strings.ToLower(fullName), "_v") {
continue
}
absPath, err := filepath.Abs(filepath.Join(machineDir, fullName))
if err != nil {
continue
}
log.Printf("[DEBUG] found plugin %q", fullName)
ret = append(ret, filepath.Clean(absPath))
}
continue
}
if strings.HasPrefix(fullName, prefix) {
// Legacy style with files directly in the base directory
absPath, err := filepath.Abs(filepath.Join(baseDir, fullName))
if err != nil {
continue
}
log.Printf("[DEBUG] found legacy plugin %q", fullName)
ret = append(ret, filepath.Clean(absPath))
}
}
}
return ret
}
// ResolvePluginPaths takes a list of paths to plugin executables (as returned
// by e.g. FindPluginPaths) and produces a PluginMetaSet describing the
// referenced plugins.
//
// If the same combination of plugin name and version appears multiple times,
// the earlier reference will be preferred. Several different versions of
// the same plugin name may be returned, in which case the methods of
// PluginMetaSet can be used to filter down.
func ResolvePluginPaths(paths []string) PluginMetaSet {
s := make(PluginMetaSet)
type nameVersion struct {
Name string
Version string
}
found := make(map[nameVersion]struct{})
for _, path := range paths {
baseName := strings.ToLower(filepath.Base(path))
if !strings.HasPrefix(baseName, "terraform-") {
// Should never happen with reasonable input
continue
}
baseName = baseName[10:]
firstDash := strings.Index(baseName, "-")
if firstDash == -1 {
// Should never happen with reasonable input
continue
}
baseName = baseName[firstDash+1:]
if baseName == "" {
// Should never happen with reasonable input
continue
}
parts := strings.SplitN(baseName, "_v", 2)
name := parts[0]
version := "0.0.0"
if len(parts) == 2 {
version = parts[1]
}
// Auto-installed plugins contain an extra name portion representing
// the expected plugin version, which we must trim off.
if underX := strings.Index(version, "_x"); underX != -1 {
version = version[:underX]
}
if _, ok := found[nameVersion{name, version}]; ok {
// Skip duplicate versions of the same plugin
// (We do this during this step because after this we will be
// dealing with sets and thus lose our ordering with which to
// decide preference.)
continue
}
s.Add(PluginMeta{
Name: name,
Version: VersionStr(version),
Path: path,
})
found[nameVersion{name, version}] = struct{}{}
}
return s
}

View File

@ -0,0 +1,120 @@
package discovery
import (
"os"
"path/filepath"
"reflect"
"testing"
)
func TestFindPluginPaths(t *testing.T) {
got := findPluginPaths(
"foo",
"mockos_mockarch",
[]string{
"test-fixtures/current-style-plugins",
"test-fixtures/legacy-style-plugins",
"test-fixtures/non-existent",
"test-fixtures/not-a-dir",
},
)
want := []string{
filepath.Join("test-fixtures", "current-style-plugins", "mockos_mockarch", "terraform-foo-bar_v0.0.1"),
filepath.Join("test-fixtures", "current-style-plugins", "mockos_mockarch", "terraform-foo-bar_v1.0.0"),
filepath.Join("test-fixtures", "legacy-style-plugins", "terraform-foo-bar"),
filepath.Join("test-fixtures", "legacy-style-plugins", "terraform-foo-baz"),
}
// Turn the paths back into relative paths, since we don't care exactly
// where this code is present on the system for the sake of this test.
baseDir, err := os.Getwd()
if err != nil {
// Should never happen
panic(err)
}
for i, absPath := range got {
if !filepath.IsAbs(absPath) {
t.Errorf("got non-absolute path %s", absPath)
}
got[i], err = filepath.Rel(baseDir, absPath)
if err != nil {
t.Fatalf("Can't make %s relative to current directory %s", absPath, baseDir)
}
}
if !reflect.DeepEqual(got, want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want)
}
}
func TestResolvePluginPaths(t *testing.T) {
got := ResolvePluginPaths([]string{
"/example/mockos_mockarch/terraform-foo-bar_v0.0.1",
"/example/mockos_mockarch/terraform-foo-baz_v0.0.1",
"/example/mockos_mockarch/terraform-foo-baz_v1.0.0",
"/example/mockos_mockarch/terraform-foo-baz_v2.0.0_x4",
"/example/mockos_mockarch/terraform-foo-upper_V2.0.0_X4",
"/example/terraform-foo-bar",
"/example/mockos_mockarch/terraform-foo-bar_vbananas",
"/example/mockos_mockarch/terraform-foo-bar_v",
"/example2/mockos_mockarch/terraform-foo-bar_v0.0.1",
})
want := []PluginMeta{
{
Name: "bar",
Version: "0.0.1",
Path: "/example/mockos_mockarch/terraform-foo-bar_v0.0.1",
},
{
Name: "baz",
Version: "0.0.1",
Path: "/example/mockos_mockarch/terraform-foo-baz_v0.0.1",
},
{
Name: "baz",
Version: "1.0.0",
Path: "/example/mockos_mockarch/terraform-foo-baz_v1.0.0",
},
{
Name: "baz",
Version: "2.0.0",
Path: "/example/mockos_mockarch/terraform-foo-baz_v2.0.0_x4",
},
{
Name: "upper",
Version: "2.0.0",
Path: "/example/mockos_mockarch/terraform-foo-upper_V2.0.0_X4",
},
{
Name: "bar",
Version: "0.0.0",
Path: "/example/terraform-foo-bar",
},
{
Name: "bar",
Version: "bananas",
Path: "/example/mockos_mockarch/terraform-foo-bar_vbananas",
},
{
Name: "bar",
Version: "",
Path: "/example/mockos_mockarch/terraform-foo-bar_v",
},
}
for p := range got {
t.Logf("got %#v", p)
}
if got, want := got.Count(), len(want); got != want {
t.Errorf("got %d items; want %d", got, want)
}
for _, wantMeta := range want {
if !got.Has(wantMeta) {
t.Errorf("missing meta %#v", wantMeta)
}
}
}

203
plugin/discovery/get.go Normal file
View File

@ -0,0 +1,203 @@
package discovery
import (
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"runtime"
"strconv"
"strings"
"golang.org/x/net/html"
cleanhttp "github.com/hashicorp/go-cleanhttp"
getter "github.com/hashicorp/go-getter"
)
// Releases are located by parsing the html listing from releases.hashicorp.com.
//
// The URL for releases follows the pattern:
// https://releases.hashicorp.com/terraform-provider-name/<x.y.z>/terraform-provider-name_<x.y.z>_<os>_<arch>.<ext>
//
// The plugin protocol version will be saved with the release and returned in
// the header X-TERRAFORM_PROTOCOL_VERSION.
const protocolVersionHeader = "x-terraform-protocol-version"
var releaseHost = "https://releases.hashicorp.com"
var httpClient = cleanhttp.DefaultClient()
// Plugins are referred to by the short name, but all URLs and files will use
// the full name prefixed with terraform-<plugin_type>-
func providerName(name string) string {
return "terraform-provider-" + name
}
// providerVersionsURL returns the path to the released versions directory for the provider:
// https://releases.hashicorp.com/terraform-provider-name/
func providerVersionsURL(name string) string {
return releaseHost + "/" + providerName(name) + "/"
}
// providerURL returns the full path to the provider file, using the current OS
// and ARCH:
// .../terraform-provider-name_<x.y.z>/terraform-provider-name_<x.y.z>_<os>_<arch>.<ext>
func providerURL(name, version string) string {
fileName := fmt.Sprintf("%s_%s_%s_%s.zip", providerName(name), version, runtime.GOOS, runtime.GOARCH)
u := fmt.Sprintf("%s%s/%s", providerVersionsURL(name), version, fileName)
return u
}
// GetProvider fetches a provider plugin based on the version constraints, and
// copies it to the dst directory.
//
// TODO: verify checksum and signature
func GetProvider(dst, provider string, req Constraints, pluginProtocolVersion uint) error {
versions, err := listProviderVersions(provider)
// TODO: return multiple errors
if err != nil {
return err
}
if len(versions) == 0 {
return fmt.Errorf("no plugins found for provider %q", provider)
}
versions = allowedVersions(versions, req)
if len(versions) == 0 {
return fmt.Errorf("no version of %q available that fulfills constraints %s", provider, req)
}
// sort them newest to oldest
Versions(versions).Sort()
// take the first matching plugin we find
for _, v := range versions {
url := providerURL(provider, v.String())
log.Printf("[DEBUG] fetching provider info for %s version %s", provider, v)
if checkPlugin(url, pluginProtocolVersion) {
log.Printf("[DEBUG] getting provider %q version %q at %s", provider, v, url)
return getter.Get(dst, url)
}
log.Printf("[INFO] incompatible ProtocolVersion for %s version %s", provider, v)
}
return fmt.Errorf("no versions of %q compatible with the plugin ProtocolVersion", provider)
}
// Return the plugin version by making a HEAD request to the provided url
func checkPlugin(url string, pluginProtocolVersion uint) bool {
resp, err := httpClient.Head(url)
if err != nil {
log.Printf("[ERROR] error fetching plugin headers: %s", err)
return false
}
if resp.StatusCode != http.StatusOK {
log.Println("[ERROR] non-200 status fetching plugin headers:", resp.Status)
return false
}
proto := resp.Header.Get(protocolVersionHeader)
if proto == "" {
log.Printf("[WARNING] missing %s from: %s", protocolVersionHeader, url)
return false
}
protoVersion, err := strconv.Atoi(proto)
if err != nil {
log.Printf("[ERROR] invalid ProtocolVersion: %s", proto)
return false
}
return protoVersion == int(pluginProtocolVersion)
}
var errVersionNotFound = errors.New("version not found")
// take the list of available versions for a plugin, and filter out those that
// don't fit the constraints.
func allowedVersions(available []Version, required Constraints) []Version {
var allowed []Version
for _, v := range available {
if required.Allows(v) {
allowed = append(allowed, v)
}
}
return allowed
}
// list the version available for the named plugin
func listProviderVersions(name string) ([]Version, error) {
versions, err := listPluginVersions(providerVersionsURL(name))
if err != nil {
return nil, fmt.Errorf("failed to fetch versions for provider %q: %s", name, err)
}
return versions, nil
}
// return a list of the plugin versions at the given URL
func listPluginVersions(url string) ([]Version, error) {
resp, err := httpClient.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := ioutil.ReadAll(resp.Body)
log.Printf("[ERROR] failed to fetch plugin versions from %s\n%s\n%s", url, resp.Status, body)
return nil, errors.New(resp.Status)
}
body, err := html.Parse(resp.Body)
if err != nil {
log.Fatal(err)
}
names := []string{}
// all we need to do is list links on the directory listing page that look like plugins
var f func(*html.Node)
f = func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "a" {
c := n.FirstChild
if c != nil && c.Type == html.TextNode && strings.HasPrefix(c.Data, "terraform-") {
names = append(names, c.Data)
return
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
f(c)
}
}
f(body)
return versionsFromNames(names), nil
}
// parse the list of directory names into a sorted list of available versions
func versionsFromNames(names []string) []Version {
var versions []Version
for _, name := range names {
parts := strings.SplitN(name, "_", 2)
if len(parts) == 2 && parts[1] != "" {
v, err := VersionStr(parts[1]).Parse()
if err != nil {
// filter invalid versions scraped from the page
log.Printf("[WARN] invalid version found for %q: %s", name, err)
continue
}
versions = append(versions, v)
}
}
return versions
}

View File

@ -0,0 +1,166 @@
package discovery
import (
"archive/zip"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"testing"
)
const testProviderFile = "test provider binary"
// return the directory listing for the "test" provider
func testListingHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(versionList))
}
// returns a 200 for a valid provider url, using the patch number for the
// plugin protocol version.
func testHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/terraform-provider-test/" {
testListingHandler(w, r)
return
}
parts := strings.Split(r.URL.Path, "/")
if len(parts) != 4 {
http.Error(w, "not found", http.StatusNotFound)
return
}
filename := parts[3]
reg := regexp.MustCompile(`(terraform-provider-test_(\d).(\d).(\d)_([^_]+)_([^._]+)).zip`)
fileParts := reg.FindStringSubmatch(filename)
if len(fileParts) != 7 {
http.Error(w, "invalid provider: "+filename, http.StatusNotFound)
return
}
w.Header().Set(protocolVersionHeader, fileParts[4])
// write a dummy file
z := zip.NewWriter(w)
f, err := z.Create(fileParts[1] + "_X" + fileParts[4])
if err != nil {
panic(err)
}
io.WriteString(f, testProviderFile)
z.Close()
}
func testReleaseServer() *httptest.Server {
handler := http.NewServeMux()
handler.HandleFunc("/terraform-provider-test/", testHandler)
return httptest.NewServer(handler)
}
func TestMain(m *testing.M) {
server := testReleaseServer()
releaseHost = server.URL
os.Exit(m.Run())
}
func TestVersionListing(t *testing.T) {
versions, err := listProviderVersions("test")
if err != nil {
t.Fatal(err)
}
Versions(versions).Sort()
expected := []string{
"1.2.4",
"1.2.3",
"1.2.1",
}
if len(versions) != len(expected) {
t.Fatalf("Received wrong number of versions. expected: %q, got: %q", expected, versions)
}
for i, v := range versions {
if v.String() != expected[i] {
t.Fatalf("incorrect version: %q, expected %q", v, expected[i])
}
}
}
func TestCheckProtocolVersions(t *testing.T) {
if checkPlugin(providerURL("test", VersionStr("1.2.3").MustParse().String()), 4) {
t.Fatal("protocol version 4 is not compatible")
}
if !checkPlugin(providerURL("test", VersionStr("1.2.3").MustParse().String()), 3) {
t.Fatal("protocol version 3 should be compatible")
}
}
func TestGetProvider(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "tf-plugin")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
// attempt to use an incompatible protocol version
err = GetProvider(tmpDir, "test", AllVersions, 5)
if err == nil {
t.Fatal("protocol version is incompatible")
}
err = GetProvider(tmpDir, "test", AllVersions, 3)
if err != nil {
t.Fatal(err)
}
// we should have version 1.2.3
fileName := fmt.Sprintf("terraform-provider-test_1.2.3_%s_%s_X3", runtime.GOOS, runtime.GOARCH)
dest := filepath.Join(tmpDir, fileName)
f, err := ioutil.ReadFile(dest)
if err != nil {
t.Fatal(err)
}
// provider should have been unzipped
if string(f) != testProviderFile {
t.Fatalf("test provider contains: %q", f)
}
}
const versionList = `<!DOCTYPE html>
<html>
<body>
<ul>
<li>
<a href="../">../</a>
</li>
<li>
<a href="/terraform-provider-test/1.2.3/">terraform-provider-test_1.2.3</a>
</li>
<li>
<a href="/terraform-provider-test/1.2.1/">terraform-provider-test_1.2.1</a>
</li>
<li>
<a href="/terraform-provider-test/1.2.4/">terraform-provider-test_1.2.4</a>
</li>
</ul>
<footer>
Proudly fronted by <a href="https://fastly.com/?utm_source=hashicorp" target="_TOP">Fastly</a>
</footer>
</body>
</html>
`

41
plugin/discovery/meta.go Normal file
View File

@ -0,0 +1,41 @@
package discovery
import (
"crypto/sha256"
"io"
"os"
)
// PluginMeta is metadata about a plugin, useful for launching the plugin
// and for understanding which plugins are available.
type PluginMeta struct {
// Name is the name of the plugin, e.g. as inferred from the plugin
// binary's filename, or by explicit configuration.
Name string
// Version is the semver version of the plugin, expressed as a string
// that might not be semver-valid.
Version VersionStr
// Path is the absolute path of the executable that can be launched
// to provide the RPC server for this plugin.
Path string
}
// SHA256 returns a SHA256 hash of the content of the referenced executable
// file, or an error if the file's contents cannot be read.
func (m PluginMeta) SHA256() ([]byte, error) {
f, err := os.Open(m.Path)
if err != nil {
return nil, err
}
defer f.Close()
h := sha256.New()
_, err = io.Copy(h, f)
if err != nil {
return nil, err
}
return h.Sum(nil), nil
}

View File

@ -0,0 +1,177 @@
package discovery
// A PluginMetaSet is a set of PluginMeta objects meeting a certain criteria.
//
// Methods on this type allow filtering of the set to produce subsets that
// meet more restrictive criteria.
type PluginMetaSet map[PluginMeta]struct{}
// Add inserts the given PluginMeta into the receiving set. This is a no-op
// if the given meta is already present.
func (s PluginMetaSet) Add(p PluginMeta) {
s[p] = struct{}{}
}
// Remove removes the given PluginMeta from the receiving set. This is a no-op
// if the given meta is not already present.
func (s PluginMetaSet) Remove(p PluginMeta) {
delete(s, p)
}
// Has returns true if the given meta is in the receiving set, or false
// otherwise.
func (s PluginMetaSet) Has(p PluginMeta) bool {
_, ok := s[p]
return ok
}
// Count returns the number of metas in the set
func (s PluginMetaSet) Count() int {
return len(s)
}
// ValidateVersions returns two new PluginMetaSets, separating those with
// versions that have syntax-valid semver versions from those that don't.
//
// Eliminating invalid versions from consideration (and possibly warning about
// them) is usually the first step of working with a meta set after discovery
// has completed.
func (s PluginMetaSet) ValidateVersions() (valid, invalid PluginMetaSet) {
valid = make(PluginMetaSet)
invalid = make(PluginMetaSet)
for p := range s {
if _, err := p.Version.Parse(); err == nil {
valid.Add(p)
} else {
invalid.Add(p)
}
}
return
}
// WithName returns the subset of metas that have the given name.
func (s PluginMetaSet) WithName(name string) PluginMetaSet {
ns := make(PluginMetaSet)
for p := range s {
if p.Name == name {
ns.Add(p)
}
}
return ns
}
// ByName groups the metas in the set by their Names, returning a map.
func (s PluginMetaSet) ByName() map[string]PluginMetaSet {
ret := make(map[string]PluginMetaSet)
for p := range s {
if _, ok := ret[p.Name]; !ok {
ret[p.Name] = make(PluginMetaSet)
}
ret[p.Name].Add(p)
}
return ret
}
// Newest returns the one item from the set that has the newest Version value.
//
// The result is meaningful only if the set is already filtered such that
// all of the metas have the same Name.
//
// If there isn't at least one meta in the set then this function will panic.
// Use Count() to ensure that there is at least one value before calling.
//
// If any of the metas have invalid version strings then this function will
// panic. Use ValidateVersions() first to filter out metas with invalid
// versions.
//
// If two metas have the same Version then one is arbitrarily chosen. This
// situation should be avoided by pre-filtering the set.
func (s PluginMetaSet) Newest() PluginMeta {
if len(s) == 0 {
panic("can't call NewestStable on empty PluginMetaSet")
}
var first = true
var winner PluginMeta
var winnerVersion Version
for p := range s {
version, err := p.Version.Parse()
if err != nil {
panic(err)
}
if first == true || version.NewerThan(winnerVersion) {
winner = p
winnerVersion = version
first = false
}
}
return winner
}
// ConstrainVersions takes a set of requirements and attempts to
// return a map from name to a set of metas that have the matching
// name and an appropriate version.
//
// If any of the given requirements match *no* plugins then its PluginMetaSet
// in the returned map will be empty.
//
// All viable metas are returned, so the caller can apply any desired filtering
// to reduce down to a single option. For example, calling Newest() to obtain
// the highest available version.
//
// If any of the metas in the set have invalid version strings then this
// function will panic. Use ValidateVersions() first to filter out metas with
// invalid versions.
func (s PluginMetaSet) ConstrainVersions(reqd PluginRequirements) map[string]PluginMetaSet {
ret := make(map[string]PluginMetaSet)
for p := range s {
name := p.Name
allowedVersions, ok := reqd[name]
if !ok {
continue
}
if _, ok := ret[p.Name]; !ok {
ret[p.Name] = make(PluginMetaSet)
}
version, err := p.Version.Parse()
if err != nil {
panic(err)
}
if allowedVersions.Allows(version) {
ret[p.Name].Add(p)
}
}
return ret
}
// OverridePaths returns a new set where any existing plugins with the given
// names are removed and replaced with the single path given in the map.
//
// This is here only to continue to support the legacy way of overriding
// plugin binaries in the .terraformrc file. It treats all given plugins
// as pre-versioning (version 0.0.0). This mechanism will eventually be
// phased out, with vendor directories being the intended replacement.
func (s PluginMetaSet) OverridePaths(paths map[string]string) PluginMetaSet {
ret := make(PluginMetaSet)
for p := range s {
if _, ok := paths[p.Name]; ok {
// Skip plugins that we're overridding
continue
}
ret.Add(p)
}
// Now add the metadata for overriding plugins
for name, path := range paths {
ret.Add(PluginMeta{
Name: name,
Version: "0.0.0",
Path: path,
})
}
return ret
}

View File

@ -0,0 +1,417 @@
package discovery
import (
"fmt"
"strings"
"testing"
)
func TestPluginMetaSetManipulation(t *testing.T) {
metas := []PluginMeta{
{
Name: "foo",
Version: "1.0.0",
Path: "test-foo",
},
{
Name: "bar",
Version: "2.0.0",
Path: "test-bar",
},
{
Name: "baz",
Version: "2.0.0",
Path: "test-bar",
},
}
s := make(PluginMetaSet)
if count := s.Count(); count != 0 {
t.Fatalf("set has Count %d before any items added", count)
}
// Can we add metas?
for _, p := range metas {
s.Add(p)
if !s.Has(p) {
t.Fatalf("%q not in set after adding it", p.Name)
}
}
if got, want := s.Count(), len(metas); got != want {
t.Fatalf("set has Count %d after all items added; want %d", got, want)
}
// Can we still retrieve earlier ones after we added later ones?
for _, p := range metas {
if !s.Has(p) {
t.Fatalf("%q not in set after all adds", p.Name)
}
}
// Can we remove metas?
for _, p := range metas {
s.Remove(p)
if s.Has(p) {
t.Fatalf("%q still in set after removing it", p.Name)
}
}
if count := s.Count(); count != 0 {
t.Fatalf("set has Count %d after all items removed", count)
}
}
func TestPluginMetaSetValidateVersions(t *testing.T) {
metas := []PluginMeta{
{
Name: "foo",
Version: "1.0.0",
Path: "test-foo",
},
{
Name: "bar",
Version: "0.0.1",
Path: "test-bar",
},
{
Name: "baz",
Version: "bananas",
Path: "test-bar",
},
}
s := make(PluginMetaSet)
for _, p := range metas {
s.Add(p)
}
valid, invalid := s.ValidateVersions()
if count := valid.Count(); count != 2 {
t.Errorf("valid set has %d metas; want 2", count)
}
if count := invalid.Count(); count != 1 {
t.Errorf("valid set has %d metas; want 1", count)
}
if !valid.Has(metas[0]) {
t.Errorf("'foo' not in valid set")
}
if !valid.Has(metas[1]) {
t.Errorf("'bar' not in valid set")
}
if !invalid.Has(metas[2]) {
t.Errorf("'baz' not in invalid set")
}
if invalid.Has(metas[0]) {
t.Errorf("'foo' in invalid set")
}
if invalid.Has(metas[1]) {
t.Errorf("'bar' in invalid set")
}
if valid.Has(metas[2]) {
t.Errorf("'baz' in valid set")
}
}
func TestPluginMetaSetWithName(t *testing.T) {
tests := []struct {
metas []PluginMeta
name string
wantCount int
}{
{
[]PluginMeta{},
"foo",
0,
},
{
[]PluginMeta{
{
Name: "foo",
Version: "0.0.1",
Path: "foo",
},
},
"foo",
1,
},
{
[]PluginMeta{
{
Name: "foo",
Version: "0.0.1",
Path: "foo",
},
},
"bar",
0,
},
}
for i, test := range tests {
t.Run(fmt.Sprintf("Test%02d", i), func(t *testing.T) {
s := make(PluginMetaSet)
for _, p := range test.metas {
s.Add(p)
}
filtered := s.WithName(test.name)
if gotCount := filtered.Count(); gotCount != test.wantCount {
t.Errorf("got count %d in %#v; want %d", gotCount, filtered, test.wantCount)
}
})
}
}
func TestPluginMetaSetByName(t *testing.T) {
metas := []PluginMeta{
{
Name: "foo",
Version: "1.0.0",
Path: "test-foo",
},
{
Name: "foo",
Version: "2.0.0",
Path: "test-foo-2",
},
{
Name: "bar",
Version: "0.0.1",
Path: "test-bar",
},
{
Name: "baz",
Version: "1.2.0",
Path: "test-bar",
},
}
s := make(PluginMetaSet)
for _, p := range metas {
s.Add(p)
}
byName := s.ByName()
if got, want := len(byName), 3; got != want {
t.Errorf("%d keys in ByName map; want %d", got, want)
}
if got, want := len(byName["foo"]), 2; got != want {
t.Errorf("%d metas for 'foo'; want %d", got, want)
}
if got, want := len(byName["bar"]), 1; got != want {
t.Errorf("%d metas for 'bar'; want %d", got, want)
}
if got, want := len(byName["baz"]), 1; got != want {
t.Errorf("%d metas for 'baz'; want %d", got, want)
}
if !byName["foo"].Has(metas[0]) {
t.Errorf("%#v missing from 'foo' set", metas[0])
}
if !byName["foo"].Has(metas[1]) {
t.Errorf("%#v missing from 'foo' set", metas[1])
}
if !byName["bar"].Has(metas[2]) {
t.Errorf("%#v missing from 'bar' set", metas[2])
}
if !byName["baz"].Has(metas[3]) {
t.Errorf("%#v missing from 'baz' set", metas[3])
}
}
func TestPluginMetaSetNewest(t *testing.T) {
tests := []struct {
versions []string
want string
}{
{
[]string{
"0.0.1",
},
"0.0.1",
},
{
[]string{
"0.0.1",
"0.0.2",
},
"0.0.2",
},
{
[]string{
"1.0.0",
"1.0.0-beta1",
},
"1.0.0",
},
{
[]string{
"0.0.1",
"1.0.0",
},
"1.0.0",
},
}
for _, test := range tests {
t.Run(strings.Join(test.versions, "|"), func(t *testing.T) {
s := make(PluginMetaSet)
for _, version := range test.versions {
s.Add(PluginMeta{
Name: "foo",
Version: VersionStr(version),
Path: "foo-V" + version,
})
}
newest := s.Newest()
if newest.Version != VersionStr(test.want) {
t.Errorf("version is %q; want %q", newest.Version, test.want)
}
})
}
}
func TestPluginMetaSetConstrainVersions(t *testing.T) {
metas := []PluginMeta{
{
Name: "foo",
Version: "1.0.0",
Path: "test-foo",
},
{
Name: "foo",
Version: "2.0.0",
Path: "test-foo-2",
},
{
Name: "foo",
Version: "3.0.0",
Path: "test-foo-2",
},
{
Name: "bar",
Version: "0.0.5",
Path: "test-bar",
},
{
Name: "baz",
Version: "0.0.1",
Path: "test-bar",
},
}
s := make(PluginMetaSet)
for _, p := range metas {
s.Add(p)
}
byName := s.ConstrainVersions(PluginRequirements{
"foo": &PluginConstraints{Versions: ConstraintStr(">=2.0.0").MustParse()},
"bar": &PluginConstraints{Versions: ConstraintStr(">=0.0.0").MustParse()},
"baz": &PluginConstraints{Versions: ConstraintStr(">=1.0.0").MustParse()},
"fun": &PluginConstraints{Versions: ConstraintStr(">5.0.0").MustParse()},
})
if got, want := len(byName), 3; got != want {
t.Errorf("%d keys in map; want %d", got, want)
}
if got, want := len(byName["foo"]), 2; got != want {
t.Errorf("%d metas for 'foo'; want %d", got, want)
}
if got, want := len(byName["bar"]), 1; got != want {
t.Errorf("%d metas for 'bar'; want %d", got, want)
}
if got, want := len(byName["baz"]), 0; got != want {
t.Errorf("%d metas for 'baz'; want %d", got, want)
}
// "fun" is not in the map at all, because we have no metas for that name
if !byName["foo"].Has(metas[1]) {
t.Errorf("%#v missing from 'foo' set", metas[1])
}
if !byName["foo"].Has(metas[2]) {
t.Errorf("%#v missing from 'foo' set", metas[2])
}
if !byName["bar"].Has(metas[3]) {
t.Errorf("%#v missing from 'bar' set", metas[3])
}
}
func TestPluginMetaSetOverridePaths(t *testing.T) {
metas := []PluginMeta{
{
Name: "foo",
Version: "1.0.0",
Path: "test-foo-1",
},
{
Name: "foo",
Version: "2.0.0",
Path: "test-foo-2",
},
{
Name: "foo",
Version: "3.0.0",
Path: "test-foo-3",
},
{
Name: "bar",
Version: "0.0.5",
Path: "test-bar-5",
},
{
Name: "bar",
Version: "0.0.6",
Path: "test-bar-6",
},
{
Name: "baz",
Version: "0.0.1",
Path: "test-bar",
},
}
s := make(PluginMetaSet)
for _, p := range metas {
s.Add(p)
}
ns := s.OverridePaths(map[string]string{
"foo": "override-foo",
"fun": "override-fun",
})
if got, want := ns.Count(), 5; got != want {
t.Errorf("got %d metas; want %d", got, want)
}
if !ns.Has(metas[3]) {
t.Errorf("new set is missing %#v", metas[3])
}
if !ns.Has(metas[4]) {
t.Errorf("new set is missing %#v", metas[4])
}
if !ns.Has(metas[5]) {
t.Errorf("new set is missing %#v", metas[5])
}
if !ns.Has(PluginMeta{
Name: "foo",
Version: "0.0.0",
Path: "override-foo",
}) {
t.Errorf("new set is missing 'foo' override")
}
if !ns.Has(PluginMeta{
Name: "fun",
Version: "0.0.0",
Path: "override-fun",
}) {
t.Errorf("new set is missing 'fun' override")
}
}

View File

@ -0,0 +1,22 @@
package discovery
import (
"fmt"
"testing"
)
func TestMetaSHA256(t *testing.T) {
m := PluginMeta{
Path: "test-fixtures/current-style-plugins/mockos_mockarch/terraform-foo-bar_v0.0.1",
}
hash, err := m.SHA256()
if err != nil {
t.Fatalf("failed: %s", err)
}
got := fmt.Sprintf("%x", hash)
want := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" // (hash of empty file)
if got != want {
t.Errorf("incorrect hash %s; want %s", got, want)
}
}

View File

@ -0,0 +1,105 @@
package discovery
import (
"bytes"
)
// PluginRequirements describes a set of plugins (assumed to be of a consistent
// kind) that are required to exist and have versions within the given
// corresponding sets.
type PluginRequirements map[string]*PluginConstraints
// PluginConstraints represents an element of PluginRequirements describing
// the constraints for a single plugin.
type PluginConstraints struct {
// Specifies that the plugin's version must be within the given
// constraints.
Versions Constraints
// If non-nil, the hash of the on-disk plugin executable must exactly
// match the SHA256 hash given here.
SHA256 []byte
}
// Allows returns true if the given version is within the receiver's version
// constraints.
func (s *PluginConstraints) Allows(v Version) bool {
return s.Versions.Allows(v)
}
// AcceptsSHA256 returns true if the given executable SHA256 hash is acceptable,
// either because it matches the constraint or because there is no such
// constraint.
func (s *PluginConstraints) AcceptsSHA256(digest []byte) bool {
if s.SHA256 == nil {
return true
}
return bytes.Equal(s.SHA256, digest)
}
// Merge takes the contents of the receiver and the other given requirements
// object and merges them together into a single requirements structure
// that satisfies both sets of requirements.
//
// Note that it doesn't make sense to merge two PluginRequirements with
// differing required plugin SHA256 hashes, since the result will never
// match any plugin.
func (r PluginRequirements) Merge(other PluginRequirements) PluginRequirements {
ret := make(PluginRequirements)
for n, c := range r {
ret[n] = &PluginConstraints{
Versions: Constraints{}.Append(c.Versions),
SHA256: c.SHA256,
}
}
for n, c := range other {
if existing, exists := ret[n]; exists {
ret[n].Versions = ret[n].Versions.Append(c.Versions)
if existing.SHA256 != nil {
if c.SHA256 != nil && !bytes.Equal(c.SHA256, existing.SHA256) {
// If we've been asked to merge two constraints with
// different SHA256 hashes then we'll produce a dummy value
// that can never match anything. This is a silly edge case
// that no reasonable caller should hit.
ret[n].SHA256 = []byte(invalidProviderHash)
}
} else {
ret[n].SHA256 = c.SHA256 // might still be nil
}
} else {
ret[n] = &PluginConstraints{
Versions: Constraints{}.Append(c.Versions),
SHA256: c.SHA256,
}
}
}
return ret
}
// LockExecutables applies additional constraints to the receiver that
// require plugin executables with specific SHA256 digests. This modifies
// the receiver in-place, since it's intended to be applied after
// version constraints have been resolved.
//
// The given map must include a key for every plugin that is already
// required. If not, any missing keys will cause the corresponding plugin
// to never match, though the direct caller doesn't necessarily need to
// guarantee this as long as the downstream code _applying_ these constraints
// is able to deal with the non-match in some way.
func (r PluginRequirements) LockExecutables(sha256s map[string][]byte) {
for name, cons := range r {
digest := sha256s[name]
if digest == nil {
// Prevent any match, which will then presumably cause the
// downstream consumer of this requirements to report an error.
cons.SHA256 = []byte(invalidProviderHash)
continue
}
cons.SHA256 = digest
}
}
const invalidProviderHash = "<invalid>"

View File

@ -0,0 +1,93 @@
package discovery
import (
"fmt"
"testing"
)
func TestPluginConstraintsAllows(t *testing.T) {
tests := []struct {
Constraints *PluginConstraints
Version string
Want bool
}{
{
&PluginConstraints{
Versions: AllVersions,
},
"1.0.0",
true,
},
{
&PluginConstraints{
Versions: ConstraintStr(">1.0.0").MustParse(),
},
"1.0.0",
false,
},
// This is not an exhaustive test because the callees
// already have plentiful tests of their own.
}
for i, test := range tests {
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
version := VersionStr(test.Version).MustParse()
got := test.Constraints.Allows(version)
if got != test.Want {
t.Logf("looking for %s in %#v", test.Version, test.Constraints)
t.Errorf("wrong result %#v; want %#v", got, test.Want)
}
})
}
}
func TestPluginConstraintsAcceptsSHA256(t *testing.T) {
mustUnhex := func(hex string) (ret []byte) {
_, err := fmt.Sscanf(hex, "%x", &ret)
if err != nil {
panic(err)
}
return ret
}
tests := []struct {
Constraints *PluginConstraints
Digest []byte
Want bool
}{
{
&PluginConstraints{
Versions: AllVersions,
SHA256: mustUnhex("0123456789abcdef"),
},
mustUnhex("0123456789abcdef"),
true,
},
{
&PluginConstraints{
Versions: AllVersions,
SHA256: mustUnhex("0123456789abcdef"),
},
mustUnhex("f00dface"),
false,
},
{
&PluginConstraints{
Versions: AllVersions,
SHA256: nil,
},
mustUnhex("f00dface"),
true,
},
}
for i, test := range tests {
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
got := test.Constraints.AcceptsSHA256(test.Digest)
if got != test.Want {
t.Logf("%#v.AcceptsSHA256(%#v)", test.Constraints, test.Digest)
t.Errorf("wrong result %#v; want %#v", got, test.Want)
}
})
}
}

View File

View File

@ -0,0 +1,66 @@
package discovery
import (
"fmt"
"sort"
version "github.com/hashicorp/go-version"
)
// A VersionStr is a string containing a possibly-invalid representation
// of a semver version number. Call Parse on it to obtain a real Version
// object, or discover that it is invalid.
type VersionStr string
// Parse transforms a VersionStr into a Version if it is
// syntactically valid. If it isn't then an error is returned instead.
func (s VersionStr) Parse() (Version, error) {
raw, err := version.NewVersion(string(s))
if err != nil {
return Version{}, err
}
return Version{raw}, nil
}
// MustParse transforms a VersionStr into a Version if it is
// syntactically valid. If it isn't then it panics.
func (s VersionStr) MustParse() Version {
ret, err := s.Parse()
if err != nil {
panic(err)
}
return ret
}
// Version represents a version number that has been parsed from
// a semver string and known to be valid.
type Version struct {
// We wrap this here just because it avoids a proliferation of
// direct go-version imports all over the place, and keeps the
// version-processing details within this package.
raw *version.Version
}
func (v Version) String() string {
return v.raw.String()
}
func (v Version) NewerThan(other Version) bool {
return v.raw.GreaterThan(other.raw)
}
// MinorUpgradeConstraintStr returns a ConstraintStr that would permit
// minor upgrades relative to the receiving version.
func (v Version) MinorUpgradeConstraintStr() ConstraintStr {
segments := v.raw.Segments()
return ConstraintStr(fmt.Sprintf("~> %d.%d", segments[0], segments[1]))
}
type Versions []Version
// Sort sorts version from newest to oldest.
func (v Versions) Sort() {
sort.Slice(v, func(i, j int) bool {
return v[i].NewerThan(v[j])
})
}

View File

@ -0,0 +1,84 @@
package discovery
import (
"sort"
version "github.com/hashicorp/go-version"
)
// A ConstraintStr is a string containing a possibly-invalid representation
// of a version constraint provided in configuration. Call Parse on it to
// obtain a real Constraint object, or discover that it is invalid.
type ConstraintStr string
// Parse transforms a ConstraintStr into a Constraints if it is
// syntactically valid. If it isn't then an error is returned instead.
func (s ConstraintStr) Parse() (Constraints, error) {
raw, err := version.NewConstraint(string(s))
if err != nil {
return Constraints{}, err
}
return Constraints{raw}, nil
}
// MustParse is like Parse but it panics if the constraint string is invalid.
func (s ConstraintStr) MustParse() Constraints {
ret, err := s.Parse()
if err != nil {
panic(err)
}
return ret
}
// Constraints represents a set of versions which any given Version is either
// a member of or not.
type Constraints struct {
raw version.Constraints
}
// AllVersions is a Constraints containing all versions
var AllVersions Constraints
func init() {
AllVersions = Constraints{
raw: make(version.Constraints, 0),
}
}
// Allows returns true if the given version permitted by the receiving
// constraints set.
func (s Constraints) Allows(v Version) bool {
return s.raw.Check(v.raw)
}
// Append combines the receiving set with the given other set to produce
// a set that is the intersection of both sets, which is to say that resulting
// constraints contain only the versions that are members of both.
func (s Constraints) Append(other Constraints) Constraints {
raw := make(version.Constraints, 0, len(s.raw)+len(other.raw))
// Since "raw" is a list of constraints that remove versions from the set,
// "Intersection" is implemented by concatenating together those lists,
// thus leaving behind only the versions not removed by either list.
raw = append(raw, s.raw...)
raw = append(raw, other.raw...)
// while the set is unordered, we sort these lexically for consistent output
sort.Slice(raw, func(i, j int) bool {
return raw[i].String() < raw[j].String()
})
return Constraints{raw}
}
// String returns a string representation of the set members as a set
// of range constraints.
func (s Constraints) String() string {
return s.raw.String()
}
// Unconstrained returns true if and only if the receiver is an empty
// constraint set.
func (s Constraints) Unconstrained() bool {
return len(s.raw) == 0
}

View File

@ -0,0 +1,64 @@
package discovery
import (
"fmt"
"testing"
)
func TestVersionSet(t *testing.T) {
tests := []struct {
ConstraintStr string
VersionStr string
ShouldHave bool
}{
// These test cases are not exhaustive since the underlying go-version
// library is well-tested. This is mainly here just to exercise our
// wrapper code, but also used as an opportunity to cover some basic
// but important cases such as the ~> constraint so that we'll be more
// likely to catch any accidental breaking behavior changes in the
// underlying library.
{
">=1.0.0",
"1.0.0",
true,
},
{
">=1.0.0",
"0.0.0",
false,
},
{
">=1.0.0",
"1.1.0-beta1",
true,
},
{
"~>1.1.0",
"1.1.2-beta1",
true,
},
{
"~>1.1.0",
"1.2.0",
false,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%s has %s", test.ConstraintStr, test.VersionStr), func(t *testing.T) {
accepted, err := ConstraintStr(test.ConstraintStr).Parse()
if err != nil {
t.Fatalf("unwanted error parsing constraints string %q: %s", test.ConstraintStr, err)
}
version, err := VersionStr(test.VersionStr).Parse()
if err != nil {
t.Fatalf("unwanted error parsing version string %q: %s", test.VersionStr, err)
}
if got, want := accepted.Allows(version), test.ShouldHave; got != want {
t.Errorf("Has returned %#v; want %#v", got, want)
}
})
}
}

Some files were not shown because too many files have changed in this diff Show More