package entrypoint import ( "context" "cryptic-net/admin" "cryptic-net/bootstrap" "cryptic-net/garage" "cryptic-net/nebula" "crypto/rand" "encoding/hex" "errors" "fmt" "net" "os" "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) } 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(), } if env, 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{}) fmt.Fprintln(os.Stderr, "starting child processes") go func() { pmuxlib.Run(ctx, pmuxConfig) close(pmuxDoneCh) }() defer func() { cancel() fmt.Fprintln(os.Stderr, "waiting for child processes to exit") <-pmuxDoneCh }() 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) } fmt.Fprintln(os.Stderr, "applying initial garage layout") if err := garageApplyLayout(ctx, env); err != nil { return fmt.Errorf("applying initial garage layout: %w", err) } fmt.Fprintln(os.Stderr, "initializing garage shared global bucket") if err := garageInitializeGlobalBucket(ctx, env); err != nil { return fmt.Errorf("initializing garage shared global bucket: %w", err) } garageS3Client := env.Bootstrap.GlobalBucketS3APIClient() fmt.Fprintln(os.Stderr, "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) } fmt.Fprintln(os.Stderr, "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 := env.Bootstrap.GlobalBucketS3APIClient() // 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, ) }, }