mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Refactor k8s api to common (#3553)
* refactor kube api to common Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * refactor more utils to common Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * refactor more utils to common Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix tests Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * switch to use node-fetch on both sides Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * cleanup Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * improve logger Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix lint errors Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix/improve tests Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix tests Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix node-fetch require error on prod build Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * preload fix Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fixes Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * read serverAddress from apiBase Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * cleanup Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
1 parent
476aa74101
commit
58a446bd45
250 files changed
+1244
-644
No files matched your search
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import orderBy from "lodash/orderBy";
|
||||
import { autoBind } from "./utils";
|
||||
import { action, computed, observable, when, makeObservable } from "mobx";
|
||||
|
||||
export interface ItemObject {
|
||||
getId(): string;
|
||||
getName(): string;
|
||||
}
|
||||
|
||||
export abstract class ItemStore<Item extends ItemObject> {
|
||||
abstract loadAll(...args: any[]): Promise<void | Item[]>;
|
||||
|
||||
protected defaultSorting = (item: Item) => item.getName();
|
||||
|
||||
@observable failedLoading = false;
|
||||
@observable isLoading = false;
|
||||
@observable isLoaded = false;
|
||||
@observable items = observable.array<Item>([], { deep: false });
|
||||
@observable selectedItemsIds = observable.map<string, boolean>();
|
||||
|
||||
constructor() {
|
||||
makeObservable(this);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
@computed get selectedItems(): Item[] {
|
||||
return this.items.filter(item => this.selectedItemsIds.get(item.getId()));
|
||||
}
|
||||
|
||||
public getItems(): Item[] {
|
||||
return Array.from(this.items);
|
||||
}
|
||||
|
||||
public getTotalCount(): number {
|
||||
return this.items.length;
|
||||
}
|
||||
|
||||
getByName(name: string, ...args: any[]): Item;
|
||||
getByName(name: string): Item {
|
||||
return this.items.find(item => item.getName() === name);
|
||||
}
|
||||
|
||||
getIndexById(id: string): number {
|
||||
return this.items.findIndex(item => item.getId() === id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return `items` sorted by the given ordering functions. If two elements of
|
||||
* `items` are sorted to the same "index" then the next sorting function is used
|
||||
* to determine where to place them relative to each other. Once the `sorting`
|
||||
* functions have been all exhausted then the order is unchanged (ie a stable sort).
|
||||
* @param items the items to be sorted (default: the current items in this store)
|
||||
* @param sorting list of functions to determine sort order (default: sorting by name)
|
||||
* @param order whether to sort from least to greatest (`"asc"` (default)) or vice-versa (`"desc"`)
|
||||
*/
|
||||
@action
|
||||
protected sortItems(items: Item[] = this.items, sorting: ((item: Item) => any)[] = [this.defaultSorting], order?: "asc" | "desc"): Item[] {
|
||||
return orderBy(items, sorting, order);
|
||||
}
|
||||
|
||||
protected async createItem(...args: any[]): Promise<any>;
|
||||
@action
|
||||
protected async createItem(request: () => Promise<Item>) {
|
||||
const newItem = await request();
|
||||
const item = this.items.find(item => item.getId() === newItem.getId());
|
||||
|
||||
if (item) {
|
||||
return item;
|
||||
} else {
|
||||
const items = this.sortItems([...this.items, newItem]);
|
||||
|
||||
this.items.replace(items);
|
||||
|
||||
return newItem;
|
||||
}
|
||||
}
|
||||
|
||||
protected async loadItems(...args: any[]): Promise<any>;
|
||||
@action
|
||||
protected async loadItems(request: () => Promise<Item[] | any>, sortItems = true) {
|
||||
if (this.isLoading) {
|
||||
await when(() => !this.isLoading);
|
||||
|
||||
return;
|
||||
}
|
||||
this.isLoading = true;
|
||||
|
||||
try {
|
||||
let items = await request();
|
||||
|
||||
if (sortItems) items = this.sortItems(items);
|
||||
this.items.replace(items);
|
||||
this.isLoaded = true;
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected async loadItem(...args: any[]): Promise<Item>
|
||||
@action
|
||||
protected async loadItem(request: () => Promise<Item>, sortItems = true) {
|
||||
const item = await Promise.resolve(request()).catch(() => null);
|
||||
|
||||
if (item) {
|
||||
const existingItem = this.items.find(el => el.getId() === item.getId());
|
||||
|
||||
if (existingItem) {
|
||||
const index = this.items.findIndex(item => item === existingItem);
|
||||
|
||||
this.items.splice(index, 1, item);
|
||||
} else {
|
||||
let items = [...this.items, item];
|
||||
|
||||
if (sortItems) items = this.sortItems(items);
|
||||
this.items.replace(items);
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
protected async updateItem(item: Item, request: () => Promise<Item>) {
|
||||
const updatedItem = await request();
|
||||
const index = this.items.findIndex(i => i.getId() === item.getId());
|
||||
|
||||
this.items.splice(index, 1, updatedItem);
|
||||
|
||||
return updatedItem;
|
||||
}
|
||||
|
||||
@action
|
||||
protected async removeItem(item: Item, request: () => Promise<any>) {
|
||||
await request();
|
||||
this.items.remove(item);
|
||||
this.selectedItemsIds.delete(item.getId());
|
||||
}
|
||||
|
||||
isSelected(item: Item) {
|
||||
return !!this.selectedItemsIds.get(item.getId());
|
||||
}
|
||||
|
||||
@action
|
||||
select(item: Item) {
|
||||
this.selectedItemsIds.set(item.getId(), true);
|
||||
}
|
||||
|
||||
@action
|
||||
unselect(item: Item) {
|
||||
this.selectedItemsIds.delete(item.getId());
|
||||
}
|
||||
|
||||
@action
|
||||
toggleSelection(item: Item) {
|
||||
if (this.isSelected(item)) {
|
||||
this.unselect(item);
|
||||
} else {
|
||||
this.select(item);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
toggleSelectionAll(visibleItems: Item[] = this.items) {
|
||||
const allSelected = visibleItems.every(this.isSelected);
|
||||
|
||||
if (allSelected) {
|
||||
visibleItems.forEach(this.unselect);
|
||||
} else {
|
||||
visibleItems.forEach(this.select);
|
||||
}
|
||||
}
|
||||
|
||||
isSelectedAll(visibleItems: Item[] = this.items) {
|
||||
if (!visibleItems.length) return false;
|
||||
|
||||
return visibleItems.every(this.isSelected);
|
||||
}
|
||||
|
||||
@action
|
||||
resetSelection() {
|
||||
this.selectedItemsIds.clear();
|
||||
}
|
||||
|
||||
@action
|
||||
reset() {
|
||||
this.resetSelection();
|
||||
this.items.clear();
|
||||
this.selectedItemsIds.clear();
|
||||
this.isLoaded = false;
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
async removeSelectedItems?(): Promise<any>;
|
||||
|
||||
* [Symbol.iterator]() {
|
||||
yield* this.items;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { ingressStore } from "../../../renderer/components/+network-ingresses/ingress.store";
|
||||
import { apiManager } from "../api-manager";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { KubeObject } from "../kube-object";
|
||||
|
||||
class TestApi extends KubeApi<KubeObject> {
|
||||
|
||||
protected async checkPreferredVersion() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
describe("ApiManager", () => {
|
||||
describe("registerApi", () => {
|
||||
it("re-register store if apiBase changed", async () => {
|
||||
const apiBase = "apis/v1/foo";
|
||||
const fallbackApiBase = "/apis/extensions/v1beta1/foo";
|
||||
const kubeApi = new TestApi({
|
||||
objectConstructor: KubeObject,
|
||||
apiBase,
|
||||
fallbackApiBases: [fallbackApiBase],
|
||||
checkPreferredVersion: true,
|
||||
});
|
||||
|
||||
apiManager.registerApi(apiBase, kubeApi);
|
||||
|
||||
// Define to use test api for ingress store
|
||||
Object.defineProperty(ingressStore, "api", { value: kubeApi });
|
||||
apiManager.registerStore(ingressStore, [kubeApi]);
|
||||
|
||||
// Test that store is returned with original apiBase
|
||||
expect(apiManager.getStore(kubeApi)).toBe(ingressStore);
|
||||
|
||||
// Change apiBase similar as checkPreferredVersion does
|
||||
Object.defineProperty(kubeApi, "apiBase", { value: fallbackApiBase });
|
||||
apiManager.registerApi(fallbackApiBase, kubeApi);
|
||||
|
||||
// Test that store is returned with new apiBase
|
||||
expect(apiManager.getStore(kubeApi)).toBe(ingressStore);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { CustomResourceDefinition } from "../endpoints";
|
||||
import type { KubeObjectMetadata } from "../kube-object";
|
||||
|
||||
describe("Crds", () => {
|
||||
describe("getVersion", () => {
|
||||
it("should get the first version name from the list of versions", () => {
|
||||
const crd = new CustomResourceDefinition({
|
||||
apiVersion: "foo",
|
||||
kind: "CustomResourceDefinition",
|
||||
metadata: {} as KubeObjectMetadata,
|
||||
});
|
||||
|
||||
crd.spec = {
|
||||
versions: [
|
||||
{
|
||||
name: "123",
|
||||
served: false,
|
||||
storage: false,
|
||||
}
|
||||
]
|
||||
} as any;
|
||||
|
||||
expect(crd.getVersion()).toBe("123");
|
||||
});
|
||||
|
||||
it("should get the first version name from the list of versions (length 2)", () => {
|
||||
const crd = new CustomResourceDefinition({
|
||||
apiVersion: "foo",
|
||||
kind: "CustomResourceDefinition",
|
||||
metadata: {} as KubeObjectMetadata,
|
||||
});
|
||||
|
||||
crd.spec = {
|
||||
versions: [
|
||||
{
|
||||
name: "123",
|
||||
served: false,
|
||||
storage: false,
|
||||
},
|
||||
{
|
||||
name: "1234",
|
||||
served: false,
|
||||
storage: false,
|
||||
}
|
||||
]
|
||||
} as any;
|
||||
|
||||
expect(crd.getVersion()).toBe("123");
|
||||
});
|
||||
|
||||
it("should get the first version name from the list of versions (length 2) even with version field", () => {
|
||||
const crd = new CustomResourceDefinition({
|
||||
apiVersion: "foo",
|
||||
kind: "CustomResourceDefinition",
|
||||
metadata: {} as KubeObjectMetadata,
|
||||
});
|
||||
|
||||
crd.spec = {
|
||||
version: "abc",
|
||||
versions: [
|
||||
{
|
||||
name: "123",
|
||||
served: false,
|
||||
storage: false,
|
||||
},
|
||||
{
|
||||
name: "1234",
|
||||
served: false,
|
||||
storage: false,
|
||||
}
|
||||
]
|
||||
} as any;
|
||||
|
||||
expect(crd.getVersion()).toBe("123");
|
||||
});
|
||||
|
||||
it("should get the first version name from the version field", () => {
|
||||
const crd = new CustomResourceDefinition({
|
||||
apiVersion: "foo",
|
||||
kind: "CustomResourceDefinition",
|
||||
metadata: {} as KubeObjectMetadata,
|
||||
});
|
||||
|
||||
crd.spec = {
|
||||
version: "abc"
|
||||
} as any;
|
||||
|
||||
expect(crd.getVersion()).toBe("abc");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { EndpointSubset } from "../endpoints";
|
||||
|
||||
describe("endpoint tests", () => {
|
||||
describe("EndpointSubset", () => {
|
||||
it.each([
|
||||
4,
|
||||
false,
|
||||
null,
|
||||
{},
|
||||
[],
|
||||
"ahe",
|
||||
/a/,
|
||||
])("should always initialize fields when given %j", (data: any) => {
|
||||
const sub = new EndpointSubset(data);
|
||||
|
||||
expect(sub.addresses).toStrictEqual([]);
|
||||
expect(sub.notReadyAddresses).toStrictEqual([]);
|
||||
expect(sub.ports).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("toString should be addresses X ports", () => {
|
||||
const sub = new EndpointSubset({
|
||||
addresses: [{
|
||||
ip: "1.1.1.1",
|
||||
}, {
|
||||
ip: "1.1.1.2",
|
||||
}] as any,
|
||||
notReadyAddresses: [],
|
||||
ports: [{
|
||||
port: "81",
|
||||
}, {
|
||||
port: "82",
|
||||
}] as any,
|
||||
});
|
||||
|
||||
expect(sub.toString()).toBe("1.1.1.1:81, 1.1.1.1:82, 1.1.1.2:81, 1.1.1.2:82");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
jest.mock("../kube-object");
|
||||
jest.mock("../kube-api");
|
||||
jest.mock("../api-manager", () => ({
|
||||
apiManager() {
|
||||
return {
|
||||
registerStore: jest.fn(),
|
||||
};
|
||||
}
|
||||
}));
|
||||
|
||||
import { IKubeApiParsed, parseKubeApi } from "../kube-api-parse";
|
||||
|
||||
/**
|
||||
* [<input-url>, <expected-result>]
|
||||
*/
|
||||
type KubeApiParseTestData = [string, Required<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)).toThrowError("invalid apiPath");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { KubeJsonApi } from "../kube-json-api";
|
||||
import { KubeObject } from "../kube-object";
|
||||
|
||||
describe("KubeApi", () => {
|
||||
let request: KubeJsonApi;
|
||||
|
||||
beforeEach(() => {
|
||||
request = new KubeJsonApi({
|
||||
serverAddress: `http://127.0.0.1:9999`,
|
||||
apiBase: "/api-kube"
|
||||
});
|
||||
});
|
||||
|
||||
it("uses url from apiBase if apiBase contains the resource", async () => {
|
||||
(fetch as any).mockResponse(async (request: any) => {
|
||||
if (request.url === "http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1") {
|
||||
return {
|
||||
body: JSON.stringify({
|
||||
resources: [{
|
||||
name: "ingresses"
|
||||
}] as any[]
|
||||
})
|
||||
};
|
||||
} else if (request.url === "http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1") {
|
||||
// Even if the old API contains ingresses, KubeApi should prefer the apiBase url
|
||||
return {
|
||||
body: JSON.stringify({
|
||||
resources: [{
|
||||
name: "ingresses"
|
||||
}] as any[]
|
||||
})
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
body: JSON.stringify({
|
||||
resources: [] as any[]
|
||||
})
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const apiBase = "/apis/networking.k8s.io/v1/ingresses";
|
||||
const fallbackApiBase = "/apis/extensions/v1beta1/ingresses";
|
||||
const kubeApi = new KubeApi({
|
||||
request,
|
||||
objectConstructor: KubeObject,
|
||||
apiBase,
|
||||
fallbackApiBases: [fallbackApiBase],
|
||||
checkPreferredVersion: true,
|
||||
});
|
||||
|
||||
await kubeApi.get();
|
||||
expect(kubeApi.apiPrefix).toEqual("/apis");
|
||||
expect(kubeApi.apiGroup).toEqual("networking.k8s.io");
|
||||
});
|
||||
|
||||
it("uses url from fallbackApiBases if apiBase lacks the resource", async () => {
|
||||
(fetch as any).mockResponse(async (request: any) => {
|
||||
if (request.url === "http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1") {
|
||||
return {
|
||||
body: JSON.stringify({
|
||||
resources: [] as any[]
|
||||
})
|
||||
};
|
||||
} else if (request.url === "http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1") {
|
||||
return {
|
||||
body: JSON.stringify({
|
||||
resources: [{
|
||||
name: "ingresses"
|
||||
}] as any[]
|
||||
})
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
body: JSON.stringify({
|
||||
resources: [] as any[]
|
||||
})
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const apiBase = "apis/networking.k8s.io/v1/ingresses";
|
||||
const fallbackApiBase = "/apis/extensions/v1beta1/ingresses";
|
||||
const kubeApi = new KubeApi({
|
||||
request,
|
||||
objectConstructor: KubeObject,
|
||||
apiBase,
|
||||
fallbackApiBases: [fallbackApiBase],
|
||||
checkPreferredVersion: true,
|
||||
});
|
||||
|
||||
await kubeApi.get();
|
||||
expect(kubeApi.apiPrefix).toEqual("/apis");
|
||||
expect(kubeApi.apiGroup).toEqual("extensions");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { 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 !Boolean(void val);
|
||||
}
|
||||
|
||||
function isNotAny(val: unknown): val is any {
|
||||
return Boolean(void val);
|
||||
}
|
||||
|
||||
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,38 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { 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",
|
||||
},
|
||||
});
|
||||
|
||||
expect(pod.getAllContainers()).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,324 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Pod } from "../endpoints";
|
||||
|
||||
interface GetDummyPodOptions {
|
||||
running?: number;
|
||||
dead?: number;
|
||||
initRunning?: number;
|
||||
initDead?: number;
|
||||
}
|
||||
|
||||
function getDummyPodDefaultOptions(): Required<GetDummyPodOptions> {
|
||||
return {
|
||||
running: 0,
|
||||
dead: 0,
|
||||
initDead: 0,
|
||||
initRunning: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function getDummyPod(opts: GetDummyPodOptions = getDummyPodDefaultOptions()): Pod {
|
||||
const pod = new Pod({
|
||||
apiVersion: "v1",
|
||||
kind: "Pod",
|
||||
metadata: {
|
||||
uid: "1",
|
||||
name: "test",
|
||||
resourceVersion: "v1",
|
||||
selfLink: "http"
|
||||
}
|
||||
});
|
||||
|
||||
pod.spec = {
|
||||
containers: [],
|
||||
initContainers: [],
|
||||
serviceAccount: "dummy",
|
||||
serviceAccountName: "dummy",
|
||||
};
|
||||
|
||||
pod.status = {
|
||||
phase: "Running",
|
||||
conditions: [],
|
||||
hostIP: "10.0.0.1",
|
||||
podIP: "10.0.0.1",
|
||||
startTime: "now",
|
||||
containerStatuses: [],
|
||||
initContainerStatuses: [],
|
||||
};
|
||||
|
||||
for (let i = 0; i < opts.running; i += 1) {
|
||||
const name = `container_r_${i}`;
|
||||
|
||||
pod.spec.containers.push({
|
||||
image: "dummy",
|
||||
imagePullPolicy: "dummy",
|
||||
name,
|
||||
});
|
||||
pod.status.containerStatuses.push({
|
||||
image: "dummy",
|
||||
imageID: "dummy",
|
||||
name,
|
||||
ready: true,
|
||||
restartCount: i,
|
||||
state: {
|
||||
running: {
|
||||
startedAt: "before"
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = 0; i < opts.dead; i += 1) {
|
||||
const name = `container_d_${i}`;
|
||||
|
||||
pod.spec.containers.push({
|
||||
image: "dummy",
|
||||
imagePullPolicy: "dummy",
|
||||
name,
|
||||
});
|
||||
pod.status.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 < opts.initRunning; i += 1) {
|
||||
const name = `container_ir_${i}`;
|
||||
|
||||
pod.spec.initContainers.push({
|
||||
image: "dummy",
|
||||
imagePullPolicy: "dummy",
|
||||
name,
|
||||
});
|
||||
pod.status.initContainerStatuses.push({
|
||||
image: "dummy",
|
||||
imageID: "dummy",
|
||||
name,
|
||||
ready: true,
|
||||
restartCount: i,
|
||||
state: {
|
||||
running: {
|
||||
startedAt: "before"
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = 0; i < opts.initDead; i += 1) {
|
||||
const name = `container_id_${i}`;
|
||||
|
||||
pod.spec.initContainers.push({
|
||||
image: "dummy",
|
||||
imagePullPolicy: "dummy",
|
||||
name,
|
||||
});
|
||||
pod.status.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: "dummy",
|
||||
name
|
||||
};
|
||||
}
|
||||
|
||||
it("getRunningContainers should return only running and init running", () => {
|
||||
const res = [
|
||||
...Array.from(new Array(running), (val, index) => getNamedContainer(`container_r_${index}`)),
|
||||
...Array.from(new Array(initRunning), (val, index) => getNamedContainer(`container_ir_${index}`)),
|
||||
];
|
||||
|
||||
expect(pod.getRunningContainers()).toStrictEqual(res);
|
||||
});
|
||||
|
||||
it("getAllContainers should return all containers", () => {
|
||||
const res = [
|
||||
...Array.from(new Array(running), (val, index) => getNamedContainer(`container_r_${index}`)),
|
||||
...Array.from(new Array(dead), (val, index) => getNamedContainer(`container_d_${index}`)),
|
||||
...Array.from(new Array(initRunning), (val, index) => getNamedContainer(`container_ir_${index}`)),
|
||||
...Array.from(new Array(initDead), (val, index) => getNamedContainer(`container_id_${index}`)),
|
||||
];
|
||||
|
||||
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 });
|
||||
|
||||
pod.status.containerStatuses[0].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 });
|
||||
|
||||
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 });
|
||||
|
||||
pod.status.phase = "Running";
|
||||
|
||||
expect(pod.hasIssues()).toStrictEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import type { KubeObjectStore } from "./kube-object.store";
|
||||
|
||||
import { action, observable, makeObservable } from "mobx";
|
||||
import { autoBind, iter } from "../utils";
|
||||
import type { KubeApi } from "./kube-api";
|
||||
import type { KubeObject } from "./kube-object";
|
||||
import { IKubeObjectRef, parseKubeApi, createKubeApiURL } from "./kube-api-parse";
|
||||
|
||||
export class ApiManager {
|
||||
private apis = observable.map<string, KubeApi<KubeObject>>();
|
||||
private stores = observable.map<string, KubeObjectStore<KubeObject>>();
|
||||
|
||||
constructor() {
|
||||
makeObservable(this);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
getApi(pathOrCallback: string | ((api: KubeApi<KubeObject>) => boolean)) {
|
||||
if (typeof pathOrCallback === "string") {
|
||||
return this.apis.get(pathOrCallback) || this.apis.get(parseKubeApi(pathOrCallback).apiBase);
|
||||
}
|
||||
|
||||
return iter.find(this.apis.values(), pathOrCallback ?? (() => true));
|
||||
}
|
||||
|
||||
getApiByKind(kind: string, apiVersion: string) {
|
||||
return iter.find(this.apis.values(), api => api.kind === kind && api.apiVersionWithGroup === apiVersion);
|
||||
}
|
||||
|
||||
registerApi(apiBase: string, api: KubeApi<KubeObject>) {
|
||||
if (!api.apiBase) return;
|
||||
|
||||
if (!this.apis.has(apiBase)) {
|
||||
this.stores.forEach((store) => {
|
||||
if (store.api === api) {
|
||||
this.stores.set(apiBase, store);
|
||||
}
|
||||
});
|
||||
|
||||
this.apis.set(apiBase, api);
|
||||
}
|
||||
}
|
||||
|
||||
protected resolveApi<K extends KubeObject>(api?: string | KubeApi<K>): KubeApi<K> | undefined {
|
||||
if (!api) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (typeof api === "string") {
|
||||
return this.getApi(api) as KubeApi<K>;
|
||||
}
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
unregisterApi(api: string | KubeApi<KubeObject>) {
|
||||
if (typeof api === "string") this.apis.delete(api);
|
||||
else {
|
||||
const apis = Array.from(this.apis.entries());
|
||||
const entry = apis.find(entry => entry[1] === api);
|
||||
|
||||
if (entry) this.unregisterApi(entry[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
registerStore(store: KubeObjectStore<KubeObject>, apis: KubeApi<KubeObject>[] = [store.api]) {
|
||||
apis.filter(Boolean).forEach(api => {
|
||||
if (api.apiBase) this.stores.set(api.apiBase, store);
|
||||
});
|
||||
}
|
||||
|
||||
getStore<S extends KubeObjectStore<KubeObject>>(api: string | KubeApi<KubeObject>): S | undefined {
|
||||
return this.stores.get(this.resolveApi(api)?.apiBase) as S;
|
||||
}
|
||||
|
||||
lookupApiLink(ref: IKubeObjectRef, parentObject: KubeObject): string {
|
||||
const {
|
||||
kind, apiVersion, name,
|
||||
namespace = parentObject.getNs()
|
||||
} = ref;
|
||||
|
||||
if (!kind) return "";
|
||||
|
||||
// search in registered apis by 'kind' & 'apiVersion'
|
||||
const api = this.getApi(api => api.kind === kind && api.apiVersionWithGroup == apiVersion);
|
||||
|
||||
if (api) {
|
||||
return api.getUrl({ namespace, name });
|
||||
}
|
||||
|
||||
// lookup api by generated resource link
|
||||
const apiPrefixes = ["/apis", "/api"];
|
||||
const resource = kind.endsWith("s") ? `${kind.toLowerCase()}es` : `${kind.toLowerCase()}s`;
|
||||
|
||||
for (const apiPrefix of apiPrefixes) {
|
||||
const apiLink = createKubeApiURL({ apiPrefix, apiVersion, name, namespace, resource });
|
||||
|
||||
if (this.getApi(apiLink)) {
|
||||
return apiLink;
|
||||
}
|
||||
}
|
||||
|
||||
// resolve by kind only (hpa's might use refs to older versions of resources for example)
|
||||
const apiByKind = this.getApi(api => api.kind === kind);
|
||||
|
||||
if (apiByKind) {
|
||||
return apiByKind.getUrl({ name, namespace });
|
||||
}
|
||||
|
||||
// otherwise generate link with default prefix
|
||||
// resource still might exists in k8s, but api is not registered in the app
|
||||
return createKubeApiURL({ apiVersion, name, namespace, resource });
|
||||
}
|
||||
}
|
||||
|
||||
export const apiManager = new ApiManager();
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import type { Cluster } from "../../main/cluster";
|
||||
|
||||
export interface ClusterContext {
|
||||
cluster?: Cluster;
|
||||
allNamespaces: string[]; // available / allowed namespaces from cluster.ts
|
||||
contextNamespaces: string[]; // selected by user (see: namespace-select.tsx)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { KubeObject } from "../kube-object";
|
||||
|
||||
export type ClusterRoleBindingSubjectKind = "Group" | "ServiceAccount" | "User";
|
||||
|
||||
export interface ClusterRoleBindingSubject {
|
||||
kind: ClusterRoleBindingSubjectKind;
|
||||
name: string;
|
||||
apiGroup?: string;
|
||||
namespace?: string;
|
||||
}
|
||||
|
||||
export interface ClusterRoleBinding {
|
||||
subjects?: ClusterRoleBindingSubject[];
|
||||
roleRef: {
|
||||
kind: string;
|
||||
name: string;
|
||||
apiGroup?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class ClusterRoleBinding extends KubeObject {
|
||||
static kind = "ClusterRoleBinding";
|
||||
static namespaced = false;
|
||||
static apiBase = "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings";
|
||||
|
||||
getSubjects() {
|
||||
return this.subjects || [];
|
||||
}
|
||||
|
||||
getSubjectNames(): string {
|
||||
return this.getSubjects().map(subject => subject.name).join(", ");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only available within kubernetes cluster pages
|
||||
*/
|
||||
let clusterRoleBindingApi: KubeApi<ClusterRoleBinding>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
clusterRoleBindingApi = new KubeApi({
|
||||
objectConstructor: ClusterRoleBinding,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
clusterRoleBindingApi
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { KubeObject } from "../kube-object";
|
||||
|
||||
export interface ClusterRole {
|
||||
rules: {
|
||||
verbs: string[];
|
||||
apiGroups: string[];
|
||||
resources: string[];
|
||||
resourceNames?: string[];
|
||||
}[];
|
||||
}
|
||||
|
||||
export class ClusterRole extends KubeObject {
|
||||
static kind = "ClusterRole";
|
||||
static namespaced = false;
|
||||
static apiBase = "/apis/rbac.authorization.k8s.io/v1/clusterroles";
|
||||
|
||||
getRules() {
|
||||
return this.rules || [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only available within kubernetes cluster pages
|
||||
*/
|
||||
let clusterRoleApi: KubeApi<ClusterRole>;
|
||||
|
||||
if (isClusterPageContext()) { // initialize automatically only when within a cluster iframe/context
|
||||
clusterRoleApi = new KubeApi({
|
||||
objectConstructor: ClusterRole,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
clusterRoleApi
|
||||
};
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { IMetrics, IMetricsReqParams, metricsApi } from "./metrics.api";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export class ClusterApi extends KubeApi<Cluster> {
|
||||
static kind = "Cluster";
|
||||
static namespaced = true;
|
||||
}
|
||||
|
||||
export function getMetricsByNodeNames(nodeNames: string[], params?: IMetricsReqParams): Promise<IClusterMetrics> {
|
||||
const nodes = nodeNames.join("|");
|
||||
const opts = { category: "cluster", nodes };
|
||||
|
||||
return metricsApi.getMetrics({
|
||||
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 enum ClusterStatus {
|
||||
ACTIVE = "Active",
|
||||
CREATING = "Creating",
|
||||
REMOVING = "Removing",
|
||||
ERROR = "Error"
|
||||
}
|
||||
|
||||
export interface IClusterMetrics<T = IMetrics> {
|
||||
[metric: string]: T;
|
||||
memoryUsage: T;
|
||||
memoryRequests: T;
|
||||
memoryLimits: T;
|
||||
memoryCapacity: T;
|
||||
cpuUsage: T;
|
||||
cpuRequests: T;
|
||||
cpuLimits: T;
|
||||
cpuCapacity: T;
|
||||
podUsage: T;
|
||||
podCapacity: T;
|
||||
fsSize: T;
|
||||
fsUsage: T;
|
||||
}
|
||||
|
||||
export interface 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";
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only available within kubernetes cluster pages
|
||||
*/
|
||||
let clusterApi: ClusterApi;
|
||||
|
||||
if (isClusterPageContext()) { // initialize automatically only when within a cluster iframe/context
|
||||
clusterApi = new ClusterApi({
|
||||
objectConstructor: Cluster,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
clusterApi
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { KubeApi } from "../kube-api";
|
||||
|
||||
export interface IComponentStatusCondition {
|
||||
type: string;
|
||||
status: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ComponentStatus {
|
||||
conditions: IComponentStatusCondition[];
|
||||
}
|
||||
|
||||
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 const componentStatusApi = new KubeApi({
|
||||
objectConstructor: ComponentStatus,
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { autoBind } from "../../utils";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export interface ConfigMap {
|
||||
data: {
|
||||
[param: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class ConfigMap extends KubeObject {
|
||||
static kind = "ConfigMap";
|
||||
static namespaced = true;
|
||||
static apiBase = "/api/v1/configmaps";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
|
||||
this.data ??= {};
|
||||
}
|
||||
|
||||
getKeys(): string[] {
|
||||
return Object.keys(this.data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only available within kubernetes cluster pages
|
||||
*/
|
||||
let configMapApi: KubeApi<ConfigMap>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
configMapApi = new KubeApi({
|
||||
objectConstructor: ConfigMap,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
configMapApi
|
||||
};
|
||||
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { crdResourcesURL } from "../../routes";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
type AdditionalPrinterColumnsCommon = {
|
||||
name: string;
|
||||
type: "integer" | "number" | "string" | "boolean" | "date";
|
||||
priority: number;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type AdditionalPrinterColumnsV1 = AdditionalPrinterColumnsCommon & {
|
||||
jsonPath: string;
|
||||
};
|
||||
|
||||
type AdditionalPrinterColumnsV1Beta = AdditionalPrinterColumnsCommon & {
|
||||
JSONPath: string;
|
||||
};
|
||||
|
||||
export interface CustomResourceDefinition {
|
||||
spec: {
|
||||
group: string;
|
||||
version?: string; // deprecated in v1 api
|
||||
names: {
|
||||
plural: string;
|
||||
singular: string;
|
||||
kind: string;
|
||||
listKind: string;
|
||||
};
|
||||
scope: "Namespaced" | "Cluster" | string;
|
||||
validation?: any;
|
||||
versions?: {
|
||||
name: string;
|
||||
served: boolean;
|
||||
storage: boolean;
|
||||
schema?: unknown; // required in v1 but not present in v1beta
|
||||
additionalPrinterColumns?: AdditionalPrinterColumnsV1[]
|
||||
}[];
|
||||
conversion: {
|
||||
strategy?: string;
|
||||
webhook?: any;
|
||||
};
|
||||
additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[]; // removed in v1
|
||||
};
|
||||
status: {
|
||||
conditions: {
|
||||
lastTransitionTime: string;
|
||||
message: string;
|
||||
reason: string;
|
||||
status: string;
|
||||
type?: string;
|
||||
}[];
|
||||
acceptedNames: {
|
||||
plural: string;
|
||||
singular: string;
|
||||
kind: string;
|
||||
shortNames: string[];
|
||||
listKind: string;
|
||||
};
|
||||
storedVersions: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export class CustomResourceDefinition extends KubeObject {
|
||||
static kind = "CustomResourceDefinition";
|
||||
static namespaced = false;
|
||||
static apiBase = "/apis/apiextensions.k8s.io/v1/customresourcedefinitions";
|
||||
|
||||
getResourceUrl() {
|
||||
return crdResourcesURL({
|
||||
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.substr(1);
|
||||
}
|
||||
|
||||
getGroup() {
|
||||
return this.spec.group;
|
||||
}
|
||||
|
||||
getScope() {
|
||||
return this.spec.scope;
|
||||
}
|
||||
|
||||
getVersion() {
|
||||
// v1 has removed the spec.version property, if it is present it must match the first version
|
||||
return this.spec.versions?.[0]?.name ?? this.spec.version;
|
||||
}
|
||||
|
||||
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.spec.versions?.find(a => this.getVersion() == a.name)?.additionalPrinterColumns
|
||||
?? this.spec.additionalPrinterColumns?.map(({ JSONPath, ...rest }) => ({ ...rest, jsonPath: JSONPath })) // map to V1 shape
|
||||
?? [];
|
||||
|
||||
return columns
|
||||
.filter(column => column.name != "Age")
|
||||
.filter(column => ignorePriority ? true : !column.priority);
|
||||
}
|
||||
|
||||
getValidation() {
|
||||
return JSON.stringify(this.spec.validation ?? this.spec.versions?.[0]?.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})`
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only available within kubernetes cluster pages
|
||||
*/
|
||||
let crdApi: KubeApi<CustomResourceDefinition>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
crdApi = new KubeApi<CustomResourceDefinition>({
|
||||
objectConstructor: CustomResourceDefinition,
|
||||
checkPreferredVersion: true,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
crdApi
|
||||
};
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import moment from "moment";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { IPodContainer } from "./pods.api";
|
||||
import { formatDuration } from "../../utils/formatDuration";
|
||||
import { autoBind } from "../../utils";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export class CronJobApi extends KubeApi<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 CronJob {
|
||||
spec: {
|
||||
schedule: string;
|
||||
concurrencyPolicy: string;
|
||||
suspend: boolean;
|
||||
jobTemplate: {
|
||||
metadata: {
|
||||
creationTimestamp?: string;
|
||||
labels?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
annotations?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
};
|
||||
spec: {
|
||||
template: {
|
||||
metadata: {
|
||||
creationTimestamp?: string;
|
||||
};
|
||||
spec: {
|
||||
containers: IPodContainer[];
|
||||
restartPolicy: string;
|
||||
terminationGracePeriodSeconds: number;
|
||||
dnsPolicy: string;
|
||||
hostPID: boolean;
|
||||
schedulerName: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
successfulJobsHistoryLimit: number;
|
||||
failedJobsHistoryLimit: number;
|
||||
};
|
||||
status: {
|
||||
lastScheduleTime?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class CronJob extends KubeObject {
|
||||
static kind = "CronJob";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/batch/v1beta1/cronjobs";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
getSuspendFlag() {
|
||||
return this.spec.suspend.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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only available within kubernetes cluster pages
|
||||
*/
|
||||
let cronJobApi: CronJobApi;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
cronJobApi = new CronJobApi({
|
||||
objectConstructor: CronJob,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
cronJobApi
|
||||
};
|
||||
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import get from "lodash/get";
|
||||
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
|
||||
import { autoBind } from "../../utils";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { metricsApi } from "./metrics.api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import type { IPodContainer, IPodMetrics } from "./pods.api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export class DaemonSet extends WorkloadKubeObject {
|
||||
static kind = "DaemonSet";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/apps/v1/daemonsets";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
declare spec: {
|
||||
selector: {
|
||||
matchLabels: {
|
||||
[name: string]: string;
|
||||
};
|
||||
};
|
||||
template: {
|
||||
metadata: {
|
||||
creationTimestamp?: string;
|
||||
labels: {
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
spec: {
|
||||
containers: IPodContainer[];
|
||||
initContainers?: IPodContainer[];
|
||||
restartPolicy: string;
|
||||
terminationGracePeriodSeconds: number;
|
||||
dnsPolicy: string;
|
||||
hostPID: boolean;
|
||||
affinity?: IAffinity;
|
||||
nodeSelector?: {
|
||||
[selector: string]: string;
|
||||
};
|
||||
securityContext: {};
|
||||
schedulerName: string;
|
||||
tolerations: {
|
||||
key: string;
|
||||
operator: string;
|
||||
effect: string;
|
||||
tolerationSeconds: number;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
updateStrategy: {
|
||||
type: string;
|
||||
rollingUpdate: {
|
||||
maxUnavailable: number;
|
||||
};
|
||||
};
|
||||
revisionHistoryLimit: number;
|
||||
};
|
||||
declare status: {
|
||||
currentNumberScheduled: number;
|
||||
numberMisscheduled: number;
|
||||
desiredNumberScheduled: number;
|
||||
numberReady: number;
|
||||
observedGeneration: number;
|
||||
updatedNumberScheduled: number;
|
||||
numberAvailable: number;
|
||||
numberUnavailable: number;
|
||||
};
|
||||
|
||||
getImages() {
|
||||
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []);
|
||||
const initContainers: IPodContainer[] = get(this, "spec.template.spec.initContainers", []);
|
||||
|
||||
return [...containers, ...initContainers].map(container => container.image);
|
||||
}
|
||||
}
|
||||
|
||||
export class DaemonSetApi extends KubeApi<DaemonSet> {
|
||||
}
|
||||
|
||||
export function getMetricsForDaemonSets(daemonsets: DaemonSet[], namespace: string, selector = ""): Promise<IPodMetrics> {
|
||||
const podSelector = daemonsets.map(daemonset => `${daemonset.getName()}-[[:alnum:]]{5}`).join("|");
|
||||
const opts = { category: "pods", pods: podSelector, namespace, selector };
|
||||
|
||||
return metricsApi.getMetrics({
|
||||
cpuUsage: opts,
|
||||
memoryUsage: opts,
|
||||
fsUsage: opts,
|
||||
networkReceive: opts,
|
||||
networkTransmit: opts,
|
||||
}, {
|
||||
namespace,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Only available within kubernetes cluster pages
|
||||
*/
|
||||
let daemonSetApi: DaemonSetApi;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
daemonSetApi = new DaemonSetApi({
|
||||
objectConstructor: DaemonSet,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
daemonSetApi
|
||||
};
|
||||
@@ -0,0 +1,246 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import moment from "moment";
|
||||
|
||||
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
|
||||
import { autoBind } from "../../utils";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { metricsApi } from "./metrics.api";
|
||||
import type { IPodMetrics } from "./pods.api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export class DeploymentApi extends KubeApi<Deployment> {
|
||||
protected getScaleApiUrl(params: { namespace: string; name: string }) {
|
||||
return `${this.getUrl(params)}/scale`;
|
||||
}
|
||||
|
||||
getReplicas(params: { namespace: string; name: string }): Promise<number> {
|
||||
return this.request
|
||||
.get(this.getScaleApiUrl(params))
|
||||
.then(({ status }: any) => status?.replicas);
|
||||
}
|
||||
|
||||
scale(params: { namespace: string; name: string }, replicas: number) {
|
||||
return this.request.put(this.getScaleApiUrl(params), {
|
||||
data: {
|
||||
metadata: params,
|
||||
spec: {
|
||||
replicas
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 function getMetricsForDeployments(deployments: Deployment[], namespace: string, selector = ""): Promise<IPodMetrics> {
|
||||
const podSelector = deployments.map(deployment => `${deployment.getName()}-[[:alnum:]]{9,}-[[:alnum:]]{5}`).join("|");
|
||||
const opts = { category: "pods", pods: podSelector, namespace, selector };
|
||||
|
||||
return metricsApi.getMetrics({
|
||||
cpuUsage: opts,
|
||||
memoryUsage: opts,
|
||||
fsUsage: opts,
|
||||
networkReceive: opts,
|
||||
networkTransmit: opts,
|
||||
}, {
|
||||
namespace,
|
||||
});
|
||||
}
|
||||
|
||||
interface IContainerProbe {
|
||||
httpGet?: {
|
||||
path?: string;
|
||||
port: number;
|
||||
scheme: string;
|
||||
host?: string;
|
||||
};
|
||||
exec?: {
|
||||
command: string[];
|
||||
};
|
||||
tcpSocket?: {
|
||||
port: number;
|
||||
};
|
||||
initialDelaySeconds?: number;
|
||||
timeoutSeconds?: number;
|
||||
periodSeconds?: number;
|
||||
successThreshold?: number;
|
||||
failureThreshold?: number;
|
||||
}
|
||||
|
||||
export class Deployment extends WorkloadKubeObject {
|
||||
static kind = "Deployment";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/apps/v1/deployments";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
declare spec: {
|
||||
replicas: number;
|
||||
selector: { matchLabels: { [app: string]: string } };
|
||||
template: {
|
||||
metadata: {
|
||||
creationTimestamp?: string;
|
||||
labels: { [app: string]: string };
|
||||
annotations?: { [app: string]: string };
|
||||
};
|
||||
spec: {
|
||||
containers: {
|
||||
name: string;
|
||||
image: string;
|
||||
args?: string[];
|
||||
ports?: {
|
||||
name: string;
|
||||
containerPort: number;
|
||||
protocol: string;
|
||||
}[];
|
||||
env?: {
|
||||
name: string;
|
||||
value: string;
|
||||
}[];
|
||||
resources: {
|
||||
limits?: {
|
||||
cpu: string;
|
||||
memory: string;
|
||||
};
|
||||
requests: {
|
||||
cpu: string;
|
||||
memory: string;
|
||||
};
|
||||
};
|
||||
volumeMounts?: {
|
||||
name: string;
|
||||
mountPath: string;
|
||||
}[];
|
||||
livenessProbe?: IContainerProbe;
|
||||
readinessProbe?: IContainerProbe;
|
||||
startupProbe?: IContainerProbe;
|
||||
terminationMessagePath: string;
|
||||
terminationMessagePolicy: string;
|
||||
imagePullPolicy: string;
|
||||
}[];
|
||||
restartPolicy: string;
|
||||
terminationGracePeriodSeconds: number;
|
||||
dnsPolicy: string;
|
||||
affinity?: IAffinity;
|
||||
nodeSelector?: {
|
||||
[selector: string]: string;
|
||||
};
|
||||
serviceAccountName: string;
|
||||
serviceAccount: string;
|
||||
securityContext: {};
|
||||
schedulerName: string;
|
||||
tolerations?: {
|
||||
key: string;
|
||||
operator: string;
|
||||
effect: string;
|
||||
tolerationSeconds: number;
|
||||
}[];
|
||||
volumes?: {
|
||||
name: string;
|
||||
configMap: {
|
||||
name: string;
|
||||
defaultMode: number;
|
||||
optional: boolean;
|
||||
};
|
||||
}[];
|
||||
};
|
||||
};
|
||||
strategy: {
|
||||
type: string;
|
||||
rollingUpdate: {
|
||||
maxUnavailable: number;
|
||||
maxSurge: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
declare status: {
|
||||
observedGeneration: number;
|
||||
replicas: number;
|
||||
updatedReplicas: number;
|
||||
readyReplicas: number;
|
||||
availableReplicas?: number;
|
||||
unavailableReplicas?: number;
|
||||
conditions: {
|
||||
type: string;
|
||||
status: string;
|
||||
lastUpdateTime: string;
|
||||
lastTransitionTime: string;
|
||||
reason: string;
|
||||
message: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
getConditions(activeOnly = false) {
|
||||
const { conditions } = this.status;
|
||||
|
||||
if (!conditions) return [];
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
let deploymentApi: DeploymentApi;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
deploymentApi = new DeploymentApi({
|
||||
objectConstructor: Deployment,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
deploymentApi
|
||||
};
|
||||
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { autoBind } from "../../utils";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { get } from "lodash";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export interface IEndpointPort {
|
||||
name?: string;
|
||||
protocol: string;
|
||||
port: number;
|
||||
}
|
||||
|
||||
export interface IEndpointAddress {
|
||||
hostname: string;
|
||||
ip: string;
|
||||
nodeName: string;
|
||||
}
|
||||
|
||||
export interface IEndpointSubset {
|
||||
addresses: IEndpointAddress[];
|
||||
notReadyAddresses: IEndpointAddress[];
|
||||
ports: IEndpointPort[];
|
||||
}
|
||||
|
||||
interface ITargetRef {
|
||||
kind: string;
|
||||
namespace: string;
|
||||
name: string;
|
||||
uid: string;
|
||||
resourceVersion: string;
|
||||
apiVersion: string;
|
||||
}
|
||||
|
||||
export class EndpointAddress implements IEndpointAddress {
|
||||
hostname: string;
|
||||
ip: string;
|
||||
nodeName: string;
|
||||
targetRef?: {
|
||||
kind: string;
|
||||
namespace: string;
|
||||
name: string;
|
||||
uid: string;
|
||||
resourceVersion: string;
|
||||
};
|
||||
|
||||
static create(data: IEndpointAddress): EndpointAddress {
|
||||
return new EndpointAddress(data);
|
||||
}
|
||||
|
||||
constructor(data: IEndpointAddress) {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.ip;
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.hostname;
|
||||
}
|
||||
|
||||
getTargetRef(): ITargetRef {
|
||||
if (this.targetRef) {
|
||||
return Object.assign(this.targetRef, {apiVersion: "v1"});
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class EndpointSubset implements IEndpointSubset {
|
||||
addresses: IEndpointAddress[];
|
||||
notReadyAddresses: IEndpointAddress[];
|
||||
ports: IEndpointPort[];
|
||||
|
||||
constructor(data: IEndpointSubset) {
|
||||
this.addresses = get(data, "addresses", []);
|
||||
this.notReadyAddresses = get(data, "notReadyAddresses", []);
|
||||
this.ports = get(data, "ports", []);
|
||||
}
|
||||
|
||||
getAddresses(): EndpointAddress[] {
|
||||
return this.addresses.map(EndpointAddress.create);
|
||||
}
|
||||
|
||||
getNotReadyAddresses(): EndpointAddress[] {
|
||||
return this.notReadyAddresses.map(EndpointAddress.create);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.addresses
|
||||
.map(address => (
|
||||
this.ports
|
||||
.map(port => `${address.ip}:${port.port}`)
|
||||
.join(", ")
|
||||
))
|
||||
.join(", ");
|
||||
}
|
||||
}
|
||||
|
||||
export interface Endpoint {
|
||||
subsets: IEndpointSubset[];
|
||||
}
|
||||
|
||||
export class Endpoint extends KubeObject {
|
||||
static kind = "Endpoints";
|
||||
static namespaced = true;
|
||||
static apiBase = "/api/v1/endpoints";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
getEndpointSubsets(): EndpointSubset[] {
|
||||
const subsets = this.subsets || [];
|
||||
|
||||
return subsets.map(s => new EndpointSubset(s));
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
if(this.subsets) {
|
||||
return this.getEndpointSubsets().map(es => es.toString()).join(", ");
|
||||
} else {
|
||||
return "<none>";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let endpointApi: KubeApi<Endpoint>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
endpointApi = new KubeApi<Endpoint>({
|
||||
objectConstructor: Endpoint,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
endpointApi
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import moment from "moment";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { formatDuration } from "../../utils/formatDuration";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export interface KubeEvent {
|
||||
involvedObject: {
|
||||
kind: string;
|
||||
namespace: string;
|
||||
name: string;
|
||||
uid: string;
|
||||
apiVersion: string;
|
||||
resourceVersion: string;
|
||||
fieldPath: string;
|
||||
};
|
||||
reason: string;
|
||||
message: string;
|
||||
source: {
|
||||
component: string;
|
||||
host: string;
|
||||
};
|
||||
firstTimestamp: string;
|
||||
lastTimestamp: string;
|
||||
count: number;
|
||||
type: "Normal" | "Warning" | string;
|
||||
eventTime: null;
|
||||
reportingComponent: string;
|
||||
reportingInstance: string;
|
||||
}
|
||||
|
||||
export class KubeEvent extends KubeObject {
|
||||
static kind = "Event";
|
||||
static namespaced = true;
|
||||
static apiBase = "/api/v1/events";
|
||||
|
||||
isWarning() {
|
||||
return this.type === "Warning";
|
||||
}
|
||||
|
||||
getSource() {
|
||||
const { component, host } = this.source;
|
||||
|
||||
return `${component} ${host || ""}`;
|
||||
}
|
||||
|
||||
getFirstSeenTime() {
|
||||
const diff = moment().diff(this.firstTimestamp);
|
||||
|
||||
return formatDuration(diff, true);
|
||||
}
|
||||
|
||||
getLastSeenTime() {
|
||||
const diff = moment().diff(this.lastTimestamp);
|
||||
|
||||
return formatDuration(diff, true);
|
||||
}
|
||||
}
|
||||
|
||||
let eventApi: KubeApi<KubeEvent>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
eventApi = new KubeApi<KubeEvent>({
|
||||
objectConstructor: KubeEvent,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
eventApi
|
||||
};
|
||||
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { compile } from "path-to-regexp";
|
||||
import { apiBase } from "../index";
|
||||
import { stringify } from "querystring";
|
||||
import { autoBind } from "../../utils";
|
||||
import type { RequestInit } from "node-fetch";
|
||||
|
||||
export type RepoHelmChartList = Record<string, HelmChart[]>;
|
||||
export type HelmChartList = Record<string, RepoHelmChartList>;
|
||||
|
||||
export interface IHelmChartDetails {
|
||||
readme: string;
|
||||
versions: HelmChart[];
|
||||
}
|
||||
|
||||
const endpoint = compile(`/v2/charts/:repo?/:name?`) as (params?: {
|
||||
repo?: string;
|
||||
name?: string;
|
||||
}) => string;
|
||||
|
||||
/**
|
||||
* Get a list of all helm charts from all saved helm repos
|
||||
*/
|
||||
export async function listCharts(): Promise<HelmChart[]> {
|
||||
const data = await apiBase.get<HelmChartList>(endpoint());
|
||||
|
||||
return Object
|
||||
.values(data)
|
||||
.reduce((allCharts, repoCharts) => allCharts.concat(Object.values(repoCharts)), [])
|
||||
.map(([chart]) => HelmChart.create(chart));
|
||||
}
|
||||
|
||||
export interface GetChartDetailsOptions {
|
||||
version?: string;
|
||||
reqInit?: RequestInit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the readme and all versions of a chart
|
||||
* @param repo The repo to get from
|
||||
* @param name The name of the chart to request the data of
|
||||
* @param options.version The version of the chart's readme to get, default latest
|
||||
* @param options.reqInit A way for passing in an abort controller or other browser request options
|
||||
*/
|
||||
export async function getChartDetails(repo: string, name: string, { version, reqInit }: GetChartDetailsOptions = {}): Promise<IHelmChartDetails> {
|
||||
const path = endpoint({ repo, name });
|
||||
|
||||
const { readme, ...data } = await apiBase.get<IHelmChartDetails>(`${path}?${stringify({ version })}`, undefined, reqInit);
|
||||
const versions = data.versions.map(HelmChart.create);
|
||||
|
||||
return {
|
||||
readme,
|
||||
versions,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get chart values related to a specific repos' version of a chart
|
||||
* @param repo The repo to get from
|
||||
* @param name The name of the chart to request the data of
|
||||
* @param version The version to get the values from
|
||||
*/
|
||||
export async function getChartValues(repo: string, name: string, version: string): Promise<string> {
|
||||
return apiBase.get<string>(`/v2/charts/${repo}/${name}/values?${stringify({ version })}`);
|
||||
}
|
||||
|
||||
export interface HelmChart {
|
||||
apiVersion: string;
|
||||
name: string;
|
||||
version: string;
|
||||
repo: string;
|
||||
kubeVersion?: string;
|
||||
created: string;
|
||||
description?: string;
|
||||
digest: string;
|
||||
keywords?: string[];
|
||||
home?: string;
|
||||
sources?: string[];
|
||||
maintainers?: {
|
||||
name: string;
|
||||
email: string;
|
||||
url: string;
|
||||
}[];
|
||||
engine?: string;
|
||||
icon?: string;
|
||||
appVersion?: string;
|
||||
deprecated?: boolean;
|
||||
tillerVersion?: string;
|
||||
}
|
||||
|
||||
export class HelmChart {
|
||||
constructor(data: HelmChart) {
|
||||
Object.assign(this, data);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
static create(data: any) {
|
||||
return new HelmChart(data);
|
||||
}
|
||||
|
||||
getId() {
|
||||
return `${this.repo}:${this.apiVersion}/${this.name}@${this.getAppVersion()}+${this.digest}`;
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
getFullName(splitter = "/") {
|
||||
return [this.getRepository(), this.getName()].join(splitter);
|
||||
}
|
||||
|
||||
getDescription() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
getIcon() {
|
||||
return this.icon;
|
||||
}
|
||||
|
||||
getHome() {
|
||||
return this.home;
|
||||
}
|
||||
|
||||
getMaintainers() {
|
||||
return this.maintainers || [];
|
||||
}
|
||||
|
||||
getVersion() {
|
||||
return this.version;
|
||||
}
|
||||
|
||||
getRepository() {
|
||||
return this.repo;
|
||||
}
|
||||
|
||||
getAppVersion() {
|
||||
return this.appVersion || "";
|
||||
}
|
||||
|
||||
getKeywords() {
|
||||
return this.keywords || [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import jsYaml from "js-yaml";
|
||||
import { autoBind, formatDuration } from "../../utils";
|
||||
import capitalize from "lodash/capitalize";
|
||||
import { apiBase } from "../index";
|
||||
import { helmChartStore } from "../../../renderer/components/+apps-helm-charts/helm-chart.store";
|
||||
import type { ItemObject } from "../../item.store";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { JsonApiData } from "../json-api";
|
||||
import { buildURLPositional } from "../../utils/buildUrl";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
|
||||
interface IReleasePayload {
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
interface IReleaseRawDetails extends IReleasePayload {
|
||||
resources: KubeJsonApiData[];
|
||||
}
|
||||
|
||||
export interface IReleaseDetails extends IReleasePayload {
|
||||
resources: KubeObject[];
|
||||
}
|
||||
|
||||
export interface IReleaseCreatePayload {
|
||||
name?: string;
|
||||
repo: string;
|
||||
chart: string;
|
||||
namespace: string;
|
||||
version: string;
|
||||
values: string;
|
||||
}
|
||||
|
||||
export interface IReleaseUpdatePayload {
|
||||
repo: string;
|
||||
chart: string;
|
||||
version: string;
|
||||
values: string;
|
||||
}
|
||||
|
||||
export interface IReleaseUpdateDetails {
|
||||
log: string;
|
||||
release: IReleaseDetails;
|
||||
}
|
||||
|
||||
export interface IReleaseRevision {
|
||||
revision: number;
|
||||
updated: string;
|
||||
status: string;
|
||||
chart: string;
|
||||
app_version: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
type EndpointParams = {}
|
||||
| { namespace: string }
|
||||
| { namespace: string, name: string }
|
||||
| { namespace: string, name: string, route: string };
|
||||
|
||||
interface EndpointQuery {
|
||||
all?: boolean;
|
||||
}
|
||||
|
||||
const endpoint = buildURLPositional<EndpointParams, EndpointQuery>("/v2/releases/:namespace?/:name?/:route?");
|
||||
|
||||
export async function listReleases(namespace?: string): Promise<HelmRelease[]> {
|
||||
const releases = await apiBase.get<HelmRelease[]>(endpoint({ namespace }));
|
||||
|
||||
return releases.map(HelmRelease.create);
|
||||
}
|
||||
|
||||
export async function getRelease(name: string, namespace: string): Promise<IReleaseDetails> {
|
||||
const path = endpoint({ name, namespace });
|
||||
const { resources: rawResources, ...details } = await apiBase.get<IReleaseRawDetails>(path);
|
||||
const resources = rawResources.map(KubeObject.create);
|
||||
|
||||
return {
|
||||
...details,
|
||||
resources
|
||||
};
|
||||
}
|
||||
|
||||
export async function createRelease(payload: IReleaseCreatePayload): Promise<IReleaseUpdateDetails> {
|
||||
const { repo, ...data } = payload;
|
||||
|
||||
data.chart = `${repo}/${data.chart}`;
|
||||
data.values = jsYaml.safeLoad(data.values);
|
||||
|
||||
return apiBase.post(endpoint(), { data });
|
||||
}
|
||||
|
||||
export async function updateRelease(name: string, namespace: string, payload: IReleaseUpdatePayload): Promise<IReleaseUpdateDetails> {
|
||||
const { repo, ...data } = payload;
|
||||
|
||||
data.chart = `${repo}/${data.chart}`;
|
||||
data.values = jsYaml.safeLoad(data.values);
|
||||
|
||||
return apiBase.put(endpoint({ name, namespace }), { data });
|
||||
}
|
||||
|
||||
export async function deleteRelease(name: string, namespace: string): Promise<JsonApiData> {
|
||||
const path = endpoint({ name, namespace });
|
||||
|
||||
return apiBase.del(path);
|
||||
}
|
||||
|
||||
export async function getReleaseValues(name: string, namespace: string, all?: boolean): Promise<string> {
|
||||
const route = "values";
|
||||
const path = endpoint({ name, namespace, route }, { all });
|
||||
|
||||
return apiBase.get<string>(path);
|
||||
}
|
||||
|
||||
export async function getReleaseHistory(name: string, namespace: string): Promise<IReleaseRevision[]> {
|
||||
const route = "history";
|
||||
const path = endpoint({ name, namespace, route });
|
||||
|
||||
return apiBase.get(path);
|
||||
}
|
||||
|
||||
export async function rollbackRelease(name: string, namespace: string, revision: number): Promise<JsonApiData> {
|
||||
const route = "rollback";
|
||||
const path = endpoint({ name, namespace, route });
|
||||
const data = { revision };
|
||||
|
||||
return apiBase.put(path, { data });
|
||||
}
|
||||
|
||||
export interface HelmRelease {
|
||||
appVersion: string;
|
||||
name: string;
|
||||
namespace: string;
|
||||
chart: string;
|
||||
status: string;
|
||||
updated: string;
|
||||
revision: string;
|
||||
}
|
||||
|
||||
export class HelmRelease implements ItemObject {
|
||||
constructor(data: any) {
|
||||
Object.assign(this, data);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
static create(data: any) {
|
||||
return new HelmRelease(data);
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.namespace + this.name;
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
getNs() {
|
||||
return this.namespace;
|
||||
}
|
||||
|
||||
getChart(withVersion = false) {
|
||||
let chart = this.chart;
|
||||
|
||||
if (!withVersion && this.getVersion() != "") {
|
||||
const search = new RegExp(`-${this.getVersion()}`);
|
||||
|
||||
chart = chart.replace(search, "");
|
||||
}
|
||||
|
||||
return chart;
|
||||
}
|
||||
|
||||
getRevision() {
|
||||
return parseInt(this.revision, 10);
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return capitalize(this.status);
|
||||
}
|
||||
|
||||
getVersion() {
|
||||
const versions = this.chart.match(/(?<=-)(v?\d+)[^-].*$/);
|
||||
|
||||
return versions?.[0] ?? "";
|
||||
}
|
||||
|
||||
getUpdated(humanize = true, compact = true) {
|
||||
const updated = this.updated.replace(/\s\w*$/, ""); // 2019-11-26 10:58:09 +0300 MSK -> 2019-11-26 10:58:09 +0300 to pass into Date()
|
||||
const updatedDate = new Date(updated).getTime();
|
||||
const diff = Date.now() - updatedDate;
|
||||
|
||||
if (humanize) {
|
||||
return formatDuration(diff, compact);
|
||||
}
|
||||
|
||||
return diff;
|
||||
}
|
||||
|
||||
// Helm does not store from what repository the release is installed,
|
||||
// so we have to try to guess it by searching charts
|
||||
async getRepo() {
|
||||
const chartName = this.getChart();
|
||||
const version = this.getVersion();
|
||||
const versions = await helmChartStore.getVersions(chartName);
|
||||
const chartVersion = versions.find(chartVersion => chartVersion.version === version);
|
||||
|
||||
return chartVersion ? chartVersion.repo : "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export enum HpaMetricType {
|
||||
Resource = "Resource",
|
||||
Pods = "Pods",
|
||||
Object = "Object",
|
||||
External = "External",
|
||||
}
|
||||
|
||||
export type IHpaMetricData<T = any> = T & {
|
||||
target?: {
|
||||
kind: string;
|
||||
name: string;
|
||||
apiVersion: string;
|
||||
};
|
||||
name?: string;
|
||||
metricName?: string;
|
||||
currentAverageUtilization?: number;
|
||||
currentAverageValue?: string;
|
||||
targetAverageUtilization?: number;
|
||||
targetAverageValue?: string;
|
||||
};
|
||||
|
||||
export interface IHpaMetric {
|
||||
[kind: string]: IHpaMetricData;
|
||||
|
||||
type: HpaMetricType;
|
||||
resource?: IHpaMetricData<{ name: string }>;
|
||||
pods?: IHpaMetricData;
|
||||
external?: IHpaMetricData;
|
||||
object?: IHpaMetricData<{
|
||||
describedObject: {
|
||||
apiVersion: string;
|
||||
kind: string;
|
||||
name: string;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface HorizontalPodAutoscaler {
|
||||
spec: {
|
||||
scaleTargetRef: {
|
||||
kind: string;
|
||||
name: string;
|
||||
apiVersion: string;
|
||||
};
|
||||
minReplicas: number;
|
||||
maxReplicas: number;
|
||||
metrics: IHpaMetric[];
|
||||
};
|
||||
status: {
|
||||
currentReplicas: number;
|
||||
desiredReplicas: number;
|
||||
currentMetrics: IHpaMetric[];
|
||||
conditions: {
|
||||
lastTransitionTime: string;
|
||||
message: string;
|
||||
reason: string;
|
||||
status: string;
|
||||
type: string;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
|
||||
export class HorizontalPodAutoscaler extends KubeObject {
|
||||
static kind = "HorizontalPodAutoscaler";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/autoscaling/v2beta1/horizontalpodautoscalers";
|
||||
|
||||
getMaxPods() {
|
||||
return this.spec.maxReplicas || 0;
|
||||
}
|
||||
|
||||
getMinPods() {
|
||||
return this.spec.minReplicas || 0;
|
||||
}
|
||||
|
||||
getReplicas() {
|
||||
return this.status.currentReplicas;
|
||||
}
|
||||
|
||||
getConditions() {
|
||||
if (!this.status.conditions) return [];
|
||||
|
||||
return this.status.conditions.map(condition => {
|
||||
const { message, reason, lastTransitionTime, status } = condition;
|
||||
|
||||
return {
|
||||
...condition,
|
||||
isReady: status === "True",
|
||||
tooltip: `${message || reason} (${lastTransitionTime})`
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
getMetrics() {
|
||||
return this.spec.metrics || [];
|
||||
}
|
||||
|
||||
getCurrentMetrics() {
|
||||
return this.status.currentMetrics || [];
|
||||
}
|
||||
|
||||
protected getMetricName(metric: IHpaMetric): string {
|
||||
const { type, resource, pods, object, external } = metric;
|
||||
|
||||
switch (type) {
|
||||
case HpaMetricType.Resource:
|
||||
return resource.name;
|
||||
case HpaMetricType.Pods:
|
||||
return pods.metricName;
|
||||
case HpaMetricType.Object:
|
||||
return object.metricName;
|
||||
case HpaMetricType.External:
|
||||
return external.metricName;
|
||||
}
|
||||
}
|
||||
|
||||
// todo: refactor
|
||||
getMetricValues(metric: IHpaMetric): string {
|
||||
const metricType = metric.type.toLowerCase();
|
||||
const currentMetric = this.getCurrentMetrics().find(current =>
|
||||
metric.type == current.type && this.getMetricName(metric) == this.getMetricName(current)
|
||||
);
|
||||
const current = currentMetric ? currentMetric[metricType] : null;
|
||||
const target = metric[metricType];
|
||||
let currentValue = "unknown";
|
||||
let targetValue = "unknown";
|
||||
|
||||
if (current) {
|
||||
currentValue = current.currentAverageUtilization || current.currentAverageValue || current.currentValue;
|
||||
if (current.currentAverageUtilization) currentValue += "%";
|
||||
}
|
||||
|
||||
if (target) {
|
||||
targetValue = target.targetAverageUtilization || target.targetAverageValue || target.targetValue;
|
||||
if (target.targetAverageUtilization) targetValue += "%";
|
||||
}
|
||||
|
||||
return `${currentValue} / ${targetValue}`;
|
||||
}
|
||||
}
|
||||
|
||||
let hpaApi: KubeApi<HorizontalPodAutoscaler>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
hpaApi = new KubeApi<HorizontalPodAutoscaler>({
|
||||
objectConstructor: HorizontalPodAutoscaler,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
hpaApi
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
// 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 "./configmap.api";
|
||||
export * from "./crd.api";
|
||||
export * from "./cron-job.api";
|
||||
export * from "./daemon-set.api";
|
||||
export * from "./deployment.api";
|
||||
export * from "./endpoint.api";
|
||||
export * from "./events.api";
|
||||
export * from "./hpa.api";
|
||||
export * from "./ingress.api";
|
||||
export * from "./job.api";
|
||||
export * from "./limit-range.api";
|
||||
export * from "./namespaces.api";
|
||||
export * from "./network-policy.api";
|
||||
export * from "./nodes.api";
|
||||
export * from "./persistent-volume.api";
|
||||
export * from "./persistent-volume-claims.api";
|
||||
export * from "./pods.api";
|
||||
export * from "./poddisruptionbudget.api";
|
||||
export * from "./pod-metrics.api";
|
||||
export * from "./podsecuritypolicy.api";
|
||||
export * from "./replica-set.api";
|
||||
export * from "./resource-quota.api";
|
||||
export * from "./role.api";
|
||||
export * from "./role-binding.api";
|
||||
export * from "./secret.api";
|
||||
export * from "./selfsubjectrulesreviews.api";
|
||||
export * from "./service.api";
|
||||
export * from "./service-accounts.api";
|
||||
export * from "./stateful-set.api";
|
||||
export * from "./storage-class.api";
|
||||
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { autoBind } from "../../utils";
|
||||
import { IMetrics, metricsApi } from "./metrics.api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export class IngressApi extends KubeApi<Ingress> {
|
||||
}
|
||||
|
||||
export function getMetricsForIngress(ingress: string, namespace: string): Promise<IIngressMetrics> {
|
||||
const opts = { category: "ingress", ingress };
|
||||
|
||||
return metricsApi.getMetrics({
|
||||
bytesSentSuccess: opts,
|
||||
bytesSentFailure: opts,
|
||||
requestDurationSeconds: opts,
|
||||
responseDurationSeconds: opts
|
||||
}, {
|
||||
namespace,
|
||||
});
|
||||
}
|
||||
|
||||
export interface IIngressMetrics<T = IMetrics> {
|
||||
[metric: string]: T;
|
||||
bytesSentSuccess: T;
|
||||
bytesSentFailure: T;
|
||||
requestDurationSeconds: T;
|
||||
responseDurationSeconds: T;
|
||||
}
|
||||
|
||||
export interface ILoadBalancerIngress {
|
||||
hostname?: string;
|
||||
ip?: string;
|
||||
}
|
||||
|
||||
// extensions/v1beta1
|
||||
interface IExtensionsBackend {
|
||||
serviceName: string;
|
||||
servicePort: number;
|
||||
}
|
||||
|
||||
// networking.k8s.io/v1
|
||||
interface INetworkingBackend {
|
||||
service: IIngressService;
|
||||
}
|
||||
|
||||
export type IIngressBackend = IExtensionsBackend | INetworkingBackend;
|
||||
|
||||
export interface IIngressService {
|
||||
name: string;
|
||||
port: {
|
||||
name?: string;
|
||||
number?: number;
|
||||
}
|
||||
}
|
||||
|
||||
export const getBackendServiceNamePort = (backend: IIngressBackend) => {
|
||||
// .service is available with networking.k8s.io/v1, otherwise using extensions/v1beta1 interface
|
||||
const serviceName = "service" in backend ? backend.service.name : backend.serviceName;
|
||||
// Port is specified either with a number or name
|
||||
const servicePort = "service" in backend ? backend.service.port.number ?? backend.service.port.name : backend.servicePort;
|
||||
|
||||
return { serviceName, servicePort };
|
||||
};
|
||||
|
||||
export interface Ingress {
|
||||
spec: {
|
||||
tls: {
|
||||
secretName: string;
|
||||
}[];
|
||||
rules?: {
|
||||
host?: string;
|
||||
http: {
|
||||
paths: {
|
||||
path?: string;
|
||||
backend: IIngressBackend;
|
||||
}[];
|
||||
};
|
||||
}[];
|
||||
// extensions/v1beta1
|
||||
backend?: IExtensionsBackend;
|
||||
// networking.k8s.io/v1
|
||||
defaultBackend?: INetworkingBackend & {
|
||||
resource: {
|
||||
apiGroup: string;
|
||||
kind: string;
|
||||
name: string;
|
||||
}
|
||||
}
|
||||
};
|
||||
status: {
|
||||
loadBalancer: {
|
||||
ingress: ILoadBalancerIngress[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export class Ingress extends KubeObject {
|
||||
static kind = "Ingress";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/networking.k8s.io/v1/ingresses";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
getRoutes() {
|
||||
const { spec: { tls, rules } } = this;
|
||||
|
||||
if (!rules) return [];
|
||||
|
||||
let protocol = "http";
|
||||
const routes: string[] = [];
|
||||
|
||||
if (tls && tls.length > 0) {
|
||||
protocol += "s";
|
||||
}
|
||||
rules.map(rule => {
|
||||
const host = rule.host ? rule.host : "*";
|
||||
|
||||
if (rule.http && rule.http.paths) {
|
||||
rule.http.paths.forEach(path => {
|
||||
const { serviceName, servicePort } = getBackendServiceNamePort(path.backend);
|
||||
|
||||
routes.push(`${protocol}://${host}${path.path || "/"} ⇢ ${serviceName}:${servicePort}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
getServiceNamePort() {
|
||||
const { spec } = this;
|
||||
const serviceName = spec?.defaultBackend?.service.name ?? spec?.backend?.serviceName;
|
||||
const servicePort = spec?.defaultBackend?.service.port.number ?? spec?.defaultBackend?.service.port.name ?? spec?.backend?.servicePort;
|
||||
|
||||
return {
|
||||
serviceName,
|
||||
servicePort
|
||||
};
|
||||
}
|
||||
|
||||
getHosts() {
|
||||
const { spec: { rules } } = this;
|
||||
|
||||
if (!rules) return [];
|
||||
|
||||
return rules.filter(rule => rule.host).map(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 && rules.length > 0) {
|
||||
if (rules.some(rule => rule.hasOwnProperty("http"))) {
|
||||
ports.push(httpPort);
|
||||
}
|
||||
} else if (servicePort !== undefined) {
|
||||
ports.push(Number(servicePort));
|
||||
}
|
||||
|
||||
if (tls && tls.length > 0) {
|
||||
ports.push(tlsPort);
|
||||
}
|
||||
|
||||
return ports.join(", ");
|
||||
}
|
||||
|
||||
getLoadBalancers() {
|
||||
const { status: { loadBalancer = { ingress: [] } } } = this;
|
||||
|
||||
return (loadBalancer.ingress ?? []).map(address => (
|
||||
address.hostname || address.ip
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let ingressApi: IngressApi;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
ingressApi = new IngressApi({
|
||||
objectConstructor: Ingress,
|
||||
// Add fallback for Kubernetes <1.19
|
||||
checkPreferredVersion: true,
|
||||
fallbackApiBases: ["/apis/extensions/v1beta1/ingresses"],
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
ingressApi
|
||||
};
|
||||
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import get from "lodash/get";
|
||||
import { autoBind } from "../../utils";
|
||||
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { metricsApi } from "./metrics.api";
|
||||
import type { JsonApiParams } from "../json-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import type { IPodContainer, IPodMetrics } from "./pods.api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export class Job extends WorkloadKubeObject {
|
||||
static kind = "Job";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/batch/v1/jobs";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
declare spec: {
|
||||
parallelism?: number;
|
||||
completions?: number;
|
||||
backoffLimit?: number;
|
||||
selector?: {
|
||||
matchLabels: {
|
||||
[name: string]: string;
|
||||
};
|
||||
};
|
||||
template: {
|
||||
metadata: {
|
||||
creationTimestamp?: string;
|
||||
labels?: {
|
||||
[name: string]: string;
|
||||
};
|
||||
annotations?: {
|
||||
[name: string]: string;
|
||||
};
|
||||
};
|
||||
spec: {
|
||||
containers: IPodContainer[];
|
||||
restartPolicy: string;
|
||||
terminationGracePeriodSeconds: number;
|
||||
dnsPolicy: string;
|
||||
hostPID: boolean;
|
||||
affinity?: IAffinity;
|
||||
nodeSelector?: {
|
||||
[selector: string]: string;
|
||||
};
|
||||
tolerations?: {
|
||||
key: string;
|
||||
operator: string;
|
||||
effect: string;
|
||||
tolerationSeconds: number;
|
||||
}[];
|
||||
schedulerName: string;
|
||||
};
|
||||
};
|
||||
containers?: IPodContainer[];
|
||||
restartPolicy?: string;
|
||||
terminationGracePeriodSeconds?: number;
|
||||
dnsPolicy?: string;
|
||||
serviceAccountName?: string;
|
||||
serviceAccount?: string;
|
||||
schedulerName?: string;
|
||||
};
|
||||
declare status: {
|
||||
conditions: {
|
||||
type: string;
|
||||
status: string;
|
||||
lastProbeTime: string;
|
||||
lastTransitionTime: string;
|
||||
message?: string;
|
||||
}[];
|
||||
startTime: string;
|
||||
completionTime: string;
|
||||
succeeded: number;
|
||||
};
|
||||
|
||||
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.14/#jobcondition-v1-batch
|
||||
return this.status.conditions?.find(({ status }) => status === "True");
|
||||
}
|
||||
|
||||
getImages() {
|
||||
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []);
|
||||
|
||||
return [...containers].map(container => container.image);
|
||||
}
|
||||
|
||||
delete() {
|
||||
const params: JsonApiParams = {
|
||||
query: { propagationPolicy: "Background" }
|
||||
};
|
||||
|
||||
return super.delete(params);
|
||||
}
|
||||
}
|
||||
|
||||
export class JobApi extends KubeApi<Job> {
|
||||
}
|
||||
|
||||
export function getMetricsForJobs(jobs: Job[], namespace: string, selector = ""): Promise<IPodMetrics> {
|
||||
const podSelector = jobs.map(job => `${job.getName()}-[[:alnum:]]{5}`).join("|");
|
||||
const opts = { category: "pods", pods: podSelector, namespace, selector };
|
||||
|
||||
return metricsApi.getMetrics({
|
||||
cpuUsage: opts,
|
||||
memoryUsage: opts,
|
||||
fsUsage: opts,
|
||||
networkReceive: opts,
|
||||
networkTransmit: opts,
|
||||
}, {
|
||||
namespace,
|
||||
});
|
||||
}
|
||||
|
||||
let jobApi: JobApi;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
jobApi = new JobApi({
|
||||
objectConstructor: Job,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
jobApi
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { autoBind } from "../../utils";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export enum LimitType {
|
||||
CONTAINER = "Container",
|
||||
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 LimitRange {
|
||||
spec: {
|
||||
limits: LimitRangeItem[];
|
||||
};
|
||||
}
|
||||
|
||||
export class LimitRange extends KubeObject {
|
||||
static kind = "LimitRange";
|
||||
static namespaced = true;
|
||||
static apiBase = "/api/v1/limitranges";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
let limitRangeApi: KubeApi<LimitRange>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
limitRangeApi = new KubeApi<LimitRange>({
|
||||
objectConstructor: LimitRange,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
limitRangeApi
|
||||
};
|
||||
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
// Metrics api
|
||||
|
||||
import moment from "moment";
|
||||
import { apiBase } from "../index";
|
||||
import type { IMetricsQuery } from "../../../main/routes/metrics-route";
|
||||
|
||||
export interface IMetrics {
|
||||
status: string;
|
||||
data: {
|
||||
resultType: string;
|
||||
result: IMetricsResult[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface IMetricsResult {
|
||||
metric: {
|
||||
[name: string]: string;
|
||||
instance: string;
|
||||
node?: string;
|
||||
pod?: string;
|
||||
kubernetes?: string;
|
||||
kubernetes_node?: string;
|
||||
kubernetes_namespace?: string;
|
||||
};
|
||||
values: [number, string][];
|
||||
}
|
||||
|
||||
export interface MetricProviderInfo {
|
||||
name: string;
|
||||
id: string;
|
||||
isConfigurable: boolean;
|
||||
}
|
||||
|
||||
export interface IMetricsReqParams {
|
||||
start?: number | string; // timestamp in seconds or valid date-string
|
||||
end?: number | string;
|
||||
step?: number; // step in seconds (default: 60s = each point 1m)
|
||||
range?: number; // time-range in seconds for data aggregation (default: 3600s = last 1h)
|
||||
namespace?: string; // rbac-proxy validation param
|
||||
}
|
||||
|
||||
export interface IResourceMetrics<T extends IMetrics> {
|
||||
[metric: string]: T;
|
||||
cpuUsage: T;
|
||||
memoryUsage: T;
|
||||
fsUsage: T;
|
||||
networkReceive: T;
|
||||
networkTransmit: T;
|
||||
}
|
||||
|
||||
export const metricsApi = {
|
||||
async getMetrics<T = IMetricsQuery>(query: T, reqParams: IMetricsReqParams = {}): Promise<T extends object ? { [K in keyof T]: IMetrics } : IMetrics> {
|
||||
const { range = 3600, step = 60, namespace } = reqParams;
|
||||
let { start, end } = reqParams;
|
||||
|
||||
if (!start && !end) {
|
||||
const timeNow = Date.now() / 1000;
|
||||
const now = moment.unix(timeNow).startOf("minute").unix(); // round date to minutes
|
||||
|
||||
start = now - range;
|
||||
end = now;
|
||||
}
|
||||
|
||||
return apiBase.post("/metrics", {
|
||||
data: query,
|
||||
query: {
|
||||
start, end, step,
|
||||
"kubernetes_namespace": namespace,
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async getMetricProviders(): Promise<MetricProviderInfo[]> {
|
||||
return apiBase.get("/metrics/providers");
|
||||
}
|
||||
};
|
||||
|
||||
export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics {
|
||||
if (!metrics?.data?.result) {
|
||||
return {
|
||||
data: {
|
||||
resultType: "",
|
||||
result: [{
|
||||
metric: {},
|
||||
values: []
|
||||
} as IMetricsResult],
|
||||
},
|
||||
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 IMetricsResult);
|
||||
}
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
export function isMetricsEmpty(metrics: Record<string, IMetrics>) {
|
||||
return Object.values(metrics).every(metric => !metric?.data?.result?.length);
|
||||
}
|
||||
|
||||
export function getItemMetrics(metrics: Record<string, IMetrics>, itemName: string): Record<string, IMetrics> | void {
|
||||
if (!metrics) return;
|
||||
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);
|
||||
|
||||
itemMetrics[metric].data.result = result ? [result] : [];
|
||||
}
|
||||
|
||||
return itemMetrics;
|
||||
}
|
||||
|
||||
export function getMetricLastPoints(metrics: Record<string, IMetrics>) {
|
||||
const result: Partial<{ [metric: string]: number }> = {};
|
||||
|
||||
Object.keys(metrics).forEach(metricName => {
|
||||
try {
|
||||
const metric = metrics[metricName];
|
||||
|
||||
if (metric.data.result.length) {
|
||||
result[metricName] = +metric.data.result[0].values.slice(-1)[0][1];
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { autoBind } from "../../../renderer/utils";
|
||||
import { metricsApi } from "./metrics.api";
|
||||
import type { IPodMetrics } from "./pods.api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export enum NamespaceStatus {
|
||||
ACTIVE = "Active",
|
||||
TERMINATING = "Terminating",
|
||||
}
|
||||
|
||||
export interface Namespace {
|
||||
status?: {
|
||||
phase: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class Namespace extends KubeObject {
|
||||
static kind = "Namespace";
|
||||
static namespaced = false;
|
||||
static apiBase = "/api/v1/namespaces";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return this.status?.phase ?? "-";
|
||||
}
|
||||
}
|
||||
|
||||
export class NamespaceApi extends KubeApi<Namespace> {
|
||||
}
|
||||
|
||||
export function getMetricsForNamespace(namespace: string, selector = ""): Promise<IPodMetrics> {
|
||||
const opts = { category: "pods", pods: ".*", namespace, selector };
|
||||
|
||||
return metricsApi.getMetrics({
|
||||
cpuUsage: opts,
|
||||
memoryUsage: opts,
|
||||
fsUsage: opts,
|
||||
networkReceive: opts,
|
||||
networkTransmit: opts,
|
||||
}, {
|
||||
namespace,
|
||||
});
|
||||
}
|
||||
|
||||
let namespacesApi: NamespaceApi;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
namespacesApi = new NamespaceApi({
|
||||
objectConstructor: Namespace,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
namespacesApi
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { autoBind } from "../../utils";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export interface IPolicyIpBlock {
|
||||
cidr: string;
|
||||
except?: string[];
|
||||
}
|
||||
|
||||
export interface IPolicySelector {
|
||||
matchLabels: {
|
||||
[label: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IPolicyIngress {
|
||||
from: {
|
||||
ipBlock?: IPolicyIpBlock;
|
||||
namespaceSelector?: IPolicySelector;
|
||||
podSelector?: IPolicySelector;
|
||||
}[];
|
||||
ports: {
|
||||
protocol: string;
|
||||
port: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface IPolicyEgress {
|
||||
to: {
|
||||
ipBlock: IPolicyIpBlock;
|
||||
}[];
|
||||
ports: {
|
||||
protocol: string;
|
||||
port: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface NetworkPolicy {
|
||||
spec: {
|
||||
podSelector: {
|
||||
matchLabels: {
|
||||
[label: string]: string;
|
||||
role: string;
|
||||
};
|
||||
};
|
||||
policyTypes: string[];
|
||||
ingress: IPolicyIngress[];
|
||||
egress: IPolicyEgress[];
|
||||
};
|
||||
}
|
||||
|
||||
export class NetworkPolicy extends KubeObject {
|
||||
static kind = "NetworkPolicy";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/networking.k8s.io/v1/networkpolicies";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
let networkPolicyApi: KubeApi<NetworkPolicy>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
networkPolicyApi = new KubeApi<NetworkPolicy>({
|
||||
objectConstructor: NetworkPolicy,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
networkPolicyApi
|
||||
};
|
||||
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { autoBind, cpuUnitsToNumber, unitsToBytes } from "../../../renderer/utils";
|
||||
import { IMetrics, metricsApi } from "./metrics.api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export class NodesApi extends KubeApi<Node> {
|
||||
}
|
||||
|
||||
export function getMetricsForAllNodes(): Promise<INodeMetrics> {
|
||||
const opts = { category: "nodes"};
|
||||
|
||||
return metricsApi.getMetrics({
|
||||
memoryUsage: opts,
|
||||
workloadMemoryUsage: opts,
|
||||
memoryCapacity: opts,
|
||||
memoryAllocatableCapacity: opts,
|
||||
cpuUsage: opts,
|
||||
cpuCapacity: opts,
|
||||
fsSize: opts,
|
||||
fsUsage: opts
|
||||
});
|
||||
}
|
||||
|
||||
export interface INodeMetrics<T = IMetrics> {
|
||||
[metric: string]: T;
|
||||
memoryUsage: T;
|
||||
workloadMemoryUsage: T;
|
||||
memoryCapacity: T;
|
||||
memoryAllocatableCapacity: T;
|
||||
cpuUsage: T;
|
||||
cpuCapacity: T;
|
||||
fsUsage: T;
|
||||
fsSize: T;
|
||||
}
|
||||
|
||||
export interface Node {
|
||||
spec: {
|
||||
podCIDR?: string;
|
||||
podCIDRs?: string[];
|
||||
providerID?: string;
|
||||
/**
|
||||
* @deprecated see https://issues.k8s.io/61966
|
||||
*/
|
||||
externalID?: string;
|
||||
taints?: {
|
||||
key: string;
|
||||
value: string;
|
||||
effect: string;
|
||||
timeAdded: string;
|
||||
}[];
|
||||
unschedulable?: boolean;
|
||||
};
|
||||
status: {
|
||||
capacity?: {
|
||||
cpu: string;
|
||||
["ephemeral-storage"]: string;
|
||||
["hugepages-1Gi"]: string;
|
||||
["hugepages-2Mi"]: string;
|
||||
memory: string;
|
||||
pods: string;
|
||||
};
|
||||
allocatable?: {
|
||||
cpu: string;
|
||||
["ephemeral-storage"]: string;
|
||||
["hugepages-1Gi"]: string;
|
||||
["hugepages-2Mi"]: string;
|
||||
memory: string;
|
||||
pods: string;
|
||||
};
|
||||
conditions?: {
|
||||
type: string;
|
||||
status: string;
|
||||
lastHeartbeatTime?: string;
|
||||
lastTransitionTime?: string;
|
||||
reason?: string;
|
||||
message?: string;
|
||||
}[];
|
||||
addresses?: {
|
||||
type: string;
|
||||
address: string;
|
||||
}[];
|
||||
daemonEndpoints?: {
|
||||
kubeletEndpoint: {
|
||||
Port: number; //it must be uppercase for backwards compatibility
|
||||
}
|
||||
}
|
||||
nodeInfo?: {
|
||||
machineID: string;
|
||||
systemUUID: string;
|
||||
bootID: string;
|
||||
kernelVersion: string;
|
||||
osImage: string;
|
||||
containerRuntimeVersion: string;
|
||||
kubeletVersion: string;
|
||||
kubeProxyVersion: string;
|
||||
operatingSystem: string;
|
||||
architecture: string;
|
||||
};
|
||||
images?: {
|
||||
names: string[];
|
||||
sizeBytes?: number;
|
||||
}[];
|
||||
volumesInUse?: string[];
|
||||
volumesAttached?: {
|
||||
name: string;
|
||||
devicePath: string;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
|
||||
export class Node extends KubeObject {
|
||||
static kind = "Node";
|
||||
static namespaced = false;
|
||||
static apiBase = "/api/v1/nodes";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
getNodeConditionText() {
|
||||
const { conditions } = this.status;
|
||||
|
||||
if (!conditions) return "";
|
||||
|
||||
return conditions.reduce((types, current) => {
|
||||
if (current.status !== "True") return "";
|
||||
|
||||
return types += ` ${current.type}`;
|
||||
}, "");
|
||||
}
|
||||
|
||||
getTaints() {
|
||||
return this.spec.taints || [];
|
||||
}
|
||||
|
||||
getRoleLabels() {
|
||||
if (!this.metadata?.labels || typeof this.metadata.labels !== "object") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const roleLabels = Object.keys(this.metadata.labels)
|
||||
.filter(key => key.includes("node-role.kubernetes.io"))
|
||||
.map(key => key.match(/([^/]+$)/)[0]); // all after last slash
|
||||
|
||||
if (this.metadata.labels["kubernetes.io/role"] != undefined) {
|
||||
roleLabels.push(this.metadata.labels["kubernetes.io/role"]);
|
||||
}
|
||||
|
||||
return roleLabels.join(", ");
|
||||
}
|
||||
|
||||
getCpuCapacity() {
|
||||
if (!this.status.capacity || !this.status.capacity.cpu) return 0;
|
||||
|
||||
return cpuUnitsToNumber(this.status.capacity.cpu);
|
||||
}
|
||||
|
||||
getMemoryCapacity() {
|
||||
if (!this.status.capacity || !this.status.capacity.memory) return 0;
|
||||
|
||||
return unitsToBytes(this.status.capacity.memory);
|
||||
}
|
||||
|
||||
getConditions() {
|
||||
const conditions = this.status.conditions || [];
|
||||
|
||||
if (this.isUnschedulable()) {
|
||||
return [{ type: "SchedulingDisabled", status: "True" }, ...conditions];
|
||||
}
|
||||
|
||||
return conditions;
|
||||
}
|
||||
|
||||
getActiveConditions() {
|
||||
return this.getConditions().filter(c => c.status === "True");
|
||||
}
|
||||
|
||||
getWarningConditions() {
|
||||
const goodConditions = ["Ready", "HostUpgrades", "SchedulingDisabled"];
|
||||
|
||||
return this.getActiveConditions().filter(condition => {
|
||||
return !goodConditions.includes(condition.type);
|
||||
});
|
||||
}
|
||||
|
||||
getKubeletVersion() {
|
||||
return this.status.nodeInfo.kubeletVersion;
|
||||
}
|
||||
|
||||
getOperatingSystem(): string {
|
||||
const label = this.getLabels().find(label => label.startsWith("kubernetes.io/os="));
|
||||
|
||||
if (label) {
|
||||
return label.split("=", 2)[1];
|
||||
}
|
||||
|
||||
return "linux";
|
||||
}
|
||||
|
||||
isUnschedulable() {
|
||||
return this.spec.unschedulable;
|
||||
}
|
||||
}
|
||||
|
||||
let nodesApi: NodesApi;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
nodesApi = new NodesApi({
|
||||
objectConstructor: Node,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
nodesApi
|
||||
};
|
||||
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { autoBind } from "../../utils";
|
||||
import { IMetrics, metricsApi } from "./metrics.api";
|
||||
import type { Pod } from "./pods.api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export class PersistentVolumeClaimsApi extends KubeApi<PersistentVolumeClaim> {
|
||||
}
|
||||
|
||||
export function getMetricsForPvc(pvc: PersistentVolumeClaim): Promise<IPvcMetrics> {
|
||||
return metricsApi.getMetrics({
|
||||
diskUsage: { category: "pvc", pvc: pvc.getName() },
|
||||
diskCapacity: { category: "pvc", pvc: pvc.getName() }
|
||||
}, {
|
||||
namespace: pvc.getNs()
|
||||
});
|
||||
}
|
||||
|
||||
export interface IPvcMetrics<T = IMetrics> {
|
||||
[key: string]: T;
|
||||
diskUsage: T;
|
||||
diskCapacity: T;
|
||||
}
|
||||
|
||||
export interface PersistentVolumeClaim {
|
||||
spec: {
|
||||
accessModes: string[];
|
||||
storageClassName: string;
|
||||
selector: {
|
||||
matchLabels: {
|
||||
release: string;
|
||||
};
|
||||
matchExpressions: {
|
||||
key: string; // environment,
|
||||
operator: string; // In,
|
||||
values: string[]; // [dev]
|
||||
}[];
|
||||
};
|
||||
resources: {
|
||||
requests: {
|
||||
storage: string; // 8Gi
|
||||
};
|
||||
};
|
||||
};
|
||||
status: {
|
||||
phase: string; // Pending
|
||||
};
|
||||
}
|
||||
|
||||
export class PersistentVolumeClaim extends KubeObject {
|
||||
static kind = "PersistentVolumeClaim";
|
||||
static namespaced = true;
|
||||
static apiBase = "/api/v1/persistentvolumeclaims";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
getPods(allPods: Pod[]): Pod[] {
|
||||
const pods = allPods.filter(pod => pod.getNs() === this.getNs());
|
||||
|
||||
return pods.filter(pod => {
|
||||
return pod.getVolumes().filter(volume =>
|
||||
volume.persistentVolumeClaim &&
|
||||
volume.persistentVolumeClaim.claimName === this.getName()
|
||||
).length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
getStorage(): string {
|
||||
if (!this.spec.resources || !this.spec.resources.requests) return "-";
|
||||
|
||||
return this.spec.resources.requests.storage;
|
||||
}
|
||||
|
||||
getMatchLabels(): string[] {
|
||||
if (!this.spec.selector || !this.spec.selector.matchLabels) return [];
|
||||
|
||||
return Object.entries(this.spec.selector.matchLabels)
|
||||
.map(([name, val]) => `${name}:${val}`);
|
||||
}
|
||||
|
||||
getMatchExpressions() {
|
||||
if (!this.spec.selector || !this.spec.selector.matchExpressions) return [];
|
||||
|
||||
return this.spec.selector.matchExpressions;
|
||||
}
|
||||
|
||||
getStatus(): string {
|
||||
if (this.status) return this.status.phase;
|
||||
|
||||
return "-";
|
||||
}
|
||||
}
|
||||
|
||||
let pvcApi: PersistentVolumeClaimsApi;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
pvcApi = new PersistentVolumeClaimsApi({
|
||||
objectConstructor: PersistentVolumeClaim,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
pvcApi
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { autoBind, unitsToBytes } from "../../utils";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export interface PersistentVolume {
|
||||
spec: {
|
||||
capacity: {
|
||||
storage: string; // 8Gi
|
||||
};
|
||||
flexVolume: {
|
||||
driver: string; // ceph.rook.io/rook-ceph-system,
|
||||
options: {
|
||||
clusterNamespace: string; // rook-ceph,
|
||||
image: string; // pvc-c5d7c485-9f1b-11e8-b0ea-9600000e54fb,
|
||||
pool: string; // replicapool,
|
||||
storageClass: string; // rook-ceph-block
|
||||
};
|
||||
};
|
||||
mountOptions?: string[];
|
||||
accessModes: string[]; // [ReadWriteOnce]
|
||||
claimRef: {
|
||||
kind: string; // PersistentVolumeClaim,
|
||||
namespace: string; // storage,
|
||||
name: string; // nfs-provisioner,
|
||||
uid: string; // c5d7c485-9f1b-11e8-b0ea-9600000e54fb,
|
||||
apiVersion: string; // v1,
|
||||
resourceVersion: string; // 292180
|
||||
};
|
||||
persistentVolumeReclaimPolicy: string; // Delete,
|
||||
storageClassName: string; // rook-ceph-block
|
||||
nfs?: {
|
||||
path: string;
|
||||
server: string;
|
||||
};
|
||||
};
|
||||
|
||||
status?: {
|
||||
phase: string;
|
||||
reason?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class PersistentVolume extends KubeObject {
|
||||
static kind = "PersistentVolume";
|
||||
static namespaced = false;
|
||||
static apiBase = "/api/v1/persistentvolumes";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
getCapacity(inBytes = false) {
|
||||
const capacity = this.spec.capacity;
|
||||
|
||||
if (capacity) {
|
||||
if (inBytes) return unitsToBytes(capacity.storage);
|
||||
|
||||
return capacity.storage;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return this.status?.phase || "-";
|
||||
}
|
||||
|
||||
getStorageClass(): string {
|
||||
return this.spec.storageClassName;
|
||||
}
|
||||
|
||||
getClaimRefName(): string {
|
||||
return this.spec.claimRef?.name ?? "";
|
||||
}
|
||||
|
||||
getStorageClassName() {
|
||||
return this.spec.storageClassName || "";
|
||||
}
|
||||
}
|
||||
|
||||
let persistentVolumeApi: KubeApi<PersistentVolume>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
persistentVolumeApi = new KubeApi({
|
||||
objectConstructor: PersistentVolume,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
persistentVolumeApi
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export interface PodMetrics {
|
||||
timestamp: string;
|
||||
window: string;
|
||||
containers: {
|
||||
name: string;
|
||||
usage: {
|
||||
cpu: string;
|
||||
memory: string;
|
||||
};
|
||||
}[];
|
||||
}
|
||||
|
||||
export class PodMetrics extends KubeObject {
|
||||
static kind = "PodMetrics";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/metrics.k8s.io/v1beta1/pods";
|
||||
}
|
||||
|
||||
let podMetricsApi: KubeApi<PodMetrics>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
podMetricsApi = new KubeApi<PodMetrics>({
|
||||
objectConstructor: PodMetrics,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
podMetricsApi
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { autoBind } from "../../utils";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export interface PodDisruptionBudget {
|
||||
spec: {
|
||||
minAvailable: string;
|
||||
maxUnavailable: string;
|
||||
selector: { matchLabels: { [app: string]: string } };
|
||||
};
|
||||
status: {
|
||||
currentHealthy: number
|
||||
desiredHealthy: number
|
||||
disruptionsAllowed: number
|
||||
expectedPods: number
|
||||
};
|
||||
}
|
||||
|
||||
export class PodDisruptionBudget extends KubeObject {
|
||||
static kind = "PodDisruptionBudget";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/policy/v1beta1/poddisruptionbudgets";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
getSelectors() {
|
||||
const selector = this.spec.selector;
|
||||
|
||||
return KubeObject.stringifyLabels(selector ? selector.matchLabels : null);
|
||||
}
|
||||
|
||||
getMinAvailable() {
|
||||
return this.spec.minAvailable || "N/A";
|
||||
}
|
||||
|
||||
getMaxUnavailable() {
|
||||
return this.spec.maxUnavailable || "N/A";
|
||||
}
|
||||
|
||||
getCurrentHealthy() {
|
||||
return this.status.currentHealthy;
|
||||
}
|
||||
|
||||
getDesiredHealthy() {
|
||||
return this.status.desiredHealthy;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let pdbApi: KubeApi<PodDisruptionBudget>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
pdbApi = new KubeApi({
|
||||
objectConstructor: PodDisruptionBudget,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
pdbApi
|
||||
};
|
||||
@@ -0,0 +1,516 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
|
||||
import { autoBind } from "../../utils";
|
||||
import { IMetrics, metricsApi } from "./metrics.api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export class PodsApi extends KubeApi<Pod> {
|
||||
async getLogs(params: { namespace: string; name: string }, query?: IPodLogsQuery): Promise<string> {
|
||||
const path = `${this.getUrl(params)}/log`;
|
||||
|
||||
return this.request.get(path, { query });
|
||||
}
|
||||
}
|
||||
|
||||
export function getMetricsForPods(pods: Pod[], namespace: string, selector = "pod, namespace"): Promise<IPodMetrics> {
|
||||
const podSelector = pods.map(pod => pod.getName()).join("|");
|
||||
const opts = { category: "pods", pods: podSelector, namespace, selector };
|
||||
|
||||
return metricsApi.getMetrics({
|
||||
cpuUsage: opts,
|
||||
cpuRequests: opts,
|
||||
cpuLimits: opts,
|
||||
memoryUsage: opts,
|
||||
memoryRequests: opts,
|
||||
memoryLimits: opts,
|
||||
fsUsage: opts,
|
||||
networkReceive: opts,
|
||||
networkTransmit: opts,
|
||||
}, {
|
||||
namespace,
|
||||
});
|
||||
}
|
||||
|
||||
export interface IPodMetrics<T = IMetrics> {
|
||||
[metric: string]: T;
|
||||
cpuUsage: T;
|
||||
memoryUsage: T;
|
||||
fsUsage: T;
|
||||
networkReceive: T;
|
||||
networkTransmit: T;
|
||||
cpuRequests?: T;
|
||||
cpuLimits?: T;
|
||||
memoryRequests?: T;
|
||||
memoryLimits?: T;
|
||||
}
|
||||
|
||||
// Reference: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#read-log-pod-v1-core
|
||||
export interface IPodLogsQuery {
|
||||
container?: string;
|
||||
tailLines?: number;
|
||||
timestamps?: boolean;
|
||||
sinceTime?: string; // Date.toISOString()-format
|
||||
follow?: boolean;
|
||||
previous?: boolean;
|
||||
}
|
||||
|
||||
export enum PodStatus {
|
||||
TERMINATED = "Terminated",
|
||||
FAILED = "Failed",
|
||||
PENDING = "Pending",
|
||||
RUNNING = "Running",
|
||||
SUCCEEDED = "Succeeded",
|
||||
EVICTED = "Evicted"
|
||||
}
|
||||
|
||||
export interface IPodContainer {
|
||||
name: string;
|
||||
image: string;
|
||||
command?: string[];
|
||||
args?: string[];
|
||||
ports?: {
|
||||
name?: string;
|
||||
containerPort: number;
|
||||
protocol: string;
|
||||
}[];
|
||||
resources?: {
|
||||
limits: {
|
||||
cpu: string;
|
||||
memory: string;
|
||||
};
|
||||
requests: {
|
||||
cpu: string;
|
||||
memory: string;
|
||||
};
|
||||
};
|
||||
env?: {
|
||||
name: string;
|
||||
value?: string;
|
||||
valueFrom?: {
|
||||
fieldRef?: {
|
||||
apiVersion: string;
|
||||
fieldPath: string;
|
||||
};
|
||||
secretKeyRef?: {
|
||||
key: string;
|
||||
name: string;
|
||||
};
|
||||
configMapKeyRef?: {
|
||||
key: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
}[];
|
||||
envFrom?: {
|
||||
configMapRef?: {
|
||||
name: string;
|
||||
};
|
||||
secretRef?: {
|
||||
name: string;
|
||||
}
|
||||
}[];
|
||||
volumeMounts?: {
|
||||
name: string;
|
||||
readOnly: boolean;
|
||||
mountPath: string;
|
||||
}[];
|
||||
livenessProbe?: IContainerProbe;
|
||||
readinessProbe?: IContainerProbe;
|
||||
startupProbe?: IContainerProbe;
|
||||
imagePullPolicy: string;
|
||||
}
|
||||
|
||||
interface IContainerProbe {
|
||||
httpGet?: {
|
||||
path?: string;
|
||||
port: number;
|
||||
scheme: string;
|
||||
host?: string;
|
||||
};
|
||||
exec?: {
|
||||
command: string[];
|
||||
};
|
||||
tcpSocket?: {
|
||||
port: number;
|
||||
};
|
||||
initialDelaySeconds?: number;
|
||||
timeoutSeconds?: number;
|
||||
periodSeconds?: number;
|
||||
successThreshold?: number;
|
||||
failureThreshold?: number;
|
||||
}
|
||||
|
||||
export interface IPodContainerStatus {
|
||||
name: string;
|
||||
state?: {
|
||||
[index: string]: object;
|
||||
running?: {
|
||||
startedAt: string;
|
||||
};
|
||||
waiting?: {
|
||||
reason: string;
|
||||
message: string;
|
||||
};
|
||||
terminated?: {
|
||||
startedAt: string;
|
||||
finishedAt: string;
|
||||
exitCode: number;
|
||||
reason: string;
|
||||
};
|
||||
};
|
||||
lastState?: {
|
||||
[index: string]: object;
|
||||
running?: {
|
||||
startedAt: string;
|
||||
};
|
||||
waiting?: {
|
||||
reason: string;
|
||||
message: string;
|
||||
};
|
||||
terminated?: {
|
||||
startedAt: string;
|
||||
finishedAt: string;
|
||||
exitCode: number;
|
||||
reason: string;
|
||||
};
|
||||
};
|
||||
ready: boolean;
|
||||
restartCount: number;
|
||||
image: string;
|
||||
imageID: string;
|
||||
containerID?: string;
|
||||
started?: boolean;
|
||||
}
|
||||
|
||||
export class Pod extends WorkloadKubeObject {
|
||||
static kind = "Pod";
|
||||
static namespaced = true;
|
||||
static apiBase = "/api/v1/pods";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
declare spec?: {
|
||||
volumes?: {
|
||||
name: string;
|
||||
persistentVolumeClaim: {
|
||||
claimName: string;
|
||||
};
|
||||
emptyDir: {
|
||||
medium?: string;
|
||||
sizeLimit?: string;
|
||||
};
|
||||
configMap: {
|
||||
name: string;
|
||||
};
|
||||
secret: {
|
||||
secretName: string;
|
||||
defaultMode: number;
|
||||
};
|
||||
}[];
|
||||
initContainers: IPodContainer[];
|
||||
containers: IPodContainer[];
|
||||
restartPolicy?: string;
|
||||
terminationGracePeriodSeconds?: number;
|
||||
activeDeadlineSeconds?: number;
|
||||
dnsPolicy?: string;
|
||||
serviceAccountName: string;
|
||||
serviceAccount: string;
|
||||
automountServiceAccountToken?: boolean;
|
||||
priority?: number;
|
||||
priorityClassName?: string;
|
||||
nodeName?: string;
|
||||
nodeSelector?: {
|
||||
[selector: string]: string;
|
||||
};
|
||||
securityContext?: {};
|
||||
imagePullSecrets?: {
|
||||
name: string;
|
||||
}[];
|
||||
hostNetwork?: boolean;
|
||||
hostPID?: boolean;
|
||||
hostIPC?: boolean;
|
||||
shareProcessNamespace?: boolean;
|
||||
hostname?: string;
|
||||
subdomain?: string;
|
||||
schedulerName?: string;
|
||||
tolerations?: {
|
||||
key?: string;
|
||||
operator?: string;
|
||||
effect?: string;
|
||||
tolerationSeconds?: number;
|
||||
value?: string;
|
||||
}[];
|
||||
hostAliases?: {
|
||||
ip: string;
|
||||
hostnames: string[];
|
||||
};
|
||||
affinity?: IAffinity;
|
||||
};
|
||||
declare status?: {
|
||||
phase: string;
|
||||
conditions: {
|
||||
type: string;
|
||||
status: string;
|
||||
lastProbeTime: number;
|
||||
lastTransitionTime: string;
|
||||
}[];
|
||||
hostIP: string;
|
||||
podIP: string;
|
||||
podIPs?: {
|
||||
ip: string
|
||||
}[];
|
||||
startTime: string;
|
||||
initContainerStatuses?: IPodContainerStatus[];
|
||||
containerStatuses?: IPodContainerStatus[];
|
||||
qosClass?: string;
|
||||
reason?: string;
|
||||
};
|
||||
|
||||
getInitContainers() {
|
||||
return this.spec?.initContainers || [];
|
||||
}
|
||||
|
||||
getContainers() {
|
||||
return this.spec?.containers || [];
|
||||
}
|
||||
|
||||
getAllContainers() {
|
||||
return [...this.getContainers(), ...this.getInitContainers()];
|
||||
}
|
||||
|
||||
getRunningContainers() {
|
||||
const runningContainerNames = new Set(
|
||||
this.getContainerStatuses()
|
||||
.filter(({ state }) => state.running)
|
||||
.map(({ name }) => name)
|
||||
);
|
||||
|
||||
return this.getAllContainers()
|
||||
.filter(({ name }) => runningContainerNames.has(name));
|
||||
}
|
||||
|
||||
getContainerStatuses(includeInitContainers = true) {
|
||||
const { containerStatuses = [], initContainerStatuses = [] } = this.status ?? {};
|
||||
|
||||
if (includeInitContainers) {
|
||||
return [...containerStatuses, ...initContainerStatuses];
|
||||
}
|
||||
|
||||
return [...containerStatuses];
|
||||
}
|
||||
|
||||
getRestartsCount(): number {
|
||||
const { containerStatuses = [] } = this.status ?? {};
|
||||
|
||||
return containerStatuses.reduce((totalCount, { restartCount }) => totalCount + restartCount, 0);
|
||||
}
|
||||
|
||||
getQosClass() {
|
||||
return this.status?.qosClass || "";
|
||||
}
|
||||
|
||||
getReason() {
|
||||
return this.status?.reason || "";
|
||||
}
|
||||
|
||||
getPriorityClassName() {
|
||||
return this.spec.priorityClassName || "";
|
||||
}
|
||||
|
||||
getStatus(): PodStatus {
|
||||
const phase = this.getStatusPhase();
|
||||
const reason = this.getReason();
|
||||
const trueConditionTypes = new Set(this.getConditions()
|
||||
.filter(({ status }) => status === "True")
|
||||
.map(({ type }) => type));
|
||||
const isInGoodCondition = ["Initialized", "Ready"].every(condition => trueConditionTypes.has(condition));
|
||||
|
||||
if (reason === PodStatus.EVICTED) {
|
||||
return PodStatus.EVICTED;
|
||||
}
|
||||
|
||||
if (phase === PodStatus.FAILED) {
|
||||
return PodStatus.FAILED;
|
||||
}
|
||||
|
||||
if (phase === PodStatus.SUCCEEDED) {
|
||||
return PodStatus.SUCCEEDED;
|
||||
}
|
||||
|
||||
if (phase === PodStatus.RUNNING && isInGoodCondition) {
|
||||
return PodStatus.RUNNING;
|
||||
}
|
||||
|
||||
return PodStatus.PENDING;
|
||||
}
|
||||
|
||||
// Returns pod phase or container error if occurred
|
||||
getStatusMessage() {
|
||||
if (this.getReason() === PodStatus.EVICTED) return "Evicted";
|
||||
if (this.metadata.deletionTimestamp) return "Terminating";
|
||||
|
||||
const statuses = this.getContainerStatuses(false); // not including initContainers
|
||||
|
||||
for (const { state } of statuses.reverse()) {
|
||||
if (state.waiting) {
|
||||
return state.waiting.reason || "Waiting";
|
||||
}
|
||||
|
||||
if (state.terminated) {
|
||||
return state.terminated.reason || "Terminated";
|
||||
}
|
||||
}
|
||||
|
||||
return this.getStatusPhase();
|
||||
}
|
||||
|
||||
getStatusPhase() {
|
||||
return this.status?.phase;
|
||||
}
|
||||
|
||||
getConditions() {
|
||||
return this.status?.conditions || [];
|
||||
}
|
||||
|
||||
getVolumes() {
|
||||
return this.spec.volumes || [];
|
||||
}
|
||||
|
||||
getSecrets(): string[] {
|
||||
return this.getVolumes()
|
||||
.filter(vol => vol.secret)
|
||||
.map(vol => vol.secret.secretName);
|
||||
}
|
||||
|
||||
getNodeSelectors(): string[] {
|
||||
const { nodeSelector = {} } = this.spec;
|
||||
|
||||
return Object.entries(nodeSelector).map(values => values.join(": "));
|
||||
}
|
||||
|
||||
getTolerations() {
|
||||
return this.spec.tolerations || [];
|
||||
}
|
||||
|
||||
getAffinity(): IAffinity {
|
||||
return this.spec.affinity;
|
||||
}
|
||||
|
||||
hasIssues() {
|
||||
for (const { type, status } of this.getConditions()) {
|
||||
if (type === "Ready" && status !== "True") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (const { state } of this.getContainerStatuses()) {
|
||||
if (state?.waiting?.reason === "CrashLookBackOff") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return this.getStatusPhase() !== "Running";
|
||||
}
|
||||
|
||||
getLivenessProbe(container: IPodContainer) {
|
||||
return this.getProbe(container.livenessProbe);
|
||||
}
|
||||
|
||||
getReadinessProbe(container: IPodContainer) {
|
||||
return this.getProbe(container.readinessProbe);
|
||||
}
|
||||
|
||||
getStartupProbe(container: IPodContainer) {
|
||||
return this.getProbe(container.startupProbe);
|
||||
}
|
||||
|
||||
getProbe(probeData: IContainerProbe) {
|
||||
if (!probeData) return [];
|
||||
const {
|
||||
httpGet, exec, tcpSocket, initialDelaySeconds, timeoutSeconds,
|
||||
periodSeconds, successThreshold, failureThreshold
|
||||
} = probeData;
|
||||
const probe = [];
|
||||
|
||||
// HTTP Request
|
||||
if (httpGet) {
|
||||
const { path, port, host, scheme } = httpGet;
|
||||
|
||||
probe.push(
|
||||
"http-get",
|
||||
`${scheme.toLowerCase()}://${host || ""}:${port || ""}${path || ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Command
|
||||
if (exec && exec.command) {
|
||||
probe.push(`exec [${exec.command.join(" ")}]`);
|
||||
}
|
||||
|
||||
// TCP Probe
|
||||
if (tcpSocket && tcpSocket.port) {
|
||||
probe.push(`tcp-socket :${tcpSocket.port}`);
|
||||
}
|
||||
probe.push(
|
||||
`delay=${initialDelaySeconds || "0"}s`,
|
||||
`timeout=${timeoutSeconds || "0"}s`,
|
||||
`period=${periodSeconds || "0"}s`,
|
||||
`#success=${successThreshold || "0"}`,
|
||||
`#failure=${failureThreshold || "0"}`,
|
||||
);
|
||||
|
||||
return probe;
|
||||
}
|
||||
|
||||
getNodeName() {
|
||||
return this.spec.nodeName;
|
||||
}
|
||||
|
||||
getSelectedNodeOs(): string | undefined {
|
||||
return this.spec.nodeSelector?.["kubernetes.io/os"] || this.spec.nodeSelector?.["beta.kubernetes.io/os"];
|
||||
}
|
||||
|
||||
getIPs(): string[] {
|
||||
if(!this.status.podIPs) return [];
|
||||
const podIPs = this.status.podIPs;
|
||||
|
||||
return podIPs.map(value => value.ip);
|
||||
}
|
||||
}
|
||||
|
||||
let podsApi: PodsApi;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
podsApi = new PodsApi({
|
||||
objectConstructor: Pod,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
podsApi
|
||||
};
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { autoBind } from "../../utils";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export interface PodSecurityPolicy {
|
||||
spec: {
|
||||
allowPrivilegeEscalation?: boolean;
|
||||
allowedCSIDrivers?: {
|
||||
name: string;
|
||||
}[];
|
||||
allowedCapabilities: string[];
|
||||
allowedFlexVolumes?: {
|
||||
driver: string;
|
||||
}[];
|
||||
allowedHostPaths?: {
|
||||
pathPrefix: string;
|
||||
readOnly: boolean;
|
||||
}[];
|
||||
allowedProcMountTypes?: string[];
|
||||
allowedUnsafeSysctls?: string[];
|
||||
defaultAddCapabilities?: string[];
|
||||
defaultAllowPrivilegeEscalation?: boolean;
|
||||
forbiddenSysctls?: string[];
|
||||
fsGroup?: {
|
||||
rule: string;
|
||||
ranges: { max: number; min: number }[];
|
||||
};
|
||||
hostIPC?: boolean;
|
||||
hostNetwork?: boolean;
|
||||
hostPID?: boolean;
|
||||
hostPorts?: {
|
||||
max: number;
|
||||
min: number;
|
||||
}[];
|
||||
privileged?: boolean;
|
||||
readOnlyRootFilesystem?: boolean;
|
||||
requiredDropCapabilities?: string[];
|
||||
runAsGroup?: {
|
||||
ranges: { max: number; min: number }[];
|
||||
rule: string;
|
||||
};
|
||||
runAsUser?: {
|
||||
rule: string;
|
||||
ranges: { max: number; min: number }[];
|
||||
};
|
||||
runtimeClass?: {
|
||||
allowedRuntimeClassNames: string[];
|
||||
defaultRuntimeClassName: string;
|
||||
};
|
||||
seLinux?: {
|
||||
rule: string;
|
||||
seLinuxOptions: {
|
||||
level: string;
|
||||
role: string;
|
||||
type: string;
|
||||
user: string;
|
||||
};
|
||||
};
|
||||
supplementalGroups?: {
|
||||
rule: string;
|
||||
ranges: { max: number; min: number }[];
|
||||
};
|
||||
volumes?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export class PodSecurityPolicy extends KubeObject {
|
||||
static kind = "PodSecurityPolicy";
|
||||
static namespaced = false;
|
||||
static apiBase = "/apis/policy/v1beta1/podsecuritypolicies";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
isPrivileged() {
|
||||
return !!this.spec.privileged;
|
||||
}
|
||||
|
||||
getVolumes() {
|
||||
return this.spec.volumes || [];
|
||||
}
|
||||
|
||||
getRules() {
|
||||
const { fsGroup, runAsGroup, runAsUser, supplementalGroups, seLinux } = this.spec;
|
||||
|
||||
return {
|
||||
fsGroup: fsGroup ? fsGroup.rule : "",
|
||||
runAsGroup: runAsGroup ? runAsGroup.rule : "",
|
||||
runAsUser: runAsUser ? runAsUser.rule : "",
|
||||
supplementalGroups: supplementalGroups ? supplementalGroups.rule : "",
|
||||
seLinux: seLinux ? seLinux.rule : "",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let pspApi: KubeApi<PodSecurityPolicy>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
pspApi = new KubeApi({
|
||||
objectConstructor: PodSecurityPolicy,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
pspApi
|
||||
};
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import get from "lodash/get";
|
||||
import { autoBind } from "../../../renderer/utils";
|
||||
import { WorkloadKubeObject } from "../workload-kube-object";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { metricsApi } from "./metrics.api";
|
||||
import type { IPodContainer, IPodMetrics, Pod } from "./pods.api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export class ReplicaSetApi extends KubeApi<ReplicaSet> {
|
||||
protected getScaleApiUrl(params: { namespace: string; name: string }) {
|
||||
return `${this.getUrl(params)}/scale`;
|
||||
}
|
||||
|
||||
getReplicas(params: { namespace: string; name: string }): Promise<number> {
|
||||
return this.request
|
||||
.get(this.getScaleApiUrl(params))
|
||||
.then(({ status }: any) => status?.replicas);
|
||||
}
|
||||
|
||||
scale(params: { namespace: string; name: string }, replicas: number) {
|
||||
return this.request.put(this.getScaleApiUrl(params), {
|
||||
data: {
|
||||
metadata: params,
|
||||
spec: {
|
||||
replicas
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getMetricsForReplicaSets(replicasets: ReplicaSet[], namespace: string, selector = ""): Promise<IPodMetrics> {
|
||||
const podSelector = replicasets.map(replicaset => `${replicaset.getName()}-[[:alnum:]]{5}`).join("|");
|
||||
const opts = { category: "pods", pods: podSelector, namespace, selector };
|
||||
|
||||
return metricsApi.getMetrics({
|
||||
cpuUsage: opts,
|
||||
memoryUsage: opts,
|
||||
fsUsage: opts,
|
||||
networkReceive: opts,
|
||||
networkTransmit: opts,
|
||||
}, {
|
||||
namespace,
|
||||
});
|
||||
}
|
||||
|
||||
export class ReplicaSet extends WorkloadKubeObject {
|
||||
static kind = "ReplicaSet";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/apps/v1/replicasets";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
declare spec: {
|
||||
replicas?: number;
|
||||
selector: { matchLabels: { [app: string]: string } };
|
||||
template?: {
|
||||
metadata: {
|
||||
labels: {
|
||||
app: string;
|
||||
};
|
||||
};
|
||||
spec?: Pod["spec"];
|
||||
};
|
||||
minReadySeconds?: number;
|
||||
};
|
||||
declare status: {
|
||||
replicas: number;
|
||||
fullyLabeledReplicas?: number;
|
||||
readyReplicas?: number;
|
||||
availableReplicas?: number;
|
||||
observedGeneration?: number;
|
||||
conditions?: {
|
||||
type: string;
|
||||
status: string;
|
||||
lastUpdateTime: string;
|
||||
lastTransitionTime: string;
|
||||
reason: string;
|
||||
message: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
getDesired() {
|
||||
return this.spec.replicas || 0;
|
||||
}
|
||||
|
||||
getCurrent() {
|
||||
return this.status.availableReplicas || 0;
|
||||
}
|
||||
|
||||
getReady() {
|
||||
return this.status.readyReplicas || 0;
|
||||
}
|
||||
|
||||
getImages() {
|
||||
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []);
|
||||
|
||||
return [...containers].map(container => container.image);
|
||||
}
|
||||
}
|
||||
|
||||
let replicaSetApi: ReplicaSetApi;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
replicaSetApi = new ReplicaSetApi({
|
||||
objectConstructor: ReplicaSet,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
replicaSetApi
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import jsYaml from "js-yaml";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { apiBase } from "../index";
|
||||
|
||||
export const resourceApplierApi = {
|
||||
annotations: [
|
||||
"kubectl.kubernetes.io/last-applied-configuration"
|
||||
],
|
||||
|
||||
async update(resource: object | string): Promise<KubeJsonApiData | null> {
|
||||
if (typeof resource === "string") {
|
||||
resource = jsYaml.safeLoad(resource);
|
||||
}
|
||||
|
||||
const [data = null] = await apiBase.post<KubeJsonApiData[]>("/stack", { data: resource });
|
||||
|
||||
return data;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export interface IResourceQuotaValues {
|
||||
[quota: string]: string;
|
||||
|
||||
// Compute Resource Quota
|
||||
"limits.cpu"?: string;
|
||||
"limits.memory"?: string;
|
||||
"requests.cpu"?: string;
|
||||
"requests.memory"?: string;
|
||||
|
||||
// Storage Resource Quota
|
||||
"requests.storage"?: string;
|
||||
"persistentvolumeclaims"?: string;
|
||||
|
||||
// Object Count Quota
|
||||
"count/pods"?: string;
|
||||
"count/persistentvolumeclaims"?: string;
|
||||
"count/services"?: string;
|
||||
"count/secrets"?: string;
|
||||
"count/configmaps"?: string;
|
||||
"count/replicationcontrollers"?: string;
|
||||
"count/deployments.apps"?: string;
|
||||
"count/replicasets.apps"?: string;
|
||||
"count/statefulsets.apps"?: string;
|
||||
"count/jobs.batch"?: string;
|
||||
"count/cronjobs.batch"?: string;
|
||||
"count/deployments.extensions"?: string;
|
||||
}
|
||||
|
||||
export interface ResourceQuota {
|
||||
spec: {
|
||||
hard: IResourceQuotaValues;
|
||||
scopeSelector?: {
|
||||
matchExpressions: {
|
||||
operator: string;
|
||||
scopeName: string;
|
||||
values: string[];
|
||||
}[];
|
||||
};
|
||||
};
|
||||
|
||||
status: {
|
||||
hard: IResourceQuotaValues;
|
||||
used: IResourceQuotaValues;
|
||||
};
|
||||
}
|
||||
|
||||
export class ResourceQuota extends KubeObject {
|
||||
static kind = "ResourceQuota";
|
||||
static namespaced = true;
|
||||
static apiBase = "/api/v1/resourcequotas";
|
||||
|
||||
getScopeSelector() {
|
||||
const { matchExpressions = [] } = this.spec.scopeSelector || {};
|
||||
|
||||
return matchExpressions;
|
||||
}
|
||||
}
|
||||
|
||||
let resourceQuotaApi: KubeApi<ResourceQuota>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
resourceQuotaApi = new KubeApi<ResourceQuota>({
|
||||
objectConstructor: ResourceQuota,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
resourceQuotaApi
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { autoBind } from "../../utils";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export type RoleBindingSubjectKind = "Group" | "ServiceAccount" | "User";
|
||||
|
||||
export interface RoleBindingSubject {
|
||||
kind: RoleBindingSubjectKind;
|
||||
name: string;
|
||||
namespace?: string;
|
||||
apiGroup?: string;
|
||||
}
|
||||
|
||||
export interface RoleBinding {
|
||||
subjects?: RoleBindingSubject[];
|
||||
roleRef: {
|
||||
kind: string;
|
||||
name: string;
|
||||
apiGroup?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class RoleBinding extends KubeObject {
|
||||
static kind = "RoleBinding";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/rbac.authorization.k8s.io/v1/rolebindings";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
getSubjects() {
|
||||
return this.subjects || [];
|
||||
}
|
||||
|
||||
getSubjectNames(): string {
|
||||
return this.getSubjects().map(subject => subject.name).join(", ");
|
||||
}
|
||||
}
|
||||
|
||||
let roleBindingApi: KubeApi<RoleBinding>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
roleBindingApi = new KubeApi({
|
||||
objectConstructor: RoleBinding,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
roleBindingApi
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export interface Role {
|
||||
rules: {
|
||||
verbs: string[];
|
||||
apiGroups: string[];
|
||||
resources: string[];
|
||||
resourceNames?: string[];
|
||||
}[];
|
||||
}
|
||||
|
||||
export class Role extends KubeObject {
|
||||
static kind = "Role";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/rbac.authorization.k8s.io/v1/roles";
|
||||
|
||||
getRules() {
|
||||
return this.rules || [];
|
||||
}
|
||||
}
|
||||
|
||||
let roleApi: KubeApi<Role>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
roleApi = new KubeApi<Role>({
|
||||
objectConstructor: Role,
|
||||
});
|
||||
}
|
||||
|
||||
export{
|
||||
roleApi
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { autoBind } from "../../utils";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export enum SecretType {
|
||||
Opaque = "Opaque",
|
||||
ServiceAccountToken = "kubernetes.io/service-account-token",
|
||||
Dockercfg = "kubernetes.io/dockercfg",
|
||||
DockerConfigJson = "kubernetes.io/dockerconfigjson",
|
||||
BasicAuth = "kubernetes.io/basic-auth",
|
||||
SSHAuth = "kubernetes.io/ssh-auth",
|
||||
TLS = "kubernetes.io/tls",
|
||||
BootstrapToken = "bootstrap.kubernetes.io/token",
|
||||
}
|
||||
|
||||
export interface ISecretRef {
|
||||
key?: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Secret {
|
||||
type: SecretType;
|
||||
data: {
|
||||
[prop: string]: string;
|
||||
token?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class Secret extends KubeObject {
|
||||
static kind = "Secret";
|
||||
static namespaced = true;
|
||||
static apiBase = "/api/v1/secrets";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
|
||||
this.data ??= {};
|
||||
}
|
||||
|
||||
getKeys(): string[] {
|
||||
return Object.keys(this.data);
|
||||
}
|
||||
|
||||
getToken() {
|
||||
return this.data.token;
|
||||
}
|
||||
}
|
||||
|
||||
let secretsApi: KubeApi<Secret>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
secretsApi = new KubeApi({
|
||||
objectConstructor: Secret,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
secretsApi
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export class SelfSubjectRulesReviewApi extends KubeApi<SelfSubjectRulesReview> {
|
||||
create({ namespace = "default" }): Promise<SelfSubjectRulesReview> {
|
||||
return super.create({}, {
|
||||
spec: {
|
||||
namespace
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export interface ISelfSubjectReviewRule {
|
||||
verbs: string[];
|
||||
apiGroups?: string[];
|
||||
resources?: string[];
|
||||
resourceNames?: string[];
|
||||
nonResourceURLs?: string[];
|
||||
}
|
||||
|
||||
export interface SelfSubjectRulesReview {
|
||||
spec: {
|
||||
namespace?: string;
|
||||
};
|
||||
status: {
|
||||
resourceRules: ISelfSubjectReviewRule[];
|
||||
nonResourceRules: ISelfSubjectReviewRule[];
|
||||
incomplete: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export class SelfSubjectRulesReview extends KubeObject {
|
||||
static kind = "SelfSubjectRulesReview";
|
||||
static namespaced = false;
|
||||
static apiBase = "/apis/authorization.k8s.io/v1/selfsubjectrulesreviews";
|
||||
|
||||
getResourceRules() {
|
||||
const rules = this.status && this.status.resourceRules || [];
|
||||
|
||||
return rules.map(rule => this.normalize(rule));
|
||||
}
|
||||
|
||||
getNonResourceRules() {
|
||||
const rules = this.status && this.status.nonResourceRules || [];
|
||||
|
||||
return rules.map(rule => this.normalize(rule));
|
||||
}
|
||||
|
||||
protected normalize(rule: ISelfSubjectReviewRule): ISelfSubjectReviewRule {
|
||||
const { apiGroups = [], resourceNames = [], verbs = [], nonResourceURLs = [], resources = [] } = rule;
|
||||
|
||||
return {
|
||||
apiGroups,
|
||||
nonResourceURLs,
|
||||
resourceNames,
|
||||
verbs,
|
||||
resources: resources.map((resource, index) => {
|
||||
const apiGroup = apiGroups.length >= index + 1 ? apiGroups[index] : apiGroups.slice(-1)[0];
|
||||
const separator = apiGroup == "" ? "" : ".";
|
||||
|
||||
return resource + separator + apiGroup;
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let selfSubjectRulesReviewApi: SelfSubjectRulesReviewApi;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
selfSubjectRulesReviewApi = new SelfSubjectRulesReviewApi({
|
||||
objectConstructor: SelfSubjectRulesReview,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
selfSubjectRulesReviewApi
|
||||
};
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { autoBind } from "../../utils";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export interface ServiceAccount {
|
||||
secrets?: {
|
||||
name: string;
|
||||
}[];
|
||||
imagePullSecrets?: {
|
||||
name: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export class ServiceAccount extends KubeObject {
|
||||
static kind = "ServiceAccount";
|
||||
static namespaced = true;
|
||||
static apiBase = "/api/v1/serviceaccounts";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
getSecrets() {
|
||||
return this.secrets || [];
|
||||
}
|
||||
|
||||
getImagePullSecrets() {
|
||||
return this.imagePullSecrets || [];
|
||||
}
|
||||
}
|
||||
|
||||
let serviceAccountsApi: KubeApi<ServiceAccount>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
serviceAccountsApi = new KubeApi<ServiceAccount>({
|
||||
objectConstructor: ServiceAccount,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
serviceAccountsApi
|
||||
};
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { autoBind } from "../../../renderer/utils";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export interface ServicePort {
|
||||
name?: string;
|
||||
protocol: string;
|
||||
port: number;
|
||||
targetPort: number;
|
||||
nodePort?: number;
|
||||
}
|
||||
|
||||
export class ServicePort {
|
||||
constructor(data: ServicePort) {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
|
||||
toString() {
|
||||
if (this.nodePort) {
|
||||
return `${this.port}:${this.nodePort}/${this.protocol}`;
|
||||
} else {
|
||||
return `${this.port}${this.port === this.targetPort ? "" : `:${this.targetPort}`}/${this.protocol}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface Service {
|
||||
spec: {
|
||||
type: string;
|
||||
clusterIP: string;
|
||||
clusterIPs?: string[];
|
||||
externalTrafficPolicy?: string;
|
||||
externalName: string;
|
||||
loadBalancerIP?: string;
|
||||
loadBalancerSourceRanges?: string[];
|
||||
sessionAffinity: string;
|
||||
selector: { [key: string]: string };
|
||||
ports: ServicePort[];
|
||||
healthCheckNodePort?: number;
|
||||
externalIPs?: string[]; // https://kubernetes.io/docs/concepts/services-networking/service/#external-ips
|
||||
topologyKeys?: string[];
|
||||
ipFamilies?: string[];
|
||||
ipFamilyPolicy?: string;
|
||||
allocateLoadBalancerNodePorts?: boolean;
|
||||
loadBalancerClass?: string;
|
||||
internalTrafficPolicy?: string;
|
||||
};
|
||||
|
||||
status: {
|
||||
loadBalancer?: {
|
||||
ingress?: {
|
||||
ip?: string;
|
||||
hostname?: string;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export class Service extends KubeObject {
|
||||
static kind = "Service";
|
||||
static namespaced = true;
|
||||
static apiBase = "/api/v1/services";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
getClusterIp() {
|
||||
return this.spec.clusterIP;
|
||||
}
|
||||
|
||||
getClusterIps() {
|
||||
return this.spec.clusterIPs || [];
|
||||
}
|
||||
|
||||
getExternalIps() {
|
||||
const lb = this.getLoadBalancer();
|
||||
|
||||
if (lb && lb.ingress) {
|
||||
return lb.ingress.map(val => val.ip || val.hostname);
|
||||
}
|
||||
|
||||
return this.spec.externalIPs || [];
|
||||
}
|
||||
|
||||
getType() {
|
||||
return this.spec.type || "-";
|
||||
}
|
||||
|
||||
getSelector(): string[] {
|
||||
if (!this.spec.selector) return [];
|
||||
|
||||
return Object.entries(this.spec.selector).map(val => val.join("="));
|
||||
}
|
||||
|
||||
getPorts(): ServicePort[] {
|
||||
const ports = this.spec.ports || [];
|
||||
|
||||
return ports.map(p => new ServicePort(p));
|
||||
}
|
||||
|
||||
getLoadBalancer() {
|
||||
return this.status.loadBalancer;
|
||||
}
|
||||
|
||||
isActive() {
|
||||
return this.getType() !== "LoadBalancer" || this.getExternalIps().length > 0;
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return this.isActive() ? "Active" : "Pending";
|
||||
}
|
||||
|
||||
getIpFamilies() {
|
||||
return this.spec.ipFamilies || [];
|
||||
}
|
||||
|
||||
getIpFamilyPolicy() {
|
||||
return this.spec.ipFamilyPolicy || "";
|
||||
}
|
||||
}
|
||||
|
||||
let serviceApi: KubeApi<Service>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
serviceApi = new KubeApi<Service>({
|
||||
objectConstructor: Service,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
serviceApi
|
||||
};
|
||||
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import get from "lodash/get";
|
||||
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
|
||||
import { autoBind } from "../../utils";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { metricsApi } from "./metrics.api";
|
||||
import type { IPodContainer, IPodMetrics } from "./pods.api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export class StatefulSetApi extends KubeApi<StatefulSet> {
|
||||
protected getScaleApiUrl(params: { namespace: string; name: string }) {
|
||||
return `${this.getUrl(params)}/scale`;
|
||||
}
|
||||
|
||||
getReplicas(params: { namespace: string; name: string }): Promise<number> {
|
||||
return this.request
|
||||
.get(this.getScaleApiUrl(params))
|
||||
.then(({ status }: any) => status?.replicas);
|
||||
}
|
||||
|
||||
scale(params: { namespace: string; name: string }, replicas: number) {
|
||||
return this.request.put(this.getScaleApiUrl(params), {
|
||||
data: {
|
||||
metadata: params,
|
||||
spec: {
|
||||
replicas
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getMetricsForStatefulSets(statefulSets: StatefulSet[], namespace: string, selector = ""): Promise<IPodMetrics> {
|
||||
const podSelector = statefulSets.map(statefulset => `${statefulset.getName()}-[[:digit:]]+`).join("|");
|
||||
const opts = { category: "pods", pods: podSelector, namespace, selector };
|
||||
|
||||
return metricsApi.getMetrics({
|
||||
cpuUsage: opts,
|
||||
memoryUsage: opts,
|
||||
fsUsage: opts,
|
||||
networkReceive: opts,
|
||||
networkTransmit: opts,
|
||||
}, {
|
||||
namespace,
|
||||
});
|
||||
}
|
||||
|
||||
export class StatefulSet extends WorkloadKubeObject {
|
||||
static kind = "StatefulSet";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/apps/v1/statefulsets";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
declare spec: {
|
||||
serviceName: string;
|
||||
replicas: number;
|
||||
selector: {
|
||||
matchLabels: {
|
||||
[key: string]: string;
|
||||
};
|
||||
};
|
||||
template: {
|
||||
metadata: {
|
||||
labels: {
|
||||
app: string;
|
||||
};
|
||||
};
|
||||
spec: {
|
||||
containers: {
|
||||
name: string;
|
||||
image: string;
|
||||
ports: {
|
||||
containerPort: number;
|
||||
name: string;
|
||||
}[];
|
||||
volumeMounts: {
|
||||
name: string;
|
||||
mountPath: string;
|
||||
}[];
|
||||
}[];
|
||||
affinity?: IAffinity;
|
||||
nodeSelector?: {
|
||||
[selector: string]: string;
|
||||
};
|
||||
tolerations: {
|
||||
key: string;
|
||||
operator: string;
|
||||
effect: string;
|
||||
tolerationSeconds: number;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
volumeClaimTemplates: {
|
||||
metadata: {
|
||||
name: string;
|
||||
};
|
||||
spec: {
|
||||
accessModes: string[];
|
||||
resources: {
|
||||
requests: {
|
||||
storage: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
}[];
|
||||
};
|
||||
declare status: {
|
||||
observedGeneration: number;
|
||||
replicas: number;
|
||||
currentReplicas: number;
|
||||
readyReplicas: number;
|
||||
currentRevision: string;
|
||||
updateRevision: string;
|
||||
collisionCount: number;
|
||||
};
|
||||
|
||||
getReplicas() {
|
||||
return this.spec.replicas || 0;
|
||||
}
|
||||
|
||||
getImages() {
|
||||
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []);
|
||||
|
||||
return [...containers].map(container => container.image);
|
||||
}
|
||||
}
|
||||
|
||||
let statefulSetApi: StatefulSetApi;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
statefulSetApi = new StatefulSetApi({
|
||||
objectConstructor: StatefulSet,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
statefulSetApi
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { autoBind } from "../../utils";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export interface StorageClass {
|
||||
provisioner: string; // e.g. "storage.k8s.io/v1"
|
||||
mountOptions?: string[];
|
||||
volumeBindingMode: string;
|
||||
reclaimPolicy: string;
|
||||
parameters: {
|
||||
[param: string]: string; // every provisioner has own set of these parameters
|
||||
};
|
||||
}
|
||||
|
||||
export class StorageClass extends KubeObject {
|
||||
static kind = "StorageClass";
|
||||
static namespaced = false;
|
||||
static apiBase = "/apis/storage.k8s.io/v1/storageclasses";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
isDefault() {
|
||||
const annotations = this.metadata.annotations || {};
|
||||
|
||||
return (
|
||||
annotations["storageclass.kubernetes.io/is-default-class"] === "true" ||
|
||||
annotations["storageclass.beta.kubernetes.io/is-default-class"] === "true"
|
||||
);
|
||||
}
|
||||
|
||||
getVolumeBindingMode() {
|
||||
return this.volumeBindingMode || "-";
|
||||
}
|
||||
|
||||
getReclaimPolicy() {
|
||||
return this.reclaimPolicy || "-";
|
||||
}
|
||||
}
|
||||
|
||||
let storageClassApi: KubeApi<StorageClass>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
storageClassApi = new KubeApi({
|
||||
objectConstructor: StorageClass,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
storageClassApi
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { JsonApi } from "./json-api";
|
||||
import { KubeJsonApi } from "./kube-json-api";
|
||||
import { apiKubePrefix, apiPrefix, isDebugging, isDevelopment } from "../../common/vars";
|
||||
import { isClusterPageContext } from "../utils/cluster-id-url-parsing";
|
||||
|
||||
let apiBase: JsonApi;
|
||||
let apiKube: KubeJsonApi;
|
||||
|
||||
if (typeof window === "undefined") {
|
||||
apiBase = new JsonApi({
|
||||
serverAddress: `http://127.0.0.1:${process.env.LENS_PROXY_PORT}`,
|
||||
apiBase: apiPrefix,
|
||||
debug: isDevelopment || isDebugging,
|
||||
}, {
|
||||
headers: {
|
||||
"Host": `localhost:${process.env.LENS_PROXY_PORT}`
|
||||
}
|
||||
});
|
||||
} else {
|
||||
apiBase = new JsonApi({
|
||||
serverAddress: `http://127.0.0.1:${window.location.port}`,
|
||||
apiBase: apiPrefix,
|
||||
debug: isDevelopment || isDebugging,
|
||||
}, {
|
||||
headers: {
|
||||
"Host": window.location.host
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
apiKube = new KubeJsonApi({
|
||||
serverAddress: `http://127.0.0.1:${window.location.port}`,
|
||||
apiBase: apiKubePrefix,
|
||||
debug: isDevelopment
|
||||
}, {
|
||||
headers: {
|
||||
"Host": window.location.host
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
apiBase,
|
||||
apiKube
|
||||
};
|
||||
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
// Base http-service / json-api class
|
||||
|
||||
import { merge } from "lodash";
|
||||
import fetch, { Response, RequestInit } from "node-fetch";
|
||||
import { stringify } from "querystring";
|
||||
import { EventEmitter } from "../../common/event-emitter";
|
||||
import logger from "../../common/logger";
|
||||
|
||||
export interface JsonApiData {
|
||||
}
|
||||
|
||||
export interface JsonApiError {
|
||||
code?: number;
|
||||
message?: string;
|
||||
errors?: { id: string; title: string; status?: number }[];
|
||||
}
|
||||
|
||||
export interface JsonApiParams<D = any> {
|
||||
query?: { [param: string]: string | number | any };
|
||||
data?: D; // request body
|
||||
}
|
||||
|
||||
export interface JsonApiLog {
|
||||
method: string;
|
||||
reqUrl: string;
|
||||
reqInit: RequestInit;
|
||||
data?: any;
|
||||
error?: any;
|
||||
}
|
||||
|
||||
export interface JsonApiConfig {
|
||||
apiBase: string;
|
||||
serverAddress: string;
|
||||
debug?: boolean;
|
||||
}
|
||||
export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
|
||||
static reqInitDefault: RequestInit = {
|
||||
headers: {
|
||||
"content-type": "application/json"
|
||||
}
|
||||
};
|
||||
|
||||
static configDefault: Partial<JsonApiConfig> = {
|
||||
debug: false
|
||||
};
|
||||
|
||||
constructor(public readonly config: JsonApiConfig, protected reqInit?: RequestInit) {
|
||||
this.config = Object.assign({}, JsonApi.configDefault, config);
|
||||
this.reqInit = merge({}, JsonApi.reqInitDefault, reqInit);
|
||||
this.parseResponse = this.parseResponse.bind(this);
|
||||
}
|
||||
|
||||
public onData = new EventEmitter<[D, Response]>();
|
||||
public onError = new EventEmitter<[JsonApiErrorParsed, Response]>();
|
||||
|
||||
get<T = D>(path: string, params?: P, reqInit: RequestInit = {}) {
|
||||
return this.request<T>(path, params, { ...reqInit, method: "get" });
|
||||
}
|
||||
|
||||
getResponse(path: string, params?: P, init: RequestInit = {}): Promise<Response> {
|
||||
let reqUrl = `${this.config.serverAddress}${this.config.apiBase}${path}`;
|
||||
const reqInit: RequestInit = merge({}, this.reqInit, init);
|
||||
const { query } = params || {} as P;
|
||||
|
||||
if (!reqInit.method) {
|
||||
reqInit.method = "get";
|
||||
}
|
||||
|
||||
if (query) {
|
||||
const queryString = stringify(query);
|
||||
|
||||
reqUrl += (reqUrl.includes("?") ? "&" : "?") + queryString;
|
||||
}
|
||||
|
||||
this.writeLog({
|
||||
method: reqInit.method.toUpperCase(),
|
||||
reqUrl,
|
||||
reqInit,
|
||||
});
|
||||
|
||||
return fetch(reqUrl, reqInit);
|
||||
}
|
||||
|
||||
post<T = D>(path: string, params?: P, reqInit: RequestInit = {}) {
|
||||
return this.request<T>(path, params, { ...reqInit, method: "post" });
|
||||
}
|
||||
|
||||
put<T = D>(path: string, params?: P, reqInit: RequestInit = {}) {
|
||||
return this.request<T>(path, params, { ...reqInit, method: "put" });
|
||||
}
|
||||
|
||||
patch<T = D>(path: string, params?: P, reqInit: RequestInit = {}) {
|
||||
return this.request<T>(path, params, { ...reqInit, method: "PATCH" });
|
||||
}
|
||||
|
||||
del<T = D>(path: string, params?: P, reqInit: RequestInit = {}) {
|
||||
return this.request<T>(path, params, { ...reqInit, method: "delete" });
|
||||
}
|
||||
|
||||
protected async request<D>(path: string, params?: P, init: RequestInit = {}) {
|
||||
let reqUrl = `${this.config.serverAddress}${this.config.apiBase}${path}`;
|
||||
const reqInit: RequestInit = merge({}, this.reqInit, init);
|
||||
const { data, query } = params || {} as P;
|
||||
|
||||
if (data && !reqInit.body) {
|
||||
reqInit.body = JSON.stringify(data);
|
||||
}
|
||||
|
||||
if (query) {
|
||||
const queryString = stringify(query);
|
||||
|
||||
reqUrl += (reqUrl.includes("?") ? "&" : "?") + queryString;
|
||||
}
|
||||
const infoLog: JsonApiLog = {
|
||||
method: reqInit.method.toUpperCase(),
|
||||
reqUrl,
|
||||
reqInit,
|
||||
};
|
||||
|
||||
const res = await fetch(reqUrl, reqInit);
|
||||
|
||||
return this.parseResponse<D>(res, infoLog);
|
||||
}
|
||||
|
||||
protected async parseResponse<D>(res: Response, log: JsonApiLog): Promise<D> {
|
||||
const { status } = res;
|
||||
|
||||
const text = await res.text();
|
||||
let data;
|
||||
|
||||
try {
|
||||
data = text ? JSON.parse(text) : ""; // DELETE-requests might not have response-body
|
||||
} catch (e) {
|
||||
data = text;
|
||||
}
|
||||
|
||||
if (status >= 200 && status < 300) {
|
||||
this.onData.emit(data, res);
|
||||
this.writeLog({ ...log, data });
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
if (log.method === "GET" && res.status === 403) {
|
||||
this.writeLog({ ...log, error: data });
|
||||
throw data;
|
||||
}
|
||||
|
||||
const error = new JsonApiErrorParsed(data, this.parseError(data, res));
|
||||
|
||||
this.onError.emit(error, res);
|
||||
this.writeLog({ ...log, error });
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
protected parseError(error: JsonApiError | string, res: Response): string[] {
|
||||
if (typeof error === "string") {
|
||||
return [error];
|
||||
}
|
||||
|
||||
if (Array.isArray(error.errors)) {
|
||||
return error.errors.map(error => error.title);
|
||||
}
|
||||
|
||||
if (error.message) {
|
||||
return [error.message];
|
||||
}
|
||||
|
||||
return [res.statusText || "Error!"];
|
||||
}
|
||||
|
||||
protected writeLog(log: JsonApiLog) {
|
||||
const { method, reqUrl, ...params } = log;
|
||||
|
||||
logger.info(`[JSON-API] request ${method} ${reqUrl}`, params);
|
||||
}
|
||||
}
|
||||
|
||||
export class JsonApiErrorParsed {
|
||||
isUsedForNotification = false;
|
||||
|
||||
constructor(private error: JsonApiError | DOMException, private messages: string[]) {
|
||||
}
|
||||
|
||||
get isAborted() {
|
||||
return this.error.code === DOMException.ABORT_ERR;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.messages.join("\n");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
// Parse kube-api path and get api-version, group, etc.
|
||||
|
||||
import { splitArray } from "../utils";
|
||||
import { isDebugging } from "../vars";
|
||||
import logger from "../../main/logger";
|
||||
import { inspect } from "util";
|
||||
|
||||
export interface IKubeObjectRef {
|
||||
kind: string;
|
||||
apiVersion: string;
|
||||
name: string;
|
||||
namespace?: string;
|
||||
}
|
||||
|
||||
export interface IKubeApiLinkRef {
|
||||
apiPrefix?: string;
|
||||
apiVersion: string;
|
||||
resource: string;
|
||||
name: string;
|
||||
namespace?: string;
|
||||
}
|
||||
|
||||
export interface IKubeApiParsed extends IKubeApiLinkRef {
|
||||
apiBase: string;
|
||||
apiGroup: string;
|
||||
apiVersionWithGroup: string;
|
||||
}
|
||||
|
||||
export function parseKubeApi(path: string): IKubeApiParsed {
|
||||
if (!isDebugging) {
|
||||
return _parseKubeApi(path);
|
||||
}
|
||||
|
||||
try {
|
||||
const res = _parseKubeApi(path);
|
||||
|
||||
logger.debug(`parseKubeApi(${inspect(path, false, null, false)}) -> ${inspect(res, false, null, false)}`);
|
||||
|
||||
return res;
|
||||
} catch (error) {
|
||||
logger.debug(`parseKubeApi(${inspect(path, false, null, false)}) threw: ${error}`);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function _parseKubeApi(path: string): IKubeApiParsed {
|
||||
const apiPath = new URL(path, "http://localhost").pathname;
|
||||
const [, prefix, ...parts] = apiPath.split("/");
|
||||
const apiPrefix = `/${prefix}`;
|
||||
const [left, right, namespaced] = splitArray(parts, "namespaces");
|
||||
let apiGroup, apiVersion, namespace, resource, name;
|
||||
|
||||
if (namespaced) {
|
||||
switch (right.length) {
|
||||
case 1:
|
||||
name = right[0];
|
||||
// fallthrough
|
||||
case 0:
|
||||
resource = "namespaces"; // special case this due to `split` removing namespaces
|
||||
break;
|
||||
default:
|
||||
[namespace, resource, name] = right;
|
||||
break;
|
||||
}
|
||||
|
||||
apiVersion = left.pop();
|
||||
apiGroup = left.join("/");
|
||||
} else {
|
||||
switch (left.length) {
|
||||
case 0:
|
||||
throw new Error(`invalid apiPath: ${apiPath}`);
|
||||
case 4:
|
||||
[apiGroup, apiVersion, resource, name] = left;
|
||||
break;
|
||||
case 2:
|
||||
resource = left.pop();
|
||||
// fallthrough
|
||||
case 1:
|
||||
apiVersion = left.pop();
|
||||
apiGroup = "";
|
||||
break;
|
||||
default:
|
||||
/**
|
||||
* Given that
|
||||
* - `apiVersion` is `GROUP/VERSION` and
|
||||
* - `VERSION` is `DNS_LABEL` which is /^[a-z0-9]((-[a-z0-9])|[a-z0-9])*$/i
|
||||
* where length <= 63
|
||||
* - `GROUP` is /^D(\.D)*$/ where D is `DNS_LABEL` and length <= 253
|
||||
*
|
||||
* There is no well defined selection from an array of items that were
|
||||
* separated by '/'
|
||||
*
|
||||
* Solution is to create a heuristic. Namely:
|
||||
* 1. if '.' in left[0] then apiGroup <- left[0]
|
||||
* 2. if left[1] matches /^v[0-9]/ then apiGroup, apiVersion <- left[0], left[1]
|
||||
* 3. otherwise assume apiVersion <- left[0]
|
||||
* 4. always resource, name <- left[(0 or 1)+1..]
|
||||
*/
|
||||
if (left[0].includes(".") || left[1].match(/^v[0-9]/)) {
|
||||
[apiGroup, apiVersion] = left;
|
||||
resource = left.slice(2).join("/");
|
||||
} else {
|
||||
apiGroup = "";
|
||||
apiVersion = left[0];
|
||||
[resource, name] = left.slice(1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const apiVersionWithGroup = [apiGroup, apiVersion].filter(v => v).join("/");
|
||||
const apiBase = [apiPrefix, apiGroup, apiVersion, resource].filter(v => v).join("/");
|
||||
|
||||
if (!apiBase) {
|
||||
throw new Error(`invalid apiPath: ${apiPath}`);
|
||||
}
|
||||
|
||||
return {
|
||||
apiBase,
|
||||
apiPrefix, apiGroup,
|
||||
apiVersion, apiVersionWithGroup,
|
||||
namespace, resource, name,
|
||||
};
|
||||
}
|
||||
|
||||
export function createKubeApiURL(ref: IKubeApiLinkRef): string {
|
||||
const { apiPrefix = "/apis", resource, apiVersion, name } = ref;
|
||||
let { namespace } = ref;
|
||||
|
||||
if (namespace) {
|
||||
namespace = `namespaces/${namespace}`;
|
||||
}
|
||||
|
||||
return [apiPrefix, apiVersion, namespace, resource, name]
|
||||
.filter(v => v)
|
||||
.join("/");
|
||||
}
|
||||
@@ -0,0 +1,493 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
// Base class for building all kubernetes apis
|
||||
|
||||
import merge from "lodash/merge";
|
||||
import { stringify } from "querystring";
|
||||
import { apiKubePrefix, isDevelopment } from "../../common/vars";
|
||||
import logger from "../../main/logger";
|
||||
import { apiManager } from "./api-manager";
|
||||
import { apiBase, apiKube } from "./index";
|
||||
import { createKubeApiURL, parseKubeApi } from "./kube-api-parse";
|
||||
import { KubeObjectConstructor, KubeObject, KubeStatus } from "./kube-object";
|
||||
import byline from "byline";
|
||||
import type { IKubeWatchEvent } from "./kube-watch-api";
|
||||
import { KubeJsonApi, KubeJsonApiData } from "./kube-json-api";
|
||||
import { noop } from "../utils";
|
||||
import type { RequestInit } from "node-fetch";
|
||||
import AbortController from "abort-controller";
|
||||
|
||||
export interface IKubeApiOptions<T extends KubeObject> {
|
||||
/**
|
||||
* base api-path for listing all resources, e.g. "/api/v1/pods"
|
||||
*/
|
||||
apiBase?: string;
|
||||
|
||||
/**
|
||||
* If the API uses a different API endpoint (e.g. apiBase) depending on the cluster version,
|
||||
* fallback API bases can be listed individually.
|
||||
* The first (existing) API base is used in the requests, if apiBase is not found.
|
||||
* This option only has effect if checkPreferredVersion is true.
|
||||
*/
|
||||
fallbackApiBases?: string[];
|
||||
|
||||
objectConstructor: KubeObjectConstructor<T>;
|
||||
request?: KubeJsonApi;
|
||||
isNamespaced?: boolean;
|
||||
kind?: string;
|
||||
checkPreferredVersion?: boolean;
|
||||
}
|
||||
|
||||
export interface IKubeApiQueryParams {
|
||||
watch?: boolean | number;
|
||||
resourceVersion?: string;
|
||||
timeoutSeconds?: number;
|
||||
limit?: number; // doesn't work with ?watch
|
||||
continue?: string; // might be used with ?limit from second request
|
||||
labelSelector?: string | string[]; // restrict list of objects by their labels, e.g. labelSelector: ["label=value"]
|
||||
fieldSelector?: string | string[]; // restrict list of objects by their fields, e.g. fieldSelector: "field=name"
|
||||
}
|
||||
|
||||
export interface KubeApiListOptions {
|
||||
namespace?: string;
|
||||
reqInit?: RequestInit;
|
||||
}
|
||||
|
||||
export interface IKubePreferredVersion {
|
||||
preferredVersion?: {
|
||||
version: string;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IKubeResourceList {
|
||||
resources: {
|
||||
kind: string;
|
||||
name: string;
|
||||
namespaced: boolean;
|
||||
singularName: string;
|
||||
storageVersionHash: string;
|
||||
verbs: string[];
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface IKubeApiCluster {
|
||||
metadata: {
|
||||
uid: string;
|
||||
}
|
||||
}
|
||||
|
||||
export function forCluster<T extends KubeObject>(cluster: IKubeApiCluster, kubeClass: KubeObjectConstructor<T>): KubeApi<T> {
|
||||
const request = new KubeJsonApi({
|
||||
serverAddress: apiBase.config.serverAddress,
|
||||
apiBase: apiKubePrefix,
|
||||
debug: isDevelopment,
|
||||
}, {
|
||||
headers: {
|
||||
"Host": apiBase.config.serverAddress
|
||||
}
|
||||
});
|
||||
|
||||
return new KubeApi({
|
||||
objectConstructor: kubeClass,
|
||||
request
|
||||
});
|
||||
}
|
||||
|
||||
export function ensureObjectSelfLink(api: KubeApi<KubeObject>, object: KubeJsonApiData) {
|
||||
if (!object.metadata.selfLink) {
|
||||
object.metadata.selfLink = createKubeApiURL({
|
||||
apiPrefix: api.apiPrefix,
|
||||
apiVersion: api.apiVersionWithGroup,
|
||||
resource: api.apiResource,
|
||||
namespace: api.isNamespaced ? object.metadata.namespace : undefined,
|
||||
name: object.metadata.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export type KubeApiWatchCallback = (data: IKubeWatchEvent<KubeJsonApiData>, error: any) => void;
|
||||
|
||||
export type KubeApiWatchOptions = {
|
||||
namespace: string;
|
||||
callback?: KubeApiWatchCallback;
|
||||
abortController?: AbortController
|
||||
};
|
||||
|
||||
export class KubeApi<T extends KubeObject> {
|
||||
readonly kind: string;
|
||||
readonly apiBase: string;
|
||||
readonly apiPrefix: string;
|
||||
readonly apiGroup: string;
|
||||
readonly apiVersion: string;
|
||||
readonly apiVersionPreferred?: string;
|
||||
readonly apiResource: string;
|
||||
readonly isNamespaced: boolean;
|
||||
|
||||
public objectConstructor: KubeObjectConstructor<T>;
|
||||
protected request: KubeJsonApi;
|
||||
protected resourceVersions = new Map<string, string>();
|
||||
protected watchDisposer: () => void;
|
||||
|
||||
constructor(protected options: IKubeApiOptions<T>) {
|
||||
const {
|
||||
objectConstructor,
|
||||
request = apiKube,
|
||||
kind = options.objectConstructor?.kind,
|
||||
isNamespaced = options.objectConstructor?.namespaced
|
||||
} = options || {};
|
||||
|
||||
if (!options.apiBase) {
|
||||
options.apiBase = objectConstructor.apiBase;
|
||||
}
|
||||
const { apiBase, apiPrefix, apiGroup, apiVersion, resource } = parseKubeApi(options.apiBase);
|
||||
|
||||
this.kind = kind;
|
||||
this.isNamespaced = isNamespaced;
|
||||
this.apiBase = apiBase;
|
||||
this.apiPrefix = apiPrefix;
|
||||
this.apiGroup = apiGroup;
|
||||
this.apiVersion = apiVersion;
|
||||
this.apiResource = resource;
|
||||
this.request = request;
|
||||
this.objectConstructor = objectConstructor;
|
||||
|
||||
this.parseResponse = this.parseResponse.bind(this);
|
||||
apiManager.registerApi(apiBase, this);
|
||||
}
|
||||
|
||||
get apiVersionWithGroup() {
|
||||
return [this.apiGroup, this.apiVersionPreferred ?? this.apiVersion]
|
||||
.filter(Boolean)
|
||||
.join("/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the latest API prefix/group that contains the required resource.
|
||||
* First tries options.apiBase, then urls in order from options.fallbackApiBases.
|
||||
*/
|
||||
private async getLatestApiPrefixGroup() {
|
||||
// Note that this.options.apiBase is the "full" url, whereas this.apiBase is parsed
|
||||
const apiBases = [this.options.apiBase, ...this.options.fallbackApiBases];
|
||||
|
||||
for (const apiUrl of apiBases) {
|
||||
// Split e.g. "/apis/extensions/v1beta1/ingresses" to parts
|
||||
const { apiPrefix, apiGroup, apiVersionWithGroup, resource } = parseKubeApi(apiUrl);
|
||||
|
||||
// Request available resources
|
||||
try {
|
||||
const response = await this.request.get<IKubeResourceList>(`${apiPrefix}/${apiVersionWithGroup}`);
|
||||
|
||||
// If the resource is found in the group, use this apiUrl
|
||||
if (response.resources?.find(kubeResource => kubeResource.name === resource)) {
|
||||
return { apiPrefix, apiGroup };
|
||||
}
|
||||
} catch (error) {
|
||||
// Exception is ignored as we can try the next url
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Can't find working API for the Kubernetes resource ${this.apiResource}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the apiPrefix and apiGroup to be used for fetching the preferred version.
|
||||
*/
|
||||
private async getPreferredVersionPrefixGroup() {
|
||||
if (this.options.fallbackApiBases) {
|
||||
try {
|
||||
return await this.getLatestApiPrefixGroup();
|
||||
} catch (error) {
|
||||
// If valid API wasn't found, log the error and return defaults below
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
apiPrefix: this.apiPrefix,
|
||||
apiGroup: this.apiGroup
|
||||
};
|
||||
}
|
||||
|
||||
protected async checkPreferredVersion() {
|
||||
if (this.options.fallbackApiBases && !this.options.checkPreferredVersion) {
|
||||
throw new Error("checkPreferredVersion must be enabled if fallbackApiBases is set in KubeApi");
|
||||
}
|
||||
|
||||
if (this.options.checkPreferredVersion && this.apiVersionPreferred === undefined) {
|
||||
const { apiPrefix, apiGroup } = await this.getPreferredVersionPrefixGroup();
|
||||
|
||||
// The apiPrefix and apiGroup might change due to fallbackApiBases, so we must override them
|
||||
Object.defineProperty(this, "apiPrefix", {
|
||||
value: apiPrefix
|
||||
});
|
||||
Object.defineProperty(this, "apiGroup", {
|
||||
value: apiGroup
|
||||
});
|
||||
|
||||
const res = await this.request.get<IKubePreferredVersion>(`${this.apiPrefix}/${this.apiGroup}`);
|
||||
|
||||
Object.defineProperty(this, "apiVersionPreferred", {
|
||||
value: res?.preferredVersion?.version ?? null,
|
||||
});
|
||||
|
||||
if (this.apiVersionPreferred) {
|
||||
Object.defineProperty(this, "apiBase", { value: this.getUrl() });
|
||||
apiManager.registerApi(this.apiBase, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setResourceVersion(namespace = "", newVersion: string) {
|
||||
this.resourceVersions.set(namespace, newVersion);
|
||||
}
|
||||
|
||||
getResourceVersion(namespace = "") {
|
||||
return this.resourceVersions.get(namespace);
|
||||
}
|
||||
|
||||
async refreshResourceVersion(params?: KubeApiListOptions) {
|
||||
return this.list(params, { limit: 1 });
|
||||
}
|
||||
|
||||
getUrl({ name = "", namespace = "" } = {}, query?: Partial<IKubeApiQueryParams>) {
|
||||
const resourcePath = createKubeApiURL({
|
||||
apiPrefix: this.apiPrefix,
|
||||
apiVersion: this.apiVersionWithGroup,
|
||||
resource: this.apiResource,
|
||||
namespace: this.isNamespaced ? namespace : undefined,
|
||||
name,
|
||||
});
|
||||
|
||||
return resourcePath + (query ? `?${stringify(this.normalizeQuery(query))}` : "");
|
||||
}
|
||||
|
||||
protected normalizeQuery(query: Partial<IKubeApiQueryParams> = {}) {
|
||||
if (query.labelSelector) {
|
||||
query.labelSelector = [query.labelSelector].flat().join(",");
|
||||
}
|
||||
|
||||
if (query.fieldSelector) {
|
||||
query.fieldSelector = [query.fieldSelector].flat().join(",");
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
protected parseResponse(data: unknown, namespace?: string): T | T[] | null {
|
||||
if (!data) return null;
|
||||
const KubeObjectConstructor = this.objectConstructor;
|
||||
|
||||
// process items list response, check before single item since there is overlap
|
||||
if (KubeObject.isJsonApiDataList(data, KubeObject.isPartialJsonApiData)) {
|
||||
const { apiVersion, items, metadata } = data;
|
||||
|
||||
this.setResourceVersion(namespace, metadata.resourceVersion);
|
||||
this.setResourceVersion("", metadata.resourceVersion);
|
||||
|
||||
return items.map((item) => {
|
||||
const object = new KubeObjectConstructor({
|
||||
kind: this.kind,
|
||||
apiVersion,
|
||||
...item,
|
||||
});
|
||||
|
||||
ensureObjectSelfLink(this, object);
|
||||
|
||||
return object;
|
||||
});
|
||||
}
|
||||
|
||||
// process a single item
|
||||
if (KubeObject.isJsonApiData(data)) {
|
||||
const object = new KubeObjectConstructor(data);
|
||||
|
||||
ensureObjectSelfLink(this, object);
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
// custom apis might return array for list response, e.g. users, groups, etc.
|
||||
if (Array.isArray(data)) {
|
||||
return data.map(data => new KubeObjectConstructor(data));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async list({ namespace = "", reqInit }: KubeApiListOptions = {}, query?: IKubeApiQueryParams): Promise<T[] | null> {
|
||||
await this.checkPreferredVersion();
|
||||
|
||||
const url = this.getUrl({ namespace });
|
||||
const res = await this.request.get(url, { query }, reqInit);
|
||||
const parsed = this.parseResponse(res, namespace);
|
||||
|
||||
if (Array.isArray(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw new Error(`GET multiple request to ${url} returned not an array: ${JSON.stringify(parsed)}`);
|
||||
}
|
||||
|
||||
async get({ name = "", namespace = "default" } = {}, query?: IKubeApiQueryParams): Promise<T | null> {
|
||||
await this.checkPreferredVersion();
|
||||
|
||||
const url = this.getUrl({ namespace, name });
|
||||
const res = await this.request.get(url, { query });
|
||||
const parsed = this.parseResponse(res);
|
||||
|
||||
if (Array.isArray(parsed)) {
|
||||
throw new Error(`GET single request to ${url} returned an array: ${JSON.stringify(parsed)}`);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
async create({ name = "", namespace = "default" } = {}, data?: Partial<T>): Promise<T | null> {
|
||||
await this.checkPreferredVersion();
|
||||
|
||||
const apiUrl = this.getUrl({ namespace });
|
||||
const res = await this.request.post(apiUrl, {
|
||||
data: merge({
|
||||
kind: this.kind,
|
||||
apiVersion: this.apiVersionWithGroup,
|
||||
metadata: {
|
||||
name,
|
||||
namespace
|
||||
}
|
||||
}, data)
|
||||
});
|
||||
const parsed = this.parseResponse(res);
|
||||
|
||||
if (Array.isArray(parsed)) {
|
||||
throw new Error(`POST request to ${apiUrl} returned an array: ${JSON.stringify(parsed)}`);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
async update({ name = "", namespace = "default" } = {}, data?: Partial<T>): Promise<T | null> {
|
||||
await this.checkPreferredVersion();
|
||||
const apiUrl = this.getUrl({ namespace, name });
|
||||
|
||||
const res = await this.request.put(apiUrl, { data });
|
||||
const parsed = this.parseResponse(res);
|
||||
|
||||
if (Array.isArray(parsed)) {
|
||||
throw new Error(`PUT request to ${apiUrl} returned an array: ${JSON.stringify(parsed)}`);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
async delete({ name = "", namespace = "default" }) {
|
||||
await this.checkPreferredVersion();
|
||||
const apiUrl = this.getUrl({ namespace, name });
|
||||
|
||||
return this.request.del(apiUrl);
|
||||
}
|
||||
|
||||
getWatchUrl(namespace = "", query: IKubeApiQueryParams = {}) {
|
||||
return this.getUrl({ namespace }, {
|
||||
watch: 1,
|
||||
resourceVersion: this.getResourceVersion(namespace),
|
||||
...query,
|
||||
});
|
||||
}
|
||||
|
||||
watch(opts: KubeApiWatchOptions = { namespace: "" }): () => void {
|
||||
let errorReceived = false;
|
||||
let timedRetry: NodeJS.Timeout;
|
||||
const { abortController: { abort, signal } = new AbortController(), namespace, callback = noop } = opts;
|
||||
|
||||
signal.addEventListener("abort", () => {
|
||||
clearTimeout(timedRetry);
|
||||
});
|
||||
|
||||
const watchUrl = this.getWatchUrl(namespace);
|
||||
const responsePromise = this.request.getResponse(watchUrl, null, { signal, timeout: 600_000 });
|
||||
|
||||
responsePromise
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return callback(null, response);
|
||||
}
|
||||
|
||||
["end", "close", "error"].forEach((eventName) => {
|
||||
response.body.on(eventName, () => {
|
||||
if (errorReceived) return; // kubernetes errors should be handled in a callback
|
||||
|
||||
clearTimeout(timedRetry);
|
||||
timedRetry = setTimeout(() => { // we did not get any kubernetes errors so let's retry
|
||||
this.watch({ ...opts, namespace, callback });
|
||||
}, 1000);
|
||||
});
|
||||
});
|
||||
|
||||
byline(response.body).on("data", (line) => {
|
||||
try {
|
||||
const event: IKubeWatchEvent<KubeJsonApiData> = JSON.parse(line);
|
||||
|
||||
if (event.type === "ERROR" && event.object.kind === "Status") {
|
||||
errorReceived = true;
|
||||
|
||||
return callback(null, new KubeStatus(event.object as any));
|
||||
}
|
||||
|
||||
this.modifyWatchEvent(event);
|
||||
callback(event, null);
|
||||
} catch (ignore) {
|
||||
// ignore parse errors
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
if (error?.type === "aborted") return; // AbortController rejects, we can ignore it
|
||||
|
||||
callback(null, error);
|
||||
});
|
||||
|
||||
return abort;
|
||||
}
|
||||
|
||||
protected modifyWatchEvent(event: IKubeWatchEvent<KubeJsonApiData>) {
|
||||
|
||||
switch (event.type) {
|
||||
case "ADDED":
|
||||
case "DELETED":
|
||||
|
||||
case "MODIFIED": {
|
||||
ensureObjectSelfLink(this, event.object);
|
||||
|
||||
const { namespace, resourceVersion } = event.object.metadata;
|
||||
|
||||
this.setResourceVersion(namespace, resourceVersion);
|
||||
this.setResourceVersion("", resourceVersion);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { JsonApi, JsonApiData, JsonApiError } from "./json-api";
|
||||
import type { Response } from "node-fetch";
|
||||
|
||||
export interface KubeJsonApiListMetadata {
|
||||
resourceVersion: string;
|
||||
selfLink?: string;
|
||||
}
|
||||
|
||||
export interface KubeJsonApiDataList<T = KubeJsonApiData> {
|
||||
kind: string;
|
||||
apiVersion: string;
|
||||
items: T[];
|
||||
metadata: KubeJsonApiListMetadata;
|
||||
}
|
||||
|
||||
export interface KubeJsonApiMetadata {
|
||||
uid: string;
|
||||
name: string;
|
||||
namespace?: string;
|
||||
creationTimestamp?: string;
|
||||
resourceVersion: string;
|
||||
continue?: string;
|
||||
finalizers?: string[];
|
||||
selfLink?: string;
|
||||
labels?: {
|
||||
[label: string]: string;
|
||||
};
|
||||
annotations?: {
|
||||
[annotation: string]: string;
|
||||
};
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface KubeJsonApiData extends JsonApiData {
|
||||
kind: string;
|
||||
apiVersion: string;
|
||||
metadata: KubeJsonApiMetadata;
|
||||
}
|
||||
|
||||
export interface KubeJsonApiError extends JsonApiError {
|
||||
code: number;
|
||||
status: string;
|
||||
message?: string;
|
||||
reason: string;
|
||||
details: {
|
||||
name: string;
|
||||
kind: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class KubeJsonApi extends JsonApi<KubeJsonApiData> {
|
||||
protected parseError(error: KubeJsonApiError | any, res: Response): string[] {
|
||||
const { status, reason, message } = error;
|
||||
|
||||
if (status && reason) {
|
||||
return [message || `${status}: ${reason}`];
|
||||
}
|
||||
|
||||
return super.parseError(error, res);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,415 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import type { ClusterContext } from "./cluster-context";
|
||||
|
||||
import { action, computed, makeObservable, observable, reaction, when } from "mobx";
|
||||
import { autoBind, noop, rejectPromiseBy } from "../utils";
|
||||
import { KubeObject, KubeStatus } from "./kube-object";
|
||||
import type { IKubeWatchEvent } from "./kube-watch-api";
|
||||
import { ItemStore } from "../item.store";
|
||||
import { apiManager } from "./api-manager";
|
||||
import { ensureObjectSelfLink, IKubeApiQueryParams, KubeApi } from "./kube-api";
|
||||
import { parseKubeApi } from "./kube-api-parse";
|
||||
import type { KubeJsonApiData } from "./kube-json-api";
|
||||
import type { RequestInit } from "node-fetch";
|
||||
import AbortController from "abort-controller";
|
||||
|
||||
export interface KubeObjectStoreLoadingParams<K extends KubeObject> {
|
||||
namespaces: string[];
|
||||
api?: KubeApi<K>;
|
||||
reqInit?: RequestInit;
|
||||
}
|
||||
|
||||
export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T> {
|
||||
static defaultContext = observable.box<ClusterContext>(); // TODO: support multiple cluster contexts
|
||||
|
||||
abstract api: KubeApi<T>;
|
||||
public readonly limit?: number;
|
||||
public readonly bufferSize: number = 50000;
|
||||
@observable private loadedNamespaces?: string[];
|
||||
|
||||
get contextReady() {
|
||||
return when(() => Boolean(this.context));
|
||||
}
|
||||
|
||||
get namespacesReady() {
|
||||
return when(() => Boolean(this.loadedNamespaces));
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
makeObservable(this);
|
||||
autoBind(this);
|
||||
this.bindWatchEventsUpdater();
|
||||
}
|
||||
|
||||
get context(): ClusterContext {
|
||||
return KubeObjectStore.defaultContext.get();
|
||||
}
|
||||
|
||||
@computed get contextItems(): T[] {
|
||||
const namespaces = this.context?.contextNamespaces ?? [];
|
||||
|
||||
return this.items.filter(item => {
|
||||
const itemNamespace = item.getNs();
|
||||
|
||||
return !itemNamespace /* cluster-wide */ || namespaces.includes(itemNamespace);
|
||||
});
|
||||
}
|
||||
|
||||
getTotalCount(): number {
|
||||
return this.contextItems.length;
|
||||
}
|
||||
|
||||
get query(): IKubeApiQueryParams {
|
||||
const { limit } = this;
|
||||
|
||||
if (!limit) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return { limit };
|
||||
}
|
||||
|
||||
getStatuses?(items: T[]): Record<string, number>;
|
||||
|
||||
getAllByNs(namespace: string | string[], strict = false): T[] {
|
||||
const namespaces: string[] = [].concat(namespace);
|
||||
|
||||
if (namespaces.length) {
|
||||
return this.items.filter(item => namespaces.includes(item.getNs()));
|
||||
}
|
||||
|
||||
if (!strict) {
|
||||
return this.items;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
getById(id: string) {
|
||||
return this.items.find(item => item.getId() === id);
|
||||
}
|
||||
|
||||
getByName(name: string, namespace?: string): T {
|
||||
return this.items.find(item => {
|
||||
return item.getName() === name && (
|
||||
namespace ? item.getNs() === namespace : true
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
getByPath(path: string): T {
|
||||
return this.items.find(item => item.selfLink === path);
|
||||
}
|
||||
|
||||
getByLabel(labels: string[] | { [label: string]: string }): T[] {
|
||||
if (Array.isArray(labels)) {
|
||||
return this.items.filter((item: T) => {
|
||||
const itemLabels = item.getLabels();
|
||||
|
||||
return labels.every(label => itemLabels.includes(label));
|
||||
});
|
||||
} else {
|
||||
return this.items.filter((item: T) => {
|
||||
const itemLabels = item.metadata.labels || {};
|
||||
|
||||
return Object.entries(labels)
|
||||
.every(([key, value]) => itemLabels[key] === value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected async loadItems({ namespaces, api, reqInit }: KubeObjectStoreLoadingParams<T>): Promise<T[]> {
|
||||
if (this.context?.cluster.isAllowedResource(api.kind)) {
|
||||
if (!api.isNamespaced) {
|
||||
return api.list({ reqInit }, this.query);
|
||||
}
|
||||
|
||||
const isLoadingAll = this.context.allNamespaces?.length > 1
|
||||
&& this.context.cluster.accessibleNamespaces.length === 0
|
||||
&& this.context.allNamespaces.every(ns => namespaces.includes(ns));
|
||||
|
||||
if (isLoadingAll) {
|
||||
this.loadedNamespaces = [];
|
||||
|
||||
return api.list({ reqInit }, this.query);
|
||||
} else {
|
||||
this.loadedNamespaces = namespaces;
|
||||
|
||||
return Promise // load resources per namespace
|
||||
.all(namespaces.map(namespace => api.list({ namespace, reqInit })))
|
||||
.then(items => items.flat().filter(Boolean));
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
protected filterItemsOnLoad(items: T[]) {
|
||||
return items;
|
||||
}
|
||||
|
||||
@action
|
||||
async loadAll(options: { namespaces?: string[], merge?: boolean, reqInit?: RequestInit } = {}): Promise<void | T[]> {
|
||||
await this.contextReady;
|
||||
this.isLoading = true;
|
||||
|
||||
try {
|
||||
const {
|
||||
namespaces = this.context.allNamespaces, // load all namespaces by default
|
||||
merge = true, // merge loaded items or return as result
|
||||
reqInit,
|
||||
} = options;
|
||||
|
||||
const items = await this.loadItems({ namespaces, api: this.api, reqInit });
|
||||
|
||||
if (merge) {
|
||||
this.mergeItems(items, { replace: false });
|
||||
} else {
|
||||
this.mergeItems(items, { replace: true });
|
||||
}
|
||||
|
||||
this.isLoaded = true;
|
||||
this.failedLoading = false;
|
||||
|
||||
return items;
|
||||
} catch (error) {
|
||||
console.warn("[KubeObjectStore] loadAll failed", this.api.apiBase, error);
|
||||
this.resetOnError(error);
|
||||
this.failedLoading = true;
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async reloadAll(opts: { force?: boolean, namespaces?: string[], merge?: boolean } = {}) {
|
||||
const { force = false, ...loadingOptions } = opts;
|
||||
|
||||
if (this.isLoading || (this.isLoaded && !force)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.loadAll(loadingOptions);
|
||||
}
|
||||
|
||||
@action
|
||||
protected mergeItems(partialItems: T[], { replace = false, updateStore = true, sort = true, filter = true } = {}): T[] {
|
||||
let items = partialItems;
|
||||
|
||||
// update existing items
|
||||
if (!replace) {
|
||||
const namespaces = partialItems.map(item => item.getNs());
|
||||
|
||||
items = [
|
||||
...this.items.filter(existingItem => !namespaces.includes(existingItem.getNs())),
|
||||
...partialItems,
|
||||
];
|
||||
}
|
||||
|
||||
if (filter) items = this.filterItemsOnLoad(items);
|
||||
if (sort) items = this.sortItems(items);
|
||||
if (updateStore) this.items.replace(items);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
protected resetOnError(error: any) {
|
||||
if (error) this.reset();
|
||||
}
|
||||
|
||||
protected async loadItem(params: { name: string; namespace?: string }): Promise<T> {
|
||||
return this.api.get(params);
|
||||
}
|
||||
|
||||
@action
|
||||
async load(params: { name: string; namespace?: string }): Promise<T> {
|
||||
const { name, namespace } = params;
|
||||
let item = this.getByName(name, namespace);
|
||||
|
||||
if (!item) {
|
||||
item = await this.loadItem(params);
|
||||
const newItems = this.sortItems([...this.items, item]);
|
||||
|
||||
this.items.replace(newItems);
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
@action
|
||||
async loadFromPath(resourcePath: string) {
|
||||
const { namespace, name } = parseKubeApi(resourcePath);
|
||||
|
||||
return this.load({ name, namespace });
|
||||
}
|
||||
|
||||
protected async createItem(params: { name: string; namespace?: string }, data?: Partial<T>): Promise<T> {
|
||||
return this.api.create(params, data);
|
||||
}
|
||||
|
||||
async create(params: { name: string; namespace?: string }, data?: Partial<T>): Promise<T> {
|
||||
const newItem = await this.createItem(params, data);
|
||||
const items = this.sortItems([...this.items, newItem]);
|
||||
|
||||
this.items.replace(items);
|
||||
|
||||
return newItem;
|
||||
}
|
||||
|
||||
async update(item: T, data: Partial<T>): Promise<T> {
|
||||
const rawItem = await item.update(data);
|
||||
const newItem = new this.api.objectConstructor(rawItem);
|
||||
|
||||
ensureObjectSelfLink(this.api, newItem);
|
||||
|
||||
const index = this.items.findIndex(item => item.getId() === newItem.getId());
|
||||
|
||||
this.items.splice(index, 1, newItem);
|
||||
|
||||
return newItem;
|
||||
}
|
||||
|
||||
async remove(item: T) {
|
||||
await item.delete();
|
||||
this.items.remove(item);
|
||||
this.selectedItemsIds.delete(item.getId());
|
||||
}
|
||||
|
||||
async removeSelectedItems() {
|
||||
return Promise.all(this.selectedItems.map(this.remove));
|
||||
}
|
||||
|
||||
// collect items from watch-api events to avoid UI blowing up with huge streams of data
|
||||
protected eventsBuffer = observable.array<IKubeWatchEvent<KubeJsonApiData>>([], { deep: false });
|
||||
|
||||
protected bindWatchEventsUpdater(delay = 1000) {
|
||||
reaction(() => this.eventsBuffer.length, this.updateFromEventsBuffer, {
|
||||
delay
|
||||
});
|
||||
}
|
||||
|
||||
subscribe() {
|
||||
const abortController = new AbortController();
|
||||
|
||||
if (this.api.isNamespaced) {
|
||||
Promise.race([rejectPromiseBy(abortController.signal), Promise.all([this.contextReady, this.namespacesReady])])
|
||||
.then(() => {
|
||||
if (this.context.cluster.isGlobalWatchEnabled && this.loadedNamespaces.length === 0) {
|
||||
return this.watchNamespace("", abortController);
|
||||
}
|
||||
|
||||
for (const namespace of this.loadedNamespaces) {
|
||||
this.watchNamespace(namespace, abortController);
|
||||
}
|
||||
})
|
||||
.catch(noop); // ignore DOMExceptions
|
||||
} else {
|
||||
this.watchNamespace("", abortController);
|
||||
}
|
||||
|
||||
return () => abortController.abort();
|
||||
}
|
||||
|
||||
private watchNamespace(namespace: string, abortController: AbortController) {
|
||||
if (!this.api.getResourceVersion(namespace)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let timedRetry: NodeJS.Timeout;
|
||||
const watch = () => this.api.watch({
|
||||
namespace,
|
||||
abortController,
|
||||
callback
|
||||
});
|
||||
|
||||
const { signal } = abortController;
|
||||
|
||||
const callback = (data: IKubeWatchEvent<T>, error: any) => {
|
||||
if (!this.isLoaded || error?.type === "aborted") return;
|
||||
|
||||
if (error instanceof Response) {
|
||||
if (error.status === 404 || error.status === 401) {
|
||||
// api has gone, or credentials are not permitted, let's not retry
|
||||
return;
|
||||
}
|
||||
|
||||
// not sure what to do, best to retry
|
||||
clearTimeout(timedRetry);
|
||||
timedRetry = setTimeout(watch, 5000);
|
||||
} else if (error instanceof KubeStatus && error.code === 410) {
|
||||
clearTimeout(timedRetry);
|
||||
// resourceVersion has gone, let's try to reload
|
||||
timedRetry = setTimeout(() => {
|
||||
(
|
||||
namespace
|
||||
? this.loadAll({ namespaces: [namespace], reqInit: { signal } })
|
||||
: this.loadAll({ merge: false, reqInit: { signal } })
|
||||
).then(watch);
|
||||
}, 1000);
|
||||
} else if (error) { // not sure what to do, best to retry
|
||||
clearTimeout(timedRetry);
|
||||
timedRetry = setTimeout(watch, 5000);
|
||||
}
|
||||
|
||||
if (data) {
|
||||
this.eventsBuffer.push(data);
|
||||
}
|
||||
};
|
||||
|
||||
signal.addEventListener("abort", () => clearTimeout(timedRetry));
|
||||
watch();
|
||||
}
|
||||
|
||||
@action
|
||||
protected updateFromEventsBuffer() {
|
||||
const items = this.getItems();
|
||||
|
||||
for (const { type, object } of this.eventsBuffer.clear()) {
|
||||
const index = items.findIndex(item => item.getId() === object.metadata?.uid);
|
||||
const item = items[index];
|
||||
const api = apiManager.getApiByKind(object.kind, object.apiVersion);
|
||||
|
||||
switch (type) {
|
||||
case "ADDED":
|
||||
case "MODIFIED":
|
||||
const newItem = new api.objectConstructor(object) as T;
|
||||
|
||||
if (!item) {
|
||||
items.push(newItem);
|
||||
} else {
|
||||
items[index] = newItem;
|
||||
}
|
||||
break;
|
||||
case "DELETED":
|
||||
if (item) {
|
||||
items.splice(index, 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// update items
|
||||
this.items.replace(this.sortItems(items.slice(-this.bufferSize)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
// Base class for all kubernetes objects
|
||||
|
||||
import moment from "moment";
|
||||
import type { KubeJsonApiData, KubeJsonApiDataList, KubeJsonApiListMetadata, KubeJsonApiMetadata } from "./kube-json-api";
|
||||
import { autoBind, formatDuration } from "../utils";
|
||||
import type { ItemObject } from "../item.store";
|
||||
import { apiKube } from "./index";
|
||||
import type { JsonApiParams } from "./json-api";
|
||||
import { resourceApplierApi } from "./endpoints/resource-applier.api";
|
||||
import { hasOptionalProperty, hasTypedProperty, isObject, isString, bindPredicate, isTypedArray, isRecord } from "../../common/utils/type-narrowing";
|
||||
import _ from "lodash";
|
||||
|
||||
export type KubeObjectConstructor<K extends KubeObject> = (new (data: KubeJsonApiData | any) => K) & {
|
||||
kind?: string;
|
||||
namespaced?: boolean;
|
||||
apiBase?: string;
|
||||
};
|
||||
|
||||
export interface KubeObjectMetadata {
|
||||
uid: string;
|
||||
name: string;
|
||||
namespace?: string;
|
||||
creationTimestamp: string;
|
||||
resourceVersion: string;
|
||||
selfLink: string;
|
||||
deletionTimestamp?: string;
|
||||
finalizers?: string[];
|
||||
continue?: string; // provided when used "?limit=" query param to fetch objects list
|
||||
labels?: {
|
||||
[label: string]: string;
|
||||
};
|
||||
annotations?: {
|
||||
[annotation: string]: string;
|
||||
};
|
||||
ownerReferences?: {
|
||||
apiVersion: string;
|
||||
kind: string;
|
||||
name: string;
|
||||
uid: string;
|
||||
controller: boolean;
|
||||
blockOwnerDeletion: boolean;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface KubeStatusData {
|
||||
kind: string;
|
||||
apiVersion: string;
|
||||
code: number;
|
||||
message?: string;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export class KubeStatus {
|
||||
public readonly kind = "Status";
|
||||
public readonly apiVersion: string;
|
||||
public readonly code: number;
|
||||
public readonly message: string;
|
||||
public readonly reason: string;
|
||||
|
||||
constructor(data: KubeStatusData) {
|
||||
this.apiVersion = data.apiVersion;
|
||||
this.code = data.code;
|
||||
this.message = data.message || "";
|
||||
this.reason = data.reason || "";
|
||||
}
|
||||
}
|
||||
|
||||
export interface KubeObjectStatus {
|
||||
conditions?: {
|
||||
lastTransitionTime: string;
|
||||
message: string;
|
||||
reason: string;
|
||||
status: string;
|
||||
type?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export type KubeMetaField = keyof KubeObjectMetadata;
|
||||
|
||||
export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata, Status = any, Spec = any> implements ItemObject {
|
||||
static readonly kind: string;
|
||||
static readonly namespaced: boolean;
|
||||
|
||||
apiVersion: string;
|
||||
kind: string;
|
||||
metadata: Metadata;
|
||||
status?: Status;
|
||||
spec?: Spec;
|
||||
managedFields?: any;
|
||||
|
||||
static create(data: KubeJsonApiData) {
|
||||
return new KubeObject(data);
|
||||
}
|
||||
|
||||
static isNonSystem(item: KubeJsonApiData | KubeObject) {
|
||||
return !item.metadata.name.startsWith("system:");
|
||||
}
|
||||
|
||||
static isJsonApiData(object: unknown): object is KubeJsonApiData {
|
||||
return (
|
||||
isObject(object)
|
||||
&& hasTypedProperty(object, "kind", isString)
|
||||
&& hasTypedProperty(object, "apiVersion", isString)
|
||||
&& hasTypedProperty(object, "metadata", KubeObject.isKubeJsonApiMetadata)
|
||||
);
|
||||
}
|
||||
|
||||
static isKubeJsonApiListMetadata(object: unknown): object is KubeJsonApiListMetadata {
|
||||
return (
|
||||
isObject(object)
|
||||
&& hasOptionalProperty(object, "resourceVersion", isString)
|
||||
&& hasOptionalProperty(object, "selfLink", isString)
|
||||
);
|
||||
}
|
||||
|
||||
static isKubeJsonApiMetadata(object: unknown): object is KubeJsonApiMetadata {
|
||||
return (
|
||||
isObject(object)
|
||||
&& hasTypedProperty(object, "uid", isString)
|
||||
&& hasTypedProperty(object, "name", isString)
|
||||
&& hasTypedProperty(object, "resourceVersion", isString)
|
||||
&& hasOptionalProperty(object, "selfLink", isString)
|
||||
&& hasOptionalProperty(object, "namespace", isString)
|
||||
&& hasOptionalProperty(object, "creationTimestamp", isString)
|
||||
&& hasOptionalProperty(object, "continue", isString)
|
||||
&& hasOptionalProperty(object, "finalizers", bindPredicate(isTypedArray, isString))
|
||||
&& hasOptionalProperty(object, "labels", bindPredicate(isRecord, isString, isString))
|
||||
&& hasOptionalProperty(object, "annotations", bindPredicate(isRecord, isString, isString))
|
||||
);
|
||||
}
|
||||
|
||||
static isPartialJsonApiMetadata(object: unknown): object is Partial<KubeJsonApiMetadata> {
|
||||
return (
|
||||
isObject(object)
|
||||
&& hasOptionalProperty(object, "uid", isString)
|
||||
&& hasOptionalProperty(object, "name", isString)
|
||||
&& hasOptionalProperty(object, "resourceVersion", isString)
|
||||
&& hasOptionalProperty(object, "selfLink", isString)
|
||||
&& hasOptionalProperty(object, "namespace", isString)
|
||||
&& hasOptionalProperty(object, "creationTimestamp", isString)
|
||||
&& hasOptionalProperty(object, "continue", isString)
|
||||
&& hasOptionalProperty(object, "finalizers", bindPredicate(isTypedArray, isString))
|
||||
&& hasOptionalProperty(object, "labels", bindPredicate(isRecord, isString, isString))
|
||||
&& hasOptionalProperty(object, "annotations", bindPredicate(isRecord, isString, isString))
|
||||
);
|
||||
}
|
||||
|
||||
static isPartialJsonApiData(object: unknown): object is Partial<KubeJsonApiData> {
|
||||
return (
|
||||
isObject(object)
|
||||
&& hasOptionalProperty(object, "kind", isString)
|
||||
&& hasOptionalProperty(object, "apiVersion", isString)
|
||||
&& hasOptionalProperty(object, "metadata", KubeObject.isPartialJsonApiMetadata)
|
||||
);
|
||||
}
|
||||
|
||||
static isJsonApiDataList<T>(object: unknown, verifyItem: (val: unknown) => val is T): object is KubeJsonApiDataList<T> {
|
||||
return (
|
||||
isObject(object)
|
||||
&& hasTypedProperty(object, "kind", isString)
|
||||
&& hasTypedProperty(object, "apiVersion", isString)
|
||||
&& hasTypedProperty(object, "metadata", KubeObject.isKubeJsonApiListMetadata)
|
||||
&& hasTypedProperty(object, "items", bindPredicate(isTypedArray, verifyItem))
|
||||
);
|
||||
}
|
||||
|
||||
static stringifyLabels(labels?: { [name: string]: string }): string[] {
|
||||
if (!labels) return [];
|
||||
|
||||
return Object.entries(labels).map(([name, value]) => `${name}=${value}`);
|
||||
}
|
||||
|
||||
protected static readonly nonEditableFields = [
|
||||
"apiVersion",
|
||||
"kind",
|
||||
"metadata.name",
|
||||
"metadata.selfLink",
|
||||
"metadata.resourceVersion",
|
||||
"metadata.uid",
|
||||
"managedFields",
|
||||
"status",
|
||||
];
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
Object.assign(this, data);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
get selfLink() {
|
||||
return this.metadata.selfLink;
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.metadata.uid;
|
||||
}
|
||||
|
||||
getResourceVersion() {
|
||||
return this.metadata.resourceVersion;
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.metadata.name;
|
||||
}
|
||||
|
||||
getNs() {
|
||||
// avoid "null" serialization via JSON.stringify when post data
|
||||
return this.metadata.namespace || undefined;
|
||||
}
|
||||
|
||||
getTimeDiffFromNow(): number {
|
||||
return Date.now() - new Date(this.metadata.creationTimestamp).getTime();
|
||||
}
|
||||
|
||||
getAge(humanize = true, compact = true, fromNow = false): string | number {
|
||||
if (fromNow) {
|
||||
return moment(this.metadata.creationTimestamp).fromNow(); // "string", getTimeDiffFromNow() cannot be used
|
||||
}
|
||||
const diff = this.getTimeDiffFromNow();
|
||||
|
||||
if (humanize) {
|
||||
return formatDuration(diff, compact);
|
||||
}
|
||||
|
||||
return diff;
|
||||
}
|
||||
|
||||
getFinalizers(): string[] {
|
||||
return this.metadata.finalizers || [];
|
||||
}
|
||||
|
||||
getLabels(): string[] {
|
||||
return KubeObject.stringifyLabels(this.metadata.labels);
|
||||
}
|
||||
|
||||
getAnnotations(filter = false): string[] {
|
||||
const labels = KubeObject.stringifyLabels(this.metadata.annotations);
|
||||
|
||||
return filter ? labels.filter(label => {
|
||||
const skip = resourceApplierApi.annotations.some(key => label.startsWith(key));
|
||||
|
||||
return !skip;
|
||||
}) : labels;
|
||||
}
|
||||
|
||||
getOwnerRefs() {
|
||||
const refs = this.metadata?.ownerReferences || [];
|
||||
const namespace = this.getNs();
|
||||
|
||||
return refs.map(ownerRef => ({ ...ownerRef, namespace }));
|
||||
}
|
||||
|
||||
getSearchFields() {
|
||||
const { getName, getId, getNs, getAnnotations, getLabels } = this;
|
||||
|
||||
return [
|
||||
getName(),
|
||||
getNs(),
|
||||
getId(),
|
||||
...getLabels(),
|
||||
...getAnnotations(true),
|
||||
];
|
||||
}
|
||||
|
||||
toPlainObject(): object {
|
||||
return JSON.parse(JSON.stringify(this));
|
||||
}
|
||||
|
||||
// use unified resource-applier api for updating all k8s objects
|
||||
async update(data: Partial<this>): Promise<KubeJsonApiData | null> {
|
||||
for (const field of KubeObject.nonEditableFields) {
|
||||
if (!_.isEqual(_.get(this, field), _.get(data, field))) {
|
||||
throw new Error(`Failed to update Kube Object: ${field} has been modified`);
|
||||
}
|
||||
}
|
||||
|
||||
return resourceApplierApi.update({
|
||||
...this.toPlainObject(),
|
||||
...data,
|
||||
});
|
||||
}
|
||||
|
||||
delete(params?: JsonApiParams) {
|
||||
return apiKube.del(this.selfLink, params);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
// Kubernetes watch-api client
|
||||
// API: https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams
|
||||
|
||||
import type { KubeObjectStore } from "./kube-object.store";
|
||||
import type { ClusterContext } from "./cluster-context";
|
||||
|
||||
import plimit from "p-limit";
|
||||
import { comparer, observable, reaction, makeObservable } from "mobx";
|
||||
import { autoBind, Disposer, noop } from "../utils";
|
||||
import type { KubeApi } from "./kube-api";
|
||||
import type { KubeJsonApiData } from "./kube-json-api";
|
||||
import { isDebugging, isProduction } from "../vars";
|
||||
import type { KubeObject } from "./kube-object";
|
||||
|
||||
export interface IKubeWatchEvent<T extends KubeJsonApiData> {
|
||||
type: "ADDED" | "MODIFIED" | "DELETED" | "ERROR";
|
||||
object?: T;
|
||||
}
|
||||
|
||||
export interface IKubeWatchSubscribeStoreOptions {
|
||||
namespaces?: string[]; // default: all accessible namespaces
|
||||
preload?: boolean; // preload store items, default: true
|
||||
waitUntilLoaded?: boolean; // subscribe only after loading all stores, default: true
|
||||
loadOnce?: boolean; // check store.isLoaded to skip loading if done already, default: false
|
||||
}
|
||||
|
||||
export interface IKubeWatchLog {
|
||||
message: string | string[] | Error;
|
||||
meta?: object;
|
||||
cssStyle?: string;
|
||||
}
|
||||
|
||||
export class KubeWatchApi {
|
||||
@observable context: ClusterContext = null;
|
||||
|
||||
constructor() {
|
||||
makeObservable(this);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
isAllowedApi(api: KubeApi<KubeObject>): boolean {
|
||||
return Boolean(this.context?.cluster.isAllowedResource(api.kind));
|
||||
}
|
||||
|
||||
preloadStores(stores: KubeObjectStore<KubeObject>[], opts: { namespaces?: string[], loadOnce?: boolean } = {}) {
|
||||
const limitRequests = plimit(1); // load stores one by one to allow quick skipping when fast clicking btw pages
|
||||
const preloading: Promise<any>[] = [];
|
||||
|
||||
for (const store of stores) {
|
||||
preloading.push(limitRequests(async () => {
|
||||
if (store.isLoaded && opts.loadOnce) return; // skip
|
||||
|
||||
return store.loadAll({ namespaces: opts.namespaces });
|
||||
}));
|
||||
}
|
||||
|
||||
return {
|
||||
loading: Promise.allSettled(preloading),
|
||||
cancelLoading: () => limitRequests.clearQueue(),
|
||||
};
|
||||
}
|
||||
|
||||
subscribeStores(stores: KubeObjectStore<KubeObject>[], opts: IKubeWatchSubscribeStoreOptions = {}): Disposer {
|
||||
const { preload = true, waitUntilLoaded = true, loadOnce = false, } = opts;
|
||||
const subscribingNamespaces = opts.namespaces ?? this.context?.allNamespaces ?? [];
|
||||
const unsubscribeList: Function[] = [];
|
||||
let isUnsubscribed = false;
|
||||
|
||||
const load = (namespaces = subscribingNamespaces) => this.preloadStores(stores, { namespaces, loadOnce });
|
||||
let preloading = preload && load();
|
||||
let cancelReloading: Disposer = noop;
|
||||
|
||||
const subscribe = () => {
|
||||
if (isUnsubscribed) return;
|
||||
|
||||
stores.forEach((store) => {
|
||||
unsubscribeList.push(store.subscribe());
|
||||
});
|
||||
};
|
||||
|
||||
if (preloading) {
|
||||
if (waitUntilLoaded) {
|
||||
preloading.loading.then(subscribe, error => {
|
||||
this.log({
|
||||
message: new Error("Loading stores has failed"),
|
||||
meta: { stores, error, options: opts },
|
||||
});
|
||||
});
|
||||
} else {
|
||||
subscribe();
|
||||
}
|
||||
|
||||
// reload stores only for context namespaces change
|
||||
cancelReloading = reaction(() => this.context?.contextNamespaces, namespaces => {
|
||||
preloading?.cancelLoading();
|
||||
unsubscribeList.forEach(unsubscribe => unsubscribe());
|
||||
unsubscribeList.length = 0;
|
||||
preloading = load(namespaces);
|
||||
preloading.loading.then(subscribe);
|
||||
}, {
|
||||
equals: comparer.shallow,
|
||||
});
|
||||
}
|
||||
|
||||
// unsubscribe
|
||||
return () => {
|
||||
if (isUnsubscribed) return;
|
||||
isUnsubscribed = true;
|
||||
cancelReloading();
|
||||
preloading?.cancelLoading();
|
||||
unsubscribeList.forEach(unsubscribe => unsubscribe());
|
||||
unsubscribeList.length = 0;
|
||||
};
|
||||
}
|
||||
|
||||
protected log({ message, cssStyle = "", meta = {} }: IKubeWatchLog) {
|
||||
if (isProduction && !isDebugging) {
|
||||
return;
|
||||
}
|
||||
|
||||
const logInfo = [`%c[KUBE-WATCH-API]:`, `font-weight: bold; ${cssStyle}`, message].flat().map(String);
|
||||
const logMeta = {
|
||||
time: new Date().toLocaleString(),
|
||||
...meta,
|
||||
};
|
||||
|
||||
if (message instanceof Error) {
|
||||
console.error(...logInfo, logMeta);
|
||||
} else {
|
||||
console.info(...logInfo, logMeta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const kubeWatchApi = new KubeWatchApi();
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import get from "lodash/get";
|
||||
import { KubeObject } from "./kube-object";
|
||||
|
||||
export interface IToleration {
|
||||
key?: string;
|
||||
operator?: string;
|
||||
effect?: string;
|
||||
value?: string;
|
||||
tolerationSeconds?: number;
|
||||
}
|
||||
|
||||
interface IMatchExpression {
|
||||
key: string;
|
||||
operator: string;
|
||||
values: string[];
|
||||
}
|
||||
|
||||
interface INodeAffinity {
|
||||
nodeSelectorTerms?: {
|
||||
matchExpressions: IMatchExpression[];
|
||||
}[];
|
||||
weight: number;
|
||||
preference: {
|
||||
matchExpressions: IMatchExpression[];
|
||||
};
|
||||
}
|
||||
|
||||
interface IPodAffinity {
|
||||
labelSelector: {
|
||||
matchExpressions: IMatchExpression[];
|
||||
};
|
||||
topologyKey: string;
|
||||
}
|
||||
|
||||
export interface IAffinity {
|
||||
nodeAffinity?: {
|
||||
requiredDuringSchedulingIgnoredDuringExecution?: INodeAffinity[];
|
||||
preferredDuringSchedulingIgnoredDuringExecution?: INodeAffinity[];
|
||||
};
|
||||
podAffinity?: {
|
||||
requiredDuringSchedulingIgnoredDuringExecution?: IPodAffinity[];
|
||||
preferredDuringSchedulingIgnoredDuringExecution?: IPodAffinity[];
|
||||
};
|
||||
podAntiAffinity?: {
|
||||
requiredDuringSchedulingIgnoredDuringExecution?: IPodAffinity[];
|
||||
preferredDuringSchedulingIgnoredDuringExecution?: IPodAffinity[];
|
||||
};
|
||||
}
|
||||
|
||||
export class WorkloadKubeObject extends KubeObject {
|
||||
getSelectors(): string[] {
|
||||
const selector = this.spec.selector;
|
||||
|
||||
return KubeObject.stringifyLabels(selector ? selector.matchLabels : null);
|
||||
}
|
||||
|
||||
getNodeSelectors(): string[] {
|
||||
const nodeSelector = get(this, "spec.template.spec.nodeSelector");
|
||||
|
||||
return KubeObject.stringifyLabels(nodeSelector);
|
||||
}
|
||||
|
||||
getTemplateLabels(): string[] {
|
||||
const labels = get(this, "spec.template.metadata.labels");
|
||||
|
||||
return KubeObject.stringifyLabels(labels);
|
||||
}
|
||||
|
||||
getTolerations(): IToleration[] {
|
||||
return get(this, "spec.template.spec.tolerations", []);
|
||||
}
|
||||
|
||||
getAffinity(): IAffinity {
|
||||
return get(this, "spec.template.spec.affinity");
|
||||
}
|
||||
|
||||
getAffinityNumber() {
|
||||
const affinity = this.getAffinity();
|
||||
|
||||
if (!affinity) return 0;
|
||||
|
||||
return Object.keys(affinity).length;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { app, ipcMain, remote } from "electron";
|
||||
import winston from "winston";
|
||||
import { consoleFormat } from "winston-console-format";
|
||||
import { isDebugging, isTestEnv } from "./vars";
|
||||
import BrowserConsole from "winston-transport-browserconsole";
|
||||
|
||||
|
||||
const logLevel = process.env.LOG_LEVEL ? process.env.LOG_LEVEL : isDebugging ? "debug" : "info";
|
||||
let consoleOptions: winston.transports.ConsoleTransportOptions;
|
||||
|
||||
if (ipcMain) {
|
||||
consoleOptions = {
|
||||
handleExceptions: false,
|
||||
level: logLevel,
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize({ level: true, message: false}),
|
||||
winston.format.padLevels(),
|
||||
winston.format.ms(),
|
||||
consoleFormat({
|
||||
showMeta: true,
|
||||
inspectOptions: {
|
||||
depth: 4,
|
||||
colors: true,
|
||||
maxArrayLength: 10,
|
||||
breakLength: 120,
|
||||
compact: Infinity,
|
||||
},
|
||||
})
|
||||
)
|
||||
};
|
||||
} else {
|
||||
consoleOptions = {
|
||||
handleExceptions: false,
|
||||
level: logLevel,
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize({ level: true, message: false}),
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
const fileOptions: winston.transports.FileTransportOptions = {
|
||||
handleExceptions: false,
|
||||
level: logLevel,
|
||||
filename: "lens.log",
|
||||
dirname: (app ?? remote?.app)?.getPath("logs"),
|
||||
maxsize: 16 * 1024,
|
||||
maxFiles: 16,
|
||||
tailable: true,
|
||||
};
|
||||
const logger = winston.createLogger({
|
||||
format: winston.format.combine(
|
||||
winston.format.simple()
|
||||
),
|
||||
transports: [
|
||||
ipcMain ? new winston.transports.Console(consoleOptions) : new BrowserConsole(),
|
||||
...(isTestEnv ? [] : [new winston.transports.File(fileOptions)]),
|
||||
],
|
||||
});
|
||||
|
||||
export default logger;
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import moment from "moment";
|
||||
import { formatDuration } from "../formatDuration";
|
||||
|
||||
const second = 1000;
|
||||
const minute = 60 * second;
|
||||
const hour = 60 * minute;
|
||||
const day = 24 * hour;
|
||||
const year = 365 * day;
|
||||
|
||||
describe("human format durations", () => {
|
||||
test("small duration should output something", () => {
|
||||
expect(formatDuration(1)).toBe("0s");
|
||||
expect(formatDuration(3)).toBe("0s");
|
||||
});
|
||||
|
||||
test("returns seconds for duration under 1 min", () => {
|
||||
const res = formatDuration(8 * second);
|
||||
|
||||
expect(res).toBe("8s");
|
||||
});
|
||||
|
||||
test("zero duration should output something", () => {
|
||||
expect(formatDuration(0)).toBe("0s");
|
||||
});
|
||||
|
||||
describe("when compact is true", () => {
|
||||
|
||||
test("duration under 3 hours return minutes", () => {
|
||||
const res = formatDuration(1 * hour + 35 * minute);
|
||||
|
||||
expect(res).toBe("95m");
|
||||
});
|
||||
|
||||
test("duration under 8 hours return hours and minutes", () => {
|
||||
const res = formatDuration(6 * hour + 15 * minute + 20 * second);
|
||||
|
||||
expect(res).toBe("6h15m");
|
||||
});
|
||||
|
||||
test("duration under 48 hours return hours", () => {
|
||||
const res = formatDuration(1 * day + 4 * hour + 15 * minute);
|
||||
|
||||
expect(res).toBe("28h");
|
||||
});
|
||||
|
||||
test("duration under 2 years return days", () => {
|
||||
const res = formatDuration(400 * day + 4 * hour + 15 * minute);
|
||||
|
||||
expect(res).toBe("400d");
|
||||
});
|
||||
|
||||
test("durations less than 8 years returns years and days", () => {
|
||||
const timeValue = Date.now() - new Date(moment().subtract(2, "years").subtract(5, "days").subtract(2, "hours").toDate()).getTime();
|
||||
|
||||
const res = formatDuration(timeValue);
|
||||
|
||||
expect(res).toBe("2y5d");
|
||||
});
|
||||
|
||||
test("durations more than 8 years returns years", () => {
|
||||
const timeValue = Date.now() - new Date(moment().subtract(9, "years").subtract(5, "days").toDate()).getTime();
|
||||
|
||||
const res = formatDuration(timeValue);
|
||||
|
||||
expect(res).toBe("9y");
|
||||
});
|
||||
|
||||
test("durations more than 8 years returns years", () => {
|
||||
const res = formatDuration(10 * year + 25 * day);
|
||||
|
||||
expect(res).toBe("10y");
|
||||
});
|
||||
test("durations shouldn't include zero magnitude parts", () => {
|
||||
const zeroSeconds = formatDuration(8 * minute);
|
||||
|
||||
expect(zeroSeconds).toBe("8m");
|
||||
|
||||
const zeroMinutes = formatDuration(8 * hour + 15 * minute);
|
||||
|
||||
expect(zeroMinutes).toBe("8h");
|
||||
|
||||
const zeroHours = formatDuration(6 * day + 2 * minute);
|
||||
|
||||
expect(zeroHours).toBe("6d");
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -48,3 +48,12 @@ export function getClusterFrameUrl(clusterId: ClusterId) {
|
||||
export function getHostedClusterId(): ClusterId | undefined {
|
||||
return getClusterIdFromHost(location.host);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true only if code is running within a cluster iframe context
|
||||
*/
|
||||
export function isClusterPageContext(): boolean {
|
||||
if (typeof window === "undefined") return false;
|
||||
|
||||
return !!getClusterIdFromHost(window.location.host);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
// Helper to convert CPU K8S units to numbers
|
||||
|
||||
const thousand = 1000;
|
||||
const million = thousand * thousand;
|
||||
const shortBillion = thousand * million;
|
||||
|
||||
export function cpuUnitsToNumber(cpu: string) {
|
||||
const cpuNum = parseInt(cpu);
|
||||
|
||||
if (cpu.includes("m")) return cpuNum / thousand;
|
||||
if (cpu.includes("u")) return cpuNum / million;
|
||||
if (cpu.includes("n")) return cpuNum / shortBillion;
|
||||
|
||||
return parseFloat(cpu);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
// Helper to convert memory from units Ki, Mi, Gi, Ti, Pi to bytes and vise versa
|
||||
|
||||
const base = 1024;
|
||||
const suffixes = ["K", "M", "G", "T", "P", "E"]; // Equivalents: Ki, Mi, Gi, Ti, Pi, Ei
|
||||
|
||||
export function unitsToBytes(value: string) {
|
||||
if (!suffixes.some(suffix => value.includes(suffix))) {
|
||||
return parseFloat(value);
|
||||
}
|
||||
|
||||
const suffix = value.replace(/[0-9]|i|\./g, "");
|
||||
const index = suffixes.indexOf(suffix);
|
||||
|
||||
return parseInt(
|
||||
(parseFloat(value) * Math.pow(base, index + 1)).toFixed(1)
|
||||
);
|
||||
}
|
||||
|
||||
export function bytesToUnits(bytes: number, precision = 1) {
|
||||
const sizes = ["B", ...suffixes];
|
||||
const index = Math.floor(Math.log(bytes) / Math.log(base));
|
||||
|
||||
if (!bytes) {
|
||||
return "N/A";
|
||||
}
|
||||
|
||||
if (index === 0) {
|
||||
return `${bytes}${sizes[index]}`;
|
||||
}
|
||||
|
||||
return `${(bytes / (1024 ** index)).toFixed(precision)}${sizes[index]}i`;
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import moment from "moment";
|
||||
|
||||
/**
|
||||
* This function formats durations in a more human readable form.
|
||||
* @param timeValue the duration in milliseconds to format
|
||||
*/
|
||||
export function formatDuration(timeValue: number, compact = true) {
|
||||
const duration = moment.duration(timeValue, "milliseconds");
|
||||
const seconds = Math.floor(duration.asSeconds());
|
||||
const separator = compact ? "": " ";
|
||||
|
||||
if (seconds < 0) {
|
||||
return "0s";
|
||||
} else if (seconds < 60*2 ) {
|
||||
return `${seconds}s`;
|
||||
}
|
||||
|
||||
const minutes = Math.floor(duration.asMinutes());
|
||||
|
||||
if (minutes < 10) {
|
||||
const seconds = duration.seconds();
|
||||
|
||||
return getMeaningfulValues([minutes, seconds], ["m", "s"], separator);
|
||||
} else if (minutes < 60 * 3) {
|
||||
if (!compact) {
|
||||
return getMeaningfulValues([minutes, duration.seconds()], ["m", "s"]);
|
||||
}
|
||||
|
||||
return `${minutes}m`;
|
||||
}
|
||||
|
||||
const hours = Math.floor(duration.asHours());
|
||||
|
||||
if(hours < 8) {
|
||||
const minutes = duration.minutes();
|
||||
|
||||
return getMeaningfulValues([hours, minutes], ["h", "m"], separator);
|
||||
} else if (hours < 48) {
|
||||
if (compact) {
|
||||
return `${hours}h`;
|
||||
} else {
|
||||
return getMeaningfulValues([hours, duration.minutes()], ["h", "m"]);
|
||||
}
|
||||
}
|
||||
|
||||
const days = Math.floor(duration.asDays());
|
||||
|
||||
if (days < 8) {
|
||||
const hours = duration.hours();
|
||||
|
||||
if (compact) {
|
||||
return getMeaningfulValues([days, hours], ["d", "h"], separator);
|
||||
} else {
|
||||
return getMeaningfulValues([days, hours, duration.minutes()], ["d", "h", "m"]);
|
||||
}
|
||||
}
|
||||
const years = Math.floor(duration.asYears());
|
||||
|
||||
if (years < 2) {
|
||||
if (compact) {
|
||||
return `${days}d`;
|
||||
} else {
|
||||
return getMeaningfulValues([days, duration.hours(), duration.minutes()], ["d", "h", "m"]);
|
||||
}
|
||||
} else if (years < 8) {
|
||||
const days = duration.days();
|
||||
|
||||
if (compact) {
|
||||
return getMeaningfulValues([years, days], ["y", "d"], separator);
|
||||
}
|
||||
}
|
||||
|
||||
if (compact) {
|
||||
return `${years}y`;
|
||||
}
|
||||
|
||||
return getMeaningfulValues([years, duration.days(), duration.hours(), duration.minutes()], ["y", "d", "h", "m"]);
|
||||
}
|
||||
|
||||
function getMeaningfulValues(values: number[], suffixes: string[], separator = " ") {
|
||||
return values
|
||||
.map((a, i): [number, string] => [a, suffixes[i]])
|
||||
.filter(([dur]) => dur > 0)
|
||||
.map(([dur, suf]) => dur + suf)
|
||||
.join(separator);
|
||||
}
|
||||
@@ -37,6 +37,7 @@ export * from "./defineGlobal";
|
||||
export * from "./delay";
|
||||
export * from "./disposer";
|
||||
export * from "./downloadFile";
|
||||
export * from "./formatDuration";
|
||||
export * from "./escapeRegExp";
|
||||
export * from "./extended-map";
|
||||
export * from "./getRandId";
|
||||
@@ -54,6 +55,8 @@ export * from "./toggle-set";
|
||||
export * from "./toJS";
|
||||
export * from "./type-narrowing";
|
||||
export * from "./types";
|
||||
export * from "./convertMemory";
|
||||
export * from "./convertCpu";
|
||||
|
||||
import * as iter from "./iter";
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Readable } from "readable-stream";
|
||||
import type { TypedArray } from "type-fest";
|
||||
|
||||
/**
|
||||
* ReadableWebToNodeStream
|
||||
*
|
||||
* Copied from https://github.com/Borewit/readable-web-to-node-stream
|
||||
*
|
||||
* Adds read error handler
|
||||
*
|
||||
* */
|
||||
export class ReadableWebToNodeStream<T extends TypedArray> extends Readable {
|
||||
|
||||
public bytesRead = 0;
|
||||
public released = false;
|
||||
|
||||
/**
|
||||
* Default web API stream reader
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultReader
|
||||
*/
|
||||
private reader: ReadableStreamReader<T>;
|
||||
private pendingRead: Promise<ReadableStreamDefaultReadResult<T>>;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param stream ReadableStream: https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream
|
||||
*/
|
||||
constructor(stream: ReadableStream<T>) {
|
||||
super();
|
||||
this.reader = stream.getReader();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of readable._read(size).
|
||||
* When readable._read() is called, if data is available from the resource,
|
||||
* the implementation should begin pushing that data into the read queue
|
||||
* https://nodejs.org/api/stream.html#stream_readable_read_size_1
|
||||
*/
|
||||
public async _read() {
|
||||
// Should start pushing data into the queue
|
||||
// Read data from the underlying Web-API-readable-stream
|
||||
if (this.released) {
|
||||
this.push(null); // Signal EOF
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.pendingRead = this.reader.read();
|
||||
const data = await this.pendingRead;
|
||||
|
||||
// clear the promise before pushing pushing new data to the queue and allow sequential calls to _read()
|
||||
delete this.pendingRead;
|
||||
|
||||
if (data.done || this.released) {
|
||||
this.push(null); // Signal EOF
|
||||
} else {
|
||||
this.bytesRead += data.value.length;
|
||||
this.push(data.value); // Push new data to the queue
|
||||
}
|
||||
} catch (error) {
|
||||
this.push(null); // Signal EOF
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is no unresolved read call to Web-API ReadableStream immediately returns;
|
||||
* otherwise will wait until the read is resolved.
|
||||
*/
|
||||
public async waitForReadToComplete() {
|
||||
if (this.pendingRead) {
|
||||
await this.pendingRead;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close wrapper
|
||||
*/
|
||||
public async close(): Promise<void> {
|
||||
await this.syncAndRelease();
|
||||
}
|
||||
|
||||
private async syncAndRelease() {
|
||||
this.released = true;
|
||||
await this.waitForReadToComplete();
|
||||
await this.reader.releaseLock();
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,7 @@ export const publicPath = "/build/" as string;
|
||||
// Webpack build paths
|
||||
export const contextDir = process.cwd();
|
||||
export const buildDir = path.join(contextDir, "static", publicPath);
|
||||
export const preloadEntrypoint = path.join(contextDir, "src/preload.ts");
|
||||
export const mainDir = path.join(contextDir, "src/main");
|
||||
export const rendererDir = path.join(contextDir, "src/renderer");
|
||||
export const htmlTemplate = path.resolve(rendererDir, "template.html");
|
||||
|
||||
Reference in new issue
Block a user