// Package bootstrap deals with the parsing and creation of bootstrap.tgz files. // It also contains some helpers which rely on bootstrap data. package bootstrap import ( "cryptic-net/admin" "cryptic-net/garage" "cryptic-net/nebula" "cryptic-net/tarutil" "cryptic-net/yamlutil" "crypto/sha512" "fmt" "io" "io/fs" "os" "path/filepath" "sort" "gopkg.in/yaml.v3" ) // Paths within the bootstrap FS which for general data. const ( adminCreationParamsPath = "admin/creation-params.yml" hostNamePath = "hostname" ) // Bootstrap is used for accessing all information contained within a // bootstrap.tgz file. type Bootstrap struct { AdminCreationParams admin.CreationParams Hosts map[string]Host HostName string NebulaHostCert nebula.HostCert GarageRPCSecret string GarageAdminToken string GarageGlobalBucketS3APICredentials garage.S3APICredentials } // FromFS loads a Boostrap instance from the given fs.FS, which presumably // represents the file structure of a bootstrap.tgz file. func FromFS(bootstrapFS fs.FS) (Bootstrap, error) { var ( b Bootstrap err error ) if b.Hosts, err = loadHosts(bootstrapFS); err != nil { return Bootstrap{}, fmt.Errorf("loading hosts info from fs: %w", err) } filesToLoadAsYAML := []struct { into interface{} path string }{ {&b.AdminCreationParams, adminCreationParamsPath}, {&b.GarageGlobalBucketS3APICredentials, garageGlobalBucketKeyYmlPath}, } for _, f := range filesToLoadAsYAML { if err := yamlutil.LoadYamlFSFile(f.into, bootstrapFS, f.path); err != nil { return Bootstrap{}, fmt.Errorf("loading %q from fs: %w", f.path, err) } } filesToLoadAsString := []struct { into *string path string }{ {&b.HostName, hostNamePath}, {&b.NebulaHostCert.CACert, nebulaCertsCACertPath}, {&b.NebulaHostCert.HostCert, nebulaCertsHostCertPath}, {&b.NebulaHostCert.HostKey, nebulaCertsHostKeyPath}, {&b.GarageRPCSecret, garageRPCSecretPath}, {&b.GarageAdminToken, garageAdminTokenPath}, } for _, f := range filesToLoadAsString { body, err := fs.ReadFile(bootstrapFS, f.path) if err != nil { return Bootstrap{}, fmt.Errorf("loading %q from fs: %w", f.path, err) } *f.into = string(body) } return b, nil } // FromReader reads a bootstrap.tgz file from the given io.Reader. func FromReader(r io.Reader) (Bootstrap, error) { fs, err := tarutil.FSFromReader(r) if err != nil { return Bootstrap{}, fmt.Errorf("reading bootstrap.tgz: %w", err) } return FromFS(fs) } // FromFile reads a bootstrap.tgz from a file at the given path. 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() return FromReader(f) } // WriteTo writes the Bootstrap as a new bootstrap.tgz to the given io.Writer. func (b Bootstrap) WriteTo(into io.Writer) error { w := tarutil.NewTGZWriter(into) for _, host := range b.Hosts { hostB, err := yaml.Marshal(host) if err != nil { return fmt.Errorf("yaml encoding host %#v: %w", host, err) } path := filepath.Join(hostsDirPath, host.Name+".yml") w.WriteFileBytes(path, hostB) } filesToWriteAsYAML := []struct { value interface{} path string }{ {b.AdminCreationParams, adminCreationParamsPath}, {b.GarageGlobalBucketS3APICredentials, garageGlobalBucketKeyYmlPath}, } for _, f := range filesToWriteAsYAML { b, err := yaml.Marshal(f.value) if err != nil { return fmt.Errorf("yaml encoding data for %q: %w", f.path, err) } w.WriteFileBytes(f.path, b) } filesToWriteAsString := []struct { value string path string }{ {b.HostName, hostNamePath}, {b.NebulaHostCert.CACert, nebulaCertsCACertPath}, {b.NebulaHostCert.HostCert, nebulaCertsHostCertPath}, {b.NebulaHostCert.HostKey, nebulaCertsHostKeyPath}, {b.GarageRPCSecret, garageRPCSecretPath}, {b.GarageAdminToken, garageAdminTokenPath}, } for _, f := range filesToWriteAsString { w.WriteFileBytes(f.path, []byte(f.value)) } return w.Close() } // 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.HostName] if !ok { panic(fmt.Sprintf("hostname %q not defined in bootstrap's hosts", b.HostName)) } 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 := yaml.NewEncoder(h).Encode(hosts); err != nil { return nil, err } return h.Sum(nil), nil } // WithHosts returns a copy of the Bootstrap with the given set of Hosts applied // to it. It will _not_ overwrite the Host for _this_ host, however. func (b Bootstrap) WithHosts(hosts map[string]Host) Bootstrap { hostsCopy := make(map[string]Host, len(hosts)) for name, host := range hosts { hostsCopy[name] = host } hostsCopy[b.HostName] = b.ThisHost() b.Hosts = hostsCopy return b }