Skip to content

Commit ba2327d

Browse files
authored
Migrate from project.json to databricks.yml (#1013)
Migrate legacy project.json to databricks.yml bundle config So far the migration-template lives in the extension dir. It also doesn't have the right amount of comments and explainations, we should follow up on that before GA
1 parent 2cb93aa commit ba2327d

File tree

11 files changed

+536
-82
lines changed

11 files changed

+536
-82
lines changed

packages/databricks-vscode/package.json

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -305,13 +305,23 @@
305305
"viewsWelcome": [
306306
{
307307
"view": "configurationView",
308-
"contents": "There are multiple Databricks projects in the workspace, please chose which one to open:\n[Open Existing Project](command:databricks.bundle.openSubProject)\n",
309-
"when": "workspaceFolderCount > 0 && databricks.context.subProjectsAvailable"
308+
"contents": "Configure your Databricks workspace:\n[Configure Databricks](command:databricks.bundle.startManualMigration)",
309+
"when": "workspaceFolderCount > 0 && databricks.context.initialized && databricks.context.pendingManualMigration"
310+
},
311+
{
312+
"view": "configurationView",
313+
"contents": "There are multiple Databricks projects in the workspace, please chose which one to open:\n[Open Existing Project](command:databricks.bundle.openSubProject)",
314+
"when": "workspaceFolderCount > 0 && databricks.context.initialized && databricks.context.subProjectsAvailable"
310315
},
311316
{
312317
"view": "configurationView",
313318
"contents": "Current workspace has no Databricks configuration at the root level, do you want to initialize a new Databricks project?\n[Initialize New Project](command:databricks.bundle.initNewProject)\n[Show Quickstart](command:databricks.quickstart.open)\nTo learn more about how to use Databricks with VS Code [read our docs](https://docs.databricks.com/dev-tools/vscode-ext.html).",
314-
"when": "workspaceFolderCount > 0"
319+
"when": "workspaceFolderCount > 0 && databricks.context.initialized && !databricks.context.pendingManualMigration"
320+
},
321+
{
322+
"view": "configurationView",
323+
"contents": "Initializing...",
324+
"when": "workspaceFolderCount > 0 && !databricks.context.initialized"
315325
},
316326
{
317327
"view": "configurationView",
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"properties": {
3+
"project_name": {
4+
"type": "string",
5+
"description": "Project name",
6+
"order": 1
7+
},
8+
"compute_id": {
9+
"type": "string",
10+
"description": "Compute id",
11+
"order": 2
12+
},
13+
"root_path": {
14+
"type": "string",
15+
"description": "Root path",
16+
"order": 3
17+
}
18+
}
19+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# This is a Databricks asset bundle definition for {{.project_name}}.
2+
# The Databricks extension requires databricks.yml configuration file.
3+
# See https://docs.databricks.com/dev-tools/bundles/index.html for documentation.
4+
5+
bundle:
6+
name: {{.project_name}}
7+
8+
targets:
9+
dev:
10+
mode: development
11+
default: true
12+
{{- if .compute_id}}
13+
compute_id: {{.compute_id}}
14+
{{- end}}
15+
workspace:
16+
host: {{workspace_host}}
17+
{{- if .root_path}}
18+
root_path: {{.root_path}}
19+
{{- end}}
20+
21+
## Optionally, there could be 'staging' or 'prod' targets here.
22+
#
23+
# prod:
24+
# workspace:
25+
# host: {{workspace_host}}

packages/databricks-vscode/src/bundle/BundleProjectManager.ts

Lines changed: 158 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
ExtensionContext,
23
QuickPickItem,
34
QuickPickItemKind,
45
Disposable,
@@ -7,6 +8,8 @@ import {
78
commands,
89
TerminalLocation,
910
} from "vscode";
11+
import fs from "node:fs/promises";
12+
import path from "path";
1013
import {ConnectionManager} from "../configuration/ConnectionManager";
1114
import {ConfigModel} from "../configuration/models/ConfigModel";
1215
import {BundleFileSet} from "./BundleFileSet";
@@ -15,41 +18,35 @@ import {Loggers} from "../logger";
1518
import {CachedValue} from "../locking/CachedValue";
1619
import {CustomWhenContext} from "../vscode-objs/CustomWhenContext";
1720
import {CliWrapper} from "../cli/CliWrapper";
18-
import {LoginWizard} from "../configuration/LoginWizard";
21+
import {LoginWizard, saveNewProfile} from "../configuration/LoginWizard";
1922
import {Mutex} from "../locking";
20-
import {AuthProvider} from "../configuration/auth/AuthProvider";
23+
import {
24+
AuthProvider,
25+
ProfileAuthProvider,
26+
} from "../configuration/auth/AuthProvider";
27+
import {ProjectConfigFile} from "../file-managers/ProjectConfigFile";
28+
import {randomUUID} from "crypto";
29+
import {onError} from "../utils/onErrorDecorator";
2130

2231
export class BundleProjectManager {
2332
private logger = logging.NamedLogger.getOrCreate(Loggers.Extension);
2433
private disposables: Disposable[] = [];
2534

26-
private _isBundleProject = new CachedValue<boolean>(async () => {
35+
private isBundleProjectCache = new CachedValue<boolean>(async () => {
2736
const rootBundleFile = await this.bundleFileSet.getRootFile();
2837
return rootBundleFile !== undefined;
2938
});
3039

31-
public onDidChangeStatus = this._isBundleProject.onDidChange;
32-
33-
private _isLegacyProject = new CachedValue<boolean>(async () => {
34-
// TODO
35-
return false;
36-
});
37-
38-
private _subProjects = new CachedValue<{absolute: Uri; relative: Uri}[]>(
39-
async () => {
40-
const projects = await this.bundleFileSet.getSubProjects();
41-
this.logger.debug(
42-
`Detected ${projects.length} sub folders with bundle projects`
43-
);
44-
this.customWhenContext.setSubProjectsAvailable(projects.length > 0);
45-
return projects;
46-
}
47-
);
40+
public onDidChangeStatus = this.isBundleProjectCache.onDidChange;
4841

4942
private projectServicesReady = false;
5043
private projectServicesMutex = new Mutex();
5144

45+
private subProjects?: {relative: Uri; absolute: Uri}[];
46+
private legacyProjectConfig?: ProjectConfigFile;
47+
5248
constructor(
49+
private context: ExtensionContext,
5350
private cli: CliWrapper,
5451
private customWhenContext: CustomWhenContext,
5552
private connectionManager: ConnectionManager,
@@ -60,15 +57,15 @@ export class BundleProjectManager {
6057
this.disposables.push(
6158
this.bundleFileSet.bundleDataCache.onDidChange(async () => {
6259
try {
63-
await this._isBundleProject.refresh();
60+
await this.isBundleProjectCache.refresh();
6461
} catch (error) {
6562
this.logger.error(
66-
"Failed to refresh isBundleProject var",
63+
"Failed to refresh isBundleProjectCache",
6764
error
6865
);
6966
}
7067
}),
71-
this._isBundleProject.onDidChange(async () => {
68+
this.isBundleProjectCache.onDidChange(async () => {
7269
try {
7370
await this.configureBundleProject();
7471
} catch (error) {
@@ -87,7 +84,7 @@ export class BundleProjectManager {
8784
}
8885

8986
public async isBundleProject(): Promise<boolean> {
90-
return await this._isBundleProject.value;
87+
return await this.isBundleProjectCache.value;
9188
}
9289

9390
public async configureWorkspace(): Promise<void> {
@@ -96,17 +93,15 @@ export class BundleProjectManager {
9693
return;
9794
}
9895

99-
// The cached value updates subProjectsAvailabe context.
100-
// We have a configurationView that shows "open project" button if the context value is true.
101-
await this._subProjects.refresh();
102-
103-
const isLegacyProject = await this._isLegacyProject.value;
104-
if (isLegacyProject) {
105-
this.logger.debug(
106-
"Detected a legacy project.json, starting automatic migration"
107-
);
108-
await this.migrateProjectJsonToBundle();
109-
}
96+
await Promise.all([
97+
// This method updates subProjectsAvailabe context.
98+
// We have a configurationView that shows "openSubProjects" button if the context value is true.
99+
this.detectSubProjects(),
100+
// This method will try to automatically create bundle config if there's existing valid project.json config.
101+
// In the case project.json auth doesn't work, it sets pendingManualMigration context to enable
102+
// configurationView with the configureManualMigration button.
103+
this.detectLegacyProjectConfig(),
104+
]);
110105
}
111106

112107
private async configureBundleProject() {
@@ -138,12 +133,20 @@ export class BundleProjectManager {
138133
// TODO
139134
}
140135

136+
private async detectSubProjects() {
137+
this.subProjects = await this.bundleFileSet.getSubProjects();
138+
this.logger.debug(
139+
`Detected ${this.subProjects?.length} sub folders with bundle projects`
140+
);
141+
this.customWhenContext.setSubProjectsAvailable(
142+
this.subProjects?.length > 0
143+
);
144+
}
145+
141146
public async openSubProjects() {
142-
const projects = await this._subProjects.value;
143-
if (projects.length === 0) {
144-
return;
147+
if (this.subProjects && this.subProjects.length > 0) {
148+
return this.promptToOpenSubProjects(this.subProjects);
145149
}
146-
return this.promptToOpenSubProjects(projects);
147150
}
148151

149152
private async promptToOpenSubProjects(
@@ -174,8 +177,117 @@ export class BundleProjectManager {
174177
await commands.executeCommand("vscode.openFolder", item.uri);
175178
}
176179

177-
private async migrateProjectJsonToBundle() {
178-
// TODO
180+
private async detectLegacyProjectConfig() {
181+
this.legacyProjectConfig = await this.loadLegacyProjectConfig();
182+
if (!this.legacyProjectConfig) {
183+
return;
184+
}
185+
this.logger.debug(
186+
"Detected a legacy project.json, starting automatic migration"
187+
);
188+
try {
189+
await this.startAutomaticMigration(this.legacyProjectConfig);
190+
} catch (error) {
191+
this.customWhenContext.setPendingManualMigration(true);
192+
const message =
193+
"Failed to perform automatic migration to Databricks Asset Bundles.";
194+
this.logger.error(message, error);
195+
const errorMessage = (error as Error)?.message ?? "Unknown Error";
196+
window.showErrorMessage(`${message} ${errorMessage}`);
197+
}
198+
}
199+
200+
private async loadLegacyProjectConfig(): Promise<
201+
ProjectConfigFile | undefined
202+
> {
203+
try {
204+
return await ProjectConfigFile.load(
205+
this.workspaceUri.fsPath,
206+
this.cli.cliPath
207+
);
208+
} catch (error) {
209+
this.logger.error("Failed to load legacy project config:", error);
210+
return undefined;
211+
}
212+
}
213+
214+
private async startAutomaticMigration(
215+
legacyProjectConfig: ProjectConfigFile
216+
) {
217+
let authProvider = legacyProjectConfig.authProvider;
218+
if (!(await authProvider.check())) {
219+
this.logger.debug(
220+
"Legacy project auth was not successful, showing 'configure' welcome screen"
221+
);
222+
this.customWhenContext.setPendingManualMigration(true);
223+
return;
224+
}
225+
if (!(authProvider instanceof ProfileAuthProvider)) {
226+
const rnd = randomUUID().slice(0, 8);
227+
const profileName = `${authProvider.authType}-${rnd}`;
228+
this.logger.debug(
229+
"Creating new profile before bundle migration",
230+
profileName
231+
);
232+
authProvider = await saveNewProfile(profileName, authProvider);
233+
}
234+
await this.migrateProjectJsonToBundle(
235+
legacyProjectConfig,
236+
authProvider as ProfileAuthProvider
237+
);
238+
}
239+
240+
@onError({
241+
popup: {
242+
prefix: "Failed to migrate the project to Databricks Asset Bundles",
243+
},
244+
})
245+
public async startManualMigration() {
246+
if (!this.legacyProjectConfig) {
247+
throw new Error("Can't migrate without project configuration");
248+
}
249+
const authProvider = await LoginWizard.run(this.cli, this.configModel);
250+
if (
251+
authProvider instanceof ProfileAuthProvider &&
252+
(await authProvider.check())
253+
) {
254+
return this.migrateProjectJsonToBundle(
255+
this.legacyProjectConfig!,
256+
authProvider
257+
);
258+
} else {
259+
this.logger.debug("Incorrect auth for the project.json migration");
260+
}
261+
}
262+
263+
private async migrateProjectJsonToBundle(
264+
legacyProjectConfig: ProjectConfigFile,
265+
authProvider: ProfileAuthProvider
266+
) {
267+
const configVars = {
268+
/* eslint-disable @typescript-eslint/naming-convention */
269+
project_name: path.basename(this.workspaceUri.fsPath),
270+
compute_id: legacyProjectConfig.clusterId,
271+
root_path: legacyProjectConfig.workspacePath?.path,
272+
/* eslint-enable @typescript-eslint/naming-convention */
273+
};
274+
this.logger.debug("Starting bundle migration, config:", configVars);
275+
const configFilePath = path.join(
276+
this.workspaceUri.fsPath,
277+
".databricks",
278+
"migration-config.json"
279+
);
280+
await fs.writeFile(configFilePath, JSON.stringify(configVars, null, 4));
281+
const templateDirPath = this.context.asAbsolutePath(
282+
path.join("resources", "migration-template")
283+
);
284+
await this.cli.bundleInit(
285+
templateDirPath,
286+
this.workspaceUri.fsPath,
287+
configFilePath,
288+
authProvider
289+
);
290+
this.logger.debug("Successfully finished bundle migration");
179291
}
180292

181293
public async initNewProject() {
@@ -191,11 +303,11 @@ export class BundleProjectManager {
191303
this.logger.debug("No parent folder provided");
192304
return;
193305
}
194-
await this.bundleInitInTerminal(parentFolder, authProvider.toEnv());
306+
await this.bundleInitInTerminal(parentFolder, authProvider);
195307
this.logger.debug(
196308
"Finished bundle init wizard, detecting projects to initialize or open"
197309
);
198-
await this._isBundleProject.refresh();
310+
await this.isBundleProjectCache.refresh();
199311
const projects = await this.bundleFileSet.getSubProjects(parentFolder);
200312
if (projects.length > 0) {
201313
this.logger.debug(
@@ -244,7 +356,7 @@ export class BundleProjectManager {
244356
const items: AuthSelectionItem[] = [
245357
{
246358
label: "Use current auth",
247-
detail: `Type: ${authProvider.authType}; Host: ${authProvider.host.hostname}`,
359+
detail: `Host: ${authProvider.host.hostname}`,
248360
approved: true,
249361
},
250362
{
@@ -267,13 +379,13 @@ export class BundleProjectManager {
267379

268380
private async bundleInitInTerminal(
269381
parentFolder: Uri,
270-
env: Record<string, string>
382+
authProvider: AuthProvider
271383
) {
272384
const terminal = window.createTerminal({
273385
name: "Databricks Project Init",
274386
isTransient: true,
275387
location: TerminalLocation.Editor,
276-
env: {...env, ...this.cli.getLogginEnvVars()},
388+
env: this.cli.getBundleInitEnvVars(authProvider),
277389
});
278390
const args = [
279391
"bundle",

0 commit comments

Comments
 (0)