Improve logging, introduce log levels

I switched to using mlog for logging, as opposed to writing directly to
Stderr. This gives us control over log levels, as well as coordination
so that we don't have multiple go-routines writing to stderr at the same
time.
This commit is contained in:
Brian Picciano 2022-11-13 16:45:42 +01:00
parent 90a30bef5e
commit 629a8ec9b2
13 changed files with 222 additions and 50 deletions

View File

@ -7,7 +7,7 @@
pname = "cryptic-net-entrypoint";
version = "unstable";
src = ./src;
vendorSha256 = "sha256-1mHD0tmITlGjeo6F+Dvd2TdEPzxWtndy/J+uGHWKen4=";
vendorSha256 = "sha256-TTTXwztv4xwF1uXcYoSka6HwgHwU1AnClF4fguXVtK4=";
subPackages = [
"cmd/entrypoint"
];

View File

@ -6,10 +6,11 @@ import (
"cryptic-net/garage"
"cryptic-net/nebula"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
"github.com/minio/minio-go/v7"
"gopkg.in/yaml.v3"
)
@ -85,6 +86,7 @@ func RemoveGarageBootstrapHost(
// stored in garage.
func (b Bootstrap) GetGarageBootstrapHosts(
ctx context.Context,
logger *mlog.Logger,
) (
map[string]Host, error,
) {
@ -103,6 +105,8 @@ func (b Bootstrap) GetGarageBootstrapHosts(
for objInfo := range objInfoCh {
ctx := mctx.Annotate(ctx, "object-key", objInfo.Key)
if objInfo.Err != nil {
return nil, fmt.Errorf("listing objects: %w", objInfo.Err)
}
@ -132,7 +136,7 @@ func (b Bootstrap) GetGarageBootstrapHosts(
)
if err != nil {
fmt.Fprintf(os.Stderr, "unwrapping signed public creds for %q: %v\n", objInfo.Key, err)
logger.Warn(ctx, "unwrapping signed public creds", err)
continue
}
@ -143,20 +147,20 @@ func (b Bootstrap) GetGarageBootstrapHosts(
)
if err != nil {
fmt.Fprintf(os.Stderr, "invalid signed public creds for %q: %v\n", objInfo.Key, err)
logger.Warn(ctx, "invalid signed public creds", err)
continue
}
var hostPublicCreds nebula.HostPublicCredentials
if err := yaml.Unmarshal(hostPublicCredsB, &hostPublicCreds); err != nil {
fmt.Fprintf(os.Stderr, "yaml unmarshaling signed public creds for %q: %v\n", objInfo.Key, err)
logger.Warn(ctx, "yaml unmarshaling signed public creds", err)
continue
}
err = nebula.ValidateSignature(hostPublicCreds.SigningKeyPEM, hostB, hostSig)
if err != nil {
fmt.Fprintf(os.Stderr, "invalid host data for %q: %v\n", objInfo.Key, err)
logger.Warn(ctx, "invalid host data", err)
continue
}

View File

@ -16,6 +16,7 @@ import (
"strings"
"code.betamike.com/cryptic-io/pmux/pmuxlib"
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
)
func randStr(l int) string {
@ -84,10 +85,17 @@ var subCmdAdminCreateNetwork = subCmd{
"Name of this host, which will be the first host in the network",
)
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`,
)
if err := flags.Parse(subCmdCtx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
ctx := subCmdCtx.ctx
if *dumpConfig {
return daemon.CopyDefaultConfig(os.Stdout, envAppDirPath)
}
@ -96,6 +104,13 @@ var subCmdAdminCreateNetwork = subCmd{
return errors.New("--name, --domain, --ip-net, and --hostname are required")
}
logLevel := mlog.LevelFromString(*logLevelStr)
if logLevel == nil {
return fmt.Errorf("couldn't parse log level %q", *logLevelStr)
}
logger := subCmdCtx.logger.WithMaxLevel(logLevel.Int())
*domain = strings.TrimRight(strings.TrimLeft(*domain, "."), ".")
ip, subnet, err := net.ParseCIDR(*ipNetStr)
@ -107,7 +122,7 @@ var subCmdAdminCreateNetwork = subCmd{
return fmt.Errorf("invalid hostname %q: %w", *hostName, err)
}
runtimeDirCleanup, err := setupAndLockRuntimeDir()
runtimeDirCleanup, err := setupAndLockRuntimeDir(ctx, logger)
if err != nil {
return fmt.Errorf("setting up runtime directory: %w", err)
}
@ -191,10 +206,10 @@ var subCmdAdminCreateNetwork = subCmd{
),
}
ctx, cancel := context.WithCancel(subCmdCtx.ctx)
ctx, cancel := context.WithCancel(ctx)
pmuxDoneCh := make(chan struct{})
fmt.Fprintln(os.Stderr, "starting child processes")
logger.Info(ctx, "starting child processes")
go func() {
// NOTE both stdout and stderr are sent to stderr, so that the user
// can pipe the resulting admin.yml to stdout.
@ -204,22 +219,22 @@ var subCmdAdminCreateNetwork = subCmd{
defer func() {
cancel()
fmt.Fprintln(os.Stderr, "waiting for child processes to exit")
logger.Info(ctx, "waiting for child processes to exit")
<-pmuxDoneCh
}()
fmt.Fprintln(os.Stderr, "waiting for garage instances to come online")
if err := waitForGarageAndNebula(ctx, hostBootstrap, daemonConfig); err != nil {
logger.Info(ctx, "waiting for garage instances to come online")
if err := waitForGarageAndNebula(ctx, logger, hostBootstrap, daemonConfig); 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, hostBootstrap, daemonConfig); err != nil {
logger.Info(ctx, "applying initial garage layout")
if err := garageApplyLayout(ctx, logger, hostBootstrap, daemonConfig); err != nil {
return fmt.Errorf("applying initial garage layout: %w", err)
}
fmt.Fprintln(os.Stderr, "initializing garage shared global bucket")
err = garageInitializeGlobalBucket(ctx, hostBootstrap, daemonConfig)
logger.Info(ctx, "initializing garage shared global bucket")
err = garageInitializeGlobalBucket(ctx, logger, hostBootstrap, daemonConfig)
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 cryptic-net being used?")
@ -228,7 +243,7 @@ var subCmdAdminCreateNetwork = subCmd{
return fmt.Errorf("initializing garage shared global bucket: %w", err)
}
fmt.Fprintln(os.Stderr, "cluster initialized successfully, writing admin.yml to stdout")
logger.Info(ctx, "cluster initialized successfully, writing admin.yml to stdout")
adm := admin.Admin{
CreationParams: adminCreationParams,

View File

@ -14,6 +14,8 @@ import (
"cryptic-net/daemon"
"code.betamike.com/cryptic-io/pmux/pmuxlib"
"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
)
// The daemon sub-command deals with starting an actual cryptic-net daemon
@ -40,6 +42,7 @@ import (
// is overwritten and true is returned.
func reloadBootstrap(
ctx context.Context,
logger *mlog.Logger,
hostBootstrap bootstrap.Bootstrap,
) (
bootstrap.Bootstrap, bool, error,
@ -47,7 +50,7 @@ func reloadBootstrap(
thisHost := hostBootstrap.ThisHost()
newHosts, err := hostBootstrap.GetGarageBootstrapHosts(ctx)
newHosts, err := hostBootstrap.GetGarageBootstrapHosts(ctx, logger)
if err != nil {
return bootstrap.Bootstrap{}, false, fmt.Errorf("getting hosts from garage: %w", err)
}
@ -85,6 +88,7 @@ func reloadBootstrap(
// updated boostrap info.
func runDaemonPmuxOnce(
ctx context.Context,
logger *mlog.Logger,
hostBootstrap bootstrap.Bootstrap,
daemonConfig daemon.Config,
) (
@ -128,13 +132,13 @@ func runDaemonPmuxOnce(
pmuxlib.Run(ctx, os.Stdout, os.Stderr, pmuxConfig)
}()
if err := waitForGarageAndNebula(ctx, hostBootstrap, daemonConfig); err != nil {
if err := waitForGarageAndNebula(ctx, logger, hostBootstrap, daemonConfig); err != nil {
return bootstrap.Bootstrap{}, fmt.Errorf("waiting for nebula/garage to start up: %w", err)
}
err = doOnce(ctx, func(ctx context.Context) error {
if err := hostBootstrap.PutGarageBoostrapHost(ctx); err != nil {
fmt.Fprintf(os.Stderr, "updating host info in garage: %v\n", err)
logger.Error(ctx, "updating host info in garage", err)
return err
}
@ -148,8 +152,8 @@ func runDaemonPmuxOnce(
if len(daemonConfig.Storage.Allocations) > 0 {
err := doOnce(ctx, func(ctx context.Context) error {
if err := garageApplyLayout(ctx, hostBootstrap, daemonConfig); err != nil {
fmt.Fprintf(os.Stderr, "applying garage layout: %v\n", err)
if err := garageApplyLayout(ctx, logger, hostBootstrap, daemonConfig); err != nil {
logger.Error(ctx, "applying garage layout", err)
return err
}
@ -179,7 +183,7 @@ func runDaemonPmuxOnce(
err error
)
if hostBootstrap, changed, err = reloadBootstrap(ctx, hostBootstrap); err != nil {
if hostBootstrap, changed, err = reloadBootstrap(ctx, logger, hostBootstrap); err != nil {
return bootstrap.Bootstrap{}, fmt.Errorf("reloading bootstrap: %w", err)
} else if changed {
@ -212,15 +216,29 @@ var subCmdDaemon = subCmd{
`Path to a bootstrap.yml file. This only needs to be provided the first time the daemon is started, after that it is ignored. If the cryptic-net binary has a bootstrap built into it then this argument is always optional.`,
)
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`,
)
if err := flags.Parse(subCmdCtx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
ctx := subCmdCtx.ctx
if *dumpConfig {
return daemon.CopyDefaultConfig(os.Stdout, envAppDirPath)
}
runtimeDirCleanup, err := setupAndLockRuntimeDir()
logLevel := mlog.LevelFromString(*logLevelStr)
if logLevel == nil {
return fmt.Errorf("couldn't parse log level %q", *logLevelStr)
}
logger := subCmdCtx.logger.WithMaxLevel(logLevel.Int())
runtimeDirCleanup, err := setupAndLockRuntimeDir(ctx, logger)
if err != nil {
return fmt.Errorf("setting up runtime directory: %w", err)
}
@ -249,7 +267,11 @@ var subCmdDaemon = subCmd{
return false
}
fmt.Fprintf(os.Stderr, "bootstrap file found at %q\n", path)
logger.Info(
mctx.Annotate(ctx, "bootstrap-file-path", path),
"bootstrap file found",
)
hostBootstrapPath = path
return true
}
@ -289,7 +311,7 @@ var subCmdDaemon = subCmd{
for {
hostBootstrap, err = runDaemonPmuxOnce(subCmdCtx.ctx, hostBootstrap, daemonConfig)
hostBootstrap, err = runDaemonPmuxOnce(ctx, logger, hostBootstrap, daemonConfig)
if errors.Is(err, context.Canceled) {
return nil

View File

@ -11,12 +11,19 @@ import (
"strconv"
"code.betamike.com/cryptic-io/pmux/pmuxlib"
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
)
func garageAdminClientLogger(logger *mlog.Logger) *mlog.Logger {
return logger.WithNamespace("garage-admin-client")
}
// newGarageAdminClient will return an AdminClient for a local garage instance,
// or it will _panic_ if there is no local instance configured.
func newGarageAdminClient(
hostBootstrap bootstrap.Bootstrap, daemonConfig daemon.Config,
logger *mlog.Logger,
hostBootstrap bootstrap.Bootstrap,
daemonConfig daemon.Config,
) *garage.AdminClient {
thisHost := hostBootstrap.ThisHost()
@ -27,11 +34,13 @@ func newGarageAdminClient(
strconv.Itoa(daemonConfig.Storage.Allocations[0].AdminPort),
),
hostBootstrap.Garage.AdminToken,
garageAdminClientLogger(logger),
)
}
func waitForGarageAndNebula(
ctx context.Context,
logger *mlog.Logger,
hostBootstrap bootstrap.Bootstrap,
daemonConfig daemon.Config,
) error {
@ -48,6 +57,8 @@ func waitForGarageAndNebula(
return nil
}
logger = garageAdminClientLogger(logger)
for _, alloc := range allocs {
adminAddr := net.JoinHostPort(
@ -58,6 +69,7 @@ func waitForGarageAndNebula(
adminClient := garage.NewAdminClient(
adminAddr,
hostBootstrap.Garage.AdminToken,
logger,
)
if err := adminClient.Wait(ctx); err != nil {
@ -165,12 +177,13 @@ func garagePmuxProcConfigs(
func garageInitializeGlobalBucket(
ctx context.Context,
logger *mlog.Logger,
hostBootstrap bootstrap.Bootstrap,
daemonConfig daemon.Config,
) error {
var (
adminClient = newGarageAdminClient(hostBootstrap, daemonConfig)
adminClient = newGarageAdminClient(logger, hostBootstrap, daemonConfig)
globalBucketCreds = hostBootstrap.Garage.GlobalBucketS3APICredentials
)
@ -227,12 +240,13 @@ func garageInitializeGlobalBucket(
func garageApplyLayout(
ctx context.Context,
logger *mlog.Logger,
hostBootstrap bootstrap.Bootstrap,
daemonConfig daemon.Config,
) error {
var (
adminClient = newGarageAdminClient(hostBootstrap, daemonConfig)
adminClient = newGarageAdminClient(logger, hostBootstrap, daemonConfig)
thisHost = hostBootstrap.ThisHost()
hostName = thisHost.Name
allocs = daemonConfig.Storage.Allocations

View File

@ -8,6 +8,7 @@ import (
"regexp"
"sort"
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
"gopkg.in/yaml.v3"
)
@ -28,12 +29,32 @@ var subCmdHostsList = subCmd{
checkLock: true,
do: func(subCmdCtx subCmdCtx) error {
flags := subCmdCtx.flagSet(false)
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`,
)
if err := flags.Parse(subCmdCtx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
ctx := subCmdCtx.ctx
logLevel := mlog.LevelFromString(*logLevelStr)
if logLevel == nil {
return fmt.Errorf("couldn't parse log level %q", *logLevelStr)
}
logger := subCmdCtx.logger.WithMaxLevel(logLevel.Int())
hostBootstrap, err := loadHostBootstrap()
if err != nil {
return fmt.Errorf("loading host bootstrap: %w", err)
}
hostsMap, err := hostBootstrap.GetGarageBootstrapHosts(subCmdCtx.ctx)
hostsMap, err := hostBootstrap.GetGarageBootstrapHosts(ctx, logger)
if err != nil {
return fmt.Errorf("retrieving hosts from garage: %w", err)
}

View File

@ -0,0 +1,56 @@
package main
import (
"fmt"
"os"
"path"
"sync"
"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
)
type logMsgHandler struct {
stderr *os.File
l sync.Mutex
}
func newLogMsgHandler() mlog.MessageHandler {
return &logMsgHandler{
stderr: os.Stderr,
}
}
func (h *logMsgHandler) Sync() error {
return h.stderr.Sync()
}
func (h *logMsgHandler) Handle(msg mlog.FullMessage) error {
h.l.Lock()
defer h.l.Unlock()
var namespaceStr string
if len(msg.Namespace) > 0 {
namespaceStr = "[" + path.Join(msg.Namespace...) + "] "
}
var annotationsStr string
if m := mctx.EvaluateAnnotations(msg.Context, nil).StringMap(); len(m) > 0 {
for k, v := range m {
annotationsStr += fmt.Sprintf(" %q=%q", k, v)
}
}
fmt.Fprintf(
h.stderr, "%s %s%s%s\n",
msg.Level.String(),
namespaceStr,
msg.Description,
annotationsStr,
)
return nil
}

View File

@ -2,13 +2,14 @@ package main
import (
"context"
"fmt"
"os"
"os/signal"
"path/filepath"
"syscall"
"github.com/adrg/xdg"
"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
)
// The purpose of this binary is to act as the entrypoint of the cryptic-net
@ -32,6 +33,12 @@ var (
func main() {
logger := mlog.NewLogger(&mlog.LoggerOpts{
MessageHandler: newLogMsgHandler(),
MaxLevel: mlog.LevelInfo.Int(),
})
defer logger.Close()
ctx, cancel := context.WithCancel(context.Background())
signalCh := make(chan os.Signal, 2)
@ -40,18 +47,20 @@ func main() {
go func() {
sig := <-signalCh
cancel()
fmt.Fprintf(os.Stderr, "got signal %v, will exit gracefully\n", sig)
ctx := mctx.Annotate(ctx, "signal", sig.String())
logger.Info(ctx, "got signal, exiting gracefully")
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)
ctx = mctx.Annotate(ctx, "signal", sig.String())
logger.FatalString(ctx, "second signal received, force quitting, there may be zombie children left behind, good luck!")
}()
err := subCmdCtx{
args: os.Args[1:],
ctx: ctx,
args: os.Args[1:],
ctx: ctx,
logger: logger,
}.doSubCmd(
subCmdAdmin,
subCmdDaemon,
@ -61,6 +70,6 @@ func main() {
)
if err != nil {
panic(err)
logger.Fatal(ctx, "error running command", err)
}
}

View File

@ -1,12 +1,15 @@
package main
import (
"context"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
"github.com/shirou/gopsutil/process"
)
@ -44,9 +47,10 @@ func writeLock() error {
}
// returns a cleanup function which will clean up the created runtime directory.
func setupAndLockRuntimeDir() (func(), error) {
func setupAndLockRuntimeDir(ctx context.Context, logger *mlog.Logger) (func(), error) {
fmt.Fprintf(os.Stderr, "will use runtime directory %q for temporary state\n", envRuntimeDirPath)
ctx = mctx.Annotate(ctx, "runtime-dir-path", envRuntimeDirPath)
logger.Info(ctx, "will use runtime directory for temporary state")
if err := os.MkdirAll(envRuntimeDirPath, 0700); err != nil {
return nil, fmt.Errorf("creating directory %q: %w", envRuntimeDirPath, err)
@ -56,9 +60,9 @@ func setupAndLockRuntimeDir() (func(), error) {
}
return func() {
fmt.Fprintf(os.Stderr, "cleaning up runtime directory %q\n", envRuntimeDirPath)
logger.Info(ctx, "cleaning up runtime directory")
if err := os.RemoveAll(envRuntimeDirPath); err != nil {
fmt.Fprintf(os.Stderr, "error removing temporary directory %q: %v", envRuntimeDirPath, err)
logger.Error(ctx, "removing temporary directory", err)
}
}, nil
}

View File

@ -6,6 +6,7 @@ import (
"os"
"strings"
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
"github.com/spf13/pflag"
)
@ -15,7 +16,8 @@ type subCmdCtx struct {
args []string // command-line arguments, excluding the subCmd itself.
subCmdNames []string // names of subCmds so far, including this one
ctx context.Context
ctx context.Context
logger *mlog.Logger
}
type subCmd struct {
@ -110,6 +112,7 @@ func (ctx subCmdCtx) doSubCmd(subCmds ...subCmd) error {
args: args,
subCmdNames: append(ctx.subCmdNames, subCmdName),
ctx: ctx.ctx,
logger: ctx.logger,
})
if err != nil {

View File

@ -7,7 +7,10 @@ import (
"fmt"
"io"
"net/http"
"net/http/httputil"
"time"
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
)
// AdminClientError gets returned from AdminClient's Do method for non-200
@ -27,17 +30,21 @@ type AdminClient struct {
c *http.Client
addr string
adminToken string
logger *mlog.Logger
}
// NewAdminClient initializes and returns an AdminClient which will use the
// given address and adminToken for all requests made.
func NewAdminClient(addr, adminToken string) *AdminClient {
//
// If Logger is nil then logs will be suppressed.
func NewAdminClient(addr, adminToken string, logger *mlog.Logger) *AdminClient {
return &AdminClient{
c: &http.Client{
Transport: http.DefaultTransport.(*http.Transport).Clone(),
},
addr: addr,
adminToken: adminToken,
logger: logger,
}
}
@ -68,11 +75,31 @@ func (c *AdminClient) Do(
req.Header.Set("Authorization", "Bearer "+c.adminToken)
if c.logger.MaxLevel() >= mlog.LevelDebug.Int() {
reqB, err := httputil.DumpRequestOut(req, true)
if err != nil {
c.logger.Error(ctx, "failed to dump http request", err)
} else {
c.logger.Debug(ctx, "------ request ------\n"+string(reqB)+"\n")
}
}
res, err := c.c.Do(req)
if err != nil {
return fmt.Errorf("performing http request: %w", err)
}
if c.logger.MaxLevel() >= mlog.LevelDebug.Int() {
resB, err := httputil.DumpResponse(res, true)
if err != nil {
c.logger.Error(ctx, "failed to dump http response", err)
} else {
c.logger.Debug(ctx, "------ response ------\n"+string(resB)+"\n")
}
}
defer res.Body.Close()
if res.StatusCode != 200 {

View File

@ -6,8 +6,8 @@ require (
code.betamike.com/cryptic-io/pmux v0.0.0-20221025185405-29241f144a2d
github.com/adrg/xdg v0.4.0
github.com/imdario/mergo v0.3.12
github.com/mediocregopher/mediocre-go-lib/v2 v2.0.0-beta.1.0.20221113151154-07f3889a705b
github.com/minio/minio-go/v7 v7.0.28
github.com/nlepage/go-tarfs v1.1.0
github.com/shirou/gopsutil v3.21.11+incompatible
github.com/slackhq/nebula v1.6.1
github.com/spf13/pflag v1.0.5

View File

@ -1,5 +1,3 @@
code.betamike.com/cryptic-io/pmux v0.0.0-20221020185531-7a7868003822 h1:c7Eu2h8gXOpOfhC1LvSYLNfiSsWTyvdI1XVpUuqMFHE=
code.betamike.com/cryptic-io/pmux v0.0.0-20221020185531-7a7868003822/go.mod h1:cBuEN/rkaM/GH24uQroX/++qDmte+mLudDUqMt6XJWs=
code.betamike.com/cryptic-io/pmux v0.0.0-20221025185405-29241f144a2d h1:s6nDTg23o9ujZZnl8ohZBDoG4SqPUyFfvod9DQjwmNU=
code.betamike.com/cryptic-io/pmux v0.0.0-20221025185405-29241f144a2d/go.mod h1:cBuEN/rkaM/GH24uQroX/++qDmte+mLudDUqMt6XJWs=
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
@ -36,6 +34,8 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mediocregopher/mediocre-go-lib/v2 v2.0.0-beta.1.0.20221113151154-07f3889a705b h1:A+IqPY72GXChyCje7YqnZrb8Q4ajUqft/etsmIOobu4=
github.com/mediocregopher/mediocre-go-lib/v2 v2.0.0-beta.1.0.20221113151154-07f3889a705b/go.mod h1:wOZVlnKYvIbkzyCJ3dxy1k40XkirvCd1pisX2O91qoQ=
github.com/minio/md5-simd v1.1.0 h1:QPfiOqlZH+Cj9teu0t9b1nTBfPbyTl16Of5MeuShdK4=
github.com/minio/md5-simd v1.1.0/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw=
github.com/minio/minio-go/v7 v7.0.28 h1:VMr3K5qGIEt+/KW3poopRh8mzi5RwuCjmrmstK196Fg=
@ -49,8 +49,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nlepage/go-tarfs v1.1.0 h1:bsACOiZMB/zFjYG/sE01070i9Fl26MnRpw0L6WuyfVs=
github.com/nlepage/go-tarfs v1.1.0/go.mod h1:IhxRcLhLkawBetnwu/JNuoPkq/6cclAllhgEa6SmzS8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=
@ -71,7 +69,6 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw=
github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk=
github.com/tklauser/numcpus v0.4.0 h1:E53Dm1HjH1/R2/aoCtXtPgzmElmn51aOkhCFSuZq//o=