1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Turn on strict mode for TS

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2021-04-13 20:06:11 -04:00
parent 6c7095fdec
commit b2616d4cf2
125 changed files with 2451 additions and 2118 deletions

View File

@ -27,7 +27,7 @@ export type CatalogEntityMetadata = {
labels: {
[key: string]: string;
}
[key: string]: string | object;
[key: string]: string | object | undefined;
};
export type CatalogEntityStatus = {

View File

@ -12,6 +12,7 @@ import { saveToAppFiles } from "./utils/saveToAppFiles";
import { KubeConfig } from "@kubernetes/client-node";
import { handleRequest, requestMain, subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc";
import { ResourceType } from "../renderer/components/+cluster-settings/components/cluster-metrics-setting";
import { assert } from "./utils";
export interface ClusterIconUpload {
clusterId: string;
@ -51,7 +52,7 @@ export interface ClusterModel {
workspace?: string;
/** User context in kubeconfig */
contextName?: string;
contextName: string;
/** Preferences */
preferences?: ClusterPreferences;
@ -106,7 +107,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
return filePath;
}
@observable activeCluster: ClusterId;
@observable activeCluster: ClusterId | null = null;
@observable removedClusters = observable.map<ClusterId, Cluster>();
@observable clusters = observable.map<ClusterId, Cluster>();
@ -218,14 +219,14 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
}
@action
setActive(clusterId: ClusterId) {
const cluster = this.clusters.get(clusterId);
setActive(clusterId?: ClusterId | null) {
const cluster = this.getById(clusterId);
if (!cluster?.enabled) {
clusterId = null;
if (!clusterId || !cluster?.enabled) {
this.activeCluster = null;
} else {
this.activeCluster = clusterId;
}
this.activeCluster = clusterId;
}
deactivate(id: ClusterId) {
@ -238,8 +239,8 @@ 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 | null): Cluster | null {
return (id ? this.clusters.get(id) : null) ?? null;
}
@action
@ -304,7 +305,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
let cluster = currentClusters.get(clusterModel.id);
if (cluster) {
cluster.updateModel(clusterModel);
Object.assign(cluster, clusterModel);
} else {
cluster = new Cluster(clusterModel);
@ -322,14 +323,14 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
}
});
this.activeCluster = newClusters.get(activeCluster)?.enabled ? activeCluster : null;
this.activeCluster = activeCluster && newClusters.get(activeCluster)?.enabled ? activeCluster : null;
this.clusters.replace(newClusters);
this.removedClusters.replace(removedClusters);
}
toJSON(): ClusterStoreModel {
return toJS({
activeCluster: this.activeCluster,
activeCluster: this.activeCluster ?? undefined,
clusters: this.clustersList.map(cluster => cluster.toJSON()),
}, {
recurseEverything: true
@ -354,6 +355,10 @@ export function getHostedClusterId() {
return getClusterIdFromHost(location.host);
}
export function getHostedCluster(): Cluster {
export function getHostedCluster(): Cluster | null {
return clusterStore.getById(getHostedClusterId());
}
export function getHostedClusterStrict(): Cluster {
return assert(getHostedCluster(), "only can get the hosted cluster in a cluster frame");
}

View File

@ -6,6 +6,7 @@ import yaml from "js-yaml";
import logger from "../main/logger";
import commandExists from "command-exists";
import { ExecValidationNotFoundError } from "./custom-errors";
import { NotFalsy } from "./utils";
export type KubeConfigValidationOpts = {
validateCluster?: boolean;
@ -26,10 +27,12 @@ function resolveTilde(filePath: string) {
export function loadConfig(pathOrContent?: string): KubeConfig {
const kc = new KubeConfig();
if (fse.pathExistsSync(pathOrContent)) {
kc.loadFromFile(path.resolve(resolveTilde(pathOrContent)));
} else {
kc.loadFromString(pathOrContent);
if (pathOrContent) {
if (fse.pathExistsSync(pathOrContent)) {
kc.loadFromFile(path.resolve(resolveTilde(pathOrContent)));
} else {
kc.loadFromString(pathOrContent);
}
}
return kc;
@ -75,9 +78,9 @@ export function splitConfig(kubeConfig: KubeConfig): KubeConfig[] {
kubeConfig.contexts.forEach(ctx => {
const kc = new KubeConfig();
kc.clusters = [kubeConfig.getCluster(ctx.cluster)].filter(n => n);
kc.users = [kubeConfig.getUser(ctx.user)].filter(n => n);
kc.contexts = [kubeConfig.getContextObject(ctx.name)].filter(n => n);
kc.clusters = [kubeConfig.getCluster(ctx.cluster)].filter(NotFalsy);
kc.users = [kubeConfig.getUser(ctx.user)].filter(NotFalsy);
kc.contexts = [kubeConfig.getContextObject(ctx.name)].filter(NotFalsy);
kc.setCurrentContext(ctx.name);
configs.push(kc);
@ -92,43 +95,37 @@ export function dumpConfigYaml(kubeConfig: Partial<KubeConfig>): string {
kind: "Config",
preferences: {},
"current-context": kubeConfig.currentContext,
clusters: kubeConfig.clusters.map(cluster => {
return {
name: cluster.name,
cluster: {
"certificate-authority-data": cluster.caData,
"certificate-authority": cluster.caFile,
server: cluster.server,
"insecure-skip-tls-verify": cluster.skipTLSVerify
}
};
}),
contexts: kubeConfig.contexts.map(context => {
return {
name: context.name,
context: {
cluster: context.cluster,
user: context.user,
namespace: context.namespace
}
};
}),
users: kubeConfig.users.map(user => {
return {
name: user.name,
user: {
"client-certificate-data": user.certData,
"client-certificate": user.certFile,
"client-key-data": user.keyData,
"client-key": user.keyFile,
"auth-provider": user.authProvider,
exec: user.exec,
token: user.token,
username: user.username,
password: user.password
}
};
})
clusters: kubeConfig.clusters?.map(cluster => ({
name: cluster.name,
cluster: {
"certificate-authority-data": cluster.caData,
"certificate-authority": cluster.caFile,
server: cluster.server,
"insecure-skip-tls-verify": cluster.skipTLSVerify
}
})),
contexts: kubeConfig.contexts?.map(context => ({
name: context.name,
context: {
cluster: context.cluster,
user: context.user,
namespace: context.namespace
}
})),
users: kubeConfig.users?.map(user => ({
name: user.name,
user: {
"client-certificate-data": user.certData,
"client-certificate": user.certFile,
"client-key-data": user.keyData,
"client-key": user.keyFile,
"auth-provider": user.authProvider,
exec: user.exec,
token: user.token,
username: user.username,
password: user.password
}
}))
};
logger.debug("Dumping KubeConfig:", config);
@ -139,21 +136,21 @@ export function dumpConfigYaml(kubeConfig: Partial<KubeConfig>): string {
export function podHasIssues(pod: V1Pod) {
// Logic adapted from dashboard
const notReady = !!pod.status.conditions.find(condition => {
const notReady = !!pod.status?.conditions?.find(condition => {
return condition.type == "Ready" && condition.status !== "True";
});
return (
notReady ||
pod.status.phase !== "Running" ||
pod.spec.priority > 500000 // We're interested in high prio pods events regardless of their running status
pod.status?.phase !== "Running" ||
(pod.spec?.priority && pod.spec?.priority > 500000) // We're interested in high priority pods events regardless of their running status
);
}
export function getNodeWarningConditions(node: V1Node) {
return node.status.conditions.filter(c =>
return node.status?.conditions?.filter(c =>
c.status.toLowerCase() === "true" && c.type !== "Ready" && c.type !== "HostUpgrades"
);
) ?? [];
}
/**

View File

@ -31,6 +31,8 @@ export class RoutingError extends Error {
return "no extension ID";
case RoutingErrorType.MISSING_EXTENSION:
return "extension not found";
default:
throw new TypeError("this.type is not RoutingErrorType");
}
}
}

View File

@ -72,7 +72,7 @@ export abstract class LensProtocolRouter extends Singleton {
/**
* find the most specific matching handler and call it
* @param routes the array of (path schemas, handler) paris to match against
* @param routes the array of (path schemas, handler) pairs to match against
* @param url the url (in its current state)
*/
protected _route(routes: [string, RouteHandler][], url: Url, extensionName?: string): void {

View File

@ -75,7 +75,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
// open at system start-up
reaction(() => this.preferences.openAtLogin, openAtLogin => {
app.setLoginItemSettings({
app.setLoginItemSettings({
openAtLogin,
openAsHidden: true,
args: ["--hidden"]
@ -102,11 +102,11 @@ export class UserStore extends BaseStore<UserStoreModel> {
@action
setHiddenTableColumns(tableId: string, names: Set<string> | string[]) {
this.preferences.hiddenTableColumns[tableId] = Array.from(names);
(this.preferences.hiddenTableColumns ??= {})[tableId] = Array.from(names);
}
getHiddenTableColumns(tableId: string): Set<string> {
return new Set(this.preferences.hiddenTableColumns[tableId]);
return new Set(this.preferences.hiddenTableColumns?.[tableId]);
}
@action

View File

@ -4,8 +4,13 @@ type Constructor<T = {}> = new (...args: any[]) => T;
export function autobind() {
return function (target: Constructor | object, prop?: string, descriptor?: PropertyDescriptor) {
if (target instanceof Function) return bindClass(target);
else return bindMethod(target, prop, descriptor);
if (target instanceof Function) {
return bindClass(target);
}
if (prop) {
return bindMethod(target, prop, descriptor);
}
};
}
@ -25,7 +30,7 @@ function bindClass<T extends Constructor>(constructor: T) {
});
}
function bindMethod(target: object, prop?: string, descriptor?: PropertyDescriptor) {
function bindMethod(target: object, prop: string, descriptor?: PropertyDescriptor) {
if (!descriptor || typeof descriptor.value !== "function") {
throw new Error(`@autobind() must be used on class or method only`);
}

View File

@ -1,10 +0,0 @@
// Debouncing promise evaluation
export function debouncePromise<T, F extends any[]>(func: (...args: F) => T | Promise<T>, timeout = 0): (...args: F) => Promise<T> {
let timer: NodeJS.Timeout;
return (...params: any[]) => new Promise(resolve => {
clearTimeout(timer);
timer = global.setTimeout(() => resolve(func.apply(this, params)), timeout);
});
}

View File

@ -8,7 +8,6 @@ export * from "./base64";
export * from "./camelCase";
export * from "./cloneJson";
export * from "./delay";
export * from "./debouncePromise";
export * from "./defineGlobal";
export * from "./getRandId";
export * from "./splitArray";
@ -19,3 +18,4 @@ export * from "./downloadFile";
export * from "./escapeRegExp";
export * from "./tar";
export * from "./type-narrowing";
export * from "./remove-falsy";

View File

@ -0,0 +1,20 @@
import { AssertionError } from "assert";
export function NotFalsy<T>(input: T | undefined | null | false | "" | 0): input is T {
return Boolean(input);
}
export function SecondNotFalsy<T, V>(input: readonly [T, V | undefined | null | false | "" | 0]): input is [T, V] {
return Boolean(input[1]);
}
export function assert<T>(input: T | undefined | null | false | "" | 0, message?: string): T {
if (!NotFalsy(input)) {
throw new AssertionError({
actual: input,
message: message ?? "input should pass checker",
});
}
return input;
}

View File

@ -12,9 +12,9 @@ import { clusterStore } from "../common/cluster-store";
export interface ClusterFeatureStatus {
/** feature's current version, as set by the implementation */
currentVersion: string;
currentVersion: string | null;
/** feature's latest version, as set by the implementation */
latestVersion: string;
latestVersion: string | null;
/** whether the feature is installed or not, as set by the implementation */
installed: boolean;
/** whether the feature can be upgraded or not, as set by the implementation */

View File

@ -6,6 +6,7 @@ import { observable, reaction, toJS, when } from "mobx";
import os from "os";
import path from "path";
import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc";
import { assert, NotFalsy } from "../common/utils";
import { getBundledExtensions } from "../common/utils/app-version";
import logger from "../main/logger";
import { extensionInstaller, PackageJson } from "./extension-installer";
@ -52,7 +53,7 @@ const isDirectoryLike = (lstat: fs.Stats) => lstat.isDirectory() || lstat.isSymb
* - "remove": When extension is removed. The event is of type LensExtensionId
*/
export class ExtensionDiscovery {
protected bundledFolderPath: string;
protected bundledFolderPath?: string;
private loadStarted = false;
private extensions: Map<string, InstalledExtension> = new Map();
@ -136,7 +137,7 @@ export class ExtensionDiscovery {
depth: 1,
ignoreInitial: true,
// Try to wait until the file has been completely copied.
// The OS might emit an event for added file even it's not completely written to the filesysten.
// The OS might emit an event for added file even it's not completely written to the filesystem.
awaitWriteFinish: {
// Wait 300ms until the file size doesn't change to consider the file written.
// For a small file like package.json this should be plenty of time.
@ -236,7 +237,7 @@ export class ExtensionDiscovery {
/**
* Uninstalls extension.
* The application will detect the folder unlink and remove the extension from the UI automatically.
* @param extension Extension to unistall.
* @param extension Extension to uninstall.
*/
async uninstallExtension({ absolutePath, manifest }: InstalledExtension) {
logger.info(`${logModule} Uninstalling ${manifest.name}`);
@ -260,7 +261,6 @@ export class ExtensionDiscovery {
// fs.remove won't throw if path is missing
await fs.remove(path.join(extensionInstaller.extensionPackagesRoot, "package-lock.json"));
try {
// Verify write access to static/extensions, which is needed for symlinking
await fs.access(this.inTreeFolderPath, fs.constants.W_OK);
@ -314,16 +314,11 @@ export class ExtensionDiscovery {
* Returns InstalledExtension from path to package.json file.
* Also updates this.packagesJson.
*/
protected async getByManifest(manifestPath: string, { isBundled = false }: {
isBundled?: boolean;
} = {}): Promise<InstalledExtension | null> {
let manifestJson: LensExtensionManifest;
protected async getByManifest(manifestPath: string, { isBundled = false }: { isBundled?: boolean; } = {}): Promise<InstalledExtension | null> {
let manifestJson: LensExtensionManifest | undefined = undefined;
try {
// check manifest file for existence
fs.accessSync(manifestPath, fs.constants.F_OK);
manifestJson = __non_webpack_require__(manifestPath);
manifestJson = __non_webpack_require__(manifestPath) as LensExtensionManifest;
const installedManifestPath = this.getInstalledManifestPath(manifestJson.name);
const isEnabled = isBundled || extensionsStore.isEnabled(installedManifestPath);
@ -380,8 +375,8 @@ export class ExtensionDiscovery {
}
async loadBundledExtensions() {
const folderPath = assert(this.bundledFolderPath, "load() must be called before loadBundledExtensions()");
const extensions: InstalledExtension[] = [];
const folderPath = this.bundledFolderPath;
const bundledExtensions = getBundledExtensions();
const paths = await fs.readdir(folderPath);

View File

@ -76,7 +76,7 @@ export class ExtensionInstaller {
});
let stderr = "";
child.stderr.on("data", data => {
child.stderr?.on("data", data => {
stderr += String(data);
});

View File

@ -3,7 +3,7 @@ import { EventEmitter } from "events";
import { isEqual } from "lodash";
import { action, computed, observable, reaction, toJS, when } from "mobx";
import path from "path";
import { getHostedCluster } from "../common/cluster-store";
import { getHostedClusterStrict } from "../common/cluster-store";
import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc";
import logger from "../main/logger";
import type { InstalledExtension } from "./extension-discovery";
@ -188,14 +188,16 @@ export class ExtensionLoader {
loadOnMain() {
logger.debug(`${logModule}: load on main`);
this.autoInitExtensions(async (extension: LensMainExtension) => {
this.autoInitExtensions(async extension => {
const mainExt = extension as LensMainExtension;
// Each .add returns a function to remove the item
const removeItems = [
registries.menuRegistry.add(extension.appMenus)
registries.menuRegistry.add(mainExt.appMenus)
];
this.events.on("remove", (removedExtension: LensRendererExtension) => {
if (removedExtension.id === extension.id) {
if (removedExtension.id === mainExt.id) {
removeItems.forEach(remove => {
remove();
});
@ -208,17 +210,19 @@ export class ExtensionLoader {
loadOnClusterManagerRenderer() {
logger.debug(`${logModule}: load on main renderer (cluster manager)`);
this.autoInitExtensions(async (extension: LensRendererExtension) => {
this.autoInitExtensions(async extension => {
const rendererExt = extension as LensRendererExtension;
const removeItems = [
registries.globalPageRegistry.add(extension.globalPages, extension),
registries.globalPageMenuRegistry.add(extension.globalPageMenus, extension),
registries.appPreferenceRegistry.add(extension.appPreferences),
registries.statusBarRegistry.add(extension.statusBarItems),
registries.commandRegistry.add(extension.commands),
registries.globalPageRegistry.add(rendererExt.globalPages, rendererExt),
registries.globalPageMenuRegistry.add(rendererExt.globalPageMenus, rendererExt),
registries.appPreferenceRegistry.add(rendererExt.appPreferences),
registries.statusBarRegistry.add(rendererExt.statusBarItems),
registries.commandRegistry.add(rendererExt.commands),
];
this.events.on("remove", (removedExtension: LensRendererExtension) => {
if (removedExtension.id === extension.id) {
if (removedExtension.id === rendererExt.id) {
removeItems.forEach(remove => {
remove();
});
@ -231,24 +235,26 @@ export class ExtensionLoader {
loadOnClusterRenderer() {
logger.debug(`${logModule}: load on cluster renderer (dashboard)`);
const cluster = getHostedCluster();
const cluster = getHostedClusterStrict();
this.autoInitExtensions(async (extension: LensRendererExtension) => {
if (await extension.isEnabledForCluster(cluster) === false) {
this.autoInitExtensions(async extension => {
const rendererExt = extension as LensRendererExtension;
if (await rendererExt.isEnabledForCluster(cluster) === false) {
return [];
}
const removeItems = [
registries.clusterPageRegistry.add(extension.clusterPages, extension),
registries.clusterPageMenuRegistry.add(extension.clusterPageMenus, extension),
registries.kubeObjectMenuRegistry.add(extension.kubeObjectMenuItems),
registries.kubeObjectDetailRegistry.add(extension.kubeObjectDetailItems),
registries.kubeObjectStatusRegistry.add(extension.kubeObjectStatusTexts),
registries.commandRegistry.add(extension.commands),
registries.clusterPageRegistry.add(rendererExt.clusterPages, rendererExt),
registries.clusterPageMenuRegistry.add(rendererExt.clusterPageMenus, rendererExt),
registries.kubeObjectMenuRegistry.add(rendererExt.kubeObjectMenuItems),
registries.kubeObjectDetailRegistry.add(rendererExt.kubeObjectDetailItems),
registries.kubeObjectStatusRegistry.add(rendererExt.kubeObjectStatusTexts),
registries.commandRegistry.add(rendererExt.commands),
];
this.events.on("remove", (removedExtension: LensRendererExtension) => {
if (removedExtension.id === extension.id) {
if (removedExtension.id === rendererExt.id) {
removeItems.forEach(remove => {
remove();
});
@ -289,7 +295,7 @@ export class ExtensionLoader {
});
}
protected requireExtension(extension: InstalledExtension): LensExtensionConstructor {
protected requireExtension(extension: InstalledExtension): LensExtensionConstructor | undefined {
let extEntrypoint = "";
try {
@ -314,7 +320,7 @@ export class ExtensionLoader {
}
}
getExtension(extId: LensExtensionId): InstalledExtension {
getExtension(extId: LensExtensionId): InstalledExtension | undefined {
return this.extensions.get(extId);
}

View File

@ -1,9 +1,10 @@
import { BaseStore } from "../common/base-store";
import * as path from "path";
import { LensExtension } from "./lens-extension";
import { assert } from "../common/utils";
export abstract class ExtensionStore<T> extends BaseStore<T> {
protected extension: LensExtension;
protected extension?: LensExtension;
async loadExtension(extension: LensExtension) {
this.extension = extension;
@ -18,6 +19,8 @@ export abstract class ExtensionStore<T> extends BaseStore<T> {
}
protected cwd() {
return path.join(super.cwd(), "extension-store", this.extension.name);
const extension = assert(this.extension, "must call loadExtension() before calling cwd()");
return path.join(super.cwd(), "extension-store", extension.name);
}
}

View File

@ -5,10 +5,10 @@ import { getExtensionPageUrl } from "./registries/page-registry";
import { CommandRegistration } from "./registries/command-registry";
export class LensRendererExtension extends LensExtension {
globalPages: PageRegistration[] = [];
clusterPages: PageRegistration[] = [];
globalPageMenus: PageMenuRegistration[] = [];
clusterPageMenus: ClusterPageMenuRegistration[] = [];
globalPages: PageRegistration<any>[] = [];
clusterPages: PageRegistration<any>[] = [];
globalPageMenus: PageMenuRegistration<any>[] = [];
clusterPageMenus: ClusterPageMenuRegistration<any>[] = [];
kubeObjectStatusTexts: KubeObjectStatusRegistration[] = [];
appPreferences: AppPreferenceRegistration[] = [];
statusBarItems: StatusBarRegistration[] = [];

View File

@ -18,7 +18,7 @@ export interface CommandRegistration {
}
export class CommandRegistry extends BaseRegistry<CommandRegistration> {
@observable activeEntity: CatalogEntity;
@observable activeEntity?: CatalogEntity;
@action
add(items: CommandRegistration | CommandRegistration[], extension?: LensExtension) {

View File

@ -12,17 +12,20 @@ export interface KubeObjectDetailRegistration {
priority?: number;
}
export interface RegisteredKubeObjectDetails extends KubeObjectDetailRegistration {
priority: number;
}
export class KubeObjectDetailRegistry extends BaseRegistry<KubeObjectDetailRegistration> {
getItemsForKind(kind: string, apiVersion: string) {
const items = this.getItems().filter((item) => {
return item.kind === kind && item.apiVersions.includes(apiVersion);
}).map((item) => {
if (item.priority === null) {
item.priority = 50;
}
return item;
});
const items = this.getItems()
.filter(item => (
item.kind === kind
&& item.apiVersions.includes(apiVersion)
))
.map(item => (
item.priority ??= 50, item as RegisteredKubeObjectDetails
));
return items.sort((a, b) => b.priority - a.priority);
}

View File

@ -4,7 +4,7 @@ import { BaseRegistry } from "./base-registry";
export interface KubeObjectStatusRegistration {
kind: string;
apiVersions: string[];
resolve: (object: KubeObject) => KubeObjectStatus;
resolve<Spec, Status>(object: KubeObject<Spec, Status>): KubeObjectStatus;
}
export class KubeObjectStatusRegistry extends BaseRegistry<KubeObjectStatusRegistration> {

View File

@ -6,13 +6,13 @@ import { action } from "mobx";
import { BaseRegistry } from "./base-registry";
import { LensExtension } from "../lens-extension";
export interface PageMenuRegistration {
target?: PageTarget;
export interface PageMenuRegistration<V> {
target?: PageTarget<V>;
title: React.ReactNode;
components: PageMenuComponents;
}
export interface ClusterPageMenuRegistration extends PageMenuRegistration {
export interface ClusterPageMenuRegistration<V> extends PageMenuRegistration<V> {
id?: string;
parentId?: string;
}
@ -21,7 +21,7 @@ export interface PageMenuComponents {
Icon: React.ComponentType<IconProps>;
}
export class PageMenuRegistry<T extends PageMenuRegistration> extends BaseRegistry<T> {
export class PageMenuRegistry<V, T extends PageMenuRegistration<V>> extends BaseRegistry<T> {
@action
add(items: T[], ext: LensExtension) {
const normalizedItems = items.map(menuItem => {
@ -37,23 +37,25 @@ export class PageMenuRegistry<T extends PageMenuRegistration> extends BaseRegist
}
}
export class ClusterPageMenuRegistry extends PageMenuRegistry<ClusterPageMenuRegistration> {
export class ClusterPageMenuRegistry extends PageMenuRegistry<any, ClusterPageMenuRegistration<any>> {
getRootItems() {
return this.getItems().filter((item) => !item.parentId);
}
getSubItems(parent: ClusterPageMenuRegistration) {
return this.getItems().filter((item) => (
item.parentId === parent.id &&
item.target.extensionId === parent.target.extensionId
));
getSubItems<V>(parent: ClusterPageMenuRegistration<V>) {
return this.getItems()
.filter(item => (
item.parentId === parent.id
&& item.target?.extensionId === parent.target?.extensionId
));
}
getByPage({ id: pageId, extensionId }: RegisteredPage) {
return this.getItems().find((item) => (
item.target.pageId == pageId &&
item.target.extensionId === extensionId
));
getByPage<V>({ id: pageId, extensionId }: RegisteredPage<V>) {
return this.getItems()
.find((item) => (
item.target?.pageId == pageId
&& item.target.extensionId === extensionId
));
}
}

View File

@ -6,54 +6,55 @@ import { BaseRegistry } from "./base-registry";
import { LensExtension, sanitizeExtensionName } from "../lens-extension";
import { PageParam, PageParamInit } from "../../renderer/navigation/page-param";
import { createPageParam } from "../../renderer/navigation/helpers";
import { NotFalsy } from "../../common/utils";
export interface PageRegistration {
export interface PageRegistration<V> {
/**
* Page ID, part of extension's page url, must be unique within same extension
* When not provided, first registered page without "id" would be used for page-menus without target.pageId for same extension
*/
id?: string;
params?: PageParams<string | ExtensionPageParamInit>;
params?: PageParams<string | ExtensionPageParamInit<V>>;
components: PageComponents;
}
// exclude "name" field since provided as key in page.params
export type ExtensionPageParamInit = Omit<PageParamInit, "name" | "isSystem">;
export type ExtensionPageParamInit<V> = Omit<PageParamInit<V>, "name" | "isSystem">;
export interface PageComponents {
Page: React.ComponentType<any>;
}
export interface PageTarget<P = PageParams> {
export interface PageTarget<V, P = PageParams<V>> {
extensionId?: string;
pageId?: string;
params?: P;
}
export interface PageParams<V = any> {
[paramName: string]: V;
}
export type PageParams<V> = Record<string, V>;
export interface PageComponentProps<P extends PageParams = {}> {
export interface PageComponentProps<V, P extends PageParams<V> = {}> {
params?: {
[N in keyof P]: PageParam<P[N]>;
}
}
export interface RegisteredPage {
export interface RegisteredPage<V> {
id: string;
extensionId: string;
url: string; // registered extension's page URL (without page params)
params: PageParams<PageParam>; // normalized params
params: PageParams<PageParam<V>>; // normalized params
components: PageComponents; // normalized components
}
export function getExtensionPageUrl(target: PageTarget): string {
const { extensionId, pageId = "", params: targetParams = {} } = target;
export function getExtensionPageUrl<V>(target: PageTarget<V>): string {
const { extensionId = "", pageId = "", params: targetParams = {} } = target;
const pagePath = ["/extension", sanitizeExtensionName(extensionId), pageId]
.filter(Boolean)
.join("/").replace(/\/+/g, "/").replace(/\/$/, ""); // normalize multi-slashes (e.g. coming from page.id)
.filter(NotFalsy)
.join("/")
.replace(/\/+/g, "/")
.replace(/\/$/, ""); // normalize multi-slashes (e.g. coming from page.id)
const pageUrl = new URL(pagePath, `http://localhost`);
@ -75,9 +76,9 @@ export function getExtensionPageUrl(target: PageTarget): string {
return pageUrl.href.replace(pageUrl.origin, "");
}
export class PageRegistry extends BaseRegistry<PageRegistration, RegisteredPage> {
protected getRegisteredItem(page: PageRegistration, ext: LensExtension): RegisteredPage {
const { id: pageId } = page;
export class PageRegistry extends BaseRegistry<PageRegistration<any>, RegisteredPage<any>> {
protected getRegisteredItem<V>(page: PageRegistration<V>, ext: LensExtension): RegisteredPage<V> {
const { id: pageId = "" } = page;
const extensionId = ext.name;
const params = this.normalizeParams(page.params);
const components = this.normalizeComponents(page.components, params);
@ -88,7 +89,7 @@ export class PageRegistry extends BaseRegistry<PageRegistration, RegisteredPage>
};
}
protected normalizeComponents(components: PageComponents, params?: PageParams<PageParam>): PageComponents {
protected normalizeComponents<V>(components: PageComponents, params?: PageParams<PageParam<V>>): PageComponents {
if (params) {
const { Page } = components;
@ -98,22 +99,21 @@ export class PageRegistry extends BaseRegistry<PageRegistration, RegisteredPage>
return components;
}
protected normalizeParams(params?: PageParams<string | ExtensionPageParamInit>): PageParams<PageParam> {
if (!params) {
return;
}
Object.entries(params).forEach(([name, value]) => {
const paramInit: PageParamInit = typeof value === "object"
? { name, ...value }
: { name, defaultValue: value };
params[paramInit.name] = createPageParam(paramInit);
});
return params as PageParams<PageParam>;
protected normalizeParams<V>(params: PageParams<string | ExtensionPageParamInit<V>> = {}): PageParams<PageParam<V>> {
return Object.fromEntries(
Object.entries(params)
.map(([name, value]) => [
name,
createPageParam<any>(
typeof value === "object"
? { name, ...value }
: { name, defaultValue: value }
)
])
);
}
getByPageTarget(target: PageTarget): RegisteredPage | null {
getByPageTarget<V>(target: PageTarget<V>): RegisteredPage<V> | null {
return this.getItems().find(page => page.extensionId === target.extensionId && page.id === target.pageId) || null;
}
}

View File

@ -15,7 +15,7 @@ export interface RouteParams {
/**
* the parts of the URI query string
*/
search: Record<string, string>;
search: Record<string, string | undefined>;
/**
* the matching parts of the path. The dynamic parts of the URI path.

View File

@ -74,7 +74,7 @@ export const startUpdateChecking = once(function (interval = 1000 * 60 * 60 * 24
broadcastMessage(UpdateAvailableChannel, backchannel, info);
} catch (error) {
logger.error(`${AutoUpdateLogPrefix}: broadcasting failed`, { error });
installVersion = undefined;
installVersion = null;
}
});

View File

@ -1,34 +1,19 @@
import request, { RequestPromiseOptions } from "request-promise-native";
import { Cluster } from "../cluster";
import { RequestPromiseOptions } from "request-promise-native";
import { Cluster, k8sRequest } from "../cluster";
export type ClusterDetectionResult = {
value: string | number | boolean
value?: string | number | boolean
accuracy: number
};
export class BaseClusterDetector {
cluster: Cluster;
key: string;
export abstract class BaseClusterDetector {
abstract key: string;
constructor(cluster: Cluster) {
this.cluster = cluster;
}
constructor(public cluster: Cluster) {}
detect(): Promise<ClusterDetectionResult> {
return null;
}
abstract detect(): Promise<ClusterDetectionResult | null>;
protected async k8sRequest<T = any>(path: string, options: RequestPromiseOptions = {}): Promise<T> {
const apiUrl = this.cluster.kubeProxyUrl + path;
return request(apiUrl, {
json: true,
timeout: 30000,
...options,
headers: {
Host: `${this.cluster.id}.${new URL(this.cluster.kubeProxyUrl).host}`, // required in ClusterManager.getClusterForRequest()
...(options.headers || {}),
},
});
return this.cluster[k8sRequest](path, options);
}
}

View File

@ -1,6 +1,7 @@
import { BaseClusterDetector } from "./base-cluster-detector";
import { createHash } from "crypto";
import { ClusterMetadataKey } from "../cluster";
import { assert, NotFalsy } from "../../common/utils";
export class ClusterIdDetector extends BaseClusterDetector {
key = ClusterMetadataKey.CLUSTER_ID;
@ -11,7 +12,7 @@ export class ClusterIdDetector extends BaseClusterDetector {
try {
id = await this.getDefaultNamespaceId();
} catch(_) {
id = this.cluster.apiUrl;
id = assert(this.cluster.apiUrl, "ClusterIdDetector can only detect for valid Cluster instances");
}
const value = createHash("sha256").update(id).digest("hex");

View File

@ -1,5 +1,6 @@
import { observable } from "mobx";
import { ClusterMetadata } from "../../common/cluster-store";
import { NotFalsy } from "../../common/utils";
import { Cluster } from "../cluster";
import { BaseClusterDetector, ClusterDetectionResult } from "./base-cluster-detector";
import { ClusterIdDetector } from "./cluster-id-detector";
@ -8,10 +9,12 @@ import { LastSeenDetector } from "./last-seen-detector";
import { NodesCountDetector } from "./nodes-count-detector";
import { VersionDetector } from "./version-detector";
export class DetectorRegistry {
registry = observable.array<typeof BaseClusterDetector>([], { deep: false });
type DerivedConstructor = new (cluster: Cluster) => BaseClusterDetector;
add(detectorClass: typeof BaseClusterDetector) {
export class DetectorRegistry {
registry = observable.array<DerivedConstructor>([], { deep: false });
add(detectorClass: DerivedConstructor) {
this.registry.push(detectorClass);
}
@ -33,13 +36,11 @@ export class DetectorRegistry {
// detector raised error, do nothing
}
}
const metadata: ClusterMetadata = {};
for (const [key, result] of Object.entries(results)) {
metadata[key] = result.value;
}
return metadata;
return Object.fromEntries(
Object.entries(results)
.filter(([, result]) => NotFalsy(result))
);
}
}

View File

@ -1,30 +1,77 @@
import { BaseClusterDetector } from "./base-cluster-detector";
import { ClusterMetadataKey } from "../cluster";
function isGKE(version: string) {
return version.includes("gke");
}
function isEKS(version: string) {
return version.includes("eks");
}
function isIKS(version: string) {
return version.includes("IKS");
}
function isMirantis(version: string) {
return version.includes("-mirantis-") || version.includes("-docker-");
}
function isTke(version: string) {
return version.includes("-tke.");
}
function isCustom(version: string) {
return version.includes("+");
}
function isVMWare(version: string) {
return version.includes("+vmware");
}
function isRke(version: string) {
return version.includes("-rancher");
}
function isK3s(version: string) {
return version.includes("+k3s");
}
function isK0s(version: string) {
return version.includes("-k0s");
}
function isAlibaba(version: string) {
return version.includes("-aliyun");
}
function isHuawei(version: string) {
return version.includes("-CCE");
}
export class DistributionDetector extends BaseClusterDetector {
key = ClusterMetadataKey.DISTRIBUTION;
version: string;
public async detect() {
this.version = await this.getKubernetesVersion();
const version = await this.getKubernetesVersion();
if (this.isRke()) {
if (isRke(version)) {
return { value: "rke", accuracy: 80};
}
if (this.isK3s()) {
if (isK3s(version)) {
return { value: "k3s", accuracy: 80};
}
if (this.isGKE()) {
if (isGKE(version)) {
return { value: "gke", accuracy: 80};
}
if (this.isEKS()) {
if (isEKS(version)) {
return { value: "eks", accuracy: 80};
}
if (this.isIKS()) {
if (isIKS(version)) {
return { value: "iks", accuracy: 80};
}
@ -36,27 +83,27 @@ export class DistributionDetector extends BaseClusterDetector {
return { value: "digitalocean", accuracy: 90};
}
if (this.isK0s()) {
if (isK0s(version)) {
return { value: "k0s", accuracy: 80};
}
if (this.isVMWare()) {
if (isVMWare(version)) {
return { value: "vmware", accuracy: 90};
}
if (this.isMirantis()) {
if (isMirantis(version)) {
return { value: "mirantis", accuracy: 90};
}
if (this.isAlibaba()) {
if (isAlibaba(version)) {
return { value: "alibaba", accuracy: 90};
}
if (this.isHuawei()) {
if (isHuawei(version)) {
return { value: "huawei", accuracy: 90};
}
if (this.isTke()) {
if (isTke(version)) {
return { value: "tencent", accuracy: 90};
}
@ -76,12 +123,12 @@ export class DistributionDetector extends BaseClusterDetector {
return { value: "docker-desktop", accuracy: 80};
}
if (this.isCustom() && await this.isOpenshift()) {
return { value: "openshift", accuracy: 90};
}
if (isCustom(version)) {
if (await this.isOpenshift()) {
return { value: "openshift", accuracy: 90 };
}
if (this.isCustom()) {
return { value: "custom", accuracy: 10};
return { value: "custom", accuracy: 10 };
}
return { value: "unknown", accuracy: 10};
@ -95,28 +142,12 @@ export class DistributionDetector extends BaseClusterDetector {
return response.gitVersion;
}
protected isGKE() {
return this.version.includes("gke");
}
protected isEKS() {
return this.version.includes("eks");
}
protected isIKS() {
return this.version.includes("IKS");
}
protected isAKS() {
return this.cluster.apiUrl.includes("azmk8s.io");
}
protected isMirantis() {
return this.version.includes("-mirantis-") || this.version.includes("-docker-");
return this.cluster.apiUrl?.includes("azmk8s.io");
}
protected isDigitalOcean() {
return this.cluster.apiUrl.endsWith("k8s.ondigitalocean.com");
return this.cluster.apiUrl?.endsWith("k8s.ondigitalocean.com");
}
protected isMinikube() {
@ -135,38 +166,6 @@ export class DistributionDetector extends BaseClusterDetector {
return this.cluster.contextName === "docker-desktop";
}
protected isTke() {
return this.version.includes("-tke.");
}
protected isCustom() {
return this.version.includes("+");
}
protected isVMWare() {
return this.version.includes("+vmware");
}
protected isRke() {
return this.version.includes("-rancher");
}
protected isK3s() {
return this.version.includes("+k3s");
}
protected isK0s() {
return this.version.includes("-k0s");
}
protected isAlibaba() {
return this.version.includes("-aliyun");
}
protected isHuawei() {
return this.version.includes("-CCE");
}
protected async isOpenshift() {
try {
const response = await this.k8sRequest("");

View File

@ -5,7 +5,9 @@ export class LastSeenDetector extends BaseClusterDetector {
key = ClusterMetadataKey.LAST_SEEN;
public async detect() {
if (!this.cluster.accessible) return null;
if (!this.cluster.accessible) {
return null;
}
await this.k8sRequest("/version");

View File

@ -5,7 +5,10 @@ export class NodesCountDetector extends BaseClusterDetector {
key = ClusterMetadataKey.NODES_COUNT;
public async detect() {
if (!this.cluster.accessible) return null;
if (!this.cluster.accessible) {
return null;
}
const nodeCount = await this.getNodeCount();
return { value: nodeCount, accuracy: 100};

View File

@ -3,7 +3,6 @@ import { ClusterMetadataKey } from "../cluster";
export class VersionDetector extends BaseClusterDetector {
key = ClusterMetadataKey.VERSION;
value: string;
public async detect() {
const version = await this.getKubernetesVersion();

View File

@ -169,23 +169,23 @@ export class ClusterManager extends Singleton {
});
}
getClusterForRequest(req: http.IncomingMessage): Cluster {
let cluster: Cluster = null;
getClusterForRequest(req: http.IncomingMessage): Cluster | null {
let cluster: Cluster | null = null;
// lens-server is connecting to 127.0.0.1:<port>/<uid>
if (req.headers.host.startsWith("127.0.0.1")) {
const clusterId = req.url.split("/")[1];
if (req.headers.host?.startsWith("127.0.0.1")) {
const clusterId = req.url?.split("/")[1];
cluster = clusterStore.getById(clusterId);
if (cluster) {
// we need to swap path prefix so that request is proxied to kube api
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix);
req.url = req.url?.replace(`/${clusterId}`, apiKubePrefix);
}
} else if (req.headers["x-cluster-id"]) {
cluster = clusterStore.getById(req.headers["x-cluster-id"].toString());
} else {
const clusterId = getClusterIdFromHost(req.headers.host);
const clusterId = getClusterIdFromHost(req.headers.host ?? "");
cluster = clusterStore.getById(clusterId);
}

View File

@ -15,6 +15,9 @@ import logger from "./logger";
import { VersionDetector } from "./cluster-detectors/version-detector";
import { detectorRegistry } from "./cluster-detectors/detector-registry";
import plimit from "p-limit";
import { assert, NotFalsy } from "../common/utils";
export const k8sRequest = Symbol("k8sRequest");
export enum ClusterStatus {
AccessGranted = 2,
@ -38,12 +41,12 @@ export type ClusterRefreshOptions = {
export interface ClusterState {
initialized: boolean;
enabled: boolean;
apiUrl: string;
apiUrl?: string;
online: boolean;
disconnected: boolean;
accessible: boolean;
ready: boolean;
failureReason: string;
failureReason?: string;
isAdmin: boolean;
allowedNamespaces: string[]
allowedResources: string[]
@ -63,20 +66,20 @@ export class Cluster implements ClusterModel, ClusterState {
*
* @internal
*/
public kubeCtl: Kubectl;
public kubeCtl?: Kubectl;
/**
* Context handler
*
* @internal
*/
public contextHandler: ContextHandler;
public contextHandler?: ContextHandler;
/**
* Owner reference
*
* If extension sets this it needs to also mark cluster as enabled on activate (or when added to a store)
*/
public ownerRef: string;
protected kubeconfigManager: KubeconfigManager;
public ownerRef?: string;
protected kubeconfigManager?: KubeconfigManager;
protected eventDisposers: Function[] = [];
protected activated = false;
private resourceAccessStatuses: Map<KubeApiResource, boolean> = new Map();
@ -85,7 +88,7 @@ export class Cluster implements ClusterModel, ClusterState {
whenReady = when(() => this.ready);
/**
* Is cluster object initializinng on-going
* Is cluster object initializing on-going
*
* @observable
*/
@ -118,14 +121,14 @@ export class Cluster implements ClusterModel, ClusterState {
*
* @observable
*/
@observable apiUrl: string; // cluster server url
@observable apiUrl?: string; // cluster server url
/**
* Internal authentication proxy URL
*
* @observable
* @internal
*/
@observable kubeProxyUrl: string; // lens-proxy to kube-api url
@observable kubeProxyUrl?: string; // lens-proxy to kube-api url
/**
* Is cluster instance enabled (disabled clusters are currently hidden)
*
@ -167,7 +170,7 @@ export class Cluster implements ClusterModel, ClusterState {
*
* @observable
*/
@observable failureReason: string;
@observable failureReason?: string;
/**
* Does user have admin like access
*
@ -186,7 +189,7 @@ export class Cluster implements ClusterModel, ClusterState {
*
* @observable
*/
@observable preferences: ClusterPreferences = {};
@observable preferences: ClusterPreferences;
/**
* Metadata
*
@ -211,7 +214,7 @@ export class Cluster implements ClusterModel, ClusterState {
*
* @observable
*/
@observable accessibleNamespaces: string[] = [];
@observable accessibleNamespaces?: string[];
/**
* Is cluster available
@ -253,13 +256,24 @@ export class Cluster implements ClusterModel, ClusterState {
}
constructor(model: ClusterModel) {
this.updateModel(model);
this.id = model.id;
this.kubeConfigPath = model.kubeConfigPath;
this.workspace = model.workspace || "Default";
this.contextName = model.contextName;
this.preferences = model.preferences ?? {};
this.metadata = model.metadata ?? {};
this.ownerRef = model.ownerRef;
this.accessibleNamespaces = model.accessibleNamespaces;
try {
const kubeconfig = this.getKubeconfig();
validateKubeConfig(kubeconfig, this.contextName, { validateCluster: true, validateUser: false, validateExec: false});
this.apiUrl = kubeconfig.getCluster(kubeconfig.getContextObject(this.contextName).cluster).server;
const cluster = kubeconfig.getContextObject(this.contextName)?.cluster;
if (cluster) {
this.apiUrl = kubeconfig.getCluster(cluster)?.server;
}
} catch(err) {
logger.error(err);
logger.error(`[CLUSTER] Failed to load kubeconfig for the cluster '${this.name || this.contextName}' (context: ${this.contextName}, kubeconfig: ${this.kubeConfigPath}).`);
@ -274,15 +288,6 @@ export class Cluster implements ClusterModel, ClusterState {
return !!this.ownerRef;
}
/**
* Update cluster data model
*
* @param model
*/
@action updateModel(model: ClusterModel) {
Object.assign(this, model);
}
/**
* Initialize a cluster (can be done only in main process)
*
@ -323,7 +328,7 @@ export class Cluster implements ClusterModel, ClusterState {
if (ipcMain) {
this.eventDisposers.push(
reaction(() => this.getState(), () => this.pushState()),
reaction(() => this.prometheusPreferences, (prefs) => this.contextHandler.setupPrometheus(prefs), { equals: comparer.structural, }),
reaction(() => this.prometheusPreferences, (prefs) => this.contextHandler?.setupPrometheus(prefs), { equals: comparer.structural, }),
() => {
clearInterval(refreshTimer);
clearInterval(refreshMetadataTimer);
@ -488,15 +493,17 @@ export class Cluster implements ClusterModel, ClusterState {
/**
* @internal
*/
async getProxyKubeconfigPath(): Promise<string> {
return this.kubeconfigManager.getPath();
async getProxyKubeconfigPath(): Promise<string | undefined> {
return this.kubeconfigManager?.getPath();
}
protected async k8sRequest<T = any>(path: string, options: RequestPromiseOptions = {}): Promise<T> {
async [k8sRequest]<T = any>(path: string, options: RequestPromiseOptions = {}): Promise<T> {
const baseUrl = assert(this.kubeProxyUrl, "constructor failed, should not be accessing k8s");
options.headers ??= {};
options.json ??= true;
options.timeout ??= 30000;
options.headers.Host = `${this.id}.${new URL(this.kubeProxyUrl).host}`; // required in ClusterManager.getClusterForRequest()
options.headers.Host = `${this.id}.${new URL(baseUrl).host}`; // required in ClusterManager.getClusterForRequest()
return request(this.kubeProxyUrl + path, options);
}
@ -511,7 +518,7 @@ export class Cluster implements ClusterModel, ClusterState {
const prometheusPrefix = this.preferences.prometheus?.prefix || "";
const metricsPath = `/api/v1/namespaces/${prometheusPath}/proxy${prometheusPrefix}/api/v1/query_range`;
return this.k8sRequest(metricsPath, {
return this[k8sRequest](metricsPath, {
timeout: 0,
resolveWithFullResponse: false,
json: true,
@ -526,8 +533,7 @@ export class Cluster implements ClusterModel, ClusterState {
const versionData = await versionDetector.detect();
this.metadata.version = versionData.value;
this.failureReason = null;
this.failureReason = undefined;
return ClusterStatus.AccessGranted;
} catch (error) {
@ -574,7 +580,7 @@ export class Cluster implements ClusterModel, ClusterState {
spec: { resourceAttributes }
});
return accessReview.body.status.allowed;
return accessReview.body.status?.allowed ?? false;
} catch (error) {
logger.error(`failed to request selfSubjectAccessReview: ${error}`);
@ -676,7 +682,7 @@ export class Cluster implements ClusterModel, ClusterState {
}
protected async getAllowedNamespaces() {
if (this.accessibleNamespaces.length) {
if (this.accessibleNamespaces?.length) {
return this.accessibleNamespaces;
}
@ -685,10 +691,10 @@ export class Cluster implements ClusterModel, ClusterState {
try {
const namespaceList = await api.listNamespace();
return namespaceList.body.items.map(ns => ns.metadata.name);
return namespaceList.body.items.map(ns => ns.metadata?.name).filter(NotFalsy);
} catch (error) {
const ctx = (await this.getProxyKubeconfig()).getContextObject(this.contextName);
const namespaceList = [ctx.namespace].filter(Boolean);
const namespaceList = [ctx?.namespace].filter(NotFalsy);
if (namespaceList.length === 0 && error instanceof HttpError && error.statusCode === 403) {
logger.info("[CLUSTER]: listing namespaces is forbidden, broadcasting", { clusterId: this.id });

View File

@ -1,4 +1,4 @@
import type { PrometheusProvider, PrometheusService } from "./prometheus/provider-registry";
import type { PrometheusService } from "./prometheus/provider-registry";
import type { ClusterPrometheusPreferences } from "../common/cluster-store";
import type { Cluster } from "./cluster";
import type httpProxy from "http-proxy";
@ -8,35 +8,54 @@ import { prometheusProviders } from "../common/prometheus-providers";
import logger from "./logger";
import { getFreePort } from "./port";
import { KubeAuthProxy } from "./kube-auth-proxy";
import { assert, NotFalsy } from "../common/utils";
import { AssertionError } from "assert";
interface VerifiedUrl extends UrlWithStringQuery {
hostname: string;
}
export class ContextHandler {
public proxyPort: number;
public clusterUrl: UrlWithStringQuery;
protected kubeAuthProxy: KubeAuthProxy;
protected apiTarget: httpProxy.ServerOptions;
protected prometheusProvider: string;
protected prometheusPath: string;
public proxyPort?: number;
public clusterUrl: VerifiedUrl;
protected kubeAuthProxy?: KubeAuthProxy;
protected apiTarget?: httpProxy.ServerOptions;
protected prometheusProvider?: string;
protected prometheusPath?: string;
constructor(protected cluster: Cluster) {
this.clusterUrl = url.parse(cluster.apiUrl);
const apiUrl = assert(cluster.apiUrl, "ContextHandler may only be created for valid clusters");
const clusterUrl = url.parse(apiUrl);
if (!clusterUrl.hostname) {
throw new AssertionError({
actual: clusterUrl.hostname,
message: "clusterUrl must have a hostname"
});
}
this.clusterUrl = clusterUrl as VerifiedUrl;
this.setupPrometheus(cluster.preferences);
}
public setupPrometheus(preferences: ClusterPrometheusPreferences = {}) {
this.prometheusProvider = preferences.prometheusProvider?.type;
this.prometheusPath = null;
if (preferences.prometheus) {
const { namespace, service, port } = preferences.prometheus;
this.prometheusPath = `${namespace}/services/${service}:${port}`;
} else {
this.prometheusPath = undefined;
}
}
protected async resolvePrometheusPath(): Promise<string> {
protected async resolvePrometheusPath(): Promise<string | undefined> {
const prometheusService = await this.getPrometheusService();
if (!prometheusService) return null;
if (!prometheusService) return;
const { service, namespace, port } = prometheusService;
return `${namespace}/services/${service}:${port}`;
@ -56,24 +75,26 @@ export class ContextHandler {
return prometheusProviders.find(p => p.id === this.prometheusProvider);
}
async getPrometheusService(): Promise<PrometheusService> {
async getPrometheusService(): Promise<PrometheusService | undefined> {
const providers = this.prometheusProvider ? prometheusProviders.filter(provider => provider.id == this.prometheusProvider) : prometheusProviders;
const prometheusPromises: Promise<PrometheusService>[] = providers.map(async (provider: PrometheusProvider): Promise<PrometheusService> => {
const apiClient = (await this.cluster.getProxyKubeconfig()).makeApiClient(CoreV1Api);
return await provider.getPrometheusService(apiClient);
});
const resolvedPrometheusServices = await Promise.all(prometheusPromises);
return resolvedPrometheusServices.filter(n => n)[0];
return (await Promise.allSettled(providers
.map(provider => (
this.cluster.getProxyKubeconfig()
.then(kc => kc.makeApiClient(CoreV1Api))
.then(client => provider.getPrometheusService(client))
))
))
.map(result => (
result.status === "fulfilled"
? result.value
: undefined
))
.find(NotFalsy);
}
async getPrometheusPath(): Promise<string> {
if (!this.prometheusPath) {
this.prometheusPath = await this.resolvePrometheusPath();
}
return this.prometheusPath;
async getPrometheusPath(): Promise<string | undefined> {
return this.prometheusPath ??= await this.resolvePrometheusPath();
}
async resolveAuthProxyUrl() {
@ -111,11 +132,7 @@ export class ContextHandler {
}
async ensurePort(): Promise<number> {
if (!this.proxyPort) {
this.proxyPort = await getFreePort();
}
return this.proxyPort;
return this.proxyPort ??= await getFreePort();
}
async ensureServer() {
@ -126,7 +143,7 @@ export class ContextHandler {
if (this.cluster.preferences.httpsProxy) {
proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy;
}
this.kubeAuthProxy = new KubeAuthProxy(this.cluster, this.proxyPort, proxyEnv);
this.kubeAuthProxy = new KubeAuthProxy(this.cluster, await this.ensurePort(), proxyEnv);
await this.kubeAuthProxy.run();
}
}
@ -134,7 +151,7 @@ export class ContextHandler {
stopServer() {
if (this.kubeAuthProxy) {
this.kubeAuthProxy.exit();
this.kubeAuthProxy = null;
this.kubeAuthProxy = undefined;
}
}

View File

@ -36,7 +36,8 @@ export class FilesystemProvisionerStore extends BaseStore<FSProvisionModel> {
this.registeredExtensions.set(extensionName, dirPath);
}
const dirPath = this.registeredExtensions.get(extensionName);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const dirPath = this.registeredExtensions.get(extensionName)!;
await fse.ensureDir(dirPath);

View File

@ -19,10 +19,12 @@ export class HelmChartManager {
this.repo = repo;
}
public async chart(name: string) {
public async chart(name?: string) {
const charts = await this.charts();
return charts[name];
if (name) {
return charts[name];
}
}
public async charts(): Promise<RepoHelmChartList> {
@ -37,7 +39,7 @@ export class HelmChartManager {
}
}
public async getReadme(name: string, version = "") {
public async getReadme(name: string, version: string) {
const helm = await helmCli.binaryPath();
if(version && version != "") {
@ -51,7 +53,7 @@ export class HelmChartManager {
}
}
public async getValues(name: string, version = "") {
public async getValues(name: string, version: string) {
const helm = await helmCli.binaryPath();
if(version && version != "") {

View File

@ -5,6 +5,7 @@ import { promiseExec} from "../promise-exec";
import { helmCli } from "./helm-cli";
import { Cluster } from "../cluster";
import { toCamelCase } from "../../common/utils/camelCase";
import { assert, NotFalsy } from "../../common/utils";
export class HelmReleaseManager {
@ -114,7 +115,8 @@ export class HelmReleaseManager {
protected async getResources(name: string, namespace: string, cluster: Cluster) {
const helm = await helmCli.binaryPath();
const kubectl = await cluster.kubeCtl.getPath();
const kubectl = assert(await cluster.kubeCtl?.getPath(), "Cluster Kubectl must be instantiated");
const pathToKubeconfig = await cluster.getProxyKubeconfigPath();
const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectl}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`).catch(() => {
return { stdout: JSON.stringify({items: []})};

View File

@ -6,10 +6,11 @@ import { Singleton } from "../../common/utils/singleton";
import { customRequestPromise } from "../../common/request";
import orderBy from "lodash/orderBy";
import logger from "../logger";
import { AssertionError } from "assert";
export type HelmEnv = Record<string, string> & {
HELM_REPOSITORY_CACHE?: string;
HELM_REPOSITORY_CONFIG?: string;
HELM_REPOSITORY_CACHE: string;
HELM_REPOSITORY_CONFIG: string;
};
export interface HelmRepoConfig {
@ -19,7 +20,7 @@ export interface HelmRepoConfig {
export interface HelmRepo {
name: string;
url: string;
cacheFilePath?: string
cacheFilePath: string
caFile?: string,
certFile?: string,
insecureSkipTlsVerify?: boolean,
@ -31,9 +32,8 @@ export interface HelmRepo {
export class HelmRepoManager extends Singleton {
static cache = {}; // todo: remove implicit updates in helm-chart-manager.ts
protected repos: HelmRepo[];
protected helmEnv: HelmEnv;
protected initialized: boolean;
protected repos: HelmRepo[] = [];
protected helmEnv?: HelmEnv;
async loadAvailableRepos(): Promise<HelmRepo[]> {
const res = await customRequestPromise({
@ -46,43 +46,46 @@ export class HelmRepoManager extends Singleton {
return orderBy<HelmRepo>(res.body, repo => repo.name);
}
async init() {
async init(): Promise<HelmEnv> {
helmCli.setLogger(logger);
await helmCli.ensureBinary();
if (!this.initialized) {
this.helmEnv = await this.parseHelmEnv();
try {
return this.helmEnv ?? await this.parseHelmEnv();
} finally {
await this.update();
this.initialized = true;
}
}
protected async parseHelmEnv() {
protected async parseHelmEnv(): Promise<HelmEnv> {
const helm = await helmCli.binaryPath();
const { stdout } = await promiseExec(`"${helm}" env`).catch((error) => {
throw(error.stderr);
});
const lines = stdout.split(/\r?\n/); // split by new line feed
const env: HelmEnv = {};
lines.forEach((line: string) => {
const [key, value] = line.split("=");
try {
const { stdout } = await promiseExec(`"${helm}" env`);
const envEntries = stdout.split(/\r?\n/) // split by new line feed
.map(line => line.split("="))
.filter(line => line.length === 2)
.map(([key, value]) => [key, value.replace(/"/g, "")]); // strip quotas
const env = Object.fromEntries(envEntries);
if (key && value) {
env[key] = value.replace(/"/g, ""); // strip quotas
if (!env.HELM_REPOSITORY_CACHE || !env.HELM_REPOSITORY_CONFIG) {
throw new AssertionError({
actual: env,
message: "HELM_REPOSITORY_CACHE and HELM_REPOSITORY_CONFIG must be defined"
});
}
});
return env;
return env as HelmEnv;
} catch (error) {
throw error.stderr;
}
}
public async repositories(): Promise<HelmRepo[]> {
if (!this.initialized) {
await this.init();
}
const helmEnv = await this.init();
try {
const repoConfigFile = this.helmEnv.HELM_REPOSITORY_CONFIG;
const repoConfigFile = helmEnv.HELM_REPOSITORY_CONFIG;
const { repositories }: HelmRepoConfig = await readFile(repoConfigFile, "utf8")
.then((yamlContent: string) => yaml.safeLoad(yamlContent))
.catch(() => ({
@ -97,7 +100,7 @@ export class HelmRepoManager extends Singleton {
return repositories.map(repo => ({
...repo,
cacheFilePath: `${this.helmEnv.HELM_REPOSITORY_CACHE}/${repo.name}-index.yaml`
cacheFilePath: `${helmEnv.HELM_REPOSITORY_CACHE}/${repo.name}-index.yaml`
}));
} catch (error) {
logger.error(`[HELM]: repositories listing error "${error}"`);
@ -106,7 +109,7 @@ export class HelmRepoManager extends Singleton {
}
}
public async repository(name: string) {
public async repository(name?: string) {
const repositories = await this.repositories();
return repositories.find(repo => repo.name == name);
@ -114,21 +117,23 @@ export class HelmRepoManager extends Singleton {
public async update() {
const helm = await helmCli.binaryPath();
const { stdout } = await promiseExec(`"${helm}" repo update`).catch((error) => {
return { stdout: error.stdout };
});
return stdout;
try {
return (await promiseExec(`"${helm}" repo update`)).stdout;
} catch (error) {
return error.stdout;
}
}
public async addRepo({ name, url }: HelmRepo) {
public async addRepo({ name, url }: { name: string, url: string }) {
logger.info(`[HELM]: adding repo "${name}" from ${url}`);
const helm = await helmCli.binaryPath();
const { stdout } = await promiseExec(`"${helm}" repo add ${name} ${url}`).catch((error) => {
throw(error.stderr);
});
return stdout;
try {
return (await promiseExec(`"${helm}" repo add ${name} ${url}`)).stdout;
} catch (error) {
return error.stdout;
}
}
public async addСustomRepo(repoAttributes : HelmRepo) {
@ -143,21 +148,23 @@ export class HelmRepoManager extends Singleton {
const certFile = repoAttributes.certFile ? ` --cert-file "${repoAttributes.certFile}"` : "";
const addRepoCommand = `"${helm}" repo add ${repoAttributes.name} ${repoAttributes.url}${insecureSkipTlsVerify}${username}${password}${caFile}${keyFile}${certFile}`;
const { stdout } = await promiseExec(addRepoCommand).catch((error) => {
throw(error.stderr);
});
return stdout;
try {
return (await promiseExec(addRepoCommand)).stdout;
} catch (error) {
return error.stdout;
}
}
public async removeRepo({ name, url }: HelmRepo): Promise<string> {
logger.info(`[HELM]: removing repo "${name}" from ${url}`);
const helm = await helmCli.binaryPath();
const { stdout } = await promiseExec(`"${helm}" repo remove ${name}`).catch((error) => {
throw(error.stderr);
});
return stdout;
try {
return (await promiseExec(`"${helm}" repo remove ${name} ${url}`)).stdout;
} catch (error) {
return error.stdout;
}
}
}

View File

@ -10,6 +10,10 @@ class HelmService {
public async installChart(cluster: Cluster, data: { chart: string; values: {}; name: string; namespace: string; version: string }) {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
if (!proxyKubeconfig) {
return;
}
return await releaseManager.installChart(data.chart, data.values, data.name, data.namespace, data.version, proxyKubeconfig);
}
@ -31,74 +35,116 @@ class HelmService {
return charts;
}
public async getChart(repoName: string, chartName: string, version = "") {
public async getChart(repoName?: string, chartName?: string, version?: string | null) {
const result = {
readme: "",
versions: {}
};
const repo = await repoManager.repository(repoName);
if (!repo || !chartName) {
return void logger.warn("[HELM-SERVICE]: Missing required information on getChart request", { repo, chartName });
}
const chartManager = new HelmChartManager(repo);
const chart = await chartManager.chart(chartName);
result.readme = await chartManager.getReadme(chartName, version);
if (!chart) {
return void logger.warn("[HELM-SERVICE]: Missing required information on getChart request", { chart });
}
result.readme = await chartManager.getReadme(chartName, version || "");
result.versions = chart;
return result;
}
public async getChartValues(repoName: string, chartName: string, version = "") {
public async getChartValues(repoName?: string, chartName?: string, version?: string | null) {
const repo = await repoManager.repository(repoName);
if (!repo || !chartName) {
return void logger.warn("[HELM-SERVICE]: Missing required information on getChartValues request", { repo, chartName });
}
const chartManager = new HelmChartManager(repo);
return chartManager.getValues(chartName, version);
return chartManager.getValues(chartName, version || "");
}
public async listReleases(cluster: Cluster, namespace: string = null) {
public async listReleases(cluster: Cluster, namespace?: string) {
await repoManager.init();
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
if (!proxyKubeconfig) {
return void logger.warn("[HELM-SERVICE]: Missing required information on listReleases request", { proxyKubeconfig });
}
return await releaseManager.listReleases(proxyKubeconfig, namespace);
}
public async getRelease(cluster: Cluster, releaseName: string, namespace: string) {
public async getRelease(cluster: Cluster, releaseName?: string, namespace?: string) {
if (!releaseName || !namespace) {
return void logger.warn("[HELM-SERVICE]: Missing required information on getRelease request", { releaseName, namespace });
}
logger.debug("Fetch release");
return await releaseManager.getRelease(releaseName, namespace, cluster);
}
public async getReleaseValues(cluster: Cluster, releaseName: string, namespace: string) {
public async getReleaseValues(cluster: Cluster, releaseName?: string, namespace?: string) {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
if (!proxyKubeconfig || !releaseName || !namespace) {
return void logger.warn("[HELM-SERVICE]: Missing required information on getReleaseValues request", { proxyKubeconfig, releaseName, namespace });
}
logger.debug("Fetch release values");
return await releaseManager.getValues(releaseName, namespace, proxyKubeconfig);
}
public async getReleaseHistory(cluster: Cluster, releaseName: string, namespace: string) {
public async getReleaseHistory(cluster: Cluster, releaseName?: string, namespace?: string) {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
if (!proxyKubeconfig || !releaseName || !namespace) {
return void logger.warn("[HELM-SERVICE]: Missing required information on getReleaseHistory request", { proxyKubeconfig, releaseName, namespace });
}
logger.debug("Fetch release history");
return await releaseManager.getHistory(releaseName, namespace, proxyKubeconfig);
}
public async deleteRelease(cluster: Cluster, releaseName: string, namespace: string) {
public async deleteRelease(cluster: Cluster, releaseName?: string, namespace?: string) {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
if (!proxyKubeconfig || !releaseName || !namespace) {
return void logger.warn("[HELM-SERVICE]: Missing required information on deleteRelease request", { proxyKubeconfig, releaseName, namespace });
}
logger.debug("Delete release");
return await releaseManager.deleteRelease(releaseName, namespace, proxyKubeconfig);
}
public async updateRelease(cluster: Cluster, releaseName: string, namespace: string, data: { chart: string; values: {}; version: string }) {
public async updateRelease(cluster: Cluster, releaseName?: string, namespace?: string, data?: { chart: string; values: {}; version: string }) {
if (!releaseName || !namespace || !data) {
return void logger.warn("[HELM-SERVICE]: Missing required information on updateRelease request", { releaseName, namespace, data });
}
logger.debug("Upgrade release");
return await releaseManager.upgradeRelease(releaseName, data.chart, data.values, namespace, data.version, cluster);
}
public async rollback(cluster: Cluster, releaseName: string, namespace: string, revision: number) {
public async rollback(cluster: Cluster, releaseName?: string, namespace?: string, revision?: number) {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
if (!proxyKubeconfig || !releaseName || !namespace || !revision) {
return void logger.warn("[HELM-SERVICE]: Missing required information on rollback request", { proxyKubeconfig, releaseName, namespace, revision });
}
logger.debug("Rollback release");
const output = await releaseManager.rollback(releaseName, namespace, revision, proxyKubeconfig);
@ -123,6 +169,10 @@ class HelmService {
const firstVersion = semver.coerce(first.version || 0);
const secondVersion = semver.coerce(second.version || 0);
if (!firstVersion || !secondVersion) {
return 0; // consider this case as equal
}
return semver.compare(secondVersion, firstVersion);
});
}

View File

@ -5,6 +5,7 @@ import type { Cluster } from "./cluster";
import { Kubectl } from "./kubectl";
import logger from "./logger";
import * as url from "url";
import { assert } from "../common/utils";
export interface KubeAuthProxyLog {
data: string;
@ -12,23 +13,24 @@ export interface KubeAuthProxyLog {
}
export class KubeAuthProxy {
public lastError: string;
public lastError?: string;
protected cluster: Cluster;
protected env: NodeJS.ProcessEnv = null;
protected proxyProcess: ChildProcess;
protected env: NodeJS.ProcessEnv;
protected proxyProcess?: ChildProcess;
protected port: number;
protected kubectl: Kubectl;
readonly acceptHosts: string;
constructor(cluster: Cluster, port: number, env: NodeJS.ProcessEnv) {
this.env = env;
this.port = port;
this.cluster = cluster;
this.kubectl = Kubectl.bundled();
}
get acceptHosts() {
return url.parse(this.cluster.apiUrl).hostname;
this.acceptHosts = assert(
this.cluster.apiUrl && url.parse(this.cluster.apiUrl).hostname,
"Cluster must be properly initialized to have a proxy created for it",
);
}
public async run(): Promise<void> {
@ -57,11 +59,11 @@ export class KubeAuthProxy {
});
this.proxyProcess.on("exit", (code) => {
this.sendIpcLogMessage({ data: `proxy exited with code: ${code}`, error: code > 0 });
this.sendIpcLogMessage({ data: `proxy exited with code: ${code}`, error: Boolean(code && code > 0) });
this.exit();
});
this.proxyProcess.stdout.on("data", (data) => {
this.proxyProcess.stdout?.on("data", (data) => {
let logItem = data.toString();
if (logItem.startsWith("Starting to serve on")) {
@ -70,7 +72,7 @@ export class KubeAuthProxy {
this.sendIpcLogMessage({ data: logItem });
});
this.proxyProcess.stderr.on("data", (data) => {
this.proxyProcess.stderr?.on("data", (data) => {
this.lastError = this.parseError(data.toString());
this.sendIpcLogMessage({ data: data.toString(), error: true });
});
@ -108,8 +110,8 @@ export class KubeAuthProxy {
logger.debug("[KUBE-AUTH]: stopping local proxy", this.cluster.getMeta());
this.proxyProcess.kill();
this.proxyProcess.removeAllListeners();
this.proxyProcess.stderr.removeAllListeners();
this.proxyProcess.stdout.removeAllListeners();
this.proxyProcess = null;
this.proxyProcess.stderr?.removeAllListeners();
this.proxyProcess.stdout?.removeAllListeners();
this.proxyProcess = undefined;
}
}

View File

@ -9,7 +9,7 @@ import logger from "./logger";
export class KubeconfigManager {
protected configDir = app.getPath("temp");
protected tempFile: string;
protected tempFile?: string;
private constructor(protected cluster: Cluster, protected contextHandler: ContextHandler, protected port: number) { }
@ -63,7 +63,7 @@ export class KubeconfigManager {
{
name: contextName,
server: this.resolveProxyUrl(),
skipTLSVerify: undefined,
skipTLSVerify: false,
}
],
users: [
@ -74,7 +74,7 @@ export class KubeconfigManager {
user: "proxy",
name: contextName,
cluster: contextName,
namespace: kubeConfig.getContextObject(contextName).namespace,
namespace: kubeConfig.getContextObject(contextName)?.namespace,
}
]
};

View File

@ -55,7 +55,7 @@ export function bundledKubectlPath(): string {
export class Kubectl {
public kubectlVersion: string;
protected directory: string;
protected directory?: string;
protected url: string;
protected path: string;
protected dirname: string;
@ -77,12 +77,17 @@ export class Kubectl {
constructor(clusterVersion: string) {
const versionParts = /^v?(\d+\.\d+)(.*)/.exec(clusterVersion);
const minorVersion = versionParts[1];
if (!versionParts || versionParts.length === 0) {
throw new Error("ClusterVersion must start of the form v#.#.#");
}
const prev = kubectlMap.get(versionParts[1]);
/* minorVersion is the first two digits of kube server version
if the version map includes that, use that version, if not, fallback to the exact x.y.z of kube version */
if (kubectlMap.has(minorVersion)) {
this.kubectlVersion = kubectlMap.get(minorVersion);
if (prev) {
this.kubectlVersion = prev;
logger.debug(`Set kubectl version ${this.kubectlVersion} for cluster version ${clusterVersion} using version map`);
} else {
this.kubectlVersion = versionParts[1] + versionParts[2];
@ -273,7 +278,7 @@ export class Kubectl {
logger.info(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`);
return new Promise((resolve, reject) => {
return new Promise<void>((resolve, reject) => {
const stream = customRequest({
url: this.url,
gzip: true,
@ -360,13 +365,12 @@ export class Kubectl {
await fsPromises.writeFile(zshScriptPath, zshScript.toString(), { mode: 0o644 });
}
protected getDownloadMirror() {
const mirror = packageMirrors.get(userStore.preferences?.downloadMirror);
if (mirror) {
return mirror;
}
return packageMirrors.get("default"); // MacOS packages are only available from default
protected getDownloadMirror(): string {
return (
userStore.preferences?.downloadMirror
&& packageMirrors.get(userStore.preferences?.downloadMirror)
)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
?? packageMirrors.get("default")!;
}
}

View File

@ -5,6 +5,7 @@ import { ensureDir, pathExists } from "fs-extra";
import * as tar from "tar";
import { isWindows } from "../common/vars";
import winston from "winston";
import { noop } from "../common/utils";
export type LensBinaryOpts = {
version: string;
@ -17,16 +18,16 @@ export type LensBinaryOpts = {
export class LensBinary {
public binaryVersion: string;
protected directory: string;
protected url: string;
protected path: string;
protected tarPath: string;
protected directory?: string;
protected url?: string;
protected path?: string;
protected tarPath?: string;
protected dirname: string;
protected binaryName: string;
protected platformName: string;
protected arch: string;
protected originalBinaryName: string;
protected requestOpts: request.Options;
protected requestOpts?: request.Options;
protected logger: Console | winston.Logger;
constructor(opts: LensBinaryOpts) {
@ -177,19 +178,21 @@ export class LensBinary {
stream.on("error", (error) => {
this.logger.error(error);
fs.unlink(binaryPath, () => {
// do nothing
});
throw(error);
fs.unlink(binaryPath, noop);
throw error;
});
return new Promise((resolve, reject) => {
return new Promise<void>((resolve, reject) => {
file.on("close", () => {
this.logger.debug(`${this.originalBinaryName} binary download closed`);
if (!this.tarPath) fs.chmod(binaryPath, 0o755, (err) => {
if (err) reject(err);
});
resolve();
if (!this.tarPath) {
fs.promises.chmod(binaryPath, 0o755)
.then(resolve)
.catch(reject);
} else {
resolve();
}
});
stream.pipe(file);
});

View File

@ -10,10 +10,11 @@ import { ClusterManager } from "./cluster-manager";
import { ContextHandler } from "./context-handler";
import logger from "./logger";
import { NodeShellSession, LocalShellSession } from "./shell-session";
import { assert } from "../common/utils";
export class LensProxy {
protected origin: string;
protected proxyServer: http.Server;
protected proxyServer?: http.Server;
protected router: Router;
protected closed = false;
protected retryCounters = new Map<string, number>();
@ -36,7 +37,7 @@ export class LensProxy {
close() {
logger.info("Closing proxy server");
this.proxyServer.close();
this.proxyServer?.close();
this.closed = true;
}
@ -52,7 +53,7 @@ export class LensProxy {
});
spdyProxy.on("upgrade", (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => {
if (req.url.startsWith(`${apiPrefix}?`)) {
if (req.url?.startsWith(`${apiPrefix}?`)) {
this.handleWsUpgrade(req, socket, head);
} else {
this.handleProxyUpgrade(proxy, req, socket, head);
@ -69,10 +70,19 @@ export class LensProxy {
const cluster = this.clusterManager.getClusterForRequest(req);
if (cluster) {
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "");
const apiUrl = url.parse(cluster.apiUrl);
const pUrl = url.parse(proxyUrl);
const connectOpts = { port: parseInt(pUrl.port), host: pUrl.hostname };
const authProxyUrl = assert(
await cluster.contextHandler?.resolveAuthProxyUrl(),
"Cluster must be fully initialized to be proxied to",
);
const proxyUrl = authProxyUrl + (req.url?.replace(apiKubePrefix, "") ?? "");
const apiUrlRaw = assert(cluster.apiUrl, "ContextHandler may only be created for valid clusters");
const apiUrl = url.parse(apiUrlRaw);
const pUrl = assert(url.parse(proxyUrl), "proxyUrl must be a valid URL");
const rawPort = assert(pUrl.port, "Port must be specified on proxyUrl");
const host = assert(pUrl.hostname, "Hostname must be specified on proxyUrl");
const connectOpts = { port: parseInt(rawPort), host };
const proxySocket = new net.Socket();
proxySocket.connect(connectOpts, () => {
@ -171,8 +181,11 @@ export class LensProxy {
const ws = new WebSocket.Server({ noServer: true });
return ws.on("connection", ((socket: WebSocket, req: http.IncomingMessage) => {
const cluster = this.clusterManager.getClusterForRequest(req);
const nodeParam = url.parse(req.url, true).query["node"]?.toString();
const cluster = assert(
this.clusterManager.getClusterForRequest(req),
"ClusterID must be a valid ID"
);
const nodeParam = req.url && url.parse(req.url, true).query["node"]?.toString();
const shell = nodeParam
? new NodeShellSession(socket, cluster, nodeParam)
: new LocalShellSession(socket, cluster);
@ -182,8 +195,8 @@ export class LensProxy {
}));
}
protected async getProxyTarget(req: http.IncomingMessage, contextHandler: ContextHandler): Promise<httpProxy.ServerOptions> {
if (req.url.startsWith(apiKubePrefix)) {
protected async getProxyTarget(req: http.IncomingMessage, contextHandler: ContextHandler): Promise<httpProxy.ServerOptions | undefined> {
if (req.url?.startsWith(apiKubePrefix)) {
delete req.headers.authorization;
req.url = req.url.replace(apiKubePrefix, "");
const isWatchRequest = req.url.includes("watch=");
@ -193,14 +206,15 @@ export class LensProxy {
}
protected getRequestId(req: http.IncomingMessage) {
return req.headers.host + req.url;
return (req.headers.host ?? "") + (req.url ?? "");
}
protected async handleRequest(proxy: httpProxy, req: http.IncomingMessage, res: http.ServerResponse) {
const cluster = this.clusterManager.getClusterForRequest(req);
if (cluster) {
const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler);
const contextHandler = assert(cluster.contextHandler, "Cluster must be initialized to handle requests");
const proxyTarget = await this.getProxyTarget(req, contextHandler);
if (proxyTarget) {
// allow to fetch apis in "clusterId.localhost:port" from "localhost:port"
@ -210,6 +224,7 @@ export class LensProxy {
return proxy.web(req, res, proxyTarget);
}
}
this.router.route(cluster, req, res);
}

View File

@ -14,7 +14,12 @@ import { exitApp } from "./exit-app";
import { broadcastMessage } from "../common/ipc";
import * as packageJson from "../../package.json";
export type MenuTopId = "mac" | "file" | "edit" | "view" | "help";
type UniversalTopMenuId = "file" | "edit" | "view" | "help";
export type MenuTopId = "mac" | UniversalTopMenuId;
interface AppMenus extends Record<UniversalTopMenuId, MenuItemConstructorOptions> {
mac?: MenuItemConstructorOptions;
}
export function initMenu(windowManager: WindowManager) {
return autorun(() => buildMenu(windowManager), {
@ -67,8 +72,8 @@ export function buildMenu(windowManager: WindowManager) {
submenu: [
{
label: "About Lens",
click(menuItem: MenuItem, browserWindow: BrowserWindow) {
showAbout(browserWindow);
click(menuItem: MenuItem, browserWindow?: BrowserWindow) {
browserWindow && showAbout(browserWindow);
}
},
{ type: "separator" },
@ -245,15 +250,15 @@ export function buildMenu(windowManager: WindowManager) {
...ignoreOnMac([
{
label: "About Lens",
click(menuItem: MenuItem, browserWindow: BrowserWindow) {
showAbout(browserWindow);
click(menuItem: MenuItem, browserWindow?: BrowserWindow) {
browserWindow && showAbout(browserWindow);
}
}
])
]
};
// Prepare menu items order
const appMenu: Record<MenuTopId, MenuItemConstructorOptions> = {
const appMenu = {
mac: macAppMenu,
file: fileMenu,
edit: editMenu,
@ -273,7 +278,7 @@ export function buildMenu(windowManager: WindowManager) {
});
if (!isMac) {
delete appMenu.mac;
delete (appMenu as AppMenus).mac;
}
const menu = Menu.buildFromTemplate(Object.values(appMenu));
@ -284,9 +289,9 @@ export function buildMenu(windowManager: WindowManager) {
// this is a workaround for the test environment (spectron) not being able to directly access
// the application menus (https://github.com/electron-userland/spectron/issues/21)
ipcMain.on("test-menu-item-click", (event: IpcMainEvent, ...names: string[]) => {
let menu: Menu = Menu.getApplicationMenu();
let menu: Menu | undefined | null = Menu.getApplicationMenu();
let menuItem: MenuItem | undefined;
const parentLabels: string[] = [];
let menuItem: MenuItem;
for (const name of names) {
parentLabels.push(name);

View File

@ -8,25 +8,15 @@ export class PrometheusHelm extends PrometheusLens {
name = "Helm";
rateAccuracy = "5m";
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService> {
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
const labelSelector = "app=prometheus,component=server,heritage=Helm";
try {
const serviceList = await client.listServiceForAllNamespaces(false, "", null, labelSelector);
const service = serviceList.body.items[0];
const serviceList = await client.listServiceForAllNamespaces(false, "", undefined, labelSelector);
if (!service) return;
return {
id: this.id,
namespace: service.metadata.namespace,
service: service.metadata.name,
port: service.spec.ports[0].port
};
return super.getPrometheusServiceRaw(serviceList.body.items[0]);
} catch(error) {
logger.warn(`PrometheusHelm: failed to list services: ${error.toString()}`);
return;
}
}
}

View File

@ -2,28 +2,22 @@ import { PrometheusProvider, PrometheusQueryOpts, PrometheusQuery, PrometheusSer
import { CoreV1Api } from "@kubernetes/client-node";
import logger from "../logger";
export class PrometheusLens implements PrometheusProvider {
export class PrometheusLens extends PrometheusProvider {
id = "lens";
name = "Lens";
rateAccuracy = "1m";
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService> {
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
try {
const resp = await client.readNamespacedService("prometheus", "lens-metrics");
const service = resp.body;
return {
id: this.id,
namespace: service.metadata.namespace,
service: service.metadata.name,
port: service.spec.ports[0].port
};
return super.getPrometheusServiceRaw(resp.body);
} catch(error) {
logger.warn(`PrometheusLens: failed to list services: ${error.response.body.message}`);
}
}
public getQueries(opts: PrometheusQueryOpts): PrometheusQuery {
public getQueries(opts: PrometheusQueryOpts): PrometheusQuery | undefined {
switch(opts.category) {
case "cluster":
return {

View File

@ -1,39 +1,40 @@
import { PrometheusProvider, PrometheusQueryOpts, PrometheusQuery, PrometheusService } from "./provider-registry";
import { CoreV1Api, V1Service } from "@kubernetes/client-node";
import { CoreV1Api } from "@kubernetes/client-node";
import logger from "../logger";
export class PrometheusOperator implements PrometheusProvider {
export class PrometheusOperator extends PrometheusProvider {
rateAccuracy = "1m";
id = "operator";
name = "Prometheus Operator";
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService> {
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
try {
let service: V1Service;
let serviceItem;
for (const labelSelector of ["operated-prometheus=true", "self-monitor=true"]) {
if (!service) {
const serviceList = await client.listServiceForAllNamespaces(null, null, null, labelSelector);
service = serviceList.body.items[0];
}
serviceItem ??= (
await client.listServiceForAllNamespaces(undefined, undefined, undefined, labelSelector)
)?.body.items[0];
}
if (!service) return;
return {
id: this.id,
namespace: service.metadata.namespace,
service: service.metadata.name,
port: service.spec.ports[0].port
};
const { metadata, spec } = serviceItem ?? {};
const { namespace, name: service } = metadata ?? {};
const { ports: [{ port }] = [] } = spec ?? {};
if (port && namespace && service) {
return {
id: this.id,
namespace,
service,
port,
};
}
} catch(error) {
logger.warn(`PrometheusOperator: failed to list services: ${error.toString()}`);
return;
}
}
public getQueries(opts: PrometheusQueryOpts): PrometheusQuery {
public getQueries(opts: PrometheusQueryOpts): PrometheusQuery | undefined {
switch(opts.category) {
case "cluster":
return {

View File

@ -1,4 +1,4 @@
import { CoreV1Api } from "@kubernetes/client-node";
import { CoreV1Api, V1Service } from "@kubernetes/client-node";
export type PrometheusClusterQuery = {
memoryUsage: string;
@ -59,11 +59,26 @@ export type PrometheusService = {
port: number;
};
export interface PrometheusProvider {
id: string;
name: string;
getQueries(opts: PrometheusQueryOpts): PrometheusQuery;
getPrometheusService(client: CoreV1Api): Promise<PrometheusService>;
export abstract class PrometheusProvider {
abstract id: string;
abstract name: string;
abstract getQueries(opts: PrometheusQueryOpts): PrometheusQuery | undefined;
abstract getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined>;
protected getPrometheusServiceRaw(raw?: V1Service): PrometheusService | undefined {
const { metadata, spec } = raw ?? {};
const { namespace, name: service } = metadata ?? {};
const { ports: [{ port }] = [] } = spec ?? {};
if (port && namespace && service) {
return {
id: this.id,
namespace,
service,
port,
};
}
}
}
export type PrometheusProviderList = {

View File

@ -2,28 +2,22 @@ import { PrometheusProvider, PrometheusQueryOpts, PrometheusQuery, PrometheusSer
import { CoreV1Api } from "@kubernetes/client-node";
import logger from "../logger";
export class PrometheusStacklight implements PrometheusProvider {
export class PrometheusStacklight extends PrometheusProvider {
id = "stacklight";
name = "Stacklight";
rateAccuracy = "1m";
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService> {
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
try {
const resp = await client.readNamespacedService("prometheus-server", "stacklight");
const service = resp.body;
return {
id: this.id,
namespace: service.metadata.namespace,
service: service.metadata.name,
port: service.spec.ports[0].port
};
return super.getPrometheusServiceRaw(resp.body);
} catch(error) {
logger.warn(`PrometheusLens: failed to list services: ${error.response.body.message}`);
}
}
public getQueries(opts: PrometheusQueryOpts): PrometheusQuery {
public getQueries(opts: PrometheusQueryOpts): PrometheusQuery | undefined {
switch(opts.category) {
case "cluster":
return {

View File

@ -7,7 +7,7 @@ import path from "path";
import * as tempy from "tempy";
import logger from "./logger";
import { appEventBus } from "../common/event-bus";
import { cloneJsonObject } from "../common/utils";
import { assert, cloneJsonObject, NotFalsy } from "../common/utils";
export class ResourceApplier {
constructor(protected cluster: Cluster) {
@ -21,7 +21,7 @@ export class ResourceApplier {
}
protected async kubectlApply(content: string): Promise<string> {
const { kubeCtl } = this.cluster;
const kubeCtl = assert(this.cluster.kubeCtl, "Cluster must be initialized correctly before being applied against");
const kubectlPath = await kubeCtl.getPath();
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
@ -53,7 +53,7 @@ export class ResourceApplier {
}
public async kubectlApplyAll(resources: string[]): Promise<string> {
const { kubeCtl } = this.cluster;
const kubeCtl = assert(this.cluster.kubeCtl, "Cluster must be initialized correctly before being applied against");
const kubectlPath = await kubeCtl.getPath();
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();

View File

@ -11,12 +11,12 @@ import logger from "./logger";
export interface RouterRequestOpts {
req: http.IncomingMessage;
res: http.ServerResponse;
cluster: Cluster;
cluster: Cluster | null;
params: RouteParams;
url: URL;
}
export interface RouteParams extends Record<string, string> {
export interface RouteParams extends Record<string, string | undefined> {
path?: string; // *-route
namespace?: string;
service?: string;
@ -30,7 +30,7 @@ export interface LensApiRequest<P = any> {
path: string;
payload: P;
params: RouteParams;
cluster: Cluster;
cluster: Cluster | null;
response: http.ServerResponse;
query: URLSearchParams;
raw: {
@ -52,10 +52,10 @@ export class Router {
return path.resolve(__static);
}
public async route(cluster: Cluster, req: http.IncomingMessage, res: http.ServerResponse): Promise<boolean> {
const url = new URL(req.url, "http://localhost");
public async route(cluster: Cluster | null, req: http.IncomingMessage, res: http.ServerResponse): Promise<boolean> {
const url = new URL(req.url ?? "", "http://localhost");
const path = url.pathname;
const method = req.method.toLowerCase();
const method = req.method?.toLowerCase() ?? "get";
const matchingRoute = this.router.route(method, path);
const routeFound = !matchingRoute.isBoom;
@ -118,6 +118,13 @@ export class Router {
return;
}
if (!req.url) {
logger.error("handleStaticFile: no URL in request");
res.statusCode = 404;
return res.end();
}
try {
const filename = path.basename(req.url);
// redirect requests to [appName].js, [appName].html /sockjs-node/ to webpack-dev-server (for hot-reload support)
@ -132,6 +139,7 @@ export class Router {
return;
}
const data = await readFile(asset);
res.setHeader("Content-Type", this.getMimeType(asset));
@ -141,10 +149,10 @@ export class Router {
if (retryCount > 5) {
logger.error("handleStaticFile:", err.toString());
res.statusCode = 404;
res.end();
return;
return res.end();
}
this.handleStaticFile(`${publicPath}/${appName}.html`, res, req, Math.max(retryCount, 0) + 1);
}
}
@ -154,7 +162,9 @@ export class Router {
this.router.add(
{ method: "get", path: "/{path*}" },
({ params, response, raw: { req } }: LensApiRequest) => {
this.handleStaticFile(params.path, response, req);
if (params.path) {
this.handleStaticFile(params.path, response, req);
}
});
this.router.add({ method: "get", path: "/version"}, versionRoute.getVersion.bind(versionRoute));

View File

@ -17,7 +17,7 @@ class HelmApiRoute extends LensApi {
try {
const chart = await helmService.getChart(params.repo, params.chart, query.get("version"));
this.respondJson(response, chart);
this.respondJson(response, chart ?? {});
} catch (error) {
this.respondText(response, error, 422);
}
@ -29,7 +29,7 @@ class HelmApiRoute extends LensApi {
try {
const values = await helmService.getChartValues(params.repo, params.chart, query.get("version"));
this.respondJson(response, values);
this.respondJson(response, values ?? {});
} catch (error) {
this.respondText(response, error, 422);
}
@ -38,10 +38,16 @@ class HelmApiRoute extends LensApi {
public async installChart(request: LensApiRequest) {
const { payload, cluster, response } = request;
if (!cluster) {
logger.error("[HELM-ROUTE]: no cluster defined on installChart request");
return this.respondText(response, "No Cluster defined on request", 404);
}
try {
const result = await helmService.installChart(cluster, payload);
this.respondJson(response, result, 201);
this.respondJson(response, result ?? {}, 201);
} catch (error) {
logger.debug(error);
this.respondText(response, error, 422);
@ -51,10 +57,16 @@ class HelmApiRoute extends LensApi {
public async updateRelease(request: LensApiRequest) {
const { cluster, params, payload, response } = request;
if (!cluster) {
logger.error("[HELM-ROUTE]: no cluster defined on updateRelease request");
return this.respondText(response, "No Cluster defined on request", 404);
}
try {
const result = await helmService.updateRelease(cluster, params.release, params.namespace, payload );
this.respondJson(response, result);
this.respondJson(response, result ?? {});
} catch (error) {
logger.debug(error);
this.respondText(response, error, 422);
@ -64,10 +76,16 @@ class HelmApiRoute extends LensApi {
public async rollbackRelease(request: LensApiRequest) {
const { cluster, params, payload, response } = request;
if (!cluster) {
logger.error("[HELM-ROUTE]: no cluster defined on rollbackRelease request");
return this.respondText(response, "No Cluster defined on request", 404);
}
try {
const result = await helmService.rollback(cluster, params.release, params.namespace, payload.revision);
this.respondJson(response, result);
this.respondJson(response, result ?? {});
} catch (error) {
logger.debug(error);
this.respondText(response, error, 422);
@ -77,10 +95,16 @@ class HelmApiRoute extends LensApi {
public async listReleases(request: LensApiRequest) {
const { cluster, params, response } = request;
if (!cluster) {
logger.error("[HELM-ROUTE]: no cluster defined on listReleases request");
return this.respondText(response, "No Cluster defined on request", 404);
}
try {
const result = await helmService.listReleases(cluster, params.namespace);
this.respondJson(response, result);
this.respondJson(response, result ?? {});
} catch(error) {
logger.debug(error);
this.respondText(response, error, 422);
@ -90,6 +114,12 @@ class HelmApiRoute extends LensApi {
public async getRelease(request: LensApiRequest) {
const { cluster, params, response } = request;
if (!cluster) {
logger.error("[HELM-ROUTE]: no cluster defined on getRelease request");
return this.respondText(response, "No Cluster defined on request", 404);
}
try {
const result = await helmService.getRelease(cluster, params.release, params.namespace);
@ -103,10 +133,16 @@ class HelmApiRoute extends LensApi {
public async getReleaseValues(request: LensApiRequest) {
const { cluster, params, response } = request;
if (!cluster) {
logger.error("[HELM-ROUTE]: no cluster defined on getReleaseValues request");
return this.respondText(response, "No Cluster defined on request", 404);
}
try {
const result = await helmService.getReleaseValues(cluster, params.release, params.namespace);
this.respondText(response, result);
this.respondText(response, result ?? "");
} catch (error) {
logger.debug(error);
this.respondText(response, error, 422);
@ -116,6 +152,12 @@ class HelmApiRoute extends LensApi {
public async getReleaseHistory(request: LensApiRequest) {
const { cluster, params, response } = request;
if (!cluster) {
logger.error("[HELM-ROUTE]: no cluster defined on getReleaseHistory request");
return this.respondText(response, "No Cluster defined on request", 404);
}
try {
const result = await helmService.getReleaseHistory(cluster, params.release, params.namespace);
@ -129,10 +171,16 @@ class HelmApiRoute extends LensApi {
public async deleteRelease(request: LensApiRequest) {
const { cluster, params, response } = request;
if (!cluster) {
logger.error("[HELM-ROUTE]: no cluster defined on deleteRelease request");
return this.respondText(response, "No Cluster defined on request", 404);
}
try {
const result = await helmService.deleteRelease(cluster, params.release, params.namespace);
this.respondJson(response, result);
this.respondJson(response, result ?? "");
} catch (error) {
logger.debug(error);
this.respondText(response, error, 422);

View File

@ -2,8 +2,15 @@ import { LensApiRequest } from "../router";
import { LensApi } from "../lens-api";
import { Cluster } from "../cluster";
import { CoreV1Api, V1Secret } from "@kubernetes/client-node";
import logger from "../logger";
import { assert } from "../../common/utils";
import { AssertionError } from "assert";
function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster) {
if (!secret.data) {
return;
}
const tokenData = Buffer.from(secret.data["token"], "base64");
return {
@ -32,7 +39,7 @@ function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster
"context": {
"user": username,
"cluster": cluster.contextName,
"namespace": secret.metadata.namespace,
"namespace": secret.metadata?.namespace,
}
}
],
@ -43,17 +50,35 @@ function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster
class KubeconfigRoute extends LensApi {
public async routeServiceAccountRoute(request: LensApiRequest) {
const { params, response, cluster} = request;
const client = (await cluster.getProxyKubeconfig()).makeApiClient(CoreV1Api);
const secretList = await client.listNamespacedSecret(params.namespace);
const secret = secretList.body.items.find(secret => {
const { annotations } = secret.metadata;
const { params, response, cluster: maybeCluster } = request;
return annotations && annotations["kubernetes.io/service-account.name"] == params.account;
});
const data = generateKubeConfig(params.account, secret, cluster);
try {
const cluster = assert(maybeCluster, "No Cluster defined on request");
const namespace = assert(params.namespace, "Namespace not provided");
const account = assert(params.account, "AccountName not provided");
this.respondJson(response, data);
const client = (await cluster.getProxyKubeconfig()).makeApiClient(CoreV1Api);
const secretList = await client.listNamespacedSecret(namespace);
const secret = assert(
secretList.body.items
.find(({ metadata }) => (
metadata?.annotations?.["kubernetes.io/service-account.name"] == account
)),
"No secret found matching the account name",
);
const data = generateKubeConfig(account, secret, cluster);
this.respondJson(response, data ?? {});
} catch (error) {
logger.error(`[KUBECONFIG-ROUTE]: routeServiceAccount failed: ${error}`);
if (error instanceof AssertionError) {
this.respondText(response, error.message, 404);
} else {
this.respondText(response, error.toString(), 404);
}
}
}
}

View File

@ -4,6 +4,9 @@ import { LensApi } from "../lens-api";
import { Cluster, ClusterMetadataKey } from "../cluster";
import { ClusterPrometheusMetadata } from "../../common/cluster-store";
import logger from "../logger";
import { assert, NotFalsy } from "../../common/utils";
import { AssertionError } from "assert";
import { PrometheusQueryOpts } from "../prometheus/provider-registry";
export type IMetricsQuery = string | string[] | {
[metricName: string]: string;
@ -41,50 +44,69 @@ async function loadMetrics(promQueries: string[], cluster: Cluster, prometheusPa
}
class MetricsRoute extends LensApi {
async routeMetrics({ response, cluster, payload, query }: LensApiRequest) {
const queryParams: IMetricsQuery = Object.fromEntries(query.entries());
const prometheusMetadata: ClusterPrometheusMetadata = {};
async routeMetrics({ response, cluster: maybeCluster, payload, query }: LensApiRequest) {
try {
const [prometheusPath, prometheusProvider] = await Promise.all([
cluster.contextHandler.getPrometheusPath(),
cluster.contextHandler.getPrometheusProvider()
]);
const cluster = assert(maybeCluster, "No Cluster defined on request");
const contextHandler = assert(cluster.contextHandler, "Cluster must be initialized to be routed against");
prometheusMetadata.provider = prometheusProvider?.id;
prometheusMetadata.autoDetected = !cluster.preferences.prometheusProvider?.type;
const queryParams: IMetricsQuery = Object.fromEntries(query.entries());
const prometheusMetadata: ClusterPrometheusMetadata = {};
if (!prometheusPath) {
try {
const [prometheusPath, prometheusProvider] = await Promise.all([
contextHandler.getPrometheusPath(),
contextHandler.getPrometheusProvider()
]);
prometheusMetadata.provider = prometheusProvider?.id;
prometheusMetadata.autoDetected = !cluster.preferences.prometheusProvider?.type;
if (!prometheusPath) {
prometheusMetadata.success = false;
this.respondJson(response, {});
return;
}
// return data in same structure as query
if (typeof payload === "string") {
const [data] = await loadMetrics([payload], cluster, prometheusPath, queryParams);
this.respondJson(response, data);
} else if (Array.isArray(payload)) {
const data = await loadMetrics(payload, cluster, prometheusPath, queryParams);
this.respondJson(response, data);
} else if (payload && typeof payload === "object") {
const queries = Object.entries(payload)
.map(([queryName, queryOpts]) => {
const queries = prometheusProvider?.getQueries(queryOpts as PrometheusQueryOpts);
if (queries) {
return (queries as Record<string, string>)[queryName];
}
})
.filter(NotFalsy);
const result = await loadMetrics(queries, cluster, prometheusPath, queryParams);
const data = Object.fromEntries(Object.keys(payload).map((metricName, i) => [metricName, result[i]]));
this.respondJson(response, data);
}
prometheusMetadata.success = true;
} catch {
prometheusMetadata.success = false;
this.respondJson(response, {});
return;
} finally {
cluster.metadata[ClusterMetadataKey.PROMETHEUS] = prometheusMetadata;
}
} catch (error) {
logger.error(`[METRICS-ROUTE]: routeMetrics failed: ${error}`);
// return data in same structure as query
if (typeof payload === "string") {
const [data] = await loadMetrics([payload], cluster, prometheusPath, queryParams);
this.respondJson(response, data);
} else if (Array.isArray(payload)) {
const data = await loadMetrics(payload, cluster, prometheusPath, queryParams);
this.respondJson(response, data);
if (error instanceof AssertionError) {
this.respondText(response, error.message, 404);
} else {
const queries = Object.entries(payload).map(([queryName, queryOpts]) => (
(prometheusProvider.getQueries(queryOpts) as Record<string, string>)[queryName]
));
const result = await loadMetrics(queries, cluster, prometheusPath, queryParams);
const data = Object.fromEntries(Object.keys(payload).map((metricName, i) => [metricName, result[i]]));
this.respondJson(response, data);
this.respondText(response, error.toString(), 404);
}
prometheusMetadata.success = true;
} catch {
prometheusMetadata.success = false;
this.respondJson(response, {});
} finally {
cluster.metadata[ClusterMetadataKey.PROMETHEUS] = prometheusMetadata;
}
}
}

View File

@ -6,33 +6,59 @@ import { getFreePort } from "../port";
import { shell } from "electron";
import * as tcpPortUsed from "tcp-port-used";
import logger from "../logger";
import { AssertionError } from "assert";
import { assert } from "../../common/utils";
interface PortForwardOpts {
clusterId: string;
process?: ChildProcessWithoutNullStreams;
kubeConfig: string;
kind: string;
namespace: string;
name: string;
port: string;
localPort?: number;
}
interface GetPortForwardOptions {
clusterId: string;
kind: string;
name: string;
namespace: string;
port: string;
}
class PortForward {
public static portForwards: PortForward[] = [];
static getPortforward(forward: {clusterId: string; kind: string; name: string; namespace: string; port: string}) {
return PortForward.portForwards.find((pf) => {
return (
pf.clusterId == forward.clusterId &&
pf.kind == forward.kind &&
pf.name == forward.name &&
pf.namespace == forward.namespace &&
pf.port == forward.port
);
});
static getPortforward(forward: GetPortForwardOptions) {
return PortForward.portForwards.find(pf => (
pf.clusterId == forward.clusterId &&
pf.kind == forward.kind &&
pf.name == forward.name &&
pf.namespace == forward.namespace &&
pf.port == forward.port
));
}
public clusterId: string;
public process: ChildProcessWithoutNullStreams;
public process?: ChildProcessWithoutNullStreams;
public kubeConfig: string;
public kind: string;
public namespace: string;
public name: string;
public port: string;
public localPort: number;
public localPort?: number;
constructor(obj: any) {
Object.assign(this, obj);
constructor(obj: PortForwardOpts) {
this.clusterId = obj.clusterId;
this.process = obj.process;
this.kubeConfig = obj.kubeConfig;
this.kind = obj.kind;
this.namespace = obj.namespace;
this.name = obj.name;
this.port = obj.port;
this.localPort = obj.localPort;
}
public async start() {
@ -75,39 +101,51 @@ class PortForward {
}
class PortForwardRoute extends LensApi {
public async routePortForward(request: LensApiRequest) {
const { params, response, cluster} = request;
const { namespace, port, resourceType, resourceName } = params;
let portForward = PortForward.getPortforward({
clusterId: cluster.id, kind: resourceType, name: resourceName,
namespace, port
});
const { params, response, cluster: maybeCluster } = request;
if (!portForward) {
logger.info(`Creating a new port-forward ${namespace}/${resourceType}/${resourceName}:${port}`);
portForward = new PortForward({
clusterId: cluster.id,
kind: resourceType,
namespace,
name: resourceName,
port,
kubeConfig: await cluster.getProxyKubeconfigPath()
});
const started = await portForward.start();
try {
const cluster = assert(maybeCluster, "No Cluster defined on request");
const namespace = assert(params.namespace, "Namespace not provided");
const port = assert(params.port, "Port not provided");
const name = assert(params.resourceName, "ResourceName not provided");
const kind = assert(params.resourceType, "ResourceName not provided");
if (!started) {
this.respondJson(response, {
message: "Failed to open port-forward"
}, 400);
let portForward = PortForward.getPortforward({ clusterId: cluster.id, kind, name, namespace, port });
return;
if (!portForward) {
const kubeConfig = assert(await cluster.getProxyKubeconfigPath(), "Cluster must be initialized before being port forwarded from");
logger.info(`Creating a new port-forward ${namespace}/${kind}/${name}:${port}`);
portForward = new PortForward({
clusterId: cluster.id,
kind,
namespace,
name,
port,
kubeConfig,
});
const started = await portForward.start();
if (!started) {
return void this.respondJson(response, {
message: "Failed to open port-forward"
}, 400);
}
}
portForward.open();
this.respondJson(response, {});
} catch (error) {
logger.error(`[PORT-FORWARD-ROUTE]: routeServiceAccount failed: ${error}`);
if (error instanceof AssertionError) {
this.respondText(response, error.message, 404);
} else {
this.respondText(response, error.toString(), 404);
}
}
portForward.open();
this.respondJson(response, {});
}
}

View File

@ -1,17 +1,27 @@
import { LensApiRequest } from "../router";
import { LensApi } from "../lens-api";
import { ResourceApplier } from "../resource-applier";
import { assert } from "../../common/utils";
import { AssertionError } from "assert";
import logger from "../logger";
class ResourceApplierApiRoute extends LensApi {
public async applyResource(request: LensApiRequest) {
const { response, cluster, payload } = request;
const { response, cluster: maybeCluster, payload } = request;
try {
const cluster = assert(maybeCluster, "No Cluster defined on request");
const resource = await new ResourceApplier(cluster).apply(payload);
this.respondJson(response, [resource], 200);
} catch (error) {
this.respondText(response, error, 422);
logger.error(`[RESOURCE-APPLIER-ROUTE]: routeServiceAccount failed: ${error}`);
if (error instanceof AssertionError) {
this.respondText(response, error.message, 404);
} else {
this.respondText(response, error, 422);
}
}
}
}

View File

@ -9,21 +9,20 @@ export class NodeShellSession extends ShellSession {
ShellType = "node-shell";
protected podId = `node-shell-${uuid()}`;
protected kc: KubeConfig;
constructor(socket: WebSocket, cluster: Cluster, protected nodeName: string) {
super(socket, cluster);
}
public async open() {
this.kc = await this.cluster.getProxyKubeconfig();
const kc = await this.cluster.getProxyKubeconfig();
const shell = await this.kubectl.getPath();
try {
await this.createNodeShellPod();
await this.waitForRunningPod();
await this.createNodeShellPod(kc);
await this.waitForRunningPod(kc);
} catch (error) {
this.deleteNodeShellPod();
this.deleteNodeShellPod(kc);
this.sendResponse("Error occurred. ");
throw new ShellOpenError("failed to create node pod", error);
@ -35,9 +34,8 @@ export class NodeShellSession extends ShellSession {
super.open(shell, args, env);
}
protected createNodeShellPod() {
return this
.kc
protected createNodeShellPod(kc: KubeConfig) {
return kc
.makeApiClient(k8s.CoreV1Api)
.createNamespacedPod("kube-system", {
metadata: {
@ -67,9 +65,9 @@ export class NodeShellSession extends ShellSession {
});
}
protected waitForRunningPod(): Promise<void> {
protected waitForRunningPod(kc: KubeConfig): Promise<void> {
return new Promise((resolve, reject) => {
const watch = new k8s.Watch(this.kc);
const watch = new k8s.Watch(kc);
watch
.watch(`/api/v1/namespaces/kube-system/pods`,
@ -99,9 +97,8 @@ export class NodeShellSession extends ShellSession {
});
}
protected deleteNodeShellPod() {
this
.kc
protected deleteNodeShellPod(kc: KubeConfig) {
kc
.makeApiClient(k8s.CoreV1Api)
.deleteNamespacedPod(this.podId, "kube-system");
}

View File

@ -25,9 +25,9 @@ export abstract class ShellSession {
protected kubectl: Kubectl;
protected running = false;
protected shellProcess: pty.IPty;
protected shellProcess?: pty.IPty;
protected kubectlBinDirP: Promise<string>;
protected kubeconfigPathP: Promise<string>;
protected kubeconfigPathP: Promise<string | undefined>;
protected get cwd(): string | undefined {
return this.cluster.preferences?.terminalCWD;
@ -71,19 +71,21 @@ export abstract class ShellSession {
switch (data[0]) {
case "0":
this.shellProcess.write(message);
this.shellProcess?.write(message);
break;
case "4":
const { Width, Height } = JSON.parse(message);
this.shellProcess.resize(Width, Height);
this.shellProcess?.resize(Width, Height);
break;
}
})
.on("close", () => {
if (this.running) {
try {
process.kill(this.shellProcess.pid);
if (this.shellProcess?.pid) {
process.kill(this.shellProcess?.pid);
}
} catch (e) {
}
}

View File

@ -13,7 +13,7 @@ import { exitApp } from "./exit-app";
const TRAY_LOG_PREFIX = "[TRAY]";
// note: instance of Tray should be saved somewhere, otherwise it disappears
export let tray: Tray;
export let tray: Tray | undefined;
export function getTrayIcon(): string {
return path.resolve(
@ -43,7 +43,7 @@ export function initTray(windowManager: WindowManager) {
try {
const menu = createTrayMenu(windowManager);
tray.setContextMenu(menu);
tray?.setContextMenu(menu);
} catch (error) {
logger.error(`${TRAY_LOG_PREFIX}: building failed`, { error });
}
@ -53,7 +53,7 @@ export function initTray(windowManager: WindowManager) {
return () => {
disposers.forEach(disposer => disposer());
tray?.destroy();
tray = null;
tray = undefined;
};
}

View File

@ -12,12 +12,12 @@ import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
import logger from "./logger";
export class WindowManager extends Singleton {
protected mainWindow: BrowserWindow;
protected splashWindow: BrowserWindow;
protected windowState: windowStateKeeper.State;
protected mainWindow: BrowserWindow | null = null;
protected splashWindow: BrowserWindow | null = null;
protected windowState: windowStateKeeper.State | null = null;
protected disposers: Record<string, Function> = {};
@observable activeClusterId: ClusterId;
@observable activeClusterId?: ClusterId;
constructor(protected proxyPort: number) {
super();
@ -30,7 +30,7 @@ export class WindowManager extends Singleton {
return `http://localhost:${this.proxyPort}`;
}
async initMainWindow(showSplash = true) {
async initMainWindow(showSplash = true): Promise<BrowserWindow> {
// Manage main window size and position with state persistence
if (!this.windowState) {
this.windowState = windowStateKeeper({
@ -77,7 +77,7 @@ export class WindowManager extends Singleton {
// clean up
this.mainWindow.on("closed", () => {
this.windowState.unmanage();
this.windowState?.unmanage();
this.mainWindow = null;
this.splashWindow = null;
app.dock?.hide(); // hide icon in dock (mac-os)
@ -104,6 +104,8 @@ export class WindowManager extends Singleton {
} catch (err) {
dialog.showErrorBox("ERROR!", err.toString());
}
return this.mainWindow;
}
protected async initMenu() {
@ -122,17 +124,18 @@ export class WindowManager extends Singleton {
}
async ensureMainWindow(): Promise<BrowserWindow> {
if (!this.mainWindow) await this.initMainWindow();
this.mainWindow.show();
const mainWindow = this.mainWindow ?? await this.initMainWindow();
return this.mainWindow;
mainWindow.show();
return mainWindow;
}
sendToView({ channel, frameInfo, data = [] }: { channel: string, frameInfo?: ClusterFrameInfo, data?: any[] }) {
if (frameInfo) {
this.mainWindow.webContents.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...data);
this.mainWindow?.webContents.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...data);
} else {
this.mainWindow.webContents.send(channel, ...data);
this.mainWindow?.webContents.send(channel, ...data);
}
}
@ -152,7 +155,7 @@ export class WindowManager extends Singleton {
}
reload() {
const frameInfo = clusterFrameMap.get(this.activeClusterId);
const frameInfo = this.activeClusterId && clusterFrameMap.get(this.activeClusterId);
if (frameInfo) {
this.sendToView({ channel: IpcRendererNavigationEvents.RELOAD_PAGE, frameInfo });
@ -186,8 +189,8 @@ export class WindowManager extends Singleton {
}
destroy() {
this.mainWindow.destroy();
this.splashWindow.destroy();
this.mainWindow?.destroy();
this.splashWindow?.destroy();
this.mainWindow = null;
this.splashWindow = null;
Object.entries(this.disposers).forEach(([name, dispose]) => {

View File

@ -26,7 +26,7 @@ export default migration({
*/
try {
// take the embedded kubeconfig and dump it into a file
cluster.kubeConfigPath = ClusterStore.embedCustomKubeConfig(cluster.id, cluster.kubeConfig);
cluster.kubeConfigPath = ClusterStore.embedCustomKubeConfig(cluster.id, cluster.kubeConfig ?? "");
cluster.contextName = loadConfig(cluster.kubeConfigPath).getCurrentContext();
delete cluster.kubeConfig;
@ -51,7 +51,7 @@ export default migration({
}
} catch (error) {
printLog(`Failed to migrate cluster icon for cluster "${cluster.id}"`, error);
delete cluster.preferences.icon;
delete cluster.preferences?.icon;
}
return cluster;

View File

@ -13,7 +13,7 @@ import { IKubeApiParsed, 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", {

View File

@ -3,13 +3,14 @@ import type { KubeObjectStore } from "../kube-object.store";
import { action, observable } from "mobx";
import { autobind } from "../utils";
import { KubeApi, parseKubeApi } from "./kube-api";
import { KubeObject } from "./kube-object";
@autobind()
export class ApiManager {
private apis = observable.map<string, KubeApi>();
private stores = observable.map<string, KubeObjectStore>();
private apis = observable.map<string, KubeApi<any, any>>();
private stores = observable.map<string, KubeObjectStore<any, any, KubeObject<any, any>>>();
getApi(pathOrCallback: string | ((api: KubeApi) => boolean)) {
getApi(pathOrCallback: string | ((api: KubeApi<any, any>) => boolean)) {
if (typeof pathOrCallback === "string") {
return this.apis.get(pathOrCallback) || this.apis.get(parseKubeApi(pathOrCallback).apiBase);
}
@ -18,10 +19,14 @@ export class ApiManager {
}
getApiByKind(kind: string, apiVersion: string) {
return Array.from(this.apis.values()).find((api) => api.kind === kind && api.apiVersionWithGroup === apiVersion);
for (const api of this.apis.values()) {
if (api.kind === kind && api.apiVersionWithGroup === apiVersion) {
return api;
}
}
}
registerApi(apiBase: string, api: KubeApi) {
registerApi<Spec, Status>(apiBase: string, api: KubeApi<Spec, Status>) {
if (!this.apis.has(apiBase)) {
this.stores.forEach((store) => {
if(store.api === api) {
@ -33,13 +38,13 @@ export class ApiManager {
}
}
protected resolveApi(api: string | KubeApi): KubeApi {
protected resolveApi<Spec, Status>(api: string | KubeApi<Spec, Status>): KubeApi<Spec, Status> | undefined {
if (typeof api === "string") return this.getApi(api);
return api;
}
unregisterApi(api: string | KubeApi) {
unregisterApi<Spec, Status>(api: string | KubeApi<Spec, Status>) {
if (typeof api === "string") this.apis.delete(api);
else {
const apis = Array.from(this.apis.entries());
@ -50,14 +55,20 @@ export class ApiManager {
}
@action
registerStore(store: KubeObjectStore, apis: KubeApi[] = [store.api]) {
registerStore<Spec, Status>(store: KubeObjectStore<Spec, Status, KubeObject<Spec, Status>>, apis: KubeApi<Spec, Status>[] = [store.api]) {
apis.forEach(api => {
this.stores.set(api.apiBase, store);
});
}
getStore<S extends KubeObjectStore>(api: string | KubeApi): S {
return this.stores.get(this.resolveApi(api)?.apiBase) as S;
getStore<Spec, Status, S extends KubeObjectStore<Spec, Status, KubeObject<Spec, Status>>>(api: string | KubeApi<Spec, Status>): S | undefined {
const apiBase = this.resolveApi(api)?.apiBase;
if (!apiBase) {
return;
}
return this.stores.get(apiBase) as S;
}
}

View File

@ -2,7 +2,7 @@ import { IMetrics, IMetricsReqParams, metricsApi } from "./metrics.api";
import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api";
export class ClusterApi extends KubeApi<Cluster> {
export class ClusterApi extends KubeApi<ClusterSpec, ClusterKubeStatus, Cluster> {
static kind = "Cluster";
static namespaced = true;
@ -50,42 +50,43 @@ export interface IClusterMetrics<T = IMetrics> {
fsUsage: T;
}
export class Cluster extends KubeObject {
interface ClusterSpec {
clusterNetwork?: {
serviceDomain?: string;
pods?: {
cidrBlocks?: string[];
};
services?: {
cidrBlocks?: string[];
};
};
providerSpec: {
value: {
profile: string;
};
};
}
interface ClusterKubeStatus {
apiEndpoints: {
host: string;
port: string;
}[];
providerStatus: {
adminUser?: string;
adminPassword?: string;
kubeconfig?: string;
processState?: string;
lensAddress?: string;
};
errorMessage?: string;
errorReason?: string;
}
export class Cluster extends KubeObject<ClusterSpec, ClusterKubeStatus> {
static kind = "Cluster";
static apiBase = "/apis/cluster.k8s.io/v1alpha1/clusters";
spec: {
clusterNetwork?: {
serviceDomain?: string;
pods?: {
cidrBlocks?: string[];
};
services?: {
cidrBlocks?: string[];
};
};
providerSpec: {
value: {
profile: string;
};
};
};
status?: {
apiEndpoints: {
host: string;
port: string;
}[];
providerStatus: {
adminUser?: string;
adminPassword?: string;
kubeconfig?: string;
processState?: string;
lensAddress?: string;
};
errorMessage?: string;
errorReason?: string;
};
getStatus() {
if (this.metadata.deletionTimestamp) return ClusterStatus.REMOVING;
if (!this.status || !this.status) return ClusterStatus.CREATING;

View File

@ -1,22 +1,14 @@
import { KubeObject } from "../kube-object";
import { KubeJsonApiData } from "../kube-json-api";
import { autobind } from "../../utils";
import { KubeApi } from "../kube-api";
@autobind()
export class ConfigMap extends KubeObject {
export class ConfigMap extends KubeObject<void, void> {
static kind = "ConfigMap";
static namespaced = true;
static apiBase = "/api/v1/configmaps";
constructor(data: KubeJsonApiData) {
super(data);
this.data = this.data || {};
}
data: {
[param: string]: string;
};
data: Record<string, string> = {};
getKeys(): string[] {
return Object.keys(this.data);

View File

@ -17,93 +17,96 @@ type AdditionalPrinterColumnsV1Beta = AdditionalPrinterColumnsCommon & {
JSONPath: string;
};
export class CustomResourceDefinition extends KubeObject {
interface CustomResourceDefinitionSpec {
group: string;
version?: string; // deprecated in v1 api
names: {
plural: string;
singular: string;
kind: string;
listKind: string;
};
scope: "Namespaced" | "Cluster" | string;
validation?: any;
versions: {
name: string;
served: boolean;
storage: boolean;
schema?: unknown; // required in v1 but not present in v1beta
additionalPrinterColumns?: AdditionalPrinterColumnsV1[];
}[];
conversion: {
strategy?: string;
webhook?: any;
};
additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[]; // removed in v1
}
interface CustomResourceDefinitionStatus {
conditions: {
lastTransitionTime: string;
message: string;
reason: string;
status: string;
type?: string;
}[];
acceptedNames: {
plural: string;
singular: string;
kind: string;
shortNames: string[];
listKind: string;
};
storedVersions: string[];
}
export class CustomResourceDefinition extends KubeObject<CustomResourceDefinitionSpec, CustomResourceDefinitionStatus> {
static kind = "CustomResourceDefinition";
static namespaced = false;
static apiBase = "/apis/apiextensions.k8s.io/v1/customresourcedefinitions";
spec: {
group: string;
version?: string; // deprecated in v1 api
names: {
plural: string;
singular: string;
kind: string;
listKind: string;
};
scope: "Namespaced" | "Cluster" | string;
validation?: any;
versions: {
name: string;
served: boolean;
storage: boolean;
schema?: unknown; // required in v1 but not present in v1beta
additionalPrinterColumns?: AdditionalPrinterColumnsV1[]
}[];
conversion: {
strategy?: string;
webhook?: any;
};
additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[]; // removed in v1
};
status: {
conditions: {
lastTransitionTime: string;
message: string;
reason: string;
status: string;
type?: string;
}[];
acceptedNames: {
plural: string;
singular: string;
kind: string;
shortNames: string[];
listKind: string;
};
storedVersions: string[];
};
getResourceUrl() {
return crdResourcesURL({
params: {
group: this.getGroup(),
name: this.getPluralName(),
}
});
const group = this.getGroup();
const name = this.getPluralName();
if (group && name) {
return crdResourcesURL({ params: { group, name } });
}
}
getResourceApiBase() {
const { group } = this.spec;
const { group } = this.spec ?? {};
return `/apis/${group}/${this.getVersion()}/${this.getPluralName()}`;
}
getPluralName() {
return this.getNames().plural;
return this.getNames()?.plural;
}
getResourceKind() {
return this.spec.names.kind;
return this.spec?.names.kind;
}
getResourceTitle() {
const name = this.getPluralName();
return name[0].toUpperCase() + name.substr(1);
if (name) {
return name[0].toUpperCase() + name.substr(1);
}
}
getGroup() {
return this.spec.group;
return this.spec?.group;
}
getScope() {
return this.spec.scope;
return this.spec?.scope;
}
getVersion() {
// v1 has removed the spec.version property, if it is present it must match the first version
return this.spec.versions[0]?.name ?? this.spec.version;
return this.spec?.versions[0]?.name ?? this.spec?.version;
}
isNamespaced() {
@ -111,20 +114,20 @@ export class CustomResourceDefinition extends KubeObject {
}
getStoredVersions() {
return this.status.storedVersions.join(", ");
return this.status?.storedVersions.join(", ");
}
getNames() {
return this.spec.names;
return this.spec?.names;
}
getConversion() {
return JSON.stringify(this.spec.conversion);
return JSON.stringify(this.spec?.conversion);
}
getPrinterColumns(ignorePriority = true): AdditionalPrinterColumnsV1[] {
const columns = this.spec.versions.find(a => this.getVersion() == a.name)?.additionalPrinterColumns
?? this.spec.additionalPrinterColumns?.map(({ JSONPath, ...rest }) => ({ ...rest, jsonPath: JSONPath })) // map to V1 shape
const columns = this.spec?.versions.find(a => this.getVersion() == a.name)?.additionalPrinterColumns
?? this.spec?.additionalPrinterColumns?.map(({ JSONPath, ...rest }) => ({ ...rest, jsonPath: JSONPath })) // map to V1 shape
?? [];
return columns
@ -133,13 +136,13 @@ export class CustomResourceDefinition extends KubeObject {
}
getValidation() {
return JSON.stringify(this.spec.validation ?? this.spec.versions?.[0]?.schema, null, 2);
return JSON.stringify(this.spec?.validation ?? this.spec?.versions?.[0]?.schema, null, 2);
}
getConditions() {
if (!this.status?.conditions) return [];
return this.status.conditions.map(condition => {
return this.status?.conditions.map(condition => {
const { message, reason, lastTransitionTime, status } = condition;
return {
@ -151,7 +154,7 @@ export class CustomResourceDefinition extends KubeObject {
}
}
export const crdApi = new KubeApi<CustomResourceDefinition>({
export const crdApi = new KubeApi({
objectConstructor: CustomResourceDefinition,
checkPreferredVersion: true,
});

View File

@ -5,7 +5,7 @@ import { formatDuration } from "../../utils/formatDuration";
import { autobind } from "../../utils";
import { KubeApi } from "../kube-api";
export class CronJobApi extends KubeApi<CronJob> {
export class CronJobApi extends KubeApi<CronJobSpec, CronJobStatus, CronJob> {
suspend(params: { namespace: string; name: string }) {
return this.request.patch(this.getUrl(params), {
data: {
@ -37,82 +37,72 @@ export class CronJobApi extends KubeApi<CronJob> {
}
}
interface CronJobSpec {
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;
}
interface CronJobStatus {
lastScheduleTime?: string;
}
@autobind()
export class CronJob extends KubeObject {
export class CronJob extends KubeObject<CronJobSpec, CronJobStatus> {
static kind = "CronJob";
static namespaced = true;
static apiBase = "/apis/batch/v1beta1/cronjobs";
kind: string;
apiVersion: string;
metadata: {
name: string;
namespace: string;
selfLink: string;
uid: string;
resourceVersion: string;
creationTimestamp: string;
labels: {
[key: string]: string;
};
annotations: {
[key: string]: string;
};
};
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;
};
getSuspendFlag() {
return this.spec.suspend.toString();
return this.spec?.suspend.toString();
}
getLastScheduleTime() {
if (!this.status.lastScheduleTime) return "-";
const diff = moment().diff(this.status.lastScheduleTime);
if (!this.status?.lastScheduleTime) return "-";
const diff = moment().diff(this.status?.lastScheduleTime);
return formatDuration(diff, true);
}
getSchedule() {
return this.spec.schedule;
return this.spec?.schedule;
}
isNeverRun() {
const schedule = this.getSchedule();
if (!schedule) {
return true;
}
const daysInMonth = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
const stamps = schedule.split(" ");
const day = Number(stamps[stamps.length - 3]); // 1-31
@ -124,7 +114,7 @@ export class CronJob extends KubeObject {
}
isSuspend() {
return this.spec.suspend;
return this.spec?.suspend;
}
}

View File

@ -1,68 +1,64 @@
import get from "lodash/get";
import { IPodContainer } from "./pods.api";
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
import { IAffinity, WorkloadKubeObject, WorkloadSpec } from "../workload-kube-object";
import { autobind } from "../../utils";
import { KubeApi } from "../kube-api";
export interface DaemonSetSpec extends WorkloadSpec {
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;
}
interface DaemonSetStatus {
currentNumberScheduled: number;
numberMisscheduled: number;
desiredNumberScheduled: number;
numberReady: number;
observedGeneration: number;
updatedNumberScheduled: number;
numberAvailable: number;
numberUnavailable: number;
}
@autobind()
export class DaemonSet extends WorkloadKubeObject {
export class DaemonSet extends WorkloadKubeObject<DaemonSetSpec, DaemonSetStatus> {
static kind = "DaemonSet";
static namespaced = true;
static apiBase = "/apis/apps/v1/daemonsets";
spec: {
selector: {
matchLabels: {
[name: string]: string;
};
};
template: {
metadata: {
creationTimestamp?: string;
labels: {
name: string;
};
};
spec: {
containers: IPodContainer[];
initContainers?: IPodContainer[];
restartPolicy: string;
terminationGracePeriodSeconds: number;
dnsPolicy: string;
hostPID: boolean;
affinity?: IAffinity;
nodeSelector?: {
[selector: string]: string;
};
securityContext: {};
schedulerName: string;
tolerations: {
key: string;
operator: string;
effect: string;
tolerationSeconds: number;
}[];
};
};
updateStrategy: {
type: string;
rollingUpdate: {
maxUnavailable: number;
};
};
revisionHistoryLimit: number;
};
status: {
currentNumberScheduled: number;
numberMisscheduled: number;
desiredNumberScheduled: number;
numberReady: number;
observedGeneration: number;
updatedNumberScheduled: number;
numberAvailable: number;
numberUnavailable: number;
};
getImages() {
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []);
const initContainers: IPodContainer[] = get(this, "spec.template.spec.initContainers", []);

View File

@ -1,18 +1,18 @@
import moment from "moment";
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
import { IAffinity, WorkloadKubeObject, WorkloadSpec } from "../workload-kube-object";
import { autobind } from "../../utils";
import { KubeApi } from "../kube-api";
export class DeploymentApi extends KubeApi<Deployment> {
export class DeploymentApi extends KubeApi<DeploymentSpec, DeploymentStatus, Deployment> {
protected getScaleApiUrl(params: { namespace: string; name: string }) {
return `${this.getUrl(params)}/scale`;
}
getReplicas(params: { namespace: string; name: string }): Promise<number> {
return this.request
.get(this.getScaleApiUrl(params))
.then(({ status }: any) => status?.replicas);
async getReplicas(params: { namespace: string; name: string }): Promise<number> {
const { status } = await this.request.get(this.getScaleApiUrl(params));
return status?.replicas ?? 0;
}
scale(params: { namespace: string; name: string }, replicas: number) {
@ -66,110 +66,114 @@ interface IContainerProbe {
failureThreshold?: number;
}
interface DeploymentSpec extends WorkloadSpec {
replicas: number;
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;
};
};
}
interface DeploymentStatus {
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;
}[];
}
@autobind()
export class Deployment extends WorkloadKubeObject {
export class Deployment extends WorkloadKubeObject<DeploymentSpec, DeploymentStatus> {
static kind = "Deployment";
static namespaced = true;
static apiBase = "/apis/apps/v1/deployments";
spec: {
replicas: number;
selector: { matchLabels: { [app: string]: string } };
template: {
metadata: {
creationTimestamp?: string;
labels: { [app: string]: string };
annotations?: { [app: string]: string };
};
spec: {
containers: {
name: string;
image: string;
args?: string[];
ports?: {
name: string;
containerPort: number;
protocol: string;
}[];
env?: {
name: string;
value: string;
}[];
resources: {
limits?: {
cpu: string;
memory: string;
};
requests: {
cpu: string;
memory: string;
};
};
volumeMounts?: {
name: string;
mountPath: string;
}[];
livenessProbe?: IContainerProbe;
readinessProbe?: IContainerProbe;
startupProbe?: IContainerProbe;
terminationMessagePath: string;
terminationMessagePolicy: string;
imagePullPolicy: string;
}[];
restartPolicy: string;
terminationGracePeriodSeconds: number;
dnsPolicy: string;
affinity?: IAffinity;
nodeSelector?: {
[selector: string]: string;
};
serviceAccountName: string;
serviceAccount: string;
securityContext: {};
schedulerName: string;
tolerations?: {
key: string;
operator: string;
effect: string;
tolerationSeconds: number;
}[];
volumes?: {
name: string;
configMap: {
name: string;
defaultMode: number;
optional: boolean;
};
}[];
};
};
strategy: {
type: string;
rollingUpdate: {
maxUnavailable: number;
maxSurge: number;
};
};
};
status: {
observedGeneration: number;
replicas: number;
updatedReplicas: number;
readyReplicas: number;
availableReplicas?: number;
unavailableReplicas?: number;
conditions: {
type: string;
status: string;
lastUpdateTime: string;
lastTransitionTime: string;
reason: string;
message: string;
}[];
};
getConditions(activeOnly = false) {
const { conditions } = this.status;
const { conditions } = this.status ?? {};
if (!conditions) return [];
@ -185,7 +189,7 @@ export class Deployment extends WorkloadKubeObject {
}
getReplicas() {
return this.spec.replicas || 0;
return this.spec?.replicas ?? 0;
}
}

View File

@ -36,13 +36,16 @@ export class EndpointAddress implements IEndpointAddress {
targetRef?: {
kind: string;
namespace: string;
apiVersion: string;
name: string;
uid: string;
resourceVersion: string;
};
constructor(data: IEndpointAddress) {
Object.assign(this, data);
this.hostname = data.hostname;
this.ip = data.ip;
this.nodeName = data.nodeName;
}
getId() {
@ -53,12 +56,14 @@ export class EndpointAddress implements IEndpointAddress {
return this.hostname;
}
getTargetRef(): ITargetRef {
getTargetRef(): ITargetRef | null {
if (this.targetRef) {
return Object.assign(this.targetRef, {apiVersion: "v1"});
} else {
return null;
this.targetRef.apiVersion = "v1";
return this.targetRef;
}
return null;
}
}
@ -68,7 +73,9 @@ export class EndpointSubset implements IEndpointSubset {
ports: IEndpointPort[];
constructor(data: IEndpointSubset) {
Object.assign(this, data);
this.addresses = data.addresses;
this.notReadyAddresses = data.notReadyAddresses;
this.ports = data.ports;
}
getAddresses(): EndpointAddress[] {
@ -101,27 +108,24 @@ export class EndpointSubset implements IEndpointSubset {
}
@autobind()
export class Endpoint extends KubeObject {
export class Endpoint extends KubeObject<void, void> {
static kind = "Endpoints";
static namespaced = true;
static apiBase = "/api/v1/endpoints";
subsets: IEndpointSubset[];
subsets?: IEndpointSubset[];
getEndpointSubsets(): EndpointSubset[] {
const subsets = this.subsets || [];
return subsets.map(s => new EndpointSubset(s));
return (this.subsets ?? []).map(s => new EndpointSubset(s));
}
toString(): string {
if(this.subsets) {
return this.getEndpointSubsets().map(es => es.toString()).join(", ");
} else {
return "<none>";
if (this.subsets) {
return this.getEndpointSubsets().map(String).join(", ");
}
}
return "<none>";
}
}
export const endpointApi = new KubeApi({

View File

@ -5,12 +5,12 @@ import { autobind } from "../../utils";
import { KubeApi } from "../kube-api";
@autobind()
export class KubeEvent extends KubeObject {
export class KubeEvent extends KubeObject<void, void> {
static kind = "Event";
static namespaced = true;
static apiBase = "/api/v1/events";
involvedObject: {
involvedObject?: {
kind: string;
namespace: string;
name: string;
@ -19,26 +19,26 @@ export class KubeEvent extends KubeObject {
resourceVersion: string;
fieldPath: string;
};
reason: string;
message: string;
source: {
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;
firstTimestamp?: string;
lastTimestamp?: string;
count?: number;
type?: "Normal" | "Warning" | string;
eventTime?: null;
reportingComponent?: string;
reportingInstance?: string;
isWarning() {
return this.type === "Warning";
}
getSource() {
const { component, host } = this.source;
const { component, host } = this.source ?? {};
return `${component} ${host || ""}`;
}

View File

@ -1,9 +1,33 @@
import { compile } from "path-to-regexp";
import { apiBase } from "../index";
import { stringify } from "querystring";
import { autobind } from "../../utils";
import { autobind, NotFalsy } from "../../utils";
export type RepoHelmChartList = Record<string, HelmChart[]>;
export interface HelmChartData {
apiVersion: string;
name: string;
version: string;
repo: string;
kubeVersion?: string;
created: string;
description?: string;
digest: string;
keywords?: string[];
home?: string;
sources?: string[];
maintainers?: {
name: string;
email: string;
url: string;
}[];
engine?: string;
icon?: string;
appVersion?: string;
deprecated?: boolean;
tillerVersion?: string;
}
export type RepoHelmChartList = Record<string, HelmChartData[]>;
export type HelmChartList = Record<string, RepoHelmChartList>;
export interface IHelmChartDetails {
@ -17,31 +41,27 @@ const endpoint = compile(`/v2/charts/:repo?/:name?`) as (params?: {
}) => string;
export const helmChartsApi = {
list() {
return apiBase
.get<HelmChartList>(endpoint())
.then(data => {
return Object
.values(data)
.reduce((allCharts, repoCharts) => allCharts.concat(Object.values(repoCharts)), [])
.map(([chart]) => HelmChart.create(chart));
});
async list() {
const data = await apiBase.get<HelmChartList>(endpoint());
return Object.values(data)
.flatMap(chartList => Object.values(chartList)[0])
.filter(NotFalsy)
.map(HelmChart.create);
},
get(repo: string, name: string, readmeVersion?: string) {
async get(repo: string, name: string, readmeVersion?: string) {
const path = endpoint({ repo, name });
return apiBase
.get<IHelmChartDetails>(`${path}?${stringify({ version: readmeVersion })}`)
.then(data => {
const versions = data.versions.map(HelmChart.create);
const readme = data.readme;
const data = await apiBase
.get<IHelmChartDetails>(`${path}?${stringify({ version: readmeVersion })}`);
const versions = data.versions.map(HelmChart.create);
const readme = data.readme;
return {
readme,
versions,
};
});
return {
readme,
versions,
};
},
getValues(repo: string, name: string, version: string) {
@ -51,12 +71,28 @@ export const helmChartsApi = {
};
@autobind()
export class HelmChart {
constructor(data: any) {
Object.assign(this, data);
export class HelmChart implements HelmChartData {
constructor(data: HelmChartData) {
this.apiVersion = data.apiVersion;
this.name = data.name;
this.version = data.version;
this.repo = data.repo;
this.kubeVersion = data.kubeVersion;
this.created = data.created;
this.description = data.description;
this.digest = data.digest;
this.keywords = data.keywords;
this.home = data.home;
this.sources = data.sources;
this.maintainers = data.maintainers;
this.engine = data.engine;
this.icon = data.icon;
this.appVersion = data.appVersion;
this.deprecated = data.deprecated;
this.tillerVersion = data.tillerVersion;
}
static create(data: any) {
static create(data: HelmChartData) {
return new HelmChart(data);
}

View File

@ -28,7 +28,7 @@ interface IReleaseRawDetails extends IReleasePayload {
}
export interface IReleaseDetails extends IReleasePayload {
resources: KubeObject[];
resources: KubeObject<any, any>[];
}
export interface IReleaseCreatePayload {
@ -79,7 +79,7 @@ export const helmReleasesApi = {
const path = endpoint({ name, namespace });
return apiBase.get<IReleaseRawDetails>(path).then(details => {
const items: KubeObject[] = JSON.parse(details.resources).items;
const items: KubeObject<any, any>[] = JSON.parse(details.resources).items;
const resources = items.map(item => KubeObject.create(item));
return {
@ -136,13 +136,29 @@ export const helmReleasesApi = {
}
};
export interface HelmReleaseData {
appVersion: string;
name: string;
namespace: string;
chart: string;
status: string;
updated: string;
revision: string;
}
@autobind()
export class HelmRelease implements ItemObject {
constructor(data: any) {
Object.assign(this, data);
constructor(data: HelmReleaseData) {
this.appVersion = data.appVersion;
this.name = data.name;
this.namespace = data.namespace;
this.chart = data.chart;
this.status = data.status;
this.updated = data.updated;
this.revision = data.revision;
}
static create(data: any) {
static create(data: HelmReleaseData) {
return new HelmRelease(data);
}

View File

@ -38,50 +38,51 @@ export interface IHpaMetric {
}>;
}
export class HorizontalPodAutoscaler extends KubeObject {
interface HorizontalPodAutoscalerSpec {
scaleTargetRef: {
kind: string;
name: string;
apiVersion: string;
};
minReplicas: number;
maxReplicas: number;
metrics: IHpaMetric[];
}
interface HorizontalPodAutoscalerStatus {
currentReplicas: number;
desiredReplicas: number;
currentMetrics: IHpaMetric[];
conditions: {
lastTransitionTime: string;
message: string;
reason: string;
status: string;
type: string;
}[];
}
export class HorizontalPodAutoscaler extends KubeObject<HorizontalPodAutoscalerSpec, HorizontalPodAutoscalerStatus> {
static kind = "HorizontalPodAutoscaler";
static namespaced = true;
static apiBase = "/apis/autoscaling/v2beta1/horizontalpodautoscalers";
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;
}[];
};
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;
}
getConditions() {
if (!this.status.conditions) return [];
if (!this.status?.conditions) return [];
return this.status.conditions.map(condition => {
return this.status?.conditions.map(condition => {
const { message, reason, lastTransitionTime, status } = condition;
return {
@ -93,23 +94,23 @@ export class HorizontalPodAutoscaler extends KubeObject {
}
getMetrics() {
return this.spec.metrics || [];
return this.spec?.metrics || [];
}
getCurrentMetrics() {
return this.status.currentMetrics || [];
return this.status?.currentMetrics || [];
}
protected getMetricName(metric: IHpaMetric): string {
protected getMetricName(metric: IHpaMetric): string | undefined {
const { type, resource, pods, object, external } = metric;
switch (type) {
case HpaMetricType.Resource:
return resource.name;
return resource?.name;
case HpaMetricType.Pods:
return pods.metricName;
case HpaMetricType.Object:
return object.metricName;
return object?.metricName;
case HpaMetricType.External:
return external.metricName;
}

View File

@ -3,7 +3,7 @@ import { autobind } from "../../utils";
import { IMetrics, metricsApi } from "./metrics.api";
import { KubeApi } from "../kube-api";
export class IngressApi extends KubeApi<Ingress> {
export class IngressApi extends KubeApi<IngressSpec, IngressStatus, Ingress> {
getMetrics(ingress: string, namespace: string): Promise<IIngressMetrics> {
const opts = { category: "ingress", ingress };
@ -61,44 +61,45 @@ export const getBackendServiceNamePort = (backend: IIngressBackend) => {
return { serviceName, servicePort };
};
interface IngressSpec {
tls: {
secretName: string;
}[];
rules?: {
host?: string;
http: {
paths: {
path?: string;
backend: IIngressBackend;
}[];
};
}[];
// extensions/v1beta1
backend?: IExtensionsBackend;
// networking.k8s.io/v1
defaultBackend?: INetworkingBackend & {
resource: {
apiGroup: string;
kind: string;
name: string;
};
};
}
interface IngressStatus {
loadBalancer: {
ingress?: ILoadBalancerIngress[];
};
}
@autobind()
export class Ingress extends KubeObject {
export class Ingress extends KubeObject<IngressSpec, IngressStatus> {
static kind = "Ingress";
static namespaced = true;
static apiBase = "/apis/networking.k8s.io/v1/ingresses";
spec: {
tls: {
secretName: string;
}[];
rules?: {
host?: string;
http: {
paths: {
path?: string;
backend: IIngressBackend;
}[];
};
}[];
// extensions/v1beta1
backend?: IExtensionsBackend;
// networking.k8s.io/v1
defaultBackend?: INetworkingBackend & {
resource: {
apiGroup: string;
kind: string;
name: string;
}
}
};
status: {
loadBalancer: {
ingress: ILoadBalancerIngress[];
};
};
getRoutes() {
const { spec: { tls, rules } } = this;
const { spec: { tls, rules } = {} } = this;
if (!rules) return [];
@ -135,7 +136,7 @@ export class Ingress extends KubeObject {
}
getHosts() {
const { spec: { rules } } = this;
const { spec: { rules } = {} } = this;
if (!rules) return [];
@ -144,7 +145,7 @@ 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)
@ -166,11 +167,9 @@ 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
));
)) ?? [];
}
}
@ -179,5 +178,4 @@ export const ingressApi = new IngressApi({
// Add fallback for Kubernetes <1.19
checkPreferredVersion: true,
fallbackApiBases: ["/apis/extensions/v1beta1/ingresses"],
logStuff: true
} as any);
});

View File

@ -1,95 +1,87 @@
import get from "lodash/get";
import { autobind } from "../../utils";
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
import { IAffinity, WorkloadKubeObject, WorkloadSpec } from "../workload-kube-object";
import { IPodContainer } from "./pods.api";
import { KubeApi } from "../kube-api";
import { JsonApiParams } from "../json-api";
interface JobSpec extends WorkloadSpec {
parallelism?: number;
completions?: number;
backoffLimit?: number;
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;
}
interface JobStatus {
conditions: {
type: string;
status: string;
lastProbeTime: string;
lastTransitionTime: string;
message?: string;
}[];
startTime: string;
completionTime: string;
succeeded: number;
}
@autobind()
export class Job extends WorkloadKubeObject {
export class Job extends WorkloadKubeObject<JobSpec, JobStatus> {
static kind = "Job";
static namespaced = true;
static apiBase = "/apis/batch/v1/jobs";
spec: {
parallelism?: number;
completions?: number;
backoffLimit?: number;
selector?: {
matchLabels: {
[name: string]: string;
};
};
template: {
metadata: {
creationTimestamp?: string;
labels?: {
[name: string]: string;
};
annotations?: {
[name: string]: string;
};
};
spec: {
containers: IPodContainer[];
restartPolicy: string;
terminationGracePeriodSeconds: number;
dnsPolicy: string;
hostPID: boolean;
affinity?: IAffinity;
nodeSelector?: {
[selector: string]: string;
};
tolerations?: {
key: string;
operator: string;
effect: string;
tolerationSeconds: number;
}[];
schedulerName: string;
};
};
containers?: IPodContainer[];
restartPolicy?: string;
terminationGracePeriodSeconds?: number;
dnsPolicy?: string;
serviceAccountName?: string;
serviceAccount?: string;
schedulerName?: string;
};
status: {
conditions: {
type: string;
status: string;
lastProbeTime: string;
lastTransitionTime: string;
message?: string;
}[];
startTime: string;
completionTime: string;
succeeded: number;
};
getDesiredCompletions() {
return this.spec.completions || 0;
return this.spec?.completions || 0;
}
getCompletions() {
return this.status.succeeded || 0;
return this.status?.succeeded || 0;
}
getParallelism() {
return this.spec.parallelism;
return this.spec?.parallelism;
}
getCondition() {
// Type of Job condition could be only Complete or Failed
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.14/#jobcondition-v1-batch
const { conditions } = this.status;
if (!conditions) return;
return conditions.find(({ status }) => status === "True");
return this.status?.conditions.find(({ status }) => status === "True");
}
getImages() {

View File

@ -29,26 +29,26 @@ export interface LimitRangeItem extends LimitRangeParts {
type: string
}
interface LimitRangeSpec {
limits: LimitRangeItem[];
}
@autobind()
export class LimitRange extends KubeObject {
export class LimitRange extends KubeObject<LimitRangeSpec> {
static kind = "LimitRange";
static namespaced = true;
static apiBase = "/api/v1/limitranges";
spec: {
limits: LimitRangeItem[];
};
getContainerLimits() {
return this.spec.limits.filter(limit => limit.type === LimitType.CONTAINER);
return this.spec?.limits.filter(limit => limit.type === LimitType.CONTAINER);
}
getPodLimits() {
return this.spec.limits.filter(limit => limit.type === LimitType.POD);
return this.spec?.limits.filter(limit => limit.type === LimitType.POD);
}
getPVCLimits() {
return this.spec.limits.filter(limit => limit.type === LimitType.PVC);
return this.spec?.limits.filter(limit => limit.type === LimitType.PVC);
}
}

View File

@ -14,8 +14,8 @@ export interface IMetrics {
export interface IMetricsResult {
metric: {
[name: string]: string;
instance: string;
[name: string]: string | undefined;
instance?: string;
node?: string;
pod?: string;
kubernetes?: string;
@ -111,12 +111,11 @@ export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics {
return metrics;
}
export function isMetricsEmpty(metrics: { [key: string]: IMetrics }) {
export function isMetricsEmpty(metrics: Record<string, IMetrics>) {
return Object.values(metrics).every(metric => !metric?.data?.result?.length);
}
export function getItemMetrics(metrics: { [key: string]: IMetrics }, itemName: string): { [key: string]: IMetrics } {
if (!metrics) return;
export function getItemMetrics(metrics: Record<string, IMetrics> = {}, itemName: string): Record<string, IMetrics> {
const itemMetrics = { ...metrics };
for (const metric in metrics) {
@ -132,7 +131,7 @@ export function getItemMetrics(metrics: { [key: string]: IMetrics }, itemName: s
return itemMetrics;
}
export function getMetricLastPoints(metrics: { [key: string]: IMetrics }) {
export function getMetricLastPoints(metrics: Record<string, IMetrics>) {
const result: Partial<{ [metric: string]: number }> = {};
Object.keys(metrics).forEach(metricName => {

View File

@ -7,18 +7,18 @@ export enum NamespaceStatus {
TERMINATING = "Terminating",
}
export interface NamespaceKubeStatus {
phase: string;
}
@autobind()
export class Namespace extends KubeObject {
export class Namespace extends KubeObject<void, NamespaceKubeStatus> {
static kind = "Namespace";
static namespaced = false;
static apiBase = "/api/v1/namespaces";
status?: {
phase: string;
};
getStatus() {
return this.status ? this.status.phase : "-";
return this.status?.phase ?? "-";
}
}

View File

@ -35,36 +35,32 @@ export interface IPolicyEgress {
}[];
}
interface NetworkPolicySpec {
podSelector: {
matchLabels: {
[label: string]: string;
role: string;
};
};
policyTypes: string[];
ingress: IPolicyIngress[];
egress: IPolicyEgress[];
}
@autobind()
export class NetworkPolicy extends KubeObject {
export class NetworkPolicy extends KubeObject<NetworkPolicySpec> {
static kind = "NetworkPolicy";
static namespaced = true;
static apiBase = "/apis/networking.k8s.io/v1/networkpolicies";
spec: {
podSelector: {
matchLabels: {
[label: string]: string;
role: string;
};
};
policyTypes: string[];
ingress: IPolicyIngress[];
egress: IPolicyEgress[];
};
getMatchLabels(): string[] {
if (!this.spec.podSelector || !this.spec.podSelector.matchLabels) return [];
return Object
.entries(this.spec.podSelector.matchLabels)
.entries(this.spec?.podSelector?.matchLabels ?? {})
.map(data => data.join(":"));
}
getTypes(): string[] {
if (!this.spec.policyTypes) return [];
return this.spec.policyTypes;
return this.spec?.policyTypes ?? [];
}
}

View File

@ -1,9 +1,9 @@
import { KubeObject } from "../kube-object";
import { autobind, cpuUnitsToNumber, unitsToBytes } from "../../utils";
import { autobind, cpuUnitsToNumber, NotFalsy, unitsToBytes } from "../../utils";
import { IMetrics, metricsApi } from "./metrics.api";
import { KubeApi } from "../kube-api";
export class NodesApi extends KubeApi<Node> {
export class NodesApi extends KubeApi<NodeSpec, NodeStatus, Node> {
getMetrics(): Promise<INodeMetrics> {
const opts = { category: "nodes"};
@ -28,85 +28,86 @@ export interface INodeMetrics<T = IMetrics> {
fsSize: T;
}
interface NodeSpec {
podCIDR: string;
externalID: string;
taints?: {
key: string;
value: string;
effect: string;
}[];
unschedulable?: boolean;
}
interface NodeStatus {
capacity: {
cpu: string;
memory: string;
pods: string;
};
allocatable: {
cpu: string;
memory: string;
pods: string;
};
conditions: {
type: string;
status?: string;
lastHeartbeatTime?: string;
lastTransitionTime?: string;
reason?: string;
message?: string;
}[];
addresses: {
type: string;
address: string;
}[];
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;
}[];
}
@autobind()
export class Node extends KubeObject {
export class Node extends KubeObject<NodeSpec, NodeStatus> {
static kind = "Node";
static namespaced = false;
static apiBase = "/api/v1/nodes";
spec: {
podCIDR: string;
externalID: string;
taints?: {
key: string;
value: string;
effect: string;
}[];
unschedulable?: boolean;
};
status: {
capacity: {
cpu: string;
memory: string;
pods: string;
};
allocatable: {
cpu: string;
memory: string;
pods: string;
};
conditions: {
type: string;
status?: string;
lastHeartbeatTime?: string;
lastTransitionTime?: string;
reason?: string;
message?: string;
}[];
addresses: {
type: string;
address: string;
}[];
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;
}[];
};
getNodeConditionText() {
const { conditions } = this.status;
if (this.status?.conditions) {
return this.status.conditions
.filter(condition => condition.status === "True")
.map(condition => condition.type)
.join(" ");
}
if (!conditions) return "";
return conditions.reduce((types, current) => {
if (current.status !== "True") return "";
return types += ` ${current.type}`;
}, "");
return "";
}
getTaints() {
return this.spec.taints || [];
return this.spec?.taints ?? [];
}
getRoleLabels() {
const roleLabels = Object.keys(this.metadata.labels).filter(key =>
key.includes("node-role.kubernetes.io")
).map(key => key.match(/([^/]+$)/)[0]); // all after last slash
const roleLabels = Object.keys(this.metadata.labels ?? {})
.filter(key => key.includes("node-role.kubernetes.io"))
.map(key => key.match(/([^/]+$)/)?.[0])
.filter(NotFalsy); // all after last slash
if (this.metadata.labels["kubernetes.io/role"] != undefined) {
if (this.metadata.labels?.["kubernetes.io/role"]) {
roleLabels.push(this.metadata.labels["kubernetes.io/role"]);
}
@ -114,19 +115,19 @@ export class Node extends KubeObject {
}
getCpuCapacity() {
if (!this.status.capacity || !this.status.capacity.cpu) return 0;
if (!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.memory) return 0;
return unitsToBytes(this.status.capacity.memory);
}
getConditions() {
const conditions = this.status.conditions || [];
const conditions = this.status?.conditions ?? [];
if (this.isUnschedulable()) {
return [{ type: "SchedulingDisabled", status: "True" }, ...conditions];
@ -148,21 +149,17 @@ export class Node extends KubeObject {
}
getKubeletVersion() {
return this.status.nodeInfo.kubeletVersion;
return this.status?.nodeInfo.kubeletVersion;
}
getOperatingSystem(): string {
const label = this.getLabels().find(label => label.startsWith("kubernetes.io/os="));
if (label) {
return label.split("=", 2)[1];
}
return "linux";
return label?.split("=", 2)[1] ?? "linux";
}
isUnschedulable() {
return this.spec.unschedulable;
return this.spec?.unschedulable ?? false;
}
}

View File

@ -4,7 +4,7 @@ import { IMetrics, metricsApi } from "./metrics.api";
import { Pod } from "./pods.api";
import { KubeApi } from "../kube-api";
export class PersistentVolumeClaimsApi extends KubeApi<PersistentVolumeClaim> {
export class PersistentVolumeClaimsApi extends KubeApi<PersistentVolumeClaimSpec, PersistentVolumeClaimStatus, PersistentVolumeClaim> {
getMetrics(pvcName: string, namespace: string): Promise<IPvcMetrics> {
return metricsApi.getMetrics({
diskUsage: { category: "pvc", pvc: pvcName },
@ -21,35 +21,36 @@ export interface IPvcMetrics<T = IMetrics> {
diskCapacity: T;
}
interface PersistentVolumeClaimSpec {
accessModes: string[];
storageClassName: string;
selector: {
matchLabels: {
release: string;
};
matchExpressions: {
key: string; // environment,
operator: string; // In,
values: string[]; // [dev]
}[];
};
resources: {
requests: {
storage: string; // 8Gi
};
};
}
interface PersistentVolumeClaimStatus {
phase: string; // Pending
}
@autobind()
export class PersistentVolumeClaim extends KubeObject {
export class PersistentVolumeClaim extends KubeObject<PersistentVolumeClaimSpec, PersistentVolumeClaimStatus> {
static kind = "PersistentVolumeClaim";
static namespaced = true;
static apiBase = "/api/v1/persistentvolumeclaims";
spec: {
accessModes: string[];
storageClassName: string;
selector: {
matchLabels: {
release: string;
};
matchExpressions: {
key: string; // environment,
operator: string; // In,
values: string[]; // [dev]
}[];
};
resources: {
requests: {
storage: string; // 8Gi
};
};
};
status: {
phase: string; // Pending
};
getPods(allPods: Pod[]): Pod[] {
const pods = allPods.filter(pod => pod.getNs() === this.getNs());
@ -62,28 +63,20 @@ export class PersistentVolumeClaim extends KubeObject {
}
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 ?? "-";
}
}

View File

@ -3,76 +3,72 @@ import { unitsToBytes } from "../../utils/convertMemory";
import { autobind } from "../../utils";
import { KubeApi } from "../kube-api";
interface PersistentVolumeSpec {
capacity: {
storage: string; // 8Gi
};
flexVolume: {
driver: string;
options: {
clusterNamespace: string;
image: string;
pool: string;
storageClass: string;
};
};
mountOptions?: string[];
accessModes: string[];
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;
storageClassName: string;
nfs?: {
path: string;
server: string;
};
}
interface PersistentVolumeStatus {
phase: string;
reason?: string;
}
@autobind()
export class PersistentVolume extends KubeObject {
export class PersistentVolume extends KubeObject<PersistentVolumeSpec, PersistentVolumeStatus> {
static kind = "PersistentVolume";
static namespaced = false;
static apiBase = "/api/v1/persistentvolumes";
spec: {
capacity: {
storage: string; // 8Gi
};
flexVolume: {
driver: string; // ceph.rook.io/rook-ceph-system,
options: {
clusterNamespace: string; // rook-ceph,
image: string; // pvc-c5d7c485-9f1b-11e8-b0ea-9600000e54fb,
pool: string; // replicapool,
storageClass: string; // rook-ceph-block
};
};
mountOptions?: string[];
accessModes: string[]; // [ReadWriteOnce]
claimRef: {
kind: string; // PersistentVolumeClaim,
namespace: string; // storage,
name: string; // nfs-provisioner,
uid: string; // c5d7c485-9f1b-11e8-b0ea-9600000e54fb,
apiVersion: string; // v1,
resourceVersion: string; // 292180
};
persistentVolumeReclaimPolicy: string; // Delete,
storageClassName: string; // rook-ceph-block
nfs?: {
path: string;
server: string;
};
};
status: {
phase: string;
reason?: string;
};
getCapacity(inBytes = false) {
const capacity = this.spec.capacity;
const storage = this.spec?.capacity?.storage ?? "0";
if (capacity) {
if (inBytes) return unitsToBytes(capacity.storage);
return capacity.storage;
if (inBytes) {
return unitsToBytes(storage);
}
return 0;
return storage;
}
getStatus() {
if (!this.status) return;
return this.status.phase || "-";
return this.status?.phase ?? "-";
}
getStorageClass(): string {
return this.spec.storageClassName;
return this.spec?.storageClassName ?? "";
}
getClaimRefName(): string {
return this.spec.claimRef?.name ?? "";
return this.spec?.claimRef?.name ?? "";
}
getStorageClassName() {
return this.spec.storageClassName || "";
return this.spec?.storageClassName ?? "";
}
}

View File

@ -1,14 +1,14 @@
import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api";
export class PodMetrics extends KubeObject {
export class PodMetrics extends KubeObject<void, void> {
static kind = "Pod";
static namespaced = true;
static apiBase = "/apis/metrics.k8s.io/v1beta1/pods";
timestamp: string;
window: string;
containers: {
timestamp?: string;
window?: string;
containers?: {
name: string;
usage: {
cpu: string;

View File

@ -1,45 +1,44 @@
import { autobind } from "../../utils";
import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api";
import { WorkloadKubeObject, WorkloadSpec } from "../workload-kube-object";
interface PodDisruptionBudgetSpec extends WorkloadSpec {
minAvailable: string;
maxUnavailable: string;
}
interface PodDisruptionBudgetStatus {
currentHealthy: number;
desiredHealthy: number;
disruptionsAllowed: number;
expectedPods: number;
}
@autobind()
export class PodDisruptionBudget extends KubeObject {
export class PodDisruptionBudget extends WorkloadKubeObject<PodDisruptionBudgetSpec, PodDisruptionBudgetStatus> {
static kind = "PodDisruptionBudget";
static namespaced = true;
static apiBase = "/apis/policy/v1beta1/poddisruptionbudgets";
spec: {
minAvailable: string;
maxUnavailable: string;
selector: { matchLabels: { [app: string]: string } };
};
status: {
currentHealthy: number
desiredHealthy: number
disruptionsAllowed: number
expectedPods: number
};
getSelectors() {
const selector = this.spec.selector;
return KubeObject.stringifyLabels(selector ? selector.matchLabels : null);
return KubeObject.stringifyLabels(this.spec?.selector?.matchLabels);
}
getMinAvailable() {
return this.spec.minAvailable || "N/A";
return this.spec?.minAvailable || "N/A";
}
getMaxUnavailable() {
return this.spec.maxUnavailable || "N/A";
return this.spec?.maxUnavailable || "N/A";
}
getCurrentHealthy() {
return this.status.currentHealthy;
return this.status?.currentHealthy ?? 0;
}
getDesiredHealthy() {
return this.status.desiredHealthy;
return this.status?.desiredHealthy ?? 0;
}
}

View File

@ -1,9 +1,10 @@
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
import { IAffinity, WorkloadKubeObject, WorkloadSpec } from "../workload-kube-object";
import { autobind } from "../../utils";
import { IMetrics, metricsApi } from "./metrics.api";
import { KubeApi } from "../kube-api";
import { Primitive } from "type-fest";
export class PodsApi extends KubeApi<Pod> {
export class PodsApi extends KubeApi<PodSpec, PodKubeStatus, Pod> {
async getLogs(params: { namespace: string; name: string }, query?: IPodLogsQuery): Promise<string> {
const path = `${this.getUrl(params)}/log`;
@ -142,7 +143,7 @@ interface IContainerProbe {
export interface IPodContainerStatus {
name: string;
state?: {
[index: string]: object;
[index: string]: Record<string, Primitive> | undefined;
running?: {
startedAt: string;
};
@ -158,7 +159,7 @@ export interface IPodContainerStatus {
};
};
lastState?: {
[index: string]: object;
[index: string]: Record<string, Primitive> | undefined;
running?: {
startedAt: string;
};
@ -181,92 +182,93 @@ export interface IPodContainerStatus {
started?: boolean;
}
interface PodSpec extends WorkloadSpec {
volumes?: {
name: string;
persistentVolumeClaim: {
claimName: string;
};
emptyDir: {
medium?: string;
sizeLimit?: string;
};
configMap: {
name: string;
};
secret: {
secretName: string;
defaultMode: number;
};
}[];
initContainers: IPodContainer[];
containers: IPodContainer[];
restartPolicy?: string;
terminationGracePeriodSeconds?: number;
activeDeadlineSeconds?: number;
dnsPolicy?: string;
serviceAccountName: string;
serviceAccount: string;
automountServiceAccountToken?: boolean;
priority?: number;
priorityClassName?: string;
nodeName?: string;
nodeSelector?: {
[selector: string]: string;
};
securityContext?: {};
imagePullSecrets?: {
name: string;
}[];
hostNetwork?: boolean;
hostPID?: boolean;
hostIPC?: boolean;
shareProcessNamespace?: boolean;
hostname?: string;
subdomain?: string;
schedulerName?: string;
tolerations?: {
key?: string;
operator?: string;
effect?: string;
tolerationSeconds?: number;
value?: string;
}[];
hostAliases?: {
ip: string;
hostnames: string[];
};
affinity?: IAffinity;
}
interface PodKubeStatus {
phase: string;
conditions: {
type: string;
status: string;
lastProbeTime: number;
lastTransitionTime: string;
}[];
hostIP: string;
podIP: string;
startTime: string;
initContainerStatuses?: IPodContainerStatus[];
containerStatuses?: IPodContainerStatus[];
qosClass?: string;
reason?: string;
}
@autobind()
export class Pod extends WorkloadKubeObject {
export class Pod extends WorkloadKubeObject<PodSpec, PodKubeStatus> {
static kind = "Pod";
static namespaced = true;
static apiBase = "/api/v1/pods";
spec: {
volumes?: {
name: string;
persistentVolumeClaim: {
claimName: string;
};
emptyDir: {
medium?: string;
sizeLimit?: string;
};
configMap: {
name: string;
};
secret: {
secretName: string;
defaultMode: number;
};
}[];
initContainers: IPodContainer[];
containers: IPodContainer[];
restartPolicy?: string;
terminationGracePeriodSeconds?: number;
activeDeadlineSeconds?: number;
dnsPolicy?: string;
serviceAccountName: string;
serviceAccount: string;
automountServiceAccountToken?: boolean;
priority?: number;
priorityClassName?: string;
nodeName?: string;
nodeSelector?: {
[selector: string]: string;
};
securityContext?: {};
imagePullSecrets?: {
name: string;
}[];
hostNetwork?: boolean;
hostPID?: boolean;
hostIPC?: boolean;
shareProcessNamespace?: boolean;
hostname?: string;
subdomain?: string;
schedulerName?: string;
tolerations?: {
key?: string;
operator?: string;
effect?: string;
tolerationSeconds?: number;
value?: string;
}[];
hostAliases?: {
ip: string;
hostnames: string[];
};
affinity?: IAffinity;
};
status?: {
phase: string;
conditions: {
type: string;
status: string;
lastProbeTime: number;
lastTransitionTime: string;
}[];
hostIP: string;
podIP: 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() {
@ -276,7 +278,7 @@ export class Pod extends WorkloadKubeObject {
getRunningContainers() {
const runningContainerNames = new Set(
this.getContainerStatuses()
.filter(({ state }) => state.running)
.filter(({ state }) => state?.running)
.map(({ name }) => name)
);
@ -309,7 +311,7 @@ export class Pod extends WorkloadKubeObject {
}
getPriorityClassName() {
return this.spec.priorityClassName || "";
return this.spec?.priorityClassName || "";
}
getStatus(): PodStatus {
@ -347,12 +349,12 @@ export class Pod extends WorkloadKubeObject {
const statuses = this.getContainerStatuses(false); // not including initContainers
for (const { state } of statuses.reverse()) {
if (state.waiting) {
return state.waiting.reason || "Waiting";
if (state?.waiting) {
return state?.waiting.reason || "Waiting";
}
if (state.terminated) {
return state.terminated.reason || "Terminated";
if (state?.terminated) {
return state?.terminated.reason || "Terminated";
}
}
@ -368,7 +370,7 @@ export class Pod extends WorkloadKubeObject {
}
getVolumes() {
return this.spec.volumes || [];
return this.spec?.volumes || [];
}
getSecrets(): string[] {
@ -378,17 +380,16 @@ export class Pod extends WorkloadKubeObject {
}
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;
return this.spec?.affinity ?? {};
}
hasIssues() {
@ -419,8 +420,11 @@ export class Pod extends WorkloadKubeObject {
return this.getProbe(container.startupProbe);
}
getProbe(probeData: IContainerProbe) {
if (!probeData) return [];
getProbe(probeData?: IContainerProbe) {
if (!probeData) {
return [];
}
const {
httpGet, exec, tcpSocket, initialDelaySeconds, timeoutSeconds,
periodSeconds, successThreshold, failureThreshold
@ -458,11 +462,11 @@ export class Pod extends WorkloadKubeObject {
}
getNodeName() {
return this.spec.nodeName;
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"];
}
}

View File

@ -2,89 +2,101 @@ import { autobind } from "../../utils";
import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api";
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;
}[];
};
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[];
}
@autobind()
export class PodSecurityPolicy extends KubeObject {
export class PodSecurityPolicy extends KubeObject<PodSecurityPolicySpec> {
static kind = "PodSecurityPolicy";
static namespaced = false;
static apiBase = "/apis/policy/v1beta1/podsecuritypolicies";
spec: {
allowPrivilegeEscalation?: boolean;
allowedCSIDrivers?: {
name: string;
}[];
allowedCapabilities: string[];
allowedFlexVolumes?: {
driver: string;
}[];
allowedHostPaths?: {
pathPrefix: string;
readOnly: boolean;
}[];
allowedProcMountTypes?: string[];
allowedUnsafeSysctls?: string[];
defaultAddCapabilities?: string[];
defaultAllowPrivilegeEscalation?: boolean;
forbiddenSysctls?: string[];
fsGroup?: {
rule: string;
ranges: { max: number; min: number }[];
};
hostIPC?: boolean;
hostNetwork?: boolean;
hostPID?: boolean;
hostPorts?: {
max: number;
min: number;
}[];
privileged?: boolean;
readOnlyRootFilesystem?: boolean;
requiredDropCapabilities?: string[];
runAsGroup?: {
ranges: { max: number; min: number }[];
rule: string;
};
runAsUser?: {
rule: string;
ranges: { max: number; min: number }[];
};
runtimeClass?: {
allowedRuntimeClassNames: string[];
defaultRuntimeClassName: string;
};
seLinux?: {
rule: string;
seLinuxOptions: {
level: string;
role: string;
type: string;
user: string;
};
};
supplementalGroups?: {
rule: string;
ranges: { max: number; min: number }[];
};
volumes?: string[];
};
isPrivileged() {
return !!this.spec.privileged;
return this.spec?.privileged ?? false;
}
getVolumes() {
return this.spec.volumes || [];
return this.spec?.volumes ?? [];
}
getRules() {
const { fsGroup, runAsGroup, runAsUser, supplementalGroups, seLinux } = this.spec;
const { fsGroup, runAsGroup, runAsUser, supplementalGroups, seLinux } = this.spec ?? {};
return {
fsGroup: fsGroup ? fsGroup.rule : "",
runAsGroup: runAsGroup ? runAsGroup.rule : "",
runAsUser: runAsUser ? runAsUser.rule : "",
supplementalGroups: supplementalGroups ? supplementalGroups.rule : "",
seLinux: seLinux ? seLinux.rule : "",
fsGroup: fsGroup?.rule ?? "",
runAsGroup: runAsGroup?.rule ?? "",
runAsUser: runAsUser?.rule ?? "",
supplementalGroups: supplementalGroups?.rule ?? "",
seLinux: seLinux?.rule ?? "",
};
}
}

View File

@ -1,10 +1,9 @@
import get from "lodash/get";
import { autobind } from "../../utils";
import { WorkloadKubeObject } from "../workload-kube-object";
import { IPodContainer, Pod } from "./pods.api";
import { WorkloadKubeObject, WorkloadSpec } from "../workload-kube-object";
import { Pod } from "./pods.api";
import { KubeApi } from "../kube-api";
export class ReplicaSetApi extends KubeApi<ReplicaSet> {
export class ReplicaSetApi extends KubeApi<ReplicaSetSpec, ReplicaSetStatus, ReplicaSet> {
protected getScaleApiUrl(params: { namespace: string; name: string }) {
return `${this.getUrl(params)}/scale`;
}
@ -27,56 +26,55 @@ export class ReplicaSetApi extends KubeApi<ReplicaSet> {
}
}
interface ReplicaSetSpec extends WorkloadSpec {
replicas?: number;
template?: {
metadata: {
labels: {
app: string;
};
};
spec?: Pod["spec"];
};
minReadySeconds?: number;
}
interface ReplicaSetStatus {
replicas: number;
fullyLabeledReplicas?: number;
readyReplicas?: number;
availableReplicas?: number;
observedGeneration?: number;
conditions?: {
type: string;
status: string;
lastUpdateTime: string;
lastTransitionTime: string;
reason: string;
message: string;
}[];
}
@autobind()
export class ReplicaSet extends WorkloadKubeObject {
export class ReplicaSet extends WorkloadKubeObject<ReplicaSetSpec, ReplicaSetStatus> {
static kind = "ReplicaSet";
static namespaced = true;
static apiBase = "/apis/apps/v1/replicasets";
spec: {
replicas?: number;
selector: { matchLabels: { [app: string]: string } };
template?: {
metadata: {
labels: {
app: string;
};
};
spec?: Pod["spec"];
};
minReadySeconds?: number;
};
status: {
replicas: number;
fullyLabeledReplicas?: number;
readyReplicas?: number;
availableReplicas?: number;
observedGeneration?: number;
conditions?: {
type: string;
status: string;
lastUpdateTime: string;
lastTransitionTime: string;
reason: string;
message: string;
}[];
};
getDesired() {
return this.spec.replicas || 0;
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", []);
return [...containers].map(container => container.image);
return this.spec?.template?.spec?.containers?.map(container => container.image) ?? [];
}
}

View File

@ -1,6 +1,5 @@
import jsYaml from "js-yaml";
import { KubeObject } from "../kube-object";
import { KubeJsonApiData } from "../kube-json-api";
import { apiBase } from "../index";
import { apiManager } from "../api-manager";
@ -9,25 +8,22 @@ export const resourceApplierApi = {
"kubectl.kubernetes.io/last-applied-configuration"
],
async update<D extends KubeObject>(resource: object | string): Promise<D> {
async update<Spec, Status, D extends KubeObject<Spec, Status>>(resource: object | string): Promise<D | D[]> {
if (typeof resource === "string") {
resource = jsYaml.safeLoad(resource);
}
return apiBase
.post<KubeJsonApiData[]>("/stack", { data: resource })
.then(data => {
const items = data.map(obj => {
const api = apiManager.getApiByKind(obj.kind, obj.apiVersion);
const data = await apiBase.post<D[]>("/stack", { data: resource });
const items = data.map(obj => {
const api = apiManager.getApiByKind(obj.kind, obj.apiVersion);
if (api) {
return new api.objectConstructor(obj);
} else {
return new KubeObject(obj);
}
});
if (api) {
return new api.objectConstructor(obj) as D;
}
return items.length === 1 ? items[0] : items;
});
return new KubeObject(obj) as D;
});
return items.length === 1 ? items[0] : items;
}
};

View File

@ -1,9 +1,8 @@
import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api";
import { KubeJsonApiData } from "../kube-json-api";
export interface IResourceQuotaValues {
[quota: string]: string;
[quota: string]: string | undefined;
// Compute Resource Quota
"limits.cpu"?: string;
@ -30,36 +29,29 @@ export interface IResourceQuotaValues {
"count/deployments.extensions"?: string;
}
export class ResourceQuota extends KubeObject {
interface ResourceQuotaSpec {
hard: IResourceQuotaValues;
scopeSelector?: {
matchExpressions: {
operator: string;
scopeName: string;
values: string[];
}[];
};
}
interface ResourceQuotaStatus {
hard: IResourceQuotaValues;
used: IResourceQuotaValues;
}
export class ResourceQuota extends KubeObject<ResourceQuotaSpec, ResourceQuotaStatus> {
static kind = "ResourceQuota";
static namespaced = true;
static apiBase = "/api/v1/resourcequotas";
constructor(data: KubeJsonApiData) {
super(data);
this.spec = this.spec || {} as any;
}
spec: {
hard: IResourceQuotaValues;
scopeSelector?: {
matchExpressions: {
operator: string;
scopeName: string;
values: string[];
}[];
};
};
status: {
hard: IResourceQuotaValues;
used: IResourceQuotaValues;
};
getScopeSelector() {
const { matchExpressions = [] } = this.spec.scopeSelector || {};
return matchExpressions;
return this.spec?.scopeSelector?.matchExpressions ?? [];
}
}

View File

@ -10,13 +10,13 @@ export interface IRoleBindingSubject {
}
@autobind()
export class RoleBinding extends KubeObject {
export class RoleBinding extends KubeObject<void, void> {
static kind = "RoleBinding";
static namespaced = true;
static apiBase = "/apis/rbac.authorization.k8s.io/v1/rolebindings";
subjects?: IRoleBindingSubject[];
roleRef: {
roleRef?: {
kind: string;
name: string;
apiGroup?: string;

View File

@ -1,12 +1,12 @@
import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api";
export class Role extends KubeObject {
export class Role extends KubeObject<void, void> {
static kind = "Role";
static namespaced = true;
static apiBase = "/apis/rbac.authorization.k8s.io/v1/roles";
rules: {
rules?: {
verbs: string[];
apiGroups: string[];
resources: string[];
@ -14,7 +14,7 @@ export class Role extends KubeObject {
}[];
getRules() {
return this.rules || [];
return this.rules ?? [];
}
}

View File

@ -1,5 +1,4 @@
import { KubeObject } from "../kube-object";
import { KubeJsonApiData } from "../kube-json-api";
import { autobind } from "../../utils";
import { KubeApi } from "../kube-api";
@ -20,21 +19,16 @@ export interface ISecretRef {
}
@autobind()
export class Secret extends KubeObject {
export class Secret extends KubeObject<void, void> {
static kind = "Secret";
static namespaced = true;
static apiBase = "/api/v1/secrets";
type: SecretType;
type?: SecretType;
data: {
[prop: string]: string;
[prop: string]: string | undefined;
token?: string;
};
constructor(data: KubeJsonApiData) {
super(data);
this.data = this.data || {};
}
} = {};
getKeys(): string[] {
return Object.keys(this.data);

View File

@ -1,14 +1,13 @@
import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api";
export class SelfSubjectRulesReviewApi extends KubeApi<SelfSubjectRulesReview> {
export class SelfSubjectRulesReviewApi extends KubeApi<SelfSubjectRulesReviewSpec, SelfSubjectRulesReviewStatus, SelfSubjectRulesReview> {
create({ namespace = "default" }): Promise<SelfSubjectRulesReview> {
return super.create({}, {
spec: {
namespace
},
}
);
});
}
}
@ -20,32 +19,28 @@ export interface ISelfSubjectReviewRule {
nonResourceURLs?: string[];
}
export class SelfSubjectRulesReview extends KubeObject {
interface SelfSubjectRulesReviewSpec {
// todo: add more types from api docs
namespace?: string;
}
interface SelfSubjectRulesReviewStatus {
resourceRules: ISelfSubjectReviewRule[];
nonResourceRules: ISelfSubjectReviewRule[];
incomplete: boolean;
}
export class SelfSubjectRulesReview extends KubeObject<SelfSubjectRulesReviewSpec, SelfSubjectRulesReviewStatus> {
static kind = "SelfSubjectRulesReview";
static namespaced = false;
static apiBase = "/apis/authorization.k8s.io/v1/selfsubjectrulesreviews";
spec: {
// todo: add more types from api docs
namespace?: string;
};
status: {
resourceRules: ISelfSubjectReviewRule[];
nonResourceRules: ISelfSubjectReviewRule[];
incomplete: boolean;
};
getResourceRules() {
const rules = this.status && this.status.resourceRules || [];
return rules.map(rule => this.normalize(rule));
return this.status?.resourceRules.map(rule => this.normalize(rule)) ?? [];
}
getNonResourceRules() {
const rules = this.status && this.status.nonResourceRules || [];
return rules.map(rule => this.normalize(rule));
return this.status?.nonResourceRules.map(rule => this.normalize(rule)) ?? [];
}
protected normalize(rule: ISelfSubjectReviewRule): ISelfSubjectReviewRule {

View File

@ -3,7 +3,7 @@ import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api";
@autobind()
export class ServiceAccount extends KubeObject {
export class ServiceAccount extends KubeObject<void, void> {
static kind = "ServiceAccount";
static namespaced = true;
static apiBase = "/api/v1/serviceaccounts";
@ -24,6 +24,6 @@ export class ServiceAccount extends KubeObject {
}
}
export const serviceAccountsApi = new KubeApi<ServiceAccount>({
export const serviceAccountsApi = new KubeApi({
objectConstructor: ServiceAccount,
});

View File

@ -7,6 +7,7 @@ export interface IServicePort {
protocol: string;
port: number;
targetPort: number;
nodePort?: number;
}
export class ServicePort implements IServicePort {
@ -17,7 +18,11 @@ export class ServicePort implements IServicePort {
nodePort?: number;
constructor(data: IServicePort) {
Object.assign(this, data);
this.name = data.name;
this.protocol = data.protocol;
this.port = data.port;
this.targetPort = data.targetPort;
this.nodePort = data.nodePort;
}
toString() {
@ -29,64 +34,60 @@ export class ServicePort implements IServicePort {
}
}
interface ServiceSpec {
type: string;
clusterIP: string;
externalTrafficPolicy?: string;
loadBalancerIP?: string;
sessionAffinity: string;
selector: {
[key: string]: string;
};
ports: ServicePort[];
externalIPs?: string[]; // https://kubernetes.io/docs/concepts/services-networking/service/#external-ips
}
interface ServiceStatus {
loadBalancer?: {
ingress?: {
ip?: string;
hostname?: string;
}[];
};
}
@autobind()
export class Service extends KubeObject {
export class Service extends KubeObject<ServiceSpec, ServiceStatus> {
static kind = "Service";
static namespaced = true;
static apiBase = "/api/v1/services";
spec: {
type: string;
clusterIP: string;
externalTrafficPolicy?: string;
loadBalancerIP?: string;
sessionAffinity: string;
selector: { [key: string]: string };
ports: ServicePort[];
externalIPs?: string[]; // https://kubernetes.io/docs/concepts/services-networking/service/#external-ips
};
status: {
loadBalancer?: {
ingress?: {
ip?: string;
hostname?: string;
}[];
};
};
getClusterIp() {
return this.spec.clusterIP;
return this.spec?.clusterIP;
}
getExternalIps() {
const lb = this.getLoadBalancer();
if (lb && lb.ingress) {
return lb.ingress.map(val => val.ip || val.hostname);
}
return this.spec.externalIPs || [];
return this.getLoadBalancer()
?.ingress
?.map(val => val.ip || val.hostname)
?? this.spec?.externalIPs
?? [];
}
getType() {
return this.spec.type || "-";
return this.spec?.type || "-";
}
getSelector(): string[] {
if (!this.spec.selector) return [];
return Object.entries(this.spec.selector).map(val => val.join("="));
return Object.entries(this.spec?.selector ?? {}).map(val => val.join("="));
}
getPorts(): ServicePort[] {
const ports = this.spec.ports || [];
return ports.map(p => new ServicePort(p));
return this.spec?.ports.map(p => new ServicePort(p)) ?? [];
}
getLoadBalancer() {
return this.status.loadBalancer;
return this.status?.loadBalancer ?? {};
}
isActive() {

View File

@ -1,10 +1,8 @@
import get from "lodash/get";
import { IPodContainer } from "./pods.api";
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
import { IAffinity, WorkloadKubeObject, WorkloadSpec } from "../workload-kube-object";
import { autobind } from "../../utils";
import { KubeApi } from "../kube-api";
export class StatefulSetApi extends KubeApi<StatefulSet> {
export class StatefulSetApi extends KubeApi<StatefulSetSpec, StatefulSetStatus, StatefulSet> {
protected getScaleApiUrl(params: { namespace: string; name: string }) {
return `${this.getUrl(params)}/scale`;
}
@ -27,83 +25,77 @@ export class StatefulSetApi extends KubeApi<StatefulSet> {
}
}
interface StatefulSetSpec extends WorkloadSpec {
serviceName: string;
replicas: number;
template: {
metadata: {
labels: {
app: string;
};
};
spec: {
containers: {
name: string;
image: string;
ports: {
containerPort: number;
name: string;
}[];
volumeMounts: {
name: string;
mountPath: string;
}[];
}[];
affinity?: IAffinity;
nodeSelector?: {
[selector: string]: string;
};
tolerations: {
key: string;
operator: string;
effect: string;
tolerationSeconds: number;
}[];
};
};
volumeClaimTemplates: {
metadata: {
name: string;
};
spec: {
accessModes: string[];
resources: {
requests: {
storage: string;
};
};
};
}[];
}
interface StatefulSetStatus {
observedGeneration: number;
replicas: number;
currentReplicas: number;
readyReplicas: number;
currentRevision: string;
updateRevision: string;
collisionCount: number;
}
@autobind()
export class StatefulSet extends WorkloadKubeObject {
export class StatefulSet extends WorkloadKubeObject<StatefulSetSpec, StatefulSetStatus> {
static kind = "StatefulSet";
static namespaced = true;
static apiBase = "/apis/apps/v1/statefulsets";
spec: {
serviceName: string;
replicas: number;
selector: {
matchLabels: {
[key: string]: string;
};
};
template: {
metadata: {
labels: {
app: string;
};
};
spec: {
containers: {
name: string;
image: string;
ports: {
containerPort: number;
name: string;
}[];
volumeMounts: {
name: string;
mountPath: string;
}[];
}[];
affinity?: IAffinity;
nodeSelector?: {
[selector: string]: string;
};
tolerations: {
key: string;
operator: string;
effect: string;
tolerationSeconds: number;
}[];
};
};
volumeClaimTemplates: {
metadata: {
name: string;
};
spec: {
accessModes: string[];
resources: {
requests: {
storage: string;
};
};
};
}[];
};
status: {
observedGeneration: number;
replicas: number;
currentReplicas: number;
readyReplicas: number;
currentRevision: string;
updateRevision: string;
collisionCount: number;
};
getReplicas() {
return this.spec.replicas || 0;
return this.spec?.replicas ?? 0;
}
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) ?? [];
}
}

View File

@ -3,16 +3,16 @@ import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api";
@autobind()
export class StorageClass extends KubeObject {
export class StorageClass extends KubeObject<void, void> {
static kind = "StorageClass";
static namespaced = false;
static apiBase = "/apis/storage.k8s.io/v1/storageclasses";
provisioner: string; // e.g. "storage.k8s.io/v1"
provisioner?: string; // e.g. "storage.k8s.io/v1"
mountOptions?: string[];
volumeBindingMode: string;
reclaimPolicy: string;
parameters: {
volumeBindingMode?: string;
reclaimPolicy?: string;
parameters?: {
[param: string]: string; // every provisioner has own set of these parameters
};

View File

@ -7,7 +7,7 @@ export const apiBase = new JsonApi({
apiBase: apiPrefix,
debug: isDevelopment || isDebugging,
});
export const apiKube = new KubeJsonApi({
export const apiKube = new KubeJsonApi<any, any>({
apiBase: apiKubePrefix,
debug: isDevelopment,
});

View File

@ -114,7 +114,7 @@ export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
reqUrl += (reqUrl.includes("?") ? "&" : "?") + queryString;
}
const infoLog: JsonApiLog = {
method: reqInit.method.toUpperCase(),
method: reqInit.method?.toUpperCase() ?? "<unknown>",
reqUrl,
reqInit,
};

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