Skip to content

Commit 4d0a1bd

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 (?) Signed-off-by: Victor Rubezhny <vrubezhny@redhat.com>
1 parent 06b87fc commit 4d0a1bd

File tree

4 files changed

+145
-47
lines changed

4 files changed

+145
-47
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 && (canCreateNamespace || canListNamespaces) && 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 && (canCreateNamespace || canListNamespaces) && !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 && (canCreateNamespace || canListNamespaces) && 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 && (canCreateNamespace || canListNamespaces) && !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 && (canCreateNamespace || canListNamespaces) && 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 && (canCreateNamespace || canListNamespaces) && !isOpenshiftCluster",
17781778
"group": "inline"
17791779
},
17801780
{

src/explorer.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
290290
if (this.kubeContext) {
291291
const config = getKubeConfigFiles();
292292
void commands.executeCommand('setContext', 'canCreateNamespace', await Oc.Instance.canCreateNamespace());
293+
void commands.executeCommand('setContext', 'canListNamespaces', await Oc.Instance.canListNamespaces());
293294
result.unshift({ label: process.env.KUBECONFIG ? 'Custom KubeConfig' : 'Default KubeConfig', description: config.join(':') })
294295
}
295296
}
@@ -299,7 +300,7 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
299300
OpenShiftExplorer.getInstance().onDidChangeContextEmitter.fire(new KubeConfigUtils().currentContext);
300301
} else if ('name' in element) { // we are dealing with context here
301302
// user is logged into cluster from current context
302-
// and project should be show as child node of current context
303+
// and project should be shown as child node of current context
303304
// there are several possible scenarios
304305
// (1) there is no namespace set in context and default namespace/project exists
305306
// * example is kubernetes provisioned with docker-desktop
@@ -321,13 +322,15 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
321322
result = [await createOrSetProjectItem(this.kubeContext.namespace)];
322323
}
323324
} else {
324-
// get list of projects or namespaces
325-
// find default namespace
326-
if (namespaces.find(item => item?.name === 'default')) {
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) {
327330
result = [{
328331
kind: 'project',
329332
metadata: {
330-
name: 'default',
333+
name: active.name,
331334
},
332335
} as KubernetesObject]
333336
} else {

src/oc/ocWrapper.ts

Lines changed: 106 additions & 28 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
*
@@ -505,7 +524,8 @@ export class Oc {
505524
}
506525

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

511531
/**
@@ -514,51 +534,109 @@ 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;
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+
}
544+
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[] {
560+
if (!projects.length) return [];
561+
562+
// If there is only one single project - mark it as active
563+
if (projects.length === 1) {
564+
projects[0].active = true;
565+
return projects;
520566
}
567+
568+
// If there is already at least one project marked as active - return the projects "as is"
521569
let activeProject = projects.find((project) => project.active);
522-
if (activeProject) return activeProject.name;
570+
if (activeProject) return projects;
523571

524-
// If not found - use Kube Config current context or 'default'
572+
// Try Kube Config current context to find existing active project
525573
const kcu = new KubeConfigUtils();
526574
const currentContext = kcu.findContext(kcu.currentContext);
527575
if (currentContext) {
528-
const active = currentContext.namespace || 'default';
529-
activeProject = projects.find((project) => project.name ===active);
576+
if (currentContext.namespace) {
577+
activeProject = projects.find((project) => project.name === currentContext.namespace);
578+
if (activeProject) {
579+
activeProject.active = true;
580+
return projects;
581+
}
582+
}
583+
}
584+
585+
// [fixup for Sandbox cluster] Get Kube Configs's curernt username and try finding a project,
586+
// which name is partially created from that username
587+
const currentUser = kcu.getCurrentUser();
588+
if (currentUser) {
589+
const projectPrefix = currentUser.name.substring(0, currentUser.name.indexOf('/'));
590+
if (projectPrefix.length > 0) {
591+
activeProject = projects.find((project) => project.name.includes(projectPrefix));
592+
if (activeProject) {
593+
activeProject.active = true;
594+
void Oc.Instance.setProject(activeProject.name);
595+
return projects;
596+
}
597+
}
598+
}
599+
600+
// Add Kube Config current context to the proect list for cases where
601+
// projects/namespaces cannot be listed due to the cluster config restrictions
602+
// (such a project/namespace can be set active manually)
603+
if (currentContext) {
604+
if (currentContext.namespace) {
605+
const fixedProjects = [
606+
{
607+
name: currentContext.namespace,
608+
active: true
609+
},
610+
...projects
611+
]
612+
void Oc.Instance.setProject(currentContext.namespace);
613+
return fixedProjects;
614+
}
615+
}
616+
617+
// Try a 'default' as a project name, if found - use it as an active project name
618+
activeProject = projects.find((project) => project.name === 'default');
619+
if (activeProject) {
620+
activeProject.active = true;
621+
return projects;
530622
}
531-
return activeProject ? activeProject.name : null;
623+
624+
projects[0].active = true;
625+
void Oc.Instance.setProject(projects[0].name);
626+
return projects;
532627
}
533628

534629
private async _listProjects(): Promise<Project[]> {
535-
const onlyOneProject = 'you have one project on this server:';
536630
const namespaces: Project[] = [];
537631
return await CliChannel.getInstance().executeTool(
538-
new CommandText('oc', 'projects')
632+
new CommandText('oc', 'projects -q')
539633
)
540634
.then( (result) => {
541635
const lines = result.stdout && result.stdout.split(/\r?\n/g);
542636
for (let line of lines) {
543637
line = line.trim();
544638
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-
}
639+
namespaces.push( {name: line, active: false });
562640
}
563641
return namespaces;
564642
})

src/openshift/project.ts

Lines changed: 25 additions & 8 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();

0 commit comments

Comments
 (0)