2022-10-16 15:18:50 +00:00
|
|
|
package entrypoint
|
|
|
|
|
|
|
|
import (
|
2022-10-16 20:17:26 +00:00
|
|
|
"context"
|
2022-10-16 15:18:50 +00:00
|
|
|
"cryptic-net/admin"
|
|
|
|
"cryptic-net/bootstrap"
|
2022-10-16 20:17:26 +00:00
|
|
|
"cryptic-net/garage"
|
2022-10-16 15:18:50 +00:00
|
|
|
"cryptic-net/nebula"
|
2022-10-16 19:22:58 +00:00
|
|
|
"crypto/rand"
|
|
|
|
"encoding/hex"
|
2022-10-16 15:18:50 +00:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
2022-10-16 20:17:26 +00:00
|
|
|
"net"
|
2022-10-16 15:18:50 +00:00
|
|
|
"os"
|
2022-10-16 20:17:26 +00:00
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/cryptic-io/pmux/pmuxlib"
|
2022-10-16 15:18:50 +00:00
|
|
|
)
|
|
|
|
|
2022-10-16 19:22:58 +00:00
|
|
|
func randStr(l int) string {
|
|
|
|
b := make([]byte, l)
|
|
|
|
if _, err := rand.Read(b); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return hex.EncodeToString(b)
|
|
|
|
}
|
|
|
|
|
2022-10-16 15:18:50 +00:00
|
|
|
func readAdmin(path string) (admin.Admin, error) {
|
|
|
|
|
|
|
|
if path == "-" {
|
|
|
|
|
|
|
|
adm, err := admin.FromReader(os.Stdin)
|
|
|
|
if err != nil {
|
|
|
|
return admin.Admin{}, fmt.Errorf("parsing admin.tgz from stdin: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return adm, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
f, err := os.Open(path)
|
|
|
|
if err != nil {
|
|
|
|
return admin.Admin{}, fmt.Errorf("opening file: %w", err)
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
return admin.FromReader(f)
|
|
|
|
}
|
|
|
|
|
2022-10-16 20:17:26 +00:00
|
|
|
var subCmdAdminCreateNetwork = subCmd{
|
|
|
|
name: "create-network",
|
|
|
|
descr: "Creates a new cryptic-net network, outputting the resulting admin.tgz to stdout",
|
|
|
|
do: func(subCmdCtx subCmdCtx) error {
|
|
|
|
|
|
|
|
flags := subCmdCtx.flagSet(false)
|
|
|
|
|
|
|
|
daemonYmlPath := flags.StringP(
|
|
|
|
"config-path", "c", "",
|
|
|
|
"Optional path to a daemon.yml file to load configuration from.",
|
|
|
|
)
|
|
|
|
|
|
|
|
dumpConfig := flags.Bool(
|
|
|
|
"dump-config", false,
|
|
|
|
"Write the default configuration file to stdout and exit.",
|
|
|
|
)
|
|
|
|
|
|
|
|
domain := flags.StringP(
|
|
|
|
"domain", "d", "",
|
|
|
|
"Domain name that should be used as the root domain in the network.",
|
|
|
|
)
|
|
|
|
|
|
|
|
subnetStr := flags.StringP(
|
|
|
|
"subnet", "s", "",
|
|
|
|
"CIDR which denotes the subnet that IPs hosts on the network can be assigned.",
|
|
|
|
)
|
|
|
|
|
|
|
|
hostName := flags.StringP(
|
|
|
|
"name", "n", "",
|
|
|
|
"Name of the host which will be the first host in the network",
|
|
|
|
)
|
|
|
|
|
|
|
|
if err := flags.Parse(subCmdCtx.args); err != nil {
|
|
|
|
return fmt.Errorf("parsing flags: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
env := subCmdCtx.env
|
|
|
|
|
|
|
|
if *dumpConfig {
|
|
|
|
return writeBuiltinDaemonYml(env, os.Stdout)
|
|
|
|
}
|
|
|
|
|
|
|
|
if *domain == "" || *subnetStr == "" || *hostName == "" {
|
|
|
|
return errors.New("--domain, --subnet, and --name are required")
|
|
|
|
}
|
|
|
|
|
|
|
|
*domain = strings.TrimRight(strings.TrimLeft(*domain, "."), ".")
|
|
|
|
|
|
|
|
ip, subnet, err := net.ParseCIDR(*subnetStr)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("parsing %q as a CIDR: %w", *subnetStr, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := validateHostName(*hostName); err != nil {
|
|
|
|
return fmt.Errorf("invalid hostname %q: %w", *hostName, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
adminCreationParams := admin.CreationParams{
|
|
|
|
ID: randStr(32),
|
|
|
|
Domain: *domain,
|
|
|
|
}
|
|
|
|
|
|
|
|
{
|
|
|
|
runtimeDirPath := env.RuntimeDirPath
|
|
|
|
|
|
|
|
fmt.Fprintf(os.Stderr, "will use runtime directory %q for temporary state\n", runtimeDirPath)
|
|
|
|
|
|
|
|
if err := os.MkdirAll(runtimeDirPath, 0700); err != nil {
|
|
|
|
return fmt.Errorf("creating directory %q: %w", runtimeDirPath, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
fmt.Fprintf(os.Stderr, "cleaning up runtime directory %q\n", runtimeDirPath)
|
|
|
|
if err := os.RemoveAll(runtimeDirPath); err != nil {
|
|
|
|
fmt.Fprintf(os.Stderr, "error removing temporary directory %q: %v", runtimeDirPath, err)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := writeMergedDaemonYml(env, *daemonYmlPath); err != nil {
|
|
|
|
return fmt.Errorf("merging and writing daemon.yml file: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
daemon := env.ThisDaemon()
|
|
|
|
|
|
|
|
if len(daemon.Storage.Allocations) < 3 {
|
|
|
|
return fmt.Errorf("daemon.yml with at least 3 allocations was not provided")
|
|
|
|
}
|
|
|
|
|
|
|
|
nebulaCACert, err := nebula.NewCACert(*domain, subnet)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("creating nebula CA cert: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
nebulaHostCert, err := nebula.NewHostCert(nebulaCACert, *hostName, ip)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("creating nebula cert for host: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
env.Bootstrap = bootstrap.Bootstrap{
|
|
|
|
AdminCreationParams: adminCreationParams,
|
|
|
|
Hosts: map[string]bootstrap.Host{
|
|
|
|
*hostName: bootstrap.Host{
|
|
|
|
Name: *hostName,
|
|
|
|
Nebula: bootstrap.NebulaHost{
|
|
|
|
IP: ip.String(),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
HostName: *hostName,
|
|
|
|
NebulaHostCert: nebulaHostCert,
|
|
|
|
GarageRPCSecret: randStr(32),
|
|
|
|
GarageGlobalBucketS3APICredentials: garage.NewS3APICredentials(),
|
|
|
|
}
|
|
|
|
|
2022-10-19 14:20:26 +00:00
|
|
|
if env, err = mergeDaemonIntoBootstrap(env); err != nil {
|
2022-10-16 20:17:26 +00:00
|
|
|
return fmt.Errorf("merging daemon.yml into bootstrap data: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO this can be gotten rid of once nebula-entrypoint is rolled into
|
|
|
|
// daemon itself
|
|
|
|
for key, val := range env.ToMap() {
|
|
|
|
if err := os.Setenv(key, val); err != nil {
|
|
|
|
return fmt.Errorf("failed to set %q to %q: %w", key, val, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
garageChildrenPmuxProcConfigs, err := garageChildrenPmuxProcConfigs(env)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("generating garage children configs: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
pmuxConfig := pmuxlib.Config{
|
|
|
|
Processes: append(
|
|
|
|
[]pmuxlib.ProcessConfig{
|
|
|
|
nebulaEntrypointPmuxProcConfig(),
|
|
|
|
},
|
|
|
|
garageChildrenPmuxProcConfigs...,
|
|
|
|
),
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx, cancel := context.WithCancel(env.Context)
|
|
|
|
pmuxDoneCh := make(chan struct{})
|
|
|
|
|
2022-10-19 14:20:26 +00:00
|
|
|
fmt.Fprintln(os.Stderr, "starting child processes")
|
2022-10-16 20:17:26 +00:00
|
|
|
go func() {
|
|
|
|
pmuxlib.Run(ctx, pmuxConfig)
|
|
|
|
close(pmuxDoneCh)
|
|
|
|
}()
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
cancel()
|
2022-10-19 14:20:26 +00:00
|
|
|
fmt.Fprintln(os.Stderr, "waiting for child processes to exit")
|
2022-10-16 20:17:26 +00:00
|
|
|
<-pmuxDoneCh
|
|
|
|
}()
|
|
|
|
|
2022-10-19 14:20:26 +00:00
|
|
|
fmt.Fprintln(os.Stderr, "waiting for garage instances to come online")
|
|
|
|
if err := waitForGarage(ctx, env); err != nil {
|
|
|
|
return fmt.Errorf("waiting for garage to start up: %w", err)
|
2022-10-16 20:17:26 +00:00
|
|
|
}
|
|
|
|
|
2022-10-19 14:20:26 +00:00
|
|
|
fmt.Fprintln(os.Stderr, "applying initial garage layout")
|
|
|
|
if err := garageApplyLayout(ctx, env); err != nil {
|
2022-10-16 20:17:26 +00:00
|
|
|
return fmt.Errorf("applying initial garage layout: %w", err)
|
|
|
|
}
|
|
|
|
|
2022-10-19 14:20:26 +00:00
|
|
|
fmt.Fprintln(os.Stderr, "initializing garage shared global bucket")
|
|
|
|
if err := garageInitializeGlobalBucket(ctx, env); err != nil {
|
2022-10-16 20:17:26 +00:00
|
|
|
return fmt.Errorf("initializing garage shared global bucket: %w", err)
|
|
|
|
}
|
|
|
|
|
2022-10-19 14:53:31 +00:00
|
|
|
garageS3Client := env.Bootstrap.GlobalBucketS3APIClient()
|
2022-10-16 20:17:26 +00:00
|
|
|
|
2022-10-19 14:20:26 +00:00
|
|
|
fmt.Fprintln(os.Stderr, "writing data for this host into garage")
|
2022-10-16 20:17:26 +00:00
|
|
|
err = bootstrap.PutGarageBoostrapHost(ctx, garageS3Client, env.Bootstrap.ThisHost())
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("putting host data into garage: %w", err)
|
|
|
|
}
|
|
|
|
|
2022-10-19 14:20:26 +00:00
|
|
|
fmt.Fprintln(os.Stderr, "cluster initialized successfully, writing admin.tgz to stdout")
|
2022-10-16 20:17:26 +00:00
|
|
|
|
|
|
|
err = admin.Admin{
|
|
|
|
CreationParams: adminCreationParams,
|
|
|
|
NebulaCACert: nebulaCACert,
|
|
|
|
GarageRPCSecret: env.Bootstrap.GarageRPCSecret,
|
|
|
|
GarageGlobalBucketS3APICredentials: env.Bootstrap.GarageGlobalBucketS3APICredentials,
|
|
|
|
GarageAdminBucketS3APICredentials: garage.NewS3APICredentials(),
|
|
|
|
}.WriteTo(os.Stdout)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("writing admin.tgz to stdout")
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2022-10-16 15:18:50 +00:00
|
|
|
var subCmdAdminMakeBootstrap = 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
|
|
|
|
|
|
|
|
adm, err := readAdmin(*adminPath)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("reading admin.tgz with --admin-path of %q: %w", *adminPath, err)
|
|
|
|
}
|
|
|
|
|
2022-10-19 14:53:31 +00:00
|
|
|
client := env.Bootstrap.GlobalBucketS3APIClient()
|
2022-10-16 15:18:50 +00:00
|
|
|
|
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
2022-10-16 20:17:26 +00:00
|
|
|
ip := net.ParseIP(host.Nebula.IP)
|
|
|
|
if ip == nil {
|
|
|
|
return fmt.Errorf("invalid IP stored with host %q: %q", *name, host.Nebula.IP)
|
|
|
|
}
|
|
|
|
|
|
|
|
nebulaHostCert, err := nebula.NewHostCert(adm.NebulaCACert, host.Name, ip)
|
2022-10-16 15:18:50 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("creating new nebula host key/cert: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
newBootstrap := bootstrap.Bootstrap{
|
2022-10-16 15:05:05 +00:00
|
|
|
AdminCreationParams: adm.CreationParams,
|
|
|
|
|
2022-10-16 15:18:50 +00:00
|
|
|
Hosts: hosts,
|
|
|
|
HostName: *name,
|
|
|
|
|
|
|
|
NebulaHostCert: nebulaHostCert,
|
|
|
|
|
|
|
|
GarageRPCSecret: adm.GarageRPCSecret,
|
2022-10-16 19:22:58 +00:00
|
|
|
GarageAdminToken: randStr(32),
|
2022-10-16 15:18:50 +00:00
|
|
|
GarageGlobalBucketS3APICredentials: adm.GarageGlobalBucketS3APICredentials,
|
|
|
|
}
|
|
|
|
|
|
|
|
return newBootstrap.WriteTo(os.Stdout)
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
var subCmdAdmin = subCmd{
|
|
|
|
name: "admin",
|
|
|
|
descr: "Sub-commands which only admins can run",
|
|
|
|
do: func(subCmdCtx subCmdCtx) error {
|
|
|
|
return subCmdCtx.doSubCmd(
|
|
|
|
subCmdAdminMakeBootstrap,
|
|
|
|
)
|
|
|
|
},
|
|
|
|
}
|