blob: f8f2c7b186c22c816fddc529fac21f0a876740f5 [file] [log] [blame]
khenaidood948f772021-08-11 17:49:24 -04001// Copyright 2015 The etcd Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package client
16
17import (
18 "context"
19 "encoding/json"
20 "errors"
21 "fmt"
22 "net/http"
23 "net/url"
24 "strconv"
25 "strings"
26 "time"
27
28 "github.com/coreos/etcd/pkg/pathutil"
29)
30
31const (
32 ErrorCodeKeyNotFound = 100
33 ErrorCodeTestFailed = 101
34 ErrorCodeNotFile = 102
35 ErrorCodeNotDir = 104
36 ErrorCodeNodeExist = 105
37 ErrorCodeRootROnly = 107
38 ErrorCodeDirNotEmpty = 108
39 ErrorCodeUnauthorized = 110
40
41 ErrorCodePrevValueRequired = 201
42 ErrorCodeTTLNaN = 202
43 ErrorCodeIndexNaN = 203
44 ErrorCodeInvalidField = 209
45 ErrorCodeInvalidForm = 210
46
47 ErrorCodeRaftInternal = 300
48 ErrorCodeLeaderElect = 301
49
50 ErrorCodeWatcherCleared = 400
51 ErrorCodeEventIndexCleared = 401
52)
53
54type Error struct {
55 Code int `json:"errorCode"`
56 Message string `json:"message"`
57 Cause string `json:"cause"`
58 Index uint64 `json:"index"`
59}
60
61func (e Error) Error() string {
62 return fmt.Sprintf("%v: %v (%v) [%v]", e.Code, e.Message, e.Cause, e.Index)
63}
64
65var (
66 ErrInvalidJSON = errors.New("client: response is invalid json. The endpoint is probably not valid etcd cluster endpoint.")
67 ErrEmptyBody = errors.New("client: response body is empty")
68)
69
70// PrevExistType is used to define an existence condition when setting
71// or deleting Nodes.
72type PrevExistType string
73
74const (
75 PrevIgnore = PrevExistType("")
76 PrevExist = PrevExistType("true")
77 PrevNoExist = PrevExistType("false")
78)
79
80var (
81 defaultV2KeysPrefix = "/v2/keys"
82)
83
84// NewKeysAPI builds a KeysAPI that interacts with etcd's key-value
85// API over HTTP.
86func NewKeysAPI(c Client) KeysAPI {
87 return NewKeysAPIWithPrefix(c, defaultV2KeysPrefix)
88}
89
90// NewKeysAPIWithPrefix acts like NewKeysAPI, but allows the caller
91// to provide a custom base URL path. This should only be used in
92// very rare cases.
93func NewKeysAPIWithPrefix(c Client, p string) KeysAPI {
94 return &httpKeysAPI{
95 client: c,
96 prefix: p,
97 }
98}
99
100type KeysAPI interface {
101 // Get retrieves a set of Nodes from etcd
102 Get(ctx context.Context, key string, opts *GetOptions) (*Response, error)
103
104 // Set assigns a new value to a Node identified by a given key. The caller
105 // may define a set of conditions in the SetOptions. If SetOptions.Dir=true
106 // then value is ignored.
107 Set(ctx context.Context, key, value string, opts *SetOptions) (*Response, error)
108
109 // Delete removes a Node identified by the given key, optionally destroying
110 // all of its children as well. The caller may define a set of required
111 // conditions in an DeleteOptions object.
112 Delete(ctx context.Context, key string, opts *DeleteOptions) (*Response, error)
113
114 // Create is an alias for Set w/ PrevExist=false
115 Create(ctx context.Context, key, value string) (*Response, error)
116
117 // CreateInOrder is used to atomically create in-order keys within the given directory.
118 CreateInOrder(ctx context.Context, dir, value string, opts *CreateInOrderOptions) (*Response, error)
119
120 // Update is an alias for Set w/ PrevExist=true
121 Update(ctx context.Context, key, value string) (*Response, error)
122
123 // Watcher builds a new Watcher targeted at a specific Node identified
124 // by the given key. The Watcher may be configured at creation time
125 // through a WatcherOptions object. The returned Watcher is designed
126 // to emit events that happen to a Node, and optionally to its children.
127 Watcher(key string, opts *WatcherOptions) Watcher
128}
129
130type WatcherOptions struct {
131 // AfterIndex defines the index after-which the Watcher should
132 // start emitting events. For example, if a value of 5 is
133 // provided, the first event will have an index >= 6.
134 //
135 // Setting AfterIndex to 0 (default) means that the Watcher
136 // should start watching for events starting at the current
137 // index, whatever that may be.
138 AfterIndex uint64
139
140 // Recursive specifies whether or not the Watcher should emit
141 // events that occur in children of the given keyspace. If set
142 // to false (default), events will be limited to those that
143 // occur for the exact key.
144 Recursive bool
145}
146
147type CreateInOrderOptions struct {
148 // TTL defines a period of time after-which the Node should
149 // expire and no longer exist. Values <= 0 are ignored. Given
150 // that the zero-value is ignored, TTL cannot be used to set
151 // a TTL of 0.
152 TTL time.Duration
153}
154
155type SetOptions struct {
156 // PrevValue specifies what the current value of the Node must
157 // be in order for the Set operation to succeed.
158 //
159 // Leaving this field empty means that the caller wishes to
160 // ignore the current value of the Node. This cannot be used
161 // to compare the Node's current value to an empty string.
162 //
163 // PrevValue is ignored if Dir=true
164 PrevValue string
165
166 // PrevIndex indicates what the current ModifiedIndex of the
167 // Node must be in order for the Set operation to succeed.
168 //
169 // If PrevIndex is set to 0 (default), no comparison is made.
170 PrevIndex uint64
171
172 // PrevExist specifies whether the Node must currently exist
173 // (PrevExist) or not (PrevNoExist). If the caller does not
174 // care about existence, set PrevExist to PrevIgnore, or simply
175 // leave it unset.
176 PrevExist PrevExistType
177
178 // TTL defines a period of time after-which the Node should
179 // expire and no longer exist. Values <= 0 are ignored. Given
180 // that the zero-value is ignored, TTL cannot be used to set
181 // a TTL of 0.
182 TTL time.Duration
183
184 // Refresh set to true means a TTL value can be updated
185 // without firing a watch or changing the node value. A
186 // value must not be provided when refreshing a key.
187 Refresh bool
188
189 // Dir specifies whether or not this Node should be created as a directory.
190 Dir bool
191
192 // NoValueOnSuccess specifies whether the response contains the current value of the Node.
193 // If set, the response will only contain the current value when the request fails.
194 NoValueOnSuccess bool
195}
196
197type GetOptions struct {
198 // Recursive defines whether or not all children of the Node
199 // should be returned.
200 Recursive bool
201
202 // Sort instructs the server whether or not to sort the Nodes.
203 // If true, the Nodes are sorted alphabetically by key in
204 // ascending order (A to z). If false (default), the Nodes will
205 // not be sorted and the ordering used should not be considered
206 // predictable.
207 Sort bool
208
209 // Quorum specifies whether it gets the latest committed value that
210 // has been applied in quorum of members, which ensures external
211 // consistency (or linearizability).
212 Quorum bool
213}
214
215type DeleteOptions struct {
216 // PrevValue specifies what the current value of the Node must
217 // be in order for the Delete operation to succeed.
218 //
219 // Leaving this field empty means that the caller wishes to
220 // ignore the current value of the Node. This cannot be used
221 // to compare the Node's current value to an empty string.
222 PrevValue string
223
224 // PrevIndex indicates what the current ModifiedIndex of the
225 // Node must be in order for the Delete operation to succeed.
226 //
227 // If PrevIndex is set to 0 (default), no comparison is made.
228 PrevIndex uint64
229
230 // Recursive defines whether or not all children of the Node
231 // should be deleted. If set to true, all children of the Node
232 // identified by the given key will be deleted. If left unset
233 // or explicitly set to false, only a single Node will be
234 // deleted.
235 Recursive bool
236
237 // Dir specifies whether or not this Node should be removed as a directory.
238 Dir bool
239}
240
241type Watcher interface {
242 // Next blocks until an etcd event occurs, then returns a Response
243 // representing that event. The behavior of Next depends on the
244 // WatcherOptions used to construct the Watcher. Next is designed to
245 // be called repeatedly, each time blocking until a subsequent event
246 // is available.
247 //
248 // If the provided context is cancelled, Next will return a non-nil
249 // error. Any other failures encountered while waiting for the next
250 // event (connection issues, deserialization failures, etc) will
251 // also result in a non-nil error.
252 Next(context.Context) (*Response, error)
253}
254
255type Response struct {
256 // Action is the name of the operation that occurred. Possible values
257 // include get, set, delete, update, create, compareAndSwap,
258 // compareAndDelete and expire.
259 Action string `json:"action"`
260
261 // Node represents the state of the relevant etcd Node.
262 Node *Node `json:"node"`
263
264 // PrevNode represents the previous state of the Node. PrevNode is non-nil
265 // only if the Node existed before the action occurred and the action
266 // caused a change to the Node.
267 PrevNode *Node `json:"prevNode"`
268
269 // Index holds the cluster-level index at the time the Response was generated.
270 // This index is not tied to the Node(s) contained in this Response.
271 Index uint64 `json:"-"`
272
273 // ClusterID holds the cluster-level ID reported by the server. This
274 // should be different for different etcd clusters.
275 ClusterID string `json:"-"`
276}
277
278type Node struct {
279 // Key represents the unique location of this Node (e.g. "/foo/bar").
280 Key string `json:"key"`
281
282 // Dir reports whether node describes a directory.
283 Dir bool `json:"dir,omitempty"`
284
285 // Value is the current data stored on this Node. If this Node
286 // is a directory, Value will be empty.
287 Value string `json:"value"`
288
289 // Nodes holds the children of this Node, only if this Node is a directory.
290 // This slice of will be arbitrarily deep (children, grandchildren, great-
291 // grandchildren, etc.) if a recursive Get or Watch request were made.
292 Nodes Nodes `json:"nodes"`
293
294 // CreatedIndex is the etcd index at-which this Node was created.
295 CreatedIndex uint64 `json:"createdIndex"`
296
297 // ModifiedIndex is the etcd index at-which this Node was last modified.
298 ModifiedIndex uint64 `json:"modifiedIndex"`
299
300 // Expiration is the server side expiration time of the key.
301 Expiration *time.Time `json:"expiration,omitempty"`
302
303 // TTL is the time to live of the key in second.
304 TTL int64 `json:"ttl,omitempty"`
305}
306
307func (n *Node) String() string {
308 return fmt.Sprintf("{Key: %s, CreatedIndex: %d, ModifiedIndex: %d, TTL: %d}", n.Key, n.CreatedIndex, n.ModifiedIndex, n.TTL)
309}
310
311// TTLDuration returns the Node's TTL as a time.Duration object
312func (n *Node) TTLDuration() time.Duration {
313 return time.Duration(n.TTL) * time.Second
314}
315
316type Nodes []*Node
317
318// interfaces for sorting
319
320func (ns Nodes) Len() int { return len(ns) }
321func (ns Nodes) Less(i, j int) bool { return ns[i].Key < ns[j].Key }
322func (ns Nodes) Swap(i, j int) { ns[i], ns[j] = ns[j], ns[i] }
323
324type httpKeysAPI struct {
325 client httpClient
326 prefix string
327}
328
329func (k *httpKeysAPI) Set(ctx context.Context, key, val string, opts *SetOptions) (*Response, error) {
330 act := &setAction{
331 Prefix: k.prefix,
332 Key: key,
333 Value: val,
334 }
335
336 if opts != nil {
337 act.PrevValue = opts.PrevValue
338 act.PrevIndex = opts.PrevIndex
339 act.PrevExist = opts.PrevExist
340 act.TTL = opts.TTL
341 act.Refresh = opts.Refresh
342 act.Dir = opts.Dir
343 act.NoValueOnSuccess = opts.NoValueOnSuccess
344 }
345
346 doCtx := ctx
347 if act.PrevExist == PrevNoExist {
348 doCtx = context.WithValue(doCtx, &oneShotCtxValue, &oneShotCtxValue)
349 }
350 resp, body, err := k.client.Do(doCtx, act)
351 if err != nil {
352 return nil, err
353 }
354
355 return unmarshalHTTPResponse(resp.StatusCode, resp.Header, body)
356}
357
358func (k *httpKeysAPI) Create(ctx context.Context, key, val string) (*Response, error) {
359 return k.Set(ctx, key, val, &SetOptions{PrevExist: PrevNoExist})
360}
361
362func (k *httpKeysAPI) CreateInOrder(ctx context.Context, dir, val string, opts *CreateInOrderOptions) (*Response, error) {
363 act := &createInOrderAction{
364 Prefix: k.prefix,
365 Dir: dir,
366 Value: val,
367 }
368
369 if opts != nil {
370 act.TTL = opts.TTL
371 }
372
373 resp, body, err := k.client.Do(ctx, act)
374 if err != nil {
375 return nil, err
376 }
377
378 return unmarshalHTTPResponse(resp.StatusCode, resp.Header, body)
379}
380
381func (k *httpKeysAPI) Update(ctx context.Context, key, val string) (*Response, error) {
382 return k.Set(ctx, key, val, &SetOptions{PrevExist: PrevExist})
383}
384
385func (k *httpKeysAPI) Delete(ctx context.Context, key string, opts *DeleteOptions) (*Response, error) {
386 act := &deleteAction{
387 Prefix: k.prefix,
388 Key: key,
389 }
390
391 if opts != nil {
392 act.PrevValue = opts.PrevValue
393 act.PrevIndex = opts.PrevIndex
394 act.Dir = opts.Dir
395 act.Recursive = opts.Recursive
396 }
397
398 doCtx := context.WithValue(ctx, &oneShotCtxValue, &oneShotCtxValue)
399 resp, body, err := k.client.Do(doCtx, act)
400 if err != nil {
401 return nil, err
402 }
403
404 return unmarshalHTTPResponse(resp.StatusCode, resp.Header, body)
405}
406
407func (k *httpKeysAPI) Get(ctx context.Context, key string, opts *GetOptions) (*Response, error) {
408 act := &getAction{
409 Prefix: k.prefix,
410 Key: key,
411 }
412
413 if opts != nil {
414 act.Recursive = opts.Recursive
415 act.Sorted = opts.Sort
416 act.Quorum = opts.Quorum
417 }
418
419 resp, body, err := k.client.Do(ctx, act)
420 if err != nil {
421 return nil, err
422 }
423
424 return unmarshalHTTPResponse(resp.StatusCode, resp.Header, body)
425}
426
427func (k *httpKeysAPI) Watcher(key string, opts *WatcherOptions) Watcher {
428 act := waitAction{
429 Prefix: k.prefix,
430 Key: key,
431 }
432
433 if opts != nil {
434 act.Recursive = opts.Recursive
435 if opts.AfterIndex > 0 {
436 act.WaitIndex = opts.AfterIndex + 1
437 }
438 }
439
440 return &httpWatcher{
441 client: k.client,
442 nextWait: act,
443 }
444}
445
446type httpWatcher struct {
447 client httpClient
448 nextWait waitAction
449}
450
451func (hw *httpWatcher) Next(ctx context.Context) (*Response, error) {
452 for {
453 httpresp, body, err := hw.client.Do(ctx, &hw.nextWait)
454 if err != nil {
455 return nil, err
456 }
457
458 resp, err := unmarshalHTTPResponse(httpresp.StatusCode, httpresp.Header, body)
459 if err != nil {
460 if err == ErrEmptyBody {
461 continue
462 }
463 return nil, err
464 }
465
466 hw.nextWait.WaitIndex = resp.Node.ModifiedIndex + 1
467 return resp, nil
468 }
469}
470
471// v2KeysURL forms a URL representing the location of a key.
472// The endpoint argument represents the base URL of an etcd
473// server. The prefix is the path needed to route from the
474// provided endpoint's path to the root of the keys API
475// (typically "/v2/keys").
476func v2KeysURL(ep url.URL, prefix, key string) *url.URL {
477 // We concatenate all parts together manually. We cannot use
478 // path.Join because it does not reserve trailing slash.
479 // We call CanonicalURLPath to further cleanup the path.
480 if prefix != "" && prefix[0] != '/' {
481 prefix = "/" + prefix
482 }
483 if key != "" && key[0] != '/' {
484 key = "/" + key
485 }
486 ep.Path = pathutil.CanonicalURLPath(ep.Path + prefix + key)
487 return &ep
488}
489
490type getAction struct {
491 Prefix string
492 Key string
493 Recursive bool
494 Sorted bool
495 Quorum bool
496}
497
498func (g *getAction) HTTPRequest(ep url.URL) *http.Request {
499 u := v2KeysURL(ep, g.Prefix, g.Key)
500
501 params := u.Query()
502 params.Set("recursive", strconv.FormatBool(g.Recursive))
503 params.Set("sorted", strconv.FormatBool(g.Sorted))
504 params.Set("quorum", strconv.FormatBool(g.Quorum))
505 u.RawQuery = params.Encode()
506
507 req, _ := http.NewRequest("GET", u.String(), nil)
508 return req
509}
510
511type waitAction struct {
512 Prefix string
513 Key string
514 WaitIndex uint64
515 Recursive bool
516}
517
518func (w *waitAction) HTTPRequest(ep url.URL) *http.Request {
519 u := v2KeysURL(ep, w.Prefix, w.Key)
520
521 params := u.Query()
522 params.Set("wait", "true")
523 params.Set("waitIndex", strconv.FormatUint(w.WaitIndex, 10))
524 params.Set("recursive", strconv.FormatBool(w.Recursive))
525 u.RawQuery = params.Encode()
526
527 req, _ := http.NewRequest("GET", u.String(), nil)
528 return req
529}
530
531type setAction struct {
532 Prefix string
533 Key string
534 Value string
535 PrevValue string
536 PrevIndex uint64
537 PrevExist PrevExistType
538 TTL time.Duration
539 Refresh bool
540 Dir bool
541 NoValueOnSuccess bool
542}
543
544func (a *setAction) HTTPRequest(ep url.URL) *http.Request {
545 u := v2KeysURL(ep, a.Prefix, a.Key)
546
547 params := u.Query()
548 form := url.Values{}
549
550 // we're either creating a directory or setting a key
551 if a.Dir {
552 params.Set("dir", strconv.FormatBool(a.Dir))
553 } else {
554 // These options are only valid for setting a key
555 if a.PrevValue != "" {
556 params.Set("prevValue", a.PrevValue)
557 }
558 form.Add("value", a.Value)
559 }
560
561 // Options which apply to both setting a key and creating a dir
562 if a.PrevIndex != 0 {
563 params.Set("prevIndex", strconv.FormatUint(a.PrevIndex, 10))
564 }
565 if a.PrevExist != PrevIgnore {
566 params.Set("prevExist", string(a.PrevExist))
567 }
568 if a.TTL > 0 {
569 form.Add("ttl", strconv.FormatUint(uint64(a.TTL.Seconds()), 10))
570 }
571
572 if a.Refresh {
573 form.Add("refresh", "true")
574 }
575 if a.NoValueOnSuccess {
576 params.Set("noValueOnSuccess", strconv.FormatBool(a.NoValueOnSuccess))
577 }
578
579 u.RawQuery = params.Encode()
580 body := strings.NewReader(form.Encode())
581
582 req, _ := http.NewRequest("PUT", u.String(), body)
583 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
584
585 return req
586}
587
588type deleteAction struct {
589 Prefix string
590 Key string
591 PrevValue string
592 PrevIndex uint64
593 Dir bool
594 Recursive bool
595}
596
597func (a *deleteAction) HTTPRequest(ep url.URL) *http.Request {
598 u := v2KeysURL(ep, a.Prefix, a.Key)
599
600 params := u.Query()
601 if a.PrevValue != "" {
602 params.Set("prevValue", a.PrevValue)
603 }
604 if a.PrevIndex != 0 {
605 params.Set("prevIndex", strconv.FormatUint(a.PrevIndex, 10))
606 }
607 if a.Dir {
608 params.Set("dir", "true")
609 }
610 if a.Recursive {
611 params.Set("recursive", "true")
612 }
613 u.RawQuery = params.Encode()
614
615 req, _ := http.NewRequest("DELETE", u.String(), nil)
616 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
617
618 return req
619}
620
621type createInOrderAction struct {
622 Prefix string
623 Dir string
624 Value string
625 TTL time.Duration
626}
627
628func (a *createInOrderAction) HTTPRequest(ep url.URL) *http.Request {
629 u := v2KeysURL(ep, a.Prefix, a.Dir)
630
631 form := url.Values{}
632 form.Add("value", a.Value)
633 if a.TTL > 0 {
634 form.Add("ttl", strconv.FormatUint(uint64(a.TTL.Seconds()), 10))
635 }
636 body := strings.NewReader(form.Encode())
637
638 req, _ := http.NewRequest("POST", u.String(), body)
639 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
640 return req
641}
642
643func unmarshalHTTPResponse(code int, header http.Header, body []byte) (res *Response, err error) {
644 switch code {
645 case http.StatusOK, http.StatusCreated:
646 if len(body) == 0 {
647 return nil, ErrEmptyBody
648 }
649 res, err = unmarshalSuccessfulKeysResponse(header, body)
650 default:
651 err = unmarshalFailedKeysResponse(body)
652 }
653 return res, err
654}
655
656var jsonIterator = caseSensitiveJsonIterator()
657
658func unmarshalSuccessfulKeysResponse(header http.Header, body []byte) (*Response, error) {
659 var res Response
660 err := jsonIterator.Unmarshal(body, &res)
661 if err != nil {
662 return nil, ErrInvalidJSON
663 }
664 if header.Get("X-Etcd-Index") != "" {
665 res.Index, err = strconv.ParseUint(header.Get("X-Etcd-Index"), 10, 64)
666 if err != nil {
667 return nil, err
668 }
669 }
670 res.ClusterID = header.Get("X-Etcd-Cluster-ID")
671 return &res, nil
672}
673
674func unmarshalFailedKeysResponse(body []byte) error {
675 var etcdErr Error
676 if err := json.Unmarshal(body, &etcdErr); err != nil {
677 return ErrInvalidJSON
678 }
679 return etcdErr
680}