diff --git a/backend/init/init.go b/backend/init/init.go index 8fcd249c4..62884e9be 100644 --- a/backend/init/init.go +++ b/backend/init/init.go @@ -3,25 +3,24 @@ package init import ( - "os" "sync" "github.com/hashicorp/terraform/backend" - "github.com/hashicorp/terraform/svchost/disco" - "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/tfdiags" - 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" + backendatlas "github.com/hashicorp/terraform/backend/atlas" + backendlocal "github.com/hashicorp/terraform/backend/local" 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" + backendconsul "github.com/hashicorp/terraform/backend/remote-state/consul" + backendetcdv2 "github.com/hashicorp/terraform/backend/remote-state/etcdv2" + backendetcdv3 "github.com/hashicorp/terraform/backend/remote-state/etcdv3" backendGCS "github.com/hashicorp/terraform/backend/remote-state/gcs" - backendInmem "github.com/hashicorp/terraform/backend/remote-state/inmem" + backendinmem "github.com/hashicorp/terraform/backend/remote-state/inmem" backendManta "github.com/hashicorp/terraform/backend/remote-state/manta" backendS3 "github.com/hashicorp/terraform/backend/remote-state/s3" backendSwift "github.com/hashicorp/terraform/backend/remote-state/swift" + + "github.com/zclconf/go-cty/cty" ) // backends is the list of available backends. This is a global variable @@ -35,49 +34,37 @@ 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]backend.InitFn +var backends map[string]func() backend.Backend var backendsLock sync.Mutex -// Init initializes the backends map with all our hardcoded backends. -func Init(services *disco.Disco) { - backendsLock.Lock() - defer backendsLock.Unlock() +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{ + "atlas": func() backend.Backend { return &backendatlas.Backend{} }, + "local": func() backend.Backend { return &backendlocal.Local{} }, + "consul": func() backend.Backend { return backendconsul.New() }, + "inmem": func() backend.Backend { return backendinmem.New() }, + "swift": func() backend.Backend { return backendSwift.New() }, + "s3": func() backend.Backend { return backendS3.New() }, + "azurerm": func() backend.Backend { return backendAzure.New() }, + "etcd": func() backend.Backend { return backendetcdv2.New() }, + "etcdv3": func() backend.Backend { return backendetcdv3.New() }, + "gcs": func() backend.Backend { return backendGCS.New() }, + "manta": func() backend.Backend { return backendManta.New() }, - backends = map[string]backend.InitFn{ - // Enhanced backends. - "local": func() backend.Backend { return backendLocal.New() }, - "remote": func() backend.Backend { - b := backendRemote.New(services) - if os.Getenv("TF_FORCE_LOCAL_BACKEND") != "" { - return backendLocal.NewWithBackend(b) - } - return b + "azure": func() backend.Backend { + return deprecateBackend( + backendAzure.New(), + `Warning: "azure" name is deprecated, please use "azurerm"`, + ) }, - - // 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() }, - "gcs": func() backend.Backend { return backendGCS.New() }, - "inmem": func() backend.Backend { return backendInmem.New() }, - "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 - // the new backend API. - backendLegacy.Init(backends) } // Backend returns the initialization factory for the given backend, or // nil if none exists. -func Backend(name string) backend.InitFn { +func Backend(name string) func() backend.Backend { backendsLock.Lock() defer backendsLock.Unlock() return backends[name] @@ -90,7 +77,7 @@ func Backend(name string) backend.InitFn { // 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 backend.InitFn) { +func Set(name string, f func() backend.Backend) { backendsLock.Lock() defer backendsLock.Unlock() @@ -109,16 +96,16 @@ type deprecatedBackendShim struct { Message string } -// Validate the Backend then add the deprecation warning. -func (b deprecatedBackendShim) Validate(c *terraform.ResourceConfig) ([]string, []error) { - warns, errs := b.Backend.Validate(c) - warns = append(warns, b.Message) - return warns, errs +// ValidateConfig delegates to the wrapped backend to validate its config +// and then appends shim's deprecation warning. +func (b deprecatedBackendShim) ValidateConfig(obj cty.Value) tfdiags.Diagnostics { + diags := b.Backend.ValidateConfig(obj) + return diags.Append(tfdiags.SimpleWarning(b.Message)) } // DeprecateBackend can be used to wrap a backend to retrun a deprecation // warning during validation. -func deprecateBackend(b backend.Backend, message string) backend.InitFn { +func deprecateBackend(b backend.Backend, message string) backend.Backend { // 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 @@ -132,10 +119,8 @@ func deprecateBackend(b backend.Backend, message string) backend.InitFn { panic("cannot use DeprecateBackend on a Local Backend") } - return func() backend.Backend { - return deprecatedBackendShim{ - Backend: b, - Message: message, - } + return deprecatedBackendShim{ + Backend: b, + Message: message, } } diff --git a/backend/remote-state/etcdv2/backend.go b/backend/remote-state/etcdv2/backend.go new file mode 100644 index 000000000..231175ea3 --- /dev/null +++ b/backend/remote-state/etcdv2/backend.go @@ -0,0 +1,96 @@ +// legacy etcd2.x backend + +package etcdv2 + +import ( + "context" + "strings" + + etcdapi "github.com/coreos/etcd/client" + "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/state" + "github.com/hashicorp/terraform/state/remote" +) + +func New() backend.Backend { + s := &schema.Backend{ + Schema: map[string]*schema.Schema{ + "path": &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: "The path where to store the state", + }, + "endpoints": &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: "A space-separated list of the etcd endpoints", + }, + "username": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Description: "Username", + }, + "password": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Description: "Password", + }, + }, + } + + result := &Backend{Backend: s} + result.Backend.ConfigureFunc = result.configure + return result +} + +type Backend struct { + *schema.Backend + + client etcdapi.Client + path string +} + +func (b *Backend) configure(ctx context.Context) error { + data := schema.FromContextBackendConfig(ctx) + + b.path = data.Get("path").(string) + + endpoints := data.Get("endpoints").(string) + username := data.Get("username").(string) + password := data.Get("password").(string) + + config := etcdapi.Config{ + Endpoints: strings.Split(endpoints, " "), + Username: username, + Password: password, + } + + client, err := etcdapi.New(config) + if err != nil { + return err + } + + b.client = client + return nil +} + +func (b *Backend) States() ([]string, error) { + return nil, backend.ErrNamedStatesNotSupported +} + +func (b *Backend) DeleteState(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: &EtcdClient{ + Client: b.client, + Path: b.path, + }, + }, nil +} diff --git a/backend/remote-state/etcdv2/backend_test.go b/backend/remote-state/etcdv2/backend_test.go new file mode 100644 index 000000000..c992f624e --- /dev/null +++ b/backend/remote-state/etcdv2/backend_test.go @@ -0,0 +1,11 @@ +package etcdv2 + +import ( + "testing" + + "github.com/hashicorp/terraform/backend" +) + +func TestBackend_impl(t *testing.T) { + var _ backend.Backend = new(Backend) +} diff --git a/state/remote/etcd.go b/backend/remote-state/etcdv2/client.go similarity index 53% rename from state/remote/etcd.go rename to backend/remote-state/etcdv2/client.go index 7993603ff..faf891ac9 100644 --- a/state/remote/etcd.go +++ b/backend/remote-state/etcdv2/client.go @@ -1,53 +1,21 @@ -package remote +package etcdv2 import ( + "context" "crypto/md5" "fmt" - "strings" etcdapi "github.com/coreos/etcd/client" - "golang.org/x/net/context" + "github.com/hashicorp/terraform/state/remote" ) -func etcdFactory(conf map[string]string) (Client, error) { - path, ok := conf["path"] - if !ok { - return nil, fmt.Errorf("missing 'path' configuration") - } - - endpoints, ok := conf["endpoints"] - if !ok || endpoints == "" { - return nil, fmt.Errorf("missing 'endpoints' configuration") - } - - config := etcdapi.Config{ - Endpoints: strings.Split(endpoints, " "), - } - if username, ok := conf["username"]; ok && username != "" { - config.Username = username - } - if password, ok := conf["password"]; ok && password != "" { - config.Password = password - } - - client, err := etcdapi.New(config) - if err != nil { - return nil, err - } - - return &EtcdClient{ - Client: client, - Path: path, - }, nil -} - // EtcdClient is a remote client that stores data in etcd. type EtcdClient struct { Client etcdapi.Client Path string } -func (c *EtcdClient) Get() (*Payload, error) { +func (c *EtcdClient) Get() (*remote.Payload, error) { resp, err := etcdapi.NewKeysAPI(c.Client).Get(context.Background(), c.Path, &etcdapi.GetOptions{Quorum: true}) if err != nil { if err, ok := err.(etcdapi.Error); ok && err.Code == etcdapi.ErrorCodeKeyNotFound { @@ -61,7 +29,7 @@ func (c *EtcdClient) Get() (*Payload, error) { data := []byte(resp.Node.Value) md5 := md5.Sum(data) - return &Payload{ + return &remote.Payload{ Data: data, MD5: md5[:], }, nil diff --git a/state/remote/etcd_test.go b/backend/remote-state/etcdv2/client_test.go similarity index 62% rename from state/remote/etcd_test.go rename to backend/remote-state/etcdv2/client_test.go index 6d06d801b..e37b5753b 100644 --- a/state/remote/etcd_test.go +++ b/backend/remote-state/etcdv2/client_test.go @@ -1,14 +1,17 @@ -package remote +package etcdv2 import ( "fmt" "os" "testing" "time" + + "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/state/remote" ) func TestEtcdClient_impl(t *testing.T) { - var _ Client = new(EtcdClient) + var _ remote.Client = new(EtcdClient) } func TestEtcdClient(t *testing.T) { @@ -17,7 +20,8 @@ func TestEtcdClient(t *testing.T) { t.Skipf("skipping; ETCD_ENDPOINT must be set") } - config := map[string]string{ + // Get the backend + config := map[string]interface{}{ "endpoints": endpoint, "path": fmt.Sprintf("tf-unit/%s", time.Now().String()), } @@ -29,10 +33,11 @@ func TestEtcdClient(t *testing.T) { config["password"] = password } - client, err := etcdFactory(config) + b := backend.TestBackendConfig(t, New(), config) + state, err := b.State(backend.DefaultStateName) if err != nil { t.Fatalf("Error for valid config: %s", err) } - testClient(t, client) + remote.TestClient(t, state.(*remote.State).Client) } diff --git a/state/remote/remote.go b/state/remote/remote.go index 33bdea559..3048dcf62 100644 --- a/state/remote/remote.go +++ b/state/remote/remote.go @@ -46,7 +46,6 @@ func NewClient(t string, conf map[string]string) (Client, error) { // NewClient. var BuiltinClients = map[string]Factory{ "artifactory": artifactoryFactory, - "etcd": etcdFactory, "http": httpFactory, "local": fileFactory, }