blob: 821919b7d94d9a98935d763c40f808b2926574ac [file] [log] [blame]
Serge Bazanskic75c9d42021-04-13 16:40:14 +02001// 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).
21package client
22
23import (
Serge Bazanskie30d7d02021-06-23 18:50:19 +020024 "context"
Serge Bazanskic75c9d42021-04-13 16:40:14 +020025 "fmt"
26 "strings"
27
Lorenz Brund13c1c62022-03-30 19:58:58 +020028 clientv3 "go.etcd.io/etcd/client/v3"
29 "go.etcd.io/etcd/client/v3/namespace"
Serge Bazanskic75c9d42021-04-13 16:40:14 +020030)
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.
45type 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 Bazanskib9013af2021-04-29 16:47:56 +020055
Serge Bazanskie30d7d02021-06-23 18:50:19 +020056 // 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 Bazanskib9013af2021-04-29 16:47:56 +020062}
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 Bazanskie30d7d02021-06-23 18:50:19 +020067func 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 Bazanskic75c9d42021-04-13 16:40:14 +020076}
77
78// local implements the Namespaced client to access a locally running etc.
79type 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.
90func 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.
101func (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
108func (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 Bazanskie30d7d02021-06-23 18:50:19 +0200120func (l *local) ThinClient(ctx context.Context) *clientv3.Client {
121 return ThinClient(ctx, l.KV, l.Lease, l.Watcher)
Serge Bazanskib9013af2021-04-29 16:47:56 +0200122}
123
Serge Bazanskic75c9d42021-04-13 16:40:14 +0200124func (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}