浏览代码

:wrench: Add service controller

Adds a set of experimental controller library to wrap and give more
syntax sugar when writing things on top of the edgeVPN API.

Controllers could be used to deploy services, and, for example,
orchestrate nodes joining a network.
Ettore Di Giacinto 3 年之前
父节点
当前提交
2db174d468
共有 6 个文件被更改,包括 666 次插入3 次删除
  1. 360 0
      api/client/service/node.go
  2. 56 0
      api/client/service/process.go
  3. 123 0
      api/client/service/role.go
  4. 112 0
      api/client/service/service.go
  5. 4 1
      go.mod
  6. 11 2
      go.sum

+ 360 - 0
api/client/service/node.go

@@ -0,0 +1,360 @@
+// Copyright © 2021-2022 Ettore Di Giacinto <[email protected]>
+//
+// 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 2 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 service
+
+import (
+	"context"
+	"embed"
+	"encoding/base64"
+	"fmt"
+	"io"
+	"io/fs"
+	"io/ioutil"
+	"os"
+	"path"
+	"path/filepath"
+	"time"
+
+	"github.com/ipfs/go-log"
+	"gopkg.in/yaml.v2"
+
+	edgeVPNClient "github.com/mudler/edgevpn/api/client"
+	"github.com/mudler/edgevpn/pkg/edgevpn"
+)
+
+// Node is the service Node.
+// It have a set of defined available roles which nodes
+// in a network can take. It takes a network token or either generates one
+type Node struct {
+	stateDir                                 string
+	tokenFile                                string
+	uuid                                     string
+	networkToken                             string
+	apiAddress                               string
+	defaultRoles, persistentRoles, stopRoles string
+
+	assets []string
+	fs     embed.FS
+	client *Client
+	roles  map[Role]func(c *RoleConfig) error
+
+	logger log.StandardLogger
+}
+
+// WithRoles defines a set of role keys
+func WithRoles(k ...RoleKey) Option {
+	return func(mm *Node) error {
+		m := map[Role]func(c *RoleConfig) error{}
+		for _, kk := range k {
+			m[kk.Role] = kk.RoleHandler
+		}
+		mm.roles = m
+		return nil
+	}
+}
+
+// WithFS accepts an embed.FS file system where to copy binaries from
+func WithFS(fs embed.FS) Option {
+	return func(k *Node) error {
+		k.fs = fs
+		return nil
+	}
+}
+
+// WithAssets is a list of assets to copy to a temporary state dir from the embedded FS
+// It is used in conjunction with WithFS to ease out binary embedding
+func WithAssets(assets ...string) Option {
+	return func(k *Node) error {
+		k.assets = assets
+		return nil
+	}
+}
+
+// WithLogger defines a logger to be used across the whole execution
+func WithLogger(l log.StandardLogger) Option {
+	return func(k *Node) error {
+		k.logger = l
+		return nil
+	}
+}
+
+// WithStopRoles allows to set a list of comma separated roles that can be applied during cleanup
+func WithStopRoles(roles string) Option {
+	return func(k *Node) error {
+		k.stopRoles = roles
+		return nil
+	}
+}
+
+// WithPersistentRoles allows to set a list of comma separated roles that can is applied persistently
+func WithPersistentRoles(roles string) Option {
+	return func(k *Node) error {
+		k.persistentRoles = roles
+		return nil
+	}
+}
+
+// WithDefaultRoles allows to set a list of comma separated roles prefixed for the node.
+// Note, by setting this the node will refuse any assigned role
+func WithDefaultRoles(roles string) Option {
+	return func(k *Node) error {
+		k.defaultRoles = roles
+		return nil
+	}
+}
+
+// WithNetworkToken allows to set a network token.
+// If not set, it is automatically generated
+func WithNetworkToken(token string) Option {
+	return func(k *Node) error {
+		k.networkToken = token
+		return nil
+	}
+}
+
+// WithAPIAddress sets the EdgeVPN API address
+func WithAPIAddress(s string) Option {
+	return func(k *Node) error {
+		k.apiAddress = s
+		return nil
+	}
+}
+
+// WithStateDir sets the node state directory.
+// It will contain the unpacked assets (if any) and the
+// process states generated by the roles.
+func WithStateDir(s string) Option {
+	return func(k *Node) error {
+		k.stateDir = s
+		return nil
+	}
+}
+
+// WithUUID sets a node UUID
+func WithUUID(s string) Option {
+	return func(k *Node) error {
+		k.uuid = s
+		return nil
+	}
+}
+
+// WithTokenfile sets a token file.
+// If a token file and a network token is not found it is written
+// to such file
+func WithTokenfile(s string) Option {
+	return func(k *Node) error {
+		k.tokenFile = s
+		return nil
+	}
+}
+
+// WithClient sets a service client
+func WithClient(e *Client) Option {
+	return func(o *Node) error {
+		o.client = e
+		return nil
+	}
+}
+
+// Option is a Node option
+type Option func(k *Node) error
+
+// NewNode returns a new service Node
+// The service Node can have role applied which are
+// polled by the API.
+// This allows to bootstrap services using the API to coordinate nodes
+// and apply roles afterwards (e.g. start vpn with a dynamically received IP, etc. )
+func NewNode(o ...Option) (*Node, error) {
+	k := &Node{
+		stateDir:   "/tmp/Node",
+		apiAddress: "localhost:7070",
+	}
+	for _, oo := range o {
+		err := oo(k)
+		if err != nil {
+			return nil, err
+		}
+	}
+	return k, nil
+}
+
+func (k *Node) copyBinary() {
+	for _, a := range k.assets {
+		b := path.Base(a)
+		aa := NewProcessController(k.stateDir)
+		p := aa.BinaryPath(b)
+		if _, err := os.Stat(p); err != nil {
+			os.MkdirAll(filepath.Join(k.stateDir, "bin"), os.ModePerm)
+			f, err := k.fs.Open(a)
+			if err != nil {
+				panic(err)
+			}
+			if err := copyFileContents(f, p); err != nil {
+				panic(err)
+			}
+		}
+	}
+}
+
+func copyFileContents(in fs.File, dst string) (err error) {
+	defer in.Close()
+	out, err := os.Create(dst)
+	if err != nil {
+		return
+	}
+	defer func() {
+		cerr := out.Close()
+		if err == nil {
+			err = cerr
+		}
+	}()
+	if _, err = io.Copy(out, in); err != nil {
+		return
+	}
+	err = out.Sync()
+
+	os.Chmod(dst, 0755)
+	return
+}
+
+// Stop stops a node by calling the stop roles
+func (k *Node) Stop() {
+	k.execRoles(k.stopRoles)
+}
+
+// Clean stops and cleanup a node
+func (k *Node) Clean() {
+	k.client.Clean()
+	k.Stop()
+	if k.stateDir != "" {
+		os.RemoveAll(k.stateDir)
+	}
+}
+
+func (k *Node) prepare() error {
+	k.copyBinary()
+
+	if k.tokenFile != "" {
+		f, err := ioutil.ReadFile(k.tokenFile)
+		if err == nil {
+			k.networkToken = string(f)
+		}
+	}
+
+	if k.networkToken == "" {
+
+		newData := edgevpn.GenerateNewConnectionData()
+		bytesData, err := yaml.Marshal(newData)
+		if err != nil {
+			return err
+		}
+
+		token := base64.StdEncoding.EncodeToString(bytesData)
+
+		k.logger.Infof("Token generated, writing to '%s'", k.tokenFile)
+		ioutil.WriteFile(k.tokenFile, []byte(token), os.ModePerm)
+		k.networkToken = token
+	}
+
+	k.execRoles(k.persistentRoles)
+
+	if k.client == nil {
+		k.client = NewClient("Node",
+			edgeVPNClient.NewClient(edgeVPNClient.WithHost(fmt.Sprintf("http://%s", k.apiAddress))))
+	}
+	return nil
+}
+
+type roleMessage struct {
+	Role Role
+}
+
+func (k *Node) options() (r []RoleOption) {
+	r = []RoleOption{
+		WithRoleLogger(k.logger),
+		WithRole(k.roles),
+		WithRoleClient(k.client),
+		WithRoleUUID(k.uuid),
+		WithRoleStateDir(k.stateDir),
+		WithRoleAPIAddress(k.apiAddress),
+		WithRoleToken(k.networkToken),
+	}
+	return
+}
+
+func (k *Node) execRoles(s string) {
+	r := Role(s)
+	k.logger.Info("Applying role", r)
+
+	r.Apply(k.options()...)
+}
+
+// Start starts the node with the context
+func (k *Node) Start(ctx context.Context) error {
+	// prepare binaries and start the default roles
+	if err := k.prepare(); err != nil {
+		return err
+	}
+
+	k.client.Advertize(k.uuid)
+
+	i := 0
+	for {
+		select {
+		case <-ctx.Done():
+			return nil
+		default:
+			i++
+			time.Sleep(10 * time.Second)
+			if i%2 == 0 {
+				k.client.Advertize(k.uuid)
+			}
+
+			uuids, _ := k.client.ActiveNodes()
+
+			for _, n := range uuids {
+				k.logger.Infof("Active: '%s'", n)
+			}
+
+			if k.persistentRoles != "" {
+				k.execRoles(k.persistentRoles)
+			}
+
+			// If we have default roles, executes them and continue
+			if k.defaultRoles != "" {
+				k.execRoles(k.defaultRoles)
+				continue
+			}
+
+			// Not enough nodes
+			if len(uuids) <= 1 {
+				k.logger.Info("not enough nodes available, sleeping...")
+				continue
+			}
+
+			// Enough active nodes.
+			d, err := k.client.Get("role", k.uuid)
+			if err == nil {
+				k.logger.Info("Roles assigned")
+				k.execRoles(d)
+			} else {
+				// we don't have a role yet, sleeping
+				k.logger.Info("No role assigned, sleeping")
+			}
+
+		}
+	}
+}

