isle/go/bootstrap/bootstrap.go

166 lines
4.1 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 (
"crypto/sha512"
"encoding/json"
"fmt"
"io"
"isle/admin"
"isle/garage"
"isle/nebula"
"net"
"os"
"path/filepath"
"sort"
)
// 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")
}
// AppDirPath returns the path within the AppDir where an embedded bootstrap
// file might be found.
func AppDirPath(appDirPath string) string {
return filepath.Join(appDirPath, "share/bootstrap.json")
}
// Garage contains parameters needed to connect to and use the garage cluster.
type Garage struct {
// TODO this should be part of some new configuration section related to
// secrets which may or may not be granted to this host
RPCSecret string
AdminToken string
// TODO this should be part of admin.CreationParams
GlobalBucketS3APICredentials garage.S3APICredentials
}
// Bootstrap is used for accessing all information contained within a
// bootstrap.json file.
type Bootstrap struct {
AdminCreationParams admin.CreationParams
CAPublicCredentials nebula.CAPublicCredentials
Garage Garage
PrivateCredentials nebula.HostPrivateCredentials
HostAssigned `json:"-"`
SignedHostAssigned nebula.Signed[HostAssigned] // signed by CA
Hosts map[string]Host
}
// New initializes and returns a new Bootstrap file for a new host. This
// function assigns Hosts an empty map.
func New(
caCreds nebula.CACredentials,
adminCreationParams admin.CreationParams,
garage Garage,
name string,
ip net.IP,
) (
Bootstrap, error,
) {
hostPubCreds, hostPrivCreds, err := nebula.NewHostCredentials(
caCreds, name, ip,
)
if err != nil {
return Bootstrap{}, fmt.Errorf("generating host credentials: %w", err)
}
assigned := HostAssigned{
Name: name,
PublicCredentials: hostPubCreds,
}
signedAssigned, err := nebula.Sign(assigned, caCreds.SigningPrivateKey)
if err != nil {
return Bootstrap{}, fmt.Errorf("signing assigned fields: %w", err)
}
return Bootstrap{
AdminCreationParams: adminCreationParams,
CAPublicCredentials: caCreds.Public,
Garage: garage,
PrivateCredentials: hostPrivCreds,
HostAssigned: assigned,
SignedHostAssigned: signedAssigned,
Hosts: map[string]Host{},
}, nil
}
// FromFile reads a bootstrap from a file at the given path. The HostAssigned
// field will automatically be unwrapped.
func FromFile(path string) (Bootstrap, error) {
f, err := os.Open(path)
if err != nil {
return Bootstrap{}, fmt.Errorf("opening file: %w", err)
}
defer f.Close()
var b Bootstrap
if err := json.NewDecoder(f).Decode(&b); err != nil {
return Bootstrap{}, fmt.Errorf("decoding json: %w", err)
}
return b, nil
}
func (b *Bootstrap) UnmarshalJSON(data []byte) error {
type inner Bootstrap
err := json.Unmarshal(data, (*inner)(b))
if err != nil {
return err
}
b.HostAssigned, err = b.SignedHostAssigned.Unwrap(
b.CAPublicCredentials.SigningKey,
)
if err != nil {
return fmt.Errorf("unwrapping HostAssigned: %w", err)
}
return nil
}
// WriteTo writes the Bootstrap as a new bootstrap to the given io.Writer.
func (b Bootstrap) WriteTo(into io.Writer) error {
return json.NewEncoder(into).Encode(b)
}
// 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
}
// Hash returns a deterministic hash of the given hosts map.
func HostsHash(hostsMap map[string]Host) ([]byte, error) {
hosts := make([]Host, 0, len(hostsMap))
for _, host := range hostsMap {
hosts = append(hosts, host)
}
sort.Slice(hosts, func(i, j int) bool {
return hosts[i].Name < hosts[j].Name
})
h := sha512.New()
if err := json.NewEncoder(h).Encode(hosts); err != nil {
return nil, err
}
return h.Sum(nil), nil
}