isle/go-workspace/src/cmd/entrypoint/hosts.go

251 lines
5.7 KiB
Go
Raw Normal View History

package entrypoint
import (
"cryptic-net/bootstrap"
"cryptic-net/nebula"
"cryptic-net/tarutil"
"errors"
"fmt"
"io/fs"
"net"
"os"
"regexp"
"sort"
"gopkg.in/yaml.v3"
)
var hostNameRegexp = regexp.MustCompile(`^[a-z][a-z0-9\-]*$`)
func validateHostName(name string) error {
if !hostNameRegexp.MatchString(name) {
return errors.New("a host's name must start with a letter and only contain letters, numbers, and dashes")
}
return nil
}
var subCmdHostsAdd = subCmd{
name: "add",
descr: "Adds a host to the network",
checkLock: true,
do: func(subCmdCtx subCmdCtx) error {
flags := subCmdCtx.flagSet(false)
name := flags.StringP(
"name", "n", "",
"Name of the new host",
)
ip := flags.StringP(
"ip", "i", "",
"IP of the new host",
)
if err := flags.Parse(subCmdCtx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
if *name == "" || *ip == "" {
return errors.New("--name and --ip are required")
}
if err := validateHostName(*name); err != nil {
return fmt.Errorf("invalid hostname %q: %w", *name, err)
}
if net.ParseIP(*ip) == nil {
return fmt.Errorf("invalid ip %q", *ip)
}
// TODO validate that the IP is in the correct CIDR
env := subCmdCtx.env
client, err := env.GlobalBucketS3APIClient()
if err != nil {
return fmt.Errorf("creating client for global bucket: %w", err)
}
host := bootstrap.Host{
Name: *name,
Nebula: bootstrap.NebulaHost{
IP: *ip,
},
}
return bootstrap.PutGarageBoostrapHost(env.Context, client, host)
},
}
var subCmdHostsList = subCmd{
name: "list",
descr: "Lists all hosts in the network, and their IPs",
checkLock: true,
do: func(subCmdCtx subCmdCtx) error {
env := subCmdCtx.env
client, err := env.GlobalBucketS3APIClient()
if err != nil {
return fmt.Errorf("creating client for global bucket: %w", err)
}
hostsMap, err := bootstrap.GetGarageBootstrapHosts(env.Context, client)
if err != nil {
return fmt.Errorf("retrieving hosts from garage: %w", err)
}
hosts := make([]bootstrap.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 })
return yaml.NewEncoder(os.Stdout).Encode(hosts)
},
}
var subCmdHostsDelete = subCmd{
name: "delete",
descr: "Deletes a host from the network",
checkLock: true,
do: func(subCmdCtx subCmdCtx) error {
flags := subCmdCtx.flagSet(false)
name := flags.StringP(
"name", "n", "",
"Name of the host to delete",
)
if err := flags.Parse(subCmdCtx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
if *name == "" {
return errors.New("--name is required")
}
env := subCmdCtx.env
client, err := env.GlobalBucketS3APIClient()
if err != nil {
return fmt.Errorf("creating client for global bucket: %w", err)
}
return bootstrap.RemoveGarageBootstrapHost(env.Context, client, *name)
},
}
func readAdminFS(path string) (fs.FS, error) {
if path == "-" {
outFS, err := tarutil.FSFromReader(os.Stdin)
if err != nil {
return nil, fmt.Errorf("reading admin.tgz from stdin: %w", err)
}
return outFS, nil
}
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("opening file: %w", err)
}
defer f.Close()
return tarutil.FSFromReader(f)
}
var subCmdHostsMakeBootstrap = subCmd{
name: "make-bootstrap",
descr: "Creates a new bootstrap.tgz file for a particular host and writes it to stdout",
checkLock: true,
do: func(subCmdCtx subCmdCtx) error {
flags := subCmdCtx.flagSet(false)
name := flags.StringP(
"name", "n", "",
"Name of the host to generate bootstrap.tgz for",
)
adminPath := flags.StringP(
"admin-path", "a", "",
`Path to admin.tgz file. If the given path is "-" then stdin is used.`,
)
if err := flags.Parse(subCmdCtx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
if *name == "" || *adminPath == "" {
return errors.New("--name and --admin-path are required")
}
env := subCmdCtx.env
adminFS, err := readAdminFS(*adminPath)
if err != nil {
return fmt.Errorf("reading admin.tgz with --admin-path of %q: %w", *adminPath, err)
}
client, err := env.GlobalBucketS3APIClient()
if err != nil {
return fmt.Errorf("creating client for global bucket: %w", err)
}
// NOTE this isn't _technically_ required, but if the `hosts add`
// command for this host has been run recently then it might not have
// made it into the bootstrap file yet, and so won't be in
// `env.Bootstrap`.
hosts, err := bootstrap.GetGarageBootstrapHosts(env.Context, client)
if err != nil {
return fmt.Errorf("retrieving host info from garage: %w", err)
}
host, ok := hosts[*name]
if !ok {
return fmt.Errorf("couldn't find host into for %q in garage, has `cryptic-net hosts add` been run yet?", *name)
}
nebulaHostCert, err := nebula.NewHostCert(adminFS, host)
if err != nil {
return fmt.Errorf("creating new nebula host key/cert: %w", err)
}
newBootstrap := bootstrap.Bootstrap{
Hosts: hosts,
HostName: *name,
NebulaCertsCACert: nebulaHostCert.CACert,
NebulaCertsHostCert: nebulaHostCert.HostCert,
NebulaCertsHostKey: nebulaHostCert.HostKey,
// TODO these should use adminFS
GarageRPCSecret: env.Bootstrap.GarageRPCSecret,
GarageGlobalBucketS3APICredentials: env.Bootstrap.GarageGlobalBucketS3APICredentials,
}
return newBootstrap.WriteTo(os.Stdout)
},
}
var subCmdHosts = subCmd{
name: "hosts",
descr: "Sub-commands having to do with configuration of hosts in the network",
do: func(subCmdCtx subCmdCtx) error {
return subCmdCtx.doSubCmd(
subCmdHostsAdd,
subCmdHostsList,
subCmdHostsDelete,
subCmdHostsMakeBootstrap,
)
},
}