2022-09-14 15:50:29 +00:00
|
|
|
package internal
|
|
|
|
|
|
|
|
import (
|
2022-12-09 10:47:06 +00:00
|
|
|
"bufio"
|
2022-10-10 08:27:45 +00:00
|
|
|
"bytes"
|
2022-12-09 10:47:06 +00:00
|
|
|
"context"
|
2023-11-07 19:06:27 +00:00
|
|
|
"encoding/json"
|
2024-02-20 16:14:37 +00:00
|
|
|
"errors"
|
2022-09-14 15:50:29 +00:00
|
|
|
"fmt"
|
2023-06-02 14:02:18 +00:00
|
|
|
"io"
|
2022-09-14 15:50:29 +00:00
|
|
|
"math/rand"
|
2024-02-20 16:14:37 +00:00
|
|
|
"net/http"
|
2022-09-14 15:50:29 +00:00
|
|
|
"os"
|
2024-02-20 16:14:37 +00:00
|
|
|
"path"
|
2022-10-10 08:27:45 +00:00
|
|
|
"path/filepath"
|
2023-06-14 12:53:27 +00:00
|
|
|
"reflect"
|
2022-09-14 15:50:29 +00:00
|
|
|
"strings"
|
2023-06-02 14:02:18 +00:00
|
|
|
"sync"
|
2022-09-14 15:50:29 +00:00
|
|
|
"testing"
|
|
|
|
"time"
|
2022-10-10 08:27:45 +00:00
|
|
|
|
2024-07-10 06:38:06 +00:00
|
|
|
"github.com/databricks/cli/cmd/root"
|
2024-10-16 12:50:17 +00:00
|
|
|
"github.com/databricks/cli/internal/acc"
|
2024-07-10 06:38:06 +00:00
|
|
|
"github.com/databricks/cli/libs/flags"
|
|
|
|
|
2023-07-25 18:19:07 +00:00
|
|
|
"github.com/databricks/cli/cmd"
|
2023-05-22 18:55:42 +00:00
|
|
|
_ "github.com/databricks/cli/cmd/version"
|
2023-11-07 19:06:27 +00:00
|
|
|
"github.com/databricks/cli/libs/cmdio"
|
2024-02-20 16:14:37 +00:00
|
|
|
"github.com/databricks/cli/libs/filer"
|
2023-10-03 11:18:55 +00:00
|
|
|
"github.com/databricks/databricks-sdk-go"
|
|
|
|
"github.com/databricks/databricks-sdk-go/apierr"
|
2024-02-20 16:14:37 +00:00
|
|
|
"github.com/databricks/databricks-sdk-go/service/catalog"
|
2023-10-03 11:18:55 +00:00
|
|
|
"github.com/databricks/databricks-sdk-go/service/compute"
|
|
|
|
"github.com/databricks/databricks-sdk-go/service/files"
|
|
|
|
"github.com/databricks/databricks-sdk-go/service/jobs"
|
|
|
|
"github.com/databricks/databricks-sdk-go/service/workspace"
|
2023-06-14 12:53:27 +00:00
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/spf13/pflag"
|
2022-10-10 08:27:45 +00:00
|
|
|
"github.com/stretchr/testify/require"
|
2023-05-26 14:02:53 +00:00
|
|
|
|
|
|
|
_ "github.com/databricks/cli/cmd/workspace"
|
2022-09-14 15:50:29 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
|
|
|
|
|
|
// GetEnvOrSkipTest proceeds with test only with that env variable
|
|
|
|
func GetEnvOrSkipTest(t *testing.T, name string) string {
|
|
|
|
value := os.Getenv(name)
|
|
|
|
if value == "" {
|
|
|
|
t.Skipf("Environment variable %s is missing", name)
|
|
|
|
}
|
|
|
|
return value
|
|
|
|
}
|
|
|
|
|
|
|
|
// RandomName gives random name with optional prefix. e.g. qa.RandomName("tf-")
|
|
|
|
func RandomName(prefix ...string) string {
|
|
|
|
randLen := 12
|
|
|
|
b := make([]byte, randLen)
|
|
|
|
for i := range b {
|
|
|
|
b[i] = charset[rand.Intn(randLen)]
|
|
|
|
}
|
|
|
|
if len(prefix) > 0 {
|
|
|
|
return fmt.Sprintf("%s%s", strings.Join(prefix, ""), b)
|
|
|
|
}
|
|
|
|
return string(b)
|
|
|
|
}
|
2022-10-10 08:27:45 +00:00
|
|
|
|
2023-05-16 16:35:39 +00:00
|
|
|
// Helper for running the root command in the background.
|
2022-12-09 10:47:06 +00:00
|
|
|
// It ensures that the background goroutine terminates upon
|
|
|
|
// test completion through cancelling the command context.
|
|
|
|
type cobraTestRunner struct {
|
|
|
|
*testing.T
|
|
|
|
|
|
|
|
args []string
|
|
|
|
stdout bytes.Buffer
|
|
|
|
stderr bytes.Buffer
|
2023-11-07 19:06:27 +00:00
|
|
|
stdinR *io.PipeReader
|
|
|
|
stdinW *io.PipeWriter
|
2022-12-09 10:47:06 +00:00
|
|
|
|
2023-09-07 14:08:16 +00:00
|
|
|
ctx context.Context
|
|
|
|
|
2023-06-02 14:02:18 +00:00
|
|
|
// Line-by-line output.
|
|
|
|
// Background goroutines populate these channels by reading from stdout/stderr pipes.
|
|
|
|
stdoutLines <-chan string
|
|
|
|
stderrLines <-chan string
|
|
|
|
|
2022-12-09 10:47:06 +00:00
|
|
|
errch <-chan error
|
|
|
|
}
|
|
|
|
|
2023-06-02 14:02:18 +00:00
|
|
|
func consumeLines(ctx context.Context, wg *sync.WaitGroup, r io.Reader) <-chan string {
|
2024-08-19 10:00:21 +00:00
|
|
|
ch := make(chan string, 30000)
|
2023-06-02 14:02:18 +00:00
|
|
|
wg.Add(1)
|
|
|
|
go func() {
|
|
|
|
defer close(ch)
|
|
|
|
defer wg.Done()
|
|
|
|
scanner := bufio.NewScanner(r)
|
|
|
|
for scanner.Scan() {
|
2024-08-12 07:07:50 +00:00
|
|
|
// We expect to be able to always send these lines into the channel.
|
|
|
|
// If we can't, it means the channel is full and likely there is a problem
|
|
|
|
// in either the test or the code under test.
|
2023-06-02 14:02:18 +00:00
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
return
|
|
|
|
case ch <- scanner.Text():
|
2024-08-12 07:07:50 +00:00
|
|
|
continue
|
|
|
|
default:
|
|
|
|
panic("line buffer is full")
|
2023-06-02 14:02:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
return ch
|
|
|
|
}
|
|
|
|
|
2023-06-14 12:53:27 +00:00
|
|
|
func (t *cobraTestRunner) registerFlagCleanup(c *cobra.Command) {
|
|
|
|
// Find target command that will be run. Example: if the command run is `databricks fs cp`,
|
|
|
|
// target command corresponds to `cp`
|
|
|
|
targetCmd, _, err := c.Find(t.args)
|
2024-07-10 06:38:06 +00:00
|
|
|
if err != nil && strings.HasPrefix(err.Error(), "unknown command") {
|
|
|
|
// even if command is unknown, we can proceed
|
|
|
|
require.NotNil(t, targetCmd)
|
|
|
|
} else {
|
|
|
|
require.NoError(t, err)
|
|
|
|
}
|
2023-06-14 12:53:27 +00:00
|
|
|
|
|
|
|
// Force initialization of default flags.
|
|
|
|
// These are initialized by cobra at execution time and would otherwise
|
|
|
|
// not be cleaned up by the cleanup function below.
|
|
|
|
targetCmd.InitDefaultHelpFlag()
|
|
|
|
targetCmd.InitDefaultVersionFlag()
|
|
|
|
|
|
|
|
// Restore flag values to their original value on test completion.
|
|
|
|
targetCmd.Flags().VisitAll(func(f *pflag.Flag) {
|
|
|
|
v := reflect.ValueOf(f.Value)
|
|
|
|
if v.Kind() == reflect.Ptr {
|
|
|
|
v = v.Elem()
|
|
|
|
}
|
|
|
|
// Store copy of the current flag value.
|
|
|
|
reset := reflect.New(v.Type()).Elem()
|
|
|
|
reset.Set(v)
|
|
|
|
t.Cleanup(func() {
|
|
|
|
v.Set(reset)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-11-07 19:06:27 +00:00
|
|
|
// Like [cobraTestRunner.Eventually], but more specific
|
|
|
|
func (t *cobraTestRunner) WaitForTextPrinted(text string, timeout time.Duration) {
|
|
|
|
t.Eventually(func() bool {
|
|
|
|
currentStdout := t.stdout.String()
|
|
|
|
return strings.Contains(currentStdout, text)
|
|
|
|
}, timeout, 50*time.Millisecond)
|
|
|
|
}
|
|
|
|
|
2024-02-14 18:04:45 +00:00
|
|
|
func (t *cobraTestRunner) WaitForOutput(text string, timeout time.Duration) {
|
|
|
|
require.Eventually(t.T, func() bool {
|
|
|
|
currentStdout := t.stdout.String()
|
|
|
|
currentErrout := t.stderr.String()
|
|
|
|
return strings.Contains(currentStdout, text) || strings.Contains(currentErrout, text)
|
|
|
|
}, timeout, 50*time.Millisecond)
|
|
|
|
}
|
|
|
|
|
2023-11-07 19:06:27 +00:00
|
|
|
func (t *cobraTestRunner) WithStdin() {
|
|
|
|
reader, writer := io.Pipe()
|
|
|
|
t.stdinR = reader
|
|
|
|
t.stdinW = writer
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t *cobraTestRunner) CloseStdin() {
|
|
|
|
if t.stdinW == nil {
|
|
|
|
panic("no standard input configured")
|
|
|
|
}
|
|
|
|
t.stdinW.Close()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t *cobraTestRunner) SendText(text string) {
|
|
|
|
if t.stdinW == nil {
|
|
|
|
panic("no standard input configured")
|
|
|
|
}
|
2024-12-09 12:56:41 +00:00
|
|
|
_, err := t.stdinW.Write([]byte(text + "\n"))
|
|
|
|
if err != nil {
|
|
|
|
panic("Failed to to write to t.stdinW")
|
|
|
|
}
|
2023-11-07 19:06:27 +00:00
|
|
|
}
|
|
|
|
|
2022-12-09 10:47:06 +00:00
|
|
|
func (t *cobraTestRunner) RunBackground() {
|
2023-06-02 14:02:18 +00:00
|
|
|
var stdoutR, stderrR io.Reader
|
|
|
|
var stdoutW, stderrW io.WriteCloser
|
|
|
|
stdoutR, stdoutW = io.Pipe()
|
|
|
|
stderrR, stderrW = io.Pipe()
|
2024-07-10 06:38:06 +00:00
|
|
|
ctx := cmdio.NewContext(t.ctx, &cmdio.Logger{
|
|
|
|
Mode: flags.ModeAppend,
|
|
|
|
Reader: bufio.Reader{},
|
|
|
|
Writer: stderrW,
|
|
|
|
})
|
|
|
|
|
|
|
|
cli := cmd.New(ctx)
|
|
|
|
cli.SetOut(stdoutW)
|
|
|
|
cli.SetErr(stderrW)
|
|
|
|
cli.SetArgs(t.args)
|
2023-11-07 19:06:27 +00:00
|
|
|
if t.stdinW != nil {
|
2024-07-10 06:38:06 +00:00
|
|
|
cli.SetIn(t.stdinR)
|
2023-11-07 19:06:27 +00:00
|
|
|
}
|
2022-12-09 10:47:06 +00:00
|
|
|
|
2023-06-14 12:53:27 +00:00
|
|
|
// Register cleanup function to restore flags to their original values
|
|
|
|
// once test has been executed. This is needed because flag values reside
|
|
|
|
// in a global singleton data-structure, and thus subsequent tests might
|
|
|
|
// otherwise interfere with each other
|
2024-07-10 06:38:06 +00:00
|
|
|
t.registerFlagCleanup(cli)
|
2023-06-14 12:53:27 +00:00
|
|
|
|
2022-12-09 10:47:06 +00:00
|
|
|
errch := make(chan error)
|
2024-07-10 06:38:06 +00:00
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
2022-12-09 10:47:06 +00:00
|
|
|
|
2023-06-02 14:02:18 +00:00
|
|
|
// Tee stdout/stderr to buffers.
|
|
|
|
stdoutR = io.TeeReader(stdoutR, &t.stdout)
|
|
|
|
stderrR = io.TeeReader(stderrR, &t.stderr)
|
|
|
|
|
|
|
|
// Consume stdout/stderr line-by-line.
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
t.stdoutLines = consumeLines(ctx, &wg, stdoutR)
|
|
|
|
t.stderrLines = consumeLines(ctx, &wg, stderrR)
|
|
|
|
|
2022-12-09 10:47:06 +00:00
|
|
|
// Run command in background.
|
|
|
|
go func() {
|
2024-07-10 06:38:06 +00:00
|
|
|
err := root.Execute(ctx, cli)
|
2022-12-09 10:47:06 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Logf("Error running command: %s", err)
|
|
|
|
}
|
|
|
|
|
2023-06-02 14:02:18 +00:00
|
|
|
// Close pipes to signal EOF.
|
|
|
|
stdoutW.Close()
|
|
|
|
stderrW.Close()
|
|
|
|
|
|
|
|
// Wait for the [consumeLines] routines to finish now that
|
|
|
|
// the pipes they're reading from have closed.
|
|
|
|
wg.Wait()
|
|
|
|
|
2022-12-09 10:47:06 +00:00
|
|
|
if t.stdout.Len() > 0 {
|
|
|
|
// Make a copy of the buffer such that it remains "unread".
|
|
|
|
scanner := bufio.NewScanner(bytes.NewBuffer(t.stdout.Bytes()))
|
|
|
|
for scanner.Scan() {
|
2023-05-16 16:35:39 +00:00
|
|
|
t.Logf("[databricks stdout]: %s", scanner.Text())
|
2022-12-09 10:47:06 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if t.stderr.Len() > 0 {
|
|
|
|
// Make a copy of the buffer such that it remains "unread".
|
|
|
|
scanner := bufio.NewScanner(bytes.NewBuffer(t.stderr.Bytes()))
|
|
|
|
for scanner.Scan() {
|
2023-05-16 16:35:39 +00:00
|
|
|
t.Logf("[databricks stderr]: %s", scanner.Text())
|
2022-12-09 10:47:06 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Reset context on command for the next test.
|
|
|
|
// These commands are globals so we have to clean up to the best of our ability after each run.
|
|
|
|
// See https://github.com/spf13/cobra/blob/a6f198b635c4b18fff81930c40d464904e55b161/command.go#L1062-L1066
|
2024-12-04 17:40:19 +00:00
|
|
|
//nolint:staticcheck // cobra sets the context and doesn't clear it
|
2024-07-10 06:38:06 +00:00
|
|
|
cli.SetContext(nil)
|
2022-12-09 10:47:06 +00:00
|
|
|
|
|
|
|
// Make caller aware of error.
|
|
|
|
errch <- err
|
|
|
|
close(errch)
|
|
|
|
}()
|
|
|
|
|
|
|
|
// Ensure command terminates upon test completion (success or failure).
|
|
|
|
t.Cleanup(func() {
|
|
|
|
// Signal termination of command.
|
|
|
|
cancel()
|
|
|
|
// Wait for goroutine to finish.
|
|
|
|
<-errch
|
|
|
|
})
|
|
|
|
|
|
|
|
t.errch = errch
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t *cobraTestRunner) Run() (bytes.Buffer, bytes.Buffer, error) {
|
|
|
|
t.RunBackground()
|
|
|
|
err := <-t.errch
|
|
|
|
return t.stdout, t.stderr, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Like [require.Eventually] but errors if the underlying command has failed.
|
2024-12-05 15:37:24 +00:00
|
|
|
func (c *cobraTestRunner) Eventually(condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...any) {
|
2022-12-09 10:47:06 +00:00
|
|
|
ch := make(chan bool, 1)
|
|
|
|
|
|
|
|
timer := time.NewTimer(waitFor)
|
|
|
|
defer timer.Stop()
|
|
|
|
|
|
|
|
ticker := time.NewTicker(tick)
|
|
|
|
defer ticker.Stop()
|
|
|
|
|
2023-06-02 14:02:18 +00:00
|
|
|
// Kick off condition check immediately.
|
|
|
|
go func() { ch <- condition() }()
|
|
|
|
|
2022-12-09 10:47:06 +00:00
|
|
|
for tick := ticker.C; ; {
|
|
|
|
select {
|
|
|
|
case err := <-c.errch:
|
|
|
|
require.Fail(c, "Command failed", err)
|
|
|
|
return
|
|
|
|
case <-timer.C:
|
|
|
|
require.Fail(c, "Condition never satisfied", msgAndArgs...)
|
|
|
|
return
|
|
|
|
case <-tick:
|
|
|
|
tick = nil
|
|
|
|
go func() { ch <- condition() }()
|
|
|
|
case v := <-ch:
|
|
|
|
if v {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
tick = ticker.C
|
|
|
|
}
|
2022-10-10 08:27:45 +00:00
|
|
|
}
|
2022-12-09 10:47:06 +00:00
|
|
|
}
|
|
|
|
|
2023-11-07 19:06:27 +00:00
|
|
|
func (t *cobraTestRunner) RunAndExpectOutput(heredoc string) {
|
|
|
|
stdout, _, err := t.Run()
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, cmdio.Heredoc(heredoc), strings.TrimSpace(stdout.String()))
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t *cobraTestRunner) RunAndParseJSON(v any) {
|
|
|
|
stdout, _, err := t.Run()
|
|
|
|
require.NoError(t, err)
|
|
|
|
err = json.Unmarshal(stdout.Bytes(), &v)
|
|
|
|
require.NoError(t, err)
|
|
|
|
}
|
|
|
|
|
2022-12-09 10:47:06 +00:00
|
|
|
func NewCobraTestRunner(t *testing.T, args ...string) *cobraTestRunner {
|
|
|
|
return &cobraTestRunner{
|
|
|
|
T: t,
|
2023-09-07 14:08:16 +00:00
|
|
|
ctx: context.Background(),
|
|
|
|
args: args,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewCobraTestRunnerWithContext(t *testing.T, ctx context.Context, args ...string) *cobraTestRunner {
|
|
|
|
return &cobraTestRunner{
|
|
|
|
T: t,
|
|
|
|
ctx: ctx,
|
2022-12-09 10:47:06 +00:00
|
|
|
args: args,
|
2022-10-10 08:27:45 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-09 14:41:04 +00:00
|
|
|
func RequireSuccessfulRun(t *testing.T, args ...string) (bytes.Buffer, bytes.Buffer) {
|
2023-06-16 15:09:08 +00:00
|
|
|
t.Logf("run args: [%s]", strings.Join(args, ", "))
|
2022-12-09 14:41:04 +00:00
|
|
|
c := NewCobraTestRunner(t, args...)
|
|
|
|
stdout, stderr, err := c.Run()
|
|
|
|
require.NoError(t, err)
|
|
|
|
return stdout, stderr
|
|
|
|
}
|
|
|
|
|
2023-05-26 12:46:08 +00:00
|
|
|
func RequireErrorRun(t *testing.T, args ...string) (bytes.Buffer, bytes.Buffer, error) {
|
|
|
|
c := NewCobraTestRunner(t, args...)
|
|
|
|
stdout, stderr, err := c.Run()
|
|
|
|
require.Error(t, err)
|
|
|
|
return stdout, stderr, err
|
|
|
|
}
|
|
|
|
|
Add support for non-Python ipynb notebooks to DABs (#1827)
## Changes
### Background
The workspace import APIs recently added support for importing Jupyter
notebooks written in R, Scala, or SQL, that is non-Python notebooks.
This now works for the `/import-file` API which we leverage in the CLI.
Note: We do not need any changes in `databricks sync`. It works out of
the box because any state mapping of local names to remote names that we
store is only scoped to the notebook extension (i.e., `.ipynb` in this
case) and is agnostic of the notebook's specific language.
### Problem this PR addresses
The extension-aware filer previously did not function because it checks
that a `.ipynb` notebook is written in Python. This PR relaxes that
constraint and adds integration tests for both the normal workspace
filer and extensions aware filer writing and reading non-Python `.ipynb`
notebooks.
This implies that after this PR DABs in the workspace / CLI from DBR
will work for non-Python notebooks as well. non-Python notebooks for
DABs deployment from local machines already works after the platform
side changes to the API landed, this PR just adds integration tests for
that bit of functionality.
Note: Any platform side changes we needed for the import API have
already been rolled out to production.
### Before
DABs deploy would work fine for non-Python notebooks. But DABs
deployments from DBR would not.
### After
DABs deploys both from local machines and DBR will work fine.
## Testing
For creating the `.ipynb` notebook fixtures used in the integration
tests I created them directly from the VSCode UI. This ensures high
fidelity with how users will create their non-Python notebooks locally.
For Python notebooks this is supported out of the box by VSCode but for
R and Scala notebooks this requires installing the Jupyter kernel for R
and Scala on my local machine and using that from VSCode.
For SQL, I ended up directly modifying the `language_info` field in the
Jupyter metadata to create the test fixture.
### Discussion: Issues with configuring language at the cell level
The language metadata for a Jupyter notebook is standardized at the
notebook level (in the `language_info` field). Unfortunately, it's not
standardized at the cell level. Thus, for example, if a user changes the
language for their cell in VSCode (which is supported by the standard
Jupyter VSCode integration), it'll cause a runtime error when the user
actually attempts to run the notebook. This is because the cell-level
metadata is encoded in a format specific to VSCode:
```
cells: []{
"vscode": {
"languageId": "sql"
}
}
```
Supporting cell level languages is thus out of scope for this PR and can
be revisited along with the workspace files team if there's strong
customer interest.
2024-11-13 21:39:51 +00:00
|
|
|
func readFile(t *testing.T, name string) string {
|
|
|
|
b, err := os.ReadFile(name)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
return string(b)
|
|
|
|
}
|
|
|
|
|
2022-10-10 08:27:45 +00:00
|
|
|
func writeFile(t *testing.T, name string, body string) string {
|
|
|
|
f, err := os.Create(filepath.Join(t.TempDir(), name))
|
|
|
|
require.NoError(t, err)
|
|
|
|
_, err = f.WriteString(body)
|
|
|
|
require.NoError(t, err)
|
|
|
|
f.Close()
|
|
|
|
return f.Name()
|
|
|
|
}
|
2023-10-03 11:18:55 +00:00
|
|
|
|
|
|
|
func GenerateNotebookTasks(notebookPath string, versions []string, nodeTypeId string) []jobs.SubmitTask {
|
|
|
|
tasks := make([]jobs.SubmitTask, 0)
|
|
|
|
for i := 0; i < len(versions); i++ {
|
|
|
|
task := jobs.SubmitTask{
|
|
|
|
TaskKey: fmt.Sprintf("notebook_%s", strings.ReplaceAll(versions[i], ".", "_")),
|
|
|
|
NotebookTask: &jobs.NotebookTask{
|
|
|
|
NotebookPath: notebookPath,
|
|
|
|
},
|
|
|
|
NewCluster: &compute.ClusterSpec{
|
|
|
|
SparkVersion: versions[i],
|
|
|
|
NumWorkers: 1,
|
|
|
|
NodeTypeId: nodeTypeId,
|
|
|
|
DataSecurityMode: compute.DataSecurityModeUserIsolation,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
tasks = append(tasks, task)
|
|
|
|
}
|
|
|
|
|
|
|
|
return tasks
|
|
|
|
}
|
|
|
|
|
|
|
|
func GenerateSparkPythonTasks(notebookPath string, versions []string, nodeTypeId string) []jobs.SubmitTask {
|
|
|
|
tasks := make([]jobs.SubmitTask, 0)
|
|
|
|
for i := 0; i < len(versions); i++ {
|
|
|
|
task := jobs.SubmitTask{
|
|
|
|
TaskKey: fmt.Sprintf("spark_%s", strings.ReplaceAll(versions[i], ".", "_")),
|
|
|
|
SparkPythonTask: &jobs.SparkPythonTask{
|
|
|
|
PythonFile: notebookPath,
|
|
|
|
},
|
|
|
|
NewCluster: &compute.ClusterSpec{
|
|
|
|
SparkVersion: versions[i],
|
|
|
|
NumWorkers: 1,
|
|
|
|
NodeTypeId: nodeTypeId,
|
|
|
|
DataSecurityMode: compute.DataSecurityModeUserIsolation,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
tasks = append(tasks, task)
|
|
|
|
}
|
|
|
|
|
|
|
|
return tasks
|
|
|
|
}
|
|
|
|
|
|
|
|
func GenerateWheelTasks(wheelPath string, versions []string, nodeTypeId string) []jobs.SubmitTask {
|
|
|
|
tasks := make([]jobs.SubmitTask, 0)
|
|
|
|
for i := 0; i < len(versions); i++ {
|
|
|
|
task := jobs.SubmitTask{
|
|
|
|
TaskKey: fmt.Sprintf("whl_%s", strings.ReplaceAll(versions[i], ".", "_")),
|
|
|
|
PythonWheelTask: &jobs.PythonWheelTask{
|
|
|
|
PackageName: "my_test_code",
|
|
|
|
EntryPoint: "run",
|
|
|
|
},
|
|
|
|
NewCluster: &compute.ClusterSpec{
|
|
|
|
SparkVersion: versions[i],
|
|
|
|
NumWorkers: 1,
|
|
|
|
NodeTypeId: nodeTypeId,
|
|
|
|
DataSecurityMode: compute.DataSecurityModeUserIsolation,
|
|
|
|
},
|
|
|
|
Libraries: []compute.Library{
|
|
|
|
{Whl: wheelPath},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
tasks = append(tasks, task)
|
|
|
|
}
|
|
|
|
|
|
|
|
return tasks
|
|
|
|
}
|
|
|
|
|
|
|
|
func TemporaryWorkspaceDir(t *testing.T, w *databricks.WorkspaceClient) string {
|
|
|
|
ctx := context.Background()
|
|
|
|
me, err := w.CurrentUser.Me(ctx)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
basePath := fmt.Sprintf("/Users/%s/%s", me.UserName, RandomName("integration-test-wsfs-"))
|
|
|
|
|
|
|
|
t.Logf("Creating %s", basePath)
|
|
|
|
err = w.Workspace.MkdirsByPath(ctx, basePath)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// Remove test directory on test completion.
|
|
|
|
t.Cleanup(func() {
|
|
|
|
t.Logf("Removing %s", basePath)
|
|
|
|
err := w.Workspace.Delete(ctx, workspace.Delete{
|
|
|
|
Path: basePath,
|
|
|
|
Recursive: true,
|
|
|
|
})
|
|
|
|
if err == nil || apierr.IsMissing(err) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
t.Logf("Unable to remove temporary workspace directory %s: %#v", basePath, err)
|
|
|
|
})
|
|
|
|
|
|
|
|
return basePath
|
|
|
|
}
|
|
|
|
|
|
|
|
func TemporaryDbfsDir(t *testing.T, w *databricks.WorkspaceClient) string {
|
|
|
|
ctx := context.Background()
|
|
|
|
path := fmt.Sprintf("/tmp/%s", RandomName("integration-test-dbfs-"))
|
|
|
|
|
|
|
|
t.Logf("Creating DBFS folder:%s", path)
|
|
|
|
err := w.Dbfs.MkdirsByPath(ctx, path)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
t.Cleanup(func() {
|
|
|
|
t.Logf("Removing DBFS folder:%s", path)
|
|
|
|
err := w.Dbfs.Delete(ctx, files.Delete{
|
|
|
|
Path: path,
|
|
|
|
Recursive: true,
|
|
|
|
})
|
|
|
|
if err == nil || apierr.IsMissing(err) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
t.Logf("unable to remove temporary dbfs directory %s: %#v", path, err)
|
|
|
|
})
|
|
|
|
|
|
|
|
return path
|
|
|
|
}
|
|
|
|
|
2024-02-20 16:14:37 +00:00
|
|
|
// Create a new UC volume in a catalog called "main" in the workspace.
|
2024-07-16 08:57:04 +00:00
|
|
|
func TemporaryUcVolume(t *testing.T, w *databricks.WorkspaceClient) string {
|
2024-02-20 16:14:37 +00:00
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
// Create a schema
|
|
|
|
schema, err := w.Schemas.Create(ctx, catalog.CreateSchema{
|
|
|
|
CatalogName: "main",
|
|
|
|
Name: RandomName("test-schema-"),
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
t.Cleanup(func() {
|
2024-12-09 12:56:41 +00:00
|
|
|
err := w.Schemas.Delete(ctx, catalog.DeleteSchemaRequest{
|
2024-02-20 16:14:37 +00:00
|
|
|
FullName: schema.FullName,
|
|
|
|
})
|
2024-12-09 12:56:41 +00:00
|
|
|
require.NoError(t, err)
|
2024-02-20 16:14:37 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
// Create a volume
|
|
|
|
volume, err := w.Volumes.Create(ctx, catalog.CreateVolumeRequestContent{
|
|
|
|
CatalogName: "main",
|
|
|
|
SchemaName: schema.Name,
|
|
|
|
Name: "my-volume",
|
|
|
|
VolumeType: catalog.VolumeTypeManaged,
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
t.Cleanup(func() {
|
2024-12-09 12:56:41 +00:00
|
|
|
err := w.Volumes.Delete(ctx, catalog.DeleteVolumeRequest{
|
2024-02-20 16:14:37 +00:00
|
|
|
Name: volume.FullName,
|
|
|
|
})
|
2024-12-09 12:56:41 +00:00
|
|
|
require.NoError(t, err)
|
2024-02-20 16:14:37 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
return path.Join("/Volumes", "main", schema.Name, volume.Name)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2023-10-03 11:18:55 +00:00
|
|
|
func TemporaryRepo(t *testing.T, w *databricks.WorkspaceClient) string {
|
|
|
|
ctx := context.Background()
|
|
|
|
me, err := w.CurrentUser.Me(ctx)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
repoPath := fmt.Sprintf("/Repos/%s/%s", me.UserName, RandomName("integration-test-repo-"))
|
|
|
|
|
|
|
|
t.Logf("Creating repo:%s", repoPath)
|
2024-10-07 13:21:05 +00:00
|
|
|
repoInfo, err := w.Repos.Create(ctx, workspace.CreateRepoRequest{
|
2023-10-03 11:18:55 +00:00
|
|
|
Url: "https://github.com/databricks/cli",
|
|
|
|
Provider: "github",
|
|
|
|
Path: repoPath,
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
t.Cleanup(func() {
|
|
|
|
t.Logf("Removing repo: %s", repoPath)
|
|
|
|
err := w.Repos.Delete(ctx, workspace.DeleteRepoRequest{
|
|
|
|
RepoId: repoInfo.Id,
|
|
|
|
})
|
|
|
|
if err == nil || apierr.IsMissing(err) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
t.Logf("unable to remove repo %s: %#v", repoPath, err)
|
|
|
|
})
|
|
|
|
|
|
|
|
return repoPath
|
|
|
|
}
|
|
|
|
|
|
|
|
func GetNodeTypeId(env string) string {
|
|
|
|
if env == "gcp" {
|
|
|
|
return "n1-standard-4"
|
2024-02-21 13:06:03 +00:00
|
|
|
} else if env == "aws" || env == "ucws" {
|
|
|
|
// aws-prod-ucws has CLOUD_ENV set to "ucws"
|
2023-10-03 11:18:55 +00:00
|
|
|
return "i3.xlarge"
|
|
|
|
}
|
|
|
|
return "Standard_DS4_v2"
|
|
|
|
}
|
2024-02-20 16:14:37 +00:00
|
|
|
|
|
|
|
func setupLocalFiler(t *testing.T) (filer.Filer, string) {
|
|
|
|
t.Log(GetEnvOrSkipTest(t, "CLOUD_ENV"))
|
|
|
|
|
|
|
|
tmp := t.TempDir()
|
|
|
|
f, err := filer.NewLocalClient(tmp)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
return f, path.Join(filepath.ToSlash(tmp))
|
|
|
|
}
|
|
|
|
|
|
|
|
func setupWsfsFiler(t *testing.T) (filer.Filer, string) {
|
Add support for non-Python ipynb notebooks to DABs (#1827)
## Changes
### Background
The workspace import APIs recently added support for importing Jupyter
notebooks written in R, Scala, or SQL, that is non-Python notebooks.
This now works for the `/import-file` API which we leverage in the CLI.
Note: We do not need any changes in `databricks sync`. It works out of
the box because any state mapping of local names to remote names that we
store is only scoped to the notebook extension (i.e., `.ipynb` in this
case) and is agnostic of the notebook's specific language.
### Problem this PR addresses
The extension-aware filer previously did not function because it checks
that a `.ipynb` notebook is written in Python. This PR relaxes that
constraint and adds integration tests for both the normal workspace
filer and extensions aware filer writing and reading non-Python `.ipynb`
notebooks.
This implies that after this PR DABs in the workspace / CLI from DBR
will work for non-Python notebooks as well. non-Python notebooks for
DABs deployment from local machines already works after the platform
side changes to the API landed, this PR just adds integration tests for
that bit of functionality.
Note: Any platform side changes we needed for the import API have
already been rolled out to production.
### Before
DABs deploy would work fine for non-Python notebooks. But DABs
deployments from DBR would not.
### After
DABs deploys both from local machines and DBR will work fine.
## Testing
For creating the `.ipynb` notebook fixtures used in the integration
tests I created them directly from the VSCode UI. This ensures high
fidelity with how users will create their non-Python notebooks locally.
For Python notebooks this is supported out of the box by VSCode but for
R and Scala notebooks this requires installing the Jupyter kernel for R
and Scala on my local machine and using that from VSCode.
For SQL, I ended up directly modifying the `language_info` field in the
Jupyter metadata to create the test fixture.
### Discussion: Issues with configuring language at the cell level
The language metadata for a Jupyter notebook is standardized at the
notebook level (in the `language_info` field). Unfortunately, it's not
standardized at the cell level. Thus, for example, if a user changes the
language for their cell in VSCode (which is supported by the standard
Jupyter VSCode integration), it'll cause a runtime error when the user
actually attempts to run the notebook. This is because the cell-level
metadata is encoded in a format specific to VSCode:
```
cells: []{
"vscode": {
"languageId": "sql"
}
}
```
Supporting cell level languages is thus out of scope for this PR and can
be revisited along with the workspace files team if there's strong
customer interest.
2024-11-13 21:39:51 +00:00
|
|
|
ctx, wt := acc.WorkspaceTest(t)
|
2024-02-20 16:14:37 +00:00
|
|
|
|
Add support for non-Python ipynb notebooks to DABs (#1827)
## Changes
### Background
The workspace import APIs recently added support for importing Jupyter
notebooks written in R, Scala, or SQL, that is non-Python notebooks.
This now works for the `/import-file` API which we leverage in the CLI.
Note: We do not need any changes in `databricks sync`. It works out of
the box because any state mapping of local names to remote names that we
store is only scoped to the notebook extension (i.e., `.ipynb` in this
case) and is agnostic of the notebook's specific language.
### Problem this PR addresses
The extension-aware filer previously did not function because it checks
that a `.ipynb` notebook is written in Python. This PR relaxes that
constraint and adds integration tests for both the normal workspace
filer and extensions aware filer writing and reading non-Python `.ipynb`
notebooks.
This implies that after this PR DABs in the workspace / CLI from DBR
will work for non-Python notebooks as well. non-Python notebooks for
DABs deployment from local machines already works after the platform
side changes to the API landed, this PR just adds integration tests for
that bit of functionality.
Note: Any platform side changes we needed for the import API have
already been rolled out to production.
### Before
DABs deploy would work fine for non-Python notebooks. But DABs
deployments from DBR would not.
### After
DABs deploys both from local machines and DBR will work fine.
## Testing
For creating the `.ipynb` notebook fixtures used in the integration
tests I created them directly from the VSCode UI. This ensures high
fidelity with how users will create their non-Python notebooks locally.
For Python notebooks this is supported out of the box by VSCode but for
R and Scala notebooks this requires installing the Jupyter kernel for R
and Scala on my local machine and using that from VSCode.
For SQL, I ended up directly modifying the `language_info` field in the
Jupyter metadata to create the test fixture.
### Discussion: Issues with configuring language at the cell level
The language metadata for a Jupyter notebook is standardized at the
notebook level (in the `language_info` field). Unfortunately, it's not
standardized at the cell level. Thus, for example, if a user changes the
language for their cell in VSCode (which is supported by the standard
Jupyter VSCode integration), it'll cause a runtime error when the user
actually attempts to run the notebook. This is because the cell-level
metadata is encoded in a format specific to VSCode:
```
cells: []{
"vscode": {
"languageId": "sql"
}
}
```
Supporting cell level languages is thus out of scope for this PR and can
be revisited along with the workspace files team if there's strong
customer interest.
2024-11-13 21:39:51 +00:00
|
|
|
tmpdir := TemporaryWorkspaceDir(t, wt.W)
|
|
|
|
f, err := filer.NewWorkspaceFilesClient(wt.W, tmpdir)
|
2024-02-20 16:14:37 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// Check if we can use this API here, skip test if we cannot.
|
|
|
|
_, err = f.Read(ctx, "we_use_this_call_to_test_if_this_api_is_enabled")
|
|
|
|
var aerr *apierr.APIError
|
|
|
|
if errors.As(err, &aerr) && aerr.StatusCode == http.StatusBadRequest {
|
|
|
|
t.Skip(aerr.Message)
|
|
|
|
}
|
|
|
|
|
|
|
|
return f, tmpdir
|
|
|
|
}
|
|
|
|
|
2024-05-30 11:59:27 +00:00
|
|
|
func setupWsfsExtensionsFiler(t *testing.T) (filer.Filer, string) {
|
Add support for non-Python ipynb notebooks to DABs (#1827)
## Changes
### Background
The workspace import APIs recently added support for importing Jupyter
notebooks written in R, Scala, or SQL, that is non-Python notebooks.
This now works for the `/import-file` API which we leverage in the CLI.
Note: We do not need any changes in `databricks sync`. It works out of
the box because any state mapping of local names to remote names that we
store is only scoped to the notebook extension (i.e., `.ipynb` in this
case) and is agnostic of the notebook's specific language.
### Problem this PR addresses
The extension-aware filer previously did not function because it checks
that a `.ipynb` notebook is written in Python. This PR relaxes that
constraint and adds integration tests for both the normal workspace
filer and extensions aware filer writing and reading non-Python `.ipynb`
notebooks.
This implies that after this PR DABs in the workspace / CLI from DBR
will work for non-Python notebooks as well. non-Python notebooks for
DABs deployment from local machines already works after the platform
side changes to the API landed, this PR just adds integration tests for
that bit of functionality.
Note: Any platform side changes we needed for the import API have
already been rolled out to production.
### Before
DABs deploy would work fine for non-Python notebooks. But DABs
deployments from DBR would not.
### After
DABs deploys both from local machines and DBR will work fine.
## Testing
For creating the `.ipynb` notebook fixtures used in the integration
tests I created them directly from the VSCode UI. This ensures high
fidelity with how users will create their non-Python notebooks locally.
For Python notebooks this is supported out of the box by VSCode but for
R and Scala notebooks this requires installing the Jupyter kernel for R
and Scala on my local machine and using that from VSCode.
For SQL, I ended up directly modifying the `language_info` field in the
Jupyter metadata to create the test fixture.
### Discussion: Issues with configuring language at the cell level
The language metadata for a Jupyter notebook is standardized at the
notebook level (in the `language_info` field). Unfortunately, it's not
standardized at the cell level. Thus, for example, if a user changes the
language for their cell in VSCode (which is supported by the standard
Jupyter VSCode integration), it'll cause a runtime error when the user
actually attempts to run the notebook. This is because the cell-level
metadata is encoded in a format specific to VSCode:
```
cells: []{
"vscode": {
"languageId": "sql"
}
}
```
Supporting cell level languages is thus out of scope for this PR and can
be revisited along with the workspace files team if there's strong
customer interest.
2024-11-13 21:39:51 +00:00
|
|
|
_, wt := acc.WorkspaceTest(t)
|
2024-05-30 11:59:27 +00:00
|
|
|
|
Add support for non-Python ipynb notebooks to DABs (#1827)
## Changes
### Background
The workspace import APIs recently added support for importing Jupyter
notebooks written in R, Scala, or SQL, that is non-Python notebooks.
This now works for the `/import-file` API which we leverage in the CLI.
Note: We do not need any changes in `databricks sync`. It works out of
the box because any state mapping of local names to remote names that we
store is only scoped to the notebook extension (i.e., `.ipynb` in this
case) and is agnostic of the notebook's specific language.
### Problem this PR addresses
The extension-aware filer previously did not function because it checks
that a `.ipynb` notebook is written in Python. This PR relaxes that
constraint and adds integration tests for both the normal workspace
filer and extensions aware filer writing and reading non-Python `.ipynb`
notebooks.
This implies that after this PR DABs in the workspace / CLI from DBR
will work for non-Python notebooks as well. non-Python notebooks for
DABs deployment from local machines already works after the platform
side changes to the API landed, this PR just adds integration tests for
that bit of functionality.
Note: Any platform side changes we needed for the import API have
already been rolled out to production.
### Before
DABs deploy would work fine for non-Python notebooks. But DABs
deployments from DBR would not.
### After
DABs deploys both from local machines and DBR will work fine.
## Testing
For creating the `.ipynb` notebook fixtures used in the integration
tests I created them directly from the VSCode UI. This ensures high
fidelity with how users will create their non-Python notebooks locally.
For Python notebooks this is supported out of the box by VSCode but for
R and Scala notebooks this requires installing the Jupyter kernel for R
and Scala on my local machine and using that from VSCode.
For SQL, I ended up directly modifying the `language_info` field in the
Jupyter metadata to create the test fixture.
### Discussion: Issues with configuring language at the cell level
The language metadata for a Jupyter notebook is standardized at the
notebook level (in the `language_info` field). Unfortunately, it's not
standardized at the cell level. Thus, for example, if a user changes the
language for their cell in VSCode (which is supported by the standard
Jupyter VSCode integration), it'll cause a runtime error when the user
actually attempts to run the notebook. This is because the cell-level
metadata is encoded in a format specific to VSCode:
```
cells: []{
"vscode": {
"languageId": "sql"
}
}
```
Supporting cell level languages is thus out of scope for this PR and can
be revisited along with the workspace files team if there's strong
customer interest.
2024-11-13 21:39:51 +00:00
|
|
|
tmpdir := TemporaryWorkspaceDir(t, wt.W)
|
|
|
|
f, err := filer.NewWorkspaceFilesExtensionsClient(wt.W, tmpdir)
|
2024-05-30 11:59:27 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
return f, tmpdir
|
|
|
|
}
|
|
|
|
|
2024-02-20 16:14:37 +00:00
|
|
|
func setupDbfsFiler(t *testing.T) (filer.Filer, string) {
|
2024-10-16 12:50:17 +00:00
|
|
|
_, wt := acc.WorkspaceTest(t)
|
2024-02-20 16:14:37 +00:00
|
|
|
|
2024-10-16 12:50:17 +00:00
|
|
|
tmpDir := TemporaryDbfsDir(t, wt.W)
|
|
|
|
f, err := filer.NewDbfsClient(wt.W, tmpDir)
|
2024-02-20 16:14:37 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
return f, path.Join("dbfs:/", tmpDir)
|
|
|
|
}
|
|
|
|
|
|
|
|
func setupUcVolumesFiler(t *testing.T) (filer.Filer, string) {
|
|
|
|
t.Log(GetEnvOrSkipTest(t, "CLOUD_ENV"))
|
|
|
|
|
|
|
|
if os.Getenv("TEST_METASTORE_ID") == "" {
|
|
|
|
t.Skip("Skipping tests that require a UC Volume when metastore id is not set.")
|
|
|
|
}
|
|
|
|
|
|
|
|
w, err := databricks.NewWorkspaceClient()
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
2024-07-16 08:57:04 +00:00
|
|
|
tmpDir := TemporaryUcVolume(t, w)
|
2024-02-20 16:14:37 +00:00
|
|
|
f, err := filer.NewFilesClient(w, tmpDir)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
return f, path.Join("dbfs:/", tmpDir)
|
|
|
|
}
|