2022-10-20 19:59:46 +00:00
package main
2022-10-16 15:18:50 +00:00
import (
2022-10-16 20:17:26 +00:00
"context"
2022-10-16 19:22:58 +00:00
"crypto/rand"
"encoding/hex"
2022-10-16 15:18:50 +00:00
"errors"
"fmt"
2023-08-05 21:53:17 +00:00
"isle/admin"
"isle/bootstrap"
"isle/daemon"
"isle/garage"
"isle/nebula"
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"
2023-07-06 15:51:38 +00:00
"code.betamike.com/micropelago/pmux/pmuxlib"
2022-11-13 15:45:42 +00:00
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
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 {
2024-06-10 16:56:36 +00:00
return admin . Admin { } , fmt . Errorf ( "parsing admin.json from stdin: %w" , err )
2022-10-16 15:18:50 +00:00
}
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" ,
2024-06-10 16:56:36 +00:00
descr : "Creates a new isle network, outputting the resulting admin.json to stdout" ,
2022-10-16 20:17:26 +00:00
do : func ( subCmdCtx subCmdCtx ) error {
flags := subCmdCtx . flagSet ( false )
2022-10-26 21:21:31 +00:00
daemonConfigPath := flags . StringP (
2022-10-16 20:17:26 +00:00
"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." ,
)
2022-11-05 11:34:49 +00:00
name := flags . StringP (
"name" , "n" , "" ,
"Human-readable name to identify the network as." ,
)
2022-10-16 20:17:26 +00:00
domain := flags . StringP (
"domain" , "d" , "" ,
"Domain name that should be used as the root domain in the network." ,
)
2022-11-03 13:54:46 +00:00
ipNetStr := flags . StringP (
"ip-net" , "i" , "" ,
` IP+prefix (e.g. "10.10.0.1/16") which denotes the IP of this host, which will be the first host in the network, and the range of IPs which other hosts in the network can be assigned ` ,
2022-10-16 20:17:26 +00:00
)
hostName := flags . StringP (
2022-11-05 11:34:49 +00:00
"hostname" , "h" , "" ,
2022-11-03 13:54:46 +00:00
"Name of this host, which will be the first host in the network" ,
2022-10-16 20:17:26 +00:00
)
2022-11-13 15:45:42 +00:00
logLevelStr := flags . StringP (
"log-level" , "l" , "info" ,
` Maximum log level which should be output. Values can be "debug", "info", "warn", "error", "fatal". Does not apply to sub-processes ` ,
)
2022-10-16 20:17:26 +00:00
if err := flags . Parse ( subCmdCtx . args ) ; err != nil {
return fmt . Errorf ( "parsing flags: %w" , err )
}
2022-11-13 15:45:42 +00:00
ctx := subCmdCtx . ctx
2022-10-16 20:17:26 +00:00
if * dumpConfig {
2022-10-26 22:37:03 +00:00
return daemon . CopyDefaultConfig ( os . Stdout , envAppDirPath )
2022-10-16 20:17:26 +00:00
}
2022-11-05 11:34:49 +00:00
if * name == "" || * domain == "" || * ipNetStr == "" || * hostName == "" {
return errors . New ( "--name, --domain, --ip-net, and --hostname are required" )
2022-10-16 20:17:26 +00:00
}
2022-11-13 15:45:42 +00:00
logLevel := mlog . LevelFromString ( * logLevelStr )
if logLevel == nil {
return fmt . Errorf ( "couldn't parse log level %q" , * logLevelStr )
}
logger := subCmdCtx . logger . WithMaxLevel ( logLevel . Int ( ) )
2022-10-16 20:17:26 +00:00
* domain = strings . TrimRight ( strings . TrimLeft ( * domain , "." ) , "." )
2022-11-03 13:54:46 +00:00
ip , subnet , err := net . ParseCIDR ( * ipNetStr )
2022-10-16 20:17:26 +00:00
if err != nil {
2022-11-03 13:54:46 +00:00
return fmt . Errorf ( "parsing %q as a CIDR: %w" , * ipNetStr , err )
2022-10-16 20:17:26 +00:00
}
if err := validateHostName ( * hostName ) ; err != nil {
return fmt . Errorf ( "invalid hostname %q: %w" , * hostName , err )
}
2022-11-13 15:45:42 +00:00
runtimeDirCleanup , err := setupAndLockRuntimeDir ( ctx , logger )
2022-10-26 22:45:40 +00:00
if err != nil {
return fmt . Errorf ( "setting up runtime directory: %w" , err )
2022-10-16 20:17:26 +00:00
}
2022-10-26 22:45:40 +00:00
defer runtimeDirCleanup ( )
2022-10-16 20:17:26 +00:00
2022-10-26 22:37:03 +00:00
daemonConfig , err := daemon . LoadConfig ( envAppDirPath , * daemonConfigPath )
2022-10-26 21:21:31 +00:00
if err != nil {
return fmt . Errorf ( "loading daemon config: %w" , err )
2022-10-16 20:17:26 +00:00
}
2022-10-26 21:21:31 +00:00
if len ( daemonConfig . Storage . Allocations ) < 3 {
return fmt . Errorf ( "daemon config with at least 3 allocations was not provided" )
2022-10-16 20:17:26 +00:00
}
2022-10-29 19:11:40 +00:00
nebulaCACreds , err := nebula . NewCACredentials ( * domain , subnet )
2022-10-16 20:17:26 +00:00
if err != nil {
return fmt . Errorf ( "creating nebula CA cert: %w" , err )
}
2022-10-26 22:45:40 +00:00
adminCreationParams := admin . CreationParams {
ID : randStr ( 32 ) ,
2022-11-05 11:34:49 +00:00
Name : * name ,
2022-10-26 22:45:40 +00:00
Domain : * domain ,
}
2024-06-10 20:31:29 +00:00
garageBootstrap := bootstrap . Garage {
2024-06-11 12:54:26 +00:00
RPCSecret : randStr ( 32 ) ,
AdminToken : randStr ( 32 ) ,
2022-10-16 20:17:26 +00:00
}
2024-06-10 20:31:29 +00:00
hostBootstrap , err := bootstrap . New (
nebulaCACreds ,
adminCreationParams ,
garageBootstrap ,
* hostName ,
ip ,
)
if err != nil {
return fmt . Errorf ( "initializing bootstrap data: %w" , err )
}
2022-11-02 13:34:40 +00:00
2023-01-17 19:31:22 +00:00
if hostBootstrap , daemonConfig , err = coalesceDaemonConfigAndBootstrap ( hostBootstrap , daemonConfig ) ; err != nil {
2022-10-26 21:21:31 +00:00
return fmt . Errorf ( "merging daemon config into bootstrap data: %w" , err )
2022-10-16 20:17:26 +00:00
}
2022-10-26 22:37:03 +00:00
nebulaPmuxProcConfig , err := nebulaPmuxProcConfig ( hostBootstrap , daemonConfig )
2022-10-16 20:17:26 +00:00
if err != nil {
2022-10-20 19:59:46 +00:00
return fmt . Errorf ( "generating nebula config: %w" , err )
}
2022-10-26 22:37:03 +00:00
garagePmuxProcConfigs , err := garagePmuxProcConfigs ( hostBootstrap , daemonConfig )
2022-10-20 19:59:46 +00:00
if err != nil {
return fmt . Errorf ( "generating garage configs: %w" , err )
2022-10-16 20:17:26 +00:00
}
pmuxConfig := pmuxlib . Config {
Processes : append (
[ ] pmuxlib . ProcessConfig {
2022-10-20 19:59:46 +00:00
nebulaPmuxProcConfig ,
2022-10-16 20:17:26 +00:00
} ,
2022-10-20 19:59:46 +00:00
garagePmuxProcConfigs ... ,
2022-10-16 20:17:26 +00:00
) ,
}
2022-11-13 15:45:42 +00:00
ctx , cancel := context . WithCancel ( ctx )
2022-10-16 20:17:26 +00:00
pmuxDoneCh := make ( chan struct { } )
2022-11-13 15:45:42 +00:00
logger . Info ( ctx , "starting child processes" )
2022-10-16 20:17:26 +00:00
go func ( ) {
2022-10-25 19:15:09 +00:00
// NOTE both stdout and stderr are sent to stderr, so that the user
2024-06-10 16:56:36 +00:00
// can pipe the resulting admin.json to stdout.
2022-10-25 19:15:09 +00:00
pmuxlib . Run ( ctx , os . Stderr , os . Stderr , pmuxConfig )
2022-10-16 20:17:26 +00:00
close ( pmuxDoneCh )
} ( )
defer func ( ) {
cancel ( )
2022-11-13 15:45:42 +00:00
logger . Info ( ctx , "waiting for child processes to exit" )
2022-10-16 20:17:26 +00:00
<- pmuxDoneCh
} ( )
2022-11-13 15:45:42 +00:00
logger . Info ( ctx , "waiting for garage instances to come online" )
if err := waitForGarageAndNebula ( ctx , logger , hostBootstrap , daemonConfig ) ; err != nil {
2022-10-19 14:20:26 +00:00
return fmt . Errorf ( "waiting for garage to start up: %w" , err )
2022-10-16 20:17:26 +00:00
}
2022-11-13 15:45:42 +00:00
logger . Info ( ctx , "applying initial garage layout" )
if err := garageApplyLayout ( ctx , logger , hostBootstrap , daemonConfig ) ; err != nil {
2022-10-16 20:17:26 +00:00
return fmt . Errorf ( "applying initial garage layout: %w" , err )
}
2022-11-13 15:45:42 +00:00
logger . Info ( ctx , "initializing garage shared global bucket" )
2024-06-11 12:54:26 +00:00
garageGlobalBucketCreds , err := garageInitializeGlobalBucket (
ctx , logger , hostBootstrap , daemonConfig ,
)
hostBootstrap . Garage . GlobalBucketS3APICredentials = garageGlobalBucketCreds
// rewrite the bootstrap now that the global bucket creds have been
// added to it.
if err := writeBootstrapToDataDir ( hostBootstrap ) ; err != nil {
return fmt . Errorf ( "writing bootstrap file: %w" , err )
}
2022-10-25 19:15:09 +00:00
if cErr := ( garage . AdminClientError { } ) ; errors . As ( err , & cErr ) && cErr . StatusCode == 409 {
2023-08-05 21:53:17 +00:00
return fmt . Errorf ( "shared global bucket has already been created, are the storage allocations from a previously initialized isle being used?" )
2022-10-25 19:15:09 +00:00
} else if err != nil {
2022-10-16 20:17:26 +00:00
return fmt . Errorf ( "initializing garage shared global bucket: %w" , err )
}
2024-06-10 16:56:36 +00:00
logger . Info ( ctx , "cluster initialized successfully, writing admin.json to stdout" )
2022-10-16 20:17:26 +00:00
2022-11-02 13:02:21 +00:00
adm := admin . Admin {
CreationParams : adminCreationParams ,
}
adm . Nebula . CACredentials = nebulaCACreds
2022-11-02 13:34:40 +00:00
adm . Garage . RPCSecret = hostBootstrap . Garage . RPCSecret
adm . Garage . GlobalBucketS3APICredentials = hostBootstrap . Garage . GlobalBucketS3APICredentials
2022-10-16 20:17:26 +00:00
2022-11-02 13:02:21 +00:00
if err := adm . WriteTo ( os . Stdout ) ; err != nil {
2024-06-10 16:56:36 +00:00
return fmt . Errorf ( "writing admin.json to stdout" )
2022-10-16 20:17:26 +00:00
}
return nil
} ,
}
2022-11-05 15:41:14 +00:00
var subCmdAdminCreateBootstrap = subCmd {
name : "create-bootstrap" ,
2024-06-10 16:56:36 +00:00
descr : "Creates a new bootstrap.json file for a particular host and writes it to stdout" ,
2024-06-10 20:31:29 +00:00
checkLock : false ,
2022-10-16 15:18:50 +00:00
do : func ( subCmdCtx subCmdCtx ) error {
flags := subCmdCtx . flagSet ( false )
2022-11-05 11:34:49 +00:00
hostName := flags . StringP (
"hostname" , "h" , "" ,
2024-06-10 16:56:36 +00:00
"Name of the host to generate bootstrap.json for" ,
2022-10-16 15:18:50 +00:00
)
2022-10-29 19:11:40 +00:00
ipStr := flags . StringP (
"ip" , "i" , "" ,
"IP of the new host" ,
)
2022-10-16 15:18:50 +00:00
adminPath := flags . StringP (
"admin-path" , "a" , "" ,
2024-06-10 16:56:36 +00:00
` Path to admin.json file. If the given path is "-" then stdin is used. ` ,
2022-10-16 15:18:50 +00:00
)
if err := flags . Parse ( subCmdCtx . args ) ; err != nil {
return fmt . Errorf ( "parsing flags: %w" , err )
}
2022-11-05 11:34:49 +00:00
if * hostName == "" || * ipStr == "" || * adminPath == "" {
return errors . New ( "--hostname, --ip, and --admin-path are required" )
2022-10-16 15:18:50 +00:00
}
2022-11-05 11:34:49 +00:00
if err := validateHostName ( * hostName ) ; err != nil {
return fmt . Errorf ( "invalid hostname %q: %w" , * hostName , err )
2022-10-29 19:11:40 +00:00
}
ip := net . ParseIP ( * ipStr )
if ip == nil {
return fmt . Errorf ( "invalid ip %q" , * ipStr )
2022-10-26 22:23:39 +00:00
}
2022-10-16 15:18:50 +00:00
adm , err := readAdmin ( * adminPath )
if err != nil {
2024-06-10 16:56:36 +00:00
return fmt . Errorf ( "reading admin.json with --admin-path of %q: %w" , * adminPath , err )
2022-10-16 15:18:50 +00:00
}
2024-06-10 20:31:29 +00:00
garageBootstrap := bootstrap . Garage {
RPCSecret : adm . Garage . RPCSecret ,
AdminToken : randStr ( 32 ) ,
GlobalBucketS3APICredentials : adm . Garage . GlobalBucketS3APICredentials ,
2022-10-16 15:18:50 +00:00
}
2024-06-10 20:31:29 +00:00
newHostBootstrap , err := bootstrap . New (
2022-11-05 14:23:29 +00:00
adm . Nebula . CACredentials ,
2024-06-10 20:31:29 +00:00
adm . CreationParams ,
garageBootstrap ,
* hostName ,
ip ,
2022-11-05 14:23:29 +00:00
)
if err != nil {
2024-06-10 20:31:29 +00:00
return fmt . Errorf ( "initializing bootstrap data: %w" , err )
2022-11-05 14:23:29 +00:00
}
2024-06-10 20:31:29 +00:00
hostBootstrap , err := loadHostBootstrap ( )
if err != nil {
return fmt . Errorf ( "loading host bootstrap: %w" , err )
2022-10-16 15:18:50 +00:00
}
2024-06-10 20:31:29 +00:00
newHostBootstrap . Hosts = hostBootstrap . Hosts
2022-11-02 13:34:40 +00:00
2022-10-26 22:23:39 +00:00
return newHostBootstrap . WriteTo ( os . Stdout )
2022-10-16 15:18:50 +00:00
} ,
}
2023-08-27 14:09:03 +00:00
var subCmdAdminCreateNebulaCert = subCmd {
name : "create-nebula-cert" ,
descr : "Creates a signed nebula certificate file and writes it to stdout" ,
checkLock : false ,
do : func ( subCmdCtx subCmdCtx ) error {
flags := subCmdCtx . flagSet ( false )
hostName := flags . StringP (
"hostname" , "h" , "" ,
2024-06-10 16:56:36 +00:00
"Name of the host to generate bootstrap.json for" ,
2023-08-27 14:09:03 +00:00
)
ipStr := flags . StringP (
"ip" , "i" , "" ,
"IP of the new host" ,
)
adminPath := flags . StringP (
"admin-path" , "a" , "" ,
2024-06-10 16:56:36 +00:00
` Path to admin.json file. If the given path is "-" then stdin is used. ` ,
2023-08-27 14:09:03 +00:00
)
pubKeyPath := flags . StringP (
"public-key-path" , "p" , "" ,
` Path to PEM file containing public key which will be embedded in the cert. ` ,
)
if err := flags . Parse ( subCmdCtx . args ) ; err != nil {
return fmt . Errorf ( "parsing flags: %w" , err )
}
if * hostName == "" || * ipStr == "" || * adminPath == "" || * pubKeyPath == "" {
return errors . New ( "--hostname, --ip, --admin-path, and --pub-key-path are required" )
}
if err := validateHostName ( * hostName ) ; err != nil {
return fmt . Errorf ( "invalid hostname %q: %w" , * hostName , err )
}
ip := net . ParseIP ( * ipStr )
if ip == nil {
return fmt . Errorf ( "invalid ip %q" , * ipStr )
}
adm , err := readAdmin ( * adminPath )
if err != nil {
2024-06-10 16:56:36 +00:00
return fmt . Errorf ( "reading admin.json with --admin-path of %q: %w" , * adminPath , err )
2023-08-27 14:09:03 +00:00
}
hostPubPEM , err := os . ReadFile ( * pubKeyPath )
if err != nil {
return fmt . Errorf ( "reading public key from %q: %w" , * pubKeyPath , err )
}
nebulaHostCertPEM , err := nebula . NewHostCertPEM (
adm . Nebula . CACredentials , string ( hostPubPEM ) , * hostName , ip ,
)
if err != nil {
return fmt . Errorf ( "creating cert: %w" , err )
}
if _ , err := os . Stdout . Write ( [ ] byte ( nebulaHostCertPEM ) ) ; err != nil {
return fmt . Errorf ( "writing to stdout: %w" , err )
}
return nil
} ,
}
2022-10-16 15:18:50 +00:00
var subCmdAdmin = subCmd {
name : "admin" ,
descr : "Sub-commands which only admins can run" ,
do : func ( subCmdCtx subCmdCtx ) error {
return subCmdCtx . doSubCmd (
2022-10-25 19:15:09 +00:00
subCmdAdminCreateNetwork ,
2022-11-05 15:41:14 +00:00
subCmdAdminCreateBootstrap ,
2023-08-27 14:09:03 +00:00
subCmdAdminCreateNebulaCert ,
2022-10-16 15:18:50 +00:00
)
} ,
}