databricks-cli/cmd/labs/feature/feature.go

199 lines
5.0 KiB
Go
Raw Normal View History

package feature
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/databricks/cli/libs/git"
"github.com/databricks/cli/libs/log"
2023-08-07 12:36:32 +00:00
"github.com/databricks/cli/libs/process"
"github.com/databricks/cli/libs/python"
"gopkg.in/yaml.v2"
)
type Feature struct {
Name string `json:"name"`
Context string `json:"context,omitempty"` // auth context
Description string `json:"description"`
Hooks struct {
Install string `json:"install,omitempty"`
Uninstall string `json:"uninstall,omitempty"`
2023-08-07 12:36:32 +00:00
} `json:"hooks,omitempty"`
Entrypoint string `json:"entrypoint"`
Commands []struct {
Name string `json:"name"`
Description string `json:"description"`
Flags []struct {
Name string `json:"name"`
Description string `json:"description"`
} `json:"flags,omitempty"`
} `json:"commands,omitempty"`
2023-08-07 12:36:32 +00:00
version string
path string
checkout *git.Repository
}
func NewFeature(name string) (*Feature, error) {
home, err := os.UserHomeDir()
if err != nil {
return nil, err
}
2023-08-07 12:36:32 +00:00
version := "latest"
split := strings.Split(name, "@")
if len(split) > 2 {
return nil, fmt.Errorf("invalid coordinates: %s", name)
}
if len(split) == 2 {
name = split[0]
version = split[1]
}
path := filepath.Join(home, ".databricks", "labs", name)
checkout, err := git.NewRepository(path)
if err != nil && !os.IsNotExist(err) {
return nil, err
}
2023-08-07 12:36:32 +00:00
return &Feature{
Name: name,
path: path,
2023-08-07 12:36:32 +00:00
version: version,
checkout: checkout,
2023-08-07 12:36:32 +00:00
}, nil
}
type release struct {
TagName string `json:"tag_name"`
Draft bool `json:"draft"`
Prerelease bool `json:"prerelease"`
PublishedAt time.Time `json:"published_at"`
}
2023-08-07 12:36:32 +00:00
func (i *Feature) loadMetadata() error {
raw, err := os.ReadFile(filepath.Join(i.path, "labs.yml"))
if err != nil {
return fmt.Errorf("read labs.yml: %w", err)
}
err = yaml.Unmarshal(raw, i)
if err != nil {
return fmt.Errorf("parse labs.yml: %w", err)
}
return nil
}
func (i *Feature) fetchLatestVersion(ctx context.Context) (*release, error) {
var tags []release
url := fmt.Sprintf("https://api.github.com/repos/databrickslabs/%s/releases", i.Name)
err := httpCall(ctx, url, &tags)
if err != nil {
return nil, err
}
return &tags[0], nil
}
2023-08-07 12:36:32 +00:00
func (i *Feature) requestedVersion(ctx context.Context) (string, error) {
if i.version == "latest" {
release, err := i.fetchLatestVersion(ctx)
if err != nil {
2023-08-07 12:36:32 +00:00
return "", err
}
2023-08-07 12:36:32 +00:00
return release.TagName, nil
}
return i.version, nil
}
2023-08-07 12:36:32 +00:00
func (i *Feature) Install(ctx context.Context) error {
if i.hasFile(".git/HEAD") {
curr, err := process.Background(ctx, []string{
"git", "tag", "--points-at", "HEAD",
}, process.WithDir(i.path))
if err != nil {
return err
}
return fmt.Errorf("%s (%s) is already installed", i.Name, curr)
}
url := fmt.Sprintf("https://github.com/databrickslabs/%s", i.Name)
version, err := i.requestedVersion(ctx)
if err != nil {
return err
}
2023-08-07 12:36:32 +00:00
log.Infof(ctx, "Installing %s (%s) into %s", url, version, i.path)
err = git.Clone(ctx, url, version, i.path)
if err != nil {
return err
}
2023-08-07 12:36:32 +00:00
err = i.loadMetadata()
if err != nil {
2023-08-07 12:36:32 +00:00
return fmt.Errorf("labs.yml: %w", err)
}
2023-08-07 12:36:32 +00:00
if i.isPython() {
err := i.installPythonTool(ctx)
if err != nil {
return err
}
}
return nil
}
2023-08-07 12:36:32 +00:00
const CacheDir = ".databricks"
func (i *Feature) Run(ctx context.Context, raw []byte) error {
2023-08-07 12:36:32 +00:00
// raw is a JSON-encoded payload that holds things like command name and flags
return i.forwardPython(ctx, filepath.Join(i.path, i.Entrypoint), string(raw))
}
2023-08-07 12:36:32 +00:00
func (i *Feature) hasFile(name string) bool {
_, err := os.Stat(filepath.Join(i.path, name))
return err == nil
}
2023-08-07 12:36:32 +00:00
func (i *Feature) isPython() bool {
return i.hasFile("setup.py") || i.hasFile("pyproject.toml")
}
func (i *Feature) venvBinDir() string {
return filepath.Join(i.path, CacheDir, "bin")
}
func (i *Feature) forwardPython(ctx context.Context, pythonArgs ...string) error {
args := []string{filepath.Join(i.venvBinDir(), "python")}
args = append(args, pythonArgs...)
return process.Forwarded(ctx, args,
process.WithDir(i.path), // we may need to skip it for install step
process.WithEnv("PYTHONPATH", i.path))
}
2023-08-07 12:36:32 +00:00
func (i *Feature) installPythonTool(ctx context.Context) error {
pythons, err := python.DetectInterpreters(ctx)
if err != nil {
return err
}
2023-08-07 12:36:32 +00:00
interpreter := pythons.Latest()
log.Debugf(ctx, "Creating Python %s virtual environment in %s", interpreter.Version, i.path)
_, err = process.Background(ctx, []string{
interpreter.Binary, "-m", "venv", CacheDir,
}, process.WithDir(i.path))
if err != nil {
2023-08-07 12:36:32 +00:00
return fmt.Errorf("create venv: %w", err)
}
2023-08-07 12:36:32 +00:00
log.Debugf(ctx, "Installing dependencies via PIP")
venvPip := filepath.Join(i.venvBinDir(), "pip")
_, err = process.Background(ctx, []string{
venvPip, "install", ".",
}, process.WithDir(i.path))
if err != nil {
return fmt.Errorf("pip install: %w", err)
}
if i.Hooks.Install != "" {
installer := filepath.Join(i.path, i.Hooks.Install)
err = i.forwardPython(ctx, installer)
if err != nil {
2023-08-07 12:36:32 +00:00
return fmt.Errorf("%s: %w", i.Hooks.Install, err)
}
}
2023-08-07 12:36:32 +00:00
return nil
}