mirror of https://github.com/databricks/cli.git
Added basic project configuration
This commit is contained in:
parent
4e8955085e
commit
1029244390
|
@ -0,0 +1,6 @@
|
||||||
|
Project Configuration
|
||||||
|
---
|
||||||
|
|
||||||
|
_Good implicit defaults is better than explicit complex configuration._
|
||||||
|
|
||||||
|
Regardless of current working directory, `bricks` finds project root with `databricks.yml` file up the directory tree. Technically, there might be couple of different Databricks Projects in the same Git repository, but the recommended scenario is to have just one `databricks.yml` in the root of Git repo.
|
|
@ -0,0 +1,159 @@
|
||||||
|
package project
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/databrickslabs/terraform-provider-databricks/clusters"
|
||||||
|
"github.com/ghodss/yaml"
|
||||||
|
gitUrls "github.com/whilp/git-urls"
|
||||||
|
"gopkg.in/ini.v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Isolation string
|
||||||
|
|
||||||
|
const (
|
||||||
|
None Isolation = ""
|
||||||
|
Soft Isolation = "soft"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConfigFile is the name of project configuration file
|
||||||
|
const ConfigFile = "databricks.yml"
|
||||||
|
|
||||||
|
type Assertions struct {
|
||||||
|
Groups []string `json:"groups,omitempty"`
|
||||||
|
Secrets []string `json:"secrets,omitempty"`
|
||||||
|
ServicePrincipals []string `json:"service_principals,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Project struct {
|
||||||
|
Name string `json:"name"` // or do default from folder name?..
|
||||||
|
Profile string `json:"profile,omitempty"`
|
||||||
|
Isolation Isolation `json:"isolation,omitempty"`
|
||||||
|
|
||||||
|
// TODO: turn to pointer for the easy YAML marshalling
|
||||||
|
DevCluster *clusters.Cluster `json:"dev_cluster,omitempty"`
|
||||||
|
|
||||||
|
// Assertions defines a list of configurations expected to be applied
|
||||||
|
// to the workspace by a higher-privileged user (or service principal)
|
||||||
|
// in order for the deploy command to work, as individual project teams
|
||||||
|
// in almost all the cases don’t have admin privileges on Databricks
|
||||||
|
// workspaces.
|
||||||
|
//
|
||||||
|
// This configuration simplifies the flexibility of individual project
|
||||||
|
// teams, make jobs deployment easier and portable across environments.
|
||||||
|
// This configuration block would contain the following entities to be
|
||||||
|
// created by administrator users or admin-level automation, like Terraform
|
||||||
|
// and/or SCIM provisioning.
|
||||||
|
Assertions *Assertions `json:"assertions,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Project) IsDevClusterDefined() bool {
|
||||||
|
return reflect.ValueOf(p.DevCluster).IsZero()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDevClusterJustReference denotes reference-only clusters.
|
||||||
|
// This conflicts with Soft isolation. Happens for cost-restricted projects,
|
||||||
|
// where there's only a single Shared Autoscaling cluster per workspace and
|
||||||
|
// general users have no ability to create other iteractive clusters.
|
||||||
|
func (p *Project) IsDevClusterJustReference() bool {
|
||||||
|
if p.DevCluster.ClusterName == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return reflect.DeepEqual(p.DevCluster, clusters.Cluster{
|
||||||
|
ClusterName: p.DevCluster.ClusterName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDatabricksProject returns true for folders with `databricks.yml`
|
||||||
|
// in the parent tree
|
||||||
|
func IsDatabricksProject() bool {
|
||||||
|
_, err := findProjectRoot()
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadProjectConf() (prj Project, err error) {
|
||||||
|
root, err := findProjectRoot()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
config, err := os.Open(fmt.Sprintf("%s/%s", root, ConfigFile))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
raw, err := ioutil.ReadAll(config)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = yaml.Unmarshal(raw, &prj)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return validateAndApplyProjectDefaults(prj)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateAndApplyProjectDefaults(prj Project) (Project, error) {
|
||||||
|
// defaultCluster := clusters.Cluster{
|
||||||
|
// NodeTypeID: "smallest",
|
||||||
|
// SparkVersion: "latest",
|
||||||
|
// AutoterminationMinutes: 30,
|
||||||
|
// }
|
||||||
|
return prj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findProjectRoot() (string, error) {
|
||||||
|
return findDirWithLeaf(ConfigFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// finds the original git repository the project is cloned from, so that
|
||||||
|
// we could automatically verify if this project is checked out in repos
|
||||||
|
// home folder of the user according to recommended best practices. Can
|
||||||
|
// also be used to determine a good enough default project name.
|
||||||
|
func getGitOrigin() (*url.URL, error) {
|
||||||
|
root, err := findDirWithLeaf(".git")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
file := fmt.Sprintf("%s/.git/config", root)
|
||||||
|
gitConfig, err := ini.Load(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
section := gitConfig.Section(`remote "origin"`)
|
||||||
|
if section == nil {
|
||||||
|
return nil, fmt.Errorf("remote `origin` is not defined in %s", file)
|
||||||
|
}
|
||||||
|
url := section.Key("url")
|
||||||
|
if url == nil {
|
||||||
|
return nil, fmt.Errorf("git origin url is not defined")
|
||||||
|
}
|
||||||
|
return gitUrls.Parse(url.Value())
|
||||||
|
}
|
||||||
|
|
||||||
|
func findDirWithLeaf(leaf string) (string, error) {
|
||||||
|
dir, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("cannot find $PWD: %s", err)
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
_, err = os.Stat(fmt.Sprintf("%s/%s", dir, leaf))
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
// TODO: test on windows
|
||||||
|
next := path.Dir(dir)
|
||||||
|
if dir == next { // or stop at $HOME?..
|
||||||
|
return "", fmt.Errorf("cannot find %s anywhere", leaf)
|
||||||
|
}
|
||||||
|
dir = next
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return dir, nil
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package project
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFindProjectRoot(t *testing.T) {
|
||||||
|
wd, _ := os.Getwd()
|
||||||
|
defer os.Chdir(wd)
|
||||||
|
err := os.Chdir("internal/test/a/b/c")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
root, err := findProjectRoot()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, fmt.Sprintf("%s/internal/test", wd), root)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindProjectRootInRoot(t *testing.T) {
|
||||||
|
wd, _ := os.Getwd()
|
||||||
|
defer os.Chdir(wd)
|
||||||
|
err := os.Chdir("/tmp")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
_, err = findProjectRoot()
|
||||||
|
assert.EqualError(t, err, "cannot find databricks.yml anywhere")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetGitOrigin(t *testing.T) {
|
||||||
|
origin, err := getGitOrigin()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "bricks.git", path.Base(origin.Path))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadProjectConf(t *testing.T) {
|
||||||
|
wd, _ := os.Getwd()
|
||||||
|
defer os.Chdir(wd)
|
||||||
|
os.Chdir("internal/test/a/b/c")
|
||||||
|
|
||||||
|
prj, err := loadProjectConf()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "dev", prj.Name)
|
||||||
|
assert.True(t, prj.IsDevClusterJustReference())
|
||||||
|
}
|
|
@ -1,32 +0,0 @@
|
||||||
package project
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/databrickslabs/terraform-provider-databricks/common"
|
|
||||||
"github.com/databrickslabs/terraform-provider-databricks/commands"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
type appContext int
|
|
||||||
|
|
||||||
const (
|
|
||||||
// DatabricksClient holds DatabricksClient
|
|
||||||
DatabricksClient appContext = 1
|
|
||||||
)
|
|
||||||
|
|
||||||
func Authenticate(ctx context.Context) context.Context {
|
|
||||||
client := common.CommonEnvironmentClient()
|
|
||||||
client.WithCommandExecutor(func(ctx context.Context, _ *common.DatabricksClient) common.CommandExecutor {
|
|
||||||
return commands.NewCommandsAPI(ctx, client)
|
|
||||||
})
|
|
||||||
return context.WithValue(ctx, DatabricksClient, client)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ClientFromContext(ctx context.Context) *common.DatabricksClient {
|
|
||||||
client, ok := ctx.Value(DatabricksClient).(*common.DatabricksClient)
|
|
||||||
if !ok {
|
|
||||||
panic("authentication is not configured")
|
|
||||||
}
|
|
||||||
return client
|
|
||||||
}
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
name: dev
|
||||||
|
profile: demo
|
||||||
|
dev_cluster:
|
||||||
|
cluster_name: Shared Autoscaling
|
|
@ -3,36 +3,93 @@ package project
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/databrickslabs/terraform-provider-databricks/clusters"
|
"github.com/databrickslabs/terraform-provider-databricks/clusters"
|
||||||
|
"github.com/databrickslabs/terraform-provider-databricks/commands"
|
||||||
"github.com/databrickslabs/terraform-provider-databricks/common"
|
"github.com/databrickslabs/terraform-provider-databricks/common"
|
||||||
"github.com/databrickslabs/terraform-provider-databricks/scim"
|
"github.com/databrickslabs/terraform-provider-databricks/scim"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CurrentUser(ctx context.Context) (scim.User, error) {
|
// Current CLI application state
|
||||||
// TODO: memoize
|
var Current inner
|
||||||
return scim.NewUsersAPI(ctx, ClientFromContext(ctx)).Me()
|
|
||||||
|
type inner struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
once sync.Once
|
||||||
|
|
||||||
|
project *Project
|
||||||
|
client *common.DatabricksClient
|
||||||
|
me *scim.User
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProjectName(ctx context.Context) string {
|
func (i *inner) init() {
|
||||||
return "dev" // TODO: parse from config file
|
i.mu.Lock()
|
||||||
}
|
defer i.mu.Unlock()
|
||||||
|
i.once.Do(func() {
|
||||||
func DevelopmentCluster(ctx context.Context) (cluster clusters.ClusterInfo, err error) {
|
client := common.CommonEnvironmentClient()
|
||||||
api := clusters.NewClustersAPI(ctx, ClientFromContext(ctx)) // TODO: rewrite with normal SDK
|
client.WithCommandExecutor(func(
|
||||||
me, err := CurrentUser(ctx)
|
ctx context.Context, c *common.DatabricksClient) common.CommandExecutor {
|
||||||
|
return commands.NewCommandsAPI(ctx, c)
|
||||||
|
})
|
||||||
|
i.client = client
|
||||||
|
prj, err := loadProjectConf()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
client.Profile = prj.Profile
|
||||||
|
i.project = &prj
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *inner) Client() *common.DatabricksClient {
|
||||||
|
i.init()
|
||||||
|
return i.client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *inner) Project() *Project {
|
||||||
|
i.init()
|
||||||
|
return i.project
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *inner) Me() *scim.User {
|
||||||
|
i.mu.Lock()
|
||||||
|
defer i.mu.Unlock()
|
||||||
|
if i.me != nil {
|
||||||
|
return i.me
|
||||||
|
}
|
||||||
|
me, err := scim.NewUsersAPI(context.Background(), i.Client()).Me()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
i.me = &me
|
||||||
|
return &me
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *inner) DevelopmentCluster(ctx context.Context) (cluster clusters.ClusterInfo, err error) {
|
||||||
|
api := clusters.NewClustersAPI(ctx, i.Client()) // TODO: rewrite with normal SDK
|
||||||
|
if i.project.DevCluster == nil {
|
||||||
|
i.project.DevCluster = &clusters.Cluster{}
|
||||||
|
}
|
||||||
|
dc := i.project.DevCluster
|
||||||
|
if i.project.Isolation == Soft {
|
||||||
|
if i.project.IsDevClusterJustReference() {
|
||||||
|
err = fmt.Errorf("projects with soft isolation cannot have named clusters")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
projectName := ProjectName(ctx)
|
me := i.Me()
|
||||||
devClusterName := fmt.Sprintf("dev/%s/%s", projectName, me.UserName)
|
dc.ClusterName = fmt.Sprintf("dev/%s/%s", i.project.Name, me.UserName)
|
||||||
return api.GetOrCreateRunningCluster(devClusterName)
|
}
|
||||||
|
if dc.ClusterName == "" {
|
||||||
|
err = fmt.Errorf("please either pick `isolation: soft` or specify a shared cluster name")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return api.GetOrCreateRunningCluster(dc.ClusterName, *dc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runCommandOnDev(ctx context.Context, language, command string) common.CommandResults {
|
func runCommandOnDev(ctx context.Context, language, command string) common.CommandResults {
|
||||||
client := ClientFromContext(ctx)
|
cluster, err := Current.DevelopmentCluster(ctx)
|
||||||
exec := client.CommandExecutor(ctx)
|
exec := Current.Client().CommandExecutor(ctx)
|
||||||
cluster, err := DevelopmentCluster(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return common.CommandResults{
|
return common.CommandResults{
|
||||||
ResultType: "error",
|
ResultType: "error",
|
||||||
|
|
Loading…
Reference in New Issue