Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions acceptance/bundle/validate/remote_file_path/databricks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
bundle:
name: remote_file_path

targets:
ok:
default: true
workspace:
file_path: /Workspace/tmp/remote_file_path/dir
bad:
workspace:
file_path: /Workspace/tmp/remote_file_path/file
5 changes: 5 additions & 0 deletions acceptance/bundle/validate/remote_file_path/out.test.toml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions acceptance/bundle/validate/remote_file_path/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

>>> [CLI] workspace mkdirs /Workspace/tmp/remote_file_path/dir

>>> [CLI] workspace import /Workspace/tmp/remote_file_path/file --format AUTO --file ./databricks.yml

=== file_path is a directory: valid destination

>>> [CLI] bundle validate -t ok
Name: remote_file_path
Target: ok
Workspace:
User: [USERNAME]
Path: /Workspace/Users/[USERNAME]/.bundle/remote_file_path/ok

Validation OK!

=== file_path is a file: invalid destination
Error: /Workspace/tmp/remote_file_path/file points to a file
Error: /Workspace/tmp/remote_file_path/file points to a file
Found 2 errors
15 changes: 15 additions & 0 deletions acceptance/bundle/validate/remote_file_path/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Git Bash on Windows rewrites leading-slash paths; keep them verbatim.
export MSYS_NO_PATHCONV=1

trace $CLI workspace mkdirs /Workspace/tmp/remote_file_path/dir
trace $CLI workspace import /Workspace/tmp/remote_file_path/file --format AUTO --file ./databricks.yml

title "file_path is a directory: valid destination\n"
trace $CLI bundle validate -t ok

# validate's slow validators run in parallel and stream diagnostics in
# nondeterministic order, so sort a projection (full output in LOG.validate). The
# destination error appears twice for now: from this validator and from FilesToSync.
title "file_path is a file: invalid destination\n"
errcode $CLI bundle validate -t bad > LOG.validate 2>&1
grep -E "^(Error|Warning|Found)" LOG.validate | sort
2 changes: 2 additions & 0 deletions acceptance/bundle/validate/remote_file_path/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# This test does not deploy, so it is engine-independent.
EnvMatrix.DATABRICKS_BUNDLE_ENGINE = []
1 change: 1 addition & 0 deletions bundle/config/validate/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func Validate(ctx context.Context, b *bundle.Bundle) {
// Slow mutators that require network or file i/o. These are only
// run in the `bundle validate` command.
FilesToSync(),
ValidateRemoteFilePath(),
ValidateFolderPermissions(),
ValidateSyncPatterns(),
)
Expand Down
55 changes: 55 additions & 0 deletions bundle/config/validate/validate_remote_file_path.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package validate

import (
"context"

"github.com/databricks/cli/bundle"
"github.com/databricks/cli/libs/diag"
"github.com/databricks/cli/libs/dyn"
"github.com/databricks/cli/libs/sync"
"github.com/databricks/databricks-sdk-go/service/iam"
)

type validateRemoteFilePath struct{ bundle.RO }

func (v *validateRemoteFilePath) Name() string {
return "validate:remote_file_path"
}

func (v *validateRemoteFilePath) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
// Under immutable_folder, file_path is not a sync destination: it is replaced
// with the content-addressed snapshot path at deploy time.
if b.IsImmutableFolder() {
return nil
}

// Nothing to check if the user opted out of syncing. Mirrors FilesToSync.
if len(b.Config.Sync.Paths) == 0 || b.Config.Workspace.FilePath == "" {
return nil
}

var me *iam.User
if b.Config.Workspace.CurrentUser != nil {
me = b.Config.Workspace.CurrentUser.User
}

// dryRun=true: assert the destination is a usable directory/repo without
// creating it. Deploy still creates it via sync.New.
err := sync.EnsureRemotePathIsUsable(ctx, b.WorkspaceClient(ctx), b.Config.Workspace.FilePath, me, true)
if err != nil {
return diag.Diagnostics{
diag.Diagnostic{
Severity: diag.Error,
Summary: err.Error(),
Locations: b.Config.GetLocations("workspace.file_path"),
Paths: []dyn.Path{dyn.MustPathFromString("workspace.file_path")},
},
}
}

