Skip to content

Commit

Permalink
Implement the Enterprise enhanced remote backend
Browse files Browse the repository at this point in the history
  • Loading branch information
Sander van Harmelen committed Aug 3, 2018
1 parent eada447 commit 1a67f7f
Show file tree
Hide file tree
Showing 37 changed files with 2,341 additions and 43 deletions.
23 changes: 19 additions & 4 deletions backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,29 @@ import (
"github.com/hashicorp/terraform/terraform"
)

// This is the name of the default, initial state that every backend
// must have. This state cannot be deleted.
// DefaultStateName is the name of the default, initial state that every
// backend must have. This state cannot be deleted.
const DefaultStateName = "default"

// Error value to return when a named state operation isn't supported.
// This must be returned rather than a custom error so that the Terraform
// CLI can detect it and handle it appropriately.
var ErrNamedStatesNotSupported = errors.New("named states not supported")
var (
// ErrNamedStatesNotSupported is returned when a named state operation
// isn't supported.
ErrNamedStatesNotSupported = errors.New("named states not supported")

// ErrDefaultStateNotSupported is returned when an operation does not support
// using the default state, but requires a named state to be selected.
ErrDefaultStateNotSupported = errors.New("default state not supported\n\n" +
"You can create a new workspace wth the \"workspace new\" command")

// ErrOperationNotSupported is returned when an unsupported operation
// is detecte by the configured backend.
ErrOperationNotSupported = errors.New("operation not supported")
)

// InitFn is used to initialize a new backend.
type InitFn func() Backend

