253 lines
6.4 KiB
Go
253 lines
6.4 KiB
Go
|
package crypticnet
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"cryptic-net/tarutil"
|
||
|
"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
|
||
|
BootstrapFS fs.FS
|
||
|
Hosts map[string]Host
|
||
|
HostName string
|
||
|
|
||
|
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
|
||
|
|
||
|
// load all values into temp variables before setting the fields on Env,
|
||
|
// so we don't leave it in an inconsistent state.
|
||
|
bootstrapFS fs.FS
|
||
|
hosts map[string]Host
|
||
|
hostNameB []byte
|
||
|
)
|
||
|
|
||
|
if bootstrapFS, err = tarutil.FSFromTGZFile(path); err != nil {
|
||
|
return fmt.Errorf("reading bootstrap file at %q: %w", e.BootstrapPath, err)
|
||
|
}
|
||
|
|
||
|
if hosts, err = LoadHosts(bootstrapFS); err != nil {
|
||
|
return fmt.Errorf("loading hosts info from bootstrap fs: %w", err)
|
||
|
}
|
||
|
|
||
|
if hostNameB, err = fs.ReadFile(bootstrapFS, "hostname"); err != nil {
|
||
|
return fmt.Errorf("loading hostname from bootstrap fs: %w", err)
|
||
|
}
|
||
|
|
||
|
e.BootstrapPath = path
|
||
|
e.BootstrapFS = bootstrapFS
|
||
|
e.Hosts = hosts
|
||
|
e.HostName = string(hostNameB)
|
||
|
|
||
|
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
|
||
|
}
|
||
|
|
||
|
// ThisHost is a shortcut for returning env.Hosts[env.HostName].
|
||
|
func (e *Env) ThisHost() Host {
|
||
|
return e.Hosts[e.HostName]
|
||
|
}
|
||
|
|
||
|
// 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)
|
||
|
}
|