187 lines
4.6 KiB
Go
187 lines
4.6 KiB
Go
// Package bootstrap deals with the parsing and creation of bootstrap.json
|
|
// files. It also contains some helpers which rely on bootstrap data.
|
|
package bootstrap
|
|
|
|
import (
|
|
"cmp"
|
|
"encoding/json"
|
|
"fmt"
|
|
"isle/nebula"
|
|
"isle/toolkit"
|
|
"maps"
|
|
"net/netip"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
|
|
"dev.mediocregopher.com/mediocre-go-lib.git/mctx"
|
|
)
|
|
|
|
// StateDirPath returns the path within the user's state directory where the
|
|
// bootstrap file is stored.
|
|
func StateDirPath(dataDirPath string) string {
|
|
return filepath.Join(dataDirPath, "bootstrap.json")
|
|
}
|
|
|
|
// CreationParams are general parameters used when creating a new network. These
|
|
// are available to all hosts within the network via their bootstrap files.
|
|
type CreationParams struct {
|
|
ID string
|
|
Name string
|
|
Domain string
|
|
}
|
|
|
|
// NewCreationParams instantiates and returns a CreationParams.
|
|
func NewCreationParams(name, domain string) CreationParams {
|
|
return CreationParams{
|
|
ID: toolkit.RandStr(32),
|
|
Name: name,
|
|
Domain: domain,
|
|
}
|
|
}
|
|
|
|
// Annotate implements the mctx.Annotator interface.
|
|
func (p CreationParams) Annotate(aa mctx.Annotations) {
|
|
aa["networkID"] = p.ID
|
|
aa["networkName"] = p.Name
|
|
aa["networkDomain"] = p.Domain
|
|
}
|
|
|
|
// Matches returns true if the given string matches some aspect of the
|
|
// CreationParams.
|
|
func (p CreationParams) Matches(str string) bool {
|
|
if strings.HasPrefix(p.ID, str) {
|
|
return true
|
|
}
|
|
|
|
if strings.EqualFold(p.Name, str) {
|
|
return true
|
|
}
|
|
|
|
if strings.EqualFold(p.Domain, str) {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Conflicts returns true if either CreationParams has some parameter which
|
|
// overlaps with that of the other.
|
|
func (p CreationParams) Conflicts(p2 CreationParams) bool {
|
|
if p.ID == p2.ID {
|
|
return true
|
|
}
|
|
|
|
if strings.EqualFold(p.Name, p2.Name) {
|
|
return true
|
|
}
|
|
|
|
if strings.EqualFold(p.Domain, p2.Domain) {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Bootstrap contains all information which is needed by a host daemon to join a
|
|
// network on boot.
|
|
type Bootstrap struct {
|
|
NetworkCreationParams CreationParams
|
|
CAPublicCredentials nebula.CAPublicCredentials
|
|
|
|
PrivateCredentials nebula.HostPrivateCredentials
|
|
HostAssigned `json:"-"`
|
|
SignedHostAssigned nebula.Signed[HostAssigned] // signed by CA
|
|
|
|
Hosts map[nebula.HostName]Host
|
|
}
|
|
|
|
// New initializes and returns a new Bootstrap file for a new host.
|
|
//
|
|
// TODO in the resulting bootstrap only include this host and hosts which are
|
|
// necessary for connecting to nebula/garage. Remember to immediately re-poll
|
|
// garage for the full hosts list during network joining.
|
|
func New(
|
|
caCreds nebula.CACredentials,
|
|
adminCreationParams CreationParams,
|
|
existingHosts map[nebula.HostName]Host,
|
|
name nebula.HostName,
|
|
ip netip.Addr,
|
|
) (
|
|
Bootstrap, error,
|
|
) {
|
|
host, hostPrivCreds, err := NewHost(caCreds, name, ip)
|
|
if err != nil {
|
|
return Bootstrap{}, fmt.Errorf("creating host: %w", err)
|
|
}
|
|
|
|
signedAssigned, err := nebula.Sign(
|
|
host.HostAssigned, caCreds.SigningPrivateKey,
|
|
)
|
|
if err != nil {
|
|
return Bootstrap{}, fmt.Errorf("signing assigned fields: %w", err)
|
|
}
|
|
|
|
existingHosts = maps.Clone(existingHosts)
|
|
existingHosts[name] = host
|
|
|
|
return Bootstrap{
|
|
NetworkCreationParams: adminCreationParams,
|
|
CAPublicCredentials: caCreds.Public,
|
|
PrivateCredentials: hostPrivCreds,
|
|
HostAssigned: host.HostAssigned,
|
|
SignedHostAssigned: signedAssigned,
|
|
Hosts: existingHosts,
|
|
}, nil
|
|
}
|
|
|
|
// UnmarshalJSON implements the json.Unmarshaler interface. It will
|
|
// automatically populate the HostAssigned field by unwrapping the
|
|
// SignedHostAssigned field.
|
|
func (b *Bootstrap) UnmarshalJSON(data []byte) error {
|
|
type inner Bootstrap
|
|
|
|
err := json.Unmarshal(data, (*inner)(b))
|
|
if err != nil {
|
|
return fmt.Errorf("json unmarshaling: %w", err)
|
|
}
|
|
|
|
// Generally this will be filled, but during unit tests we sometimes leave
|
|
// it empty for convenience.
|
|
if b.SignedHostAssigned != nil {
|
|
b.HostAssigned, err = b.SignedHostAssigned.Unwrap(
|
|
b.CAPublicCredentials.SigningKey,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("unwrapping HostAssigned: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ThisHost is a shortcut for b.Hosts[b.HostName], but will panic if the
|
|
// HostName isn't found in the Hosts map.
|
|
func (b Bootstrap) ThisHost() Host {
|
|
host, ok := b.Hosts[b.Name]
|
|
if !ok {
|
|
panic(fmt.Sprintf("hostname %q not defined in bootstrap's hosts", b.Name))
|
|
}
|
|
|
|
return host
|
|
}
|
|
|
|
// HostsOrdered returns the Hosts as a slice in a deterministic order.
|
|
func (b Bootstrap) HostsOrdered() []Host {
|
|
hosts := make([]Host, 0, len(b.Hosts))
|
|
for _, host := range b.Hosts {
|
|
hosts = append(hosts, host)
|
|
}
|
|
|
|
slices.SortFunc(hosts, func(a, b Host) int {
|
|
return cmp.Compare(a.Name, b.Name)
|
|
})
|
|
|
|
return hosts
|
|
}
|