Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve set active or create new project/namespace workflow #4122

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading