isle/go-workspace/src/env.go
Brian Picciano 836e69735d Some large inter-related refactors, moving towards network creation command
Host types have been moved within the `bootstrap` package.

Refactored how boostrap FS is interacted with. There is now a
`Bootstrap` struct which has pre-loaded all data within the bootstrap
FS. This helps centralize the logic around reading the data (though not
yet completely).

Choosing of the garage node to interact with no longer occurs in like
three different places. It occurs at the environment level now, and is
aided by the new `garage.Peer` type.
2022-10-15 16:28:03 +02:00

227 lines
5.6 KiB
Go

package crypticnet
import (
"context"
"cryptic-net/bootstrap"
"cryptic-net/yamlutil"
"errors"
"fmt"
"io/fs"
"os"
"os/signal"
"path/filepath"
"sync"
"syscall"
"github.com/adrg/xdg"
)
// Names of various environment variables which get set by the entrypoint.
const (
DaemonYmlPathEnvVar = "_DAEMON_YML_PATH"
BootstrapPathEnvVar = "_BOOTSTRAP_PATH"
RuntimeDirPathEnvVar = "_RUNTIME_DIR_PATH"
DataDirPathEnvVar = "_DATA_DIR_PATH"
)
// Env contains the values of environment variables, as well as other entities
// which are useful across all processes.
type Env struct {
Context context.Context
AppDirPath string
DaemonYmlPath string
RuntimeDirPath string
DataDirPath string
// If NewEnv is called with bootstrapOptional, and a bootstrap file is not
// found, then these fields will not be set.
BootstrapPath string
Bootstrap bootstrap.Bootstrap
thisDaemon DaemonYml
thisDaemonOnce sync.Once
}
func getAppDirPath() string {
appDirPath := os.Getenv("APPDIR")
if appDirPath == "" {
appDirPath = "."
}
return appDirPath
}
// NewEnv calculates an Env instance based on the APPDIR and XDG envvars.
//
// If bootstrapOptional is true then NewEnv will first check if a bootstrap file
// can be found in the expected places, and if not then it will not populate
// BootstrapFS or any other fields based on it.
func NewEnv(bootstrapOptional bool) (*Env, error) {
runtimeDirPath := filepath.Join(xdg.RuntimeDir, "cryptic-net")
appDirPath := getAppDirPath()
env := &Env{
AppDirPath: appDirPath,
DaemonYmlPath: filepath.Join(runtimeDirPath, "daemon.yml"),
RuntimeDirPath: runtimeDirPath,
DataDirPath: filepath.Join(xdg.DataHome, "cryptic-net"),
}
return env, env.init(bootstrapOptional)
}
// ReadEnv reads an Env from the process's environment variables, rather than
// calculating like NewEnv does.
func ReadEnv() (*Env, error) {
var err error
readEnv := func(key string) string {
if err != nil {
return ""
}
val := os.Getenv(key)
if val == "" {
err = fmt.Errorf("envvar %q not set", key)
}
return val
}
env := &Env{
AppDirPath: getAppDirPath(),
DaemonYmlPath: readEnv(DaemonYmlPathEnvVar),
RuntimeDirPath: readEnv(RuntimeDirPathEnvVar),
DataDirPath: readEnv(DataDirPathEnvVar),
}
if err != nil {
return nil, err
}
return env, env.init(false)
}
// DataDirBootstrapPath returns the path to the bootstrap file within the user's
// data dir. If the file does not exist there it will be found in the AppDirPath
// by ReloadBootstrap.
func (e *Env) DataDirBootstrapPath() string {
return filepath.Join(e.DataDirPath, "bootstrap.tgz")
}
// LoadBootstrap sets BootstrapPath to the given value, and loads BootstrapFS
// and all derived fields based on that.
func (e *Env) LoadBootstrap(path string) error {
var err error
if e.Bootstrap, err = bootstrap.FromFile(path); err != nil {
return fmt.Errorf("parsing bootstrap.tgz at %q: %w", path, err)
}
e.BootstrapPath = path
return nil
}
func (e *Env) initBootstrap(bootstrapOptional bool) error {
exists := func(path string) (bool, error) {
if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {
return false, nil
} else if err != nil {
return false, fmt.Errorf("stat'ing %q: %w", path, err)
}
return true, nil
}
// start by checking if a bootstrap can be found in the user's data
// directory. This will only not be the case if daemon has never been
// successfully started.
{
bootstrapPath := e.DataDirBootstrapPath()
if exists, err := exists(bootstrapPath); err != nil {
return fmt.Errorf("determining if %q exists: %w", bootstrapPath, err)
} else if exists {
return e.LoadBootstrap(bootstrapPath)
}
}
// fallback to checking within the AppDir for a bootstrap which has been
// embedded into the binary.
{
bootstrapPath := filepath.Join(e.AppDirPath, "share/bootstrap.tgz")
if exists, err := exists(bootstrapPath); err != nil {
return fmt.Errorf("determining if %q exists: %w", bootstrapPath, err)
} else if !exists && !bootstrapOptional {
return fmt.Errorf("boostrap file not found at %q", bootstrapPath)
} else if exists {
return e.LoadBootstrap(bootstrapPath)
}
}
return nil
}
func (e *Env) init(bootstrapOptional bool) error {
var cancel context.CancelFunc
e.Context, cancel = context.WithCancel(context.Background())
signalCh := make(chan os.Signal, 2)
signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-signalCh
cancel()
fmt.Fprintf(os.Stderr, "got signal %v, will exit gracefully\n", sig)
sig = <-signalCh
fmt.Fprintf(os.Stderr, "second interrupt signal %v received, force quitting, there may be zombie children left behind, good luck!\n", sig)
os.Stderr.Sync()
os.Exit(1)
}()
if err := e.initBootstrap(bootstrapOptional); err != nil {
return fmt.Errorf("initializing bootstrap data: %w", err)
}
return nil
}
// ToMap returns the Env as a map of key/value strings. If this map is set into
// a process's environment, then that process can read it back using ReadEnv.
func (e *Env) ToMap() map[string]string {
return map[string]string{
DaemonYmlPathEnvVar: e.DaemonYmlPath,
BootstrapPathEnvVar: e.BootstrapPath,
RuntimeDirPathEnvVar: e.RuntimeDirPath,
DataDirPathEnvVar: e.DataDirPath,
}
}
// ThisDaemon returns the DaemonYml (loaded from DaemonYmlPath) for the
// currently running process.
func (e *Env) ThisDaemon() DaemonYml {
e.thisDaemonOnce.Do(func() {
if err := yamlutil.LoadYamlFile(&e.thisDaemon, e.DaemonYmlPath); err != nil {
panic(err)
}
})
return e.thisDaemon
}
// BinPath returns the absolute path to a binary in the AppDir.
func (e *Env) BinPath(name string) string {
return filepath.Join(e.AppDirPath, "bin", name)
}