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

Ban circular dependencies

- ApiManager now creates the instances of all stores

- ReleaseStore is Singleton-like

- Move most types and helper functions into seperate files

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2021-05-19 08:17:51 -04:00
parent 492fa6f649
commit 33422ce975
232 changed files with 2537 additions and 1633 deletions

View File

@ -33,12 +33,22 @@ module.exports = {
}
},
overrides: [
{
files: [
"extensions/**/*.ts",
"extensions/**/*.tsx",
],
rules: {
"import/no-unresolved": "off", // warns on @k8slens/extensions
}
},
{
files: [
"**/*.js"
],
extends: [
"eslint:recommended",
"plugin:import/recommended",
],
env: {
node: true
@ -53,6 +63,7 @@ module.exports = {
],
rules: {
"header/header": [2, "./license-header"],
// "import/no-cycle": 2,
"indent": ["error", 2, {
"SwitchCase": 1,
}],
@ -93,6 +104,8 @@ module.exports = {
parser: "@typescript-eslint/parser",
extends: [
"plugin:@typescript-eslint/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
],
plugins: [
"header",
@ -104,6 +117,7 @@ module.exports = {
},
rules: {
"header/header": [2, "./license-header"],
// "import/no-cycle": 2,
"no-invalid-this": "off",
"@typescript-eslint/no-invalid-this": ["error"],
"@typescript-eslint/explicit-function-return-type": "off",
@ -158,6 +172,8 @@ module.exports = {
extends: [
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
],
parserOptions: {
ecmaVersion: 2018,
@ -166,6 +182,7 @@ module.exports = {
},
rules: {
"header/header": [2, "./license-header"],
// "import/no-cycle": 2,
"no-invalid-this": "off",
"@typescript-eslint/no-invalid-this": ["error"],
"@typescript-eslint/explicit-function-return-type": "off",

View File

@ -22,7 +22,7 @@
import { K8sApi } from "@k8slens/extensions";
export function resolveStatus(object: K8sApi.KubeObject): K8sApi.KubeObjectStatus {
const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi);
const eventStore = K8sApi.ApiManager.getInstance().getStore(K8sApi.eventApi);
const events = (eventStore as K8sApi.EventStore).getEventsByObject(object);
const warnings = events.filter(evt => evt.isWarning());
@ -42,7 +42,7 @@ export function resolveStatusForPods(pod: K8sApi.Pod): K8sApi.KubeObjectStatus {
if (!pod.hasIssues()) {
return null;
}
const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi);
const eventStore = K8sApi.ApiManager.getInstance().getStore(K8sApi.eventApi);
const events = (eventStore as K8sApi.EventStore).getEventsByObject(pod);
const warnings = events.filter(evt => evt.isWarning());
@ -59,7 +59,7 @@ export function resolveStatusForPods(pod: K8sApi.Pod): K8sApi.KubeObjectStatus {
}
export function resolveStatusForCronJobs(cronJob: K8sApi.CronJob): K8sApi.KubeObjectStatus {
const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi);
const eventStore = K8sApi.ApiManager.getInstance().getStore(K8sApi.eventApi);
let events = (eventStore as K8sApi.EventStore).getEventsByObject(cronJob);
const warnings = events.filter(evt => evt.isWarning());

View File

@ -315,6 +315,7 @@
"electron-notarize": "^0.3.0",
"eslint": "^7.7.0",
"eslint-plugin-header": "^3.1.1",
"eslint-plugin-import": "^2.23.2",
"eslint-plugin-react": "^7.21.5",
"eslint-plugin-unused-imports": "^1.0.1",
"file-loader": "^6.0.0",

View File

View File

@ -23,9 +23,11 @@ import fs from "fs";
import mockFs from "mock-fs";
import yaml from "js-yaml";
import { Cluster } from "../../main/cluster";
import { ClusterStore, getClusterIdFromHost } from "../cluster-store";
import { ClusterStore } from "../cluster-store";
import { Console } from "console";
import { stdout, stderr } from "process";
import { embedCustomKubeConfig } from "../utils";
import { getClusterIdFromHost } from "../cluster-types";
console = new Console(stdout, stderr);
@ -102,7 +104,7 @@ describe("empty config", () => {
icon: "data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5",
clusterName: "minikube"
},
kubeConfigPath: ClusterStore.embedCustomKubeConfig("foo", kubeconfig)
kubeConfigPath: embedCustomKubeConfig("foo", kubeconfig)
})
);
});
@ -135,7 +137,7 @@ describe("empty config", () => {
preferences: {
clusterName: "prod"
},
kubeConfigPath: ClusterStore.embedCustomKubeConfig("prod", kubeconfig)
kubeConfigPath: embedCustomKubeConfig("prod", kubeconfig)
}),
new Cluster({
id: "dev",
@ -143,7 +145,7 @@ describe("empty config", () => {
preferences: {
clusterName: "dev"
},
kubeConfigPath: ClusterStore.embedCustomKubeConfig("dev", kubeconfig)
kubeConfigPath: embedCustomKubeConfig("dev", kubeconfig)
})
);
});
@ -154,7 +156,7 @@ describe("empty config", () => {
});
it("check if cluster's kubeconfig file saved", () => {
const file = ClusterStore.embedCustomKubeConfig("boo", "kubeconfig");
const file = embedCustomKubeConfig("boo", "kubeconfig");
expect(fs.readFileSync(file, "utf8")).toBe("kubeconfig");
});

View File

@ -24,8 +24,7 @@ import Config from "conf";
import type { Options as ConfOptions } from "conf/dist/source/types";
import { app, ipcMain, ipcRenderer, remote } from "electron";
import { IReactionOptions, observable, reaction, runInAction, when } from "mobx";
import Singleton from "./utils/singleton";
import { getAppVersion } from "./utils/app-version";
import { Singleton, getAppVersion } from "./utils";
import logger from "../main/logger";
import { broadcastMessage, ipcMainOn, ipcRendererOn } from "./ipc";
import isEqual from "lodash/isEqual";

View File

@ -20,13 +20,13 @@
*/
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
import { CatalogEntity, CatalogEntityActionContext, CatalogEntityAddMenuContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
import { CatalogEntity, CatalogEntityActionContext, CatalogEntityAddMenuContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus, CatalogCategory, CatalogCategorySpec } from "../catalog";
import { clusterActivateHandler, clusterDisconnectHandler } from "../cluster-ipc";
import { ClusterStore } from "../cluster-store";
import { requestMain } from "../ipc";
import { productName } from "../vars";
import { CatalogCategory, CatalogCategorySpec } from "../catalog";
import { addClusterURL } from "../routes";
import { storedKubeConfigFolder } from "../utils";
import { app } from "electron";
export type KubernetesClusterPrometheusMetrics = {
@ -109,7 +109,7 @@ export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, Kube
},
];
if (this.metadata.labels["file"]?.startsWith(ClusterStore.storedKubeConfigFolder)) {
if (this.metadata.labels["file"]?.startsWith(storedKubeConfigFolder())) {
context.menuItems.push({
title: "Delete",
onlyVisibleForSource: "local",

View File

@ -19,86 +19,9 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { ClusterId, ClusterStore } from "./cluster-store";
import { appEventBus } from "./event-bus";
import { ResourceApplier } from "../main/resource-applier";
import { ipcMain, IpcMainInvokeEvent } from "electron";
import { clusterFrameMap } from "./cluster-frames";
export const clusterActivateHandler = "cluster:activate";
export const clusterSetFrameIdHandler = "cluster:set-frame-id";
export const clusterRefreshHandler = "cluster:refresh";
export const clusterDisconnectHandler = "cluster:disconnect";
export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all";
export const clusterKubectlDeleteAllHandler = "cluster:kubectl-delete-all";
if (ipcMain) {
ipcMain.handle(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
return ClusterStore.getInstance()
.getById(clusterId)
?.activate(force);
});
ipcMain.handle(clusterSetFrameIdHandler, (event: IpcMainInvokeEvent, clusterId: ClusterId) => {
const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) {
clusterFrameMap.set(cluster.id, { frameId: event.frameId, processId: event.processId });
cluster.pushState();
}
});
ipcMain.handle(clusterRefreshHandler, (event, clusterId: ClusterId) => {
return ClusterStore.getInstance()
.getById(clusterId)
?.refresh({ refreshMetadata: true });
});
ipcMain.handle(clusterDisconnectHandler, (event, clusterId: ClusterId) => {
appEventBus.emit({name: "cluster", action: "stop"});
const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) {
cluster.disconnect();
clusterFrameMap.delete(cluster.id);
}
});
ipcMain.handle(clusterKubectlApplyAllHandler, async (event, clusterId: ClusterId, resources: string[], extraArgs: string[]) => {
appEventBus.emit({name: "cluster", action: "kubectl-apply-all"});
const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) {
const applier = new ResourceApplier(cluster);
try {
const stdout = await applier.kubectlApplyAll(resources, extraArgs);
return { stdout };
} catch (error: any) {
return { stderr: error };
}
} else {
throw `${clusterId} is not a valid cluster id`;
}
});
ipcMain.handle(clusterKubectlDeleteAllHandler, async (event, clusterId: ClusterId, resources: string[], extraArgs: string[]) => {
appEventBus.emit({name: "cluster", action: "kubectl-delete-all"});
const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) {
const applier = new ResourceApplier(cluster);
try {
const stdout = await applier.kubectlDeleteAll(resources, extraArgs);
return { stdout };
} catch (error: any) {
return { stderr: error };
}
} else {
throw `${clusterId} is not a valid cluster id`;
}
});
}

View File

@ -19,120 +19,24 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import path from "path";
import { app, ipcMain, ipcRenderer, remote, webFrame } from "electron";
import { unlink } from "fs-extra";
import * as fse from "fs-extra";
import path from "path";
import { action, comparer, computed, observable, reaction, toJS } from "mobx";
import { BaseStore } from "./base-store";
import { Cluster, ClusterState } from "../main/cluster";
import { Cluster } from "../main/cluster";
import migrations from "../migrations/cluster-store";
import logger from "../main/logger";
import { appEventBus } from "./event-bus";
import { dumpConfigYaml } from "./kube-helpers";
import { saveToAppFiles } from "./utils/saveToAppFiles";
import type { KubeConfig } from "@kubernetes/client-node";
import { ipcMainOn, ipcRendererOn, requestMain } from "./ipc";
import type { ResourceType } from "../renderer/components/cluster-settings/components/cluster-metrics-setting";
import { disposer, noop } from "./utils";
export interface ClusterIconUpload {
clusterId: string;
name: string;
path: string;
}
export interface ClusterMetadata {
[key: string]: string | number | boolean | object;
}
export type ClusterPrometheusMetadata = {
success?: boolean;
provider?: string;
autoDetected?: boolean;
};
export interface ClusterStoreModel {
activeCluster?: ClusterId; // last opened cluster
clusters?: ClusterModel[];
}
export type ClusterId = string;
export interface UpdateClusterModel extends Omit<ClusterModel, "id"> {
id?: ClusterId;
}
export interface ClusterModel {
/** Unique id for a cluster */
id: ClusterId;
/** Path to cluster kubeconfig */
kubeConfigPath: string;
/**
* Workspace id
*
* @deprecated
*/
workspace?: string;
/** User context in kubeconfig */
contextName?: string;
/** Preferences */
preferences?: ClusterPreferences;
/** Metadata */
metadata?: ClusterMetadata;
/** List of accessible namespaces */
accessibleNamespaces?: string[];
/** @deprecated */
kubeConfig?: string; // yaml
}
export interface ClusterPreferences extends ClusterPrometheusPreferences {
terminalCWD?: string;
clusterName?: string;
iconOrder?: number;
icon?: string;
httpsProxy?: string;
hiddenMetrics?: string[];
}
export interface ClusterPrometheusPreferences {
prometheus?: {
namespace: string;
service: string;
port: number;
prefix: string;
};
prometheusProvider?: {
type: string;
};
}
import { disposer, getCustomKubeConfigPath, noop } from "./utils";
import type { ClusterStoreModel, ClusterId, ClusterModel, ClusterState } from "./cluster-types";
import { getHostedClusterId } from "./cluster-types";
export class ClusterStore extends BaseStore<ClusterStoreModel> {
private static StateChannel = "cluster:state";
static get storedKubeConfigFolder(): string {
return path.resolve((app || remote.app).getPath("userData"), "kubeconfigs");
}
static getCustomKubeConfigPath(clusterId: ClusterId): string {
return path.resolve(ClusterStore.storedKubeConfigFolder, clusterId);
}
static embedCustomKubeConfig(clusterId: ClusterId, kubeConfig: KubeConfig | string): string {
const filePath = ClusterStore.getCustomKubeConfigPath(clusterId);
const fileContents = typeof kubeConfig == "string" ? kubeConfig : dumpConfigYaml(kubeConfig);
saveToAppFiles(filePath, fileContents, { mode: 0o600 });
return filePath;
}
@observable activeCluster: ClusterId;
@observable removedClusters = observable.map<ClusterId, Cluster>();
@observable clusters = observable.map<ClusterId, Cluster>();
@ -311,9 +215,13 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
}
// remove only custom kubeconfigs (pasted as text)
if (cluster.kubeConfigPath == ClusterStore.getCustomKubeConfigPath(clusterId)) {
await unlink(cluster.kubeConfigPath).catch(noop);
if (cluster.kubeConfigPath == getCustomKubeConfigPath(clusterId)) {
await fse.unlink(cluster.kubeConfigPath).catch(noop);
}
const localStorage = path.resolve((app || remote.app).getPath("userData"), "lens-local-storage", `${cluster.id}.json`);
await fse.unlink(localStorage).catch(noop);
}
}
@ -361,21 +269,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
}
}
export function getClusterIdFromHost(host: string): ClusterId | undefined {
// e.g host == "%clusterId.localhost:45345"
const subDomains = host.split(":")[0].split(".");
return subDomains.slice(-2, -1)[0]; // ClusterId or undefined
}
export function getClusterFrameUrl(clusterId: ClusterId) {
return `//${clusterId}.${location.host}`;
}
export function getHostedClusterId() {
return getClusterIdFromHost(location.host);
}
export function getHostedCluster(): Cluster {
return ClusterStore.getInstance().getById(getHostedClusterId());
}

145
src/common/cluster-types.ts Normal file
View File

@ -0,0 +1,145 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export enum ClusterStatus {
AccessGranted = 2,
AccessDenied = 1,
Offline = 0
}
export enum ClusterMetadataKey {
VERSION = "version",
CLUSTER_ID = "id",
DISTRIBUTION = "distribution",
NODES_COUNT = "nodes",
LAST_SEEN = "lastSeen",
PROMETHEUS = "prometheus"
}
export type ClusterRefreshOptions = {
refreshMetadata?: boolean
};
export interface ClusterState {
apiUrl: string;
online: boolean;
disconnected: boolean;
accessible: boolean;
ready: boolean;
failureReason: string;
isAdmin: boolean;
allowedNamespaces: string[]
allowedResources: string[]
isGlobalWatchEnabled: boolean;
}
export interface ClusterIconUpload {
clusterId: string;
name: string;
path: string;
}
export interface ClusterMetadata {
[key: string]: string | number | boolean | object;
}
export type ClusterPrometheusMetadata = {
success?: boolean;
provider?: string;
autoDetected?: boolean;
};
export interface ClusterStoreModel {
activeCluster?: ClusterId; // last opened cluster
clusters?: ClusterModel[];
}
export type ClusterId = string;
export interface UpdateClusterModel extends Omit<ClusterModel, "id"> {
id?: ClusterId;
}
export interface ClusterModel {
/** Unique id for a cluster */
id: ClusterId;
/** Path to cluster kubeconfig */
kubeConfigPath: string;
/**
* Workspace id
*
* @deprecated
*/
workspace?: string;
/** User context in kubeconfig */
contextName?: string;
/** Preferences */
preferences?: ClusterPreferences;
/** Metadata */
metadata?: ClusterMetadata;
/** List of accessible namespaces */
accessibleNamespaces?: string[];
/** @deprecated */
kubeConfig?: string; // yaml
}
export interface ClusterPreferences extends ClusterPrometheusPreferences {
terminalCWD?: string;
clusterName?: string;
iconOrder?: number;
icon?: string;
httpsProxy?: string;
hiddenMetrics?: string[];
}
export interface ClusterPrometheusPreferences {
prometheus?: {
namespace: string;
service: string;
port: number;
prefix: string;
};
prometheusProvider?: {
type: string;
};
}
export function getClusterIdFromHost(host: string): ClusterId | undefined {
// e.g host == "%clusterId.localhost:45345"
const subDomains = host.split(":")[0].split(".");
return subDomains.slice(-2, -1)[0]; // ClusterId or undefined
}
export function getClusterFrameUrl(clusterId: ClusterId) {
return `//${clusterId}.${location.host}`;
}
export function getHostedClusterId() {
return getClusterIdFromHost(location.host);
}

View File

@ -80,6 +80,6 @@ export function ipcRendererOn(channel: string, listener: (event: Electron.IpcRen
return () => ipcRenderer.off(channel, listener);
}
export function bindBroadcastHandlers() {
export function initGetSubFramesHandler() {
ipcMain.handle(subFramesChannel, getSubFrames);
}

View File

@ -19,8 +19,6 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getHostedCluster } from "./cluster-store";
export type KubeResource =
"namespaces" | "nodes" | "events" | "resourcequotas" | "services" | "limitranges" |
"secrets" | "configmaps" | "ingresses" | "networkpolicies" | "persistentvolumeclaims" | "persistentvolumes" | "storageclasses" |
@ -73,18 +71,3 @@ export const apiResourceRecord: Record<KubeResource, KubeApiResourceData> = {
// TODO: auto-populate all resources dynamically (see: kubectl api-resources -o=wide -v=7)
export const apiResources: KubeApiResource[] = Object.entries(apiResourceRecord)
.map(([apiName, data]) => ({ apiName: apiName as KubeResource, ...data }));
export function isAllowedResource(resources: KubeResource | KubeResource[]) {
if (!Array.isArray(resources)) {
resources = [resources];
}
const { allowedResources = [] } = getHostedCluster() || {};
for (const resource of resources) {
if (!allowedResources.includes(resource)) {
return false;
}
}
return true;
}

View File

@ -26,14 +26,13 @@ import { readFile } from "fs-extra";
import { action, computed, observable, reaction, toJS } from "mobx";
import moment from "moment-timezone";
import { BaseStore } from "./base-store";
import migrations from "../migrations/user-store";
import migrations, { fileNameMigration } from "../migrations/user-store";
import { getAppVersion } from "./utils/app-version";
import { kubeConfigDefaultPath, loadConfig } from "./kube-helpers";
import { appEventBus } from "./event-bus";
import logger from "../main/logger";
import path from "path";
import os from "os";
import { fileNameMigration } from "../migrations/user-store";
import { ObservableToggleSet } from "../renderer/utils";
export interface UserStoreModel {

View File

@ -32,14 +32,13 @@ export * from "./debouncePromise";
export * from "./defineGlobal";
export * from "./delay";
export * from "./disposer";
export * from "./disposer";
export * from "./downloadFile";
export * from "./escapeRegExp";
export * from "./extended-map";
export * from "./getRandId";
export * from "./openExternal";
export * from "./reject-promise";
export * from "./saveToAppFiles";
export * from "./kubeconfig-files";
export * from "./singleton";
export * from "./splitArray";
export * from "./tar";

View File

@ -19,17 +19,27 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
// Save file to electron app directory (e.g. "/Users/$USER/Library/Application Support/Lens" for MacOS)
import path from "path";
import type { KubeConfig } from "@kubernetes/client-node";
import { app, remote } from "electron";
import { ensureDirSync, writeFileSync } from "fs-extra";
import type { WriteFileOptions } from "fs";
import path from "path";
import type { ClusterId } from "../cluster-types";
import { dumpConfigYaml } from "../kube-helpers";
import * as fse from "fs-extra";
export function saveToAppFiles(filePath: string, contents: any, options?: WriteFileOptions): string {
const absPath = path.resolve((app || remote.app).getPath("userData"), filePath);
ensureDirSync(path.dirname(absPath));
writeFileSync(absPath, contents, options);
return absPath;
export function storedKubeConfigFolder(): string {
return path.resolve((app || remote.app).getPath("userData"), "kubeconfigs");
}
export function getCustomKubeConfigPath(clusterId: ClusterId): string {
return path.resolve(storedKubeConfigFolder(), clusterId);
}
export function embedCustomKubeConfig(clusterId: ClusterId, kubeConfig: KubeConfig | string): string {
const filePath = getCustomKubeConfigPath(clusterId);
const fileContents = typeof kubeConfig == "string" ? kubeConfig : dumpConfigYaml(kubeConfig);
fse.ensureDirSync(path.dirname(filePath));
fse.writeFileSync(filePath, fileContents, { mode: 0o600 });
return filePath;
}

View File

@ -53,4 +53,8 @@ export class CommandRegistry extends BaseRegistry<CommandRegistration> {
return super.add(filteredItems, extension);
}
getById(commandId: string): CommandRegistration | undefined {
return this.getItems().find(({ id }) => id == commandId);
}
}

View File

@ -19,9 +19,8 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export { isAllowedResource } from "../../common/rbac";
export { ResourceStack } from "../../common/k8s/resource-stack";
export { apiManager } from "../../renderer/api/api-manager";
export { ApiManager } from "../../renderer/api/api-manager";
export { KubeObjectStore } from "../../renderer/kube-object.store";
export { KubeApi, forCluster } from "../../renderer/api/kube-api";
export { KubeObject } from "../../renderer/api/kube-object";
@ -38,13 +37,13 @@ export { ReplicaSet, replicaSetApi } from "../../renderer/api/endpoints";
export { ResourceQuota, resourceQuotaApi } from "../../renderer/api/endpoints";
export { LimitRange, limitRangeApi } from "../../renderer/api/endpoints";
export { HorizontalPodAutoscaler, hpaApi } from "../../renderer/api/endpoints";
export { PodDisruptionBudget, pdbApi } from "../../renderer/api/endpoints";
export { PodDisruptionBudget, podDisruptionBudgetApi } from "../../renderer/api/endpoints";
export { Service, serviceApi } from "../../renderer/api/endpoints";
export { Endpoint, endpointApi } from "../../renderer/api/endpoints";
export { Ingress, ingressApi, IngressApi } from "../../renderer/api/endpoints";
export { NetworkPolicy, networkPolicyApi } from "../../renderer/api/endpoints";
export { PersistentVolume, persistentVolumeApi } from "../../renderer/api/endpoints";
export { PersistentVolumeClaim, pvcApi, PersistentVolumeClaimsApi } from "../../renderer/api/endpoints";
export { PersistentVolumeClaim, persistentVolumeClaimsApi, PersistentVolumeClaimsApi } from "../../renderer/api/endpoints";
export { StorageClass, storageClassApi } from "../../renderer/api/endpoints";
export { Namespace, namespacesApi } from "../../renderer/api/endpoints";
export { KubeEvent, eventApi } from "../../renderer/api/endpoints";
@ -63,31 +62,30 @@ export type { ISecretRef } from "../../renderer/api/endpoints";
export type { KubeObjectStatus } from "./kube-object-status";
// stores
export type { EventStore } from "../../renderer/components/+events/event.store";
export type { PodsStore } from "../../renderer/components/+workloads-pods/pods.store";
export type { NodesStore } from "../../renderer/components/+nodes/nodes.store";
export type { DeploymentStore } from "../../renderer/components/+workloads-deployments/deployments.store";
export type { DaemonSetStore } from "../../renderer/components/+workloads-daemonsets/daemonsets.store";
export type { StatefulSetStore } from "../../renderer/components/+workloads-statefulsets/statefulset.store";
export type { JobStore } from "../../renderer/components/+workloads-jobs/job.store";
export type { CronJobStore } from "../../renderer/components/+workloads-cronjobs/cronjob.store";
export type { ConfigMapsStore } from "../../renderer/components/+config-maps/config-maps.store";
export type { SecretsStore } from "../../renderer/components/+config-secrets/secrets.store";
export type { ReplicaSetStore } from "../../renderer/components/+workloads-replicasets/replicasets.store";
export type { ResourceQuotasStore } from "../../renderer/components/+config-resource-quotas/resource-quotas.store";
export type { LimitRangesStore } from "../../renderer/components/+config-limit-ranges/limit-ranges.store";
export type { HPAStore } from "../../renderer/components/+config-autoscalers/hpa.store";
export type { PodDisruptionBudgetsStore } from "../../renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.store";
export type { ServiceStore } from "../../renderer/components/+network-services/services.store";
export type { EndpointStore } from "../../renderer/components/+network-endpoints/endpoints.store";
export type { IngressStore } from "../../renderer/components/+network-ingresses/ingress.store";
export type { NetworkPolicyStore } from "../../renderer/components/+network-policies/network-policy.store";
export type { PersistentVolumesStore } from "../../renderer/components/+storage-volumes/volumes.store";
export type { VolumeClaimStore } from "../../renderer/components/+storage-volume-claims/volume-claim.store";
export type { StorageClassStore } from "../../renderer/components/+storage-classes/storage-class.store";
export type { NamespaceStore } from "../../renderer/components/+namespaces/namespace.store";
export type { ServiceAccountsStore } from "../../renderer/components/+user-management-service-accounts/service-accounts.store";
export type { RolesStore } from "../../renderer/components/+user-management-roles/roles.store";
export type { RoleBindingsStore } from "../../renderer/components/+user-management-roles-bindings/role-bindings.store";
export type { CRDStore } from "../../renderer/components/+custom-resources/crd.store";
export type { CRDResourceStore } from "../../renderer/components/+custom-resources/crd-resource.store";
export type { EventStore } from "../../renderer/components/+events";
export type { PodsStore } from "../../renderer/components/+workloads-pods";
export type { NodesStore } from "../../renderer/components/+nodes";
export type { DeploymentStore } from "../../renderer/components/+workloads-deployments";
export type { DaemonSetStore } from "../../renderer/components/+workloads-daemonsets";
export type { StatefulSetStore } from "../../renderer/components/+workloads-statefulsets";
export type { JobStore } from "../../renderer/components/+workloads-jobs";
export type { CronJobStore } from "../../renderer/components/+workloads-cronjobs";
export type { ConfigMapsStore } from "../../renderer/components/+config-maps";
export type { SecretsStore } from "../../renderer/components/+config-secrets";
export type { ReplicaSetStore } from "../../renderer/components/+workloads-replicasets";
export type { ResourceQuotasStore } from "../../renderer/components/+config-resource-quotas";
export type { LimitRangesStore } from "../../renderer/components/+config-limit-ranges";
export type { HpaStore as HPAStore } from "../../renderer/components/+config-autoscalers";
export type { PodDisruptionBudgetsStore } from "../../renderer/components/+config-pod-disruption-budgets";
export type { ServiceStore } from "../../renderer/components/+network-services";
export type { EndpointStore } from "../../renderer/components/+network-endpoints";
export type { IngressStore } from "../../renderer/components/+network-ingresses";
export type { NetworkPolicyStore } from "../../renderer/components/+network-policies";
export type { PersistentVolumesStore } from "../../renderer/components/+storage-volumes";
export type { PersistentVolumeClaimStore as VolumeClaimStore } from "../../renderer/components/+storage-volume-claims";
export type { StorageClassStore } from "../../renderer/components/+storage-classes";
export type { NamespaceStore } from "../../renderer/components/+namespaces";
export type { ServiceAccountsStore } from "../../renderer/components/+user-management-service-accounts";
export type { RolesStore } from "../../renderer/components/+user-management-roles";
export type { RoleBindingsStore } from "../../renderer/components/+user-management-roles-bindings";
export type { CrdStore as CRDStore } from "../../renderer/components/+custom-resources";

View File

@ -24,7 +24,7 @@ import { navigation } from "../../renderer/navigation";
export type { PageParamInit, PageParam } from "../../renderer/navigation/page-param";
export { navigate, isActiveRoute } from "../../renderer/navigation/helpers";
export { hideDetails, showDetails, getDetailsUrl } from "../../renderer/components/kube-object/kube-object-details";
export { hideDetails, showDetails, getDetailsUrl } from "../../renderer/components/kube-object/params";
export type { URLParams } from "../../common/utils/buildUrl";
// exporting to extensions-api version of helper without `isSystem` flag

View File

@ -25,14 +25,15 @@ import { watch } from "chokidar";
import fs from "fs";
import fse from "fs-extra";
import type stream from "stream";
import { Disposer, ExtendedObservableMap, iter, Singleton } from "../../common/utils";
import { Disposer, ExtendedObservableMap, iter, Singleton, storedKubeConfigFolder } from "../../common/utils";
import logger from "../logger";
import type { KubeConfig } from "@kubernetes/client-node";
import { loadConfigFromString, splitConfig, validateKubeConfig } from "../../common/kube-helpers";
import { Cluster } from "../cluster";
import { catalogEntityFromCluster } from "../cluster-manager";
import { UserStore } from "../../common/user-store";
import { ClusterStore, UpdateClusterModel } from "../../common/cluster-store";
import { ClusterStore } from "../../common/cluster-store";
import type { UpdateClusterModel } from "../../common/cluster-types";
import { createHash } from "crypto";
const logPrefix = "[KUBECONFIG-SYNC]:";
@ -62,7 +63,7 @@ export class KubeconfigSyncManager extends Singleton {
)));
// This must be done so that c&p-ed clusters are visible
this.startNewSync(ClusterStore.storedKubeConfigFolder);
this.startNewSync(storedKubeConfigFolder());
for (const filePath of UserStore.getInstance().syncKubeconfigEntries.keys()) {
this.startNewSync(filePath);

View File

@ -21,7 +21,7 @@
import { BaseClusterDetector } from "./base-cluster-detector";
import { createHash } from "crypto";
import { ClusterMetadataKey } from "../cluster";
import { ClusterMetadataKey } from "../../common/cluster-types";
export class ClusterIdDetector extends BaseClusterDetector {
key = ClusterMetadataKey.CLUSTER_ID;

View File

@ -20,7 +20,7 @@
*/
import { observable } from "mobx";
import type { ClusterMetadata } from "../../common/cluster-store";
import type { ClusterMetadata } from "../../common/cluster-types";
import type { Cluster } from "../cluster";
import type { BaseClusterDetector, ClusterDetectionResult } from "./base-cluster-detector";
import { ClusterIdDetector } from "./cluster-id-detector";

View File

@ -20,7 +20,7 @@
*/
import { BaseClusterDetector } from "./base-cluster-detector";
import { ClusterMetadataKey } from "../cluster";
import { ClusterMetadataKey } from "../../common/cluster-types";
export class DistributionDetector extends BaseClusterDetector {
key = ClusterMetadataKey.DISTRIBUTION;
@ -60,7 +60,7 @@ export class DistributionDetector extends BaseClusterDetector {
if (this.isK0s()) {
return { value: "k0s", accuracy: 80};
}
if (this.isVMWare()) {
return { value: "vmware", accuracy: 90};
}
@ -179,7 +179,7 @@ export class DistributionDetector extends BaseClusterDetector {
protected isK0s() {
return this.version.includes("-k0s");
}
protected isAlibaba() {
return this.version.includes("-aliyun");
}

View File

@ -20,7 +20,7 @@
*/
import { BaseClusterDetector } from "./base-cluster-detector";
import { ClusterMetadataKey } from "../cluster";
import { ClusterMetadataKey } from "../../common/cluster-types";
export class LastSeenDetector extends BaseClusterDetector {
key = ClusterMetadataKey.LAST_SEEN;

View File

@ -19,8 +19,8 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { ClusterMetadataKey } from "../../common/cluster-types";
import { BaseClusterDetector } from "./base-cluster-detector";
import { ClusterMetadataKey } from "../cluster";
export class NodesCountDetector extends BaseClusterDetector {
key = ClusterMetadataKey.NODES_COUNT;

View File

@ -19,8 +19,8 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { ClusterMetadataKey } from "../../common/cluster-types";
import { BaseClusterDetector } from "./base-cluster-detector";
import { ClusterMetadataKey } from "../cluster";
export class VersionDetector extends BaseClusterDetector {
key = ClusterMetadataKey.VERSION;

View File

@ -19,11 +19,11 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import "../common/cluster-ipc";
import type http from "http";
import { ipcMain } from "electron";
import { action, autorun, reaction, toJS } from "mobx";
import { ClusterStore, getClusterIdFromHost } from "../common/cluster-store";
import { ClusterStore } from "../common/cluster-store";
import { getClusterIdFromHost } from "../common/cluster-types";
import type { Cluster } from "./cluster";
import logger from "./logger";
import { apiKubePrefix } from "../common/vars";

View File

@ -20,7 +20,6 @@
*/
import { ipcMain } from "electron";
import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel } from "../common/cluster-store";
import { action, comparer, computed, observable, reaction, toJS, when } from "mobx";
import { broadcastMessage, ClusterListNamespaceForbiddenChannel } from "../common/ipc";
import { ContextHandler } from "./context-handler";
@ -33,38 +32,8 @@ import logger from "./logger";
import { VersionDetector } from "./cluster-detectors/version-detector";
import { detectorRegistry } from "./cluster-detectors/detector-registry";
import plimit from "p-limit";
export enum ClusterStatus {
AccessGranted = 2,
AccessDenied = 1,
Offline = 0
}
export enum ClusterMetadataKey {
VERSION = "version",
CLUSTER_ID = "id",
DISTRIBUTION = "distribution",
NODES_COUNT = "nodes",
LAST_SEEN = "lastSeen",
PROMETHEUS = "prometheus"
}
export type ClusterRefreshOptions = {
refreshMetadata?: boolean
};
export interface ClusterState {
apiUrl: string;
online: boolean;
disconnected: boolean;
accessible: boolean;
ready: boolean;
failureReason: string;
isAdmin: boolean;
allowedNamespaces: string[]
allowedResources: string[]
isGlobalWatchEnabled: boolean;
}
import type { ClusterModel, ClusterState, ClusterId, ClusterPreferences, ClusterMetadata, ClusterPrometheusPreferences, UpdateClusterModel, ClusterRefreshOptions } from "../common/cluster-types";
import { ClusterStatus } from "../common/cluster-types";
/**
* Cluster
@ -711,4 +680,12 @@ export class Cluster implements ClusterModel, ClusterState {
return true; // allowed by default for other resources
}
isAllowedResources(...kinds: string[]): boolean {
return kinds.every(kind => this.isAllowedResource(kind));
}
isAnyAllowedResources(...kinds: string[]): boolean {
return kinds.length === 0 || kinds.some(kind => this.isAllowedResource(kind));
}
}

View File

@ -20,7 +20,7 @@
*/
import type { PrometheusService } from "./prometheus/provider-registry";
import type { ClusterPrometheusPreferences } from "../common/cluster-store";
import type { ClusterPrometheusPreferences } from "../common/cluster-types";
import type { Cluster } from "./cluster";
import type httpProxy from "http-proxy";
import url, { UrlWithStringQuery } from "url";

View File

@ -26,17 +26,11 @@ import { ClusterManager } from "./cluster-manager";
import logger from "./logger";
export function exitApp() {
console.log("before windowManager");
const windowManager = WindowManager.getInstance(false);
console.log("before clusterManager");
const clusterManager = ClusterManager.getInstance(false);
console.log("after clusterManager");
appEventBus.emit({ name: "service", action: "close" });
windowManager?.hide();
clusterManager?.stop();
WindowManager.getInstance(false)?.hide();
ClusterManager.getInstance(false)?.stop();
logger.info("SERVICE:QUIT");
setTimeout(() => {
app.exit();

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import * as tempy from "tempy";
import tempy from "tempy";
import fse from "fs-extra";
import * as yaml from "js-yaml";
import { promiseExec} from "../promise-exec";

View File

@ -45,8 +45,8 @@ import type { LensExtensionId } from "../extensions/lens-extension";
import { FilesystemProvisionerStore } from "./extension-filesystem";
import { installDeveloperTools } from "./developer-tools";
import { LensProtocolRouterMain } from "./protocol-handler";
import { getAppVersion, getAppVersionFromProxyServer } from "../common/utils";
import { bindBroadcastHandlers } from "../common/ipc";
import { disposer, getAppVersion, getAppVersionFromProxyServer } from "../common/utils";
import { initGetSubFramesHandler } from "../common/ipc";
import { startUpdateChecking } from "./app-updater";
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
import { CatalogPusher } from "./catalog-pusher";
@ -54,10 +54,14 @@ import { catalogEntityRegistry } from "../common/catalog";
import { HotbarStore } from "../common/hotbar-store";
import { HelmRepoManager } from "./helm/helm-repo-manager";
import { KubeconfigSyncManager } from "./catalog-sources";
import { handleWsUpgrade } from "./proxy/ws-upgrade";
import { initRegistries } from "./initializers";
import { initIpcMainHandlers } from "./initializers/ipc-handlers";
import { Router } from "./router";
import { initMenu } from "./menu";
import { initTray } from "./tray";
const workingDir = path.join(app.getPath("appData"), appName);
const cleanup = disposer();
app.setName(appName);
@ -114,7 +118,8 @@ app.on("ready", async () => {
logger.info("🐚 Syncing shell environment");
await shellSync();
bindBroadcastHandlers();
initGetSubFramesHandler();
initIpcMainHandlers();
powerMonitor.on("shutdown", () => {
app.exit();
@ -140,14 +145,15 @@ app.on("ready", async () => {
filesystemStore.load(),
]);
const lensProxy = LensProxy.createInstance(handleWsUpgrade);
ClusterManager.createInstance();
KubeconfigSyncManager.createInstance().startSync();
try {
logger.info("🔌 Starting LensProxy");
await lensProxy.listen();
await LensProxy.createInstance(
new Router(),
req => ClusterManager.getInstance().getClusterForRequest(req),
).listen();
} catch (error) {
dialog.showErrorBox("Lens Error", `Could not start proxy: ${error?.message || "unknown error"}`);
app.exit();
@ -156,7 +162,7 @@ app.on("ready", async () => {
// test proxy connection
try {
logger.info("🔎 Testing LensProxy connection ...");
const versionFromProxy = await getAppVersionFromProxyServer(lensProxy.port);
const versionFromProxy = await getAppVersionFromProxyServer(LensProxy.getInstance().port);
if (getAppVersion() !== versionFromProxy) {
logger.error("Proxy server responded with invalid response");
@ -182,6 +188,8 @@ app.on("ready", async () => {
logger.info("🖥️ Starting WindowManager");
const windowManager = WindowManager.createInstance();
cleanup.push(initMenu(windowManager), initTray(windowManager));
installDeveloperTools();
if (!startHidden) {
@ -259,6 +267,8 @@ app.on("will-quit", (event) => {
return; // skip exit to make tray work, to quit go to app's global menu or tray's menu
}
cleanup();
});
app.on("open-url", (event, rawUrl) => {

View File

@ -0,0 +1,93 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { ipcMain, IpcMainInvokeEvent } from "electron";
import { clusterFrameMap } from "../../common/cluster-frames";
import * as channels from "../../common/cluster-ipc";
import { ClusterStore } from "../../common/cluster-store";
import type { ClusterId } from "../../common/cluster-types";
import { appEventBus } from "../../common/event-bus";
import { ResourceApplier } from "../resource-applier";
export function initIpcMainHandlers() {
ipcMain.handle(channels.clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
return ClusterStore.getInstance()
.getById(clusterId)
?.activate(force);
});
ipcMain.handle(channels.clusterSetFrameIdHandler, (event: IpcMainInvokeEvent, clusterId: ClusterId) => {
const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) {
clusterFrameMap.set(cluster.id, { frameId: event.frameId, processId: event.processId });
cluster.pushState();
}
});
ipcMain.handle(channels.clusterRefreshHandler, (event, clusterId: ClusterId) => {
return ClusterStore.getInstance()
.getById(clusterId)
?.refresh({ refreshMetadata: true });
});
ipcMain.handle(channels.clusterDisconnectHandler, (event, clusterId: ClusterId) => {
appEventBus.emit({ name: "cluster", action: "stop" });
const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) {
cluster.disconnect();
clusterFrameMap.delete(cluster.id);
}
});
ipcMain.handle(channels.clusterKubectlApplyAllHandler, (event, clusterId: ClusterId, resources: string[]) => {
appEventBus.emit({ name: "cluster", action: "kubectl-apply-all" });
const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) {
const applier = new ResourceApplier(cluster);
applier.kubectlApplyAll(resources);
} else {
throw `${clusterId} is not a valid cluster id`;
}
});
ipcMain.handle(channels.clusterKubectlDeleteAllHandler, async (event, clusterId: ClusterId, resources: string[], extraArgs: string[]) => {
appEventBus.emit({ name: "cluster", action: "kubectl-delete-all" });
const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) {
const applier = new ResourceApplier(cluster);
try {
const stdout = await applier.kubectlDeleteAll(resources, extraArgs);
return { stdout };
} catch (error: any) {
return { stderr: error };
}
} else {
throw `${clusterId} is not a valid cluster id`;
}
});
}

View File

@ -25,41 +25,40 @@ import spdy from "spdy";
import httpProxy from "http-proxy";
import url from "url";
import { apiPrefix, apiKubePrefix } from "../../common/vars";
import { Router } from "../router";
import type { Router } from "../router";
import type { ContextHandler } from "../context-handler";
import logger from "../logger";
import { Singleton } from "../../common/utils";
import { ClusterManager } from "../cluster-manager";
type WSUpgradeHandler = (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => void;
import type { Cluster } from "../cluster";
import { LocalShellSession, NodeShellSession } from "../shell-session";
import type * as WebSocket from "ws";
import { Server } from "ws";
export class LensProxy extends Singleton {
protected origin: string;
protected proxyServer: http.Server;
protected router = new Router();
protected closed = false;
protected retryCounters = new Map<string, number>();
public port: number;
constructor(handleWsUpgrade: WSUpgradeHandler) {
constructor(protected router: Router, protected getClusterForRequest: (req: http.IncomingMessage) => Cluster) {
super();
const proxy = this.createProxy();
this.proxyServer = spdy.createServer({
spdy: {
plain: true,
protocols: ["http/1.1", "spdy/3.1"]
}
}, (req: http.IncomingMessage, res: http.ServerResponse) => {
this.handleRequest(proxy, req, res);
});
this.proxyServer
this.proxyServer = spdy
.createServer({
spdy: {
plain: true,
protocols: ["http/1.1", "spdy/3.1"]
}
}, (req: http.IncomingMessage, res: http.ServerResponse) => {
this.handleRequest(proxy, req, res);
})
.on("upgrade", (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => {
if (req.url.startsWith(`${apiPrefix}?`)) {
handleWsUpgrade(req, socket, head);
this.handleWsUpgrade(req, socket, head);
} else {
this.handleProxyUpgrade(proxy, req, socket, head);
}
@ -103,8 +102,27 @@ export class LensProxy extends Singleton {
this.closed = true;
}
protected async handleWsUpgrade(req: http.IncomingMessage, socket: net.Socket, head: Buffer) {
const wsServer = new Server({ noServer: true });
wsServer.on("connection", ((socket: WebSocket, req: http.IncomingMessage) => {
const cluster = this.getClusterForRequest(req);
const nodeParam = url.parse(req.url, true).query["node"]?.toString();
const shell = nodeParam
? new NodeShellSession(socket, cluster, nodeParam)
: new LocalShellSession(socket, cluster);
shell.open()
.catch(error => logger.error(`[SHELL-SESSION]: failed to open: ${error}`, { error }));
}));
wsServer.handleUpgrade(req, socket, head, (con) => {
wsServer.emit("connection", con, req);
});
}
protected async handleProxyUpgrade(proxy: httpProxy, req: http.IncomingMessage, socket: net.Socket, head: Buffer) {
const cluster = ClusterManager.getInstance().getClusterForRequest(req);
const cluster = this.getClusterForRequest(req);
if (cluster) {
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "");
@ -205,7 +223,7 @@ export class LensProxy extends Singleton {
return proxy;
}
protected async getProxyTarget(req: http.IncomingMessage, contextHandler: ContextHandler): Promise<httpProxy.ServerOptions | void> {
protected async getProxyTarget(req: http.IncomingMessage, contextHandler: ContextHandler): Promise<httpProxy.ServerOptions | null> {
if (req.url.startsWith(apiKubePrefix)) {
delete req.headers.authorization;
req.url = req.url.replace(apiKubePrefix, "");
@ -213,6 +231,8 @@ export class LensProxy extends Singleton {
return contextHandler.getApiTarget(isWatchRequest);
}
return null;
}
protected getRequestId(req: http.IncomingMessage) {
@ -220,7 +240,7 @@ export class LensProxy extends Singleton {
}
protected async handleRequest(proxy: httpProxy, req: http.IncomingMessage, res: http.ServerResponse) {
const cluster = ClusterManager.getInstance().getClusterForRequest(req);
const cluster = this.getClusterForRequest(req);
if (cluster) {
const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler);

View File

@ -25,7 +25,7 @@ import { exec } from "child_process";
import fs from "fs";
import * as yaml from "js-yaml";
import path from "path";
import * as tempy from "tempy";
import tempy from "tempy";
import logger from "./logger";
import { appEventBus } from "../common/event-bus";
import { cloneJsonObject } from "../common/utils";
@ -34,19 +34,19 @@ export class ResourceApplier {
constructor(protected cluster: Cluster) {
}
async apply(resource: KubernetesObject | any): Promise<string> {
async apply(resource: KubernetesObject | any): Promise<any> {
resource = this.sanitizeObject(resource);
appEventBus.emit({name: "resource", action: "apply"});
return await this.kubectlApply(yaml.safeDump(resource));
}
protected async kubectlApply(content: string): Promise<string> {
protected async kubectlApply(content: string): Promise<any> {
const { kubeCtl } = this.cluster;
const kubectlPath = await kubeCtl.getPath();
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
return new Promise<string>((resolve, reject) => {
return new Promise<any>((resolve, reject) => {
const fileName = tempy.file({ name: "resource.yaml" });
fs.writeFileSync(fileName, content);

View File

@ -22,10 +22,10 @@
import _ from "lodash";
import type { LensApiRequest } from "../router";
import { respondJson } from "../utils/http-responses";
import { Cluster, ClusterMetadataKey } from "../cluster";
import type { ClusterPrometheusMetadata } from "../../common/cluster-store";
import type { Cluster } from "../cluster";
import logger from "../logger";
import { getMetrics } from "../k8s-request";
import { ClusterPrometheusMetadata, ClusterMetadataKey } from "../../common/cluster-types";
export type IMetricsQuery = string | string[] | {
[metricName: string]: string;

View File

@ -19,14 +19,12 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { ClusterId } from "../common/cluster-store";
import type { ClusterId } from "../common/cluster-types";
import { observable } from "mobx";
import { app, BrowserWindow, dialog, shell, webContents } from "electron";
import windowStateKeeper from "electron-window-state";
import { appEventBus } from "../common/event-bus";
import { ipcMainOn } from "../common/ipc";
import { initMenu } from "./menu";
import { initTray } from "./tray";
import { Singleton } from "../common/utils";
import { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames";
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
@ -38,15 +36,15 @@ export class WindowManager extends Singleton {
protected mainWindow: BrowserWindow;
protected splashWindow: BrowserWindow;
protected windowState: windowStateKeeper.State;
protected disposers: Record<string, Function> = {};
@observable activeClusterId: ClusterId;
constructor() {
super();
this.bindEvents();
this.initMenu();
this.initTray();
ipcMainOn(IpcRendererNavigationEvents.CLUSTER_VIEW_CURRENT_ID, (event, clusterId: ClusterId) => {
this.activeClusterId = clusterId;
});
}
get mainUrl() {
@ -130,21 +128,6 @@ export class WindowManager extends Singleton {
}
}
protected async initMenu() {
this.disposers.menuAutoUpdater = initMenu(this);
}
protected initTray() {
this.disposers.trayAutoUpdater = initTray(this);
}
protected bindEvents() {
// track visible cluster from ui
ipcMainOn(IpcRendererNavigationEvents.CLUSTER_VIEW_CURRENT_ID, (event, clusterId: ClusterId) => {
this.activeClusterId = clusterId;
});
}
async ensureMainWindow(): Promise<BrowserWindow> {
if (!this.mainWindow) await this.initMainWindow();
this.mainWindow.show();
@ -214,9 +197,5 @@ export class WindowManager extends Singleton {
this.splashWindow.destroy();
this.mainWindow = null;
this.splashWindow = null;
Object.entries(this.disposers).forEach(([name, dispose]) => {
dispose();
delete this.disposers[name];
});
}
}

View File

@ -26,14 +26,15 @@ import path from "path";
import { app, remote } from "electron";
import { migration } from "../migration-wrapper";
import fse from "fs-extra";
import { ClusterModel, ClusterStore } from "../../common/cluster-store";
import { loadConfig } from "../../common/kube-helpers";
import type { ClusterModel } from "../../common/cluster-types";
import { getCustomKubeConfigPath, embedCustomKubeConfig } from "../../common/utils";
export default migration({
version: "3.6.0-beta.1",
run(store, printLog) {
const userDataPath = (app || remote.app).getPath("userData");
const kubeConfigBase = ClusterStore.getCustomKubeConfigPath("");
const kubeConfigBase = getCustomKubeConfigPath("");
const storedClusters: ClusterModel[] = store.get("clusters") || [];
if (!storedClusters.length) return;
@ -47,7 +48,7 @@ export default migration({
*/
try {
// take the embedded kubeconfig and dump it into a file
cluster.kubeConfigPath = ClusterStore.embedCustomKubeConfig(cluster.id, cluster.kubeConfig);
cluster.kubeConfigPath = embedCustomKubeConfig(cluster.id, cluster.kubeConfig);
cluster.contextName = loadConfig(cluster.kubeConfigPath).getCurrentContext();
delete cluster.kubeConfig;

View File

@ -22,9 +22,9 @@
// Fix embedded kubeconfig paths under snap config
import { migration } from "../migration-wrapper";
import type { ClusterModel } from "../../common/cluster-store";
import { getAppVersion } from "../../common/utils/app-version";
import fs from "fs";
import type { ClusterModel } from "../../common/cluster-types";
export default migration({
version: getAppVersion(), // Run always after upgrade

View File

@ -19,43 +19,59 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { ingressStore } from "../../components/+network-ingresses/ingress.store";
import { apiManager } from "../api-manager";
import { Cluster } from "../../../main/cluster";
import { KubeObjectStore } from "../../kube-object.store";
import { ApiManager } from "../api-manager";
import { KubeApi } from "../kube-api";
import { KubeObject } from "../kube-object";
class TestApi extends KubeApi {
protected async checkPreferredVersion() {
return;
}
}
describe("ApiManager", () => {
beforeEach(() => {
ApiManager.createInstance(new Cluster({
id: "foobar",
kubeConfigPath: "/foobar",
}));
});
afterEach(() => {
ApiManager.resetInstance();
});
describe("registerApi", () => {
it("re-register store if apiBase changed", async () => {
const apiBase = "apis/v1/foo";
const fallbackApiBase = "/apis/extensions/v1beta1/foo";
const kubeApi = new TestApi({
objectConstructor: KubeObject,
apiBase,
fallbackApiBases: [fallbackApiBase],
checkPreferredVersion: true,
});
apiManager.registerApi(apiBase, kubeApi);
ApiManager.getInstance().registerApi(apiBase, kubeApi);
class TestStore extends KubeObjectStore<KubeObject> {
api = kubeApi;
}
// Define to use test api for ingress store
Object.defineProperty(ingressStore, "api", { value: kubeApi });
apiManager.registerStore(ingressStore, [kubeApi]);
ApiManager.getInstance().registerStore(TestStore);
// Test that store is returned with original apiBase
expect(apiManager.getStore(kubeApi)).toBe(ingressStore);
expect(ApiManager.getInstance().getStore(kubeApi)).toBeInstanceOf(TestStore);
// Change apiBase similar as checkPreferredVersion does
Object.defineProperty(kubeApi, "apiBase", { value: fallbackApiBase });
apiManager.registerApi(fallbackApiBase, kubeApi);
ApiManager.getInstance().registerApi(fallbackApiBase, kubeApi);
// Test that store is returned with new apiBase
expect(apiManager.getStore(kubeApi)).toBe(ingressStore);
expect(ApiManager.getInstance().getStore(kubeApi)).toBeInstanceOf(TestStore);
});
});
});

View File

@ -20,6 +20,7 @@
*/
import { KubeApi } from "../kube-api";
import { KubeObject } from "../kube-object";
describe("KubeApi", () => {
it("uses url from apiBase if apiBase contains the resource", async () => {
@ -53,6 +54,7 @@ describe("KubeApi", () => {
const apiBase = "/apis/networking.k8s.io/v1/ingresses";
const fallbackApiBase = "/apis/extensions/v1beta1/ingresses";
const kubeApi = new KubeApi({
objectConstructor: KubeObject,
apiBase,
fallbackApiBases: [fallbackApiBase],
checkPreferredVersion: true,
@ -91,6 +93,7 @@ describe("KubeApi", () => {
const apiBase = "apis/networking.k8s.io/v1/ingresses";
const fallbackApiBase = "/apis/extensions/v1beta1/ingresses";
const kubeApi = new KubeApi({
objectConstructor: KubeObject,
apiBase,
fallbackApiBases: [fallbackApiBase],
checkPreferredVersion: true,

View File

@ -19,16 +19,37 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { KubeObjectStore } from "../kube-object.store";
import type { KubeObjectStore, KubeObjectStoreConstructor } from "../kube-object.store";
import { action, observable } from "mobx";
import { autobind } from "../utils";
import { KubeApi, parseKubeApi } from "./kube-api";
import { autobind, Singleton } from "../utils";
import type { KubeApi } from "./kube-api";
import { parseKubeApi } from "./kube-api-parse";
import type { IKubeApiLinkRef, IKubeObjectRef } from "./kube-api-parse";
import type { KubeObject } from "./kube-object";
import type { Cluster } from "../../main/cluster";
export function createKubeApiURL(ref: IKubeApiLinkRef): string {
const { apiPrefix = "/apis", resource, apiVersion, name } = ref;
let { namespace } = ref;
if (namespace) {
namespace = `namespaces/${namespace}`;
}
return [apiPrefix, apiVersion, namespace, resource, name]
.filter(v => v)
.join("/");
}
@autobind()
export class ApiManager {
export class ApiManager extends Singleton {
private apis = observable.map<string, KubeApi>();
private stores = observable.map<string, KubeObjectStore>();
private stores = observable.map<string, KubeObjectStore<KubeObject>>();
constructor(protected cluster: Cluster) {
super();
}
getApi(pathOrCallback: string | ((api: KubeApi) => boolean)) {
if (typeof pathOrCallback === "string") {
@ -71,15 +92,56 @@ export class ApiManager {
}
@action
registerStore(store: KubeObjectStore, apis: KubeApi[] = [store.api]) {
apis.forEach(api => {
registerStore<Store extends KubeObjectStore<KubeObject>>(storeConstructor: KubeObjectStoreConstructor<Store>, apis?: KubeApi[]): this {
const store = new storeConstructor(this.cluster);
for (const api of apis ?? [store.api]) {
this.stores.set(api.apiBase, store);
});
}
return this;
}
getStore<S extends KubeObjectStore>(api: string | KubeApi): S {
return this.stores.get(this.resolveApi(api)?.apiBase) as S;
getStore<Store extends KubeObjectStore<KubeObject>>(api: string | KubeApi): Store | undefined {
return this.stores.get(this.resolveApi(api)?.apiBase) as Store;
}
lookupApiLink(ref: IKubeObjectRef, parentObject: KubeObject): string {
const {
kind, apiVersion, name,
namespace = parentObject.getNs()
} = ref;
if (!kind) return "";
// search in registered apis by 'kind' & 'apiVersion'
const api = this.getApi(api => api.kind === kind && api.apiVersionWithGroup == apiVersion);
if (api) {
return api.getUrl({ namespace, name });
}
// lookup api by generated resource link
const apiPrefixes = ["/apis", "/api"];
const resource = kind.endsWith("s") ? `${kind.toLowerCase()}es` : `${kind.toLowerCase()}s`;
for (const apiPrefix of apiPrefixes) {
const apiLink = createKubeApiURL({ apiPrefix, apiVersion, name, namespace, resource });
if (this.getApi(apiLink)) {
return apiLink;
}
}
// resolve by kind only (hpa's might use refs to older versions of resources for example)
const apiByKind = this.getApi(api => api.kind === kind);
if (apiByKind) {
return apiByKind.getUrl({ name, namespace });
}
// otherwise generate link with default prefix
// resource still might exists in k8s, but api is not registered in the app
return createKubeApiURL({ apiVersion, name, namespace, resource });
}
}
export const apiManager = new ApiManager();

View File

@ -23,7 +23,7 @@ import { IMetrics, IMetricsReqParams, metricsApi } from "./metrics.api";
import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api";
export class ClusterApi extends KubeApi<Cluster> {
export class ClusterApi extends KubeApi<KubeCluster> {
static kind = "Cluster";
static namespaced = true;
@ -71,7 +71,7 @@ export interface IClusterMetrics<T = IMetrics> {
fsUsage: T;
}
export class Cluster extends KubeObject {
export class KubeCluster extends KubeObject {
static kind = "Cluster";
static apiBase = "/apis/cluster.k8s.io/v1alpha1/clusters";
@ -117,5 +117,5 @@ export class Cluster extends KubeObject {
}
export const clusterApi = new ClusterApi({
objectConstructor: Cluster,
objectConstructor: KubeCluster,
});

View File

@ -108,6 +108,6 @@ export class PersistentVolumeClaim extends KubeObject {
}
}
export const pvcApi = new PersistentVolumeClaimsApi({
export const persistentVolumeClaimsApi = new PersistentVolumeClaimsApi({
objectConstructor: PersistentVolumeClaim,
});

View File

@ -65,6 +65,6 @@ export class PodDisruptionBudget extends KubeObject {
}
export const pdbApi = new KubeApi({
export const podDisruptionBudgetApi = new KubeApi({
objectConstructor: PodDisruptionBudget,
});

View File

@ -110,6 +110,6 @@ export class PodSecurityPolicy extends KubeObject {
}
}
export const pspApi = new KubeApi({
export const podSecurityPolicyApi = new KubeApi({
objectConstructor: PodSecurityPolicy,
});

View File

@ -20,35 +20,21 @@
*/
import jsYaml from "js-yaml";
import { KubeObject } from "../kube-object";
import type { KubeJsonApiData } from "../kube-json-api";
import { apiBase } from "../index";
import { apiManager } from "../api-manager";
export const resourceApplierApi = {
annotations: [
"kubectl.kubernetes.io/last-applied-configuration"
],
async update<D extends KubeObject>(resource: object | string): Promise<D> {
async update(resource: object | string): Promise<KubeJsonApiData> {
if (typeof resource === "string") {
resource = jsYaml.safeLoad(resource);
}
return apiBase
.post<KubeJsonApiData[]>("/stack", { data: resource })
.then(data => {
const items = data.map(obj => {
const api = apiManager.getApiByKind(obj.kind, obj.apiVersion);
const result = await apiBase.post<KubeJsonApiData[]>("/stack", { data: resource });
if (api) {
return new api.objectConstructor(obj);
} else {
return new KubeObject(obj);
}
});
return items.length === 1 ? items[0] : items;
});
return result[0];
}
};

View File

@ -21,9 +21,7 @@
// Parse kube-api path and get api-version, group, etc.
import type { KubeObject } from "./kube-object";
import { splitArray } from "../../common/utils";
import { apiManager } from "./api-manager";
export interface IKubeObjectRef {
kind: string;
@ -123,55 +121,3 @@ export function parseKubeApi(path: string): IKubeApiParsed {
namespace, resource, name,
};
}
export function createKubeApiURL(ref: IKubeApiLinkRef): string {
const { apiPrefix = "/apis", resource, apiVersion, name } = ref;
let { namespace } = ref;
if (namespace) {
namespace = `namespaces/${namespace}`;
}
return [apiPrefix, apiVersion, namespace, resource, name]
.filter(v => v)
.join("/");
}
export function lookupApiLink(ref: IKubeObjectRef, parentObject: KubeObject): string {
const {
kind, apiVersion, name,
namespace = parentObject.getNs()
} = ref;
if (!kind) return "";
// search in registered apis by 'kind' & 'apiVersion'
const api = apiManager.getApi(api => api.kind === kind && api.apiVersionWithGroup == apiVersion);
if (api) {
return api.getUrl({ namespace, name });
}
// lookup api by generated resource link
const apiPrefixes = ["/apis", "/api"];
const resource = kind.endsWith("s") ? `${kind.toLowerCase()}es` : `${kind.toLowerCase()}s`;
for (const apiPrefix of apiPrefixes) {
const apiLink = createKubeApiURL({ apiPrefix, apiVersion, name, namespace, resource });
if (apiManager.getApi(apiLink)) {
return apiLink;
}
}
// resolve by kind only (hpa's might use refs to older versions of resources for example)
const apiByKind = apiManager.getApi(api => api.kind === kind);
if (apiByKind) {
return apiByKind.getUrl({ name, namespace });
}
// otherwise generate link with default prefix
// resource still might exists in k8s, but api is not registered in the app
return createKubeApiURL({ apiVersion, name, namespace, resource });
}

View File

@ -25,9 +25,9 @@ import merge from "lodash/merge";
import { stringify } from "querystring";
import { apiKubePrefix, isDevelopment, isTestEnv } from "../../common/vars";
import logger from "../../main/logger";
import { apiManager } from "./api-manager";
import { ApiManager, createKubeApiURL } from "./api-manager";
import { apiKube } from "./index";
import { createKubeApiURL, parseKubeApi } from "./kube-api-parse";
import { parseKubeApi } from "./kube-api-parse";
import { IKubeObjectConstructor, KubeObject, KubeStatus } from "./kube-object";
import byline from "byline";
import type { IKubeWatchEvent } from "./kube-watch-api";
@ -49,18 +49,13 @@ export interface IKubeApiOptions<T extends KubeObject> {
*/
fallbackApiBases?: string[];
objectConstructor?: IKubeObjectConstructor<T>;
objectConstructor: IKubeObjectConstructor<T>;
request?: KubeJsonApi;
isNamespaced?: boolean;
kind?: string;
checkPreferredVersion?: boolean;
}
export interface KubeApiListOptions {
namespace?: string;
reqInit?: RequestInit;
}
export interface IKubeApiQueryParams {
watch?: boolean | number;
resourceVersion?: string;
@ -152,7 +147,7 @@ export class KubeApi<T extends KubeObject = any> {
constructor(protected options: IKubeApiOptions<T>) {
const {
objectConstructor = KubeObject as IKubeObjectConstructor,
objectConstructor,
request = apiKube,
kind = options.objectConstructor?.kind,
isNamespaced = options.objectConstructor?.namespaced
@ -174,8 +169,7 @@ export class KubeApi<T extends KubeObject = any> {
this.objectConstructor = objectConstructor;
this.checkPreferredVersion();
this.parseResponse = this.parseResponse.bind(this);
apiManager.registerApi(apiBase, this);
ApiManager.getInstance().registerApi(apiBase, this);
}
get apiVersionWithGroup() {
@ -264,7 +258,7 @@ export class KubeApi<T extends KubeObject = any> {
if (this.apiVersionPreferred) {
Object.defineProperty(this, "apiBase", { value: this.getUrl() });
apiManager.registerApi(this.apiBase, this);
ApiManager.getInstance().registerApi(this.apiBase, this);
}
}
}
@ -506,5 +500,3 @@ export class KubeApi<T extends KubeObject = any> {
}
}
}
export * from "./kube-api-parse";

View File

@ -265,11 +265,11 @@ export class KubeObject implements ItemObject {
}
// use unified resource-applier api for updating all k8s objects
async update<T extends KubeObject>(data: Partial<T>) {
return resourceApplierApi.update<T>({
async updateReturnNew(data: Partial<this>): Promise<this> {
return new (this.constructor as any)(resourceApplierApi.update({
...this.toPlainObject(),
...data,
});
}));
}
delete(params?: JsonApiParams) {

View File

@ -23,14 +23,17 @@
// API: https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams
import type { KubeObjectStore } from "../kube-object.store";
import type { ClusterContext } from "../components/context";
import { allNamespaces, selectedNamespaces } from "../components/context";
import plimit from "p-limit";
import { comparer, IReactionDisposer, observable, reaction, when } from "mobx";
import { autobind, noop } from "../utils";
import type { KubeApi } from "./kube-api";
import { comparer, IReactionDisposer, reaction } from "mobx";
import { autobind, Disposer, noop, Singleton } from "../utils";
import type { KubeJsonApiData } from "./kube-json-api";
import { isDebugging, isProduction } from "../../common/vars";
import type { Cluster } from "../../main/cluster";
import type { KubeObject } from "./kube-object";
import type { KubeApi } from "./kube-api";
import { ApiManager } from "./api-manager";
export interface IKubeWatchEvent<T = KubeJsonApiData> {
type: "ADDED" | "MODIFIED" | "DELETED" | "ERROR";
@ -51,16 +54,12 @@ export interface IKubeWatchLog {
}
@autobind()
export class KubeWatchApi {
@observable context: ClusterContext = null;
contextReady = when(() => Boolean(this.context));
isAllowedApi(api: KubeApi): boolean {
return Boolean(this.context?.cluster.isAllowedResource(api.kind));
export class KubeWatchApi extends Singleton {
constructor(protected cluster: Cluster) {
super();
}
preloadStores(stores: KubeObjectStore[], opts: { namespaces?: string[], loadOnce?: boolean } = {}) {
private preloadStores(stores: KubeObjectStore<KubeObject>[], opts: { namespaces?: string[], loadOnce?: boolean } = {}) {
const limitRequests = plimit(1); // load stores one by one to allow quick skipping when fast clicking btw pages
const preloading: Promise<any>[] = [];
@ -78,9 +77,15 @@ export class KubeWatchApi {
};
}
subscribeStores(stores: KubeObjectStore[], opts: IKubeWatchSubscribeStoreOptions = {}): () => void {
subscribeApis(apis: KubeApi[], opts: IKubeWatchSubscribeStoreOptions = {}): Disposer {
const manager = ApiManager.getInstance();
return this.subscribeStores(apis.map(api => manager.getStore(api)), opts);
}
subscribeStores(stores: KubeObjectStore<KubeObject>[], opts: IKubeWatchSubscribeStoreOptions = {}): Disposer {
const { preload = true, waitUntilLoaded = true, loadOnce = false, } = opts;
const subscribingNamespaces = opts.namespaces ?? this.context?.allNamespaces ?? [];
const subscribingNamespaces = opts.namespaces ?? allNamespaces(this.cluster);
const unsubscribeList: Function[] = [];
let isUnsubscribed = false;
@ -109,7 +114,7 @@ export class KubeWatchApi {
}
// reload stores only for context namespaces change
cancelReloading = reaction(() => this.context?.contextNamespaces, namespaces => {
cancelReloading = reaction(() => selectedNamespaces(), namespaces => {
preloading?.cancelLoading();
unsubscribeList.forEach(unsubscribe => unsubscribe());
unsubscribeList.length = 0;
@ -149,5 +154,3 @@ export class KubeWatchApi {
}
}
}
export const kubeWatchApi = new KubeWatchApi();

View File

@ -37,7 +37,7 @@ import { ExtensionDiscovery } from "../extensions/extension-discovery";
import { ExtensionLoader } from "../extensions/extension-loader";
import { ExtensionsStore } from "../extensions/extensions-store";
import { FilesystemProvisionerStore } from "../main/extension-filesystem";
import { App } from "./components/app";
import { ClusterFrame } from "./components/app";
import { LensApp } from "./lens-app";
import { ThemeStore } from "./theme.store";
import { HelmRepoManager } from "../main/helm/helm-repo-manager";
@ -127,4 +127,4 @@ export async function bootstrap(App: AppComponent) {
}
// run
bootstrap(process.isMainFrame ? LensApp : App);
bootstrap(process.isMainFrame ? LensApp : ClusterFrame);

View File

@ -38,6 +38,7 @@ import { PageLayout } from "../layout/page-layout";
import { docsUrl } from "../../../common/vars";
import { Input } from "../input";
import { catalogURL, preferencesURL } from "../../../common/routes";
import { embedCustomKubeConfig } from "../../utils";
@observer
export class AddCluster extends React.Component {
@ -107,7 +108,7 @@ export class AddCluster extends React.Component {
}).map(context => {
const clusterId = uuid();
const kubeConfig = this.kubeContexts.get(context);
const kubeConfigPath = ClusterStore.embedCustomKubeConfig(clusterId, kubeConfig); // save in app-files folder
const kubeConfigPath = embedCustomKubeConfig(clusterId, kubeConfig); // save in app-files folder
return {
id: clusterId,

View File

@ -37,14 +37,14 @@ import { Spinner } from "../spinner";
import { Table, TableCell, TableHead, TableRow } from "../table";
import { AceEditor } from "../ace-editor";
import { Button } from "../button";
import { releaseStore } from "./release.store";
import { ReleaseStore } from "./release.store";
import { Notifications } from "../notifications";
import { createUpgradeChartTab } from "../dock/upgrade-chart.store";
import { ThemeStore } from "../../theme.store";
import { apiManager } from "../../api/api-manager";
import { ApiManager } from "../../api/api-manager";
import { SubTitle } from "../layout/sub-title";
import { secretsStore } from "../+config-secrets/secrets.store";
import { Secret } from "../../api/endpoints";
import type { SecretsStore } from "../+config-secrets/secrets.store";
import { Secret, secretsApi } from "../../api/endpoints";
import { getDetailsUrl } from "../kube-object";
import { Checkbox } from "../checkbox";
@ -62,6 +62,10 @@ export class ReleaseDetails extends Component<Props> {
@observable saving = false;
@observable releaseSecret: Secret;
private get secretsStore() {
return ApiManager.getInstance().getStore<SecretsStore>(secretsApi);
}
@disposeOnUnmount
releaseSelector = reaction(() => this.props.release, release => {
if (!release) return;
@ -72,9 +76,9 @@ export class ReleaseDetails extends Component<Props> {
);
@disposeOnUnmount
secretWatcher = reaction(() => secretsStore.items.toJS(), () => {
secretWatcher = reaction(() => this.secretsStore.items.toJS(), () => {
if (!this.props.release) return;
const { getReleaseSecret } = releaseStore;
const { getReleaseSecret } = ReleaseStore.getInstance();
const { release } = this.props;
const secret = getReleaseSecret(release);
@ -115,7 +119,7 @@ export class ReleaseDetails extends Component<Props> {
this.saving = true;
try {
await releaseStore.update(name, namespace, data);
await ReleaseStore.getInstance().update(name, namespace, data);
Notifications.ok(
<p>Release <b>{name}</b> successfully updated!</p>
);
@ -197,7 +201,7 @@ export class ReleaseDetails extends Component<Props> {
{items.map(item => {
const name = item.getName();
const namespace = item.getNs();
const api = apiManager.getApi(item.metadata.selfLink);
const api = ApiManager.getInstance().getApi(item.metadata.selfLink);
const detailsUrl = api ? getDetailsUrl(api.getUrl({ name, namespace })) : "";
return (

View File

@ -22,12 +22,12 @@
import React from "react";
import type { HelmRelease } from "../../api/endpoints/helm-releases.api";
import { autobind, cssNames } from "../../utils";
import { releaseStore } from "./release.store";
import { MenuActions, MenuActionsProps } from "../menu/menu-actions";
import { MenuItem } from "../menu";
import { Icon } from "../icon";
import { ReleaseRollbackDialog } from "./release-rollback-dialog";
import { createUpgradeChartTab } from "../dock/upgrade-chart.store";
import { ReleaseStore } from "./release.store";
interface Props extends MenuActionsProps {
release: HelmRelease;
@ -37,7 +37,7 @@ interface Props extends MenuActionsProps {
export class HelmReleaseMenu extends React.Component<Props> {
@autobind()
remove() {
return releaseStore.remove(this.props.release);
return ReleaseStore.getInstance().remove(this.props.release);
}
@autobind()

View File

@ -27,7 +27,7 @@ import { observer } from "mobx-react";
import { Dialog, DialogProps } from "../dialog";
import { Wizard, WizardStep } from "../wizard";
import { getReleaseHistory, HelmRelease, IReleaseRevision } from "../../api/endpoints/helm-releases.api";
import { releaseStore } from "./release.store";
import { ReleaseStore } from "./release.store";
import { Select, SelectOption } from "../select";
import { Notifications } from "../notifications";
import orderBy from "lodash/orderBy";
@ -73,7 +73,7 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
const revisionNumber = this.revision.revision;
try {
await releaseStore.rollback(this.release.getName(), this.release.getNs(), revisionNumber);
await ReleaseStore.getInstance().rollback(this.release.getName(), this.release.getNs(), revisionNumber);
this.close();
} catch (err) {
Notifications.error(err);

View File

@ -24,24 +24,44 @@ import { action, observable, reaction, when } from "mobx";
import { autobind } from "../../utils";
import { createRelease, deleteRelease, HelmRelease, IReleaseCreatePayload, IReleaseUpdatePayload, listReleases, rollbackRelease, updateRelease } from "../../api/endpoints/helm-releases.api";
import { ItemStore } from "../../item.store";
import type { Secret } from "../../api/endpoints";
import { secretsStore } from "../+config-secrets/secrets.store";
import { namespaceStore } from "../+namespaces/namespace.store";
import { Secret, secretsApi } from "../../api/endpoints";
import type { SecretsStore } from "../+config-secrets/secrets.store";
import { Notifications } from "../notifications";
import { ApiManager } from "../../api/api-manager";
import { isLoadingFromAllNamespaces, selectedNamespaces } from "../context";
import type { Cluster } from "../../../main/cluster";
@autobind()
export class ReleaseStore extends ItemStore<HelmRelease> {
private static instance?: ReleaseStore;
static getInstance() {
if (!this.instance) {
throw new TypeError("instance of ReleaseStore is not created");
}
return this.instance;
}
static createInstance(cluster: Cluster) {
return this.instance ??= new this(cluster);
}
private get secretsStore() {
return ApiManager.getInstance().getStore<SecretsStore>(secretsApi);
}
releaseSecrets = observable.map<string, Secret>();
constructor() {
private constructor(protected cluster: Cluster) {
super();
when(() => secretsStore.isLoaded, () => {
when(() => this.secretsStore.isLoaded, () => {
this.releaseSecrets.replace(this.getReleaseSecrets());
});
}
watchAssociatedSecrets(): (() => void) {
return reaction(() => secretsStore.items.toJS(), () => {
return reaction(() => this.secretsStore.items.toJS(), () => {
if (this.isLoading) return;
const newSecrets = this.getReleaseSecrets();
const amountChanged = newSecrets.length !== this.releaseSecrets.size;
@ -57,19 +77,19 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
}
watchSelecteNamespaces(): (() => void) {
return reaction(() => namespaceStore.context.contextNamespaces, namespaces => {
return reaction(() => selectedNamespaces(), namespaces => {
this.loadAll(namespaces);
});
}
private getReleaseSecrets() {
return secretsStore
return this.secretsStore
.getByLabel({ owner: "helm" })
.map(s => [s.getId(), s] as const);
}
getReleaseSecret(release: HelmRelease) {
return secretsStore.getByLabel({
return this.secretsStore.getByLabel({
owner: "helm",
name: release.getName()
})
@ -100,15 +120,11 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
}
async loadFromContextNamespaces(): Promise<void> {
return this.loadAll(namespaceStore.context.contextNamespaces);
return this.loadAll(selectedNamespaces());
}
async loadItems(namespaces: string[]) {
const isLoadingAll = namespaceStore.context.allNamespaces?.length > 1
&& namespaceStore.context.cluster.accessibleNamespaces.length === 0
&& namespaceStore.context.allNamespaces.every(ns => namespaces.includes(ns));
if (isLoadingAll) {
if (isLoadingFromAllNamespaces(this.cluster, namespaces)) {
return listReleases();
}
@ -149,5 +165,3 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
return Promise.all(this.selectedItems.map(this.remove));
}
}
export const releaseStore = new ReleaseStore();

View File

@ -25,15 +25,17 @@ import React, { Component } from "react";
import kebabCase from "lodash/kebabCase";
import { disposeOnUnmount, observer } from "mobx-react";
import type { RouteComponentProps } from "react-router";
import { releaseStore } from "./release.store";
import { ReleaseStore } from "./release.store";
import type { HelmRelease } from "../../api/endpoints/helm-releases.api";
import { ReleaseDetails } from "./release-details";
import { ReleaseRollbackDialog } from "./release-rollback-dialog";
import { navigation } from "../../navigation";
import { ItemListLayout } from "../item-object-list/item-list-layout";
import { HelmReleaseMenu } from "./release-menu";
import { secretsStore } from "../+config-secrets/secrets.store";
import type { SecretsStore } from "../+config-secrets/secrets.store";
import { ReleaseRouteParams, releaseURL } from "../../../common/routes";
import { secretsApi } from "../../api/endpoints";
import { ApiManager } from "../../api/api-manager";
enum columnId {
name = "name",
@ -51,17 +53,21 @@ interface Props extends RouteComponentProps<ReleaseRouteParams> {
@observer
export class HelmReleases extends Component<Props> {
private get secretsStore() {
return ApiManager.getInstance().getStore<SecretsStore>(secretsApi);
}
componentDidMount() {
disposeOnUnmount(this, [
releaseStore.watchAssociatedSecrets(),
releaseStore.watchSelecteNamespaces(),
ReleaseStore.getInstance().watchAssociatedSecrets(),
ReleaseStore.getInstance().watchSelecteNamespaces(),
]);
}
get selectedRelease() {
const { match: { params: { name, namespace } } } = this.props;
return releaseStore.items.find(release => {
return ReleaseStore.getInstance().items.find(release => {
return release.getName() == name && release.getNs() == namespace;
});
}
@ -99,8 +105,8 @@ export class HelmReleases extends Component<Props> {
isConfigurable
tableId="helm_releases"
className="HelmReleases"
store={releaseStore}
dependentStores={[secretsStore]}
store={ReleaseStore.getInstance()}
dependentStores={[this.secretsStore]}
sortingCallbacks={{
[columnId.name]: (release: HelmRelease) => release.getName(),
[columnId.namespace]: (release: HelmRelease) => release.getNs(),

View File

@ -26,10 +26,11 @@ import { HelmCharts } from "../+apps-helm-charts";
import { HelmReleases } from "../+apps-releases";
import { namespaceUrlParam } from "../+namespaces/namespace.store";
import { helmChartsURL, helmChartsRoute, releaseURL, releaseRoute } from "../../../common/routes";
import type { Cluster } from "../../../main/cluster";
@observer
export class Apps extends React.Component {
static get tabRoutes(): TabLayoutRoute[] {
export class Apps extends React.Component<{ cluster: Cluster }> {
static tabRoutes(): TabLayoutRoute[] {
const query = namespaceUrlParam.toObjectParam();
return [
@ -50,7 +51,7 @@ export class Apps extends React.Component {
render() {
return (
<TabLayout className="Apps" tabs={Apps.tabRoutes}/>
<TabLayout className="Apps" tabs={Apps.tabRoutes()}/>
);
}
}

View File

@ -27,14 +27,15 @@ import { computed } from "mobx";
import { Icon } from "../icon";
import { SubHeader } from "../layout/sub-header";
import { Table, TableCell, TableHead, TableRow } from "../table";
import { nodesStore } from "../+nodes/nodes.store";
import { eventStore } from "../+events/event.store";
import type { NodesStore } from "../+nodes";
import type { EventStore } from "../+events/event.store";
import { autobind, cssNames, prevDefault } from "../../utils";
import type { ItemObject } from "../../item.store";
import { Spinner } from "../spinner";
import { ThemeStore } from "../../theme.store";
import { lookupApiLink } from "../../api/kube-api";
import { kubeSelectedUrlParam, showDetails } from "../kube-object";
import { ApiManager } from "../../api/api-manager";
import { eventApi, nodesApi } from "../../api/endpoints";
interface Props {
className?: string;
@ -62,11 +63,19 @@ export class ClusterIssues extends React.Component<Props> {
[sortBy.age]: (warning: IWarning) => warning.timeDiffFromNow,
};
private get nodesStore() {
return ApiManager.getInstance().getStore<NodesStore>(nodesApi);
}
private get eventStore() {
return ApiManager.getInstance().getStore<EventStore>(eventApi);
}
@computed get warnings() {
const warnings: IWarning[] = [];
// Node bad conditions
nodesStore.items.forEach(node => {
this.nodesStore.items.forEach(node => {
const { kind, selfLink, getId, getName, getAge, getTimeDiffFromNow } = node;
node.getWarningConditions().forEach(({ message }) => {
@ -83,7 +92,7 @@ export class ClusterIssues extends React.Component<Props> {
});
// Warning events for Workloads
const events = eventStore.getWarnings();
const events = this.eventStore.getWarnings();
events.forEach(error => {
const { message, involvedObject, getAge, getTimeDiffFromNow } = error;
@ -96,7 +105,7 @@ export class ClusterIssues extends React.Component<Props> {
age: getAge(),
message,
kind,
selfLink: lookupApiLink(involvedObject, error),
selfLink: ApiManager.getInstance().lookupApiLink(involvedObject, error),
});
});
@ -135,7 +144,7 @@ export class ClusterIssues extends React.Component<Props> {
renderContent() {
const { warnings } = this;
if (!eventStore.isLoaded) {
if (!this.eventStore.isLoaded) {
return (
<Spinner center/>
);

View File

@ -23,12 +23,16 @@ import "./cluster-metric-switchers.scss";
import React from "react";
import { observer } from "mobx-react";
import { nodesStore } from "../+nodes/nodes.store";
import type { NodesStore } from "../+nodes";
import { cssNames } from "../../utils";
import { Radio, RadioGroup } from "../radio";
import { clusterOverviewStore, MetricNodeRole, MetricType } from "./cluster-overview.store";
import { ClusterObjectStore, MetricNodeRole, MetricType } from "./cluster-overview.store";
import { ApiManager } from "../../api/api-manager";
import { clusterApi, nodesApi } from "../../api/endpoints";
export const ClusterMetricSwitchers = observer(() => {
const nodesStore = ApiManager.getInstance().getStore<NodesStore>(nodesApi);
const clusterOverviewStore = ApiManager.getInstance().getStore<ClusterObjectStore>(clusterApi);
const { metricType, metricNodeRole, getMetricsValues, metrics } = clusterOverviewStore;
const { masterNodes, workerNodes } = nodesStore;
const metricsValues = getMetricsValues(metrics);

View File

@ -24,7 +24,7 @@ import "./cluster-metrics.scss";
import React from "react";
import { observer } from "mobx-react";
import type { ChartOptions, ChartPoint } from "chart.js";
import { clusterOverviewStore, MetricType } from "./cluster-overview.store";
import { ClusterObjectStore, MetricType } from "./cluster-overview.store";
import { BarChart } from "../chart";
import { bytesToUnits } from "../../utils";
import { Spinner } from "../spinner";
@ -32,8 +32,11 @@ import { ZebraStripes } from "../chart/zebra-stripes.plugin";
import { ClusterNoMetrics } from "./cluster-no-metrics";
import { ClusterMetricSwitchers } from "./cluster-metric-switchers";
import { getMetricLastPoints } from "../../api/endpoints/metrics.api";
import { ApiManager } from "../../api/api-manager";
import { clusterApi } from "../../api/endpoints";
export const ClusterMetrics = observer(() => {
const clusterOverviewStore = ApiManager.getInstance().getStore<ClusterObjectStore>(clusterApi);
const { metricType, metricNodeRole, getMetricsValues, metricsLoaded, metrics } = clusterOverviewStore;
const { memoryCapacity, cpuCapacity } = getMetricLastPoints(clusterOverviewStore.metrics);
const metricValues = getMetricsValues(metrics);

View File

@ -21,11 +21,11 @@
import { action, observable, reaction, when } from "mobx";
import { KubeObjectStore } from "../../kube-object.store";
import { Cluster, clusterApi, IClusterMetrics } from "../../api/endpoints";
import { KubeCluster, clusterApi, IClusterMetrics, nodesApi } from "../../api/endpoints";
import { autobind, createStorage } from "../../utils";
import { IMetricsReqParams, normalizeMetrics } from "../../api/endpoints/metrics.api";
import { nodesStore } from "../+nodes/nodes.store";
import { apiManager } from "../../api/api-manager";
import type { NodesStore } from "../+nodes/nodes.store";
import { ApiManager } from "../../api/api-manager";
export enum MetricType {
MEMORY = "memory",
@ -43,7 +43,7 @@ export interface ClusterOverviewStorageState {
}
@autobind()
export class ClusterOverviewStore extends KubeObjectStore<Cluster> implements ClusterOverviewStorageState {
export class ClusterObjectStore extends KubeObjectStore<KubeCluster> implements ClusterOverviewStorageState {
api = clusterApi;
@observable metrics: Partial<IClusterMetrics> = {};
@ -70,12 +70,11 @@ export class ClusterOverviewStore extends KubeObjectStore<Cluster> implements Cl
this.storage.merge({ metricNodeRole: value });
}
constructor() {
super();
this.init();
private get nodesStore() {
return ApiManager.getInstance().getStore<NodesStore>(nodesApi);
}
private init() {
protected init = () => {
// TODO: refactor, seems not a correct place to be
// auto-refresh metrics on user-action
reaction(() => this.metricNodeRole, () => {
@ -85,18 +84,18 @@ export class ClusterOverviewStore extends KubeObjectStore<Cluster> implements Cl
});
// check which node type to select
reaction(() => nodesStore.items.length, () => {
const { masterNodes, workerNodes } = nodesStore;
reaction(() => this.nodesStore.items.length, () => {
const { masterNodes, workerNodes } = this.nodesStore;
if (!masterNodes.length) this.metricNodeRole = MetricNodeRole.WORKER;
if (!workerNodes.length) this.metricNodeRole = MetricNodeRole.MASTER;
});
}
};
@action
async loadMetrics(params?: IMetricsReqParams) {
await when(() => nodesStore.isLoaded);
const { masterNodes, workerNodes } = nodesStore;
await when(() => this.nodesStore.isLoaded);
const { masterNodes, workerNodes } = this.nodesStore;
const nodes = this.metricNodeRole === MetricNodeRole.MASTER && masterNodes.length ? masterNodes : workerNodes;
this.metrics = await clusterApi.getMetrics(nodes.map(node => node.getName()), params);
@ -126,6 +125,3 @@ export class ClusterOverviewStore extends KubeObjectStore<Cluster> implements Cl
this.storage?.reset();
}
}
export const clusterOverviewStore = new ClusterOverviewStore();
apiManager.registerStore(clusterOverviewStore);

View File

@ -24,24 +24,38 @@ import "./cluster-overview.scss";
import React from "react";
import { reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import { nodesStore } from "../+nodes/nodes.store";
import { podsStore } from "../+workloads-pods/pods.store";
import { ClusterStore, getHostedCluster } from "../../../common/cluster-store";
import { interval } from "../../utils";
import { TabLayout } from "../layout/tab-layout";
import { Spinner } from "../spinner";
import { ClusterIssues } from "./cluster-issues";
import { ClusterMetrics } from "./cluster-metrics";
import { clusterOverviewStore } from "./cluster-overview.store";
import { ClusterPieCharts } from "./cluster-pie-charts";
import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting";
import { clusterApi, nodesApi, podsApi } from "../../api/endpoints";
import type { NodesStore } from "../+nodes";
import type { PodsStore } from "../+workloads-pods";
import { ApiManager } from "../../api/api-manager";
import type { ClusterObjectStore } from "./cluster-overview.store";
@observer
export class ClusterOverview extends React.Component {
private get nodesStore() {
return ApiManager.getInstance().getStore<NodesStore>(nodesApi);
}
private get podsStore() {
return ApiManager.getInstance().getStore<PodsStore>(podsApi);
}
private get clusterObjectStore() {
return ApiManager.getInstance().getStore<ClusterObjectStore>(clusterApi);
}
private metricPoller = interval(60, () => this.loadMetrics());
loadMetrics() {
getHostedCluster().available && clusterOverviewStore.loadMetrics();
getHostedCluster().available && this.clusterObjectStore.loadMetrics();
}
componentDidMount() {
@ -49,7 +63,7 @@ export class ClusterOverview extends React.Component {
disposeOnUnmount(this, [
reaction(
() => clusterOverviewStore.metricNodeRole, // Toggle Master/Worker node switcher
() => this.clusterObjectStore.metricNodeRole, // Toggle Master/Worker node switcher
() => this.metricPoller.restart(true)
),
]);
@ -86,7 +100,7 @@ export class ClusterOverview extends React.Component {
}
render() {
const isLoaded = nodesStore.isLoaded && podsStore.isLoaded;
const isLoaded = this.nodesStore.isLoaded && this.podsStore.isLoaded;
const isMetricsHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.Cluster);
return (

View File

@ -23,17 +23,22 @@ import "./cluster-pie-charts.scss";
import React from "react";
import { observer } from "mobx-react";
import { clusterOverviewStore, MetricNodeRole } from "./cluster-overview.store";
import { ClusterObjectStore, MetricNodeRole } from "./cluster-overview.store";
import { Spinner } from "../spinner";
import { Icon } from "../icon";
import { nodesStore } from "../+nodes/nodes.store";
import type { NodesStore } from "../+nodes";
import { ChartData, PieChart } from "../chart";
import { ClusterNoMetrics } from "./cluster-no-metrics";
import { bytesToUnits } from "../../utils";
import { ThemeStore } from "../../theme.store";
import { getMetricLastPoints } from "../../api/endpoints/metrics.api";
import { ApiManager } from "../../api/api-manager";
import { clusterApi, nodesApi } from "../../api/endpoints";
export const ClusterPieCharts = observer(() => {
const clusterOverviewStore = ApiManager.getInstance().getStore<ClusterObjectStore>(clusterApi);
const nodesStore = ApiManager.getInstance().getStore<NodesStore>(nodesApi);
const renderLimitWarning = () => {
return (
<div className="node-warning flex gaps align-center">

View File

@ -30,8 +30,8 @@ import { KubeObjectDetailsProps, getDetailsUrl } from "../kube-object";
import { cssNames } from "../../utils";
import { HorizontalPodAutoscaler, HpaMetricType, IHpaMetric } from "../../api/endpoints/hpa.api";
import { Table, TableCell, TableHead, TableRow } from "../table";
import { lookupApiLink } from "../../api/kube-api";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { ApiManager } from "../../api/api-manager";
interface Props extends KubeObjectDetailsProps<HorizontalPodAutoscaler> {
}
@ -54,7 +54,7 @@ export class HpaDetails extends React.Component<Props> {
case HpaMetricType.Object:
const { target } = metric.object;
const { kind, name } = target;
const objectUrl = getDetailsUrl(lookupApiLink(target, hpa));
const objectUrl = getDetailsUrl(ApiManager.getInstance().lookupApiLink(target, hpa));
return (
<>
@ -107,7 +107,7 @@ export class HpaDetails extends React.Component<Props> {
<DrawerItem name="Reference">
{scaleTargetRef && (
<Link to={getDetailsUrl(lookupApiLink(scaleTargetRef, hpa))}>
<Link to={getDetailsUrl(ApiManager.getInstance().lookupApiLink(scaleTargetRef, hpa))}>
{scaleTargetRef.kind}/{scaleTargetRef.name}
</Link>
)}

View File

@ -22,12 +22,8 @@
import { autobind } from "../../utils";
import { KubeObjectStore } from "../../kube-object.store";
import { HorizontalPodAutoscaler, hpaApi } from "../../api/endpoints/hpa.api";
import { apiManager } from "../../api/api-manager";
@autobind()
export class HPAStore extends KubeObjectStore<HorizontalPodAutoscaler> {
export class HpaStore extends KubeObjectStore<HorizontalPodAutoscaler> {
api = hpaApi;
}
export const hpaStore = new HPAStore();
apiManager.registerStore(hpaStore);

View File

@ -25,12 +25,13 @@ import React from "react";
import { observer } from "mobx-react";
import type { RouteComponentProps } from "react-router";
import { KubeObjectListLayout } from "../kube-object";
import type { HorizontalPodAutoscaler } from "../../api/endpoints/hpa.api";
import { hpaStore } from "./hpa.store";
import { HorizontalPodAutoscaler, hpaApi } from "../../api/endpoints/hpa.api";
import { Badge } from "../badge";
import { cssNames } from "../../utils";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { IHpaRouteParams } from "../../../common/routes";
import { ApiManager } from "../../api/api-manager";
import type { HpaStore } from "./hpa.store";
enum columnId {
name = "name",
@ -48,6 +49,10 @@ interface Props extends RouteComponentProps<IHpaRouteParams> {
@observer
export class HorizontalPodAutoscalers extends React.Component<Props> {
private get hpaStore() {
return ApiManager.getInstance().getStore<HpaStore>(hpaApi);
}
getTargets(hpa: HorizontalPodAutoscaler) {
const metrics = hpa.getMetrics();
const metricsRemainCount = metrics.length - 1;
@ -62,7 +67,8 @@ export class HorizontalPodAutoscalers extends React.Component<Props> {
<KubeObjectListLayout
isConfigurable
tableId="configuration_hpa"
className="HorizontalPodAutoscalers" store={hpaStore}
className="HorizontalPodAutoscalers"
store={this.hpaStore}
sortingCallbacks={{
[columnId.name]: (item: HorizontalPodAutoscaler) => item.getName(),
[columnId.namespace]: (item: HorizontalPodAutoscaler) => item.getNs(),

View File

@ -20,4 +20,5 @@
*/
export * from "./hpa";
export * from "./hpa.store";
export * from "./hpa-details";

View File

@ -20,4 +20,5 @@
*/
export * from "./limit-ranges";
export * from "./limit-ranges.store";
export * from "./limit-range-details";

View File

@ -21,13 +21,9 @@
import { autobind } from "../../../common/utils/autobind";
import { KubeObjectStore } from "../../kube-object.store";
import { apiManager } from "../../api/api-manager";
import { LimitRange, limitRangeApi } from "../../api/endpoints/limit-range.api";
@autobind()
export class LimitRangesStore extends KubeObjectStore<LimitRange> {
api = limitRangeApi;
}
export const limitRangeStore = new LimitRangesStore();
apiManager.registerStore(limitRangeStore);

View File

@ -24,11 +24,12 @@ import "./limit-ranges.scss";
import type { RouteComponentProps } from "react-router";
import { observer } from "mobx-react";
import { KubeObjectListLayout } from "../kube-object/kube-object-list-layout";
import { limitRangeStore } from "./limit-ranges.store";
import React from "react";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { LimitRange } from "../../api/endpoints/limit-range.api";
import { LimitRange, limitRangeApi } from "../../api/endpoints/limit-range.api";
import type { LimitRangeRouteParams } from "../../../common/routes";
import { ApiManager } from "../../api/api-manager";
import type { LimitRangesStore } from "./limit-ranges.store";
enum columnId {
name = "name",
@ -41,13 +42,17 @@ interface Props extends RouteComponentProps<LimitRangeRouteParams> {
@observer
export class LimitRanges extends React.Component<Props> {
private get limitRangeStore() {
return ApiManager.getInstance().getStore<LimitRangesStore>(limitRangeApi);
}
render() {
return (
<KubeObjectListLayout
isConfigurable
tableId="configuration_limitranges"
className="LimitRanges"
store={limitRangeStore}
store={this.limitRangeStore}
sortingCallbacks={{
[columnId.name]: (item: LimitRange) => item.getName(),
[columnId.namespace]: (item: LimitRange) => item.getNs(),

View File

@ -28,16 +28,21 @@ import { DrawerTitle } from "../drawer";
import { Notifications } from "../notifications";
import { Input } from "../input";
import { Button } from "../button";
import { configMapsStore } from "./config-maps.store";
import type { KubeObjectDetailsProps } from "../kube-object";
import type { ConfigMap } from "../../api/endpoints";
import { ConfigMap, configMapApi } from "../../api/endpoints";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import type { ConfigMapsStore } from ".";
import { ApiManager } from "../../api/api-manager";
interface Props extends KubeObjectDetailsProps<ConfigMap> {
}
@observer
export class ConfigMapDetails extends React.Component<Props> {
private get configMapsStore() {
return ApiManager.getInstance().getStore<ConfigMapsStore>(configMapApi);
}
@observable isSaving = false;
@observable data = observable.map();
@ -58,7 +63,7 @@ export class ConfigMapDetails extends React.Component<Props> {
try {
this.isSaving = true;
await configMapsStore.update(configMap, { ...configMap, data: this.data.toJSON() });
await this.configMapsStore.update(configMap, { ...configMap, data: this.data.toJSON() });
Notifications.ok(
<p>
<>ConfigMap <b>{configMap.getName()}</b> successfully updated.</>

View File

@ -22,12 +22,8 @@
import { KubeObjectStore } from "../../kube-object.store";
import { autobind } from "../../utils";
import { ConfigMap, configMapApi } from "../../api/endpoints/configmap.api";
import { apiManager } from "../../api/api-manager";
@autobind()
export class ConfigMapsStore extends KubeObjectStore<ConfigMap> {
api = configMapApi;
}
export const configMapsStore = new ConfigMapsStore();
apiManager.registerStore(configMapsStore);

View File

@ -24,11 +24,12 @@ import "./config-maps.scss";
import React from "react";
import { observer } from "mobx-react";
import type { RouteComponentProps } from "react-router";
import { configMapsStore } from "./config-maps.store";
import type { ConfigMap } from "../../api/endpoints/configmap.api";
import { ConfigMap, configMapApi } from "../../api/endpoints/configmap.api";
import { KubeObjectListLayout } from "../kube-object";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { ConfigMapsRouteParams } from "../../../common/routes";
import { ApiManager } from "../../api/api-manager";
import type { ConfigMapsStore } from "./config-maps.store";
enum columnId {
name = "name",
@ -42,12 +43,17 @@ interface Props extends RouteComponentProps<ConfigMapsRouteParams> {
@observer
export class ConfigMaps extends React.Component<Props> {
private get configMapsStore() {
return ApiManager.getInstance().getStore<ConfigMapsStore>(configMapApi);
}
render() {
return (
<KubeObjectListLayout
isConfigurable
tableId="configuration_configmaps"
className="ConfigMaps" store={configMapsStore}
className="ConfigMaps"
store={this.configMapsStore}
sortingCallbacks={{
[columnId.name]: (item: ConfigMap) => item.getName(),
[columnId.namespace]: (item: ConfigMap) => item.getNs(),

View File

@ -20,4 +20,5 @@
*/
export * from "./config-maps";
export * from "./config-maps.store";
export * from "./config-map-details";

View File

@ -20,4 +20,5 @@
*/
export * from "./pod-disruption-budgets";
export * from "./pod-disruption-budgets.store";
export * from "./pod-disruption-budgets-details";

View File

@ -21,13 +21,9 @@
import { KubeObjectStore } from "../../kube-object.store";
import { autobind } from "../../utils";
import { PodDisruptionBudget, pdbApi } from "../../api/endpoints/poddisruptionbudget.api";
import { apiManager } from "../../api/api-manager";
import { PodDisruptionBudget, podDisruptionBudgetApi } from "../../api/endpoints/poddisruptionbudget.api";
@autobind()
export class PodDisruptionBudgetsStore extends KubeObjectStore<PodDisruptionBudget> {
api = pdbApi;
api = podDisruptionBudgetApi;
}
export const podDisruptionBudgetsStore = new PodDisruptionBudgetsStore();
apiManager.registerStore(podDisruptionBudgetsStore);

View File

@ -23,10 +23,11 @@ import "./pod-disruption-budgets.scss";
import * as React from "react";
import { observer } from "mobx-react";
import { podDisruptionBudgetsStore } from "./pod-disruption-budgets.store";
import type { PodDisruptionBudget } from "../../api/endpoints/poddisruptionbudget.api";
import { PodDisruptionBudget, podDisruptionBudgetApi } from "../../api/endpoints/poddisruptionbudget.api";
import { KubeObjectDetailsProps, KubeObjectListLayout } from "../kube-object";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { PodDisruptionBudgetsStore } from "./pod-disruption-budgets.store";
import { ApiManager } from "../../api/api-manager";
enum columnId {
name = "name",
@ -43,13 +44,17 @@ interface Props extends KubeObjectDetailsProps<PodDisruptionBudget> {
@observer
export class PodDisruptionBudgets extends React.Component<Props> {
private get podDisruptionBudgetsStore() {
return ApiManager.getInstance().getStore<PodDisruptionBudgetsStore>(podDisruptionBudgetApi);
}
render() {
return (
<KubeObjectListLayout
isConfigurable
tableId="configuration_distribution_budgets"
className="PodDisruptionBudgets"
store={podDisruptionBudgetsStore}
store={this.podDisruptionBudgetsStore}
sortingCallbacks={{
[columnId.name]: (pdb: PodDisruptionBudget) => pdb.getName(),
[columnId.namespace]: (pdb: PodDisruptionBudget) => pdb.getNs(),

View File

@ -20,4 +20,5 @@
*/
export * from "./resource-quotas";
export * from "./resource-quotas.store";
export * from "./resource-quota-details";

View File

@ -22,12 +22,8 @@
import { KubeObjectStore } from "../../kube-object.store";
import { autobind } from "../../utils";
import { ResourceQuota, resourceQuotaApi } from "../../api/endpoints/resource-quota.api";
import { apiManager } from "../../api/api-manager";
@autobind()
export class ResourceQuotasStore extends KubeObjectStore<ResourceQuota> {
api = resourceQuotaApi;
}
export const resourceQuotaStore = new ResourceQuotasStore();
apiManager.registerStore(resourceQuotaStore);

View File

@ -25,11 +25,12 @@ import React from "react";
import { observer } from "mobx-react";
import type { RouteComponentProps } from "react-router";
import { KubeObjectListLayout } from "../kube-object";
import type { ResourceQuota } from "../../api/endpoints/resource-quota.api";
import { ResourceQuota, resourceQuotaApi } from "../../api/endpoints/resource-quota.api";
import { AddQuotaDialog } from "./add-quota-dialog";
import { resourceQuotaStore } from "./resource-quotas.store";
import type { ResourceQuotasStore } from "./resource-quotas.store";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { ResourceQuotaRouteParams } from "../../../common/routes";
import { ApiManager } from "../../api/api-manager";
enum columnId {
name = "name",
@ -42,13 +43,18 @@ interface Props extends RouteComponentProps<ResourceQuotaRouteParams> {
@observer
export class ResourceQuotas extends React.Component<Props> {
private get resourceQuotaStore() {
return ApiManager.getInstance().getStore<ResourceQuotasStore>(resourceQuotaApi);
}
render() {
return (
<>
<KubeObjectListLayout
isConfigurable
tableId="configuration_quotas"
className="ResourceQuotas" store={resourceQuotaStore}
className="ResourceQuotas"
store={this.resourceQuotaStore}
sortingCallbacks={{
[columnId.name]: (item: ResourceQuota) => item.getName(),
[columnId.namespace]: (item: ResourceQuota) => item.getNs(),

View File

@ -20,4 +20,5 @@
*/
export * from "./secrets";
export * from "./secrets.store";
export * from "./secret-details";

View File

@ -31,16 +31,21 @@ import { Button } from "../button";
import { Notifications } from "../notifications";
import { base64 } from "../../utils";
import { Icon } from "../icon";
import { secretsStore } from "./secrets.store";
import type { KubeObjectDetailsProps } from "../kube-object";
import type { Secret } from "../../api/endpoints";
import { Secret, secretsApi } from "../../api/endpoints";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import type { SecretsStore } from ".";
import { ApiManager } from "../../api/api-manager";
interface Props extends KubeObjectDetailsProps<Secret> {
}
@observer
export class SecretDetails extends React.Component<Props> {
private get secretsStore() {
return ApiManager.getInstance().getStore<SecretsStore>(secretsApi);
}
@observable isSaving = false;
@observable data: { [name: string]: string } = {};
@observable revealSecret: { [name: string]: boolean } = {};
@ -64,7 +69,7 @@ export class SecretDetails extends React.Component<Props> {
this.isSaving = true;
try {
await secretsStore.update(secret, { ...secret, data: this.data });
await this.secretsStore.update(secret, { ...secret, data: this.data });
Notifications.ok("Secret successfully updated.");
} catch (err) {
Notifications.error(err);

View File

@ -22,12 +22,8 @@
import { KubeObjectStore } from "../../kube-object.store";
import { autobind } from "../../utils";
import { Secret, secretsApi } from "../../api/endpoints";
import { apiManager } from "../../api/api-manager";
@autobind()
export class SecretsStore extends KubeObjectStore<Secret> {
api = secretsApi;
}
export const secretsStore = new SecretsStore();
apiManager.registerStore(secretsStore);

View File

@ -24,13 +24,14 @@ import "./secrets.scss";
import React from "react";
import { observer } from "mobx-react";
import type { RouteComponentProps } from "react-router";
import type { Secret } from "../../api/endpoints";
import { Secret, secretsApi } from "../../api/endpoints";
import { AddSecretDialog } from "./add-secret-dialog";
import { KubeObjectListLayout } from "../kube-object";
import { Badge } from "../badge";
import { secretsStore } from "./secrets.store";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { SecretsRouteParams } from "../../../common/routes";
import type { SecretsStore } from "./secrets.store";
import { ApiManager } from "../../api/api-manager";
enum columnId {
name = "name",
@ -46,13 +47,18 @@ interface Props extends RouteComponentProps<SecretsRouteParams> {
@observer
export class Secrets extends React.Component<Props> {
private get secretsStore() {
return ApiManager.getInstance().getStore<SecretsStore>(secretsApi);
}
render() {
return (
<>
<KubeObjectListLayout
isConfigurable
tableId="configuration_secrets"
className="Secrets" store={secretsStore}
className="Secrets"
store={this.secretsStore}
sortingCallbacks={{
[columnId.name]: (item: Secret) => item.getName(),
[columnId.namespace]: (item: Secret) => item.getNs(),

View File

@ -28,17 +28,17 @@ import { namespaceUrlParam } from "../+namespaces/namespace.store";
import { ResourceQuotas } from "../+config-resource-quotas";
import { PodDisruptionBudgets } from "../+config-pod-disruption-budgets";
import { HorizontalPodAutoscalers } from "../+config-autoscalers";
import { isAllowedResource } from "../../../common/rbac";
import { LimitRanges } from "../+config-limit-ranges";
import * as routes from "../../../common/routes";
import type { Cluster } from "../../../main/cluster";
@observer
export class Config extends React.Component {
static get tabRoutes(): TabLayoutRoute[] {
export class Config extends React.Component<{ cluster: Cluster }> {
static tabRoutes(cluster: Cluster): TabLayoutRoute[] {
const query = namespaceUrlParam.toObjectParam();
const tabs: TabLayoutRoute[] = [];
if (isAllowedResource("configmaps")) {
if (cluster.isAllowedResource("configmaps")) {
tabs.push({
title: "ConfigMaps",
component: ConfigMaps,
@ -47,7 +47,7 @@ export class Config extends React.Component {
});
}
if (isAllowedResource("secrets")) {
if (cluster.isAllowedResource("secrets")) {
tabs.push({
title: "Secrets",
component: Secrets,
@ -56,7 +56,7 @@ export class Config extends React.Component {
});
}
if (isAllowedResource("resourcequotas")) {
if (cluster.isAllowedResource("resourcequotas")) {
tabs.push({
title: "Resource Quotas",
component: ResourceQuotas,
@ -65,7 +65,7 @@ export class Config extends React.Component {
});
}
if (isAllowedResource("limitranges")) {
if (cluster.isAllowedResource("limitranges")) {
tabs.push({
title: "Limit Ranges",
component: LimitRanges,
@ -74,7 +74,7 @@ export class Config extends React.Component {
});
}
if (isAllowedResource("horizontalpodautoscalers")) {
if (cluster.isAllowedResource("horizontalpodautoscalers")) {
tabs.push({
title: "HPA",
component: HorizontalPodAutoscalers,
@ -83,7 +83,7 @@ export class Config extends React.Component {
});
}
if (isAllowedResource("poddisruptionbudgets")) {
if (cluster.isAllowedResource("poddisruptionbudgets")) {
tabs.push({
title: "Pod Disruption Budgets",
component: PodDisruptionBudgets,
@ -97,7 +97,7 @@ export class Config extends React.Component {
render() {
return (
<TabLayout className="Config" tabs={Config.tabRoutes}/>
<TabLayout className="Config" tabs={Config.tabRoutes(this.props.cluster)}/>
);
}
}

View File

@ -27,11 +27,12 @@ import { observer } from "mobx-react";
import { Link } from "react-router-dom";
import { stopPropagation } from "../../utils";
import { KubeObjectListLayout } from "../kube-object";
import { crdStore } from "./crd.store";
import type { CustomResourceDefinition } from "../../api/endpoints/crd.api";
import type { CrdStore } from "./crd.store";
import { crdApi, CustomResourceDefinition } from "../../api/endpoints/crd.api";
import { Select, SelectOption } from "../select";
import { createPageParam } from "../../navigation";
import { Icon } from "../icon";
import { ApiManager } from "../../api/api-manager";
export const crdGroupsUrlParam = createPageParam<string[]>({
name: "groups",
@ -54,12 +55,16 @@ export class CrdList extends React.Component {
return crdGroupsUrlParam.get();
}
private get crdStore() {
return ApiManager.getInstance().getStore<CrdStore>(crdApi);
}
@computed get items() {
if (this.selectedGroups.length) {
return crdStore.items.filter(item => this.selectedGroups.includes(item.getGroup()));
return this.crdStore.items.filter(item => this.selectedGroups.includes(item.getGroup()));
}
return crdStore.items; // show all by default
return this.crdStore.items; // show all by default
}
toggleSelection(group: string) {
@ -88,7 +93,7 @@ export class CrdList extends React.Component {
tableId="crd"
className="CrdList"
isClusterScoped={true}
store={crdStore}
store={this.crdStore}
items={items}
sortingCallbacks={sortingCallbacks}
searchFilters={Object.values(sortingCallbacks)}
@ -105,7 +110,7 @@ export class CrdList extends React.Component {
<Select
className="group-select"
placeholder={placeholder}
options={Object.keys(crdStore.groups)}
options={Object.keys(this.crdStore.groups)}
onChange={({ value: group }: SelectOption) => this.toggleSelection(group)}
closeMenuOnSelect={false}
controlShouldRenderValue={false}

View File

@ -29,11 +29,12 @@ import { cssNames } from "../../utils";
import { Badge } from "../badge";
import { DrawerItem } from "../drawer";
import type { KubeObjectDetailsProps } from "../kube-object";
import { crdStore } from "./crd.store";
import type { CrdStore } from "./crd.store";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { Input } from "../input";
import type { AdditionalPrinterColumnsV1, CustomResourceDefinition } from "../../api/endpoints/crd.api";
import { AdditionalPrinterColumnsV1, crdApi, CustomResourceDefinition } from "../../api/endpoints/crd.api";
import { parseJsonPath } from "../../utils/jsonPath";
import { ApiManager } from "../../api/api-manager";
interface Props extends KubeObjectDetailsProps<CustomResourceDefinition> {
}
@ -61,7 +62,7 @@ function convertSpecValue(value: any): any {
@observer
export class CrdResourceDetails extends React.Component<Props> {
@computed get crd() {
return crdStore.getByObject(this.props.object);
return ApiManager.getInstance().getStore<CrdStore>(crdApi).getByObject(this.props.object);
}
renderAdditionalColumns(crd: CustomResourceDefinition, columns: AdditionalPrinterColumnsV1[]) {

View File

@ -28,11 +28,12 @@ import type { RouteComponentProps } from "react-router";
import { KubeObjectListLayout } from "../kube-object";
import type { KubeObject } from "../../api/kube-object";
import { autorun, computed } from "mobx";
import { crdStore } from "./crd.store";
import type { TableSortCallback } from "../table";
import { apiManager } from "../../api/api-manager";
import { ApiManager } from "../../api/api-manager";
import { parseJsonPath } from "../../utils/jsonPath";
import type { CRDRouteParams } from "../../../common/routes";
import { crdApi } from "../../api/endpoints";
import type { CrdStore } from "./crd.store";
interface Props extends RouteComponentProps<CRDRouteParams> {
}
@ -57,16 +58,20 @@ export class CrdResources extends React.Component<Props> {
]);
}
private get crdStore() {
return ApiManager.getInstance().getStore<CrdStore>(crdApi);
}
@computed get crd() {
const { group, name } = this.props.match.params;
return crdStore.getByGroup(group, name);
return this.crdStore.getByGroup(group, name);
}
@computed get store() {
if (!this.crd) return null;
return apiManager.getStore(this.crd.getResourceApiBase());
return ApiManager.getInstance().getStore(this.crd.getResourceApiBase());
}
render() {

View File

@ -23,28 +23,29 @@ import { computed, reaction } from "mobx";
import { KubeObjectStore } from "../../kube-object.store";
import { autobind } from "../../utils";
import { crdApi, CustomResourceDefinition } from "../../api/endpoints/crd.api";
import { apiManager } from "../../api/api-manager";
import { ApiManager } from "../../api/api-manager";
import { KubeApi } from "../../api/kube-api";
import { CRDResourceStore } from "./crd-resource.store";
import type { KubeObject } from "../../api/kube-object";
import { KubeObject } from "../../api/kube-object";
import type { Cluster } from "../../../main/cluster";
function initStore(crd: CustomResourceDefinition) {
const manager = ApiManager.getInstance();
const apiBase = crd.getResourceApiBase();
const kind = crd.getResourceKind();
const isNamespaced = crd.isNamespaced();
const api = apiManager.getApi(apiBase) || new KubeApi({ apiBase, kind, isNamespaced });
const api = manager.getApi(apiBase) || new KubeApi({ objectConstructor: KubeObject, apiBase, kind, isNamespaced });
if (!apiManager.getStore(api)) {
apiManager.registerStore(new CRDResourceStore(api));
if (!manager.getStore(api)) {
manager.registerStore(class CRDResourceStore extends KubeObjectStore<KubeObject> { api = api; });
}
}
@autobind()
export class CRDStore extends KubeObjectStore<CustomResourceDefinition> {
export class CrdStore extends KubeObjectStore<CustomResourceDefinition> {
api = crdApi;
constructor() {
super();
constructor(cluster: Cluster) {
super(cluster);
// auto-init stores for crd-s
reaction(() => this.items.toJS(), items => items.forEach(initStore));
@ -87,7 +88,3 @@ export class CRDStore extends KubeObjectStore<CustomResourceDefinition> {
));
}
}
export const crdStore = new CRDStore();
apiManager.registerStore(crdStore);

View File

@ -26,10 +26,11 @@ import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
import { CrdList } from "./crd-list";
import { CrdResources } from "./crd-resources";
import { crdURL, crdDefinitionsRoute, crdResourcesRoute } from "../../../common/routes";
import type { Cluster } from "../../../main/cluster";
@observer
export class CustomResources extends React.Component {
static get tabRoutes(): TabLayoutRoute[] {
export class CustomResources extends React.Component<{ cluster: Cluster }> {
static tabRoutes(): TabLayoutRoute[] {
return [
{
title: "Definitions",

View File

@ -19,6 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export * from "./crd.store";
export * from "./crd-list";
export * from "./crd-details";
export * from "./crd-resources";

View File

@ -30,8 +30,8 @@ import { KubeObjectDetailsProps, getDetailsUrl } from "../kube-object";
import type { KubeEvent } from "../../api/endpoints/events.api";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { Table, TableCell, TableHead, TableRow } from "../table";
import { lookupApiLink } from "../../api/kube-api";
import { LocaleDate } from "../locale-date";
import { ApiManager } from "../../api/api-manager";
interface Props extends KubeObjectDetailsProps<KubeEvent> {
}
@ -81,7 +81,7 @@ export class EventDetails extends React.Component<Props> {
</TableHead>
<TableRow>
<TableCell>
<Link to={getDetailsUrl(lookupApiLink(involvedObject, event))}>
<Link to={getDetailsUrl(ApiManager.getInstance().lookupApiLink(involvedObject, event))}>
{name}
</Link>
</TableCell>

View File

@ -25,12 +25,16 @@ import { KubeObjectStore } from "../../kube-object.store";
import { autobind } from "../../utils";
import { eventApi, KubeEvent } from "../../api/endpoints/events.api";
import type { KubeObject } from "../../api/kube-object";
import { Pod } from "../../api/endpoints/pods.api";
import { podsStore } from "../+workloads-pods/pods.store";
import { apiManager } from "../../api/api-manager";
import { Pod, podsApi } from "../../api/endpoints/pods.api";
import type { PodsStore } from "../+workloads-pods";
import { ApiManager } from "../../api/api-manager";
@autobind()
export class EventStore extends KubeObjectStore<KubeEvent> {
private get podsStore() {
return ApiManager.getInstance().getStore<PodsStore>(podsApi);
}
api = eventApi;
limit = 1000;
saveLimit = 50000;
@ -63,7 +67,7 @@ export class EventStore extends KubeObjectStore<KubeEvent> {
const { kind, uid } = recent.involvedObject;
if (kind == Pod.kind) { // Wipe out running pods
const pod = podsStore.items.find(pod => pod.getId() == uid);
const pod = this.podsStore.items.find(pod => pod.getId() == uid);
if (!pod || (!pod.hasIssues() && pod.spec.priority < 500000)) return undefined;
}
@ -78,6 +82,3 @@ export class EventStore extends KubeObjectStore<KubeEvent> {
return this.getWarnings().length;
}
}
export const eventStore = new EventStore();
apiManager.registerStore(eventStore);

View File

@ -26,17 +26,17 @@ import { computed, observable } from "mobx";
import { observer } from "mobx-react";
import { orderBy } from "lodash";
import { TabLayout } from "../layout/tab-layout";
import { EventStore, eventStore } from "./event.store";
import type { EventStore } from "./event.store";
import { getDetailsUrl, KubeObjectListLayout, KubeObjectListLayoutProps } from "../kube-object";
import type { KubeEvent } from "../../api/endpoints/events.api";
import { eventApi, KubeEvent } from "../../api/endpoints/events.api";
import type { TableSortCallbacks, TableSortParams, TableProps } from "../table";
import type { IHeaderPlaceholders } from "../item-object-list";
import { Tooltip } from "../tooltip";
import { Link } from "react-router-dom";
import { cssNames, IClassName, stopPropagation } from "../../utils";
import { Icon } from "../icon";
import { lookupApiLink } from "../../api/kube-api";
import { eventsURL } from "../../../common/routes";
import { ApiManager } from "../../api/api-manager";
enum columnId {
message = "message",
@ -48,7 +48,7 @@ enum columnId {
age = "age",
}
interface Props extends Partial<KubeObjectListLayoutProps> {
interface Props extends Partial<KubeObjectListLayoutProps<KubeEvent>> {
className?: IClassName;
compact?: boolean;
compactLimit?: number;
@ -60,6 +60,10 @@ const defaultProps: Partial<Props> = {
@observer
export class Events extends React.Component<Props> {
private get eventStore() {
return ApiManager.getInstance().getStore<EventStore>(eventApi);
}
static defaultProps = defaultProps as object;
@observable sorting: TableSortParams = {
@ -81,12 +85,8 @@ export class Events extends React.Component<Props> {
onSort: params => this.sorting = params,
};
get store(): EventStore {
return eventStore;
}
@computed get items(): KubeEvent[] {
const items = this.store.contextItems;
const items = this.eventStore.contextItems;
const { sortBy, orderBy: order } = this.sorting;
// we must sort items before passing to "KubeObjectListLayout -> Table"
@ -106,7 +106,7 @@ export class Events extends React.Component<Props> {
customizeHeader = ({ info, title }: IHeaderPlaceholders) => {
const { compact } = this.props;
const { store, items, visibleItems } = this;
const { eventStore, items, visibleItems } = this;
const allEventsAreShown = visibleItems.length === items.length;
// handle "compact"-mode header
@ -126,14 +126,14 @@ export class Events extends React.Component<Props> {
small
material="help_outline"
className="help-icon"
tooltip={`Limited to ${store.limit}`}
tooltip={`Limited to ${eventStore.limit}`}
/>
</>
};
};
render() {
const { store, visibleItems } = this;
const { eventStore, visibleItems } = this;
const { compact, compactLimit, className, ...layoutProps } = this.props;
const events = (
@ -141,7 +141,7 @@ export class Events extends React.Component<Props> {
{...layoutProps}
isConfigurable
tableId="events"
store={store}
store={eventStore}
className={cssNames("Events", className, { compact })}
renderHeaderTitle="Events"
customizeHeader={this.customizeHeader}
@ -184,7 +184,7 @@ export class Events extends React.Component<Props> {
)
},
event.getNs(),
<Link key="link" to={getDetailsUrl(lookupApiLink(involvedObject, event))} onClick={stopPropagation}>
<Link key="link" to={getDetailsUrl(ApiManager.getInstance().lookupApiLink(involvedObject, event))} onClick={stopPropagation}>
{involvedObject.kind}: {involvedObject.name}
</Link>,
event.getSource(),

View File

@ -20,4 +20,5 @@
*/
export * from "./events";
export * from "./event.store";
export * from "./event-details";

View File

@ -26,7 +26,9 @@ import { observer } from "mobx-react";
import type { KubeObject } from "../../api/kube-object";
import { DrawerItem, DrawerTitle } from "../drawer";
import { cssNames } from "../../utils";
import { eventStore } from "./event.store";
import type { EventStore } from "./event.store";
import { ApiManager } from "../../api/api-manager";
import { eventApi } from "../../api/endpoints";
export interface KubeEventDetailsProps {
object: KubeObject;
@ -34,13 +36,17 @@ export interface KubeEventDetailsProps {
@observer
export class KubeEventDetails extends React.Component<KubeEventDetailsProps> {
private get eventStore() {
return ApiManager.getInstance().getStore<EventStore>(eventApi);
}
async componentDidMount() {
eventStore.reloadAll();
this.eventStore.reloadAll();
}
render() {
const { object } = this.props;
const events = eventStore.getEventsByObject(object);
const events = this.eventStore.getEventsByObject(object);
if (!events.length) {
return (

View File

@ -24,9 +24,10 @@ import "./kube-event-icon.scss";
import React from "react";
import { Icon } from "../icon";
import type { KubeObject } from "../../api/kube-object";
import { eventStore } from "./event.store";
import type { EventStore } from "./event.store";
import { cssNames } from "../../utils";
import type { KubeEvent } from "../../api/endpoints/events.api";
import { eventApi, KubeEvent } from "../../api/endpoints/events.api";
import { ApiManager } from "../../api/api-manager";
interface Props {
object: KubeObject;
@ -39,11 +40,15 @@ const defaultProps: Partial<Props> = {
};
export class KubeEventIcon extends React.Component<Props> {
private get eventStore() {
return ApiManager.getInstance().getStore<EventStore>(eventApi);
}
static defaultProps = defaultProps as object;
render() {
const { object, showWarningsOnly, filterEvents } = this.props;
const events = eventStore.getEventsByObject(object);
const events = this.eventStore.getEventsByObject(object);
let warnings = events.filter(evt => evt.isWarning());
if (filterEvents) warnings = filterEvents(warnings);

View File

@ -26,11 +26,12 @@ import { observable } from "mobx";
import { observer } from "mobx-react";
import { Dialog, DialogProps } from "../dialog";
import { Wizard, WizardStep } from "../wizard";
import { namespaceStore } from "./namespace.store";
import type { Namespace } from "../../api/endpoints";
import { Namespace, namespacesApi } from "../../api/endpoints";
import { Input } from "../input";
import { systemName } from "../input/input_validators";
import { Notifications } from "../notifications";
import type { NamespaceStore } from "./namespace.store";
import { ApiManager } from "../../api/api-manager";
interface Props extends DialogProps {
onSuccess?(ns: Namespace): void;
@ -39,6 +40,10 @@ interface Props extends DialogProps {
@observer
export class AddNamespaceDialog extends React.Component<Props> {
private get namespaceStore() {
return ApiManager.getInstance().getStore<NamespaceStore>(namespacesApi);
}
@observable static isOpen = false;
@observable namespace = "";
@ -59,7 +64,7 @@ export class AddNamespaceDialog extends React.Component<Props> {
const { onSuccess, onError } = this.props;
try {
const created = await namespaceStore.create({ name: namespace });
const created = await this.namespaceStore.create({ name: namespace });
onSuccess?.(created);
AddNamespaceDialog.close();

View File

@ -20,5 +20,6 @@
*/
export * from "./namespaces";
export * from "./namespace.store";
export * from "./namespace-details";
export * from "./add-namespace-dialog";

Some files were not shown because too many files have changed in this diff Show More