Skip to content

Commit 59117e6

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: #3999 - The project listing is fixed, so annotated projects are shown now Fixes: #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 #4109) Issue: #4080 (?) Improved the detection whether we're logged in to a cluster or not. Signed-off-by: Victor Rubezhny <vrubezhny@redhat.com>
1 parent 4141fdf commit 59117e6

File tree

6 files changed

+197
-87
lines changed

6 files changed

+197
-87
lines changed

package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1720,12 +1720,12 @@
17201720
},
17211721
{
17221722
"command": "openshift.project.set",
1723-
"when": "view == openshiftProjectExplorer && viewItem == openshift.k8sContext && canCreateNamespace && isOpenshiftCluster",
1723+
"when": "view == openshiftProjectExplorer && viewItem == openshift.k8sContext && isOpenshiftCluster",
17241724
"group": "c1@2"
17251725
},
17261726
{
17271727
"command": "openshift.namespace.set",
1728-
"when": "view == openshiftProjectExplorer && viewItem == openshift.k8sContext && canCreateNamespace && !isOpenshiftCluster",
1728+
"when": "view == openshiftProjectExplorer && viewItem == openshift.k8sContext && !isOpenshiftCluster",
17291729
"group": "c1@2"
17301730
},
17311731
{
@@ -1750,12 +1750,12 @@
17501750
},
17511751
{
17521752
"command": "openshift.project.set",
1753-
"when": "view == openshiftProjectExplorer && viewItem =~ /openshift.project.*/i && canCreateNamespace && isOpenshiftCluster",
1753+
"when": "view == openshiftProjectExplorer && viewItem =~ /openshift.project.*/i && isOpenshiftCluster",
17541754
"group": "p3@1"
17551755
},
17561756
{
17571757
"command": "openshift.namespace.set",
1758-
"when": "view == openshiftProjectExplorer && viewItem =~ /openshift.project.*/i && canCreateNamespace && !isOpenshiftCluster",
1758+
"when": "view == openshiftProjectExplorer && viewItem =~ /openshift.project.*/i && !isOpenshiftCluster",
17591759
"group": "p3@1"
17601760
},
17611761
{
@@ -1770,12 +1770,12 @@
17701770
},
17711771
{
17721772
"command": "openshift.project.set",
1773-
"when": "view == openshiftProjectExplorer && viewItem =~ /openshift.project.*/i && canCreateNamespace && isOpenshiftCluster",
1773+
"when": "view == openshiftProjectExplorer && viewItem =~ /openshift.project.*/i && isOpenshiftCluster",
17741774
"group": "inline"
17751775
},
17761776
{
17771777
"command": "openshift.namespace.set",
1778-
"when": "view == openshiftProjectExplorer && viewItem =~ /openshift.project.*/i && canCreateNamespace && !isOpenshiftCluster",
1778+
"when": "view == openshiftProjectExplorer && viewItem =~ /openshift.project.*/i && !isOpenshiftCluster",
17791779
"group": "inline"
17801780
},
17811781
{

src/explorer.ts

Lines changed: 49 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ async function createOrSetProjectItem(projectName: string): Promise<ExplorerItem
7676
};
7777
}
7878

