| Serge Bazanski | c75c9d4 | 2021-04-13 16:40:14 +0200 | [diff] [blame] | 1 | // Copyright 2020 The Monogon Project Authors. |
| 2 | // |
| 3 | // SPDX-License-Identifier: Apache-2.0 |
| 4 | // |
| 5 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 6 | // you may not use this file except in compliance with the License. |
| 7 | // You may obtain a copy of the License at |
| 8 | // |
| 9 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | // |
| 11 | // Unless required by applicable law or agreed to in writing, software |
| 12 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 | // See the License for the specific language governing permissions and |
| 15 | // limitations under the License. |
| 16 | |
| 17 | // package client implements a higher-level client for consensus/etcd that is |
| 18 | // to be used within the Metropolis node code for unprivileged access (ie. |
| 19 | // access by local services that simply wish to access etcd KV without |
| 20 | // management access). |
| 21 | package client |
| 22 | |
| 23 | import ( |
| Serge Bazanski | e30d7d0 | 2021-06-23 18:50:19 +0200 | [diff] [blame] | 24 | "context" |
| Serge Bazanski | c75c9d4 | 2021-04-13 16:40:14 +0200 | [diff] [blame] | 25 | "fmt" |
| 26 | "strings" |
| 27 | |
| Lorenz Brun | d13c1c6 | 2022-03-30 19:58:58 +0200 | [diff] [blame] | 28 | clientv3 "go.etcd.io/etcd/client/v3" |
| 29 | "go.etcd.io/etcd/client/v3/namespace" |
| Serge Bazanski | c75c9d4 | 2021-04-13 16:40:14 +0200 | [diff] [blame] | 30 | ) |
| 31 | |
| 32 | // Namespaced etcd/consensus client. Each Namespaced client allows access to a |
| 33 | // subtree of the etcd key/value space, and each can emit more clients that |
| 34 | // reside in their respective subtree - effectively permitting delegated, |
| 35 | // hierarchical access to the etcd store. |
| 36 | // Note: the namespaces should not be treated as a security boundary, as it's |
| 37 | // very likely possible that compromised services could navigate upwards in the |
| 38 | // k/v space if needed. Instead, this mechanism should only be seen as |
| 39 | // containerization for the purpose of simplifying code that needs to access |
| 40 | // etcd, and especially code that needs to pass this access around to its |
| 41 | // subordinate code. |
| 42 | // This client embeds the KV, Lease and Watcher etcd client interfaces to |
| 43 | // perform the actual etcd operations, and the Sub method to create subtree |
| 44 | // clients of this client. |
| 45 | type Namespaced interface { |
| 46 | clientv3.KV |
| 47 | clientv3.Lease |
| 48 | clientv3.Watcher |
| 49 | |
| 50 | // Sub returns a child client from this client, at a sub-namespace 'space'. |
| 51 | // The given 'space' path in a series of created clients (eg. |
| 52 | // Namespace.Sub("a").Sub("b").Sub("c") are used to create an etcd k/v |
| 53 | // prefix `a:b:c/` into which K/V access is remapped. |
| 54 | Sub(space string) (Namespaced, error) |
| Serge Bazanski | b9013af | 2021-04-29 16:47:56 +0200 | [diff] [blame] | 55 | |
| Serge Bazanski | e30d7d0 | 2021-06-23 18:50:19 +0200 | [diff] [blame] | 56 | // ThinClient returns a clientv3.Client which has the same namespacing as the |
| 57 | // namespaced interface. It only implements the KV, Lease and Watcher interfaces |
| 58 | // - all other interfaces are unimplemented and will panic when called. The |
| 59 | // given context is returned by client.Ctx() and is used by some library code |
| 60 | // (eg. etcd client-go's built-in concurrency library). |
| 61 | ThinClient(ctx context.Context) *clientv3.Client |
| Serge Bazanski | b9013af | 2021-04-29 16:47:56 +0200 | [diff] [blame] | 62 | } |
| 63 | |
| 64 | // ThinClient takes a set of KV, Lease and Watcher etcd clients and turns them |
| 65 | // into a full Client struct. The rest of the interfaces (Cluster, Auth, |
| 66 | // Maintenance) will all panic when called. |
| Serge Bazanski | e30d7d0 | 2021-06-23 18:50:19 +0200 | [diff] [blame] | 67 | func ThinClient(ctx context.Context, kv clientv3.KV, lease clientv3.Lease, watcher clientv3.Watcher) *clientv3.Client { |
| 68 | cli := clientv3.NewCtxClient(ctx) |
| 69 | cli.Cluster = &unimplementedCluster{} |
| 70 | cli.KV = kv |
| 71 | cli.Lease = lease |
| 72 | cli.Watcher = watcher |
| 73 | cli.Auth = &unimplementedAuth{} |
| 74 | cli.Maintenance = &unimplementedMaintenance{} |
| 75 | return cli |
| Serge Bazanski | c75c9d4 | 2021-04-13 16:40:14 +0200 | [diff] [blame] | 76 | } |
| 77 | |
| 78 | // local implements the Namespaced client to access a locally running etc. |
| 79 | type local struct { |
| 80 | root *clientv3.Client |
| 81 | path []string |
| 82 | |
| 83 | clientv3.KV |
| 84 | clientv3.Lease |
| 85 | clientv3.Watcher |
| 86 | } |
| 87 | |
| 88 | // NewLocal returns a local Namespaced client starting at the root of the given |
| 89 | // etcd client. |
| 90 | func NewLocal(cl *clientv3.Client) Namespaced { |
| 91 | l := &local{ |
| 92 | root: cl, |
| 93 | path: nil, |
| 94 | } |
| 95 | l.populate() |
| 96 | return l |
| 97 | } |
| 98 | |
| 99 | // populate prepares the namespaced KV/Watcher/Lease clients given the current |
| 100 | // root and path of the local client. |
| 101 | func (l *local) populate() { |
| 102 | space := strings.Join(l.path, ":") + "/" |
| 103 | l.KV = namespace.NewKV(l.root, space) |
| 104 | l.Watcher = namespace.NewWatcher(l.root, space) |
| 105 | l.Lease = namespace.NewLease(l.root, space) |
| 106 | } |
| 107 | |
| 108 | func (l *local) Sub(space string) (Namespaced, error) { |
| 109 | if strings.Contains(space, ":") { |
| 110 | return nil, fmt.Errorf("sub-namespace name cannot contain ':' characters") |
| 111 | } |
| 112 | sub := &local{ |
| 113 | root: l.root, |
| 114 | path: append(l.path, space), |
| 115 | } |
| 116 | sub.populate() |
| 117 | return sub, nil |
| 118 | } |
| 119 | |
| Serge Bazanski | e30d7d0 | 2021-06-23 18:50:19 +0200 | [diff] [blame] | 120 | func (l *local) ThinClient(ctx context.Context) *clientv3.Client { |
| 121 | return ThinClient(ctx, l.KV, l.Lease, l.Watcher) |
| Serge Bazanski | b9013af | 2021-04-29 16:47:56 +0200 | [diff] [blame] | 122 | } |
| 123 | |
| Serge Bazanski | c75c9d4 | 2021-04-13 16:40:14 +0200 | [diff] [blame] | 124 | func (l *local) Close() error { |
| 125 | errW := l.Watcher.Close() |
| 126 | errL := l.Lease.Close() |
| 127 | if errW == nil && errL == nil { |
| 128 | return nil |
| 129 | } |
| 130 | if errW != nil && errL == nil { |
| 131 | return fmt.Errorf("closing watcher: %w", errW) |
| 132 | } |
| 133 | if errL != nil && errW == nil { |
| 134 | return fmt.Errorf("closing lease: %w", errL) |
| 135 | } |
| 136 | return fmt.Errorf("closing watcher: %v, closing lease: %v", errW, errL) |
| 137 | } |