Refactor watches to use native k8s api (#2095)

* fix lint

Signed-off-by: Roman <ixrock@gmail.com>

* fixes & refactoring

Signed-off-by: Roman <ixrock@gmail.com>

* fix lint, micro-refactoring

Signed-off-by: Roman <ixrock@gmail.com>

* more refactoring, clean up, responding to comments

Signed-off-by: Roman <ixrock@gmail.com>

* fix: remove extra check for cluster.allowedApi from processing buffered watch-api events

Signed-off-by: Roman <ixrock@gmail.com>

* refactoring, detaching NamespaceStore from KubeObjectStore

Signed-off-by: Roman <ixrock@gmail.com>

* fix: wait for contextReady in NamespaceStore

Signed-off-by: Roman <ixrock@gmail.com>

* refactoring & fixes

Signed-off-by: Roman <ixrock@gmail.com>

* fix lint

Signed-off-by: Roman <ixrock@gmail.com>

* fixes: reloading context stores on NamespaceSelect-change

Signed-off-by: Roman <ixrock@gmail.com>

* optimize loading all resources when "all namespaces" selected -> single request per resource (when have rights)

Signed-off-by: Roman <ixrock@gmail.com>

* use native k8s api watches

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* retry watch when it makes sense

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* workaround for browser connection limits

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* cleanup

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* cleanup

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* use always random subdomain for getResponse

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* resubscribe stores on contextNamespace change

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* modify watch event before calling callback

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

Co-authored-by: Roman <ixrock@gmail.com>
This commit is contained in:
Jari KolehmainenandRoman authored and GitHub committed 2021-02-09 15:31:15 +02:00
1 parent 6ded5e73fa
commit 035dd470ef
34 files changed
+414 -599

No files matched your search

@@ -73,8 +73,8 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
}
}
async loadSelectedNamespaces(): Promise<void> {
return this.loadAll(namespaceStore.getContextNamespaces());
async loadFromContextNamespaces(): Promise<void> {
return this.loadAll(namespaceStore.contextNamespaces);
}
async loadItems(namespaces: string[]) {
@@ -86,7 +86,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
async create(payload: IReleaseCreatePayload) {
const response = await helmReleasesApi.create(payload);
if (this.isLoaded) this.loadSelectedNamespaces();
if (this.isLoaded) this.loadFromContextNamespaces();
return response;
}
@@ -94,7 +94,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
async update(name: string, namespace: string, payload: IReleaseUpdatePayload) {
const response = await helmReleasesApi.update(name, namespace, payload);
if (this.isLoaded) this.loadSelectedNamespaces();
if (this.isLoaded) this.loadFromContextNamespaces();
return response;
}
@@ -102,7 +102,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
async rollback(name: string, namespace: string, revision: number) {
const response = await helmReleasesApi.rollback(name, namespace, revision);
if (this.isLoaded) this.loadSelectedNamespaces();
if (this.isLoaded) this.loadFromContextNamespaces();
return response;
}
@@ -30,7 +30,7 @@ export class CrdResources extends React.Component<Props> {
const { store } = this;
if (store && !store.isLoading && !store.isLoaded) {
store.loadSelectedNamespaces();
store.reloadAll();
}
})
]);
@@ -97,7 +97,7 @@ export class CrdResources extends React.Component<Props> {
...extraColumns.map((column) => {
let value = jsonPath.value(crdInstance, parseJsonPath(column.jsonPath.slice(1)));
if (Array.isArray(value) || typeof value === "object") {
if (Array.isArray(value) || typeof value === "object") {
value = JSON.stringify(value);
}
@@ -14,7 +14,7 @@ export interface KubeEventDetailsProps {
@observer
export class KubeEventDetails extends React.Component<KubeEventDetailsProps> {
async componentDidMount() {
eventStore.loadSelectedNamespaces();
eventStore.reloadAll();
}
render() {
@@ -32,8 +32,8 @@ export class NamespaceDetails extends React.Component<Props> {
}
componentDidMount() {
resourceQuotaStore.loadSelectedNamespaces();
limitRangeStore.loadSelectedNamespaces();
resourceQuotaStore.reloadAll();
limitRangeStore.reloadAll();
}
render() {
@@ -82,7 +82,7 @@ export class NamespaceSelect extends React.Component<Props> {
@observer
export class NamespaceSelectFilter extends React.Component {
@computed get placeholder(): React.ReactNode {
const namespaces = namespaceStore.getContextNamespaces();
const namespaces = namespaceStore.contextNamespaces;
switch (namespaces.length) {
case 0:
@@ -1,10 +1,9 @@
import { action, comparer, computed, IReactionDisposer, IReactionOptions, observable, reaction, toJS, when } from "mobx";
import { action, comparer, computed, IReactionDisposer, IReactionOptions, observable, reaction } from "mobx";
import { autobind, createStorage } from "../../utils";
import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store";
import { Namespace, namespacesApi } from "../../api/endpoints/namespaces.api";
import { createPageParam } from "../../navigation";
import { apiManager } from "../../api/api-manager";
import { clusterStore, getHostedCluster } from "../../../common/cluster-store";
const storage = createStorage<string[]>("context_namespaces", []);
@@ -35,9 +34,6 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
api = namespacesApi;
@observable private contextNs = observable.set<string>();
@observable isReady = false;
whenReady = when(() => this.isReady);
constructor() {
super();
@@ -45,15 +41,11 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
}
private async init() {
await clusterStore.whenLoaded;
if (!getHostedCluster()) return;
await getHostedCluster().whenReady; // wait for cluster-state from main
await this.contextReady;
this.setContext(this.initialNamespaces);
this.autoLoadAllowedNamespaces();
this.autoUpdateUrlAndLocalStorage();
this.isReady = true;
}
public onContextChange(callback: (contextNamespaces: string[]) => void, opts: IReactionOptions = {}): IReactionDisposer {
@@ -73,16 +65,12 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
}
private autoLoadAllowedNamespaces(): IReactionDisposer {
return reaction(() => this.allowedNamespaces, namespaces => this.loadAll(namespaces), {
return reaction(() => this.allowedNamespaces, namespaces => this.loadAll({ namespaces }), {
fireImmediately: true,
equals: comparer.shallow,
});
}
@computed get allowedNamespaces(): string[] {
return toJS(getHostedCluster().allowedNamespaces);
}
@computed
private get initialNamespaces(): string[] {
const namespaces = new Set(this.allowedNamespaces);
@@ -103,27 +91,26 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
return [];
}
getContextNamespaces(): string[] {
@computed get allowedNamespaces(): string[] {
return Array.from(new Set([
...(this.context?.allNamespaces ?? []), // allowed namespaces from cluster (main), updating every 30s
...this.items.map(item => item.getName()), // loaded namespaces from k8s api
].flat()));
}
@computed get contextNamespaces(): string[] {
const namespaces = Array.from(this.contextNs);
// show all namespaces when nothing selected
if (!namespaces.length) {
// return actual namespaces list since "allowedNamespaces" updating every 30s in cluster and thus might be stale
if (this.isLoaded) {
return this.items.map(namespace => namespace.getName());
}
return this.allowedNamespaces;
return this.allowedNamespaces; // show all namespaces when nothing selected
}
return namespaces;
}
getSubscribeApis() {
const { accessibleNamespaces } = getHostedCluster();
// if user has given static list of namespaces let's not start watches because watch adds stuff that's not wanted
if (accessibleNamespaces.length > 0) {
if (this.context?.cluster.accessibleNamespaces.length > 0) {
return [];
}
@@ -29,7 +29,7 @@ export class NodeDetails extends React.Component<Props> {
});
async componentDidMount() {
podsStore.loadSelectedNamespaces();
podsStore.reloadAll();
}
componentWillUnmount() {
@@ -7,7 +7,7 @@ import { Dialog, DialogProps } from "../dialog";
import { Wizard, WizardStep } from "../wizard";
import { Select, SelectOption } from "../select";
import { SubTitle } from "../layout/sub-title";
import { IRoleBindingSubject, RoleBinding, ServiceAccount, Role } from "../../api/endpoints";
import { IRoleBindingSubject, Role, RoleBinding, ServiceAccount } from "../../api/endpoints";
import { Icon } from "../icon";
import { Input } from "../input";
import { NamespaceSelect } from "../+namespaces/namespace-select";
@@ -19,6 +19,7 @@ import { namespaceStore } from "../+namespaces/namespace.store";
import { serviceAccountsStore } from "../+user-management-service-accounts/service-accounts.store";
import { roleBindingsStore } from "./role-bindings.store";
import { showDetails } from "../kube-object";
import { KubeObjectStore } from "../../kube-object.store";
interface BindingSelectOption extends SelectOption {
value: string; // binding name
@@ -73,14 +74,14 @@ export class AddRoleBindingDialog extends React.Component<Props> {
};
async loadData() {
const stores = [
const stores: KubeObjectStore[] = [
namespaceStore,
rolesStore,
serviceAccountsStore,
];
this.isLoading = true;
await Promise.all(stores.map(store => store.loadSelectedNamespaces()));
await Promise.all(stores.map(store => store.reloadAll()));
this.isLoading = false;
}
@@ -136,8 +137,7 @@ export class AddRoleBindingDialog extends React.Component<Props> {
roleBinding: this.roleBinding,
addSubjects: subjects,
});
}
else {
} else {
const name = useRoleForBindingName ? selectedRole.getName() : bindingName;
roleBinding = await roleBindingsStore.create({ name, namespace }, {
@@ -265,7 +265,7 @@ export class AddRoleBindingDialog extends React.Component<Props> {
</h5>
);
const disableNext = this.isLoading || !selectedRole || !selectedBindings.length;
const nextLabel = isEditing ? "Update" : "Create";
const nextLabel = isEditing ? "Update" : "Create";
return (
<Dialog
@@ -20,7 +20,7 @@ interface Props extends KubeObjectDetailsProps<CronJob> {
@observer
export class CronJobDetails extends React.Component<Props> {
async componentDidMount() {
jobStore.loadSelectedNamespaces();
jobStore.reloadAll();
}
render() {
@@ -30,7 +30,7 @@ export class DaemonSetDetails extends React.Component<Props> {
});
componentDidMount() {
podsStore.loadSelectedNamespaces();
podsStore.reloadAll();
}
componentWillUnmount() {
@@ -31,7 +31,7 @@ export class DeploymentDetails extends React.Component<Props> {
});
componentDidMount() {
podsStore.loadSelectedNamespaces();
podsStore.reloadAll();
}
componentWillUnmount() {
@@ -25,7 +25,7 @@ interface Props extends KubeObjectDetailsProps<Job> {
@observer
export class JobDetails extends React.Component<Props> {
async componentDidMount() {
podsStore.loadSelectedNamespaces();
podsStore.reloadAll();
}
render() {
@@ -6,7 +6,6 @@ import { OverviewWorkloadStatus } from "./overview-workload-status";
import { Link } from "react-router-dom";
import { workloadURL, workloadStores } from "../+workloads";
import { namespaceStore } from "../+namespaces/namespace.store";
import { PageFiltersList } from "../item-object-list/page-filters-list";
import { NamespaceSelectFilter } from "../+namespaces/namespace-select";
import { isAllowedResource, KubeResource } from "../../../common/rbac";
import { ResourceNames } from "../../../renderer/utils/rbac";
@@ -27,7 +26,7 @@ export class OverviewStatuses extends React.Component {
@autobind()
renderWorkload(resource: KubeResource): React.ReactElement {
const store = workloadStores[resource];
const items = store.getAllByNs(namespaceStore.getContextNamespaces());
const items = store.getAllByNs(namespaceStore.contextNamespaces);
return (
<div className="workload" key={resource}>
@@ -50,7 +49,6 @@ export class OverviewStatuses extends React.Component {
<h5 className="box grow">Overview</h5>
<NamespaceSelectFilter />
</div>
<PageFiltersList />
<div className="workloads">
{workloads}
</div>
@@ -16,6 +16,7 @@ import { cronJobStore } from "../+workloads-cronjobs/cronjob.store";
import { Events } from "../+events";
import { isAllowedResource } from "../../../common/rbac";
import { kubeWatchApi } from "../../api/kube-watch-api";
import { clusterContext } from "../context";
interface Props extends RouteComponentProps<IWorkloadsOverviewRouteParams> {
}
@@ -29,6 +30,7 @@ export class WorkloadsOverview extends React.Component<Props> {
jobStore, cronJobStore, eventStore,
], {
preload: true,
namespaces: clusterContext.contextNamespaces,
}),
]);
}
@@ -29,7 +29,7 @@ export class ReplicaSetDetails extends React.Component<Props> {
});
async componentDidMount() {
podsStore.loadSelectedNamespaces();
podsStore.reloadAll();
}
componentWillUnmount() {
@@ -30,7 +30,7 @@ export class StatefulSetDetails extends React.Component<Props> {
});
componentDidMount() {
podsStore.loadSelectedNamespaces();
podsStore.reloadAll();
}
componentWillUnmount() {
+8 -9
View File
@@ -43,12 +43,13 @@ import { ClusterPageMenuRegistration, clusterPageMenuRegistry } from "../../exte
import { TabLayout, TabLayoutRoute } from "./layout/tab-layout";
import { StatefulSetScaleDialog } from "./+workloads-statefulsets/statefulset-scale-dialog";
import { eventStore } from "./+events/event.store";
import { namespaceStore } from "./+namespaces/namespace.store";
import { nodesStore } from "./+nodes/nodes.store";
import { podsStore } from "./+workloads-pods/pods.store";
import { kubeWatchApi } from "../api/kube-watch-api";
import { ReplicaSetScaleDialog } from "./+workloads-replicasets/replicaset-scale-dialog";
import { CommandContainer } from "./command-palette/command-container";
import { KubeObjectStore } from "../kube-object.store";
import { clusterContext } from "./context";
@observer
export class App extends React.Component {
@@ -76,11 +77,9 @@ export class App extends React.Component {
});
whatInput.ask(); // Start to monitor user input device
await namespaceStore.whenReady;
await kubeWatchApi.init({
getCluster: getHostedCluster,
getNamespaces: namespaceStore.getContextNamespaces,
});
// Setup hosted cluster context
KubeObjectStore.defaultContext = clusterContext;
kubeWatchApi.context = clusterContext;
}
componentDidMount() {
@@ -163,9 +162,9 @@ export class App extends React.Component {
const tabRoutes = this.getTabLayoutRoutes(menu);
if (tabRoutes.length > 0) {
const pageComponent = () => <TabLayout tabs={tabRoutes} />;
const pageComponent = () => <TabLayout tabs={tabRoutes}/>;
route = <Route key={`extension-tab-layout-route-${index}`} component={pageComponent} path={tabRoutes.map((tab) => tab.routePath)} />;
route = <Route key={`extension-tab-layout-route-${index}`} component={pageComponent} path={tabRoutes.map((tab) => tab.routePath)}/>;
this.extensionRoutes.set(menu, route);
} else {
const page = clusterPageRegistry.getByPageTarget(menu.target);
@@ -229,7 +228,7 @@ export class App extends React.Component {
<StatefulSetScaleDialog/>
<ReplicaSetScaleDialog/>
<CronJobTriggerDialog/>
<CommandContainer cluster={cluster} />
<CommandContainer cluster={cluster}/>
</ErrorBoundary>
</Router>
);
+23
View File
@@ -0,0 +1,23 @@
import type { Cluster } from "../../main/cluster";
import { getHostedCluster } from "../../common/cluster-store";
import { namespaceStore } from "./+namespaces/namespace.store";
export interface ClusterContext {
cluster?: Cluster;
allNamespaces?: string[]; // available / allowed namespaces from cluster.ts
contextNamespaces?: string[]; // selected by user (see: namespace-select.tsx)
}
export const clusterContext: ClusterContext = {
get cluster(): Cluster | null {
return getHostedCluster();
},
get allNamespaces(): string[] {
return this.cluster?.allowedNamespaces ?? [];
},
get contextNamespaces(): string[] {
return namespaceStore.contextNamespaces ?? [];
},
};
@@ -80,7 +80,7 @@ export class UpgradeChartStore extends DockTabStore<IChartUpgradeData> {
const values = this.values.getData(tabId);
await Promise.all([
!releaseStore.isLoaded && releaseStore.loadSelectedNamespaces(),
!releaseStore.isLoaded && releaseStore.loadFromContextNamespaces(),
!values && this.loadValues(tabId)
]);
}
@@ -38,6 +38,7 @@ interface IHeaderPlaceholders {
export interface ItemListLayoutProps<T extends ItemObject = ItemObject> {
tableId?: string;
className: IClassName;
items?: T[];
store: ItemStore<T>;
dependentStores?: ItemStore[];
preloadStores?: boolean;
@@ -138,7 +139,8 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
const { store, dependentStores } = this.props;
const stores = Array.from(new Set([store, ...dependentStores]));
stores.forEach(store => store.loadAll(namespaceStore.getContextNamespaces()));
// load context namespaces by default (see also: `<NamespaceSelectFilter/>`)
stores.forEach(store => store.loadAll(namespaceStore.contextNamespaces));
}
private filterCallbacks: { [type: string]: ItemsFilter } = {
@@ -179,11 +181,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
@computed get filters() {
let { activeFilters } = pageFilters;
const { isClusterScoped, isSearchable, searchFilters } = this.props;
if (isClusterScoped) {
activeFilters = activeFilters.filter(({ type }) => type !== FilterType.NAMESPACE);
}
const { isSearchable, searchFilters } = this.props;
if (!(isSearchable && searchFilters)) {
activeFilters = activeFilters.filter(({ type }) => type !== FilterType.SEARCH);
@@ -217,7 +215,9 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
}
});
return this.applyFilters(filterItems, allItems);
const items = this.props.items ?? allItems;
return this.applyFilters(filterItems, items);
}
@autobind()
@@ -337,8 +337,8 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
}
renderInfo() {
const { allItems, items, isReady, userSettings, filters } = this;
const allItemsCount = allItems.length;
const { items, isReady, userSettings, filters } = this;
const allItemsCount = this.props.store.getTotalCount();
const itemsCount = items.length;
const isFiltered = isReady && filters.length > 0;
@@ -1,6 +1,5 @@
import { computed, observable, reaction } from "mobx";
import { autobind } from "../../utils";
import { namespaceStore } from "../+namespaces/namespace.store";
import { searchUrlParam } from "../input/search-input-url";
export enum FilterType {
@@ -24,32 +23,6 @@ export class PageFiltersStore {
constructor() {
this.syncWithGlobalSearch();
this.syncWithContextNamespace();
}
protected syncWithContextNamespace() {
const disposers = [
reaction(() => this.getValues(FilterType.NAMESPACE), filteredNs => {
if (filteredNs.length !== namespaceStore.getContextNamespaces().length) {
namespaceStore.setContext(filteredNs);
}
}),
namespaceStore.onContextChange(namespaces => {
const filteredNs = this.getValues(FilterType.NAMESPACE);
const isChanged = namespaces.length !== filteredNs.length;
if (isChanged) {
this.filters.replace([
...this.filters.filter(({ type }) => type !== FilterType.NAMESPACE),
...namespaces.map(ns => ({ type: FilterType.NAMESPACE, value: ns })),
]);
}
}, {
fireImmediately: true
})
];
return () => disposers.forEach(dispose => dispose());
}
protected syncWithGlobalSearch() {
@@ -8,6 +8,7 @@ import { KubeObjectStore } from "../../kube-object.store";
import { KubeObjectMenu } from "./kube-object-menu";
import { kubeSelectedUrlParam, showDetails } from "./kube-object-details";
import { kubeWatchApi } from "../../api/kube-watch-api";
import { clusterContext } from "../context";
export interface KubeObjectListLayoutProps extends ItemListLayoutProps {
store: KubeObjectStore;
@@ -26,7 +27,8 @@ export class KubeObjectListLayout extends React.Component<KubeObjectListLayoutPr
disposeOnUnmount(this, [
kubeWatchApi.subscribeStores(stores, {
preload: true
preload: true,
namespaces: clusterContext.contextNamespaces,
})
]);
}
@@ -40,12 +42,14 @@ export class KubeObjectListLayout extends React.Component<KubeObjectListLayoutPr
};
render() {
const items = this.props.store.contextItems;
const { className, ...layoutProps } = this.props;
return (
<ItemListLayout
{...layoutProps}
className={cssNames("KubeObjectListLayout", className)}
items={items}
preloadStores={false} // loading handled in kubeWatchApi.subscribeStores()
detailsItem={this.selectedItem}
onDetails={this.onDetails}
+1 -1
View File
@@ -40,7 +40,7 @@ interface Props {
@observer
export class Sidebar extends React.Component<Props> {
async componentDidMount() {
crdStore.loadSelectedNamespaces();
crdStore.reloadAll();
}
renderCustomResources() {