11import {
2+ ExtensionContext ,
23 QuickPickItem ,
34 QuickPickItemKind ,
45 Disposable ,
78 commands ,
89 TerminalLocation ,
910} from "vscode" ;
11+ import fs from "node:fs/promises" ;
12+ import path from "path" ;
1013import { ConnectionManager } from "../configuration/ConnectionManager" ;
1114import { ConfigModel } from "../configuration/models/ConfigModel" ;
1215import { BundleFileSet } from "./BundleFileSet" ;
@@ -15,41 +18,35 @@ import {Loggers} from "../logger";
1518import { CachedValue } from "../locking/CachedValue" ;
1619import { CustomWhenContext } from "../vscode-objs/CustomWhenContext" ;
1720import { CliWrapper } from "../cli/CliWrapper" ;
18- import { LoginWizard } from "../configuration/LoginWizard" ;
21+ import { LoginWizard , saveNewProfile } from "../configuration/LoginWizard" ;
1922import { 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
2231export 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