1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

loading k8s resources into stores per selected namespaces -- part 2

- fix: generating helm chart id

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2021-01-07 22:37:23 +02:00
parent 47bea52160
commit f71829ff14
8 changed files with 112 additions and 47 deletions

View File

@ -86,7 +86,7 @@ export class HelmChart {
tillerVersion?: string; tillerVersion?: string;
getId() { getId() {
return this.digest; return `${this.apiVersion}/${this.name}@${this.getAppVersion()}`;
} }
getName() { getName() {

View File

@ -62,27 +62,37 @@ export class KubeWatchApi {
}); });
} }
protected getQuery(): Partial<IKubeWatchRouteQuery> { // FIXME: use POST to send apis for subscribing (list could be huge)
const { isAdmin, allowedNamespaces } = getHostedCluster(); // TODO: try to use normal fetch res.body stream to consume watch-api updates
// https://github.com/lensapp/lens/issues/1898
protected async getQuery() {
const { namespaceStore } = await import("../components/+namespaces/namespace.store");
await namespaceStore.whenReady;
const { isAdmin } = getHostedCluster();
return { return {
api: this.activeApis.map(api => { api: this.activeApis.map(api => {
if (isAdmin) return api.getWatchUrl(); if (isAdmin && !api.isNamespaced) {
return api.getWatchUrl();
return allowedNamespaces.map(namespace => api.getWatchUrl(namespace)); }
if (api.isNamespaced) {
return namespaceStore.getContextNamespaces().map(namespace => api.getWatchUrl(namespace));
}
return [];
}).flat() }).flat()
}; };
} }
// todo: maybe switch to websocket to avoid often reconnects // todo: maybe switch to websocket to avoid often reconnects
@autobind() @autobind()
protected connect() { protected async connect() {
if (this.evtSource) this.disconnect(); // close previous connection if (this.evtSource) this.disconnect(); // close previous connection
if (!this.activeApis.length) { const query = await this.getQuery();
if (!this.activeApis.length || !query.api.length) {
return; return;
} }
const query = this.getQuery();
const apiUrl = `${apiPrefix}/watch?${stringify(query)}`; const apiUrl = `${apiPrefix}/watch?${stringify(query)}`;
this.evtSource = new EventSource(apiUrl); this.evtSource = new EventSource(apiUrl);

View File

@ -5,7 +5,7 @@ import { HelmRelease, helmReleasesApi, IReleaseCreatePayload, IReleaseUpdatePayl
import { ItemStore } from "../../item.store"; import { ItemStore } from "../../item.store";
import { Secret } from "../../api/endpoints"; import { Secret } from "../../api/endpoints";
import { secretsStore } from "../+config-secrets/secrets.store"; import { secretsStore } from "../+config-secrets/secrets.store";
import { getHostedCluster } from "../../../common/cluster-store"; import { namespaceStore } from "../+namespaces/namespace.store";
@autobind() @autobind()
export class ReleaseStore extends ItemStore<HelmRelease> { export class ReleaseStore extends ItemStore<HelmRelease> {
@ -60,31 +60,25 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
@action @action
async loadAll() { async loadAll() {
this.isLoading = true; this.isLoading = true;
let items; let items: HelmRelease[];
try { try {
const { isAdmin, allowedNamespaces } = getHostedCluster(); items = await this.loadItems(namespaceStore.getContextNamespaces());
items = await this.loadItems(!isAdmin ? allowedNamespaces : null);
} finally {
if (items) {
items = this.sortItems(items); items = this.sortItems(items);
this.items.replace(items); this.items.replace(items);
}
this.isLoaded = true; this.isLoaded = true;
} catch (error) {
console.error(`Loading Helm Chart releases has failed: ${error}`);
} finally {
this.isLoading = false; this.isLoading = false;
} }
} }
async loadItems(namespaces?: string[]) { async loadItems(namespaces: string[]) {
if (!namespaces) {
return helmReleasesApi.list();
} else {
return Promise return Promise
.all(namespaces.map(namespace => helmReleasesApi.list(namespace))) .all(namespaces.map(namespace => helmReleasesApi.list(namespace)))
.then(items => items.flat()); .then(items => items.flat());
} }
}
async create(payload: IReleaseCreatePayload) { async create(payload: IReleaseCreatePayload) {
const response = await helmReleasesApi.create(payload); const response = await helmReleasesApi.create(payload);

View File

@ -1,11 +1,11 @@
import { action, comparer, observable, reaction } from "mobx"; import { action, comparer, observable, reaction, when } from "mobx";
import { autobind, createStorage } from "../../utils"; import { autobind, createStorage } from "../../utils";
import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store"; import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store";
import { Namespace, namespacesApi } from "../../api/endpoints/namespaces.api"; import { Namespace, namespacesApi } from "../../api/endpoints/namespaces.api";
import { createPageParam } from "../../navigation"; import { createPageParam } from "../../navigation";
import { apiManager } from "../../api/api-manager"; import { apiManager } from "../../api/api-manager";
import { isAllowedResource } from "../../../common/rbac"; import { isAllowedResource } from "../../../common/rbac";
import { getHostedCluster } from "../../../common/cluster-store"; import { clusterStore, getHostedCluster } from "../../../common/cluster-store";
const storage = createStorage<string[]>("context_namespaces", []); const storage = createStorage<string[]>("context_namespaces", []);
@ -21,14 +21,25 @@ export const namespaceUrlParam = createPageParam<string[]>({
@autobind() @autobind()
export class NamespaceStore extends KubeObjectStore<Namespace> { export class NamespaceStore extends KubeObjectStore<Namespace> {
api = namespacesApi; api = namespacesApi;
contextNs = observable.array<string>();
@observable contextNs = observable.array<string>();
@observable isReady = false;
whenReady = when(() => this.isReady);
constructor() { constructor() {
super(); super();
this.init(); this.init();
} }
private init() { private async init() {
await clusterStore.whenLoaded;
if (!getHostedCluster()) return;
await getHostedCluster().whenReady; // wait for cluster-state from main
await this.loadAll(); // auto-load allowed namespaces
this.isReady = true;
this.setContext(this.initNamespaces); this.setContext(this.initNamespaces);
return reaction(() => this.contextNs.toJS(), namespaces => { return reaction(() => this.contextNs.toJS(), namespaces => {
@ -40,13 +51,52 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
}); });
} }
get initNamespaces() { get allowedNamespaces(): string[] {
return namespaceUrlParam.get(); return getHostedCluster().allowedNamespaces;
} }
// FIXME: page/app reload doesn't restore previously selected namespaces
get initNamespaces() {
const allowedNamespaces = new Set(this.allowedNamespaces);
const lastUsedNamespaces = new Set(storage.get());
// remove previously saved, but currently disallowed namespaces
lastUsedNamespaces.forEach(namespace => {
if (!allowedNamespaces.has(namespace)) {
lastUsedNamespaces.delete(namespace);
}
});
// return previously saved and currently allowed namespaces
if (lastUsedNamespaces.size) {
return Array.from(lastUsedNamespaces);
}
// otherwise select "default" or first allowed namespace
else {
if (allowedNamespaces.has("default")) {
return ["default"];
} else if (allowedNamespaces.size) {
return [Array.from(allowedNamespaces)[0]];
}
}
return [];
}
getContextNamespaces(): string[] {
let namespaces = this.contextNs.toJS();
if (!namespaces.length) {
return [...this.allowedNamespaces]; // show all namespaces when nothing selected
}
return namespaces;
}
/**
* @deprecated
*/
getContextParams() { getContextParams() {
return { return {
namespaces: this.contextNs.toJS(), namespaces: this.getContextNamespaces(),
}; };
} }
@ -61,6 +111,12 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
return super.subscribe(apis); return super.subscribe(apis);
} }
async loadAll() {
return super.loadAll({
namespaces: this.allowedNamespaces,
});
}
protected async loadItems({ isAdmin, namespaces }: KubeObjectStoreLoadingParams) { protected async loadItems({ isAdmin, namespaces }: KubeObjectStoreLoadingParams) {
if (!isAllowedResource("namespaces")) { if (!isAllowedResource("namespaces")) {
return namespaces.map(this.getDummyNamespace); return namespaces.map(this.getDummyNamespace);

View File

@ -27,7 +27,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.contextNs); const items = store.getAllByNs(namespaceStore.getContextNamespaces());
return ( return (
<div className="workload" key={resource}> <div className="workload" key={resource}>

View File

@ -110,6 +110,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
]); ]);
} }
// FIXME: reload and re-subscribe stores when context namespaces changed
async componentDidMount() { async componentDidMount() {
const { store, dependentStores, isClusterScoped } = this.props; const { store, dependentStores, isClusterScoped } = this.props;
const stores = [store, ...dependentStores]; const stores = [store, ...dependentStores];

View File

@ -9,6 +9,7 @@ export interface ItemObject {
@autobind() @autobind()
export abstract class ItemStore<T extends ItemObject = ItemObject> { export abstract class ItemStore<T extends ItemObject = ItemObject> {
abstract loadAll(...args: any[]): any;
abstract loadAll(): Promise<void>; abstract loadAll(): Promise<void>;
protected defaultSorting = (item: T) => item.getName(); protected defaultSorting = (item: T) => item.getName();
@ -40,8 +41,7 @@ export abstract class ItemStore<T extends ItemObject = ItemObject> {
if (item) { if (item) {
return item; return item;
} } else {
else {
const items = this.sortItems([...this.items, newItem]); const items = this.sortItems([...this.items, newItem]);
this.items.replace(items); this.items.replace(items);
@ -83,8 +83,7 @@ export abstract class ItemStore<T extends ItemObject = ItemObject> {
const index = this.items.findIndex(item => item === existingItem); const index = this.items.findIndex(item => item === existingItem);
this.items.splice(index, 1, item); this.items.splice(index, 1, item);
} } else {
else {
let items = [...this.items, item]; let items = [...this.items, item];
if (sortItems) items = this.sortItems(items); if (sortItems) items = this.sortItems(items);
@ -130,8 +129,7 @@ export abstract class ItemStore<T extends ItemObject = ItemObject> {
toggleSelection(item: T) { toggleSelection(item: T) {
if (this.isSelected(item)) { if (this.isSelected(item)) {
this.unselect(item); this.unselect(item);
} } else {
else {
this.select(item); this.select(item);
} }
} }
@ -142,8 +140,7 @@ export abstract class ItemStore<T extends ItemObject = ItemObject> {
if (allSelected) { if (allSelected) {
visibleItems.forEach(this.unselect); visibleItems.forEach(this.unselect);
} } else {
else {
visibleItems.forEach(this.select); visibleItems.forEach(this.select);
} }
} }

View File

@ -93,13 +93,20 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
} }
@action @action
async loadAll() { async loadAll(params: { namespaces?: string[] } = {}) {
this.isLoading = true; this.isLoading = true;
let items: T[]; let items: T[];
try { try {
const { allowedNamespaces: namespaces, isAdmin } = getHostedCluster(); const { namespaceStore } = await import("./components/+namespaces/namespace.store");
items = await this.loadItems({ isAdmin, namespaces, api: this.api }); const contextNamespaces = params.namespaces || namespaceStore.getContextNamespaces();
items = await this.loadItems({
isAdmin: getHostedCluster().isAdmin,
namespaces: contextNamespaces,
api: this.api,
});
items = this.filterItemsOnLoad(items); items = this.filterItemsOnLoad(items);
items = this.sortItems(items); items = this.sortItems(items);
this.items.replace(items); this.items.replace(items);