isle/go-workspace/src/cmd/entrypoint/admin.go
Brian Picciano 41e0b56617 Implement admin create-network command
This required a lot of re-implementation of how garage gets interacted
with, including updating cluster layout using the admin API and
initialization of the global bucket key.
2022-10-19 15:41:18 +02:00

427 lines
11 KiB
Go

package entrypoint
import (
"context"
"cryptic-net/admin"
"cryptic-net/bootstrap"
"cryptic-net/garage"
"cryptic-net/nebula"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"log"
"net"
"os"
"strconv"
"strings"
"github.com/cryptic-io/pmux/pmuxlib"
)
func randStr(l int) string {
b := make([]byte, l)
if _, err := rand.Read(b); err != nil {
panic(err)
}
return hex.EncodeToString(b)
}
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)
}
func garageInitializeGlobalBucket(
ctx context.Context,
adminClient *garage.AdminClient,
globalBucketCreds garage.S3APICredentials,
) error {
// first attempt to import the key
err := adminClient.Do(ctx, nil, "POST", "/v0/key/import", map[string]string{
"accessKeyId": globalBucketCreds.ID,
"secretAccessKey": globalBucketCreds.Secret,
"name": "shared-global-bucket-key",
})
if err != nil {
return fmt.Errorf("importing global bucket key into garage: %w", err)
}
// create global bucket
err = adminClient.Do(ctx, nil, "POST", "/v0/bucket", map[string]string{
"globalAlias": garage.GlobalBucket,
})
if err != nil {
return fmt.Errorf("creating global bucket: %w", err)
}
// retrieve newly created bucket's id
var getBucketRes struct {
ID string `json:"id"`
}
err = adminClient.Do(
ctx, &getBucketRes,
"GET", "/v0/bucket?globalAlias="+garage.GlobalBucket, nil,
)
if err != nil {
return fmt.Errorf("fetching global bucket id: %w", err)
}
// allow shared global bucket key to perform all operations
err = adminClient.Do(ctx, nil, "POST", "/v0/bucket/allow", map[string]interface{}{
"bucketId": getBucketRes.ID,
"accessKeyId": globalBucketCreds.ID,
"permissions": map[string]bool{
"read": true,
"write": true,
},
})
if err != nil {
return fmt.Errorf("granting permissions to shared global bucket key: %w", err)
}
return nil
}
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(),
}
// this will also write the bootstrap file
if err := mergeDaemonIntoBootstrap(env); err != nil {
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{})
log.Printf("starting child processes")
go func() {
pmuxlib.Run(ctx, pmuxConfig)
close(pmuxDoneCh)
}()
defer func() {
cancel()
log.Printf("waiting for child processes to exit")
<-pmuxDoneCh
}()
var garageAdminClient *garage.AdminClient
garageAdminClients := map[string]*garage.AdminClient{}
for _, alloc := range daemon.Storage.Allocations {
garageAdminAddr := net.JoinHostPort(ip.String(), strconv.Itoa(alloc.AdminPort))
garageAdminClient = garage.NewAdminClient(
garageAdminAddr,
env.Bootstrap.GarageAdminToken,
)
garageAdminClients[garageAdminAddr] = garageAdminClient
}
log.Printf("waiting for garage instances to come online")
for garageAdminAddr, garageAdminClient := range garageAdminClients {
if err := garageAdminClient.Wait(ctx); err != nil {
return fmt.Errorf("waiting for garage instance %q to start up: %w", garageAdminAddr, err)
}
}
log.Printf("applying initial garage layout")
err = garageApplyLayout(
ctx,
garageAdminClient,
*hostName, ip.String(),
daemon.Storage.Allocations,
)
if err != nil {
return fmt.Errorf("applying initial garage layout: %w", err)
}
log.Printf("initializing garage shared global bucket")
err = garageInitializeGlobalBucket(
ctx,
garageAdminClient,
env.Bootstrap.GarageGlobalBucketS3APICredentials,
)
if err != nil {
return fmt.Errorf("initializing garage shared global bucket: %w", err)
}
garageS3Client, err := env.Bootstrap.GlobalBucketS3APIClient()
if err != nil {
return fmt.Errorf("initializing garage shared global bucket client: %w", err)
}
log.Printf("writing data for this host into garage")
err = bootstrap.PutGarageBoostrapHost(ctx, garageS3Client, env.Bootstrap.ThisHost())
if err != nil {
return fmt.Errorf("putting host data into garage: %w", err)
}
log.Printf("cluster initialized successfully, writing admin.tgz to stdout")
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
},
}
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)
}
client, err := env.Bootstrap.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)
}
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)
if err != nil {
return fmt.Errorf("creating new nebula host key/cert: %w", err)
}
newBootstrap := bootstrap.Bootstrap{
AdminCreationParams: adm.CreationParams,
Hosts: hosts,
HostName: *name,
NebulaHostCert: nebulaHostCert,
GarageRPCSecret: adm.GarageRPCSecret,
GarageAdminToken: randStr(32),
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,
)
},
}