Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 15 additions & 12 deletions src/explorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,18 @@ type PackageJSON = {
bugs: string;
};

const CREATE_OR_SET_PROJECT_ITEM = {
label: 'Create new or set active Project',
command: {
title: 'Create new or set active Project',
command: 'openshift.project.set'
}
};
function createOrSetProjectItem(projectName: string): ExplorerItem {
return {
label: `${projectName}`,
description: 'Missing project. Create new or set active Project',
tooltip: `${projectName} - Missing project. Create new or set active Project`,
iconPath: new ThemeIcon('warning'),
command: {
title: 'Create new or set active Project',
command: 'openshift.project.set'
}
};
}

export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Disposable {
private static instance: OpenShiftExplorer;
Expand Down Expand Up @@ -252,11 +257,8 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
name: this.kubeContext.namespace,
},
} as KubernetesObject]
} else if (namespaces.length >= 1) {
// switch to first accessible namespace
await Odo.Instance.setProject(namespaces[0].name);
} else {
result = [CREATE_OR_SET_PROJECT_ITEM]
result = [createOrSetProjectItem(this.kubeContext.namespace)];
}
} else {
// get list of projects or namespaces
Expand All @@ -269,7 +271,8 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
},
} as KubernetesObject]
} else {
result = [CREATE_OR_SET_PROJECT_ITEM]
const projectName = this.kubeConfig.extractProjectNameFromCurrentContext() || 'default';
result = [createOrSetProjectItem(projectName)];
}
}

Expand Down
135 changes: 108 additions & 27 deletions src/openshift/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ import { VsCommandError, vsCommand } from '../vscommand';
import { OpenShiftTerminalManager } from '../webview/openshift-terminal/openShiftTerminal';
import OpenShiftItem, { clusterRequired } from './openshiftItem';
import fetch = require('make-fetch-happen');
import { Cluster as KcuCluster } from '@kubernetes/client-node/dist/config_types';
import { Cluster as KcuCluster, Context as KcuContext } from '@kubernetes/client-node/dist/config_types';

export interface QuickPickItemExt extends QuickPickItem {
name: string,
cluster: string
}

export class Cluster extends OpenShiftItem {

Expand Down Expand Up @@ -152,6 +157,15 @@ export class Cluster extends OpenShiftItem {
});
}

private static getProjectLabel(ctx: KcuContext): string {
const k8sConfig = new KubeConfigUtils();
const pn = k8sConfig.extractProjectNameFromContextName(ctx.name) || '';
const ns = ctx.namespace || pn;
let label = ns.length > 0 ? ns : '[default]';
if (ns !== pn && pn.length > 0) label = `${label} (${pn})`;
return label;
}

