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:
parent
6c7095fdec
commit
b2616d4cf2
@ -27,7 +27,7 @@ export type CatalogEntityMetadata = {
|
||||
labels: {
|
||||
[key: string]: string;
|
||||
}
|
||||
[key: string]: string | object;
|
||||
[key: string]: string | object | undefined;
|
||||
};
|
||||
|
||||
export type CatalogEntityStatus = {
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
@ -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"
|
||||
);
|
||||
) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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`);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
@ -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";
|
||||
|
||||
20
src/common/utils/remove-falsy.ts
Normal file
20
src/common/utils/remove-falsy.ts
Normal 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;
|
||||
}
|
||||
@ -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 */
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -76,7 +76,7 @@ export class ExtensionInstaller {
|
||||
});
|
||||
let stderr = "";
|
||||
|
||||
child.stderr.on("data", data => {
|
||||
child.stderr?.on("data", data => {
|
||||
stderr += String(data);
|
||||
});
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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[] = [];
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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");
|
||||
|
||||
|
||||
@ -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))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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("");
|
||||
|
||||
@ -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");
|
||||
|
||||
|
||||
@ -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};
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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 != "") {
|
||||
|
||||
@ -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: []})};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@ -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")!;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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, {});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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]) => {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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", {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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", []);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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 || ""}`;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 => {
|
||||
|
||||
@ -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 ?? "-";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 ?? "-";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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"];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 ?? "",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@ -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 ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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) ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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
Loading…
Reference in New Issue
Block a user