Merge pull request #15208 from hashicorp/0.10-plugins
0.10 Core/Provider split work
This commit is contained in:
commit
73139ba6aa
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
`
|
||||
|
|
|
@ -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
|
||||
|
|
193
command/init.go
193
command/init.go
|
@ -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]
|
||||
`
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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.
|
||||
|
||||
`
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
provider "test" {
|
||||
|
||||
}
|
||||
|
||||
# No resource block present, so import fails
|
|
@ -0,0 +1,8 @@
|
|||
provider "test" {
|
||||
foo = "bar"
|
||||
|
||||
alias = "alias"
|
||||
}
|
||||
|
||||
resource "test_instance" "foo" {
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
# Declaring this resource implies that we depend on the
|
||||
# "test" provider, making it available for import.
|
||||
resource "test_instance" "foo" {
|
||||
}
|
|
@ -3,3 +3,6 @@ variable "foo" {}
|
|||
provider "test" {
|
||||
foo = "${var.foo}"
|
||||
}
|
||||
|
||||
resource "test_instance" "foo" {
|
||||
}
|
||||
|
|
|
@ -3,3 +3,6 @@ variable "foo" {}
|
|||
provider "test" {
|
||||
foo = "${var.foo}"
|
||||
}
|
||||
|
||||
resource "test_instance" "foo" {
|
||||
}
|
||||
|
|
|
@ -3,3 +3,6 @@ variable "foo" {}
|
|||
provider "test" {
|
||||
foo = "${var.foo}"
|
||||
}
|
||||
|
||||
resource "test_instance" "foo" {
|
||||
}
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
provider "test" {
|
||||
foo = "bar"
|
||||
}
|
||||
|
||||
resource "test_instance" "foo" {
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
provider "test" {
|
||||
version = "1.2.3"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
provider "test" {
|
||||
version = "1.2.3"
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
provider "foo" {
|
||||
|
||||
}
|
||||
|
||||
resource "bar_instance" "test" {
|
||||
|
||||
}
|
||||
|
||||
provider "baz" {
|
||||
version = "1.2.0"
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
resource "aws_instance" "foo" {}
|
||||
resource "test_instance" "foo" {}
|
||||
|
||||
atlas {
|
||||
name = "foo"
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
13
commands.go
13
commands.go
|
@ -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
265
config.go
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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{}{}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
provider "aws" {
|
||||
version = "bananas"
|
||||
a = "a"
|
||||
b = "b"
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
provider "aws" {
|
||||
version = "0.0.1"
|
||||
a = "a"
|
||||
b = "b"
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
provider "test" {
|
||||
version = "bananas!"
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
10
main.go
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 ""
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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>
|
||||
`
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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>"
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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])
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue