Remove package project (#321)

## Changes
<!-- Summary of your changes that are easy to understand -->

This PR removes the project package and it's dependents in the bricks
repo

## Tests
<!-- How is this tested? -->
This commit is contained in:
shreyas-goenka 2023-04-11 16:59:27 +02:00 committed by GitHub
parent 946906221d
commit 375eb1c502
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 12 additions and 1501 deletions

View File

@ -7,9 +7,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"github.com/databricks/bricks/cmd/prompt"
"github.com/databricks/bricks/cmd/root" "github.com/databricks/bricks/cmd/root"
"github.com/databricks/bricks/project"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gopkg.in/ini.v1" "gopkg.in/ini.v1"
) )
@ -20,7 +18,7 @@ type Configs struct {
Profile string `ini:"-"` Profile string `ini:"-"`
} }
var noInteractive, tokenMode bool var tokenMode bool
func (cfg *Configs) loadNonInteractive(cmd *cobra.Command) error { func (cfg *Configs) loadNonInteractive(cmd *cobra.Command) error {
host, err := cmd.Flags().GetString("host") host, err := cmd.Flags().GetString("host")
@ -43,53 +41,6 @@ func (cfg *Configs) loadNonInteractive(cmd *cobra.Command) error {
return nil return nil
} }
func (cfg *Configs) loadInteractive(cmd *cobra.Command) error {
res := prompt.Results{}
questions := prompt.Questions{}
host, err := cmd.Flags().GetString("host")
if err != nil || host == "" {
questions = append(questions, prompt.Text{
Key: "host",
Label: "Databricks Host",
Default: func(res prompt.Results) string {
return cfg.Host
},
Callback: func(ans prompt.Answer, config *project.Config, res prompt.Results) {
cfg.Host = ans.Value
},
})
} else {
cfg.Host = host
}
if tokenMode {
questions = append(questions, prompt.Text{
Key: "token",
Label: "Databricks Token",
Default: func(res prompt.Results) string {
return cfg.Token
},
Callback: func(ans prompt.Answer, config *project.Config, res prompt.Results) {
cfg.Token = ans.Value
},
})
}
err = questions.Ask(res)
if err != nil {
return err
}
for _, answer := range res {
if answer.Callback != nil {
answer.Callback(answer, nil, res)
}
}
return nil
}
var configureCmd = &cobra.Command{ var configureCmd = &cobra.Command{
Use: "configure", Use: "configure",
Short: "Configure authentication", Short: "Configure authentication",
@ -135,11 +86,7 @@ var configureCmd = &cobra.Command{
return fmt.Errorf("unmarshal loaded config: %w", err) return fmt.Errorf("unmarshal loaded config: %w", err)
} }
if noInteractive { err = cfg.loadNonInteractive(cmd)
err = cfg.loadNonInteractive(cmd)
} else {
err = cfg.loadInteractive(cmd)
}
if err != nil { if err != nil {
return fmt.Errorf("reading configs: %w", err) return fmt.Errorf("reading configs: %w", err)
} }
@ -171,7 +118,6 @@ var configureCmd = &cobra.Command{
func init() { func init() {
root.RootCmd.AddCommand(configureCmd) root.RootCmd.AddCommand(configureCmd)
configureCmd.Flags().BoolVarP(&tokenMode, "token", "t", false, "Configure using Databricks Personal Access Token") configureCmd.Flags().BoolVarP(&tokenMode, "token", "t", false, "Configure using Databricks Personal Access Token")
configureCmd.Flags().BoolVar(&noInteractive, "no-interactive", false, "Don't show interactive prompts for inputs. Read directly from stdin.")
configureCmd.Flags().String("host", "", "Host to connect to.") configureCmd.Flags().String("host", "", "Host to connect to.")
configureCmd.Flags().String("profile", "DEFAULT", "CLI connection profile to use.") configureCmd.Flags().String("profile", "DEFAULT", "CLI connection profile to use.")
} }

View File

@ -52,7 +52,7 @@ func TestDefaultConfigureNoInteractive(t *testing.T) {
}) })
os.Stdin = inp os.Stdin = inp
root.RootCmd.SetArgs([]string{"configure", "--token", "--no-interactive", "--host", "host"}) root.RootCmd.SetArgs([]string{"configure", "--token", "--host", "host"})
err := root.RootCmd.ExecuteContext(ctx) err := root.RootCmd.ExecuteContext(ctx)
assert.NoError(t, err) assert.NoError(t, err)
@ -84,7 +84,7 @@ func TestConfigFileFromEnvNoInteractive(t *testing.T) {
t.Cleanup(func() { os.Stdin = oldStdin }) t.Cleanup(func() { os.Stdin = oldStdin })
os.Stdin = inp os.Stdin = inp
root.RootCmd.SetArgs([]string{"configure", "--token", "--no-interactive", "--host", "host"}) root.RootCmd.SetArgs([]string{"configure", "--token", "--host", "host"})
err := root.RootCmd.ExecuteContext(ctx) err := root.RootCmd.ExecuteContext(ctx)
assert.NoError(t, err) assert.NoError(t, err)
@ -112,7 +112,7 @@ func TestCustomProfileConfigureNoInteractive(t *testing.T) {
t.Cleanup(func() { os.Stdin = oldStdin }) t.Cleanup(func() { os.Stdin = oldStdin })
os.Stdin = inp os.Stdin = inp
root.RootCmd.SetArgs([]string{"configure", "--token", "--no-interactive", "--host", "host", "--profile", "CUSTOM"}) root.RootCmd.SetArgs([]string{"configure", "--token", "--host", "host", "--profile", "CUSTOM"})
err := root.RootCmd.ExecuteContext(ctx) err := root.RootCmd.ExecuteContext(ctx)
assert.NoError(t, err) assert.NoError(t, err)

View File

@ -14,14 +14,4 @@ var fsCmd = &cobra.Command{
func init() { func init() {
root.RootCmd.AddCommand(fsCmd) root.RootCmd.AddCommand(fsCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// fsCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// fsCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
} }

View File

@ -3,7 +3,6 @@ package fs
import ( import (
"fmt" "fmt"
"github.com/databricks/bricks/project"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -12,25 +11,12 @@ var lsCmd = &cobra.Command{
Use: "ls <dir-name>", Use: "ls <dir-name>",
Short: "Lists files", Short: "Lists files",
Long: `Lists files`, Long: `Lists files`,
Args: cobra.ExactArgs(1),
PreRunE: project.Configure, RunE: func(cmd *cobra.Command, args []string) error {
Run: func(cmd *cobra.Command, args []string) { return fmt.Errorf("TODO")
wsc := project.Get(cmd.Context()).WorkspacesClient()
listStatusResponse, err := wsc.Dbfs.ListByPath(cmd.Context(), args[0])
if err != nil {
panic(err)
}
files := listStatusResponse.Files
// TODO: output formatting: JSON, CSV, tables and default
for _, v := range files {
fmt.Printf("[-] %s (%d, %v)\n", v.Path, v.FileSize, v.IsDir)
}
}, },
} }
func init() { func init() {
// TODO: pietern: conditionally register commands
// fabianj: don't do it
fsCmd.AddCommand(lsCmd) fsCmd.AddCommand(lsCmd)
} }

View File

@ -1,128 +0,0 @@
package init
import (
"embed"
"fmt"
"os"
"path"
"github.com/databricks/bricks/cmd/prompt"
"github.com/databricks/bricks/cmd/root"
"github.com/databricks/bricks/project"
"github.com/ghodss/yaml"
"github.com/spf13/cobra"
)
//go:embed templates
var templates embed.FS
// initCmd represents the init command
var initCmd = &cobra.Command{
Use: "init",
Short: "Project starter templates",
Long: `Generate project templates`,
RunE: func(cmd *cobra.Command, args []string) error {
if project.IsDatabricksProject() {
return fmt.Errorf("this path is already a Databricks project")
}
profileChoice, err := getConnectionProfile()
if err != nil {
return err
}
wd, _ := os.Getwd()
q := prompt.Questions{
prompt.Text{
Key: "name",
Label: "Project name",
Default: func(res prompt.Results) string {
return path.Base(wd)
},
Callback: func(ans prompt.Answer, config *project.Config, res prompt.Results) {
config.Name = ans.Value
},
},
*profileChoice,
prompt.Choice{Key: "language", Label: "Project language", Answers: prompt.Answers{
{
Value: "Python",
Details: "Machine learning and data engineering focused projects",
Callback: nil,
},
{
Value: "Scala",
Details: "Data engineering focused projects with strong typing",
Callback: nil,
},
}},
prompt.Choice{Key: "isolation", Label: "Deployment isolation", Answers: prompt.Answers{
{
Value: "None",
Details: "Use shared Databricks workspace resources for all project team members",
Callback: nil,
},
{
Value: "Soft",
Details: "Prepend prefixes to each team member's deployment",
Callback: func(
ans prompt.Answer, config *project.Config, res prompt.Results) {
config.Isolation = project.Soft
},
},
}},
// DBR selection
// Choice{"cloud", "Cloud", Answers{
// {"AWS", "Amazon Web Services", nil},
// {"Azure", "Microsoft Azure Cloud", nil},
// {"GCP", "Google Cloud Platform", nil},
// }},
// Choice{"ci", "Continuous Integration", Answers{
// {"None", "Do not create continuous integration configuration", nil},
// {"GitHub Actions", "Create .github/workflows/push.yml configuration", nil},
// {"Azure DevOps", "Create basic build and test pipelines", nil},
// }},
// Choice{"ide", "Integrated Development Environment", Answers{
// {"None", "Do not create templates for IDE", nil},
// {"VSCode", "Create .devcontainer and other useful things", nil},
// {"PyCharm", "Create project conf and other things", nil},
// }},
}
res := prompt.Results{}
err = q.Ask(res)
if err != nil {
return err
}
var config project.Config
for _, ans := range res {
if ans.Callback == nil {
continue
}
ans.Callback(ans, &config, res)
}
raw, err := yaml.Marshal(config)
if err != nil {
return err
}
newConfig, err := os.Create(fmt.Sprintf("%s/%s", wd, project.ConfigFile))
if err != nil {
return err
}
_, err = newConfig.Write(raw)
if err != nil {
return err
}
d, err := templates.ReadDir(".")
if err != nil {
return err
}
for _, v := range d {
cmd.Printf("template found: %v", v.Name())
}
cmd.Print("Config initialized!")
return err
},
}
func init() {
root.RootCmd.AddCommand(initCmd)
}

View File

@ -1,57 +0,0 @@
package init
import (
"fmt"
"github.com/databricks/bricks/cmd/prompt"
"github.com/databricks/bricks/project"
"github.com/mitchellh/go-homedir"
"gopkg.in/ini.v1"
)
func loadCliProfiles() (profiles []prompt.Answer, err error) {
file, err := homedir.Expand("~/.databrickscfg")
if err != nil {
return
}
gitConfig, err := ini.Load(file)
if err != nil {
return
}
for _, v := range gitConfig.Sections() {
host, err := v.GetKey("host")
if err != nil {
// invalid profile
continue
}
// TODO: verify these tokens to work, becaus they may be expired
profiles = append(profiles, prompt.Answer{
Value: v.Name(),
Details: fmt.Sprintf(`Connecting to "%s" workspace`, host),
Callback: func(ans prompt.Answer, config *project.Config, _ prompt.Results) {
if config.Environments == nil {
config.Environments = make(map[string]project.Environment)
}
config.Environments[project.DefaultEnvironment] = project.Environment{
Workspace: project.Workspace{
Profile: ans.Value,
},
}
},
})
}
return
}
func getConnectionProfile() (*prompt.Choice, error) {
profiles, err := loadCliProfiles()
if err != nil {
return nil, err
}
// TODO: propmt for password and create ~/.databrickscfg
return &prompt.Choice{
Key: "profile",
Label: "Databricks CLI profile",
Answers: profiles,
}, err
}

View File

@ -1,4 +0,0 @@
# Databricks notebook source
# this was automatically generated
display(spark.tables())

View File

@ -1,89 +0,0 @@
package prompt
import (
"fmt"
"io"
"github.com/databricks/bricks/project"
"github.com/manifoldco/promptui"
)
type Results map[string]Answer
type Question interface {
Ask(res Results) (key string, ans Answer, err error)
}
type Questions []Question
func (qq Questions) Ask(res Results) error {
for _, v := range qq {
key, ans, err := v.Ask(res)
if err != nil {
return err
}
res[key] = ans
}
return nil
}
type Text struct {
Key string
Label string
Default func(res Results) string
Callback AnswerCallback
Stdin io.ReadCloser
}
func (t Text) Ask(res Results) (string, Answer, error) {
def := ""
if t.Default != nil {
def = t.Default(res)
}
v, err := (&promptui.Prompt{
Label: t.Label,
Default: def,
Stdin: t.Stdin,
}).Run()
return t.Key, Answer{
Value: v,
Callback: t.Callback,
}, err
}
type Choice struct {
Key string
Label string
Answers []Answer
Stdin io.ReadCloser
}
func (q Choice) Ask(res Results) (string, Answer, error) {
// TODO: validate and re-ask
prompt := promptui.Select{
Label: q.Label,
Items: q.Answers,
Templates: &promptui.SelectTemplates{
Label: `{{ .Value }}`,
Details: `{{ .Details | green }}`,
Selected: fmt.Sprintf(`{{ "%s" | faint }}: {{ .Value | bold }}`, q.Label),
},
Stdin: q.Stdin,
}
i, _, err := prompt.Run()
return q.Key, q.Answers[i], err
}
type Answers []Answer
type AnswerCallback func(ans Answer, config *project.Config, res Results)
type Answer struct {
Value string
Details string
Callback AnswerCallback
}
func (a Answer) String() string {
return a.Value
}

3
go.mod
View File

@ -6,8 +6,6 @@ require (
github.com/atotto/clipboard v0.1.4 github.com/atotto/clipboard v0.1.4
github.com/databricks/databricks-sdk-go v0.7.0 github.com/databricks/databricks-sdk-go v0.7.0
github.com/ghodss/yaml v1.0.0 // MIT + NOTICE github.com/ghodss/yaml v1.0.0 // MIT + NOTICE
github.com/manifoldco/promptui v0.9.0 // BSD-3-Clause license
github.com/mitchellh/go-homedir v1.1.0 // MIT
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // BSD-2-Clause github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // BSD-2-Clause
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // MIT github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // MIT
github.com/spf13/cobra v1.7.0 // Apache 2.0 github.com/spf13/cobra v1.7.0 // Apache 2.0
@ -41,7 +39,6 @@ require (
require ( require (
cloud.google.com/go/compute v1.19.0 // indirect cloud.google.com/go/compute v1.19.0 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/golang/protobuf v1.5.3 // indirect github.com/golang/protobuf v1.5.3 // indirect

11
go.sum
View File

@ -12,12 +12,6 @@ github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkE
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.1.0 h1:bZgT/A+cikZnKIwn7xL2OBj012Bmvho/o6RpRvv3GKY= github.com/cloudflare/circl v1.1.0 h1:bZgT/A+cikZnKIwn7xL2OBj012Bmvho/o6RpRvv3GKY=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
@ -89,15 +83,11 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
@ -159,7 +149,6 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@ -6,9 +6,7 @@ import (
_ "github.com/databricks/bricks/cmd/bundle" _ "github.com/databricks/bricks/cmd/bundle"
_ "github.com/databricks/bricks/cmd/bundle/debug" _ "github.com/databricks/bricks/cmd/bundle/debug"
_ "github.com/databricks/bricks/cmd/bundle/debug/deploy" _ "github.com/databricks/bricks/cmd/bundle/debug/deploy"
_ "github.com/databricks/bricks/cmd/configure"
_ "github.com/databricks/bricks/cmd/fs" _ "github.com/databricks/bricks/cmd/fs"
_ "github.com/databricks/bricks/cmd/init"
"github.com/databricks/bricks/cmd/root" "github.com/databricks/bricks/cmd/root"
_ "github.com/databricks/bricks/cmd/sync" _ "github.com/databricks/bricks/cmd/sync"
_ "github.com/databricks/bricks/cmd/version" _ "github.com/databricks/bricks/cmd/version"

View File

@ -1,6 +0,0 @@
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.

View File

@ -1,137 +0,0 @@
package project
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"github.com/databricks/bricks/folders"
"github.com/databricks/databricks-sdk-go/service/clusters"
"github.com/ghodss/yaml"
)
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 Config struct {
Name string `json:"name"` // or do default from folder name?..
Isolation Isolation `json:"isolation,omitempty"`
// development-time vs deployment-time resources
DevCluster *clusters.ClusterInfo `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 dont 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"`
// Environments contain this project's defined environments.
// They can be used to differentiate settings and resources between
// development, staging, production, etc.
// If not specified, the code below initializes this field with a
// single default-initialized environment called "development".
Environments map[string]Environment `json:"environments"`
}
func (c Config) IsDevClusterDefined() bool {
return reflect.ValueOf(c.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 (c *Config) IsDevClusterJustReference() bool {
if c.DevCluster.ClusterName == "" {
return false
}
return reflect.DeepEqual(c.DevCluster, &clusters.ClusterInfo{
ClusterName: c.DevCluster.ClusterName,
})
}
// IsDatabricksProject returns true for folders with `databricks.yml`
// in the parent tree
func IsDatabricksProject() bool {
_, err := findProjectRoot()
return err == nil
}
func loadProjectConf(root string) (c Config, err error) {
configFilePath := filepath.Join(root, ConfigFile)
if _, err := os.Stat(configFilePath); errors.Is(err, os.ErrNotExist) {
baseDir := filepath.Base(root)
// If bricks config file is missing we assume the project root dir name
// as the name of the project
return validateAndApplyProjectDefaults(Config{Name: baseDir})
}
config, err := os.Open(configFilePath)
if err != nil {
return
}
defer config.Close()
raw, err := io.ReadAll(config)
if err != nil {
return
}
err = yaml.Unmarshal(raw, &c)
if err != nil {
return
}
return validateAndApplyProjectDefaults(c)
}
func validateAndApplyProjectDefaults(c Config) (Config, error) {
// If no environments are specified, define default environment under default name.
if c.Environments == nil {
c.Environments = make(map[string]Environment)
c.Environments[DefaultEnvironment] = Environment{}
}
// defaultCluster := clusters.ClusterInfo{
// NodeTypeID: "smallest",
// SparkVersion: "latest",
// AutoterminationMinutes: 30,
// }
return c, nil
}
func findProjectRoot() (string, error) {
wd, err := os.Getwd()
if err != nil {
return "", err
}
dir, err := folders.FindDirWithLeaf(wd, ConfigFile)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return "", fmt.Errorf("cannot find %s anywhere", ConfigFile)
}
}
return dir, nil
}

View File

@ -1,14 +0,0 @@
package project
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestLoadProjectConf(t *testing.T) {
prj, err := loadProjectConf("./testdata")
assert.NoError(t, err)
assert.Equal(t, "dev", prj.Name)
assert.True(t, prj.IsDevClusterJustReference())
}

View File

@ -1,41 +0,0 @@
package project
import (
"os"
"github.com/spf13/cobra"
)
const bricksEnv = "BRICKS_ENV"
const DefaultEnvironment = "development"
// Workspace defines configurables at the workspace level.
type Workspace struct {
Profile string `json:"profile,omitempty"`
}
// Environment defines all configurables for a single environment.
type Environment struct {
Workspace Workspace `json:"workspace"`
}
// getEnvironment returns the name of the environment to operate in.
func getEnvironment(cmd *cobra.Command) (value string) {
// The command line flag takes precedence.
flag := cmd.Flag("environment")
if flag != nil {
value = flag.Value.String()
if value != "" {
return
}
}
// If it's not set, use the environment variable.
value = os.Getenv(bricksEnv)
if value != "" {
return
}
return DefaultEnvironment
}

View File

@ -1,38 +0,0 @@
package project
import (
"testing"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
)
func TestEnvironmentFromCommand(t *testing.T) {
var cmd cobra.Command
cmd.Flags().String("environment", "", "specify environment")
cmd.Flags().Set("environment", "env-from-arg")
t.Setenv(bricksEnv, "")
value := getEnvironment(&cmd)
assert.Equal(t, "env-from-arg", value)
}
func TestEnvironmentFromEnvironment(t *testing.T) {
var cmd cobra.Command
cmd.Flags().String("environment", "", "specify environment")
cmd.Flags().Set("environment", "")
t.Setenv(bricksEnv, "env-from-env")
value := getEnvironment(&cmd)
assert.Equal(t, "env-from-env", value)
}
func TestEnvironmentDefault(t *testing.T) {
var cmd cobra.Command
cmd.Flags().String("environment", "", "specify environment")
cmd.Flags().Set("environment", "")
t.Setenv(bricksEnv, "")
value := getEnvironment(&cmd)
assert.Equal(t, DefaultEnvironment, value)
}

View File

@ -1,81 +0,0 @@
package project
// type Flavor interface {
// // Name returns a tuple of flavor key and readable name
// Name() (string, string)
// // Detected returns true on successful metadata checks
// Detected() bool
// // Build triggers packaging subprocesses
// Build(context.Context) error
// // TODO: Init() Questions
// // TODO: Deploy(context.Context) error
// }
// var _ Flavor = PythonWheel{}
// type PythonWheel struct{}
// func (pw PythonWheel) Name() (string, string) {
// return "wheel", "Python Wheel"
// }
// func (pw PythonWheel) Detected() bool {
// root, err := findProjectRoot()
// if err != nil {
// return false
// }
// _, err = os.Stat(fmt.Sprintf("%s/setup.py", root))
// return err == nil
// }
// func (pw PythonWheel) Build(ctx context.Context) error {
// defer toTheRootAndBack()()
// // do subprocesses or https://github.com/go-python/cpy3
// // it all depends on complexity and binary size
// // TODO: detect if there's an .venv here and call setup.py with ENV vars of it
// // TODO: where.exe python (WIN) / which python (UNIX)
// cmd := exec.CommandContext(ctx, "python", "setup.py", "bdist-wheel")
// err := cmd.Run()
// if err != nil {
// return err
// }
// return nil
// }
// func toTheRootAndBack() func() {
// wd, _ := os.Getwd()
// root, _ := findProjectRoot()
// os.Chdir(root)
// return func() {
// os.Chdir(wd)
// }
// }
// var _ Flavor = PythonNotebooks{}
// type PythonNotebooks struct{}
// func (n PythonNotebooks) Name() (string, string) {
// // or just "notebooks", as we might shuffle in scala?...
// return "python-notebooks", "Python Notebooks"
// }
// func (n PythonNotebooks) Detected() bool {
// // TODO: Steps:
// // - get all filenames
// // - read first X bytes from random 10 files and check
// // if they're "Databricks Notebook Source"
// return false
// }
// func (n PythonNotebooks) Build(ctx context.Context) error {
// // TODO: perhaps some linting?..
// return nil
// }
// func (n PythonNotebooks) Deploy(ctx context.Context) error {
// // TODO: recursively upload notebooks to a given workspace path
// return nil
// }

View File

@ -1,271 +0,0 @@
package project
import (
"context"
"fmt"
"os"
"path/filepath"
"sync"
"github.com/databricks/bricks/libs/git"
"github.com/databricks/databricks-sdk-go"
"github.com/databricks/databricks-sdk-go/service/commands"
"github.com/databricks/databricks-sdk-go/service/scim"
"github.com/spf13/cobra"
)
const CacheDirName = ".databricks"
type project struct {
mu sync.Mutex
root string
env string
config *Config
environment *Environment
wsc *databricks.WorkspaceClient
me *scim.User
fileSet *git.FileSet
}
// Configure is used as a PreRunE function for all commands that
// require a project to be configured. If a project could successfully
// be found and loaded, it is set on the command's context object.
func Configure(cmd *cobra.Command, args []string) error {
root, err := getRoot()
if err != nil {
return err
}
ctx, err := Initialize(cmd.Context(), root, getEnvironment(cmd))
if err != nil {
return err
}
cmd.SetContext(ctx)
return nil
}
// Placeholder to use as unique key in context.Context.
var projectKey int
// Initialize loads a project configuration given a root and environment.
// It stores the project on a new context.
// The project is available through the `Get()` function.
func Initialize(ctx context.Context, root, env string) (context.Context, error) {
config, err := loadProjectConf(root)
if err != nil {
return nil, err
}
// Confirm that the specified environment is valid.
environment, ok := config.Environments[env]
if !ok {
return nil, fmt.Errorf("environment [%s] not defined", env)
}
fileSet, err := git.NewFileSet(root)
if err != nil {
return nil, err
}
err = fileSet.EnsureValidGitIgnoreExists()
if err != nil {
return nil, err
}
p := project{
root: root,
env: env,
config: &config,
environment: &environment,
fileSet: fileSet,
}
p.initializeWorkspacesClient(ctx)
return context.WithValue(ctx, &projectKey, &p), nil
}
func (p *project) initializeWorkspacesClient(ctx context.Context) {
var config databricks.Config
// If the config specifies a profile, or other authentication related properties,
// pass them along to the SDK here. If nothing is defined, the SDK will figure
// out which autentication mechanism to use using enviroment variables.
if p.environment.Workspace.Profile != "" {
config.Profile = p.environment.Workspace.Profile
}
p.wsc = databricks.Must(databricks.NewWorkspaceClient(&config))
}
// Get returns the project as configured on the context.
// It panics if it isn't configured.
func Get(ctx context.Context) *project {
project, ok := ctx.Value(&projectKey).(*project)
if !ok {
panic(`context not configured with project`)
}
return project
}
// Make sure to initialize the workspaces client on project init
func (p *project) WorkspacesClient() *databricks.WorkspaceClient {
return p.wsc
}
func (p *project) Root() string {
return p.root
}
func (p *project) GetFileSet() *git.FileSet {
return p.fileSet
}
// This cache dir will contain any state, state overrides (per user overrides
// to the project config) or any generated artifacts (eg: sync snapshots)
// that should never be checked into Git.
//
// We enfore that cache dir (.databricks) is added to .gitignore
// because it contains per-user overrides that we do not want users to
// accidentally check into git
func (p *project) CacheDir() (string, error) {
// assert cache dir is present in git ignore
ign, err := p.fileSet.IgnoreDirectory(fmt.Sprintf("/%s/", CacheDirName))
if err != nil {
return "", fmt.Errorf("failed to check if directory %s is ignored: %w", CacheDirName, err)
}
if !ign {
return "", fmt.Errorf("please add /%s/ to .gitignore", CacheDirName)
}
cacheDirPath := filepath.Join(p.root, CacheDirName)
// create cache dir if it does not exist
if _, err := os.Stat(cacheDirPath); os.IsNotExist(err) {
err = os.Mkdir(cacheDirPath, os.ModeDir|os.ModePerm)
if err != nil {
return "", fmt.Errorf("failed to create cache directory %s with error: %s", cacheDirPath, err)
}
}
return cacheDirPath, nil
}
func (p *project) Config() Config {
return *p.config
}
func (p *project) Environment() Environment {
return *p.environment
}
func (p *project) Me() (*scim.User, error) {
p.mu.Lock()
defer p.mu.Unlock()
if p.me != nil {
return p.me, nil
}
me, err := p.wsc.CurrentUser.Me(context.Background())
if err != nil {
return nil, err
}
p.me = me
return me, nil
}
func (p *project) DeploymentIsolationPrefix() string {
if p.config.Isolation == None {
return p.config.Name
}
if p.config.Isolation == Soft {
me, err := p.Me()
if err != nil {
panic(err)
}
return fmt.Sprintf("%s/%s", p.config.Name, me.UserName)
}
panic(fmt.Errorf("unknow project isolation: %s", p.config.Isolation))
}
func getClusterIdFromClusterName(ctx context.Context,
wsc *databricks.WorkspaceClient,
clusterName string,
) (clusterId string, err error) {
clusterInfo, err := wsc.Clusters.GetByClusterName(ctx, clusterName)
if err != nil {
return "", err
}
return clusterInfo.ClusterId, nil
}
// Old version of getting development cluster details with isolation implemented.
// Kept just for reference. Remove once isolation is implemented properly
/*
func (p *project) DevelopmentCluster(ctx context.Context) (cluster clusters.ClusterInfo, err error) {
api := clusters.NewClustersAPI(ctx, p.Client()) // TODO: rewrite with normal SDK
if p.project.DevCluster == nil {
p.project.DevCluster = &clusters.Cluster{}
}
dc := p.project.DevCluster
if p.project.Isolation == Soft {
if p.project.IsDevClusterJustReference() {
err = fmt.Errorf("projects with soft isolation cannot have named clusters")
return
}
dc.ClusterName = fmt.Sprintf("dev/%s", p.DeploymentIsolationPrefix())
}
if dc.ClusterName == "" {
err = fmt.Errorf("please either pick `isolation: soft` or specify a shared cluster name")
return
}
return app.GetOrCreateRunningCluster(dc.ClusterName, *dc)
}
func runCommandOnDev(ctx context.Context, language, command string) common.CommandResults {
cluster, err := Current.DevelopmentCluster(ctx)
exec := Current.Client().CommandExecutor(ctx)
if err != nil {
return common.CommandResults{
ResultType: "error",
Summary: err.Error(),
}
}
return exec.Execute(cluster.ClusterID, language, command)
}
func RunPythonOnDev(ctx context.Context, command string) common.CommandResults {
return runCommandOnDev(ctx, "python", command)
}
*/
// TODO: Add safe access to p.project and p.project.DevCluster that throws errors if
// the fields are not defined properly
func (p *project) GetDevelopmentClusterId(ctx context.Context) (clusterId string, err error) {
clusterId = p.config.DevCluster.ClusterId
clusterName := p.config.DevCluster.ClusterName
if clusterId != "" {
return
} else if clusterName != "" {
// Add workspaces client on init
return getClusterIdFromClusterName(ctx, p.wsc, clusterName)
} else {
// TODO: Add the project config file location used to error message
err = fmt.Errorf("please define either development cluster's cluster_id or cluster_name in your project config")
return
}
}
func runCommandOnDev(ctx context.Context, language, command string) commands.Results {
clusterId, err := Get(ctx).GetDevelopmentClusterId(ctx)
if err != nil {
return commands.Results{
ResultType: "error",
Summary: err.Error(),
}
}
return Get(ctx).wsc.CommandExecutor.Execute(ctx, clusterId, language, command)
}
func RunPythonOnDev(ctx context.Context, command string) commands.Results {
return runCommandOnDev(ctx, "python", command)
}

View File

@ -1,117 +0,0 @@
package project
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestProjectInitialize(t *testing.T) {
ctx, err := Initialize(context.Background(), "./testdata", DefaultEnvironment)
require.NoError(t, err)
assert.Equal(t, Get(ctx).config.Name, "dev")
}
func TestProjectInitializationCreatesGitIgnoreIfAbsent(t *testing.T) {
// create project root with databricks.yml
projectDir := t.TempDir()
f1, err := os.Create(filepath.Join(projectDir, "databricks.yml"))
assert.NoError(t, err)
defer f1.Close()
ctx, err := Initialize(context.Background(), projectDir, DefaultEnvironment)
assert.NoError(t, err)
gitIgnorePath := filepath.Join(projectDir, ".gitignore")
assert.FileExists(t, gitIgnorePath)
fileBytes, err := os.ReadFile(gitIgnorePath)
assert.NoError(t, err)
assert.Contains(t, string(fileBytes), ".databricks")
prj := Get(ctx)
_, err = prj.CacheDir()
assert.NoError(t, err)
}
func TestProjectInitializationAddsCacheDirToGitIgnore(t *testing.T) {
// create project root with databricks.yml
projectDir := t.TempDir()
f1, err := os.Create(filepath.Join(projectDir, "databricks.yml"))
assert.NoError(t, err)
f1.Close()
gitIgnorePath := filepath.Join(projectDir, ".gitignore")
f2, err := os.Create(gitIgnorePath)
assert.NoError(t, err)
f2.Close()
ctx, err := Initialize(context.Background(), projectDir, DefaultEnvironment)
assert.NoError(t, err)
fileBytes, err := os.ReadFile(gitIgnorePath)
assert.NoError(t, err)
assert.Contains(t, string(fileBytes), ".databricks")
// Muck with mtime of this file manually because in GitHub Actions runners the
// mtime isn't updated on write automatically (probably to save I/Os).
// We perform a reload of .gitignore files only if their mtime has changed.
// Add a minute to ensure it is different if the value is truncated to full seconds.
future := time.Now().Add(time.Minute)
err = os.Chtimes(gitIgnorePath, future, future)
require.NoError(t, err)
prj := Get(ctx)
_, err = prj.CacheDir()
assert.NoError(t, err)
}
func TestProjectInitializationDoesNotAddCacheDirToGitIgnoreIfAlreadyPresent(t *testing.T) {
// create project root with databricks.yml
projectDir := t.TempDir()
f1, err := os.Create(filepath.Join(projectDir, "databricks.yml"))
assert.NoError(t, err)
f1.Close()
gitIgnorePath := filepath.Join(projectDir, ".gitignore")
err = os.WriteFile(gitIgnorePath, []byte(".databricks"), 0o644)
assert.NoError(t, err)
_, err = Initialize(context.Background(), projectDir, DefaultEnvironment)
assert.NoError(t, err)
fileBytes, err := os.ReadFile(gitIgnorePath)
assert.NoError(t, err)
assert.Equal(t, 1, strings.Count(string(fileBytes), ".databricks"))
}
func TestProjectCacheDir(t *testing.T) {
// create project root with databricks.yml
projectDir := t.TempDir()
f1, err := os.Create(filepath.Join(projectDir, "databricks.yml"))
assert.NoError(t, err)
f1.Close()
// create .gitignore with the .databricks dir in it
f2, err := os.Create(filepath.Join(projectDir, ".gitignore"))
assert.NoError(t, err)
content := []byte("/.databricks/")
_, err = f2.Write(content)
assert.NoError(t, err)
f2.Close()
ctx, err := Initialize(context.Background(), projectDir, DefaultEnvironment)
assert.NoError(t, err)
prj := Get(ctx)
cacheDir, err := prj.CacheDir()
assert.NoError(t, err)
assert.Equal(t, filepath.Join(projectDir, ".databricks"), cacheDir)
}

View File

@ -1,37 +0,0 @@
package project
import (
"fmt"
"os"
"github.com/databricks/bricks/folders"
)
const bricksRoot = "BRICKS_ROOT"
// getRoot returns the project root.
// If the `BRICKS_ROOT` environment variable is set, we assume its value
// to be a valid project root. Otherwise we try to find it by traversing
// the path and looking for a project configuration file.
func getRoot() (string, error) {
path, ok := os.LookupEnv(bricksRoot)
if ok {
stat, err := os.Stat(path)
if err == nil && !stat.IsDir() {
err = fmt.Errorf("not a directory")
}
if err != nil {
return "", fmt.Errorf(`invalid project root %s="%s": %w`, bricksRoot, path, err)
}
return path, nil
}
wd, err := os.Getwd()
if err != nil {
return "", err
}
path, err = folders.FindDirWithLeaf(wd, ConfigFile)
if err != nil {
return "", fmt.Errorf(`unable to locate project root`)
}
return path, nil
}

View File

@ -1,92 +0,0 @@
package project
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
)
// Changes into specified directory for the duration of the test.
// Returns the current working directory.
func chdir(t *testing.T, dir string) string {
wd, err := os.Getwd()
require.NoError(t, err)
abs, err := filepath.Abs(dir)
require.NoError(t, err)
err = os.Chdir(abs)
require.NoError(t, err)
t.Cleanup(func() {
err := os.Chdir(wd)
require.NoError(t, err)
})
return wd
}
func TestRootFromEnv(t *testing.T) {
dir := t.TempDir()
t.Setenv(bricksRoot, dir)
// It should pull the root from the environment variable.
root, err := getRoot()
require.NoError(t, err)
require.Equal(t, root, dir)
}
func TestRootFromEnvDoesntExist(t *testing.T) {
dir := t.TempDir()
t.Setenv(bricksRoot, filepath.Join(dir, "doesntexist"))
// It should pull the root from the environment variable.
_, err := getRoot()
require.Errorf(t, err, "invalid project root")
}
func TestRootFromEnvIsFile(t *testing.T) {
dir := t.TempDir()
f, err := os.Create(filepath.Join(dir, "invalid"))
require.NoError(t, err)
f.Close()
t.Setenv(bricksRoot, f.Name())
// It should pull the root from the environment variable.
_, err = getRoot()
require.Errorf(t, err, "invalid project root")
}
func TestRootIfEnvIsEmpty(t *testing.T) {
dir := ""
t.Setenv(bricksRoot, dir)
// It should pull the root from the environment variable.
_, err := getRoot()
require.Errorf(t, err, "invalid project root")
}
func TestRootLookup(t *testing.T) {
// Have to set then unset to allow the testing package to revert it to its original value.
t.Setenv(bricksRoot, "")
os.Unsetenv(bricksRoot)
// It should find the project root from $PWD.
wd := chdir(t, "./testdata/a/b/c")
root, err := getRoot()
require.NoError(t, err)
require.Equal(t, root, filepath.Join(wd, "testdata"))
}
func TestRootLookupError(t *testing.T) {
// Have to set then unset to allow the testing package to revert it to its original value.
t.Setenv(bricksRoot, "")
os.Unsetenv(bricksRoot)
// It can't find a project root from a temporary directory.
_ = chdir(t, t.TempDir())
_, err := getRoot()
require.ErrorContains(t, err, "unable to locate project root")
}

View File

@ -1,2 +0,0 @@
.databricks

View File

View File

View File

@ -1,4 +0,0 @@
name: dev
profile: demo
dev_cluster:
cluster_name: Shared Autoscaling

View File

@ -9,7 +9,7 @@ import (
"strings" "strings"
"github.com/databricks/bricks/libs/log" "github.com/databricks/bricks/libs/log"
"github.com/databricks/bricks/project" "github.com/databricks/databricks-sdk-go"
"github.com/databricks/databricks-sdk-go/service/dbfs" "github.com/databricks/databricks-sdk-go/service/dbfs"
) )
@ -63,7 +63,10 @@ func UploadWheelToDBFSWithPEP503(ctx context.Context, dir string) (string, error
// extra index URLs. See more pointers at https://stackoverflow.com/q/30889494/277035 // extra index URLs. See more pointers at https://stackoverflow.com/q/30889494/277035
dbfsLoc := fmt.Sprintf("%s/%s/%s", DBFSWheelLocation, dist.NormalizedName(), path.Base(wheel)) dbfsLoc := fmt.Sprintf("%s/%s/%s", DBFSWheelLocation, dist.NormalizedName(), path.Base(wheel))
wsc := project.Get(ctx).WorkspacesClient() wsc, err := databricks.NewWorkspaceClient(&databricks.Config{})
if err != nil {
return "", err
}
wf, err := os.Open(wheel) wf, err := os.Open(wheel)
if err != nil { if err != nil {
return "", err return "", err

View File

@ -1,195 +0,0 @@
/*
How to simplify terraform configuration for the project?
---
Solve the following adoption slowers:
- remove the need for `required_providers` block
- authenticate Databricks provider with the same DatabricksClient
- skip downloading and locking Databricks provider every time (few seconds)
- users won't have to copy-paste these into their configs:
```hcl
terraform {
required_providers {
databricks = {
source = "databrickslabs/databricks"
}
}
}
provider "databricks" {
}
```
Terraform Plugin SDK v2 is using similar techniques for testing providers. One may find
details in github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource/plugin.go. In short:
- init provider isntance
- start terraform plugin GRPC server
- "reattach" providers and specify the `tfexec.Reattach` options, which essentially
forward GRPC address to terraform subprocess.
- this can be done by either adding a source depenency on Databricks provider
or adding a special launch mode to it.
For now
---
Let's see how far we can get without GRPC magic.
*/
package terraform
const DeploymentStateRemoteLocation = "dbfs:/FileStore/deployment-state"
// type TerraformDeployer struct {
// WorkDir string
// CopyTfs bool
// tf *tfexec.Terraform
// }
// func (d *TerraformDeployer) Init(ctx context.Context) error {
// if d.CopyTfs {
// panic("copying tf configuration files to a temporary dir not yet implemented")
// }
// // TODO: most likely merge the methods
// exec, err := newTerraform(ctx, d.WorkDir, map[string]string{})
// if err != nil {
// return err
// }
// d.tf = exec
// return nil
// }
// // returns location of terraform state on DBFS based on project's deployment isolation level.
// func (d *TerraformDeployer) remoteTfstateLoc() string {
// prefix := project.Current.DeploymentIsolationPrefix()
// return fmt.Sprintf("%s/%s/terraform.tfstate", DeploymentStateRemoteLocation, prefix)
// }
// // returns structured representation of terraform state on DBFS.
// func (d *TerraformDeployer) remoteState(ctx context.Context) (*tfjson.State, int, error) {
// raw, err := utilities.ReadDbfsFile(ctx,
// project.Current.WorkspacesClient(),
// d.remoteTfstateLoc(),
// )
// if err != nil {
// return nil, 0, err
// }
// return d.tfstateFromReader(bytes.NewBuffer(raw))
// }
// // opens file handle for local-backend terraform state, that has to be closed in the calling
// // methods. this file alone is not the authoritative state of deployment and has to properly
// // be synced with remote counterpart.
// func (d *TerraformDeployer) openLocalState() (*os.File, error) {
// return os.Open(fmt.Sprintf("%s/terraform.tfstate", d.WorkDir))
// }
// // returns structured representation of terraform state on local machine. as part of
// // the optimistic concurrency control, please make sure to always compare the serial
// // number of local and remote states before proceeding with deployment.
// func (d *TerraformDeployer) localState() (*tfjson.State, int, error) {
// local, err := d.openLocalState()
// if err != nil {
// return nil, 0, err
// }
// defer local.Close()
// return d.tfstateFromReader(local)
// }
// // converts input stream into structured representation of terraform state and deployment
// // serial number, that helps controlling versioning and synchronisation via optimistic locking.
// func (d *TerraformDeployer) tfstateFromReader(reader io.Reader) (*tfjson.State, int, error) {
// var state tfjson.State
// state.UseJSONNumber(true)
// decoder := json.NewDecoder(reader)
// decoder.UseNumber()
// err := decoder.Decode(&state)
// if err != nil {
// return nil, 0, err
// }
// err = state.Validate()
// if err != nil {
// return nil, 0, err
// }
// var serialWrapper struct {
// Serial int `json:"serial,omitempty"`
// }
// // TODO: use byte buffer if this decoder fails on double reading
// err = decoder.Decode(&serialWrapper)
// if err != nil {
// return nil, 0, err
// }
// return &state, serialWrapper.Serial, nil
// }
// // uploads terraform state from local directory to designated DBFS location.
// func (d *TerraformDeployer) uploadTfstate(ctx context.Context) error {
// local, err := d.openLocalState()
// if err != nil {
// return err
// }
// defer local.Close()
// raw, err := io.ReadAll(local)
// if err != nil {
// return err
// }
// // TODO: make sure that deployment locks are implemented
// return utilities.CreateDbfsFile(ctx,
// project.Current.WorkspacesClient(),
// d.remoteTfstateLoc(),
// raw,
// true,
// )
// }
// // downloads terraform state from DBFS to local working directory.
// func (d *TerraformDeployer) downloadTfstate(ctx context.Context) error {
// remote, serialDeployed, err := d.remoteState(ctx)
// if err != nil {
// return err
// }
// log.Debugf(ctx, "remote serial is %d", serialDeployed)
// local, err := d.openLocalState()
// if err != nil {
// return err
// }
// defer local.Close()
// raw, err := json.Marshal(remote)
// if err != nil {
// return err
// }
// _, err = io.Copy(local, bytes.NewBuffer(raw))
// return err
// }
// // installs terraform to a temporary directory (for now)
// func installTerraform(ctx context.Context) (string, error) {
// // TODO: let configuration and/or environment variable specify
// // terraform binary. Or detect if terraform is installed in the $PATH
// installer := &releases.ExactVersion{
// Product: product.Terraform,
// Version: version.Must(version.NewVersion("1.1.0")),
// }
// return installer.Install(ctx)
// }
// func newTerraform(ctx context.Context, workDir string, env map[string]string) (*tfexec.Terraform, error) {
// execPath, err := installTerraform(ctx)
// if err != nil {
// return nil, err
// }
// // TODO: figure out how to cleanup/skip `.terraform*` files and dirs, not to confuse users
// // one of the options: take entire working directory with *.tf files and move them to tmpdir.
// // make it optional, of course, otherwise debugging may become super hard.
// tf, err := tfexec.NewTerraform(workDir, execPath)
// if err != nil {
// return nil, err
// }
// err = tf.SetEnv(env)
// if err != nil {
// return nil, err
// }
// return tf, err
// }

View File

@ -1,44 +0,0 @@
package terraform
// func TestSomething(t *testing.T) {
// ctx := context.Background()
// tf, err := newTerraform(ctx, "testdata/simplest", map[string]string{
// "DATABRICKS_HOST": "..",
// "DATABRICKS_TOKEN": "..",
// })
// assert.NoError(t, err)
// err = tf.Init(ctx)
// assert.NoError(t, err)
// planLoc := path.Join(t.TempDir(), "tfplan.zip")
// hasChanges, err := tf.Plan(ctx, tfexec.Out(planLoc))
// assert.True(t, hasChanges)
// assert.NoError(t, err)
// plan, err := tf.ShowPlanFile(ctx, planLoc)
// assert.NoError(t, err)
// assert.NotNil(t, plan)
// found := false
// for _, r := range plan.Config.RootModule.Resources {
// // TODO: add validator to prevent non-Databricks resources in *.tf files, as
// // we're not replacing terraform, we're wrapping it up for the average user.
// if r.Type != "databricks_job" {
// continue
// }
// // TODO: validate that libraries on jobs defined in *.tf and libraries
// // in `install_requires` defined in setup.py are the same. Exist with
// // the explanatory error otherwise.
// found = true
// // resource "databricks_job" "this"
// assert.Equal(t, "this", r.Name)
// // this is just a PoC to show how to retrieve DBR version from definitions.
// // production code should perform rigorous nil checks...
// nc := r.Expressions["new_cluster"]
// firstBlock := nc.NestedBlocks[0]
// ver := firstBlock["spark_version"].ConstantValue.(string)
// assert.Equal(t, "10.0.1", ver)
// }
// assert.True(t, found)
// }

View File

@ -1,41 +0,0 @@
terraform {
required_providers {
databricks = {
source = "databrickslabs/databricks"
}
}
}
provider "databricks" {
}
# This file cannot be used for tests until `InsecureSkipVerify` is exposed though env var
# data "databricks_current_user" "me" {}
# data "databricks_spark_version" "latest" {}
# data "databricks_node_type" "smallest" {
# local_disk = true
# }
resource "databricks_notebook" "this" {
path = "/Users/me@example.com/Terraform"
language = "PYTHON"
content_base64 = base64encode(<<-EOT
# created from ${abspath(path.module)}
display(spark.range(10))
EOT
)
}
resource "databricks_job" "this" {
name = "Terraform Demo (me@example.com)"
new_cluster {
num_workers = 1
spark_version = "10.0.1"
node_type_id = "i3.xlarge"
}
notebook_task {
notebook_path = databricks_notebook.this.path
}
}