blob: 8e7400004f4e017596b668f78d6fe4ae475b266d [file] [log] [blame]
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +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
Serge Bazanski216fe7b2021-05-21 18:36:16 +020017// Package consensus implements a managed etcd cluster member service, with a
18// self-hosted CA system for issuing peer certificates. Currently each
19// Metropolis node runs an etcd member, and connects to the etcd member locally
20// over a domain socket.
Serge Bazanskicb883e22020-07-06 17:47:55 +020021//
22// The service supports two modes of startup:
Serge Bazanski216fe7b2021-05-21 18:36:16 +020023// - initializing a new cluster, by bootstrapping the CA in memory, starting a
24// cluster, committing the CA to etcd afterwards, and saving the new node's
25// certificate to local storage
26// - joining an existing cluster, using certificates from local storage and
27// loading the CA from etcd. This flow is also used when the node joins a
28// cluster for the first time (then the certificates required must be
29// provisioned externally before starting the consensus service).
Serge Bazanskicb883e22020-07-06 17:47:55 +020030//
Serge Bazanski216fe7b2021-05-21 18:36:16 +020031// Regardless of how the etcd member service was started, the resulting running
32// service is further managed and used in the same way.
Serge Bazanskicb883e22020-07-06 17:47:55 +020033//
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +020034package consensus
35
36import (
37 "context"
Lorenz Bruna4ea9d02019-10-31 11:40:30 +010038 "encoding/pem"
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +020039 "fmt"
Lorenz Bruna4ea9d02019-10-31 11:40:30 +010040 "net/url"
Serge Bazanskicb883e22020-07-06 17:47:55 +020041 "sync"
Lorenz Bruna4ea9d02019-10-31 11:40:30 +010042 "time"
43
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +020044 "go.etcd.io/etcd/clientv3"
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +020045 "go.etcd.io/etcd/embed"
Lorenz Brunfc5dbc62020-05-28 12:18:07 +020046 "go.uber.org/atomic"
Hendrik Hofstadt8efe51e2020-02-28 12:53:41 +010047
Serge Bazanski31370b02021-01-07 16:31:14 +010048 node "source.monogon.dev/metropolis/node"
49 "source.monogon.dev/metropolis/node/core/consensus/ca"
Serge Bazanskia105db52021-04-12 19:57:46 +020050 "source.monogon.dev/metropolis/node/core/consensus/client"
Serge Bazanski31370b02021-01-07 16:31:14 +010051 "source.monogon.dev/metropolis/node/core/localstorage"
Serge Bazanski50009e02021-07-07 14:35:27 +020052 "source.monogon.dev/metropolis/pkg/logtree/unraw"
Serge Bazanski31370b02021-01-07 16:31:14 +010053 "source.monogon.dev/metropolis/pkg/supervisor"
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +020054)
55
56const (
Serge Bazanski662b5b32020-12-21 13:49:00 +010057 DefaultClusterToken = "METROPOLIS"
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +020058 DefaultLogger = "zap"
59)
60
Serge Bazanskicb883e22020-07-06 17:47:55 +020061// Service is the etcd cluster member service.
62type Service struct {
63 // The configuration with which the service was started. This is immutable.
64 config *Config
Lorenz Bruna4ea9d02019-10-31 11:40:30 +010065
Serge Bazanski216fe7b2021-05-21 18:36:16 +020066 // stateMu guards state. This is locked internally on public methods of
67 // Service that require access to state. The state might be recreated on
68 // service restart.
Serge Bazanskicb883e22020-07-06 17:47:55 +020069 stateMu sync.Mutex
70 state *state
71}
Lorenz Brun6e8f69c2019-11-18 10:44:24 +010072
Serge Bazanskicb883e22020-07-06 17:47:55 +020073// state is the runtime state of a running etcd member.
74type state struct {
75 etcd *embed.Etcd
76 ready atomic.Bool
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +020077
Serge Bazanskicb883e22020-07-06 17:47:55 +020078 ca *ca.CA
Serge Bazanski216fe7b2021-05-21 18:36:16 +020079 // cl is an etcd client that loops back to the localy running etcd server.
80 // This runs over the Client unix domain socket that etcd starts.
Serge Bazanskicb883e22020-07-06 17:47:55 +020081 cl *clientv3.Client
82}
Leopold Schabel68c58752019-11-14 21:00:59 +010083
Serge Bazanskicb883e22020-07-06 17:47:55 +020084type Config struct {
85 // Data directory (persistent, encrypted storage) for etcd.
86 Data *localstorage.DataEtcdDirectory
87 // Ephemeral directory for etcd.
88 Ephemeral *localstorage.EphemeralConsensusDirectory
Leopold Schabel68c58752019-11-14 21:00:59 +010089
Serge Bazanski216fe7b2021-05-21 18:36:16 +020090 // Name is the cluster name. This must be the same amongst all etcd members
91 // within one cluster.
Serge Bazanskicb883e22020-07-06 17:47:55 +020092 Name string
Serge Bazanski216fe7b2021-05-21 18:36:16 +020093 // NewCluster selects whether the etcd member will start a new cluster and
94 // bootstrap a CA and the first member certificate, or load existing PKI
95 // certificates from disk.
Serge Bazanskicb883e22020-07-06 17:47:55 +020096 NewCluster bool
Serge Bazanski216fe7b2021-05-21 18:36:16 +020097 // Port is the port at which this cluster member will listen for other
98 // members. If zero, defaults to the global Metropolis setting.
Serge Bazanskicb883e22020-07-06 17:47:55 +020099 Port int
Serge Bazanski34fe8c62021-03-16 13:20:09 +0100100
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200101 // externalHost is used by tests to override the address at which etcd
102 // should listen for peer connections.
Serge Bazanski42e61c62021-03-18 15:07:18 +0100103 externalHost string
Serge Bazanskicb883e22020-07-06 17:47:55 +0200104}
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200105
Serge Bazanskicb883e22020-07-06 17:47:55 +0200106func New(config Config) *Service {
107 return &Service{
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200108 config: &config,
109 }
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200110}
111
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200112// configure transforms the service configuration into an embedded etcd
113// configuration. This is pure and side effect free.
Serge Bazanskicb883e22020-07-06 17:47:55 +0200114func (s *Service) configure(ctx context.Context) (*embed.Config, error) {
115 if err := s.config.Ephemeral.MkdirAll(0700); err != nil {
116 return nil, fmt.Errorf("failed to create ephemeral directory: %w", err)
117 }
118 if err := s.config.Data.MkdirAll(0700); err != nil {
119 return nil, fmt.Errorf("failed to create data directory: %w", err)
120 }
Lorenz Brun52f7f292020-06-24 16:42:02 +0200121
Serge Bazanski34fe8c62021-03-16 13:20:09 +0100122 if s.config.Name == "" {
123 return nil, fmt.Errorf("Name not set")
124 }
Serge Bazanskicb883e22020-07-06 17:47:55 +0200125 port := s.config.Port
126 if port == 0 {
Serge Bazanski549b72b2021-01-07 14:54:19 +0100127 port = node.ConsensusPort
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200128 }
129
130 cfg := embed.NewConfig()
131
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200132 cfg.Name = s.config.Name
Serge Bazanskicb883e22020-07-06 17:47:55 +0200133 cfg.Dir = s.config.Data.Data.FullPath()
134 cfg.InitialClusterToken = DefaultClusterToken
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200135
Serge Bazanskicb883e22020-07-06 17:47:55 +0200136 cfg.PeerTLSInfo.CertFile = s.config.Data.PeerPKI.Certificate.FullPath()
137 cfg.PeerTLSInfo.KeyFile = s.config.Data.PeerPKI.Key.FullPath()
138 cfg.PeerTLSInfo.TrustedCAFile = s.config.Data.PeerPKI.CACertificate.FullPath()
139 cfg.PeerTLSInfo.ClientCertAuth = true
140 cfg.PeerTLSInfo.CRLFile = s.config.Data.PeerCRL.FullPath()
141
142 cfg.LCUrls = []url.URL{{
143 Scheme: "unix",
144 Path: s.config.Ephemeral.ClientSocket.FullPath() + ":0",
145 }}
146 cfg.ACUrls = []url.URL{}
147 cfg.LPUrls = []url.URL{{
148 Scheme: "https",
Serge Bazanski34fe8c62021-03-16 13:20:09 +0100149 Host: fmt.Sprintf("[::]:%d", port),
Serge Bazanskicb883e22020-07-06 17:47:55 +0200150 }}
Serge Bazanski34fe8c62021-03-16 13:20:09 +0100151
152 // Always listen on the address pointed to by our name - unless running in
153 // tests, where we can't control our hostname easily.
Serge Bazanski42e61c62021-03-18 15:07:18 +0100154 externalHost := fmt.Sprintf("%s:%d", s.config.Name, port)
155 if s.config.externalHost != "" {
156 externalHost = fmt.Sprintf("%s:%d", s.config.externalHost, port)
Serge Bazanski34fe8c62021-03-16 13:20:09 +0100157 }
Serge Bazanskicb883e22020-07-06 17:47:55 +0200158 cfg.APUrls = []url.URL{{
159 Scheme: "https",
Serge Bazanski42e61c62021-03-18 15:07:18 +0100160 Host: externalHost,
Serge Bazanskicb883e22020-07-06 17:47:55 +0200161 }}
162
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200163 if s.config.NewCluster {
164 cfg.ClusterState = "new"
165 cfg.InitialCluster = cfg.InitialClusterFromName(cfg.Name)
Serge Bazanski34fe8c62021-03-16 13:20:09 +0100166 } else {
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200167 cfg.ClusterState = "existing"
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200168 }
169
Serge Bazanski50009e02021-07-07 14:35:27 +0200170 converter := unraw.Converter{
171 Parser: parseEtcdLogEntry,
172 // The initial line from a starting etcd instance is fairly long.
173 MaximumLineLength: 8192,
174 LeveledLogger: supervisor.Logger(ctx),
175 }
176 fifoPath := s.config.Ephemeral.ServerLogsFIFO.FullPath()
177 pipe, err := converter.NamedPipeReader(fifoPath)
178 if err != nil {
179 return nil, fmt.Errorf("could not get named pipe reader: %w", err)
180 }
181 if err := supervisor.Run(ctx, "pipe", pipe); err != nil {
182 return nil, fmt.Errorf("could not start server log reader: %w", err)
183 }
184
Serge Bazanskic7359672020-10-30 16:38:57 +0100185 // TODO(q3k): pipe logs from etcd to supervisor.RawLogger via a file.
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200186 cfg.Logger = DefaultLogger
Serge Bazanski50009e02021-07-07 14:35:27 +0200187 cfg.LogOutputs = []string{fifoPath}
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200188
Serge Bazanskicb883e22020-07-06 17:47:55 +0200189 return cfg, nil
190}
191
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200192// Run is a Supervisor runnable that starts the etcd member service. It will
193// become healthy once the member joins the cluster successfully.
Serge Bazanskicb883e22020-07-06 17:47:55 +0200194func (s *Service) Run(ctx context.Context) error {
195 st := &state{
196 ready: *atomic.NewBool(false),
197 }
198 s.stateMu.Lock()
199 s.state = st
200 s.stateMu.Unlock()
201
202 if s.config.NewCluster {
Serge Bazanski3ea1a3a2021-03-16 13:17:33 +0100203 // Create certificate if absent. It can only be present if we attempt
204 // to re-start the service in NewCluster after a failure. This can
205 // happen if etcd crashed or failed to start up before (eg. because of
206 // networking not having settled yet).
Serge Bazanskicb883e22020-07-06 17:47:55 +0200207 absent, err := s.config.Data.PeerPKI.AllAbsent()
208 if err != nil {
209 return fmt.Errorf("checking certificate existence: %w", err)
210 }
Serge Bazanskicb883e22020-07-06 17:47:55 +0200211
Serge Bazanski3ea1a3a2021-03-16 13:17:33 +0100212 if absent {
213 // Generate CA, keep in memory, write it down in etcd later.
214 st.ca, err = ca.New("Metropolis etcd peer Root CA")
215 if err != nil {
216 return fmt.Errorf("when creating new cluster's peer CA: %w", err)
217 }
Serge Bazanskicb883e22020-07-06 17:47:55 +0200218
Serge Bazanski34fe8c62021-03-16 13:20:09 +0100219 cert, key, err := st.ca.Issue(ctx, nil, s.config.Name)
Serge Bazanski3ea1a3a2021-03-16 13:17:33 +0100220 if err != nil {
221 return fmt.Errorf("when issuing new cluster's first certificate: %w", err)
222 }
Serge Bazanskicb883e22020-07-06 17:47:55 +0200223
Serge Bazanski3ea1a3a2021-03-16 13:17:33 +0100224 if err := s.config.Data.PeerPKI.MkdirAll(0700); err != nil {
225 return fmt.Errorf("when creating PKI directory: %w", err)
226 }
227 if err := s.config.Data.PeerPKI.CACertificate.Write(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: st.ca.CACertRaw}), 0600); err != nil {
228 return fmt.Errorf("when writing CA certificate to disk: %w", err)
229 }
230 if err := s.config.Data.PeerPKI.Certificate.Write(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert}), 0600); err != nil {
231 return fmt.Errorf("when writing certificate to disk: %w", err)
232 }
233 if err := s.config.Data.PeerPKI.Key.Write(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: key}), 0600); err != nil {
234 return fmt.Errorf("when writing certificate to disk: %w", err)
235 }
Serge Bazanskicb883e22020-07-06 17:47:55 +0200236 }
237 }
238
Serge Bazanski3ea1a3a2021-03-16 13:17:33 +0100239 // Expect certificate to be present on disk.
240 present, err := s.config.Data.PeerPKI.AllExist()
241 if err != nil {
242 return fmt.Errorf("checking certificate existence: %w", err)
243 }
244 if !present {
245 return fmt.Errorf("etcd starting without fully ready certificates - aborted NewCluster or corrupted local storage?")
Serge Bazanskicb883e22020-07-06 17:47:55 +0200246 }
247
248 cfg, err := s.configure(ctx)
249 if err != nil {
250 return fmt.Errorf("when configuring etcd: %w", err)
251 }
252
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200253 server, err := embed.StartEtcd(cfg)
Serge Bazanskicb883e22020-07-06 17:47:55 +0200254 keep := false
255 defer func() {
256 if !keep && server != nil {
257 server.Close()
258 }
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200259 }()
Lorenz Bruna4ea9d02019-10-31 11:40:30 +0100260 if err != nil {
Serge Bazanskicb883e22020-07-06 17:47:55 +0200261 return fmt.Errorf("failed to start etcd: %w", err)
Lorenz Bruna4ea9d02019-10-31 11:40:30 +0100262 }
Serge Bazanskicb883e22020-07-06 17:47:55 +0200263 st.etcd = server
Lorenz Bruna4ea9d02019-10-31 11:40:30 +0100264
Serge Bazanskicb883e22020-07-06 17:47:55 +0200265 okay := true
266 select {
267 case <-st.etcd.Server.ReadyNotify():
268 case <-ctx.Done():
269 okay = false
Lorenz Brun52f7f292020-06-24 16:42:02 +0200270 }
271
Serge Bazanskicb883e22020-07-06 17:47:55 +0200272 if !okay {
273 supervisor.Logger(ctx).Info("context done, aborting wait")
274 return ctx.Err()
Lorenz Brun52f7f292020-06-24 16:42:02 +0200275 }
276
Serge Bazanskicb883e22020-07-06 17:47:55 +0200277 socket := s.config.Ephemeral.ClientSocket.FullPath()
278 cl, err := clientv3.New(clientv3.Config{
279 Endpoints: []string{fmt.Sprintf("unix://%s:0", socket)},
280 DialTimeout: time.Second,
Lorenz Brun52f7f292020-06-24 16:42:02 +0200281 })
282 if err != nil {
Serge Bazanskicb883e22020-07-06 17:47:55 +0200283 return fmt.Errorf("failed to connect to new etcd instance: %w", err)
Lorenz Bruna4ea9d02019-10-31 11:40:30 +0100284 }
Serge Bazanskicb883e22020-07-06 17:47:55 +0200285 st.cl = cl
286
287 if s.config.NewCluster {
288 if st.ca == nil {
289 panic("peerCA has not been generated")
290 }
291
292 // Save new CA into etcd.
293 err = st.ca.Save(ctx, cl.KV)
294 if err != nil {
295 return fmt.Errorf("failed to save new CA to etcd: %w", err)
296 }
297 } else {
298 // Load existing CA from etcd.
299 st.ca, err = ca.Load(ctx, cl.KV)
300 if err != nil {
301 return fmt.Errorf("failed to load CA from etcd: %w", err)
302 }
303 }
304
305 // Start CRL watcher.
306 if err := supervisor.Run(ctx, "crl", s.watchCRL); err != nil {
307 return fmt.Errorf("failed to start CRL watcher: %w", err)
308 }
309 // Start autopromoter.
310 if err := supervisor.Run(ctx, "autopromoter", s.autopromoter); err != nil {
311 return fmt.Errorf("failed to start autopromoter: %w", err)
312 }
313
314 supervisor.Logger(ctx).Info("etcd is now ready")
315 keep = true
316 st.ready.Store(true)
317 supervisor.Signal(ctx, supervisor.SignalHealthy)
318
319 <-ctx.Done()
320 st.etcd.Close()
321 return ctx.Err()
Lorenz Bruna4ea9d02019-10-31 11:40:30 +0100322}
323
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200324// watchCRL is a sub-runnable of the etcd cluster member service that updates
325// the on-local-storage CRL to match the newest available version in etcd.
Serge Bazanskicb883e22020-07-06 17:47:55 +0200326func (s *Service) watchCRL(ctx context.Context) error {
327 s.stateMu.Lock()
328 cl := s.state.cl
329 ca := s.state.ca
330 s.stateMu.Unlock()
331
332 supervisor.Signal(ctx, supervisor.SignalHealthy)
333 for e := range ca.WaitCRLChange(ctx, cl.KV, cl.Watcher) {
334 if e.Err != nil {
335 return fmt.Errorf("watching CRL: %w", e.Err)
Lorenz Bruna4ea9d02019-10-31 11:40:30 +0100336 }
Serge Bazanskicb883e22020-07-06 17:47:55 +0200337
338 if err := s.config.Data.PeerCRL.Write(e.CRL, 0600); err != nil {
339 return fmt.Errorf("saving CRL: %w", err)
340 }
341 }
342
343 // unreachable
344 return nil
345}
346
347func (s *Service) autopromoter(ctx context.Context) error {
348 t := time.NewTicker(5 * time.Second)
349 defer t.Stop()
350
351 autopromote := func() {
352 s.stateMu.Lock()
353 st := s.state
354 s.stateMu.Unlock()
355
356 if st.etcd.Server.Leader() != st.etcd.Server.ID() {
357 return
358 }
359
360 for _, member := range st.etcd.Server.Cluster().Members() {
361 if !member.IsLearner {
Lorenz Bruna4ea9d02019-10-31 11:40:30 +0100362 continue
363 }
Lorenz Bruna4ea9d02019-10-31 11:40:30 +0100364
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200365 // We always call PromoteMember since the metadata necessary to
366 // decide if we should is private. Luckily etcd already does
367 // sanity checks internally and will refuse to promote nodes that
368 // aren't connected or are still behind on transactions.
Serge Bazanskicb883e22020-07-06 17:47:55 +0200369 if _, err := st.etcd.Server.PromoteMember(ctx, uint64(member.ID)); err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100370 supervisor.Logger(ctx).Infof("Failed to promote consensus node %s: %v", member.Name, err)
Serge Bazanskicb883e22020-07-06 17:47:55 +0200371 } else {
Serge Bazanskic7359672020-10-30 16:38:57 +0100372 supervisor.Logger(ctx).Infof("Promoted new consensus node %s", member.Name)
Lorenz Bruna4ea9d02019-10-31 11:40:30 +0100373 }
374 }
375 }
Lorenz Bruna4ea9d02019-10-31 11:40:30 +0100376
Serge Bazanskicb883e22020-07-06 17:47:55 +0200377 for {
378 select {
379 case <-ctx.Done():
380 return ctx.Err()
381 case <-t.C:
382 autopromote()
Lorenz Brun52f7f292020-06-24 16:42:02 +0200383 }
384 }
385}
386
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200387// IsReady returns whether etcd is ready and synced
388func (s *Service) IsReady() bool {
Serge Bazanskicb883e22020-07-06 17:47:55 +0200389 s.stateMu.Lock()
390 defer s.stateMu.Unlock()
391 if s.state == nil {
392 return false
393 }
394 return s.state.ready.Load()
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200395}
396
Serge Bazanskicb883e22020-07-06 17:47:55 +0200397func (s *Service) WaitReady(ctx context.Context) error {
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200398 // TODO(q3k): reimplement the atomic ready flag as an event synchronization
399 // mechanism
Serge Bazanskicb883e22020-07-06 17:47:55 +0200400 if s.IsReady() {
401 return nil
402 }
403 t := time.NewTicker(100 * time.Millisecond)
404 defer t.Stop()
405 for {
406 select {
407 case <-ctx.Done():
408 return ctx.Err()
409 case <-t.C:
410 if s.IsReady() {
411 return nil
412 }
413 }
414 }
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200415}
416
Serge Bazanskia105db52021-04-12 19:57:46 +0200417func (s *Service) Client() client.Namespaced {
Serge Bazanskicb883e22020-07-06 17:47:55 +0200418 s.stateMu.Lock()
419 defer s.stateMu.Unlock()
Serge Bazanskia105db52021-04-12 19:57:46 +0200420 // 'namespaced' is the root of all namespaced clients within the etcd K/V
421 // store, with further paths in a colon-separated format, eg.:
422 // namespaced:example/
423 // namespaced:foo:bar:baz/
424 client, err := client.NewLocal(s.state.cl).Sub("namespaced")
425 if err != nil {
426 // This error can only happen due to a malformed path, which is
427 // constant. Thus, this is a programming error and we panic.
428 panic(fmt.Errorf("Could not get consensus etcd client: %v", err))
429 }
430 return client
Serge Bazanskicb883e22020-07-06 17:47:55 +0200431}
432
433func (s *Service) Cluster() clientv3.Cluster {
434 s.stateMu.Lock()
435 defer s.stateMu.Unlock()
436 return s.state.cl.Cluster
437}
438
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200439// MemberInfo returns information about this etcd cluster member: its ID and
440// name. This will block until this information is available (ie. the cluster
441// status is Ready).
Serge Bazanskicb883e22020-07-06 17:47:55 +0200442func (s *Service) MemberInfo(ctx context.Context) (id uint64, name string, err error) {
443 if err = s.WaitReady(ctx); err != nil {
444 err = fmt.Errorf("when waiting for cluster readiness: %w", err)
445 return
446 }
447
448 s.stateMu.Lock()
449 defer s.stateMu.Unlock()
450 id = uint64(s.state.etcd.Server.ID())
451 name = s.config.Name
452 return
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200453}