mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Restructure to monorepo (#6907)
* wip: restructure to monorepo Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * refactor create-release-pr to a package Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * build fixes Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * github workflow fixes Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix typo Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * add webpack-env types to core Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix github workflows Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * refactor/fix integration tests Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * lint fix Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * yarn run dev Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * eslint settings for vscode Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * move templates to right package Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * open-lens build fixes Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * integration test fix Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix nx task dependencies Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * use bash shell for unit tests in test workflow Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix test:unit for windows Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix win-ca webpack error in open-lens Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix win-ca webpack error in open-lens Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix build:app on windows Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * remove ELECTRON_BUILDER_EXTRA_ARGS Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * sync src/ from master Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * remove Makefile from core Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
1 parent
0066ca4ea2
commit
2657df2293
3321 files changed
+28699
-11447
No files matched your search
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
import createClusterInjectable from "../../../main/create-cluster/create-cluster.injectable";
|
||||
import clusterFrameContextForNamespacedResourcesInjectable from "../../../renderer/cluster-frame-context/for-namespaced-resources.injectable";
|
||||
import hostedClusterInjectable from "../../../renderer/cluster-frame-context/hosted-cluster.injectable";
|
||||
import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting";
|
||||
import storesAndApisCanBeCreatedInjectable from "../../../renderer/stores-apis-can-be-created.injectable";
|
||||
import directoryForKubeConfigsInjectable from "../../app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
|
||||
import directoryForUserDataInjectable from "../../app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import loggerInjectable from "../../logger.injectable";
|
||||
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";
|
||||
import maybeKubeApiInjectable from "../maybe-kube-api.injectable";
|
||||
|
||||
class TestApi extends KubeApi<KubeObject> {
|
||||
protected async checkPreferredVersion() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
class TestStore extends KubeObjectStore<KubeObject, TestApi> {
|
||||
|
||||
}
|
||||
|
||||
describe("ApiManager", () => {
|
||||
let apiManager: ApiManager;
|
||||
let di: DiContainer;
|
||||
|
||||
beforeEach(() => {
|
||||
di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
di.override(directoryForUserDataInjectable, () => "/some-user-store-path");
|
||||
di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs");
|
||||
di.override(storesAndApisCanBeCreatedInjectable, () => true);
|
||||
|
||||
const createCluster = di.inject(createClusterInjectable);
|
||||
|
||||
di.override(hostedClusterInjectable, () => createCluster({
|
||||
contextName: "some-context-name",
|
||||
id: "some-cluster-id",
|
||||
kubeConfigPath: "/some-path-to-a-kubeconfig",
|
||||
}, {
|
||||
clusterServerUrl: "https://localhost:8080",
|
||||
}));
|
||||
|
||||
apiManager = di.inject(apiManagerInjectable);
|
||||
});
|
||||
|
||||
describe("registerApi", () => {
|
||||
it("re-register store if apiBase changed", async () => {
|
||||
const apiBase = "apis/v1/foo";
|
||||
const fallbackApiBase = "/apis/extensions/v1beta1/foo";
|
||||
const kubeApi = new TestApi({
|
||||
logger: di.inject(loggerInjectable),
|
||||
maybeKubeApi: di.inject(maybeKubeApiInjectable),
|
||||
}, {
|
||||
objectConstructor: KubeObject,
|
||||
apiBase,
|
||||
kind: "foo",
|
||||
fallbackApiBases: [fallbackApiBase],
|
||||
checkPreferredVersion: true,
|
||||
});
|
||||
const kubeStore = new TestStore({
|
||||
context: di.inject(clusterFrameContextForNamespacedResourcesInjectable),
|
||||
logger: di.inject(loggerInjectable),
|
||||
}, kubeApi);
|
||||
|
||||
apiManager.registerApi(apiBase, kubeApi);
|
||||
|
||||
// Define to use test api for ingress store
|
||||
Object.defineProperty(kubeStore, "api", { value: kubeApi });
|
||||
apiManager.registerStore(kubeStore, [kubeApi]);
|
||||
|
||||
// Test that store is returned with original apiBase
|
||||
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(kubeStore);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { CustomResourceDefinitionSpec } from "../endpoints";
|
||||
import { CustomResourceDefinition } from "../endpoints";
|
||||
|
||||
describe("Crds", () => {
|
||||
describe("getVersion()", () => {
|
||||
it("should throw if none of the versions are served", () => {
|
||||
const crd = new CustomResourceDefinition({
|
||||
apiVersion: "apiextensions.k8s.io/v1",
|
||||
kind: "CustomResourceDefinition",
|
||||
metadata: {
|
||||
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",
|
||||
served: false,
|
||||
storage: false,
|
||||
},
|
||||
{
|
||||
name: "1234",
|
||||
served: false,
|
||||
storage: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(() => crd.getVersion()).toThrowError("Failed to find a version for CustomResourceDefinition foo");
|
||||
});
|
||||
|
||||
it("should get the version that is both served and stored", () => {
|
||||
const crd = new CustomResourceDefinition({
|
||||
apiVersion: "apiextensions.k8s.io/v1",
|
||||
kind: "CustomResourceDefinition",
|
||||
metadata: {
|
||||
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",
|
||||
served: true,
|
||||
storage: true,
|
||||
},
|
||||
{
|
||||
name: "1234",
|
||||
served: false,
|
||||
storage: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(crd.getVersion()).toBe("123");
|
||||
});
|
||||
|
||||
it("should get the version that is only stored", () => {
|
||||
const crd = new CustomResourceDefinition({
|
||||
apiVersion: "apiextensions.k8s.io/v1",
|
||||
kind: "CustomResourceDefinition",
|
||||
metadata: {
|
||||
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",
|
||||
served: false,
|
||||
storage: true,
|
||||
},
|
||||
{
|
||||
name: "1234",
|
||||
served: false,
|
||||
storage: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(crd.getVersion()).toBe("123");
|
||||
});
|
||||
|
||||
it("should get the version that is both served and stored even with version field", () => {
|
||||
const crd = new CustomResourceDefinition({
|
||||
apiVersion: "apiextensions.k8s.io/v1",
|
||||
kind: "CustomResourceDefinition",
|
||||
metadata: {
|
||||
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: [
|
||||
{
|
||||
name: "123",
|
||||
served: true,
|
||||
storage: true,
|
||||
},
|
||||
{
|
||||
name: "1234",
|
||||
served: false,
|
||||
storage: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(crd.getVersion()).toBe("123");
|
||||
});
|
||||
|
||||
it("should get the version name from the version field, ignoring versions on v1beta", () => {
|
||||
const crd = new CustomResourceDefinition({
|
||||
apiVersion: "apiextensions.k8s.io/v1beta1",
|
||||
kind: "CustomResourceDefinition",
|
||||
metadata: {
|
||||
name: "foo",
|
||||
resourceVersion: "12345",
|
||||
uid: "12345",
|
||||
selfLink: "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/foo",
|
||||
},
|
||||
spec: {
|
||||
version: "abc",
|
||||
versions: [
|
||||
{
|
||||
name: "foobar",
|
||||
served: true,
|
||||
storage: true,
|
||||
},
|
||||
],
|
||||
} as CustomResourceDefinitionSpec,
|
||||
});
|
||||
|
||||
expect(crd.getVersion()).toBe("abc");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
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";
|
||||
|
||||
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", () => {
|
||||
it("requests Kubernetes API with PATCH verb and correct amount of replicas", () => {
|
||||
deploymentApi.scale({ namespace: "default", name: "deployment-1" }, 5);
|
||||
|
||||
expect(kubeJsonApi.patch).toHaveBeenCalledWith("/apis/apps/v1/namespaces/default/deployments/deployment-1/scale", {
|
||||
data: {
|
||||
spec: {
|
||||
replicas: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"content-type": "application/merge-patch+json",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { formatEndpointSubset } from "../endpoints";
|
||||
|
||||
describe("endpoint tests", () => {
|
||||
describe("EndpointSubset", () => {
|
||||
it("formatEndpointSubset should be addresses X ports", () => {
|
||||
const formatted = formatEndpointSubset({
|
||||
addresses: [{
|
||||
ip: "1.1.1.1",
|
||||
}, {
|
||||
ip: "1.1.1.2",
|
||||
}],
|
||||
notReadyAddresses: [],
|
||||
ports: [{
|
||||
port: 81,
|
||||
}, {
|
||||
port: 82,
|
||||
}],
|
||||
});
|
||||
|
||||
expect(formatted).toBe("1.1.1.1:81, 1.1.1.1:82, 1.1.1.2:81, 1.1.1.2:82");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,275 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { anyObject } from "jest-mock-extended";
|
||||
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 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 never)).toThrowError('"apiVersion" is required');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "!",
|
||||
} as never)).toThrowError('"name" is required');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "!",
|
||||
name: "!",
|
||||
} as never)).toThrowError('"version" is required');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "!",
|
||||
name: "!",
|
||||
version: "!",
|
||||
} as never)).toThrowError('"repo" is required');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "!",
|
||||
name: "!",
|
||||
version: "!",
|
||||
repo: "!",
|
||||
} as never)).toThrowError('"created" is required');
|
||||
});
|
||||
|
||||
it("should throw on fields being wrong type", () => {
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: 1,
|
||||
name: "!",
|
||||
version: "!",
|
||||
repo: "!",
|
||||
created: "!",
|
||||
digest: "!",
|
||||
} as never)).toThrowError('"apiVersion" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: 1,
|
||||
version: "!",
|
||||
repo: "!",
|
||||
created: "!",
|
||||
digest: "!",
|
||||
} as never)).toThrowError('"name" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "!",
|
||||
name: "!",
|
||||
version: "!",
|
||||
repo: "!",
|
||||
created: "!",
|
||||
digest: 1,
|
||||
} as never)).toThrowError('"digest" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "",
|
||||
version: 1,
|
||||
repo: "!",
|
||||
created: "!",
|
||||
digest: "!",
|
||||
} as never)).toThrowError('"version" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
version: "1",
|
||||
repo: 1,
|
||||
created: "!",
|
||||
digest: "!",
|
||||
} as never)).toThrowError('"repo" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
version: "1",
|
||||
repo: "1",
|
||||
created: 1,
|
||||
digest: "a",
|
||||
} as never)).toThrowError('"created" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
version: "1",
|
||||
repo: "1",
|
||||
created: "!",
|
||||
digest: 1,
|
||||
} as never)).toThrowError('"digest" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
version: "1",
|
||||
repo: "1",
|
||||
digest: "1",
|
||||
created: "!",
|
||||
kubeVersion: 1,
|
||||
} as never)).toThrowError('"kubeVersion" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
version: "1",
|
||||
repo: "1",
|
||||
digest: "1",
|
||||
created: "!",
|
||||
description: 1,
|
||||
} as never)).toThrowError('"description" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
version: "1",
|
||||
repo: "1",
|
||||
digest: "1",
|
||||
created: "!",
|
||||
home: 1,
|
||||
} as never)).toThrowError('"home" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
version: "1",
|
||||
repo: "1",
|
||||
digest: "1",
|
||||
created: "!",
|
||||
engine: 1,
|
||||
} as never)).toThrowError('"engine" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
version: "1",
|
||||
repo: "1",
|
||||
digest: "1",
|
||||
created: "!",
|
||||
icon: 1,
|
||||
} as never)).toThrowError('"icon" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
version: "1",
|
||||
repo: "1",
|
||||
digest: "1",
|
||||
created: "!",
|
||||
appVersion: 1,
|
||||
} as never)).toThrowError('"appVersion" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
version: "1",
|
||||
repo: "1",
|
||||
digest: "1",
|
||||
created: "!",
|
||||
tillerVersion: 1,
|
||||
} as never)).toThrowError('"tillerVersion" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
version: "1",
|
||||
repo: "1",
|
||||
digest: "1",
|
||||
created: "!",
|
||||
deprecated: 1,
|
||||
} as never)).toThrowError('"deprecated" must be a boolean');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
version: "1",
|
||||
repo: "1",
|
||||
digest: "1",
|
||||
created: "!",
|
||||
keywords: 1,
|
||||
} as never)).toThrowError('"keywords" must be an array');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
version: "1",
|
||||
repo: "1",
|
||||
digest: "1",
|
||||
created: "!",
|
||||
sources: 1,
|
||||
} as never)).toThrowError('"sources" must be an array');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
version: "1",
|
||||
repo: "1",
|
||||
digest: "1",
|
||||
created: "!",
|
||||
maintainers: 1,
|
||||
} as never)).toThrowError('"maintainers" must be an array');
|
||||
});
|
||||
|
||||
it("should filter non-string keywords", () => {
|
||||
const chart = HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
version: "1",
|
||||
repo: "1",
|
||||
digest: "1",
|
||||
created: "!",
|
||||
keywords: [1, "a", false, {}, "b"] as never,
|
||||
});
|
||||
|
||||
expect(chart?.keywords).toStrictEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
it("should filter non-string sources", () => {
|
||||
const chart = HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
version: "1",
|
||||
repo: "1",
|
||||
digest: "1",
|
||||
created: "!",
|
||||
sources: [1, "a", false, {}, "b"] as never,
|
||||
});
|
||||
|
||||
expect(chart?.sources).toStrictEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
it("should filter invalid maintainers", () => {
|
||||
const chart = HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
version: "1",
|
||||
repo: "1",
|
||||
digest: "1",
|
||||
created: "!",
|
||||
maintainers: [{
|
||||
name: "a",
|
||||
email: "b",
|
||||
url: "c",
|
||||
}] as never,
|
||||
});
|
||||
|
||||
expect(chart?.maintainers).toStrictEqual([{
|
||||
name: "a",
|
||||
email: "b",
|
||||
url: "c",
|
||||
}]);
|
||||
});
|
||||
|
||||
it("should warn on unknown fields", () => {
|
||||
const { warn } = console;
|
||||
const warnFn = console.warn = jest.fn();
|
||||
|
||||
HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
version: "1",
|
||||
repo: "1",
|
||||
digest: "1",
|
||||
created: "!",
|
||||
maintainers: [{
|
||||
name: "a",
|
||||
email: "b",
|
||||
url: "c",
|
||||
}] as never,
|
||||
"asdjhajksdhadjks": 1,
|
||||
} as never);
|
||||
|
||||
expect(warnFn).toHaveBeenCalledWith("HelmChart data has unexpected fields", {
|
||||
original: anyObject(),
|
||||
unknownFields: ["asdjhajksdhadjks"],
|
||||
});
|
||||
console.warn = warn;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { computeRuleDeclarations, Ingress } from "../endpoints";
|
||||
|
||||
describe("Ingress", () => {
|
||||
it("given no loadbalancer ingresses in status property, loadbalancers should be an empty array", () => {
|
||||
const ingress = new Ingress({
|
||||
apiVersion: "networking.k8s.io/v1",
|
||||
kind: "Ingress",
|
||||
metadata: {
|
||||
name: "foo",
|
||||
resourceVersion: "1",
|
||||
uid: "bar",
|
||||
namespace: "default",
|
||||
selfLink: "/apis/networking.k8s.io/v1/ingresses/default/foo",
|
||||
},
|
||||
status: {
|
||||
loadBalancer: {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(ingress.getLoadBalancers()).toEqual([]);
|
||||
});
|
||||
|
||||
it("given loadbalancer ingresses in status property, loadbalancers should be flat array of ip adresses and hostnames", () => {
|
||||
const ingress = new Ingress({
|
||||
apiVersion: "networking.k8s.io/v1",
|
||||
kind: "Ingress",
|
||||
metadata: {
|
||||
name: "foo",
|
||||
resourceVersion: "1",
|
||||
uid: "bar",
|
||||
namespace: "default",
|
||||
selfLink: "/apis/networking.k8s.io/v1/ingresses/default/foo",
|
||||
},
|
||||
status: {
|
||||
loadBalancer: {
|
||||
ingress: [{
|
||||
ip: "10.0.0.27",
|
||||
},
|
||||
{
|
||||
hostname: "localhost",
|
||||
}],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(ingress.getLoadBalancers()).toEqual(["10.0.0.27", "localhost"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeRuleDeclarations", () => {
|
||||
it("given no tls field, should format links as http://", () => {
|
||||
const ingress = new Ingress({
|
||||
apiVersion: "networking.k8s.io/v1",
|
||||
kind: "Ingress",
|
||||
metadata: {
|
||||
name: "foo",
|
||||
resourceVersion: "1",
|
||||
uid: "bar",
|
||||
namespace: "default",
|
||||
selfLink: "/apis/networking.k8s.io/v1/ingresses/default/foo",
|
||||
},
|
||||
});
|
||||
|
||||
const result = computeRuleDeclarations(ingress, {
|
||||
host: "foo.bar",
|
||||
http: {
|
||||
paths: [{
|
||||
pathType: "Exact",
|
||||
backend: {
|
||||
service: {
|
||||
name: "my-service",
|
||||
port: {
|
||||
number: 8080,
|
||||
},
|
||||
},
|
||||
},
|
||||
}],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result[0].url).toBe("http://foo.bar/");
|
||||
});
|
||||
|
||||
it("given no tls entries, should format links as http://", () => {
|
||||
const ingress = new Ingress({
|
||||
apiVersion: "networking.k8s.io/v1",
|
||||
kind: "Ingress",
|
||||
metadata: {
|
||||
name: "foo",
|
||||
resourceVersion: "1",
|
||||
uid: "bar",
|
||||
namespace: "default",
|
||||
selfLink: "/apis/networking.k8s.io/v1/ingresses/default/foo",
|
||||
},
|
||||
});
|
||||
|
||||
ingress.spec = {
|
||||
tls: [],
|
||||
};
|
||||
|
||||
const result = computeRuleDeclarations(ingress, {
|
||||
host: "foo.bar",
|
||||
http: {
|
||||
paths: [{
|
||||
pathType: "Exact",
|
||||
backend: {
|
||||
service: {
|
||||
name: "my-service",
|
||||
port: {
|
||||
number: 8080,
|
||||
},
|
||||
},
|
||||
},
|
||||
}],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result[0].url).toBe("http://foo.bar/");
|
||||
});
|
||||
|
||||
it("given some tls entries, should format links as https://", () => {
|
||||
const ingress = new Ingress({
|
||||
apiVersion: "networking.k8s.io/v1",
|
||||
kind: "Ingress",
|
||||
metadata: {
|
||||
name: "foo",
|
||||
resourceVersion: "1",
|
||||
uid: "bar",
|
||||
namespace: "default",
|
||||
selfLink: "/apis/networking.k8s.io/v1/ingresses/default/foo",
|
||||
},
|
||||
});
|
||||
|
||||
ingress.spec = {
|
||||
tls: [{
|
||||
secretName: "my-secret",
|
||||
}],
|
||||
};
|
||||
|
||||
const result = computeRuleDeclarations(ingress, {
|
||||
host: "foo.bar",
|
||||
http: {
|
||||
paths: [{
|
||||
pathType: "Exact",
|
||||
backend: {
|
||||
service: {
|
||||
name: "my-service",
|
||||
port: {
|
||||
number: 8080,
|
||||
},
|
||||
},
|
||||
},
|
||||
}],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result[0].url).toBe("https://foo.bar/");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
jest.mock("../kube-object");
|
||||
jest.mock("../kube-api");
|
||||
jest.mock("../api-manager", () => ({
|
||||
apiManager() {
|
||||
return {
|
||||
registerStore: jest.fn(),
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
import type { IKubeApiParsed } from "../kube-api-parse";
|
||||
import { parseKubeApi } from "../kube-api-parse";
|
||||
|
||||
/**
|
||||
* [<input-url>, <expected-result>]
|
||||
*/
|
||||
type KubeApiParseTestData = [string, IKubeApiParsed];
|
||||
|
||||
const tests: KubeApiParseTestData[] = [
|
||||
["/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions/prometheuses.monitoring.coreos.com", {
|
||||
apiBase: "/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions",
|
||||
apiPrefix: "/apis",
|
||||
apiGroup: "apiextensions.k8s.io",
|
||||
apiVersion: "v1beta1",
|
||||
apiVersionWithGroup: "apiextensions.k8s.io/v1beta1",
|
||||
namespace: undefined,
|
||||
resource: "customresourcedefinitions",
|
||||
name: "prometheuses.monitoring.coreos.com",
|
||||
}],
|
||||
["/api/v1/namespaces/kube-system/pods/coredns-6955765f44-v8p27", {
|
||||
apiBase: "/api/v1/pods",
|
||||
apiPrefix: "/api",
|
||||
apiGroup: "",
|
||||
apiVersion: "v1",
|
||||
apiVersionWithGroup: "v1",
|
||||
namespace: "kube-system",
|
||||
resource: "pods",
|
||||
name: "coredns-6955765f44-v8p27",
|
||||
}],
|
||||
["/apis/stable.example.com/foo1/crontabs", {
|
||||
apiBase: "/apis/stable.example.com/foo1/crontabs",
|
||||
apiPrefix: "/apis",
|
||||
apiGroup: "stable.example.com",
|
||||
apiVersion: "foo1",
|
||||
apiVersionWithGroup: "stable.example.com/foo1",
|
||||
resource: "crontabs",
|
||||
name: undefined,
|
||||
namespace: undefined,
|
||||
}],
|
||||
["/apis/cluster.k8s.io/v1alpha1/clusters", {
|
||||
apiBase: "/apis/cluster.k8s.io/v1alpha1/clusters",
|
||||
apiPrefix: "/apis",
|
||||
apiGroup: "cluster.k8s.io",
|
||||
apiVersion: "v1alpha1",
|
||||
apiVersionWithGroup: "cluster.k8s.io/v1alpha1",
|
||||
resource: "clusters",
|
||||
name: undefined,
|
||||
namespace: undefined,
|
||||
}],
|
||||
["/api/v1/namespaces", {
|
||||
apiBase: "/api/v1/namespaces",
|
||||
apiPrefix: "/api",
|
||||
apiGroup: "",
|
||||
apiVersion: "v1",
|
||||
apiVersionWithGroup: "v1",
|
||||
resource: "namespaces",
|
||||
name: undefined,
|
||||
namespace: undefined,
|
||||
}],
|
||||
["/api/v1/secrets", {
|
||||
apiBase: "/api/v1/secrets",
|
||||
apiPrefix: "/api",
|
||||
apiGroup: "",
|
||||
apiVersion: "v1",
|
||||
apiVersionWithGroup: "v1",
|
||||
resource: "secrets",
|
||||
name: undefined,
|
||||
namespace: undefined,
|
||||
}],
|
||||
["/api/v1/nodes/minikube", {
|
||||
apiBase: "/api/v1/nodes",
|
||||
apiPrefix: "/api",
|
||||
apiGroup: "",
|
||||
apiVersion: "v1",
|
||||
apiVersionWithGroup: "v1",
|
||||
resource: "nodes",
|
||||
name: "minikube",
|
||||
namespace: undefined,
|
||||
}],
|
||||
["/api/foo-bar/nodes/minikube", {
|
||||
apiBase: "/api/foo-bar/nodes",
|
||||
apiPrefix: "/api",
|
||||
apiGroup: "",
|
||||
apiVersion: "foo-bar",
|
||||
apiVersionWithGroup: "foo-bar",
|
||||
resource: "nodes",
|
||||
name: "minikube",
|
||||
namespace: undefined,
|
||||
}],
|
||||
["/api/v1/namespaces/kube-public", {
|
||||
apiBase: "/api/v1/namespaces",
|
||||
apiPrefix: "/api",
|
||||
apiGroup: "",
|
||||
apiVersion: "v1",
|
||||
apiVersionWithGroup: "v1",
|
||||
resource: "namespaces",
|
||||
name: "kube-public",
|
||||
namespace: undefined,
|
||||
}],
|
||||
];
|
||||
|
||||
const throwtests = [
|
||||
undefined,
|
||||
"",
|
||||
"ajklsmh",
|
||||
];
|
||||
|
||||
describe("parseApi unit tests", () => {
|
||||
it.each(tests)("testing %j", (url, expected) => {
|
||||
expect(parseKubeApi(url)).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it.each(throwtests)("testing %j should throw", (url) => {
|
||||
expect(() => parseKubeApi(url as never)).toThrowError("invalid apiPath");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,708 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { ApiManager } from "../api-manager";
|
||||
import type { IngressApi } from "../endpoints";
|
||||
import { Ingress } from "../endpoints";
|
||||
import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting";
|
||||
import type { Fetch } from "../../fetch/fetch.injectable";
|
||||
import fetchInjectable from "../../fetch/fetch.injectable";
|
||||
import type { AsyncFnMock } from "@async-fn/jest";
|
||||
import asyncFn from "@async-fn/jest";
|
||||
import { flushPromises } from "../../test-utils/flush-promises";
|
||||
import setupAutoRegistrationInjectable from "../../../renderer/before-frame-starts/runnables/setup-auto-registration.injectable";
|
||||
import { createMockResponseFromString } from "../../../test-utils/mock-responses";
|
||||
import storesAndApisCanBeCreatedInjectable from "../../../renderer/stores-apis-can-be-created.injectable";
|
||||
import directoryForUserDataInjectable from "../../app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import createClusterInjectable from "../../../main/create-cluster/create-cluster.injectable";
|
||||
import hostedClusterInjectable from "../../../renderer/cluster-frame-context/hosted-cluster.injectable";
|
||||
import directoryForKubeConfigsInjectable from "../../app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
|
||||
import apiManagerInjectable from "../api-manager/manager.injectable";
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
import ingressApiInjectable from "../endpoints/ingress.api.injectable";
|
||||
|
||||
describe("KubeApi", () => {
|
||||
let fetchMock: AsyncFnMock<Fetch>;
|
||||
let apiManager: ApiManager;
|
||||
let di: DiContainer;
|
||||
|
||||
beforeEach(async () => {
|
||||
di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
fetchMock = asyncFn();
|
||||
di.override(fetchInjectable, () => fetchMock);
|
||||
|
||||
di.override(directoryForUserDataInjectable, () => "/some-user-store-path");
|
||||
di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs");
|
||||
di.override(storesAndApisCanBeCreatedInjectable, () => true);
|
||||
|
||||
const createCluster = di.inject(createClusterInjectable);
|
||||
|
||||
di.override(hostedClusterInjectable, () => createCluster({
|
||||
contextName: "some-context-name",
|
||||
id: "some-cluster-id",
|
||||
kubeConfigPath: "/some-path-to-a-kubeconfig",
|
||||
}, {
|
||||
clusterServerUrl: "https://localhost:8080",
|
||||
}));
|
||||
|
||||
apiManager = di.inject(apiManagerInjectable);
|
||||
|
||||
const setupAutoRegistration = di.inject(setupAutoRegistrationInjectable);
|
||||
|
||||
setupAutoRegistration.run();
|
||||
});
|
||||
|
||||
describe("on first call to IngressApi.get()", () => {
|
||||
let ingressApi: IngressApi;
|
||||
let getCall: Promise<Ingress | null>;
|
||||
|
||||
beforeEach(async () => {
|
||||
ingressApi = di.inject(ingressApiInjectable);
|
||||
getCall = ingressApi.get({
|
||||
name: "foo",
|
||||
namespace: "default",
|
||||
});
|
||||
|
||||
// This is needed because of how JS promises work
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("requests version list from the api group from the initial apiBase", () => {
|
||||
expect(fetchMock.mock.lastCall).toMatchObject([
|
||||
"https://127.0.0.1:12345/api-kube/apis/networking.k8s.io",
|
||||
{
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
method: "get",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe("when the version list from the api group resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await fetchMock.resolveSpecific(
|
||||
["https://127.0.0.1:12345/api-kube/apis/networking.k8s.io"],
|
||||
createMockResponseFromString("https://127.0.0.1:12345/api-kube/apis/networking.k8s.io", JSON.stringify({
|
||||
apiVersion: "v1",
|
||||
kind: "APIGroup",
|
||||
name: "networking.k8s.io",
|
||||
versions: [
|
||||
{
|
||||
groupVersion: "networking.k8s.io/v1",
|
||||
version: "v1",
|
||||
},
|
||||
{
|
||||
groupVersion: "networking.k8s.io/v1beta1",
|
||||
version: "v1beta1",
|
||||
},
|
||||
],
|
||||
preferredVersion: {
|
||||
groupVersion: "networking.k8s.io/v1",
|
||||
version: "v1",
|
||||
},
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
||||
it("requests resources from the versioned api group from the initial apiBase", () => {
|
||||
expect(fetchMock.mock.lastCall).toMatchObject([
|
||||
"https://127.0.0.1:12345/api-kube/apis/networking.k8s.io/v1",
|
||||
{
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
method: "get",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe("when resource request fufills with a resource", () => {
|
||||
beforeEach(async () => {
|
||||
await fetchMock.resolveSpecific(
|
||||
["https://127.0.0.1:12345/api-kube/apis/networking.k8s.io/v1"],
|
||||
createMockResponseFromString("https://127.0.0.1:12345/api-kube/apis/networking.k8s.io/v1", JSON.stringify({
|
||||
resources: [{
|
||||
name: "ingresses",
|
||||
}],
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
||||
it("makes the request to get the resource", () => {
|
||||
expect(fetchMock.mock.lastCall).toMatchObject([
|
||||
"https://127.0.0.1:12345/api-kube/apis/networking.k8s.io/v1/namespaces/default/ingresses/foo",
|
||||
{
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
method: "get",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("sets fields in the api instance", () => {
|
||||
expect(ingressApi).toEqual(expect.objectContaining({
|
||||
apiVersionPreferred: "v1",
|
||||
apiPrefix: "/apis",
|
||||
apiGroup: "networking.k8s.io",
|
||||
}));
|
||||
});
|
||||
|
||||
it("api is retrievable with the new apiBase", () => {
|
||||
expect(apiManager.getApi("/apis/networking.k8s.io/v1/ingresses")).toBeDefined();
|
||||
});
|
||||
|
||||
describe("when the request resolves with no data", () => {
|
||||
let result: Ingress | null;
|
||||
|
||||
beforeEach(async () => {
|
||||
await fetchMock.resolveSpecific(
|
||||
["https://127.0.0.1:12345/api-kube/apis/networking.k8s.io/v1/namespaces/default/ingresses/foo"],
|
||||
createMockResponseFromString("https://127.0.0.1:12345/api-kube/apis/networking.k8s.io/v1/namespaces/default/ingresses/foo", JSON.stringify({})),
|
||||
);
|
||||
result = await getCall;
|
||||
});
|
||||
|
||||
it("results in the get call resolving to null", () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
describe("on the second call to IngressApi.get()", () => {
|
||||
let getCall: Promise<Ingress | null>;
|
||||
|
||||
beforeEach(async () => {
|
||||
getCall = ingressApi.get({
|
||||
name: "foo1",
|
||||
namespace: "default",
|
||||
});
|
||||
|
||||
// This is needed because of how JS promises work
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("makes the request to get the resource", () => {
|
||||
expect(fetchMock.mock.lastCall).toMatchObject([
|
||||
"https://127.0.0.1:12345/api-kube/apis/networking.k8s.io/v1/namespaces/default/ingresses/foo1",
|
||||
{
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
method: "get",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe("when the request resolves with no data", () => {
|
||||
let result: Ingress | null;
|
||||
|
||||
beforeEach(async () => {
|
||||
await fetchMock.resolveSpecific(
|
||||
["https://127.0.0.1:12345/api-kube/apis/networking.k8s.io/v1/namespaces/default/ingresses/foo1"],
|
||||
createMockResponseFromString("https://127.0.0.1:12345/api-kube/apis/networking.k8s.io/v1/namespaces/default/ingresses/foo1", JSON.stringify({})),
|
||||
);
|
||||
result = await getCall;
|
||||
});
|
||||
|
||||
it("results in the get call resolving to null", () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the request resolves with data", () => {
|
||||
let result: Ingress | null;
|
||||
|
||||
beforeEach(async () => {
|
||||
await fetchMock.resolveSpecific(
|
||||
["https://127.0.0.1:12345/api-kube/apis/networking.k8s.io/v1/namespaces/default/ingresses/foo"],
|
||||
createMockResponseFromString("https://127.0.0.1:12345/api-kube/apis/networking.k8s.io/v1/namespaces/default/ingresses/foo", JSON.stringify({
|
||||
apiVersion: "v1",
|
||||
kind: "Ingress",
|
||||
metadata: {
|
||||
name: "foo",
|
||||
namespace: "default",
|
||||
resourceVersion: "1",
|
||||
uid: "12345",
|
||||
},
|
||||
})),
|
||||
);
|
||||
result = await getCall;
|
||||
});
|
||||
|
||||
it("results in the get call resolving to an instance", () => {
|
||||
expect(result).toBeInstanceOf(Ingress);
|
||||
});
|
||||
|
||||
describe("on the second call to IngressApi.get()", () => {
|
||||
let getCall: Promise<Ingress | null>;
|
||||
|
||||
beforeEach(async () => {
|
||||
getCall = ingressApi.get({
|
||||
name: "foo1",
|
||||
namespace: "default",
|
||||
});
|
||||
|
||||
// This is needed because of how JS promises work
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("makes the request to get the resource", () => {
|
||||
expect(fetchMock.mock.lastCall).toMatchObject([
|
||||
"https://127.0.0.1:12345/api-kube/apis/networking.k8s.io/v1/namespaces/default/ingresses/foo1",
|
||||
{
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
method: "get",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe("when the request resolves with no data", () => {
|
||||
let result: Ingress | null;
|
||||
|
||||
beforeEach(async () => {
|
||||
await fetchMock.resolveSpecific(
|
||||
["https://127.0.0.1:12345/api-kube/apis/networking.k8s.io/v1/namespaces/default/ingresses/foo1"],
|
||||
createMockResponseFromString("https://127.0.0.1:12345/api-kube/apis/networking.k8s.io/v1/namespaces/default/ingresses/foo1", JSON.stringify({})),
|
||||
);
|
||||
result = await getCall;
|
||||
});
|
||||
|
||||
it("results in the get call resolving to null", () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when resource request fufills with no resource", () => {
|
||||
beforeEach(async () => {
|
||||
await fetchMock.resolveSpecific(
|
||||
["https://127.0.0.1:12345/api-kube/apis/networking.k8s.io/v1"],
|
||||
createMockResponseFromString("https://127.0.0.1:12345/api-kube/apis/networking.k8s.io/v1", JSON.stringify({
|
||||
resources: [],
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
||||
it("requests resources from the second versioned api group from the initial apiBase", () => {
|
||||
expect(fetchMock.mock.lastCall).toMatchObject([
|
||||
"https://127.0.0.1:12345/api-kube/apis/networking.k8s.io/v1beta1",
|
||||
{
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
method: "get",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
|
||||
describe("when resource request fufills with a resource", () => {
|
||||
beforeEach(async () => {
|
||||
await fetchMock.resolveSpecific(
|
||||
["https://127.0.0.1:12345/api-kube/apis/networking.k8s.io/v1beta1"],
|
||||
createMockResponseFromString("https://127.0.0.1:12345/api-kube/apis/networking.k8s.io/v1beta1", JSON.stringify({
|
||||
resources: [{
|
||||
name: "ingresses",
|
||||
}],
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
||||
it("makes the request to get the resource", () => {
|
||||
expect(fetchMock.mock.lastCall).toMatchObject([
|
||||
"https://127.0.0.1:12345/api-kube/apis/networking.k8s.io/v1beta1/namespaces/default/ingresses/foo",
|
||||
{
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
method: "get",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("sets fields in the api instance", () => {
|
||||
expect(ingressApi).toEqual(expect.objectContaining({
|
||||
apiVersionPreferred: "v1beta1",
|
||||
apiPrefix: "/apis",
|
||||
apiGroup: "networking.k8s.io",
|
||||
}));
|
||||
});
|
||||
|
||||
it("api is retrievable with the new apiBase", () => {
|
||||
expect(apiManager.getApi("/apis/networking.k8s.io/v1beta1/ingresses")).toBeDefined();
|
||||
});
|
||||
|
||||
it("api is retrievable with the old apiBase", () => {
|
||||
expect(apiManager.getApi("/apis/networking.k8s.io/v1/ingresses")).toBeDefined();
|
||||
});
|
||||
|
||||
describe("when the request resolves with no data", () => {
|
||||
let result: Ingress | null;
|
||||
|
||||
beforeEach(async () => {
|
||||
await fetchMock.resolveSpecific(
|
||||
["https://127.0.0.1:12345/api-kube/apis/networking.k8s.io/v1beta1/namespaces/default/ingresses/foo"],
|
||||
createMockResponseFromString("https://127.0.0.1:12345/api-kube/apis/networking.k8s.io/v1beta1/namespaces/default/ingresses/foo", JSON.stringify({})),
|
||||
);
|
||||
result = await getCall;
|
||||
});
|
||||
|
||||
it("results in the get call resolving to null", () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
describe("on the second call to IngressApi.get()", () => {
|
||||
let getCall: Promise<Ingress | null>;
|
||||
|
||||
beforeEach(async () => {
|
||||
getCall = ingressApi.get({
|
||||
name: "foo1",
|
||||
namespace: "default",
|
||||
});
|
||||
|
||||
// This is needed because of how JS promises work
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("makes the request to get the resource", () => {
|
||||
expect(fetchMock.mock.lastCall).toMatchObject([
|
||||
"https://127.0.0.1:12345/api-kube/apis/networking.k8s.io/v1beta1/namespaces/default/ingresses/foo1",
|
||||
{
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
method: "get",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe("when the request resolves with no data", () => {
|
||||
let result: Ingress | null;
|
||||
|
||||
beforeEach(async () => {
|
||||
await fetchMock.resolveSpecific(
|
||||
["https://127.0.0.1:12345/api-kube/apis/networking.k8s.io/v1beta1/namespaces/default/ingresses/foo1"],
|
||||
createMockResponseFromString("https://127.0.0.1:12345/api-kube/apis/networking.k8s.io/v1beta1/namespaces/default/ingresses/foo1", JSON.stringify({})),
|
||||
);
|
||||
result = await getCall;
|
||||
});
|
||||
|
||||
it("results in the get call resolving to null", () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the request resolves with data", () => {
|
||||
let result: Ingress | null;
|
||||
|
||||
beforeEach(async () => {
|
||||
await fetchMock.resolveSpecific(
|
||||
["https://127.0.0.1:12345/api-kube/apis/networking.k8s.io/v1beta1/namespaces/default/ingresses/foo"],
|
||||
createMockResponseFromString("https://127.0.0.1:12345/api-kube/apis/networking.k8s.io/v1beta1/namespaces/default/ingresses/foo", JSON.stringify({
|
||||
apiVersion: "v1",
|
||||
kind: "Ingress",
|
||||
metadata: {
|
||||
name: "foo",
|
||||
namespace: "default",
|
||||
resourceVersion: "1",
|
||||
uid: "12345",
|
||||
},
|
||||
})),
|
||||
);
|
||||
result = await getCall;
|
||||
});
|
||||
|
||||
it("results in the get call resolving to an instance", () => {
|
||||
expect(result).toBeInstanceOf(Ingress);
|
||||
});
|
||||
|
||||
describe("on the second call to IngressApi.get()", () => {
|
||||
let getCall: Promise<Ingress | null>;
|
||||
|
||||
beforeEach(async () => {
|
||||
getCall = ingressApi.get({
|
||||
name: "foo1",
|
||||
namespace: "default",
|
||||
});
|
||||
|
||||
// This is needed because of how JS promises work
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("makes the request to get the resource", () => {
|
||||
expect(fetchMock.mock.lastCall).toMatchObject([
|
||||
"https://127.0.0.1:12345/api-kube/apis/networking.k8s.io/v1beta1/namespaces/default/ingresses/foo1",
|
||||
{
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
method: "get",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe("when the request resolves with no data", () => {
|
||||
let result: Ingress | null;
|
||||
|
||||
beforeEach(async () => {
|
||||
await fetchMock.resolveSpecific(
|
||||
["https://127.0.0.1:12345/api-kube/apis/networking.k8s.io/v1beta1/namespaces/default/ingresses/foo1"],
|
||||
createMockResponseFromString("https://127.0.0.1:12345/api-kube/apis/networking.k8s.io/v1beta1/namespaces/default/ingresses/foo1", JSON.stringify({})),
|
||||
);
|
||||
result = await getCall;
|
||||
});
|
||||
|
||||
it("results in the get call resolving to null", () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the version list from the api group resolves with no versions", () => {
|
||||
beforeEach(async () => {
|
||||
await fetchMock.resolveSpecific(
|
||||
["https://127.0.0.1:12345/api-kube/apis/networking.k8s.io"],
|
||||
createMockResponseFromString("https://127.0.0.1:12345/api-kube/apis/networking.k8s.io", JSON.stringify({
|
||||
"metadata": {},
|
||||
"status": "Failure",
|
||||
"message": "the server could not find the requested resource",
|
||||
"reason": "NotFound",
|
||||
"details": {
|
||||
"causes": [
|
||||
{
|
||||
"reason": "UnexpectedServerResponse",
|
||||
"message": "404 page not found",
|
||||
},
|
||||
],
|
||||
},
|
||||
"code": 404,
|
||||
}), 404),
|
||||
);
|
||||
});
|
||||
|
||||
it("requests the resources from the base api url from the fallback api", () => {
|
||||
expect(fetchMock.mock.lastCall).toMatchObject([
|
||||
"https://127.0.0.1:12345/api-kube/apis/extensions",
|
||||
{
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
method: "get",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe("when resource request fufills with a resource", () => {
|
||||
beforeEach(async () => {
|
||||
await fetchMock.resolveSpecific(
|
||||
["https://127.0.0.1:12345/api-kube/apis/extensions"],
|
||||
createMockResponseFromString("https://127.0.0.1:12345/api-kube/apis/extensions", JSON.stringify({
|
||||
apiVersion: "v1",
|
||||
kind: "APIGroup",
|
||||
name: "extensions",
|
||||
versions: [
|
||||
{
|
||||
groupVersion: "extensions/v1beta1",
|
||||
version: "v1beta1",
|
||||
},
|
||||
],
|
||||
preferredVersion: {
|
||||
groupVersion: "extensions/v1beta1",
|
||||
version: "v1beta1",
|
||||
},
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
||||
it("requests resource versions from the versioned api group from the fallback apiBase", () => {
|
||||
expect(fetchMock.mock.lastCall).toMatchObject([
|
||||
"https://127.0.0.1:12345/api-kube/apis/extensions/v1beta1",
|
||||
{
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
method: "get",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe("when the preferred version request resolves to v1beta1", () => {
|
||||
beforeEach(async () => {
|
||||
await fetchMock.resolveSpecific(
|
||||
["https://127.0.0.1:12345/api-kube/apis/extensions/v1beta1"],
|
||||
createMockResponseFromString("https://127.0.0.1:12345/api-kube/apis/extensions", JSON.stringify({
|
||||
resources: [{
|
||||
name: "ingresses",
|
||||
}],
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
||||
it("makes the request to get the resource", () => {
|
||||
expect(fetchMock.mock.lastCall).toMatchObject([
|
||||
"https://127.0.0.1:12345/api-kube/apis/extensions/v1beta1/namespaces/default/ingresses/foo",
|
||||
{
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
method: "get",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("sets fields in the api instance", () => {
|
||||
expect(ingressApi).toEqual(expect.objectContaining({
|
||||
apiVersionPreferred: "v1beta1",
|
||||
apiPrefix: "/apis",
|
||||
apiGroup: "extensions",
|
||||
}));
|
||||
});
|
||||
|
||||
it("api is retrievable with the new apiBase", () => {
|
||||
expect(apiManager.getApi("/apis/extensions/v1beta1/ingresses")).toBeDefined();
|
||||
});
|
||||
|
||||
describe("when the request resolves with no data", () => {
|
||||
let result: Ingress | null;
|
||||
|
||||
beforeEach(async () => {
|
||||
await fetchMock.resolveSpecific(
|
||||
["https://127.0.0.1:12345/api-kube/apis/extensions/v1beta1/namespaces/default/ingresses/foo"],
|
||||
createMockResponseFromString("https://127.0.0.1:12345/api-kube/apis/extensions/v1beta1/namespaces/default/ingresses/foo", JSON.stringify({})),
|
||||
);
|
||||
result = await getCall;
|
||||
});
|
||||
|
||||
it("results in the get call resolving to null", () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
describe("on the second call to IngressApi.get()", () => {
|
||||
let getCall: Promise<Ingress | null>;
|
||||
|
||||
beforeEach(async () => {
|
||||
getCall = ingressApi.get({
|
||||
name: "foo1",
|
||||
namespace: "default",
|
||||
});
|
||||
|
||||
// This is needed because of how JS promises work
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("makes the request to get the resource", () => {
|
||||
expect(fetchMock.mock.lastCall).toMatchObject([
|
||||
"https://127.0.0.1:12345/api-kube/apis/extensions/v1beta1/namespaces/default/ingresses/foo1",
|
||||
{
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
method: "get",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe("when the request resolves with no data", () => {
|
||||
let result: Ingress | null;
|
||||
|
||||
beforeEach(async () => {
|
||||
await fetchMock.resolveSpecific(
|
||||
["https://127.0.0.1:12345/api-kube/apis/extensions/v1beta1/namespaces/default/ingresses/foo1"],
|
||||
createMockResponseFromString("https://127.0.0.1:12345/api-kube/apis/extensions/v1beta1/namespaces/default/ingresses/foo1", JSON.stringify({})),
|
||||
);
|
||||
result = await getCall;
|
||||
});
|
||||
|
||||
it("results in the get call resolving to null", () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the request resolves with data", () => {
|
||||
let result: Ingress | null;
|
||||
|
||||
beforeEach(async () => {
|
||||
await fetchMock.resolveSpecific(
|
||||
["https://127.0.0.1:12345/api-kube/apis/extensions/v1beta1/namespaces/default/ingresses/foo"],
|
||||
createMockResponseFromString("https://127.0.0.1:12345/api-kube/apis/extensions/v1beta1/namespaces/default/ingresses/foo", JSON.stringify({
|
||||
apiVersion: "v1beta1",
|
||||
kind: "Ingress",
|
||||
metadata: {
|
||||
name: "foo",
|
||||
namespace: "default",
|
||||
resourceVersion: "1",
|
||||
uid: "12345",
|
||||
},
|
||||
})),
|
||||
);
|
||||
result = await getCall;
|
||||
});
|
||||
|
||||
it("results in the get call resolving to an instance", () => {
|
||||
expect(result).toBeInstanceOf(Ingress);
|
||||
});
|
||||
|
||||
describe("on the second call to IngressApi.get()", () => {
|
||||
let getCall: Promise<Ingress | null>;
|
||||
|
||||
beforeEach(async () => {
|
||||
getCall = ingressApi.get({
|
||||
name: "foo1",
|
||||
namespace: "default",
|
||||
});
|
||||
|
||||
// This is needed because of how JS promises work
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("makes the request to get the resource", () => {
|
||||
expect(fetchMock.mock.lastCall).toMatchObject([
|
||||
"https://127.0.0.1:12345/api-kube/apis/extensions/v1beta1/namespaces/default/ingresses/foo1",
|
||||
{
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
method: "get",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe("when the request resolves with no data", () => {
|
||||
let result: Ingress | null;
|
||||
|
||||
beforeEach(async () => {
|
||||
await fetchMock.resolveSpecific(
|
||||
["https://127.0.0.1:12345/api-kube/apis/extensions/v1beta1/namespaces/default/ingresses/foo1"],
|
||||
createMockResponseFromString("https://127.0.0.1:12345/api-kube/apis/extensions/v1beta1/namespaces/default/ingresses/foo1", JSON.stringify({})),
|
||||
);
|
||||
result = await getCall;
|
||||
});
|
||||
|
||||
it("results in the get call resolving to null", () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { noop } from "../../utils";
|
||||
import type { KubeApi } from "../kube-api";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { KubeObjectStoreLoadingParams } from "../kube-object.store";
|
||||
import { KubeObjectStore } from "../kube-object.store";
|
||||
|
||||
class FakeKubeObjectStore extends KubeObjectStore<KubeObject> {
|
||||
constructor(private readonly _loadItems: (params: KubeObjectStoreLoadingParams) => KubeObject[], api: Partial<KubeApi<KubeObject>>) {
|
||||
super({
|
||||
context: {
|
||||
allNamespaces: [],
|
||||
contextNamespaces: [],
|
||||
hasSelectedAll: false,
|
||||
isGlobalWatchEnabled: () => true,
|
||||
isLoadingAll: () => true,
|
||||
},
|
||||
logger: {
|
||||
debug: noop,
|
||||
error: noop,
|
||||
info: noop,
|
||||
silly: noop,
|
||||
warn: noop,
|
||||
},
|
||||
}, api as KubeApi<KubeObject>);
|
||||
}
|
||||
|
||||
async loadItems(params: KubeObjectStoreLoadingParams) {
|
||||
return this._loadItems(params);
|
||||
}
|
||||
}
|
||||
|
||||
describe("KubeObjectStore", () => {
|
||||
it("should remove an object from the list of items after it is not returned from listing the same namespace again", async () => {
|
||||
const loadItems = jest.fn();
|
||||
const obj = new KubeObject({
|
||||
apiVersion: "v1",
|
||||
kind: "Foo",
|
||||
metadata: {
|
||||
name: "some-obj-name",
|
||||
resourceVersion: "1",
|
||||
uid: "some-uid",
|
||||
namespace: "default",
|
||||
selfLink: "/some/self/link",
|
||||
},
|
||||
});
|
||||
const store = new FakeKubeObjectStore(loadItems, {
|
||||
isNamespaced: true,
|
||||
});
|
||||
|
||||
loadItems.mockImplementationOnce(() => [obj]);
|
||||
|
||||
await store.loadAll({
|
||||
namespaces: ["default"],
|
||||
});
|
||||
|
||||
expect(store.items).toContain(obj);
|
||||
|
||||
loadItems.mockImplementationOnce(() => []);
|
||||
|
||||
await store.loadAll({
|
||||
namespaces: ["default"],
|
||||
});
|
||||
|
||||
expect(store.items).not.toContain(obj);
|
||||
});
|
||||
|
||||
it("should not remove an object that is not returned, if it is in a different namespace", async () => {
|
||||
const loadItems = jest.fn();
|
||||
const objInDefaultNamespace = new KubeObject({
|
||||
apiVersion: "v1",
|
||||
kind: "Foo",
|
||||
metadata: {
|
||||
name: "some-obj-name",
|
||||
resourceVersion: "1",
|
||||
uid: "some-uid",
|
||||
namespace: "default",
|
||||
selfLink: "/some/self/link",
|
||||
},
|
||||
});
|
||||
const objNotInDefaultNamespace = new KubeObject({
|
||||
apiVersion: "v1",
|
||||
kind: "Foo",
|
||||
metadata: {
|
||||
name: "some-obj-name",
|
||||
resourceVersion: "1",
|
||||
uid: "some-uid",
|
||||
namespace: "not-default",
|
||||
selfLink: "/some/self/link",
|
||||
},
|
||||
});
|
||||
const store = new FakeKubeObjectStore(loadItems, {
|
||||
isNamespaced: true,
|
||||
});
|
||||
|
||||
loadItems.mockImplementationOnce(() => [objInDefaultNamespace]);
|
||||
|
||||
await store.loadAll({
|
||||
namespaces: ["default"],
|
||||
});
|
||||
|
||||
expect(store.items).toContain(objInDefaultNamespace);
|
||||
|
||||
loadItems.mockImplementationOnce(() => [objNotInDefaultNamespace]);
|
||||
|
||||
await store.loadAll({
|
||||
namespaces: ["not-default"],
|
||||
});
|
||||
|
||||
expect(store.items).toContain(objInDefaultNamespace);
|
||||
});
|
||||
|
||||
it("should remove all objects not returned if the api is cluster-scoped", async () => {
|
||||
const loadItems = jest.fn();
|
||||
const clusterScopedObject1 = new KubeObject({
|
||||
apiVersion: "v1",
|
||||
kind: "Foo",
|
||||
metadata: {
|
||||
name: "some-obj-name",
|
||||
resourceVersion: "1",
|
||||
uid: "some-uid",
|
||||
selfLink: "/some/self/link",
|
||||
},
|
||||
});
|
||||
const clusterScopedObject2 = new KubeObject({
|
||||
apiVersion: "v1",
|
||||
kind: "Foo",
|
||||
metadata: {
|
||||
name: "some-obj-name",
|
||||
resourceVersion: "1",
|
||||
uid: "some-uid",
|
||||
namespace: "not-default",
|
||||
selfLink: "/some/self/link",
|
||||
},
|
||||
});
|
||||
const store = new FakeKubeObjectStore(loadItems, {
|
||||
isNamespaced: false,
|
||||
});
|
||||
|
||||
loadItems.mockImplementationOnce(() => [clusterScopedObject1]);
|
||||
|
||||
await store.loadAll({});
|
||||
|
||||
expect(store.items).toContain(clusterScopedObject1);
|
||||
|
||||
loadItems.mockImplementationOnce(() => [clusterScopedObject2]);
|
||||
|
||||
await store.loadAll({});
|
||||
|
||||
expect(store.items).not.toContain(clusterScopedObject1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { KubeObject } from "../kube-object";
|
||||
|
||||
describe("KubeObject", () => {
|
||||
describe("isJsonApiData", () => {
|
||||
{
|
||||
type TestCase = [any];
|
||||
const tests: TestCase[] = [
|
||||
[false],
|
||||
[true],
|
||||
[null],
|
||||
[undefined],
|
||||
[""],
|
||||
[1],
|
||||
[(): unknown => void 0],
|
||||
[Symbol("hello")],
|
||||
[{}],
|
||||
];
|
||||
|
||||
it.each(tests)("should reject invalid value: %p", (input) => {
|
||||
expect(KubeObject.isJsonApiData(input)).toBe(false);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
type TestCase = [string, any];
|
||||
const tests: TestCase[] = [
|
||||
["kind", { apiVersion: "", metadata: { uid: "", name: "", resourceVersion: "", selfLink: "" }}],
|
||||
["apiVersion", { kind: "", metadata: { uid: "", name: "", resourceVersion: "", selfLink: "" }}],
|
||||
["metadata", { kind: "", apiVersion: "" }],
|
||||
["metadata.uid", { kind: "", apiVersion: "", metadata: { name: "", resourceVersion: "", selfLink: "" }}],
|
||||
["metadata.name", { kind: "", apiVersion: "", metadata: { uid: "", resourceVersion: "", selfLink: "" }}],
|
||||
["metadata.resourceVersion", { kind: "", apiVersion: "", metadata: { uid: "", name: "", selfLink: "" }}],
|
||||
];
|
||||
|
||||
it.each(tests)("should reject with missing: %s", (missingField, input) => {
|
||||
expect(KubeObject.isJsonApiData(input)).toBe(false);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
type TestCase = [string, any];
|
||||
const tests: TestCase[] = [
|
||||
["kind", { kind: 1, apiVersion: "", metadata: {}}],
|
||||
["apiVersion", { apiVersion: 1, kind: "", metadata: {}}],
|
||||
["metadata", { kind: "", apiVersion: "", metadata: "" }],
|
||||
["metadata.uid", { kind: "", apiVersion: "", metadata: { uid: 1 }}],
|
||||
["metadata.name", { kind: "", apiVersion: "", metadata: { uid: "", name: 1 }}],
|
||||
["metadata.resourceVersion", { kind: "", apiVersion: "", metadata: { uid: "", name: "", resourceVersion: 1 }}],
|
||||
["metadata.selfLink", { kind: "", apiVersion: "", metadata: { uid: "", name: "", resourceVersion: "", selfLink: 1 }}],
|
||||
["metadata.namespace", { kind: "", apiVersion: "", metadata: { uid: "", name: "", resourceVersion: "", selfLink: "", namespace: 1 }}],
|
||||
["metadata.creationTimestamp", { kind: "", apiVersion: "", metadata: { uid: "", name: "", resourceVersion: "", selfLink: "", creationTimestamp: 1 }}],
|
||||
["metadata.continue", { kind: "", apiVersion: "", metadata: { uid: "", name: "", resourceVersion: "", selfLink: "", continue: 1 }}],
|
||||
["metadata.finalizers", { kind: "", apiVersion: "", metadata: { uid: "", name: "", resourceVersion: "", selfLink: "", finalizers: 1 }}],
|
||||
["metadata.finalizers", { kind: "", apiVersion: "", metadata: { uid: "", name: "", resourceVersion: "", selfLink: "", finalizers: [1] }}],
|
||||
["metadata.finalizers", { kind: "", apiVersion: "", metadata: { uid: "", name: "", resourceVersion: "", selfLink: "", finalizers: {}}}],
|
||||
["metadata.labels", { kind: "", apiVersion: "", metadata: { uid: "", name: "", resourceVersion: "", selfLink: "", labels: 1 }}],
|
||||
["metadata.labels", { kind: "", apiVersion: "", metadata: { uid: "", name: "", resourceVersion: "", selfLink: "", labels: { food: 1 }}}],
|
||||
["metadata.annotations", { kind: "", apiVersion: "", metadata: { uid: "", name: "", resourceVersion: "", selfLink: "", annotations: 1 }}],
|
||||
["metadata.annotations", { kind: "", apiVersion: "", metadata: { uid: "", name: "", resourceVersion: "", selfLink: "", annotations: { food: 1 }}}],
|
||||
];
|
||||
|
||||
it.each(tests)("should reject with wrong type for field: %s", (missingField, input) => {
|
||||
expect(KubeObject.isJsonApiData(input)).toBe(false);
|
||||
});
|
||||
}
|
||||
|
||||
it("should accept valid KubeJsonApiData (ignoring other fields)", () => {
|
||||
const valid = { kind: "", apiVersion: "", metadata: { uid: "", name: "", resourceVersion: "", selfLink: "", annotations: { food: "" }}};
|
||||
|
||||
expect(KubeObject.isJsonApiData(valid)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isPartialJsonApiData", () => {
|
||||
{
|
||||
type TestCase = [any];
|
||||
const tests: TestCase[] = [
|
||||
[false],
|
||||
[true],
|
||||
[null],
|
||||
[undefined],
|
||||
[""],
|
||||
[1],
|
||||
[(): unknown => void 0],
|
||||
[Symbol("hello")],
|
||||
];
|
||||
|
||||
it.each(tests)("should reject invalid value: %p", (input) => {
|
||||
expect(KubeObject.isPartialJsonApiData(input)).toBe(false);
|
||||
});
|
||||
}
|
||||
|
||||
it("should accept {}", () => {
|
||||
expect(KubeObject.isPartialJsonApiData({})).toBe(true);
|
||||
});
|
||||
|
||||
{
|
||||
type TestCase = [string, any];
|
||||
const tests: TestCase[] = [
|
||||
["kind", { apiVersion: "", metadata: { uid: "", name: "", resourceVersion: "", selfLink: "" }}],
|
||||
["apiVersion", { kind: "", metadata: { uid: "", name: "", resourceVersion: "", selfLink: "" }}],
|
||||
["metadata", { kind: "", apiVersion: "" }],
|
||||
];
|
||||
|
||||
it.each(tests)("should not reject with missing top level field: %s", (missingField, input) => {
|
||||
expect(KubeObject.isPartialJsonApiData(input)).toBe(true);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
type TestCase = [string, any];
|
||||
const tests: TestCase[] = [
|
||||
["kind", { kind: 1, apiVersion: "", metadata: { uid: "", name: "", resourceVersion: "", selfLink: "" }}],
|
||||
["apiVersion", { apiVersion: 1, kind: "", metadata: { uid: "", name: "", resourceVersion: "", selfLink: "" }}],
|
||||
["metadata", { kind: "", apiVersion: "", metadata: "" }],
|
||||
["metadata.uid", { kind: "", apiVersion: "", metadata: { uid: 1, name: "", resourceVersion: "", selfLink: "" }}],
|
||||
["metadata.name", { kind: "", apiVersion: "", metadata: { uid: "", name: 1, resourceVersion: "", selfLink: "" }}],
|
||||
["metadata.resourceVersion", { kind: "", apiVersion: "", metadata: { uid: "", name: "", resourceVersion: 1, selfLink: "" }}],
|
||||
["metadata.selfLink", { kind: "", apiVersion: "", metadata: { uid: "", name: "", resourceVersion: "", selfLink: 1 }}],
|
||||
["metadata.namespace", { kind: "", apiVersion: "", metadata: { uid: "", name: "", resourceVersion: "", selfLink: "", namespace: 1 }}],
|
||||
["metadata.creationTimestamp", { kind: "", apiVersion: "", metadata: { uid: "", name: "", resourceVersion: "", selfLink: "", creationTimestamp: 1 }}],
|
||||
["metadata.continue", { kind: "", apiVersion: "", metadata: { uid: "", name: "", resourceVersion: "", selfLink: "", continue: 1 }}],
|
||||
["metadata.finalizers", { kind: "", apiVersion: "", metadata: { uid: "", name: "", resourceVersion: "", selfLink: "", finalizers: 1 }}],
|
||||
["metadata.finalizers", { kind: "", apiVersion: "", metadata: { uid: "", name: "", resourceVersion: "", selfLink: "", finalizers: [1] }}],
|
||||
["metadata.finalizers", { kind: "", apiVersion: "", metadata: { uid: "", name: "", resourceVersion: "", selfLink: "", finalizers: {}}}],
|
||||
["metadata.labels", { kind: "", apiVersion: "", metadata: { uid: "", name: "", resourceVersion: "", selfLink: "", labels: 1 }}],
|
||||
["metadata.labels", { kind: "", apiVersion: "", metadata: { uid: "", name: "", resourceVersion: "", selfLink: "", labels: { food: 1 }}}],
|
||||
["metadata.annotations", { kind: "", apiVersion: "", metadata: { uid: "", name: "", resourceVersion: "", selfLink: "", annotations: 1 }}],
|
||||
["metadata.annotations", { kind: "", apiVersion: "", metadata: { uid: "", name: "", resourceVersion: "", selfLink: "", annotations: { food: 1 }}}],
|
||||
];
|
||||
|
||||
it.each(tests)("should reject with wrong type for field: %s", (missingField, input) => {
|
||||
expect(KubeObject.isPartialJsonApiData(input)).toBe(false);
|
||||
});
|
||||
}
|
||||
|
||||
it("should accept valid Partial<KubeJsonApiData> (ignoring other fields)", () => {
|
||||
const valid = { kind: "", apiVersion: "", metadata: { uid: "", name: "", resourceVersion: "", selfLink: "", annotations: { food: "" }}};
|
||||
|
||||
expect(KubeObject.isPartialJsonApiData(valid)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isJsonApiDataList", () => {
|
||||
function isAny(val: unknown): val is any {
|
||||
return true;
|
||||
}
|
||||
|
||||
function isNotAny(val: unknown): val is any {
|
||||
return false;
|
||||
}
|
||||
|
||||
function isBoolean(val: unknown): val is boolean {
|
||||
return typeof val === "boolean";
|
||||
}
|
||||
|
||||
{
|
||||
type TestCase = [any];
|
||||
const tests: TestCase[] = [
|
||||
[false],
|
||||
[true],
|
||||
[null],
|
||||
[undefined],
|
||||
[""],
|
||||
[1],
|
||||
[(): unknown => void 0],
|
||||
[Symbol("hello")],
|
||||
[{}],
|
||||
];
|
||||
|
||||
it.each(tests)("should reject invalid value: %p", (input) => {
|
||||
expect(KubeObject.isJsonApiDataList(input, isAny)).toBe(false);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
type TestCase = [string, any];
|
||||
const tests: TestCase[] = [
|
||||
["kind", { apiVersion: "", items: [], metadata: { resourceVersion: "", selfLink: "" }}],
|
||||
["apiVersion", { kind: "", items: [], metadata: { resourceVersion: "", selfLink: "" }}],
|
||||
["metadata", { kind: "", items: [], apiVersion: "" }],
|
||||
];
|
||||
|
||||
it.each(tests)("should reject with missing: %s", (missingField, input) => {
|
||||
expect(KubeObject.isJsonApiDataList(input, isAny)).toBe(false);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
type TestCase = [string, any];
|
||||
const tests: TestCase[] = [
|
||||
["kind", { kind: 1, items: [], apiVersion: "", metadata: { resourceVersion: "", selfLink: "" }}],
|
||||
["apiVersion", { kind: "", items: [], apiVersion: 1, metadata: { resourceVersion: "", selfLink: "" }}],
|
||||
["metadata", { kind: "", items: [], apiVersion: "", metadata: 1 }],
|
||||
["metadata.resourceVersion", { kind: "", items: [], apiVersion: "", metadata: { resourceVersion: 1, selfLink: "" }}],
|
||||
["metadata.selfLink", { kind: "", items: [], apiVersion: "", metadata: { resourceVersion: "", selfLink: 1 }}],
|
||||
["items", { kind: "", items: 1, apiVersion: "", metadata: { resourceVersion: "", selfLink: "" }}],
|
||||
["items", { kind: "", items: "", apiVersion: "", metadata: { resourceVersion: "", selfLink: "" }}],
|
||||
["items", { kind: "", items: {}, apiVersion: "", metadata: { resourceVersion: "", selfLink: "" }}],
|
||||
["items[0]", { kind: "", items: [""], apiVersion: "", metadata: { resourceVersion: "", selfLink: "" }}],
|
||||
];
|
||||
|
||||
it.each(tests)("should reject with wrong type for field: %s", (missingField, input) => {
|
||||
expect(KubeObject.isJsonApiDataList(input, isNotAny)).toBe(false);
|
||||
});
|
||||
}
|
||||
|
||||
it("should accept valid KubeJsonApiDataList (ignoring other fields)", () => {
|
||||
const valid = { kind: "", items: [false], apiVersion: "", metadata: { resourceVersion: "", selfLink: "" }};
|
||||
|
||||
expect(KubeObject.isJsonApiDataList(valid, isBoolean)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { Node } from "../endpoints";
|
||||
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
describe("Node tests", () => {
|
||||
describe("isMasterNode()", () => {
|
||||
it("given a master node labelled before kubernetes 1.20, should return true", () => {
|
||||
const node = new Node({
|
||||
apiVersion: "foo",
|
||||
kind: "Node",
|
||||
metadata: {
|
||||
name: "bar",
|
||||
resourceVersion: "1",
|
||||
uid: "bat",
|
||||
labels: {
|
||||
"node-role.kubernetes.io/master": "NoSchedule",
|
||||
},
|
||||
selfLink: "/api/v1/nodes/bar",
|
||||
},
|
||||
});
|
||||
|
||||
expect(node.isMasterNode()).toBe(true);
|
||||
});
|
||||
|
||||
it("given a master node labelled after kubernetes 1.20, should return true", () => {
|
||||
const node = new Node({
|
||||
apiVersion: "foo",
|
||||
kind: "Node",
|
||||
metadata: {
|
||||
name: "bar",
|
||||
resourceVersion: "1",
|
||||
uid: "bat",
|
||||
labels: {
|
||||
"node-role.kubernetes.io/control-plane": "NoSchedule",
|
||||
},
|
||||
selfLink: "/api/v1/nodes/bar",
|
||||
},
|
||||
});
|
||||
|
||||
expect(node.isMasterNode()).toBe(true);
|
||||
});
|
||||
|
||||
it("given a non master node, should return false", () => {
|
||||
const node = new Node({
|
||||
apiVersion: "foo",
|
||||
kind: "Node",
|
||||
metadata: {
|
||||
name: "bar",
|
||||
resourceVersion: "1",
|
||||
uid: "bat",
|
||||
labels: {},
|
||||
selfLink: "/api/v1/nodes/bar",
|
||||
},
|
||||
});
|
||||
|
||||
expect(node.isMasterNode()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getRoleLabels()", () => {
|
||||
it("should return empty string if labels is not present", () => {
|
||||
const node = new Node({
|
||||
apiVersion: "foo",
|
||||
kind: "Node",
|
||||
metadata: {
|
||||
name: "bar",
|
||||
resourceVersion: "1",
|
||||
uid: "bat",
|
||||
selfLink: "/api/v1/nodes/bar",
|
||||
},
|
||||
});
|
||||
|
||||
expect(node.getRoleLabels()).toBe("");
|
||||
});
|
||||
|
||||
it("should return empty string if labels is empty object", () => {
|
||||
const node = new Node({
|
||||
apiVersion: "foo",
|
||||
kind: "Node",
|
||||
metadata: {
|
||||
name: "bar",
|
||||
resourceVersion: "1",
|
||||
uid: "bat",
|
||||
labels: {},
|
||||
selfLink: "/api/v1/nodes/bar",
|
||||
},
|
||||
});
|
||||
|
||||
expect(node.getRoleLabels()).toBe("");
|
||||
});
|
||||
|
||||
it("should return rest of keys with substring node-role.kubernetes.io/", () => {
|
||||
const node = new Node({
|
||||
apiVersion: "foo",
|
||||
kind: "Node",
|
||||
metadata: {
|
||||
name: "bar",
|
||||
resourceVersion: "1",
|
||||
uid: "bat",
|
||||
labels: {
|
||||
"node-role.kubernetes.io/foobar": "bat",
|
||||
"hellonode-role.kubernetes.io/foobar1": "bat",
|
||||
},
|
||||
selfLink: "/api/v1/nodes/bar",
|
||||
},
|
||||
});
|
||||
|
||||
expect(node.getRoleLabels()).toBe("foobar, foobar1");
|
||||
});
|
||||
|
||||
it("should return rest of keys with substring node-role.kubernetes.io/ after last /", () => {
|
||||
const node = new Node({
|
||||
apiVersion: "foo",
|
||||
kind: "Node",
|
||||
metadata: {
|
||||
name: "bar",
|
||||
resourceVersion: "1",
|
||||
uid: "bat",
|
||||
labels: {
|
||||
"node-role.kubernetes.io/foobar": "bat",
|
||||
"hellonode-role.kubernetes.io//////foobar1": "bat",
|
||||
},
|
||||
selfLink: "/api/v1/nodes/bar",
|
||||
},
|
||||
});
|
||||
|
||||
expect(node.getRoleLabels()).toBe("foobar, foobar1");
|
||||
});
|
||||
|
||||
it("should return value of label kubernetes.io/role if present", () => {
|
||||
const node = new Node({
|
||||
apiVersion: "foo",
|
||||
kind: "Node",
|
||||
metadata: {
|
||||
name: "bar",
|
||||
resourceVersion: "1",
|
||||
uid: "bat",
|
||||
labels: {
|
||||
"kubernetes.io/role": "master",
|
||||
},
|
||||
selfLink: "/api/v1/nodes/bar",
|
||||
},
|
||||
});
|
||||
|
||||
expect(node.getRoleLabels()).toBe("master");
|
||||
});
|
||||
|
||||
it("should return value of label node.kubernetes.io/role if present", () => {
|
||||
const node = new Node({
|
||||
apiVersion: "foo",
|
||||
kind: "Node",
|
||||
metadata: {
|
||||
name: "bar",
|
||||
resourceVersion: "1",
|
||||
uid: "bat",
|
||||
labels: {
|
||||
"node.kubernetes.io/role": "master",
|
||||
},
|
||||
selfLink: "/api/v1/nodes/bar",
|
||||
},
|
||||
});
|
||||
|
||||
expect(node.getRoleLabels()).toBe("master");
|
||||
});
|
||||
|
||||
it("all sources should be joined together", () => {
|
||||
const node = new Node({
|
||||
apiVersion: "foo",
|
||||
kind: "Node",
|
||||
metadata: {
|
||||
name: "bar",
|
||||
resourceVersion: "1",
|
||||
uid: "bat",
|
||||
labels: {
|
||||
"aksjhdkjahsdnode-role.kubernetes.io/foobar": "bat",
|
||||
"kubernetes.io/role": "master",
|
||||
"node.kubernetes.io/role": "master-v2-max",
|
||||
},
|
||||
selfLink: "/api/v1/nodes/bar",
|
||||
},
|
||||
});
|
||||
|
||||
expect(node.getRoleLabels()).toBe("foobar, master, master-v2-max");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { Pod } from "../endpoints";
|
||||
|
||||
describe("Pod tests", () => {
|
||||
it("getAllContainers() should never throw", () => {
|
||||
const pod = new Pod({
|
||||
apiVersion: "foo",
|
||||
kind: "Pod",
|
||||
metadata: {
|
||||
name: "foobar",
|
||||
resourceVersion: "foobar",
|
||||
uid: "foobar",
|
||||
namespace: "default",
|
||||
selfLink: "/api/v1/pods/default/foobar",
|
||||
},
|
||||
});
|
||||
|
||||
expect(pod.getAllContainers()).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,318 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import assert from "assert";
|
||||
import type { Container, PodContainerStatus } from "../endpoints";
|
||||
import { Pod } from "../endpoints";
|
||||
|
||||
interface GetDummyPodOptions {
|
||||
running?: number;
|
||||
dead?: number;
|
||||
initRunning?: number;
|
||||
initDead?: number;
|
||||
}
|
||||
|
||||
function getDummyPod(rawOpts: GetDummyPodOptions = {}): Pod {
|
||||
const {
|
||||
running = 0,
|
||||
dead = 0,
|
||||
initDead = 0,
|
||||
initRunning = 0,
|
||||
} = rawOpts;
|
||||
|
||||
const containers: Container[] = [];
|
||||
const initContainers: Container[] = [];
|
||||
const containerStatuses: PodContainerStatus[] = [];
|
||||
const initContainerStatuses: PodContainerStatus[] = [];
|
||||
const pod = new Pod({
|
||||
apiVersion: "v1",
|
||||
kind: "Pod",
|
||||
metadata: {
|
||||
uid: "1",
|
||||
name: "test",
|
||||
resourceVersion: "v1",
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
for (let i = 0; i < running; i += 1) {
|
||||
const name = `container_running_${i}`;
|
||||
|
||||
containers.push({
|
||||
image: "dummy",
|
||||
imagePullPolicy: "Always",
|
||||
name,
|
||||
});
|
||||
containerStatuses.push({
|
||||
image: "dummy",
|
||||
imageID: "dummy",
|
||||
name,
|
||||
ready: true,
|
||||
restartCount: i,
|
||||
state: {
|
||||
running: {
|
||||
startedAt: "before",
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = 0; i < dead; i += 1) {
|
||||
const name = `container_dead_${i}`;
|
||||
|
||||
containers.push({
|
||||
image: "dummy",
|
||||
imagePullPolicy: "Always",
|
||||
name,
|
||||
});
|
||||
containerStatuses.push({
|
||||
image: "dummy",
|
||||
imageID: "dummy",
|
||||
name,
|
||||
ready: false,
|
||||
restartCount: i,
|
||||
state: {
|
||||
terminated: {
|
||||
startedAt: "before",
|
||||
exitCode: i+1,
|
||||
finishedAt: "later",
|
||||
reason: `reason_${i}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = 0; i < initRunning; i += 1) {
|
||||
const name = `container_init-running_${i}`;
|
||||
|
||||
initContainers.push({
|
||||
image: "dummy",
|
||||
imagePullPolicy: "Always",
|
||||
name,
|
||||
});
|
||||
initContainerStatuses.push({
|
||||
image: "dummy",
|
||||
imageID: "dummy",
|
||||
name,
|
||||
ready: true,
|
||||
restartCount: i,
|
||||
state: {
|
||||
running: {
|
||||
startedAt: "before",
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = 0; i < initDead; i += 1) {
|
||||
const name = `container_init-dead_${i}`;
|
||||
|
||||
initContainers.push({
|
||||
image: "dummy",
|
||||
imagePullPolicy: "Always",
|
||||
name,
|
||||
});
|
||||
initContainerStatuses.push({
|
||||
image: "dummy",
|
||||
imageID: "dummy",
|
||||
name,
|
||||
ready: false,
|
||||
restartCount: i,
|
||||
state: {
|
||||
terminated: {
|
||||
startedAt: "before",
|
||||
exitCode: i+1,
|
||||
finishedAt: "later",
|
||||
reason: `reason_${i}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return pod;
|
||||
}
|
||||
|
||||
describe("Pods", () => {
|
||||
const podTests = [];
|
||||
|
||||
for (let r = 0; r < 3; r += 1) {
|
||||
for (let d = 0; d < 3; d += 1) {
|
||||
for (let ir = 0; ir < 3; ir += 1) {
|
||||
for (let id = 0; id < 3; id += 1) {
|
||||
podTests.push([r, d, ir, id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe.each(podTests)("for [%d running, %d dead] & initial [%d running, %d dead]", (running, dead, initRunning, initDead) => {
|
||||
const pod = getDummyPod({ running, dead, initRunning, initDead });
|
||||
|
||||
function getNamedContainer(name: string) {
|
||||
return {
|
||||
image: "dummy",
|
||||
imagePullPolicy: "Always",
|
||||
name,
|
||||
};
|
||||
}
|
||||
|
||||
it("getRunningContainers should return only running and init running", () => {
|
||||
const res = [
|
||||
...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);
|
||||
});
|
||||
|
||||
it("getAllContainers should return all containers", () => {
|
||||
const res = [
|
||||
...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);
|
||||
});
|
||||
|
||||
it("getRestartsCount should return total restart counts", () => {
|
||||
function sum(len: number): number {
|
||||
let res = 0;
|
||||
|
||||
for (let i = 0; i < len; i += 1) {
|
||||
res += i;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
expect(pod.getRestartsCount()).toStrictEqual(sum(running) + sum(dead));
|
||||
});
|
||||
|
||||
it("hasIssues should return false", () => {
|
||||
expect(pod.hasIssues()).toStrictEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSelectedNodeOs", () => {
|
||||
it("should return stable", () => {
|
||||
const pod = getDummyPod();
|
||||
|
||||
pod.spec.nodeSelector = {
|
||||
"kubernetes.io/os": "foobar",
|
||||
};
|
||||
|
||||
expect(pod.getSelectedNodeOs()).toStrictEqual("foobar");
|
||||
});
|
||||
|
||||
it("should return beta", () => {
|
||||
const pod = getDummyPod();
|
||||
|
||||
pod.spec.nodeSelector = {
|
||||
"beta.kubernetes.io/os": "foobar1",
|
||||
};
|
||||
|
||||
expect(pod.getSelectedNodeOs()).toStrictEqual("foobar1");
|
||||
});
|
||||
|
||||
it("should return stable over beta", () => {
|
||||
const pod = getDummyPod();
|
||||
|
||||
pod.spec.nodeSelector = {
|
||||
"kubernetes.io/os": "foobar2",
|
||||
"beta.kubernetes.io/os": "foobar3",
|
||||
};
|
||||
|
||||
expect(pod.getSelectedNodeOs()).toStrictEqual("foobar2");
|
||||
});
|
||||
|
||||
it("should return undefined if none set", () => {
|
||||
const pod = getDummyPod();
|
||||
|
||||
expect(pod.getSelectedNodeOs()).toStrictEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasIssues", () => {
|
||||
it("should return true if a condition isn't ready", () => {
|
||||
const pod = getDummyPod({ running: 1 });
|
||||
|
||||
pod.status?.conditions.push({
|
||||
type: "Ready",
|
||||
status: "foobar",
|
||||
lastProbeTime: 1,
|
||||
lastTransitionTime: "longer ago",
|
||||
});
|
||||
|
||||
expect(pod.hasIssues()).toStrictEqual(true);
|
||||
});
|
||||
|
||||
it("should return false if a condition is non-ready", () => {
|
||||
const pod = getDummyPod({ running: 1 });
|
||||
|
||||
pod.status?.conditions.push({
|
||||
type: "dummy",
|
||||
status: "foobar",
|
||||
lastProbeTime: 1,
|
||||
lastTransitionTime: "longer ago",
|
||||
});
|
||||
|
||||
expect(pod.hasIssues()).toStrictEqual(false);
|
||||
});
|
||||
|
||||
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];
|
||||
|
||||
assert(firstStatus);
|
||||
|
||||
firstStatus.state = {
|
||||
waiting: {
|
||||
reason: "CrashLookBackOff",
|
||||
message: "too much foobar",
|
||||
},
|
||||
};
|
||||
|
||||
expect(pod.hasIssues()).toStrictEqual(true);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
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";
|
||||
|
||||
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", () => {
|
||||
it("requests Kubernetes API with PATCH verb and correct amount of replicas", () => {
|
||||
statefulSetApi.scale({ namespace: "default", name: "statefulset-1" }, 5);
|
||||
|
||||
expect(kubeJsonApi.patch).toHaveBeenCalledWith("/apis/apps/v1/namespaces/default/statefulsets/statefulset-1/scale", {
|
||||
data: {
|
||||
spec: {
|
||||
replicas: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"content-type": "application/merge-patch+json",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||
|
||||
export const apiBaseServerAddressInjectionToken = getInjectionToken<string>({
|
||||
id: "api-base-config-server-address-token",
|
||||
});
|
||||
|
||||
export const apiBaseHostHeaderInjectionToken = getInjectionToken<string>({
|
||||
id: "api-base-host-header-token",
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 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 { apiPrefix } from "../vars";
|
||||
import isDebuggingInjectable from "../vars/is-debugging.injectable";
|
||||
import isDevelopmentInjectable from "../vars/is-development.injectable";
|
||||
import { apiBaseHostHeaderInjectionToken, apiBaseServerAddressInjectionToken } from "./api-base-configs";
|
||||
import createJsonApiInjectable from "./create-json-api.injectable";
|
||||
|
||||
const apiBaseInjectable = getInjectable({
|
||||
id: "api-base",
|
||||
instantiate: (di) => {
|
||||
const createJsonApi = di.inject(createJsonApiInjectable);
|
||||
const isDebugging = di.inject(isDebuggingInjectable);
|
||||
const isDevelopment = di.inject(isDevelopmentInjectable);
|
||||
const serverAddress = di.inject(apiBaseServerAddressInjectionToken);
|
||||
const hostHeaderValue = di.inject(apiBaseHostHeaderInjectionToken);
|
||||
|
||||
return createJsonApi({
|
||||
serverAddress,
|
||||
apiBase: apiPrefix,
|
||||
debug: isDevelopment || isDebugging,
|
||||
}, {
|
||||
headers: {
|
||||
"Host": hostHeaderValue,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default apiBaseInjectable;
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||
import type { KubeJsonApi } from "./kube-json-api";
|
||||
|
||||
export const apiKubeInjectionToken = getInjectionToken<KubeJsonApi>({
|
||||
id: "api-kube-injection-token",
|
||||
});
|
||||
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* 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 type { IComputedValue } from "mobx";
|
||||
import { autorun, action, observable } from "mobx";
|
||||
import type { KubeApi } from "../kube-api";
|
||||
import type { KubeObject, ObjectReference } from "../kube-object";
|
||||
import { parseKubeApi, createKubeApiURL } from "../kube-api-parse";
|
||||
import { chain, find } from "../../utils/iter";
|
||||
|
||||
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 type FindApiCallback = (api: KubeApi<KubeObject>) => boolean;
|
||||
|
||||
interface Dependencies {
|
||||
readonly apis: IComputedValue<KubeApi[]>;
|
||||
readonly stores: IComputedValue<KubeObjectStore[]>;
|
||||
}
|
||||
|
||||
export class ApiManager {
|
||||
private readonly externalApis = observable.array<KubeApi>();
|
||||
private readonly externalStores = observable.array<KubeObjectStore>();
|
||||
|
||||
private readonly apis = observable.map<string, KubeApi>();
|
||||
|
||||
constructor(private readonly dependencies: Dependencies) {
|
||||
// NOTE: this is done to preserve the old behaviour of an API being discoverable using all previous apiBases
|
||||
autorun(() => {
|
||||
const apis = chain(this.dependencies.apis.get().values())
|
||||
.concat(this.externalApis.values());
|
||||
const removedApis = new Set(this.apis.values());
|
||||
|
||||
for (const api of apis) {
|
||||
removedApis.delete(api);
|
||||
this.apis.set(api.apiBase, api);
|
||||
}
|
||||
|
||||
for (const api of removedApis) {
|
||||
for (const [apiBase, storedApi] of this.apis) {
|
||||
if (storedApi === api) {
|
||||
this.apis.delete(apiBase);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getApi(pathOrCallback: string | FindApiCallback) {
|
||||
if (typeof pathOrCallback === "function") {
|
||||
return find(this.apis.values(), pathOrCallback);
|
||||
}
|
||||
|
||||
const { apiBase } = parseKubeApi(pathOrCallback);
|
||||
|
||||
return this.apis.get(apiBase);
|
||||
}
|
||||
|
||||
getApiByKind(kind: string, apiVersion: string) {
|
||||
return this.getApi(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>(...args: [RegisterableApi<Api>] | [string, RegisterableApi<Api>]) {
|
||||
if (args.length === 1) {
|
||||
this.externalApis.push(args[0]);
|
||||
} else {
|
||||
this.externalApis.push(args[1]);
|
||||
}
|
||||
}
|
||||
|
||||
unregisterApi(apiOrBase: string | KubeApi<KubeObject>) {
|
||||
if (typeof apiOrBase === "string") {
|
||||
const api = this.externalApis.find(api => api.apiBase === apiOrBase);
|
||||
|
||||
if (api) {
|
||||
this.externalApis.remove(api);
|
||||
}
|
||||
} else {
|
||||
this.unregisterApi(apiOrBase.apiBase);
|
||||
}
|
||||
}
|
||||
|
||||
registerStore<KubeObj>(store: RegisterableStore<KubeObj>): void;
|
||||
/**
|
||||
* @deprecated KubeObjectStore's should only every be about a single KubeApi type
|
||||
*/
|
||||
registerStore<KubeObj>(store: RegisterableStore<KubeObj>, apis: KubeApi<KubeObject>[]): void;
|
||||
|
||||
@action
|
||||
registerStore<KubeObj>(store: RegisterableStore<KubeObj>): void {
|
||||
this.externalStores.push(store);
|
||||
}
|
||||
|
||||
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(apiOrBase: string | KubeApi | undefined): KubeObjectStore | undefined {
|
||||
if (!apiOrBase) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { apiBase } = typeof apiOrBase === "string"
|
||||
? parseKubeApi(apiOrBase)
|
||||
: apiOrBase;
|
||||
const api = this.getApi(apiBase);
|
||||
|
||||
if (!api) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return chain(this.dependencies.stores.get().values())
|
||||
.concat(this.externalStores.values())
|
||||
.find(store => store.api.apiBase === api.apiBase);
|
||||
}
|
||||
|
||||
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.formatUrlForNotListing({ 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.formatUrlForNotListing({ 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,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,10 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||
import type { KubeObjectStore } from "../kube-object.store";
|
||||
|
||||
export const kubeObjectStoreInjectionToken = getInjectionToken<KubeObjectStore<any, any, any>>({
|
||||
id: "kube-object-store-token",
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 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 { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { ApiManager } from "./api-manager";
|
||||
import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx";
|
||||
import { kubeObjectStoreInjectionToken } from "./kube-object-store-token";
|
||||
import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
|
||||
import { computed } from "mobx";
|
||||
|
||||
const apiManagerInjectable = getInjectable({
|
||||
id: "api-manager",
|
||||
instantiate: (di) => {
|
||||
const computedInjectMany = di.inject(computedInjectManyInjectable);
|
||||
const storesAndApisCanBeCreated = di.inject(storesAndApisCanBeCreatedInjectionToken);
|
||||
|
||||
return new ApiManager({
|
||||
apis: storesAndApisCanBeCreated
|
||||
? computedInjectMany(kubeApiInjectionToken)
|
||||
: computed(() => []),
|
||||
stores: storesAndApisCanBeCreated
|
||||
? computedInjectMany(kubeObjectStoreInjectionToken)
|
||||
: computed(() => []),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default apiManagerInjectable;
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* 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 type { KubeObjectStoreDependencies } from "../kube-object.store";
|
||||
import { KubeObjectStore } from "../kube-object.store";
|
||||
import type { KubeObject } from "../kube-object";
|
||||
|
||||
export class CustomResourceStore<K extends KubeObject> extends KubeObjectStore<K, KubeApi<K>> {
|
||||
constructor(deps: KubeObjectStoreDependencies, api: KubeApi<K>) {
|
||||
super(deps, api);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 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 { Agent } from "https";
|
||||
import type { RequestInit } from "node-fetch";
|
||||
import lensProxyCertificateInjectable from "../certificate/lens-proxy-certificate.injectable";
|
||||
import fetchInjectable from "../fetch/fetch.injectable";
|
||||
import loggerInjectable from "../logger.injectable";
|
||||
import type { JsonApiConfig, JsonApiData, JsonApiDependencies, JsonApiParams } from "./json-api";
|
||||
import { JsonApi } from "./json-api";
|
||||
|
||||
export type CreateJsonApi = <Data = JsonApiData, Params extends JsonApiParams<Data> = JsonApiParams<Data>>(config: JsonApiConfig, reqInit?: RequestInit) => JsonApi<Data, Params>;
|
||||
|
||||
const createJsonApiInjectable = getInjectable({
|
||||
id: "create-json-api",
|
||||
instantiate: (di): CreateJsonApi => {
|
||||
const deps: JsonApiDependencies = {
|
||||
fetch: di.inject(fetchInjectable),
|
||||
logger: di.inject(loggerInjectable),
|
||||
};
|
||||
const lensProxyCert = di.inject(lensProxyCertificateInjectable);
|
||||
|
||||
return (config, reqInit) => {
|
||||
if (!config.getRequestOptions) {
|
||||
config.getRequestOptions = async () => {
|
||||
const agent = new Agent({
|
||||
ca: lensProxyCert.get().cert,
|
||||
});
|
||||
|
||||
return {
|
||||
agent,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
return new JsonApi(deps, config, reqInit);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default createJsonApiInjectable;
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* 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 loggerInjectable from "../logger.injectable";
|
||||
import { apiKubePrefix } from "../vars";
|
||||
import isDevelopmentInjectable from "../vars/is-development.injectable";
|
||||
import apiBaseInjectable from "./api-base.injectable";
|
||||
import type { KubeApiConstructor } from "./create-kube-api-for-remote-cluster.injectable";
|
||||
import createKubeJsonApiInjectable from "./create-kube-json-api.injectable";
|
||||
import { KubeApi } from "./kube-api";
|
||||
import type { KubeJsonApiDataFor, KubeObject, KubeObjectConstructor } from "./kube-object";
|
||||
|
||||
export interface CreateKubeApiForLocalClusterConfig {
|
||||
metadata: {
|
||||
uid: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CreateKubeApiForCluster {
|
||||
<Object extends KubeObject, Api extends KubeApi<Object>, Data extends KubeJsonApiDataFor<Object>>(
|
||||
cluster: CreateKubeApiForLocalClusterConfig,
|
||||
kubeClass: KubeObjectConstructor<Object, Data>,
|
||||
apiClass: KubeApiConstructor<Object, Api>,
|
||||
): Api;
|
||||
<Object extends KubeObject, Data extends KubeJsonApiDataFor<Object>>(
|
||||
cluster: CreateKubeApiForLocalClusterConfig,
|
||||
kubeClass: KubeObjectConstructor<Object, Data>,
|
||||
apiClass?: KubeApiConstructor<Object, KubeApi<Object>>,
|
||||
): KubeApi<Object>;
|
||||
}
|
||||
|
||||
const createKubeApiForClusterInjectable = getInjectable({
|
||||
id: "create-kube-api-for-cluster",
|
||||
instantiate: (di): CreateKubeApiForCluster => {
|
||||
const apiBase = di.inject(apiBaseInjectable);
|
||||
const isDevelopment = di.inject(isDevelopmentInjectable);
|
||||
const createKubeJsonApi = di.inject(createKubeJsonApiInjectable);
|
||||
const logger = di.inject(loggerInjectable);
|
||||
|
||||
return (
|
||||
cluster: CreateKubeApiForLocalClusterConfig,
|
||||
kubeClass: KubeObjectConstructor<KubeObject, KubeJsonApiDataFor<KubeObject>>,
|
||||
apiClass?: KubeApiConstructor<KubeObject, KubeApi<KubeObject>>,
|
||||
) => {
|
||||
const request = createKubeJsonApi(
|
||||
{
|
||||
serverAddress: apiBase.config.serverAddress,
|
||||
apiBase: apiKubePrefix,
|
||||
debug: isDevelopment,
|
||||
}, {
|
||||
headers: {
|
||||
"Host": `${cluster.metadata.uid}.lens.app:${new URL(apiBase.config.serverAddress).port}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (apiClass) {
|
||||
return new apiClass({
|
||||
objectConstructor: kubeClass,
|
||||
request,
|
||||
});
|
||||
}
|
||||
|
||||
return new KubeApi(
|
||||
{
|
||||
logger,
|
||||
maybeKubeApi: undefined,
|
||||
},
|
||||
{
|
||||
objectConstructor: kubeClass,
|
||||
request,
|
||||
},
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default createKubeApiForClusterInjectable;
|
||||
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* 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 { AgentOptions } from "https";
|
||||
import { Agent } from "https";
|
||||
import type { RequestInit } from "node-fetch";
|
||||
import loggerInjectable from "../logger.injectable";
|
||||
import isDevelopmentInjectable from "../vars/is-development.injectable";
|
||||
import createKubeJsonApiInjectable from "./create-kube-json-api.injectable";
|
||||
import type { KubeApiOptions } from "./kube-api";
|
||||
import { KubeApi } from "./kube-api";
|
||||
import type { KubeJsonApiDataFor, KubeObject, KubeObjectConstructor } from "./kube-object";
|
||||
|
||||
export interface CreateKubeApiForRemoteClusterConfig {
|
||||
cluster: {
|
||||
server: string;
|
||||
caData?: string;
|
||||
skipTLSVerify?: boolean;
|
||||
};
|
||||
user: {
|
||||
token?: string | (() => Promise<string>);
|
||||
clientCertificateData?: string;
|
||||
clientKeyData?: string;
|
||||
};
|
||||
/**
|
||||
* Custom instance of https.agent to use for the requests
|
||||
*
|
||||
* @remarks the custom agent replaced default agent, options skipTLSVerify,
|
||||
* clientCertificateData, clientKeyData and caData are ignored.
|
||||
*/
|
||||
agent?: Agent;
|
||||
}
|
||||
|
||||
export type KubeApiConstructor<Object extends KubeObject, Api extends KubeApi<Object>> = new (apiOpts: KubeApiOptions<Object>) => Api;
|
||||
|
||||
export interface CreateKubeApiForRemoteCluster {
|
||||
<Object extends KubeObject, Api extends KubeApi<Object>, Data extends KubeJsonApiDataFor<Object>>(
|
||||
config: CreateKubeApiForRemoteClusterConfig,
|
||||
kubeClass: KubeObjectConstructor<Object, Data>,
|
||||
apiClass: KubeApiConstructor<Object, Api>,
|
||||
): Api;
|
||||
<Object extends KubeObject, Data extends KubeJsonApiDataFor<Object>>(
|
||||
config: CreateKubeApiForRemoteClusterConfig,
|
||||
kubeClass: KubeObjectConstructor<Object, Data>,
|
||||
apiClass?: KubeApiConstructor<Object, KubeApi<Object>>,
|
||||
): KubeApi<Object>;
|
||||
}
|
||||
|
||||
const createKubeApiForRemoteClusterInjectable = getInjectable({
|
||||
id: "create-kube-api-for-remote-cluster",
|
||||
instantiate: (di): CreateKubeApiForRemoteCluster => {
|
||||
const isDevelopment = di.inject(isDevelopmentInjectable);
|
||||
const createKubeJsonApi = di.inject(createKubeJsonApiInjectable);
|
||||
const logger = di.inject(loggerInjectable);
|
||||
|
||||
return (
|
||||
config: CreateKubeApiForRemoteClusterConfig,
|
||||
kubeClass: KubeObjectConstructor<KubeObject, KubeJsonApiDataFor<KubeObject>>,
|
||||
apiClass?: KubeApiConstructor<KubeObject, KubeApi<KubeObject>>,
|
||||
) => {
|
||||
const reqInit: RequestInit = {};
|
||||
const agentOptions: AgentOptions = {};
|
||||
|
||||
if (config.cluster.skipTLSVerify === true) {
|
||||
agentOptions.rejectUnauthorized = false;
|
||||
}
|
||||
|
||||
if (config.user.clientCertificateData) {
|
||||
agentOptions.cert = config.user.clientCertificateData;
|
||||
}
|
||||
|
||||
if (config.user.clientKeyData) {
|
||||
agentOptions.key = config.user.clientKeyData;
|
||||
}
|
||||
|
||||
if (config.cluster.caData) {
|
||||
agentOptions.ca = config.cluster.caData;
|
||||
}
|
||||
|
||||
if (Object.keys(agentOptions).length > 0) {
|
||||
reqInit.agent = new Agent(agentOptions);
|
||||
}
|
||||
|
||||
if (config.agent) {
|
||||
reqInit.agent = config.agent;
|
||||
}
|
||||
|
||||
const token = config.user.token;
|
||||
const request = createKubeJsonApi({
|
||||
serverAddress: config.cluster.server,
|
||||
apiBase: "",
|
||||
debug: isDevelopment,
|
||||
...(token ? {
|
||||
getRequestOptions: async () => ({
|
||||
headers: {
|
||||
"Authorization": `Bearer ${typeof token === "function" ? await token() : token}`,
|
||||
},
|
||||
}),
|
||||
} : {}),
|
||||
}, reqInit);
|
||||
|
||||
if (apiClass) {
|
||||
return new apiClass({
|
||||
objectConstructor: kubeClass,
|
||||
request,
|
||||
});
|
||||
}
|
||||
|
||||
return new KubeApi(
|
||||
{
|
||||
logger,
|
||||
maybeKubeApi: undefined,
|
||||
},
|
||||
{
|
||||
objectConstructor: kubeClass,
|
||||
request,
|
||||
},
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default createKubeApiForRemoteClusterInjectable;
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 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 loggerInjectable from "../logger.injectable";
|
||||
import type { DerivedKubeApiOptions, KubeApiDependencies } from "./kube-api";
|
||||
import maybeKubeApiInjectable from "./maybe-kube-api.injectable";
|
||||
|
||||
export interface CreateKubeApi {
|
||||
<Api>(ctor: new (deps: KubeApiDependencies, opts: DerivedKubeApiOptions) => Api, opts?: DerivedKubeApiOptions): Api;
|
||||
}
|
||||
|
||||
const createKubeApiInjectable = getInjectable({
|
||||
id: "create-kube-api",
|
||||
instantiate: (di): CreateKubeApi => {
|
||||
const deps: KubeApiDependencies = {
|
||||
logger: di.inject(loggerInjectable),
|
||||
maybeKubeApi: di.inject(maybeKubeApiInjectable),
|
||||
};
|
||||
|
||||
return (ctor, opts) => new ctor(deps, opts ?? {});
|
||||
},
|
||||
});
|
||||
|
||||
export default createKubeApiInjectable;
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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 { apiKubePrefix } from "../vars";
|
||||
import isDebuggingInjectable from "../vars/is-debugging.injectable";
|
||||
import { apiBaseHostHeaderInjectionToken, apiBaseServerAddressInjectionToken } from "./api-base-configs";
|
||||
import createKubeJsonApiInjectable from "./create-kube-json-api.injectable";
|
||||
import type { KubeJsonApi } from "./kube-json-api";
|
||||
|
||||
export type CreateKubeJsonApiForCluster = (clusterId: string) => KubeJsonApi;
|
||||
|
||||
const createKubeJsonApiForClusterInjectable = getInjectable({
|
||||
id: "create-kube-json-api-for-cluster",
|
||||
instantiate: (di): CreateKubeJsonApiForCluster => {
|
||||
const createKubeJsonApi = di.inject(createKubeJsonApiInjectable);
|
||||
const isDebugging = di.inject(isDebuggingInjectable);
|
||||
|
||||
return (clusterId) => createKubeJsonApi(
|
||||
{
|
||||
serverAddress: di.inject(apiBaseServerAddressInjectionToken),
|
||||
apiBase: apiKubePrefix,
|
||||
debug: isDebugging,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Host": `${clusterId}.${di.inject(apiBaseHostHeaderInjectionToken)}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default createKubeJsonApiForClusterInjectable;
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 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 { Agent } from "https";
|
||||
import type { RequestInit } from "node-fetch";
|
||||
import lensProxyCertificateInjectable from "../certificate/lens-proxy-certificate.injectable";
|
||||
import fetchInjectable from "../fetch/fetch.injectable";
|
||||
import loggerInjectable from "../logger.injectable";
|
||||
import type { JsonApiConfig, JsonApiDependencies } from "./json-api";
|
||||
import { KubeJsonApi } from "./kube-json-api";
|
||||
|
||||
export type CreateKubeJsonApi = (config: JsonApiConfig, reqInit?: RequestInit) => KubeJsonApi;
|
||||
|
||||
const createKubeJsonApiInjectable = getInjectable({
|
||||
id: "create-kube-json-api",
|
||||
instantiate: (di): CreateKubeJsonApi => {
|
||||
const dependencies: JsonApiDependencies = {
|
||||
fetch: di.inject(fetchInjectable),
|
||||
logger: di.inject(loggerInjectable),
|
||||
};
|
||||
const lensProxyCert = di.inject(lensProxyCertificateInjectable);
|
||||
|
||||
return (config, reqInit) => {
|
||||
if (!config.getRequestOptions) {
|
||||
config.getRequestOptions = async () => {
|
||||
const agent = new Agent({
|
||||
ca: lensProxyCert.get().cert,
|
||||
});
|
||||
|
||||
return {
|
||||
agent,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
return new KubeJsonApi(dependencies, config, reqInit);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default createKubeJsonApiInjectable;
|
||||
@@ -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 assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { ClusterRoleBindingApi } from "./cluster-role-binding.api";
|
||||
import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
|
||||
import loggerInjectable from "../../logger.injectable";
|
||||
import maybeKubeApiInjectable from "../maybe-kube-api.injectable";
|
||||
|
||||
const clusterRoleBindingApiInjectable = getInjectable({
|
||||
id: "cluster-role-binding-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "clusterRoleBindingApi is only accessible in certain environments");
|
||||
|
||||
return new ClusterRoleBindingApi({
|
||||
logger: di.inject(loggerInjectable),
|
||||
maybeKubeApi: di.inject(maybeKubeApiInjectable),
|
||||
});
|
||||
},
|
||||
|
||||
injectionToken: kubeApiInjectionToken,
|
||||
});
|
||||
|
||||
export default clusterRoleBindingApiInjectable;
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { DerivedKubeApiOptions, KubeApiDependencies } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import type { ClusterScopedMetadata, KubeObjectMetadata, KubeObjectScope } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { RoleRef } from "./types/role-ref";
|
||||
import type { Subject } from "./types/subject";
|
||||
|
||||
export interface ClusterRoleBindingData extends KubeJsonApiData<KubeObjectMetadata<KubeObjectScope.Cluster>, void, void> {
|
||||
subjects?: Subject[];
|
||||
roleRef: RoleRef;
|
||||
}
|
||||
|
||||
export class ClusterRoleBinding extends KubeObject<
|
||||
ClusterScopedMetadata,
|
||||
void,
|
||||
void
|
||||
> {
|
||||
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 ?? [];
|
||||
}
|
||||
|
||||
getSubjectNames(): string {
|
||||
return this.getSubjects().map(subject => subject.name).join(", ");
|
||||
}
|
||||
}
|
||||
|
||||
export class ClusterRoleBindingApi extends KubeApi<ClusterRoleBinding, ClusterRoleBindingData> {
|
||||
constructor(deps: KubeApiDependencies, opts: DerivedKubeApiOptions = {}) {
|
||||
super(deps, {
|
||||
...opts,
|
||||
objectConstructor: ClusterRoleBinding,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { ClusterRoleApi } from "./cluster-role.api";
|
||||
import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
|
||||
import loggerInjectable from "../../logger.injectable";
|
||||
import maybeKubeApiInjectable from "../maybe-kube-api.injectable";
|
||||
|
||||
const clusterRoleApiInjectable = getInjectable({
|
||||
id: "cluster-role-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "clusterRoleApi is only available in certain environments");
|
||||
|
||||
return new ClusterRoleApi({
|
||||
logger: di.inject(loggerInjectable),
|
||||
maybeKubeApi: di.inject(maybeKubeApiInjectable),
|
||||
});
|
||||
},
|
||||
|
||||
injectionToken: kubeApiInjectionToken,
|
||||
});
|
||||
|
||||
export default clusterRoleApiInjectable;
|
||||
@@ -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 { DerivedKubeApiOptions, KubeApiDependencies } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import type { ClusterScopedMetadata, 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 ClusterRoleData extends KubeJsonApiData<KubeObjectMetadata<KubeObjectScope.Cluster>, void, void> {
|
||||
rules?: PolicyRule[];
|
||||
aggregationRule?: AggregationRule;
|
||||
}
|
||||
|
||||
export class ClusterRole extends KubeObject<
|
||||
ClusterScopedMetadata,
|
||||
void,
|
||||
void
|
||||
> {
|
||||
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 || [];
|
||||
}
|
||||
}
|
||||
|
||||
export class ClusterRoleApi extends KubeApi<ClusterRole, ClusterRoleData> {
|
||||
constructor(deps: KubeApiDependencies, opts: DerivedKubeApiOptions = {}) {
|
||||
super(deps, {
|
||||
...opts,
|
||||
objectConstructor: ClusterRole,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { ClusterApi } from "./cluster.api";
|
||||
import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
|
||||
import loggerInjectable from "../../logger.injectable";
|
||||
import maybeKubeApiInjectable from "../maybe-kube-api.injectable";
|
||||
|
||||
const clusterApiInjectable = getInjectable({
|
||||
id: "cluster-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "clusterApi is only available in certain environments");
|
||||
|
||||
return new ClusterApi({
|
||||
logger: di.inject(loggerInjectable),
|
||||
maybeKubeApi: di.inject(maybeKubeApiInjectable),
|
||||
});
|
||||
},
|
||||
|
||||
injectionToken: kubeApiInjectionToken,
|
||||
});
|
||||
|
||||
export default clusterApiInjectable;
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* 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 { DerivedKubeApiOptions, KubeApiDependencies } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
|
||||
export class ClusterApi extends KubeApi<Cluster> {
|
||||
/**
|
||||
* @deprecated This field is legacy and never used.
|
||||
*/
|
||||
static kind = "Cluster";
|
||||
|
||||
/**
|
||||
* @deprecated This field is legacy and never used.
|
||||
*/
|
||||
static namespaced = true;
|
||||
|
||||
constructor(deps: KubeApiDependencies, opts?: DerivedKubeApiOptions) {
|
||||
super(deps, {
|
||||
...opts ?? {},
|
||||
objectConstructor: Cluster,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export enum ClusterStatus {
|
||||
ACTIVE = "Active",
|
||||
CREATING = "Creating",
|
||||
REMOVING = "Removing",
|
||||
ERROR = "Error",
|
||||
}
|
||||
|
||||
export interface Cluster {
|
||||
spec: {
|
||||
clusterNetwork?: {
|
||||
serviceDomain?: string;
|
||||
pods?: {
|
||||
cidrBlocks?: string[];
|
||||
};
|
||||
services?: {
|
||||
cidrBlocks?: string[];
|
||||
};
|
||||
};
|
||||
providerSpec: {
|
||||
value: {
|
||||
profile: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
status?: {
|
||||
apiEndpoints: {
|
||||
host: string;
|
||||
port: string;
|
||||
}[];
|
||||
providerStatus: {
|
||||
adminUser?: string;
|
||||
adminPassword?: string;
|
||||
kubeconfig?: string;
|
||||
processState?: string;
|
||||
lensAddress?: string;
|
||||
};
|
||||
errorMessage?: string;
|
||||
errorReason?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class Cluster extends KubeObject {
|
||||
static kind = "Cluster";
|
||||
static apiBase = "/apis/cluster.k8s.io/v1alpha1/clusters";
|
||||
static namespaced = true;
|
||||
|
||||
getStatus() {
|
||||
if (this.metadata.deletionTimestamp) return ClusterStatus.REMOVING;
|
||||
if (!this.status || !this.status) return ClusterStatus.CREATING;
|
||||
if (this.status.errorMessage) return ClusterStatus.ERROR;
|
||||
|
||||
return ClusterStatus.ACTIVE;
|
||||
}
|
||||
}
|
||||
@@ -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 assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { ComponentStatusApi } from "./component-status.api";
|
||||
import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
|
||||
import maybeKubeApiInjectable from "../maybe-kube-api.injectable";
|
||||
import loggerInjectable from "../../logger.injectable";
|
||||
|
||||
const componentStatusApiInjectable = getInjectable({
|
||||
id: "component-status-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "componentStatusApi is only available in certain environments");
|
||||
|
||||
return new ComponentStatusApi({
|
||||
logger: di.inject(loggerInjectable),
|
||||
maybeKubeApi: di.inject(maybeKubeApiInjectable),
|
||||
});
|
||||
},
|
||||
|
||||
injectionToken: kubeApiInjectionToken,
|
||||
});
|
||||
|
||||
export default componentStatusApiInjectable;
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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 { DerivedKubeApiOptions, KubeApiDependencies } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
|
||||
export interface ComponentStatusCondition {
|
||||
type: string;
|
||||
status: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ComponentStatus {
|
||||
conditions: ComponentStatusCondition[];
|
||||
}
|
||||
|
||||
export class ComponentStatus extends KubeObject {
|
||||
static kind = "ComponentStatus";
|
||||
static namespaced = false;
|
||||
static apiBase = "/api/v1/componentstatuses";
|
||||
|
||||
getTruthyConditions() {
|
||||
return this.conditions.filter(c => c.status === "True");
|
||||
}
|
||||
}
|
||||
|
||||
export class ComponentStatusApi extends KubeApi<ComponentStatus> {
|
||||
constructor(deps: KubeApiDependencies, opts: DerivedKubeApiOptions = {}) {
|
||||
super(deps, {
|
||||
...opts,
|
||||
objectConstructor: ComponentStatus,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { ConfigMapApi } from "./config-map.api";
|
||||
import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
|
||||
import loggerInjectable from "../../logger.injectable";
|
||||
import maybeKubeApiInjectable from "../maybe-kube-api.injectable";
|
||||
|
||||
const configMapApiInjectable = getInjectable({
|
||||
id: "config-map-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "configMapApi is only available in certain environments");
|
||||
|
||||
return new ConfigMapApi({
|
||||
logger: di.inject(loggerInjectable),
|
||||
maybeKubeApi: di.inject(maybeKubeApiInjectable),
|
||||
});
|
||||
},
|
||||
|
||||
injectionToken: kubeApiInjectionToken,
|
||||
});
|
||||
|
||||
export default configMapApiInjectable;
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { KubeObjectMetadata, KubeObjectScope, NamespaceScopedMetadata } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import type { DerivedKubeApiOptions, KubeApiDependencies } 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<
|
||||
NamespaceScopedMetadata,
|
||||
void,
|
||||
void
|
||||
> {
|
||||
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(deps: KubeApiDependencies, opts?: DerivedKubeApiOptions) {
|
||||
super(deps, {
|
||||
objectConstructor: ConfigMap,
|
||||
...opts ?? {},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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";
|
||||
import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
|
||||
import loggerInjectable from "../../logger.injectable";
|
||||
import maybeKubeApiInjectable from "../maybe-kube-api.injectable";
|
||||
|
||||
const cronJobApiInjectable = getInjectable({
|
||||
id: "cron-job-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "cronJobApi is only available in certain environments");
|
||||
|
||||
return new CronJobApi({
|
||||
logger: di.inject(loggerInjectable),
|
||||
maybeKubeApi: di.inject(maybeKubeApiInjectable),
|
||||
}, {
|
||||
checkPreferredVersion: true,
|
||||
});
|
||||
},
|
||||
|
||||
injectionToken: kubeApiInjectionToken,
|
||||
});
|
||||
|
||||
export default cronJobApiInjectable;
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import moment from "moment";
|
||||
import type { NamespaceScopedMetadata, ObjectReference } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { formatDuration } from "../../utils/formatDuration";
|
||||
import type { DerivedKubeApiOptions, KubeApiDependencies } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { JobTemplateSpec } from "./types/job-template-spec";
|
||||
|
||||
export class CronJobApi extends KubeApi<CronJob> {
|
||||
constructor(deps: KubeApiDependencies, opts: DerivedKubeApiOptions) {
|
||||
super(deps, {
|
||||
...opts,
|
||||
objectConstructor: CronJob,
|
||||
});
|
||||
}
|
||||
|
||||
suspend(params: { namespace: string; name: string }) {
|
||||
return this.request.patch(this.getUrl(params), {
|
||||
data: {
|
||||
spec: {
|
||||
suspend: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"content-type": "application/strategic-merge-patch+json",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
resume(params: { namespace: string; name: string }) {
|
||||
return this.request.patch(this.getUrl(params), {
|
||||
data: {
|
||||
spec: {
|
||||
suspend: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"content-type": "application/strategic-merge-patch+json",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export interface CronJobSpec {
|
||||
concurrencyPolicy?: string;
|
||||
failedJobsHistoryLimit?: number;
|
||||
jobTemplate?: JobTemplateSpec;
|
||||
schedule: string;
|
||||
startingDeadlineSeconds?: number;
|
||||
successfulJobsHistoryLimit?: number;
|
||||
suspend?: boolean;
|
||||
}
|
||||
|
||||
export interface CronJobStatus {
|
||||
lastScheduleTime?: string;
|
||||
lastSuccessfulTime?: string;
|
||||
active?: ObjectReference[];
|
||||
}
|
||||
|
||||
export class CronJob extends KubeObject<
|
||||
NamespaceScopedMetadata,
|
||||
CronJobStatus,
|
||||
CronJobSpec
|
||||
> {
|
||||
static readonly kind = "CronJob";
|
||||
static readonly namespaced = true;
|
||||
static readonly apiBase = "/apis/batch/v1/cronjobs";
|
||||
|
||||
getSuspendFlag() {
|
||||
return (this.spec.suspend ?? false).toString();
|
||||
}
|
||||
|
||||
getLastScheduleTime() {
|
||||
if (!this.status?.lastScheduleTime) return "-";
|
||||
const diff = moment().diff(this.status.lastScheduleTime);
|
||||
|
||||
return formatDuration(diff, true);
|
||||
}
|
||||
|
||||
getSchedule() {
|
||||
return this.spec.schedule;
|
||||
}
|
||||
|
||||
isNeverRun() {
|
||||
const schedule = this.getSchedule();
|
||||
const daysInMonth = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||||
const stamps = schedule.split(" ");
|
||||
const day = Number(stamps[stamps.length - 3]); // 1-31
|
||||
const month = Number(stamps[stamps.length - 2]); // 1-12
|
||||
|
||||
if (schedule.startsWith("@")) return false;
|
||||
|
||||
return day > daysInMonth[month - 1];
|
||||
}
|
||||
|
||||
isSuspend() {
|
||||
return this.spec.suspend;
|
||||
}
|
||||
}
|
||||
+27
@@ -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 assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { CustomResourceDefinitionApi } from "./custom-resource-definition.api";
|
||||
import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
|
||||
import maybeKubeApiInjectable from "../maybe-kube-api.injectable";
|
||||
import loggerInjectable from "../../logger.injectable";
|
||||
|
||||
const customResourceDefinitionApiInjectable = getInjectable({
|
||||
id: "custom-resource-definition-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "customResourceDefinitionApi is only available in certain environments");
|
||||
|
||||
return new CustomResourceDefinitionApi({
|
||||
logger: di.inject(loggerInjectable),
|
||||
maybeKubeApi: di.inject(maybeKubeApiInjectable),
|
||||
});
|
||||
},
|
||||
|
||||
injectionToken: kubeApiInjectionToken,
|
||||
});
|
||||
|
||||
export default customResourceDefinitionApiInjectable;
|
||||
@@ -0,0 +1,244 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
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, ClusterScopedMetadata } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { DerivedKubeApiOptions, KubeApiDependencies } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { JSONSchemaProps } from "./types/json-schema-props";
|
||||
|
||||
interface AdditionalPrinterColumnsCommon {
|
||||
name: string;
|
||||
type: "integer" | "number" | "string" | "boolean" | "date";
|
||||
priority?: number;
|
||||
format?: "int32" | "int64" | "float" | "double" | "byte" | "binary" | "date" | "date-time" | "password";
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export type AdditionalPrinterColumnsV1 = AdditionalPrinterColumnsCommon & {
|
||||
jsonPath: string;
|
||||
};
|
||||
|
||||
type AdditionalPrinterColumnsV1Beta = AdditionalPrinterColumnsCommon & {
|
||||
JSONPath: string;
|
||||
};
|
||||
|
||||
export interface CustomResourceValidation {
|
||||
openAPIV3Schema?: JSONSchemaProps;
|
||||
}
|
||||
|
||||
export interface CustomResourceDefinitionVersion {
|
||||
name: string;
|
||||
served: boolean;
|
||||
storage: boolean;
|
||||
schema?: CustomResourceValidation; // required in v1 but not present in v1beta
|
||||
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: CustomResourceDefinitionNames;
|
||||
scope: "Namespaced" | "Cluster";
|
||||
/**
|
||||
* @deprecated for apiextensions.k8s.io/v1 but used in v1beta1
|
||||
*/
|
||||
validation?: object;
|
||||
versions?: CustomResourceDefinitionVersion[];
|
||||
conversion?: CustomResourceConversion;
|
||||
/**
|
||||
* @deprecated for apiextensions.k8s.io/v1 but used in v1beta1
|
||||
*/
|
||||
additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[];
|
||||
preserveUnknownFields?: boolean;
|
||||
}
|
||||
|
||||
export interface CustomResourceDefinitionConditionAcceptedNames {
|
||||
plural: string;
|
||||
singular: string;
|
||||
kind: string;
|
||||
shortNames: string[];
|
||||
listKind: string;
|
||||
}
|
||||
|
||||
export interface CustomResourceDefinitionStatus {
|
||||
conditions?: BaseKubeObjectCondition[];
|
||||
acceptedNames: CustomResourceDefinitionConditionAcceptedNames;
|
||||
storedVersions: string[];
|
||||
}
|
||||
|
||||
export class CustomResourceDefinition extends KubeObject<
|
||||
ClusterScopedMetadata,
|
||||
CustomResourceDefinitionStatus,
|
||||
CustomResourceDefinitionSpec
|
||||
> {
|
||||
static kind = "CustomResourceDefinition";
|
||||
static namespaced = false;
|
||||
static apiBase = "/apis/apiextensions.k8s.io/v1/customresourcedefinitions";
|
||||
|
||||
getResourceUrl() {
|
||||
const di = getLegacyGlobalDiForExtensionApi();
|
||||
|
||||
const customResourcesRoute = di.inject(customResourcesRouteInjectable);
|
||||
|
||||
return buildURL(customResourcesRoute.path, {
|
||||
params: {
|
||||
group: this.getGroup(),
|
||||
name: this.getPluralName(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getResourceApiBase() {
|
||||
const { group } = this.spec;
|
||||
|
||||
return `/apis/${group}/${this.getVersion()}/${this.getPluralName()}`;
|
||||
}
|
||||
|
||||
getPluralName() {
|
||||
return this.getNames().plural;
|
||||
}
|
||||
|
||||
getResourceKind() {
|
||||
return this.spec.names.kind;
|
||||
}
|
||||
|
||||
getResourceTitle() {
|
||||
const name = this.getPluralName();
|
||||
|
||||
return name[0].toUpperCase() + name.slice(1);
|
||||
}
|
||||
|
||||
getGroup() {
|
||||
return this.spec.group;
|
||||
}
|
||||
|
||||
getScope() {
|
||||
return this.spec.scope;
|
||||
}
|
||||
|
||||
getPreferedVersion(): CustomResourceDefinitionVersion {
|
||||
const { apiVersion } = this;
|
||||
|
||||
switch (apiVersion) {
|
||||
case "apiextensions.k8s.io/v1":
|
||||
for (const version of this.spec.versions ?? []) {
|
||||
if (version.storage) {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "apiextensions.k8s.io/v1beta1": {
|
||||
const { additionalPrinterColumns: apc } = this.spec;
|
||||
const additionalPrinterColumns = apc?.map(({ JSONPath, ...apc }) => ({ ...apc, jsonPath: JSONPath }));
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
name: this.spec.version!,
|
||||
served: true,
|
||||
storage: true,
|
||||
schema: this.spec.validation,
|
||||
additionalPrinterColumns,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unknown apiVersion=${apiVersion}: Failed to find a version for CustomResourceDefinition ${this.metadata.name}`);
|
||||
}
|
||||
|
||||
getVersion() {
|
||||
return this.getPreferedVersion().name;
|
||||
}
|
||||
|
||||
isNamespaced() {
|
||||
return this.getScope() === "Namespaced";
|
||||
}
|
||||
|
||||
getStoredVersions() {
|
||||
return this.status?.storedVersions.join(", ") ?? "";
|
||||
}
|
||||
|
||||
getNames() {
|
||||
return this.spec.names;
|
||||
}
|
||||
|
||||
getConversion() {
|
||||
return JSON.stringify(this.spec.conversion);
|
||||
}
|
||||
|
||||
getPrinterColumns(ignorePriority = true): AdditionalPrinterColumnsV1[] {
|
||||
const columns = this.getPreferedVersion().additionalPrinterColumns ?? [];
|
||||
|
||||
return columns
|
||||
.filter(column => column.name.toLowerCase() != "age" && (ignorePriority || !column.priority));
|
||||
}
|
||||
|
||||
getValidation() {
|
||||
return JSON.stringify(this.getPreferedVersion().schema, null, 2);
|
||||
}
|
||||
|
||||
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})`,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class CustomResourceDefinitionApi extends KubeApi<CustomResourceDefinition> {
|
||||
constructor(deps: KubeApiDependencies, opts: DerivedKubeApiOptions = {}) {
|
||||
super(deps, {
|
||||
objectConstructor: CustomResourceDefinition,
|
||||
checkPreferredVersion: true,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { DaemonSetApi } from "./daemon-set.api";
|
||||
import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
|
||||
import loggerInjectable from "../../logger.injectable";
|
||||
import maybeKubeApiInjectable from "../maybe-kube-api.injectable";
|
||||
|
||||
const daemonSetApiInjectable = getInjectable({
|
||||
id: "daemon-set-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "daemonSetApi is only available in certain environements");
|
||||
|
||||
return new DaemonSetApi({
|
||||
logger: di.inject(loggerInjectable),
|
||||
maybeKubeApi: di.inject(maybeKubeApiInjectable),
|
||||
});
|
||||
},
|
||||
|
||||
injectionToken: kubeApiInjectionToken,
|
||||
});
|
||||
|
||||
export default daemonSetApiInjectable;
|
||||
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import moment from "moment";
|
||||
|
||||
import type { DerivedKubeApiOptions, KubeApiDependencies } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeObjectStatus, LabelSelector, NamespaceScopedMetadata } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { PodTemplateSpec } from "./types/pod-template-spec";
|
||||
|
||||
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<
|
||||
NamespaceScopedMetadata,
|
||||
DaemonSetStatus,
|
||||
DaemonSetSpec
|
||||
> {
|
||||
static kind = "DaemonSet";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/apps/v1/daemonsets";
|
||||
|
||||
getSelectors(): string[] {
|
||||
return KubeObject.stringifyLabels(this.spec.selector.matchLabels);
|
||||
}
|
||||
|
||||
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 = 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(deps: KubeApiDependencies, opts?: DerivedKubeApiOptions) {
|
||||
super(deps, {
|
||||
...opts ?? {},
|
||||
objectConstructor: DaemonSet,
|
||||
});
|
||||
}
|
||||
|
||||
restart(params: { namespace: string; name: string }) {
|
||||
return this.request.patch(this.getUrl(params), {
|
||||
data: {
|
||||
spec: {
|
||||
template: {
|
||||
metadata: {
|
||||
annotations: { "kubectl.kubernetes.io/restartedAt" : moment.utc().format() },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"content-type": "application/strategic-merge-patch+json",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { DeploymentApi } from "./deployment.api";
|
||||
import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
|
||||
import loggerInjectable from "../../logger.injectable";
|
||||
import maybeKubeApiInjectable from "../maybe-kube-api.injectable";
|
||||
|
||||
const deploymentApiInjectable = getInjectable({
|
||||
id: "deployment-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "deploymentApi is only available in certain environments");
|
||||
|
||||
return new DeploymentApi({
|
||||
logger: di.inject(loggerInjectable),
|
||||
maybeKubeApi: di.inject(maybeKubeApiInjectable),
|
||||
});
|
||||
},
|
||||
|
||||
injectionToken: kubeApiInjectionToken,
|
||||
});
|
||||
|
||||
export default deploymentApiInjectable;
|
||||
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import moment from "moment";
|
||||
|
||||
import type { DerivedKubeApiOptions, KubeApiDependencies } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { PodSpec } from "./pod.api";
|
||||
import type { KubeObjectStatus, LabelSelector, NamespaceScopedMetadata } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { hasTypedProperty, isNumber, isObject } from "../../utils";
|
||||
|
||||
export class DeploymentApi extends KubeApi<Deployment> {
|
||||
constructor(deps: KubeApiDependencies, opts?: DerivedKubeApiOptions) {
|
||||
super(deps, {
|
||||
...opts ?? {},
|
||||
objectConstructor: Deployment,
|
||||
});
|
||||
}
|
||||
|
||||
protected getScaleApiUrl(params: { namespace: string; name: string }) {
|
||||
return `${this.getUrl(params)}/scale`;
|
||||
}
|
||||
|
||||
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) {
|
||||
return this.request.patch(this.getScaleApiUrl(params), {
|
||||
data: {
|
||||
spec: {
|
||||
replicas,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"content-type": "application/merge-patch+json",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
restart(params: { namespace: string; name: string }) {
|
||||
return this.request.patch(this.getUrl(params), {
|
||||
data: {
|
||||
spec: {
|
||||
template: {
|
||||
metadata: {
|
||||
annotations: { "kubectl.kubernetes.io/restartedAt" : moment.utc().format() },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"content-type": "application/strategic-merge-patch+json",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export interface DeploymentSpec {
|
||||
replicas: number;
|
||||
selector: LabelSelector;
|
||||
template: {
|
||||
metadata: {
|
||||
creationTimestamp?: string;
|
||||
labels: Partial<Record<string, string>>;
|
||||
annotations?: Partial<Record<string, string>>;
|
||||
};
|
||||
spec: PodSpec;
|
||||
};
|
||||
strategy: {
|
||||
type: string;
|
||||
rollingUpdate: {
|
||||
maxUnavailable: number;
|
||||
maxSurge: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface DeploymentStatus extends KubeObjectStatus {
|
||||
observedGeneration: number;
|
||||
replicas: number;
|
||||
updatedReplicas: number;
|
||||
readyReplicas: number;
|
||||
availableReplicas?: number;
|
||||
unavailableReplicas?: number;
|
||||
}
|
||||
|
||||
export class Deployment extends KubeObject<
|
||||
NamespaceScopedMetadata,
|
||||
DeploymentStatus,
|
||||
DeploymentSpec
|
||||
> {
|
||||
static kind = "Deployment";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/apps/v1/deployments";
|
||||
|
||||
getSelectors(): string[] {
|
||||
return KubeObject.stringifyLabels(this.spec.selector.matchLabels);
|
||||
}
|
||||
|
||||
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 (activeOnly) {
|
||||
return conditions.filter(c => c.status === "True");
|
||||
}
|
||||
|
||||
return conditions;
|
||||
}
|
||||
|
||||
getConditionsText(activeOnly = true) {
|
||||
return this.getConditions(activeOnly)
|
||||
.map(({ type }) => type)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
getReplicas() {
|
||||
return this.spec.replicas || 0;
|
||||
}
|
||||
}
|
||||
@@ -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 assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { EndpointsApi } from "./endpoint.api";
|
||||
import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
|
||||
import loggerInjectable from "../../logger.injectable";
|
||||
import maybeKubeApiInjectable from "../maybe-kube-api.injectable";
|
||||
|
||||
const endpointsApiInjectable = getInjectable({
|
||||
id: "endpoints-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "endpointsApi is only available in certain environments");
|
||||
|
||||
return new EndpointsApi({
|
||||
logger: di.inject(loggerInjectable),
|
||||
maybeKubeApi: di.inject(maybeKubeApiInjectable),
|
||||
});
|
||||
},
|
||||
|
||||
injectionToken: kubeApiInjectionToken,
|
||||
});
|
||||
|
||||
export default endpointsApiInjectable;
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* 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 { KubeObjectMetadata, KubeObjectScope, NamespaceScopedMetadata, ObjectReference } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { DerivedKubeApiOptions, KubeApiDependencies } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
|
||||
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;
|
||||
port: number;
|
||||
}
|
||||
|
||||
export interface EndpointAddress {
|
||||
hostname?: string;
|
||||
ip: string;
|
||||
nodeName?: string;
|
||||
targetRef?: ObjectReference;
|
||||
}
|
||||
|
||||
export interface EndpointSubset {
|
||||
addresses?: EndpointAddress[];
|
||||
notReadyAddresses?: EndpointAddress[];
|
||||
ports?: EndpointPort[];
|
||||
}
|
||||
|
||||
export interface EndpointsData extends KubeJsonApiData<KubeObjectMetadata<KubeObjectScope.Namespace>, void, void> {
|
||||
subsets?: EndpointSubset[];
|
||||
}
|
||||
|
||||
export class Endpoints extends KubeObject<
|
||||
NamespaceScopedMetadata,
|
||||
void,
|
||||
void
|
||||
> {
|
||||
static kind = "Endpoints";
|
||||
static namespaced = true;
|
||||
static apiBase = "/api/v1/endpoints";
|
||||
|
||||
subsets?: EndpointSubset[];
|
||||
|
||||
constructor({ subsets, ...rest }: EndpointsData) {
|
||||
super(rest);
|
||||
autoBind(this);
|
||||
this.subsets = subsets;
|
||||
}
|
||||
|
||||
getEndpointSubsets(): Required<EndpointSubset>[] {
|
||||
return this.subsets?.map(({
|
||||
addresses = [],
|
||||
notReadyAddresses = [],
|
||||
ports = [],
|
||||
}) => ({
|
||||
addresses,
|
||||
notReadyAddresses,
|
||||
ports,
|
||||
})) ?? [];
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.getEndpointSubsets()
|
||||
.map(formatEndpointSubset)
|
||||
.join(", ") || "<none>";
|
||||
}
|
||||
}
|
||||
|
||||
export class EndpointsApi extends KubeApi<Endpoints, EndpointsData> {
|
||||
constructor(deps: KubeApiDependencies, opts: DerivedKubeApiOptions = {}) {
|
||||
super(deps, {
|
||||
objectConstructor: Endpoints,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { KubeEventApi } from "./events.api";
|
||||
import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
|
||||
import loggerInjectable from "../../logger.injectable";
|
||||
import maybeKubeApiInjectable from "../maybe-kube-api.injectable";
|
||||
|
||||
const kubeEventApiInjectable = getInjectable({
|
||||
id: "kube-event-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "kubeEventApi is only available in certain environments");
|
||||
|
||||
return new KubeEventApi({
|
||||
logger: di.inject(loggerInjectable),
|
||||
maybeKubeApi: di.inject(maybeKubeApiInjectable),
|
||||
});
|
||||
},
|
||||
|
||||
injectionToken: kubeApiInjectionToken,
|
||||
});
|
||||
|
||||
export default kubeEventApiInjectable;
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import moment from "moment";
|
||||
import type { KubeObjectMetadata, KubeObjectScope, ObjectReference } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { formatDuration } from "../../utils/formatDuration";
|
||||
import type { DerivedKubeApiOptions, KubeApiDependencies } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
|
||||
export interface EventSeries {
|
||||
count?: number;
|
||||
lastObservedTime?: string;
|
||||
}
|
||||
|
||||
export interface EventSource {
|
||||
component?: string;
|
||||
host?: string;
|
||||
}
|
||||
|
||||
export interface KubeEventData extends KubeJsonApiData<KubeObjectMetadata<KubeObjectScope.Namespace>, 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<KubeObjectMetadata<KubeObjectScope.Namespace>, void, void> {
|
||||
static kind = "Event";
|
||||
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() {
|
||||
if (!this.source?.component) {
|
||||
return "<unknown>";
|
||||
}
|
||||
|
||||
const { component, host = "" } = this.source;
|
||||
|
||||
return `${component} ${host}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This function is not reactive to changing of time. If rendering use `<ReactiveDuration />` instead
|
||||
*/
|
||||
getFirstSeenTime() {
|
||||
const diff = moment().diff(this.firstTimestamp);
|
||||
|
||||
return formatDuration(diff, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This function is not reactive to changing of time. If rendering use `<ReactiveDuration />` instead
|
||||
*/
|
||||
getLastSeenTime() {
|
||||
const diff = moment().diff(this.lastTimestamp);
|
||||
|
||||
return formatDuration(diff, true);
|
||||
}
|
||||
}
|
||||
|
||||
export class KubeEventApi extends KubeApi<KubeEvent, KubeEventData> {
|
||||
constructor(deps: KubeApiDependencies, opts: DerivedKubeApiOptions = {}) {
|
||||
super(deps, {
|
||||
objectConstructor: KubeEvent,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { autoBind, bifurcateArray } from "../../utils";
|
||||
import Joi from "joi";
|
||||
|
||||
export interface RawHelmChart {
|
||||
apiVersion: string;
|
||||
name: string;
|
||||
version: string;
|
||||
repo: string;
|
||||
created: string;
|
||||
digest?: string;
|
||||
kubeVersion?: string;
|
||||
description?: string;
|
||||
home?: string;
|
||||
engine?: string;
|
||||
icon?: string;
|
||||
appVersion?: string;
|
||||
type?: string;
|
||||
tillerVersion?: string;
|
||||
deprecated?: boolean;
|
||||
keywords?: string[];
|
||||
sources?: string[];
|
||||
urls?: string[];
|
||||
maintainers?: HelmChartMaintainer[];
|
||||
dependencies?: RawHelmChartDependency[];
|
||||
annotations?: Record<string, string>;
|
||||
}
|
||||
|
||||
const helmChartMaintainerValidator = Joi.object<HelmChartMaintainer>({
|
||||
name: Joi
|
||||
.string()
|
||||
.required(),
|
||||
email: Joi
|
||||
.string()
|
||||
.required(),
|
||||
url: Joi
|
||||
.string()
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const helmChartDependencyValidator = Joi.object<HelmChartDependency, true, RawHelmChartDependency>({
|
||||
name: Joi
|
||||
.string()
|
||||
.required(),
|
||||
repository: Joi
|
||||
.string()
|
||||
.required(),
|
||||
condition: Joi
|
||||
.string()
|
||||
.optional(),
|
||||
version: Joi
|
||||
.string()
|
||||
.required(),
|
||||
tags: Joi
|
||||
.array()
|
||||
.items(Joi.string())
|
||||
.default(() => ([])),
|
||||
});
|
||||
|
||||
const helmChartValidator = Joi.object<HelmChart, true, RawHelmChart>({
|
||||
apiVersion: Joi
|
||||
.string()
|
||||
.required(),
|
||||
name: Joi
|
||||
.string()
|
||||
.required(),
|
||||
version: Joi
|
||||
.string()
|
||||
.required(),
|
||||
repo: Joi
|
||||
.string()
|
||||
.required(),
|
||||
created: Joi
|
||||
.string()
|
||||
.required(),
|
||||
digest: Joi
|
||||
.string()
|
||||
.optional(),
|
||||
kubeVersion: Joi
|
||||
.string()
|
||||
.optional(),
|
||||
description: Joi
|
||||
.string()
|
||||
.default(""),
|
||||
home: Joi
|
||||
.string()
|
||||
.optional(),
|
||||
engine: Joi
|
||||
.string()
|
||||
.optional(),
|
||||
icon: Joi
|
||||
.string()
|
||||
.optional(),
|
||||
appVersion: Joi
|
||||
.string()
|
||||
.optional(),
|
||||
tillerVersion: Joi
|
||||
.string()
|
||||
.optional(),
|
||||
type: Joi
|
||||
.string()
|
||||
.optional(),
|
||||
deprecated: Joi
|
||||
.boolean()
|
||||
.default(false),
|
||||
keywords: Joi
|
||||
.array()
|
||||
.items(Joi.string())
|
||||
.options({
|
||||
stripUnknown: {
|
||||
arrays: true,
|
||||
},
|
||||
})
|
||||
.default(() => ([])),
|
||||
sources: Joi
|
||||
.array()
|
||||
.items(Joi.string())
|
||||
.options({
|
||||
stripUnknown: {
|
||||
arrays: true,
|
||||
},
|
||||
})
|
||||
.default(() => ([])),
|
||||
urls: Joi
|
||||
.array()
|
||||
.items(Joi.string())
|
||||
.options({
|
||||
stripUnknown: {
|
||||
arrays: true,
|
||||
},
|
||||
})
|
||||
.default(() => ([])),
|
||||
maintainers: Joi
|
||||
.array()
|
||||
.items(helmChartMaintainerValidator)
|
||||
.options({
|
||||
stripUnknown: {
|
||||
arrays: true,
|
||||
},
|
||||
})
|
||||
.default(() => ([])),
|
||||
dependencies: Joi
|
||||
.array()
|
||||
.items(helmChartDependencyValidator)
|
||||
.options({
|
||||
stripUnknown: {
|
||||
arrays: true,
|
||||
},
|
||||
})
|
||||
.default(() => ([])),
|
||||
annotations: Joi
|
||||
.object({})
|
||||
.pattern(/.*/, Joi.string())
|
||||
.default(() => ({})),
|
||||
});
|
||||
|
||||
export interface HelmChartCreateOpts {
|
||||
onError?: "throw" | "log";
|
||||
}
|
||||
|
||||
export interface HelmChartMaintainer {
|
||||
name: string;
|
||||
email: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export interface RawHelmChartDependency {
|
||||
name: string;
|
||||
repository: string;
|
||||
condition?: string;
|
||||
version: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export type HelmChartDependency = Required<Omit<RawHelmChartDependency, "condition">>
|
||||
& Pick<RawHelmChartDependency, "condition">;
|
||||
|
||||
export interface 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;
|
||||
}
|
||||
|
||||
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;
|
||||
this.repo = value.repo;
|
||||
this.kubeVersion = value.kubeVersion;
|
||||
this.created = value.created;
|
||||
this.description = value.description;
|
||||
this.digest = value.digest;
|
||||
this.keywords = value.keywords;
|
||||
this.home = value.home;
|
||||
this.sources = value.sources;
|
||||
this.maintainers = value.maintainers;
|
||||
this.engine = value.engine;
|
||||
this.icon = value.icon;
|
||||
this.apiVersion = value.apiVersion;
|
||||
this.deprecated = value.deprecated;
|
||||
this.tillerVersion = value.tillerVersion;
|
||||
this.annotations = value.annotations;
|
||||
this.urls = value.urls;
|
||||
this.dependencies = value.dependencies;
|
||||
this.type = value.type;
|
||||
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
static create(data: RawHelmChart): HelmChart;
|
||||
static create(data: RawHelmChart, opts?: HelmChartCreateOpts): HelmChart | undefined;
|
||||
static create(data: RawHelmChart, { onError = "throw" }: HelmChartCreateOpts = {}): HelmChart | undefined {
|
||||
const result = helmChartValidator.validate(data, {
|
||||
abortEarly: false,
|
||||
});
|
||||
|
||||
if (!result.error) {
|
||||
return new HelmChart(result.value);
|
||||
}
|
||||
|
||||
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(result.value as unknown as HelmChartData);
|
||||
}
|
||||
|
||||
const validationError = new Joi.ValidationError(actualErrors.map(er => er.message).join(". "), actualErrors, result.error._original);
|
||||
|
||||
if (onError === "throw") {
|
||||
throw validationError;
|
||||
}
|
||||
|
||||
console.warn("[HELM-CHART]: failed to validate data", data, validationError);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
const digestPart = this.digest
|
||||
? `+${this.digest}`
|
||||
: "";
|
||||
|
||||
return `${this.repo}:${this.apiVersion}/${this.name}@${this.getAppVersion()}${digestPart}`;
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
getFullName(seperator = "/"): string {
|
||||
return [this.getRepository(), this.getName()].join(seperator);
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
getIcon(): string | undefined {
|
||||
return this.icon;
|
||||
}
|
||||
|
||||
getHome(): string | undefined {
|
||||
return this.home;
|
||||
}
|
||||
|
||||
getMaintainers(): HelmChartMaintainer[] {
|
||||
return this.maintainers;
|
||||
}
|
||||
|
||||
getVersion(): string {
|
||||
return this.version;
|
||||
}
|
||||
|
||||
getRepository(): string {
|
||||
return this.repo;
|
||||
}
|
||||
|
||||
getAppVersion(): string | undefined {
|
||||
return this.appVersion;
|
||||
}
|
||||
|
||||
getKeywords(): string[] {
|
||||
return this.keywords;
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 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 { RawHelmChart } from "../helm-charts.api";
|
||||
import { HelmChart } from "../helm-charts.api";
|
||||
import { isDefined } from "../../../utils";
|
||||
import apiBaseInjectable from "../../api-base.injectable";
|
||||
|
||||
export type RequestHelmCharts = () => Promise<HelmChart[]>;
|
||||
export type RepoHelmChartList = Record<string, RawHelmChart[]>;
|
||||
|
||||
/**
|
||||
* Get a list of all helm charts from all saved helm repos
|
||||
*/
|
||||
const requestHelmChartsInjectable = getInjectable({
|
||||
id: "request-helm-charts",
|
||||
instantiate: (di) => {
|
||||
const apiBase = di.inject(apiBaseInjectable);
|
||||
|
||||
return async () => {
|
||||
const data = await apiBase.get<Record<string, RepoHelmChartList>>("/v2/charts");
|
||||
|
||||
return Object
|
||||
.values(data)
|
||||
.reduce((allCharts, repoCharts) => allCharts.concat(Object.values(repoCharts)), new Array<RawHelmChart[]>())
|
||||
.map(([chart]) => HelmChart.create(chart, { onError: "log" }))
|
||||
.filter(isDefined);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default requestHelmChartsInjectable;
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 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 { AsyncResult } from "../../../utils/async-result";
|
||||
import { urlBuilderFor } from "../../../utils/buildUrl";
|
||||
import apiBaseInjectable from "../../api-base.injectable";
|
||||
|
||||
const requestReadmeEndpoint = urlBuilderFor("/v2/charts/:repo/:name/readme");
|
||||
|
||||
export type RequestHelmChartReadme = (repo: string, name: string, version?: string) => Promise<AsyncResult<string>>;
|
||||
|
||||
const requestHelmChartReadmeInjectable = getInjectable({
|
||||
id: "request-helm-chart-readme",
|
||||
instantiate: (di): RequestHelmChartReadme => {
|
||||
const apiBase = di.inject(apiBaseInjectable);
|
||||
|
||||
return (repo, name, version) => (
|
||||
apiBase.get(requestReadmeEndpoint.compile({ name, repo }, { version }))
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default requestHelmChartReadmeInjectable;
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 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 { AsyncResult } from "../../../utils/async-result";
|
||||
import { urlBuilderFor } from "../../../utils/buildUrl";
|
||||
import apiBaseInjectable from "../../api-base.injectable";
|
||||
|
||||
const requestValuesEndpoint = urlBuilderFor("/v2/charts/:repo/:name/values");
|
||||
|
||||
export type RequestHelmChartValues = (repo: string, name: string, version: string) => Promise<AsyncResult<string>>;
|
||||
|
||||
const requestHelmChartValuesInjectable = getInjectable({
|
||||
id: "request-helm-chart-values",
|
||||
instantiate: (di): RequestHelmChartValues => {
|
||||
const apiBase = di.inject(apiBaseInjectable);
|
||||
|
||||
return (repo, name, version) => (
|
||||
apiBase.get(requestValuesEndpoint.compile({ repo, name }, { version }))
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default requestHelmChartValuesInjectable;
|
||||
+31
@@ -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 } from "@ogre-tools/injectable";
|
||||
import { urlBuilderFor } from "../../../utils/buildUrl";
|
||||
import { HelmChart } from "../helm-charts.api";
|
||||
import type { RawHelmChart } from "../helm-charts.api";
|
||||
import { isDefined } from "../../../utils";
|
||||
import apiBaseInjectable from "../../api-base.injectable";
|
||||
|
||||
const requestVersionsEndpoint = urlBuilderFor("/v2/charts/:repo/:name/versions");
|
||||
|
||||
export type RequestHelmChartVersions = (repo: string, chartName: string) => Promise<HelmChart[]>;
|
||||
|
||||
const requestHelmChartVersionsInjectable = getInjectable({
|
||||
id: "request-helm-chart-versions",
|
||||
instantiate: (di): RequestHelmChartVersions => {
|
||||
const apiBase = di.inject(apiBaseInjectable);
|
||||
|
||||
return async (repo, name) => {
|
||||
const rawVersions = await apiBase.get(requestVersionsEndpoint.compile({ name, repo })) as RawHelmChart[];
|
||||
|
||||
return rawVersions
|
||||
.map(version => HelmChart.create(version, { onError: "log" }))
|
||||
.filter(isDefined);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default requestHelmChartVersionsInjectable;
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { ItemObject } from "../../item.store";
|
||||
import type { HelmReleaseDetails } from "./helm-releases.api/request-details.injectable";
|
||||
|
||||
export interface HelmReleaseUpdateDetails {
|
||||
log: string;
|
||||
release: HelmReleaseDetails;
|
||||
}
|
||||
|
||||
export interface HelmReleaseDto {
|
||||
appVersion: string;
|
||||
name: string;
|
||||
namespace: string;
|
||||
chart: string;
|
||||
status: string;
|
||||
updated: string;
|
||||
revision: string;
|
||||
}
|
||||
|
||||
export interface HelmRelease extends HelmReleaseDto, ItemObject {
|
||||
getNs: () => string;
|
||||
getChart: (withVersion?: boolean) => string;
|
||||
getRevision: () => number;
|
||||
getStatus: () => string;
|
||||
getVersion: () => string;
|
||||
getUpdated: (humanize?: boolean, compact?: boolean) => string | number;
|
||||
getRepo: () => Promise<string>;
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { getGlobalOverride } from "../../../test-utils/get-global-override";
|
||||
import requestHelmReleaseConfigurationInjectable from "./request-configuration.injectable";
|
||||
|
||||
export default getGlobalOverride(requestHelmReleaseConfigurationInjectable, () => () => {
|
||||
throw new Error("Tried to call requestHelmReleaseConfiguration with no override");
|
||||
});
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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 { urlBuilderFor } from "../../../utils/buildUrl";
|
||||
import apiBaseInjectable from "../../api-base.injectable";
|
||||
|
||||
export type RequestHelmReleaseConfiguration = (
|
||||
name: string,
|
||||
namespace: string,
|
||||
all: boolean
|
||||
) => Promise<string>;
|
||||
|
||||
const requestConfigurationEnpoint = urlBuilderFor("/v2/releases/:namespace/:name/values");
|
||||
|
||||
const requestHelmReleaseConfigurationInjectable = getInjectable({
|
||||
id: "request-helm-release-configuration",
|
||||
|
||||
instantiate: (di): RequestHelmReleaseConfiguration => {
|
||||
const apiBase = di.inject(apiBaseInjectable);
|
||||
|
||||
return (name, namespace, all: boolean) => (
|
||||
apiBase.get(requestConfigurationEnpoint.compile({ name, namespace }, { all }))
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default requestHelmReleaseConfigurationInjectable;
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import yaml from "js-yaml";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { HelmReleaseUpdateDetails } from "../helm-releases.api";
|
||||
import { urlBuilderFor } from "../../../utils/buildUrl";
|
||||
import apiBaseInjectable from "../../api-base.injectable";
|
||||
|
||||
interface HelmReleaseCreatePayload {
|
||||
name?: string;
|
||||
repo: string;
|
||||
chart: string;
|
||||
namespace: string;
|
||||
version: string;
|
||||
values: string;
|
||||
}
|
||||
|
||||
export type RequestCreateHelmRelease = (payload: HelmReleaseCreatePayload) => Promise<HelmReleaseUpdateDetails>;
|
||||
|
||||
const requestCreateEndpoint = urlBuilderFor("/v2/releases");
|
||||
|
||||
const requestCreateHelmReleaseInjectable = getInjectable({
|
||||
id: "request-create-helm-release",
|
||||
|
||||
instantiate: (di): RequestCreateHelmRelease => {
|
||||
const apiBase = di.inject(apiBaseInjectable);
|
||||
|
||||
return ({ repo, chart, values, ...data }) => {
|
||||
return apiBase.post(requestCreateEndpoint.compile({}), {
|
||||
data: {
|
||||
chart: `${repo}/${chart}`,
|
||||
values: yaml.load(values),
|
||||
...data,
|
||||
},
|
||||
});
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default requestCreateHelmReleaseInjectable;
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 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 { urlBuilderFor } from "../../../utils/buildUrl";
|
||||
import apiBaseInjectable from "../../api-base.injectable";
|
||||
|
||||
export type RequestDeleteHelmRelease = (name: string, namespace: string) => Promise<void>;
|
||||
|
||||
const requestDeleteEndpoint = urlBuilderFor("/v2/releases/:namespace/:name");
|
||||
|
||||
const requestDeleteHelmReleaseInjectable = getInjectable({
|
||||
id: "request-delete-helm-release",
|
||||
instantiate: (di): RequestDeleteHelmRelease => {
|
||||
const apiBase = di.inject(apiBaseInjectable);
|
||||
|
||||
return (name, namespace) => apiBase.del(requestDeleteEndpoint.compile({ name, namespace }));
|
||||
},
|
||||
});
|
||||
|
||||
export default requestDeleteHelmReleaseInjectable;
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 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 { KubeJsonApiData } from "../../kube-json-api";
|
||||
import { urlBuilderFor } from "../../../utils/buildUrl";
|
||||
import apiBaseInjectable from "../../api-base.injectable";
|
||||
|
||||
export interface HelmReleaseDetails {
|
||||
resources: KubeJsonApiData[];
|
||||
name: string;
|
||||
namespace: string;
|
||||
version: string;
|
||||
config: string; // release values
|
||||
manifest: string;
|
||||
info: {
|
||||
deleted: string;
|
||||
description: string;
|
||||
first_deployed: string;
|
||||
last_deployed: string;
|
||||
notes: string;
|
||||
status: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type CallForHelmReleaseDetails = (name: string, namespace: string) => Promise<HelmReleaseDetails>;
|
||||
|
||||
const requestDetailsEnpoint = urlBuilderFor("/v2/releases/:namespace/:name");
|
||||
|
||||
const requestHelmReleaseDetailsInjectable = getInjectable({
|
||||
id: "call-for-helm-release-details",
|
||||
|
||||
instantiate: (di): CallForHelmReleaseDetails => {
|
||||
const apiBase = di.inject(apiBaseInjectable);
|
||||
|
||||
return (name, namespace) => apiBase.get(requestDetailsEnpoint.compile({ name, namespace }));
|
||||
},
|
||||
});
|
||||
|
||||
export default requestHelmReleaseDetailsInjectable;
|
||||
+31
@@ -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 } from "@ogre-tools/injectable";
|
||||
import { urlBuilderFor } from "../../../utils/buildUrl";
|
||||
import apiBaseInjectable from "../../api-base.injectable";
|
||||
|
||||
export interface HelmReleaseRevision {
|
||||
revision: number;
|
||||
updated: string;
|
||||
status: string;
|
||||
chart: string;
|
||||
app_version: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export type RequestHelmReleaseHistory = (name: string, namespace: string) => Promise<HelmReleaseRevision[]>;
|
||||
|
||||
const requestHistoryEnpoint = urlBuilderFor("/v2/releases/:namespace/:name/history");
|
||||
|
||||
const requestHelmReleaseHistoryInjectable = getInjectable({
|
||||
id: "request-helm-release-history",
|
||||
instantiate: (di): RequestHelmReleaseHistory => {
|
||||
const apiBase = di.inject(apiBaseInjectable);
|
||||
|
||||
return (name, namespace) => apiBase.get(requestHistoryEnpoint.compile({ name, namespace }));
|
||||
},
|
||||
});
|
||||
|
||||
export default requestHelmReleaseHistoryInjectable;
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 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 { urlBuilderFor } from "../../../utils/buildUrl";
|
||||
import apiBaseInjectable from "../../api-base.injectable";
|
||||
import type { HelmReleaseDto } from "../helm-releases.api";
|
||||
|
||||
export type RequestHelmReleases = (namespace?: string) => Promise<HelmReleaseDto[]>;
|
||||
|
||||
const requestHelmReleasesEndpoint = urlBuilderFor("/v2/releases/:namespace?");
|
||||
|
||||
const requestHelmReleasesInjectable = getInjectable({
|
||||
id: "request-helm-releases",
|
||||
|
||||
instantiate: (di): RequestHelmReleases => {
|
||||
const apiBase = di.inject(apiBaseInjectable);
|
||||
|
||||
return (namespace) => apiBase.get(requestHelmReleasesEndpoint.compile({ namespace }));
|
||||
},
|
||||
});
|
||||
|
||||
export default requestHelmReleasesInjectable;
|
||||
+27
@@ -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 { urlBuilderFor } from "../../../utils/buildUrl";
|
||||
import apiBaseInjectable from "../../api-base.injectable";
|
||||
|
||||
export type RequestHelmReleaseRollback = (name: string, namespace: string, revision: number) => Promise<void>;
|
||||
|
||||
const requestRollbackEndpoint = urlBuilderFor("/v2/releases/:namespace/:name");
|
||||
|
||||
const requestHelmReleaseRollbackInjectable = getInjectable({
|
||||
id: "request-helm-release-rollback",
|
||||
instantiate: (di): RequestHelmReleaseRollback => {
|
||||
const apiBase = di.inject(apiBaseInjectable);
|
||||
|
||||
return async (name, namespace, revision) => {
|
||||
await apiBase.put(
|
||||
requestRollbackEndpoint.compile({ name, namespace }),
|
||||
{ data: { revision }},
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default requestHelmReleaseRollbackInjectable;
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 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 { urlBuilderFor } from "../../../utils/buildUrl";
|
||||
import type { AsyncResult } from "../../../utils/async-result";
|
||||
import apiBaseInjectable from "../../api-base.injectable";
|
||||
|
||||
interface HelmReleaseUpdatePayload {
|
||||
repo: string;
|
||||
chart: string;
|
||||
version: string;
|
||||
values: string;
|
||||
}
|
||||
|
||||
export type RequestHelmReleaseUpdate = (
|
||||
name: string,
|
||||
namespace: string,
|
||||
payload: HelmReleaseUpdatePayload
|
||||
) => Promise<AsyncResult<void, unknown>>;
|
||||
|
||||
const requestUpdateEndpoint = urlBuilderFor("/v2/releases/:namespace/:name");
|
||||
|
||||
const requestHelmReleaseUpdateInjectable = getInjectable({
|
||||
id: "request-helm-release-update",
|
||||
|
||||
instantiate: (di): RequestHelmReleaseUpdate => {
|
||||
const apiBase = di.inject(apiBaseInjectable);
|
||||
|
||||
return async (name, namespace, { repo, chart, values, version }) => {
|
||||
try {
|
||||
await apiBase.put(requestUpdateEndpoint.compile({ name, namespace }), {
|
||||
data: {
|
||||
chart: `${repo}/${chart}`,
|
||||
values,
|
||||
version,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
return { callWasSuccessful: false, error: e };
|
||||
}
|
||||
|
||||
return { callWasSuccessful: true };
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default requestHelmReleaseUpdateInjectable;
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 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 { urlBuilderFor } from "../../../utils/buildUrl";
|
||||
import apiBaseInjectable from "../../api-base.injectable";
|
||||
|
||||
export type RequestHelmReleaseValues = (name: string, namespace: string, all?: boolean) => Promise<string>;
|
||||
|
||||
const requestValuesEndpoint = urlBuilderFor("/v2/release/:namespace/:name/values");
|
||||
|
||||
const requestHelmReleaseValuesInjectable = getInjectable({
|
||||
id: "request-helm-release-values",
|
||||
instantiate: (di): RequestHelmReleaseValues => {
|
||||
const apiBase = di.inject(apiBaseInjectable);
|
||||
|
||||
return (name, namespace, all) => apiBase.get(requestValuesEndpoint.compile({ name, namespace }, { all }));
|
||||
},
|
||||
});
|
||||
|
||||
export default requestHelmReleaseValuesInjectable;
|
||||
+27
@@ -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 assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { HorizontalPodAutoscalerApi } from "./horizontal-pod-autoscaler.api";
|
||||
import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
|
||||
import loggerInjectable from "../../logger.injectable";
|
||||
import maybeKubeApiInjectable from "../maybe-kube-api.injectable";
|
||||
|
||||
const horizontalPodAutoscalerApiInjectable = getInjectable({
|
||||
id: "horizontal-pod-autoscaler-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "horizontalPodAutoscalerApi is only available in certain environments");
|
||||
|
||||
return new HorizontalPodAutoscalerApi({
|
||||
logger: di.inject(loggerInjectable),
|
||||
maybeKubeApi: di.inject(maybeKubeApiInjectable),
|
||||
});
|
||||
},
|
||||
|
||||
injectionToken: kubeApiInjectionToken,
|
||||
});
|
||||
|
||||
export default horizontalPodAutoscalerApiInjectable;
|
||||
@@ -0,0 +1,379 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { OptionVarient } from "../../utils";
|
||||
import type { DerivedKubeApiOptions, KubeApiDependencies } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { BaseKubeObjectCondition, LabelSelector, NamespaceScopedMetadata } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
|
||||
export enum HpaMetricType {
|
||||
Resource = "Resource",
|
||||
Pods = "Pods",
|
||||
Object = "Object",
|
||||
External = "External",
|
||||
ContainerResource = "ContainerResource",
|
||||
}
|
||||
|
||||
export interface MetricCurrentTarget {
|
||||
current?: string;
|
||||
target?: string;
|
||||
}
|
||||
|
||||
export interface HorizontalPodAutoscalerMetricTarget {
|
||||
kind: string;
|
||||
name: string;
|
||||
apiVersion: string;
|
||||
}
|
||||
|
||||
export interface V2ContainerResourceMetricSource {
|
||||
container: string;
|
||||
name: string;
|
||||
target?: {
|
||||
averageUtilization?: number;
|
||||
averageValue?: string;
|
||||
type?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface V2Beta1ContainerResourceMetricSource {
|
||||
container: string;
|
||||
name: string;
|
||||
targetAverageUtilization?: number;
|
||||
targetAverageValue?: string;
|
||||
}
|
||||
|
||||
export type ContainerResourceMetricSource =
|
||||
| V2ContainerResourceMetricSource
|
||||
| V2Beta1ContainerResourceMetricSource;
|
||||
|
||||
export interface V2ExternalMetricSource {
|
||||
metricName?: string;
|
||||
metricSelector?: LabelSelector;
|
||||
metric?: {
|
||||
name?: string;
|
||||
selector?: LabelSelector;
|
||||
};
|
||||
target?: {
|
||||
type: string;
|
||||
value?: string;
|
||||
averageValue?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface V2Beta1ExternalMetricSource {
|
||||
metricName?: string;
|
||||
metricSelector?: LabelSelector;
|
||||
targetAverageValue?: string;
|
||||
targetValue?: string;
|
||||
metric?: {
|
||||
selector?: LabelSelector;
|
||||
};
|
||||
}
|
||||
|
||||
export type ExternalMetricSource =
|
||||
| V2Beta1ExternalMetricSource
|
||||
| V2ExternalMetricSource;
|
||||
|
||||
export interface V2ObjectMetricSource {
|
||||
metric?: {
|
||||
name?: string;
|
||||
selector?: LabelSelector;
|
||||
};
|
||||
target?: {
|
||||
type?: string;
|
||||
value?: string;
|
||||
averageValue?: string;
|
||||
};
|
||||
describedObject?: CrossVersionObjectReference;
|
||||
}
|
||||
|
||||
export interface V2Beta1ObjectMetricSource {
|
||||
averageValue?: string;
|
||||
metricName?: string;
|
||||
selector?: LabelSelector;
|
||||
targetValue?: string;
|
||||
describedObject?: CrossVersionObjectReference;
|
||||
}
|
||||
|
||||
export type ObjectMetricSource =
|
||||
| V2ObjectMetricSource
|
||||
| V2Beta1ObjectMetricSource;
|
||||
|
||||
export interface V2PodsMetricSource {
|
||||
metric?: {
|
||||
name?: string;
|
||||
selector?: LabelSelector;
|
||||
};
|
||||
target?: {
|
||||
averageValue?: string;
|
||||
type?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface V2Beta1PodsMetricSource {
|
||||
metricName?: string;
|
||||
selector?: LabelSelector;
|
||||
targetAverageValue?: string;
|
||||
}
|
||||
|
||||
export type PodsMetricSource =
|
||||
| V2PodsMetricSource
|
||||
| V2Beta1PodsMetricSource;
|
||||
|
||||
export interface V2ResourceMetricSource {
|
||||
name: string;
|
||||
target?: {
|
||||
averageUtilization?: number;
|
||||
averageValue?: string;
|
||||
type?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface V2Beta1ResourceMetricSource {
|
||||
name: string;
|
||||
targetAverageUtilization?: number;
|
||||
targetAverageValue?: string;
|
||||
}
|
||||
|
||||
export type ResourceMetricSource =
|
||||
| V2ResourceMetricSource
|
||||
| V2Beta1ResourceMetricSource;
|
||||
|
||||
export interface BaseHorizontalPodAutoscalerMetricSpec {
|
||||
containerResource: ContainerResourceMetricSource;
|
||||
external: ExternalMetricSource;
|
||||
object: ObjectMetricSource;
|
||||
pods: PodsMetricSource;
|
||||
resource: ResourceMetricSource;
|
||||
}
|
||||
|
||||
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">;
|
||||
|
||||
interface HorizontalPodAutoscalerBehavior {
|
||||
scaleUp?: HPAScalingRules;
|
||||
scaleDown?: HPAScalingRules;
|
||||
}
|
||||
|
||||
interface HPAScalingRules {
|
||||
stabilizationWindowSecond?: number;
|
||||
selectPolicy?: ScalingPolicySelect;
|
||||
policies?: HPAScalingPolicy[];
|
||||
}
|
||||
|
||||
type ScalingPolicySelect = string;
|
||||
|
||||
interface HPAScalingPolicy {
|
||||
type: HPAScalingPolicyType;
|
||||
value: number;
|
||||
periodSeconds: number;
|
||||
}
|
||||
|
||||
type HPAScalingPolicyType = string;
|
||||
|
||||
export interface V2ContainerResourceMetricStatus {
|
||||
container?: string;
|
||||
name: string;
|
||||
current?: {
|
||||
averageUtilization?: number;
|
||||
averageValue?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface V2Beta1ContainerResourceMetricStatus {
|
||||
container?: string;
|
||||
currentAverageUtilization?: number;
|
||||
currentAverageValue?: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type ContainerResourceMetricStatus =
|
||||
| V2ContainerResourceMetricStatus
|
||||
| V2Beta1ContainerResourceMetricStatus;
|
||||
|
||||
export interface V2ExternalMetricStatus {
|
||||
metric?: {
|
||||
name?: string;
|
||||
selector?: LabelSelector;
|
||||
};
|
||||
current?: {
|
||||
averageValue?: string;
|
||||
value?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface V2Beta1ExternalMetricStatus {
|
||||
currentAverageValue?: string;
|
||||
currentValue?: string;
|
||||
metricName?: string;
|
||||
metricSelector?: LabelSelector;
|
||||
}
|
||||
|
||||
export type ExternalMetricStatus =
|
||||
| V2Beta1ExternalMetricStatus
|
||||
| V2ExternalMetricStatus;
|
||||
|
||||
export interface V2ObjectMetricStatus {
|
||||
metric?: {
|
||||
name?: string;
|
||||
selector?: LabelSelector;
|
||||
};
|
||||
current?: {
|
||||
type?: string;
|
||||
value?: string;
|
||||
averageValue?: string;
|
||||
};
|
||||
describedObject?: CrossVersionObjectReference;
|
||||
}
|
||||
|
||||
export interface V2Beta1ObjectMetricStatus {
|
||||
averageValue?: string;
|
||||
currentValue?: string;
|
||||
metricName?: string;
|
||||
selector?: LabelSelector;
|
||||
describedObject?: CrossVersionObjectReference;
|
||||
}
|
||||
|
||||
export type ObjectMetricStatus =
|
||||
| V2Beta1ObjectMetricStatus
|
||||
| V2ObjectMetricStatus;
|
||||
|
||||
export interface V2PodsMetricStatus {
|
||||
selector?: LabelSelector;
|
||||
metric?: {
|
||||
name?: string;
|
||||
selector?: LabelSelector;
|
||||
};
|
||||
current?: {
|
||||
averageValue?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface V2Beta1PodsMetricStatus {
|
||||
currentAverageValue?: string;
|
||||
metricName?: string;
|
||||
selector?: LabelSelector;
|
||||
}
|
||||
|
||||
export type PodsMetricStatus =
|
||||
| V2Beta1PodsMetricStatus
|
||||
| V2PodsMetricStatus;
|
||||
|
||||
export interface V2ResourceMetricStatus {
|
||||
name: string;
|
||||
current?: {
|
||||
averageUtilization?: number;
|
||||
averageValue?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface V2Beta1ResourceMetricStatus {
|
||||
currentAverageUtilization?: number;
|
||||
currentAverageValue?: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type ResourceMetricStatus =
|
||||
| V2Beta1ResourceMetricStatus
|
||||
| V2ResourceMetricStatus;
|
||||
|
||||
export interface BaseHorizontalPodAutoscalerMetricStatus {
|
||||
containerResource: ContainerResourceMetricStatus;
|
||||
external: ExternalMetricStatus;
|
||||
object: ObjectMetricStatus;
|
||||
pods: PodsMetricStatus;
|
||||
resource: ResourceMetricStatus;
|
||||
}
|
||||
|
||||
export type HorizontalPodAutoscalerMetricStatus =
|
||||
| OptionVarient<HpaMetricType.Resource, BaseHorizontalPodAutoscalerMetricStatus, "resource">
|
||||
| OptionVarient<HpaMetricType.External, BaseHorizontalPodAutoscalerMetricStatus, "external">
|
||||
| OptionVarient<HpaMetricType.Object, BaseHorizontalPodAutoscalerMetricStatus, "object">
|
||||
| OptionVarient<HpaMetricType.Pods, BaseHorizontalPodAutoscalerMetricStatus, "pods">
|
||||
| OptionVarient<HpaMetricType.ContainerResource, BaseHorizontalPodAutoscalerMetricStatus, "containerResource">;
|
||||
|
||||
export interface CrossVersionObjectReference {
|
||||
kind: string;
|
||||
name: string;
|
||||
apiVersion: string;
|
||||
}
|
||||
|
||||
export interface HorizontalPodAutoscalerSpec {
|
||||
scaleTargetRef: CrossVersionObjectReference;
|
||||
minReplicas?: number;
|
||||
maxReplicas: number;
|
||||
metrics?: HorizontalPodAutoscalerMetricSpec[];
|
||||
behavior?: HorizontalPodAutoscalerBehavior;
|
||||
targetCPUUtilizationPercentage?: number; // used only in autoscaling/v1
|
||||
}
|
||||
|
||||
export interface HorizontalPodAutoscalerStatus {
|
||||
conditions?: BaseKubeObjectCondition[];
|
||||
currentReplicas: number;
|
||||
desiredReplicas: number;
|
||||
currentMetrics?: HorizontalPodAutoscalerMetricStatus[];
|
||||
currentCPUUtilizationPercentage?: number; // used only in autoscaling/v1
|
||||
}
|
||||
|
||||
export class HorizontalPodAutoscaler extends KubeObject<
|
||||
NamespaceScopedMetadata,
|
||||
HorizontalPodAutoscalerStatus,
|
||||
HorizontalPodAutoscalerSpec
|
||||
> {
|
||||
static readonly kind = "HorizontalPodAutoscaler";
|
||||
static readonly namespaced = true;
|
||||
static readonly apiBase = "/apis/autoscaling/v2/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 ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
export class HorizontalPodAutoscalerApi extends KubeApi<HorizontalPodAutoscaler> {
|
||||
constructor(deps: KubeApiDependencies, opts?: DerivedKubeApiOptions) {
|
||||
super(deps, {
|
||||
...opts ?? {},
|
||||
objectConstructor: HorizontalPodAutoscaler,
|
||||
checkPreferredVersion: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
// Kubernetes apis
|
||||
// Docs: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.10/
|
||||
|
||||
export * from "./cluster.api";
|
||||
export * from "./cluster-role.api";
|
||||
export * from "./cluster-role-binding.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 "./horizontal-pod-autoscaler.api";
|
||||
export * from "./ingress.api";
|
||||
export * from "./ingress-class.api";
|
||||
export * from "./job.api";
|
||||
export * from "./lease.api";
|
||||
export * from "./limit-range.api";
|
||||
export * from "./namespace.api";
|
||||
export * from "./network-policy.api";
|
||||
export * from "./node.api";
|
||||
export * from "./persistent-volume.api";
|
||||
export * from "./persistent-volume-claim.api";
|
||||
export * from "./pod.api";
|
||||
export * from "./pod-disruption-budget.api";
|
||||
export * from "./pod-metrics.api";
|
||||
export * from "./pod-security-policy.api";
|
||||
export * from "./priority-class.api";
|
||||
export * from "./replica-set.api";
|
||||
export * from "./resource-quota.api";
|
||||
export * from "./role.api";
|
||||
export * from "./role-binding.api";
|
||||
export * from "./runtime-class.api";
|
||||
export * from "./secret.api";
|
||||
export * from "./self-subject-rules-reviews.api";
|
||||
export * from "./service.api";
|
||||
export * from "./service-account.api";
|
||||
export * from "./stateful-set.api";
|
||||
export * from "./storage-class.api";
|
||||
export * from "./types";
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 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 { IngressClassApi } from "./ingress-class.api";
|
||||
import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
|
||||
import loggerInjectable from "../../logger.injectable";
|
||||
import maybeKubeApiInjectable from "../maybe-kube-api.injectable";
|
||||
|
||||
const ingressClassApiInjectable = getInjectable({
|
||||
id: "ingress-class-api",
|
||||
instantiate: (di) => new IngressClassApi({
|
||||
logger: di.inject(loggerInjectable),
|
||||
maybeKubeApi: di.inject(maybeKubeApiInjectable),
|
||||
}),
|
||||
|
||||
injectionToken: kubeApiInjectionToken,
|
||||
});
|
||||
|
||||
export default ingressClassApiInjectable;
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* 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 { KubeApiDependencies, ResourceDescriptor } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
|
||||
export class IngressClassApi extends KubeApi<IngressClass> {
|
||||
constructor(dependencies: KubeApiDependencies) {
|
||||
super(dependencies, {
|
||||
objectConstructor: IngressClass,
|
||||
checkPreferredVersion: true,
|
||||
fallbackApiBases: ["/apis/extensions/v1beta1/ingressclasses"],
|
||||
});
|
||||
}
|
||||
|
||||
setAsDefault({ name }: ResourceDescriptor, isDefault = true) {
|
||||
const reqUrl = this.formatUrlForNotListing({ name });
|
||||
|
||||
return this.request.patch(reqUrl, {
|
||||
data: {
|
||||
metadata: {
|
||||
annotations: {
|
||||
[IngressClass.ANNOTATION_IS_DEFAULT]: JSON.stringify(isDefault),
|
||||
},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
headers: {
|
||||
"content-type": "application/strategic-merge-patch+json",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// API docs: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#ingressclass-v1-networking-k8s-io
|
||||
export type IngressClassMetadata = KubeObjectMetadata<KubeObjectScope.Cluster> & {
|
||||
"name": string;
|
||||
"labels"?: {
|
||||
[name: string]: string | undefined;
|
||||
"app.kubernetes.io/component"?: "controller";
|
||||
};
|
||||
"annotations"?: {
|
||||
[name: string]: string | undefined;
|
||||
"ingressclass.kubernetes.io/is-default-class"?: "true";
|
||||
};
|
||||
};
|
||||
|
||||
export interface IngressClassParametersReference {
|
||||
"apiGroup": string; // k8s.example.net
|
||||
"scope": "Namespace" | "Cluster";
|
||||
"kind": "ClusterIngressParameter" | "IngressParameter";
|
||||
"name": string; // external-config-1
|
||||
"namespace"?: string; // namespaced for IngressClass must be defined in `spec.parameters.namespace` instead of `metadata.namespace` (!)
|
||||
}
|
||||
|
||||
export interface IngressClassSpec {
|
||||
controller: string; // "example.com/ingress-controller"
|
||||
parameters?: IngressClassParametersReference;
|
||||
}
|
||||
|
||||
export interface IngressClassStatus {
|
||||
}
|
||||
|
||||
export class IngressClass extends KubeObject<IngressClassMetadata, IngressClassStatus, IngressClassSpec> {
|
||||
static readonly kind = "IngressClass";
|
||||
static readonly namespaced = false;
|
||||
static readonly apiBase = "/apis/networking.k8s.io/v1/ingressclasses";
|
||||
static readonly ANNOTATION_IS_DEFAULT = "ingressclass.kubernetes.io/is-default-class";
|
||||
|
||||
getController(): string {
|
||||
return this.spec.controller;
|
||||
}
|
||||
|
||||
getCtrlApiGroup() {
|
||||
return this.spec?.parameters?.apiGroup;
|
||||
}
|
||||
|
||||
getCtrlScope() {
|
||||
return this.spec?.parameters?.scope;
|
||||
}
|
||||
|
||||
getCtrlNs() {
|
||||
return this.spec?.parameters?.namespace;
|
||||
}
|
||||
|
||||
getCtrlKind() {
|
||||
return this.spec?.parameters?.kind;
|
||||
}
|
||||
|
||||
getCtrlName() {
|
||||
return this.spec?.parameters?.name as string;
|
||||
}
|
||||
|
||||
get isDefault() {
|
||||
return this.metadata.annotations?.[IngressClass.ANNOTATION_IS_DEFAULT] === "true";
|
||||
}
|
||||
}
|
||||
@@ -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 assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { IngressApi } from "./ingress.api";
|
||||
import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
|
||||
import loggerInjectable from "../../logger.injectable";
|
||||
import maybeKubeApiInjectable from "../maybe-kube-api.injectable";
|
||||
|
||||
const ingressApiInjectable = getInjectable({
|
||||
id: "ingress-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "ingressApi is only available in certain environments");
|
||||
|
||||
return new IngressApi({
|
||||
logger: di.inject(loggerInjectable),
|
||||
maybeKubeApi: di.inject(maybeKubeApiInjectable),
|
||||
});
|
||||
},
|
||||
|
||||
injectionToken: kubeApiInjectionToken,
|
||||
});
|
||||
|
||||
export default ingressApiInjectable;
|
||||
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { NamespaceScopedMetadata, TypedLocalObjectReference } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { hasTypedProperty, isString, iter } from "../../utils";
|
||||
import type { DerivedKubeApiOptions, KubeApiDependencies } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { RequireExactlyOne } from "type-fest";
|
||||
|
||||
export class IngressApi extends KubeApi<Ingress> {
|
||||
constructor(deps: KubeApiDependencies, opts?: DerivedKubeApiOptions) {
|
||||
super(deps, {
|
||||
...opts ?? {},
|
||||
objectConstructor: Ingress,
|
||||
// Add fallback for Kubernetes <1.19
|
||||
checkPreferredVersion: true,
|
||||
fallbackApiBases: ["/apis/extensions/v1beta1/ingresses"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export interface ILoadBalancerIngress {
|
||||
hostname?: string;
|
||||
ip?: string;
|
||||
}
|
||||
|
||||
// extensions/v1beta1
|
||||
export interface ExtensionsBackend {
|
||||
serviceName?: string;
|
||||
servicePort?: number | string;
|
||||
}
|
||||
|
||||
// networking.k8s.io/v1
|
||||
export interface NetworkingBackend {
|
||||
service?: IngressService;
|
||||
}
|
||||
|
||||
export type IngressBackend = (ExtensionsBackend | NetworkingBackend) & {
|
||||
resource?: TypedLocalObjectReference;
|
||||
};
|
||||
|
||||
export interface IngressService {
|
||||
name: string;
|
||||
port: RequireExactlyOne<{
|
||||
name: string;
|
||||
number: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
function isExtensionsBackend(backend: IngressBackend): backend is ExtensionsBackend {
|
||||
return hasTypedProperty(backend, "serviceName", isString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format an ingress backend into the name of the service and port
|
||||
* @param backend The ingress target
|
||||
*/
|
||||
export function getBackendServiceNamePort(backend: IngressBackend | undefined): string {
|
||||
if (!backend) {
|
||||
return "<unknown>";
|
||||
}
|
||||
|
||||
if (isExtensionsBackend(backend)) {
|
||||
return `${backend.serviceName}:${backend.servicePort}`;
|
||||
}
|
||||
|
||||
if (backend.service) {
|
||||
const { name, port } = backend.service;
|
||||
|
||||
return `${name}:${port.number ?? port.name}`;
|
||||
}
|
||||
|
||||
return "<unknown>";
|
||||
}
|
||||
|
||||
export interface HTTPIngressPath {
|
||||
pathType: "Exact" | "Prefix" | "ImplementationSpecific";
|
||||
path?: string;
|
||||
backend?: IngressBackend;
|
||||
}
|
||||
|
||||
export interface HTTPIngressRuleValue {
|
||||
paths: HTTPIngressPath[];
|
||||
}
|
||||
|
||||
export interface IngressRule {
|
||||
host?: string;
|
||||
http?: HTTPIngressRuleValue;
|
||||
}
|
||||
|
||||
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 class Ingress extends KubeObject<
|
||||
NamespaceScopedMetadata,
|
||||
IngressStatus,
|
||||
IngressSpec
|
||||
> {
|
||||
static readonly kind = "Ingress";
|
||||
static readonly namespaced = true;
|
||||
static readonly apiBase = "/apis/networking.k8s.io/v1/ingresses";
|
||||
|
||||
getRules() {
|
||||
return this.spec.rules ?? [];
|
||||
}
|
||||
|
||||
getRoutes(): string[] {
|
||||
return computeRouteDeclarations(this).map(({ url, service }) => `${url} ⇢ ${service}`);
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
getHosts() {
|
||||
const { spec: { rules = [] }} = this;
|
||||
|
||||
return [...iter.filterMap(rules, rule => rule.host)];
|
||||
}
|
||||
|
||||
getPorts() {
|
||||
const ports: number[] = [];
|
||||
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.length > 0) {
|
||||
if (rules.some(rule => rule.http)) {
|
||||
ports.push(httpPort);
|
||||
}
|
||||
} else if (servicePort !== undefined) {
|
||||
ports.push(Number(servicePort));
|
||||
}
|
||||
|
||||
if (tls && tls.length > 0) {
|
||||
ports.push(tlsPort);
|
||||
}
|
||||
|
||||
return ports.join(", ");
|
||||
}
|
||||
|
||||
getLoadBalancers() {
|
||||
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
|
||||
? "http"
|
||||
: "https";
|
||||
|
||||
return paths.map(({ path = "/", backend }) => ({
|
||||
displayAsLink: !host.includes("*"),
|
||||
pathname: path,
|
||||
url: `${protocol}://${host}${path}`,
|
||||
service: getBackendServiceNamePort(backend),
|
||||
}));
|
||||
}
|
||||
|
||||
export function computeRouteDeclarations(ingress: Ingress): ComputedIngressRoute[] {
|
||||
return ingress.getRules().flatMap(rule => computeRuleDeclarations(ingress, rule));
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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";
|
||||
import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
|
||||
import loggerInjectable from "../../logger.injectable";
|
||||
import maybeKubeApiInjectable from "../maybe-kube-api.injectable";
|
||||
|
||||
const jobApiInjectable = getInjectable({
|
||||
id: "job-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "jobApi is only available in certain environments");
|
||||
|
||||
return new JobApi({
|
||||
logger: di.inject(loggerInjectable),
|
||||
maybeKubeApi: di.inject(maybeKubeApiInjectable),
|
||||
}, {
|
||||
checkPreferredVersion: true,
|
||||
});
|
||||
},
|
||||
|
||||
injectionToken: kubeApiInjectionToken,
|
||||
});
|
||||
|
||||
export default jobApiInjectable;
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { DerivedKubeApiOptions, KubeApiDependencies } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { PodSpec } from "./pod.api";
|
||||
import type { Container } from "./types/container";
|
||||
import type { KubeObjectStatus, LabelSelector, NamespaceScopedMetadata } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
|
||||
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?: Container[];
|
||||
restartPolicy?: string;
|
||||
terminationGracePeriodSeconds?: number;
|
||||
dnsPolicy?: string;
|
||||
serviceAccountName?: string;
|
||||
serviceAccount?: string;
|
||||
schedulerName?: string;
|
||||
}
|
||||
|
||||
export interface JobStatus extends KubeObjectStatus {
|
||||
startTime: string;
|
||||
completionTime: string;
|
||||
succeeded: number;
|
||||
}
|
||||
|
||||
export class Job extends KubeObject<
|
||||
NamespaceScopedMetadata,
|
||||
JobStatus,
|
||||
JobSpec
|
||||
> {
|
||||
static readonly kind = "Job";
|
||||
static readonly namespaced = true;
|
||||
static readonly apiBase = "/apis/batch/v1/jobs";
|
||||
|
||||
getSelectors(): string[] {
|
||||
return KubeObject.stringifyLabels(this.spec.selector?.matchLabels);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
getCompletions() {
|
||||
return this.status?.succeeded ?? 0;
|
||||
}
|
||||
|
||||
getParallelism() {
|
||||
return this.spec.parallelism;
|
||||
}
|
||||
|
||||
getCondition() {
|
||||
// Type of Job condition could be only Complete or Failed
|
||||
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.21/#jobcondition-v1-batch
|
||||
return this.status?.conditions?.find(({ status }) => status === "True");
|
||||
}
|
||||
|
||||
getImages() {
|
||||
return this.spec.template.spec.containers?.map(container => container.image) ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
export class JobApi extends KubeApi<Job> {
|
||||
constructor(deps: KubeApiDependencies, opts?: DerivedKubeApiOptions) {
|
||||
super(deps, {
|
||||
...opts ?? {},
|
||||
objectConstructor: Job,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { LeaseApi } from "./lease.api";
|
||||
import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
|
||||
import loggerInjectable from "../../logger.injectable";
|
||||
import maybeKubeApiInjectable from "../maybe-kube-api.injectable";
|
||||
|
||||
const leaseApiInjectable = getInjectable({
|
||||
id: "lease-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "leaseApi is only available in certain environments");
|
||||
|
||||
return new LeaseApi({
|
||||
logger: di.inject(loggerInjectable),
|
||||
maybeKubeApi: di.inject(maybeKubeApiInjectable),
|
||||
});
|
||||
},
|
||||
|
||||
injectionToken: kubeApiInjectionToken,
|
||||
});
|
||||
|
||||
export default leaseApiInjectable;
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { DerivedKubeApiOptions, KubeApiDependencies } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { NamespaceScopedMetadata } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
|
||||
export interface LeaseSpec {
|
||||
acquireTime?: string;
|
||||
holderIdentity: string;
|
||||
leaseDurationSeconds: number;
|
||||
leaseTransitions?: number;
|
||||
renewTime: string;
|
||||
}
|
||||
|
||||
export class Lease extends KubeObject<
|
||||
NamespaceScopedMetadata,
|
||||
void,
|
||||
LeaseSpec
|
||||
> {
|
||||
static readonly kind = "Lease";
|
||||
static readonly namespaced = true;
|
||||
static readonly apiBase = "/apis/coordination.k8s.io/v1/leases";
|
||||
|
||||
getAcquireTime(): string {
|
||||
return this.spec.acquireTime || "";
|
||||
}
|
||||
|
||||
getHolderIdentity(): string {
|
||||
return this.spec.holderIdentity;
|
||||
}
|
||||
|
||||
getLeaseDurationSeconds(): number {
|
||||
return this.spec.leaseDurationSeconds;
|
||||
}
|
||||
|
||||
getLeaseTransitions(): number | undefined {
|
||||
return this.spec.leaseTransitions;
|
||||
}
|
||||
|
||||
getRenewTime(): string {
|
||||
return this.spec.renewTime;
|
||||
}
|
||||
}
|
||||
|
||||
export class LeaseApi extends KubeApi<Lease> {
|
||||
constructor(deps: KubeApiDependencies, opts?: DerivedKubeApiOptions) {
|
||||
super(deps, {
|
||||
...opts ?? {},
|
||||
objectConstructor: Lease,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { LimitRangeApi } from "./limit-range.api";
|
||||
import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
|
||||
import loggerInjectable from "../../logger.injectable";
|
||||
import maybeKubeApiInjectable from "../maybe-kube-api.injectable";
|
||||
|
||||
const limitRangeApiInjectable = getInjectable({
|
||||
id: "limit-range-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "limitRangeApi is only available in certain environments");
|
||||
|
||||
return new LimitRangeApi({
|
||||
logger: di.inject(loggerInjectable),
|
||||
maybeKubeApi: di.inject(maybeKubeApiInjectable),
|
||||
});
|
||||
},
|
||||
|
||||
injectionToken: kubeApiInjectionToken,
|
||||
});
|
||||
|
||||
export default limitRangeApiInjectable;
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { NamespaceScopedMetadata } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { DerivedKubeApiOptions, KubeApiDependencies } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
|
||||
export enum LimitType {
|
||||
CONTAINER = "Container",
|
||||
POD = "Pod",
|
||||
PVC = "PersistentVolumeClaim",
|
||||
}
|
||||
|
||||
export enum Resource {
|
||||
MEMORY = "memory",
|
||||
CPU = "cpu",
|
||||
STORAGE = "storage",
|
||||
EPHEMERAL_STORAGE = "ephemeral-storage",
|
||||
}
|
||||
|
||||
export enum LimitPart {
|
||||
MAX = "max",
|
||||
MIN = "min",
|
||||
DEFAULT = "default",
|
||||
DEFAULT_REQUEST = "defaultRequest",
|
||||
MAX_LIMIT_REQUEST_RATIO = "maxLimitRequestRatio",
|
||||
}
|
||||
|
||||
type LimitRangeParts = Partial<Record<LimitPart, Record<string, string>>>;
|
||||
|
||||
export interface LimitRangeItem extends LimitRangeParts {
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface LimitRangeSpec {
|
||||
limits: LimitRangeItem[];
|
||||
}
|
||||
|
||||
export class LimitRange extends KubeObject<
|
||||
NamespaceScopedMetadata,
|
||||
void,
|
||||
LimitRangeSpec
|
||||
> {
|
||||
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);
|
||||
}
|
||||
|
||||
getPodLimits() {
|
||||
return this.spec.limits.filter(limit => limit.type === LimitType.POD);
|
||||
}
|
||||
|
||||
getPVCLimits() {
|
||||
return this.spec.limits.filter(limit => limit.type === LimitType.PVC);
|
||||
}
|
||||
}
|
||||
|
||||
export class LimitRangeApi extends KubeApi<LimitRange> {
|
||||
constructor(deps: KubeApiDependencies, opts?: DerivedKubeApiOptions) {
|
||||
super(deps, {
|
||||
objectConstructor: LimitRange,
|
||||
...opts ?? {},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
// Metrics api
|
||||
|
||||
import moment from "moment";
|
||||
import { isDefined, object } from "../../utils";
|
||||
|
||||
export interface MetricData {
|
||||
status: string;
|
||||
data: {
|
||||
resultType: string;
|
||||
result: MetricResult[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface MetricResult {
|
||||
metric: {
|
||||
[name: string]: string | undefined;
|
||||
instance?: string;
|
||||
node?: string;
|
||||
pod?: string;
|
||||
kubernetes?: string;
|
||||
kubernetes_node?: string;
|
||||
kubernetes_namespace?: string;
|
||||
};
|
||||
values: [number, string][];
|
||||
}
|
||||
|
||||
export function normalizeMetrics(metrics: MetricData | undefined | null, frames = 60): MetricData {
|
||||
if (!metrics?.data?.result) {
|
||||
return {
|
||||
data: {
|
||||
resultType: "",
|
||||
result: [{
|
||||
metric: {},
|
||||
values: [],
|
||||
}],
|
||||
},
|
||||
status: "",
|
||||
};
|
||||
}
|
||||
|
||||
const { result } = metrics.data;
|
||||
|
||||
if (result.length) {
|
||||
if (frames > 0) {
|
||||
// fill the gaps
|
||||
result.forEach(res => {
|
||||
if (!res.values || !res.values.length) return;
|
||||
|
||||
let now = moment().startOf("minute").subtract(1, "minute").unix();
|
||||
let timestamp = res.values[0][0];
|
||||
|
||||
while (timestamp <= now) {
|
||||
timestamp = moment.unix(timestamp).add(1, "minute").unix();
|
||||
|
||||
if (!res.values.find((value) => value[0] === timestamp)) {
|
||||
res.values.push([timestamp, "0"]);
|
||||
}
|
||||
}
|
||||
|
||||
while (res.values.length < frames) {
|
||||
const timestamp = moment.unix(res.values[0][0]).subtract(1, "minute").unix();
|
||||
|
||||
if (!res.values.find((value) => value[0] === timestamp)) {
|
||||
res.values.unshift([timestamp, "0"]);
|
||||
}
|
||||
now = timestamp;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
// always return at least empty values array
|
||||
result.push({
|
||||
metric: {},
|
||||
values: [],
|
||||
} as MetricResult);
|
||||
}
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
export function isMetricsEmpty(metrics: Partial<Record<string, MetricData>>) {
|
||||
return Object.values(metrics).every(metric => !metric?.data?.result?.length);
|
||||
}
|
||||
|
||||
export function getItemMetrics<Keys extends string>(metrics: Partial<Record<Keys, MetricData>> | null | undefined, itemName: string): Partial<Record<Keys, MetricData>> | undefined {
|
||||
if (!metrics) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const itemMetrics = { ...metrics };
|
||||
|
||||
for (const metric in metrics) {
|
||||
if (!metrics[metric]?.data?.result) {
|
||||
continue;
|
||||
}
|
||||
const results = metrics[metric]?.data.result;
|
||||
const result = results?.find(res => Object.values(res.metric)[0] == itemName);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
itemMetrics[metric]!.data.result = result ? [result] : [];
|
||||
}
|
||||
|
||||
return itemMetrics;
|
||||
}
|
||||
|
||||
export function getMetricLastPoints<Keys extends string>(metrics: Partial<Record<Keys, MetricData>>): Partial<Record<Keys, number>> {
|
||||
return object.fromEntries(
|
||||
object.entries(metrics)
|
||||
.map(([metricName, metric]) => {
|
||||
try {
|
||||
return [metricName, +metric.data.result[0].values.slice(-1)[0][1]] as const;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
})
|
||||
.filter(isDefined),
|
||||
);
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* 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 { MetricData } from "../metrics.api";
|
||||
import type { RequestMetricsParams } from "./request-metrics.injectable";
|
||||
import requestMetricsInjectable from "./request-metrics.injectable";
|
||||
|
||||
export interface ClusterMetricData {
|
||||
memoryUsage: MetricData;
|
||||
memoryRequests: MetricData;
|
||||
memoryLimits: MetricData;
|
||||
memoryCapacity: MetricData;
|
||||
memoryAllocatableCapacity: MetricData;
|
||||
cpuUsage: MetricData;
|
||||
cpuRequests: MetricData;
|
||||
cpuLimits: MetricData;
|
||||
cpuCapacity: MetricData;
|
||||
cpuAllocatableCapacity: MetricData;
|
||||
podUsage: MetricData;
|
||||
podCapacity: MetricData;
|
||||
podAllocatableCapacity: MetricData;
|
||||
fsSize: MetricData;
|
||||
fsUsage: MetricData;
|
||||
}
|
||||
|
||||
export type RequestClusterMetricsByNodeNames = (nodeNames: string[], params?: RequestMetricsParams) => Promise<ClusterMetricData>;
|
||||
|
||||
const requestClusterMetricsByNodeNamesInjectable = getInjectable({
|
||||
id: "get-cluster-metrics-by-node-names",
|
||||
instantiate: (di): RequestClusterMetricsByNodeNames => {
|
||||
const requestMetrics = di.inject(requestMetricsInjectable);
|
||||
|
||||
return (nodeNames, params) => {
|
||||
const opts = {
|
||||
category: "cluster",
|
||||
nodes: nodeNames.join("|"),
|
||||
};
|
||||
|
||||
return requestMetrics({
|
||||
memoryUsage: opts,
|
||||
workloadMemoryUsage: opts,
|
||||
memoryRequests: opts,
|
||||
memoryLimits: opts,
|
||||
memoryCapacity: opts,
|
||||
memoryAllocatableCapacity: opts,
|
||||
cpuUsage: opts,
|
||||
cpuRequests: opts,
|
||||
cpuLimits: opts,
|
||||
cpuCapacity: opts,
|
||||
cpuAllocatableCapacity: opts,
|
||||
podUsage: opts,
|
||||
podCapacity: opts,
|
||||
podAllocatableCapacity: opts,
|
||||
fsSize: opts,
|
||||
fsUsage: opts,
|
||||
}, params);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default requestClusterMetricsByNodeNamesInjectable;
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 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 { MetricData } from "../metrics.api";
|
||||
import requestMetricsInjectable from "./request-metrics.injectable";
|
||||
|
||||
export interface IngressMetricData {
|
||||
bytesSentSuccess: MetricData;
|
||||
bytesSentFailure: MetricData;
|
||||
requestDurationSeconds: MetricData;
|
||||
responseDurationSeconds: MetricData;
|
||||
}
|
||||
|
||||
export type RequestIngressMetrics = (ingress: string, namespace: string) => Promise<IngressMetricData>;
|
||||
|
||||
const requestIngressMetricsInjectable = getInjectable({
|
||||
id: "request-ingress-metrics",
|
||||
instantiate: (di): RequestIngressMetrics => {
|
||||
const requestMetrics = di.inject(requestMetricsInjectable);
|
||||
|
||||
return (ingress, namespace) => {
|
||||
const opts = { category: "ingress", ingress, namespace };
|
||||
|
||||
return requestMetrics({
|
||||
bytesSentSuccess: opts,
|
||||
bytesSentFailure: opts,
|
||||
requestDurationSeconds: opts,
|
||||
responseDurationSeconds: opts,
|
||||
}, {
|
||||
namespace,
|
||||
});
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default requestIngressMetricsInjectable;
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 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 { MetricData } from "../metrics.api";
|
||||
import requestMetricsInjectable from "./request-metrics.injectable";
|
||||
|
||||
export interface NodeMetricData {
|
||||
memoryUsage: MetricData;
|
||||
workloadMemoryUsage: MetricData;
|
||||
memoryCapacity: MetricData;
|
||||
memoryAllocatableCapacity: MetricData;
|
||||
cpuUsage: MetricData;
|
||||
cpuCapacity: MetricData;
|
||||
fsUsage: MetricData;
|
||||
fsSize: MetricData;
|
||||
}
|
||||
|
||||
export type RequestAllNodeMetrics = () => Promise<NodeMetricData>;
|
||||
|
||||
const requestAllNodeMetricsInjectable = getInjectable({
|
||||
id: "request-all-node-metrics",
|
||||
instantiate: (di): RequestAllNodeMetrics => {
|
||||
const requestMetrics = di.inject(requestMetricsInjectable);
|
||||
|
||||
return () => {
|
||||
const opts = { category: "nodes" };
|
||||
|
||||
return requestMetrics({
|
||||
memoryUsage: opts,
|
||||
workloadMemoryUsage: opts,
|
||||
memoryCapacity: opts,
|
||||
memoryAllocatableCapacity: opts,
|
||||
cpuUsage: opts,
|
||||
cpuCapacity: opts,
|
||||
fsSize: opts,
|
||||
fsUsage: opts,
|
||||
});
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default requestAllNodeMetricsInjectable;
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 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 { getSecondsFromUnixEpoch } from "../../../utils/date/get-current-date-time";
|
||||
import apiBaseInjectable from "../../api-base.injectable";
|
||||
import type { MetricData } from "../metrics.api";
|
||||
|
||||
|
||||
export interface RequestMetricsParams {
|
||||
/**
|
||||
* timestamp in seconds or valid date-string
|
||||
*/
|
||||
start?: number | string;
|
||||
|
||||
/**
|
||||
* timestamp in seconds or valid date-string
|
||||
*/
|
||||
end?: number | string;
|
||||
|
||||
/**
|
||||
* step in seconds
|
||||
* @default 60 (1 minute)
|
||||
*/
|
||||
step?: number;
|
||||
|
||||
/**
|
||||
* time-range in seconds for data aggregation
|
||||
* @default 3600 (1 hour)
|
||||
*/
|
||||
range?: number;
|
||||
|
||||
/**
|
||||
* rbac-proxy validation param
|
||||
*/
|
||||
namespace?: string;
|
||||
}
|
||||
|
||||
export interface RequestMetrics {
|
||||
(query: string, params?: RequestMetricsParams): Promise<MetricData>;
|
||||
(query: string[], params?: RequestMetricsParams): Promise<MetricData[]>;
|
||||
<Keys extends string>(query: Record<Keys, Partial<Record<string, string>>>, params?: RequestMetricsParams): Promise<Record<Keys, MetricData>>;
|
||||
}
|
||||
|
||||
const requestMetricsInjectable = getInjectable({
|
||||
id: "request-metrics",
|
||||
instantiate: (di) => {
|
||||
const apiBase = di.inject(apiBaseInjectable);
|
||||
|
||||
return (async (query: object, params: RequestMetricsParams = {}) => {
|
||||
const { range = 3600, step = 60, namespace } = params;
|
||||
let { start, end } = params;
|
||||
|
||||
if (!start && !end) {
|
||||
const now = getSecondsFromUnixEpoch();
|
||||
|
||||
start = now - range;
|
||||
end = now;
|
||||
}
|
||||
|
||||
return apiBase.post("/metrics", {
|
||||
data: query,
|
||||
query: {
|
||||
start, end, step,
|
||||
"kubernetes_namespace": namespace,
|
||||
},
|
||||
});
|
||||
}) as RequestMetrics;
|
||||
},
|
||||
});
|
||||
|
||||
export default requestMetricsInjectable;
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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 { MetricData } from "../metrics.api";
|
||||
import type { PersistentVolumeClaim } from "../persistent-volume-claim.api";
|
||||
import requestMetricsInjectable from "./request-metrics.injectable";
|
||||
|
||||
export interface PersistentVolumeClaimMetricData {
|
||||
diskUsage: MetricData;
|
||||
diskCapacity: MetricData;
|
||||
}
|
||||
|
||||
export type RequestPersistentVolumeClaimMetrics = (claim: PersistentVolumeClaim) => Promise<PersistentVolumeClaimMetricData>;
|
||||
|
||||
const requestPersistentVolumeClaimMetricsInjectable = getInjectable({
|
||||
id: "request-persistent-volume-claim-metrics",
|
||||
instantiate: (di): RequestPersistentVolumeClaimMetrics => {
|
||||
const requestMetrics = di.inject(requestMetricsInjectable);
|
||||
|
||||
return (claim) => {
|
||||
const opts = { category: "pvc", pvc: claim.getName(), namespace: claim.getNs() };
|
||||
|
||||
return requestMetrics({
|
||||
diskUsage: opts,
|
||||
diskCapacity: opts,
|
||||
}, {
|
||||
namespace: opts.namespace,
|
||||
});
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default requestPersistentVolumeClaimMetricsInjectable;
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 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 { DaemonSet } from "../daemon-set.api";
|
||||
import type { MetricData } from "../metrics.api";
|
||||
import requestMetricsInjectable from "./request-metrics.injectable";
|
||||
|
||||
export interface DaemonSetPodMetricData {
|
||||
cpuUsage: MetricData;
|
||||
memoryUsage: MetricData;
|
||||
fsUsage: MetricData;
|
||||
fsWrites: MetricData;
|
||||
fsReads: MetricData;
|
||||
networkReceive: MetricData;
|
||||
networkTransmit: MetricData;
|
||||
}
|
||||
|
||||
export type RequestPodMetricsForDaemonSets = (daemonsets: DaemonSet[], namespace: string, selector?: string) => Promise<DaemonSetPodMetricData>;
|
||||
|
||||
const requestPodMetricsForDaemonSetsInjectable = getInjectable({
|
||||
id: "request-pod-metrics-for-daemon-sets",
|
||||
instantiate: (di): RequestPodMetricsForDaemonSets => {
|
||||
const requestMetrics = di.inject(requestMetricsInjectable);
|
||||
|
||||
return (daemonSets, namespace, selector = "") => {
|
||||
const podSelector = daemonSets.map(daemonSet => `${daemonSet.getName()}-[[:alnum:]]{5}`).join("|");
|
||||
const opts = { category: "pods", pods: podSelector, namespace, selector };
|
||||
|
||||
return requestMetrics({
|
||||
cpuUsage: opts,
|
||||
memoryUsage: opts,
|
||||
fsUsage: opts,
|
||||
fsWrites: opts,
|
||||
fsReads: opts,
|
||||
networkReceive: opts,
|
||||
networkTransmit: opts,
|
||||
}, {
|
||||
namespace,
|
||||
});
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default requestPodMetricsForDaemonSetsInjectable;
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 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 { Deployment } from "../deployment.api";
|
||||
import type { MetricData } from "../metrics.api";
|
||||
import requestMetricsInjectable from "./request-metrics.injectable";
|
||||
|
||||
export interface DeploymentPodMetricData {
|
||||
cpuUsage: MetricData;
|
||||
memoryUsage: MetricData;
|
||||
fsUsage: MetricData;
|
||||
fsWrites: MetricData;
|
||||
fsReads: MetricData;
|
||||
networkReceive: MetricData;
|
||||
networkTransmit: MetricData;
|
||||
}
|
||||
|
||||
export type RequestPodMetricsForDeployments = (deployments: Deployment[], namespace: string, selector?: string) => Promise<DeploymentPodMetricData>;
|
||||
|
||||
const requestPodMetricsForDeploymentsInjectable = getInjectable({
|
||||
id: "request-pod-metrics-for-deployments",
|
||||
instantiate: (di): RequestPodMetricsForDeployments => {
|
||||
const requestMetrics = di.inject(requestMetricsInjectable);
|
||||
|
||||
return (deployments, namespace, selector = "") => {
|
||||
const podSelector = deployments.map(deployment => `${deployment.getName()}-[[:alnum:]]{9,}-[[:alnum:]]{5}`).join("|");
|
||||
const opts = { category: "pods", pods: podSelector, namespace, selector };
|
||||
|
||||
return requestMetrics({
|
||||
cpuUsage: opts,
|
||||
memoryUsage: opts,
|
||||
fsUsage: opts,
|
||||
fsWrites: opts,
|
||||
fsReads: opts,
|
||||
networkReceive: opts,
|
||||
networkTransmit: opts,
|
||||
}, {
|
||||
namespace,
|
||||
});
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default requestPodMetricsForDeploymentsInjectable;
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 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 { Job } from "../job.api";
|
||||
import type { MetricData } from "../metrics.api";
|
||||
import requestMetricsInjectable from "./request-metrics.injectable";
|
||||
|
||||
export interface JobPodMetricData {
|
||||
cpuUsage: MetricData;
|
||||
memoryUsage: MetricData;
|
||||
fsUsage: MetricData;
|
||||
fsWrites: MetricData;
|
||||
fsReads: MetricData;
|
||||
networkReceive: MetricData;
|
||||
networkTransmit: MetricData;
|
||||
}
|
||||
|
||||
export type RequestPodMetricsForJobs = (jobs: Job[], namespace: string, selector?: string) => Promise<JobPodMetricData>;
|
||||
|
||||
const requestPodMetricsForJobsInjectable = getInjectable({
|
||||
id: "request-pod-metrics-for-jobs",
|
||||
instantiate: (di): RequestPodMetricsForJobs => {
|
||||
const requestMetrics = di.inject(requestMetricsInjectable);
|
||||
|
||||
return (jobs, namespace, selector = "") => {
|
||||
const podSelector = jobs.map(job => `${job.getName()}-[[:alnum:]]{5}`).join("|");
|
||||
const opts = { category: "pods", pods: podSelector, namespace, selector };
|
||||
|
||||
return requestMetrics({
|
||||
cpuUsage: opts,
|
||||
memoryUsage: opts,
|
||||
fsUsage: opts,
|
||||
fsWrites: opts,
|
||||
fsReads: opts,
|
||||
networkReceive: opts,
|
||||
networkTransmit: opts,
|
||||
}, {
|
||||
namespace,
|
||||
});
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default requestPodMetricsForJobsInjectable;
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 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 { MetricData } from "../metrics.api";
|
||||
import type { ReplicaSet } from "../replica-set.api";
|
||||
import requestMetricsInjectable from "./request-metrics.injectable";
|
||||
|
||||
export interface ReplicaSetPodMetricData {
|
||||
cpuUsage: MetricData;
|
||||
memoryUsage: MetricData;
|
||||
fsUsage: MetricData;
|
||||
fsWrites: MetricData;
|
||||
fsReads: MetricData;
|
||||
networkReceive: MetricData;
|
||||
networkTransmit: MetricData;
|
||||
}
|
||||
|
||||
export type RequestPodMetricsForReplicaSets = (replicaSets: ReplicaSet[], namespace: string, selector?: string) => Promise<ReplicaSetPodMetricData>;
|
||||
|
||||
const requestPodMetricsForReplicaSetsInjectable = getInjectable({
|
||||
id: "request-pod-metrics-for-replica-sets",
|
||||
instantiate: (di): RequestPodMetricsForReplicaSets => {
|
||||
const requestMetrics = di.inject(requestMetricsInjectable);
|
||||
|
||||
return (replicaSets, namespace, selector = "") => {
|
||||
const podSelector = replicaSets.map(replicaSet => `${replicaSet.getName()}-[[:alnum:]]{5}`).join("|");
|
||||
const opts = { category: "pods", pods: podSelector, namespace, selector };
|
||||
|
||||
return requestMetrics({
|
||||
cpuUsage: opts,
|
||||
memoryUsage: opts,
|
||||
fsUsage: opts,
|
||||
fsWrites: opts,
|
||||
fsReads: opts,
|
||||
networkReceive: opts,
|
||||
networkTransmit: opts,
|
||||
}, {
|
||||
namespace,
|
||||
});
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default requestPodMetricsForReplicaSetsInjectable;
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 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 { MetricData } from "../metrics.api";
|
||||
import type { StatefulSet } from "../stateful-set.api";
|
||||
import requestMetricsInjectable from "./request-metrics.injectable";
|
||||
|
||||
export interface StatefulSetPodMetricData {
|
||||
cpuUsage: MetricData;
|
||||
memoryUsage: MetricData;
|
||||
fsUsage: MetricData;
|
||||
fsWrites: MetricData;
|
||||
fsReads: MetricData;
|
||||
networkReceive: MetricData;
|
||||
networkTransmit: MetricData;
|
||||
}
|
||||
|
||||
export type RequestPodMetricsForStatefulSets = (statefulSets: StatefulSet[], namespace: string, selector?: string) => Promise<StatefulSetPodMetricData>;
|
||||
|
||||
const requestPodMetricsForStatefulSetsInjectable = getInjectable({
|
||||
id: "request-pod-metrics-for-stateful-sets",
|
||||
instantiate: (di): RequestPodMetricsForStatefulSets => {
|
||||
const requestMetrics = di.inject(requestMetricsInjectable);
|
||||
|
||||
return (statefulSets, namespace, selector = "") => {
|
||||
const podSelector = statefulSets.map(statefulset => `${statefulset.getName()}-[[:digit:]]+`).join("|");
|
||||
const opts = { category: "pods", pods: podSelector, namespace, selector };
|
||||
|
||||
return requestMetrics({
|
||||
cpuUsage: opts,
|
||||
memoryUsage: opts,
|
||||
fsUsage: opts,
|
||||
fsWrites: opts,
|
||||
fsReads: opts,
|
||||
networkReceive: opts,
|
||||
networkTransmit: opts,
|
||||
}, {
|
||||
namespace,
|
||||
});
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default requestPodMetricsForStatefulSetsInjectable;
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 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 { MetricData } from "../metrics.api";
|
||||
import requestMetricsInjectable from "./request-metrics.injectable";
|
||||
|
||||
export interface PodMetricInNamespaceData {
|
||||
cpuUsage: MetricData;
|
||||
memoryUsage: MetricData;
|
||||
fsUsage: MetricData;
|
||||
fsWrites: MetricData;
|
||||
fsReads: MetricData;
|
||||
networkReceive: MetricData;
|
||||
networkTransmit: MetricData;
|
||||
}
|
||||
|
||||
export type RequestPodMetricsInNamespace = (namespace: string, selector?: string) => Promise<PodMetricInNamespaceData>;
|
||||
|
||||
const requestPodMetricsInNamespaceInjectable = getInjectable({
|
||||
id: "request-pod-metrics-in-namespace",
|
||||
instantiate: (di): RequestPodMetricsInNamespace => {
|
||||
const requestMetrics = di.inject(requestMetricsInjectable);
|
||||
|
||||
return (namespace, selector) => {
|
||||
const opts = { category: "pods", pods: ".*", namespace, selector };
|
||||
|
||||
return requestMetrics({
|
||||
cpuUsage: opts,
|
||||
memoryUsage: opts,
|
||||
fsUsage: opts,
|
||||
fsWrites: opts,
|
||||
fsReads: opts,
|
||||
networkReceive: opts,
|
||||
networkTransmit: opts,
|
||||
}, {
|
||||
namespace,
|
||||
});
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default requestPodMetricsInNamespaceInjectable;
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 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 { MetricData } from "../metrics.api";
|
||||
import type { Pod } from "../pod.api";
|
||||
import requestMetricsInjectable from "./request-metrics.injectable";
|
||||
|
||||
export interface PodMetricData {
|
||||
cpuUsage: MetricData;
|
||||
memoryUsage: MetricData;
|
||||
fsUsage: MetricData;
|
||||
fsWrites: MetricData;
|
||||
fsReads: MetricData;
|
||||
networkReceive: MetricData;
|
||||
networkTransmit: MetricData;
|
||||
cpuRequests: MetricData;
|
||||
cpuLimits: MetricData;
|
||||
memoryRequests: MetricData;
|
||||
memoryLimits: MetricData;
|
||||
}
|
||||
|
||||
export type RequestPodMetrics = (pods: Pod[], namespace: string, selector?: string) => Promise<PodMetricData>;
|
||||
|
||||
const requestPodMetricsInjectable = getInjectable({
|
||||
id: "request-pod-metrics",
|
||||
instantiate: (di): RequestPodMetrics => {
|
||||
const requestMetrics = di.inject(requestMetricsInjectable);
|
||||
|
||||
return (pods, namespace, selector = "pod, namespace") => {
|
||||
const podSelector = pods.map(pod => pod.getName()).join("|");
|
||||
const opts = { category: "pods", pods: podSelector, namespace, selector };
|
||||
|
||||
return requestMetrics({
|
||||
cpuUsage: opts,
|
||||
cpuRequests: opts,
|
||||
cpuLimits: opts,
|
||||
memoryUsage: opts,
|
||||
memoryRequests: opts,
|
||||
memoryLimits: opts,
|
||||
fsUsage: opts,
|
||||
fsWrites: opts,
|
||||
fsReads: opts,
|
||||
networkReceive: opts,
|
||||
networkTransmit: opts,
|
||||
}, {
|
||||
namespace,
|
||||
});
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default requestPodMetricsInjectable;
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 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 apiBaseInjectable from "../../api-base.injectable";
|
||||
|
||||
export interface MetricProviderInfo {
|
||||
name: string;
|
||||
id: string;
|
||||
isConfigurable: boolean;
|
||||
}
|
||||
|
||||
export type RequestMetricsProviders = () => Promise<MetricProviderInfo[]>;
|
||||
|
||||
const requestMetricsProvidersInjectable = getInjectable({
|
||||
id: "request-metrics-providers",
|
||||
instantiate: (di): RequestMetricsProviders => {
|
||||
const apiBase = di.inject(apiBaseInjectable);
|
||||
|
||||
return () => apiBase.get("/metrics/providers");
|
||||
},
|
||||
});
|
||||
|
||||
export default requestMetricsProvidersInjectable;
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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";
|
||||
import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
|
||||
import loggerInjectable from "../../logger.injectable";
|
||||
import maybeKubeApiInjectable from "../maybe-kube-api.injectable";
|
||||
|
||||
const namespaceApiInjectable = getInjectable({
|
||||
id: "namespace-api",
|
||||
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "namespaceApi is only available in certain environments");
|
||||
|
||||
return new NamespaceApi({
|
||||
logger: di.inject(loggerInjectable),
|
||||
maybeKubeApi: di.inject(maybeKubeApiInjectable),
|
||||
});
|
||||
},
|
||||
|
||||
injectionToken: kubeApiInjectionToken,
|
||||
});
|
||||
|
||||
export default namespaceApiInjectable;
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { DerivedKubeApiOptions, KubeApiDependencies } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { ClusterScopedMetadata, KubeObjectStatus } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
|
||||
export enum NamespaceStatusKind {
|
||||
ACTIVE = "Active",
|
||||
TERMINATING = "Terminating",
|
||||
}
|
||||
|
||||
export interface NamespaceSpec {
|
||||
finalizers?: string[];
|
||||
}
|
||||
|
||||
export interface NamespaceStatus extends KubeObjectStatus {
|
||||
phase?: string;
|
||||
}
|
||||
|
||||
export class Namespace extends KubeObject<
|
||||
ClusterScopedMetadata,
|
||||
NamespaceStatus,
|
||||
NamespaceSpec
|
||||
> {
|
||||
static readonly kind = "Namespace";
|
||||
static readonly namespaced = false;
|
||||
static readonly apiBase = "/api/v1/namespaces";
|
||||
|
||||
getStatus() {
|
||||
return this.status?.phase ?? "-";
|
||||
}
|
||||
}
|
||||
|
||||
export class NamespaceApi extends KubeApi<Namespace> {
|
||||
constructor(deps: KubeApiDependencies, opts?: DerivedKubeApiOptions) {
|
||||
super(deps, {
|
||||
...opts ?? {},
|
||||
objectConstructor: Namespace,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { NetworkPolicyApi } from "./network-policy.api";
|
||||
import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
|
||||
import loggerInjectable from "../../logger.injectable";
|
||||
import maybeKubeApiInjectable from "../maybe-kube-api.injectable";
|
||||
|
||||
const networkPolicyApiInjectable = getInjectable({
|
||||
id: "network-policy-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "networkPolicyApi is only available in certain environments");
|
||||
|
||||
return new NetworkPolicyApi({
|
||||
logger: di.inject(loggerInjectable),
|
||||
maybeKubeApi: di.inject(maybeKubeApiInjectable),
|
||||
});
|
||||
},
|
||||
|
||||
injectionToken: kubeApiInjectionToken,
|
||||
});
|
||||
|
||||
export default networkPolicyApiInjectable;
|
||||
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { LabelSelector, NamespaceScopedMetadata } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { DerivedKubeApiOptions, KubeApiDependencies } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
|
||||
export interface IPolicyIpBlock {
|
||||
cidr: string;
|
||||
except?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `LabelSelector` instead
|
||||
*/
|
||||
export type IPolicySelector = LabelSelector;
|
||||
|
||||
export interface NetworkPolicyPort {
|
||||
/**
|
||||
* The protocol which network traffic must match.
|
||||
*
|
||||
* One of:
|
||||
* - `"TCP"`
|
||||
* - `"UDP"`
|
||||
* - `"SCTP"`
|
||||
*
|
||||
* @default "TCP"
|
||||
*/
|
||||
protocol?: string;
|
||||
|
||||
/**
|
||||
* The port on the given protocol. This can either be a numerical or named
|
||||
* port on a pod. If this field is not provided, this matches all port names and
|
||||
* numbers.
|
||||
*
|
||||
* If present, only traffic on the specified protocol AND port will be matched.
|
||||
*/
|
||||
port?: number | string;
|
||||
|
||||
/**
|
||||
* If set, indicates that the range of ports from port to endPort, inclusive,
|
||||
* should be allowed by the policy. This field cannot be defined if the port field
|
||||
* is not defined or if the port field is defined as a named (string) port.
|
||||
*
|
||||
* The endPort must be equal or greater than port.
|
||||
*/
|
||||
endPort?: number;
|
||||
}
|
||||
|
||||
export interface NetworkPolicyPeer {
|
||||
/**
|
||||
* IPBlock defines policy on a particular IPBlock. If this field is set then
|
||||
* neither of the other fields can be.
|
||||
*/
|
||||
ipBlock?: IPolicyIpBlock;
|
||||
|
||||
/**
|
||||
* Selects Namespaces using cluster-scoped labels. This field follows standard label
|
||||
* selector semantics; if present but empty, it selects all namespaces.
|
||||
*
|
||||
* If PodSelector is also set, then the NetworkPolicyPeer as a whole selects
|
||||
* the Pods matching PodSelector in the Namespaces selected by NamespaceSelector.
|
||||
*
|
||||
* Otherwise it selects all Pods in the Namespaces selected by NamespaceSelector.
|
||||
*/
|
||||
namespaceSelector?: LabelSelector;
|
||||
|
||||
/**
|
||||
* This is a label selector which selects Pods. This field follows standard label
|
||||
* selector semantics; if present but empty, it selects all pods.
|
||||
*
|
||||
* If NamespaceSelector is also set, then the NetworkPolicyPeer as a whole selects
|
||||
* the Pods matching PodSelector in the Namespaces selected by NamespaceSelector.
|
||||
*
|
||||
* Otherwise it selects the Pods matching PodSelector in the policy's own Namespace.
|
||||
*/
|
||||
podSelector?: LabelSelector;
|
||||
}
|
||||
|
||||
export interface IPolicyIngress {
|
||||
from?: NetworkPolicyPeer[];
|
||||
ports?: NetworkPolicyPort[];
|
||||
}
|
||||
|
||||
export interface IPolicyEgress {
|
||||
to?: NetworkPolicyPeer[];
|
||||
ports?: NetworkPolicyPort[];
|
||||
}
|
||||
|
||||
export type PolicyType = "Ingress" | "Egress";
|
||||
|
||||
export interface NetworkPolicySpec {
|
||||
podSelector: LabelSelector;
|
||||
policyTypes?: PolicyType[];
|
||||
ingress?: IPolicyIngress[];
|
||||
egress?: IPolicyEgress[];
|
||||
}
|
||||
|
||||
export class NetworkPolicy extends KubeObject<
|
||||
NamespaceScopedMetadata,
|
||||
void,
|
||||
NetworkPolicySpec
|
||||
> {
|
||||
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 [];
|
||||
|
||||
return Object
|
||||
.entries(this.spec.podSelector.matchLabels)
|
||||
.map(data => data.join(":"));
|
||||
}
|
||||
|
||||
getTypes(): string[] {
|
||||
if (!this.spec.policyTypes) return [];
|
||||
|
||||
return this.spec.policyTypes;
|
||||
}
|
||||
}
|
||||
|
||||
export class NetworkPolicyApi extends KubeApi<NetworkPolicy> {
|
||||
constructor(deps: KubeApiDependencies, opts: DerivedKubeApiOptions = {}) {
|
||||
super(deps, {
|
||||
objectConstructor: NetworkPolicy,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 assert from "assert";
|
||||
import { storesAndApisCanBeCreatedInjectionToken } from "../stores-apis-can-be-created.token";
|
||||
import { NodeApi } from "./node.api";
|
||||
import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
|
||||
import loggerInjectable from "../../logger.injectable";
|
||||
import maybeKubeApiInjectable from "../maybe-kube-api.injectable";
|
||||
|
||||
const nodeApiInjectable = getInjectable({
|
||||
id: "node-api",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectionToken), "nodeApi is only available in certain environments");
|
||||
|
||||
return new NodeApi({
|
||||
logger: di.inject(loggerInjectable),
|
||||
maybeKubeApi: di.inject(maybeKubeApiInjectable),
|
||||
});
|
||||
},
|
||||
|
||||
injectionToken: kubeApiInjectionToken,
|
||||
});
|
||||
|
||||
export default nodeApiInjectable;
|
||||
Loaded 100 of 3321 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user