Skip to content

Commit bc0d488

Browse files
authored
Add validate runs for pipelines (#1436)
## Changes Validate action is available as a separate button on the pipeline item in the resource explorer: <img width="748" alt="Screenshot 2024-11-01 at 10 02 27" src="https://github.com/user-attachments/assets/5dd2a147-f074-4443-9018-b101e53439c6"> Haven't found a super suitable icon for this action (in the workspace we don't use any icons for the validate action). Currently using the "beaker" icon that's generally used in tests, and reached out to the designers for help. ## Tests Yes
1 parent 74ed930 commit bc0d488

18 files changed

+529
-182
lines changed

packages/databricks-vscode/package.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,13 @@
224224
"enablement": "databricks.context.activated && databricks.context.bundle.isTargetSet && databricks.context.bundle.deploymentState == idle",
225225
"category": "Databricks"
226226
},
227+
{
228+
"command": "databricks.bundle.deployAndValidate",
229+
"icon": "$(beaker)",
230+
"title": "Deploy the bundle and validate the pipeline",
231+
"enablement": "databricks.context.activated && databricks.context.bundle.isTargetSet && databricks.context.bundle.deploymentState == idle",
232+
"category": "Databricks"
233+
},
227234
{
228235
"command": "databricks.bundle.cancelRun",
229236
"title": "Cancel run",
@@ -544,6 +551,11 @@
544551
"when": "view == dabsResourceExplorerView && viewItem =~ /^databricks.bundle.*.runnable.*$/ && databricks.context.bundle.deploymentState == idle",
545552
"group": "inline@0"
546553
},
554+
{
555+
"command": "databricks.bundle.deployAndValidate",
556+
"when": "view == dabsResourceExplorerView && viewItem =~ /^databricks.bundle.resource=pipelines.runnable.*$/ && databricks.context.bundle.deploymentState == idle",
557+
"group": "inline@0"
558+
},
547559
{
548560
"command": "databricks.bundle.cancelRun",
549561
"when": "view == dabsResourceExplorerView && viewItem =~ /^databricks.bundle.*.cancellable.*$/ && databricks.context.bundle.deploymentState == idle",
@@ -645,6 +657,10 @@
645657
"command": "databricks.bundle.deployAndRun",
646658
"when": "false"
647659
},
660+
{
661+
"command": "databricks.bundle.deployAndValidate",
662+
"when": "false"
663+
},
648664
{
649665
"command": "databricks.bundle.cancelRun",
650666
"when": "false"

packages/databricks-vscode/src/bundle/models/BundleRemoteStateModel.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,10 @@ export class BundleRemoteStateModel extends BaseModelWithStateCache<BundleRemote
122122
);
123123
}
124124

125-
public async getRunCommand(resourceKey: string) {
125+
public async getRunCommand(
126+
resourceKey: string,
127+
additionalArgs: string[] = []
128+
) {
126129
if (this.target === undefined) {
127130
throw new Error("Target is undefined");
128131
}
@@ -135,7 +138,8 @@ export class BundleRemoteStateModel extends BaseModelWithStateCache<BundleRemote
135138
this.authProvider,
136139
resourceKey,
137140
this.workspaceFolder,
138-
this.workspaceConfigs.databrickscfgLocation
141+
this.workspaceConfigs.databrickscfgLocation,
142+
additionalArgs
139143
);
140144
}
141145

