1// Copyright 2019 The etcd Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package raftpb
16
17import (
18	"fmt"
19	"strconv"
20	"strings"
21
22	"github.com/gogo/protobuf/proto"
23)
24
25// ConfChangeI abstracts over ConfChangeV2 and (legacy) ConfChange to allow
26// treating them in a unified manner.
27type ConfChangeI interface {
28	AsV2() ConfChangeV2
29	AsV1() (ConfChange, bool)
30}
31
32// MarshalConfChange calls Marshal on the underlying ConfChange or ConfChangeV2
33// and returns the result along with the corresponding EntryType.
34func MarshalConfChange(c ConfChangeI) (EntryType, []byte, error) {
35	var typ EntryType
36	var ccdata []byte
37	var err error
38	if ccv1, ok := c.AsV1(); ok {
39		typ = EntryConfChange
40		ccdata, err = ccv1.Marshal()
41	} else {
42		ccv2 := c.AsV2()
43		typ = EntryConfChangeV2
44		ccdata, err = ccv2.Marshal()
45	}
46	return typ, ccdata, err
47}
48
49// AsV2 returns a V2 configuration change carrying out the same operation.
50func (c ConfChange) AsV2() ConfChangeV2 {
51	return ConfChangeV2{
52		Changes: []ConfChangeSingle{{
53			Type:   c.Type,
54			NodeID: c.NodeID,
55		}},
56		Context: c.Context,
57	}
58}
59
60// AsV1 returns the ConfChange and true.
61func (c ConfChange) AsV1() (ConfChange, bool) {
62	return c, true
63}
64
65// AsV2 is the identity.
66func (c ConfChangeV2) AsV2() ConfChangeV2 { return c }
67
68// AsV1 returns ConfChange{} and false.
69func (c ConfChangeV2) AsV1() (ConfChange, bool) { return ConfChange{}, false }
70
71// EnterJoint returns two bools. The second bool is true if and only if this
72// config change will use Joint Consensus, which is the case if it contains more
73// than one change or if the use of Joint Consensus was requested explicitly.
74// The first bool can only be true if second one is, and indicates whether the
75// Joint State will be left automatically.
76func (c ConfChangeV2) EnterJoint() (autoLeave bool, ok bool) {
77	// NB: in theory, more config changes could qualify for the "simple"
78	// protocol but it depends on the config on top of which the changes apply.
79	// For example, adding two learners is not OK if both nodes are part of the
80	// base config (i.e. two voters are turned into learners in the process of
81	// applying the conf change). In practice, these distinctions should not
82	// matter, so we keep it simple and use Joint Consensus liberally.
83	if c.Transition != ConfChangeTransitionAuto || len(c.Changes) > 1 {
84		// Use Joint Consensus.
85		var autoLeave bool
86		switch c.Transition {
87		case ConfChangeTransitionAuto:
88			autoLeave = true
89		case ConfChangeTransitionJointImplicit:
90			autoLeave = true
91		case ConfChangeTransitionJointExplicit:
92		default:
93			panic(fmt.Sprintf("unknown transition: %+v", c))
94		}
95		return autoLeave, true
96	}
97	return false, false
98}
99
100// LeaveJoint is true if the configuration change leaves a joint configuration.
101// This is the case if the ConfChangeV2 is zero, with the possible exception of
102// the Context field.
103func (c ConfChangeV2) LeaveJoint() bool {
104	// NB: c is already a copy.
105	c.Context = nil
106	return proto.Equal(&c, &ConfChangeV2{})
107}
108
109// ConfChangesFromString parses a Space-delimited sequence of operations into a
110// slice of ConfChangeSingle. The supported operations are:
111// - vn: make n a voter,
112// - ln: make n a learner,
113// - rn: remove n, and
114// - un: update n.
115func ConfChangesFromString(s string) ([]ConfChangeSingle, error) {
116	var ccs []ConfChangeSingle
117	toks := strings.Split(strings.TrimSpace(s), " ")
118	if toks[0] == "" {
119		toks = nil
120	}
121	for _, tok := range toks {
122		if len(tok) < 2 {
123			return nil, fmt.Errorf("unknown token %s", tok)
124		}
125		var cc ConfChangeSingle
126		switch tok[0] {
127		case 'v':
128			cc.Type = ConfChangeAddNode
129		case 'l':
130			cc.Type = ConfChangeAddLearnerNode
131		case 'r':
132			cc.Type = ConfChangeRemoveNode
133		case 'u':
134			cc.Type = ConfChangeUpdateNode
135		default:
136			return nil, fmt.Errorf("unknown input: %s", tok)
137		}
138		id, err := strconv.ParseUint(tok[1:], 10, 64)
139		if err != nil {
140			return nil, err
141		}
142		cc.NodeID = id
143		ccs = append(ccs, cc)
144	}
145	return ccs, nil
146}
147
148// ConfChangesToString is the inverse to ConfChangesFromString.
149func ConfChangesToString(ccs []ConfChangeSingle) string {
150	var buf strings.Builder
151	for i, cc := range ccs {
152		if i > 0 {
153			buf.WriteByte(' ')
154		}
155		switch cc.Type {
156		case ConfChangeAddNode:
157			buf.WriteByte('v')
158		case ConfChangeAddLearnerNode:
159			buf.WriteByte('l')
160		case ConfChangeRemoveNode:
161			buf.WriteByte('r')
162		case ConfChangeUpdateNode:
163			buf.WriteByte('u')
164		default:
165			buf.WriteString("unknown")
166		}
167		fmt.Fprintf(&buf, "%d", cc.NodeID)
168	}
169	return buf.String()
170}
171