mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Turn on strict mode in tsconfig.json, some helpful lints, and required cleanup where strictness necessitates it (#5195)
* Turn on strict mode in tsconfig.json - Add route, clusterRoute, and payloadValidatedClusterRoute helper functions to improve types with backend routes - Turn on the following new lints: - react/jsx-first-prop-new-line - react/jsx-wrap-multilines - react/jsx-one-expression-per-line - react/jsx-max-props-per-line - react/jsx-indent - react/jsx-indent-props Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix build Signed-off-by: Sebastian Malton <sebastian@malton.name> * Replace KubeObject scope strings with enum Signed-off-by: Sebastian Malton <sebastian@malton.name> * Revert package.json version changes Signed-off-by: Sebastian Malton <sebastian@malton.name> * revert move hostedCluster(Id) Signed-off-by: Sebastian Malton <sebastian@malton.name> * change some type param names to be not single letters Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove copy-extension-themes Signed-off-by: Sebastian Malton <sebastian@malton.name> * add new make clean action Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix build to not use webpack for generating types Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix kube-object-menu.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix select.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix catalog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * revert move fileNameMigration to index Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix ref logic error Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix log-resource-selector.test.tsx tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix dock-store.test.ts test by overriding createStorage to not touch file system Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix cluster.test.ts tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix kube=api.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fixed hotbar-store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix kubeconfig-manager.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix cluster-role-bindings/__tests__/dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix role-bindings/__tests__/dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix pods.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix delete-cluster-dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix daemonset.store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix replicaset.store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix statefulsets/dialog/dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix replicasets/scale-dialog/dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix deployments.store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix deployments/scale/dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix cronjob.store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix stateful-set.api.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix deployment.api.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix api-manager.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix statefulset.store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix job.store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix pods.store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix scroll-spy.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix hotbar-remove-command.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix catalog-entity-registry.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix welcome.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix verify-that-all-routes-have-route-component.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix pod-tolerations.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * better fix for previous 3 fixes, plus also select.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix kube-object-menu.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix app-paths.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix dock-tabs.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix isReactNode typing Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix sub-title.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix drawer.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix list-layout.tsx and header.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix error-boundary.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix upgrade-chart/store.ts and dock-tab.store.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix install-chart/store.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix edit-resource/store.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix create-resource/store.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix namespace-select.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix namespace-select-filter.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix crd-list.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix wrong types for extensions Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix circular dependency Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix circular dependency on catalogCategoryRegistry Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix api-kube Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix type errors, most <Select /> errors Signed-off-by: Sebastian Malton <sebastian@malton.name> * fixing more type errors Signed-off-by: Sebastian Malton <sebastian@malton.name> * some more fixing type errors Signed-off-by: Sebastian Malton <sebastian@malton.name> * convert all KubeApis to injectable with legacy global backups Signed-off-by: Sebastian Malton <sebastian@malton.name> * factor out into a common file all the exports Signed-off-by: Sebastian Malton <sebastian@malton.name> * convert all KubeObjectStores to injectable with legacy global backups Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix lint Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove unused legacy KubeApi globals Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix bad previous commit Signed-off-by: Sebastian Malton <sebastian@malton.name> * more crash fixing Signed-off-by: Sebastian Malton <sebastian@malton.name> * try and fix behavioural tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix sidebar-and-tab-navigation-for-core.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix sidebar-and-tab-navigation-for-extensions.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-using-application-menu.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix catalog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * Make ThemeStore non-singleton and fix navigation-to-terminal-preferences.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * extensions.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix catalog-entity-registry.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-using-application-menu.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix log-resource-selector.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix dock-tabs.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix delete-cluster-dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-to-kubernetes-preferences.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-to-editor-preferences.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-to-proxy-preferences.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-using-application-menu.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-to-application-preferences.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix dock-store.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix select.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix role-bindings/__tests__/dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix hotbar-remove-command.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix cluster-role-bindings/__tests__/dialog.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-to-extension-specific-preferences.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-to-telemetry-preferences.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix closing-preferences.test.tsx Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-to-editor-preferences.test.ts Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix navigation-to-proxy-preferences.test.ts - Fix other type errors too Signed-off-by: Sebastian Malton <sebastian@malton.name> * final tweaks Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add more tsconfig files, fix bug in <Catalog> - Make all of history, navigation injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix type errors Signed-off-by: Sebastian Malton <sebastian@malton.name> * Convert all of kube-details-params/ and navigate/ to injectable - This fixes a runtime error that was encountered during testing Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix runtime errors on renderer - remove all static uses of `createPageParam` (and then removed the legacy global) - Made LensRendererExtension and LensMainExtension just used dependencies and not the getLegacyDi - Fixed circular dep in extension-loader Signed-off-by: Sebastian Malton <sebastian@malton.name> * Move registerStore calls to after injectMany Signed-off-by: Sebastian Malton <sebastian@malton.name> * replace all the rest of the legacy uses of apiManager Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix stack overflow and cycles in DI Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix NamespaceSelectFilter not opening Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix WizardStep and AddNamespaceDialog Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix KubeApi's not being registered Signed-off-by: Sebastian Malton <sebastian@malton.name> * cleanup WindowManager Signed-off-by: Sebastian Malton <sebastian@malton.name> * Proper fix for Wizard, fix NamespaceStore.subscribe Signed-off-by: Sebastian Malton <sebastian@malton.name> * Rewrite withTooltip to be more type correct - Fixes mobx related "too many recursive actions" error - Change all the uses of withTooltips to be functional components Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add e2e test to cover kube api registration Signed-off-by: Sebastian Malton <sebastian@malton.name> * cleanup internal-commands Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove cast in <Animate> Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix command-palette e2e test Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix type error after rebase Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix test name Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix lint Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix code to help CodeQL scanner Signed-off-by: Sebastian Malton <sebastian@malton.name> * update intree extension lock files Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix build-extensions picking wrong @types/react Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix tests from rebase Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix type error Signed-off-by: Sebastian Malton <sebastian@malton.name> * Make KubeconfigSyncManager more injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix crash in test mode for Dialog Signed-off-by: Sebastian Malton <sebastian@malton.name> * make Select snapshots deterministic Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix new type error Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix kube-object.store.test.ts typing Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix merge build issues Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix snapshots after merge Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix lint after merge Signed-off-by: Sebastian Malton <sebastian@malton.name> * reexport BaseKubeJsonApiObjectMetadata Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix typo in terminalSpawningPool Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove duplicate license header Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix typo to waitUntilDefined Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove iter use from getLegacyGlobalDiForExtensionApi Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove complex createStorage override Signed-off-by: Sebastian Malton <sebastian@malton.name> * override logger with mocks only when needed for tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove specialized overrideStore flags for getDiForUnitTesting Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove unnecessary | undefined types from the exactOptionalFieldTypes experiment Signed-off-by: Sebastian Malton <sebastian@malton.name> * use more descriptive name for local test mocks Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove unnecessary addition to 'make clean' target Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove oddity of KubeObjectStore.getById(undefined) being allowed Signed-off-by: Sebastian Malton <sebastian@malton.name> * rename KubeObject.getDescriptor in favour of name without fundemental JS meaning Signed-off-by: Sebastian Malton <sebastian@malton.name> * Simplify legacyRegisterApi when working in behaviour unit tests - Don't emit within main environment as there should be no auto registering there Signed-off-by: Sebastian Malton <sebastian@malton.name> * change confusing variable name in ReactiveDuration Signed-off-by: Sebastian Malton <sebastian@malton.name> * make visitor pattern more explicit for Entity contextMenuOpen Signed-off-by: Sebastian Malton <sebastian@malton.name> * toggleDetails -> toggleKubeDetailsPane is more specific Signed-off-by: Sebastian Malton <sebastian@malton.name> * remove outdated comment Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix bug where LensExtension dependencies are not set Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix tests from the revert of react 18 Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix more tests from merge Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix typings with new is-compatible-extension tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * more type fixing Signed-off-by: Sebastian Malton <sebastian@malton.name> * Revert in-tree extension versions Signed-off-by: Sebastian Malton <sebastian@malton.name> * Improve name of guarding injectable for stores and apis - New name better implies that it is just a guard state and does not do anything Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add helper for <Select>.isMulti for storing in a Set<Value> Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix is-compatible-extension.test.ts types Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
1 parent
381d77c633
commit
dfcb7c3330
1062 files changed
+26512
-28689
No files matched your search
@@ -30,9 +30,9 @@ interface TestStoreModel {
|
||||
}
|
||||
|
||||
class TestStore extends BaseStore<TestStoreModel> {
|
||||
@observable a: string;
|
||||
@observable b: string;
|
||||
@observable c: string;
|
||||
@observable a = "";
|
||||
@observable b = "";
|
||||
@observable c = "";
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
@@ -90,7 +90,6 @@ describe("BaseStore", () => {
|
||||
|
||||
await mainDi.runSetups();
|
||||
|
||||
store = undefined;
|
||||
TestStore.resetInstance();
|
||||
|
||||
const mockOpts = {
|
||||
|
||||
@@ -14,16 +14,13 @@ import { stdout, stderr } from "process";
|
||||
import getCustomKubeConfigDirectoryInjectable from "../app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable";
|
||||
import clusterStoreInjectable from "../cluster-store/cluster-store.injectable";
|
||||
import type { ClusterModel } from "../cluster-types";
|
||||
import type {
|
||||
DiContainer,
|
||||
} from "@ogre-tools/injectable";
|
||||
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token";
|
||||
|
||||
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
|
||||
import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable";
|
||||
import appVersionInjectable from "../get-configuration-file-model/app-version/app-version.injectable";
|
||||
import assert from "assert";
|
||||
|
||||
console = new Console(stdout, stderr);
|
||||
|
||||
@@ -148,6 +145,8 @@ describe("cluster-store", () => {
|
||||
it("adds new cluster to store", async () => {
|
||||
const storedCluster = clusterStore.getById("foo");
|
||||
|
||||
assert(storedCluster);
|
||||
|
||||
expect(storedCluster.id).toBe("foo");
|
||||
expect(storedCluster.preferences.terminalCWD).toBe("/some-directory-for-user-data");
|
||||
expect(storedCluster.preferences.icon).toBe(
|
||||
@@ -249,6 +248,8 @@ describe("cluster-store", () => {
|
||||
it("allows to retrieve a cluster", () => {
|
||||
const storedCluster = clusterStore.getById("cluster1");
|
||||
|
||||
assert(storedCluster);
|
||||
|
||||
expect(storedCluster.id).toBe("cluster1");
|
||||
expect(storedCluster.preferences.terminalCWD).toBe("/foo");
|
||||
});
|
||||
@@ -379,6 +380,7 @@ users:
|
||||
it("migrates to modern format with icon not in file", async () => {
|
||||
const { icon } = clusterStore.clustersList[0].preferences;
|
||||
|
||||
assert(icon);
|
||||
expect(icon.startsWith("data:;base64,")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import type { AppEvent } from "../app-event-bus/event-bus";
|
||||
import { appEventBus } from "../app-event-bus/event-bus";
|
||||
import { Console } from "console";
|
||||
import { assert, Console } from "console";
|
||||
import { stdout, stderr } from "process";
|
||||
|
||||
console = new Console(stdout, stderr);
|
||||
@@ -13,14 +13,15 @@ console = new Console(stdout, stderr);
|
||||
describe("event bus tests", () => {
|
||||
describe("emit", () => {
|
||||
it("emits an event", () => {
|
||||
let event: AppEvent = null;
|
||||
let event: AppEvent | undefined;
|
||||
|
||||
appEventBus.addListener((data) => {
|
||||
event = data;
|
||||
});
|
||||
|
||||
appEventBus.emit({ name: "foo", action: "bar" });
|
||||
expect(event.name).toBe("foo");
|
||||
assert(event);
|
||||
expect(event?.name).toBe("foo");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -21,7 +21,7 @@ describe("EventEmitter", () => {
|
||||
let called = false;
|
||||
const e = new EventEmitter<[]>();
|
||||
|
||||
e.addListener(() => 0 as any, {});
|
||||
e.addListener(() => 0 as never, {});
|
||||
e.addListener(() => { called = true; }, {});
|
||||
e.emit();
|
||||
|
||||
|
||||
@@ -5,69 +5,21 @@
|
||||
|
||||
import { anyObject } from "jest-mock-extended";
|
||||
import mockFs from "mock-fs";
|
||||
import logger from "../../main/logger";
|
||||
import type { CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "../catalog";
|
||||
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
|
||||
import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable";
|
||||
import appVersionInjectable from "../get-configuration-file-model/app-version/app-version.injectable";
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
import hotbarStoreInjectable from "../hotbar-store.injectable";
|
||||
import { HotbarStore } from "../hotbar-store";
|
||||
import hotbarStoreInjectable from "../hotbars/store.injectable";
|
||||
import type { HotbarStore } from "../hotbars/store";
|
||||
import catalogEntityRegistryInjectable from "../../main/catalog/entity-registry.injectable";
|
||||
import { computed } from "mobx";
|
||||
import hasCategoryForEntityInjectable from "../catalog/has-category-for-entity.injectable";
|
||||
import catalogCatalogEntityInjectable from "../catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable";
|
||||
import loggerInjectable from "../logger.injectable";
|
||||
import type { Logger } from "../logger";
|
||||
|
||||
jest.mock("../../main/catalog/catalog-entity-registry", () => ({
|
||||
catalogEntityRegistry: {
|
||||
items: [
|
||||
getMockCatalogEntity({
|
||||
apiVersion: "v1",
|
||||
kind: "Cluster",
|
||||
|
||||
status: {
|
||||
phase: "Running",
|
||||
},
|
||||
|
||||
metadata: {
|
||||
uid: "1dfa26e2ebab15780a3547e9c7fa785c",
|
||||
name: "mycluster",
|
||||
source: "local",
|
||||
labels: {},
|
||||
},
|
||||
}),
|
||||
|
||||
getMockCatalogEntity({
|
||||
apiVersion: "v1",
|
||||
kind: "Cluster",
|
||||
|
||||
status: {
|
||||
phase: "Running",
|
||||
},
|
||||
|
||||
metadata: {
|
||||
uid: "55b42c3c7ba3b04193416cda405269a5",
|
||||
name: "my_shiny_cluster",
|
||||
source: "remote",
|
||||
labels: {},
|
||||
},
|
||||
}),
|
||||
|
||||
getMockCatalogEntity({
|
||||
apiVersion: "v1",
|
||||
kind: "Cluster",
|
||||
|
||||
status: {
|
||||
phase: "Running",
|
||||
},
|
||||
|
||||
metadata: {
|
||||
uid: "catalog-entity",
|
||||
name: "Catalog",
|
||||
source: "app",
|
||||
labels: {},
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
}));
|
||||
console.log("I am here as reminder against mockfs (and to fix console logging)");
|
||||
|
||||
function getMockCatalogEntity(data: Partial<CatalogEntityData> & CatalogEntityKindData): CatalogEntity {
|
||||
return {
|
||||
@@ -84,69 +36,91 @@ function getMockCatalogEntity(data: Partial<CatalogEntityData> & CatalogEntityKi
|
||||
} as CatalogEntity;
|
||||
}
|
||||
|
||||
const testCluster = getMockCatalogEntity({
|
||||
apiVersion: "v1",
|
||||
kind: "Cluster",
|
||||
status: {
|
||||
phase: "Running",
|
||||
},
|
||||
metadata: {
|
||||
uid: "test",
|
||||
name: "test",
|
||||
labels: {},
|
||||
},
|
||||
});
|
||||
|
||||
const minikubeCluster = getMockCatalogEntity({
|
||||
apiVersion: "v1",
|
||||
kind: "Cluster",
|
||||
status: {
|
||||
phase: "Running",
|
||||
},
|
||||
metadata: {
|
||||
uid: "minikube",
|
||||
name: "minikube",
|
||||
labels: {},
|
||||
},
|
||||
});
|
||||
|
||||
const awsCluster = getMockCatalogEntity({
|
||||
apiVersion: "v1",
|
||||
kind: "Cluster",
|
||||
status: {
|
||||
phase: "Running",
|
||||
},
|
||||
metadata: {
|
||||
uid: "aws",
|
||||
name: "aws",
|
||||
labels: {},
|
||||
},
|
||||
});
|
||||
|
||||
describe("HotbarStore", () => {
|
||||
let di: DiContainer;
|
||||
let hotbarStore: HotbarStore;
|
||||
let testCluster: CatalogEntity;
|
||||
let minikubeCluster: CatalogEntity;
|
||||
let awsCluster: CatalogEntity;
|
||||
let loggerMock: jest.Mocked<Logger>;
|
||||
|
||||
beforeEach(async () => {
|
||||
di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
(di as any).unoverride(hotbarStoreInjectable);
|
||||
|
||||
testCluster = getMockCatalogEntity({
|
||||
apiVersion: "v1",
|
||||
kind: "Cluster",
|
||||
status: {
|
||||
phase: "Running",
|
||||
},
|
||||
metadata: {
|
||||
uid: "some-test-id",
|
||||
name: "my-test-cluster",
|
||||
source: "local",
|
||||
labels: {},
|
||||
},
|
||||
});
|
||||
minikubeCluster = getMockCatalogEntity({
|
||||
apiVersion: "v1",
|
||||
kind: "Cluster",
|
||||
status: {
|
||||
phase: "Running",
|
||||
},
|
||||
metadata: {
|
||||
uid: "some-minikube-id",
|
||||
name: "my-minikube-cluster",
|
||||
source: "local",
|
||||
labels: {},
|
||||
},
|
||||
});
|
||||
awsCluster = getMockCatalogEntity({
|
||||
apiVersion: "v1",
|
||||
kind: "Cluster",
|
||||
status: {
|
||||
phase: "Running",
|
||||
},
|
||||
metadata: {
|
||||
uid: "some-aws-id",
|
||||
name: "my-aws-cluster",
|
||||
source: "local",
|
||||
labels: {},
|
||||
},
|
||||
});
|
||||
|
||||
di.override(hasCategoryForEntityInjectable, () => () => true);
|
||||
|
||||
loggerMock = {
|
||||
warn: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
info: jest.fn(),
|
||||
silly: jest.fn(),
|
||||
};
|
||||
|
||||
di.override(loggerInjectable, () => loggerMock);
|
||||
|
||||
const catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable);
|
||||
const catalogCatalogEntity = di.inject(catalogCatalogEntityInjectable);
|
||||
|
||||
catalogEntityRegistry.addComputedSource("some-id", computed(() => [
|
||||
testCluster,
|
||||
minikubeCluster,
|
||||
awsCluster,
|
||||
catalogCatalogEntity,
|
||||
]));
|
||||
|
||||
di.permitSideEffects(getConfigurationFileModelInjectable);
|
||||
di.permitSideEffects(appVersionInjectable);
|
||||
|
||||
di.override(hotbarStoreInjectable, () => {
|
||||
HotbarStore.resetInstance();
|
||||
|
||||
return HotbarStore.createInstance({
|
||||
catalogCatalogEntity: di.inject(catalogCatalogEntityInjectable),
|
||||
});
|
||||
});
|
||||
di.permitSideEffects(hotbarStoreInjectable);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
describe("given no migrations", () => {
|
||||
describe("given no previous data in store, running all migrations", () => {
|
||||
beforeEach(async () => {
|
||||
mockFs();
|
||||
|
||||
@@ -174,7 +148,7 @@ describe("HotbarStore", () => {
|
||||
});
|
||||
|
||||
it("initially adds catalog entity as first item", () => {
|
||||
expect(hotbarStore.getActive().items[0].entity.name).toEqual("Catalog");
|
||||
expect(hotbarStore.getActive().items[0]?.entity.name).toEqual("Catalog");
|
||||
});
|
||||
|
||||
it("adds items", () => {
|
||||
@@ -186,7 +160,7 @@ describe("HotbarStore", () => {
|
||||
|
||||
it("removes items", () => {
|
||||
hotbarStore.addToHotbar(testCluster);
|
||||
hotbarStore.removeFromHotbar("test");
|
||||
hotbarStore.removeFromHotbar("some-test-id");
|
||||
hotbarStore.removeFromHotbar("catalog-entity");
|
||||
const items = hotbarStore.getActive().items.filter(Boolean);
|
||||
|
||||
@@ -211,7 +185,7 @@ describe("HotbarStore", () => {
|
||||
hotbarStore.restackItems(1, 5);
|
||||
|
||||
expect(hotbarStore.getActive().items[5]).toBeTruthy();
|
||||
expect(hotbarStore.getActive().items[5].entity.uid).toEqual("test");
|
||||
expect(hotbarStore.getActive().items[5]?.entity.uid).toEqual("some-test-id");
|
||||
});
|
||||
|
||||
it("moves items down", () => {
|
||||
@@ -224,7 +198,7 @@ describe("HotbarStore", () => {
|
||||
|
||||
const items = hotbarStore.getActive().items.map(item => item?.entity.uid || null);
|
||||
|
||||
expect(items.slice(0, 4)).toEqual(["aws", "catalog-entity", "test", "minikube"]);
|
||||
expect(items.slice(0, 4)).toEqual(["some-aws-id", "catalog-entity", "some-test-id", "some-minikube-id"]);
|
||||
});
|
||||
|
||||
it("moves items up", () => {
|
||||
@@ -237,28 +211,21 @@ describe("HotbarStore", () => {
|
||||
|
||||
const items = hotbarStore.getActive().items.map(item => item?.entity.uid || null);
|
||||
|
||||
expect(items.slice(0, 4)).toEqual(["catalog-entity", "minikube", "aws", "test"]);
|
||||
expect(items.slice(0, 4)).toEqual(["catalog-entity", "some-minikube-id", "some-aws-id", "some-test-id"]);
|
||||
});
|
||||
|
||||
it("logs an error if cellIndex is out of bounds", () => {
|
||||
hotbarStore.add({ name: "hottest", id: "hottest" });
|
||||
hotbarStore.setActiveHotbar("hottest");
|
||||
|
||||
const { error } = logger;
|
||||
const mocked = jest.fn();
|
||||
|
||||
logger.error = mocked;
|
||||
|
||||
hotbarStore.addToHotbar(testCluster, -1);
|
||||
expect(mocked).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject());
|
||||
expect(loggerMock.error).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject());
|
||||
|
||||
hotbarStore.addToHotbar(testCluster, 12);
|
||||
expect(mocked).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject());
|
||||
expect(loggerMock.error).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject());
|
||||
|
||||
hotbarStore.addToHotbar(testCluster, 13);
|
||||
expect(mocked).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject());
|
||||
|
||||
logger.error = error;
|
||||
expect(loggerMock.error).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject());
|
||||
});
|
||||
|
||||
it("throws an error if getId is invalid or returns not a string", () => {
|
||||
@@ -275,7 +242,7 @@ describe("HotbarStore", () => {
|
||||
hotbarStore.addToHotbar(testCluster);
|
||||
hotbarStore.restackItems(1, 1);
|
||||
|
||||
expect(hotbarStore.getActive().items[1].entity.uid).toEqual("test");
|
||||
expect(hotbarStore.getActive().items[1]?.entity.uid).toEqual("some-test-id");
|
||||
});
|
||||
|
||||
it("new items takes first empty cell", () => {
|
||||
@@ -284,7 +251,7 @@ describe("HotbarStore", () => {
|
||||
hotbarStore.restackItems(0, 3);
|
||||
hotbarStore.addToHotbar(minikubeCluster);
|
||||
|
||||
expect(hotbarStore.getActive().items[0].entity.uid).toEqual("minikube");
|
||||
expect(hotbarStore.getActive().items[0]?.entity.uid).toEqual("some-minikube-id");
|
||||
});
|
||||
|
||||
it("throws if invalid arguments provided", () => {
|
||||
@@ -315,7 +282,7 @@ describe("HotbarStore", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("given pre beta-5 configurations", () => {
|
||||
describe("given data from 5.0.0-beta.3 and version being 5.0.0-beta.10", () => {
|
||||
beforeEach(async () => {
|
||||
const configurationToBeMigrated = {
|
||||
"some-electron-app-path-for-user-data": {
|
||||
@@ -332,7 +299,7 @@ describe("HotbarStore", () => {
|
||||
items: [
|
||||
{
|
||||
entity: {
|
||||
uid: "1dfa26e2ebab15780a3547e9c7fa785c",
|
||||
uid: "some-aws-id",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -381,15 +348,17 @@ describe("HotbarStore", () => {
|
||||
|
||||
mockFs(configurationToBeMigrated);
|
||||
|
||||
di.override(appVersionInjectable, () => "5.0.0-beta.10");
|
||||
|
||||
await di.runSetups();
|
||||
|
||||
hotbarStore = di.inject(hotbarStoreInjectable);
|
||||
});
|
||||
|
||||
it("allows to retrieve a hotbar", () => {
|
||||
const hotbar = hotbarStore.getById("3caac17f-aec2-4723-9694-ad204465d935");
|
||||
const hotbar = hotbarStore.findById("3caac17f-aec2-4723-9694-ad204465d935");
|
||||
|
||||
expect(hotbar.id).toBe("3caac17f-aec2-4723-9694-ad204465d935");
|
||||
expect(hotbar?.id).toBe("3caac17f-aec2-4723-9694-ad204465d935");
|
||||
});
|
||||
|
||||
it("clears cells without entity", () => {
|
||||
@@ -403,17 +372,9 @@ describe("HotbarStore", () => {
|
||||
|
||||
expect(items[0]).toEqual({
|
||||
entity: {
|
||||
name: "mycluster",
|
||||
name: "my-aws-cluster",
|
||||
source: "local",
|
||||
uid: "1dfa26e2ebab15780a3547e9c7fa785c",
|
||||
},
|
||||
});
|
||||
|
||||
expect(items[1]).toEqual({
|
||||
entity: {
|
||||
name: "my_shiny_cluster",
|
||||
source: "remote",
|
||||
uid: "55b42c3c7ba3b04193416cda405269a5",
|
||||
uid: "some-aws-id",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,13 +6,14 @@ import https from "https";
|
||||
import os from "os";
|
||||
import { getMacRootCA, getWinRootCA, injectCAs, DSTRootCAX3 } from "../system-ca";
|
||||
import { dependencies, devDependencies } from "../../../package.json";
|
||||
import assert from "assert";
|
||||
|
||||
const deps = { ...dependencies, ...devDependencies };
|
||||
|
||||
// Skip the test if mac-ca is not installed, or os is not darwin
|
||||
(deps["mac-ca"] && os.platform().includes("darwin") ? describe: describe.skip)("inject CA for Mac", () => {
|
||||
// for reset https.globalAgent.options.ca after testing
|
||||
let _ca: string | Buffer | (string | Buffer)[];
|
||||
let _ca: string | Buffer | (string | Buffer)[] | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
_ca = https.globalAgent.options.ca;
|
||||
@@ -44,6 +45,7 @@ const deps = { ...dependencies, ...devDependencies };
|
||||
injectCAs(osxCAs);
|
||||
const injected = https.globalAgent.options.ca;
|
||||
|
||||
assert(injected);
|
||||
expect(injected.includes(DSTRootCAX3)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@@ -51,7 +53,7 @@ const deps = { ...dependencies, ...devDependencies };
|
||||
// Skip the test if win-ca is not installed, or os is not win32
|
||||
(deps["win-ca"] && os.platform().includes("win32") ? describe: describe.skip)("inject CA for Windows", () => {
|
||||
// for reset https.globalAgent.options.ca after testing
|
||||
let _ca: string | Buffer | (string | Buffer)[];
|
||||
let _ca: string | Buffer | (string | Buffer)[] | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
_ca = https.globalAgent.options.ca;
|
||||
|
||||
@@ -30,7 +30,7 @@ import userStoreInjectable from "../user-store/user-store.injectable";
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import type { ClusterStoreModel } from "../cluster-store/cluster-store";
|
||||
import { defaultTheme } from "../vars";
|
||||
import { defaultThemeId } from "../vars";
|
||||
import writeFileInjectable from "../fs/write-file.injectable";
|
||||
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
|
||||
import getConfigurationFileModelInjectable
|
||||
@@ -49,7 +49,7 @@ describe("user store tests", () => {
|
||||
|
||||
mockFs();
|
||||
|
||||
di.override(writeFileInjectable, () => () => undefined);
|
||||
di.override(writeFileInjectable, () => () => Promise.resolve());
|
||||
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
||||
di.override(userStoreInjectable, () => UserStore.createInstance());
|
||||
|
||||
@@ -80,7 +80,7 @@ describe("user store tests", () => {
|
||||
userStore.httpsProxy = "abcd://defg";
|
||||
|
||||
expect(userStore.httpsProxy).toBe("abcd://defg");
|
||||
expect(userStore.colorTheme).toBe(defaultTheme);
|
||||
expect(userStore.colorTheme).toBe(defaultThemeId);
|
||||
|
||||
userStore.colorTheme = "light";
|
||||
expect(userStore.colorTheme).toBe("light");
|
||||
@@ -89,7 +89,7 @@ describe("user store tests", () => {
|
||||
it("correctly resets theme to default value", async () => {
|
||||
userStore.colorTheme = "some other theme";
|
||||
userStore.resetTheme();
|
||||
expect(userStore.colorTheme).toBe(defaultTheme);
|
||||
expect(userStore.colorTheme).toBe(defaultThemeId);
|
||||
});
|
||||
|
||||
it("correctly calculates if the last seen version is an old release", () => {
|
||||
|
||||
@@ -2,20 +2,30 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { kubernetesClusterCategory } from "../kubernetes-cluster";
|
||||
|
||||
import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting";
|
||||
import kubernetesClusterCategoryInjectable from "../../catalog/categories/kubernetes-cluster.injectable";
|
||||
import type { KubernetesClusterCategory } from "../kubernetes-cluster";
|
||||
|
||||
|
||||
describe("kubernetesClusterCategory", () => {
|
||||
let kubernetesClusterCategory: KubernetesClusterCategory;
|
||||
|
||||
beforeEach(() => {
|
||||
const di = getDiForUnitTesting();
|
||||
|
||||
kubernetesClusterCategory = di.inject(kubernetesClusterCategoryInjectable);
|
||||
});
|
||||
|
||||
describe("filteredItems", () => {
|
||||
const item1 = {
|
||||
icon: "Icon",
|
||||
title: "Title",
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
onClick: () => {},
|
||||
};
|
||||
const item2 = {
|
||||
icon: "Icon 2",
|
||||
title: "Title 2",
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
onClick: () => {},
|
||||
};
|
||||
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
|
||||
import { navigate } from "../../renderer/navigation";
|
||||
import type { CatalogEntityMetadata, CatalogEntitySpec, CatalogEntityStatus } from "../catalog";
|
||||
import { CatalogCategory, CatalogEntity } from "../catalog";
|
||||
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
|
||||
import { CatalogCategory, CatalogEntity, categoryVersion } from "../catalog/catalog-entity";
|
||||
|
||||
interface GeneralEntitySpec extends CatalogEntitySpec {
|
||||
path: string;
|
||||
@@ -23,18 +22,6 @@ export class GeneralEntity extends CatalogEntity<CatalogEntityMetadata, CatalogE
|
||||
async onRun() {
|
||||
navigate(this.spec.path);
|
||||
}
|
||||
|
||||
public onSettingsOpen(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
public onDetailsOpen(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
public onContextMenuOpen(): void {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export class GeneralCategory extends CatalogCategory {
|
||||
@@ -47,15 +34,10 @@ export class GeneralCategory extends CatalogCategory {
|
||||
public spec = {
|
||||
group: "entity.k8slens.dev",
|
||||
versions: [
|
||||
{
|
||||
name: "v1alpha1",
|
||||
entityClass: GeneralEntity,
|
||||
},
|
||||
categoryVersion("v1alpha1", GeneralEntity),
|
||||
],
|
||||
names: {
|
||||
kind: "General",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
catalogCategoryRegistry.add(new GeneralCategory());
|
||||
@@ -3,13 +3,12 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
|
||||
import type { CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus, CatalogCategorySpec } from "../catalog";
|
||||
import { CatalogEntity, CatalogCategory } from "../catalog";
|
||||
import { CatalogEntity, CatalogCategory, categoryVersion } from "../catalog/catalog-entity";
|
||||
import { ClusterStore } from "../cluster-store/cluster-store";
|
||||
import { broadcastMessage } from "../ipc";
|
||||
import { app } from "electron";
|
||||
import type { CatalogEntitySpec } from "../catalog/catalog-entity";
|
||||
import type { CatalogEntityConstructor, CatalogEntitySpec } from "../catalog/catalog-entity";
|
||||
import { IpcRendererNavigationEvents } from "../../renderer/navigation/events";
|
||||
import { requestClusterActivation, requestClusterDisconnection } from "../../renderer/ipc";
|
||||
import KubeClusterCategoryIcon from "./icons/kubernetes.svg";
|
||||
@@ -60,6 +59,10 @@ export type KubernetesClusterStatusPhase = "connected" | "connecting" | "disconn
|
||||
export interface KubernetesClusterStatus extends CatalogEntityStatus {
|
||||
}
|
||||
|
||||
export function isKubernetesCluster(item: unknown): item is KubernetesCluster {
|
||||
return item instanceof KubernetesCluster;
|
||||
}
|
||||
|
||||
export class KubernetesCluster<
|
||||
Metadata extends KubernetesClusterMetadata = KubernetesClusterMetadata,
|
||||
Status extends KubernetesClusterStatus = KubernetesClusterStatus,
|
||||
@@ -99,7 +102,7 @@ export class KubernetesCluster<
|
||||
//
|
||||
}
|
||||
|
||||
async onContextMenuOpen(context: CatalogEntityContextMenuContext) {
|
||||
onContextMenuOpen(context: CatalogEntityContextMenuContext) {
|
||||
if (!this.metadata.source || this.metadata.source === "local") {
|
||||
context.menuItems.push({
|
||||
title: "Settings",
|
||||
@@ -128,14 +131,10 @@ export class KubernetesCluster<
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
catalogCategoryRegistry
|
||||
.getCategoryForEntity<KubernetesClusterCategory>(this)
|
||||
?.emit("contextMenuOpen", this, context);
|
||||
}
|
||||
}
|
||||
|
||||
class KubernetesClusterCategory extends CatalogCategory {
|
||||
export class KubernetesClusterCategory extends CatalogCategory {
|
||||
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
|
||||
public readonly kind = "CatalogCategory";
|
||||
public metadata = {
|
||||
@@ -145,17 +144,10 @@ class KubernetesClusterCategory extends CatalogCategory {
|
||||
public spec: CatalogCategorySpec = {
|
||||
group: "entity.k8slens.dev",
|
||||
versions: [
|
||||
{
|
||||
name: "v1alpha1",
|
||||
entityClass: KubernetesCluster,
|
||||
},
|
||||
categoryVersion("v1alpha1", KubernetesCluster as CatalogEntityConstructor<KubernetesCluster>),
|
||||
],
|
||||
names: {
|
||||
kind: "KubernetesCluster",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const kubernetesClusterCategory = new KubernetesClusterCategory();
|
||||
|
||||
catalogCategoryRegistry.add(kubernetesClusterCategory);
|
||||
@@ -4,8 +4,7 @@
|
||||
*/
|
||||
|
||||
import type { CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
|
||||
import { CatalogCategory, CatalogEntity } from "../catalog";
|
||||
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
|
||||
import { CatalogCategory, CatalogEntity, categoryVersion } from "../catalog/catalog-entity";
|
||||
import { productName } from "../vars";
|
||||
import { WeblinkStore } from "../weblink-store";
|
||||
|
||||
@@ -30,11 +29,7 @@ export class WebLink extends CatalogEntity<CatalogEntityMetadata, WebLinkStatus,
|
||||
window.open(this.spec.url, "_blank");
|
||||
}
|
||||
|
||||
public onSettingsOpen(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
async onContextMenuOpen(context: CatalogEntityContextMenuContext) {
|
||||
onContextMenuOpen(context: CatalogEntityContextMenuContext) {
|
||||
if (this.metadata.source === "local") {
|
||||
context.menuItems.push({
|
||||
title: "Delete",
|
||||
@@ -45,10 +40,6 @@ export class WebLink extends CatalogEntity<CatalogEntityMetadata, WebLinkStatus,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
catalogCategoryRegistry
|
||||
.getCategoryForEntity<WebLinkCategory>(this)
|
||||
?.emit("contextMenuOpen", this, context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,15 +53,10 @@ export class WebLinkCategory extends CatalogCategory {
|
||||
public spec = {
|
||||
group: "entity.k8slens.dev",
|
||||
versions: [
|
||||
{
|
||||
name: "v1alpha1",
|
||||
entityClass: WebLink,
|
||||
},
|
||||
categoryVersion("v1alpha1", WebLink),
|
||||
],
|
||||
names: {
|
||||
kind: "WebLink",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
catalogCategoryRegistry.add(new WebLinkCategory());
|
||||
@@ -3,96 +3,10 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { action, computed, observable, makeObservable } from "mobx";
|
||||
import { once } from "lodash";
|
||||
import { iter, getOrInsertMap, strictSet } from "../utils";
|
||||
import type { Disposer } from "../utils";
|
||||
import type { CatalogCategory, CatalogEntityData, CatalogEntityKindData } from "./catalog-entity";
|
||||
import { asLegacyGlobalForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
||||
import catalogCategoryRegistryInjectable from "./category-registry.injectable";
|
||||
|
||||
export type CategoryFilter = (category: CatalogCategory) => any;
|
||||
|
||||
export class CatalogCategoryRegistry {
|
||||
protected categories = observable.set<CatalogCategory>();
|
||||
protected groupKinds = new Map<string, Map<string, CatalogCategory>>();
|
||||
protected filters = observable.set<CategoryFilter>([], {
|
||||
deep: false,
|
||||
});
|
||||
|
||||
constructor() {
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
@action add(category: CatalogCategory): Disposer {
|
||||
const byGroup = getOrInsertMap(this.groupKinds, category.spec.group);
|
||||
|
||||
this.categories.add(category);
|
||||
strictSet(byGroup, category.spec.names.kind, category);
|
||||
|
||||
return () => {
|
||||
this.categories.delete(category);
|
||||
byGroup.delete(category.spec.names.kind);
|
||||
};
|
||||
}
|
||||
|
||||
@computed get items() {
|
||||
return Array.from(this.categories);
|
||||
}
|
||||
|
||||
@computed get filteredItems() {
|
||||
return Array.from(
|
||||
iter.reduce(
|
||||
this.filters,
|
||||
iter.filter,
|
||||
this.items.values(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
getForGroupKind<T extends CatalogCategory>(group: string, kind: string): T | undefined {
|
||||
return this.groupKinds.get(group)?.get(kind) as T;
|
||||
}
|
||||
|
||||
getEntityForData(data: CatalogEntityData & CatalogEntityKindData) {
|
||||
const category = this.getCategoryForEntity(data);
|
||||
|
||||
if (!category) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const splitApiVersion = data.apiVersion.split("/");
|
||||
const version = splitApiVersion[1];
|
||||
|
||||
const specVersion = category.spec.versions.find((v) => v.name === version);
|
||||
|
||||
if (!specVersion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new specVersion.entityClass(data);
|
||||
}
|
||||
|
||||
getCategoryForEntity<T extends CatalogCategory>(data: CatalogEntityData & CatalogEntityKindData): T | undefined {
|
||||
const splitApiVersion = data.apiVersion.split("/");
|
||||
const group = splitApiVersion[0];
|
||||
|
||||
return this.getForGroupKind(group, data.kind);
|
||||
}
|
||||
|
||||
getByName(name: string) {
|
||||
return this.items.find(category => category.metadata?.name == name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new filter to the set of category filters
|
||||
* @param fn The function that should return a truthy value if that category should be displayed
|
||||
* @returns A function to remove that filter
|
||||
*/
|
||||
addCatalogCategoryFilter(fn: CategoryFilter): Disposer {
|
||||
this.filters.add(fn);
|
||||
|
||||
return once(() => void this.filters.delete(fn));
|
||||
}
|
||||
}
|
||||
|
||||
export const catalogCategoryRegistry = new CatalogCategoryRegistry();
|
||||
/**
|
||||
* @deprecated use `di.inject(catalogCategoryRegistryInjectable)` instead
|
||||
*/
|
||||
export const catalogCategoryRegistry = asLegacyGlobalForExtensionApi(catalogCategoryRegistryInjectable);
|
||||
@@ -11,19 +11,19 @@ import type { Disposer } from "../utils";
|
||||
import { iter } from "../utils";
|
||||
import type { CategoryColumnRegistration } from "../../renderer/components/+catalog/custom-category-columns";
|
||||
|
||||
type ExtractEntityMetadataType<Entity> = Entity extends CatalogEntity<infer Metadata> ? Metadata : never;
|
||||
type ExtractEntityStatusType<Entity> = Entity extends CatalogEntity<any, infer Status> ? Status : never;
|
||||
type ExtractEntitySpecType<Entity> = Entity extends CatalogEntity<any, any, infer Spec> ? Spec : never;
|
||||
export type CatalogEntityDataFor<Entity> = Entity extends CatalogEntity<infer Metadata, infer Status, infer Spec>
|
||||
? CatalogEntityData<Metadata, Status, Spec>
|
||||
: never;
|
||||
|
||||
export type CatalogEntityInstanceFrom<Constructor> = Constructor extends CatalogEntityConstructor<infer Entity>
|
||||
? Entity
|
||||
: never;
|
||||
|
||||
export type CatalogEntityConstructor<Entity extends CatalogEntity> = (
|
||||
(new (data: CatalogEntityData<
|
||||
ExtractEntityMetadataType<Entity>,
|
||||
ExtractEntityStatusType<Entity>,
|
||||
ExtractEntitySpecType<Entity>
|
||||
>) => Entity)
|
||||
new (data: CatalogEntityDataFor<Entity>) => Entity
|
||||
);
|
||||
|
||||
export interface CatalogCategoryVersion<Entity extends CatalogEntity> {
|
||||
export interface CatalogCategoryVersion {
|
||||
/**
|
||||
* The specific version that the associated constructor is for. This MUST be
|
||||
* a DNS label and SHOULD be of the form `vN`, `vNalphaY`, or `vNbetaY` where
|
||||
@@ -35,19 +35,19 @@ export interface CatalogCategoryVersion<Entity extends CatalogEntity> {
|
||||
* - `v1alpha2`
|
||||
* - `v3beta2`
|
||||
*/
|
||||
name: string;
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* The constructor for the entities.
|
||||
*/
|
||||
entityClass: CatalogEntityConstructor<Entity>;
|
||||
readonly entityClass: CatalogEntityConstructor<CatalogEntity>;
|
||||
}
|
||||
|
||||
export interface CatalogCategorySpec {
|
||||
/**
|
||||
* The grouping for for the category. This MUST be a DNS label.
|
||||
*/
|
||||
group: string;
|
||||
readonly group: string;
|
||||
|
||||
/**
|
||||
* The specific versions of the constructors.
|
||||
@@ -56,18 +56,18 @@ export interface CatalogCategorySpec {
|
||||
* For example, if `group = "entity.k8slens.dev"` and there is an entry in `.versions` with
|
||||
* `name = "v1alpha1"` then the resulting `.apiVersion` MUST be `entity.k8slens.dev/v1alpha1`
|
||||
*/
|
||||
versions: CatalogCategoryVersion<CatalogEntity>[];
|
||||
readonly versions: CatalogCategoryVersion[];
|
||||
|
||||
/**
|
||||
* This is the concerning the category
|
||||
*/
|
||||
names: {
|
||||
readonly names: {
|
||||
/**
|
||||
* The kind of entity that this category is for. This value MUST be a DNS
|
||||
* label and MUST be equal to the `kind` fields that are produced by the
|
||||
* `.versions.[] | .entityClass` fields.
|
||||
*/
|
||||
kind: string;
|
||||
readonly kind: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -81,7 +81,7 @@ export interface CatalogCategorySpec {
|
||||
*
|
||||
* These columns will not be used in the "Browse" view.
|
||||
*/
|
||||
displayColumns?: CategoryColumnRegistration[];
|
||||
readonly displayColumns?: CategoryColumnRegistration[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,6 +109,30 @@ export interface CatalogCategoryEvents {
|
||||
contextMenuOpen: (entity: CatalogEntity, context: CatalogEntityContextMenuContext) => void;
|
||||
}
|
||||
|
||||
export interface CatalogCategoryMetadata {
|
||||
/**
|
||||
* The name of your category. The category can be searched for by this
|
||||
* value. This will also be used for the catalog menu.
|
||||
*/
|
||||
readonly name: string;
|
||||
/**
|
||||
* Either an `<svg>` or the name of an icon from {@link IconProps}
|
||||
*/
|
||||
readonly icon: string;
|
||||
}
|
||||
|
||||
export function categoryVersion<
|
||||
T extends CatalogEntity<Metadata, Status, Spec>,
|
||||
Metadata extends CatalogEntityMetadata,
|
||||
Status extends CatalogEntityStatus,
|
||||
Spec extends CatalogEntitySpec,
|
||||
>(name: string, entityClass: new (data: CatalogEntityData<Metadata, Status, Spec>) => T): CatalogCategoryVersion {
|
||||
return {
|
||||
name,
|
||||
entityClass: entityClass as CatalogEntityConstructor<T>,
|
||||
};
|
||||
}
|
||||
|
||||
export abstract class CatalogCategory extends (EventEmitter as new () => TypedEmitter<CatalogCategoryEvents>) {
|
||||
/**
|
||||
* The version of category that you are wanting to declare.
|
||||
@@ -131,28 +155,17 @@ export abstract class CatalogCategory extends (EventEmitter as new () => TypedEm
|
||||
/**
|
||||
* The data about the category itself
|
||||
*/
|
||||
abstract readonly metadata: {
|
||||
/**
|
||||
* The name of your category. The category can be searched for by this
|
||||
* value. This will also be used for the catalog menu.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Either an `<svg>` or the name of an icon from {@link IconProps}
|
||||
*/
|
||||
icon: string;
|
||||
};
|
||||
abstract readonly metadata: CatalogCategoryMetadata;
|
||||
|
||||
/**
|
||||
* The most important part of a category, as it is where entity versions are declared.
|
||||
*/
|
||||
abstract spec: CatalogCategorySpec;
|
||||
abstract readonly spec: CatalogCategorySpec;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected filters = observable.set<AddMenuFilter>([], {
|
||||
protected readonly filters = observable.set<AddMenuFilter>([], {
|
||||
deep: false,
|
||||
});
|
||||
|
||||
@@ -217,14 +230,16 @@ export abstract class CatalogCategory extends (EventEmitter as new () => TypedEm
|
||||
}
|
||||
}
|
||||
|
||||
export interface CatalogEntityMetadata {
|
||||
export type EntityMetadataObject = { [Key in string]?: EntityMetadataValue };
|
||||
export type EntityMetadataValue = string | number | boolean | EntityMetadataObject | undefined;
|
||||
|
||||
export interface CatalogEntityMetadata extends EntityMetadataObject {
|
||||
uid: string;
|
||||
name: string;
|
||||
shortName?: string;
|
||||
description?: string;
|
||||
source?: string;
|
||||
labels: Record<string, string>;
|
||||
[key: string]: string | object;
|
||||
}
|
||||
|
||||
export interface CatalogEntityStatus {
|
||||
@@ -392,7 +407,7 @@ export abstract class CatalogEntity<
|
||||
return this.status.enabled ?? true;
|
||||
}
|
||||
|
||||
public abstract onRun?(context: CatalogEntityActionContext): void | Promise<void>;
|
||||
public abstract onContextMenuOpen(context: CatalogEntityContextMenuContext): void | Promise<void>;
|
||||
public abstract onSettingsOpen(context: CatalogEntitySettingsContext): void | Promise<void>;
|
||||
public onRun?(context: CatalogEntityActionContext): void | Promise<void>;
|
||||
public onContextMenuOpen?(context: CatalogEntityContextMenuContext): void | Promise<void>;
|
||||
public onSettingsOpen?(context: CatalogEntitySettingsContext): void | Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { GeneralCategory } from "../../catalog-entities";
|
||||
import { builtInCategoryInjectionToken } from "../category-registry.injectable";
|
||||
|
||||
const generalCategoryInjectable = getInjectable({
|
||||
id: "general-category",
|
||||
instantiate: () => new GeneralCategory(),
|
||||
injectionToken: builtInCategoryInjectionToken,
|
||||
});
|
||||
|
||||
export default generalCategoryInjectable;
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { KubernetesClusterCategory } from "../../catalog-entities/kubernetes-cluster";
|
||||
import { builtInCategoryInjectionToken } from "../category-registry.injectable";
|
||||
|
||||
const kubernetesClusterCategoryInjectable = getInjectable({
|
||||
id: "kubernetes-cluster-category",
|
||||
instantiate: () => new KubernetesClusterCategory(),
|
||||
injectionToken: builtInCategoryInjectionToken,
|
||||
});
|
||||
|
||||
export default kubernetesClusterCategoryInjectable;
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { WebLinkCategory } from "../../catalog-entities";
|
||||
import { builtInCategoryInjectionToken } from "../category-registry.injectable";
|
||||
|
||||
const weblinkCategoryInjectable = getInjectable({
|
||||
id: "weblink-category",
|
||||
instantiate: () => new WebLinkCategory(),
|
||||
injectionToken: builtInCategoryInjectionToken,
|
||||
});
|
||||
|
||||
export default weblinkCategoryInjectable;
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
|
||||
import type { CatalogCategory } from "./catalog-entity";
|
||||
import { CatalogCategoryRegistry } from "./category-registry";
|
||||
|
||||
export const builtInCategoryInjectionToken = getInjectionToken<CatalogCategory>({
|
||||
id: "built-in-category-token",
|
||||
});
|
||||
|
||||
const catalogCategoryRegistryInjectable = getInjectable({
|
||||
id: "catalog-category-registry",
|
||||
instantiate: (di) => {
|
||||
const registry = new CatalogCategoryRegistry();
|
||||
const categories = di.injectMany(builtInCategoryInjectionToken);
|
||||
|
||||
for (const category of categories) {
|
||||
registry.add(category);
|
||||
}
|
||||
|
||||
return registry;
|
||||
},
|
||||
});
|
||||
|
||||
export default catalogCategoryRegistryInjectable;
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { action, computed, observable, makeObservable } from "mobx";
|
||||
import { once } from "lodash";
|
||||
import { iter, getOrInsertMap, strictSet } from "../utils";
|
||||
import type { Disposer } from "../utils";
|
||||
import type { CatalogCategory, CatalogEntityData, CatalogEntityKindData } from "./catalog-entity";
|
||||
|
||||
export type CategoryFilter = (category: CatalogCategory) => any;
|
||||
|
||||
export class CatalogCategoryRegistry {
|
||||
protected readonly categories = observable.set<CatalogCategory>();
|
||||
protected readonly groupKinds = new Map<string, Map<string, CatalogCategory>>();
|
||||
protected readonly filters = observable.set<CategoryFilter>([], {
|
||||
deep: false,
|
||||
});
|
||||
|
||||
constructor() {
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
@action add(category: CatalogCategory): Disposer {
|
||||
const byGroup = getOrInsertMap(this.groupKinds, category.spec.group);
|
||||
|
||||
this.categories.add(category);
|
||||
strictSet(byGroup, category.spec.names.kind, category);
|
||||
|
||||
return () => {
|
||||
this.categories.delete(category);
|
||||
byGroup.delete(category.spec.names.kind);
|
||||
};
|
||||
}
|
||||
|
||||
@computed get items() {
|
||||
return Array.from(this.categories);
|
||||
}
|
||||
|
||||
@computed get filteredItems() {
|
||||
return Array.from(
|
||||
iter.reduce(
|
||||
this.filters,
|
||||
iter.filter,
|
||||
this.items.values(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
getForGroupKind<T extends CatalogCategory>(group: string, kind: string): T | undefined {
|
||||
return this.groupKinds.get(group)?.get(kind) as T;
|
||||
}
|
||||
|
||||
getEntityForData(data: CatalogEntityData & CatalogEntityKindData) {
|
||||
const category = this.getCategoryForEntity(data);
|
||||
|
||||
if (!category) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const splitApiVersion = data.apiVersion.split("/");
|
||||
const version = splitApiVersion[1];
|
||||
|
||||
const specVersion = category.spec.versions.find((v) => v.name === version);
|
||||
|
||||
if (!specVersion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new specVersion.entityClass(data);
|
||||
}
|
||||
|
||||
hasCategoryForEntity({ kind, apiVersion }: CatalogEntityData & CatalogEntityKindData): boolean {
|
||||
const splitApiVersion = apiVersion.split("/");
|
||||
const group = splitApiVersion[0];
|
||||
|
||||
return this.groupKinds.get(group)?.has(kind) ?? false;
|
||||
}
|
||||
|
||||
getCategoryForEntity<T extends CatalogCategory>(data: CatalogEntityData & CatalogEntityKindData): T | undefined {
|
||||
const splitApiVersion = data.apiVersion.split("/");
|
||||
const group = splitApiVersion[0];
|
||||
|
||||
return this.getForGroupKind(group, data.kind);
|
||||
}
|
||||
|
||||
getByName(name: string) {
|
||||
return this.items.find(category => category.metadata?.name == name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new filter to the set of category filters
|
||||
* @param fn The function that should return a truthy value if that category should be displayed
|
||||
* @returns A function to remove that filter
|
||||
*/
|
||||
addCatalogCategoryFilter(fn: CategoryFilter): Disposer {
|
||||
this.filters.add(fn);
|
||||
|
||||
return once(() => void this.filters.delete(fn));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { CatalogEntityData, CatalogEntityKindData } from "./catalog-entity";
|
||||
import catalogCategoryRegistryInjectable from "./category-registry.injectable";
|
||||
|
||||
export type HasCategoryForEntity = (data: CatalogEntityData & CatalogEntityKindData) => boolean;
|
||||
|
||||
const hasCategoryForEntityInjectable = getInjectable({
|
||||
id: "has-category-for-entity",
|
||||
instantiate: (di): HasCategoryForEntity => {
|
||||
const registry = di.inject(catalogCategoryRegistryInjectable);
|
||||
|
||||
return (data) => registry.hasCategoryForEntity(data);
|
||||
},
|
||||
});
|
||||
|
||||
export default hasCategoryForEntityInjectable;
|
||||
@@ -4,4 +4,5 @@
|
||||
*/
|
||||
|
||||
export * from "./catalog-category-registry";
|
||||
export * from "./category-registry";
|
||||
export * from "./catalog-entity";
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { CatalogEntity, CatalogEntityContextMenuContext } from "./catalog-entity";
|
||||
import catalogCategoryRegistryInjectable from "./category-registry.injectable";
|
||||
|
||||
export type VisitEntityContextMenu = (entity: CatalogEntity, context: CatalogEntityContextMenuContext) => void;
|
||||
|
||||
const visitEntityContextMenuInjectable = getInjectable({
|
||||
id: "visit-entity-context-menu",
|
||||
instantiate: (di): VisitEntityContextMenu => {
|
||||
const categoryRegistry = di.inject(catalogCategoryRegistryInjectable);
|
||||
|
||||
return (entity, context) => {
|
||||
entity.onContextMenuOpen?.(context);
|
||||
categoryRegistry.getCategoryForEntity(entity)?.emit("contextMenuOpen", entity, context);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default visitEntityContextMenuInjectable;
|
||||
@@ -12,7 +12,7 @@ const allowedResourcesInjectable = getInjectable({
|
||||
instantiate: (di) => {
|
||||
const cluster = di.inject(hostedClusterInjectable);
|
||||
|
||||
return computed(() => new Set(cluster.allowedResources), {
|
||||
return computed(() => new Set(cluster?.allowedResources), {
|
||||
// This needs to be here so that during refresh changes are only propogated when necessary
|
||||
equals: (cur, prev) => comparer.structural(cur, prev),
|
||||
});
|
||||
|
||||
@@ -103,8 +103,12 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
return this.clusters.size > 0;
|
||||
}
|
||||
|
||||
getById(id: ClusterId): Cluster | null {
|
||||
return this.clusters.get(id) ?? null;
|
||||
getById(id: ClusterId | undefined): Cluster | undefined {
|
||||
if (id) {
|
||||
return this.clusters.get(id);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
addCluster(clusterOrModel: ClusterModel | Cluster): Cluster {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { getClusterIdFromHost } from "../utils";
|
||||
|
||||
const hostedClusterIdInjectable = getInjectable({
|
||||
id: "hosted-cluster-id",
|
||||
instantiate: () => getClusterIdFromHost(location.host),
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default hostedClusterIdInjectable;
|
||||
@@ -3,16 +3,17 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { getHostedClusterId } from "../utils";
|
||||
import hostedClusterIdInjectable from "./hosted-cluster-id.injectable";
|
||||
import clusterStoreInjectable from "./cluster-store.injectable";
|
||||
|
||||
const hostedClusterInjectable = getInjectable({
|
||||
id: "hosted-cluster",
|
||||
|
||||
instantiate: (di) => {
|
||||
const hostedClusterId = getHostedClusterId();
|
||||
const hostedClusterId = di.inject(hostedClusterIdInjectable);
|
||||
const store = di.inject(clusterStoreInjectable);
|
||||
|
||||
return di.inject(clusterStoreInjectable).getById(hostedClusterId);
|
||||
return store.getById(hostedClusterId);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { ipcMain } from "electron";
|
||||
import { action, comparer, computed, makeObservable, observable, reaction, when } from "mobx";
|
||||
import { broadcastMessage } from "../ipc";
|
||||
import type { ContextHandler } from "../../main/context-handler/context-handler";
|
||||
import type { ClusterContextHandler } from "../../main/context-handler/context-handler";
|
||||
import type { KubeConfig } from "@kubernetes/client-node";
|
||||
import { HttpError } from "@kubernetes/client-node";
|
||||
import type { Kubectl } from "../../main/kubectl/kubectl";
|
||||
@@ -14,22 +13,24 @@ import type { KubeconfigManager } from "../../main/kubeconfig-manager/kubeconfig
|
||||
import { loadConfigFromFile, loadConfigFromFileSync, validateKubeConfig } from "../kube-helpers";
|
||||
import type { KubeApiResource, KubeResource } from "../rbac";
|
||||
import { apiResourceRecord, apiResources } from "../rbac";
|
||||
import logger from "../../main/logger";
|
||||
import { VersionDetector } from "../../main/cluster-detectors/version-detector";
|
||||
import { DetectorRegistry } from "../../main/cluster-detectors/detector-registry";
|
||||
import plimit from "p-limit";
|
||||
import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate } from "../cluster-types";
|
||||
import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../cluster-types";
|
||||
import { disposer, toJS } from "../utils";
|
||||
import { disposer, isDefined, isRequestError, toJS } from "../utils";
|
||||
import type { Response } from "request";
|
||||
import { clusterListNamespaceForbiddenChannel } from "../ipc/cluster";
|
||||
import type { CanI } from "./authorization-review.injectable";
|
||||
import type { ListNamespaces } from "./list-namespaces.injectable";
|
||||
import assert from "assert";
|
||||
import type { Logger } from "../logger";
|
||||
|
||||
export interface ClusterDependencies {
|
||||
readonly directoryForKubeConfigs: string;
|
||||
createKubeconfigManager: (cluster: Cluster) => KubeconfigManager;
|
||||
createContextHandler: (cluster: Cluster) => ContextHandler;
|
||||
readonly logger: Logger;
|
||||
createKubeconfigManager: (cluster: Cluster) => KubeconfigManager | undefined;
|
||||
createContextHandler: (cluster: Cluster) => ClusterContextHandler | undefined;
|
||||
createKubectl: (clusterVersion: string) => Kubectl;
|
||||
createAuthorizationReview: (config: KubeConfig) => CanI;
|
||||
createListNamespaces: (config: KubeConfig) => ListNamespaces;
|
||||
@@ -43,17 +44,31 @@ export interface ClusterDependencies {
|
||||
export class Cluster implements ClusterModel, ClusterState {
|
||||
/** Unique id for a cluster */
|
||||
public readonly id: ClusterId;
|
||||
private kubeCtl: Kubectl;
|
||||
private kubeCtl: Kubectl | undefined;
|
||||
/**
|
||||
* Context handler
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public contextHandler: ContextHandler;
|
||||
protected proxyKubeconfigManager: KubeconfigManager;
|
||||
protected eventsDisposer = disposer();
|
||||
protected readonly _contextHandler: ClusterContextHandler | undefined;
|
||||
protected readonly _proxyKubeconfigManager: KubeconfigManager | undefined;
|
||||
protected readonly eventsDisposer = disposer();
|
||||
protected activated = false;
|
||||
private resourceAccessStatuses: Map<KubeApiResource, boolean> = new Map();
|
||||
private readonly resourceAccessStatuses = new Map<KubeApiResource, boolean>();
|
||||
|
||||
public get contextHandler() {
|
||||
// TODO: remove these once main/renderer are seperate classes
|
||||
assert(this._contextHandler, "contextHandler is only defined in the main environment");
|
||||
|
||||
return this._contextHandler;
|
||||
}
|
||||
|
||||
protected get proxyKubeconfigManager() {
|
||||
// TODO: remove these once main/renderer are seperate classes
|
||||
assert(this._proxyKubeconfigManager, "proxyKubeconfigManager is only defined in the main environment");
|
||||
|
||||
return this._proxyKubeconfigManager;
|
||||
}
|
||||
|
||||
get whenReady() {
|
||||
return when(() => this.ready);
|
||||
@@ -64,21 +79,21 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
*
|
||||
* @observable
|
||||
*/
|
||||
@observable contextName: string;
|
||||
@observable contextName!: string;
|
||||
/**
|
||||
* Path to kubeconfig
|
||||
*
|
||||
* @observable
|
||||
*/
|
||||
@observable kubeConfigPath: string;
|
||||
@observable kubeConfigPath!: string;
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@observable workspace: string;
|
||||
@observable workspace?: string;
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@observable workspaces: string[];
|
||||
@observable workspaces?: string[];
|
||||
/**
|
||||
* Kubernetes API server URL
|
||||
*
|
||||
@@ -215,7 +230,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
* @computed
|
||||
* @internal
|
||||
*/
|
||||
@computed get defaultNamespace(): string {
|
||||
@computed get defaultNamespace(): string | undefined {
|
||||
return this.preferences.defaultNamespace;
|
||||
}
|
||||
|
||||
@@ -231,19 +246,24 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
throw validationError;
|
||||
}
|
||||
|
||||
this.apiUrl = config.getCluster(config.getContextObject(this.contextName).cluster).server;
|
||||
const context = config.getContextObject(this.contextName);
|
||||
|
||||
if (ipcMain) {
|
||||
// for the time being, until renderer gets its own cluster type
|
||||
this.contextHandler = this.dependencies.createContextHandler(this);
|
||||
this.proxyKubeconfigManager = this.dependencies.createKubeconfigManager(this);
|
||||
assert(context);
|
||||
|
||||
logger.debug(`[CLUSTER]: Cluster init success`, {
|
||||
id: this.id,
|
||||
context: this.contextName,
|
||||
apiUrl: this.apiUrl,
|
||||
});
|
||||
}
|
||||
const cluster = config.getCluster(context.cluster);
|
||||
|
||||
assert(cluster);
|
||||
|
||||
this.apiUrl = cluster.server;
|
||||
|
||||
// for the time being, until renderer gets its own cluster type
|
||||
this._contextHandler = this.dependencies.createContextHandler(this);
|
||||
this._proxyKubeconfigManager = this.dependencies.createKubeconfigManager(this);
|
||||
this.dependencies.logger.debug(`[CLUSTER]: Cluster init success`, {
|
||||
id: this.id,
|
||||
context: this.contextName,
|
||||
apiUrl: this.apiUrl,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -255,6 +275,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
// Note: do not assign ID as that should never be updated
|
||||
|
||||
this.kubeConfigPath = model.kubeConfigPath;
|
||||
this.contextName = model.contextName;
|
||||
|
||||
if (model.workspace) {
|
||||
this.workspace = model.workspace;
|
||||
@@ -264,10 +285,6 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
this.workspaces = model.workspaces;
|
||||
}
|
||||
|
||||
if (model.contextName) {
|
||||
this.contextName = model.contextName;
|
||||
}
|
||||
|
||||
if (model.preferences) {
|
||||
this.preferences = model.preferences;
|
||||
}
|
||||
@@ -289,7 +306,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
* @internal
|
||||
*/
|
||||
protected bindEvents() {
|
||||
logger.info(`[CLUSTER]: bind events`, this.getMeta());
|
||||
this.dependencies.logger.info(`[CLUSTER]: bind events`, this.getMeta());
|
||||
const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000); // every 30s
|
||||
const refreshMetadataTimer = setInterval(() => !this.disconnected && this.refreshMetadata(), 900000); // every 15 minutes
|
||||
|
||||
@@ -310,13 +327,13 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
* @internal
|
||||
*/
|
||||
protected async recreateProxyKubeconfig() {
|
||||
logger.info("[CLUSTER]: Recreating proxy kubeconfig");
|
||||
this.dependencies.logger.info("[CLUSTER]: Recreating proxy kubeconfig");
|
||||
|
||||
try {
|
||||
await this.proxyKubeconfigManager.clear();
|
||||
await this.getProxyKubeconfig();
|
||||
} catch (error) {
|
||||
logger.error(`[CLUSTER]: failed to recreate proxy kubeconfig`, error);
|
||||
this.dependencies.logger.error(`[CLUSTER]: failed to recreate proxy kubeconfig`, error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,7 +347,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
return this.pushState();
|
||||
}
|
||||
|
||||
logger.info(`[CLUSTER]: activate`, this.getMeta());
|
||||
this.dependencies.logger.info(`[CLUSTER]: activate`, this.getMeta());
|
||||
|
||||
if (!this.eventsDisposer.length) {
|
||||
this.bindEvents();
|
||||
@@ -348,7 +365,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
await this.refreshAccessibility();
|
||||
// download kubectl in background, so it's not blocking dashboard
|
||||
this.ensureKubectl()
|
||||
.catch(error => logger.warn(`[CLUSTER]: failed to download kubectl for clusterId=${this.id}`, error));
|
||||
.catch(error => this.dependencies.logger.warn(`[CLUSTER]: failed to download kubectl for clusterId=${this.id}`, error));
|
||||
this.broadcastConnectUpdate("Connected, waiting for view to load ...");
|
||||
}
|
||||
|
||||
@@ -372,9 +389,8 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
*/
|
||||
@action
|
||||
async reconnect() {
|
||||
logger.info(`[CLUSTER]: reconnect`, this.getMeta());
|
||||
this.contextHandler?.stopServer();
|
||||
await this.contextHandler?.ensureServer();
|
||||
this.dependencies.logger.info(`[CLUSTER]: reconnect`, this.getMeta());
|
||||
await this.contextHandler?.restartServer();
|
||||
this.disconnected = false;
|
||||
}
|
||||
|
||||
@@ -383,10 +399,10 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
*/
|
||||
@action disconnect(): void {
|
||||
if (this.disconnected) {
|
||||
return void logger.debug("[CLUSTER]: already disconnected", { id: this.id });
|
||||
return void this.dependencies.logger.debug("[CLUSTER]: already disconnected", { id: this.id });
|
||||
}
|
||||
|
||||
logger.info(`[CLUSTER]: disconnecting`, { id: this.id });
|
||||
this.dependencies.logger.info(`[CLUSTER]: disconnecting`, { id: this.id });
|
||||
this.eventsDisposer();
|
||||
this.contextHandler?.stopServer();
|
||||
this.disconnected = true;
|
||||
@@ -397,7 +413,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
this.allowedNamespaces = [];
|
||||
this.resourceAccessStatuses.clear();
|
||||
this.pushState();
|
||||
logger.info(`[CLUSTER]: disconnected`, { id: this.id });
|
||||
this.dependencies.logger.info(`[CLUSTER]: disconnected`, { id: this.id });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -406,7 +422,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
*/
|
||||
@action
|
||||
async refresh(opts: ClusterRefreshOptions = {}) {
|
||||
logger.info(`[CLUSTER]: refresh`, this.getMeta());
|
||||
this.dependencies.logger.info(`[CLUSTER]: refresh`, this.getMeta());
|
||||
await this.refreshConnectionStatus();
|
||||
|
||||
if (this.accessible) {
|
||||
@@ -424,7 +440,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
*/
|
||||
@action
|
||||
async refreshMetadata() {
|
||||
logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta());
|
||||
this.dependencies.logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta());
|
||||
const metadata = await DetectorRegistry.getInstance().detectForCluster(this);
|
||||
const existingMetadata = this.metadata;
|
||||
|
||||
@@ -495,11 +511,31 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
|
||||
return ClusterStatus.AccessGranted;
|
||||
} catch (error) {
|
||||
logger.error(`[CLUSTER]: Failed to connect to "${this.contextName}": ${error}`);
|
||||
this.dependencies.logger.error(`[CLUSTER]: Failed to connect to "${this.contextName}": ${error}`);
|
||||
|
||||
if (error.statusCode) {
|
||||
if (error.statusCode >= 400 && error.statusCode < 500) {
|
||||
this.broadcastConnectUpdate("Invalid credentials", true);
|
||||
if (isRequestError(error)) {
|
||||
if (error.statusCode) {
|
||||
if (error.statusCode >= 400 && error.statusCode < 500) {
|
||||
this.broadcastConnectUpdate("Invalid credentials", true);
|
||||
|
||||
return ClusterStatus.AccessDenied;
|
||||
}
|
||||
|
||||
const message = String(error.error || error.message) || String(error);
|
||||
|
||||
this.broadcastConnectUpdate(message, true);
|
||||
|
||||
return ClusterStatus.Offline;
|
||||
}
|
||||
|
||||
if (error.failed === true) {
|
||||
if (error.timedOut === true) {
|
||||
this.broadcastConnectUpdate("Connection timed out", true);
|
||||
|
||||
return ClusterStatus.Offline;
|
||||
}
|
||||
|
||||
this.broadcastConnectUpdate("Failed to fetch credentials", true);
|
||||
|
||||
return ClusterStatus.AccessDenied;
|
||||
}
|
||||
@@ -507,26 +543,10 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
const message = String(error.error || error.message) || String(error);
|
||||
|
||||
this.broadcastConnectUpdate(message, true);
|
||||
|
||||
return ClusterStatus.Offline;
|
||||
} else {
|
||||
this.broadcastConnectUpdate("Unknown error has occurred", true);
|
||||
}
|
||||
|
||||
if (error.failed === true) {
|
||||
if (error.timedOut === true) {
|
||||
this.broadcastConnectUpdate("Connection timed out", true);
|
||||
|
||||
return ClusterStatus.Offline;
|
||||
}
|
||||
|
||||
this.broadcastConnectUpdate("Failed to fetch credentials", true);
|
||||
|
||||
return ClusterStatus.AccessDenied;
|
||||
}
|
||||
|
||||
const message = String(error.error || error.message) || String(error);
|
||||
|
||||
this.broadcastConnectUpdate(message, true);
|
||||
|
||||
return ClusterStatus.Offline;
|
||||
}
|
||||
}
|
||||
@@ -575,7 +595,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
* @param state cluster state
|
||||
*/
|
||||
pushState(state = this.getState()) {
|
||||
logger.silly(`[CLUSTER]: push-state`, state);
|
||||
this.dependencies.logger.silly(`[CLUSTER]: push-state`, state);
|
||||
broadcastMessage("cluster:state", this.id, state);
|
||||
}
|
||||
|
||||
@@ -598,7 +618,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
broadcastConnectUpdate(message: string, isError = false): void {
|
||||
const update: KubeAuthUpdate = { message, isError };
|
||||
|
||||
logger.debug(`[CLUSTER]: broadcasting connection update`, { ...update, meta: this.getMeta() });
|
||||
this.dependencies.logger.debug(`[CLUSTER]: broadcasting connection update`, { ...update, meta: this.getMeta() });
|
||||
broadcastMessage(`cluster:${this.id}:connection-update`, update);
|
||||
}
|
||||
|
||||
@@ -613,12 +633,12 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
return await listNamespaces();
|
||||
} catch (error) {
|
||||
const ctx = proxyConfig.getContextObject(this.contextName);
|
||||
const namespaceList = [ctx.namespace].filter(Boolean);
|
||||
const namespaceList = [ctx?.namespace].filter(isDefined);
|
||||
|
||||
if (namespaceList.length === 0 && error instanceof HttpError && error.statusCode === 403) {
|
||||
const { response } = error as HttpError & { response: Response };
|
||||
|
||||
logger.info("[CLUSTER]: listing namespaces is forbidden, broadcasting", { clusterId: this.id, error: response.body });
|
||||
this.dependencies.logger.info("[CLUSTER]: listing namespaces is forbidden, broadcasting", { clusterId: this.id, error: response.body });
|
||||
broadcastMessage(clusterListNamespaceForbiddenChannel, this.id);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,5 +6,8 @@ import { getInjectionToken } from "@ogre-tools/injectable";
|
||||
import type { ClusterModel } from "../cluster-types";
|
||||
import type { Cluster } from "./cluster";
|
||||
|
||||
export const createClusterInjectionToken =
|
||||
getInjectionToken<(model: ClusterModel) => Cluster>({ id: "create-cluster-token" });
|
||||
export type CreateCluster = (model: ClusterModel) => Cluster;
|
||||
|
||||
export const createClusterInjectionToken = getInjectionToken<CreateCluster>({
|
||||
id: "create-cluster-token",
|
||||
});
|
||||
@@ -5,6 +5,7 @@
|
||||
import type { KubeConfig } from "@kubernetes/client-node";
|
||||
import { CoreV1Api } from "@kubernetes/client-node";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { isDefined } from "../utils";
|
||||
|
||||
export type ListNamespaces = () => Promise<string[]>;
|
||||
|
||||
@@ -14,7 +15,9 @@ export function listNamespaces(config: KubeConfig): ListNamespaces {
|
||||
return async () => {
|
||||
const { body: { items }} = await coreApi.listNamespace();
|
||||
|
||||
return items.map(ns => ns.metadata.name);
|
||||
return items
|
||||
.map(ns => ns.metadata?.name)
|
||||
.filter(isDefined);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -14,12 +14,9 @@ describe("verify-that-all-routes-have-component", () => {
|
||||
it("verify that routes have route component", async () => {
|
||||
const rendererDi = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
rendererDi.override(
|
||||
clusterStoreInjectable,
|
||||
() => ({ getById: (): null => null } as unknown as ClusterStore),
|
||||
);
|
||||
|
||||
await rendererDi.runSetups();
|
||||
rendererDi.override(clusterStoreInjectable, () => ({
|
||||
getById: () => null,
|
||||
} as unknown as ClusterStore));
|
||||
|
||||
const routes = rendererDi.injectMany(routeInjectionToken);
|
||||
const routeComponents = rendererDi.injectMany(
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import hotbarStoreInjectable from "./store.injectable";
|
||||
import type { CreateHotbarData, CreateHotbarOptions } from "./types";
|
||||
|
||||
export type AddHotbar = (data: CreateHotbarData, opts?: CreateHotbarOptions) => void;
|
||||
|
||||
const addHotbarInjectable = getInjectable({
|
||||
id: "add-hotbar",
|
||||
instantiate: (di): AddHotbar => {
|
||||
const store = di.inject(hotbarStoreInjectable);
|
||||
|
||||
return (data, opts) => store.add(data, opts);
|
||||
},
|
||||
});
|
||||
|
||||
export default addHotbarInjectable;
|
||||
@@ -3,8 +3,9 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import catalogCatalogEntityInjectable from "./catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable";
|
||||
import { HotbarStore } from "./hotbar-store";
|
||||
import catalogCatalogEntityInjectable from "../catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable";
|
||||
import { HotbarStore } from "./store";
|
||||
import loggerInjectable from "../logger.injectable";
|
||||
|
||||
const hotbarStoreInjectable = getInjectable({
|
||||
id: "hotbar-store",
|
||||
@@ -14,6 +15,7 @@ const hotbarStoreInjectable = getInjectable({
|
||||
|
||||
return HotbarStore.createInstance({
|
||||
catalogCatalogEntity: di.inject(catalogCatalogEntityInjectable),
|
||||
logger: di.inject(loggerInjectable),
|
||||
});
|
||||
},
|
||||
|
||||
@@ -4,22 +4,17 @@
|
||||
*/
|
||||
|
||||
import { action, comparer, observable, makeObservable, computed } from "mobx";
|
||||
import { BaseStore } from "./base-store";
|
||||
import migrations from "../migrations/hotbar-store";
|
||||
import { toJS } from "./utils";
|
||||
import type { CatalogEntity } from "./catalog";
|
||||
import logger from "../main/logger";
|
||||
import { broadcastMessage } from "./ipc";
|
||||
import type {
|
||||
Hotbar,
|
||||
CreateHotbarData,
|
||||
CreateHotbarOptions } from "./hotbar-types";
|
||||
import {
|
||||
defaultHotbarCells,
|
||||
getEmptyHotbar,
|
||||
} from "./hotbar-types";
|
||||
import { hotbarTooManyItemsChannel } from "./ipc/hotbar";
|
||||
import type { GeneralEntity } from "./catalog-entities";
|
||||
import { BaseStore } from "../base-store";
|
||||
import migrations from "../../migrations/hotbar-store";
|
||||
import { toJS } from "../utils";
|
||||
import type { CatalogEntity } from "../catalog";
|
||||
import { broadcastMessage } from "../ipc";
|
||||
import type { Hotbar, CreateHotbarData, CreateHotbarOptions } from "./types";
|
||||
import { defaultHotbarCells, getEmptyHotbar } from "./types";
|
||||
import { hotbarTooManyItemsChannel } from "../ipc/hotbar";
|
||||
import type { GeneralEntity } from "../catalog-entities";
|
||||
import type { Logger } from "../logger";
|
||||
import assert from "assert";
|
||||
|
||||
export interface HotbarStoreModel {
|
||||
hotbars: Hotbar[];
|
||||
@@ -27,15 +22,16 @@ export interface HotbarStoreModel {
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
catalogCatalogEntity: GeneralEntity;
|
||||
readonly catalogCatalogEntity: GeneralEntity;
|
||||
readonly logger: Logger;
|
||||
}
|
||||
|
||||
export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
||||
readonly displayName = "HotbarStore";
|
||||
@observable hotbars: Hotbar[] = [];
|
||||
@observable private _activeHotbarId: string;
|
||||
@observable private _activeHotbarId!: string;
|
||||
|
||||
constructor(private dependencies: Dependencies) {
|
||||
constructor(private readonly dependencies: Dependencies) {
|
||||
super({
|
||||
configName: "lens-hotbar-store",
|
||||
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
|
||||
@@ -62,7 +58,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
||||
this._activeHotbarId = this.hotbars[hotbar].id;
|
||||
}
|
||||
} else if (typeof hotbar === "string") {
|
||||
if (this.getById(hotbar)) {
|
||||
if (this.findById(hotbar)) {
|
||||
this._activeHotbarId = hotbar;
|
||||
}
|
||||
} else {
|
||||
@@ -120,34 +116,35 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
||||
return toJS(model);
|
||||
}
|
||||
|
||||
getActive() {
|
||||
return this.getById(this.activeHotbarId);
|
||||
getActive(): Hotbar {
|
||||
const hotbar = this.findById(this.activeHotbarId);
|
||||
|
||||
assert(hotbar, "There MUST always be an active hotbar");
|
||||
|
||||
return hotbar;
|
||||
}
|
||||
|
||||
getByName(name: string) {
|
||||
findByName(name: string) {
|
||||
return this.hotbars.find((hotbar) => hotbar.name === name);
|
||||
}
|
||||
|
||||
getById(id: string) {
|
||||
findById(id: string) {
|
||||
return this.hotbars.find((hotbar) => hotbar.id === id);
|
||||
}
|
||||
|
||||
add = action(
|
||||
(
|
||||
data: CreateHotbarData,
|
||||
{ setActive = false }: CreateHotbarOptions = {},
|
||||
) => {
|
||||
const hotbar = getEmptyHotbar(data.name, data.id);
|
||||
@action
|
||||
add(data: CreateHotbarData, { setActive = false }: CreateHotbarOptions = {}) {
|
||||
const hotbar = getEmptyHotbar(data.name, data.id);
|
||||
|
||||
this.hotbars.push(hotbar);
|
||||
this.hotbars.push(hotbar);
|
||||
|
||||
if (setActive) {
|
||||
this._activeHotbarId = hotbar.id;
|
||||
}
|
||||
},
|
||||
);
|
||||
if (setActive) {
|
||||
this._activeHotbarId = hotbar.id;
|
||||
}
|
||||
}
|
||||
|
||||
setHotbarName = action((id: string, name: string) => {
|
||||
@action
|
||||
setHotbarName(id: string, name: string): void {
|
||||
const index = this.hotbars.findIndex((hotbar) => hotbar.id === id);
|
||||
|
||||
if (index < 0) {
|
||||
@@ -158,19 +155,18 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
||||
}
|
||||
|
||||
this.hotbars[index].name = name;
|
||||
});
|
||||
}
|
||||
|
||||
remove = action((hotbar: Hotbar) => {
|
||||
if (this.hotbars.length <= 1) {
|
||||
throw new Error("Cannot remove the last hotbar");
|
||||
}
|
||||
@action
|
||||
remove(hotbar: Hotbar) {
|
||||
assert(this.hotbars.length >= 2, "Cannot remove the last hotbar");
|
||||
|
||||
this.hotbars = this.hotbars.filter((h) => h !== hotbar);
|
||||
|
||||
if (this.activeHotbarId === hotbar.id) {
|
||||
this.setActiveHotbar(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
addToHotbar(item: CatalogEntity, cellIndex?: number) {
|
||||
@@ -209,7 +205,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
||||
} else if (0 <= cellIndex && cellIndex < hotbar.items.length) {
|
||||
hotbar.items[cellIndex] = newItem;
|
||||
} else {
|
||||
logger.error(
|
||||
this.dependencies.logger.error(
|
||||
`[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range`,
|
||||
{ entityId: uid, hotbarId: hotbar.id, cellIndex },
|
||||
);
|
||||
@@ -246,8 +242,9 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
||||
|
||||
findClosestEmptyIndex(from: number, direction = 1) {
|
||||
let index = from;
|
||||
const hotbar = this.getActive();
|
||||
|
||||
while (this.getActive().items[index] != null) {
|
||||
while (hotbar.items[index] != null) {
|
||||
index += direction;
|
||||
}
|
||||
|
||||
@@ -314,11 +311,9 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
this.getActive().items.findIndex(
|
||||
(item) => item?.entity.uid === entity.getId(),
|
||||
) >= 0
|
||||
);
|
||||
const indexInActiveHotbar = this.getActive().items.findIndex(item => item?.entity.uid === entity.getId());
|
||||
|
||||
return indexInActiveHotbar >= 0;
|
||||
}
|
||||
|
||||
getDisplayLabel(hotbar: Hotbar): string {
|
||||
@@ -4,13 +4,13 @@
|
||||
*/
|
||||
|
||||
import * as uuid from "uuid";
|
||||
import type { Tuple } from "./utils";
|
||||
import { tuple } from "./utils";
|
||||
import type { Tuple } from "../utils";
|
||||
import { tuple } from "../utils";
|
||||
|
||||
export interface HotbarItem {
|
||||
entity: {
|
||||
uid: string;
|
||||
name?: string;
|
||||
name: string;
|
||||
source?: string;
|
||||
};
|
||||
params?: {
|
||||
@@ -6,5 +6,5 @@ import type { Channel } from "../channel";
|
||||
|
||||
export const createChannel = <Message>(name: string): Channel<Message> => ({
|
||||
name,
|
||||
_template: null,
|
||||
_template: null as never,
|
||||
});
|
||||
@@ -53,7 +53,7 @@ describe("type enforced ipc tests", () => {
|
||||
const source = new EventEmitter();
|
||||
const listener = () => called += 1;
|
||||
const results = [true, false, true];
|
||||
const verifier = (args: unknown[]): args is [] => results.pop();
|
||||
const verifier = (args: unknown[]): args is [] => results.pop() ?? false;
|
||||
const channel = "foobar";
|
||||
|
||||
onCorrect({ source, listener, verifier, channel });
|
||||
|
||||
@@ -13,8 +13,6 @@ export interface ItemObject {
|
||||
}
|
||||
|
||||
export abstract class ItemStore<Item extends ItemObject> {
|
||||
abstract loadAll(...args: any[]): Promise<void | Item[]>;
|
||||
|
||||
protected defaultSorting = (item: Item) => item.getName();
|
||||
|
||||
@observable failedLoading = false;
|
||||
@@ -44,8 +42,7 @@ export abstract class ItemStore<Item extends ItemObject> {
|
||||
return this.items.length;
|
||||
}
|
||||
|
||||
getByName(name: string, ...args: any[]): Item;
|
||||
getByName(name: string): Item {
|
||||
getByName(name: string): Item | undefined {
|
||||
return this.items.find(item => item.getName() === name);
|
||||
}
|
||||
|
||||
@@ -115,7 +112,6 @@ export abstract class ItemStore<Item extends ItemObject> {
|
||||
}
|
||||
}
|
||||
|
||||
protected async loadItem(...args: any[]): Promise<Item>;
|
||||
@action
|
||||
protected async loadItem(request: () => Promise<Item>, sortItems = true) {
|
||||
const item = await Promise.resolve(request()).catch(() => null);
|
||||
@@ -133,9 +129,9 @@ export abstract class ItemStore<Item extends ItemObject> {
|
||||
if (sortItems) items = this.sortItems(items);
|
||||
this.items.replace(items);
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
@action
|
||||
|
||||
@@ -3,19 +3,32 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { ingressStore } from "../../../renderer/components/+network-ingresses/ingress.store";
|
||||
import { apiManager } from "../api-manager";
|
||||
import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting";
|
||||
import type { ApiManager } from "../api-manager";
|
||||
import apiManagerInjectable from "../api-manager/manager.injectable";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { KubeObjectStore } from "../kube-object.store";
|
||||
|
||||
class TestApi extends KubeApi<KubeObject> {
|
||||
|
||||
protected async checkPreferredVersion() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
class TestStore extends KubeObjectStore<KubeObject, TestApi> {
|
||||
|
||||
}
|
||||
|
||||
describe("ApiManager", () => {
|
||||
let apiManager: ApiManager;
|
||||
|
||||
beforeEach(() => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
apiManager = di.inject(apiManagerInjectable);
|
||||
});
|
||||
|
||||
describe("registerApi", () => {
|
||||
it("re-register store if apiBase changed", async () => {
|
||||
const apiBase = "apis/v1/foo";
|
||||
@@ -23,25 +36,27 @@ describe("ApiManager", () => {
|
||||
const kubeApi = new TestApi({
|
||||
objectConstructor: KubeObject,
|
||||
apiBase,
|
||||
kind: "foo",
|
||||
fallbackApiBases: [fallbackApiBase],
|
||||
checkPreferredVersion: true,
|
||||
});
|
||||
const kubeStore = new TestStore(kubeApi);
|
||||
|
||||
apiManager.registerApi(apiBase, kubeApi);
|
||||
|
||||
// Define to use test api for ingress store
|
||||
Object.defineProperty(ingressStore, "api", { value: kubeApi });
|
||||
apiManager.registerStore(ingressStore, [kubeApi]);
|
||||
Object.defineProperty(kubeStore, "api", { value: kubeApi });
|
||||
apiManager.registerStore(kubeStore, [kubeApi]);
|
||||
|
||||
// Test that store is returned with original apiBase
|
||||
expect(apiManager.getStore(kubeApi)).toBe(ingressStore);
|
||||
expect(apiManager.getStore(kubeApi)).toBe(kubeStore);
|
||||
|
||||
// Change apiBase similar as checkPreferredVersion does
|
||||
Object.defineProperty(kubeApi, "apiBase", { value: fallbackApiBase });
|
||||
apiManager.registerApi(fallbackApiBase, kubeApi);
|
||||
|
||||
// Test that store is returned with new apiBase
|
||||
expect(apiManager.getStore(kubeApi)).toBe(ingressStore);
|
||||
expect(apiManager.getStore(kubeApi)).toBe(kubeStore);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -16,8 +16,15 @@ describe("Crds", () => {
|
||||
name: "foo",
|
||||
resourceVersion: "12345",
|
||||
uid: "12345",
|
||||
selfLink: "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/foo",
|
||||
},
|
||||
spec: {
|
||||
group: "foo.bar",
|
||||
names: {
|
||||
kind: "Foo",
|
||||
plural: "foos",
|
||||
},
|
||||
scope: "Namespaced",
|
||||
versions: [
|
||||
{
|
||||
name: "123",
|
||||
@@ -44,8 +51,15 @@ describe("Crds", () => {
|
||||
name: "foo",
|
||||
resourceVersion: "12345",
|
||||
uid: "12345",
|
||||
selfLink: "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/foo",
|
||||
},
|
||||
spec: {
|
||||
group: "foo.bar",
|
||||
names: {
|
||||
kind: "Foo",
|
||||
plural: "foos",
|
||||
},
|
||||
scope: "Namespaced",
|
||||
versions: [
|
||||
{
|
||||
name: "123",
|
||||
@@ -72,8 +86,15 @@ describe("Crds", () => {
|
||||
name: "foo",
|
||||
resourceVersion: "12345",
|
||||
uid: "12345",
|
||||
selfLink: "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/foo",
|
||||
},
|
||||
spec: {
|
||||
group: "foo.bar",
|
||||
names: {
|
||||
kind: "Foo",
|
||||
plural: "foos",
|
||||
},
|
||||
scope: "Namespaced",
|
||||
versions: [
|
||||
{
|
||||
name: "123",
|
||||
@@ -100,8 +121,15 @@ describe("Crds", () => {
|
||||
name: "foo",
|
||||
resourceVersion: "12345",
|
||||
uid: "12345",
|
||||
selfLink: "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/foo",
|
||||
},
|
||||
spec: {
|
||||
group: "foo.bar",
|
||||
names: {
|
||||
kind: "Foo",
|
||||
plural: "foos",
|
||||
},
|
||||
scope: "Namespaced",
|
||||
version: "abc",
|
||||
versions: [
|
||||
{
|
||||
@@ -129,6 +157,7 @@ describe("Crds", () => {
|
||||
name: "foo",
|
||||
resourceVersion: "12345",
|
||||
uid: "12345",
|
||||
selfLink: "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/foo",
|
||||
},
|
||||
spec: {
|
||||
version: "abc",
|
||||
|
||||
@@ -3,31 +3,39 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { Deployment, DeploymentApi } from "../endpoints/deployment.api";
|
||||
import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting";
|
||||
import storesAndApisCanBeCreatedInjectable from "../../../renderer/stores-apis-can-be-created.injectable";
|
||||
import apiKubeInjectable from "../../../renderer/k8s/api-kube.injectable";
|
||||
import type { DeploymentApi } from "../endpoints/deployment.api";
|
||||
import deploymentApiInjectable from "../endpoints/deployment.api.injectable";
|
||||
import type { KubeJsonApi } from "../kube-json-api";
|
||||
|
||||
class DeploymentApiTest extends DeploymentApi {
|
||||
public setRequest(request: any) {
|
||||
this.request = request;
|
||||
}
|
||||
}
|
||||
|
||||
describe("DeploymentApi", () => {
|
||||
let deploymentApi: DeploymentApi;
|
||||
let kubeJsonApi: jest.Mocked<KubeJsonApi>;
|
||||
|
||||
beforeEach(() => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
di.override(storesAndApisCanBeCreatedInjectable, () => true);
|
||||
kubeJsonApi = {
|
||||
getResponse: jest.fn(),
|
||||
get: jest.fn(),
|
||||
post: jest.fn(),
|
||||
put: jest.fn(),
|
||||
patch: jest.fn(),
|
||||
del: jest.fn(),
|
||||
} as never;
|
||||
di.override(apiKubeInjectable, () => kubeJsonApi);
|
||||
|
||||
deploymentApi = di.inject(deploymentApiInjectable);
|
||||
});
|
||||
|
||||
describe("scale", () => {
|
||||
const requestMock = {
|
||||
patch: () => ({}),
|
||||
} as unknown as KubeJsonApi;
|
||||
|
||||
const sub = new DeploymentApiTest({ objectConstructor: Deployment });
|
||||
|
||||
sub.setRequest(requestMock);
|
||||
|
||||
it("requests Kubernetes API with PATCH verb and correct amount of replicas", () => {
|
||||
const patchSpy = jest.spyOn(requestMock, "patch");
|
||||
deploymentApi.scale({ namespace: "default", name: "deployment-1" }, 5);
|
||||
|
||||
sub.scale({ namespace: "default", name: "deployment-1" }, 5);
|
||||
|
||||
expect(patchSpy).toHaveBeenCalledWith("/apis/apps/v1/namespaces/default/deployments/deployment-1/scale", {
|
||||
expect(kubeJsonApi.patch).toHaveBeenCalledWith("/apis/apps/v1/namespaces/default/deployments/deployment-1/scale", {
|
||||
data: {
|
||||
spec: {
|
||||
replicas: 5,
|
||||
|
||||
@@ -3,42 +3,26 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { EndpointSubset } from "../endpoints";
|
||||
import { formatEndpointSubset } from "../endpoints";
|
||||
|
||||
describe("endpoint tests", () => {
|
||||
describe("EndpointSubset", () => {
|
||||
it.each([
|
||||
4,
|
||||
false,
|
||||
null,
|
||||
{},
|
||||
[],
|
||||
"ahe",
|
||||
/a/,
|
||||
])("should always initialize fields when given %j", (data: any) => {
|
||||
const sub = new EndpointSubset(data);
|
||||
|
||||
expect(sub.addresses).toStrictEqual([]);
|
||||
expect(sub.notReadyAddresses).toStrictEqual([]);
|
||||
expect(sub.ports).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("toString should be addresses X ports", () => {
|
||||
const sub = new EndpointSubset({
|
||||
it("formatEndpointSubset should be addresses X ports", () => {
|
||||
const formatted = formatEndpointSubset({
|
||||
addresses: [{
|
||||
ip: "1.1.1.1",
|
||||
}, {
|
||||
ip: "1.1.1.2",
|
||||
}] as any,
|
||||
}],
|
||||
notReadyAddresses: [],
|
||||
ports: [{
|
||||
port: "81",
|
||||
port: 81,
|
||||
}, {
|
||||
port: "82",
|
||||
}] as any,
|
||||
port: 82,
|
||||
}],
|
||||
});
|
||||
|
||||
expect(sub.toString()).toBe("1.1.1.1:81, 1.1.1.1:82, 1.1.1.2:81, 1.1.1.2:82");
|
||||
expect(formatted).toBe("1.1.1.1:81, 1.1.1.1:82, 1.1.1.2:81, 1.1.1.2:82");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -9,33 +9,33 @@ import { HelmChart } from "../endpoints/helm-charts.api";
|
||||
describe("HelmChart tests", () => {
|
||||
describe("HelmChart.create() tests", () => {
|
||||
it("should throw on non-object input", () => {
|
||||
expect(() => HelmChart.create("" as any)).toThrowError('"value" must be of type object');
|
||||
expect(() => HelmChart.create(1 as any)).toThrowError('"value" must be of type object');
|
||||
expect(() => HelmChart.create(false as any)).toThrowError('"value" must be of type object');
|
||||
expect(() => HelmChart.create([] as any)).toThrowError('"value" must be of type object');
|
||||
expect(() => HelmChart.create(Symbol() as any)).toThrowError('"value" must be of type object');
|
||||
expect(() => HelmChart.create("" as never)).toThrowError('"value" must be of type object');
|
||||
expect(() => HelmChart.create(1 as never)).toThrowError('"value" must be of type object');
|
||||
expect(() => HelmChart.create(false as never)).toThrowError('"value" must be of type object');
|
||||
expect(() => HelmChart.create([] as never)).toThrowError('"value" must be of type object');
|
||||
expect(() => HelmChart.create(Symbol() as never)).toThrowError('"value" must be of type object');
|
||||
});
|
||||
|
||||
it("should throw on missing fields", () => {
|
||||
expect(() => HelmChart.create({} as any)).toThrowError('"apiVersion" is required');
|
||||
expect(() => HelmChart.create({} as never)).toThrowError('"apiVersion" is required');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "!",
|
||||
} as any)).toThrowError('"name" is required');
|
||||
} as never)).toThrowError('"name" is required');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "!",
|
||||
name: "!",
|
||||
} as any)).toThrowError('"version" is required');
|
||||
} as never)).toThrowError('"version" is required');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "!",
|
||||
name: "!",
|
||||
version: "!",
|
||||
} as any)).toThrowError('"repo" is required');
|
||||
} as never)).toThrowError('"repo" is required');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "!",
|
||||
name: "!",
|
||||
version: "!",
|
||||
repo: "!",
|
||||
} as any)).toThrowError('"created" is required');
|
||||
} as never)).toThrowError('"created" is required');
|
||||
});
|
||||
|
||||
it("should throw on fields being wrong type", () => {
|
||||
@@ -46,7 +46,7 @@ describe("HelmChart tests", () => {
|
||||
repo: "!",
|
||||
created: "!",
|
||||
digest: "!",
|
||||
} as any)).toThrowError('"apiVersion" must be a string');
|
||||
} as never)).toThrowError('"apiVersion" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: 1,
|
||||
@@ -54,7 +54,7 @@ describe("HelmChart tests", () => {
|
||||
repo: "!",
|
||||
created: "!",
|
||||
digest: "!",
|
||||
} as any)).toThrowError('"name" must be a string');
|
||||
} as never)).toThrowError('"name" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "!",
|
||||
name: "!",
|
||||
@@ -62,7 +62,7 @@ describe("HelmChart tests", () => {
|
||||
repo: "!",
|
||||
created: "!",
|
||||
digest: 1,
|
||||
} as any)).toThrowError('"digest" must be a string');
|
||||
} as never)).toThrowError('"digest" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "",
|
||||
@@ -70,7 +70,7 @@ describe("HelmChart tests", () => {
|
||||
repo: "!",
|
||||
created: "!",
|
||||
digest: "!",
|
||||
} as any)).toThrowError('"version" must be a string');
|
||||
} as never)).toThrowError('"version" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
@@ -78,7 +78,7 @@ describe("HelmChart tests", () => {
|
||||
repo: 1,
|
||||
created: "!",
|
||||
digest: "!",
|
||||
} as any)).toThrowError('"repo" must be a string');
|
||||
} as never)).toThrowError('"repo" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
@@ -86,7 +86,7 @@ describe("HelmChart tests", () => {
|
||||
repo: "1",
|
||||
created: 1,
|
||||
digest: "a",
|
||||
} as any)).toThrowError('"created" must be a string');
|
||||
} as never)).toThrowError('"created" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
@@ -94,7 +94,7 @@ describe("HelmChart tests", () => {
|
||||
repo: "1",
|
||||
created: "!",
|
||||
digest: 1,
|
||||
} as any)).toThrowError('"digest" must be a string');
|
||||
} as never)).toThrowError('"digest" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
@@ -103,7 +103,7 @@ describe("HelmChart tests", () => {
|
||||
digest: "1",
|
||||
created: "!",
|
||||
kubeVersion: 1,
|
||||
} as any)).toThrowError('"kubeVersion" must be a string');
|
||||
} as never)).toThrowError('"kubeVersion" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
@@ -112,7 +112,7 @@ describe("HelmChart tests", () => {
|
||||
digest: "1",
|
||||
created: "!",
|
||||
description: 1,
|
||||
} as any)).toThrowError('"description" must be a string');
|
||||
} as never)).toThrowError('"description" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
@@ -121,7 +121,7 @@ describe("HelmChart tests", () => {
|
||||
digest: "1",
|
||||
created: "!",
|
||||
home: 1,
|
||||
} as any)).toThrowError('"home" must be a string');
|
||||
} as never)).toThrowError('"home" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
@@ -130,7 +130,7 @@ describe("HelmChart tests", () => {
|
||||
digest: "1",
|
||||
created: "!",
|
||||
engine: 1,
|
||||
} as any)).toThrowError('"engine" must be a string');
|
||||
} as never)).toThrowError('"engine" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
@@ -139,7 +139,7 @@ describe("HelmChart tests", () => {
|
||||
digest: "1",
|
||||
created: "!",
|
||||
icon: 1,
|
||||
} as any)).toThrowError('"icon" must be a string');
|
||||
} as never)).toThrowError('"icon" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
@@ -148,7 +148,7 @@ describe("HelmChart tests", () => {
|
||||
digest: "1",
|
||||
created: "!",
|
||||
appVersion: 1,
|
||||
} as any)).toThrowError('"appVersion" must be a string');
|
||||
} as never)).toThrowError('"appVersion" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
@@ -157,7 +157,7 @@ describe("HelmChart tests", () => {
|
||||
digest: "1",
|
||||
created: "!",
|
||||
tillerVersion: 1,
|
||||
} as any)).toThrowError('"tillerVersion" must be a string');
|
||||
} as never)).toThrowError('"tillerVersion" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
@@ -166,7 +166,7 @@ describe("HelmChart tests", () => {
|
||||
digest: "1",
|
||||
created: "!",
|
||||
deprecated: 1,
|
||||
} as any)).toThrowError('"deprecated" must be a boolean');
|
||||
} as never)).toThrowError('"deprecated" must be a boolean');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
@@ -175,7 +175,7 @@ describe("HelmChart tests", () => {
|
||||
digest: "1",
|
||||
created: "!",
|
||||
keywords: 1,
|
||||
} as any)).toThrowError('"keywords" must be an array');
|
||||
} as never)).toThrowError('"keywords" must be an array');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
@@ -184,7 +184,7 @@ describe("HelmChart tests", () => {
|
||||
digest: "1",
|
||||
created: "!",
|
||||
sources: 1,
|
||||
} as any)).toThrowError('"sources" must be an array');
|
||||
} as never)).toThrowError('"sources" must be an array');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
@@ -193,7 +193,7 @@ describe("HelmChart tests", () => {
|
||||
digest: "1",
|
||||
created: "!",
|
||||
maintainers: 1,
|
||||
} as any)).toThrowError('"maintainers" must be an array');
|
||||
} as never)).toThrowError('"maintainers" must be an array');
|
||||
});
|
||||
|
||||
it("should filter non-string keywords", () => {
|
||||
@@ -204,10 +204,10 @@ describe("HelmChart tests", () => {
|
||||
repo: "1",
|
||||
digest: "1",
|
||||
created: "!",
|
||||
keywords: [1, "a", false, {}, "b"] as any,
|
||||
keywords: [1, "a", false, {}, "b"] as never,
|
||||
});
|
||||
|
||||
expect(chart.keywords).toStrictEqual(["a", "b"]);
|
||||
expect(chart?.keywords).toStrictEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
it("should filter non-string sources", () => {
|
||||
@@ -218,10 +218,10 @@ describe("HelmChart tests", () => {
|
||||
repo: "1",
|
||||
digest: "1",
|
||||
created: "!",
|
||||
sources: [1, "a", false, {}, "b"] as any,
|
||||
sources: [1, "a", false, {}, "b"] as never,
|
||||
});
|
||||
|
||||
expect(chart.sources).toStrictEqual(["a", "b"]);
|
||||
expect(chart?.sources).toStrictEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
it("should filter invalid maintainers", () => {
|
||||
@@ -236,10 +236,10 @@ describe("HelmChart tests", () => {
|
||||
name: "a",
|
||||
email: "b",
|
||||
url: "c",
|
||||
}] as any,
|
||||
}] as never,
|
||||
});
|
||||
|
||||
expect(chart.maintainers).toStrictEqual([{
|
||||
expect(chart?.maintainers).toStrictEqual([{
|
||||
name: "a",
|
||||
email: "b",
|
||||
url: "c",
|
||||
@@ -261,9 +261,9 @@ describe("HelmChart tests", () => {
|
||||
name: "a",
|
||||
email: "b",
|
||||
url: "c",
|
||||
}] as any,
|
||||
}] as never,
|
||||
"asdjhajksdhadjks": 1,
|
||||
} as any);
|
||||
} as never);
|
||||
|
||||
expect(warnFn).toHaveBeenCalledWith("HelmChart data has unexpected fields", {
|
||||
original: anyObject(),
|
||||
|
||||
@@ -14,6 +14,8 @@ describe("computeRuleDeclarations", () => {
|
||||
name: "foo",
|
||||
resourceVersion: "1",
|
||||
uid: "bar",
|
||||
namespace: "default",
|
||||
selfLink: "/apis/networking.k8s.io/v1/ingresses/default/foo",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -21,6 +23,7 @@ describe("computeRuleDeclarations", () => {
|
||||
host: "foo.bar",
|
||||
http: {
|
||||
paths: [{
|
||||
pathType: "Exact",
|
||||
backend: {
|
||||
service: {
|
||||
name: "my-service",
|
||||
@@ -44,6 +47,8 @@ describe("computeRuleDeclarations", () => {
|
||||
name: "foo",
|
||||
resourceVersion: "1",
|
||||
uid: "bar",
|
||||
namespace: "default",
|
||||
selfLink: "/apis/networking.k8s.io/v1/ingresses/default/foo",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -55,6 +60,7 @@ describe("computeRuleDeclarations", () => {
|
||||
host: "foo.bar",
|
||||
http: {
|
||||
paths: [{
|
||||
pathType: "Exact",
|
||||
backend: {
|
||||
service: {
|
||||
name: "my-service",
|
||||
@@ -78,6 +84,8 @@ describe("computeRuleDeclarations", () => {
|
||||
name: "foo",
|
||||
resourceVersion: "1",
|
||||
uid: "bar",
|
||||
namespace: "default",
|
||||
selfLink: "/apis/networking.k8s.io/v1/ingresses/default/foo",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -91,6 +99,7 @@ describe("computeRuleDeclarations", () => {
|
||||
host: "foo.bar",
|
||||
http: {
|
||||
paths: [{
|
||||
pathType: "Exact",
|
||||
backend: {
|
||||
service: {
|
||||
name: "my-service",
|
||||
|
||||
@@ -19,7 +19,7 @@ import { parseKubeApi } from "../kube-api-parse";
|
||||
/**
|
||||
* [<input-url>, <expected-result>]
|
||||
*/
|
||||
type KubeApiParseTestData = [string, Required<IKubeApiParsed>];
|
||||
type KubeApiParseTestData = [string, IKubeApiParsed];
|
||||
|
||||
const tests: KubeApiParseTestData[] = [
|
||||
["/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions/prometheuses.monitoring.coreos.com", {
|
||||
@@ -126,6 +126,6 @@ describe("parseApi unit tests", () => {
|
||||
});
|
||||
|
||||
it.each(throwtests)("testing %j should throw", (url) => {
|
||||
expect(() => parseKubeApi(url)).toThrowError("invalid apiPath");
|
||||
expect(() => parseKubeApi(url as never)).toThrowError("invalid apiPath");
|
||||
});
|
||||
});
|
||||
@@ -3,34 +3,36 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { Request } from "node-fetch";
|
||||
import { forRemoteCluster, KubeApi } from "../kube-api";
|
||||
import { KubeJsonApi } from "../kube-json-api";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import AbortController from "abort-controller";
|
||||
import { delay } from "../../utils/delay";
|
||||
import { PassThrough } from "stream";
|
||||
import type { ApiManager } from "../api-manager";
|
||||
import { apiManager } from "../api-manager";
|
||||
import { Ingress, Pod } from "../endpoints";
|
||||
import { ApiManager } from "../api-manager";
|
||||
import type { FetchMock } from "jest-fetch-mock/types";
|
||||
import { DeploymentApi, Ingress, IngressApi, Pod, PodApi } from "../endpoints";
|
||||
import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting";
|
||||
import apiManagerInjectable from "../api-manager/manager.injectable";
|
||||
import autoRegistrationInjectable from "../api-manager/auto-registration.injectable";
|
||||
|
||||
jest.mock("../api-manager");
|
||||
|
||||
const mockApiManager = apiManager as jest.Mocked<ApiManager>;
|
||||
|
||||
class TestKubeObject extends KubeObject {
|
||||
static kind = "Pod";
|
||||
static namespaced = true;
|
||||
static apiBase = "/api/v1/pods";
|
||||
}
|
||||
|
||||
class TestKubeApi extends KubeApi<TestKubeObject> {
|
||||
public async checkPreferredVersion() {
|
||||
return super.checkPreferredVersion();
|
||||
}
|
||||
}
|
||||
const mockFetch = fetch as FetchMock;
|
||||
|
||||
describe("forRemoteCluster", () => {
|
||||
let apiManager: jest.Mocked<ApiManager>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
await di.runSetups();
|
||||
|
||||
apiManager = new ApiManager() as jest.Mocked<ApiManager>;
|
||||
|
||||
di.override(apiManagerInjectable, () => apiManager);
|
||||
});
|
||||
|
||||
it("builds api client for KubeObject", async () => {
|
||||
const api = forRemoteCluster({
|
||||
cluster: {
|
||||
@@ -39,7 +41,7 @@ describe("forRemoteCluster", () => {
|
||||
user: {
|
||||
token: "daa",
|
||||
},
|
||||
}, TestKubeObject);
|
||||
}, Pod);
|
||||
|
||||
expect(api).toBeInstanceOf(KubeApi);
|
||||
});
|
||||
@@ -52,9 +54,9 @@ describe("forRemoteCluster", () => {
|
||||
user: {
|
||||
token: "daa",
|
||||
},
|
||||
}, TestKubeObject, TestKubeApi);
|
||||
}, Pod, PodApi);
|
||||
|
||||
expect(api).toBeInstanceOf(TestKubeApi);
|
||||
expect(api).toBeInstanceOf(PodApi);
|
||||
});
|
||||
|
||||
it("calls right api endpoint", async () => {
|
||||
@@ -65,9 +67,9 @@ describe("forRemoteCluster", () => {
|
||||
user: {
|
||||
token: "daa",
|
||||
},
|
||||
}, TestKubeObject);
|
||||
}, Pod);
|
||||
|
||||
(fetch as any).mockResponse(async (request: any) => {
|
||||
mockFetch.mockResponse(async (request: any) => {
|
||||
expect(request.url).toEqual("https://127.0.0.1:6443/api/v1/pods");
|
||||
|
||||
return {
|
||||
@@ -83,22 +85,31 @@ describe("forRemoteCluster", () => {
|
||||
|
||||
describe("KubeApi", () => {
|
||||
let request: KubeJsonApi;
|
||||
let apiManager: jest.Mocked<ApiManager>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
await di.runSetups();
|
||||
|
||||
beforeEach(() => {
|
||||
request = new KubeJsonApi({
|
||||
serverAddress: `http://127.0.0.1:9999`,
|
||||
apiBase: "/api-kube",
|
||||
});
|
||||
apiManager = new ApiManager() as jest.Mocked<ApiManager>;
|
||||
|
||||
di.override(apiManagerInjectable, () => apiManager);
|
||||
di.inject(autoRegistrationInjectable);
|
||||
});
|
||||
|
||||
it("uses url from apiBase if apiBase contains the resource", async () => {
|
||||
(fetch as any).mockResponse(async (request: any) => {
|
||||
mockFetch.mockResponse(async (request: any) => {
|
||||
if (request.url === "http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1") {
|
||||
return {
|
||||
body: JSON.stringify({
|
||||
resources: [{
|
||||
name: "ingresses",
|
||||
}] as any[],
|
||||
}],
|
||||
}),
|
||||
};
|
||||
} else if (request.url === "http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1") {
|
||||
@@ -107,13 +118,13 @@ describe("KubeApi", () => {
|
||||
body: JSON.stringify({
|
||||
resources: [{
|
||||
name: "ingresses",
|
||||
}] as any[],
|
||||
}],
|
||||
}),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
body: JSON.stringify({
|
||||
resources: [] as any[],
|
||||
resources: [],
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -121,9 +132,9 @@ describe("KubeApi", () => {
|
||||
|
||||
const apiBase = "/apis/networking.k8s.io/v1/ingresses";
|
||||
const fallbackApiBase = "/apis/extensions/v1beta1/ingresses";
|
||||
const kubeApi = new KubeApi({
|
||||
const kubeApi = new IngressApi({
|
||||
request,
|
||||
objectConstructor: KubeObject,
|
||||
objectConstructor: Ingress,
|
||||
apiBase,
|
||||
fallbackApiBases: [fallbackApiBase],
|
||||
checkPreferredVersion: true,
|
||||
@@ -138,11 +149,11 @@ describe("KubeApi", () => {
|
||||
});
|
||||
|
||||
it("uses url from fallbackApiBases if apiBase lacks the resource", async () => {
|
||||
(fetch as any).mockResponse(async (request: any) => {
|
||||
mockFetch.mockResponse(async (request: any) => {
|
||||
if (request.url === "http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1") {
|
||||
return {
|
||||
body: JSON.stringify({
|
||||
resources: [] as any[],
|
||||
resources: [],
|
||||
}),
|
||||
};
|
||||
} else if (request.url === "http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1") {
|
||||
@@ -150,13 +161,13 @@ describe("KubeApi", () => {
|
||||
body: JSON.stringify({
|
||||
resources: [{
|
||||
name: "ingresses",
|
||||
}] as any[],
|
||||
}],
|
||||
}),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
body: JSON.stringify({
|
||||
resources: [] as any[],
|
||||
resources: [],
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -164,9 +175,10 @@ describe("KubeApi", () => {
|
||||
|
||||
const apiBase = "apis/networking.k8s.io/v1/ingresses";
|
||||
const fallbackApiBase = "/apis/extensions/v1beta1/ingresses";
|
||||
const kubeApi = new KubeApi({
|
||||
const kubeApi = new IngressApi({
|
||||
request,
|
||||
objectConstructor: Object.assign(KubeObject, { apiBase }),
|
||||
kind: "Ingress",
|
||||
fallbackApiBases: [fallbackApiBase],
|
||||
checkPreferredVersion: true,
|
||||
});
|
||||
@@ -183,107 +195,100 @@ describe("KubeApi", () => {
|
||||
it("registers with apiManager if checkPreferredVersion changes apiVersionPreferred", async () => {
|
||||
expect.hasAssertions();
|
||||
|
||||
const api = new TestKubeApi({
|
||||
const api = new IngressApi({
|
||||
objectConstructor: Ingress,
|
||||
checkPreferredVersion: true,
|
||||
fallbackApiBases: ["/apis/extensions/v1beta1/ingresses"],
|
||||
request: {
|
||||
get: jest.fn()
|
||||
.mockImplementationOnce((path: string) => {
|
||||
expect(path).toBe("/apis/networking.k8s.io/v1");
|
||||
|
||||
throw new Error("no");
|
||||
})
|
||||
.mockImplementationOnce((path: string) => {
|
||||
expect(path).toBe("/apis/extensions/v1beta1");
|
||||
|
||||
return {
|
||||
resources: [
|
||||
{
|
||||
name: "ingresses",
|
||||
},
|
||||
],
|
||||
};
|
||||
})
|
||||
.mockImplementationOnce((path: string) => {
|
||||
expect(path).toBe("/apis/extensions");
|
||||
|
||||
return {
|
||||
preferredVersion: {
|
||||
version: "v1beta1",
|
||||
},
|
||||
};
|
||||
.mockImplementation((path: string) => {
|
||||
switch (path) {
|
||||
case "/apis/networking.k8s.io/v1":
|
||||
throw new Error("no");
|
||||
case "/apis/extensions/v1beta1":
|
||||
return {
|
||||
resources: [
|
||||
{
|
||||
name: "ingresses",
|
||||
},
|
||||
],
|
||||
};
|
||||
case "/apis/extensions":
|
||||
return {
|
||||
preferredVersion: {
|
||||
version: "v1beta1",
|
||||
},
|
||||
};
|
||||
default:
|
||||
throw new Error("unknown path");
|
||||
}
|
||||
}),
|
||||
} as any,
|
||||
} as Partial<KubeJsonApi> as KubeJsonApi,
|
||||
});
|
||||
|
||||
await api.checkPreferredVersion();
|
||||
await (api as any).checkPreferredVersion();
|
||||
|
||||
expect(api.apiVersionPreferred).toBe("v1beta1");
|
||||
expect(mockApiManager.registerApi).toBeCalledWith("/apis/extensions/v1beta1/ingresses", expect.anything());
|
||||
expect(apiManager.registerApi).toBeCalledWith(api);
|
||||
});
|
||||
|
||||
it("registers with apiManager if checkPreferredVersion changes apiVersionPreferred with non-grouped apis", async () => {
|
||||
expect.hasAssertions();
|
||||
|
||||
const api = new TestKubeApi({
|
||||
const api = new PodApi({
|
||||
objectConstructor: Pod,
|
||||
checkPreferredVersion: true,
|
||||
fallbackApiBases: ["/api/v1beta1/pods"],
|
||||
request: {
|
||||
get: jest.fn()
|
||||
.mockImplementationOnce((path: string) => {
|
||||
expect(path).toBe("/api/v1");
|
||||
|
||||
throw new Error("no");
|
||||
})
|
||||
.mockImplementationOnce((path: string) => {
|
||||
expect(path).toBe("/api/v1beta1");
|
||||
|
||||
return {
|
||||
resources: [
|
||||
{
|
||||
name: "pods",
|
||||
},
|
||||
],
|
||||
};
|
||||
})
|
||||
.mockImplementationOnce((path: string) => {
|
||||
expect(path).toBe("/api");
|
||||
|
||||
return {
|
||||
preferredVersion: {
|
||||
version: "v1beta1",
|
||||
},
|
||||
};
|
||||
.mockImplementation((path: string) => {
|
||||
switch (path) {
|
||||
case "/api/v1":
|
||||
throw new Error("no");
|
||||
case "/api/v1beta1":
|
||||
return {
|
||||
resources: [
|
||||
{
|
||||
name: "pods",
|
||||
},
|
||||
],
|
||||
};
|
||||
case "/api":
|
||||
return {
|
||||
preferredVersion: {
|
||||
version: "v1beta1",
|
||||
},
|
||||
};
|
||||
default:
|
||||
throw new Error("unknown path");
|
||||
}
|
||||
}),
|
||||
} as any,
|
||||
} as Partial<KubeJsonApi> as KubeJsonApi,
|
||||
});
|
||||
|
||||
await api.checkPreferredVersion();
|
||||
await (api as any).checkPreferredVersion();
|
||||
|
||||
expect(api.apiVersionPreferred).toBe("v1beta1");
|
||||
expect(mockApiManager.registerApi).toBeCalledWith("/api/v1beta1/pods", expect.anything());
|
||||
expect(apiManager.registerApi).toBeCalledWith(api);
|
||||
});
|
||||
});
|
||||
|
||||
describe("patch", () => {
|
||||
let api: TestKubeApi;
|
||||
let api: DeploymentApi;
|
||||
|
||||
beforeEach(() => {
|
||||
api = new TestKubeApi({
|
||||
api = new DeploymentApi({
|
||||
request,
|
||||
objectConstructor: TestKubeObject,
|
||||
});
|
||||
});
|
||||
|
||||
it("sends strategic patch by default", async () => {
|
||||
expect.hasAssertions();
|
||||
|
||||
(fetch as any).mockResponse(async (request: Request) => {
|
||||
mockFetch.mockResponse(async request => {
|
||||
expect(request.method).toEqual("PATCH");
|
||||
expect(request.headers.get("content-type")).toMatch("strategic-merge-patch");
|
||||
expect(request.body.toString()).toEqual(JSON.stringify({ spec: { replicas: 2 }}));
|
||||
expect(request.body?.toString()).toEqual(JSON.stringify({ spec: { replicas: 2 }}));
|
||||
|
||||
return {};
|
||||
});
|
||||
@@ -296,10 +301,10 @@ describe("KubeApi", () => {
|
||||
it("allows to use merge patch", async () => {
|
||||
expect.hasAssertions();
|
||||
|
||||
(fetch as any).mockResponse(async (request: Request) => {
|
||||
mockFetch.mockResponse(async request => {
|
||||
expect(request.method).toEqual("PATCH");
|
||||
expect(request.headers.get("content-type")).toMatch("merge-patch");
|
||||
expect(request.body.toString()).toEqual(JSON.stringify({ spec: { replicas: 2 }}));
|
||||
expect(request.body?.toString()).toEqual(JSON.stringify({ spec: { replicas: 2 }}));
|
||||
|
||||
return {};
|
||||
});
|
||||
@@ -312,10 +317,10 @@ describe("KubeApi", () => {
|
||||
it("allows to use json patch", async () => {
|
||||
expect.hasAssertions();
|
||||
|
||||
(fetch as any).mockResponse(async (request: Request) => {
|
||||
mockFetch.mockResponse(async request => {
|
||||
expect(request.method).toEqual("PATCH");
|
||||
expect(request.headers.get("content-type")).toMatch("json-patch");
|
||||
expect(request.body.toString()).toEqual(JSON.stringify([{ op: "replace", path: "/spec/replicas", value: 2 }]));
|
||||
expect(request.body?.toString()).toEqual(JSON.stringify([{ op: "replace", path: "/spec/replicas", value: 2 }]));
|
||||
|
||||
return {};
|
||||
});
|
||||
@@ -328,10 +333,10 @@ describe("KubeApi", () => {
|
||||
it("allows deep partial patch", async () => {
|
||||
expect.hasAssertions();
|
||||
|
||||
(fetch as any).mockResponse(async (request: Request) => {
|
||||
mockFetch.mockResponse(async request => {
|
||||
expect(request.method).toEqual("PATCH");
|
||||
expect(request.headers.get("content-type")).toMatch("merge-patch");
|
||||
expect(request.body.toString()).toEqual(JSON.stringify({ metadata: { annotations: { provisioned: "true" }}}));
|
||||
expect(request.body?.toString()).toEqual(JSON.stringify({ metadata: { annotations: { provisioned: "true" }}}));
|
||||
|
||||
return {};
|
||||
});
|
||||
@@ -345,18 +350,18 @@ describe("KubeApi", () => {
|
||||
});
|
||||
|
||||
describe("delete", () => {
|
||||
let api: TestKubeApi;
|
||||
let api: PodApi;
|
||||
|
||||
beforeEach(() => {
|
||||
api = new TestKubeApi({
|
||||
api = new PodApi({
|
||||
request,
|
||||
objectConstructor: TestKubeObject,
|
||||
objectConstructor: Pod,
|
||||
});
|
||||
});
|
||||
|
||||
it("sends correct request with empty namespace", async () => {
|
||||
expect.hasAssertions();
|
||||
(fetch as any).mockResponse(async (request: Request) => {
|
||||
mockFetch.mockResponse(async request => {
|
||||
expect(request.method).toEqual("DELETE");
|
||||
expect(request.url).toEqual("http://127.0.0.1:9999/api-kube/api/v1/pods/foo?propagationPolicy=Background");
|
||||
|
||||
@@ -368,7 +373,7 @@ describe("KubeApi", () => {
|
||||
|
||||
it("sends correct request without namespace", async () => {
|
||||
expect.hasAssertions();
|
||||
(fetch as any).mockResponse(async (request: Request) => {
|
||||
mockFetch.mockResponse(async request => {
|
||||
expect(request.method).toEqual("DELETE");
|
||||
expect(request.url).toEqual("http://127.0.0.1:9999/api-kube/api/v1/namespaces/default/pods/foo?propagationPolicy=Background");
|
||||
|
||||
@@ -380,7 +385,7 @@ describe("KubeApi", () => {
|
||||
|
||||
it("sends correct request with namespace", async () => {
|
||||
expect.hasAssertions();
|
||||
(fetch as any).mockResponse(async (request: Request) => {
|
||||
mockFetch.mockResponse(async request => {
|
||||
expect(request.method).toEqual("DELETE");
|
||||
expect(request.url).toEqual("http://127.0.0.1:9999/api-kube/api/v1/namespaces/kube-system/pods/foo?propagationPolicy=Background");
|
||||
|
||||
@@ -392,7 +397,7 @@ describe("KubeApi", () => {
|
||||
|
||||
it("allows to change propagationPolicy", async () => {
|
||||
expect.hasAssertions();
|
||||
(fetch as any).mockResponse(async (request: Request) => {
|
||||
mockFetch.mockResponse(async request => {
|
||||
expect(request.method).toEqual("DELETE");
|
||||
expect(request.url).toMatch("propagationPolicy=Orphan");
|
||||
|
||||
@@ -404,13 +409,13 @@ describe("KubeApi", () => {
|
||||
});
|
||||
|
||||
describe("watch", () => {
|
||||
let api: TestKubeApi;
|
||||
let api: PodApi;
|
||||
let stream: PassThrough;
|
||||
|
||||
beforeEach(() => {
|
||||
api = new TestKubeApi({
|
||||
api = new PodApi({
|
||||
request,
|
||||
objectConstructor: TestKubeObject,
|
||||
objectConstructor: Pod,
|
||||
});
|
||||
stream = new PassThrough();
|
||||
});
|
||||
@@ -423,9 +428,10 @@ describe("KubeApi", () => {
|
||||
it("sends a valid watch request", () => {
|
||||
const spy = jest.spyOn(request, "getResponse");
|
||||
|
||||
(fetch as any).mockResponse(async () => {
|
||||
mockFetch.mockResponse(async () => {
|
||||
return {
|
||||
body: stream,
|
||||
// needed for https://github.com/jefflau/jest-fetch-mock/issues/218
|
||||
body: stream as unknown as string,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -436,9 +442,10 @@ describe("KubeApi", () => {
|
||||
it("sends timeout as a query parameter", async () => {
|
||||
const spy = jest.spyOn(request, "getResponse");
|
||||
|
||||
(fetch as any).mockResponse(async () => {
|
||||
mockFetch.mockResponse(async () => {
|
||||
return {
|
||||
body: stream,
|
||||
// needed for https://github.com/jefflau/jest-fetch-mock/issues/218
|
||||
body: stream as unknown as string,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -449,13 +456,14 @@ describe("KubeApi", () => {
|
||||
it("aborts watch using abortController", async (done) => {
|
||||
const spy = jest.spyOn(request, "getResponse");
|
||||
|
||||
(fetch as any).mockResponse(async (request: Request) => {
|
||||
(request as any).signal.addEventListener("abort", () => {
|
||||
mockFetch.mockResponse(async request => {
|
||||
request.signal.addEventListener("abort", () => {
|
||||
done();
|
||||
});
|
||||
|
||||
return {
|
||||
body: stream,
|
||||
// needed for https://github.com/jefflau/jest-fetch-mock/issues/218
|
||||
body: stream as unknown as string,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -478,9 +486,9 @@ describe("KubeApi", () => {
|
||||
it("if request ended", (done) => {
|
||||
const spy = jest.spyOn(request, "getResponse");
|
||||
|
||||
jest.spyOn(stream, "on").mockImplementation((eventName: string, callback: Function) => {
|
||||
jest.spyOn(stream, "on").mockImplementation((event: string | symbol, callback: Function) => {
|
||||
// End the request in 100ms.
|
||||
if (eventName === "end") {
|
||||
if (event === "end") {
|
||||
setTimeout(() => {
|
||||
callback();
|
||||
}, 100);
|
||||
@@ -493,8 +501,8 @@ describe("KubeApi", () => {
|
||||
jest.spyOn(global, "fetch").mockImplementation(async () => {
|
||||
return {
|
||||
ok: true,
|
||||
body: stream,
|
||||
} as any;
|
||||
body: stream as never,
|
||||
} as Partial<Response> as Response;
|
||||
});
|
||||
|
||||
api.watch({
|
||||
@@ -512,9 +520,10 @@ describe("KubeApi", () => {
|
||||
it("if request not closed after timeout", (done) => {
|
||||
const spy = jest.spyOn(request, "getResponse");
|
||||
|
||||
(fetch as any).mockResponse(async () => {
|
||||
mockFetch.mockResponse(async () => {
|
||||
return {
|
||||
body: stream,
|
||||
// needed for https://github.com/jefflau/jest-fetch-mock/issues/218
|
||||
body: stream as unknown as string,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -536,9 +545,9 @@ describe("KubeApi", () => {
|
||||
it("retries only once if request ends and timeout is set", (done) => {
|
||||
const spy = jest.spyOn(request, "getResponse");
|
||||
|
||||
jest.spyOn(stream, "on").mockImplementation((eventName: string, callback: Function) => {
|
||||
jest.spyOn(stream, "on").mockImplementation((event: string | symbol, callback: Function) => {
|
||||
// End the request in 100ms.
|
||||
if (eventName === "end") {
|
||||
if (event === "end") {
|
||||
setTimeout(() => {
|
||||
callback();
|
||||
}, 100);
|
||||
@@ -551,8 +560,8 @@ describe("KubeApi", () => {
|
||||
jest.spyOn(global, "fetch").mockImplementation(async () => {
|
||||
return {
|
||||
ok: true,
|
||||
body: stream,
|
||||
} as any;
|
||||
body: stream as never,
|
||||
} as Partial<Response> as Response;
|
||||
});
|
||||
|
||||
const timeoutSeconds = 0.5;
|
||||
@@ -577,21 +586,21 @@ describe("KubeApi", () => {
|
||||
});
|
||||
|
||||
describe("create", () => {
|
||||
let api: TestKubeApi;
|
||||
let api: PodApi;
|
||||
|
||||
beforeEach(() => {
|
||||
api = new TestKubeApi({
|
||||
api = new PodApi({
|
||||
request,
|
||||
objectConstructor: TestKubeObject,
|
||||
objectConstructor: Pod,
|
||||
});
|
||||
});
|
||||
|
||||
it("should add kind and apiVersion", async () => {
|
||||
expect.hasAssertions();
|
||||
|
||||
(fetch as any).mockResponse(async (request: Request) => {
|
||||
mockFetch.mockResponse(async request => {
|
||||
expect(request.method).toEqual("POST");
|
||||
expect(JSON.parse(request.body.toString())).toEqual({
|
||||
expect(JSON.parse(String(request.body))).toEqual({
|
||||
kind: "Pod",
|
||||
apiVersion: "v1",
|
||||
metadata: {
|
||||
@@ -643,9 +652,9 @@ describe("KubeApi", () => {
|
||||
it("doesn't override metadata.labels", async () => {
|
||||
expect.hasAssertions();
|
||||
|
||||
(fetch as any).mockResponse(async (request: Request) => {
|
||||
mockFetch.mockResponse(async request => {
|
||||
expect(request.method).toEqual("POST");
|
||||
expect(JSON.parse(request.body.toString())).toEqual({
|
||||
expect(JSON.parse(String(request.body))).toEqual({
|
||||
kind: "Pod",
|
||||
apiVersion: "v1",
|
||||
metadata: {
|
||||
@@ -674,21 +683,21 @@ describe("KubeApi", () => {
|
||||
});
|
||||
|
||||
describe("update", () => {
|
||||
let api: TestKubeApi;
|
||||
let api: PodApi;
|
||||
|
||||
beforeEach(() => {
|
||||
api = new TestKubeApi({
|
||||
api = new PodApi({
|
||||
request,
|
||||
objectConstructor: TestKubeObject,
|
||||
objectConstructor: Pod,
|
||||
});
|
||||
});
|
||||
|
||||
it("doesn't override metadata.labels", async () => {
|
||||
expect.hasAssertions();
|
||||
|
||||
(fetch as any).mockResponse(async (request: Request) => {
|
||||
mockFetch.mockResponse(async request => {
|
||||
expect(request.method).toEqual("PUT");
|
||||
expect(JSON.parse(request.body.toString())).toEqual({
|
||||
expect(JSON.parse(String(request.body))).toEqual({
|
||||
metadata: {
|
||||
name: "foobar",
|
||||
namespace: "default",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { Cluster } from "../../cluster/cluster";
|
||||
import type { ClusterContext } from "../cluster-context";
|
||||
import type { KubeApi } from "../kube-api";
|
||||
import { KubeObject } from "../kube-object";
|
||||
@@ -14,6 +15,7 @@ class FakeKubeObjectStore extends KubeObjectStore<KubeObject> {
|
||||
allNamespaces: [],
|
||||
contextNamespaces: [],
|
||||
hasSelectedAll: false,
|
||||
cluster: {} as Cluster,
|
||||
} as ClusterContext;
|
||||
|
||||
get context() {
|
||||
@@ -40,6 +42,7 @@ describe("KubeObjectStore", () => {
|
||||
resourceVersion: "1",
|
||||
uid: "some-uid",
|
||||
namespace: "default",
|
||||
selfLink: "/some/self/link",
|
||||
},
|
||||
});
|
||||
const store = new FakeKubeObjectStore(loadItems, {
|
||||
@@ -73,6 +76,7 @@ describe("KubeObjectStore", () => {
|
||||
resourceVersion: "1",
|
||||
uid: "some-uid",
|
||||
namespace: "default",
|
||||
selfLink: "/some/self/link",
|
||||
},
|
||||
});
|
||||
const objNotInDefaultNamespace = new KubeObject({
|
||||
@@ -83,6 +87,7 @@ describe("KubeObjectStore", () => {
|
||||
resourceVersion: "1",
|
||||
uid: "some-uid",
|
||||
namespace: "not-default",
|
||||
selfLink: "/some/self/link",
|
||||
},
|
||||
});
|
||||
const store = new FakeKubeObjectStore(loadItems, {
|
||||
@@ -115,6 +120,7 @@ describe("KubeObjectStore", () => {
|
||||
name: "some-obj-name",
|
||||
resourceVersion: "1",
|
||||
uid: "some-uid",
|
||||
selfLink: "/some/self/link",
|
||||
},
|
||||
});
|
||||
const clusterScopedObject2 = new KubeObject({
|
||||
@@ -125,6 +131,7 @@ describe("KubeObjectStore", () => {
|
||||
resourceVersion: "1",
|
||||
uid: "some-uid",
|
||||
namespace: "not-default",
|
||||
selfLink: "/some/self/link",
|
||||
},
|
||||
});
|
||||
const store = new FakeKubeObjectStore(loadItems, {
|
||||
|
||||
@@ -18,6 +18,7 @@ describe("Nodes tests", () => {
|
||||
name: "bar",
|
||||
resourceVersion: "1",
|
||||
uid: "bat",
|
||||
selfLink: "/api/v1/nodes/bar",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -33,6 +34,7 @@ describe("Nodes tests", () => {
|
||||
resourceVersion: "1",
|
||||
uid: "bat",
|
||||
labels: {},
|
||||
selfLink: "/api/v1/nodes/bar",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -51,6 +53,7 @@ describe("Nodes tests", () => {
|
||||
"node-role.kubernetes.io/foobar": "bat",
|
||||
"hellonode-role.kubernetes.io/foobar1": "bat",
|
||||
},
|
||||
selfLink: "/api/v1/nodes/bar",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -69,6 +72,7 @@ describe("Nodes tests", () => {
|
||||
"node-role.kubernetes.io/foobar": "bat",
|
||||
"hellonode-role.kubernetes.io//////foobar1": "bat",
|
||||
},
|
||||
selfLink: "/api/v1/nodes/bar",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -86,6 +90,7 @@ describe("Nodes tests", () => {
|
||||
labels: {
|
||||
"kubernetes.io/role": "master",
|
||||
},
|
||||
selfLink: "/api/v1/nodes/bar",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -103,6 +108,7 @@ describe("Nodes tests", () => {
|
||||
labels: {
|
||||
"node.kubernetes.io/role": "master",
|
||||
},
|
||||
selfLink: "/api/v1/nodes/bar",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -122,6 +128,7 @@ describe("Nodes tests", () => {
|
||||
"kubernetes.io/role": "master",
|
||||
"node.kubernetes.io/role": "master-v2-max",
|
||||
},
|
||||
selfLink: "/api/v1/nodes/bar",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ describe("Pod tests", () => {
|
||||
name: "foobar",
|
||||
resourceVersion: "foobar",
|
||||
uid: "foobar",
|
||||
namespace: "default",
|
||||
selfLink: "/api/v1/pods/default/foobar",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import assert from "assert";
|
||||
import type { PodContainer, PodContainerStatus } from "../endpoints";
|
||||
import { Pod } from "../endpoints";
|
||||
|
||||
interface GetDummyPodOptions {
|
||||
@@ -12,16 +14,18 @@ interface GetDummyPodOptions {
|
||||
initDead?: number;
|
||||
}
|
||||
|
||||
function getDummyPodDefaultOptions(): Required<GetDummyPodOptions> {
|
||||
return {
|
||||
running: 0,
|
||||
dead: 0,
|
||||
initDead: 0,
|
||||
initRunning: 0,
|
||||
};
|
||||
}
|
||||
function getDummyPod(rawOpts: GetDummyPodOptions = {}): Pod {
|
||||
const {
|
||||
running = 0,
|
||||
dead = 0,
|
||||
initDead = 0,
|
||||
initRunning = 0,
|
||||
} = rawOpts;
|
||||
|
||||
function getDummyPod(opts: GetDummyPodOptions = getDummyPodDefaultOptions()): Pod {
|
||||
const containers: PodContainer[] = [];
|
||||
const initContainers: PodContainer[] = [];
|
||||
const containerStatuses: PodContainerStatus[] = [];
|
||||
const initContainerStatuses: PodContainerStatus[] = [];
|
||||
const pod = new Pod({
|
||||
apiVersion: "v1",
|
||||
kind: "Pod",
|
||||
@@ -29,36 +33,35 @@ function getDummyPod(opts: GetDummyPodOptions = getDummyPodDefaultOptions()): Po
|
||||
uid: "1",
|
||||
name: "test",
|
||||
resourceVersion: "v1",
|
||||
selfLink: "http",
|
||||
namespace: "default",
|
||||
selfLink: "/api/v1/pods/default/test",
|
||||
},
|
||||
spec: {
|
||||
containers,
|
||||
initContainers,
|
||||
serviceAccount: "dummy",
|
||||
serviceAccountName: "dummy",
|
||||
},
|
||||
status: {
|
||||
phase: "Running",
|
||||
conditions: [],
|
||||
hostIP: "10.0.0.1",
|
||||
podIP: "10.0.0.1",
|
||||
startTime: "now",
|
||||
containerStatuses,
|
||||
initContainerStatuses,
|
||||
},
|
||||
});
|
||||
|
||||
pod.spec = {
|
||||
containers: [],
|
||||
initContainers: [],
|
||||
serviceAccount: "dummy",
|
||||
serviceAccountName: "dummy",
|
||||
};
|
||||
for (let i = 0; i < running; i += 1) {
|
||||
const name = `container_running_${i}`;
|
||||
|
||||
pod.status = {
|
||||
phase: "Running",
|
||||
conditions: [],
|
||||
hostIP: "10.0.0.1",
|
||||
podIP: "10.0.0.1",
|
||||
startTime: "now",
|
||||
containerStatuses: [],
|
||||
initContainerStatuses: [],
|
||||
};
|
||||
|
||||
for (let i = 0; i < opts.running; i += 1) {
|
||||
const name = `container_r_${i}`;
|
||||
|
||||
pod.spec.containers.push({
|
||||
containers.push({
|
||||
image: "dummy",
|
||||
imagePullPolicy: "dummy",
|
||||
name,
|
||||
});
|
||||
pod.status.containerStatuses.push({
|
||||
containerStatuses.push({
|
||||
image: "dummy",
|
||||
imageID: "dummy",
|
||||
name,
|
||||
@@ -72,15 +75,15 @@ function getDummyPod(opts: GetDummyPodOptions = getDummyPodDefaultOptions()): Po
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = 0; i < opts.dead; i += 1) {
|
||||
const name = `container_d_${i}`;
|
||||
for (let i = 0; i < dead; i += 1) {
|
||||
const name = `container_dead_${i}`;
|
||||
|
||||
pod.spec.containers.push({
|
||||
containers.push({
|
||||
image: "dummy",
|
||||
imagePullPolicy: "dummy",
|
||||
name,
|
||||
});
|
||||
pod.status.containerStatuses.push({
|
||||
containerStatuses.push({
|
||||
image: "dummy",
|
||||
imageID: "dummy",
|
||||
name,
|
||||
@@ -97,15 +100,15 @@ function getDummyPod(opts: GetDummyPodOptions = getDummyPodDefaultOptions()): Po
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = 0; i < opts.initRunning; i += 1) {
|
||||
const name = `container_ir_${i}`;
|
||||
for (let i = 0; i < initRunning; i += 1) {
|
||||
const name = `container_init-running_${i}`;
|
||||
|
||||
pod.spec.initContainers.push({
|
||||
initContainers.push({
|
||||
image: "dummy",
|
||||
imagePullPolicy: "dummy",
|
||||
name,
|
||||
});
|
||||
pod.status.initContainerStatuses.push({
|
||||
initContainerStatuses.push({
|
||||
image: "dummy",
|
||||
imageID: "dummy",
|
||||
name,
|
||||
@@ -119,15 +122,15 @@ function getDummyPod(opts: GetDummyPodOptions = getDummyPodDefaultOptions()): Po
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = 0; i < opts.initDead; i += 1) {
|
||||
const name = `container_id_${i}`;
|
||||
for (let i = 0; i < initDead; i += 1) {
|
||||
const name = `container_init-dead_${i}`;
|
||||
|
||||
pod.spec.initContainers.push({
|
||||
initContainers.push({
|
||||
image: "dummy",
|
||||
imagePullPolicy: "dummy",
|
||||
name,
|
||||
});
|
||||
pod.status.initContainerStatuses.push({
|
||||
initContainerStatuses.push({
|
||||
image: "dummy",
|
||||
imageID: "dummy",
|
||||
name,
|
||||
@@ -173,8 +176,8 @@ describe("Pods", () => {
|
||||
|
||||
it("getRunningContainers should return only running and init running", () => {
|
||||
const res = [
|
||||
...Array.from(new Array(running), (val, index) => getNamedContainer(`container_r_${index}`)),
|
||||
...Array.from(new Array(initRunning), (val, index) => getNamedContainer(`container_ir_${index}`)),
|
||||
...Array.from(new Array(running), (val, index) => getNamedContainer(`container_running_${index}`)),
|
||||
...Array.from(new Array(initRunning), (val, index) => getNamedContainer(`container_init-running_${index}`)),
|
||||
];
|
||||
|
||||
expect(pod.getRunningContainers()).toStrictEqual(res);
|
||||
@@ -182,10 +185,10 @@ describe("Pods", () => {
|
||||
|
||||
it("getAllContainers should return all containers", () => {
|
||||
const res = [
|
||||
...Array.from(new Array(running), (val, index) => getNamedContainer(`container_r_${index}`)),
|
||||
...Array.from(new Array(dead), (val, index) => getNamedContainer(`container_d_${index}`)),
|
||||
...Array.from(new Array(initRunning), (val, index) => getNamedContainer(`container_ir_${index}`)),
|
||||
...Array.from(new Array(initDead), (val, index) => getNamedContainer(`container_id_${index}`)),
|
||||
...Array.from(new Array(running), (val, index) => getNamedContainer(`container_running_${index}`)),
|
||||
...Array.from(new Array(dead), (val, index) => getNamedContainer(`container_dead_${index}`)),
|
||||
...Array.from(new Array(initRunning), (val, index) => getNamedContainer(`container_init-running_${index}`)),
|
||||
...Array.from(new Array(initDead), (val, index) => getNamedContainer(`container_init-dead_${index}`)),
|
||||
];
|
||||
|
||||
expect(pod.getAllContainers()).toStrictEqual(res);
|
||||
@@ -253,7 +256,7 @@ describe("Pods", () => {
|
||||
it("should return true if a condition isn't ready", () => {
|
||||
const pod = getDummyPod({ running: 1 });
|
||||
|
||||
pod.status.conditions.push({
|
||||
pod.status?.conditions.push({
|
||||
type: "Ready",
|
||||
status: "foobar",
|
||||
lastProbeTime: 1,
|
||||
@@ -266,7 +269,7 @@ describe("Pods", () => {
|
||||
it("should return false if a condition is non-ready", () => {
|
||||
const pod = getDummyPod({ running: 1 });
|
||||
|
||||
pod.status.conditions.push({
|
||||
pod.status?.conditions.push({
|
||||
type: "dummy",
|
||||
status: "foobar",
|
||||
lastProbeTime: 1,
|
||||
@@ -278,8 +281,11 @@ describe("Pods", () => {
|
||||
|
||||
it("should return true if a current container is in a crash loop back off", () => {
|
||||
const pod = getDummyPod({ running: 1 });
|
||||
const firstStatus = pod.status?.containerStatuses?.[0];
|
||||
|
||||
pod.status.containerStatuses[0].state = {
|
||||
assert(firstStatus);
|
||||
|
||||
firstStatus.state = {
|
||||
waiting: {
|
||||
reason: "CrashLookBackOff",
|
||||
message: "too much foobar",
|
||||
@@ -292,6 +298,8 @@ describe("Pods", () => {
|
||||
it("should return true if a current phase isn't running", () => {
|
||||
const pod = getDummyPod({ running: 1 });
|
||||
|
||||
assert(pod.status);
|
||||
|
||||
pod.status.phase = "not running";
|
||||
|
||||
expect(pod.hasIssues()).toStrictEqual(true);
|
||||
@@ -300,6 +308,8 @@ describe("Pods", () => {
|
||||
it("should return false if a current phase is running", () => {
|
||||
const pod = getDummyPod({ running: 1 });
|
||||
|
||||
assert(pod.status);
|
||||
|
||||
pod.status.phase = "Running";
|
||||
|
||||
expect(pod.hasIssues()).toStrictEqual(false);
|
||||
|
||||
@@ -3,31 +3,39 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { StatefulSet, StatefulSetApi } from "../endpoints/stateful-set.api";
|
||||
import storesAndApisCanBeCreatedInjectable from "../../../renderer/stores-apis-can-be-created.injectable";
|
||||
import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting";
|
||||
import apiKubeInjectable from "../../../renderer/k8s/api-kube.injectable";
|
||||
import type { StatefulSetApi } from "../endpoints";
|
||||
import statefulSetApiInjectable from "../endpoints/stateful-set.api.injectable";
|
||||
import type { KubeJsonApi } from "../kube-json-api";
|
||||
|
||||
class StatefulSetApiTest extends StatefulSetApi {
|
||||
public setRequest(request: any) {
|
||||
this.request = request;
|
||||
}
|
||||
}
|
||||
|
||||
describe("StatefulSetApi", () => {
|
||||
let statefulSetApi: StatefulSetApi;
|
||||
let kubeJsonApi: jest.Mocked<KubeJsonApi>;
|
||||
|
||||
beforeEach(() => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
di.override(storesAndApisCanBeCreatedInjectable, () => true);
|
||||
kubeJsonApi = {
|
||||
getResponse: jest.fn(),
|
||||
get: jest.fn(),
|
||||
post: jest.fn(),
|
||||
put: jest.fn(),
|
||||
patch: jest.fn(),
|
||||
del: jest.fn(),
|
||||
} as never;
|
||||
di.override(apiKubeInjectable, () => kubeJsonApi);
|
||||
|
||||
statefulSetApi = di.inject(statefulSetApiInjectable);
|
||||
});
|
||||
|
||||
describe("scale", () => {
|
||||
const requestMock = {
|
||||
patch: () => ({}),
|
||||
} as unknown as KubeJsonApi;
|
||||
|
||||
const sub = new StatefulSetApiTest({ objectConstructor: StatefulSet });
|
||||
|
||||
sub.setRequest(requestMock);
|
||||
|
||||
it("requests Kubernetes API with PATCH verb and correct amount of replicas", () => {
|
||||
const patchSpy = jest.spyOn(requestMock, "patch");
|
||||
statefulSetApi.scale({ namespace: "default", name: "statefulset-1" }, 5);
|
||||
|
||||
sub.scale({ namespace: "default", name: "statefulset-1" }, 5);
|
||||
|
||||
expect(patchSpy).toHaveBeenCalledWith("/apis/apps/v1/namespaces/default/statefulsets/statefulset-1/scale", {
|
||||
expect(kubeJsonApi.patch).toHaveBeenCalledWith("/apis/apps/v1/namespaces/default/statefulsets/statefulset-1/scale", {
|
||||
data: {
|
||||
spec: {
|
||||
replicas: 5,
|
||||
|
||||
@@ -3,18 +3,12 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { isClusterPageContext } from "../utils";
|
||||
import { KubeJsonApi } from "./kube-json-api";
|
||||
import { apiKubePrefix, isDevelopment } from "../vars";
|
||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||
import { asLegacyGlobalForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
||||
import type { KubeJsonApi } from "./kube-json-api";
|
||||
|
||||
export const apiKube = isClusterPageContext()
|
||||
? new KubeJsonApi({
|
||||
serverAddress: `http://127.0.0.1:${window.location.port}`,
|
||||
apiBase: apiKubePrefix,
|
||||
debug: isDevelopment,
|
||||
}, {
|
||||
headers: {
|
||||
"Host": window.location.host,
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
export const apiKubeInjectionToken = getInjectionToken<KubeJsonApi>({
|
||||
id: "api-kube-injection-token",
|
||||
});
|
||||
|
||||
export const apiKube = asLegacyGlobalForExtensionApi(apiKubeInjectionToken);
|
||||
@@ -1,123 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { KubeObjectStore } from "./kube-object.store";
|
||||
|
||||
import { action, observable, makeObservable } from "mobx";
|
||||
import { autoBind, iter } from "../utils";
|
||||
import type { KubeApi } from "./kube-api";
|
||||
import type { KubeObject } from "./kube-object";
|
||||
import type { IKubeObjectRef } from "./kube-api-parse";
|
||||
import { parseKubeApi, createKubeApiURL } from "./kube-api-parse";
|
||||
|
||||
export class ApiManager {
|
||||
private apis = observable.map<string, KubeApi<KubeObject>>();
|
||||
private stores = observable.map<string, KubeObjectStore<KubeObject>>();
|
||||
|
||||
constructor() {
|
||||
makeObservable(this);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
getApi(pathOrCallback: string | ((api: KubeApi<KubeObject>) => boolean)) {
|
||||
if (typeof pathOrCallback === "string") {
|
||||
return this.apis.get(pathOrCallback) || this.apis.get(parseKubeApi(pathOrCallback).apiBase);
|
||||
}
|
||||
|
||||
return iter.find(this.apis.values(), pathOrCallback ?? (() => true));
|
||||
}
|
||||
|
||||
getApiByKind(kind: string, apiVersion: string) {
|
||||
return iter.find(this.apis.values(), api => api.kind === kind && api.apiVersionWithGroup === apiVersion);
|
||||
}
|
||||
|
||||
registerApi<K extends KubeObject>(apiBase: string, api: KubeApi<K>) {
|
||||
if (!api.apiBase) return;
|
||||
|
||||
if (!this.apis.has(apiBase)) {
|
||||
this.stores.forEach((store) => {
|
||||
if (store.api === api) {
|
||||
this.stores.set(apiBase, store);
|
||||
}
|
||||
});
|
||||
|
||||
this.apis.set(apiBase, api);
|
||||
}
|
||||
}
|
||||
|
||||
protected resolveApi(api?: string | KubeApi<KubeObject>): KubeApi<KubeObject> | undefined {
|
||||
if (!api) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (typeof api === "string") {
|
||||
return this.getApi(api) as KubeApi<KubeObject>;
|
||||
}
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
unregisterApi(api: string | KubeApi<KubeObject>) {
|
||||
if (typeof api === "string") this.apis.delete(api);
|
||||
else {
|
||||
const apis = Array.from(this.apis.entries());
|
||||
const entry = apis.find(entry => entry[1] === api);
|
||||
|
||||
if (entry) this.unregisterApi(entry[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
registerStore<K extends KubeObject>(store: KubeObjectStore<K>, apis: KubeApi<K>[] = [store.api]) {
|
||||
apis.filter(Boolean).forEach(api => {
|
||||
if (api.apiBase) this.stores.set(api.apiBase, store);
|
||||
});
|
||||
}
|
||||
|
||||
getStore<S extends KubeObjectStore<KubeObject>>(api: string | KubeApi<KubeObject>): S | undefined {
|
||||
return this.stores.get(this.resolveApi(api)?.apiBase) as S;
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { KubeObjectStore } from "../kube-object.store";
|
||||
|
||||
import { action, observable, makeObservable } from "mobx";
|
||||
import { autoBind, isDefined, iter } from "../../utils";
|
||||
import type { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiDataFor, KubeObject, ObjectReference } from "../kube-object";
|
||||
import { parseKubeApi, createKubeApiURL } from "../kube-api-parse";
|
||||
|
||||
export type RegisterableStore<Store> = Store extends KubeObjectStore<any, any, any>
|
||||
? Store
|
||||
: never;
|
||||
export type RegisterableApi<Api> = Api extends KubeApi<any, any>
|
||||
? Api
|
||||
: never;
|
||||
export type KubeObjectStoreFrom<Api> = Api extends KubeApi<infer KubeObj, infer ApiData>
|
||||
? KubeObjectStore<KubeObj, Api, ApiData>
|
||||
: never;
|
||||
|
||||
export class ApiManager {
|
||||
private readonly apis = observable.map<string, KubeApi>();
|
||||
private readonly stores = observable.map<string, KubeObjectStore>();
|
||||
|
||||
constructor() {
|
||||
makeObservable(this);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
getApi(pathOrCallback: string | ((api: KubeApi<KubeObject>) => boolean)) {
|
||||
if (typeof pathOrCallback === "string") {
|
||||
return this.apis.get(pathOrCallback) || this.apis.get(parseKubeApi(pathOrCallback).apiBase);
|
||||
}
|
||||
|
||||
return iter.find(this.apis.values(), pathOrCallback ?? (() => true));
|
||||
}
|
||||
|
||||
getApiByKind(kind: string, apiVersion: string) {
|
||||
return iter.find(this.apis.values(), api => api.kind === kind && api.apiVersionWithGroup === apiVersion);
|
||||
}
|
||||
|
||||
registerApi<Api>(api: RegisterableApi<Api>): void;
|
||||
/**
|
||||
* @deprecated Just register the `api` by itself
|
||||
*/
|
||||
registerApi<Api>(apiBase: string, api: RegisterableApi<Api>): void;
|
||||
registerApi<Api>(apiBaseRaw: string | RegisterableApi<Api>, apiRaw?: RegisterableApi<Api>) {
|
||||
const api = typeof apiBaseRaw === "string"
|
||||
? apiRaw
|
||||
: apiBaseRaw;
|
||||
|
||||
if (!api?.apiBase) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.apis.has(api.apiBase)) {
|
||||
this.stores.forEach((store) => {
|
||||
if (store.api === api) {
|
||||
this.stores.set(api.apiBase, store);
|
||||
}
|
||||
});
|
||||
|
||||
this.apis.set(api.apiBase, api);
|
||||
}
|
||||
}
|
||||
|
||||
protected resolveApi(api: undefined | string | KubeApi): KubeApi | undefined {
|
||||
if (!api) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (typeof api === "string") {
|
||||
return this.getApi(api);
|
||||
}
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
unregisterApi(api: string | KubeApi<KubeObject>) {
|
||||
if (typeof api === "string") this.apis.delete(api);
|
||||
else {
|
||||
const apis = Array.from(this.apis.entries());
|
||||
const entry = apis.find(entry => entry[1] === api);
|
||||
|
||||
if (entry) this.unregisterApi(entry[0]);
|
||||
}
|
||||
}
|
||||
|
||||
registerStore<KubeObj>(store: RegisterableStore<KubeObj>): void;
|
||||
/**
|
||||
* @deprecated KubeObjectStore's should only every be about a single KubeApi type
|
||||
*/
|
||||
registerStore<KubeObj extends KubeObject>(store: KubeObjectStore<KubeObj, KubeApi<KubeObj>, KubeJsonApiDataFor<KubeObj>>, apis: KubeApi<KubeObj>[]): void;
|
||||
|
||||
@action
|
||||
registerStore<KubeObj extends KubeObject>(store: KubeObjectStore<KubeObj, KubeApi<KubeObj>, KubeJsonApiDataFor<KubeObj>>, apis: KubeApi<KubeObj>[] = [store.api]): void {
|
||||
for (const api of apis.filter(isDefined)) {
|
||||
if (api.apiBase) {
|
||||
this.stores.set(api.apiBase, store as never);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getStore(api: string | undefined): KubeObjectStore | undefined;
|
||||
getStore<Api>(api: RegisterableApi<Api>): KubeObjectStoreFrom<Api> | undefined;
|
||||
/**
|
||||
* @deprecated use an actual cast instead of hiding it with this unused type param
|
||||
*/
|
||||
getStore<Store extends KubeObjectStore>(api: string | KubeApi): Store | undefined ;
|
||||
getStore(api: string | KubeApi | undefined): KubeObjectStore | undefined {
|
||||
const { apiBase } = this.resolveApi(api) ?? {};
|
||||
|
||||
if (apiBase) {
|
||||
return this.stores.get(apiBase);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
lookupApiLink(ref: ObjectReference, parentObject?: KubeObject): string {
|
||||
const {
|
||||
kind, apiVersion = "v1", 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 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import EventEmitter from "events";
|
||||
import type TypedEventEmitter from "typed-emitter";
|
||||
import type { CustomResourceDefinition } from "../endpoints";
|
||||
import type { KubeApi } from "../kube-api";
|
||||
|
||||
export interface LegacyAutoRegistration {
|
||||
customResourceDefinition: (crd: CustomResourceDefinition) => void;
|
||||
kubeApi: (api: KubeApi<any, any>) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used to remove dependency cycles from auto registering of instances
|
||||
*
|
||||
* - Custom Resource Definitions get their own registered store (will need in the future)
|
||||
* - All KubeApi's get auto registered (this should be changed in the future)
|
||||
*/
|
||||
const autoRegistrationEmitterInjectable = getInjectable({
|
||||
id: "auto-registration-emitter",
|
||||
instantiate: (): TypedEventEmitter<LegacyAutoRegistration> => new EventEmitter(),
|
||||
});
|
||||
|
||||
export default autoRegistrationEmitterInjectable;
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { CustomResourceDefinition } from "../endpoints";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import autoRegistrationEmitterInjectable from "./auto-registration-emitter.injectable";
|
||||
import apiManagerInjectable from "./manager.injectable";
|
||||
import { CustomResourceStore } from "./resource.store";
|
||||
|
||||
const autoRegistrationInjectable = getInjectable({
|
||||
id: "api-manager-auto-registration",
|
||||
instantiate: (di) => {
|
||||
const autoRegistrationEmitter = di.inject(autoRegistrationEmitterInjectable);
|
||||
const beforeApiManagerInitializationCrds: CustomResourceDefinition[] = [];
|
||||
const beforeApiManagerInitializationApis: KubeApi[] = [];
|
||||
let initialized = false;
|
||||
|
||||
const autoInitCustomResourceStore = (crd: CustomResourceDefinition) => {
|
||||
const objectConstructor = class extends KubeObject {
|
||||
static readonly kind = crd.getResourceKind();
|
||||
static readonly namespaced = crd.isNamespaced();
|
||||
static readonly apiBase = crd.getResourceApiBase();
|
||||
};
|
||||
|
||||
const api = (() => {
|
||||
const rawApi = apiManager.getApi(objectConstructor.apiBase);
|
||||
|
||||
if (rawApi) {
|
||||
return rawApi;
|
||||
}
|
||||
|
||||
const api = new KubeApi({ objectConstructor });
|
||||
|
||||
apiManager.registerApi(api);
|
||||
|
||||
return api;
|
||||
})();
|
||||
|
||||
if (!apiManager.getStore(api)) {
|
||||
apiManager.registerStore(new CustomResourceStore(api));
|
||||
}
|
||||
};
|
||||
const autoInitKubeApi = (api: KubeApi) => {
|
||||
apiManager.registerApi(api);
|
||||
};
|
||||
|
||||
autoRegistrationEmitter
|
||||
.on("customResourceDefinition", (crd) => {
|
||||
if (initialized) {
|
||||
autoInitCustomResourceStore(crd);
|
||||
} else {
|
||||
beforeApiManagerInitializationCrds.push(crd);
|
||||
}
|
||||
})
|
||||
.on("kubeApi", (api) => {
|
||||
if (initialized) {
|
||||
autoInitKubeApi(api);
|
||||
} else {
|
||||
beforeApiManagerInitializationApis.push(api);
|
||||
}
|
||||
});
|
||||
|
||||
const apiManager = di.inject(apiManagerInjectable);
|
||||
|
||||
beforeApiManagerInitializationCrds.forEach(autoInitCustomResourceStore);
|
||||
beforeApiManagerInitializationApis.forEach(autoInitKubeApi);
|
||||
initialized = true;
|
||||
},
|
||||
});
|
||||
|
||||
export default autoRegistrationInjectable;
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
export * from "./api-manager";
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import type { KubeObjectStore } from "../kube-object.store";
|
||||
import { ApiManager } from "./api-manager";
|
||||
|
||||
export const kubeObjectStoreInjectionToken = getInjectionToken<KubeObjectStore<any, any, any>>({
|
||||
id: "kube-object-store-token",
|
||||
});
|
||||
|
||||
const apiManagerInjectable = getInjectable({
|
||||
id: "api-manager",
|
||||
instantiate: (di) => {
|
||||
const apiManager = new ApiManager();
|
||||
|
||||
if (di.inject(storesAndApisCanBeCreatedInjectionToken)) {
|
||||
const stores = di.injectMany(kubeObjectStoreInjectionToken);
|
||||
|
||||
for (const store of stores) {
|
||||
apiManager.registerStore(store);
|
||||
}
|
||||
}
|
||||
|
||||
return apiManager;
|
||||
},
|
||||
});
|
||||
|
||||
export default apiManagerInjectable;
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { KubeApi } from "../kube-api";
|
||||
import { KubeObjectStore } from "../kube-object.store";
|
||||
import type { KubeObject } from "../kube-object";
|
||||
|
||||
export class CustomResourceStore<K extends KubeObject> extends KubeObjectStore<K, KubeApi<K>> {
|
||||
constructor(api: KubeApi<K>) {
|
||||
super(api);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
import type { Cluster } from "../cluster/cluster";
|
||||
|
||||
export interface ClusterContext {
|
||||
cluster?: Cluster;
|
||||
cluster: Cluster;
|
||||
allNamespaces: string[]; // available / allowed namespaces from cluster.ts
|
||||
contextNamespaces: string[]; // selected by user (see: namespace-select.tsx)
|
||||
hasSelectedAll: boolean;
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { ClusterRoleBindingApi } from "./cluster-role-binding.api";
|
||||
|
||||
const clusterRoleBindingApiInjectable = getInjectable({
|
||||
id: "cluster-role-binding-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "clusterRoleBindingApi is only accessible in certain environments");
|
||||
|
||||
return new ClusterRoleBindingApi();
|
||||
},
|
||||
});
|
||||
|
||||
export default clusterRoleBindingApiInjectable;
|
||||
@@ -2,35 +2,39 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import type { DerivedKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import type { KubeObjectMetadata, KubeObjectScope } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { RoleRef } from "./types/role-ref";
|
||||
import type { Subject } from "./types/subject";
|
||||
|
||||
export type ClusterRoleBindingSubjectKind = "Group" | "ServiceAccount" | "User";
|
||||
|
||||
export interface ClusterRoleBindingSubject {
|
||||
kind: ClusterRoleBindingSubjectKind;
|
||||
name: string;
|
||||
apiGroup?: string;
|
||||
namespace?: string;
|
||||
export interface ClusterRoleBindingData extends KubeJsonApiData<KubeObjectMetadata<KubeObjectScope.Cluster>, void, void> {
|
||||
subjects?: Subject[];
|
||||
roleRef: RoleRef;
|
||||
}
|
||||
|
||||
export interface ClusterRoleBinding {
|
||||
subjects?: ClusterRoleBindingSubject[];
|
||||
roleRef: {
|
||||
kind: string;
|
||||
name: string;
|
||||
apiGroup?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class ClusterRoleBinding extends KubeObject {
|
||||
export class ClusterRoleBinding extends KubeObject<void, void, KubeObjectScope.Cluster> {
|
||||
static kind = "ClusterRoleBinding";
|
||||
static namespaced = false;
|
||||
static apiBase = "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings";
|
||||
|
||||
subjects?: Subject[];
|
||||
roleRef: RoleRef;
|
||||
|
||||
constructor({
|
||||
subjects,
|
||||
roleRef,
|
||||
...rest
|
||||
}: ClusterRoleBindingData) {
|
||||
super(rest);
|
||||
this.subjects = subjects;
|
||||
this.roleRef = roleRef;
|
||||
}
|
||||
|
||||
getSubjects() {
|
||||
return this.subjects || [];
|
||||
return this.subjects ?? [];
|
||||
}
|
||||
|
||||
getSubjectNames(): string {
|
||||
@@ -38,17 +42,11 @@ export class ClusterRoleBinding extends KubeObject {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only available within kubernetes cluster pages
|
||||
*/
|
||||
let clusterRoleBindingApi: KubeApi<ClusterRoleBinding>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
clusterRoleBindingApi = new KubeApi({
|
||||
objectConstructor: ClusterRoleBinding,
|
||||
});
|
||||
export class ClusterRoleBindingApi extends KubeApi<ClusterRoleBinding, ClusterRoleBindingData> {
|
||||
constructor(opts: DerivedKubeApiOptions = {}) {
|
||||
super({
|
||||
...opts,
|
||||
objectConstructor: ClusterRoleBinding,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
clusterRoleBindingApi,
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { ClusterRoleApi } from "./cluster-role.api";
|
||||
|
||||
const clusterRoleApiInjectable = getInjectable({
|
||||
id: "cluster-role-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "clusterRoleApi is only available in certain environments");
|
||||
|
||||
return new ClusterRoleApi();
|
||||
},
|
||||
});
|
||||
|
||||
export default clusterRoleApiInjectable;
|
||||
@@ -3,40 +3,43 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import type { DerivedKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import type { KubeObjectMetadata, KubeObjectScope } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { AggregationRule } from "./types/aggregation-rule";
|
||||
import type { PolicyRule } from "./types/policy-rule";
|
||||
|
||||
export interface ClusterRole {
|
||||
rules: {
|
||||
verbs: string[];
|
||||
apiGroups: string[];
|
||||
resources: string[];
|
||||
resourceNames?: string[];
|
||||
}[];
|
||||
export interface ClusterRoleData extends KubeJsonApiData<KubeObjectMetadata<KubeObjectScope.Cluster>, void, void> {
|
||||
rules?: PolicyRule[];
|
||||
aggregationRule?: AggregationRule;
|
||||
}
|
||||
|
||||
export class ClusterRole extends KubeObject {
|
||||
export class ClusterRole extends KubeObject<void, void, KubeObjectScope.Cluster> {
|
||||
static kind = "ClusterRole";
|
||||
static namespaced = false;
|
||||
static apiBase = "/apis/rbac.authorization.k8s.io/v1/clusterroles";
|
||||
|
||||
rules?: PolicyRule[];
|
||||
aggregationRule?: AggregationRule;
|
||||
|
||||
constructor({ rules, aggregationRule, ...rest }: ClusterRoleData) {
|
||||
super(rest);
|
||||
this.rules = rules;
|
||||
this.aggregationRule = aggregationRule;
|
||||
}
|
||||
|
||||
getRules() {
|
||||
return this.rules || [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only available within kubernetes cluster pages
|
||||
*/
|
||||
let clusterRoleApi: KubeApi<ClusterRole>;
|
||||
|
||||
if (isClusterPageContext()) { // initialize automatically only when within a cluster iframe/context
|
||||
clusterRoleApi = new KubeApi({
|
||||
objectConstructor: ClusterRole,
|
||||
});
|
||||
export class ClusterRoleApi extends KubeApi<ClusterRole, ClusterRoleData> {
|
||||
constructor(opts: DerivedKubeApiOptions = {}) {
|
||||
super({
|
||||
...opts,
|
||||
objectConstructor: ClusterRole,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
clusterRoleApi,
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { ClusterApi } from "./cluster.api";
|
||||
|
||||
const clusterApiInjectable = getInjectable({
|
||||
id: "cluster-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "clusterApi is only available in certain environments");
|
||||
|
||||
return new ClusterApi();
|
||||
},
|
||||
});
|
||||
|
||||
export default clusterApiInjectable;
|
||||
@@ -3,18 +3,25 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { IMetrics, IMetricsReqParams } from "./metrics.api";
|
||||
import type { MetricData, IMetricsReqParams } from "./metrics.api";
|
||||
import { metricsApi } from "./metrics.api";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export class ClusterApi extends KubeApi<Cluster> {
|
||||
static kind = "Cluster";
|
||||
static namespaced = true;
|
||||
|
||||
constructor(opts: DerivedKubeApiOptions & IgnoredKubeApiOptions = {}) {
|
||||
super({
|
||||
...opts,
|
||||
objectConstructor: Cluster,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getMetricsByNodeNames(nodeNames: string[], params?: IMetricsReqParams): Promise<IClusterMetrics> {
|
||||
export function getMetricsByNodeNames(nodeNames: string[], params?: IMetricsReqParams): Promise<ClusterMetricData> {
|
||||
const nodes = nodeNames.join("|");
|
||||
const opts = { category: "cluster", nodes };
|
||||
|
||||
@@ -45,20 +52,19 @@ export enum ClusterStatus {
|
||||
ERROR = "Error",
|
||||
}
|
||||
|
||||
export interface IClusterMetrics<T = IMetrics> {
|
||||
[metric: string]: T;
|
||||
memoryUsage: T;
|
||||
memoryRequests: T;
|
||||
memoryLimits: T;
|
||||
memoryCapacity: T;
|
||||
cpuUsage: T;
|
||||
cpuRequests: T;
|
||||
cpuLimits: T;
|
||||
cpuCapacity: T;
|
||||
podUsage: T;
|
||||
podCapacity: T;
|
||||
fsSize: T;
|
||||
fsUsage: T;
|
||||
export interface ClusterMetricData extends Partial<Record<string, MetricData>> {
|
||||
memoryUsage: MetricData;
|
||||
memoryRequests: MetricData;
|
||||
memoryLimits: MetricData;
|
||||
memoryCapacity: MetricData;
|
||||
cpuUsage: MetricData;
|
||||
cpuRequests: MetricData;
|
||||
cpuLimits: MetricData;
|
||||
cpuCapacity: MetricData;
|
||||
podUsage: MetricData;
|
||||
podCapacity: MetricData;
|
||||
fsSize: MetricData;
|
||||
fsUsage: MetricData;
|
||||
}
|
||||
|
||||
export interface Cluster {
|
||||
@@ -107,18 +113,3 @@ export class Cluster extends KubeObject {
|
||||
return ClusterStatus.ACTIVE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only available within kubernetes cluster pages
|
||||
*/
|
||||
let clusterApi: ClusterApi;
|
||||
|
||||
if (isClusterPageContext()) { // initialize automatically only when within a cluster iframe/context
|
||||
clusterApi = new ClusterApi({
|
||||
objectConstructor: Cluster,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
clusterApi,
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { ComponentStatusApi } from "./component-status.api";
|
||||
|
||||
const componentStatusApiInjectable = getInjectable({
|
||||
id: "component-status-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "componentStatusApi is only available in certain environments");
|
||||
|
||||
return new ComponentStatusApi();
|
||||
},
|
||||
});
|
||||
|
||||
export default componentStatusApiInjectable;
|
||||
@@ -4,16 +4,17 @@
|
||||
*/
|
||||
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { DerivedKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
|
||||
export interface IComponentStatusCondition {
|
||||
export interface ComponentStatusCondition {
|
||||
type: string;
|
||||
status: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ComponentStatus {
|
||||
conditions: IComponentStatusCondition[];
|
||||
conditions: ComponentStatusCondition[];
|
||||
}
|
||||
|
||||
export class ComponentStatus extends KubeObject {
|
||||
@@ -26,6 +27,11 @@ export class ComponentStatus extends KubeObject {
|
||||
}
|
||||
}
|
||||
|
||||
export const componentStatusApi = new KubeApi({
|
||||
objectConstructor: ComponentStatus,
|
||||
});
|
||||
export class ComponentStatusApi extends KubeApi<ComponentStatus> {
|
||||
constructor(opts: DerivedKubeApiOptions = {}) {
|
||||
super({
|
||||
...opts,
|
||||
objectConstructor: ComponentStatus,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { ConfigMapApi } from "./config-map.api";
|
||||
|
||||
const configMapApiInjectable = getInjectable({
|
||||
id: "config-map-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "configMapApi is only available in certain environments");
|
||||
|
||||
return new ConfigMapApi();
|
||||
},
|
||||
});
|
||||
|
||||
export default configMapApiInjectable;
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { KubeObjectMetadata, KubeObjectScope } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import type { DerivedKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { autoBind } from "../../utils";
|
||||
|
||||
export interface ConfigMapData extends KubeJsonApiData<KubeObjectMetadata<KubeObjectScope.Namespace>, void, void> {
|
||||
data?: Partial<Record<string, string>>;
|
||||
binaryData?: Partial<Record<string, string>>;
|
||||
immutable?: boolean;
|
||||
}
|
||||
|
||||
export class ConfigMap extends KubeObject<void, void, KubeObjectScope.Namespace> {
|
||||
static kind = "ConfigMap";
|
||||
static namespaced = true;
|
||||
static apiBase = "/api/v1/configmaps";
|
||||
|
||||
data: Partial<Record<string, string>>;
|
||||
binaryData: Partial<Record<string, string>>;
|
||||
immutable?: boolean;
|
||||
|
||||
constructor({ data, binaryData, immutable, ...rest }: ConfigMapData) {
|
||||
super(rest);
|
||||
autoBind(this);
|
||||
|
||||
this.data = data ?? {};
|
||||
this.binaryData = binaryData ?? {};
|
||||
this.immutable = immutable;
|
||||
}
|
||||
|
||||
getKeys(): string[] {
|
||||
return Object.keys(this.data);
|
||||
}
|
||||
}
|
||||
|
||||
export class ConfigMapApi extends KubeApi<ConfigMap, ConfigMapData> {
|
||||
constructor(opts?: DerivedKubeApiOptions) {
|
||||
super({
|
||||
objectConstructor: ConfigMap,
|
||||
...opts ?? {},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { autoBind } from "../../utils";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export interface ConfigMap {
|
||||
data: {
|
||||
[param: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class ConfigMap extends KubeObject {
|
||||
static kind = "ConfigMap";
|
||||
static namespaced = true;
|
||||
static apiBase = "/api/v1/configmaps";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
|
||||
this.data ??= {};
|
||||
}
|
||||
|
||||
getKeys(): string[] {
|
||||
return Object.keys(this.data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only available within kubernetes cluster pages
|
||||
*/
|
||||
let configMapApi: KubeApi<ConfigMap>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
configMapApi = new KubeApi({
|
||||
objectConstructor: ConfigMap,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
configMapApi,
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { CronJobApi } from "./cron-job.api";
|
||||
|
||||
const cronJobApiInjectable = getInjectable({
|
||||
id: "cron-job-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "cronJobApi is only available in certain environments");
|
||||
|
||||
return new CronJobApi();
|
||||
},
|
||||
});
|
||||
|
||||
export default cronJobApiInjectable;
|
||||
@@ -4,15 +4,21 @@
|
||||
*/
|
||||
|
||||
import moment from "moment";
|
||||
import type { KubeObjectScope, ObjectReference } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { IPodContainer } from "./pods.api";
|
||||
import { formatDuration } from "../../utils/formatDuration";
|
||||
import { autoBind } from "../../utils";
|
||||
import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import type { JobTemplateSpec } from "./types/job-template-spec";
|
||||
|
||||
export class CronJobApi extends KubeApi<CronJob> {
|
||||
constructor(opts: DerivedKubeApiOptions & IgnoredKubeApiOptions = {}) {
|
||||
super({
|
||||
...opts,
|
||||
objectConstructor: CronJob,
|
||||
});
|
||||
}
|
||||
|
||||
suspend(params: { namespace: string; name: string }) {
|
||||
return this.request.patch(this.getUrl(params), {
|
||||
data: {
|
||||
@@ -44,61 +50,33 @@ export class CronJobApi extends KubeApi<CronJob> {
|
||||
}
|
||||
}
|
||||
|
||||
export interface CronJob {
|
||||
spec: {
|
||||
schedule: string;
|
||||
concurrencyPolicy: string;
|
||||
suspend: boolean;
|
||||
jobTemplate: {
|
||||
metadata: {
|
||||
creationTimestamp?: string;
|
||||
labels?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
annotations?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
};
|
||||
spec: {
|
||||
template: {
|
||||
metadata: {
|
||||
creationTimestamp?: string;
|
||||
};
|
||||
spec: {
|
||||
containers: IPodContainer[];
|
||||
restartPolicy: string;
|
||||
terminationGracePeriodSeconds: number;
|
||||
dnsPolicy: string;
|
||||
hostPID: boolean;
|
||||
schedulerName: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
successfulJobsHistoryLimit: number;
|
||||
failedJobsHistoryLimit: number;
|
||||
};
|
||||
status: {
|
||||
lastScheduleTime?: string;
|
||||
};
|
||||
export interface CronJobSpec {
|
||||
concurrencyPolicy?: string;
|
||||
failedJobsHistoryLimit?: number;
|
||||
jobTemplate?: JobTemplateSpec;
|
||||
schedule: string;
|
||||
startingDeadlineSeconds?: number;
|
||||
successfulJobsHistoryLimit?: number;
|
||||
suspend?: boolean;
|
||||
}
|
||||
|
||||
export class CronJob extends KubeObject {
|
||||
static kind = "CronJob";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/batch/v1beta1/cronjobs";
|
||||
export interface CronJobStatus {
|
||||
lastScheduleTime?: string;
|
||||
lastSuccessfulTime?: string;
|
||||
active?: ObjectReference[];
|
||||
}
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
export class CronJob extends KubeObject<CronJobStatus, CronJobSpec, KubeObjectScope.Namespace> {
|
||||
static readonly kind = "CronJob";
|
||||
static readonly namespaced = true;
|
||||
static readonly apiBase = "/apis/batch/v1beta1/cronjobs";
|
||||
|
||||
getSuspendFlag() {
|
||||
return this.spec.suspend.toString();
|
||||
return (this.spec.suspend ?? false).toString();
|
||||
}
|
||||
|
||||
getLastScheduleTime() {
|
||||
if (!this.status.lastScheduleTime) return "-";
|
||||
if (!this.status?.lastScheduleTime) return "-";
|
||||
const diff = moment().diff(this.status.lastScheduleTime);
|
||||
|
||||
return formatDuration(diff, true);
|
||||
@@ -124,18 +102,3 @@ export class CronJob extends KubeObject {
|
||||
return this.spec.suspend;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only available within kubernetes cluster pages
|
||||
*/
|
||||
let cronJobApi: CronJobApi;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
cronJobApi = new CronJobApi({
|
||||
objectConstructor: CronJob,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
cronJobApi,
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { CustomResourceDefinitionApi } from "./custom-resource-definition.api";
|
||||
|
||||
const customResourceDefinitionApiInjectable = getInjectable({
|
||||
id: "custom-resource-definition-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "customResourceDefinitionApi is only available in certain environments");
|
||||
|
||||
return new CustomResourceDefinitionApi();
|
||||
},
|
||||
});
|
||||
|
||||
export default customResourceDefinitionApiInjectable;
|
||||
+65
-64
@@ -3,13 +3,13 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { KubeCreationError, KubeObject } from "../kube-object";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { getLegacyGlobalDiForExtensionApi } from "../../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
|
||||
import customResourcesRouteInjectable from "../../front-end-routing/routes/cluster/custom-resources/custom-resources/custom-resources-route.injectable";
|
||||
import { buildURL } from "../../utils/buildUrl";
|
||||
import type { BaseKubeObjectCondition, KubeObjectScope } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { DerivedKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
|
||||
interface AdditionalPrinterColumnsCommon {
|
||||
name: string;
|
||||
@@ -26,7 +26,7 @@ type AdditionalPrinterColumnsV1Beta = AdditionalPrinterColumnsCommon & {
|
||||
JSONPath: string;
|
||||
};
|
||||
|
||||
export interface CRDVersion {
|
||||
export interface CustomResourceDefinitionVersion {
|
||||
name: string;
|
||||
served: boolean;
|
||||
storage: boolean;
|
||||
@@ -34,72 +34,78 @@ export interface CRDVersion {
|
||||
additionalPrinterColumns?: AdditionalPrinterColumnsV1[];
|
||||
}
|
||||
|
||||
export interface CustomResourceDefinitionNames {
|
||||
categories?: string[];
|
||||
kind: string;
|
||||
listKind?: string;
|
||||
plural: string;
|
||||
shortNames?: string[];
|
||||
singular?: string;
|
||||
}
|
||||
|
||||
export interface CustomResourceConversion {
|
||||
strategy?: string;
|
||||
webhook?: WebhookConversion;
|
||||
}
|
||||
|
||||
export interface WebhookConversion {
|
||||
clientConfig?: WebhookClientConfig[];
|
||||
conversionReviewVersions: string[];
|
||||
}
|
||||
|
||||
export interface WebhookClientConfig {
|
||||
caBundle?: string;
|
||||
url?: string;
|
||||
service?: ServiceReference;
|
||||
}
|
||||
|
||||
export interface ServiceReference {
|
||||
name: string;
|
||||
namespace: string;
|
||||
path?: string;
|
||||
port?: number;
|
||||
}
|
||||
|
||||
export interface CustomResourceDefinitionSpec {
|
||||
group: string;
|
||||
/**
|
||||
* @deprecated for apiextensions.k8s.io/v1 but used in v1beta1
|
||||
*/
|
||||
version?: string;
|
||||
names: {
|
||||
plural: string;
|
||||
singular: string;
|
||||
kind: string;
|
||||
listKind: string;
|
||||
};
|
||||
names: CustomResourceDefinitionNames;
|
||||
scope: "Namespaced" | "Cluster";
|
||||
/**
|
||||
* @deprecated for apiextensions.k8s.io/v1 but used in v1beta1
|
||||
*/
|
||||
validation?: object;
|
||||
versions?: CRDVersion[];
|
||||
conversion: {
|
||||
strategy?: string;
|
||||
webhook?: any;
|
||||
};
|
||||
versions?: CustomResourceDefinitionVersion[];
|
||||
conversion?: CustomResourceConversion;
|
||||
/**
|
||||
* @deprecated for apiextensions.k8s.io/v1 but used in v1beta1
|
||||
*/
|
||||
additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[];
|
||||
preserveUnknownFields?: boolean;
|
||||
}
|
||||
|
||||
export interface CustomResourceDefinition {
|
||||
spec: CustomResourceDefinitionSpec;
|
||||
status: {
|
||||
conditions: {
|
||||
lastTransitionTime: string;
|
||||
message: string;
|
||||
reason: string;
|
||||
status: string;
|
||||
type?: string;
|
||||
}[];
|
||||
acceptedNames: {
|
||||
plural: string;
|
||||
singular: string;
|
||||
kind: string;
|
||||
shortNames: string[];
|
||||
listKind: string;
|
||||
};
|
||||
storedVersions: string[];
|
||||
};
|
||||
export interface CustomResourceDefinitionConditionAcceptedNames {
|
||||
plural: string;
|
||||
singular: string;
|
||||
kind: string;
|
||||
shortNames: string[];
|
||||
listKind: string;
|
||||
}
|
||||
|
||||
export interface CRDApiData extends KubeJsonApiData {
|
||||
spec: object; // TODO: make better
|
||||
export interface CustomResourceDefinitionStatus {
|
||||
conditions?: BaseKubeObjectCondition[];
|
||||
acceptedNames: CustomResourceDefinitionConditionAcceptedNames;
|
||||
storedVersions: string[];
|
||||
}
|
||||
|
||||
export class CustomResourceDefinition extends KubeObject {
|
||||
export class CustomResourceDefinition extends KubeObject<CustomResourceDefinitionStatus, CustomResourceDefinitionSpec, KubeObjectScope.Cluster> {
|
||||
static kind = "CustomResourceDefinition";
|
||||
static namespaced = false;
|
||||
static apiBase = "/apis/apiextensions.k8s.io/v1/customresourcedefinitions";
|
||||
|
||||
constructor(data: CRDApiData) {
|
||||
super(data);
|
||||
|
||||
if (!data.spec || typeof data.spec !== "object") {
|
||||
throw new KubeCreationError("Cannot create a CustomResourceDefinition from an object without spec", data);
|
||||
}
|
||||
}
|
||||
|
||||
getResourceUrl() {
|
||||
const di = getLegacyGlobalDiForExtensionApi();
|
||||
|
||||
@@ -141,12 +147,12 @@ export class CustomResourceDefinition extends KubeObject {
|
||||
return this.spec.scope;
|
||||
}
|
||||
|
||||
getPreferedVersion(): CRDVersion {
|
||||
getPreferedVersion(): CustomResourceDefinitionVersion {
|
||||
const { apiVersion } = this;
|
||||
|
||||
switch (apiVersion) {
|
||||
case "apiextensions.k8s.io/v1":
|
||||
for (const version of this.spec.versions) {
|
||||
for (const version of this.spec.versions ?? []) {
|
||||
if (version.storage) {
|
||||
return version;
|
||||
}
|
||||
@@ -158,7 +164,8 @@ export class CustomResourceDefinition extends KubeObject {
|
||||
const additionalPrinterColumns = apc?.map(({ JSONPath, ...apc }) => ({ ...apc, jsonPath: JSONPath }));
|
||||
|
||||
return {
|
||||
name: this.spec.version,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
name: this.spec.version!,
|
||||
served: true,
|
||||
storage: true,
|
||||
schema: this.spec.validation,
|
||||
@@ -179,7 +186,7 @@ export class CustomResourceDefinition extends KubeObject {
|
||||
}
|
||||
|
||||
getStoredVersions() {
|
||||
return this.status.storedVersions.join(", ");
|
||||
return this.status?.storedVersions.join(", ") ?? "";
|
||||
}
|
||||
|
||||
getNames() {
|
||||
@@ -216,18 +223,12 @@ export class CustomResourceDefinition extends KubeObject {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only available within kubernetes cluster pages
|
||||
*/
|
||||
let crdApi: KubeApi<CustomResourceDefinition>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
crdApi = new KubeApi<CustomResourceDefinition>({
|
||||
objectConstructor: CustomResourceDefinition,
|
||||
checkPreferredVersion: true,
|
||||
});
|
||||
export class CustomResourceDefinitionApi extends KubeApi<CustomResourceDefinition> {
|
||||
constructor(opts: DerivedKubeApiOptions = {}) {
|
||||
super({
|
||||
objectConstructor: CustomResourceDefinition,
|
||||
checkPreferredVersion: true,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
crdApi,
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { DaemonSetApi } from "./daemon-set.api";
|
||||
|
||||
const daemonSetApiInjectable = getInjectable({
|
||||
id: "daemon-set-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "daemonSetApi is only available in certain environements");
|
||||
|
||||
return new DaemonSetApi();
|
||||
},
|
||||
});
|
||||
|
||||
export default daemonSetApiInjectable;
|
||||
@@ -3,88 +3,91 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import get from "lodash/get";
|
||||
import type { IAffinity } from "../workload-kube-object";
|
||||
import { WorkloadKubeObject } from "../workload-kube-object";
|
||||
import { autoBind } from "../../utils";
|
||||
import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { metricsApi } from "./metrics.api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import type { IPodContainer, IPodMetrics } from "./pods.api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import type { LabelSelector } from "../kube-object";
|
||||
import type { PodMetricData } from "./pod.api";
|
||||
import type { KubeObjectScope, KubeObjectStatus, LabelSelector } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { PodTemplateSpec } from "./types/pod-template-spec";
|
||||
|
||||
export class DaemonSet extends WorkloadKubeObject {
|
||||
export interface RollingUpdateDaemonSet {
|
||||
maxUnavailable?: number | string;
|
||||
maxSurge?: number | string;
|
||||
}
|
||||
|
||||
export interface DaemonSetUpdateStrategy {
|
||||
type: string;
|
||||
rollingUpdate: RollingUpdateDaemonSet;
|
||||
}
|
||||
|
||||
export interface DaemonSetSpec {
|
||||
selector: LabelSelector;
|
||||
template: PodTemplateSpec;
|
||||
updateStrategy: DaemonSetUpdateStrategy;
|
||||
minReadySeconds?: number;
|
||||
revisionHistoryLimit?: number;
|
||||
}
|
||||
|
||||
export interface DaemonSetStatus extends KubeObjectStatus {
|
||||
collisionCount?: number;
|
||||
currentNumberScheduled: number;
|
||||
desiredNumberScheduled: number;
|
||||
numberAvailable?: number;
|
||||
numberMisscheduled: number;
|
||||
numberReady: number;
|
||||
numberUnavailable?: number;
|
||||
observedGeneration?: number;
|
||||
updatedNumberScheduled?: number;
|
||||
}
|
||||
|
||||
export class DaemonSet extends KubeObject<DaemonSetStatus, DaemonSetSpec, KubeObjectScope.Namespace> {
|
||||
static kind = "DaemonSet";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/apps/v1/daemonsets";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
getSelectors(): string[] {
|
||||
return KubeObject.stringifyLabels(this.spec.selector.matchLabels);
|
||||
}
|
||||
|
||||
declare spec: {
|
||||
selector: LabelSelector;
|
||||
template: {
|
||||
metadata: {
|
||||
creationTimestamp?: string;
|
||||
labels: {
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
spec: {
|
||||
containers: IPodContainer[];
|
||||
initContainers?: IPodContainer[];
|
||||
restartPolicy: string;
|
||||
terminationGracePeriodSeconds: number;
|
||||
dnsPolicy: string;
|
||||
hostPID: boolean;
|
||||
affinity?: IAffinity;
|
||||
nodeSelector?: {
|
||||
[selector: string]: string;
|
||||
};
|
||||
securityContext: {};
|
||||
schedulerName: string;
|
||||
tolerations: {
|
||||
key: string;
|
||||
operator: string;
|
||||
effect: string;
|
||||
tolerationSeconds: number;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
updateStrategy: {
|
||||
type: string;
|
||||
rollingUpdate: {
|
||||
maxUnavailable: number;
|
||||
};
|
||||
};
|
||||
revisionHistoryLimit: number;
|
||||
};
|
||||
declare status: {
|
||||
currentNumberScheduled: number;
|
||||
numberMisscheduled: number;
|
||||
desiredNumberScheduled: number;
|
||||
numberReady: number;
|
||||
observedGeneration: number;
|
||||
updatedNumberScheduled: number;
|
||||
numberAvailable: number;
|
||||
numberUnavailable: number;
|
||||
};
|
||||
getNodeSelectors(): string[] {
|
||||
return KubeObject.stringifyLabels(this.spec.template.spec?.nodeSelector);
|
||||
}
|
||||
|
||||
getTemplateLabels(): string[] {
|
||||
return KubeObject.stringifyLabels(this.spec.template.metadata?.labels);
|
||||
}
|
||||
|
||||
getTolerations() {
|
||||
return this.spec.template.spec?.tolerations ?? [];
|
||||
}
|
||||
|
||||
getAffinity() {
|
||||
return this.spec.template.spec?.affinity;
|
||||
}
|
||||
|
||||
getAffinityNumber() {
|
||||
return Object.keys(this.getAffinity() ?? {}).length;
|
||||
}
|
||||
|
||||
getImages() {
|
||||
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []);
|
||||
const initContainers: IPodContainer[] = get(this, "spec.template.spec.initContainers", []);
|
||||
const containers = this.spec.template?.spec?.containers ?? [];
|
||||
const initContainers = this.spec.template?.spec?.initContainers ?? [];
|
||||
|
||||
return [...containers, ...initContainers].map(container => container.image);
|
||||
}
|
||||
}
|
||||
|
||||
export class DaemonSetApi extends KubeApi<DaemonSet> {
|
||||
constructor(opts: DerivedKubeApiOptions & IgnoredKubeApiOptions = {}) {
|
||||
super({
|
||||
...opts,
|
||||
objectConstructor: DaemonSet,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getMetricsForDaemonSets(daemonsets: DaemonSet[], namespace: string, selector = ""): Promise<IPodMetrics> {
|
||||
export function getMetricsForDaemonSets(daemonsets: DaemonSet[], namespace: string, selector = ""): Promise<PodMetricData> {
|
||||
const podSelector = daemonsets.map(daemonset => `${daemonset.getName()}-[[:alnum:]]{5}`).join("|");
|
||||
const opts = { category: "pods", pods: podSelector, namespace, selector };
|
||||
|
||||
@@ -100,18 +103,3 @@ export function getMetricsForDaemonSets(daemonsets: DaemonSet[], namespace: stri
|
||||
namespace,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Only available within kubernetes cluster pages
|
||||
*/
|
||||
let daemonSetApi: DaemonSetApi;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
daemonSetApi = new DaemonSetApi({
|
||||
objectConstructor: DaemonSet,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
daemonSetApi,
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { DeploymentApi } from "./deployment.api";
|
||||
|
||||
const deploymentApiInjectable = getInjectable({
|
||||
id: "deployment-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "deploymentApi is only available in certain environments");
|
||||
|
||||
return new DeploymentApi();
|
||||
},
|
||||
});
|
||||
|
||||
export default deploymentApiInjectable;
|
||||
@@ -5,25 +5,34 @@
|
||||
|
||||
import moment from "moment";
|
||||
|
||||
import type { IAffinity } from "../workload-kube-object";
|
||||
import { WorkloadKubeObject } from "../workload-kube-object";
|
||||
import { autoBind } from "../../utils";
|
||||
import type { DerivedKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { metricsApi } from "./metrics.api";
|
||||
import type { IPodMetrics } from "./pods.api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import type { LabelSelector } from "../kube-object";
|
||||
import type { PodMetricData, PodSpec } from "./pod.api";
|
||||
import type { KubeObjectScope, KubeObjectStatus, LabelSelector } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { hasTypedProperty, isNumber, isObject } from "../../utils";
|
||||
|
||||
export class DeploymentApi extends KubeApi<Deployment> {
|
||||
constructor(opts?: DerivedKubeApiOptions) {
|
||||
super({
|
||||
objectConstructor: Deployment,
|
||||
...opts ?? {},
|
||||
});
|
||||
}
|
||||
|
||||
protected getScaleApiUrl(params: { namespace: string; name: string }) {
|
||||
return `${this.getUrl(params)}/scale`;
|
||||
}
|
||||
|
||||
getReplicas(params: { namespace: string; name: string }): Promise<number> {
|
||||
return this.request
|
||||
.get(this.getScaleApiUrl(params))
|
||||
.then(({ status }: any) => status?.replicas);
|
||||
async getReplicas(params: { namespace: string; name: string }): Promise<number> {
|
||||
const { status } = await this.request.get(this.getScaleApiUrl(params));
|
||||
|
||||
if (isObject(status) && hasTypedProperty(status, "replicas", isNumber)) {
|
||||
return status.replicas;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
scale(params: { namespace: string; name: string }, replicas: number) {
|
||||
@@ -61,7 +70,7 @@ export class DeploymentApi extends KubeApi<Deployment> {
|
||||
}
|
||||
}
|
||||
|
||||
export function getMetricsForDeployments(deployments: Deployment[], namespace: string, selector = ""): Promise<IPodMetrics> {
|
||||
export function getMetricsForDeployments(deployments: Deployment[], namespace: string, selector = ""): Promise<PodMetricData> {
|
||||
const podSelector = deployments.map(deployment => `${deployment.getName()}-[[:alnum:]]{9,}-[[:alnum:]]{5}`).join("|");
|
||||
const opts = { category: "pods", pods: podSelector, namespace, selector };
|
||||
|
||||
@@ -78,136 +87,66 @@ export function getMetricsForDeployments(deployments: Deployment[], namespace: s
|
||||
});
|
||||
}
|
||||
|
||||
interface IContainerProbe {
|
||||
httpGet?: {
|
||||
path?: string;
|
||||
port: number;
|
||||
scheme: string;
|
||||
host?: string;
|
||||
export interface DeploymentSpec {
|
||||
replicas: number;
|
||||
selector: LabelSelector;
|
||||
template: {
|
||||
metadata: {
|
||||
creationTimestamp?: string;
|
||||
labels: Partial<Record<string, string>>;
|
||||
annotations?: Partial<Record<string, string>>;
|
||||
};
|
||||
spec: PodSpec;
|
||||
};
|
||||
exec?: {
|
||||
command: string[];
|
||||
strategy: {
|
||||
type: string;
|
||||
rollingUpdate: {
|
||||
maxUnavailable: number;
|
||||
maxSurge: number;
|
||||
};
|
||||
};
|
||||
tcpSocket?: {
|
||||
port: number;
|
||||
};
|
||||
initialDelaySeconds?: number;
|
||||
timeoutSeconds?: number;
|
||||
periodSeconds?: number;
|
||||
successThreshold?: number;
|
||||
failureThreshold?: number;
|
||||
}
|
||||
|
||||
export class Deployment extends WorkloadKubeObject {
|
||||
export interface DeploymentStatus extends KubeObjectStatus {
|
||||
observedGeneration: number;
|
||||
replicas: number;
|
||||
updatedReplicas: number;
|
||||
readyReplicas: number;
|
||||
availableReplicas?: number;
|
||||
unavailableReplicas?: number;
|
||||
}
|
||||
|
||||
export class Deployment extends KubeObject<DeploymentStatus, DeploymentSpec, KubeObjectScope.Namespace> {
|
||||
static kind = "Deployment";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/apps/v1/deployments";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
getSelectors(): string[] {
|
||||
return KubeObject.stringifyLabels(this.spec.selector.matchLabels);
|
||||
}
|
||||
|
||||
declare spec: {
|
||||
replicas: number;
|
||||
selector: LabelSelector;
|
||||
template: {
|
||||
metadata: {
|
||||
creationTimestamp?: string;
|
||||
labels: { [app: string]: string };
|
||||
annotations?: { [app: string]: string };
|
||||
};
|
||||
spec: {
|
||||
containers: {
|
||||
name: string;
|
||||
image: string;
|
||||
args?: string[];
|
||||
ports?: {
|
||||
name: string;
|
||||
containerPort: number;
|
||||
protocol: string;
|
||||
}[];
|
||||
env?: {
|
||||
name: string;
|
||||
value: string;
|
||||
}[];
|
||||
resources: {
|
||||
limits?: {
|
||||
cpu: string;
|
||||
memory: string;
|
||||
};
|
||||
requests: {
|
||||
cpu: string;
|
||||
memory: string;
|
||||
};
|
||||
};
|
||||
volumeMounts?: {
|
||||
name: string;
|
||||
mountPath: string;
|
||||
}[];
|
||||
livenessProbe?: IContainerProbe;
|
||||
readinessProbe?: IContainerProbe;
|
||||
startupProbe?: IContainerProbe;
|
||||
terminationMessagePath: string;
|
||||
terminationMessagePolicy: string;
|
||||
imagePullPolicy: string;
|
||||
}[];
|
||||
restartPolicy: string;
|
||||
terminationGracePeriodSeconds: number;
|
||||
dnsPolicy: string;
|
||||
affinity?: IAffinity;
|
||||
nodeSelector?: {
|
||||
[selector: string]: string;
|
||||
};
|
||||
serviceAccountName: string;
|
||||
serviceAccount: string;
|
||||
securityContext: {};
|
||||
schedulerName: string;
|
||||
tolerations?: {
|
||||
key: string;
|
||||
operator: string;
|
||||
effect: string;
|
||||
tolerationSeconds: number;
|
||||
}[];
|
||||
volumes?: {
|
||||
name: string;
|
||||
configMap: {
|
||||
name: string;
|
||||
defaultMode: number;
|
||||
optional: boolean;
|
||||
};
|
||||
}[];
|
||||
};
|
||||
};
|
||||
strategy: {
|
||||
type: string;
|
||||
rollingUpdate: {
|
||||
maxUnavailable: number;
|
||||
maxSurge: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
declare status: {
|
||||
observedGeneration: number;
|
||||
replicas: number;
|
||||
updatedReplicas: number;
|
||||
readyReplicas: number;
|
||||
availableReplicas?: number;
|
||||
unavailableReplicas?: number;
|
||||
conditions: {
|
||||
type: string;
|
||||
status: string;
|
||||
lastUpdateTime: string;
|
||||
lastTransitionTime: string;
|
||||
reason: string;
|
||||
message: string;
|
||||
}[];
|
||||
};
|
||||
getNodeSelectors(): string[] {
|
||||
return KubeObject.stringifyLabels(this.spec.template.spec.nodeSelector);
|
||||
}
|
||||
|
||||
getTemplateLabels(): string[] {
|
||||
return KubeObject.stringifyLabels(this.spec.template.metadata.labels);
|
||||
}
|
||||
|
||||
getTolerations() {
|
||||
return this.spec.template.spec.tolerations ?? [];
|
||||
}
|
||||
|
||||
getAffinity() {
|
||||
return this.spec.template.spec.affinity;
|
||||
}
|
||||
|
||||
getAffinityNumber() {
|
||||
return Object.keys(this.getAffinity() ?? {}).length;
|
||||
}
|
||||
|
||||
getConditions(activeOnly = false) {
|
||||
const { conditions } = this.status;
|
||||
|
||||
if (!conditions) return [];
|
||||
const { conditions = [] } = this.status ?? {};
|
||||
|
||||
if (activeOnly) {
|
||||
return conditions.filter(c => c.status === "True");
|
||||
@@ -217,22 +156,12 @@ export class Deployment extends WorkloadKubeObject {
|
||||
}
|
||||
|
||||
getConditionsText(activeOnly = true) {
|
||||
return this.getConditions(activeOnly).map(({ type }) => type).join(" ");
|
||||
return this.getConditions(activeOnly)
|
||||
.map(({ type }) => type)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
getReplicas() {
|
||||
return this.spec.replicas || 0;
|
||||
}
|
||||
}
|
||||
|
||||
let deploymentApi: DeploymentApi;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
deploymentApi = new DeploymentApi({
|
||||
objectConstructor: Deployment,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
deploymentApi,
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { EndpointsApi } from "./endpoint.api";
|
||||
|
||||
const endpointsApiInjectable = getInjectable({
|
||||
id: "endpoints-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "endpointsApi is only available in certain environments");
|
||||
|
||||
return new EndpointsApi();
|
||||
},
|
||||
});
|
||||
|
||||
export default endpointsApiInjectable;
|
||||
@@ -4,144 +4,114 @@
|
||||
*/
|
||||
|
||||
import { autoBind } from "../../utils";
|
||||
import type { KubeObjectMetadata, KubeObjectScope, ObjectReference } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { DerivedKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { get } from "lodash";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export interface IEndpointPort {
|
||||
export function formatEndpointSubset(subset: EndpointSubset): string {
|
||||
const { addresses, ports } = subset;
|
||||
|
||||
if (!addresses || !ports) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return addresses
|
||||
.map(address => (
|
||||
ports
|
||||
.map(port => `${address.ip}:${port.port}`)
|
||||
.join(", ")
|
||||
))
|
||||
.join(", ");
|
||||
|
||||
}
|
||||
|
||||
export interface ForZone {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface EndpointHints {
|
||||
forZones?: ForZone[];
|
||||
}
|
||||
|
||||
export interface EndpointConditions {
|
||||
ready?: boolean;
|
||||
serving?: boolean;
|
||||
terminating?: boolean;
|
||||
}
|
||||
|
||||
export interface EndpointData {
|
||||
addresses: string[];
|
||||
conditions?: EndpointConditions;
|
||||
hints?: EndpointHints;
|
||||
hostname?: string;
|
||||
nodeName?: string;
|
||||
targetRef?: ObjectReference;
|
||||
zone?: string;
|
||||
}
|
||||
|
||||
export interface EndpointPort {
|
||||
appProtocol?: string;
|
||||
name?: string;
|
||||
protocol: string;
|
||||
protocol?: string;
|
||||
port: number;
|
||||
}
|
||||
|
||||
export interface IEndpointAddress {
|
||||
hostname: string;
|
||||
export interface EndpointAddress {
|
||||
hostname?: string;
|
||||
ip: string;
|
||||
nodeName: string;
|
||||
nodeName?: string;
|
||||
targetRef?: ObjectReference;
|
||||
}
|
||||
|
||||
export interface IEndpointSubset {
|
||||
addresses: IEndpointAddress[];
|
||||
notReadyAddresses: IEndpointAddress[];
|
||||
ports: IEndpointPort[];
|
||||
export interface EndpointSubset {
|
||||
addresses?: EndpointAddress[];
|
||||
notReadyAddresses?: EndpointAddress[];
|
||||
ports?: EndpointPort[];
|
||||
}
|
||||
|
||||
interface ITargetRef {
|
||||
kind: string;
|
||||
namespace: string;
|
||||
name: string;
|
||||
uid: string;
|
||||
resourceVersion: string;
|
||||
apiVersion: string;
|
||||
export interface EndpointsData extends KubeJsonApiData<KubeObjectMetadata<KubeObjectScope.Namespace>, void, void> {
|
||||
subsets?: EndpointSubset[];
|
||||
}
|
||||
|
||||
export class EndpointAddress implements IEndpointAddress {
|
||||
hostname: string;
|
||||
ip: string;
|
||||
nodeName: string;
|
||||
targetRef?: {
|
||||
kind: string;
|
||||
namespace: string;
|
||||
name: string;
|
||||
uid: string;
|
||||
resourceVersion: string;
|
||||
};
|
||||
|
||||
static create(data: IEndpointAddress): EndpointAddress {
|
||||
return new EndpointAddress(data);
|
||||
}
|
||||
|
||||
constructor(data: IEndpointAddress) {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.ip;
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.hostname;
|
||||
}
|
||||
|
||||
getTargetRef(): ITargetRef {
|
||||
if (this.targetRef) {
|
||||
return Object.assign(this.targetRef, { apiVersion: "v1" });
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class EndpointSubset implements IEndpointSubset {
|
||||
addresses: IEndpointAddress[];
|
||||
notReadyAddresses: IEndpointAddress[];
|
||||
ports: IEndpointPort[];
|
||||
|
||||
constructor(data: IEndpointSubset) {
|
||||
this.addresses = get(data, "addresses", []);
|
||||
this.notReadyAddresses = get(data, "notReadyAddresses", []);
|
||||
this.ports = get(data, "ports", []);
|
||||
}
|
||||
|
||||
getAddresses(): EndpointAddress[] {
|
||||
return this.addresses.map(EndpointAddress.create);
|
||||
}
|
||||
|
||||
getNotReadyAddresses(): EndpointAddress[] {
|
||||
return this.notReadyAddresses.map(EndpointAddress.create);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.addresses
|
||||
.map(address => (
|
||||
this.ports
|
||||
.map(port => `${address.ip}:${port.port}`)
|
||||
.join(", ")
|
||||
))
|
||||
.join(", ");
|
||||
}
|
||||
}
|
||||
|
||||
export interface Endpoint {
|
||||
subsets: IEndpointSubset[];
|
||||
}
|
||||
|
||||
export class Endpoint extends KubeObject {
|
||||
export class Endpoints extends KubeObject<void, void, KubeObjectScope.Namespace> {
|
||||
static kind = "Endpoints";
|
||||
static namespaced = true;
|
||||
static apiBase = "/api/v1/endpoints";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
subsets?: EndpointSubset[];
|
||||
|
||||
constructor({ subsets, ...rest }: EndpointsData) {
|
||||
super(rest);
|
||||
autoBind(this);
|
||||
this.subsets = subsets;
|
||||
}
|
||||
|
||||
getEndpointSubsets(): EndpointSubset[] {
|
||||
const subsets = this.subsets || [];
|
||||
|
||||
return subsets.map(s => new EndpointSubset(s));
|
||||
getEndpointSubsets(): Required<EndpointSubset>[] {
|
||||
return this.subsets?.map(({
|
||||
addresses = [],
|
||||
notReadyAddresses = [],
|
||||
ports = [],
|
||||
}) => ({
|
||||
addresses,
|
||||
notReadyAddresses,
|
||||
ports,
|
||||
})) ?? [];
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
if(this.subsets) {
|
||||
return this.getEndpointSubsets().map(es => es.toString()).join(", ");
|
||||
} else {
|
||||
return "<none>";
|
||||
}
|
||||
return this.getEndpointSubsets()
|
||||
.map(formatEndpointSubset)
|
||||
.join(", ") || "<none>";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let endpointApi: KubeApi<Endpoint>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
endpointApi = new KubeApi<Endpoint>({
|
||||
objectConstructor: Endpoint,
|
||||
});
|
||||
export class EndpointsApi extends KubeApi<Endpoints, EndpointsData> {
|
||||
constructor(opts: DerivedKubeApiOptions = {}) {
|
||||
super({
|
||||
objectConstructor: Endpoints,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
endpointApi,
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { KubeEventApi } from "./events.api";
|
||||
|
||||
const kubeEventApiInjectable = getInjectable({
|
||||
id: "kube-event-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "kubeEventApi is only available in certain environments");
|
||||
|
||||
return new KubeEventApi();
|
||||
},
|
||||
});
|
||||
|
||||
export default kubeEventApiInjectable;
|
||||
@@ -4,34 +4,38 @@
|
||||
*/
|
||||
|
||||
import moment from "moment";
|
||||
import type { KubeObjectMetadata, ObjectReference } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { formatDuration } from "../../utils/formatDuration";
|
||||
import type { DerivedKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
|
||||
export interface KubeEvent {
|
||||
involvedObject: {
|
||||
kind: string;
|
||||
namespace: string;
|
||||
name: string;
|
||||
uid: string;
|
||||
apiVersion: string;
|
||||
resourceVersion: string;
|
||||
fieldPath: string;
|
||||
};
|
||||
reason: string;
|
||||
message: string;
|
||||
source: {
|
||||
component: string;
|
||||
host: string;
|
||||
};
|
||||
firstTimestamp: string;
|
||||
lastTimestamp: string;
|
||||
count: number;
|
||||
type: "Normal" | "Warning" | string;
|
||||
eventTime: null;
|
||||
reportingComponent: string;
|
||||
reportingInstance: string;
|
||||
export interface EventSeries {
|
||||
count?: number;
|
||||
lastObservedTime?: string;
|
||||
}
|
||||
|
||||
export interface EventSource {
|
||||
component?: string;
|
||||
host?: string;
|
||||
}
|
||||
|
||||
export interface KubeEventData extends KubeJsonApiData<KubeObjectMetadata, void, void> {
|
||||
action?: string;
|
||||
count?: number;
|
||||
eventTime?: string;
|
||||
firstTimestamp?: string;
|
||||
involvedObject: Required<ObjectReference>;
|
||||
lastTimestamp?: string;
|
||||
message?: string;
|
||||
reason?: string;
|
||||
related?: ObjectReference;
|
||||
reportingComponent?: string;
|
||||
reportingInstance?: string;
|
||||
series?: EventSeries;
|
||||
source?: EventSource;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export class KubeEvent extends KubeObject {
|
||||
@@ -39,14 +43,73 @@ export class KubeEvent extends KubeObject {
|
||||
static namespaced = true;
|
||||
static apiBase = "/api/v1/events";
|
||||
|
||||
action?: string;
|
||||
count?: number;
|
||||
eventTime?: string;
|
||||
firstTimestamp?: string;
|
||||
involvedObject: Required<ObjectReference>;
|
||||
lastTimestamp?: string;
|
||||
message?: string;
|
||||
reason?: string;
|
||||
related?: ObjectReference;
|
||||
reportingComponent?: string;
|
||||
reportingInstance?: string;
|
||||
series?: EventSeries;
|
||||
source?: EventSource;
|
||||
|
||||
/**
|
||||
* Current supported values are:
|
||||
* - "Normal"
|
||||
* - "Warning"
|
||||
*/
|
||||
type?: string;
|
||||
|
||||
constructor({
|
||||
action,
|
||||
count,
|
||||
eventTime,
|
||||
firstTimestamp,
|
||||
involvedObject,
|
||||
lastTimestamp,
|
||||
message,
|
||||
reason,
|
||||
related,
|
||||
reportingComponent,
|
||||
reportingInstance,
|
||||
series,
|
||||
source,
|
||||
type,
|
||||
...rest
|
||||
}: KubeEventData) {
|
||||
super(rest);
|
||||
this.action = action;
|
||||
this.count = count;
|
||||
this.eventTime = eventTime;
|
||||
this.firstTimestamp = firstTimestamp;
|
||||
this.involvedObject = involvedObject;
|
||||
this.lastTimestamp = lastTimestamp;
|
||||
this.message = message;
|
||||
this.reason = reason;
|
||||
this.related = related;
|
||||
this.reportingComponent = reportingComponent;
|
||||
this.reportingInstance = reportingInstance;
|
||||
this.series = series;
|
||||
this.source = source;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
isWarning() {
|
||||
return this.type === "Warning";
|
||||
}
|
||||
|
||||
getSource() {
|
||||
const { component, host } = this.source;
|
||||
if (!this.source?.component) {
|
||||
return "<unknown>";
|
||||
}
|
||||
|
||||
return `${component} ${host || ""}`;
|
||||
const { component, host = "" } = this.source;
|
||||
|
||||
return `${component} ${host}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,14 +131,11 @@ export class KubeEvent extends KubeObject {
|
||||
}
|
||||
}
|
||||
|
||||
let eventApi: KubeApi<KubeEvent>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
eventApi = new KubeApi<KubeEvent>({
|
||||
objectConstructor: KubeEvent,
|
||||
});
|
||||
export class KubeEventApi extends KubeApi<KubeEvent, KubeEventData> {
|
||||
constructor(opts: DerivedKubeApiOptions = {}) {
|
||||
super({
|
||||
objectConstructor: KubeEvent,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
eventApi,
|
||||
};
|
||||
@@ -7,7 +7,7 @@ import { compile } from "path-to-regexp";
|
||||
import { apiBase } from "../index";
|
||||
import { stringify } from "querystring";
|
||||
import type { RequestInit } from "node-fetch";
|
||||
import { autoBind, bifurcateArray } from "../../utils";
|
||||
import { autoBind, bifurcateArray, isDefined } from "../../utils";
|
||||
import Joi from "joi";
|
||||
|
||||
export type RepoHelmChartList = Record<string, RawHelmChart[]>;
|
||||
@@ -30,9 +30,9 @@ export async function listCharts(): Promise<HelmChart[]> {
|
||||
|
||||
return Object
|
||||
.values(data)
|
||||
.reduce((allCharts, repoCharts) => allCharts.concat(Object.values(repoCharts)), [])
|
||||
.reduce((allCharts, repoCharts) => allCharts.concat(Object.values(repoCharts)), new Array<RawHelmChart[]>())
|
||||
.map(([chart]) => HelmChart.create(chart, { onError: "log" }))
|
||||
.filter(Boolean);
|
||||
.filter(isDefined);
|
||||
}
|
||||
|
||||
export interface GetChartDetailsOptions {
|
||||
@@ -51,7 +51,7 @@ export async function getChartDetails(repo: string, name: string, { version, req
|
||||
const path = endpoint({ repo, name });
|
||||
|
||||
const { readme, ...data } = await apiBase.get<IHelmChartDetails>(`${path}?${stringify({ version })}`, undefined, reqInit);
|
||||
const versions = data.versions.map(version => HelmChart.create(version, { onError: "log" })).filter(Boolean);
|
||||
const versions = data.versions.map(version => HelmChart.create(version, { onError: "log" })).filter(isDefined);
|
||||
|
||||
return {
|
||||
readme,
|
||||
@@ -242,7 +242,7 @@ export interface RawHelmChartDependency {
|
||||
export type HelmChartDependency = Required<Omit<RawHelmChartDependency, "condition">>
|
||||
& Pick<RawHelmChartDependency, "condition">;
|
||||
|
||||
export interface HelmChart {
|
||||
export interface HelmChartData {
|
||||
apiVersion: string;
|
||||
name: string;
|
||||
version: string;
|
||||
@@ -266,8 +266,30 @@ export interface HelmChart {
|
||||
tillerVersion?: string;
|
||||
}
|
||||
|
||||
export class HelmChart {
|
||||
private constructor(value: HelmChart) {
|
||||
export class HelmChart implements HelmChartData {
|
||||
apiVersion: string;
|
||||
name: string;
|
||||
version: string;
|
||||
repo: string;
|
||||
created: string;
|
||||
description: string;
|
||||
keywords: string[];
|
||||
sources: string[];
|
||||
urls: string[];
|
||||
annotations: Record<string, string>;
|
||||
dependencies: HelmChartDependency[];
|
||||
maintainers: HelmChartMaintainer[];
|
||||
deprecated: boolean;
|
||||
kubeVersion?: string;
|
||||
digest?: string;
|
||||
home?: string;
|
||||
engine?: string;
|
||||
icon?: string;
|
||||
appVersion?: string;
|
||||
type?: string;
|
||||
tillerVersion?: string;
|
||||
|
||||
private constructor(value: HelmChart | HelmChartData) {
|
||||
this.apiVersion = value.apiVersion;
|
||||
this.name = value.name;
|
||||
this.version = value.version;
|
||||
@@ -294,25 +316,25 @@ export class HelmChart {
|
||||
}
|
||||
|
||||
static create(data: RawHelmChart, { onError = "throw" }: HelmChartCreateOpts = {}): HelmChart | undefined {
|
||||
const { value, error } = helmChartValidator.validate(data, {
|
||||
const result = helmChartValidator.validate(data, {
|
||||
abortEarly: false,
|
||||
});
|
||||
|
||||
if (!error) {
|
||||
return new HelmChart(value);
|
||||
if (!result.error) {
|
||||
return new HelmChart(result.value);
|
||||
}
|
||||
|
||||
const [actualErrors, unknownDetails] = bifurcateArray(error.details, ({ type }) => type === "object.unknown");
|
||||
const [actualErrors, unknownDetails] = bifurcateArray(result.error.details, ({ type }) => type === "object.unknown");
|
||||
|
||||
if (unknownDetails.length > 0) {
|
||||
console.warn("HelmChart data has unexpected fields", { original: data, unknownFields: unknownDetails.flatMap(d => d.path) });
|
||||
}
|
||||
|
||||
if (actualErrors.length === 0) {
|
||||
return new HelmChart(value);
|
||||
return new HelmChart(result.value as unknown as HelmChartData);
|
||||
}
|
||||
|
||||
const validationError = new Joi.ValidationError(actualErrors.map(er => er.message).join(". "), actualErrors, error._original);
|
||||
const validationError = new Joi.ValidationError(actualErrors.map(er => er.message).join(". "), actualErrors, result.error._original);
|
||||
|
||||
if (onError === "throw") {
|
||||
throw validationError;
|
||||
@@ -347,7 +369,7 @@ export class HelmChart {
|
||||
return this.icon;
|
||||
}
|
||||
|
||||
getHome(): string {
|
||||
getHome(): string | undefined {
|
||||
return this.home;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@ import capitalize from "lodash/capitalize";
|
||||
import { apiBase } from "../index";
|
||||
import { helmChartStore } from "../../../renderer/components/+helm-charts/helm-chart.store";
|
||||
import type { ItemObject } from "../../item.store";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { JsonApiData } from "../json-api";
|
||||
import { buildURLPositional } from "../../utils/buildUrl";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
|
||||
interface IReleasePayload {
|
||||
export interface HelmReleaseDetails {
|
||||
resources: KubeJsonApiData[];
|
||||
name: string;
|
||||
namespace: string;
|
||||
version: string;
|
||||
@@ -30,15 +30,7 @@ interface IReleasePayload {
|
||||
};
|
||||
}
|
||||
|
||||
interface IReleaseRawDetails extends IReleasePayload {
|
||||
resources: KubeJsonApiData[];
|
||||
}
|
||||
|
||||
export interface IReleaseDetails extends IReleasePayload {
|
||||
resources: KubeObject[];
|
||||
}
|
||||
|
||||
export interface IReleaseCreatePayload {
|
||||
export interface HelmReleaseCreatePayload {
|
||||
name?: string;
|
||||
repo: string;
|
||||
chart: string;
|
||||
@@ -47,19 +39,19 @@ export interface IReleaseCreatePayload {
|
||||
values: string;
|
||||
}
|
||||
|
||||
export interface IReleaseUpdatePayload {
|
||||
export interface HelmReleaseUpdatePayload {
|
||||
repo: string;
|
||||
chart: string;
|
||||
version: string;
|
||||
values: string;
|
||||
}
|
||||
|
||||
export interface IReleaseUpdateDetails {
|
||||
export interface HelmReleaseUpdateDetails {
|
||||
log: string;
|
||||
release: IReleaseDetails;
|
||||
release: HelmReleaseDetails;
|
||||
}
|
||||
|
||||
export interface IReleaseRevision {
|
||||
export interface HelmReleaseRevision {
|
||||
revision: number;
|
||||
updated: string;
|
||||
status: string;
|
||||
@@ -85,18 +77,13 @@ export async function listReleases(namespace?: string): Promise<HelmRelease[]> {
|
||||
return releases.map(toHelmRelease);
|
||||
}
|
||||
|
||||
export async function getRelease(name: string, namespace: string): Promise<IReleaseDetails> {
|
||||
export async function getRelease(name: string, namespace: string): Promise<HelmReleaseDetails> {
|
||||
const path = endpoint({ name, namespace });
|
||||
const { resources: rawResources, ...details } = await apiBase.get<IReleaseRawDetails>(path);
|
||||
const resources = rawResources.map(KubeObject.create);
|
||||
|
||||
return {
|
||||
...details,
|
||||
resources,
|
||||
};
|
||||
return apiBase.get(path);
|
||||
}
|
||||
|
||||
export async function createRelease(payload: IReleaseCreatePayload): Promise<IReleaseUpdateDetails> {
|
||||
export async function createRelease(payload: HelmReleaseCreatePayload): Promise<HelmReleaseUpdateDetails> {
|
||||
const { repo, chart: rawChart, values: rawValues, ...data } = payload;
|
||||
const chart = `${repo}/${rawChart}`;
|
||||
const values = yaml.load(rawValues);
|
||||
@@ -110,7 +97,7 @@ export async function createRelease(payload: IReleaseCreatePayload): Promise<IRe
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateRelease(name: string, namespace: string, payload: IReleaseUpdatePayload): Promise<IReleaseUpdateDetails> {
|
||||
export async function updateRelease(name: string, namespace: string, payload: HelmReleaseUpdatePayload): Promise<HelmReleaseUpdateDetails> {
|
||||
const { repo, chart: rawChart, values: rawValues, ...data } = payload;
|
||||
const chart = `${repo}/${rawChart}`;
|
||||
const values = yaml.load(rawValues);
|
||||
@@ -137,7 +124,7 @@ export async function getReleaseValues(name: string, namespace: string, all?: bo
|
||||
return apiBase.get<string>(path);
|
||||
}
|
||||
|
||||
export async function getReleaseHistory(name: string, namespace: string): Promise<IReleaseRevision[]> {
|
||||
export async function getReleaseHistory(name: string, namespace: string): Promise<HelmReleaseRevision[]> {
|
||||
const route = "history";
|
||||
const path = endpoint({ name, namespace, route });
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { HorizontalPodAutoscalerApi } from "./horizontal-pod-autoscaler.api";
|
||||
|
||||
const horizontalPodAutoscalerApiInjectable = getInjectable({
|
||||
id: "horizontal-pod-autoscaler-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "horizontalPodAutoscalerApi is only available in certain environments");
|
||||
|
||||
return new HorizontalPodAutoscalerApi();
|
||||
},
|
||||
});
|
||||
|
||||
export default horizontalPodAutoscalerApiInjectable;
|
||||
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { BaseKubeObjectCondition, KubeObjectScope, LabelSelector } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { DerivedKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { OptionVarient } from "../../utils";
|
||||
|
||||
export enum HpaMetricType {
|
||||
Resource = "Resource",
|
||||
Pods = "Pods",
|
||||
Object = "Object",
|
||||
External = "External",
|
||||
ContainerResource = "ContainerResource",
|
||||
}
|
||||
|
||||
export interface HorizontalPodAutoscalerMetricTarget {
|
||||
kind: string;
|
||||
name: string;
|
||||
apiVersion: string;
|
||||
}
|
||||
|
||||
export interface ContainerResourceMetricSource {
|
||||
container: string;
|
||||
name: string;
|
||||
targetAverageUtilization?: number;
|
||||
targetAverageValue?: string;
|
||||
}
|
||||
|
||||
export interface ExternalMetricSource {
|
||||
metricName: string;
|
||||
metricSelector?: LabelSelector;
|
||||
targetAverageValue?: string;
|
||||
targetValue?: string;
|
||||
}
|
||||
|
||||
export interface ObjectMetricSource {
|
||||
averageValue?: string;
|
||||
metricName: string;
|
||||
selector?: LabelSelector;
|
||||
target: CrossVersionObjectReference;
|
||||
targetValue: string;
|
||||
}
|
||||
|
||||
export interface PodsMetricSource {
|
||||
metricName: string;
|
||||
selector?: LabelSelector;
|
||||
targetAverageValue: string;
|
||||
}
|
||||
|
||||
export interface ResourceMetricSource {
|
||||
name: string;
|
||||
targetAverageUtilization?: number;
|
||||
targetAverageValue?: string;
|
||||
}
|
||||
|
||||
export interface BaseHorizontalPodAutoscalerMetricSpec {
|
||||
resource: ResourceMetricSource;
|
||||
object: ObjectMetricSource;
|
||||
external: ExternalMetricSource;
|
||||
pods: PodsMetricSource;
|
||||
containerResource: ContainerResourceMetricSource;
|
||||
}
|
||||
|
||||
export type HorizontalPodAutoscalerMetricSpec =
|
||||
| OptionVarient<HpaMetricType.Resource, BaseHorizontalPodAutoscalerMetricSpec, "resource">
|
||||
| OptionVarient<HpaMetricType.External, BaseHorizontalPodAutoscalerMetricSpec, "external">
|
||||
| OptionVarient<HpaMetricType.Object, BaseHorizontalPodAutoscalerMetricSpec, "object">
|
||||
| OptionVarient<HpaMetricType.Pods, BaseHorizontalPodAutoscalerMetricSpec, "pods">
|
||||
| OptionVarient<HpaMetricType.ContainerResource, BaseHorizontalPodAutoscalerMetricSpec, "containerResource">;
|
||||
|
||||
export interface CrossVersionObjectReference {
|
||||
kind: string;
|
||||
name: string;
|
||||
apiVersion: string;
|
||||
}
|
||||
|
||||
export interface HorizontalPodAutoscalerSpec {
|
||||
scaleTargetRef: CrossVersionObjectReference;
|
||||
minReplicas?: number;
|
||||
maxReplicas: number;
|
||||
metrics?: HorizontalPodAutoscalerMetricSpec[];
|
||||
}
|
||||
|
||||
export interface HorizontalPodAutoscalerStatus {
|
||||
conditions?: BaseKubeObjectCondition[];
|
||||
currentReplicas: number;
|
||||
desiredReplicas: number;
|
||||
currentMetrics: HorizontalPodAutoscalerMetricSpec[];
|
||||
}
|
||||
|
||||
interface MetricCurrentTarget {
|
||||
current?: string;
|
||||
target?: string;
|
||||
}
|
||||
|
||||
export class HorizontalPodAutoscaler extends KubeObject<HorizontalPodAutoscalerStatus, HorizontalPodAutoscalerSpec, KubeObjectScope.Namespace> {
|
||||
static readonly kind = "HorizontalPodAutoscaler";
|
||||
static readonly namespaced = true;
|
||||
static readonly apiBase = "/apis/autoscaling/v2beta1/horizontalpodautoscalers";
|
||||
|
||||
getMaxPods() {
|
||||
return this.spec.maxReplicas ?? 0;
|
||||
}
|
||||
|
||||
getMinPods() {
|
||||
return this.spec.minReplicas ?? 0;
|
||||
}
|
||||
|
||||
getReplicas() {
|
||||
return this.status?.currentReplicas ?? 0;
|
||||
}
|
||||
|
||||
getReadyConditions() {
|
||||
return this.getConditions().filter(({ isReady }) => isReady);
|
||||
}
|
||||
|
||||
getConditions() {
|
||||
return this.status?.conditions?.map(condition => {
|
||||
const { message, reason, lastTransitionTime, status } = condition;
|
||||
|
||||
return {
|
||||
...condition,
|
||||
isReady: status === "True",
|
||||
tooltip: `${message || reason} (${lastTransitionTime})`,
|
||||
};
|
||||
}) ?? [];
|
||||
}
|
||||
|
||||
getMetrics() {
|
||||
return this.spec.metrics ?? [];
|
||||
}
|
||||
|
||||
getCurrentMetrics() {
|
||||
return this.status?.currentMetrics ?? [];
|
||||
}
|
||||
|
||||
protected getMetricName(metric: HorizontalPodAutoscalerMetricSpec): string {
|
||||
switch (metric.type) {
|
||||
case HpaMetricType.Resource:
|
||||
return metric.resource.name;
|
||||
case HpaMetricType.Pods:
|
||||
return metric.pods.metricName;
|
||||
case HpaMetricType.Object:
|
||||
return metric.object.metricName;
|
||||
case HpaMetricType.External:
|
||||
return metric.external.metricName;
|
||||
case HpaMetricType.ContainerResource:
|
||||
return metric.containerResource.name;
|
||||
default:
|
||||
return `<unknown metric type: ${(metric as HorizontalPodAutoscalerMetricSpec).type}>`;
|
||||
}
|
||||
}
|
||||
|
||||
protected getResourceMetricValue(currentMetric: ResourceMetricSource | undefined, targetMetric: ResourceMetricSource): MetricCurrentTarget {
|
||||
return {
|
||||
current: (
|
||||
currentMetric?.targetAverageUtilization
|
||||
? `${currentMetric.targetAverageUtilization}%`
|
||||
: currentMetric?.targetAverageValue
|
||||
),
|
||||
target: (
|
||||
targetMetric?.targetAverageUtilization
|
||||
? `${targetMetric.targetAverageUtilization}%`
|
||||
: targetMetric?.targetAverageValue
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
protected getPodsMetricValue(currentMetric: PodsMetricSource | undefined, targetMetric: PodsMetricSource): MetricCurrentTarget {
|
||||
return {
|
||||
current: currentMetric?.targetAverageValue,
|
||||
target: targetMetric?.targetAverageValue,
|
||||
};
|
||||
}
|
||||
|
||||
protected getObjectMetricValue(currentMetric: ObjectMetricSource | undefined, targetMetric: ObjectMetricSource): MetricCurrentTarget {
|
||||
return {
|
||||
current: (
|
||||
currentMetric?.targetValue
|
||||
?? currentMetric?.averageValue
|
||||
),
|
||||
target: (
|
||||
targetMetric?.targetValue
|
||||
?? targetMetric?.averageValue
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
protected getExternalMetricValue(currentMetric: ExternalMetricSource | undefined, targetMetric: ExternalMetricSource): MetricCurrentTarget {
|
||||
return {
|
||||
current: (
|
||||
currentMetric?.targetValue
|
||||
?? currentMetric?.targetAverageValue
|
||||
),
|
||||
target: (
|
||||
targetMetric?.targetValue
|
||||
?? targetMetric?.targetAverageValue
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
protected getContainerResourceMetricValue(currentMetric: ContainerResourceMetricSource | undefined, targetMetric: ContainerResourceMetricSource): MetricCurrentTarget {
|
||||
return {
|
||||
current: (
|
||||
currentMetric?.targetAverageUtilization
|
||||
? `${currentMetric.targetAverageUtilization}%`
|
||||
: currentMetric?.targetAverageValue
|
||||
),
|
||||
target: (
|
||||
targetMetric?.targetAverageUtilization
|
||||
? `${targetMetric.targetAverageUtilization}%`
|
||||
: targetMetric?.targetAverageValue
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
protected getMetricCurrentTarget(metric: HorizontalPodAutoscalerMetricSpec): MetricCurrentTarget {
|
||||
const currentMetric = this.getMetrics()
|
||||
.find(m => (
|
||||
m.type === metric.type
|
||||
&& this.getMetricName(m) === this.getMetricName(metric)
|
||||
));
|
||||
|
||||
switch (metric.type) {
|
||||
case HpaMetricType.Resource:
|
||||
return this.getResourceMetricValue(currentMetric?.resource, metric.resource);
|
||||
case HpaMetricType.Pods:
|
||||
return this.getPodsMetricValue(currentMetric?.pods, metric.pods);
|
||||
case HpaMetricType.Object:
|
||||
return this.getObjectMetricValue(currentMetric?.object, metric.object);
|
||||
case HpaMetricType.External:
|
||||
return this.getExternalMetricValue(currentMetric?.external, metric.external);
|
||||
case HpaMetricType.ContainerResource:
|
||||
return this.getContainerResourceMetricValue(currentMetric?.containerResource, metric.containerResource);
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
getMetricValues(metric: HorizontalPodAutoscalerMetricSpec): string {
|
||||
const {
|
||||
current = "unknown",
|
||||
target = "unknown",
|
||||
} = this.getMetricCurrentTarget(metric);
|
||||
|
||||
return `${current} / ${target}`;
|
||||
}
|
||||
}
|
||||
|
||||
export class HorizontalPodAutoscalerApi extends KubeApi<HorizontalPodAutoscaler> {
|
||||
constructor(opts?: DerivedKubeApiOptions) {
|
||||
super({
|
||||
objectConstructor: HorizontalPodAutoscaler,
|
||||
...opts ?? {},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export enum HpaMetricType {
|
||||
Resource = "Resource",
|
||||
Pods = "Pods",
|
||||
Object = "Object",
|
||||
External = "External",
|
||||
}
|
||||
|
||||
export type IHpaMetricData<T = any> = T & {
|
||||
target?: {
|
||||
kind: string;
|
||||
name: string;
|
||||
apiVersion: string;
|
||||
};
|
||||
name?: string;
|
||||
metricName?: string;
|
||||
currentAverageUtilization?: number;
|
||||
currentAverageValue?: string;
|
||||
targetAverageUtilization?: number;
|
||||
targetAverageValue?: string;
|
||||
};
|
||||
|
||||
export interface IHpaMetric {
|
||||
[kind: string]: IHpaMetricData;
|
||||
|
||||
type: HpaMetricType;
|
||||
resource?: IHpaMetricData<{ name: string }>;
|
||||
pods?: IHpaMetricData;
|
||||
external?: IHpaMetricData;
|
||||
object?: IHpaMetricData<{
|
||||
describedObject: {
|
||||
apiVersion: string;
|
||||
kind: string;
|
||||
name: string;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface HorizontalPodAutoscaler {
|
||||
spec: {
|
||||
scaleTargetRef: {
|
||||
kind: string;
|
||||
name: string;
|
||||
apiVersion: string;
|
||||
};
|
||||
minReplicas: number;
|
||||
maxReplicas: number;
|
||||
metrics: IHpaMetric[];
|
||||
};
|
||||
status: {
|
||||
currentReplicas: number;
|
||||
desiredReplicas: number;
|
||||
currentMetrics: IHpaMetric[];
|
||||
conditions: {
|
||||
lastTransitionTime: string;
|
||||
message: string;
|
||||
reason: string;
|
||||
status: string;
|
||||
type: string;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
|
||||
export class HorizontalPodAutoscaler extends KubeObject {
|
||||
static kind = "HorizontalPodAutoscaler";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/autoscaling/v2beta1/horizontalpodautoscalers";
|
||||
|
||||
getMaxPods() {
|
||||
return this.spec.maxReplicas || 0;
|
||||
}
|
||||
|
||||
getMinPods() {
|
||||
return this.spec.minReplicas || 0;
|
||||
}
|
||||
|
||||
getReplicas() {
|
||||
return this.status.currentReplicas;
|
||||
}
|
||||
|
||||
getConditions() {
|
||||
if (!this.status.conditions) return [];
|
||||
|
||||
return this.status.conditions.map(condition => {
|
||||
const { message, reason, lastTransitionTime, status } = condition;
|
||||
|
||||
return {
|
||||
...condition,
|
||||
isReady: status === "True",
|
||||
tooltip: `${message || reason} (${lastTransitionTime})`,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
getMetrics() {
|
||||
return this.spec.metrics || [];
|
||||
}
|
||||
|
||||
getCurrentMetrics() {
|
||||
return this.status.currentMetrics || [];
|
||||
}
|
||||
|
||||
protected getMetricName(metric: IHpaMetric): string {
|
||||
const { type, resource, pods, object, external } = metric;
|
||||
|
||||
switch (type) {
|
||||
case HpaMetricType.Resource:
|
||||
return resource.name;
|
||||
case HpaMetricType.Pods:
|
||||
return pods.metricName;
|
||||
case HpaMetricType.Object:
|
||||
return object.metricName;
|
||||
case HpaMetricType.External:
|
||||
return external.metricName;
|
||||
}
|
||||
}
|
||||
|
||||
// todo: refactor
|
||||
getMetricValues(metric: IHpaMetric): string {
|
||||
const metricType = metric.type.toLowerCase();
|
||||
const currentMetric = this.getCurrentMetrics().find(current =>
|
||||
metric.type == current.type && this.getMetricName(metric) == this.getMetricName(current),
|
||||
);
|
||||
const current = currentMetric ? currentMetric[metricType] : null;
|
||||
const target = metric[metricType];
|
||||
let currentValue = "unknown";
|
||||
let targetValue = "unknown";
|
||||
|
||||
if (current) {
|
||||
currentValue = current.currentAverageUtilization || current.currentAverageValue || current.currentValue;
|
||||
if (current.currentAverageUtilization) currentValue += "%";
|
||||
}
|
||||
|
||||
if (target) {
|
||||
targetValue = target.targetAverageUtilization || target.targetAverageValue || target.targetValue;
|
||||
if (target.targetAverageUtilization) targetValue += "%";
|
||||
}
|
||||
|
||||
return `${currentValue} / ${targetValue}`;
|
||||
}
|
||||
}
|
||||
|
||||
let hpaApi: KubeApi<HorizontalPodAutoscaler>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
hpaApi = new KubeApi<HorizontalPodAutoscaler>({
|
||||
objectConstructor: HorizontalPodAutoscaler,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
hpaApi,
|
||||
};
|
||||
@@ -9,33 +9,34 @@
|
||||
export * from "./cluster.api";
|
||||
export * from "./cluster-role.api";
|
||||
export * from "./cluster-role-binding.api";
|
||||
export * from "./configmap.api";
|
||||
export * from "./crd.api";
|
||||
export * from "./config-map.api";
|
||||
export * from "./custom-resource-definition.api";
|
||||
export * from "./cron-job.api";
|
||||
export * from "./daemon-set.api";
|
||||
export * from "./deployment.api";
|
||||
export * from "./endpoint.api";
|
||||
export * from "./events.api";
|
||||
export * from "./hpa.api";
|
||||
export * from "./horizontal-pod-autoscaler.api";
|
||||
export * from "./ingress.api";
|
||||
export * from "./job.api";
|
||||
export * from "./limit-range.api";
|
||||
export * from "./namespaces.api";
|
||||
export * from "./namespace.api";
|
||||
export * from "./network-policy.api";
|
||||
export * from "./nodes.api";
|
||||
export * from "./node.api";
|
||||
export * from "./persistent-volume.api";
|
||||
export * from "./persistent-volume-claims.api";
|
||||
export * from "./pods.api";
|
||||
export * from "./poddisruptionbudget.api";
|
||||
export * from "./persistent-volume-claim.api";
|
||||
export * from "./pod.api";
|
||||
export * from "./pod-disruption-budget.api";
|
||||
export * from "./pod-metrics.api";
|
||||
export * from "./podsecuritypolicy.api";
|
||||
export * from "./pod-security-policy.api";
|
||||
export * from "./replica-set.api";
|
||||
export * from "./resource-quota.api";
|
||||
export * from "./role.api";
|
||||
export * from "./role-binding.api";
|
||||
export * from "./secret.api";
|
||||
export * from "./selfsubjectrulesreviews.api";
|
||||
export * from "./self-subject-rules-reviews.api";
|
||||
export * from "./service.api";
|
||||
export * from "./service-accounts.api";
|
||||
export * from "./service-account.api";
|
||||
export * from "./stateful-set.api";
|
||||
export * from "./storage-class.api";
|
||||
export * from "./legacy-globals";
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { IngressApi } from "./ingress.api";
|
||||
|
||||
const ingressApiInjectable = getInjectable({
|
||||
id: "ingress-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "ingressApi is only available in certain environments");
|
||||
|
||||
return new IngressApi();
|
||||
},
|
||||
});
|
||||
|
||||
export default ingressApiInjectable;
|
||||
@@ -3,20 +3,28 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { TypedLocalObjectReference } from "../kube-object";
|
||||
import type { KubeObjectScope, TypedLocalObjectReference } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { autoBind, hasTypedProperty, isString, iter } from "../../utils";
|
||||
import type { IMetrics } from "./metrics.api";
|
||||
import { hasTypedProperty, isString, iter } from "../../utils";
|
||||
import type { MetricData } from "./metrics.api";
|
||||
import { metricsApi } from "./metrics.api";
|
||||
import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import type { RequireExactlyOne } from "type-fest";
|
||||
|
||||
export class IngressApi extends KubeApi<Ingress> {
|
||||
constructor(opts: DerivedKubeApiOptions & IgnoredKubeApiOptions = {}) {
|
||||
super({
|
||||
...opts,
|
||||
objectConstructor: Ingress,
|
||||
// Add fallback for Kubernetes <1.19
|
||||
checkPreferredVersion: true,
|
||||
fallbackApiBases: ["/apis/extensions/v1beta1/ingresses"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getMetricsForIngress(ingress: string, namespace: string): Promise<IIngressMetrics> {
|
||||
export function getMetricsForIngress(ingress: string, namespace: string): Promise<IngressMetricData> {
|
||||
const opts = { category: "ingress", ingress, namespace };
|
||||
|
||||
return metricsApi.getMetrics({
|
||||
@@ -29,12 +37,11 @@ export function getMetricsForIngress(ingress: string, namespace: string): Promis
|
||||
});
|
||||
}
|
||||
|
||||
export interface IIngressMetrics<T = IMetrics> {
|
||||
[metric: string]: T;
|
||||
bytesSentSuccess: T;
|
||||
bytesSentFailure: T;
|
||||
requestDurationSeconds: T;
|
||||
responseDurationSeconds: T;
|
||||
export interface IngressMetricData extends Partial<Record<string, MetricData>> {
|
||||
bytesSentSuccess: MetricData;
|
||||
bytesSentFailure: MetricData;
|
||||
requestDurationSeconds: MetricData;
|
||||
responseDurationSeconds: MetricData;
|
||||
}
|
||||
|
||||
export interface ILoadBalancerIngress {
|
||||
@@ -73,7 +80,11 @@ function isExtensionsBackend(backend: IngressBackend): backend is ExtensionsBack
|
||||
* Format an ingress backend into the name of the service and port
|
||||
* @param backend The ingress target
|
||||
*/
|
||||
export function getBackendServiceNamePort(backend: IngressBackend): string {
|
||||
export function getBackendServiceNamePort(backend: IngressBackend | undefined): string {
|
||||
if (!backend) {
|
||||
return "<unknown>";
|
||||
}
|
||||
|
||||
if (isExtensionsBackend(backend)) {
|
||||
return `${backend.serviceName}:${backend.servicePort}`;
|
||||
}
|
||||
@@ -87,60 +98,52 @@ export function getBackendServiceNamePort(backend: IngressBackend): string {
|
||||
return "<unknown>";
|
||||
}
|
||||
|
||||
export interface HTTPIngressPath {
|
||||
pathType: "Exact" | "Prefix" | "ImplementationSpecific";
|
||||
path?: string;
|
||||
backend?: IngressBackend;
|
||||
}
|
||||
|
||||
export interface HTTPIngressRuleValue {
|
||||
paths: HTTPIngressPath[];
|
||||
}
|
||||
|
||||
export interface IngressRule {
|
||||
host?: string;
|
||||
http?: {
|
||||
paths: {
|
||||
path?: string;
|
||||
backend: IngressBackend;
|
||||
}[];
|
||||
};
|
||||
http?: HTTPIngressRuleValue;
|
||||
}
|
||||
|
||||
export interface Ingress {
|
||||
spec?: {
|
||||
tls?: {
|
||||
secretName: string;
|
||||
}[];
|
||||
rules?: IngressRule[];
|
||||
// extensions/v1beta1
|
||||
backend?: ExtensionsBackend;
|
||||
/**
|
||||
* The default backend which is exactly on of:
|
||||
* - service
|
||||
* - resource
|
||||
*/
|
||||
defaultBackend?: RequireExactlyOne<NetworkingBackend & {
|
||||
resource: {
|
||||
apiGroup: string;
|
||||
kind: string;
|
||||
name: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
status: {
|
||||
loadBalancer: {
|
||||
ingress: ILoadBalancerIngress[];
|
||||
export interface IngressSpec {
|
||||
tls: {
|
||||
secretName: string;
|
||||
}[];
|
||||
rules?: IngressRule[];
|
||||
// extensions/v1beta1
|
||||
backend?: ExtensionsBackend;
|
||||
/**
|
||||
* The default backend which is exactly on of:
|
||||
* - service
|
||||
* - resource
|
||||
*/
|
||||
defaultBackend?: RequireExactlyOne<NetworkingBackend & {
|
||||
resource: {
|
||||
apiGroup: string;
|
||||
kind: string;
|
||||
name: string;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface IngressStatus {
|
||||
loadBalancer: {
|
||||
ingress: ILoadBalancerIngress[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ComputedIngressRoute {
|
||||
displayAsLink: boolean;
|
||||
pathname: string;
|
||||
url: string;
|
||||
service: string;
|
||||
}
|
||||
|
||||
export class Ingress extends KubeObject {
|
||||
static kind = "Ingress";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/networking.k8s.io/v1/ingresses";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
export class Ingress extends KubeObject<IngressStatus, IngressSpec, KubeObjectScope.Namespace> {
|
||||
static readonly kind = "Ingress";
|
||||
static readonly namespaced = true;
|
||||
static readonly apiBase = "/apis/networking.k8s.io/v1/ingresses";
|
||||
|
||||
getRules() {
|
||||
return this.spec.rules ?? [];
|
||||
@@ -150,12 +153,16 @@ export class Ingress extends KubeObject {
|
||||
return computeRouteDeclarations(this).map(({ url, service }) => `${url} ⇢ ${service}`);
|
||||
}
|
||||
|
||||
getServiceNamePort(): ExtensionsBackend {
|
||||
getServiceNamePort(): ExtensionsBackend | undefined {
|
||||
const { spec: { backend, defaultBackend } = {}} = this;
|
||||
|
||||
const serviceName = defaultBackend?.service?.name ?? backend?.serviceName;
|
||||
const servicePort = defaultBackend?.service?.port.number ?? defaultBackend?.service?.port.name ?? backend?.servicePort;
|
||||
|
||||
if (!serviceName || !servicePort) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
serviceName,
|
||||
servicePort,
|
||||
@@ -170,14 +177,14 @@ export class Ingress extends KubeObject {
|
||||
|
||||
getPorts() {
|
||||
const ports: number[] = [];
|
||||
const { spec: { tls, rules, backend, defaultBackend }} = this;
|
||||
const { spec: { tls, rules = [], backend, defaultBackend }} = this;
|
||||
const httpPort = 80;
|
||||
const tlsPort = 443;
|
||||
// Note: not using the port name (string)
|
||||
const servicePort = defaultBackend?.service?.port.number ?? backend?.servicePort;
|
||||
|
||||
if (rules && rules.length > 0) {
|
||||
if (rules.some(rule => Object.prototype.hasOwnProperty.call(rule, "http"))) {
|
||||
if (rules.length > 0) {
|
||||
if (rules.some(rule => rule.http)) {
|
||||
ports.push(httpPort);
|
||||
}
|
||||
} else if (servicePort !== undefined) {
|
||||
@@ -192,14 +199,19 @@ export class Ingress extends KubeObject {
|
||||
}
|
||||
|
||||
getLoadBalancers() {
|
||||
const { status: { loadBalancer = { ingress: [] }}} = this;
|
||||
|
||||
return (loadBalancer.ingress ?? []).map(address => (
|
||||
return this.status?.loadBalancer.ingress.map(address => (
|
||||
address.hostname || address.ip
|
||||
));
|
||||
)) ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
export interface ComputedIngressRoute {
|
||||
displayAsLink: boolean;
|
||||
pathname: string;
|
||||
url: string;
|
||||
service: string;
|
||||
}
|
||||
|
||||
export function computeRuleDeclarations(ingress: Ingress, rule: IngressRule): ComputedIngressRoute[] {
|
||||
const { host = "*", http: { paths } = { paths: [] }} = rule;
|
||||
const protocol = (ingress.spec?.tls?.length ?? 0) === 0
|
||||
@@ -217,18 +229,3 @@ export function computeRuleDeclarations(ingress: Ingress, rule: IngressRule): Co
|
||||
export function computeRouteDeclarations(ingress: Ingress): ComputedIngressRoute[] {
|
||||
return ingress.getRules().flatMap(rule => computeRuleDeclarations(ingress, rule));
|
||||
}
|
||||
|
||||
let ingressApi: IngressApi;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
ingressApi = new IngressApi({
|
||||
objectConstructor: Ingress,
|
||||
// Add fallback for Kubernetes <1.19
|
||||
checkPreferredVersion: true,
|
||||
fallbackApiBases: ["/apis/extensions/v1beta1/ingresses"],
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
ingressApi,
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { JobApi } from "./job.api";
|
||||
|
||||
const jobApiInjectable = getInjectable({
|
||||
id: "job-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "jobApi is only available in certain environments");
|
||||
|
||||
return new JobApi();
|
||||
},
|
||||
});
|
||||
|
||||
export default jobApiInjectable;
|
||||
@@ -3,88 +3,76 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import get from "lodash/get";
|
||||
import { autoBind } from "../../utils";
|
||||
import type { IAffinity } from "../workload-kube-object";
|
||||
import { WorkloadKubeObject } from "../workload-kube-object";
|
||||
import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { metricsApi } from "./metrics.api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import type { IPodContainer, IPodMetrics } from "./pods.api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import type { LabelSelector } from "../kube-object";
|
||||
import type { PodContainer, PodMetricData, PodSpec } from "./pod.api";
|
||||
import type { KubeObjectScope, KubeObjectStatus, LabelSelector } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
|
||||
export class Job extends WorkloadKubeObject {
|
||||
static kind = "Job";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/batch/v1/jobs";
|
||||
export interface JobSpec {
|
||||
parallelism?: number;
|
||||
completions?: number;
|
||||
backoffLimit?: number;
|
||||
selector?: LabelSelector;
|
||||
template: {
|
||||
metadata: {
|
||||
creationTimestamp?: string;
|
||||
labels?: Partial<Record<string, string>>;
|
||||
annotations?: Partial<Record<string, string>>;
|
||||
};
|
||||
spec: PodSpec;
|
||||
};
|
||||
containers?: PodContainer[];
|
||||
restartPolicy?: string;
|
||||
terminationGracePeriodSeconds?: number;
|
||||
dnsPolicy?: string;
|
||||
serviceAccountName?: string;
|
||||
serviceAccount?: string;
|
||||
schedulerName?: string;
|
||||
}
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
export interface JobStatus extends KubeObjectStatus {
|
||||
startTime: string;
|
||||
completionTime: string;
|
||||
succeeded: number;
|
||||
}
|
||||
|
||||
export class Job extends KubeObject<JobStatus, JobSpec, KubeObjectScope.Namespace> {
|
||||
static readonly kind = "Job";
|
||||
static readonly namespaced = true;
|
||||
static readonly apiBase = "/apis/batch/v1/jobs";
|
||||
|
||||
getSelectors(): string[] {
|
||||
return KubeObject.stringifyLabels(this.spec.selector?.matchLabels);
|
||||
}
|
||||
|
||||
declare spec: {
|
||||
parallelism?: number;
|
||||
completions?: number;
|
||||
backoffLimit?: number;
|
||||
selector?: LabelSelector;
|
||||
template: {
|
||||
metadata: {
|
||||
creationTimestamp?: string;
|
||||
labels?: {
|
||||
[name: string]: string;
|
||||
};
|
||||
annotations?: {
|
||||
[name: string]: string;
|
||||
};
|
||||
};
|
||||
spec: {
|
||||
containers: IPodContainer[];
|
||||
restartPolicy: string;
|
||||
terminationGracePeriodSeconds: number;
|
||||
dnsPolicy: string;
|
||||
hostPID: boolean;
|
||||
affinity?: IAffinity;
|
||||
nodeSelector?: {
|
||||
[selector: string]: string;
|
||||
};
|
||||
tolerations?: {
|
||||
key: string;
|
||||
operator: string;
|
||||
effect: string;
|
||||
tolerationSeconds: number;
|
||||
}[];
|
||||
schedulerName: string;
|
||||
};
|
||||
};
|
||||
containers?: IPodContainer[];
|
||||
restartPolicy?: string;
|
||||
terminationGracePeriodSeconds?: number;
|
||||
dnsPolicy?: string;
|
||||
serviceAccountName?: string;
|
||||
serviceAccount?: string;
|
||||
schedulerName?: string;
|
||||
};
|
||||
declare status: {
|
||||
conditions: {
|
||||
type: string;
|
||||
status: string;
|
||||
lastProbeTime: string;
|
||||
lastTransitionTime: string;
|
||||
message?: string;
|
||||
}[];
|
||||
startTime: string;
|
||||
completionTime: string;
|
||||
succeeded: number;
|
||||
};
|
||||
getNodeSelectors(): string[] {
|
||||
return KubeObject.stringifyLabels(this.spec.template.spec.nodeSelector);
|
||||
}
|
||||
|
||||
getTemplateLabels(): string[] {
|
||||
return KubeObject.stringifyLabels(this.spec.template.metadata.labels);
|
||||
}
|
||||
|
||||
getTolerations() {
|
||||
return this.spec.template.spec.tolerations ?? [];
|
||||
}
|
||||
|
||||
getAffinity() {
|
||||
return this.spec.template.spec.affinity;
|
||||
}
|
||||
|
||||
getAffinityNumber() {
|
||||
return Object.keys(this.getAffinity() ?? {}).length;
|
||||
}
|
||||
|
||||
getDesiredCompletions() {
|
||||
return this.spec.completions || 0;
|
||||
return this.spec.completions ?? 0;
|
||||
}
|
||||
|
||||
getCompletions() {
|
||||
return this.status.succeeded || 0;
|
||||
return this.status?.succeeded ?? 0;
|
||||
}
|
||||
|
||||
getParallelism() {
|
||||
@@ -93,21 +81,25 @@ export class Job extends WorkloadKubeObject {
|
||||
|
||||
getCondition() {
|
||||
// Type of Job condition could be only Complete or Failed
|
||||
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.14/#jobcondition-v1-batch
|
||||
return this.status.conditions?.find(({ status }) => status === "True");
|
||||
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.21/#jobcondition-v1-batch
|
||||
return this.status?.conditions?.find(({ status }) => status === "True");
|
||||
}
|
||||
|
||||
getImages() {
|
||||
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []);
|
||||
|
||||
return [...containers].map(container => container.image);
|
||||
return this.spec.template.spec.containers?.map(container => container.image) ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
export class JobApi extends KubeApi<Job> {
|
||||
constructor(opts: DerivedKubeApiOptions & IgnoredKubeApiOptions = {}) {
|
||||
super({
|
||||
...opts,
|
||||
objectConstructor: Job,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getMetricsForJobs(jobs: Job[], namespace: string, selector = ""): Promise<IPodMetrics> {
|
||||
export function getMetricsForJobs(jobs: Job[], namespace: string, selector = ""): Promise<PodMetricData> {
|
||||
const podSelector = jobs.map(job => `${job.getName()}-[[:alnum:]]{5}`).join("|");
|
||||
const opts = { category: "pods", pods: podSelector, namespace, selector };
|
||||
|
||||
@@ -123,15 +115,3 @@ export function getMetricsForJobs(jobs: Job[], namespace: string, selector = "")
|
||||
namespace,
|
||||
});
|
||||
}
|
||||
|
||||
let jobApi: JobApi;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
jobApi = new JobApi({
|
||||
objectConstructor: Job,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
jobApi,
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { asLegacyGlobalForExtensionApi } from "../../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
||||
import configMapApiInjectable from "./config-map.api.injectable";
|
||||
import cronJobApiInjectable from "./cron-job.api.injectable";
|
||||
import jobApiInjectable from "./job.api.injectable";
|
||||
import networkPolicyApiInjectable from "./network-policy.api.injectable";
|
||||
import nodeApiInjectable from "./node.api.injectable";
|
||||
import persistentVolumeClaimApiInjectable from "./persistent-volume-claim.api.injectable";
|
||||
import podApiInjectable from "./pod.api.injectable";
|
||||
import resourceQuotaApiInjectable from "./resource-quota.api.injectable";
|
||||
import roleApiInjectable from "./role.api.injectable";
|
||||
import secretApiInjectable from "./secret.api.injectable";
|
||||
import serviceApiInjectable from "./service.api.injectable";
|
||||
import storageClassApiInjectable from "./storage-class.api.injectable";
|
||||
|
||||
/**
|
||||
* @deprecated use `di.inject(roleApiInjectable)` instead
|
||||
*/
|
||||
export const roleApi = asLegacyGlobalForExtensionApi(roleApiInjectable);
|
||||
|
||||
/**
|
||||
* @deprecated use `di.inject(podApiInjectable)` instead
|
||||
*/
|
||||
export const podApi = asLegacyGlobalForExtensionApi(podApiInjectable);
|
||||
|
||||
/**
|
||||
* @deprecated use `di.inject(cronJobApiInjectable)` instead
|
||||
*/
|
||||
export const cronJobApi = asLegacyGlobalForExtensionApi(cronJobApiInjectable);
|
||||
|
||||
/**
|
||||
* @deprecated use `di.inject(jobApiInjectable)` instead
|
||||
*/
|
||||
export const jobApi = asLegacyGlobalForExtensionApi(jobApiInjectable);
|
||||
|
||||
/**
|
||||
* @deprecated use `di.inject(configMapApiInjectable)` instead
|
||||
*/
|
||||
export const configMapApi = asLegacyGlobalForExtensionApi(configMapApiInjectable);
|
||||
|
||||
/**
|
||||
* @deprecated use `di.inject(networkPolicyApiInjectable)` instead
|
||||
*/
|
||||
export const networkPolicyApi = asLegacyGlobalForExtensionApi(networkPolicyApiInjectable);
|
||||
|
||||
/**
|
||||
* @deprecated use `di.inject(nodeApiInjectable)` instead
|
||||
*/
|
||||
export const nodeApi = asLegacyGlobalForExtensionApi(nodeApiInjectable);
|
||||
|
||||
/**
|
||||
* @deprecated use `di.inject(persistentVolumeClaimApiInjectable)` instead
|
||||
*/
|
||||
export const persistentVolumeClaimApi = asLegacyGlobalForExtensionApi(persistentVolumeClaimApiInjectable);
|
||||
|
||||
/**
|
||||
* @deprecated use `di.inject(resourceQuotaApiInjectable)` instead
|
||||
*/
|
||||
export const resourceQuotaApi = asLegacyGlobalForExtensionApi(resourceQuotaApiInjectable);
|
||||
|
||||
/**
|
||||
* @deprecated use `di.inject(secretApiInjectable)` instead
|
||||
*/
|
||||
export const secretApi = asLegacyGlobalForExtensionApi(secretApiInjectable);
|
||||
|
||||
/**
|
||||
* @deprecated use `di.inject(serviceApiInjectable)` instead
|
||||
*/
|
||||
export const serviceApi = asLegacyGlobalForExtensionApi(serviceApiInjectable);
|
||||
|
||||
/**
|
||||
* @deprecated use `di.inject(storageClassApiInjectable)` instead
|
||||
*/
|
||||
export const storageClassApi = asLegacyGlobalForExtensionApi(storageClassApiInjectable);
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { LimitRangeApi } from "./limit-range.api";
|
||||
|
||||
const limitRangeApiInjectable = getInjectable({
|
||||
id: "limit-range-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "limitRangeApi is only available in certain environments");
|
||||
|
||||
return new LimitRangeApi();
|
||||
},
|
||||
});
|
||||
|
||||
export default limitRangeApiInjectable;
|
||||
@@ -3,11 +3,10 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { KubeObjectScope } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { DerivedKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { autoBind } from "../../utils";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export enum LimitType {
|
||||
CONTAINER = "Container",
|
||||
@@ -36,21 +35,14 @@ export interface LimitRangeItem extends LimitRangeParts {
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface LimitRange {
|
||||
spec: {
|
||||
limits: LimitRangeItem[];
|
||||
};
|
||||
export interface LimitRangeSpec {
|
||||
limits: LimitRangeItem[];
|
||||
}
|
||||
|
||||
export class LimitRange extends KubeObject {
|
||||
static kind = "LimitRange";
|
||||
static namespaced = true;
|
||||
static apiBase = "/api/v1/limitranges";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
export class LimitRange extends KubeObject<void, LimitRangeSpec, KubeObjectScope.Namespace> {
|
||||
static readonly kind = "LimitRange";
|
||||
static readonly namespaced = true;
|
||||
static readonly apiBase = "/api/v1/limitranges";
|
||||
|
||||
getContainerLimits() {
|
||||
return this.spec.limits.filter(limit => limit.type === LimitType.CONTAINER);
|
||||
@@ -65,14 +57,11 @@ export class LimitRange extends KubeObject {
|
||||
}
|
||||
}
|
||||
|
||||
let limitRangeApi: KubeApi<LimitRange>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
limitRangeApi = new KubeApi<LimitRange>({
|
||||
objectConstructor: LimitRange,
|
||||
});
|
||||
export class LimitRangeApi extends KubeApi<LimitRange> {
|
||||
constructor(opts?: DerivedKubeApiOptions) {
|
||||
super({
|
||||
objectConstructor: LimitRange,
|
||||
...opts ?? {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
limitRangeApi,
|
||||
};
|
||||
@@ -9,18 +9,18 @@ import moment from "moment";
|
||||
import { apiBase } from "../index";
|
||||
import type { IMetricsQuery } from "../../../main/routes/metrics/metrics-query";
|
||||
|
||||
export interface IMetrics {
|
||||
export interface MetricData {
|
||||
status: string;
|
||||
data: {
|
||||
resultType: string;
|
||||
result: IMetricsResult[];
|
||||
result: MetricResult[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface IMetricsResult {
|
||||
export interface MetricResult {
|
||||
metric: {
|
||||
[name: string]: string;
|
||||
instance: string;
|
||||
[name: string]: string | undefined;
|
||||
instance?: string;
|
||||
node?: string;
|
||||
pod?: string;
|
||||
kubernetes?: string;
|
||||
@@ -44,7 +44,7 @@ export interface IMetricsReqParams {
|
||||
namespace?: string; // rbac-proxy validation param
|
||||
}
|
||||
|
||||
export interface IResourceMetrics<T extends IMetrics> {
|
||||
export interface IResourceMetrics<T extends MetricData> {
|
||||
[metric: string]: T;
|
||||
cpuUsage: T;
|
||||
memoryUsage: T;
|
||||
@@ -56,7 +56,7 @@ export interface IResourceMetrics<T extends IMetrics> {
|
||||
}
|
||||
|
||||
export const metricsApi = {
|
||||
async getMetrics<T = IMetricsQuery>(query: T, reqParams: IMetricsReqParams = {}): Promise<T extends object ? { [K in keyof T]: IMetrics } : IMetrics> {
|
||||
async getMetrics<T = IMetricsQuery>(query: T, reqParams: IMetricsReqParams = {}): Promise<T extends object ? { [K in keyof T]: MetricData } : MetricData> {
|
||||
const { range = 3600, step = 60, namespace } = reqParams;
|
||||
let { start, end } = reqParams;
|
||||
|
||||
@@ -82,7 +82,7 @@ export const metricsApi = {
|
||||
},
|
||||
};
|
||||
|
||||
export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics {
|
||||
export function normalizeMetrics(metrics: MetricData | undefined | null, frames = 60): MetricData {
|
||||
if (!metrics?.data?.result) {
|
||||
return {
|
||||
data: {
|
||||
@@ -90,7 +90,7 @@ export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics {
|
||||
result: [{
|
||||
metric: {},
|
||||
values: [],
|
||||
} as IMetricsResult],
|
||||
}],
|
||||
},
|
||||
status: "",
|
||||
};
|
||||
@@ -131,17 +131,17 @@ export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics {
|
||||
result.push({
|
||||
metric: {},
|
||||
values: [],
|
||||
} as IMetricsResult);
|
||||
} as MetricResult);
|
||||
}
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
export function isMetricsEmpty(metrics: Record<string, IMetrics>) {
|
||||
export function isMetricsEmpty(metrics: Partial<Record<string, MetricData>>) {
|
||||
return Object.values(metrics).every(metric => !metric?.data?.result?.length);
|
||||
}
|
||||
|
||||
export function getItemMetrics(metrics: Record<string, IMetrics>, itemName: string): Record<string, IMetrics> | undefined {
|
||||
export function getItemMetrics(metrics: Partial<Record<string, MetricData>> | null | undefined, itemName: string): Partial<Record<string, MetricData>> | undefined {
|
||||
if (!metrics) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -152,23 +152,24 @@ export function getItemMetrics(metrics: Record<string, IMetrics>, itemName: stri
|
||||
if (!metrics[metric]?.data?.result) {
|
||||
continue;
|
||||
}
|
||||
const results = metrics[metric].data.result;
|
||||
const result = results.find(res => Object.values(res.metric)[0] == itemName);
|
||||
const results = metrics[metric]?.data.result;
|
||||
const result = results?.find(res => Object.values(res.metric)[0] == itemName);
|
||||
|
||||
itemMetrics[metric].data.result = result ? [result] : [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
itemMetrics[metric]!.data.result = result ? [result] : [];
|
||||
}
|
||||
|
||||
return itemMetrics;
|
||||
}
|
||||
|
||||
export function getMetricLastPoints(metrics: Record<string, IMetrics>) {
|
||||
export function getMetricLastPoints<T extends Partial<Record<string, MetricData>>>(metrics: T): Record<keyof T, number> {
|
||||
const result: Partial<{ [metric: string]: number }> = {};
|
||||
|
||||
Object.keys(metrics).forEach(metricName => {
|
||||
try {
|
||||
const metric = metrics[metricName];
|
||||
|
||||
if (metric.data.result.length) {
|
||||
if (metric?.data.result.length) {
|
||||
result[metricName] = +metric.data.result[0].values.slice(-1)[0][1];
|
||||
}
|
||||
} catch {
|
||||
@@ -178,5 +179,5 @@ export function getMetricLastPoints(metrics: Record<string, IMetrics>) {
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
return result;
|
||||
return result as Record<keyof T, number>;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { NamespaceApi } from "./namespace.api";
|
||||
|
||||
const namespaceApiInjectable = getInjectable({
|
||||
id: "namespace-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "namespaceApi is only available in certain environments");
|
||||
|
||||
return new NamespaceApi();
|
||||
},
|
||||
});
|
||||
|
||||
export default namespaceApiInjectable;
|
||||
+20
-30
@@ -3,34 +3,30 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeObjectScope, KubeObjectStatus } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { autoBind } from "../../../renderer/utils";
|
||||
import { metricsApi } from "./metrics.api";
|
||||
import type { IPodMetrics } from "./pods.api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import type { PodMetricData } from "./pod.api";
|
||||
|
||||
export enum NamespaceStatus {
|
||||
export enum NamespaceStatusKind {
|
||||
ACTIVE = "Active",
|
||||
TERMINATING = "Terminating",
|
||||
}
|
||||
|
||||
export interface Namespace {
|
||||
status?: {
|
||||
phase: string;
|
||||
};
|
||||
export interface NamespaceSpec {
|
||||
finalizers?: string[];
|
||||
}
|
||||
|
||||
export class Namespace extends KubeObject {
|
||||
static kind = "Namespace";
|
||||
static namespaced = false;
|
||||
static apiBase = "/api/v1/namespaces";
|
||||
export interface NamespaceStatus extends KubeObjectStatus {
|
||||
phase?: string;
|
||||
}
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
export class Namespace extends KubeObject<NamespaceStatus, NamespaceSpec, KubeObjectScope.Cluster> {
|
||||
static readonly kind = "Namespace";
|
||||
static readonly namespaced = false;
|
||||
static readonly apiBase = "/api/v1/namespaces";
|
||||
|
||||
getStatus() {
|
||||
return this.status?.phase ?? "-";
|
||||
@@ -38,9 +34,15 @@ export class Namespace extends KubeObject {
|
||||
}
|
||||
|
||||
export class NamespaceApi extends KubeApi<Namespace> {
|
||||
constructor(opts: DerivedKubeApiOptions & IgnoredKubeApiOptions = {}) {
|
||||
super({
|
||||
...opts,
|
||||
objectConstructor: Namespace,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getMetricsForNamespace(namespace: string, selector = ""): Promise<IPodMetrics> {
|
||||
export function getMetricsForNamespace(namespace: string, selector = ""): Promise<PodMetricData> {
|
||||
const opts = { category: "pods", pods: ".*", namespace, selector };
|
||||
|
||||
return metricsApi.getMetrics({
|
||||
@@ -55,15 +57,3 @@ export function getMetricsForNamespace(namespace: string, selector = ""): Promis
|
||||
namespace,
|
||||
});
|
||||
}
|
||||
|
||||
let namespacesApi: NamespaceApi;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
namespacesApi = new NamespaceApi({
|
||||
objectConstructor: Namespace,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
namespacesApi,
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { NetworkPolicyApi } from "./network-policy.api";
|
||||
|
||||
const networkPolicyApiInjectable = getInjectable({
|
||||
id: "network-policy-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "networkPolicyApi is only available in certain environments");
|
||||
|
||||
return new NetworkPolicyApi();
|
||||
},
|
||||
});
|
||||
|
||||
export default networkPolicyApiInjectable;
|
||||
@@ -3,12 +3,10 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { LabelSelector } from "../kube-object";
|
||||
import type { KubeObjectScope, LabelSelector } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { autoBind } from "../../utils";
|
||||
import type { DerivedKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export interface IPolicyIpBlock {
|
||||
cidr: string;
|
||||
@@ -105,15 +103,10 @@ export interface NetworkPolicy {
|
||||
spec: NetworkPolicySpec;
|
||||
}
|
||||
|
||||
export class NetworkPolicy extends KubeObject {
|
||||
static kind = "NetworkPolicy";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/networking.k8s.io/v1/networkpolicies";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
export class NetworkPolicy extends KubeObject<void, NetworkPolicySpec, KubeObjectScope.Namespace> {
|
||||
static readonly kind = "NetworkPolicy";
|
||||
static readonly namespaced = true;
|
||||
static readonly apiBase = "/apis/networking.k8s.io/v1/networkpolicies";
|
||||
|
||||
getMatchLabels(): string[] {
|
||||
if (!this.spec.podSelector || !this.spec.podSelector.matchLabels) return [];
|
||||
@@ -130,14 +123,11 @@ export class NetworkPolicy extends KubeObject {
|
||||
}
|
||||
}
|
||||
|
||||
let networkPolicyApi: KubeApi<NetworkPolicy>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
networkPolicyApi = new KubeApi<NetworkPolicy>({
|
||||
objectConstructor: NetworkPolicy,
|
||||
});
|
||||
export class NetworkPolicyApi extends KubeApi<NetworkPolicy> {
|
||||
constructor(opts: DerivedKubeApiOptions = {}) {
|
||||
super({
|
||||
objectConstructor: NetworkPolicy,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
networkPolicyApi,
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { NodeApi } from "./node.api";
|
||||
|
||||
const nodeApiInjectable = getInjectable({
|
||||
id: "node-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "nodeApi is only available in certain environments");
|
||||
|
||||
return new NodeApi();
|
||||
},
|
||||
});
|
||||
|
||||
export default nodeApiInjectable;
|
||||
Loaded 100 of 1062 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user