From 98e280295b823770be0d090242256cf47e871481 Mon Sep 17 00:00:00 2001 From: Matthew Nibecker Date: Wed, 6 Sep 2023 14:28:08 -0700 Subject: [PATCH] zed: Default app data location If no lake location is specified, zed will use a per-os default location. Also if the default location is used, zed will first check if there's a service on 9867 and opt to connect to that instead of operating locally. --- cli/lakeflags/datadir.go | 36 +++++++++++++++++ cli/lakeflags/flags.go | 58 +++++++++++++++++++++------- cmd/zed/serve/command.go | 11 +----- cmd/zed/use/command.go | 2 +- cmd/zed/ztests/no-lake-location.yaml | 2 +- cmd/zed/ztests/xdg-data-home.yaml | 8 ++++ compiler/ztests/from-error.yaml | 2 +- 7 files changed, 92 insertions(+), 27 deletions(-) create mode 100644 cli/lakeflags/datadir.go create mode 100644 cmd/zed/ztests/xdg-data-home.yaml diff --git a/cli/lakeflags/datadir.go b/cli/lakeflags/datadir.go new file mode 100644 index 0000000000..d88020276d --- /dev/null +++ b/cli/lakeflags/datadir.go @@ -0,0 +1,36 @@ +package lakeflags + +import ( + "os" + "path/filepath" + "runtime" +) + +var defaultDataDir string + +func init() { + defaultDataDir = getDefaultDataDir() +} + +// getDefaultDataDir returns the default data directory for the current user. +// Derived from https://github.com/btcsuite/btcd/blob/master/btcutil/appdata.go +func getDefaultDataDir() string { + // Resolve the XDG data home directory if set. + if xdgDataHome := os.Getenv("XDG_DATA_HOME"); xdgDataHome != "" { + return filepath.Join(xdgDataHome, "zed") + } + if runtime.GOOS == "windows" { + if appData := os.Getenv("LOCALAPPDATA"); appData != "" { + return filepath.Join(appData, "zed") + } + } + if homeDir, _ := os.UserHomeDir(); homeDir != "" { + // Follow the XDG spec which states: + // If $XDG_DATA_HOME is either not set or empty, a default equal to + // $HOME/.local/share should be used. + return filepath.Join(homeDir, ".local", "share", "zed") + } + // Return an empty string which will cause an error if a default data + // directory cannot be found. + return "" +} diff --git a/cli/lakeflags/flags.go b/cli/lakeflags/flags.go index b4678fba3c..81bd329b12 100644 --- a/cli/lakeflags/flags.go +++ b/cli/lakeflags/flags.go @@ -5,6 +5,7 @@ import ( "errors" "flag" "fmt" + "net" "os" "path/filepath" "strings" @@ -21,11 +22,8 @@ var ErrNoHEAD = errors.New("HEAD not specified: indicate with -use or run the \" type Flags struct { ConfigDir string - // LakeSpecified is set to true if the lake is explicitly set via either - // command line flag or environment variable. - LakeSpecified bool - Lake string - Quiet bool + Lake string + Quiet bool } func (l *Flags) SetFlags(fs *flag.FlagSet) { @@ -35,20 +33,19 @@ func (l *Flags) SetFlags(fs *flag.FlagSet) { dir = filepath.Join(dir, ".zed") } fs.StringVar(&l.ConfigDir, "configdir", dir, "configuration and credentials directory") - l.Lake = "http://localhost:9867" if s, ok := os.LookupEnv("ZED_LAKE"); ok { l.Lake = strings.TrimRight(s, "/") - l.LakeSpecified = true + } else { + l.Lake = defaultDataDir } fs.Func("lake", fmt.Sprintf("lake location (env ZED_LAKE) (default %s)", l.Lake), func(s string) error { l.Lake = strings.TrimRight(s, "/") - l.LakeSpecified = true return nil }) } func (l *Flags) Connection() (*client.Connection, error) { - uri, err := l.URI() + uri, err := l.ClientURI() if err != nil { return nil, err } @@ -62,8 +59,16 @@ func (l *Flags) Connection() (*client.Connection, error) { return conn, nil } +func portInUse(port string) bool { + ln, err := net.Listen("tcp", ":"+port) + if err == nil { + ln.Close() + } + return err != nil +} + func (l *Flags) Open(ctx context.Context) (api.Interface, error) { - uri, err := l.URI() + uri, err := l.ClientURI() if err != nil { return nil, err } @@ -85,13 +90,38 @@ func (l *Flags) AuthStore() *auth0.Store { return auth0.NewStore(filepath.Join(l.ConfigDir, "credentials.json")) } -func (l *Flags) URI() (*storage.URI, error) { - if l.Lake == "" { - return nil, errors.New("lake location must be set (either with the -lake flag or ZED_LAKE environment variable)") - } +// ClientURI returns the URI of the lake to connect to. If the lake path is +// the defaultDataDir, it first checks if a service is running on port 9867 +// and if so, uses http://localhost:9867 as the lake location. +func (l *Flags) ClientURI() (*storage.URI, error) { u, err := storage.ParseURI(l.Lake) + // If the lake is the defaultDataDir, first check if a service is running + // on port 9867 and if so, use this as the lake location. + if u.String() == storage.MustParseURI(defaultDataDir).String() && !portInUse("9867") { + u = storage.MustParseURI("http://localhost:9867") + } if err != nil { err = fmt.Errorf("error parsing lake location: %w", err) } return u, err } + +// ServiceURI returns the URI of the lake location for a service to run at. An +// error is returned if the lake location is not a local lake. +func (l *Flags) ServiceURI() (*storage.URI, error) { + u, err := l.uri() + if err != nil { + return nil, err + } + if !api.IsLakeService(u.String()) { + return nil, errors.New("cannot open connection on local lake") + } + return u, nil +} + +func (l *Flags) uri() (*storage.URI, error) { + if l.Lake == "" { + return nil, errors.New("lake location must be set (either with the -lake flag or ZED_LAKE environment variable)") + } + return storage.ParseURI(l.Lake) +} diff --git a/cmd/zed/serve/command.go b/cmd/zed/serve/command.go index 4caf08458c..97659cfe85 100644 --- a/cmd/zed/serve/command.go +++ b/cmd/zed/serve/command.go @@ -14,7 +14,6 @@ import ( "github.com/brimdata/zed/cli" "github.com/brimdata/zed/cli/logflags" "github.com/brimdata/zed/cmd/zed/root" - "github.com/brimdata/zed/lake/api" "github.com/brimdata/zed/pkg/charm" "github.com/brimdata/zed/pkg/fs" "github.com/brimdata/zed/pkg/httpd" @@ -73,17 +72,9 @@ func (c *Command) Run(args []string) error { return err } defer cleanup() - if !c.LakeFlags.LakeSpecified { - c.LakeFlags.Lake = "" - } - uri, err := c.LakeFlags.URI() - if err != nil { + if c.conf.Root, err = c.LakeFlags.ServiceURI(); err != nil { return err } - if api.IsLakeService(uri.String()) { - return errors.New("serve command available for local lakes only") - } - c.conf.Root = uri if c.rootContentFile != "" { f, err := fs.Open(c.rootContentFile) if err != nil { diff --git a/cmd/zed/use/command.go b/cmd/zed/use/command.go index 527123c247..6923aeecb7 100644 --- a/cmd/zed/use/command.go +++ b/cmd/zed/use/command.go @@ -84,7 +84,7 @@ func (c *Command) Run(args []string) error { return errors.New("default pool and branch unset") } fmt.Printf("HEAD at %s\n", head) - if u, err := c.LakeFlags.URI(); err == nil { + if u, err := c.LakeFlags.ClientURI(); err == nil { fmt.Printf("Lake at %s\n", u) } return nil diff --git a/cmd/zed/ztests/no-lake-location.yaml b/cmd/zed/ztests/no-lake-location.yaml index cedc088f87..0fade08661 100644 --- a/cmd/zed/ztests/no-lake-location.yaml +++ b/cmd/zed/ztests/no-lake-location.yaml @@ -1,6 +1,6 @@ script: | ! zed ls -lake '' - ! zed serve + ! zed serve -lake '' outputs: - name: stderr diff --git a/cmd/zed/ztests/xdg-data-home.yaml b/cmd/zed/ztests/xdg-data-home.yaml new file mode 100644 index 0000000000..0077704ed5 --- /dev/null +++ b/cmd/zed/ztests/xdg-data-home.yaml @@ -0,0 +1,8 @@ +script: | + export XDG_DATA_HOME=path/to/lake + zed init + +outputs: + - name: stdout + data: | + lake created: path/to/lake/zed diff --git a/compiler/ztests/from-error.yaml b/compiler/ztests/from-error.yaml index 209544a444..c40e85b392 100644 --- a/compiler/ztests/from-error.yaml +++ b/compiler/ztests/from-error.yaml @@ -1,5 +1,5 @@ script: | - ! zc -C -s 'from p' + ! zc -lake='' -C -s 'from p' echo === >&2 export ZED_LAKE=test zed init