+ 56 - 0
api/client/service/process.go

@@ -0,0 +1,56 @@
+// Copyright © 2021-2022 Ettore Di Giacinto <[email protected]>
+//
+// 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 2 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 service
+
+import (
+	"os/exec"
+	"path/filepath"
+
+	process "github.com/mudler/go-processmanager"
+)
+
+// NewProcessController returns a new process controller associated with the state directory
+func NewProcessController(statedir string) *ProcessController {
+	return &ProcessController{stateDir: statedir}
+}
+
+// ProcessController syntax sugar around go-processmanager
+type ProcessController struct {
+	stateDir string
+}
+
+// Process returns a process associated within binaries inside the state dir
+func (a *ProcessController) Process(state, p string, args ...string) *process.Process {
+	return process.New(
+		process.WithName(a.BinaryPath(p)),
+		process.WithArgs(args...),
+		process.WithStateDir(filepath.Join(a.stateDir, "proc", state)),
+	)
+}
+
+// BinaryPath returns the binary path of the program requested as argument.
+// The binary path is relative to the process state directory
+func (a *ProcessController) BinaryPath(b string) string {
+	return filepath.Join(a.stateDir, "bin", b)
+}
+
+// Run simply runs a command from a binary in the state directory
+func (a *ProcessController) Run(command string, args ...string) (string, error) {
+	cmd := exec.Command(a.BinaryPath(command), args...)
+	out, err := cmd.CombinedOutput()
+
+	return string(out), err
+}

