Skip to content

Commit 41892e5

Browse files
vrubezhnydatho7561
authored andcommitted
Improve switch context workflow
Signed-off-by: Victor Rubezhny <vrubezhny@redhat.com>
1 parent 011944f commit 41892e5

File tree

4 files changed

+169
-45
lines changed

4 files changed

+169
-45
lines changed

src/explorer.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,18 @@ type PackageJSON = {
4848
bugs: string;
4949
};
5050

51-
const CREATE_OR_SET_PROJECT_ITEM = {
52-
label: 'Create new or set active Project',
53-
command: {
54-
title: 'Create new or set active Project',
55-
command: 'openshift.project.set'
56-
}
57-
};
51+
function createOrSetProjectItem(projectName: string): ExplorerItem {
52+
return {
53+
label: `${projectName}`,
54+
description: 'Missing project. Create new or set active Project',
55+
tooltip: `${projectName} - Missing project. Create new or set active Project`,
56+
iconPath: new ThemeIcon('warning'),
57+
command: {
58+
title: 'Create new or set active Project',
59+
command: 'openshift.project.set'
60+
}
61+
};
62+
}
5863

5964
export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Disposable {
6065
private static instance: OpenShiftExplorer;
@@ -252,11 +257,8 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
252257
name: this.kubeContext.namespace,
253258
},
254259
} as KubernetesObject]
255-
} else if (namespaces.length >= 1) {
256-
// switch to first accessible namespace
257-
await Odo.Instance.setProject(namespaces[0].name);
258260
} else {
259-
result = [CREATE_OR_SET_PROJECT_ITEM]
261+
result = [createOrSetProjectItem(this.kubeContext.namespace)];
260262
}
261263
} else {
262264
// get list of projects or namespaces
@@ -269,7 +271,8 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
269271
},
270272
} as KubernetesObject]
271273
} else {
272-
result = [CREATE_OR_SET_PROJECT_ITEM]
274+
const projectName = this.kubeConfig.extractProjectNameFromCurrentContext() || 'default';
275+
result = [createOrSetProjectItem(projectName)];
273276
}
274277
}
275278

src/openshift/cluster.ts

Lines changed: 108 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@ import { VsCommandError, vsCommand } from '../vscommand';
2323
import { OpenShiftTerminalManager } from '../webview/openshift-terminal/openShiftTerminal';
2424
import OpenShiftItem, { clusterRequired } from './openshiftItem';
2525
import fetch = require('make-fetch-happen');
26-
import { Cluster as KcuCluster } from '@kubernetes/client-node/dist/config_types';
26+
import { Cluster as KcuCluster, Context as KcuContext } from '@kubernetes/client-node/dist/config_types';
27+
28+
export interface QuickPickItemExt extends QuickPickItem {
29+
name: string,
30+
cluster: string
31+
}
2732

2833
export class Cluster extends OpenShiftItem {
2934

@@ -152,6 +157,15 @@ export class Cluster extends OpenShiftItem {
152157
});
153158
}
154159

