diff --git a/bundle/config/root.go b/bundle/config/root.go index 88197c2b87c..2bc905bd605 100644 --- a/bundle/config/root.go +++ b/bundle/config/root.go @@ -471,3 +471,9 @@ func (r Root) GetLocation(path string) dyn.Location { } return v.Location() } + +// Value returns the dynamic configuration value of the root object. This value +// is the source of truth and is kept in sync with values in the typed configuration. +func (r Root) Value() dyn.Value { + return r.value +} diff --git a/cmd/bundle/validate.go b/cmd/bundle/validate.go index 8d49ec961b1..a1f8d268197 100644 --- a/cmd/bundle/validate.go +++ b/cmd/bundle/validate.go @@ -119,7 +119,7 @@ func renderTextOutput(cmd *cobra.Command, b *bundle.Bundle, diags diag.Diagnosti } func renderJsonOutput(cmd *cobra.Command, b *bundle.Bundle, diags diag.Diagnostics) error { - buf, err := json.MarshalIndent(b.Config, "", " ") + buf, err := json.MarshalIndent(b.Config.Value().AsAny(), "", " ") if err != nil { return err } diff --git a/internal/bundle/helpers.go b/internal/bundle/helpers.go index 560a0474b19..a17964b167f 100644 --- a/internal/bundle/helpers.go +++ b/internal/bundle/helpers.go @@ -51,6 +51,13 @@ func writeConfigFile(t *testing.T, config map[string]any) (string, error) { return filepath, err } +func validateBundle(t *testing.T, ctx context.Context, path string) ([]byte, error) { + t.Setenv("BUNDLE_ROOT", path) + c := internal.NewCobraTestRunnerWithContext(t, ctx, "bundle", "validate", "--output", "json") + stdout, _, err := c.Run() + return stdout.Bytes(), err +} + func deployBundle(t *testing.T, ctx context.Context, path string) error { t.Setenv("BUNDLE_ROOT", path) c := internal.NewCobraTestRunnerWithContext(t, ctx, "bundle", "deploy", "--force-lock") diff --git a/internal/bundle/validate_test.go b/internal/bundle/validate_test.go new file mode 100644 index 00000000000..18da89e4cea --- /dev/null +++ b/internal/bundle/validate_test.go @@ -0,0 +1,60 @@ +package bundle + +import ( + "context" + "encoding/json" + "testing" + + "github.com/databricks/cli/internal/testutil" + "github.com/databricks/cli/libs/dyn" + "github.com/databricks/cli/libs/dyn/convert" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAccBundleValidate(t *testing.T) { + testutil.GetEnvOrSkipTest(t, "CLOUD_ENV") + + tmpDir := t.TempDir() + testutil.WriteFile(t, + ` +bundle: + name: "foobar" + +resources: + jobs: + outer_loop: + name: outer loop + tasks: + - task_key: my task + run_job_task: + job_id: ${resources.jobs.inner_loop.id} + + inner_loop: + name: inner loop + +`, tmpDir, "databricks.yml") + + ctx := context.Background() + stdout, err := validateBundle(t, ctx, tmpDir) + require.NoError(t, err) + + config := make(map[string]any) + err = json.Unmarshal(stdout, &config) + require.NoError(t, err) + + getValue := func(key string) any { + v, err := convert.FromTyped(config, dyn.NilValue) + require.NoError(t, err) + v, err = dyn.GetByPath(v, dyn.MustPathFromString(key)) + require.NoError(t, err) + return v.AsAny() + } + + assert.Equal(t, "foobar", getValue("bundle.name")) + assert.Equal(t, "outer loop", getValue("resources.jobs.outer_loop.name")) + assert.Equal(t, "inner loop", getValue("resources.jobs.inner_loop.name")) + assert.Equal(t, "my task", getValue("resources.jobs.outer_loop.tasks[0].task_key")) + // Assert resource references are retained in the output. + assert.Equal(t, "${resources.jobs.inner_loop.id}", getValue("resources.jobs.outer_loop.tasks[0].run_job_task.job_id")) +} diff --git a/internal/testutil/file.go b/internal/testutil/file.go new file mode 100644 index 00000000000..ba2c3280e2c --- /dev/null +++ b/internal/testutil/file.go @@ -0,0 +1,48 @@ +package testutil + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TouchNotebook(t *testing.T, elems ...string) string { + path := filepath.Join(elems...) + err := os.MkdirAll(filepath.Dir(path), 0755) + require.NoError(t, err) + + err = os.WriteFile(path, []byte("# Databricks notebook source"), 0644) + require.NoError(t, err) + return path +} + +func Touch(t *testing.T, elems ...string) string { + path := filepath.Join(elems...) + err := os.MkdirAll(filepath.Dir(path), 0755) + require.NoError(t, err) + + f, err := os.Create(path) + require.NoError(t, err) + + err = f.Close() + require.NoError(t, err) + return path +} + +func WriteFile(t *testing.T, content string, elems ...string) string { + path := filepath.Join(elems...) + err := os.MkdirAll(filepath.Dir(path), 0755) + require.NoError(t, err) + + f, err := os.Create(path) + require.NoError(t, err) + + _, err = f.WriteString(content) + require.NoError(t, err) + + err = f.Close() + require.NoError(t, err) + return path +} diff --git a/internal/testutil/touch.go b/internal/testutil/touch.go deleted file mode 100644 index 55683f3eda0..00000000000 --- a/internal/testutil/touch.go +++ /dev/null @@ -1,26 +0,0 @@ -package testutil - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" -) - -func TouchNotebook(t *testing.T, elems ...string) string { - path := filepath.Join(elems...) - os.MkdirAll(filepath.Dir(path), 0755) - err := os.WriteFile(path, []byte("# Databricks notebook source"), 0644) - require.NoError(t, err) - return path -} - -func Touch(t *testing.T, elems ...string) string { - path := filepath.Join(elems...) - os.MkdirAll(filepath.Dir(path), 0755) - f, err := os.Create(path) - require.NoError(t, err) - f.Close() - return path -}