Skip to content

Commit 58aa471

Browse files
committed
Improve set active or create new project/namespace workflow
- Now, when switching projects/namespaces, one can manually type in a project name to be set as an active one. - No more 'Missing project/namespace' item for a project/namespace that doesn't exist on a cluster. This allows working normally on clusters with restrictions on list for projects. Fixes: redhat-developer#3999 - The project listing is fixed, so annotated projects are shown now Fixes: redhat-developer#4101 - For a Sandbox cluster a project which name contains current user name is used as a default one when logging in (A follow up to redhat-developer#4109) Issue: redhat-developer#4080 (?) Improved the detection whether we're logged in to a cluster or not. Signed-off-by: Victor Rubezhny <vrubezhny@redhat.com>
1 parent 3cf0e6b commit 58aa471

File tree

6 files changed

+177
-83
lines changed

6 files changed

+177
-83
lines changed

package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1719,12 +1719,12 @@
17191719
},
17201720
{
17211721
"command": "openshift.project.set",
1722-
"when": "view == openshiftProjectExplorer && viewItem == openshift.k8sContext && canCreateNamespace && isOpenshiftCluster",
1722+
"when": "view == openshiftProjectExplorer && viewItem == openshift.k8sContext && isOpenshiftCluster",
17231723
"group": "c1@2"
17241724
},
17251725
{
17261726
"command": "openshift.namespace.set",
1727-
"when": "view == openshiftProjectExplorer && viewItem == openshift.k8sContext && canCreateNamespace && !isOpenshiftCluster",
1727+
"when": "view == openshiftProjectExplorer && viewItem == openshift.k8sContext && !isOpenshiftCluster",
17281728
"group": "c1@2"
17291729
},
17301730
{
@@ -1749,12 +1749,12 @@
17491749
},
17501750
{
17511751
"command": "openshift.project.set",
1752-
"when": "view == openshiftProjectExplorer && viewItem =~ /openshift.project.*/i && canCreateNamespace && isOpenshiftCluster",
1752+
"when": "view == openshiftProjectExplorer && viewItem =~ /openshift.project.*/i && isOpenshiftCluster",
17531753
"group": "p3@1"
17541754
},
17551755
{
17561756
"command": "openshift.namespace.set",
1757-
"when": "view == openshiftProjectExplorer && viewItem =~ /openshift.project.*/i && canCreateNamespace && !isOpenshiftCluster",
1757+
"when": "view == openshiftProjectExplorer && viewItem =~ /openshift.project.*/i && !isOpenshiftCluster",
17581758
"group": "p3@1"
17591759
},
17601760
{
@@ -1769,12 +1769,12 @@
17691769
},
17701770
{
17711771
"command": "openshift.project.set",
1772-
"when": "view == openshiftProjectExplorer && viewItem =~ /openshift.project.*/i && canCreateNamespace && isOpenshiftCluster",
1772+
"when": "view == openshiftProjectExplorer && viewItem =~ /openshift.project.*/i && isOpenshiftCluster",
17731773
"group": "inline"
17741774
},
17751775
{
17761776
"command": "openshift.namespace.set",
1777-
"when": "view == openshiftProjectExplorer && viewItem =~ /openshift.project.*/i && canCreateNamespace && !isOpenshiftCluster",
1777+
"when": "view == openshiftProjectExplorer && viewItem =~ /openshift.project.*/i && !isOpenshiftCluster",
17781778
"group": "inline"
17791779
},
17801780
{

src/explorer.ts

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,17 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
138138

139139
private static generateOpenshiftProjectContextValue(namespace: string): Thenable<string> {
140140
const contextValue = `openshift.project.${namespace}`;
141-
return Oc.Instance.canDeleteNamespace(namespace)
142-
.then(result => (result ? `${contextValue}.can-delete` : contextValue));
141+
const allTrue = arr => arr.every(Boolean);
142+
143+
return Promise.all([
144+
Oc.Instance.canDeleteNamespace(namespace),
145+
Oc.Instance.getProjects(true)
146+
.then((clusterProjects) => {
147+
const existing = clusterProjects.find((project) => project.name === namespace);
148+
return existing !== undefined;
149+
})
150+
])
151+
.then(result => (allTrue(result) ? `${contextValue}.can-delete` : contextValue));
143152
}
144153

145154
// eslint-disable-next-line class-methods-use-this
@@ -290,6 +299,7 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
290299
if (this.kubeContext) {
291300
const config = getKubeConfigFiles();
292301
void commands.executeCommand('setContext', 'canCreateNamespace', await Oc.Instance.canCreateNamespace());
302+
void commands.executeCommand('setContext', 'canListNamespaces', await Oc.Instance.canListNamespaces());
293303
result.unshift({ label: process.env.KUBECONFIG ? 'Custom KubeConfig' : 'Default KubeConfig', description: config.join(':') })
294304
}
295305
}
@@ -299,7 +309,7 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
299309
OpenShiftExplorer.getInstance().onDidChangeContextEmitter.fire(new KubeConfigUtils().currentContext);
300310
} else if ('name' in element) { // we are dealing with context here
301311
// user is logged into cluster from current context
302-
// and project should be show as child node of current context
312+
// and project should be shown as child node of current context
303313
// there are several possible scenarios
304314
// (1) there is no namespace set in context and default namespace/project exists
305315
// * example is kubernetes provisioned with docker-desktop
@@ -309,31 +319,23 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
309319
// (3) there is namespace set in context and namespace exists in the cluster
310320
// (4) there is namespace set in context and namespace does not exist in the cluster
311321
const namespaces = await Oc.Instance.getProjects();
312-
if (this.kubeContext.namespace) {
313-
if (namespaces.find(item => item.name === this.kubeContext.namespace)) {
314-
result = [{
315-
kind: 'project',
316-
metadata: {
317-
name: this.kubeContext.namespace,
318-
},
319-
} as KubernetesObject];
320-
} else {
321-
result = [await createOrSetProjectItem(this.kubeContext.namespace)];
322-
}
322+
// Actually 'Oc.Instance.getProjects()' takes care of setting up at least one project as
323+
// an active project, so here after it's enough just to search the array for it.
324+
// The only case where there could be no active project set is empty projects array.
325+
let active = namespaces.find((project) => project.active);
326+
if (!active) active = namespaces.find(item => item?.name === 'default');
327+
328+
// find active or default namespace
329+
if (active) {
330+
result = [{
331+
kind: 'project',
332+
metadata: {
333+
name: active.name,
334+
},
335+
} as KubernetesObject]
323336
} else {
324-
// get list of projects or namespaces
325-
// find default namespace
326-
if (namespaces.find(item => item?.name === 'default')) {
327-
result = [{
328-
kind: 'project',
329-
metadata: {
330-
name: 'default',
331-
},
332-
} as KubernetesObject]
333-
} else {
334-
const projectName = this.kubeConfig.extractProjectNameFromCurrentContext() || 'default';
335-
result = [await createOrSetProjectItem(projectName)];
336-
}
337+
const projectName = this.kubeConfig.extractProjectNameFromCurrentContext() || 'default';
338+
result = [await createOrSetProjectItem(projectName)];
337339
}
338340

339341
// The 'Create Service' menu visibility

src/oc/ocWrapper.ts

Lines changed: 103 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,25 @@ export class Oc {
200200
return false;
201201
}
202202

203+
/**
204+
* Returns true if the current user is authorized to list namespaces on the cluster, and false otherwise.
205+
*
206+
* @returns true if the current user is authorized to list namespaces on the cluster, and false otherwise
207+
*/
208+
public async canListNamespaces(): Promise<boolean> {
209+
try {
210+
const result = await CliChannel.getInstance().executeTool(
211+
new CommandText('oc', 'auth can-i list projects'),
212+
);
213+
if (result.stdout === 'yes') {
214+
return true;
215+
}
216+
} catch {
217+
//ignore
218+
}
219+
return false;
220+
}
221+
203222
/**
204223
* Returns true if the current user is authorized to delete a namespace on the cluster, and false otherwise.
205224
*
@@ -504,8 +523,9 @@ export class Oc {
504523
}
505524
}
506525

507-
public async getProjects(): Promise<Project[]> {
508-
return this._listProjects();
526+
public async getProjects(onlyFromCluster: boolean = false): Promise<Project[]> {
527+
return this._listProjects()
528+
.then((projects) => onlyFromCluster ? projects : this.fixActiveProject(projects));
509529
}
510530

511531
/**
@@ -514,51 +534,103 @@ export class Oc {
514534
* @returns the active project or null if no project is active
515535
*/
516536
public async getActiveProject(): Promise<string> {
517-
const projects = await this._listProjects();
518-
if (!projects.length) {
519-
return null;
520-
}
521-
let activeProject = projects.find((project) => project.active);
522-
if (activeProject) return activeProject.name;
537+
return this._listProjects()
538+
.then((projects) => {
539+
const fixedProjects = this.fixActiveProject(projects);
540+
const activeProject = fixedProjects.find((project) => project.active);
541+
return activeProject ? activeProject.name : null;
542+
});
543+
}
523544

524-
// If not found - use Kube Config current context or 'default'
545+
/**
546+
* Fixes the projects array by marking up an active project (if not set)
547+
* by the following rules:
548+
* - If there is only one single project - mark it as active
549+
* - If there is already at least one project marked as active - return the projects "as is"
550+
* - If Kube Config's current context has a namespace set - find an according project
551+
* and mark it as active
552+
* - [fixup for Sandbox cluster] Get Kube Configs's curernt username and try finding a project,
553+
* which name is partially created from that username - if found, treat it as an active project
554+
* - Try a 'default' as a project name, if found - use it as an active project name
555+
* - Use first project as active
556+
*
557+
* @returns The array of Projects with at least one project marked as an active
558+
*/
559+
public fixActiveProject(projects: Project[]): Project[] {
525560
const kcu = new KubeConfigUtils();
526561
const currentContext = kcu.findContext(kcu.currentContext);
562+
563+
let fixedProjects = projects.length ? projects : [];
564+
let activeProject = undefined;
565+
527566
if (currentContext) {
528-
const active = currentContext.namespace || 'default';
529-
activeProject = projects.find((project) => project.name ===active);
567+
// Try Kube Config current context to find existing active project
568+
if (currentContext.namespace) {
569+
activeProject = fixedProjects.find((project) => project.name === currentContext.namespace);
570+
if (activeProject) {
571+
activeProject.active = true;
572+
return fixedProjects;
573+
}
574+
}
575+
576+
// [fixup for Sandbox cluster] Get Kube Configs's curernt username and try finding a project,
577+
// which name is partially created from that username
578+
const currentUser = kcu.getCurrentUser();
579+
if (currentUser) {
580+
const projectPrefix = currentUser.name.substring(0, currentUser.name.indexOf('/'));
581+
if (projectPrefix.length > 0) {
582+
activeProject = fixedProjects.find((project) => project.name.includes(projectPrefix));
583+
if (activeProject) {
584+
activeProject.active = true;
585+
void Oc.Instance.setProject(activeProject.name);
586+
return fixedProjects;
587+
}
588+
}
589+
}
590+
591+
// Add Kube Config current context to the proect list for cases where
592+
// projects/namespaces cannot be listed due to the cluster config restrictions
593+
// (such a project/namespace can be set active manually)
594+
if (currentContext.namespace) {
595+
fixedProjects = [
596+
{
597+
name: currentContext.namespace,
598+
active: true
599+
},
600+
...projects
601+
]
602+
void Oc.Instance.setProject(currentContext.namespace);
603+
return fixedProjects;
604+
}
605+
}
606+
607+
// Try a 'default' as a project name, if found - use it as an active project name
608+
activeProject = fixedProjects.find((project) => project.name === 'default');
609+
if (activeProject) {
610+
activeProject.active = true;
611+
return fixedProjects;
530612
}
531-
return activeProject ? activeProject.name : null;
613+
614+
// Set the first available project as active
615+
if (fixedProjects.length > 0) {
616+
fixedProjects[0].active = true;
617+
void Oc.Instance.setProject(fixedProjects[0].name);
618+
}
619+
620+
return fixedProjects;
532621
}
533622

534623
private async _listProjects(): Promise<Project[]> {
535-
const onlyOneProject = 'you have one project on this server:';
536624
const namespaces: Project[] = [];
537625
return await CliChannel.getInstance().executeTool(
538-
new CommandText('oc', 'projects')
626+
new CommandText('oc', 'projects -q')
539627
)
540628
.then( (result) => {
541629
const lines = result.stdout && result.stdout.split(/\r?\n/g);
542630
for (let line of lines) {
543631
line = line.trim();
544632
if (line === '') continue;
545-
if (line.toLocaleLowerCase().startsWith(onlyOneProject)) {
546-
const matches = line.match(/You\shave\sone\sproject\son\sthis\sserver:\s"([a-zA-Z0-9]+[a-zA-Z0-9.-]*)"./);
547-
if (matches) {
548-
namespaces.push({name: matches[1], active: true});
549-
break; // No more projects are to be listed
550-
}
551-
} else {
552-
const words: string[] = line.split(' ');
553-
if (words.length > 0 && words.length <= 2) {
554-
// The list of projects may have eithe 1 (project name) or 2 words
555-
// (an asterisk char, indicating that the project is active, and project name).
556-
// Otherwise, it's either a header or a footer text
557-
const active = words.length === 2 && words[0].trim() === '*';
558-
const projectName = words[words.length - 1] // The last word of array
559-
namespaces.push( {name: projectName, active });
560-
}
561-
}
633+
namespaces.push( {name: line, active: false });
562634
}
563635
return namespaces;
564636
})

src/openshift/project.ts

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,42 @@ export class Project extends OpenShiftItem {
1919
static async set(): Promise<string | null> {
2020
let message: string = null;
2121
const kind = await getNamespaceKind();
22+
const canCreateProjects = await Oc.Instance.canCreateNamespace();
23+
const canListProjects = await Oc.Instance.canListNamespaces();
24+
2225
const createNewProject = {
2326
label: `Create new ${kind}`,
2427
description: `Create new ${kind} and make it active`
2528
};
29+
const manuallySetProject = {
30+
label: `Manually set active ${kind}`,
31+
description: `Type in ${kind} name and make it active`
32+
};
2633
const projectsAndCreateNew = Oc.Instance
2734
.getProjects() //
28-
.then((projects) => [
29-
createNewProject,
30-
...projects.map((project) => ({
31-
label: project.name,
32-
description: project.active ? 'Currently active': '',
33-
})),
34-
]);
35+
.then((projects) => {
36+
const items = [];
37+
if (canCreateProjects) {
38+
items.push(createNewProject);
39+
}
40+
items.push(manuallySetProject);
41+
if (canListProjects) {
42+
items.push(...projects.map((project) => ({
43+
label: project.name,
44+
description: project.active ? 'Currently active': '',
45+
})));
46+
}
47+
return items;
48+
});
3549
const selectedItem = await window.showQuickPick(projectsAndCreateNew, {placeHolder: `Select ${kind} to activate or create new one`});
3650
if (!selectedItem) return null;
3751
if (selectedItem === createNewProject) {
3852
await commands.executeCommand('openshift.project.create');
3953
} else {
40-
const projectName = selectedItem.label;
54+
const projectName = selectedItem === manuallySetProject ?
55+
await Project.getProjectName(`${kind} name`, new Promise((resolve) => {resolve([])})) : selectedItem.label;
56+
if (!projectName) return null;
57+
4158
await Oc.Instance.setProject(projectName);
4259
OpenShiftExplorer.getInstance().refresh();
4360
Project.serverlessView.refresh();
@@ -50,7 +67,7 @@ export class Project extends OpenShiftItem {
5067
@vsCommand('openshift.namespace.create')
5168
static async create(): Promise<string> {
5269
const kind = await getNamespaceKind();
53-
const projectList = Oc.Instance.getProjects();
70+
const projectList = Oc.Instance.getProjects(true);
5471
let projectName = await Project.getProjectName(`${kind} name`, projectList);
5572
if (!projectName) return null;
5673
projectName = projectName.trim();
@@ -76,7 +93,7 @@ export class Project extends OpenShiftItem {
7693
static async delFromPalette(): Promise<string | null> {
7794
const kind = await getNamespaceKind();
7895
const projects = Oc.Instance
79-
.getProjects() //
96+
.getProjects(true) // Get only projects existing on cluster
8097
.then((projects) => [
8198
...projects.map((project) => ({
8299
label: project.name,

src/util/loginUtil.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,9 @@ export class LoginUtil {
3535
if (serverURI && !(`${serverCheck}`.toLowerCase().includes(serverURI.toLowerCase()))) return true;
3636

3737
return await CliChannel.getInstance().executeSyncTool(
38-
new CommandText('oc', 'whoami'), { timeout: 5000 })
39-
.then((user) => false) // Active user is set - no need to login
40-
.catch((error) => {
41-
if (!error.stderr) return true; // Error with no reason - require to login
42-
43-
// if reason is "forbidden" or not determined - require to login, otherwise - no need to login
44-
const matches = error.stderr.match(/Error\sfrom\sserver\s\(([a-zA-Z]*)\):*/);
45-
return matches && matches[1].toLocaleLowerCase() !== 'forbidden' ? false : true;
46-
});
38+
new CommandText('oc', 'api-versions'), { timeout: 5000 })
39+
.then((response) => !response || response.trim().length === 0) // Active user is set - no need to login
40+
.catch((error) => true);
4741
})
4842
.catch((error) => true); // Can't get server - require to login
4943
}

0 commit comments

Comments
 (0)