mirror of https://github.com/dexidp/dex.git
58 changed files with 28531 additions and 6 deletions
@ -0,0 +1,92 @@
|
||||
package etcd |
||||
|
||||
import ( |
||||
"time" |
||||
|
||||
"github.com/coreos/dex/storage" |
||||
"github.com/coreos/etcd/clientv3" |
||||
"github.com/coreos/etcd/clientv3/namespace" |
||||
"github.com/coreos/etcd/pkg/transport" |
||||
"github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
var ( |
||||
defaultDialTimeout = 2 * time.Second |
||||
) |
||||
|
||||
// SSL represents SSL options for etcd databases.
|
||||
type SSL struct { |
||||
ServerName string `json:"serverName" yaml:"serverName"` |
||||
CAFile string `json:"caFile" yaml:"caFile"` |
||||
KeyFile string `json:"keyFile" yaml:"keyFile"` |
||||
CertFile string `json:"certFile" yaml:"certFile"` |
||||
} |
||||
|
||||
// Etcd options for connecting to etcd databases.
|
||||
// If you are using a shared etcd cluster for storage, it might be useful to
|
||||
// configure an etcd namespace either via Namespace field or using `etcd grpc-proxy
|
||||
// --namespace=<prefix>`
|
||||
type Etcd struct { |
||||
Endpoints []string `json:"endpoints" yaml:"endpoints"` |
||||
Namespace string `json:"namespace" yaml:"namespace"` |
||||
Username string `json:"username" yaml:"username"` |
||||
Password string `json:"password" yaml:"password"` |
||||
SSL SSL `json:"ssl" yaml:"ssl"` |
||||
} |
||||
|
||||
// Open creates a new storage implementation backed by Etcd
|
||||
func (p *Etcd) Open(logger logrus.FieldLogger) (storage.Storage, error) { |
||||
return p.open(logger) |
||||
} |
||||
|
||||
func (p *Etcd) open(logger logrus.FieldLogger) (*conn, error) { |
||||
cfg := clientv3.Config{ |
||||
Endpoints: p.Endpoints, |
||||
DialTimeout: defaultDialTimeout * time.Second, |
||||
Username: p.Username, |
||||
Password: p.Password, |
||||
} |
||||
|
||||
var cfgtls *transport.TLSInfo |
||||
tlsinfo := transport.TLSInfo{} |
||||
if p.SSL.CertFile != "" { |
||||
tlsinfo.CertFile = p.SSL.CertFile |
||||
cfgtls = &tlsinfo |
||||
} |
||||
|
||||
if p.SSL.KeyFile != "" { |
||||
tlsinfo.KeyFile = p.SSL.KeyFile |
||||
cfgtls = &tlsinfo |
||||
} |
||||
|
||||
if p.SSL.CAFile != "" { |
||||
tlsinfo.CAFile = p.SSL.CAFile |
||||
cfgtls = &tlsinfo |
||||
} |
||||
|
||||
if p.SSL.ServerName != "" { |
||||
tlsinfo.ServerName = p.SSL.ServerName |
||||
cfgtls = &tlsinfo |
||||
} |
||||
|
||||
if cfgtls != nil { |
||||
clientTLS, err := cfgtls.ClientConfig() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
cfg.TLS = clientTLS |
||||
} |
||||
|
||||
db, err := clientv3.New(cfg) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if len(p.Namespace) > 0 { |
||||
db.KV = namespace.NewKV(db.KV, p.Namespace) |
||||
} |
||||
c := &conn{ |
||||
db: db, |
||||
logger: logger, |
||||
} |
||||
return c, nil |
||||
} |
||||
@ -0,0 +1,532 @@
|
||||
package etcd |
||||
|
||||
import ( |
||||
"context" |
||||
"encoding/json" |
||||
"fmt" |
||||
"strings" |
||||
"time" |
||||
|
||||
"github.com/coreos/dex/storage" |
||||
"github.com/coreos/etcd/clientv3" |
||||
"github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
const ( |
||||
clientPrefix = "client/" |
||||
authCodePrefix = "auth_code/" |
||||
refreshTokenPrefix = "refresh_token/" |
||||
authRequestPrefix = "auth_req/" |
||||
passwordPrefix = "password/" |
||||
offlineSessionPrefix = "offline_session/" |
||||
connectorPrefix = "connector/" |
||||
keysName = "openid-connect-keys" |
||||
|
||||
// defaultStorageTimeout will be applied to all storage's operations.
|
||||
defaultStorageTimeout = 5 * time.Second |
||||
) |
||||
|
||||
type conn struct { |
||||
db *clientv3.Client |
||||
logger logrus.FieldLogger |
||||
} |
||||
|
||||
func (c *conn) Close() error { |
||||
return c.db.Close() |
||||
} |
||||
|
||||
func (c *conn) GarbageCollect(now time.Time) (result storage.GCResult, err error) { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
authRequests, err := c.listAuthRequests(ctx) |
||||
if err != nil { |
||||
return result, err |
||||
} |
||||
|
||||
var delErr error |
||||
for _, authRequest := range authRequests { |
||||
if now.After(authRequest.Expiry) { |
||||
if err := c.deleteKey(ctx, keyID(authRequestPrefix, authRequest.ID)); err != nil { |
||||
c.logger.Errorf("failed to delete auth request: %v", err) |
||||
delErr = fmt.Errorf("failed to delete auth request: %v", err) |
||||
} |
||||
result.AuthRequests++ |
||||
} |
||||
} |
||||
if delErr != nil { |
||||
return result, delErr |
||||
} |
||||
|
||||
authCodes, err := c.listAuthCodes(ctx) |
||||
if err != nil { |
||||
return result, err |
||||
} |
||||
|
||||
for _, authCode := range authCodes { |
||||
if now.After(authCode.Expiry) { |
||||
if err := c.deleteKey(ctx, keyID(authCodePrefix, authCode.ID)); err != nil { |
||||
c.logger.Errorf("failed to delete auth code %v", err) |
||||
delErr = fmt.Errorf("failed to delete auth code: %v", err) |
||||
} |
||||
result.AuthCodes++ |
||||
} |
||||
} |
||||
return result, delErr |
||||
} |
||||
|
||||
func (c *conn) CreateAuthRequest(a storage.AuthRequest) error { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
return c.txnCreate(ctx, keyID(authRequestPrefix, a.ID), fromStorageAuthRequest(a)) |
||||
} |
||||
|
||||
func (c *conn) GetAuthRequest(id string) (a storage.AuthRequest, err error) { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
var req AuthRequest |
||||
if err = c.getKey(ctx, keyID(authRequestPrefix, id), &req); err != nil { |
||||
return |
||||
} |
||||
return toStorageAuthRequest(req), nil |
||||
} |
||||
|
||||
func (c *conn) UpdateAuthRequest(id string, updater func(a storage.AuthRequest) (storage.AuthRequest, error)) error { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
return c.txnUpdate(ctx, keyID(authRequestPrefix, id), func(currentValue []byte) ([]byte, error) { |
||||
var current AuthRequest |
||||
if len(currentValue) > 0 { |
||||
if err := json.Unmarshal(currentValue, ¤t); err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
updated, err := updater(toStorageAuthRequest(current)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return json.Marshal(fromStorageAuthRequest(updated)) |
||||
}) |
||||
} |
||||
|
||||
func (c *conn) DeleteAuthRequest(id string) error { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
return c.deleteKey(ctx, keyID(authRequestPrefix, id)) |
||||
} |
||||
|
||||
func (c *conn) CreateAuthCode(a storage.AuthCode) error { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
return c.txnCreate(ctx, keyID(authCodePrefix, a.ID), fromStorageAuthCode(a)) |
||||
} |
||||
|
||||
func (c *conn) GetAuthCode(id string) (a storage.AuthCode, err error) { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
err = c.getKey(ctx, keyID(authCodePrefix, id), &a) |
||||
return a, err |
||||
} |
||||
|
||||
func (c *conn) DeleteAuthCode(id string) error { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
return c.deleteKey(ctx, keyID(authCodePrefix, id)) |
||||
} |
||||
|
||||
func (c *conn) CreateRefresh(r storage.RefreshToken) error { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
return c.txnCreate(ctx, keyID(refreshTokenPrefix, r.ID), fromStorageRefreshToken(r)) |
||||
} |
||||
|
||||
func (c *conn) GetRefresh(id string) (r storage.RefreshToken, err error) { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
var token RefreshToken |
||||
if err = c.getKey(ctx, keyID(refreshTokenPrefix, id), &token); err != nil { |
||||
return |
||||
} |
||||
return toStorageRefreshToken(token), nil |
||||
} |
||||
|
||||
func (c *conn) UpdateRefreshToken(id string, updater func(old storage.RefreshToken) (storage.RefreshToken, error)) error { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
return c.txnUpdate(ctx, keyID(refreshTokenPrefix, id), func(currentValue []byte) ([]byte, error) { |
||||
var current RefreshToken |
||||
if len(currentValue) > 0 { |
||||
if err := json.Unmarshal([]byte(currentValue), ¤t); err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
updated, err := updater(toStorageRefreshToken(current)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return json.Marshal(fromStorageRefreshToken(updated)) |
||||
}) |
||||
} |
||||
|
||||
func (c *conn) DeleteRefresh(id string) error { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
return c.deleteKey(ctx, keyID(refreshTokenPrefix, id)) |
||||
} |
||||
|
||||
func (c *conn) ListRefreshTokens() (tokens []storage.RefreshToken, err error) { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
res, err := c.db.Get(ctx, refreshTokenPrefix, clientv3.WithPrefix()) |
||||
if err != nil { |
||||
return tokens, err |
||||
} |
||||
for _, v := range res.Kvs { |
||||
var token RefreshToken |
||||
if err = json.Unmarshal(v.Value, &token); err != nil { |
||||
return tokens, err |
||||
} |
||||
tokens = append(tokens, toStorageRefreshToken(token)) |
||||
} |
||||
return tokens, nil |
||||
} |
||||
|
||||
func (c *conn) CreateClient(cli storage.Client) error { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
return c.txnCreate(ctx, keyID(clientPrefix, cli.ID), cli) |
||||
} |
||||
|
||||
func (c *conn) GetClient(id string) (cli storage.Client, err error) { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
err = c.getKey(ctx, keyID(clientPrefix, id), &cli) |
||||
return cli, err |
||||
} |
||||
|
||||
func (c *conn) UpdateClient(id string, updater func(old storage.Client) (storage.Client, error)) error { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
return c.txnUpdate(ctx, keyID(clientPrefix, id), func(currentValue []byte) ([]byte, error) { |
||||
var current storage.Client |
||||
if len(currentValue) > 0 { |
||||
if err := json.Unmarshal(currentValue, ¤t); err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
updated, err := updater(current) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return json.Marshal(updated) |
||||
}) |
||||
} |
||||
|
||||
func (c *conn) DeleteClient(id string) error { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
return c.deleteKey(ctx, keyID(clientPrefix, id)) |
||||
} |
||||
|
||||
func (c *conn) ListClients() (clients []storage.Client, err error) { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
res, err := c.db.Get(ctx, clientPrefix, clientv3.WithPrefix()) |
||||
if err != nil { |
||||
return clients, err |
||||
} |
||||
for _, v := range res.Kvs { |
||||
var cli storage.Client |
||||
if err = json.Unmarshal(v.Value, &cli); err != nil { |
||||
return clients, err |
||||
} |
||||
clients = append(clients, cli) |
||||
} |
||||
return clients, nil |
||||
} |
||||
|
||||
func (c *conn) CreatePassword(p storage.Password) error { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
return c.txnCreate(ctx, passwordPrefix+strings.ToLower(p.Email), p) |
||||
} |
||||
|
||||
func (c *conn) GetPassword(email string) (p storage.Password, err error) { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
err = c.getKey(ctx, keyEmail(passwordPrefix, email), &p) |
||||
return p, err |
||||
} |
||||
|
||||
func (c *conn) UpdatePassword(email string, updater func(p storage.Password) (storage.Password, error)) error { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
return c.txnUpdate(ctx, keyEmail(passwordPrefix, email), func(currentValue []byte) ([]byte, error) { |
||||
var current storage.Password |
||||
if len(currentValue) > 0 { |
||||
if err := json.Unmarshal(currentValue, ¤t); err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
updated, err := updater(current) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return json.Marshal(updated) |
||||
}) |
||||
} |
||||
|
||||
func (c *conn) DeletePassword(email string) error { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
return c.deleteKey(ctx, keyEmail(passwordPrefix, email)) |
||||
} |
||||
|
||||
func (c *conn) ListPasswords() (passwords []storage.Password, err error) { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
res, err := c.db.Get(ctx, passwordPrefix, clientv3.WithPrefix()) |
||||
if err != nil { |
||||
return passwords, err |
||||
} |
||||
for _, v := range res.Kvs { |
||||
var p storage.Password |
||||
if err = json.Unmarshal(v.Value, &p); err != nil { |
||||
return passwords, err |
||||
} |
||||
passwords = append(passwords, p) |
||||
} |
||||
return passwords, nil |
||||
} |
||||
|
||||
func (c *conn) CreateOfflineSessions(s storage.OfflineSessions) error { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
return c.txnCreate(ctx, keySession(offlineSessionPrefix, s.UserID, s.ConnID), fromStorageOfflineSessions(s)) |
||||
} |
||||
|
||||
func (c *conn) UpdateOfflineSessions(userID string, connID string, updater func(s storage.OfflineSessions) (storage.OfflineSessions, error)) error { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
return c.txnUpdate(ctx, keySession(offlineSessionPrefix, userID, connID), func(currentValue []byte) ([]byte, error) { |
||||
var current OfflineSessions |
||||
if len(currentValue) > 0 { |
||||
if err := json.Unmarshal(currentValue, ¤t); err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
updated, err := updater(toStorageOfflineSessions(current)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return json.Marshal(fromStorageOfflineSessions(updated)) |
||||
}) |
||||
} |
||||
|
||||
func (c *conn) GetOfflineSessions(userID string, connID string) (s storage.OfflineSessions, err error) { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
var os OfflineSessions |
||||
if err = c.getKey(ctx, keySession(offlineSessionPrefix, userID, connID), &os); err != nil { |
||||
return |
||||
} |
||||
return toStorageOfflineSessions(os), nil |
||||
} |
||||
|
||||
func (c *conn) DeleteOfflineSessions(userID string, connID string) error { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
return c.deleteKey(ctx, keySession(offlineSessionPrefix, userID, connID)) |
||||
} |
||||
|
||||
func (c *conn) CreateConnector(connector storage.Connector) error { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
return c.txnCreate(ctx, keyID(connectorPrefix, connector.ID), connector) |
||||
} |
||||
|
||||
func (c *conn) GetConnector(id string) (conn storage.Connector, err error) { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
err = c.getKey(ctx, keyID(connectorPrefix, id), &conn) |
||||
return conn, err |
||||
} |
||||
|
||||
func (c *conn) UpdateConnector(id string, updater func(s storage.Connector) (storage.Connector, error)) error { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
return c.txnUpdate(ctx, keyID(connectorPrefix, id), func(currentValue []byte) ([]byte, error) { |
||||
var current storage.Connector |
||||
if len(currentValue) > 0 { |
||||
if err := json.Unmarshal(currentValue, ¤t); err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
updated, err := updater(current) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return json.Marshal(updated) |
||||
}) |
||||
} |
||||
|
||||
func (c *conn) DeleteConnector(id string) error { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
return c.deleteKey(ctx, keyID(connectorPrefix, id)) |
||||
} |
||||
|
||||
func (c *conn) ListConnectors() (connectors []storage.Connector, err error) { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
res, err := c.db.Get(ctx, connectorPrefix, clientv3.WithPrefix()) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
for _, v := range res.Kvs { |
||||
var c storage.Connector |
||||
if err = json.Unmarshal(v.Value, &c); err != nil { |
||||
return nil, err |
||||
} |
||||
connectors = append(connectors, c) |
||||
} |
||||
return connectors, nil |
||||
} |
||||
|
||||
func (c *conn) GetKeys() (keys storage.Keys, err error) { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
res, err := c.db.Get(ctx, keysName) |
||||
if err != nil { |
||||
return keys, err |
||||
} |
||||
if res.Count > 0 && len(res.Kvs) > 0 { |
||||
err = json.Unmarshal(res.Kvs[0].Value, &keys) |
||||
} |
||||
return keys, err |
||||
} |
||||
|
||||
func (c *conn) UpdateKeys(updater func(old storage.Keys) (storage.Keys, error)) error { |
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultStorageTimeout) |
||||
defer cancel() |
||||
return c.txnUpdate(ctx, keysName, func(currentValue []byte) ([]byte, error) { |
||||
var current storage.Keys |
||||
if len(currentValue) > 0 { |
||||
if err := json.Unmarshal(currentValue, ¤t); err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
updated, err := updater(current) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return json.Marshal(updated) |
||||
}) |
||||
} |
||||
|
||||
func (c *conn) deleteKey(ctx context.Context, key string) error { |
||||
res, err := c.db.Delete(ctx, key) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if res.Deleted == 0 { |
||||
return storage.ErrNotFound |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (c *conn) getKey(ctx context.Context, key string, value interface{}) error { |
||||
r, err := c.db.Get(ctx, key) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if r.Count == 0 { |
||||
return storage.ErrNotFound |
||||
} |
||||
return json.Unmarshal(r.Kvs[0].Value, value) |
||||
} |
||||
|
||||
func (c *conn) listAuthRequests(ctx context.Context) (reqs []AuthRequest, err error) { |
||||
res, err := c.db.Get(ctx, authRequestPrefix, clientv3.WithPrefix()) |
||||
if err != nil { |
||||
return reqs, err |
||||
} |
||||
for _, v := range res.Kvs { |
||||
var r AuthRequest |
||||
if err = json.Unmarshal(v.Value, &r); err != nil { |
||||
return reqs, err |
||||
} |
||||
reqs = append(reqs, r) |
||||
} |
||||
return reqs, nil |
||||
} |
||||
|
||||
func (c *conn) listAuthCodes(ctx context.Context) (codes []AuthCode, err error) { |
||||
res, err := c.db.Get(ctx, authCodePrefix, clientv3.WithPrefix()) |
||||
if err != nil { |
||||
return codes, err |
||||
} |
||||
for _, v := range res.Kvs { |
||||
var c AuthCode |
||||
if err = json.Unmarshal(v.Value, &c); err != nil { |
||||
return codes, err |
||||
} |
||||
codes = append(codes, c) |
||||
} |
||||
return codes, nil |
||||
} |
||||
|
||||
func (c *conn) txnCreate(ctx context.Context, key string, value interface{}) error { |
||||
b, err := json.Marshal(value) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
txn := c.db.Txn(ctx) |
||||
res, err := txn. |
||||
If(clientv3.Compare(clientv3.CreateRevision(key), "=", 0)). |
||||
Then(clientv3.OpPut(key, string(b))). |
||||
Commit() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if !res.Succeeded { |
||||
return storage.ErrAlreadyExists |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (c *conn) txnUpdate(ctx context.Context, key string, update func(current []byte) ([]byte, error)) error { |
||||
getResp, err := c.db.Get(ctx, key) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
var currentValue []byte |
||||
var modRev int64 |
||||
if len(getResp.Kvs) > 0 { |
||||
currentValue = getResp.Kvs[0].Value |
||||
modRev = getResp.Kvs[0].ModRevision |
||||
} |
||||
|
||||
updatedValue, err := update(currentValue) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
txn := c.db.Txn(ctx) |
||||
updateResp, err := txn. |
||||
If(clientv3.Compare(clientv3.ModRevision(key), "=", modRev)). |
||||
Then(clientv3.OpPut(key, string(updatedValue))). |
||||
Commit() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if !updateResp.Succeeded { |
||||
return fmt.Errorf("failed to update key=%q: concurrent conflicting update happened", key) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func keyID(prefix, id string) string { return prefix + id } |
||||
func keyEmail(prefix, email string) string { return prefix + strings.ToLower(email) } |
||||
func keySession(prefix, userID, connID string) string { |
||||
return prefix + strings.ToLower(userID+"|"+connID) |
||||
} |
||||
@ -0,0 +1,94 @@
|
||||
package etcd |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"os" |
||||
"runtime" |
||||
"strings" |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/coreos/dex/storage" |
||||
"github.com/coreos/dex/storage/conformance" |
||||
"github.com/coreos/etcd/clientv3" |
||||
"github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
func withTimeout(t time.Duration, f func()) { |
||||
c := make(chan struct{}) |
||||
defer close(c) |
||||
|
||||
go func() { |
||||
select { |
||||
case <-c: |
||||
case <-time.After(t): |
||||
// Dump a stack trace of the program. Useful for debugging deadlocks.
|
||||
buf := make([]byte, 2<<20) |
||||
fmt.Fprintf(os.Stderr, "%s\n", buf[:runtime.Stack(buf, true)]) |
||||
panic("test took too long") |
||||
} |
||||
}() |
||||
|
||||
f() |
||||
} |
||||
|
||||
func cleanDB(c *conn) error { |
||||
ctx := context.TODO() |
||||
for _, prefix := range []string{ |
||||
clientPrefix, |
||||
authCodePrefix, |
||||
refreshTokenPrefix, |
||||
authRequestPrefix, |
||||
passwordPrefix, |
||||
offlineSessionPrefix, |
||||
connectorPrefix, |
||||
} { |
||||
_, err := c.db.Delete(ctx, prefix, clientv3.WithPrefix()) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
var logger = &logrus.Logger{ |
||||
Out: os.Stderr, |
||||
Formatter: &logrus.TextFormatter{DisableColors: true}, |
||||
Level: logrus.DebugLevel, |
||||
} |
||||
|
||||
func TestEtcd(t *testing.T) { |
||||
testEtcdEnv := "DEX_ETCD_ENDPOINTS" |
||||
endpointsStr := os.Getenv(testEtcdEnv) |
||||
if endpointsStr == "" { |
||||
t.Skipf("test environment variable %q not set, skipping", testEtcdEnv) |
||||
return |
||||
} |
||||
endpoints := strings.Split(endpointsStr, ",") |
||||
|
||||
newStorage := func() storage.Storage { |
||||
s := &Etcd{ |
||||
Endpoints: endpoints, |
||||
} |
||||
conn, err := s.open(logger) |
||||
if err != nil { |
||||
fmt.Fprintln(os.Stdout, err) |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
if err := cleanDB(conn); err != nil { |
||||
fmt.Fprintln(os.Stdout, err) |
||||
t.Fatal(err) |
||||
} |
||||
return conn |
||||
} |
||||
|
||||
withTimeout(time.Second*10, func() { |
||||
conformance.RunTests(t, newStorage) |
||||
}) |
||||
|
||||
withTimeout(time.Minute*1, func() { |
||||
conformance.RunTransactionTests(t, newStorage) |
||||
}) |
||||
} |
||||
@ -0,0 +1,109 @@
|
||||
#!/bin/bash |
||||
|
||||
if [ "$EUID" -ne 0 ] |
||||
then echo "Please run as root" |
||||
exit |
||||
fi |
||||
|
||||
function usage { |
||||
cat << EOF >> /dev/stderr |
||||
Usage: sudo ./standup.sh [create|destroy] [etcd] |
||||
|
||||
This is a script for standing up test databases. It uses systemd to daemonize |
||||
rkt containers running on a local loopback IP. |
||||
|
||||
The general workflow is to create a daemonized container, use the output to set |
||||
the test environment variables, run the tests, then destroy the container. |
||||
|
||||
sudo ./standup.sh create etcd |
||||
# Copy environment variables and run tests. |
||||
go test -v -i # always install test dependencies |
||||
go test -v |
||||
sudo ./standup.sh destroy etcd |
||||
|
||||
EOF |
||||
exit 2 |
||||
} |
||||
|
||||
function main { |
||||
if [ "$#" -ne 2 ]; then |
||||
usage |
||||
exit 2 |
||||
fi |
||||
|
||||
case "$1" in |
||||
"create") |
||||
case "$2" in |
||||
"etcd") |
||||
create_etcd;; |
||||
*) |
||||
usage |
||||
exit 2 |
||||
;; |
||||
esac |
||||
;; |
||||
"destroy") |
||||
case "$2" in |
||||
"etcd") |
||||
destroy_etcd;; |
||||
*) |
||||
usage |
||||
exit 2 |
||||
;; |
||||
esac |
||||
;; |
||||
*) |
||||
usage |
||||
exit 2 |
||||
;; |
||||
esac |
||||
} |
||||
|
||||
function wait_for_file { |
||||
while [ ! -f $1 ]; do |
||||
sleep 1 |
||||
done |
||||
} |
||||
|
||||
function wait_for_container { |
||||
while [ -z "$( rkt list --full | grep $1 | grep running )" ]; do |
||||
sleep 1 |
||||
done |
||||
} |
||||
|
||||
function create_etcd { |
||||
UUID_FILE=/tmp/dex-etcd-uuid |
||||
if [ -f $UUID_FILE ]; then |
||||
echo "etcd database already exists, try ./standup.sh destroy etcd" |
||||
exit 2 |
||||
fi |
||||
|
||||
echo "Starting etcd . To view progress run:" |
||||
echo "" |
||||
echo " journalctl -fu dex-etcd" |
||||
echo "" |
||||
UNIFIED_CGROUP_HIERARCHY=no \ |
||||
systemd-run --unit=dex-etcd \ |
||||
rkt run --uuid-file-save=$UUID_FILE --insecure-options=image \ |
||||
--net=host \ |
||||
docker://quay.io/coreos/etcd:v3.2.9 |
||||
|
||||
wait_for_file $UUID_FILE |
||||
|
||||
UUID=$( cat $UUID_FILE ) |
||||
wait_for_container $UUID |
||||
echo "To run tests export the following environment variables:" |
||||
echo "" |
||||
echo " export DEX_ETCD_ENDPOINTS=http://localhost:2379" |
||||
echo "" |
||||
} |
||||
|
||||
function destroy_etcd { |
||||
UUID_FILE=/tmp/dex-etcd-uuid |
||||
systemctl stop dex-etcd |
||||
rkt rm --uuid-file=$UUID_FILE |
||||
rm $UUID_FILE |
||||
} |
||||
|
||||
|
||||
main $@ |
||||
@ -0,0 +1,229 @@
|
||||
package etcd |
||||
|
||||
import ( |
||||
"time" |
||||
|
||||
"github.com/coreos/dex/storage" |
||||
jose "gopkg.in/square/go-jose.v2" |
||||
) |
||||
|
||||
// AuthCode is a mirrored struct from storage with JSON struct tags
|
||||
type AuthCode struct { |
||||
ID string `json:"ID"` |
||||
ClientID string `json:"clientID"` |
||||
RedirectURI string `json:"redirectURI"` |
||||
Nonce string `json:"nonce,omitempty"` |
||||
Scopes []string `json:"scopes,omitempty"` |
||||
|
||||
ConnectorID string `json:"connectorID,omitempty"` |
||||
ConnectorData []byte `json:"connectorData,omitempty"` |
||||
Claims Claims `json:"claims,omitempty"` |
||||
|
||||
Expiry time.Time `json:"expiry"` |
||||
} |
||||
|
||||
func fromStorageAuthCode(a storage.AuthCode) AuthCode { |
||||
return AuthCode{ |
||||
ID: a.ID, |
||||
ClientID: a.ClientID, |
||||
RedirectURI: a.RedirectURI, |
||||
ConnectorID: a.ConnectorID, |
||||
ConnectorData: a.ConnectorData, |
||||
Nonce: a.Nonce, |
||||
Scopes: a.Scopes, |
||||
Claims: fromStorageClaims(a.Claims), |
||||
Expiry: a.Expiry, |
||||
} |
||||
} |
||||
|
||||
// AuthRequest is a mirrored struct from storage with JSON struct tags
|
||||
type AuthRequest struct { |
||||
ID string `json:"id"` |
||||
ClientID string `json:"client_id"` |
||||
|
||||
ResponseTypes []string `json:"response_types"` |
||||
Scopes []string `json:"scopes"` |
||||
RedirectURI string `json:"redirect_uri"` |
||||
Nonce string `json:"nonce"` |
||||
State string `json:"state"` |
||||
|
||||
ForceApprovalPrompt bool `json:"force_approval_prompt"` |
||||
|
||||
Expiry time.Time `json:"expiry"` |
||||
|
||||
LoggedIn bool `json:"logged_in"` |
||||
|
||||
Claims Claims `json:"claims"` |
||||
|
||||
ConnectorID string `json:"connector_id"` |
||||
ConnectorData []byte `json:"connector_data"` |
||||
} |
||||
|
||||
func fromStorageAuthRequest(a storage.AuthRequest) AuthRequest { |
||||
return AuthRequest{ |
||||
ID: a.ID, |
||||
ClientID: a.ClientID, |
||||
ResponseTypes: a.ResponseTypes, |
||||
Scopes: a.Scopes, |
||||
RedirectURI: a.RedirectURI, |
||||
Nonce: a.Nonce, |
||||
State: a.State, |
||||
ForceApprovalPrompt: a.ForceApprovalPrompt, |
||||
Expiry: a.Expiry, |
||||
LoggedIn: a.LoggedIn, |
||||
Claims: fromStorageClaims(a.Claims), |
||||
ConnectorID: a.ConnectorID, |
||||
ConnectorData: a.ConnectorData, |
||||
} |
||||
} |
||||
|
||||
func toStorageAuthRequest(a AuthRequest) storage.AuthRequest { |
||||
return storage.AuthRequest{ |
||||
ID: a.ID, |
||||
ClientID: a.ClientID, |
||||
ResponseTypes: a.ResponseTypes, |
||||
Scopes: a.Scopes, |
||||
RedirectURI: a.RedirectURI, |
||||
Nonce: a.Nonce, |
||||
State: a.State, |
||||
ForceApprovalPrompt: a.ForceApprovalPrompt, |
||||
LoggedIn: a.LoggedIn, |
||||
ConnectorID: a.ConnectorID, |
||||
ConnectorData: a.ConnectorData, |
||||
Expiry: a.Expiry, |
||||
Claims: toStorageClaims(a.Claims), |
||||
} |
||||
} |
||||
|
||||
// RefreshToken is a mirrored struct from storage with JSON struct tags
|
||||
type RefreshToken struct { |
||||
ID string `json:"id"` |
||||
|
||||
Token string `json:"token"` |
||||
|
||||
CreatedAt time.Time `json:"created_at"` |
||||
LastUsed time.Time `json:"last_used"` |
||||
|
||||
ClientID string `json:"client_id"` |
||||
|
||||
ConnectorID string `json:"connector_id"` |
||||
ConnectorData []byte `json:"connector_data"` |
||||
Claims Claims `json:"claims"` |
||||
|
||||
Scopes []string `json:"scopes"` |
||||
|
||||
Nonce string `json:"nonce"` |
||||
} |
||||
|
||||
func toStorageRefreshToken(r RefreshToken) storage.RefreshToken { |
||||
return storage.RefreshToken{ |
||||
ID: r.ID, |
||||
Token: r.Token, |
||||
CreatedAt: r.CreatedAt, |
||||
LastUsed: r.LastUsed, |
||||
ClientID: r.ClientID, |
||||
ConnectorID: r.ConnectorID, |
||||
ConnectorData: r.ConnectorData, |
||||
Scopes: r.Scopes, |
||||
Nonce: r.Nonce, |
||||
Claims: toStorageClaims(r.Claims), |
||||
} |
||||
} |
||||
|
||||
func fromStorageRefreshToken(r storage.RefreshToken) RefreshToken { |
||||
return RefreshToken{ |
||||
ID: r.ID, |
||||
Token: r.Token, |
||||
CreatedAt: r.CreatedAt, |
||||
LastUsed: r.LastUsed, |
||||
ClientID: r.ClientID, |
||||
ConnectorID: r.ConnectorID, |
||||
ConnectorData: r.ConnectorData, |
||||
Scopes: r.Scopes, |
||||
Nonce: r.Nonce, |
||||
Claims: fromStorageClaims(r.Claims), |
||||
} |
||||
} |
||||
|
||||
// Claims is a mirrored struct from storage with JSON struct tags.
|
||||
type Claims struct { |
||||
UserID string `json:"userID"` |
||||
Username string `json:"username"` |
||||
Email string `json:"email"` |
||||
EmailVerified bool `json:"emailVerified"` |
||||
Groups []string `json:"groups,omitempty"` |
||||
} |
||||
|
||||
func fromStorageClaims(i storage.Claims) Claims { |
||||
return Claims{ |
||||
UserID: i.UserID, |
||||
Username: i.Username, |
||||
Email: i.Email, |
||||
EmailVerified: i.EmailVerified, |
||||
Groups: i.Groups, |
||||
} |
||||
} |
||||
|
||||
func toStorageClaims(i Claims) storage.Claims { |
||||
return storage.Claims{ |
||||
UserID: i.UserID, |
||||
Username: i.Username, |
||||
Email: i.Email, |
||||
EmailVerified: i.EmailVerified, |
||||
Groups: i.Groups, |
||||
} |
||||
} |
||||
|
||||
// Keys is a mirrored struct from storage with JSON struct tags
|
||||
type Keys struct { |
||||
SigningKey *jose.JSONWebKey `json:"signing_key,omitempty"` |
||||
SigningKeyPub *jose.JSONWebKey `json:"signing_key_pub,omitempty"` |
||||
VerificationKeys []storage.VerificationKey `json:"verification_keys"` |
||||
NextRotation time.Time `json:"next_rotation"` |
||||
} |
||||
|
||||
func fromStorageKeys(keys storage.Keys) Keys { |
||||
return Keys{ |
||||
SigningKey: keys.SigningKey, |
||||
SigningKeyPub: keys.SigningKeyPub, |
||||
VerificationKeys: keys.VerificationKeys, |
||||
NextRotation: keys.NextRotation, |
||||
} |
||||
} |
||||
|
||||
func toStorageKeys(keys Keys) storage.Keys { |
||||
return storage.Keys{ |
||||
SigningKey: keys.SigningKey, |
||||
SigningKeyPub: keys.SigningKeyPub, |
||||
VerificationKeys: keys.VerificationKeys, |
||||
NextRotation: keys.NextRotation, |
||||
} |
||||
} |
||||
|
||||
// OfflineSessions is a mirrored struct from storage with JSON struct tags
|
||||
type OfflineSessions struct { |
||||
UserID string `json:"user_id,omitempty"` |
||||
ConnID string `json:"conn_id,omitempty"` |
||||
Refresh map[string]*storage.RefreshTokenRef `json:"refresh,omitempty"` |
||||
} |
||||
|
||||
func fromStorageOfflineSessions(o storage.OfflineSessions) OfflineSessions { |
||||
return OfflineSessions{ |
||||
UserID: o.UserID, |
||||
ConnID: o.ConnID, |
||||
Refresh: o.Refresh, |
||||
} |
||||
} |
||||
|
||||
func toStorageOfflineSessions(o OfflineSessions) storage.OfflineSessions { |
||||
s := storage.OfflineSessions{ |
||||
UserID: o.UserID, |
||||
ConnID: o.ConnID, |
||||
Refresh: o.Refresh, |
||||
} |
||||
if s.Refresh == nil { |
||||
// Server code assumes this will be non-nil.
|
||||
s.Refresh = make(map[string]*storage.RefreshTokenRef) |
||||
} |
||||
return s |
||||
} |
||||
@ -0,0 +1,202 @@
|
||||
|
||||
Apache License |
||||
Version 2.0, January 2004 |
||||
http://www.apache.org/licenses/ |
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION |
||||
|
||||
1. Definitions. |
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, |
||||
and distribution as defined by Sections 1 through 9 of this document. |
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by |
||||
the copyright owner that is granting the License. |
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all |
||||
other entities that control, are controlled by, or are under common |
||||
control with that entity. For the purposes of this definition, |
||||
"control" means (i) the power, direct or indirect, to cause the |
||||
direction or management of such entity, whether by contract or |
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the |
||||
outstanding shares, or (iii) beneficial ownership of such entity. |
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity |
||||
exercising permissions granted by this License. |
||||
|
||||
"Source" form shall mean the preferred form for making modifications, |
||||
including but not limited to software source code, documentation |
||||
source, and configuration files. |
||||
|
||||
"Object" form shall mean any form resulting from mechanical |
||||
transformation or translation of a Source form, including but |
||||
not limited to compiled object code, generated documentation, |
||||
and conversions to other media types. |
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or |
||||
Object form, made available under the License, as indicated by a |
||||
copyright notice that is included in or attached to the work |
||||
(an example is provided in the Appendix below). |
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object |
||||
form, that is based on (or derived from) the Work and for which the |
||||
editorial revisions, annotations, elaborations, or other modifications |
||||
represent, as a whole, an original work of authorship. For the purposes |
||||
of this License, Derivative Works shall not include works that remain |
||||
separable from, or merely link (or bind by name) to the interfaces of, |
||||
the Work and Derivative Works thereof. |
||||
|
||||
"Contribution" shall mean any work of authorship, including |
||||
the original version of the Work and any modifications or additions |
||||
to that Work or Derivative Works thereof, that is intentionally |
||||
submitted to Licensor for inclusion in the Work by the copyright owner |
||||
or by an individual or Legal Entity authorized to submit on behalf of |
||||
the copyright owner. For the purposes of this definition, "submitted" |
||||
means any form of electronic, verbal, or written communication sent |
||||
to the Licensor or its representatives, including but not limited to |
||||
communication on electronic mailing lists, source code control systems, |
||||
and issue tracking systems that are managed by, or on behalf of, the |
||||
Licensor for the purpose of discussing and improving the Work, but |
||||
excluding communication that is conspicuously marked or otherwise |
||||
designated in writing by the copyright owner as "Not a Contribution." |
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity |
||||
on behalf of whom a Contribution has been received by Licensor and |
||||
subsequently incorporated within the Work. |
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of |
||||
this License, each Contributor hereby grants to You a perpetual, |
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
||||
copyright license to reproduce, prepare Derivative Works of, |
||||
publicly display, publicly perform, sublicense, and distribute the |
||||
Work and such Derivative Works in Source or Object form. |
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of |
||||
this License, each Contributor hereby grants to You a perpetual, |
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
||||
(except as stated in this section) patent license to make, have made, |
||||
use, offer to sell, sell, import, and otherwise transfer the Work, |
||||
where such license applies only to those patent claims licensable |
||||
by such Contributor that are necessarily infringed by their |
||||
Contribution(s) alone or by combination of their Contribution(s) |
||||
with the Work to which such Contribution(s) was submitted. If You |
||||
institute patent litigation against any entity (including a |
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work |
||||
or a Contribution incorporated within the Work constitutes direct |
||||
or contributory patent infringement, then any patent licenses |
||||
granted to You under this License for that Work shall terminate |
||||
as of the date such litigation is filed. |
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the |
||||
Work or Derivative Works thereof in any medium, with or without |
||||
modifications, and in Source or Object form, provided that You |
||||
meet the following conditions: |
||||
|
||||
(a) You must give any other recipients of the Work or |
||||
Derivative Works a copy of this License; and |
||||
|
||||
(b) You must cause any modified files to carry prominent notices |
||||
stating that You changed the files; and |
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works |
||||
that You distribute, all copyright, patent, trademark, and |
||||
attribution notices from the Source form of the Work, |
||||
excluding those notices that do not pertain to any part of |
||||
the Derivative Works; and |
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its |
||||
distribution, then any Derivative Works that You distribute must |
||||
include a readable copy of the attribution notices contained |
||||
within such NOTICE file, excluding those notices that do not |
||||
pertain to any part of the Derivative Works, in at least one |
||||
of the following places: within a NOTICE text file distributed |
||||
as part of the Derivative Works; within the Source form or |
||||
documentation, if provided along with the Derivative Works; or, |
||||
within a display generated by the Derivative Works, if and |
||||
wherever such third-party notices normally appear. The contents |
||||
of the NOTICE file are for informational purposes only and |
||||
do not modify the License. You may add Your own attribution |
||||
notices within Derivative Works that You distribute, alongside |
||||
or as an addendum to the NOTICE text from the Work, provided |
||||
that such additional attribution notices cannot be construed |
||||
as modifying the License. |
||||
|
||||
You may add Your own copyright statement to Your modifications and |
||||
may provide additional or different license terms and conditions |
||||
for use, reproduction, or distribution of Your modifications, or |
||||
for any such Derivative Works as a whole, provided Your use, |
||||
reproduction, and distribution of the Work otherwise complies with |
||||
the conditions stated in this License. |
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise, |
||||
any Contribution intentionally submitted for inclusion in the Work |
||||
by You to the Licensor shall be under the terms and conditions of |
||||
this License, without any additional terms or conditions. |
||||
Notwithstanding the above, nothing herein shall supersede or modify |
||||
the terms of any separate license agreement you may have executed |
||||
with Licensor regarding such Contributions. |
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade |
||||
names, trademarks, service marks, or product names of the Licensor, |
||||
except as required for reasonable and customary use in describing the |
||||
origin of the Work and reproducing the content of the NOTICE file. |
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or |
||||
agreed to in writing, Licensor provides the Work (and each |
||||
Contributor provides its Contributions) on an "AS IS" BASIS, |
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
||||
implied, including, without limitation, any warranties or conditions |
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A |
||||
PARTICULAR PURPOSE. You are solely responsible for determining the |
||||
appropriateness of using or redistributing the Work and assume any |
||||
risks associated with Your exercise of permissions under this License. |
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory, |
||||
whether in tort (including negligence), contract, or otherwise, |
||||
unless required by applicable law (such as deliberate and grossly |
||||
negligent acts) or agreed to in writing, shall any Contributor be |
||||
liable to You for damages, including any direct, indirect, special, |
||||
incidental, or consequential damages of any character arising as a |
||||
result of this License or out of the use or inability to use the |
||||
Work (including but not limited to damages for loss of goodwill, |
||||
work stoppage, computer failure or malfunction, or any and all |
||||
other commercial damages or losses), even if such Contributor |
||||
has been advised of the possibility of such damages. |
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing |
||||
the Work or Derivative Works thereof, You may choose to offer, |
||||
and charge a fee for, acceptance of support, warranty, indemnity, |
||||
or other liability obligations and/or rights consistent with this |
||||
License. However, in accepting such obligations, You may act only |
||||
on Your own behalf and on Your sole responsibility, not on behalf |
||||
of any other Contributor, and only if You agree to indemnify, |
||||
defend, and hold each Contributor harmless for any liability |
||||
incurred by, or claims asserted against, such Contributor by reason |
||||
of your accepting any such warranty or additional liability. |
||||
|
||||
END OF TERMS AND CONDITIONS |
||||
|
||||
APPENDIX: How to apply the Apache License to your work. |
||||
|
||||
To apply the Apache License to your work, attach the following |
||||
boilerplate notice, with the fields enclosed by brackets "[]" |
||||
replaced with your own identifying information. (Don't include |
||||
the brackets!) The text should be enclosed in the appropriate |
||||
comment syntax for the file format. We also recommend that a |
||||
file or class name and description of purpose be included on the |
||||
same "printed page" as the copyright notice for easier |
||||
identification within third-party archives. |
||||
|
||||
Copyright [yyyy] [name of copyright owner] |
||||
|
||||
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. |
||||
@ -0,0 +1,5 @@
|
||||
CoreOS Project |
||||
Copyright 2014 CoreOS, Inc |
||||
|
||||
This product includes software developed at CoreOS, Inc. |
||||
(http://www.coreos.com/). |
||||
@ -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,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,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,43 @@
|
||||
// Copyright 2017 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 namespace is a clientv3 wrapper that translates all keys to begin
|
||||
// with a given prefix.
|
||||
//
|
||||
// First, create a client:
|
||||
//
|
||||
// cli, err := clientv3.New(clientv3.Config{Endpoints: []string{"localhost:2379"}})
|
||||
// if err != nil {
|
||||
// // handle error!
|
||||
// }
|
||||
//
|
||||
// Next, override the client interfaces:
|
||||
//
|
||||
// unprefixedKV := cli.KV
|
||||
// cli.KV = namespace.NewKV(cli.KV, "my-prefix/")
|
||||
// cli.Watcher = namespace.NewWatcher(cli.Watcher, "my-prefix/")
|
||||
// cli.Lease = namespace.NewLease(cli.Lease, "my-prefix/")
|
||||
//
|
||||
// Now calls using 'cli' will namespace / prefix all keys with "my-prefix/":
|
||||
//
|
||||
// cli.Put(context.TODO(), "abc", "123")
|
||||
// resp, _ := unprefixedKV.Get(context.TODO(), "my-prefix/abc")
|
||||
// fmt.Printf("%s\n", resp.Kvs[0].Value)
|
||||
// // Output: 123
|
||||
// unprefixedKV.Put(context.TODO(), "my-prefix/abc", "456")
|
||||
// resp, _ = cli.Get("abc")
|
||||
// fmt.Printf("%s\n", resp.Kvs[0].Value)
|
||||
// // Output: 456
|
||||
//
|
||||
package namespace |
||||
@ -0,0 +1,189 @@
|
||||
// Copyright 2017 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 namespace |
||||
|
||||
import ( |
||||
"golang.org/x/net/context" |
||||
|
||||
"github.com/coreos/etcd/clientv3" |
||||
"github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes" |
||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb" |
||||
) |
||||
|
||||
type kvPrefix struct { |
||||
clientv3.KV |
||||
pfx string |
||||
} |
||||
|
||||
// NewKV wraps a KV instance so that all requests
|
||||
// are prefixed with a given string.
|
||||
func NewKV(kv clientv3.KV, prefix string) clientv3.KV { |
||||
return &kvPrefix{kv, prefix} |
||||
} |
||||
|
||||
func (kv *kvPrefix) Put(ctx context.Context, key, val string, opts ...clientv3.OpOption) (*clientv3.PutResponse, error) { |
||||
if len(key) == 0 { |
||||
return nil, rpctypes.ErrEmptyKey |
||||
} |
||||
op := kv.prefixOp(clientv3.OpPut(key, val, opts...)) |
||||
r, err := kv.KV.Do(ctx, op) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
put := r.Put() |
||||
kv.unprefixPutResponse(put) |
||||
return put, nil |
||||
} |
||||
|
||||
func (kv *kvPrefix) Get(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.GetResponse, error) { |
||||
if len(key) == 0 { |
||||
return nil, rpctypes.ErrEmptyKey |
||||
} |
||||
r, err := kv.KV.Do(ctx, kv.prefixOp(clientv3.OpGet(key, opts...))) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
get := r.Get() |
||||
kv.unprefixGetResponse(get) |
||||
return get, nil |
||||
} |
||||
|
||||
func (kv *kvPrefix) Delete(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.DeleteResponse, error) { |
||||
if len(key) == 0 { |
||||
return nil, rpctypes.ErrEmptyKey |
||||
} |
||||
r, err := kv.KV.Do(ctx, kv.prefixOp(clientv3.OpDelete(key, opts...))) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
del := r.Del() |
||||
kv.unprefixDeleteResponse(del) |
||||
return del, nil |
||||
} |
||||
|
||||
func (kv *kvPrefix) Do(ctx context.Context, op clientv3.Op) (clientv3.OpResponse, error) { |
||||
if len(op.KeyBytes()) == 0 { |
||||
return clientv3.OpResponse{}, rpctypes.ErrEmptyKey |
||||
} |
||||
r, err := kv.KV.Do(ctx, kv.prefixOp(op)) |
||||
if err != nil { |
||||
return r, err |
||||
} |
||||
switch { |
||||
case r.Get() != nil: |
||||
kv.unprefixGetResponse(r.Get()) |
||||
case r.Put() != nil: |
||||
kv.unprefixPutResponse(r.Put()) |
||||
case r.Del() != nil: |
||||
kv.unprefixDeleteResponse(r.Del()) |
||||
} |
||||
return r, nil |
||||
} |
||||
|
||||
type txnPrefix struct { |
||||
clientv3.Txn |
||||
kv *kvPrefix |
||||
} |
||||
|
||||
func (kv *kvPrefix) Txn(ctx context.Context) clientv3.Txn { |
||||
return &txnPrefix{kv.KV.Txn(ctx), kv} |
||||
} |
||||
|
||||
func (txn *txnPrefix) If(cs ...clientv3.Cmp) clientv3.Txn { |
||||
newCmps := make([]clientv3.Cmp, len(cs)) |
||||
for i := range cs { |
||||
newCmps[i] = cs[i] |
||||
pfxKey, _ := txn.kv.prefixInterval(cs[i].KeyBytes(), nil) |
||||
newCmps[i].WithKeyBytes(pfxKey) |
||||
} |
||||
txn.Txn = txn.Txn.If(newCmps...) |
||||
return txn |
||||
} |
||||
|
||||
func (txn *txnPrefix) Then(ops ...clientv3.Op) clientv3.Txn { |
||||
newOps := make([]clientv3.Op, len(ops)) |
||||
for i := range ops { |
||||
newOps[i] = txn.kv.prefixOp(ops[i]) |
||||
} |
||||
txn.Txn = txn.Txn.Then(newOps...) |
||||
return txn |
||||
} |
||||
|
||||
func (txn *txnPrefix) Else(ops ...clientv3.Op) clientv3.Txn { |
||||
newOps := make([]clientv3.Op, len(ops)) |
||||
for i := range ops { |
||||
newOps[i] = txn.kv.prefixOp(ops[i]) |
||||
} |
||||
txn.Txn = txn.Txn.Else(newOps...) |
||||
return txn |
||||
} |
||||
|
||||
func (txn *txnPrefix) Commit() (*clientv3.TxnResponse, error) { |
||||
resp, err := txn.Txn.Commit() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
txn.kv.unprefixTxnResponse(resp) |
||||
return resp, nil |
||||
} |
||||
|
||||
func (kv *kvPrefix) prefixOp(op clientv3.Op) clientv3.Op { |
||||
begin, end := kv.prefixInterval(op.KeyBytes(), op.RangeBytes()) |
||||
op.WithKeyBytes(begin) |
||||
op.WithRangeBytes(end) |
||||
return op |
||||
} |
||||
|
||||
func (kv *kvPrefix) unprefixGetResponse(resp *clientv3.GetResponse) { |
||||
for i := range resp.Kvs { |
||||
resp.Kvs[i].Key = resp.Kvs[i].Key[len(kv.pfx):] |
||||
} |
||||
} |
||||
|
||||
func (kv *kvPrefix) unprefixPutResponse(resp *clientv3.PutResponse) { |
||||
if resp.PrevKv != nil { |
||||
resp.PrevKv.Key = resp.PrevKv.Key[len(kv.pfx):] |
||||
} |
||||
} |
||||
|
||||
func (kv *kvPrefix) unprefixDeleteResponse(resp *clientv3.DeleteResponse) { |
||||
for i := range resp.PrevKvs { |
||||
resp.PrevKvs[i].Key = resp.PrevKvs[i].Key[len(kv.pfx):] |
||||
} |
||||
} |
||||
|
||||
func (kv *kvPrefix) unprefixTxnResponse(resp *clientv3.TxnResponse) { |
||||
for _, r := range resp.Responses { |
||||
switch tv := r.Response.(type) { |
||||
case *pb.ResponseOp_ResponseRange: |
||||
if tv.ResponseRange != nil { |
||||
kv.unprefixGetResponse((*clientv3.GetResponse)(tv.ResponseRange)) |
||||
} |
||||
case *pb.ResponseOp_ResponsePut: |
||||
if tv.ResponsePut != nil { |
||||
kv.unprefixPutResponse((*clientv3.PutResponse)(tv.ResponsePut)) |
||||
} |
||||
case *pb.ResponseOp_ResponseDeleteRange: |
||||
if tv.ResponseDeleteRange != nil { |
||||
kv.unprefixDeleteResponse((*clientv3.DeleteResponse)(tv.ResponseDeleteRange)) |
||||
} |
||||
default: |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (p *kvPrefix) prefixInterval(key, end []byte) (pfxKey []byte, pfxEnd []byte) { |
||||
return prefixInterval(p.pfx, key, end) |
||||
} |
||||
@ -0,0 +1,58 @@
|
||||
// Copyright 2017 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 namespace |
||||
|
||||
import ( |
||||
"bytes" |
||||
|
||||
"golang.org/x/net/context" |
||||
|
||||
"github.com/coreos/etcd/clientv3" |
||||
) |
||||
|
||||
type leasePrefix struct { |
||||
clientv3.Lease |
||||
pfx []byte |
||||
} |
||||
|
||||
// NewLease wraps a Lease interface to filter for only keys with a prefix
|
||||
// and remove that prefix when fetching attached keys through TimeToLive.
|
||||
func NewLease(l clientv3.Lease, prefix string) clientv3.Lease { |
||||
return &leasePrefix{l, []byte(prefix)} |
||||
} |
||||
|
||||
func (l *leasePrefix) TimeToLive(ctx context.Context, id clientv3.LeaseID, opts ...clientv3.LeaseOption) (*clientv3.LeaseTimeToLiveResponse, error) { |
||||
resp, err := l.Lease.TimeToLive(ctx, id, opts...) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if len(resp.Keys) > 0 { |
||||
var outKeys [][]byte |
||||
for i := range resp.Keys { |
||||
if len(resp.Keys[i]) < len(l.pfx) { |
||||
// too short
|
||||
continue |
||||
} |
||||
if !bytes.Equal(resp.Keys[i][:len(l.pfx)], l.pfx) { |
||||
// doesn't match prefix
|
||||
continue |
||||
} |
||||
// strip prefix
|
||||
outKeys = append(outKeys, resp.Keys[i][len(l.pfx):]) |
||||
} |
||||
resp.Keys = outKeys |
||||
} |
||||
return resp, nil |
||||
} |
||||
@ -0,0 +1,42 @@
|
||||
// Copyright 2017 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 namespace |
||||
|
||||
func prefixInterval(pfx string, key, end []byte) (pfxKey []byte, pfxEnd []byte) { |
||||
pfxKey = make([]byte, len(pfx)+len(key)) |
||||
copy(pfxKey[copy(pfxKey, pfx):], key) |
||||
|
||||
if len(end) == 1 && end[0] == 0 { |
||||
// the edge of the keyspace
|
||||
pfxEnd = make([]byte, len(pfx)) |
||||
copy(pfxEnd, pfx) |
||||
ok := false |
||||
for i := len(pfxEnd) - 1; i >= 0; i-- { |
||||
if pfxEnd[i]++; pfxEnd[i] != 0 { |
||||
ok = true |
||||
break |
||||
} |
||||
} |
||||
if !ok { |
||||
// 0xff..ff => 0x00
|
||||
pfxEnd = []byte{0} |
||||
} |
||||
} else if len(end) >= 1 { |
||||
pfxEnd = make([]byte, len(pfx)+len(end)) |
||||
copy(pfxEnd[copy(pfxEnd, pfx):], end) |
||||
} |
||||
|
||||
return pfxKey, pfxEnd |
||||
} |
||||
@ -0,0 +1,84 @@
|
||||
// Copyright 2017 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 namespace |
||||
|
||||
import ( |
||||
"sync" |
||||
|
||||
"golang.org/x/net/context" |
||||
|
||||
"github.com/coreos/etcd/clientv3" |
||||
) |
||||
|
||||
type watcherPrefix struct { |
||||
clientv3.Watcher |
||||
pfx string |
||||
|
||||
wg sync.WaitGroup |
||||
stopc chan struct{} |
||||
stopOnce sync.Once |
||||
} |
||||
|
||||
// NewWatcher wraps a Watcher instance so that all Watch requests
|
||||
// are prefixed with a given string and all Watch responses have
|
||||
// the prefix removed.
|
||||
func NewWatcher(w clientv3.Watcher, prefix string) clientv3.Watcher { |
||||
return &watcherPrefix{Watcher: w, pfx: prefix, stopc: make(chan struct{})} |
||||
} |
||||
|
||||
func (w *watcherPrefix) Watch(ctx context.Context, key string, opts ...clientv3.OpOption) clientv3.WatchChan { |
||||
// since OpOption is opaque, determine range for prefixing through an OpGet
|
||||
op := clientv3.OpGet(key, opts...) |
||||
end := op.RangeBytes() |
||||
pfxBegin, pfxEnd := prefixInterval(w.pfx, []byte(key), end) |
||||
if pfxEnd != nil { |
||||
opts = append(opts, clientv3.WithRange(string(pfxEnd))) |
||||
} |
||||
|
||||
wch := w.Watcher.Watch(ctx, string(pfxBegin), opts...) |
||||
|
||||
// translate watch events from prefixed to unprefixed
|
||||
pfxWch := make(chan clientv3.WatchResponse) |
||||
w.wg.Add(1) |
||||
go func() { |
||||
defer func() { |
||||
close(pfxWch) |
||||
w.wg.Done() |
||||
}() |
||||
for wr := range wch { |
||||
for i := range wr.Events { |
||||
wr.Events[i].Kv.Key = wr.Events[i].Kv.Key[len(w.pfx):] |
||||
if wr.Events[i].PrevKv != nil { |
||||
wr.Events[i].PrevKv.Key = wr.Events[i].Kv.Key |
||||
} |
||||
} |
||||
select { |
||||
case pfxWch <- wr: |
||||
case <-ctx.Done(): |
||||
return |
||||
case <-w.stopc: |
||||
return |
||||
} |
||||
} |
||||
}() |
||||
return pfxWch |
||||
} |
||||
|
||||
func (w *watcherPrefix) Close() error { |
||||
err := w.Watcher.Close() |
||||
w.stopOnce.Do(func() { close(w.stopc) }) |
||||
w.wg.Wait() |
||||
return err |
||||
} |
||||
@ -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} |
||||
} |
||||
@ -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 |
||||
@ -0,0 +1,190 @@
|
||||
// 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") |
||||
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(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) |
||||
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" |
||||
) |
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,29 @@
|
||||
// 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 main is a simple wrapper of the real etcd entrypoint package
|
||||
// (located at github.com/coreos/etcd/etcdmain) to ensure that etcd is still
|
||||
// "go getable"; e.g. `go get github.com/coreos/etcd` works as expected and
|
||||
// builds a binary in $GOBIN/etcd
|
||||
//
|
||||
// This package should NOT be extended or modified in any way; to modify the
|
||||
// etcd binary, work in the `github.com/coreos/etcd/etcdmain` package.
|
||||
//
|
||||
package main |
||||
|
||||
import "github.com/coreos/etcd/etcdmain" |
||||
|
||||
func main() { |
||||
etcdmain.Main() |
||||
} |
||||
@ -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,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 tlsutil provides utility functions for handling TLS.
|
||||
package tlsutil |
||||
@ -0,0 +1,72 @@
|
||||
// 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 tlsutil |
||||
|
||||
import ( |
||||
"crypto/tls" |
||||
"crypto/x509" |
||||
"encoding/pem" |
||||
"io/ioutil" |
||||
) |
||||
|
||||
// NewCertPool creates x509 certPool with provided CA files.
|
||||
func NewCertPool(CAFiles []string) (*x509.CertPool, error) { |
||||
certPool := x509.NewCertPool() |
||||
|
||||
for _, CAFile := range CAFiles { |
||||
pemByte, err := ioutil.ReadFile(CAFile) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
for { |
||||
var block *pem.Block |
||||
block, pemByte = pem.Decode(pemByte) |
||||
if block == nil { |
||||
break |
||||
} |
||||
cert, err := x509.ParseCertificate(block.Bytes) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
certPool.AddCert(cert) |
||||
} |
||||
} |
||||
|
||||
return certPool, nil |
||||
} |
||||
|
||||
// NewCert generates TLS cert by using the given cert,key and parse function.
|
||||
func NewCert(certfile, keyfile string, parseFunc func([]byte, []byte) (tls.Certificate, error)) (*tls.Certificate, error) { |
||||
cert, err := ioutil.ReadFile(certfile) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
key, err := ioutil.ReadFile(keyfile) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if parseFunc == nil { |
||||
parseFunc = tls.X509KeyPair |
||||
} |
||||
|
||||
tlsCert, err := parseFunc(cert, key) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return &tlsCert, nil |
||||
} |
||||
@ -0,0 +1,17 @@
|
||||
// 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 transport implements various HTTP transport utilities based on Go
|
||||
// net package.
|
||||
package transport |
||||
@ -0,0 +1,94 @@
|
||||
// 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 transport |
||||
|
||||
import ( |
||||
"crypto/tls" |
||||
"fmt" |
||||
"net" |
||||
"time" |
||||
) |
||||
|
||||
type keepAliveConn interface { |
||||
SetKeepAlive(bool) error |
||||
SetKeepAlivePeriod(d time.Duration) error |
||||
} |
||||
|
||||
// NewKeepAliveListener returns a listener that listens on the given address.
|
||||
// Be careful when wrap around KeepAliveListener with another Listener if TLSInfo is not nil.
|
||||
// Some pkgs (like go/http) might expect Listener to return TLSConn type to start TLS handshake.
|
||||
// http://tldp.org/HOWTO/TCP-Keepalive-HOWTO/overview.html
|
||||
func NewKeepAliveListener(l net.Listener, scheme string, tlscfg *tls.Config) (net.Listener, error) { |
||||
if scheme == "https" { |
||||
if tlscfg == nil { |
||||
return nil, fmt.Errorf("cannot listen on TLS for given listener: KeyFile and CertFile are not presented") |
||||
} |
||||
return newTLSKeepaliveListener(l, tlscfg), nil |
||||
} |
||||
|
||||
return &keepaliveListener{ |
||||
Listener: l, |
||||
}, nil |
||||
} |
||||
|
||||
type keepaliveListener struct{ net.Listener } |
||||
|
||||
func (kln *keepaliveListener) Accept() (net.Conn, error) { |
||||
c, err := kln.Listener.Accept() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
kac := c.(keepAliveConn) |
||||
// detection time: tcp_keepalive_time + tcp_keepalive_probes + tcp_keepalive_intvl
|
||||
// default on linux: 30 + 8 * 30
|
||||
// default on osx: 30 + 8 * 75
|
||||
kac.SetKeepAlive(true) |
||||
kac.SetKeepAlivePeriod(30 * time.Second) |
||||
return c, nil |
||||
} |
||||
|
||||
// A tlsKeepaliveListener implements a network listener (net.Listener) for TLS connections.
|
||||
type tlsKeepaliveListener struct { |
||||
net.Listener |
||||
config *tls.Config |
||||
} |
||||
|
||||
// Accept waits for and returns the next incoming TLS connection.
|
||||
// The returned connection c is a *tls.Conn.
|
||||
func (l *tlsKeepaliveListener) Accept() (c net.Conn, err error) { |
||||
c, err = l.Listener.Accept() |
||||
if err != nil { |
||||
return |
||||
} |
||||
kac := c.(keepAliveConn) |
||||
// detection time: tcp_keepalive_time + tcp_keepalive_probes + tcp_keepalive_intvl
|
||||
// default on linux: 30 + 8 * 30
|
||||
// default on osx: 30 + 8 * 75
|
||||
kac.SetKeepAlive(true) |
||||
kac.SetKeepAlivePeriod(30 * time.Second) |
||||
c = tls.Server(c, l.config) |
||||
return |
||||
} |
||||
|
||||
// NewListener creates a Listener which accepts connections from an inner
|
||||
// Listener and wraps each connection with Server.
|
||||
// The configuration config must be non-nil and must have
|
||||
// at least one certificate.
|
||||
func newTLSKeepaliveListener(inner net.Listener, config *tls.Config) net.Listener { |
||||
l := &tlsKeepaliveListener{} |
||||
l.Listener = inner |
||||
l.config = config |
||||
return l |
||||
} |
||||
@ -0,0 +1,80 @@
|
||||
// Copyright 2013 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 transport provides network utility functions, complementing the more
|
||||
// common ones in the net package.
|
||||
package transport |
||||
|
||||
import ( |
||||
"errors" |
||||
"net" |
||||
"sync" |
||||
"time" |
||||
) |
||||
|
||||
var ( |
||||
ErrNotTCP = errors.New("only tcp connections have keepalive") |
||||
) |
||||
|
||||
// LimitListener returns a Listener that accepts at most n simultaneous
|
||||
// connections from the provided Listener.
|
||||
func LimitListener(l net.Listener, n int) net.Listener { |
||||
return &limitListener{l, make(chan struct{}, n)} |
||||
} |
||||
|
||||
type limitListener struct { |
||||
net.Listener |
||||
sem chan struct{} |
||||
} |
||||
|
||||
func (l *limitListener) acquire() { l.sem <- struct{}{} } |
||||
func (l *limitListener) release() { <-l.sem } |
||||
|
||||
func (l *limitListener) Accept() (net.Conn, error) { |
||||
l.acquire() |
||||
c, err := l.Listener.Accept() |
||||
if err != nil { |
||||
l.release() |
||||
return nil, err |
||||
} |
||||
return &limitListenerConn{Conn: c, release: l.release}, nil |
||||
} |
||||
|
||||
type limitListenerConn struct { |
||||
net.Conn |
||||
releaseOnce sync.Once |
||||
release func() |
||||
} |
||||
|
||||
func (l *limitListenerConn) Close() error { |
||||
err := l.Conn.Close() |
||||
l.releaseOnce.Do(l.release) |
||||
return err |
||||
} |
||||
|
||||
func (l *limitListenerConn) SetKeepAlive(doKeepAlive bool) error { |
||||
tcpc, ok := l.Conn.(*net.TCPConn) |
||||
if !ok { |
||||
return ErrNotTCP |
||||
} |
||||
return tcpc.SetKeepAlive(doKeepAlive) |
||||
} |
||||
|
||||
func (l *limitListenerConn) SetKeepAlivePeriod(d time.Duration) error { |
||||
tcpc, ok := l.Conn.(*net.TCPConn) |
||||
if !ok { |
||||
return ErrNotTCP |
||||
} |
||||
return tcpc.SetKeepAlivePeriod(d) |
||||
} |
||||
@ -0,0 +1,260 @@
|
||||
// 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 transport |
||||
|
||||
import ( |
||||
"crypto/ecdsa" |
||||
"crypto/elliptic" |
||||
"crypto/rand" |
||||
"crypto/tls" |
||||
"crypto/x509" |
||||
"crypto/x509/pkix" |
||||
"encoding/pem" |
||||
"fmt" |
||||
"math/big" |
||||
"net" |
||||
"os" |
||||
"path/filepath" |
||||
"strings" |
||||
"time" |
||||
|
||||
"github.com/coreos/etcd/pkg/tlsutil" |
||||
) |
||||
|
||||
func NewListener(addr, scheme string, tlsinfo *TLSInfo) (l net.Listener, err error) { |
||||
if l, err = newListener(addr, scheme); err != nil { |
||||
return nil, err |
||||
} |
||||
return wrapTLS(addr, scheme, tlsinfo, l) |
||||
} |
||||
|
||||
func newListener(addr string, scheme string) (net.Listener, error) { |
||||
if scheme == "unix" || scheme == "unixs" { |
||||
// unix sockets via unix://laddr
|
||||
return NewUnixListener(addr) |
||||
} |
||||
return net.Listen("tcp", addr) |
||||
} |
||||
|
||||
func wrapTLS(addr, scheme string, tlsinfo *TLSInfo, l net.Listener) (net.Listener, error) { |
||||
if scheme != "https" && scheme != "unixs" { |
||||
return l, nil |
||||
} |
||||
return newTLSListener(l, tlsinfo) |
||||
} |
||||
|
||||
type TLSInfo struct { |
||||
CertFile string |
||||
KeyFile string |
||||
CAFile string |
||||
TrustedCAFile string |
||||
ClientCertAuth bool |
||||
|
||||
// ServerName ensures the cert matches the given host in case of discovery / virtual hosting
|
||||
ServerName string |
||||
|
||||
// HandshakeFailure is optionally called when a connection fails to handshake. The
|
||||
// connection will be closed immediately afterwards.
|
||||
HandshakeFailure func(*tls.Conn, error) |
||||
|
||||
selfCert bool |
||||
|
||||
// parseFunc exists to simplify testing. Typically, parseFunc
|
||||
// should be left nil. In that case, tls.X509KeyPair will be used.
|
||||
parseFunc func([]byte, []byte) (tls.Certificate, error) |
||||
} |
||||
|
||||
func (info TLSInfo) String() string { |
||||
return fmt.Sprintf("cert = %s, key = %s, ca = %s, trusted-ca = %s, client-cert-auth = %v", info.CertFile, info.KeyFile, info.CAFile, info.TrustedCAFile, info.ClientCertAuth) |
||||
} |
||||
|
||||
func (info TLSInfo) Empty() bool { |
||||
return info.CertFile == "" && info.KeyFile == "" |
||||
} |
||||
|
||||
func SelfCert(dirpath string, hosts []string) (info TLSInfo, err error) { |
||||
if err = os.MkdirAll(dirpath, 0700); err != nil { |
||||
return |
||||
} |
||||
|
||||
certPath := filepath.Join(dirpath, "cert.pem") |
||||
keyPath := filepath.Join(dirpath, "key.pem") |
||||
_, errcert := os.Stat(certPath) |
||||
_, errkey := os.Stat(keyPath) |
||||
if errcert == nil && errkey == nil { |
||||
info.CertFile = certPath |
||||
info.KeyFile = keyPath |
||||
info.selfCert = true |
||||
return |
||||
} |
||||
|
||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) |
||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
tmpl := x509.Certificate{ |
||||
SerialNumber: serialNumber, |
||||
Subject: pkix.Name{Organization: []string{"etcd"}}, |
||||
NotBefore: time.Now(), |
||||
NotAfter: time.Now().Add(365 * (24 * time.Hour)), |
||||
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, |
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, |
||||
BasicConstraintsValid: true, |
||||
} |
||||
|
||||
for _, host := range hosts { |
||||
h, _, _ := net.SplitHostPort(host) |
||||
if ip := net.ParseIP(h); ip != nil { |
||||
tmpl.IPAddresses = append(tmpl.IPAddresses, ip) |
||||
} else { |
||||
tmpl.DNSNames = append(tmpl.DNSNames, h) |
||||
} |
||||
} |
||||
|
||||
priv, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, &priv.PublicKey, priv) |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
certOut, err := os.Create(certPath) |
||||
if err != nil { |
||||
return |
||||
} |
||||
pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) |
||||
certOut.Close() |
||||
|
||||
b, err := x509.MarshalECPrivateKey(priv) |
||||
if err != nil { |
||||
return |
||||
} |
||||
keyOut, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) |
||||
if err != nil { |
||||
return |
||||
} |
||||
pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}) |
||||
keyOut.Close() |
||||
|
||||
return SelfCert(dirpath, hosts) |
||||
} |
||||
|
||||
func (info TLSInfo) baseConfig() (*tls.Config, error) { |
||||
if info.KeyFile == "" || info.CertFile == "" { |
||||
return nil, fmt.Errorf("KeyFile and CertFile must both be present[key: %v, cert: %v]", info.KeyFile, info.CertFile) |
||||
} |
||||
|
||||
tlsCert, err := tlsutil.NewCert(info.CertFile, info.KeyFile, info.parseFunc) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
cfg := &tls.Config{ |
||||
Certificates: []tls.Certificate{*tlsCert}, |
||||
MinVersion: tls.VersionTLS12, |
||||
ServerName: info.ServerName, |
||||
} |
||||
// this only reloads certs when there's a client request
|
||||
// TODO: support server-side refresh (e.g. inotify, SIGHUP), caching
|
||||
cfg.GetCertificate = func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { |
||||
return tlsutil.NewCert(info.CertFile, info.KeyFile, info.parseFunc) |
||||
} |
||||
cfg.GetClientCertificate = func(unused *tls.CertificateRequestInfo) (*tls.Certificate, error) { |
||||
return tlsutil.NewCert(info.CertFile, info.KeyFile, info.parseFunc) |
||||
} |
||||
return cfg, nil |
||||
} |
||||
|
||||
// cafiles returns a list of CA file paths.
|
||||
func (info TLSInfo) cafiles() []string { |
||||
cs := make([]string, 0) |
||||
if info.CAFile != "" { |
||||
cs = append(cs, info.CAFile) |
||||
} |
||||
if info.TrustedCAFile != "" { |
||||
cs = append(cs, info.TrustedCAFile) |
||||
} |
||||
return cs |
||||
} |
||||
|
||||
// ServerConfig generates a tls.Config object for use by an HTTP server.
|
||||
func (info TLSInfo) ServerConfig() (*tls.Config, error) { |
||||
cfg, err := info.baseConfig() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
cfg.ClientAuth = tls.NoClientCert |
||||
if info.CAFile != "" || info.ClientCertAuth { |
||||
cfg.ClientAuth = tls.RequireAndVerifyClientCert |
||||
} |
||||
|
||||
CAFiles := info.cafiles() |
||||
if len(CAFiles) > 0 { |
||||
cp, err := tlsutil.NewCertPool(CAFiles) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
cfg.ClientCAs = cp |
||||
} |
||||
|
||||
// "h2" NextProtos is necessary for enabling HTTP2 for go's HTTP server
|
||||
cfg.NextProtos = []string{"h2"} |
||||
|
||||
return cfg, nil |
||||
} |
||||
|
||||
// ClientConfig generates a tls.Config object for use by an HTTP client.
|
||||
func (info TLSInfo) ClientConfig() (*tls.Config, error) { |
||||
var cfg *tls.Config |
||||
var err error |
||||
|
||||
if !info.Empty() { |
||||
cfg, err = info.baseConfig() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} else { |
||||
cfg = &tls.Config{ServerName: info.ServerName} |
||||
} |
||||
|
||||
CAFiles := info.cafiles() |
||||
if len(CAFiles) > 0 { |
||||
cfg.RootCAs, err = tlsutil.NewCertPool(CAFiles) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
if info.selfCert { |
||||
cfg.InsecureSkipVerify = true |
||||
} |
||||
return cfg, nil |
||||
} |
||||
|
||||
// IsClosedConnError returns true if the error is from closing listener, cmux.
|
||||
// copied from golang.org/x/net/http2/http2.go
|
||||
func IsClosedConnError(err error) bool { |
||||
// 'use of closed network connection' (Go <=1.8)
|
||||
// 'use of closed file or network connection' (Go >1.8, internal/poll.ErrClosing)
|
||||
// 'mux: listener closed' (cmux.ErrListenerClosed)
|
||||
return err != nil && strings.Contains(err.Error(), "closed") |
||||
} |
||||
@ -0,0 +1,217 @@
|
||||
// Copyright 2017 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 transport |
||||
|
||||
import ( |
||||
"context" |
||||
"crypto/tls" |
||||
"crypto/x509" |
||||
"fmt" |
||||
"net" |
||||
"strings" |
||||
"sync" |
||||
) |
||||
|
||||
// tlsListener overrides a TLS listener so it will reject client
|
||||
// certificates with insufficient SAN credentials.
|
||||
type tlsListener struct { |
||||
net.Listener |
||||
connc chan net.Conn |
||||
donec chan struct{} |
||||
err error |
||||
handshakeFailure func(*tls.Conn, error) |
||||
} |
||||
|
||||
func newTLSListener(l net.Listener, tlsinfo *TLSInfo) (net.Listener, error) { |
||||
if tlsinfo == nil || tlsinfo.Empty() { |
||||
l.Close() |
||||
return nil, fmt.Errorf("cannot listen on TLS for %s: KeyFile and CertFile are not presented", l.Addr().String()) |
||||
} |
||||
tlscfg, err := tlsinfo.ServerConfig() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
hf := tlsinfo.HandshakeFailure |
||||
if hf == nil { |
||||
hf = func(*tls.Conn, error) {} |
||||
} |
||||
tlsl := &tlsListener{ |
||||
Listener: tls.NewListener(l, tlscfg), |
||||
connc: make(chan net.Conn), |
||||
donec: make(chan struct{}), |
||||
handshakeFailure: hf, |
||||
} |
||||
go tlsl.acceptLoop() |
||||
return tlsl, nil |
||||
} |
||||
|
||||
func (l *tlsListener) Accept() (net.Conn, error) { |
||||
select { |
||||
case conn := <-l.connc: |
||||
return conn, nil |
||||
case <-l.donec: |
||||
return nil, l.err |
||||
} |
||||
} |
||||
|
||||
// acceptLoop launches each TLS handshake in a separate goroutine
|
||||
// to prevent a hanging TLS connection from blocking other connections.
|
||||
func (l *tlsListener) acceptLoop() { |
||||
var wg sync.WaitGroup |
||||
var pendingMu sync.Mutex |
||||
|
||||
pending := make(map[net.Conn]struct{}) |
||||
ctx, cancel := context.WithCancel(context.Background()) |
||||
defer func() { |
||||
cancel() |
||||
pendingMu.Lock() |
||||
for c := range pending { |
||||
c.Close() |
||||
} |
||||
pendingMu.Unlock() |
||||
wg.Wait() |
||||
close(l.donec) |
||||
}() |
||||
|
||||
for { |
||||
conn, err := l.Listener.Accept() |
||||
if err != nil { |
||||
l.err = err |
||||
return |
||||
} |
||||
|
||||
pendingMu.Lock() |
||||
pending[conn] = struct{}{} |
||||
pendingMu.Unlock() |
||||
|
||||
wg.Add(1) |
||||
go func() { |
||||
defer func() { |
||||
if conn != nil { |
||||
conn.Close() |
||||
} |
||||
wg.Done() |
||||
}() |
||||
|
||||
tlsConn := conn.(*tls.Conn) |
||||
herr := tlsConn.Handshake() |
||||
pendingMu.Lock() |
||||
delete(pending, conn) |
||||
pendingMu.Unlock() |
||||
if herr != nil { |
||||
l.handshakeFailure(tlsConn, herr) |
||||
return |
||||
} |
||||
|
||||
st := tlsConn.ConnectionState() |
||||
if len(st.PeerCertificates) > 0 { |
||||
cert := st.PeerCertificates[0] |
||||
addr := tlsConn.RemoteAddr().String() |
||||
if cerr := checkCert(ctx, cert, addr); cerr != nil { |
||||
l.handshakeFailure(tlsConn, cerr) |
||||
return |
||||
} |
||||
} |
||||
select { |
||||
case l.connc <- tlsConn: |
||||
conn = nil |
||||
case <-ctx.Done(): |
||||
} |
||||
}() |
||||
} |
||||
} |
||||
|
||||
func checkCert(ctx context.Context, cert *x509.Certificate, remoteAddr string) error { |
||||
h, _, herr := net.SplitHostPort(remoteAddr) |
||||
if len(cert.IPAddresses) == 0 && len(cert.DNSNames) == 0 { |
||||
return nil |
||||
} |
||||
if herr != nil { |
||||
return herr |
||||
} |
||||
if len(cert.IPAddresses) > 0 { |
||||
cerr := cert.VerifyHostname(h) |
||||
if cerr == nil { |
||||
return nil |
||||
} |
||||
if len(cert.DNSNames) == 0 { |
||||
return cerr |
||||
} |
||||
} |
||||
if len(cert.DNSNames) > 0 { |
||||
ok, err := isHostInDNS(ctx, h, cert.DNSNames) |
||||
if ok { |
||||
return nil |
||||
} |
||||
errStr := "" |
||||
if err != nil { |
||||
errStr = " (" + err.Error() + ")" |
||||
} |
||||
return fmt.Errorf("tls: %q does not match any of DNSNames %q"+errStr, h, cert.DNSNames) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func isHostInDNS(ctx context.Context, host string, dnsNames []string) (ok bool, err error) { |
||||
// reverse lookup
|
||||
wildcards, names := []string{}, []string{} |
||||
for _, dns := range dnsNames { |
||||
if strings.HasPrefix(dns, "*.") { |
||||
wildcards = append(wildcards, dns[1:]) |
||||
} else { |
||||
names = append(names, dns) |
||||
} |
||||
} |
||||
lnames, lerr := net.DefaultResolver.LookupAddr(ctx, host) |
||||
for _, name := range lnames { |
||||
// strip trailing '.' from PTR record
|
||||
if name[len(name)-1] == '.' { |
||||
name = name[:len(name)-1] |
||||
} |
||||
for _, wc := range wildcards { |
||||
if strings.HasSuffix(name, wc) { |
||||
return true, nil |
||||
} |
||||
} |
||||
for _, n := range names { |
||||
if n == name { |
||||
return true, nil |
||||
} |
||||
} |
||||
} |
||||
err = lerr |
||||
|
||||
// forward lookup
|
||||
for _, dns := range names { |
||||
addrs, lerr := net.DefaultResolver.LookupHost(ctx, dns) |
||||
if lerr != nil { |
||||
err = lerr |
||||
continue |
||||
} |
||||
for _, addr := range addrs { |
||||
if addr == host { |
||||
return true, nil |
||||
} |
||||
} |
||||
} |
||||
return false, err |
||||
} |
||||
|
||||
func (l *tlsListener) Close() error { |
||||
err := l.Listener.Close() |
||||
<-l.donec |
||||
return err |
||||
} |
||||
@ -0,0 +1,44 @@
|
||||
// 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 transport |
||||
|
||||
import ( |
||||
"net" |
||||
"time" |
||||
) |
||||
|
||||
type timeoutConn struct { |
||||
net.Conn |
||||
wtimeoutd time.Duration |
||||
rdtimeoutd time.Duration |
||||
} |
||||
|
||||
func (c timeoutConn) Write(b []byte) (n int, err error) { |
||||
if c.wtimeoutd > 0 { |
||||
if err := c.SetWriteDeadline(time.Now().Add(c.wtimeoutd)); err != nil { |
||||
return 0, err |
||||
} |
||||
} |
||||
return c.Conn.Write(b) |
||||
} |
||||
|
||||
func (c timeoutConn) Read(b []byte) (n int, err error) { |
||||
if c.rdtimeoutd > 0 { |
||||
if err := c.SetReadDeadline(time.Now().Add(c.rdtimeoutd)); err != nil { |
||||
return 0, err |
||||
} |
||||
} |
||||
return c.Conn.Read(b) |
||||
} |
||||
@ -0,0 +1,36 @@
|
||||
// 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 transport |
||||
|
||||
import ( |
||||
"net" |
||||
"time" |
||||
) |
||||
|
||||
type rwTimeoutDialer struct { |
||||
wtimeoutd time.Duration |
||||
rdtimeoutd time.Duration |
||||
net.Dialer |
||||
} |
||||
|
||||
func (d *rwTimeoutDialer) Dial(network, address string) (net.Conn, error) { |
||||
conn, err := d.Dialer.Dial(network, address) |
||||
tconn := &timeoutConn{ |
||||
rdtimeoutd: d.rdtimeoutd, |
||||
wtimeoutd: d.wtimeoutd, |
||||
Conn: conn, |
||||
} |
||||
return tconn, err |
||||
} |
||||
@ -0,0 +1,57 @@
|
||||
// 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 transport |
||||
|
||||
import ( |
||||
"net" |
||||
"time" |
||||
) |
||||
|
||||
// NewTimeoutListener returns a listener that listens on the given address.
|
||||
// If read/write on the accepted connection blocks longer than its time limit,
|
||||
// it will return timeout error.
|
||||
func NewTimeoutListener(addr string, scheme string, tlsinfo *TLSInfo, rdtimeoutd, wtimeoutd time.Duration) (net.Listener, error) { |
||||
ln, err := newListener(addr, scheme) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
ln = &rwTimeoutListener{ |
||||
Listener: ln, |
||||
rdtimeoutd: rdtimeoutd, |
||||
wtimeoutd: wtimeoutd, |
||||
} |
||||
if ln, err = wrapTLS(addr, scheme, tlsinfo, ln); err != nil { |
||||
return nil, err |
||||
} |
||||
return ln, nil |
||||
} |
||||
|
||||
type rwTimeoutListener struct { |
||||
net.Listener |
||||
wtimeoutd time.Duration |
||||
rdtimeoutd time.Duration |
||||
} |
||||
|
||||
func (rwln *rwTimeoutListener) Accept() (net.Conn, error) { |
||||
c, err := rwln.Listener.Accept() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return timeoutConn{ |
||||
Conn: c, |
||||
wtimeoutd: rwln.wtimeoutd, |
||||
rdtimeoutd: rwln.rdtimeoutd, |
||||
}, nil |
||||
} |
||||
@ -0,0 +1,51 @@
|
||||
// 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 transport |
||||
|
||||
import ( |
||||
"net" |
||||
"net/http" |
||||
"time" |
||||
) |
||||
|
||||
// NewTimeoutTransport returns a transport created using the given TLS info.
|
||||
// If read/write on the created connection blocks longer than its time limit,
|
||||
// it will return timeout error.
|
||||
// If read/write timeout is set, transport will not be able to reuse connection.
|
||||
func NewTimeoutTransport(info TLSInfo, dialtimeoutd, rdtimeoutd, wtimeoutd time.Duration) (*http.Transport, error) { |
||||
tr, err := NewTransport(info, dialtimeoutd) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if rdtimeoutd != 0 || wtimeoutd != 0 { |
||||
// the timed out connection will timeout soon after it is idle.
|
||||
// it should not be put back to http transport as an idle connection for future usage.
|
||||
tr.MaxIdleConnsPerHost = -1 |
||||
} else { |
||||
// allow more idle connections between peers to avoid unnecessary port allocation.
|
||||
tr.MaxIdleConnsPerHost = 1024 |
||||
} |
||||
|
||||
tr.Dial = (&rwTimeoutDialer{ |
||||
Dialer: net.Dialer{ |
||||
Timeout: dialtimeoutd, |
||||
KeepAlive: 30 * time.Second, |
||||
}, |
||||
rdtimeoutd: rdtimeoutd, |
||||
wtimeoutd: wtimeoutd, |
||||
}).Dial |
||||
return tr, nil |
||||
} |
||||
@ -0,0 +1,49 @@
|
||||
// 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 transport |
||||
|
||||
import ( |
||||
"fmt" |
||||
"strings" |
||||
"time" |
||||
) |
||||
|
||||
// ValidateSecureEndpoints scans the given endpoints against tls info, returning only those
|
||||
// endpoints that could be validated as secure.
|
||||
func ValidateSecureEndpoints(tlsInfo TLSInfo, eps []string) ([]string, error) { |
||||
t, err := NewTransport(tlsInfo, 5*time.Second) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
var errs []string |
||||
var endpoints []string |
||||
for _, ep := range eps { |
||||
if !strings.HasPrefix(ep, "https://") { |
||||
errs = append(errs, fmt.Sprintf("%q is insecure", ep)) |
||||
continue |
||||
} |
||||
conn, cerr := t.Dial("tcp", ep[len("https://"):]) |
||||
if cerr != nil { |
||||
errs = append(errs, fmt.Sprintf("%q failed to dial (%v)", ep, cerr)) |
||||
continue |
||||
} |
||||
conn.Close() |
||||
endpoints = append(endpoints, ep) |
||||
} |
||||
if len(errs) != 0 { |
||||
err = fmt.Errorf("%s", strings.Join(errs, ",")) |
||||
} |
||||
return endpoints, err |
||||
} |
||||
@ -0,0 +1,71 @@
|
||||
// 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 transport |
||||
|
||||
import ( |
||||
"net" |
||||
"net/http" |
||||
"strings" |
||||
"time" |
||||
) |
||||
|
||||
type unixTransport struct{ *http.Transport } |
||||
|
||||
func NewTransport(info TLSInfo, dialtimeoutd time.Duration) (*http.Transport, error) { |
||||
cfg, err := info.ClientConfig() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
t := &http.Transport{ |
||||
Proxy: http.ProxyFromEnvironment, |
||||
Dial: (&net.Dialer{ |
||||
Timeout: dialtimeoutd, |
||||
// value taken from http.DefaultTransport
|
||||
KeepAlive: 30 * time.Second, |
||||
}).Dial, |
||||
// value taken from http.DefaultTransport
|
||||
TLSHandshakeTimeout: 10 * time.Second, |
||||
TLSClientConfig: cfg, |
||||
} |
||||
|
||||
dialer := (&net.Dialer{ |
||||
Timeout: dialtimeoutd, |
||||
KeepAlive: 30 * time.Second, |
||||
}) |
||||
dial := func(net, addr string) (net.Conn, error) { |
||||
return dialer.Dial("unix", addr) |
||||
} |
||||
|
||||
tu := &http.Transport{ |
||||
Proxy: http.ProxyFromEnvironment, |
||||
Dial: dial, |
||||
TLSHandshakeTimeout: 10 * time.Second, |
||||
TLSClientConfig: cfg, |
||||
} |
||||
ut := &unixTransport{tu} |
||||
|
||||
t.RegisterProtocol("unix", ut) |
||||
t.RegisterProtocol("unixs", ut) |
||||
|
||||
return t, nil |
||||
} |
||||
|
||||
func (urt *unixTransport) RoundTrip(req *http.Request) (*http.Response, error) { |
||||
url := *req.URL |
||||
req.URL = &url |
||||
req.URL.Scheme = strings.Replace(req.URL.Scheme, "unix", "http", 1) |
||||
return urt.Transport.RoundTrip(req) |
||||
} |
||||
@ -0,0 +1,40 @@
|
||||
// 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 transport |
||||
|
||||
import ( |
||||
"net" |
||||
"os" |
||||
) |
||||
|
||||
type unixListener struct{ net.Listener } |
||||
|
||||
func NewUnixListener(addr string) (net.Listener, error) { |
||||
if err := os.Remove(addr); err != nil && !os.IsNotExist(err) { |
||||
return nil, err |
||||
} |
||||
l, err := net.Listen("unix", addr) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return &unixListener{l}, nil |
||||
} |
||||
|
||||
func (ul *unixListener) Close() error { |
||||
if err := os.Remove(ul.Addr().String()); err != nil && !os.IsNotExist(err) { |
||||
return err |
||||
} |
||||
return ul.Listener.Close() |
||||
} |
||||
Loading…
Reference in new issue