blob: 821919b7d94d9a98935d763c40f808b2926574ac [file] [log] [blame]
// Copyright 2020 The Monogon Project Authors.
//
// SPDX-License-Identifier: Apache-2.0
//
// 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 client implements a higher-level client for consensus/etcd that is
// to be used within the Metropolis node code for unprivileged access (ie.
// access by local services that simply wish to access etcd KV without
// management access).
package client
import (
"context"
"fmt"
"strings"
clientv3 "go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/client/v3/namespace"
)
// Namespaced etcd/consensus client. Each Namespaced client allows access to a
// subtree of the etcd key/value space, and each can emit more clients that
// reside in their respective subtree - effectively permitting delegated,
// hierarchical access to the etcd store.
// Note: the namespaces should not be treated as a security boundary, as it's
// very likely possible that compromised services could navigate upwards in the
// k/v space if needed. Instead, this mechanism should only be seen as
// containerization for the purpose of simplifying code that needs to access
// etcd, and especially code that needs to pass this access around to its
// subordinate code.
// This client embeds the KV, Lease and Watcher etcd client interfaces to
// perform the actual etcd operations, and the Sub method to create subtree
// clients of this client.
type Namespaced interface {
clientv3.KV
clientv3.Lease
clientv3.Watcher
// Sub returns a child client from this client, at a sub-namespace 'space'.
// The given 'space' path in a series of created clients (eg.
// Namespace.Sub("a").Sub("b").Sub("c") are used to create an etcd k/v
// prefix `a:b:c/` into which K/V access is remapped.
Sub(space string) (Namespaced, error)
// ThinClient returns a clientv3.Client which has the same namespacing as the
// namespaced interface. It only implements the KV, Lease and Watcher interfaces
// - all other interfaces are unimplemented and will panic when called. The
// given context is returned by client.Ctx() and is used by some library code
// (eg. etcd client-go's built-in concurrency library).
ThinClient(ctx context.Context) *clientv3.Client
}
// ThinClient takes a set of KV, Lease and Watcher etcd clients and turns them
// into a full Client struct. The rest of the interfaces (Cluster, Auth,
// Maintenance) will all panic when called.
func ThinClient(ctx context.Context, kv clientv3.KV, lease clientv3.Lease, watcher clientv3.Watcher) *clientv3.Client {
cli := clientv3.NewCtxClient(ctx)
cli.Cluster = &unimplementedCluster{}
cli.KV = kv
cli.Lease = lease
cli.Watcher = watcher
cli.Auth = &unimplementedAuth{}
cli.Maintenance = &unimplementedMaintenance{}
return cli
}
// local implements the Namespaced client to access a locally running etc.
type local struct {
root *clientv3.Client
path []string
clientv3.KV
clientv3.Lease
clientv3.Watcher
}
// NewLocal returns a local Namespaced client starting at the root of the given
// etcd client.
func NewLocal(cl *clientv3.Client) Namespaced {
l := &local{
root: cl,
path: nil,
}
l.populate()
return l
}
// populate prepares the namespaced KV/Watcher/Lease clients given the current
// root and path of the local client.
func (l *local) populate() {
space := strings.Join(l.path, ":") + "/"
l.KV = namespace.NewKV(l.root, space)
l.Watcher = namespace.NewWatcher(l.root, space)
l.Lease = namespace.NewLease(l.root, space)
}
func (l *local) Sub(space string) (Namespaced, error) {
if strings.Contains(space, ":") {
return nil, fmt.Errorf("sub-namespace name cannot contain ':' characters")
}
sub := &local{
root: l.root,
path: append(l.path, space),
}
sub.populate()
return sub, nil
}
func (l *local) ThinClient(ctx context.Context) *clientv3.Client {
return ThinClient(ctx, l.KV, l.Lease, l.Watcher)
}
func (l *local) Close() error {
errW := l.Watcher.Close()
errL := l.Lease.Close()
if errW == nil && errL == nil {
return nil
}
if errW != nil && errL == nil {
return fmt.Errorf("closing watcher: %w", errW)
}
if errL != nil && errW == nil {
return fmt.Errorf("closing lease: %w", errL)
}
return fmt.Errorf("closing watcher: %v, closing lease: %v", errW, errL)
}