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,
},
}],
"react/jsx-max-props-per-line": ["error", {
"maximum": {
"single": 2,
"multi": 1,
},
}],
"react/jsx-first-prop-new-line": ["error", "multiline"],
"react/jsx-one-expression-per-line": ["error", {
"allow": "single-child",
}],
"react/jsx-indent": ["error", 2],
"react/jsx-indent-props": ["error", 2],
"react/jsx-closing-tag-location": "error",
"react/jsx-wrap-multilines": ["error", {
"declaration": "parens-new-line",
"assignment": "parens-new-line",
"return": "parens-new-line",
"arrow": "parens-new-line",
"condition": "parens-new-line",
"logical": "parens-new-line",
"prop": "parens-new-line",
}],
"react/display-name": "off",
"space-before-function-paren": "off",
"@typescript-eslint/space-before-function-paren": ["error", {

View File

@ -5,11 +5,11 @@
import fs from "fs-extra";
import path from "path";
import defaultBaseLensTheme from "../src/renderer/themes/lens-dark.json";
import defaultBaseLensTheme from "../src/renderer/themes/lens-dark";
const outputCssFile = path.resolve("src/renderer/themes/theme-vars.css");
const banner = `/*
const banner = `/*
Generated Lens theme CSS-variables, don't edit manually.
To refresh file run $: yarn run ts-node build/${path.basename(__filename)}
*/`;

View File

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

View File

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

View File

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

View File

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

View File

@ -68,7 +68,9 @@ export function NodeMenu(props: NodeMenuProps) {
labelOk: `Drain Node`,
message: (
<p>
Are you sure you want to drain <b>{nodeName}</b>?
{"Are you sure you want to drain "}
<b>{nodeName}</b>
?
</p>
),
});
@ -77,26 +79,42 @@ export function NodeMenu(props: NodeMenuProps) {
return (
<>
<MenuItem onClick={shell}>
<Icon svg="ssh" interactive={toolbar} tooltip={toolbar && "Node shell"}/>
<Icon
svg="ssh"
interactive={toolbar}
tooltip={toolbar && "Node shell"}
/>
<span className="title">Shell</span>
</MenuItem>
{
node.isUnschedulable()
? (
<MenuItem onClick={unCordon}>
<Icon material="play_circle_filled" tooltip={toolbar && "Uncordon"} interactive={toolbar} />
<Icon
material="play_circle_filled"
tooltip={toolbar && "Uncordon"}
interactive={toolbar}
/>
<span className="title">Uncordon</span>
</MenuItem>
)
: (
<MenuItem onClick={cordon}>
<Icon material="pause_circle_filled" tooltip={toolbar && "Cordon"} interactive={toolbar} />
<Icon
material="pause_circle_filled"
tooltip={toolbar && "Cordon"}
interactive={toolbar}
/>
<span className="title">Cordon</span>
</MenuItem>
)
}
<MenuItem onClick={drain}>
<Icon material="delete_sweep" tooltip={toolbar && "Drain"} interactive={toolbar}/>
<Icon
material="delete_sweep"
tooltip={toolbar && "Drain"}
interactive={toolbar}
/>
<span className="title">Drain</span>
</MenuItem>
</>

View File

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

View File

@ -71,7 +71,11 @@ export class PodAttachMenu extends React.Component<PodAttachMenuProps> {
return (
<MenuItem onClick={Util.prevDefault(() => this.attachToPod(containers[0].name))}>
<Icon material="pageview" interactive={toolbar} tooltip={toolbar && "Attach to Pod"}/>
<Icon
material="pageview"
interactive={toolbar}
tooltip={toolbar && "Attach to Pod"}
/>
<span className="title">Attach Pod</span>
{containers.length > 1 && (
<>
@ -82,7 +86,11 @@ export class PodAttachMenu extends React.Component<PodAttachMenuProps> {
const { name } = container;
return (
<MenuItem key={name} onClick={Util.prevDefault(() => this.attachToPod(name))} className="flex align-center">
<MenuItem
key={name}
onClick={Util.prevDefault(() => this.attachToPod(name))}
className="flex align-center"
>
<StatusBrick/>
<span>{name}</span>
</MenuItem>

View File

@ -46,7 +46,11 @@ export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
return (
<MenuItem onClick={Util.prevDefault(() => this.showLogs(containers[0]))}>
<Icon material="subject" interactive={toolbar} tooltip={toolbar && "Pod Logs"}/>
<Icon
material="subject"
interactive={toolbar}
tooltip={toolbar && "Pod Logs"}
/>
<span className="title">Logs</span>
{containers.length > 1 && (
<>
@ -63,7 +67,11 @@ export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
) : null;
return (
<MenuItem key={name} onClick={Util.prevDefault(() => this.showLogs(container))} className="flex align-center">
<MenuItem
key={name}
onClick={Util.prevDefault(() => this.showLogs(container))}
className="flex align-center"
>
{brick}
<span>{name}</span>
</MenuItem>

View File

@ -79,7 +79,11 @@ export class PodShellMenu extends React.Component<PodShellMenuProps> {
return (
<MenuItem onClick={Util.prevDefault(() => this.execShell(containers[0].name))}>
<Icon svg="ssh" interactive={toolbar} tooltip={toolbar && "Pod Shell"} />
<Icon
svg="ssh"
interactive={toolbar}
tooltip={toolbar && "Pod Shell"}
/>
<span className="title">Shell</span>
{containers.length > 1 && (
<>
@ -90,7 +94,11 @@ export class PodShellMenu extends React.Component<PodShellMenuProps> {
const { name } = container;
return (
<MenuItem key={name} onClick={Util.prevDefault(() => this.execShell(name))} className="flex align-center">
<MenuItem
key={name}
onClick={Util.prevDefault(() => this.execShell(name))}
className="flex align-center"
>
<StatusBrick/>
<span>{name}</span>
</MenuItem>

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,19 +11,19 @@ import type { Disposer } from "../utils";
import { iter } from "../utils";
import type { CategoryColumnRegistration } from "../../renderer/components/+catalog/custom-category-columns";
type ExtractEntityMetadataType<Entity> = Entity extends CatalogEntity<infer Metadata> ? Metadata : never;
type ExtractEntityStatusType<Entity> = Entity extends CatalogEntity<any, infer Status> ? Status : never;
type ExtractEntitySpecType<Entity> = Entity extends CatalogEntity<any, any, infer Spec> ? Spec : never;
export type CatalogEntityDataFor<Entity> = Entity extends CatalogEntity<infer Metadata, infer Status, infer Spec>
? CatalogEntityData<Metadata, Status, Spec>
: never;
export type CatalogEntityInstanceFrom<Constructor> = Constructor extends CatalogEntityConstructor<infer Entity>
? Entity
: never;
export type CatalogEntityConstructor<Entity extends CatalogEntity> = (
(new (data: CatalogEntityData<
ExtractEntityMetadataType<Entity>,
ExtractEntityStatusType<Entity>,
ExtractEntitySpecType<Entity>
>) => Entity)
new (data: CatalogEntityDataFor<Entity>) => Entity
);
export interface CatalogCategoryVersion<Entity extends CatalogEntity> {
export interface CatalogCategoryVersion {
/**
* The specific version that the associated constructor is for. This MUST be
* a DNS label and SHOULD be of the form `vN`, `vNalphaY`, or `vNbetaY` where
@ -35,19 +35,19 @@ export interface CatalogCategoryVersion<Entity extends CatalogEntity> {
* - `v1alpha2`
* - `v3beta2`
*/
name: string;
readonly name: string;
/**
* The constructor for the entities.
*/
entityClass: CatalogEntityConstructor<Entity>;
readonly entityClass: CatalogEntityConstructor<CatalogEntity>;
}
export interface CatalogCategorySpec {
/**
* The grouping for for the category. This MUST be a DNS label.
*/
group: string;
readonly group: string;
/**
* The specific versions of the constructors.
@ -56,18 +56,18 @@ export interface CatalogCategorySpec {
* For example, if `group = "entity.k8slens.dev"` and there is an entry in `.versions` with
* `name = "v1alpha1"` then the resulting `.apiVersion` MUST be `entity.k8slens.dev/v1alpha1`
*/
versions: CatalogCategoryVersion<CatalogEntity>[];
readonly versions: CatalogCategoryVersion[];
/**
* This is the concerning the category
*/
names: {
readonly names: {
/**
* The kind of entity that this category is for. This value MUST be a DNS
* label and MUST be equal to the `kind` fields that are produced by the
* `.versions.[] | .entityClass` fields.
*/
kind: string;
readonly kind: string;
};
/**
@ -81,7 +81,7 @@ export interface CatalogCategorySpec {
*
* These columns will not be used in the "Browse" view.
*/
displayColumns?: CategoryColumnRegistration[];
readonly displayColumns?: CategoryColumnRegistration[];
}
/**
@ -109,6 +109,30 @@ export interface CatalogCategoryEvents {
contextMenuOpen: (entity: CatalogEntity, context: CatalogEntityContextMenuContext) => void;
}
export interface CatalogCategoryMetadata {
/**
* The name of your category. The category can be searched for by this
* value. This will also be used for the catalog menu.
*/
readonly name: string;
/**
* Either an `<svg>` or the name of an icon from {@link IconProps}
*/
readonly icon: string;
}
export function categoryVersion<
T extends CatalogEntity<Metadata, Status, Spec>,
Metadata extends CatalogEntityMetadata,
Status extends CatalogEntityStatus,
Spec extends CatalogEntitySpec,
>(name: string, entityClass: new (data: CatalogEntityData<Metadata, Status, Spec>) => T): CatalogCategoryVersion {
return {
name,
entityClass: entityClass as CatalogEntityConstructor<T>,
};
}
export abstract class CatalogCategory extends (EventEmitter as new () => TypedEmitter<CatalogCategoryEvents>) {
/**
* The version of category that you are wanting to declare.
@ -131,28 +155,17 @@ export abstract class CatalogCategory extends (EventEmitter as new () => TypedEm
/**
* The data about the category itself
*/
abstract readonly metadata: {
/**
* The name of your category. The category can be searched for by this
* value. This will also be used for the catalog menu.
*/
name: string;
/**
* Either an `<svg>` or the name of an icon from {@link IconProps}
*/
icon: string;
};
abstract readonly metadata: CatalogCategoryMetadata;
/**
* The most important part of a category, as it is where entity versions are declared.
*/
abstract spec: CatalogCategorySpec;
abstract readonly spec: CatalogCategorySpec;
/**
* @internal
*/
protected filters = observable.set<AddMenuFilter>([], {
protected readonly filters = observable.set<AddMenuFilter>([], {
deep: false,
});
@ -217,14 +230,16 @@ export abstract class CatalogCategory extends (EventEmitter as new () => TypedEm
}
}
export interface CatalogEntityMetadata {
export type EntityMetadataObject = { [Key in string]?: EntityMetadataValue };
export type EntityMetadataValue = string | number | boolean | EntityMetadataObject | undefined;
export interface CatalogEntityMetadata extends EntityMetadataObject {
uid: string;
name: string;
shortName?: string;
description?: string;
source?: string;
labels: Record<string, string>;
[key: string]: string | object;
}
export interface CatalogEntityStatus {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { action, comparer, observable, makeObservable, computed } from "mobx";
import { action, comparer, observable, makeObservable, computed, runInAction } from "mobx";
import { BaseStore } from "./base-store";
import migrations from "../migrations/hotbar-store";
import { toJS } from "./utils";
@ -33,7 +33,7 @@ interface Dependencies {
export class HotbarStore extends BaseStore<HotbarStoreModel> {
readonly displayName = "HotbarStore";
@observable hotbars: Hotbar[] = [];
@observable private _activeHotbarId: string;
@observable private _activeHotbarId!: string;
constructor(private dependencies: Dependencies) {
super({
@ -120,8 +120,22 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
return toJS(model);
}
getActive() {
return this.getById(this.activeHotbarId);
getActive(): Hotbar {
const hotbar = this.getById(this.activeHotbarId);
if (hotbar) {
return hotbar;
}
runInAction(() => {
if (this.hotbars.length === 0) {
this.hotbars.push(getEmptyHotbar("Default"));
}
this._activeHotbarId = this.hotbars[0].id;
});
return this.hotbars[0];
}
getByName(name: string) {
@ -147,7 +161,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
},
);
setHotbarName = action((id: string, name: string) => {
setHotbarName = action((id: string, name: string): void => {
const index = this.hotbars.findIndex((hotbar) => hotbar.id === id);
if (index < 0) {

View File

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

View File

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

View File

@ -53,7 +53,7 @@ describe("type enforced ipc tests", () => {
const source = new EventEmitter();
const listener = () => called += 1;
const results = [true, false, true];
const verifier = (args: unknown[]): args is [] => results.pop();
const verifier = (args: unknown[]): args is [] => results.pop() ?? false;
const channel = "foobar";
onCorrect({ source, listener, verifier, channel });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,15 +6,24 @@
import type { KubeObjectStore } from "./kube-object.store";
import { action, observable, makeObservable } from "mobx";
import { autoBind, iter } from "../utils";
import { autoBind, isDefined, iter } from "../utils";
import type { KubeApi } from "./kube-api";
import type { KubeObject } from "./kube-object";
import type { IKubeObjectRef } from "./kube-api-parse";
import type { KubeJsonApiDataFor, KubeObject, ObjectReference } from "./kube-object";
import { parseKubeApi, createKubeApiURL } from "./kube-api-parse";
export type RegisterableStore<S> = S extends KubeObjectStore<any, any, any>
? S
: never;
export type RegisterableApi<A> = A extends KubeApi<any, any>
? A
: never;
export type KubeObjectStoreFrom<A> = A extends KubeApi<infer K, infer D>
? KubeObjectStore<K, A, D>
: never;
export class ApiManager {
private apis = observable.map<string, KubeApi<KubeObject>>();
private stores = observable.map<string, KubeObjectStore<KubeObject>>();
private readonly apis = observable.map<string, KubeApi>();
private readonly stores = observable.map<string, KubeObjectStore>();
constructor() {
makeObservable(this);
@ -33,27 +42,27 @@ export class ApiManager {
return iter.find(this.apis.values(), api => api.kind === kind && api.apiVersionWithGroup === apiVersion);
}
registerApi<K extends KubeObject>(apiBase: string, api: KubeApi<K>) {
registerApi<A>(apiBase: string, api: RegisterableApi<A>) {
if (!api.apiBase) return;
if (!this.apis.has(apiBase)) {
this.stores.forEach((store) => {
if (store.api === api) {
if (store.api as never === api) {
this.stores.set(apiBase, store);
}
});
this.apis.set(apiBase, api);
this.apis.set(apiBase, api as never);
}
}
protected resolveApi(api?: string | KubeApi<KubeObject>): KubeApi<KubeObject> | undefined {
protected resolveApi(api: undefined | string | KubeApi): KubeApi | undefined {
if (!api) {
return undefined;
}
if (typeof api === "string") {
return this.getApi(api) as KubeApi<KubeObject>;
return this.getApi(api);
}
return api;
@ -69,20 +78,40 @@ export class ApiManager {
}
}
registerStore<K>(store: RegisterableStore<K>): void;
/**
* @deprecated KubeObjectStore's should only every be about a single KubeApi type
*/
registerStore<K extends KubeObject>(store: KubeObjectStore<K, KubeApi<K>, KubeJsonApiDataFor<K>>, apis: KubeApi<K>[]): void;
@action
registerStore<K extends KubeObject>(store: KubeObjectStore<K>, apis: KubeApi<K>[] = [store.api]) {
apis.filter(Boolean).forEach(api => {
if (api.apiBase) this.stores.set(api.apiBase, store);
});
registerStore<K extends KubeObject>(store: KubeObjectStore<K, KubeApi<K>, KubeJsonApiDataFor<K>>, apis: KubeApi<K>[] = [store.api]): void {
for (const api of apis.filter(isDefined)) {
if (api.apiBase) {
this.stores.set(api.apiBase, store as never);
}
}
}
getStore<S extends KubeObjectStore<KubeObject>>(api: string | KubeApi<KubeObject>): S | undefined {
return this.stores.get(this.resolveApi(api)?.apiBase) as S;
getStore(api: string | undefined): KubeObjectStore | undefined;
getStore<A>(api: RegisterableApi<A>): KubeObjectStoreFrom<A> | undefined;
/**
* @deprecated use an actual cast instead of hiding it with this unused type param
*/
getStore<S extends KubeObjectStore>(api: string | KubeApi): S | undefined ;
getStore(api: string | KubeApi | undefined): KubeObjectStore | undefined {
const { apiBase } = this.resolveApi(api) ?? {};
if (apiBase) {
return this.stores.get(apiBase);
}
return undefined;
}
lookupApiLink(ref: IKubeObjectRef, parentObject?: KubeObject): string {
lookupApiLink(ref: ObjectReference, parentObject?: KubeObject): string {
const {
kind, apiVersion, name,
kind, apiVersion = "v1", name,
namespace = parentObject?.getNs(),
} = ref;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,20 +3,26 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { IAffinity } from "../workload-kube-object";
import { WorkloadKubeObject } from "../workload-kube-object";
import { autoBind } from "../../utils";
import type { IMetrics } from "./metrics.api";
import type { MetricData } from "./metrics.api";
import { metricsApi } from "./metrics.api";
import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api";
import { KubeApi } from "../kube-api";
import type { KubeJsonApiData } from "../kube-json-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
import type { RequireExactlyOne } from "type-fest";
import type { KubeObjectMetadata, LocalObjectReference } from "../kube-object";
import type { KubeObjectMetadata, LocalObjectReference, Affinity, Toleration, LabelSelector } from "../kube-object";
import type { SecretReference } from "./secret.api";
import type { PersistentVolumeClaimSpec } from "./persistent-volume-claims.api";
import { KubeObject } from "../kube-object";
import { isDefined } from "../../utils";
export class PodApi extends KubeApi<Pod> {
constructor(opts: DerivedKubeApiOptions & IgnoredKubeApiOptions = {}) {
super({
...opts,
objectConstructor: Pod,
});
}
export class PodsApi extends KubeApi<Pod> {
getLogs = async (params: { namespace: string; name: string }, query?: IPodLogsQuery): Promise<string> => {
const path = `${this.getUrl(params)}/log`;
@ -24,7 +30,7 @@ export class PodsApi extends KubeApi<Pod> {
};
}
export function getMetricsForPods(pods: Pod[], namespace: string, selector = "pod, namespace"): Promise<IPodMetrics> {
export function getMetricsForPods(pods: Pod[], namespace: string, selector = "pod, namespace"): Promise<PodMetricData> {
const podSelector = pods.map(pod => pod.getName()).join("|");
const opts = { category: "pods", pods: podSelector, namespace, selector };
@ -45,19 +51,18 @@ export function getMetricsForPods(pods: Pod[], namespace: string, selector = "po
});
}
export interface IPodMetrics<T = IMetrics> {
[metric: string]: T;
cpuUsage: T;
memoryUsage: T;
fsUsage: T;
fsWrites: T;
fsReads: T;
networkReceive: T;
networkTransmit: T;
cpuRequests?: T;
cpuLimits?: T;
memoryRequests?: T;
memoryLimits?: T;
export interface PodMetricData extends Partial<Record<string, MetricData>> {
cpuUsage: MetricData;
memoryUsage: MetricData;
fsUsage: MetricData;
fsWrites: MetricData;
fsReads: MetricData;
networkReceive: MetricData;
networkTransmit: MetricData;
cpuRequests?: MetricData;
cpuLimits?: MetricData;
memoryRequests?: MetricData;
memoryLimits?: MetricData;
}
// Reference: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#read-log-pod-v1-core
@ -70,7 +75,7 @@ export interface IPodLogsQuery {
previous?: boolean;
}
export enum PodStatus {
export enum PodStatusPhase {
TERMINATED = "Terminated",
FAILED = "Failed",
PENDING = "Pending",
@ -79,26 +84,41 @@ export enum PodStatus {
EVICTED = "Evicted",
}
export interface ContainerPort {
containerPort: number;
hostIP?: string;
hostPort?: number;
name?: string;
protocol?: "UDP" | "TCP" | "SCTP";
}
export interface VolumeMount {
name: string;
readOnly?: boolean;
mountPath: string;
mountPropagation?: string;
subPath?: string;
subPathExpr?: string;
}
export interface IPodContainer extends Partial<Record<PodContainerProbe, IContainerProbe>> {
name: string;
image: string;
command?: string[];
args?: string[];
ports?: {
name?: string;
containerPort: number;
protocol: string;
}[];
ports?: ContainerPort[];
resources?: {
limits: {
limits?: {
cpu: string;
memory: string;
};
requests: {
requests?: {
cpu: string;
memory: string;
};
};
terminationMessagePath?: string;
terminationMessagePolicy?: string;
env?: {
name: string;
value?: string;
@ -121,12 +141,8 @@ export interface IPodContainer extends Partial<Record<PodContainerProbe, IContai
configMapRef?: LocalObjectReference;
secretRef?: LocalObjectReference;
}[];
volumeMounts?: {
name: string;
readOnly: boolean;
mountPath: string;
}[];
imagePullPolicy: string;
volumeMounts?: VolumeMount[];
imagePullPolicy?: string;
}
export type PodContainerProbe = "livenessProbe" | "readinessProbe" | "startupProbe";
@ -631,85 +647,133 @@ export interface PodVolumeVariants {
*/
export type PodVolumeKind = keyof PodVolumeVariants;
export type PodVolume = RequireExactlyOne<PodVolumeVariants> & {
export type PodSpecVolume = RequireExactlyOne<PodVolumeVariants> & {
name: string;
};
export class Pod extends WorkloadKubeObject {
export interface HostAlias {
ip: string;
hostnames: string[];
}
export interface SELinuxOptions {
level?: string;
role?: string;
type?: string;
user?: string;
}
export interface SeccompProfile {
localhostProfile?: string;
type: string;
}
export interface Sysctl {
name: string;
value: string;
}
export interface WindowsSecurityContextOptions {
labelSelector?: LabelSelector;
maxSkew: number;
topologyKey: string;
whenUnsatisfiable: string;
}
export interface PodSecurityContext {
fsGroup?: number;
fsGroupChangePolicy?: string;
runAsGroup?: number;
runAsNonRoot?: boolean;
runAsUser?: number;
seLinuxOptions?: SELinuxOptions;
seccompProfile?: SeccompProfile;
supplementalGroups?: number[];
sysctls?: Sysctl;
windowsOptions?: WindowsSecurityContextOptions;
}
export interface TopologySpreadConstraint {
}
export interface PodSpec {
activeDeadlineSeconds?: number;
affinity?: Affinity;
automountServiceAccountToken?: boolean;
containers?: IPodContainer[];
dnsPolicy?: string;
enableServiceLinks?: boolean;
ephemeralContainers?: unknown[];
hostAliases?: HostAlias[];
hostIPC?: boolean;
hostname?: string;
hostNetwork?: boolean;
hostPID?: boolean;
imagePullSecrets?: LocalObjectReference[];
initContainers?: IPodContainer[];
nodeName?: string;
nodeSelector?: Record<string, string | undefined>;
overhead?: Record<string, string | undefined>;
preemptionPolicy?: string;
priority?: number;
priorityClassName?: string;
readinessGates?: unknown[];
restartPolicy?: string;
runtimeClassName?: string;
schedulerName?: string;
securityContext?: PodSecurityContext;
serviceAccount?: string;
serviceAccountName?: string;
setHostnameAsFQDN?: boolean;
shareProcessNamespace?: boolean;
subdomain?: string;
terminationGracePeriodSeconds?: number;
tolerations?: Toleration[];
topologySpreadConstraints?: TopologySpreadConstraint[];
volumes?: PodSpecVolume[];
}
export interface PodCondition {
lastProbeTime?: number;
lastTransitionTime?: string;
message?: string;
reason?: string;
type: string;
status: string;
}
export interface PodStatus {
phase: string;
conditions: PodCondition[];
hostIP: string;
podIP: string;
podIPs?: {
ip: string;
}[];
startTime: string;
initContainerStatuses?: IPodContainerStatus[];
containerStatuses?: IPodContainerStatus[];
qosClass?: string;
reason?: string;
}
export class Pod extends KubeObject<PodStatus, PodSpec, "namespace-scoped"> {
static kind = "Pod";
static namespaced = true;
static apiBase = "/api/v1/pods";
constructor(data: KubeJsonApiData) {
super(data);
autoBind(this);
getAffinityNumber() {
return Object.keys(this.getAffinity()).length;
}
declare spec?: {
volumes?: PodVolume[];
initContainers: IPodContainer[];
containers: IPodContainer[];
restartPolicy?: string;
terminationGracePeriodSeconds?: number;
activeDeadlineSeconds?: number;
dnsPolicy?: string;
serviceAccountName: string;
serviceAccount: string;
automountServiceAccountToken?: boolean;
priority?: number;
priorityClassName?: string;
nodeName?: string;
nodeSelector?: {
[selector: string]: string;
};
securityContext?: {};
imagePullSecrets?: LocalObjectReference[];
hostNetwork?: boolean;
hostPID?: boolean;
hostIPC?: boolean;
shareProcessNamespace?: boolean;
hostname?: string;
subdomain?: string;
schedulerName?: string;
tolerations?: {
key?: string;
operator?: string;
effect?: string;
tolerationSeconds?: number;
value?: string;
}[];
hostAliases?: {
ip: string;
hostnames: string[];
};
affinity?: IAffinity;
};
declare status?: {
phase: string;
conditions: {
type: string;
status: string;
lastProbeTime: number;
lastTransitionTime: string;
}[];
hostIP: string;
podIP: string;
podIPs?: {
ip: string;
}[];
startTime: string;
initContainerStatuses?: IPodContainerStatus[];
containerStatuses?: IPodContainerStatus[];
qosClass?: string;
reason?: string;
};
getInitContainers() {
return this.spec?.initContainers || [];
return this.spec?.initContainers ?? [];
}
getContainers() {
return this.spec?.containers || [];
return this.spec?.containers ?? [];
}
getAllContainers() {
@ -719,7 +783,7 @@ export class Pod extends WorkloadKubeObject {
getRunningContainers() {
const runningContainerNames = new Set(
this.getContainerStatuses()
.filter(({ state }) => state.running)
.filter(({ state }) => state?.running)
.map(({ name }) => name),
);
@ -752,10 +816,10 @@ export class Pod extends WorkloadKubeObject {
}
getPriorityClassName() {
return this.spec.priorityClassName || "";
return this.spec?.priorityClassName || "";
}
getStatus(): PodStatus {
getStatus(): PodStatusPhase {
const phase = this.getStatusPhase();
const reason = this.getReason();
const trueConditionTypes = new Set(this.getConditions()
@ -763,28 +827,28 @@ export class Pod extends WorkloadKubeObject {
.map(({ type }) => type));
const isInGoodCondition = ["Initialized", "Ready"].every(condition => trueConditionTypes.has(condition));
if (reason === PodStatus.EVICTED) {
return PodStatus.EVICTED;
if (reason === PodStatusPhase.EVICTED) {
return PodStatusPhase.EVICTED;
}
if (phase === PodStatus.FAILED) {
return PodStatus.FAILED;
if (phase === PodStatusPhase.FAILED) {
return PodStatusPhase.FAILED;
}
if (phase === PodStatus.SUCCEEDED) {
return PodStatus.SUCCEEDED;
if (phase === PodStatusPhase.SUCCEEDED) {
return PodStatusPhase.SUCCEEDED;
}
if (phase === PodStatus.RUNNING && isInGoodCondition) {
return PodStatus.RUNNING;
if (phase === PodStatusPhase.RUNNING && isInGoodCondition) {
return PodStatusPhase.RUNNING;
}
return PodStatus.PENDING;
return PodStatusPhase.PENDING;
}
// Returns pod phase or container error if occurred
getStatusMessage(): string {
if (this.getReason() === PodStatus.EVICTED) {
if (this.getReason() === PodStatusPhase.EVICTED) {
return "Evicted";
}
@ -800,31 +864,30 @@ export class Pod extends WorkloadKubeObject {
}
getConditions() {
return this.status?.conditions || [];
return this.status?.conditions ?? [];
}
getVolumes() {
return this.spec.volumes || [];
return this.spec?.volumes ?? [];
}
getSecrets(): string[] {
return this.getVolumes()
.filter(vol => vol.secret)
.map(vol => vol.secret.secretName);
.map(vol => vol.secret?.secretName)
.filter(isDefined);
}
getNodeSelectors(): string[] {
const { nodeSelector = {}} = this.spec;
return Object.entries(nodeSelector).map(values => values.join(": "));
return Object.entries(this.spec?.nodeSelector ?? {})
.map(values => values.join(": "));
}
getTolerations() {
return this.spec.tolerations || [];
return this.spec?.tolerations ?? [];
}
getAffinity(): IAffinity {
return this.spec.affinity;
getAffinity(): Affinity {
return this.spec?.affinity ?? {};
}
hasIssues() {
@ -907,30 +970,21 @@ export class Pod extends WorkloadKubeObject {
return probe;
}
getNodeName() {
return this.spec.nodeName;
getNodeName(): string | undefined {
return this.spec?.nodeName;
}
getSelectedNodeOs(): string | undefined {
return this.spec.nodeSelector?.["kubernetes.io/os"] || this.spec.nodeSelector?.["beta.kubernetes.io/os"];
return this.spec?.nodeSelector?.["kubernetes.io/os"] || this.spec?.nodeSelector?.["beta.kubernetes.io/os"];
}
getIPs(): string[] {
if(!this.status.podIPs) return [];
const podIPs = this.status.podIPs;
const podIPs = this.status?.podIPs ?? [];
return podIPs.map(value => value.ip);
}
}
let podsApi: PodsApi;
if (isClusterPageContext()) {
podsApi = new PodsApi({
objectConstructor: Pod,
});
}
export {
podsApi,
};
export const podApi = isClusterPageContext()
? new PodApi()
: undefined as never;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,8 +3,8 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
// Clone json-serializable object
import type { LabelSelector } from "../../kube-object";
export function cloneJsonObject<T = object>(obj: T): T {
return JSON.parse(JSON.stringify(obj));
export interface AggregationRule {
clusterRoleSelectors?: LabelSelector;
}

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,53 +6,210 @@
// Base class for all kubernetes objects
import moment from "moment";
import type { KubeJsonApiData, KubeJsonApiDataList, KubeJsonApiListMetadata, KubeJsonApiMetadata } from "./kube-json-api";
import { autoBind, formatDuration } from "../utils";
import type { KubeJsonApiData, KubeJsonApiDataList, KubeJsonApiListMetadata } from "./kube-json-api";
import { autoBind, formatDuration, hasOptionalTypedProperty, hasTypedProperty, isObject, isString, isNumber, bindPredicate, isTypedArray, isRecord, json } from "../utils";
import type { ItemObject } from "../item.store";
import { apiKube } from "./index";
import type { JsonApiParams } from "./json-api";
import * as resourceApplierApi from "./endpoints/resource-applier.api";
import { hasOptionalProperty, hasTypedProperty, isObject, isString, bindPredicate, isTypedArray, isRecord } from "../../common/utils/type-narrowing";
import type { Patch } from "rfc6902";
import assert from "assert";
import type { JsonObject } from "type-fest";
export type KubeObjectConstructor<K extends KubeObject> = (new (data: KubeJsonApiData | any) => K) & {
kind?: string;
namespaced?: boolean;
apiBase?: string;
export type KubeJsonApiDataFor<K> = K extends KubeObject<infer Status, infer Spec, infer Scope>
? KubeJsonApiData<KubeObjectMetadata<Scope>, Status, Spec>
: never;
export interface KubeObjectConstructorData {
readonly kind?: string;
readonly namespaced?: boolean;
readonly apiBase?: string;
}
export type KubeObjectConstructor<K extends KubeObject<any, any, KubeObjectScope>, Data> = (new (data: Data) => K) & KubeObjectConstructorData;
export interface OwnerReference {
apiVersion: string;
kind: string;
name: string;
uid: string;
controller?: boolean;
blockOwnerDeletion?: boolean;
}
export type KubeTemplateObjectMetadata<Namespaced extends KubeObjectScope> = Pick<KubeJsonApiObjectMetadata<KubeObjectScope>, "annotations" | "finalizers" | "generateName" | "labels" | "ownerReferences"> & {
name?: string;
namespace?: ScopedNamespace<Namespaced>;
};
/**
* A reference to an object in the same namespace
*/
export interface LocalObjectReference {
name: string;
interface BaseKubeJsonApiObjectMetadata<Namespaced extends KubeObjectScope> {
/**
* Annotations is an unstructured key value map stored with a resource that may be set by
* external tools to store and retrieve arbitrary metadata. They are not queryable and should be
* preserved when modifying objects.
*
* More info: http://kubernetes.io/docs/user-guide/annotations
*/
annotations?: Partial<Record<string, string>>;
/**
* The name of the cluster which the object belongs to. This is used to distinguish resources
* with same name and namespace in different clusters. This field is not set anywhere right now
* and apiserver is going to ignore it if set in create or update request.
*/
clusterName?: string;
/**
* CreationTimestamp is a timestamp representing the server time when this object was created. It
* is not guaranteed to be set in happens-before order across separate operations. Clients may
* not set this value. It is represented in RFC3339 form and is in UTC. Populated by the system.
*
* More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
*/
readonly creationTimestamp?: string;
/**
* Number of seconds allowed for this object to gracefully terminate before it will be removed
* from the system. Only set when deletionTimestamp is also set. May only be shortened.
*/
readonly deletionGracePeriodSeconds?: number;
/**
* DeletionTimestamp is RFC 3339 date and time at which this resource will be deleted. This field
* is set by the server when a graceful deletion is requested by the user, and is not directly
* settable by a client. The resource is expected to be deleted (no longer visible from resource
* lists, and not reachable by name) after the time in this field, once the finalizers list is
* empty. As long as the finalizers list contains items, deletion is blocked. Once the
* `deletionTimestamp` is set, this value may not be unset or be set further into the future,
* although it may be shortened or the resource may be deleted prior to this time. For example,
* a user may request that a pod is deleted in 30 seconds. The Kubelet will react by sending a
* graceful termination signal to the containers in the pod. After that 30 seconds, the Kubelet
* will send a hard termination signal (SIGKILL) to the container and after cleanup, remove the
* pod from the API. In the presence of network partitions, this object may still exist after
* this timestamp, until an administrator or automated process can determine the resource is
* fully terminated. If not set, graceful deletion of the object has not been requested.
* Populated by the system when a graceful deletion is requested.
*
* More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
*/
readonly deletionTimestamp?: string;
/**
* Must be empty before the object is deleted from the registry. Each entry is an identifier for
* the responsible component that will remove the entry from the list. If the deletionTimestamp
* of the object is non-nil, entries in this list can only be removed. Finalizers may be
* processed and removed in any order. Order is NOT enforced because it introduces significant
* risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder
* it. If the finalizer list is processed in order, then this can lead to a situation in which
* the component responsible for the first finalizer in the list is waiting for a signal (field
* value, external system, or other) produced by a component responsible for a finalizer later in
* the list, resulting in a deadlock. Without enforced ordering finalizers are free to order
* amongst themselves and are not vulnerable to ordering changes in the list.
*/
finalizers?: string[];
/**
* GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the
* Name field has not been provided. If this field is used, the name returned to the client will
* be different than the name passed. This value will also be combined with a unique suffix. The
* provided value has the same validation rules as the Name field, and may be truncated by the
* length of the suffix required to make the value unique on the server. If this field is
* specified and the generated name exists, the server will NOT return a 409 - instead, it will
* either return 201 Created or 500 with Reason ServerTimeout indicating a unique name could not
* be found in the time allotted, and the client should retry (optionally after the time indicated
* in the Retry-After header). Applied only if Name is not specified.
*
* More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency
*/
generateName?: string;
/**
* A sequence number representing a specific generation of the desired state. Populated by the
* system.
*/
readonly generation?: number;
/**
* Map of string keys and values that can be used to organize and categorize (scope and select)
* objects. May match selectors of replication controllers and services.
*
* More info: http://kubernetes.io/docs/user-guide/labels
*/
labels?: Partial<Record<string, string>>;
/**
* ManagedFields maps workflow-id and version to the set of fields that are managed by that
* workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set
* or understand this field. A workflow can be the user's name, a controller's name, or the name
* of a specific apply path like "ci-cd". The set of fields is always in the version that the
* workflow used when modifying the object.
*/
managedFields?: unknown[];
/**
* Name must be unique within a namespace. Is required when creating resources, although some
* resources may allow a client to request the generation of an appropriate name automatically.
* Name is primarily intended for creation idempotence and configuration definition.
*
* More info: http://kubernetes.io/docs/user-guide/identifiers#names
*/
readonly name: string;
/**
* Namespace defines the space within which each name must be unique. An empty namespace is
* equivalent to the "default" namespace, but "default" is the canonical representation. Not all
* objects are required to be scoped to a namespace - the value of this field for those objects
* will be empty. Must be a DNS_LABEL. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/namespaces
*/
readonly namespace?: ScopedNamespace<Namespaced>;
/**
* List of objects depended by this object. If ALL objects in the list have been deleted, this
* object will be garbage collected. If this object is managed by a controller, then an entry in
* this list will point to this controller, with the controller field set to true. There cannot
* be more than one managing controller.
*/
ownerReferences?: OwnerReference[];
/**
* An opaque value that represents the internal version of this object that can be used by
* clients to determine when objects have changed. May be used for optimistic concurrency, change
* detection, and the watch operation on a resource or set of resources. Clients must treat these
* values as opaque and passed unmodified back to the server. They may only be valid for a
* particular resource or set of resources. Populated by the system. Value must be treated as
* opaque by clients.
*
* More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency
*/
readonly resourceVersion?: string;
/**
* SelfLink is a URL representing this object. Populated by the system.
*/
readonly selfLink?: string;
/**
* UID is the unique in time and space value for this object. It is typically generated by the
* server on successful creation of a resource and is not allowed to change on PUT operations.
* Populated by the system.
*
* More info: http://kubernetes.io/docs/user-guide/identifiers#uids
*/
readonly uid?: string;
[key: string]: unknown;
}
export interface KubeObjectMetadata {
uid: string;
name: string;
namespace?: string;
creationTimestamp: string;
resourceVersion: string;
selfLink: string;
deletionTimestamp?: string;
finalizers?: string[];
continue?: string; // provided when used "?limit=" query param to fetch objects list
labels?: {
[label: string]: string;
};
annotations?: {
[annotation: string]: string;
};
ownerReferences?: {
apiVersion: string;
kind: string;
name: string;
uid: string;
controller: boolean;
blockOwnerDeletion: boolean;
}[];
}
export type KubeJsonApiObjectMetadata<Namespaced extends KubeObjectScope = KubeObjectScope> = Namespaced extends "namespace-scoped"
? BaseKubeJsonApiObjectMetadata<"namespace-scoped"> & { readonly namespace: string }
: BaseKubeJsonApiObjectMetadata<Namespaced>;
export type KubeObjectMetadata<Namespaced extends KubeObjectScope = KubeObjectScope> = KubeJsonApiObjectMetadata<Namespaced> & {
readonly selfLink: string;
readonly uid: string;
readonly name: string;
readonly resourceVersion: string;
};
export interface KubeStatusData {
kind: string;
@ -62,6 +219,16 @@ export interface KubeStatusData {
reason?: string;
}
export function isKubeStatusData(object: unknown): object is KubeStatusData {
return isObject(object)
&& hasTypedProperty(object, "kind", isString)
&& hasTypedProperty(object, "apiVersion", isString)
&& hasTypedProperty(object, "code", isNumber)
&& hasOptionalTypedProperty(object, "message", isString)
&& hasOptionalTypedProperty(object, "reason", isString)
&& object.kind === "Status";
}
export class KubeStatus {
public readonly kind = "Status";
public readonly apiVersion: string;
@ -77,17 +244,36 @@ export class KubeStatus {
}
}
export interface KubeObjectStatus {
conditions?: {
lastTransitionTime: string;
message: string;
reason: string;
status: string;
type?: string;
}[];
export interface BaseKubeObjectCondition {
/**
* Last time the condition transit from one status to another.
*
* @type Date
*/
lastTransitionTime?: string;
/**
* A human readable message indicating details about last transition.
*/
message?: string;
/**
* brief (usually one word) readon for the condition's last transition.
*/
reason?: string;
/**
* Status of the condition
*/
status: "True" | "False" | "Unknown";
/**
* Type of the condition
*/
type: string;
}
export type KubeMetaField = keyof KubeObjectMetadata;
export interface KubeObjectStatus {
conditions?: BaseKubeObjectCondition[];
}
export type KubeMetaField = keyof KubeJsonApiObjectMetadata;
export class KubeCreationError extends Error {
constructor(message: string, public data: any) {
@ -118,35 +304,95 @@ export type LabelMatchExpression = {
}
);
export interface Toleration {
key?: string;
operator?: string;
effect?: string;
value?: string;
tolerationSeconds?: number;
}
export interface ObjectReference {
apiVersion?: string;
fieldPath?: string;
kind?: string;
name: string;
namespace?: string;
resourceVersion?: string;
uid?: string;
}
export interface LocalObjectReference {
name: string;
}
export interface TypedLocalObjectReference {
apiGroup?: string;
kind: string;
name: string;
}
export interface NodeAffinity {
nodeSelectorTerms?: LabelSelector[];
weight: number;
preference: LabelSelector;
}
export interface PodAffinity {
labelSelector: LabelSelector;
topologyKey: string;
}
export interface SpecificAffinity<T> {
requiredDuringSchedulingIgnoredDuringExecution?: T[];
preferredDuringSchedulingIgnoredDuringExecution?: T[];
}
export interface Affinity {
nodeAffinity?: SpecificAffinity<NodeAffinity>;
podAffinity?: SpecificAffinity<PodAffinity>;
podAntiAffinity?: SpecificAffinity<PodAffinity>;
}
export interface LabelSelector {
matchLabels?: Record<string, string | undefined>;
matchExpressions?: LabelMatchExpression[];
}
export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata, Status = any, Spec = any> implements ItemObject {
export type KubeObjectScope = "namespace-scoped" | "cluster-scoped";
export type ScopedNamespace<Namespaced extends KubeObjectScope> = (
Namespaced extends "namespace-scoped"
? string
: Namespaced extends "cluster-scoped"
? undefined
: string | undefined
);
export class KubeObject<
Status = unknown,
Spec = unknown,
Namespaced extends KubeObjectScope = KubeObjectScope,
> implements ItemObject {
static readonly kind?: string;
static readonly namespaced?: boolean;
static readonly apiBase?: string;
apiVersion: string;
kind: string;
metadata: Metadata;
apiVersion!: string;
kind!: string;
metadata!: KubeObjectMetadata<Namespaced>;
status?: Status;
spec?: Spec;
managedFields?: any;
spec!: Spec;
static create(data: KubeJsonApiData) {
static create<
Metadata extends KubeObjectMetadata = KubeObjectMetadata,
Status = unknown,
Spec = unknown,
>(data: KubeJsonApiData<Metadata, Status, Spec>) {
return new KubeObject(data);
}
static isNonSystem(item: KubeJsonApiData | KubeObject) {
return !item.metadata.name.startsWith("system:");
static isNonSystem(item: KubeJsonApiData | KubeObject<unknown, unknown, KubeObjectScope>) {
return !item.metadata.name?.startsWith("system:");
}
static isJsonApiData(object: unknown): object is KubeJsonApiData {
@ -161,49 +407,49 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
static isKubeJsonApiListMetadata(object: unknown): object is KubeJsonApiListMetadata {
return (
isObject(object)
&& hasOptionalProperty(object, "resourceVersion", isString)
&& hasOptionalProperty(object, "selfLink", isString)
&& hasOptionalTypedProperty(object, "resourceVersion", isString)
&& hasOptionalTypedProperty(object, "selfLink", isString)
);
}
static isKubeJsonApiMetadata(object: unknown): object is KubeJsonApiMetadata {
static isKubeJsonApiMetadata(object: unknown): object is KubeJsonApiObjectMetadata {
return (
isObject(object)
&& hasTypedProperty(object, "uid", isString)
&& hasTypedProperty(object, "name", isString)
&& hasTypedProperty(object, "resourceVersion", isString)
&& hasOptionalProperty(object, "selfLink", isString)
&& hasOptionalProperty(object, "namespace", isString)
&& hasOptionalProperty(object, "creationTimestamp", isString)
&& hasOptionalProperty(object, "continue", isString)
&& hasOptionalProperty(object, "finalizers", bindPredicate(isTypedArray, isString))
&& hasOptionalProperty(object, "labels", bindPredicate(isRecord, isString, isString))
&& hasOptionalProperty(object, "annotations", bindPredicate(isRecord, isString, isString))
&& hasOptionalTypedProperty(object, "selfLink", isString)
&& hasOptionalTypedProperty(object, "namespace", isString)
&& hasOptionalTypedProperty(object, "creationTimestamp", isString)
&& hasOptionalTypedProperty(object, "continue", isString)
&& hasOptionalTypedProperty(object, "finalizers", bindPredicate(isTypedArray, isString))
&& hasOptionalTypedProperty(object, "labels", bindPredicate(isRecord, isString, isString))
&& hasOptionalTypedProperty(object, "annotations", bindPredicate(isRecord, isString, isString))
);
}
static isPartialJsonApiMetadata(object: unknown): object is Partial<KubeJsonApiMetadata> {
static isPartialJsonApiMetadata(object: unknown): object is Partial<KubeJsonApiObjectMetadata> {
return (
isObject(object)
&& hasOptionalProperty(object, "uid", isString)
&& hasOptionalProperty(object, "name", isString)
&& hasOptionalProperty(object, "resourceVersion", isString)
&& hasOptionalProperty(object, "selfLink", isString)
&& hasOptionalProperty(object, "namespace", isString)
&& hasOptionalProperty(object, "creationTimestamp", isString)
&& hasOptionalProperty(object, "continue", isString)
&& hasOptionalProperty(object, "finalizers", bindPredicate(isTypedArray, isString))
&& hasOptionalProperty(object, "labels", bindPredicate(isRecord, isString, isString))
&& hasOptionalProperty(object, "annotations", bindPredicate(isRecord, isString, isString))
&& hasOptionalTypedProperty(object, "uid", isString)
&& hasOptionalTypedProperty(object, "name", isString)
&& hasOptionalTypedProperty(object, "resourceVersion", isString)
&& hasOptionalTypedProperty(object, "selfLink", isString)
&& hasOptionalTypedProperty(object, "namespace", isString)
&& hasOptionalTypedProperty(object, "creationTimestamp", isString)
&& hasOptionalTypedProperty(object, "continue", isString)
&& hasOptionalTypedProperty(object, "finalizers", bindPredicate(isTypedArray, isString))
&& hasOptionalTypedProperty(object, "labels", bindPredicate(isRecord, isString, isString))
&& hasOptionalTypedProperty(object, "annotations", bindPredicate(isRecord, isString, isString))
);
}
static isPartialJsonApiData(object: unknown): object is Partial<KubeJsonApiData> {
return (
isObject(object)
&& hasOptionalProperty(object, "kind", isString)
&& hasOptionalProperty(object, "apiVersion", isString)
&& hasOptionalProperty(object, "metadata", KubeObject.isPartialJsonApiMetadata)
&& hasOptionalTypedProperty(object, "kind", isString)
&& hasOptionalTypedProperty(object, "apiVersion", isString)
&& hasOptionalTypedProperty(object, "metadata", KubeObject.isPartialJsonApiMetadata)
);
}
@ -217,7 +463,7 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
);
}
static stringifyLabels(labels?: { [name: string]: string }): string[] {
static stringifyLabels(labels?: Record<string, string | undefined>): string[] {
if (!labels) return [];
return Object.entries(labels).map(([name, value]) => `${name}=${value}`);
@ -240,7 +486,7 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
...KubeObject.nonEditablePathPrefixes,
]);
constructor(data: KubeJsonApiData) {
constructor(data: KubeJsonApiData<KubeObjectMetadata<Namespaced>, Status, Spec>) {
if (typeof data !== "object") {
throw new TypeError(`Cannot create a KubeObject from ${typeof data}`);
}
@ -265,13 +511,20 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
return this.metadata.resourceVersion;
}
getDescriptor() {
const ns = this.getNs();
const res = ns ? `${ns}/` : "";
return res + this.getName();
}
getName() {
return this.metadata.name;
}
getNs() {
getNs(): ScopedNamespace<Namespaced> {
// avoid "null" serialization via JSON.stringify when post data
return this.metadata.namespace || undefined;
return (this.metadata.namespace || undefined) as never;
}
/**
@ -279,6 +532,10 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
* creation timestamp of this object.
*/
getCreationTimestamp() {
if (!this.metadata.creationTimestamp) {
return Date.now();
}
return new Date(this.metadata.creationTimestamp).getTime();
}
@ -288,6 +545,10 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
* NOTE: Generally you can use `getCreationTimestamp` instead.
*/
getTimeDiffFromNow(): number {
if (!this.metadata.creationTimestamp) {
return 0;
}
return Date.now() - new Date(this.metadata.creationTimestamp).getTime();
}
@ -346,8 +607,8 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
];
}
toPlainObject(): object {
return JSON.parse(JSON.stringify(this));
toPlainObject() {
return json.parse(JSON.stringify(this)) as JsonObject;
}
/**
@ -390,6 +651,8 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
* @deprecated use KubeApi.delete instead
*/
delete(params?: JsonApiParams) {
assert(this.selfLink, "selfLink must be present to delete self");
return apiKube.del(this.selfLink, params);
}
}

View File

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

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