Move some environment variables into daemon package
This commit is contained in:
parent
c3609252a5
commit
c808fa81b9
@ -93,12 +93,18 @@ func New(
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FromReader reads a bootstrap file from the given io.Reader.
|
// FromFile reads a bootstrap from a file at the given path. The HostAssigned
|
||||||
func FromReader(r io.Reader) (Bootstrap, error) {
|
// field will automatically be unwrapped.
|
||||||
|
func FromFile(path string) (Bootstrap, error) {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return Bootstrap{}, fmt.Errorf("opening file: %w", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
var b Bootstrap
|
var b Bootstrap
|
||||||
|
|
||||||
err := json.NewDecoder(r).Decode(&b)
|
if err := json.NewDecoder(f).Decode(&b); err != nil {
|
||||||
if err != nil {
|
|
||||||
return Bootstrap{}, fmt.Errorf("decoding json: %w", err)
|
return Bootstrap{}, fmt.Errorf("decoding json: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,19 +112,7 @@ func FromReader(r io.Reader) (Bootstrap, error) {
|
|||||||
return Bootstrap{}, fmt.Errorf("unwrapping host assigned: %w", err)
|
return Bootstrap{}, fmt.Errorf("unwrapping host assigned: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return b, err
|
return b, nil
|
||||||
}
|
|
||||||
|
|
||||||
// FromFile reads a bootstrap from a file at the given path.
|
|
||||||
func FromFile(path string) (Bootstrap, error) {
|
|
||||||
|
|
||||||
f, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return Bootstrap{}, fmt.Errorf("opening file: %w", err)
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
return FromReader(f)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteTo writes the Bootstrap as a new bootstrap to the given io.Writer.
|
// WriteTo writes the Bootstrap as a new bootstrap to the given io.Writer.
|
||||||
|
@ -171,9 +171,7 @@ var subCmdAdminCreateNetwork = subCmd{
|
|||||||
logger.WithNamespace("daemon"),
|
logger.WithNamespace("daemon"),
|
||||||
daemonConfig,
|
daemonConfig,
|
||||||
hostBootstrap,
|
hostBootstrap,
|
||||||
envRuntimeDirPath,
|
|
||||||
envBinDirPath,
|
envBinDirPath,
|
||||||
envStateDirPath,
|
|
||||||
&daemon.Opts{
|
&daemon.Opts{
|
||||||
// SkipHostBootstrapPush is required, because the global bucket
|
// SkipHostBootstrapPush is required, because the global bucket
|
||||||
// hasn't actually been initialized yet, so there's nowhere to
|
// hasn't actually been initialized yet, so there's nowhere to
|
||||||
|
@ -11,7 +11,7 @@ import (
|
|||||||
|
|
||||||
func loadHostBootstrap() (bootstrap.Bootstrap, error) {
|
func loadHostBootstrap() (bootstrap.Bootstrap, error) {
|
||||||
|
|
||||||
stateDirPath := bootstrap.StateDirPath(envStateDirPath)
|
stateDirPath := bootstrap.StateDirPath(daemonEnvVars.StateDirPath)
|
||||||
|
|
||||||
hostBootstrap, err := bootstrap.FromFile(stateDirPath)
|
hostBootstrap, err := bootstrap.FromFile(stateDirPath)
|
||||||
if errors.Is(err, fs.ErrNotExist) {
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
@ -29,7 +29,7 @@ func loadHostBootstrap() (bootstrap.Bootstrap, error) {
|
|||||||
|
|
||||||
func writeBootstrapToStateDir(hostBootstrap bootstrap.Bootstrap) error {
|
func writeBootstrapToStateDir(hostBootstrap bootstrap.Bootstrap) error {
|
||||||
|
|
||||||
path := bootstrap.StateDirPath(envStateDirPath)
|
path := bootstrap.StateDirPath(daemonEnvVars.StateDirPath)
|
||||||
dirPath := filepath.Dir(path)
|
dirPath := filepath.Dir(path)
|
||||||
|
|
||||||
if err := os.MkdirAll(dirPath, 0700); err != nil {
|
if err := os.MkdirAll(dirPath, 0700); err != nil {
|
||||||
|
@ -98,9 +98,7 @@ func runDaemonPmuxOnce(
|
|||||||
logger.WithNamespace("daemon"),
|
logger.WithNamespace("daemon"),
|
||||||
daemonConfig,
|
daemonConfig,
|
||||||
hostBootstrap,
|
hostBootstrap,
|
||||||
envRuntimeDirPath,
|
|
||||||
envBinDirPath,
|
envBinDirPath,
|
||||||
envStateDirPath,
|
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -214,7 +212,7 @@ var subCmdDaemon = subCmd{
|
|||||||
defer runtimeDirCleanup()
|
defer runtimeDirCleanup()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
bootstrapStateDirPath = bootstrap.StateDirPath(envStateDirPath)
|
bootstrapStateDirPath = bootstrap.StateDirPath(daemonEnvVars.StateDirPath)
|
||||||
bootstrapAppDirPath = bootstrap.AppDirPath(envAppDirPath)
|
bootstrapAppDirPath = bootstrap.AppDirPath(envAppDirPath)
|
||||||
|
|
||||||
hostBootstrapPath string
|
hostBootstrapPath string
|
||||||
|
@ -66,10 +66,12 @@ func newHTTPServer(
|
|||||||
) (
|
) (
|
||||||
*http.Server, error,
|
*http.Server, error,
|
||||||
) {
|
) {
|
||||||
l, err := net.Listen("unix", envSocketPath)
|
l, err := net.Listen("unix", daemonEnvVars.HTTPSocketPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
"failed to listen on socket %q: %w", envSocketPath, err,
|
"failed to listen on socket %q: %w",
|
||||||
|
daemonEnvVars.HTTPSocketPath,
|
||||||
|
err,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ import (
|
|||||||
// order to prevent it from doing so.
|
// order to prevent it from doing so.
|
||||||
func initMCConfigDir() (string, error) {
|
func initMCConfigDir() (string, error) {
|
||||||
var (
|
var (
|
||||||
path = filepath.Join(envStateDirPath, "mc")
|
path = filepath.Join(daemonEnvVars.StateDirPath, "mc")
|
||||||
sharePath = filepath.Join(path, "share")
|
sharePath = filepath.Join(path, "share")
|
||||||
configJSONPath = filepath.Join(path, "config.json")
|
configJSONPath = filepath.Join(path, "config.json")
|
||||||
)
|
)
|
||||||
|
@ -1,47 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func envOr(name string, fallback func() string) string {
|
|
||||||
if v := os.Getenv(name); v != "" {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
return fallback()
|
|
||||||
}
|
|
||||||
|
|
||||||
func firstExistingDir(paths ...string) (string, error) {
|
|
||||||
var errs []error
|
|
||||||
for _, path := range paths {
|
|
||||||
stat, err := os.Stat(path)
|
|
||||||
switch {
|
|
||||||
case errors.Is(err, fs.ErrExist):
|
|
||||||
continue
|
|
||||||
case err != nil:
|
|
||||||
errs = append(
|
|
||||||
errs, fmt.Errorf("checking if path %q exists: %w", path, err),
|
|
||||||
)
|
|
||||||
case !stat.IsDir():
|
|
||||||
errs = append(
|
|
||||||
errs, fmt.Errorf("path %q exists but is not a directory", path),
|
|
||||||
)
|
|
||||||
default:
|
|
||||||
return path, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err := fmt.Errorf(
|
|
||||||
"no directory found at any of the following paths: %s",
|
|
||||||
strings.Join(paths, ", "),
|
|
||||||
)
|
|
||||||
if len(errs) > 0 {
|
|
||||||
err = errors.Join(slices.Insert(errs, 0, err)...)
|
|
||||||
}
|
|
||||||
return "", err
|
|
||||||
}
|
|
@ -2,7 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"isle/daemon"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -10,14 +10,8 @@ import (
|
|||||||
|
|
||||||
"dev.mediocregopher.com/mediocre-go-lib.git/mctx"
|
"dev.mediocregopher.com/mediocre-go-lib.git/mctx"
|
||||||
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
|
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
|
||||||
"github.com/adrg/xdg"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// The purpose of this binary is to act as the entrypoint of the isle
|
|
||||||
// process. It processes the command-line arguments which are passed in, and
|
|
||||||
// then passes execution along to an appropriate binary housed in AppDir/bin
|
|
||||||
// (usually a bash script, which is more versatile than a go program).
|
|
||||||
|
|
||||||
func getAppDirPath() string {
|
func getAppDirPath() string {
|
||||||
appDirPath := os.Getenv("APPDIR")
|
appDirPath := os.Getenv("APPDIR")
|
||||||
if appDirPath == "" {
|
if appDirPath == "" {
|
||||||
@ -26,39 +20,10 @@ func getAppDirPath() string {
|
|||||||
return appDirPath
|
return appDirPath
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRPCSocketDirPath() string {
|
|
||||||
path, err := firstExistingDir(
|
|
||||||
"/run",
|
|
||||||
"/var/run",
|
|
||||||
"/tmp",
|
|
||||||
"/dev/shm",
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("Failed to find directory for RPC socket: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
// RUNTIME_DIRECTORY/STATE_DIRECTORY are used by the systemd service in
|
|
||||||
// conjunction with the RuntimeDirectory/StateDirectory directives.
|
|
||||||
var (
|
var (
|
||||||
|
daemonEnvVars = daemon.GetEnvVars()
|
||||||
envAppDirPath = getAppDirPath()
|
envAppDirPath = getAppDirPath()
|
||||||
envRuntimeDirPath = envOr(
|
|
||||||
"RUNTIME_DIRECTORY",
|
|
||||||
func() string { return filepath.Join(xdg.RuntimeDir, "isle") },
|
|
||||||
)
|
|
||||||
envStateDirPath = envOr(
|
|
||||||
"STATE_DIRECTORY",
|
|
||||||
func() string { return filepath.Join(xdg.StateHome, "isle") },
|
|
||||||
)
|
|
||||||
envBinDirPath = filepath.Join(envAppDirPath, "bin")
|
envBinDirPath = filepath.Join(envAppDirPath, "bin")
|
||||||
envSocketPath = envOr(
|
|
||||||
"ISLE_SOCKET_PATH",
|
|
||||||
func() string {
|
|
||||||
return filepath.Join(getRPCSocketDirPath(), "isle-daemon.sock")
|
|
||||||
},
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func binPath(name string) string {
|
func binPath(name string) string {
|
||||||
|
@ -16,7 +16,7 @@ import (
|
|||||||
var errDaemonNotRunning = errors.New("no isle daemon process running")
|
var errDaemonNotRunning = errors.New("no isle daemon process running")
|
||||||
|
|
||||||
func lockFilePath() string {
|
func lockFilePath() string {
|
||||||
return filepath.Join(envRuntimeDirPath, "lock")
|
return filepath.Join(daemonEnvVars.RuntimeDirPath, "lock")
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeLock() error {
|
func writeLock() error {
|
||||||
@ -49,11 +49,11 @@ func writeLock() error {
|
|||||||
// returns a cleanup function which will clean up the created runtime directory.
|
// returns a cleanup function which will clean up the created runtime directory.
|
||||||
func setupAndLockRuntimeDir(ctx context.Context, logger *mlog.Logger) (func(), error) {
|
func setupAndLockRuntimeDir(ctx context.Context, logger *mlog.Logger) (func(), error) {
|
||||||
|
|
||||||
ctx = mctx.Annotate(ctx, "runtimeDirPath", envRuntimeDirPath)
|
ctx = mctx.Annotate(ctx, "runtimeDirPath", daemonEnvVars.RuntimeDirPath)
|
||||||
logger.Info(ctx, "will use runtime directory for temporary state")
|
logger.Info(ctx, "will use runtime directory for temporary state")
|
||||||
|
|
||||||
if err := os.MkdirAll(envRuntimeDirPath, 0700); err != nil {
|
if err := os.MkdirAll(daemonEnvVars.RuntimeDirPath, 0700); err != nil {
|
||||||
return nil, fmt.Errorf("creating directory %q: %w", envRuntimeDirPath, err)
|
return nil, fmt.Errorf("creating directory %q: %w", daemonEnvVars.RuntimeDirPath, err)
|
||||||
|
|
||||||
} else if err := writeLock(); err != nil {
|
} else if err := writeLock(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -61,7 +61,7 @@ func setupAndLockRuntimeDir(ctx context.Context, logger *mlog.Logger) (func(), e
|
|||||||
|
|
||||||
return func() {
|
return func() {
|
||||||
logger.Info(ctx, "cleaning up runtime directory")
|
logger.Info(ctx, "cleaning up runtime directory")
|
||||||
if err := os.RemoveAll(envRuntimeDirPath); err != nil {
|
if err := os.RemoveAll(daemonEnvVars.RuntimeDirPath); err != nil {
|
||||||
logger.Error(ctx, "removing temporary directory", err)
|
logger.Error(ctx, "removing temporary directory", err)
|
||||||
}
|
}
|
||||||
}, nil
|
}, nil
|
||||||
|
@ -110,7 +110,7 @@ func (ctx subCmdCtx) doSubCmd(subCmds ...subCmd) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
daemonRCPClient := jsonrpc2.NewUnixHTTPClient(
|
daemonRCPClient := jsonrpc2.NewUnixHTTPClient(
|
||||||
envSocketPath, daemonHTTPRPCPath,
|
daemonEnvVars.HTTPSocketPath, daemonHTTPRPCPath,
|
||||||
)
|
)
|
||||||
|
|
||||||
err := subCmd.do(subCmdCtx{
|
err := subCmd.do(subCmdCtx{
|
||||||
|
@ -18,7 +18,7 @@ type daemon struct {
|
|||||||
logger *mlog.Logger
|
logger *mlog.Logger
|
||||||
config Config
|
config Config
|
||||||
hostBootstrap bootstrap.Bootstrap
|
hostBootstrap bootstrap.Bootstrap
|
||||||
stateDirPath string
|
binDirPath string
|
||||||
opts Opts
|
opts Opts
|
||||||
|
|
||||||
pmuxCancelFn context.CancelFunc
|
pmuxCancelFn context.CancelFunc
|
||||||
@ -56,6 +56,8 @@ type Opts struct {
|
|||||||
// Stdout and Stderr are what the associated outputs from child processes
|
// Stdout and Stderr are what the associated outputs from child processes
|
||||||
// will be directed to.
|
// will be directed to.
|
||||||
Stdout, Stderr io.Writer
|
Stdout, Stderr io.Writer
|
||||||
|
|
||||||
|
EnvVars EnvVars // Defaults to that returned by GetEnvVars.
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Opts) withDefaults() *Opts {
|
func (o *Opts) withDefaults() *Opts {
|
||||||
@ -71,6 +73,10 @@ func (o *Opts) withDefaults() *Opts {
|
|||||||
o.Stderr = os.Stderr
|
o.Stderr = os.Stderr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if o.EnvVars == (EnvVars{}) {
|
||||||
|
o.EnvVars = GetEnvVars()
|
||||||
|
}
|
||||||
|
|
||||||
return o
|
return o
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,7 +87,7 @@ func New(
|
|||||||
logger *mlog.Logger,
|
logger *mlog.Logger,
|
||||||
config Config,
|
config Config,
|
||||||
hostBootstrap bootstrap.Bootstrap,
|
hostBootstrap bootstrap.Bootstrap,
|
||||||
runtimeDirPath, binDirPath, stateDirPath string,
|
binDirPath string,
|
||||||
opts *Opts,
|
opts *Opts,
|
||||||
) (
|
) (
|
||||||
Daemon, error,
|
Daemon, error,
|
||||||
@ -89,21 +95,27 @@ func New(
|
|||||||
opts = opts.withDefaults()
|
opts = opts.withDefaults()
|
||||||
|
|
||||||
nebulaPmuxProcConfig, err := nebulaPmuxProcConfig(
|
nebulaPmuxProcConfig, err := nebulaPmuxProcConfig(
|
||||||
runtimeDirPath, binDirPath, hostBootstrap, config,
|
opts.EnvVars.RuntimeDirPath,
|
||||||
|
binDirPath,
|
||||||
|
hostBootstrap,
|
||||||
|
config,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("generating nebula config: %w", err)
|
return nil, fmt.Errorf("generating nebula config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dnsmasqPmuxProcConfig, err := dnsmasqPmuxProcConfig(
|
dnsmasqPmuxProcConfig, err := dnsmasqPmuxProcConfig(
|
||||||
runtimeDirPath, binDirPath, hostBootstrap, config,
|
opts.EnvVars.RuntimeDirPath,
|
||||||
|
binDirPath,
|
||||||
|
hostBootstrap,
|
||||||
|
config,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("generating dnsmasq config: %w", err)
|
return nil, fmt.Errorf("generating dnsmasq config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
garagePmuxProcConfigs, err := garagePmuxProcConfigs(
|
garagePmuxProcConfigs, err := garagePmuxProcConfigs(
|
||||||
runtimeDirPath, binDirPath, hostBootstrap, config,
|
opts.EnvVars.RuntimeDirPath, binDirPath, hostBootstrap, config,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("generating garage children configs: %w", err)
|
return nil, fmt.Errorf("generating garage children configs: %w", err)
|
||||||
@ -125,7 +137,7 @@ func New(
|
|||||||
logger: logger,
|
logger: logger,
|
||||||
config: config,
|
config: config,
|
||||||
hostBootstrap: hostBootstrap,
|
hostBootstrap: hostBootstrap,
|
||||||
stateDirPath: stateDirPath,
|
binDirPath: binDirPath,
|
||||||
opts: *opts,
|
opts: *opts,
|
||||||
pmuxCancelFn: pmuxCancelFn,
|
pmuxCancelFn: pmuxCancelFn,
|
||||||
pmuxStoppedCh: make(chan struct{}),
|
pmuxStoppedCh: make(chan struct{}),
|
||||||
|
102
go/daemon/env.go
Normal file
102
go/daemon/env.go
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
package daemon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/adrg/xdg"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EnvVars are variables which are derived based on the environment which the
|
||||||
|
// process is running in.
|
||||||
|
type EnvVars struct {
|
||||||
|
RuntimeDirPath string // TODO should be private to this package
|
||||||
|
StateDirPath string // TODO should be private to this package
|
||||||
|
HTTPSocketPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRPCSocketDirPath() string {
|
||||||
|
path, err := firstExistingDir(
|
||||||
|
"/run",
|
||||||
|
"/var/run",
|
||||||
|
"/tmp",
|
||||||
|
"/dev/shm",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("Failed to find directory for RPC socket: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEnvVars will return the EnvVars of the current processes, as determined by
|
||||||
|
// the process's environment.
|
||||||
|
var GetEnvVars = sync.OnceValue(func() (v EnvVars) {
|
||||||
|
// RUNTIME_DIRECTORY/STATE_DIRECTORY are used by the systemd service in
|
||||||
|
// conjunction with the RuntimeDirectory/StateDirectory directives.
|
||||||
|
|
||||||
|
v.RuntimeDirPath = envOr(
|
||||||
|
"RUNTIME_DIRECTORY",
|
||||||
|
func() string { return filepath.Join(xdg.RuntimeDir, "isle") },
|
||||||
|
)
|
||||||
|
|
||||||
|
v.StateDirPath = envOr(
|
||||||
|
"STATE_DIRECTORY",
|
||||||
|
func() string { return filepath.Join(xdg.StateHome, "isle") },
|
||||||
|
)
|
||||||
|
|
||||||
|
v.HTTPSocketPath = envOr(
|
||||||
|
"ISLE_SOCKET_PATH",
|
||||||
|
func() string {
|
||||||
|
return filepath.Join(getRPCSocketDirPath(), "isle-daemon.sock")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
})
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Jigs
|
||||||
|
|
||||||
|
func envOr(name string, fallback func() string) string {
|
||||||
|
if v := os.Getenv(name); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return fallback()
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstExistingDir(paths ...string) (string, error) {
|
||||||
|
var errs []error
|
||||||
|
for _, path := range paths {
|
||||||
|
stat, err := os.Stat(path)
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, fs.ErrExist):
|
||||||
|
continue
|
||||||
|
case err != nil:
|
||||||
|
errs = append(
|
||||||
|
errs, fmt.Errorf("checking if path %q exists: %w", path, err),
|
||||||
|
)
|
||||||
|
case !stat.IsDir():
|
||||||
|
errs = append(
|
||||||
|
errs, fmt.Errorf("path %q exists but is not a directory", path),
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := fmt.Errorf(
|
||||||
|
"no directory found at any of the following paths: %s",
|
||||||
|
strings.Join(paths, ", "),
|
||||||
|
)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
err = errors.Join(slices.Insert(errs, 0, err)...)
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user