Skip to content
Open
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
1 change: 1 addition & 0 deletions NEXT_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
* Fix permissions added to a job or pipeline by a Python (PyDABs) mutator failing to deploy with "must have exactly one owner"; the deploying identity is now set as owner, matching resources whose permissions are declared in YAML ([#5821](https://github.com/databricks/cli/pull/5821)).
* Remove duplicate enum values for jsonschema.json ([#5839](https://github.com/databricks/cli/pull/5839)).
* direct: volumes: support `volume_path` property ([#5550](https://github.com/databricks/cli/pull/5550)).
* Added an `env:` section to `scripts.<name>` for declaring environment variables that may reference `${bundle.*}`, `${workspace.*}`, and `${var.*}` ([#4179](https://github.com/databricks/cli/issues/4179)).

### Dependency updates

Expand Down
14 changes: 14 additions & 0 deletions acceptance/bundle/run/scripts/env-bad-prefix/databricks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
bundle:
name: script-env-bad-prefix

resources:
jobs:
my_job:
name: my-job

scripts:
bad:
env:
JOB_ID: ${resources.jobs.my_job.id}
UNKNOWN: ${something.else}
content: echo "$JOB_ID $UNKNOWN"
3 changes: 3 additions & 0 deletions acceptance/bundle/run/scripts/env-bad-prefix/out.test.toml

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

16 changes: 16 additions & 0 deletions acceptance/bundle/run/scripts/env-bad-prefix/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

>>> [CLI] bundle validate
Error: ${resources.jobs.my_job.id} cannot be used in scripts.bad.env.JOB_ID; only ${bundle.*}, ${workspace.*}, and ${var.*} are resolved before scripts execute
Comment thread
shreyas-goenka marked this conversation as resolved.
at scripts.bad.env.JOB_ID
in databricks.yml:12:15

Error: ${something.else} cannot be used in scripts.bad.env.UNKNOWN; only ${bundle.*}, ${workspace.*}, and ${var.*} are resolved before scripts execute
at scripts.bad.env.UNKNOWN
in databricks.yml:13:16

Name: script-env-bad-prefix
Target: default

Found 2 errors

Exit code: 1
1 change: 1 addition & 0 deletions acceptance/bundle/run/scripts/env-bad-prefix/script
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
errcode trace $CLI bundle validate

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
errcode trace $CLI bundle validate
musterr trace $CLI bundle validate

5 changes: 5 additions & 0 deletions acceptance/bundle/run/scripts/env-bad-prefix/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Local = true
Cloud = false

[EnvMatrix]
DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"]
21 changes: 21 additions & 0 deletions acceptance/bundle/run/scripts/env-section/databricks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
bundle:
name: script-env-section

variables:
region:
default: us-west-2
database:
default: mydb

scripts:
show_env:
env:
BUNDLE_NAME: ${bundle.name}
REGION: ${var.region}
DATABASE: ${var.database}
MIXED: "region=${var.region};db=${var.database}"
content: |-
echo "bundle=$BUNDLE_NAME"
echo "region=$REGION"
echo "database=$DATABASE"
echo "mixed=$MIXED"
3 changes: 3 additions & 0 deletions acceptance/bundle/run/scripts/env-section/out.test.toml

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

6 changes: 6 additions & 0 deletions acceptance/bundle/run/scripts/env-section/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

>>> [CLI] bundle run show_env
bundle=script-env-section
region=us-west-2
database=mydb
mixed=region=us-west-2;db=mydb
1 change: 1 addition & 0 deletions acceptance/bundle/run/scripts/env-section/script
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
trace $CLI bundle run show_env
5 changes: 5 additions & 0 deletions acceptance/bundle/run/scripts/env-section/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Local = true
Cloud = false

[EnvMatrix]
DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"]
56 changes: 40 additions & 16 deletions acceptance/bundle/run/scripts/no-interpolation/output.txt
Original file line number Diff line number Diff line change
@@ -1,40 +1,64 @@

>>> [CLI] bundle deploy
Error: Found ${FOO} in script one. Interpolation syntax ${...} is not allowed in scripts
Error: Found ${FOO} in script one.content. Interpolation syntax ${...} is not supported in script content
at scripts.one.content
in databricks.yml:6:14

We do not support the ${...} interpolation syntax in scripts because
it's ambiguous whether it's a variable reference or reference to an
environment variable.
The ${...} syntax is not allowed in script content. To use a bundle value,
declare an environment variable in the script's "env:" section and reference it
from "content" with $NAME:

Error: Found ${var.BAR} in script two. Interpolation syntax ${...} is not allowed in scripts
scripts:
one:
env:
MY_VAR: ${var.foo}
content: echo "$MY_VAR"

Error: Found ${var.BAR} in script two.content. Interpolation syntax ${...} is not supported in script content
at scripts.two.content
in databricks.yml:8:14

We do not support the ${...} interpolation syntax in scripts because
it's ambiguous whether it's a variable reference or reference to an
environment variable.
The ${...} syntax is not allowed in script content. To use a bundle value,
declare an environment variable in the script's "env:" section and reference it
from "content" with $NAME:

scripts:
two:
env:
MY_VAR: ${var.foo}
content: echo "$MY_VAR"


Exit code: 1

>>> [CLI] bundle run foo
Error: Found ${FOO} in script one. Interpolation syntax ${...} is not allowed in scripts
Error: Found ${FOO} in script one.content. Interpolation syntax ${...} is not supported in script content
at scripts.one.content
in databricks.yml:6:14

We do not support the ${...} interpolation syntax in scripts because
it's ambiguous whether it's a variable reference or reference to an
environment variable.
The ${...} syntax is not allowed in script content. To use a bundle value,
declare an environment variable in the script's "env:" section and reference it
from "content" with $NAME:

Error: Found ${var.BAR} in script two. Interpolation syntax ${...} is not allowed in scripts
scripts:
one:
env:
MY_VAR: ${var.foo}
content: echo "$MY_VAR"

Error: Found ${var.BAR} in script two.content. Interpolation syntax ${...} is not supported in script content
at scripts.two.content
in databricks.yml:8:14

We do not support the ${...} interpolation syntax in scripts because
it's ambiguous whether it's a variable reference or reference to an
environment variable.
The ${...} syntax is not allowed in script content. To use a bundle value,
declare an environment variable in the script's "env:" section and reference it
from "content" with $NAME:

scripts:
two:
env:
MY_VAR: ${var.foo}
content: echo "$MY_VAR"


Exit code: 1
7 changes: 7 additions & 0 deletions bundle/config/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ import (
type Script struct {
// Content of the script to be executed.
Content string `json:"content"`

// Env is a map of environment variables exported when running the script.
// Values may reference ${bundle.*}, ${workspace.*}, ${var.*} (or
// ${variables.*}); other prefixes are rejected at validation time.
// Use this to pass bundle configuration into a script's shell environment
// without polluting the script content with DABs interpolation syntax.
Env map[string]string `json:"env,omitempty"`
}

type Root struct { //nolint:recvcheck // value receivers for read-only accessors, pointer for mutators
Expand Down
82 changes: 64 additions & 18 deletions bundle/config/validate/scripts.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import (
"context"
"fmt"
"maps"
"regexp"
"slices"
"strings"

"github.com/databricks/cli/bundle"
"github.com/databricks/cli/libs/diag"
"github.com/databricks/cli/libs/dyn"
"github.com/databricks/cli/libs/dyn/dynvar"
)

type validateScripts struct{}
Expand All @@ -22,44 +23,89 @@ func (f *validateScripts) Name() string {
return "validate:scripts"
}

func (f *validateScripts) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
diags := diag.Diagnostics{}
// allowedEnvRefPrefixes are the variable prefixes that may appear in a
// script's "env:" section. These match the prefixes resolved before scripts
// execute (defaultPrefixes in resolve_variable_references.go); "var" is the
// shorthand for "variables".
var allowedEnvRefPrefixes = []string{"bundle", "workspace", "var", "variables"}

re := regexp.MustCompile(`\$\{.*\}`)
func (f *validateScripts) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
var diags diag.Diagnostics

// Sort the scripts to have a deterministic order for the
// generated diagnostics.
scriptKeys := slices.Sorted(maps.Keys(b.Config.Scripts))

for _, k := range scriptKeys {
script := b.Config.Scripts[k]
p := dyn.NewPath(dyn.Key("scripts"), dyn.Key(k), dyn.Key("content"))
contentPath := dyn.NewPath(dyn.Key("scripts"), dyn.Key(k), dyn.Key("content"))

if script.Content == "" {
diags = append(diags, diag.Diagnostic{
Severity: diag.Error,
Summary: fmt.Sprintf("Script %s has no content", k),
Paths: []dyn.Path{p},
Paths: []dyn.Path{contentPath},
})
continue
}

v, err := dyn.GetByPath(b.Config.Value(), p)
if err != nil {
return diags.Extend(diag.FromErr(err))
diags = diags.Extend(validateScriptContent(b, k, script.Content, contentPath))
diags = diags.Extend(validateScriptEnv(b, k, script.Env))
}

return diags
}

// validateScriptContent rejects any ${...} reference in a script's content.
// Content is passed to the shell as-is, so ${...} would be ambiguous with a
// bundle reference; reference a declared env entry with $NAME instead.
func validateScriptContent(b *bundle.Bundle, key, content string, p dyn.Path) diag.Diagnostics {
ref, ok := dynvar.NewRef(dyn.V(content))
if !ok {
return nil
}

first := ref.Matches[0][0]
v, _ := dyn.GetByPath(b.Config.Value(), p)
return diag.Diagnostics{{
Severity: diag.Error,
Summary: fmt.Sprintf("Found %s in script %s.content. Interpolation syntax ${...} is not supported in script content", first, key),
Detail: `The ${...} syntax is not allowed in script content. To use a bundle value,
declare an environment variable in the script's "env:" section and reference it
from "content" with $NAME:

scripts:
` + key + `:
env:
MY_VAR: ${var.foo}
content: echo "$MY_VAR"`,
Locations: v.Locations(),
Paths: []dyn.Path{p},
}}
}

func validateScriptEnv(b *bundle.Bundle, key string, env map[string]string) diag.Diagnostics {
var diags diag.Diagnostics

for _, name := range slices.Sorted(maps.Keys(env)) {
ref, ok := dynvar.NewRef(dyn.V(env[name]))
if !ok {
continue
}

// Check for interpolation syntax
match := re.FindString(script.Content)
if match != "" {
envValuePath := dyn.NewPath(dyn.Key("scripts"), dyn.Key(key), dyn.Key("env"), dyn.Key(name))
v, _ := dyn.GetByPath(b.Config.Value(), envValuePath)

for _, refPath := range ref.References() {
prefix, _, _ := strings.Cut(refPath, ".")
if slices.Contains(allowedEnvRefPrefixes, prefix) {
continue
}
diags = append(diags, diag.Diagnostic{
Severity: diag.Error,
Summary: fmt.Sprintf("Found %s in script %s. Interpolation syntax ${...} is not allowed in scripts", match, k),
Detail: `We do not support the ${...} interpolation syntax in scripts because
it's ambiguous whether it's a variable reference or reference to an
environment variable.`,
Severity: diag.Error,
Summary: fmt.Sprintf("${%s} cannot be used in scripts.%s.env.%s; only ${bundle.*}, ${workspace.*}, and ${var.*} are resolved before scripts execute", refPath, key, name),
Locations: v.Locations(),
Paths: []dyn.Path{p},
Paths: []dyn.Path{envValuePath},
})
}
}
Expand Down
3 changes: 3 additions & 0 deletions bundle/internal/schema/annotations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1971,6 +1971,9 @@ scripts:
"content":
"description": |-
PLACEHOLDER
"env":
"description": |-
PLACEHOLDER

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add a description here so it shows up in tooltips via the jsonschema

sync:
"description": |-
The files and file paths to include or exclude in the bundle.
Expand Down
3 changes: 3 additions & 0 deletions bundle/schema/jsonschema.json

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

13 changes: 10 additions & 3 deletions cmd/bundle/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"slices"

"github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/config"
"github.com/databricks/cli/bundle/env"
"github.com/databricks/cli/bundle/resources"
"github.com/databricks/cli/bundle/run"
Expand Down Expand Up @@ -165,7 +166,7 @@ Example usage:
if len(runArgs) > 0 {
return fmt.Errorf("additional arguments are not supported for scripts. Got: %v. We recommend using environment variables to pass runtime arguments to a script. For example: FOO=bar databricks bundle run my_script", runArgs)
}
return executeScript(b.Config.Scripts[key].Content, cmd, b)
return executeScript(b.Config.Scripts[key], cmd, b)
}

return nil
Expand Down Expand Up @@ -276,8 +277,14 @@ func scriptEnv(cmd *cobra.Command, b *bundle.Bundle) []string {
return out
}

func executeScript(content string, cmd *cobra.Command, b *bundle.Bundle) error {
return execv.Shell(content, b.BundleRootPath, scriptEnv(cmd, b))
func executeScript(script config.Script, cmd *cobra.Command, b *bundle.Bundle) error {
env := scriptEnv(cmd, b)
// Append after the auth/target variables so a script's env: section takes
// precedence on collision (os/exec uses the last value for a duplicate key).
for _, name := range slices.Sorted(maps.Keys(script.Env)) {
env = append(env, name+"="+script.Env[name])
}
Comment on lines +281 to +286

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add an acceptance test for this

return execv.Shell(script.Content, b.BundleRootPath, env)
}

func executeInline(cmd *cobra.Command, args []string, b *bundle.Bundle) error {
Expand Down
Loading