+ 123 - 0
api/client/service/role.go

@@ -0,0 +1,123 @@
+// Copyright © 2021-2022 Ettore Di Giacinto <[email protected]>
+//
+// 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 2 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 service
+
+import (
+	"strings"
+
+	"github.com/ipfs/go-log"
+)
+
+// Role is a service role.
+// It is identified by a unique string which is sent over the wire
+// and streamed to/from the clients.
+// Roles can be applied either directly, or assigned within roles in the API
+type Role string
+
+// RoleConfig is the role config structure, which holds all the objects that can be used by a Role
+type RoleConfig struct {
+	Client                                              *Client
+	UUID, ServiceID, StateDir, APIAddress, NetworkToken string
+	Logger                                              log.StandardLogger
+
+	roles map[Role]func(c *RoleConfig) error
+}
+
+// RoleOption is a role option
+type RoleOption func(c *RoleConfig)
+
+// RoleKey is an association between a Role(string) and a Handler which actually
+// fullfills the role
+type RoleKey struct {
+	RoleHandler func(c *RoleConfig) error
+	Role        Role
+}
+
+// WithRole sets the available roles
+func WithRole(f map[Role]func(c *RoleConfig) error) RoleOption {
+	return func(c *RoleConfig) {
+		c.roles = f
+	}
+}
+
+// WithRoleLogger sets a logger for the role action
+func WithRoleLogger(l log.StandardLogger) RoleOption {
+	return func(c *RoleConfig) {
+		c.Logger = l
+	}
+}
+
+// WithRoleUUID sets the UUID which performs the role
+func WithRoleUUID(u string) RoleOption {
+	return func(c *RoleConfig) {
+		c.UUID = u
+	}
+}
+
+// WithRoleStateDir sets the statedir for the role
+func WithRoleStateDir(s string) RoleOption {
+	return func(c *RoleConfig) {
+		c.StateDir = s
+	}
+}
+
+// WithRoleToken sets the network token which can be used by the role
+func WithRoleToken(s string) RoleOption {
+	return func(c *RoleConfig) {
+		c.NetworkToken = s
+	}
+}
+
+// WithRoleAPIAddress sets the API Address used during the execution
+func WithRoleAPIAddress(s string) RoleOption {
+	return func(c *RoleConfig) {
+		c.APIAddress = s
+	}
+}
+
+// WithRoleServiceID sets a role service ID
+func WithRoleServiceID(s string) RoleOption {
+	return func(c *RoleConfig) {
+		c.ServiceID = s
+	}
+}
+
+// WithRoleClient sets a client for a role
+func WithRoleClient(e *Client) RoleOption {
+	return func(c *RoleConfig) {
+		c.Client = e
+	}
+}
+
+// Apply applies a role and takes a list of options
+func (rr Role) Apply(opts ...RoleOption) {
+	c := &RoleConfig{}
+	for _, o := range opts {
+		o(c)
+	}
+
+	for _, role := range strings.Split(string(rr), ",") {
+		r := Role(role)
+		if f, exists := c.roles[r]; exists {
+			c.Logger.Info("Role loaded. Applying ", r)
+			if err := f(c); err != nil {
+				c.Logger.Warning("Failed applying role", role, err)
+			}
+		} else {
+			c.Logger.Warn("Unknown role: ", r)
+		}
+	}
+}

