1
0
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:
Sebastian Malton 2022-03-17 16:54:05 -04:00
parent e532b90b72
commit f594bf03f3
689 changed files with 14532 additions and 11305 deletions

View File

@ -139,6 +139,28 @@ module.exports = {
"requireLast": false, "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", "react/display-name": "off",
"space-before-function-paren": "off", "space-before-function-paren": "off",
"@typescript-eslint/space-before-function-paren": ["error", { "@typescript-eslint/space-before-function-paren": ["error", {

View File

@ -5,7 +5,7 @@
import fs from "fs-extra"; import fs from "fs-extra";
import path from "path"; 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 outputCssFile = path.resolve("src/renderer/themes/theme-vars.css");

View File

@ -1,6 +1,6 @@
{ {
"name": "kube-object-event-status", "name": "kube-object-event-status",
"version": "0.0.1", "version": "5.5.0-alpha.0.1649256751918",
"description": "Adds kube object status from events", "description": "Adds kube object status from events",
"renderer": "dist/renderer.js", "renderer": "dist/renderer.js",
"lens": { "lens": {

View File

@ -1,6 +1,6 @@
{ {
"name": "lens-metrics-cluster-feature", "name": "lens-metrics-cluster-feature",
"version": "0.0.1", "version": "5.5.0-alpha.0.1649256751918",
"description": "Lens metrics cluster feature", "description": "Lens metrics cluster feature",
"renderer": "dist/renderer.js", "renderer": "dist/renderer.js",
"lens": { "lens": {

View File

@ -208,14 +208,14 @@ export class MetricsSettings extends React.Component<MetricsSettingsProps> {
<section> <section>
<SubTitle title="Prometheus" /> <SubTitle title="Prometheus" />
<FormSwitch <FormSwitch
control={ control={(
<Switcher <Switcher
disabled={this.featureStates.kubeStateMetrics === undefined || !this.isTogglable} disabled={this.featureStates.kubeStateMetrics === undefined || !this.isTogglable}
checked={!!this.featureStates.prometheus && this.props.cluster.status.phase == "connected"} checked={!!this.featureStates.prometheus && this.props.cluster.status.phase == "connected"}
onChange={v => this.togglePrometheus(v.target.checked)} onChange={v => this.togglePrometheus(v.target.checked)}
name="prometheus" name="prometheus"
/> />
} )}
label="Enable bundled Prometheus metrics stack" label="Enable bundled Prometheus metrics stack"
/> />
<small className="hint"> <small className="hint">
@ -226,14 +226,14 @@ export class MetricsSettings extends React.Component<MetricsSettingsProps> {
<section> <section>
<SubTitle title="Kube State Metrics" /> <SubTitle title="Kube State Metrics" />
<FormSwitch <FormSwitch
control={ control={(
<Switcher <Switcher
disabled={this.featureStates.kubeStateMetrics === undefined || !this.isTogglable} disabled={this.featureStates.kubeStateMetrics === undefined || !this.isTogglable}
checked={!!this.featureStates.kubeStateMetrics && this.props.cluster.status.phase == "connected"} checked={!!this.featureStates.kubeStateMetrics && this.props.cluster.status.phase == "connected"}
onChange={v => this.toggleKubeStateMetrics(v.target.checked)} onChange={v => this.toggleKubeStateMetrics(v.target.checked)}
name="node-exporter" name="node-exporter"
/> />
} )}
label="Enable bundled kube-state-metrics stack" label="Enable bundled kube-state-metrics stack"
/> />
<small className="hint"> <small className="hint">
@ -245,14 +245,14 @@ export class MetricsSettings extends React.Component<MetricsSettingsProps> {
<section> <section>
<SubTitle title="Node Exporter" /> <SubTitle title="Node Exporter" />
<FormSwitch <FormSwitch
control={ control={(
<Switcher <Switcher
disabled={this.featureStates.nodeExporter === undefined || !this.isTogglable} disabled={this.featureStates.nodeExporter === undefined || !this.isTogglable}
checked={!!this.featureStates.nodeExporter && this.props.cluster.status.phase == "connected"} checked={!!this.featureStates.nodeExporter && this.props.cluster.status.phase == "connected"}
onChange={v => this.toggleNodeExporter(v.target.checked)} onChange={v => this.toggleNodeExporter(v.target.checked)}
name="node-exporter" name="node-exporter"
/> />
} )}
label="Enable bundled node-exporter stack" label="Enable bundled node-exporter stack"
/> />
<small className="hint"> <small className="hint">
@ -271,9 +271,11 @@ export class MetricsSettings extends React.Component<MetricsSettingsProps> {
className="w-60 h-14" className="w-60 h-14"
/> />
{this.canUpgrade && (<small className="hint"> {this.canUpgrade && (
An update is available for enabled metrics components. <small className="hint">
</small>)} An update is available for enabled metrics components.
</small>
)}
</section> </section>
</> </>
); );

View File

@ -1,6 +1,6 @@
{ {
"name": "lens-node-menu", "name": "lens-node-menu",
"version": "0.0.1", "version": "5.5.0-alpha.0.1649256751918",
"description": "Lens node menu", "description": "Lens node menu",
"renderer": "dist/renderer.js", "renderer": "dist/renderer.js",
"lens": { "lens": {

View File

@ -68,7 +68,9 @@ export function NodeMenu(props: NodeMenuProps) {
labelOk: `Drain Node`, labelOk: `Drain Node`,
message: ( message: (
<p> <p>
Are you sure you want to drain <b>{nodeName}</b>? {"Are you sure you want to drain "}
<b>{nodeName}</b>
?
</p> </p>
), ),
}); });
@ -77,26 +79,42 @@ export function NodeMenu(props: NodeMenuProps) {
return ( return (
<> <>
<MenuItem onClick={shell}> <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> <span className="title">Shell</span>
</MenuItem> </MenuItem>
{ {
node.isUnschedulable() node.isUnschedulable()
? ( ? (
<MenuItem onClick={unCordon}> <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> <span className="title">Uncordon</span>
</MenuItem> </MenuItem>
) )
: ( : (
<MenuItem onClick={cordon}> <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> <span className="title">Cordon</span>
</MenuItem> </MenuItem>
) )
} }
<MenuItem onClick={drain}> <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> <span className="title">Drain</span>
</MenuItem> </MenuItem>
</> </>

View File

@ -1,6 +1,6 @@
{ {
"name": "lens-pod-menu", "name": "lens-pod-menu",
"version": "0.0.1", "version": "5.5.0-alpha.0.1649256751918",
"description": "Lens pod menu", "description": "Lens pod menu",
"renderer": "dist/renderer.js", "renderer": "dist/renderer.js",
"lens": { "lens": {

View File

@ -71,7 +71,11 @@ export class PodAttachMenu extends React.Component<PodAttachMenuProps> {
return ( return (
<MenuItem onClick={Util.prevDefault(() => this.attachToPod(containers[0].name))}> <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> <span className="title">Attach Pod</span>
{containers.length > 1 && ( {containers.length > 1 && (
<> <>
@ -82,7 +86,11 @@ export class PodAttachMenu extends React.Component<PodAttachMenuProps> {
const { name } = container; const { name } = container;
return ( 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/> <StatusBrick/>
<span>{name}</span> <span>{name}</span>
</MenuItem> </MenuItem>

View File

@ -46,7 +46,11 @@ export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
return ( return (
<MenuItem onClick={Util.prevDefault(() => this.showLogs(containers[0]))}> <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> <span className="title">Logs</span>
{containers.length > 1 && ( {containers.length > 1 && (
<> <>
@ -63,7 +67,11 @@ export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
) : null; ) : null;
return ( 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} {brick}
<span>{name}</span> <span>{name}</span>
</MenuItem> </MenuItem>

View File

@ -79,7 +79,11 @@ export class PodShellMenu extends React.Component<PodShellMenuProps> {
return ( return (
<MenuItem onClick={Util.prevDefault(() => this.execShell(containers[0].name))}> <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> <span className="title">Shell</span>
{containers.length > 1 && ( {containers.length > 1 && (
<> <>
@ -90,7 +94,11 @@ export class PodShellMenu extends React.Component<PodShellMenuProps> {
const { name } = container; const { name } = container;
return ( 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/> <StatusBrick/>
<span>{name}</span> <span>{name}</span>
</MenuItem> </MenuItem>

View File

@ -108,6 +108,10 @@ export async function lauchMinikubeClusterFromCatalog(window: Page): Promise<Fra
const frame = await minikubeFrame.contentFrame(); const frame = await minikubeFrame.contentFrame();
if (!frame) {
throw new Error("No iframe for minikube found");
}
await frame.waitForSelector("[data-testid=cluster-sidebar]"); await frame.waitForSelector("[data-testid=cluster-sidebar]");
return frame; return frame;

View File

@ -3,7 +3,7 @@
"productName": "OpenLens", "productName": "OpenLens",
"description": "OpenLens - Open Source IDE for Kubernetes", "description": "OpenLens - Open Source IDE for Kubernetes",
"homepage": "https://github.com/lensapp/lens", "homepage": "https://github.com/lensapp/lens",
"version": "5.5.0-alpha.0", "version": "5.5.0-alpha.0.1649256751918",
"main": "static/build/main.js", "main": "static/build/main.js",
"copyright": "© 2021 OpenLens Authors", "copyright": "© 2021 OpenLens Authors",
"license": "MIT", "license": "MIT",
@ -269,13 +269,14 @@
"tar": "^6.1.11", "tar": "^6.1.11",
"tcp-port-used": "^1.0.2", "tcp-port-used": "^1.0.2",
"tempy": "1.0.1", "tempy": "1.0.1",
"typed-regex": "^0.0.8",
"url-parse": "^1.5.10", "url-parse": "^1.5.10",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"win-ca": "^3.5.0", "win-ca": "^3.5.0",
"winston": "^3.6.0", "winston": "^3.6.0",
"winston-console-format": "^1.0.8", "winston-console-format": "^1.0.8",
"winston-transport-browserconsole": "^1.0.5", "winston-transport-browserconsole": "^1.0.5",
"ws": "^7.5.7" "ws": "^8.5.0"
}, },
"devDependencies": { "devDependencies": {
"@async-fn/jest": "1.5.3", "@async-fn/jest": "1.5.3",
@ -289,6 +290,7 @@
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"@types/byline": "^4.2.33", "@types/byline": "^4.2.33",
"@types/chart.js": "^2.9.36", "@types/chart.js": "^2.9.36",
"@types/circular-dependency-plugin": "5.0.5",
"@types/cli-progress": "^3.9.2", "@types/cli-progress": "^3.9.2",
"@types/color": "^3.0.3", "@types/color": "^3.0.3",
"@types/crypto-js": "^3.1.47", "@types/crypto-js": "^3.1.47",

View File

@ -30,9 +30,9 @@ interface TestStoreModel {
} }
class TestStore extends BaseStore<TestStoreModel> { class TestStore extends BaseStore<TestStoreModel> {
@observable a: string; @observable a = "";
@observable b: string; @observable b = "";
@observable c: string; @observable c = "";
constructor() { constructor() {
super({ super({
@ -90,7 +90,6 @@ describe("BaseStore", () => {
await mainDi.runSetups(); await mainDi.runSetups();
store = undefined;
TestStore.resetInstance(); TestStore.resetInstance();
const mockOpts = { const mockOpts = {

View File

@ -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 getCustomKubeConfigDirectoryInjectable from "../app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable";
import clusterStoreInjectable from "../cluster-store/cluster-store.injectable"; import clusterStoreInjectable from "../cluster-store/cluster-store.injectable";
import type { ClusterModel } from "../cluster-types"; import type { ClusterModel } from "../cluster-types";
import type { import type { DiContainer } from "@ogre-tools/injectable";
DiContainer,
} from "@ogre-tools/injectable";
import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token"; import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable"; 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 appVersionInjectable from "../get-configuration-file-model/app-version/app-version.injectable";
import assert from "assert";
console = new Console(stdout, stderr); console = new Console(stdout, stderr);
@ -148,6 +145,8 @@ describe("cluster-store", () => {
it("adds new cluster to store", async () => { it("adds new cluster to store", async () => {
const storedCluster = clusterStore.getById("foo"); const storedCluster = clusterStore.getById("foo");
assert(storedCluster);
expect(storedCluster.id).toBe("foo"); expect(storedCluster.id).toBe("foo");
expect(storedCluster.preferences.terminalCWD).toBe("/some-directory-for-user-data"); expect(storedCluster.preferences.terminalCWD).toBe("/some-directory-for-user-data");
expect(storedCluster.preferences.icon).toBe( expect(storedCluster.preferences.icon).toBe(
@ -249,6 +248,8 @@ describe("cluster-store", () => {
it("allows to retrieve a cluster", () => { it("allows to retrieve a cluster", () => {
const storedCluster = clusterStore.getById("cluster1"); const storedCluster = clusterStore.getById("cluster1");
assert(storedCluster);
expect(storedCluster.id).toBe("cluster1"); expect(storedCluster.id).toBe("cluster1");
expect(storedCluster.preferences.terminalCWD).toBe("/foo"); expect(storedCluster.preferences.terminalCWD).toBe("/foo");
}); });
@ -379,6 +380,7 @@ users:
it("migrates to modern format with icon not in file", async () => { it("migrates to modern format with icon not in file", async () => {
const { icon } = clusterStore.clustersList[0].preferences; const { icon } = clusterStore.clustersList[0].preferences;
assert(icon);
expect(icon.startsWith("data:;base64,")).toBe(true); expect(icon.startsWith("data:;base64,")).toBe(true);
}); });
}); });

View File

@ -5,7 +5,7 @@
import type { AppEvent } from "../app-event-bus/event-bus"; import type { AppEvent } from "../app-event-bus/event-bus";
import { appEventBus } 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"; import { stdout, stderr } from "process";
console = new Console(stdout, stderr); console = new Console(stdout, stderr);
@ -13,14 +13,15 @@ console = new Console(stdout, stderr);
describe("event bus tests", () => { describe("event bus tests", () => {
describe("emit", () => { describe("emit", () => {
it("emits an event", () => { it("emits an event", () => {
let event: AppEvent = null; let event: AppEvent | undefined;
appEventBus.addListener((data) => { appEventBus.addListener((data) => {
event = data; event = data;
}); });
appEventBus.emit({ name: "foo", action: "bar" }); appEventBus.emit({ name: "foo", action: "bar" });
expect(event.name).toBe("foo"); assert(event);
expect(event?.name).toBe("foo");
}); });
}); });
}); });

View File

@ -21,7 +21,7 @@ describe("EventEmitter", () => {
let called = false; let called = false;
const e = new EventEmitter<[]>(); const e = new EventEmitter<[]>();
e.addListener(() => 0 as any, {}); e.addListener(() => 0 as never, {});
e.addListener(() => { called = true; }, {}); e.addListener(() => { called = true; }, {});
e.emit(); e.emit();

View File

@ -174,7 +174,7 @@ describe("HotbarStore", () => {
}); });
it("initially adds catalog entity as first item", () => { 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", () => { it("adds items", () => {
@ -211,7 +211,7 @@ describe("HotbarStore", () => {
hotbarStore.restackItems(1, 5); hotbarStore.restackItems(1, 5);
expect(hotbarStore.getActive().items[5]).toBeTruthy(); 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", () => { it("moves items down", () => {
@ -275,7 +275,7 @@ describe("HotbarStore", () => {
hotbarStore.addToHotbar(testCluster); hotbarStore.addToHotbar(testCluster);
hotbarStore.restackItems(1, 1); 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", () => { it("new items takes first empty cell", () => {
@ -284,7 +284,7 @@ describe("HotbarStore", () => {
hotbarStore.restackItems(0, 3); hotbarStore.restackItems(0, 3);
hotbarStore.addToHotbar(minikubeCluster); 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", () => { it("throws if invalid arguments provided", () => {
@ -389,7 +389,7 @@ describe("HotbarStore", () => {
it("allows to retrieve a hotbar", () => { it("allows to retrieve a hotbar", () => {
const hotbar = hotbarStore.getById("3caac17f-aec2-4723-9694-ad204465d935"); 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", () => { it("clears cells without entity", () => {

View File

@ -6,13 +6,14 @@ import https from "https";
import os from "os"; import os from "os";
import { getMacRootCA, getWinRootCA, injectCAs, DSTRootCAX3 } from "../system-ca"; import { getMacRootCA, getWinRootCA, injectCAs, DSTRootCAX3 } from "../system-ca";
import { dependencies, devDependencies } from "../../../package.json"; import { dependencies, devDependencies } from "../../../package.json";
import assert from "assert";
const deps = { ...dependencies, ...devDependencies }; const deps = { ...dependencies, ...devDependencies };
// Skip the test if mac-ca is not installed, or os is not darwin // 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", () => { (deps["mac-ca"] && os.platform().includes("darwin") ? describe: describe.skip)("inject CA for Mac", () => {
// for reset https.globalAgent.options.ca after testing // for reset https.globalAgent.options.ca after testing
let _ca: string | Buffer | (string | Buffer)[]; let _ca: string | Buffer | (string | Buffer)[] | undefined;
beforeEach(() => { beforeEach(() => {
_ca = https.globalAgent.options.ca; _ca = https.globalAgent.options.ca;
@ -44,6 +45,7 @@ const deps = { ...dependencies, ...devDependencies };
injectCAs(osxCAs); injectCAs(osxCAs);
const injected = https.globalAgent.options.ca; const injected = https.globalAgent.options.ca;
assert(injected);
expect(injected.includes(DSTRootCAX3)).toBeFalsy(); 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 // 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", () => { (deps["win-ca"] && os.platform().includes("win32") ? describe: describe.skip)("inject CA for Windows", () => {
// for reset https.globalAgent.options.ca after testing // for reset https.globalAgent.options.ca after testing
let _ca: string | Buffer | (string | Buffer)[]; let _ca: string | Buffer | (string | Buffer)[] | undefined;
beforeEach(() => { beforeEach(() => {
_ca = https.globalAgent.options.ca; _ca = https.globalAgent.options.ca;

View File

@ -49,7 +49,7 @@ describe("user store tests", () => {
mockFs(); mockFs();
di.override(writeFileInjectable, () => () => undefined); di.override(writeFileInjectable, () => () => Promise.resolve());
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
di.override(userStoreInjectable, () => UserStore.createInstance()); di.override(userStoreInjectable, () => UserStore.createInstance());

View File

@ -5,7 +5,7 @@
import { navigate } from "../../renderer/navigation"; import { navigate } from "../../renderer/navigation";
import type { CatalogEntityMetadata, CatalogEntitySpec, CatalogEntityStatus } from "../catalog"; 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"; import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
interface GeneralEntitySpec extends CatalogEntitySpec { interface GeneralEntitySpec extends CatalogEntitySpec {
@ -47,10 +47,7 @@ export class GeneralCategory extends CatalogCategory {
public spec = { public spec = {
group: "entity.k8slens.dev", group: "entity.k8slens.dev",
versions: [ versions: [
{ categoryVersion("v1alpha1", GeneralEntity),
name: "v1alpha1",
entityClass: GeneralEntity,
},
], ],
names: { names: {
kind: "General", kind: "General",

View File

@ -5,11 +5,11 @@
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry"; import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
import type { CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus, CatalogCategorySpec } from "../catalog"; 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 { ClusterStore } from "../cluster-store/cluster-store";
import { broadcastMessage } from "../ipc"; import { broadcastMessage } from "../ipc";
import { app } from "electron"; 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 { IpcRendererNavigationEvents } from "../../renderer/navigation/events";
import { requestClusterActivation, requestClusterDisconnection } from "../../renderer/ipc"; import { requestClusterActivation, requestClusterDisconnection } from "../../renderer/ipc";
import KubeClusterCategoryIcon from "./icons/kubernetes.svg"; import KubeClusterCategoryIcon from "./icons/kubernetes.svg";
@ -60,6 +60,10 @@ export type KubernetesClusterStatusPhase = "connected" | "connecting" | "disconn
export interface KubernetesClusterStatus extends CatalogEntityStatus { export interface KubernetesClusterStatus extends CatalogEntityStatus {
} }
export function isKubernetesCluster(item: unknown): item is KubernetesCluster {
return item instanceof KubernetesCluster;
}
export class KubernetesCluster< export class KubernetesCluster<
Metadata extends KubernetesClusterMetadata = KubernetesClusterMetadata, Metadata extends KubernetesClusterMetadata = KubernetesClusterMetadata,
Status extends KubernetesClusterStatus = KubernetesClusterStatus, Status extends KubernetesClusterStatus = KubernetesClusterStatus,
@ -145,10 +149,7 @@ class KubernetesClusterCategory extends CatalogCategory {
public spec: CatalogCategorySpec = { public spec: CatalogCategorySpec = {
group: "entity.k8slens.dev", group: "entity.k8slens.dev",
versions: [ versions: [
{ categoryVersion("v1alpha1", KubernetesCluster as CatalogEntityConstructor<KubernetesCluster>),
name: "v1alpha1",
entityClass: KubernetesCluster,
},
], ],
names: { names: {
kind: "KubernetesCluster", kind: "KubernetesCluster",

View File

@ -4,7 +4,7 @@
*/ */
import type { CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog"; 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 { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
import { productName } from "../vars"; import { productName } from "../vars";
import { WeblinkStore } from "../weblink-store"; import { WeblinkStore } from "../weblink-store";
@ -47,7 +47,7 @@ export class WebLink extends CatalogEntity<CatalogEntityMetadata, WebLinkStatus,
} }
catalogCategoryRegistry catalogCategoryRegistry
.getCategoryForEntity<WebLinkCategory>(this) .getCategoryForEntity(this)
?.emit("contextMenuOpen", this, context); ?.emit("contextMenuOpen", this, context);
} }
} }
@ -62,10 +62,7 @@ export class WebLinkCategory extends CatalogCategory {
public spec = { public spec = {
group: "entity.k8slens.dev", group: "entity.k8slens.dev",
versions: [ versions: [
{ categoryVersion("v1alpha1", WebLink),
name: "v1alpha1",
entityClass: WebLink,
},
], ],
names: { names: {
kind: "WebLink", kind: "WebLink",

View File

@ -11,19 +11,19 @@ import type { Disposer } from "../utils";
import { iter } from "../utils"; import { iter } from "../utils";
import type { CategoryColumnRegistration } from "../../renderer/components/+catalog/custom-category-columns"; import type { CategoryColumnRegistration } from "../../renderer/components/+catalog/custom-category-columns";
type ExtractEntityMetadataType<Entity> = Entity extends CatalogEntity<infer Metadata> ? Metadata : never; export type CatalogEntityDataFor<Entity> = Entity extends CatalogEntity<infer Metadata, infer Status, infer Spec>
type ExtractEntityStatusType<Entity> = Entity extends CatalogEntity<any, infer Status> ? Status : never; ? CatalogEntityData<Metadata, Status, Spec>
type ExtractEntitySpecType<Entity> = Entity extends CatalogEntity<any, any, infer Spec> ? Spec : never; : never;
export type CatalogEntityInstanceFrom<Constructor> = Constructor extends CatalogEntityConstructor<infer Entity>
? Entity
: never;
export type CatalogEntityConstructor<Entity extends CatalogEntity> = ( export type CatalogEntityConstructor<Entity extends CatalogEntity> = (
(new (data: CatalogEntityData< new (data: CatalogEntityDataFor<Entity>) => Entity
ExtractEntityMetadataType<Entity>,
ExtractEntityStatusType<Entity>,
ExtractEntitySpecType<Entity>
>) => Entity)
); );
export interface CatalogCategoryVersion<Entity extends CatalogEntity> { export interface CatalogCategoryVersion {
/** /**
* The specific version that the associated constructor is for. This MUST be * 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 * 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` * - `v1alpha2`
* - `v3beta2` * - `v3beta2`
*/ */
name: string; readonly name: string;
/** /**
* The constructor for the entities. * The constructor for the entities.
*/ */
entityClass: CatalogEntityConstructor<Entity>; readonly entityClass: CatalogEntityConstructor<CatalogEntity>;
} }
export interface CatalogCategorySpec { export interface CatalogCategorySpec {
/** /**
* The grouping for for the category. This MUST be a DNS label. * The grouping for for the category. This MUST be a DNS label.
*/ */
group: string; readonly group: string;
/** /**
* The specific versions of the constructors. * 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 * 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` * `name = "v1alpha1"` then the resulting `.apiVersion` MUST be `entity.k8slens.dev/v1alpha1`
*/ */
versions: CatalogCategoryVersion<CatalogEntity>[]; readonly versions: CatalogCategoryVersion[];
/** /**
* This is the concerning the category * This is the concerning the category
*/ */
names: { readonly names: {
/** /**
* The kind of entity that this category is for. This value MUST be a DNS * 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 * label and MUST be equal to the `kind` fields that are produced by the
* `.versions.[] | .entityClass` fields. * `.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. * 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; 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>) { export abstract class CatalogCategory extends (EventEmitter as new () => TypedEmitter<CatalogCategoryEvents>) {
/** /**
* The version of category that you are wanting to declare. * 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 * The data about the category itself
*/ */
abstract readonly metadata: { abstract readonly metadata: CatalogCategoryMetadata;
/**
* 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;
};
/** /**
* The most important part of a category, as it is where entity versions are declared. * The most important part of a category, as it is where entity versions are declared.
*/ */
abstract spec: CatalogCategorySpec; abstract readonly spec: CatalogCategorySpec;
/** /**
* @internal * @internal
*/ */
protected filters = observable.set<AddMenuFilter>([], { protected readonly filters = observable.set<AddMenuFilter>([], {
deep: false, 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; uid: string;
name: string; name: string;
shortName?: string; shortName?: string;
description?: string; description?: string;
source?: string; source?: string;
labels: Record<string, string>; labels: Record<string, string>;
[key: string]: string | object;
} }
export interface CatalogEntityStatus { export interface CatalogEntityStatus {

View File

@ -4,7 +4,7 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { comparer, computed } from "mobx"; import { comparer, computed } from "mobx";
import hostedClusterInjectable from "./hosted-cluster.injectable"; import hostedClusterInjectable from "../../renderer/cluster/hosted-cluster.injectable";
const allowedResourcesInjectable = getInjectable({ const allowedResourcesInjectable = getInjectable({
id: "allowed-resources", id: "allowed-resources",
@ -12,7 +12,7 @@ const allowedResourcesInjectable = getInjectable({
instantiate: (di) => { instantiate: (di) => {
const cluster = di.inject(hostedClusterInjectable); 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 // This needs to be here so that during refresh changes are only propogated when necessary
equals: (cur, prev) => comparer.structural(cur, prev), equals: (cur, prev) => comparer.structural(cur, prev),
}); });

View File

@ -103,8 +103,12 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
return this.clusters.size > 0; return this.clusters.size > 0;
} }
getById(id: ClusterId): Cluster | null { getById(id: ClusterId | undefined): Cluster | undefined {
return this.clusters.get(id) ?? null; if (id) {
return this.clusters.get(id);
}
return undefined;
} }
addCluster(clusterOrModel: ClusterModel | Cluster): Cluster { addCluster(clusterOrModel: ClusterModel | Cluster): Cluster {

View File

@ -6,7 +6,7 @@
import { ipcMain } from "electron"; import { ipcMain } from "electron";
import { action, comparer, computed, makeObservable, observable, reaction, when } from "mobx"; import { action, comparer, computed, makeObservable, observable, reaction, when } from "mobx";
import { broadcastMessage } from "../ipc"; 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 type { KubeConfig } from "@kubernetes/client-node";
import { HttpError } from "@kubernetes/client-node"; import { HttpError } from "@kubernetes/client-node";
import type { Kubectl } from "../../main/kubectl/kubectl"; 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 plimit from "p-limit";
import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate } from "../cluster-types"; import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate } from "../cluster-types";
import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } 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 type { Response } from "request";
import { clusterListNamespaceForbiddenChannel } from "../ipc/cluster"; import { clusterListNamespaceForbiddenChannel } from "../ipc/cluster";
import type { CanI } from "./authorization-review.injectable"; import type { CanI } from "./authorization-review.injectable";
import type { ListNamespaces } from "./list-namespaces.injectable"; import type { ListNamespaces } from "./list-namespaces.injectable";
import assert from "assert";
export interface ClusterDependencies { export interface ClusterDependencies {
readonly directoryForKubeConfigs: string; readonly directoryForKubeConfigs: string;
createKubeconfigManager: (cluster: Cluster) => KubeconfigManager; createKubeconfigManager: (cluster: Cluster) => KubeconfigManager;
createContextHandler: (cluster: Cluster) => ContextHandler; createContextHandler: (cluster: Cluster) => ClusterContextHandler;
createKubectl: (clusterVersion: string) => Kubectl; createKubectl: (clusterVersion: string) => Kubectl;
createAuthorizationReview: (config: KubeConfig) => CanI; createAuthorizationReview: (config: KubeConfig) => CanI;
createListNamespaces: (config: KubeConfig) => ListNamespaces; createListNamespaces: (config: KubeConfig) => ListNamespaces;
@ -43,17 +44,31 @@ export interface ClusterDependencies {
export class Cluster implements ClusterModel, ClusterState { export class Cluster implements ClusterModel, ClusterState {
/** Unique id for a cluster */ /** Unique id for a cluster */
public readonly id: ClusterId; public readonly id: ClusterId;
private kubeCtl: Kubectl; private kubeCtl: Kubectl | undefined;
/** /**
* Context handler * Context handler
* *
* @internal * @internal
*/ */
public contextHandler: ContextHandler; protected readonly _contextHandler: ClusterContextHandler | undefined;
protected proxyKubeconfigManager: KubeconfigManager; protected readonly _proxyKubeconfigManager: KubeconfigManager | undefined;
protected eventsDisposer = disposer(); protected readonly eventsDisposer = disposer();
protected activated = false; 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() { get whenReady() {
return when(() => this.ready); return when(() => this.ready);
@ -64,21 +79,21 @@ export class Cluster implements ClusterModel, ClusterState {
* *
* @observable * @observable
*/ */
@observable contextName: string; @observable contextName!: string;
/** /**
* Path to kubeconfig * Path to kubeconfig
* *
* @observable * @observable
*/ */
@observable kubeConfigPath: string; @observable kubeConfigPath!: string;
/** /**
* @deprecated * @deprecated
*/ */
@observable workspace: string; @observable workspace?: string;
/** /**
* @deprecated * @deprecated
*/ */
@observable workspaces: string[]; @observable workspaces?: string[];
/** /**
* Kubernetes API server URL * Kubernetes API server URL
* *
@ -215,7 +230,7 @@ export class Cluster implements ClusterModel, ClusterState {
* @computed * @computed
* @internal * @internal
*/ */
@computed get defaultNamespace(): string { @computed get defaultNamespace(): string | undefined {
return this.preferences.defaultNamespace; return this.preferences.defaultNamespace;
} }
@ -231,12 +246,20 @@ export class Cluster implements ClusterModel, ClusterState {
throw validationError; 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) { if (ipcMain) {
// for the time being, until renderer gets its own cluster type // for the time being, until renderer gets its own cluster type
this.contextHandler = this.dependencies.createContextHandler(this); this._contextHandler = this.dependencies.createContextHandler(this);
this.proxyKubeconfigManager = this.dependencies.createKubeconfigManager(this); this._proxyKubeconfigManager = this.dependencies.createKubeconfigManager(this);
logger.debug(`[CLUSTER]: Cluster init success`, { logger.debug(`[CLUSTER]: Cluster init success`, {
id: this.id, id: this.id,
@ -255,6 +278,7 @@ export class Cluster implements ClusterModel, ClusterState {
// Note: do not assign ID as that should never be updated // Note: do not assign ID as that should never be updated
this.kubeConfigPath = model.kubeConfigPath; this.kubeConfigPath = model.kubeConfigPath;
this.contextName = model.contextName;
if (model.workspace) { if (model.workspace) {
this.workspace = model.workspace; this.workspace = model.workspace;
@ -264,10 +288,6 @@ export class Cluster implements ClusterModel, ClusterState {
this.workspaces = model.workspaces; this.workspaces = model.workspaces;
} }
if (model.contextName) {
this.contextName = model.contextName;
}
if (model.preferences) { if (model.preferences) {
this.preferences = model.preferences; this.preferences = model.preferences;
} }
@ -373,8 +393,7 @@ export class Cluster implements ClusterModel, ClusterState {
@action @action
async reconnect() { async reconnect() {
logger.info(`[CLUSTER]: reconnect`, this.getMeta()); logger.info(`[CLUSTER]: reconnect`, this.getMeta());
this.contextHandler?.stopServer(); await this.contextHandler?.restartServer();
await this.contextHandler?.ensureServer();
this.disconnected = false; this.disconnected = false;
} }
@ -497,32 +516,36 @@ export class Cluster implements ClusterModel, ClusterState {
} catch (error) { } catch (error) {
logger.error(`[CLUSTER]: Failed to connect to "${this.contextName}": ${error}`); logger.error(`[CLUSTER]: Failed to connect to "${this.contextName}": ${error}`);
if (error.statusCode) { if (isRequestError(error)) {
if (error.statusCode >= 400 && error.statusCode < 500) { if (error.statusCode) {
this.broadcastConnectUpdate("Invalid credentials", true); 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); this.broadcastConnectUpdate(error.error || error.message, true);
return ClusterStatus.Offline;
}
if (error.failed === true) {
if (error.timedOut === true) {
this.broadcastConnectUpdate("Connection timed out", true);
return ClusterStatus.Offline; 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; return ClusterStatus.Offline;
} }
} }
@ -609,7 +632,7 @@ export class Cluster implements ClusterModel, ClusterState {
return await listNamespaces(); return await listNamespaces();
} catch (error) { } catch (error) {
const ctx = proxyConfig.getContextObject(this.contextName); 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) { if (namespaceList.length === 0 && error instanceof HttpError && error.statusCode === 403) {
const { response } = error as HttpError & { response: Response }; const { response } = error as HttpError & { response: Response };

View File

@ -6,5 +6,8 @@ import { getInjectionToken } from "@ogre-tools/injectable";
import type { ClusterModel } from "../cluster-types"; import type { ClusterModel } from "../cluster-types";
import type { Cluster } from "./cluster"; import type { Cluster } from "./cluster";
export const createClusterInjectionToken = export type CreateCluster = (model: ClusterModel) => Cluster;
getInjectionToken<(model: ClusterModel) => Cluster>({ id: "create-cluster-token" });
export const createClusterInjectionToken = getInjectionToken<CreateCluster>({
id: "create-cluster-token",
});

View File

@ -5,6 +5,7 @@
import type { KubeConfig } from "@kubernetes/client-node"; import type { KubeConfig } from "@kubernetes/client-node";
import { CoreV1Api } from "@kubernetes/client-node"; import { CoreV1Api } from "@kubernetes/client-node";
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { isDefined } from "../utils";
export type ListNamespaces = () => Promise<string[]>; export type ListNamespaces = () => Promise<string[]>;
@ -14,7 +15,9 @@ export function listNamespaces(config: KubeConfig): ListNamespaces {
return async () => { return async () => {
const { body: { items }} = await coreApi.listNamespace(); const { body: { items }} = await coreApi.listNamespace();
return items.map(ns => ns.metadata.name); return items
.map(ns => ns.metadata?.name)
.filter(isDefined);
}; };
} }

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * 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 { BaseStore } from "./base-store";
import migrations from "../migrations/hotbar-store"; import migrations from "../migrations/hotbar-store";
import { toJS } from "./utils"; import { toJS } from "./utils";
@ -33,7 +33,7 @@ interface Dependencies {
export class HotbarStore extends BaseStore<HotbarStoreModel> { export class HotbarStore extends BaseStore<HotbarStoreModel> {
readonly displayName = "HotbarStore"; readonly displayName = "HotbarStore";
@observable hotbars: Hotbar[] = []; @observable hotbars: Hotbar[] = [];
@observable private _activeHotbarId: string; @observable private _activeHotbarId!: string;
constructor(private dependencies: Dependencies) { constructor(private dependencies: Dependencies) {
super({ super({
@ -120,8 +120,22 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
return toJS(model); return toJS(model);
} }
getActive() { getActive(): Hotbar {
return this.getById(this.activeHotbarId); 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) { 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); const index = this.hotbars.findIndex((hotbar) => hotbar.id === id);
if (index < 0) { if (index < 0) {

View File

@ -10,7 +10,7 @@ import { tuple } from "./utils";
export interface HotbarItem { export interface HotbarItem {
entity: { entity: {
uid: string; uid: string;
name?: string; name: string;
source?: string; source?: string;
}; };
params?: { params?: {

View File

@ -6,5 +6,5 @@ import type { Channel } from "../channel";
export const createChannel = <Message>(name: string): Channel<Message> => ({ export const createChannel = <Message>(name: string): Channel<Message> => ({
name, name,
_template: null, _template: null as never,
}); });

View File

@ -53,7 +53,7 @@ describe("type enforced ipc tests", () => {
const source = new EventEmitter(); const source = new EventEmitter();
const listener = () => called += 1; const listener = () => called += 1;
const results = [true, false, true]; 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"; const channel = "foobar";
onCorrect({ source, listener, verifier, channel }); onCorrect({ source, listener, verifier, channel });

View File

@ -13,8 +13,6 @@ export interface ItemObject {
} }
export abstract class ItemStore<Item extends ItemObject> { export abstract class ItemStore<Item extends ItemObject> {
abstract loadAll(...args: any[]): Promise<void | Item[]>;
protected defaultSorting = (item: Item) => item.getName(); protected defaultSorting = (item: Item) => item.getName();
@observable failedLoading = false; @observable failedLoading = false;
@ -44,8 +42,7 @@ export abstract class ItemStore<Item extends ItemObject> {
return this.items.length; return this.items.length;
} }
getByName(name: string, ...args: any[]): Item; getByName(name: string): Item | undefined {
getByName(name: string): Item {
return this.items.find(item => item.getName() === name); 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 @action
protected async loadItem(request: () => Promise<Item>, sortItems = true) { protected async loadItem(request: () => Promise<Item>, sortItems = true) {
const item = await Promise.resolve(request()).catch(() => null); 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); if (sortItems) items = this.sortItems(items);
this.items.replace(items); this.items.replace(items);
} }
return item;
} }
return item;
} }
@action @action

View File

@ -3,18 +3,21 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * 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 { apiManager } from "../api-manager";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { KubeObjectStore } from "../kube-object.store";
class TestApi extends KubeApi<KubeObject> { class TestApi extends KubeApi<KubeObject> {
protected async checkPreferredVersion() { protected async checkPreferredVersion() {
return; return;
} }
} }
class TestStore extends KubeObjectStore<KubeObject, TestApi> {
}
describe("ApiManager", () => { describe("ApiManager", () => {
describe("registerApi", () => { describe("registerApi", () => {
it("re-register store if apiBase changed", async () => { it("re-register store if apiBase changed", async () => {
@ -26,22 +29,23 @@ describe("ApiManager", () => {
fallbackApiBases: [fallbackApiBase], fallbackApiBases: [fallbackApiBase],
checkPreferredVersion: true, checkPreferredVersion: true,
}); });
const kubeStore = new TestStore(kubeApi);
apiManager.registerApi(apiBase, kubeApi); apiManager.registerApi(apiBase, kubeApi);
// Define to use test api for ingress store // Define to use test api for ingress store
Object.defineProperty(ingressStore, "api", { value: kubeApi }); Object.defineProperty(kubeStore, "api", { value: kubeApi });
apiManager.registerStore(ingressStore, [kubeApi]); apiManager.registerStore(kubeStore, [kubeApi]);
// Test that store is returned with original apiBase // 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 // Change apiBase similar as checkPreferredVersion does
Object.defineProperty(kubeApi, "apiBase", { value: fallbackApiBase }); Object.defineProperty(kubeApi, "apiBase", { value: fallbackApiBase });
apiManager.registerApi(fallbackApiBase, kubeApi); apiManager.registerApi(fallbackApiBase, kubeApi);
// Test that store is returned with new apiBase // Test that store is returned with new apiBase
expect(apiManager.getStore(kubeApi)).toBe(ingressStore); expect(apiManager.getStore(kubeApi)).toBe(kubeStore);
}); });
}); });
}); });

View File

@ -16,8 +16,15 @@ describe("Crds", () => {
name: "foo", name: "foo",
resourceVersion: "12345", resourceVersion: "12345",
uid: "12345", uid: "12345",
selfLink: "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/foo",
}, },
spec: { spec: {
group: "foo.bar",
names: {
kind: "Foo",
plural: "foos",
},
scope: "Namespaced",
versions: [ versions: [
{ {
name: "123", name: "123",
@ -44,8 +51,15 @@ describe("Crds", () => {
name: "foo", name: "foo",
resourceVersion: "12345", resourceVersion: "12345",
uid: "12345", uid: "12345",
selfLink: "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/foo",
}, },
spec: { spec: {
group: "foo.bar",
names: {
kind: "Foo",
plural: "foos",
},
scope: "Namespaced",
versions: [ versions: [
{ {
name: "123", name: "123",
@ -72,8 +86,15 @@ describe("Crds", () => {
name: "foo", name: "foo",
resourceVersion: "12345", resourceVersion: "12345",
uid: "12345", uid: "12345",
selfLink: "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/foo",
}, },
spec: { spec: {
group: "foo.bar",
names: {
kind: "Foo",
plural: "foos",
},
scope: "Namespaced",
versions: [ versions: [
{ {
name: "123", name: "123",
@ -100,8 +121,15 @@ describe("Crds", () => {
name: "foo", name: "foo",
resourceVersion: "12345", resourceVersion: "12345",
uid: "12345", uid: "12345",
selfLink: "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/foo",
}, },
spec: { spec: {
group: "foo.bar",
names: {
kind: "Foo",
plural: "foos",
},
scope: "Namespaced",
version: "abc", version: "abc",
versions: [ versions: [
{ {
@ -129,6 +157,7 @@ describe("Crds", () => {
name: "foo", name: "foo",
resourceVersion: "12345", resourceVersion: "12345",
uid: "12345", uid: "12345",
selfLink: "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/foo",
}, },
spec: { spec: {
version: "abc", version: "abc",

View File

@ -3,11 +3,13 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * 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"; import type { KubeJsonApi } from "../kube-json-api";
class DeploymentApiTest extends DeploymentApi { class DeploymentApiTest extends DeploymentApi {
public setRequest(request: any) { declare protected request: KubeJsonApi;
public setRequest(request: KubeJsonApi) {
this.request = request; this.request = request;
} }
} }
@ -18,7 +20,7 @@ describe("DeploymentApi", () => {
patch: () => ({}), patch: () => ({}),
} as unknown as KubeJsonApi; } as unknown as KubeJsonApi;
const sub = new DeploymentApiTest({ objectConstructor: Deployment }); const sub = new DeploymentApiTest();
sub.setRequest(requestMock); sub.setRequest(requestMock);

View File

@ -3,42 +3,26 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { EndpointSubset } from "../endpoints"; import { formatEndpointSubset } from "../endpoints";
describe("endpoint tests", () => { describe("endpoint tests", () => {
describe("EndpointSubset", () => { describe("EndpointSubset", () => {
it.each([ it("formatEndpointSubset should be addresses X ports", () => {
4, const formatted = formatEndpointSubset({
false,
null,
{},
[],
"ahe",
/a/,
])("should always initialize fields when given %j", (data: any) => {
const sub = new EndpointSubset(data);
expect(sub.addresses).toStrictEqual([]);
expect(sub.notReadyAddresses).toStrictEqual([]);
expect(sub.ports).toStrictEqual([]);
});
it("toString should be addresses X ports", () => {
const sub = new EndpointSubset({
addresses: [{ addresses: [{
ip: "1.1.1.1", ip: "1.1.1.1",
}, { }, {
ip: "1.1.1.2", ip: "1.1.1.2",
}] as any, }],
notReadyAddresses: [], notReadyAddresses: [],
ports: [{ ports: [{
port: "81", port: 81,
}, { }, {
port: "82", port: 82,
}] as any, }],
}); });
expect(sub.toString()).toBe("1.1.1.1:81, 1.1.1.1:82, 1.1.1.2:81, 1.1.1.2:82"); expect(formatted).toBe("1.1.1.1:81, 1.1.1.1:82, 1.1.1.2:81, 1.1.1.2:82");
}); });
}); });
}); });

View File

@ -9,33 +9,33 @@ import { HelmChart } from "../endpoints/helm-charts.api";
describe("HelmChart tests", () => { describe("HelmChart tests", () => {
describe("HelmChart.create() tests", () => { describe("HelmChart.create() tests", () => {
it("should throw on non-object input", () => { it("should throw on non-object input", () => {
expect(() => HelmChart.create("" 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 any)).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 any)).toThrowError('"value" must be of type object'); expect(() => HelmChart.create(false as never)).toThrowError('"value" must be of type object');
expect(() => HelmChart.create([] as any)).toThrowError('"value" must be of type object'); expect(() => HelmChart.create([] as never)).toThrowError('"value" must be of type object');
expect(() => HelmChart.create(Symbol() as any)).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", () => { 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({ expect(() => HelmChart.create({
apiVersion: "!", apiVersion: "!",
} as any)).toThrowError('"name" is required'); } as never)).toThrowError('"name" is required');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "!", apiVersion: "!",
name: "!", name: "!",
} as any)).toThrowError('"version" is required'); } as never)).toThrowError('"version" is required');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "!", apiVersion: "!",
name: "!", name: "!",
version: "!", version: "!",
} as any)).toThrowError('"repo" is required'); } as never)).toThrowError('"repo" is required');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "!", apiVersion: "!",
name: "!", name: "!",
version: "!", version: "!",
repo: "!", repo: "!",
} as any)).toThrowError('"created" is required'); } as never)).toThrowError('"created" is required');
}); });
it("should throw on fields being wrong type", () => { it("should throw on fields being wrong type", () => {
@ -46,7 +46,7 @@ describe("HelmChart tests", () => {
repo: "!", repo: "!",
created: "!", created: "!",
digest: "!", digest: "!",
} as any)).toThrowError('"apiVersion" must be a string'); } as never)).toThrowError('"apiVersion" must be a string');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "1", apiVersion: "1",
name: 1, name: 1,
@ -54,7 +54,7 @@ describe("HelmChart tests", () => {
repo: "!", repo: "!",
created: "!", created: "!",
digest: "!", digest: "!",
} as any)).toThrowError('"name" must be a string'); } as never)).toThrowError('"name" must be a string');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "!", apiVersion: "!",
name: "!", name: "!",
@ -62,7 +62,7 @@ describe("HelmChart tests", () => {
repo: "!", repo: "!",
created: "!", created: "!",
digest: 1, digest: 1,
} as any)).toThrowError('"digest" must be a string'); } as never)).toThrowError('"digest" must be a string');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "1", apiVersion: "1",
name: "", name: "",
@ -70,7 +70,7 @@ describe("HelmChart tests", () => {
repo: "!", repo: "!",
created: "!", created: "!",
digest: "!", digest: "!",
} as any)).toThrowError('"version" must be a string'); } as never)).toThrowError('"version" must be a string');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "1", apiVersion: "1",
name: "1", name: "1",
@ -78,7 +78,7 @@ describe("HelmChart tests", () => {
repo: 1, repo: 1,
created: "!", created: "!",
digest: "!", digest: "!",
} as any)).toThrowError('"repo" must be a string'); } as never)).toThrowError('"repo" must be a string');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "1", apiVersion: "1",
name: "1", name: "1",
@ -86,7 +86,7 @@ describe("HelmChart tests", () => {
repo: "1", repo: "1",
created: 1, created: 1,
digest: "a", digest: "a",
} as any)).toThrowError('"created" must be a string'); } as never)).toThrowError('"created" must be a string');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "1", apiVersion: "1",
name: "1", name: "1",
@ -94,7 +94,7 @@ describe("HelmChart tests", () => {
repo: "1", repo: "1",
created: "!", created: "!",
digest: 1, digest: 1,
} as any)).toThrowError('"digest" must be a string'); } as never)).toThrowError('"digest" must be a string');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "1", apiVersion: "1",
name: "1", name: "1",
@ -103,7 +103,7 @@ describe("HelmChart tests", () => {
digest: "1", digest: "1",
created: "!", created: "!",
kubeVersion: 1, kubeVersion: 1,
} as any)).toThrowError('"kubeVersion" must be a string'); } as never)).toThrowError('"kubeVersion" must be a string');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "1", apiVersion: "1",
name: "1", name: "1",
@ -112,7 +112,7 @@ describe("HelmChart tests", () => {
digest: "1", digest: "1",
created: "!", created: "!",
description: 1, description: 1,
} as any)).toThrowError('"description" must be a string'); } as never)).toThrowError('"description" must be a string');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "1", apiVersion: "1",
name: "1", name: "1",
@ -121,7 +121,7 @@ describe("HelmChart tests", () => {
digest: "1", digest: "1",
created: "!", created: "!",
home: 1, home: 1,
} as any)).toThrowError('"home" must be a string'); } as never)).toThrowError('"home" must be a string');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "1", apiVersion: "1",
name: "1", name: "1",
@ -130,7 +130,7 @@ describe("HelmChart tests", () => {
digest: "1", digest: "1",
created: "!", created: "!",
engine: 1, engine: 1,
} as any)).toThrowError('"engine" must be a string'); } as never)).toThrowError('"engine" must be a string');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "1", apiVersion: "1",
name: "1", name: "1",
@ -139,7 +139,7 @@ describe("HelmChart tests", () => {
digest: "1", digest: "1",
created: "!", created: "!",
icon: 1, icon: 1,
} as any)).toThrowError('"icon" must be a string'); } as never)).toThrowError('"icon" must be a string');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "1", apiVersion: "1",
name: "1", name: "1",
@ -148,7 +148,7 @@ describe("HelmChart tests", () => {
digest: "1", digest: "1",
created: "!", created: "!",
appVersion: 1, appVersion: 1,
} as any)).toThrowError('"appVersion" must be a string'); } as never)).toThrowError('"appVersion" must be a string');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "1", apiVersion: "1",
name: "1", name: "1",
@ -157,7 +157,7 @@ describe("HelmChart tests", () => {
digest: "1", digest: "1",
created: "!", created: "!",
tillerVersion: 1, tillerVersion: 1,
} as any)).toThrowError('"tillerVersion" must be a string'); } as never)).toThrowError('"tillerVersion" must be a string');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "1", apiVersion: "1",
name: "1", name: "1",
@ -166,7 +166,7 @@ describe("HelmChart tests", () => {
digest: "1", digest: "1",
created: "!", created: "!",
deprecated: 1, deprecated: 1,
} as any)).toThrowError('"deprecated" must be a boolean'); } as never)).toThrowError('"deprecated" must be a boolean');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "1", apiVersion: "1",
name: "1", name: "1",
@ -175,7 +175,7 @@ describe("HelmChart tests", () => {
digest: "1", digest: "1",
created: "!", created: "!",
keywords: 1, keywords: 1,
} as any)).toThrowError('"keywords" must be an array'); } as never)).toThrowError('"keywords" must be an array');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "1", apiVersion: "1",
name: "1", name: "1",
@ -184,7 +184,7 @@ describe("HelmChart tests", () => {
digest: "1", digest: "1",
created: "!", created: "!",
sources: 1, sources: 1,
} as any)).toThrowError('"sources" must be an array'); } as never)).toThrowError('"sources" must be an array');
expect(() => HelmChart.create({ expect(() => HelmChart.create({
apiVersion: "1", apiVersion: "1",
name: "1", name: "1",
@ -193,7 +193,7 @@ describe("HelmChart tests", () => {
digest: "1", digest: "1",
created: "!", created: "!",
maintainers: 1, maintainers: 1,
} as any)).toThrowError('"maintainers" must be an array'); } as never)).toThrowError('"maintainers" must be an array');
}); });
it("should filter non-string keywords", () => { it("should filter non-string keywords", () => {
@ -204,10 +204,10 @@ describe("HelmChart tests", () => {
repo: "1", repo: "1",
digest: "1", digest: "1",
created: "!", 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", () => { it("should filter non-string sources", () => {
@ -218,10 +218,10 @@ describe("HelmChart tests", () => {
repo: "1", repo: "1",
digest: "1", digest: "1",
created: "!", 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", () => { it("should filter invalid maintainers", () => {
@ -236,10 +236,10 @@ describe("HelmChart tests", () => {
name: "a", name: "a",
email: "b", email: "b",
url: "c", url: "c",
}] as any, }] as never,
}); });
expect(chart.maintainers).toStrictEqual([{ expect(chart?.maintainers).toStrictEqual([{
name: "a", name: "a",
email: "b", email: "b",
url: "c", url: "c",
@ -261,9 +261,9 @@ describe("HelmChart tests", () => {
name: "a", name: "a",
email: "b", email: "b",
url: "c", url: "c",
}] as any, }] as never,
"asdjhajksdhadjks": 1, "asdjhajksdhadjks": 1,
} as any); } as never);
expect(warnFn).toHaveBeenCalledWith("HelmChart data has unexpected fields", { expect(warnFn).toHaveBeenCalledWith("HelmChart data has unexpected fields", {
original: anyObject(), original: anyObject(),

View File

@ -14,6 +14,8 @@ describe("computeRuleDeclarations", () => {
name: "foo", name: "foo",
resourceVersion: "1", resourceVersion: "1",
uid: "bar", uid: "bar",
namespace: "default",
selfLink: "/apis/networking.k8s.io/v1/ingresses/default/foo",
}, },
}); });
@ -21,6 +23,7 @@ describe("computeRuleDeclarations", () => {
host: "foo.bar", host: "foo.bar",
http: { http: {
paths: [{ paths: [{
pathType: "Exact",
backend: { backend: {
service: { service: {
name: "my-service", name: "my-service",
@ -44,6 +47,8 @@ describe("computeRuleDeclarations", () => {
name: "foo", name: "foo",
resourceVersion: "1", resourceVersion: "1",
uid: "bar", uid: "bar",
namespace: "default",
selfLink: "/apis/networking.k8s.io/v1/ingresses/default/foo",
}, },
}); });
@ -55,6 +60,7 @@ describe("computeRuleDeclarations", () => {
host: "foo.bar", host: "foo.bar",
http: { http: {
paths: [{ paths: [{
pathType: "Exact",
backend: { backend: {
service: { service: {
name: "my-service", name: "my-service",
@ -78,6 +84,8 @@ describe("computeRuleDeclarations", () => {
name: "foo", name: "foo",
resourceVersion: "1", resourceVersion: "1",
uid: "bar", uid: "bar",
namespace: "default",
selfLink: "/apis/networking.k8s.io/v1/ingresses/default/foo",
}, },
}); });
@ -91,6 +99,7 @@ describe("computeRuleDeclarations", () => {
host: "foo.bar", host: "foo.bar",
http: { http: {
paths: [{ paths: [{
pathType: "Exact",
backend: { backend: {
service: { service: {
name: "my-service", name: "my-service",

View File

@ -19,7 +19,7 @@ import { parseKubeApi } from "../kube-api-parse";
/** /**
* [<input-url>, <expected-result>] * [<input-url>, <expected-result>]
*/ */
type KubeApiParseTestData = [string, Required<IKubeApiParsed>]; type KubeApiParseTestData = [string, IKubeApiParsed];
const tests: KubeApiParseTestData[] = [ const tests: KubeApiParseTestData[] = [
["/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions/prometheuses.monitoring.coreos.com", { ["/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) => { it.each(throwtests)("testing %j should throw", (url) => {
expect(() => parseKubeApi(url)).toThrowError("invalid apiPath"); expect(() => parseKubeApi(url as never)).toThrowError("invalid apiPath");
}); });
}); });

View File

@ -3,7 +3,6 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * 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 { forRemoteCluster, KubeApi } from "../kube-api";
import { KubeJsonApi } from "../kube-json-api"; import { KubeJsonApi } from "../kube-json-api";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
@ -12,11 +11,12 @@ import { delay } from "../../utils/delay";
import { PassThrough } from "stream"; import { PassThrough } from "stream";
import type { ApiManager } from "../api-manager"; import type { ApiManager } from "../api-manager";
import { 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"); jest.mock("../api-manager");
const mockApiManager = apiManager as jest.Mocked<ApiManager>; const mockApiManager = apiManager as jest.Mocked<ApiManager>;
const mockFetch = fetch as FetchMock;
class TestKubeObject extends KubeObject { class TestKubeObject extends KubeObject {
static kind = "Pod"; static kind = "Pod";
@ -67,7 +67,7 @@ describe("forRemoteCluster", () => {
}, },
}, TestKubeObject); }, 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"); expect(request.url).toEqual("https://127.0.0.1:6443/api/v1/pods");
return { return {
@ -92,13 +92,13 @@ describe("KubeApi", () => {
}); });
it("uses url from apiBase if apiBase contains the resource", async () => { 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") { if (request.url === "http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1") {
return { return {
body: JSON.stringify({ body: JSON.stringify({
resources: [{ resources: [{
name: "ingresses", name: "ingresses",
}] as any[], }],
}), }),
}; };
} else if (request.url === "http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1") { } else if (request.url === "http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1") {
@ -107,13 +107,13 @@ describe("KubeApi", () => {
body: JSON.stringify({ body: JSON.stringify({
resources: [{ resources: [{
name: "ingresses", name: "ingresses",
}] as any[], }],
}), }),
}; };
} else { } else {
return { return {
body: JSON.stringify({ body: JSON.stringify({
resources: [] as any[], resources: [],
}), }),
}; };
} }
@ -138,11 +138,11 @@ describe("KubeApi", () => {
}); });
it("uses url from fallbackApiBases if apiBase lacks the resource", async () => { 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") { if (request.url === "http://127.0.0.1:9999/api-kube/apis/networking.k8s.io/v1") {
return { return {
body: JSON.stringify({ body: JSON.stringify({
resources: [] as any[], resources: [],
}), }),
}; };
} else if (request.url === "http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1") { } else if (request.url === "http://127.0.0.1:9999/api-kube/apis/extensions/v1beta1") {
@ -150,13 +150,13 @@ describe("KubeApi", () => {
body: JSON.stringify({ body: JSON.stringify({
resources: [{ resources: [{
name: "ingresses", name: "ingresses",
}] as any[], }],
}), }),
}; };
} else { } else {
return { return {
body: JSON.stringify({ body: JSON.stringify({
resources: [] as any[], resources: [],
}), }),
}; };
} }
@ -184,7 +184,7 @@ describe("KubeApi", () => {
expect.hasAssertions(); expect.hasAssertions();
const api = new TestKubeApi({ const api = new TestKubeApi({
objectConstructor: Ingress, objectConstructor: TestKubeObject,
checkPreferredVersion: true, checkPreferredVersion: true,
fallbackApiBases: ["/apis/extensions/v1beta1/ingresses"], fallbackApiBases: ["/apis/extensions/v1beta1/ingresses"],
request: { request: {
@ -214,7 +214,7 @@ describe("KubeApi", () => {
}, },
}; };
}), }),
} as any, } as Partial<KubeJsonApi> as KubeJsonApi,
}); });
await api.checkPreferredVersion(); await api.checkPreferredVersion();
@ -227,7 +227,7 @@ describe("KubeApi", () => {
expect.hasAssertions(); expect.hasAssertions();
const api = new TestKubeApi({ const api = new TestKubeApi({
objectConstructor: Pod, objectConstructor: TestKubeObject,
checkPreferredVersion: true, checkPreferredVersion: true,
fallbackApiBases: ["/api/v1beta1/pods"], fallbackApiBases: ["/api/v1beta1/pods"],
request: { request: {
@ -257,7 +257,7 @@ describe("KubeApi", () => {
}, },
}; };
}), }),
} as any, } as Partial<KubeJsonApi> as KubeJsonApi,
}); });
await api.checkPreferredVersion(); await api.checkPreferredVersion();
@ -280,10 +280,10 @@ describe("KubeApi", () => {
it("sends strategic patch by default", async () => { it("sends strategic patch by default", async () => {
expect.hasAssertions(); expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => { mockFetch.mockResponse(async request => {
expect(request.method).toEqual("PATCH"); expect(request.method).toEqual("PATCH");
expect(request.headers.get("content-type")).toMatch("strategic-merge-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 {}; return {};
}); });
@ -296,10 +296,10 @@ describe("KubeApi", () => {
it("allows to use merge patch", async () => { it("allows to use merge patch", async () => {
expect.hasAssertions(); expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => { mockFetch.mockResponse(async request => {
expect(request.method).toEqual("PATCH"); expect(request.method).toEqual("PATCH");
expect(request.headers.get("content-type")).toMatch("merge-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 {}; return {};
}); });
@ -312,10 +312,10 @@ describe("KubeApi", () => {
it("allows to use json patch", async () => { it("allows to use json patch", async () => {
expect.hasAssertions(); expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => { mockFetch.mockResponse(async request => {
expect(request.method).toEqual("PATCH"); expect(request.method).toEqual("PATCH");
expect(request.headers.get("content-type")).toMatch("json-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 {}; return {};
}); });
@ -328,10 +328,10 @@ describe("KubeApi", () => {
it("allows deep partial patch", async () => { it("allows deep partial patch", async () => {
expect.hasAssertions(); expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => { mockFetch.mockResponse(async request => {
expect(request.method).toEqual("PATCH"); expect(request.method).toEqual("PATCH");
expect(request.headers.get("content-type")).toMatch("merge-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 {}; return {};
}); });
@ -356,7 +356,7 @@ describe("KubeApi", () => {
it("sends correct request with empty namespace", async () => { it("sends correct request with empty namespace", async () => {
expect.hasAssertions(); expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => { mockFetch.mockResponse(async request => {
expect(request.method).toEqual("DELETE"); expect(request.method).toEqual("DELETE");
expect(request.url).toEqual("http://127.0.0.1:9999/api-kube/api/v1/pods/foo?propagationPolicy=Background"); 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 () => { it("sends correct request without namespace", async () => {
expect.hasAssertions(); expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => { mockFetch.mockResponse(async request => {
expect(request.method).toEqual("DELETE"); 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"); 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 () => { it("sends correct request with namespace", async () => {
expect.hasAssertions(); expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => { mockFetch.mockResponse(async request => {
expect(request.method).toEqual("DELETE"); 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"); 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 () => { it("allows to change propagationPolicy", async () => {
expect.hasAssertions(); expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => { mockFetch.mockResponse(async request => {
expect(request.method).toEqual("DELETE"); expect(request.method).toEqual("DELETE");
expect(request.url).toMatch("propagationPolicy=Orphan"); expect(request.url).toMatch("propagationPolicy=Orphan");
@ -423,9 +423,10 @@ describe("KubeApi", () => {
it("sends a valid watch request", () => { it("sends a valid watch request", () => {
const spy = jest.spyOn(request, "getResponse"); const spy = jest.spyOn(request, "getResponse");
(fetch as any).mockResponse(async () => { mockFetch.mockResponse(async () => {
return { 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 () => { it("sends timeout as a query parameter", async () => {
const spy = jest.spyOn(request, "getResponse"); const spy = jest.spyOn(request, "getResponse");
(fetch as any).mockResponse(async () => { mockFetch.mockResponse(async () => {
return { 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) => { it("aborts watch using abortController", async (done) => {
const spy = jest.spyOn(request, "getResponse"); const spy = jest.spyOn(request, "getResponse");
(fetch as any).mockResponse(async (request: Request) => { mockFetch.mockResponse(async request => {
(request as any).signal.addEventListener("abort", () => { request.signal.addEventListener("abort", () => {
done(); done();
}); });
return { 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) => { it("if request ended", (done) => {
const spy = jest.spyOn(request, "getResponse"); 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. // End the request in 100ms.
if (eventName === "end") { if (event === "end") {
setTimeout(() => { setTimeout(() => {
callback(); callback();
}, 100); }, 100);
@ -493,8 +496,8 @@ describe("KubeApi", () => {
jest.spyOn(global, "fetch").mockImplementation(async () => { jest.spyOn(global, "fetch").mockImplementation(async () => {
return { return {
ok: true, ok: true,
body: stream, body: stream as never,
} as any; } as Partial<Response> as Response;
}); });
api.watch({ api.watch({
@ -512,9 +515,10 @@ describe("KubeApi", () => {
it("if request not closed after timeout", (done) => { it("if request not closed after timeout", (done) => {
const spy = jest.spyOn(request, "getResponse"); const spy = jest.spyOn(request, "getResponse");
(fetch as any).mockResponse(async () => { mockFetch.mockResponse(async () => {
return { 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) => { it("retries only once if request ends and timeout is set", (done) => {
const spy = jest.spyOn(request, "getResponse"); 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. // End the request in 100ms.
if (eventName === "end") { if (event === "end") {
setTimeout(() => { setTimeout(() => {
callback(); callback();
}, 100); }, 100);
@ -551,8 +555,8 @@ describe("KubeApi", () => {
jest.spyOn(global, "fetch").mockImplementation(async () => { jest.spyOn(global, "fetch").mockImplementation(async () => {
return { return {
ok: true, ok: true,
body: stream, body: stream as never,
} as any; } as Partial<Response> as Response;
}); });
const timeoutSeconds = 0.5; const timeoutSeconds = 0.5;
@ -589,9 +593,9 @@ describe("KubeApi", () => {
it("should add kind and apiVersion", async () => { it("should add kind and apiVersion", async () => {
expect.hasAssertions(); expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => { mockFetch.mockResponse(async request => {
expect(request.method).toEqual("POST"); expect(request.method).toEqual("POST");
expect(JSON.parse(request.body.toString())).toEqual({ expect(JSON.parse(String(request.body))).toEqual({
kind: "Pod", kind: "Pod",
apiVersion: "v1", apiVersion: "v1",
metadata: { metadata: {
@ -643,9 +647,9 @@ describe("KubeApi", () => {
it("doesn't override metadata.labels", async () => { it("doesn't override metadata.labels", async () => {
expect.hasAssertions(); expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => { mockFetch.mockResponse(async request => {
expect(request.method).toEqual("POST"); expect(request.method).toEqual("POST");
expect(JSON.parse(request.body.toString())).toEqual({ expect(JSON.parse(String(request.body))).toEqual({
kind: "Pod", kind: "Pod",
apiVersion: "v1", apiVersion: "v1",
metadata: { metadata: {
@ -686,9 +690,9 @@ describe("KubeApi", () => {
it("doesn't override metadata.labels", async () => { it("doesn't override metadata.labels", async () => {
expect.hasAssertions(); expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => { mockFetch.mockResponse(async request => {
expect(request.method).toEqual("PUT"); expect(request.method).toEqual("PUT");
expect(JSON.parse(request.body.toString())).toEqual({ expect(JSON.parse(String(request.body))).toEqual({
metadata: { metadata: {
name: "foobar", name: "foobar",
namespace: "default", namespace: "default",

View File

@ -18,6 +18,7 @@ describe("Nodes tests", () => {
name: "bar", name: "bar",
resourceVersion: "1", resourceVersion: "1",
uid: "bat", uid: "bat",
selfLink: "/api/v1/nodes/bar",
}, },
}); });
@ -33,6 +34,7 @@ describe("Nodes tests", () => {
resourceVersion: "1", resourceVersion: "1",
uid: "bat", uid: "bat",
labels: {}, labels: {},
selfLink: "/api/v1/nodes/bar",
}, },
}); });
@ -51,6 +53,7 @@ describe("Nodes tests", () => {
"node-role.kubernetes.io/foobar": "bat", "node-role.kubernetes.io/foobar": "bat",
"hellonode-role.kubernetes.io/foobar1": "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", "node-role.kubernetes.io/foobar": "bat",
"hellonode-role.kubernetes.io//////foobar1": "bat", "hellonode-role.kubernetes.io//////foobar1": "bat",
}, },
selfLink: "/api/v1/nodes/bar",
}, },
}); });
@ -86,6 +90,7 @@ describe("Nodes tests", () => {
labels: { labels: {
"kubernetes.io/role": "master", "kubernetes.io/role": "master",
}, },
selfLink: "/api/v1/nodes/bar",
}, },
}); });
@ -103,6 +108,7 @@ describe("Nodes tests", () => {
labels: { labels: {
"node.kubernetes.io/role": "master", "node.kubernetes.io/role": "master",
}, },
selfLink: "/api/v1/nodes/bar",
}, },
}); });
@ -122,6 +128,7 @@ describe("Nodes tests", () => {
"kubernetes.io/role": "master", "kubernetes.io/role": "master",
"node.kubernetes.io/role": "master-v2-max", "node.kubernetes.io/role": "master-v2-max",
}, },
selfLink: "/api/v1/nodes/bar",
}, },
}); });

View File

@ -14,6 +14,8 @@ describe("Pod tests", () => {
name: "foobar", name: "foobar",
resourceVersion: "foobar", resourceVersion: "foobar",
uid: "foobar", uid: "foobar",
namespace: "default",
selfLink: "/api/v1/pods/default/foobar",
}, },
}); });

View File

@ -3,6 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import assert from "assert";
import { Pod } from "../endpoints"; import { Pod } from "../endpoints";
interface GetDummyPodOptions { interface GetDummyPodOptions {
@ -12,16 +13,15 @@ interface GetDummyPodOptions {
initDead?: number; initDead?: number;
} }
function getDummyPodDefaultOptions(): Required<GetDummyPodOptions> { function getDummyPod(rawOpts?: GetDummyPodOptions): Pod {
return { const opts = {
...(rawOpts ?? {}),
running: 0, running: 0,
dead: 0, dead: 0,
initDead: 0, initDead: 0,
initRunning: 0, initRunning: 0,
}; };
}
function getDummyPod(opts: GetDummyPodOptions = getDummyPodDefaultOptions()): Pod {
const pod = new Pod({ const pod = new Pod({
apiVersion: "v1", apiVersion: "v1",
kind: "Pod", kind: "Pod",
@ -29,36 +29,35 @@ function getDummyPod(opts: GetDummyPodOptions = getDummyPodDefaultOptions()): Po
uid: "1", uid: "1",
name: "test", name: "test",
resourceVersion: "v1", 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) { for (let i = 0; i < opts.running; i += 1) {
const name = `container_r_${i}`; const name = `container_r_${i}`;
pod.spec.containers.push({ pod.spec.containers?.push({
image: "dummy", image: "dummy",
imagePullPolicy: "dummy", imagePullPolicy: "dummy",
name, name,
}); });
pod.status.containerStatuses.push({ pod.status?.containerStatuses?.push({
image: "dummy", image: "dummy",
imageID: "dummy", imageID: "dummy",
name, name,
@ -75,12 +74,12 @@ function getDummyPod(opts: GetDummyPodOptions = getDummyPodDefaultOptions()): Po
for (let i = 0; i < opts.dead; i += 1) { for (let i = 0; i < opts.dead; i += 1) {
const name = `container_d_${i}`; const name = `container_d_${i}`;
pod.spec.containers.push({ pod.spec.containers?.push({
image: "dummy", image: "dummy",
imagePullPolicy: "dummy", imagePullPolicy: "dummy",
name, name,
}); });
pod.status.containerStatuses.push({ pod.status?.containerStatuses?.push({
image: "dummy", image: "dummy",
imageID: "dummy", imageID: "dummy",
name, name,
@ -100,12 +99,12 @@ function getDummyPod(opts: GetDummyPodOptions = getDummyPodDefaultOptions()): Po
for (let i = 0; i < opts.initRunning; i += 1) { for (let i = 0; i < opts.initRunning; i += 1) {
const name = `container_ir_${i}`; const name = `container_ir_${i}`;
pod.spec.initContainers.push({ pod.spec.initContainers?.push({
image: "dummy", image: "dummy",
imagePullPolicy: "dummy", imagePullPolicy: "dummy",
name, name,
}); });
pod.status.initContainerStatuses.push({ pod.status?.initContainerStatuses?.push({
image: "dummy", image: "dummy",
imageID: "dummy", imageID: "dummy",
name, name,
@ -122,12 +121,12 @@ function getDummyPod(opts: GetDummyPodOptions = getDummyPodDefaultOptions()): Po
for (let i = 0; i < opts.initDead; i += 1) { for (let i = 0; i < opts.initDead; i += 1) {
const name = `container_id_${i}`; const name = `container_id_${i}`;
pod.spec.initContainers.push({ pod.spec.initContainers?.push({
image: "dummy", image: "dummy",
imagePullPolicy: "dummy", imagePullPolicy: "dummy",
name, name,
}); });
pod.status.initContainerStatuses.push({ pod.status?.initContainerStatuses?.push({
image: "dummy", image: "dummy",
imageID: "dummy", imageID: "dummy",
name, name,
@ -253,7 +252,7 @@ describe("Pods", () => {
it("should return true if a condition isn't ready", () => { it("should return true if a condition isn't ready", () => {
const pod = getDummyPod({ running: 1 }); const pod = getDummyPod({ running: 1 });
pod.status.conditions.push({ pod.status?.conditions.push({
type: "Ready", type: "Ready",
status: "foobar", status: "foobar",
lastProbeTime: 1, lastProbeTime: 1,
@ -266,7 +265,7 @@ describe("Pods", () => {
it("should return false if a condition is non-ready", () => { it("should return false if a condition is non-ready", () => {
const pod = getDummyPod({ running: 1 }); const pod = getDummyPod({ running: 1 });
pod.status.conditions.push({ pod.status?.conditions.push({
type: "dummy", type: "dummy",
status: "foobar", status: "foobar",
lastProbeTime: 1, lastProbeTime: 1,
@ -278,8 +277,11 @@ describe("Pods", () => {
it("should return true if a current container is in a crash loop back off", () => { it("should return true if a current container is in a crash loop back off", () => {
const pod = getDummyPod({ running: 1 }); const pod = getDummyPod({ running: 1 });
const firstStatus = pod.status?.containerStatuses?.[0];
pod.status.containerStatuses[0].state = { assert(firstStatus);
firstStatus.state = {
waiting: { waiting: {
reason: "CrashLookBackOff", reason: "CrashLookBackOff",
message: "too much foobar", message: "too much foobar",
@ -292,6 +294,8 @@ describe("Pods", () => {
it("should return true if a current phase isn't running", () => { it("should return true if a current phase isn't running", () => {
const pod = getDummyPod({ running: 1 }); const pod = getDummyPod({ running: 1 });
assert(pod.status);
pod.status.phase = "not running"; pod.status.phase = "not running";
expect(pod.hasIssues()).toStrictEqual(true); expect(pod.hasIssues()).toStrictEqual(true);
@ -300,6 +304,8 @@ describe("Pods", () => {
it("should return false if a current phase is running", () => { it("should return false if a current phase is running", () => {
const pod = getDummyPod({ running: 1 }); const pod = getDummyPod({ running: 1 });
assert(pod.status);
pod.status.phase = "Running"; pod.status.phase = "Running";
expect(pod.hasIssues()).toStrictEqual(false); expect(pod.hasIssues()).toStrictEqual(false);

View File

@ -7,7 +7,8 @@ import { StatefulSet, StatefulSetApi } from "../endpoints/stateful-set.api";
import type { KubeJsonApi } from "../kube-json-api"; import type { KubeJsonApi } from "../kube-json-api";
class StatefulSetApiTest extends StatefulSetApi { class StatefulSetApiTest extends StatefulSetApi {
public setRequest(request: any) { declare protected request: KubeJsonApi;
public setRequest(request: KubeJsonApi) {
this.request = request; this.request = request;
} }
} }

View File

@ -17,4 +17,4 @@ export const apiKube = isClusterPageContext()
"Host": window.location.host, "Host": window.location.host,
}, },
}) })
: undefined; : undefined as never;

View File

@ -6,15 +6,24 @@
import type { KubeObjectStore } from "./kube-object.store"; import type { KubeObjectStore } from "./kube-object.store";
import { action, observable, makeObservable } from "mobx"; 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 { KubeApi } from "./kube-api";
import type { KubeObject } from "./kube-object"; import type { KubeJsonApiDataFor, KubeObject, ObjectReference } from "./kube-object";
import type { IKubeObjectRef } from "./kube-api-parse";
import { parseKubeApi, createKubeApiURL } from "./kube-api-parse"; 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 { export class ApiManager {
private apis = observable.map<string, KubeApi<KubeObject>>(); private readonly apis = observable.map<string, KubeApi>();
private stores = observable.map<string, KubeObjectStore<KubeObject>>(); private readonly stores = observable.map<string, KubeObjectStore>();
constructor() { constructor() {
makeObservable(this); makeObservable(this);
@ -33,27 +42,27 @@ export class ApiManager {
return iter.find(this.apis.values(), api => api.kind === kind && api.apiVersionWithGroup === apiVersion); 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 (!api.apiBase) return;
if (!this.apis.has(apiBase)) { if (!this.apis.has(apiBase)) {
this.stores.forEach((store) => { this.stores.forEach((store) => {
if (store.api === api) { if (store.api as never === api) {
this.stores.set(apiBase, store); 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) { if (!api) {
return undefined; return undefined;
} }
if (typeof api === "string") { if (typeof api === "string") {
return this.getApi(api) as KubeApi<KubeObject>; return this.getApi(api);
} }
return 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 @action
registerStore<K extends KubeObject>(store: KubeObjectStore<K>, apis: KubeApi<K>[] = [store.api]) { registerStore<K extends KubeObject>(store: KubeObjectStore<K, KubeApi<K>, KubeJsonApiDataFor<K>>, apis: KubeApi<K>[] = [store.api]): void {
apis.filter(Boolean).forEach(api => { for (const api of apis.filter(isDefined)) {
if (api.apiBase) this.stores.set(api.apiBase, store); if (api.apiBase) {
}); this.stores.set(api.apiBase, store as never);
}
}
} }
getStore<S extends KubeObjectStore<KubeObject>>(api: string | KubeApi<KubeObject>): S | undefined { getStore(api: string | undefined): KubeObjectStore | undefined;
return this.stores.get(this.resolveApi(api)?.apiBase) as S; 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 { const {
kind, apiVersion, name, kind, apiVersion = "v1", name,
namespace = parentObject?.getNs(), namespace = parentObject?.getNs(),
} = ref; } = ref;

View File

@ -3,34 +3,39 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
import type { DerivedKubeApiOptions } from "../kube-api";
import { KubeApi } 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 { 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 ClusterRoleBindingData extends KubeJsonApiData<KubeObjectMetadata<"cluster-scoped">, void, void> {
subjects?: Subject[];
export interface ClusterRoleBindingSubject { roleRef: RoleRef;
kind: ClusterRoleBindingSubjectKind;
name: string;
apiGroup?: string;
namespace?: string;
} }
export interface ClusterRoleBinding { export class ClusterRoleBinding extends KubeObject<void, void, "cluster-scoped"> {
subjects?: ClusterRoleBindingSubject[];
roleRef: {
kind: string;
name: string;
apiGroup?: string;
};
}
export class ClusterRoleBinding extends KubeObject {
static kind = "ClusterRoleBinding"; static kind = "ClusterRoleBinding";
static namespaced = false; static namespaced = false;
static apiBase = "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings"; 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() { getSubjects() {
return this.subjects || []; return this.subjects ?? [];
} }
getSubjectNames(): string { getSubjectNames(): string {
@ -38,17 +43,15 @@ export class ClusterRoleBinding extends KubeObject {
} }
} }
/** export class ClusterRoleBindingApi extends KubeApi<ClusterRoleBinding, ClusterRoleBindingData> {
* Only available within kubernetes cluster pages constructor(opts: DerivedKubeApiOptions = {}) {
*/ super({
let clusterRoleBindingApi: KubeApi<ClusterRoleBinding>; ...opts,
objectConstructor: ClusterRoleBinding,
if (isClusterPageContext()) { });
clusterRoleBindingApi = new KubeApi({ }
objectConstructor: ClusterRoleBinding,
});
} }
export { export const clusterRoleBindingApi = isClusterPageContext()
clusterRoleBindingApi, ? new ClusterRoleBindingApi()
}; : undefined as never;

View File

@ -4,39 +4,47 @@
*/ */
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
import type { DerivedKubeApiOptions } from "../kube-api";
import { KubeApi } 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 { KubeObject } from "../kube-object";
import type { AggregationRule } from "./types/aggregation-rule";
import type { PolicyRule } from "./types/policy-rule";
export interface ClusterRole { export interface ClusterRoleData extends KubeJsonApiData<KubeObjectMetadata<"cluster-scoped">, void, void> {
rules: { rules?: PolicyRule[];
verbs: string[]; aggregationRule?: AggregationRule;
apiGroups: string[];
resources: string[];
resourceNames?: string[];
}[];
} }
export class ClusterRole extends KubeObject { export class ClusterRole extends KubeObject<void, void, "cluster-scoped"> {
static kind = "ClusterRole"; static kind = "ClusterRole";
static namespaced = false; static namespaced = false;
static apiBase = "/apis/rbac.authorization.k8s.io/v1/clusterroles"; 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() { getRules() {
return this.rules || []; return this.rules || [];
} }
} }
/** export class ClusterRoleApi extends KubeApi<ClusterRole, ClusterRoleData> {
* Only available within kubernetes cluster pages constructor(opts: DerivedKubeApiOptions = {}) {
*/ super({
let clusterRoleApi: KubeApi<ClusterRole>; ...opts,
objectConstructor: ClusterRole,
if (isClusterPageContext()) { // initialize automatically only when within a cluster iframe/context });
clusterRoleApi = new KubeApi({ }
objectConstructor: ClusterRole,
});
} }
export { export const clusterRoleApi = isClusterPageContext()
clusterRoleApi, ? new ClusterRoleApi()
}; : undefined as never;

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * 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 { metricsApi } from "./metrics.api";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
@ -14,7 +14,7 @@ export class ClusterApi extends KubeApi<Cluster> {
static namespaced = true; 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 nodes = nodeNames.join("|");
const opts = { category: "cluster", nodes }; const opts = { category: "cluster", nodes };
@ -45,20 +45,19 @@ export enum ClusterStatus {
ERROR = "Error", ERROR = "Error",
} }
export interface IClusterMetrics<T = IMetrics> { export interface ClusterMetricData extends Partial<Record<string, MetricData>> {
[metric: string]: T; memoryUsage: MetricData;
memoryUsage: T; memoryRequests: MetricData;
memoryRequests: T; memoryLimits: MetricData;
memoryLimits: T; memoryCapacity: MetricData;
memoryCapacity: T; cpuUsage: MetricData;
cpuUsage: T; cpuRequests: MetricData;
cpuRequests: T; cpuLimits: MetricData;
cpuLimits: T; cpuCapacity: MetricData;
cpuCapacity: T; podUsage: MetricData;
podUsage: T; podCapacity: MetricData;
podCapacity: T; fsSize: MetricData;
fsSize: T; fsUsage: MetricData;
fsUsage: T;
} }
export interface Cluster { export interface Cluster {

View File

@ -3,28 +3,36 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { KubeObjectMetadata } from "../kube-object";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import type { KubeJsonApiData } from "../kube-json-api"; import type { KubeJsonApiData } from "../kube-json-api";
import type { DerivedKubeApiOptions } from "../kube-api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import { autoBind } from "../../utils"; import { autoBind } from "../../utils";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
export interface ConfigMap { export interface ConfigMapData extends KubeJsonApiData<KubeObjectMetadata<"namespace-scoped">, void, void> {
data: { data?: Partial<Record<string, string>>;
[param: 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 kind = "ConfigMap";
static namespaced = true; static namespaced = true;
static apiBase = "/api/v1/configmaps"; static apiBase = "/api/v1/configmaps";
constructor(data: KubeJsonApiData) { data: Partial<Record<string, string>>;
super(data); binaryData: Partial<Record<string, string>>;
immutable?: boolean;
constructor({ data, binaryData, immutable, ...rest }: ConfigMapData) {
super(rest);
autoBind(this); autoBind(this);
this.data ??= {}; this.data = data ?? {};
this.binaryData = binaryData ?? {};
this.immutable = immutable;
} }
getKeys(): string[] { getKeys(): string[] {
@ -32,17 +40,15 @@ export class ConfigMap extends KubeObject {
} }
} }
/** export class ConfigMapApi extends KubeApi<ConfigMap, ConfigMapData> {
* Only available within kubernetes cluster pages constructor(opts?: DerivedKubeApiOptions) {
*/ super({
let configMapApi: KubeApi<ConfigMap>; objectConstructor: ConfigMap,
...opts ?? {},
if (isClusterPageContext()) { });
configMapApi = new KubeApi({ }
objectConstructor: ConfigMap,
});
} }
export { export const configMapApi = isClusterPageContext()
configMapApi, ? new ConfigMapApi()
}; : undefined as never;

View File

@ -3,13 +3,14 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * 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 { 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 customResourcesRouteInjectable from "../../front-end-routing/routes/cluster/custom-resources/custom-resources/custom-resources-route.injectable";
import { buildURL } from "../../utils/buildUrl"; 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 { interface AdditionalPrinterColumnsCommon {
name: string; name: string;
@ -26,7 +27,7 @@ type AdditionalPrinterColumnsV1Beta = AdditionalPrinterColumnsCommon & {
JSONPath: string; JSONPath: string;
}; };
export interface CRDVersion { export interface CustomResourceDefinitionVersion {
name: string; name: string;
served: boolean; served: boolean;
storage: boolean; storage: boolean;
@ -34,72 +35,78 @@ export interface CRDVersion {
additionalPrinterColumns?: AdditionalPrinterColumnsV1[]; 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 { export interface CustomResourceDefinitionSpec {
group: string; group: string;
/** /**
* @deprecated for apiextensions.k8s.io/v1 but used in v1beta1 * @deprecated for apiextensions.k8s.io/v1 but used in v1beta1
*/ */
version?: string; version?: string;
names: { names: CustomResourceDefinitionNames;
plural: string;
singular: string;
kind: string;
listKind: string;
};
scope: "Namespaced" | "Cluster"; scope: "Namespaced" | "Cluster";
/** /**
* @deprecated for apiextensions.k8s.io/v1 but used in v1beta1 * @deprecated for apiextensions.k8s.io/v1 but used in v1beta1
*/ */
validation?: object; validation?: object;
versions?: CRDVersion[]; versions?: CustomResourceDefinitionVersion[];
conversion: { conversion?: CustomResourceConversion;
strategy?: string;
webhook?: any;
};
/** /**
* @deprecated for apiextensions.k8s.io/v1 but used in v1beta1 * @deprecated for apiextensions.k8s.io/v1 but used in v1beta1
*/ */
additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[]; additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[];
preserveUnknownFields?: boolean;
} }
export interface CustomResourceDefinition { export interface CustomResourceDefinitionConditionAcceptedNames {
spec: CustomResourceDefinitionSpec; plural: string;
status: { singular: string;
conditions: { kind: string;
lastTransitionTime: string; shortNames: string[];
message: string; listKind: string;
reason: string;
status: string;
type?: string;
}[];
acceptedNames: {
plural: string;
singular: string;
kind: string;
shortNames: string[];
listKind: string;
};
storedVersions: string[];
};
} }
export interface CRDApiData extends KubeJsonApiData { export interface CustomResourceDefinitionStatus {
spec: object; // TODO: make better conditions?: BaseKubeObjectCondition[];
acceptedNames: CustomResourceDefinitionConditionAcceptedNames;
storedVersions: string[];
} }
export class CustomResourceDefinition extends KubeObject { export class CustomResourceDefinition extends KubeObject<CustomResourceDefinitionStatus, CustomResourceDefinitionSpec, "cluster-scoped"> {
static kind = "CustomResourceDefinition"; static kind = "CustomResourceDefinition";
static namespaced = false; static namespaced = false;
static apiBase = "/apis/apiextensions.k8s.io/v1/customresourcedefinitions"; 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() { getResourceUrl() {
const di = getLegacyGlobalDiForExtensionApi(); const di = getLegacyGlobalDiForExtensionApi();
@ -141,12 +148,12 @@ export class CustomResourceDefinition extends KubeObject {
return this.spec.scope; return this.spec.scope;
} }
getPreferedVersion(): CRDVersion { getPreferedVersion(): CustomResourceDefinitionVersion {
const { apiVersion } = this; const { apiVersion } = this;
switch (apiVersion) { switch (apiVersion) {
case "apiextensions.k8s.io/v1": case "apiextensions.k8s.io/v1":
for (const version of this.spec.versions) { for (const version of this.spec.versions ?? []) {
if (version.storage) { if (version.storage) {
return version; return version;
} }
@ -158,7 +165,8 @@ export class CustomResourceDefinition extends KubeObject {
const additionalPrinterColumns = apc?.map(({ JSONPath, ...apc }) => ({ ...apc, jsonPath: JSONPath })); const additionalPrinterColumns = apc?.map(({ JSONPath, ...apc }) => ({ ...apc, jsonPath: JSONPath }));
return { return {
name: this.spec.version, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
name: this.spec.version!,
served: true, served: true,
storage: true, storage: true,
schema: this.spec.validation, schema: this.spec.validation,
@ -179,7 +187,7 @@ export class CustomResourceDefinition extends KubeObject {
} }
getStoredVersions() { getStoredVersions() {
return this.status.storedVersions.join(", "); return this.status?.storedVersions.join(", ") ?? "";
} }
getNames() { getNames() {
@ -216,18 +224,16 @@ export class CustomResourceDefinition extends KubeObject {
} }
} }
/** export class CustomResourceDefinitionApi extends KubeApi<CustomResourceDefinition> {
* Only available within kubernetes cluster pages constructor(opts: DerivedKubeApiOptions = {}) {
*/ super({
let crdApi: KubeApi<CustomResourceDefinition>; objectConstructor: CustomResourceDefinition,
checkPreferredVersion: true,
if (isClusterPageContext()) { ...opts,
crdApi = new KubeApi<CustomResourceDefinition>({ });
objectConstructor: CustomResourceDefinition, }
checkPreferredVersion: true,
});
} }
export { export const crdApi = isClusterPageContext()
crdApi, ? new CustomResourceDefinitionApi()
}; : undefined as never;

View File

@ -4,15 +4,22 @@
*/ */
import moment from "moment"; import moment from "moment";
import type { ObjectReference } from "../kube-object";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import type { IPodContainer } from "./pods.api";
import { formatDuration } from "../../utils/formatDuration"; import { formatDuration } from "../../utils/formatDuration";
import { autoBind } from "../../utils"; import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
import type { JobTemplateSpec } from "./types/job-template-spec";
export class CronJobApi extends KubeApi<CronJob> { export class CronJobApi extends KubeApi<CronJob> {
constructor(opts: DerivedKubeApiOptions & IgnoredKubeApiOptions = {}) {
super({
...opts,
objectConstructor: CronJob,
});
}
suspend(params: { namespace: string; name: string }) { suspend(params: { namespace: string; name: string }) {
return this.request.patch(this.getUrl(params), { return this.request.patch(this.getUrl(params), {
data: { data: {
@ -44,61 +51,33 @@ export class CronJobApi extends KubeApi<CronJob> {
} }
} }
export interface CronJob { export interface CronJobSpec {
spec: { concurrencyPolicy?: string;
schedule: string; failedJobsHistoryLimit?: number;
concurrencyPolicy: string; jobTemplate?: JobTemplateSpec;
suspend: boolean; schedule: string;
jobTemplate: { startingDeadlineSeconds?: number;
metadata: { successfulJobsHistoryLimit?: number;
creationTimestamp?: string; suspend?: boolean;
labels?: {
[key: string]: string;
};
annotations?: {
[key: string]: string;
};
};
spec: {
template: {
metadata: {
creationTimestamp?: string;
};
spec: {
containers: IPodContainer[];
restartPolicy: string;
terminationGracePeriodSeconds: number;
dnsPolicy: string;
hostPID: boolean;
schedulerName: string;
};
};
};
};
successfulJobsHistoryLimit: number;
failedJobsHistoryLimit: number;
};
status: {
lastScheduleTime?: string;
};
} }
export class CronJob extends KubeObject { export interface CronJobStatus {
static kind = "CronJob"; lastScheduleTime?: string;
static namespaced = true; lastSuccessfulTime?: string;
static apiBase = "/apis/batch/v1beta1/cronjobs"; active?: ObjectReference[];
}
constructor(data: KubeJsonApiData) { export class CronJob extends KubeObject<CronJobStatus, CronJobSpec, "namespace-scoped"> {
super(data); static readonly kind = "CronJob";
autoBind(this); static readonly namespaced = true;
} static readonly apiBase = "/apis/batch/v1beta1/cronjobs";
getSuspendFlag() { getSuspendFlag() {
return this.spec.suspend.toString(); return (this.spec.suspend ?? false).toString();
} }
getLastScheduleTime() { getLastScheduleTime() {
if (!this.status.lastScheduleTime) return "-"; if (!this.status?.lastScheduleTime) return "-";
const diff = moment().diff(this.status.lastScheduleTime); const diff = moment().diff(this.status.lastScheduleTime);
return formatDuration(diff, true); return formatDuration(diff, true);
@ -125,17 +104,6 @@ export class CronJob extends KubeObject {
} }
} }
/** export const cronJobApi = isClusterPageContext()
* Only available within kubernetes cluster pages ? new CronJobApi()
*/ : undefined as never;
let cronJobApi: CronJobApi;
if (isClusterPageContext()) {
cronJobApi = new CronJobApi({
objectConstructor: CronJob,
});
}
export {
cronJobApi,
};

View File

@ -3,88 +3,92 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import get from "lodash/get"; import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api";
import type { IAffinity } from "../workload-kube-object";
import { WorkloadKubeObject } from "../workload-kube-object";
import { autoBind } from "../../utils";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import { metricsApi } from "./metrics.api"; import { metricsApi } from "./metrics.api";
import type { KubeJsonApiData } from "../kube-json-api"; import type { PodMetricData } from "./pods.api";
import type { IPodContainer, IPodMetrics } from "./pods.api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; 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 kind = "DaemonSet";
static namespaced = true; static namespaced = true;
static apiBase = "/apis/apps/v1/daemonsets"; static apiBase = "/apis/apps/v1/daemonsets";
constructor(data: KubeJsonApiData) { getSelectors(): string[] {
super(data); return KubeObject.stringifyLabels(this.spec.selector.matchLabels);
autoBind(this);
} }
declare spec: { getNodeSelectors(): string[] {
selector: LabelSelector; return KubeObject.stringifyLabels(this.spec.template.spec?.nodeSelector);
template: { }
metadata: {
creationTimestamp?: string; getTemplateLabels(): string[] {
labels: { return KubeObject.stringifyLabels(this.spec.template.metadata?.labels);
name: string; }
};
}; getTolerations() {
spec: { return this.spec.template.spec?.tolerations ?? [];
containers: IPodContainer[]; }
initContainers?: IPodContainer[];
restartPolicy: string; getAffinity() {
terminationGracePeriodSeconds: number; return this.spec.template.spec?.affinity;
dnsPolicy: string; }
hostPID: boolean;
affinity?: IAffinity; getAffinityNumber() {
nodeSelector?: { return Object.keys(this.getAffinity() ?? {}).length;
[selector: string]: string; }
};
securityContext: {};
schedulerName: string;
tolerations: {
key: string;
operator: string;
effect: string;
tolerationSeconds: number;
}[];
};
};
updateStrategy: {
type: string;
rollingUpdate: {
maxUnavailable: number;
};
};
revisionHistoryLimit: number;
};
declare status: {
currentNumberScheduled: number;
numberMisscheduled: number;
desiredNumberScheduled: number;
numberReady: number;
observedGeneration: number;
updatedNumberScheduled: number;
numberAvailable: number;
numberUnavailable: number;
};
getImages() { getImages() {
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []); const containers = this.spec.template?.spec?.containers ?? [];
const initContainers: IPodContainer[] = get(this, "spec.template.spec.initContainers", []); const initContainers = this.spec.template?.spec?.initContainers ?? [];
return [...containers, ...initContainers].map(container => container.image); return [...containers, ...initContainers].map(container => container.image);
} }
} }
export class DaemonSetApi extends KubeApi<DaemonSet> { 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 podSelector = daemonsets.map(daemonset => `${daemonset.getName()}-[[:alnum:]]{5}`).join("|");
const opts = { category: "pods", pods: podSelector, namespace, selector }; const opts = { category: "pods", pods: podSelector, namespace, selector };
@ -101,17 +105,6 @@ export function getMetricsForDaemonSets(daemonsets: DaemonSet[], namespace: stri
}); });
} }
/** export const daemonSetApi = isClusterPageContext()
* Only available within kubernetes cluster pages ? new DaemonSetApi()
*/ : undefined as never;
let daemonSetApi: DaemonSetApi;
if (isClusterPageContext()) {
daemonSetApi = new DaemonSetApi({
objectConstructor: DaemonSet,
});
}
export {
daemonSetApi,
};

View File

@ -5,25 +5,35 @@
import moment from "moment"; import moment from "moment";
import type { IAffinity } from "../workload-kube-object"; import type { DerivedKubeApiOptions } from "../kube-api";
import { WorkloadKubeObject } from "../workload-kube-object";
import { autoBind } from "../../utils";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import { metricsApi } from "./metrics.api"; import { metricsApi } from "./metrics.api";
import type { IPodMetrics } from "./pods.api"; import type { PodMetricData, PodSpec } from "./pods.api";
import type { KubeJsonApiData } from "../kube-json-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; 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> { export class DeploymentApi extends KubeApi<Deployment> {
constructor(opts?: DerivedKubeApiOptions) {
super({
objectConstructor: Deployment,
...opts ?? {},
});
}
protected getScaleApiUrl(params: { namespace: string; name: string }) { protected getScaleApiUrl(params: { namespace: string; name: string }) {
return `${this.getUrl(params)}/scale`; return `${this.getUrl(params)}/scale`;
} }
getReplicas(params: { namespace: string; name: string }): Promise<number> { async getReplicas(params: { namespace: string; name: string }): Promise<number> {
return this.request const { status } = await this.request.get(this.getScaleApiUrl(params));
.get(this.getScaleApiUrl(params))
.then(({ status }: any) => status?.replicas); if (isObject(status) && hasTypedProperty(status, "replicas", isNumber)) {
return status.replicas;
}
return 0;
} }
scale(params: { namespace: string; name: string }, replicas: number) { 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 podSelector = deployments.map(deployment => `${deployment.getName()}-[[:alnum:]]{9,}-[[:alnum:]]{5}`).join("|");
const opts = { category: "pods", pods: podSelector, namespace, selector }; const opts = { category: "pods", pods: podSelector, namespace, selector };
@ -78,136 +88,66 @@ export function getMetricsForDeployments(deployments: Deployment[], namespace: s
}); });
} }
interface IContainerProbe { export interface DeploymentSpec {
httpGet?: { replicas: number;
path?: string; selector: LabelSelector;
port: number; template: {
scheme: string; metadata: {
host?: string; creationTimestamp?: string;
labels: Record<string, string | undefined>;
annotations?: Record<string, string | undefined>;
};
spec: PodSpec;
}; };
exec?: { strategy: {
command: string[]; 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 kind = "Deployment";
static namespaced = true; static namespaced = true;
static apiBase = "/apis/apps/v1/deployments"; static apiBase = "/apis/apps/v1/deployments";
constructor(data: KubeJsonApiData) { getSelectors(): string[] {
super(data); return KubeObject.stringifyLabels(this.spec.selector.matchLabels);
autoBind(this);
} }
declare spec: { getNodeSelectors(): string[] {
replicas: number; return KubeObject.stringifyLabels(this.spec.template.spec.nodeSelector);
selector: LabelSelector; }
template: {
metadata: { getTemplateLabels(): string[] {
creationTimestamp?: string; return KubeObject.stringifyLabels(this.spec.template.metadata.labels);
labels: { [app: string]: string }; }
annotations?: { [app: string]: string };
}; getTolerations() {
spec: { return this.spec.template.spec.tolerations ?? [];
containers: { }
name: string;
image: string; getAffinity() {
args?: string[]; return this.spec.template.spec.affinity;
ports?: { }
name: string;
containerPort: number; getAffinityNumber() {
protocol: string; return Object.keys(this.getAffinity() ?? {}).length;
}[]; }
env?: {
name: string;
value: string;
}[];
resources: {
limits?: {
cpu: string;
memory: string;
};
requests: {
cpu: string;
memory: string;
};
};
volumeMounts?: {
name: string;
mountPath: string;
}[];
livenessProbe?: IContainerProbe;
readinessProbe?: IContainerProbe;
startupProbe?: IContainerProbe;
terminationMessagePath: string;
terminationMessagePolicy: string;
imagePullPolicy: string;
}[];
restartPolicy: string;
terminationGracePeriodSeconds: number;
dnsPolicy: string;
affinity?: IAffinity;
nodeSelector?: {
[selector: string]: string;
};
serviceAccountName: string;
serviceAccount: string;
securityContext: {};
schedulerName: string;
tolerations?: {
key: string;
operator: string;
effect: string;
tolerationSeconds: number;
}[];
volumes?: {
name: string;
configMap: {
name: string;
defaultMode: number;
optional: boolean;
};
}[];
};
};
strategy: {
type: string;
rollingUpdate: {
maxUnavailable: number;
maxSurge: number;
};
};
};
declare status: {
observedGeneration: number;
replicas: number;
updatedReplicas: number;
readyReplicas: number;
availableReplicas?: number;
unavailableReplicas?: number;
conditions: {
type: string;
status: string;
lastUpdateTime: string;
lastTransitionTime: string;
reason: string;
message: string;
}[];
};
getConditions(activeOnly = false) { getConditions(activeOnly = false) {
const { conditions } = this.status; const { conditions = [] } = this.status ?? {};
if (!conditions) return [];
if (activeOnly) { if (activeOnly) {
return conditions.filter(c => c.status === "True"); return conditions.filter(c => c.status === "True");
@ -217,7 +157,9 @@ export class Deployment extends WorkloadKubeObject {
} }
getConditionsText(activeOnly = true) { getConditionsText(activeOnly = true) {
return this.getConditions(activeOnly).map(({ type }) => type).join(" "); return this.getConditions(activeOnly)
.map(({ type }) => type)
.join(" ");
} }
getReplicas() { getReplicas() {
@ -225,14 +167,6 @@ export class Deployment extends WorkloadKubeObject {
} }
} }
let deploymentApi: DeploymentApi; export const deploymentApi = isClusterPageContext()
? new DeploymentApi()
if (isClusterPageContext()) { : undefined as never;
deploymentApi = new DeploymentApi({
objectConstructor: Deployment,
});
}
export {
deploymentApi,
};

View File

@ -4,144 +4,119 @@
*/ */
import { autoBind } from "../../utils"; import { autoBind } from "../../utils";
import type { KubeObjectMetadata, ObjectReference } from "../kube-object";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import type { DerivedKubeApiOptions } from "../kube-api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api"; import type { KubeJsonApiData } from "../kube-json-api";
import { get } from "lodash";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; 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; name?: string;
protocol: string; protocol?: string;
port: number; port: number;
} }
export interface IEndpointAddress { export interface EndpointAddress {
hostname: string; hostname?: string;
ip: string; ip: string;
nodeName: string; nodeName?: string;
targetRef?: ObjectReference;
} }
export interface IEndpointSubset { export interface EndpointSubset {
addresses: IEndpointAddress[]; addresses?: EndpointAddress[];
notReadyAddresses: IEndpointAddress[]; notReadyAddresses?: EndpointAddress[];
ports: IEndpointPort[]; ports?: EndpointPort[];
} }
interface ITargetRef { export interface EndpointsData extends KubeJsonApiData<KubeObjectMetadata<"namespace-scoped">, void, void> {
kind: string; subsets?: EndpointSubset[];
namespace: string;
name: string;
uid: string;
resourceVersion: string;
apiVersion: string;
} }
export class EndpointAddress implements IEndpointAddress { export class Endpoints extends KubeObject<void, void, "namespace-scoped"> {
hostname: string;
ip: string;
nodeName: string;
targetRef?: {
kind: string;
namespace: string;
name: string;
uid: string;
resourceVersion: string;
};
static create(data: IEndpointAddress): EndpointAddress {
return new EndpointAddress(data);
}
constructor(data: IEndpointAddress) {
Object.assign(this, data);
}
getId() {
return this.ip;
}
getName() {
return this.hostname;
}
getTargetRef(): ITargetRef {
if (this.targetRef) {
return Object.assign(this.targetRef, { apiVersion: "v1" });
} else {
return null;
}
}
}
export class EndpointSubset implements IEndpointSubset {
addresses: IEndpointAddress[];
notReadyAddresses: IEndpointAddress[];
ports: IEndpointPort[];
constructor(data: IEndpointSubset) {
this.addresses = get(data, "addresses", []);
this.notReadyAddresses = get(data, "notReadyAddresses", []);
this.ports = get(data, "ports", []);
}
getAddresses(): EndpointAddress[] {
return this.addresses.map(EndpointAddress.create);
}
getNotReadyAddresses(): EndpointAddress[] {
return this.notReadyAddresses.map(EndpointAddress.create);
}
toString(): string {
return this.addresses
.map(address => (
this.ports
.map(port => `${address.ip}:${port.port}`)
.join(", ")
))
.join(", ");
}
}
export interface Endpoint {
subsets: IEndpointSubset[];
}
export class Endpoint extends KubeObject {
static kind = "Endpoints"; static kind = "Endpoints";
static namespaced = true; static namespaced = true;
static apiBase = "/api/v1/endpoints"; static apiBase = "/api/v1/endpoints";
constructor(data: KubeJsonApiData) { subsets?: EndpointSubset[];
super(data);
constructor({ subsets, ...rest }: EndpointsData) {
super(rest);
autoBind(this); autoBind(this);
this.subsets = subsets;
} }
getEndpointSubsets(): EndpointSubset[] { getEndpointSubsets(): Required<EndpointSubset>[] {
const subsets = this.subsets || []; return this.subsets?.map(({
addresses = [],
return subsets.map(s => new EndpointSubset(s)); notReadyAddresses = [],
ports = [],
}) => ({
addresses,
notReadyAddresses,
ports,
})) ?? [];
} }
toString(): string { toString(): string {
if(this.subsets) { return this.getEndpointSubsets()
return this.getEndpointSubsets().map(es => es.toString()).join(", "); .map(formatEndpointSubset)
} else { .join(", ") || "<none>";
return "<none>";
}
} }
} }
let endpointApi: KubeApi<Endpoint>; export class EndpointsApi extends KubeApi<Endpoints, EndpointsData> {
constructor(opts: DerivedKubeApiOptions = {}) {
if (isClusterPageContext()) { super({
endpointApi = new KubeApi<Endpoint>({ objectConstructor: Endpoints,
objectConstructor: Endpoint, ...opts,
}); });
}
} }
export { export const endpointsApi = isClusterPageContext()
endpointApi, ? new EndpointsApi()
}; : undefined as never;

View File

@ -4,34 +4,39 @@
*/ */
import moment from "moment"; import moment from "moment";
import type { KubeObjectMetadata, ObjectReference } from "../kube-object";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { formatDuration } from "../../utils/formatDuration"; import { formatDuration } from "../../utils/formatDuration";
import type { DerivedKubeApiOptions } from "../kube-api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
import type { KubeJsonApiData } from "../kube-json-api";
export interface KubeEvent { export interface EventSeries {
involvedObject: { count?: number;
kind: string; lastObservedTime?: string;
namespace: string; }
name: string;
uid: string; export interface EventSource {
apiVersion: string; component?: string;
resourceVersion: string; host?: string;
fieldPath: string; }
};
reason: string; export interface KubeEventData extends KubeJsonApiData<KubeObjectMetadata, void, void> {
message: string; action?: string;
source: { count?: number;
component: string; eventTime?: string;
host: string; firstTimestamp?: string;
}; involvedObject: Required<ObjectReference>;
firstTimestamp: string; lastTimestamp?: string;
lastTimestamp: string; message?: string;
count: number; reason?: string;
type: "Normal" | "Warning" | string; related?: ObjectReference;
eventTime: null; reportingComponent?: string;
reportingComponent: string; reportingInstance?: string;
reportingInstance: string; series?: EventSeries;
source?: EventSource;
type?: string;
} }
export class KubeEvent extends KubeObject { export class KubeEvent extends KubeObject {
@ -39,14 +44,73 @@ export class KubeEvent extends KubeObject {
static namespaced = true; static namespaced = true;
static apiBase = "/api/v1/events"; 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() { isWarning() {
return this.type === "Warning"; return this.type === "Warning";
} }
getSource() { 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>; export class KubeEventApi extends KubeApi<KubeEvent, KubeEventData> {
constructor(opts: DerivedKubeApiOptions = {}) {
if (isClusterPageContext()) { super({
eventApi = new KubeApi<KubeEvent>({ objectConstructor: KubeEvent,
objectConstructor: KubeEvent, ...opts,
}); });
}
} }
export { export const eventApi = isClusterPageContext()
eventApi, ? new KubeEventApi()
}; : undefined as never;

View File

@ -7,7 +7,7 @@ import { compile } from "path-to-regexp";
import { apiBase } from "../index"; import { apiBase } from "../index";
import { stringify } from "querystring"; import { stringify } from "querystring";
import type { RequestInit } from "node-fetch"; import type { RequestInit } from "node-fetch";
import { autoBind, bifurcateArray } from "../../utils"; import { autoBind, bifurcateArray, isDefined } from "../../utils";
import Joi from "joi"; import Joi from "joi";
export type RepoHelmChartList = Record<string, RawHelmChart[]>; export type RepoHelmChartList = Record<string, RawHelmChart[]>;
@ -30,9 +30,9 @@ export async function listCharts(): Promise<HelmChart[]> {
return Object return Object
.values(data) .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" })) .map(([chart]) => HelmChart.create(chart, { onError: "log" }))
.filter(Boolean); .filter(isDefined);
} }
export interface GetChartDetailsOptions { export interface GetChartDetailsOptions {
@ -51,7 +51,7 @@ export async function getChartDetails(repo: string, name: string, { version, req
const path = endpoint({ repo, name }); const path = endpoint({ repo, name });
const { readme, ...data } = await apiBase.get<IHelmChartDetails>(`${path}?${stringify({ version })}`, undefined, reqInit); 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 { return {
readme, readme,
@ -242,7 +242,7 @@ export interface RawHelmChartDependency {
export type HelmChartDependency = Required<Omit<RawHelmChartDependency, "condition">> export type HelmChartDependency = Required<Omit<RawHelmChartDependency, "condition">>
& Pick<RawHelmChartDependency, "condition">; & Pick<RawHelmChartDependency, "condition">;
export interface HelmChart { export interface HelmChartData {
apiVersion: string; apiVersion: string;
name: string; name: string;
version: string; version: string;
@ -266,8 +266,30 @@ export interface HelmChart {
tillerVersion?: string; tillerVersion?: string;
} }
export class HelmChart { export class HelmChart implements HelmChartData {
private constructor(value: HelmChart) { 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.apiVersion = value.apiVersion;
this.name = value.name; this.name = value.name;
this.version = value.version; this.version = value.version;
@ -294,25 +316,25 @@ export class HelmChart {
} }
static create(data: RawHelmChart, { onError = "throw" }: HelmChartCreateOpts = {}): HelmChart | undefined { static create(data: RawHelmChart, { onError = "throw" }: HelmChartCreateOpts = {}): HelmChart | undefined {
const { value, error } = helmChartValidator.validate(data, { const result = helmChartValidator.validate(data, {
abortEarly: false, abortEarly: false,
}); });
if (!error) { if (!result.error) {
return new HelmChart(value); 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) { if (unknownDetails.length > 0) {
console.warn("HelmChart data has unexpected fields", { original: data, unknownFields: unknownDetails.flatMap(d => d.path) }); console.warn("HelmChart data has unexpected fields", { original: data, unknownFields: unknownDetails.flatMap(d => d.path) });
} }
if (actualErrors.length === 0) { 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") { if (onError === "throw") {
throw validationError; throw validationError;
@ -347,7 +369,7 @@ export class HelmChart {
return this.icon; return this.icon;
} }
getHome(): string { getHome(): string | undefined {
return this.home; return this.home;
} }

View File

@ -9,12 +9,12 @@ import capitalize from "lodash/capitalize";
import { apiBase } from "../index"; import { apiBase } from "../index";
import { helmChartStore } from "../../../renderer/components/+helm-charts/helm-chart.store"; import { helmChartStore } from "../../../renderer/components/+helm-charts/helm-chart.store";
import type { ItemObject } from "../../item.store"; import type { ItemObject } from "../../item.store";
import { KubeObject } from "../kube-object";
import type { JsonApiData } from "../json-api"; import type { JsonApiData } from "../json-api";
import { buildURLPositional } from "../../utils/buildUrl"; import { buildURLPositional } from "../../utils/buildUrl";
import type { KubeJsonApiData } from "../kube-json-api"; import type { KubeJsonApiData } from "../kube-json-api";
interface IReleasePayload { export interface HelmReleaseDetails {
resources: KubeJsonApiData[];
name: string; name: string;
namespace: string; namespace: string;
version: string; version: string;
@ -30,15 +30,7 @@ interface IReleasePayload {
}; };
} }
interface IReleaseRawDetails extends IReleasePayload { export interface HelmReleaseCreatePayload {
resources: KubeJsonApiData[];
}
export interface IReleaseDetails extends IReleasePayload {
resources: KubeObject[];
}
export interface IReleaseCreatePayload {
name?: string; name?: string;
repo: string; repo: string;
chart: string; chart: string;
@ -47,19 +39,19 @@ export interface IReleaseCreatePayload {
values: string; values: string;
} }
export interface IReleaseUpdatePayload { export interface HelmReleaseUpdatePayload {
repo: string; repo: string;
chart: string; chart: string;
version: string; version: string;
values: string; values: string;
} }
export interface IReleaseUpdateDetails { export interface HelmReleaseUpdateDetails {
log: string; log: string;
release: IReleaseDetails; release: HelmReleaseDetails;
} }
export interface IReleaseRevision { export interface HelmReleaseRevision {
revision: number; revision: number;
updated: string; updated: string;
status: string; status: string;
@ -85,18 +77,13 @@ export async function listReleases(namespace?: string): Promise<HelmRelease[]> {
return releases.map(toHelmRelease); 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 path = endpoint({ name, namespace });
const { resources: rawResources, ...details } = await apiBase.get<IReleaseRawDetails>(path);
const resources = rawResources.map(KubeObject.create);
return { return apiBase.get(path);
...details,
resources,
};
} }
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 { repo, chart: rawChart, values: rawValues, ...data } = payload;
const chart = `${repo}/${rawChart}`; const chart = `${repo}/${rawChart}`;
const values = yaml.load(rawValues); 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 { repo, chart: rawChart, values: rawValues, ...data } = payload;
const chart = `${repo}/${rawChart}`; const chart = `${repo}/${rawChart}`;
const values = yaml.load(rawValues); const values = yaml.load(rawValues);
@ -137,7 +124,7 @@ export async function getReleaseValues(name: string, namespace: string, all?: bo
return apiBase.get<string>(path); 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 route = "history";
const path = endpoint({ name, namespace, route }); const path = endpoint({ name, namespace, route });

View File

@ -3,93 +3,124 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * 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 { KubeObject } from "../kube-object";
import type { DerivedKubeApiOptions } from "../kube-api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
import type { OptionVarient } from "../../utils";
export enum HpaMetricType { export enum HpaMetricType {
Resource = "Resource", Resource = "Resource",
Pods = "Pods", Pods = "Pods",
Object = "Object", Object = "Object",
External = "External", External = "External",
ContainerResource = "ContainerResource",
} }
export type IHpaMetricData<T = any> = T & { export interface HorizontalPodAutoscalerMetricTarget {
target?: { kind: string;
kind: string; name: string;
name: string; apiVersion: string;
apiVersion: string; }
};
name?: string; export interface ContainerResourceMetricSource {
metricName?: string; container: string;
currentAverageUtilization?: number; name: string;
currentAverageValue?: string;
targetAverageUtilization?: number; targetAverageUtilization?: number;
targetAverageValue?: string; 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 { export interface ExternalMetricSource {
spec: { metricName: string;
scaleTargetRef: { metricSelector?: LabelSelector;
kind: string; targetAverageValue?: string;
name: string; targetValue?: string;
apiVersion: string;
};
minReplicas: number;
maxReplicas: number;
metrics: IHpaMetric[];
};
status: {
currentReplicas: number;
desiredReplicas: number;
currentMetrics: IHpaMetric[];
conditions: {
lastTransitionTime: string;
message: string;
reason: string;
status: string;
type: string;
}[];
};
} }
export class HorizontalPodAutoscaler extends KubeObject { export interface ObjectMetricSource {
static kind = "HorizontalPodAutoscaler"; averageValue?: string;
static namespaced = true; metricName: string;
static apiBase = "/apis/autoscaling/v2beta1/horizontalpodautoscalers"; 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() { getMaxPods() {
return this.spec.maxReplicas || 0; return this.spec.maxReplicas ?? 0;
} }
getMinPods() { getMinPods() {
return this.spec.minReplicas || 0; return this.spec.minReplicas ?? 0;
} }
getReplicas() { getReplicas() {
return this.status.currentReplicas; return this.status?.currentReplicas ?? 0;
}
getReadyConditions() {
return this.getConditions().filter(({ isReady }) => isReady);
} }
getConditions() { getConditions() {
if (!this.status.conditions) return []; return this.status?.conditions?.map(condition => {
return this.status.conditions.map(condition => {
const { message, reason, lastTransitionTime, status } = condition; const { message, reason, lastTransitionTime, status } = condition;
return { return {
@ -97,65 +128,139 @@ export class HorizontalPodAutoscaler extends KubeObject {
isReady: status === "True", isReady: status === "True",
tooltip: `${message || reason} (${lastTransitionTime})`, tooltip: `${message || reason} (${lastTransitionTime})`,
}; };
}); }) ?? [];
} }
getMetrics() { getMetrics() {
return this.spec.metrics || []; return this.spec.metrics ?? [];
} }
getCurrentMetrics() { getCurrentMetrics() {
return this.status.currentMetrics || []; return this.status?.currentMetrics ?? [];
} }
protected getMetricName(metric: IHpaMetric): string { protected getMetricName(metric: HorizontalPodAutoscalerMetricSpec): string {
const { type, resource, pods, object, external } = metric; switch (metric.type) {
switch (type) {
case HpaMetricType.Resource: case HpaMetricType.Resource:
return resource.name; return metric.resource.name;
case HpaMetricType.Pods: case HpaMetricType.Pods:
return pods.metricName; return metric.pods.metricName;
case HpaMetricType.Object: case HpaMetricType.Object:
return object.metricName; return metric.object.metricName;
case HpaMetricType.External: 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 protected getResourceMetricValue(currentMetric: ResourceMetricSource | undefined, targetMetric: ResourceMetricSource): MetricCurrentTarget {
getMetricValues(metric: IHpaMetric): string { return {
const metricType = metric.type.toLowerCase(); current: (
const currentMetric = this.getCurrentMetrics().find(current => currentMetric?.targetAverageUtilization
metric.type == current.type && this.getMetricName(metric) == this.getMetricName(current), ? `${currentMetric.targetAverageUtilization}%`
); : currentMetric?.targetAverageValue
const current = currentMetric ? currentMetric[metricType] : null; ),
const target = metric[metricType]; target: (
let currentValue = "unknown"; targetMetric?.targetAverageUtilization
let targetValue = "unknown"; ? `${targetMetric.targetAverageUtilization}%`
: targetMetric?.targetAverageValue
),
};
}
if (current) { protected getPodsMetricValue(currentMetric: PodsMetricSource | undefined, targetMetric: PodsMetricSource): MetricCurrentTarget {
currentValue = current.currentAverageUtilization || current.currentAverageValue || current.currentValue; return {
if (current.currentAverageUtilization) currentValue += "%"; 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) { getMetricValues(metric: HorizontalPodAutoscalerMetricSpec): string {
targetValue = target.targetAverageUtilization || target.targetAverageValue || target.targetValue; const {
if (target.targetAverageUtilization) targetValue += "%"; current = "unknown",
} target = "unknown",
} = this.getMetricCurrentTarget(metric);
return `${currentValue} / ${targetValue}`; return `${current} / ${target}`;
} }
} }
let hpaApi: KubeApi<HorizontalPodAutoscaler>; export class HorizontalPodAutoscalerApi extends KubeApi<HorizontalPodAutoscaler> {
constructor(opts?: DerivedKubeApiOptions) {
if (isClusterPageContext()) { super({
hpaApi = new KubeApi<HorizontalPodAutoscaler>({ objectConstructor: HorizontalPodAutoscaler,
objectConstructor: HorizontalPodAutoscaler, ...opts ?? {},
}); });
}
} }
export { export const horizontalPodAutoscalerApi = isClusterPageContext()
hpaApi, ? new HorizontalPodAutoscalerApi()
}; : undefined as never;

View File

@ -5,18 +5,27 @@
import type { TypedLocalObjectReference } from "../kube-object"; import type { TypedLocalObjectReference } from "../kube-object";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { autoBind, hasTypedProperty, isString, iter } from "../../utils"; import { hasTypedProperty, isString, iter } from "../../utils";
import type { IMetrics } from "./metrics.api"; import type { MetricData } from "./metrics.api";
import { metricsApi } from "./metrics.api"; import { metricsApi } from "./metrics.api";
import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
import type { RequireExactlyOne } from "type-fest"; import type { RequireExactlyOne } from "type-fest";
export class IngressApi extends KubeApi<Ingress> { 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 }; const opts = { category: "ingress", ingress, namespace };
return metricsApi.getMetrics({ return metricsApi.getMetrics({
@ -29,12 +38,11 @@ export function getMetricsForIngress(ingress: string, namespace: string): Promis
}); });
} }
export interface IIngressMetrics<T = IMetrics> { export interface IngressMetricData extends Partial<Record<string, MetricData>> {
[metric: string]: T; bytesSentSuccess: MetricData;
bytesSentSuccess: T; bytesSentFailure: MetricData;
bytesSentFailure: T; requestDurationSeconds: MetricData;
requestDurationSeconds: T; responseDurationSeconds: MetricData;
responseDurationSeconds: T;
} }
export interface ILoadBalancerIngress { 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 * Format an ingress backend into the name of the service and port
* @param backend The ingress target * @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)) { if (isExtensionsBackend(backend)) {
return `${backend.serviceName}:${backend.servicePort}`; return `${backend.serviceName}:${backend.servicePort}`;
} }
@ -87,60 +99,52 @@ export function getBackendServiceNamePort(backend: IngressBackend): string {
return "<unknown>"; return "<unknown>";
} }
export interface HTTPIngressPath {
pathType: "Exact" | "Prefix" | "ImplementationSpecific";
path?: string;
backend?: IngressBackend;
}
export interface HTTPIngressRuleValue {
paths: HTTPIngressPath[];
}
export interface IngressRule { export interface IngressRule {
host?: string; host?: string;
http?: { http?: HTTPIngressRuleValue;
paths: {
path?: string;
backend: IngressBackend;
}[];
};
} }
export interface Ingress { export interface IngressSpec {
spec?: { tls: {
tls?: { secretName: string;
secretName: string; }[];
}[]; rules?: IngressRule[];
rules?: IngressRule[]; // extensions/v1beta1
// extensions/v1beta1 backend?: ExtensionsBackend;
backend?: ExtensionsBackend; /**
/** * The default backend which is exactly on of:
* The default backend which is exactly on of: * - service
* - service * - resource
* - resource */
*/ defaultBackend?: RequireExactlyOne<NetworkingBackend & {
defaultBackend?: RequireExactlyOne<NetworkingBackend & { resource: {
resource: { apiGroup: string;
apiGroup: string; kind: string;
kind: string; name: string;
name: string;
};
}>;
};
status: {
loadBalancer: {
ingress: ILoadBalancerIngress[];
}; };
}>;
}
export interface IngressStatus {
loadBalancer: {
ingress: ILoadBalancerIngress[];
}; };
} }
export interface ComputedIngressRoute { export class Ingress extends KubeObject<IngressStatus, IngressSpec, "namespace-scoped"> {
displayAsLink: boolean; static readonly kind = "Ingress";
pathname: string; static readonly namespaced = true;
url: string; static readonly apiBase = "/apis/networking.k8s.io/v1/ingresses";
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);
}
getRules() { getRules() {
return this.spec.rules ?? []; return this.spec.rules ?? [];
@ -150,12 +154,16 @@ export class Ingress extends KubeObject {
return computeRouteDeclarations(this).map(({ url, service }) => `${url}${service}`); return computeRouteDeclarations(this).map(({ url, service }) => `${url}${service}`);
} }
getServiceNamePort(): ExtensionsBackend { getServiceNamePort(): ExtensionsBackend | undefined {
const { spec: { backend, defaultBackend } = {}} = this; const { spec: { backend, defaultBackend } = {}} = this;
const serviceName = defaultBackend?.service?.name ?? backend?.serviceName; const serviceName = defaultBackend?.service?.name ?? backend?.serviceName;
const servicePort = defaultBackend?.service?.port.number ?? defaultBackend?.service?.port.name ?? backend?.servicePort; const servicePort = defaultBackend?.service?.port.number ?? defaultBackend?.service?.port.name ?? backend?.servicePort;
if (!serviceName || !servicePort) {
return undefined;
}
return { return {
serviceName, serviceName,
servicePort, servicePort,
@ -170,14 +178,14 @@ export class Ingress extends KubeObject {
getPorts() { getPorts() {
const ports: number[] = []; const ports: number[] = [];
const { spec: { tls, rules, backend, defaultBackend }} = this; const { spec: { tls, rules = [], backend, defaultBackend }} = this;
const httpPort = 80; const httpPort = 80;
const tlsPort = 443; const tlsPort = 443;
// Note: not using the port name (string) // Note: not using the port name (string)
const servicePort = defaultBackend?.service?.port.number ?? backend?.servicePort; const servicePort = defaultBackend?.service?.port.number ?? backend?.servicePort;
if (rules && rules.length > 0) { if (rules.length > 0) {
if (rules.some(rule => Object.prototype.hasOwnProperty.call(rule, "http"))) { if (rules.some(rule => rule.http)) {
ports.push(httpPort); ports.push(httpPort);
} }
} else if (servicePort !== undefined) { } else if (servicePort !== undefined) {
@ -192,14 +200,19 @@ export class Ingress extends KubeObject {
} }
getLoadBalancers() { getLoadBalancers() {
const { status: { loadBalancer = { ingress: [] }}} = this; return this.status?.loadBalancer.ingress.map(address => (
return (loadBalancer.ingress ?? []).map(address => (
address.hostname || address.ip address.hostname || address.ip
)); )) ?? [];
} }
} }
export interface ComputedIngressRoute {
displayAsLink: boolean;
pathname: string;
url: string;
service: string;
}
export function computeRuleDeclarations(ingress: Ingress, rule: IngressRule): ComputedIngressRoute[] { export function computeRuleDeclarations(ingress: Ingress, rule: IngressRule): ComputedIngressRoute[] {
const { host = "*", http: { paths } = { paths: [] }} = rule; const { host = "*", http: { paths } = { paths: [] }} = rule;
const protocol = (ingress.spec?.tls?.length ?? 0) === 0 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)); return ingress.getRules().flatMap(rule => computeRuleDeclarations(ingress, rule));
} }
let ingressApi: IngressApi; export const ingressApi = isClusterPageContext()
? new IngressApi()
if (isClusterPageContext()) { : undefined as never;
ingressApi = new IngressApi({
objectConstructor: Ingress,
// Add fallback for Kubernetes <1.19
checkPreferredVersion: true,
fallbackApiBases: ["/apis/extensions/v1beta1/ingresses"],
});
}
export {
ingressApi,
};

View File

@ -3,88 +3,77 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import get from "lodash/get"; import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api";
import { autoBind } from "../../utils";
import type { IAffinity } from "../workload-kube-object";
import { WorkloadKubeObject } from "../workload-kube-object";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import { metricsApi } from "./metrics.api"; import { metricsApi } from "./metrics.api";
import type { KubeJsonApiData } from "../kube-json-api"; import type { IPodContainer, PodMetricData, PodSpec } from "./pods.api";
import type { IPodContainer, IPodMetrics } from "./pods.api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; 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 { export interface JobSpec {
static kind = "Job"; parallelism?: number;
static namespaced = true; completions?: number;
static apiBase = "/apis/batch/v1/jobs"; 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) { export interface JobStatus extends KubeObjectStatus {
super(data); startTime: string;
autoBind(this); 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: { getNodeSelectors(): string[] {
parallelism?: number; return KubeObject.stringifyLabels(this.spec.template.spec.nodeSelector);
completions?: number; }
backoffLimit?: number;
selector?: LabelSelector; getTemplateLabels(): string[] {
template: { return KubeObject.stringifyLabels(this.spec.template.metadata.labels);
metadata: { }
creationTimestamp?: string;
labels?: { getTolerations() {
[name: string]: string; return this.spec.template.spec.tolerations ?? [];
}; }
annotations?: {
[name: string]: string; getAffinity() {
}; return this.spec.template.spec.affinity;
}; }
spec: {
containers: IPodContainer[]; getAffinityNumber() {
restartPolicy: string; return Object.keys(this.getAffinity() ?? {}).length;
terminationGracePeriodSeconds: number; }
dnsPolicy: string;
hostPID: boolean;
affinity?: IAffinity;
nodeSelector?: {
[selector: string]: string;
};
tolerations?: {
key: string;
operator: string;
effect: string;
tolerationSeconds: number;
}[];
schedulerName: string;
};
};
containers?: IPodContainer[];
restartPolicy?: string;
terminationGracePeriodSeconds?: number;
dnsPolicy?: string;
serviceAccountName?: string;
serviceAccount?: string;
schedulerName?: string;
};
declare status: {
conditions: {
type: string;
status: string;
lastProbeTime: string;
lastTransitionTime: string;
message?: string;
}[];
startTime: string;
completionTime: string;
succeeded: number;
};
getDesiredCompletions() { getDesiredCompletions() {
return this.spec.completions || 0; return this.spec.completions ?? 0;
} }
getCompletions() { getCompletions() {
return this.status.succeeded || 0; return this.status?.succeeded ?? 0;
} }
getParallelism() { getParallelism() {
@ -94,20 +83,24 @@ export class Job extends WorkloadKubeObject {
getCondition() { getCondition() {
// Type of Job condition could be only Complete or Failed // Type of Job condition could be only Complete or Failed
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.14/#jobcondition-v1-batch // 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() { getImages() {
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []); return this.spec.template.spec.containers?.map(container => container.image) ?? [];
return [...containers].map(container => container.image);
} }
} }
export class JobApi extends KubeApi<Job> { 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 podSelector = jobs.map(job => `${job.getName()}-[[:alnum:]]{5}`).join("|");
const opts = { category: "pods", pods: podSelector, namespace, selector }; const opts = { category: "pods", pods: podSelector, namespace, selector };
@ -124,14 +117,6 @@ export function getMetricsForJobs(jobs: Job[], namespace: string, selector = "")
}); });
} }
let jobApi: JobApi; export const jobApi = isClusterPageContext()
? new JobApi()
if (isClusterPageContext()) { : undefined as never;
jobApi = new JobApi({
objectConstructor: Job,
});
}
export {
jobApi,
};

View File

@ -4,9 +4,8 @@
*/ */
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import type { DerivedKubeApiOptions } from "../kube-api";
import { KubeApi } 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"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
export enum LimitType { export enum LimitType {
@ -36,21 +35,14 @@ export interface LimitRangeItem extends LimitRangeParts {
type: string; type: string;
} }
export interface LimitRange { export interface LimitRangeSpec {
spec: { limits: LimitRangeItem[];
limits: LimitRangeItem[];
};
} }
export class LimitRange extends KubeObject { export class LimitRange extends KubeObject<void, LimitRangeSpec, "namespace-scoped"> {
static kind = "LimitRange"; static readonly kind = "LimitRange";
static namespaced = true; static readonly namespaced = true;
static apiBase = "/api/v1/limitranges"; static readonly apiBase = "/api/v1/limitranges";
constructor(data: KubeJsonApiData) {
super(data);
autoBind(this);
}
getContainerLimits() { getContainerLimits() {
return this.spec.limits.filter(limit => limit.type === LimitType.CONTAINER); return this.spec.limits.filter(limit => limit.type === LimitType.CONTAINER);
@ -65,14 +57,15 @@ export class LimitRange extends KubeObject {
} }
} }
let limitRangeApi: KubeApi<LimitRange>; export class LimitRangeApi extends KubeApi<LimitRange> {
constructor(opts?: DerivedKubeApiOptions) {
if (isClusterPageContext()) { super({
limitRangeApi = new KubeApi<LimitRange>({ objectConstructor: LimitRange,
objectConstructor: LimitRange, ...opts ?? {},
}); });
}
} }
export { export const limitRangeApi = isClusterPageContext()
limitRangeApi, ? new LimitRangeApi()
}; : undefined as never;

View File

@ -9,18 +9,18 @@ import moment from "moment";
import { apiBase } from "../index"; import { apiBase } from "../index";
import type { IMetricsQuery } from "../../../main/routes/metrics/metrics-query"; import type { IMetricsQuery } from "../../../main/routes/metrics/metrics-query";
export interface IMetrics { export interface MetricData {
status: string; status: string;
data: { data: {
resultType: string; resultType: string;
result: IMetricsResult[]; result: MetricResult[];
}; };
} }
export interface IMetricsResult { export interface MetricResult {
metric: { metric: {
[name: string]: string; [name: string]: string | undefined;
instance: string; instance?: string;
node?: string; node?: string;
pod?: string; pod?: string;
kubernetes?: string; kubernetes?: string;
@ -44,7 +44,7 @@ export interface IMetricsReqParams {
namespace?: string; // rbac-proxy validation param namespace?: string; // rbac-proxy validation param
} }
export interface IResourceMetrics<T extends IMetrics> { export interface IResourceMetrics<T extends MetricData> {
[metric: string]: T; [metric: string]: T;
cpuUsage: T; cpuUsage: T;
memoryUsage: T; memoryUsage: T;
@ -56,7 +56,7 @@ export interface IResourceMetrics<T extends IMetrics> {
} }
export const metricsApi = { 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; const { range = 3600, step = 60, namespace } = reqParams;
let { start, end } = 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) { if (!metrics?.data?.result) {
return { return {
data: { data: {
@ -90,7 +90,7 @@ export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics {
result: [{ result: [{
metric: {}, metric: {},
values: [], values: [],
} as IMetricsResult], }],
}, },
status: "", status: "",
}; };
@ -131,17 +131,17 @@ export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics {
result.push({ result.push({
metric: {}, metric: {},
values: [], values: [],
} as IMetricsResult); } as MetricResult);
} }
return metrics; 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); 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) { if (!metrics) {
return undefined; return undefined;
} }
@ -152,23 +152,24 @@ export function getItemMetrics(metrics: Record<string, IMetrics>, itemName: stri
if (!metrics[metric]?.data?.result) { if (!metrics[metric]?.data?.result) {
continue; continue;
} }
const results = metrics[metric].data.result; const results = metrics[metric]?.data.result;
const result = results.find(res => Object.values(res.metric)[0] == itemName); 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; 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 }> = {}; const result: Partial<{ [metric: string]: number }> = {};
Object.keys(metrics).forEach(metricName => { Object.keys(metrics).forEach(metricName => {
try { try {
const metric = metrics[metricName]; 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]; result[metricName] = +metric.data.result[0].values.slice(-1)[0][1];
} }
} catch { } catch {
@ -178,5 +179,5 @@ export function getMetricLastPoints(metrics: Record<string, IMetrics>) {
return result; return result;
}, {}); }, {});
return result; return result as Record<keyof T, number>;
} }

View File

@ -4,33 +4,29 @@
*/ */
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeObjectStatus } from "../kube-object";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { autoBind } from "../../../renderer/utils";
import { metricsApi } from "./metrics.api"; import { metricsApi } from "./metrics.api";
import type { IPodMetrics } from "./pods.api"; import type { PodMetricData } from "./pods.api";
import type { KubeJsonApiData } from "../kube-json-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
export enum NamespaceStatus { export enum NamespaceStatusKind {
ACTIVE = "Active", ACTIVE = "Active",
TERMINATING = "Terminating", TERMINATING = "Terminating",
} }
export interface Namespace { export interface NamespaceSpec {
status?: { finalizers?: string[];
phase: string;
};
} }
export class Namespace extends KubeObject { export interface NamespaceStatus extends KubeObjectStatus {
static kind = "Namespace"; phase?: string;
static namespaced = false; }
static apiBase = "/api/v1/namespaces";
constructor(data: KubeJsonApiData) { export class Namespace extends KubeObject<NamespaceStatus, NamespaceSpec, "cluster-scoped"> {
super(data); static readonly kind = "Namespace";
autoBind(this); static readonly namespaced = false;
} static readonly apiBase = "/api/v1/namespaces";
getStatus() { getStatus() {
return this.status?.phase ?? "-"; return this.status?.phase ?? "-";
@ -40,7 +36,7 @@ export class Namespace extends KubeObject {
export class NamespaceApi extends KubeApi<Namespace> { 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 }; const opts = { category: "pods", pods: ".*", namespace, selector };
return metricsApi.getMetrics({ return metricsApi.getMetrics({

View File

@ -5,9 +5,8 @@
import type { LabelSelector } from "../kube-object"; import type { LabelSelector } from "../kube-object";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { autoBind } from "../../utils"; import type { DerivedKubeApiOptions } from "../kube-api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
export interface IPolicyIpBlock { export interface IPolicyIpBlock {
@ -105,15 +104,10 @@ export interface NetworkPolicy {
spec: NetworkPolicySpec; spec: NetworkPolicySpec;
} }
export class NetworkPolicy extends KubeObject { export class NetworkPolicy extends KubeObject<void, NetworkPolicySpec, "namespace-scoped"> {
static kind = "NetworkPolicy"; static readonly kind = "NetworkPolicy";
static namespaced = true; static readonly namespaced = true;
static apiBase = "/apis/networking.k8s.io/v1/networkpolicies"; static readonly apiBase = "/apis/networking.k8s.io/v1/networkpolicies";
constructor(data: KubeJsonApiData) {
super(data);
autoBind(this);
}
getMatchLabels(): string[] { getMatchLabels(): string[] {
if (!this.spec.podSelector || !this.spec.podSelector.matchLabels) return []; if (!this.spec.podSelector || !this.spec.podSelector.matchLabels) return [];
@ -130,14 +124,15 @@ export class NetworkPolicy extends KubeObject {
} }
} }
let networkPolicyApi: KubeApi<NetworkPolicy>; export class NetworkPolicyApi extends KubeApi<NetworkPolicy> {
constructor(opts: DerivedKubeApiOptions = {}) {
if (isClusterPageContext()) { super({
networkPolicyApi = new KubeApi<NetworkPolicy>({ objectConstructor: NetworkPolicy,
objectConstructor: NetworkPolicy, ...opts,
}); });
}
} }
export { export const networkPolicyApi = isClusterPageContext()
networkPolicyApi, ? new NetworkPolicyApi()
}; : undefined as never;

View File

@ -3,18 +3,26 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { BaseKubeObjectCondition } from "../kube-object";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { autoBind, cpuUnitsToNumber, iter, unitsToBytes } from "../../../renderer/utils"; import { cpuUnitsToNumber, unitsToBytes } from "../../../renderer/utils";
import type { IMetrics } from "./metrics.api"; import type { MetricData } from "./metrics.api";
import { metricsApi } from "./metrics.api"; import { metricsApi } from "./metrics.api";
import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; 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" }; const opts = { category: "nodes" };
return metricsApi.getMetrics({ return metricsApi.getMetrics({
@ -29,16 +37,15 @@ export function getMetricsForAllNodes(): Promise<INodeMetrics> {
}); });
} }
export interface INodeMetrics<T = IMetrics> { export interface NodeMetricData extends Partial<Record<string, MetricData>> {
[metric: string]: T; memoryUsage: MetricData;
memoryUsage: T; workloadMemoryUsage: MetricData;
workloadMemoryUsage: T; memoryCapacity: MetricData;
memoryCapacity: T; memoryAllocatableCapacity: MetricData;
memoryAllocatableCapacity: T; cpuUsage: MetricData;
cpuUsage: T; cpuCapacity: MetricData;
cpuCapacity: T; fsUsage: MetricData;
fsUsage: T; fsSize: MetricData;
fsSize: T;
} }
export interface NodeTaint { export interface NodeTaint {
@ -56,116 +63,127 @@ export function formatNodeTaint(taint: NodeTaint): string {
return `${taint.key}:${taint.effect}`; return `${taint.key}:${taint.effect}`;
} }
export interface NodeCondition { export interface NodeCondition extends BaseKubeObjectCondition {
type: string; /**
status: string; * Last time we got an update on a given condition.
*/
lastHeartbeatTime?: string; lastHeartbeatTime?: string;
lastTransitionTime?: string;
reason?: string;
message?: string;
}
export interface Node {
spec: {
podCIDR?: string;
podCIDRs?: string[];
providerID?: string;
/**
* @deprecated see https://issues.k8s.io/61966
*/
externalID?: string;
taints?: NodeTaint[];
unschedulable?: boolean;
};
status: {
capacity?: {
cpu: string;
"ephemeral-storage": string;
"hugepages-1Gi": string;
"hugepages-2Mi": string;
memory: string;
pods: string;
};
allocatable?: {
cpu: string;
"ephemeral-storage": string;
"hugepages-1Gi": string;
"hugepages-2Mi": string;
memory: string;
pods: string;
};
conditions?: NodeCondition[];
addresses?: {
type: string;
address: string;
}[];
daemonEndpoints?: {
kubeletEndpoint: {
Port: number; //it must be uppercase for backwards compatibility
};
};
nodeInfo?: {
machineID: string;
systemUUID: string;
bootID: string;
kernelVersion: string;
osImage: string;
containerRuntimeVersion: string;
kubeletVersion: string;
kubeProxyVersion: string;
operatingSystem: string;
architecture: string;
};
images?: {
names: string[];
sizeBytes?: number;
}[];
volumesInUse?: string[];
volumesAttached?: {
name: string;
devicePath: string;
}[];
};
}
/**
* Iterate over `conditions` yielding the `type` field if the `status` field is
* the string `"True"`
* @param conditions An iterator of some conditions
*/
function* getTrueConditionTypes(conditions: IterableIterator<NodeCondition> | Iterable<NodeCondition>): IterableIterator<string> {
for (const { status, type } of conditions) {
if (status === "True") {
yield type;
}
}
} }
/** /**
* This regex is used in the `getRoleLabels()` method bellow, but placed here * This regex is used in the `getRoleLabels()` method bellow, but placed here
* as factoring out regexes is best practice. * 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 { export interface NodeSpec {
static kind = "Node"; podCIDR?: string;
static namespaced = false; podCIDRs?: string[];
static apiBase = "/api/v1/nodes"; providerID?: string;
/**
* @deprecated see https://issues.k8s.io/61966
*/
externalID?: string;
taints?: NodeTaint[];
unschedulable?: boolean;
}
constructor(data: KubeJsonApiData) { export interface NodeAddress {
super(data); type: "Hostname" | "ExternalIP" | "InternalIP";
autoBind(this); 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 * Returns the concatination of all current condition types which have a status
* of `"True"` * of `"True"`
*/ */
getNodeConditionText(): string { getNodeConditionText(): string {
return iter.join( if (!this.status?.conditions) {
getTrueConditionTypes(this.status?.conditions ?? []), return "";
" ", }
);
return this.status.conditions
.filter(condition => condition.status === "True")
.map(condition => condition.type)
.join(" ");
} }
getTaints() { getTaints() {
@ -182,9 +200,9 @@ export class Node extends KubeObject {
const roleLabels: string[] = []; const roleLabels: string[] = [];
for (const labelKey of Object.keys(labels)) { 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); roleLabels.push(match.groups.role);
} }
} }
@ -201,19 +219,19 @@ export class Node extends KubeObject {
} }
getCpuCapacity() { 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); return cpuUnitsToNumber(this.status.capacity.cpu);
} }
getMemoryCapacity() { 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); return unitsToBytes(this.status.capacity.memory);
} }
getConditions() { getConditions(): NodeCondition[] {
const conditions = this.status.conditions || []; const conditions = this.status?.conditions || [];
if (this.isUnschedulable()) { if (this.isUnschedulable()) {
return [{ type: "SchedulingDisabled", status: "True" }, ...conditions]; return [{ type: "SchedulingDisabled", status: "True" }, ...conditions];
@ -235,7 +253,7 @@ export class Node extends KubeObject {
} }
getKubeletVersion() { getKubeletVersion() {
return this.status.nodeInfo.kubeletVersion; return this.status?.nodeInfo?.kubeletVersion ?? "<unknown>";
} }
getOperatingSystem(): string { getOperatingSystem(): string {
@ -250,14 +268,6 @@ export class Node extends KubeObject {
} }
} }
let nodesApi: NodesApi; export const nodeApi = isClusterPageContext()
? new NodeApi()
if (isClusterPageContext()) { : undefined as never;
nodesApi = new NodesApi({
objectConstructor: Node,
});
}
export {
nodesApi,
};

View File

@ -3,20 +3,27 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * 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 { KubeObject } from "../kube-object";
import { autoBind } from "../../utils"; import type { MetricData } from "./metrics.api";
import type { IMetrics } from "./metrics.api";
import { metricsApi } from "./metrics.api"; import { metricsApi } from "./metrics.api";
import type { Pod } from "./pods.api"; import type { Pod } from "./pods.api";
import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; 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() }; const opts = { category: "pvc", pvc: pvc.getName(), namespace: pvc.getNs() };
return metricsApi.getMetrics({ return metricsApi.getMetrics({
@ -27,91 +34,59 @@ export function getMetricsForPvc(pvc: PersistentVolumeClaim): Promise<IPvcMetric
}); });
} }
export interface IPvcMetrics<T = IMetrics> { export interface PersistentVolumeClaimMetricData extends Partial<Record<string, MetricData>> {
[key: string]: T; diskUsage: MetricData;
diskUsage: T; diskCapacity: MetricData;
diskCapacity: T;
} }
export interface PersistentVolumeClaimSpec { export interface PersistentVolumeClaimSpec {
accessModes: string[]; accessModes?: string[];
selector: LabelSelector; dataSource?: TypedLocalObjectReference;
resources: { dataSourceRef?: TypedLocalObjectReference;
requests?: Record<string, string>; resources?: ResourceRequirements;
limits?: Record<string, string>; selector?: LabelSelector;
};
volumeName?: string;
storageClassName?: string; storageClassName?: string;
volumeMode?: string; volumeMode?: string;
dataSource?: { volumeName?: string;
apiGroup: string;
kind: string;
name: string;
};
} }
export interface PersistentVolumeClaim { export interface PersistentVolumeClaimStatus {
spec: PersistentVolumeClaimSpec; phase: string; // Pending
status: {
phase: string; // Pending
};
} }
export class PersistentVolumeClaim extends KubeObject { export class PersistentVolumeClaim extends KubeObject<PersistentVolumeClaimStatus, PersistentVolumeClaimSpec, "namespace-scoped"> {
static kind = "PersistentVolumeClaim"; static readonly kind = "PersistentVolumeClaim";
static namespaced = true; static readonly namespaced = true;
static apiBase = "/api/v1/persistentvolumeclaims"; static readonly apiBase = "/api/v1/persistentvolumeclaims";
constructor(data: KubeJsonApiData) { getPods(pods: Pod[]): Pod[] {
super(data); return pods
autoBind(this); .filter(pod => pod.getNs() === this.getNs())
} .filter(pod => (
pod.getVolumes()
getPods(allPods: Pod[]): Pod[] { .filter(volume => volume.persistentVolumeClaim?.claimName === this.getName())
const pods = allPods.filter(pod => pod.getNs() === this.getNs()); .length > 0
));
return pods.filter(pod => {
return pod.getVolumes().filter(volume =>
volume.persistentVolumeClaim &&
volume.persistentVolumeClaim.claimName === this.getName(),
).length > 0;
});
} }
getStorage(): string { 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[] { 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}`); .map(([name, val]) => `${name}:${val}`);
} }
getMatchExpressions() { getMatchExpressions() {
if (!this.spec.selector || !this.spec.selector.matchExpressions) return []; return this.spec.selector?.matchExpressions ?? [];
return this.spec.selector.matchExpressions;
} }
getStatus(): string { getStatus(): string {
if (this.status) return this.status.phase; return this.status?.phase ?? "-";
return "-";
} }
} }
let pvcApi: PersistentVolumeClaimsApi; export const persistentVolumeClaimApi = isClusterPageContext()
? new PersistentVolumeClaimApi()
if (isClusterPageContext()) { : undefined as never;
pvcApi = new PersistentVolumeClaimsApi({
objectConstructor: PersistentVolumeClaim,
});
}
export {
pvcApi,
};

View File

@ -3,64 +3,77 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * 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 { 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 { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
import type { ResourceRequirements } from "./types/resource-requirements";
export interface PersistentVolume { export interface PersistentVolumeSpec {
spec: { /**
capacity: { * AccessModes contains the desired access modes the volume should have.
storage: string; // 8Gi *
}; * More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1
flexVolume: { */
driver: string; // ceph.rook.io/rook-ceph-system, accessModes?: string[];
options: { dataSource?: TypedLocalObjectReference;
clusterNamespace: string; // rook-ceph, dataSourceRef?: TypedLocalObjectReference;
image: string; // pvc-c5d7c485-9f1b-11e8-b0ea-9600000e54fb, resources?: ResourceRequirements;
pool: string; // replicapool, selector?: LabelSelector;
storageClass: string; // rook-ceph-block
}; /**
}; * Name of the StorageClass required by the claim.
mountOptions?: string[]; *
accessModes: string[]; // [ReadWriteOnce] * More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1
claimRef: { */
kind: string; // PersistentVolumeClaim, storageClassName?: string;
namespace: string; // storage,
name: string; // nfs-provisioner, /**
uid: string; // c5d7c485-9f1b-11e8-b0ea-9600000e54fb, * Defines what type of volume is required by the claim. Value of Filesystem is implied when not
apiVersion: string; // v1, * included in claim spec.
resourceVersion: string; // 292180 */
}; volumeMode?: string;
persistentVolumeReclaimPolicy: string; // Delete,
storageClassName: string; // rook-ceph-block /**
nfs?: { * A description of the persistent volume\'s resources and capacity.
path: string; *
server: string; * 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
}; };
}; };
mountOptions?: string[];
status?: { claimRef?: ObjectReference;
phase: string; persistentVolumeReclaimPolicy?: string; // Delete,
reason?: string; 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 kind = "PersistentVolume";
static namespaced = false; static namespaced = false;
static apiBase = "/api/v1/persistentvolumes"; static apiBase = "/api/v1/persistentvolumes";
constructor(data: KubeJsonApiData) {
super(data);
autoBind(this);
}
getCapacity(inBytes = false) { getCapacity(inBytes = false) {
const capacity = this.spec.capacity; const capacity = this.spec.capacity;
if (capacity) { if (capacity?.storage) {
if (inBytes) return unitsToBytes(capacity.storage); if (inBytes) return unitsToBytes(capacity.storage);
return capacity.storage; return capacity.storage;
@ -74,7 +87,7 @@ export class PersistentVolume extends KubeObject {
} }
getStorageClass(): string { getStorageClass(): string {
return this.spec.storageClassName; return this.spec.storageClassName ?? "";
} }
getClaimRefName(): string { getClaimRefName(): string {
@ -86,14 +99,15 @@ export class PersistentVolume extends KubeObject {
} }
} }
let persistentVolumeApi: KubeApi<PersistentVolume>; export class PersistentVolumeApi extends KubeApi<PersistentVolume> {
constructor(opts: DerivedKubeApiOptions = {}) {
if (isClusterPageContext()) { super({
persistentVolumeApi = new KubeApi({ ...opts,
objectConstructor: PersistentVolume, objectConstructor: PersistentVolume,
}); });
}
} }
export { export const persistentVolumeApi = isClusterPageContext()
persistentVolumeApi, ? new PersistentVolumeApi()
}; : undefined as never;

View File

@ -3,36 +3,60 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { KubeObjectMetadata } from "../kube-object";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import type { DerivedKubeApiOptions } from "../kube-api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; 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; timestamp: string;
window: string; window: string;
containers: { containers: PodMetricsContainer[];
name: string;
usage: {
cpu: string;
memory: string;
};
}[];
} }
export class PodMetrics extends KubeObject { export interface PodMetricsContainerUsage {
static kind = "PodMetrics"; cpu: string;
static namespaced = true; memory: string;
static apiBase = "/apis/metrics.k8s.io/v1beta1/pods";
} }
let podMetricsApi: KubeApi<PodMetrics>; export interface PodMetricsContainer {
name: string;
if (isClusterPageContext()) { usage: PodMetricsContainerUsage;
podMetricsApi = new KubeApi<PodMetrics>({
objectConstructor: PodMetrics,
});
} }
export { export class PodMetrics extends KubeObject<void, void, "namespace-scoped"> {
podMetricsApi, 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;

View File

@ -3,41 +3,32 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { autoBind } from "../../utils";
import type { LabelSelector } from "../kube-object"; import type { LabelSelector } from "../kube-object";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import type { DerivedKubeApiOptions } from "../kube-api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
export interface PodDisruptionBudget { export interface PodDisruptionBudgetSpec {
spec: { minAvailable: string;
minAvailable: string; maxUnavailable: string;
maxUnavailable: string; selector: LabelSelector;
selector: LabelSelector;
};
status: {
currentHealthy: number;
desiredHealthy: number;
disruptionsAllowed: number;
expectedPods: number;
};
} }
export class PodDisruptionBudget extends KubeObject { export interface PodDisruptionBudgetStatus {
static kind = "PodDisruptionBudget"; currentHealthy: number;
static namespaced = true; desiredHealthy: number;
static apiBase = "/apis/policy/v1beta1/poddisruptionbudgets"; disruptionsAllowed: number;
expectedPods: number;
}
constructor(data: KubeJsonApiData) { export class PodDisruptionBudget extends KubeObject<PodDisruptionBudgetStatus, PodDisruptionBudgetSpec, "namespace-scoped"> {
super(data); static readonly kind = "PodDisruptionBudget";
autoBind(this); static readonly namespaced = true;
} static readonly apiBase = "/apis/policy/v1beta1/poddisruptionbudgets";
getSelectors() { getSelectors() {
const selector = this.spec.selector; return KubeObject.stringifyLabels(this.spec.selector.matchLabels);
return KubeObject.stringifyLabels(selector ? selector.matchLabels : null);
} }
getMinAvailable() { getMinAvailable() {
@ -49,23 +40,23 @@ export class PodDisruptionBudget extends KubeObject {
} }
getCurrentHealthy() { getCurrentHealthy() {
return this.status.currentHealthy; return this.status?.currentHealthy ?? 0;
} }
getDesiredHealthy() { getDesiredHealthy() {
return this.status.desiredHealthy; return this.status?.desiredHealthy ?? 0;
} }
} }
let pdbApi: KubeApi<PodDisruptionBudget>; export class PodDisruptionBudgetApi extends KubeApi<PodDisruptionBudget> {
constructor(opts: DerivedKubeApiOptions = {}) {
if (isClusterPageContext()) { super({
pdbApi = new KubeApi({ objectConstructor: PodDisruptionBudget,
objectConstructor: PodDisruptionBudget, ...opts,
}); });
}
} }
export { export const podDisruptionBudgetApi = isClusterPageContext()
pdbApi, ? new PodDisruptionBudgetApi()
}; : undefined as never;

View File

@ -3,20 +3,26 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { IAffinity } from "../workload-kube-object"; import type { MetricData } from "./metrics.api";
import { WorkloadKubeObject } from "../workload-kube-object";
import { autoBind } from "../../utils";
import type { IMetrics } from "./metrics.api";
import { metricsApi } from "./metrics.api"; import { metricsApi } from "./metrics.api";
import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
import type { RequireExactlyOne } from "type-fest"; 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 { SecretReference } from "./secret.api";
import type { PersistentVolumeClaimSpec } from "./persistent-volume-claims.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> => { getLogs = async (params: { namespace: string; name: string }, query?: IPodLogsQuery): Promise<string> => {
const path = `${this.getUrl(params)}/log`; 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 podSelector = pods.map(pod => pod.getName()).join("|");
const opts = { category: "pods", pods: podSelector, namespace, selector }; 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> { export interface PodMetricData extends Partial<Record<string, MetricData>> {
[metric: string]: T; cpuUsage: MetricData;
cpuUsage: T; memoryUsage: MetricData;
memoryUsage: T; fsUsage: MetricData;
fsUsage: T; fsWrites: MetricData;
fsWrites: T; fsReads: MetricData;
fsReads: T; networkReceive: MetricData;
networkReceive: T; networkTransmit: MetricData;
networkTransmit: T; cpuRequests?: MetricData;
cpuRequests?: T; cpuLimits?: MetricData;
cpuLimits?: T; memoryRequests?: MetricData;
memoryRequests?: T; memoryLimits?: MetricData;
memoryLimits?: T;
} }
// Reference: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#read-log-pod-v1-core // 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; previous?: boolean;
} }
export enum PodStatus { export enum PodStatusPhase {
TERMINATED = "Terminated", TERMINATED = "Terminated",
FAILED = "Failed", FAILED = "Failed",
PENDING = "Pending", PENDING = "Pending",
@ -79,26 +84,41 @@ export enum PodStatus {
EVICTED = "Evicted", 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>> { export interface IPodContainer extends Partial<Record<PodContainerProbe, IContainerProbe>> {
name: string; name: string;
image: string; image: string;
command?: string[]; command?: string[];
args?: string[]; args?: string[];
ports?: { ports?: ContainerPort[];
name?: string;
containerPort: number;
protocol: string;
}[];
resources?: { resources?: {
limits: { limits?: {
cpu: string; cpu: string;
memory: string; memory: string;
}; };
requests: { requests?: {
cpu: string; cpu: string;
memory: string; memory: string;
}; };
}; };
terminationMessagePath?: string;
terminationMessagePolicy?: string;
env?: { env?: {
name: string; name: string;
value?: string; value?: string;
@ -121,12 +141,8 @@ export interface IPodContainer extends Partial<Record<PodContainerProbe, IContai
configMapRef?: LocalObjectReference; configMapRef?: LocalObjectReference;
secretRef?: LocalObjectReference; secretRef?: LocalObjectReference;
}[]; }[];
volumeMounts?: { volumeMounts?: VolumeMount[];
name: string; imagePullPolicy?: string;
readOnly: boolean;
mountPath: string;
}[];
imagePullPolicy: string;
} }
export type PodContainerProbe = "livenessProbe" | "readinessProbe" | "startupProbe"; export type PodContainerProbe = "livenessProbe" | "readinessProbe" | "startupProbe";
@ -631,85 +647,133 @@ export interface PodVolumeVariants {
*/ */
export type PodVolumeKind = keyof PodVolumeVariants; export type PodVolumeKind = keyof PodVolumeVariants;
export type PodVolume = RequireExactlyOne<PodVolumeVariants> & { export type PodSpecVolume = RequireExactlyOne<PodVolumeVariants> & {
name: string; 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 kind = "Pod";
static namespaced = true; static namespaced = true;
static apiBase = "/api/v1/pods"; static apiBase = "/api/v1/pods";
constructor(data: KubeJsonApiData) { getAffinityNumber() {
super(data); return Object.keys(this.getAffinity()).length;
autoBind(this);
} }
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() { getInitContainers() {
return this.spec?.initContainers || []; return this.spec?.initContainers ?? [];
} }
getContainers() { getContainers() {
return this.spec?.containers || []; return this.spec?.containers ?? [];
} }
getAllContainers() { getAllContainers() {
@ -719,7 +783,7 @@ export class Pod extends WorkloadKubeObject {
getRunningContainers() { getRunningContainers() {
const runningContainerNames = new Set( const runningContainerNames = new Set(
this.getContainerStatuses() this.getContainerStatuses()
.filter(({ state }) => state.running) .filter(({ state }) => state?.running)
.map(({ name }) => name), .map(({ name }) => name),
); );
@ -752,10 +816,10 @@ export class Pod extends WorkloadKubeObject {
} }
getPriorityClassName() { getPriorityClassName() {
return this.spec.priorityClassName || ""; return this.spec?.priorityClassName || "";
} }
getStatus(): PodStatus { getStatus(): PodStatusPhase {
const phase = this.getStatusPhase(); const phase = this.getStatusPhase();
const reason = this.getReason(); const reason = this.getReason();
const trueConditionTypes = new Set(this.getConditions() const trueConditionTypes = new Set(this.getConditions()
@ -763,28 +827,28 @@ export class Pod extends WorkloadKubeObject {
.map(({ type }) => type)); .map(({ type }) => type));
const isInGoodCondition = ["Initialized", "Ready"].every(condition => trueConditionTypes.has(condition)); const isInGoodCondition = ["Initialized", "Ready"].every(condition => trueConditionTypes.has(condition));
if (reason === PodStatus.EVICTED) { if (reason === PodStatusPhase.EVICTED) {
return PodStatus.EVICTED; return PodStatusPhase.EVICTED;
} }
if (phase === PodStatus.FAILED) { if (phase === PodStatusPhase.FAILED) {
return PodStatus.FAILED; return PodStatusPhase.FAILED;
} }
if (phase === PodStatus.SUCCEEDED) { if (phase === PodStatusPhase.SUCCEEDED) {
return PodStatus.SUCCEEDED; return PodStatusPhase.SUCCEEDED;
} }
if (phase === PodStatus.RUNNING && isInGoodCondition) { if (phase === PodStatusPhase.RUNNING && isInGoodCondition) {
return PodStatus.RUNNING; return PodStatusPhase.RUNNING;
} }
return PodStatus.PENDING; return PodStatusPhase.PENDING;
} }
// Returns pod phase or container error if occurred // Returns pod phase or container error if occurred
getStatusMessage(): string { getStatusMessage(): string {
if (this.getReason() === PodStatus.EVICTED) { if (this.getReason() === PodStatusPhase.EVICTED) {
return "Evicted"; return "Evicted";
} }
@ -800,31 +864,30 @@ export class Pod extends WorkloadKubeObject {
} }
getConditions() { getConditions() {
return this.status?.conditions || []; return this.status?.conditions ?? [];
} }
getVolumes() { getVolumes() {
return this.spec.volumes || []; return this.spec?.volumes ?? [];
} }
getSecrets(): string[] { getSecrets(): string[] {
return this.getVolumes() return this.getVolumes()
.filter(vol => vol.secret) .map(vol => vol.secret?.secretName)
.map(vol => vol.secret.secretName); .filter(isDefined);
} }
getNodeSelectors(): string[] { getNodeSelectors(): string[] {
const { nodeSelector = {}} = this.spec; return Object.entries(this.spec?.nodeSelector ?? {})
.map(values => values.join(": "));
return Object.entries(nodeSelector).map(values => values.join(": "));
} }
getTolerations() { getTolerations() {
return this.spec.tolerations || []; return this.spec?.tolerations ?? [];
} }
getAffinity(): IAffinity { getAffinity(): Affinity {
return this.spec.affinity; return this.spec?.affinity ?? {};
} }
hasIssues() { hasIssues() {
@ -907,30 +970,21 @@ export class Pod extends WorkloadKubeObject {
return probe; return probe;
} }
getNodeName() { getNodeName(): string | undefined {
return this.spec.nodeName; return this.spec?.nodeName;
} }
getSelectedNodeOs(): string | undefined { 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[] { getIPs(): string[] {
if(!this.status.podIPs) return []; const podIPs = this.status?.podIPs ?? [];
const podIPs = this.status.podIPs;
return podIPs.map(value => value.ip); return podIPs.map(value => value.ip);
} }
} }
let podsApi: PodsApi; export const podApi = isClusterPageContext()
? new PodApi()
if (isClusterPageContext()) { : undefined as never;
podsApi = new PodsApi({
objectConstructor: Pod,
});
}
export {
podsApi,
};

View File

@ -3,83 +3,87 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { autoBind } from "../../utils";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import type { DerivedKubeApiOptions } from "../kube-api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
export interface PodSecurityPolicy { export interface PodSecurityPolicySpec {
spec: { allowPrivilegeEscalation?: boolean;
allowPrivilegeEscalation?: boolean; allowedCSIDrivers?: {
allowedCSIDrivers?: { name: string;
name: string; }[];
}[]; allowedCapabilities: string[];
allowedCapabilities: string[]; allowedFlexVolumes?: {
allowedFlexVolumes?: { driver: string;
driver: string; }[];
}[]; allowedHostPaths?: {
allowedHostPaths?: { pathPrefix: string;
pathPrefix: string; readOnly: boolean;
readOnly: boolean; }[];
}[]; allowedProcMountTypes?: string[];
allowedProcMountTypes?: string[]; allowedUnsafeSysctls?: string[];
allowedUnsafeSysctls?: string[]; defaultAddCapabilities?: string[];
defaultAddCapabilities?: string[]; defaultAllowPrivilegeEscalation?: boolean;
defaultAllowPrivilegeEscalation?: boolean; forbiddenSysctls?: string[];
forbiddenSysctls?: string[]; fsGroup?: {
fsGroup?: { rule: string;
rule: string; ranges: {
ranges: { max: number; min: number }[];
};
hostIPC?: boolean;
hostNetwork?: boolean;
hostPID?: boolean;
hostPorts?: {
max: number; max: number;
min: 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 { export class PodSecurityPolicy extends KubeObject<void, PodSecurityPolicySpec, "cluster-scoped"> {
static kind = "PodSecurityPolicy"; static readonly kind = "PodSecurityPolicy";
static namespaced = false; static readonly namespaced = false;
static apiBase = "/apis/policy/v1beta1/podsecuritypolicies"; static readonly apiBase = "/apis/policy/v1beta1/podsecuritypolicies";
constructor(data: KubeJsonApiData) {
super(data);
autoBind(this);
}
isPrivileged() { isPrivileged() {
return !!this.spec.privileged; 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 { export const podSecurityPolicyApi = isClusterPageContext()
pspApi, ? new PodSecurityPolicyApi()
}; : undefined as never;

View File

@ -3,25 +3,31 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import get from "lodash/get"; import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api";
import { autoBind } from "../../../renderer/utils";
import { WorkloadKubeObject } from "../workload-kube-object";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import { metricsApi } from "./metrics.api"; import { metricsApi } from "./metrics.api";
import type { IPodContainer, IPodMetrics, Pod } from "./pods.api"; import type { PodMetricData } from "./pods.api";
import type { KubeJsonApiData } from "../kube-json-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; 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> { export class ReplicaSetApi extends KubeApi<ReplicaSet> {
constructor(opts: DerivedKubeApiOptions & IgnoredKubeApiOptions = {}) {
super({
...opts,
objectConstructor: ReplicaSet,
});
}
protected getScaleApiUrl(params: { namespace: string; name: string }) { protected getScaleApiUrl(params: { namespace: string; name: string }) {
return `${this.getUrl(params)}/scale`; return `${this.getUrl(params)}/scale`;
} }
getReplicas(params: { namespace: string; name: string }): Promise<number> { async getReplicas(params: { namespace: string; name: string }): Promise<number> {
return this.request const { status } = await this.request.get(this.getScaleApiUrl(params));
.get(this.getScaleApiUrl(params))
.then(({ status }: any) => status?.replicas); return (status as { replicas: number })?.replicas;
} }
scale(params: { namespace: string; name: string }, replicas: number) { 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 podSelector = replicasets.map(replicaset => `${replicaset.getName()}-[[:alnum:]]{5}`).join("|");
const opts = { category: "pods", pods: podSelector, namespace, selector }; 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 kind = "ReplicaSet";
static namespaced = true; static namespaced = true;
static apiBase = "/apis/apps/v1/replicasets"; static apiBase = "/apis/apps/v1/replicasets";
constructor(data: KubeJsonApiData) { getSelectors(): string[] {
super(data); return KubeObject.stringifyLabels(this.spec.selector.matchLabels);
autoBind(this);
} }
declare spec: { getNodeSelectors(): string[] {
replicas?: number; return KubeObject.stringifyLabels(this.spec.template?.spec?.nodeSelector);
selector: LabelSelector; }
template?: {
metadata: { getTemplateLabels(): string[] {
labels: { return KubeObject.stringifyLabels(this.spec.template?.metadata?.labels);
app: string; }
};
}; getTolerations() {
spec?: Pod["spec"]; return this.spec.template?.spec?.tolerations ?? [];
}; }
minReadySeconds?: number;
}; getAffinity() {
declare status: { return this.spec.template?.spec?.affinity;
replicas: number; }
fullyLabeledReplicas?: number;
readyReplicas?: number; getAffinityNumber() {
availableReplicas?: number; return Object.keys(this.getAffinity() ?? {}).length;
observedGeneration?: number; }
conditions?: {
type: string;
status: string;
lastUpdateTime: string;
lastTransitionTime: string;
reason: string;
message: string;
}[];
};
getDesired() { getDesired() {
return this.spec.replicas || 0; return this.spec.replicas ?? 0;
} }
getCurrent() { getCurrent() {
return this.status.availableReplicas || 0; return this.status?.availableReplicas ?? 0;
} }
getReady() { getReady() {
return this.status.readyReplicas || 0; return this.status?.readyReplicas ?? 0;
} }
getImages() { 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; export const replicaSetApi = isClusterPageContext()
? new ReplicaSetApi()
if (isClusterPageContext()) { : undefined as never;
replicaSetApi = new ReplicaSetApi({
objectConstructor: ReplicaSet,
});
}
export {
replicaSetApi,
};

View File

@ -16,7 +16,7 @@ export async function update(resource: object | string): Promise<KubeJsonApiData
if (typeof resource === "string") { if (typeof resource === "string") {
const parsed = yaml.load(resource); const parsed = yaml.load(resource);
if (typeof parsed !== "object") { if (!parsed || typeof parsed !== "object") {
throw new Error("Cannot update resource to string or number"); 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 }); 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", { return apiBase.patch<KubeJsonApiData>("/stack", {
data: { data: {
name, name,

View File

@ -4,12 +4,11 @@
*/ */
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import type { DerivedKubeApiOptions } from "../kube-api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
export interface IResourceQuotaValues { export type IResourceQuotaValues = Partial<Record<string, string>> & {
[quota: string]: string;
// Compute Resource Quota // Compute Resource Quota
"limits.cpu"?: string; "limits.cpu"?: string;
"limits.memory"?: string; "limits.memory"?: string;
@ -33,46 +32,43 @@ export interface IResourceQuotaValues {
"count/jobs.batch"?: string; "count/jobs.batch"?: string;
"count/cronjobs.batch"?: string; "count/cronjobs.batch"?: string;
"count/deployments.extensions"?: string; "count/deployments.extensions"?: string;
} };
export interface ResourceQuota { export interface ResourceQuotaSpec {
spec: { hard: IResourceQuotaValues;
hard: IResourceQuotaValues; scopeSelector?: {
scopeSelector?: { matchExpressions: {
matchExpressions: { operator: string;
operator: string; scopeName: string;
scopeName: string; values: string[];
values: string[]; }[];
}[];
};
};
status: {
hard: IResourceQuotaValues;
used: IResourceQuotaValues;
}; };
} }
export class ResourceQuota extends KubeObject { export interface ResourceQuotaStatus {
static kind = "ResourceQuota"; hard: IResourceQuotaValues;
static namespaced = true; used: IResourceQuotaValues;
static apiBase = "/api/v1/resourcequotas"; }
export class ResourceQuota extends KubeObject<ResourceQuotaStatus, ResourceQuotaSpec, "namespace-scoped"> {
static readonly kind = "ResourceQuota";
static readonly namespaced = true;
static readonly apiBase = "/api/v1/resourcequotas";
getScopeSelector() { getScopeSelector() {
const { matchExpressions = [] } = this.spec.scopeSelector || {}; return this.spec.scopeSelector?.matchExpressions ?? [];
return matchExpressions;
} }
} }
let resourceQuotaApi: KubeApi<ResourceQuota>; export class ResourceQuotaApi extends KubeApi<ResourceQuota> {
constructor(opts: DerivedKubeApiOptions = {}) {
if (isClusterPageContext()) { super({
resourceQuotaApi = new KubeApi<ResourceQuota>({ objectConstructor: ResourceQuota,
objectConstructor: ResourceQuota, ...opts,
}); });
}
} }
export { export const resourceQuotaApi = isClusterPageContext()
resourceQuotaApi, ? new ResourceQuotaApi()
}; : undefined as never;

View File

@ -3,38 +3,32 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * 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 { KubeObject } from "../kube-object";
import type { DerivedKubeApiOptions } from "../kube-api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api"; import type { KubeJsonApiData } from "../kube-json-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; 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 RoleBindingData extends KubeJsonApiData<KubeObjectMetadata<"namespace-scoped">, void, void> {
subjects?: Subject[];
export interface RoleBindingSubject { roleRef: RoleRef;
kind: RoleBindingSubjectKind;
name: string;
namespace?: string;
apiGroup?: string;
} }
export interface RoleBinding { export class RoleBinding extends KubeObject<void, void, "namespace-scoped"> {
subjects?: RoleBindingSubject[]; static readonly kind = "RoleBinding";
roleRef: { static readonly namespaced = true;
kind: string; static readonly apiBase = "/apis/rbac.authorization.k8s.io/v1/rolebindings";
name: string;
apiGroup?: string;
};
}
export class RoleBinding extends KubeObject { subjects?: Subject[];
static kind = "RoleBinding"; roleRef: RoleRef;
static namespaced = true;
static apiBase = "/apis/rbac.authorization.k8s.io/v1/rolebindings";
constructor(data: KubeJsonApiData) { constructor({ subjects, roleRef, ...rest }: RoleBindingData) {
super(data); super(rest);
autoBind(this); this.subjects = subjects;
this.roleRef = roleRef;
} }
getSubjects() { getSubjects() {
@ -46,14 +40,15 @@ export class RoleBinding extends KubeObject {
} }
} }
let roleBindingApi: KubeApi<RoleBinding>; export class RoleBindingApi extends KubeApi<RoleBinding, RoleBindingData> {
constructor(opts: DerivedKubeApiOptions = {}) {
if (isClusterPageContext()) { super({
roleBindingApi = new KubeApi({ ...opts,
objectConstructor: RoleBinding, objectConstructor: RoleBinding,
}); });
}
} }
export { export const roleBindingApi = isClusterPageContext()
roleBindingApi, ? new RoleBindingApi()
}; : undefined as never;

View File

@ -3,37 +3,43 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { KubeObjectMetadata } from "../kube-object";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import type { DerivedKubeApiOptions } from "../kube-api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; 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 { export interface RoleData extends KubeJsonApiData<KubeObjectMetadata<"namespace-scoped">, void, void> {
rules: { rules?: PolicyRule[];
verbs: string[];
apiGroups: string[];
resources: string[];
resourceNames?: string[];
}[];
} }
export class Role extends KubeObject { export class Role extends KubeObject<void, void, "namespace-scoped"> {
static kind = "Role"; static readonly kind = "Role";
static namespaced = true; static readonly namespaced = true;
static apiBase = "/apis/rbac.authorization.k8s.io/v1/roles"; static readonly apiBase = "/apis/rbac.authorization.k8s.io/v1/roles";
rules?: PolicyRule[];
constructor({ rules, ...rest }: RoleData) {
super(rest);
this.rules = rules;
}
getRules() { getRules() {
return this.rules || []; return this.rules || [];
} }
} }
let roleApi: KubeApi<Role>; export class RoleApi extends KubeApi<Role, RoleData> {
constructor(opts: DerivedKubeApiOptions = {}) {
if (isClusterPageContext()) { super({
roleApi = new KubeApi<Role>({ ...opts,
objectConstructor: Role, objectConstructor: Role,
}); });
}
} }
export{ export const roleApi = isClusterPageContext()
roleApi, ? new RoleApi()
}; : undefined as never;

View File

@ -3,9 +3,11 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { KubeObjectMetadata } from "../kube-object";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import type { KubeJsonApiData } from "../kube-json-api"; import type { KubeJsonApiData } from "../kube-json-api";
import { autoBind } from "../../utils"; import { autoBind } from "../../utils";
import type { DerivedKubeApiOptions } from "../kube-api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
@ -20,34 +22,30 @@ export enum SecretType {
BootstrapToken = "bootstrap.kubernetes.io/token", BootstrapToken = "bootstrap.kubernetes.io/token",
} }
export interface ISecretRef {
key?: string;
name: string;
}
export interface SecretReference { export interface SecretReference {
name: string; name: string;
namespace?: string; namespace?: string;
} }
export interface SecretData extends KubeJsonApiData { export interface SecretData extends KubeJsonApiData<KubeObjectMetadata<"namespace-scoped">, void, void> {
type: SecretType; type: SecretType;
data?: Record<string, string>; data?: Partial<Record<string, string>>;
} }
export class Secret extends KubeObject { export class Secret extends KubeObject<void, void, "namespace-scoped"> {
static kind = "Secret"; static readonly kind = "Secret";
static namespaced = true; static readonly namespaced = true;
static apiBase = "/api/v1/secrets"; static readonly apiBase = "/api/v1/secrets";
declare type: SecretType; type: SecretType;
declare data: Record<string, string>; data: Partial<Record<string, string>>;
constructor(data: SecretData) { constructor({ data = {}, type, ...rest }: SecretData) {
super(data); super(rest);
autoBind(this); autoBind(this);
this.data ??= {}; this.data = data;
this.type = type;
} }
getKeys(): string[] { getKeys(): string[] {
@ -59,14 +57,15 @@ export class Secret extends KubeObject {
} }
} }
let secretsApi: KubeApi<Secret>; export class SecretApi extends KubeApi<Secret, SecretData> {
constructor(options: DerivedKubeApiOptions = {}) {
if (isClusterPageContext()) { super({
secretsApi = new KubeApi({ ...options,
objectConstructor: Secret, objectConstructor: Secret,
}); });
}
} }
export { export const secretApi = isClusterPageContext()
secretsApi, ? new SecretApi()
}; : undefined as never;

View File

@ -8,7 +8,7 @@ import { KubeApi } from "../kube-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
export class SelfSubjectRulesReviewApi extends KubeApi<SelfSubjectRulesReview> { export class SelfSubjectRulesReviewApi extends KubeApi<SelfSubjectRulesReview> {
create({ namespace = "default" }): Promise<SelfSubjectRulesReview> { create({ namespace = "default" }) {
return super.create({}, { return super.create({}, {
spec: { spec: {
namespace, namespace,

View File

@ -3,29 +3,38 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * 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 { KubeObject } from "../kube-object";
import type { DerivedKubeApiOptions } from "../kube-api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api"; import type { KubeJsonApiData } from "../kube-json-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
export interface ServiceAccount { export interface ServiceAccountData extends KubeJsonApiData<KubeObjectMetadata<"namespace-scoped">, void, void> {
secrets?: { automountServiceAccountToken?: boolean;
name: string; imagePullSecrets?: LocalObjectReference[];
}[]; secrets?: ObjectReference[];
imagePullSecrets?: {
name: string;
}[];
} }
export class ServiceAccount extends KubeObject { export class ServiceAccount extends KubeObject<void, void, "namespace-scoped"> {
static kind = "ServiceAccount"; static readonly kind = "ServiceAccount";
static namespaced = true; static readonly namespaced = true;
static apiBase = "/api/v1/serviceaccounts"; static readonly apiBase = "/api/v1/serviceaccounts";
constructor(data: KubeJsonApiData) { automountServiceAccountToken?: boolean;
super(data); imagePullSecrets?: LocalObjectReference[];
autoBind(this); secrets?: ObjectReference[];
constructor({
automountServiceAccountToken,
imagePullSecrets,
secrets,
...rest
}: ServiceAccountData) {
super(rest);
this.automountServiceAccountToken = automountServiceAccountToken;
this.imagePullSecrets = imagePullSecrets;
this.secrets = secrets;
} }
getSecrets() { getSecrets() {
@ -37,14 +46,15 @@ export class ServiceAccount extends KubeObject {
} }
} }
let serviceAccountsApi: KubeApi<ServiceAccount>; export class ServiceAccountApi extends KubeApi<ServiceAccount, ServiceAccountData> {
constructor(opts: DerivedKubeApiOptions = {}) {
if (isClusterPageContext()) { super({
serviceAccountsApi = new KubeApi<ServiceAccount>({ ...opts,
objectConstructor: ServiceAccount, objectConstructor: ServiceAccount,
}); });
}
} }
export { export const serviceAccountApi = isClusterPageContext()
serviceAccountsApi, ? new ServiceAccountApi()
}; : undefined as never;

View File

@ -3,10 +3,9 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { autoBind } from "../../../renderer/utils";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import type { DerivedKubeApiOptions } from "../kube-api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
export interface ServicePort { export interface ServicePort {
@ -31,47 +30,40 @@ export class ServicePort {
} }
} }
export interface Service { export interface ServiceSpec {
spec: { type: string;
type: string; clusterIP: string;
clusterIP: string; clusterIPs?: string[];
clusterIPs?: string[]; externalTrafficPolicy?: string;
externalTrafficPolicy?: string; externalName?: string;
externalName?: string; loadBalancerIP?: string;
loadBalancerIP?: string; loadBalancerSourceRanges?: string[];
loadBalancerSourceRanges?: string[]; sessionAffinity: string;
sessionAffinity: string; selector: Partial<Record<string, string>>;
selector: { [key: string]: string }; ports: ServicePort[];
ports: ServicePort[]; healthCheckNodePort?: number;
healthCheckNodePort?: number; externalIPs?: string[]; // https://kubernetes.io/docs/concepts/services-networking/service/#external-ips
externalIPs?: string[]; // https://kubernetes.io/docs/concepts/services-networking/service/#external-ips topologyKeys?: string[];
topologyKeys?: string[]; ipFamilies?: string[];
ipFamilies?: string[]; ipFamilyPolicy?: string;
ipFamilyPolicy?: string; allocateLoadBalancerNodePorts?: boolean;
allocateLoadBalancerNodePorts?: boolean; loadBalancerClass?: string;
loadBalancerClass?: string; internalTrafficPolicy?: string;
internalTrafficPolicy?: string; }
};
status: { export interface ServiceStatus {
loadBalancer?: { loadBalancer?: {
ingress?: { ingress?: {
ip?: string; ip?: string;
hostname?: string; hostname?: string;
}[]; }[];
};
}; };
} }
export class Service extends KubeObject { export class Service extends KubeObject<ServiceStatus, ServiceSpec, "namespace-scoped"> {
static kind = "Service"; static readonly kind = "Service";
static namespaced = true; static readonly namespaced = true;
static apiBase = "/api/v1/services"; static readonly apiBase = "/api/v1/services";
constructor(data: KubeJsonApiData) {
super(data);
autoBind(this);
}
getClusterIp() { getClusterIp() {
return this.spec.clusterIP; return this.spec.clusterIP;
@ -112,7 +104,7 @@ export class Service extends KubeObject {
} }
getLoadBalancer() { getLoadBalancer() {
return this.status.loadBalancer; return this.status?.loadBalancer;
} }
isActive() { isActive() {
@ -132,14 +124,15 @@ export class Service extends KubeObject {
} }
} }
let serviceApi: KubeApi<Service>; export class ServiceApi extends KubeApi<Service> {
constructor(opts: DerivedKubeApiOptions = {}) {
if (isClusterPageContext()) { super({
serviceApi = new KubeApi<Service>({ ...opts,
objectConstructor: Service, objectConstructor: Service,
}); });
}
} }
export { export const serviceApi = isClusterPageContext()
serviceApi, ? new ServiceApi()
}; : undefined as never;

View File

@ -3,17 +3,24 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { IAffinity } from "../workload-kube-object"; import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api";
import { WorkloadKubeObject } from "../workload-kube-object";
import { autoBind } from "../../utils";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import { metricsApi } from "./metrics.api"; import { metricsApi } from "./metrics.api";
import type { IPodMetrics } from "./pods.api"; import type { PodMetricData } from "./pods.api";
import type { KubeJsonApiData } from "../kube-json-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
import type { LabelSelector } from "../kube-object"; 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> { export class StatefulSetApi extends KubeApi<StatefulSet> {
constructor(opts: DerivedKubeApiOptions & IgnoredKubeApiOptions = {}) {
super({
...opts,
objectConstructor: StatefulSet,
});
}
protected getScaleApiUrl(params: { namespace: string; name: string }) { protected getScaleApiUrl(params: { namespace: string; name: string }) {
return `${this.getUrl(params)}/scale`; 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 podSelector = statefulSets.map(statefulset => `${statefulset.getName()}-[[:digit:]]+`).join("|");
const opts = { category: "pods", pods: podSelector, namespace, selector }; const opts = { category: "pods", pods: podSelector, namespace, selector };
@ -57,74 +64,52 @@ export function getMetricsForStatefulSets(statefulSets: StatefulSet[], namespace
}); });
} }
export class StatefulSet extends WorkloadKubeObject { export interface StatefulSetSpec {
static kind = "StatefulSet"; serviceName: string;
static namespaced = true; replicas: number;
static apiBase = "/apis/apps/v1/statefulsets"; selector: LabelSelector;
template: PodTemplateSpec;
volumeClaimTemplates: PersistentVolumeClaimTemplateSpec[];
}
constructor(data: KubeJsonApiData) { export interface StatefulSetStatus {
super(data); observedGeneration: number;
autoBind(this); 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: { getNodeSelectors(): string[] {
serviceName: string; return KubeObject.stringifyLabels(this.spec.template.spec?.nodeSelector);
replicas: number; }
selector: LabelSelector;
template: { getTemplateLabels(): string[] {
metadata: { return KubeObject.stringifyLabels(this.spec.template.metadata?.labels);
labels: { }
app: string;
}; getTolerations() {
}; return this.spec.template.spec?.tolerations ?? [];
spec: { }
containers: null | {
name: string; getAffinity() {
image: string; return this.spec.template.spec?.affinity ?? {};
ports: { }
containerPort: number;
name: string; getAffinityNumber() {
}[]; return Object.keys(this.getAffinity()).length;
volumeMounts: { }
name: string;
mountPath: string;
}[];
}[];
affinity?: IAffinity;
nodeSelector?: {
[selector: string]: string;
};
tolerations: {
key: string;
operator: string;
effect: string;
tolerationSeconds: number;
}[];
};
};
volumeClaimTemplates: {
metadata: {
name: string;
};
spec: {
accessModes: string[];
resources: {
requests: {
storage: string;
};
};
};
}[];
};
declare status: {
observedGeneration: number;
replicas: number;
currentReplicas: number;
readyReplicas: number;
currentRevision: string;
updateRevision: string;
collisionCount: number;
};
getReplicas() { getReplicas() {
return this.spec.replicas || 0; return this.spec.replicas || 0;
@ -137,14 +122,6 @@ export class StatefulSet extends WorkloadKubeObject {
} }
} }
let statefulSetApi: StatefulSetApi; export const statefulSetApi = isClusterPageContext()
? new StatefulSetApi()
if (isClusterPageContext()) { : undefined as never;
statefulSetApi = new StatefulSetApi({
objectConstructor: StatefulSet,
});
}
export {
statefulSetApi,
};

View File

@ -4,29 +4,64 @@
*/ */
import { autoBind } from "../../utils"; import { autoBind } from "../../utils";
import type { KubeObjectMetadata } from "../kube-object";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import type { DerivedKubeApiOptions } from "../kube-api";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api"; import type { KubeJsonApiData } from "../kube-json-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
export interface StorageClass { export interface TopologySelectorLabelRequirement {
provisioner: string; // e.g. "storage.k8s.io/v1" key: string;
mountOptions?: string[]; values: string[];
volumeBindingMode: string;
reclaimPolicy: string;
parameters: {
[param: string]: string; // every provisioner has own set of these parameters
};
} }
export class StorageClass extends KubeObject { export interface TopologySelectorTerm {
static kind = "StorageClass"; matchLabelExpressions?: TopologySelectorLabelRequirement[];
static namespaced = false; }
static apiBase = "/apis/storage.k8s.io/v1/storageclasses";
constructor(data: KubeJsonApiData) { export interface StorageClassData extends KubeJsonApiData<KubeObjectMetadata<"cluster-scoped">, void, void> {
super(data); 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); autoBind(this);
this.allowVolumeExpansion = allowVolumeExpansion;
this.allowedTopologies = allowedTopologies;
this.mountOptions = mountOptions;
this.parameters = parameters;
this.provisioner = provisioner;
this.reclaimPolicy = reclaimPolicy;
this.volumeBindingMode = volumeBindingMode;
} }
isDefault() { isDefault() {
@ -47,14 +82,15 @@ export class StorageClass extends KubeObject {
} }
} }
let storageClassApi: KubeApi<StorageClass>; export class StorageClassApi extends KubeApi<StorageClass, StorageClassData> {
constructor(opts: DerivedKubeApiOptions = {}) {
if (isClusterPageContext()) { super({
storageClassApi = new KubeApi({ ...opts,
objectConstructor: StorageClass, objectConstructor: StorageClass,
}); });
}
} }
export { export const storageClassApi = isClusterPageContext()
storageClassApi, ? new StorageClassApi()
}; : undefined as never;

View File

@ -3,8 +3,8 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * 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 { export interface AggregationRule {
return JSON.parse(JSON.stringify(obj)); clusterRoleSelectors?: LabelSelector;
} }

View 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;
}

View 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 { PersistentVolumeSpec } from "../persistent-volume.api";
export interface PersistentVolumeClaimTemplateSpec {
metadata?: KubeTemplateObjectMetadata<"cluster-scoped">;
spec?: PersistentVolumeSpec;
}

View 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;
}

View 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[];
}

View 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>>;
}

View File

@ -3,8 +3,8 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import fetch from "node-fetch"; export interface RoleRef {
apiGroup: string;
export { kind: string;
fetch, name: string;
}; }

View 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;
}

View File

@ -13,6 +13,8 @@ import fetch from "node-fetch";
import { stringify } from "querystring"; import { stringify } from "querystring";
import { EventEmitter } from "../../common/event-emitter"; import { EventEmitter } from "../../common/event-emitter";
import logger from "../../common/logger"; import logger from "../../common/logger";
import type { Defaulted } from "../utils";
import { json } from "../utils";
export interface JsonApiData {} export interface JsonApiData {}
@ -35,38 +37,40 @@ export interface JsonApiLog {
error?: any; error?: any;
} }
export type GetRequestOptions = () => Promise<RequestInit>;
export interface JsonApiConfig { export interface JsonApiConfig {
apiBase: string; apiBase: string;
serverAddress: string; serverAddress: string;
debug?: boolean; debug?: boolean;
getRequestOptions?: () => Promise<RequestInit>; getRequestOptions?: GetRequestOptions;
} }
const httpAgent = new HttpAgent({ keepAlive: true }); const httpAgent = new HttpAgent({ keepAlive: true });
const httpsAgent = new HttpsAgent({ keepAlive: true }); const httpsAgent = new HttpsAgent({ keepAlive: true });
export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> { export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
static reqInitDefault: RequestInit = { static readonly reqInitDefault = {
headers: { headers: {
"content-type": "application/json", "content-type": "application/json",
}, },
}; };
protected readonly reqInit: Defaulted<RequestInit, keyof typeof JsonApi["reqInitDefault"]>;
static configDefault: Partial<JsonApiConfig> = { static readonly configDefault: Partial<JsonApiConfig> = {
debug: false, 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.config = Object.assign({}, JsonApi.configDefault, config);
this.reqInit = merge({}, JsonApi.reqInitDefault, reqInit); this.reqInit = merge({}, JsonApi.reqInitDefault, reqInit);
this.parseResponse = this.parseResponse.bind(this); this.parseResponse = this.parseResponse.bind(this);
this.getRequestOptions = config.getRequestOptions ?? (() => Promise.resolve({})); this.getRequestOptions = config.getRequestOptions ?? (() => Promise.resolve({}));
} }
public onData = new EventEmitter<[D, Response]>(); public readonly onData = new EventEmitter<[D, Response]>();
public onError = new EventEmitter<[JsonApiErrorParsed, Response]>(); public readonly onError = new EventEmitter<[JsonApiErrorParsed, Response]>();
private readonly getRequestOptions: GetRequestOptions;
private getRequestOptions: JsonApiConfig["getRequestOptions"];
get<T = D>(path: string, params?: P, reqInit: RequestInit = {}) { get<T = D>(path: string, params?: P, reqInit: RequestInit = {}) {
return this.request<T>(path, params, { ...reqInit, method: "get" }); 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> { async getResponse(path: string, params?: P, init: RequestInit = {}): Promise<Response> {
let reqUrl = `${this.config.serverAddress}${this.config.apiBase}${path}`; 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, this.reqInit,
await this.getRequestOptions(), await this.getRequestOptions(),
init, init,
); );
const { query } = params || {} as P; const { query } = params || {} as P;
if (!reqInit.method) {
reqInit.method = "get";
}
if (!reqInit.agent) {
reqInit.agent = reqUrl.startsWith("https:") ? httpsAgent : httpAgent;
}
if (query) { if (query) {
const queryString = stringify(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" }); 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}`; let reqUrl = `${this.config.serverAddress}${this.config.apiBase}${path}`;
const reqInit: RequestInit = merge( const reqInit = merge(
{}, {},
this.reqInit, this.reqInit,
await this.getRequestOptions(), await this.getRequestOptions(),
@ -149,10 +148,10 @@ export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
const { status } = res; const { status } = res;
const text = await res.text(); const text = await res.text();
let data; let data: any;
try { 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) { } catch (e) {
data = text; data = text;
} }

View File

@ -6,16 +6,6 @@
// Parse kube-api path and get api-version, group, etc. // Parse kube-api path and get api-version, group, etc.
import { splitArray } from "../utils"; 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 { export interface IKubeApiLinkRef {
apiPrefix?: string; apiPrefix?: string;
@ -32,35 +22,21 @@ export interface IKubeApiParsed extends IKubeApiLinkRef {
} }
export function parseKubeApi(path: string): IKubeApiParsed { 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 apiPath = new URL(path, "http://localhost").pathname;
const [, prefix, ...parts] = apiPath.split("/"); const [, prefix, ...parts] = apiPath.split("/");
const apiPrefix = `/${prefix}`; const apiPrefix = `/${prefix}`;
const [left, right, namespaced] = splitArray(parts, "namespaces"); 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) { if (namespaced) {
switch (right.length) { switch (right.length) {
case 1: case 1:
name = right[0]; name = right[0];
// fallthrough // fallthrough
case 0: case 0:
resource = "namespaces"; // special case this due to `split` removing namespaces resource = "namespaces"; // special case this due to `split` removing namespaces
break; break;
@ -69,7 +45,8 @@ function _parseKubeApi(path: string): IKubeApiParsed {
break; break;
} }
apiVersion = left.pop(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
apiVersion = left.pop()!;
apiGroup = left.join("/"); apiGroup = left.join("/");
} else { } else {
switch (left.length) { switch (left.length) {
@ -79,10 +56,12 @@ function _parseKubeApi(path: string): IKubeApiParsed {
[apiGroup, apiVersion, resource, name] = left; [apiGroup, apiVersion, resource, name] = left;
break; break;
case 2: case 2:
resource = left.pop(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
resource = left.pop()!;
// fallthrough // fallthrough
case 1: case 1:
apiVersion = left.pop(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
apiVersion = left.pop()!;
apiGroup = ""; apiGroup = "";
break; break;
default: default:

View File

@ -12,30 +12,51 @@ import logger from "../../main/logger";
import { apiManager } from "./api-manager"; import { apiManager } from "./api-manager";
import { apiBase, apiKube } from "./index"; import { apiBase, apiKube } from "./index";
import { createKubeApiURL, parseKubeApi } from "./kube-api-parse"; import { createKubeApiURL, parseKubeApi } from "./kube-api-parse";
import type { KubeObjectConstructor } from "./kube-object"; import type { KubeObjectConstructor, KubeJsonApiDataFor, KubeObjectMetadata, KubeObjectScope } from "./kube-object";
import { KubeObject, KubeStatus } from "./kube-object"; import { KubeObject, KubeStatus, isKubeStatusData } from "./kube-object";
import byline from "byline"; import byline from "byline";
import type { IKubeWatchEvent } from "./kube-watch-event"; import type { IKubeWatchEvent } from "./kube-watch-event";
import type { KubeJsonApiData } from "./kube-json-api"; import type { KubeJsonApiData } from "./kube-json-api";
import { KubeJsonApi } 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 { RequestInit } from "node-fetch";
import type AbortController from "abort-controller"; import type AbortController from "abort-controller";
import type { AgentOptions } from "https"; import type { AgentOptions } from "https";
import { Agent } from "https"; import { Agent } from "https";
import type { Patch } from "rfc6902"; import type { Patch } from "rfc6902";
import assert from "assert";
import type { PartialDeep } from "type-fest";
/** /**
* The options used for creating a `KubeApi` * 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" * base api-path for listing all resources, e.g. "/api/v1/pods"
* *
* If not specified then will be the one on the `objectConstructor` * If not specified then will be the one on the `objectConstructor`
* @deprecated should be specified by `objectConstructor`
*/ */
apiBase?: string; 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, * If the API uses a different API endpoint (e.g. apiBase) depending on the cluster version,
* fallback API bases can be listed individually. * fallback API bases can be listed individually.
@ -50,27 +71,34 @@ export interface IKubeApiOptions<T extends KubeObject> {
*/ */
checkPreferredVersion?: boolean; checkPreferredVersion?: boolean;
/**
* The constructor for the kube objects returned from the API
*/
objectConstructor: KubeObjectConstructor<T>;
/** /**
* The api instance to use for making requests * The api instance to use for making requests
* *
* @default apiKube * @default apiKube
*/ */
request?: KubeJsonApi; 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 { export interface IKubeApiQueryParams {
@ -118,10 +146,6 @@ export type PropagationPolicy = undefined | "Orphan" | "Foreground" | "Backgroun
*/ */
export interface IKubeApiCluster extends ILocalKubeApiConfig { } export interface IKubeApiCluster extends ILocalKubeApiConfig { }
export type PartialKubeObject<T extends KubeObject> = Partial<Omit<T, "metadata">> & {
metadata?: Partial<T["metadata"]>;
};
export interface IRemoteKubeApiConfig { export interface IRemoteKubeApiConfig {
cluster: { cluster: {
server: string; 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 url = new URL(apiBase.config.serverAddress);
const request = new KubeJsonApi({ const request = new KubeJsonApi({
serverAddress: apiBase.config.serverAddress, 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({ return new apiClass({
objectConstructor: kubeClass, objectConstructor: kubeClass as KubeObjectConstructor<T, KubeJsonApiDataFor<T>>,
request, 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 reqInit: RequestInit = {};
const agentOptions: AgentOptions = {}; const agentOptions: AgentOptions = {};
@ -196,38 +243,26 @@ export function forRemoteCluster<T extends KubeObject, Y extends KubeApi<T> = Ku
}, reqInit); }, reqInit);
if (!apiClass) { if (!apiClass) {
apiClass = KubeApi as new (apiOpts: IKubeApiOptions<T>) => Y; apiClass = KubeApi as new (apiOpts: IKubeApiOptions<K>) => Y;
} }
return new apiClass({ return new apiClass({
objectConstructor: kubeClass as KubeObjectConstructor<T>, objectConstructor: kubeClass as KubeObjectConstructor<K, KubeJsonApiDataFor<K>>,
request, request,
}); });
} }
export function ensureObjectSelfLink(api: KubeApi<KubeObject>, object: KubeJsonApiData) { export type KubeApiWatchCallback<T extends KubeJsonApiData = KubeJsonApiData> = (data: IKubeWatchEvent<T>, error: any) => void;
if (!object.metadata.selfLink) {
object.metadata.selfLink = createKubeApiURL({
apiPrefix: api.apiPrefix,
apiVersion: api.apiVersionWithGroup,
resource: api.apiResource,
namespace: api.isNamespaced ? object.metadata.namespace : undefined,
name: object.metadata.name,
});
}
}
export type KubeApiWatchCallback = (data: IKubeWatchEvent<KubeJsonApiData>, error: any) => void; export interface KubeApiWatchOptions<T extends KubeObject<any, any, KubeObjectScope>, Data extends KubeJsonApiDataFor<T>> {
export interface KubeApiWatchOptions {
namespace: string; namespace: string;
callback?: KubeApiWatchCallback; callback?: KubeApiWatchCallback<Data> | undefined;
abortController?: AbortController; abortController?: AbortController | undefined;
watchId?: string; watchId?: string | undefined;
retry?: boolean; retry?: boolean | undefined;
// timeout in seconds // timeout in seconds
timeout?: number; timeout?: number | undefined;
} }
export type KubeApiPatchType = "merge" | "json" | "strategic"; export type KubeApiPatchType = "merge" | "json" | "strategic";
@ -261,54 +296,74 @@ export interface DeleteResourceDescriptor extends ResourceDescriptor {
propagationPolicy?: PropagationPolicy; 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 kind: string;
readonly apiVersion: string; readonly apiVersion: string;
apiBase: string; apiBase: string;
apiPrefix: string; apiPrefix: string;
apiGroup: string; apiGroup: string;
apiVersionPreferred?: string; apiVersionPreferred: string | undefined;
readonly apiResource: string; readonly apiResource: string;
readonly isNamespaced: boolean; readonly isNamespaced: boolean;
public objectConstructor: KubeObjectConstructor<T>; public readonly objectConstructor: KubeObjectConstructor<K, D>;
protected request: KubeJsonApi; protected readonly request: KubeJsonApi;
protected resourceVersions = new Map<string, string>(); protected readonly resourceVersions = new Map<string, string>();
protected watchDisposer: () => void; protected readonly watchDisposer: Disposer | undefined;
private watchId = 1; private watchId = 1;
protected readonly doCheckPreferredVersion: boolean;
protected readonly fullApiPathname: string;
protected readonly fallbackApiBases?: string[];
constructor(protected options: IKubeApiOptions<T>) { constructor({
const { objectConstructor, request, kind, isNamespaced } = options; objectConstructor,
const { apiBase, apiPrefix, apiGroup, apiVersion, resource } = parseKubeApi(options.apiBase || objectConstructor.apiBase); 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; const { apiBase, apiPrefix, apiGroup, apiVersion, resource } = parseKubeApi(fullApiPathname);
this.kind = kind ?? objectConstructor.kind;
assert(kind);
assert(apiPrefix);
this.doCheckPreferredVersion = doCheckPreferredVersion;
this.fallbackApiBases = fallbackApiBases;
this.fullApiPathname = fullApiPathname;
this.kind = kind;
this.isNamespaced = isNamespaced ?? objectConstructor.namespaced ?? false; this.isNamespaced = isNamespaced ?? objectConstructor.namespaced ?? false;
this.apiBase = apiBase; this.apiBase = apiBase;
this.apiPrefix = apiPrefix; this.apiPrefix = apiPrefix;
this.apiGroup = apiGroup; this.apiGroup = apiGroup;
this.apiVersion = apiVersion; this.apiVersion = apiVersion;
this.apiResource = resource; this.apiResource = resource;
this.request = request ?? apiKube; this.request = request;
this.objectConstructor = objectConstructor; this.objectConstructor = objectConstructor;
this.parseResponse = this.parseResponse.bind(this); this.parseResponse = this.parseResponse.bind(this);
apiManager.registerApi(apiBase, this); apiManager.registerApi<KubeApi<K, D>>(apiBase, this);
} }
get apiVersionWithGroup() { get apiVersionWithGroup() {
return [this.apiGroup, this.apiVersionPreferred ?? this.apiVersion] return [this.apiGroup, this.apiVersionPreferred ?? this.apiVersion]
.filter(Boolean) .filter(isDefined)
.join("/"); .join("/");
} }
/** /**
* Returns the latest API prefix/group that contains the required resource. * 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() { private async getLatestApiPrefixGroup() {
// Note that this.options.apiBase is the "full" url, whereas this.apiBase is parsed // Note that this.fullApiPathname is the "full" url, whereas this.apiBase is parsed
const apiBases = [this.options.apiBase, this.objectConstructor.apiBase, ...this.options.fallbackApiBases]; const apiBases = [this.fullApiPathname, this.objectConstructor.apiBase, ...this.fallbackApiBases ?? []];
for (const apiUrl of apiBases) { for (const apiUrl of apiBases) {
if (!apiUrl) { 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. * Get the apiPrefix and apiGroup to be used for fetching the preferred version.
*/ */
private async getPreferredVersionPrefixGroup() { private async getPreferredVersionPrefixGroup() {
if (this.options.fallbackApiBases) { if (this.fallbackApiBases) {
try { try {
return await this.getLatestApiPrefixGroup(); return await this.getLatestApiPrefixGroup();
} catch (error) { } catch (error) {
@ -354,25 +409,27 @@ export class KubeApi<T extends KubeObject> {
} }
protected async checkPreferredVersion() { 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"); 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(); const { apiPrefix, apiGroup } = await this.getPreferredVersionPrefixGroup();
assert(apiPrefix);
// The apiPrefix and apiGroup might change due to fallbackApiBases, so we must override them // The apiPrefix and apiGroup might change due to fallbackApiBases, so we must override them
this.apiPrefix = apiPrefix; this.apiPrefix = apiPrefix;
this.apiGroup = apiGroup; 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); const res = await this.request.get<IKubePreferredVersion>(url);
this.apiVersionPreferred = res?.preferredVersion?.version ?? null; this.apiVersionPreferred = res?.preferredVersion?.version;
if (this.apiVersionPreferred) { if (this.apiVersionPreferred) {
this.apiBase = this.computeApiBase(); 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; return query;
} }
protected parseResponse(data: unknown, namespace?: string): T | T[] | null { protected parseResponse(data: unknown, namespace?: string): K | K[] | null {
if (!data) return null; if (!data) {
return null;
}
const KubeObjectConstructor = this.objectConstructor; const KubeObjectConstructor = this.objectConstructor;
// process items list response, check before single item since there is overlap // 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(namespace, metadata.resourceVersion);
this.setResourceVersion("", metadata.resourceVersion); this.setResourceVersion("", metadata.resourceVersion);
return items.map((item) => { return items
const object = new KubeObjectConstructor({ .map((item) => {
kind: this.kind, if (item.metadata) {
apiVersion, this.ensureMetadataSelfLink(item.metadata);
...item, } 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 // process a single item
if (KubeObject.isJsonApiData(data)) { if (KubeObject.isJsonApiData(data)) {
const object = new KubeObjectConstructor(data); this.ensureMetadataSelfLink(data.metadata);
ensureObjectSelfLink(this, object); return new KubeObjectConstructor(data as never);
return object;
} }
// custom apis might return array for list response, e.g. users, groups, etc. // custom apis might return array for list response, e.g. users, groups, etc.
if (Array.isArray(data)) { 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; 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(); await this.checkPreferredVersion();
const url = this.getUrl({ namespace }); 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)}`); 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(); await this.checkPreferredVersion();
const url = this.getUrl(desc); const url = this.getUrl(desc);
@ -494,7 +572,7 @@ export class KubeApi<T extends KubeObject> {
return parsed; 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(); await this.checkPreferredVersion();
const apiUrl = this.getUrl({ namespace }); const apiUrl = this.getUrl({ namespace });
@ -517,7 +595,7 @@ export class KubeApi<T extends KubeObject> {
return parsed; 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(); await this.checkPreferredVersion();
const apiUrl = this.getUrl({ namespace, name }); const apiUrl = this.getUrl({ namespace, name });
@ -538,7 +616,7 @@ export class KubeApi<T extends KubeObject> {
return parsed; 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(); await this.checkPreferredVersion();
const apiUrl = this.getUrl(desc); 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)}`); 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) { 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 errorReceived = false;
let timedRetry: NodeJS.Timeout; let timedRetry: NodeJS.Timeout;
const { namespace, callback = noop, retry, timeout } = opts; const { namespace, callback = noop, retry, timeout } = opts;
@ -653,9 +731,9 @@ export class KubeApi<T extends KubeObject> {
byline(response.body).on("data", (line) => { byline(response.body).on("data", (line) => {
try { 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; errorReceived = true;
return callback(null, new KubeStatus(event.object)); 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") { if (event.type === "ERROR") {
return; return;
} }
ensureObjectSelfLink(this, event.object); this.ensureMetadataSelfLink(event.object.metadata);
const { namespace, resourceVersion } = 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(namespace, resourceVersion);
this.setResourceVersion("", resourceVersion); this.setResourceVersion("", resourceVersion);
} }

View File

@ -8,6 +8,7 @@ import { JsonApi } from "./json-api";
import type { Response } from "node-fetch"; import type { Response } from "node-fetch";
import { apiKubePrefix, isDebugging } from "../vars"; import { apiKubePrefix, isDebugging } from "../vars";
import { apiBase } from "./api-base"; import { apiBase } from "./api-base";
import type { KubeJsonApiObjectMetadata } from "./kube-object";
export interface KubeJsonApiListMetadata { export interface KubeJsonApiListMetadata {
resourceVersion: string; resourceVersion: string;
@ -21,28 +22,17 @@ export interface KubeJsonApiDataList<T = KubeJsonApiData> {
metadata: KubeJsonApiListMetadata; metadata: KubeJsonApiListMetadata;
} }
export interface KubeJsonApiMetadata { export interface KubeJsonApiData<
uid: string; Metadata extends KubeJsonApiObjectMetadata = KubeJsonApiObjectMetadata,
name: string; Status = unknown,
namespace?: string; Spec = unknown,
creationTimestamp?: string; > extends JsonApiData {
resourceVersion: string;
continue?: string;
finalizers?: string[];
selfLink?: string;
labels?: {
[label: string]: string;
};
annotations?: {
[annotation: string]: string;
};
[key: string]: any;
}
export interface KubeJsonApiData extends JsonApiData {
kind: string; kind: string;
apiVersion: string; apiVersion: string;
metadata: KubeJsonApiMetadata; metadata: Metadata;
status?: Status;
spec?: Spec;
[otherKeys: string]: unknown;
} }
export interface KubeJsonApiError extends JsonApiError { export interface KubeJsonApiError extends JsonApiError {

View File

@ -7,22 +7,19 @@ import type { ClusterContext } from "./cluster-context";
import { action, computed, makeObservable, observable, reaction, when } from "mobx"; import { action, computed, makeObservable, observable, reaction, when } from "mobx";
import type { Disposer } from "../utils"; import type { Disposer } from "../utils";
import { autoBind, noop, rejectPromiseBy } from "../utils"; import { autoBind, includes, isRequestError, noop, rejectPromiseBy } from "../utils";
import type { KubeObject } from "./kube-object"; import type { KubeJsonApiDataFor, KubeObject } from "./kube-object";
import { KubeStatus } from "./kube-object"; import { KubeStatus } from "./kube-object";
import type { IKubeWatchEvent } from "./kube-watch-event"; import type { IKubeWatchEvent } from "./kube-watch-event";
import { ItemStore } from "../item.store"; import { ItemStore } from "../item.store";
import type { IKubeApiQueryParams, KubeApi } from "./kube-api"; import type { IKubeApiQueryParams, KubeApi, KubeApiWatchCallback } from "./kube-api";
import { ensureObjectSelfLink } from "./kube-api";
import { parseKubeApi } from "./kube-api-parse"; import { parseKubeApi } from "./kube-api-parse";
import type { KubeJsonApiData } from "./kube-json-api";
import type { RequestInit } from "node-fetch"; 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 AbortController from "abort-controller";
import type { Patch } from "rfc6902"; import type { Patch } from "rfc6902";
import logger from "../logger"; import logger from "../logger";
import assert from "assert";
import type { PartialDeep } from "type-fest";
export interface KubeObjectStoreLoadingParams { export interface KubeObjectStoreLoadingParams {
namespaces: string[]; namespaces: string[];
@ -68,13 +65,32 @@ export interface MergeItemsOptions {
namespaces: string[]; namespaces: string[];
} }
export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T> { export interface StatusProvider<K> {
static defaultContext = observable.box<ClusterContext>(); // TODO: support multiple cluster contexts 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 limit?: number;
public readonly bufferSize: number = 50000; public readonly bufferSize: number;
@observable private loadedNamespaces?: string[]; @observable private loadedNamespaces: string[] | undefined = undefined;
get contextReady() { get contextReady() {
return when(() => Boolean(this.context)); return when(() => Boolean(this.context));
@ -84,9 +100,11 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
return when(() => Boolean(this.loadedNamespaces)); return when(() => Boolean(this.loadedNamespaces));
} }
constructor(api?: KubeApi<T>) { constructor(api: A, opts?: KubeObjectStoreOptions) {
super(); super();
if (api) this.api = api; this.api = api;
this.limit = opts?.limit;
this.bufferSize = opts?.bufferSize ?? 50_000;
makeObservable(this); makeObservable(this);
autoBind(this); autoBind(this);
@ -98,7 +116,7 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
} }
// TODO: Circular dependency: KubeObjectStore -> ClusterFrameContext -> NamespaceStore -> KubeObjectStore // TODO: Circular dependency: KubeObjectStore -> ClusterFrameContext -> NamespaceStore -> KubeObjectStore
@computed get contextItems(): T[] { @computed get contextItems(): K[] {
const namespaces = this.context?.contextNamespaces ?? []; const namespaces = this.context?.contextNamespaces ?? [];
return this.items.filter(item => { return this.items.filter(item => {
@ -122,13 +140,11 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
return { limit }; return { limit };
} }
getStatuses?(items: T[]): Record<string, number>; getAllByNs(namespace: string | string[], strict = false): K[] {
const namespaces = [namespace].flat();
getAllByNs(namespace: string | string[], strict = false): T[] {
const namespaces: string[] = [].concat(namespace);
if (namespaces.length) { if (namespaces.length) {
return this.items.filter(item => namespaces.includes(item.getNs())); return this.items.filter(item => includes(namespaces, item.getNs()));
} }
if (!strict) { if (!strict) {
@ -138,11 +154,11 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
return []; return [];
} }
getById(id: string) { getById(id: string | undefined): K | undefined {
return this.items.find(item => item.getId() === id); 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 this.items.find(item => {
return item.getName() === name && ( return item.getName() === name && (
namespace ? item.getNs() === namespace : true 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); 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)) { if (Array.isArray(labels)) {
return this.items.filter((item: T) => { return this.items.filter((item: K) => {
const itemLabels = item.getLabels(); const itemLabels = item.getLabels();
return labels.every(label => itemLabels.includes(label)); return labels.every(label => itemLabels.includes(label));
}); });
} else { } else {
return this.items.filter((item: T) => { return this.items.filter((item: K) => {
const itemLabels = item.metadata.labels || {}; const itemLabels = item.metadata.labels || {};
return Object.entries(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[]> { protected async loadItems({ namespaces, reqInit, onLoadFailure }: KubeObjectStoreLoadingParams): Promise<K[]> {
if (!this.context?.cluster.isAllowedResource(this.api.kind)) { if (!this.context?.cluster?.isAllowedResource(this.api.kind)) {
return []; return [];
} }
@ -189,9 +205,13 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
if (onLoadFailure) { if (onLoadFailure) {
try { try {
return await res; return await res ?? [];
} catch (error) { } 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 // reset the store because we are loading all, so that nothing is displayed
this.items.clear(); 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; this.loadedNamespaces = namespaces;
@ -209,12 +229,12 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
const results = await Promise.allSettled( const results = await Promise.allSettled(
namespaces.map(namespace => this.api.list({ namespace, reqInit }, this.query)), namespaces.map(namespace => this.api.list({ namespace, reqInit }, this.query)),
); );
const res: T[] = []; const res: K[] = [];
for (const result of results) { for (const result of results) {
switch (result.status) { switch (result.status) {
case "fulfilled": case "fulfilled":
res.push(...result.value); res.push(...result.value ?? []);
break; break;
case "rejected": case "rejected":
@ -230,12 +250,12 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
return res; return res;
} }
protected filterItemsOnLoad(items: T[]) { protected filterItemsOnLoad(items: K[]) {
return items; return items;
} }
@action @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; await this.contextReady;
namespaces ??= this.context.contextNamespaces; namespaces ??= this.context.contextNamespaces;
this.isLoading = true; this.isLoading = true;
@ -261,7 +281,7 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
} }
@action @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; const { force = false, ...loadingOptions } = opts;
if (this.isLoading || (this.isLoaded && !force)) { if (this.isLoading || (this.isLoaded && !force)) {
@ -272,7 +292,7 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
} }
@action @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; let items = partialItems;
// update existing items // update existing items
@ -280,7 +300,7 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
const ns = new Set(namespaces); const ns = new Set(namespaces);
items = [ items = [
...this.items.filter(existingItem => !ns.has(existingItem.getNs())), ...this.items.filter(item => !ns.has(item.getNs() as string)),
...partialItems, ...partialItems,
]; ];
} }
@ -296,17 +316,18 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
if (error) this.reset(); 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); return this.api.get(params);
} }
@action @action
async load(params: { name: string; namespace?: string }): Promise<T> { async load(params: { name: string; namespace?: string }): Promise<K> {
const { name, namespace } = params; const { name, namespace } = params;
let item = this.getByName(name, namespace); let item: K | null | undefined = this.getByName(name, namespace);
if (!item) { if (!item) {
item = await this.loadItem(params); item = await this.loadItem(params);
assert(item, "Failed to load item from kube");
const newItems = this.sortItems([...this.items, item]); const newItems = this.sortItems([...this.items, item]);
this.items.replace(newItems); this.items.replace(newItems);
@ -319,15 +340,19 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
async loadFromPath(resourcePath: string) { async loadFromPath(resourcePath: string) {
const { namespace, name } = parseKubeApi(resourcePath); const { namespace, name } = parseKubeApi(resourcePath);
assert(name && namespace, "Both name and namesapce must be part of resourcePath");
return this.load({ name, namespace }); 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); 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); const newItem = await this.createItem(params, data);
assert(newItem, "Failed to create item from kube");
const items = this.sortItems([...this.items, newItem]); const items = this.sortItems([...this.items, newItem]);
this.items.replace(items); this.items.replace(items);
@ -335,12 +360,9 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
return newItem; return newItem;
} }
private postUpdate(rawItem: KubeJsonApiData): T { private postUpdate(newItem: K): K {
const newItem = new this.api.objectConstructor(rawItem);
const index = this.items.findIndex(item => item.getId() === newItem.getId()); const index = this.items.findIndex(item => item.getId() === newItem.getId());
ensureObjectSelfLink(this.api, newItem);
if (index < 0) { if (index < 0) {
this.items.push(newItem); this.items.push(newItem);
} else { } else {
@ -350,45 +372,49 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
return newItem; return newItem;
} }
async patch(item: T, patch: Patch): Promise<T> { async patch(item: K, patch: Patch): Promise<K> {
return this.postUpdate( const rawItem = await this.api.patch(
await this.api.patch( {
{ name: item.getName(), namespace: item.getNs(),
name: item.getName(), namespace: item.getNs(), },
}, patch,
patch, "json",
"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> { async update(item: K, data: PartialDeep<K>): Promise<K> {
return this.postUpdate( const rawItem = await this.api.update(
await this.api.update( {
{ name: item.getName(),
name: item.getName(), namespace: item.getNs(),
namespace: item.getNs(), },
}, data,
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() }); await this.api.delete({ name: item.getName(), namespace: item.getNs() });
this.selectedItemsIds.delete(item.getId()); this.selectedItemsIds.delete(item.getId());
} }
async removeSelectedItems() { 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)); await Promise.all(items.map(this.remove));
} }
// collect items from watch-api events to avoid UI blowing up with huge streams of data // 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) { protected bindWatchEventsUpdater(delay = 1000) {
reaction(() => this.eventsBuffer.length, this.updateFromEventsBuffer, { reaction(() => this.eventsBuffer.length, this.updateFromEventsBuffer, {
@ -400,7 +426,9 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
if (this.api.isNamespaced) { if (this.api.isNamespaced) {
Promise.race([rejectPromiseBy(abortController.signal), Promise.all([this.contextReady, this.namespacesReady])]) Promise.race([rejectPromiseBy(abortController.signal), Promise.all([this.contextReady, this.namespacesReady])])
.then(() => { .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 }); return this.watchNamespace("", abortController, { onLoadFailure });
} }
@ -430,7 +458,7 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
const { signal } = abortController; const { signal } = abortController;
const callback = (data: IKubeWatchEvent<T>, error: any) => { const callback: KubeApiWatchCallback<D> = (data, error) => {
if (!this.isLoaded || error?.type === "aborted") return; if (!this.isLoaded || error?.type === "aborted") return;
if (error instanceof Response) { if (error instanceof Response) {

View File

@ -6,53 +6,210 @@
// Base class for all kubernetes objects // Base class for all kubernetes objects
import moment from "moment"; import moment from "moment";
import type { KubeJsonApiData, KubeJsonApiDataList, KubeJsonApiListMetadata, KubeJsonApiMetadata } from "./kube-json-api"; import type { KubeJsonApiData, KubeJsonApiDataList, KubeJsonApiListMetadata } from "./kube-json-api";
import { autoBind, formatDuration } from "../utils"; import { autoBind, formatDuration, hasOptionalTypedProperty, hasTypedProperty, isObject, isString, isNumber, bindPredicate, isTypedArray, isRecord, json } from "../utils";
import type { ItemObject } from "../item.store"; import type { ItemObject } from "../item.store";
import { apiKube } from "./index"; import { apiKube } from "./index";
import type { JsonApiParams } from "./json-api"; import type { JsonApiParams } from "./json-api";
import * as resourceApplierApi from "./endpoints/resource-applier.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 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) & { export type KubeJsonApiDataFor<K> = K extends KubeObject<infer Status, infer Spec, infer Scope>
kind?: string; ? KubeJsonApiData<KubeObjectMetadata<Scope>, Status, Spec>
namespaced?: boolean; : never;
apiBase?: string;
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>;
}; };
/** interface BaseKubeJsonApiObjectMetadata<Namespaced extends KubeObjectScope> {
* A reference to an object in the same namespace /**
*/ * Annotations is an unstructured key value map stored with a resource that may be set by
export interface LocalObjectReference { * external tools to store and retrieve arbitrary metadata. They are not queryable and should be
name: string; * 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 { export type KubeJsonApiObjectMetadata<Namespaced extends KubeObjectScope = KubeObjectScope> = Namespaced extends "namespace-scoped"
uid: string; ? BaseKubeJsonApiObjectMetadata<"namespace-scoped"> & { readonly namespace: string }
name: string; : BaseKubeJsonApiObjectMetadata<Namespaced>;
namespace?: string;
creationTimestamp: string; export type KubeObjectMetadata<Namespaced extends KubeObjectScope = KubeObjectScope> = KubeJsonApiObjectMetadata<Namespaced> & {
resourceVersion: string; readonly selfLink: string;
selfLink: string; readonly uid: string;
deletionTimestamp?: string; readonly name: string;
finalizers?: string[]; readonly resourceVersion: string;
continue?: string; // provided when used "?limit=" query param to fetch objects list };
labels?: {
[label: string]: string;
};
annotations?: {
[annotation: string]: string;
};
ownerReferences?: {
apiVersion: string;
kind: string;
name: string;
uid: string;
controller: boolean;
blockOwnerDeletion: boolean;
}[];
}
export interface KubeStatusData { export interface KubeStatusData {
kind: string; kind: string;
@ -62,6 +219,16 @@ export interface KubeStatusData {
reason?: string; 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 { export class KubeStatus {
public readonly kind = "Status"; public readonly kind = "Status";
public readonly apiVersion: string; public readonly apiVersion: string;
@ -77,17 +244,36 @@ export class KubeStatus {
} }
} }
export interface KubeObjectStatus { export interface BaseKubeObjectCondition {
conditions?: { /**
lastTransitionTime: string; * Last time the condition transit from one status to another.
message: string; *
reason: string; * @type Date
status: string; */
type?: string; 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 { export class KubeCreationError extends Error {
constructor(message: string, public data: any) { 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 { export interface TypedLocalObjectReference {
apiGroup?: string; apiGroup?: string;
kind: string; kind: string;
name: 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 { export interface LabelSelector {
matchLabels?: Record<string, string | undefined>; matchLabels?: Record<string, string | undefined>;
matchExpressions?: LabelMatchExpression[]; 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 kind?: string;
static readonly namespaced?: boolean; static readonly namespaced?: boolean;
static readonly apiBase?: string; static readonly apiBase?: string;
apiVersion: string; apiVersion!: string;
kind: string; kind!: string;
metadata: Metadata; metadata!: KubeObjectMetadata<Namespaced>;
status?: Status; status?: Status;
spec?: Spec; spec!: Spec;
managedFields?: any;
static create(data: KubeJsonApiData) { static create<
Metadata extends KubeObjectMetadata = KubeObjectMetadata,
Status = unknown,
Spec = unknown,
>(data: KubeJsonApiData<Metadata, Status, Spec>) {
return new KubeObject(data); return new KubeObject(data);
} }
static isNonSystem(item: KubeJsonApiData | KubeObject) { static isNonSystem(item: KubeJsonApiData | KubeObject<unknown, unknown, KubeObjectScope>) {
return !item.metadata.name.startsWith("system:"); return !item.metadata.name?.startsWith("system:");
} }
static isJsonApiData(object: unknown): object is KubeJsonApiData { 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 { static isKubeJsonApiListMetadata(object: unknown): object is KubeJsonApiListMetadata {
return ( return (
isObject(object) isObject(object)
&& hasOptionalProperty(object, "resourceVersion", isString) && hasOptionalTypedProperty(object, "resourceVersion", isString)
&& hasOptionalProperty(object, "selfLink", isString) && hasOptionalTypedProperty(object, "selfLink", isString)
); );
} }
static isKubeJsonApiMetadata(object: unknown): object is KubeJsonApiMetadata { static isKubeJsonApiMetadata(object: unknown): object is KubeJsonApiObjectMetadata {
return ( return (
isObject(object) isObject(object)
&& hasTypedProperty(object, "uid", isString) && hasTypedProperty(object, "uid", isString)
&& hasTypedProperty(object, "name", isString) && hasTypedProperty(object, "name", isString)
&& hasTypedProperty(object, "resourceVersion", isString) && hasTypedProperty(object, "resourceVersion", isString)
&& hasOptionalProperty(object, "selfLink", isString) && hasOptionalTypedProperty(object, "selfLink", isString)
&& hasOptionalProperty(object, "namespace", isString) && hasOptionalTypedProperty(object, "namespace", isString)
&& hasOptionalProperty(object, "creationTimestamp", isString) && hasOptionalTypedProperty(object, "creationTimestamp", isString)
&& hasOptionalProperty(object, "continue", isString) && hasOptionalTypedProperty(object, "continue", isString)
&& hasOptionalProperty(object, "finalizers", bindPredicate(isTypedArray, isString)) && hasOptionalTypedProperty(object, "finalizers", bindPredicate(isTypedArray, isString))
&& hasOptionalProperty(object, "labels", bindPredicate(isRecord, isString, isString)) && hasOptionalTypedProperty(object, "labels", bindPredicate(isRecord, isString, isString))
&& hasOptionalProperty(object, "annotations", 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 ( return (
isObject(object) isObject(object)
&& hasOptionalProperty(object, "uid", isString) && hasOptionalTypedProperty(object, "uid", isString)
&& hasOptionalProperty(object, "name", isString) && hasOptionalTypedProperty(object, "name", isString)
&& hasOptionalProperty(object, "resourceVersion", isString) && hasOptionalTypedProperty(object, "resourceVersion", isString)
&& hasOptionalProperty(object, "selfLink", isString) && hasOptionalTypedProperty(object, "selfLink", isString)
&& hasOptionalProperty(object, "namespace", isString) && hasOptionalTypedProperty(object, "namespace", isString)
&& hasOptionalProperty(object, "creationTimestamp", isString) && hasOptionalTypedProperty(object, "creationTimestamp", isString)
&& hasOptionalProperty(object, "continue", isString) && hasOptionalTypedProperty(object, "continue", isString)
&& hasOptionalProperty(object, "finalizers", bindPredicate(isTypedArray, isString)) && hasOptionalTypedProperty(object, "finalizers", bindPredicate(isTypedArray, isString))
&& hasOptionalProperty(object, "labels", bindPredicate(isRecord, isString, isString)) && hasOptionalTypedProperty(object, "labels", bindPredicate(isRecord, isString, isString))
&& hasOptionalProperty(object, "annotations", bindPredicate(isRecord, isString, isString)) && hasOptionalTypedProperty(object, "annotations", bindPredicate(isRecord, isString, isString))
); );
} }
static isPartialJsonApiData(object: unknown): object is Partial<KubeJsonApiData> { static isPartialJsonApiData(object: unknown): object is Partial<KubeJsonApiData> {
return ( return (
isObject(object) isObject(object)
&& hasOptionalProperty(object, "kind", isString) && hasOptionalTypedProperty(object, "kind", isString)
&& hasOptionalProperty(object, "apiVersion", isString) && hasOptionalTypedProperty(object, "apiVersion", isString)
&& hasOptionalProperty(object, "metadata", KubeObject.isPartialJsonApiMetadata) && 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 []; if (!labels) return [];
return Object.entries(labels).map(([name, value]) => `${name}=${value}`); return Object.entries(labels).map(([name, value]) => `${name}=${value}`);
@ -240,7 +486,7 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
...KubeObject.nonEditablePathPrefixes, ...KubeObject.nonEditablePathPrefixes,
]); ]);
constructor(data: KubeJsonApiData) { constructor(data: KubeJsonApiData<KubeObjectMetadata<Namespaced>, Status, Spec>) {
if (typeof data !== "object") { if (typeof data !== "object") {
throw new TypeError(`Cannot create a KubeObject from ${typeof data}`); 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; return this.metadata.resourceVersion;
} }
getDescriptor() {
const ns = this.getNs();
const res = ns ? `${ns}/` : "";
return res + this.getName();
}
getName() { getName() {
return this.metadata.name; return this.metadata.name;
} }
getNs() { getNs(): ScopedNamespace<Namespaced> {
// avoid "null" serialization via JSON.stringify when post data // 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. * creation timestamp of this object.
*/ */
getCreationTimestamp() { getCreationTimestamp() {
if (!this.metadata.creationTimestamp) {
return Date.now();
}
return new Date(this.metadata.creationTimestamp).getTime(); 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. * NOTE: Generally you can use `getCreationTimestamp` instead.
*/ */
getTimeDiffFromNow(): number { getTimeDiffFromNow(): number {
if (!this.metadata.creationTimestamp) {
return 0;
}
return Date.now() - new Date(this.metadata.creationTimestamp).getTime(); return Date.now() - new Date(this.metadata.creationTimestamp).getTime();
} }
@ -346,8 +607,8 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
]; ];
} }
toPlainObject(): object { toPlainObject() {
return JSON.parse(JSON.stringify(this)); return json.parse(JSON.stringify(this)) as JsonObject;
} }
/** /**
@ -390,6 +651,8 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
* @deprecated use KubeApi.delete instead * @deprecated use KubeApi.delete instead
*/ */
delete(params?: JsonApiParams) { delete(params?: JsonApiParams) {
assert(this.selfLink, "selfLink must be present to delete self");
return apiKube.del(this.selfLink, params); return apiKube.del(this.selfLink, params);
} }
} }

View File

@ -3,14 +3,13 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * 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"; import type { KubeStatusData } from "./kube-object";
export type IKubeWatchEvent<T extends KubeJsonApiData> = { export type IKubeWatchEvent<T> = {
type: "ADDED" | "MODIFIED" | "DELETED"; readonly type: "ADDED" | "MODIFIED" | "DELETED";
object: T; readonly object: T;
} | { } | {
type: "ERROR"; readonly type: "ERROR";
object: KubeStatusData; readonly object?: KubeStatusData;
}; };

Some files were not shown because too many files have changed in this diff Show More