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:
Sebastian Malton authored and GitHub committed 2022-05-16 07:17:57 -04:00
1 parent 381d77c633
commit dfcb7c3330
1062 files changed
+26512 -28689

No files matched your search

@@ -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);
});
});
});
+29
View File
@@ -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");
});
});
+149 -140
View File
@@ -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",
},
});
+62 -52
View File
@@ -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,
+8 -14
View File
@@ -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);
-123
View File
@@ -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;
+6
View File
@@ -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);
}
}
+1 -1
View File
@@ -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;
+23 -32
View File
@@ -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;
+29 -66
View File
@@ -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;
@@ -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;
+66 -78
View File
@@ -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;
+72 -143
View File
@@ -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;
+82 -112
View File
@@ -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;
+96 -36
View File
@@ -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,
};
+36 -14
View File
@@ -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 ?? {},
});
}
}
-161
View File
@@ -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,
};
+12 -11
View File
@@ -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;
+79 -82
View File
@@ -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;
+69 -89
View File
@@ -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;
+15 -26
View File
@@ -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,
};
+19 -18
View File
@@ -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;
@@ -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;
+268
View File
@@ -0,0 +1,268 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { BaseKubeObjectCondition, KubeObjectScope } from "../kube-object";
import { KubeObject } from "../kube-object";
import { cpuUnitsToNumber, unitsToBytes } from "../../../renderer/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 { TypedRegEx } from "typed-regex";
export class NodeApi extends KubeApi<Node> {
constructor(opts: DerivedKubeApiOptions & IgnoredKubeApiOptions = {}) {
super({
...opts,
objectConstructor: Node,
});
}
}
export function getMetricsForAllNodes(): Promise<NodeMetricData> {
const opts = { category: "nodes" };
return metricsApi.getMetrics({
memoryUsage: opts,
workloadMemoryUsage: opts,
memoryCapacity: opts,
memoryAllocatableCapacity: opts,
cpuUsage: opts,
cpuCapacity: opts,
fsSize: opts,
fsUsage: opts,
});
}
export interface NodeMetricData extends Partial<Record<string, MetricData>> {
memoryUsage: MetricData;
workloadMemoryUsage: MetricData;
memoryCapacity: MetricData;
memoryAllocatableCapacity: MetricData;
cpuUsage: MetricData;
cpuCapacity: MetricData;
fsUsage: MetricData;
fsSize: MetricData;
}
export interface NodeTaint {
key: string;
value?: string;
effect: string;
timeAdded: string;
}
export function formatNodeTaint(taint: NodeTaint): string {
if (taint.value) {
return `${taint.key}=${taint.value}:${taint.effect}`;
}
return `${taint.key}:${taint.effect}`;
}
export interface NodeCondition extends BaseKubeObjectCondition {
/**
* Last time we got an update on a given condition.
*/
lastHeartbeatTime?: string;
}
/**
* This regex is used in the `getRoleLabels()` method bellow, but placed here
* as factoring out regexes is best practice.
*/
const nodeRoleLabelKeyMatcher = TypedRegEx("^.*node-role.kubernetes.io/+(?<role>.+)$");
export interface NodeSpec {
podCIDR?: string;
podCIDRs?: string[];
providerID?: string;
/**
* @deprecated see https://issues.k8s.io/61966
*/
externalID?: string;
taints?: NodeTaint[];
unschedulable?: boolean;
}
export interface NodeAddress {
type: "Hostname" | "ExternalIP" | "InternalIP";
address: string;
}
export interface NodeStatusResources extends Partial<Record<string, string>> {
cpu?: string;
"ephemeral-storage"?: string;
"hugepages-1Gi"?: string;
"hugepages-2Mi"?: string;
memory?: string;
pods?: string;
}
export interface ConfigMapNodeConfigSource {
kubeletConfigKey: string;
name: string;
namespace: string;
resourceVersion?: string;
uid?: string;
}
export interface NodeConfigSource {
configMap?: ConfigMapNodeConfigSource;
}
export interface NodeConfigStatus {
active?: NodeConfigSource;
assigned?: NodeConfigSource;
lastKnownGood?: NodeConfigSource;
error?: string;
}
export interface DaemonEndpoint {
Port: number; //it must be uppercase for backwards compatibility
}
export interface NodeDaemonEndpoints {
kubeletEndpoint?: DaemonEndpoint;
}
export interface ContainerImage {
names?: string[];
sizeBytes?: number;
}
export interface NodeSystemInfo {
architecture: string;
bootID: string;
containerRuntimeVersion: string;
kernelVersion: string;
kubeProxyVersion: string;
kubeletVersion: string;
machineID: string;
operatingSystem: string;
osImage: string;
systemUUID: string;
}
export interface AttachedVolume {
name: string;
devicePath: string;
}
export interface NodeStatus {
capacity?: NodeStatusResources;
allocatable?: NodeStatusResources;
conditions?: NodeCondition[];
addresses?: NodeAddress[];
config?: NodeConfigStatus;
daemonEndpoints?: NodeDaemonEndpoints;
images?: ContainerImage[];
nodeInfo?: NodeSystemInfo;
phase?: string;
volumesInUse?: string[];
volumesAttached?: AttachedVolume[];
}
export class Node extends KubeObject<NodeStatus, NodeSpec, KubeObjectScope.Cluster> {
static readonly kind = "Node";
static readonly namespaced = false;
static readonly apiBase = "/api/v1/nodes";
/**
* Returns the concatination of all current condition types which have a status
* of `"True"`
*/
getNodeConditionText(): string {
if (!this.status?.conditions) {
return "";
}
return this.status.conditions
.filter(condition => condition.status === "True")
.map(condition => condition.type)
.join(" ");
}
getTaints() {
return this.spec.taints || [];
}
getRoleLabels(): string {
const { labels } = this.metadata;
if (!labels || typeof labels !== "object") {
return "";
}
const roleLabels: string[] = [];
for (const labelKey of Object.keys(labels)) {
const match = nodeRoleLabelKeyMatcher.match(labelKey);
if (match?.groups) {
roleLabels.push(match.groups.role);
}
}
if (typeof labels["kubernetes.io/role"] === "string") {
roleLabels.push(labels["kubernetes.io/role"]);
}
if (typeof labels["node.kubernetes.io/role"] === "string") {
roleLabels.push(labels["node.kubernetes.io/role"]);
}
return roleLabels.join(", ");
}
getCpuCapacity() {
if (!this.status?.capacity || !this.status.capacity.cpu) return 0;
return cpuUnitsToNumber(this.status.capacity.cpu);
}
getMemoryCapacity() {
if (!this.status?.capacity || !this.status.capacity.memory) return 0;
return unitsToBytes(this.status.capacity.memory);
}
getConditions(): NodeCondition[] {
const conditions = this.status?.conditions || [];
if (this.isUnschedulable()) {
return [{ type: "SchedulingDisabled", status: "True" }, ...conditions];
}
return conditions;
}
getActiveConditions() {
return this.getConditions().filter(c => c.status === "True");
}
getWarningConditions() {
const goodConditions = ["Ready", "HostUpgrades", "SchedulingDisabled"];
return this.getActiveConditions().filter(condition => {
return !goodConditions.includes(condition.type);
});
}
getKubeletVersion() {
return this.status?.nodeInfo?.kubeletVersion ?? "<unknown>";
}
getOperatingSystem(): string {
return this.metadata?.labels?.["kubernetes.io/os"]
|| this.metadata?.labels?.["beta.kubernetes.io/os"]
|| this.status?.nodeInfo?.operatingSystem
|| "linux";
}
isUnschedulable() {
return this.spec.unschedulable;
}
}
-263
View File
@@ -1,263 +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 { autoBind, cpuUnitsToNumber, iter, unitsToBytes } from "../../../renderer/utils";
import type { IMetrics } from "./metrics.api";
import { metricsApi } from "./metrics.api";
import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
export class NodesApi extends KubeApi<Node> {
}
export function getMetricsForAllNodes(): Promise<INodeMetrics> {
const opts = { category: "nodes" };
return metricsApi.getMetrics({
memoryUsage: opts,
workloadMemoryUsage: opts,
memoryCapacity: opts,
memoryAllocatableCapacity: opts,
cpuUsage: opts,
cpuCapacity: opts,
fsSize: opts,
fsUsage: opts,
});
}
export interface INodeMetrics<T = IMetrics> {
[metric: string]: T;
memoryUsage: T;
workloadMemoryUsage: T;
memoryCapacity: T;
memoryAllocatableCapacity: T;
cpuUsage: T;
cpuCapacity: T;
fsUsage: T;
fsSize: T;
}
export interface NodeTaint {
key: string;
value?: string;
effect: string;
timeAdded: string;
}
export function formatNodeTaint(taint: NodeTaint): string {
if (taint.value) {
return `${taint.key}=${taint.value}:${taint.effect}`;
}
return `${taint.key}:${taint.effect}`;
}
export interface NodeCondition {
type: string;
status: string;
lastHeartbeatTime?: string;
lastTransitionTime?: string;
reason?: string;
message?: string;
}
export interface Node {
spec: {
podCIDR?: string;
podCIDRs?: string[];
providerID?: string;
/**
* @deprecated see https://issues.k8s.io/61966
*/
externalID?: string;
taints?: NodeTaint[];
unschedulable?: boolean;
};
status: {
capacity?: {
cpu: string;
"ephemeral-storage": string;
"hugepages-1Gi": string;
"hugepages-2Mi": string;
memory: string;
pods: string;
};
allocatable?: {
cpu: string;
"ephemeral-storage": string;
"hugepages-1Gi": string;
"hugepages-2Mi": string;
memory: string;
pods: string;
};
conditions?: NodeCondition[];
addresses?: {
type: string;
address: string;
}[];
daemonEndpoints?: {
kubeletEndpoint: {
Port: number; //it must be uppercase for backwards compatibility
};
};
nodeInfo?: {
machineID: string;
systemUUID: string;
bootID: string;
kernelVersion: string;
osImage: string;
containerRuntimeVersion: string;
kubeletVersion: string;
kubeProxyVersion: string;
operatingSystem: string;
architecture: string;
};
images?: {
names: string[];
sizeBytes?: number;
}[];
volumesInUse?: string[];
volumesAttached?: {
name: string;
devicePath: string;
}[];
};
}
/**
* Iterate over `conditions` yielding the `type` field if the `status` field is
* the string `"True"`
* @param conditions An iterator of some conditions
*/
function* getTrueConditionTypes(conditions: IterableIterator<NodeCondition> | Iterable<NodeCondition>): IterableIterator<string> {
for (const { status, type } of conditions) {
if (status === "True") {
yield type;
}
}
}
/**
* This regex is used in the `getRoleLabels()` method bellow, but placed here
* as factoring out regexes is best practice.
*/
const nodeRoleLabelKeyMatcher = /^.*node-role.kubernetes.io\/+(?<role>.+)$/;
export class Node extends KubeObject {
static kind = "Node";
static namespaced = false;
static apiBase = "/api/v1/nodes";
constructor(data: KubeJsonApiData) {
super(data);
autoBind(this);
}
/**
* Returns the concatination of all current condition types which have a status
* of `"True"`
*/
getNodeConditionText(): string {
return iter.join(
getTrueConditionTypes(this.status?.conditions ?? []),
" ",
);
}
getTaints() {
return this.spec.taints || [];
}
getRoleLabels(): string {
const { labels } = this.metadata;
if (!labels || typeof labels !== "object") {
return "";
}
const roleLabels: string[] = [];
for (const labelKey of Object.keys(labels)) {
const match = nodeRoleLabelKeyMatcher.exec(labelKey);
if (match) {
roleLabels.push(match.groups.role);
}
}
if (typeof labels["kubernetes.io/role"] === "string") {
roleLabels.push(labels["kubernetes.io/role"]);
}
if (typeof labels["node.kubernetes.io/role"] === "string") {
roleLabels.push(labels["node.kubernetes.io/role"]);
}
return roleLabels.join(", ");
}
getCpuCapacity() {
if (!this.status.capacity || !this.status.capacity.cpu) return 0;
return cpuUnitsToNumber(this.status.capacity.cpu);
}
getMemoryCapacity() {
if (!this.status.capacity || !this.status.capacity.memory) return 0;
return unitsToBytes(this.status.capacity.memory);
}
getConditions() {
const conditions = this.status.conditions || [];
if (this.isUnschedulable()) {
return [{ type: "SchedulingDisabled", status: "True" }, ...conditions];
}
return conditions;
}
getActiveConditions() {
return this.getConditions().filter(c => c.status === "True");
}
getWarningConditions() {
const goodConditions = ["Ready", "HostUpgrades", "SchedulingDisabled"];
return this.getActiveConditions().filter(condition => {
return !goodConditions.includes(condition.type);
});
}
getKubeletVersion() {
return this.status.nodeInfo.kubeletVersion;
}
getOperatingSystem(): string {
return this.metadata?.labels?.["kubernetes.io/os"]
|| this.metadata?.labels?.["beta.kubernetes.io/os"]
|| this.status?.nodeInfo?.operatingSystem
|| "linux";
}
isUnschedulable() {
return this.spec.unschedulable;
}
}
let nodesApi: NodesApi;
if (isClusterPageContext()) {
nodesApi = new NodesApi({
objectConstructor: Node,
});
}
export {
nodesApi,
};
@@ -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 { PersistentVolumeClaimApi } from "./persistent-volume-claim.api";
const persistentVolumeClaimApiInjectable = getInjectable({
id: "persistent-volume-claim-api",
instantiate: (di) => {
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "persistentVolumeClaimApi is only available in certain environments");
return new PersistentVolumeClaimApi();
},
});
export default persistentVolumeClaimApiInjectable;
@@ -0,0 +1,87 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { KubeObjectScope, LabelSelector, TypedLocalObjectReference } from "../kube-object";
import { KubeObject } from "../kube-object";
import type { MetricData } from "./metrics.api";
import { metricsApi } from "./metrics.api";
import type { Pod } from "./pod.api";
import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api";
import { KubeApi } from "../kube-api";
import { object } from "../../utils";
import type { ResourceRequirements } from "./types/resource-requirements";
export class PersistentVolumeClaimApi extends KubeApi<PersistentVolumeClaim> {
constructor(opts: DerivedKubeApiOptions & IgnoredKubeApiOptions = {}) {
super({
...opts,
objectConstructor: PersistentVolumeClaim,
});
}
}
export function getMetricsForPvc(pvc: PersistentVolumeClaim): Promise<PersistentVolumeClaimMetricData> {
const opts = { category: "pvc", pvc: pvc.getName(), namespace: pvc.getNs() };
return metricsApi.getMetrics({
diskUsage: opts,
diskCapacity: opts,
}, {
namespace: opts.namespace,
});
}
export interface PersistentVolumeClaimMetricData extends Partial<Record<string, MetricData>> {
diskUsage: MetricData;
diskCapacity: MetricData;
}
export interface PersistentVolumeClaimSpec {
accessModes?: string[];
dataSource?: TypedLocalObjectReference;
dataSourceRef?: TypedLocalObjectReference;
resources?: ResourceRequirements;
selector?: LabelSelector;
storageClassName?: string;
volumeMode?: string;
volumeName?: string;
}
export interface PersistentVolumeClaimStatus {
phase: string; // Pending
}
export class PersistentVolumeClaim extends KubeObject<PersistentVolumeClaimStatus, PersistentVolumeClaimSpec, KubeObjectScope.Namespace> {
static readonly kind = "PersistentVolumeClaim";
static readonly namespaced = true;
static readonly apiBase = "/api/v1/persistentvolumeclaims";
getPods(pods: Pod[]): Pod[] {
return pods
.filter(pod => pod.getNs() === this.getNs())
.filter(pod => (
pod.getVolumes()
.filter(volume => volume.persistentVolumeClaim?.claimName === this.getName())
.length > 0
));
}
getStorage(): string {
return this.spec.resources?.requests?.storage ?? "-";
}
getMatchLabels(): string[] {
return object.entries(this.spec.selector?.matchLabels)
.map(([name, val]) => `${name}:${val}`);
}
getMatchExpressions() {
return this.spec.selector?.matchExpressions ?? [];
}
getStatus(): string {
return this.status?.phase ?? "-";
}
}
@@ -1,117 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { LabelSelector } from "../kube-object";
import { KubeObject } from "../kube-object";
import { autoBind } from "../../utils";
import type { IMetrics } from "./metrics.api";
import { metricsApi } from "./metrics.api";
import type { Pod } from "./pods.api";
import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
export class PersistentVolumeClaimsApi extends KubeApi<PersistentVolumeClaim> {
}
export function getMetricsForPvc(pvc: PersistentVolumeClaim): Promise<IPvcMetrics> {
const opts = { category: "pvc", pvc: pvc.getName(), namespace: pvc.getNs() };
return metricsApi.getMetrics({
diskUsage: opts,
diskCapacity: opts,
}, {
namespace: opts.namespace,
});
}
export interface IPvcMetrics<T = IMetrics> {
[key: string]: T;
diskUsage: T;
diskCapacity: T;
}
export interface PersistentVolumeClaimSpec {
accessModes: string[];
selector: LabelSelector;
resources: {
requests?: Record<string, string>;
limits?: Record<string, string>;
};
volumeName?: string;
storageClassName?: string;
volumeMode?: string;
dataSource?: {
apiGroup: string;
kind: string;
name: string;
};
}
export interface PersistentVolumeClaim {
spec: PersistentVolumeClaimSpec;
status: {
phase: string; // Pending
};
}
export class PersistentVolumeClaim extends KubeObject {
static kind = "PersistentVolumeClaim";
static namespaced = true;
static apiBase = "/api/v1/persistentvolumeclaims";
constructor(data: KubeJsonApiData) {
super(data);
autoBind(this);
}
getPods(allPods: Pod[]): Pod[] {
const pods = allPods.filter(pod => pod.getNs() === this.getNs());
return pods.filter(pod => {
return pod.getVolumes().filter(volume =>
volume.persistentVolumeClaim &&
volume.persistentVolumeClaim.claimName === this.getName(),
).length > 0;
});
}
getStorage(): string {
if (!this.spec.resources || !this.spec.resources.requests) return "-";
return this.spec.resources.requests.storage;
}
getMatchLabels(): string[] {
if (!this.spec.selector || !this.spec.selector.matchLabels) return [];
return Object.entries(this.spec.selector.matchLabels)
.map(([name, val]) => `${name}:${val}`);
}
getMatchExpressions() {
if (!this.spec.selector || !this.spec.selector.matchExpressions) return [];
return this.spec.selector.matchExpressions;
}
getStatus(): string {
if (this.status) return this.status.phase;
return "-";
}
}
let pvcApi: PersistentVolumeClaimsApi;
if (isClusterPageContext()) {
pvcApi = new PersistentVolumeClaimsApi({
objectConstructor: PersistentVolumeClaim,
});
}
export {
pvcApi,
};
@@ -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 { PersistentVolumeApi } from "./persistent-volume.api";
const persistentVolumeApiInjectable = getInjectable({
id: "persistent-volume-api",
instantiate: (di) => {
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "persistentVolumeApi is only available in certain environments");
return new PersistentVolumeApi();
},
});
export default persistentVolumeApiInjectable;
@@ -3,64 +3,76 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { KubeObjectScope, LabelSelector, ObjectReference, TypedLocalObjectReference } from "../kube-object";
import { KubeObject } from "../kube-object";
import { autoBind, unitsToBytes } from "../../utils";
import { unitsToBytes } 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";
import type { ResourceRequirements } from "./types/resource-requirements";
export interface PersistentVolume {
spec: {
capacity: {
storage: string; // 8Gi
};
flexVolume: {
driver: string; // ceph.rook.io/rook-ceph-system,
options: {
clusterNamespace: string; // rook-ceph,
image: string; // pvc-c5d7c485-9f1b-11e8-b0ea-9600000e54fb,
pool: string; // replicapool,
storageClass: string; // rook-ceph-block
};
};
mountOptions?: string[];
accessModes: string[]; // [ReadWriteOnce]
claimRef: {
kind: string; // PersistentVolumeClaim,
namespace: string; // storage,
name: string; // nfs-provisioner,
uid: string; // c5d7c485-9f1b-11e8-b0ea-9600000e54fb,
apiVersion: string; // v1,
resourceVersion: string; // 292180
};
persistentVolumeReclaimPolicy: string; // Delete,
storageClassName: string; // rook-ceph-block
nfs?: {
path: string;
server: string;
export interface PersistentVolumeSpec {
/**
* AccessModes contains the desired access modes the volume should have.
*
* More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1
*/
accessModes?: string[];
dataSource?: TypedLocalObjectReference;
dataSourceRef?: TypedLocalObjectReference;
resources?: ResourceRequirements;
selector?: LabelSelector;
/**
* Name of the StorageClass required by the claim.
*
* More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1
*/
storageClassName?: string;
/**
* Defines what type of volume is required by the claim. Value of Filesystem is implied when not
* included in claim spec.
*/
volumeMode?: string;
/**
* A description of the persistent volume\'s resources and capacity.
*
* More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#capacity
*/
capacity?: Partial<Record<string, string>>;
flexVolume?: {
driver: string; // ceph.rook.io/rook-ceph-system,
options: {
clusterNamespace: string; // rook-ceph,
image: string; // pvc-c5d7c485-9f1b-11e8-b0ea-9600000e54fb,
pool: string; // replicapool,
storageClass: string; // rook-ceph-block
};
};
status?: {
phase: string;
reason?: string;
mountOptions?: string[];
claimRef?: ObjectReference;
persistentVolumeReclaimPolicy?: string; // Delete,
nfs?: {
path: string;
server: string;
};
}
export class PersistentVolume extends KubeObject {
export interface PersistentVolumeStatus {
phase: string;
reason?: string;
}
export class PersistentVolume extends KubeObject<PersistentVolumeStatus, PersistentVolumeSpec, KubeObjectScope.Cluster> {
static kind = "PersistentVolume";
static namespaced = false;
static apiBase = "/api/v1/persistentvolumes";
constructor(data: KubeJsonApiData) {
super(data);
autoBind(this);
}
getCapacity(inBytes = false) {
const capacity = this.spec.capacity;
if (capacity) {
if (capacity?.storage) {
if (inBytes) return unitsToBytes(capacity.storage);
return capacity.storage;
@@ -74,7 +86,7 @@ export class PersistentVolume extends KubeObject {
}
getStorageClass(): string {
return this.spec.storageClassName;
return this.spec.storageClassName ?? "";
}
getClaimRefName(): string {
@@ -86,14 +98,11 @@ export class PersistentVolume extends KubeObject {
}
}
let persistentVolumeApi: KubeApi<PersistentVolume>;
if (isClusterPageContext()) {
persistentVolumeApi = new KubeApi({
objectConstructor: PersistentVolume,
});
export class PersistentVolumeApi extends KubeApi<PersistentVolume> {
constructor(opts: DerivedKubeApiOptions = {}) {
super({
...opts,
objectConstructor: PersistentVolume,
});
}
}
export {
persistentVolumeApi,
};
@@ -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 { PodDisruptionBudgetApi } from "./pod-disruption-budget.api";
const podDisruptionBudgetApiInjectable = getInjectable({
id: "pod-disruption-budget-api",
instantiate: (di) => {
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "podDisruptionBudgetApi is only available in certain environments");
return new PodDisruptionBudgetApi();
},
});
export default podDisruptionBudgetApiInjectable;
@@ -0,0 +1,57 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { KubeObjectScope, LabelSelector } from "../kube-object";
import { KubeObject } from "../kube-object";
import type { DerivedKubeApiOptions } from "../kube-api";
import { KubeApi } from "../kube-api";
export interface PodDisruptionBudgetSpec {
minAvailable: string;
maxUnavailable: string;
selector: LabelSelector;
}
export interface PodDisruptionBudgetStatus {
currentHealthy: number;
desiredHealthy: number;
disruptionsAllowed: number;
expectedPods: number;
}
export class PodDisruptionBudget extends KubeObject<PodDisruptionBudgetStatus, PodDisruptionBudgetSpec, KubeObjectScope.Namespace> {
static readonly kind = "PodDisruptionBudget";
static readonly namespaced = true;
static readonly apiBase = "/apis/policy/v1beta1/poddisruptionbudgets";
getSelectors() {
return KubeObject.stringifyLabels(this.spec.selector.matchLabels);
}
getMinAvailable() {
return this.spec.minAvailable || "N/A";
}
getMaxUnavailable() {
return this.spec.maxUnavailable || "N/A";
}
getCurrentHealthy() {
return this.status?.currentHealthy ?? 0;
}
getDesiredHealthy() {
return this.status?.desiredHealthy ?? 0;
}
}
export class PodDisruptionBudgetApi extends KubeApi<PodDisruptionBudget> {
constructor(opts: DerivedKubeApiOptions = {}) {
super({
objectConstructor: PodDisruptionBudget,
...opts,
});
}
}
@@ -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 { PodMetricsApi } from "./pod-metrics.api";
const podMetricsApiInjectable = getInjectable({
id: "pod-metrics-api",
instantiate: (di) => {
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "podMetricsApi is only available in certain environments");
return new PodMetricsApi();
},
});
export default podMetricsApiInjectable;
+41 -22
View File
@@ -3,36 +3,55 @@
* 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 { 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 PodMetrics {
export interface PodMetricsData extends KubeJsonApiData<KubeObjectMetadata<KubeObjectScope.Namespace>, void, void> {
timestamp: string;
window: string;
containers: {
name: string;
usage: {
cpu: string;
memory: string;
};
}[];
containers: PodMetricsContainer[];
}
export class PodMetrics extends KubeObject {
static kind = "PodMetrics";
static namespaced = true;
static apiBase = "/apis/metrics.k8s.io/v1beta1/pods";
export interface PodMetricsContainerUsage {
cpu: string;
memory: string;
}
let podMetricsApi: KubeApi<PodMetrics>;
if (isClusterPageContext()) {
podMetricsApi = new KubeApi<PodMetrics>({
objectConstructor: PodMetrics,
});
export interface PodMetricsContainer {
name: string;
usage: PodMetricsContainerUsage;
}
export {
podMetricsApi,
};
export class PodMetrics extends KubeObject<void, void, KubeObjectScope.Namespace> {
static readonly kind = "PodMetrics";
static readonly namespaced = true;
static readonly apiBase = "/apis/metrics.k8s.io/v1beta1/pods";
timestamp: string;
window: string;
containers: PodMetricsContainer[];
constructor({
timestamp,
window,
containers,
...rest
}: PodMetricsData) {
super(rest);
this.timestamp = timestamp;
this.window = window;
this.containers = containers;
}
}
export class PodMetricsApi extends KubeApi<PodMetrics, PodMetricsData> {
constructor(opts: DerivedKubeApiOptions = {}) {
super({
...opts,
objectConstructor: PodMetrics,
});
}
}
@@ -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 { PodSecurityPolicyApi } from "./pod-security-policy.api";
const podSecurityPolicyApiInjectable = getInjectable({
id: "pod-security-policy-api",
instantiate: (di) => {
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "podSecurityPolicyApi is only available in certain environments");
return new PodSecurityPolicyApi();
},
});
export default podSecurityPolicyApiInjectable;
@@ -0,0 +1,117 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* 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";
export interface PodSecurityPolicySpec {
allowPrivilegeEscalation?: boolean;
allowedCSIDrivers?: {
name: string;
}[];
allowedCapabilities: string[];
allowedFlexVolumes?: {
driver: string;
}[];
allowedHostPaths?: {
pathPrefix: string;
readOnly: boolean;
}[];
allowedProcMountTypes?: string[];
allowedUnsafeSysctls?: string[];
defaultAddCapabilities?: string[];
defaultAllowPrivilegeEscalation?: boolean;
forbiddenSysctls?: string[];
fsGroup?: {
rule: string;
ranges: {
max: number;
min: number;
}[];
};
hostIPC?: boolean;
hostNetwork?: boolean;
hostPID?: boolean;
hostPorts?: {
max: number;
min: number;
}[];
privileged?: boolean;
readOnlyRootFilesystem?: boolean;
requiredDropCapabilities?: string[];
runAsGroup?: {
ranges: {
max: number;
min: number;
}[];
rule: string;
};
runAsUser?: {
rule: string;
ranges: {
max: number;
min: number;
}[];
};
runtimeClass?: {
allowedRuntimeClassNames: string[];
defaultRuntimeClassName: string;
};
seLinux?: {
rule: string;
seLinuxOptions: {
level: string;
role: string;
type: string;
user: string;
};
};
supplementalGroups?: {
rule: string;
ranges: {
max: number;
min: number;
}[];
};
volumes?: string[];
}
export class PodSecurityPolicy extends KubeObject<void, PodSecurityPolicySpec, KubeObjectScope.Cluster> {
static readonly kind = "PodSecurityPolicy";
static readonly namespaced = false;
static readonly apiBase = "/apis/policy/v1beta1/podsecuritypolicies";
isPrivileged() {
return !!this.spec.privileged;
}
getVolumes() {
return this.spec.volumes || [];
}
getRules() {
const { fsGroup, runAsGroup, runAsUser, supplementalGroups, seLinux } = this.spec;
return {
fsGroup: fsGroup ? fsGroup.rule : "",
runAsGroup: runAsGroup ? runAsGroup.rule : "",
runAsUser: runAsUser ? runAsUser.rule : "",
supplementalGroups: supplementalGroups ? supplementalGroups.rule : "",
seLinux: seLinux ? seLinux.rule : "",
};
}
}
export class PodSecurityPolicyApi extends KubeApi<PodSecurityPolicy> {
constructor(opts: DerivedKubeApiOptions = {}) {
super({
...opts,
objectConstructor: PodSecurityPolicy,
});
}
}
@@ -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 { PodApi } from "./pod.api";
const podApiInjectable = getInjectable({
id: "pod-api",
instantiate: (di) => {
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "podApi is only available in certain environments");
return new PodApi();
},
});
export default podApiInjectable;
@@ -3,28 +3,33 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { IAffinity } from "../workload-kube-object";
import { WorkloadKubeObject } from "../workload-kube-object";
import { autoBind } from "../../utils";
import type { IMetrics } from "./metrics.api";
import type { MetricData } from "./metrics.api";
import { metricsApi } from "./metrics.api";
import type { DerivedKubeApiOptions, IgnoredKubeApiOptions, ResourceDescriptor } 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";
import type { KubeObjectMetadata, LocalObjectReference } from "../kube-object";
import type { KubeObjectMetadata, LocalObjectReference, Affinity, Toleration, LabelSelector, KubeObjectScope } from "../kube-object";
import type { SecretReference } from "./secret.api";
import type { PersistentVolumeClaimSpec } from "./persistent-volume-claims.api";
import type { PersistentVolumeClaimSpec } from "./persistent-volume-claim.api";
import { KubeObject } from "../kube-object";
import { isDefined } from "../../utils";
export class PodsApi extends KubeApi<Pod> {
getLogs = async (params: { namespace: string; name: string }, query?: IPodLogsQuery): Promise<string> => {
export class PodApi extends KubeApi<Pod> {
constructor(opts: DerivedKubeApiOptions & IgnoredKubeApiOptions = {}) {
super({
...opts,
objectConstructor: Pod,
});
}
async getLogs(params: ResourceDescriptor, query?: PodLogsQuery): Promise<string> {
const path = `${this.getUrl(params)}/log`;
return this.request.get(path, { query });
};
}
}
export function getMetricsForPods(pods: Pod[], namespace: string, selector = "pod, namespace"): Promise<IPodMetrics> {
export function getMetricsForPods(pods: Pod[], namespace: string, selector = "pod, namespace"): Promise<PodMetricData> {
const podSelector = pods.map(pod => pod.getName()).join("|");
const opts = { category: "pods", pods: podSelector, namespace, selector };
@@ -45,23 +50,22 @@ export function getMetricsForPods(pods: Pod[], namespace: string, selector = "po
});
}
export interface IPodMetrics<T = IMetrics> {
[metric: string]: T;
cpuUsage: T;
memoryUsage: T;
fsUsage: T;
fsWrites: T;
fsReads: T;
networkReceive: T;
networkTransmit: T;
cpuRequests?: T;
cpuLimits?: T;
memoryRequests?: T;
memoryLimits?: T;
export interface PodMetricData extends Partial<Record<string, MetricData>> {
cpuUsage: MetricData;
memoryUsage: MetricData;
fsUsage: MetricData;
fsWrites: MetricData;
fsReads: MetricData;
networkReceive: MetricData;
networkTransmit: MetricData;
cpuRequests?: MetricData;
cpuLimits?: MetricData;
memoryRequests?: MetricData;
memoryLimits?: MetricData;
}
// Reference: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#read-log-pod-v1-core
export interface IPodLogsQuery {
export interface PodLogsQuery {
container?: string;
tailLines?: number;
timestamps?: boolean;
@@ -70,7 +74,7 @@ export interface IPodLogsQuery {
previous?: boolean;
}
export enum PodStatus {
export enum PodStatusPhase {
TERMINATED = "Terminated",
FAILED = "Failed",
PENDING = "Pending",
@@ -79,26 +83,41 @@ export enum PodStatus {
EVICTED = "Evicted",
}
export interface IPodContainer extends Partial<Record<PodContainerProbe, IContainerProbe>> {
export interface ContainerPort {
containerPort: number;
hostIP?: string;
hostPort?: number;
name?: string;
protocol?: "UDP" | "TCP" | "SCTP";
}
export interface VolumeMount {
name: string;
readOnly?: boolean;
mountPath: string;
mountPropagation?: string;
subPath?: string;
subPathExpr?: string;
}
export interface PodContainer extends Partial<Record<PodContainerProbe, IContainerProbe>> {
name: string;
image: string;
command?: string[];
args?: string[];
ports?: {
name?: string;
containerPort: number;
protocol: string;
}[];
ports?: ContainerPort[];
resources?: {
limits: {
limits?: {
cpu: string;
memory: string;
};
requests: {
requests?: {
cpu: string;
memory: string;
};
};
terminationMessagePath?: string;
terminationMessagePolicy?: string;
env?: {
name: string;
value?: string;
@@ -121,12 +140,8 @@ export interface IPodContainer extends Partial<Record<PodContainerProbe, IContai
configMapRef?: LocalObjectReference;
secretRef?: LocalObjectReference;
}[];
volumeMounts?: {
name: string;
readOnly: boolean;
mountPath: string;
}[];
imagePullPolicy: string;
volumeMounts?: VolumeMount[];
imagePullPolicy?: string;
}
export type PodContainerProbe = "livenessProbe" | "readinessProbe" | "startupProbe";
@@ -185,7 +200,7 @@ export interface ContainerState {
terminated?: ContainerStateTerminated;
}
export interface IPodContainerStatus {
export interface PodContainerStatus {
name: string;
state?: ContainerState;
lastState?: ContainerState;
@@ -631,85 +646,133 @@ export interface PodVolumeVariants {
*/
export type PodVolumeKind = keyof PodVolumeVariants;
export type PodVolume = RequireExactlyOne<PodVolumeVariants> & {
export type PodSpecVolume = RequireExactlyOne<PodVolumeVariants> & {
name: string;
};
export class Pod extends WorkloadKubeObject {
export interface HostAlias {
ip: string;
hostnames: string[];
}
export interface SELinuxOptions {
level?: string;
role?: string;
type?: string;
user?: string;
}
export interface SeccompProfile {
localhostProfile?: string;
type: string;
}
export interface Sysctl {
name: string;
value: string;
}
export interface WindowsSecurityContextOptions {
labelSelector?: LabelSelector;
maxSkew: number;
topologyKey: string;
whenUnsatisfiable: string;
}
export interface PodSecurityContext {
fsGroup?: number;
fsGroupChangePolicy?: string;
runAsGroup?: number;
runAsNonRoot?: boolean;
runAsUser?: number;
seLinuxOptions?: SELinuxOptions;
seccompProfile?: SeccompProfile;
supplementalGroups?: number[];
sysctls?: Sysctl;
windowsOptions?: WindowsSecurityContextOptions;
}
export interface TopologySpreadConstraint {
}
export interface PodSpec {
activeDeadlineSeconds?: number;
affinity?: Affinity;
automountServiceAccountToken?: boolean;
containers?: PodContainer[];
dnsPolicy?: string;
enableServiceLinks?: boolean;
ephemeralContainers?: unknown[];
hostAliases?: HostAlias[];
hostIPC?: boolean;
hostname?: string;
hostNetwork?: boolean;
hostPID?: boolean;
imagePullSecrets?: LocalObjectReference[];
initContainers?: PodContainer[];
nodeName?: string;
nodeSelector?: Partial<Record<string, string>>;
overhead?: Partial<Record<string, string>>;
preemptionPolicy?: string;
priority?: number;
priorityClassName?: string;
readinessGates?: unknown[];
restartPolicy?: string;
runtimeClassName?: string;
schedulerName?: string;
securityContext?: PodSecurityContext;
serviceAccount?: string;
serviceAccountName?: string;
setHostnameAsFQDN?: boolean;
shareProcessNamespace?: boolean;
subdomain?: string;
terminationGracePeriodSeconds?: number;
tolerations?: Toleration[];
topologySpreadConstraints?: TopologySpreadConstraint[];
volumes?: PodSpecVolume[];
}
export interface PodCondition {
lastProbeTime?: number;
lastTransitionTime?: string;
message?: string;
reason?: string;
type: string;
status: string;
}
export interface PodStatus {
phase: string;
conditions: PodCondition[];
hostIP: string;
podIP: string;
podIPs?: {
ip: string;
}[];
startTime: string;
initContainerStatuses?: PodContainerStatus[];
containerStatuses?: PodContainerStatus[];
qosClass?: string;
reason?: string;
}
export class Pod extends KubeObject<PodStatus, PodSpec, KubeObjectScope.Namespace> {
static kind = "Pod";
static namespaced = true;
static apiBase = "/api/v1/pods";
constructor(data: KubeJsonApiData) {
super(data);
autoBind(this);
getAffinityNumber() {
return Object.keys(this.getAffinity()).length;
}
declare spec?: {
volumes?: PodVolume[];
initContainers: IPodContainer[];
containers: IPodContainer[];
restartPolicy?: string;
terminationGracePeriodSeconds?: number;
activeDeadlineSeconds?: number;
dnsPolicy?: string;
serviceAccountName: string;
serviceAccount: string;
automountServiceAccountToken?: boolean;
priority?: number;
priorityClassName?: string;
nodeName?: string;
nodeSelector?: {
[selector: string]: string;
};
securityContext?: {};
imagePullSecrets?: LocalObjectReference[];
hostNetwork?: boolean;
hostPID?: boolean;
hostIPC?: boolean;
shareProcessNamespace?: boolean;
hostname?: string;
subdomain?: string;
schedulerName?: string;
tolerations?: {
key?: string;
operator?: string;
effect?: string;
tolerationSeconds?: number;
value?: string;
}[];
hostAliases?: {
ip: string;
hostnames: string[];
};
affinity?: IAffinity;
};
declare status?: {
phase: string;
conditions: {
type: string;
status: string;
lastProbeTime: number;
lastTransitionTime: string;
}[];
hostIP: string;
podIP: string;
podIPs?: {
ip: string;
}[];
startTime: string;
initContainerStatuses?: IPodContainerStatus[];
containerStatuses?: IPodContainerStatus[];
qosClass?: string;
reason?: string;
};
getInitContainers() {
return this.spec?.initContainers || [];
return this.spec?.initContainers ?? [];
}
getContainers() {
return this.spec?.containers || [];
return this.spec?.containers ?? [];
}
getAllContainers() {
@@ -719,7 +782,7 @@ export class Pod extends WorkloadKubeObject {
getRunningContainers() {
const runningContainerNames = new Set(
this.getContainerStatuses()
.filter(({ state }) => state.running)
.filter(({ state }) => state?.running)
.map(({ name }) => name),
);
@@ -752,10 +815,10 @@ export class Pod extends WorkloadKubeObject {
}
getPriorityClassName() {
return this.spec.priorityClassName || "";
return this.spec?.priorityClassName || "";
}
getStatus(): PodStatus {
getStatus(): PodStatusPhase {
const phase = this.getStatusPhase();
const reason = this.getReason();
const trueConditionTypes = new Set(this.getConditions()
@@ -763,28 +826,28 @@ export class Pod extends WorkloadKubeObject {
.map(({ type }) => type));
const isInGoodCondition = ["Initialized", "Ready"].every(condition => trueConditionTypes.has(condition));
if (reason === PodStatus.EVICTED) {
return PodStatus.EVICTED;
if (reason === PodStatusPhase.EVICTED) {
return PodStatusPhase.EVICTED;
}
if (phase === PodStatus.FAILED) {
return PodStatus.FAILED;
if (phase === PodStatusPhase.FAILED) {
return PodStatusPhase.FAILED;
}
if (phase === PodStatus.SUCCEEDED) {
return PodStatus.SUCCEEDED;
if (phase === PodStatusPhase.SUCCEEDED) {
return PodStatusPhase.SUCCEEDED;
}
if (phase === PodStatus.RUNNING && isInGoodCondition) {
return PodStatus.RUNNING;
if (phase === PodStatusPhase.RUNNING && isInGoodCondition) {
return PodStatusPhase.RUNNING;
}
return PodStatus.PENDING;
return PodStatusPhase.PENDING;
}
// Returns pod phase or container error if occurred
getStatusMessage(): string {
if (this.getReason() === PodStatus.EVICTED) {
if (this.getReason() === PodStatusPhase.EVICTED) {
return "Evicted";
}
@@ -800,31 +863,30 @@ export class Pod extends WorkloadKubeObject {
}
getConditions() {
return this.status?.conditions || [];
return this.status?.conditions ?? [];
}
getVolumes() {
return this.spec.volumes || [];
return this.spec?.volumes ?? [];
}
getSecrets(): string[] {
return this.getVolumes()
.filter(vol => vol.secret)
.map(vol => vol.secret.secretName);
.map(vol => vol.secret?.secretName)
.filter(isDefined);
}
getNodeSelectors(): string[] {
const { nodeSelector = {}} = this.spec;
return Object.entries(nodeSelector).map(values => values.join(": "));
return Object.entries(this.spec?.nodeSelector ?? {})
.map(values => values.join(": "));
}
getTolerations() {
return this.spec.tolerations || [];
return this.spec?.tolerations ?? [];
}
getAffinity(): IAffinity {
return this.spec.affinity;
getAffinity(): Affinity {
return this.spec?.affinity ?? {};
}
hasIssues() {
@@ -843,19 +905,19 @@ export class Pod extends WorkloadKubeObject {
return this.getStatusPhase() !== "Running";
}
getLivenessProbe(container: IPodContainer) {
getLivenessProbe(container: PodContainer) {
return this.getProbe(container, "livenessProbe");
}
getReadinessProbe(container: IPodContainer) {
getReadinessProbe(container: PodContainer) {
return this.getProbe(container, "readinessProbe");
}
getStartupProbe(container: IPodContainer) {
getStartupProbe(container: PodContainer) {
return this.getProbe(container, "startupProbe");
}
private getProbe(container: IPodContainer, field: PodContainerProbe): string[] {
private getProbe(container: PodContainer, field: PodContainerProbe): string[] {
const probe: string[] = [];
const probeData = container[field];
@@ -907,30 +969,17 @@ export class Pod extends WorkloadKubeObject {
return probe;
}
getNodeName() {
return this.spec.nodeName;
getNodeName(): string | undefined {
return this.spec?.nodeName;
}
getSelectedNodeOs(): string | undefined {
return this.spec.nodeSelector?.["kubernetes.io/os"] || this.spec.nodeSelector?.["beta.kubernetes.io/os"];
return this.spec?.nodeSelector?.["kubernetes.io/os"] || this.spec?.nodeSelector?.["beta.kubernetes.io/os"];
}
getIPs(): string[] {
if(!this.status.podIPs) return [];
const podIPs = this.status.podIPs;
const podIPs = this.status?.podIPs ?? [];
return podIPs.map(value => value.ip);
}
}
let podsApi: PodsApi;
if (isClusterPageContext()) {
podsApi = new PodsApi({
objectConstructor: Pod,
});
}
export {
podsApi,
};
@@ -1,71 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { autoBind } from "../../utils";
import type { LabelSelector } from "../kube-object";
import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
export interface PodDisruptionBudget {
spec: {
minAvailable: string;
maxUnavailable: string;
selector: LabelSelector;
};
status: {
currentHealthy: number;
desiredHealthy: number;
disruptionsAllowed: number;
expectedPods: number;
};
}
export class PodDisruptionBudget extends KubeObject {
static kind = "PodDisruptionBudget";
static namespaced = true;
static apiBase = "/apis/policy/v1beta1/poddisruptionbudgets";
constructor(data: KubeJsonApiData) {
super(data);
autoBind(this);
}
getSelectors() {
const selector = this.spec.selector;
return KubeObject.stringifyLabels(selector ? selector.matchLabels : null);
}
getMinAvailable() {
return this.spec.minAvailable || "N/A";
}
getMaxUnavailable() {
return this.spec.maxUnavailable || "N/A";
}
getCurrentHealthy() {
return this.status.currentHealthy;
}
getDesiredHealthy() {
return this.status.desiredHealthy;
}
}
let pdbApi: KubeApi<PodDisruptionBudget>;
if (isClusterPageContext()) {
pdbApi = new KubeApi({
objectConstructor: PodDisruptionBudget,
});
}
export {
pdbApi,
};
@@ -1,115 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { autoBind } from "../../utils";
import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
export interface PodSecurityPolicy {
spec: {
allowPrivilegeEscalation?: boolean;
allowedCSIDrivers?: {
name: string;
}[];
allowedCapabilities: string[];
allowedFlexVolumes?: {
driver: string;
}[];
allowedHostPaths?: {
pathPrefix: string;
readOnly: boolean;
}[];
allowedProcMountTypes?: string[];
allowedUnsafeSysctls?: string[];
defaultAddCapabilities?: string[];
defaultAllowPrivilegeEscalation?: boolean;
forbiddenSysctls?: string[];
fsGroup?: {
rule: string;
ranges: { max: number; min: number }[];
};
hostIPC?: boolean;
hostNetwork?: boolean;
hostPID?: boolean;
hostPorts?: {
max: number;
min: number;
}[];
privileged?: boolean;
readOnlyRootFilesystem?: boolean;
requiredDropCapabilities?: string[];
runAsGroup?: {
ranges: { max: number; min: number }[];
rule: string;
};
runAsUser?: {
rule: string;
ranges: { max: number; min: number }[];
};
runtimeClass?: {
allowedRuntimeClassNames: string[];
defaultRuntimeClassName: string;
};
seLinux?: {
rule: string;
seLinuxOptions: {
level: string;
role: string;
type: string;
user: string;
};
};
supplementalGroups?: {
rule: string;
ranges: { max: number; min: number }[];
};
volumes?: string[];
};
}
export class PodSecurityPolicy extends KubeObject {
static kind = "PodSecurityPolicy";
static namespaced = false;
static apiBase = "/apis/policy/v1beta1/podsecuritypolicies";
constructor(data: KubeJsonApiData) {
super(data);
autoBind(this);
}
isPrivileged() {
return !!this.spec.privileged;
}
getVolumes() {
return this.spec.volumes || [];
}
getRules() {
const { fsGroup, runAsGroup, runAsUser, supplementalGroups, seLinux } = this.spec;
return {
fsGroup: fsGroup ? fsGroup.rule : "",
runAsGroup: runAsGroup ? runAsGroup.rule : "",
runAsUser: runAsUser ? runAsUser.rule : "",
supplementalGroups: supplementalGroups ? supplementalGroups.rule : "",
seLinux: seLinux ? seLinux.rule : "",
};
}
}
let pspApi: KubeApi<PodSecurityPolicy>;
if (isClusterPageContext()) {
pspApi = new KubeApi({
objectConstructor: PodSecurityPolicy,
});
}
export {
pspApi,
};
@@ -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 { ReplicaSetApi } from "./replica-set.api";
const replicaSetApiInjectable = getInjectable({
id: "replica-set-api",
instantiate: (di) => {
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "replicaSetApi is only available in certain environments");
return new ReplicaSetApi();
},
});
export default replicaSetApiInjectable;
+59 -61
View File
@@ -3,25 +3,30 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import get from "lodash/get";
import { autoBind } from "../../../renderer/utils";
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 { IPodContainer, IPodMetrics, Pod } 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 } 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 ReplicaSetApi extends KubeApi<ReplicaSet> {
constructor(opts: DerivedKubeApiOptions & IgnoredKubeApiOptions = {}) {
super({
...opts,
objectConstructor: ReplicaSet,
});
}
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));
return (status as { replicas: number })?.replicas;
}
scale(params: { namespace: string; name: string }, replicas: number) {
@@ -36,7 +41,7 @@ export class ReplicaSetApi extends KubeApi<ReplicaSet> {
}
}
export function getMetricsForReplicaSets(replicasets: ReplicaSet[], namespace: string, selector = ""): Promise<IPodMetrics> {
export function getMetricsForReplicaSets(replicasets: ReplicaSet[], namespace: string, selector = ""): Promise<PodMetricData> {
const podSelector = replicasets.map(replicaset => `${replicaset.getName()}-[[:alnum:]]{5}`).join("|");
const opts = { category: "pods", pods: podSelector, namespace, selector };
@@ -53,72 +58,65 @@ export function getMetricsForReplicaSets(replicasets: ReplicaSet[], namespace: s
});
}
export class ReplicaSet extends WorkloadKubeObject {
export interface ReplicaSetSpec {
replicas?: number;
selector: LabelSelector;
template?: PodTemplateSpec;
minReadySeconds?: number;
}
export interface ReplicaSetStatus extends KubeObjectStatus {
replicas: number;
fullyLabeledReplicas?: number;
readyReplicas?: number;
availableReplicas?: number;
observedGeneration?: number;
}
export class ReplicaSet extends KubeObject<ReplicaSetStatus, ReplicaSetSpec, KubeObjectScope.Namespace> {
static kind = "ReplicaSet";
static namespaced = true;
static apiBase = "/apis/apps/v1/replicasets";
constructor(data: KubeJsonApiData) {
super(data);
autoBind(this);
getSelectors(): string[] {
return KubeObject.stringifyLabels(this.spec.selector.matchLabels);
}
declare spec: {
replicas?: number;
selector: LabelSelector;
template?: {
metadata: {
labels: {
app: string;
};
};
spec?: Pod["spec"];
};
minReadySeconds?: number;
};
declare status: {
replicas: number;
fullyLabeledReplicas?: number;
readyReplicas?: number;
availableReplicas?: number;
observedGeneration?: 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;
}
getDesired() {
return this.spec.replicas || 0;
return this.spec.replicas ?? 0;
}
getCurrent() {
return this.status.availableReplicas || 0;
return this.status?.availableReplicas ?? 0;
}
getReady() {
return this.status.readyReplicas || 0;
return this.status?.readyReplicas ?? 0;
}
getImages() {
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []);
const containers = this.spec.template?.spec?.containers ?? [];
return [...containers].map(container => container.image);
return containers.map(container => container.image);
}
}
let replicaSetApi: ReplicaSetApi;
if (isClusterPageContext()) {
replicaSetApi = new ReplicaSetApi({
objectConstructor: ReplicaSet,
});
}
export {
replicaSetApi,
};
@@ -16,7 +16,7 @@ export async function update(resource: object | string): Promise<KubeJsonApiData
if (typeof resource === "string") {
const parsed = yaml.load(resource);
if (typeof parsed !== "object") {
if (!parsed || typeof parsed !== "object") {
throw new Error("Cannot update resource to string or number");
}
@@ -26,7 +26,7 @@ export async function update(resource: object | string): Promise<KubeJsonApiData
return apiBase.post<KubeJsonApiData>("/stack", { data: resource });
}
export async function patch(name: string, kind: string, ns: string, patch: Patch): Promise<KubeJsonApiData> {
export async function patch(name: string, kind: string, ns: string | undefined, patch: Patch): Promise<KubeJsonApiData> {
return apiBase.patch<KubeJsonApiData>("/stack", {
data: {
name,
@@ -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 { ResourceQuotaApi } from "./resource-quota.api";
const resourceQuotaApiInjectable = getInjectable({
id: "resource-quota-api",
instantiate: (di) => {
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "resourceQuotaApi is only available in certain environments");
return new ResourceQuotaApi();
},
});
export default resourceQuotaApiInjectable;
@@ -3,13 +3,12 @@
* 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 { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
export interface IResourceQuotaValues {
[quota: string]: string;
export type IResourceQuotaValues = Partial<Record<string, string>> & {
// Compute Resource Quota
"limits.cpu"?: string;
"limits.memory"?: string;
@@ -33,46 +32,39 @@ export interface IResourceQuotaValues {
"count/jobs.batch"?: string;
"count/cronjobs.batch"?: string;
"count/deployments.extensions"?: string;
}
};
export interface ResourceQuota {
spec: {
hard: IResourceQuotaValues;
scopeSelector?: {
matchExpressions: {
operator: string;
scopeName: string;
values: string[];
}[];
};
};
status: {
hard: IResourceQuotaValues;
used: IResourceQuotaValues;
export interface ResourceQuotaSpec {
hard: IResourceQuotaValues;
scopeSelector?: {
matchExpressions: {
operator: string;
scopeName: string;
values: string[];
}[];
};
}
export class ResourceQuota extends KubeObject {
static kind = "ResourceQuota";
static namespaced = true;
static apiBase = "/api/v1/resourcequotas";
export interface ResourceQuotaStatus {
hard: IResourceQuotaValues;
used: IResourceQuotaValues;
}
export class ResourceQuota extends KubeObject<ResourceQuotaStatus, ResourceQuotaSpec, KubeObjectScope.Namespace> {
static readonly kind = "ResourceQuota";
static readonly namespaced = true;
static readonly apiBase = "/api/v1/resourcequotas";
getScopeSelector() {
const { matchExpressions = [] } = this.spec.scopeSelector || {};
return matchExpressions;
return this.spec.scopeSelector?.matchExpressions ?? [];
}
}
let resourceQuotaApi: KubeApi<ResourceQuota>;
if (isClusterPageContext()) {
resourceQuotaApi = new KubeApi<ResourceQuota>({
objectConstructor: ResourceQuota,
});
export class ResourceQuotaApi extends KubeApi<ResourceQuota> {
constructor(opts: DerivedKubeApiOptions = {}) {
super({
objectConstructor: ResourceQuota,
...opts,
});
}
}
export {
resourceQuotaApi,
};
@@ -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 { RoleBindingApi } from "./role-binding.api";
const roleBindingApiInjectable = getInjectable({
id: "role-binding-api",
instantiate: (di) => {
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "roleBindingApi is only available in certain environments");
return new RoleBindingApi();
},
});
export default roleBindingApiInjectable;
@@ -3,38 +3,31 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { autoBind } from "../../utils";
import type { KubeObjectMetadata, KubeObjectScope } 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 { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
import type { RoleRef } from "./types/role-ref";
import type { Subject } from "./types/subject";
export type RoleBindingSubjectKind = "Group" | "ServiceAccount" | "User";
export interface RoleBindingSubject {
kind: RoleBindingSubjectKind;
name: string;
namespace?: string;
apiGroup?: string;
export interface RoleBindingData extends KubeJsonApiData<KubeObjectMetadata<KubeObjectScope.Namespace>, void, void> {
subjects?: Subject[];
roleRef: RoleRef;
}
export interface RoleBinding {
subjects?: RoleBindingSubject[];
roleRef: {
kind: string;
name: string;
apiGroup?: string;
};
}
export class RoleBinding extends KubeObject<void, void, KubeObjectScope.Namespace> {
static readonly kind = "RoleBinding";
static readonly namespaced = true;
static readonly apiBase = "/apis/rbac.authorization.k8s.io/v1/rolebindings";
export class RoleBinding extends KubeObject {
static kind = "RoleBinding";
static namespaced = true;
static apiBase = "/apis/rbac.authorization.k8s.io/v1/rolebindings";
subjects?: Subject[];
roleRef: RoleRef;
constructor(data: KubeJsonApiData) {
super(data);
autoBind(this);
constructor({ subjects, roleRef, ...rest }: RoleBindingData) {
super(rest);
this.subjects = subjects;
this.roleRef = roleRef;
}
getSubjects() {
@@ -46,14 +39,11 @@ export class RoleBinding extends KubeObject {
}
}
let roleBindingApi: KubeApi<RoleBinding>;
if (isClusterPageContext()) {
roleBindingApi = new KubeApi({
objectConstructor: RoleBinding,
});
export class RoleBindingApi extends KubeApi<RoleBinding, RoleBindingData> {
constructor(opts: DerivedKubeApiOptions = {}) {
super({
...opts,
objectConstructor: RoleBinding,
});
}
}
export {
roleBindingApi,
};
@@ -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 { RoleApi } from "./role.api";
const roleApiInjectable = getInjectable({
id: "role-api",
instantiate: (di) => {
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "roleApi is only available in certain environments");
return new RoleApi();
},
});
export default roleApiInjectable;
+23 -22
View File
@@ -3,37 +3,38 @@
* 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 { DerivedKubeApiOptions } from "../kube-api";
import { KubeApi } from "../kube-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
import type { KubeJsonApiData } from "../kube-json-api";
import type { PolicyRule } from "./types/policy-rule";
export interface Role {
rules: {
verbs: string[];
apiGroups: string[];
resources: string[];
resourceNames?: string[];
}[];
export interface RoleData extends KubeJsonApiData<KubeObjectMetadata<KubeObjectScope.Namespace>, void, void> {
rules?: PolicyRule[];
}
export class Role extends KubeObject {
static kind = "Role";
static namespaced = true;
static apiBase = "/apis/rbac.authorization.k8s.io/v1/roles";
export class Role extends KubeObject<void, void, KubeObjectScope.Namespace> {
static readonly kind = "Role";
static readonly namespaced = true;
static readonly apiBase = "/apis/rbac.authorization.k8s.io/v1/roles";
rules?: PolicyRule[];
constructor({ rules, ...rest }: RoleData) {
super(rest);
this.rules = rules;
}
getRules() {
return this.rules || [];
}
}
let roleApi: KubeApi<Role>;
if (isClusterPageContext()) {
roleApi = new KubeApi<Role>({
objectConstructor: Role,
});
export class RoleApi extends KubeApi<Role, RoleData> {
constructor(opts: DerivedKubeApiOptions = {}) {
super({
...opts,
objectConstructor: Role,
});
}
}
export{
roleApi,
};
@@ -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 { SecretApi } from "./secret.api";
const secretApiInjectable = getInjectable({
id: "secret-api",
instantiate: (di) => {
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "secretApi is only available in certain environments");
return new SecretApi();
},
});
export default secretApiInjectable;
+31 -26
View File
@@ -3,11 +3,12 @@
* 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 { autoBind } from "../../utils";
import type { DerivedKubeApiOptions } from "../kube-api";
import { KubeApi } from "../kube-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
export enum SecretType {
Opaque = "Opaque",
@@ -20,34 +21,41 @@ export enum SecretType {
BootstrapToken = "bootstrap.kubernetes.io/token",
}
export interface ISecretRef {
key?: string;
name: string;
}
export const reverseSecretTypeMap = {
[SecretType.Opaque]: "Opaque",
[SecretType.ServiceAccountToken]: "ServiceAccountToken",
[SecretType.Dockercfg]: "Dockercfg",
[SecretType.DockerConfigJson]: "DockerConfigJson",
[SecretType.BasicAuth]: "BasicAuth",
[SecretType.SSHAuth]: "SSHAuth",
[SecretType.TLS]: "TLS",
[SecretType.BootstrapToken]: "BootstrapToken",
};
export interface SecretReference {
name: string;
namespace?: string;
}
export interface SecretData extends KubeJsonApiData {
export interface SecretData extends KubeJsonApiData<KubeObjectMetadata<KubeObjectScope.Namespace>, void, void> {
type: SecretType;
data?: Record<string, string>;
data?: Partial<Record<string, string>>;
}
export class Secret extends KubeObject {
static kind = "Secret";
static namespaced = true;
static apiBase = "/api/v1/secrets";
export class Secret extends KubeObject<void, void, KubeObjectScope.Namespace> {
static readonly kind = "Secret";
static readonly namespaced = true;
static readonly apiBase = "/api/v1/secrets";
declare type: SecretType;
declare data: Record<string, string>;
type: SecretType;
data: Partial<Record<string, string>>;
constructor(data: SecretData) {
super(data);
constructor({ data = {}, type, ...rest }: SecretData) {
super(rest);
autoBind(this);
this.data ??= {};
this.data = data;
this.type = type;
}
getKeys(): string[] {
@@ -59,14 +67,11 @@ export class Secret extends KubeObject {
}
}
let secretsApi: KubeApi<Secret>;
if (isClusterPageContext()) {
secretsApi = new KubeApi({
objectConstructor: Secret,
});
export class SecretApi extends KubeApi<Secret, SecretData> {
constructor(options: DerivedKubeApiOptions = {}) {
super({
...options,
objectConstructor: Secret,
});
}
}
export {
secretsApi,
};
@@ -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 { SelfSubjectRulesReviewApi } from "./self-subject-rules-reviews.api";
const selfSubjectRulesReviewApiInjectable = getInjectable({
id: "self-subject-rules-review-api",
instantiate: (di) => {
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "selfSubjectRulesReviewApi is only available in certain environments");
return new SelfSubjectRulesReviewApi();
},
});
export default selfSubjectRulesReviewApiInjectable;
@@ -4,11 +4,18 @@
*/
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 SelfSubjectRulesReviewApi extends KubeApi<SelfSubjectRulesReview> {
create({ namespace = "default" }): Promise<SelfSubjectRulesReview> {
constructor(opts: DerivedKubeApiOptions & IgnoredKubeApiOptions = {}) {
super({
...opts,
objectConstructor: SelfSubjectRulesReview,
});
}
create({ namespace = "default" }) {
return super.create({}, {
spec: {
namespace,
@@ -71,15 +78,3 @@ export class SelfSubjectRulesReview extends KubeObject {
}
}
let selfSubjectRulesReviewApi: SelfSubjectRulesReviewApi;
if (isClusterPageContext()) {
selfSubjectRulesReviewApi = new SelfSubjectRulesReviewApi({
objectConstructor: SelfSubjectRulesReview,
});
}
export {
selfSubjectRulesReviewApi,
};
@@ -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 { ServiceAccountApi } from "./service-account.api";
const serviceAccountApiInjectable = getInjectable({
id: "service-account-api",
instantiate: (di) => {
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "serviceAccountApi is only available in certain environments");
return new ServiceAccountApi();
},
});
export default serviceAccountApiInjectable;
@@ -0,0 +1,55 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { KubeObjectMetadata, KubeObjectScope, LocalObjectReference, 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";
export interface ServiceAccountData extends KubeJsonApiData<KubeObjectMetadata<KubeObjectScope.Namespace>, void, void> {
automountServiceAccountToken?: boolean;
imagePullSecrets?: LocalObjectReference[];
secrets?: ObjectReference[];
}
export class ServiceAccount extends KubeObject<void, void, KubeObjectScope.Namespace> {
static readonly kind = "ServiceAccount";
static readonly namespaced = true;
static readonly apiBase = "/api/v1/serviceaccounts";
automountServiceAccountToken?: boolean;
imagePullSecrets?: LocalObjectReference[];
secrets?: ObjectReference[];
constructor({
automountServiceAccountToken,
imagePullSecrets,
secrets,
...rest
}: ServiceAccountData) {
super(rest);
this.automountServiceAccountToken = automountServiceAccountToken;
this.imagePullSecrets = imagePullSecrets;
this.secrets = secrets;
}
getSecrets() {
return this.secrets || [];
}
getImagePullSecrets() {
return this.imagePullSecrets || [];
}
}
export class ServiceAccountApi extends KubeApi<ServiceAccount, ServiceAccountData> {
constructor(opts: DerivedKubeApiOptions = {}) {
super({
...opts,
objectConstructor: ServiceAccount,
});
}
}
@@ -1,50 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { autoBind } from "../../utils";
import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
export interface ServiceAccount {
secrets?: {
name: string;
}[];
imagePullSecrets?: {
name: string;
}[];
}
export class ServiceAccount extends KubeObject {
static kind = "ServiceAccount";
static namespaced = true;
static apiBase = "/api/v1/serviceaccounts";
constructor(data: KubeJsonApiData) {
super(data);
autoBind(this);
}
getSecrets() {
return this.secrets || [];
}
getImagePullSecrets() {
return this.imagePullSecrets || [];
}
}
let serviceAccountsApi: KubeApi<ServiceAccount>;
if (isClusterPageContext()) {
serviceAccountsApi = new KubeApi<ServiceAccount>({
objectConstructor: ServiceAccount,
});
}
export {
serviceAccountsApi,
};
@@ -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 { ServiceApi } from "./service.api";
const serviceApiInjectable = getInjectable({
id: "service-api",
instantiate: (di) => {
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "serviceApi is only available in certain environments");
return new ServiceApi();
},
});
export default serviceApiInjectable;
+40 -51
View File
@@ -3,11 +3,10 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { autoBind } from "../../../renderer/utils";
import type { KubeObjectScope } 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 { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
export interface ServicePort {
name?: string;
@@ -31,47 +30,40 @@ export class ServicePort {
}
}
export interface Service {
spec: {
type: string;
clusterIP: string;
clusterIPs?: string[];
externalTrafficPolicy?: string;
externalName?: string;
loadBalancerIP?: string;
loadBalancerSourceRanges?: string[];
sessionAffinity: string;
selector: { [key: string]: string };
ports: ServicePort[];
healthCheckNodePort?: number;
externalIPs?: string[]; // https://kubernetes.io/docs/concepts/services-networking/service/#external-ips
topologyKeys?: string[];
ipFamilies?: string[];
ipFamilyPolicy?: string;
allocateLoadBalancerNodePorts?: boolean;
loadBalancerClass?: string;
internalTrafficPolicy?: string;
};
export interface ServiceSpec {
type: string;
clusterIP: string;
clusterIPs?: string[];
externalTrafficPolicy?: string;
externalName?: string;
loadBalancerIP?: string;
loadBalancerSourceRanges?: string[];
sessionAffinity: string;
selector: Partial<Record<string, string>>;
ports: ServicePort[];
healthCheckNodePort?: number;
externalIPs?: string[]; // https://kubernetes.io/docs/concepts/services-networking/service/#external-ips
topologyKeys?: string[];
ipFamilies?: string[];
ipFamilyPolicy?: string;
allocateLoadBalancerNodePorts?: boolean;
loadBalancerClass?: string;
internalTrafficPolicy?: string;
}
status: {
loadBalancer?: {
ingress?: {
ip?: string;
hostname?: string;
}[];
};
export interface ServiceStatus {
loadBalancer?: {
ingress?: {
ip?: string;
hostname?: string;
}[];
};
}
export class Service extends KubeObject {
static kind = "Service";
static namespaced = true;
static apiBase = "/api/v1/services";
constructor(data: KubeJsonApiData) {
super(data);
autoBind(this);
}
export class Service extends KubeObject<ServiceStatus, ServiceSpec, KubeObjectScope.Namespace> {
static readonly kind = "Service";
static readonly namespaced = true;
static readonly apiBase = "/api/v1/services";
getClusterIp() {
return this.spec.clusterIP;
@@ -112,7 +104,7 @@ export class Service extends KubeObject {
}
getLoadBalancer() {
return this.status.loadBalancer;
return this.status?.loadBalancer;
}
isActive() {
@@ -132,14 +124,11 @@ export class Service extends KubeObject {
}
}
let serviceApi: KubeApi<Service>;
if (isClusterPageContext()) {
serviceApi = new KubeApi<Service>({
objectConstructor: Service,
});
export class ServiceApi extends KubeApi<Service> {
constructor(opts: DerivedKubeApiOptions = {}) {
super({
...opts,
objectConstructor: Service,
});
}
}
export {
serviceApi,
};
@@ -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 { StatefulSetApi } from "./stateful-set.api";
const statefulSetApiInjectable = getInjectable({
id: "stateful-set-api",
instantiate: (di) => {
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "statefulSetApi is only available in certain environments");
return new StatefulSetApi();
},
});
export default statefulSetApiInjectable;
Loaded 100 of 1062 files, more files were not shown because too many files have changed in this diff. Show more