+ 112 - 0
api/client/service/service.go

@@ -0,0 +1,112 @@
+// Copyright © 2021-2022 Ettore Di Giacinto <[email protected]>
+//
+// 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 2 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 service
+
+import (
+	"fmt"
+	"strings"
+	"time"
+
+	edgeVPNClient "github.com/mudler/edgevpn/api/client"
+)
+
+// Client is a wrapper of an edgeVPN client
+// with additional metadata and syntax sugar
+type Client struct {
+	serviceID string
+	*edgeVPNClient.Client
+}
+
+// NewClient returns a new client with an associated service ID
+func NewClient(serviceID string, c *edgeVPNClient.Client) *Client {
+	return &Client{serviceID: serviceID, Client: c}
+}
+
+// ListItems returns list of items associated with the serviceID and the given suffix
+func (c Client) ListItems(serviceID, suffix string) (strs []string, err error) {
+	buckets, err := c.Client.GetBucketKeys(serviceID)
+	if err != nil {
+		return
+	}
+	for _, b := range buckets {
+		if strings.HasSuffix(b, suffix) {
+			b = strings.ReplaceAll(b, "-"+suffix, "")
+			strs = append(strs, b)
+		}
+	}
+	return
+}
+
+type advertizeMessage struct {
+	Time time.Time
+}
+
+// Advertize advertize the given uuid to the ledger
+func (c Client) Advertize(uuid string) error {
+	return c.Client.Put(c.serviceID, fmt.Sprintf("%s-uuid", uuid), advertizeMessage{Time: time.Now()})
+}
+
+// ActiveNodes returns a list of active nodes
+func (c Client) ActiveNodes() (active []string, err error) {
+	uuids, err := c.ListItems(c.serviceID, "uuid")
+	if err != nil {
+		return
+	}
+	for _, u := range uuids {
+		var d advertizeMessage
+		res, err := c.Client.GetBucketKey(c.serviceID, fmt.Sprintf("%s-uuid", u))
+		if err != nil {
+			continue
+		}
+		res.Unmarshal(&d)
+
+		if d.Time.Add(2 * time.Minute).After(time.Now()) {
+			active = append(active, u)
+		}
+	}
+	return
+}
+
+// Clean cleans up the serviceID associated data
+func (c Client) Clean() error {
+	return c.Client.DeleteBucket(c.serviceID)
+}
+
+func reverse(ss []string) {
+	last := len(ss) - 1
+	for i := 0; i < len(ss)/2; i++ {
+		ss[i], ss[last-i] = ss[last-i], ss[i]
+	}
+}
+
+// Get returns generic data from the API
+// e.g. get("ip", uuid)
+func (c Client) Get(args ...string) (string, error) {
+	reverse(args)
+	key := strings.Join(args, "-")
+	var role string
+	d, err := c.Client.GetBucketKey(c.serviceID, key)
+	if err == nil {
+		d.Unmarshal(&role)
+	}
+	return role, err
+}
+
+// Set generic data to the API
+// e.g. set("ip", uuid, "value")
+func (c Client) Set(thing, uuid, value string) error {
+	return c.Client.Put(c.serviceID, fmt.Sprintf("%s-%s", uuid, thing), value)
+}

