diff --git a/src/common/ipc/cluster.ipc.ts b/src/common/ipc/cluster.ipc.ts new file mode 100644 index 0000000000..a7b13ba290 --- /dev/null +++ b/src/common/ipc/cluster.ipc.ts @@ -0,0 +1,11 @@ +/** + * This channel is broadcast on whenever the cluster fails to list namespaces + * during a refresh and no `accessibleNamespaces` have been set. + */ +export const ClusterListNamespaceForbiddenChannel = "cluster:list-namespace-forbidden"; + +export type ListNamespaceForbiddenArgs = [clusterId: string]; + +export function isListNamespaceForbiddenArgs(args: unknown[]): args is ListNamespaceForbiddenArgs { + return args.length === 1 && typeof args[0] === "string"; +} diff --git a/src/common/ipc/index.ts b/src/common/ipc/index.ts index c5e864dc75..f67d794626 100644 --- a/src/common/ipc/index.ts +++ b/src/common/ipc/index.ts @@ -1,4 +1,5 @@ export * from "./ipc"; export * from "./invalid-kubeconfig"; -export * from "./update-available"; +export * from "./update-available.ipc"; +export * from "./cluster.ipc"; export * from "./type-enforced-ipc"; diff --git a/src/common/ipc/update-available/index.ts b/src/common/ipc/update-available.ipc.ts similarity index 84% rename from src/common/ipc/update-available/index.ts rename to src/common/ipc/update-available.ipc.ts index 1e3fcf1268..8571c08512 100644 --- a/src/common/ipc/update-available/index.ts +++ b/src/common/ipc/update-available.ipc.ts @@ -3,10 +3,7 @@ import { UpdateInfo } from "electron-updater"; export const UpdateAvailableChannel = "update-available"; export const AutoUpdateLogPrefix = "[UPDATE-CHECKER]"; -/** - * [, ] - */ -export type UpdateAvailableFromMain = [string, UpdateInfo]; +export type UpdateAvailableFromMain = [backChannel: string, updateInfo: UpdateInfo]; export function areArgsUpdateAvailableFromMain(args: unknown[]): args is UpdateAvailableFromMain { if (args.length !== 2) { @@ -32,7 +29,7 @@ export type BackchannelArg = { now: boolean; }; -export type UpdateAvailableToBackchannel = [BackchannelArg]; +export type UpdateAvailableToBackchannel = [updateDecision: BackchannelArg]; export function areArgsUpdateAvailableToBackchannel(args: unknown[]): args is UpdateAvailableToBackchannel { if (args.length !== 1) { diff --git a/src/main/cluster.ts b/src/main/cluster.ts index 169c99c5a8..19f0945c3e 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -4,9 +4,9 @@ import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api"; import type { WorkspaceId } from "../common/workspace-store"; import { action, comparer, computed, observable, reaction, toJS, when } from "mobx"; import { apiKubePrefix } from "../common/vars"; -import { broadcastMessage, InvalidKubeconfigChannel } from "../common/ipc"; +import { broadcastMessage, InvalidKubeconfigChannel, ClusterListNamespaceForbiddenChannel } from "../common/ipc"; import { ContextHandler } from "./context-handler"; -import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"; +import { AuthorizationV1Api, CoreV1Api, HttpError, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"; import { Kubectl } from "./kubectl"; import { KubeconfigManager } from "./kubeconfig-manager"; import { loadConfig, validateKubeConfig } from "../common/kube-helpers"; @@ -690,10 +690,14 @@ export class Cluster implements ClusterModel, ClusterState { return namespaceList.body.items.map(ns => ns.metadata.name); } catch (error) { const ctx = (await this.getProxyKubeconfig()).getContextObject(this.contextName); + const namespaceList = [ctx.namespace].filter(Boolean); - if (ctx.namespace) return [ctx.namespace]; + if (namespaceList.length === 0 && error instanceof HttpError && error.statusCode === 403) { + logger.info("[CLUSTER]: listing namespaces is forbidden, broadcasting", { clusterId: this.id }); + broadcastMessage(ClusterListNamespaceForbiddenChannel, this.id); + } - return []; + return namespaceList; } } diff --git a/src/renderer/components/+cluster-settings/cluster-settings.tsx b/src/renderer/components/+cluster-settings/cluster-settings.tsx index 4ec8e1ccdd..40472be1ec 100644 --- a/src/renderer/components/+cluster-settings/cluster-settings.tsx +++ b/src/renderer/components/+cluster-settings/cluster-settings.tsx @@ -15,6 +15,7 @@ import { clusterStore } from "../../../common/cluster-store"; import { PageLayout } from "../layout/page-layout"; import { requestMain } from "../../../common/ipc"; import { clusterActivateHandler, clusterRefreshHandler } from "../../../common/cluster-ipc"; +import { navigation } from "../../navigation"; interface Props extends RouteComponentProps { } @@ -30,6 +31,10 @@ export class ClusterSettings extends React.Component { } componentDidMount() { + const { hash } = navigation.location; + + document.getElementById(hash.slice(1))?.scrollIntoView(); + disposeOnUnmount(this, [ reaction(() => this.cluster, this.refreshCluster, { fireImmediately: true, diff --git a/src/renderer/components/+cluster-settings/components/cluster-accessible-namespaces.tsx b/src/renderer/components/+cluster-settings/components/cluster-accessible-namespaces.tsx index b538a59a95..d1ad7dbeef 100644 --- a/src/renderer/components/+cluster-settings/components/cluster-accessible-namespaces.tsx +++ b/src/renderer/components/+cluster-settings/components/cluster-accessible-namespaces.tsx @@ -16,7 +16,7 @@ export class ClusterAccessibleNamespaces extends React.Component { render() { return ( <> - +

This setting is useful for manually specifying which namespaces you have access to. This is useful when you do not have permissions to list namespaces.

{ render() { - const { compact, title, children } = this.props; - let { className } = this.props; - - className = cssNames("SubTitle", className, { + const { className, compact, title, children, id } = this.props; + const classNames = cssNames("SubTitle", className, { compact, }); return ( -
+
{title} {children}
); diff --git a/src/renderer/ipc/index.tsx b/src/renderer/ipc/index.tsx index 5f5e04d9d2..ccbeef4797 100644 --- a/src/renderer/ipc/index.tsx +++ b/src/renderer/ipc/index.tsx @@ -1,10 +1,13 @@ import React from "react"; import { ipcRenderer, IpcRendererEvent } from "electron"; -import { areArgsUpdateAvailableFromMain, UpdateAvailableChannel, onCorrect, UpdateAvailableFromMain, BackchannelArg } from "../../common/ipc"; +import { areArgsUpdateAvailableFromMain, UpdateAvailableChannel, onCorrect, UpdateAvailableFromMain, BackchannelArg, ClusterListNamespaceForbiddenChannel, isListNamespaceForbiddenArgs, ListNamespaceForbiddenArgs } from "../../common/ipc"; import { Notifications, notificationsStore } from "../components/notifications"; import { Button } from "../components/button"; import { isMac } from "../../common/vars"; import { invalidKubeconfigHandler } from "./invalid-kubeconfig-handler"; +import { clusterStore } from "../../common/cluster-store"; +import { navigate } from "../navigation"; +import { clusterSettingsURL } from "../components/+cluster-settings"; function sendToBackchannel(backchannel: string, notificationId: string, data: BackchannelArg): void { notificationsStore.remove(notificationId); @@ -42,7 +45,8 @@ function UpdateAvailableHandler(event: IpcRendererEvent, ...[backchannel, update
- ), { + ), + { id: notificationId, onClose() { sendToBackchannel(backchannel, notificationId, { doUpdate: false }); @@ -51,6 +55,42 @@ function UpdateAvailableHandler(event: IpcRendererEvent, ...[backchannel, update ); } +const listNamespacesForbiddenHandlerDisplayedAt = new Map(); +const intervalBetweenNotifications = 1000 * 60; // 60s + +function ListNamespacesForbiddenHandler(event: IpcRendererEvent, ...[clusterId]: ListNamespaceForbiddenArgs): void { + const lastDisplayedAt = listNamespacesForbiddenHandlerDisplayedAt.get(clusterId); + const wasDisplayed = Boolean(lastDisplayedAt); + const now = Date.now(); + + if (!wasDisplayed || (now - lastDisplayedAt) > intervalBetweenNotifications) { + listNamespacesForbiddenHandlerDisplayedAt.set(clusterId, now); + } else { + // don't bother the user too often + return; + } + + const notificationId = `list-namespaces-forbidden:${clusterId}`; + + Notifications.info( + ( +
+ Add Accessible Namespaces +

Cluster {clusterStore.active.name} does not have permissions to list namespaces. Please add the namespaces you have access to.

+
+
+
+ ), + { + id: notificationId, + } + ); +} + export function registerIpcHandlers() { onCorrect({ source: ipcRenderer, @@ -59,4 +99,10 @@ export function registerIpcHandlers() { verifier: areArgsUpdateAvailableFromMain, }); onCorrect(invalidKubeconfigHandler); + onCorrect({ + source: ipcRenderer, + channel: ClusterListNamespaceForbiddenChannel, + listener: ListNamespacesForbiddenHandler, + verifier: isListNamespaceForbiddenArgs, + }); }