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: 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>
  • Loading branch information
vrubezhny committed May 16, 2024
1 parent 3cf0e6b commit 58aa471
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 83 deletions.
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1719,12 +1719,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 @@ -1749,12 +1749,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 @@ -1769,12 +1769,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
56 changes: 29 additions & 27 deletions src/explorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,17 @@ 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
Expand Down Expand Up @@ -290,6 +299,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 +309,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 +319,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
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
37 changes: 27 additions & 10 deletions src/openshift/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,42 @@ export class Project extends OpenShiftItem {
static async set(): Promise<string | null> {
let message: string = null;
const kind = await getNamespaceKind();
const canCreateProjects = await Oc.Instance.canCreateNamespace();
const canListProjects = await Oc.Instance.canListNamespaces();

const createNewProject = {
label: `Create new ${kind}`,
description: `Create new ${kind} and make it active`
};
const manuallySetProject = {
label: `Manually set active ${kind}`,
description: `Type in ${kind} name and make it active`
};
const projectsAndCreateNew = Oc.Instance
.getProjects() //
.then((projects) => [
createNewProject,
...projects.map((project) => ({
label: project.name,
description: project.active ? 'Currently active': '',
})),
]);
.then((projects) => {
const items = [];
if (canCreateProjects) {
items.push(createNewProject);
}
items.push(manuallySetProject);
if (canListProjects) {
items.push(...projects.map((project) => ({
label: project.name,
description: project.active ? 'Currently active': '',
})));
}
return items;
});
const selectedItem = await window.showQuickPick(projectsAndCreateNew, {placeHolder: `Select ${kind} to activate or create new one`});
if (!selectedItem) return null;
if (selectedItem === createNewProject) {
await commands.executeCommand('openshift.project.create');
} else {
const projectName = selectedItem.label;
const projectName = selectedItem === manuallySetProject ?
await Project.getProjectName(`${kind} name`, new Promise((resolve) => {resolve([])})) : selectedItem.label;
if (!projectName) return null;

await Oc.Instance.setProject(projectName);
OpenShiftExplorer.getInstance().refresh();
Project.serverlessView.refresh();
Expand All @@ -50,7 +67,7 @@ export class Project extends OpenShiftItem {
@vsCommand('openshift.namespace.create')
static async create(): Promise<string> {
const kind = await getNamespaceKind();
const projectList = Oc.Instance.getProjects();
const projectList = Oc.Instance.getProjects(true);
let projectName = await Project.getProjectName(`${kind} name`, projectList);
if (!projectName) return null;
projectName = projectName.trim();
Expand All @@ -76,7 +93,7 @@ export class Project extends OpenShiftItem {
static async delFromPalette(): Promise<string | null> {
const kind = await getNamespaceKind();
const projects = Oc.Instance
.getProjects() //
.getProjects(true) // Get only projects existing on cluster
.then((projects) => [
...projects.map((project) => ({
label: project.name,
Expand Down
12 changes: 3 additions & 9 deletions src/util/loginUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,9 @@ export class LoginUtil {
if (serverURI && !(`${serverCheck}`.toLowerCase().includes(serverURI.toLowerCase()))) return true;

return await CliChannel.getInstance().executeSyncTool(
new CommandText('oc', 'whoami'), { timeout: 5000 })
.then((user) => false) // Active user is set - no need to login
.catch((error) => {
if (!error.stderr) return true; // Error with no reason - require to login

// if reason is "forbidden" or not determined - require to login, otherwise - no need to login
const matches = error.stderr.match(/Error\sfrom\sserver\s\(([a-zA-Z]*)\):*/);
return matches && matches[1].toLocaleLowerCase() !== 'forbidden' ? false : true;
});
new CommandText('oc', 'api-versions'), { timeout: 5000 })
.then((response) => !response || response.trim().length === 0) // Active user is set - no need to login
.catch((error) => true);
})
.catch((error) => true); // Can't get server - require to login
}
Expand Down
Loading

0 comments on commit 58aa471

Please sign in to comment.