2023-08-05 16:06:50 +00:00
|
|
|
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"
|
2023-08-05 16:06:50 +00:00
|
|
|
"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"`
|
2023-08-05 16:06:50 +00:00
|
|
|
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
|
2023-08-05 16:06:50 +00:00
|
|
|
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]
|
|
|
|
}
|
2023-08-05 16:06:50 +00:00
|
|
|
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{
|
2023-08-05 16:06:50 +00:00
|
|
|
Name: name,
|
|
|
|
path: path,
|
2023-08-07 12:36:32 +00:00
|
|
|
version: version,
|
2023-08-05 16:06:50 +00:00
|
|
|
checkout: checkout,
|
2023-08-07 12:36:32 +00:00
|
|
|
}, nil
|
2023-08-05 16:06:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
2023-08-05 16:06:50 +00:00
|
|
|
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)
|
2023-08-05 16:06:50 +00:00
|
|
|
if err != nil {
|
2023-08-07 12:36:32 +00:00
|
|
|
return "", err
|
2023-08-05 16:06:50 +00:00
|
|
|
}
|
2023-08-07 12:36:32 +00:00
|
|
|
return release.TagName, nil
|
|
|
|
}
|
|
|
|
return i.version, nil
|
2023-08-05 16:06:50 +00:00
|
|
|
}
|
|
|
|
|
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)
|
2023-08-05 16:06:50 +00:00
|
|
|
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)
|
2023-08-05 16:06:50 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-08-07 12:36:32 +00:00
|
|
|
err = i.loadMetadata()
|
2023-08-05 16:06:50 +00:00
|
|
|
if err != nil {
|
2023-08-07 12:36:32 +00:00
|
|
|
return fmt.Errorf("labs.yml: %w", err)
|
2023-08-05 16:06:50 +00:00
|
|
|
}
|
2023-08-07 12:36:32 +00:00
|
|
|
if i.isPython() {
|
|
|
|
err := i.installPythonTool(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-08-05 16:06:50 +00:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-08-07 12:36:32 +00:00
|
|
|
const CacheDir = ".databricks"
|
|
|
|
|
2023-08-05 16:06:50 +00:00
|
|
|
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-05 16:06:50 +00:00
|
|
|
|
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-05 16:06:50 +00:00
|
|
|
|
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-05 16:06:50 +00:00
|
|
|
|
2023-08-07 12:36:32 +00:00
|
|
|
func (i *Feature) installPythonTool(ctx context.Context) error {
|
|
|
|
pythons, err := python.DetectInterpreters(ctx)
|
2023-08-05 16:06:50 +00:00
|
|
|
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))
|
2023-08-05 16:06:50 +00:00
|
|
|
if err != nil {
|
2023-08-07 12:36:32 +00:00
|
|
|
return fmt.Errorf("create venv: %w", err)
|
2023-08-05 16:06:50 +00:00
|
|
|
}
|
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)
|
2023-08-05 16:06:50 +00:00
|
|
|
if err != nil {
|
2023-08-07 12:36:32 +00:00
|
|
|
return fmt.Errorf("%s: %w", i.Hooks.Install, err)
|
2023-08-05 16:06:50 +00:00
|
|
|
}
|
|
|
|
}
|
2023-08-07 12:36:32 +00:00
|
|
|
return nil
|
2023-08-05 16:06:50 +00:00
|
|
|
}
|