mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Fix: loading in namespaces under reduced RBAC
- don't assume that loading "all namespaces" that lens knows about is the same as loading "all namespaces" that exist on the cluster - Track when actually listing namespaces Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
f14f3b3287
commit
aed51c1ab4
@ -49,6 +49,7 @@ export interface ClusterState {
|
|||||||
allowedNamespaces: string[]
|
allowedNamespaces: string[]
|
||||||
allowedResources: string[]
|
allowedResources: string[]
|
||||||
isGlobalWatchEnabled: boolean;
|
isGlobalWatchEnabled: boolean;
|
||||||
|
listedNamespaces: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -201,6 +202,15 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
* @observable
|
* @observable
|
||||||
*/
|
*/
|
||||||
@observable allowedNamespaces: string[] = [];
|
@observable allowedNamespaces: string[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is true if `allowedNamespaces` was filled by a successful kubeAPI list
|
||||||
|
* namespaces request.
|
||||||
|
*
|
||||||
|
* @observable
|
||||||
|
*/
|
||||||
|
@observable listedNamespaces = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of allowed resources
|
* List of allowed resources
|
||||||
*
|
*
|
||||||
@ -630,6 +640,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
allowedNamespaces: this.allowedNamespaces,
|
allowedNamespaces: this.allowedNamespaces,
|
||||||
allowedResources: this.allowedResources,
|
allowedResources: this.allowedResources,
|
||||||
isGlobalWatchEnabled: this.isGlobalWatchEnabled,
|
isGlobalWatchEnabled: this.isGlobalWatchEnabled,
|
||||||
|
listedNamespaces: this.listedNamespaces,
|
||||||
};
|
};
|
||||||
|
|
||||||
return toJS(state, {
|
return toJS(state, {
|
||||||
@ -669,6 +680,8 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
|
|
||||||
protected async getAllowedNamespaces() {
|
protected async getAllowedNamespaces() {
|
||||||
if (this.accessibleNamespaces.length) {
|
if (this.accessibleNamespaces.length) {
|
||||||
|
this.listedNamespaces = false;
|
||||||
|
|
||||||
return this.accessibleNamespaces;
|
return this.accessibleNamespaces;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -677,8 +690,11 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
try {
|
try {
|
||||||
const namespaceList = await api.listNamespace();
|
const namespaceList = await api.listNamespace();
|
||||||
|
|
||||||
|
this.listedNamespaces = true;
|
||||||
|
|
||||||
return namespaceList.body.items.map(ns => ns.metadata.name);
|
return namespaceList.body.items.map(ns => ns.metadata.name);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
this.listedNamespaces = false;
|
||||||
const ctx = this.getProxyKubeconfig().getContextObject(this.contextName);
|
const ctx = this.getProxyKubeconfig().getContextObject(this.contextName);
|
||||||
|
|
||||||
if (ctx.namespace) return [ctx.namespace];
|
if (ctx.namespace) return [ctx.namespace];
|
||||||
|
|||||||
@ -64,36 +64,51 @@ export class KubeWatchApi {
|
|||||||
let isUnsubscribed = false;
|
let isUnsubscribed = false;
|
||||||
|
|
||||||
const load = (namespaces = subscribingNamespaces) => this.preloadStores(stores, { namespaces, loadOnce });
|
const load = (namespaces = subscribingNamespaces) => this.preloadStores(stores, { namespaces, loadOnce });
|
||||||
let preloading = preload && load();
|
let preloading: boolean | ReturnType<typeof load> = preload && load();
|
||||||
let cancelReloading: IReactionDisposer = noop;
|
let cancelReloading: IReactionDisposer = noop;
|
||||||
|
let ac = new AbortController();
|
||||||
|
|
||||||
const subscribe = () => {
|
const subscribe = async (signal: AbortSignal) => {
|
||||||
if (isUnsubscribed) return;
|
if (isUnsubscribed || signal.aborted) return;
|
||||||
|
|
||||||
stores.forEach((store) => {
|
for (const store of stores) {
|
||||||
unsubscribeList.push(store.subscribe());
|
if (!signal.aborted) {
|
||||||
});
|
unsubscribeList.push(await store.subscribe());
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let subscribeP: Promise<void>;
|
||||||
|
|
||||||
if (preloading) {
|
if (preloading) {
|
||||||
if (waitUntilLoaded) {
|
if (waitUntilLoaded) {
|
||||||
preloading.loading.then(subscribe, error => {
|
subscribeP = preloading.loading
|
||||||
this.log({
|
.then(() => subscribe(ac.signal))
|
||||||
message: new Error("Loading stores has failed"),
|
.catch(error => {
|
||||||
meta: { stores, error, options: opts },
|
this.log({
|
||||||
|
message: new Error("Loading stores has failed"),
|
||||||
|
meta: { stores, error, options: opts },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
subscribe();
|
subscribeP = subscribe(ac.signal);
|
||||||
}
|
}
|
||||||
|
|
||||||
// reload stores only for context namespaces change
|
// reload stores only for context namespaces change
|
||||||
cancelReloading = reaction(() => this.context?.contextNamespaces, namespaces => {
|
cancelReloading = reaction(() => this.context?.selectedNamespaces, namespaces => {
|
||||||
preloading?.cancelLoading();
|
if (typeof preloading === "object") {
|
||||||
unsubscribeList.forEach(unsubscribe => unsubscribe());
|
preloading.cancelLoading();
|
||||||
unsubscribeList.length = 0;
|
}
|
||||||
preloading = load(namespaces);
|
ac.abort();
|
||||||
preloading.loading.then(subscribe);
|
subscribeP.then(() => {
|
||||||
|
unsubscribeList.forEach(unsubscribe => unsubscribe());
|
||||||
|
unsubscribeList.length = 0;
|
||||||
|
|
||||||
|
ac = new AbortController();
|
||||||
|
preloading = load(namespaces);
|
||||||
|
preloading.loading
|
||||||
|
.then(() => subscribeP = subscribe(ac.signal));
|
||||||
|
});
|
||||||
}, {
|
}, {
|
||||||
equals: comparer.shallow,
|
equals: comparer.shallow,
|
||||||
});
|
});
|
||||||
@ -104,9 +119,15 @@ export class KubeWatchApi {
|
|||||||
if (isUnsubscribed) return;
|
if (isUnsubscribed) return;
|
||||||
isUnsubscribed = true;
|
isUnsubscribed = true;
|
||||||
cancelReloading();
|
cancelReloading();
|
||||||
preloading?.cancelLoading();
|
|
||||||
unsubscribeList.forEach(unsubscribe => unsubscribe());
|
if (typeof preloading === "object") {
|
||||||
unsubscribeList.length = 0;
|
preloading.cancelLoading();
|
||||||
|
}
|
||||||
|
ac.abort();
|
||||||
|
subscribeP.then(() => {
|
||||||
|
unsubscribeList.forEach(unsubscribe => unsubscribe());
|
||||||
|
unsubscribeList.length = 0;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -74,7 +74,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async loadFromContextNamespaces(): Promise<void> {
|
async loadFromContextNamespaces(): Promise<void> {
|
||||||
return this.loadAll(namespaceStore.contextNamespaces);
|
return this.loadAll(namespaceStore.selectedNamespaces);
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadItems(namespaces: string[]) {
|
async loadItems(namespaces: string[]) {
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { namespaceStore } from "./namespace.store";
|
|||||||
|
|
||||||
const Placeholder = observer((props: PlaceholderProps<any>) => {
|
const Placeholder = observer((props: PlaceholderProps<any>) => {
|
||||||
const getPlaceholder = (): React.ReactNode => {
|
const getPlaceholder = (): React.ReactNode => {
|
||||||
const namespaces = namespaceStore.contextNamespaces;
|
const namespaces = namespaceStore.selectedNamespaces;
|
||||||
|
|
||||||
switch (namespaces.length) {
|
switch (namespaces.length) {
|
||||||
case 0:
|
case 0:
|
||||||
|
|||||||
@ -65,7 +65,11 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private autoLoadAllowedNamespaces(): IReactionDisposer {
|
private autoLoadAllowedNamespaces(): IReactionDisposer {
|
||||||
return reaction(() => this.allowedNamespaces, namespaces => this.loadAll({ namespaces }), {
|
return reaction(() => this.allowedNamespaces, namespaces => {
|
||||||
|
console.log("autoLoadAllowedNamespaces", namespaces);
|
||||||
|
|
||||||
|
return this.loadAll({ namespaces });
|
||||||
|
}, {
|
||||||
fireImmediately: true,
|
fireImmediately: true,
|
||||||
equals: comparer.shallow,
|
equals: comparer.shallow,
|
||||||
});
|
});
|
||||||
@ -98,7 +102,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|||||||
].flat()));
|
].flat()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get contextNamespaces(): string[] {
|
@computed get selectedNamespaces(): string[] {
|
||||||
const namespaces = Array.from(this.contextNs);
|
const namespaces = Array.from(this.contextNs);
|
||||||
|
|
||||||
if (!namespaces.length) {
|
if (!namespaces.length) {
|
||||||
@ -108,9 +112,11 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|||||||
return namespaces;
|
return namespaces;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSubscribeApis() {
|
async getSubscribeApis() {
|
||||||
|
await this.contextReady;
|
||||||
|
|
||||||
// if user has given static list of namespaces let's not start watches because watch adds stuff that's not wanted
|
// if user has given static list of namespaces let's not start watches because watch adds stuff that's not wanted
|
||||||
if (this.context?.cluster.accessibleNamespaces.length > 0) {
|
if (this.context.cluster.accessibleNamespaces.length > 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { apiManager } from "../../api/api-manager";
|
|||||||
export class RoleBindingsStore extends KubeObjectStore<RoleBinding> {
|
export class RoleBindingsStore extends KubeObjectStore<RoleBinding> {
|
||||||
api = clusterRoleBindingApi;
|
api = clusterRoleBindingApi;
|
||||||
|
|
||||||
getSubscribeApis() {
|
async getSubscribeApis() {
|
||||||
return [clusterRoleBindingApi, roleBindingApi];
|
return [clusterRoleBindingApi, roleBindingApi];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { apiManager } from "../../api/api-manager";
|
|||||||
export class RolesStore extends KubeObjectStore<Role> {
|
export class RolesStore extends KubeObjectStore<Role> {
|
||||||
api = clusterRoleApi;
|
api = clusterRoleApi;
|
||||||
|
|
||||||
getSubscribeApis() {
|
async getSubscribeApis() {
|
||||||
return [roleApi, clusterRoleApi];
|
return [roleApi, clusterRoleApi];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -26,7 +26,7 @@ export class OverviewStatuses extends React.Component {
|
|||||||
@autobind()
|
@autobind()
|
||||||
renderWorkload(resource: KubeResource): React.ReactElement {
|
renderWorkload(resource: KubeResource): React.ReactElement {
|
||||||
const store = workloadStores[resource];
|
const store = workloadStores[resource];
|
||||||
const items = store.getAllByNs(namespaceStore.contextNamespaces);
|
const items = store.getAllByNs(namespaceStore.selectedNamespaces);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="workload" key={resource}>
|
<div className="workload" key={resource}>
|
||||||
|
|||||||
@ -30,7 +30,7 @@ export class WorkloadsOverview extends React.Component<Props> {
|
|||||||
jobStore, cronJobStore, eventStore,
|
jobStore, cronJobStore, eventStore,
|
||||||
], {
|
], {
|
||||||
preload: true,
|
preload: true,
|
||||||
namespaces: clusterContext.contextNamespaces,
|
namespaces: clusterContext.selectedNamespaces,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { namespaceStore } from "./+namespaces/namespace.store";
|
|||||||
export interface ClusterContext {
|
export interface ClusterContext {
|
||||||
cluster?: Cluster;
|
cluster?: Cluster;
|
||||||
allNamespaces?: string[]; // available / allowed namespaces from cluster.ts
|
allNamespaces?: string[]; // available / allowed namespaces from cluster.ts
|
||||||
contextNamespaces?: string[]; // selected by user (see: namespace-select.tsx)
|
selectedNamespaces?: string[]; // selected by user (see: namespace-select.tsx)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const clusterContext: ClusterContext = {
|
export const clusterContext: ClusterContext = {
|
||||||
@ -17,7 +17,7 @@ export const clusterContext: ClusterContext = {
|
|||||||
return this.cluster?.allowedNamespaces ?? [];
|
return this.cluster?.allowedNamespaces ?? [];
|
||||||
},
|
},
|
||||||
|
|
||||||
get contextNamespaces(): string[] {
|
get selectedNamespaces(): string[] {
|
||||||
return namespaceStore.contextNamespaces ?? [];
|
return namespaceStore.selectedNamespaces ?? [];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -140,7 +140,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
const stores = Array.from(new Set([store, ...dependentStores]));
|
const stores = Array.from(new Set([store, ...dependentStores]));
|
||||||
|
|
||||||
// load context namespaces by default (see also: `<NamespaceSelectFilter/>`)
|
// load context namespaces by default (see also: `<NamespaceSelectFilter/>`)
|
||||||
stores.forEach(store => store.loadAll(namespaceStore.contextNamespaces));
|
stores.forEach(store => store.loadAll(namespaceStore.selectedNamespaces));
|
||||||
}
|
}
|
||||||
|
|
||||||
private filterCallbacks: { [type: string]: ItemsFilter } = {
|
private filterCallbacks: { [type: string]: ItemsFilter } = {
|
||||||
|
|||||||
@ -28,7 +28,7 @@ export class KubeObjectListLayout extends React.Component<KubeObjectListLayoutPr
|
|||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
kubeWatchApi.subscribeStores(stores, {
|
kubeWatchApi.subscribeStores(stores, {
|
||||||
preload: true,
|
preload: true,
|
||||||
namespaces: clusterContext.contextNamespaces,
|
namespaces: clusterContext.selectedNamespaces,
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { ClusterContext } from "./components/context";
|
import type { ClusterContext } from "./components/context";
|
||||||
|
|
||||||
import { action, computed, observable, reaction, when } from "mobx";
|
import { action, computed, observable, reaction, when } from "mobx";
|
||||||
import { autobind } from "./utils";
|
import { autobind, Disposer } from "./utils";
|
||||||
import { KubeObject, KubeStatus } from "./api/kube-object";
|
import { KubeObject, KubeStatus } from "./api/kube-object";
|
||||||
import { IKubeWatchEvent } from "./api/kube-watch-api";
|
import { IKubeWatchEvent } from "./api/kube-watch-api";
|
||||||
import { ItemStore } from "./item.store";
|
import { ItemStore } from "./item.store";
|
||||||
@ -35,7 +35,7 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
@computed get contextItems(): T[] {
|
@computed get contextItems(): T[] {
|
||||||
const namespaces = this.context?.contextNamespaces ?? [];
|
const namespaces = this.context?.selectedNamespaces ?? [];
|
||||||
|
|
||||||
return this.items.filter(item => {
|
return this.items.filter(item => {
|
||||||
const itemNamespace = item.getNs();
|
const itemNamespace = item.getNs();
|
||||||
@ -111,7 +111,7 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
|
|
||||||
const isLoadingAll = this.context.allNamespaces.every(ns => namespaces.includes(ns));
|
const isLoadingAll = this.context.allNamespaces.every(ns => namespaces.includes(ns));
|
||||||
|
|
||||||
if (isLoadingAll) {
|
if (isLoadingAll && this.context.cluster?.listedNamespaces) {
|
||||||
this.loadedNamespaces = [];
|
this.loadedNamespaces = [];
|
||||||
|
|
||||||
return api.list({}, this.query);
|
return api.list({}, this.query);
|
||||||
@ -264,11 +264,15 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getSubscribeApis(): KubeApi[] {
|
async getSubscribeApis(): Promise<KubeApi[]> {
|
||||||
return [this.api];
|
return [this.api];
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe(apis = this.getSubscribeApis()) {
|
async subscribe(apis?: KubeApi[]): Promise<Disposer> {
|
||||||
|
|
||||||
|
await this.contextReady;
|
||||||
|
apis ??= await this.getSubscribeApis();
|
||||||
|
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
const namespaces = [...this.loadedNamespaces];
|
const namespaces = [...this.loadedNamespaces];
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
export const isElectron = !!navigator.userAgent.match(/Electron/);
|
export const isElectron = !!navigator.userAgent.match(/Electron/);
|
||||||
|
|
||||||
|
export type Disposer = () => void;
|
||||||
|
|
||||||
export * from "../../common/utils";
|
export * from "../../common/utils";
|
||||||
|
|
||||||
export * from "./cssVar";
|
export * from "./cssVar";
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user