Skip to content

Commit b2e7e6f

Browse files
Download spark_python_task workspace files in bundle generate job (#5799)
## Changes `bundle generate job` only downloaded notebook tasks. Files referenced by `spark_python_task` were left as absolute `/Workspace/...` paths in the generated config, so the source file was never downloaded and the config wasn't portable. It now downloads workspace files referenced by `spark_python_task` and rewrites them to a relative path, reusing the same `markFileForDownload` helper already used for pipeline libraries. Git-sourced files (`source: GIT`) and cloud URIs (`dbfs:/`, `s3:/`, `adls:/`, `gcs:/`) are left untouched. ## Why Reported by a user: generating a job with a notebook task and a `spark_python_task` downloaded only the notebook. The `spark_python_task` branch was simply never handled in `MarkTaskForDownload`. ## Tests - Unit tests in `bundle/generate/downloader_test.go` covering the download+rewrite path and the skipped cases (cloud URI, `source: GIT`). - New acceptance test `acceptance/bundle/generate/spark_python_task_job` exercising the full CLI: a workspace-file task is downloaded and rewritten, a `dbfs:/` cloud-URI task is preserved. Identical output on both `terraform` and `direct` engines. _This PR was written by Isaac, an AI coding agent._
1 parent ccdc2f3 commit b2e7e6f

10 files changed

Lines changed: 148 additions & 3 deletions

File tree

NEXT_CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
### Bundles
1212

13+
* `bundle generate job` now downloads workspace files referenced by `spark_python_task`, rewriting them to a relative path like it already does for notebooks. Git-sourced files and cloud URIs are left untouched ([#5799](https://github.com/databricks/cli/pull/5799)).
14+
1315
### Dependency updates
1416

1517
### API Changes
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
bundle:
2+
name: spark_python_task_job
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
resources:
2+
jobs:
3+
out:
4+
name: sparkpythonjob
5+
tasks:
6+
- task_key: workspace_file_task
7+
spark_python_task:
8+
python_file: outjob.py
9+
- task_key: cloud_uri_task
10+
spark_python_task:
11+
python_file: dbfs:/FileStore/job.py

acceptance/bundle/generate/spark_python_task_job/out.test.toml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
print("Hello, World!")
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
File successfully saved to outjob.py
2+
Job configuration successfully saved to out.job.yml
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
$CLI bundle generate job --existing-job-id 1234 --config-dir . --key out --force --source-dir .
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
[[Server]]
2+
Pattern = "GET /api/2.2/jobs/get"
3+
Response.Body = '''
4+
{
5+
"job_id": 11223344,
6+
"settings": {
7+
"name": "sparkpythonjob",
8+
"tasks": [
9+
{
10+
"task_key": "workspace_file_task",
11+
"spark_python_task": {
12+
"python_file": "/Workspace/Users/tester@databricks.com/outjob.py"
13+
}
14+
},
15+
{
16+
"task_key": "cloud_uri_task",
17+
"spark_python_task": {
18+
"python_file": "dbfs:/FileStore/job.py"
19+
}
20+
}
21+
]
22+
}
23+
}
24+
'''
25+
26+
[[Server]]
27+
Pattern = "GET /api/2.0/workspace/get-status"
28+
Response.Body = '''
29+
{
30+
"path": "/Workspace/Users/tester@databricks.com/outjob.py",
31+
"object_type": "FILE",
32+
"language": "PYTHON",
33+
"repos_export_format": "SOURCE"
34+
}
35+
'''
36+
37+
[[Server]]
38+
Pattern = "GET /api/2.0/workspace/export"
39+
Response.Body = '''
40+
print("Hello, World!")
41+
'''

bundle/generate/downloader.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,24 @@ type Downloader struct {
3737
}
3838

3939
func (n *Downloader) MarkTaskForDownload(ctx context.Context, task *jobs.Task) error {
40-
if task.NotebookTask == nil {
41-
return nil
40+
if task.NotebookTask != nil {
41+
return n.markNotebookForDownload(ctx, &task.NotebookTask.NotebookPath)
4242
}
4343

44-
return n.markNotebookForDownload(ctx, &task.NotebookTask.NotebookPath)
44+
if task.SparkPythonTask != nil && isWorkspaceFileTask(task.SparkPythonTask.Source, task.SparkPythonTask.PythonFile) {
45+
return n.markFileForDownload(ctx, &task.SparkPythonTask.PythonFile)
46+
}
47+
48+
return nil
49+
}
50+
51+
// isWorkspaceFileTask reports whether a task file lives in the Databricks
52+
// workspace and should be downloaded. Files sourced from Git (source: GIT) are
53+
// left to the Git repository, and python_file also accepts cloud URIs
54+
// (dbfs:/, s3:/, adls:/, gcs:/) which are not workspace paths; per the Jobs API,
55+
// workspace files are absolute and begin with "/".
56+
func isWorkspaceFileTask(source jobs.Source, filePath string) bool {
57+
return source != jobs.SourceGit && strings.HasPrefix(filePath, "/")
4558
}
4659

4760
func (n *Downloader) MarkPipelineLibraryForDownload(ctx context.Context, lib *pipelines.PipelineLibrary) error {
@@ -218,6 +231,9 @@ func (n *Downloader) MarkTasksForDownload(ctx context.Context, tasks []jobs.Task
218231
if task.NotebookTask != nil {
219232
paths = append(paths, task.NotebookTask.NotebookPath)
220233
}
234+
if task.SparkPythonTask != nil && isWorkspaceFileTask(task.SparkPythonTask.Source, task.SparkPythonTask.PythonFile) {
235+
paths = append(paths, task.SparkPythonTask.PythonFile)
236+
}
221237
}
222238
if len(paths) > 0 {
223239
n.basePath = commonDirPrefix(paths)

bundle/generate/downloader_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,72 @@ func TestDownloader_MarkTasksForDownload_SingleNotebook(t *testing.T) {
295295
assert.Len(t, downloader.files, 1)
296296
}
297297

298+
func TestDownloader_MarkTasksForDownload_SparkPythonFile(t *testing.T) {
299+
ctx := t.Context()
300+
m := mocks.NewMockWorkspaceClient(t)
301+
302+
dir := "base/dir"
303+
sourceDir := filepath.Join(dir, "source")
304+
configDir := filepath.Join(dir, "config")
305+
downloader := NewDownloader(m.WorkspaceClient, sourceDir, configDir)
306+
307+
pythonFile := "/Users/user/project/etl/job.py"
308+
m.GetMockWorkspaceAPI().EXPECT().GetStatusByPath(ctx, pythonFile).Return(&workspace.ObjectInfo{
309+
Path: pythonFile,
310+
}, nil)
311+
312+
tasks := []jobs.Task{
313+
{
314+
TaskKey: "spark_python_task",
315+
SparkPythonTask: &jobs.SparkPythonTask{
316+
PythonFile: pythonFile,
317+
},
318+
},
319+
}
320+
321+
err := downloader.MarkTasksForDownload(ctx, tasks)
322+
require.NoError(t, err)
323+
324+
assert.Equal(t, "../source/job.py", tasks[0].SparkPythonTask.PythonFile)
325+
require.Len(t, downloader.files, 1)
326+
f := downloader.files[filepath.Join(sourceDir, "job.py")]
327+
assert.Equal(t, pythonFile, f.path)
328+
assert.Equal(t, workspace.ExportFormatSource, f.format)
329+
}
330+
331+
func TestDownloader_MarkTasksForDownload_SparkPythonFileSkipped(t *testing.T) {
332+
ctx := t.Context()
333+
// Cloud URIs and Git-sourced files are not workspace paths, so no
334+
// get-status/download requests should be made for them.
335+
w := newTestWorkspaceClient(t, func(w http.ResponseWriter, r *http.Request) {
336+
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
337+
})
338+
339+
downloader := NewDownloader(w, "source", "config")
340+
341+
tasks := []jobs.Task{
342+
{
343+
TaskKey: "cloud_uri_task",
344+
SparkPythonTask: &jobs.SparkPythonTask{
345+
PythonFile: "dbfs:/FileStore/job.py",
346+
},
347+
},
348+
{
349+
TaskKey: "git_task",
350+
SparkPythonTask: &jobs.SparkPythonTask{
351+
PythonFile: "etl/job.py",
352+
Source: jobs.SourceGit,
353+
},
354+
},
355+
}
356+
357+
err := downloader.MarkTasksForDownload(ctx, tasks)
358+
require.NoError(t, err)
359+
assert.Empty(t, downloader.files)
360+
assert.Equal(t, "dbfs:/FileStore/job.py", tasks[0].SparkPythonTask.PythonFile)
361+
assert.Equal(t, "etl/job.py", tasks[1].SparkPythonTask.PythonFile)
362+
}
363+
298364
func TestDownloader_MarkTasksForDownload_NoNotebooks(t *testing.T) {
299365
ctx := t.Context()
300366
w := newTestWorkspaceClient(t, func(w http.ResponseWriter, r *http.Request) {

0 commit comments

Comments
 (0)