// Backend is the minimal interface that must be implemented to enable Terraform.
type Backend interface {
Expand Down
39 changes: 28 additions & 11 deletions backend/init/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@
package init

import (
"os"
"sync"

"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/svchost/disco"
"github.com/hashicorp/terraform/terraform"

backendAtlas "github.com/hashicorp/terraform/backend/atlas"
backendLegacy "github.com/hashicorp/terraform/backend/legacy"
backendLocal "github.com/hashicorp/terraform/backend/local"
backendRemote "github.com/hashicorp/terraform/backend/remote"
backendAzure "github.com/hashicorp/terraform/backend/remote-state/azure"
backendConsul "github.com/hashicorp/terraform/backend/remote-state/consul"
backendEtcdv3 "github.com/hashicorp/terraform/backend/remote-state/etcdv3"
Expand All @@ -32,17 +35,27 @@ import (
// complex structures and supporting that over the plugin system is currently
// prohibitively difficult. For those wanting to implement a custom backend,
// they can do so with recompilation.
var backends map[string]func() backend.Backend
var backends map[string]backend.InitFn
var backendsLock sync.Mutex

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{
// Init initializes the backends map with all our hardcoded backends.
func Init(services *disco.Disco) {
backendsLock.Lock()
defer backendsLock.Unlock()

backends = map[string]backend.InitFn{
// Enhanced backends.
"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"`),
"remote": func() backend.Backend {
b := backendRemote.New(services)
if os.Getenv("TF_FORCE_LOCAL_BACKEND") != "" {
return backendLocal.NewWithBackend(b)
}
return b
},

// Remote State backends.
"atlas": func() backend.Backend { return backendAtlas.New() },
"azurerm": func() backend.Backend { return backendAzure.New() },
"consul": func() backend.Backend { return backendConsul.New() },
"etcdv3": func() backend.Backend { return backendEtcdv3.New() },
Expand All @@ -51,6 +64,10 @@ func init() {
"manta": func() backend.Backend { return backendManta.New() },
"s3": func() backend.Backend { return backendS3.New() },
"swift": func() backend.Backend { return backendSwift.New() },

// Deprecated backends.
"azure": deprecateBackend(backendAzure.New(),
`Warning: "azure" name is deprecated, please use "azurerm"`),
}

// Add the legacy remote backends that haven't yet been converted to
Expand All @@ -60,7 +77,7 @@ func init() {

// Backend returns the initialization factory for the given backend, or
// nil if none exists.
func Backend(name string) func() backend.Backend {
func Backend(name string) backend.InitFn {
backendsLock.Lock()
defer backendsLock.Unlock()
return backends[name]
Expand All @@ -73,7 +90,7 @@ func Backend(name string) func() backend.Backend {
// This method sets this backend globally and care should be taken to do
// this only before Terraform is executing to prevent odd behavior of backends
// changing mid-execution.
func Set(name string, f func() backend.Backend) {
func Set(name string, f backend.InitFn) {
backendsLock.Lock()
defer backendsLock.Unlock()

Expand Down Expand Up @@ -101,7 +118,7 @@ func (b deprecatedBackendShim) Validate(c *terraform.ResourceConfig) ([]string,

// DeprecateBackend can be used to wrap a backend to retrun a deprecation
// warning during validation.
func deprecateBackend(b backend.Backend, message string) func() backend.Backend {
func deprecateBackend(b backend.Backend, message string) backend.InitFn {
// Since a Backend wrapped by deprecatedBackendShim can no longer be
// asserted as an Enhanced or Local backend, disallow those types here
// entirely. If something other than a basic backend.Backend needs to be
Expand Down
110 changes: 110 additions & 0 deletions backend/init/init_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package init

import (
"os"
"reflect"
"testing"

backendLocal "github.com/hashicorp/terraform/backend/local"
)

func TestInit_backend(t *testing.T) {
// Initialize the backends map
Init(nil)

backends := []struct {
Name string
Type string
}{
{
"local",
"*local.Local",
}, {
"remote",
"*remote.Remote",
}, {
"atlas",
"*atlas.Backend",
}, {
"azurerm",
"*azure.Backend",
}, {
"consul",
"*consul.Backend",
}, {
"etcdv3",
"*etcd.Backend",
}, {
"gcs",
"*gcs.Backend",
}, {
"inmem",
"*inmem.Backend",
}, {
"manta",
"*manta.Backend",
}, {
"s3",
"*s3.Backend",
}, {
"swift",
"*swift.Backend",
}, {
"azure",
"init.deprecatedBackendShim",
},
}

// Make sure we get the requested backend
for _, b := range backends {
f := Backend(b.Name)
bType := reflect.TypeOf(f()).String()

if bType != b.Type {
t.Fatalf("expected backend %q to be %q, got: %q", b.Name, b.Type, bType)
}
}
}

func TestInit_forceLocalBackend(t *testing.T) {
// Initialize the backends map
Init(nil)

enhancedBackends := []struct {
Name string
Type string
}{
{
"local",
"nil",
}, {
"remote",
"*remote.Remote",
},
}

// Set the TF_FORCE_LOCAL_BACKEND flag so all enhanced backends will
// return a local.Local backend with themselves as embedded backend.
if err := os.Setenv("TF_FORCE_LOCAL_BACKEND", "1"); err != nil {
t.Fatalf("error setting environment variable TF_FORCE_LOCAL_BACKEND: %v", err)
}

// Make sure we always get the local backend.
for _, b := range enhancedBackends {
f := Backend(b.Name)

local, ok := f().(*backendLocal.Local)
if !ok {
t.Fatalf("expected backend %q to be \"*local.Local\", got: %T", b.Name, f())
}

bType := "nil"
if local.Backend != nil {
bType = reflect.TypeOf(local.Backend).String()
}

if bType != b.Type {
t.Fatalf("expected local.Backend to be %s, got: %s", b.Type, bType)
}
}
}
4 changes: 2 additions & 2 deletions backend/legacy/legacy.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import (
//
// If a type is already in the map, it will not be added. This will allow
// us to slowly convert the legacy types to first-class backends.
func Init(m map[string]func() backend.Backend) {
for k, _ := range remote.BuiltinClients {
func Init(m map[string]backend.InitFn) {
for k := range remote.BuiltinClients {
if _, ok := m[k]; !ok {
// Copy the "k" value since the variable "k" is reused for
// each key (address doesn't change).
Expand Down
4 changes: 2 additions & 2 deletions backend/legacy/legacy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
)

func TestInit(t *testing.T) {
m := make(map[string]func() backend.Backend)
m := make(map[string]backend.InitFn)
Init(m)

for k, _ := range remote.BuiltinClients {
Expand All @@ -24,7 +24,7 @@ func TestInit(t *testing.T) {
}

func TestInit_ignoreExisting(t *testing.T) {
m := make(map[string]func() backend.Backend)
m := make(map[string]backend.InitFn)
m["local"] = nil
Init(m)

Expand Down
44 changes: 44 additions & 0 deletions backend/local/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,50 @@ func (b *TestLocalSingleState) DeleteState(string) error {
return backend.ErrNamedStatesNotSupported
}

// TestNewLocalNoDefault is a factory for creating a TestLocalNoDefaultState.
// This function matches the signature required for backend/init.
func TestNewLocalNoDefault() backend.Backend {
return &TestLocalNoDefaultState{Local: New()}
}

// TestLocalNoDefaultState is a backend implementation that wraps
// Local and modifies it to support named states, but not the
// default state. It returns ErrDefaultStateNotSupported when the
// DefaultStateName is used.
type TestLocalNoDefaultState struct {
*Local
}

func (b *TestLocalNoDefaultState) State(name string) (state.State, error) {
if name == backend.DefaultStateName {
return nil, backend.ErrDefaultStateNotSupported
}
return b.Local.State(name)
}

func (b *TestLocalNoDefaultState) States() ([]string, error) {
states, err := b.Local.States()
if err != nil {
return nil, err
}

filtered := states[:0]
for _, name := range states {
if name != backend.DefaultStateName {
filtered = append(filtered, name)
}
}

return filtered, nil
}

func (b *TestLocalNoDefaultState) DeleteState(name string) error {
if name == backend.DefaultStateName {
return backend.ErrDefaultStateNotSupported
}
return b.Local.DeleteState(name)
}

func testTempDir(t *testing.T) string {
d, err := ioutil.TempDir("", "tf")
if err != nil {
Expand Down
Loading

0 comments on commit 1a67f7f

Please sign in to comment.