// 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 }