160+
private static getProjectLabel(ctx: KcuContext): string {
161+
const k8sConfig = new KubeConfigUtils();
162+
const pn = k8sConfig.extractProjectNameFromContextName(ctx.name) || '';
163+
const ns = ctx.namespace || pn;
164+
let label = ns.length > 0 ? ns : '[default]';
165+
if (ns !== pn && pn.length > 0) label = `${label} (${pn})`;
166+
return label;
167+
}
168+
155169
@vsCommand('openshift.explorer.switchContext')
156170
static async switchContext(): Promise<string> {
157171
return new Promise<string>((resolve, reject) => {
@@ -161,8 +175,19 @@ export class Cluster extends OpenShiftItem {
161175
);
162176
const deleteBtn = new quickBtn(new ThemeIcon('trash'), 'Delete');
163177
const quickPick = window.createQuickPick();
164-
const contextNames: QuickPickItem[] = contexts.map((ctx) => ({
165-
label: `${ctx.name}`,
178+
const contextNames: QuickPickItemExt[] = contexts
179+
.map((ctx) => {
180+
return {
181+
...ctx,
182+
label: Cluster.getProjectLabel(ctx)
183+
}
184+
})
185+
.map((ctx) => ({
186+
name: `${ctx.name}`,
187+
cluster: `${ctx.cluster}`,
188+
label: `${ctx.label}`,
189+
description: `on ${ctx.cluster}`,
190+
detail: `User: ${ctx.user}`,
166191
buttons: [deleteBtn],
167192
}));
168193
quickPick.items = contextNames;
@@ -188,11 +213,32 @@ export class Cluster extends OpenShiftItem {
188213
selection = selects;
189214
});
190215
quickPick.onDidAccept(() => {
191-
const choice = selection[0];
216+
const choice = selection[0] as QuickPickItemExt;
192217
hideDisposable.dispose();
193218
quickPick.hide();
194-
Oc.Instance.setContext(choice.label)
195-
.then(() => resolve(`Cluster context is changed to: ${choice.label}.`))
219+
Oc.Instance.setContext(choice.name)
220+
.then(async () => {
221+
const clusterURL = k8sConfig.findClusterURL(choice.cluster);
222+
if (await LoginUtil.Instance.requireLogin(clusterURL)) {
223+
const status = await Cluster.login(choice.name, true);
224+
if (status) {
225+
if (Cluster.isSandboxCluster(clusterURL)
226+
&& !k8sConfig.equalsToCurrentContext(choice.name)) {
227+
await window.showWarningMessage(
228+
'The cluster appears to be a OpenShift Dev Sandbox cluster, \
229+
but the required project doesn\'t appear to be existing. \
230+
The cluster provided default project is selected instead. ',
231+
'OK',
232+
);
233+
}
234+
}
235+
}
236+
const kcu = new KubeConfigUtils();
237+
const currentContext = kcu.findContext(kcu.currentContext);
238+
const pr = currentContext ? Cluster.getProjectLabel(currentContext) : choice.label;
239+
const cl = currentContext ? currentContext.cluster : choice.description;
240+
resolve(`Cluster context is changed to ${pr} on ${cl}.`);
241+
})
196242
.catch(reject);
197243
});
198244
quickPick.onDidTriggerButton((button) => {
@@ -337,7 +383,7 @@ export class Cluster extends OpenShiftItem {
337383
* - `undefined` if user pressed `Back` button
338384
* @returns string contaning cluster login method name or null if cancelled or undefined if Back is pressed
339385
*/
340-
private static async getLoginMethod(): Promise<string | null | undefined> {
386+
private static async getLoginMethod(clusterURL: string): Promise<string | null | undefined> {
341387
return new Promise<string | null | undefined>((resolve, reject) => {
342388
const loginActions: QuickPickItem[] = [
343389
{
@@ -350,6 +396,7 @@ export class Cluster extends OpenShiftItem {
350396
}
351397
];
352398
const quickPick = window.createQuickPick();
399+
quickPick.placeholder=`Select the log in method for: ${clusterURL}`;
353400
quickPick.items = [...loginActions];
354401
const cancelBtn = new quickBtn(new ThemeIcon('close'), 'Cancel');
355402
quickPick.buttons = [QuickInputButtons.Back, cancelBtn];
@@ -376,18 +423,25 @@ export class Cluster extends OpenShiftItem {
376423

377424
/**
378425
* Checks if we're already logged in to a cluster.
379-
* So, if we are, no need to re-enter User Credentials of Token
426+
* So, if we are, no need to re-enter User Credentials of Token.
427+
*
428+
* If contextName is specified and points to a cluster with the same URI as clusterURI,
429+
* the context is used as context to be switched to. Otherwise, we'll use the first
430+
* context found for the clusrtURI specified.
380431
*
381432
* @param clusterURI URI of the cluster to login
382433
* @returns true in case we should continue with asking for credentials,
383434
* false in case we're already logged in
384435
*/
385-
static async shouldAskForLoginCredentials(clusterURI: string): Promise<boolean> {
436+
static async shouldAskForLoginCredentials(clusterURI: string, contextName?: string): Promise<boolean> {
386437
const kcu = new KubeConfigUtils();
387438
const cluster: KcuCluster = kcu.findCluster(clusterURI);
388439
if (!cluster) return true;
389440

390-
const context = kcu.findContext(cluster.name);
441+
let context: KcuContext = contextName && kcu.findContext(contextName);
442+
if (!context || context.cluster !== context.cluster) {
443+
context = kcu.findContextForCluster(cluster.name);
444+
}
391445
if (!context) return true;
392446

393447
// Save `current-context`
@@ -402,13 +456,42 @@ export class Cluster extends OpenShiftItem {
402456
return true;
403457
}
404458

459+
private static isOpenshiftLocalCluster(clusterURL: string): boolean {
460+
try {
461+
return new URL(clusterURL).hostname === 'api.crc.testing';
462+
} catch (_) {
463+
return false;
464+
}
465+
}
466+
467+
private static isSandboxCluster(clusterURL: string): boolean {
468+
try {
469+
return /api\.sandbox-.*openshiftapps\.com/.test(new URL(clusterURL).hostname);
470+
} catch (_) {
471+
return false;
472+
}
473+
}
474+
475+
/**
476+
* Login to a cluster
477+
*
478+
* @param context - Required context name
479+
* @param skipConfirmation - 'true' in case we don't need any confirmation, 'false' - otherwise
480+
* @returns Successful login message, otherwise - 'null'
481+
*/
405482
@vsCommand('openshift.explorer.login')
406-
static async login(context?: any, skipConfirmation = false): Promise<string> {
483+
static async login(context?: string, skipConfirmation = false): Promise<string> {
407484
const response = await Cluster.requestLoginConfirmation(skipConfirmation);
408485

409486
if (response !== 'Yes') return null;
410487

411488
let clusterURL: string;
489+
if (context) {
490+
// If context is specified, we'll initialize clusterURL from it
491+
const kcu = new KubeConfigUtils();
492+
const ctx = kcu.findContext(context);
493+
clusterURL = ctx && kcu.findClusterURL(ctx.cluster);
494+
}
412495

413496
enum Step {
414497
selectCluster = 'selectCluster',
@@ -423,8 +506,9 @@ export class Cluster extends OpenShiftItem {
423506
case Step.selectCluster: {
424507
let clusterIsUp = false;
425508
do {
426-
clusterURL = await Cluster.getUrl();
427-
509+
if (!clusterURL) {
510+
clusterURL = await Cluster.getUrl();
511+
}
428512
if (!clusterURL) return null;
429513

430514
try {
@@ -439,14 +523,7 @@ export class Cluster extends OpenShiftItem {
439523
// so it's running
440524
clusterIsUp = true;
441525
} catch (e) {
442-
let clusterURLObj: any = undefined;
443-
try {
444-
clusterURLObj = new URL(clusterURL);
445-
} catch (_) {
446-
// Ignore
447-
}
448-
if (clusterURLObj && clusterURLObj.hostname === 'api.crc.testing') {
449-
const startCrc = 'Start OpenShift Local';
526+
if (Cluster.isOpenshiftLocalCluster(clusterURL)) { const startCrc = 'Start OpenShift Local';
450527
const promptResponse = await window.showWarningMessage(
451528
'The cluster appears to be a OpenShift Local cluster, but it isn\'t running',
452529
'Use a different cluster',
@@ -458,7 +535,7 @@ export class Cluster extends OpenShiftItem {
458535
// it will take the cluster a few minutes to stabilize
459536
return null;
460537
}
461-
} else if (clusterURLObj && /api\.sandbox-.*openshiftapps\.com/.test(clusterURLObj.hostname)) {
538+
} else if (Cluster.isSandboxCluster(clusterURL)) {
462539
const devSandboxSignup = 'Sign up for OpenShift Dev Sandbox';
463540
const promptResponse = await window.showWarningMessage(
464541
'The cluster appears to be a OpenShift Dev Sandbox cluster, but it isn\'t running',
@@ -480,14 +557,14 @@ export class Cluster extends OpenShiftItem {
480557
} while (!clusterIsUp);
481558

482559
// contibue if cluster requires User Credentials/Token
483-
if(!(await Cluster.shouldAskForLoginCredentials(clusterURL))) {
560+
if(!(await Cluster.shouldAskForLoginCredentials(clusterURL, context))) {
484561
return null;
485562
}
486563
step = Step.selectLoginMethod;
487564
break;
488565
}
489566
case Step.selectLoginMethod: {
490-
const result = await Cluster.getLoginMethod();
567+
const result = await Cluster.getLoginMethod(clusterURL);
491568
if (result === null) { // User cancelled the operation
492569
return null;
493570
} else if (!result) { // Back button is hit
@@ -564,6 +641,7 @@ export class Cluster extends OpenShiftItem {
564641
const addUser: QuickPickItem = { label: addUserLabel };
565642

566643
const quickPick = window.createQuickPick();
644+
quickPick.placeholder=`Select or add username for: ${clusterURL}`;
567645
quickPick.items = [addUser, ...users];
568646
const cancelBtn = new quickBtn(new ThemeIcon('close'), 'Cancel');
569647
quickPick.buttons = [QuickInputButtons.Back, cancelBtn];
@@ -632,7 +710,8 @@ export class Cluster extends OpenShiftItem {
632710
if (!username) {
633711
const prompt = 'Provide Username for basic authentication to the API server';
634712
const validateInput = (value: string) => NameValidator.emptyName('User name cannot be empty', value ? value : '');
635-
const newUsername = await inputValue(prompt, '', false, validateInput);
713+
const newUsername = await inputValue(prompt, '', false, validateInput,
714+
`Provide Username for: ${clusterURL}`);
636715

637716
if (newUsername === null) {
638717
return null; // Cancel
@@ -650,7 +729,8 @@ export class Cluster extends OpenShiftItem {
650729
password = await TokenStore.getItem('login', username);
651730
const prompt = 'Provide Password for basic authentication to the API server';
652731
const validateInput = (value: string) => NameValidator.emptyName('Password cannot be empty', value ? value : '');
653-
const newPassword = await inputValue(prompt, password, true, validateInput);
732+
const newPassword = await inputValue(prompt, password, true, validateInput,
733+
`Provide Password for: ${clusterURL}`);
654734

655735
if (newPassword === null) {
656736
return null; // Cancel
@@ -747,7 +827,8 @@ export class Cluster extends OpenShiftItem {
747827
if (!userToken) {
748828
const prompt = 'Provide Bearer token for authentication to the API server';
749829
const validateInput = (value: string) => NameValidator.emptyName('Bearer token cannot be empty', value ? value : '');
750-
ocToken = await inputValue(prompt, token ? token : '', true, validateInput);
830+
ocToken = await inputValue(prompt, token ? token : '', true, validateInput,
831+
`Provide Bearer token for: ${clusterURL}`);
751832
if (ocToken === null) {
752833
return null; // Cancel
753834
} else if (!ocToken) {

src/util/kubeUtils.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,51 @@ export class KubeConfigUtils extends KubeConfig {
102102
return this.getClusters().find((cluster: Cluster) => cluster.server === clusterServer);
103103
}
104104

105-
public findContext(clusterName: string): Context {
105+
public findClusterURL(clusterNameOrURL: string): string {
106+
let clusterObj: Cluster = this.findCluster(clusterNameOrURL);
107+
clusterObj = clusterObj || this.clusters.find((cluster: Cluster) => cluster.name === clusterNameOrURL);
108+
return clusterObj ? clusterObj.server : undefined;
109+
}
110+
111+
public findContext(contextName: string): Context {
112+
return this.getContexts().find((context: Context) => context.name === contextName);
113+
}
114+
115+
public findContextForCluster(clusterName: string): Context {
106116
return this.getContexts().find((context: Context) => context.cluster === clusterName);
107117
}
118+
119+
public extractProjectNameFromCurrentContext():string {
120+
const currentContextName = this.getCurrentContext();
121+
return this.extractProjectNameFromContextName(currentContextName);
122+
}
123+
124+
public extractProjectNameFromContextName(contextName: string):string {
125+
if (contextName && contextName.includes('/') && !contextName.startsWith('/')) {
126+
return contextName.split('/')[0];
127+
}
128+
return undefined;
129+
}
130+
131+
public equalContexts(c1:string, c2:string): boolean {
132+
if (c1 === c2) return true;
133+
const context1 = this.findContext(c1);
134+
const context2 = this.findContext(c2);
135+
if (context1 === context2) return true; // Both are undefibed or reference the same object
136+
if (context1 === undefined && context2 !== undefined) return false;
137+
if (context1 === undefined && context2 !== undefined) return false;
138+
if (context1.cluster !== context2.cluster) return false;
139+
if (context1.namespace !== context2.namespace) return false;
140+
if (context1.user !== context2.user) return false;
141+
return true;
142+
}
143+
144+
public equalsToCurrentContext(contextName:string): boolean {
145+
const currentContext = this.findContext(this.currentContext);
146+
if (!currentContext) return false;
147+
148+
return this.equalContexts(currentContext.name, contextName);
149+
}
108150
}
109151

110152
/**

src/util/loginUtil.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,21 +34,19 @@ export class LoginUtil {
3434
new CommandText('oc', 'status'), { timeout: 5000 })
3535
.then((server) => {
3636
const serverCheck = server ? server.trim() : '';
37-
return serverURI ?
38-
serverURI.toLowerCase() !== `${serverCheck}`.toLowerCase() :
39-
false;
37+
return serverURI ? !(`${serverCheck}`.toLowerCase().includes(serverURI.toLowerCase())) : false;
4038
})
4139
.catch((error) => {
4240
// In case of Kind cluster we're don't need to provide any credentials,
4341
// but Kind normally reports lack of project access rights error:
4442
// "you do not have rights to view project..."
4543
// Here we return 'false' in such case in order to prevent requesting for
4644
// login credentials for Kind-like clusters.
47-
return (error.strerr && error.stderr.toLowerCase().indexOf('error: you do not have rights to view project') !== -1) ? false : true;
45+
return (error.stderr && error.stderr.toLowerCase().includes('you do not have rights to view project')) ? false : true;
4846
});
4947
}
5048

51-
/**
49+
/**
5250
* Log out of the current OpenShift cluster.
5351
*
5452
* @throws if you are not currently logged into an OpenShift cluster

0 commit comments

Comments
 (0)