return nil
}

func ValidateRemoteFilePath() bundle.ReadOnlyMutator {
return &validateRemoteFilePath{}
}
111 changes: 111 additions & 0 deletions bundle/config/validate/validate_remote_file_path_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package validate

import (
"testing"

"github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/config"
"github.com/databricks/cli/libs/diag"
sdkconfig "github.com/databricks/databricks-sdk-go/config"
"github.com/databricks/databricks-sdk-go/experimental/mocks"
"github.com/databricks/databricks-sdk-go/service/iam"
"github.com/databricks/databricks-sdk-go/service/workspace"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)

const testRemoteFilePath = "/Workspace/Users/me/.bundle/files"

func setupBundleForRemoteFilePathTest(t *testing.T, objectType workspace.ObjectType) *bundle.Bundle {
b := &bundle.Bundle{
Config: config.Root{
Workspace: config.Workspace{
FilePath: testRemoteFilePath,
CurrentUser: &config.User{
User: &iam.User{UserName: "me@example.com"},
},
},
Sync: config.Sync{
Paths: []string{"."},
},
},
}

m := mocks.NewMockWorkspaceClient(t)
m.WorkspaceClient.Config = &sdkconfig.Config{
Host: "https://foo.test",
}

// No MkdirsByPath expectation: the dry-run check must not mutate remote state,
// so an unexpected mkdir call fails the test.
m.GetMockWorkspaceAPI().EXPECT().GetStatusByPath(mock.Anything, testRemoteFilePath).Return(&workspace.ObjectInfo{
ObjectType: objectType,
Path: testRemoteFilePath,
}, nil)

b.SetWorkpaceClient(m.WorkspaceClient)
return b
}

func TestValidateRemoteFilePath_Directory(t *testing.T) {
b := setupBundleForRemoteFilePathTest(t, workspace.ObjectTypeDirectory)

ctx := t.Context()
diags := ValidateRemoteFilePath().Apply(ctx, b)
assert.Empty(t, diags)
}

func TestValidateRemoteFilePath_NotADirectory(t *testing.T) {
b := setupBundleForRemoteFilePathTest(t, workspace.ObjectTypeNotebook)

ctx := t.Context()
diags := ValidateRemoteFilePath().Apply(ctx, b)
require.Len(t, diags, 1)
assert.Equal(t, diag.Error, diags[0].Severity)
assert.Contains(t, diags[0].Summary, testRemoteFilePath)
}

func TestValidateRemoteFilePath_NoSyncPaths(t *testing.T) {
// No sync paths: nothing to check, so no workspace client is even set.
b := &bundle.Bundle{
Config: config.Root{
Workspace: config.Workspace{
FilePath: testRemoteFilePath,
},
Sync: config.Sync{
Paths: []string{},
},
},
}

ctx := t.Context()
diags := ValidateRemoteFilePath().Apply(ctx, b)
assert.Empty(t, diags)
}

func TestValidateRemoteFilePath_ImmutableFolder(t *testing.T) {
// Under immutable_folder, file_path is not a sync destination, so it is not checked.
b := &bundle.Bundle{
Config: config.Root{
Workspace: config.Workspace{
FilePath: testRemoteFilePath,
},
Sync: config.Sync{
Paths: []string{"."},
},
Experimental: &config.Experimental{
ImmutableFolder: true,
},
},
}

// Inject a mock client with no expectations: any workspace call (which would
// happen if the check were not skipped) fails the test.
m := mocks.NewMockWorkspaceClient(t)
b.SetWorkpaceClient(m.WorkspaceClient)

ctx := t.Context()
diags := ValidateRemoteFilePath().Apply(ctx, b)
assert.Empty(t, diags)
}
Loading