+ 4 - 1
go.mod

@@ -22,11 +22,14 @@ require (
 	github.com/libp2p/go-libp2p-kad-dht v0.15.0
 	github.com/libp2p/go-libp2p-pubsub v0.6.0
 	github.com/mattn/go-colorable v0.1.12 // indirect
+	github.com/miekg/dns v1.1.45 // indirect
 	github.com/mudler/go-isterminal v0.0.0-20211031135732-5e4e06fc5a58
+	github.com/mudler/go-processmanager v0.0.0-20211226182900-899fbb0b97f6
 	github.com/multiformats/go-base32 v0.0.4 // indirect
 	github.com/multiformats/go-multiaddr v0.4.1
+	github.com/multiformats/go-multicodec v0.4.0 // indirect
 	github.com/onsi/ginkgo v1.16.5
-	github.com/onsi/gomega v1.13.0
+	github.com/onsi/gomega v1.16.0
 	github.com/peterbourgon/diskv v2.0.1+incompatible
 	github.com/pkg/errors v0.9.1
 	github.com/prometheus/common v0.32.1 // indirect

+ 11 - 2
go.sum

@@ -776,7 +776,8 @@ github.com/miekg/dns v1.1.28/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7
 github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
 github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg=
 github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
-github.com/miekg/dns v1.1.44/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
+github.com/miekg/dns v1.1.45 h1:g5fRIhm9nx7g8osrAvgb16QJfmyMsyOCb+J7LSv+Qzk=
+github.com/miekg/dns v1.1.45/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
 github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8=
 github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms=
 github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc=
@@ -812,6 +813,8 @@ github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
 github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
 github.com/mudler/go-isterminal v0.0.0-20211031135732-5e4e06fc5a58 h1:bMXak5giXxc++J/TUY7qW24D8ASxqLQRqOoduuFgdIM=
 github.com/mudler/go-isterminal v0.0.0-20211031135732-5e4e06fc5a58/go.mod h1:bZC4k76DbPOxOcMq6Z9oEKAZrOhsfh9jAZ9Hu3qVAQI=
+github.com/mudler/go-processmanager v0.0.0-20211226182900-899fbb0b97f6 h1:LiWUDzDh/DkFYhUeIIdWPuMt/LsEVD3vc/QKDVXlWAY=
+github.com/mudler/go-processmanager v0.0.0-20211226182900-899fbb0b97f6/go.mod h1:HGGAOJhipApckwNV8ZTliRJqxctUv3xRY+zbQEwuytc=
 github.com/multiformats/go-base32 v0.0.3/go.mod h1:pLiuGC8y0QR3Ue4Zug5UzK9LjgbkL8NSQj0zQ5Nz/AA=
 github.com/multiformats/go-base32 v0.0.4 h1:+qMh4a2f37b4xTNs6mqitDinryCI+tfO2dRVMN9mjSE=
 github.com/multiformats/go-base32 v0.0.4/go.mod h1:jNLFzjPZtp3aIARHbJRZIaPuspdH0J6q39uUM5pnABM=
@@ -853,6 +856,8 @@ github.com/multiformats/go-multibase v0.0.3/go.mod h1:5+1R4eQrT3PkYZ24C3W2Ue2tPw
 github.com/multiformats/go-multicodec v0.2.0/go.mod h1:/y4YVwkfMyry5kFbMTbLJKErhycTIftytRV+llXdyS4=
 github.com/multiformats/go-multicodec v0.3.0 h1:tstDwfIjiHbnIjeM5Lp+pMrSeN+LCMsEwOrkPmWm03A=
 github.com/multiformats/go-multicodec v0.3.0/go.mod h1:qGGaQmioCDh+TeFOnxrbU0DaIPw8yFgAZgFG0V7p1qQ=
+github.com/multiformats/go-multicodec v0.4.0 h1:fbqb6ky7erjdD+/zaEBJgZWu1i8D6i/wmPywGK7sdow=
+github.com/multiformats/go-multicodec v0.4.0/go.mod h1:1Hj/eHRaVWSXiSNNfcEPcwZleTmdNP81xlxDLnWU9GQ=
 github.com/multiformats/go-multihash v0.0.1/go.mod h1:w/5tugSrLEbWqlcgJabL3oHFKTwfvkofsjW2Qa1ct4U=
 github.com/multiformats/go-multihash v0.0.5/go.mod h1:lt/HCbqlQwlPBz7lv0sQCdtfcMtlJvakRUn/0Ual8po=
 github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew=
@@ -906,8 +911,9 @@ github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
 github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
-github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak=
 github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY=
+github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c=
+github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
 github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
 github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
@@ -1278,6 +1284,7 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
 golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
 golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
 golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@@ -1465,6 +1472,7 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.8 h1:P1HhGGuLW4aAclzjtmJdf0mJOjVUZUzOTqkAkWL+l6w=
 golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1586,6 +1594,7 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
 gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473/go.mod h1:N1eN2tsCx0Ydtgjl4cqmbRCsY4/+z4cYDeqwZTk6zog=
 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
 gopkg.in/src-d/go-cli.v0 v0.0.0-20181105080154-d492247bbc0d/go.mod h1:z+K8VcOYVYcSwSjGebuDL6176A1XskgbtNl64NSg+n8=
 gopkg.in/src-d/go-log.v1 v1.0.1/go.mod h1:GN34hKP0g305ysm2/hctJ0Y8nWP3zxXXJ8GFabTyABE=