m/n/c/consensus: add client
This implementes a thin wrapper around etcd's clientv3.Client, with the
following advantages:
- Only implements KV, Watcher and Lease interfaces, ie. unprivileged
namespaceable interfaces - not cluster management interfaces. These
will be available to both remote and local etcd connections.
- Adds recursive namespacing functionality, which permits different
parts of the subsystem to receive their own somewhat-sandboxed etcd
subtree. This not only makes the etcd keyspace layout more strict,
but also simplifies passing around etcd clients, as major components
(like the kubernetes subsystem) can hand out its own sub-clients,
instead of them having to be globally declared ahead of time.
Test Plan: Exercised by tests.
X-Origin-Diff: phab/D756
GitOrigin-RevId: 03fead9a89c301a2e70df8a007b7ecb60b2364c7
diff --git a/metropolis/node/core/consensus/client/BUILD.bazel b/metropolis/node/core/consensus/client/BUILD.bazel
new file mode 100644
index 0000000..35dd097
--- /dev/null
+++ b/metropolis/node/core/consensus/client/BUILD.bazel
@@ -0,0 +1,12 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "go_default_library",
+ srcs = ["client.go"],
+ importpath = "source.monogon.dev/metropolis/node/core/consensus/client",
+ visibility = ["//visibility:public"],
+ deps = [
+ "@io_etcd_go_etcd//clientv3:go_default_library",
+ "@io_etcd_go_etcd//clientv3/namespace:go_default_library",
+ ],
+)
diff --git a/metropolis/node/core/consensus/client/client.go b/metropolis/node/core/consensus/client/client.go
new file mode 100644
index 0000000..168f445
--- /dev/null
+++ b/metropolis/node/core/consensus/client/client.go
@@ -0,0 +1,111 @@
+// 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 (
+ "fmt"
+ "strings"
+
+ "go.etcd.io/etcd/clientv3"
+ "go.etcd.io/etcd/clientv3/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)
+}
+
+// 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) 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)
+}