Use New() instead of `once.Do(b.init)`

This commit is contained in:
Sander van Harmelen 2018-07-04 12:11:35 +02:00
parent 97d1c46602
commit 495d1ea350
13 changed files with 223 additions and 225 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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