The old system with vendor/ and git submodules worked great, unfortunately FUD around git submodules seemed to scare people away and golang moved to a go.mod system that adds a new lock file format instead of using the built-in git version. It's now almost impossible to use modern golang without this, so we've switched. So much for the golang compatibility promise-- turns out it doesn't apply to the useful parts that I actually care about like this. Thanks to frebib for his incredibly valuable contributions to this patch. This snide commit message is mine alone. This patch also mixes in some changes due to legacy golang as we've also bumped the minimum version to 1.16 in the docs and tests. Lastly, we had to disable some tests and fix up a few other misc things to get this passing. We've definitely hot bugs in the go.mod system, and our Makefile tries to workaround those.
238 lines
8.3 KiB
Go
238 lines
8.3 KiB
Go
// Mgmt
|
|
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
package etcd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/purpleidea/mgmt/util/errwrap"
|
|
|
|
etcdtypes "go.etcd.io/etcd/client/pkg/v3/types"
|
|
etcd "go.etcd.io/etcd/client/v3"
|
|
etcdutil "go.etcd.io/etcd/client/v3/clientv3util"
|
|
)
|
|
|
|
// volunteer offers yourself up to be a server if needed. If you specify a nil
|
|
// value for urls, then this will unvolunteer yourself.
|
|
func (obj *EmbdEtcd) volunteer(ctx context.Context, urls etcdtypes.URLs) error {
|
|
if obj.Debug {
|
|
if urls == nil {
|
|
obj.Logf("unvolunteer...")
|
|
defer obj.Logf("unvolunteer: done!")
|
|
} else {
|
|
obj.Logf("volunteer: %s", urls.String())
|
|
defer obj.Logf("volunteer: done!")
|
|
}
|
|
}
|
|
// volunteer to be a server
|
|
key := fmt.Sprintf(obj.NS+volunteerPathFmt, obj.Hostname)
|
|
ifs := []etcd.Cmp{} // list matching the desired state
|
|
ops := []etcd.Op{} // list of ops in this txn
|
|
els := []etcd.Op{}
|
|
if urls != nil {
|
|
data := urls.String() // value is usually a peer "serverURL"
|
|
// XXX: bug: https://github.com/etcd-io/etcd/issues/10566
|
|
// XXX: reverse things with els to workaround the bug :(
|
|
//ifs = append(ifs, etcd.Compare(etcd.Value(key), "!=", data)) // desired state
|
|
//ops = append(ops, etcd.OpPut(key, data, etcd.WithLease(obj.leaseID)))
|
|
ifs = append(ifs, etcd.Compare(etcd.Value(key), "=", data)) // desired state
|
|
ifs = append(ifs, etcd.Compare(etcd.LeaseValue(key), "=", obj.leaseID))
|
|
els = append(ops, etcd.OpPut(key, data, etcd.WithLease(obj.leaseID)))
|
|
|
|
} else { // delete message if set to erase
|
|
ifs = append(ifs, etcdutil.KeyExists(key)) // desired state
|
|
ops = append(ops, etcd.OpDelete(key))
|
|
}
|
|
|
|
_, err := obj.client.Txn(ctx, ifs, ops, els)
|
|
msg := "volunteering failed"
|
|
if urls == nil {
|
|
msg = "unvolunteering failed"
|
|
}
|
|
return errwrap.Wrapf(err, msg)
|
|
}
|
|
|
|
// nominate nominates a particular client to be a server (peer). If you specify
|
|
// a nil value for urls, then this will unnominate that member.
|
|
func (obj *EmbdEtcd) nominate(ctx context.Context, hostname string, urls etcdtypes.URLs) error {
|
|
if obj.Debug {
|
|
if urls == nil {
|
|
obj.Logf("unnominate(%s)...", hostname)
|
|
defer obj.Logf("unnominate(%s): done!", hostname)
|
|
} else {
|
|
obj.Logf("nominate(%s): %s", hostname, urls.String())
|
|
defer obj.Logf("nominate(%s): done!", hostname)
|
|
}
|
|
}
|
|
// nominate someone to be a server
|
|
key := fmt.Sprintf(obj.NS+nominatedPathFmt, hostname)
|
|
ifs := []etcd.Cmp{} // list matching the desired state
|
|
ops := []etcd.Op{} // list of ops in this txn
|
|
els := []etcd.Op{}
|
|
if urls != nil {
|
|
data := urls.String()
|
|
// XXX: bug: https://github.com/etcd-io/etcd/issues/10566
|
|
// XXX: reverse things with els to workaround the bug :(
|
|
//ifs = append(ifs, etcd.Compare(etcd.Value(key), "!=", data)) // desired state
|
|
//ops = append(ops, etcd.OpPut(key, data)) // TODO: add a TTL? (etcd.WithLease)
|
|
ifs = append(ifs, etcd.Compare(etcd.Value(key), "=", data)) // desired state
|
|
els = append(ops, etcd.OpPut(key, data)) // TODO: add a TTL? (etcd.WithLease)
|
|
|
|
} else { // delete message if set to erase
|
|
ifs = append(ifs, etcdutil.KeyExists(key)) // desired state
|
|
ops = append(ops, etcd.OpDelete(key))
|
|
}
|
|
|
|
_, err := obj.client.Txn(ctx, ifs, ops, els)
|
|
msg := "nominate failed"
|
|
if urls == nil {
|
|
msg = "unnominate failed"
|
|
}
|
|
return errwrap.Wrapf(err, msg)
|
|
}
|
|
|
|
// advertise idempotently advertises the list of available client endpoints for
|
|
// the given member. If you specify a nil value for urls, then this will remove
|
|
// that member.
|
|
func (obj *EmbdEtcd) advertise(ctx context.Context, hostname string, urls etcdtypes.URLs) error {
|
|
if obj.Debug {
|
|
if urls == nil {
|
|
obj.Logf("unadvertise(%s)...", hostname)
|
|
defer obj.Logf("unadvertise(%s): done!", hostname)
|
|
} else {
|
|
obj.Logf("advertise(%s): %s", hostname, urls.String())
|
|
defer obj.Logf("advertise(%s): done!", hostname)
|
|
}
|
|
}
|
|
// advertise endpoints
|
|
key := fmt.Sprintf(obj.NS+endpointsPathFmt, hostname)
|
|
ifs := []etcd.Cmp{} // list matching the desired state
|
|
ops := []etcd.Op{} // list of ops in this txn
|
|
els := []etcd.Op{}
|
|
if urls != nil {
|
|
data := urls.String() // value is usually a "clientURL"
|
|
// XXX: bug: https://github.com/etcd-io/etcd/issues/10566
|
|
// XXX: reverse things with els to workaround the bug :(
|
|
//ifs = append(ifs, etcd.Compare(etcd.Value(key), "!=", data)) // desired state
|
|
//ops = append(ops, etcd.OpPut(key, data, etcd.WithLease(obj.leaseID)))
|
|
ifs = append(ifs, etcd.Compare(etcd.Value(key), "=", data)) // desired state
|
|
ifs = append(ifs, etcd.Compare(etcd.LeaseValue(key), "=", obj.leaseID))
|
|
els = append(ops, etcd.OpPut(key, data, etcd.WithLease(obj.leaseID)))
|
|
} else { // delete in this case
|
|
ifs = append(ifs, etcdutil.KeyExists(key)) // desired state
|
|
ops = append(ops, etcd.OpDelete(key))
|
|
}
|
|
|
|
_, err := obj.client.Txn(ctx, ifs, ops, els)
|
|
msg := "advertising failed"
|
|
if urls == nil {
|
|
msg = "unadvertising failed"
|
|
}
|
|
return errwrap.Wrapf(err, msg)
|
|
}
|
|
|
|
// getVolunteers returns a urls map of available etcd server volunteers.
|
|
func (obj *EmbdEtcd) getVolunteers(ctx context.Context) (etcdtypes.URLsMap, error) {
|
|
if obj.Debug {
|
|
obj.Logf("getVolunteers()")
|
|
defer obj.Logf("getVolunteers(): done!")
|
|
}
|
|
p := obj.NS + VolunteerPath
|
|
keyMap, err := obj.client.Get(ctx, p, etcd.WithPrefix())
|
|
if err != nil {
|
|
return nil, errwrap.Wrapf(err, "can't get peer volunteers")
|
|
}
|
|
volunteers := make(etcdtypes.URLsMap)
|
|
for key, val := range keyMap { // loop through directory of volunteers
|
|
if !strings.HasPrefix(key, p) {
|
|
continue
|
|
}
|
|
name := key[len(p):] // get name of volunteer
|
|
if val == "" { // skip "erased" values
|
|
continue
|
|
}
|
|
urls, err := etcdtypes.NewURLs(strings.Split(val, ","))
|
|
if err != nil {
|
|
return nil, errwrap.Wrapf(err, "data format error")
|
|
}
|
|
volunteers[name] = urls // add to map
|
|
}
|
|
return volunteers, nil
|
|
}
|
|
|
|
// getNominated returns a urls map of nominated etcd server volunteers.
|
|
// NOTE: I know 'nominees' might be more correct, but is less consistent here
|
|
func (obj *EmbdEtcd) getNominated(ctx context.Context) (etcdtypes.URLsMap, error) {
|
|
if obj.Debug {
|
|
obj.Logf("getNominated()")
|
|
defer obj.Logf("getNominated(): done!")
|
|
}
|
|
p := obj.NS + NominatedPath
|
|
keyMap, err := obj.client.Get(ctx, p, etcd.WithPrefix()) // map[string]string, bool
|
|
if err != nil {
|
|
return nil, errwrap.Wrapf(err, "can't get nominated peers")
|
|
}
|
|
nominated := make(etcdtypes.URLsMap)
|
|
for key, val := range keyMap { // loop through directory of nominated
|
|
if !strings.HasPrefix(key, p) {
|
|
continue
|
|
}
|
|
name := key[len(p):] // get name of nominee
|
|
if val == "" { // skip "erased" values
|
|
continue
|
|
}
|
|
urls, err := etcdtypes.NewURLs(strings.Split(val, ","))
|
|
if err != nil {
|
|
return nil, errwrap.Wrapf(err, "data format error")
|
|
}
|
|
nominated[name] = urls // add to map
|
|
}
|
|
return nominated, nil
|
|
}
|
|
|
|
// getEndpoints returns a urls map of available endpoints for clients.
|
|
func (obj *EmbdEtcd) getEndpoints(ctx context.Context) (etcdtypes.URLsMap, error) {
|
|
if obj.Debug {
|
|
obj.Logf("getEndpoints()")
|
|
defer obj.Logf("getEndpoints(): done!")
|
|
}
|
|
p := obj.NS + EndpointsPath
|
|
keyMap, err := obj.client.Get(ctx, p, etcd.WithPrefix())
|
|
if err != nil {
|
|
return nil, errwrap.Wrapf(err, "can't get client endpoints")
|
|
}
|
|
endpoints := make(etcdtypes.URLsMap)
|
|
for key, val := range keyMap { // loop through directory of endpoints
|
|
if !strings.HasPrefix(key, p) {
|
|
continue
|
|
}
|
|
name := key[len(p):] // get name of volunteer
|
|
if val == "" { // skip "erased" values
|
|
continue
|
|
}
|
|
urls, err := etcdtypes.NewURLs(strings.Split(val, ","))
|
|
if err != nil {
|
|
return nil, errwrap.Wrapf(err, "data format error")
|
|
}
|
|
endpoints[name] = urls // add to map
|
|
}
|
|
return endpoints, nil
|
|
}
|