79+
function couldNotGetItem(item: string, clusterURL: string): ExplorerItem {
80+
return {
81+
label: `Couldn't get ${item} for server ${clusterURL}`,
82+
iconPath: new ThemeIcon('error')
83+
};
84+
}
85+
7986
export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Disposable {
8087
private static instance: OpenShiftExplorer;
8188

@@ -138,14 +145,23 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
138145

139146
private static generateOpenshiftProjectContextValue(namespace: string): Thenable<string> {
140147
const contextValue = `openshift.project.${namespace}`;
141-
return Oc.Instance.canDeleteNamespace(namespace)
142-
.then(result => (result ? `${contextValue}.can-delete` : contextValue));
148+
const allTrue = arr => arr.every(Boolean);
149+
150+
return Promise.all([
151+
Oc.Instance.canDeleteNamespace(namespace),
152+
Oc.Instance.getProjects(true)
153+
.then((clusterProjects) => {
154+
const existing = clusterProjects.find((project) => project.name === namespace);
155+
return existing !== undefined;
156+
})
157+
])
158+
.then(result => (allTrue(result) ? `${contextValue}.can-delete` : contextValue));
143159
}
144160

145161
// eslint-disable-next-line class-methods-use-this
146162
async getTreeItem(element: ExplorerItem): Promise<TreeItem> {
147163

148-
if ('command' in element) {
164+
if ('command' in element || ('label' in element && 'iconPath' in element)) {
149165
return element;
150166
}
151167

@@ -290,6 +306,7 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
290306
if (this.kubeContext) {
291307
const config = getKubeConfigFiles();
292308
void commands.executeCommand('setContext', 'canCreateNamespace', await Oc.Instance.canCreateNamespace());
309+
void commands.executeCommand('setContext', 'canListNamespaces', await Oc.Instance.canListNamespaces());
293310
result.unshift({ label: process.env.KUBECONFIG ? 'Custom KubeConfig' : 'Default KubeConfig', description: config.join(':') })
294311
}
295312
}
@@ -299,7 +316,7 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
299316
OpenShiftExplorer.getInstance().onDidChangeContextEmitter.fire(new KubeConfigUtils().currentContext);
300317
} else if ('name' in element) { // we are dealing with context here
301318
// user is logged into cluster from current context
302-
// and project should be show as child node of current context
319+
// and project should be shown as child node of current context
303320
// there are several possible scenarios
304321
// (1) there is no namespace set in context and default namespace/project exists
305322
// * example is kubernetes provisioned with docker-desktop
@@ -309,31 +326,23 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
309326
// (3) there is namespace set in context and namespace exists in the cluster
310327
// (4) there is namespace set in context and namespace does not exist in the cluster
311328
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-
}
329+
// Actually 'Oc.Instance.getProjects()' takes care of setting up at least one project as
330+
// an active project, so here after it's enough just to search the array for it.
331+
// The only case where there could be no active project set is empty projects array.
332+
let active = namespaces.find((project) => project.active);
333+
if (!active) active = namespaces.find(item => item?.name === 'default');
334+
335+
// find active or default namespace
336+
if (active) {
337+
result = [{
338+
kind: 'project',
339+
metadata: {
340+
name: active.name,
341+
},
342+
} as KubernetesObject]
323343
} 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-
}
344+
const projectName = this.kubeConfig.extractProjectNameFromCurrentContext() || 'default';
345+
result = [await createOrSetProjectItem(projectName)];
337346
}
338347

339348
// The 'Create Service' menu visibility
@@ -369,7 +378,12 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
369378
}
370379
}
371380
} else if ('kind' in element && element.kind === 'Deployment') {
372-
return await this.getPods(element);
381+
try {
382+
const pods = await Oc.Instance.getKubernetesObjects('pods');
383+
return pods.filter((pod) => pod.metadata.name.indexOf(element.metadata.name) !== -1);
384+
} catch {
385+
return [ couldNotGetItem(element.kind, this.kubeConfig.getCluster(this.kubeContext.cluster)?.server) ];
386+
}
373387
} else if ('kind' in element && element.kind === 'project') {
374388
const deployments = {
375389
kind: 'deployments',
@@ -446,7 +460,7 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
446460
}
447461
} else if ('kind' in element) {
448462
const collectableServices: CustomResourceDefinitionStub[] = await this.getServiceKinds();
449-
let collections: KubernetesObject[] | Helm.HelmRelease[];
463+
let collections: KubernetesObject[] | Helm.HelmRelease[] | ExplorerItem[];
450464
switch (element.kind) {
451465
case 'helmReleases':
452466
collections = await Helm.getHelmReleases();
@@ -457,7 +471,11 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
457471
}
458472
break;
459473
default:
460-
collections = await Oc.Instance.getKubernetesObjects(element.kind);
474+
try {
475+
collections = await Oc.Instance.getKubernetesObjects(element.kind);
476+
} catch {
477+
collections = [ couldNotGetItem(element.kind, this.kubeConfig.getCluster(this.kubeContext.cluster)?.server) ];
478+
}
461479
break;
462480
}
463481
const toCollect = [

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
})

0 commit comments

Comments
 (0)