From 36a4dc173c557a4e7c134a216bf973bc172379e1 Mon Sep 17 00:00:00 2001 From: rugpanov Date: Fri, 12 Jun 2026 19:23:13 +0200 Subject: [PATCH] Pass DATABRICKS_CLI_PATH and resolve the platform-specific CLI binary *Why*: * On Windows, `databricks bundle` deploys failed with "databricks CLI not found" because the SDK/Terraform provider could not locate the bundled CLI. * The forwarded path was extensionless (`bin/databricks`), but the bundled Windows binary is `databricks.exe`; the Go SDK/Terraform do a literal file lookup and do not auto-append `.exe` the way Windows CreateProcess does. * The legacy project.json persisted the bundled CLI path (`databricksPath`), which `fromJSON` preferred over the freshly resolved one. That snapshot is version-pinned and, on Windows, extensionless, so it overrode the corrected path during legacy-to-bundle migration. *What:* * Forward the resolved CLI path to subprocesses via the DATABRICKS_CLI_PATH env var so they don't fall back to a PATH search. * Make CliWrapper.cliPath return the platform-specific binary name (`databricks.exe` on win32, `databricks` elsewhere). * In AuthProvider.fromJSON, always use the freshly resolved bundled CLI path and ignore the persisted `databricksPath`, since it is an extension-managed, version-pinned value that is stale after any upgrade. *Verification:* * Added unit tests for the platform-specific binary name, for toEnv() exposing DATABRICKS_CLI_PATH, and for fromJSON ignoring a stale persisted databricksPath; suite passes in the VS Code test host (14 relevant tests green). * Built the win32-x64 VSIX and confirmed the bundled extension contains both the env var and the `.exe` path, with `bin/databricks.exe` packaged. Co-authored-by: Isaac --- .../src/cli/CliWrapper.test.ts | 34 ++++++++- .../databricks-vscode/src/cli/CliWrapper.ts | 10 ++- .../configuration/auth/AuthProvider.test.ts | 69 +++++++++++++++++++ .../src/configuration/auth/AuthProvider.ts | 13 +++- 4 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 packages/databricks-vscode/src/configuration/auth/AuthProvider.test.ts diff --git a/packages/databricks-vscode/src/cli/CliWrapper.test.ts b/packages/databricks-vscode/src/cli/CliWrapper.test.ts index 46fdce5fb..810e177f2 100644 --- a/packages/databricks-vscode/src/cli/CliWrapper.test.ts +++ b/packages/databricks-vscode/src/cli/CliWrapper.test.ts @@ -21,7 +21,12 @@ import {ChildProcess, ChildProcessWithoutNullStreams} from "child_process"; import {Readable} from "stream"; const execFile = promisify(execFileCb); -const cliPath = path.join(__dirname, "../../bin/databricks"); +// Mirror CliWrapper.cliPath: the bundled binary is `databricks.exe` on Windows. +const cliPath = path.join( + __dirname, + "../../bin/" + + (process.platform === "win32" ? "databricks.exe" : "databricks") +); // eslint-disable-next-line @typescript-eslint/no-var-requires const extensionVersion = require("../../package.json").version; @@ -53,6 +58,33 @@ describe(__filename, function () { assert.ok(result.stdout.indexOf("databricks") > 0); }); + it("should resolve the platform-specific CLI binary name", () => { + const cli = createCliWrapper(); + const originalPlatform = process.platform; + const setPlatform = (platform: NodeJS.Platform) => + Object.defineProperty(process, "platform", {value: platform}); + try { + // On Windows the bundled binary is `databricks.exe`. The `.exe` is + // required because cliPath is forwarded to the SDK/Terraform via + // DATABRICKS_CLI_PATH, which does a literal (no auto-`.exe`) lookup. + setPlatform("win32"); + assert.ok( + cli.cliPath.endsWith(path.join("bin", "databricks.exe")), + `expected win32 cliPath to end with bin/databricks.exe, got ${cli.cliPath}` + ); + + for (const platform of ["linux", "darwin"] as NodeJS.Platform[]) { + setPlatform(platform); + assert.ok( + cli.cliPath.endsWith(path.join("bin", "databricks")), + `expected ${platform} cliPath to end with bin/databricks, got ${cli.cliPath}` + ); + } + } finally { + setPlatform(originalPlatform); + } + }); + let mocks: any[] = []; afterEach(() => { mocks.forEach((mock) => reset(mock)); diff --git a/packages/databricks-vscode/src/cli/CliWrapper.ts b/packages/databricks-vscode/src/cli/CliWrapper.ts index 776c79dd0..17ca9170b 100644 --- a/packages/databricks-vscode/src/cli/CliWrapper.ts +++ b/packages/databricks-vscode/src/cli/CliWrapper.ts @@ -298,7 +298,15 @@ export class CliWrapper { } get cliPath(): string { - return this.extensionContext.asAbsolutePath("./bin/databricks"); + // The bundled binary is named `databricks.exe` on Windows. We must + // include the extension here: while spawning the CLI ourselves works + // without it (Windows' CreateProcess auto-appends `.exe`), this path is + // also forwarded to the Databricks Go SDK / Terraform provider via the + // DATABRICKS_CLI_PATH env var, and they do a literal file lookup that + // fails on an extensionless path with "databricks CLI not found". + const binName = + process.platform === "win32" ? "databricks.exe" : "databricks"; + return this.extensionContext.asAbsolutePath(`./bin/${binName}`); } getLoggingArguments(): string[] { diff --git a/packages/databricks-vscode/src/configuration/auth/AuthProvider.test.ts b/packages/databricks-vscode/src/configuration/auth/AuthProvider.test.ts new file mode 100644 index 000000000..21374ed74 --- /dev/null +++ b/packages/databricks-vscode/src/configuration/auth/AuthProvider.test.ts @@ -0,0 +1,69 @@ +import * as assert from "assert"; +import {instance, mock, when} from "ts-mockito"; +import {AuthProvider, DatabricksCliAuthProvider} from "./AuthProvider"; +import {CliWrapper} from "../../cli/CliWrapper"; + +describe(__filename, () => { + describe("DatabricksCliAuthProvider.toEnv", () => { + const host = new URL("https://test.cloud.databricks.com"); + const cliPath = "/path/to/bin/databricks"; + + function createProvider(profile?: string, workspaceId?: string) { + return new DatabricksCliAuthProvider( + host, + cliPath, + instance(mock(CliWrapper)), + profile, + workspaceId + ); + } + + it("should expose DATABRICKS_CLI_PATH so the SDK/Terraform provider can locate the bundled CLI", () => { + const env = createProvider("dev").toEnv(); + + assert.equal(env["DATABRICKS_CLI_PATH"], cliPath); + assert.equal(env["DATABRICKS_HOST"], host.toString()); + assert.equal(env["DATABRICKS_AUTH_TYPE"], "databricks-cli"); + assert.equal(env["DATABRICKS_CONFIG_PROFILE"], "dev"); + }); + + it("should include DATABRICKS_CLI_PATH even without a profile or workspace id", () => { + const env = createProvider().toEnv(); + + assert.equal(env["DATABRICKS_CLI_PATH"], cliPath); + assert.ok(!("DATABRICKS_CONFIG_PROFILE" in env)); + assert.ok(!("DATABRICKS_WORKSPACE_ID" in env)); + }); + }); + + describe("AuthProvider.fromJSON", () => { + it("should ignore a persisted databricksPath and use the freshly resolved bundled CLI path", () => { + // Simulate an upgraded install: the new extension resolves a + // versioned, platform-correct path, while project.json still holds + // an old, extensionless snapshot from the previous version. + const freshPath = + "/ext/databricks.databricks-2.12.0/bin/databricks.exe"; + const stalePath = + "/ext/databricks.databricks-2.11.0/bin/databricks"; + + const cliMock = mock(CliWrapper); + when(cliMock.cliPath).thenReturn(freshPath); + + const provider = AuthProvider.fromJSON( + { + host: "https://test.cloud.databricks.com", + authType: "databricks-cli", + databricksPath: stalePath, + profile: "dev", + }, + instance(cliMock) + ); + + assert.ok(provider instanceof DatabricksCliAuthProvider); + assert.equal( + (provider as DatabricksCliAuthProvider).cliPath, + freshPath + ); + }); + }); +}); diff --git a/packages/databricks-vscode/src/configuration/auth/AuthProvider.ts b/packages/databricks-vscode/src/configuration/auth/AuthProvider.ts index 2aec9bf66..350e769af 100644 --- a/packages/databricks-vscode/src/configuration/auth/AuthProvider.ts +++ b/packages/databricks-vscode/src/configuration/auth/AuthProvider.ts @@ -163,7 +163,14 @@ export abstract class AuthProvider { case "databricks-cli": return new DatabricksCliAuthProvider( host, - json.databricksPath ?? cli.cliPath, + // Always use the freshly resolved bundled CLI path and + // ignore any persisted `databricksPath`. That field is a + // snapshot of a previous install's bundled binary: it is + // version-pinned (e.g. .../databricks-2.11.0/bin/...) and, + // on Windows, was stored without the `.exe` suffix, so a + // persisted value is stale/wrong and must not win over the + // path computed by the current extension. + cli.cliPath, cli, json.profile, json.workspaceId @@ -355,6 +362,10 @@ export class DatabricksCliAuthProvider extends AuthProvider { const env: Record = { DATABRICKS_HOST: this.host.toString(), DATABRICKS_AUTH_TYPE: "databricks-cli", + // Point the SDK/Terraform provider at the bundled CLI so they don't + // fall back to searching PATH (and fail with "databricks CLI not + // found") in subprocesses that don't inherit our resolved path. + DATABRICKS_CLI_PATH: this.cliPath, }; if (this.profile) { env["DATABRICKS_CONFIG_PROFILE"] = this.profile;