@vsCommand('openshift.explorer.switchContext')
static async switchContext(): Promise<string> {
return new Promise<string>((resolve, reject) => {
Expand All @@ -161,8 +175,19 @@ export class Cluster extends OpenShiftItem {
);
const deleteBtn = new quickBtn(new ThemeIcon('trash'), 'Delete');
const quickPick = window.createQuickPick();
const contextNames: QuickPickItem[] = contexts.map((ctx) => ({
label: `${ctx.name}`,
const contextNames: QuickPickItemExt[] = contexts
.map((ctx) => {
return {
...ctx,
label: Cluster.getProjectLabel(ctx)
}
})
.map((ctx) => ({
name: `${ctx.name}`,
cluster: `${ctx.cluster}`,
label: `${ctx.label}`,
description: `on ${ctx.cluster}`,
detail: `User: ${ctx.user}`,
buttons: [deleteBtn],
}));
quickPick.items = contextNames;
Expand All @@ -188,11 +213,32 @@ export class Cluster extends OpenShiftItem {
selection = selects;
});
quickPick.onDidAccept(() => {
const choice = selection[0];
const choice = selection[0] as QuickPickItemExt;
hideDisposable.dispose();
quickPick.hide();
Oc.Instance.setContext(choice.label)
.then(() => resolve(`Cluster context is changed to: ${choice.label}.`))
Oc.Instance.setContext(choice.name)
.then(async () => {
const clusterURL = k8sConfig.findClusterURL(choice.cluster);
if (await LoginUtil.Instance.requireLogin(clusterURL)) {
const status = await Cluster.login(choice.name, true);
if (status) {
if (Cluster.isSandboxCluster(clusterURL)
&& !k8sConfig.equalsToCurrentContext(choice.name)) {
await window.showWarningMessage(
'The cluster appears to be a OpenShift Dev Sandbox cluster, \
but the required project doesn\'t appear to be existing. \
The cluster provided default project is selected instead. ',
'OK',
);
}
}
}
const kcu = new KubeConfigUtils();
const currentContext = kcu.findContext(kcu.currentContext);
const pr = currentContext ? Cluster.getProjectLabel(currentContext) : choice.label;
const cl = currentContext ? currentContext.cluster : choice.description;
resolve(`Cluster context is changed to ${pr} on ${cl}.`);
})
.catch(reject);
});
quickPick.onDidTriggerButton((button) => {
Expand Down Expand Up @@ -337,7 +383,7 @@ export class Cluster extends OpenShiftItem {
* - `undefined` if user pressed `Back` button
* @returns string contaning cluster login method name or null if cancelled or undefined if Back is pressed
*/
private static async getLoginMethod(): Promise<string | null | undefined> {
private static async getLoginMethod(clusterURL: string): Promise<string | null | undefined> {
return new Promise<string | null | undefined>((resolve, reject) => {
const loginActions: QuickPickItem[] = [
{
Expand All @@ -350,6 +396,7 @@ export class Cluster extends OpenShiftItem {
}
];
const quickPick = window.createQuickPick();
quickPick.placeholder=`Select the log in method for: ${clusterURL}`;
quickPick.items = [...loginActions];
const cancelBtn = new quickBtn(new ThemeIcon('close'), 'Cancel');
quickPick.buttons = [QuickInputButtons.Back, cancelBtn];
Expand All @@ -376,18 +423,25 @@ export class Cluster extends OpenShiftItem {

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

const context = kcu.findContext(cluster.name);
let context: KcuContext = contextName && kcu.findContext(contextName);
if (!context || context.cluster !== context.cluster) {
context = kcu.findContextForCluster(cluster.name);
}
if (!context) return true;

// Save `current-context`
Expand All @@ -402,13 +456,42 @@ export class Cluster extends OpenShiftItem {
return true;
}

private static isOpenshiftLocalCluster(clusterURL: string): boolean {
try {
return new URL(clusterURL).hostname === 'api.crc.testing';
} catch (_) {
return false;
}
}

private static isSandboxCluster(clusterURL: string): boolean {
try {
return /api\.sandbox-.*openshiftapps\.com/.test(new URL(clusterURL).hostname);
} catch (_) {
return false;
}
}

/**
* Login to a cluster
*
* @param context - Required context name
* @param skipConfirmation - 'true' in case we don't need any confirmation, 'false' - otherwise
* @returns Successful login message, otherwise - 'null'
*/
@vsCommand('openshift.explorer.login')
static async login(context?: any, skipConfirmation = false): Promise<string> {
static async login(context?: string, skipConfirmation = false): Promise<string> {
const response = await Cluster.requestLoginConfirmation(skipConfirmation);

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

let clusterURL: string;
if (context) {
// If context is specified, we'll initialize clusterURL from it
const kcu = new KubeConfigUtils();
const ctx = kcu.findContext(context);
clusterURL = ctx && kcu.findClusterURL(ctx.cluster);
}

enum Step {
selectCluster = 'selectCluster',
Expand All @@ -423,8 +506,9 @@ export class Cluster extends OpenShiftItem {
case Step.selectCluster: {
let clusterIsUp = false;
do {
clusterURL = await Cluster.getUrl();

if (!clusterURL) {
clusterURL = await Cluster.getUrl();
}
if (!clusterURL) return null;

try {
Expand All @@ -439,14 +523,7 @@ export class Cluster extends OpenShiftItem {
// so it's running
clusterIsUp = true;
} catch (e) {
let clusterURLObj: any = undefined;
try {
clusterURLObj = new URL(clusterURL);
} catch (_) {
// Ignore
}
if (clusterURLObj && clusterURLObj.hostname === 'api.crc.testing') {
const startCrc = 'Start OpenShift Local';
if (Cluster.isOpenshiftLocalCluster(clusterURL)) { const startCrc = 'Start OpenShift Local';
const promptResponse = await window.showWarningMessage(
'The cluster appears to be a OpenShift Local cluster, but it isn\'t running',
'Use a different cluster',
Expand All @@ -458,7 +535,7 @@ export class Cluster extends OpenShiftItem {
// it will take the cluster a few minutes to stabilize
return null;
}
} else if (clusterURLObj && /api\.sandbox-.*openshiftapps\.com/.test(clusterURLObj.hostname)) {
} else if (Cluster.isSandboxCluster(clusterURL)) {
const devSandboxSignup = 'Sign up for OpenShift Dev Sandbox';
const promptResponse = await window.showWarningMessage(
'The cluster appears to be a OpenShift Dev Sandbox cluster, but it isn\'t running',
Expand All @@ -480,14 +557,14 @@ export class Cluster extends OpenShiftItem {
} while (!clusterIsUp);

// contibue if cluster requires User Credentials/Token
if(!(await Cluster.shouldAskForLoginCredentials(clusterURL))) {
if(!(await Cluster.shouldAskForLoginCredentials(clusterURL, context))) {
return null;
}
step = Step.selectLoginMethod;
break;
}
case Step.selectLoginMethod: {
const result = await Cluster.getLoginMethod();
const result = await Cluster.getLoginMethod(clusterURL);
if (result === null) { // User cancelled the operation
return null;
} else if (!result) { // Back button is hit
Expand Down Expand Up @@ -564,6 +641,7 @@ export class Cluster extends OpenShiftItem {
const addUser: QuickPickItem = { label: addUserLabel };

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

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

if (newPassword === null) {
return null; // Cancel
Expand Down Expand Up @@ -747,7 +827,8 @@ export class Cluster extends OpenShiftItem {
if (!userToken) {
const prompt = 'Provide Bearer token for authentication to the API server';
const validateInput = (value: string) => NameValidator.emptyName('Bearer token cannot be empty', value ? value : '');
ocToken = await inputValue(prompt, token ? token : '', true, validateInput);
ocToken = await inputValue(prompt, token ? token : '', true, validateInput,
`Provide Bearer token for: ${clusterURL}`);
if (ocToken === null) {
return null; // Cancel
} else if (!ocToken) {
Expand Down
44 changes: 43 additions & 1 deletion src/util/kubeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,51 @@ export class KubeConfigUtils extends KubeConfig {
return this.getClusters().find((cluster: Cluster) => cluster.server === clusterServer);
}

public findContext(clusterName: string): Context {
public findClusterURL(clusterNameOrURL: string): string {
let clusterObj: Cluster = this.findCluster(clusterNameOrURL);
clusterObj = clusterObj || this.clusters.find((cluster: Cluster) => cluster.name === clusterNameOrURL);
return clusterObj ? clusterObj.server : undefined;
}

public findContext(contextName: string): Context {
return this.getContexts().find((context: Context) => context.name === contextName);
}

public findContextForCluster(clusterName: string): Context {
return this.getContexts().find((context: Context) => context.cluster === clusterName);
}

public extractProjectNameFromCurrentContext():string {
const currentContextName = this.getCurrentContext();
return this.extractProjectNameFromContextName(currentContextName);
}

public extractProjectNameFromContextName(contextName: string):string {
if (contextName && contextName.includes('/') && !contextName.startsWith('/')) {
return contextName.split('/')[0];
}
return undefined;
}

public equalContexts(c1:string, c2:string): boolean {
if (c1 === c2) return true;
const context1 = this.findContext(c1);
const context2 = this.findContext(c2);
if (context1 === context2) return true; // Both are undefibed or reference the same object
if (context1 === undefined && context2 !== undefined) return false;
if (context1 === undefined && context2 !== undefined) return false;
if (context1.cluster !== context2.cluster) return false;
if (context1.namespace !== context2.namespace) return false;
if (context1.user !== context2.user) return false;
return true;
}

public equalsToCurrentContext(contextName:string): boolean {
const currentContext = this.findContext(this.currentContext);
if (!currentContext) return false;

return this.equalContexts(currentContext.name, contextName);
}
}

/**
Expand Down
8 changes: 3 additions & 5 deletions src/util/loginUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,19 @@ export class LoginUtil {
new CommandText('oc', 'status'), { timeout: 5000 })
.then((server) => {
const serverCheck = server ? server.trim() : '';
return serverURI ?
serverURI.toLowerCase() !== `${serverCheck}`.toLowerCase() :
false;
return serverURI ? !(`${serverCheck}`.toLowerCase().includes(serverURI.toLowerCase())) : false;
})
.catch((error) => {
// In case of Kind cluster we're don't need to provide any credentials,
// but Kind normally reports lack of project access rights error:
// "you do not have rights to view project..."
// Here we return 'false' in such case in order to prevent requesting for
// login credentials for Kind-like clusters.
return (error.strerr && error.stderr.toLowerCase().indexOf('error: you do not have rights to view project') !== -1) ? false : true;
return (error.stderr && error.stderr.toLowerCase().includes('you do not have rights to view project')) ? false : true;
});
}

/**
/**
* Log out of the current OpenShift cluster.
*
* @throws if you are not currently logged into an OpenShift cluster
Expand Down