packages/databricks-vscode/src/bundle/run/BundleRunStatusManager.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ export class BundleRunStatusManager implements Disposable {
6060

6161
async run(
6262
resourceKey: string,
63-
resourceType: ResourceKey<BundleRemoteState>
63+
resourceType: ResourceKey<BundleRemoteState>,
64+
additionalArgs: string[] = []
6465
) {
6566
const target = this.configModel.target;
6667
const authProvider = this.configModel.authProvider;
@@ -99,7 +100,8 @@ export class BundleRunStatusManager implements Disposable {
99100
try {
100101
const result = await this.bundleRunTerminalManager.run(
101102
resourceKey,
102-
(data) => remoteRunStatus.parseId(data)
103+
(data) => remoteRunStatus.parseId(data),
104+
additionalArgs
103105
);
104106
if (result.cancelled) {
105107
await remoteRunStatus.cancel();

packages/databricks-vscode/src/bundle/run/BundleRunTerminalManager.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ export class BundleRunTerminalManager implements Disposable {
2525

2626
async run(
2727
resourceKey: string,
28-
onDidUpdate?: (data: string) => void
28+
onDidUpdate?: (data: string) => void,
29+
additionalArgs: string[] = []
2930
): Promise<{cancelled: boolean; exitCode?: number | null}> {
3031
const target = this.bundleRemoteStateModel.target;
3132
if (target === undefined) {
@@ -72,8 +73,10 @@ export class BundleRunTerminalManager implements Disposable {
7273
onCancellationEvent.dispose();
7374
}, this.disposables);
7475

75-
const cmd =
76-
await this.bundleRemoteStateModel.getRunCommand(resourceKey);
76+
const cmd = await this.bundleRemoteStateModel.getRunCommand(
77+
resourceKey,
78+
additionalArgs
79+
);
7780

7881
// spawn a new process with the latest command, in the same terminal.
7982
terminal.pty.spawn(cmd);

packages/databricks-vscode/src/cli/CliWrapper.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -690,7 +690,8 @@ export class CliWrapper {
690690
authProvider: AuthProvider,
691691
resourceKey: string,
692692
workspaceFolder: Uri,
693-
configfilePath?: string
693+
configfilePath?: string,
694+
additionalArgs: string[] = []
694695
): Promise<{
695696
cmd: string;
696697
args: string[];
@@ -711,7 +712,14 @@ export class CliWrapper {
711712

712713
return {
713714
cmd: this.cliPath,
714-
args: ["bundle", "run", "--target", target, resourceKey],
715+
args: [
716+
"bundle",
717+
"run",
718+
"--target",
719+
target,
720+
resourceKey,
721+
...additionalArgs,
722+
],
715723
options: {
716724
cwd: workspaceFolder.fsPath,
717725
env,

packages/databricks-vscode/src/extension.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,11 @@ export async function activate(
680680
bundleCommands.deployAndRun,
681681
bundleCommands
682682
),
683+
telemetry.registerCommand(
684+
"databricks.bundle.deployAndValidate",
685+
bundleCommands.deployAndValidate,
686+
bundleCommands
687+
),
683688
telemetry.registerCommand(
684689
"databricks.bundle.cancelRun",
685690
bundleCommands.cancelRun,

packages/databricks-vscode/src/telemetry/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export type ManualLoginSource =
3030
| "command"
3131
| "api";
3232
export type BundleRunResourceType = "pipelines" | "jobs";
33+
export type BundleRunType = "run" | "validate" | "partial-refresh";
3334

3435
/** Documentation about all of the properties and metrics of the event. */
3536
type EventDescription<T> = {[K in keyof T]?: {comment?: string}};
@@ -129,6 +130,7 @@ export class EventTypes {
129130
success: boolean;
130131
cancelled?: boolean;
131132
resourceType?: BundleRunResourceType;
133+
runType?: BundleRunType;
132134
} & DurationMeasurement
133135
> = {
134136
comment: "Execute a bundle resource",
Lines changed: 20 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,25 @@
11
import assert from "node:assert";
22
import {
33
dismissNotifications,
4-
getUniqueResourceName,
54
getViewSection,
5+
waitForDeployment,
66
waitForLogin,
77
waitForTreeItems,
88
} from "./utils/commonUtils.ts";
99
import {CustomTreeSection, Workbench} from "wdio-vscode-service";
10+
import {createProjectWithJob} from "./utils/dabsFixtures.ts";
1011
import {
11-
getBasicBundleConfig,
12-
getSimpleJobsResource,
13-
writeRootBundleConfig,
14-
} from "./utils/dabsFixtures.ts";
15-
import path from "node:path";
16-
import fs from "fs/promises";
17-
import {BundleSchema} from "../../bundle/types.ts";
18-
import {getJobViewItem} from "./utils/dabsExplorerUtils.ts";
19-
import {fileURLToPath} from "url";
20-
21-
/* eslint-disable @typescript-eslint/naming-convention */
22-
const __filename = fileURLToPath(import.meta.url);
23-
const __dirname = path.dirname(__filename);
24-
/* eslint-enable @typescript-eslint/naming-convention */
12+
getResourceViewItem,
13+
waitForRunStatus,
14+
} from "./utils/dabsExplorerUtils.ts";
2515

2616
describe("Deploy and run job", async function () {
2717
let workbench: Workbench;
2818
let resourceExplorerView: CustomTreeSection;
29-
let vscodeWorkspaceRoot: string;
3019
let jobName: string;
31-
let clusterId: string;
3220

3321
this.timeout(3 * 60 * 1000);
3422

35-
async function createProjectWithJob() {
36-
/**
37-
* process.env.WORKSPACE_PATH (cwd)
38-
* ├── databricks.yml
39-
* └── src
40-
* └── notebook.ipynb
41-
*/
42-
43-
const projectName = getUniqueResourceName("deploy_and_run_job");
44-
const notebookTaskName = getUniqueResourceName("notebook_task");
45-
/* eslint-disable @typescript-eslint/naming-convention */
46-
const jobDef = getSimpleJobsResource({
47-
tasks: [
48-
{
49-
task_key: notebookTaskName,
50-
notebook_task: {
51-
notebook_path: "src/notebook.ipynb",
52-
},
53-
existing_cluster_id: clusterId,
54-
},
55-
],
56-
});
57-
jobName = jobDef.name!;
58-
59-
const schemaDef: BundleSchema = getBasicBundleConfig({
60-
bundle: {
61-
name: projectName,
62-
deployment: {},
63-
},
64-
targets: {
65-
dev_test: {
66-
resources: {
67-
jobs: {
68-
vscode_integration_test: jobDef,
69-
},
70-
},
71-
},
72-
},
73-
});
74-
/* eslint-enable @typescript-eslint/naming-convention */
75-
76-
await writeRootBundleConfig(schemaDef, vscodeWorkspaceRoot);
77-
78-
await fs.mkdir(path.join(vscodeWorkspaceRoot, "src"), {
79-
recursive: true,
80-
});
81-
await fs.copyFile(
82-
path.join(__dirname, "resources", "spark_select_1.ipynb"),
83-
path.join(vscodeWorkspaceRoot, "src", "notebook.ipynb")
84-
);
85-
}
86-
8723
before(async function () {
8824
assert(
8925
process.env.TEST_DEFAULT_CLUSTER_ID,
@@ -98,10 +34,11 @@ describe("Deploy and run job", async function () {
9834
"DATABRICKS_HOST env var doesn't exist"
9935
);
10036

101-
clusterId = process.env.TEST_DEFAULT_CLUSTER_ID;
10237
workbench = await browser.getWorkbench();
103-
vscodeWorkspaceRoot = process.env.WORKSPACE_PATH;
104-
await createProjectWithJob();
38+
jobName = await createProjectWithJob(
39+
process.env.WORKSPACE_PATH,
40+
process.env.TEST_DEFAULT_CLUSTER_ID
41+
);
10542
await dismissNotifications();
10643
});
10744

@@ -127,7 +64,11 @@ describe("Deploy and run job", async function () {
12764
await outputView.selectChannel("Databricks Bundle Logs");
12865
await outputView.clearText();
12966

130-
const jobItem = await getJobViewItem(resourceExplorerView, jobName);
67+
const jobItem = await getResourceViewItem(
68+
resourceExplorerView,
69+
"Workflows",
70+
jobName
71+
);
13172
assert(jobItem, `Job ${jobName} not found in resource explorer`);
13273

13374
const deployAndRunButton = await jobItem.getActionButton(
@@ -136,72 +77,13 @@ describe("Deploy and run job", async function () {
13677
assert(deployAndRunButton, "Deploy and run button not found");
13778
await deployAndRunButton.elem.click();
13879

139-
console.log("Waiting for deployment to finish");
140-
// Wait for the deployment to finish
141-
await browser.waitUntil(
142-
async () => {
143-
try {
144-
await browser.executeWorkbench(async (vscode) => {
145-
await vscode.commands.executeCommand(
146-
"workbench.panel.output.focus"
147-
);
148-
});
149-
const outputView = await workbench
150-
.getBottomBar()
151-
.openOutputView();
152-
153-
if (
154-
(await outputView.getCurrentChannel()) !==
155-
"Databricks Bundle Logs"
156-
) {
157-
await outputView.selectChannel(
158-
"Databricks Bundle Logs"
159-
);
160-
}
161-
162-
const logs = (await outputView.getText()).join("");
163-
console.log(logs);
164-
return (
165-
logs.includes("Bundle deployed successfully") &&
166-
logs.includes("Bundle configuration refreshed")
167-
);
168-
} catch (e) {
169-
return false;
170-
}
171-
},
172-
{
173-
timeout: 60_000,
174-
interval: 1_000,
175-
timeoutMsg:
176-
"Can't find 'Bundle deployed successfully' message in output channel",
177-
}
178-
);
179-
180-
console.log("Waiting for run to finish");
181-
// Wait for status to reach success
182-
await browser.waitUntil(
183-
async () => {
184-
const jobItem = await getJobViewItem(
185-
resourceExplorerView,
186-
jobName
187-
);
188-
if (jobItem === undefined) {
189-
return false;
190-
}
191-
192-
const runStatusItem = await jobItem.findChildItem("Run Status");
193-
if (runStatusItem === undefined) {
194-
return false;
195-
}
80+
await waitForDeployment();
19681

197-
return (await runStatusItem.getDescription()) === "Success";
198-
},
199-
{
200-
timeout: 120_000,
201-
interval: 2_000,
202-
timeoutMsg:
203-
"The run status didn't reach success within 120 seconds",
204-
}
82+
await waitForRunStatus(
83+
resourceExplorerView,
84+
"Workflows",
85+
jobName,
86+
"Success"
20587
);
20688
});
20789
});

0 commit comments

Comments
 (0)