Skip to content

Commit

Permalink
Bundle cli dependencies (#1147)
Browse files Browse the repository at this point in the history
Bundle terraform binaries with the extension and expose them with env
vars to the CLI and the terminal

Depends on databricks/cli#1294
  • Loading branch information
ilia-db authored Apr 8, 2024
1 parent f2fb7a4 commit 5b4f896
Show file tree
Hide file tree
Showing 12 changed files with 288 additions and 15 deletions.
1 change: 1 addition & 0 deletions packages/databricks-vscode/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ bin/**
src/test/e2e/workspace/
extension/
.pytest_cache/
.build/

# Telemetry file, automatically generated by packages/databricks-vscode/scripts/generateTelemetry.ts
telemetry.json
1 change: 1 addition & 0 deletions packages/databricks-vscode/.vscodeignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ coverage/
logs/
extension/
**/*.vsix
.build/
4 changes: 2 additions & 2 deletions packages/databricks-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -776,7 +776,7 @@
"useYarn": false
},
"cli": {
"version": "0.216.0"
"version": "0.217.0"
},
"scripts": {
"vscode:prepublish": "rm -rf out && yarn run package:compile && yarn run package:wrappers:write && yarn run package:jupyter-init-script:write && yarn run package:copy-webview-toolkit && yarn run generate-telemetry",
Expand All @@ -787,7 +787,7 @@
"package:darwin:arm64": "./scripts/package-vsix.sh darwin-arm64",
"package:win32:x64": "./scripts/package-vsix.sh win32-x64",
"package:win32:arm64": "./scripts/package-vsix.sh win32-arm64",
"package:all": "yarn run package:linux:x64 && yarn run package:linux:arm64 && yarn run package:darwin:x64 && yarn run package:darwin:arm64 && yarn run package:win32:x64 && yarn run package:win32:arm64",
"package:all": "rm -rf ./.build/ && yarn run package:linux:x64 && yarn run package:linux:arm64 && yarn run package:darwin:x64 && yarn run package:darwin:arm64 && yarn run package:win32:x64 && yarn run package:win32:arm64",
"package:cli:fetch": "bash ./scripts/fetch-databricks-cli.sh ${CLI_ARCH:-}",
"package:cli:link": "rm -f ./bin/databricks && mkdir -p bin && ln -s ../../../../cli/cli bin/databricks",
"package:wrappers:write": "ts-node ./scripts/writeIpynbWrapper.ts -s ./resources/python/notebook.workflow-wrapper.py -o ./resources/python/generated/notebook.workflow-wrapper.json",
Expand Down
11 changes: 8 additions & 3 deletions packages/databricks-vscode/scripts/fetch-databricks-cli.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/bash
#!/bin/bash
set -ex

CLI_VERSION=$(cat package.json | jq -r .cli.version)
Expand All @@ -8,6 +8,11 @@ if [ -z "$CLI_ARCH" ]; then
CLI_ARCH="$(uname -s | awk '{print tolower($0)}')_$(uname -m)"
fi

CLI_DEST=$2
if [ -z "$CLI_DEST" ]; then
CLI_DEST=./bin
fi

CLI_DIR=$(mktemp -d -t databricks-XXXXXXXXXX)
pushd $CLI_DIR
gh release download v${CLI_VERSION} --pattern "databricks_cli_${CLI_VERSION}_${CLI_ARCH}.zip" --repo databricks/cli
Expand All @@ -16,8 +21,8 @@ rm databricks_*_$CLI_ARCH.zip
ls

popd
mkdir -p bin
cd ./bin
mkdir -p $CLI_DEST
cd $CLI_DEST
rm -rf databricks
mv $CLI_DIR/databricks* .
rm -rf $CLI_DIR
16 changes: 15 additions & 1 deletion packages/databricks-vscode/scripts/package-vsix.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,23 @@ case $ARCH in
;;
esac

# Download databricks cli to the .build directory with the correct arch for the environment that runs this script (not the target arch of the vsix)
# This CLI is used to request metadata about CLIs terraform dependencies.
if [ ! -d ./.build ]; then
mkdir ./.build
BUILD_PLATFORM_ARCH="$(uname -s | awk '{print tolower($0)}')_$(uname -m)"
./scripts/fetch-databricks-cli.sh $BUILD_PLATFORM_ARCH ./.build
fi

rm -rf bin
./scripts/fetch-databricks-cli.sh $CLI_ARCH
yarn ts-node ./scripts/set_arch_in_package.ts $VSXI_ARCH -f package.json --cliArch $CLI_ARCH -V $VSXI_ARCH -c $(git rev-parse --short HEAD)
yarn ts-node ./scripts/setArchInPackage.ts $VSXI_ARCH -f package.json --cliArch $CLI_ARCH -V $VSXI_ARCH -c $(git rev-parse --short HEAD)

# Don't bundle terraform for win32-arm64 as they don't support it yet: https://github.com/hashicorp/terraform/issues/32719
if [ $ARCH != "win32-arm64" ]; then
yarn ts-node ./scripts/setupCLIDependencies.ts --cli ./.build/databricks --binDir ./bin --package ./package.json --arch $CLI_ARCH
fi

yarn run prettier package.json --write
TAG="release-v$(cat package.json | jq -r .version)" yarn run package -t $VSXI_ARCH

Expand Down
135 changes: 135 additions & 0 deletions packages/databricks-vscode/scripts/setupCLIDependencies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import {mkdirp} from "fs-extra";
import assert from "node:assert";
import {spawnSync} from "node:child_process";
import {cp, readFile, writeFile} from "node:fs/promises";
import {tmpdir} from "node:os";
import path from "node:path";
import yargs from "yargs";
import type {
TerraformMetadata,
TerraformMetadataFromCli,
} from "../src/utils/terraformUtils";

async function main() {
const argv = await yargs
.option("cli", {
description: "Path to the Databricks CLI",
type: "string",
requiresArg: true,
})
.option("binDir", {
description: "Path to the bin directory",
type: "string",
requiresArg: true,
})
.option("arch", {
description: "Architecture of databricks cli.",
type: "string",
requiresArg: true,
})
.option("package", {
description: "path/to/package.json",
type: "string",
requiresArg: true,
}).argv;

const res = spawn(argv.cli!, [
"bundle",
"debug",
"terraform",
"--output",
"json",
]);
const dependencies = JSON.parse(res.stdout.toString());
const terraform = dependencies.terraform as TerraformMetadataFromCli;
assert(terraform, "cli must return terraform dependencies");
assert(terraform.version, "cli must return terraform version");
assert(terraform.providerHost, "cli must return provider host");
assert(terraform.providerSource, "cli must return provider source");
assert(terraform.providerVersion, "cli must return provider version");

const tempDir = path.join(tmpdir(), `terraform_${Date.now()}`);
const depsDir = path.join(argv.binDir!, "dependencies");
await mkdirp(tempDir);
await mkdirp(depsDir);

// Download terraform bin for the selected arch
const arch = argv.arch!;
const terraformZip = `terraform_${terraform.version}_${arch}.zip`;
const terraformUrl = `https://releases.hashicorp.com/terraform/${terraform.version}/${terraformZip}`;
spawn("curl", ["-sLO", terraformUrl], {cwd: tempDir});
// Check sha of the archive
const shasumsFile = `terraform_${terraform.version}_SHA256SUMS`;
const shasumsUrl = `https://releases.hashicorp.com/terraform/${terraform.version}/${shasumsFile}`;
spawn("curl", ["-sLO", shasumsUrl], {cwd: tempDir});
const shasumRes = spawn(
"shasum",
["--algorithm", "256", "--check", shasumsFile],
{cwd: tempDir}
);
assert(
shasumRes.output.toString().includes(`${terraformZip}: OK`),
"sha256sum check failed"
);
spawn("unzip", ["-q", terraformZip], {cwd: tempDir});
const fileExt = arch.includes("windows") ? ".exe" : "";
const terraformBinRelPath = path.join(depsDir, `terraform${fileExt}`);
await cp(`${tempDir}/terraform${fileExt}`, terraformBinRelPath);
// Set the path to the terraform bin, the extension will use it to setup the environment variables
const execRelPath = terraformBinRelPath;

// Download databricks provider archive for the selected arch
const providerZip = `terraform-provider-databricks_${terraform.providerVersion}_${arch}.zip`;
spawn(
"gh",
[
"release",
"download",
`v${terraform.providerVersion}`,
"--pattern",
providerZip,
"--repo",
"databricks/terraform-provider-databricks",
],
{cwd: tempDir}
);
const providersMirrorRelPath = path.join(depsDir, "providers");
const databricksProviderDir = path.join(
providersMirrorRelPath,
terraform.providerHost,
terraform.providerSource
);
await mkdirp(databricksProviderDir);
await cp(
path.join(tempDir, providerZip),
path.join(databricksProviderDir, providerZip)
);
// Set the path to the providers mirror dir, the extension will use it
// to create the terraform CLI config at runtime.
const terraformCliConfigRelPath = path.join(depsDir, "config.tfrc");

// Save the info about all dependencies to the package.json
const terraformMetadata: TerraformMetadata = {
...terraform,
execRelPath,
providersMirrorRelPath,
terraformCliConfigRelPath,
};
const rawData = await readFile(argv.package!, {encoding: "utf-8"});
const jsonData = JSON.parse(rawData);
jsonData["terraformMetadata"] = terraformMetadata;
await writeFile(argv.package!, JSON.stringify(jsonData, null, 4), {
encoding: "utf-8",
});
}

function spawn(command: string, args: string[], options: any = {}) {
const child = spawnSync(command, args, options);
if (child.error) {
throw child.error;
} else {
return child;
}
}

main();
26 changes: 21 additions & 5 deletions packages/databricks-vscode/src/cli/CliWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,10 @@ export class CliWrapper {
try {
res = await execFile(cmd.command, cmd.args, {
env: {
...EnvVarGenerators.getEnvVarsForCli(configfilePath),
...EnvVarGenerators.getEnvVarsForCli(
this.extensionContext,
configfilePath
),
...EnvVarGenerators.getProxyEnvVars(),
},
});
Expand Down Expand Up @@ -295,7 +298,10 @@ export class CliWrapper {
const {stdout, stderr} = await execFile(cmd[0], cmd.slice(1), {
cwd: workspaceFolder.fsPath,
env: {
...EnvVarGenerators.getEnvVarsForCli(configfilePath),
...EnvVarGenerators.getEnvVarsForCli(
this.extensionContext,
configfilePath
),
...EnvVarGenerators.getProxyEnvVars(),
...authProvider.toEnv(),
...this.getLogginEnvVars(),
Expand Down Expand Up @@ -340,7 +346,10 @@ export class CliWrapper {
const {stdout, stderr} = await execFile(cmd[0], cmd.slice(1), {
cwd: workspaceFolder.fsPath,
env: {
...EnvVarGenerators.getEnvVarsForCli(configfilePath),
...EnvVarGenerators.getEnvVarsForCli(
this.extensionContext,
configfilePath
),
...EnvVarGenerators.getProxyEnvVars(),
...authProvider.toEnv(),
...this.getLogginEnvVars(),
Expand Down Expand Up @@ -369,6 +378,7 @@ export class CliWrapper {
getBundleInitEnvVars(authProvider: AuthProvider) {
return removeUndefinedKeys({
...EnvVarGenerators.getEnvVarsForCli(
this.extensionContext,
workspaceConfigs.databrickscfgLocation
),
...EnvVarGenerators.getProxyEnvVars(),
Expand Down Expand Up @@ -425,7 +435,10 @@ export class CliWrapper {

// Add python executable to PATH
const executable = await pythonExtension.getPythonExecutable();
const cliEnvVars = EnvVarGenerators.getEnvVarsForCli(configfilePath);
const cliEnvVars = EnvVarGenerators.getEnvVarsForCli(
this.extensionContext,
configfilePath
);
let shellPath = cliEnvVars.PATH;
if (executable) {
shellPath = `${path.dirname(executable)}${
Expand Down Expand Up @@ -491,7 +504,10 @@ export class CliWrapper {
options: SpawnOptionsWithoutStdio;
} {
const env: Record<string, string> = removeUndefinedKeys({
...EnvVarGenerators.getEnvVarsForCli(configfilePath),
...EnvVarGenerators.getEnvVarsForCli(
this.extensionContext,
configfilePath
),
...EnvVarGenerators.getProxyEnvVars(),
...authProvider.toEnv(),
...this.getLogginEnvVars(),
Expand Down
32 changes: 31 additions & 1 deletion packages/databricks-vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ import {PublicApi} from "@databricks/databricks-vscode-types";
import {LoggerManager, Loggers} from "./logger";
import {logging} from "@databricks/databricks-sdk";
import {workspaceConfigs} from "./vscode-objs/WorkspaceConfigs";
import {FileUtils, PackageJsonUtils, UtilsCommands} from "./utils";
import {
FileUtils,
PackageJsonUtils,
TerraformUtils,
UtilsCommands,
} from "./utils";
import {ConfigureAutocomplete} from "./language/ConfigureAutocomplete";
import {WorkspaceFsCommands, WorkspaceFsDataProvider} from "./workspace-fs";
import {CustomWhenContext} from "./vscode-objs/CustomWhenContext";
Expand Down Expand Up @@ -63,6 +68,10 @@ import {TreeItemDecorationProvider} from "./ui/bundle-resource-explorer/Decorati
import {BundleInitWizard} from "./bundle/BundleInitWizard";
import {DatabricksDebugConfigurationProvider} from "./run/DatabricksDebugConfigurationProvider";
import {isIntegrationTest} from "./utils/developmentUtils";
import {getCLIDependenciesEnvVars} from "./utils/envVarGenerators";

// eslint-disable-next-line @typescript-eslint/no-var-requires
const packageJson = require("../package.json");

const customWhenContext = new CustomWhenContext();

Expand Down Expand Up @@ -170,6 +179,27 @@ export async function activate(
`${path.delimiter}${context.asAbsolutePath("./bin")}`
);

// We always use bundled terraform and databricks provider.
// Updating environment collection means that the variables will be set in all terminals.
// If users use different CLI version in their terminal it will only pick the variables if
// the dependency versions (that we set together with bin and config paths) match the internal versions of the CLI.
const cliDeps = getCLIDependenciesEnvVars(context);
for (const [key, value] of Object.entries(cliDeps)) {
logging.NamedLogger.getOrCreate(Loggers.Extension).debug(
`Setting env var ${key}=${value}`
);
context.environmentVariableCollection.replace(key, value);
}
TerraformUtils.updateTerraformCliConfig(
context,
packageJson.terraformMetadata
).catch((e) => {
logging.NamedLogger.getOrCreate(Loggers.Extension).error(
"Failed to update terraform cli config",
e
);
});

logging.NamedLogger.getOrCreate(Loggers.Extension).debug("Metadata", {
metadata: packageMetadata,
});
Expand Down
32 changes: 29 additions & 3 deletions packages/databricks-vscode/src/utils/envVarGenerators.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import {Loggers} from "../logger";
import {readFile} from "fs/promises";
import {Uri} from "vscode";
import {ExtensionContext, Uri} from "vscode";
import {logging, Headers} from "@databricks/databricks-sdk";
import {ConnectionManager} from "../configuration/ConnectionManager";
import {ConfigModel} from "../configuration/models/ConfigModel";
import {TerraformMetadata} from "./terraformUtils";

// eslint-disable-next-line @typescript-eslint/no-var-requires
const extensionVersion = require("../../package.json").version;
const packageJson = require("../../package.json");

const extensionVersion = packageJson.version;
const terraformMetadata = packageJson.terraformMetadata as TerraformMetadata;

//Get env variables from user's .env file
export async function getUserEnvVars(userEnvPath: Uri) {
Expand Down Expand Up @@ -132,7 +136,10 @@ export function getProxyEnvVars() {
};
}

export function getEnvVarsForCli(configfilePath?: string) {
export function getEnvVarsForCli(
extensionContext: ExtensionContext,
configfilePath?: string
) {
/* eslint-disable @typescript-eslint/naming-convention */
return {
HOME: process.env.HOME,
Expand All @@ -142,6 +149,25 @@ export function getEnvVarsForCli(configfilePath?: string) {
DATABRICKS_OUTPUT_FORMAT: "json",
DATABRICKS_CLI_UPSTREAM: "databricks-vscode",
DATABRICKS_CLI_UPSTREAM_VERSION: extensionVersion,
...getCLIDependenciesEnvVars(extensionContext),
};
/* eslint-enable @typescript-eslint/naming-convention */
}

export function getCLIDependenciesEnvVars(extensionContext: ExtensionContext) {
if (!terraformMetadata) {
return {};
}
/* eslint-disable @typescript-eslint/naming-convention */
return {
DATABRICKS_TF_VERSION: terraformMetadata.version,
DATABRICKS_TF_EXEC_PATH: extensionContext.asAbsolutePath(
terraformMetadata.execRelPath
),
DATABRICKS_TF_PROVIDER_VERSION: terraformMetadata.providerVersion,
DATABRICKS_TF_CLI_CONFIG_FILE: extensionContext.asAbsolutePath(
terraformMetadata.terraformCliConfigRelPath
),
};
/* eslint-enable @typescript-eslint/naming-convention */
}
Expand Down
Loading

0 comments on commit 5b4f896

Please sign in to comment.