diff --git a/backend/atlas/backend.go b/backend/atlas/backend.go index 660327ae0..655de23c9 100644 --- a/backend/atlas/backend.go +++ b/backend/atlas/backend.go @@ -39,59 +39,14 @@ type Backend struct { // schema is the schema for configuration, set by init schema *schema.Backend - once sync.Once // opLock locks operations opLock sync.Mutex } -func (b *Backend) Input( - ui terraform.UIInput, c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) { - b.once.Do(b.init) - return b.schema.Input(ui, c) -} - -func (b *Backend) Validate(c *terraform.ResourceConfig) ([]string, []error) { - b.once.Do(b.init) - return b.schema.Validate(c) -} - -func (b *Backend) Configure(c *terraform.ResourceConfig) error { - b.once.Do(b.init) - return b.schema.Configure(c) -} - -func (b *Backend) States() ([]string, error) { - return nil, backend.ErrNamedStatesNotSupported -} - -func (b *Backend) DeleteState(name string) error { - return backend.ErrNamedStatesNotSupported -} - -func (b *Backend) State(name string) (state.State, error) { - if name != backend.DefaultStateName { - return nil, backend.ErrNamedStatesNotSupported - } - - return &remote.State{Client: b.stateClient}, nil -} - -// Colorize returns the Colorize structure that can be used for colorizing -// output. This is gauranteed to always return a non-nil value and so is useful -// as a helper to wrap any potentially colored strings. -func (b *Backend) Colorize() *colorstring.Colorize { - if b.CLIColor != nil { - return b.CLIColor - } - - return &colorstring.Colorize{ - Colors: colorstring.DefaultColors, - Disable: true, - } -} - -func (b *Backend) init() { +// New returns a new initialized Atlas backend. +func New() *Backend { + b := &Backend{} b.schema = &schema.Backend{ Schema: map[string]*schema.Schema{ "name": &schema.Schema{ @@ -115,11 +70,13 @@ func (b *Backend) init() { }, }, - ConfigureFunc: b.schemaConfigure, + ConfigureFunc: b.configure, } + + return b } -func (b *Backend) schemaConfigure(ctx context.Context) error { +func (b *Backend) configure(ctx context.Context) error { d := schema.FromContextBackendConfig(ctx) // Parse the address @@ -153,6 +110,47 @@ func (b *Backend) schemaConfigure(ctx context.Context) error { return nil } +func (b *Backend) Input(ui terraform.UIInput, c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) { + return b.schema.Input(ui, c) +} + +func (b *Backend) Validate(c *terraform.ResourceConfig) ([]string, []error) { + return b.schema.Validate(c) +} + +func (b *Backend) Configure(c *terraform.ResourceConfig) error { + return b.schema.Configure(c) +} + +func (b *Backend) State(name string) (state.State, error) { + if name != backend.DefaultStateName { + return nil, backend.ErrNamedStatesNotSupported + } + return &remote.State{Client: b.stateClient}, nil +} + +func (b *Backend) DeleteState(name string) error { + return backend.ErrNamedStatesNotSupported +} + +func (b *Backend) States() ([]string, error) { + return nil, backend.ErrNamedStatesNotSupported +} + +// Colorize returns the Colorize structure that can be used for colorizing +// output. This is gauranteed to always return a non-nil value and so is useful +// as a helper to wrap any potentially colored strings. +func (b *Backend) Colorize() *colorstring.Colorize { + if b.CLIColor != nil { + return b.CLIColor + } + + return &colorstring.Colorize{ + Colors: colorstring.DefaultColors, + Disable: true, + } +} + var schemaDescriptions = map[string]string{ "name": "Full name of the environment in Atlas, such as 'hashicorp/myenv'", "access_token": "Access token to use to access Atlas. If ATLAS_TOKEN is set then\n" + diff --git a/backend/atlas/backend_test.go b/backend/atlas/backend_test.go index 313a528d2..286d3de8d 100644 --- a/backend/atlas/backend_test.go +++ b/backend/atlas/backend_test.go @@ -18,7 +18,7 @@ func TestConfigure_envAddr(t *testing.T) { defer os.Setenv("ATLAS_ADDRESS", os.Getenv("ATLAS_ADDRESS")) os.Setenv("ATLAS_ADDRESS", "http://foo.com") - b := &Backend{} + b := New() err := b.Configure(terraform.NewResourceConfig(config.TestRawConfig(t, map[string]interface{}{ "name": "foo/bar", }))) @@ -35,7 +35,7 @@ func TestConfigure_envToken(t *testing.T) { defer os.Setenv("ATLAS_TOKEN", os.Getenv("ATLAS_TOKEN")) os.Setenv("ATLAS_TOKEN", "foo") - b := &Backend{} + b := New() err := b.Configure(terraform.NewResourceConfig(config.TestRawConfig(t, map[string]interface{}{ "name": "foo/bar", }))) diff --git a/backend/atlas/state_client_test.go b/backend/atlas/state_client_test.go index 2fe85559d..5135bfd7d 100644 --- a/backend/atlas/state_client_test.go +++ b/backend/atlas/state_client_test.go @@ -20,7 +20,7 @@ import ( ) func testStateClient(t *testing.T, c map[string]interface{}) remote.Client { - b := backend.TestBackendConfig(t, &Backend{}, c) + b := backend.TestBackendConfig(t, New(), c) raw, err := b.State(backend.DefaultStateName) if err != nil { t.Fatalf("err: %s", err) diff --git a/backend/init/init.go b/backend/init/init.go index aeadb779f..056827905 100644 --- a/backend/init/init.go +++ b/backend/init/init.go @@ -39,8 +39,8 @@ func init() { // Our hardcoded backends. We don't need to acquire a lock here // since init() code is serial and can't spawn goroutines. backends = map[string]func() backend.Backend{ - "local": func() backend.Backend { return &backendLocal.Local{} }, - "atlas": func() backend.Backend { return &backendAtlas.Backend{} }, + "local": func() backend.Backend { return backendLocal.New() }, + "atlas": func() backend.Backend { return backendAtlas.New() }, "azure": deprecateBackend(backendAzure.New(), `Warning: "azure" name is deprecated, please use "azurerm"`), "azurerm": func() backend.Backend { return backendAzure.New() }, diff --git a/backend/local/backend.go b/backend/local/backend.go index eb4eff4b0..abb4d37c9 100644 --- a/backend/local/backend.go +++ b/backend/local/backend.go @@ -91,94 +91,106 @@ type Local struct { schema *schema.Backend opLock sync.Mutex - once sync.Once } -func (b *Local) Input( - ui terraform.UIInput, c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) { - b.once.Do(b.init) +// New returns a new initialized local backend. +func New() *Local { + return NewWithBackend(nil) +} +// NewWithBackend returns a new local backend initialized with a +// dedicated backend for non-enhanced behavior. +func NewWithBackend(backend backend.Backend) *Local { + b := &Local{ + Backend: backend, + } + + b.schema = &schema.Backend{ + Schema: map[string]*schema.Schema{ + "path": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "", + }, + + "workspace_dir": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "", + }, + + "environment_dir": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "", + ConflictsWith: []string{"workspace_dir"}, + Deprecated: "workspace_dir should be used instead, with the same meaning", + }, + }, + + ConfigureFunc: b.configure, + } + + return b +} + +func (b *Local) configure(ctx context.Context) error { + d := schema.FromContextBackendConfig(ctx) + + // Set the path if it is set + pathRaw, ok := d.GetOk("path") + if ok { + path := pathRaw.(string) + if path == "" { + return fmt.Errorf("configured path is empty") + } + + b.StatePath = path + b.StateOutPath = path + } + + if raw, ok := d.GetOk("workspace_dir"); ok { + path := raw.(string) + if path != "" { + b.StateWorkspaceDir = path + } + } + + // Legacy name, which ConflictsWith workspace_dir + if raw, ok := d.GetOk("environment_dir"); ok { + path := raw.(string) + if path != "" { + b.StateWorkspaceDir = path + } + } + + return nil +} + +func (b *Local) Input(ui terraform.UIInput, c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) { f := b.schema.Input if b.Backend != nil { f = b.Backend.Input } - return f(ui, c) } func (b *Local) Validate(c *terraform.ResourceConfig) ([]string, []error) { - b.once.Do(b.init) - f := b.schema.Validate if b.Backend != nil { f = b.Backend.Validate } - return f(c) } func (b *Local) Configure(c *terraform.ResourceConfig) error { - b.once.Do(b.init) - f := b.schema.Configure if b.Backend != nil { f = b.Backend.Configure } - return f(c) } -func (b *Local) States() ([]string, error) { - // If we have a backend handling state, defer to that. - if b.Backend != nil { - return b.Backend.States() - } - - // the listing always start with "default" - envs := []string{backend.DefaultStateName} - - entries, err := ioutil.ReadDir(b.stateWorkspaceDir()) - // no error if there's no envs configured - if os.IsNotExist(err) { - return envs, nil - } - if err != nil { - return nil, err - } - - var listed []string - for _, entry := range entries { - if entry.IsDir() { - listed = append(listed, filepath.Base(entry.Name())) - } - } - - sort.Strings(listed) - envs = append(envs, listed...) - - return envs, nil -} - -// DeleteState removes a named state. -// The "default" state cannot be removed. -func (b *Local) DeleteState(name string) error { - // If we have a backend handling state, defer to that. - if b.Backend != nil { - return b.Backend.DeleteState(name) - } - - if name == "" { - return errors.New("empty state name") - } - - if name == backend.DefaultStateName { - return errors.New("cannot delete default state") - } - - delete(b.states, name) - return os.RemoveAll(filepath.Join(b.stateWorkspaceDir(), name)) -} - func (b *Local) State(name string) (state.State, error) { statePath, stateOutPath, backupPath := b.StatePaths(name) @@ -216,6 +228,57 @@ func (b *Local) State(name string) (state.State, error) { return s, nil } +// DeleteState removes a named state. +// The "default" state cannot be removed. +func (b *Local) DeleteState(name string) error { + // If we have a backend handling state, defer to that. + if b.Backend != nil { + return b.Backend.DeleteState(name) + } + + if name == "" { + return errors.New("empty state name") + } + + if name == backend.DefaultStateName { + return errors.New("cannot delete default state") + } + + delete(b.states, name) + return os.RemoveAll(filepath.Join(b.stateWorkspaceDir(), name)) +} + +func (b *Local) States() ([]string, error) { + // If we have a backend handling state, defer to that. + if b.Backend != nil { + return b.Backend.States() + } + + // the listing always start with "default" + envs := []string{backend.DefaultStateName} + + entries, err := ioutil.ReadDir(b.stateWorkspaceDir()) + // no error if there's no envs configured + if os.IsNotExist(err) { + return envs, nil + } + if err != nil { + return nil, err + } + + var listed []string + for _, entry := range entries { + if entry.IsDir() { + listed = append(listed, filepath.Base(entry.Name())) + } + } + + sort.Strings(listed) + envs = append(envs, listed...) + + return envs, nil +} + // Operation implements backend.Enhanced // // This will initialize an in-memory terraform.Context to perform the @@ -348,68 +411,6 @@ func (b *Local) Colorize() *colorstring.Colorize { } } -func (b *Local) init() { - b.schema = &schema.Backend{ - Schema: map[string]*schema.Schema{ - "path": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Default: "", - }, - - "workspace_dir": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Default: "", - }, - - "environment_dir": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Default: "", - ConflictsWith: []string{"workspace_dir"}, - - Deprecated: "workspace_dir should be used instead, with the same meaning", - }, - }, - - ConfigureFunc: b.schemaConfigure, - } -} - -func (b *Local) schemaConfigure(ctx context.Context) error { - d := schema.FromContextBackendConfig(ctx) - - // Set the path if it is set - pathRaw, ok := d.GetOk("path") - if ok { - path := pathRaw.(string) - if path == "" { - return fmt.Errorf("configured path is empty") - } - - b.StatePath = path - b.StateOutPath = path - } - - if raw, ok := d.GetOk("workspace_dir"); ok { - path := raw.(string) - if path != "" { - b.StateWorkspaceDir = path - } - } - - // Legacy name, which ConflictsWith workspace_dir - if raw, ok := d.GetOk("environment_dir"); ok { - path := raw.(string) - if path != "" { - b.StateWorkspaceDir = path - } - } - - return nil -} - // StatePaths returns the StatePath, StateOutPath, and StateBackupPath as // configured from the CLI. func (b *Local) StatePaths(name string) (string, string, string) { diff --git a/backend/local/backend_local.go b/backend/local/backend_local.go index d26fefa57..4c810f50e 100644 --- a/backend/local/backend_local.go +++ b/backend/local/backend_local.go @@ -5,15 +5,13 @@ import ( "errors" "log" - "github.com/hashicorp/terraform/command/clistate" - "github.com/hashicorp/terraform/command/format" - - "github.com/hashicorp/terraform/tfdiags" - "github.com/hashicorp/errwrap" "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/command/clistate" + "github.com/hashicorp/terraform/command/format" "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/tfdiags" ) // backend.Local implementation. diff --git a/backend/local/backend_test.go b/backend/local/backend_test.go index 3852aae2f..7162f7a6d 100644 --- a/backend/local/backend_test.go +++ b/backend/local/backend_test.go @@ -15,14 +15,14 @@ import ( ) func TestLocal_impl(t *testing.T) { - var _ backend.Enhanced = new(Local) - var _ backend.Local = new(Local) - var _ backend.CLI = new(Local) + var _ backend.Enhanced = New() + var _ backend.Local = New() + var _ backend.CLI = New() } func TestLocal_backend(t *testing.T) { defer testTmpDir(t)() - b := &Local{} + b := New() backend.TestBackendStates(t, b) backend.TestBackendStateLocks(t, b, b) } @@ -49,7 +49,7 @@ func checkState(t *testing.T, path, expected string) { } func TestLocal_StatePaths(t *testing.T) { - b := &Local{} + b := New() // Test the defaults path, out, back := b.StatePaths("") @@ -94,7 +94,7 @@ func TestLocal_addAndRemoveStates(t *testing.T) { dflt := backend.DefaultStateName expectedStates := []string{dflt} - b := &Local{} + b := New() states, err := b.States() if err != nil { t.Fatal(err) @@ -210,13 +210,11 @@ func (b *testDelegateBackend) DeleteState(name string) error { // verify that the MultiState methods are dispatched to the correct Backend. func TestLocal_multiStateBackend(t *testing.T) { // assign a separate backend where we can read the state - b := &Local{ - Backend: &testDelegateBackend{ - stateErr: true, - statesErr: true, - deleteErr: true, - }, - } + b := NewWithBackend(&testDelegateBackend{ + stateErr: true, + statesErr: true, + deleteErr: true, + }) if _, err := b.State("test"); err != errTestDelegateState { t.Fatal("expected errTestDelegateState, got:", err) diff --git a/backend/local/testing.go b/backend/local/testing.go index 4d480f6d7..bd07fc886 100644 --- a/backend/local/testing.go +++ b/backend/local/testing.go @@ -18,13 +18,14 @@ import ( // public fields without any locks. func TestLocal(t *testing.T) (*Local, func()) { tempDir := testTempDir(t) - local := &Local{ - StatePath: filepath.Join(tempDir, "state.tfstate"), - StateOutPath: filepath.Join(tempDir, "state.tfstate"), - StateBackupPath: filepath.Join(tempDir, "state.tfstate.bak"), - StateWorkspaceDir: filepath.Join(tempDir, "state.tfstate.d"), - ContextOpts: &terraform.ContextOpts{}, - } + + local := New() + local.StatePath = filepath.Join(tempDir, "state.tfstate") + local.StateOutPath = filepath.Join(tempDir, "state.tfstate") + local.StateBackupPath = filepath.Join(tempDir, "state.tfstate.bak") + local.StateWorkspaceDir = filepath.Join(tempDir, "state.tfstate.d") + local.ContextOpts = &terraform.ContextOpts{} + cleanup := func() { if err := os.RemoveAll(tempDir); err != nil { t.Fatal("error clecanup up test:", err) @@ -69,7 +70,7 @@ func TestLocalProvider(t *testing.T, b *Local, name string) *terraform.MockResou // TestNewLocalSingle is a factory for creating a TestLocalSingleState. // This function matches the signature required for backend/init. func TestNewLocalSingle() backend.Backend { - return &TestLocalSingleState{} + return &TestLocalSingleState{Local: New()} } // TestLocalSingleState is a backend implementation that wraps Local @@ -79,7 +80,7 @@ func TestNewLocalSingle() backend.Backend { // This isn't an actual use case, this is exported just to provide a // easy way to test that behavior. type TestLocalSingleState struct { - Local + *Local } func (b *TestLocalSingleState) State(name string) (state.State, error) { diff --git a/backend/remote-state/gcs/backend.go b/backend/remote-state/gcs/backend.go index fc5109264..26b430b41 100644 --- a/backend/remote-state/gcs/backend.go +++ b/backend/remote-state/gcs/backend.go @@ -18,10 +18,10 @@ import ( "google.golang.org/api/option" ) -// gcsBackend implements "backend".Backend for GCS. +// Backend implements "backend".Backend for GCS. // Input(), Validate() and Configure() are implemented by embedding *schema.Backend. // State(), DeleteState() and States() are implemented explicitly. -type gcsBackend struct { +type Backend struct { *schema.Backend storageClient *storage.Client @@ -38,9 +38,9 @@ type gcsBackend struct { } func New() backend.Backend { - be := &gcsBackend{} - be.Backend = &schema.Backend{ - ConfigureFunc: be.configure, + b := &Backend{} + b.Backend = &schema.Backend{ + ConfigureFunc: b.configure, Schema: map[string]*schema.Schema{ "bucket": { Type: schema.TypeString, @@ -91,10 +91,10 @@ func New() backend.Backend { }, } - return be + return b } -func (b *gcsBackend) configure(ctx context.Context) error { +func (b *Backend) configure(ctx context.Context) error { if b.storageClient != nil { return nil } diff --git a/backend/remote-state/gcs/backend_state.go b/backend/remote-state/gcs/backend_state.go index 61e3e3f25..bc75465b5 100644 --- a/backend/remote-state/gcs/backend_state.go +++ b/backend/remote-state/gcs/backend_state.go @@ -21,7 +21,7 @@ const ( // States returns a list of names for the states found on GCS. The default // state is always returned as the first element in the slice. -func (b *gcsBackend) States() ([]string, error) { +func (b *Backend) States() ([]string, error) { states := []string{backend.DefaultStateName} bucket := b.storageClient.Bucket(b.bucketName) @@ -54,7 +54,7 @@ func (b *gcsBackend) States() ([]string, error) { } // DeleteState deletes the named state. The "default" state cannot be deleted. -func (b *gcsBackend) DeleteState(name string) error { +func (b *Backend) DeleteState(name string) error { if name == backend.DefaultStateName { return fmt.Errorf("cowardly refusing to delete the %q state", name) } @@ -68,7 +68,7 @@ func (b *gcsBackend) DeleteState(name string) error { } // client returns a remoteClient for the named state. -func (b *gcsBackend) client(name string) (*remoteClient, error) { +func (b *Backend) client(name string) (*remoteClient, error) { if name == "" { return nil, fmt.Errorf("%q is not a valid state name", name) } @@ -85,7 +85,7 @@ func (b *gcsBackend) client(name string) (*remoteClient, error) { // State reads and returns the named state from GCS. If the named state does // not yet exist, a new state file is created. -func (b *gcsBackend) State(name string) (state.State, error) { +func (b *Backend) State(name string) (state.State, error) { c, err := b.client(name) if err != nil { return nil, err @@ -144,14 +144,14 @@ func (b *gcsBackend) State(name string) (state.State, error) { return st, nil } -func (b *gcsBackend) stateFile(name string) string { +func (b *Backend) stateFile(name string) string { if name == backend.DefaultStateName && b.defaultStateFile != "" { return b.defaultStateFile } return path.Join(b.prefix, name+stateFileSuffix) } -func (b *gcsBackend) lockFile(name string) string { +func (b *Backend) lockFile(name string) string { if name == backend.DefaultStateName && b.defaultStateFile != "" { return strings.TrimSuffix(b.defaultStateFile, stateFileSuffix) + lockFileSuffix } diff --git a/backend/remote-state/gcs/backend_test.go b/backend/remote-state/gcs/backend_test.go index b561b1f23..61d27da8c 100644 --- a/backend/remote-state/gcs/backend_test.go +++ b/backend/remote-state/gcs/backend_test.go @@ -39,7 +39,7 @@ func TestStateFile(t *testing.T) { {"state", "legacy.state", "test", "state/test.tfstate", "state/test.tflock"}, } for _, c := range cases { - b := &gcsBackend{ + b := &Backend{ prefix: c.prefix, defaultStateFile: c.defaultStateFile, } @@ -188,7 +188,7 @@ func setupBackend(t *testing.T, bucket, prefix, key string) backend.Backend { } b := backend.TestBackendConfig(t, New(), config) - be := b.(*gcsBackend) + be := b.(*Backend) // create the bucket if it doesn't exist bkt := be.storageClient.Bucket(bucket) @@ -213,7 +213,7 @@ func setupBackend(t *testing.T, bucket, prefix, key string) backend.Backend { // teardownBackend deletes all states from be except the default state. func teardownBackend(t *testing.T, be backend.Backend, prefix string) { t.Helper() - gcsBE, ok := be.(*gcsBackend) + gcsBE, ok := be.(*Backend) if !ok { t.Fatalf("be is a %T, want a *gcsBackend", be) } diff --git a/command/init.go b/command/init.go index 86df82ee7..efa4b5724 100644 --- a/command/init.go +++ b/command/init.go @@ -138,11 +138,13 @@ func (c *InitCommand) Run(args []string) int { // If our directory is empty, then we're done. We can't get or setup // the backend with an empty directory. - if empty, err := config.IsEmptyDir(path); err != nil { + empty, err := config.IsEmptyDir(path) + if err != nil { c.Ui.Error(fmt.Sprintf( "Error checking configuration: %s", err)) return 1 - } else if empty { + } + if empty { c.Ui.Output(c.Colorize().Color(strings.TrimSpace(outputInitEmpty))) return 0 } diff --git a/command/meta_backend.go b/command/meta_backend.go index 73d346950..185e8b6da 100644 --- a/command/meta_backend.go +++ b/command/meta_backend.go @@ -136,7 +136,7 @@ func (m *Meta) Backend(opts *BackendOpts) (backend.Enhanced, error) { } // Build the local backend - local := &backendLocal.Local{Backend: b} + local := backendLocal.NewWithBackend(b) if err := local.CLIInit(cliOpts); err != nil { // Local backend isn't allowed to fail. It would be a bug. panic(err)