-
Notifications
You must be signed in to change notification settings - Fork 9.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
etcd v3 backend with lock support. #15680
Merged
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
52c97e9
First steps towards an 'etcdv3' backend.
bmcustodio 8afb824
Bump to 'etcd' v3.2.7.
bmcustodio fee5cab
Make 'gofmt' happy.
bmcustodio 70aad79
Make 'endpoints' a schema.TypeList.
bmcustodio b896348
Sort the result of 'Backend.States()'.
bmcustodio b8f4f6d
Delete lock info when unlocking.
bmcustodio bb4dec6
Make sure we don't relock.
bmcustodio 038f5eb
Remove 'lockSuffix' as not needed in 'etcd' v3.
bmcustodio 6570948
Fixing the test suite.
bmcustodio 3c21b9c
Get rid of 'keyEnvPrefix'.
bmcustodio 54dc50e
Cleanup 'etcd' after every test.
bmcustodio 8f7b315
Add documentation.
bmcustodio 6daf1d9
Allow for username and password to be read from envvars.
bmcustodio c8ff10f
Reuse 'etcd' client.
bmcustodio 524c393
Add TLS support.
bmcustodio 4d23195
Add a note about running the 'etcdv3' tests.
bmcustodio File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the changes in etcd means that there isn't going to be a direct upgrade from etcd2, you can probably get rid of the legacy default state path, and assume everything has a "workspace" and get rid of the conditionals around
keyEnvPrefix
and such.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jbardin I'm not sure I understand what you mean. 😶 Could you please clarify/exemplify?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I meant that the reason the other backends have the "default" state handled separately is solely for backwards compatibility with state files that existed before envs/workspaces.
So rather than dealing with the
keyEnvPrefix
you've adopted from the others, you could use the same hierarchy in all cases ofprefix/name
, and just include "default" in there.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done @ 3c21b9c. Thank you for your guidance. 🙂