2022-10-20 19:59:46 +00:00
package main
2022-10-16 15:18:50 +00:00
import (
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"
2024-06-22 15:49:56 +00:00
"dev.mediocregopher.com/mediocre-go-lib.git/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
2024-06-17 18:51:02 +00:00
if hostBootstrap , 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
}
2024-06-17 18:51:02 +00:00
daemonInst , err := daemon . New (
ctx ,
logger . WithNamespace ( "daemon" ) ,
daemonConfig ,
hostBootstrap ,
envBinDirPath ,
& daemon . Opts {
// SkipHostBootstrapPush is required, because the global bucket
// hasn't actually been initialized yet, so there's nowhere to
// push to.
SkipHostBootstrapPush : true ,
// NOTE both stdout and stderr are sent to stderr, so that the
// user can pipe the resulting admin.json to stdout.
Stdout : os . Stderr ,
} ,
)
2022-10-16 20:17:26 +00:00
if err != nil {
2024-06-17 18:51:02 +00:00
return fmt . Errorf ( "initializing daemon: %w" , err )
2022-10-20 19:59:46 +00:00
}
2024-06-17 18:51:02 +00:00
logger . Info ( ctx , "initializing garage shared global bucket" )
garageGlobalBucketCreds , err := garageInitializeGlobalBucket (
ctx , logger , hostBootstrap , daemonConfig ,
)
2022-10-16 20:17:26 +00:00
2024-06-17 18:51:02 +00:00
if cErr := ( garage . AdminClientError { } ) ; errors . As ( err , & cErr ) && cErr . StatusCode == 409 {
return fmt . Errorf ( "shared global bucket has already been created, are the storage allocations from a previously initialized isle being used?" )
2022-10-16 20:17:26 +00:00
2024-06-17 18:51:02 +00:00
} else if err != nil {
return fmt . Errorf ( "initializing garage shared global bucket: %w" , err )
2022-10-16 20:17:26 +00:00
}
2024-06-24 16:55:36 +00:00
if err := daemonInst . Shutdown ( ) ; err != nil {
2024-06-17 18:51:02 +00:00
return fmt . Errorf ( "shutting down daemon: %w (this can mean there are zombie children leftover)" , err )
2022-10-16 20:17:26 +00:00
}
2024-06-11 12:54:26 +00:00
hostBootstrap . Garage . GlobalBucketS3APICredentials = garageGlobalBucketCreds
// rewrite the bootstrap now that the global bucket creds have been
// added to it.
2024-06-17 20:15:28 +00:00
if err := writeBootstrapToStateDir ( hostBootstrap ) ; err != nil {
2024-06-11 12:54:26 +00:00
return fmt . Errorf ( "writing bootstrap file: %w" , err )
}
2022-10-25 19:15:09 +00:00
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 )
}
2024-06-15 21:02:24 +00:00
var hostPub nebula . EncryptingPublicKey
if err := hostPub . UnmarshalNebulaPEM ( hostPubPEM ) ; err != nil {
return fmt . Errorf ( "unmarshaling public key as PEM: %w" , err )
}
nebulaHostCert , err := nebula . NewHostCert (
adm . Nebula . CACredentials , hostPub , * hostName , ip ,
2023-08-27 14:09:03 +00:00
)
if err != nil {
return fmt . Errorf ( "creating cert: %w" , err )
}
2024-06-23 12:37:10 +00:00
nebulaHostCertPEM , err := nebulaHostCert . Unwrap ( ) . MarshalToPEM ( )
2024-06-15 21:02:24 +00:00
if err != nil {
return fmt . Errorf ( "marshaling cert to PEM: %w" , err )
}
2023-08-27 14:09:03 +00:00
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
)
} ,
}