Skip to content

Commit

Permalink
Improve set active or create new project/namespace workflow
Browse files Browse the repository at this point in the history
- 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>
  • Loading branch information
vrubezhny committed May 17, 2024
1 parent c0466d1 commit 382d4f3
Show file tree
Hide file tree
Showing 6 changed files with 197 additions and 87 deletions.
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1720,12 +1720,12 @@
},
{
"command": "openshift.project.set",
"when": "view == openshiftProjectExplorer && viewItem == openshift.k8sContext && canCreateNamespace && isOpenshiftCluster",
"when": "view == openshiftProjectExplorer && viewItem == openshift.k8sContext && isOpenshiftCluster",
"group": "c1@2"
},
{
"command": "openshift.namespace.set",
"when": "view == openshiftProjectExplorer && viewItem == openshift.k8sContext && canCreateNamespace && !isOpenshiftCluster",
"when": "view == openshiftProjectExplorer && viewItem == openshift.k8sContext && !isOpenshiftCluster",
"group": "c1@2"
},
{
Expand All @@ -1750,12 +1750,12 @@
},
{
"command": "openshift.project.set",
"when": "view == openshiftProjectExplorer && viewItem =~ /openshift.project.*/i && canCreateNamespace && isOpenshiftCluster",
"when": "view == openshiftProjectExplorer && viewItem =~ /openshift.project.*/i && isOpenshiftCluster",
"group": "p3@1"
},
{
"command": "openshift.namespace.set",
"when": "view == openshiftProjectExplorer && viewItem =~ /openshift.project.*/i && canCreateNamespace && !isOpenshiftCluster",
"when": "view == openshiftProjectExplorer && viewItem =~ /openshift.project.*/i && !isOpenshiftCluster",
"group": "p3@1"
},
{
Expand All @@ -1770,12 +1770,12 @@
},
{
"command": "openshift.project.set",
"when": "view == openshiftProjectExplorer && viewItem =~ /openshift.project.*/i && canCreateNamespace && isOpenshiftCluster",
"when": "view == openshiftProjectExplorer && viewItem =~ /openshift.project.*/i && isOpenshiftCluster",
"group": "inline"
},
{
"command": "openshift.namespace.set",
"when": "view == openshiftProjectExplorer && viewItem =~ /openshift.project.*/i && canCreateNamespace && !isOpenshiftCluster",
"when": "view == openshiftProjectExplorer && viewItem =~ /openshift.project.*/i && !isOpenshiftCluster",
"group": "inline"
},
{
Expand Down
80 changes: 49 additions & 31 deletions src/explorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ async function createOrSetProjectItem(projectName: string): Promise<ExplorerItem
};
}

function couldNotGetItem(item: string, clusterURL: string): ExplorerItem {
return {
label: `Couldn't get ${item} for server ${clusterURL}`,
iconPath: new ThemeIcon('error')
};
}

