blob: a38da8bc4ea68d743ebf328c20e08234b9cdc07c [file] [log] [blame]
Serge Bazanski1ebd1e12020-07-13 19:17:16 +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
17package cluster
18
19import (
20 "context"
Serge Bazanski1ebd1e12020-07-13 19:17:16 +020021 "encoding/hex"
22 "fmt"
23 "net"
Serge Bazanski42e61c62021-03-18 15:07:18 +010024 "strings"
Serge Bazanski1ebd1e12020-07-13 19:17:16 +020025
Serge Bazanski1ebd1e12020-07-13 19:17:16 +020026 "go.etcd.io/etcd/clientv3"
27 "golang.org/x/sys/unix"
Serge Bazanski42e61c62021-03-18 15:07:18 +010028 "google.golang.org/protobuf/proto"
Serge Bazanski1ebd1e12020-07-13 19:17:16 +020029
Serge Bazanski31370b02021-01-07 16:31:14 +010030 "source.monogon.dev/metropolis/node/core/localstorage"
Serge Bazanski42e61c62021-03-18 15:07:18 +010031 "source.monogon.dev/metropolis/pkg/supervisor"
32 ppb "source.monogon.dev/metropolis/proto/private"
Serge Bazanski1ebd1e12020-07-13 19:17:16 +020033)
34
Serge Bazanski42e61c62021-03-18 15:07:18 +010035// Node is a Metropolis cluster member. A node is a virtual or physical machine
36// running Metropolis. This object represents a node only as part of a cluster
37// - ie., this object will never be available outside of
38// //metropolis/node/core/cluster if the Node is not part of a Cluster. Nodes
39// are inherently tied to their long term storage, which is etcd. As such,
40// methods on this object relate heavily to the Node's expected lifecycle on
41// etcd.
Serge Bazanski1ebd1e12020-07-13 19:17:16 +020042type Node struct {
Serge Bazanski42e61c62021-03-18 15:07:18 +010043 // clusterUnlockKey is half of the unlock key required to mount the node's
44 // data partition. It's stored in etcd, and will only be provided to the
45 // Node if it can prove its identity via an integrity mechanism (ie. via
46 // TPM), or when the Node was just created (as the key is generated locally
47 // by localstorage on first format/mount). The other part of the unlock
48 // key is the LocalUnlockKey that's present on the node's ESP partition.
Serge Bazanski1ebd1e12020-07-13 19:17:16 +020049 clusterUnlockKey []byte
Serge Bazanski1ebd1e12020-07-13 19:17:16 +020050
Serge Bazanski42e61c62021-03-18 15:07:18 +010051 pubkey []byte
Serge Bazanski1ebd1e12020-07-13 19:17:16 +020052
Serge Bazanski42e61c62021-03-18 15:07:18 +010053 state ppb.Node_FSMState
54
55 // A Node can have multiple Roles. Each Role is represented by the presence
56 // of NodeRole* structures in this structure, with a nil pointer
57 // representing the lack of a role.
Serge Bazanski1ebd1e12020-07-13 19:17:16 +020058 consensusMember *NodeRoleConsensusMember
59 kubernetesWorker *NodeRoleKubernetesWorker
Serge Bazanski42e61c62021-03-18 15:07:18 +010060
61 // At runtime, this represents an etcd client to the consensus cluster. This
62 // is used by applications (like Kubernetes).
63 KV clientv3.KV
Serge Bazanski1ebd1e12020-07-13 19:17:16 +020064}
65
Serge Bazanski42e61c62021-03-18 15:07:18 +010066// NodeRoleConsensusMember defines that the Node is a consensus (etcd) cluster
67// member.
Serge Bazanski1ebd1e12020-07-13 19:17:16 +020068type NodeRoleConsensusMember struct {
Serge Bazanski1ebd1e12020-07-13 19:17:16 +020069}
70
Serge Bazanski42e61c62021-03-18 15:07:18 +010071// NodeRoleKubernetesWorker defines that the Node should be running the
72// Kubernetes control and data plane.
Serge Bazanski1ebd1e12020-07-13 19:17:16 +020073type NodeRoleKubernetesWorker struct {
Serge Bazanski1ebd1e12020-07-13 19:17:16 +020074}
75
Serge Bazanski42e61c62021-03-18 15:07:18 +010076// ID returns the name of this node, which is `metropolis-{pubkeyHash}`. This
77// name should be the primary way to refer to Metropoils nodes within a
78// cluster, and is guaranteed to be unique by relying on cryptographic
79// randomness.
Serge Bazanski1ebd1e12020-07-13 19:17:16 +020080func (n *Node) ID() string {
Serge Bazanski662b5b32020-12-21 13:49:00 +010081 return fmt.Sprintf("metropolis-%s", n.IDBare())
Serge Bazanski1ebd1e12020-07-13 19:17:16 +020082}
83
84// IDBare returns the `{pubkeyHash}` part of the node ID.
85func (n Node) IDBare() string {
Serge Bazanski42e61c62021-03-18 15:07:18 +010086 return hex.EncodeToString(n.pubkey[:16])
Serge Bazanski1ebd1e12020-07-13 19:17:16 +020087}
88
89func (n *Node) String() string {
90 return n.ID()
91}
92
Serge Bazanski42e61c62021-03-18 15:07:18 +010093// ConsensusMember returns a copy of the NodeRoleConsensusMember struct if the
94// Node is a consensus member, otherwise nil.
Serge Bazanski1ebd1e12020-07-13 19:17:16 +020095func (n *Node) ConsensusMember() *NodeRoleConsensusMember {
96 if n.consensusMember == nil {
97 return nil
98 }
99 cm := *n.consensusMember
100 return &cm
101}
102
Serge Bazanski42e61c62021-03-18 15:07:18 +0100103// KubernetesWorker returns a copy of the NodeRoleKubernetesWorker struct if
104// the Node is a kubernetes worker, otherwise nil.
Serge Bazanski1ebd1e12020-07-13 19:17:16 +0200105func (n *Node) KubernetesWorker() *NodeRoleKubernetesWorker {
106 if n.kubernetesWorker == nil {
107 return nil
108 }
109 kw := *n.kubernetesWorker
110 return &kw
111}
112
Serge Bazanski42e61c62021-03-18 15:07:18 +0100113// etcdPath builds the etcd path in which this node's protobuf-serialized state
114// is stored in etcd.
Serge Bazanski1ebd1e12020-07-13 19:17:16 +0200115func (n *Node) etcdPath() string {
116 return fmt.Sprintf("/nodes/%s", n.ID())
117}
118
Serge Bazanski42e61c62021-03-18 15:07:18 +0100119// proto serializes the Node object into protobuf, to be used for saving to
120// etcd.
121func (n *Node) proto() *ppb.Node {
122 msg := &ppb.Node{
Serge Bazanski1ebd1e12020-07-13 19:17:16 +0200123 ClusterUnlockKey: n.clusterUnlockKey,
Serge Bazanski42e61c62021-03-18 15:07:18 +0100124 PublicKey: n.pubkey,
125 FsmState: n.state,
126 Roles: &ppb.Node_Roles{},
Serge Bazanski1ebd1e12020-07-13 19:17:16 +0200127 }
128 if n.consensusMember != nil {
Serge Bazanski42e61c62021-03-18 15:07:18 +0100129 msg.Roles.ConsensusMember = &ppb.Node_Roles_ConsensusMember{}
Serge Bazanski1ebd1e12020-07-13 19:17:16 +0200130 }
131 if n.kubernetesWorker != nil {
Serge Bazanski42e61c62021-03-18 15:07:18 +0100132 msg.Roles.KubernetesWorker = &ppb.Node_Roles_KubernetesWorker{}
Serge Bazanski1ebd1e12020-07-13 19:17:16 +0200133 }
134 return msg
135}
136
Serge Bazanski42e61c62021-03-18 15:07:18 +0100137// Store saves the Node into etcd. This should be called only once per Node
138// (ie. when the Node has been created).
Serge Bazanski1ebd1e12020-07-13 19:17:16 +0200139func (n *Node) Store(ctx context.Context, kv clientv3.KV) error {
Serge Bazanski42e61c62021-03-18 15:07:18 +0100140 // Currently the only flow to store a node to etcd is a write-once flow:
141 // once a node is created, it cannot be deleted or updated. In the future,
142 // flows to change cluster node roles might be introduced (ie. to promote
143 // nodes to consensus members, etc).
Serge Bazanski1ebd1e12020-07-13 19:17:16 +0200144 key := n.etcdPath()
145 msg := n.proto()
146 nodeRaw, err := proto.Marshal(msg)
147 if err != nil {
148 return fmt.Errorf("failed to marshal node: %w", err)
149 }
150
151 res, err := kv.Txn(ctx).If(
152 clientv3.Compare(clientv3.CreateRevision(key), "=", 0),
153 ).Then(
154 clientv3.OpPut(key, string(nodeRaw)),
155 ).Commit()
156 if err != nil {
157 return fmt.Errorf("failed to store node: %w", err)
158 }
159
160 if !res.Succeeded {
161 return fmt.Errorf("attempted to re-register node (unsupported flow)")
162 }
163 return nil
164}
165
Serge Bazanski42e61c62021-03-18 15:07:18 +0100166// MakeConsensusMember turns the node into a consensus member. This only
167// configures internal fields, and does not actually start any services.
168func (n *Node) MakeConsensusMember() error {
Serge Bazanski1ebd1e12020-07-13 19:17:16 +0200169 if n.consensusMember != nil {
170 return fmt.Errorf("node already is consensus member")
171 }
Serge Bazanski42e61c62021-03-18 15:07:18 +0100172 n.consensusMember = &NodeRoleConsensusMember{}
Serge Bazanski1ebd1e12020-07-13 19:17:16 +0200173 return nil
174}
175
Serge Bazanski42e61c62021-03-18 15:07:18 +0100176// MakeKubernetesWorker turns the node into a kubernetes worker. This only
177// configures internal fields, and does not actually start any services.
178func (n *Node) MakeKubernetesWorker() error {
Serge Bazanski1ebd1e12020-07-13 19:17:16 +0200179 if n.kubernetesWorker != nil {
180 return fmt.Errorf("node is already kubernetes worker")
181 }
Serge Bazanski42e61c62021-03-18 15:07:18 +0100182 n.kubernetesWorker = &NodeRoleKubernetesWorker{}
Serge Bazanski1ebd1e12020-07-13 19:17:16 +0200183 return nil
184}
185
Serge Bazanski42e61c62021-03-18 15:07:18 +0100186// ConfigureLocalHostname uses the node's ID as a hostname, and sets the
187// current hostname, and local files like hosts and machine-id accordingly.
188func (n *Node) ConfigureLocalHostname(ctx context.Context, ephemeral *localstorage.EphemeralDirectory, address net.IP) error {
Serge Bazanski1ebd1e12020-07-13 19:17:16 +0200189 if err := unix.Sethostname([]byte(n.ID())); err != nil {
190 return fmt.Errorf("failed to set runtime hostname: %w", err)
191 }
Serge Bazanski42e61c62021-03-18 15:07:18 +0100192 hosts := []string{
193 "127.0.0.1 localhost",
194 "::1 localhost",
195 fmt.Sprintf("%s %s", address.String(), n.ID()),
Serge Bazanski1ebd1e12020-07-13 19:17:16 +0200196 }
Serge Bazanski42e61c62021-03-18 15:07:18 +0100197 if err := ephemeral.Hosts.Write([]byte(strings.Join(hosts, "\n")), 0644); err != nil {
198 return fmt.Errorf("failed to write /ephemeral/hosts: %w", err)
Serge Bazanski1ebd1e12020-07-13 19:17:16 +0200199 }
Serge Bazanski42e61c62021-03-18 15:07:18 +0100200 if err := ephemeral.MachineID.Write([]byte(n.IDBare()), 0644); err != nil {
201 return fmt.Errorf("failed to write /ephemeral/machine-id: %w", err)
202 }
203
204 // Check that we are self-resolvable.
205 ip, err := net.ResolveIPAddr("ip", n.ID())
206 if err != nil {
207 return fmt.Errorf("failed to self-resolve: %w", err)
208 }
209 supervisor.Logger(ctx).Infof("This is node %s at %v", n.ID(), ip)
Serge Bazanski1ebd1e12020-07-13 19:17:16 +0200210 return nil
211}