blob: ed44140751a2cb17639956cd2a17b76fab23fd25 [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"
52 "source.monogon.dev/metropolis/pkg/supervisor"
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +020053)
54
55const (
Serge Bazanski662b5b32020-12-21 13:49:00 +010056 DefaultClusterToken = "METROPOLIS"
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +020057 DefaultLogger = "zap"
58)
59
Serge Bazanskicb883e22020-07-06 17:47:55 +020060// Service is the etcd cluster member service.
61type Service struct {
62 // The configuration with which the service was started. This is immutable.
63 config *Config
Lorenz Bruna4ea9d02019-10-31 11:40:30 +010064
Serge Bazanski216fe7b2021-05-21 18:36:16 +020065 // stateMu guards state. This is locked internally on public methods of
66 // Service that require access to state. The state might be recreated on
67 // service restart.
Serge Bazanskicb883e22020-07-06 17:47:55 +020068 stateMu sync.Mutex
69 state *state
70}
Lorenz Brun6e8f69c2019-11-18 10:44:24 +010071
Serge Bazanskicb883e22020-07-06 17:47:55 +020072// state is the runtime state of a running etcd member.
73type state struct {
74 etcd *embed.Etcd
75 ready atomic.Bool
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +020076
Serge Bazanskicb883e22020-07-06 17:47:55 +020077 ca *ca.CA
Serge Bazanski216fe7b2021-05-21 18:36:16 +020078 // cl is an etcd client that loops back to the localy running etcd server.
79 // This runs over the Client unix domain socket that etcd starts.
Serge Bazanskicb883e22020-07-06 17:47:55 +020080 cl *clientv3.Client
81}
Leopold Schabel68c58752019-11-14 21:00:59 +010082
Serge Bazanskicb883e22020-07-06 17:47:55 +020083type Config struct {
84 // Data directory (persistent, encrypted storage) for etcd.
85 Data *localstorage.DataEtcdDirectory
86 // Ephemeral directory for etcd.
87 Ephemeral *localstorage.EphemeralConsensusDirectory
Leopold Schabel68c58752019-11-14 21:00:59 +010088
Serge Bazanski216fe7b2021-05-21 18:36:16 +020089 // Name is the cluster name. This must be the same amongst all etcd members
90 // within one cluster.
Serge Bazanskicb883e22020-07-06 17:47:55 +020091 Name string
Serge Bazanski216fe7b2021-05-21 18:36:16 +020092 // NewCluster selects whether the etcd member will start a new cluster and
93 // bootstrap a CA and the first member certificate, or load existing PKI
94 // certificates from disk.
Serge Bazanskicb883e22020-07-06 17:47:55 +020095 NewCluster bool
Serge Bazanski216fe7b2021-05-21 18:36:16 +020096 // Port is the port at which this cluster member will listen for other
97 // members. If zero, defaults to the global Metropolis setting.
Serge Bazanskicb883e22020-07-06 17:47:55 +020098 Port int
Serge Bazanski34fe8c62021-03-16 13:20:09 +010099
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200100 // externalHost is used by tests to override the address at which etcd
101 // should listen for peer connections.
Serge Bazanski42e61c62021-03-18 15:07:18 +0100102 externalHost string
Serge Bazanskicb883e22020-07-06 17:47:55 +0200103}
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200104
Serge Bazanskicb883e22020-07-06 17:47:55 +0200105func New(config Config) *Service {
106 return &Service{
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200107 config: &config,
108 }
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200109}
110
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200111// configure transforms the service configuration into an embedded etcd
112// configuration. This is pure and side effect free.
Serge Bazanskicb883e22020-07-06 17:47:55 +0200113func (s *Service) configure(ctx context.Context) (*embed.Config, error) {
114 if err := s.config.Ephemeral.MkdirAll(0700); err != nil {
115 return nil, fmt.Errorf("failed to create ephemeral directory: %w", err)
116 }
117 if err := s.config.Data.MkdirAll(0700); err != nil {
118 return nil, fmt.Errorf("failed to create data directory: %w", err)
119 }
Lorenz Brun52f7f292020-06-24 16:42:02 +0200120
Serge Bazanski34fe8c62021-03-16 13:20:09 +0100121 if s.config.Name == "" {
122 return nil, fmt.Errorf("Name not set")
123 }
Serge Bazanskicb883e22020-07-06 17:47:55 +0200124 port := s.config.Port
125 if port == 0 {
Serge Bazanski549b72b2021-01-07 14:54:19 +0100126 port = node.ConsensusPort
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200127 }
128
129 cfg := embed.NewConfig()
130
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200131 cfg.Name = s.config.Name
Serge Bazanskicb883e22020-07-06 17:47:55 +0200132 cfg.Dir = s.config.Data.Data.FullPath()
133 cfg.InitialClusterToken = DefaultClusterToken
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200134
Serge Bazanskicb883e22020-07-06 17:47:55 +0200135 cfg.PeerTLSInfo.CertFile = s.config.Data.PeerPKI.Certificate.FullPath()
136 cfg.PeerTLSInfo.KeyFile = s.config.Data.PeerPKI.Key.FullPath()
137 cfg.PeerTLSInfo.TrustedCAFile = s.config.Data.PeerPKI.CACertificate.FullPath()
138 cfg.PeerTLSInfo.ClientCertAuth = true
139 cfg.PeerTLSInfo.CRLFile = s.config.Data.PeerCRL.FullPath()
140
141 cfg.LCUrls = []url.URL{{
142 Scheme: "unix",
143 Path: s.config.Ephemeral.ClientSocket.FullPath() + ":0",
144 }}
145 cfg.ACUrls = []url.URL{}
146 cfg.LPUrls = []url.URL{{
147 Scheme: "https",
Serge Bazanski34fe8c62021-03-16 13:20:09 +0100148 Host: fmt.Sprintf("[::]:%d", port),
Serge Bazanskicb883e22020-07-06 17:47:55 +0200149 }}
Serge Bazanski34fe8c62021-03-16 13:20:09 +0100150
151 // Always listen on the address pointed to by our name - unless running in
152 // tests, where we can't control our hostname easily.
Serge Bazanski42e61c62021-03-18 15:07:18 +0100153 externalHost := fmt.Sprintf("%s:%d", s.config.Name, port)
154 if s.config.externalHost != "" {
155 externalHost = fmt.Sprintf("%s:%d", s.config.externalHost, port)
Serge Bazanski34fe8c62021-03-16 13:20:09 +0100156 }
Serge Bazanskicb883e22020-07-06 17:47:55 +0200157 cfg.APUrls = []url.URL{{
158 Scheme: "https",
Serge Bazanski42e61c62021-03-18 15:07:18 +0100159 Host: externalHost,
Serge Bazanskicb883e22020-07-06 17:47:55 +0200160 }}
161
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200162 if s.config.NewCluster {
163 cfg.ClusterState = "new"
164 cfg.InitialCluster = cfg.InitialClusterFromName(cfg.Name)
Serge Bazanski34fe8c62021-03-16 13:20:09 +0100165 } else {
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200166 cfg.ClusterState = "existing"
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200167 }
168
Serge Bazanskic7359672020-10-30 16:38:57 +0100169 // TODO(q3k): pipe logs from etcd to supervisor.RawLogger via a file.
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200170 cfg.Logger = DefaultLogger
Serge Bazanskic7359672020-10-30 16:38:57 +0100171 cfg.LogOutputs = []string{"stderr"}
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200172
Serge Bazanskicb883e22020-07-06 17:47:55 +0200173 return cfg, nil
174}
175
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200176// Run is a Supervisor runnable that starts the etcd member service. It will
177// become healthy once the member joins the cluster successfully.
Serge Bazanskicb883e22020-07-06 17:47:55 +0200178func (s *Service) Run(ctx context.Context) error {
179 st := &state{
180 ready: *atomic.NewBool(false),
181 }
182 s.stateMu.Lock()
183 s.state = st
184 s.stateMu.Unlock()
185
186 if s.config.NewCluster {
Serge Bazanski3ea1a3a2021-03-16 13:17:33 +0100187 // Create certificate if absent. It can only be present if we attempt
188 // to re-start the service in NewCluster after a failure. This can
189 // happen if etcd crashed or failed to start up before (eg. because of
190 // networking not having settled yet).
Serge Bazanskicb883e22020-07-06 17:47:55 +0200191 absent, err := s.config.Data.PeerPKI.AllAbsent()
192 if err != nil {
193 return fmt.Errorf("checking certificate existence: %w", err)
194 }
Serge Bazanskicb883e22020-07-06 17:47:55 +0200195
Serge Bazanski3ea1a3a2021-03-16 13:17:33 +0100196 if absent {
197 // Generate CA, keep in memory, write it down in etcd later.
198 st.ca, err = ca.New("Metropolis etcd peer Root CA")
199 if err != nil {
200 return fmt.Errorf("when creating new cluster's peer CA: %w", err)
201 }
Serge Bazanskicb883e22020-07-06 17:47:55 +0200202
Serge Bazanski34fe8c62021-03-16 13:20:09 +0100203 cert, key, err := st.ca.Issue(ctx, nil, s.config.Name)
Serge Bazanski3ea1a3a2021-03-16 13:17:33 +0100204 if err != nil {
205 return fmt.Errorf("when issuing new cluster's first certificate: %w", err)
206 }
Serge Bazanskicb883e22020-07-06 17:47:55 +0200207
Serge Bazanski3ea1a3a2021-03-16 13:17:33 +0100208 if err := s.config.Data.PeerPKI.MkdirAll(0700); err != nil {
209 return fmt.Errorf("when creating PKI directory: %w", err)
210 }
211 if err := s.config.Data.PeerPKI.CACertificate.Write(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: st.ca.CACertRaw}), 0600); err != nil {
212 return fmt.Errorf("when writing CA certificate to disk: %w", err)
213 }
214 if err := s.config.Data.PeerPKI.Certificate.Write(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert}), 0600); err != nil {
215 return fmt.Errorf("when writing certificate to disk: %w", err)
216 }
217 if err := s.config.Data.PeerPKI.Key.Write(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: key}), 0600); err != nil {
218 return fmt.Errorf("when writing certificate to disk: %w", err)
219 }
Serge Bazanskicb883e22020-07-06 17:47:55 +0200220 }
221 }
222
Serge Bazanski3ea1a3a2021-03-16 13:17:33 +0100223 // Expect certificate to be present on disk.
224 present, err := s.config.Data.PeerPKI.AllExist()
225 if err != nil {
226 return fmt.Errorf("checking certificate existence: %w", err)
227 }
228 if !present {
229 return fmt.Errorf("etcd starting without fully ready certificates - aborted NewCluster or corrupted local storage?")
Serge Bazanskicb883e22020-07-06 17:47:55 +0200230 }
231
232 cfg, err := s.configure(ctx)
233 if err != nil {
234 return fmt.Errorf("when configuring etcd: %w", err)
235 }
236
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200237 server, err := embed.StartEtcd(cfg)
Serge Bazanskicb883e22020-07-06 17:47:55 +0200238 keep := false
239 defer func() {
240 if !keep && server != nil {
241 server.Close()
242 }
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200243 }()
Lorenz Bruna4ea9d02019-10-31 11:40:30 +0100244 if err != nil {
Serge Bazanskicb883e22020-07-06 17:47:55 +0200245 return fmt.Errorf("failed to start etcd: %w", err)
Lorenz Bruna4ea9d02019-10-31 11:40:30 +0100246 }
Serge Bazanskicb883e22020-07-06 17:47:55 +0200247 st.etcd = server
Lorenz Bruna4ea9d02019-10-31 11:40:30 +0100248
Serge Bazanskicb883e22020-07-06 17:47:55 +0200249 okay := true
250 select {
251 case <-st.etcd.Server.ReadyNotify():
252 case <-ctx.Done():
253 okay = false
Lorenz Brun52f7f292020-06-24 16:42:02 +0200254 }
255
Serge Bazanskicb883e22020-07-06 17:47:55 +0200256 if !okay {
257 supervisor.Logger(ctx).Info("context done, aborting wait")
258 return ctx.Err()
Lorenz Brun52f7f292020-06-24 16:42:02 +0200259 }
260
Serge Bazanskicb883e22020-07-06 17:47:55 +0200261 socket := s.config.Ephemeral.ClientSocket.FullPath()
262 cl, err := clientv3.New(clientv3.Config{
263 Endpoints: []string{fmt.Sprintf("unix://%s:0", socket)},
264 DialTimeout: time.Second,
Lorenz Brun52f7f292020-06-24 16:42:02 +0200265 })
266 if err != nil {
Serge Bazanskicb883e22020-07-06 17:47:55 +0200267 return fmt.Errorf("failed to connect to new etcd instance: %w", err)
Lorenz Bruna4ea9d02019-10-31 11:40:30 +0100268 }
Serge Bazanskicb883e22020-07-06 17:47:55 +0200269 st.cl = cl
270
271 if s.config.NewCluster {
272 if st.ca == nil {
273 panic("peerCA has not been generated")
274 }
275
276 // Save new CA into etcd.
277 err = st.ca.Save(ctx, cl.KV)
278 if err != nil {
279 return fmt.Errorf("failed to save new CA to etcd: %w", err)
280 }
281 } else {
282 // Load existing CA from etcd.
283 st.ca, err = ca.Load(ctx, cl.KV)
284 if err != nil {
285 return fmt.Errorf("failed to load CA from etcd: %w", err)
286 }
287 }
288
289 // Start CRL watcher.
290 if err := supervisor.Run(ctx, "crl", s.watchCRL); err != nil {
291 return fmt.Errorf("failed to start CRL watcher: %w", err)
292 }
293 // Start autopromoter.
294 if err := supervisor.Run(ctx, "autopromoter", s.autopromoter); err != nil {
295 return fmt.Errorf("failed to start autopromoter: %w", err)
296 }
297
298 supervisor.Logger(ctx).Info("etcd is now ready")
299 keep = true
300 st.ready.Store(true)
301 supervisor.Signal(ctx, supervisor.SignalHealthy)
302
303 <-ctx.Done()
304 st.etcd.Close()
305 return ctx.Err()
Lorenz Bruna4ea9d02019-10-31 11:40:30 +0100306}
307
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200308// watchCRL is a sub-runnable of the etcd cluster member service that updates
309// the on-local-storage CRL to match the newest available version in etcd.
Serge Bazanskicb883e22020-07-06 17:47:55 +0200310func (s *Service) watchCRL(ctx context.Context) error {
311 s.stateMu.Lock()
312 cl := s.state.cl
313 ca := s.state.ca
314 s.stateMu.Unlock()
315
316 supervisor.Signal(ctx, supervisor.SignalHealthy)
317 for e := range ca.WaitCRLChange(ctx, cl.KV, cl.Watcher) {
318 if e.Err != nil {
319 return fmt.Errorf("watching CRL: %w", e.Err)
Lorenz Bruna4ea9d02019-10-31 11:40:30 +0100320 }
Serge Bazanskicb883e22020-07-06 17:47:55 +0200321
322 if err := s.config.Data.PeerCRL.Write(e.CRL, 0600); err != nil {
323 return fmt.Errorf("saving CRL: %w", err)
324 }
325 }
326
327 // unreachable
328 return nil
329}
330
331func (s *Service) autopromoter(ctx context.Context) error {
332 t := time.NewTicker(5 * time.Second)
333 defer t.Stop()
334
335 autopromote := func() {
336 s.stateMu.Lock()
337 st := s.state
338 s.stateMu.Unlock()
339
340 if st.etcd.Server.Leader() != st.etcd.Server.ID() {
341 return
342 }
343
344 for _, member := range st.etcd.Server.Cluster().Members() {
345 if !member.IsLearner {
Lorenz Bruna4ea9d02019-10-31 11:40:30 +0100346 continue
347 }
Lorenz Bruna4ea9d02019-10-31 11:40:30 +0100348
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200349 // We always call PromoteMember since the metadata necessary to
350 // decide if we should is private. Luckily etcd already does
351 // sanity checks internally and will refuse to promote nodes that
352 // aren't connected or are still behind on transactions.
Serge Bazanskicb883e22020-07-06 17:47:55 +0200353 if _, err := st.etcd.Server.PromoteMember(ctx, uint64(member.ID)); err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100354 supervisor.Logger(ctx).Infof("Failed to promote consensus node %s: %v", member.Name, err)
Serge Bazanskicb883e22020-07-06 17:47:55 +0200355 } else {
Serge Bazanskic7359672020-10-30 16:38:57 +0100356 supervisor.Logger(ctx).Infof("Promoted new consensus node %s", member.Name)
Lorenz Bruna4ea9d02019-10-31 11:40:30 +0100357 }
358 }
359 }
Lorenz Bruna4ea9d02019-10-31 11:40:30 +0100360
Serge Bazanskicb883e22020-07-06 17:47:55 +0200361 for {
362 select {
363 case <-ctx.Done():
364 return ctx.Err()
365 case <-t.C:
366 autopromote()
Lorenz Brun52f7f292020-06-24 16:42:02 +0200367 }
368 }
369}
370
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200371// IsReady returns whether etcd is ready and synced
372func (s *Service) IsReady() bool {
Serge Bazanskicb883e22020-07-06 17:47:55 +0200373 s.stateMu.Lock()
374 defer s.stateMu.Unlock()
375 if s.state == nil {
376 return false
377 }
378 return s.state.ready.Load()
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200379}
380
Serge Bazanskicb883e22020-07-06 17:47:55 +0200381func (s *Service) WaitReady(ctx context.Context) error {
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200382 // TODO(q3k): reimplement the atomic ready flag as an event synchronization
383 // mechanism
Serge Bazanskicb883e22020-07-06 17:47:55 +0200384 if s.IsReady() {
385 return nil
386 }
387 t := time.NewTicker(100 * time.Millisecond)
388 defer t.Stop()
389 for {
390 select {
391 case <-ctx.Done():
392 return ctx.Err()
393 case <-t.C:
394 if s.IsReady() {
395 return nil
396 }
397 }
398 }
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200399}
400
Serge Bazanskia105db52021-04-12 19:57:46 +0200401func (s *Service) Client() client.Namespaced {
Serge Bazanskicb883e22020-07-06 17:47:55 +0200402 s.stateMu.Lock()
403 defer s.stateMu.Unlock()
Serge Bazanskia105db52021-04-12 19:57:46 +0200404 // 'namespaced' is the root of all namespaced clients within the etcd K/V
405 // store, with further paths in a colon-separated format, eg.:
406 // namespaced:example/
407 // namespaced:foo:bar:baz/
408 client, err := client.NewLocal(s.state.cl).Sub("namespaced")
409 if err != nil {
410 // This error can only happen due to a malformed path, which is
411 // constant. Thus, this is a programming error and we panic.
412 panic(fmt.Errorf("Could not get consensus etcd client: %v", err))
413 }
414 return client
Serge Bazanskicb883e22020-07-06 17:47:55 +0200415}
416
417func (s *Service) Cluster() clientv3.Cluster {
418 s.stateMu.Lock()
419 defer s.stateMu.Unlock()
420 return s.state.cl.Cluster
421}
422
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200423// MemberInfo returns information about this etcd cluster member: its ID and
424// name. This will block until this information is available (ie. the cluster
425// status is Ready).
Serge Bazanskicb883e22020-07-06 17:47:55 +0200426func (s *Service) MemberInfo(ctx context.Context) (id uint64, name string, err error) {
427 if err = s.WaitReady(ctx); err != nil {
428 err = fmt.Errorf("when waiting for cluster readiness: %w", err)
429 return
430 }
431
432 s.stateMu.Lock()
433 defer s.stateMu.Unlock()
434 id = uint64(s.state.etcd.Server.ID())
435 name = s.config.Name
436 return
Hendrik Hofstadt0d7c91e2019-10-23 21:44:47 +0200437}