Fix `panic: $HOME is not set` (#1027)

This PR adds error to `env.UserHomeDir(ctx)`

Fixes https://github.com/databricks/setup-cli/issues/73

---------

Co-authored-by: Pieter Noordhuis <pieter.noordhuis@databricks.com>
This commit is contained in:
Serge Smertin 2023-11-29 20:08:27 +01:00 committed by GitHub
parent 4d8d825746
commit 65458cbde6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 168 additions and 96 deletions

View File

@ -19,13 +19,17 @@ func newClearCacheCommand() *cobra.Command {
if err != nil { if err != nil {
return err return err
} }
_ = os.Remove(project.PathInLabs(ctx, "databrickslabs-repositories.json")) cache, err := project.PathInLabs(ctx, "databrickslabs-repositories.json")
if err != nil {
return err
}
_ = os.Remove(cache)
logger := log.GetLogger(ctx) logger := log.GetLogger(ctx)
for _, prj := range projects { for _, prj := range projects {
logger.Info("clearing labs project cache", slog.String("name", prj.Name)) logger.Info("clearing labs project cache", slog.String("name", prj.Name))
_ = os.RemoveAll(prj.CacheDir(ctx)) _ = os.RemoveAll(prj.CacheDir())
// recreating empty cache folder for downstream apps to work normally // recreating empty cache folder for downstream apps to work normally
_ = prj.EnsureFoldersExist(ctx) _ = prj.EnsureFoldersExist()
} }
return nil return nil
}, },

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"github.com/databricks/cli/cmd/labs/project" "github.com/databricks/cli/cmd/labs/project"
"github.com/databricks/cli/libs/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -30,7 +31,8 @@ func New(ctx context.Context) *cobra.Command {
) )
all, err := project.Installed(ctx) all, err := project.Installed(ctx)
if err != nil { if err != nil {
panic(err) log.Errorf(ctx, "Cannot retrieve installed labs: %s", err)
return cmd
} }
for _, v := range all { for _, v := range all {
v.Register(cmd) v.Register(cmd)

View File

@ -16,7 +16,10 @@ type labsMeta struct {
} }
func allRepos(ctx context.Context) (github.Repositories, error) { func allRepos(ctx context.Context) (github.Repositories, error) {
cacheDir := project.PathInLabs(ctx) cacheDir, err := project.PathInLabs(ctx)
if err != nil {
return nil, err
}
cache := github.NewRepositoryCache("databrickslabs", cacheDir) cache := github.NewRepositoryCache("databrickslabs", cacheDir)
return cache.Load(ctx) return cache.Load(ctx)
} }

View File

@ -54,15 +54,15 @@ func (e *Entrypoint) NeedsWarehouse() bool {
func (e *Entrypoint) Prepare(cmd *cobra.Command) (map[string]string, error) { func (e *Entrypoint) Prepare(cmd *cobra.Command) (map[string]string, error) {
ctx := cmd.Context() ctx := cmd.Context()
libDir := e.EffectiveLibDir(ctx) libDir := e.EffectiveLibDir()
environment := map[string]string{ environment := map[string]string{
"DATABRICKS_CLI_VERSION": build.GetInfo().Version, "DATABRICKS_CLI_VERSION": build.GetInfo().Version,
"DATABRICKS_LABS_CACHE_DIR": e.CacheDir(ctx), "DATABRICKS_LABS_CACHE_DIR": e.CacheDir(),
"DATABRICKS_LABS_CONFIG_DIR": e.ConfigDir(ctx), "DATABRICKS_LABS_CONFIG_DIR": e.ConfigDir(),
"DATABRICKS_LABS_STATE_DIR": e.StateDir(ctx), "DATABRICKS_LABS_STATE_DIR": e.StateDir(),
"DATABRICKS_LABS_LIB_DIR": libDir, "DATABRICKS_LABS_LIB_DIR": libDir,
} }
if e.IsPythonProject(ctx) { if e.IsPythonProject() {
e.preparePython(ctx, environment) e.preparePython(ctx, environment)
} }
cfg, err := e.validLogin(cmd) cfg, err := e.validLogin(cmd)
@ -112,7 +112,7 @@ func (e *Entrypoint) preparePython(ctx context.Context, environment map[string]s
// Here we are also supporting the "src" layout for python projects. // Here we are also supporting the "src" layout for python projects.
// //
// See https://docs.python.org/3/using/cmdline.html#envvar-PYTHONPATH // See https://docs.python.org/3/using/cmdline.html#envvar-PYTHONPATH
libDir := e.EffectiveLibDir(ctx) libDir := e.EffectiveLibDir()
// The intention for every install is to be sandboxed - not dependent on anything else than Python binary. // The intention for every install is to be sandboxed - not dependent on anything else than Python binary.
// Having ability to override PYTHONPATH in the mix will break this assumption. Need strong evidence that // Having ability to override PYTHONPATH in the mix will break this assumption. Need strong evidence that
// this is really needed. // this is really needed.
@ -139,21 +139,28 @@ func (e *Entrypoint) joinPaths(paths ...string) string {
return strings.Join(paths, string(os.PathListSeparator)) return strings.Join(paths, string(os.PathListSeparator))
} }
func (e *Entrypoint) envAwareConfig(ctx context.Context) *config.Config { func (e *Entrypoint) envAwareConfig(ctx context.Context) (*config.Config, error) {
home, err := env.UserHomeDir(ctx)
if err != nil {
return nil, err
}
return &config.Config{ return &config.Config{
ConfigFile: filepath.Join(env.UserHomeDir(ctx), ".databrickscfg"), ConfigFile: filepath.Join(home, ".databrickscfg"),
Loaders: []config.Loader{ Loaders: []config.Loader{
env.NewConfigLoader(ctx), env.NewConfigLoader(ctx),
config.ConfigAttributes, config.ConfigAttributes,
config.ConfigFile, config.ConfigFile,
}, },
} }, nil
} }
func (e *Entrypoint) envAwareConfigWithProfile(ctx context.Context, profile string) *config.Config { func (e *Entrypoint) envAwareConfigWithProfile(ctx context.Context, profile string) (*config.Config, error) {
cfg := e.envAwareConfig(ctx) cfg, err := e.envAwareConfig(ctx)
if err != nil {
return nil, err
}
cfg.Profile = profile cfg.Profile = profile
return cfg return cfg, nil
} }
func (e *Entrypoint) getLoginConfig(cmd *cobra.Command) (*loginConfig, *config.Config, error) { func (e *Entrypoint) getLoginConfig(cmd *cobra.Command) (*loginConfig, *config.Config, error) {
@ -164,11 +171,18 @@ func (e *Entrypoint) getLoginConfig(cmd *cobra.Command) (*loginConfig, *config.C
profileOverride := e.profileOverride(cmd) profileOverride := e.profileOverride(cmd)
if profileOverride != "" { if profileOverride != "" {
log.Infof(ctx, "Overriding login profile: %s", profileOverride) log.Infof(ctx, "Overriding login profile: %s", profileOverride)
return &loginConfig{}, e.envAwareConfigWithProfile(ctx, profileOverride), nil cfg, err := e.envAwareConfigWithProfile(ctx, profileOverride)
if err != nil {
return nil, nil, err
}
return &loginConfig{}, cfg, nil
} }
lc, err := e.loadLoginConfig(ctx) lc, err := e.loadLoginConfig(ctx)
isNoLoginConfig := errors.Is(err, fs.ErrNotExist) isNoLoginConfig := errors.Is(err, fs.ErrNotExist)
defaultConfig := e.envAwareConfig(ctx) defaultConfig, err := e.envAwareConfig(ctx)
if err != nil {
return nil, nil, err
}
if isNoLoginConfig && !e.IsBundleAware && e.isAuthConfigured(defaultConfig) { if isNoLoginConfig && !e.IsBundleAware && e.isAuthConfigured(defaultConfig) {
log.Debugf(ctx, "Login is configured via environment variables") log.Debugf(ctx, "Login is configured via environment variables")
return &loginConfig{}, defaultConfig, nil return &loginConfig{}, defaultConfig, nil
@ -181,7 +195,11 @@ func (e *Entrypoint) getLoginConfig(cmd *cobra.Command) (*loginConfig, *config.C
} }
if e.IsAccountLevel { if e.IsAccountLevel {
log.Debugf(ctx, "Using account-level login profile: %s", lc.AccountProfile) log.Debugf(ctx, "Using account-level login profile: %s", lc.AccountProfile)
return lc, e.envAwareConfigWithProfile(ctx, lc.AccountProfile), nil cfg, err := e.envAwareConfigWithProfile(ctx, lc.AccountProfile)
if err != nil {
return nil, nil, err
}
return lc, cfg, nil
} }
if e.IsBundleAware { if e.IsBundleAware {
err = root.TryConfigureBundle(cmd, []string{}) err = root.TryConfigureBundle(cmd, []string{})
@ -194,7 +212,11 @@ func (e *Entrypoint) getLoginConfig(cmd *cobra.Command) (*loginConfig, *config.C
} }
} }
log.Debugf(ctx, "Using workspace-level login profile: %s", lc.WorkspaceProfile) log.Debugf(ctx, "Using workspace-level login profile: %s", lc.WorkspaceProfile)
return lc, e.envAwareConfigWithProfile(ctx, lc.WorkspaceProfile), nil cfg, err := e.envAwareConfigWithProfile(ctx, lc.WorkspaceProfile)
if err != nil {
return nil, nil, err
}
return lc, cfg, nil
} }
func (e *Entrypoint) validLogin(cmd *cobra.Command) (*config.Config, error) { func (e *Entrypoint) validLogin(cmd *cobra.Command) (*config.Config, error) {

View File

@ -29,7 +29,10 @@ func (d *devInstallation) Install(ctx context.Context) error {
} }
_, err := d.Installer.validLogin(d.Command) _, err := d.Installer.validLogin(d.Command)
if errors.Is(err, ErrNoLoginConfig) { if errors.Is(err, ErrNoLoginConfig) {
cfg := d.Installer.envAwareConfig(ctx) cfg, err := d.Installer.envAwareConfig(ctx)
if err != nil {
return err
}
lc := &loginConfig{Entrypoint: d.Installer.Entrypoint} lc := &loginConfig{Entrypoint: d.Installer.Entrypoint}
_, err = lc.askWorkspace(ctx, cfg) _, err = lc.askWorkspace(ctx, cfg)
if err != nil { if err != nil {
@ -39,7 +42,7 @@ func (d *devInstallation) Install(ctx context.Context) error {
if err != nil { if err != nil {
return fmt.Errorf("ask for account: %w", err) return fmt.Errorf("ask for account: %w", err)
} }
err = lc.EnsureFoldersExist(ctx) err = lc.EnsureFoldersExist()
if err != nil { if err != nil {
return fmt.Errorf("folders: %w", err) return fmt.Errorf("folders: %w", err)
} }
@ -97,7 +100,10 @@ func NewUpgrader(cmd *cobra.Command, name string) (*installer, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("remote: %w", err) return nil, fmt.Errorf("remote: %w", err)
} }
prj.folder = PathInLabs(cmd.Context(), name) prj.folder, err = PathInLabs(cmd.Context(), name)
if err != nil {
return nil, err
}
return &installer{ return &installer{
Project: prj, Project: prj,
version: version, version: version,
@ -111,7 +117,10 @@ type fetcher struct {
func (f *fetcher) checkReleasedVersions(cmd *cobra.Command, version string) (string, error) { func (f *fetcher) checkReleasedVersions(cmd *cobra.Command, version string) (string, error) {
ctx := cmd.Context() ctx := cmd.Context()
cacheDir := PathInLabs(ctx, f.name, "cache") cacheDir, err := PathInLabs(ctx, f.name, "cache")
if err != nil {
return "", err
}
// `databricks labs isntall X` doesn't know which exact version to fetch, so first // `databricks labs isntall X` doesn't know which exact version to fetch, so first
// we fetch all versions and then pick the latest one dynamically. // we fetch all versions and then pick the latest one dynamically.
versions, err := github.NewReleaseCache("databrickslabs", f.name, cacheDir).Load(ctx) versions, err := github.NewReleaseCache("databrickslabs", f.name, cacheDir).Load(ctx)

View File

@ -12,10 +12,13 @@ import (
"github.com/databricks/cli/libs/env" "github.com/databricks/cli/libs/env"
) )
func PathInLabs(ctx context.Context, dirs ...string) string { func PathInLabs(ctx context.Context, dirs ...string) (string, error) {
homdeDir := env.UserHomeDir(ctx) homeDir, err := env.UserHomeDir(ctx)
prefix := []string{homdeDir, ".databricks", "labs"} if err != nil {
return filepath.Join(append(prefix, dirs...)...) return "", err
}
prefix := []string{homeDir, ".databricks", "labs"}
return filepath.Join(append(prefix, dirs...)...), nil
} }
func tryLoadAndParseJSON[T any](jsonFile string) (*T, error) { func tryLoadAndParseJSON[T any](jsonFile string) (*T, error) {

View File

@ -9,6 +9,7 @@ import (
"path/filepath" "path/filepath"
"github.com/databricks/cli/folders" "github.com/databricks/cli/folders"
"github.com/databricks/cli/libs/env"
"github.com/databricks/cli/libs/log" "github.com/databricks/cli/libs/log"
) )
@ -26,7 +27,13 @@ func projectInDevMode(ctx context.Context) (*Project, error) {
} }
func Installed(ctx context.Context) (projects []*Project, err error) { func Installed(ctx context.Context) (projects []*Project, err error) {
labsDir, err := os.ReadDir(PathInLabs(ctx)) root, err := PathInLabs(ctx)
if errors.Is(err, env.ErrNoHomeEnv) {
return nil, nil
} else if err != nil {
return nil, err
}
labsDir, err := os.ReadDir(root)
if err != nil && !errors.Is(err, fs.ErrNotExist) { if err != nil && !errors.Is(err, fs.ErrNotExist) {
return nil, err return nil, err
} }
@ -44,7 +51,7 @@ func Installed(ctx context.Context) (projects []*Project, err error) {
if projectDev != nil && v.Name() == projectDev.Name { if projectDev != nil && v.Name() == projectDev.Name {
continue continue
} }
labsYml := PathInLabs(ctx, v.Name(), "lib", "labs.yml") labsYml := filepath.Join(root, v.Name(), "lib", "labs.yml")
prj, err := Load(ctx, labsYml) prj, err := Load(ctx, labsYml)
if errors.Is(err, fs.ErrNotExist) { if errors.Is(err, fs.ErrNotExist) {
continue continue

View File

@ -55,7 +55,7 @@ func (h *hook) runHook(cmd *cobra.Command) error {
if err != nil { if err != nil {
return fmt.Errorf("prepare: %w", err) return fmt.Errorf("prepare: %w", err)
} }
libDir := h.EffectiveLibDir(ctx) libDir := h.EffectiveLibDir()
args := []string{} args := []string{}
if strings.HasSuffix(h.Script, ".py") { if strings.HasSuffix(h.Script, ".py") {
args = append(args, h.virtualEnvPython(ctx)) args = append(args, h.virtualEnvPython(ctx))
@ -80,14 +80,20 @@ type installer struct {
} }
func (i *installer) Install(ctx context.Context) error { func (i *installer) Install(ctx context.Context) error {
err := i.EnsureFoldersExist(ctx) err := i.EnsureFoldersExist()
if err != nil { if err != nil {
return fmt.Errorf("folders: %w", err) return fmt.Errorf("folders: %w", err)
} }
i.folder = PathInLabs(ctx, i.Name) i.folder, err = PathInLabs(ctx, i.Name)
if err != nil {
return err
}
w, err := i.login(ctx) w, err := i.login(ctx)
if err != nil && errors.Is(err, databrickscfg.ErrNoConfiguration) { if err != nil && errors.Is(err, databrickscfg.ErrNoConfiguration) {
cfg := i.Installer.envAwareConfig(ctx) cfg, err := i.Installer.envAwareConfig(ctx)
if err != nil {
return err
}
w, err = databricks.NewWorkspaceClient((*databricks.Config)(cfg)) w, err = databricks.NewWorkspaceClient((*databricks.Config)(cfg))
if err != nil { if err != nil {
return fmt.Errorf("no ~/.databrickscfg: %w", err) return fmt.Errorf("no ~/.databrickscfg: %w", err)
@ -138,7 +144,7 @@ func (i *installer) warningf(text string, v ...any) {
} }
func (i *installer) cleanupLib(ctx context.Context) error { func (i *installer) cleanupLib(ctx context.Context) error {
libDir := i.LibDir(ctx) libDir := i.LibDir()
err := os.RemoveAll(libDir) err := os.RemoveAll(libDir)
if err != nil { if err != nil {
return fmt.Errorf("remove all: %w", err) return fmt.Errorf("remove all: %w", err)
@ -157,7 +163,10 @@ func (i *installer) login(ctx context.Context) (*databricks.WorkspaceClient, err
} }
cfg, err := i.metaEntrypoint(ctx).validLogin(i.cmd) cfg, err := i.metaEntrypoint(ctx).validLogin(i.cmd)
if errors.Is(err, ErrNoLoginConfig) { if errors.Is(err, ErrNoLoginConfig) {
cfg = i.Installer.envAwareConfig(ctx) cfg, err = i.Installer.envAwareConfig(ctx)
if err != nil {
return nil, err
}
} else if err != nil { } else if err != nil {
return nil, fmt.Errorf("valid: %w", err) return nil, fmt.Errorf("valid: %w", err)
} }
@ -188,7 +197,7 @@ func (i *installer) downloadLibrary(ctx context.Context) error {
if err != nil { if err != nil {
return fmt.Errorf("cleanup: %w", err) return fmt.Errorf("cleanup: %w", err)
} }
libTarget := i.LibDir(ctx) libTarget := i.LibDir()
// we may support wheels, jars, and golang binaries. but those are not zipballs // we may support wheels, jars, and golang binaries. but those are not zipballs
if i.IsZipball() { if i.IsZipball() {
feedback <- fmt.Sprintf("Downloading and unpacking zipball for %s", i.version) feedback <- fmt.Sprintf("Downloading and unpacking zipball for %s", i.version)
@ -254,10 +263,10 @@ func (i *installer) setupPythonVirtualEnvironment(ctx context.Context, w *databr
} }
func (i *installer) installPythonDependencies(ctx context.Context, spec string) error { func (i *installer) installPythonDependencies(ctx context.Context, spec string) error {
if !i.IsPythonProject(ctx) { if !i.IsPythonProject() {
return nil return nil
} }
libDir := i.LibDir(ctx) libDir := i.LibDir()
log.Debugf(ctx, "Installing Python dependencies for: %s", libDir) log.Debugf(ctx, "Installing Python dependencies for: %s", libDir)
// maybe we'll need to add call one of the two scripts: // maybe we'll need to add call one of the two scripts:
// - python3 -m ensurepip --default-pip // - python3 -m ensurepip --default-pip
@ -281,6 +290,6 @@ func (i *installer) runInstallHook(ctx context.Context) error {
if i.Installer.Script == "" { if i.Installer.Script == "" {
return nil return nil
} }
log.Debugf(ctx, "Launching installer script %s in %s", i.Installer.Script, i.LibDir(ctx)) log.Debugf(ctx, "Launching installer script %s in %s", i.Installer.Script, i.LibDir())
return i.Installer.runHook(i.cmd) return i.Installer.runHook(i.cmd)
} }

View File

@ -105,7 +105,7 @@ func installerContext(t *testing.T, server *httptest.Server) context.Context {
ctx = github.WithUserContentOverride(ctx, server.URL) ctx = github.WithUserContentOverride(ctx, server.URL)
ctx = env.WithUserHomeDir(ctx, t.TempDir()) ctx = env.WithUserHomeDir(ctx, t.TempDir())
// trick release cache to thing it went to github already // trick release cache to thing it went to github already
cachePath := project.PathInLabs(ctx, "blueprint", "cache") cachePath, _ := project.PathInLabs(ctx, "blueprint", "cache")
err := os.MkdirAll(cachePath, ownerRWXworldRX) err := os.MkdirAll(cachePath, ownerRWXworldRX)
require.NoError(t, err) require.NoError(t, err)
bs := []byte(`{"refreshed_at": "2033-01-01T00:00:00.92857+02:00","data": [{"tag_name": "v0.3.15"}]}`) bs := []byte(`{"refreshed_at": "2033-01-01T00:00:00.92857+02:00","data": [{"tag_name": "v0.3.15"}]}`)
@ -317,8 +317,8 @@ func TestInstallerWorksForDevelopment(t *testing.T) {
// development installer assumes it's in the active virtualenv // development installer assumes it's in the active virtualenv
ctx = env.Set(ctx, "PYTHON_BIN", py) ctx = env.Set(ctx, "PYTHON_BIN", py)
home, _ := env.UserHomeDir(ctx)
err = os.WriteFile(filepath.Join(env.UserHomeDir(ctx), ".databrickscfg"), []byte(fmt.Sprintf(` err = os.WriteFile(filepath.Join(home, ".databrickscfg"), []byte(fmt.Sprintf(`
[profile-one] [profile-one]
host = %s host = %s
token = ... token = ...
@ -399,7 +399,7 @@ func TestUpgraderWorksForReleases(t *testing.T) {
py, _ = filepath.Abs(py) py, _ = filepath.Abs(py)
ctx = env.Set(ctx, "PYTHON_BIN", py) ctx = env.Set(ctx, "PYTHON_BIN", py)
cachePath := project.PathInLabs(ctx, "blueprint", "cache") cachePath, _ := project.PathInLabs(ctx, "blueprint", "cache")
bs := []byte(`{"refreshed_at": "2033-01-01T00:00:00.92857+02:00","data": [{"tag_name": "v0.4.0"}]}`) bs := []byte(`{"refreshed_at": "2033-01-01T00:00:00.92857+02:00","data": [{"tag_name": "v0.4.0"}]}`)
err := os.WriteFile(filepath.Join(cachePath, "databrickslabs-blueprint-releases.json"), bs, ownerRW) err := os.WriteFile(filepath.Join(cachePath, "databrickslabs-blueprint-releases.json"), bs, ownerRW)
require.NoError(t, err) require.NoError(t, err)

View File

@ -49,6 +49,11 @@ func readFromBytes(ctx context.Context, labsYmlRaw []byte) (*Project, error) {
if project.Uninstaller != nil { if project.Uninstaller != nil {
project.Uninstaller.Entrypoint = e project.Uninstaller.Entrypoint = e
} }
rootDir, err := PathInLabs(ctx, project.Name)
if err != nil {
return nil, err
}
project.rootDir = rootDir
return &project, nil return &project, nil
} }
@ -64,6 +69,7 @@ type Project struct {
Commands []*proxy `yaml:"commands,omitempty"` Commands []*proxy `yaml:"commands,omitempty"`
folder string folder string
rootDir string
} }
func (p *Project) IsZipball() bool { func (p *Project) IsZipball() bool {
@ -108,22 +114,22 @@ func (p *Project) fileExists(name string) bool {
return err == nil return err == nil
} }
func (p *Project) projectFilePath(ctx context.Context, name string) string { func (p *Project) projectFilePath(name string) string {
return filepath.Join(p.EffectiveLibDir(ctx), name) return filepath.Join(p.EffectiveLibDir(), name)
} }
func (p *Project) IsPythonProject(ctx context.Context) bool { func (p *Project) IsPythonProject() bool {
if p.fileExists(p.projectFilePath(ctx, "setup.py")) { if p.fileExists(p.projectFilePath("setup.py")) {
return true return true
} }
if p.fileExists(p.projectFilePath(ctx, "pyproject.toml")) { if p.fileExists(p.projectFilePath("pyproject.toml")) {
return true return true
} }
return false return false
} }
func (p *Project) IsDeveloperMode(ctx context.Context) bool { func (p *Project) IsDeveloperMode() bool {
return p.folder != "" && !strings.HasPrefix(p.LibDir(ctx), p.folder) return p.folder != "" && !strings.HasPrefix(p.LibDir(), p.folder)
} }
func (p *Project) HasFolder() bool { func (p *Project) HasFolder() bool {
@ -161,36 +167,32 @@ func (p *Project) Register(parent *cobra.Command) {
} }
} }
func (p *Project) rootDir(ctx context.Context) string { func (p *Project) CacheDir() string {
return PathInLabs(ctx, p.Name) return filepath.Join(p.rootDir, "cache")
} }
func (p *Project) CacheDir(ctx context.Context) string { func (p *Project) ConfigDir() string {
return filepath.Join(p.rootDir(ctx), "cache") return filepath.Join(p.rootDir, "config")
} }
func (p *Project) ConfigDir(ctx context.Context) string { func (p *Project) LibDir() string {
return filepath.Join(p.rootDir(ctx), "config") return filepath.Join(p.rootDir, "lib")
} }
func (p *Project) LibDir(ctx context.Context) string { func (p *Project) EffectiveLibDir() string {
return filepath.Join(p.rootDir(ctx), "lib") if p.IsDeveloperMode() {
}
func (p *Project) EffectiveLibDir(ctx context.Context) string {
if p.IsDeveloperMode(ctx) {
// developer is working on a local checkout, that is not inside of installed root // developer is working on a local checkout, that is not inside of installed root
return p.folder return p.folder
} }
return p.LibDir(ctx) return p.LibDir()
} }
func (p *Project) StateDir(ctx context.Context) string { func (p *Project) StateDir() string {
return filepath.Join(p.rootDir(ctx), "state") return filepath.Join(p.rootDir, "state")
} }
func (p *Project) EnsureFoldersExist(ctx context.Context) error { func (p *Project) EnsureFoldersExist() error {
dirs := []string{p.CacheDir(ctx), p.ConfigDir(ctx), p.LibDir(ctx), p.StateDir(ctx)} dirs := []string{p.CacheDir(), p.ConfigDir(), p.LibDir(), p.StateDir()}
for _, v := range dirs { for _, v := range dirs {
err := os.MkdirAll(v, ownerRWXworldRX) err := os.MkdirAll(v, ownerRWXworldRX)
if err != nil { if err != nil {
@ -209,11 +211,11 @@ func (p *Project) Uninstall(cmd *cobra.Command) error {
} }
ctx := cmd.Context() ctx := cmd.Context()
log.Infof(ctx, "Removing project: %s", p.Name) log.Infof(ctx, "Removing project: %s", p.Name)
return os.RemoveAll(p.rootDir(ctx)) return os.RemoveAll(p.rootDir)
} }
func (p *Project) virtualEnvPath(ctx context.Context) string { func (p *Project) virtualEnvPath(ctx context.Context) string {
if p.IsDeveloperMode(ctx) { if p.IsDeveloperMode() {
// When a virtual environment has been activated, the VIRTUAL_ENV environment variable // When a virtual environment has been activated, the VIRTUAL_ENV environment variable
// is set to the path of the environment. Since explicitly activating a virtual environment // is set to the path of the environment. Since explicitly activating a virtual environment
// is not required to use it, VIRTUAL_ENV cannot be relied upon to determine whether a virtual // is not required to use it, VIRTUAL_ENV cannot be relied upon to determine whether a virtual
@ -225,14 +227,14 @@ func (p *Project) virtualEnvPath(ctx context.Context) string {
logger.Debugf(ctx, "(development mode) using active virtual environment from: %s", activatedVenv) logger.Debugf(ctx, "(development mode) using active virtual environment from: %s", activatedVenv)
return activatedVenv return activatedVenv
} }
nonActivatedVenv, err := python.DetectVirtualEnvPath(p.EffectiveLibDir(ctx)) nonActivatedVenv, err := python.DetectVirtualEnvPath(p.EffectiveLibDir())
if err == nil { if err == nil {
logger.Debugf(ctx, "(development mode) using virtual environment from: %s", nonActivatedVenv) logger.Debugf(ctx, "(development mode) using virtual environment from: %s", nonActivatedVenv)
return nonActivatedVenv return nonActivatedVenv
} }
} }
// by default, we pick Virtual Environment from DATABRICKS_LABS_STATE_DIR // by default, we pick Virtual Environment from DATABRICKS_LABS_STATE_DIR
return filepath.Join(p.StateDir(ctx), "venv") return filepath.Join(p.StateDir(), "venv")
} }
func (p *Project) virtualEnvPython(ctx context.Context) string { func (p *Project) virtualEnvPython(ctx context.Context) string {
@ -247,13 +249,13 @@ func (p *Project) virtualEnvPython(ctx context.Context) string {
} }
func (p *Project) loginFile(ctx context.Context) string { func (p *Project) loginFile(ctx context.Context) string {
if p.IsDeveloperMode(ctx) { if p.IsDeveloperMode() {
// developers may not want to pollute the state in // developers may not want to pollute the state in
// ~/.databricks/labs/X/config while the version is not yet // ~/.databricks/labs/X/config while the version is not yet
// released // released
return p.projectFilePath(ctx, ".databricks-login.json") return p.projectFilePath(".databricks-login.json")
} }
return filepath.Join(p.ConfigDir(ctx), "login.json") return filepath.Join(p.ConfigDir(), "login.json")
} }
func (p *Project) loadLoginConfig(ctx context.Context) (*loginConfig, error) { func (p *Project) loadLoginConfig(ctx context.Context) (*loginConfig, error) {
@ -268,11 +270,11 @@ func (p *Project) loadLoginConfig(ctx context.Context) (*loginConfig, error) {
} }
func (p *Project) versionFile(ctx context.Context) string { func (p *Project) versionFile(ctx context.Context) string {
return filepath.Join(p.StateDir(ctx), "version.json") return filepath.Join(p.StateDir(), "version.json")
} }
func (p *Project) InstalledVersion(ctx context.Context) (*version, error) { func (p *Project) InstalledVersion(ctx context.Context) (*version, error) {
if p.IsDeveloperMode(ctx) { if p.IsDeveloperMode() {
return &version{ return &version{
Version: "*", Version: "*",
Date: time.Now(), Date: time.Now(),
@ -300,12 +302,12 @@ func (p *Project) writeVersionFile(ctx context.Context, ver string) error {
// giving users hints when they need to update their installations. // giving users hints when they need to update their installations.
func (p *Project) checkUpdates(cmd *cobra.Command) error { func (p *Project) checkUpdates(cmd *cobra.Command) error {
ctx := cmd.Context() ctx := cmd.Context()
if p.IsDeveloperMode(ctx) { if p.IsDeveloperMode() {
// skipping update check for projects in developer mode, that // skipping update check for projects in developer mode, that
// might not be installed yet // might not be installed yet
return nil return nil
} }
r := github.NewReleaseCache("databrickslabs", p.Name, p.CacheDir(ctx)) r := github.NewReleaseCache("databrickslabs", p.Name, p.CacheDir())
versions, err := r.Load(ctx) versions, err := r.Load(ctx)
if err != nil { if err != nil {
return err return err

View File

@ -60,7 +60,7 @@ func (cp *proxy) runE(cmd *cobra.Command, _ []string) error {
cmd.OutOrStdout(), cmd.OutOrStdout(),
cmd.ErrOrStderr(), cmd.ErrOrStderr(),
process.WithEnvs(envs)) process.WithEnvs(envs))
if errors.Is(err, fs.ErrNotExist) && cp.IsPythonProject(ctx) { if errors.Is(err, fs.ErrNotExist) && cp.IsPythonProject() {
msg := "cannot find Python %s. Please re-run: databricks labs install %s" msg := "cannot find Python %s. Please re-run: databricks labs install %s"
return fmt.Errorf(msg, cp.MinPython, cp.Name) return fmt.Errorf(msg, cp.MinPython, cp.Name)
} }
@ -113,9 +113,9 @@ func (cp *proxy) commandInput(cmd *cobra.Command) ([]string, error) {
} }
args := []string{} args := []string{}
ctx := cmd.Context() ctx := cmd.Context()
if cp.IsPythonProject(ctx) { if cp.IsPythonProject() {
args = append(args, cp.virtualEnvPython(ctx)) args = append(args, cp.virtualEnvPython(ctx))
libDir := cp.EffectiveLibDir(cmd.Context()) libDir := cp.EffectiveLibDir()
entrypoint := filepath.Join(libDir, cp.Main) entrypoint := filepath.Join(libDir, cp.Main)
args = append(args, entrypoint) args = append(args, entrypoint)
} }

View File

@ -37,7 +37,7 @@ func newShowCommand() *cobra.Command {
} }
name := args[0] name := args[0]
for _, v := range installed { for _, v := range installed {
isDev := name == "." && v.IsDeveloperMode(ctx) isDev := name == "." && v.IsDeveloperMode()
isMatch := name == v.Name isMatch := name == v.Name
if !(isDev || isMatch) { if !(isDev || isMatch) {
continue continue
@ -45,10 +45,10 @@ func newShowCommand() *cobra.Command {
return cmdio.Render(ctx, map[string]any{ return cmdio.Render(ctx, map[string]any{
"name": v.Name, "name": v.Name,
"description": v.Description, "description": v.Description,
"cache_dir": v.CacheDir(ctx), "cache_dir": v.CacheDir(),
"config_dir": v.ConfigDir(ctx), "config_dir": v.ConfigDir(),
"lib_dir": v.EffectiveLibDir(ctx), "lib_dir": v.EffectiveLibDir(),
"is_python": v.IsPythonProject(ctx), "is_python": v.IsPythonProject(),
}) })
} }
return nil return nil

View File

@ -76,7 +76,10 @@ func GetPath(ctx context.Context) (string, error) {
configFile = "~/.databrickscfg" configFile = "~/.databrickscfg"
} }
if strings.HasPrefix(configFile, "~") { if strings.HasPrefix(configFile, "~") {
homedir := env.UserHomeDir(ctx) homedir, err := env.UserHomeDir(ctx)
if err != nil {
return "", err
}
configFile = filepath.Join(homedir, configFile[1:]) configFile = filepath.Join(homedir, configFile[1:])
} }
return configFile, nil return configFile, nil
@ -108,7 +111,11 @@ func LoadProfiles(ctx context.Context, fn ProfileMatchFunction) (file string, pr
// Replace homedir with ~ if applicable. // Replace homedir with ~ if applicable.
// This is to make the output more readable. // This is to make the output more readable.
file = filepath.Clean(f.Path()) file = filepath.Clean(f.Path())
homedir := filepath.Clean(env.UserHomeDir(ctx)) home, err := env.UserHomeDir(ctx)
if err != nil {
return "", nil, err
}
homedir := filepath.Clean(home)
if strings.HasPrefix(file, homedir) { if strings.HasPrefix(file, homedir) {
file = "~" + file[len(homedir):] file = "~" + file[len(homedir):]
} }

11
libs/env/context.go vendored
View File

@ -2,7 +2,7 @@ package env
import ( import (
"context" "context"
"fmt" "errors"
"os" "os"
"runtime" "runtime"
"strings" "strings"
@ -76,12 +76,15 @@ func WithUserHomeDir(ctx context.Context, value string) context.Context {
return Set(ctx, homeEnvVar(), value) return Set(ctx, homeEnvVar(), value)
} }
func UserHomeDir(ctx context.Context) string { // ErrNoHomeEnv indicates the absence of $HOME env variable
var ErrNoHomeEnv = errors.New("$HOME is not set")
func UserHomeDir(ctx context.Context) (string, error) {
home := Get(ctx, homeEnvVar()) home := Get(ctx, homeEnvVar())
if home == "" { if home == "" {
panic(fmt.Errorf("$HOME is not set")) return "", ErrNoHomeEnv
} }
return home return home, nil
} }
// All returns environment variables that are defined in both os.Environ // All returns environment variables that are defined in both os.Environ

View File

@ -51,6 +51,7 @@ func TestContext(t *testing.T) {
func TestHome(t *testing.T) { func TestHome(t *testing.T) {
ctx := context.Background() ctx := context.Background()
ctx = WithUserHomeDir(ctx, "...") ctx = WithUserHomeDir(ctx, "...")
home := UserHomeDir(ctx) home, err := UserHomeDir(ctx)
assert.Equal(t, "...", home) assert.Equal(t, "...", home)
assert.NoError(t, err)
} }