mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Turn on strict mode in tsconfig.json
- Add route, clusterRoute, and payloadValidatedClusterRoute helper functions to improve types with backend routes - Turn on the following new lints: - react/jsx-first-prop-new-line - react/jsx-wrap-multilines - react/jsx-one-expression-per-line - react/jsx-max-props-per-line - react/jsx-indent - react/jsx-indent-props Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
e532b90b72
commit
f594bf03f3
22
.eslintrc.js
22
.eslintrc.js
@ -139,6 +139,28 @@ module.exports = {
|
||||
"requireLast": false,
|
||||
},
|
||||
}],
|
||||
"react/jsx-max-props-per-line": ["error", {
|
||||
"maximum": {
|
||||
"single": 2,
|
||||
"multi": 1,
|
||||
},
|
||||
}],
|
||||
"react/jsx-first-prop-new-line": ["error", "multiline"],
|
||||
"react/jsx-one-expression-per-line": ["error", {
|
||||
"allow": "single-child",
|
||||
}],
|
||||
"react/jsx-indent": ["error", 2],
|
||||
"react/jsx-indent-props": ["error", 2],
|
||||
"react/jsx-closing-tag-location": "error",
|
||||
"react/jsx-wrap-multilines": ["error", {
|
||||
"declaration": "parens-new-line",
|
||||
"assignment": "parens-new-line",
|
||||
"return": "parens-new-line",
|
||||
"arrow": "parens-new-line",
|
||||
"condition": "parens-new-line",
|
||||
"logical": "parens-new-line",
|
||||
"prop": "parens-new-line",
|
||||
}],
|
||||
"react/display-name": "off",
|
||||
"space-before-function-paren": "off",
|
||||
"@typescript-eslint/space-before-function-paren": ["error", {
|
||||
|
||||
@ -5,11 +5,11 @@
|
||||
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import defaultBaseLensTheme from "../src/renderer/themes/lens-dark.json";
|
||||
import defaultBaseLensTheme from "../src/renderer/themes/lens-dark";
|
||||
|
||||
const outputCssFile = path.resolve("src/renderer/themes/theme-vars.css");
|
||||
|
||||
const banner = `/*
|
||||
const banner = `/*
|
||||
Generated Lens theme CSS-variables, don't edit manually.
|
||||
To refresh file run $: yarn run ts-node build/${path.basename(__filename)}
|
||||
*/`;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kube-object-event-status",
|
||||
"version": "0.0.1",
|
||||
"version": "5.5.0-alpha.0.1649256751918",
|
||||
"description": "Adds kube object status from events",
|
||||
"renderer": "dist/renderer.js",
|
||||
"lens": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "lens-metrics-cluster-feature",
|
||||
"version": "0.0.1",
|
||||
"version": "5.5.0-alpha.0.1649256751918",
|
||||
"description": "Lens metrics cluster feature",
|
||||
"renderer": "dist/renderer.js",
|
||||
"lens": {
|
||||
|
||||
@ -208,14 +208,14 @@ export class MetricsSettings extends React.Component<MetricsSettingsProps> {
|
||||
<section>
|
||||
<SubTitle title="Prometheus" />
|
||||
<FormSwitch
|
||||
control={
|
||||
control={(
|
||||
<Switcher
|
||||
disabled={this.featureStates.kubeStateMetrics === undefined || !this.isTogglable}
|
||||
checked={!!this.featureStates.prometheus && this.props.cluster.status.phase == "connected"}
|
||||
onChange={v => this.togglePrometheus(v.target.checked)}
|
||||
name="prometheus"
|
||||
/>
|
||||
}
|
||||
)}
|
||||
label="Enable bundled Prometheus metrics stack"
|
||||
/>
|
||||
<small className="hint">
|
||||
@ -226,14 +226,14 @@ export class MetricsSettings extends React.Component<MetricsSettingsProps> {
|
||||
<section>
|
||||
<SubTitle title="Kube State Metrics" />
|
||||
<FormSwitch
|
||||
control={
|
||||
control={(
|
||||
<Switcher
|
||||
disabled={this.featureStates.kubeStateMetrics === undefined || !this.isTogglable}
|
||||
checked={!!this.featureStates.kubeStateMetrics && this.props.cluster.status.phase == "connected"}
|
||||
onChange={v => this.toggleKubeStateMetrics(v.target.checked)}
|
||||
name="node-exporter"
|
||||
/>
|
||||
}
|
||||
)}
|
||||
label="Enable bundled kube-state-metrics stack"
|
||||
/>
|
||||
<small className="hint">
|
||||
@ -245,14 +245,14 @@ export class MetricsSettings extends React.Component<MetricsSettingsProps> {
|
||||
<section>
|
||||
<SubTitle title="Node Exporter" />
|
||||
<FormSwitch
|
||||
control={
|
||||
control={(
|
||||
<Switcher
|
||||
disabled={this.featureStates.nodeExporter === undefined || !this.isTogglable}
|
||||
checked={!!this.featureStates.nodeExporter && this.props.cluster.status.phase == "connected"}
|
||||
onChange={v => this.toggleNodeExporter(v.target.checked)}
|
||||
name="node-exporter"
|
||||
/>
|
||||
}
|
||||
)}
|
||||
label="Enable bundled node-exporter stack"
|
||||
/>
|
||||
<small className="hint">
|
||||
@ -271,9 +271,11 @@ export class MetricsSettings extends React.Component<MetricsSettingsProps> {
|
||||
className="w-60 h-14"
|
||||
/>
|
||||
|
||||
{this.canUpgrade && (<small className="hint">
|
||||
An update is available for enabled metrics components.
|
||||
</small>)}
|
||||
{this.canUpgrade && (
|
||||
<small className="hint">
|
||||
An update is available for enabled metrics components.
|
||||
</small>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "lens-node-menu",
|
||||
"version": "0.0.1",
|
||||
"version": "5.5.0-alpha.0.1649256751918",
|
||||
"description": "Lens node menu",
|
||||
"renderer": "dist/renderer.js",
|
||||
"lens": {
|
||||
|
||||
@ -68,7 +68,9 @@ export function NodeMenu(props: NodeMenuProps) {
|
||||
labelOk: `Drain Node`,
|
||||
message: (
|
||||
<p>
|
||||
Are you sure you want to drain <b>{nodeName}</b>?
|
||||
{"Are you sure you want to drain "}
|
||||
<b>{nodeName}</b>
|
||||
?
|
||||
</p>
|
||||
),
|
||||
});
|
||||
@ -77,26 +79,42 @@ export function NodeMenu(props: NodeMenuProps) {
|
||||
return (
|
||||
<>
|
||||
<MenuItem onClick={shell}>
|
||||
<Icon svg="ssh" interactive={toolbar} tooltip={toolbar && "Node shell"}/>
|
||||
<Icon
|
||||
svg="ssh"
|
||||
interactive={toolbar}
|
||||
tooltip={toolbar && "Node shell"}
|
||||
/>
|
||||
<span className="title">Shell</span>
|
||||
</MenuItem>
|
||||
{
|
||||
node.isUnschedulable()
|
||||
? (
|
||||
<MenuItem onClick={unCordon}>
|
||||
<Icon material="play_circle_filled" tooltip={toolbar && "Uncordon"} interactive={toolbar} />
|
||||
<Icon
|
||||
material="play_circle_filled"
|
||||
tooltip={toolbar && "Uncordon"}
|
||||
interactive={toolbar}
|
||||
/>
|
||||
<span className="title">Uncordon</span>
|
||||
</MenuItem>
|
||||
)
|
||||
: (
|
||||
<MenuItem onClick={cordon}>
|
||||
<Icon material="pause_circle_filled" tooltip={toolbar && "Cordon"} interactive={toolbar} />
|
||||
<Icon
|
||||
material="pause_circle_filled"
|
||||
tooltip={toolbar && "Cordon"}
|
||||
interactive={toolbar}
|
||||
/>
|
||||
<span className="title">Cordon</span>
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
<MenuItem onClick={drain}>
|
||||
<Icon material="delete_sweep" tooltip={toolbar && "Drain"} interactive={toolbar}/>
|
||||
<Icon
|
||||
material="delete_sweep"
|
||||
tooltip={toolbar && "Drain"}
|
||||
interactive={toolbar}
|
||||
/>
|
||||
<span className="title">Drain</span>
|
||||
</MenuItem>
|
||||
</>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "lens-pod-menu",
|
||||
"version": "0.0.1",
|
||||
"version": "5.5.0-alpha.0.1649256751918",
|
||||
"description": "Lens pod menu",
|
||||
"renderer": "dist/renderer.js",
|
||||
"lens": {
|
||||
|
||||
@ -71,7 +71,11 @@ export class PodAttachMenu extends React.Component<PodAttachMenuProps> {
|
||||
|
||||
return (
|
||||
<MenuItem onClick={Util.prevDefault(() => this.attachToPod(containers[0].name))}>
|
||||
<Icon material="pageview" interactive={toolbar} tooltip={toolbar && "Attach to Pod"}/>
|
||||
<Icon
|
||||
material="pageview"
|
||||
interactive={toolbar}
|
||||
tooltip={toolbar && "Attach to Pod"}
|
||||
/>
|
||||
<span className="title">Attach Pod</span>
|
||||
{containers.length > 1 && (
|
||||
<>
|
||||
@ -82,7 +86,11 @@ export class PodAttachMenu extends React.Component<PodAttachMenuProps> {
|
||||
const { name } = container;
|
||||
|
||||
return (
|
||||
<MenuItem key={name} onClick={Util.prevDefault(() => this.attachToPod(name))} className="flex align-center">
|
||||
<MenuItem
|
||||
key={name}
|
||||
onClick={Util.prevDefault(() => this.attachToPod(name))}
|
||||
className="flex align-center"
|
||||
>
|
||||
<StatusBrick/>
|
||||
<span>{name}</span>
|
||||
</MenuItem>
|
||||
|
||||
@ -46,7 +46,11 @@ export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
|
||||
|
||||
return (
|
||||
<MenuItem onClick={Util.prevDefault(() => this.showLogs(containers[0]))}>
|
||||
<Icon material="subject" interactive={toolbar} tooltip={toolbar && "Pod Logs"}/>
|
||||
<Icon
|
||||
material="subject"
|
||||
interactive={toolbar}
|
||||
tooltip={toolbar && "Pod Logs"}
|
||||
/>
|
||||
<span className="title">Logs</span>
|
||||
{containers.length > 1 && (
|
||||
<>
|
||||
@ -63,7 +67,11 @@ export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<MenuItem key={name} onClick={Util.prevDefault(() => this.showLogs(container))} className="flex align-center">
|
||||
<MenuItem
|
||||
key={name}
|
||||
onClick={Util.prevDefault(() => this.showLogs(container))}
|
||||
className="flex align-center"
|
||||
>
|
||||
{brick}
|
||||
<span>{name}</span>
|
||||
</MenuItem>
|
||||
|
||||
@ -79,7 +79,11 @@ export class PodShellMenu extends React.Component<PodShellMenuProps> {
|
||||
|
||||
return (
|
||||
<MenuItem onClick={Util.prevDefault(() => this.execShell(containers[0].name))}>
|
||||
<Icon svg="ssh" interactive={toolbar} tooltip={toolbar && "Pod Shell"} />
|
||||
<Icon
|
||||
svg="ssh"
|
||||
interactive={toolbar}
|
||||
tooltip={toolbar && "Pod Shell"}
|
||||
/>
|
||||
<span className="title">Shell</span>
|
||||
{containers.length > 1 && (
|
||||
<>
|
||||
@ -90,7 +94,11 @@ export class PodShellMenu extends React.Component<PodShellMenuProps> {
|
||||
const { name } = container;
|
||||
|
||||
return (
|
||||
<MenuItem key={name} onClick={Util.prevDefault(() => this.execShell(name))} className="flex align-center">
|
||||
<MenuItem
|
||||
key={name}
|
||||
onClick={Util.prevDefault(() => this.execShell(name))}
|
||||
className="flex align-center"
|
||||
>
|
||||
<StatusBrick/>
|
||||
<span>{name}</span>
|
||||
</MenuItem>
|
||||
|
||||
@ -108,6 +108,10 @@ export async function lauchMinikubeClusterFromCatalog(window: Page): Promise<Fra
|
||||
|
||||
const frame = await minikubeFrame.contentFrame();
|
||||
|
||||
if (!frame) {
|
||||
throw new Error("No iframe for minikube found");
|
||||
}
|
||||
|
||||
await frame.waitForSelector("[data-testid=cluster-sidebar]");
|
||||
|
||||
return frame;
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"productName": "OpenLens",
|
||||
"description": "OpenLens - Open Source IDE for Kubernetes",
|
||||
"homepage": "https://github.com/lensapp/lens",
|
||||
"version": "5.5.0-alpha.0",
|
||||
"version": "5.5.0-alpha.0.1649256751918",
|
||||
"main": "static/build/main.js",
|
||||
"copyright": "© 2021 OpenLens Authors",
|
||||
"license": "MIT",
|
||||
@ -269,13 +269,14 @@
|
||||
"tar": "^6.1.11",
|
||||
"tcp-port-used": "^1.0.2",
|
||||
"tempy": "1.0.1",
|
||||
"typed-regex": "^0.0.8",
|
||||
"url-parse": "^1.5.10",
|
||||
"uuid": "^8.3.2",
|
||||
"win-ca": "^3.5.0",
|
||||
"winston": "^3.6.0",
|
||||
"winston-console-format": "^1.0.8",
|
||||
"winston-transport-browserconsole": "^1.0.5",
|
||||
"ws": "^7.5.7"
|
||||
"ws": "^8.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@async-fn/jest": "1.5.3",
|
||||
@ -289,6 +290,7 @@
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/byline": "^4.2.33",
|
||||
"@types/chart.js": "^2.9.36",
|
||||
"@types/circular-dependency-plugin": "5.0.5",
|
||||
"@types/cli-progress": "^3.9.2",
|
||||
"@types/color": "^3.0.3",
|
||||
"@types/crypto-js": "^3.1.47",
|
||||
|
||||
@ -30,9 +30,9 @@ interface TestStoreModel {
|
||||
}
|
||||
|
||||
class TestStore extends BaseStore<TestStoreModel> {
|
||||
@observable a: string;
|
||||
@observable b: string;
|
||||
@observable c: string;
|
||||
@observable a = "";
|
||||
@observable b = "";
|
||||
@observable c = "";
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
@ -90,7 +90,6 @@ describe("BaseStore", () => {
|
||||
|
||||
await mainDi.runSetups();
|
||||
|
||||
store = undefined;
|
||||
TestStore.resetInstance();
|
||||
|
||||
const mockOpts = {
|
||||
|
||||
@ -14,16 +14,13 @@ import { stdout, stderr } from "process";
|
||||
import getCustomKubeConfigDirectoryInjectable from "../app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable";
|
||||
import clusterStoreInjectable from "../cluster-store/cluster-store.injectable";
|
||||
import type { ClusterModel } from "../cluster-types";
|
||||
import type {
|
||||
DiContainer,
|
||||
} from "@ogre-tools/injectable";
|
||||
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token";
|
||||
|
||||
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
|
||||
import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable";
|
||||
import appVersionInjectable from "../get-configuration-file-model/app-version/app-version.injectable";
|
||||
import assert from "assert";
|
||||
|
||||
console = new Console(stdout, stderr);
|
||||
|
||||
@ -148,6 +145,8 @@ describe("cluster-store", () => {
|
||||
it("adds new cluster to store", async () => {
|
||||
const storedCluster = clusterStore.getById("foo");
|
||||
|
||||
assert(storedCluster);
|
||||
|
||||
expect(storedCluster.id).toBe("foo");
|
||||
expect(storedCluster.preferences.terminalCWD).toBe("/some-directory-for-user-data");
|
||||
expect(storedCluster.preferences.icon).toBe(
|
||||
@ -249,6 +248,8 @@ describe("cluster-store", () => {
|
||||
it("allows to retrieve a cluster", () => {
|
||||
const storedCluster = clusterStore.getById("cluster1");
|
||||
|
||||
assert(storedCluster);
|
||||
|
||||
expect(storedCluster.id).toBe("cluster1");
|
||||
expect(storedCluster.preferences.terminalCWD).toBe("/foo");
|
||||
});
|
||||
@ -379,6 +380,7 @@ users:
|
||||
it("migrates to modern format with icon not in file", async () => {
|
||||
const { icon } = clusterStore.clustersList[0].preferences;
|
||||
|
||||
assert(icon);
|
||||
expect(icon.startsWith("data:;base64,")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
import type { AppEvent } from "../app-event-bus/event-bus";
|
||||
import { appEventBus } from "../app-event-bus/event-bus";
|
||||
import { Console } from "console";
|
||||
import { assert, Console } from "console";
|
||||
import { stdout, stderr } from "process";
|
||||
|
||||
console = new Console(stdout, stderr);
|
||||
@ -13,14 +13,15 @@ console = new Console(stdout, stderr);
|
||||
describe("event bus tests", () => {
|
||||
describe("emit", () => {
|
||||
it("emits an event", () => {
|
||||
let event: AppEvent = null;
|
||||
let event: AppEvent | undefined;
|
||||
|
||||
appEventBus.addListener((data) => {
|
||||
event = data;
|
||||
});
|
||||
|
||||
appEventBus.emit({ name: "foo", action: "bar" });
|
||||
expect(event.name).toBe("foo");
|
||||
assert(event);
|
||||
expect(event?.name).toBe("foo");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -21,7 +21,7 @@ describe("EventEmitter", () => {
|
||||
let called = false;
|
||||
const e = new EventEmitter<[]>();
|
||||
|
||||
e.addListener(() => 0 as any, {});
|
||||
e.addListener(() => 0 as never, {});
|
||||
e.addListener(() => { called = true; }, {});
|
||||
e.emit();
|
||||
|
||||
|
||||
@ -174,7 +174,7 @@ describe("HotbarStore", () => {
|
||||
});
|
||||
|
||||
it("initially adds catalog entity as first item", () => {
|
||||
expect(hotbarStore.getActive().items[0].entity.name).toEqual("Catalog");
|
||||
expect(hotbarStore.getActive().items[0]?.entity.name).toEqual("Catalog");
|
||||
});
|
||||
|
||||
it("adds items", () => {
|
||||
@ -211,7 +211,7 @@ describe("HotbarStore", () => {
|
||||
hotbarStore.restackItems(1, 5);
|
||||
|
||||
expect(hotbarStore.getActive().items[5]).toBeTruthy();
|
||||
expect(hotbarStore.getActive().items[5].entity.uid).toEqual("test");
|
||||
expect(hotbarStore.getActive().items[5]?.entity.uid).toEqual("test");
|
||||
});
|
||||
|
||||
it("moves items down", () => {
|
||||
@ -275,7 +275,7 @@ describe("HotbarStore", () => {
|
||||
hotbarStore.addToHotbar(testCluster);
|
||||
hotbarStore.restackItems(1, 1);
|
||||
|
||||
expect(hotbarStore.getActive().items[1].entity.uid).toEqual("test");
|
||||
expect(hotbarStore.getActive().items[1]?.entity.uid).toEqual("test");
|
||||
});
|
||||
|
||||
it("new items takes first empty cell", () => {
|
||||
@ -284,7 +284,7 @@ describe("HotbarStore", () => {
|
||||
hotbarStore.restackItems(0, 3);
|
||||
hotbarStore.addToHotbar(minikubeCluster);
|
||||
|
||||
expect(hotbarStore.getActive().items[0].entity.uid).toEqual("minikube");
|
||||
expect(hotbarStore.getActive().items[0]?.entity.uid).toEqual("minikube");
|
||||
});
|
||||
|
||||
it("throws if invalid arguments provided", () => {
|
||||
@ -389,7 +389,7 @@ describe("HotbarStore", () => {
|
||||
it("allows to retrieve a hotbar", () => {
|
||||
const hotbar = hotbarStore.getById("3caac17f-aec2-4723-9694-ad204465d935");
|
||||
|
||||
expect(hotbar.id).toBe("3caac17f-aec2-4723-9694-ad204465d935");
|
||||
expect(hotbar?.id).toBe("3caac17f-aec2-4723-9694-ad204465d935");
|
||||
});
|
||||
|
||||
it("clears cells without entity", () => {
|
||||
|
||||
@ -6,13 +6,14 @@ import https from "https";
|
||||
import os from "os";
|
||||
import { getMacRootCA, getWinRootCA, injectCAs, DSTRootCAX3 } from "../system-ca";
|
||||
import { dependencies, devDependencies } from "../../../package.json";
|
||||
import assert from "assert";
|
||||
|
||||
const deps = { ...dependencies, ...devDependencies };
|
||||
|
||||
// Skip the test if mac-ca is not installed, or os is not darwin
|
||||
(deps["mac-ca"] && os.platform().includes("darwin") ? describe: describe.skip)("inject CA for Mac", () => {
|
||||
// for reset https.globalAgent.options.ca after testing
|
||||
let _ca: string | Buffer | (string | Buffer)[];
|
||||
let _ca: string | Buffer | (string | Buffer)[] | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
_ca = https.globalAgent.options.ca;
|
||||
@ -44,6 +45,7 @@ const deps = { ...dependencies, ...devDependencies };
|
||||
injectCAs(osxCAs);
|
||||
const injected = https.globalAgent.options.ca;
|
||||
|
||||
assert(injected);
|
||||
expect(injected.includes(DSTRootCAX3)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@ -51,7 +53,7 @@ const deps = { ...dependencies, ...devDependencies };
|
||||
// Skip the test if win-ca is not installed, or os is not win32
|
||||
(deps["win-ca"] && os.platform().includes("win32") ? describe: describe.skip)("inject CA for Windows", () => {
|
||||
// for reset https.globalAgent.options.ca after testing
|
||||
let _ca: string | Buffer | (string | Buffer)[];
|
||||
let _ca: string | Buffer | (string | Buffer)[] | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
_ca = https.globalAgent.options.ca;
|
||||
|
||||
@ -49,7 +49,7 @@ describe("user store tests", () => {
|
||||
|
||||
mockFs();
|
||||
|
||||
di.override(writeFileInjectable, () => () => undefined);
|
||||
di.override(writeFileInjectable, () => () => Promise.resolve());
|
||||
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
||||
di.override(userStoreInjectable, () => UserStore.createInstance());
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
import { navigate } from "../../renderer/navigation";
|
||||
import type { CatalogEntityMetadata, CatalogEntitySpec, CatalogEntityStatus } from "../catalog";
|
||||
import { CatalogCategory, CatalogEntity } from "../catalog";
|
||||
import { CatalogCategory, CatalogEntity, categoryVersion } from "../catalog";
|
||||
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
|
||||
|
||||
interface GeneralEntitySpec extends CatalogEntitySpec {
|
||||
@ -47,10 +47,7 @@ export class GeneralCategory extends CatalogCategory {
|
||||
public spec = {
|
||||
group: "entity.k8slens.dev",
|
||||
versions: [
|
||||
{
|
||||
name: "v1alpha1",
|
||||
entityClass: GeneralEntity,
|
||||
},
|
||||
categoryVersion("v1alpha1", GeneralEntity),
|
||||
],
|
||||
names: {
|
||||
kind: "General",
|
||||
|
||||
@ -5,11 +5,11 @@
|
||||
|
||||
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
|
||||
import type { CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus, CatalogCategorySpec } from "../catalog";
|
||||
import { CatalogEntity, CatalogCategory } from "../catalog";
|
||||
import { CatalogEntity, CatalogCategory, categoryVersion } from "../catalog";
|
||||
import { ClusterStore } from "../cluster-store/cluster-store";
|
||||
import { broadcastMessage } from "../ipc";
|
||||
import { app } from "electron";
|
||||
import type { CatalogEntitySpec } from "../catalog/catalog-entity";
|
||||
import type { CatalogEntityConstructor, CatalogEntitySpec } from "../catalog/catalog-entity";
|
||||
import { IpcRendererNavigationEvents } from "../../renderer/navigation/events";
|
||||
import { requestClusterActivation, requestClusterDisconnection } from "../../renderer/ipc";
|
||||
import KubeClusterCategoryIcon from "./icons/kubernetes.svg";
|
||||
@ -60,6 +60,10 @@ export type KubernetesClusterStatusPhase = "connected" | "connecting" | "disconn
|
||||
export interface KubernetesClusterStatus extends CatalogEntityStatus {
|
||||
}
|
||||
|
||||
export function isKubernetesCluster(item: unknown): item is KubernetesCluster {
|
||||
return item instanceof KubernetesCluster;
|
||||
}
|
||||
|
||||
export class KubernetesCluster<
|
||||
Metadata extends KubernetesClusterMetadata = KubernetesClusterMetadata,
|
||||
Status extends KubernetesClusterStatus = KubernetesClusterStatus,
|
||||
@ -145,10 +149,7 @@ class KubernetesClusterCategory extends CatalogCategory {
|
||||
public spec: CatalogCategorySpec = {
|
||||
group: "entity.k8slens.dev",
|
||||
versions: [
|
||||
{
|
||||
name: "v1alpha1",
|
||||
entityClass: KubernetesCluster,
|
||||
},
|
||||
categoryVersion("v1alpha1", KubernetesCluster as CatalogEntityConstructor<KubernetesCluster>),
|
||||
],
|
||||
names: {
|
||||
kind: "KubernetesCluster",
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import type { CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
|
||||
import { CatalogCategory, CatalogEntity } from "../catalog";
|
||||
import { CatalogCategory, CatalogEntity, categoryVersion } from "../catalog";
|
||||
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
|
||||
import { productName } from "../vars";
|
||||
import { WeblinkStore } from "../weblink-store";
|
||||
@ -47,7 +47,7 @@ export class WebLink extends CatalogEntity<CatalogEntityMetadata, WebLinkStatus,
|
||||
}
|
||||
|
||||
catalogCategoryRegistry
|
||||
.getCategoryForEntity<WebLinkCategory>(this)
|
||||
.getCategoryForEntity(this)
|
||||
?.emit("contextMenuOpen", this, context);
|
||||
}
|
||||
}
|
||||
@ -62,10 +62,7 @@ export class WebLinkCategory extends CatalogCategory {
|
||||
public spec = {
|
||||
group: "entity.k8slens.dev",
|
||||
versions: [
|
||||
{
|
||||
name: "v1alpha1",
|
||||
entityClass: WebLink,
|
||||
},
|
||||
categoryVersion("v1alpha1", WebLink),
|
||||
],
|
||||
names: {
|
||||
kind: "WebLink",
|
||||
|
||||
@ -11,19 +11,19 @@ import type { Disposer } from "../utils";
|
||||
import { iter } from "../utils";
|
||||
import type { CategoryColumnRegistration } from "../../renderer/components/+catalog/custom-category-columns";
|
||||
|
||||
type ExtractEntityMetadataType<Entity> = Entity extends CatalogEntity<infer Metadata> ? Metadata : never;
|
||||
type ExtractEntityStatusType<Entity> = Entity extends CatalogEntity<any, infer Status> ? Status : never;
|
||||
type ExtractEntitySpecType<Entity> = Entity extends CatalogEntity<any, any, infer Spec> ? Spec : never;
|
||||
export type CatalogEntityDataFor<Entity> = Entity extends CatalogEntity<infer Metadata, infer Status, infer Spec>
|
||||
? CatalogEntityData<Metadata, Status, Spec>
|
||||
: never;
|
||||
|
||||
export type CatalogEntityInstanceFrom<Constructor> = Constructor extends CatalogEntityConstructor<infer Entity>
|
||||
? Entity
|
||||
: never;
|
||||
|
||||
export type CatalogEntityConstructor<Entity extends CatalogEntity> = (
|
||||
(new (data: CatalogEntityData<
|
||||
ExtractEntityMetadataType<Entity>,
|
||||
ExtractEntityStatusType<Entity>,
|
||||
ExtractEntitySpecType<Entity>
|
||||
>) => Entity)
|
||||
new (data: CatalogEntityDataFor<Entity>) => Entity
|
||||
);
|
||||
|
||||
export interface CatalogCategoryVersion<Entity extends CatalogEntity> {
|
||||
export interface CatalogCategoryVersion {
|
||||
/**
|
||||
* The specific version that the associated constructor is for. This MUST be
|
||||
* a DNS label and SHOULD be of the form `vN`, `vNalphaY`, or `vNbetaY` where
|
||||
@ -35,19 +35,19 @@ export interface CatalogCategoryVersion<Entity extends CatalogEntity> {
|
||||
* - `v1alpha2`
|
||||
* - `v3beta2`
|
||||
*/
|
||||
name: string;
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* The constructor for the entities.
|
||||
*/
|
||||
entityClass: CatalogEntityConstructor<Entity>;
|
||||
readonly entityClass: CatalogEntityConstructor<CatalogEntity>;
|
||||
}
|
||||
|
||||
export interface CatalogCategorySpec {
|
||||
/**
|
||||
* The grouping for for the category. This MUST be a DNS label.
|
||||
*/
|
||||
group: string;
|
||||
readonly group: string;
|
||||
|
||||
/**
|
||||
* The specific versions of the constructors.
|
||||
@ -56,18 +56,18 @@ export interface CatalogCategorySpec {
|
||||
* For example, if `group = "entity.k8slens.dev"` and there is an entry in `.versions` with
|
||||
* `name = "v1alpha1"` then the resulting `.apiVersion` MUST be `entity.k8slens.dev/v1alpha1`
|
||||
*/
|
||||
versions: CatalogCategoryVersion<CatalogEntity>[];
|
||||
readonly versions: CatalogCategoryVersion[];
|
||||
|
||||
/**
|
||||
* This is the concerning the category
|
||||
*/
|
||||
names: {
|
||||
readonly names: {
|
||||
/**
|
||||
* The kind of entity that this category is for. This value MUST be a DNS
|
||||
* label and MUST be equal to the `kind` fields that are produced by the
|
||||
* `.versions.[] | .entityClass` fields.
|
||||
*/
|
||||
kind: string;
|
||||
readonly kind: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -81,7 +81,7 @@ export interface CatalogCategorySpec {
|
||||
*
|
||||
* These columns will not be used in the "Browse" view.
|
||||
*/
|
||||
displayColumns?: CategoryColumnRegistration[];
|
||||
readonly displayColumns?: CategoryColumnRegistration[];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -109,6 +109,30 @@ export interface CatalogCategoryEvents {
|
||||
contextMenuOpen: (entity: CatalogEntity, context: CatalogEntityContextMenuContext) => void;
|
||||
}
|
||||
|
||||
export interface CatalogCategoryMetadata {
|
||||
/**
|
||||
* The name of your category. The category can be searched for by this
|
||||
* value. This will also be used for the catalog menu.
|
||||
*/
|
||||
readonly name: string;
|
||||
/**
|
||||
* Either an `<svg>` or the name of an icon from {@link IconProps}
|
||||
*/
|
||||
readonly icon: string;
|
||||
}
|
||||
|
||||
export function categoryVersion<
|
||||
T extends CatalogEntity<Metadata, Status, Spec>,
|
||||
Metadata extends CatalogEntityMetadata,
|
||||
Status extends CatalogEntityStatus,
|
||||
Spec extends CatalogEntitySpec,
|
||||
>(name: string, entityClass: new (data: CatalogEntityData<Metadata, Status, Spec>) => T): CatalogCategoryVersion {
|
||||
return {
|
||||
name,
|
||||
entityClass: entityClass as CatalogEntityConstructor<T>,
|
||||
};
|
||||
}
|
||||
|
||||
export abstract class CatalogCategory extends (EventEmitter as new () => TypedEmitter<CatalogCategoryEvents>) {
|
||||
/**
|
||||
* The version of category that you are wanting to declare.
|
||||
@ -131,28 +155,17 @@ export abstract class CatalogCategory extends (EventEmitter as new () => TypedEm
|
||||
/**
|
||||
* The data about the category itself
|
||||
*/
|
||||
abstract readonly metadata: {
|
||||
/**
|
||||
* The name of your category. The category can be searched for by this
|
||||
* value. This will also be used for the catalog menu.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Either an `<svg>` or the name of an icon from {@link IconProps}
|
||||
*/
|
||||
icon: string;
|
||||
};
|
||||
abstract readonly metadata: CatalogCategoryMetadata;
|
||||
|
||||
/**
|
||||
* The most important part of a category, as it is where entity versions are declared.
|
||||
*/
|
||||
abstract spec: CatalogCategorySpec;
|
||||
abstract readonly spec: CatalogCategorySpec;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected filters = observable.set<AddMenuFilter>([], {
|
||||
protected readonly filters = observable.set<AddMenuFilter>([], {
|
||||
deep: false,
|
||||
});
|
||||
|
||||
@ -217,14 +230,16 @@ export abstract class CatalogCategory extends (EventEmitter as new () => TypedEm
|
||||
}
|
||||
}
|
||||
|
||||
export interface CatalogEntityMetadata {
|
||||
export type EntityMetadataObject = { [Key in string]?: EntityMetadataValue };
|
||||
export type EntityMetadataValue = string | number | boolean | EntityMetadataObject | undefined;
|
||||
|
||||
export interface CatalogEntityMetadata extends EntityMetadataObject {
|
||||
uid: string;
|
||||
name: string;
|
||||
shortName?: string;
|
||||
description?: string;
|
||||
source?: string;
|
||||
labels: Record<string, string>;
|
||||
[key: string]: string | object;
|
||||
}
|
||||
|
||||
export interface CatalogEntityStatus {
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { comparer, computed } from "mobx";
|
||||
import hostedClusterInjectable from "./hosted-cluster.injectable";
|
||||
import hostedClusterInjectable from "../../renderer/cluster/hosted-cluster.injectable";
|
||||
|
||||
const allowedResourcesInjectable = getInjectable({
|
||||
id: "allowed-resources",
|
||||
@ -12,7 +12,7 @@ const allowedResourcesInjectable = getInjectable({
|
||||
instantiate: (di) => {
|
||||
const cluster = di.inject(hostedClusterInjectable);
|
||||
|
||||
return computed(() => new Set(cluster.allowedResources), {
|
||||
return computed(() => new Set(cluster?.allowedResources), {
|
||||
// This needs to be here so that during refresh changes are only propogated when necessary
|
||||
equals: (cur, prev) => comparer.structural(cur, prev),
|
||||
});
|
||||
|
||||
@ -103,8 +103,12 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
return this.clusters.size > 0;
|
||||
}
|
||||
|
||||
getById(id: ClusterId): Cluster | null {
|
||||
return this.clusters.get(id) ?? null;
|
||||
getById(id: ClusterId | undefined): Cluster | undefined {
|
||||
if (id) {
|
||||
return this.clusters.get(id);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
addCluster(clusterOrModel: ClusterModel | Cluster): Cluster {
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
import { ipcMain } from "electron";
|
||||
import { action, comparer, computed, makeObservable, observable, reaction, when } from "mobx";
|
||||
import { broadcastMessage } from "../ipc";
|
||||
import type { ContextHandler } from "../../main/context-handler/context-handler";
|
||||
import type { ClusterContextHandler } from "../../main/context-handler/context-handler";
|
||||
import type { KubeConfig } from "@kubernetes/client-node";
|
||||
import { HttpError } from "@kubernetes/client-node";
|
||||
import type { Kubectl } from "../../main/kubectl/kubectl";
|
||||
@ -20,16 +20,17 @@ import { DetectorRegistry } from "../../main/cluster-detectors/detector-registry
|
||||
import plimit from "p-limit";
|
||||
import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate } from "../cluster-types";
|
||||
import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../cluster-types";
|
||||
import { disposer, toJS } from "../utils";
|
||||
import { disposer, isDefined, isRequestError, toJS } from "../utils";
|
||||
import type { Response } from "request";
|
||||
import { clusterListNamespaceForbiddenChannel } from "../ipc/cluster";
|
||||
import type { CanI } from "./authorization-review.injectable";
|
||||
import type { ListNamespaces } from "./list-namespaces.injectable";
|
||||
import assert from "assert";
|
||||
|
||||
export interface ClusterDependencies {
|
||||
readonly directoryForKubeConfigs: string;
|
||||
createKubeconfigManager: (cluster: Cluster) => KubeconfigManager;
|
||||
createContextHandler: (cluster: Cluster) => ContextHandler;
|
||||
createContextHandler: (cluster: Cluster) => ClusterContextHandler;
|
||||
createKubectl: (clusterVersion: string) => Kubectl;
|
||||
createAuthorizationReview: (config: KubeConfig) => CanI;
|
||||
createListNamespaces: (config: KubeConfig) => ListNamespaces;
|
||||
@ -43,17 +44,31 @@ export interface ClusterDependencies {
|
||||
export class Cluster implements ClusterModel, ClusterState {
|
||||
/** Unique id for a cluster */
|
||||
public readonly id: ClusterId;
|
||||
private kubeCtl: Kubectl;
|
||||
private kubeCtl: Kubectl | undefined;
|
||||
/**
|
||||
* Context handler
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public contextHandler: ContextHandler;
|
||||
protected proxyKubeconfigManager: KubeconfigManager;
|
||||
protected eventsDisposer = disposer();
|
||||
protected readonly _contextHandler: ClusterContextHandler | undefined;
|
||||
protected readonly _proxyKubeconfigManager: KubeconfigManager | undefined;
|
||||
protected readonly eventsDisposer = disposer();
|
||||
protected activated = false;
|
||||
private resourceAccessStatuses: Map<KubeApiResource, boolean> = new Map();
|
||||
private readonly resourceAccessStatuses = new Map<KubeApiResource, boolean>();
|
||||
|
||||
public get contextHandler() {
|
||||
// TODO: remove these once main/renderer are seperate classes
|
||||
assert(this._contextHandler);
|
||||
|
||||
return this._contextHandler;
|
||||
}
|
||||
|
||||
protected get proxyKubeconfigManager() {
|
||||
// TODO: remove these once main/renderer are seperate classes
|
||||
assert(this._proxyKubeconfigManager);
|
||||
|
||||
return this._proxyKubeconfigManager;
|
||||
}
|
||||
|
||||
get whenReady() {
|
||||
return when(() => this.ready);
|
||||
@ -64,21 +79,21 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
*
|
||||
* @observable
|
||||
*/
|
||||
@observable contextName: string;
|
||||
@observable contextName!: string;
|
||||
/**
|
||||
* Path to kubeconfig
|
||||
*
|
||||
* @observable
|
||||
*/
|
||||
@observable kubeConfigPath: string;
|
||||
@observable kubeConfigPath!: string;
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@observable workspace: string;
|
||||
@observable workspace?: string;
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@observable workspaces: string[];
|
||||
@observable workspaces?: string[];
|
||||
/**
|
||||
* Kubernetes API server URL
|
||||
*
|
||||
@ -215,7 +230,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
* @computed
|
||||
* @internal
|
||||
*/
|
||||
@computed get defaultNamespace(): string {
|
||||
@computed get defaultNamespace(): string | undefined {
|
||||
return this.preferences.defaultNamespace;
|
||||
}
|
||||
|
||||
@ -231,12 +246,20 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
throw validationError;
|
||||
}
|
||||
|
||||
this.apiUrl = config.getCluster(config.getContextObject(this.contextName).cluster).server;
|
||||
const context = config.getContextObject(this.contextName);
|
||||
|
||||
assert(context);
|
||||
|
||||
const cluster = config.getCluster(context.cluster);
|
||||
|
||||
assert(cluster);
|
||||
|
||||
this.apiUrl = cluster.server;
|
||||
|
||||
if (ipcMain) {
|
||||
// for the time being, until renderer gets its own cluster type
|
||||
this.contextHandler = this.dependencies.createContextHandler(this);
|
||||
this.proxyKubeconfigManager = this.dependencies.createKubeconfigManager(this);
|
||||
this._contextHandler = this.dependencies.createContextHandler(this);
|
||||
this._proxyKubeconfigManager = this.dependencies.createKubeconfigManager(this);
|
||||
|
||||
logger.debug(`[CLUSTER]: Cluster init success`, {
|
||||
id: this.id,
|
||||
@ -255,6 +278,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
// Note: do not assign ID as that should never be updated
|
||||
|
||||
this.kubeConfigPath = model.kubeConfigPath;
|
||||
this.contextName = model.contextName;
|
||||
|
||||
if (model.workspace) {
|
||||
this.workspace = model.workspace;
|
||||
@ -264,10 +288,6 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
this.workspaces = model.workspaces;
|
||||
}
|
||||
|
||||
if (model.contextName) {
|
||||
this.contextName = model.contextName;
|
||||
}
|
||||
|
||||
if (model.preferences) {
|
||||
this.preferences = model.preferences;
|
||||
}
|
||||
@ -373,8 +393,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
@action
|
||||
async reconnect() {
|
||||
logger.info(`[CLUSTER]: reconnect`, this.getMeta());
|
||||
this.contextHandler?.stopServer();
|
||||
await this.contextHandler?.ensureServer();
|
||||
await this.contextHandler?.restartServer();
|
||||
this.disconnected = false;
|
||||
}
|
||||
|
||||
@ -497,32 +516,36 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
} catch (error) {
|
||||
logger.error(`[CLUSTER]: Failed to connect to "${this.contextName}": ${error}`);
|
||||
|
||||
if (error.statusCode) {
|
||||
if (error.statusCode >= 400 && error.statusCode < 500) {
|
||||
this.broadcastConnectUpdate("Invalid credentials", true);
|
||||
if (isRequestError(error)) {
|
||||
if (error.statusCode) {
|
||||
if (error.statusCode >= 400 && error.statusCode < 500) {
|
||||
this.broadcastConnectUpdate("Invalid credentials", true);
|
||||
|
||||
return ClusterStatus.AccessDenied;
|
||||
}
|
||||
return ClusterStatus.AccessDenied;
|
||||
}
|
||||
|
||||
this.broadcastConnectUpdate(error.error || error.message, true);
|
||||
|
||||
return ClusterStatus.Offline;
|
||||
}
|
||||
|
||||
if (error.failed === true) {
|
||||
if (error.timedOut === true) {
|
||||
this.broadcastConnectUpdate("Connection timed out", true);
|
||||
this.broadcastConnectUpdate(error.error || error.message, true);
|
||||
|
||||
return ClusterStatus.Offline;
|
||||
}
|
||||
|
||||
this.broadcastConnectUpdate("Failed to fetch credentials", true);
|
||||
if (error.failed === true) {
|
||||
if (error.timedOut === true) {
|
||||
this.broadcastConnectUpdate("Connection timed out", true);
|
||||
|
||||
return ClusterStatus.AccessDenied;
|
||||
return ClusterStatus.Offline;
|
||||
}
|
||||
|
||||
this.broadcastConnectUpdate("Failed to fetch credentials", true);
|
||||
|
||||
return ClusterStatus.AccessDenied;
|
||||
}
|
||||
|
||||
this.broadcastConnectUpdate(error.message, true);
|
||||
} else {
|
||||
this.broadcastConnectUpdate("Unknown error has occurred", true);
|
||||
}
|
||||
|
||||
this.broadcastConnectUpdate(error.message, true);
|
||||
|
||||
return ClusterStatus.Offline;
|
||||
}
|
||||
}
|
||||
@ -609,7 +632,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
return await listNamespaces();
|
||||
} catch (error) {
|
||||
const ctx = proxyConfig.getContextObject(this.contextName);
|
||||
const namespaceList = [ctx.namespace].filter(Boolean);
|
||||
const namespaceList = [ctx?.namespace].filter(isDefined);
|
||||
|
||||
if (namespaceList.length === 0 && error instanceof HttpError && error.statusCode === 403) {
|
||||
const { response } = error as HttpError & { response: Response };
|
||||
|
||||
@ -6,5 +6,8 @@ import { getInjectionToken } from "@ogre-tools/injectable";
|
||||
import type { ClusterModel } from "../cluster-types";
|
||||
import type { Cluster } from "./cluster";
|
||||
|
||||
export const createClusterInjectionToken =
|
||||
getInjectionToken<(model: ClusterModel) => Cluster>({ id: "create-cluster-token" });
|
||||
export type CreateCluster = (model: ClusterModel) => Cluster;
|
||||
|
||||
export const createClusterInjectionToken = getInjectionToken<CreateCluster>({
|
||||
id: "create-cluster-token",
|
||||
});
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
import type { KubeConfig } from "@kubernetes/client-node";
|
||||
import { CoreV1Api } from "@kubernetes/client-node";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { isDefined } from "../utils";
|
||||
|
||||
export type ListNamespaces = () => Promise<string[]>;
|
||||
|
||||
@ -14,7 +15,9 @@ export function listNamespaces(config: KubeConfig): ListNamespaces {
|
||||
return async () => {
|
||||
const { body: { items }} = await coreApi.listNamespace();
|
||||
|
||||
return items.map(ns => ns.metadata.name);
|
||||
return items
|
||||
.map(ns => ns.metadata?.name)
|
||||
.filter(isDefined);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { action, comparer, observable, makeObservable, computed } from "mobx";
|
||||
import { action, comparer, observable, makeObservable, computed, runInAction } from "mobx";
|
||||
import { BaseStore } from "./base-store";
|
||||
import migrations from "../migrations/hotbar-store";
|
||||
import { toJS } from "./utils";
|
||||
@ -33,7 +33,7 @@ interface Dependencies {
|
||||
export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
||||
readonly displayName = "HotbarStore";
|
||||
@observable hotbars: Hotbar[] = [];
|
||||
@observable private _activeHotbarId: string;
|
||||
@observable private _activeHotbarId!: string;
|
||||
|
||||
constructor(private dependencies: Dependencies) {
|
||||
super({
|
||||
@ -120,8 +120,22 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
||||
return toJS(model);
|
||||
}
|
||||
|
||||
getActive() {
|
||||
return this.getById(this.activeHotbarId);
|
||||
getActive(): Hotbar {
|
||||
const hotbar = this.getById(this.activeHotbarId);
|
||||
|
||||
if (hotbar) {
|
||||
return hotbar;
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
if (this.hotbars.length === 0) {
|
||||
this.hotbars.push(getEmptyHotbar("Default"));
|
||||
}
|
||||
|
||||
this._activeHotbarId = this.hotbars[0].id;
|
||||
});
|
||||
|
||||
return this.hotbars[0];
|
||||
}
|
||||
|
||||
getByName(name: string) {
|
||||
@ -147,7 +161,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
||||
},
|
||||
);
|
||||
|
||||
setHotbarName = action((id: string, name: string) => {
|
||||
setHotbarName = action((id: string, name: string): void => {
|
||||
const index = this.hotbars.findIndex((hotbar) => hotbar.id === id);
|
||||
|
||||
if (index < 0) {
|
||||
|
||||
@ -10,7 +10,7 @@ import { tuple } from "./utils";
|
||||
export interface HotbarItem {
|
||||
entity: {
|
||||
uid: string;
|
||||
name?: string;
|
||||
name: string;
|
||||
source?: string;
|
||||
};
|
||||
params?: {
|
||||
|
||||
@ -6,5 +6,5 @@ import type { Channel } from "../channel";
|
||||
|
||||
export const createChannel = <Message>(name: string): Channel<Message> => ({
|
||||
name,
|
||||
_template: null,
|
||||
_template: null as never,
|
||||
});
|
||||
|
||||
@ -53,7 +53,7 @@ describe("type enforced ipc tests", () => {
|
||||
const source = new EventEmitter();
|
||||
const listener = () => called += 1;
|
||||
const results = [true, false, true];
|
||||
const verifier = (args: unknown[]): args is [] => results.pop();
|
||||
const verifier = (args: unknown[]): args is [] => results.pop() ?? false;
|
||||
const channel = "foobar";
|
||||
|
||||
onCorrect({ source, listener, verifier, channel });
|
||||
|
||||
@ -13,8 +13,6 @@ export interface ItemObject {
|
||||
}
|
||||
|
||||
export abstract class ItemStore<Item extends ItemObject> {
|
||||
abstract loadAll(...args: any[]): Promise<void | Item[]>;
|
||||
|
||||
protected defaultSorting = (item: Item) => item.getName();
|
||||
|
||||
@observable failedLoading = false;
|
||||
@ -44,8 +42,7 @@ export abstract class ItemStore<Item extends ItemObject> {
|
||||
return this.items.length;
|
||||
}
|
||||
|
||||
getByName(name: string, ...args: any[]): Item;
|
||||
getByName(name: string): Item {
|
||||
getByName(name: string): Item | undefined {
|
||||
return this.items.find(item => item.getName() === name);
|
||||
}
|
||||
|
||||
@ -115,7 +112,6 @@ export abstract class ItemStore<Item extends ItemObject> {
|
||||
}
|
||||
}
|
||||
|
||||
protected async loadItem(...args: any[]): Promise<Item>;
|
||||
@action
|
||||
protected async loadItem(request: () => Promise<Item>, sortItems = true) {
|
||||
const item = await Promise.resolve(request()).catch(() => null);
|
||||
@ -133,9 +129,9 @@ export abstract class ItemStore<Item extends ItemObject> {
|
||||
if (sortItems) items = this.sortItems(items);
|
||||
this.items.replace(items);
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
@action
|
||||
|
||||
@ -3,18 +3,21 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { ingressStore } from "../../../renderer/components/+network-ingresses/ingress.store";
|
||||
import { apiManager } from "../api-manager";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { KubeObjectStore } from "../kube-object.store";
|
||||
|
||||
class TestApi extends KubeApi<KubeObject> {
|
||||
|
||||
protected async checkPreferredVersion() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
class TestStore extends KubeObjectStore<KubeObject, TestApi> {
|
||||
|
||||
}
|
||||
|
||||
describe("ApiManager", () => {
|
||||
describe("registerApi", () => {
|
||||
it("re-register store if apiBase changed", async () => {
|
||||
@ -26,22 +29,23 @@ describe("ApiManager", () => {
|
||||
fallbackApiBases: [fallbackApiBase],
|
||||
checkPreferredVersion: true,
|
||||
});
|
||||
const kubeStore = new TestStore(kubeApi);
|
||||
|
||||
apiManager.registerApi(apiBase, kubeApi);
|
||||
|
||||
// Define to use test api for ingress store
|
||||
Object.defineProperty(ingressStore, "api", { value: kubeApi });
|
||||
apiManager.registerStore(ingressStore, [kubeApi]);
|
||||
Object.defineProperty(kubeStore, "api", { value: kubeApi });
|
||||
apiManager.registerStore(kubeStore, [kubeApi]);
|
||||
|
||||
// Test that store is returned with original apiBase
|
||||
expect(apiManager.getStore(kubeApi)).toBe(ingressStore);
|
||||
expect(apiManager.getStore(kubeApi)).toBe(kubeStore);
|
||||
|
||||
// Change apiBase similar as checkPreferredVersion does
|
||||
Object.defineProperty(kubeApi, "apiBase", { value: fallbackApiBase });
|
||||
apiManager.registerApi(fallbackApiBase, kubeApi);
|
||||
|
||||
// Test that store is returned with new apiBase
|
||||
expect(apiManager.getStore(kubeApi)).toBe(ingressStore);
|
||||
expect(apiManager.getStore(kubeApi)).toBe(kubeStore);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -16,8 +16,15 @@ describe("Crds", () => {
|
||||
name: "foo",
|
||||
resourceVersion: "12345",
|
||||
uid: "12345",
|
||||
selfLink: "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/foo",
|
||||
},
|
||||
spec: {
|
||||
group: "foo.bar",
|
||||
names: {
|
||||
kind: "Foo",
|
||||
plural: "foos",
|
||||
},
|
||||
scope: "Namespaced",
|
||||
versions: [
|
||||
{
|
||||
name: "123",
|
||||
@ -44,8 +51,15 @@ describe("Crds", () => {
|
||||
name: "foo",
|
||||
resourceVersion: "12345",
|
||||
uid: "12345",
|
||||
selfLink: "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/foo",
|
||||
},
|
||||
spec: {
|
||||
group: "foo.bar",
|
||||
names: {
|
||||
kind: "Foo",
|
||||
plural: "foos",
|
||||
},
|
||||
scope: "Namespaced",
|
||||
versions: [
|
||||
{
|
||||
name: "123",
|
||||
@ -72,8 +86,15 @@ describe("Crds", () => {
|
||||
name: "foo",
|
||||
resourceVersion: "12345",
|
||||
uid: "12345",
|
||||
selfLink: "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/foo",
|
||||
},
|
||||
spec: {
|
||||
group: "foo.bar",
|
||||
names: {
|
||||
kind: "Foo",
|
||||
plural: "foos",
|
||||
},
|
||||
scope: "Namespaced",
|
||||
versions: [
|
||||
{
|
||||
name: "123",
|
||||
@ -100,8 +121,15 @@ describe("Crds", () => {
|
||||
name: "foo",
|
||||
resourceVersion: "12345",
|
||||
uid: "12345",
|
||||
selfLink: "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/foo",
|
||||
},
|
||||
spec: {
|
||||
group: "foo.bar",
|
||||
names: {
|
||||
kind: "Foo",
|
||||
plural: "foos",
|
||||
},
|
||||
scope: "Namespaced",
|
||||
version: "abc",
|
||||
versions: [
|
||||
{
|
||||
@ -129,6 +157,7 @@ describe("Crds", () => {
|
||||
name: "foo",
|
||||
resourceVersion: "12345",
|
||||
uid: "12345",
|
||||
selfLink: "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/foo",
|
||||
},
|
||||
spec: {
|
||||
version: "abc",
|
||||
|
||||
@ -3,11 +3,13 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { Deployment, DeploymentApi } from "../endpoints/deployment.api";
|
||||
import { DeploymentApi } from "../endpoints/deployment.api";
|
||||
import type { KubeJsonApi } from "../kube-json-api";
|
||||
|
||||
class DeploymentApiTest extends DeploymentApi {
|
||||
public setRequest(request: any) {
|
||||
declare protected request: KubeJsonApi;
|
||||
|
||||
public setRequest(request: KubeJsonApi) {
|
||||
this.request = request;
|
||||
}
|
||||
}
|
||||
@ -18,7 +20,7 @@ describe("DeploymentApi", () => {
|
||||
patch: () => ({}),
|
||||
} as unknown as KubeJsonApi;
|
||||
|
||||
const sub = new DeploymentApiTest({ objectConstructor: Deployment });
|
||||
const sub = new DeploymentApiTest();
|
||||
|
||||
sub.setRequest(requestMock);
|
||||
|
||||
|
||||
@ -3,42 +3,26 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { EndpointSubset } from "../endpoints";
|
||||
import { formatEndpointSubset } from "../endpoints";
|
||||
|
||||
describe("endpoint tests", () => {
|
||||
describe("EndpointSubset", () => {
|
||||
it.each([
|
||||
4,
|
||||
false,
|
||||
null,
|
||||
{},
|
||||
[],
|
||||
"ahe",
|
||||
/a/,
|
||||
])("should always initialize fields when given %j", (data: any) => {
|
||||
const sub = new EndpointSubset(data);
|
||||
|
||||
expect(sub.addresses).toStrictEqual([]);
|
||||
expect(sub.notReadyAddresses).toStrictEqual([]);
|
||||
expect(sub.ports).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("toString should be addresses X ports", () => {
|
||||
const sub = new EndpointSubset({
|
||||
it("formatEndpointSubset should be addresses X ports", () => {
|
||||
const formatted = formatEndpointSubset({
|
||||
addresses: [{
|
||||
ip: "1.1.1.1",
|
||||
}, {
|
||||
ip: "1.1.1.2",
|
||||
}] as any,
|
||||
}],
|
||||
notReadyAddresses: [],
|
||||
ports: [{
|
||||
port: "81",
|
||||
port: 81,
|
||||
}, {
|
||||
port: "82",
|
||||
}] as any,
|
||||
port: 82,
|
||||
}],
|
||||
});
|
||||
|
||||
expect(sub.toString()).toBe("1.1.1.1:81, 1.1.1.1:82, 1.1.1.2:81, 1.1.1.2:82");
|
||||
expect(formatted).toBe("1.1.1.1:81, 1.1.1.1:82, 1.1.1.2:81, 1.1.1.2:82");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -9,33 +9,33 @@ import { HelmChart } from "../endpoints/helm-charts.api";
|
||||
describe("HelmChart tests", () => {
|
||||
describe("HelmChart.create() tests", () => {
|
||||
it("should throw on non-object input", () => {
|
||||
expect(() => HelmChart.create("" as any)).toThrowError('"value" must be of type object');
|
||||
expect(() => HelmChart.create(1 as any)).toThrowError('"value" must be of type object');
|
||||
expect(() => HelmChart.create(false as any)).toThrowError('"value" must be of type object');
|
||||
expect(() => HelmChart.create([] as any)).toThrowError('"value" must be of type object');
|
||||
expect(() => HelmChart.create(Symbol() as any)).toThrowError('"value" must be of type object');
|
||||
expect(() => HelmChart.create("" as never)).toThrowError('"value" must be of type object');
|
||||
expect(() => HelmChart.create(1 as never)).toThrowError('"value" must be of type object');
|
||||
expect(() => HelmChart.create(false as never)).toThrowError('"value" must be of type object');
|
||||
expect(() => HelmChart.create([] as never)).toThrowError('"value" must be of type object');
|
||||
expect(() => HelmChart.create(Symbol() as never)).toThrowError('"value" must be of type object');
|
||||
});
|
||||
|
||||
it("should throw on missing fields", () => {
|
||||
expect(() => HelmChart.create({} as any)).toThrowError('"apiVersion" is required');
|
||||
expect(() => HelmChart.create({} as never)).toThrowError('"apiVersion" is required');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "!",
|
||||
} as any)).toThrowError('"name" is required');
|
||||
} as never)).toThrowError('"name" is required');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "!",
|
||||
name: "!",
|
||||
} as any)).toThrowError('"version" is required');
|
||||
} as never)).toThrowError('"version" is required');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "!",
|
||||
name: "!",
|
||||
version: "!",
|
||||
} as any)).toThrowError('"repo" is required');
|
||||
} as never)).toThrowError('"repo" is required');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "!",
|
||||
name: "!",
|
||||
version: "!",
|
||||
repo: "!",
|
||||
} as any)).toThrowError('"created" is required');
|
||||
} as never)).toThrowError('"created" is required');
|
||||
});
|
||||
|
||||
it("should throw on fields being wrong type", () => {
|
||||
@ -46,7 +46,7 @@ describe("HelmChart tests", () => {
|
||||
repo: "!",
|
||||
created: "!",
|
||||
digest: "!",
|
||||
} as any)).toThrowError('"apiVersion" must be a string');
|
||||
} as never)).toThrowError('"apiVersion" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: 1,
|
||||
@ -54,7 +54,7 @@ describe("HelmChart tests", () => {
|
||||
repo: "!",
|
||||
created: "!",
|
||||
digest: "!",
|
||||
} as any)).toThrowError('"name" must be a string');
|
||||
} as never)).toThrowError('"name" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "!",
|
||||
name: "!",
|
||||
@ -62,7 +62,7 @@ describe("HelmChart tests", () => {
|
||||
repo: "!",
|
||||
created: "!",
|
||||
digest: 1,
|
||||
} as any)).toThrowError('"digest" must be a string');
|
||||
} as never)).toThrowError('"digest" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "",
|
||||
@ -70,7 +70,7 @@ describe("HelmChart tests", () => {
|
||||
repo: "!",
|
||||
created: "!",
|
||||
digest: "!",
|
||||
} as any)).toThrowError('"version" must be a string');
|
||||
} as never)).toThrowError('"version" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
@ -78,7 +78,7 @@ describe("HelmChart tests", () => {
|
||||
repo: 1,
|
||||
created: "!",
|
||||
digest: "!",
|
||||
} as any)).toThrowError('"repo" must be a string');
|
||||
} as never)).toThrowError('"repo" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
@ -86,7 +86,7 @@ describe("HelmChart tests", () => {
|
||||
repo: "1",
|
||||
created: 1,
|
||||
digest: "a",
|
||||
} as any)).toThrowError('"created" must be a string');
|
||||
} as never)).toThrowError('"created" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
@ -94,7 +94,7 @@ describe("HelmChart tests", () => {
|
||||
repo: "1",
|
||||
created: "!",
|
||||
digest: 1,
|
||||
} as any)).toThrowError('"digest" must be a string');
|
||||
} as never)).toThrowError('"digest" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
@ -103,7 +103,7 @@ describe("HelmChart tests", () => {
|
||||
digest: "1",
|
||||
created: "!",
|
||||
kubeVersion: 1,
|
||||
} as any)).toThrowError('"kubeVersion" must be a string');
|
||||
} as never)).toThrowError('"kubeVersion" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
@ -112,7 +112,7 @@ describe("HelmChart tests", () => {
|
||||
digest: "1",
|
||||
created: "!",
|
||||
description: 1,
|
||||
} as any)).toThrowError('"description" must be a string');
|
||||
} as never)).toThrowError('"description" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
@ -121,7 +121,7 @@ describe("HelmChart tests", () => {
|
||||
digest: "1",
|
||||
created: "!",
|
||||
home: 1,
|
||||
} as any)).toThrowError('"home" must be a string');
|
||||
} as never)).toThrowError('"home" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
@ -130,7 +130,7 @@ describe("HelmChart tests", () => {
|
||||
digest: "1",
|
||||
created: "!",
|
||||
engine: 1,
|
||||
} as any)).toThrowError('"engine" must be a string');
|
||||
} as never)).toThrowError('"engine" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
@ -139,7 +139,7 @@ describe("HelmChart tests", () => {
|
||||
digest: "1",
|
||||
created: "!",
|
||||
icon: 1,
|
||||
} as any)).toThrowError('"icon" must be a string');
|
||||
} as never)).toThrowError('"icon" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
@ -148,7 +148,7 @@ describe("HelmChart tests", () => {
|
||||
digest: "1",
|
||||
created: "!",
|
||||
appVersion: 1,
|
||||
} as any)).toThrowError('"appVersion" must be a string');
|
||||
} as never)).toThrowError('"appVersion" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
@ -157,7 +157,7 @@ describe("HelmChart tests", () => {
|
||||
digest: "1",
|
||||
created: "!",
|
||||
tillerVersion: 1,
|
||||
} as any)).toThrowError('"tillerVersion" must be a string');
|
||||
} as never)).toThrowError('"tillerVersion" must be a string');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
@ -166,7 +166,7 @@ describe("HelmChart tests", () => {
|
||||
digest: "1",
|
||||
created: "!",
|
||||
deprecated: 1,
|
||||
} as any)).toThrowError('"deprecated" must be a boolean');
|
||||
} as never)).toThrowError('"deprecated" must be a boolean');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
@ -175,7 +175,7 @@ describe("HelmChart tests", () => {
|
||||
digest: "1",
|
||||
created: "!",
|
||||
keywords: 1,
|
||||
} as any)).toThrowError('"keywords" must be an array');
|
||||
} as never)).toThrowError('"keywords" must be an array');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
@ -184,7 +184,7 @@ describe("HelmChart tests", () => {
|
||||
digest: "1",
|
||||
created: "!",
|
||||
sources: 1,
|
||||
} as any)).toThrowError('"sources" must be an array');
|
||||
} as never)).toThrowError('"sources" must be an array');
|
||||
expect(() => HelmChart.create({
|
||||
apiVersion: "1",
|
||||
name: "1",
|
||||
@ -193,7 +193,7 @@ describe("HelmChart tests", () => {
|
||||
digest: "1",
|
||||
created: "!",
|
||||
maintainers: 1,
|
||||
} as any)).toThrowError('"maintainers" must be an array');
|
||||
} as never)).toThrowError('"maintainers" must be an array');
|
||||
});
|
||||
|
||||
it("should filter non-string keywords", () => {
|
||||
@ -204,10 +204,10 @@ describe("HelmChart tests", () => {
|
||||
repo: "1",
|
||||
digest: "1",
|
||||
created: "!",
|
||||
keywords: [1, "a", false, {}, "b"] as any,
|
||||
keywords: [1, "a", false, {}, "b"] as never,
|
||||
});
|
||||
|
||||
expect(chart.keywords).toStrictEqual(["a", "b"]);
|
||||
expect(chart?.keywords).toStrictEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
it("should filter non-string sources", () => {
|
||||
@ -218,10 +218,10 @@ describe("HelmChart tests", () => {
|
||||
repo: "1",
|
||||
digest: "1",
|
||||
created: "!",
|
||||
sources: [1, "a", false, {}, "b"] as any,
|
||||
sources: [1, "a", false, {}, "b"] as never,
|
||||
});
|
||||
|
||||
expect(chart.sources).toStrictEqual(["a", "b"]);
|
||||
expect(chart?.sources).toStrictEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
it("should filter invalid maintainers", () => {
|
||||
@ -236,10 +236,10 @@ describe("HelmChart tests", () => {
|
||||
name: "a",
|
||||
email: "b",
|
||||
url: "c",
|
||||
}] as any,
|
||||
}] as never,
|
||||
});
|
||||
|
||||
expect(chart.maintainers).toStrictEqual([{
|
||||
expect(chart?.maintainers).toStrictEqual([{
|
||||
name: "a",
|
||||
email: "b",
|
||||
url: "c",
|
||||
@ -261,9 +261,9 @@ describe("HelmChart tests", () => {
|
||||
name: "a",
|
||||
email: "b",
|
||||
url: "c",
|
||||
}] as any,
|
||||
}] as never,
|
||||
"asdjhajksdhadjks": 1,
|
||||
} as any);
|
||||
} as never);
|
||||
|
||||
expect(warnFn).toHaveBeenCalledWith("HelmChart data has unexpected fields", {
|
||||
original: anyObject(),
|
||||
|
||||
@ -14,6 +14,8 @@ describe("computeRuleDeclarations", () => {
|
||||
name: "foo",
|
||||
resourceVersion: "1",
|
||||
uid: "bar",
|
||||
namespace: "default",
|
||||
selfLink: "/apis/networking.k8s.io/v1/ingresses/default/foo",
|
||||
},
|
||||
});
|
||||
|
||||
@ -21,6 +23,7 @@ describe("computeRuleDeclarations", () => {
|
||||
host: "foo.bar",
|
||||
http: {
|
||||
paths: [{
|
||||
pathType: "Exact",
|
||||
backend: {
|
||||
service: {
|
||||
name: "my-service",
|
||||
@ -44,6 +47,8 @@ describe("computeRuleDeclarations", () => {
|
||||
name: "foo",
|
||||
resourceVersion: "1",
|
||||
uid: "bar",
|
||||
namespace: "default",
|
||||
selfLink: "/apis/networking.k8s.io/v1/ingresses/default/foo",
|
||||
},
|
||||
});
|
||||
|
||||
@ -55,6 +60,7 @@ describe("computeRuleDeclarations", () => {
|
||||
host: "foo.bar",
|
||||
http: {
|
||||
paths: [{
|
||||
pathType: "Exact",
|
||||
backend: {
|
||||
service: {
|
||||
name: "my-service",
|
||||
@ -78,6 +84,8 @@ describe("computeRuleDeclarations", () => {
|
||||
name: "foo",
|
||||
resourceVersion: "1",
|
||||
uid: "bar",
|
||||
namespace: "default",
|
||||
selfLink: "/apis/networking.k8s.io/v1/ingresses/default/foo",
|
||||
},
|
||||
});
|
||||
|
||||
@ -91,6 +99,7 @@ describe("computeRuleDeclarations", () => {
|
||||
host: "foo.bar",
|
||||
http: {
|
||||
paths: [{
|
||||
pathType: "Exact",
|
||||
backend: {
|
||||
service: {
|
||||
name: "my-service",
|
||||
|
||||
@ -19,7 +19,7 @@ import { parseKubeApi } from "../kube-api-parse";
|
||||
/**
|
||||
* [<input-url>, <expected-result>]
|
||||
*/
|
||||
type KubeApiParseTestData = [string, Required<IKubeApiParsed>];
|
||||
type KubeApiParseTestData = [string, IKubeApiParsed];
|
||||
|
||||
const tests: KubeApiParseTestData[] = [
|
||||
["/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions/prometheuses.monitoring.coreos.com", {
|
||||
@ -126,6 +126,6 @@ describe("parseApi unit tests", () => {
|
||||
});
|
||||
|
||||
it.each(throwtests)("testing %j should throw", (url) => {
|
||||
expect(() => parseKubeApi(url)).toThrowError("invalid apiPath");
|
||||
expect(() => parseKubeApi(url as never)).toThrowError("invalid apiPath");
|
||||
});
|
||||
});
|
||||
|
||||
@ -3,7 +3,6 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { Request } from "node-fetch";
|
||||
import { forRemoteCluster, KubeApi } from "../kube-api";
|
||||
import { KubeJsonApi } from "../kube-json-api";
|
||||
import { KubeObject } from "../kube-object";
|
||||
@ -12,11 +11,12 @@ import { delay } from "../../utils/delay";
|
||||
import { PassThrough } from "stream";
|
||||
import type { ApiManager } from "../api-manager";
|
||||
import { apiManager } from "../api-manager";
|
||||
import { Ingress, Pod } from "../endpoints";
|
||||
import type { FetchMock } from "jest-fetch-mock/types";
|
||||
|
||||
jest.mock("../api-manager");
|
||||
|
||||
const mockApiManager = apiManager as jest.Mocked<ApiManager>;
|
||||
const mockFetch = fetch as FetchMock;
|
||||
|
||||
class TestKubeObject extends KubeObject {
|
||||
static kind = "Pod";
|
||||
@ -67,7 +67,7 @@ describe("forRemoteCluster", () => {
|
||||
},
|
||||
}, TestKubeObject);
|
||||
|
||||
(fetch as any).mockResponse(async (request: any) => {
|
||||
mockFetch.mockResponse(async (request: any) => {
|
||||
expect(request.url).toEqual("https://127.0.0.1:6443/api/v1/pods");
|
||||
|
||||
return {
|
||||
@ -92,13 +92,13 @@ describe("KubeApi", () => {
|
||||
});
|
||||
|
||||
it("uses url from apiBase if apiBase contains the resource", async () => {
|
||||
(fetch as any).mockResponse(async (request: any) => {
|
||||
mockFetch.mockResponse(async (request: any) => {
|
||||
if (request.url === "http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1") {
|
||||
return {
|
||||
body: JSON.stringify({
|
||||
resources: [{
|
||||
name: "ingresses",
|
||||
}] as any[],
|
||||
}],
|
||||
}),
|
||||
};
|
||||
} else if (request.url === "http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1") {
|
||||
@ -107,13 +107,13 @@ describe("KubeApi", () => {
|
||||
body: JSON.stringify({
|
||||
resources: [{
|
||||
name: "ingresses",
|
||||
}] as any[],
|
||||
}],
|
||||
}),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
body: JSON.stringify({
|
||||
resources: [] as any[],
|
||||
resources: [],
|
||||
}),
|
||||
};
|
||||
}
|
||||
@ -138,11 +138,11 @@ describe("KubeApi", () => {
|
||||
});
|
||||
|
||||
it("uses url from fallbackApiBases if apiBase lacks the resource", async () => {
|
||||
(fetch as any).mockResponse(async (request: any) => {
|
||||
mockFetch.mockResponse(async (request: any) => {
|
||||
if (request.url === "http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1") {
|
||||
return {
|
||||
body: JSON.stringify({
|
||||
resources: [] as any[],
|
||||
resources: [],
|
||||
}),
|
||||
};
|
||||
} else if (request.url === "http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1") {
|
||||
@ -150,13 +150,13 @@ describe("KubeApi", () => {
|
||||
body: JSON.stringify({
|
||||
resources: [{
|
||||
name: "ingresses",
|
||||
}] as any[],
|
||||
}],
|
||||
}),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
body: JSON.stringify({
|
||||
resources: [] as any[],
|
||||
resources: [],
|
||||
}),
|
||||
};
|
||||
}
|
||||
@ -184,7 +184,7 @@ describe("KubeApi", () => {
|
||||
expect.hasAssertions();
|
||||
|
||||
const api = new TestKubeApi({
|
||||
objectConstructor: Ingress,
|
||||
objectConstructor: TestKubeObject,
|
||||
checkPreferredVersion: true,
|
||||
fallbackApiBases: ["/apis/extensions/v1beta1/ingresses"],
|
||||
request: {
|
||||
@ -214,7 +214,7 @@ describe("KubeApi", () => {
|
||||
},
|
||||
};
|
||||
}),
|
||||
} as any,
|
||||
} as Partial<KubeJsonApi> as KubeJsonApi,
|
||||
});
|
||||
|
||||
await api.checkPreferredVersion();
|
||||
@ -227,7 +227,7 @@ describe("KubeApi", () => {
|
||||
expect.hasAssertions();
|
||||
|
||||
const api = new TestKubeApi({
|
||||
objectConstructor: Pod,
|
||||
objectConstructor: TestKubeObject,
|
||||
checkPreferredVersion: true,
|
||||
fallbackApiBases: ["/api/v1beta1/pods"],
|
||||
request: {
|
||||
@ -257,7 +257,7 @@ describe("KubeApi", () => {
|
||||
},
|
||||
};
|
||||
}),
|
||||
} as any,
|
||||
} as Partial<KubeJsonApi> as KubeJsonApi,
|
||||
});
|
||||
|
||||
await api.checkPreferredVersion();
|
||||
@ -280,10 +280,10 @@ describe("KubeApi", () => {
|
||||
it("sends strategic patch by default", async () => {
|
||||
expect.hasAssertions();
|
||||
|
||||
(fetch as any).mockResponse(async (request: Request) => {
|
||||
mockFetch.mockResponse(async request => {
|
||||
expect(request.method).toEqual("PATCH");
|
||||
expect(request.headers.get("content-type")).toMatch("strategic-merge-patch");
|
||||
expect(request.body.toString()).toEqual(JSON.stringify({ spec: { replicas: 2 }}));
|
||||
expect(request.body?.toString()).toEqual(JSON.stringify({ spec: { replicas: 2 }}));
|
||||
|
||||
return {};
|
||||
});
|
||||
@ -296,10 +296,10 @@ describe("KubeApi", () => {
|
||||
it("allows to use merge patch", async () => {
|
||||
expect.hasAssertions();
|
||||
|
||||
(fetch as any).mockResponse(async (request: Request) => {
|
||||
mockFetch.mockResponse(async request => {
|
||||
expect(request.method).toEqual("PATCH");
|
||||
expect(request.headers.get("content-type")).toMatch("merge-patch");
|
||||
expect(request.body.toString()).toEqual(JSON.stringify({ spec: { replicas: 2 }}));
|
||||
expect(request.body?.toString()).toEqual(JSON.stringify({ spec: { replicas: 2 }}));
|
||||
|
||||
return {};
|
||||
});
|
||||
@ -312,10 +312,10 @@ describe("KubeApi", () => {
|
||||
it("allows to use json patch", async () => {
|
||||
expect.hasAssertions();
|
||||
|
||||
(fetch as any).mockResponse(async (request: Request) => {
|
||||
mockFetch.mockResponse(async request => {
|
||||
expect(request.method).toEqual("PATCH");
|
||||
expect(request.headers.get("content-type")).toMatch("json-patch");
|
||||
expect(request.body.toString()).toEqual(JSON.stringify([{ op: "replace", path: "/spec/replicas", value: 2 }]));
|
||||
expect(request.body?.toString()).toEqual(JSON.stringify([{ op: "replace", path: "/spec/replicas", value: 2 }]));
|
||||
|
||||
return {};
|
||||
});
|
||||
@ -328,10 +328,10 @@ describe("KubeApi", () => {
|
||||
it("allows deep partial patch", async () => {
|
||||
expect.hasAssertions();
|
||||
|
||||
(fetch as any).mockResponse(async (request: Request) => {
|
||||
mockFetch.mockResponse(async request => {
|
||||
expect(request.method).toEqual("PATCH");
|
||||
expect(request.headers.get("content-type")).toMatch("merge-patch");
|
||||
expect(request.body.toString()).toEqual(JSON.stringify({ metadata: { annotations: { provisioned: "true" }}}));
|
||||
expect(request.body?.toString()).toEqual(JSON.stringify({ metadata: { annotations: { provisioned: "true" }}}));
|
||||
|
||||
return {};
|
||||
});
|
||||
@ -356,7 +356,7 @@ describe("KubeApi", () => {
|
||||
|
||||
it("sends correct request with empty namespace", async () => {
|
||||
expect.hasAssertions();
|
||||
(fetch as any).mockResponse(async (request: Request) => {
|
||||
mockFetch.mockResponse(async request => {
|
||||
expect(request.method).toEqual("DELETE");
|
||||
expect(request.url).toEqual("http://127.0.0.1:9999/api-kube/api/v1/pods/foo?propagationPolicy=Background");
|
||||
|
||||
@ -368,7 +368,7 @@ describe("KubeApi", () => {
|
||||
|
||||
it("sends correct request without namespace", async () => {
|
||||
expect.hasAssertions();
|
||||
(fetch as any).mockResponse(async (request: Request) => {
|
||||
mockFetch.mockResponse(async request => {
|
||||
expect(request.method).toEqual("DELETE");
|
||||
expect(request.url).toEqual("http://127.0.0.1:9999/api-kube/api/v1/namespaces/default/pods/foo?propagationPolicy=Background");
|
||||
|
||||
@ -380,7 +380,7 @@ describe("KubeApi", () => {
|
||||
|
||||
it("sends correct request with namespace", async () => {
|
||||
expect.hasAssertions();
|
||||
(fetch as any).mockResponse(async (request: Request) => {
|
||||
mockFetch.mockResponse(async request => {
|
||||
expect(request.method).toEqual("DELETE");
|
||||
expect(request.url).toEqual("http://127.0.0.1:9999/api-kube/api/v1/namespaces/kube-system/pods/foo?propagationPolicy=Background");
|
||||
|
||||
@ -392,7 +392,7 @@ describe("KubeApi", () => {
|
||||
|
||||
it("allows to change propagationPolicy", async () => {
|
||||
expect.hasAssertions();
|
||||
(fetch as any).mockResponse(async (request: Request) => {
|
||||
mockFetch.mockResponse(async request => {
|
||||
expect(request.method).toEqual("DELETE");
|
||||
expect(request.url).toMatch("propagationPolicy=Orphan");
|
||||
|
||||
@ -423,9 +423,10 @@ describe("KubeApi", () => {
|
||||
it("sends a valid watch request", () => {
|
||||
const spy = jest.spyOn(request, "getResponse");
|
||||
|
||||
(fetch as any).mockResponse(async () => {
|
||||
mockFetch.mockResponse(async () => {
|
||||
return {
|
||||
body: stream,
|
||||
// needed for https://github.com/jefflau/jest-fetch-mock/issues/218
|
||||
body: stream as unknown as string,
|
||||
};
|
||||
});
|
||||
|
||||
@ -436,9 +437,10 @@ describe("KubeApi", () => {
|
||||
it("sends timeout as a query parameter", async () => {
|
||||
const spy = jest.spyOn(request, "getResponse");
|
||||
|
||||
(fetch as any).mockResponse(async () => {
|
||||
mockFetch.mockResponse(async () => {
|
||||
return {
|
||||
body: stream,
|
||||
// needed for https://github.com/jefflau/jest-fetch-mock/issues/218
|
||||
body: stream as unknown as string,
|
||||
};
|
||||
});
|
||||
|
||||
@ -449,13 +451,14 @@ describe("KubeApi", () => {
|
||||
it("aborts watch using abortController", async (done) => {
|
||||
const spy = jest.spyOn(request, "getResponse");
|
||||
|
||||
(fetch as any).mockResponse(async (request: Request) => {
|
||||
(request as any).signal.addEventListener("abort", () => {
|
||||
mockFetch.mockResponse(async request => {
|
||||
request.signal.addEventListener("abort", () => {
|
||||
done();
|
||||
});
|
||||
|
||||
return {
|
||||
body: stream,
|
||||
// needed for https://github.com/jefflau/jest-fetch-mock/issues/218
|
||||
body: stream as unknown as string,
|
||||
};
|
||||
});
|
||||
|
||||
@ -478,9 +481,9 @@ describe("KubeApi", () => {
|
||||
it("if request ended", (done) => {
|
||||
const spy = jest.spyOn(request, "getResponse");
|
||||
|
||||
jest.spyOn(stream, "on").mockImplementation((eventName: string, callback: Function) => {
|
||||
jest.spyOn(stream, "on").mockImplementation((event: string | symbol, callback: Function) => {
|
||||
// End the request in 100ms.
|
||||
if (eventName === "end") {
|
||||
if (event === "end") {
|
||||
setTimeout(() => {
|
||||
callback();
|
||||
}, 100);
|
||||
@ -493,8 +496,8 @@ describe("KubeApi", () => {
|
||||
jest.spyOn(global, "fetch").mockImplementation(async () => {
|
||||
return {
|
||||
ok: true,
|
||||
body: stream,
|
||||
} as any;
|
||||
body: stream as never,
|
||||
} as Partial<Response> as Response;
|
||||
});
|
||||
|
||||
api.watch({
|
||||
@ -512,9 +515,10 @@ describe("KubeApi", () => {
|
||||
it("if request not closed after timeout", (done) => {
|
||||
const spy = jest.spyOn(request, "getResponse");
|
||||
|
||||
(fetch as any).mockResponse(async () => {
|
||||
mockFetch.mockResponse(async () => {
|
||||
return {
|
||||
body: stream,
|
||||
// needed for https://github.com/jefflau/jest-fetch-mock/issues/218
|
||||
body: stream as unknown as string,
|
||||
};
|
||||
});
|
||||
|
||||
@ -536,9 +540,9 @@ describe("KubeApi", () => {
|
||||
it("retries only once if request ends and timeout is set", (done) => {
|
||||
const spy = jest.spyOn(request, "getResponse");
|
||||
|
||||
jest.spyOn(stream, "on").mockImplementation((eventName: string, callback: Function) => {
|
||||
jest.spyOn(stream, "on").mockImplementation((event: string | symbol, callback: Function) => {
|
||||
// End the request in 100ms.
|
||||
if (eventName === "end") {
|
||||
if (event === "end") {
|
||||
setTimeout(() => {
|
||||
callback();
|
||||
}, 100);
|
||||
@ -551,8 +555,8 @@ describe("KubeApi", () => {
|
||||
jest.spyOn(global, "fetch").mockImplementation(async () => {
|
||||
return {
|
||||
ok: true,
|
||||
body: stream,
|
||||
} as any;
|
||||
body: stream as never,
|
||||
} as Partial<Response> as Response;
|
||||
});
|
||||
|
||||
const timeoutSeconds = 0.5;
|
||||
@ -589,9 +593,9 @@ describe("KubeApi", () => {
|
||||
it("should add kind and apiVersion", async () => {
|
||||
expect.hasAssertions();
|
||||
|
||||
(fetch as any).mockResponse(async (request: Request) => {
|
||||
mockFetch.mockResponse(async request => {
|
||||
expect(request.method).toEqual("POST");
|
||||
expect(JSON.parse(request.body.toString())).toEqual({
|
||||
expect(JSON.parse(String(request.body))).toEqual({
|
||||
kind: "Pod",
|
||||
apiVersion: "v1",
|
||||
metadata: {
|
||||
@ -643,9 +647,9 @@ describe("KubeApi", () => {
|
||||
it("doesn't override metadata.labels", async () => {
|
||||
expect.hasAssertions();
|
||||
|
||||
(fetch as any).mockResponse(async (request: Request) => {
|
||||
mockFetch.mockResponse(async request => {
|
||||
expect(request.method).toEqual("POST");
|
||||
expect(JSON.parse(request.body.toString())).toEqual({
|
||||
expect(JSON.parse(String(request.body))).toEqual({
|
||||
kind: "Pod",
|
||||
apiVersion: "v1",
|
||||
metadata: {
|
||||
@ -686,9 +690,9 @@ describe("KubeApi", () => {
|
||||
it("doesn't override metadata.labels", async () => {
|
||||
expect.hasAssertions();
|
||||
|
||||
(fetch as any).mockResponse(async (request: Request) => {
|
||||
mockFetch.mockResponse(async request => {
|
||||
expect(request.method).toEqual("PUT");
|
||||
expect(JSON.parse(request.body.toString())).toEqual({
|
||||
expect(JSON.parse(String(request.body))).toEqual({
|
||||
metadata: {
|
||||
name: "foobar",
|
||||
namespace: "default",
|
||||
|
||||
@ -18,6 +18,7 @@ describe("Nodes tests", () => {
|
||||
name: "bar",
|
||||
resourceVersion: "1",
|
||||
uid: "bat",
|
||||
selfLink: "/api/v1/nodes/bar",
|
||||
},
|
||||
});
|
||||
|
||||
@ -33,6 +34,7 @@ describe("Nodes tests", () => {
|
||||
resourceVersion: "1",
|
||||
uid: "bat",
|
||||
labels: {},
|
||||
selfLink: "/api/v1/nodes/bar",
|
||||
},
|
||||
});
|
||||
|
||||
@ -51,6 +53,7 @@ describe("Nodes tests", () => {
|
||||
"node-role.kubernetes.io/foobar": "bat",
|
||||
"hellonode-role.kubernetes.io/foobar1": "bat",
|
||||
},
|
||||
selfLink: "/api/v1/nodes/bar",
|
||||
},
|
||||
});
|
||||
|
||||
@ -69,6 +72,7 @@ describe("Nodes tests", () => {
|
||||
"node-role.kubernetes.io/foobar": "bat",
|
||||
"hellonode-role.kubernetes.io//////foobar1": "bat",
|
||||
},
|
||||
selfLink: "/api/v1/nodes/bar",
|
||||
},
|
||||
});
|
||||
|
||||
@ -86,6 +90,7 @@ describe("Nodes tests", () => {
|
||||
labels: {
|
||||
"kubernetes.io/role": "master",
|
||||
},
|
||||
selfLink: "/api/v1/nodes/bar",
|
||||
},
|
||||
});
|
||||
|
||||
@ -103,6 +108,7 @@ describe("Nodes tests", () => {
|
||||
labels: {
|
||||
"node.kubernetes.io/role": "master",
|
||||
},
|
||||
selfLink: "/api/v1/nodes/bar",
|
||||
},
|
||||
});
|
||||
|
||||
@ -122,6 +128,7 @@ describe("Nodes tests", () => {
|
||||
"kubernetes.io/role": "master",
|
||||
"node.kubernetes.io/role": "master-v2-max",
|
||||
},
|
||||
selfLink: "/api/v1/nodes/bar",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -14,6 +14,8 @@ describe("Pod tests", () => {
|
||||
name: "foobar",
|
||||
resourceVersion: "foobar",
|
||||
uid: "foobar",
|
||||
namespace: "default",
|
||||
selfLink: "/api/v1/pods/default/foobar",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import assert from "assert";
|
||||
import { Pod } from "../endpoints";
|
||||
|
||||
interface GetDummyPodOptions {
|
||||
@ -12,16 +13,15 @@ interface GetDummyPodOptions {
|
||||
initDead?: number;
|
||||
}
|
||||
|
||||
function getDummyPodDefaultOptions(): Required<GetDummyPodOptions> {
|
||||
return {
|
||||
function getDummyPod(rawOpts?: GetDummyPodOptions): Pod {
|
||||
const opts = {
|
||||
...(rawOpts ?? {}),
|
||||
running: 0,
|
||||
dead: 0,
|
||||
initDead: 0,
|
||||
initRunning: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function getDummyPod(opts: GetDummyPodOptions = getDummyPodDefaultOptions()): Pod {
|
||||
const pod = new Pod({
|
||||
apiVersion: "v1",
|
||||
kind: "Pod",
|
||||
@ -29,36 +29,35 @@ function getDummyPod(opts: GetDummyPodOptions = getDummyPodDefaultOptions()): Po
|
||||
uid: "1",
|
||||
name: "test",
|
||||
resourceVersion: "v1",
|
||||
selfLink: "http",
|
||||
namespace: "default",
|
||||
selfLink: "/api/v1/pods/default/test",
|
||||
},
|
||||
spec: {
|
||||
containers: [],
|
||||
initContainers: [],
|
||||
serviceAccount: "dummy",
|
||||
serviceAccountName: "dummy",
|
||||
},
|
||||
status: {
|
||||
phase: "Running",
|
||||
conditions: [],
|
||||
hostIP: "10.0.0.1",
|
||||
podIP: "10.0.0.1",
|
||||
startTime: "now",
|
||||
containerStatuses: [],
|
||||
initContainerStatuses: [],
|
||||
},
|
||||
});
|
||||
|
||||
pod.spec = {
|
||||
containers: [],
|
||||
initContainers: [],
|
||||
serviceAccount: "dummy",
|
||||
serviceAccountName: "dummy",
|
||||
};
|
||||
|
||||
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({
|
||||
pod.spec.containers?.push({
|
||||
image: "dummy",
|
||||
imagePullPolicy: "dummy",
|
||||
name,
|
||||
});
|
||||
pod.status.containerStatuses.push({
|
||||
pod.status?.containerStatuses?.push({
|
||||
image: "dummy",
|
||||
imageID: "dummy",
|
||||
name,
|
||||
@ -75,12 +74,12 @@ function getDummyPod(opts: GetDummyPodOptions = getDummyPodDefaultOptions()): Po
|
||||
for (let i = 0; i < opts.dead; i += 1) {
|
||||
const name = `container_d_${i}`;
|
||||
|
||||
pod.spec.containers.push({
|
||||
pod.spec.containers?.push({
|
||||
image: "dummy",
|
||||
imagePullPolicy: "dummy",
|
||||
name,
|
||||
});
|
||||
pod.status.containerStatuses.push({
|
||||
pod.status?.containerStatuses?.push({
|
||||
image: "dummy",
|
||||
imageID: "dummy",
|
||||
name,
|
||||
@ -100,12 +99,12 @@ function getDummyPod(opts: GetDummyPodOptions = getDummyPodDefaultOptions()): Po
|
||||
for (let i = 0; i < opts.initRunning; i += 1) {
|
||||
const name = `container_ir_${i}`;
|
||||
|
||||
pod.spec.initContainers.push({
|
||||
pod.spec.initContainers?.push({
|
||||
image: "dummy",
|
||||
imagePullPolicy: "dummy",
|
||||
name,
|
||||
});
|
||||
pod.status.initContainerStatuses.push({
|
||||
pod.status?.initContainerStatuses?.push({
|
||||
image: "dummy",
|
||||
imageID: "dummy",
|
||||
name,
|
||||
@ -122,12 +121,12 @@ function getDummyPod(opts: GetDummyPodOptions = getDummyPodDefaultOptions()): Po
|
||||
for (let i = 0; i < opts.initDead; i += 1) {
|
||||
const name = `container_id_${i}`;
|
||||
|
||||
pod.spec.initContainers.push({
|
||||
pod.spec.initContainers?.push({
|
||||
image: "dummy",
|
||||
imagePullPolicy: "dummy",
|
||||
name,
|
||||
});
|
||||
pod.status.initContainerStatuses.push({
|
||||
pod.status?.initContainerStatuses?.push({
|
||||
image: "dummy",
|
||||
imageID: "dummy",
|
||||
name,
|
||||
@ -253,7 +252,7 @@ describe("Pods", () => {
|
||||
it("should return true if a condition isn't ready", () => {
|
||||
const pod = getDummyPod({ running: 1 });
|
||||
|
||||
pod.status.conditions.push({
|
||||
pod.status?.conditions.push({
|
||||
type: "Ready",
|
||||
status: "foobar",
|
||||
lastProbeTime: 1,
|
||||
@ -266,7 +265,7 @@ describe("Pods", () => {
|
||||
it("should return false if a condition is non-ready", () => {
|
||||
const pod = getDummyPod({ running: 1 });
|
||||
|
||||
pod.status.conditions.push({
|
||||
pod.status?.conditions.push({
|
||||
type: "dummy",
|
||||
status: "foobar",
|
||||
lastProbeTime: 1,
|
||||
@ -278,8 +277,11 @@ describe("Pods", () => {
|
||||
|
||||
it("should return true if a current container is in a crash loop back off", () => {
|
||||
const pod = getDummyPod({ running: 1 });
|
||||
const firstStatus = pod.status?.containerStatuses?.[0];
|
||||
|
||||
pod.status.containerStatuses[0].state = {
|
||||
assert(firstStatus);
|
||||
|
||||
firstStatus.state = {
|
||||
waiting: {
|
||||
reason: "CrashLookBackOff",
|
||||
message: "too much foobar",
|
||||
@ -292,6 +294,8 @@ describe("Pods", () => {
|
||||
it("should return true if a current phase isn't running", () => {
|
||||
const pod = getDummyPod({ running: 1 });
|
||||
|
||||
assert(pod.status);
|
||||
|
||||
pod.status.phase = "not running";
|
||||
|
||||
expect(pod.hasIssues()).toStrictEqual(true);
|
||||
@ -300,6 +304,8 @@ describe("Pods", () => {
|
||||
it("should return false if a current phase is running", () => {
|
||||
const pod = getDummyPod({ running: 1 });
|
||||
|
||||
assert(pod.status);
|
||||
|
||||
pod.status.phase = "Running";
|
||||
|
||||
expect(pod.hasIssues()).toStrictEqual(false);
|
||||
|
||||
@ -7,7 +7,8 @@ import { StatefulSet, StatefulSetApi } from "../endpoints/stateful-set.api";
|
||||
import type { KubeJsonApi } from "../kube-json-api";
|
||||
|
||||
class StatefulSetApiTest extends StatefulSetApi {
|
||||
public setRequest(request: any) {
|
||||
declare protected request: KubeJsonApi;
|
||||
public setRequest(request: KubeJsonApi) {
|
||||
this.request = request;
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,4 +17,4 @@ export const apiKube = isClusterPageContext()
|
||||
"Host": window.location.host,
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
: undefined as never;
|
||||
|
||||
@ -6,15 +6,24 @@
|
||||
import type { KubeObjectStore } from "./kube-object.store";
|
||||
|
||||
import { action, observable, makeObservable } from "mobx";
|
||||
import { autoBind, iter } from "../utils";
|
||||
import { autoBind, isDefined, iter } from "../utils";
|
||||
import type { KubeApi } from "./kube-api";
|
||||
import type { KubeObject } from "./kube-object";
|
||||
import type { IKubeObjectRef } from "./kube-api-parse";
|
||||
import type { KubeJsonApiDataFor, KubeObject, ObjectReference } from "./kube-object";
|
||||
import { parseKubeApi, createKubeApiURL } from "./kube-api-parse";
|
||||
|
||||
export type RegisterableStore<S> = S extends KubeObjectStore<any, any, any>
|
||||
? S
|
||||
: never;
|
||||
export type RegisterableApi<A> = A extends KubeApi<any, any>
|
||||
? A
|
||||
: never;
|
||||
export type KubeObjectStoreFrom<A> = A extends KubeApi<infer K, infer D>
|
||||
? KubeObjectStore<K, A, D>
|
||||
: never;
|
||||
|
||||
export class ApiManager {
|
||||
private apis = observable.map<string, KubeApi<KubeObject>>();
|
||||
private stores = observable.map<string, KubeObjectStore<KubeObject>>();
|
||||
private readonly apis = observable.map<string, KubeApi>();
|
||||
private readonly stores = observable.map<string, KubeObjectStore>();
|
||||
|
||||
constructor() {
|
||||
makeObservable(this);
|
||||
@ -33,27 +42,27 @@ export class ApiManager {
|
||||
return iter.find(this.apis.values(), api => api.kind === kind && api.apiVersionWithGroup === apiVersion);
|
||||
}
|
||||
|
||||
registerApi<K extends KubeObject>(apiBase: string, api: KubeApi<K>) {
|
||||
registerApi<A>(apiBase: string, api: RegisterableApi<A>) {
|
||||
if (!api.apiBase) return;
|
||||
|
||||
if (!this.apis.has(apiBase)) {
|
||||
this.stores.forEach((store) => {
|
||||
if (store.api === api) {
|
||||
if (store.api as never === api) {
|
||||
this.stores.set(apiBase, store);
|
||||
}
|
||||
});
|
||||
|
||||
this.apis.set(apiBase, api);
|
||||
this.apis.set(apiBase, api as never);
|
||||
}
|
||||
}
|
||||
|
||||
protected resolveApi(api?: string | KubeApi<KubeObject>): KubeApi<KubeObject> | undefined {
|
||||
protected resolveApi(api: undefined | string | KubeApi): KubeApi | undefined {
|
||||
if (!api) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (typeof api === "string") {
|
||||
return this.getApi(api) as KubeApi<KubeObject>;
|
||||
return this.getApi(api);
|
||||
}
|
||||
|
||||
return api;
|
||||
@ -69,20 +78,40 @@ export class ApiManager {
|
||||
}
|
||||
}
|
||||
|
||||
registerStore<K>(store: RegisterableStore<K>): void;
|
||||
/**
|
||||
* @deprecated KubeObjectStore's should only every be about a single KubeApi type
|
||||
*/
|
||||
registerStore<K extends KubeObject>(store: KubeObjectStore<K, KubeApi<K>, KubeJsonApiDataFor<K>>, apis: KubeApi<K>[]): void;
|
||||
|
||||
@action
|
||||
registerStore<K extends KubeObject>(store: KubeObjectStore<K>, apis: KubeApi<K>[] = [store.api]) {
|
||||
apis.filter(Boolean).forEach(api => {
|
||||
if (api.apiBase) this.stores.set(api.apiBase, store);
|
||||
});
|
||||
registerStore<K extends KubeObject>(store: KubeObjectStore<K, KubeApi<K>, KubeJsonApiDataFor<K>>, apis: KubeApi<K>[] = [store.api]): void {
|
||||
for (const api of apis.filter(isDefined)) {
|
||||
if (api.apiBase) {
|
||||
this.stores.set(api.apiBase, store as never);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getStore<S extends KubeObjectStore<KubeObject>>(api: string | KubeApi<KubeObject>): S | undefined {
|
||||
return this.stores.get(this.resolveApi(api)?.apiBase) as S;
|
||||
getStore(api: string | undefined): KubeObjectStore | undefined;
|
||||
getStore<A>(api: RegisterableApi<A>): KubeObjectStoreFrom<A> | undefined;
|
||||
/**
|
||||
* @deprecated use an actual cast instead of hiding it with this unused type param
|
||||
*/
|
||||
getStore<S extends KubeObjectStore>(api: string | KubeApi): S | undefined ;
|
||||
getStore(api: string | KubeApi | undefined): KubeObjectStore | undefined {
|
||||
const { apiBase } = this.resolveApi(api) ?? {};
|
||||
|
||||
if (apiBase) {
|
||||
return this.stores.get(apiBase);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
lookupApiLink(ref: IKubeObjectRef, parentObject?: KubeObject): string {
|
||||
lookupApiLink(ref: ObjectReference, parentObject?: KubeObject): string {
|
||||
const {
|
||||
kind, apiVersion, name,
|
||||
kind, apiVersion = "v1", name,
|
||||
namespace = parentObject?.getNs(),
|
||||
} = ref;
|
||||
|
||||
|
||||
@ -3,34 +3,39 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import type { DerivedKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import type { KubeObjectMetadata } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { RoleRef } from "./types/role-ref";
|
||||
import type { Subject } from "./types/subject";
|
||||
|
||||
export type ClusterRoleBindingSubjectKind = "Group" | "ServiceAccount" | "User";
|
||||
|
||||
export interface ClusterRoleBindingSubject {
|
||||
kind: ClusterRoleBindingSubjectKind;
|
||||
name: string;
|
||||
apiGroup?: string;
|
||||
namespace?: string;
|
||||
export interface ClusterRoleBindingData extends KubeJsonApiData<KubeObjectMetadata<"cluster-scoped">, void, void> {
|
||||
subjects?: Subject[];
|
||||
roleRef: RoleRef;
|
||||
}
|
||||
|
||||
export interface ClusterRoleBinding {
|
||||
subjects?: ClusterRoleBindingSubject[];
|
||||
roleRef: {
|
||||
kind: string;
|
||||
name: string;
|
||||
apiGroup?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class ClusterRoleBinding extends KubeObject {
|
||||
export class ClusterRoleBinding extends KubeObject<void, void, "cluster-scoped"> {
|
||||
static kind = "ClusterRoleBinding";
|
||||
static namespaced = false;
|
||||
static apiBase = "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings";
|
||||
|
||||
subjects?: Subject[];
|
||||
roleRef: RoleRef;
|
||||
|
||||
constructor({
|
||||
subjects,
|
||||
roleRef,
|
||||
...rest
|
||||
}: ClusterRoleBindingData) {
|
||||
super(rest);
|
||||
this.subjects = subjects;
|
||||
this.roleRef = roleRef;
|
||||
}
|
||||
|
||||
getSubjects() {
|
||||
return this.subjects || [];
|
||||
return this.subjects ?? [];
|
||||
}
|
||||
|
||||
getSubjectNames(): string {
|
||||
@ -38,17 +43,15 @@ export class ClusterRoleBinding extends KubeObject {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only available within kubernetes cluster pages
|
||||
*/
|
||||
let clusterRoleBindingApi: KubeApi<ClusterRoleBinding>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
clusterRoleBindingApi = new KubeApi({
|
||||
objectConstructor: ClusterRoleBinding,
|
||||
});
|
||||
export class ClusterRoleBindingApi extends KubeApi<ClusterRoleBinding, ClusterRoleBindingData> {
|
||||
constructor(opts: DerivedKubeApiOptions = {}) {
|
||||
super({
|
||||
...opts,
|
||||
objectConstructor: ClusterRoleBinding,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
clusterRoleBindingApi,
|
||||
};
|
||||
export const clusterRoleBindingApi = isClusterPageContext()
|
||||
? new ClusterRoleBindingApi()
|
||||
: undefined as never;
|
||||
|
||||
@ -4,39 +4,47 @@
|
||||
*/
|
||||
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import type { DerivedKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import type { KubeObjectMetadata } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { AggregationRule } from "./types/aggregation-rule";
|
||||
import type { PolicyRule } from "./types/policy-rule";
|
||||
|
||||
export interface ClusterRole {
|
||||
rules: {
|
||||
verbs: string[];
|
||||
apiGroups: string[];
|
||||
resources: string[];
|
||||
resourceNames?: string[];
|
||||
}[];
|
||||
export interface ClusterRoleData extends KubeJsonApiData<KubeObjectMetadata<"cluster-scoped">, void, void> {
|
||||
rules?: PolicyRule[];
|
||||
aggregationRule?: AggregationRule;
|
||||
}
|
||||
|
||||
export class ClusterRole extends KubeObject {
|
||||
export class ClusterRole extends KubeObject<void, void, "cluster-scoped"> {
|
||||
static kind = "ClusterRole";
|
||||
static namespaced = false;
|
||||
static apiBase = "/apis/rbac.authorization.k8s.io/v1/clusterroles";
|
||||
|
||||
rules?: PolicyRule[];
|
||||
aggregationRule?: AggregationRule;
|
||||
|
||||
constructor({ rules, aggregationRule, ...rest }: ClusterRoleData) {
|
||||
super(rest);
|
||||
this.rules = rules;
|
||||
this.aggregationRule = aggregationRule;
|
||||
}
|
||||
|
||||
getRules() {
|
||||
return this.rules || [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only available within kubernetes cluster pages
|
||||
*/
|
||||
let clusterRoleApi: KubeApi<ClusterRole>;
|
||||
|
||||
if (isClusterPageContext()) { // initialize automatically only when within a cluster iframe/context
|
||||
clusterRoleApi = new KubeApi({
|
||||
objectConstructor: ClusterRole,
|
||||
});
|
||||
export class ClusterRoleApi extends KubeApi<ClusterRole, ClusterRoleData> {
|
||||
constructor(opts: DerivedKubeApiOptions = {}) {
|
||||
super({
|
||||
...opts,
|
||||
objectConstructor: ClusterRole,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
clusterRoleApi,
|
||||
};
|
||||
export const clusterRoleApi = isClusterPageContext()
|
||||
? new ClusterRoleApi()
|
||||
: undefined as never;
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { IMetrics, IMetricsReqParams } from "./metrics.api";
|
||||
import type { MetricData, IMetricsReqParams } from "./metrics.api";
|
||||
import { metricsApi } from "./metrics.api";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { KubeApi } from "../kube-api";
|
||||
@ -14,7 +14,7 @@ export class ClusterApi extends KubeApi<Cluster> {
|
||||
static namespaced = true;
|
||||
}
|
||||
|
||||
export function getMetricsByNodeNames(nodeNames: string[], params?: IMetricsReqParams): Promise<IClusterMetrics> {
|
||||
export function getMetricsByNodeNames(nodeNames: string[], params?: IMetricsReqParams): Promise<ClusterMetricData> {
|
||||
const nodes = nodeNames.join("|");
|
||||
const opts = { category: "cluster", nodes };
|
||||
|
||||
@ -45,20 +45,19 @@ export enum ClusterStatus {
|
||||
ERROR = "Error",
|
||||
}
|
||||
|
||||
export interface IClusterMetrics<T = IMetrics> {
|
||||
[metric: string]: T;
|
||||
memoryUsage: T;
|
||||
memoryRequests: T;
|
||||
memoryLimits: T;
|
||||
memoryCapacity: T;
|
||||
cpuUsage: T;
|
||||
cpuRequests: T;
|
||||
cpuLimits: T;
|
||||
cpuCapacity: T;
|
||||
podUsage: T;
|
||||
podCapacity: T;
|
||||
fsSize: T;
|
||||
fsUsage: T;
|
||||
export interface ClusterMetricData extends Partial<Record<string, MetricData>> {
|
||||
memoryUsage: MetricData;
|
||||
memoryRequests: MetricData;
|
||||
memoryLimits: MetricData;
|
||||
memoryCapacity: MetricData;
|
||||
cpuUsage: MetricData;
|
||||
cpuRequests: MetricData;
|
||||
cpuLimits: MetricData;
|
||||
cpuCapacity: MetricData;
|
||||
podUsage: MetricData;
|
||||
podCapacity: MetricData;
|
||||
fsSize: MetricData;
|
||||
fsUsage: MetricData;
|
||||
}
|
||||
|
||||
export interface Cluster {
|
||||
|
||||
@ -3,28 +3,36 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { KubeObjectMetadata } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import type { DerivedKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { autoBind } from "../../utils";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export interface ConfigMap {
|
||||
data: {
|
||||
[param: string]: string;
|
||||
};
|
||||
export interface ConfigMapData extends KubeJsonApiData<KubeObjectMetadata<"namespace-scoped">, void, void> {
|
||||
data?: Partial<Record<string, string>>;
|
||||
binaryData?: Partial<Record<string, string>>;
|
||||
immutable?: boolean;
|
||||
}
|
||||
|
||||
export class ConfigMap extends KubeObject {
|
||||
export class ConfigMap extends KubeObject<void, void, "namespace-scoped"> {
|
||||
static kind = "ConfigMap";
|
||||
static namespaced = true;
|
||||
static apiBase = "/api/v1/configmaps";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
data: Partial<Record<string, string>>;
|
||||
binaryData: Partial<Record<string, string>>;
|
||||
immutable?: boolean;
|
||||
|
||||
constructor({ data, binaryData, immutable, ...rest }: ConfigMapData) {
|
||||
super(rest);
|
||||
autoBind(this);
|
||||
|
||||
this.data ??= {};
|
||||
this.data = data ?? {};
|
||||
this.binaryData = binaryData ?? {};
|
||||
this.immutable = immutable;
|
||||
}
|
||||
|
||||
getKeys(): string[] {
|
||||
@ -32,17 +40,15 @@ export class ConfigMap extends KubeObject {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only available within kubernetes cluster pages
|
||||
*/
|
||||
let configMapApi: KubeApi<ConfigMap>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
configMapApi = new KubeApi({
|
||||
objectConstructor: ConfigMap,
|
||||
});
|
||||
export class ConfigMapApi extends KubeApi<ConfigMap, ConfigMapData> {
|
||||
constructor(opts?: DerivedKubeApiOptions) {
|
||||
super({
|
||||
objectConstructor: ConfigMap,
|
||||
...opts ?? {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
configMapApi,
|
||||
};
|
||||
export const configMapApi = isClusterPageContext()
|
||||
? new ConfigMapApi()
|
||||
: undefined as never;
|
||||
|
||||
@ -3,13 +3,14 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { KubeCreationError, KubeObject } from "../kube-object";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { getLegacyGlobalDiForExtensionApi } from "../../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
|
||||
import customResourcesRouteInjectable from "../../front-end-routing/routes/cluster/custom-resources/custom-resources/custom-resources-route.injectable";
|
||||
import { buildURL } from "../../utils/buildUrl";
|
||||
import type { BaseKubeObjectCondition } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { DerivedKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
interface AdditionalPrinterColumnsCommon {
|
||||
name: string;
|
||||
@ -26,7 +27,7 @@ type AdditionalPrinterColumnsV1Beta = AdditionalPrinterColumnsCommon & {
|
||||
JSONPath: string;
|
||||
};
|
||||
|
||||
export interface CRDVersion {
|
||||
export interface CustomResourceDefinitionVersion {
|
||||
name: string;
|
||||
served: boolean;
|
||||
storage: boolean;
|
||||
@ -34,72 +35,78 @@ export interface CRDVersion {
|
||||
additionalPrinterColumns?: AdditionalPrinterColumnsV1[];
|
||||
}
|
||||
|
||||
export interface CustomResourceDefinitionNames {
|
||||
categories?: string[];
|
||||
kind: string;
|
||||
listKind?: string;
|
||||
plural: string;
|
||||
shortNames?: string[];
|
||||
singular?: string;
|
||||
}
|
||||
|
||||
export interface CustomResourceConversion {
|
||||
strategy?: string;
|
||||
webhook?: WebhookConversion;
|
||||
}
|
||||
|
||||
export interface WebhookConversion {
|
||||
clientConfig?: WebhookClientConfig[];
|
||||
conversionReviewVersions: string[];
|
||||
}
|
||||
|
||||
export interface WebhookClientConfig {
|
||||
caBundle?: string;
|
||||
url?: string;
|
||||
service?: ServiceReference;
|
||||
}
|
||||
|
||||
export interface ServiceReference {
|
||||
name: string;
|
||||
namespace: string;
|
||||
path?: string;
|
||||
port?: number;
|
||||
}
|
||||
|
||||
export interface CustomResourceDefinitionSpec {
|
||||
group: string;
|
||||
/**
|
||||
* @deprecated for apiextensions.k8s.io/v1 but used in v1beta1
|
||||
*/
|
||||
version?: string;
|
||||
names: {
|
||||
plural: string;
|
||||
singular: string;
|
||||
kind: string;
|
||||
listKind: string;
|
||||
};
|
||||
names: CustomResourceDefinitionNames;
|
||||
scope: "Namespaced" | "Cluster";
|
||||
/**
|
||||
* @deprecated for apiextensions.k8s.io/v1 but used in v1beta1
|
||||
*/
|
||||
validation?: object;
|
||||
versions?: CRDVersion[];
|
||||
conversion: {
|
||||
strategy?: string;
|
||||
webhook?: any;
|
||||
};
|
||||
versions?: CustomResourceDefinitionVersion[];
|
||||
conversion?: CustomResourceConversion;
|
||||
/**
|
||||
* @deprecated for apiextensions.k8s.io/v1 but used in v1beta1
|
||||
*/
|
||||
additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[];
|
||||
preserveUnknownFields?: boolean;
|
||||
}
|
||||
|
||||
export interface CustomResourceDefinition {
|
||||
spec: CustomResourceDefinitionSpec;
|
||||
status: {
|
||||
conditions: {
|
||||
lastTransitionTime: string;
|
||||
message: string;
|
||||
reason: string;
|
||||
status: string;
|
||||
type?: string;
|
||||
}[];
|
||||
acceptedNames: {
|
||||
plural: string;
|
||||
singular: string;
|
||||
kind: string;
|
||||
shortNames: string[];
|
||||
listKind: string;
|
||||
};
|
||||
storedVersions: string[];
|
||||
};
|
||||
export interface CustomResourceDefinitionConditionAcceptedNames {
|
||||
plural: string;
|
||||
singular: string;
|
||||
kind: string;
|
||||
shortNames: string[];
|
||||
listKind: string;
|
||||
}
|
||||
|
||||
export interface CRDApiData extends KubeJsonApiData {
|
||||
spec: object; // TODO: make better
|
||||
export interface CustomResourceDefinitionStatus {
|
||||
conditions?: BaseKubeObjectCondition[];
|
||||
acceptedNames: CustomResourceDefinitionConditionAcceptedNames;
|
||||
storedVersions: string[];
|
||||
}
|
||||
|
||||
export class CustomResourceDefinition extends KubeObject {
|
||||
export class CustomResourceDefinition extends KubeObject<CustomResourceDefinitionStatus, CustomResourceDefinitionSpec, "cluster-scoped"> {
|
||||
static kind = "CustomResourceDefinition";
|
||||
static namespaced = false;
|
||||
static apiBase = "/apis/apiextensions.k8s.io/v1/customresourcedefinitions";
|
||||
|
||||
constructor(data: CRDApiData) {
|
||||
super(data);
|
||||
|
||||
if (!data.spec || typeof data.spec !== "object") {
|
||||
throw new KubeCreationError("Cannot create a CustomResourceDefinition from an object without spec", data);
|
||||
}
|
||||
}
|
||||
|
||||
getResourceUrl() {
|
||||
const di = getLegacyGlobalDiForExtensionApi();
|
||||
|
||||
@ -141,12 +148,12 @@ export class CustomResourceDefinition extends KubeObject {
|
||||
return this.spec.scope;
|
||||
}
|
||||
|
||||
getPreferedVersion(): CRDVersion {
|
||||
getPreferedVersion(): CustomResourceDefinitionVersion {
|
||||
const { apiVersion } = this;
|
||||
|
||||
switch (apiVersion) {
|
||||
case "apiextensions.k8s.io/v1":
|
||||
for (const version of this.spec.versions) {
|
||||
for (const version of this.spec.versions ?? []) {
|
||||
if (version.storage) {
|
||||
return version;
|
||||
}
|
||||
@ -158,7 +165,8 @@ export class CustomResourceDefinition extends KubeObject {
|
||||
const additionalPrinterColumns = apc?.map(({ JSONPath, ...apc }) => ({ ...apc, jsonPath: JSONPath }));
|
||||
|
||||
return {
|
||||
name: this.spec.version,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
name: this.spec.version!,
|
||||
served: true,
|
||||
storage: true,
|
||||
schema: this.spec.validation,
|
||||
@ -179,7 +187,7 @@ export class CustomResourceDefinition extends KubeObject {
|
||||
}
|
||||
|
||||
getStoredVersions() {
|
||||
return this.status.storedVersions.join(", ");
|
||||
return this.status?.storedVersions.join(", ") ?? "";
|
||||
}
|
||||
|
||||
getNames() {
|
||||
@ -216,18 +224,16 @@ export class CustomResourceDefinition extends KubeObject {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only available within kubernetes cluster pages
|
||||
*/
|
||||
let crdApi: KubeApi<CustomResourceDefinition>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
crdApi = new KubeApi<CustomResourceDefinition>({
|
||||
objectConstructor: CustomResourceDefinition,
|
||||
checkPreferredVersion: true,
|
||||
});
|
||||
export class CustomResourceDefinitionApi extends KubeApi<CustomResourceDefinition> {
|
||||
constructor(opts: DerivedKubeApiOptions = {}) {
|
||||
super({
|
||||
objectConstructor: CustomResourceDefinition,
|
||||
checkPreferredVersion: true,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
crdApi,
|
||||
};
|
||||
export const crdApi = isClusterPageContext()
|
||||
? new CustomResourceDefinitionApi()
|
||||
: undefined as never;
|
||||
|
||||
@ -4,15 +4,22 @@
|
||||
*/
|
||||
|
||||
import moment from "moment";
|
||||
import type { ObjectReference } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { IPodContainer } from "./pods.api";
|
||||
import { formatDuration } from "../../utils/formatDuration";
|
||||
import { autoBind } from "../../utils";
|
||||
import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import type { JobTemplateSpec } from "./types/job-template-spec";
|
||||
|
||||
export class CronJobApi extends KubeApi<CronJob> {
|
||||
constructor(opts: DerivedKubeApiOptions & IgnoredKubeApiOptions = {}) {
|
||||
super({
|
||||
...opts,
|
||||
objectConstructor: CronJob,
|
||||
});
|
||||
}
|
||||
|
||||
suspend(params: { namespace: string; name: string }) {
|
||||
return this.request.patch(this.getUrl(params), {
|
||||
data: {
|
||||
@ -44,61 +51,33 @@ export class CronJobApi extends KubeApi<CronJob> {
|
||||
}
|
||||
}
|
||||
|
||||
export interface CronJob {
|
||||
spec: {
|
||||
schedule: string;
|
||||
concurrencyPolicy: string;
|
||||
suspend: boolean;
|
||||
jobTemplate: {
|
||||
metadata: {
|
||||
creationTimestamp?: string;
|
||||
labels?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
annotations?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
};
|
||||
spec: {
|
||||
template: {
|
||||
metadata: {
|
||||
creationTimestamp?: string;
|
||||
};
|
||||
spec: {
|
||||
containers: IPodContainer[];
|
||||
restartPolicy: string;
|
||||
terminationGracePeriodSeconds: number;
|
||||
dnsPolicy: string;
|
||||
hostPID: boolean;
|
||||
schedulerName: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
successfulJobsHistoryLimit: number;
|
||||
failedJobsHistoryLimit: number;
|
||||
};
|
||||
status: {
|
||||
lastScheduleTime?: string;
|
||||
};
|
||||
export interface CronJobSpec {
|
||||
concurrencyPolicy?: string;
|
||||
failedJobsHistoryLimit?: number;
|
||||
jobTemplate?: JobTemplateSpec;
|
||||
schedule: string;
|
||||
startingDeadlineSeconds?: number;
|
||||
successfulJobsHistoryLimit?: number;
|
||||
suspend?: boolean;
|
||||
}
|
||||
|
||||
export class CronJob extends KubeObject {
|
||||
static kind = "CronJob";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/batch/v1beta1/cronjobs";
|
||||
export interface CronJobStatus {
|
||||
lastScheduleTime?: string;
|
||||
lastSuccessfulTime?: string;
|
||||
active?: ObjectReference[];
|
||||
}
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
export class CronJob extends KubeObject<CronJobStatus, CronJobSpec, "namespace-scoped"> {
|
||||
static readonly kind = "CronJob";
|
||||
static readonly namespaced = true;
|
||||
static readonly apiBase = "/apis/batch/v1beta1/cronjobs";
|
||||
|
||||
getSuspendFlag() {
|
||||
return this.spec.suspend.toString();
|
||||
return (this.spec.suspend ?? false).toString();
|
||||
}
|
||||
|
||||
getLastScheduleTime() {
|
||||
if (!this.status.lastScheduleTime) return "-";
|
||||
if (!this.status?.lastScheduleTime) return "-";
|
||||
const diff = moment().diff(this.status.lastScheduleTime);
|
||||
|
||||
return formatDuration(diff, true);
|
||||
@ -125,17 +104,6 @@ export class CronJob extends KubeObject {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only available within kubernetes cluster pages
|
||||
*/
|
||||
let cronJobApi: CronJobApi;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
cronJobApi = new CronJobApi({
|
||||
objectConstructor: CronJob,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
cronJobApi,
|
||||
};
|
||||
export const cronJobApi = isClusterPageContext()
|
||||
? new CronJobApi()
|
||||
: undefined as never;
|
||||
|
||||
@ -3,88 +3,92 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import get from "lodash/get";
|
||||
import type { IAffinity } from "../workload-kube-object";
|
||||
import { WorkloadKubeObject } from "../workload-kube-object";
|
||||
import { autoBind } from "../../utils";
|
||||
import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { metricsApi } from "./metrics.api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import type { IPodContainer, IPodMetrics } from "./pods.api";
|
||||
import type { PodMetricData } from "./pods.api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import type { LabelSelector } from "../kube-object";
|
||||
import type { KubeObjectStatus, LabelSelector } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { PodTemplateSpec } from "./types/pod-template-spec";
|
||||
|
||||
export class DaemonSet extends WorkloadKubeObject {
|
||||
export interface RollingUpdateDaemonSet {
|
||||
maxUnavailable?: number | string;
|
||||
maxSurge?: number | string;
|
||||
}
|
||||
|
||||
export interface DaemonSetUpdateStrategy {
|
||||
type: string;
|
||||
rollingUpdate: RollingUpdateDaemonSet;
|
||||
}
|
||||
|
||||
export interface DaemonSetSpec {
|
||||
selector: LabelSelector;
|
||||
template: PodTemplateSpec;
|
||||
updateStrategy: DaemonSetUpdateStrategy;
|
||||
minReadySeconds?: number;
|
||||
revisionHistoryLimit?: number;
|
||||
}
|
||||
|
||||
export interface DaemonSetStatus extends KubeObjectStatus {
|
||||
collisionCount?: number;
|
||||
currentNumberScheduled: number;
|
||||
desiredNumberScheduled: number;
|
||||
numberAvailable?: number;
|
||||
numberMisscheduled: number;
|
||||
numberReady: number;
|
||||
numberUnavailable?: number;
|
||||
observedGeneration?: number;
|
||||
updatedNumberScheduled?: number;
|
||||
}
|
||||
|
||||
export class DaemonSet extends KubeObject<DaemonSetStatus, DaemonSetSpec, "namespace-scoped"> {
|
||||
static kind = "DaemonSet";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/apps/v1/daemonsets";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
getSelectors(): string[] {
|
||||
return KubeObject.stringifyLabels(this.spec.selector.matchLabels);
|
||||
}
|
||||
|
||||
declare spec: {
|
||||
selector: LabelSelector;
|
||||
template: {
|
||||
metadata: {
|
||||
creationTimestamp?: string;
|
||||
labels: {
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
spec: {
|
||||
containers: IPodContainer[];
|
||||
initContainers?: IPodContainer[];
|
||||
restartPolicy: string;
|
||||
terminationGracePeriodSeconds: number;
|
||||
dnsPolicy: string;
|
||||
hostPID: boolean;
|
||||
affinity?: IAffinity;
|
||||
nodeSelector?: {
|
||||
[selector: string]: string;
|
||||
};
|
||||
securityContext: {};
|
||||
schedulerName: string;
|
||||
tolerations: {
|
||||
key: string;
|
||||
operator: string;
|
||||
effect: string;
|
||||
tolerationSeconds: number;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
updateStrategy: {
|
||||
type: string;
|
||||
rollingUpdate: {
|
||||
maxUnavailable: number;
|
||||
};
|
||||
};
|
||||
revisionHistoryLimit: number;
|
||||
};
|
||||
declare status: {
|
||||
currentNumberScheduled: number;
|
||||
numberMisscheduled: number;
|
||||
desiredNumberScheduled: number;
|
||||
numberReady: number;
|
||||
observedGeneration: number;
|
||||
updatedNumberScheduled: number;
|
||||
numberAvailable: number;
|
||||
numberUnavailable: number;
|
||||
};
|
||||
getNodeSelectors(): string[] {
|
||||
return KubeObject.stringifyLabels(this.spec.template.spec?.nodeSelector);
|
||||
}
|
||||
|
||||
getTemplateLabels(): string[] {
|
||||
return KubeObject.stringifyLabels(this.spec.template.metadata?.labels);
|
||||
}
|
||||
|
||||
getTolerations() {
|
||||
return this.spec.template.spec?.tolerations ?? [];
|
||||
}
|
||||
|
||||
getAffinity() {
|
||||
return this.spec.template.spec?.affinity;
|
||||
}
|
||||
|
||||
getAffinityNumber() {
|
||||
return Object.keys(this.getAffinity() ?? {}).length;
|
||||
}
|
||||
|
||||
getImages() {
|
||||
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []);
|
||||
const initContainers: IPodContainer[] = get(this, "spec.template.spec.initContainers", []);
|
||||
const containers = this.spec.template?.spec?.containers ?? [];
|
||||
const initContainers = this.spec.template?.spec?.initContainers ?? [];
|
||||
|
||||
return [...containers, ...initContainers].map(container => container.image);
|
||||
}
|
||||
}
|
||||
|
||||
export class DaemonSetApi extends KubeApi<DaemonSet> {
|
||||
constructor(opts: DerivedKubeApiOptions & IgnoredKubeApiOptions = {}) {
|
||||
super({
|
||||
...opts,
|
||||
objectConstructor: DaemonSet,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getMetricsForDaemonSets(daemonsets: DaemonSet[], namespace: string, selector = ""): Promise<IPodMetrics> {
|
||||
export function getMetricsForDaemonSets(daemonsets: DaemonSet[], namespace: string, selector = ""): Promise<PodMetricData> {
|
||||
const podSelector = daemonsets.map(daemonset => `${daemonset.getName()}-[[:alnum:]]{5}`).join("|");
|
||||
const opts = { category: "pods", pods: podSelector, namespace, selector };
|
||||
|
||||
@ -101,17 +105,6 @@ export function getMetricsForDaemonSets(daemonsets: DaemonSet[], namespace: stri
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Only available within kubernetes cluster pages
|
||||
*/
|
||||
let daemonSetApi: DaemonSetApi;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
daemonSetApi = new DaemonSetApi({
|
||||
objectConstructor: DaemonSet,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
daemonSetApi,
|
||||
};
|
||||
export const daemonSetApi = isClusterPageContext()
|
||||
? new DaemonSetApi()
|
||||
: undefined as never;
|
||||
|
||||
@ -5,25 +5,35 @@
|
||||
|
||||
import moment from "moment";
|
||||
|
||||
import type { IAffinity } from "../workload-kube-object";
|
||||
import { WorkloadKubeObject } from "../workload-kube-object";
|
||||
import { autoBind } from "../../utils";
|
||||
import type { DerivedKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { metricsApi } from "./metrics.api";
|
||||
import type { IPodMetrics } from "./pods.api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import type { PodMetricData, PodSpec } from "./pods.api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import type { LabelSelector } from "../kube-object";
|
||||
import type { KubeObjectStatus, LabelSelector } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { hasTypedProperty, isNumber, isObject } from "../../utils";
|
||||
|
||||
export class DeploymentApi extends KubeApi<Deployment> {
|
||||
constructor(opts?: DerivedKubeApiOptions) {
|
||||
super({
|
||||
objectConstructor: Deployment,
|
||||
...opts ?? {},
|
||||
});
|
||||
}
|
||||
|
||||
protected getScaleApiUrl(params: { namespace: string; name: string }) {
|
||||
return `${this.getUrl(params)}/scale`;
|
||||
}
|
||||
|
||||
getReplicas(params: { namespace: string; name: string }): Promise<number> {
|
||||
return this.request
|
||||
.get(this.getScaleApiUrl(params))
|
||||
.then(({ status }: any) => status?.replicas);
|
||||
async getReplicas(params: { namespace: string; name: string }): Promise<number> {
|
||||
const { status } = await this.request.get(this.getScaleApiUrl(params));
|
||||
|
||||
if (isObject(status) && hasTypedProperty(status, "replicas", isNumber)) {
|
||||
return status.replicas;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
scale(params: { namespace: string; name: string }, replicas: number) {
|
||||
@ -61,7 +71,7 @@ export class DeploymentApi extends KubeApi<Deployment> {
|
||||
}
|
||||
}
|
||||
|
||||
export function getMetricsForDeployments(deployments: Deployment[], namespace: string, selector = ""): Promise<IPodMetrics> {
|
||||
export function getMetricsForDeployments(deployments: Deployment[], namespace: string, selector = ""): Promise<PodMetricData> {
|
||||
const podSelector = deployments.map(deployment => `${deployment.getName()}-[[:alnum:]]{9,}-[[:alnum:]]{5}`).join("|");
|
||||
const opts = { category: "pods", pods: podSelector, namespace, selector };
|
||||
|
||||
@ -78,136 +88,66 @@ export function getMetricsForDeployments(deployments: Deployment[], namespace: s
|
||||
});
|
||||
}
|
||||
|
||||
interface IContainerProbe {
|
||||
httpGet?: {
|
||||
path?: string;
|
||||
port: number;
|
||||
scheme: string;
|
||||
host?: string;
|
||||
export interface DeploymentSpec {
|
||||
replicas: number;
|
||||
selector: LabelSelector;
|
||||
template: {
|
||||
metadata: {
|
||||
creationTimestamp?: string;
|
||||
labels: Record<string, string | undefined>;
|
||||
annotations?: Record<string, string | undefined>;
|
||||
};
|
||||
spec: PodSpec;
|
||||
};
|
||||
exec?: {
|
||||
command: string[];
|
||||
strategy: {
|
||||
type: string;
|
||||
rollingUpdate: {
|
||||
maxUnavailable: number;
|
||||
maxSurge: number;
|
||||
};
|
||||
};
|
||||
tcpSocket?: {
|
||||
port: number;
|
||||
};
|
||||
initialDelaySeconds?: number;
|
||||
timeoutSeconds?: number;
|
||||
periodSeconds?: number;
|
||||
successThreshold?: number;
|
||||
failureThreshold?: number;
|
||||
}
|
||||
|
||||
export class Deployment extends WorkloadKubeObject {
|
||||
export interface DeploymentStatus extends KubeObjectStatus {
|
||||
observedGeneration: number;
|
||||
replicas: number;
|
||||
updatedReplicas: number;
|
||||
readyReplicas: number;
|
||||
availableReplicas?: number;
|
||||
unavailableReplicas?: number;
|
||||
}
|
||||
|
||||
export class Deployment extends KubeObject<DeploymentStatus, DeploymentSpec, "namespace-scoped"> {
|
||||
static kind = "Deployment";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/apps/v1/deployments";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
getSelectors(): string[] {
|
||||
return KubeObject.stringifyLabels(this.spec.selector.matchLabels);
|
||||
}
|
||||
|
||||
declare spec: {
|
||||
replicas: number;
|
||||
selector: LabelSelector;
|
||||
template: {
|
||||
metadata: {
|
||||
creationTimestamp?: string;
|
||||
labels: { [app: string]: string };
|
||||
annotations?: { [app: string]: string };
|
||||
};
|
||||
spec: {
|
||||
containers: {
|
||||
name: string;
|
||||
image: string;
|
||||
args?: string[];
|
||||
ports?: {
|
||||
name: string;
|
||||
containerPort: number;
|
||||
protocol: string;
|
||||
}[];
|
||||
env?: {
|
||||
name: string;
|
||||
value: string;
|
||||
}[];
|
||||
resources: {
|
||||
limits?: {
|
||||
cpu: string;
|
||||
memory: string;
|
||||
};
|
||||
requests: {
|
||||
cpu: string;
|
||||
memory: string;
|
||||
};
|
||||
};
|
||||
volumeMounts?: {
|
||||
name: string;
|
||||
mountPath: string;
|
||||
}[];
|
||||
livenessProbe?: IContainerProbe;
|
||||
readinessProbe?: IContainerProbe;
|
||||
startupProbe?: IContainerProbe;
|
||||
terminationMessagePath: string;
|
||||
terminationMessagePolicy: string;
|
||||
imagePullPolicy: string;
|
||||
}[];
|
||||
restartPolicy: string;
|
||||
terminationGracePeriodSeconds: number;
|
||||
dnsPolicy: string;
|
||||
affinity?: IAffinity;
|
||||
nodeSelector?: {
|
||||
[selector: string]: string;
|
||||
};
|
||||
serviceAccountName: string;
|
||||
serviceAccount: string;
|
||||
securityContext: {};
|
||||
schedulerName: string;
|
||||
tolerations?: {
|
||||
key: string;
|
||||
operator: string;
|
||||
effect: string;
|
||||
tolerationSeconds: number;
|
||||
}[];
|
||||
volumes?: {
|
||||
name: string;
|
||||
configMap: {
|
||||
name: string;
|
||||
defaultMode: number;
|
||||
optional: boolean;
|
||||
};
|
||||
}[];
|
||||
};
|
||||
};
|
||||
strategy: {
|
||||
type: string;
|
||||
rollingUpdate: {
|
||||
maxUnavailable: number;
|
||||
maxSurge: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
declare status: {
|
||||
observedGeneration: number;
|
||||
replicas: number;
|
||||
updatedReplicas: number;
|
||||
readyReplicas: number;
|
||||
availableReplicas?: number;
|
||||
unavailableReplicas?: number;
|
||||
conditions: {
|
||||
type: string;
|
||||
status: string;
|
||||
lastUpdateTime: string;
|
||||
lastTransitionTime: string;
|
||||
reason: string;
|
||||
message: string;
|
||||
}[];
|
||||
};
|
||||
getNodeSelectors(): string[] {
|
||||
return KubeObject.stringifyLabels(this.spec.template.spec.nodeSelector);
|
||||
}
|
||||
|
||||
getTemplateLabels(): string[] {
|
||||
return KubeObject.stringifyLabels(this.spec.template.metadata.labels);
|
||||
}
|
||||
|
||||
getTolerations() {
|
||||
return this.spec.template.spec.tolerations ?? [];
|
||||
}
|
||||
|
||||
getAffinity() {
|
||||
return this.spec.template.spec.affinity;
|
||||
}
|
||||
|
||||
getAffinityNumber() {
|
||||
return Object.keys(this.getAffinity() ?? {}).length;
|
||||
}
|
||||
|
||||
getConditions(activeOnly = false) {
|
||||
const { conditions } = this.status;
|
||||
|
||||
if (!conditions) return [];
|
||||
const { conditions = [] } = this.status ?? {};
|
||||
|
||||
if (activeOnly) {
|
||||
return conditions.filter(c => c.status === "True");
|
||||
@ -217,7 +157,9 @@ export class Deployment extends WorkloadKubeObject {
|
||||
}
|
||||
|
||||
getConditionsText(activeOnly = true) {
|
||||
return this.getConditions(activeOnly).map(({ type }) => type).join(" ");
|
||||
return this.getConditions(activeOnly)
|
||||
.map(({ type }) => type)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
getReplicas() {
|
||||
@ -225,14 +167,6 @@ export class Deployment extends WorkloadKubeObject {
|
||||
}
|
||||
}
|
||||
|
||||
let deploymentApi: DeploymentApi;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
deploymentApi = new DeploymentApi({
|
||||
objectConstructor: Deployment,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
deploymentApi,
|
||||
};
|
||||
export const deploymentApi = isClusterPageContext()
|
||||
? new DeploymentApi()
|
||||
: undefined as never;
|
||||
|
||||
@ -4,144 +4,119 @@
|
||||
*/
|
||||
|
||||
import { autoBind } from "../../utils";
|
||||
import type { KubeObjectMetadata, ObjectReference } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { DerivedKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { get } from "lodash";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export interface IEndpointPort {
|
||||
export function formatEndpointSubset(subset: EndpointSubset): string {
|
||||
const { addresses, ports } = subset;
|
||||
|
||||
if (!addresses || !ports) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return addresses
|
||||
.map(address => (
|
||||
ports
|
||||
.map(port => `${address.ip}:${port.port}`)
|
||||
.join(", ")
|
||||
))
|
||||
.join(", ");
|
||||
|
||||
}
|
||||
|
||||
export interface ForZone {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface EndpointHints {
|
||||
forZones?: ForZone[];
|
||||
}
|
||||
|
||||
export interface EndpointConditions {
|
||||
ready?: boolean;
|
||||
serving?: boolean;
|
||||
terminating?: boolean;
|
||||
}
|
||||
|
||||
export interface EndpointData {
|
||||
addresses: string[];
|
||||
conditions?: EndpointConditions;
|
||||
hints?: EndpointHints;
|
||||
hostname?: string;
|
||||
nodeName?: string;
|
||||
targetRef?: ObjectReference;
|
||||
zone?: string;
|
||||
}
|
||||
|
||||
export interface EndpointPort {
|
||||
appProtocol?: string;
|
||||
name?: string;
|
||||
protocol: string;
|
||||
protocol?: string;
|
||||
port: number;
|
||||
}
|
||||
|
||||
export interface IEndpointAddress {
|
||||
hostname: string;
|
||||
export interface EndpointAddress {
|
||||
hostname?: string;
|
||||
ip: string;
|
||||
nodeName: string;
|
||||
nodeName?: string;
|
||||
targetRef?: ObjectReference;
|
||||
}
|
||||
|
||||
export interface IEndpointSubset {
|
||||
addresses: IEndpointAddress[];
|
||||
notReadyAddresses: IEndpointAddress[];
|
||||
ports: IEndpointPort[];
|
||||
export interface EndpointSubset {
|
||||
addresses?: EndpointAddress[];
|
||||
notReadyAddresses?: EndpointAddress[];
|
||||
ports?: EndpointPort[];
|
||||
}
|
||||
|
||||
interface ITargetRef {
|
||||
kind: string;
|
||||
namespace: string;
|
||||
name: string;
|
||||
uid: string;
|
||||
resourceVersion: string;
|
||||
apiVersion: string;
|
||||
export interface EndpointsData extends KubeJsonApiData<KubeObjectMetadata<"namespace-scoped">, void, void> {
|
||||
subsets?: EndpointSubset[];
|
||||
}
|
||||
|
||||
export class EndpointAddress implements IEndpointAddress {
|
||||
hostname: string;
|
||||
ip: string;
|
||||
nodeName: string;
|
||||
targetRef?: {
|
||||
kind: string;
|
||||
namespace: string;
|
||||
name: string;
|
||||
uid: string;
|
||||
resourceVersion: string;
|
||||
};
|
||||
|
||||
static create(data: IEndpointAddress): EndpointAddress {
|
||||
return new EndpointAddress(data);
|
||||
}
|
||||
|
||||
constructor(data: IEndpointAddress) {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.ip;
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.hostname;
|
||||
}
|
||||
|
||||
getTargetRef(): ITargetRef {
|
||||
if (this.targetRef) {
|
||||
return Object.assign(this.targetRef, { apiVersion: "v1" });
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class EndpointSubset implements IEndpointSubset {
|
||||
addresses: IEndpointAddress[];
|
||||
notReadyAddresses: IEndpointAddress[];
|
||||
ports: IEndpointPort[];
|
||||
|
||||
constructor(data: IEndpointSubset) {
|
||||
this.addresses = get(data, "addresses", []);
|
||||
this.notReadyAddresses = get(data, "notReadyAddresses", []);
|
||||
this.ports = get(data, "ports", []);
|
||||
}
|
||||
|
||||
getAddresses(): EndpointAddress[] {
|
||||
return this.addresses.map(EndpointAddress.create);
|
||||
}
|
||||
|
||||
getNotReadyAddresses(): EndpointAddress[] {
|
||||
return this.notReadyAddresses.map(EndpointAddress.create);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.addresses
|
||||
.map(address => (
|
||||
this.ports
|
||||
.map(port => `${address.ip}:${port.port}`)
|
||||
.join(", ")
|
||||
))
|
||||
.join(", ");
|
||||
}
|
||||
}
|
||||
|
||||
export interface Endpoint {
|
||||
subsets: IEndpointSubset[];
|
||||
}
|
||||
|
||||
export class Endpoint extends KubeObject {
|
||||
export class Endpoints extends KubeObject<void, void, "namespace-scoped"> {
|
||||
static kind = "Endpoints";
|
||||
static namespaced = true;
|
||||
static apiBase = "/api/v1/endpoints";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
subsets?: EndpointSubset[];
|
||||
|
||||
constructor({ subsets, ...rest }: EndpointsData) {
|
||||
super(rest);
|
||||
autoBind(this);
|
||||
this.subsets = subsets;
|
||||
}
|
||||
|
||||
getEndpointSubsets(): EndpointSubset[] {
|
||||
const subsets = this.subsets || [];
|
||||
|
||||
return subsets.map(s => new EndpointSubset(s));
|
||||
getEndpointSubsets(): Required<EndpointSubset>[] {
|
||||
return this.subsets?.map(({
|
||||
addresses = [],
|
||||
notReadyAddresses = [],
|
||||
ports = [],
|
||||
}) => ({
|
||||
addresses,
|
||||
notReadyAddresses,
|
||||
ports,
|
||||
})) ?? [];
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
if(this.subsets) {
|
||||
return this.getEndpointSubsets().map(es => es.toString()).join(", ");
|
||||
} else {
|
||||
return "<none>";
|
||||
}
|
||||
return this.getEndpointSubsets()
|
||||
.map(formatEndpointSubset)
|
||||
.join(", ") || "<none>";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let endpointApi: KubeApi<Endpoint>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
endpointApi = new KubeApi<Endpoint>({
|
||||
objectConstructor: Endpoint,
|
||||
});
|
||||
export class EndpointsApi extends KubeApi<Endpoints, EndpointsData> {
|
||||
constructor(opts: DerivedKubeApiOptions = {}) {
|
||||
super({
|
||||
objectConstructor: Endpoints,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
endpointApi,
|
||||
};
|
||||
export const endpointsApi = isClusterPageContext()
|
||||
? new EndpointsApi()
|
||||
: undefined as never;
|
||||
|
||||
@ -4,34 +4,39 @@
|
||||
*/
|
||||
|
||||
import moment from "moment";
|
||||
import type { KubeObjectMetadata, ObjectReference } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { formatDuration } from "../../utils/formatDuration";
|
||||
import type { DerivedKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
|
||||
export interface KubeEvent {
|
||||
involvedObject: {
|
||||
kind: string;
|
||||
namespace: string;
|
||||
name: string;
|
||||
uid: string;
|
||||
apiVersion: string;
|
||||
resourceVersion: string;
|
||||
fieldPath: string;
|
||||
};
|
||||
reason: string;
|
||||
message: string;
|
||||
source: {
|
||||
component: string;
|
||||
host: string;
|
||||
};
|
||||
firstTimestamp: string;
|
||||
lastTimestamp: string;
|
||||
count: number;
|
||||
type: "Normal" | "Warning" | string;
|
||||
eventTime: null;
|
||||
reportingComponent: string;
|
||||
reportingInstance: string;
|
||||
export interface EventSeries {
|
||||
count?: number;
|
||||
lastObservedTime?: string;
|
||||
}
|
||||
|
||||
export interface EventSource {
|
||||
component?: string;
|
||||
host?: string;
|
||||
}
|
||||
|
||||
export interface KubeEventData extends KubeJsonApiData<KubeObjectMetadata, void, void> {
|
||||
action?: string;
|
||||
count?: number;
|
||||
eventTime?: string;
|
||||
firstTimestamp?: string;
|
||||
involvedObject: Required<ObjectReference>;
|
||||
lastTimestamp?: string;
|
||||
message?: string;
|
||||
reason?: string;
|
||||
related?: ObjectReference;
|
||||
reportingComponent?: string;
|
||||
reportingInstance?: string;
|
||||
series?: EventSeries;
|
||||
source?: EventSource;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export class KubeEvent extends KubeObject {
|
||||
@ -39,14 +44,73 @@ export class KubeEvent extends KubeObject {
|
||||
static namespaced = true;
|
||||
static apiBase = "/api/v1/events";
|
||||
|
||||
action?: string;
|
||||
count?: number;
|
||||
eventTime?: string;
|
||||
firstTimestamp?: string;
|
||||
involvedObject: Required<ObjectReference>;
|
||||
lastTimestamp?: string;
|
||||
message?: string;
|
||||
reason?: string;
|
||||
related?: ObjectReference;
|
||||
reportingComponent?: string;
|
||||
reportingInstance?: string;
|
||||
series?: EventSeries;
|
||||
source?: EventSource;
|
||||
|
||||
/**
|
||||
* Current supported values are:
|
||||
* - "Normal"
|
||||
* - "Warning"
|
||||
*/
|
||||
type?: string;
|
||||
|
||||
constructor({
|
||||
action,
|
||||
count,
|
||||
eventTime,
|
||||
firstTimestamp,
|
||||
involvedObject,
|
||||
lastTimestamp,
|
||||
message,
|
||||
reason,
|
||||
related,
|
||||
reportingComponent,
|
||||
reportingInstance,
|
||||
series,
|
||||
source,
|
||||
type,
|
||||
...rest
|
||||
}: KubeEventData) {
|
||||
super(rest);
|
||||
this.action = action;
|
||||
this.count = count;
|
||||
this.eventTime = eventTime;
|
||||
this.firstTimestamp = firstTimestamp;
|
||||
this.involvedObject = involvedObject;
|
||||
this.lastTimestamp = lastTimestamp;
|
||||
this.message = message;
|
||||
this.reason = reason;
|
||||
this.related = related;
|
||||
this.reportingComponent = reportingComponent;
|
||||
this.reportingInstance = reportingInstance;
|
||||
this.series = series;
|
||||
this.source = source;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
isWarning() {
|
||||
return this.type === "Warning";
|
||||
}
|
||||
|
||||
getSource() {
|
||||
const { component, host } = this.source;
|
||||
if (!this.source?.component) {
|
||||
return "<unknown>";
|
||||
}
|
||||
|
||||
return `${component} ${host || ""}`;
|
||||
const { component, host = "" } = this.source;
|
||||
|
||||
return `${component} ${host}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -68,14 +132,15 @@ export class KubeEvent extends KubeObject {
|
||||
}
|
||||
}
|
||||
|
||||
let eventApi: KubeApi<KubeEvent>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
eventApi = new KubeApi<KubeEvent>({
|
||||
objectConstructor: KubeEvent,
|
||||
});
|
||||
export class KubeEventApi extends KubeApi<KubeEvent, KubeEventData> {
|
||||
constructor(opts: DerivedKubeApiOptions = {}) {
|
||||
super({
|
||||
objectConstructor: KubeEvent,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
eventApi,
|
||||
};
|
||||
export const eventApi = isClusterPageContext()
|
||||
? new KubeEventApi()
|
||||
: undefined as never;
|
||||
|
||||
@ -7,7 +7,7 @@ import { compile } from "path-to-regexp";
|
||||
import { apiBase } from "../index";
|
||||
import { stringify } from "querystring";
|
||||
import type { RequestInit } from "node-fetch";
|
||||
import { autoBind, bifurcateArray } from "../../utils";
|
||||
import { autoBind, bifurcateArray, isDefined } from "../../utils";
|
||||
import Joi from "joi";
|
||||
|
||||
export type RepoHelmChartList = Record<string, RawHelmChart[]>;
|
||||
@ -30,9 +30,9 @@ export async function listCharts(): Promise<HelmChart[]> {
|
||||
|
||||
return Object
|
||||
.values(data)
|
||||
.reduce((allCharts, repoCharts) => allCharts.concat(Object.values(repoCharts)), [])
|
||||
.reduce((allCharts, repoCharts) => allCharts.concat(Object.values(repoCharts)), new Array<RawHelmChart[]>())
|
||||
.map(([chart]) => HelmChart.create(chart, { onError: "log" }))
|
||||
.filter(Boolean);
|
||||
.filter(isDefined);
|
||||
}
|
||||
|
||||
export interface GetChartDetailsOptions {
|
||||
@ -51,7 +51,7 @@ export async function getChartDetails(repo: string, name: string, { version, req
|
||||
const path = endpoint({ repo, name });
|
||||
|
||||
const { readme, ...data } = await apiBase.get<IHelmChartDetails>(`${path}?${stringify({ version })}`, undefined, reqInit);
|
||||
const versions = data.versions.map(version => HelmChart.create(version, { onError: "log" })).filter(Boolean);
|
||||
const versions = data.versions.map(version => HelmChart.create(version, { onError: "log" })).filter(isDefined);
|
||||
|
||||
return {
|
||||
readme,
|
||||
@ -242,7 +242,7 @@ export interface RawHelmChartDependency {
|
||||
export type HelmChartDependency = Required<Omit<RawHelmChartDependency, "condition">>
|
||||
& Pick<RawHelmChartDependency, "condition">;
|
||||
|
||||
export interface HelmChart {
|
||||
export interface HelmChartData {
|
||||
apiVersion: string;
|
||||
name: string;
|
||||
version: string;
|
||||
@ -266,8 +266,30 @@ export interface HelmChart {
|
||||
tillerVersion?: string;
|
||||
}
|
||||
|
||||
export class HelmChart {
|
||||
private constructor(value: HelmChart) {
|
||||
export class HelmChart implements HelmChartData {
|
||||
apiVersion: string;
|
||||
name: string;
|
||||
version: string;
|
||||
repo: string;
|
||||
created: string;
|
||||
description: string;
|
||||
keywords: string[];
|
||||
sources: string[];
|
||||
urls: string[];
|
||||
annotations: Record<string, string>;
|
||||
dependencies: HelmChartDependency[];
|
||||
maintainers: HelmChartMaintainer[];
|
||||
deprecated: boolean;
|
||||
kubeVersion?: string;
|
||||
digest?: string;
|
||||
home?: string;
|
||||
engine?: string;
|
||||
icon?: string;
|
||||
appVersion?: string;
|
||||
type?: string;
|
||||
tillerVersion?: string;
|
||||
|
||||
private constructor(value: HelmChart | HelmChartData) {
|
||||
this.apiVersion = value.apiVersion;
|
||||
this.name = value.name;
|
||||
this.version = value.version;
|
||||
@ -294,25 +316,25 @@ export class HelmChart {
|
||||
}
|
||||
|
||||
static create(data: RawHelmChart, { onError = "throw" }: HelmChartCreateOpts = {}): HelmChart | undefined {
|
||||
const { value, error } = helmChartValidator.validate(data, {
|
||||
const result = helmChartValidator.validate(data, {
|
||||
abortEarly: false,
|
||||
});
|
||||
|
||||
if (!error) {
|
||||
return new HelmChart(value);
|
||||
if (!result.error) {
|
||||
return new HelmChart(result.value);
|
||||
}
|
||||
|
||||
const [actualErrors, unknownDetails] = bifurcateArray(error.details, ({ type }) => type === "object.unknown");
|
||||
const [actualErrors, unknownDetails] = bifurcateArray(result.error.details, ({ type }) => type === "object.unknown");
|
||||
|
||||
if (unknownDetails.length > 0) {
|
||||
console.warn("HelmChart data has unexpected fields", { original: data, unknownFields: unknownDetails.flatMap(d => d.path) });
|
||||
}
|
||||
|
||||
if (actualErrors.length === 0) {
|
||||
return new HelmChart(value);
|
||||
return new HelmChart(result.value as unknown as HelmChartData);
|
||||
}
|
||||
|
||||
const validationError = new Joi.ValidationError(actualErrors.map(er => er.message).join(". "), actualErrors, error._original);
|
||||
const validationError = new Joi.ValidationError(actualErrors.map(er => er.message).join(". "), actualErrors, result.error._original);
|
||||
|
||||
if (onError === "throw") {
|
||||
throw validationError;
|
||||
@ -347,7 +369,7 @@ export class HelmChart {
|
||||
return this.icon;
|
||||
}
|
||||
|
||||
getHome(): string {
|
||||
getHome(): string | undefined {
|
||||
return this.home;
|
||||
}
|
||||
|
||||
|
||||
@ -9,12 +9,12 @@ import capitalize from "lodash/capitalize";
|
||||
import { apiBase } from "../index";
|
||||
import { helmChartStore } from "../../../renderer/components/+helm-charts/helm-chart.store";
|
||||
import type { ItemObject } from "../../item.store";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { JsonApiData } from "../json-api";
|
||||
import { buildURLPositional } from "../../utils/buildUrl";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
|
||||
interface IReleasePayload {
|
||||
export interface HelmReleaseDetails {
|
||||
resources: KubeJsonApiData[];
|
||||
name: string;
|
||||
namespace: string;
|
||||
version: string;
|
||||
@ -30,15 +30,7 @@ interface IReleasePayload {
|
||||
};
|
||||
}
|
||||
|
||||
interface IReleaseRawDetails extends IReleasePayload {
|
||||
resources: KubeJsonApiData[];
|
||||
}
|
||||
|
||||
export interface IReleaseDetails extends IReleasePayload {
|
||||
resources: KubeObject[];
|
||||
}
|
||||
|
||||
export interface IReleaseCreatePayload {
|
||||
export interface HelmReleaseCreatePayload {
|
||||
name?: string;
|
||||
repo: string;
|
||||
chart: string;
|
||||
@ -47,19 +39,19 @@ export interface IReleaseCreatePayload {
|
||||
values: string;
|
||||
}
|
||||
|
||||
export interface IReleaseUpdatePayload {
|
||||
export interface HelmReleaseUpdatePayload {
|
||||
repo: string;
|
||||
chart: string;
|
||||
version: string;
|
||||
values: string;
|
||||
}
|
||||
|
||||
export interface IReleaseUpdateDetails {
|
||||
export interface HelmReleaseUpdateDetails {
|
||||
log: string;
|
||||
release: IReleaseDetails;
|
||||
release: HelmReleaseDetails;
|
||||
}
|
||||
|
||||
export interface IReleaseRevision {
|
||||
export interface HelmReleaseRevision {
|
||||
revision: number;
|
||||
updated: string;
|
||||
status: string;
|
||||
@ -85,18 +77,13 @@ export async function listReleases(namespace?: string): Promise<HelmRelease[]> {
|
||||
return releases.map(toHelmRelease);
|
||||
}
|
||||
|
||||
export async function getRelease(name: string, namespace: string): Promise<IReleaseDetails> {
|
||||
export async function getRelease(name: string, namespace: string): Promise<HelmReleaseDetails> {
|
||||
const path = endpoint({ name, namespace });
|
||||
const { resources: rawResources, ...details } = await apiBase.get<IReleaseRawDetails>(path);
|
||||
const resources = rawResources.map(KubeObject.create);
|
||||
|
||||
return {
|
||||
...details,
|
||||
resources,
|
||||
};
|
||||
return apiBase.get(path);
|
||||
}
|
||||
|
||||
export async function createRelease(payload: IReleaseCreatePayload): Promise<IReleaseUpdateDetails> {
|
||||
export async function createRelease(payload: HelmReleaseCreatePayload): Promise<HelmReleaseUpdateDetails> {
|
||||
const { repo, chart: rawChart, values: rawValues, ...data } = payload;
|
||||
const chart = `${repo}/${rawChart}`;
|
||||
const values = yaml.load(rawValues);
|
||||
@ -110,7 +97,7 @@ export async function createRelease(payload: IReleaseCreatePayload): Promise<IRe
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateRelease(name: string, namespace: string, payload: IReleaseUpdatePayload): Promise<IReleaseUpdateDetails> {
|
||||
export async function updateRelease(name: string, namespace: string, payload: HelmReleaseUpdatePayload): Promise<HelmReleaseUpdateDetails> {
|
||||
const { repo, chart: rawChart, values: rawValues, ...data } = payload;
|
||||
const chart = `${repo}/${rawChart}`;
|
||||
const values = yaml.load(rawValues);
|
||||
@ -137,7 +124,7 @@ export async function getReleaseValues(name: string, namespace: string, all?: bo
|
||||
return apiBase.get<string>(path);
|
||||
}
|
||||
|
||||
export async function getReleaseHistory(name: string, namespace: string): Promise<IReleaseRevision[]> {
|
||||
export async function getReleaseHistory(name: string, namespace: string): Promise<HelmReleaseRevision[]> {
|
||||
const route = "history";
|
||||
const path = endpoint({ name, namespace, route });
|
||||
|
||||
|
||||
@ -3,93 +3,124 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { BaseKubeObjectCondition, LabelSelector } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { DerivedKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import type { OptionVarient } from "../../utils";
|
||||
|
||||
export enum HpaMetricType {
|
||||
Resource = "Resource",
|
||||
Pods = "Pods",
|
||||
Object = "Object",
|
||||
External = "External",
|
||||
ContainerResource = "ContainerResource",
|
||||
}
|
||||
|
||||
export type IHpaMetricData<T = any> = T & {
|
||||
target?: {
|
||||
kind: string;
|
||||
name: string;
|
||||
apiVersion: string;
|
||||
};
|
||||
name?: string;
|
||||
metricName?: string;
|
||||
currentAverageUtilization?: number;
|
||||
currentAverageValue?: string;
|
||||
export interface HorizontalPodAutoscalerMetricTarget {
|
||||
kind: string;
|
||||
name: string;
|
||||
apiVersion: string;
|
||||
}
|
||||
|
||||
export interface ContainerResourceMetricSource {
|
||||
container: string;
|
||||
name: 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 interface ExternalMetricSource {
|
||||
metricName: string;
|
||||
metricSelector?: LabelSelector;
|
||||
targetAverageValue?: string;
|
||||
targetValue?: string;
|
||||
}
|
||||
|
||||
export class HorizontalPodAutoscaler extends KubeObject {
|
||||
static kind = "HorizontalPodAutoscaler";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/autoscaling/v2beta1/horizontalpodautoscalers";
|
||||
export interface ObjectMetricSource {
|
||||
averageValue?: string;
|
||||
metricName: string;
|
||||
selector?: LabelSelector;
|
||||
target: CrossVersionObjectReference;
|
||||
targetValue: string;
|
||||
}
|
||||
|
||||
export interface PodsMetricSource {
|
||||
metricName: string;
|
||||
selector?: LabelSelector;
|
||||
targetAverageValue: string;
|
||||
}
|
||||
|
||||
export interface ResourceMetricSource {
|
||||
name: string;
|
||||
targetAverageUtilization?: number;
|
||||
targetAverageValue?: string;
|
||||
}
|
||||
|
||||
export interface BaseHorizontalPodAutoscalerMetricSpec {
|
||||
resource: ResourceMetricSource;
|
||||
object: ObjectMetricSource;
|
||||
external: ExternalMetricSource;
|
||||
pods: PodsMetricSource;
|
||||
containerResource: ContainerResourceMetricSource;
|
||||
}
|
||||
|
||||
export type HorizontalPodAutoscalerMetricSpec =
|
||||
| OptionVarient<HpaMetricType.Resource, BaseHorizontalPodAutoscalerMetricSpec, "resource">
|
||||
| OptionVarient<HpaMetricType.External, BaseHorizontalPodAutoscalerMetricSpec, "external">
|
||||
| OptionVarient<HpaMetricType.Object, BaseHorizontalPodAutoscalerMetricSpec, "object">
|
||||
| OptionVarient<HpaMetricType.Pods, BaseHorizontalPodAutoscalerMetricSpec, "pods">
|
||||
| OptionVarient<HpaMetricType.ContainerResource, BaseHorizontalPodAutoscalerMetricSpec, "containerResource">;
|
||||
|
||||
export interface CrossVersionObjectReference {
|
||||
kind: string;
|
||||
name: string;
|
||||
apiVersion: string;
|
||||
}
|
||||
|
||||
export interface HorizontalPodAutoscalerSpec {
|
||||
scaleTargetRef: CrossVersionObjectReference;
|
||||
minReplicas?: number;
|
||||
maxReplicas: number;
|
||||
metrics?: HorizontalPodAutoscalerMetricSpec[];
|
||||
}
|
||||
|
||||
export interface HorizontalPodAutoscalerStatus {
|
||||
conditions?: BaseKubeObjectCondition[];
|
||||
currentReplicas: number;
|
||||
desiredReplicas: number;
|
||||
currentMetrics: HorizontalPodAutoscalerMetricSpec[];
|
||||
}
|
||||
|
||||
interface MetricCurrentTarget {
|
||||
current?: string | undefined;
|
||||
target?: string | undefined;
|
||||
}
|
||||
|
||||
export class HorizontalPodAutoscaler extends KubeObject<HorizontalPodAutoscalerStatus, HorizontalPodAutoscalerSpec, "namespace-scoped"> {
|
||||
static readonly kind = "HorizontalPodAutoscaler";
|
||||
static readonly namespaced = true;
|
||||
static readonly apiBase = "/apis/autoscaling/v2beta1/horizontalpodautoscalers";
|
||||
|
||||
getMaxPods() {
|
||||
return this.spec.maxReplicas || 0;
|
||||
return this.spec.maxReplicas ?? 0;
|
||||
}
|
||||
|
||||
getMinPods() {
|
||||
return this.spec.minReplicas || 0;
|
||||
return this.spec.minReplicas ?? 0;
|
||||
}
|
||||
|
||||
getReplicas() {
|
||||
return this.status.currentReplicas;
|
||||
return this.status?.currentReplicas ?? 0;
|
||||
}
|
||||
|
||||
getReadyConditions() {
|
||||
return this.getConditions().filter(({ isReady }) => isReady);
|
||||
}
|
||||
|
||||
getConditions() {
|
||||
if (!this.status.conditions) return [];
|
||||
|
||||
return this.status.conditions.map(condition => {
|
||||
return this.status?.conditions?.map(condition => {
|
||||
const { message, reason, lastTransitionTime, status } = condition;
|
||||
|
||||
return {
|
||||
@ -97,65 +128,139 @@ export class HorizontalPodAutoscaler extends KubeObject {
|
||||
isReady: status === "True",
|
||||
tooltip: `${message || reason} (${lastTransitionTime})`,
|
||||
};
|
||||
});
|
||||
}) ?? [];
|
||||
}
|
||||
|
||||
getMetrics() {
|
||||
return this.spec.metrics || [];
|
||||
return this.spec.metrics ?? [];
|
||||
}
|
||||
|
||||
getCurrentMetrics() {
|
||||
return this.status.currentMetrics || [];
|
||||
return this.status?.currentMetrics ?? [];
|
||||
}
|
||||
|
||||
protected getMetricName(metric: IHpaMetric): string {
|
||||
const { type, resource, pods, object, external } = metric;
|
||||
|
||||
switch (type) {
|
||||
protected getMetricName(metric: HorizontalPodAutoscalerMetricSpec): string {
|
||||
switch (metric.type) {
|
||||
case HpaMetricType.Resource:
|
||||
return resource.name;
|
||||
return metric.resource.name;
|
||||
case HpaMetricType.Pods:
|
||||
return pods.metricName;
|
||||
return metric.pods.metricName;
|
||||
case HpaMetricType.Object:
|
||||
return object.metricName;
|
||||
return metric.object.metricName;
|
||||
case HpaMetricType.External:
|
||||
return external.metricName;
|
||||
return metric.external.metricName;
|
||||
case HpaMetricType.ContainerResource:
|
||||
return metric.containerResource.name;
|
||||
default:
|
||||
return `<unknown metric type: ${(metric as HorizontalPodAutoscalerMetricSpec).type}>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 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";
|
||||
protected getResourceMetricValue(currentMetric: ResourceMetricSource | undefined, targetMetric: ResourceMetricSource): MetricCurrentTarget {
|
||||
return {
|
||||
current: (
|
||||
currentMetric?.targetAverageUtilization
|
||||
? `${currentMetric.targetAverageUtilization}%`
|
||||
: currentMetric?.targetAverageValue
|
||||
),
|
||||
target: (
|
||||
targetMetric?.targetAverageUtilization
|
||||
? `${targetMetric.targetAverageUtilization}%`
|
||||
: targetMetric?.targetAverageValue
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (current) {
|
||||
currentValue = current.currentAverageUtilization || current.currentAverageValue || current.currentValue;
|
||||
if (current.currentAverageUtilization) currentValue += "%";
|
||||
protected getPodsMetricValue(currentMetric: PodsMetricSource | undefined, targetMetric: PodsMetricSource): MetricCurrentTarget {
|
||||
return {
|
||||
current: currentMetric?.targetAverageValue,
|
||||
target: targetMetric?.targetAverageValue,
|
||||
};
|
||||
}
|
||||
|
||||
protected getObjectMetricValue(currentMetric: ObjectMetricSource | undefined, targetMetric: ObjectMetricSource): MetricCurrentTarget {
|
||||
return {
|
||||
current: (
|
||||
currentMetric?.targetValue
|
||||
?? currentMetric?.averageValue
|
||||
),
|
||||
target: (
|
||||
targetMetric?.targetValue
|
||||
?? targetMetric?.averageValue
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
protected getExternalMetricValue(currentMetric: ExternalMetricSource | undefined, targetMetric: ExternalMetricSource): MetricCurrentTarget {
|
||||
return {
|
||||
current: (
|
||||
currentMetric?.targetValue
|
||||
?? currentMetric?.targetAverageValue
|
||||
),
|
||||
target: (
|
||||
targetMetric?.targetValue
|
||||
?? targetMetric?.targetAverageValue
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
protected getContainerResourceMetricValue(currentMetric: ContainerResourceMetricSource | undefined, targetMetric: ContainerResourceMetricSource): MetricCurrentTarget {
|
||||
return {
|
||||
current: (
|
||||
currentMetric?.targetAverageUtilization
|
||||
? `${currentMetric.targetAverageUtilization}%`
|
||||
: currentMetric?.targetAverageValue
|
||||
),
|
||||
target: (
|
||||
targetMetric?.targetAverageUtilization
|
||||
? `${targetMetric.targetAverageUtilization}%`
|
||||
: targetMetric?.targetAverageValue
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
protected getMetricCurrentTarget(metric: HorizontalPodAutoscalerMetricSpec): MetricCurrentTarget {
|
||||
const currentMetric = this.getMetrics()
|
||||
.find(m => (
|
||||
m.type === metric.type
|
||||
&& this.getMetricName(m) === this.getMetricName(metric)
|
||||
));
|
||||
|
||||
switch (metric.type) {
|
||||
case HpaMetricType.Resource:
|
||||
return this.getResourceMetricValue(currentMetric?.resource, metric.resource);
|
||||
case HpaMetricType.Pods:
|
||||
return this.getPodsMetricValue(currentMetric?.pods, metric.pods);
|
||||
case HpaMetricType.Object:
|
||||
return this.getObjectMetricValue(currentMetric?.object, metric.object);
|
||||
case HpaMetricType.External:
|
||||
return this.getExternalMetricValue(currentMetric?.external, metric.external);
|
||||
case HpaMetricType.ContainerResource:
|
||||
return this.getContainerResourceMetricValue(currentMetric?.containerResource, metric.containerResource);
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
if (target) {
|
||||
targetValue = target.targetAverageUtilization || target.targetAverageValue || target.targetValue;
|
||||
if (target.targetAverageUtilization) targetValue += "%";
|
||||
}
|
||||
getMetricValues(metric: HorizontalPodAutoscalerMetricSpec): string {
|
||||
const {
|
||||
current = "unknown",
|
||||
target = "unknown",
|
||||
} = this.getMetricCurrentTarget(metric);
|
||||
|
||||
return `${currentValue} / ${targetValue}`;
|
||||
return `${current} / ${target}`;
|
||||
}
|
||||
}
|
||||
|
||||
let hpaApi: KubeApi<HorizontalPodAutoscaler>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
hpaApi = new KubeApi<HorizontalPodAutoscaler>({
|
||||
objectConstructor: HorizontalPodAutoscaler,
|
||||
});
|
||||
export class HorizontalPodAutoscalerApi extends KubeApi<HorizontalPodAutoscaler> {
|
||||
constructor(opts?: DerivedKubeApiOptions) {
|
||||
super({
|
||||
objectConstructor: HorizontalPodAutoscaler,
|
||||
...opts ?? {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
hpaApi,
|
||||
};
|
||||
export const horizontalPodAutoscalerApi = isClusterPageContext()
|
||||
? new HorizontalPodAutoscalerApi()
|
||||
: undefined as never;
|
||||
|
||||
@ -5,18 +5,27 @@
|
||||
|
||||
import type { TypedLocalObjectReference } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { autoBind, hasTypedProperty, isString, iter } from "../../utils";
|
||||
import type { IMetrics } from "./metrics.api";
|
||||
import { hasTypedProperty, isString, iter } from "../../utils";
|
||||
import type { MetricData } from "./metrics.api";
|
||||
import { metricsApi } from "./metrics.api";
|
||||
import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import type { RequireExactlyOne } from "type-fest";
|
||||
|
||||
export class IngressApi extends KubeApi<Ingress> {
|
||||
constructor(opts: DerivedKubeApiOptions & IgnoredKubeApiOptions = {}) {
|
||||
super({
|
||||
...opts,
|
||||
objectConstructor: Ingress,
|
||||
// Add fallback for Kubernetes <1.19
|
||||
checkPreferredVersion: true,
|
||||
fallbackApiBases: ["/apis/extensions/v1beta1/ingresses"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getMetricsForIngress(ingress: string, namespace: string): Promise<IIngressMetrics> {
|
||||
export function getMetricsForIngress(ingress: string, namespace: string): Promise<IngressMetricData> {
|
||||
const opts = { category: "ingress", ingress, namespace };
|
||||
|
||||
return metricsApi.getMetrics({
|
||||
@ -29,12 +38,11 @@ export function getMetricsForIngress(ingress: string, namespace: string): Promis
|
||||
});
|
||||
}
|
||||
|
||||
export interface IIngressMetrics<T = IMetrics> {
|
||||
[metric: string]: T;
|
||||
bytesSentSuccess: T;
|
||||
bytesSentFailure: T;
|
||||
requestDurationSeconds: T;
|
||||
responseDurationSeconds: T;
|
||||
export interface IngressMetricData extends Partial<Record<string, MetricData>> {
|
||||
bytesSentSuccess: MetricData;
|
||||
bytesSentFailure: MetricData;
|
||||
requestDurationSeconds: MetricData;
|
||||
responseDurationSeconds: MetricData;
|
||||
}
|
||||
|
||||
export interface ILoadBalancerIngress {
|
||||
@ -73,7 +81,11 @@ function isExtensionsBackend(backend: IngressBackend): backend is ExtensionsBack
|
||||
* Format an ingress backend into the name of the service and port
|
||||
* @param backend The ingress target
|
||||
*/
|
||||
export function getBackendServiceNamePort(backend: IngressBackend): string {
|
||||
export function getBackendServiceNamePort(backend: IngressBackend | undefined): string {
|
||||
if (!backend) {
|
||||
return "<unknown>";
|
||||
}
|
||||
|
||||
if (isExtensionsBackend(backend)) {
|
||||
return `${backend.serviceName}:${backend.servicePort}`;
|
||||
}
|
||||
@ -87,60 +99,52 @@ export function getBackendServiceNamePort(backend: IngressBackend): string {
|
||||
return "<unknown>";
|
||||
}
|
||||
|
||||
export interface HTTPIngressPath {
|
||||
pathType: "Exact" | "Prefix" | "ImplementationSpecific";
|
||||
path?: string;
|
||||
backend?: IngressBackend;
|
||||
}
|
||||
|
||||
export interface HTTPIngressRuleValue {
|
||||
paths: HTTPIngressPath[];
|
||||
}
|
||||
|
||||
export interface IngressRule {
|
||||
host?: string;
|
||||
http?: {
|
||||
paths: {
|
||||
path?: string;
|
||||
backend: IngressBackend;
|
||||
}[];
|
||||
};
|
||||
http?: HTTPIngressRuleValue;
|
||||
}
|
||||
|
||||
export interface Ingress {
|
||||
spec?: {
|
||||
tls?: {
|
||||
secretName: string;
|
||||
}[];
|
||||
rules?: IngressRule[];
|
||||
// extensions/v1beta1
|
||||
backend?: ExtensionsBackend;
|
||||
/**
|
||||
* The default backend which is exactly on of:
|
||||
* - service
|
||||
* - resource
|
||||
*/
|
||||
defaultBackend?: RequireExactlyOne<NetworkingBackend & {
|
||||
resource: {
|
||||
apiGroup: string;
|
||||
kind: string;
|
||||
name: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
status: {
|
||||
loadBalancer: {
|
||||
ingress: ILoadBalancerIngress[];
|
||||
export interface IngressSpec {
|
||||
tls: {
|
||||
secretName: string;
|
||||
}[];
|
||||
rules?: IngressRule[];
|
||||
// extensions/v1beta1
|
||||
backend?: ExtensionsBackend;
|
||||
/**
|
||||
* The default backend which is exactly on of:
|
||||
* - service
|
||||
* - resource
|
||||
*/
|
||||
defaultBackend?: RequireExactlyOne<NetworkingBackend & {
|
||||
resource: {
|
||||
apiGroup: string;
|
||||
kind: string;
|
||||
name: string;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface IngressStatus {
|
||||
loadBalancer: {
|
||||
ingress: ILoadBalancerIngress[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ComputedIngressRoute {
|
||||
displayAsLink: boolean;
|
||||
pathname: string;
|
||||
url: string;
|
||||
service: string;
|
||||
}
|
||||
|
||||
export class Ingress extends KubeObject {
|
||||
static kind = "Ingress";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/networking.k8s.io/v1/ingresses";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
export class Ingress extends KubeObject<IngressStatus, IngressSpec, "namespace-scoped"> {
|
||||
static readonly kind = "Ingress";
|
||||
static readonly namespaced = true;
|
||||
static readonly apiBase = "/apis/networking.k8s.io/v1/ingresses";
|
||||
|
||||
getRules() {
|
||||
return this.spec.rules ?? [];
|
||||
@ -150,12 +154,16 @@ export class Ingress extends KubeObject {
|
||||
return computeRouteDeclarations(this).map(({ url, service }) => `${url} ⇢ ${service}`);
|
||||
}
|
||||
|
||||
getServiceNamePort(): ExtensionsBackend {
|
||||
getServiceNamePort(): ExtensionsBackend | undefined {
|
||||
const { spec: { backend, defaultBackend } = {}} = this;
|
||||
|
||||
const serviceName = defaultBackend?.service?.name ?? backend?.serviceName;
|
||||
const servicePort = defaultBackend?.service?.port.number ?? defaultBackend?.service?.port.name ?? backend?.servicePort;
|
||||
|
||||
if (!serviceName || !servicePort) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
serviceName,
|
||||
servicePort,
|
||||
@ -170,14 +178,14 @@ export class Ingress extends KubeObject {
|
||||
|
||||
getPorts() {
|
||||
const ports: number[] = [];
|
||||
const { spec: { tls, rules, backend, defaultBackend }} = this;
|
||||
const { spec: { tls, rules = [], backend, defaultBackend }} = this;
|
||||
const httpPort = 80;
|
||||
const tlsPort = 443;
|
||||
// Note: not using the port name (string)
|
||||
const servicePort = defaultBackend?.service?.port.number ?? backend?.servicePort;
|
||||
|
||||
if (rules && rules.length > 0) {
|
||||
if (rules.some(rule => Object.prototype.hasOwnProperty.call(rule, "http"))) {
|
||||
if (rules.length > 0) {
|
||||
if (rules.some(rule => rule.http)) {
|
||||
ports.push(httpPort);
|
||||
}
|
||||
} else if (servicePort !== undefined) {
|
||||
@ -192,14 +200,19 @@ export class Ingress extends KubeObject {
|
||||
}
|
||||
|
||||
getLoadBalancers() {
|
||||
const { status: { loadBalancer = { ingress: [] }}} = this;
|
||||
|
||||
return (loadBalancer.ingress ?? []).map(address => (
|
||||
return this.status?.loadBalancer.ingress.map(address => (
|
||||
address.hostname || address.ip
|
||||
));
|
||||
)) ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
export interface ComputedIngressRoute {
|
||||
displayAsLink: boolean;
|
||||
pathname: string;
|
||||
url: string;
|
||||
service: string;
|
||||
}
|
||||
|
||||
export function computeRuleDeclarations(ingress: Ingress, rule: IngressRule): ComputedIngressRoute[] {
|
||||
const { host = "*", http: { paths } = { paths: [] }} = rule;
|
||||
const protocol = (ingress.spec?.tls?.length ?? 0) === 0
|
||||
@ -218,17 +231,6 @@ export function computeRouteDeclarations(ingress: Ingress): ComputedIngressRoute
|
||||
return ingress.getRules().flatMap(rule => computeRuleDeclarations(ingress, rule));
|
||||
}
|
||||
|
||||
let ingressApi: IngressApi;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
ingressApi = new IngressApi({
|
||||
objectConstructor: Ingress,
|
||||
// Add fallback for Kubernetes <1.19
|
||||
checkPreferredVersion: true,
|
||||
fallbackApiBases: ["/apis/extensions/v1beta1/ingresses"],
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
ingressApi,
|
||||
};
|
||||
export const ingressApi = isClusterPageContext()
|
||||
? new IngressApi()
|
||||
: undefined as never;
|
||||
|
||||
@ -3,88 +3,77 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import get from "lodash/get";
|
||||
import { autoBind } from "../../utils";
|
||||
import type { IAffinity } from "../workload-kube-object";
|
||||
import { WorkloadKubeObject } from "../workload-kube-object";
|
||||
import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { metricsApi } from "./metrics.api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import type { IPodContainer, IPodMetrics } from "./pods.api";
|
||||
import type { IPodContainer, PodMetricData, PodSpec } from "./pods.api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import type { LabelSelector } from "../kube-object";
|
||||
import type { KubeObjectStatus, LabelSelector } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
|
||||
export class Job extends WorkloadKubeObject {
|
||||
static kind = "Job";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/batch/v1/jobs";
|
||||
export interface JobSpec {
|
||||
parallelism?: number;
|
||||
completions?: number;
|
||||
backoffLimit?: number;
|
||||
selector?: LabelSelector;
|
||||
template: {
|
||||
metadata: {
|
||||
creationTimestamp?: string;
|
||||
labels?: Partial<Record<string, string>>;
|
||||
annotations?: Partial<Record<string, string>>;
|
||||
};
|
||||
spec: PodSpec;
|
||||
};
|
||||
containers?: IPodContainer[];
|
||||
restartPolicy?: string;
|
||||
terminationGracePeriodSeconds?: number;
|
||||
dnsPolicy?: string;
|
||||
serviceAccountName?: string;
|
||||
serviceAccount?: string;
|
||||
schedulerName?: string;
|
||||
}
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
export interface JobStatus extends KubeObjectStatus {
|
||||
startTime: string;
|
||||
completionTime: string;
|
||||
succeeded: number;
|
||||
}
|
||||
|
||||
export class Job extends KubeObject<JobStatus, JobSpec, "namespace-scoped"> {
|
||||
static readonly kind = "Job";
|
||||
static readonly namespaced = true;
|
||||
static readonly apiBase = "/apis/batch/v1/jobs";
|
||||
|
||||
getSelectors(): string[] {
|
||||
return KubeObject.stringifyLabels(this.spec.selector?.matchLabels);
|
||||
}
|
||||
|
||||
declare spec: {
|
||||
parallelism?: number;
|
||||
completions?: number;
|
||||
backoffLimit?: number;
|
||||
selector?: LabelSelector;
|
||||
template: {
|
||||
metadata: {
|
||||
creationTimestamp?: string;
|
||||
labels?: {
|
||||
[name: string]: string;
|
||||
};
|
||||
annotations?: {
|
||||
[name: string]: string;
|
||||
};
|
||||
};
|
||||
spec: {
|
||||
containers: IPodContainer[];
|
||||
restartPolicy: string;
|
||||
terminationGracePeriodSeconds: number;
|
||||
dnsPolicy: string;
|
||||
hostPID: boolean;
|
||||
affinity?: IAffinity;
|
||||
nodeSelector?: {
|
||||
[selector: string]: string;
|
||||
};
|
||||
tolerations?: {
|
||||
key: string;
|
||||
operator: string;
|
||||
effect: string;
|
||||
tolerationSeconds: number;
|
||||
}[];
|
||||
schedulerName: string;
|
||||
};
|
||||
};
|
||||
containers?: IPodContainer[];
|
||||
restartPolicy?: string;
|
||||
terminationGracePeriodSeconds?: number;
|
||||
dnsPolicy?: string;
|
||||
serviceAccountName?: string;
|
||||
serviceAccount?: string;
|
||||
schedulerName?: string;
|
||||
};
|
||||
declare status: {
|
||||
conditions: {
|
||||
type: string;
|
||||
status: string;
|
||||
lastProbeTime: string;
|
||||
lastTransitionTime: string;
|
||||
message?: string;
|
||||
}[];
|
||||
startTime: string;
|
||||
completionTime: string;
|
||||
succeeded: number;
|
||||
};
|
||||
getNodeSelectors(): string[] {
|
||||
return KubeObject.stringifyLabels(this.spec.template.spec.nodeSelector);
|
||||
}
|
||||
|
||||
getTemplateLabels(): string[] {
|
||||
return KubeObject.stringifyLabels(this.spec.template.metadata.labels);
|
||||
}
|
||||
|
||||
getTolerations() {
|
||||
return this.spec.template.spec.tolerations ?? [];
|
||||
}
|
||||
|
||||
getAffinity() {
|
||||
return this.spec.template.spec.affinity;
|
||||
}
|
||||
|
||||
getAffinityNumber() {
|
||||
return Object.keys(this.getAffinity() ?? {}).length;
|
||||
}
|
||||
|
||||
getDesiredCompletions() {
|
||||
return this.spec.completions || 0;
|
||||
return this.spec.completions ?? 0;
|
||||
}
|
||||
|
||||
getCompletions() {
|
||||
return this.status.succeeded || 0;
|
||||
return this.status?.succeeded ?? 0;
|
||||
}
|
||||
|
||||
getParallelism() {
|
||||
@ -94,20 +83,24 @@ export class Job extends WorkloadKubeObject {
|
||||
getCondition() {
|
||||
// Type of Job condition could be only Complete or Failed
|
||||
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.14/#jobcondition-v1-batch
|
||||
return this.status.conditions?.find(({ status }) => status === "True");
|
||||
return this.status?.conditions?.find(({ status }) => status === "True");
|
||||
}
|
||||
|
||||
getImages() {
|
||||
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []);
|
||||
|
||||
return [...containers].map(container => container.image);
|
||||
return this.spec.template.spec.containers?.map(container => container.image) ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
export class JobApi extends KubeApi<Job> {
|
||||
constructor(opts: DerivedKubeApiOptions & IgnoredKubeApiOptions = {}) {
|
||||
super({
|
||||
...opts,
|
||||
objectConstructor: Job,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getMetricsForJobs(jobs: Job[], namespace: string, selector = ""): Promise<IPodMetrics> {
|
||||
export function getMetricsForJobs(jobs: Job[], namespace: string, selector = ""): Promise<PodMetricData> {
|
||||
const podSelector = jobs.map(job => `${job.getName()}-[[:alnum:]]{5}`).join("|");
|
||||
const opts = { category: "pods", pods: podSelector, namespace, selector };
|
||||
|
||||
@ -124,14 +117,6 @@ export function getMetricsForJobs(jobs: Job[], namespace: string, selector = "")
|
||||
});
|
||||
}
|
||||
|
||||
let jobApi: JobApi;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
jobApi = new JobApi({
|
||||
objectConstructor: Job,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
jobApi,
|
||||
};
|
||||
export const jobApi = isClusterPageContext()
|
||||
? new JobApi()
|
||||
: undefined as never;
|
||||
|
||||
@ -4,9 +4,8 @@
|
||||
*/
|
||||
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { DerivedKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { autoBind } from "../../utils";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export enum LimitType {
|
||||
@ -36,21 +35,14 @@ export interface LimitRangeItem extends LimitRangeParts {
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface LimitRange {
|
||||
spec: {
|
||||
limits: LimitRangeItem[];
|
||||
};
|
||||
export interface LimitRangeSpec {
|
||||
limits: LimitRangeItem[];
|
||||
}
|
||||
|
||||
export class LimitRange extends KubeObject {
|
||||
static kind = "LimitRange";
|
||||
static namespaced = true;
|
||||
static apiBase = "/api/v1/limitranges";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
export class LimitRange extends KubeObject<void, LimitRangeSpec, "namespace-scoped"> {
|
||||
static readonly kind = "LimitRange";
|
||||
static readonly namespaced = true;
|
||||
static readonly apiBase = "/api/v1/limitranges";
|
||||
|
||||
getContainerLimits() {
|
||||
return this.spec.limits.filter(limit => limit.type === LimitType.CONTAINER);
|
||||
@ -65,14 +57,15 @@ export class LimitRange extends KubeObject {
|
||||
}
|
||||
}
|
||||
|
||||
let limitRangeApi: KubeApi<LimitRange>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
limitRangeApi = new KubeApi<LimitRange>({
|
||||
objectConstructor: LimitRange,
|
||||
});
|
||||
export class LimitRangeApi extends KubeApi<LimitRange> {
|
||||
constructor(opts?: DerivedKubeApiOptions) {
|
||||
super({
|
||||
objectConstructor: LimitRange,
|
||||
...opts ?? {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
limitRangeApi,
|
||||
};
|
||||
export const limitRangeApi = isClusterPageContext()
|
||||
? new LimitRangeApi()
|
||||
: undefined as never;
|
||||
|
||||
@ -9,18 +9,18 @@ import moment from "moment";
|
||||
import { apiBase } from "../index";
|
||||
import type { IMetricsQuery } from "../../../main/routes/metrics/metrics-query";
|
||||
|
||||
export interface IMetrics {
|
||||
export interface MetricData {
|
||||
status: string;
|
||||
data: {
|
||||
resultType: string;
|
||||
result: IMetricsResult[];
|
||||
result: MetricResult[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface IMetricsResult {
|
||||
export interface MetricResult {
|
||||
metric: {
|
||||
[name: string]: string;
|
||||
instance: string;
|
||||
[name: string]: string | undefined;
|
||||
instance?: string;
|
||||
node?: string;
|
||||
pod?: string;
|
||||
kubernetes?: string;
|
||||
@ -44,7 +44,7 @@ export interface IMetricsReqParams {
|
||||
namespace?: string; // rbac-proxy validation param
|
||||
}
|
||||
|
||||
export interface IResourceMetrics<T extends IMetrics> {
|
||||
export interface IResourceMetrics<T extends MetricData> {
|
||||
[metric: string]: T;
|
||||
cpuUsage: T;
|
||||
memoryUsage: T;
|
||||
@ -56,7 +56,7 @@ export interface IResourceMetrics<T extends IMetrics> {
|
||||
}
|
||||
|
||||
export const metricsApi = {
|
||||
async getMetrics<T = IMetricsQuery>(query: T, reqParams: IMetricsReqParams = {}): Promise<T extends object ? { [K in keyof T]: IMetrics } : IMetrics> {
|
||||
async getMetrics<T = IMetricsQuery>(query: T, reqParams: IMetricsReqParams = {}): Promise<T extends object ? { [K in keyof T]: MetricData } : MetricData> {
|
||||
const { range = 3600, step = 60, namespace } = reqParams;
|
||||
let { start, end } = reqParams;
|
||||
|
||||
@ -82,7 +82,7 @@ export const metricsApi = {
|
||||
},
|
||||
};
|
||||
|
||||
export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics {
|
||||
export function normalizeMetrics(metrics: MetricData | undefined | null, frames = 60): MetricData {
|
||||
if (!metrics?.data?.result) {
|
||||
return {
|
||||
data: {
|
||||
@ -90,7 +90,7 @@ export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics {
|
||||
result: [{
|
||||
metric: {},
|
||||
values: [],
|
||||
} as IMetricsResult],
|
||||
}],
|
||||
},
|
||||
status: "",
|
||||
};
|
||||
@ -131,17 +131,17 @@ export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics {
|
||||
result.push({
|
||||
metric: {},
|
||||
values: [],
|
||||
} as IMetricsResult);
|
||||
} as MetricResult);
|
||||
}
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
export function isMetricsEmpty(metrics: Record<string, IMetrics>) {
|
||||
export function isMetricsEmpty(metrics: Partial<Record<string, MetricData>>) {
|
||||
return Object.values(metrics).every(metric => !metric?.data?.result?.length);
|
||||
}
|
||||
|
||||
export function getItemMetrics(metrics: Record<string, IMetrics>, itemName: string): Record<string, IMetrics> | undefined {
|
||||
export function getItemMetrics(metrics: Partial<Record<string, MetricData>> | null | undefined, itemName: string): Partial<Record<string, MetricData>> | undefined {
|
||||
if (!metrics) {
|
||||
return undefined;
|
||||
}
|
||||
@ -152,23 +152,24 @@ export function getItemMetrics(metrics: Record<string, IMetrics>, itemName: stri
|
||||
if (!metrics[metric]?.data?.result) {
|
||||
continue;
|
||||
}
|
||||
const results = metrics[metric].data.result;
|
||||
const result = results.find(res => Object.values(res.metric)[0] == itemName);
|
||||
const results = metrics[metric]?.data.result;
|
||||
const result = results?.find(res => Object.values(res.metric)[0] == itemName);
|
||||
|
||||
itemMetrics[metric].data.result = result ? [result] : [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
itemMetrics[metric]!.data.result = result ? [result] : [];
|
||||
}
|
||||
|
||||
return itemMetrics;
|
||||
}
|
||||
|
||||
export function getMetricLastPoints(metrics: Record<string, IMetrics>) {
|
||||
export function getMetricLastPoints<T extends Partial<Record<string, MetricData>>>(metrics: T): Record<keyof T, number> {
|
||||
const result: Partial<{ [metric: string]: number }> = {};
|
||||
|
||||
Object.keys(metrics).forEach(metricName => {
|
||||
try {
|
||||
const metric = metrics[metricName];
|
||||
|
||||
if (metric.data.result.length) {
|
||||
if (metric?.data.result.length) {
|
||||
result[metricName] = +metric.data.result[0].values.slice(-1)[0][1];
|
||||
}
|
||||
} catch {
|
||||
@ -178,5 +179,5 @@ export function getMetricLastPoints(metrics: Record<string, IMetrics>) {
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
return result;
|
||||
return result as Record<keyof T, number>;
|
||||
}
|
||||
|
||||
@ -4,33 +4,29 @@
|
||||
*/
|
||||
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeObjectStatus } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { autoBind } from "../../../renderer/utils";
|
||||
import { metricsApi } from "./metrics.api";
|
||||
import type { IPodMetrics } from "./pods.api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import type { PodMetricData } from "./pods.api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export enum NamespaceStatus {
|
||||
export enum NamespaceStatusKind {
|
||||
ACTIVE = "Active",
|
||||
TERMINATING = "Terminating",
|
||||
}
|
||||
|
||||
export interface Namespace {
|
||||
status?: {
|
||||
phase: string;
|
||||
};
|
||||
export interface NamespaceSpec {
|
||||
finalizers?: string[];
|
||||
}
|
||||
|
||||
export class Namespace extends KubeObject {
|
||||
static kind = "Namespace";
|
||||
static namespaced = false;
|
||||
static apiBase = "/api/v1/namespaces";
|
||||
export interface NamespaceStatus extends KubeObjectStatus {
|
||||
phase?: string;
|
||||
}
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
export class Namespace extends KubeObject<NamespaceStatus, NamespaceSpec, "cluster-scoped"> {
|
||||
static readonly kind = "Namespace";
|
||||
static readonly namespaced = false;
|
||||
static readonly apiBase = "/api/v1/namespaces";
|
||||
|
||||
getStatus() {
|
||||
return this.status?.phase ?? "-";
|
||||
@ -40,7 +36,7 @@ export class Namespace extends KubeObject {
|
||||
export class NamespaceApi extends KubeApi<Namespace> {
|
||||
}
|
||||
|
||||
export function getMetricsForNamespace(namespace: string, selector = ""): Promise<IPodMetrics> {
|
||||
export function getMetricsForNamespace(namespace: string, selector = ""): Promise<PodMetricData> {
|
||||
const opts = { category: "pods", pods: ".*", namespace, selector };
|
||||
|
||||
return metricsApi.getMetrics({
|
||||
|
||||
@ -5,9 +5,8 @@
|
||||
|
||||
import type { LabelSelector } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { autoBind } from "../../utils";
|
||||
import type { DerivedKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export interface IPolicyIpBlock {
|
||||
@ -105,15 +104,10 @@ export interface NetworkPolicy {
|
||||
spec: NetworkPolicySpec;
|
||||
}
|
||||
|
||||
export class NetworkPolicy extends KubeObject {
|
||||
static kind = "NetworkPolicy";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/networking.k8s.io/v1/networkpolicies";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
export class NetworkPolicy extends KubeObject<void, NetworkPolicySpec, "namespace-scoped"> {
|
||||
static readonly kind = "NetworkPolicy";
|
||||
static readonly namespaced = true;
|
||||
static readonly apiBase = "/apis/networking.k8s.io/v1/networkpolicies";
|
||||
|
||||
getMatchLabels(): string[] {
|
||||
if (!this.spec.podSelector || !this.spec.podSelector.matchLabels) return [];
|
||||
@ -130,14 +124,15 @@ export class NetworkPolicy extends KubeObject {
|
||||
}
|
||||
}
|
||||
|
||||
let networkPolicyApi: KubeApi<NetworkPolicy>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
networkPolicyApi = new KubeApi<NetworkPolicy>({
|
||||
objectConstructor: NetworkPolicy,
|
||||
});
|
||||
export class NetworkPolicyApi extends KubeApi<NetworkPolicy> {
|
||||
constructor(opts: DerivedKubeApiOptions = {}) {
|
||||
super({
|
||||
objectConstructor: NetworkPolicy,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
networkPolicyApi,
|
||||
};
|
||||
export const networkPolicyApi = isClusterPageContext()
|
||||
? new NetworkPolicyApi()
|
||||
: undefined as never;
|
||||
|
||||
@ -3,18 +3,26 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { BaseKubeObjectCondition } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { autoBind, cpuUnitsToNumber, iter, unitsToBytes } from "../../../renderer/utils";
|
||||
import type { IMetrics } from "./metrics.api";
|
||||
import { cpuUnitsToNumber, unitsToBytes } from "../../../renderer/utils";
|
||||
import type { MetricData } from "./metrics.api";
|
||||
import { metricsApi } from "./metrics.api";
|
||||
import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import { TypedRegEx } from "typed-regex";
|
||||
|
||||
export class NodesApi extends KubeApi<Node> {
|
||||
export class NodeApi extends KubeApi<Node> {
|
||||
constructor(opts: DerivedKubeApiOptions & IgnoredKubeApiOptions = {}) {
|
||||
super({
|
||||
...opts,
|
||||
objectConstructor: Node,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getMetricsForAllNodes(): Promise<INodeMetrics> {
|
||||
export function getMetricsForAllNodes(): Promise<NodeMetricData> {
|
||||
const opts = { category: "nodes" };
|
||||
|
||||
return metricsApi.getMetrics({
|
||||
@ -29,16 +37,15 @@ export function getMetricsForAllNodes(): Promise<INodeMetrics> {
|
||||
});
|
||||
}
|
||||
|
||||
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 NodeMetricData extends Partial<Record<string, MetricData>> {
|
||||
memoryUsage: MetricData;
|
||||
workloadMemoryUsage: MetricData;
|
||||
memoryCapacity: MetricData;
|
||||
memoryAllocatableCapacity: MetricData;
|
||||
cpuUsage: MetricData;
|
||||
cpuCapacity: MetricData;
|
||||
fsUsage: MetricData;
|
||||
fsSize: MetricData;
|
||||
}
|
||||
|
||||
export interface NodeTaint {
|
||||
@ -56,116 +63,127 @@ export function formatNodeTaint(taint: NodeTaint): string {
|
||||
return `${taint.key}:${taint.effect}`;
|
||||
}
|
||||
|
||||
export interface NodeCondition {
|
||||
type: string;
|
||||
status: string;
|
||||
export interface NodeCondition extends BaseKubeObjectCondition {
|
||||
/**
|
||||
* Last time we got an update on a given condition.
|
||||
*/
|
||||
lastHeartbeatTime?: string;
|
||||
lastTransitionTime?: string;
|
||||
reason?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface Node {
|
||||
spec: {
|
||||
podCIDR?: string;
|
||||
podCIDRs?: string[];
|
||||
providerID?: string;
|
||||
/**
|
||||
* @deprecated see https://issues.k8s.io/61966
|
||||
*/
|
||||
externalID?: string;
|
||||
taints?: NodeTaint[];
|
||||
unschedulable?: boolean;
|
||||
};
|
||||
status: {
|
||||
capacity?: {
|
||||
cpu: string;
|
||||
"ephemeral-storage": string;
|
||||
"hugepages-1Gi": string;
|
||||
"hugepages-2Mi": string;
|
||||
memory: string;
|
||||
pods: string;
|
||||
};
|
||||
allocatable?: {
|
||||
cpu: string;
|
||||
"ephemeral-storage": string;
|
||||
"hugepages-1Gi": string;
|
||||
"hugepages-2Mi": string;
|
||||
memory: string;
|
||||
pods: string;
|
||||
};
|
||||
conditions?: NodeCondition[];
|
||||
addresses?: {
|
||||
type: string;
|
||||
address: string;
|
||||
}[];
|
||||
daemonEndpoints?: {
|
||||
kubeletEndpoint: {
|
||||
Port: number; //it must be uppercase for backwards compatibility
|
||||
};
|
||||
};
|
||||
nodeInfo?: {
|
||||
machineID: string;
|
||||
systemUUID: string;
|
||||
bootID: string;
|
||||
kernelVersion: string;
|
||||
osImage: string;
|
||||
containerRuntimeVersion: string;
|
||||
kubeletVersion: string;
|
||||
kubeProxyVersion: string;
|
||||
operatingSystem: string;
|
||||
architecture: string;
|
||||
};
|
||||
images?: {
|
||||
names: string[];
|
||||
sizeBytes?: number;
|
||||
}[];
|
||||
volumesInUse?: string[];
|
||||
volumesAttached?: {
|
||||
name: string;
|
||||
devicePath: string;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over `conditions` yielding the `type` field if the `status` field is
|
||||
* the string `"True"`
|
||||
* @param conditions An iterator of some conditions
|
||||
*/
|
||||
function* getTrueConditionTypes(conditions: IterableIterator<NodeCondition> | Iterable<NodeCondition>): IterableIterator<string> {
|
||||
for (const { status, type } of conditions) {
|
||||
if (status === "True") {
|
||||
yield type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This regex is used in the `getRoleLabels()` method bellow, but placed here
|
||||
* as factoring out regexes is best practice.
|
||||
*/
|
||||
const nodeRoleLabelKeyMatcher = /^.*node-role.kubernetes.io\/+(?<role>.+)$/;
|
||||
const nodeRoleLabelKeyMatcher = TypedRegEx("^.*node-role.kubernetes.io/+(?<role>.+)$");
|
||||
|
||||
export class Node extends KubeObject {
|
||||
static kind = "Node";
|
||||
static namespaced = false;
|
||||
static apiBase = "/api/v1/nodes";
|
||||
export interface NodeSpec {
|
||||
podCIDR?: string;
|
||||
podCIDRs?: string[];
|
||||
providerID?: string;
|
||||
/**
|
||||
* @deprecated see https://issues.k8s.io/61966
|
||||
*/
|
||||
externalID?: string;
|
||||
taints?: NodeTaint[];
|
||||
unschedulable?: boolean;
|
||||
}
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
export interface NodeAddress {
|
||||
type: "Hostname" | "ExternalIP" | "InternalIP";
|
||||
address: string;
|
||||
}
|
||||
|
||||
export interface NodeStatusResources extends Partial<Record<string, string>> {
|
||||
cpu?: string;
|
||||
"ephemeral-storage"?: string;
|
||||
"hugepages-1Gi"?: string;
|
||||
"hugepages-2Mi"?: string;
|
||||
memory?: string;
|
||||
pods?: string;
|
||||
}
|
||||
|
||||
export interface ConfigMapNodeConfigSource {
|
||||
kubeletConfigKey: string;
|
||||
name: string;
|
||||
namespace: string;
|
||||
resourceVersion?: string;
|
||||
uid?: string;
|
||||
}
|
||||
|
||||
export interface NodeConfigSource {
|
||||
configMap?: ConfigMapNodeConfigSource;
|
||||
}
|
||||
|
||||
export interface NodeConfigStatus {
|
||||
active?: NodeConfigSource;
|
||||
assigned?: NodeConfigSource;
|
||||
lastKnownGood?: NodeConfigSource;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface DaemonEndpoint {
|
||||
Port: number; //it must be uppercase for backwards compatibility
|
||||
}
|
||||
|
||||
export interface NodeDaemonEndpoints {
|
||||
kubeletEndpoint?: DaemonEndpoint;
|
||||
}
|
||||
|
||||
export interface ContainerImage {
|
||||
names?: string[];
|
||||
sizeBytes?: number;
|
||||
}
|
||||
|
||||
export interface NodeSystemInfo {
|
||||
architecture: string;
|
||||
bootID: string;
|
||||
containerRuntimeVersion: string;
|
||||
kernelVersion: string;
|
||||
kubeProxyVersion: string;
|
||||
kubeletVersion: string;
|
||||
machineID: string;
|
||||
operatingSystem: string;
|
||||
osImage: string;
|
||||
systemUUID: string;
|
||||
}
|
||||
|
||||
export interface AttachedVolume {
|
||||
name: string;
|
||||
devicePath: string;
|
||||
}
|
||||
|
||||
export interface NodeStatus {
|
||||
capacity?: NodeStatusResources;
|
||||
allocatable?: NodeStatusResources;
|
||||
conditions?: NodeCondition[];
|
||||
addresses?: NodeAddress[];
|
||||
config?: NodeConfigStatus;
|
||||
daemonEndpoints?: NodeDaemonEndpoints;
|
||||
images?: ContainerImage[];
|
||||
nodeInfo?: NodeSystemInfo;
|
||||
phase?: string;
|
||||
volumesInUse?: string[];
|
||||
volumesAttached?: AttachedVolume[];
|
||||
}
|
||||
|
||||
export class Node extends KubeObject<NodeStatus, NodeSpec, "cluster-scoped"> {
|
||||
static readonly kind = "Node";
|
||||
static readonly namespaced = false;
|
||||
static readonly apiBase = "/api/v1/nodes";
|
||||
|
||||
/**
|
||||
* Returns the concatination of all current condition types which have a status
|
||||
* of `"True"`
|
||||
*/
|
||||
getNodeConditionText(): string {
|
||||
return iter.join(
|
||||
getTrueConditionTypes(this.status?.conditions ?? []),
|
||||
" ",
|
||||
);
|
||||
if (!this.status?.conditions) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return this.status.conditions
|
||||
.filter(condition => condition.status === "True")
|
||||
.map(condition => condition.type)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
getTaints() {
|
||||
@ -182,9 +200,9 @@ export class Node extends KubeObject {
|
||||
const roleLabels: string[] = [];
|
||||
|
||||
for (const labelKey of Object.keys(labels)) {
|
||||
const match = nodeRoleLabelKeyMatcher.exec(labelKey);
|
||||
const match = nodeRoleLabelKeyMatcher.match(labelKey);
|
||||
|
||||
if (match) {
|
||||
if (match?.groups) {
|
||||
roleLabels.push(match.groups.role);
|
||||
}
|
||||
}
|
||||
@ -201,19 +219,19 @@ export class Node extends KubeObject {
|
||||
}
|
||||
|
||||
getCpuCapacity() {
|
||||
if (!this.status.capacity || !this.status.capacity.cpu) return 0;
|
||||
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;
|
||||
if (!this.status?.capacity || !this.status.capacity.memory) return 0;
|
||||
|
||||
return unitsToBytes(this.status.capacity.memory);
|
||||
}
|
||||
|
||||
getConditions() {
|
||||
const conditions = this.status.conditions || [];
|
||||
getConditions(): NodeCondition[] {
|
||||
const conditions = this.status?.conditions || [];
|
||||
|
||||
if (this.isUnschedulable()) {
|
||||
return [{ type: "SchedulingDisabled", status: "True" }, ...conditions];
|
||||
@ -235,7 +253,7 @@ export class Node extends KubeObject {
|
||||
}
|
||||
|
||||
getKubeletVersion() {
|
||||
return this.status.nodeInfo.kubeletVersion;
|
||||
return this.status?.nodeInfo?.kubeletVersion ?? "<unknown>";
|
||||
}
|
||||
|
||||
getOperatingSystem(): string {
|
||||
@ -250,14 +268,6 @@ export class Node extends KubeObject {
|
||||
}
|
||||
}
|
||||
|
||||
let nodesApi: NodesApi;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
nodesApi = new NodesApi({
|
||||
objectConstructor: Node,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
nodesApi,
|
||||
};
|
||||
export const nodeApi = isClusterPageContext()
|
||||
? new NodeApi()
|
||||
: undefined as never;
|
||||
|
||||
@ -3,20 +3,27 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { LabelSelector } from "../kube-object";
|
||||
import type { LabelSelector, TypedLocalObjectReference } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { autoBind } from "../../utils";
|
||||
import type { IMetrics } from "./metrics.api";
|
||||
import type { MetricData } from "./metrics.api";
|
||||
import { metricsApi } from "./metrics.api";
|
||||
import type { Pod } from "./pods.api";
|
||||
import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import { object } from "../../utils";
|
||||
import type { ResourceRequirements } from "./types/resource-requirements";
|
||||
|
||||
export class PersistentVolumeClaimsApi extends KubeApi<PersistentVolumeClaim> {
|
||||
export class PersistentVolumeClaimApi extends KubeApi<PersistentVolumeClaim> {
|
||||
constructor(opts: DerivedKubeApiOptions & IgnoredKubeApiOptions = {}) {
|
||||
super({
|
||||
...opts,
|
||||
objectConstructor: PersistentVolumeClaim,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getMetricsForPvc(pvc: PersistentVolumeClaim): Promise<IPvcMetrics> {
|
||||
export function getMetricsForPvc(pvc: PersistentVolumeClaim): Promise<PersistentVolumeClaimMetricData> {
|
||||
const opts = { category: "pvc", pvc: pvc.getName(), namespace: pvc.getNs() };
|
||||
|
||||
return metricsApi.getMetrics({
|
||||
@ -27,91 +34,59 @@ export function getMetricsForPvc(pvc: PersistentVolumeClaim): Promise<IPvcMetric
|
||||
});
|
||||
}
|
||||
|
||||
export interface IPvcMetrics<T = IMetrics> {
|
||||
[key: string]: T;
|
||||
diskUsage: T;
|
||||
diskCapacity: T;
|
||||
export interface PersistentVolumeClaimMetricData extends Partial<Record<string, MetricData>> {
|
||||
diskUsage: MetricData;
|
||||
diskCapacity: MetricData;
|
||||
}
|
||||
|
||||
export interface PersistentVolumeClaimSpec {
|
||||
accessModes: string[];
|
||||
selector: LabelSelector;
|
||||
resources: {
|
||||
requests?: Record<string, string>;
|
||||
limits?: Record<string, string>;
|
||||
};
|
||||
volumeName?: string;
|
||||
accessModes?: string[];
|
||||
dataSource?: TypedLocalObjectReference;
|
||||
dataSourceRef?: TypedLocalObjectReference;
|
||||
resources?: ResourceRequirements;
|
||||
selector?: LabelSelector;
|
||||
storageClassName?: string;
|
||||
volumeMode?: string;
|
||||
dataSource?: {
|
||||
apiGroup: string;
|
||||
kind: string;
|
||||
name: string;
|
||||
};
|
||||
volumeName?: string;
|
||||
}
|
||||
|
||||
export interface PersistentVolumeClaim {
|
||||
spec: PersistentVolumeClaimSpec;
|
||||
status: {
|
||||
phase: string; // Pending
|
||||
};
|
||||
export interface PersistentVolumeClaimStatus {
|
||||
phase: string; // Pending
|
||||
}
|
||||
|
||||
export class PersistentVolumeClaim extends KubeObject {
|
||||
static kind = "PersistentVolumeClaim";
|
||||
static namespaced = true;
|
||||
static apiBase = "/api/v1/persistentvolumeclaims";
|
||||
export class PersistentVolumeClaim extends KubeObject<PersistentVolumeClaimStatus, PersistentVolumeClaimSpec, "namespace-scoped"> {
|
||||
static readonly kind = "PersistentVolumeClaim";
|
||||
static readonly namespaced = true;
|
||||
static readonly 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;
|
||||
});
|
||||
getPods(pods: Pod[]): Pod[] {
|
||||
return pods
|
||||
.filter(pod => pod.getNs() === this.getNs())
|
||||
.filter(pod => (
|
||||
pod.getVolumes()
|
||||
.filter(volume => volume.persistentVolumeClaim?.claimName === this.getName())
|
||||
.length > 0
|
||||
));
|
||||
}
|
||||
|
||||
getStorage(): string {
|
||||
if (!this.spec.resources || !this.spec.resources.requests) return "-";
|
||||
|
||||
return this.spec.resources.requests.storage;
|
||||
return this.spec.resources?.requests?.storage ?? "-";
|
||||
}
|
||||
|
||||
getMatchLabels(): string[] {
|
||||
if (!this.spec.selector || !this.spec.selector.matchLabels) return [];
|
||||
|
||||
return Object.entries(this.spec.selector.matchLabels)
|
||||
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;
|
||||
return this.spec.selector?.matchExpressions ?? [];
|
||||
}
|
||||
|
||||
getStatus(): string {
|
||||
if (this.status) return this.status.phase;
|
||||
|
||||
return "-";
|
||||
return this.status?.phase ?? "-";
|
||||
}
|
||||
}
|
||||
|
||||
let pvcApi: PersistentVolumeClaimsApi;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
pvcApi = new PersistentVolumeClaimsApi({
|
||||
objectConstructor: PersistentVolumeClaim,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
pvcApi,
|
||||
};
|
||||
export const persistentVolumeClaimApi = isClusterPageContext()
|
||||
? new PersistentVolumeClaimApi()
|
||||
: undefined as never;
|
||||
|
||||
@ -3,64 +3,77 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { LabelSelector, ObjectReference, TypedLocalObjectReference } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { autoBind, unitsToBytes } from "../../utils";
|
||||
import { unitsToBytes } from "../../utils";
|
||||
import type { DerivedKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import type { ResourceRequirements } from "./types/resource-requirements";
|
||||
|
||||
export interface PersistentVolume {
|
||||
spec: {
|
||||
capacity: {
|
||||
storage: string; // 8Gi
|
||||
};
|
||||
flexVolume: {
|
||||
driver: string; // ceph.rook.io/rook-ceph-system,
|
||||
options: {
|
||||
clusterNamespace: string; // rook-ceph,
|
||||
image: string; // pvc-c5d7c485-9f1b-11e8-b0ea-9600000e54fb,
|
||||
pool: string; // replicapool,
|
||||
storageClass: string; // rook-ceph-block
|
||||
};
|
||||
};
|
||||
mountOptions?: string[];
|
||||
accessModes: string[]; // [ReadWriteOnce]
|
||||
claimRef: {
|
||||
kind: string; // PersistentVolumeClaim,
|
||||
namespace: string; // storage,
|
||||
name: string; // nfs-provisioner,
|
||||
uid: string; // c5d7c485-9f1b-11e8-b0ea-9600000e54fb,
|
||||
apiVersion: string; // v1,
|
||||
resourceVersion: string; // 292180
|
||||
};
|
||||
persistentVolumeReclaimPolicy: string; // Delete,
|
||||
storageClassName: string; // rook-ceph-block
|
||||
nfs?: {
|
||||
path: string;
|
||||
server: string;
|
||||
export interface PersistentVolumeSpec {
|
||||
/**
|
||||
* AccessModes contains the desired access modes the volume should have.
|
||||
*
|
||||
* More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1
|
||||
*/
|
||||
accessModes?: string[];
|
||||
dataSource?: TypedLocalObjectReference;
|
||||
dataSourceRef?: TypedLocalObjectReference;
|
||||
resources?: ResourceRequirements;
|
||||
selector?: LabelSelector;
|
||||
|
||||
/**
|
||||
* Name of the StorageClass required by the claim.
|
||||
*
|
||||
* More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1
|
||||
*/
|
||||
storageClassName?: string;
|
||||
|
||||
/**
|
||||
* Defines what type of volume is required by the claim. Value of Filesystem is implied when not
|
||||
* included in claim spec.
|
||||
*/
|
||||
volumeMode?: string;
|
||||
|
||||
/**
|
||||
* A description of the persistent volume\'s resources and capacity.
|
||||
*
|
||||
* More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#capacity
|
||||
*/
|
||||
capacity?: Partial<Record<string, string>>;
|
||||
flexVolume?: {
|
||||
driver: string; // ceph.rook.io/rook-ceph-system,
|
||||
options: {
|
||||
clusterNamespace: string; // rook-ceph,
|
||||
image: string; // pvc-c5d7c485-9f1b-11e8-b0ea-9600000e54fb,
|
||||
pool: string; // replicapool,
|
||||
storageClass: string; // rook-ceph-block
|
||||
};
|
||||
};
|
||||
|
||||
status?: {
|
||||
phase: string;
|
||||
reason?: string;
|
||||
mountOptions?: string[];
|
||||
claimRef?: ObjectReference;
|
||||
persistentVolumeReclaimPolicy?: string; // Delete,
|
||||
nfs?: {
|
||||
path: string;
|
||||
server: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class PersistentVolume extends KubeObject {
|
||||
export interface PersistentVolumeStatus {
|
||||
phase: string;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export class PersistentVolume extends KubeObject<PersistentVolumeStatus, PersistentVolumeSpec, "cluster-scoped"> {
|
||||
static kind = "PersistentVolume";
|
||||
static namespaced = false;
|
||||
static apiBase = "/api/v1/persistentvolumes";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
getCapacity(inBytes = false) {
|
||||
const capacity = this.spec.capacity;
|
||||
|
||||
if (capacity) {
|
||||
if (capacity?.storage) {
|
||||
if (inBytes) return unitsToBytes(capacity.storage);
|
||||
|
||||
return capacity.storage;
|
||||
@ -74,7 +87,7 @@ export class PersistentVolume extends KubeObject {
|
||||
}
|
||||
|
||||
getStorageClass(): string {
|
||||
return this.spec.storageClassName;
|
||||
return this.spec.storageClassName ?? "";
|
||||
}
|
||||
|
||||
getClaimRefName(): string {
|
||||
@ -86,14 +99,15 @@ export class PersistentVolume extends KubeObject {
|
||||
}
|
||||
}
|
||||
|
||||
let persistentVolumeApi: KubeApi<PersistentVolume>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
persistentVolumeApi = new KubeApi({
|
||||
objectConstructor: PersistentVolume,
|
||||
});
|
||||
export class PersistentVolumeApi extends KubeApi<PersistentVolume> {
|
||||
constructor(opts: DerivedKubeApiOptions = {}) {
|
||||
super({
|
||||
...opts,
|
||||
objectConstructor: PersistentVolume,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
persistentVolumeApi,
|
||||
};
|
||||
export const persistentVolumeApi = isClusterPageContext()
|
||||
? new PersistentVolumeApi()
|
||||
: undefined as never;
|
||||
|
||||
@ -3,36 +3,60 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { KubeObjectMetadata } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { DerivedKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
|
||||
export interface PodMetrics {
|
||||
export interface PodMetricsData extends KubeJsonApiData<KubeObjectMetadata<"namespace-scoped">, void, void> {
|
||||
timestamp: string;
|
||||
window: string;
|
||||
containers: {
|
||||
name: string;
|
||||
usage: {
|
||||
cpu: string;
|
||||
memory: string;
|
||||
};
|
||||
}[];
|
||||
containers: PodMetricsContainer[];
|
||||
}
|
||||
|
||||
export class PodMetrics extends KubeObject {
|
||||
static kind = "PodMetrics";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/metrics.k8s.io/v1beta1/pods";
|
||||
export interface PodMetricsContainerUsage {
|
||||
cpu: string;
|
||||
memory: string;
|
||||
}
|
||||
|
||||
let podMetricsApi: KubeApi<PodMetrics>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
podMetricsApi = new KubeApi<PodMetrics>({
|
||||
objectConstructor: PodMetrics,
|
||||
});
|
||||
export interface PodMetricsContainer {
|
||||
name: string;
|
||||
usage: PodMetricsContainerUsage;
|
||||
}
|
||||
|
||||
export {
|
||||
podMetricsApi,
|
||||
};
|
||||
export class PodMetrics extends KubeObject<void, void, "namespace-scoped"> {
|
||||
static readonly kind = "PodMetrics";
|
||||
static readonly namespaced = true;
|
||||
static readonly apiBase = "/apis/metrics.k8s.io/v1beta1/pods";
|
||||
|
||||
timestamp: string;
|
||||
window: string;
|
||||
containers: PodMetricsContainer[];
|
||||
|
||||
constructor({
|
||||
timestamp,
|
||||
window,
|
||||
containers,
|
||||
...rest
|
||||
}: PodMetricsData) {
|
||||
super(rest);
|
||||
this.timestamp = timestamp;
|
||||
this.window = window;
|
||||
this.containers = containers;
|
||||
}
|
||||
}
|
||||
|
||||
export class PodMetricsApi extends KubeApi<PodMetrics, PodMetricsData> {
|
||||
constructor(opts: DerivedKubeApiOptions = {}) {
|
||||
super({
|
||||
...opts,
|
||||
objectConstructor: PodMetrics,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const podMetricsApi = isClusterPageContext()
|
||||
? new PodMetricsApi()
|
||||
: undefined as never;
|
||||
|
||||
@ -3,41 +3,32 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { autoBind } from "../../utils";
|
||||
import type { LabelSelector } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { DerivedKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export interface PodDisruptionBudget {
|
||||
spec: {
|
||||
minAvailable: string;
|
||||
maxUnavailable: string;
|
||||
selector: LabelSelector;
|
||||
};
|
||||
status: {
|
||||
currentHealthy: number;
|
||||
desiredHealthy: number;
|
||||
disruptionsAllowed: number;
|
||||
expectedPods: number;
|
||||
};
|
||||
export interface PodDisruptionBudgetSpec {
|
||||
minAvailable: string;
|
||||
maxUnavailable: string;
|
||||
selector: LabelSelector;
|
||||
}
|
||||
|
||||
export class PodDisruptionBudget extends KubeObject {
|
||||
static kind = "PodDisruptionBudget";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/policy/v1beta1/poddisruptionbudgets";
|
||||
export interface PodDisruptionBudgetStatus {
|
||||
currentHealthy: number;
|
||||
desiredHealthy: number;
|
||||
disruptionsAllowed: number;
|
||||
expectedPods: number;
|
||||
}
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
export class PodDisruptionBudget extends KubeObject<PodDisruptionBudgetStatus, PodDisruptionBudgetSpec, "namespace-scoped"> {
|
||||
static readonly kind = "PodDisruptionBudget";
|
||||
static readonly namespaced = true;
|
||||
static readonly apiBase = "/apis/policy/v1beta1/poddisruptionbudgets";
|
||||
|
||||
getSelectors() {
|
||||
const selector = this.spec.selector;
|
||||
|
||||
return KubeObject.stringifyLabels(selector ? selector.matchLabels : null);
|
||||
return KubeObject.stringifyLabels(this.spec.selector.matchLabels);
|
||||
}
|
||||
|
||||
getMinAvailable() {
|
||||
@ -49,23 +40,23 @@ export class PodDisruptionBudget extends KubeObject {
|
||||
}
|
||||
|
||||
getCurrentHealthy() {
|
||||
return this.status.currentHealthy;
|
||||
return this.status?.currentHealthy ?? 0;
|
||||
}
|
||||
|
||||
getDesiredHealthy() {
|
||||
return this.status.desiredHealthy;
|
||||
return this.status?.desiredHealthy ?? 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let pdbApi: KubeApi<PodDisruptionBudget>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
pdbApi = new KubeApi({
|
||||
objectConstructor: PodDisruptionBudget,
|
||||
});
|
||||
export class PodDisruptionBudgetApi extends KubeApi<PodDisruptionBudget> {
|
||||
constructor(opts: DerivedKubeApiOptions = {}) {
|
||||
super({
|
||||
objectConstructor: PodDisruptionBudget,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
pdbApi,
|
||||
};
|
||||
export const podDisruptionBudgetApi = isClusterPageContext()
|
||||
? new PodDisruptionBudgetApi()
|
||||
: undefined as never;
|
||||
|
||||
@ -3,20 +3,26 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { IAffinity } from "../workload-kube-object";
|
||||
import { WorkloadKubeObject } from "../workload-kube-object";
|
||||
import { autoBind } from "../../utils";
|
||||
import type { IMetrics } from "./metrics.api";
|
||||
import type { MetricData } from "./metrics.api";
|
||||
import { metricsApi } from "./metrics.api";
|
||||
import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import type { RequireExactlyOne } from "type-fest";
|
||||
import type { KubeObjectMetadata, LocalObjectReference } from "../kube-object";
|
||||
import type { KubeObjectMetadata, LocalObjectReference, Affinity, Toleration, LabelSelector } from "../kube-object";
|
||||
import type { SecretReference } from "./secret.api";
|
||||
import type { PersistentVolumeClaimSpec } from "./persistent-volume-claims.api";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { isDefined } from "../../utils";
|
||||
|
||||
export class PodApi extends KubeApi<Pod> {
|
||||
constructor(opts: DerivedKubeApiOptions & IgnoredKubeApiOptions = {}) {
|
||||
super({
|
||||
...opts,
|
||||
objectConstructor: Pod,
|
||||
});
|
||||
}
|
||||
|
||||
export class PodsApi extends KubeApi<Pod> {
|
||||
getLogs = async (params: { namespace: string; name: string }, query?: IPodLogsQuery): Promise<string> => {
|
||||
const path = `${this.getUrl(params)}/log`;
|
||||
|
||||
@ -24,7 +30,7 @@ export class PodsApi extends KubeApi<Pod> {
|
||||
};
|
||||
}
|
||||
|
||||
export function getMetricsForPods(pods: Pod[], namespace: string, selector = "pod, namespace"): Promise<IPodMetrics> {
|
||||
export function getMetricsForPods(pods: Pod[], namespace: string, selector = "pod, namespace"): Promise<PodMetricData> {
|
||||
const podSelector = pods.map(pod => pod.getName()).join("|");
|
||||
const opts = { category: "pods", pods: podSelector, namespace, selector };
|
||||
|
||||
@ -45,19 +51,18 @@ export function getMetricsForPods(pods: Pod[], namespace: string, selector = "po
|
||||
});
|
||||
}
|
||||
|
||||
export interface IPodMetrics<T = IMetrics> {
|
||||
[metric: string]: T;
|
||||
cpuUsage: T;
|
||||
memoryUsage: T;
|
||||
fsUsage: T;
|
||||
fsWrites: T;
|
||||
fsReads: T;
|
||||
networkReceive: T;
|
||||
networkTransmit: T;
|
||||
cpuRequests?: T;
|
||||
cpuLimits?: T;
|
||||
memoryRequests?: T;
|
||||
memoryLimits?: T;
|
||||
export interface PodMetricData extends Partial<Record<string, MetricData>> {
|
||||
cpuUsage: MetricData;
|
||||
memoryUsage: MetricData;
|
||||
fsUsage: MetricData;
|
||||
fsWrites: MetricData;
|
||||
fsReads: MetricData;
|
||||
networkReceive: MetricData;
|
||||
networkTransmit: MetricData;
|
||||
cpuRequests?: MetricData;
|
||||
cpuLimits?: MetricData;
|
||||
memoryRequests?: MetricData;
|
||||
memoryLimits?: MetricData;
|
||||
}
|
||||
|
||||
// Reference: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#read-log-pod-v1-core
|
||||
@ -70,7 +75,7 @@ export interface IPodLogsQuery {
|
||||
previous?: boolean;
|
||||
}
|
||||
|
||||
export enum PodStatus {
|
||||
export enum PodStatusPhase {
|
||||
TERMINATED = "Terminated",
|
||||
FAILED = "Failed",
|
||||
PENDING = "Pending",
|
||||
@ -79,26 +84,41 @@ export enum PodStatus {
|
||||
EVICTED = "Evicted",
|
||||
}
|
||||
|
||||
export interface ContainerPort {
|
||||
containerPort: number;
|
||||
hostIP?: string;
|
||||
hostPort?: number;
|
||||
name?: string;
|
||||
protocol?: "UDP" | "TCP" | "SCTP";
|
||||
}
|
||||
|
||||
export interface VolumeMount {
|
||||
name: string;
|
||||
readOnly?: boolean;
|
||||
mountPath: string;
|
||||
mountPropagation?: string;
|
||||
subPath?: string;
|
||||
subPathExpr?: string;
|
||||
}
|
||||
|
||||
export interface IPodContainer extends Partial<Record<PodContainerProbe, IContainerProbe>> {
|
||||
name: string;
|
||||
image: string;
|
||||
command?: string[];
|
||||
args?: string[];
|
||||
ports?: {
|
||||
name?: string;
|
||||
containerPort: number;
|
||||
protocol: string;
|
||||
}[];
|
||||
ports?: ContainerPort[];
|
||||
resources?: {
|
||||
limits: {
|
||||
limits?: {
|
||||
cpu: string;
|
||||
memory: string;
|
||||
};
|
||||
requests: {
|
||||
requests?: {
|
||||
cpu: string;
|
||||
memory: string;
|
||||
};
|
||||
};
|
||||
terminationMessagePath?: string;
|
||||
terminationMessagePolicy?: string;
|
||||
env?: {
|
||||
name: string;
|
||||
value?: string;
|
||||
@ -121,12 +141,8 @@ export interface IPodContainer extends Partial<Record<PodContainerProbe, IContai
|
||||
configMapRef?: LocalObjectReference;
|
||||
secretRef?: LocalObjectReference;
|
||||
}[];
|
||||
volumeMounts?: {
|
||||
name: string;
|
||||
readOnly: boolean;
|
||||
mountPath: string;
|
||||
}[];
|
||||
imagePullPolicy: string;
|
||||
volumeMounts?: VolumeMount[];
|
||||
imagePullPolicy?: string;
|
||||
}
|
||||
|
||||
export type PodContainerProbe = "livenessProbe" | "readinessProbe" | "startupProbe";
|
||||
@ -631,85 +647,133 @@ export interface PodVolumeVariants {
|
||||
*/
|
||||
export type PodVolumeKind = keyof PodVolumeVariants;
|
||||
|
||||
export type PodVolume = RequireExactlyOne<PodVolumeVariants> & {
|
||||
export type PodSpecVolume = RequireExactlyOne<PodVolumeVariants> & {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export class Pod extends WorkloadKubeObject {
|
||||
|
||||
export interface HostAlias {
|
||||
ip: string;
|
||||
hostnames: string[];
|
||||
}
|
||||
|
||||
export interface SELinuxOptions {
|
||||
level?: string;
|
||||
role?: string;
|
||||
type?: string;
|
||||
user?: string;
|
||||
}
|
||||
|
||||
export interface SeccompProfile {
|
||||
localhostProfile?: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface Sysctl {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface WindowsSecurityContextOptions {
|
||||
labelSelector?: LabelSelector;
|
||||
maxSkew: number;
|
||||
topologyKey: string;
|
||||
whenUnsatisfiable: string;
|
||||
}
|
||||
|
||||
export interface PodSecurityContext {
|
||||
fsGroup?: number;
|
||||
fsGroupChangePolicy?: string;
|
||||
runAsGroup?: number;
|
||||
runAsNonRoot?: boolean;
|
||||
runAsUser?: number;
|
||||
seLinuxOptions?: SELinuxOptions;
|
||||
seccompProfile?: SeccompProfile;
|
||||
supplementalGroups?: number[];
|
||||
sysctls?: Sysctl;
|
||||
windowsOptions?: WindowsSecurityContextOptions;
|
||||
}
|
||||
|
||||
export interface TopologySpreadConstraint {
|
||||
|
||||
}
|
||||
|
||||
export interface PodSpec {
|
||||
activeDeadlineSeconds?: number;
|
||||
affinity?: Affinity;
|
||||
automountServiceAccountToken?: boolean;
|
||||
containers?: IPodContainer[];
|
||||
dnsPolicy?: string;
|
||||
enableServiceLinks?: boolean;
|
||||
ephemeralContainers?: unknown[];
|
||||
hostAliases?: HostAlias[];
|
||||
hostIPC?: boolean;
|
||||
hostname?: string;
|
||||
hostNetwork?: boolean;
|
||||
hostPID?: boolean;
|
||||
imagePullSecrets?: LocalObjectReference[];
|
||||
initContainers?: IPodContainer[];
|
||||
nodeName?: string;
|
||||
nodeSelector?: Record<string, string | undefined>;
|
||||
overhead?: Record<string, string | undefined>;
|
||||
preemptionPolicy?: string;
|
||||
priority?: number;
|
||||
priorityClassName?: string;
|
||||
readinessGates?: unknown[];
|
||||
restartPolicy?: string;
|
||||
runtimeClassName?: string;
|
||||
schedulerName?: string;
|
||||
securityContext?: PodSecurityContext;
|
||||
serviceAccount?: string;
|
||||
serviceAccountName?: string;
|
||||
setHostnameAsFQDN?: boolean;
|
||||
shareProcessNamespace?: boolean;
|
||||
subdomain?: string;
|
||||
terminationGracePeriodSeconds?: number;
|
||||
tolerations?: Toleration[];
|
||||
topologySpreadConstraints?: TopologySpreadConstraint[];
|
||||
volumes?: PodSpecVolume[];
|
||||
}
|
||||
|
||||
export interface PodCondition {
|
||||
lastProbeTime?: number;
|
||||
lastTransitionTime?: string;
|
||||
message?: string;
|
||||
reason?: string;
|
||||
type: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface PodStatus {
|
||||
phase: string;
|
||||
conditions: PodCondition[];
|
||||
hostIP: string;
|
||||
podIP: string;
|
||||
podIPs?: {
|
||||
ip: string;
|
||||
}[];
|
||||
startTime: string;
|
||||
initContainerStatuses?: IPodContainerStatus[];
|
||||
containerStatuses?: IPodContainerStatus[];
|
||||
qosClass?: string;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export class Pod extends KubeObject<PodStatus, PodSpec, "namespace-scoped"> {
|
||||
static kind = "Pod";
|
||||
static namespaced = true;
|
||||
static apiBase = "/api/v1/pods";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
getAffinityNumber() {
|
||||
return Object.keys(this.getAffinity()).length;
|
||||
}
|
||||
|
||||
declare spec?: {
|
||||
volumes?: PodVolume[];
|
||||
initContainers: IPodContainer[];
|
||||
containers: IPodContainer[];
|
||||
restartPolicy?: string;
|
||||
terminationGracePeriodSeconds?: number;
|
||||
activeDeadlineSeconds?: number;
|
||||
dnsPolicy?: string;
|
||||
serviceAccountName: string;
|
||||
serviceAccount: string;
|
||||
automountServiceAccountToken?: boolean;
|
||||
priority?: number;
|
||||
priorityClassName?: string;
|
||||
nodeName?: string;
|
||||
nodeSelector?: {
|
||||
[selector: string]: string;
|
||||
};
|
||||
securityContext?: {};
|
||||
imagePullSecrets?: LocalObjectReference[];
|
||||
hostNetwork?: boolean;
|
||||
hostPID?: boolean;
|
||||
hostIPC?: boolean;
|
||||
shareProcessNamespace?: boolean;
|
||||
hostname?: string;
|
||||
subdomain?: string;
|
||||
schedulerName?: string;
|
||||
tolerations?: {
|
||||
key?: string;
|
||||
operator?: string;
|
||||
effect?: string;
|
||||
tolerationSeconds?: number;
|
||||
value?: string;
|
||||
}[];
|
||||
hostAliases?: {
|
||||
ip: string;
|
||||
hostnames: string[];
|
||||
};
|
||||
affinity?: IAffinity;
|
||||
};
|
||||
declare status?: {
|
||||
phase: string;
|
||||
conditions: {
|
||||
type: string;
|
||||
status: string;
|
||||
lastProbeTime: number;
|
||||
lastTransitionTime: string;
|
||||
}[];
|
||||
hostIP: string;
|
||||
podIP: string;
|
||||
podIPs?: {
|
||||
ip: string;
|
||||
}[];
|
||||
startTime: string;
|
||||
initContainerStatuses?: IPodContainerStatus[];
|
||||
containerStatuses?: IPodContainerStatus[];
|
||||
qosClass?: string;
|
||||
reason?: string;
|
||||
};
|
||||
|
||||
getInitContainers() {
|
||||
return this.spec?.initContainers || [];
|
||||
return this.spec?.initContainers ?? [];
|
||||
}
|
||||
|
||||
getContainers() {
|
||||
return this.spec?.containers || [];
|
||||
return this.spec?.containers ?? [];
|
||||
}
|
||||
|
||||
getAllContainers() {
|
||||
@ -719,7 +783,7 @@ export class Pod extends WorkloadKubeObject {
|
||||
getRunningContainers() {
|
||||
const runningContainerNames = new Set(
|
||||
this.getContainerStatuses()
|
||||
.filter(({ state }) => state.running)
|
||||
.filter(({ state }) => state?.running)
|
||||
.map(({ name }) => name),
|
||||
);
|
||||
|
||||
@ -752,10 +816,10 @@ export class Pod extends WorkloadKubeObject {
|
||||
}
|
||||
|
||||
getPriorityClassName() {
|
||||
return this.spec.priorityClassName || "";
|
||||
return this.spec?.priorityClassName || "";
|
||||
}
|
||||
|
||||
getStatus(): PodStatus {
|
||||
getStatus(): PodStatusPhase {
|
||||
const phase = this.getStatusPhase();
|
||||
const reason = this.getReason();
|
||||
const trueConditionTypes = new Set(this.getConditions()
|
||||
@ -763,28 +827,28 @@ export class Pod extends WorkloadKubeObject {
|
||||
.map(({ type }) => type));
|
||||
const isInGoodCondition = ["Initialized", "Ready"].every(condition => trueConditionTypes.has(condition));
|
||||
|
||||
if (reason === PodStatus.EVICTED) {
|
||||
return PodStatus.EVICTED;
|
||||
if (reason === PodStatusPhase.EVICTED) {
|
||||
return PodStatusPhase.EVICTED;
|
||||
}
|
||||
|
||||
if (phase === PodStatus.FAILED) {
|
||||
return PodStatus.FAILED;
|
||||
if (phase === PodStatusPhase.FAILED) {
|
||||
return PodStatusPhase.FAILED;
|
||||
}
|
||||
|
||||
if (phase === PodStatus.SUCCEEDED) {
|
||||
return PodStatus.SUCCEEDED;
|
||||
if (phase === PodStatusPhase.SUCCEEDED) {
|
||||
return PodStatusPhase.SUCCEEDED;
|
||||
}
|
||||
|
||||
if (phase === PodStatus.RUNNING && isInGoodCondition) {
|
||||
return PodStatus.RUNNING;
|
||||
if (phase === PodStatusPhase.RUNNING && isInGoodCondition) {
|
||||
return PodStatusPhase.RUNNING;
|
||||
}
|
||||
|
||||
return PodStatus.PENDING;
|
||||
return PodStatusPhase.PENDING;
|
||||
}
|
||||
|
||||
// Returns pod phase or container error if occurred
|
||||
getStatusMessage(): string {
|
||||
if (this.getReason() === PodStatus.EVICTED) {
|
||||
if (this.getReason() === PodStatusPhase.EVICTED) {
|
||||
return "Evicted";
|
||||
}
|
||||
|
||||
@ -800,31 +864,30 @@ export class Pod extends WorkloadKubeObject {
|
||||
}
|
||||
|
||||
getConditions() {
|
||||
return this.status?.conditions || [];
|
||||
return this.status?.conditions ?? [];
|
||||
}
|
||||
|
||||
getVolumes() {
|
||||
return this.spec.volumes || [];
|
||||
return this.spec?.volumes ?? [];
|
||||
}
|
||||
|
||||
getSecrets(): string[] {
|
||||
return this.getVolumes()
|
||||
.filter(vol => vol.secret)
|
||||
.map(vol => vol.secret.secretName);
|
||||
.map(vol => vol.secret?.secretName)
|
||||
.filter(isDefined);
|
||||
}
|
||||
|
||||
getNodeSelectors(): string[] {
|
||||
const { nodeSelector = {}} = this.spec;
|
||||
|
||||
return Object.entries(nodeSelector).map(values => values.join(": "));
|
||||
return Object.entries(this.spec?.nodeSelector ?? {})
|
||||
.map(values => values.join(": "));
|
||||
}
|
||||
|
||||
getTolerations() {
|
||||
return this.spec.tolerations || [];
|
||||
return this.spec?.tolerations ?? [];
|
||||
}
|
||||
|
||||
getAffinity(): IAffinity {
|
||||
return this.spec.affinity;
|
||||
getAffinity(): Affinity {
|
||||
return this.spec?.affinity ?? {};
|
||||
}
|
||||
|
||||
hasIssues() {
|
||||
@ -907,30 +970,21 @@ export class Pod extends WorkloadKubeObject {
|
||||
return probe;
|
||||
}
|
||||
|
||||
getNodeName() {
|
||||
return this.spec.nodeName;
|
||||
getNodeName(): string | undefined {
|
||||
return this.spec?.nodeName;
|
||||
}
|
||||
|
||||
getSelectedNodeOs(): string | undefined {
|
||||
return this.spec.nodeSelector?.["kubernetes.io/os"] || this.spec.nodeSelector?.["beta.kubernetes.io/os"];
|
||||
return this.spec?.nodeSelector?.["kubernetes.io/os"] || this.spec?.nodeSelector?.["beta.kubernetes.io/os"];
|
||||
}
|
||||
|
||||
getIPs(): string[] {
|
||||
if(!this.status.podIPs) return [];
|
||||
const podIPs = this.status.podIPs;
|
||||
const podIPs = this.status?.podIPs ?? [];
|
||||
|
||||
return podIPs.map(value => value.ip);
|
||||
}
|
||||
}
|
||||
|
||||
let podsApi: PodsApi;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
podsApi = new PodsApi({
|
||||
objectConstructor: Pod,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
podsApi,
|
||||
};
|
||||
export const podApi = isClusterPageContext()
|
||||
? new PodApi()
|
||||
: undefined as never;
|
||||
|
||||
@ -3,83 +3,87 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { autoBind } from "../../utils";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { DerivedKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export interface 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?: {
|
||||
export interface PodSecurityPolicySpec {
|
||||
allowPrivilegeEscalation?: boolean;
|
||||
allowedCSIDrivers?: {
|
||||
name: string;
|
||||
}[];
|
||||
allowedCapabilities: string[];
|
||||
allowedFlexVolumes?: {
|
||||
driver: string;
|
||||
}[];
|
||||
allowedHostPaths?: {
|
||||
pathPrefix: string;
|
||||
readOnly: boolean;
|
||||
}[];
|
||||
allowedProcMountTypes?: string[];
|
||||
allowedUnsafeSysctls?: string[];
|
||||
defaultAddCapabilities?: string[];
|
||||
defaultAllowPrivilegeEscalation?: boolean;
|
||||
forbiddenSysctls?: string[];
|
||||
fsGroup?: {
|
||||
rule: string;
|
||||
ranges: {
|
||||
max: number;
|
||||
min: number;
|
||||
}[];
|
||||
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[];
|
||||
};
|
||||
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);
|
||||
}
|
||||
export class PodSecurityPolicy extends KubeObject<void, PodSecurityPolicySpec, "cluster-scoped"> {
|
||||
static readonly kind = "PodSecurityPolicy";
|
||||
static readonly namespaced = false;
|
||||
static readonly apiBase = "/apis/policy/v1beta1/podsecuritypolicies";
|
||||
|
||||
isPrivileged() {
|
||||
return !!this.spec.privileged;
|
||||
@ -102,14 +106,16 @@ export class PodSecurityPolicy extends KubeObject {
|
||||
}
|
||||
}
|
||||
|
||||
let pspApi: KubeApi<PodSecurityPolicy>;
|
||||
export class PodSecurityPolicyApi extends KubeApi<PodSecurityPolicy> {
|
||||
constructor(opts: DerivedKubeApiOptions = {}) {
|
||||
super({
|
||||
...opts,
|
||||
objectConstructor: PodSecurityPolicy,
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
pspApi = new KubeApi({
|
||||
objectConstructor: PodSecurityPolicy,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
pspApi,
|
||||
};
|
||||
export const podSecurityPolicyApi = isClusterPageContext()
|
||||
? new PodSecurityPolicyApi()
|
||||
: undefined as never;
|
||||
|
||||
@ -3,25 +3,31 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import get from "lodash/get";
|
||||
import { autoBind } from "../../../renderer/utils";
|
||||
import { WorkloadKubeObject } from "../workload-kube-object";
|
||||
import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { metricsApi } from "./metrics.api";
|
||||
import type { IPodContainer, IPodMetrics, Pod } from "./pods.api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import type { PodMetricData } from "./pods.api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import type { LabelSelector } from "../kube-object";
|
||||
import type { KubeObjectStatus, LabelSelector } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { PodTemplateSpec } from "./types/pod-template-spec";
|
||||
|
||||
export class ReplicaSetApi extends KubeApi<ReplicaSet> {
|
||||
constructor(opts: DerivedKubeApiOptions & IgnoredKubeApiOptions = {}) {
|
||||
super({
|
||||
...opts,
|
||||
objectConstructor: ReplicaSet,
|
||||
});
|
||||
}
|
||||
|
||||
protected getScaleApiUrl(params: { namespace: string; name: string }) {
|
||||
return `${this.getUrl(params)}/scale`;
|
||||
}
|
||||
|
||||
getReplicas(params: { namespace: string; name: string }): Promise<number> {
|
||||
return this.request
|
||||
.get(this.getScaleApiUrl(params))
|
||||
.then(({ status }: any) => status?.replicas);
|
||||
async getReplicas(params: { namespace: string; name: string }): Promise<number> {
|
||||
const { status } = await this.request.get(this.getScaleApiUrl(params));
|
||||
|
||||
return (status as { replicas: number })?.replicas;
|
||||
}
|
||||
|
||||
scale(params: { namespace: string; name: string }, replicas: number) {
|
||||
@ -36,7 +42,7 @@ export class ReplicaSetApi extends KubeApi<ReplicaSet> {
|
||||
}
|
||||
}
|
||||
|
||||
export function getMetricsForReplicaSets(replicasets: ReplicaSet[], namespace: string, selector = ""): Promise<IPodMetrics> {
|
||||
export function getMetricsForReplicaSets(replicasets: ReplicaSet[], namespace: string, selector = ""): Promise<PodMetricData> {
|
||||
const podSelector = replicasets.map(replicaset => `${replicaset.getName()}-[[:alnum:]]{5}`).join("|");
|
||||
const opts = { category: "pods", pods: podSelector, namespace, selector };
|
||||
|
||||
@ -53,72 +59,69 @@ export function getMetricsForReplicaSets(replicasets: ReplicaSet[], namespace: s
|
||||
});
|
||||
}
|
||||
|
||||
export class ReplicaSet extends WorkloadKubeObject {
|
||||
export interface ReplicaSetSpec {
|
||||
replicas?: number;
|
||||
selector: LabelSelector;
|
||||
template?: PodTemplateSpec;
|
||||
minReadySeconds?: number;
|
||||
}
|
||||
|
||||
export interface ReplicaSetStatus extends KubeObjectStatus {
|
||||
replicas: number;
|
||||
fullyLabeledReplicas?: number;
|
||||
readyReplicas?: number;
|
||||
availableReplicas?: number;
|
||||
observedGeneration?: number;
|
||||
}
|
||||
|
||||
export class ReplicaSet extends KubeObject<ReplicaSetStatus, ReplicaSetSpec, "namespace-scoped"> {
|
||||
static kind = "ReplicaSet";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/apps/v1/replicasets";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
getSelectors(): string[] {
|
||||
return KubeObject.stringifyLabels(this.spec.selector.matchLabels);
|
||||
}
|
||||
|
||||
declare spec: {
|
||||
replicas?: number;
|
||||
selector: LabelSelector;
|
||||
template?: {
|
||||
metadata: {
|
||||
labels: {
|
||||
app: string;
|
||||
};
|
||||
};
|
||||
spec?: Pod["spec"];
|
||||
};
|
||||
minReadySeconds?: number;
|
||||
};
|
||||
declare status: {
|
||||
replicas: number;
|
||||
fullyLabeledReplicas?: number;
|
||||
readyReplicas?: number;
|
||||
availableReplicas?: number;
|
||||
observedGeneration?: number;
|
||||
conditions?: {
|
||||
type: string;
|
||||
status: string;
|
||||
lastUpdateTime: string;
|
||||
lastTransitionTime: string;
|
||||
reason: string;
|
||||
message: string;
|
||||
}[];
|
||||
};
|
||||
getNodeSelectors(): string[] {
|
||||
return KubeObject.stringifyLabels(this.spec.template?.spec?.nodeSelector);
|
||||
}
|
||||
|
||||
getTemplateLabels(): string[] {
|
||||
return KubeObject.stringifyLabels(this.spec.template?.metadata?.labels);
|
||||
}
|
||||
|
||||
getTolerations() {
|
||||
return this.spec.template?.spec?.tolerations ?? [];
|
||||
}
|
||||
|
||||
getAffinity() {
|
||||
return this.spec.template?.spec?.affinity;
|
||||
}
|
||||
|
||||
getAffinityNumber() {
|
||||
return Object.keys(this.getAffinity() ?? {}).length;
|
||||
}
|
||||
|
||||
getDesired() {
|
||||
return this.spec.replicas || 0;
|
||||
return this.spec.replicas ?? 0;
|
||||
}
|
||||
|
||||
getCurrent() {
|
||||
return this.status.availableReplicas || 0;
|
||||
return this.status?.availableReplicas ?? 0;
|
||||
}
|
||||
|
||||
getReady() {
|
||||
return this.status.readyReplicas || 0;
|
||||
return this.status?.readyReplicas ?? 0;
|
||||
}
|
||||
|
||||
getImages() {
|
||||
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []);
|
||||
const containers = this.spec.template?.spec?.containers ?? [];
|
||||
|
||||
return [...containers].map(container => container.image);
|
||||
return containers.map(container => container.image);
|
||||
}
|
||||
}
|
||||
|
||||
let replicaSetApi: ReplicaSetApi;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
replicaSetApi = new ReplicaSetApi({
|
||||
objectConstructor: ReplicaSet,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
replicaSetApi,
|
||||
};
|
||||
export const replicaSetApi = isClusterPageContext()
|
||||
? new ReplicaSetApi()
|
||||
: undefined as never;
|
||||
|
||||
@ -16,7 +16,7 @@ export async function update(resource: object | string): Promise<KubeJsonApiData
|
||||
if (typeof resource === "string") {
|
||||
const parsed = yaml.load(resource);
|
||||
|
||||
if (typeof parsed !== "object") {
|
||||
if (!parsed || typeof parsed !== "object") {
|
||||
throw new Error("Cannot update resource to string or number");
|
||||
}
|
||||
|
||||
@ -26,7 +26,7 @@ export async function update(resource: object | string): Promise<KubeJsonApiData
|
||||
return apiBase.post<KubeJsonApiData>("/stack", { data: resource });
|
||||
}
|
||||
|
||||
export async function patch(name: string, kind: string, ns: string, patch: Patch): Promise<KubeJsonApiData> {
|
||||
export async function patch(name: string, kind: string, ns: string | undefined, patch: Patch): Promise<KubeJsonApiData> {
|
||||
return apiBase.patch<KubeJsonApiData>("/stack", {
|
||||
data: {
|
||||
name,
|
||||
|
||||
@ -4,12 +4,11 @@
|
||||
*/
|
||||
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { DerivedKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export interface IResourceQuotaValues {
|
||||
[quota: string]: string;
|
||||
|
||||
export type IResourceQuotaValues = Partial<Record<string, string>> & {
|
||||
// Compute Resource Quota
|
||||
"limits.cpu"?: string;
|
||||
"limits.memory"?: string;
|
||||
@ -33,46 +32,43 @@ export interface IResourceQuotaValues {
|
||||
"count/jobs.batch"?: string;
|
||||
"count/cronjobs.batch"?: string;
|
||||
"count/deployments.extensions"?: string;
|
||||
}
|
||||
};
|
||||
|
||||
export interface ResourceQuota {
|
||||
spec: {
|
||||
hard: IResourceQuotaValues;
|
||||
scopeSelector?: {
|
||||
matchExpressions: {
|
||||
operator: string;
|
||||
scopeName: string;
|
||||
values: string[];
|
||||
}[];
|
||||
};
|
||||
};
|
||||
|
||||
status: {
|
||||
hard: IResourceQuotaValues;
|
||||
used: IResourceQuotaValues;
|
||||
export interface ResourceQuotaSpec {
|
||||
hard: IResourceQuotaValues;
|
||||
scopeSelector?: {
|
||||
matchExpressions: {
|
||||
operator: string;
|
||||
scopeName: string;
|
||||
values: string[];
|
||||
}[];
|
||||
};
|
||||
}
|
||||
|
||||
export class ResourceQuota extends KubeObject {
|
||||
static kind = "ResourceQuota";
|
||||
static namespaced = true;
|
||||
static apiBase = "/api/v1/resourcequotas";
|
||||
export interface ResourceQuotaStatus {
|
||||
hard: IResourceQuotaValues;
|
||||
used: IResourceQuotaValues;
|
||||
}
|
||||
|
||||
export class ResourceQuota extends KubeObject<ResourceQuotaStatus, ResourceQuotaSpec, "namespace-scoped"> {
|
||||
static readonly kind = "ResourceQuota";
|
||||
static readonly namespaced = true;
|
||||
static readonly apiBase = "/api/v1/resourcequotas";
|
||||
|
||||
getScopeSelector() {
|
||||
const { matchExpressions = [] } = this.spec.scopeSelector || {};
|
||||
|
||||
return matchExpressions;
|
||||
return this.spec.scopeSelector?.matchExpressions ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
let resourceQuotaApi: KubeApi<ResourceQuota>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
resourceQuotaApi = new KubeApi<ResourceQuota>({
|
||||
objectConstructor: ResourceQuota,
|
||||
});
|
||||
export class ResourceQuotaApi extends KubeApi<ResourceQuota> {
|
||||
constructor(opts: DerivedKubeApiOptions = {}) {
|
||||
super({
|
||||
objectConstructor: ResourceQuota,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
resourceQuotaApi,
|
||||
};
|
||||
export const resourceQuotaApi = isClusterPageContext()
|
||||
? new ResourceQuotaApi()
|
||||
: undefined as never;
|
||||
|
||||
@ -3,38 +3,32 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { autoBind } from "../../utils";
|
||||
import type { KubeObjectMetadata } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { DerivedKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import type { RoleRef } from "./types/role-ref";
|
||||
import type { Subject } from "./types/subject";
|
||||
|
||||
export type RoleBindingSubjectKind = "Group" | "ServiceAccount" | "User";
|
||||
|
||||
export interface RoleBindingSubject {
|
||||
kind: RoleBindingSubjectKind;
|
||||
name: string;
|
||||
namespace?: string;
|
||||
apiGroup?: string;
|
||||
export interface RoleBindingData extends KubeJsonApiData<KubeObjectMetadata<"namespace-scoped">, void, void> {
|
||||
subjects?: Subject[];
|
||||
roleRef: RoleRef;
|
||||
}
|
||||
|
||||
export interface RoleBinding {
|
||||
subjects?: RoleBindingSubject[];
|
||||
roleRef: {
|
||||
kind: string;
|
||||
name: string;
|
||||
apiGroup?: string;
|
||||
};
|
||||
}
|
||||
export class RoleBinding extends KubeObject<void, void, "namespace-scoped"> {
|
||||
static readonly kind = "RoleBinding";
|
||||
static readonly namespaced = true;
|
||||
static readonly apiBase = "/apis/rbac.authorization.k8s.io/v1/rolebindings";
|
||||
|
||||
export class RoleBinding extends KubeObject {
|
||||
static kind = "RoleBinding";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/rbac.authorization.k8s.io/v1/rolebindings";
|
||||
subjects?: Subject[];
|
||||
roleRef: RoleRef;
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
constructor({ subjects, roleRef, ...rest }: RoleBindingData) {
|
||||
super(rest);
|
||||
this.subjects = subjects;
|
||||
this.roleRef = roleRef;
|
||||
}
|
||||
|
||||
getSubjects() {
|
||||
@ -46,14 +40,15 @@ export class RoleBinding extends KubeObject {
|
||||
}
|
||||
}
|
||||
|
||||
let roleBindingApi: KubeApi<RoleBinding>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
roleBindingApi = new KubeApi({
|
||||
objectConstructor: RoleBinding,
|
||||
});
|
||||
export class RoleBindingApi extends KubeApi<RoleBinding, RoleBindingData> {
|
||||
constructor(opts: DerivedKubeApiOptions = {}) {
|
||||
super({
|
||||
...opts,
|
||||
objectConstructor: RoleBinding,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
roleBindingApi,
|
||||
};
|
||||
export const roleBindingApi = isClusterPageContext()
|
||||
? new RoleBindingApi()
|
||||
: undefined as never;
|
||||
|
||||
@ -3,37 +3,43 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { KubeObjectMetadata } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { DerivedKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import type { PolicyRule } from "./types/policy-rule";
|
||||
|
||||
export interface Role {
|
||||
rules: {
|
||||
verbs: string[];
|
||||
apiGroups: string[];
|
||||
resources: string[];
|
||||
resourceNames?: string[];
|
||||
}[];
|
||||
export interface RoleData extends KubeJsonApiData<KubeObjectMetadata<"namespace-scoped">, void, void> {
|
||||
rules?: PolicyRule[];
|
||||
}
|
||||
|
||||
export class Role extends KubeObject {
|
||||
static kind = "Role";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/rbac.authorization.k8s.io/v1/roles";
|
||||
export class Role extends KubeObject<void, void, "namespace-scoped"> {
|
||||
static readonly kind = "Role";
|
||||
static readonly namespaced = true;
|
||||
static readonly apiBase = "/apis/rbac.authorization.k8s.io/v1/roles";
|
||||
rules?: PolicyRule[];
|
||||
|
||||
constructor({ rules, ...rest }: RoleData) {
|
||||
super(rest);
|
||||
this.rules = rules;
|
||||
}
|
||||
|
||||
getRules() {
|
||||
return this.rules || [];
|
||||
}
|
||||
}
|
||||
|
||||
let roleApi: KubeApi<Role>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
roleApi = new KubeApi<Role>({
|
||||
objectConstructor: Role,
|
||||
});
|
||||
export class RoleApi extends KubeApi<Role, RoleData> {
|
||||
constructor(opts: DerivedKubeApiOptions = {}) {
|
||||
super({
|
||||
...opts,
|
||||
objectConstructor: Role,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export{
|
||||
roleApi,
|
||||
};
|
||||
export const roleApi = isClusterPageContext()
|
||||
? new RoleApi()
|
||||
: undefined as never;
|
||||
|
||||
@ -3,9 +3,11 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { KubeObjectMetadata } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { autoBind } from "../../utils";
|
||||
import type { DerivedKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
@ -20,34 +22,30 @@ export enum SecretType {
|
||||
BootstrapToken = "bootstrap.kubernetes.io/token",
|
||||
}
|
||||
|
||||
export interface ISecretRef {
|
||||
key?: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface SecretReference {
|
||||
name: string;
|
||||
namespace?: string;
|
||||
}
|
||||
|
||||
export interface SecretData extends KubeJsonApiData {
|
||||
export interface SecretData extends KubeJsonApiData<KubeObjectMetadata<"namespace-scoped">, void, void> {
|
||||
type: SecretType;
|
||||
data?: Record<string, string>;
|
||||
data?: Partial<Record<string, string>>;
|
||||
}
|
||||
|
||||
export class Secret extends KubeObject {
|
||||
static kind = "Secret";
|
||||
static namespaced = true;
|
||||
static apiBase = "/api/v1/secrets";
|
||||
export class Secret extends KubeObject<void, void, "namespace-scoped"> {
|
||||
static readonly kind = "Secret";
|
||||
static readonly namespaced = true;
|
||||
static readonly apiBase = "/api/v1/secrets";
|
||||
|
||||
declare type: SecretType;
|
||||
declare data: Record<string, string>;
|
||||
type: SecretType;
|
||||
data: Partial<Record<string, string>>;
|
||||
|
||||
constructor(data: SecretData) {
|
||||
super(data);
|
||||
constructor({ data = {}, type, ...rest }: SecretData) {
|
||||
super(rest);
|
||||
autoBind(this);
|
||||
|
||||
this.data ??= {};
|
||||
this.data = data;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
getKeys(): string[] {
|
||||
@ -59,14 +57,15 @@ export class Secret extends KubeObject {
|
||||
}
|
||||
}
|
||||
|
||||
let secretsApi: KubeApi<Secret>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
secretsApi = new KubeApi({
|
||||
objectConstructor: Secret,
|
||||
});
|
||||
export class SecretApi extends KubeApi<Secret, SecretData> {
|
||||
constructor(options: DerivedKubeApiOptions = {}) {
|
||||
super({
|
||||
...options,
|
||||
objectConstructor: Secret,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
secretsApi,
|
||||
};
|
||||
export const secretApi = isClusterPageContext()
|
||||
? new SecretApi()
|
||||
: undefined as never;
|
||||
|
||||
@ -8,7 +8,7 @@ import { KubeApi } from "../kube-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export class SelfSubjectRulesReviewApi extends KubeApi<SelfSubjectRulesReview> {
|
||||
create({ namespace = "default" }): Promise<SelfSubjectRulesReview> {
|
||||
create({ namespace = "default" }) {
|
||||
return super.create({}, {
|
||||
spec: {
|
||||
namespace,
|
||||
|
||||
@ -3,29 +3,38 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { autoBind } from "../../utils";
|
||||
import type { KubeObjectMetadata, LocalObjectReference, ObjectReference } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { DerivedKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export interface ServiceAccount {
|
||||
secrets?: {
|
||||
name: string;
|
||||
}[];
|
||||
imagePullSecrets?: {
|
||||
name: string;
|
||||
}[];
|
||||
export interface ServiceAccountData extends KubeJsonApiData<KubeObjectMetadata<"namespace-scoped">, void, void> {
|
||||
automountServiceAccountToken?: boolean;
|
||||
imagePullSecrets?: LocalObjectReference[];
|
||||
secrets?: ObjectReference[];
|
||||
}
|
||||
|
||||
export class ServiceAccount extends KubeObject {
|
||||
static kind = "ServiceAccount";
|
||||
static namespaced = true;
|
||||
static apiBase = "/api/v1/serviceaccounts";
|
||||
export class ServiceAccount extends KubeObject<void, void, "namespace-scoped"> {
|
||||
static readonly kind = "ServiceAccount";
|
||||
static readonly namespaced = true;
|
||||
static readonly apiBase = "/api/v1/serviceaccounts";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
automountServiceAccountToken?: boolean;
|
||||
imagePullSecrets?: LocalObjectReference[];
|
||||
secrets?: ObjectReference[];
|
||||
|
||||
constructor({
|
||||
automountServiceAccountToken,
|
||||
imagePullSecrets,
|
||||
secrets,
|
||||
...rest
|
||||
}: ServiceAccountData) {
|
||||
super(rest);
|
||||
this.automountServiceAccountToken = automountServiceAccountToken;
|
||||
this.imagePullSecrets = imagePullSecrets;
|
||||
this.secrets = secrets;
|
||||
}
|
||||
|
||||
getSecrets() {
|
||||
@ -37,14 +46,15 @@ export class ServiceAccount extends KubeObject {
|
||||
}
|
||||
}
|
||||
|
||||
let serviceAccountsApi: KubeApi<ServiceAccount>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
serviceAccountsApi = new KubeApi<ServiceAccount>({
|
||||
objectConstructor: ServiceAccount,
|
||||
});
|
||||
export class ServiceAccountApi extends KubeApi<ServiceAccount, ServiceAccountData> {
|
||||
constructor(opts: DerivedKubeApiOptions = {}) {
|
||||
super({
|
||||
...opts,
|
||||
objectConstructor: ServiceAccount,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
serviceAccountsApi,
|
||||
};
|
||||
export const serviceAccountApi = isClusterPageContext()
|
||||
? new ServiceAccountApi()
|
||||
: undefined as never;
|
||||
|
||||
@ -3,10 +3,9 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { autoBind } from "../../../renderer/utils";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { DerivedKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export interface ServicePort {
|
||||
@ -31,47 +30,40 @@ export class ServicePort {
|
||||
}
|
||||
}
|
||||
|
||||
export interface Service {
|
||||
spec: {
|
||||
type: string;
|
||||
clusterIP: string;
|
||||
clusterIPs?: string[];
|
||||
externalTrafficPolicy?: string;
|
||||
externalName?: string;
|
||||
loadBalancerIP?: string;
|
||||
loadBalancerSourceRanges?: string[];
|
||||
sessionAffinity: string;
|
||||
selector: { [key: string]: string };
|
||||
ports: ServicePort[];
|
||||
healthCheckNodePort?: number;
|
||||
externalIPs?: string[]; // https://kubernetes.io/docs/concepts/services-networking/service/#external-ips
|
||||
topologyKeys?: string[];
|
||||
ipFamilies?: string[];
|
||||
ipFamilyPolicy?: string;
|
||||
allocateLoadBalancerNodePorts?: boolean;
|
||||
loadBalancerClass?: string;
|
||||
internalTrafficPolicy?: string;
|
||||
};
|
||||
export interface ServiceSpec {
|
||||
type: string;
|
||||
clusterIP: string;
|
||||
clusterIPs?: string[];
|
||||
externalTrafficPolicy?: string;
|
||||
externalName?: string;
|
||||
loadBalancerIP?: string;
|
||||
loadBalancerSourceRanges?: string[];
|
||||
sessionAffinity: string;
|
||||
selector: Partial<Record<string, string>>;
|
||||
ports: ServicePort[];
|
||||
healthCheckNodePort?: number;
|
||||
externalIPs?: string[]; // https://kubernetes.io/docs/concepts/services-networking/service/#external-ips
|
||||
topologyKeys?: string[];
|
||||
ipFamilies?: string[];
|
||||
ipFamilyPolicy?: string;
|
||||
allocateLoadBalancerNodePorts?: boolean;
|
||||
loadBalancerClass?: string;
|
||||
internalTrafficPolicy?: string;
|
||||
}
|
||||
|
||||
status: {
|
||||
loadBalancer?: {
|
||||
ingress?: {
|
||||
ip?: string;
|
||||
hostname?: string;
|
||||
}[];
|
||||
};
|
||||
export interface ServiceStatus {
|
||||
loadBalancer?: {
|
||||
ingress?: {
|
||||
ip?: string;
|
||||
hostname?: string;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
|
||||
export class Service extends KubeObject {
|
||||
static kind = "Service";
|
||||
static namespaced = true;
|
||||
static apiBase = "/api/v1/services";
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
}
|
||||
export class Service extends KubeObject<ServiceStatus, ServiceSpec, "namespace-scoped"> {
|
||||
static readonly kind = "Service";
|
||||
static readonly namespaced = true;
|
||||
static readonly apiBase = "/api/v1/services";
|
||||
|
||||
getClusterIp() {
|
||||
return this.spec.clusterIP;
|
||||
@ -112,7 +104,7 @@ export class Service extends KubeObject {
|
||||
}
|
||||
|
||||
getLoadBalancer() {
|
||||
return this.status.loadBalancer;
|
||||
return this.status?.loadBalancer;
|
||||
}
|
||||
|
||||
isActive() {
|
||||
@ -132,14 +124,15 @@ export class Service extends KubeObject {
|
||||
}
|
||||
}
|
||||
|
||||
let serviceApi: KubeApi<Service>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
serviceApi = new KubeApi<Service>({
|
||||
objectConstructor: Service,
|
||||
});
|
||||
export class ServiceApi extends KubeApi<Service> {
|
||||
constructor(opts: DerivedKubeApiOptions = {}) {
|
||||
super({
|
||||
...opts,
|
||||
objectConstructor: Service,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
serviceApi,
|
||||
};
|
||||
export const serviceApi = isClusterPageContext()
|
||||
? new ServiceApi()
|
||||
: undefined as never;
|
||||
|
||||
@ -3,17 +3,24 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { IAffinity } from "../workload-kube-object";
|
||||
import { WorkloadKubeObject } from "../workload-kube-object";
|
||||
import { autoBind } from "../../utils";
|
||||
import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import { metricsApi } from "./metrics.api";
|
||||
import type { IPodMetrics } from "./pods.api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import type { PodMetricData } from "./pods.api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import type { LabelSelector } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { PodTemplateSpec } from "./types/pod-template-spec";
|
||||
import type { PersistentVolumeClaimTemplateSpec } from "./types/persistent-volume-claim-template-spec";
|
||||
|
||||
export class StatefulSetApi extends KubeApi<StatefulSet> {
|
||||
constructor(opts: DerivedKubeApiOptions & IgnoredKubeApiOptions = {}) {
|
||||
super({
|
||||
...opts,
|
||||
objectConstructor: StatefulSet,
|
||||
});
|
||||
}
|
||||
|
||||
protected getScaleApiUrl(params: { namespace: string; name: string }) {
|
||||
return `${this.getUrl(params)}/scale`;
|
||||
}
|
||||
@ -40,7 +47,7 @@ export class StatefulSetApi extends KubeApi<StatefulSet> {
|
||||
}
|
||||
}
|
||||
|
||||
export function getMetricsForStatefulSets(statefulSets: StatefulSet[], namespace: string, selector = ""): Promise<IPodMetrics> {
|
||||
export function getMetricsForStatefulSets(statefulSets: StatefulSet[], namespace: string, selector = ""): Promise<PodMetricData> {
|
||||
const podSelector = statefulSets.map(statefulset => `${statefulset.getName()}-[[:digit:]]+`).join("|");
|
||||
const opts = { category: "pods", pods: podSelector, namespace, selector };
|
||||
|
||||
@ -57,74 +64,52 @@ export function getMetricsForStatefulSets(statefulSets: StatefulSet[], namespace
|
||||
});
|
||||
}
|
||||
|
||||
export class StatefulSet extends WorkloadKubeObject {
|
||||
static kind = "StatefulSet";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/apps/v1/statefulsets";
|
||||
export interface StatefulSetSpec {
|
||||
serviceName: string;
|
||||
replicas: number;
|
||||
selector: LabelSelector;
|
||||
template: PodTemplateSpec;
|
||||
volumeClaimTemplates: PersistentVolumeClaimTemplateSpec[];
|
||||
}
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
autoBind(this);
|
||||
export interface StatefulSetStatus {
|
||||
observedGeneration: number;
|
||||
replicas: number;
|
||||
currentReplicas: number;
|
||||
readyReplicas: number;
|
||||
currentRevision: string;
|
||||
updateRevision: string;
|
||||
collisionCount: number;
|
||||
}
|
||||
|
||||
export class StatefulSet extends KubeObject<StatefulSetStatus, StatefulSetSpec, "namespace-scoped"> {
|
||||
static readonly kind = "StatefulSet";
|
||||
static readonly namespaced = true;
|
||||
static readonly apiBase = "/apis/apps/v1/statefulsets";
|
||||
|
||||
getSelectors(): string[] {
|
||||
return KubeObject.stringifyLabels(this.spec.selector.matchLabels);
|
||||
}
|
||||
|
||||
declare spec: {
|
||||
serviceName: string;
|
||||
replicas: number;
|
||||
selector: LabelSelector;
|
||||
template: {
|
||||
metadata: {
|
||||
labels: {
|
||||
app: string;
|
||||
};
|
||||
};
|
||||
spec: {
|
||||
containers: null | {
|
||||
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;
|
||||
};
|
||||
getNodeSelectors(): string[] {
|
||||
return KubeObject.stringifyLabels(this.spec.template.spec?.nodeSelector);
|
||||
}
|
||||
|
||||
getTemplateLabels(): string[] {
|
||||
return KubeObject.stringifyLabels(this.spec.template.metadata?.labels);
|
||||
}
|
||||
|
||||
getTolerations() {
|
||||
return this.spec.template.spec?.tolerations ?? [];
|
||||
}
|
||||
|
||||
getAffinity() {
|
||||
return this.spec.template.spec?.affinity ?? {};
|
||||
}
|
||||
|
||||
getAffinityNumber() {
|
||||
return Object.keys(this.getAffinity()).length;
|
||||
}
|
||||
|
||||
getReplicas() {
|
||||
return this.spec.replicas || 0;
|
||||
@ -137,14 +122,6 @@ export class StatefulSet extends WorkloadKubeObject {
|
||||
}
|
||||
}
|
||||
|
||||
let statefulSetApi: StatefulSetApi;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
statefulSetApi = new StatefulSetApi({
|
||||
objectConstructor: StatefulSet,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
statefulSetApi,
|
||||
};
|
||||
export const statefulSetApi = isClusterPageContext()
|
||||
? new StatefulSetApi()
|
||||
: undefined as never;
|
||||
|
||||
@ -4,29 +4,64 @@
|
||||
*/
|
||||
|
||||
import { autoBind } from "../../utils";
|
||||
import type { KubeObjectMetadata } from "../kube-object";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { DerivedKubeApiOptions } from "../kube-api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
|
||||
export interface 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 interface TopologySelectorLabelRequirement {
|
||||
key: string;
|
||||
values: string[];
|
||||
}
|
||||
|
||||
export class StorageClass extends KubeObject {
|
||||
static kind = "StorageClass";
|
||||
static namespaced = false;
|
||||
static apiBase = "/apis/storage.k8s.io/v1/storageclasses";
|
||||
export interface TopologySelectorTerm {
|
||||
matchLabelExpressions?: TopologySelectorLabelRequirement[];
|
||||
}
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
export interface StorageClassData extends KubeJsonApiData<KubeObjectMetadata<"cluster-scoped">, void, void> {
|
||||
allowVolumeExpansion?: boolean;
|
||||
allowedTopologies?: TopologySelectorTerm[];
|
||||
mountOptions?: string[];
|
||||
parameters?: Partial<Record<string, string>>;
|
||||
provisioner: string;
|
||||
reclaimPolicy?: string;
|
||||
volumeBindingMode?: string;
|
||||
}
|
||||
|
||||
export class StorageClass extends KubeObject<void, void, "cluster-scoped"> {
|
||||
static readonly kind = "StorageClass";
|
||||
static readonly namespaced = false;
|
||||
static readonly apiBase = "/apis/storage.k8s.io/v1/storageclasses";
|
||||
|
||||
allowVolumeExpansion?: boolean;
|
||||
allowedTopologies: TopologySelectorTerm[];
|
||||
mountOptions: string[];
|
||||
parameters: Partial<Record<string, string>>;
|
||||
provisioner: string;
|
||||
reclaimPolicy: string;
|
||||
volumeBindingMode?: string;
|
||||
|
||||
constructor({
|
||||
allowVolumeExpansion,
|
||||
allowedTopologies = [],
|
||||
mountOptions = [],
|
||||
parameters = {},
|
||||
provisioner,
|
||||
reclaimPolicy = "Delete",
|
||||
volumeBindingMode,
|
||||
...rest
|
||||
}: StorageClassData) {
|
||||
super(rest);
|
||||
autoBind(this);
|
||||
this.allowVolumeExpansion = allowVolumeExpansion;
|
||||
this.allowedTopologies = allowedTopologies;
|
||||
this.mountOptions = mountOptions;
|
||||
this.parameters = parameters;
|
||||
this.provisioner = provisioner;
|
||||
this.reclaimPolicy = reclaimPolicy;
|
||||
this.volumeBindingMode = volumeBindingMode;
|
||||
}
|
||||
|
||||
isDefault() {
|
||||
@ -47,14 +82,15 @@ export class StorageClass extends KubeObject {
|
||||
}
|
||||
}
|
||||
|
||||
let storageClassApi: KubeApi<StorageClass>;
|
||||
|
||||
if (isClusterPageContext()) {
|
||||
storageClassApi = new KubeApi({
|
||||
objectConstructor: StorageClass,
|
||||
});
|
||||
export class StorageClassApi extends KubeApi<StorageClass, StorageClassData> {
|
||||
constructor(opts: DerivedKubeApiOptions = {}) {
|
||||
super({
|
||||
...opts,
|
||||
objectConstructor: StorageClass,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
storageClassApi,
|
||||
};
|
||||
export const storageClassApi = isClusterPageContext()
|
||||
? new StorageClassApi()
|
||||
: undefined as never;
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
// Clone json-serializable object
|
||||
import type { LabelSelector } from "../../kube-object";
|
||||
|
||||
export function cloneJsonObject<T = object>(obj: T): T {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
export interface AggregationRule {
|
||||
clusterRoleSelectors?: LabelSelector;
|
||||
}
|
||||
12
src/common/k8s-api/endpoints/types/job-template-spec.ts
Normal file
12
src/common/k8s-api/endpoints/types/job-template-spec.ts
Normal file
@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { KubeTemplateObjectMetadata } from "../../kube-object";
|
||||
import type { JobSpec } from "../job.api";
|
||||
|
||||
export interface JobTemplateSpec {
|
||||
metadata?: KubeTemplateObjectMetadata<"namespace-scoped">;
|
||||
spec?: JobSpec;
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { KubeTemplateObjectMetadata } from "../../kube-object";
|
||||
import type { PersistentVolumeSpec } from "../persistent-volume.api";
|
||||
|
||||
export interface PersistentVolumeClaimTemplateSpec {
|
||||
metadata?: KubeTemplateObjectMetadata<"cluster-scoped">;
|
||||
spec?: PersistentVolumeSpec;
|
||||
}
|
||||
12
src/common/k8s-api/endpoints/types/pod-template-spec.ts
Normal file
12
src/common/k8s-api/endpoints/types/pod-template-spec.ts
Normal file
@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { KubeTemplateObjectMetadata } from "../../kube-object";
|
||||
import type { PodSpec } from "../pods.api";
|
||||
|
||||
export interface PodTemplateSpec {
|
||||
metadata?: KubeTemplateObjectMetadata<"namespace-scoped">;
|
||||
spec?: PodSpec;
|
||||
}
|
||||
12
src/common/k8s-api/endpoints/types/policy-rule.ts
Normal file
12
src/common/k8s-api/endpoints/types/policy-rule.ts
Normal file
@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
export interface PolicyRule {
|
||||
verbs: string[];
|
||||
apiGroups?: string[];
|
||||
resources?: string[];
|
||||
resourceNames?: string[];
|
||||
nonResourceURLs?: string[];
|
||||
}
|
||||
25
src/common/k8s-api/endpoints/types/resource-requirements.ts
Normal file
25
src/common/k8s-api/endpoints/types/resource-requirements.ts
Normal file
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
/**
|
||||
* ResourceRequirements describes the compute resource requirements.
|
||||
*/
|
||||
export interface ResourceRequirements {
|
||||
/**
|
||||
* Limits describes the maximum amount of compute resources allowed.
|
||||
*
|
||||
* More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
|
||||
*/
|
||||
limits?: Partial<Record<string, string>>;
|
||||
|
||||
/**
|
||||
* Requests describes the minimum amount of compute resources required. If Requests is omitted
|
||||
* for a container, it defaults to Limits if that is explicitly specified, otherwise to an
|
||||
* implementation-defined value.
|
||||
*
|
||||
* More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
|
||||
*/
|
||||
requests?: Partial<Record<string, string>>;
|
||||
}
|
||||
@ -3,8 +3,8 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import fetch from "node-fetch";
|
||||
|
||||
export {
|
||||
fetch,
|
||||
};
|
||||
export interface RoleRef {
|
||||
apiGroup: string;
|
||||
kind: string;
|
||||
name: string;
|
||||
}
|
||||
13
src/common/k8s-api/endpoints/types/subject.ts
Normal file
13
src/common/k8s-api/endpoints/types/subject.ts
Normal file
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
export type SubjectKind = "Group" | "ServiceAccount" | "User";
|
||||
|
||||
export interface Subject {
|
||||
apiGroup?: string;
|
||||
kind: SubjectKind;
|
||||
name: string;
|
||||
namespace?: string;
|
||||
}
|
||||
@ -13,6 +13,8 @@ import fetch from "node-fetch";
|
||||
import { stringify } from "querystring";
|
||||
import { EventEmitter } from "../../common/event-emitter";
|
||||
import logger from "../../common/logger";
|
||||
import type { Defaulted } from "../utils";
|
||||
import { json } from "../utils";
|
||||
|
||||
export interface JsonApiData {}
|
||||
|
||||
@ -35,38 +37,40 @@ export interface JsonApiLog {
|
||||
error?: any;
|
||||
}
|
||||
|
||||
export type GetRequestOptions = () => Promise<RequestInit>;
|
||||
|
||||
export interface JsonApiConfig {
|
||||
apiBase: string;
|
||||
serverAddress: string;
|
||||
debug?: boolean;
|
||||
getRequestOptions?: () => Promise<RequestInit>;
|
||||
getRequestOptions?: GetRequestOptions;
|
||||
}
|
||||
|
||||
const httpAgent = new HttpAgent({ keepAlive: true });
|
||||
const httpsAgent = new HttpsAgent({ keepAlive: true });
|
||||
|
||||
export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
|
||||
static reqInitDefault: RequestInit = {
|
||||
static readonly reqInitDefault = {
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
};
|
||||
protected readonly reqInit: Defaulted<RequestInit, keyof typeof JsonApi["reqInitDefault"]>;
|
||||
|
||||
static configDefault: Partial<JsonApiConfig> = {
|
||||
static readonly configDefault: Partial<JsonApiConfig> = {
|
||||
debug: false,
|
||||
};
|
||||
|
||||
constructor(public readonly config: JsonApiConfig, protected reqInit?: RequestInit) {
|
||||
constructor(public readonly config: JsonApiConfig, reqInit?: RequestInit) {
|
||||
this.config = Object.assign({}, JsonApi.configDefault, config);
|
||||
this.reqInit = merge({}, JsonApi.reqInitDefault, reqInit);
|
||||
this.parseResponse = this.parseResponse.bind(this);
|
||||
this.getRequestOptions = config.getRequestOptions ?? (() => Promise.resolve({}));
|
||||
}
|
||||
|
||||
public onData = new EventEmitter<[D, Response]>();
|
||||
public onError = new EventEmitter<[JsonApiErrorParsed, Response]>();
|
||||
|
||||
private getRequestOptions: JsonApiConfig["getRequestOptions"];
|
||||
public readonly onData = new EventEmitter<[D, Response]>();
|
||||
public readonly onError = new EventEmitter<[JsonApiErrorParsed, Response]>();
|
||||
private readonly getRequestOptions: GetRequestOptions;
|
||||
|
||||
get<T = D>(path: string, params?: P, reqInit: RequestInit = {}) {
|
||||
return this.request<T>(path, params, { ...reqInit, method: "get" });
|
||||
@ -74,22 +78,17 @@ export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
|
||||
|
||||
async getResponse(path: string, params?: P, init: RequestInit = {}): Promise<Response> {
|
||||
let reqUrl = `${this.config.serverAddress}${this.config.apiBase}${path}`;
|
||||
const reqInit: RequestInit = merge(
|
||||
{},
|
||||
const reqInit = merge(
|
||||
{
|
||||
method: "get",
|
||||
agent: reqUrl.startsWith("https:") ? httpsAgent : httpAgent,
|
||||
},
|
||||
this.reqInit,
|
||||
await this.getRequestOptions(),
|
||||
init,
|
||||
);
|
||||
const { query } = params || {} as P;
|
||||
|
||||
if (!reqInit.method) {
|
||||
reqInit.method = "get";
|
||||
}
|
||||
|
||||
if (!reqInit.agent) {
|
||||
reqInit.agent = reqUrl.startsWith("https:") ? httpsAgent : httpAgent;
|
||||
}
|
||||
|
||||
if (query) {
|
||||
const queryString = stringify(query);
|
||||
|
||||
@ -115,9 +114,9 @@ export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
|
||||
return this.request<T>(path, params, { ...reqInit, method: "delete" });
|
||||
}
|
||||
|
||||
protected async request<D>(path: string, params?: P, init: RequestInit = {}) {
|
||||
protected async request<D>(path: string, params: P | undefined, init: Defaulted<RequestInit, "method">) {
|
||||
let reqUrl = `${this.config.serverAddress}${this.config.apiBase}${path}`;
|
||||
const reqInit: RequestInit = merge(
|
||||
const reqInit = merge(
|
||||
{},
|
||||
this.reqInit,
|
||||
await this.getRequestOptions(),
|
||||
@ -149,10 +148,10 @@ export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
|
||||
const { status } = res;
|
||||
|
||||
const text = await res.text();
|
||||
let data;
|
||||
let data: any;
|
||||
|
||||
try {
|
||||
data = text ? JSON.parse(text) : ""; // DELETE-requests might not have response-body
|
||||
data = text ? json.parse(text) : ""; // DELETE-requests might not have response-body
|
||||
} catch (e) {
|
||||
data = text;
|
||||
}
|
||||
|
||||
@ -6,16 +6,6 @@
|
||||
// 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;
|
||||
@ -32,35 +22,21 @@ export interface IKubeApiParsed extends IKubeApiLinkRef {
|
||||
}
|
||||
|
||||
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;
|
||||
let apiGroup!: string;
|
||||
let apiVersion!: string;
|
||||
let namespace!: string;
|
||||
let resource!: string;
|
||||
let name!: string;
|
||||
|
||||
if (namespaced) {
|
||||
switch (right.length) {
|
||||
case 1:
|
||||
name = right[0];
|
||||
// fallthrough
|
||||
// fallthrough
|
||||
case 0:
|
||||
resource = "namespaces"; // special case this due to `split` removing namespaces
|
||||
break;
|
||||
@ -69,7 +45,8 @@ function _parseKubeApi(path: string): IKubeApiParsed {
|
||||
break;
|
||||
}
|
||||
|
||||
apiVersion = left.pop();
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
apiVersion = left.pop()!;
|
||||
apiGroup = left.join("/");
|
||||
} else {
|
||||
switch (left.length) {
|
||||
@ -79,10 +56,12 @@ function _parseKubeApi(path: string): IKubeApiParsed {
|
||||
[apiGroup, apiVersion, resource, name] = left;
|
||||
break;
|
||||
case 2:
|
||||
resource = left.pop();
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
resource = left.pop()!;
|
||||
// fallthrough
|
||||
case 1:
|
||||
apiVersion = left.pop();
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
apiVersion = left.pop()!;
|
||||
apiGroup = "";
|
||||
break;
|
||||
default:
|
||||
|
||||
@ -12,30 +12,51 @@ import logger from "../../main/logger";
|
||||
import { apiManager } from "./api-manager";
|
||||
import { apiBase, apiKube } from "./index";
|
||||
import { createKubeApiURL, parseKubeApi } from "./kube-api-parse";
|
||||
import type { KubeObjectConstructor } from "./kube-object";
|
||||
import { KubeObject, KubeStatus } from "./kube-object";
|
||||
import type { KubeObjectConstructor, KubeJsonApiDataFor, KubeObjectMetadata, KubeObjectScope } from "./kube-object";
|
||||
import { KubeObject, KubeStatus, isKubeStatusData } from "./kube-object";
|
||||
import byline from "byline";
|
||||
import type { IKubeWatchEvent } from "./kube-watch-event";
|
||||
import type { KubeJsonApiData } from "./kube-json-api";
|
||||
import { KubeJsonApi } from "./kube-json-api";
|
||||
import { noop, WrappedAbortController } from "../utils";
|
||||
import type { Disposer } from "../utils";
|
||||
import { isDefined, noop, WrappedAbortController } from "../utils";
|
||||
import type { RequestInit } from "node-fetch";
|
||||
import type AbortController from "abort-controller";
|
||||
import type { AgentOptions } from "https";
|
||||
import { Agent } from "https";
|
||||
import type { Patch } from "rfc6902";
|
||||
import assert from "assert";
|
||||
import type { PartialDeep } from "type-fest";
|
||||
|
||||
/**
|
||||
* The options used for creating a `KubeApi`
|
||||
*/
|
||||
export interface IKubeApiOptions<T extends KubeObject> {
|
||||
export interface IKubeApiOptions<T extends KubeObject<any, any, KubeObjectScope>, Data extends KubeJsonApiDataFor<T> = KubeJsonApiDataFor<T>> extends DerivedKubeApiOptions {
|
||||
/**
|
||||
* base api-path for listing all resources, e.g. "/api/v1/pods"
|
||||
*
|
||||
* If not specified then will be the one on the `objectConstructor`
|
||||
* @deprecated should be specified by `objectConstructor`
|
||||
*/
|
||||
apiBase?: string;
|
||||
|
||||
/**
|
||||
* The constructor for the kube objects returned from the API
|
||||
*/
|
||||
objectConstructor: KubeObjectConstructor<T, Data>;
|
||||
|
||||
/**
|
||||
* @deprecated should be specified by `objectConstructor`
|
||||
*/
|
||||
isNamespaced?: boolean;
|
||||
|
||||
/**
|
||||
* @deprecated should be specified by `objectConstructor`
|
||||
*/
|
||||
kind?: string;
|
||||
}
|
||||
|
||||
export interface DerivedKubeApiOptions {
|
||||
/**
|
||||
* If the API uses a different API endpoint (e.g. apiBase) depending on the cluster version,
|
||||
* fallback API bases can be listed individually.
|
||||
@ -50,27 +71,34 @@ export interface IKubeApiOptions<T extends KubeObject> {
|
||||
*/
|
||||
checkPreferredVersion?: boolean;
|
||||
|
||||
/**
|
||||
* The constructor for the kube objects returned from the API
|
||||
*/
|
||||
objectConstructor: KubeObjectConstructor<T>;
|
||||
|
||||
/**
|
||||
* The api instance to use for making requests
|
||||
*
|
||||
* @default apiKube
|
||||
*/
|
||||
request?: KubeJsonApi;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This type is only present for backwards compatable typescript support
|
||||
*/
|
||||
export interface IgnoredKubeApiOptions {
|
||||
/**
|
||||
* @deprecated should be specified by `objectConstructor`
|
||||
* @deprecated this option is overridden and should not be used
|
||||
*/
|
||||
isNamespaced?: boolean;
|
||||
|
||||
objectConstructor?: any;
|
||||
/**
|
||||
* @deprecated should be specified by `objectConstructor`
|
||||
* @deprecated this option is overridden and should not be used
|
||||
*/
|
||||
kind?: string;
|
||||
kind?: any;
|
||||
/**
|
||||
* @deprecated this option is overridden and should not be used
|
||||
*/
|
||||
isNamespaces?: any;
|
||||
/**
|
||||
* @deprecated this option is overridden and should not be used
|
||||
*/
|
||||
apiBase?: any;
|
||||
}
|
||||
|
||||
export interface IKubeApiQueryParams {
|
||||
@ -118,10 +146,6 @@ export type PropagationPolicy = undefined | "Orphan" | "Foreground" | "Backgroun
|
||||
*/
|
||||
export interface IKubeApiCluster extends ILocalKubeApiConfig { }
|
||||
|
||||
export type PartialKubeObject<T extends KubeObject> = Partial<Omit<T, "metadata">> & {
|
||||
metadata?: Partial<T["metadata"]>;
|
||||
};
|
||||
|
||||
export interface IRemoteKubeApiConfig {
|
||||
cluster: {
|
||||
server: string;
|
||||
@ -135,7 +159,20 @@ export interface IRemoteKubeApiConfig {
|
||||
};
|
||||
}
|
||||
|
||||
export function forCluster<T extends KubeObject, Y extends KubeApi<T> = KubeApi<T>>(cluster: ILocalKubeApiConfig, kubeClass: KubeObjectConstructor<T>, apiClass: new (apiOpts: IKubeApiOptions<T>) => Y = null): KubeApi<T> {
|
||||
export function forCluster<
|
||||
T extends KubeObject<any, any, KubeObjectScope>,
|
||||
Y extends KubeApi<T>,
|
||||
Data extends KubeJsonApiDataFor<T>,
|
||||
>(cluster: ILocalKubeApiConfig, kubeClass: KubeObjectConstructor<T, Data>, apiClass: new (apiOpts: IKubeApiOptions<T>) => Y): Y;
|
||||
export function forCluster<
|
||||
T extends KubeObject<any, any, KubeObjectScope>,
|
||||
Data extends KubeJsonApiDataFor<T>,
|
||||
>(cluster: ILocalKubeApiConfig, kubeClass: KubeObjectConstructor<T, Data>, apiClass?: new (apiOpts: IKubeApiOptions<T>) => KubeApi<T>): KubeApi<T>;
|
||||
|
||||
export function forCluster<
|
||||
T extends KubeObject<any, any, KubeObjectScope>,
|
||||
Data extends KubeJsonApiDataFor<T>,
|
||||
>(cluster: ILocalKubeApiConfig, kubeClass: KubeObjectConstructor<T, Data>, apiClass: (new (apiOpts: IKubeApiOptions<T>) => KubeApi<T>) = KubeApi): KubeApi<T> {
|
||||
const url = new URL(apiBase.config.serverAddress);
|
||||
const request = new KubeJsonApi({
|
||||
serverAddress: apiBase.config.serverAddress,
|
||||
@ -147,17 +184,27 @@ export function forCluster<T extends KubeObject, Y extends KubeApi<T> = KubeApi<
|
||||
},
|
||||
});
|
||||
|
||||
if (!apiClass) {
|
||||
apiClass = KubeApi as new (apiOpts: IKubeApiOptions<T>) => Y;
|
||||
}
|
||||
|
||||
return new apiClass({
|
||||
objectConstructor: kubeClass,
|
||||
objectConstructor: kubeClass as KubeObjectConstructor<T, KubeJsonApiDataFor<T>>,
|
||||
request,
|
||||
});
|
||||
}
|
||||
|
||||
export function forRemoteCluster<T extends KubeObject, Y extends KubeApi<T> = KubeApi<T>>(config: IRemoteKubeApiConfig, kubeClass: KubeObjectConstructor<T>, apiClass: new (apiOpts: IKubeApiOptions<T>) => Y = null): Y {
|
||||
export function forRemoteCluster<
|
||||
T extends KubeObject<any, any, KubeObjectScope>,
|
||||
Y extends KubeApi<T>,
|
||||
Data extends KubeJsonApiDataFor<T>,
|
||||
>(config: IRemoteKubeApiConfig, kubeClass: KubeObjectConstructor<T, Data>, apiClass: new (apiOpts: IKubeApiOptions<T>) => Y): Y;
|
||||
export function forRemoteCluster<
|
||||
T extends KubeObject<any, any, KubeObjectScope>,
|
||||
Data extends KubeJsonApiDataFor<T>,
|
||||
>(config: IRemoteKubeApiConfig, kubeClass: KubeObjectConstructor<T, Data>, apiClass?: new (apiOpts: IKubeApiOptions<T>) => KubeApi<T>): KubeApi<T>;
|
||||
|
||||
export function forRemoteCluster<
|
||||
K extends KubeObject<any, any, KubeObjectScope>,
|
||||
Y extends KubeApi<K>,
|
||||
Data extends KubeJsonApiDataFor<K>,
|
||||
>(config: IRemoteKubeApiConfig, kubeClass: KubeObjectConstructor<K, Data>, apiClass: new (apiOpts: IKubeApiOptions<K>) => KubeApi<K> = KubeApi): KubeApi<K> {
|
||||
const reqInit: RequestInit = {};
|
||||
const agentOptions: AgentOptions = {};
|
||||
|
||||
@ -196,38 +243,26 @@ export function forRemoteCluster<T extends KubeObject, Y extends KubeApi<T> = Ku
|
||||
}, reqInit);
|
||||
|
||||
if (!apiClass) {
|
||||
apiClass = KubeApi as new (apiOpts: IKubeApiOptions<T>) => Y;
|
||||
apiClass = KubeApi as new (apiOpts: IKubeApiOptions<K>) => Y;
|
||||
}
|
||||
|
||||
return new apiClass({
|
||||
objectConstructor: kubeClass as KubeObjectConstructor<T>,
|
||||
objectConstructor: kubeClass as KubeObjectConstructor<K, KubeJsonApiDataFor<K>>,
|
||||
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<T extends KubeJsonApiData = KubeJsonApiData> = (data: IKubeWatchEvent<T>, error: any) => void;
|
||||
|
||||
export type KubeApiWatchCallback = (data: IKubeWatchEvent<KubeJsonApiData>, error: any) => void;
|
||||
|
||||
export interface KubeApiWatchOptions {
|
||||
export interface KubeApiWatchOptions<T extends KubeObject<any, any, KubeObjectScope>, Data extends KubeJsonApiDataFor<T>> {
|
||||
namespace: string;
|
||||
callback?: KubeApiWatchCallback;
|
||||
abortController?: AbortController;
|
||||
watchId?: string;
|
||||
retry?: boolean;
|
||||
callback?: KubeApiWatchCallback<Data> | undefined;
|
||||
abortController?: AbortController | undefined;
|
||||
watchId?: string | undefined;
|
||||
retry?: boolean | undefined;
|
||||
|
||||
// timeout in seconds
|
||||
timeout?: number;
|
||||
timeout?: number | undefined;
|
||||
}
|
||||
|
||||
export type KubeApiPatchType = "merge" | "json" | "strategic";
|
||||
@ -261,54 +296,74 @@ export interface DeleteResourceDescriptor extends ResourceDescriptor {
|
||||
propagationPolicy?: PropagationPolicy;
|
||||
}
|
||||
|
||||
export class KubeApi<T extends KubeObject> {
|
||||
export class KubeApi<
|
||||
K extends KubeObject = KubeObject,
|
||||
D extends KubeJsonApiDataFor<K> = KubeJsonApiDataFor<K>,
|
||||
> {
|
||||
readonly kind: string;
|
||||
readonly apiVersion: string;
|
||||
apiBase: string;
|
||||
apiPrefix: string;
|
||||
apiGroup: string;
|
||||
apiVersionPreferred?: string;
|
||||
apiVersionPreferred: string | undefined;
|
||||
readonly apiResource: string;
|
||||
readonly isNamespaced: boolean;
|
||||
|
||||
public objectConstructor: KubeObjectConstructor<T>;
|
||||
protected request: KubeJsonApi;
|
||||
protected resourceVersions = new Map<string, string>();
|
||||
protected watchDisposer: () => void;
|
||||
public readonly objectConstructor: KubeObjectConstructor<K, D>;
|
||||
protected readonly request: KubeJsonApi;
|
||||
protected readonly resourceVersions = new Map<string, string>();
|
||||
protected readonly watchDisposer: Disposer | undefined;
|
||||
private watchId = 1;
|
||||
protected readonly doCheckPreferredVersion: boolean;
|
||||
protected readonly fullApiPathname: string;
|
||||
protected readonly fallbackApiBases?: string[];
|
||||
|
||||
constructor(protected options: IKubeApiOptions<T>) {
|
||||
const { objectConstructor, request, kind, isNamespaced } = options;
|
||||
const { apiBase, apiPrefix, apiGroup, apiVersion, resource } = parseKubeApi(options.apiBase || objectConstructor.apiBase);
|
||||
constructor({
|
||||
objectConstructor,
|
||||
request = apiKube,
|
||||
kind = objectConstructor.kind,
|
||||
isNamespaced,
|
||||
apiBase: fullApiPathname = objectConstructor.apiBase,
|
||||
checkPreferredVersion: doCheckPreferredVersion = false,
|
||||
fallbackApiBases,
|
||||
}: IKubeApiOptions<K, D>) {
|
||||
assert(fullApiPathname);
|
||||
assert(request, "request MUST be provided if not in a cluster page frame context");
|
||||
|
||||
this.options = options;
|
||||
this.kind = kind ?? objectConstructor.kind;
|
||||
const { apiBase, apiPrefix, apiGroup, apiVersion, resource } = parseKubeApi(fullApiPathname);
|
||||
|
||||
assert(kind);
|
||||
assert(apiPrefix);
|
||||
|
||||
this.doCheckPreferredVersion = doCheckPreferredVersion;
|
||||
this.fallbackApiBases = fallbackApiBases;
|
||||
this.fullApiPathname = fullApiPathname;
|
||||
this.kind = kind;
|
||||
this.isNamespaced = isNamespaced ?? objectConstructor.namespaced ?? false;
|
||||
this.apiBase = apiBase;
|
||||
this.apiPrefix = apiPrefix;
|
||||
this.apiGroup = apiGroup;
|
||||
this.apiVersion = apiVersion;
|
||||
this.apiResource = resource;
|
||||
this.request = request ?? apiKube;
|
||||
this.request = request;
|
||||
this.objectConstructor = objectConstructor;
|
||||
|
||||
this.parseResponse = this.parseResponse.bind(this);
|
||||
apiManager.registerApi(apiBase, this);
|
||||
apiManager.registerApi<KubeApi<K, D>>(apiBase, this);
|
||||
}
|
||||
|
||||
get apiVersionWithGroup() {
|
||||
return [this.apiGroup, this.apiVersionPreferred ?? this.apiVersion]
|
||||
.filter(Boolean)
|
||||
.filter(isDefined)
|
||||
.join("/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the latest API prefix/group that contains the required resource.
|
||||
* First tries options.apiBase, then urls in order from options.fallbackApiBases.
|
||||
* First tries fullApiPathname, then urls in order from fallbackApiBases.
|
||||
*/
|
||||
private async getLatestApiPrefixGroup() {
|
||||
// Note that this.options.apiBase is the "full" url, whereas this.apiBase is parsed
|
||||
const apiBases = [this.options.apiBase, this.objectConstructor.apiBase, ...this.options.fallbackApiBases];
|
||||
// Note that this.fullApiPathname is the "full" url, whereas this.apiBase is parsed
|
||||
const apiBases = [this.fullApiPathname, this.objectConstructor.apiBase, ...this.fallbackApiBases ?? []];
|
||||
|
||||
for (const apiUrl of apiBases) {
|
||||
if (!apiUrl) {
|
||||
@ -338,7 +393,7 @@ export class KubeApi<T extends KubeObject> {
|
||||
* Get the apiPrefix and apiGroup to be used for fetching the preferred version.
|
||||
*/
|
||||
private async getPreferredVersionPrefixGroup() {
|
||||
if (this.options.fallbackApiBases) {
|
||||
if (this.fallbackApiBases) {
|
||||
try {
|
||||
return await this.getLatestApiPrefixGroup();
|
||||
} catch (error) {
|
||||
@ -354,25 +409,27 @@ export class KubeApi<T extends KubeObject> {
|
||||
}
|
||||
|
||||
protected async checkPreferredVersion() {
|
||||
if (this.options.fallbackApiBases && !this.options.checkPreferredVersion) {
|
||||
if (this.fallbackApiBases && !this.doCheckPreferredVersion) {
|
||||
throw new Error("checkPreferredVersion must be enabled if fallbackApiBases is set in KubeApi");
|
||||
}
|
||||
|
||||
if (this.options.checkPreferredVersion && this.apiVersionPreferred === undefined) {
|
||||
if (this.doCheckPreferredVersion && this.apiVersionPreferred === undefined) {
|
||||
const { apiPrefix, apiGroup } = await this.getPreferredVersionPrefixGroup();
|
||||
|
||||
assert(apiPrefix);
|
||||
|
||||
// The apiPrefix and apiGroup might change due to fallbackApiBases, so we must override them
|
||||
this.apiPrefix = apiPrefix;
|
||||
this.apiGroup = apiGroup;
|
||||
|
||||
const url = [apiPrefix, apiGroup].filter(Boolean).join("/");
|
||||
const url = [apiPrefix, apiGroup].filter(isDefined).join("/");
|
||||
const res = await this.request.get<IKubePreferredVersion>(url);
|
||||
|
||||
this.apiVersionPreferred = res?.preferredVersion?.version ?? null;
|
||||
this.apiVersionPreferred = res?.preferredVersion?.version;
|
||||
|
||||
if (this.apiVersionPreferred) {
|
||||
this.apiBase = this.computeApiBase();
|
||||
apiManager.registerApi(this.apiBase, this);
|
||||
apiManager.registerApi<KubeApi<K, D>>(this.apiBase, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -421,8 +478,11 @@ export class KubeApi<T extends KubeObject> {
|
||||
return query;
|
||||
}
|
||||
|
||||
protected parseResponse(data: unknown, namespace?: string): T | T[] | null {
|
||||
if (!data) return null;
|
||||
protected parseResponse(data: unknown, namespace?: string): K | K[] | null {
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const KubeObjectConstructor = this.objectConstructor;
|
||||
|
||||
// process items list response, check before single item since there is overlap
|
||||
@ -432,37 +492,55 @@ export class KubeApi<T extends KubeObject> {
|
||||
this.setResourceVersion(namespace, metadata.resourceVersion);
|
||||
this.setResourceVersion("", metadata.resourceVersion);
|
||||
|
||||
return items.map((item) => {
|
||||
const object = new KubeObjectConstructor({
|
||||
kind: this.kind,
|
||||
apiVersion,
|
||||
...item,
|
||||
});
|
||||
return items
|
||||
.map((item) => {
|
||||
if (item.metadata) {
|
||||
this.ensureMetadataSelfLink(item.metadata);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
ensureObjectSelfLink(this, object);
|
||||
const object = new KubeObjectConstructor({
|
||||
...(item as D),
|
||||
kind: this.kind,
|
||||
apiVersion,
|
||||
});
|
||||
|
||||
return object;
|
||||
});
|
||||
return object;
|
||||
})
|
||||
.filter(isDefined);
|
||||
}
|
||||
|
||||
// process a single item
|
||||
if (KubeObject.isJsonApiData(data)) {
|
||||
const object = new KubeObjectConstructor(data);
|
||||
this.ensureMetadataSelfLink(data.metadata);
|
||||
|
||||
ensureObjectSelfLink(this, object);
|
||||
|
||||
return object;
|
||||
return new KubeObjectConstructor(data as never);
|
||||
}
|
||||
|
||||
// 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 data.map(data => {
|
||||
this.ensureMetadataSelfLink(data.metadata);
|
||||
|
||||
return new KubeObjectConstructor(data);
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async list({ namespace = "", reqInit }: KubeApiListOptions = {}, query?: IKubeApiQueryParams): Promise<T[] | null> {
|
||||
private ensureMetadataSelfLink<T extends { selfLink?: string; namespace?: string; name: string }>(metadata: T): asserts metadata is T & { selfLink: string } {
|
||||
metadata.selfLink ||= createKubeApiURL({
|
||||
apiPrefix: this.apiPrefix,
|
||||
apiVersion: this.apiVersionWithGroup,
|
||||
resource: this.apiResource,
|
||||
namespace: metadata.namespace,
|
||||
name: metadata.name,
|
||||
});
|
||||
}
|
||||
|
||||
async list({ namespace = "", reqInit }: KubeApiListOptions = {}, query?: IKubeApiQueryParams): Promise<K[] | null> {
|
||||
await this.checkPreferredVersion();
|
||||
|
||||
const url = this.getUrl({ namespace });
|
||||
@ -480,7 +558,7 @@ export class KubeApi<T extends KubeObject> {
|
||||
throw new Error(`GET multiple request to ${url} returned not an array: ${JSON.stringify(parsed)}`);
|
||||
}
|
||||
|
||||
async get(desc: ResourceDescriptor, query?: IKubeApiQueryParams): Promise<T | null> {
|
||||
async get(desc: ResourceDescriptor, query?: IKubeApiQueryParams): Promise<K | null> {
|
||||
await this.checkPreferredVersion();
|
||||
|
||||
const url = this.getUrl(desc);
|
||||
@ -494,7 +572,7 @@ export class KubeApi<T extends KubeObject> {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
async create({ name, namespace }: Partial<ResourceDescriptor>, data?: PartialKubeObject<T>): Promise<T | null> {
|
||||
async create({ name, namespace }: Partial<ResourceDescriptor>, data?: PartialDeep<K>): Promise<K | null> {
|
||||
await this.checkPreferredVersion();
|
||||
|
||||
const apiUrl = this.getUrl({ namespace });
|
||||
@ -517,7 +595,7 @@ export class KubeApi<T extends KubeObject> {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
async update({ name, namespace }: ResourceDescriptor, data: PartialKubeObject<T>): Promise<T | null> {
|
||||
async update({ name, namespace }: ResourceDescriptor, data: PartialDeep<K>): Promise<K | null> {
|
||||
await this.checkPreferredVersion();
|
||||
const apiUrl = this.getUrl({ namespace, name });
|
||||
|
||||
@ -538,7 +616,7 @@ export class KubeApi<T extends KubeObject> {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
async patch(desc: ResourceDescriptor, data?: PartialKubeObject<T> | Patch, strategy: KubeApiPatchType = "strategic") {
|
||||
async patch(desc: ResourceDescriptor, data?: PartialDeep<K> | Patch, strategy: KubeApiPatchType = "strategic"): Promise<K | null> {
|
||||
await this.checkPreferredVersion();
|
||||
const apiUrl = this.getUrl(desc);
|
||||
|
||||
@ -553,7 +631,7 @@ export class KubeApi<T extends KubeObject> {
|
||||
throw new Error(`PATCH request to ${apiUrl} returned an array: ${JSON.stringify(parsed)}`);
|
||||
}
|
||||
|
||||
return parsed as T | null;
|
||||
return parsed;
|
||||
}
|
||||
|
||||
async delete({ propagationPolicy = "Background", ...desc }: DeleteResourceDescriptor) {
|
||||
@ -575,7 +653,7 @@ export class KubeApi<T extends KubeObject> {
|
||||
});
|
||||
}
|
||||
|
||||
watch(opts: KubeApiWatchOptions = { namespace: "", retry: false }): () => void {
|
||||
watch(opts: KubeApiWatchOptions<K, D> = { namespace: "", retry: false }): () => void {
|
||||
let errorReceived = false;
|
||||
let timedRetry: NodeJS.Timeout;
|
||||
const { namespace, callback = noop, retry, timeout } = opts;
|
||||
@ -653,9 +731,9 @@ export class KubeApi<T extends KubeObject> {
|
||||
|
||||
byline(response.body).on("data", (line) => {
|
||||
try {
|
||||
const event: IKubeWatchEvent<KubeJsonApiData> = JSON.parse(line);
|
||||
const event = JSON.parse(line) as IKubeWatchEvent<KubeJsonApiData<KubeObjectMetadata>>;
|
||||
|
||||
if (event.type === "ERROR" && event.object.kind === "Status") {
|
||||
if (event.type === "ERROR" && isKubeStatusData(event.object)) {
|
||||
errorReceived = true;
|
||||
|
||||
return callback(null, new KubeStatus(event.object));
|
||||
@ -679,15 +757,17 @@ export class KubeApi<T extends KubeObject> {
|
||||
};
|
||||
}
|
||||
|
||||
protected modifyWatchEvent(event: IKubeWatchEvent<KubeJsonApiData>) {
|
||||
protected modifyWatchEvent(event: IKubeWatchEvent<KubeJsonApiData<KubeObjectMetadata>>) {
|
||||
if (event.type === "ERROR") {
|
||||
return;
|
||||
}
|
||||
|
||||
ensureObjectSelfLink(this, event.object);
|
||||
this.ensureMetadataSelfLink(event.object.metadata);
|
||||
|
||||
const { namespace, resourceVersion } = event.object.metadata;
|
||||
|
||||
assert(resourceVersion, "watch events failed to return resourceVersion from kube api");
|
||||
|
||||
this.setResourceVersion(namespace, resourceVersion);
|
||||
this.setResourceVersion("", resourceVersion);
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import { JsonApi } from "./json-api";
|
||||
import type { Response } from "node-fetch";
|
||||
import { apiKubePrefix, isDebugging } from "../vars";
|
||||
import { apiBase } from "./api-base";
|
||||
import type { KubeJsonApiObjectMetadata } from "./kube-object";
|
||||
|
||||
export interface KubeJsonApiListMetadata {
|
||||
resourceVersion: string;
|
||||
@ -21,28 +22,17 @@ export interface KubeJsonApiDataList<T = KubeJsonApiData> {
|
||||
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 {
|
||||
export interface KubeJsonApiData<
|
||||
Metadata extends KubeJsonApiObjectMetadata = KubeJsonApiObjectMetadata,
|
||||
Status = unknown,
|
||||
Spec = unknown,
|
||||
> extends JsonApiData {
|
||||
kind: string;
|
||||
apiVersion: string;
|
||||
metadata: KubeJsonApiMetadata;
|
||||
metadata: Metadata;
|
||||
status?: Status;
|
||||
spec?: Spec;
|
||||
[otherKeys: string]: unknown;
|
||||
}
|
||||
|
||||
export interface KubeJsonApiError extends JsonApiError {
|
||||
|
||||
@ -7,22 +7,19 @@ import type { ClusterContext } from "./cluster-context";
|
||||
|
||||
import { action, computed, makeObservable, observable, reaction, when } from "mobx";
|
||||
import type { Disposer } from "../utils";
|
||||
import { autoBind, noop, rejectPromiseBy } from "../utils";
|
||||
import type { KubeObject } from "./kube-object";
|
||||
import { autoBind, includes, isRequestError, noop, rejectPromiseBy } from "../utils";
|
||||
import type { KubeJsonApiDataFor, KubeObject } from "./kube-object";
|
||||
import { KubeStatus } from "./kube-object";
|
||||
import type { IKubeWatchEvent } from "./kube-watch-event";
|
||||
import { ItemStore } from "../item.store";
|
||||
import type { IKubeApiQueryParams, KubeApi } from "./kube-api";
|
||||
import { ensureObjectSelfLink } from "./kube-api";
|
||||
import type { IKubeApiQueryParams, KubeApi, KubeApiWatchCallback } from "./kube-api";
|
||||
import { parseKubeApi } from "./kube-api-parse";
|
||||
import type { KubeJsonApiData } from "./kube-json-api";
|
||||
import type { RequestInit } from "node-fetch";
|
||||
|
||||
// BUG: https://github.com/mysticatea/abort-controller/pull/22
|
||||
// eslint-disable-next-line import/no-named-as-default
|
||||
import AbortController from "abort-controller";
|
||||
import type { Patch } from "rfc6902";
|
||||
import logger from "../logger";
|
||||
import assert from "assert";
|
||||
import type { PartialDeep } from "type-fest";
|
||||
|
||||
export interface KubeObjectStoreLoadingParams {
|
||||
namespaces: string[];
|
||||
@ -68,13 +65,32 @@ export interface MergeItemsOptions {
|
||||
namespaces: string[];
|
||||
}
|
||||
|
||||
export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T> {
|
||||
static defaultContext = observable.box<ClusterContext>(); // TODO: support multiple cluster contexts
|
||||
export interface StatusProvider<K> {
|
||||
getStatuses(items: K[]): Record<string, number>;
|
||||
}
|
||||
|
||||
public readonly api: KubeApi<T>;
|
||||
export interface KubeObjectStoreOptions {
|
||||
limit?: number;
|
||||
bufferSize?: number;
|
||||
}
|
||||
|
||||
export type KubeApiDataFrom<K extends KubeObject, A> = A extends KubeApi<K, infer D>
|
||||
? D extends KubeJsonApiDataFor<K>
|
||||
? D
|
||||
: never
|
||||
: never;
|
||||
|
||||
export abstract class KubeObjectStore<
|
||||
K extends KubeObject = KubeObject,
|
||||
A extends KubeApi<K, D> = KubeApi<K, KubeJsonApiDataFor<K>>,
|
||||
D extends KubeJsonApiDataFor<K> = KubeApiDataFrom<K, A>,
|
||||
> extends ItemStore<K> {
|
||||
static readonly defaultContext = observable.box<ClusterContext>(); // TODO: support multiple cluster contexts
|
||||
|
||||
public readonly api: A;
|
||||
public readonly limit?: number;
|
||||
public readonly bufferSize: number = 50000;
|
||||
@observable private loadedNamespaces?: string[];
|
||||
public readonly bufferSize: number;
|
||||
@observable private loadedNamespaces: string[] | undefined = undefined;
|
||||
|
||||
get contextReady() {
|
||||
return when(() => Boolean(this.context));
|
||||
@ -84,9 +100,11 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
||||
return when(() => Boolean(this.loadedNamespaces));
|
||||
}
|
||||
|
||||
constructor(api?: KubeApi<T>) {
|
||||
constructor(api: A, opts?: KubeObjectStoreOptions) {
|
||||
super();
|
||||
if (api) this.api = api;
|
||||
this.api = api;
|
||||
this.limit = opts?.limit;
|
||||
this.bufferSize = opts?.bufferSize ?? 50_000;
|
||||
|
||||
makeObservable(this);
|
||||
autoBind(this);
|
||||
@ -98,7 +116,7 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
||||
}
|
||||
|
||||
// TODO: Circular dependency: KubeObjectStore -> ClusterFrameContext -> NamespaceStore -> KubeObjectStore
|
||||
@computed get contextItems(): T[] {
|
||||
@computed get contextItems(): K[] {
|
||||
const namespaces = this.context?.contextNamespaces ?? [];
|
||||
|
||||
return this.items.filter(item => {
|
||||
@ -122,13 +140,11 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
||||
return { limit };
|
||||
}
|
||||
|
||||
getStatuses?(items: T[]): Record<string, number>;
|
||||
|
||||
getAllByNs(namespace: string | string[], strict = false): T[] {
|
||||
const namespaces: string[] = [].concat(namespace);
|
||||
getAllByNs(namespace: string | string[], strict = false): K[] {
|
||||
const namespaces = [namespace].flat();
|
||||
|
||||
if (namespaces.length) {
|
||||
return this.items.filter(item => namespaces.includes(item.getNs()));
|
||||
return this.items.filter(item => includes(namespaces, item.getNs()));
|
||||
}
|
||||
|
||||
if (!strict) {
|
||||
@ -138,11 +154,11 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
||||
return [];
|
||||
}
|
||||
|
||||
getById(id: string) {
|
||||
getById(id: string | undefined): K | undefined {
|
||||
return this.items.find(item => item.getId() === id);
|
||||
}
|
||||
|
||||
getByName(name: string, namespace?: string): T {
|
||||
getByName(name: string, namespace?: string): K | undefined {
|
||||
return this.items.find(item => {
|
||||
return item.getName() === name && (
|
||||
namespace ? item.getNs() === namespace : true
|
||||
@ -150,19 +166,19 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
||||
});
|
||||
}
|
||||
|
||||
getByPath(path: string): T {
|
||||
getByPath(path: string): K | undefined {
|
||||
return this.items.find(item => item.selfLink === path);
|
||||
}
|
||||
|
||||
getByLabel(labels: string[] | { [label: string]: string }): T[] {
|
||||
getByLabel(labels: string[] | { [label: string]: string }): K[] {
|
||||
if (Array.isArray(labels)) {
|
||||
return this.items.filter((item: T) => {
|
||||
return this.items.filter((item: K) => {
|
||||
const itemLabels = item.getLabels();
|
||||
|
||||
return labels.every(label => itemLabels.includes(label));
|
||||
});
|
||||
} else {
|
||||
return this.items.filter((item: T) => {
|
||||
return this.items.filter((item: K) => {
|
||||
const itemLabels = item.metadata.labels || {};
|
||||
|
||||
return Object.entries(labels)
|
||||
@ -171,8 +187,8 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
||||
}
|
||||
}
|
||||
|
||||
protected async loadItems({ namespaces, reqInit, onLoadFailure }: KubeObjectStoreLoadingParams): Promise<T[]> {
|
||||
if (!this.context?.cluster.isAllowedResource(this.api.kind)) {
|
||||
protected async loadItems({ namespaces, reqInit, onLoadFailure }: KubeObjectStoreLoadingParams): Promise<K[]> {
|
||||
if (!this.context?.cluster?.isAllowedResource(this.api.kind)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@ -189,9 +205,13 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
||||
|
||||
if (onLoadFailure) {
|
||||
try {
|
||||
return await res;
|
||||
return await res ?? [];
|
||||
} catch (error) {
|
||||
onLoadFailure(error?.message || error?.toString() || "Unknown error");
|
||||
onLoadFailure((
|
||||
isRequestError(error)
|
||||
? error.message || error.toString()
|
||||
: "Unknown error"
|
||||
));
|
||||
|
||||
// reset the store because we are loading all, so that nothing is displayed
|
||||
this.items.clear();
|
||||
@ -201,7 +221,7 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
return await res ?? [];
|
||||
}
|
||||
|
||||
this.loadedNamespaces = namespaces;
|
||||
@ -209,12 +229,12 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
||||
const results = await Promise.allSettled(
|
||||
namespaces.map(namespace => this.api.list({ namespace, reqInit }, this.query)),
|
||||
);
|
||||
const res: T[] = [];
|
||||
const res: K[] = [];
|
||||
|
||||
for (const result of results) {
|
||||
switch (result.status) {
|
||||
case "fulfilled":
|
||||
res.push(...result.value);
|
||||
res.push(...result.value ?? []);
|
||||
break;
|
||||
|
||||
case "rejected":
|
||||
@ -230,12 +250,12 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
||||
return res;
|
||||
}
|
||||
|
||||
protected filterItemsOnLoad(items: T[]) {
|
||||
protected filterItemsOnLoad(items: K[]) {
|
||||
return items;
|
||||
}
|
||||
|
||||
@action
|
||||
async loadAll({ namespaces, merge = true, reqInit, onLoadFailure }: KubeObjectStoreLoadAllParams = {}): Promise<undefined | T[]> {
|
||||
async loadAll({ namespaces, merge = true, reqInit, onLoadFailure }: KubeObjectStoreLoadAllParams = {}): Promise<undefined | K[]> {
|
||||
await this.contextReady;
|
||||
namespaces ??= this.context.contextNamespaces;
|
||||
this.isLoading = true;
|
||||
@ -261,7 +281,7 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
||||
}
|
||||
|
||||
@action
|
||||
async reloadAll(opts: { force?: boolean; namespaces?: string[]; merge?: boolean } = {}): Promise<undefined | T[]> {
|
||||
async reloadAll(opts: { force?: boolean; namespaces?: string[]; merge?: boolean } = {}): Promise<undefined | K[]> {
|
||||
const { force = false, ...loadingOptions } = opts;
|
||||
|
||||
if (this.isLoading || (this.isLoaded && !force)) {
|
||||
@ -272,7 +292,7 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
||||
}
|
||||
|
||||
@action
|
||||
protected mergeItems(partialItems: T[], { merge = true, updateStore = true, sort = true, filter = true, namespaces }: MergeItemsOptions): T[] {
|
||||
protected mergeItems(partialItems: K[], { merge = true, updateStore = true, sort = true, filter = true, namespaces }: MergeItemsOptions): K[] {
|
||||
let items = partialItems;
|
||||
|
||||
// update existing items
|
||||
@ -280,7 +300,7 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
||||
const ns = new Set(namespaces);
|
||||
|
||||
items = [
|
||||
...this.items.filter(existingItem => !ns.has(existingItem.getNs())),
|
||||
...this.items.filter(item => !ns.has(item.getNs() as string)),
|
||||
...partialItems,
|
||||
];
|
||||
}
|
||||
@ -296,17 +316,18 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
||||
if (error) this.reset();
|
||||
}
|
||||
|
||||
protected async loadItem(params: { name: string; namespace?: string }): Promise<T> {
|
||||
protected async loadItem(params: { name: string; namespace?: string }): Promise<K | null> {
|
||||
return this.api.get(params);
|
||||
}
|
||||
|
||||
@action
|
||||
async load(params: { name: string; namespace?: string }): Promise<T> {
|
||||
async load(params: { name: string; namespace?: string }): Promise<K> {
|
||||
const { name, namespace } = params;
|
||||
let item = this.getByName(name, namespace);
|
||||
let item: K | null | undefined = this.getByName(name, namespace);
|
||||
|
||||
if (!item) {
|
||||
item = await this.loadItem(params);
|
||||
assert(item, "Failed to load item from kube");
|
||||
const newItems = this.sortItems([...this.items, item]);
|
||||
|
||||
this.items.replace(newItems);
|
||||
@ -319,15 +340,19 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
||||
async loadFromPath(resourcePath: string) {
|
||||
const { namespace, name } = parseKubeApi(resourcePath);
|
||||
|
||||
assert(name && namespace, "Both name and namesapce must be part of resourcePath");
|
||||
|
||||
return this.load({ name, namespace });
|
||||
}
|
||||
|
||||
protected async createItem(params: { name: string; namespace?: string }, data?: Partial<T>): Promise<T> {
|
||||
protected async createItem(params: { name: string; namespace?: string }, data?: PartialDeep<K>): Promise<K | null> {
|
||||
return this.api.create(params, data);
|
||||
}
|
||||
|
||||
async create(params: { name: string; namespace?: string }, data?: Partial<T>): Promise<T> {
|
||||
async create(params: { name: string; namespace?: string }, data?: PartialDeep<K>): Promise<K> {
|
||||
const newItem = await this.createItem(params, data);
|
||||
|
||||
assert(newItem, "Failed to create item from kube");
|
||||
const items = this.sortItems([...this.items, newItem]);
|
||||
|
||||
this.items.replace(items);
|
||||
@ -335,12 +360,9 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
||||
return newItem;
|
||||
}
|
||||
|
||||
private postUpdate(rawItem: KubeJsonApiData): T {
|
||||
const newItem = new this.api.objectConstructor(rawItem);
|
||||
private postUpdate(newItem: K): K {
|
||||
const index = this.items.findIndex(item => item.getId() === newItem.getId());
|
||||
|
||||
ensureObjectSelfLink(this.api, newItem);
|
||||
|
||||
if (index < 0) {
|
||||
this.items.push(newItem);
|
||||
} else {
|
||||
@ -350,45 +372,49 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
||||
return newItem;
|
||||
}
|
||||
|
||||
async patch(item: T, patch: Patch): Promise<T> {
|
||||
return this.postUpdate(
|
||||
await this.api.patch(
|
||||
{
|
||||
name: item.getName(), namespace: item.getNs(),
|
||||
},
|
||||
patch,
|
||||
"json",
|
||||
),
|
||||
async patch(item: K, patch: Patch): Promise<K> {
|
||||
const rawItem = await this.api.patch(
|
||||
{
|
||||
name: item.getName(), namespace: item.getNs(),
|
||||
},
|
||||
patch,
|
||||
"json",
|
||||
);
|
||||
|
||||
assert(rawItem, `Failed to patch ${item.getDescriptor()} of ${item.kind} ${item.apiVersion}`);
|
||||
|
||||
return this.postUpdate(rawItem);
|
||||
}
|
||||
|
||||
async update(item: T, data: Partial<T>): Promise<T> {
|
||||
return this.postUpdate(
|
||||
await this.api.update(
|
||||
{
|
||||
name: item.getName(),
|
||||
namespace: item.getNs(),
|
||||
},
|
||||
data,
|
||||
),
|
||||
async update(item: K, data: PartialDeep<K>): Promise<K> {
|
||||
const rawItem = await this.api.update(
|
||||
{
|
||||
name: item.getName(),
|
||||
namespace: item.getNs(),
|
||||
},
|
||||
data,
|
||||
);
|
||||
|
||||
assert(rawItem, `Failed to update ${item.getDescriptor()} of ${item.kind} ${item.apiVersion}`);
|
||||
|
||||
return this.postUpdate(rawItem);
|
||||
}
|
||||
|
||||
async remove(item: T) {
|
||||
async remove(item: K) {
|
||||
await this.api.delete({ name: item.getName(), namespace: item.getNs() });
|
||||
this.selectedItemsIds.delete(item.getId());
|
||||
}
|
||||
|
||||
async removeSelectedItems() {
|
||||
return Promise.all(this.selectedItems.map(this.remove));
|
||||
await Promise.all(this.selectedItems.map(this.remove));
|
||||
}
|
||||
|
||||
async removeItems(items: T[]) {
|
||||
async removeItems(items: K[]) {
|
||||
await Promise.all(items.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 eventsBuffer = observable.array<IKubeWatchEvent<D>>([], { deep: false });
|
||||
|
||||
protected bindWatchEventsUpdater(delay = 1000) {
|
||||
reaction(() => this.eventsBuffer.length, this.updateFromEventsBuffer, {
|
||||
@ -400,7 +426,9 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
||||
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) {
|
||||
assert(this.loadedNamespaces);
|
||||
|
||||
if (this.context.cluster?.isGlobalWatchEnabled && this.loadedNamespaces.length === 0) {
|
||||
return this.watchNamespace("", abortController, { onLoadFailure });
|
||||
}
|
||||
|
||||
@ -430,7 +458,7 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
||||
|
||||
const { signal } = abortController;
|
||||
|
||||
const callback = (data: IKubeWatchEvent<T>, error: any) => {
|
||||
const callback: KubeApiWatchCallback<D> = (data, error) => {
|
||||
if (!this.isLoaded || error?.type === "aborted") return;
|
||||
|
||||
if (error instanceof Response) {
|
||||
|
||||
@ -6,53 +6,210 @@
|
||||
// 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 { KubeJsonApiData, KubeJsonApiDataList, KubeJsonApiListMetadata } from "./kube-json-api";
|
||||
import { autoBind, formatDuration, hasOptionalTypedProperty, hasTypedProperty, isObject, isString, isNumber, bindPredicate, isTypedArray, isRecord, json } from "../utils";
|
||||
import type { ItemObject } from "../item.store";
|
||||
import { apiKube } from "./index";
|
||||
import type { JsonApiParams } from "./json-api";
|
||||
import * as resourceApplierApi from "./endpoints/resource-applier.api";
|
||||
import { hasOptionalProperty, hasTypedProperty, isObject, isString, bindPredicate, isTypedArray, isRecord } from "../../common/utils/type-narrowing";
|
||||
import type { Patch } from "rfc6902";
|
||||
import assert from "assert";
|
||||
import type { JsonObject } from "type-fest";
|
||||
|
||||
export type KubeObjectConstructor<K extends KubeObject> = (new (data: KubeJsonApiData | any) => K) & {
|
||||
kind?: string;
|
||||
namespaced?: boolean;
|
||||
apiBase?: string;
|
||||
export type KubeJsonApiDataFor<K> = K extends KubeObject<infer Status, infer Spec, infer Scope>
|
||||
? KubeJsonApiData<KubeObjectMetadata<Scope>, Status, Spec>
|
||||
: never;
|
||||
|
||||
export interface KubeObjectConstructorData {
|
||||
readonly kind?: string;
|
||||
readonly namespaced?: boolean;
|
||||
readonly apiBase?: string;
|
||||
}
|
||||
|
||||
export type KubeObjectConstructor<K extends KubeObject<any, any, KubeObjectScope>, Data> = (new (data: Data) => K) & KubeObjectConstructorData;
|
||||
|
||||
export interface OwnerReference {
|
||||
apiVersion: string;
|
||||
kind: string;
|
||||
name: string;
|
||||
uid: string;
|
||||
controller?: boolean;
|
||||
blockOwnerDeletion?: boolean;
|
||||
}
|
||||
|
||||
export type KubeTemplateObjectMetadata<Namespaced extends KubeObjectScope> = Pick<KubeJsonApiObjectMetadata<KubeObjectScope>, "annotations" | "finalizers" | "generateName" | "labels" | "ownerReferences"> & {
|
||||
name?: string;
|
||||
namespace?: ScopedNamespace<Namespaced>;
|
||||
};
|
||||
|
||||
/**
|
||||
* A reference to an object in the same namespace
|
||||
*/
|
||||
export interface LocalObjectReference {
|
||||
name: string;
|
||||
interface BaseKubeJsonApiObjectMetadata<Namespaced extends KubeObjectScope> {
|
||||
/**
|
||||
* Annotations is an unstructured key value map stored with a resource that may be set by
|
||||
* external tools to store and retrieve arbitrary metadata. They are not queryable and should be
|
||||
* preserved when modifying objects.
|
||||
*
|
||||
* More info: http://kubernetes.io/docs/user-guide/annotations
|
||||
*/
|
||||
annotations?: Partial<Record<string, string>>;
|
||||
|
||||
/**
|
||||
* The name of the cluster which the object belongs to. This is used to distinguish resources
|
||||
* with same name and namespace in different clusters. This field is not set anywhere right now
|
||||
* and apiserver is going to ignore it if set in create or update request.
|
||||
*/
|
||||
clusterName?: string;
|
||||
|
||||
/**
|
||||
* CreationTimestamp is a timestamp representing the server time when this object was created. It
|
||||
* is not guaranteed to be set in happens-before order across separate operations. Clients may
|
||||
* not set this value. It is represented in RFC3339 form and is in UTC. Populated by the system.
|
||||
*
|
||||
* More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
|
||||
*/
|
||||
readonly creationTimestamp?: string;
|
||||
|
||||
/**
|
||||
* Number of seconds allowed for this object to gracefully terminate before it will be removed
|
||||
* from the system. Only set when deletionTimestamp is also set. May only be shortened.
|
||||
*/
|
||||
readonly deletionGracePeriodSeconds?: number;
|
||||
|
||||
/**
|
||||
* DeletionTimestamp is RFC 3339 date and time at which this resource will be deleted. This field
|
||||
* is set by the server when a graceful deletion is requested by the user, and is not directly
|
||||
* settable by a client. The resource is expected to be deleted (no longer visible from resource
|
||||
* lists, and not reachable by name) after the time in this field, once the finalizers list is
|
||||
* empty. As long as the finalizers list contains items, deletion is blocked. Once the
|
||||
* `deletionTimestamp` is set, this value may not be unset or be set further into the future,
|
||||
* although it may be shortened or the resource may be deleted prior to this time. For example,
|
||||
* a user may request that a pod is deleted in 30 seconds. The Kubelet will react by sending a
|
||||
* graceful termination signal to the containers in the pod. After that 30 seconds, the Kubelet
|
||||
* will send a hard termination signal (SIGKILL) to the container and after cleanup, remove the
|
||||
* pod from the API. In the presence of network partitions, this object may still exist after
|
||||
* this timestamp, until an administrator or automated process can determine the resource is
|
||||
* fully terminated. If not set, graceful deletion of the object has not been requested.
|
||||
* Populated by the system when a graceful deletion is requested.
|
||||
*
|
||||
* More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
|
||||
*/
|
||||
readonly deletionTimestamp?: string;
|
||||
|
||||
/**
|
||||
* Must be empty before the object is deleted from the registry. Each entry is an identifier for
|
||||
* the responsible component that will remove the entry from the list. If the deletionTimestamp
|
||||
* of the object is non-nil, entries in this list can only be removed. Finalizers may be
|
||||
* processed and removed in any order. Order is NOT enforced because it introduces significant
|
||||
* risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder
|
||||
* it. If the finalizer list is processed in order, then this can lead to a situation in which
|
||||
* the component responsible for the first finalizer in the list is waiting for a signal (field
|
||||
* value, external system, or other) produced by a component responsible for a finalizer later in
|
||||
* the list, resulting in a deadlock. Without enforced ordering finalizers are free to order
|
||||
* amongst themselves and are not vulnerable to ordering changes in the list.
|
||||
*/
|
||||
finalizers?: string[];
|
||||
|
||||
/**
|
||||
* GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the
|
||||
* Name field has not been provided. If this field is used, the name returned to the client will
|
||||
* be different than the name passed. This value will also be combined with a unique suffix. The
|
||||
* provided value has the same validation rules as the Name field, and may be truncated by the
|
||||
* length of the suffix required to make the value unique on the server. If this field is
|
||||
* specified and the generated name exists, the server will NOT return a 409 - instead, it will
|
||||
* either return 201 Created or 500 with Reason ServerTimeout indicating a unique name could not
|
||||
* be found in the time allotted, and the client should retry (optionally after the time indicated
|
||||
* in the Retry-After header). Applied only if Name is not specified.
|
||||
*
|
||||
* More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency
|
||||
*/
|
||||
generateName?: string;
|
||||
|
||||
/**
|
||||
* A sequence number representing a specific generation of the desired state. Populated by the
|
||||
* system.
|
||||
*/
|
||||
readonly generation?: number;
|
||||
|
||||
/**
|
||||
* Map of string keys and values that can be used to organize and categorize (scope and select)
|
||||
* objects. May match selectors of replication controllers and services.
|
||||
*
|
||||
* More info: http://kubernetes.io/docs/user-guide/labels
|
||||
*/
|
||||
labels?: Partial<Record<string, string>>;
|
||||
|
||||
/**
|
||||
* ManagedFields maps workflow-id and version to the set of fields that are managed by that
|
||||
* workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set
|
||||
* or understand this field. A workflow can be the user's name, a controller's name, or the name
|
||||
* of a specific apply path like "ci-cd". The set of fields is always in the version that the
|
||||
* workflow used when modifying the object.
|
||||
*/
|
||||
managedFields?: unknown[];
|
||||
|
||||
/**
|
||||
* Name must be unique within a namespace. Is required when creating resources, although some
|
||||
* resources may allow a client to request the generation of an appropriate name automatically.
|
||||
* Name is primarily intended for creation idempotence and configuration definition.
|
||||
*
|
||||
* More info: http://kubernetes.io/docs/user-guide/identifiers#names
|
||||
*/
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* Namespace defines the space within which each name must be unique. An empty namespace is
|
||||
* equivalent to the "default" namespace, but "default" is the canonical representation. Not all
|
||||
* objects are required to be scoped to a namespace - the value of this field for those objects
|
||||
* will be empty. Must be a DNS_LABEL. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/namespaces
|
||||
*/
|
||||
readonly namespace?: ScopedNamespace<Namespaced>;
|
||||
|
||||
/**
|
||||
* List of objects depended by this object. If ALL objects in the list have been deleted, this
|
||||
* object will be garbage collected. If this object is managed by a controller, then an entry in
|
||||
* this list will point to this controller, with the controller field set to true. There cannot
|
||||
* be more than one managing controller.
|
||||
*/
|
||||
ownerReferences?: OwnerReference[];
|
||||
|
||||
/**
|
||||
* An opaque value that represents the internal version of this object that can be used by
|
||||
* clients to determine when objects have changed. May be used for optimistic concurrency, change
|
||||
* detection, and the watch operation on a resource or set of resources. Clients must treat these
|
||||
* values as opaque and passed unmodified back to the server. They may only be valid for a
|
||||
* particular resource or set of resources. Populated by the system. Value must be treated as
|
||||
* opaque by clients.
|
||||
*
|
||||
* More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency
|
||||
*/
|
||||
readonly resourceVersion?: string;
|
||||
|
||||
/**
|
||||
* SelfLink is a URL representing this object. Populated by the system.
|
||||
*/
|
||||
readonly selfLink?: string;
|
||||
|
||||
/**
|
||||
* UID is the unique in time and space value for this object. It is typically generated by the
|
||||
* server on successful creation of a resource and is not allowed to change on PUT operations.
|
||||
* Populated by the system.
|
||||
*
|
||||
* More info: http://kubernetes.io/docs/user-guide/identifiers#uids
|
||||
*/
|
||||
readonly uid?: string;
|
||||
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
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 type KubeJsonApiObjectMetadata<Namespaced extends KubeObjectScope = KubeObjectScope> = Namespaced extends "namespace-scoped"
|
||||
? BaseKubeJsonApiObjectMetadata<"namespace-scoped"> & { readonly namespace: string }
|
||||
: BaseKubeJsonApiObjectMetadata<Namespaced>;
|
||||
|
||||
export type KubeObjectMetadata<Namespaced extends KubeObjectScope = KubeObjectScope> = KubeJsonApiObjectMetadata<Namespaced> & {
|
||||
readonly selfLink: string;
|
||||
readonly uid: string;
|
||||
readonly name: string;
|
||||
readonly resourceVersion: string;
|
||||
};
|
||||
|
||||
export interface KubeStatusData {
|
||||
kind: string;
|
||||
@ -62,6 +219,16 @@ export interface KubeStatusData {
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export function isKubeStatusData(object: unknown): object is KubeStatusData {
|
||||
return isObject(object)
|
||||
&& hasTypedProperty(object, "kind", isString)
|
||||
&& hasTypedProperty(object, "apiVersion", isString)
|
||||
&& hasTypedProperty(object, "code", isNumber)
|
||||
&& hasOptionalTypedProperty(object, "message", isString)
|
||||
&& hasOptionalTypedProperty(object, "reason", isString)
|
||||
&& object.kind === "Status";
|
||||
}
|
||||
|
||||
export class KubeStatus {
|
||||
public readonly kind = "Status";
|
||||
public readonly apiVersion: string;
|
||||
@ -77,17 +244,36 @@ export class KubeStatus {
|
||||
}
|
||||
}
|
||||
|
||||
export interface KubeObjectStatus {
|
||||
conditions?: {
|
||||
lastTransitionTime: string;
|
||||
message: string;
|
||||
reason: string;
|
||||
status: string;
|
||||
type?: string;
|
||||
}[];
|
||||
export interface BaseKubeObjectCondition {
|
||||
/**
|
||||
* Last time the condition transit from one status to another.
|
||||
*
|
||||
* @type Date
|
||||
*/
|
||||
lastTransitionTime?: string;
|
||||
/**
|
||||
* A human readable message indicating details about last transition.
|
||||
*/
|
||||
message?: string;
|
||||
/**
|
||||
* brief (usually one word) readon for the condition's last transition.
|
||||
*/
|
||||
reason?: string;
|
||||
/**
|
||||
* Status of the condition
|
||||
*/
|
||||
status: "True" | "False" | "Unknown";
|
||||
/**
|
||||
* Type of the condition
|
||||
*/
|
||||
type: string;
|
||||
}
|
||||
|
||||
export type KubeMetaField = keyof KubeObjectMetadata;
|
||||
export interface KubeObjectStatus {
|
||||
conditions?: BaseKubeObjectCondition[];
|
||||
}
|
||||
|
||||
export type KubeMetaField = keyof KubeJsonApiObjectMetadata;
|
||||
|
||||
export class KubeCreationError extends Error {
|
||||
constructor(message: string, public data: any) {
|
||||
@ -118,35 +304,95 @@ export type LabelMatchExpression = {
|
||||
}
|
||||
);
|
||||
|
||||
export interface Toleration {
|
||||
key?: string;
|
||||
operator?: string;
|
||||
effect?: string;
|
||||
value?: string;
|
||||
tolerationSeconds?: number;
|
||||
}
|
||||
|
||||
export interface ObjectReference {
|
||||
apiVersion?: string;
|
||||
fieldPath?: string;
|
||||
kind?: string;
|
||||
name: string;
|
||||
namespace?: string;
|
||||
resourceVersion?: string;
|
||||
uid?: string;
|
||||
}
|
||||
|
||||
export interface LocalObjectReference {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface TypedLocalObjectReference {
|
||||
apiGroup?: string;
|
||||
kind: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface NodeAffinity {
|
||||
nodeSelectorTerms?: LabelSelector[];
|
||||
weight: number;
|
||||
preference: LabelSelector;
|
||||
}
|
||||
|
||||
export interface PodAffinity {
|
||||
labelSelector: LabelSelector;
|
||||
topologyKey: string;
|
||||
}
|
||||
|
||||
export interface SpecificAffinity<T> {
|
||||
requiredDuringSchedulingIgnoredDuringExecution?: T[];
|
||||
preferredDuringSchedulingIgnoredDuringExecution?: T[];
|
||||
}
|
||||
|
||||
export interface Affinity {
|
||||
nodeAffinity?: SpecificAffinity<NodeAffinity>;
|
||||
podAffinity?: SpecificAffinity<PodAffinity>;
|
||||
podAntiAffinity?: SpecificAffinity<PodAffinity>;
|
||||
}
|
||||
|
||||
export interface LabelSelector {
|
||||
matchLabels?: Record<string, string | undefined>;
|
||||
matchExpressions?: LabelMatchExpression[];
|
||||
}
|
||||
|
||||
export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata, Status = any, Spec = any> implements ItemObject {
|
||||
export type KubeObjectScope = "namespace-scoped" | "cluster-scoped";
|
||||
export type ScopedNamespace<Namespaced extends KubeObjectScope> = (
|
||||
Namespaced extends "namespace-scoped"
|
||||
? string
|
||||
: Namespaced extends "cluster-scoped"
|
||||
? undefined
|
||||
: string | undefined
|
||||
);
|
||||
|
||||
export class KubeObject<
|
||||
Status = unknown,
|
||||
Spec = unknown,
|
||||
Namespaced extends KubeObjectScope = KubeObjectScope,
|
||||
> implements ItemObject {
|
||||
static readonly kind?: string;
|
||||
static readonly namespaced?: boolean;
|
||||
static readonly apiBase?: string;
|
||||
|
||||
apiVersion: string;
|
||||
kind: string;
|
||||
metadata: Metadata;
|
||||
apiVersion!: string;
|
||||
kind!: string;
|
||||
metadata!: KubeObjectMetadata<Namespaced>;
|
||||
status?: Status;
|
||||
spec?: Spec;
|
||||
managedFields?: any;
|
||||
spec!: Spec;
|
||||
|
||||
static create(data: KubeJsonApiData) {
|
||||
static create<
|
||||
Metadata extends KubeObjectMetadata = KubeObjectMetadata,
|
||||
Status = unknown,
|
||||
Spec = unknown,
|
||||
>(data: KubeJsonApiData<Metadata, Status, Spec>) {
|
||||
return new KubeObject(data);
|
||||
}
|
||||
|
||||
static isNonSystem(item: KubeJsonApiData | KubeObject) {
|
||||
return !item.metadata.name.startsWith("system:");
|
||||
static isNonSystem(item: KubeJsonApiData | KubeObject<unknown, unknown, KubeObjectScope>) {
|
||||
return !item.metadata.name?.startsWith("system:");
|
||||
}
|
||||
|
||||
static isJsonApiData(object: unknown): object is KubeJsonApiData {
|
||||
@ -161,49 +407,49 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
|
||||
static isKubeJsonApiListMetadata(object: unknown): object is KubeJsonApiListMetadata {
|
||||
return (
|
||||
isObject(object)
|
||||
&& hasOptionalProperty(object, "resourceVersion", isString)
|
||||
&& hasOptionalProperty(object, "selfLink", isString)
|
||||
&& hasOptionalTypedProperty(object, "resourceVersion", isString)
|
||||
&& hasOptionalTypedProperty(object, "selfLink", isString)
|
||||
);
|
||||
}
|
||||
|
||||
static isKubeJsonApiMetadata(object: unknown): object is KubeJsonApiMetadata {
|
||||
static isKubeJsonApiMetadata(object: unknown): object is KubeJsonApiObjectMetadata {
|
||||
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))
|
||||
&& hasOptionalTypedProperty(object, "selfLink", isString)
|
||||
&& hasOptionalTypedProperty(object, "namespace", isString)
|
||||
&& hasOptionalTypedProperty(object, "creationTimestamp", isString)
|
||||
&& hasOptionalTypedProperty(object, "continue", isString)
|
||||
&& hasOptionalTypedProperty(object, "finalizers", bindPredicate(isTypedArray, isString))
|
||||
&& hasOptionalTypedProperty(object, "labels", bindPredicate(isRecord, isString, isString))
|
||||
&& hasOptionalTypedProperty(object, "annotations", bindPredicate(isRecord, isString, isString))
|
||||
);
|
||||
}
|
||||
|
||||
static isPartialJsonApiMetadata(object: unknown): object is Partial<KubeJsonApiMetadata> {
|
||||
static isPartialJsonApiMetadata(object: unknown): object is Partial<KubeJsonApiObjectMetadata> {
|
||||
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))
|
||||
&& hasOptionalTypedProperty(object, "uid", isString)
|
||||
&& hasOptionalTypedProperty(object, "name", isString)
|
||||
&& hasOptionalTypedProperty(object, "resourceVersion", isString)
|
||||
&& hasOptionalTypedProperty(object, "selfLink", isString)
|
||||
&& hasOptionalTypedProperty(object, "namespace", isString)
|
||||
&& hasOptionalTypedProperty(object, "creationTimestamp", isString)
|
||||
&& hasOptionalTypedProperty(object, "continue", isString)
|
||||
&& hasOptionalTypedProperty(object, "finalizers", bindPredicate(isTypedArray, isString))
|
||||
&& hasOptionalTypedProperty(object, "labels", bindPredicate(isRecord, isString, isString))
|
||||
&& hasOptionalTypedProperty(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)
|
||||
&& hasOptionalTypedProperty(object, "kind", isString)
|
||||
&& hasOptionalTypedProperty(object, "apiVersion", isString)
|
||||
&& hasOptionalTypedProperty(object, "metadata", KubeObject.isPartialJsonApiMetadata)
|
||||
);
|
||||
}
|
||||
|
||||
@ -217,7 +463,7 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
|
||||
);
|
||||
}
|
||||
|
||||
static stringifyLabels(labels?: { [name: string]: string }): string[] {
|
||||
static stringifyLabels(labels?: Record<string, string | undefined>): string[] {
|
||||
if (!labels) return [];
|
||||
|
||||
return Object.entries(labels).map(([name, value]) => `${name}=${value}`);
|
||||
@ -240,7 +486,7 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
|
||||
...KubeObject.nonEditablePathPrefixes,
|
||||
]);
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
constructor(data: KubeJsonApiData<KubeObjectMetadata<Namespaced>, Status, Spec>) {
|
||||
if (typeof data !== "object") {
|
||||
throw new TypeError(`Cannot create a KubeObject from ${typeof data}`);
|
||||
}
|
||||
@ -265,13 +511,20 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
|
||||
return this.metadata.resourceVersion;
|
||||
}
|
||||
|
||||
getDescriptor() {
|
||||
const ns = this.getNs();
|
||||
const res = ns ? `${ns}/` : "";
|
||||
|
||||
return res + this.getName();
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.metadata.name;
|
||||
}
|
||||
|
||||
getNs() {
|
||||
getNs(): ScopedNamespace<Namespaced> {
|
||||
// avoid "null" serialization via JSON.stringify when post data
|
||||
return this.metadata.namespace || undefined;
|
||||
return (this.metadata.namespace || undefined) as never;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -279,6 +532,10 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
|
||||
* creation timestamp of this object.
|
||||
*/
|
||||
getCreationTimestamp() {
|
||||
if (!this.metadata.creationTimestamp) {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
return new Date(this.metadata.creationTimestamp).getTime();
|
||||
}
|
||||
|
||||
@ -288,6 +545,10 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
|
||||
* NOTE: Generally you can use `getCreationTimestamp` instead.
|
||||
*/
|
||||
getTimeDiffFromNow(): number {
|
||||
if (!this.metadata.creationTimestamp) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Date.now() - new Date(this.metadata.creationTimestamp).getTime();
|
||||
}
|
||||
|
||||
@ -346,8 +607,8 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
|
||||
];
|
||||
}
|
||||
|
||||
toPlainObject(): object {
|
||||
return JSON.parse(JSON.stringify(this));
|
||||
toPlainObject() {
|
||||
return json.parse(JSON.stringify(this)) as JsonObject;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -390,6 +651,8 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
|
||||
* @deprecated use KubeApi.delete instead
|
||||
*/
|
||||
delete(params?: JsonApiParams) {
|
||||
assert(this.selfLink, "selfLink must be present to delete self");
|
||||
|
||||
return apiKube.del(this.selfLink, params);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,14 +3,13 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { KubeJsonApiData } from "./kube-json-api";
|
||||
import type { KubeStatusData } from "./kube-object";
|
||||
|
||||
export type IKubeWatchEvent<T extends KubeJsonApiData> = {
|
||||
type: "ADDED" | "MODIFIED" | "DELETED";
|
||||
object: T;
|
||||
export type IKubeWatchEvent<T> = {
|
||||
readonly type: "ADDED" | "MODIFIED" | "DELETED";
|
||||
readonly object: T;
|
||||
} | {
|
||||
type: "ERROR";
|
||||
object: KubeStatusData;
|
||||
readonly type: "ERROR";
|
||||
readonly object?: KubeStatusData;
|
||||
};
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user