Skip to content

Commit daf9a93

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 daf9a93

File tree

6 files changed

+163
-63
lines changed

6 files changed

+163
-63
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: 19 additions & 7 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
@@ -321,13 +331,15 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
321331
result = [await createOrSetProjectItem(this.kubeContext.namespace)];
322332
}
323333
} else {
324-
// get list of projects or namespaces
325-
// find default namespace
326-
if (namespaces.find(item => item?.name === 'default')) {
334+
let active = namespaces.find((project) => project.active);
335+
if (!active) active = namespaces.find(item => item?.name === 'default');
336+
337+
// find active or default namespace
338+
if (active) {
327339
result = [{
328340
kind: 'project',
329341
metadata: {
330-
name: 'default',
342+
name: active.name,
331343
},
332344
} as KubernetesObject]
333345
} else {

src/oc/ocWrapper.ts

Lines changed: 99 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,99 @@ 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 = projects.find((project) => project.name.includes(projectPrefix));
583+
if (activeProject) {
584+
activeProject.active = true;
585+
void Oc.Instance.setProject(activeProject.name);
586+
return projects;
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+
}
530605
}
531-
return activeProject ? activeProject.name : null;
606+
607+
// Try a 'default' as a project name, if found - use it as an active project name
608+
activeProject = projects.find((project) => project.name === 'default');
609+
if (activeProject) {
610+
activeProject.active = true;
611+
return projects;
612+
}
613+
614+
projects[0].active = true;
615+
void Oc.Instance.setProject(projects[0].name);
616+
return projects;
532617
}
533618

534619
private async _listProjects(): Promise<Project[]> {
535-
const onlyOneProject = 'you have one project on this server:';
536620
const namespaces: Project[] = [];
537621
return await CliChannel.getInstance().executeTool(
538-
new CommandText('oc', 'projects')
622+
new CommandText('oc', 'projects -q')
539623
)
540624
.then( (result) => {
541625
const lines = result.stdout && result.stdout.split(/\r?\n/g);
542626
for (let line of lines) {
543627
line = line.trim();
544628
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-
}
629+
namespaces.push( {name: line, active: false });
562630
}
563631
return namespaces;
564632
})

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
}

test/integration/ocWrapper.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,15 @@ suite('./oc/ocWrapper.ts', function () {
102102
const project3 = 'my-test-project-3';
103103
await Oc.Instance.createProject(project3);
104104
await Oc.Instance.deleteProject(project3);
105+
106+
// Because 'my-test-project-3' namepace is still stays configured in Kube Config,
107+
// it's been returned by `getProjects` in order to allow working with clusters that
108+
// have a restriction on listing projects/namespaces.
109+
// (see: https://github.com/redhat-developer/vscode-openshift-tools/issues/3999)
110+
// So we need to set any other project as active before aqcuiring projects from the cluster,
111+
// in order to make sure that required `my-test-projet-2` is deleted on the cluster:
112+
await Oc.Instance.setProject('default');
113+
105114
const projects = await Oc.Instance.getProjects();
106115
const projectNames = projects.map((project) => project.name);
107116
expect(projectNames).to.not.contain(project3);

0 commit comments

Comments
 (0)