First steps towards an 'etcdv3' backend.
This commit is contained in:
parent
1f63a96b10
commit
52c97e9fc9
|
@ -13,6 +13,7 @@ import (
|
||||||
backendlocal "github.com/hashicorp/terraform/backend/local"
|
backendlocal "github.com/hashicorp/terraform/backend/local"
|
||||||
backendAzure "github.com/hashicorp/terraform/backend/remote-state/azure"
|
backendAzure "github.com/hashicorp/terraform/backend/remote-state/azure"
|
||||||
backendconsul "github.com/hashicorp/terraform/backend/remote-state/consul"
|
backendconsul "github.com/hashicorp/terraform/backend/remote-state/consul"
|
||||||
|
backendetcdv3 "github.com/hashicorp/terraform/backend/remote-state/etcdv3"
|
||||||
backendinmem "github.com/hashicorp/terraform/backend/remote-state/inmem"
|
backendinmem "github.com/hashicorp/terraform/backend/remote-state/inmem"
|
||||||
backendS3 "github.com/hashicorp/terraform/backend/remote-state/s3"
|
backendS3 "github.com/hashicorp/terraform/backend/remote-state/s3"
|
||||||
backendSwift "github.com/hashicorp/terraform/backend/remote-state/swift"
|
backendSwift "github.com/hashicorp/terraform/backend/remote-state/swift"
|
||||||
|
@ -45,6 +46,7 @@ func init() {
|
||||||
"azure": deprecateBackend(backendAzure.New(),
|
"azure": deprecateBackend(backendAzure.New(),
|
||||||
`Warning: "azure" name is deprecated, please use "azurerm"`),
|
`Warning: "azure" name is deprecated, please use "azurerm"`),
|
||||||
"azurerm": func() backend.Backend { return backendAzure.New() },
|
"azurerm": func() backend.Backend { return backendAzure.New() },
|
||||||
|
"etcdv3": func() backend.Backend { return backendetcdv3.New() },
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the legacy remote backends that haven't yet been convertd to
|
// Add the legacy remote backends that haven't yet been convertd to
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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.
|
||||||
|
`
|
|
@ -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)
|
||||||
|
}
|
|
@ -0,0 +1,195 @@
|
||||||
|
package etcd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
etcdv3 "github.com/coreos/etcd/clientv3"
|
||||||
|
etcdv3sync "github.com/coreos/etcd/clientv3/concurrency"
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
"github.com/hashicorp/terraform/state"
|
||||||
|
"github.com/hashicorp/terraform/state/remote"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
lockAcquireTimeout = 2 * time.Second
|
||||||
|
lockInfoSuffix = ".lockinfo"
|
||||||
|
lockSuffix = ".lock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RemoteClient is a remote client that will store data in etcd.
|
||||||
|
type RemoteClient struct {
|
||||||
|
Client *etcdv3.Client
|
||||||
|
DoLock bool
|
||||||
|
Key string
|
||||||
|
|
||||||
|
etcdMutex *etcdv3sync.Mutex
|
||||||
|
etcdSession *etcdv3sync.Session
|
||||||
|
info *state.LockInfo
|
||||||
|
mu sync.Mutex
|
||||||
|
modRevision int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RemoteClient) Get() (*remote.Payload, error) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
res, err := c.Client.KV.Get(context.TODO(), c.Key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if res.Count == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if res.Count >= 2 {
|
||||||
|
return nil, fmt.Errorf("Expected a single result but got %d.", res.Count)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.modRevision = res.Kvs[0].ModRevision
|
||||||
|
|
||||||
|
payload := res.Kvs[0].Value
|
||||||
|
md5 := md5.Sum(payload)
|
||||||
|
|
||||||
|
return &remote.Payload{
|
||||||
|
Data: payload,
|
||||||
|
MD5: md5[:],
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RemoteClient) Put(data []byte) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
res, err := etcdv3.NewKV(c.Client).Txn(context.TODO()).If(
|
||||||
|
etcdv3.Compare(etcdv3.ModRevision(c.Key), "=", c.modRevision),
|
||||||
|
).Then(
|
||||||
|
etcdv3.OpPut(c.Key, string(data)),
|
||||||
|
etcdv3.OpGet(c.Key),
|
||||||
|
).Commit()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !res.Succeeded {
|
||||||
|
return fmt.Errorf("The transaction did not succeed.")
|
||||||
|
}
|
||||||
|
if len(res.Responses) != 2 {
|
||||||
|
return fmt.Errorf("Expected two responses but got %d.", len(res.Responses))
|
||||||
|
}
|
||||||
|
|
||||||
|
c.modRevision = res.Responses[1].GetResponseRange().Kvs[0].ModRevision
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RemoteClient) Delete() error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
_, err := c.Client.KV.Delete(context.TODO(), c.Key)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RemoteClient) Lock(info *state.LockInfo) (string, error) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
if !c.DoLock {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c.info = info
|
||||||
|
return c.lock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RemoteClient) Unlock(id string) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
if !c.DoLock {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.unlock(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RemoteClient) getLockInfo() (*state.LockInfo, error) {
|
||||||
|
res, err := c.Client.KV.Get(context.TODO(), c.Key+lockInfoSuffix)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if res.Count == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
li := &state.LockInfo{}
|
||||||
|
err = json.Unmarshal(res.Kvs[0].Value, li)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Error unmarshaling lock info: %s.", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return li, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RemoteClient) putLockInfo(info *state.LockInfo) error {
|
||||||
|
c.info.Path = c.Key
|
||||||
|
c.info.Created = time.Now().UTC()
|
||||||
|
|
||||||
|
_, err := c.Client.KV.Put(context.TODO(), c.Key+lockInfoSuffix, string(c.info.Marshal()))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RemoteClient) lock() (string, error) {
|
||||||
|
session, err := etcdv3sync.NewSession(c.Client)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.TODO(), lockAcquireTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
mutex := etcdv3sync.NewMutex(session, c.Key+lockSuffix)
|
||||||
|
if err1 := mutex.Lock(ctx); err1 != nil {
|
||||||
|
lockInfo, err2 := c.getLockInfo()
|
||||||
|
if err2 != nil {
|
||||||
|
return "", &state.LockError{Err: err2}
|
||||||
|
}
|
||||||
|
return "", &state.LockError{Info: lockInfo, Err: err1}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.etcdMutex = mutex
|
||||||
|
c.etcdSession = session
|
||||||
|
|
||||||
|
err = c.putLockInfo(c.info)
|
||||||
|
if err != nil {
|
||||||
|
if unlockErr := c.unlock(c.info.ID); unlockErr != nil {
|
||||||
|
err = multierror.Append(err, unlockErr)
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.info.ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RemoteClient) unlock(id string) error {
|
||||||
|
if c.etcdMutex == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var errs error
|
||||||
|
|
||||||
|
if err := c.etcdMutex.Unlock(context.TODO()); err != nil {
|
||||||
|
errs = multierror.Append(errs, err)
|
||||||
|
}
|
||||||
|
if err := c.etcdSession.Close(); err != nil {
|
||||||
|
errs = multierror.Append(errs, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.etcdMutex = nil
|
||||||
|
c.etcdSession = nil
|
||||||
|
|
||||||
|
return errs
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
package etcd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/backend"
|
||||||
|
"github.com/hashicorp/terraform/state"
|
||||||
|
"github.com/hashicorp/terraform/state/remote"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRemoteClient_impl(t *testing.T) {
|
||||||
|
var _ remote.Client = new(RemoteClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoteClient(t *testing.T) {
|
||||||
|
prepareEtcdv3(t)
|
||||||
|
|
||||||
|
// Get the backend
|
||||||
|
b := backend.TestBackendConfig(t, New(), map[string]interface{}{
|
||||||
|
"endpoints": os.Getenv("TF_ETCDV3_ENDPOINTS"),
|
||||||
|
"prefix": fmt.Sprintf("%s/%s", keyPrefix, time.Now().String()),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Grab the client
|
||||||
|
state, err := b.State(backend.DefaultStateName)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error: %s.", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test
|
||||||
|
remote.TestClient(t, state.(*remote.State).Client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEtcdv3_stateLock(t *testing.T) {
|
||||||
|
prepareEtcdv3(t)
|
||||||
|
|
||||||
|
key := fmt.Sprintf("tf-unit/%s", time.Now().String())
|
||||||
|
|
||||||
|
// Get the backend
|
||||||
|
s1, err := backend.TestBackendConfig(t, New(), map[string]interface{}{
|
||||||
|
"endpoints": os.Getenv("TF_ETCDV3_ENDPOINTS"),
|
||||||
|
"prefix": key,
|
||||||
|
}).State(backend.DefaultStateName)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s2, err := backend.TestBackendConfig(t, New(), map[string]interface{}{
|
||||||
|
"endpoints": os.Getenv("TF_ETCDV3_ENDPOINTS"),
|
||||||
|
"prefix": key,
|
||||||
|
}).State(backend.DefaultStateName)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
remote.TestRemoteLocks(t, s1.(*remote.State).Client, s2.(*remote.State).Client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEtcdv3_destroyLock(t *testing.T) {
|
||||||
|
prepareEtcdv3(t)
|
||||||
|
|
||||||
|
// Get the backend
|
||||||
|
b := backend.TestBackendConfig(t, New(), map[string]interface{}{
|
||||||
|
"endpoints": os.Getenv("TF_ETCDV3_ENDPOINTS"),
|
||||||
|
"prefix": fmt.Sprintf("tf-unit/%s", time.Now().String()),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Grab the client
|
||||||
|
s, err := b.State(backend.DefaultStateName)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := s.(*remote.State).Client.(*RemoteClient)
|
||||||
|
|
||||||
|
info := state.NewLockInfo()
|
||||||
|
id, err := c.Lock(info)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lockPath := c.Key + lockSuffix
|
||||||
|
|
||||||
|
if err := c.Unlock(id); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := c.Client.KV.Get(context.TODO(), lockPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if res.Count != 0 {
|
||||||
|
t.Fatalf("lock key not cleaned up at: %s", string(res.Kvs[0].Key))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,824 @@
|
||||||
|
// Code generated by protoc-gen-gogo.
|
||||||
|
// source: auth.proto
|
||||||
|
// DO NOT EDIT!
|
||||||
|
|
||||||
|
/*
|
||||||
|
Package authpb is a generated protocol buffer package.
|
||||||
|
|
||||||
|
It is generated from these files:
|
||||||
|
auth.proto
|
||||||
|
|
||||||
|
It has these top-level messages:
|
||||||
|
User
|
||||||
|
Permission
|
||||||
|
Role
|
||||||
|
*/
|
||||||
|
package authpb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
proto "github.com/golang/protobuf/proto"
|
||||||
|
|
||||||
|
math "math"
|
||||||
|
|
||||||
|
io "io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reference imports to suppress errors if they are not otherwise used.
|
||||||
|
var _ = proto.Marshal
|
||||||
|
var _ = fmt.Errorf
|
||||||
|
var _ = math.Inf
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file
|
||||||
|
// is compatible with the proto package it is being compiled against.
|
||||||
|
// A compilation error at this line likely means your copy of the
|
||||||
|
// proto package needs to be updated.
|
||||||
|
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
|
||||||
|
|
||||||
|
type Permission_Type int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
READ Permission_Type = 0
|
||||||
|
WRITE Permission_Type = 1
|
||||||
|
READWRITE Permission_Type = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
var Permission_Type_name = map[int32]string{
|
||||||
|
0: "READ",
|
||||||
|
1: "WRITE",
|
||||||
|
2: "READWRITE",
|
||||||
|
}
|
||||||
|
var Permission_Type_value = map[string]int32{
|
||||||
|
"READ": 0,
|
||||||
|
"WRITE": 1,
|
||||||
|
"READWRITE": 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x Permission_Type) String() string {
|
||||||
|
return proto.EnumName(Permission_Type_name, int32(x))
|
||||||
|
}
|
||||||
|
func (Permission_Type) EnumDescriptor() ([]byte, []int) { return fileDescriptorAuth, []int{1, 0} }
|
||||||
|
|
||||||
|
// User is a single entry in the bucket authUsers
|
||||||
|
type User struct {
|
||||||
|
Name []byte `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
|
Password []byte `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"`
|
||||||
|
Roles []string `protobuf:"bytes,3,rep,name=roles" json:"roles,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *User) Reset() { *m = User{} }
|
||||||
|
func (m *User) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*User) ProtoMessage() {}
|
||||||
|
func (*User) Descriptor() ([]byte, []int) { return fileDescriptorAuth, []int{0} }
|
||||||
|
|
||||||
|
// Permission is a single entity
|
||||||
|
type Permission struct {
|
||||||
|
PermType Permission_Type `protobuf:"varint,1,opt,name=permType,proto3,enum=authpb.Permission_Type" json:"permType,omitempty"`
|
||||||
|
Key []byte `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"`
|
||||||
|
RangeEnd []byte `protobuf:"bytes,3,opt,name=range_end,json=rangeEnd,proto3" json:"range_end,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Permission) Reset() { *m = Permission{} }
|
||||||
|
func (m *Permission) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*Permission) ProtoMessage() {}
|
||||||
|
func (*Permission) Descriptor() ([]byte, []int) { return fileDescriptorAuth, []int{1} }
|
||||||
|
|
||||||
|
// Role is a single entry in the bucket authRoles
|
||||||
|
type Role struct {
|
||||||
|
Name []byte `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
|
KeyPermission []*Permission `protobuf:"bytes,2,rep,name=keyPermission" json:"keyPermission,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Role) Reset() { *m = Role{} }
|
||||||
|
func (m *Role) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*Role) ProtoMessage() {}
|
||||||
|
func (*Role) Descriptor() ([]byte, []int) { return fileDescriptorAuth, []int{2} }
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
proto.RegisterType((*User)(nil), "authpb.User")
|
||||||
|
proto.RegisterType((*Permission)(nil), "authpb.Permission")
|
||||||
|
proto.RegisterType((*Role)(nil), "authpb.Role")
|
||||||
|
proto.RegisterEnum("authpb.Permission_Type", Permission_Type_name, Permission_Type_value)
|
||||||
|
}
|
||||||
|
func (m *User) Marshal() (dAtA []byte, err error) {
|
||||||
|
size := m.Size()
|
||||||
|
dAtA = make([]byte, size)
|
||||||
|
n, err := m.MarshalTo(dAtA)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return dAtA[:n], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *User) MarshalTo(dAtA []byte) (int, error) {
|
||||||
|
var i int
|
||||||
|
_ = i
|
||||||
|
var l int
|
||||||
|
_ = l
|
||||||
|
if len(m.Name) > 0 {
|
||||||
|
dAtA[i] = 0xa
|
||||||
|
i++
|
||||||
|
i = encodeVarintAuth(dAtA, i, uint64(len(m.Name)))
|
||||||
|
i += copy(dAtA[i:], m.Name)
|
||||||
|
}
|
||||||
|
if len(m.Password) > 0 {
|
||||||
|
dAtA[i] = 0x12
|
||||||
|
i++
|
||||||
|
i = encodeVarintAuth(dAtA, i, uint64(len(m.Password)))
|
||||||
|
i += copy(dAtA[i:], m.Password)
|
||||||
|
}
|
||||||
|
if len(m.Roles) > 0 {
|
||||||
|
for _, s := range m.Roles {
|
||||||
|
dAtA[i] = 0x1a
|
||||||
|
i++
|
||||||
|
l = len(s)
|
||||||
|
for l >= 1<<7 {
|
||||||
|
dAtA[i] = uint8(uint64(l)&0x7f | 0x80)
|
||||||
|
l >>= 7
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
dAtA[i] = uint8(l)
|
||||||
|
i++
|
||||||
|
i += copy(dAtA[i:], s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Permission) Marshal() (dAtA []byte, err error) {
|
||||||
|
size := m.Size()
|
||||||
|
dAtA = make([]byte, size)
|
||||||
|
n, err := m.MarshalTo(dAtA)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return dAtA[:n], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Permission) MarshalTo(dAtA []byte) (int, error) {
|
||||||
|
var i int
|
||||||
|
_ = i
|
||||||
|
var l int
|
||||||
|
_ = l
|
||||||
|
if m.PermType != 0 {
|
||||||
|
dAtA[i] = 0x8
|
||||||
|
i++
|
||||||
|
i = encodeVarintAuth(dAtA, i, uint64(m.PermType))
|
||||||
|
}
|
||||||
|
if len(m.Key) > 0 {
|
||||||
|
dAtA[i] = 0x12
|
||||||
|
i++
|
||||||
|
i = encodeVarintAuth(dAtA, i, uint64(len(m.Key)))
|
||||||
|
i += copy(dAtA[i:], m.Key)
|
||||||
|
}
|
||||||
|
if len(m.RangeEnd) > 0 {
|
||||||
|
dAtA[i] = 0x1a
|
||||||
|
i++
|
||||||
|
i = encodeVarintAuth(dAtA, i, uint64(len(m.RangeEnd)))
|
||||||
|
i += copy(dAtA[i:], m.RangeEnd)
|
||||||
|
}
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Role) Marshal() (dAtA []byte, err error) {
|
||||||
|
size := m.Size()
|
||||||
|
dAtA = make([]byte, size)
|
||||||
|
n, err := m.MarshalTo(dAtA)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return dAtA[:n], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Role) MarshalTo(dAtA []byte) (int, error) {
|
||||||
|
var i int
|
||||||
|
_ = i
|
||||||
|
var l int
|
||||||
|
_ = l
|
||||||
|
if len(m.Name) > 0 {
|
||||||
|
dAtA[i] = 0xa
|
||||||
|
i++
|
||||||
|
i = encodeVarintAuth(dAtA, i, uint64(len(m.Name)))
|
||||||
|
i += copy(dAtA[i:], m.Name)
|
||||||
|
}
|
||||||
|
if len(m.KeyPermission) > 0 {
|
||||||
|
for _, msg := range m.KeyPermission {
|
||||||
|
dAtA[i] = 0x12
|
||||||
|
i++
|
||||||
|
i = encodeVarintAuth(dAtA, i, uint64(msg.Size()))
|
||||||
|
n, err := msg.MarshalTo(dAtA[i:])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
i += n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeFixed64Auth(dAtA []byte, offset int, v uint64) int {
|
||||||
|
dAtA[offset] = uint8(v)
|
||||||
|
dAtA[offset+1] = uint8(v >> 8)
|
||||||
|
dAtA[offset+2] = uint8(v >> 16)
|
||||||
|
dAtA[offset+3] = uint8(v >> 24)
|
||||||
|
dAtA[offset+4] = uint8(v >> 32)
|
||||||
|
dAtA[offset+5] = uint8(v >> 40)
|
||||||
|
dAtA[offset+6] = uint8(v >> 48)
|
||||||
|
dAtA[offset+7] = uint8(v >> 56)
|
||||||
|
return offset + 8
|
||||||
|
}
|
||||||
|
func encodeFixed32Auth(dAtA []byte, offset int, v uint32) int {
|
||||||
|
dAtA[offset] = uint8(v)
|
||||||
|
dAtA[offset+1] = uint8(v >> 8)
|
||||||
|
dAtA[offset+2] = uint8(v >> 16)
|
||||||
|
dAtA[offset+3] = uint8(v >> 24)
|
||||||
|
return offset + 4
|
||||||
|
}
|
||||||
|
func encodeVarintAuth(dAtA []byte, offset int, v uint64) int {
|
||||||
|
for v >= 1<<7 {
|
||||||
|
dAtA[offset] = uint8(v&0x7f | 0x80)
|
||||||
|
v >>= 7
|
||||||
|
offset++
|
||||||
|
}
|
||||||
|
dAtA[offset] = uint8(v)
|
||||||
|
return offset + 1
|
||||||
|
}
|
||||||
|
func (m *User) Size() (n int) {
|
||||||
|
var l int
|
||||||
|
_ = l
|
||||||
|
l = len(m.Name)
|
||||||
|
if l > 0 {
|
||||||
|
n += 1 + l + sovAuth(uint64(l))
|
||||||
|
}
|
||||||
|
l = len(m.Password)
|
||||||
|
if l > 0 {
|
||||||
|
n += 1 + l + sovAuth(uint64(l))
|
||||||
|
}
|
||||||
|
if len(m.Roles) > 0 {
|
||||||
|
for _, s := range m.Roles {
|
||||||
|
l = len(s)
|
||||||
|
n += 1 + l + sovAuth(uint64(l))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Permission) Size() (n int) {
|
||||||
|
var l int
|
||||||
|
_ = l
|
||||||
|
if m.PermType != 0 {
|
||||||
|
n += 1 + sovAuth(uint64(m.PermType))
|
||||||
|
}
|
||||||
|
l = len(m.Key)
|
||||||
|
if l > 0 {
|
||||||
|
n += 1 + l + sovAuth(uint64(l))
|
||||||
|
}
|
||||||
|
l = len(m.RangeEnd)
|
||||||
|
if l > 0 {
|
||||||
|
n += 1 + l + sovAuth(uint64(l))
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Role) Size() (n int) {
|
||||||
|
var l int
|
||||||
|
_ = l
|
||||||
|
l = len(m.Name)
|
||||||
|
if l > 0 {
|
||||||
|
n += 1 + l + sovAuth(uint64(l))
|
||||||
|
}
|
||||||
|
if len(m.KeyPermission) > 0 {
|
||||||
|
for _, e := range m.KeyPermission {
|
||||||
|
l = e.Size()
|
||||||
|
n += 1 + l + sovAuth(uint64(l))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func sovAuth(x uint64) (n int) {
|
||||||
|
for {
|
||||||
|
n++
|
||||||
|
x >>= 7
|
||||||
|
if x == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
func sozAuth(x uint64) (n int) {
|
||||||
|
return sovAuth(uint64((x << 1) ^ uint64((int64(x) >> 63))))
|
||||||
|
}
|
||||||
|
func (m *User) Unmarshal(dAtA []byte) error {
|
||||||
|
l := len(dAtA)
|
||||||
|
iNdEx := 0
|
||||||
|
for iNdEx < l {
|
||||||
|
preIndex := iNdEx
|
||||||
|
var wire uint64
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return ErrIntOverflowAuth
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
wire |= (uint64(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fieldNum := int32(wire >> 3)
|
||||||
|
wireType := int(wire & 0x7)
|
||||||
|
if wireType == 4 {
|
||||||
|
return fmt.Errorf("proto: User: wiretype end group for non-group")
|
||||||
|
}
|
||||||
|
if fieldNum <= 0 {
|
||||||
|
return fmt.Errorf("proto: User: illegal tag %d (wire type %d)", fieldNum, wire)
|
||||||
|
}
|
||||||
|
switch fieldNum {
|
||||||
|
case 1:
|
||||||
|
if wireType != 2 {
|
||||||
|
return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType)
|
||||||
|
}
|
||||||
|
var byteLen int
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return ErrIntOverflowAuth
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
byteLen |= (int(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if byteLen < 0 {
|
||||||
|
return ErrInvalidLengthAuth
|
||||||
|
}
|
||||||
|
postIndex := iNdEx + byteLen
|
||||||
|
if postIndex > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
m.Name = append(m.Name[:0], dAtA[iNdEx:postIndex]...)
|
||||||
|
if m.Name == nil {
|
||||||
|
m.Name = []byte{}
|
||||||
|
}
|
||||||
|
iNdEx = postIndex
|
||||||
|
case 2:
|
||||||
|
if wireType != 2 {
|
||||||
|
return fmt.Errorf("proto: wrong wireType = %d for field Password", wireType)
|
||||||
|
}
|
||||||
|
var byteLen int
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return ErrIntOverflowAuth
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
byteLen |= (int(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if byteLen < 0 {
|
||||||
|
return ErrInvalidLengthAuth
|
||||||
|
}
|
||||||
|
postIndex := iNdEx + byteLen
|
||||||
|
if postIndex > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
m.Password = append(m.Password[:0], dAtA[iNdEx:postIndex]...)
|
||||||
|
if m.Password == nil {
|
||||||
|
m.Password = []byte{}
|
||||||
|
}
|
||||||
|
iNdEx = postIndex
|
||||||
|
case 3:
|
||||||
|
if wireType != 2 {
|
||||||
|
return fmt.Errorf("proto: wrong wireType = %d for field Roles", wireType)
|
||||||
|
}
|
||||||
|
var stringLen uint64
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return ErrIntOverflowAuth
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
stringLen |= (uint64(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
intStringLen := int(stringLen)
|
||||||
|
if intStringLen < 0 {
|
||||||
|
return ErrInvalidLengthAuth
|
||||||
|
}
|
||||||
|
postIndex := iNdEx + intStringLen
|
||||||
|
if postIndex > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
m.Roles = append(m.Roles, string(dAtA[iNdEx:postIndex]))
|
||||||
|
iNdEx = postIndex
|
||||||
|
default:
|
||||||
|
iNdEx = preIndex
|
||||||
|
skippy, err := skipAuth(dAtA[iNdEx:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if skippy < 0 {
|
||||||
|
return ErrInvalidLengthAuth
|
||||||
|
}
|
||||||
|
if (iNdEx + skippy) > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
iNdEx += skippy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if iNdEx > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *Permission) Unmarshal(dAtA []byte) error {
|
||||||
|
l := len(dAtA)
|
||||||
|
iNdEx := 0
|
||||||
|
for iNdEx < l {
|
||||||
|
preIndex := iNdEx
|
||||||
|
var wire uint64
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return ErrIntOverflowAuth
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
wire |= (uint64(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fieldNum := int32(wire >> 3)
|
||||||
|
wireType := int(wire & 0x7)
|
||||||
|
if wireType == 4 {
|
||||||
|
return fmt.Errorf("proto: Permission: wiretype end group for non-group")
|
||||||
|
}
|
||||||
|
if fieldNum <= 0 {
|
||||||
|
return fmt.Errorf("proto: Permission: illegal tag %d (wire type %d)", fieldNum, wire)
|
||||||
|
}
|
||||||
|
switch fieldNum {
|
||||||
|
case 1:
|
||||||
|
if wireType != 0 {
|
||||||
|
return fmt.Errorf("proto: wrong wireType = %d for field PermType", wireType)
|
||||||
|
}
|
||||||
|
m.PermType = 0
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return ErrIntOverflowAuth
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
m.PermType |= (Permission_Type(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 2:
|
||||||
|
if wireType != 2 {
|
||||||
|
return fmt.Errorf("proto: wrong wireType = %d for field Key", wireType)
|
||||||
|
}
|
||||||
|
var byteLen int
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return ErrIntOverflowAuth
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
byteLen |= (int(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if byteLen < 0 {
|
||||||
|
return ErrInvalidLengthAuth
|
||||||
|
}
|
||||||
|
postIndex := iNdEx + byteLen
|
||||||
|
if postIndex > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
m.Key = append(m.Key[:0], dAtA[iNdEx:postIndex]...)
|
||||||
|
if m.Key == nil {
|
||||||
|
m.Key = []byte{}
|
||||||
|
}
|
||||||
|
iNdEx = postIndex
|
||||||
|
case 3:
|
||||||
|
if wireType != 2 {
|
||||||
|
return fmt.Errorf("proto: wrong wireType = %d for field RangeEnd", wireType)
|
||||||
|
}
|
||||||
|
var byteLen int
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return ErrIntOverflowAuth
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
byteLen |= (int(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if byteLen < 0 {
|
||||||
|
return ErrInvalidLengthAuth
|
||||||
|
}
|
||||||
|
postIndex := iNdEx + byteLen
|
||||||
|
if postIndex > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
m.RangeEnd = append(m.RangeEnd[:0], dAtA[iNdEx:postIndex]...)
|
||||||
|
if m.RangeEnd == nil {
|
||||||
|
m.RangeEnd = []byte{}
|
||||||
|
}
|
||||||
|
iNdEx = postIndex
|
||||||
|
default:
|
||||||
|
iNdEx = preIndex
|
||||||
|
skippy, err := skipAuth(dAtA[iNdEx:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if skippy < 0 {
|
||||||
|
return ErrInvalidLengthAuth
|
||||||
|
}
|
||||||
|
if (iNdEx + skippy) > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
iNdEx += skippy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if iNdEx > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *Role) Unmarshal(dAtA []byte) error {
|
||||||
|
l := len(dAtA)
|
||||||
|
iNdEx := 0
|
||||||
|
for iNdEx < l {
|
||||||
|
preIndex := iNdEx
|
||||||
|
var wire uint64
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return ErrIntOverflowAuth
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
wire |= (uint64(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fieldNum := int32(wire >> 3)
|
||||||
|
wireType := int(wire & 0x7)
|
||||||
|
if wireType == 4 {
|
||||||
|
return fmt.Errorf("proto: Role: wiretype end group for non-group")
|
||||||
|
}
|
||||||
|
if fieldNum <= 0 {
|
||||||
|
return fmt.Errorf("proto: Role: illegal tag %d (wire type %d)", fieldNum, wire)
|
||||||
|
}
|
||||||
|
switch fieldNum {
|
||||||
|
case 1:
|
||||||
|
if wireType != 2 {
|
||||||
|
return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType)
|
||||||
|
}
|
||||||
|
var byteLen int
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return ErrIntOverflowAuth
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
byteLen |= (int(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if byteLen < 0 {
|
||||||
|
return ErrInvalidLengthAuth
|
||||||
|
}
|
||||||
|
postIndex := iNdEx + byteLen
|
||||||
|
if postIndex > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
m.Name = append(m.Name[:0], dAtA[iNdEx:postIndex]...)
|
||||||
|
if m.Name == nil {
|
||||||
|
m.Name = []byte{}
|
||||||
|
}
|
||||||
|
iNdEx = postIndex
|
||||||
|
case 2:
|
||||||
|
if wireType != 2 {
|
||||||
|
return fmt.Errorf("proto: wrong wireType = %d for field KeyPermission", wireType)
|
||||||
|
}
|
||||||
|
var msglen int
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return ErrIntOverflowAuth
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
msglen |= (int(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if msglen < 0 {
|
||||||
|
return ErrInvalidLengthAuth
|
||||||
|
}
|
||||||
|
postIndex := iNdEx + msglen
|
||||||
|
if postIndex > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
m.KeyPermission = append(m.KeyPermission, &Permission{})
|
||||||
|
if err := m.KeyPermission[len(m.KeyPermission)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
iNdEx = postIndex
|
||||||
|
default:
|
||||||
|
iNdEx = preIndex
|
||||||
|
skippy, err := skipAuth(dAtA[iNdEx:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if skippy < 0 {
|
||||||
|
return ErrInvalidLengthAuth
|
||||||
|
}
|
||||||
|
if (iNdEx + skippy) > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
iNdEx += skippy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if iNdEx > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func skipAuth(dAtA []byte) (n int, err error) {
|
||||||
|
l := len(dAtA)
|
||||||
|
iNdEx := 0
|
||||||
|
for iNdEx < l {
|
||||||
|
var wire uint64
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return 0, ErrIntOverflowAuth
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return 0, io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
wire |= (uint64(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wireType := int(wire & 0x7)
|
||||||
|
switch wireType {
|
||||||
|
case 0:
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return 0, ErrIntOverflowAuth
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return 0, io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
iNdEx++
|
||||||
|
if dAtA[iNdEx-1] < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return iNdEx, nil
|
||||||
|
case 1:
|
||||||
|
iNdEx += 8
|
||||||
|
return iNdEx, nil
|
||||||
|
case 2:
|
||||||
|
var length int
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return 0, ErrIntOverflowAuth
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return 0, io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
length |= (int(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iNdEx += length
|
||||||
|
if length < 0 {
|
||||||
|
return 0, ErrInvalidLengthAuth
|
||||||
|
}
|
||||||
|
return iNdEx, nil
|
||||||
|
case 3:
|
||||||
|
for {
|
||||||
|
var innerWire uint64
|
||||||
|
var start int = iNdEx
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return 0, ErrIntOverflowAuth
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return 0, io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
innerWire |= (uint64(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
innerWireType := int(innerWire & 0x7)
|
||||||
|
if innerWireType == 4 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
next, err := skipAuth(dAtA[start:])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
iNdEx = start + next
|
||||||
|
}
|
||||||
|
return iNdEx, nil
|
||||||
|
case 4:
|
||||||
|
return iNdEx, nil
|
||||||
|
case 5:
|
||||||
|
iNdEx += 4
|
||||||
|
return iNdEx, nil
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidLengthAuth = fmt.Errorf("proto: negative length found during unmarshaling")
|
||||||
|
ErrIntOverflowAuth = fmt.Errorf("proto: integer overflow")
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() { proto.RegisterFile("auth.proto", fileDescriptorAuth) }
|
||||||
|
|
||||||
|
var fileDescriptorAuth = []byte{
|
||||||
|
// 288 bytes of a gzipped FileDescriptorProto
|
||||||
|
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x90, 0xc1, 0x4a, 0xc3, 0x30,
|
||||||
|
0x1c, 0xc6, 0x9b, 0xb6, 0x1b, 0xed, 0x5f, 0x27, 0x25, 0x0c, 0x0c, 0x13, 0x42, 0xe9, 0xa9, 0x78,
|
||||||
|
0xa8, 0xb0, 0x5d, 0xbc, 0x2a, 0xf6, 0x20, 0x78, 0x90, 0x50, 0xf1, 0x28, 0x1d, 0x0d, 0x75, 0x6c,
|
||||||
|
0x6d, 0x4a, 0x32, 0x91, 0xbe, 0x89, 0x07, 0x1f, 0x68, 0xc7, 0x3d, 0x82, 0xab, 0x2f, 0x22, 0x4d,
|
||||||
|
0x64, 0x43, 0xdc, 0xed, 0xfb, 0xbe, 0xff, 0x97, 0xe4, 0x97, 0x3f, 0x40, 0xfe, 0xb6, 0x7e, 0x4d,
|
||||||
|
0x1a, 0x29, 0xd6, 0x02, 0x0f, 0x7b, 0xdd, 0xcc, 0x27, 0xe3, 0x52, 0x94, 0x42, 0x47, 0x57, 0xbd,
|
||||||
|
0x32, 0xd3, 0xe8, 0x01, 0xdc, 0x27, 0xc5, 0x25, 0xc6, 0xe0, 0xd6, 0x79, 0xc5, 0x09, 0x0a, 0x51,
|
||||||
|
0x7c, 0xca, 0xb4, 0xc6, 0x13, 0xf0, 0x9a, 0x5c, 0xa9, 0x77, 0x21, 0x0b, 0x62, 0xeb, 0x7c, 0xef,
|
||||||
|
0xf1, 0x18, 0x06, 0x52, 0xac, 0xb8, 0x22, 0x4e, 0xe8, 0xc4, 0x3e, 0x33, 0x26, 0xfa, 0x44, 0x00,
|
||||||
|
0x8f, 0x5c, 0x56, 0x0b, 0xa5, 0x16, 0xa2, 0xc6, 0x33, 0xf0, 0x1a, 0x2e, 0xab, 0xac, 0x6d, 0xcc,
|
||||||
|
0xc5, 0x67, 0xd3, 0xf3, 0xc4, 0xd0, 0x24, 0x87, 0x56, 0xd2, 0x8f, 0xd9, 0xbe, 0x88, 0x03, 0x70,
|
||||||
|
0x96, 0xbc, 0xfd, 0x7d, 0xb0, 0x97, 0xf8, 0x02, 0x7c, 0x99, 0xd7, 0x25, 0x7f, 0xe1, 0x75, 0x41,
|
||||||
|
0x1c, 0x03, 0xa2, 0x83, 0xb4, 0x2e, 0xa2, 0x4b, 0x70, 0xf5, 0x31, 0x0f, 0x5c, 0x96, 0xde, 0xdc,
|
||||||
|
0x05, 0x16, 0xf6, 0x61, 0xf0, 0xcc, 0xee, 0xb3, 0x34, 0x40, 0x78, 0x04, 0x7e, 0x1f, 0x1a, 0x6b,
|
||||||
|
0x47, 0x19, 0xb8, 0x4c, 0xac, 0xf8, 0xd1, 0xcf, 0x5e, 0xc3, 0x68, 0xc9, 0xdb, 0x03, 0x16, 0xb1,
|
||||||
|
0x43, 0x27, 0x3e, 0x99, 0xe2, 0xff, 0xc0, 0xec, 0x6f, 0xf1, 0x96, 0x6c, 0x76, 0xd4, 0xda, 0xee,
|
||||||
|
0xa8, 0xb5, 0xe9, 0x28, 0xda, 0x76, 0x14, 0x7d, 0x75, 0x14, 0x7d, 0x7c, 0x53, 0x6b, 0x3e, 0xd4,
|
||||||
|
0x3b, 0x9e, 0xfd, 0x04, 0x00, 0x00, 0xff, 0xff, 0xcc, 0x76, 0x8d, 0x4f, 0x8f, 0x01, 0x00, 0x00,
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
package authpb;
|
||||||
|
|
||||||
|
import "gogoproto/gogo.proto";
|
||||||
|
|
||||||
|
option (gogoproto.marshaler_all) = true;
|
||||||
|
option (gogoproto.sizer_all) = true;
|
||||||
|
option (gogoproto.unmarshaler_all) = true;
|
||||||
|
option (gogoproto.goproto_getters_all) = false;
|
||||||
|
option (gogoproto.goproto_enum_prefix_all) = false;
|
||||||
|
|
||||||
|
// User is a single entry in the bucket authUsers
|
||||||
|
message User {
|
||||||
|
bytes name = 1;
|
||||||
|
bytes password = 2;
|
||||||
|
repeated string roles = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permission is a single entity
|
||||||
|
message Permission {
|
||||||
|
enum Type {
|
||||||
|
READ = 0;
|
||||||
|
WRITE = 1;
|
||||||
|
READWRITE = 2;
|
||||||
|
}
|
||||||
|
Type permType = 1;
|
||||||
|
|
||||||
|
bytes key = 2;
|
||||||
|
bytes range_end = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Role is a single entry in the bucket authRoles
|
||||||
|
message Role {
|
||||||
|
bytes name = 1;
|
||||||
|
|
||||||
|
repeated Permission keyPermission = 2;
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
# etcd/clientv3
|
||||||
|
|
||||||
|
[![Godoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](https://godoc.org/github.com/coreos/etcd/clientv3)
|
||||||
|
|
||||||
|
`etcd/clientv3` is the official Go etcd client for v3.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get github.com/coreos/etcd/clientv3
|
||||||
|
```
|
||||||
|
|
||||||
|
## Get started
|
||||||
|
|
||||||
|
Create client using `clientv3.New`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
cli, err := clientv3.New(clientv3.Config{
|
||||||
|
Endpoints: []string{"localhost:2379", "localhost:22379", "localhost:32379"},
|
||||||
|
DialTimeout: 5 * time.Second,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
// handle error!
|
||||||
|
}
|
||||||
|
defer cli.Close()
|
||||||
|
```
|
||||||
|
|
||||||
|
etcd v3 uses [`gRPC`](http://www.grpc.io) for remote procedure calls. And `clientv3` uses
|
||||||
|
[`grpc-go`](https://github.com/grpc/grpc-go) to connect to etcd. Make sure to close the client after using it.
|
||||||
|
If the client is not closed, the connection will have leaky goroutines. To specify client request timeout,
|
||||||
|
pass `context.WithTimeout` to APIs:
|
||||||
|
|
||||||
|
```go
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
resp, err := cli.Put(ctx, "sample_key", "sample_value")
|
||||||
|
cancel()
|
||||||
|
if err != nil {
|
||||||
|
// handle error!
|
||||||
|
}
|
||||||
|
// use the response
|
||||||
|
```
|
||||||
|
|
||||||
|
etcd uses `cmd/vendor` directory to store external dependencies, which are
|
||||||
|
to be compiled into etcd release binaries. `client` can be imported without
|
||||||
|
vendoring. For full compatibility, it is recommended to vendor builds using
|
||||||
|
etcd's vendored packages, using tools like godep, as in
|
||||||
|
[vendor directories](https://golang.org/cmd/go/#hdr-Vendor_Directories).
|
||||||
|
For more detail, please read [Go vendor design](https://golang.org/s/go15vendor).
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
etcd client returns 2 types of errors:
|
||||||
|
|
||||||
|
1. context error: canceled or deadline exceeded.
|
||||||
|
2. gRPC error: see [api/v3rpc/rpctypes](https://godoc.org/github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes).
|
||||||
|
|
||||||
|
Here is the example code to handle client errors:
|
||||||
|
|
||||||
|
```go
|
||||||
|
resp, err := cli.Put(ctx, "", "")
|
||||||
|
if err != nil {
|
||||||
|
switch err {
|
||||||
|
case context.Canceled:
|
||||||
|
log.Fatalf("ctx is canceled by another routine: %v", err)
|
||||||
|
case context.DeadlineExceeded:
|
||||||
|
log.Fatalf("ctx is attached with a deadline is exceeded: %v", err)
|
||||||
|
case rpctypes.ErrEmptyKey:
|
||||||
|
log.Fatalf("client-side error: %v", err)
|
||||||
|
default:
|
||||||
|
log.Fatalf("bad cluster endpoints, which are not etcd servers: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Metrics
|
||||||
|
|
||||||
|
The etcd client optionally exposes RPC metrics through [go-grpc-prometheus](https://github.com/grpc-ecosystem/go-grpc-prometheus). See the [examples](https://github.com/coreos/etcd/blob/master/clientv3/example_metrics_test.go).
|
||||||
|
|
||||||
|
## Namespacing
|
||||||
|
|
||||||
|
The [namespace](https://godoc.org/github.com/coreos/etcd/clientv3/namespace) package provides `clientv3` interface wrappers to transparently isolate client requests to a user-defined prefix.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
More code examples can be found at [GoDoc](https://godoc.org/github.com/coreos/etcd/clientv3).
|
|
@ -0,0 +1,222 @@
|
||||||
|
// Copyright 2016 The etcd Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package clientv3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/coreos/etcd/auth/authpb"
|
||||||
|
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
AuthEnableResponse pb.AuthEnableResponse
|
||||||
|
AuthDisableResponse pb.AuthDisableResponse
|
||||||
|
AuthenticateResponse pb.AuthenticateResponse
|
||||||
|
AuthUserAddResponse pb.AuthUserAddResponse
|
||||||
|
AuthUserDeleteResponse pb.AuthUserDeleteResponse
|
||||||
|
AuthUserChangePasswordResponse pb.AuthUserChangePasswordResponse
|
||||||
|
AuthUserGrantRoleResponse pb.AuthUserGrantRoleResponse
|
||||||
|
AuthUserGetResponse pb.AuthUserGetResponse
|
||||||
|
AuthUserRevokeRoleResponse pb.AuthUserRevokeRoleResponse
|
||||||
|
AuthRoleAddResponse pb.AuthRoleAddResponse
|
||||||
|
AuthRoleGrantPermissionResponse pb.AuthRoleGrantPermissionResponse
|
||||||
|
AuthRoleGetResponse pb.AuthRoleGetResponse
|
||||||
|
AuthRoleRevokePermissionResponse pb.AuthRoleRevokePermissionResponse
|
||||||
|
AuthRoleDeleteResponse pb.AuthRoleDeleteResponse
|
||||||
|
AuthUserListResponse pb.AuthUserListResponse
|
||||||
|
AuthRoleListResponse pb.AuthRoleListResponse
|
||||||
|
|
||||||
|
PermissionType authpb.Permission_Type
|
||||||
|
Permission authpb.Permission
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PermRead = authpb.READ
|
||||||
|
PermWrite = authpb.WRITE
|
||||||
|
PermReadWrite = authpb.READWRITE
|
||||||
|
)
|
||||||
|
|
||||||
|
type Auth interface {
|
||||||
|
// AuthEnable enables auth of an etcd cluster.
|
||||||
|
AuthEnable(ctx context.Context) (*AuthEnableResponse, error)
|
||||||
|
|
||||||
|
// AuthDisable disables auth of an etcd cluster.
|
||||||
|
AuthDisable(ctx context.Context) (*AuthDisableResponse, error)
|
||||||
|
|
||||||
|
// UserAdd adds a new user to an etcd cluster.
|
||||||
|
UserAdd(ctx context.Context, name string, password string) (*AuthUserAddResponse, error)
|
||||||
|
|
||||||
|
// UserDelete deletes a user from an etcd cluster.
|
||||||
|
UserDelete(ctx context.Context, name string) (*AuthUserDeleteResponse, error)
|
||||||
|
|
||||||
|
// UserChangePassword changes a password of a user.
|
||||||
|
UserChangePassword(ctx context.Context, name string, password string) (*AuthUserChangePasswordResponse, error)
|
||||||
|
|
||||||
|
// UserGrantRole grants a role to a user.
|
||||||
|
UserGrantRole(ctx context.Context, user string, role string) (*AuthUserGrantRoleResponse, error)
|
||||||
|
|
||||||
|
// UserGet gets a detailed information of a user.
|
||||||
|
UserGet(ctx context.Context, name string) (*AuthUserGetResponse, error)
|
||||||
|
|
||||||
|
// UserList gets a list of all users.
|
||||||
|
UserList(ctx context.Context) (*AuthUserListResponse, error)
|
||||||
|
|
||||||
|
// UserRevokeRole revokes a role of a user.
|
||||||
|
UserRevokeRole(ctx context.Context, name string, role string) (*AuthUserRevokeRoleResponse, error)
|
||||||
|
|
||||||
|
// RoleAdd adds a new role to an etcd cluster.
|
||||||
|
RoleAdd(ctx context.Context, name string) (*AuthRoleAddResponse, error)
|
||||||
|
|
||||||
|
// RoleGrantPermission grants a permission to a role.
|
||||||
|
RoleGrantPermission(ctx context.Context, name string, key, rangeEnd string, permType PermissionType) (*AuthRoleGrantPermissionResponse, error)
|
||||||
|
|
||||||
|
// RoleGet gets a detailed information of a role.
|
||||||
|
RoleGet(ctx context.Context, role string) (*AuthRoleGetResponse, error)
|
||||||
|
|
||||||
|
// RoleList gets a list of all roles.
|
||||||
|
RoleList(ctx context.Context) (*AuthRoleListResponse, error)
|
||||||
|
|
||||||
|
// RoleRevokePermission revokes a permission from a role.
|
||||||
|
RoleRevokePermission(ctx context.Context, role string, key, rangeEnd string) (*AuthRoleRevokePermissionResponse, error)
|
||||||
|
|
||||||
|
// RoleDelete deletes a role.
|
||||||
|
RoleDelete(ctx context.Context, role string) (*AuthRoleDeleteResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type auth struct {
|
||||||
|
remote pb.AuthClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuth(c *Client) Auth {
|
||||||
|
return &auth{remote: pb.NewAuthClient(c.ActiveConnection())}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *auth) AuthEnable(ctx context.Context) (*AuthEnableResponse, error) {
|
||||||
|
resp, err := auth.remote.AuthEnable(ctx, &pb.AuthEnableRequest{}, grpc.FailFast(false))
|
||||||
|
return (*AuthEnableResponse)(resp), toErr(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *auth) AuthDisable(ctx context.Context) (*AuthDisableResponse, error) {
|
||||||
|
resp, err := auth.remote.AuthDisable(ctx, &pb.AuthDisableRequest{}, grpc.FailFast(false))
|
||||||
|
return (*AuthDisableResponse)(resp), toErr(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *auth) UserAdd(ctx context.Context, name string, password string) (*AuthUserAddResponse, error) {
|
||||||
|
resp, err := auth.remote.UserAdd(ctx, &pb.AuthUserAddRequest{Name: name, Password: password})
|
||||||
|
return (*AuthUserAddResponse)(resp), toErr(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *auth) UserDelete(ctx context.Context, name string) (*AuthUserDeleteResponse, error) {
|
||||||
|
resp, err := auth.remote.UserDelete(ctx, &pb.AuthUserDeleteRequest{Name: name})
|
||||||
|
return (*AuthUserDeleteResponse)(resp), toErr(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *auth) UserChangePassword(ctx context.Context, name string, password string) (*AuthUserChangePasswordResponse, error) {
|
||||||
|
resp, err := auth.remote.UserChangePassword(ctx, &pb.AuthUserChangePasswordRequest{Name: name, Password: password})
|
||||||
|
return (*AuthUserChangePasswordResponse)(resp), toErr(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *auth) UserGrantRole(ctx context.Context, user string, role string) (*AuthUserGrantRoleResponse, error) {
|
||||||
|
resp, err := auth.remote.UserGrantRole(ctx, &pb.AuthUserGrantRoleRequest{User: user, Role: role})
|
||||||
|
return (*AuthUserGrantRoleResponse)(resp), toErr(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *auth) UserGet(ctx context.Context, name string) (*AuthUserGetResponse, error) {
|
||||||
|
resp, err := auth.remote.UserGet(ctx, &pb.AuthUserGetRequest{Name: name}, grpc.FailFast(false))
|
||||||
|
return (*AuthUserGetResponse)(resp), toErr(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *auth) UserList(ctx context.Context) (*AuthUserListResponse, error) {
|
||||||
|
resp, err := auth.remote.UserList(ctx, &pb.AuthUserListRequest{}, grpc.FailFast(false))
|
||||||
|
return (*AuthUserListResponse)(resp), toErr(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *auth) UserRevokeRole(ctx context.Context, name string, role string) (*AuthUserRevokeRoleResponse, error) {
|
||||||
|
resp, err := auth.remote.UserRevokeRole(ctx, &pb.AuthUserRevokeRoleRequest{Name: name, Role: role})
|
||||||
|
return (*AuthUserRevokeRoleResponse)(resp), toErr(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *auth) RoleAdd(ctx context.Context, name string) (*AuthRoleAddResponse, error) {
|
||||||
|
resp, err := auth.remote.RoleAdd(ctx, &pb.AuthRoleAddRequest{Name: name})
|
||||||
|
return (*AuthRoleAddResponse)(resp), toErr(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *auth) RoleGrantPermission(ctx context.Context, name string, key, rangeEnd string, permType PermissionType) (*AuthRoleGrantPermissionResponse, error) {
|
||||||
|
perm := &authpb.Permission{
|
||||||
|
Key: []byte(key),
|
||||||
|
RangeEnd: []byte(rangeEnd),
|
||||||
|
PermType: authpb.Permission_Type(permType),
|
||||||
|
}
|
||||||
|
resp, err := auth.remote.RoleGrantPermission(ctx, &pb.AuthRoleGrantPermissionRequest{Name: name, Perm: perm})
|
||||||
|
return (*AuthRoleGrantPermissionResponse)(resp), toErr(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *auth) RoleGet(ctx context.Context, role string) (*AuthRoleGetResponse, error) {
|
||||||
|
resp, err := auth.remote.RoleGet(ctx, &pb.AuthRoleGetRequest{Role: role}, grpc.FailFast(false))
|
||||||
|
return (*AuthRoleGetResponse)(resp), toErr(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *auth) RoleList(ctx context.Context) (*AuthRoleListResponse, error) {
|
||||||
|
resp, err := auth.remote.RoleList(ctx, &pb.AuthRoleListRequest{}, grpc.FailFast(false))
|
||||||
|
return (*AuthRoleListResponse)(resp), toErr(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *auth) RoleRevokePermission(ctx context.Context, role string, key, rangeEnd string) (*AuthRoleRevokePermissionResponse, error) {
|
||||||
|
resp, err := auth.remote.RoleRevokePermission(ctx, &pb.AuthRoleRevokePermissionRequest{Role: role, Key: key, RangeEnd: rangeEnd})
|
||||||
|
return (*AuthRoleRevokePermissionResponse)(resp), toErr(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *auth) RoleDelete(ctx context.Context, role string) (*AuthRoleDeleteResponse, error) {
|
||||||
|
resp, err := auth.remote.RoleDelete(ctx, &pb.AuthRoleDeleteRequest{Role: role})
|
||||||
|
return (*AuthRoleDeleteResponse)(resp), toErr(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func StrToPermissionType(s string) (PermissionType, error) {
|
||||||
|
val, ok := authpb.Permission_Type_value[strings.ToUpper(s)]
|
||||||
|
if ok {
|
||||||
|
return PermissionType(val), nil
|
||||||
|
}
|
||||||
|
return PermissionType(-1), fmt.Errorf("invalid permission type: %s", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
type authenticator struct {
|
||||||
|
conn *grpc.ClientConn // conn in-use
|
||||||
|
remote pb.AuthClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *authenticator) authenticate(ctx context.Context, name string, password string) (*AuthenticateResponse, error) {
|
||||||
|
resp, err := auth.remote.Authenticate(ctx, &pb.AuthenticateRequest{Name: name, Password: password}, grpc.FailFast(false))
|
||||||
|
return (*AuthenticateResponse)(resp), toErr(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *authenticator) close() {
|
||||||
|
auth.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAuthenticator(endpoint string, opts []grpc.DialOption) (*authenticator, error) {
|
||||||
|
conn, err := grpc.Dial(endpoint, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &authenticator{
|
||||||
|
conn: conn,
|
||||||
|
remote: pb.NewAuthClient(conn),
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -0,0 +1,356 @@
|
||||||
|
// Copyright 2016 The etcd Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package clientv3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrNoAddrAvilable is returned by Get() when the balancer does not have
|
||||||
|
// any active connection to endpoints at the time.
|
||||||
|
// This error is returned only when opts.BlockingWait is true.
|
||||||
|
var ErrNoAddrAvilable = grpc.Errorf(codes.Unavailable, "there is no address available")
|
||||||
|
|
||||||
|
// simpleBalancer does the bare minimum to expose multiple eps
|
||||||
|
// to the grpc reconnection code path
|
||||||
|
type simpleBalancer struct {
|
||||||
|
// addrs are the client's endpoints for grpc
|
||||||
|
addrs []grpc.Address
|
||||||
|
// notifyCh notifies grpc of the set of addresses for connecting
|
||||||
|
notifyCh chan []grpc.Address
|
||||||
|
|
||||||
|
// readyc closes once the first connection is up
|
||||||
|
readyc chan struct{}
|
||||||
|
readyOnce sync.Once
|
||||||
|
|
||||||
|
// mu protects upEps, pinAddr, and connectingAddr
|
||||||
|
mu sync.RWMutex
|
||||||
|
|
||||||
|
// upc closes when upEps transitions from empty to non-zero or the balancer closes.
|
||||||
|
upc chan struct{}
|
||||||
|
|
||||||
|
// downc closes when grpc calls down() on pinAddr
|
||||||
|
downc chan struct{}
|
||||||
|
|
||||||
|
// stopc is closed to signal updateNotifyLoop should stop.
|
||||||
|
stopc chan struct{}
|
||||||
|
|
||||||
|
// donec closes when all goroutines are exited
|
||||||
|
donec chan struct{}
|
||||||
|
|
||||||
|
// updateAddrsC notifies updateNotifyLoop to update addrs.
|
||||||
|
updateAddrsC chan struct{}
|
||||||
|
|
||||||
|
// grpc issues TLS cert checks using the string passed into dial so
|
||||||
|
// that string must be the host. To recover the full scheme://host URL,
|
||||||
|
// have a map from hosts to the original endpoint.
|
||||||
|
host2ep map[string]string
|
||||||
|
|
||||||
|
// pinAddr is the currently pinned address; set to the empty string on
|
||||||
|
// intialization and shutdown.
|
||||||
|
pinAddr string
|
||||||
|
|
||||||
|
closed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSimpleBalancer(eps []string) *simpleBalancer {
|
||||||
|
notifyCh := make(chan []grpc.Address, 1)
|
||||||
|
addrs := make([]grpc.Address, len(eps))
|
||||||
|
for i := range eps {
|
||||||
|
addrs[i].Addr = getHost(eps[i])
|
||||||
|
}
|
||||||
|
sb := &simpleBalancer{
|
||||||
|
addrs: addrs,
|
||||||
|
notifyCh: notifyCh,
|
||||||
|
readyc: make(chan struct{}),
|
||||||
|
upc: make(chan struct{}),
|
||||||
|
stopc: make(chan struct{}),
|
||||||
|
downc: make(chan struct{}),
|
||||||
|
donec: make(chan struct{}),
|
||||||
|
updateAddrsC: make(chan struct{}, 1),
|
||||||
|
host2ep: getHost2ep(eps),
|
||||||
|
}
|
||||||
|
close(sb.downc)
|
||||||
|
go sb.updateNotifyLoop()
|
||||||
|
return sb
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *simpleBalancer) Start(target string, config grpc.BalancerConfig) error { return nil }
|
||||||
|
|
||||||
|
func (b *simpleBalancer) ConnectNotify() <-chan struct{} {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
return b.upc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *simpleBalancer) getEndpoint(host string) string {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
return b.host2ep[host]
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHost2ep(eps []string) map[string]string {
|
||||||
|
hm := make(map[string]string, len(eps))
|
||||||
|
for i := range eps {
|
||||||
|
_, host, _ := parseEndpoint(eps[i])
|
||||||
|
hm[host] = eps[i]
|
||||||
|
}
|
||||||
|
return hm
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *simpleBalancer) updateAddrs(eps []string) {
|
||||||
|
np := getHost2ep(eps)
|
||||||
|
|
||||||
|
b.mu.Lock()
|
||||||
|
|
||||||
|
match := len(np) == len(b.host2ep)
|
||||||
|
for k, v := range np {
|
||||||
|
if b.host2ep[k] != v {
|
||||||
|
match = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if match {
|
||||||
|
// same endpoints, so no need to update address
|
||||||
|
b.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b.host2ep = np
|
||||||
|
|
||||||
|
addrs := make([]grpc.Address, 0, len(eps))
|
||||||
|
for i := range eps {
|
||||||
|
addrs = append(addrs, grpc.Address{Addr: getHost(eps[i])})
|
||||||
|
}
|
||||||
|
b.addrs = addrs
|
||||||
|
|
||||||
|
// updating notifyCh can trigger new connections,
|
||||||
|
// only update addrs if all connections are down
|
||||||
|
// or addrs does not include pinAddr.
|
||||||
|
update := !hasAddr(addrs, b.pinAddr)
|
||||||
|
b.mu.Unlock()
|
||||||
|
|
||||||
|
if update {
|
||||||
|
select {
|
||||||
|
case b.updateAddrsC <- struct{}{}:
|
||||||
|
case <-b.stopc:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasAddr(addrs []grpc.Address, targetAddr string) bool {
|
||||||
|
for _, addr := range addrs {
|
||||||
|
if targetAddr == addr.Addr {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *simpleBalancer) updateNotifyLoop() {
|
||||||
|
defer close(b.donec)
|
||||||
|
|
||||||
|
for {
|
||||||
|
b.mu.RLock()
|
||||||
|
upc, downc, addr := b.upc, b.downc, b.pinAddr
|
||||||
|
b.mu.RUnlock()
|
||||||
|
// downc or upc should be closed
|
||||||
|
select {
|
||||||
|
case <-downc:
|
||||||
|
downc = nil
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-upc:
|
||||||
|
upc = nil
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case downc == nil && upc == nil:
|
||||||
|
// stale
|
||||||
|
select {
|
||||||
|
case <-b.stopc:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
case downc == nil:
|
||||||
|
b.notifyAddrs()
|
||||||
|
select {
|
||||||
|
case <-upc:
|
||||||
|
case <-b.updateAddrsC:
|
||||||
|
b.notifyAddrs()
|
||||||
|
case <-b.stopc:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case upc == nil:
|
||||||
|
select {
|
||||||
|
// close connections that are not the pinned address
|
||||||
|
case b.notifyCh <- []grpc.Address{{Addr: addr}}:
|
||||||
|
case <-downc:
|
||||||
|
case <-b.stopc:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-downc:
|
||||||
|
case <-b.updateAddrsC:
|
||||||
|
case <-b.stopc:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b.notifyAddrs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *simpleBalancer) notifyAddrs() {
|
||||||
|
b.mu.RLock()
|
||||||
|
addrs := b.addrs
|
||||||
|
b.mu.RUnlock()
|
||||||
|
select {
|
||||||
|
case b.notifyCh <- addrs:
|
||||||
|
case <-b.stopc:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *simpleBalancer) Up(addr grpc.Address) func(error) {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
// gRPC might call Up after it called Close. We add this check
|
||||||
|
// to "fix" it up at application layer. Or our simplerBalancer
|
||||||
|
// might panic since b.upc is closed.
|
||||||
|
if b.closed {
|
||||||
|
return func(err error) {}
|
||||||
|
}
|
||||||
|
// gRPC might call Up on a stale address.
|
||||||
|
// Prevent updating pinAddr with a stale address.
|
||||||
|
if !hasAddr(b.addrs, addr.Addr) {
|
||||||
|
return func(err error) {}
|
||||||
|
}
|
||||||
|
if b.pinAddr != "" {
|
||||||
|
return func(err error) {}
|
||||||
|
}
|
||||||
|
// notify waiting Get()s and pin first connected address
|
||||||
|
close(b.upc)
|
||||||
|
b.downc = make(chan struct{})
|
||||||
|
b.pinAddr = addr.Addr
|
||||||
|
// notify client that a connection is up
|
||||||
|
b.readyOnce.Do(func() { close(b.readyc) })
|
||||||
|
return func(err error) {
|
||||||
|
b.mu.Lock()
|
||||||
|
b.upc = make(chan struct{})
|
||||||
|
close(b.downc)
|
||||||
|
b.pinAddr = ""
|
||||||
|
b.mu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *simpleBalancer) Get(ctx context.Context, opts grpc.BalancerGetOptions) (grpc.Address, func(), error) {
|
||||||
|
var (
|
||||||
|
addr string
|
||||||
|
closed bool
|
||||||
|
)
|
||||||
|
|
||||||
|
// If opts.BlockingWait is false (for fail-fast RPCs), it should return
|
||||||
|
// an address it has notified via Notify immediately instead of blocking.
|
||||||
|
if !opts.BlockingWait {
|
||||||
|
b.mu.RLock()
|
||||||
|
closed = b.closed
|
||||||
|
addr = b.pinAddr
|
||||||
|
b.mu.RUnlock()
|
||||||
|
if closed {
|
||||||
|
return grpc.Address{Addr: ""}, nil, grpc.ErrClientConnClosing
|
||||||
|
}
|
||||||
|
if addr == "" {
|
||||||
|
return grpc.Address{Addr: ""}, nil, ErrNoAddrAvilable
|
||||||
|
}
|
||||||
|
return grpc.Address{Addr: addr}, func() {}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
b.mu.RLock()
|
||||||
|
ch := b.upc
|
||||||
|
b.mu.RUnlock()
|
||||||
|
select {
|
||||||
|
case <-ch:
|
||||||
|
case <-b.donec:
|
||||||
|
return grpc.Address{Addr: ""}, nil, grpc.ErrClientConnClosing
|
||||||
|
case <-ctx.Done():
|
||||||
|
return grpc.Address{Addr: ""}, nil, ctx.Err()
|
||||||
|
}
|
||||||
|
b.mu.RLock()
|
||||||
|
closed = b.closed
|
||||||
|
addr = b.pinAddr
|
||||||
|
b.mu.RUnlock()
|
||||||
|
// Close() which sets b.closed = true can be called before Get(), Get() must exit if balancer is closed.
|
||||||
|
if closed {
|
||||||
|
return grpc.Address{Addr: ""}, nil, grpc.ErrClientConnClosing
|
||||||
|
}
|
||||||
|
if addr != "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return grpc.Address{Addr: addr}, func() {}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *simpleBalancer) Notify() <-chan []grpc.Address { return b.notifyCh }
|
||||||
|
|
||||||
|
func (b *simpleBalancer) Close() error {
|
||||||
|
b.mu.Lock()
|
||||||
|
// In case gRPC calls close twice. TODO: remove the checking
|
||||||
|
// when we are sure that gRPC wont call close twice.
|
||||||
|
if b.closed {
|
||||||
|
b.mu.Unlock()
|
||||||
|
<-b.donec
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
b.closed = true
|
||||||
|
close(b.stopc)
|
||||||
|
b.pinAddr = ""
|
||||||
|
|
||||||
|
// In the case of following scenario:
|
||||||
|
// 1. upc is not closed; no pinned address
|
||||||
|
// 2. client issues an rpc, calling invoke(), which calls Get(), enters for loop, blocks
|
||||||
|
// 3. clientconn.Close() calls balancer.Close(); closed = true
|
||||||
|
// 4. for loop in Get() never exits since ctx is the context passed in by the client and may not be canceled
|
||||||
|
// we must close upc so Get() exits from blocking on upc
|
||||||
|
select {
|
||||||
|
case <-b.upc:
|
||||||
|
default:
|
||||||
|
// terminate all waiting Get()s
|
||||||
|
close(b.upc)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.mu.Unlock()
|
||||||
|
|
||||||
|
// wait for updateNotifyLoop to finish
|
||||||
|
<-b.donec
|
||||||
|
close(b.notifyCh)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHost(ep string) string {
|
||||||
|
url, uerr := url.Parse(ep)
|
||||||
|
if uerr != nil || !strings.Contains(ep, "://") {
|
||||||
|
return ep
|
||||||
|
}
|
||||||
|
return url.Host
|
||||||
|
}
|
|
@ -0,0 +1,515 @@
|
||||||
|
// Copyright 2016 The etcd Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package clientv3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/credentials"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNoAvailableEndpoints = errors.New("etcdclient: no available endpoints")
|
||||||
|
ErrOldCluster = errors.New("etcdclient: old cluster version")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client provides and manages an etcd v3 client session.
|
||||||
|
type Client struct {
|
||||||
|
Cluster
|
||||||
|
KV
|
||||||
|
Lease
|
||||||
|
Watcher
|
||||||
|
Auth
|
||||||
|
Maintenance
|
||||||
|
|
||||||
|
conn *grpc.ClientConn
|
||||||
|
dialerrc chan error
|
||||||
|
|
||||||
|
cfg Config
|
||||||
|
creds *credentials.TransportCredentials
|
||||||
|
balancer *simpleBalancer
|
||||||
|
retryWrapper retryRpcFunc
|
||||||
|
retryAuthWrapper retryRpcFunc
|
||||||
|
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
|
||||||
|
// Username is a username for authentication
|
||||||
|
Username string
|
||||||
|
// Password is a password for authentication
|
||||||
|
Password string
|
||||||
|
// tokenCred is an instance of WithPerRPCCredentials()'s argument
|
||||||
|
tokenCred *authTokenCredential
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new etcdv3 client from a given configuration.
|
||||||
|
func New(cfg Config) (*Client, error) {
|
||||||
|
if len(cfg.Endpoints) == 0 {
|
||||||
|
return nil, ErrNoAvailableEndpoints
|
||||||
|
}
|
||||||
|
|
||||||
|
return newClient(&cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCtxClient creates a client with a context but no underlying grpc
|
||||||
|
// connection. This is useful for embedded cases that override the
|
||||||
|
// service interface implementations and do not need connection management.
|
||||||
|
func NewCtxClient(ctx context.Context) *Client {
|
||||||
|
cctx, cancel := context.WithCancel(ctx)
|
||||||
|
return &Client{ctx: cctx, cancel: cancel}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFromURL creates a new etcdv3 client from a URL.
|
||||||
|
func NewFromURL(url string) (*Client, error) {
|
||||||
|
return New(Config{Endpoints: []string{url}})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close shuts down the client's etcd connections.
|
||||||
|
func (c *Client) Close() error {
|
||||||
|
c.cancel()
|
||||||
|
c.Watcher.Close()
|
||||||
|
c.Lease.Close()
|
||||||
|
if c.conn != nil {
|
||||||
|
return toErr(c.ctx, c.conn.Close())
|
||||||
|
}
|
||||||
|
return c.ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctx is a context for "out of band" messages (e.g., for sending
|
||||||
|
// "clean up" message when another context is canceled). It is
|
||||||
|
// canceled on client Close().
|
||||||
|
func (c *Client) Ctx() context.Context { return c.ctx }
|
||||||
|
|
||||||
|
// Endpoints lists the registered endpoints for the client.
|
||||||
|
func (c *Client) Endpoints() (eps []string) {
|
||||||
|
// copy the slice; protect original endpoints from being changed
|
||||||
|
eps = make([]string, len(c.cfg.Endpoints))
|
||||||
|
copy(eps, c.cfg.Endpoints)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetEndpoints updates client's endpoints.
|
||||||
|
func (c *Client) SetEndpoints(eps ...string) {
|
||||||
|
c.cfg.Endpoints = eps
|
||||||
|
c.balancer.updateAddrs(eps)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync synchronizes client's endpoints with the known endpoints from the etcd membership.
|
||||||
|
func (c *Client) Sync(ctx context.Context) error {
|
||||||
|
mresp, err := c.MemberList(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var eps []string
|
||||||
|
for _, m := range mresp.Members {
|
||||||
|
eps = append(eps, m.ClientURLs...)
|
||||||
|
}
|
||||||
|
c.SetEndpoints(eps...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) autoSync() {
|
||||||
|
if c.cfg.AutoSyncInterval == time.Duration(0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-c.ctx.Done():
|
||||||
|
return
|
||||||
|
case <-time.After(c.cfg.AutoSyncInterval):
|
||||||
|
ctx, _ := context.WithTimeout(c.ctx, 5*time.Second)
|
||||||
|
if err := c.Sync(ctx); err != nil && err != c.ctx.Err() {
|
||||||
|
logger.Println("Auto sync endpoints failed:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type authTokenCredential struct {
|
||||||
|
token string
|
||||||
|
tokenMu *sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cred authTokenCredential) RequireTransportSecurity() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cred authTokenCredential) GetRequestMetadata(ctx context.Context, s ...string) (map[string]string, error) {
|
||||||
|
cred.tokenMu.RLock()
|
||||||
|
defer cred.tokenMu.RUnlock()
|
||||||
|
return map[string]string{
|
||||||
|
"token": cred.token,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseEndpoint(endpoint string) (proto string, host string, scheme string) {
|
||||||
|
proto = "tcp"
|
||||||
|
host = endpoint
|
||||||
|
url, uerr := url.Parse(endpoint)
|
||||||
|
if uerr != nil || !strings.Contains(endpoint, "://") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
scheme = url.Scheme
|
||||||
|
|
||||||
|
// strip scheme:// prefix since grpc dials by host
|
||||||
|
host = url.Host
|
||||||
|
switch url.Scheme {
|
||||||
|
case "http", "https":
|
||||||
|
case "unix", "unixs":
|
||||||
|
proto = "unix"
|
||||||
|
host = url.Host + url.Path
|
||||||
|
default:
|
||||||
|
proto, host = "", ""
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) processCreds(scheme string) (creds *credentials.TransportCredentials) {
|
||||||
|
creds = c.creds
|
||||||
|
switch scheme {
|
||||||
|
case "unix":
|
||||||
|
case "http":
|
||||||
|
creds = nil
|
||||||
|
case "https", "unixs":
|
||||||
|
if creds != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
tlsconfig := &tls.Config{}
|
||||||
|
emptyCreds := credentials.NewTLS(tlsconfig)
|
||||||
|
creds = &emptyCreds
|
||||||
|
default:
|
||||||
|
creds = nil
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// dialSetupOpts gives the dial opts prior to any authentication
|
||||||
|
func (c *Client) dialSetupOpts(endpoint string, dopts ...grpc.DialOption) (opts []grpc.DialOption) {
|
||||||
|
if c.cfg.DialTimeout > 0 {
|
||||||
|
opts = []grpc.DialOption{grpc.WithTimeout(c.cfg.DialTimeout)}
|
||||||
|
}
|
||||||
|
opts = append(opts, dopts...)
|
||||||
|
|
||||||
|
f := func(host string, t time.Duration) (net.Conn, error) {
|
||||||
|
proto, host, _ := parseEndpoint(c.balancer.getEndpoint(host))
|
||||||
|
if host == "" && endpoint != "" {
|
||||||
|
// dialing an endpoint not in the balancer; use
|
||||||
|
// endpoint passed into dial
|
||||||
|
proto, host, _ = parseEndpoint(endpoint)
|
||||||
|
}
|
||||||
|
if proto == "" {
|
||||||
|
return nil, fmt.Errorf("unknown scheme for %q", host)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-c.ctx.Done():
|
||||||
|
return nil, c.ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
dialer := &net.Dialer{Timeout: t}
|
||||||
|
conn, err := dialer.DialContext(c.ctx, proto, host)
|
||||||
|
if err != nil {
|
||||||
|
select {
|
||||||
|
case c.dialerrc <- err:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return conn, err
|
||||||
|
}
|
||||||
|
opts = append(opts, grpc.WithDialer(f))
|
||||||
|
|
||||||
|
creds := c.creds
|
||||||
|
if _, _, scheme := parseEndpoint(endpoint); len(scheme) != 0 {
|
||||||
|
creds = c.processCreds(scheme)
|
||||||
|
}
|
||||||
|
if creds != nil {
|
||||||
|
opts = append(opts, grpc.WithTransportCredentials(*creds))
|
||||||
|
} else {
|
||||||
|
opts = append(opts, grpc.WithInsecure())
|
||||||
|
}
|
||||||
|
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dial connects to a single endpoint using the client's config.
|
||||||
|
func (c *Client) Dial(endpoint string) (*grpc.ClientConn, error) {
|
||||||
|
return c.dial(endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) getToken(ctx context.Context) error {
|
||||||
|
var err error // return last error in a case of fail
|
||||||
|
var auth *authenticator
|
||||||
|
|
||||||
|
for i := 0; i < len(c.cfg.Endpoints); i++ {
|
||||||
|
endpoint := c.cfg.Endpoints[i]
|
||||||
|
host := getHost(endpoint)
|
||||||
|
// use dial options without dopts to avoid reusing the client balancer
|
||||||
|
auth, err = newAuthenticator(host, c.dialSetupOpts(endpoint))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
defer auth.close()
|
||||||
|
|
||||||
|
var resp *AuthenticateResponse
|
||||||
|
resp, err = auth.authenticate(ctx, c.Username, c.Password)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
c.tokenCred.tokenMu.Lock()
|
||||||
|
c.tokenCred.token = resp.Token
|
||||||
|
c.tokenCred.tokenMu.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) dial(endpoint string, dopts ...grpc.DialOption) (*grpc.ClientConn, error) {
|
||||||
|
opts := c.dialSetupOpts(endpoint, dopts...)
|
||||||
|
host := getHost(endpoint)
|
||||||
|
if c.Username != "" && c.Password != "" {
|
||||||
|
c.tokenCred = &authTokenCredential{
|
||||||
|
tokenMu: &sync.RWMutex{},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := c.ctx
|
||||||
|
if c.cfg.DialTimeout > 0 {
|
||||||
|
cctx, cancel := context.WithTimeout(ctx, c.cfg.DialTimeout)
|
||||||
|
defer cancel()
|
||||||
|
ctx = cctx
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.getToken(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if toErr(ctx, err) != rpctypes.ErrAuthNotEnabled {
|
||||||
|
if err == ctx.Err() && ctx.Err() != c.ctx.Err() {
|
||||||
|
err = grpc.ErrClientConnTimeout
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
opts = append(opts, grpc.WithPerRPCCredentials(c.tokenCred))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
opts = append(opts, c.cfg.DialOptions...)
|
||||||
|
|
||||||
|
conn, err := grpc.DialContext(c.ctx, host, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithRequireLeader requires client requests to only succeed
|
||||||
|
// when the cluster has a leader.
|
||||||
|
func WithRequireLeader(ctx context.Context) context.Context {
|
||||||
|
md := metadata.Pairs(rpctypes.MetadataRequireLeaderKey, rpctypes.MetadataHasLeader)
|
||||||
|
return metadata.NewContext(ctx, md)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newClient(cfg *Config) (*Client, error) {
|
||||||
|
if cfg == nil {
|
||||||
|
cfg = &Config{}
|
||||||
|
}
|
||||||
|
var creds *credentials.TransportCredentials
|
||||||
|
if cfg.TLS != nil {
|
||||||
|
c := credentials.NewTLS(cfg.TLS)
|
||||||
|
creds = &c
|
||||||
|
}
|
||||||
|
|
||||||
|
// use a temporary skeleton client to bootstrap first connection
|
||||||
|
baseCtx := context.TODO()
|
||||||
|
if cfg.Context != nil {
|
||||||
|
baseCtx = cfg.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(baseCtx)
|
||||||
|
client := &Client{
|
||||||
|
conn: nil,
|
||||||
|
dialerrc: make(chan error, 1),
|
||||||
|
cfg: *cfg,
|
||||||
|
creds: creds,
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
}
|
||||||
|
if cfg.Username != "" && cfg.Password != "" {
|
||||||
|
client.Username = cfg.Username
|
||||||
|
client.Password = cfg.Password
|
||||||
|
}
|
||||||
|
|
||||||
|
client.balancer = newSimpleBalancer(cfg.Endpoints)
|
||||||
|
// use Endpoints[0] so that for https:// without any tls config given, then
|
||||||
|
// grpc will assume the ServerName is in the endpoint.
|
||||||
|
conn, err := client.dial(cfg.Endpoints[0], grpc.WithBalancer(client.balancer))
|
||||||
|
if err != nil {
|
||||||
|
client.cancel()
|
||||||
|
client.balancer.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
client.conn = conn
|
||||||
|
client.retryWrapper = client.newRetryWrapper()
|
||||||
|
client.retryAuthWrapper = client.newAuthRetryWrapper()
|
||||||
|
|
||||||
|
// wait for a connection
|
||||||
|
if cfg.DialTimeout > 0 {
|
||||||
|
hasConn := false
|
||||||
|
waitc := time.After(cfg.DialTimeout)
|
||||||
|
select {
|
||||||
|
case <-client.balancer.readyc:
|
||||||
|
hasConn = true
|
||||||
|
case <-ctx.Done():
|
||||||
|
case <-waitc:
|
||||||
|
}
|
||||||
|
if !hasConn {
|
||||||
|
err := grpc.ErrClientConnTimeout
|
||||||
|
select {
|
||||||
|
case err = <-client.dialerrc:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
client.cancel()
|
||||||
|
client.balancer.Close()
|
||||||
|
conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Cluster = NewCluster(client)
|
||||||
|
client.KV = NewKV(client)
|
||||||
|
client.Lease = NewLease(client)
|
||||||
|
client.Watcher = NewWatcher(client)
|
||||||
|
client.Auth = NewAuth(client)
|
||||||
|
client.Maintenance = NewMaintenance(client)
|
||||||
|
|
||||||
|
if cfg.RejectOldCluster {
|
||||||
|
if err := client.checkVersion(); err != nil {
|
||||||
|
client.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go client.autoSync()
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) checkVersion() (err error) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
errc := make(chan error, len(c.cfg.Endpoints))
|
||||||
|
ctx, cancel := context.WithCancel(c.ctx)
|
||||||
|
if c.cfg.DialTimeout > 0 {
|
||||||
|
ctx, _ = context.WithTimeout(ctx, c.cfg.DialTimeout)
|
||||||
|
}
|
||||||
|
wg.Add(len(c.cfg.Endpoints))
|
||||||
|
for _, ep := range c.cfg.Endpoints {
|
||||||
|
// if cluster is current, any endpoint gives a recent version
|
||||||
|
go func(e string) {
|
||||||
|
defer wg.Done()
|
||||||
|
resp, rerr := c.Status(ctx, e)
|
||||||
|
if rerr != nil {
|
||||||
|
errc <- rerr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
vs := strings.Split(resp.Version, ".")
|
||||||
|
maj, min := 0, 0
|
||||||
|
if len(vs) >= 2 {
|
||||||
|
maj, rerr = strconv.Atoi(vs[0])
|
||||||
|
min, rerr = strconv.Atoi(vs[1])
|
||||||
|
}
|
||||||
|
if maj < 3 || (maj == 3 && min < 2) {
|
||||||
|
rerr = ErrOldCluster
|
||||||
|
}
|
||||||
|
errc <- rerr
|
||||||
|
}(ep)
|
||||||
|
}
|
||||||
|
// wait for success
|
||||||
|
for i := 0; i < len(c.cfg.Endpoints); i++ {
|
||||||
|
if err = <-errc; err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
wg.Wait()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActiveConnection returns the current in-use connection
|
||||||
|
func (c *Client) ActiveConnection() *grpc.ClientConn { return c.conn }
|
||||||
|
|
||||||
|
// isHaltErr returns true if the given error and context indicate no forward
|
||||||
|
// progress can be made, even after reconnecting.
|
||||||
|
func isHaltErr(ctx context.Context, err error) bool {
|
||||||
|
if ctx != nil && ctx.Err() != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
code := grpc.Code(err)
|
||||||
|
// Unavailable codes mean the system will be right back.
|
||||||
|
// (e.g., can't connect, lost leader)
|
||||||
|
// Treat Internal codes as if something failed, leaving the
|
||||||
|
// system in an inconsistent state, but retrying could make progress.
|
||||||
|
// (e.g., failed in middle of send, corrupted frame)
|
||||||
|
// TODO: are permanent Internal errors possible from grpc?
|
||||||
|
return code != codes.Unavailable && code != codes.Internal
|
||||||
|
}
|
||||||
|
|
||||||
|
func toErr(ctx context.Context, err error) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err = rpctypes.Error(err)
|
||||||
|
if _, ok := err.(rpctypes.EtcdError); ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
code := grpc.Code(err)
|
||||||
|
switch code {
|
||||||
|
case codes.DeadlineExceeded:
|
||||||
|
fallthrough
|
||||||
|
case codes.Canceled:
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
err = ctx.Err()
|
||||||
|
}
|
||||||
|
case codes.Unavailable:
|
||||||
|
err = ErrNoAvailableEndpoints
|
||||||
|
case codes.FailedPrecondition:
|
||||||
|
err = grpc.ErrClientConnClosing
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func canceledByCaller(stopCtx context.Context, err error) bool {
|
||||||
|
if stopCtx.Err() == nil || err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return err == context.Canceled || err == context.DeadlineExceeded
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
// Copyright 2016 The etcd Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package clientv3
|
||||||
|
|
||||||
|
import (
|
||||||
|
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Member pb.Member
|
||||||
|
MemberListResponse pb.MemberListResponse
|
||||||
|
MemberAddResponse pb.MemberAddResponse
|
||||||
|
MemberRemoveResponse pb.MemberRemoveResponse
|
||||||
|
MemberUpdateResponse pb.MemberUpdateResponse
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cluster interface {
|
||||||
|
// MemberList lists the current cluster membership.
|
||||||
|
MemberList(ctx context.Context) (*MemberListResponse, error)
|
||||||
|
|
||||||
|
// MemberAdd adds a new member into the cluster.
|
||||||
|
MemberAdd(ctx context.Context, peerAddrs []string) (*MemberAddResponse, error)
|
||||||
|
|
||||||
|
// MemberRemove removes an existing member from the cluster.
|
||||||
|
MemberRemove(ctx context.Context, id uint64) (*MemberRemoveResponse, error)
|
||||||
|
|
||||||
|
// MemberUpdate updates the peer addresses of the member.
|
||||||
|
MemberUpdate(ctx context.Context, id uint64, peerAddrs []string) (*MemberUpdateResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type cluster struct {
|
||||||
|
remote pb.ClusterClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCluster(c *Client) Cluster {
|
||||||
|
return &cluster{remote: RetryClusterClient(c)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClusterFromClusterClient(remote pb.ClusterClient) Cluster {
|
||||||
|
return &cluster{remote: remote}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cluster) MemberAdd(ctx context.Context, peerAddrs []string) (*MemberAddResponse, error) {
|
||||||
|
r := &pb.MemberAddRequest{PeerURLs: peerAddrs}
|
||||||
|
resp, err := c.remote.MemberAdd(ctx, r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, toErr(ctx, err)
|
||||||
|
}
|
||||||
|
return (*MemberAddResponse)(resp), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cluster) MemberRemove(ctx context.Context, id uint64) (*MemberRemoveResponse, error) {
|
||||||
|
r := &pb.MemberRemoveRequest{ID: id}
|
||||||
|
resp, err := c.remote.MemberRemove(ctx, r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, toErr(ctx, err)
|
||||||
|
}
|
||||||
|
return (*MemberRemoveResponse)(resp), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cluster) MemberUpdate(ctx context.Context, id uint64, peerAddrs []string) (*MemberUpdateResponse, error) {
|
||||||
|
// it is safe to retry on update.
|
||||||
|
for {
|
||||||
|
r := &pb.MemberUpdateRequest{ID: id, PeerURLs: peerAddrs}
|
||||||
|
resp, err := c.remote.MemberUpdate(ctx, r, grpc.FailFast(false))
|
||||||
|
if err == nil {
|
||||||
|
return (*MemberUpdateResponse)(resp), nil
|
||||||
|
}
|
||||||
|
if isHaltErr(ctx, err) {
|
||||||
|
return nil, toErr(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cluster) MemberList(ctx context.Context) (*MemberListResponse, error) {
|
||||||
|
// it is safe to retry on list.
|
||||||
|
for {
|
||||||
|
resp, err := c.remote.MemberList(ctx, &pb.MemberListRequest{}, grpc.FailFast(false))
|
||||||
|
if err == nil {
|
||||||
|
return (*MemberListResponse)(resp), nil
|
||||||
|
}
|
||||||
|
if isHaltErr(ctx, err) {
|
||||||
|
return nil, toErr(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
// Copyright 2016 The etcd Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package clientv3
|
||||||
|
|
||||||
|
import (
|
||||||
|
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CompactOp represents a compact operation.
|
||||||
|
type CompactOp struct {
|
||||||
|
revision int64
|
||||||
|
physical bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompactOption configures compact operation.
|
||||||
|
type CompactOption func(*CompactOp)
|
||||||
|
|
||||||
|
func (op *CompactOp) applyCompactOpts(opts []CompactOption) {
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(op)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpCompact wraps slice CompactOption to create a CompactOp.
|
||||||
|
func OpCompact(rev int64, opts ...CompactOption) CompactOp {
|
||||||
|
ret := CompactOp{revision: rev}
|
||||||
|
ret.applyCompactOpts(opts)
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (op CompactOp) toRequest() *pb.CompactionRequest {
|
||||||
|
return &pb.CompactionRequest{Revision: op.revision, Physical: op.physical}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithCompactPhysical makes compact RPC call wait until
|
||||||
|
// the compaction is physically applied to the local database
|
||||||
|
// such that compacted entries are totally removed from the
|
||||||
|
// backend database.
|
||||||
|
func WithCompactPhysical() CompactOption {
|
||||||
|
return func(op *CompactOp) { op.physical = true }
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
// Copyright 2016 The etcd Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package clientv3
|
||||||
|
|
||||||
|
import (
|
||||||
|
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CompareTarget int
|
||||||
|
type CompareResult int
|
||||||
|
|
||||||
|
const (
|
||||||
|
CompareVersion CompareTarget = iota
|
||||||
|
CompareCreated
|
||||||
|
CompareModified
|
||||||
|
CompareValue
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cmp pb.Compare
|
||||||
|
|
||||||
|
func Compare(cmp Cmp, result string, v interface{}) Cmp {
|
||||||
|
var r pb.Compare_CompareResult
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case "=":
|
||||||
|
r = pb.Compare_EQUAL
|
||||||
|
case "!=":
|
||||||
|
r = pb.Compare_NOT_EQUAL
|
||||||
|
case ">":
|
||||||
|
r = pb.Compare_GREATER
|
||||||
|
case "<":
|
||||||
|
r = pb.Compare_LESS
|
||||||
|
default:
|
||||||
|
panic("Unknown result op")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmp.Result = r
|
||||||
|
switch cmp.Target {
|
||||||
|
case pb.Compare_VALUE:
|
||||||
|
val, ok := v.(string)
|
||||||
|
if !ok {
|
||||||
|
panic("bad compare value")
|
||||||
|
}
|
||||||
|
cmp.TargetUnion = &pb.Compare_Value{Value: []byte(val)}
|
||||||
|
case pb.Compare_VERSION:
|
||||||
|
cmp.TargetUnion = &pb.Compare_Version{Version: mustInt64(v)}
|
||||||
|
case pb.Compare_CREATE:
|
||||||
|
cmp.TargetUnion = &pb.Compare_CreateRevision{CreateRevision: mustInt64(v)}
|
||||||
|
case pb.Compare_MOD:
|
||||||
|
cmp.TargetUnion = &pb.Compare_ModRevision{ModRevision: mustInt64(v)}
|
||||||
|
default:
|
||||||
|
panic("Unknown compare type")
|
||||||
|
}
|
||||||
|
return cmp
|
||||||
|
}
|
||||||
|
|
||||||
|
func Value(key string) Cmp {
|
||||||
|
return Cmp{Key: []byte(key), Target: pb.Compare_VALUE}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Version(key string) Cmp {
|
||||||
|
return Cmp{Key: []byte(key), Target: pb.Compare_VERSION}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateRevision(key string) Cmp {
|
||||||
|
return Cmp{Key: []byte(key), Target: pb.Compare_CREATE}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ModRevision(key string) Cmp {
|
||||||
|
return Cmp{Key: []byte(key), Target: pb.Compare_MOD}
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyBytes returns the byte slice holding with the comparison key.
|
||||||
|
func (cmp *Cmp) KeyBytes() []byte { return cmp.Key }
|
||||||
|
|
||||||
|
// WithKeyBytes sets the byte slice for the comparison key.
|
||||||
|
func (cmp *Cmp) WithKeyBytes(key []byte) { cmp.Key = key }
|
||||||
|
|
||||||
|
// ValueBytes returns the byte slice holding the comparison value, if any.
|
||||||
|
func (cmp *Cmp) ValueBytes() []byte {
|
||||||
|
if tu, ok := cmp.TargetUnion.(*pb.Compare_Value); ok {
|
||||||
|
return tu.Value
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithValueBytes sets the byte slice for the comparison's value.
|
||||||
|
func (cmp *Cmp) WithValueBytes(v []byte) { cmp.TargetUnion.(*pb.Compare_Value).Value = v }
|
||||||
|
|
||||||
|
func mustInt64(val interface{}) int64 {
|
||||||
|
if v, ok := val.(int64); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
if v, ok := val.(int); ok {
|
||||||
|
return int64(v)
|
||||||
|
}
|
||||||
|
panic("bad value")
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
// Copyright 2016 The etcd Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// Package concurrency implements concurrency operations on top of
|
||||||
|
// etcd such as distributed locks, barriers, and elections.
|
||||||
|
package concurrency
|
|
@ -0,0 +1,243 @@
|
||||||
|
// Copyright 2016 The etcd Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package concurrency
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
v3 "github.com/coreos/etcd/clientv3"
|
||||||
|
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||||
|
"github.com/coreos/etcd/mvcc/mvccpb"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrElectionNotLeader = errors.New("election: not leader")
|
||||||
|
ErrElectionNoLeader = errors.New("election: no leader")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Election struct {
|
||||||
|
session *Session
|
||||||
|
|
||||||
|
keyPrefix string
|
||||||
|
|
||||||
|
leaderKey string
|
||||||
|
leaderRev int64
|
||||||
|
leaderSession *Session
|
||||||
|
hdr *pb.ResponseHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewElection returns a new election on a given key prefix.
|
||||||
|
func NewElection(s *Session, pfx string) *Election {
|
||||||
|
return &Election{session: s, keyPrefix: pfx + "/"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResumeElection initializes an election with a known leader.
|
||||||
|
func ResumeElection(s *Session, pfx string, leaderKey string, leaderRev int64) *Election {
|
||||||
|
return &Election{
|
||||||
|
session: s,
|
||||||
|
leaderKey: leaderKey,
|
||||||
|
leaderRev: leaderRev,
|
||||||
|
leaderSession: s,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Campaign puts a value as eligible for the election. It blocks until
|
||||||
|
// it is elected, an error occurs, or the context is cancelled.
|
||||||
|
func (e *Election) Campaign(ctx context.Context, val string) error {
|
||||||
|
s := e.session
|
||||||
|
client := e.session.Client()
|
||||||
|
|
||||||
|
k := fmt.Sprintf("%s%x", e.keyPrefix, s.Lease())
|
||||||
|
txn := client.Txn(ctx).If(v3.Compare(v3.CreateRevision(k), "=", 0))
|
||||||
|
txn = txn.Then(v3.OpPut(k, val, v3.WithLease(s.Lease())))
|
||||||
|
txn = txn.Else(v3.OpGet(k))
|
||||||
|
resp, err := txn.Commit()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.leaderKey, e.leaderRev, e.leaderSession = k, resp.Header.Revision, s
|
||||||
|
if !resp.Succeeded {
|
||||||
|
kv := resp.Responses[0].GetResponseRange().Kvs[0]
|
||||||
|
e.leaderRev = kv.CreateRevision
|
||||||
|
if string(kv.Value) != val {
|
||||||
|
if err = e.Proclaim(ctx, val); err != nil {
|
||||||
|
e.Resign(ctx)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = waitDeletes(ctx, client, e.keyPrefix, e.leaderRev-1)
|
||||||
|
if err != nil {
|
||||||
|
// clean up in case of context cancel
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
e.Resign(client.Ctx())
|
||||||
|
default:
|
||||||
|
e.leaderSession = nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.hdr = resp.Header
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proclaim lets the leader announce a new value without another election.
|
||||||
|
func (e *Election) Proclaim(ctx context.Context, val string) error {
|
||||||
|
if e.leaderSession == nil {
|
||||||
|
return ErrElectionNotLeader
|
||||||
|
}
|
||||||
|
client := e.session.Client()
|
||||||
|
cmp := v3.Compare(v3.CreateRevision(e.leaderKey), "=", e.leaderRev)
|
||||||
|
txn := client.Txn(ctx).If(cmp)
|
||||||
|
txn = txn.Then(v3.OpPut(e.leaderKey, val, v3.WithLease(e.leaderSession.Lease())))
|
||||||
|
tresp, terr := txn.Commit()
|
||||||
|
if terr != nil {
|
||||||
|
return terr
|
||||||
|
}
|
||||||
|
if !tresp.Succeeded {
|
||||||
|
e.leaderKey = ""
|
||||||
|
return ErrElectionNotLeader
|
||||||
|
}
|
||||||
|
|
||||||
|
e.hdr = tresp.Header
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resign lets a leader start a new election.
|
||||||
|
func (e *Election) Resign(ctx context.Context) (err error) {
|
||||||
|
if e.leaderSession == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
client := e.session.Client()
|
||||||
|
cmp := v3.Compare(v3.CreateRevision(e.leaderKey), "=", e.leaderRev)
|
||||||
|
resp, err := client.Txn(ctx).If(cmp).Then(v3.OpDelete(e.leaderKey)).Commit()
|
||||||
|
if err == nil {
|
||||||
|
e.hdr = resp.Header
|
||||||
|
}
|
||||||
|
e.leaderKey = ""
|
||||||
|
e.leaderSession = nil
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leader returns the leader value for the current election.
|
||||||
|
func (e *Election) Leader(ctx context.Context) (*v3.GetResponse, error) {
|
||||||
|
client := e.session.Client()
|
||||||
|
resp, err := client.Get(ctx, e.keyPrefix, v3.WithFirstCreate()...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if len(resp.Kvs) == 0 {
|
||||||
|
// no leader currently elected
|
||||||
|
return nil, ErrElectionNoLeader
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Observe returns a channel that reliably observes ordered leader proposals
|
||||||
|
// as GetResponse values on every current elected leader key. It will not
|
||||||
|
// necessarily fetch all historical leader updates, but will always post the
|
||||||
|
// most recent leader value.
|
||||||
|
//
|
||||||
|
// The channel closes when the context is canceled or the underlying watcher
|
||||||
|
// is otherwise disrupted.
|
||||||
|
func (e *Election) Observe(ctx context.Context) <-chan v3.GetResponse {
|
||||||
|
retc := make(chan v3.GetResponse)
|
||||||
|
go e.observe(ctx, retc)
|
||||||
|
return retc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Election) observe(ctx context.Context, ch chan<- v3.GetResponse) {
|
||||||
|
client := e.session.Client()
|
||||||
|
|
||||||
|
defer close(ch)
|
||||||
|
for {
|
||||||
|
resp, err := client.Get(ctx, e.keyPrefix, v3.WithFirstCreate()...)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var kv *mvccpb.KeyValue
|
||||||
|
var hdr *pb.ResponseHeader
|
||||||
|
|
||||||
|
if len(resp.Kvs) == 0 {
|
||||||
|
cctx, cancel := context.WithCancel(ctx)
|
||||||
|
// wait for first key put on prefix
|
||||||
|
opts := []v3.OpOption{v3.WithRev(resp.Header.Revision), v3.WithPrefix()}
|
||||||
|
wch := client.Watch(cctx, e.keyPrefix, opts...)
|
||||||
|
for kv == nil {
|
||||||
|
wr, ok := <-wch
|
||||||
|
if !ok || wr.Err() != nil {
|
||||||
|
cancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// only accept PUTs; a DELETE will make observe() spin
|
||||||
|
for _, ev := range wr.Events {
|
||||||
|
if ev.Type == mvccpb.PUT {
|
||||||
|
hdr, kv = &wr.Header, ev.Kv
|
||||||
|
// may have multiple revs; hdr.rev = the last rev
|
||||||
|
// set to kv's rev in case batch has multiple PUTs
|
||||||
|
hdr.Revision = kv.ModRevision
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
} else {
|
||||||
|
hdr, kv = resp.Header, resp.Kvs[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case ch <- v3.GetResponse{Header: hdr, Kvs: []*mvccpb.KeyValue{kv}}:
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cctx, cancel := context.WithCancel(ctx)
|
||||||
|
wch := client.Watch(cctx, string(kv.Key), v3.WithRev(hdr.Revision+1))
|
||||||
|
keyDeleted := false
|
||||||
|
for !keyDeleted {
|
||||||
|
wr, ok := <-wch
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, ev := range wr.Events {
|
||||||
|
if ev.Type == mvccpb.DELETE {
|
||||||
|
keyDeleted = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
resp.Header = &wr.Header
|
||||||
|
resp.Kvs = []*mvccpb.KeyValue{ev.Kv}
|
||||||
|
select {
|
||||||
|
case ch <- *resp:
|
||||||
|
case <-cctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key returns the leader key if elected, empty string otherwise.
|
||||||
|
func (e *Election) Key() string { return e.leaderKey }
|
||||||
|
|
||||||
|
// Rev returns the leader key's creation revision, if elected.
|
||||||
|
func (e *Election) Rev() int64 { return e.leaderRev }
|
||||||
|
|
||||||
|
// Header is the response header from the last successful election proposal.
|
||||||
|
func (m *Election) Header() *pb.ResponseHeader { return m.hdr }
|
|
@ -0,0 +1,65 @@
|
||||||
|
// Copyright 2016 The etcd Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package concurrency
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
v3 "github.com/coreos/etcd/clientv3"
|
||||||
|
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||||
|
"github.com/coreos/etcd/mvcc/mvccpb"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
func waitDelete(ctx context.Context, client *v3.Client, key string, rev int64) error {
|
||||||
|
cctx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var wr v3.WatchResponse
|
||||||
|
wch := client.Watch(cctx, key, v3.WithRev(rev))
|
||||||
|
for wr = range wch {
|
||||||
|
for _, ev := range wr.Events {
|
||||||
|
if ev.Type == mvccpb.DELETE {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := wr.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return fmt.Errorf("lost watcher waiting for delete")
|
||||||
|
}
|
||||||
|
|
||||||
|
// waitDeletes efficiently waits until all keys matching the prefix and no greater
|
||||||
|
// than the create revision.
|
||||||
|
func waitDeletes(ctx context.Context, client *v3.Client, pfx string, maxCreateRev int64) (*pb.ResponseHeader, error) {
|
||||||
|
getOpts := append(v3.WithLastCreate(), v3.WithMaxCreateRev(maxCreateRev))
|
||||||
|
for {
|
||||||
|
resp, err := client.Get(ctx, pfx, getOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(resp.Kvs) == 0 {
|
||||||
|
return resp.Header, nil
|
||||||
|
}
|
||||||
|
lastKey := string(resp.Kvs[0].Key)
|
||||||
|
if err = waitDelete(ctx, client, lastKey, resp.Header.Revision); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
// Copyright 2016 The etcd Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package concurrency
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
v3 "github.com/coreos/etcd/clientv3"
|
||||||
|
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mutex implements the sync Locker interface with etcd
|
||||||
|
type Mutex struct {
|
||||||
|
s *Session
|
||||||
|
|
||||||
|
pfx string
|
||||||
|
myKey string
|
||||||
|
myRev int64
|
||||||
|
hdr *pb.ResponseHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMutex(s *Session, pfx string) *Mutex {
|
||||||
|
return &Mutex{s, pfx + "/", "", -1, nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock locks the mutex with a cancelable context. If the context is canceled
|
||||||
|
// while trying to acquire the lock, the mutex tries to clean its stale lock entry.
|
||||||
|
func (m *Mutex) Lock(ctx context.Context) error {
|
||||||
|
s := m.s
|
||||||
|
client := m.s.Client()
|
||||||
|
|
||||||
|
m.myKey = fmt.Sprintf("%s%x", m.pfx, s.Lease())
|
||||||
|
cmp := v3.Compare(v3.CreateRevision(m.myKey), "=", 0)
|
||||||
|
// put self in lock waiters via myKey; oldest waiter holds lock
|
||||||
|
put := v3.OpPut(m.myKey, "", v3.WithLease(s.Lease()))
|
||||||
|
// reuse key in case this session already holds the lock
|
||||||
|
get := v3.OpGet(m.myKey)
|
||||||
|
resp, err := client.Txn(ctx).If(cmp).Then(put).Else(get).Commit()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.myRev = resp.Header.Revision
|
||||||
|
if !resp.Succeeded {
|
||||||
|
m.myRev = resp.Responses[0].GetResponseRange().Kvs[0].CreateRevision
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait for deletion revisions prior to myKey
|
||||||
|
hdr, werr := waitDeletes(ctx, client, m.pfx, m.myRev-1)
|
||||||
|
// release lock key if cancelled
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
m.Unlock(client.Ctx())
|
||||||
|
default:
|
||||||
|
m.hdr = hdr
|
||||||
|
}
|
||||||
|
return werr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mutex) Unlock(ctx context.Context) error {
|
||||||
|
client := m.s.Client()
|
||||||
|
if _, err := client.Delete(ctx, m.myKey); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.myKey = "\x00"
|
||||||
|
m.myRev = -1
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mutex) IsOwner() v3.Cmp {
|
||||||
|
return v3.Compare(v3.CreateRevision(m.myKey), "=", m.myRev)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mutex) Key() string { return m.myKey }
|
||||||
|
|
||||||
|
// Header is the response header received from etcd on acquiring the lock.
|
||||||
|
func (m *Mutex) Header() *pb.ResponseHeader { return m.hdr }
|
||||||
|
|
||||||
|
type lockerMutex struct{ *Mutex }
|
||||||
|
|
||||||
|
func (lm *lockerMutex) Lock() {
|
||||||
|
client := lm.s.Client()
|
||||||
|
if err := lm.Mutex.Lock(client.Ctx()); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (lm *lockerMutex) Unlock() {
|
||||||
|
client := lm.s.Client()
|
||||||
|
if err := lm.Mutex.Unlock(client.Ctx()); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLocker creates a sync.Locker backed by an etcd mutex.
|
||||||
|
func NewLocker(s *Session, pfx string) sync.Locker {
|
||||||
|
return &lockerMutex{NewMutex(s, pfx)}
|
||||||
|
}
|
|
@ -0,0 +1,140 @@
|
||||||
|
// Copyright 2016 The etcd Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package concurrency
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
v3 "github.com/coreos/etcd/clientv3"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultSessionTTL = 60
|
||||||
|
|
||||||
|
// Session represents a lease kept alive for the lifetime of a client.
|
||||||
|
// Fault-tolerant applications may use sessions to reason about liveness.
|
||||||
|
type Session struct {
|
||||||
|
client *v3.Client
|
||||||
|
opts *sessionOptions
|
||||||
|
id v3.LeaseID
|
||||||
|
|
||||||
|
cancel context.CancelFunc
|
||||||
|
donec <-chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSession gets the leased session for a client.
|
||||||
|
func NewSession(client *v3.Client, opts ...SessionOption) (*Session, error) {
|
||||||
|
ops := &sessionOptions{ttl: defaultSessionTTL, ctx: client.Ctx()}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(ops)
|
||||||
|
}
|
||||||
|
|
||||||
|
id := ops.leaseID
|
||||||
|
if id == v3.NoLease {
|
||||||
|
resp, err := client.Grant(ops.ctx, int64(ops.ttl))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
id = v3.LeaseID(resp.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(ops.ctx)
|
||||||
|
keepAlive, err := client.KeepAlive(ctx, id)
|
||||||
|
if err != nil || keepAlive == nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
donec := make(chan struct{})
|
||||||
|
s := &Session{client: client, opts: ops, id: id, cancel: cancel, donec: donec}
|
||||||
|
|
||||||
|
// keep the lease alive until client error or cancelled context
|
||||||
|
go func() {
|
||||||
|
defer close(donec)
|
||||||
|
for range keepAlive {
|
||||||
|
// eat messages until keep alive channel closes
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client is the etcd client that is attached to the session.
|
||||||
|
func (s *Session) Client() *v3.Client {
|
||||||
|
return s.client
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lease is the lease ID for keys bound to the session.
|
||||||
|
func (s *Session) Lease() v3.LeaseID { return s.id }
|
||||||
|
|
||||||
|
// Done returns a channel that closes when the lease is orphaned, expires, or
|
||||||
|
// is otherwise no longer being refreshed.
|
||||||
|
func (s *Session) Done() <-chan struct{} { return s.donec }
|
||||||
|
|
||||||
|
// Orphan ends the refresh for the session lease. This is useful
|
||||||
|
// in case the state of the client connection is indeterminate (revoke
|
||||||
|
// would fail) or when transferring lease ownership.
|
||||||
|
func (s *Session) Orphan() {
|
||||||
|
s.cancel()
|
||||||
|
<-s.donec
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close orphans the session and revokes the session lease.
|
||||||
|
func (s *Session) Close() error {
|
||||||
|
s.Orphan()
|
||||||
|
// if revoke takes longer than the ttl, lease is expired anyway
|
||||||
|
ctx, cancel := context.WithTimeout(s.opts.ctx, time.Duration(s.opts.ttl)*time.Second)
|
||||||
|
_, err := s.client.Revoke(ctx, s.id)
|
||||||
|
cancel()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type sessionOptions struct {
|
||||||
|
ttl int
|
||||||
|
leaseID v3.LeaseID
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionOption configures Session.
|
||||||
|
type SessionOption func(*sessionOptions)
|
||||||
|
|
||||||
|
// WithTTL configures the session's TTL in seconds.
|
||||||
|
// If TTL is <= 0, the default 60 seconds TTL will be used.
|
||||||
|
func WithTTL(ttl int) SessionOption {
|
||||||
|
return func(so *sessionOptions) {
|
||||||
|
if ttl > 0 {
|
||||||
|
so.ttl = ttl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLease specifies the existing leaseID to be used for the session.
|
||||||
|
// This is useful in process restart scenario, for example, to reclaim
|
||||||
|
// leadership from an election prior to restart.
|
||||||
|
func WithLease(leaseID v3.LeaseID) SessionOption {
|
||||||
|
return func(so *sessionOptions) {
|
||||||
|
so.leaseID = leaseID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithContext assigns a context to the session instead of defaulting to
|
||||||
|
// using the client context. This is useful for canceling NewSession and
|
||||||
|
// Close operations immediately without having to close the client. If the
|
||||||
|
// context is canceled before Close() completes, the session's lease will be
|
||||||
|
// abandoned and left to expire instead of being revoked.
|
||||||
|
func WithContext(ctx context.Context) SessionOption {
|
||||||
|
return func(so *sessionOptions) {
|
||||||
|
so.ctx = ctx
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,386 @@
|
||||||
|
// Copyright 2016 The etcd Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package concurrency
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
|
||||||
|
v3 "github.com/coreos/etcd/clientv3"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// STM is an interface for software transactional memory.
|
||||||
|
type STM interface {
|
||||||
|
// Get returns the value for a key and inserts the key in the txn's read set.
|
||||||
|
// If Get fails, it aborts the transaction with an error, never returning.
|
||||||
|
Get(key ...string) string
|
||||||
|
// Put adds a value for a key to the write set.
|
||||||
|
Put(key, val string, opts ...v3.OpOption)
|
||||||
|
// Rev returns the revision of a key in the read set.
|
||||||
|
Rev(key string) int64
|
||||||
|
// Del deletes a key.
|
||||||
|
Del(key string)
|
||||||
|
|
||||||
|
// commit attempts to apply the txn's changes to the server.
|
||||||
|
commit() *v3.TxnResponse
|
||||||
|
reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Isolation is an enumeration of transactional isolation levels which
|
||||||
|
// describes how transactions should interfere and conflict.
|
||||||
|
type Isolation int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// SerializableSnapshot provides serializable isolation and also checks
|
||||||
|
// for write conflicts.
|
||||||
|
SerializableSnapshot Isolation = iota
|
||||||
|
// Serializable reads within the same transactiona attempt return data
|
||||||
|
// from the at the revision of the first read.
|
||||||
|
Serializable
|
||||||
|
// RepeatableReads reads within the same transaction attempt always
|
||||||
|
// return the same data.
|
||||||
|
RepeatableReads
|
||||||
|
// ReadCommitted reads keys from any committed revision.
|
||||||
|
ReadCommitted
|
||||||
|
)
|
||||||
|
|
||||||
|
// stmError safely passes STM errors through panic to the STM error channel.
|
||||||
|
type stmError struct{ err error }
|
||||||
|
|
||||||
|
type stmOptions struct {
|
||||||
|
iso Isolation
|
||||||
|
ctx context.Context
|
||||||
|
prefetch []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type stmOption func(*stmOptions)
|
||||||
|
|
||||||
|
// WithIsolation specifies the transaction isolation level.
|
||||||
|
func WithIsolation(lvl Isolation) stmOption {
|
||||||
|
return func(so *stmOptions) { so.iso = lvl }
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAbortContext specifies the context for permanently aborting the transaction.
|
||||||
|
func WithAbortContext(ctx context.Context) stmOption {
|
||||||
|
return func(so *stmOptions) { so.ctx = ctx }
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPrefetch is a hint to prefetch a list of keys before trying to apply.
|
||||||
|
// If an STM transaction will unconditionally fetch a set of keys, prefetching
|
||||||
|
// those keys will save the round-trip cost from requesting each key one by one
|
||||||
|
// with Get().
|
||||||
|
func WithPrefetch(keys ...string) stmOption {
|
||||||
|
return func(so *stmOptions) { so.prefetch = append(so.prefetch, keys...) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSTM initiates a new STM instance, using snapshot isolation by default.
|
||||||
|
func NewSTM(c *v3.Client, apply func(STM) error, so ...stmOption) (*v3.TxnResponse, error) {
|
||||||
|
opts := &stmOptions{ctx: c.Ctx()}
|
||||||
|
for _, f := range so {
|
||||||
|
f(opts)
|
||||||
|
}
|
||||||
|
if len(opts.prefetch) != 0 {
|
||||||
|
f := apply
|
||||||
|
apply = func(s STM) error {
|
||||||
|
s.Get(opts.prefetch...)
|
||||||
|
return f(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return runSTM(mkSTM(c, opts), apply)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mkSTM(c *v3.Client, opts *stmOptions) STM {
|
||||||
|
switch opts.iso {
|
||||||
|
case SerializableSnapshot:
|
||||||
|
s := &stmSerializable{
|
||||||
|
stm: stm{client: c, ctx: opts.ctx},
|
||||||
|
prefetch: make(map[string]*v3.GetResponse),
|
||||||
|
}
|
||||||
|
s.conflicts = func() []v3.Cmp {
|
||||||
|
return append(s.rset.cmps(), s.wset.cmps(s.rset.first()+1)...)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
case Serializable:
|
||||||
|
s := &stmSerializable{
|
||||||
|
stm: stm{client: c, ctx: opts.ctx},
|
||||||
|
prefetch: make(map[string]*v3.GetResponse),
|
||||||
|
}
|
||||||
|
s.conflicts = func() []v3.Cmp { return s.rset.cmps() }
|
||||||
|
return s
|
||||||
|
case RepeatableReads:
|
||||||
|
s := &stm{client: c, ctx: opts.ctx, getOpts: []v3.OpOption{v3.WithSerializable()}}
|
||||||
|
s.conflicts = func() []v3.Cmp { return s.rset.cmps() }
|
||||||
|
return s
|
||||||
|
case ReadCommitted:
|
||||||
|
s := &stm{client: c, ctx: opts.ctx, getOpts: []v3.OpOption{v3.WithSerializable()}}
|
||||||
|
s.conflicts = func() []v3.Cmp { return nil }
|
||||||
|
return s
|
||||||
|
default:
|
||||||
|
panic("unsupported stm")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type stmResponse struct {
|
||||||
|
resp *v3.TxnResponse
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func runSTM(s STM, apply func(STM) error) (*v3.TxnResponse, error) {
|
||||||
|
outc := make(chan stmResponse, 1)
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
e, ok := r.(stmError)
|
||||||
|
if !ok {
|
||||||
|
// client apply panicked
|
||||||
|
panic(r)
|
||||||
|
}
|
||||||
|
outc <- stmResponse{nil, e.err}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
var out stmResponse
|
||||||
|
for {
|
||||||
|
s.reset()
|
||||||
|
if out.err = apply(s); out.err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if out.resp = s.commit(); out.resp != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outc <- out
|
||||||
|
}()
|
||||||
|
r := <-outc
|
||||||
|
return r.resp, r.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// stm implements repeatable-read software transactional memory over etcd
|
||||||
|
type stm struct {
|
||||||
|
client *v3.Client
|
||||||
|
ctx context.Context
|
||||||
|
// rset holds read key values and revisions
|
||||||
|
rset readSet
|
||||||
|
// wset holds overwritten keys and their values
|
||||||
|
wset writeSet
|
||||||
|
// getOpts are the opts used for gets
|
||||||
|
getOpts []v3.OpOption
|
||||||
|
// conflicts computes the current conflicts on the txn
|
||||||
|
conflicts func() []v3.Cmp
|
||||||
|
}
|
||||||
|
|
||||||
|
type stmPut struct {
|
||||||
|
val string
|
||||||
|
op v3.Op
|
||||||
|
}
|
||||||
|
|
||||||
|
type readSet map[string]*v3.GetResponse
|
||||||
|
|
||||||
|
func (rs readSet) add(keys []string, txnresp *v3.TxnResponse) {
|
||||||
|
for i, resp := range txnresp.Responses {
|
||||||
|
rs[keys[i]] = (*v3.GetResponse)(resp.GetResponseRange())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs readSet) first() int64 {
|
||||||
|
ret := int64(math.MaxInt64 - 1)
|
||||||
|
for _, resp := range rs {
|
||||||
|
if len(resp.Kvs) > 0 && resp.Kvs[0].ModRevision < ret {
|
||||||
|
ret = resp.Kvs[0].ModRevision
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// cmps guards the txn from updates to read set
|
||||||
|
func (rs readSet) cmps() []v3.Cmp {
|
||||||
|
cmps := make([]v3.Cmp, 0, len(rs))
|
||||||
|
for k, rk := range rs {
|
||||||
|
cmps = append(cmps, isKeyCurrent(k, rk))
|
||||||
|
}
|
||||||
|
return cmps
|
||||||
|
}
|
||||||
|
|
||||||
|
type writeSet map[string]stmPut
|
||||||
|
|
||||||
|
func (ws writeSet) get(keys ...string) *stmPut {
|
||||||
|
for _, key := range keys {
|
||||||
|
if wv, ok := ws[key]; ok {
|
||||||
|
return &wv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// cmps returns a cmp list testing no writes have happened past rev
|
||||||
|
func (ws writeSet) cmps(rev int64) []v3.Cmp {
|
||||||
|
cmps := make([]v3.Cmp, 0, len(ws))
|
||||||
|
for key := range ws {
|
||||||
|
cmps = append(cmps, v3.Compare(v3.ModRevision(key), "<", rev))
|
||||||
|
}
|
||||||
|
return cmps
|
||||||
|
}
|
||||||
|
|
||||||
|
// puts is the list of ops for all pending writes
|
||||||
|
func (ws writeSet) puts() []v3.Op {
|
||||||
|
puts := make([]v3.Op, 0, len(ws))
|
||||||
|
for _, v := range ws {
|
||||||
|
puts = append(puts, v.op)
|
||||||
|
}
|
||||||
|
return puts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stm) Get(keys ...string) string {
|
||||||
|
if wv := s.wset.get(keys...); wv != nil {
|
||||||
|
return wv.val
|
||||||
|
}
|
||||||
|
return respToValue(s.fetch(keys...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stm) Put(key, val string, opts ...v3.OpOption) {
|
||||||
|
s.wset[key] = stmPut{val, v3.OpPut(key, val, opts...)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stm) Del(key string) { s.wset[key] = stmPut{"", v3.OpDelete(key)} }
|
||||||
|
|
||||||
|
func (s *stm) Rev(key string) int64 {
|
||||||
|
if resp := s.fetch(key); resp != nil && len(resp.Kvs) != 0 {
|
||||||
|
return resp.Kvs[0].ModRevision
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stm) commit() *v3.TxnResponse {
|
||||||
|
txnresp, err := s.client.Txn(s.ctx).If(s.conflicts()...).Then(s.wset.puts()...).Commit()
|
||||||
|
if err != nil {
|
||||||
|
panic(stmError{err})
|
||||||
|
}
|
||||||
|
if txnresp.Succeeded {
|
||||||
|
return txnresp
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stm) fetch(keys ...string) *v3.GetResponse {
|
||||||
|
if len(keys) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ops := make([]v3.Op, len(keys))
|
||||||
|
for i, key := range keys {
|
||||||
|
if resp, ok := s.rset[key]; ok {
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
ops[i] = v3.OpGet(key, s.getOpts...)
|
||||||
|
}
|
||||||
|
txnresp, err := s.client.Txn(s.ctx).Then(ops...).Commit()
|
||||||
|
if err != nil {
|
||||||
|
panic(stmError{err})
|
||||||
|
}
|
||||||
|
s.rset.add(keys, txnresp)
|
||||||
|
return (*v3.GetResponse)(txnresp.Responses[0].GetResponseRange())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stm) reset() {
|
||||||
|
s.rset = make(map[string]*v3.GetResponse)
|
||||||
|
s.wset = make(map[string]stmPut)
|
||||||
|
}
|
||||||
|
|
||||||
|
type stmSerializable struct {
|
||||||
|
stm
|
||||||
|
prefetch map[string]*v3.GetResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stmSerializable) Get(keys ...string) string {
|
||||||
|
if wv := s.wset.get(keys...); wv != nil {
|
||||||
|
return wv.val
|
||||||
|
}
|
||||||
|
firstRead := len(s.rset) == 0
|
||||||
|
for _, key := range keys {
|
||||||
|
if resp, ok := s.prefetch[key]; ok {
|
||||||
|
delete(s.prefetch, key)
|
||||||
|
s.rset[key] = resp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp := s.stm.fetch(keys...)
|
||||||
|
if firstRead {
|
||||||
|
// txn's base revision is defined by the first read
|
||||||
|
s.getOpts = []v3.OpOption{
|
||||||
|
v3.WithRev(resp.Header.Revision),
|
||||||
|
v3.WithSerializable(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return respToValue(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stmSerializable) Rev(key string) int64 {
|
||||||
|
s.Get(key)
|
||||||
|
return s.stm.Rev(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stmSerializable) gets() ([]string, []v3.Op) {
|
||||||
|
keys := make([]string, 0, len(s.rset))
|
||||||
|
ops := make([]v3.Op, 0, len(s.rset))
|
||||||
|
for k := range s.rset {
|
||||||
|
keys = append(keys, k)
|
||||||
|
ops = append(ops, v3.OpGet(k))
|
||||||
|
}
|
||||||
|
return keys, ops
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stmSerializable) commit() *v3.TxnResponse {
|
||||||
|
keys, getops := s.gets()
|
||||||
|
txn := s.client.Txn(s.ctx).If(s.conflicts()...).Then(s.wset.puts()...)
|
||||||
|
// use Else to prefetch keys in case of conflict to save a round trip
|
||||||
|
txnresp, err := txn.Else(getops...).Commit()
|
||||||
|
if err != nil {
|
||||||
|
panic(stmError{err})
|
||||||
|
}
|
||||||
|
if txnresp.Succeeded {
|
||||||
|
return txnresp
|
||||||
|
}
|
||||||
|
// load prefetch with Else data
|
||||||
|
s.rset.add(keys, txnresp)
|
||||||
|
s.prefetch = s.rset
|
||||||
|
s.getOpts = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isKeyCurrent(k string, r *v3.GetResponse) v3.Cmp {
|
||||||
|
if len(r.Kvs) != 0 {
|
||||||
|
return v3.Compare(v3.ModRevision(k), "=", r.Kvs[0].ModRevision)
|
||||||
|
}
|
||||||
|
return v3.Compare(v3.ModRevision(k), "=", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func respToValue(resp *v3.GetResponse) string {
|
||||||
|
if resp == nil || len(resp.Kvs) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(resp.Kvs[0].Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSTMRepeatable is deprecated.
|
||||||
|
func NewSTMRepeatable(ctx context.Context, c *v3.Client, apply func(STM) error) (*v3.TxnResponse, error) {
|
||||||
|
return NewSTM(c, apply, WithAbortContext(ctx), WithIsolation(RepeatableReads))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSTMSerializable is deprecated.
|
||||||
|
func NewSTMSerializable(ctx context.Context, c *v3.Client, apply func(STM) error) (*v3.TxnResponse, error) {
|
||||||
|
return NewSTM(c, apply, WithAbortContext(ctx), WithIsolation(Serializable))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSTMReadCommitted is deprecated.
|
||||||
|
func NewSTMReadCommitted(ctx context.Context, c *v3.Client, apply func(STM) error) (*v3.TxnResponse, error) {
|
||||||
|
return NewSTM(c, apply, WithAbortContext(ctx), WithIsolation(ReadCommitted))
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
// Copyright 2016 The etcd Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package clientv3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
// Endpoints is a list of URLs.
|
||||||
|
Endpoints []string `json:"endpoints"`
|
||||||
|
|
||||||
|
// AutoSyncInterval is the interval to update endpoints with its latest members.
|
||||||
|
// 0 disables auto-sync. By default auto-sync is disabled.
|
||||||
|
AutoSyncInterval time.Duration `json:"auto-sync-interval"`
|
||||||
|
|
||||||
|
// DialTimeout is the timeout for failing to establish a connection.
|
||||||
|
DialTimeout time.Duration `json:"dial-timeout"`
|
||||||
|
|
||||||
|
// TLS holds the client secure credentials, if any.
|
||||||
|
TLS *tls.Config
|
||||||
|
|
||||||
|
// Username is a username for authentication.
|
||||||
|
Username string `json:"username"`
|
||||||
|
|
||||||
|
// Password is a password for authentication.
|
||||||
|
Password string `json:"password"`
|
||||||
|
|
||||||
|
// RejectOldCluster when set will refuse to create a client against an outdated cluster.
|
||||||
|
RejectOldCluster bool `json:"reject-old-cluster"`
|
||||||
|
|
||||||
|
// DialOptions is a list of dial options for the grpc client (e.g., for interceptors).
|
||||||
|
DialOptions []grpc.DialOption
|
||||||
|
|
||||||
|
// Context is the default client context; it can be used to cancel grpc dial out and
|
||||||
|
// other operations that do not have an explicit context.
|
||||||
|
Context context.Context
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
// Copyright 2016 The etcd Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// Package clientv3 implements the official Go etcd client for v3.
|
||||||
|
//
|
||||||
|
// Create client using `clientv3.New`:
|
||||||
|
//
|
||||||
|
// cli, err := clientv3.New(clientv3.Config{
|
||||||
|
// Endpoints: []string{"localhost:2379", "localhost:22379", "localhost:32379"},
|
||||||
|
// DialTimeout: 5 * time.Second,
|
||||||
|
// })
|
||||||
|
// if err != nil {
|
||||||
|
// // handle error!
|
||||||
|
// }
|
||||||
|
// defer cli.Close()
|
||||||
|
//
|
||||||
|
// Make sure to close the client after using it. If the client is not closed, the
|
||||||
|
// connection will have leaky goroutines.
|
||||||
|
//
|
||||||
|
// To specify client request timeout, pass context.WithTimeout to APIs:
|
||||||
|
//
|
||||||
|
// ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
// resp, err := kvc.Put(ctx, "sample_key", "sample_value")
|
||||||
|
// cancel()
|
||||||
|
// if err != nil {
|
||||||
|
// // handle error!
|
||||||
|
// }
|
||||||
|
// // use the response
|
||||||
|
//
|
||||||
|
// The Client has internal state (watchers and leases), so Clients should be reused instead of created as needed.
|
||||||
|
// Clients are safe for concurrent use by multiple goroutines.
|
||||||
|
//
|
||||||
|
// etcd client returns 2 types of errors:
|
||||||
|
//
|
||||||
|
// 1. context error: canceled or deadline exceeded.
|
||||||
|
// 2. gRPC error: see https://github.com/coreos/etcd/blob/master/etcdserver/api/v3rpc/rpctypes/error.go
|
||||||
|
//
|
||||||
|
// Here is the example code to handle client errors:
|
||||||
|
//
|
||||||
|
// resp, err := kvc.Put(ctx, "", "")
|
||||||
|
// if err != nil {
|
||||||
|
// if err == context.Canceled {
|
||||||
|
// // ctx is canceled by another routine
|
||||||
|
// } else if err == context.DeadlineExceeded {
|
||||||
|
// // ctx is attached with a deadline and it exceeded
|
||||||
|
// } else if verr, ok := err.(*v3rpc.ErrEmptyKey); ok {
|
||||||
|
// // process (verr.Errors)
|
||||||
|
// } else {
|
||||||
|
// // bad cluster endpoints, which are not etcd servers
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
package clientv3
|
|
@ -0,0 +1,162 @@
|
||||||
|
// Copyright 2015 The etcd Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package clientv3
|
||||||
|
|
||||||
|
import (
|
||||||
|
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
CompactResponse pb.CompactionResponse
|
||||||
|
PutResponse pb.PutResponse
|
||||||
|
GetResponse pb.RangeResponse
|
||||||
|
DeleteResponse pb.DeleteRangeResponse
|
||||||
|
TxnResponse pb.TxnResponse
|
||||||
|
)
|
||||||
|
|
||||||
|
type KV interface {
|
||||||
|
// Put puts a key-value pair into etcd.
|
||||||
|
// Note that key,value can be plain bytes array and string is
|
||||||
|
// an immutable representation of that bytes array.
|
||||||
|
// To get a string of bytes, do string([]byte{0x10, 0x20}).
|
||||||
|
Put(ctx context.Context, key, val string, opts ...OpOption) (*PutResponse, error)
|
||||||
|
|
||||||
|
// Get retrieves keys.
|
||||||
|
// By default, Get will return the value for "key", if any.
|
||||||
|
// When passed WithRange(end), Get will return the keys in the range [key, end).
|
||||||
|
// When passed WithFromKey(), Get returns keys greater than or equal to key.
|
||||||
|
// When passed WithRev(rev) with rev > 0, Get retrieves keys at the given revision;
|
||||||
|
// if the required revision is compacted, the request will fail with ErrCompacted .
|
||||||
|
// When passed WithLimit(limit), the number of returned keys is bounded by limit.
|
||||||
|
// When passed WithSort(), the keys will be sorted.
|
||||||
|
Get(ctx context.Context, key string, opts ...OpOption) (*GetResponse, error)
|
||||||
|
|
||||||
|
// Delete deletes a key, or optionally using WithRange(end), [key, end).
|
||||||
|
Delete(ctx context.Context, key string, opts ...OpOption) (*DeleteResponse, error)
|
||||||
|
|
||||||
|
// Compact compacts etcd KV history before the given rev.
|
||||||
|
Compact(ctx context.Context, rev int64, opts ...CompactOption) (*CompactResponse, error)
|
||||||
|
|
||||||
|
// Do applies a single Op on KV without a transaction.
|
||||||
|
// Do is useful when creating arbitrary operations to be issued at a
|
||||||
|
// later time; the user can range over the operations, calling Do to
|
||||||
|
// execute them. Get/Put/Delete, on the other hand, are best suited
|
||||||
|
// for when the operation should be issued at the time of declaration.
|
||||||
|
Do(ctx context.Context, op Op) (OpResponse, error)
|
||||||
|
|
||||||
|
// Txn creates a transaction.
|
||||||
|
Txn(ctx context.Context) Txn
|
||||||
|
}
|
||||||
|
|
||||||
|
type OpResponse struct {
|
||||||
|
put *PutResponse
|
||||||
|
get *GetResponse
|
||||||
|
del *DeleteResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
func (op OpResponse) Put() *PutResponse { return op.put }
|
||||||
|
func (op OpResponse) Get() *GetResponse { return op.get }
|
||||||
|
func (op OpResponse) Del() *DeleteResponse { return op.del }
|
||||||
|
|
||||||
|
type kv struct {
|
||||||
|
remote pb.KVClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewKV(c *Client) KV {
|
||||||
|
return &kv{remote: RetryKVClient(c)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewKVFromKVClient(remote pb.KVClient) KV {
|
||||||
|
return &kv{remote: remote}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *kv) Put(ctx context.Context, key, val string, opts ...OpOption) (*PutResponse, error) {
|
||||||
|
r, err := kv.Do(ctx, OpPut(key, val, opts...))
|
||||||
|
return r.put, toErr(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *kv) Get(ctx context.Context, key string, opts ...OpOption) (*GetResponse, error) {
|
||||||
|
r, err := kv.Do(ctx, OpGet(key, opts...))
|
||||||
|
return r.get, toErr(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *kv) Delete(ctx context.Context, key string, opts ...OpOption) (*DeleteResponse, error) {
|
||||||
|
r, err := kv.Do(ctx, OpDelete(key, opts...))
|
||||||
|
return r.del, toErr(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *kv) Compact(ctx context.Context, rev int64, opts ...CompactOption) (*CompactResponse, error) {
|
||||||
|
resp, err := kv.remote.Compact(ctx, OpCompact(rev, opts...).toRequest())
|
||||||
|
if err != nil {
|
||||||
|
return nil, toErr(ctx, err)
|
||||||
|
}
|
||||||
|
return (*CompactResponse)(resp), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *kv) Txn(ctx context.Context) Txn {
|
||||||
|
return &txn{
|
||||||
|
kv: kv,
|
||||||
|
ctx: ctx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *kv) Do(ctx context.Context, op Op) (OpResponse, error) {
|
||||||
|
for {
|
||||||
|
resp, err := kv.do(ctx, op)
|
||||||
|
if err == nil {
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if isHaltErr(ctx, err) {
|
||||||
|
return resp, toErr(ctx, err)
|
||||||
|
}
|
||||||
|
// do not retry on modifications
|
||||||
|
if op.isWrite() {
|
||||||
|
return resp, toErr(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *kv) do(ctx context.Context, op Op) (OpResponse, error) {
|
||||||
|
var err error
|
||||||
|
switch op.t {
|
||||||
|
// TODO: handle other ops
|
||||||
|
case tRange:
|
||||||
|
var resp *pb.RangeResponse
|
||||||
|
resp, err = kv.remote.Range(ctx, op.toRangeRequest(), grpc.FailFast(false))
|
||||||
|
if err == nil {
|
||||||
|
return OpResponse{get: (*GetResponse)(resp)}, nil
|
||||||
|
}
|
||||||
|
case tPut:
|
||||||
|
var resp *pb.PutResponse
|
||||||
|
r := &pb.PutRequest{Key: op.key, Value: op.val, Lease: int64(op.leaseID), PrevKv: op.prevKV, IgnoreValue: op.ignoreValue, IgnoreLease: op.ignoreLease}
|
||||||
|
resp, err = kv.remote.Put(ctx, r)
|
||||||
|
if err == nil {
|
||||||
|
return OpResponse{put: (*PutResponse)(resp)}, nil
|
||||||
|
}
|
||||||
|
case tDeleteRange:
|
||||||
|
var resp *pb.DeleteRangeResponse
|
||||||
|
r := &pb.DeleteRangeRequest{Key: op.key, RangeEnd: op.end, PrevKv: op.prevKV}
|
||||||
|
resp, err = kv.remote.DeleteRange(ctx, r)
|
||||||
|
if err == nil {
|
||||||
|
return OpResponse{del: (*DeleteResponse)(resp)}, nil
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
panic("Unknown op")
|
||||||
|
}
|
||||||
|
return OpResponse{}, err
|
||||||
|
}
|
|
@ -0,0 +1,547 @@
|
||||||
|
// Copyright 2016 The etcd Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package clientv3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
|
||||||
|
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
LeaseRevokeResponse pb.LeaseRevokeResponse
|
||||||
|
LeaseID int64
|
||||||
|
)
|
||||||
|
|
||||||
|
// LeaseGrantResponse is used to convert the protobuf grant response.
|
||||||
|
type LeaseGrantResponse struct {
|
||||||
|
*pb.ResponseHeader
|
||||||
|
ID LeaseID
|
||||||
|
TTL int64
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
|
||||||
|
// LeaseKeepAliveResponse is used to convert the protobuf keepalive response.
|
||||||
|
type LeaseKeepAliveResponse struct {
|
||||||
|
*pb.ResponseHeader
|
||||||
|
ID LeaseID
|
||||||
|
TTL int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// LeaseTimeToLiveResponse is used to convert the protobuf lease timetolive response.
|
||||||
|
type LeaseTimeToLiveResponse struct {
|
||||||
|
*pb.ResponseHeader
|
||||||
|
ID LeaseID `json:"id"`
|
||||||
|
|
||||||
|
// TTL is the remaining TTL in seconds for the lease; the lease will expire in under TTL+1 seconds.
|
||||||
|
TTL int64 `json:"ttl"`
|
||||||
|
|
||||||
|
// GrantedTTL is the initial granted time in seconds upon lease creation/renewal.
|
||||||
|
GrantedTTL int64 `json:"granted-ttl"`
|
||||||
|
|
||||||
|
// Keys is the list of keys attached to this lease.
|
||||||
|
Keys [][]byte `json:"keys"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// defaultTTL is the assumed lease TTL used for the first keepalive
|
||||||
|
// deadline before the actual TTL is known to the client.
|
||||||
|
defaultTTL = 5 * time.Second
|
||||||
|
// a small buffer to store unsent lease responses.
|
||||||
|
leaseResponseChSize = 16
|
||||||
|
// NoLease is a lease ID for the absence of a lease.
|
||||||
|
NoLease LeaseID = 0
|
||||||
|
|
||||||
|
// retryConnWait is how long to wait before retrying request due to an error
|
||||||
|
retryConnWait = 500 * time.Millisecond
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrKeepAliveHalted is returned if client keep alive loop halts with an unexpected error.
|
||||||
|
//
|
||||||
|
// This usually means that automatic lease renewal via KeepAlive is broken, but KeepAliveOnce will still work as expected.
|
||||||
|
type ErrKeepAliveHalted struct {
|
||||||
|
Reason error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrKeepAliveHalted) Error() string {
|
||||||
|
s := "etcdclient: leases keep alive halted"
|
||||||
|
if e.Reason != nil {
|
||||||
|
s += ": " + e.Reason.Error()
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
type Lease interface {
|
||||||
|
// Grant creates a new lease.
|
||||||
|
Grant(ctx context.Context, ttl int64) (*LeaseGrantResponse, error)
|
||||||
|
|
||||||
|
// Revoke revokes the given lease.
|
||||||
|
Revoke(ctx context.Context, id LeaseID) (*LeaseRevokeResponse, error)
|
||||||
|
|
||||||
|
// TimeToLive retrieves the lease information of the given lease ID.
|
||||||
|
TimeToLive(ctx context.Context, id LeaseID, opts ...LeaseOption) (*LeaseTimeToLiveResponse, error)
|
||||||
|
|
||||||
|
// KeepAlive keeps the given lease alive forever.
|
||||||
|
KeepAlive(ctx context.Context, id LeaseID) (<-chan *LeaseKeepAliveResponse, error)
|
||||||
|
|
||||||
|
// KeepAliveOnce renews the lease once. In most of the cases, Keepalive
|
||||||
|
// should be used instead of KeepAliveOnce.
|
||||||
|
KeepAliveOnce(ctx context.Context, id LeaseID) (*LeaseKeepAliveResponse, error)
|
||||||
|
|
||||||
|
// Close releases all resources Lease keeps for efficient communication
|
||||||
|
// with the etcd server.
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type lessor struct {
|
||||||
|
mu sync.Mutex // guards all fields
|
||||||
|
|
||||||
|
// donec is closed and loopErr is set when recvKeepAliveLoop stops
|
||||||
|
donec chan struct{}
|
||||||
|
loopErr error
|
||||||
|
|
||||||
|
remote pb.LeaseClient
|
||||||
|
|
||||||
|
stream pb.Lease_LeaseKeepAliveClient
|
||||||
|
streamCancel context.CancelFunc
|
||||||
|
|
||||||
|
stopCtx context.Context
|
||||||
|
stopCancel context.CancelFunc
|
||||||
|
|
||||||
|
keepAlives map[LeaseID]*keepAlive
|
||||||
|
|
||||||
|
// firstKeepAliveTimeout is the timeout for the first keepalive request
|
||||||
|
// before the actual TTL is known to the lease client
|
||||||
|
firstKeepAliveTimeout time.Duration
|
||||||
|
|
||||||
|
// firstKeepAliveOnce ensures stream starts after first KeepAlive call.
|
||||||
|
firstKeepAliveOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
// keepAlive multiplexes a keepalive for a lease over multiple channels
|
||||||
|
type keepAlive struct {
|
||||||
|
chs []chan<- *LeaseKeepAliveResponse
|
||||||
|
ctxs []context.Context
|
||||||
|
// deadline is the time the keep alive channels close if no response
|
||||||
|
deadline time.Time
|
||||||
|
// nextKeepAlive is when to send the next keep alive message
|
||||||
|
nextKeepAlive time.Time
|
||||||
|
// donec is closed on lease revoke, expiration, or cancel.
|
||||||
|
donec chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLease(c *Client) Lease {
|
||||||
|
return NewLeaseFromLeaseClient(RetryLeaseClient(c), c.cfg.DialTimeout+time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLeaseFromLeaseClient(remote pb.LeaseClient, keepAliveTimeout time.Duration) Lease {
|
||||||
|
l := &lessor{
|
||||||
|
donec: make(chan struct{}),
|
||||||
|
keepAlives: make(map[LeaseID]*keepAlive),
|
||||||
|
remote: remote,
|
||||||
|
firstKeepAliveTimeout: keepAliveTimeout,
|
||||||
|
}
|
||||||
|
if l.firstKeepAliveTimeout == time.Second {
|
||||||
|
l.firstKeepAliveTimeout = defaultTTL
|
||||||
|
}
|
||||||
|
reqLeaderCtx := WithRequireLeader(context.Background())
|
||||||
|
l.stopCtx, l.stopCancel = context.WithCancel(reqLeaderCtx)
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *lessor) Grant(ctx context.Context, ttl int64) (*LeaseGrantResponse, error) {
|
||||||
|
for {
|
||||||
|
r := &pb.LeaseGrantRequest{TTL: ttl}
|
||||||
|
resp, err := l.remote.LeaseGrant(ctx, r)
|
||||||
|
if err == nil {
|
||||||
|
gresp := &LeaseGrantResponse{
|
||||||
|
ResponseHeader: resp.GetHeader(),
|
||||||
|
ID: LeaseID(resp.ID),
|
||||||
|
TTL: resp.TTL,
|
||||||
|
Error: resp.Error,
|
||||||
|
}
|
||||||
|
return gresp, nil
|
||||||
|
}
|
||||||
|
if isHaltErr(ctx, err) {
|
||||||
|
return nil, toErr(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *lessor) Revoke(ctx context.Context, id LeaseID) (*LeaseRevokeResponse, error) {
|
||||||
|
for {
|
||||||
|
r := &pb.LeaseRevokeRequest{ID: int64(id)}
|
||||||
|
resp, err := l.remote.LeaseRevoke(ctx, r)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return (*LeaseRevokeResponse)(resp), nil
|
||||||
|
}
|
||||||
|
if isHaltErr(ctx, err) {
|
||||||
|
return nil, toErr(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *lessor) TimeToLive(ctx context.Context, id LeaseID, opts ...LeaseOption) (*LeaseTimeToLiveResponse, error) {
|
||||||
|
for {
|
||||||
|
r := toLeaseTimeToLiveRequest(id, opts...)
|
||||||
|
resp, err := l.remote.LeaseTimeToLive(ctx, r, grpc.FailFast(false))
|
||||||
|
if err == nil {
|
||||||
|
gresp := &LeaseTimeToLiveResponse{
|
||||||
|
ResponseHeader: resp.GetHeader(),
|
||||||
|
ID: LeaseID(resp.ID),
|
||||||
|
TTL: resp.TTL,
|
||||||
|
GrantedTTL: resp.GrantedTTL,
|
||||||
|
Keys: resp.Keys,
|
||||||
|
}
|
||||||
|
return gresp, nil
|
||||||
|
}
|
||||||
|
if isHaltErr(ctx, err) {
|
||||||
|
return nil, toErr(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *lessor) KeepAlive(ctx context.Context, id LeaseID) (<-chan *LeaseKeepAliveResponse, error) {
|
||||||
|
ch := make(chan *LeaseKeepAliveResponse, leaseResponseChSize)
|
||||||
|
|
||||||
|
l.mu.Lock()
|
||||||
|
// ensure that recvKeepAliveLoop is still running
|
||||||
|
select {
|
||||||
|
case <-l.donec:
|
||||||
|
err := l.loopErr
|
||||||
|
l.mu.Unlock()
|
||||||
|
close(ch)
|
||||||
|
return ch, ErrKeepAliveHalted{Reason: err}
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
ka, ok := l.keepAlives[id]
|
||||||
|
if !ok {
|
||||||
|
// create fresh keep alive
|
||||||
|
ka = &keepAlive{
|
||||||
|
chs: []chan<- *LeaseKeepAliveResponse{ch},
|
||||||
|
ctxs: []context.Context{ctx},
|
||||||
|
deadline: time.Now().Add(l.firstKeepAliveTimeout),
|
||||||
|
nextKeepAlive: time.Now(),
|
||||||
|
donec: make(chan struct{}),
|
||||||
|
}
|
||||||
|
l.keepAlives[id] = ka
|
||||||
|
} else {
|
||||||
|
// add channel and context to existing keep alive
|
||||||
|
ka.ctxs = append(ka.ctxs, ctx)
|
||||||
|
ka.chs = append(ka.chs, ch)
|
||||||
|
}
|
||||||
|
l.mu.Unlock()
|
||||||
|
|
||||||
|
go l.keepAliveCtxCloser(id, ctx, ka.donec)
|
||||||
|
l.firstKeepAliveOnce.Do(func() {
|
||||||
|
go l.recvKeepAliveLoop()
|
||||||
|
go l.deadlineLoop()
|
||||||
|
})
|
||||||
|
|
||||||
|
return ch, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *lessor) KeepAliveOnce(ctx context.Context, id LeaseID) (*LeaseKeepAliveResponse, error) {
|
||||||
|
for {
|
||||||
|
resp, err := l.keepAliveOnce(ctx, id)
|
||||||
|
if err == nil {
|
||||||
|
if resp.TTL <= 0 {
|
||||||
|
err = rpctypes.ErrLeaseNotFound
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
if isHaltErr(ctx, err) {
|
||||||
|
return nil, toErr(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *lessor) Close() error {
|
||||||
|
l.stopCancel()
|
||||||
|
// close for synchronous teardown if stream goroutines never launched
|
||||||
|
l.firstKeepAliveOnce.Do(func() { close(l.donec) })
|
||||||
|
<-l.donec
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *lessor) keepAliveCtxCloser(id LeaseID, ctx context.Context, donec <-chan struct{}) {
|
||||||
|
select {
|
||||||
|
case <-donec:
|
||||||
|
return
|
||||||
|
case <-l.donec:
|
||||||
|
return
|
||||||
|
case <-ctx.Done():
|
||||||
|
}
|
||||||
|
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
|
ka, ok := l.keepAlives[id]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// close channel and remove context if still associated with keep alive
|
||||||
|
for i, c := range ka.ctxs {
|
||||||
|
if c == ctx {
|
||||||
|
close(ka.chs[i])
|
||||||
|
ka.ctxs = append(ka.ctxs[:i], ka.ctxs[i+1:]...)
|
||||||
|
ka.chs = append(ka.chs[:i], ka.chs[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// remove if no one more listeners
|
||||||
|
if len(ka.chs) == 0 {
|
||||||
|
delete(l.keepAlives, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// closeRequireLeader scans all keep alives for ctxs that have require leader
|
||||||
|
// and closes the associated channels.
|
||||||
|
func (l *lessor) closeRequireLeader() {
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
for _, ka := range l.keepAlives {
|
||||||
|
reqIdxs := 0
|
||||||
|
// find all required leader channels, close, mark as nil
|
||||||
|
for i, ctx := range ka.ctxs {
|
||||||
|
md, ok := metadata.FromContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ks := md[rpctypes.MetadataRequireLeaderKey]
|
||||||
|
if len(ks) < 1 || ks[0] != rpctypes.MetadataHasLeader {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
close(ka.chs[i])
|
||||||
|
ka.chs[i] = nil
|
||||||
|
reqIdxs++
|
||||||
|
}
|
||||||
|
if reqIdxs == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// remove all channels that required a leader from keepalive
|
||||||
|
newChs := make([]chan<- *LeaseKeepAliveResponse, len(ka.chs)-reqIdxs)
|
||||||
|
newCtxs := make([]context.Context, len(newChs))
|
||||||
|
newIdx := 0
|
||||||
|
for i := range ka.chs {
|
||||||
|
if ka.chs[i] == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newChs[newIdx], newCtxs[newIdx] = ka.chs[i], ka.ctxs[newIdx]
|
||||||
|
newIdx++
|
||||||
|
}
|
||||||
|
ka.chs, ka.ctxs = newChs, newCtxs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *lessor) keepAliveOnce(ctx context.Context, id LeaseID) (*LeaseKeepAliveResponse, error) {
|
||||||
|
cctx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
stream, err := l.remote.LeaseKeepAlive(cctx, grpc.FailFast(false))
|
||||||
|
if err != nil {
|
||||||
|
return nil, toErr(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = stream.Send(&pb.LeaseKeepAliveRequest{ID: int64(id)})
|
||||||
|
if err != nil {
|
||||||
|
return nil, toErr(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, rerr := stream.Recv()
|
||||||
|
if rerr != nil {
|
||||||
|
return nil, toErr(ctx, rerr)
|
||||||
|
}
|
||||||
|
|
||||||
|
karesp := &LeaseKeepAliveResponse{
|
||||||
|
ResponseHeader: resp.GetHeader(),
|
||||||
|
ID: LeaseID(resp.ID),
|
||||||
|
TTL: resp.TTL,
|
||||||
|
}
|
||||||
|
return karesp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *lessor) recvKeepAliveLoop() (gerr error) {
|
||||||
|
defer func() {
|
||||||
|
l.mu.Lock()
|
||||||
|
close(l.donec)
|
||||||
|
l.loopErr = gerr
|
||||||
|
for _, ka := range l.keepAlives {
|
||||||
|
ka.Close()
|
||||||
|
}
|
||||||
|
l.keepAlives = make(map[LeaseID]*keepAlive)
|
||||||
|
l.mu.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
stream, err := l.resetRecv()
|
||||||
|
if err != nil {
|
||||||
|
if canceledByCaller(l.stopCtx, err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for {
|
||||||
|
resp, err := stream.Recv()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if canceledByCaller(l.stopCtx, err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if toErr(l.stopCtx, err) == rpctypes.ErrNoLeader {
|
||||||
|
l.closeRequireLeader()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
l.recvKeepAlive(resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(retryConnWait):
|
||||||
|
continue
|
||||||
|
case <-l.stopCtx.Done():
|
||||||
|
return l.stopCtx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// resetRecv opens a new lease stream and starts sending LeaseKeepAliveRequests
|
||||||
|
func (l *lessor) resetRecv() (pb.Lease_LeaseKeepAliveClient, error) {
|
||||||
|
sctx, cancel := context.WithCancel(l.stopCtx)
|
||||||
|
stream, err := l.remote.LeaseKeepAlive(sctx, grpc.FailFast(false))
|
||||||
|
if err != nil {
|
||||||
|
cancel()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
if l.stream != nil && l.streamCancel != nil {
|
||||||
|
l.streamCancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
l.streamCancel = cancel
|
||||||
|
l.stream = stream
|
||||||
|
|
||||||
|
go l.sendKeepAliveLoop(stream)
|
||||||
|
return stream, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// recvKeepAlive updates a lease based on its LeaseKeepAliveResponse
|
||||||
|
func (l *lessor) recvKeepAlive(resp *pb.LeaseKeepAliveResponse) {
|
||||||
|
karesp := &LeaseKeepAliveResponse{
|
||||||
|
ResponseHeader: resp.GetHeader(),
|
||||||
|
ID: LeaseID(resp.ID),
|
||||||
|
TTL: resp.TTL,
|
||||||
|
}
|
||||||
|
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
|
ka, ok := l.keepAlives[karesp.ID]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if karesp.TTL <= 0 {
|
||||||
|
// lease expired; close all keep alive channels
|
||||||
|
delete(l.keepAlives, karesp.ID)
|
||||||
|
ka.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// send update to all channels
|
||||||
|
nextKeepAlive := time.Now().Add((time.Duration(karesp.TTL) * time.Second) / 3.0)
|
||||||
|
ka.deadline = time.Now().Add(time.Duration(karesp.TTL) * time.Second)
|
||||||
|
for _, ch := range ka.chs {
|
||||||
|
select {
|
||||||
|
case ch <- karesp:
|
||||||
|
ka.nextKeepAlive = nextKeepAlive
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// deadlineLoop reaps any keep alive channels that have not received a response
|
||||||
|
// within the lease TTL
|
||||||
|
func (l *lessor) deadlineLoop() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
case <-l.donec:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
l.mu.Lock()
|
||||||
|
for id, ka := range l.keepAlives {
|
||||||
|
if ka.deadline.Before(now) {
|
||||||
|
// waited too long for response; lease may be expired
|
||||||
|
ka.Close()
|
||||||
|
delete(l.keepAlives, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
l.mu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendKeepAliveLoop sends LeaseKeepAliveRequests for the lifetime of a lease stream
|
||||||
|
func (l *lessor) sendKeepAliveLoop(stream pb.Lease_LeaseKeepAliveClient) {
|
||||||
|
for {
|
||||||
|
var tosend []LeaseID
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
l.mu.Lock()
|
||||||
|
for id, ka := range l.keepAlives {
|
||||||
|
if ka.nextKeepAlive.Before(now) {
|
||||||
|
tosend = append(tosend, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
l.mu.Unlock()
|
||||||
|
|
||||||
|
for _, id := range tosend {
|
||||||
|
r := &pb.LeaseKeepAliveRequest{ID: int64(id)}
|
||||||
|
if err := stream.Send(r); err != nil {
|
||||||
|
// TODO do something with this error?
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(500 * time.Millisecond):
|
||||||
|
case <-stream.Context().Done():
|
||||||
|
return
|
||||||
|
case <-l.donec:
|
||||||
|
return
|
||||||
|
case <-l.stopCtx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ka *keepAlive) Close() {
|
||||||
|
close(ka.donec)
|
||||||
|
for _, ch := range ka.chs {
|
||||||
|
close(ch)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
// Copyright 2016 The etcd Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package clientv3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"google.golang.org/grpc/grpclog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logger is the logger used by client library.
|
||||||
|
// It implements grpclog.Logger interface.
|
||||||
|
type Logger grpclog.Logger
|
||||||
|
|
||||||
|
var (
|
||||||
|
logger settableLogger
|
||||||
|
)
|
||||||
|
|
||||||
|
type settableLogger struct {
|
||||||
|
l grpclog.Logger
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// disable client side logs by default
|
||||||
|
logger.mu.Lock()
|
||||||
|
logger.l = log.New(ioutil.Discard, "", 0)
|
||||||
|
|
||||||
|
// logger has to override the grpclog at initialization so that
|
||||||
|
// any changes to the grpclog go through logger with locking
|
||||||
|
// instead of through SetLogger
|
||||||
|
//
|
||||||
|
// now updates only happen through settableLogger.set
|
||||||
|
grpclog.SetLogger(&logger)
|
||||||
|
logger.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLogger sets client-side Logger. By default, logs are disabled.
|
||||||
|
func SetLogger(l Logger) {
|
||||||
|
logger.set(l)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLogger returns the current logger.
|
||||||
|
func GetLogger() Logger {
|
||||||
|
return logger.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *settableLogger) set(l Logger) {
|
||||||
|
s.mu.Lock()
|
||||||
|
logger.l = l
|
||||||
|
s.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *settableLogger) get() Logger {
|
||||||
|
s.mu.RLock()
|
||||||
|
l := logger.l
|
||||||
|
s.mu.RUnlock()
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
// implement the grpclog.Logger interface
|
||||||
|
|
||||||
|
func (s *settableLogger) Fatal(args ...interface{}) { s.get().Fatal(args...) }
|
||||||
|
func (s *settableLogger) Fatalf(format string, args ...interface{}) { s.get().Fatalf(format, args...) }
|
||||||
|
func (s *settableLogger) Fatalln(args ...interface{}) { s.get().Fatalln(args...) }
|
||||||
|
func (s *settableLogger) Print(args ...interface{}) { s.get().Print(args...) }
|
||||||
|
func (s *settableLogger) Printf(format string, args ...interface{}) { s.get().Printf(format, args...) }
|
||||||
|
func (s *settableLogger) Println(args ...interface{}) { s.get().Println(args...) }
|
|
@ -0,0 +1,182 @@
|
||||||
|
// Copyright 2016 The etcd Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package clientv3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
DefragmentResponse pb.DefragmentResponse
|
||||||
|
AlarmResponse pb.AlarmResponse
|
||||||
|
AlarmMember pb.AlarmMember
|
||||||
|
StatusResponse pb.StatusResponse
|
||||||
|
)
|
||||||
|
|
||||||
|
type Maintenance interface {
|
||||||
|
// AlarmList gets all active alarms.
|
||||||
|
AlarmList(ctx context.Context) (*AlarmResponse, error)
|
||||||
|
|
||||||
|
// AlarmDisarm disarms a given alarm.
|
||||||
|
AlarmDisarm(ctx context.Context, m *AlarmMember) (*AlarmResponse, error)
|
||||||
|
|
||||||
|
// Defragment defragments storage backend of the etcd member with given endpoint.
|
||||||
|
// Defragment is only needed when deleting a large number of keys and want to reclaim
|
||||||
|
// the resources.
|
||||||
|
// Defragment is an expensive operation. User should avoid defragmenting multiple members
|
||||||
|
// at the same time.
|
||||||
|
// To defragment multiple members in the cluster, user need to call defragment multiple
|
||||||
|
// times with different endpoints.
|
||||||
|
Defragment(ctx context.Context, endpoint string) (*DefragmentResponse, error)
|
||||||
|
|
||||||
|
// Status gets the status of the endpoint.
|
||||||
|
Status(ctx context.Context, endpoint string) (*StatusResponse, error)
|
||||||
|
|
||||||
|
// Snapshot provides a reader for a snapshot of a backend.
|
||||||
|
Snapshot(ctx context.Context) (io.ReadCloser, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type maintenance struct {
|
||||||
|
dial func(endpoint string) (pb.MaintenanceClient, func(), error)
|
||||||
|
remote pb.MaintenanceClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMaintenance(c *Client) Maintenance {
|
||||||
|
return &maintenance{
|
||||||
|
dial: func(endpoint string) (pb.MaintenanceClient, func(), error) {
|
||||||
|
conn, err := c.dial(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
cancel := func() { conn.Close() }
|
||||||
|
return pb.NewMaintenanceClient(conn), cancel, nil
|
||||||
|
},
|
||||||
|
remote: pb.NewMaintenanceClient(c.conn),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMaintenanceFromMaintenanceClient(remote pb.MaintenanceClient) Maintenance {
|
||||||
|
return &maintenance{
|
||||||
|
dial: func(string) (pb.MaintenanceClient, func(), error) {
|
||||||
|
return remote, func() {}, nil
|
||||||
|
},
|
||||||
|
remote: remote,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *maintenance) AlarmList(ctx context.Context) (*AlarmResponse, error) {
|
||||||
|
req := &pb.AlarmRequest{
|
||||||
|
Action: pb.AlarmRequest_GET,
|
||||||
|
MemberID: 0, // all
|
||||||
|
Alarm: pb.AlarmType_NONE, // all
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
resp, err := m.remote.Alarm(ctx, req, grpc.FailFast(false))
|
||||||
|
if err == nil {
|
||||||
|
return (*AlarmResponse)(resp), nil
|
||||||
|
}
|
||||||
|
if isHaltErr(ctx, err) {
|
||||||
|
return nil, toErr(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *maintenance) AlarmDisarm(ctx context.Context, am *AlarmMember) (*AlarmResponse, error) {
|
||||||
|
req := &pb.AlarmRequest{
|
||||||
|
Action: pb.AlarmRequest_DEACTIVATE,
|
||||||
|
MemberID: am.MemberID,
|
||||||
|
Alarm: am.Alarm,
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.MemberID == 0 && req.Alarm == pb.AlarmType_NONE {
|
||||||
|
ar, err := m.AlarmList(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, toErr(ctx, err)
|
||||||
|
}
|
||||||
|
ret := AlarmResponse{}
|
||||||
|
for _, am := range ar.Alarms {
|
||||||
|
dresp, derr := m.AlarmDisarm(ctx, (*AlarmMember)(am))
|
||||||
|
if derr != nil {
|
||||||
|
return nil, toErr(ctx, derr)
|
||||||
|
}
|
||||||
|
ret.Alarms = append(ret.Alarms, dresp.Alarms...)
|
||||||
|
}
|
||||||
|
return &ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := m.remote.Alarm(ctx, req, grpc.FailFast(false))
|
||||||
|
if err == nil {
|
||||||
|
return (*AlarmResponse)(resp), nil
|
||||||
|
}
|
||||||
|
return nil, toErr(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *maintenance) Defragment(ctx context.Context, endpoint string) (*DefragmentResponse, error) {
|
||||||
|
remote, cancel, err := m.dial(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, toErr(ctx, err)
|
||||||
|
}
|
||||||
|
defer cancel()
|
||||||
|
resp, err := remote.Defragment(ctx, &pb.DefragmentRequest{}, grpc.FailFast(false))
|
||||||
|
if err != nil {
|
||||||
|
return nil, toErr(ctx, err)
|
||||||
|
}
|
||||||
|
return (*DefragmentResponse)(resp), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *maintenance) Status(ctx context.Context, endpoint string) (*StatusResponse, error) {
|
||||||
|
remote, cancel, err := m.dial(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, toErr(ctx, err)
|
||||||
|
}
|
||||||
|
defer cancel()
|
||||||
|
resp, err := remote.Status(ctx, &pb.StatusRequest{}, grpc.FailFast(false))
|
||||||
|
if err != nil {
|
||||||
|
return nil, toErr(ctx, err)
|
||||||
|
}
|
||||||
|
return (*StatusResponse)(resp), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *maintenance) Snapshot(ctx context.Context) (io.ReadCloser, error) {
|
||||||
|
ss, err := m.remote.Snapshot(ctx, &pb.SnapshotRequest{}, grpc.FailFast(false))
|
||||||
|
if err != nil {
|
||||||
|
return nil, toErr(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pr, pw := io.Pipe()
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
resp, err := ss.Recv()
|
||||||
|
if err != nil {
|
||||||
|
pw.CloseWithError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if resp == nil && err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if _, werr := pw.Write(resp.Blob); werr != nil {
|
||||||
|
pw.CloseWithError(werr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pw.Close()
|
||||||
|
}()
|
||||||
|
return pr, nil
|
||||||
|
}
|
|
@ -0,0 +1,437 @@
|
||||||
|
// Copyright 2016 The etcd Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package clientv3
|
||||||
|
|
||||||
|
import pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||||
|
|
||||||
|
type opType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// A default Op has opType 0, which is invalid.
|
||||||
|
tRange opType = iota + 1
|
||||||
|
tPut
|
||||||
|
tDeleteRange
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
noPrefixEnd = []byte{0}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Op represents an Operation that kv can execute.
|
||||||
|
type Op struct {
|
||||||
|
t opType
|
||||||
|
key []byte
|
||||||
|
end []byte
|
||||||
|
|
||||||
|
// for range
|
||||||
|
limit int64
|
||||||
|
sort *SortOption
|
||||||
|
serializable bool
|
||||||
|
keysOnly bool
|
||||||
|
countOnly bool
|
||||||
|
minModRev int64
|
||||||
|
maxModRev int64
|
||||||
|
minCreateRev int64
|
||||||
|
maxCreateRev int64
|
||||||
|
|
||||||
|
// for range, watch
|
||||||
|
rev int64
|
||||||
|
|
||||||
|
// for watch, put, delete
|
||||||
|
prevKV bool
|
||||||
|
|
||||||
|
// for put
|
||||||
|
ignoreValue bool
|
||||||
|
ignoreLease bool
|
||||||
|
|
||||||
|
// progressNotify is for progress updates.
|
||||||
|
progressNotify bool
|
||||||
|
// createdNotify is for created event
|
||||||
|
createdNotify bool
|
||||||
|
// filters for watchers
|
||||||
|
filterPut bool
|
||||||
|
filterDelete bool
|
||||||
|
|
||||||
|
// for put
|
||||||
|
val []byte
|
||||||
|
leaseID LeaseID
|
||||||
|
}
|
||||||
|
|
||||||
|
// accesors / mutators
|
||||||
|
|
||||||
|
// KeyBytes returns the byte slice holding the Op's key.
|
||||||
|
func (op Op) KeyBytes() []byte { return op.key }
|
||||||
|
|
||||||
|
// WithKeyBytes sets the byte slice for the Op's key.
|
||||||
|
func (op *Op) WithKeyBytes(key []byte) { op.key = key }
|
||||||
|
|
||||||
|
// RangeBytes returns the byte slice holding with the Op's range end, if any.
|
||||||
|
func (op Op) RangeBytes() []byte { return op.end }
|
||||||
|
|
||||||
|
// WithRangeBytes sets the byte slice for the Op's range end.
|
||||||
|
func (op *Op) WithRangeBytes(end []byte) { op.end = end }
|
||||||
|
|
||||||
|
// ValueBytes returns the byte slice holding the Op's value, if any.
|
||||||
|
func (op Op) ValueBytes() []byte { return op.val }
|
||||||
|
|
||||||
|
// WithValueBytes sets the byte slice for the Op's value.
|
||||||
|
func (op *Op) WithValueBytes(v []byte) { op.val = v }
|
||||||
|
|
||||||
|
func (op Op) toRangeRequest() *pb.RangeRequest {
|
||||||
|
if op.t != tRange {
|
||||||
|
panic("op.t != tRange")
|
||||||
|
}
|
||||||
|
r := &pb.RangeRequest{
|
||||||
|
Key: op.key,
|
||||||
|
RangeEnd: op.end,
|
||||||
|
Limit: op.limit,
|
||||||
|
Revision: op.rev,
|
||||||
|
Serializable: op.serializable,
|
||||||
|
KeysOnly: op.keysOnly,
|
||||||
|
CountOnly: op.countOnly,
|
||||||
|
MinModRevision: op.minModRev,
|
||||||
|
MaxModRevision: op.maxModRev,
|
||||||
|
MinCreateRevision: op.minCreateRev,
|
||||||
|
MaxCreateRevision: op.maxCreateRev,
|
||||||
|
}
|
||||||
|
if op.sort != nil {
|
||||||
|
r.SortOrder = pb.RangeRequest_SortOrder(op.sort.Order)
|
||||||
|
r.SortTarget = pb.RangeRequest_SortTarget(op.sort.Target)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (op Op) toRequestOp() *pb.RequestOp {
|
||||||
|
switch op.t {
|
||||||
|
case tRange:
|
||||||
|
return &pb.RequestOp{Request: &pb.RequestOp_RequestRange{RequestRange: op.toRangeRequest()}}
|
||||||
|
case tPut:
|
||||||
|
r := &pb.PutRequest{Key: op.key, Value: op.val, Lease: int64(op.leaseID), PrevKv: op.prevKV, IgnoreValue: op.ignoreValue, IgnoreLease: op.ignoreLease}
|
||||||
|
return &pb.RequestOp{Request: &pb.RequestOp_RequestPut{RequestPut: r}}
|
||||||
|
case tDeleteRange:
|
||||||
|
r := &pb.DeleteRangeRequest{Key: op.key, RangeEnd: op.end, PrevKv: op.prevKV}
|
||||||
|
return &pb.RequestOp{Request: &pb.RequestOp_RequestDeleteRange{RequestDeleteRange: r}}
|
||||||
|
default:
|
||||||
|
panic("Unknown Op")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (op Op) isWrite() bool {
|
||||||
|
return op.t != tRange
|
||||||
|
}
|
||||||
|
|
||||||
|
func OpGet(key string, opts ...OpOption) Op {
|
||||||
|
ret := Op{t: tRange, key: []byte(key)}
|
||||||
|
ret.applyOpts(opts)
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func OpDelete(key string, opts ...OpOption) Op {
|
||||||
|
ret := Op{t: tDeleteRange, key: []byte(key)}
|
||||||
|
ret.applyOpts(opts)
|
||||||
|
switch {
|
||||||
|
case ret.leaseID != 0:
|
||||||
|
panic("unexpected lease in delete")
|
||||||
|
case ret.limit != 0:
|
||||||
|
panic("unexpected limit in delete")
|
||||||
|
case ret.rev != 0:
|
||||||
|
panic("unexpected revision in delete")
|
||||||
|
case ret.sort != nil:
|
||||||
|
panic("unexpected sort in delete")
|
||||||
|
case ret.serializable:
|
||||||
|
panic("unexpected serializable in delete")
|
||||||
|
case ret.countOnly:
|
||||||
|
panic("unexpected countOnly in delete")
|
||||||
|
case ret.minModRev != 0, ret.maxModRev != 0:
|
||||||
|
panic("unexpected mod revision filter in delete")
|
||||||
|
case ret.minCreateRev != 0, ret.maxCreateRev != 0:
|
||||||
|
panic("unexpected create revision filter in delete")
|
||||||
|
case ret.filterDelete, ret.filterPut:
|
||||||
|
panic("unexpected filter in delete")
|
||||||
|
case ret.createdNotify:
|
||||||
|
panic("unexpected createdNotify in delete")
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func OpPut(key, val string, opts ...OpOption) Op {
|
||||||
|
ret := Op{t: tPut, key: []byte(key), val: []byte(val)}
|
||||||
|
ret.applyOpts(opts)
|
||||||
|
switch {
|
||||||
|
case ret.end != nil:
|
||||||
|
panic("unexpected range in put")
|
||||||
|
case ret.limit != 0:
|
||||||
|
panic("unexpected limit in put")
|
||||||
|
case ret.rev != 0:
|
||||||
|
panic("unexpected revision in put")
|
||||||
|
case ret.sort != nil:
|
||||||
|
panic("unexpected sort in put")
|
||||||
|
case ret.serializable:
|
||||||
|
panic("unexpected serializable in put")
|
||||||
|
case ret.countOnly:
|
||||||
|
panic("unexpected countOnly in put")
|
||||||
|
case ret.minModRev != 0, ret.maxModRev != 0:
|
||||||
|
panic("unexpected mod revision filter in put")
|
||||||
|
case ret.minCreateRev != 0, ret.maxCreateRev != 0:
|
||||||
|
panic("unexpected create revision filter in put")
|
||||||
|
case ret.filterDelete, ret.filterPut:
|
||||||
|
panic("unexpected filter in put")
|
||||||
|
case ret.createdNotify:
|
||||||
|
panic("unexpected createdNotify in put")
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func opWatch(key string, opts ...OpOption) Op {
|
||||||
|
ret := Op{t: tRange, key: []byte(key)}
|
||||||
|
ret.applyOpts(opts)
|
||||||
|
switch {
|
||||||
|
case ret.leaseID != 0:
|
||||||
|
panic("unexpected lease in watch")
|
||||||
|
case ret.limit != 0:
|
||||||
|
panic("unexpected limit in watch")
|
||||||
|
case ret.sort != nil:
|
||||||
|
panic("unexpected sort in watch")
|
||||||
|
case ret.serializable:
|
||||||
|
panic("unexpected serializable in watch")
|
||||||
|
case ret.countOnly:
|
||||||
|
panic("unexpected countOnly in watch")
|
||||||
|
case ret.minModRev != 0, ret.maxModRev != 0:
|
||||||
|
panic("unexpected mod revision filter in watch")
|
||||||
|
case ret.minCreateRev != 0, ret.maxCreateRev != 0:
|
||||||
|
panic("unexpected create revision filter in watch")
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (op *Op) applyOpts(opts []OpOption) {
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(op)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpOption configures Operations like Get, Put, Delete.
|
||||||
|
type OpOption func(*Op)
|
||||||
|
|
||||||
|
// WithLease attaches a lease ID to a key in 'Put' request.
|
||||||
|
func WithLease(leaseID LeaseID) OpOption {
|
||||||
|
return func(op *Op) { op.leaseID = leaseID }
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLimit limits the number of results to return from 'Get' request.
|
||||||
|
// If WithLimit is given a 0 limit, it is treated as no limit.
|
||||||
|
func WithLimit(n int64) OpOption { return func(op *Op) { op.limit = n } }
|
||||||
|
|
||||||
|
// WithRev specifies the store revision for 'Get' request.
|
||||||
|
// Or the start revision of 'Watch' request.
|
||||||
|
func WithRev(rev int64) OpOption { return func(op *Op) { op.rev = rev } }
|
||||||
|
|
||||||
|
// WithSort specifies the ordering in 'Get' request. It requires
|
||||||
|
// 'WithRange' and/or 'WithPrefix' to be specified too.
|
||||||
|
// 'target' specifies the target to sort by: key, version, revisions, value.
|
||||||
|
// 'order' can be either 'SortNone', 'SortAscend', 'SortDescend'.
|
||||||
|
func WithSort(target SortTarget, order SortOrder) OpOption {
|
||||||
|
return func(op *Op) {
|
||||||
|
if target == SortByKey && order == SortAscend {
|
||||||
|
// If order != SortNone, server fetches the entire key-space,
|
||||||
|
// and then applies the sort and limit, if provided.
|
||||||
|
// Since current mvcc.Range implementation returns results
|
||||||
|
// sorted by keys in lexicographically ascending order,
|
||||||
|
// client should ignore SortOrder if the target is SortByKey.
|
||||||
|
order = SortNone
|
||||||
|
}
|
||||||
|
op.sort = &SortOption{target, order}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrefixRangeEnd gets the range end of the prefix.
|
||||||
|
// 'Get(foo, WithPrefix())' is equal to 'Get(foo, WithRange(GetPrefixRangeEnd(foo))'.
|
||||||
|
func GetPrefixRangeEnd(prefix string) string {
|
||||||
|
return string(getPrefix([]byte(prefix)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPrefix(key []byte) []byte {
|
||||||
|
end := make([]byte, len(key))
|
||||||
|
copy(end, key)
|
||||||
|
for i := len(end) - 1; i >= 0; i-- {
|
||||||
|
if end[i] < 0xff {
|
||||||
|
end[i] = end[i] + 1
|
||||||
|
end = end[:i+1]
|
||||||
|
return end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// next prefix does not exist (e.g., 0xffff);
|
||||||
|
// default to WithFromKey policy
|
||||||
|
return noPrefixEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPrefix enables 'Get', 'Delete', or 'Watch' requests to operate
|
||||||
|
// on the keys with matching prefix. For example, 'Get(foo, WithPrefix())'
|
||||||
|
// can return 'foo1', 'foo2', and so on.
|
||||||
|
func WithPrefix() OpOption {
|
||||||
|
return func(op *Op) {
|
||||||
|
if len(op.key) == 0 {
|
||||||
|
op.key, op.end = []byte{0}, []byte{0}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
op.end = getPrefix(op.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithRange specifies the range of 'Get', 'Delete', 'Watch' requests.
|
||||||
|
// For example, 'Get' requests with 'WithRange(end)' returns
|
||||||
|
// the keys in the range [key, end).
|
||||||
|
// endKey must be lexicographically greater than start key.
|
||||||
|
func WithRange(endKey string) OpOption {
|
||||||
|
return func(op *Op) { op.end = []byte(endKey) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithFromKey specifies the range of 'Get', 'Delete', 'Watch' requests
|
||||||
|
// to be equal or greater than the key in the argument.
|
||||||
|
func WithFromKey() OpOption { return WithRange("\x00") }
|
||||||
|
|
||||||
|
// WithSerializable makes 'Get' request serializable. By default,
|
||||||
|
// it's linearizable. Serializable requests are better for lower latency
|
||||||
|
// requirement.
|
||||||
|
func WithSerializable() OpOption {
|
||||||
|
return func(op *Op) { op.serializable = true }
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithKeysOnly makes the 'Get' request return only the keys and the corresponding
|
||||||
|
// values will be omitted.
|
||||||
|
func WithKeysOnly() OpOption {
|
||||||
|
return func(op *Op) { op.keysOnly = true }
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithCountOnly makes the 'Get' request return only the count of keys.
|
||||||
|
func WithCountOnly() OpOption {
|
||||||
|
return func(op *Op) { op.countOnly = true }
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMinModRev filters out keys for Get with modification revisions less than the given revision.
|
||||||
|
func WithMinModRev(rev int64) OpOption { return func(op *Op) { op.minModRev = rev } }
|
||||||
|
|
||||||
|
// WithMaxModRev filters out keys for Get with modification revisions greater than the given revision.
|
||||||
|
func WithMaxModRev(rev int64) OpOption { return func(op *Op) { op.maxModRev = rev } }
|
||||||
|
|
||||||
|
// WithMinCreateRev filters out keys for Get with creation revisions less than the given revision.
|
||||||
|
func WithMinCreateRev(rev int64) OpOption { return func(op *Op) { op.minCreateRev = rev } }
|
||||||
|
|
||||||
|
// WithMaxCreateRev filters out keys for Get with creation revisions greater than the given revision.
|
||||||
|
func WithMaxCreateRev(rev int64) OpOption { return func(op *Op) { op.maxCreateRev = rev } }
|
||||||
|
|
||||||
|
// WithFirstCreate gets the key with the oldest creation revision in the request range.
|
||||||
|
func WithFirstCreate() []OpOption { return withTop(SortByCreateRevision, SortAscend) }
|
||||||
|
|
||||||
|
// WithLastCreate gets the key with the latest creation revision in the request range.
|
||||||
|
func WithLastCreate() []OpOption { return withTop(SortByCreateRevision, SortDescend) }
|
||||||
|
|
||||||
|
// WithFirstKey gets the lexically first key in the request range.
|
||||||
|
func WithFirstKey() []OpOption { return withTop(SortByKey, SortAscend) }
|
||||||
|
|
||||||
|
// WithLastKey gets the lexically last key in the request range.
|
||||||
|
func WithLastKey() []OpOption { return withTop(SortByKey, SortDescend) }
|
||||||
|
|
||||||
|
// WithFirstRev gets the key with the oldest modification revision in the request range.
|
||||||
|
func WithFirstRev() []OpOption { return withTop(SortByModRevision, SortAscend) }
|
||||||
|
|
||||||
|
// WithLastRev gets the key with the latest modification revision in the request range.
|
||||||
|
func WithLastRev() []OpOption { return withTop(SortByModRevision, SortDescend) }
|
||||||
|
|
||||||
|
// withTop gets the first key over the get's prefix given a sort order
|
||||||
|
func withTop(target SortTarget, order SortOrder) []OpOption {
|
||||||
|
return []OpOption{WithPrefix(), WithSort(target, order), WithLimit(1)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithProgressNotify makes watch server send periodic progress updates
|
||||||
|
// every 10 minutes when there is no incoming events.
|
||||||
|
// Progress updates have zero events in WatchResponse.
|
||||||
|
func WithProgressNotify() OpOption {
|
||||||
|
return func(op *Op) {
|
||||||
|
op.progressNotify = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithCreatedNotify makes watch server sends the created event.
|
||||||
|
func WithCreatedNotify() OpOption {
|
||||||
|
return func(op *Op) {
|
||||||
|
op.createdNotify = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithFilterPut discards PUT events from the watcher.
|
||||||
|
func WithFilterPut() OpOption {
|
||||||
|
return func(op *Op) { op.filterPut = true }
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithFilterDelete discards DELETE events from the watcher.
|
||||||
|
func WithFilterDelete() OpOption {
|
||||||
|
return func(op *Op) { op.filterDelete = true }
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPrevKV gets the previous key-value pair before the event happens. If the previous KV is already compacted,
|
||||||
|
// nothing will be returned.
|
||||||
|
func WithPrevKV() OpOption {
|
||||||
|
return func(op *Op) {
|
||||||
|
op.prevKV = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithIgnoreValue updates the key using its current value.
|
||||||
|
// Empty value should be passed when ignore_value is set.
|
||||||
|
// Returns an error if the key does not exist.
|
||||||
|
func WithIgnoreValue() OpOption {
|
||||||
|
return func(op *Op) {
|
||||||
|
op.ignoreValue = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithIgnoreLease updates the key using its current lease.
|
||||||
|
// Empty lease should be passed when ignore_lease is set.
|
||||||
|
// Returns an error if the key does not exist.
|
||||||
|
func WithIgnoreLease() OpOption {
|
||||||
|
return func(op *Op) {
|
||||||
|
op.ignoreLease = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LeaseOp represents an Operation that lease can execute.
|
||||||
|
type LeaseOp struct {
|
||||||
|
id LeaseID
|
||||||
|
|
||||||
|
// for TimeToLive
|
||||||
|
attachedKeys bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// LeaseOption configures lease operations.
|
||||||
|
type LeaseOption func(*LeaseOp)
|
||||||
|
|
||||||
|
func (op *LeaseOp) applyOpts(opts []LeaseOption) {
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(op)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAttachedKeys requests lease timetolive API to return
|
||||||
|
// attached keys of given lease ID.
|
||||||
|
func WithAttachedKeys() LeaseOption {
|
||||||
|
return func(op *LeaseOp) { op.attachedKeys = true }
|
||||||
|
}
|
||||||
|
|
||||||
|
func toLeaseTimeToLiveRequest(id LeaseID, opts ...LeaseOption) *pb.LeaseTimeToLiveRequest {
|
||||||
|
ret := &LeaseOp{id: id}
|
||||||
|
ret.applyOpts(opts)
|
||||||
|
return &pb.LeaseTimeToLiveRequest{ID: int64(id), Keys: ret.attachedKeys}
|
||||||
|
}
|
|
@ -0,0 +1,293 @@
|
||||||
|
// Copyright 2016 The etcd Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package clientv3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
|
||||||
|
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
)
|
||||||
|
|
||||||
|
type rpcFunc func(ctx context.Context) error
|
||||||
|
type retryRpcFunc func(context.Context, rpcFunc) error
|
||||||
|
|
||||||
|
func (c *Client) newRetryWrapper() retryRpcFunc {
|
||||||
|
return func(rpcCtx context.Context, f rpcFunc) error {
|
||||||
|
for {
|
||||||
|
err := f(rpcCtx)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
eErr := rpctypes.Error(err)
|
||||||
|
// always stop retry on etcd errors
|
||||||
|
if _, ok := eErr.(rpctypes.EtcdError); ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// only retry if unavailable
|
||||||
|
if grpc.Code(err) != codes.Unavailable {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-c.balancer.ConnectNotify():
|
||||||
|
case <-rpcCtx.Done():
|
||||||
|
return rpcCtx.Err()
|
||||||
|
case <-c.ctx.Done():
|
||||||
|
return c.ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) newAuthRetryWrapper() retryRpcFunc {
|
||||||
|
return func(rpcCtx context.Context, f rpcFunc) error {
|
||||||
|
for {
|
||||||
|
err := f(rpcCtx)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// always stop retry on etcd errors other than invalid auth token
|
||||||
|
if rpctypes.Error(err) == rpctypes.ErrInvalidAuthToken {
|
||||||
|
gterr := c.getToken(rpcCtx)
|
||||||
|
if gterr != nil {
|
||||||
|
return err // return the original error for simplicity
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RetryKVClient implements a KVClient that uses the client's FailFast retry policy.
|
||||||
|
func RetryKVClient(c *Client) pb.KVClient {
|
||||||
|
retryWrite := &retryWriteKVClient{pb.NewKVClient(c.conn), c.retryWrapper}
|
||||||
|
return &retryKVClient{&retryWriteKVClient{retryWrite, c.retryAuthWrapper}}
|
||||||
|
}
|
||||||
|
|
||||||
|
type retryKVClient struct {
|
||||||
|
*retryWriteKVClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rkv *retryKVClient) Range(ctx context.Context, in *pb.RangeRequest, opts ...grpc.CallOption) (resp *pb.RangeResponse, err error) {
|
||||||
|
err = rkv.retryf(ctx, func(rctx context.Context) error {
|
||||||
|
resp, err = rkv.retryWriteKVClient.Range(rctx, in, opts...)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type retryWriteKVClient struct {
|
||||||
|
pb.KVClient
|
||||||
|
retryf retryRpcFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rkv *retryWriteKVClient) Put(ctx context.Context, in *pb.PutRequest, opts ...grpc.CallOption) (resp *pb.PutResponse, err error) {
|
||||||
|
err = rkv.retryf(ctx, func(rctx context.Context) error {
|
||||||
|
resp, err = rkv.KVClient.Put(rctx, in, opts...)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rkv *retryWriteKVClient) DeleteRange(ctx context.Context, in *pb.DeleteRangeRequest, opts ...grpc.CallOption) (resp *pb.DeleteRangeResponse, err error) {
|
||||||
|
err = rkv.retryf(ctx, func(rctx context.Context) error {
|
||||||
|
resp, err = rkv.KVClient.DeleteRange(rctx, in, opts...)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rkv *retryWriteKVClient) Txn(ctx context.Context, in *pb.TxnRequest, opts ...grpc.CallOption) (resp *pb.TxnResponse, err error) {
|
||||||
|
err = rkv.retryf(ctx, func(rctx context.Context) error {
|
||||||
|
resp, err = rkv.KVClient.Txn(rctx, in, opts...)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rkv *retryWriteKVClient) Compact(ctx context.Context, in *pb.CompactionRequest, opts ...grpc.CallOption) (resp *pb.CompactionResponse, err error) {
|
||||||
|
err = rkv.retryf(ctx, func(rctx context.Context) error {
|
||||||
|
resp, err = rkv.KVClient.Compact(rctx, in, opts...)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type retryLeaseClient struct {
|
||||||
|
pb.LeaseClient
|
||||||
|
retryf retryRpcFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// RetryLeaseClient implements a LeaseClient that uses the client's FailFast retry policy.
|
||||||
|
func RetryLeaseClient(c *Client) pb.LeaseClient {
|
||||||
|
retry := &retryLeaseClient{pb.NewLeaseClient(c.conn), c.retryWrapper}
|
||||||
|
return &retryLeaseClient{retry, c.retryAuthWrapper}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rlc *retryLeaseClient) LeaseGrant(ctx context.Context, in *pb.LeaseGrantRequest, opts ...grpc.CallOption) (resp *pb.LeaseGrantResponse, err error) {
|
||||||
|
err = rlc.retryf(ctx, func(rctx context.Context) error {
|
||||||
|
resp, err = rlc.LeaseClient.LeaseGrant(rctx, in, opts...)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return resp, err
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rlc *retryLeaseClient) LeaseRevoke(ctx context.Context, in *pb.LeaseRevokeRequest, opts ...grpc.CallOption) (resp *pb.LeaseRevokeResponse, err error) {
|
||||||
|
err = rlc.retryf(ctx, func(rctx context.Context) error {
|
||||||
|
resp, err = rlc.LeaseClient.LeaseRevoke(rctx, in, opts...)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type retryClusterClient struct {
|
||||||
|
pb.ClusterClient
|
||||||
|
retryf retryRpcFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// RetryClusterClient implements a ClusterClient that uses the client's FailFast retry policy.
|
||||||
|
func RetryClusterClient(c *Client) pb.ClusterClient {
|
||||||
|
return &retryClusterClient{pb.NewClusterClient(c.conn), c.retryWrapper}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rcc *retryClusterClient) MemberAdd(ctx context.Context, in *pb.MemberAddRequest, opts ...grpc.CallOption) (resp *pb.MemberAddResponse, err error) {
|
||||||
|
err = rcc.retryf(ctx, func(rctx context.Context) error {
|
||||||
|
resp, err = rcc.ClusterClient.MemberAdd(rctx, in, opts...)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rcc *retryClusterClient) MemberRemove(ctx context.Context, in *pb.MemberRemoveRequest, opts ...grpc.CallOption) (resp *pb.MemberRemoveResponse, err error) {
|
||||||
|
err = rcc.retryf(ctx, func(rctx context.Context) error {
|
||||||
|
resp, err = rcc.ClusterClient.MemberRemove(rctx, in, opts...)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rcc *retryClusterClient) MemberUpdate(ctx context.Context, in *pb.MemberUpdateRequest, opts ...grpc.CallOption) (resp *pb.MemberUpdateResponse, err error) {
|
||||||
|
err = rcc.retryf(ctx, func(rctx context.Context) error {
|
||||||
|
resp, err = rcc.ClusterClient.MemberUpdate(rctx, in, opts...)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type retryAuthClient struct {
|
||||||
|
pb.AuthClient
|
||||||
|
retryf retryRpcFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// RetryAuthClient implements a AuthClient that uses the client's FailFast retry policy.
|
||||||
|
func RetryAuthClient(c *Client) pb.AuthClient {
|
||||||
|
return &retryAuthClient{pb.NewAuthClient(c.conn), c.retryWrapper}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rac *retryAuthClient) AuthEnable(ctx context.Context, in *pb.AuthEnableRequest, opts ...grpc.CallOption) (resp *pb.AuthEnableResponse, err error) {
|
||||||
|
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||||
|
resp, err = rac.AuthClient.AuthEnable(rctx, in, opts...)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rac *retryAuthClient) AuthDisable(ctx context.Context, in *pb.AuthDisableRequest, opts ...grpc.CallOption) (resp *pb.AuthDisableResponse, err error) {
|
||||||
|
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||||
|
resp, err = rac.AuthClient.AuthDisable(rctx, in, opts...)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rac *retryAuthClient) UserAdd(ctx context.Context, in *pb.AuthUserAddRequest, opts ...grpc.CallOption) (resp *pb.AuthUserAddResponse, err error) {
|
||||||
|
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||||
|
resp, err = rac.AuthClient.UserAdd(rctx, in, opts...)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rac *retryAuthClient) UserDelete(ctx context.Context, in *pb.AuthUserDeleteRequest, opts ...grpc.CallOption) (resp *pb.AuthUserDeleteResponse, err error) {
|
||||||
|
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||||
|
resp, err = rac.AuthClient.UserDelete(rctx, in, opts...)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rac *retryAuthClient) UserChangePassword(ctx context.Context, in *pb.AuthUserChangePasswordRequest, opts ...grpc.CallOption) (resp *pb.AuthUserChangePasswordResponse, err error) {
|
||||||
|
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||||
|
resp, err = rac.AuthClient.UserChangePassword(rctx, in, opts...)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rac *retryAuthClient) UserGrantRole(ctx context.Context, in *pb.AuthUserGrantRoleRequest, opts ...grpc.CallOption) (resp *pb.AuthUserGrantRoleResponse, err error) {
|
||||||
|
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||||
|
resp, err = rac.AuthClient.UserGrantRole(rctx, in, opts...)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rac *retryAuthClient) UserRevokeRole(ctx context.Context, in *pb.AuthUserRevokeRoleRequest, opts ...grpc.CallOption) (resp *pb.AuthUserRevokeRoleResponse, err error) {
|
||||||
|
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||||
|
resp, err = rac.AuthClient.UserRevokeRole(rctx, in, opts...)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rac *retryAuthClient) RoleAdd(ctx context.Context, in *pb.AuthRoleAddRequest, opts ...grpc.CallOption) (resp *pb.AuthRoleAddResponse, err error) {
|
||||||
|
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||||
|
resp, err = rac.AuthClient.RoleAdd(rctx, in, opts...)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rac *retryAuthClient) RoleDelete(ctx context.Context, in *pb.AuthRoleDeleteRequest, opts ...grpc.CallOption) (resp *pb.AuthRoleDeleteResponse, err error) {
|
||||||
|
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||||
|
resp, err = rac.AuthClient.RoleDelete(rctx, in, opts...)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rac *retryAuthClient) RoleGrantPermission(ctx context.Context, in *pb.AuthRoleGrantPermissionRequest, opts ...grpc.CallOption) (resp *pb.AuthRoleGrantPermissionResponse, err error) {
|
||||||
|
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||||
|
resp, err = rac.AuthClient.RoleGrantPermission(rctx, in, opts...)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rac *retryAuthClient) RoleRevokePermission(ctx context.Context, in *pb.AuthRoleRevokePermissionRequest, opts ...grpc.CallOption) (resp *pb.AuthRoleRevokePermissionResponse, err error) {
|
||||||
|
err = rac.retryf(ctx, func(rctx context.Context) error {
|
||||||
|
resp, err = rac.AuthClient.RoleRevokePermission(rctx, in, opts...)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return resp, err
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
// Copyright 2016 The etcd Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package clientv3
|
||||||
|
|
||||||
|
type SortTarget int
|
||||||
|
type SortOrder int
|
||||||
|
|
||||||
|
const (
|
||||||
|
SortNone SortOrder = iota
|
||||||
|
SortAscend
|
||||||
|
SortDescend
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SortByKey SortTarget = iota
|
||||||
|
SortByVersion
|
||||||
|
SortByCreateRevision
|
||||||
|
SortByModRevision
|
||||||
|
SortByValue
|
||||||
|
)
|
||||||
|
|
||||||
|
type SortOption struct {
|
||||||
|
Target SortTarget
|
||||||
|
Order SortOrder
|
||||||
|
}
|
|
@ -0,0 +1,164 @@
|
||||||
|
// Copyright 2016 The etcd Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package clientv3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Txn is the interface that wraps mini-transactions.
|
||||||
|
//
|
||||||
|
// Tx.If(
|
||||||
|
// Compare(Value(k1), ">", v1),
|
||||||
|
// Compare(Version(k1), "=", 2)
|
||||||
|
// ).Then(
|
||||||
|
// OpPut(k2,v2), OpPut(k3,v3)
|
||||||
|
// ).Else(
|
||||||
|
// OpPut(k4,v4), OpPut(k5,v5)
|
||||||
|
// ).Commit()
|
||||||
|
//
|
||||||
|
type Txn interface {
|
||||||
|
// If takes a list of comparison. If all comparisons passed in succeed,
|
||||||
|
// the operations passed into Then() will be executed. Or the operations
|
||||||
|
// passed into Else() will be executed.
|
||||||
|
If(cs ...Cmp) Txn
|
||||||
|
|
||||||
|
// Then takes a list of operations. The Ops list will be executed, if the
|
||||||
|
// comparisons passed in If() succeed.
|
||||||
|
Then(ops ...Op) Txn
|
||||||
|
|
||||||
|
// Else takes a list of operations. The Ops list will be executed, if the
|
||||||
|
// comparisons passed in If() fail.
|
||||||
|
Else(ops ...Op) Txn
|
||||||
|
|
||||||
|
// Commit tries to commit the transaction.
|
||||||
|
Commit() (*TxnResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type txn struct {
|
||||||
|
kv *kv
|
||||||
|
ctx context.Context
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
cif bool
|
||||||
|
cthen bool
|
||||||
|
celse bool
|
||||||
|
|
||||||
|
isWrite bool
|
||||||
|
|
||||||
|
cmps []*pb.Compare
|
||||||
|
|
||||||
|
sus []*pb.RequestOp
|
||||||
|
fas []*pb.RequestOp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (txn *txn) If(cs ...Cmp) Txn {
|
||||||
|
txn.mu.Lock()
|
||||||
|
defer txn.mu.Unlock()
|
||||||
|
|
||||||
|
if txn.cif {
|
||||||
|
panic("cannot call If twice!")
|
||||||
|
}
|
||||||
|
|
||||||
|
if txn.cthen {
|
||||||
|
panic("cannot call If after Then!")
|
||||||
|
}
|
||||||
|
|
||||||
|
if txn.celse {
|
||||||
|
panic("cannot call If after Else!")
|
||||||
|
}
|
||||||
|
|
||||||
|
txn.cif = true
|
||||||
|
|
||||||
|
for i := range cs {
|
||||||
|
txn.cmps = append(txn.cmps, (*pb.Compare)(&cs[i]))
|
||||||
|
}
|
||||||
|
|
||||||
|
return txn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (txn *txn) Then(ops ...Op) Txn {
|
||||||
|
txn.mu.Lock()
|
||||||
|
defer txn.mu.Unlock()
|
||||||
|
|
||||||
|
if txn.cthen {
|
||||||
|
panic("cannot call Then twice!")
|
||||||
|
}
|
||||||
|
if txn.celse {
|
||||||
|
panic("cannot call Then after Else!")
|
||||||
|
}
|
||||||
|
|
||||||
|
txn.cthen = true
|
||||||
|
|
||||||
|
for _, op := range ops {
|
||||||
|
txn.isWrite = txn.isWrite || op.isWrite()
|
||||||
|
txn.sus = append(txn.sus, op.toRequestOp())
|
||||||
|
}
|
||||||
|
|
||||||
|
return txn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (txn *txn) Else(ops ...Op) Txn {
|
||||||
|
txn.mu.Lock()
|
||||||
|
defer txn.mu.Unlock()
|
||||||
|
|
||||||
|
if txn.celse {
|
||||||
|
panic("cannot call Else twice!")
|
||||||
|
}
|
||||||
|
|
||||||
|
txn.celse = true
|
||||||
|
|
||||||
|
for _, op := range ops {
|
||||||
|
txn.isWrite = txn.isWrite || op.isWrite()
|
||||||
|
txn.fas = append(txn.fas, op.toRequestOp())
|
||||||
|
}
|
||||||
|
|
||||||
|
return txn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (txn *txn) Commit() (*TxnResponse, error) {
|
||||||
|
txn.mu.Lock()
|
||||||
|
defer txn.mu.Unlock()
|
||||||
|
for {
|
||||||
|
resp, err := txn.commit()
|
||||||
|
if err == nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
if isHaltErr(txn.ctx, err) {
|
||||||
|
return nil, toErr(txn.ctx, err)
|
||||||
|
}
|
||||||
|
if txn.isWrite {
|
||||||
|
return nil, toErr(txn.ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (txn *txn) commit() (*TxnResponse, error) {
|
||||||
|
r := &pb.TxnRequest{Compare: txn.cmps, Success: txn.sus, Failure: txn.fas}
|
||||||
|
|
||||||
|
var opts []grpc.CallOption
|
||||||
|
if !txn.isWrite {
|
||||||
|
opts = []grpc.CallOption{grpc.FailFast(false)}
|
||||||
|
}
|
||||||
|
resp, err := txn.kv.remote.Txn(txn.ctx, r, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return (*TxnResponse)(resp), nil
|
||||||
|
}
|
|
@ -0,0 +1,797 @@
|
||||||
|
// Copyright 2016 The etcd Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package clientv3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
v3rpc "github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
|
||||||
|
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||||
|
mvccpb "github.com/coreos/etcd/mvcc/mvccpb"
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
EventTypeDelete = mvccpb.DELETE
|
||||||
|
EventTypePut = mvccpb.PUT
|
||||||
|
|
||||||
|
closeSendErrTimeout = 250 * time.Millisecond
|
||||||
|
)
|
||||||
|
|
||||||
|
type Event mvccpb.Event
|
||||||
|
|
||||||
|
type WatchChan <-chan WatchResponse
|
||||||
|
|
||||||
|
type Watcher interface {
|
||||||
|
// Watch watches on a key or prefix. The watched events will be returned
|
||||||
|
// through the returned channel.
|
||||||
|
// If the watch is slow or the required rev is compacted, the watch request
|
||||||
|
// might be canceled from the server-side and the chan will be closed.
|
||||||
|
// 'opts' can be: 'WithRev' and/or 'WithPrefix'.
|
||||||
|
Watch(ctx context.Context, key string, opts ...OpOption) WatchChan
|
||||||
|
|
||||||
|
// Close closes the watcher and cancels all watch requests.
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type WatchResponse struct {
|
||||||
|
Header pb.ResponseHeader
|
||||||
|
Events []*Event
|
||||||
|
|
||||||
|
// CompactRevision is the minimum revision the watcher may receive.
|
||||||
|
CompactRevision int64
|
||||||
|
|
||||||
|
// Canceled is used to indicate watch failure.
|
||||||
|
// If the watch failed and the stream was about to close, before the channel is closed,
|
||||||
|
// the channel sends a final response that has Canceled set to true with a non-nil Err().
|
||||||
|
Canceled bool
|
||||||
|
|
||||||
|
// Created is used to indicate the creation of the watcher.
|
||||||
|
Created bool
|
||||||
|
|
||||||
|
closeErr error
|
||||||
|
|
||||||
|
// cancelReason is a reason of canceling watch
|
||||||
|
cancelReason string
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsCreate returns true if the event tells that the key is newly created.
|
||||||
|
func (e *Event) IsCreate() bool {
|
||||||
|
return e.Type == EventTypePut && e.Kv.CreateRevision == e.Kv.ModRevision
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsModify returns true if the event tells that a new value is put on existing key.
|
||||||
|
func (e *Event) IsModify() bool {
|
||||||
|
return e.Type == EventTypePut && e.Kv.CreateRevision != e.Kv.ModRevision
|
||||||
|
}
|
||||||
|
|
||||||
|
// Err is the error value if this WatchResponse holds an error.
|
||||||
|
func (wr *WatchResponse) Err() error {
|
||||||
|
switch {
|
||||||
|
case wr.closeErr != nil:
|
||||||
|
return v3rpc.Error(wr.closeErr)
|
||||||
|
case wr.CompactRevision != 0:
|
||||||
|
return v3rpc.ErrCompacted
|
||||||
|
case wr.Canceled:
|
||||||
|
if len(wr.cancelReason) != 0 {
|
||||||
|
return v3rpc.Error(grpc.Errorf(codes.FailedPrecondition, "%s", wr.cancelReason))
|
||||||
|
}
|
||||||
|
return v3rpc.ErrFutureRev
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsProgressNotify returns true if the WatchResponse is progress notification.
|
||||||
|
func (wr *WatchResponse) IsProgressNotify() bool {
|
||||||
|
return len(wr.Events) == 0 && !wr.Canceled && !wr.Created && wr.CompactRevision == 0 && wr.Header.Revision != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// watcher implements the Watcher interface
|
||||||
|
type watcher struct {
|
||||||
|
remote pb.WatchClient
|
||||||
|
|
||||||
|
// mu protects the grpc streams map
|
||||||
|
mu sync.RWMutex
|
||||||
|
|
||||||
|
// streams holds all the active grpc streams keyed by ctx value.
|
||||||
|
streams map[string]*watchGrpcStream
|
||||||
|
}
|
||||||
|
|
||||||
|
// watchGrpcStream tracks all watch resources attached to a single grpc stream.
|
||||||
|
type watchGrpcStream struct {
|
||||||
|
owner *watcher
|
||||||
|
remote pb.WatchClient
|
||||||
|
|
||||||
|
// ctx controls internal remote.Watch requests
|
||||||
|
ctx context.Context
|
||||||
|
// ctxKey is the key used when looking up this stream's context
|
||||||
|
ctxKey string
|
||||||
|
cancel context.CancelFunc
|
||||||
|
|
||||||
|
// substreams holds all active watchers on this grpc stream
|
||||||
|
substreams map[int64]*watcherStream
|
||||||
|
// resuming holds all resuming watchers on this grpc stream
|
||||||
|
resuming []*watcherStream
|
||||||
|
|
||||||
|
// reqc sends a watch request from Watch() to the main goroutine
|
||||||
|
reqc chan *watchRequest
|
||||||
|
// respc receives data from the watch client
|
||||||
|
respc chan *pb.WatchResponse
|
||||||
|
// donec closes to broadcast shutdown
|
||||||
|
donec chan struct{}
|
||||||
|
// errc transmits errors from grpc Recv to the watch stream reconn logic
|
||||||
|
errc chan error
|
||||||
|
// closingc gets the watcherStream of closing watchers
|
||||||
|
closingc chan *watcherStream
|
||||||
|
// wg is Done when all substream goroutines have exited
|
||||||
|
wg sync.WaitGroup
|
||||||
|
|
||||||
|
// resumec closes to signal that all substreams should begin resuming
|
||||||
|
resumec chan struct{}
|
||||||
|
// closeErr is the error that closed the watch stream
|
||||||
|
closeErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
// watchRequest is issued by the subscriber to start a new watcher
|
||||||
|
type watchRequest struct {
|
||||||
|
ctx context.Context
|
||||||
|
key string
|
||||||
|
end string
|
||||||
|
rev int64
|
||||||
|
// send created notification event if this field is true
|
||||||
|
createdNotify bool
|
||||||
|
// progressNotify is for progress updates
|
||||||
|
progressNotify bool
|
||||||
|
// filters is the list of events to filter out
|
||||||
|
filters []pb.WatchCreateRequest_FilterType
|
||||||
|
// get the previous key-value pair before the event happens
|
||||||
|
prevKV bool
|
||||||
|
// retc receives a chan WatchResponse once the watcher is established
|
||||||
|
retc chan chan WatchResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
// watcherStream represents a registered watcher
|
||||||
|
type watcherStream struct {
|
||||||
|
// initReq is the request that initiated this request
|
||||||
|
initReq watchRequest
|
||||||
|
|
||||||
|
// outc publishes watch responses to subscriber
|
||||||
|
outc chan WatchResponse
|
||||||
|
// recvc buffers watch responses before publishing
|
||||||
|
recvc chan *WatchResponse
|
||||||
|
// donec closes when the watcherStream goroutine stops.
|
||||||
|
donec chan struct{}
|
||||||
|
// closing is set to true when stream should be scheduled to shutdown.
|
||||||
|
closing bool
|
||||||
|
// id is the registered watch id on the grpc stream
|
||||||
|
id int64
|
||||||
|
|
||||||
|
// buf holds all events received from etcd but not yet consumed by the client
|
||||||
|
buf []*WatchResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWatcher(c *Client) Watcher {
|
||||||
|
return NewWatchFromWatchClient(pb.NewWatchClient(c.conn))
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWatchFromWatchClient(wc pb.WatchClient) Watcher {
|
||||||
|
return &watcher{
|
||||||
|
remote: wc,
|
||||||
|
streams: make(map[string]*watchGrpcStream),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// never closes
|
||||||
|
var valCtxCh = make(chan struct{})
|
||||||
|
var zeroTime = time.Unix(0, 0)
|
||||||
|
|
||||||
|
// ctx with only the values; never Done
|
||||||
|
type valCtx struct{ context.Context }
|
||||||
|
|
||||||
|
func (vc *valCtx) Deadline() (time.Time, bool) { return zeroTime, false }
|
||||||
|
func (vc *valCtx) Done() <-chan struct{} { return valCtxCh }
|
||||||
|
func (vc *valCtx) Err() error { return nil }
|
||||||
|
|
||||||
|
func (w *watcher) newWatcherGrpcStream(inctx context.Context) *watchGrpcStream {
|
||||||
|
ctx, cancel := context.WithCancel(&valCtx{inctx})
|
||||||
|
wgs := &watchGrpcStream{
|
||||||
|
owner: w,
|
||||||
|
remote: w.remote,
|
||||||
|
ctx: ctx,
|
||||||
|
ctxKey: fmt.Sprintf("%v", inctx),
|
||||||
|
cancel: cancel,
|
||||||
|
substreams: make(map[int64]*watcherStream),
|
||||||
|
|
||||||
|
respc: make(chan *pb.WatchResponse),
|
||||||
|
reqc: make(chan *watchRequest),
|
||||||
|
donec: make(chan struct{}),
|
||||||
|
errc: make(chan error, 1),
|
||||||
|
closingc: make(chan *watcherStream),
|
||||||
|
resumec: make(chan struct{}),
|
||||||
|
}
|
||||||
|
go wgs.run()
|
||||||
|
return wgs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch posts a watch request to run() and waits for a new watcher channel
|
||||||
|
func (w *watcher) Watch(ctx context.Context, key string, opts ...OpOption) WatchChan {
|
||||||
|
ow := opWatch(key, opts...)
|
||||||
|
|
||||||
|
var filters []pb.WatchCreateRequest_FilterType
|
||||||
|
if ow.filterPut {
|
||||||
|
filters = append(filters, pb.WatchCreateRequest_NOPUT)
|
||||||
|
}
|
||||||
|
if ow.filterDelete {
|
||||||
|
filters = append(filters, pb.WatchCreateRequest_NODELETE)
|
||||||
|
}
|
||||||
|
|
||||||
|
wr := &watchRequest{
|
||||||
|
ctx: ctx,
|
||||||
|
createdNotify: ow.createdNotify,
|
||||||
|
key: string(ow.key),
|
||||||
|
end: string(ow.end),
|
||||||
|
rev: ow.rev,
|
||||||
|
progressNotify: ow.progressNotify,
|
||||||
|
filters: filters,
|
||||||
|
prevKV: ow.prevKV,
|
||||||
|
retc: make(chan chan WatchResponse, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
ok := false
|
||||||
|
ctxKey := fmt.Sprintf("%v", ctx)
|
||||||
|
|
||||||
|
// find or allocate appropriate grpc watch stream
|
||||||
|
w.mu.Lock()
|
||||||
|
if w.streams == nil {
|
||||||
|
// closed
|
||||||
|
w.mu.Unlock()
|
||||||
|
ch := make(chan WatchResponse)
|
||||||
|
close(ch)
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
wgs := w.streams[ctxKey]
|
||||||
|
if wgs == nil {
|
||||||
|
wgs = w.newWatcherGrpcStream(ctx)
|
||||||
|
w.streams[ctxKey] = wgs
|
||||||
|
}
|
||||||
|
donec := wgs.donec
|
||||||
|
reqc := wgs.reqc
|
||||||
|
w.mu.Unlock()
|
||||||
|
|
||||||
|
// couldn't create channel; return closed channel
|
||||||
|
closeCh := make(chan WatchResponse, 1)
|
||||||
|
|
||||||
|
// submit request
|
||||||
|
select {
|
||||||
|
case reqc <- wr:
|
||||||
|
ok = true
|
||||||
|
case <-wr.ctx.Done():
|
||||||
|
case <-donec:
|
||||||
|
if wgs.closeErr != nil {
|
||||||
|
closeCh <- WatchResponse{closeErr: wgs.closeErr}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// retry; may have dropped stream from no ctxs
|
||||||
|
return w.Watch(ctx, key, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// receive channel
|
||||||
|
if ok {
|
||||||
|
select {
|
||||||
|
case ret := <-wr.retc:
|
||||||
|
return ret
|
||||||
|
case <-ctx.Done():
|
||||||
|
case <-donec:
|
||||||
|
if wgs.closeErr != nil {
|
||||||
|
closeCh <- WatchResponse{closeErr: wgs.closeErr}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// retry; may have dropped stream from no ctxs
|
||||||
|
return w.Watch(ctx, key, opts...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close(closeCh)
|
||||||
|
return closeCh
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *watcher) Close() (err error) {
|
||||||
|
w.mu.Lock()
|
||||||
|
streams := w.streams
|
||||||
|
w.streams = nil
|
||||||
|
w.mu.Unlock()
|
||||||
|
for _, wgs := range streams {
|
||||||
|
if werr := wgs.Close(); werr != nil {
|
||||||
|
err = werr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *watchGrpcStream) Close() (err error) {
|
||||||
|
w.cancel()
|
||||||
|
<-w.donec
|
||||||
|
select {
|
||||||
|
case err = <-w.errc:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return toErr(w.ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *watcher) closeStream(wgs *watchGrpcStream) {
|
||||||
|
w.mu.Lock()
|
||||||
|
close(wgs.donec)
|
||||||
|
wgs.cancel()
|
||||||
|
if w.streams != nil {
|
||||||
|
delete(w.streams, wgs.ctxKey)
|
||||||
|
}
|
||||||
|
w.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *watchGrpcStream) addSubstream(resp *pb.WatchResponse, ws *watcherStream) {
|
||||||
|
if resp.WatchId == -1 {
|
||||||
|
// failed; no channel
|
||||||
|
close(ws.recvc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ws.id = resp.WatchId
|
||||||
|
w.substreams[ws.id] = ws
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *watchGrpcStream) sendCloseSubstream(ws *watcherStream, resp *WatchResponse) {
|
||||||
|
select {
|
||||||
|
case ws.outc <- *resp:
|
||||||
|
case <-ws.initReq.ctx.Done():
|
||||||
|
case <-time.After(closeSendErrTimeout):
|
||||||
|
}
|
||||||
|
close(ws.outc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *watchGrpcStream) closeSubstream(ws *watcherStream) {
|
||||||
|
// send channel response in case stream was never established
|
||||||
|
select {
|
||||||
|
case ws.initReq.retc <- ws.outc:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
// close subscriber's channel
|
||||||
|
if closeErr := w.closeErr; closeErr != nil && ws.initReq.ctx.Err() == nil {
|
||||||
|
go w.sendCloseSubstream(ws, &WatchResponse{closeErr: w.closeErr})
|
||||||
|
} else if ws.outc != nil {
|
||||||
|
close(ws.outc)
|
||||||
|
}
|
||||||
|
if ws.id != -1 {
|
||||||
|
delete(w.substreams, ws.id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := range w.resuming {
|
||||||
|
if w.resuming[i] == ws {
|
||||||
|
w.resuming[i] = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// run is the root of the goroutines for managing a watcher client
|
||||||
|
func (w *watchGrpcStream) run() {
|
||||||
|
var wc pb.Watch_WatchClient
|
||||||
|
var closeErr error
|
||||||
|
|
||||||
|
// substreams marked to close but goroutine still running; needed for
|
||||||
|
// avoiding double-closing recvc on grpc stream teardown
|
||||||
|
closing := make(map[*watcherStream]struct{})
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
w.closeErr = closeErr
|
||||||
|
// shutdown substreams and resuming substreams
|
||||||
|
for _, ws := range w.substreams {
|
||||||
|
if _, ok := closing[ws]; !ok {
|
||||||
|
close(ws.recvc)
|
||||||
|
closing[ws] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, ws := range w.resuming {
|
||||||
|
if _, ok := closing[ws]; ws != nil && !ok {
|
||||||
|
close(ws.recvc)
|
||||||
|
closing[ws] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.joinSubstreams()
|
||||||
|
for range closing {
|
||||||
|
w.closeSubstream(<-w.closingc)
|
||||||
|
}
|
||||||
|
w.wg.Wait()
|
||||||
|
w.owner.closeStream(w)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// start a stream with the etcd grpc server
|
||||||
|
if wc, closeErr = w.newWatchClient(); closeErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelSet := make(map[int64]struct{})
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
// Watch() requested
|
||||||
|
case wreq := <-w.reqc:
|
||||||
|
outc := make(chan WatchResponse, 1)
|
||||||
|
ws := &watcherStream{
|
||||||
|
initReq: *wreq,
|
||||||
|
id: -1,
|
||||||
|
outc: outc,
|
||||||
|
// unbufffered so resumes won't cause repeat events
|
||||||
|
recvc: make(chan *WatchResponse),
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.donec = make(chan struct{})
|
||||||
|
w.wg.Add(1)
|
||||||
|
go w.serveSubstream(ws, w.resumec)
|
||||||
|
|
||||||
|
// queue up for watcher creation/resume
|
||||||
|
w.resuming = append(w.resuming, ws)
|
||||||
|
if len(w.resuming) == 1 {
|
||||||
|
// head of resume queue, can register a new watcher
|
||||||
|
wc.Send(ws.initReq.toPB())
|
||||||
|
}
|
||||||
|
// New events from the watch client
|
||||||
|
case pbresp := <-w.respc:
|
||||||
|
switch {
|
||||||
|
case pbresp.Created:
|
||||||
|
// response to head of queue creation
|
||||||
|
if ws := w.resuming[0]; ws != nil {
|
||||||
|
w.addSubstream(pbresp, ws)
|
||||||
|
w.dispatchEvent(pbresp)
|
||||||
|
w.resuming[0] = nil
|
||||||
|
}
|
||||||
|
if ws := w.nextResume(); ws != nil {
|
||||||
|
wc.Send(ws.initReq.toPB())
|
||||||
|
}
|
||||||
|
case pbresp.Canceled:
|
||||||
|
delete(cancelSet, pbresp.WatchId)
|
||||||
|
if ws, ok := w.substreams[pbresp.WatchId]; ok {
|
||||||
|
// signal to stream goroutine to update closingc
|
||||||
|
close(ws.recvc)
|
||||||
|
closing[ws] = struct{}{}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// dispatch to appropriate watch stream
|
||||||
|
if ok := w.dispatchEvent(pbresp); ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// watch response on unexpected watch id; cancel id
|
||||||
|
if _, ok := cancelSet[pbresp.WatchId]; ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cancelSet[pbresp.WatchId] = struct{}{}
|
||||||
|
cr := &pb.WatchRequest_CancelRequest{
|
||||||
|
CancelRequest: &pb.WatchCancelRequest{
|
||||||
|
WatchId: pbresp.WatchId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
req := &pb.WatchRequest{RequestUnion: cr}
|
||||||
|
wc.Send(req)
|
||||||
|
}
|
||||||
|
// watch client failed to recv; spawn another if possible
|
||||||
|
case err := <-w.errc:
|
||||||
|
if isHaltErr(w.ctx, err) || toErr(w.ctx, err) == v3rpc.ErrNoLeader {
|
||||||
|
closeErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if wc, closeErr = w.newWatchClient(); closeErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ws := w.nextResume(); ws != nil {
|
||||||
|
wc.Send(ws.initReq.toPB())
|
||||||
|
}
|
||||||
|
cancelSet = make(map[int64]struct{})
|
||||||
|
case <-w.ctx.Done():
|
||||||
|
return
|
||||||
|
case ws := <-w.closingc:
|
||||||
|
w.closeSubstream(ws)
|
||||||
|
delete(closing, ws)
|
||||||
|
if len(w.substreams)+len(w.resuming) == 0 {
|
||||||
|
// no more watchers on this stream, shutdown
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// nextResume chooses the next resuming to register with the grpc stream. Abandoned
|
||||||
|
// streams are marked as nil in the queue since the head must wait for its inflight registration.
|
||||||
|
func (w *watchGrpcStream) nextResume() *watcherStream {
|
||||||
|
for len(w.resuming) != 0 {
|
||||||
|
if w.resuming[0] != nil {
|
||||||
|
return w.resuming[0]
|
||||||
|
}
|
||||||
|
w.resuming = w.resuming[1:len(w.resuming)]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// dispatchEvent sends a WatchResponse to the appropriate watcher stream
|
||||||
|
func (w *watchGrpcStream) dispatchEvent(pbresp *pb.WatchResponse) bool {
|
||||||
|
events := make([]*Event, len(pbresp.Events))
|
||||||
|
for i, ev := range pbresp.Events {
|
||||||
|
events[i] = (*Event)(ev)
|
||||||
|
}
|
||||||
|
wr := &WatchResponse{
|
||||||
|
Header: *pbresp.Header,
|
||||||
|
Events: events,
|
||||||
|
CompactRevision: pbresp.CompactRevision,
|
||||||
|
Created: pbresp.Created,
|
||||||
|
Canceled: pbresp.Canceled,
|
||||||
|
cancelReason: pbresp.CancelReason,
|
||||||
|
}
|
||||||
|
ws, ok := w.substreams[pbresp.WatchId]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case ws.recvc <- wr:
|
||||||
|
case <-ws.donec:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// serveWatchClient forwards messages from the grpc stream to run()
|
||||||
|
func (w *watchGrpcStream) serveWatchClient(wc pb.Watch_WatchClient) {
|
||||||
|
for {
|
||||||
|
resp, err := wc.Recv()
|
||||||
|
if err != nil {
|
||||||
|
select {
|
||||||
|
case w.errc <- err:
|
||||||
|
case <-w.donec:
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case w.respc <- resp:
|
||||||
|
case <-w.donec:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// serveSubstream forwards watch responses from run() to the subscriber
|
||||||
|
func (w *watchGrpcStream) serveSubstream(ws *watcherStream, resumec chan struct{}) {
|
||||||
|
if ws.closing {
|
||||||
|
panic("created substream goroutine but substream is closing")
|
||||||
|
}
|
||||||
|
|
||||||
|
// nextRev is the minimum expected next revision
|
||||||
|
nextRev := ws.initReq.rev
|
||||||
|
resuming := false
|
||||||
|
defer func() {
|
||||||
|
if !resuming {
|
||||||
|
ws.closing = true
|
||||||
|
}
|
||||||
|
close(ws.donec)
|
||||||
|
if !resuming {
|
||||||
|
w.closingc <- ws
|
||||||
|
}
|
||||||
|
w.wg.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
emptyWr := &WatchResponse{}
|
||||||
|
for {
|
||||||
|
curWr := emptyWr
|
||||||
|
outc := ws.outc
|
||||||
|
|
||||||
|
if len(ws.buf) > 0 {
|
||||||
|
curWr = ws.buf[0]
|
||||||
|
} else {
|
||||||
|
outc = nil
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case outc <- *curWr:
|
||||||
|
if ws.buf[0].Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ws.buf[0] = nil
|
||||||
|
ws.buf = ws.buf[1:]
|
||||||
|
case wr, ok := <-ws.recvc:
|
||||||
|
if !ok {
|
||||||
|
// shutdown from closeSubstream
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if wr.Created {
|
||||||
|
if ws.initReq.retc != nil {
|
||||||
|
ws.initReq.retc <- ws.outc
|
||||||
|
// to prevent next write from taking the slot in buffered channel
|
||||||
|
// and posting duplicate create events
|
||||||
|
ws.initReq.retc = nil
|
||||||
|
|
||||||
|
// send first creation event only if requested
|
||||||
|
if ws.initReq.createdNotify {
|
||||||
|
ws.outc <- *wr
|
||||||
|
}
|
||||||
|
// once the watch channel is returned, a current revision
|
||||||
|
// watch must resume at the store revision. This is necessary
|
||||||
|
// for the following case to work as expected:
|
||||||
|
// wch := m1.Watch("a")
|
||||||
|
// m2.Put("a", "b")
|
||||||
|
// <-wch
|
||||||
|
// If the revision is only bound on the first observed event,
|
||||||
|
// if wch is disconnected before the Put is issued, then reconnects
|
||||||
|
// after it is committed, it'll miss the Put.
|
||||||
|
if ws.initReq.rev == 0 {
|
||||||
|
nextRev = wr.Header.Revision
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// current progress of watch; <= store revision
|
||||||
|
nextRev = wr.Header.Revision
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(wr.Events) > 0 {
|
||||||
|
nextRev = wr.Events[len(wr.Events)-1].Kv.ModRevision + 1
|
||||||
|
}
|
||||||
|
ws.initReq.rev = nextRev
|
||||||
|
|
||||||
|
// created event is already sent above,
|
||||||
|
// watcher should not post duplicate events
|
||||||
|
if wr.Created {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO pause channel if buffer gets too large
|
||||||
|
ws.buf = append(ws.buf, wr)
|
||||||
|
case <-w.ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ws.initReq.ctx.Done():
|
||||||
|
return
|
||||||
|
case <-resumec:
|
||||||
|
resuming = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// lazily send cancel message if events on missing id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *watchGrpcStream) newWatchClient() (pb.Watch_WatchClient, error) {
|
||||||
|
// mark all substreams as resuming
|
||||||
|
close(w.resumec)
|
||||||
|
w.resumec = make(chan struct{})
|
||||||
|
w.joinSubstreams()
|
||||||
|
for _, ws := range w.substreams {
|
||||||
|
ws.id = -1
|
||||||
|
w.resuming = append(w.resuming, ws)
|
||||||
|
}
|
||||||
|
// strip out nils, if any
|
||||||
|
var resuming []*watcherStream
|
||||||
|
for _, ws := range w.resuming {
|
||||||
|
if ws != nil {
|
||||||
|
resuming = append(resuming, ws)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.resuming = resuming
|
||||||
|
w.substreams = make(map[int64]*watcherStream)
|
||||||
|
|
||||||
|
// connect to grpc stream while accepting watcher cancelation
|
||||||
|
stopc := make(chan struct{})
|
||||||
|
donec := w.waitCancelSubstreams(stopc)
|
||||||
|
wc, err := w.openWatchClient()
|
||||||
|
close(stopc)
|
||||||
|
<-donec
|
||||||
|
|
||||||
|
// serve all non-closing streams, even if there's a client error
|
||||||
|
// so that the teardown path can shutdown the streams as expected.
|
||||||
|
for _, ws := range w.resuming {
|
||||||
|
if ws.closing {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ws.donec = make(chan struct{})
|
||||||
|
w.wg.Add(1)
|
||||||
|
go w.serveSubstream(ws, w.resumec)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, v3rpc.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// receive data from new grpc stream
|
||||||
|
go w.serveWatchClient(wc)
|
||||||
|
return wc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *watchGrpcStream) waitCancelSubstreams(stopc <-chan struct{}) <-chan struct{} {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(len(w.resuming))
|
||||||
|
donec := make(chan struct{})
|
||||||
|
for i := range w.resuming {
|
||||||
|
go func(ws *watcherStream) {
|
||||||
|
defer wg.Done()
|
||||||
|
if ws.closing {
|
||||||
|
if ws.initReq.ctx.Err() != nil && ws.outc != nil {
|
||||||
|
close(ws.outc)
|
||||||
|
ws.outc = nil
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-ws.initReq.ctx.Done():
|
||||||
|
// closed ws will be removed from resuming
|
||||||
|
ws.closing = true
|
||||||
|
close(ws.outc)
|
||||||
|
ws.outc = nil
|
||||||
|
w.wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer w.wg.Done()
|
||||||
|
w.closingc <- ws
|
||||||
|
}()
|
||||||
|
case <-stopc:
|
||||||
|
}
|
||||||
|
}(w.resuming[i])
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
defer close(donec)
|
||||||
|
wg.Wait()
|
||||||
|
}()
|
||||||
|
return donec
|
||||||
|
}
|
||||||
|
|
||||||
|
// joinSubstream waits for all substream goroutines to complete
|
||||||
|
func (w *watchGrpcStream) joinSubstreams() {
|
||||||
|
for _, ws := range w.substreams {
|
||||||
|
<-ws.donec
|
||||||
|
}
|
||||||
|
for _, ws := range w.resuming {
|
||||||
|
if ws != nil {
|
||||||
|
<-ws.donec
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// openWatchClient retries opening a watchclient until retryConnection fails
|
||||||
|
func (w *watchGrpcStream) openWatchClient() (ws pb.Watch_WatchClient, err error) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-w.ctx.Done():
|
||||||
|
if err == nil {
|
||||||
|
return nil, w.ctx.Err()
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
if ws, err = w.remote.Watch(w.ctx, grpc.FailFast(false)); ws != nil && err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if isHaltErr(w.ctx, err) {
|
||||||
|
return nil, v3rpc.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ws, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// toPB converts an internal watch request structure to its protobuf messagefunc (wr *watchRequest)
|
||||||
|
func (wr *watchRequest) toPB() *pb.WatchRequest {
|
||||||
|
req := &pb.WatchCreateRequest{
|
||||||
|
StartRevision: wr.rev,
|
||||||
|
Key: []byte(wr.key),
|
||||||
|
RangeEnd: []byte(wr.end),
|
||||||
|
ProgressNotify: wr.progressNotify,
|
||||||
|
Filters: wr.filters,
|
||||||
|
PrevKv: wr.prevKV,
|
||||||
|
}
|
||||||
|
cr := &pb.WatchRequest_CreateRequest{CreateRequest: req}
|
||||||
|
return &pb.WatchRequest{RequestUnion: cr}
|
||||||
|
}
|
16
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes/doc.go
generated
vendored
Normal file
16
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes/doc.go
generated
vendored
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
// Copyright 2016 The etcd Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// Package rpctypes has types and values shared by the etcd server and client for v3 RPC interaction.
|
||||||
|
package rpctypes
|
193
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes/error.go
generated
vendored
Normal file
193
vendor/github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes/error.go
generated
vendored
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
// Copyright 2015 The etcd Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package rpctypes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// server-side error
|
||||||
|
ErrGRPCEmptyKey = grpc.Errorf(codes.InvalidArgument, "etcdserver: key is not provided")
|
||||||
|
ErrGRPCKeyNotFound = grpc.Errorf(codes.InvalidArgument, "etcdserver: key not found")
|
||||||
|
ErrGRPCValueProvided = grpc.Errorf(codes.InvalidArgument, "etcdserver: value is provided")
|
||||||
|
ErrGRPCLeaseProvided = grpc.Errorf(codes.InvalidArgument, "etcdserver: lease is provided")
|
||||||
|
ErrGRPCTooManyOps = grpc.Errorf(codes.InvalidArgument, "etcdserver: too many operations in txn request")
|
||||||
|
ErrGRPCDuplicateKey = grpc.Errorf(codes.InvalidArgument, "etcdserver: duplicate key given in txn request")
|
||||||
|
ErrGRPCCompacted = grpc.Errorf(codes.OutOfRange, "etcdserver: mvcc: required revision has been compacted")
|
||||||
|
ErrGRPCFutureRev = grpc.Errorf(codes.OutOfRange, "etcdserver: mvcc: required revision is a future revision")
|
||||||
|
ErrGRPCNoSpace = grpc.Errorf(codes.ResourceExhausted, "etcdserver: mvcc: database space exceeded")
|
||||||
|
|
||||||
|
ErrGRPCLeaseNotFound = grpc.Errorf(codes.NotFound, "etcdserver: requested lease not found")
|
||||||
|
ErrGRPCLeaseExist = grpc.Errorf(codes.FailedPrecondition, "etcdserver: lease already exists")
|
||||||
|
|
||||||
|
ErrGRPCMemberExist = grpc.Errorf(codes.FailedPrecondition, "etcdserver: member ID already exist")
|
||||||
|
ErrGRPCPeerURLExist = grpc.Errorf(codes.FailedPrecondition, "etcdserver: Peer URLs already exists")
|
||||||
|
ErrGRPCMemberNotEnoughStarted = grpc.Errorf(codes.FailedPrecondition, "etcdserver: re-configuration failed due to not enough started members")
|
||||||
|
ErrGRPCMemberBadURLs = grpc.Errorf(codes.InvalidArgument, "etcdserver: given member URLs are invalid")
|
||||||
|
ErrGRPCMemberNotFound = grpc.Errorf(codes.NotFound, "etcdserver: member not found")
|
||||||
|
|
||||||
|
ErrGRPCRequestTooLarge = grpc.Errorf(codes.InvalidArgument, "etcdserver: request is too large")
|
||||||
|
ErrGRPCRequestTooManyRequests = grpc.Errorf(codes.ResourceExhausted, "etcdserver: too many requests")
|
||||||
|
|
||||||
|
ErrGRPCRootUserNotExist = grpc.Errorf(codes.FailedPrecondition, "etcdserver: root user does not exist")
|
||||||
|
ErrGRPCRootRoleNotExist = grpc.Errorf(codes.FailedPrecondition, "etcdserver: root user does not have root role")
|
||||||
|
ErrGRPCUserAlreadyExist = grpc.Errorf(codes.FailedPrecondition, "etcdserver: user name already exists")
|
||||||
|
ErrGRPCUserEmpty = grpc.Errorf(codes.InvalidArgument, "etcdserver: user name is empty")
|
||||||
|
ErrGRPCUserNotFound = grpc.Errorf(codes.FailedPrecondition, "etcdserver: user name not found")
|
||||||
|
ErrGRPCRoleAlreadyExist = grpc.Errorf(codes.FailedPrecondition, "etcdserver: role name already exists")
|
||||||
|
ErrGRPCRoleNotFound = grpc.Errorf(codes.FailedPrecondition, "etcdserver: role name not found")
|
||||||
|
ErrGRPCAuthFailed = grpc.Errorf(codes.InvalidArgument, "etcdserver: authentication failed, invalid user ID or password")
|
||||||
|
ErrGRPCPermissionDenied = grpc.Errorf(codes.PermissionDenied, "etcdserver: permission denied")
|
||||||
|
ErrGRPCRoleNotGranted = grpc.Errorf(codes.FailedPrecondition, "etcdserver: role is not granted to the user")
|
||||||
|
ErrGRPCPermissionNotGranted = grpc.Errorf(codes.FailedPrecondition, "etcdserver: permission is not granted to the role")
|
||||||
|
ErrGRPCAuthNotEnabled = grpc.Errorf(codes.FailedPrecondition, "etcdserver: authentication is not enabled")
|
||||||
|
ErrGRPCInvalidAuthToken = grpc.Errorf(codes.Unauthenticated, "etcdserver: invalid auth token")
|
||||||
|
ErrGRPCInvalidAuthMgmt = grpc.Errorf(codes.InvalidArgument, "etcdserver: invalid auth management")
|
||||||
|
|
||||||
|
ErrGRPCNoLeader = grpc.Errorf(codes.Unavailable, "etcdserver: no leader")
|
||||||
|
ErrGRPCNotLeader = grpc.Errorf(codes.Unavailable, "etcdserver: not leader")
|
||||||
|
ErrGRPCNotCapable = grpc.Errorf(codes.Unavailable, "etcdserver: not capable")
|
||||||
|
ErrGRPCStopped = grpc.Errorf(codes.Unavailable, "etcdserver: server stopped")
|
||||||
|
ErrGRPCTimeout = grpc.Errorf(codes.Unavailable, "etcdserver: request timed out")
|
||||||
|
ErrGRPCTimeoutDueToLeaderFail = grpc.Errorf(codes.Unavailable, "etcdserver: request timed out, possibly due to previous leader failure")
|
||||||
|
ErrGRPCTimeoutDueToConnectionLost = grpc.Errorf(codes.Unavailable, "etcdserver: request timed out, possibly due to connection lost")
|
||||||
|
ErrGRPCUnhealthy = grpc.Errorf(codes.Unavailable, "etcdserver: unhealthy cluster")
|
||||||
|
|
||||||
|
errStringToError = map[string]error{
|
||||||
|
grpc.ErrorDesc(ErrGRPCEmptyKey): ErrGRPCEmptyKey,
|
||||||
|
grpc.ErrorDesc(ErrGRPCKeyNotFound): ErrGRPCKeyNotFound,
|
||||||
|
grpc.ErrorDesc(ErrGRPCValueProvided): ErrGRPCValueProvided,
|
||||||
|
grpc.ErrorDesc(ErrGRPCLeaseProvided): ErrGRPCLeaseProvided,
|
||||||
|
|
||||||
|
grpc.ErrorDesc(ErrGRPCTooManyOps): ErrGRPCTooManyOps,
|
||||||
|
grpc.ErrorDesc(ErrGRPCDuplicateKey): ErrGRPCDuplicateKey,
|
||||||
|
grpc.ErrorDesc(ErrGRPCCompacted): ErrGRPCCompacted,
|
||||||
|
grpc.ErrorDesc(ErrGRPCFutureRev): ErrGRPCFutureRev,
|
||||||
|
grpc.ErrorDesc(ErrGRPCNoSpace): ErrGRPCNoSpace,
|
||||||
|
|
||||||
|
grpc.ErrorDesc(ErrGRPCLeaseNotFound): ErrGRPCLeaseNotFound,
|
||||||
|
grpc.ErrorDesc(ErrGRPCLeaseExist): ErrGRPCLeaseExist,
|
||||||
|
|
||||||
|
grpc.ErrorDesc(ErrGRPCMemberExist): ErrGRPCMemberExist,
|
||||||
|
grpc.ErrorDesc(ErrGRPCPeerURLExist): ErrGRPCPeerURLExist,
|
||||||
|
grpc.ErrorDesc(ErrGRPCMemberNotEnoughStarted): ErrGRPCMemberNotEnoughStarted,
|
||||||
|
grpc.ErrorDesc(ErrGRPCMemberBadURLs): ErrGRPCMemberBadURLs,
|
||||||
|
grpc.ErrorDesc(ErrGRPCMemberNotFound): ErrGRPCMemberNotFound,
|
||||||
|
|
||||||
|
grpc.ErrorDesc(ErrGRPCRequestTooLarge): ErrGRPCRequestTooLarge,
|
||||||
|
grpc.ErrorDesc(ErrGRPCRequestTooManyRequests): ErrGRPCRequestTooManyRequests,
|
||||||
|
|
||||||
|
grpc.ErrorDesc(ErrGRPCRootUserNotExist): ErrGRPCRootUserNotExist,
|
||||||
|
grpc.ErrorDesc(ErrGRPCRootRoleNotExist): ErrGRPCRootRoleNotExist,
|
||||||
|
grpc.ErrorDesc(ErrGRPCUserAlreadyExist): ErrGRPCUserAlreadyExist,
|
||||||
|
grpc.ErrorDesc(ErrGRPCUserEmpty): ErrGRPCUserEmpty,
|
||||||
|
grpc.ErrorDesc(ErrGRPCUserNotFound): ErrGRPCUserNotFound,
|
||||||
|
grpc.ErrorDesc(ErrGRPCRoleAlreadyExist): ErrGRPCRoleAlreadyExist,
|
||||||
|
grpc.ErrorDesc(ErrGRPCRoleNotFound): ErrGRPCRoleNotFound,
|
||||||
|
grpc.ErrorDesc(ErrGRPCAuthFailed): ErrGRPCAuthFailed,
|
||||||
|
grpc.ErrorDesc(ErrGRPCPermissionDenied): ErrGRPCPermissionDenied,
|
||||||
|
grpc.ErrorDesc(ErrGRPCRoleNotGranted): ErrGRPCRoleNotGranted,
|
||||||
|
grpc.ErrorDesc(ErrGRPCPermissionNotGranted): ErrGRPCPermissionNotGranted,
|
||||||
|
grpc.ErrorDesc(ErrGRPCAuthNotEnabled): ErrGRPCAuthNotEnabled,
|
||||||
|
grpc.ErrorDesc(ErrGRPCInvalidAuthToken): ErrGRPCInvalidAuthToken,
|
||||||
|
grpc.ErrorDesc(ErrGRPCInvalidAuthMgmt): ErrGRPCInvalidAuthMgmt,
|
||||||
|
|
||||||
|
grpc.ErrorDesc(ErrGRPCNoLeader): ErrGRPCNoLeader,
|
||||||
|
grpc.ErrorDesc(ErrGRPCNotLeader): ErrGRPCNotLeader,
|
||||||
|
grpc.ErrorDesc(ErrGRPCNotCapable): ErrGRPCNotCapable,
|
||||||
|
grpc.ErrorDesc(ErrGRPCStopped): ErrGRPCStopped,
|
||||||
|
grpc.ErrorDesc(ErrGRPCTimeout): ErrGRPCTimeout,
|
||||||
|
grpc.ErrorDesc(ErrGRPCTimeoutDueToLeaderFail): ErrGRPCTimeoutDueToLeaderFail,
|
||||||
|
grpc.ErrorDesc(ErrGRPCTimeoutDueToConnectionLost): ErrGRPCTimeoutDueToConnectionLost,
|
||||||
|
grpc.ErrorDesc(ErrGRPCUnhealthy): ErrGRPCUnhealthy,
|
||||||
|
}
|
||||||
|
|
||||||
|
// client-side error
|
||||||
|
ErrEmptyKey = Error(ErrGRPCEmptyKey)
|
||||||
|
ErrKeyNotFound = Error(ErrGRPCKeyNotFound)
|
||||||
|
ErrValueProvided = Error(ErrGRPCValueProvided)
|
||||||
|
ErrLeaseProvided = Error(ErrGRPCLeaseProvided)
|
||||||
|
ErrTooManyOps = Error(ErrGRPCTooManyOps)
|
||||||
|
ErrDuplicateKey = Error(ErrGRPCDuplicateKey)
|
||||||
|
ErrCompacted = Error(ErrGRPCCompacted)
|
||||||
|
ErrFutureRev = Error(ErrGRPCFutureRev)
|
||||||
|
ErrNoSpace = Error(ErrGRPCNoSpace)
|
||||||
|
|
||||||
|
ErrLeaseNotFound = Error(ErrGRPCLeaseNotFound)
|
||||||
|
ErrLeaseExist = Error(ErrGRPCLeaseExist)
|
||||||
|
|
||||||
|
ErrMemberExist = Error(ErrGRPCMemberExist)
|
||||||
|
ErrPeerURLExist = Error(ErrGRPCPeerURLExist)
|
||||||
|
ErrMemberNotEnoughStarted = Error(ErrGRPCMemberNotEnoughStarted)
|
||||||
|
ErrMemberBadURLs = Error(ErrGRPCMemberBadURLs)
|
||||||
|
ErrMemberNotFound = Error(ErrGRPCMemberNotFound)
|
||||||
|
|
||||||
|
ErrRequestTooLarge = Error(ErrGRPCRequestTooLarge)
|
||||||
|
ErrTooManyRequests = Error(ErrGRPCRequestTooManyRequests)
|
||||||
|
|
||||||
|
ErrRootUserNotExist = Error(ErrGRPCRootUserNotExist)
|
||||||
|
ErrRootRoleNotExist = Error(ErrGRPCRootRoleNotExist)
|
||||||
|
ErrUserAlreadyExist = Error(ErrGRPCUserAlreadyExist)
|
||||||
|
ErrUserEmpty = Error(ErrGRPCUserEmpty)
|
||||||
|
ErrUserNotFound = Error(ErrGRPCUserNotFound)
|
||||||
|
ErrRoleAlreadyExist = Error(ErrGRPCRoleAlreadyExist)
|
||||||
|
ErrRoleNotFound = Error(ErrGRPCRoleNotFound)
|
||||||
|
ErrAuthFailed = Error(ErrGRPCAuthFailed)
|
||||||
|
ErrPermissionDenied = Error(ErrGRPCPermissionDenied)
|
||||||
|
ErrRoleNotGranted = Error(ErrGRPCRoleNotGranted)
|
||||||
|
ErrPermissionNotGranted = Error(ErrGRPCPermissionNotGranted)
|
||||||
|
ErrAuthNotEnabled = Error(ErrGRPCAuthNotEnabled)
|
||||||
|
ErrInvalidAuthToken = Error(ErrGRPCInvalidAuthToken)
|
||||||
|
ErrInvalidAuthMgmt = Error(ErrGRPCInvalidAuthMgmt)
|
||||||
|
|
||||||
|
ErrNoLeader = Error(ErrGRPCNoLeader)
|
||||||
|
ErrNotLeader = Error(ErrGRPCNotLeader)
|
||||||
|
ErrNotCapable = Error(ErrGRPCNotCapable)
|
||||||
|
ErrStopped = Error(ErrGRPCStopped)
|
||||||
|
ErrTimeout = Error(ErrGRPCTimeout)
|
||||||
|
ErrTimeoutDueToLeaderFail = Error(ErrGRPCTimeoutDueToLeaderFail)
|
||||||
|
ErrTimeoutDueToConnectionLost = Error(ErrGRPCTimeoutDueToConnectionLost)
|
||||||
|
ErrUnhealthy = Error(ErrGRPCUnhealthy)
|
||||||
|
)
|
||||||
|
|
||||||
|
// EtcdError defines gRPC server errors.
|
||||||
|
// (https://github.com/grpc/grpc-go/blob/master/rpc_util.go#L319-L323)
|
||||||
|
type EtcdError struct {
|
||||||
|
code codes.Code
|
||||||
|
desc string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Code returns grpc/codes.Code.
|
||||||
|
// TODO: define clientv3/codes.Code.
|
||||||
|
func (e EtcdError) Code() codes.Code {
|
||||||
|
return e.code
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e EtcdError) Error() string {
|
||||||
|
return e.desc
|
||||||
|
}
|
||||||
|
|
||||||
|
func Error(err error) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
verr, ok := errStringToError[grpc.ErrorDesc(err)]
|
||||||
|
if !ok { // not gRPC error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return EtcdError{code: grpc.Code(verr), desc: grpc.ErrorDesc(verr)}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
// Copyright 2016 The etcd Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package rpctypes
|
||||||
|
|
||||||
|
var (
|
||||||
|
MetadataRequireLeaderKey = "hasleader"
|
||||||
|
MetadataHasLeader = "true"
|
||||||
|
)
|
1049
vendor/github.com/coreos/etcd/etcdserver/etcdserverpb/etcdserver.pb.go
generated
vendored
Normal file
1049
vendor/github.com/coreos/etcd/etcdserver/etcdserverpb/etcdserver.pb.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
34
vendor/github.com/coreos/etcd/etcdserver/etcdserverpb/etcdserver.proto
generated
vendored
Normal file
34
vendor/github.com/coreos/etcd/etcdserver/etcdserverpb/etcdserver.proto
generated
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
syntax = "proto2";
|
||||||
|
package etcdserverpb;
|
||||||
|
|
||||||
|
import "gogoproto/gogo.proto";
|
||||||
|
|
||||||
|
option (gogoproto.marshaler_all) = true;
|
||||||
|
option (gogoproto.sizer_all) = true;
|
||||||
|
option (gogoproto.unmarshaler_all) = true;
|
||||||
|
option (gogoproto.goproto_getters_all) = false;
|
||||||
|
|
||||||
|
message Request {
|
||||||
|
optional uint64 ID = 1 [(gogoproto.nullable) = false];
|
||||||
|
optional string Method = 2 [(gogoproto.nullable) = false];
|
||||||
|
optional string Path = 3 [(gogoproto.nullable) = false];
|
||||||
|
optional string Val = 4 [(gogoproto.nullable) = false];
|
||||||
|
optional bool Dir = 5 [(gogoproto.nullable) = false];
|
||||||
|
optional string PrevValue = 6 [(gogoproto.nullable) = false];
|
||||||
|
optional uint64 PrevIndex = 7 [(gogoproto.nullable) = false];
|
||||||
|
optional bool PrevExist = 8 [(gogoproto.nullable) = true];
|
||||||
|
optional int64 Expiration = 9 [(gogoproto.nullable) = false];
|
||||||
|
optional bool Wait = 10 [(gogoproto.nullable) = false];
|
||||||
|
optional uint64 Since = 11 [(gogoproto.nullable) = false];
|
||||||
|
optional bool Recursive = 12 [(gogoproto.nullable) = false];
|
||||||
|
optional bool Sorted = 13 [(gogoproto.nullable) = false];
|
||||||
|
optional bool Quorum = 14 [(gogoproto.nullable) = false];
|
||||||
|
optional int64 Time = 15 [(gogoproto.nullable) = false];
|
||||||
|
optional bool Stream = 16 [(gogoproto.nullable) = false];
|
||||||
|
optional bool Refresh = 17 [(gogoproto.nullable) = true];
|
||||||
|
}
|
||||||
|
|
||||||
|
message Metadata {
|
||||||
|
optional uint64 NodeID = 1 [(gogoproto.nullable) = false];
|
||||||
|
optional uint64 ClusterID = 2 [(gogoproto.nullable) = false];
|
||||||
|
}
|
2094
vendor/github.com/coreos/etcd/etcdserver/etcdserverpb/raft_internal.pb.go
generated
vendored
Normal file
2094
vendor/github.com/coreos/etcd/etcdserver/etcdserverpb/raft_internal.pb.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
74
vendor/github.com/coreos/etcd/etcdserver/etcdserverpb/raft_internal.proto
generated
vendored
Normal file
74
vendor/github.com/coreos/etcd/etcdserver/etcdserverpb/raft_internal.proto
generated
vendored
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
package etcdserverpb;
|
||||||
|
|
||||||
|
import "gogoproto/gogo.proto";
|
||||||
|
import "etcdserver.proto";
|
||||||
|
import "rpc.proto";
|
||||||
|
|
||||||
|
option (gogoproto.marshaler_all) = true;
|
||||||
|
option (gogoproto.sizer_all) = true;
|
||||||
|
option (gogoproto.unmarshaler_all) = true;
|
||||||
|
option (gogoproto.goproto_getters_all) = false;
|
||||||
|
|
||||||
|
message RequestHeader {
|
||||||
|
uint64 ID = 1;
|
||||||
|
// username is a username that is associated with an auth token of gRPC connection
|
||||||
|
string username = 2;
|
||||||
|
// auth_revision is a revision number of auth.authStore. It is not related to mvcc
|
||||||
|
uint64 auth_revision = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An InternalRaftRequest is the union of all requests which can be
|
||||||
|
// sent via raft.
|
||||||
|
message InternalRaftRequest {
|
||||||
|
RequestHeader header = 100;
|
||||||
|
uint64 ID = 1;
|
||||||
|
|
||||||
|
Request v2 = 2;
|
||||||
|
|
||||||
|
RangeRequest range = 3;
|
||||||
|
PutRequest put = 4;
|
||||||
|
DeleteRangeRequest delete_range = 5;
|
||||||
|
TxnRequest txn = 6;
|
||||||
|
CompactionRequest compaction = 7;
|
||||||
|
|
||||||
|
LeaseGrantRequest lease_grant = 8;
|
||||||
|
LeaseRevokeRequest lease_revoke = 9;
|
||||||
|
|
||||||
|
AlarmRequest alarm = 10;
|
||||||
|
|
||||||
|
AuthEnableRequest auth_enable = 1000;
|
||||||
|
AuthDisableRequest auth_disable = 1011;
|
||||||
|
|
||||||
|
InternalAuthenticateRequest authenticate = 1012;
|
||||||
|
|
||||||
|
AuthUserAddRequest auth_user_add = 1100;
|
||||||
|
AuthUserDeleteRequest auth_user_delete = 1101;
|
||||||
|
AuthUserGetRequest auth_user_get = 1102;
|
||||||
|
AuthUserChangePasswordRequest auth_user_change_password = 1103;
|
||||||
|
AuthUserGrantRoleRequest auth_user_grant_role = 1104;
|
||||||
|
AuthUserRevokeRoleRequest auth_user_revoke_role = 1105;
|
||||||
|
AuthUserListRequest auth_user_list = 1106;
|
||||||
|
AuthRoleListRequest auth_role_list = 1107;
|
||||||
|
|
||||||
|
AuthRoleAddRequest auth_role_add = 1200;
|
||||||
|
AuthRoleDeleteRequest auth_role_delete = 1201;
|
||||||
|
AuthRoleGetRequest auth_role_get = 1202;
|
||||||
|
AuthRoleGrantPermissionRequest auth_role_grant_permission = 1203;
|
||||||
|
AuthRoleRevokePermissionRequest auth_role_revoke_permission = 1204;
|
||||||
|
}
|
||||||
|
|
||||||
|
message EmptyResponse {
|
||||||
|
}
|
||||||
|
|
||||||
|
// What is the difference between AuthenticateRequest (defined in rpc.proto) and InternalAuthenticateRequest?
|
||||||
|
// InternalAuthenticateRequest has a member that is filled by etcdserver and shouldn't be user-facing.
|
||||||
|
// For avoiding misusage the field, we have an internal version of AuthenticateRequest.
|
||||||
|
message InternalAuthenticateRequest {
|
||||||
|
string name = 1;
|
||||||
|
string password = 2;
|
||||||
|
|
||||||
|
// simple_token is generated in API layer (etcdserver/v3_server.go)
|
||||||
|
string simple_token = 3;
|
||||||
|
}
|
||||||
|
|
18179
vendor/github.com/coreos/etcd/etcdserver/etcdserverpb/rpc.pb.go
generated
vendored
Normal file
18179
vendor/github.com/coreos/etcd/etcdserver/etcdserverpb/rpc.pb.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,735 @@
|
||||||
|
// Code generated by protoc-gen-gogo.
|
||||||
|
// source: kv.proto
|
||||||
|
// DO NOT EDIT!
|
||||||
|
|
||||||
|
/*
|
||||||
|
Package mvccpb is a generated protocol buffer package.
|
||||||
|
|
||||||
|
It is generated from these files:
|
||||||
|
kv.proto
|
||||||
|
|
||||||
|
It has these top-level messages:
|
||||||
|
KeyValue
|
||||||
|
Event
|
||||||
|
*/
|
||||||
|
package mvccpb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
proto "github.com/golang/protobuf/proto"
|
||||||
|
|
||||||
|
math "math"
|
||||||
|
|
||||||
|
io "io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reference imports to suppress errors if they are not otherwise used.
|
||||||
|
var _ = proto.Marshal
|
||||||
|
var _ = fmt.Errorf
|
||||||
|
var _ = math.Inf
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file
|
||||||
|
// is compatible with the proto package it is being compiled against.
|
||||||
|
// A compilation error at this line likely means your copy of the
|
||||||
|
// proto package needs to be updated.
|
||||||
|
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
|
||||||
|
|
||||||
|
type Event_EventType int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
PUT Event_EventType = 0
|
||||||
|
DELETE Event_EventType = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
var Event_EventType_name = map[int32]string{
|
||||||
|
0: "PUT",
|
||||||
|
1: "DELETE",
|
||||||
|
}
|
||||||
|
var Event_EventType_value = map[string]int32{
|
||||||
|
"PUT": 0,
|
||||||
|
"DELETE": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x Event_EventType) String() string {
|
||||||
|
return proto.EnumName(Event_EventType_name, int32(x))
|
||||||
|
}
|
||||||
|
func (Event_EventType) EnumDescriptor() ([]byte, []int) { return fileDescriptorKv, []int{1, 0} }
|
||||||
|
|
||||||
|
type KeyValue struct {
|
||||||
|
// key is the key in bytes. An empty key is not allowed.
|
||||||
|
Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
|
||||||
|
// create_revision is the revision of last creation on this key.
|
||||||
|
CreateRevision int64 `protobuf:"varint,2,opt,name=create_revision,json=createRevision,proto3" json:"create_revision,omitempty"`
|
||||||
|
// mod_revision is the revision of last modification on this key.
|
||||||
|
ModRevision int64 `protobuf:"varint,3,opt,name=mod_revision,json=modRevision,proto3" json:"mod_revision,omitempty"`
|
||||||
|
// version is the version of the key. A deletion resets
|
||||||
|
// the version to zero and any modification of the key
|
||||||
|
// increases its version.
|
||||||
|
Version int64 `protobuf:"varint,4,opt,name=version,proto3" json:"version,omitempty"`
|
||||||
|
// value is the value held by the key, in bytes.
|
||||||
|
Value []byte `protobuf:"bytes,5,opt,name=value,proto3" json:"value,omitempty"`
|
||||||
|
// lease is the ID of the lease that attached to key.
|
||||||
|
// When the attached lease expires, the key will be deleted.
|
||||||
|
// If lease is 0, then no lease is attached to the key.
|
||||||
|
Lease int64 `protobuf:"varint,6,opt,name=lease,proto3" json:"lease,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *KeyValue) Reset() { *m = KeyValue{} }
|
||||||
|
func (m *KeyValue) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*KeyValue) ProtoMessage() {}
|
||||||
|
func (*KeyValue) Descriptor() ([]byte, []int) { return fileDescriptorKv, []int{0} }
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
// type is the kind of event. If type is a PUT, it indicates
|
||||||
|
// new data has been stored to the key. If type is a DELETE,
|
||||||
|
// it indicates the key was deleted.
|
||||||
|
Type Event_EventType `protobuf:"varint,1,opt,name=type,proto3,enum=mvccpb.Event_EventType" json:"type,omitempty"`
|
||||||
|
// kv holds the KeyValue for the event.
|
||||||
|
// A PUT event contains current kv pair.
|
||||||
|
// A PUT event with kv.Version=1 indicates the creation of a key.
|
||||||
|
// A DELETE/EXPIRE event contains the deleted key with
|
||||||
|
// its modification revision set to the revision of deletion.
|
||||||
|
Kv *KeyValue `protobuf:"bytes,2,opt,name=kv" json:"kv,omitempty"`
|
||||||
|
// prev_kv holds the key-value pair before the event happens.
|
||||||
|
PrevKv *KeyValue `protobuf:"bytes,3,opt,name=prev_kv,json=prevKv" json:"prev_kv,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Event) Reset() { *m = Event{} }
|
||||||
|
func (m *Event) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*Event) ProtoMessage() {}
|
||||||
|
func (*Event) Descriptor() ([]byte, []int) { return fileDescriptorKv, []int{1} }
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
proto.RegisterType((*KeyValue)(nil), "mvccpb.KeyValue")
|
||||||
|
proto.RegisterType((*Event)(nil), "mvccpb.Event")
|
||||||
|
proto.RegisterEnum("mvccpb.Event_EventType", Event_EventType_name, Event_EventType_value)
|
||||||
|
}
|
||||||
|
func (m *KeyValue) Marshal() (dAtA []byte, err error) {
|
||||||
|
size := m.Size()
|
||||||
|
dAtA = make([]byte, size)
|
||||||
|
n, err := m.MarshalTo(dAtA)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return dAtA[:n], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *KeyValue) MarshalTo(dAtA []byte) (int, error) {
|
||||||
|
var i int
|
||||||
|
_ = i
|
||||||
|
var l int
|
||||||
|
_ = l
|
||||||
|
if len(m.Key) > 0 {
|
||||||
|
dAtA[i] = 0xa
|
||||||
|
i++
|
||||||
|
i = encodeVarintKv(dAtA, i, uint64(len(m.Key)))
|
||||||
|
i += copy(dAtA[i:], m.Key)
|
||||||
|
}
|
||||||
|
if m.CreateRevision != 0 {
|
||||||
|
dAtA[i] = 0x10
|
||||||
|
i++
|
||||||
|
i = encodeVarintKv(dAtA, i, uint64(m.CreateRevision))
|
||||||
|
}
|
||||||
|
if m.ModRevision != 0 {
|
||||||
|
dAtA[i] = 0x18
|
||||||
|
i++
|
||||||
|
i = encodeVarintKv(dAtA, i, uint64(m.ModRevision))
|
||||||
|
}
|
||||||
|
if m.Version != 0 {
|
||||||
|
dAtA[i] = 0x20
|
||||||
|
i++
|
||||||
|
i = encodeVarintKv(dAtA, i, uint64(m.Version))
|
||||||
|
}
|
||||||
|
if len(m.Value) > 0 {
|
||||||
|
dAtA[i] = 0x2a
|
||||||
|
i++
|
||||||
|
i = encodeVarintKv(dAtA, i, uint64(len(m.Value)))
|
||||||
|
i += copy(dAtA[i:], m.Value)
|
||||||
|
}
|
||||||
|
if m.Lease != 0 {
|
||||||
|
dAtA[i] = 0x30
|
||||||
|
i++
|
||||||
|
i = encodeVarintKv(dAtA, i, uint64(m.Lease))
|
||||||
|
}
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Event) Marshal() (dAtA []byte, err error) {
|
||||||
|
size := m.Size()
|
||||||
|
dAtA = make([]byte, size)
|
||||||
|
n, err := m.MarshalTo(dAtA)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return dAtA[:n], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Event) MarshalTo(dAtA []byte) (int, error) {
|
||||||
|
var i int
|
||||||
|
_ = i
|
||||||
|
var l int
|
||||||
|
_ = l
|
||||||
|
if m.Type != 0 {
|
||||||
|
dAtA[i] = 0x8
|
||||||
|
i++
|
||||||
|
i = encodeVarintKv(dAtA, i, uint64(m.Type))
|
||||||
|
}
|
||||||
|
if m.Kv != nil {
|
||||||
|
dAtA[i] = 0x12
|
||||||
|
i++
|
||||||
|
i = encodeVarintKv(dAtA, i, uint64(m.Kv.Size()))
|
||||||
|
n1, err := m.Kv.MarshalTo(dAtA[i:])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
i += n1
|
||||||
|
}
|
||||||
|
if m.PrevKv != nil {
|
||||||
|
dAtA[i] = 0x1a
|
||||||
|
i++
|
||||||
|
i = encodeVarintKv(dAtA, i, uint64(m.PrevKv.Size()))
|
||||||
|
n2, err := m.PrevKv.MarshalTo(dAtA[i:])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
i += n2
|
||||||
|
}
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeFixed64Kv(dAtA []byte, offset int, v uint64) int {
|
||||||
|
dAtA[offset] = uint8(v)
|
||||||
|
dAtA[offset+1] = uint8(v >> 8)
|
||||||
|
dAtA[offset+2] = uint8(v >> 16)
|
||||||
|
dAtA[offset+3] = uint8(v >> 24)
|
||||||
|
dAtA[offset+4] = uint8(v >> 32)
|
||||||
|
dAtA[offset+5] = uint8(v >> 40)
|
||||||
|
dAtA[offset+6] = uint8(v >> 48)
|
||||||
|
dAtA[offset+7] = uint8(v >> 56)
|
||||||
|
return offset + 8
|
||||||
|
}
|
||||||
|
func encodeFixed32Kv(dAtA []byte, offset int, v uint32) int {
|
||||||
|
dAtA[offset] = uint8(v)
|
||||||
|
dAtA[offset+1] = uint8(v >> 8)
|
||||||
|
dAtA[offset+2] = uint8(v >> 16)
|
||||||
|
dAtA[offset+3] = uint8(v >> 24)
|
||||||
|
return offset + 4
|
||||||
|
}
|
||||||
|
func encodeVarintKv(dAtA []byte, offset int, v uint64) int {
|
||||||
|
for v >= 1<<7 {
|
||||||
|
dAtA[offset] = uint8(v&0x7f | 0x80)
|
||||||
|
v >>= 7
|
||||||
|
offset++
|
||||||
|
}
|
||||||
|
dAtA[offset] = uint8(v)
|
||||||
|
return offset + 1
|
||||||
|
}
|
||||||
|
func (m *KeyValue) Size() (n int) {
|
||||||
|
var l int
|
||||||
|
_ = l
|
||||||
|
l = len(m.Key)
|
||||||
|
if l > 0 {
|
||||||
|
n += 1 + l + sovKv(uint64(l))
|
||||||
|
}
|
||||||
|
if m.CreateRevision != 0 {
|
||||||
|
n += 1 + sovKv(uint64(m.CreateRevision))
|
||||||
|
}
|
||||||
|
if m.ModRevision != 0 {
|
||||||
|
n += 1 + sovKv(uint64(m.ModRevision))
|
||||||
|
}
|
||||||
|
if m.Version != 0 {
|
||||||
|
n += 1 + sovKv(uint64(m.Version))
|
||||||
|
}
|
||||||
|
l = len(m.Value)
|
||||||
|
if l > 0 {
|
||||||
|
n += 1 + l + sovKv(uint64(l))
|
||||||
|
}
|
||||||
|
if m.Lease != 0 {
|
||||||
|
n += 1 + sovKv(uint64(m.Lease))
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Event) Size() (n int) {
|
||||||
|
var l int
|
||||||
|
_ = l
|
||||||
|
if m.Type != 0 {
|
||||||
|
n += 1 + sovKv(uint64(m.Type))
|
||||||
|
}
|
||||||
|
if m.Kv != nil {
|
||||||
|
l = m.Kv.Size()
|
||||||
|
n += 1 + l + sovKv(uint64(l))
|
||||||
|
}
|
||||||
|
if m.PrevKv != nil {
|
||||||
|
l = m.PrevKv.Size()
|
||||||
|
n += 1 + l + sovKv(uint64(l))
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func sovKv(x uint64) (n int) {
|
||||||
|
for {
|
||||||
|
n++
|
||||||
|
x >>= 7
|
||||||
|
if x == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
func sozKv(x uint64) (n int) {
|
||||||
|
return sovKv(uint64((x << 1) ^ uint64((int64(x) >> 63))))
|
||||||
|
}
|
||||||
|
func (m *KeyValue) Unmarshal(dAtA []byte) error {
|
||||||
|
l := len(dAtA)
|
||||||
|
iNdEx := 0
|
||||||
|
for iNdEx < l {
|
||||||
|
preIndex := iNdEx
|
||||||
|
var wire uint64
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return ErrIntOverflowKv
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
wire |= (uint64(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fieldNum := int32(wire >> 3)
|
||||||
|
wireType := int(wire & 0x7)
|
||||||
|
if wireType == 4 {
|
||||||
|
return fmt.Errorf("proto: KeyValue: wiretype end group for non-group")
|
||||||
|
}
|
||||||
|
if fieldNum <= 0 {
|
||||||
|
return fmt.Errorf("proto: KeyValue: illegal tag %d (wire type %d)", fieldNum, wire)
|
||||||
|
}
|
||||||
|
switch fieldNum {
|
||||||
|
case 1:
|
||||||
|
if wireType != 2 {
|
||||||
|
return fmt.Errorf("proto: wrong wireType = %d for field Key", wireType)
|
||||||
|
}
|
||||||
|
var byteLen int
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return ErrIntOverflowKv
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
byteLen |= (int(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if byteLen < 0 {
|
||||||
|
return ErrInvalidLengthKv
|
||||||
|
}
|
||||||
|
postIndex := iNdEx + byteLen
|
||||||
|
if postIndex > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
m.Key = append(m.Key[:0], dAtA[iNdEx:postIndex]...)
|
||||||
|
if m.Key == nil {
|
||||||
|
m.Key = []byte{}
|
||||||
|
}
|
||||||
|
iNdEx = postIndex
|
||||||
|
case 2:
|
||||||
|
if wireType != 0 {
|
||||||
|
return fmt.Errorf("proto: wrong wireType = %d for field CreateRevision", wireType)
|
||||||
|
}
|
||||||
|
m.CreateRevision = 0
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return ErrIntOverflowKv
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
m.CreateRevision |= (int64(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 3:
|
||||||
|
if wireType != 0 {
|
||||||
|
return fmt.Errorf("proto: wrong wireType = %d for field ModRevision", wireType)
|
||||||
|
}
|
||||||
|
m.ModRevision = 0
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return ErrIntOverflowKv
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
m.ModRevision |= (int64(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 4:
|
||||||
|
if wireType != 0 {
|
||||||
|
return fmt.Errorf("proto: wrong wireType = %d for field Version", wireType)
|
||||||
|
}
|
||||||
|
m.Version = 0
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return ErrIntOverflowKv
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
m.Version |= (int64(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 5:
|
||||||
|
if wireType != 2 {
|
||||||
|
return fmt.Errorf("proto: wrong wireType = %d for field Value", wireType)
|
||||||
|
}
|
||||||
|
var byteLen int
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return ErrIntOverflowKv
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
byteLen |= (int(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if byteLen < 0 {
|
||||||
|
return ErrInvalidLengthKv
|
||||||
|
}
|
||||||
|
postIndex := iNdEx + byteLen
|
||||||
|
if postIndex > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
m.Value = append(m.Value[:0], dAtA[iNdEx:postIndex]...)
|
||||||
|
if m.Value == nil {
|
||||||
|
m.Value = []byte{}
|
||||||
|
}
|
||||||
|
iNdEx = postIndex
|
||||||
|
case 6:
|
||||||
|
if wireType != 0 {
|
||||||
|
return fmt.Errorf("proto: wrong wireType = %d for field Lease", wireType)
|
||||||
|
}
|
||||||
|
m.Lease = 0
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return ErrIntOverflowKv
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
m.Lease |= (int64(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
iNdEx = preIndex
|
||||||
|
skippy, err := skipKv(dAtA[iNdEx:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if skippy < 0 {
|
||||||
|
return ErrInvalidLengthKv
|
||||||
|
}
|
||||||
|
if (iNdEx + skippy) > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
iNdEx += skippy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if iNdEx > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (m *Event) Unmarshal(dAtA []byte) error {
|
||||||
|
l := len(dAtA)
|
||||||
|
iNdEx := 0
|
||||||
|
for iNdEx < l {
|
||||||
|
preIndex := iNdEx
|
||||||
|
var wire uint64
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return ErrIntOverflowKv
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
wire |= (uint64(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fieldNum := int32(wire >> 3)
|
||||||
|
wireType := int(wire & 0x7)
|
||||||
|
if wireType == 4 {
|
||||||
|
return fmt.Errorf("proto: Event: wiretype end group for non-group")
|
||||||
|
}
|
||||||
|
if fieldNum <= 0 {
|
||||||
|
return fmt.Errorf("proto: Event: illegal tag %d (wire type %d)", fieldNum, wire)
|
||||||
|
}
|
||||||
|
switch fieldNum {
|
||||||
|
case 1:
|
||||||
|
if wireType != 0 {
|
||||||
|
return fmt.Errorf("proto: wrong wireType = %d for field Type", wireType)
|
||||||
|
}
|
||||||
|
m.Type = 0
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return ErrIntOverflowKv
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
m.Type |= (Event_EventType(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 2:
|
||||||
|
if wireType != 2 {
|
||||||
|
return fmt.Errorf("proto: wrong wireType = %d for field Kv", wireType)
|
||||||
|
}
|
||||||
|
var msglen int
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return ErrIntOverflowKv
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
msglen |= (int(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if msglen < 0 {
|
||||||
|
return ErrInvalidLengthKv
|
||||||
|
}
|
||||||
|
postIndex := iNdEx + msglen
|
||||||
|
if postIndex > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
if m.Kv == nil {
|
||||||
|
m.Kv = &KeyValue{}
|
||||||
|
}
|
||||||
|
if err := m.Kv.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
iNdEx = postIndex
|
||||||
|
case 3:
|
||||||
|
if wireType != 2 {
|
||||||
|
return fmt.Errorf("proto: wrong wireType = %d for field PrevKv", wireType)
|
||||||
|
}
|
||||||
|
var msglen int
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return ErrIntOverflowKv
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
msglen |= (int(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if msglen < 0 {
|
||||||
|
return ErrInvalidLengthKv
|
||||||
|
}
|
||||||
|
postIndex := iNdEx + msglen
|
||||||
|
if postIndex > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
if m.PrevKv == nil {
|
||||||
|
m.PrevKv = &KeyValue{}
|
||||||
|
}
|
||||||
|
if err := m.PrevKv.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
iNdEx = postIndex
|
||||||
|
default:
|
||||||
|
iNdEx = preIndex
|
||||||
|
skippy, err := skipKv(dAtA[iNdEx:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if skippy < 0 {
|
||||||
|
return ErrInvalidLengthKv
|
||||||
|
}
|
||||||
|
if (iNdEx + skippy) > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
iNdEx += skippy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if iNdEx > l {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func skipKv(dAtA []byte) (n int, err error) {
|
||||||
|
l := len(dAtA)
|
||||||
|
iNdEx := 0
|
||||||
|
for iNdEx < l {
|
||||||
|
var wire uint64
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return 0, ErrIntOverflowKv
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return 0, io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
wire |= (uint64(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wireType := int(wire & 0x7)
|
||||||
|
switch wireType {
|
||||||
|
case 0:
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return 0, ErrIntOverflowKv
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return 0, io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
iNdEx++
|
||||||
|
if dAtA[iNdEx-1] < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return iNdEx, nil
|
||||||
|
case 1:
|
||||||
|
iNdEx += 8
|
||||||
|
return iNdEx, nil
|
||||||
|
case 2:
|
||||||
|
var length int
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return 0, ErrIntOverflowKv
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return 0, io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
length |= (int(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iNdEx += length
|
||||||
|
if length < 0 {
|
||||||
|
return 0, ErrInvalidLengthKv
|
||||||
|
}
|
||||||
|
return iNdEx, nil
|
||||||
|
case 3:
|
||||||
|
for {
|
||||||
|
var innerWire uint64
|
||||||
|
var start int = iNdEx
|
||||||
|
for shift := uint(0); ; shift += 7 {
|
||||||
|
if shift >= 64 {
|
||||||
|
return 0, ErrIntOverflowKv
|
||||||
|
}
|
||||||
|
if iNdEx >= l {
|
||||||
|
return 0, io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
b := dAtA[iNdEx]
|
||||||
|
iNdEx++
|
||||||
|
innerWire |= (uint64(b) & 0x7F) << shift
|
||||||
|
if b < 0x80 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
innerWireType := int(innerWire & 0x7)
|
||||||
|
if innerWireType == 4 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
next, err := skipKv(dAtA[start:])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
iNdEx = start + next
|
||||||
|
}
|
||||||
|
return iNdEx, nil
|
||||||
|
case 4:
|
||||||
|
return iNdEx, nil
|
||||||
|
case 5:
|
||||||
|
iNdEx += 4
|
||||||
|
return iNdEx, nil
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidLengthKv = fmt.Errorf("proto: negative length found during unmarshaling")
|
||||||
|
ErrIntOverflowKv = fmt.Errorf("proto: integer overflow")
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() { proto.RegisterFile("kv.proto", fileDescriptorKv) }
|
||||||
|
|
||||||
|
var fileDescriptorKv = []byte{
|
||||||
|
// 303 bytes of a gzipped FileDescriptorProto
|
||||||
|
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x90, 0x41, 0x4e, 0xc2, 0x40,
|
||||||
|
0x14, 0x86, 0x3b, 0x14, 0x0a, 0x3e, 0x08, 0x36, 0x13, 0x12, 0x27, 0x2e, 0x26, 0x95, 0x8d, 0x18,
|
||||||
|
0x13, 0x4c, 0xf0, 0x06, 0xc6, 0xae, 0x70, 0x61, 0x1a, 0x74, 0x4b, 0x4a, 0x79, 0x21, 0xa4, 0x94,
|
||||||
|
0x69, 0x4a, 0x9d, 0xa4, 0x37, 0x71, 0xef, 0xde, 0x73, 0xb0, 0xe4, 0x08, 0x52, 0x2f, 0x62, 0xfa,
|
||||||
|
0xc6, 0xe2, 0xc6, 0xcd, 0xe4, 0xfd, 0xff, 0xff, 0x65, 0xe6, 0x7f, 0x03, 0x9d, 0x58, 0x8f, 0xd3,
|
||||||
|
0x4c, 0xe5, 0x8a, 0x3b, 0x89, 0x8e, 0xa2, 0x74, 0x71, 0x39, 0x58, 0xa9, 0x95, 0x22, 0xeb, 0xae,
|
||||||
|
0x9a, 0x4c, 0x3a, 0xfc, 0x64, 0xd0, 0x99, 0x62, 0xf1, 0x1a, 0x6e, 0xde, 0x90, 0xbb, 0x60, 0xc7,
|
||||||
|
0x58, 0x08, 0xe6, 0xb1, 0x51, 0x2f, 0xa8, 0x46, 0x7e, 0x0d, 0xe7, 0x51, 0x86, 0x61, 0x8e, 0xf3,
|
||||||
|
0x0c, 0xf5, 0x7a, 0xb7, 0x56, 0x5b, 0xd1, 0xf0, 0xd8, 0xc8, 0x0e, 0xfa, 0xc6, 0x0e, 0x7e, 0x5d,
|
||||||
|
0x7e, 0x05, 0xbd, 0x44, 0x2d, 0xff, 0x28, 0x9b, 0xa8, 0x6e, 0xa2, 0x96, 0x27, 0x44, 0x40, 0x5b,
|
||||||
|
0x63, 0x46, 0x69, 0x93, 0xd2, 0x5a, 0xf2, 0x01, 0xb4, 0x74, 0x55, 0x40, 0xb4, 0xe8, 0x65, 0x23,
|
||||||
|
0x2a, 0x77, 0x83, 0xe1, 0x0e, 0x85, 0x43, 0xb4, 0x11, 0xc3, 0x0f, 0x06, 0x2d, 0x5f, 0xe3, 0x36,
|
||||||
|
0xe7, 0xb7, 0xd0, 0xcc, 0x8b, 0x14, 0xa9, 0x6e, 0x7f, 0x72, 0x31, 0x36, 0x7b, 0x8e, 0x29, 0x34,
|
||||||
|
0xe7, 0xac, 0x48, 0x31, 0x20, 0x88, 0x7b, 0xd0, 0x88, 0x35, 0x75, 0xef, 0x4e, 0xdc, 0x1a, 0xad,
|
||||||
|
0x17, 0x0f, 0x1a, 0xb1, 0xe6, 0x37, 0xd0, 0x4e, 0x33, 0xd4, 0xf3, 0x58, 0x53, 0xf9, 0xff, 0x30,
|
||||||
|
0xa7, 0x02, 0xa6, 0x7a, 0xe8, 0xc1, 0xd9, 0xe9, 0x7e, 0xde, 0x06, 0xfb, 0xf9, 0x65, 0xe6, 0x5a,
|
||||||
|
0x1c, 0xc0, 0x79, 0xf4, 0x9f, 0xfc, 0x99, 0xef, 0xb2, 0x07, 0xb1, 0x3f, 0x4a, 0xeb, 0x70, 0x94,
|
||||||
|
0xd6, 0xbe, 0x94, 0xec, 0x50, 0x4a, 0xf6, 0x55, 0x4a, 0xf6, 0xfe, 0x2d, 0xad, 0x85, 0x43, 0xff,
|
||||||
|
0x7e, 0xff, 0x13, 0x00, 0x00, 0xff, 0xff, 0xb5, 0x45, 0x92, 0x5d, 0xa1, 0x01, 0x00, 0x00,
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
package mvccpb;
|
||||||
|
|
||||||
|
import "gogoproto/gogo.proto";
|
||||||
|
|
||||||
|
option (gogoproto.marshaler_all) = true;
|
||||||
|
option (gogoproto.sizer_all) = true;
|
||||||
|
option (gogoproto.unmarshaler_all) = true;
|
||||||
|
option (gogoproto.goproto_getters_all) = false;
|
||||||
|
option (gogoproto.goproto_enum_prefix_all) = false;
|
||||||
|
|
||||||
|
message KeyValue {
|
||||||
|
// key is the key in bytes. An empty key is not allowed.
|
||||||
|
bytes key = 1;
|
||||||
|
// create_revision is the revision of last creation on this key.
|
||||||
|
int64 create_revision = 2;
|
||||||
|
// mod_revision is the revision of last modification on this key.
|
||||||
|
int64 mod_revision = 3;
|
||||||
|
// version is the version of the key. A deletion resets
|
||||||
|
// the version to zero and any modification of the key
|
||||||
|
// increases its version.
|
||||||
|
int64 version = 4;
|
||||||
|
// value is the value held by the key, in bytes.
|
||||||
|
bytes value = 5;
|
||||||
|
// lease is the ID of the lease that attached to key.
|
||||||
|
// When the attached lease expires, the key will be deleted.
|
||||||
|
// If lease is 0, then no lease is attached to the key.
|
||||||
|
int64 lease = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Event {
|
||||||
|
enum EventType {
|
||||||
|
PUT = 0;
|
||||||
|
DELETE = 1;
|
||||||
|
}
|
||||||
|
// type is the kind of event. If type is a PUT, it indicates
|
||||||
|
// new data has been stored to the key. If type is a DELETE,
|
||||||
|
// it indicates the key was deleted.
|
||||||
|
EventType type = 1;
|
||||||
|
// kv holds the KeyValue for the event.
|
||||||
|
// A PUT event contains current kv pair.
|
||||||
|
// A PUT event with kv.Version=1 indicates the creation of a key.
|
||||||
|
// A DELETE/EXPIRE event contains the deleted key with
|
||||||
|
// its modification revision set to the revision of deletion.
|
||||||
|
KeyValue kv = 2;
|
||||||
|
|
||||||
|
// prev_kv holds the key-value pair before the event happens.
|
||||||
|
KeyValue prev_kv = 3;
|
||||||
|
}
|
|
@ -918,6 +918,22 @@
|
||||||
"path": "github.com/coreos/etcd/client",
|
"path": "github.com/coreos/etcd/client",
|
||||||
"revision": "e5527914aa42cae3063f52892e1ca4518da0e4ae"
|
"revision": "e5527914aa42cae3063f52892e1ca4518da0e4ae"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "sUY/zcJDOG367WcDYCPRyreB4sI=",
|
||||||
|
"path": "github.com/coreos/etcd/clientv3",
|
||||||
|
"revision": "c31bec0f29facff13f7c3e3d948e55dd6689ed42",
|
||||||
|
"revisionTime": "2017-07-19T15:37:30Z",
|
||||||
|
"version": "v3.2.4",
|
||||||
|
"versionExact": "v3.2.4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checksumSHA1": "HWGjJoAdeVaq4dqkSKF6iYQj1aY=",
|
||||||
|
"path": "github.com/coreos/etcd/clientv3/concurrency",
|
||||||
|
"revision": "c31bec0f29facff13f7c3e3d948e55dd6689ed42",
|
||||||
|
"revisionTime": "2017-07-19T15:37:30Z",
|
||||||
|
"version": "v3.2.4",
|
||||||
|
"versionExact": "v3.2.4"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"checksumSHA1": "mKIXx1kDwmVmdIpZ3pJtRBuUKso=",
|
"checksumSHA1": "mKIXx1kDwmVmdIpZ3pJtRBuUKso=",
|
||||||
"comment": "v2.3.0-alpha.0-652-ge552791",
|
"comment": "v2.3.0-alpha.0-652-ge552791",
|
||||||
|
|
Loading…
Reference in New Issue