2021-04-20 21:31:37 +00:00
|
|
|
package entrypoint
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"cryptic-net/bootstrap"
|
2022-10-15 14:28:03 +00:00
|
|
|
bootstrap_creator "cryptic-net/bootstrap/creator"
|
2021-04-20 21:31:37 +00:00
|
|
|
"cryptic-net/garage"
|
|
|
|
"cryptic-net/tarutil"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io/fs"
|
|
|
|
"net"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"regexp"
|
|
|
|
|
|
|
|
"github.com/minio/minio-go/v7"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
|
|
)
|
|
|
|
|
|
|
|
const nebulaHostPathPrefix = "nebula/hosts/"
|
|
|
|
|
|
|
|
func nebulaHostPath(name string) string {
|
|
|
|
return filepath.Join(nebulaHostPathPrefix, name+".yml")
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
|
2022-10-15 14:28:03 +00:00
|
|
|
client, err := env.GlobalBucketS3APIClient()
|
2021-04-20 21:31:37 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("creating client for global bucket: %w", err)
|
|
|
|
}
|
|
|
|
|
2022-10-15 14:28:03 +00:00
|
|
|
nebulaHost := bootstrap.NebulaHost{
|
2021-04-20 21:31:37 +00:00
|
|
|
Name: *name,
|
|
|
|
IP: *ip,
|
|
|
|
}
|
|
|
|
|
|
|
|
bodyBuf := new(bytes.Buffer)
|
|
|
|
|
|
|
|
if err := yaml.NewEncoder(bodyBuf).Encode(nebulaHost); err != nil {
|
|
|
|
return fmt.Errorf("marshaling nebula host to yaml: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
filePath := nebulaHostPath(*name)
|
|
|
|
|
|
|
|
_, err = client.PutObject(
|
|
|
|
env.Context, garage.GlobalBucket, filePath,
|
|
|
|
bodyBuf, int64(bodyBuf.Len()),
|
|
|
|
minio.PutObjectOptions{},
|
|
|
|
)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("writing to %q in global bucket: %w", filePath, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
|
2022-10-15 14:28:03 +00:00
|
|
|
client, err := env.GlobalBucketS3APIClient()
|
2021-04-20 21:31:37 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("creating client for global bucket: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
objInfoCh := client.ListObjects(
|
|
|
|
env.Context, garage.GlobalBucket,
|
|
|
|
minio.ListObjectsOptions{
|
|
|
|
Prefix: nebulaHostPathPrefix,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-env.Context.Done():
|
|
|
|
return env.Context.Err()
|
|
|
|
|
|
|
|
case objInfo, ok := <-objInfoCh:
|
|
|
|
|
|
|
|
if !ok {
|
|
|
|
return nil
|
|
|
|
} else if objInfo.Err != nil {
|
|
|
|
return objInfo.Err
|
|
|
|
}
|
|
|
|
|
|
|
|
obj, err := client.GetObject(
|
|
|
|
env.Context, garage.GlobalBucket, objInfo.Key,
|
|
|
|
minio.GetObjectOptions{},
|
|
|
|
)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("retrieving object %q from global bucket: %w", objInfo.Key, err)
|
|
|
|
}
|
|
|
|
|
2022-10-15 14:28:03 +00:00
|
|
|
var nebulaHost bootstrap.NebulaHost
|
2021-04-20 21:31:37 +00:00
|
|
|
|
|
|
|
err = yaml.NewDecoder(obj).Decode(&nebulaHost)
|
|
|
|
obj.Close()
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("yaml decoding %q from global bucket: %w", objInfo.Key, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Fprintf(
|
|
|
|
os.Stdout, "%s\t%s\n",
|
|
|
|
nebulaHost.Name, nebulaHost.IP,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
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 new host",
|
|
|
|
)
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
filePath := nebulaHostPath(*name)
|
|
|
|
|
2022-10-15 14:28:03 +00:00
|
|
|
client, err := env.GlobalBucketS3APIClient()
|
2021-04-20 21:31:37 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("creating client for global bucket: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = client.RemoveObject(
|
|
|
|
env.Context, garage.GlobalBucket, filePath,
|
|
|
|
minio.RemoveObjectOptions{},
|
|
|
|
)
|
|
|
|
|
|
|
|
if garage.IsKeyNotFound(err) {
|
|
|
|
return fmt.Errorf("host %q not found", *name)
|
|
|
|
} else if err != nil {
|
|
|
|
return fmt.Errorf("removing object %q from global bucket: %w", filePath, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
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")
|
|
|
|
}
|
|
|
|
|
|
|
|
adminFS, err := readAdminFS(*adminPath)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("reading admin.tgz with --admin-path of %q: %w", *adminPath, err)
|
|
|
|
}
|
|
|
|
|
2022-10-15 14:28:03 +00:00
|
|
|
return bootstrap_creator.NewForHost(subCmdCtx.env, adminFS, *name, os.Stdout)
|
2021-04-20 21:31:37 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
|
|
|
)
|
|
|
|
},
|
|
|
|
}
|