Skip to content

Commit 2533aec

Browse files
Add UI to configure PAT auth (#1033)
## Changes <!-- Summary of your changes that are easy to understand --> ## Tests <!-- How is this tested? -->
1 parent 9f16b58 commit 2533aec

File tree

6 files changed

+146
-18
lines changed

6 files changed

+146
-18
lines changed

packages/databricks-vscode/package.json

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -387,11 +387,6 @@
387387
}
388388
],
389389
"view/item/context": [
390-
{
391-
"command": "databricks.utils.openExternal",
392-
"when": "viewItem =~ /^databricks.*\\.(has-url).*$/",
393-
"group": "inline@1"
394-
},
395390
{
396391
"command": "databricks.utils.openExternal",
397392
"when": "viewItem =~ /^databricks.*\\.(has-url).*$/",

packages/databricks-vscode/src/configuration/LoginWizard.ts

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@ import {
1111
MultiStepInput,
1212
ValidationMessageType,
1313
} from "../ui/MultiStepInputWizard";
14-
import {CliWrapper} from "../cli/CliWrapper";
14+
import {CliWrapper, ConfigEntry} from "../cli/CliWrapper";
1515
import {workspaceConfigs} from "../vscode-objs/WorkspaceConfigs";
1616
import {
1717
AuthProvider,
1818
AuthType,
1919
AzureCliAuthProvider,
2020
DatabricksCliAuthProvider,
21+
PersonalAccessTokenAuthProvider,
2122
ProfileAuthProvider,
2223
} from "./auth/AuthProvider";
2324
import {UrlUtils} from "../utils";
@@ -45,6 +46,13 @@ interface State {
4546
export class LoginWizard {
4647
private state = {} as Partial<State>;
4748
private readonly title = "Configure Databricks Workspace";
49+
private _profiles: Array<ConfigEntry> = [];
50+
async getProfiles() {
51+
if (this._profiles.length === 0) {
52+
this._profiles = await listProfiles(this.cliWrapper);
53+
}
54+
return this._profiles;
55+
}
4856
constructor(
4957
private readonly cliWrapper: CliWrapper,
5058
private readonly configModel: ConfigModel
@@ -64,9 +72,8 @@ export class LoginWizard {
6472
});
6573
}
6674

67-
const profiles = await listProfiles(this.cliWrapper);
6875
items.push(
69-
...profiles.map((profile) => {
76+
...(await this.getProfiles()).map((profile) => {
7077
return {
7178
label: profile.host!.toString(),
7279
detail: `Profile: ${profile.name}`,
@@ -113,7 +120,10 @@ export class LoginWizard {
113120
input: MultiStepInput
114121
): Promise<InputStep | void> {
115122
const items: Array<AuthTypeQuickPickItem> = [];
116-
123+
items.push({
124+
label: "Create new Databricks CLI profile",
125+
kind: QuickPickItemKind.Separator,
126+
});
117127
for (const authMethod of authMethodsForHostname(this.state.host!)) {
118128
switch (authMethod) {
119129
case "azure-cli":
@@ -131,10 +141,9 @@ export class LoginWizard {
131141
authType: "databricks-cli",
132142
});
133143
break;
134-
135144
case "profile":
136145
{
137-
const profiles = (await listProfiles(this.cliWrapper))
146+
const profiles = (await this.getProfiles())
138147
.filter((profile) => {
139148
return (
140149
profile.host?.hostname ===
@@ -159,6 +168,11 @@ export class LoginWizard {
159168
};
160169
});
161170

171+
items.push({
172+
label: "Personal Access Token",
173+
detail: "Create a new profile and authenticate using the 'databricks' command line tool",
174+
authType: "pat",
175+
});
162176
if (profiles.length !== 0) {
163177
items.push(
164178
{
@@ -235,7 +249,7 @@ export class LoginWizard {
235249
let initialValue = this.configModel.target ?? "";
236250

237251
// If the initialValue profile already exists, then create a unique name.
238-
const profiles = await listProfiles(this.cliWrapper);
252+
const profiles = await this.getProfiles();
239253
if (profiles.find((profile) => profile.name === initialValue)) {
240254
const suffix = randomUUID().slice(0, 8);
241255
initialValue = `${this.configModel.target}-${suffix}`;
@@ -254,7 +268,6 @@ export class LoginWizard {
254268
type: "error",
255269
};
256270
}
257-
const profiles = await listProfiles(this.cliWrapper);
258271
if (profiles.find((profile) => profile.name === value)) {
259272
return {
260273
message: `Profile ${value} already exists`,
@@ -269,7 +282,10 @@ export class LoginWizard {
269282
return;
270283
}
271284

272-
let authProvider: AzureCliAuthProvider | DatabricksCliAuthProvider;
285+
let authProvider:
286+
| AzureCliAuthProvider
287+
| DatabricksCliAuthProvider
288+
| PersonalAccessTokenAuthProvider;
273289
switch (pick.authType) {
274290
case "azure-cli":
275291
authProvider = new AzureCliAuthProvider(this.state.host!);
@@ -282,6 +298,20 @@ export class LoginWizard {
282298
);
283299
break;
284300

301+
case "pat":
302+
{
303+
const token = await collectTokenForPatAuth(input, 4, 4);
304+
if (token === undefined) {
305+
// Token can never be undefined unless the users cancels the whole process.
306+
// Therefore, we can safely return here.
307+
return;
308+
}
309+
authProvider = new PersonalAccessTokenAuthProvider(
310+
this.state.host!,
311+
token
312+
);
313+
}
314+
break;
285315
default:
286316
throw new Error(
287317
`Unknown auth type: ${pick.authType} for profile creation`
@@ -430,3 +460,31 @@ function authMethodsForHostname(host: URL): Array<AuthType> {
430460

431461
return ["profile"];
432462
}
463+
464+
async function collectTokenForPatAuth(
465+
input: MultiStepInput,
466+
step: number,
467+
totalSteps: number
468+
) {
469+
const token = await input.showInputBox({
470+
title: "Enter Personal Access Token",
471+
step,
472+
totalSteps,
473+
validate: async (value) => {
474+
if (value.length === 0) {
475+
return {
476+
message: "Token cannot be empty",
477+
type: "error",
478+
};
479+
}
480+
},
481+
placeholder: "Enter Personal Access Token",
482+
ignoreFocusOut: true,
483+
});
484+
485+
if (token === undefined) {
486+
return;
487+
}
488+
489+
return token;
490+
}

packages/databricks-vscode/src/configuration/auth/AuthProvider.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,3 +312,70 @@ export class AzureCliAuthProvider extends AuthProvider {
312312
return result;
313313
}
314314
}
315+
316+
export class PersonalAccessTokenAuthProvider extends AuthProvider {
317+
constructor(
318+
host: URL,
319+
private readonly token: string
320+
) {
321+
super(host, "pat");
322+
}
323+
324+
describe(): string {
325+
return "Personal Access Token";
326+
}
327+
toJSON(): Record<string, string | undefined> {
328+
return {
329+
host: this.host.toString(),
330+
authType: this.authType,
331+
token: this.token,
332+
};
333+
}
334+
toEnv(): Record<string, string> {
335+
return {
336+
DATABRICKS_HOST: this.host.toString(),
337+
DATABRICKS_AUTH_TYPE: this.authType,
338+
DATABRICKS_TOKEN: this.token,
339+
};
340+
}
341+
toIni(): Record<string, string | undefined> | undefined {
342+
return {
343+
host: this.host.toString(),
344+
token: this.token,
345+
};
346+
}
347+
protected async _check(): Promise<boolean> {
348+
while (true) {
349+
try {
350+
const workspaceClient = this.getWorkspaceClient();
351+
await workspaceClient.currentUser.me();
352+
return true;
353+
} catch (e) {
354+
let message: string = `Can't login with the provided Personal Access Token`;
355+
if (e instanceof Error) {
356+
message = `Can't login with the provided Personal Access Token: ${e.message}`;
357+
}
358+
logging.NamedLogger.getOrCreate(Loggers.Extension).error(
359+
message,
360+
e
361+
);
362+
const choice = await window.showErrorMessage(
363+
message,
364+
"Retry",
365+
"Cancel"
366+
);
367+
if (choice === "Retry") {
368+
continue;
369+
}
370+
return false;
371+
}
372+
}
373+
}
374+
protected getSdkConfig(): Config {
375+
return new Config({
376+
host: this.host.toString(),
377+
authType: "pat",
378+
token: this.token,
379+
});
380+
}
381+
}

packages/databricks-vscode/src/extension.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,8 @@ export async function activate(
539539

540540
const bundleCommands = new BundleCommands(
541541
bundleRemoteStateModel,
542-
bundleRunStatusManager
542+
bundleRunStatusManager,
543+
bundleValidateModel
543544
);
544545
context.subscriptions.push(
545546
bundleResourceExplorerTreeDataProvider,

packages/databricks-vscode/src/ui/MultiStepInputWizard.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ interface InputBoxParameters {
7171
totalSteps: number;
7272
placeholder: string;
7373
initialValue?: string;
74-
validate: (
74+
validate?: (
7575
value: string
7676
) => Promise<string | undefined | ValidationMessageType>;
7777
buttons?: QuickInputButton[];

packages/databricks-vscode/src/ui/bundle-resource-explorer/BundleCommands.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {onError} from "../../utils/onErrorDecorator";
44
import {BundleResourceExplorerTreeNode} from "./types";
55
import {BundleRunStatusManager} from "../../bundle/run/BundleRunStatusManager";
66
import {Mutex} from "../../locking";
7+
import {BundleValidateModel} from "../../bundle/models/BundleValidateModel";
78
import {PipelineTreeNode} from "./PipelineTreeNode";
89
import {JobTreeNode} from "./JobTreeNode";
910

@@ -28,9 +29,15 @@ export class BundleCommands implements Disposable {
2829

2930
constructor(
3031
private readonly bundleRemoteStateModel: BundleRemoteStateModel,
31-
private readonly bundleRunStatusManager: BundleRunStatusManager
32+
private readonly bundleRunStatusManager: BundleRunStatusManager,
33+
private readonly bundleValidateModel: BundleValidateModel
3234
) {
33-
this.disposables.push(this.outputChannel);
35+
this.disposables.push(
36+
this.outputChannel,
37+
this.bundleValidateModel.onDidChange(async () => {
38+
await this.refreshRemoteState();
39+
})
40+
);
3441
}
3542

3643
private refreshStateMutex = new Mutex();

0 commit comments

Comments
 (0)