-
Notifications
You must be signed in to change notification settings - Fork 9.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
First steps towards an 'etcdv3' backend.
- Loading branch information
1 parent
94a1ca7
commit a3da2b0
Showing
45 changed files
with
30,166 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
package etcd | ||
|
||
import ( | ||
"context" | ||
|
||
etcdv3 "github.com/coreos/etcd/clientv3" | ||
"github.com/hashicorp/terraform/backend" | ||
"github.com/hashicorp/terraform/helper/schema" | ||
"strings" | ||
) | ||
|
||
func New() backend.Backend { | ||
s := &schema.Backend{ | ||
Schema: map[string]*schema.Schema{ | ||
"endpoints": &schema.Schema{ | ||
Type: schema.TypeString, | ||
Required: true, | ||
Description: "Comma-separated list of endpoints for the etcd cluster.", | ||
}, | ||
|
||
"username": &schema.Schema{ | ||
Type: schema.TypeString, | ||
Optional: true, | ||
Description: "Username used to connect to the etcd cluster.", | ||
Default: "", // To prevent input. | ||
}, | ||
|
||
"password": &schema.Schema{ | ||
Type: schema.TypeString, | ||
Optional: true, | ||
Description: "Password used to connect to the etcd cluster.", | ||
Default: "", // To prevent input. | ||
}, | ||
|
||
"prefix": &schema.Schema{ | ||
Type: schema.TypeString, | ||
Required: true, | ||
Description: "The prefix to use when storing state in etcd.", | ||
}, | ||
|
||
"lock": &schema.Schema{ | ||
Type: schema.TypeBool, | ||
Optional: true, | ||
Description: "Lock state access.", | ||
Default: true, | ||
}, | ||
}, | ||
} | ||
|
||
result := &Backend{Backend: s} | ||
result.Backend.ConfigureFunc = result.configure | ||
return result | ||
} | ||
|
||
type Backend struct { | ||
*schema.Backend | ||
|
||
// The fields below are set from configure. | ||
data *schema.ResourceData | ||
lock bool | ||
prefix string | ||
} | ||
|
||
func (b *Backend) configure(ctx context.Context) error { | ||
// Grab the resource data. | ||
b.data = schema.FromContextBackendConfig(ctx) | ||
// Store the lock information. | ||
b.lock = b.data.Get("lock").(bool) | ||
// Store the prefix information. | ||
b.prefix = b.data.Get("prefix").(string) | ||
// Initialize a client to test config. | ||
_, err := b.rawClient() | ||
// Return err, if any. | ||
return err | ||
} | ||
|
||
func (b *Backend) rawClient() (*etcdv3.Client, error) { | ||
config := etcdv3.Config{} | ||
|
||
if v, ok := b.data.GetOk("endpoints"); ok && v.(string) != "" { | ||
config.Endpoints = strings.Split(v.(string), ",") | ||
} | ||
if v, ok := b.data.GetOk("username"); ok && v.(string) != "" { | ||
config.Username = v.(string) | ||
} | ||
if v, ok := b.data.GetOk("password"); ok && v.(string) != "" { | ||
config.Password = v.(string) | ||
} | ||
|
||
return etcdv3.New(config) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
package etcd | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"strings" | ||
|
||
etcdv3 "github.com/coreos/etcd/clientv3" | ||
"github.com/hashicorp/terraform/backend" | ||
"github.com/hashicorp/terraform/state" | ||
"github.com/hashicorp/terraform/state/remote" | ||
"github.com/hashicorp/terraform/terraform" | ||
) | ||
|
||
const ( | ||
keyEnvPrefix = "-env:" | ||
) | ||
|
||
func (b *Backend) States() ([]string, error) { | ||
client, err := b.rawClient() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
prefix := b.determineKey("") | ||
res, err := client.Get(context.TODO(), prefix, etcdv3.WithPrefix(), etcdv3.WithKeysOnly()) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
result := make([]string, 1, len(res.Kvs)+1) | ||
result[0] = backend.DefaultStateName | ||
for _, kv := range res.Kvs { | ||
result = append(result, strings.TrimPrefix(string(kv.Key), prefix)) | ||
} | ||
|
||
return result, nil | ||
} | ||
|
||
func (b *Backend) DeleteState(name string) error { | ||
if name == backend.DefaultStateName || name == "" { | ||
return fmt.Errorf("Can't delete default state.") | ||
} | ||
|
||
client, err := b.rawClient() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
path := b.determineKey(name) | ||
|
||
_, err = client.Delete(context.TODO(), path) | ||
return err | ||
} | ||
|
||
func (b *Backend) State(name string) (state.State, error) { | ||
client, err := b.rawClient() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
var stateMgr state.State = &remote.State{ | ||
Client: &RemoteClient{ | ||
Client: client, | ||
DoLock: b.lock, | ||
Key: b.determineKey(name), | ||
}, | ||
} | ||
|
||
if !b.lock { | ||
stateMgr = &state.LockDisabled{Inner: stateMgr} | ||
} | ||
|
||
lockInfo := state.NewLockInfo() | ||
lockInfo.Operation = "init" | ||
lockId, err := stateMgr.Lock(lockInfo) | ||
if err != nil { | ||
return nil, fmt.Errorf("Failed to lock state in etcd: %s.", err) | ||
} | ||
|
||
lockUnlock := func(parent error) error { | ||
if err := stateMgr.Unlock(lockId); err != nil { | ||
return fmt.Errorf(strings.TrimSpace(errStateUnlock), lockId, err) | ||
} | ||
return parent | ||
} | ||
|
||
if err := stateMgr.RefreshState(); err != nil { | ||
err = lockUnlock(err) | ||
return nil, err | ||
} | ||
|
||
if v := stateMgr.State(); v == nil { | ||
if err := stateMgr.WriteState(terraform.NewState()); err != nil { | ||
err = lockUnlock(err) | ||
return nil, err | ||
} | ||
if err := stateMgr.PersistState(); err != nil { | ||
err = lockUnlock(err) | ||
return nil, err | ||
} | ||
} | ||
|
||
if err := lockUnlock(nil); err != nil { | ||
return nil, err | ||
} | ||
|
||
return stateMgr, nil | ||
} | ||
|
||
func (b *Backend) determineKey(name string) string { | ||
prefix := b.prefix | ||
if name != backend.DefaultStateName { | ||
prefix += fmt.Sprintf("%s%s", keyEnvPrefix, name) | ||
} | ||
return prefix | ||
} | ||
|
||
const errStateUnlock = ` | ||
Error unlocking etcd state. Lock ID: %s | ||
Error: %s | ||
You may have to force-unlock this state in order to use it again. | ||
` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
package etcd | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"os" | ||
"strings" | ||
"testing" | ||
"time" | ||
|
||
etcdv3 "github.com/coreos/etcd/clientv3" | ||
"github.com/hashicorp/terraform/backend" | ||
) | ||
|
||
const ( | ||
keyPrefix = "tf-unit" | ||
) | ||
|
||
func TestBackend_impl(t *testing.T) { | ||
var _ backend.Backend = new(Backend) | ||
} | ||
|
||
func prepareEtcdv3(t *testing.T) { | ||
skip := os.Getenv("TF_ACC") == "" && os.Getenv("TF_ETCDV3_TEST") == "" | ||
if skip { | ||
t.Log("etcd server tests require setting TF_ACC or TF_ETCDV3_TEST") | ||
t.Skip() | ||
} | ||
|
||
client, err := etcdv3.New(etcdv3.Config{ | ||
Endpoints: strings.Split(os.Getenv("TF_ETCDV3_ENDPOINTS"), ","), | ||
}) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
res, err := client.KV.Delete(context.TODO(), keyPrefix, etcdv3.WithPrefix()) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
t.Logf("Cleaned up %d keys.", res.Deleted) | ||
} | ||
|
||
func TestBackend(t *testing.T) { | ||
prepareEtcdv3(t) | ||
|
||
path := fmt.Sprintf("tf-unit/%s", time.Now().String()) | ||
|
||
// Get the backend. We need two to test locking. | ||
b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{ | ||
"endpoints": os.Getenv("TF_ETCDV3_ENDPOINTS"), | ||
"prefix": path, | ||
}) | ||
|
||
b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{ | ||
"endpoints": os.Getenv("TF_ETCDV3_ENDPOINTS"), | ||
"prefix": path, | ||
}) | ||
|
||
// Test | ||
backend.TestBackend(t, b1, b2) | ||
} | ||
|
||
func TestBackend_lockDisabled(t *testing.T) { | ||
prepareEtcdv3(t) | ||
|
||
key := fmt.Sprintf("%s/%s", keyPrefix, time.Now().String()) | ||
|
||
// Get the backend. We need two to test locking. | ||
b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{ | ||
"endpoints": os.Getenv("TF_ETCDV3_ENDPOINTS"), | ||
"lock": false, | ||
"prefix": key, | ||
}) | ||
|
||
b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{ | ||
"endpoints": os.Getenv("TF_ETCDV3_ENDPOINTS"), | ||
"lock": false, | ||
"prefix": key + "/" + "different", // Diff so locking test would fail if it was locking | ||
}) | ||
|
||
// Test | ||
backend.TestBackend(t, b1, b2) | ||
} |
Oops, something went wrong.