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 @@ -7,6 +7,7 @@
### CLI

### Bundles
* Pass authentication environment variables to Python mutator process ([#4172](https://github.com/databricks/cli/pull/4172))

### Dependency updates

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
bundle:
name: auth_env_test

sync: {paths: []} # don't need to copy files

python:
resources:
- "resources:load_resources"
6 changes: 6 additions & 0 deletions acceptance/bundle/python/auth-env-passthrough/out.test.toml

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

5 changes: 5 additions & 0 deletions acceptance/bundle/python/auth-env-passthrough/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

>>> uv run [UV_ARGS] -q [CLI] bundle validate --output json
{
"job_name": "auth_env_test_job"
}
32 changes: 32 additions & 0 deletions acceptance/bundle/python/auth-env-passthrough/resources.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import os

from databricks.bundles.core import Bundle, Resources


def load_resources(bundle: Bundle) -> Resources:
host = os.environ.get("DATABRICKS_HOST", "NOT_SET")

if host == "NOT_SET":
raise ValueError("DATABRICKS_HOST was not passed to Python subprocess")

if not host.startswith("http://") and not host.startswith("https://"):
raise ValueError(f"DATABRICKS_HOST has invalid format: {host}")

home = os.environ.get("HOME", os.environ.get("USERPROFILE", "NOT_SET"))
if home == "NOT_SET":
raise ValueError("HOME/USERPROFILE was not passed to Python subprocess")

profile = os.environ.get("DATABRICKS_CONFIG_PROFILE")
if profile is not None:
raise ValueError(
f"DATABRICKS_CONFIG_PROFILE should have been removed but was: {profile}. "
"Conflicting auth env vars must be cleaned to prevent auth issues."
)

resources = Resources()
resources.add_job(
resource_name="test_job",
job={"name": "auth_env_test_job"},
)

return resources
10 changes: 10 additions & 0 deletions acceptance/bundle/python/auth-env-passthrough/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
echo "$DATABRICKS_BUNDLES_WHEEL" > "requirements-latest.txt"

# Verify that auth env vars (DATABRICKS_HOST) are correctly passed to the
# Python subprocess via auth.ProcessEnv. The Python code validates that
# both DATABRICKS_HOST and HOME are present in the environment.

trace uv run $UV_ARGS -q $CLI bundle validate --output json | \
jq '{job_name: .resources.jobs.test_job.name}'

rm -fr .databricks __pycache__
19 changes: 19 additions & 0 deletions acceptance/bundle/python/auth-env-passthrough/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# This test verifies that auth env vars from the bundle config are correctly
# passed to the Python subprocess via auth.ProcessEnv, and that conflicting
# auth env vars are REMOVED to prevent auth issues.

Local = true
Cloud = false

Ignore = [
"requirements-latest.txt",
]

[Env]
DATABRICKS_CONFIG_PROFILE = "conflicting_profile"

[EnvMatrix]
UV_ARGS = [
"--with databricks-bundles==0.266.0",
"--with-requirements requirements-latest.txt --no-cache",
]
13 changes: 13 additions & 0 deletions acceptance/bundle/python/workspace-client-auth/databricks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
bundle:
name: workspace_client_auth_test

sync: {paths: []}

python:
mutators:
- "mutators:test_workspace_client"

resources:
jobs:
test_job:
name: "Test Job"
11 changes: 11 additions & 0 deletions acceptance/bundle/python/workspace-client-auth/mutators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from dataclasses import replace
from databricks.bundles.jobs import Job
from databricks.bundles.core import job_mutator, Bundle
from databricks.sdk import WorkspaceClient


@job_mutator
def test_workspace_client(bundle: Bundle, job: Job) -> Job:
w = WorkspaceClient()
user = w.current_user.me()
return replace(job, description=f"Validated by user: {user.user_name}")

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

5 changes: 5 additions & 0 deletions acceptance/bundle/python/workspace-client-auth/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

>>> uv run [UV_ARGS] -q [CLI] bundle validate --output json
{
"description": "Validated by user: [USERNAME]"
}
6 changes: 6 additions & 0 deletions acceptance/bundle/python/workspace-client-auth/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
echo "$DATABRICKS_BUNDLES_WHEEL" > "requirements-latest.txt"

trace uv run $UV_ARGS -q $CLI bundle validate --output json | \
jq '{description: .resources.jobs.test_job.description}'

rm -fr .databricks __pycache__
5 changes: 5 additions & 0 deletions acceptance/bundle/python/workspace-client-auth/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[EnvMatrix]
UV_ARGS = [
"--with databricks-bundles==0.266.0 --with databricks-sdk",
"--with-requirements requirements-latest.txt --with databricks-sdk --no-cache",
]
6 changes: 6 additions & 0 deletions bundle/config/mutator/python/python_mutator.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

"github.com/databricks/cli/bundle/config/mutator/resourcemutator"

"github.com/databricks/cli/libs/auth"
"github.com/databricks/cli/libs/log"
"github.com/databricks/cli/libs/logdiag"

Expand Down Expand Up @@ -104,6 +105,7 @@ type runPythonMutatorOpts struct {
bundleRootPath string
pythonPath string
loadLocations bool
authEnvs []string
}

// getOpts adapts deprecated PyDABs and upcoming Python configuration
Expand Down Expand Up @@ -222,6 +224,8 @@ func (m *pythonMutator) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagno
var result applyPythonOutputResult
mutateDiagsHasError := errors.New("unexpected error")

authEnvs := auth.ProcessEnv(b.WorkspaceClient().Config)

err = b.Config.Mutate(func(leftRoot dyn.Value) (dyn.Value, error) {
pythonPath, err := detectExecutable(ctx, opts.venvPath)
if err != nil {
Expand All @@ -238,6 +242,7 @@ func (m *pythonMutator) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagno
bundleRootPath: b.BundleRootPath,
pythonPath: pythonPath,
loadLocations: opts.loadLocations,
authEnvs: authEnvs,
})
mutateDiags = diags
if diags.HasError() {
Expand Down Expand Up @@ -364,6 +369,7 @@ func (m *pythonMutator) runPythonMutator(ctx context.Context, root dyn.Value, op
process.WithDir(opts.bundleRootPath),
process.WithStderrWriter(stderrWriter),
process.WithStdoutWriter(stdoutWriter),
process.WithEnviron(opts.authEnvs),
)
if processErr != nil {
logger.Debugf(ctx, "python mutator process failed: %s", processErr)
Expand Down
70 changes: 70 additions & 0 deletions bundle/config/mutator/python/python_mutator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,73 @@ or activate the environment before running CLI commands:
assert.Equal(t, expected, out)
}

func TestPythonMutator_authEnvVarsPassedToSubprocess(t *testing.T) {
withFakeVEnv(t, ".venv")

b := loadYaml("databricks.yml", `
experimental:
python:
venv_path: .venv
resources: ["resources:load_resources"]
resources:
jobs:
job0:
name: job_0
workspace:
host: https://acme.databricks.com`)

ctx := withProcessStubWithEnvCheck(
t,
[]string{
interpreterPath(".venv"),
"-m",
"databricks.bundles.build",
"--phase",
"load_resources",
},
`{
"experimental": {
"python": {
"resources": ["resources:load_resources"],
"venv_path": ".venv"
}
},
"resources": {
"jobs": {
"job0": {
name: "job_0"
}
}
},
"workspace": {
"host": "https://acme.databricks.com"
}
}`,
"",
"",
func(cmd *exec.Cmd) {
foundHost := false
for _, envVar := range cmd.Env {
if envVar == "DATABRICKS_HOST=https://acme.databricks.com" {
foundHost = true
break
}
}
assert.True(t, foundHost, "DATABRICKS_HOST should be passed to subprocess")
},
)

mutator := PythonMutator(PythonMutatorPhaseLoadResources)
diags := bundle.Apply(ctx, b, mutator)

assert.NoError(t, diags.Error())
}

func withProcessStub(t *testing.T, args []string, output, diagnostics, locations string) context.Context {
return withProcessStubWithEnvCheck(t, args, output, diagnostics, locations, nil)
}

func withProcessStubWithEnvCheck(t *testing.T, args []string, output, diagnostics, locations string, envCheck func(*exec.Cmd)) context.Context {
ctx := context.Background()
ctx, stub := process.WithStub(ctx)

Expand Down Expand Up @@ -535,6 +601,10 @@ func withProcessStub(t *testing.T, args []string, output, diagnostics, locations
err = os.WriteFile(diagnosticsPath, []byte(diagnostics), 0o600)
require.NoError(t, err)

if envCheck != nil {
envCheck(actual)
}

return nil
})

Expand Down
7 changes: 7 additions & 0 deletions libs/process/opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ func WithEnvs(envs map[string]string) execOption {
}
}

func WithEnviron(envs []string) execOption {
return func(ctx context.Context, c *exec.Cmd) error {
c.Env = envs
return nil
}
}

func WithDir(dir string) execOption {
return func(_ context.Context, c *exec.Cmd) error {
c.Dir = dir
Expand Down