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