-
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.
Merge pull request #15680 from brunomcustodio/etcdv3-backend
[WIP] etcd v3 backend with lock support.
- Loading branch information
Showing
62 changed files
with
31,974 additions
and
1 deletion.
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
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,157 @@ | ||
package etcd | ||
|
||
import ( | ||
"context" | ||
|
||
etcdv3 "github.com/coreos/etcd/clientv3" | ||
"github.com/coreos/etcd/pkg/transport" | ||
"github.com/hashicorp/terraform/backend" | ||
"github.com/hashicorp/terraform/helper/schema" | ||
) | ||
|
||
const ( | ||
endpointsKey = "endpoints" | ||
usernameKey = "username" | ||
usernameEnvVarName = "ETCDV3_USERNAME" | ||
passwordKey = "password" | ||
passwordEnvVarName = "ETCDV3_PASSWORD" | ||
prefixKey = "prefix" | ||
lockKey = "lock" | ||
cacertPathKey = "cacert_path" | ||
certPathKey = "cert_path" | ||
keyPathKey = "key_path" | ||
) | ||
|
||
func New() backend.Backend { | ||
s := &schema.Backend{ | ||
Schema: map[string]*schema.Schema{ | ||
endpointsKey: &schema.Schema{ | ||
Type: schema.TypeList, | ||
Elem: &schema.Schema{ | ||
Type: schema.TypeString, | ||
}, | ||
MinItems: 1, | ||
Required: true, | ||
Description: "Endpoints for the etcd cluster.", | ||
}, | ||
|
||
usernameKey: &schema.Schema{ | ||
Type: schema.TypeString, | ||
Optional: true, | ||
Description: "Username used to connect to the etcd cluster.", | ||
DefaultFunc: schema.EnvDefaultFunc(usernameEnvVarName, ""), | ||
}, | ||
|
||
passwordKey: &schema.Schema{ | ||
Type: schema.TypeString, | ||
Optional: true, | ||
Description: "Password used to connect to the etcd cluster.", | ||
DefaultFunc: schema.EnvDefaultFunc(passwordEnvVarName, ""), | ||
}, | ||
|
||
prefixKey: &schema.Schema{ | ||
Type: schema.TypeString, | ||
Optional: true, | ||
Description: "An optional prefix to be added to keys when to storing state in etcd.", | ||
Default: "", | ||
}, | ||
|
||
lockKey: &schema.Schema{ | ||
Type: schema.TypeBool, | ||
Optional: true, | ||
Description: "Whether to lock state access.", | ||
Default: true, | ||
}, | ||
|
||
cacertPathKey: &schema.Schema{ | ||
Type: schema.TypeString, | ||
Optional: true, | ||
Description: "The path to a PEM-encoded CA bundle with which to verify certificates of TLS-enabled etcd servers.", | ||
Default: "", | ||
}, | ||
|
||
certPathKey: &schema.Schema{ | ||
Type: schema.TypeString, | ||
Optional: true, | ||
Description: "The path to a PEM-encoded certificate to provide to etcd for secure client identification.", | ||
Default: "", | ||
}, | ||
|
||
keyPathKey: &schema.Schema{ | ||
Type: schema.TypeString, | ||
Optional: true, | ||
Description: "The path to a PEM-encoded key to provide to etcd for secure client identification.", | ||
Default: "", | ||
}, | ||
}, | ||
} | ||
|
||
result := &Backend{Backend: s} | ||
result.Backend.ConfigureFunc = result.configure | ||
return result | ||
} | ||
|
||
type Backend struct { | ||
*schema.Backend | ||
|
||
// The fields below are set from configure. | ||
client *etcdv3.Client | ||
data *schema.ResourceData | ||
lock bool | ||
prefix string | ||
} | ||
|
||
func (b *Backend) configure(ctx context.Context) error { | ||
var err error | ||
// Grab the resource data. | ||
b.data = schema.FromContextBackendConfig(ctx) | ||
// Store the lock information. | ||
b.lock = b.data.Get(lockKey).(bool) | ||
// Store the prefix information. | ||
b.prefix = b.data.Get(prefixKey).(string) | ||
// Initialize a client to test config. | ||
b.client, err = b.rawClient() | ||
// Return err, if any. | ||
return err | ||
} | ||
|
||
func (b *Backend) rawClient() (*etcdv3.Client, error) { | ||
config := etcdv3.Config{} | ||
tlsInfo := transport.TLSInfo{} | ||
|
||
if v, ok := b.data.GetOk(endpointsKey); ok { | ||
config.Endpoints = retrieveEndpoints(v) | ||
} | ||
if v, ok := b.data.GetOk(usernameKey); ok && v.(string) != "" { | ||
config.Username = v.(string) | ||
} | ||
if v, ok := b.data.GetOk(passwordKey); ok && v.(string) != "" { | ||
config.Password = v.(string) | ||
} | ||
if v, ok := b.data.GetOk(cacertPathKey); ok && v.(string) != "" { | ||
tlsInfo.TrustedCAFile = v.(string) | ||
} | ||
if v, ok := b.data.GetOk(certPathKey); ok && v.(string) != "" { | ||
tlsInfo.CertFile = v.(string) | ||
} | ||
if v, ok := b.data.GetOk(keyPathKey); ok && v.(string) != "" { | ||
tlsInfo.KeyFile = v.(string) | ||
} | ||
|
||
if tlsCfg, err := tlsInfo.ClientConfig(); err != nil { | ||
return nil, err | ||
} else if !tlsInfo.Empty() { | ||
config.TLS = tlsCfg // Assign TLS configuration only if it valid and non-empty. | ||
} | ||
|
||
return etcdv3.New(config) | ||
} | ||
|
||
func retrieveEndpoints(v interface{}) []string { | ||
var endpoints []string | ||
list := v.([]interface{}) | ||
for _, ep := range list { | ||
endpoints = append(endpoints, ep.(string)) | ||
} | ||
return endpoints | ||
} |
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,103 @@ | ||
package etcd | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"sort" | ||
"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" | ||
) | ||
|
||
func (b *Backend) States() ([]string, error) { | ||
res, err := b.client.Get(context.TODO(), b.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), b.prefix)) | ||
} | ||
sort.Strings(result[1:]) | ||
|
||
return result, nil | ||
} | ||
|
||
func (b *Backend) DeleteState(name string) error { | ||
if name == backend.DefaultStateName || name == "" { | ||
return fmt.Errorf("Can't delete default state.") | ||
} | ||
|
||
key := b.determineKey(name) | ||
|
||
_, err := b.client.Delete(context.TODO(), key) | ||
return err | ||
} | ||
|
||
func (b *Backend) State(name string) (state.State, error) { | ||
var stateMgr state.State = &remote.State{ | ||
Client: &RemoteClient{ | ||
Client: b.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 { | ||
return b.prefix + name | ||
} | ||
|
||
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,93 @@ | ||
package etcd | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"os" | ||
"strings" | ||
"testing" | ||
"time" | ||
|
||
etcdv3 "github.com/coreos/etcd/clientv3" | ||
"github.com/hashicorp/terraform/backend" | ||
) | ||
|
||
var ( | ||
etcdv3Endpoints = strings.Split(os.Getenv("TF_ETCDV3_ENDPOINTS"), ",") | ||
) | ||
|
||
const ( | ||
keyPrefix = "tf-unit" | ||
) | ||
|
||
func TestBackend_impl(t *testing.T) { | ||
var _ backend.Backend = new(Backend) | ||
} | ||
|
||
func cleanupEtcdv3(t *testing.T) { | ||
client, err := etcdv3.New(etcdv3.Config{ | ||
Endpoints: etcdv3Endpoints, | ||
}) | ||
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 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() | ||
} | ||
cleanupEtcdv3(t) | ||
} | ||
|
||
func TestBackend(t *testing.T) { | ||
prepareEtcdv3(t) | ||
defer cleanupEtcdv3(t) | ||
|
||
prefix := fmt.Sprintf("%s/%s/", keyPrefix, time.Now().Format(time.RFC3339)) | ||
|
||
// Get the backend. We need two to test locking. | ||
b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{ | ||
"endpoints": etcdv3Endpoints, | ||
"prefix": prefix, | ||
}) | ||
|
||
b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{ | ||
"endpoints": etcdv3Endpoints, | ||
"prefix": prefix, | ||
}) | ||
|
||
// Test | ||
backend.TestBackend(t, b1, b2) | ||
} | ||
|
||
func TestBackend_lockDisabled(t *testing.T) { | ||
prepareEtcdv3(t) | ||
defer cleanupEtcdv3(t) | ||
|
||
prefix := fmt.Sprintf("%s/%s/", keyPrefix, time.Now().Format(time.RFC3339)) | ||
|
||
// Get the backend. We need two to test locking. | ||
b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{ | ||
"endpoints": etcdv3Endpoints, | ||
"prefix": prefix, | ||
"lock": false, | ||
}) | ||
|
||
b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{ | ||
"endpoints": etcdv3Endpoints, | ||
"prefix": prefix + "/" + "different", // Diff so locking test would fail if it was locking | ||
"lock": false, | ||
}) | ||
|
||
// Test | ||
backend.TestBackend(t, b1, b2) | ||
} |
Oops, something went wrong.