mirror of https://github.com/databricks/cli.git
Merge remote-tracking branch 'origin/main' into acceptance-init-with-output
This commit is contained in:
commit
5f0d8d3b89
2
Makefile
2
Makefile
|
@ -1,4 +1,4 @@
|
||||||
default: build
|
default: vendor fmt lint
|
||||||
|
|
||||||
PACKAGES=./acceptance/... ./libs/... ./internal/... ./cmd/... ./bundle/... .
|
PACKAGES=./acceptance/... ./libs/... ./internal/... ./cmd/... ./bundle/... .
|
||||||
|
|
||||||
|
|
|
@ -88,7 +88,7 @@ func TestAccept(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, user)
|
require.NotNil(t, user)
|
||||||
testdiff.PrepareReplacementsUser(t, &repls, *user)
|
testdiff.PrepareReplacementsUser(t, &repls, *user)
|
||||||
testdiff.PrepareReplacements(t, &repls, workspaceClient)
|
testdiff.PrepareReplacementsWorkspaceClient(t, &repls, workspaceClient)
|
||||||
|
|
||||||
testDirs := getTests(t)
|
testDirs := getTests(t)
|
||||||
require.NotEmpty(t, testDirs)
|
require.NotEmpty(t, testDirs)
|
||||||
|
@ -259,10 +259,23 @@ func BuildCLI(t *testing.T, cwd, coverDir string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
args := []string{"go", "build", "-mod", "vendor", "-o", execPath}
|
args := []string{
|
||||||
|
"go", "build",
|
||||||
|
"-mod", "vendor",
|
||||||
|
"-o", execPath,
|
||||||
|
}
|
||||||
|
|
||||||
if coverDir != "" {
|
if coverDir != "" {
|
||||||
args = append(args, "-cover")
|
args = append(args, "-cover")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
// Get this error on my local Windows:
|
||||||
|
// error obtaining VCS status: exit status 128
|
||||||
|
// Use -buildvcs=false to disable VCS stamping.
|
||||||
|
args = append(args, "-buildvcs=false")
|
||||||
|
}
|
||||||
|
|
||||||
cmd := exec.Command(args[0], args[1:]...)
|
cmd := exec.Command(args[0], args[1:]...)
|
||||||
cmd.Dir = ".."
|
cmd.Dir = ".."
|
||||||
out, err := cmd.CombinedOutput()
|
out, err := cmd.CombinedOutput()
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
bundle:
|
||||||
|
name: TestResolveVariableReferencesForPrimitiveNonStringFields
|
||||||
|
|
||||||
|
variables:
|
||||||
|
no_alert_for_canceled_runs: {}
|
||||||
|
no_alert_for_skipped_runs: {}
|
||||||
|
min_workers: {}
|
||||||
|
max_workers: {}
|
||||||
|
spot_bid_max_price: {}
|
||||||
|
|
||||||
|
resources:
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
notification_settings:
|
||||||
|
no_alert_for_canceled_runs: ${var.no_alert_for_canceled_runs}
|
||||||
|
no_alert_for_skipped_runs: ${var.no_alert_for_skipped_runs}
|
||||||
|
tasks:
|
||||||
|
- new_cluster:
|
||||||
|
autoscale:
|
||||||
|
min_workers: ${var.min_workers}
|
||||||
|
max_workers: ${var.max_workers}
|
||||||
|
azure_attributes:
|
||||||
|
spot_bid_max_price: ${var.spot_bid_max_price}
|
|
@ -0,0 +1,52 @@
|
||||||
|
{
|
||||||
|
"variables": {
|
||||||
|
"max_workers": {
|
||||||
|
"value": "2"
|
||||||
|
},
|
||||||
|
"min_workers": {
|
||||||
|
"value": "1"
|
||||||
|
},
|
||||||
|
"no_alert_for_canceled_runs": {
|
||||||
|
"value": "true"
|
||||||
|
},
|
||||||
|
"no_alert_for_skipped_runs": {
|
||||||
|
"value": "false"
|
||||||
|
},
|
||||||
|
"spot_bid_max_price": {
|
||||||
|
"value": "0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"jobs": {
|
||||||
|
"job1": {
|
||||||
|
"deployment": {
|
||||||
|
"kind": "BUNDLE",
|
||||||
|
"metadata_file_path": "/Workspace/Users/$USERNAME/.bundle/TestResolveVariableReferencesForPrimitiveNonStringFields/default/state/metadata.json"
|
||||||
|
},
|
||||||
|
"edit_mode": "UI_LOCKED",
|
||||||
|
"format": "MULTI_TASK",
|
||||||
|
"notification_settings": {
|
||||||
|
"no_alert_for_canceled_runs": true,
|
||||||
|
"no_alert_for_skipped_runs": false
|
||||||
|
},
|
||||||
|
"permissions": [],
|
||||||
|
"queue": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"tags": {},
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"new_cluster": {
|
||||||
|
"autoscale": {
|
||||||
|
"max_workers": 2,
|
||||||
|
"min_workers": 1
|
||||||
|
},
|
||||||
|
"azure_attributes": {
|
||||||
|
"spot_bid_max_price": 0.5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"task_key": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
export BUNDLE_VAR_no_alert_for_skipped_runs=false
|
||||||
|
export BUNDLE_VAR_max_workers=2
|
||||||
|
export BUNDLE_VAR_min_workers=3 # shadowed by --var below
|
||||||
|
$CLI bundle validate -o json --var no_alert_for_canceled_runs=true --var min_workers=1 --var spot_bid_max_price=0.5 | jq '{ variables, jobs: .resources.jobs }'
|
|
@ -0,0 +1,9 @@
|
||||||
|
bundle:
|
||||||
|
name: TestResolveVariableReferencesToBundleVariables
|
||||||
|
|
||||||
|
workspace:
|
||||||
|
root_path: "${bundle.name}/${var.foo}"
|
||||||
|
|
||||||
|
variables:
|
||||||
|
foo:
|
||||||
|
value: "bar"
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"artifact_path": "TestResolveVariableReferencesToBundleVariables/bar/artifacts",
|
||||||
|
"current_user": {
|
||||||
|
"short_name": "$USERNAME",
|
||||||
|
"userName": "$USERNAME"
|
||||||
|
},
|
||||||
|
"file_path": "TestResolveVariableReferencesToBundleVariables/bar/files",
|
||||||
|
"resource_path": "TestResolveVariableReferencesToBundleVariables/bar/resources",
|
||||||
|
"root_path": "TestResolveVariableReferencesToBundleVariables/bar",
|
||||||
|
"state_path": "TestResolveVariableReferencesToBundleVariables/bar/state"
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
$CLI bundle validate -o json | jq .workspace
|
|
@ -13,129 +13,9 @@ import (
|
||||||
"github.com/databricks/databricks-sdk-go/service/compute"
|
"github.com/databricks/databricks-sdk-go/service/compute"
|
||||||
"github.com/databricks/databricks-sdk-go/service/jobs"
|
"github.com/databricks/databricks-sdk-go/service/jobs"
|
||||||
"github.com/databricks/databricks-sdk-go/service/pipelines"
|
"github.com/databricks/databricks-sdk-go/service/pipelines"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestResolveVariableReferencesToBundleVariables(t *testing.T) {
|
|
||||||
b := &bundle.Bundle{
|
|
||||||
Config: config.Root{
|
|
||||||
Bundle: config.Bundle{
|
|
||||||
Name: "example",
|
|
||||||
},
|
|
||||||
Workspace: config.Workspace{
|
|
||||||
RootPath: "${bundle.name}/${var.foo}",
|
|
||||||
},
|
|
||||||
Variables: map[string]*variable.Variable{
|
|
||||||
"foo": {
|
|
||||||
Value: "bar",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply with a valid prefix. This should change the workspace root path.
|
|
||||||
diags := bundle.Apply(context.Background(), b, ResolveVariableReferences("bundle", "variables"))
|
|
||||||
require.NoError(t, diags.Error())
|
|
||||||
require.Equal(t, "example/bar", b.Config.Workspace.RootPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResolveVariableReferencesForPrimitiveNonStringFields(t *testing.T) {
|
|
||||||
var diags diag.Diagnostics
|
|
||||||
|
|
||||||
b := &bundle.Bundle{
|
|
||||||
Config: config.Root{
|
|
||||||
Variables: map[string]*variable.Variable{
|
|
||||||
"no_alert_for_canceled_runs": {},
|
|
||||||
"no_alert_for_skipped_runs": {},
|
|
||||||
"min_workers": {},
|
|
||||||
"max_workers": {},
|
|
||||||
"spot_bid_max_price": {},
|
|
||||||
},
|
|
||||||
Resources: config.Resources{
|
|
||||||
Jobs: map[string]*resources.Job{
|
|
||||||
"job1": {
|
|
||||||
JobSettings: &jobs.JobSettings{
|
|
||||||
NotificationSettings: &jobs.JobNotificationSettings{
|
|
||||||
NoAlertForCanceledRuns: false,
|
|
||||||
NoAlertForSkippedRuns: false,
|
|
||||||
},
|
|
||||||
Tasks: []jobs.Task{
|
|
||||||
{
|
|
||||||
NewCluster: &compute.ClusterSpec{
|
|
||||||
Autoscale: &compute.AutoScale{
|
|
||||||
MinWorkers: 0,
|
|
||||||
MaxWorkers: 0,
|
|
||||||
},
|
|
||||||
AzureAttributes: &compute.AzureAttributes{
|
|
||||||
SpotBidMaxPrice: 0.0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// Initialize the variables.
|
|
||||||
diags = bundle.ApplyFunc(ctx, b, func(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
|
|
||||||
err := b.Config.InitializeVariables([]string{
|
|
||||||
"no_alert_for_canceled_runs=true",
|
|
||||||
"no_alert_for_skipped_runs=true",
|
|
||||||
"min_workers=1",
|
|
||||||
"max_workers=2",
|
|
||||||
"spot_bid_max_price=0.5",
|
|
||||||
})
|
|
||||||
return diag.FromErr(err)
|
|
||||||
})
|
|
||||||
require.NoError(t, diags.Error())
|
|
||||||
|
|
||||||
// Assign the variables to the dynamic configuration.
|
|
||||||
diags = bundle.ApplyFunc(ctx, b, func(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
|
|
||||||
err := b.Config.Mutate(func(v dyn.Value) (dyn.Value, error) {
|
|
||||||
var p dyn.Path
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// Set the notification settings.
|
|
||||||
p = dyn.MustPathFromString("resources.jobs.job1.notification_settings")
|
|
||||||
v, err = dyn.SetByPath(v, p.Append(dyn.Key("no_alert_for_canceled_runs")), dyn.V("${var.no_alert_for_canceled_runs}"))
|
|
||||||
require.NoError(t, err)
|
|
||||||
v, err = dyn.SetByPath(v, p.Append(dyn.Key("no_alert_for_skipped_runs")), dyn.V("${var.no_alert_for_skipped_runs}"))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Set the min and max workers.
|
|
||||||
p = dyn.MustPathFromString("resources.jobs.job1.tasks[0].new_cluster.autoscale")
|
|
||||||
v, err = dyn.SetByPath(v, p.Append(dyn.Key("min_workers")), dyn.V("${var.min_workers}"))
|
|
||||||
require.NoError(t, err)
|
|
||||||
v, err = dyn.SetByPath(v, p.Append(dyn.Key("max_workers")), dyn.V("${var.max_workers}"))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Set the spot bid max price.
|
|
||||||
p = dyn.MustPathFromString("resources.jobs.job1.tasks[0].new_cluster.azure_attributes")
|
|
||||||
v, err = dyn.SetByPath(v, p.Append(dyn.Key("spot_bid_max_price")), dyn.V("${var.spot_bid_max_price}"))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
return v, nil
|
|
||||||
})
|
|
||||||
return diag.FromErr(err)
|
|
||||||
})
|
|
||||||
require.NoError(t, diags.Error())
|
|
||||||
|
|
||||||
// Apply for the variable prefix. This should resolve the variables to their values.
|
|
||||||
diags = bundle.Apply(context.Background(), b, ResolveVariableReferences("variables"))
|
|
||||||
require.NoError(t, diags.Error())
|
|
||||||
assert.True(t, b.Config.Resources.Jobs["job1"].JobSettings.NotificationSettings.NoAlertForCanceledRuns)
|
|
||||||
assert.True(t, b.Config.Resources.Jobs["job1"].JobSettings.NotificationSettings.NoAlertForSkippedRuns)
|
|
||||||
assert.Equal(t, 1, b.Config.Resources.Jobs["job1"].JobSettings.Tasks[0].NewCluster.Autoscale.MinWorkers)
|
|
||||||
assert.Equal(t, 2, b.Config.Resources.Jobs["job1"].JobSettings.Tasks[0].NewCluster.Autoscale.MaxWorkers)
|
|
||||||
assert.InDelta(t, 0.5, b.Config.Resources.Jobs["job1"].JobSettings.Tasks[0].NewCluster.AzureAttributes.SpotBidMaxPrice, 0.0001)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResolveComplexVariable(t *testing.T) {
|
func TestResolveComplexVariable(t *testing.T) {
|
||||||
b := &bundle.Bundle{
|
b := &bundle.Bundle{
|
||||||
Config: config.Root{
|
Config: config.Root{
|
||||||
|
|
|
@ -58,7 +58,10 @@ func testDefaultPython(t *testing.T, pythonVersion string) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, user)
|
require.NotNil(t, user)
|
||||||
testdiff.PrepareReplacementsUser(t, replacements, *user)
|
testdiff.PrepareReplacementsUser(t, replacements, *user)
|
||||||
testdiff.PrepareReplacements(t, replacements, wt.W)
|
testdiff.PrepareReplacementsWorkspaceClient(t, replacements, wt.W)
|
||||||
|
testdiff.PrepareReplacementsUUID(t, replacements)
|
||||||
|
testdiff.PrepareReplacementsNumber(t, replacements)
|
||||||
|
testdiff.PrepareReplacementsTemporaryDirectory(t, replacements)
|
||||||
|
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
testutil.Chdir(t, tmpDir)
|
testutil.Chdir(t, tmpDir)
|
||||||
|
|
|
@ -85,7 +85,7 @@ func testExecutorWithShell(t *testing.T, shell string) {
|
||||||
|
|
||||||
// Create temporary directory with only the shell executable in the PATH.
|
// Create temporary directory with only the shell executable in the PATH.
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
t.Setenv("PATH", tmpDir)
|
t.Setenv("PATH", fmt.Sprintf("%s%c%s", tmpDir, os.PathListSeparator, os.Getenv("PATH")))
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
err = os.Symlink(p, fmt.Sprintf("%s/%s.exe", tmpDir, shell))
|
err = os.Symlink(p, fmt.Sprintf("%s/%s.exe", tmpDir, shell))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
package testdiff
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type key int
|
||||||
|
|
||||||
|
const (
|
||||||
|
replacementsMapKey = key(1)
|
||||||
|
)
|
||||||
|
|
||||||
|
func WithReplacementsMap(ctx context.Context) (context.Context, *ReplacementsContext) {
|
||||||
|
value := ctx.Value(replacementsMapKey)
|
||||||
|
if value != nil {
|
||||||
|
if existingMap, ok := value.(*ReplacementsContext); ok {
|
||||||
|
return ctx, existingMap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newMap := &ReplacementsContext{}
|
||||||
|
ctx = context.WithValue(ctx, replacementsMapKey, newMap)
|
||||||
|
return ctx, newMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetReplacementsMap(ctx context.Context) *ReplacementsContext {
|
||||||
|
value := ctx.Value(replacementsMapKey)
|
||||||
|
if value != nil {
|
||||||
|
if existingMap, ok := value.(*ReplacementsContext); ok {
|
||||||
|
return existingMap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package testdiff
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetReplacementsMap_Nil(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
repls := GetReplacementsMap(ctx)
|
||||||
|
assert.Nil(t, repls)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetReplacementsMap_NotNil(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
ctx, _ = WithReplacementsMap(ctx)
|
||||||
|
repls := GetReplacementsMap(ctx)
|
||||||
|
assert.NotNil(t, repls)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithReplacementsMap_UseExisting(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
ctx, r1 := WithReplacementsMap(ctx)
|
||||||
|
ctx, r2 := WithReplacementsMap(ctx)
|
||||||
|
repls := GetReplacementsMap(ctx)
|
||||||
|
assert.Equal(t, r1, repls)
|
||||||
|
assert.Equal(t, r2, repls)
|
||||||
|
}
|
|
@ -2,25 +2,15 @@ package testdiff
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/databricks/cli/internal/testutil"
|
"github.com/databricks/cli/internal/testutil"
|
||||||
"github.com/databricks/cli/libs/iamutil"
|
|
||||||
"github.com/databricks/databricks-sdk-go"
|
|
||||||
"github.com/databricks/databricks-sdk-go/service/iam"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
testerName = "$USERNAME"
|
|
||||||
)
|
|
||||||
|
|
||||||
var OverwriteMode = false
|
var OverwriteMode = false
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -75,12 +65,6 @@ func AssertOutputJQ(t testutil.TestingT, ctx context.Context, out, outTitle, exp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
uuidRegex = regexp.MustCompile(`[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}`)
|
|
||||||
numIdRegex = regexp.MustCompile(`[0-9]{3,}`)
|
|
||||||
privatePathRegex = regexp.MustCompile(`(/tmp|/private)(/.*)/([a-zA-Z0-9]+)`)
|
|
||||||
)
|
|
||||||
|
|
||||||
func ReplaceOutput(t testutil.TestingT, ctx context.Context, out string) string {
|
func ReplaceOutput(t testutil.TestingT, ctx context.Context, out string) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
out = NormalizeNewlines(out)
|
out = NormalizeNewlines(out)
|
||||||
|
@ -88,139 +72,7 @@ func ReplaceOutput(t testutil.TestingT, ctx context.Context, out string) string
|
||||||
if replacements == nil {
|
if replacements == nil {
|
||||||
t.Fatal("WithReplacementsMap was not called")
|
t.Fatal("WithReplacementsMap was not called")
|
||||||
}
|
}
|
||||||
out = replacements.Replace(out)
|
return replacements.Replace(out)
|
||||||
out = uuidRegex.ReplaceAllString(out, "<UUID>")
|
|
||||||
out = numIdRegex.ReplaceAllString(out, "<NUMID>")
|
|
||||||
out = privatePathRegex.ReplaceAllString(out, "/tmp/.../$3")
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
type key int
|
|
||||||
|
|
||||||
const (
|
|
||||||
replacementsMapKey = key(1)
|
|
||||||
)
|
|
||||||
|
|
||||||
type Replacement struct {
|
|
||||||
Old string
|
|
||||||
New string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReplacementsContext struct {
|
|
||||||
Repls []Replacement
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ReplacementsContext) Replace(s string) string {
|
|
||||||
// QQQ Should probably only replace whole words
|
|
||||||
for _, repl := range r.Repls {
|
|
||||||
s = strings.ReplaceAll(s, repl.Old, repl.New)
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ReplacementsContext) Set(old, new string) {
|
|
||||||
if old == "" || new == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always include both verbatim and json version of replacement.
|
|
||||||
// This helps when the string in question contains \ or other chars that need to be quoted.
|
|
||||||
// In that case we cannot rely that json(old) == '"{old}"' and need to add it explicitly.
|
|
||||||
|
|
||||||
encodedNew, err := json.Marshal(new)
|
|
||||||
if err == nil {
|
|
||||||
encodedOld, err := json.Marshal(old)
|
|
||||||
if err == nil {
|
|
||||||
r.Repls = append(r.Repls, Replacement{Old: string(encodedOld), New: string(encodedNew)})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
r.Repls = append(r.Repls, Replacement{Old: old, New: new})
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithReplacementsMap(ctx context.Context) (context.Context, *ReplacementsContext) {
|
|
||||||
value := ctx.Value(replacementsMapKey)
|
|
||||||
if value != nil {
|
|
||||||
if existingMap, ok := value.(*ReplacementsContext); ok {
|
|
||||||
return ctx, existingMap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newMap := &ReplacementsContext{}
|
|
||||||
ctx = context.WithValue(ctx, replacementsMapKey, newMap)
|
|
||||||
return ctx, newMap
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetReplacementsMap(ctx context.Context) *ReplacementsContext {
|
|
||||||
value := ctx.Value(replacementsMapKey)
|
|
||||||
if value != nil {
|
|
||||||
if existingMap, ok := value.(*ReplacementsContext); ok {
|
|
||||||
return existingMap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func PrepareReplacements(t testutil.TestingT, r *ReplacementsContext, w *databricks.WorkspaceClient) {
|
|
||||||
t.Helper()
|
|
||||||
// in some clouds (gcp) w.Config.Host includes "https://" prefix in others it's really just a host (azure)
|
|
||||||
host := strings.TrimPrefix(strings.TrimPrefix(w.Config.Host, "http://"), "https://")
|
|
||||||
r.Set(host, "$DATABRICKS_HOST")
|
|
||||||
r.Set(w.Config.ClusterID, "$DATABRICKS_CLUSTER_ID")
|
|
||||||
r.Set(w.Config.WarehouseID, "$DATABRICKS_WAREHOUSE_ID")
|
|
||||||
r.Set(w.Config.ServerlessComputeID, "$DATABRICKS_SERVERLESS_COMPUTE_ID")
|
|
||||||
r.Set(w.Config.MetadataServiceURL, "$DATABRICKS_METADATA_SERVICE_URL")
|
|
||||||
r.Set(w.Config.AccountID, "$DATABRICKS_ACCOUNT_ID")
|
|
||||||
r.Set(w.Config.Token, "$DATABRICKS_TOKEN")
|
|
||||||
r.Set(w.Config.Username, "$DATABRICKS_USERNAME")
|
|
||||||
r.Set(w.Config.Password, "$DATABRICKS_PASSWORD")
|
|
||||||
r.Set(w.Config.Profile, "$DATABRICKS_CONFIG_PROFILE")
|
|
||||||
r.Set(w.Config.ConfigFile, "$DATABRICKS_CONFIG_FILE")
|
|
||||||
r.Set(w.Config.GoogleServiceAccount, "$DATABRICKS_GOOGLE_SERVICE_ACCOUNT")
|
|
||||||
r.Set(w.Config.GoogleCredentials, "$GOOGLE_CREDENTIALS")
|
|
||||||
r.Set(w.Config.AzureResourceID, "$DATABRICKS_AZURE_RESOURCE_ID")
|
|
||||||
r.Set(w.Config.AzureClientSecret, "$ARM_CLIENT_SECRET")
|
|
||||||
// r.Set(w.Config.AzureClientID, "$ARM_CLIENT_ID")
|
|
||||||
r.Set(w.Config.AzureClientID, testerName)
|
|
||||||
r.Set(w.Config.AzureTenantID, "$ARM_TENANT_ID")
|
|
||||||
r.Set(w.Config.ActionsIDTokenRequestURL, "$ACTIONS_ID_TOKEN_REQUEST_URL")
|
|
||||||
r.Set(w.Config.ActionsIDTokenRequestToken, "$ACTIONS_ID_TOKEN_REQUEST_TOKEN")
|
|
||||||
r.Set(w.Config.AzureEnvironment, "$ARM_ENVIRONMENT")
|
|
||||||
r.Set(w.Config.ClientID, "$DATABRICKS_CLIENT_ID")
|
|
||||||
r.Set(w.Config.ClientSecret, "$DATABRICKS_CLIENT_SECRET")
|
|
||||||
r.Set(w.Config.DatabricksCliPath, "$DATABRICKS_CLI_PATH")
|
|
||||||
// This is set to words like "path" that happen too frequently
|
|
||||||
// r.Set(w.Config.AuthType, "$DATABRICKS_AUTH_TYPE")
|
|
||||||
}
|
|
||||||
|
|
||||||
func PrepareReplacementsUser(t testutil.TestingT, r *ReplacementsContext, u iam.User) {
|
|
||||||
t.Helper()
|
|
||||||
// There could be exact matches or overlap between different name fields, so sort them by length
|
|
||||||
// to ensure we match the largest one first and map them all to the same token
|
|
||||||
|
|
||||||
r.Set(u.UserName, testerName)
|
|
||||||
r.Set(u.DisplayName, testerName)
|
|
||||||
if u.Name != nil {
|
|
||||||
r.Set(u.Name.FamilyName, testerName)
|
|
||||||
r.Set(u.Name.GivenName, testerName)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, val := range u.Emails {
|
|
||||||
r.Set(val.Value, testerName)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.Set(iamutil.GetShortUserName(&u), testerName)
|
|
||||||
|
|
||||||
for ind, val := range u.Groups {
|
|
||||||
r.Set(val.Value, fmt.Sprintf("$USER.Groups[%d]", ind))
|
|
||||||
}
|
|
||||||
|
|
||||||
r.Set(u.Id, "$USER.Id")
|
|
||||||
|
|
||||||
for ind, val := range u.Roles {
|
|
||||||
r.Set(val.Value, fmt.Sprintf("$USER.Roles[%d]", ind))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NormalizeNewlines(input string) string {
|
func NormalizeNewlines(input string) string {
|
||||||
|
|
|
@ -0,0 +1,153 @@
|
||||||
|
package testdiff
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/internal/testutil"
|
||||||
|
"github.com/databricks/cli/libs/iamutil"
|
||||||
|
"github.com/databricks/databricks-sdk-go"
|
||||||
|
"github.com/databricks/databricks-sdk-go/service/iam"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
testerName = "$USERNAME"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
uuidRegex = regexp.MustCompile(`[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}`)
|
||||||
|
numIdRegex = regexp.MustCompile(`[0-9]{3,}`)
|
||||||
|
privatePathRegex = regexp.MustCompile(`(/tmp|/private)(/.*)/([a-zA-Z0-9]+)`)
|
||||||
|
)
|
||||||
|
|
||||||
|
type Replacement struct {
|
||||||
|
Old *regexp.Regexp
|
||||||
|
New string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReplacementsContext struct {
|
||||||
|
Repls []Replacement
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReplacementsContext) Replace(s string) string {
|
||||||
|
// QQQ Should probably only replace whole words
|
||||||
|
for _, repl := range r.Repls {
|
||||||
|
s = repl.Old.ReplaceAllString(s, repl.New)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReplacementsContext) append(pattern *regexp.Regexp, replacement string) {
|
||||||
|
r.Repls = append(r.Repls, Replacement{
|
||||||
|
Old: pattern,
|
||||||
|
New: replacement,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReplacementsContext) appendLiteral(old, new string) {
|
||||||
|
r.append(
|
||||||
|
// Transform the input strings such that they can be used as literal strings in regular expressions.
|
||||||
|
regexp.MustCompile(regexp.QuoteMeta(old)),
|
||||||
|
// Transform the replacement string such that `$` is interpreted as a literal dollar sign.
|
||||||
|
// For more information about how the replacement string is used, see [regexp.Regexp.Expand].
|
||||||
|
strings.ReplaceAll(new, `$`, `$$`),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReplacementsContext) Set(old, new string) {
|
||||||
|
if old == "" || new == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always include both verbatim and json version of replacement.
|
||||||
|
// This helps when the string in question contains \ or other chars that need to be quoted.
|
||||||
|
// In that case we cannot rely that json(old) == '"{old}"' and need to add it explicitly.
|
||||||
|
|
||||||
|
encodedNew, err := json.Marshal(new)
|
||||||
|
if err == nil {
|
||||||
|
encodedOld, err := json.Marshal(old)
|
||||||
|
if err == nil {
|
||||||
|
r.appendLiteral(string(encodedOld), string(encodedNew))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.appendLiteral(old, new)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PrepareReplacementsWorkspaceClient(t testutil.TestingT, r *ReplacementsContext, w *databricks.WorkspaceClient) {
|
||||||
|
t.Helper()
|
||||||
|
// in some clouds (gcp) w.Config.Host includes "https://" prefix in others it's really just a host (azure)
|
||||||
|
host := strings.TrimPrefix(strings.TrimPrefix(w.Config.Host, "http://"), "https://")
|
||||||
|
r.Set(host, "$DATABRICKS_HOST")
|
||||||
|
r.Set(w.Config.ClusterID, "$DATABRICKS_CLUSTER_ID")
|
||||||
|
r.Set(w.Config.WarehouseID, "$DATABRICKS_WAREHOUSE_ID")
|
||||||
|
r.Set(w.Config.ServerlessComputeID, "$DATABRICKS_SERVERLESS_COMPUTE_ID")
|
||||||
|
r.Set(w.Config.MetadataServiceURL, "$DATABRICKS_METADATA_SERVICE_URL")
|
||||||
|
r.Set(w.Config.AccountID, "$DATABRICKS_ACCOUNT_ID")
|
||||||
|
r.Set(w.Config.Token, "$DATABRICKS_TOKEN")
|
||||||
|
r.Set(w.Config.Username, "$DATABRICKS_USERNAME")
|
||||||
|
r.Set(w.Config.Password, "$DATABRICKS_PASSWORD")
|
||||||
|
r.Set(w.Config.Profile, "$DATABRICKS_CONFIG_PROFILE")
|
||||||
|
r.Set(w.Config.ConfigFile, "$DATABRICKS_CONFIG_FILE")
|
||||||
|
r.Set(w.Config.GoogleServiceAccount, "$DATABRICKS_GOOGLE_SERVICE_ACCOUNT")
|
||||||
|
r.Set(w.Config.GoogleCredentials, "$GOOGLE_CREDENTIALS")
|
||||||
|
r.Set(w.Config.AzureResourceID, "$DATABRICKS_AZURE_RESOURCE_ID")
|
||||||
|
r.Set(w.Config.AzureClientSecret, "$ARM_CLIENT_SECRET")
|
||||||
|
// r.Set(w.Config.AzureClientID, "$ARM_CLIENT_ID")
|
||||||
|
r.Set(w.Config.AzureClientID, testerName)
|
||||||
|
r.Set(w.Config.AzureTenantID, "$ARM_TENANT_ID")
|
||||||
|
r.Set(w.Config.ActionsIDTokenRequestURL, "$ACTIONS_ID_TOKEN_REQUEST_URL")
|
||||||
|
r.Set(w.Config.ActionsIDTokenRequestToken, "$ACTIONS_ID_TOKEN_REQUEST_TOKEN")
|
||||||
|
r.Set(w.Config.AzureEnvironment, "$ARM_ENVIRONMENT")
|
||||||
|
r.Set(w.Config.ClientID, "$DATABRICKS_CLIENT_ID")
|
||||||
|
r.Set(w.Config.ClientSecret, "$DATABRICKS_CLIENT_SECRET")
|
||||||
|
r.Set(w.Config.DatabricksCliPath, "$DATABRICKS_CLI_PATH")
|
||||||
|
// This is set to words like "path" that happen too frequently
|
||||||
|
// r.Set(w.Config.AuthType, "$DATABRICKS_AUTH_TYPE")
|
||||||
|
}
|
||||||
|
|
||||||
|
func PrepareReplacementsUser(t testutil.TestingT, r *ReplacementsContext, u iam.User) {
|
||||||
|
t.Helper()
|
||||||
|
// There could be exact matches or overlap between different name fields, so sort them by length
|
||||||
|
// to ensure we match the largest one first and map them all to the same token
|
||||||
|
|
||||||
|
r.Set(u.UserName, testerName)
|
||||||
|
r.Set(u.DisplayName, testerName)
|
||||||
|
if u.Name != nil {
|
||||||
|
r.Set(u.Name.FamilyName, testerName)
|
||||||
|
r.Set(u.Name.GivenName, testerName)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, val := range u.Emails {
|
||||||
|
r.Set(val.Value, testerName)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Set(iamutil.GetShortUserName(&u), testerName)
|
||||||
|
|
||||||
|
for ind, val := range u.Groups {
|
||||||
|
r.Set(val.Value, fmt.Sprintf("$USER.Groups[%d]", ind))
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Set(u.Id, "$USER.Id")
|
||||||
|
|
||||||
|
for ind, val := range u.Roles {
|
||||||
|
r.Set(val.Value, fmt.Sprintf("$USER.Roles[%d]", ind))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PrepareReplacementsUUID(t testutil.TestingT, r *ReplacementsContext) {
|
||||||
|
t.Helper()
|
||||||
|
r.append(uuidRegex, "<UUID>")
|
||||||
|
}
|
||||||
|
|
||||||
|
func PrepareReplacementsNumber(t testutil.TestingT, r *ReplacementsContext) {
|
||||||
|
t.Helper()
|
||||||
|
r.append(numIdRegex, "<NUMID>")
|
||||||
|
}
|
||||||
|
|
||||||
|
func PrepareReplacementsTemporaryDirectory(t testutil.TestingT, r *ReplacementsContext) {
|
||||||
|
t.Helper()
|
||||||
|
r.append(privatePathRegex, "/tmp/.../$3")
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package testdiff
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReplacement_Literal(t *testing.T) {
|
||||||
|
var repls ReplacementsContext
|
||||||
|
|
||||||
|
repls.Set(`foobar`, `[replacement]`)
|
||||||
|
assert.Equal(t, `[replacement]`, repls.Replace(`foobar`))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReplacement_Encoded(t *testing.T) {
|
||||||
|
var repls ReplacementsContext
|
||||||
|
|
||||||
|
repls.Set(`foo"bar`, `[replacement]`)
|
||||||
|
assert.Equal(t, `"[replacement]"`, repls.Replace(`"foo\"bar"`))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReplacement_UUID(t *testing.T) {
|
||||||
|
var repls ReplacementsContext
|
||||||
|
|
||||||
|
PrepareReplacementsUUID(t, &repls)
|
||||||
|
|
||||||
|
assert.Equal(t, "<UUID>", repls.Replace("123e4567-e89b-12d3-a456-426614174000"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReplacement_Number(t *testing.T) {
|
||||||
|
var repls ReplacementsContext
|
||||||
|
|
||||||
|
PrepareReplacementsNumber(t, &repls)
|
||||||
|
|
||||||
|
assert.Equal(t, "12", repls.Replace("12"))
|
||||||
|
assert.Equal(t, "<NUMID>", repls.Replace("123"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReplacement_TemporaryDirectory(t *testing.T) {
|
||||||
|
var repls ReplacementsContext
|
||||||
|
|
||||||
|
PrepareReplacementsTemporaryDirectory(t, &repls)
|
||||||
|
|
||||||
|
assert.Equal(t, "/tmp/.../tail", repls.Replace("/tmp/foo/bar/qux/tail"))
|
||||||
|
}
|
Loading…
Reference in New Issue