export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Disposable {
private static instance: OpenShiftExplorer;

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

private static generateOpenshiftProjectContextValue(namespace: string): Thenable<string> {
const contextValue = `openshift.project.${namespace}`;
return Oc.Instance.canDeleteNamespace(namespace)
.then(result => (result ? `${contextValue}.can-delete` : contextValue));
const allTrue = arr => arr.every(Boolean);

return Promise.all([
Oc.Instance.canDeleteNamespace(namespace),
Oc.Instance.getProjects(true)
.then((clusterProjects) => {
const existing = clusterProjects.find((project) => project.name === namespace);
return existing !== undefined;
})
])
.then(result => (allTrue(result) ? `${contextValue}.can-delete` : contextValue));
}

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

if ('command' in element) {
if ('command' in element || ('label' in element && 'iconPath' in element)) {
return element;
}

Expand Down Expand Up @@ -290,6 +306,7 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
if (this.kubeContext) {
const config = getKubeConfigFiles();
void commands.executeCommand('setContext', 'canCreateNamespace', await Oc.Instance.canCreateNamespace());
void commands.executeCommand('setContext', 'canListNamespaces', await Oc.Instance.canListNamespaces());
result.unshift({ label: process.env.KUBECONFIG ? 'Custom KubeConfig' : 'Default KubeConfig', description: config.join(':') })
}
}
Expand All @@ -299,7 +316,7 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
OpenShiftExplorer.getInstance().onDidChangeContextEmitter.fire(new KubeConfigUtils().currentContext);
} else if ('name' in element) { // we are dealing with context here
// user is logged into cluster from current context
// and project should be show as child node of current context
// and project should be shown as child node of current context
// there are several possible scenarios
// (1) there is no namespace set in context and default namespace/project exists
// * example is kubernetes provisioned with docker-desktop
Expand All @@ -309,31 +326,23 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
// (3) there is namespace set in context and namespace exists in the cluster
// (4) there is namespace set in context and namespace does not exist in the cluster
const namespaces = await Oc.Instance.getProjects();
if (this.kubeContext.namespace) {
if (namespaces.find(item => item.name === this.kubeContext.namespace)) {
result = [{
kind: 'project',
metadata: {
name: this.kubeContext.namespace,
},
} as KubernetesObject];
} else {
result = [await createOrSetProjectItem(this.kubeContext.namespace)];
}
// Actually 'Oc.Instance.getProjects()' takes care of setting up at least one project as
// an active project, so here after it's enough just to search the array for it.
// The only case where there could be no active project set is empty projects array.
let active = namespaces.find((project) => project.active);
if (!active) active = namespaces.find(item => item?.name === 'default');

// find active or default namespace
if (active) {
result = [{
kind: 'project',
metadata: {
name: active.name,
},
} as KubernetesObject]
} else {
// get list of projects or namespaces
// find default namespace
if (namespaces.find(item => item?.name === 'default')) {
result = [{
kind: 'project',
metadata: {
name: 'default',
},
} as KubernetesObject]
} else {
const projectName = this.kubeConfig.extractProjectNameFromCurrentContext() || 'default';
result = [await createOrSetProjectItem(projectName)];
}
const projectName = this.kubeConfig.extractProjectNameFromCurrentContext() || 'default';
result = [await createOrSetProjectItem(projectName)];
}

// The 'Create Service' menu visibility
Expand Down Expand Up @@ -369,7 +378,12 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
}
}
} else if ('kind' in element && element.kind === 'Deployment') {
return await this.getPods(element);
try {
const pods = await Oc.Instance.getKubernetesObjects('pods');
return pods.filter((pod) => pod.metadata.name.indexOf(element.metadata.name) !== -1);
} catch {
return [ couldNotGetItem(element.kind, this.kubeConfig.getCluster(this.kubeContext.cluster)?.server) ];
}
} else if ('kind' in element && element.kind === 'project') {
const deployments = {
kind: 'deployments',
Expand Down Expand Up @@ -446,7 +460,7 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
}
} else if ('kind' in element) {
const collectableServices: CustomResourceDefinitionStub[] = await this.getServiceKinds();
let collections: KubernetesObject[] | Helm.HelmRelease[];
let collections: KubernetesObject[] | Helm.HelmRelease[] | ExplorerItem[];
switch (element.kind) {
case 'helmReleases':
collections = await Helm.getHelmReleases();
Expand All @@ -457,7 +471,11 @@ export class OpenShiftExplorer implements TreeDataProvider<ExplorerItem>, Dispos
}
break;
default:
collections = await Oc.Instance.getKubernetesObjects(element.kind);
try {
collections = await Oc.Instance.getKubernetesObjects(element.kind);
} catch {
collections = [ couldNotGetItem(element.kind, this.kubeConfig.getCluster(this.kubeContext.cluster)?.server) ];
}
break;
}
const toCollect = [
Expand Down
134 changes: 103 additions & 31 deletions src/oc/ocWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,25 @@ export class Oc {
return false;
}

/**
* Returns true if the current user is authorized to list namespaces on the cluster, and false otherwise.
*
* @returns true if the current user is authorized to list namespaces on the cluster, and false otherwise
*/
public async canListNamespaces(): Promise<boolean> {
try {
const result = await CliChannel.getInstance().executeTool(
new CommandText('oc', 'auth can-i list projects'),
);
if (result.stdout === 'yes') {
return true;
}
} catch {
//ignore
}
return false;
}

/**
* Returns true if the current user is authorized to delete a namespace on the cluster, and false otherwise.
*
Expand Down Expand Up @@ -504,8 +523,9 @@ export class Oc {
}
}

public async getProjects(): Promise<Project[]> {
return this._listProjects();
public async getProjects(onlyFromCluster: boolean = false): Promise<Project[]> {
return this._listProjects()
.then((projects) => onlyFromCluster ? projects : this.fixActiveProject(projects));
}

/**
Expand All @@ -514,51 +534,103 @@ export class Oc {
* @returns the active project or null if no project is active
*/
public async getActiveProject(): Promise<string> {
const projects = await this._listProjects();
if (!projects.length) {
return null;
}
let activeProject = projects.find((project) => project.active);
if (activeProject) return activeProject.name;
return this._listProjects()
.then((projects) => {
const fixedProjects = this.fixActiveProject(projects);
const activeProject = fixedProjects.find((project) => project.active);
return activeProject ? activeProject.name : null;
});
}

// If not found - use Kube Config current context or 'default'
/**
* Fixes the projects array by marking up an active project (if not set)
* by the following rules:
* - If there is only one single project - mark it as active
* - If there is already at least one project marked as active - return the projects "as is"
* - If Kube Config's current context has a namespace set - find an according project
* and mark it as active
* - [fixup for Sandbox cluster] Get Kube Configs's curernt username and try finding a project,
* which name is partially created from that username - if found, treat it as an active project
* - Try a 'default' as a project name, if found - use it as an active project name
* - Use first project as active
*
* @returns The array of Projects with at least one project marked as an active
*/
public fixActiveProject(projects: Project[]): Project[] {
const kcu = new KubeConfigUtils();
const currentContext = kcu.findContext(kcu.currentContext);

let fixedProjects = projects.length ? projects : [];
let activeProject = undefined;

if (currentContext) {
const active = currentContext.namespace || 'default';
activeProject = projects.find((project) => project.name ===active);
// Try Kube Config current context to find existing active project
if (currentContext.namespace) {
activeProject = fixedProjects.find((project) => project.name === currentContext.namespace);
if (activeProject) {
activeProject.active = true;
return fixedProjects;
}
}

// [fixup for Sandbox cluster] Get Kube Configs's curernt username and try finding a project,
// which name is partially created from that username
const currentUser = kcu.getCurrentUser();
if (currentUser) {
const projectPrefix = currentUser.name.substring(0, currentUser.name.indexOf('/'));
if (projectPrefix.length > 0) {
activeProject = fixedProjects.find((project) => project.name.includes(projectPrefix));
if (activeProject) {
activeProject.active = true;
void Oc.Instance.setProject(activeProject.name);
return fixedProjects;
}
}
}

// Add Kube Config current context to the proect list for cases where
// projects/namespaces cannot be listed due to the cluster config restrictions
// (such a project/namespace can be set active manually)
if (currentContext.namespace) {
fixedProjects = [
{
name: currentContext.namespace,
active: true
},
...projects
]
void Oc.Instance.setProject(currentContext.namespace);
return fixedProjects;
}
}

// Try a 'default' as a project name, if found - use it as an active project name
activeProject = fixedProjects.find((project) => project.name === 'default');
if (activeProject) {
activeProject.active = true;
return fixedProjects;
}
return activeProject ? activeProject.name : null;

// Set the first available project as active
if (fixedProjects.length > 0) {
fixedProjects[0].active = true;
void Oc.Instance.setProject(fixedProjects[0].name);
}

return fixedProjects;
}

private async _listProjects(): Promise<Project[]> {
const onlyOneProject = 'you have one project on this server:';
const namespaces: Project[] = [];
return await CliChannel.getInstance().executeTool(
new CommandText('oc', 'projects')
new CommandText('oc', 'projects -q')
)
.then( (result) => {
const lines = result.stdout && result.stdout.split(/\r?\n/g);
for (let line of lines) {
line = line.trim();
if (line === '') continue;
if (line.toLocaleLowerCase().startsWith(onlyOneProject)) {
const matches = line.match(/You\shave\sone\sproject\son\sthis\sserver:\s"([a-zA-Z0-9]+[a-zA-Z0-9.-]*)"./);
if (matches) {
namespaces.push({name: matches[1], active: true});
break; // No more projects are to be listed
}
} else {
const words: string[] = line.split(' ');
if (words.length > 0 && words.length <= 2) {
// The list of projects may have eithe 1 (project name) or 2 words
// (an asterisk char, indicating that the project is active, and project name).
// Otherwise, it's either a header or a footer text
const active = words.length === 2 && words[0].trim() === '*';
const projectName = words[words.length - 1] // The last word of array
namespaces.push( {name: projectName, active });
}
}
namespaces.push( {name: line, active: false });
}
return namespaces;
})
Expand Down
Loading

0 comments on commit 382d4f3

Please sign in to comment.