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: {
|
labels: {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
}
|
}
|
||||||
[key: string]: string | object;
|
[key: string]: string | object | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CatalogEntityStatus = {
|
export type CatalogEntityStatus = {
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { saveToAppFiles } from "./utils/saveToAppFiles";
|
|||||||
import { KubeConfig } from "@kubernetes/client-node";
|
import { KubeConfig } from "@kubernetes/client-node";
|
||||||
import { handleRequest, requestMain, subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc";
|
import { handleRequest, requestMain, subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc";
|
||||||
import { ResourceType } from "../renderer/components/+cluster-settings/components/cluster-metrics-setting";
|
import { ResourceType } from "../renderer/components/+cluster-settings/components/cluster-metrics-setting";
|
||||||
|
import { assert } from "./utils";
|
||||||
|
|
||||||
export interface ClusterIconUpload {
|
export interface ClusterIconUpload {
|
||||||
clusterId: string;
|
clusterId: string;
|
||||||
@ -51,7 +52,7 @@ export interface ClusterModel {
|
|||||||
workspace?: string;
|
workspace?: string;
|
||||||
|
|
||||||
/** User context in kubeconfig */
|
/** User context in kubeconfig */
|
||||||
contextName?: string;
|
contextName: string;
|
||||||
|
|
||||||
/** Preferences */
|
/** Preferences */
|
||||||
preferences?: ClusterPreferences;
|
preferences?: ClusterPreferences;
|
||||||
@ -106,7 +107,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@observable activeCluster: ClusterId;
|
@observable activeCluster: ClusterId | null = null;
|
||||||
@observable removedClusters = observable.map<ClusterId, Cluster>();
|
@observable removedClusters = observable.map<ClusterId, Cluster>();
|
||||||
@observable clusters = observable.map<ClusterId, Cluster>();
|
@observable clusters = observable.map<ClusterId, Cluster>();
|
||||||
|
|
||||||
@ -218,14 +219,14 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
setActive(clusterId: ClusterId) {
|
setActive(clusterId?: ClusterId | null) {
|
||||||
const cluster = this.clusters.get(clusterId);
|
const cluster = this.getById(clusterId);
|
||||||
|
|
||||||
if (!cluster?.enabled) {
|
if (!clusterId || !cluster?.enabled) {
|
||||||
clusterId = null;
|
this.activeCluster = null;
|
||||||
|
} else {
|
||||||
|
this.activeCluster = clusterId;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.activeCluster = clusterId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deactivate(id: ClusterId) {
|
deactivate(id: ClusterId) {
|
||||||
@ -238,8 +239,8 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
return this.clusters.size > 0;
|
return this.clusters.size > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
getById(id: ClusterId): Cluster | null {
|
getById(id?: ClusterId | null): Cluster | null {
|
||||||
return this.clusters.get(id) ?? null;
|
return (id ? this.clusters.get(id) : null) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@ -304,7 +305,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
let cluster = currentClusters.get(clusterModel.id);
|
let cluster = currentClusters.get(clusterModel.id);
|
||||||
|
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
cluster.updateModel(clusterModel);
|
Object.assign(cluster, clusterModel);
|
||||||
} else {
|
} else {
|
||||||
cluster = new Cluster(clusterModel);
|
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.clusters.replace(newClusters);
|
||||||
this.removedClusters.replace(removedClusters);
|
this.removedClusters.replace(removedClusters);
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON(): ClusterStoreModel {
|
toJSON(): ClusterStoreModel {
|
||||||
return toJS({
|
return toJS({
|
||||||
activeCluster: this.activeCluster,
|
activeCluster: this.activeCluster ?? undefined,
|
||||||
clusters: this.clustersList.map(cluster => cluster.toJSON()),
|
clusters: this.clustersList.map(cluster => cluster.toJSON()),
|
||||||
}, {
|
}, {
|
||||||
recurseEverything: true
|
recurseEverything: true
|
||||||
@ -354,6 +355,10 @@ export function getHostedClusterId() {
|
|||||||
return getClusterIdFromHost(location.host);
|
return getClusterIdFromHost(location.host);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getHostedCluster(): Cluster {
|
export function getHostedCluster(): Cluster | null {
|
||||||
return clusterStore.getById(getHostedClusterId());
|
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 logger from "../main/logger";
|
||||||
import commandExists from "command-exists";
|
import commandExists from "command-exists";
|
||||||
import { ExecValidationNotFoundError } from "./custom-errors";
|
import { ExecValidationNotFoundError } from "./custom-errors";
|
||||||
|
import { NotFalsy } from "./utils";
|
||||||
|
|
||||||
export type KubeConfigValidationOpts = {
|
export type KubeConfigValidationOpts = {
|
||||||
validateCluster?: boolean;
|
validateCluster?: boolean;
|
||||||
@ -26,10 +27,12 @@ function resolveTilde(filePath: string) {
|
|||||||
export function loadConfig(pathOrContent?: string): KubeConfig {
|
export function loadConfig(pathOrContent?: string): KubeConfig {
|
||||||
const kc = new KubeConfig();
|
const kc = new KubeConfig();
|
||||||
|
|
||||||
if (fse.pathExistsSync(pathOrContent)) {
|
if (pathOrContent) {
|
||||||
kc.loadFromFile(path.resolve(resolveTilde(pathOrContent)));
|
if (fse.pathExistsSync(pathOrContent)) {
|
||||||
} else {
|
kc.loadFromFile(path.resolve(resolveTilde(pathOrContent)));
|
||||||
kc.loadFromString(pathOrContent);
|
} else {
|
||||||
|
kc.loadFromString(pathOrContent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return kc;
|
return kc;
|
||||||
@ -75,9 +78,9 @@ export function splitConfig(kubeConfig: KubeConfig): KubeConfig[] {
|
|||||||
kubeConfig.contexts.forEach(ctx => {
|
kubeConfig.contexts.forEach(ctx => {
|
||||||
const kc = new KubeConfig();
|
const kc = new KubeConfig();
|
||||||
|
|
||||||
kc.clusters = [kubeConfig.getCluster(ctx.cluster)].filter(n => n);
|
kc.clusters = [kubeConfig.getCluster(ctx.cluster)].filter(NotFalsy);
|
||||||
kc.users = [kubeConfig.getUser(ctx.user)].filter(n => n);
|
kc.users = [kubeConfig.getUser(ctx.user)].filter(NotFalsy);
|
||||||
kc.contexts = [kubeConfig.getContextObject(ctx.name)].filter(n => n);
|
kc.contexts = [kubeConfig.getContextObject(ctx.name)].filter(NotFalsy);
|
||||||
kc.setCurrentContext(ctx.name);
|
kc.setCurrentContext(ctx.name);
|
||||||
|
|
||||||
configs.push(kc);
|
configs.push(kc);
|
||||||
@ -92,43 +95,37 @@ export function dumpConfigYaml(kubeConfig: Partial<KubeConfig>): string {
|
|||||||
kind: "Config",
|
kind: "Config",
|
||||||
preferences: {},
|
preferences: {},
|
||||||
"current-context": kubeConfig.currentContext,
|
"current-context": kubeConfig.currentContext,
|
||||||
clusters: kubeConfig.clusters.map(cluster => {
|
clusters: kubeConfig.clusters?.map(cluster => ({
|
||||||
return {
|
name: cluster.name,
|
||||||
name: cluster.name,
|
cluster: {
|
||||||
cluster: {
|
"certificate-authority-data": cluster.caData,
|
||||||
"certificate-authority-data": cluster.caData,
|
"certificate-authority": cluster.caFile,
|
||||||
"certificate-authority": cluster.caFile,
|
server: cluster.server,
|
||||||
server: cluster.server,
|
"insecure-skip-tls-verify": cluster.skipTLSVerify
|
||||||
"insecure-skip-tls-verify": cluster.skipTLSVerify
|
}
|
||||||
}
|
})),
|
||||||
};
|
contexts: kubeConfig.contexts?.map(context => ({
|
||||||
}),
|
name: context.name,
|
||||||
contexts: kubeConfig.contexts.map(context => {
|
context: {
|
||||||
return {
|
cluster: context.cluster,
|
||||||
name: context.name,
|
user: context.user,
|
||||||
context: {
|
namespace: context.namespace
|
||||||
cluster: context.cluster,
|
}
|
||||||
user: context.user,
|
})),
|
||||||
namespace: context.namespace
|
users: kubeConfig.users?.map(user => ({
|
||||||
}
|
name: user.name,
|
||||||
};
|
user: {
|
||||||
}),
|
"client-certificate-data": user.certData,
|
||||||
users: kubeConfig.users.map(user => {
|
"client-certificate": user.certFile,
|
||||||
return {
|
"client-key-data": user.keyData,
|
||||||
name: user.name,
|
"client-key": user.keyFile,
|
||||||
user: {
|
"auth-provider": user.authProvider,
|
||||||
"client-certificate-data": user.certData,
|
exec: user.exec,
|
||||||
"client-certificate": user.certFile,
|
token: user.token,
|
||||||
"client-key-data": user.keyData,
|
username: user.username,
|
||||||
"client-key": user.keyFile,
|
password: user.password
|
||||||
"auth-provider": user.authProvider,
|
}
|
||||||
exec: user.exec,
|
}))
|
||||||
token: user.token,
|
|
||||||
username: user.username,
|
|
||||||
password: user.password
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})
|
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.debug("Dumping KubeConfig:", config);
|
logger.debug("Dumping KubeConfig:", config);
|
||||||
@ -139,21 +136,21 @@ export function dumpConfigYaml(kubeConfig: Partial<KubeConfig>): string {
|
|||||||
|
|
||||||
export function podHasIssues(pod: V1Pod) {
|
export function podHasIssues(pod: V1Pod) {
|
||||||
// Logic adapted from dashboard
|
// 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 condition.type == "Ready" && condition.status !== "True";
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
notReady ||
|
notReady ||
|
||||||
pod.status.phase !== "Running" ||
|
pod.status?.phase !== "Running" ||
|
||||||
pod.spec.priority > 500000 // We're interested in high prio pods events regardless of their running status
|
(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) {
|
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"
|
c.status.toLowerCase() === "true" && c.type !== "Ready" && c.type !== "HostUpgrades"
|
||||||
);
|
) ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -31,6 +31,8 @@ export class RoutingError extends Error {
|
|||||||
return "no extension ID";
|
return "no extension ID";
|
||||||
case RoutingErrorType.MISSING_EXTENSION:
|
case RoutingErrorType.MISSING_EXTENSION:
|
||||||
return "extension not found";
|
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
|
* 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)
|
* @param url the url (in its current state)
|
||||||
*/
|
*/
|
||||||
protected _route(routes: [string, RouteHandler][], url: Url, extensionName?: string): void {
|
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
|
// open at system start-up
|
||||||
reaction(() => this.preferences.openAtLogin, openAtLogin => {
|
reaction(() => this.preferences.openAtLogin, openAtLogin => {
|
||||||
app.setLoginItemSettings({
|
app.setLoginItemSettings({
|
||||||
openAtLogin,
|
openAtLogin,
|
||||||
openAsHidden: true,
|
openAsHidden: true,
|
||||||
args: ["--hidden"]
|
args: ["--hidden"]
|
||||||
@ -102,11 +102,11 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
|||||||
|
|
||||||
@action
|
@action
|
||||||
setHiddenTableColumns(tableId: string, names: Set<string> | string[]) {
|
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> {
|
getHiddenTableColumns(tableId: string): Set<string> {
|
||||||
return new Set(this.preferences.hiddenTableColumns[tableId]);
|
return new Set(this.preferences.hiddenTableColumns?.[tableId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
|||||||
@ -4,8 +4,13 @@ type Constructor<T = {}> = new (...args: any[]) => T;
|
|||||||
|
|
||||||
export function autobind() {
|
export function autobind() {
|
||||||
return function (target: Constructor | object, prop?: string, descriptor?: PropertyDescriptor) {
|
return function (target: Constructor | object, prop?: string, descriptor?: PropertyDescriptor) {
|
||||||
if (target instanceof Function) return bindClass(target);
|
if (target instanceof Function) {
|
||||||
else return bindMethod(target, prop, descriptor);
|
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") {
|
if (!descriptor || typeof descriptor.value !== "function") {
|
||||||
throw new Error(`@autobind() must be used on class or method only`);
|
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 "./camelCase";
|
||||||
export * from "./cloneJson";
|
export * from "./cloneJson";
|
||||||
export * from "./delay";
|
export * from "./delay";
|
||||||
export * from "./debouncePromise";
|
|
||||||
export * from "./defineGlobal";
|
export * from "./defineGlobal";
|
||||||
export * from "./getRandId";
|
export * from "./getRandId";
|
||||||
export * from "./splitArray";
|
export * from "./splitArray";
|
||||||
@ -19,3 +18,4 @@ export * from "./downloadFile";
|
|||||||
export * from "./escapeRegExp";
|
export * from "./escapeRegExp";
|
||||||
export * from "./tar";
|
export * from "./tar";
|
||||||
export * from "./type-narrowing";
|
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 {
|
export interface ClusterFeatureStatus {
|
||||||
/** feature's current version, as set by the implementation */
|
/** feature's current version, as set by the implementation */
|
||||||
currentVersion: string;
|
currentVersion: string | null;
|
||||||
/** feature's latest version, as set by the implementation */
|
/** 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 */
|
/** whether the feature is installed or not, as set by the implementation */
|
||||||
installed: boolean;
|
installed: boolean;
|
||||||
/** whether the feature can be upgraded or not, as set by the implementation */
|
/** 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 os from "os";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc";
|
import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc";
|
||||||
|
import { assert, NotFalsy } from "../common/utils";
|
||||||
import { getBundledExtensions } from "../common/utils/app-version";
|
import { getBundledExtensions } from "../common/utils/app-version";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
import { extensionInstaller, PackageJson } from "./extension-installer";
|
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
|
* - "remove": When extension is removed. The event is of type LensExtensionId
|
||||||
*/
|
*/
|
||||||
export class ExtensionDiscovery {
|
export class ExtensionDiscovery {
|
||||||
protected bundledFolderPath: string;
|
protected bundledFolderPath?: string;
|
||||||
|
|
||||||
private loadStarted = false;
|
private loadStarted = false;
|
||||||
private extensions: Map<string, InstalledExtension> = new Map();
|
private extensions: Map<string, InstalledExtension> = new Map();
|
||||||
@ -136,7 +137,7 @@ export class ExtensionDiscovery {
|
|||||||
depth: 1,
|
depth: 1,
|
||||||
ignoreInitial: true,
|
ignoreInitial: true,
|
||||||
// Try to wait until the file has been completely copied.
|
// 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: {
|
awaitWriteFinish: {
|
||||||
// Wait 300ms until the file size doesn't change to consider the file written.
|
// 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.
|
// For a small file like package.json this should be plenty of time.
|
||||||
@ -236,7 +237,7 @@ export class ExtensionDiscovery {
|
|||||||
/**
|
/**
|
||||||
* Uninstalls extension.
|
* Uninstalls extension.
|
||||||
* The application will detect the folder unlink and remove the extension from the UI automatically.
|
* 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) {
|
async uninstallExtension({ absolutePath, manifest }: InstalledExtension) {
|
||||||
logger.info(`${logModule} Uninstalling ${manifest.name}`);
|
logger.info(`${logModule} Uninstalling ${manifest.name}`);
|
||||||
@ -260,7 +261,6 @@ export class ExtensionDiscovery {
|
|||||||
// fs.remove won't throw if path is missing
|
// fs.remove won't throw if path is missing
|
||||||
await fs.remove(path.join(extensionInstaller.extensionPackagesRoot, "package-lock.json"));
|
await fs.remove(path.join(extensionInstaller.extensionPackagesRoot, "package-lock.json"));
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Verify write access to static/extensions, which is needed for symlinking
|
// Verify write access to static/extensions, which is needed for symlinking
|
||||||
await fs.access(this.inTreeFolderPath, fs.constants.W_OK);
|
await fs.access(this.inTreeFolderPath, fs.constants.W_OK);
|
||||||
@ -314,16 +314,11 @@ export class ExtensionDiscovery {
|
|||||||
* Returns InstalledExtension from path to package.json file.
|
* Returns InstalledExtension from path to package.json file.
|
||||||
* Also updates this.packagesJson.
|
* Also updates this.packagesJson.
|
||||||
*/
|
*/
|
||||||
protected async getByManifest(manifestPath: string, { isBundled = false }: {
|
protected async getByManifest(manifestPath: string, { isBundled = false }: { isBundled?: boolean; } = {}): Promise<InstalledExtension | null> {
|
||||||
isBundled?: boolean;
|
let manifestJson: LensExtensionManifest | undefined = undefined;
|
||||||
} = {}): Promise<InstalledExtension | null> {
|
|
||||||
let manifestJson: LensExtensionManifest;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// check manifest file for existence
|
manifestJson = __non_webpack_require__(manifestPath) as LensExtensionManifest;
|
||||||
fs.accessSync(manifestPath, fs.constants.F_OK);
|
|
||||||
|
|
||||||
manifestJson = __non_webpack_require__(manifestPath);
|
|
||||||
const installedManifestPath = this.getInstalledManifestPath(manifestJson.name);
|
const installedManifestPath = this.getInstalledManifestPath(manifestJson.name);
|
||||||
|
|
||||||
const isEnabled = isBundled || extensionsStore.isEnabled(installedManifestPath);
|
const isEnabled = isBundled || extensionsStore.isEnabled(installedManifestPath);
|
||||||
@ -380,8 +375,8 @@ export class ExtensionDiscovery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async loadBundledExtensions() {
|
async loadBundledExtensions() {
|
||||||
|
const folderPath = assert(this.bundledFolderPath, "load() must be called before loadBundledExtensions()");
|
||||||
const extensions: InstalledExtension[] = [];
|
const extensions: InstalledExtension[] = [];
|
||||||
const folderPath = this.bundledFolderPath;
|
|
||||||
const bundledExtensions = getBundledExtensions();
|
const bundledExtensions = getBundledExtensions();
|
||||||
const paths = await fs.readdir(folderPath);
|
const paths = await fs.readdir(folderPath);
|
||||||
|
|
||||||
|
|||||||
@ -76,7 +76,7 @@ export class ExtensionInstaller {
|
|||||||
});
|
});
|
||||||
let stderr = "";
|
let stderr = "";
|
||||||
|
|
||||||
child.stderr.on("data", data => {
|
child.stderr?.on("data", data => {
|
||||||
stderr += String(data);
|
stderr += String(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { EventEmitter } from "events";
|
|||||||
import { isEqual } from "lodash";
|
import { isEqual } from "lodash";
|
||||||
import { action, computed, observable, reaction, toJS, when } from "mobx";
|
import { action, computed, observable, reaction, toJS, when } from "mobx";
|
||||||
import path from "path";
|
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 { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
import type { InstalledExtension } from "./extension-discovery";
|
import type { InstalledExtension } from "./extension-discovery";
|
||||||
@ -188,14 +188,16 @@ export class ExtensionLoader {
|
|||||||
|
|
||||||
loadOnMain() {
|
loadOnMain() {
|
||||||
logger.debug(`${logModule}: load on main`);
|
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
|
// Each .add returns a function to remove the item
|
||||||
const removeItems = [
|
const removeItems = [
|
||||||
registries.menuRegistry.add(extension.appMenus)
|
registries.menuRegistry.add(mainExt.appMenus)
|
||||||
];
|
];
|
||||||
|
|
||||||
this.events.on("remove", (removedExtension: LensRendererExtension) => {
|
this.events.on("remove", (removedExtension: LensRendererExtension) => {
|
||||||
if (removedExtension.id === extension.id) {
|
if (removedExtension.id === mainExt.id) {
|
||||||
removeItems.forEach(remove => {
|
removeItems.forEach(remove => {
|
||||||
remove();
|
remove();
|
||||||
});
|
});
|
||||||
@ -208,17 +210,19 @@ export class ExtensionLoader {
|
|||||||
|
|
||||||
loadOnClusterManagerRenderer() {
|
loadOnClusterManagerRenderer() {
|
||||||
logger.debug(`${logModule}: load on main renderer (cluster manager)`);
|
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 = [
|
const removeItems = [
|
||||||
registries.globalPageRegistry.add(extension.globalPages, extension),
|
registries.globalPageRegistry.add(rendererExt.globalPages, rendererExt),
|
||||||
registries.globalPageMenuRegistry.add(extension.globalPageMenus, extension),
|
registries.globalPageMenuRegistry.add(rendererExt.globalPageMenus, rendererExt),
|
||||||
registries.appPreferenceRegistry.add(extension.appPreferences),
|
registries.appPreferenceRegistry.add(rendererExt.appPreferences),
|
||||||
registries.statusBarRegistry.add(extension.statusBarItems),
|
registries.statusBarRegistry.add(rendererExt.statusBarItems),
|
||||||
registries.commandRegistry.add(extension.commands),
|
registries.commandRegistry.add(rendererExt.commands),
|
||||||
];
|
];
|
||||||
|
|
||||||
this.events.on("remove", (removedExtension: LensRendererExtension) => {
|
this.events.on("remove", (removedExtension: LensRendererExtension) => {
|
||||||
if (removedExtension.id === extension.id) {
|
if (removedExtension.id === rendererExt.id) {
|
||||||
removeItems.forEach(remove => {
|
removeItems.forEach(remove => {
|
||||||
remove();
|
remove();
|
||||||
});
|
});
|
||||||
@ -231,24 +235,26 @@ export class ExtensionLoader {
|
|||||||
|
|
||||||
loadOnClusterRenderer() {
|
loadOnClusterRenderer() {
|
||||||
logger.debug(`${logModule}: load on cluster renderer (dashboard)`);
|
logger.debug(`${logModule}: load on cluster renderer (dashboard)`);
|
||||||
const cluster = getHostedCluster();
|
const cluster = getHostedClusterStrict();
|
||||||
|
|
||||||
this.autoInitExtensions(async (extension: LensRendererExtension) => {
|
this.autoInitExtensions(async extension => {
|
||||||
if (await extension.isEnabledForCluster(cluster) === false) {
|
const rendererExt = extension as LensRendererExtension;
|
||||||
|
|
||||||
|
if (await rendererExt.isEnabledForCluster(cluster) === false) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeItems = [
|
const removeItems = [
|
||||||
registries.clusterPageRegistry.add(extension.clusterPages, extension),
|
registries.clusterPageRegistry.add(rendererExt.clusterPages, rendererExt),
|
||||||
registries.clusterPageMenuRegistry.add(extension.clusterPageMenus, extension),
|
registries.clusterPageMenuRegistry.add(rendererExt.clusterPageMenus, rendererExt),
|
||||||
registries.kubeObjectMenuRegistry.add(extension.kubeObjectMenuItems),
|
registries.kubeObjectMenuRegistry.add(rendererExt.kubeObjectMenuItems),
|
||||||
registries.kubeObjectDetailRegistry.add(extension.kubeObjectDetailItems),
|
registries.kubeObjectDetailRegistry.add(rendererExt.kubeObjectDetailItems),
|
||||||
registries.kubeObjectStatusRegistry.add(extension.kubeObjectStatusTexts),
|
registries.kubeObjectStatusRegistry.add(rendererExt.kubeObjectStatusTexts),
|
||||||
registries.commandRegistry.add(extension.commands),
|
registries.commandRegistry.add(rendererExt.commands),
|
||||||
];
|
];
|
||||||
|
|
||||||
this.events.on("remove", (removedExtension: LensRendererExtension) => {
|
this.events.on("remove", (removedExtension: LensRendererExtension) => {
|
||||||
if (removedExtension.id === extension.id) {
|
if (removedExtension.id === rendererExt.id) {
|
||||||
removeItems.forEach(remove => {
|
removeItems.forEach(remove => {
|
||||||
remove();
|
remove();
|
||||||
});
|
});
|
||||||
@ -289,7 +295,7 @@ export class ExtensionLoader {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected requireExtension(extension: InstalledExtension): LensExtensionConstructor {
|
protected requireExtension(extension: InstalledExtension): LensExtensionConstructor | undefined {
|
||||||
let extEntrypoint = "";
|
let extEntrypoint = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -314,7 +320,7 @@ export class ExtensionLoader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getExtension(extId: LensExtensionId): InstalledExtension {
|
getExtension(extId: LensExtensionId): InstalledExtension | undefined {
|
||||||
return this.extensions.get(extId);
|
return this.extensions.get(extId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import { BaseStore } from "../common/base-store";
|
import { BaseStore } from "../common/base-store";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import { LensExtension } from "./lens-extension";
|
import { LensExtension } from "./lens-extension";
|
||||||
|
import { assert } from "../common/utils";
|
||||||
|
|
||||||
export abstract class ExtensionStore<T> extends BaseStore<T> {
|
export abstract class ExtensionStore<T> extends BaseStore<T> {
|
||||||
protected extension: LensExtension;
|
protected extension?: LensExtension;
|
||||||
|
|
||||||
async loadExtension(extension: LensExtension) {
|
async loadExtension(extension: LensExtension) {
|
||||||
this.extension = extension;
|
this.extension = extension;
|
||||||
@ -18,6 +19,8 @@ export abstract class ExtensionStore<T> extends BaseStore<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected cwd() {
|
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";
|
import { CommandRegistration } from "./registries/command-registry";
|
||||||
|
|
||||||
export class LensRendererExtension extends LensExtension {
|
export class LensRendererExtension extends LensExtension {
|
||||||
globalPages: PageRegistration[] = [];
|
globalPages: PageRegistration<any>[] = [];
|
||||||
clusterPages: PageRegistration[] = [];
|
clusterPages: PageRegistration<any>[] = [];
|
||||||
globalPageMenus: PageMenuRegistration[] = [];
|
globalPageMenus: PageMenuRegistration<any>[] = [];
|
||||||
clusterPageMenus: ClusterPageMenuRegistration[] = [];
|
clusterPageMenus: ClusterPageMenuRegistration<any>[] = [];
|
||||||
kubeObjectStatusTexts: KubeObjectStatusRegistration[] = [];
|
kubeObjectStatusTexts: KubeObjectStatusRegistration[] = [];
|
||||||
appPreferences: AppPreferenceRegistration[] = [];
|
appPreferences: AppPreferenceRegistration[] = [];
|
||||||
statusBarItems: StatusBarRegistration[] = [];
|
statusBarItems: StatusBarRegistration[] = [];
|
||||||
|
|||||||
@ -18,7 +18,7 @@ export interface CommandRegistration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class CommandRegistry extends BaseRegistry<CommandRegistration> {
|
export class CommandRegistry extends BaseRegistry<CommandRegistration> {
|
||||||
@observable activeEntity: CatalogEntity;
|
@observable activeEntity?: CatalogEntity;
|
||||||
|
|
||||||
@action
|
@action
|
||||||
add(items: CommandRegistration | CommandRegistration[], extension?: LensExtension) {
|
add(items: CommandRegistration | CommandRegistration[], extension?: LensExtension) {
|
||||||
|
|||||||
@ -12,17 +12,20 @@ export interface KubeObjectDetailRegistration {
|
|||||||
priority?: number;
|
priority?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RegisteredKubeObjectDetails extends KubeObjectDetailRegistration {
|
||||||
|
priority: number;
|
||||||
|
}
|
||||||
|
|
||||||
export class KubeObjectDetailRegistry extends BaseRegistry<KubeObjectDetailRegistration> {
|
export class KubeObjectDetailRegistry extends BaseRegistry<KubeObjectDetailRegistration> {
|
||||||
getItemsForKind(kind: string, apiVersion: string) {
|
getItemsForKind(kind: string, apiVersion: string) {
|
||||||
const items = this.getItems().filter((item) => {
|
const items = this.getItems()
|
||||||
return item.kind === kind && item.apiVersions.includes(apiVersion);
|
.filter(item => (
|
||||||
}).map((item) => {
|
item.kind === kind
|
||||||
if (item.priority === null) {
|
&& item.apiVersions.includes(apiVersion)
|
||||||
item.priority = 50;
|
))
|
||||||
}
|
.map(item => (
|
||||||
|
item.priority ??= 50, item as RegisteredKubeObjectDetails
|
||||||
return item;
|
));
|
||||||
});
|
|
||||||
|
|
||||||
return items.sort((a, b) => b.priority - a.priority);
|
return items.sort((a, b) => b.priority - a.priority);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { BaseRegistry } from "./base-registry";
|
|||||||
export interface KubeObjectStatusRegistration {
|
export interface KubeObjectStatusRegistration {
|
||||||
kind: string;
|
kind: string;
|
||||||
apiVersions: string[];
|
apiVersions: string[];
|
||||||
resolve: (object: KubeObject) => KubeObjectStatus;
|
resolve<Spec, Status>(object: KubeObject<Spec, Status>): KubeObjectStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class KubeObjectStatusRegistry extends BaseRegistry<KubeObjectStatusRegistration> {
|
export class KubeObjectStatusRegistry extends BaseRegistry<KubeObjectStatusRegistration> {
|
||||||
|
|||||||
@ -6,13 +6,13 @@ import { action } from "mobx";
|
|||||||
import { BaseRegistry } from "./base-registry";
|
import { BaseRegistry } from "./base-registry";
|
||||||
import { LensExtension } from "../lens-extension";
|
import { LensExtension } from "../lens-extension";
|
||||||
|
|
||||||
export interface PageMenuRegistration {
|
export interface PageMenuRegistration<V> {
|
||||||
target?: PageTarget;
|
target?: PageTarget<V>;
|
||||||
title: React.ReactNode;
|
title: React.ReactNode;
|
||||||
components: PageMenuComponents;
|
components: PageMenuComponents;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClusterPageMenuRegistration extends PageMenuRegistration {
|
export interface ClusterPageMenuRegistration<V> extends PageMenuRegistration<V> {
|
||||||
id?: string;
|
id?: string;
|
||||||
parentId?: string;
|
parentId?: string;
|
||||||
}
|
}
|
||||||
@ -21,7 +21,7 @@ export interface PageMenuComponents {
|
|||||||
Icon: React.ComponentType<IconProps>;
|
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
|
@action
|
||||||
add(items: T[], ext: LensExtension) {
|
add(items: T[], ext: LensExtension) {
|
||||||
const normalizedItems = items.map(menuItem => {
|
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() {
|
getRootItems() {
|
||||||
return this.getItems().filter((item) => !item.parentId);
|
return this.getItems().filter((item) => !item.parentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSubItems(parent: ClusterPageMenuRegistration) {
|
getSubItems<V>(parent: ClusterPageMenuRegistration<V>) {
|
||||||
return this.getItems().filter((item) => (
|
return this.getItems()
|
||||||
item.parentId === parent.id &&
|
.filter(item => (
|
||||||
item.target.extensionId === parent.target.extensionId
|
item.parentId === parent.id
|
||||||
));
|
&& item.target?.extensionId === parent.target?.extensionId
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
getByPage({ id: pageId, extensionId }: RegisteredPage) {
|
getByPage<V>({ id: pageId, extensionId }: RegisteredPage<V>) {
|
||||||
return this.getItems().find((item) => (
|
return this.getItems()
|
||||||
item.target.pageId == pageId &&
|
.find((item) => (
|
||||||
item.target.extensionId === extensionId
|
item.target?.pageId == pageId
|
||||||
));
|
&& item.target.extensionId === extensionId
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,54 +6,55 @@ import { BaseRegistry } from "./base-registry";
|
|||||||
import { LensExtension, sanitizeExtensionName } from "../lens-extension";
|
import { LensExtension, sanitizeExtensionName } from "../lens-extension";
|
||||||
import { PageParam, PageParamInit } from "../../renderer/navigation/page-param";
|
import { PageParam, PageParamInit } from "../../renderer/navigation/page-param";
|
||||||
import { createPageParam } from "../../renderer/navigation/helpers";
|
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
|
* 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
|
* When not provided, first registered page without "id" would be used for page-menus without target.pageId for same extension
|
||||||
*/
|
*/
|
||||||
id?: string;
|
id?: string;
|
||||||
params?: PageParams<string | ExtensionPageParamInit>;
|
params?: PageParams<string | ExtensionPageParamInit<V>>;
|
||||||
components: PageComponents;
|
components: PageComponents;
|
||||||
}
|
}
|
||||||
|
|
||||||
// exclude "name" field since provided as key in page.params
|
// 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 {
|
export interface PageComponents {
|
||||||
Page: React.ComponentType<any>;
|
Page: React.ComponentType<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PageTarget<P = PageParams> {
|
export interface PageTarget<V, P = PageParams<V>> {
|
||||||
extensionId?: string;
|
extensionId?: string;
|
||||||
pageId?: string;
|
pageId?: string;
|
||||||
params?: P;
|
params?: P;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PageParams<V = any> {
|
export type PageParams<V> = Record<string, V>;
|
||||||
[paramName: string]: V;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PageComponentProps<P extends PageParams = {}> {
|
export interface PageComponentProps<V, P extends PageParams<V> = {}> {
|
||||||
params?: {
|
params?: {
|
||||||
[N in keyof P]: PageParam<P[N]>;
|
[N in keyof P]: PageParam<P[N]>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RegisteredPage {
|
export interface RegisteredPage<V> {
|
||||||
id: string;
|
id: string;
|
||||||
extensionId: string;
|
extensionId: string;
|
||||||
url: string; // registered extension's page URL (without page params)
|
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
|
components: PageComponents; // normalized components
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getExtensionPageUrl(target: PageTarget): string {
|
export function getExtensionPageUrl<V>(target: PageTarget<V>): string {
|
||||||
const { extensionId, pageId = "", params: targetParams = {} } = target;
|
const { extensionId = "", pageId = "", params: targetParams = {} } = target;
|
||||||
|
|
||||||
const pagePath = ["/extension", sanitizeExtensionName(extensionId), pageId]
|
const pagePath = ["/extension", sanitizeExtensionName(extensionId), pageId]
|
||||||
.filter(Boolean)
|
.filter(NotFalsy)
|
||||||
.join("/").replace(/\/+/g, "/").replace(/\/$/, ""); // normalize multi-slashes (e.g. coming from page.id)
|
.join("/")
|
||||||
|
.replace(/\/+/g, "/")
|
||||||
|
.replace(/\/$/, ""); // normalize multi-slashes (e.g. coming from page.id)
|
||||||
|
|
||||||
const pageUrl = new URL(pagePath, `http://localhost`);
|
const pageUrl = new URL(pagePath, `http://localhost`);
|
||||||
|
|
||||||
@ -75,9 +76,9 @@ export function getExtensionPageUrl(target: PageTarget): string {
|
|||||||
return pageUrl.href.replace(pageUrl.origin, "");
|
return pageUrl.href.replace(pageUrl.origin, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PageRegistry extends BaseRegistry<PageRegistration, RegisteredPage> {
|
export class PageRegistry extends BaseRegistry<PageRegistration<any>, RegisteredPage<any>> {
|
||||||
protected getRegisteredItem(page: PageRegistration, ext: LensExtension): RegisteredPage {
|
protected getRegisteredItem<V>(page: PageRegistration<V>, ext: LensExtension): RegisteredPage<V> {
|
||||||
const { id: pageId } = page;
|
const { id: pageId = "" } = page;
|
||||||
const extensionId = ext.name;
|
const extensionId = ext.name;
|
||||||
const params = this.normalizeParams(page.params);
|
const params = this.normalizeParams(page.params);
|
||||||
const components = this.normalizeComponents(page.components, 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) {
|
if (params) {
|
||||||
const { Page } = components;
|
const { Page } = components;
|
||||||
|
|
||||||
@ -98,22 +99,21 @@ export class PageRegistry extends BaseRegistry<PageRegistration, RegisteredPage>
|
|||||||
return components;
|
return components;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected normalizeParams(params?: PageParams<string | ExtensionPageParamInit>): PageParams<PageParam> {
|
protected normalizeParams<V>(params: PageParams<string | ExtensionPageParamInit<V>> = {}): PageParams<PageParam<V>> {
|
||||||
if (!params) {
|
return Object.fromEntries(
|
||||||
return;
|
Object.entries(params)
|
||||||
}
|
.map(([name, value]) => [
|
||||||
Object.entries(params).forEach(([name, value]) => {
|
name,
|
||||||
const paramInit: PageParamInit = typeof value === "object"
|
createPageParam<any>(
|
||||||
? { name, ...value }
|
typeof value === "object"
|
||||||
: { name, defaultValue: value };
|
? { name, ...value }
|
||||||
|
: { name, defaultValue: value }
|
||||||
params[paramInit.name] = createPageParam(paramInit);
|
)
|
||||||
});
|
])
|
||||||
|
);
|
||||||
return params as PageParams<PageParam>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
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
|
* 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.
|
* 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);
|
broadcastMessage(UpdateAvailableChannel, backchannel, info);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`${AutoUpdateLogPrefix}: broadcasting failed`, { error });
|
logger.error(`${AutoUpdateLogPrefix}: broadcasting failed`, { error });
|
||||||
installVersion = undefined;
|
installVersion = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,34 +1,19 @@
|
|||||||
import request, { RequestPromiseOptions } from "request-promise-native";
|
import { RequestPromiseOptions } from "request-promise-native";
|
||||||
import { Cluster } from "../cluster";
|
import { Cluster, k8sRequest } from "../cluster";
|
||||||
|
|
||||||
export type ClusterDetectionResult = {
|
export type ClusterDetectionResult = {
|
||||||
value: string | number | boolean
|
value?: string | number | boolean
|
||||||
accuracy: number
|
accuracy: number
|
||||||
};
|
};
|
||||||
|
|
||||||
export class BaseClusterDetector {
|
export abstract class BaseClusterDetector {
|
||||||
cluster: Cluster;
|
abstract key: string;
|
||||||
key: string;
|
|
||||||
|
|
||||||
constructor(cluster: Cluster) {
|
constructor(public cluster: Cluster) {}
|
||||||
this.cluster = cluster;
|
|
||||||
}
|
|
||||||
|
|
||||||
detect(): Promise<ClusterDetectionResult> {
|
abstract detect(): Promise<ClusterDetectionResult | null>;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async k8sRequest<T = any>(path: string, options: RequestPromiseOptions = {}): Promise<T> {
|
protected async k8sRequest<T = any>(path: string, options: RequestPromiseOptions = {}): Promise<T> {
|
||||||
const apiUrl = this.cluster.kubeProxyUrl + path;
|
return this.cluster[k8sRequest](path, options);
|
||||||
|
|
||||||
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 || {}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { BaseClusterDetector } from "./base-cluster-detector";
|
import { BaseClusterDetector } from "./base-cluster-detector";
|
||||||
import { createHash } from "crypto";
|
import { createHash } from "crypto";
|
||||||
import { ClusterMetadataKey } from "../cluster";
|
import { ClusterMetadataKey } from "../cluster";
|
||||||
|
import { assert, NotFalsy } from "../../common/utils";
|
||||||
|
|
||||||
export class ClusterIdDetector extends BaseClusterDetector {
|
export class ClusterIdDetector extends BaseClusterDetector {
|
||||||
key = ClusterMetadataKey.CLUSTER_ID;
|
key = ClusterMetadataKey.CLUSTER_ID;
|
||||||
@ -11,7 +12,7 @@ export class ClusterIdDetector extends BaseClusterDetector {
|
|||||||
try {
|
try {
|
||||||
id = await this.getDefaultNamespaceId();
|
id = await this.getDefaultNamespaceId();
|
||||||
} catch(_) {
|
} 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");
|
const value = createHash("sha256").update(id).digest("hex");
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import { ClusterMetadata } from "../../common/cluster-store";
|
import { ClusterMetadata } from "../../common/cluster-store";
|
||||||
|
import { NotFalsy } from "../../common/utils";
|
||||||
import { Cluster } from "../cluster";
|
import { Cluster } from "../cluster";
|
||||||
import { BaseClusterDetector, ClusterDetectionResult } from "./base-cluster-detector";
|
import { BaseClusterDetector, ClusterDetectionResult } from "./base-cluster-detector";
|
||||||
import { ClusterIdDetector } from "./cluster-id-detector";
|
import { ClusterIdDetector } from "./cluster-id-detector";
|
||||||
@ -8,10 +9,12 @@ import { LastSeenDetector } from "./last-seen-detector";
|
|||||||
import { NodesCountDetector } from "./nodes-count-detector";
|
import { NodesCountDetector } from "./nodes-count-detector";
|
||||||
import { VersionDetector } from "./version-detector";
|
import { VersionDetector } from "./version-detector";
|
||||||
|
|
||||||
export class DetectorRegistry {
|
type DerivedConstructor = new (cluster: Cluster) => BaseClusterDetector;
|
||||||
registry = observable.array<typeof BaseClusterDetector>([], { deep: false });
|
|
||||||
|
|
||||||
add(detectorClass: typeof BaseClusterDetector) {
|
export class DetectorRegistry {
|
||||||
|
registry = observable.array<DerivedConstructor>([], { deep: false });
|
||||||
|
|
||||||
|
add(detectorClass: DerivedConstructor) {
|
||||||
this.registry.push(detectorClass);
|
this.registry.push(detectorClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,13 +36,11 @@ export class DetectorRegistry {
|
|||||||
// detector raised error, do nothing
|
// detector raised error, do nothing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const metadata: ClusterMetadata = {};
|
|
||||||
|
|
||||||
for (const [key, result] of Object.entries(results)) {
|
return Object.fromEntries(
|
||||||
metadata[key] = result.value;
|
Object.entries(results)
|
||||||
}
|
.filter(([, result]) => NotFalsy(result))
|
||||||
|
);
|
||||||
return metadata;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,30 +1,77 @@
|
|||||||
import { BaseClusterDetector } from "./base-cluster-detector";
|
import { BaseClusterDetector } from "./base-cluster-detector";
|
||||||
import { ClusterMetadataKey } from "../cluster";
|
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 {
|
export class DistributionDetector extends BaseClusterDetector {
|
||||||
key = ClusterMetadataKey.DISTRIBUTION;
|
key = ClusterMetadataKey.DISTRIBUTION;
|
||||||
version: string;
|
|
||||||
|
|
||||||
public async detect() {
|
public async detect() {
|
||||||
this.version = await this.getKubernetesVersion();
|
const version = await this.getKubernetesVersion();
|
||||||
|
|
||||||
if (this.isRke()) {
|
if (isRke(version)) {
|
||||||
return { value: "rke", accuracy: 80};
|
return { value: "rke", accuracy: 80};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isK3s()) {
|
if (isK3s(version)) {
|
||||||
return { value: "k3s", accuracy: 80};
|
return { value: "k3s", accuracy: 80};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isGKE()) {
|
if (isGKE(version)) {
|
||||||
return { value: "gke", accuracy: 80};
|
return { value: "gke", accuracy: 80};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isEKS()) {
|
if (isEKS(version)) {
|
||||||
return { value: "eks", accuracy: 80};
|
return { value: "eks", accuracy: 80};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isIKS()) {
|
if (isIKS(version)) {
|
||||||
return { value: "iks", accuracy: 80};
|
return { value: "iks", accuracy: 80};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,27 +83,27 @@ export class DistributionDetector extends BaseClusterDetector {
|
|||||||
return { value: "digitalocean", accuracy: 90};
|
return { value: "digitalocean", accuracy: 90};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isK0s()) {
|
if (isK0s(version)) {
|
||||||
return { value: "k0s", accuracy: 80};
|
return { value: "k0s", accuracy: 80};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isVMWare()) {
|
if (isVMWare(version)) {
|
||||||
return { value: "vmware", accuracy: 90};
|
return { value: "vmware", accuracy: 90};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isMirantis()) {
|
if (isMirantis(version)) {
|
||||||
return { value: "mirantis", accuracy: 90};
|
return { value: "mirantis", accuracy: 90};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isAlibaba()) {
|
if (isAlibaba(version)) {
|
||||||
return { value: "alibaba", accuracy: 90};
|
return { value: "alibaba", accuracy: 90};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isHuawei()) {
|
if (isHuawei(version)) {
|
||||||
return { value: "huawei", accuracy: 90};
|
return { value: "huawei", accuracy: 90};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isTke()) {
|
if (isTke(version)) {
|
||||||
return { value: "tencent", accuracy: 90};
|
return { value: "tencent", accuracy: 90};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,12 +123,12 @@ export class DistributionDetector extends BaseClusterDetector {
|
|||||||
return { value: "docker-desktop", accuracy: 80};
|
return { value: "docker-desktop", accuracy: 80};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isCustom() && await this.isOpenshift()) {
|
if (isCustom(version)) {
|
||||||
return { value: "openshift", accuracy: 90};
|
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};
|
return { value: "unknown", accuracy: 10};
|
||||||
@ -95,28 +142,12 @@ export class DistributionDetector extends BaseClusterDetector {
|
|||||||
return response.gitVersion;
|
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() {
|
protected isAKS() {
|
||||||
return this.cluster.apiUrl.includes("azmk8s.io");
|
return this.cluster.apiUrl?.includes("azmk8s.io");
|
||||||
}
|
|
||||||
|
|
||||||
protected isMirantis() {
|
|
||||||
return this.version.includes("-mirantis-") || this.version.includes("-docker-");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected isDigitalOcean() {
|
protected isDigitalOcean() {
|
||||||
return this.cluster.apiUrl.endsWith("k8s.ondigitalocean.com");
|
return this.cluster.apiUrl?.endsWith("k8s.ondigitalocean.com");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected isMinikube() {
|
protected isMinikube() {
|
||||||
@ -135,38 +166,6 @@ export class DistributionDetector extends BaseClusterDetector {
|
|||||||
return this.cluster.contextName === "docker-desktop";
|
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() {
|
protected async isOpenshift() {
|
||||||
try {
|
try {
|
||||||
const response = await this.k8sRequest("");
|
const response = await this.k8sRequest("");
|
||||||
|
|||||||
@ -5,7 +5,9 @@ export class LastSeenDetector extends BaseClusterDetector {
|
|||||||
key = ClusterMetadataKey.LAST_SEEN;
|
key = ClusterMetadataKey.LAST_SEEN;
|
||||||
|
|
||||||
public async detect() {
|
public async detect() {
|
||||||
if (!this.cluster.accessible) return null;
|
if (!this.cluster.accessible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
await this.k8sRequest("/version");
|
await this.k8sRequest("/version");
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,10 @@ export class NodesCountDetector extends BaseClusterDetector {
|
|||||||
key = ClusterMetadataKey.NODES_COUNT;
|
key = ClusterMetadataKey.NODES_COUNT;
|
||||||
|
|
||||||
public async detect() {
|
public async detect() {
|
||||||
if (!this.cluster.accessible) return null;
|
if (!this.cluster.accessible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const nodeCount = await this.getNodeCount();
|
const nodeCount = await this.getNodeCount();
|
||||||
|
|
||||||
return { value: nodeCount, accuracy: 100};
|
return { value: nodeCount, accuracy: 100};
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { ClusterMetadataKey } from "../cluster";
|
|||||||
|
|
||||||
export class VersionDetector extends BaseClusterDetector {
|
export class VersionDetector extends BaseClusterDetector {
|
||||||
key = ClusterMetadataKey.VERSION;
|
key = ClusterMetadataKey.VERSION;
|
||||||
value: string;
|
|
||||||
|
|
||||||
public async detect() {
|
public async detect() {
|
||||||
const version = await this.getKubernetesVersion();
|
const version = await this.getKubernetesVersion();
|
||||||
|
|||||||
@ -169,23 +169,23 @@ export class ClusterManager extends Singleton {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getClusterForRequest(req: http.IncomingMessage): Cluster {
|
getClusterForRequest(req: http.IncomingMessage): Cluster | null {
|
||||||
let cluster: Cluster = null;
|
let cluster: Cluster | null = null;
|
||||||
|
|
||||||
// lens-server is connecting to 127.0.0.1:<port>/<uid>
|
// lens-server is connecting to 127.0.0.1:<port>/<uid>
|
||||||
if (req.headers.host.startsWith("127.0.0.1")) {
|
if (req.headers.host?.startsWith("127.0.0.1")) {
|
||||||
const clusterId = req.url.split("/")[1];
|
const clusterId = req.url?.split("/")[1];
|
||||||
|
|
||||||
cluster = clusterStore.getById(clusterId);
|
cluster = clusterStore.getById(clusterId);
|
||||||
|
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
// we need to swap path prefix so that request is proxied to kube api
|
// 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"]) {
|
} else if (req.headers["x-cluster-id"]) {
|
||||||
cluster = clusterStore.getById(req.headers["x-cluster-id"].toString());
|
cluster = clusterStore.getById(req.headers["x-cluster-id"].toString());
|
||||||
} else {
|
} else {
|
||||||
const clusterId = getClusterIdFromHost(req.headers.host);
|
const clusterId = getClusterIdFromHost(req.headers.host ?? "");
|
||||||
|
|
||||||
cluster = clusterStore.getById(clusterId);
|
cluster = clusterStore.getById(clusterId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,9 @@ import logger from "./logger";
|
|||||||
import { VersionDetector } from "./cluster-detectors/version-detector";
|
import { VersionDetector } from "./cluster-detectors/version-detector";
|
||||||
import { detectorRegistry } from "./cluster-detectors/detector-registry";
|
import { detectorRegistry } from "./cluster-detectors/detector-registry";
|
||||||
import plimit from "p-limit";
|
import plimit from "p-limit";
|
||||||
|
import { assert, NotFalsy } from "../common/utils";
|
||||||
|
|
||||||
|
export const k8sRequest = Symbol("k8sRequest");
|
||||||
|
|
||||||
export enum ClusterStatus {
|
export enum ClusterStatus {
|
||||||
AccessGranted = 2,
|
AccessGranted = 2,
|
||||||
@ -38,12 +41,12 @@ export type ClusterRefreshOptions = {
|
|||||||
export interface ClusterState {
|
export interface ClusterState {
|
||||||
initialized: boolean;
|
initialized: boolean;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
apiUrl: string;
|
apiUrl?: string;
|
||||||
online: boolean;
|
online: boolean;
|
||||||
disconnected: boolean;
|
disconnected: boolean;
|
||||||
accessible: boolean;
|
accessible: boolean;
|
||||||
ready: boolean;
|
ready: boolean;
|
||||||
failureReason: string;
|
failureReason?: string;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
allowedNamespaces: string[]
|
allowedNamespaces: string[]
|
||||||
allowedResources: string[]
|
allowedResources: string[]
|
||||||
@ -63,20 +66,20 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
*
|
*
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
public kubeCtl: Kubectl;
|
public kubeCtl?: Kubectl;
|
||||||
/**
|
/**
|
||||||
* Context handler
|
* Context handler
|
||||||
*
|
*
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
public contextHandler: ContextHandler;
|
public contextHandler?: ContextHandler;
|
||||||
/**
|
/**
|
||||||
* Owner reference
|
* Owner reference
|
||||||
*
|
*
|
||||||
* If extension sets this it needs to also mark cluster as enabled on activate (or when added to a store)
|
* If extension sets this it needs to also mark cluster as enabled on activate (or when added to a store)
|
||||||
*/
|
*/
|
||||||
public ownerRef: string;
|
public ownerRef?: string;
|
||||||
protected kubeconfigManager: KubeconfigManager;
|
protected kubeconfigManager?: KubeconfigManager;
|
||||||
protected eventDisposers: Function[] = [];
|
protected eventDisposers: Function[] = [];
|
||||||
protected activated = false;
|
protected activated = false;
|
||||||
private resourceAccessStatuses: Map<KubeApiResource, boolean> = new Map();
|
private resourceAccessStatuses: Map<KubeApiResource, boolean> = new Map();
|
||||||
@ -85,7 +88,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
whenReady = when(() => this.ready);
|
whenReady = when(() => this.ready);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is cluster object initializinng on-going
|
* Is cluster object initializing on-going
|
||||||
*
|
*
|
||||||
* @observable
|
* @observable
|
||||||
*/
|
*/
|
||||||
@ -118,14 +121,14 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
*
|
*
|
||||||
* @observable
|
* @observable
|
||||||
*/
|
*/
|
||||||
@observable apiUrl: string; // cluster server url
|
@observable apiUrl?: string; // cluster server url
|
||||||
/**
|
/**
|
||||||
* Internal authentication proxy URL
|
* Internal authentication proxy URL
|
||||||
*
|
*
|
||||||
* @observable
|
* @observable
|
||||||
* @internal
|
* @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)
|
* Is cluster instance enabled (disabled clusters are currently hidden)
|
||||||
*
|
*
|
||||||
@ -167,7 +170,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
*
|
*
|
||||||
* @observable
|
* @observable
|
||||||
*/
|
*/
|
||||||
@observable failureReason: string;
|
@observable failureReason?: string;
|
||||||
/**
|
/**
|
||||||
* Does user have admin like access
|
* Does user have admin like access
|
||||||
*
|
*
|
||||||
@ -186,7 +189,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
*
|
*
|
||||||
* @observable
|
* @observable
|
||||||
*/
|
*/
|
||||||
@observable preferences: ClusterPreferences = {};
|
@observable preferences: ClusterPreferences;
|
||||||
/**
|
/**
|
||||||
* Metadata
|
* Metadata
|
||||||
*
|
*
|
||||||
@ -211,7 +214,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
*
|
*
|
||||||
* @observable
|
* @observable
|
||||||
*/
|
*/
|
||||||
@observable accessibleNamespaces: string[] = [];
|
@observable accessibleNamespaces?: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is cluster available
|
* Is cluster available
|
||||||
@ -253,13 +256,24 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor(model: ClusterModel) {
|
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 {
|
try {
|
||||||
const kubeconfig = this.getKubeconfig();
|
const kubeconfig = this.getKubeconfig();
|
||||||
|
|
||||||
validateKubeConfig(kubeconfig, this.contextName, { validateCluster: true, validateUser: false, validateExec: false});
|
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) {
|
} catch(err) {
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
logger.error(`[CLUSTER] Failed to load kubeconfig for the cluster '${this.name || this.contextName}' (context: ${this.contextName}, kubeconfig: ${this.kubeConfigPath}).`);
|
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;
|
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)
|
* Initialize a cluster (can be done only in main process)
|
||||||
*
|
*
|
||||||
@ -323,7 +328,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
if (ipcMain) {
|
if (ipcMain) {
|
||||||
this.eventDisposers.push(
|
this.eventDisposers.push(
|
||||||
reaction(() => this.getState(), () => this.pushState()),
|
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(refreshTimer);
|
||||||
clearInterval(refreshMetadataTimer);
|
clearInterval(refreshMetadataTimer);
|
||||||
@ -488,15 +493,17 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
async getProxyKubeconfigPath(): Promise<string> {
|
async getProxyKubeconfigPath(): Promise<string | undefined> {
|
||||||
return this.kubeconfigManager.getPath();
|
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.headers ??= {};
|
||||||
options.json ??= true;
|
options.json ??= true;
|
||||||
options.timeout ??= 30000;
|
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);
|
return request(this.kubeProxyUrl + path, options);
|
||||||
}
|
}
|
||||||
@ -511,7 +518,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
const prometheusPrefix = this.preferences.prometheus?.prefix || "";
|
const prometheusPrefix = this.preferences.prometheus?.prefix || "";
|
||||||
const metricsPath = `/api/v1/namespaces/${prometheusPath}/proxy${prometheusPrefix}/api/v1/query_range`;
|
const metricsPath = `/api/v1/namespaces/${prometheusPath}/proxy${prometheusPrefix}/api/v1/query_range`;
|
||||||
|
|
||||||
return this.k8sRequest(metricsPath, {
|
return this[k8sRequest](metricsPath, {
|
||||||
timeout: 0,
|
timeout: 0,
|
||||||
resolveWithFullResponse: false,
|
resolveWithFullResponse: false,
|
||||||
json: true,
|
json: true,
|
||||||
@ -526,8 +533,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
const versionData = await versionDetector.detect();
|
const versionData = await versionDetector.detect();
|
||||||
|
|
||||||
this.metadata.version = versionData.value;
|
this.metadata.version = versionData.value;
|
||||||
|
this.failureReason = undefined;
|
||||||
this.failureReason = null;
|
|
||||||
|
|
||||||
return ClusterStatus.AccessGranted;
|
return ClusterStatus.AccessGranted;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -574,7 +580,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
spec: { resourceAttributes }
|
spec: { resourceAttributes }
|
||||||
});
|
});
|
||||||
|
|
||||||
return accessReview.body.status.allowed;
|
return accessReview.body.status?.allowed ?? false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`failed to request selfSubjectAccessReview: ${error}`);
|
logger.error(`failed to request selfSubjectAccessReview: ${error}`);
|
||||||
|
|
||||||
@ -676,7 +682,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async getAllowedNamespaces() {
|
protected async getAllowedNamespaces() {
|
||||||
if (this.accessibleNamespaces.length) {
|
if (this.accessibleNamespaces?.length) {
|
||||||
return this.accessibleNamespaces;
|
return this.accessibleNamespaces;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -685,10 +691,10 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
try {
|
try {
|
||||||
const namespaceList = await api.listNamespace();
|
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) {
|
} catch (error) {
|
||||||
const ctx = (await this.getProxyKubeconfig()).getContextObject(this.contextName);
|
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) {
|
if (namespaceList.length === 0 && error instanceof HttpError && error.statusCode === 403) {
|
||||||
logger.info("[CLUSTER]: listing namespaces is forbidden, broadcasting", { clusterId: this.id });
|
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 { ClusterPrometheusPreferences } from "../common/cluster-store";
|
||||||
import type { Cluster } from "./cluster";
|
import type { Cluster } from "./cluster";
|
||||||
import type httpProxy from "http-proxy";
|
import type httpProxy from "http-proxy";
|
||||||
@ -8,35 +8,54 @@ import { prometheusProviders } from "../common/prometheus-providers";
|
|||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { getFreePort } from "./port";
|
import { getFreePort } from "./port";
|
||||||
import { KubeAuthProxy } from "./kube-auth-proxy";
|
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 {
|
export class ContextHandler {
|
||||||
public proxyPort: number;
|
public proxyPort?: number;
|
||||||
public clusterUrl: UrlWithStringQuery;
|
public clusterUrl: VerifiedUrl;
|
||||||
protected kubeAuthProxy: KubeAuthProxy;
|
protected kubeAuthProxy?: KubeAuthProxy;
|
||||||
protected apiTarget: httpProxy.ServerOptions;
|
protected apiTarget?: httpProxy.ServerOptions;
|
||||||
protected prometheusProvider: string;
|
protected prometheusProvider?: string;
|
||||||
protected prometheusPath: string;
|
protected prometheusPath?: string;
|
||||||
|
|
||||||
constructor(protected cluster: Cluster) {
|
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);
|
this.setupPrometheus(cluster.preferences);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setupPrometheus(preferences: ClusterPrometheusPreferences = {}) {
|
public setupPrometheus(preferences: ClusterPrometheusPreferences = {}) {
|
||||||
this.prometheusProvider = preferences.prometheusProvider?.type;
|
this.prometheusProvider = preferences.prometheusProvider?.type;
|
||||||
this.prometheusPath = null;
|
|
||||||
|
|
||||||
if (preferences.prometheus) {
|
if (preferences.prometheus) {
|
||||||
const { namespace, service, port } = preferences.prometheus;
|
const { namespace, service, port } = preferences.prometheus;
|
||||||
|
|
||||||
this.prometheusPath = `${namespace}/services/${service}:${port}`;
|
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();
|
const prometheusService = await this.getPrometheusService();
|
||||||
|
|
||||||
if (!prometheusService) return null;
|
if (!prometheusService) return;
|
||||||
|
|
||||||
const { service, namespace, port } = prometheusService;
|
const { service, namespace, port } = prometheusService;
|
||||||
|
|
||||||
return `${namespace}/services/${service}:${port}`;
|
return `${namespace}/services/${service}:${port}`;
|
||||||
@ -56,24 +75,26 @@ export class ContextHandler {
|
|||||||
return prometheusProviders.find(p => p.id === this.prometheusProvider);
|
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 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);
|
return (await Promise.allSettled(providers
|
||||||
});
|
.map(provider => (
|
||||||
const resolvedPrometheusServices = await Promise.all(prometheusPromises);
|
this.cluster.getProxyKubeconfig()
|
||||||
|
.then(kc => kc.makeApiClient(CoreV1Api))
|
||||||
return resolvedPrometheusServices.filter(n => n)[0];
|
.then(client => provider.getPrometheusService(client))
|
||||||
|
))
|
||||||
|
))
|
||||||
|
.map(result => (
|
||||||
|
result.status === "fulfilled"
|
||||||
|
? result.value
|
||||||
|
: undefined
|
||||||
|
))
|
||||||
|
.find(NotFalsy);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPrometheusPath(): Promise<string> {
|
async getPrometheusPath(): Promise<string | undefined> {
|
||||||
if (!this.prometheusPath) {
|
return this.prometheusPath ??= await this.resolvePrometheusPath();
|
||||||
this.prometheusPath = await this.resolvePrometheusPath();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.prometheusPath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async resolveAuthProxyUrl() {
|
async resolveAuthProxyUrl() {
|
||||||
@ -111,11 +132,7 @@ export class ContextHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async ensurePort(): Promise<number> {
|
async ensurePort(): Promise<number> {
|
||||||
if (!this.proxyPort) {
|
return this.proxyPort ??= await getFreePort();
|
||||||
this.proxyPort = await getFreePort();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.proxyPort;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async ensureServer() {
|
async ensureServer() {
|
||||||
@ -126,7 +143,7 @@ export class ContextHandler {
|
|||||||
if (this.cluster.preferences.httpsProxy) {
|
if (this.cluster.preferences.httpsProxy) {
|
||||||
proxyEnv.HTTPS_PROXY = 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();
|
await this.kubeAuthProxy.run();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -134,7 +151,7 @@ export class ContextHandler {
|
|||||||
stopServer() {
|
stopServer() {
|
||||||
if (this.kubeAuthProxy) {
|
if (this.kubeAuthProxy) {
|
||||||
this.kubeAuthProxy.exit();
|
this.kubeAuthProxy.exit();
|
||||||
this.kubeAuthProxy = null;
|
this.kubeAuthProxy = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -36,7 +36,8 @@ export class FilesystemProvisionerStore extends BaseStore<FSProvisionModel> {
|
|||||||
this.registeredExtensions.set(extensionName, dirPath);
|
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);
|
await fse.ensureDir(dirPath);
|
||||||
|
|
||||||
|
|||||||
@ -19,10 +19,12 @@ export class HelmChartManager {
|
|||||||
this.repo = repo;
|
this.repo = repo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async chart(name: string) {
|
public async chart(name?: string) {
|
||||||
const charts = await this.charts();
|
const charts = await this.charts();
|
||||||
|
|
||||||
return charts[name];
|
if (name) {
|
||||||
|
return charts[name];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async charts(): Promise<RepoHelmChartList> {
|
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();
|
const helm = await helmCli.binaryPath();
|
||||||
|
|
||||||
if(version && version != "") {
|
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();
|
const helm = await helmCli.binaryPath();
|
||||||
|
|
||||||
if(version && version != "") {
|
if(version && version != "") {
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { promiseExec} from "../promise-exec";
|
|||||||
import { helmCli } from "./helm-cli";
|
import { helmCli } from "./helm-cli";
|
||||||
import { Cluster } from "../cluster";
|
import { Cluster } from "../cluster";
|
||||||
import { toCamelCase } from "../../common/utils/camelCase";
|
import { toCamelCase } from "../../common/utils/camelCase";
|
||||||
|
import { assert, NotFalsy } from "../../common/utils";
|
||||||
|
|
||||||
export class HelmReleaseManager {
|
export class HelmReleaseManager {
|
||||||
|
|
||||||
@ -114,7 +115,8 @@ export class HelmReleaseManager {
|
|||||||
|
|
||||||
protected async getResources(name: string, namespace: string, cluster: Cluster) {
|
protected async getResources(name: string, namespace: string, cluster: Cluster) {
|
||||||
const helm = await helmCli.binaryPath();
|
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 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(() => {
|
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: []})};
|
return { stdout: JSON.stringify({items: []})};
|
||||||
|
|||||||
@ -6,10 +6,11 @@ import { Singleton } from "../../common/utils/singleton";
|
|||||||
import { customRequestPromise } from "../../common/request";
|
import { customRequestPromise } from "../../common/request";
|
||||||
import orderBy from "lodash/orderBy";
|
import orderBy from "lodash/orderBy";
|
||||||
import logger from "../logger";
|
import logger from "../logger";
|
||||||
|
import { AssertionError } from "assert";
|
||||||
|
|
||||||
export type HelmEnv = Record<string, string> & {
|
export type HelmEnv = Record<string, string> & {
|
||||||
HELM_REPOSITORY_CACHE?: string;
|
HELM_REPOSITORY_CACHE: string;
|
||||||
HELM_REPOSITORY_CONFIG?: string;
|
HELM_REPOSITORY_CONFIG: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface HelmRepoConfig {
|
export interface HelmRepoConfig {
|
||||||
@ -19,7 +20,7 @@ export interface HelmRepoConfig {
|
|||||||
export interface HelmRepo {
|
export interface HelmRepo {
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
cacheFilePath?: string
|
cacheFilePath: string
|
||||||
caFile?: string,
|
caFile?: string,
|
||||||
certFile?: string,
|
certFile?: string,
|
||||||
insecureSkipTlsVerify?: boolean,
|
insecureSkipTlsVerify?: boolean,
|
||||||
@ -31,9 +32,8 @@ export interface HelmRepo {
|
|||||||
export class HelmRepoManager extends Singleton {
|
export class HelmRepoManager extends Singleton {
|
||||||
static cache = {}; // todo: remove implicit updates in helm-chart-manager.ts
|
static cache = {}; // todo: remove implicit updates in helm-chart-manager.ts
|
||||||
|
|
||||||
protected repos: HelmRepo[];
|
protected repos: HelmRepo[] = [];
|
||||||
protected helmEnv: HelmEnv;
|
protected helmEnv?: HelmEnv;
|
||||||
protected initialized: boolean;
|
|
||||||
|
|
||||||
async loadAvailableRepos(): Promise<HelmRepo[]> {
|
async loadAvailableRepos(): Promise<HelmRepo[]> {
|
||||||
const res = await customRequestPromise({
|
const res = await customRequestPromise({
|
||||||
@ -46,43 +46,46 @@ export class HelmRepoManager extends Singleton {
|
|||||||
return orderBy<HelmRepo>(res.body, repo => repo.name);
|
return orderBy<HelmRepo>(res.body, repo => repo.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init(): Promise<HelmEnv> {
|
||||||
helmCli.setLogger(logger);
|
helmCli.setLogger(logger);
|
||||||
await helmCli.ensureBinary();
|
await helmCli.ensureBinary();
|
||||||
|
|
||||||
if (!this.initialized) {
|
try {
|
||||||
this.helmEnv = await this.parseHelmEnv();
|
return this.helmEnv ?? await this.parseHelmEnv();
|
||||||
|
} finally {
|
||||||
await this.update();
|
await this.update();
|
||||||
this.initialized = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async parseHelmEnv() {
|
protected async parseHelmEnv(): Promise<HelmEnv> {
|
||||||
const helm = await helmCli.binaryPath();
|
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) => {
|
try {
|
||||||
const [key, value] = line.split("=");
|
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) {
|
if (!env.HELM_REPOSITORY_CACHE || !env.HELM_REPOSITORY_CONFIG) {
|
||||||
env[key] = value.replace(/"/g, ""); // strip quotas
|
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[]> {
|
public async repositories(): Promise<HelmRepo[]> {
|
||||||
if (!this.initialized) {
|
const helmEnv = await this.init();
|
||||||
await this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const repoConfigFile = this.helmEnv.HELM_REPOSITORY_CONFIG;
|
const repoConfigFile = helmEnv.HELM_REPOSITORY_CONFIG;
|
||||||
const { repositories }: HelmRepoConfig = await readFile(repoConfigFile, "utf8")
|
const { repositories }: HelmRepoConfig = await readFile(repoConfigFile, "utf8")
|
||||||
.then((yamlContent: string) => yaml.safeLoad(yamlContent))
|
.then((yamlContent: string) => yaml.safeLoad(yamlContent))
|
||||||
.catch(() => ({
|
.catch(() => ({
|
||||||
@ -97,7 +100,7 @@ export class HelmRepoManager extends Singleton {
|
|||||||
|
|
||||||
return repositories.map(repo => ({
|
return repositories.map(repo => ({
|
||||||
...repo,
|
...repo,
|
||||||
cacheFilePath: `${this.helmEnv.HELM_REPOSITORY_CACHE}/${repo.name}-index.yaml`
|
cacheFilePath: `${helmEnv.HELM_REPOSITORY_CACHE}/${repo.name}-index.yaml`
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[HELM]: repositories listing error "${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();
|
const repositories = await this.repositories();
|
||||||
|
|
||||||
return repositories.find(repo => repo.name == name);
|
return repositories.find(repo => repo.name == name);
|
||||||
@ -114,21 +117,23 @@ export class HelmRepoManager extends Singleton {
|
|||||||
|
|
||||||
public async update() {
|
public async update() {
|
||||||
const helm = await helmCli.binaryPath();
|
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}`);
|
logger.info(`[HELM]: adding repo "${name}" from ${url}`);
|
||||||
const helm = await helmCli.binaryPath();
|
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) {
|
public async addСustomRepo(repoAttributes : HelmRepo) {
|
||||||
@ -143,21 +148,23 @@ export class HelmRepoManager extends Singleton {
|
|||||||
const certFile = repoAttributes.certFile ? ` --cert-file "${repoAttributes.certFile}"` : "";
|
const certFile = repoAttributes.certFile ? ` --cert-file "${repoAttributes.certFile}"` : "";
|
||||||
|
|
||||||
const addRepoCommand = `"${helm}" repo add ${repoAttributes.name} ${repoAttributes.url}${insecureSkipTlsVerify}${username}${password}${caFile}${keyFile}${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> {
|
public async removeRepo({ name, url }: HelmRepo): Promise<string> {
|
||||||
logger.info(`[HELM]: removing repo "${name}" from ${url}`);
|
logger.info(`[HELM]: removing repo "${name}" from ${url}`);
|
||||||
const helm = await helmCli.binaryPath();
|
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 }) {
|
public async installChart(cluster: Cluster, data: { chart: string; values: {}; name: string; namespace: string; version: string }) {
|
||||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||||
|
|
||||||
|
if (!proxyKubeconfig) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return await releaseManager.installChart(data.chart, data.values, data.name, data.namespace, data.version, proxyKubeconfig);
|
return await releaseManager.installChart(data.chart, data.values, data.name, data.namespace, data.version, proxyKubeconfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,74 +35,116 @@ class HelmService {
|
|||||||
return charts;
|
return charts;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getChart(repoName: string, chartName: string, version = "") {
|
public async getChart(repoName?: string, chartName?: string, version?: string | null) {
|
||||||
const result = {
|
const result = {
|
||||||
readme: "",
|
readme: "",
|
||||||
versions: {}
|
versions: {}
|
||||||
};
|
};
|
||||||
const repo = await repoManager.repository(repoName);
|
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 chartManager = new HelmChartManager(repo);
|
||||||
const chart = await chartManager.chart(chartName);
|
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;
|
result.versions = chart;
|
||||||
|
|
||||||
return result;
|
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);
|
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);
|
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();
|
await repoManager.init();
|
||||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
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);
|
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");
|
logger.debug("Fetch release");
|
||||||
|
|
||||||
return await releaseManager.getRelease(releaseName, namespace, cluster);
|
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();
|
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");
|
logger.debug("Fetch release values");
|
||||||
|
|
||||||
return await releaseManager.getValues(releaseName, namespace, proxyKubeconfig);
|
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();
|
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");
|
logger.debug("Fetch release history");
|
||||||
|
|
||||||
return await releaseManager.getHistory(releaseName, namespace, proxyKubeconfig);
|
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();
|
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");
|
logger.debug("Delete release");
|
||||||
|
|
||||||
return await releaseManager.deleteRelease(releaseName, namespace, proxyKubeconfig);
|
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");
|
logger.debug("Upgrade release");
|
||||||
|
|
||||||
return await releaseManager.upgradeRelease(releaseName, data.chart, data.values, namespace, data.version, cluster);
|
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();
|
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");
|
logger.debug("Rollback release");
|
||||||
const output = await releaseManager.rollback(releaseName, namespace, revision, proxyKubeconfig);
|
const output = await releaseManager.rollback(releaseName, namespace, revision, proxyKubeconfig);
|
||||||
|
|
||||||
@ -123,6 +169,10 @@ class HelmService {
|
|||||||
const firstVersion = semver.coerce(first.version || 0);
|
const firstVersion = semver.coerce(first.version || 0);
|
||||||
const secondVersion = semver.coerce(second.version || 0);
|
const secondVersion = semver.coerce(second.version || 0);
|
||||||
|
|
||||||
|
if (!firstVersion || !secondVersion) {
|
||||||
|
return 0; // consider this case as equal
|
||||||
|
}
|
||||||
|
|
||||||
return semver.compare(secondVersion, firstVersion);
|
return semver.compare(secondVersion, firstVersion);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import type { Cluster } from "./cluster";
|
|||||||
import { Kubectl } from "./kubectl";
|
import { Kubectl } from "./kubectl";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import * as url from "url";
|
import * as url from "url";
|
||||||
|
import { assert } from "../common/utils";
|
||||||
|
|
||||||
export interface KubeAuthProxyLog {
|
export interface KubeAuthProxyLog {
|
||||||
data: string;
|
data: string;
|
||||||
@ -12,23 +13,24 @@ export interface KubeAuthProxyLog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class KubeAuthProxy {
|
export class KubeAuthProxy {
|
||||||
public lastError: string;
|
public lastError?: string;
|
||||||
|
|
||||||
protected cluster: Cluster;
|
protected cluster: Cluster;
|
||||||
protected env: NodeJS.ProcessEnv = null;
|
protected env: NodeJS.ProcessEnv;
|
||||||
protected proxyProcess: ChildProcess;
|
protected proxyProcess?: ChildProcess;
|
||||||
protected port: number;
|
protected port: number;
|
||||||
protected kubectl: Kubectl;
|
protected kubectl: Kubectl;
|
||||||
|
readonly acceptHosts: string;
|
||||||
|
|
||||||
constructor(cluster: Cluster, port: number, env: NodeJS.ProcessEnv) {
|
constructor(cluster: Cluster, port: number, env: NodeJS.ProcessEnv) {
|
||||||
this.env = env;
|
this.env = env;
|
||||||
this.port = port;
|
this.port = port;
|
||||||
this.cluster = cluster;
|
this.cluster = cluster;
|
||||||
this.kubectl = Kubectl.bundled();
|
this.kubectl = Kubectl.bundled();
|
||||||
}
|
this.acceptHosts = assert(
|
||||||
|
this.cluster.apiUrl && url.parse(this.cluster.apiUrl).hostname,
|
||||||
get acceptHosts() {
|
"Cluster must be properly initialized to have a proxy created for it",
|
||||||
return url.parse(this.cluster.apiUrl).hostname;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async run(): Promise<void> {
|
public async run(): Promise<void> {
|
||||||
@ -57,11 +59,11 @@ export class KubeAuthProxy {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.proxyProcess.on("exit", (code) => {
|
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.exit();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.proxyProcess.stdout.on("data", (data) => {
|
this.proxyProcess.stdout?.on("data", (data) => {
|
||||||
let logItem = data.toString();
|
let logItem = data.toString();
|
||||||
|
|
||||||
if (logItem.startsWith("Starting to serve on")) {
|
if (logItem.startsWith("Starting to serve on")) {
|
||||||
@ -70,7 +72,7 @@ export class KubeAuthProxy {
|
|||||||
this.sendIpcLogMessage({ data: logItem });
|
this.sendIpcLogMessage({ data: logItem });
|
||||||
});
|
});
|
||||||
|
|
||||||
this.proxyProcess.stderr.on("data", (data) => {
|
this.proxyProcess.stderr?.on("data", (data) => {
|
||||||
this.lastError = this.parseError(data.toString());
|
this.lastError = this.parseError(data.toString());
|
||||||
this.sendIpcLogMessage({ data: data.toString(), error: true });
|
this.sendIpcLogMessage({ data: data.toString(), error: true });
|
||||||
});
|
});
|
||||||
@ -108,8 +110,8 @@ export class KubeAuthProxy {
|
|||||||
logger.debug("[KUBE-AUTH]: stopping local proxy", this.cluster.getMeta());
|
logger.debug("[KUBE-AUTH]: stopping local proxy", this.cluster.getMeta());
|
||||||
this.proxyProcess.kill();
|
this.proxyProcess.kill();
|
||||||
this.proxyProcess.removeAllListeners();
|
this.proxyProcess.removeAllListeners();
|
||||||
this.proxyProcess.stderr.removeAllListeners();
|
this.proxyProcess.stderr?.removeAllListeners();
|
||||||
this.proxyProcess.stdout.removeAllListeners();
|
this.proxyProcess.stdout?.removeAllListeners();
|
||||||
this.proxyProcess = null;
|
this.proxyProcess = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import logger from "./logger";
|
|||||||
|
|
||||||
export class KubeconfigManager {
|
export class KubeconfigManager {
|
||||||
protected configDir = app.getPath("temp");
|
protected configDir = app.getPath("temp");
|
||||||
protected tempFile: string;
|
protected tempFile?: string;
|
||||||
|
|
||||||
private constructor(protected cluster: Cluster, protected contextHandler: ContextHandler, protected port: number) { }
|
private constructor(protected cluster: Cluster, protected contextHandler: ContextHandler, protected port: number) { }
|
||||||
|
|
||||||
@ -63,7 +63,7 @@ export class KubeconfigManager {
|
|||||||
{
|
{
|
||||||
name: contextName,
|
name: contextName,
|
||||||
server: this.resolveProxyUrl(),
|
server: this.resolveProxyUrl(),
|
||||||
skipTLSVerify: undefined,
|
skipTLSVerify: false,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
users: [
|
users: [
|
||||||
@ -74,7 +74,7 @@ export class KubeconfigManager {
|
|||||||
user: "proxy",
|
user: "proxy",
|
||||||
name: contextName,
|
name: contextName,
|
||||||
cluster: contextName,
|
cluster: contextName,
|
||||||
namespace: kubeConfig.getContextObject(contextName).namespace,
|
namespace: kubeConfig.getContextObject(contextName)?.namespace,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@ -55,7 +55,7 @@ export function bundledKubectlPath(): string {
|
|||||||
|
|
||||||
export class Kubectl {
|
export class Kubectl {
|
||||||
public kubectlVersion: string;
|
public kubectlVersion: string;
|
||||||
protected directory: string;
|
protected directory?: string;
|
||||||
protected url: string;
|
protected url: string;
|
||||||
protected path: string;
|
protected path: string;
|
||||||
protected dirname: string;
|
protected dirname: string;
|
||||||
@ -77,12 +77,17 @@ export class Kubectl {
|
|||||||
|
|
||||||
constructor(clusterVersion: string) {
|
constructor(clusterVersion: string) {
|
||||||
const versionParts = /^v?(\d+\.\d+)(.*)/.exec(clusterVersion);
|
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
|
/* 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 the version map includes that, use that version, if not, fallback to the exact x.y.z of kube version */
|
||||||
if (kubectlMap.has(minorVersion)) {
|
if (prev) {
|
||||||
this.kubectlVersion = kubectlMap.get(minorVersion);
|
this.kubectlVersion = prev;
|
||||||
logger.debug(`Set kubectl version ${this.kubectlVersion} for cluster version ${clusterVersion} using version map`);
|
logger.debug(`Set kubectl version ${this.kubectlVersion} for cluster version ${clusterVersion} using version map`);
|
||||||
} else {
|
} else {
|
||||||
this.kubectlVersion = versionParts[1] + versionParts[2];
|
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}`);
|
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({
|
const stream = customRequest({
|
||||||
url: this.url,
|
url: this.url,
|
||||||
gzip: true,
|
gzip: true,
|
||||||
@ -360,13 +365,12 @@ export class Kubectl {
|
|||||||
await fsPromises.writeFile(zshScriptPath, zshScript.toString(), { mode: 0o644 });
|
await fsPromises.writeFile(zshScriptPath, zshScript.toString(), { mode: 0o644 });
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getDownloadMirror() {
|
protected getDownloadMirror(): string {
|
||||||
const mirror = packageMirrors.get(userStore.preferences?.downloadMirror);
|
return (
|
||||||
|
userStore.preferences?.downloadMirror
|
||||||
if (mirror) {
|
&& packageMirrors.get(userStore.preferences?.downloadMirror)
|
||||||
return mirror;
|
)
|
||||||
}
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
?? packageMirrors.get("default")!;
|
||||||
return packageMirrors.get("default"); // MacOS packages are only available from default
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { ensureDir, pathExists } from "fs-extra";
|
|||||||
import * as tar from "tar";
|
import * as tar from "tar";
|
||||||
import { isWindows } from "../common/vars";
|
import { isWindows } from "../common/vars";
|
||||||
import winston from "winston";
|
import winston from "winston";
|
||||||
|
import { noop } from "../common/utils";
|
||||||
|
|
||||||
export type LensBinaryOpts = {
|
export type LensBinaryOpts = {
|
||||||
version: string;
|
version: string;
|
||||||
@ -17,16 +18,16 @@ export type LensBinaryOpts = {
|
|||||||
export class LensBinary {
|
export class LensBinary {
|
||||||
|
|
||||||
public binaryVersion: string;
|
public binaryVersion: string;
|
||||||
protected directory: string;
|
protected directory?: string;
|
||||||
protected url: string;
|
protected url?: string;
|
||||||
protected path: string;
|
protected path?: string;
|
||||||
protected tarPath: string;
|
protected tarPath?: string;
|
||||||
protected dirname: string;
|
protected dirname: string;
|
||||||
protected binaryName: string;
|
protected binaryName: string;
|
||||||
protected platformName: string;
|
protected platformName: string;
|
||||||
protected arch: string;
|
protected arch: string;
|
||||||
protected originalBinaryName: string;
|
protected originalBinaryName: string;
|
||||||
protected requestOpts: request.Options;
|
protected requestOpts?: request.Options;
|
||||||
protected logger: Console | winston.Logger;
|
protected logger: Console | winston.Logger;
|
||||||
|
|
||||||
constructor(opts: LensBinaryOpts) {
|
constructor(opts: LensBinaryOpts) {
|
||||||
@ -177,19 +178,21 @@ export class LensBinary {
|
|||||||
|
|
||||||
stream.on("error", (error) => {
|
stream.on("error", (error) => {
|
||||||
this.logger.error(error);
|
this.logger.error(error);
|
||||||
fs.unlink(binaryPath, () => {
|
fs.unlink(binaryPath, noop);
|
||||||
// do nothing
|
throw error;
|
||||||
});
|
|
||||||
throw(error);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
file.on("close", () => {
|
file.on("close", () => {
|
||||||
this.logger.debug(`${this.originalBinaryName} binary download closed`);
|
this.logger.debug(`${this.originalBinaryName} binary download closed`);
|
||||||
if (!this.tarPath) fs.chmod(binaryPath, 0o755, (err) => {
|
|
||||||
if (err) reject(err);
|
if (!this.tarPath) {
|
||||||
});
|
fs.promises.chmod(binaryPath, 0o755)
|
||||||
resolve();
|
.then(resolve)
|
||||||
|
.catch(reject);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
stream.pipe(file);
|
stream.pipe(file);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -10,10 +10,11 @@ import { ClusterManager } from "./cluster-manager";
|
|||||||
import { ContextHandler } from "./context-handler";
|
import { ContextHandler } from "./context-handler";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { NodeShellSession, LocalShellSession } from "./shell-session";
|
import { NodeShellSession, LocalShellSession } from "./shell-session";
|
||||||
|
import { assert } from "../common/utils";
|
||||||
|
|
||||||
export class LensProxy {
|
export class LensProxy {
|
||||||
protected origin: string;
|
protected origin: string;
|
||||||
protected proxyServer: http.Server;
|
protected proxyServer?: http.Server;
|
||||||
protected router: Router;
|
protected router: Router;
|
||||||
protected closed = false;
|
protected closed = false;
|
||||||
protected retryCounters = new Map<string, number>();
|
protected retryCounters = new Map<string, number>();
|
||||||
@ -36,7 +37,7 @@ export class LensProxy {
|
|||||||
|
|
||||||
close() {
|
close() {
|
||||||
logger.info("Closing proxy server");
|
logger.info("Closing proxy server");
|
||||||
this.proxyServer.close();
|
this.proxyServer?.close();
|
||||||
this.closed = true;
|
this.closed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,7 +53,7 @@ export class LensProxy {
|
|||||||
});
|
});
|
||||||
|
|
||||||
spdyProxy.on("upgrade", (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => {
|
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);
|
this.handleWsUpgrade(req, socket, head);
|
||||||
} else {
|
} else {
|
||||||
this.handleProxyUpgrade(proxy, req, socket, head);
|
this.handleProxyUpgrade(proxy, req, socket, head);
|
||||||
@ -69,10 +70,19 @@ export class LensProxy {
|
|||||||
const cluster = this.clusterManager.getClusterForRequest(req);
|
const cluster = this.clusterManager.getClusterForRequest(req);
|
||||||
|
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "");
|
const authProxyUrl = assert(
|
||||||
const apiUrl = url.parse(cluster.apiUrl);
|
await cluster.contextHandler?.resolveAuthProxyUrl(),
|
||||||
const pUrl = url.parse(proxyUrl);
|
"Cluster must be fully initialized to be proxied to",
|
||||||
const connectOpts = { port: parseInt(pUrl.port), host: pUrl.hostname };
|
);
|
||||||
|
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();
|
const proxySocket = new net.Socket();
|
||||||
|
|
||||||
proxySocket.connect(connectOpts, () => {
|
proxySocket.connect(connectOpts, () => {
|
||||||
@ -171,8 +181,11 @@ export class LensProxy {
|
|||||||
const ws = new WebSocket.Server({ noServer: true });
|
const ws = new WebSocket.Server({ noServer: true });
|
||||||
|
|
||||||
return ws.on("connection", ((socket: WebSocket, req: http.IncomingMessage) => {
|
return ws.on("connection", ((socket: WebSocket, req: http.IncomingMessage) => {
|
||||||
const cluster = this.clusterManager.getClusterForRequest(req);
|
const cluster = assert(
|
||||||
const nodeParam = url.parse(req.url, true).query["node"]?.toString();
|
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
|
const shell = nodeParam
|
||||||
? new NodeShellSession(socket, cluster, nodeParam)
|
? new NodeShellSession(socket, cluster, nodeParam)
|
||||||
: new LocalShellSession(socket, cluster);
|
: new LocalShellSession(socket, cluster);
|
||||||
@ -182,8 +195,8 @@ export class LensProxy {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getProxyTarget(req: http.IncomingMessage, contextHandler: ContextHandler): Promise<httpProxy.ServerOptions> {
|
protected async getProxyTarget(req: http.IncomingMessage, contextHandler: ContextHandler): Promise<httpProxy.ServerOptions | undefined> {
|
||||||
if (req.url.startsWith(apiKubePrefix)) {
|
if (req.url?.startsWith(apiKubePrefix)) {
|
||||||
delete req.headers.authorization;
|
delete req.headers.authorization;
|
||||||
req.url = req.url.replace(apiKubePrefix, "");
|
req.url = req.url.replace(apiKubePrefix, "");
|
||||||
const isWatchRequest = req.url.includes("watch=");
|
const isWatchRequest = req.url.includes("watch=");
|
||||||
@ -193,14 +206,15 @@ export class LensProxy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected getRequestId(req: http.IncomingMessage) {
|
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) {
|
protected async handleRequest(proxy: httpProxy, req: http.IncomingMessage, res: http.ServerResponse) {
|
||||||
const cluster = this.clusterManager.getClusterForRequest(req);
|
const cluster = this.clusterManager.getClusterForRequest(req);
|
||||||
|
|
||||||
if (cluster) {
|
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) {
|
if (proxyTarget) {
|
||||||
// allow to fetch apis in "clusterId.localhost:port" from "localhost:port"
|
// allow to fetch apis in "clusterId.localhost:port" from "localhost:port"
|
||||||
@ -210,6 +224,7 @@ export class LensProxy {
|
|||||||
return proxy.web(req, res, proxyTarget);
|
return proxy.web(req, res, proxyTarget);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.router.route(cluster, req, res);
|
this.router.route(cluster, req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,12 @@ import { exitApp } from "./exit-app";
|
|||||||
import { broadcastMessage } from "../common/ipc";
|
import { broadcastMessage } from "../common/ipc";
|
||||||
import * as packageJson from "../../package.json";
|
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) {
|
export function initMenu(windowManager: WindowManager) {
|
||||||
return autorun(() => buildMenu(windowManager), {
|
return autorun(() => buildMenu(windowManager), {
|
||||||
@ -67,8 +72,8 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: "About Lens",
|
label: "About Lens",
|
||||||
click(menuItem: MenuItem, browserWindow: BrowserWindow) {
|
click(menuItem: MenuItem, browserWindow?: BrowserWindow) {
|
||||||
showAbout(browserWindow);
|
browserWindow && showAbout(browserWindow);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ type: "separator" },
|
{ type: "separator" },
|
||||||
@ -245,15 +250,15 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
...ignoreOnMac([
|
...ignoreOnMac([
|
||||||
{
|
{
|
||||||
label: "About Lens",
|
label: "About Lens",
|
||||||
click(menuItem: MenuItem, browserWindow: BrowserWindow) {
|
click(menuItem: MenuItem, browserWindow?: BrowserWindow) {
|
||||||
showAbout(browserWindow);
|
browserWindow && showAbout(browserWindow);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
// Prepare menu items order
|
// Prepare menu items order
|
||||||
const appMenu: Record<MenuTopId, MenuItemConstructorOptions> = {
|
const appMenu = {
|
||||||
mac: macAppMenu,
|
mac: macAppMenu,
|
||||||
file: fileMenu,
|
file: fileMenu,
|
||||||
edit: editMenu,
|
edit: editMenu,
|
||||||
@ -273,7 +278,7 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!isMac) {
|
if (!isMac) {
|
||||||
delete appMenu.mac;
|
delete (appMenu as AppMenus).mac;
|
||||||
}
|
}
|
||||||
|
|
||||||
const menu = Menu.buildFromTemplate(Object.values(appMenu));
|
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
|
// 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)
|
// the application menus (https://github.com/electron-userland/spectron/issues/21)
|
||||||
ipcMain.on("test-menu-item-click", (event: IpcMainEvent, ...names: string[]) => {
|
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[] = [];
|
const parentLabels: string[] = [];
|
||||||
let menuItem: MenuItem;
|
|
||||||
|
|
||||||
for (const name of names) {
|
for (const name of names) {
|
||||||
parentLabels.push(name);
|
parentLabels.push(name);
|
||||||
|
|||||||
@ -8,25 +8,15 @@ export class PrometheusHelm extends PrometheusLens {
|
|||||||
name = "Helm";
|
name = "Helm";
|
||||||
rateAccuracy = "5m";
|
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";
|
const labelSelector = "app=prometheus,component=server,heritage=Helm";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const serviceList = await client.listServiceForAllNamespaces(false, "", null, labelSelector);
|
const serviceList = await client.listServiceForAllNamespaces(false, "", undefined, labelSelector);
|
||||||
const service = serviceList.body.items[0];
|
|
||||||
|
|
||||||
if (!service) return;
|
return super.getPrometheusServiceRaw(serviceList.body.items[0]);
|
||||||
|
|
||||||
return {
|
|
||||||
id: this.id,
|
|
||||||
namespace: service.metadata.namespace,
|
|
||||||
service: service.metadata.name,
|
|
||||||
port: service.spec.ports[0].port
|
|
||||||
};
|
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
logger.warn(`PrometheusHelm: failed to list services: ${error.toString()}`);
|
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 { CoreV1Api } from "@kubernetes/client-node";
|
||||||
import logger from "../logger";
|
import logger from "../logger";
|
||||||
|
|
||||||
export class PrometheusLens implements PrometheusProvider {
|
export class PrometheusLens extends PrometheusProvider {
|
||||||
id = "lens";
|
id = "lens";
|
||||||
name = "Lens";
|
name = "Lens";
|
||||||
rateAccuracy = "1m";
|
rateAccuracy = "1m";
|
||||||
|
|
||||||
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService> {
|
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
||||||
try {
|
try {
|
||||||
const resp = await client.readNamespacedService("prometheus", "lens-metrics");
|
const resp = await client.readNamespacedService("prometheus", "lens-metrics");
|
||||||
const service = resp.body;
|
|
||||||
|
|
||||||
return {
|
return super.getPrometheusServiceRaw(resp.body);
|
||||||
id: this.id,
|
|
||||||
namespace: service.metadata.namespace,
|
|
||||||
service: service.metadata.name,
|
|
||||||
port: service.spec.ports[0].port
|
|
||||||
};
|
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
logger.warn(`PrometheusLens: failed to list services: ${error.response.body.message}`);
|
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) {
|
switch(opts.category) {
|
||||||
case "cluster":
|
case "cluster":
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1,39 +1,40 @@
|
|||||||
import { PrometheusProvider, PrometheusQueryOpts, PrometheusQuery, PrometheusService } from "./provider-registry";
|
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";
|
import logger from "../logger";
|
||||||
|
|
||||||
export class PrometheusOperator implements PrometheusProvider {
|
export class PrometheusOperator extends PrometheusProvider {
|
||||||
rateAccuracy = "1m";
|
rateAccuracy = "1m";
|
||||||
id = "operator";
|
id = "operator";
|
||||||
name = "Prometheus Operator";
|
name = "Prometheus Operator";
|
||||||
|
|
||||||
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService> {
|
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
||||||
try {
|
try {
|
||||||
let service: V1Service;
|
let serviceItem;
|
||||||
|
|
||||||
for (const labelSelector of ["operated-prometheus=true", "self-monitor=true"]) {
|
for (const labelSelector of ["operated-prometheus=true", "self-monitor=true"]) {
|
||||||
if (!service) {
|
serviceItem ??= (
|
||||||
const serviceList = await client.listServiceForAllNamespaces(null, null, null, labelSelector);
|
await client.listServiceForAllNamespaces(undefined, undefined, undefined, labelSelector)
|
||||||
|
)?.body.items[0];
|
||||||
service = serviceList.body.items[0];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!service) return;
|
|
||||||
|
|
||||||
return {
|
const { metadata, spec } = serviceItem ?? {};
|
||||||
id: this.id,
|
const { namespace, name: service } = metadata ?? {};
|
||||||
namespace: service.metadata.namespace,
|
const { ports: [{ port }] = [] } = spec ?? {};
|
||||||
service: service.metadata.name,
|
|
||||||
port: service.spec.ports[0].port
|
if (port && namespace && service) {
|
||||||
};
|
return {
|
||||||
|
id: this.id,
|
||||||
|
namespace,
|
||||||
|
service,
|
||||||
|
port,
|
||||||
|
};
|
||||||
|
}
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
logger.warn(`PrometheusOperator: failed to list services: ${error.toString()}`);
|
logger.warn(`PrometheusOperator: failed to list services: ${error.toString()}`);
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getQueries(opts: PrometheusQueryOpts): PrometheusQuery {
|
public getQueries(opts: PrometheusQueryOpts): PrometheusQuery | undefined {
|
||||||
switch(opts.category) {
|
switch(opts.category) {
|
||||||
case "cluster":
|
case "cluster":
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { CoreV1Api } from "@kubernetes/client-node";
|
import { CoreV1Api, V1Service } from "@kubernetes/client-node";
|
||||||
|
|
||||||
export type PrometheusClusterQuery = {
|
export type PrometheusClusterQuery = {
|
||||||
memoryUsage: string;
|
memoryUsage: string;
|
||||||
@ -59,11 +59,26 @@ export type PrometheusService = {
|
|||||||
port: number;
|
port: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface PrometheusProvider {
|
export abstract class PrometheusProvider {
|
||||||
id: string;
|
abstract id: string;
|
||||||
name: string;
|
abstract name: string;
|
||||||
getQueries(opts: PrometheusQueryOpts): PrometheusQuery;
|
abstract getQueries(opts: PrometheusQueryOpts): PrometheusQuery | undefined;
|
||||||
getPrometheusService(client: CoreV1Api): Promise<PrometheusService>;
|
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 = {
|
export type PrometheusProviderList = {
|
||||||
|
|||||||
@ -2,28 +2,22 @@ import { PrometheusProvider, PrometheusQueryOpts, PrometheusQuery, PrometheusSer
|
|||||||
import { CoreV1Api } from "@kubernetes/client-node";
|
import { CoreV1Api } from "@kubernetes/client-node";
|
||||||
import logger from "../logger";
|
import logger from "../logger";
|
||||||
|
|
||||||
export class PrometheusStacklight implements PrometheusProvider {
|
export class PrometheusStacklight extends PrometheusProvider {
|
||||||
id = "stacklight";
|
id = "stacklight";
|
||||||
name = "Stacklight";
|
name = "Stacklight";
|
||||||
rateAccuracy = "1m";
|
rateAccuracy = "1m";
|
||||||
|
|
||||||
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService> {
|
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
||||||
try {
|
try {
|
||||||
const resp = await client.readNamespacedService("prometheus-server", "stacklight");
|
const resp = await client.readNamespacedService("prometheus-server", "stacklight");
|
||||||
const service = resp.body;
|
|
||||||
|
|
||||||
return {
|
return super.getPrometheusServiceRaw(resp.body);
|
||||||
id: this.id,
|
|
||||||
namespace: service.metadata.namespace,
|
|
||||||
service: service.metadata.name,
|
|
||||||
port: service.spec.ports[0].port
|
|
||||||
};
|
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
logger.warn(`PrometheusLens: failed to list services: ${error.response.body.message}`);
|
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) {
|
switch(opts.category) {
|
||||||
case "cluster":
|
case "cluster":
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import path from "path";
|
|||||||
import * as tempy from "tempy";
|
import * as tempy from "tempy";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { appEventBus } from "../common/event-bus";
|
import { appEventBus } from "../common/event-bus";
|
||||||
import { cloneJsonObject } from "../common/utils";
|
import { assert, cloneJsonObject, NotFalsy } from "../common/utils";
|
||||||
|
|
||||||
export class ResourceApplier {
|
export class ResourceApplier {
|
||||||
constructor(protected cluster: Cluster) {
|
constructor(protected cluster: Cluster) {
|
||||||
@ -21,7 +21,7 @@ export class ResourceApplier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async kubectlApply(content: string): Promise<string> {
|
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 kubectlPath = await kubeCtl.getPath();
|
||||||
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
|
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
|
||||||
|
|
||||||
@ -53,7 +53,7 @@ export class ResourceApplier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async kubectlApplyAll(resources: string[]): Promise<string> {
|
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 kubectlPath = await kubeCtl.getPath();
|
||||||
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
|
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
|
||||||
|
|
||||||
|
|||||||
@ -11,12 +11,12 @@ import logger from "./logger";
|
|||||||
export interface RouterRequestOpts {
|
export interface RouterRequestOpts {
|
||||||
req: http.IncomingMessage;
|
req: http.IncomingMessage;
|
||||||
res: http.ServerResponse;
|
res: http.ServerResponse;
|
||||||
cluster: Cluster;
|
cluster: Cluster | null;
|
||||||
params: RouteParams;
|
params: RouteParams;
|
||||||
url: URL;
|
url: URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RouteParams extends Record<string, string> {
|
export interface RouteParams extends Record<string, string | undefined> {
|
||||||
path?: string; // *-route
|
path?: string; // *-route
|
||||||
namespace?: string;
|
namespace?: string;
|
||||||
service?: string;
|
service?: string;
|
||||||
@ -30,7 +30,7 @@ export interface LensApiRequest<P = any> {
|
|||||||
path: string;
|
path: string;
|
||||||
payload: P;
|
payload: P;
|
||||||
params: RouteParams;
|
params: RouteParams;
|
||||||
cluster: Cluster;
|
cluster: Cluster | null;
|
||||||
response: http.ServerResponse;
|
response: http.ServerResponse;
|
||||||
query: URLSearchParams;
|
query: URLSearchParams;
|
||||||
raw: {
|
raw: {
|
||||||
@ -52,10 +52,10 @@ export class Router {
|
|||||||
return path.resolve(__static);
|
return path.resolve(__static);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async route(cluster: Cluster, req: http.IncomingMessage, res: http.ServerResponse): Promise<boolean> {
|
public async route(cluster: Cluster | null, req: http.IncomingMessage, res: http.ServerResponse): Promise<boolean> {
|
||||||
const url = new URL(req.url, "http://localhost");
|
const url = new URL(req.url ?? "", "http://localhost");
|
||||||
const path = url.pathname;
|
const path = url.pathname;
|
||||||
const method = req.method.toLowerCase();
|
const method = req.method?.toLowerCase() ?? "get";
|
||||||
const matchingRoute = this.router.route(method, path);
|
const matchingRoute = this.router.route(method, path);
|
||||||
const routeFound = !matchingRoute.isBoom;
|
const routeFound = !matchingRoute.isBoom;
|
||||||
|
|
||||||
@ -118,6 +118,13 @@ export class Router {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!req.url) {
|
||||||
|
logger.error("handleStaticFile: no URL in request");
|
||||||
|
res.statusCode = 404;
|
||||||
|
|
||||||
|
return res.end();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const filename = path.basename(req.url);
|
const filename = path.basename(req.url);
|
||||||
// redirect requests to [appName].js, [appName].html /sockjs-node/ to webpack-dev-server (for hot-reload support)
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await readFile(asset);
|
const data = await readFile(asset);
|
||||||
|
|
||||||
res.setHeader("Content-Type", this.getMimeType(asset));
|
res.setHeader("Content-Type", this.getMimeType(asset));
|
||||||
@ -141,10 +149,10 @@ export class Router {
|
|||||||
if (retryCount > 5) {
|
if (retryCount > 5) {
|
||||||
logger.error("handleStaticFile:", err.toString());
|
logger.error("handleStaticFile:", err.toString());
|
||||||
res.statusCode = 404;
|
res.statusCode = 404;
|
||||||
res.end();
|
|
||||||
|
|
||||||
return;
|
return res.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.handleStaticFile(`${publicPath}/${appName}.html`, res, req, Math.max(retryCount, 0) + 1);
|
this.handleStaticFile(`${publicPath}/${appName}.html`, res, req, Math.max(retryCount, 0) + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -154,7 +162,9 @@ export class Router {
|
|||||||
this.router.add(
|
this.router.add(
|
||||||
{ method: "get", path: "/{path*}" },
|
{ method: "get", path: "/{path*}" },
|
||||||
({ params, response, raw: { req } }: LensApiRequest) => {
|
({ 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));
|
this.router.add({ method: "get", path: "/version"}, versionRoute.getVersion.bind(versionRoute));
|
||||||
|
|||||||
@ -17,7 +17,7 @@ class HelmApiRoute extends LensApi {
|
|||||||
try {
|
try {
|
||||||
const chart = await helmService.getChart(params.repo, params.chart, query.get("version"));
|
const chart = await helmService.getChart(params.repo, params.chart, query.get("version"));
|
||||||
|
|
||||||
this.respondJson(response, chart);
|
this.respondJson(response, chart ?? {});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.respondText(response, error, 422);
|
this.respondText(response, error, 422);
|
||||||
}
|
}
|
||||||
@ -29,7 +29,7 @@ class HelmApiRoute extends LensApi {
|
|||||||
try {
|
try {
|
||||||
const values = await helmService.getChartValues(params.repo, params.chart, query.get("version"));
|
const values = await helmService.getChartValues(params.repo, params.chart, query.get("version"));
|
||||||
|
|
||||||
this.respondJson(response, values);
|
this.respondJson(response, values ?? {});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.respondText(response, error, 422);
|
this.respondText(response, error, 422);
|
||||||
}
|
}
|
||||||
@ -38,10 +38,16 @@ class HelmApiRoute extends LensApi {
|
|||||||
public async installChart(request: LensApiRequest) {
|
public async installChart(request: LensApiRequest) {
|
||||||
const { payload, cluster, response } = request;
|
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 {
|
try {
|
||||||
const result = await helmService.installChart(cluster, payload);
|
const result = await helmService.installChart(cluster, payload);
|
||||||
|
|
||||||
this.respondJson(response, result, 201);
|
this.respondJson(response, result ?? {}, 201);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.debug(error);
|
logger.debug(error);
|
||||||
this.respondText(response, error, 422);
|
this.respondText(response, error, 422);
|
||||||
@ -51,10 +57,16 @@ class HelmApiRoute extends LensApi {
|
|||||||
public async updateRelease(request: LensApiRequest) {
|
public async updateRelease(request: LensApiRequest) {
|
||||||
const { cluster, params, payload, response } = request;
|
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 {
|
try {
|
||||||
const result = await helmService.updateRelease(cluster, params.release, params.namespace, payload );
|
const result = await helmService.updateRelease(cluster, params.release, params.namespace, payload );
|
||||||
|
|
||||||
this.respondJson(response, result);
|
this.respondJson(response, result ?? {});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.debug(error);
|
logger.debug(error);
|
||||||
this.respondText(response, error, 422);
|
this.respondText(response, error, 422);
|
||||||
@ -64,10 +76,16 @@ class HelmApiRoute extends LensApi {
|
|||||||
public async rollbackRelease(request: LensApiRequest) {
|
public async rollbackRelease(request: LensApiRequest) {
|
||||||
const { cluster, params, payload, response } = request;
|
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 {
|
try {
|
||||||
const result = await helmService.rollback(cluster, params.release, params.namespace, payload.revision);
|
const result = await helmService.rollback(cluster, params.release, params.namespace, payload.revision);
|
||||||
|
|
||||||
this.respondJson(response, result);
|
this.respondJson(response, result ?? {});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.debug(error);
|
logger.debug(error);
|
||||||
this.respondText(response, error, 422);
|
this.respondText(response, error, 422);
|
||||||
@ -77,10 +95,16 @@ class HelmApiRoute extends LensApi {
|
|||||||
public async listReleases(request: LensApiRequest) {
|
public async listReleases(request: LensApiRequest) {
|
||||||
const { cluster, params, response } = request;
|
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 {
|
try {
|
||||||
const result = await helmService.listReleases(cluster, params.namespace);
|
const result = await helmService.listReleases(cluster, params.namespace);
|
||||||
|
|
||||||
this.respondJson(response, result);
|
this.respondJson(response, result ?? {});
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
logger.debug(error);
|
logger.debug(error);
|
||||||
this.respondText(response, error, 422);
|
this.respondText(response, error, 422);
|
||||||
@ -90,6 +114,12 @@ class HelmApiRoute extends LensApi {
|
|||||||
public async getRelease(request: LensApiRequest) {
|
public async getRelease(request: LensApiRequest) {
|
||||||
const { cluster, params, response } = request;
|
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 {
|
try {
|
||||||
const result = await helmService.getRelease(cluster, params.release, params.namespace);
|
const result = await helmService.getRelease(cluster, params.release, params.namespace);
|
||||||
|
|
||||||
@ -103,10 +133,16 @@ class HelmApiRoute extends LensApi {
|
|||||||
public async getReleaseValues(request: LensApiRequest) {
|
public async getReleaseValues(request: LensApiRequest) {
|
||||||
const { cluster, params, response } = request;
|
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 {
|
try {
|
||||||
const result = await helmService.getReleaseValues(cluster, params.release, params.namespace);
|
const result = await helmService.getReleaseValues(cluster, params.release, params.namespace);
|
||||||
|
|
||||||
this.respondText(response, result);
|
this.respondText(response, result ?? "");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.debug(error);
|
logger.debug(error);
|
||||||
this.respondText(response, error, 422);
|
this.respondText(response, error, 422);
|
||||||
@ -116,6 +152,12 @@ class HelmApiRoute extends LensApi {
|
|||||||
public async getReleaseHistory(request: LensApiRequest) {
|
public async getReleaseHistory(request: LensApiRequest) {
|
||||||
const { cluster, params, response } = request;
|
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 {
|
try {
|
||||||
const result = await helmService.getReleaseHistory(cluster, params.release, params.namespace);
|
const result = await helmService.getReleaseHistory(cluster, params.release, params.namespace);
|
||||||
|
|
||||||
@ -129,10 +171,16 @@ class HelmApiRoute extends LensApi {
|
|||||||
public async deleteRelease(request: LensApiRequest) {
|
public async deleteRelease(request: LensApiRequest) {
|
||||||
const { cluster, params, response } = request;
|
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 {
|
try {
|
||||||
const result = await helmService.deleteRelease(cluster, params.release, params.namespace);
|
const result = await helmService.deleteRelease(cluster, params.release, params.namespace);
|
||||||
|
|
||||||
this.respondJson(response, result);
|
this.respondJson(response, result ?? "");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.debug(error);
|
logger.debug(error);
|
||||||
this.respondText(response, error, 422);
|
this.respondText(response, error, 422);
|
||||||
|
|||||||
@ -2,8 +2,15 @@ import { LensApiRequest } from "../router";
|
|||||||
import { LensApi } from "../lens-api";
|
import { LensApi } from "../lens-api";
|
||||||
import { Cluster } from "../cluster";
|
import { Cluster } from "../cluster";
|
||||||
import { CoreV1Api, V1Secret } from "@kubernetes/client-node";
|
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) {
|
function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster) {
|
||||||
|
if (!secret.data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const tokenData = Buffer.from(secret.data["token"], "base64");
|
const tokenData = Buffer.from(secret.data["token"], "base64");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -32,7 +39,7 @@ function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster
|
|||||||
"context": {
|
"context": {
|
||||||
"user": username,
|
"user": username,
|
||||||
"cluster": cluster.contextName,
|
"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 {
|
class KubeconfigRoute extends LensApi {
|
||||||
|
|
||||||
public async routeServiceAccountRoute(request: LensApiRequest) {
|
public async routeServiceAccountRoute(request: LensApiRequest) {
|
||||||
const { params, response, cluster} = request;
|
const { params, response, cluster: maybeCluster } = 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;
|
|
||||||
|
|
||||||
return annotations && annotations["kubernetes.io/service-account.name"] == params.account;
|
try {
|
||||||
});
|
const cluster = assert(maybeCluster, "No Cluster defined on request");
|
||||||
const data = generateKubeConfig(params.account, secret, cluster);
|
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 { Cluster, ClusterMetadataKey } from "../cluster";
|
||||||
import { ClusterPrometheusMetadata } from "../../common/cluster-store";
|
import { ClusterPrometheusMetadata } from "../../common/cluster-store";
|
||||||
import logger from "../logger";
|
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[] | {
|
export type IMetricsQuery = string | string[] | {
|
||||||
[metricName: string]: string;
|
[metricName: string]: string;
|
||||||
@ -41,50 +44,69 @@ async function loadMetrics(promQueries: string[], cluster: Cluster, prometheusPa
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MetricsRoute extends LensApi {
|
class MetricsRoute extends LensApi {
|
||||||
async routeMetrics({ response, cluster, payload, query }: LensApiRequest) {
|
async routeMetrics({ response, cluster: maybeCluster, payload, query }: LensApiRequest) {
|
||||||
const queryParams: IMetricsQuery = Object.fromEntries(query.entries());
|
|
||||||
const prometheusMetadata: ClusterPrometheusMetadata = {};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [prometheusPath, prometheusProvider] = await Promise.all([
|
const cluster = assert(maybeCluster, "No Cluster defined on request");
|
||||||
cluster.contextHandler.getPrometheusPath(),
|
const contextHandler = assert(cluster.contextHandler, "Cluster must be initialized to be routed against");
|
||||||
cluster.contextHandler.getPrometheusProvider()
|
|
||||||
]);
|
|
||||||
|
|
||||||
prometheusMetadata.provider = prometheusProvider?.id;
|
const queryParams: IMetricsQuery = Object.fromEntries(query.entries());
|
||||||
prometheusMetadata.autoDetected = !cluster.preferences.prometheusProvider?.type;
|
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;
|
prometheusMetadata.success = false;
|
||||||
this.respondJson(response, {});
|
this.respondJson(response, {});
|
||||||
|
} finally {
|
||||||
return;
|
cluster.metadata[ClusterMetadataKey.PROMETHEUS] = prometheusMetadata;
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[METRICS-ROUTE]: routeMetrics failed: ${error}`);
|
||||||
|
|
||||||
// return data in same structure as query
|
if (error instanceof AssertionError) {
|
||||||
if (typeof payload === "string") {
|
this.respondText(response, error.message, 404);
|
||||||
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 {
|
} else {
|
||||||
const queries = Object.entries(payload).map(([queryName, queryOpts]) => (
|
this.respondText(response, error.toString(), 404);
|
||||||
(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);
|
|
||||||
}
|
}
|
||||||
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 { shell } from "electron";
|
||||||
import * as tcpPortUsed from "tcp-port-used";
|
import * as tcpPortUsed from "tcp-port-used";
|
||||||
import logger from "../logger";
|
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 {
|
class PortForward {
|
||||||
public static portForwards: PortForward[] = [];
|
public static portForwards: PortForward[] = [];
|
||||||
|
|
||||||
static getPortforward(forward: {clusterId: string; kind: string; name: string; namespace: string; port: string}) {
|
static getPortforward(forward: GetPortForwardOptions) {
|
||||||
return PortForward.portForwards.find((pf) => {
|
return PortForward.portForwards.find(pf => (
|
||||||
return (
|
pf.clusterId == forward.clusterId &&
|
||||||
pf.clusterId == forward.clusterId &&
|
pf.kind == forward.kind &&
|
||||||
pf.kind == forward.kind &&
|
pf.name == forward.name &&
|
||||||
pf.name == forward.name &&
|
pf.namespace == forward.namespace &&
|
||||||
pf.namespace == forward.namespace &&
|
pf.port == forward.port
|
||||||
pf.port == forward.port
|
));
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public clusterId: string;
|
public clusterId: string;
|
||||||
public process: ChildProcessWithoutNullStreams;
|
public process?: ChildProcessWithoutNullStreams;
|
||||||
public kubeConfig: string;
|
public kubeConfig: string;
|
||||||
public kind: string;
|
public kind: string;
|
||||||
public namespace: string;
|
public namespace: string;
|
||||||
public name: string;
|
public name: string;
|
||||||
public port: string;
|
public port: string;
|
||||||
public localPort: number;
|
public localPort?: number;
|
||||||
|
|
||||||
constructor(obj: any) {
|
constructor(obj: PortForwardOpts) {
|
||||||
Object.assign(this, obj);
|
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() {
|
public async start() {
|
||||||
@ -75,39 +101,51 @@ class PortForward {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class PortForwardRoute extends LensApi {
|
class PortForwardRoute extends LensApi {
|
||||||
|
|
||||||
public async routePortForward(request: LensApiRequest) {
|
public async routePortForward(request: LensApiRequest) {
|
||||||
const { params, response, cluster} = request;
|
const { params, response, cluster: maybeCluster } = request;
|
||||||
const { namespace, port, resourceType, resourceName } = params;
|
|
||||||
let portForward = PortForward.getPortforward({
|
|
||||||
clusterId: cluster.id, kind: resourceType, name: resourceName,
|
|
||||||
namespace, port
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!portForward) {
|
try {
|
||||||
logger.info(`Creating a new port-forward ${namespace}/${resourceType}/${resourceName}:${port}`);
|
const cluster = assert(maybeCluster, "No Cluster defined on request");
|
||||||
portForward = new PortForward({
|
const namespace = assert(params.namespace, "Namespace not provided");
|
||||||
clusterId: cluster.id,
|
const port = assert(params.port, "Port not provided");
|
||||||
kind: resourceType,
|
const name = assert(params.resourceName, "ResourceName not provided");
|
||||||
namespace,
|
const kind = assert(params.resourceType, "ResourceName not provided");
|
||||||
name: resourceName,
|
|
||||||
port,
|
|
||||||
kubeConfig: await cluster.getProxyKubeconfigPath()
|
|
||||||
});
|
|
||||||
const started = await portForward.start();
|
|
||||||
|
|
||||||
if (!started) {
|
let portForward = PortForward.getPortforward({ clusterId: cluster.id, kind, name, namespace, port });
|
||||||
this.respondJson(response, {
|
|
||||||
message: "Failed to open port-forward"
|
|
||||||
}, 400);
|
|
||||||
|
|
||||||
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 { LensApiRequest } from "../router";
|
||||||
import { LensApi } from "../lens-api";
|
import { LensApi } from "../lens-api";
|
||||||
import { ResourceApplier } from "../resource-applier";
|
import { ResourceApplier } from "../resource-applier";
|
||||||
|
import { assert } from "../../common/utils";
|
||||||
|
import { AssertionError } from "assert";
|
||||||
|
import logger from "../logger";
|
||||||
|
|
||||||
class ResourceApplierApiRoute extends LensApi {
|
class ResourceApplierApiRoute extends LensApi {
|
||||||
public async applyResource(request: LensApiRequest) {
|
public async applyResource(request: LensApiRequest) {
|
||||||
const { response, cluster, payload } = request;
|
const { response, cluster: maybeCluster, payload } = request;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const cluster = assert(maybeCluster, "No Cluster defined on request");
|
||||||
const resource = await new ResourceApplier(cluster).apply(payload);
|
const resource = await new ResourceApplier(cluster).apply(payload);
|
||||||
|
|
||||||
this.respondJson(response, [resource], 200);
|
this.respondJson(response, [resource], 200);
|
||||||
} catch (error) {
|
} 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";
|
ShellType = "node-shell";
|
||||||
|
|
||||||
protected podId = `node-shell-${uuid()}`;
|
protected podId = `node-shell-${uuid()}`;
|
||||||
protected kc: KubeConfig;
|
|
||||||
|
|
||||||
constructor(socket: WebSocket, cluster: Cluster, protected nodeName: string) {
|
constructor(socket: WebSocket, cluster: Cluster, protected nodeName: string) {
|
||||||
super(socket, cluster);
|
super(socket, cluster);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async open() {
|
public async open() {
|
||||||
this.kc = await this.cluster.getProxyKubeconfig();
|
const kc = await this.cluster.getProxyKubeconfig();
|
||||||
const shell = await this.kubectl.getPath();
|
const shell = await this.kubectl.getPath();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.createNodeShellPod();
|
await this.createNodeShellPod(kc);
|
||||||
await this.waitForRunningPod();
|
await this.waitForRunningPod(kc);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.deleteNodeShellPod();
|
this.deleteNodeShellPod(kc);
|
||||||
this.sendResponse("Error occurred. ");
|
this.sendResponse("Error occurred. ");
|
||||||
|
|
||||||
throw new ShellOpenError("failed to create node pod", error);
|
throw new ShellOpenError("failed to create node pod", error);
|
||||||
@ -35,9 +34,8 @@ export class NodeShellSession extends ShellSession {
|
|||||||
super.open(shell, args, env);
|
super.open(shell, args, env);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected createNodeShellPod() {
|
protected createNodeShellPod(kc: KubeConfig) {
|
||||||
return this
|
return kc
|
||||||
.kc
|
|
||||||
.makeApiClient(k8s.CoreV1Api)
|
.makeApiClient(k8s.CoreV1Api)
|
||||||
.createNamespacedPod("kube-system", {
|
.createNamespacedPod("kube-system", {
|
||||||
metadata: {
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
const watch = new k8s.Watch(this.kc);
|
const watch = new k8s.Watch(kc);
|
||||||
|
|
||||||
watch
|
watch
|
||||||
.watch(`/api/v1/namespaces/kube-system/pods`,
|
.watch(`/api/v1/namespaces/kube-system/pods`,
|
||||||
@ -99,9 +97,8 @@ export class NodeShellSession extends ShellSession {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected deleteNodeShellPod() {
|
protected deleteNodeShellPod(kc: KubeConfig) {
|
||||||
this
|
kc
|
||||||
.kc
|
|
||||||
.makeApiClient(k8s.CoreV1Api)
|
.makeApiClient(k8s.CoreV1Api)
|
||||||
.deleteNamespacedPod(this.podId, "kube-system");
|
.deleteNamespacedPod(this.podId, "kube-system");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,9 +25,9 @@ export abstract class ShellSession {
|
|||||||
|
|
||||||
protected kubectl: Kubectl;
|
protected kubectl: Kubectl;
|
||||||
protected running = false;
|
protected running = false;
|
||||||
protected shellProcess: pty.IPty;
|
protected shellProcess?: pty.IPty;
|
||||||
protected kubectlBinDirP: Promise<string>;
|
protected kubectlBinDirP: Promise<string>;
|
||||||
protected kubeconfigPathP: Promise<string>;
|
protected kubeconfigPathP: Promise<string | undefined>;
|
||||||
|
|
||||||
protected get cwd(): string | undefined {
|
protected get cwd(): string | undefined {
|
||||||
return this.cluster.preferences?.terminalCWD;
|
return this.cluster.preferences?.terminalCWD;
|
||||||
@ -71,19 +71,21 @@ export abstract class ShellSession {
|
|||||||
|
|
||||||
switch (data[0]) {
|
switch (data[0]) {
|
||||||
case "0":
|
case "0":
|
||||||
this.shellProcess.write(message);
|
this.shellProcess?.write(message);
|
||||||
break;
|
break;
|
||||||
case "4":
|
case "4":
|
||||||
const { Width, Height } = JSON.parse(message);
|
const { Width, Height } = JSON.parse(message);
|
||||||
|
|
||||||
this.shellProcess.resize(Width, Height);
|
this.shellProcess?.resize(Width, Height);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on("close", () => {
|
.on("close", () => {
|
||||||
if (this.running) {
|
if (this.running) {
|
||||||
try {
|
try {
|
||||||
process.kill(this.shellProcess.pid);
|
if (this.shellProcess?.pid) {
|
||||||
|
process.kill(this.shellProcess?.pid);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { exitApp } from "./exit-app";
|
|||||||
const TRAY_LOG_PREFIX = "[TRAY]";
|
const TRAY_LOG_PREFIX = "[TRAY]";
|
||||||
|
|
||||||
// note: instance of Tray should be saved somewhere, otherwise it disappears
|
// note: instance of Tray should be saved somewhere, otherwise it disappears
|
||||||
export let tray: Tray;
|
export let tray: Tray | undefined;
|
||||||
|
|
||||||
export function getTrayIcon(): string {
|
export function getTrayIcon(): string {
|
||||||
return path.resolve(
|
return path.resolve(
|
||||||
@ -43,7 +43,7 @@ export function initTray(windowManager: WindowManager) {
|
|||||||
try {
|
try {
|
||||||
const menu = createTrayMenu(windowManager);
|
const menu = createTrayMenu(windowManager);
|
||||||
|
|
||||||
tray.setContextMenu(menu);
|
tray?.setContextMenu(menu);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`${TRAY_LOG_PREFIX}: building failed`, { error });
|
logger.error(`${TRAY_LOG_PREFIX}: building failed`, { error });
|
||||||
}
|
}
|
||||||
@ -53,7 +53,7 @@ export function initTray(windowManager: WindowManager) {
|
|||||||
return () => {
|
return () => {
|
||||||
disposers.forEach(disposer => disposer());
|
disposers.forEach(disposer => disposer());
|
||||||
tray?.destroy();
|
tray?.destroy();
|
||||||
tray = null;
|
tray = undefined;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,12 +12,12 @@ import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
|
|||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
|
|
||||||
export class WindowManager extends Singleton {
|
export class WindowManager extends Singleton {
|
||||||
protected mainWindow: BrowserWindow;
|
protected mainWindow: BrowserWindow | null = null;
|
||||||
protected splashWindow: BrowserWindow;
|
protected splashWindow: BrowserWindow | null = null;
|
||||||
protected windowState: windowStateKeeper.State;
|
protected windowState: windowStateKeeper.State | null = null;
|
||||||
protected disposers: Record<string, Function> = {};
|
protected disposers: Record<string, Function> = {};
|
||||||
|
|
||||||
@observable activeClusterId: ClusterId;
|
@observable activeClusterId?: ClusterId;
|
||||||
|
|
||||||
constructor(protected proxyPort: number) {
|
constructor(protected proxyPort: number) {
|
||||||
super();
|
super();
|
||||||
@ -30,7 +30,7 @@ export class WindowManager extends Singleton {
|
|||||||
return `http://localhost:${this.proxyPort}`;
|
return `http://localhost:${this.proxyPort}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async initMainWindow(showSplash = true) {
|
async initMainWindow(showSplash = true): Promise<BrowserWindow> {
|
||||||
// Manage main window size and position with state persistence
|
// Manage main window size and position with state persistence
|
||||||
if (!this.windowState) {
|
if (!this.windowState) {
|
||||||
this.windowState = windowStateKeeper({
|
this.windowState = windowStateKeeper({
|
||||||
@ -77,7 +77,7 @@ export class WindowManager extends Singleton {
|
|||||||
|
|
||||||
// clean up
|
// clean up
|
||||||
this.mainWindow.on("closed", () => {
|
this.mainWindow.on("closed", () => {
|
||||||
this.windowState.unmanage();
|
this.windowState?.unmanage();
|
||||||
this.mainWindow = null;
|
this.mainWindow = null;
|
||||||
this.splashWindow = null;
|
this.splashWindow = null;
|
||||||
app.dock?.hide(); // hide icon in dock (mac-os)
|
app.dock?.hide(); // hide icon in dock (mac-os)
|
||||||
@ -104,6 +104,8 @@ export class WindowManager extends Singleton {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
dialog.showErrorBox("ERROR!", err.toString());
|
dialog.showErrorBox("ERROR!", err.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return this.mainWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async initMenu() {
|
protected async initMenu() {
|
||||||
@ -122,17 +124,18 @@ export class WindowManager extends Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async ensureMainWindow(): Promise<BrowserWindow> {
|
async ensureMainWindow(): Promise<BrowserWindow> {
|
||||||
if (!this.mainWindow) await this.initMainWindow();
|
const mainWindow = this.mainWindow ?? await this.initMainWindow();
|
||||||
this.mainWindow.show();
|
|
||||||
|
|
||||||
return this.mainWindow;
|
mainWindow.show();
|
||||||
|
|
||||||
|
return mainWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendToView({ channel, frameInfo, data = [] }: { channel: string, frameInfo?: ClusterFrameInfo, data?: any[] }) {
|
sendToView({ channel, frameInfo, data = [] }: { channel: string, frameInfo?: ClusterFrameInfo, data?: any[] }) {
|
||||||
if (frameInfo) {
|
if (frameInfo) {
|
||||||
this.mainWindow.webContents.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...data);
|
this.mainWindow?.webContents.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...data);
|
||||||
} else {
|
} else {
|
||||||
this.mainWindow.webContents.send(channel, ...data);
|
this.mainWindow?.webContents.send(channel, ...data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,7 +155,7 @@ export class WindowManager extends Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reload() {
|
reload() {
|
||||||
const frameInfo = clusterFrameMap.get(this.activeClusterId);
|
const frameInfo = this.activeClusterId && clusterFrameMap.get(this.activeClusterId);
|
||||||
|
|
||||||
if (frameInfo) {
|
if (frameInfo) {
|
||||||
this.sendToView({ channel: IpcRendererNavigationEvents.RELOAD_PAGE, frameInfo });
|
this.sendToView({ channel: IpcRendererNavigationEvents.RELOAD_PAGE, frameInfo });
|
||||||
@ -186,8 +189,8 @@ export class WindowManager extends Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.mainWindow.destroy();
|
this.mainWindow?.destroy();
|
||||||
this.splashWindow.destroy();
|
this.splashWindow?.destroy();
|
||||||
this.mainWindow = null;
|
this.mainWindow = null;
|
||||||
this.splashWindow = null;
|
this.splashWindow = null;
|
||||||
Object.entries(this.disposers).forEach(([name, dispose]) => {
|
Object.entries(this.disposers).forEach(([name, dispose]) => {
|
||||||
|
|||||||
@ -26,7 +26,7 @@ export default migration({
|
|||||||
*/
|
*/
|
||||||
try {
|
try {
|
||||||
// take the embedded kubeconfig and dump it into a file
|
// 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();
|
cluster.contextName = loadConfig(cluster.kubeConfigPath).getCurrentContext();
|
||||||
delete cluster.kubeConfig;
|
delete cluster.kubeConfig;
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ export default migration({
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
printLog(`Failed to migrate cluster icon for cluster "${cluster.id}"`, error);
|
printLog(`Failed to migrate cluster icon for cluster "${cluster.id}"`, error);
|
||||||
delete cluster.preferences.icon;
|
delete cluster.preferences?.icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
return cluster;
|
return cluster;
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { IKubeApiParsed, parseKubeApi } from "../kube-api-parse";
|
|||||||
/**
|
/**
|
||||||
* [<input-url>, <expected-result>]
|
* [<input-url>, <expected-result>]
|
||||||
*/
|
*/
|
||||||
type KubeApiParseTestData = [string, Required<IKubeApiParsed>];
|
type KubeApiParseTestData = [string, IKubeApiParsed];
|
||||||
|
|
||||||
const tests: KubeApiParseTestData[] = [
|
const tests: KubeApiParseTestData[] = [
|
||||||
["/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions/prometheuses.monitoring.coreos.com", {
|
["/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions/prometheuses.monitoring.coreos.com", {
|
||||||
|
|||||||
@ -3,13 +3,14 @@ import type { KubeObjectStore } from "../kube-object.store";
|
|||||||
import { action, observable } from "mobx";
|
import { action, observable } from "mobx";
|
||||||
import { autobind } from "../utils";
|
import { autobind } from "../utils";
|
||||||
import { KubeApi, parseKubeApi } from "./kube-api";
|
import { KubeApi, parseKubeApi } from "./kube-api";
|
||||||
|
import { KubeObject } from "./kube-object";
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class ApiManager {
|
export class ApiManager {
|
||||||
private apis = observable.map<string, KubeApi>();
|
private apis = observable.map<string, KubeApi<any, any>>();
|
||||||
private stores = observable.map<string, KubeObjectStore>();
|
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") {
|
if (typeof pathOrCallback === "string") {
|
||||||
return this.apis.get(pathOrCallback) || this.apis.get(parseKubeApi(pathOrCallback).apiBase);
|
return this.apis.get(pathOrCallback) || this.apis.get(parseKubeApi(pathOrCallback).apiBase);
|
||||||
}
|
}
|
||||||
@ -18,10 +19,14 @@ export class ApiManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getApiByKind(kind: string, apiVersion: string) {
|
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)) {
|
if (!this.apis.has(apiBase)) {
|
||||||
this.stores.forEach((store) => {
|
this.stores.forEach((store) => {
|
||||||
if(store.api === api) {
|
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);
|
if (typeof api === "string") return this.getApi(api);
|
||||||
|
|
||||||
return api;
|
return api;
|
||||||
}
|
}
|
||||||
|
|
||||||
unregisterApi(api: string | KubeApi) {
|
unregisterApi<Spec, Status>(api: string | KubeApi<Spec, Status>) {
|
||||||
if (typeof api === "string") this.apis.delete(api);
|
if (typeof api === "string") this.apis.delete(api);
|
||||||
else {
|
else {
|
||||||
const apis = Array.from(this.apis.entries());
|
const apis = Array.from(this.apis.entries());
|
||||||
@ -50,14 +55,20 @@ export class ApiManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@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 => {
|
apis.forEach(api => {
|
||||||
this.stores.set(api.apiBase, store);
|
this.stores.set(api.apiBase, store);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getStore<S extends KubeObjectStore>(api: string | KubeApi): S {
|
getStore<Spec, Status, S extends KubeObjectStore<Spec, Status, KubeObject<Spec, Status>>>(api: string | KubeApi<Spec, Status>): S | undefined {
|
||||||
return this.stores.get(this.resolveApi(api)?.apiBase) as S;
|
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 { KubeObject } from "../kube-object";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
|
|
||||||
export class ClusterApi extends KubeApi<Cluster> {
|
export class ClusterApi extends KubeApi<ClusterSpec, ClusterKubeStatus, Cluster> {
|
||||||
static kind = "Cluster";
|
static kind = "Cluster";
|
||||||
static namespaced = true;
|
static namespaced = true;
|
||||||
|
|
||||||
@ -50,42 +50,43 @@ export interface IClusterMetrics<T = IMetrics> {
|
|||||||
fsUsage: T;
|
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 kind = "Cluster";
|
||||||
static apiBase = "/apis/cluster.k8s.io/v1alpha1/clusters";
|
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() {
|
getStatus() {
|
||||||
if (this.metadata.deletionTimestamp) return ClusterStatus.REMOVING;
|
if (this.metadata.deletionTimestamp) return ClusterStatus.REMOVING;
|
||||||
if (!this.status || !this.status) return ClusterStatus.CREATING;
|
if (!this.status || !this.status) return ClusterStatus.CREATING;
|
||||||
|
|||||||
@ -1,22 +1,14 @@
|
|||||||
import { KubeObject } from "../kube-object";
|
import { KubeObject } from "../kube-object";
|
||||||
import { KubeJsonApiData } from "../kube-json-api";
|
|
||||||
import { autobind } from "../../utils";
|
import { autobind } from "../../utils";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class ConfigMap extends KubeObject {
|
export class ConfigMap extends KubeObject<void, void> {
|
||||||
static kind = "ConfigMap";
|
static kind = "ConfigMap";
|
||||||
static namespaced = true;
|
static namespaced = true;
|
||||||
static apiBase = "/api/v1/configmaps";
|
static apiBase = "/api/v1/configmaps";
|
||||||
|
|
||||||
constructor(data: KubeJsonApiData) {
|
data: Record<string, string> = {};
|
||||||
super(data);
|
|
||||||
this.data = this.data || {};
|
|
||||||
}
|
|
||||||
|
|
||||||
data: {
|
|
||||||
[param: string]: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
getKeys(): string[] {
|
getKeys(): string[] {
|
||||||
return Object.keys(this.data);
|
return Object.keys(this.data);
|
||||||
|
|||||||
@ -17,93 +17,96 @@ type AdditionalPrinterColumnsV1Beta = AdditionalPrinterColumnsCommon & {
|
|||||||
JSONPath: string;
|
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 kind = "CustomResourceDefinition";
|
||||||
static namespaced = false;
|
static namespaced = false;
|
||||||
static apiBase = "/apis/apiextensions.k8s.io/v1/customresourcedefinitions";
|
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() {
|
getResourceUrl() {
|
||||||
return crdResourcesURL({
|
const group = this.getGroup();
|
||||||
params: {
|
const name = this.getPluralName();
|
||||||
group: this.getGroup(),
|
|
||||||
name: this.getPluralName(),
|
if (group && name) {
|
||||||
}
|
return crdResourcesURL({ params: { group, name } });
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getResourceApiBase() {
|
getResourceApiBase() {
|
||||||
const { group } = this.spec;
|
const { group } = this.spec ?? {};
|
||||||
|
|
||||||
return `/apis/${group}/${this.getVersion()}/${this.getPluralName()}`;
|
return `/apis/${group}/${this.getVersion()}/${this.getPluralName()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getPluralName() {
|
getPluralName() {
|
||||||
return this.getNames().plural;
|
return this.getNames()?.plural;
|
||||||
}
|
}
|
||||||
|
|
||||||
getResourceKind() {
|
getResourceKind() {
|
||||||
return this.spec.names.kind;
|
return this.spec?.names.kind;
|
||||||
}
|
}
|
||||||
|
|
||||||
getResourceTitle() {
|
getResourceTitle() {
|
||||||
const name = this.getPluralName();
|
const name = this.getPluralName();
|
||||||
|
|
||||||
return name[0].toUpperCase() + name.substr(1);
|
if (name) {
|
||||||
|
return name[0].toUpperCase() + name.substr(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getGroup() {
|
getGroup() {
|
||||||
return this.spec.group;
|
return this.spec?.group;
|
||||||
}
|
}
|
||||||
|
|
||||||
getScope() {
|
getScope() {
|
||||||
return this.spec.scope;
|
return this.spec?.scope;
|
||||||
}
|
}
|
||||||
|
|
||||||
getVersion() {
|
getVersion() {
|
||||||
// v1 has removed the spec.version property, if it is present it must match the first version
|
// 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() {
|
isNamespaced() {
|
||||||
@ -111,20 +114,20 @@ export class CustomResourceDefinition extends KubeObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getStoredVersions() {
|
getStoredVersions() {
|
||||||
return this.status.storedVersions.join(", ");
|
return this.status?.storedVersions.join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
getNames() {
|
getNames() {
|
||||||
return this.spec.names;
|
return this.spec?.names;
|
||||||
}
|
}
|
||||||
|
|
||||||
getConversion() {
|
getConversion() {
|
||||||
return JSON.stringify(this.spec.conversion);
|
return JSON.stringify(this.spec?.conversion);
|
||||||
}
|
}
|
||||||
|
|
||||||
getPrinterColumns(ignorePriority = true): AdditionalPrinterColumnsV1[] {
|
getPrinterColumns(ignorePriority = true): AdditionalPrinterColumnsV1[] {
|
||||||
const columns = this.spec.versions.find(a => this.getVersion() == a.name)?.additionalPrinterColumns
|
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
|
?? this.spec?.additionalPrinterColumns?.map(({ JSONPath, ...rest }) => ({ ...rest, jsonPath: JSONPath })) // map to V1 shape
|
||||||
?? [];
|
?? [];
|
||||||
|
|
||||||
return columns
|
return columns
|
||||||
@ -133,13 +136,13 @@ export class CustomResourceDefinition extends KubeObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getValidation() {
|
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() {
|
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;
|
const { message, reason, lastTransitionTime, status } = condition;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -151,7 +154,7 @@ export class CustomResourceDefinition extends KubeObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const crdApi = new KubeApi<CustomResourceDefinition>({
|
export const crdApi = new KubeApi({
|
||||||
objectConstructor: CustomResourceDefinition,
|
objectConstructor: CustomResourceDefinition,
|
||||||
checkPreferredVersion: true,
|
checkPreferredVersion: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { formatDuration } from "../../utils/formatDuration";
|
|||||||
import { autobind } from "../../utils";
|
import { autobind } from "../../utils";
|
||||||
import { KubeApi } from "../kube-api";
|
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 }) {
|
suspend(params: { namespace: string; name: string }) {
|
||||||
return this.request.patch(this.getUrl(params), {
|
return this.request.patch(this.getUrl(params), {
|
||||||
data: {
|
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()
|
@autobind()
|
||||||
export class CronJob extends KubeObject {
|
export class CronJob extends KubeObject<CronJobSpec, CronJobStatus> {
|
||||||
static kind = "CronJob";
|
static kind = "CronJob";
|
||||||
static namespaced = true;
|
static namespaced = true;
|
||||||
static apiBase = "/apis/batch/v1beta1/cronjobs";
|
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() {
|
getSuspendFlag() {
|
||||||
return this.spec.suspend.toString();
|
return this.spec?.suspend.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
getLastScheduleTime() {
|
getLastScheduleTime() {
|
||||||
if (!this.status.lastScheduleTime) return "-";
|
if (!this.status?.lastScheduleTime) return "-";
|
||||||
const diff = moment().diff(this.status.lastScheduleTime);
|
const diff = moment().diff(this.status?.lastScheduleTime);
|
||||||
|
|
||||||
return formatDuration(diff, true);
|
return formatDuration(diff, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSchedule() {
|
getSchedule() {
|
||||||
return this.spec.schedule;
|
return this.spec?.schedule;
|
||||||
}
|
}
|
||||||
|
|
||||||
isNeverRun() {
|
isNeverRun() {
|
||||||
const schedule = this.getSchedule();
|
const schedule = this.getSchedule();
|
||||||
|
|
||||||
|
if (!schedule) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const daysInMonth = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
const daysInMonth = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||||||
const stamps = schedule.split(" ");
|
const stamps = schedule.split(" ");
|
||||||
const day = Number(stamps[stamps.length - 3]); // 1-31
|
const day = Number(stamps[stamps.length - 3]); // 1-31
|
||||||
@ -124,7 +114,7 @@ export class CronJob extends KubeObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isSuspend() {
|
isSuspend() {
|
||||||
return this.spec.suspend;
|
return this.spec?.suspend;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,68 +1,64 @@
|
|||||||
import get from "lodash/get";
|
import get from "lodash/get";
|
||||||
import { IPodContainer } from "./pods.api";
|
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 { autobind } from "../../utils";
|
||||||
import { KubeApi } from "../kube-api";
|
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()
|
@autobind()
|
||||||
export class DaemonSet extends WorkloadKubeObject {
|
export class DaemonSet extends WorkloadKubeObject<DaemonSetSpec, DaemonSetStatus> {
|
||||||
static kind = "DaemonSet";
|
static kind = "DaemonSet";
|
||||||
static namespaced = true;
|
static namespaced = true;
|
||||||
static apiBase = "/apis/apps/v1/daemonsets";
|
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() {
|
getImages() {
|
||||||
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []);
|
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []);
|
||||||
const initContainers: IPodContainer[] = get(this, "spec.template.spec.initContainers", []);
|
const initContainers: IPodContainer[] = get(this, "spec.template.spec.initContainers", []);
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
|
||||||
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
|
import { IAffinity, WorkloadKubeObject, WorkloadSpec } from "../workload-kube-object";
|
||||||
import { autobind } from "../../utils";
|
import { autobind } from "../../utils";
|
||||||
import { KubeApi } from "../kube-api";
|
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 }) {
|
protected getScaleApiUrl(params: { namespace: string; name: string }) {
|
||||||
return `${this.getUrl(params)}/scale`;
|
return `${this.getUrl(params)}/scale`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getReplicas(params: { namespace: string; name: string }): Promise<number> {
|
async getReplicas(params: { namespace: string; name: string }): Promise<number> {
|
||||||
return this.request
|
const { status } = await this.request.get(this.getScaleApiUrl(params));
|
||||||
.get(this.getScaleApiUrl(params))
|
|
||||||
.then(({ status }: any) => status?.replicas);
|
return status?.replicas ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
scale(params: { namespace: string; name: string }, replicas: number) {
|
scale(params: { namespace: string; name: string }, replicas: number) {
|
||||||
@ -66,110 +66,114 @@ interface IContainerProbe {
|
|||||||
failureThreshold?: number;
|
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()
|
@autobind()
|
||||||
export class Deployment extends WorkloadKubeObject {
|
export class Deployment extends WorkloadKubeObject<DeploymentSpec, DeploymentStatus> {
|
||||||
static kind = "Deployment";
|
static kind = "Deployment";
|
||||||
static namespaced = true;
|
static namespaced = true;
|
||||||
static apiBase = "/apis/apps/v1/deployments";
|
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) {
|
getConditions(activeOnly = false) {
|
||||||
const { conditions } = this.status;
|
const { conditions } = this.status ?? {};
|
||||||
|
|
||||||
if (!conditions) return [];
|
if (!conditions) return [];
|
||||||
|
|
||||||
@ -185,7 +189,7 @@ export class Deployment extends WorkloadKubeObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getReplicas() {
|
getReplicas() {
|
||||||
return this.spec.replicas || 0;
|
return this.spec?.replicas ?? 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -36,13 +36,16 @@ export class EndpointAddress implements IEndpointAddress {
|
|||||||
targetRef?: {
|
targetRef?: {
|
||||||
kind: string;
|
kind: string;
|
||||||
namespace: string;
|
namespace: string;
|
||||||
|
apiVersion: string;
|
||||||
name: string;
|
name: string;
|
||||||
uid: string;
|
uid: string;
|
||||||
resourceVersion: string;
|
resourceVersion: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(data: IEndpointAddress) {
|
constructor(data: IEndpointAddress) {
|
||||||
Object.assign(this, data);
|
this.hostname = data.hostname;
|
||||||
|
this.ip = data.ip;
|
||||||
|
this.nodeName = data.nodeName;
|
||||||
}
|
}
|
||||||
|
|
||||||
getId() {
|
getId() {
|
||||||
@ -53,12 +56,14 @@ export class EndpointAddress implements IEndpointAddress {
|
|||||||
return this.hostname;
|
return this.hostname;
|
||||||
}
|
}
|
||||||
|
|
||||||
getTargetRef(): ITargetRef {
|
getTargetRef(): ITargetRef | null {
|
||||||
if (this.targetRef) {
|
if (this.targetRef) {
|
||||||
return Object.assign(this.targetRef, {apiVersion: "v1"});
|
this.targetRef.apiVersion = "v1";
|
||||||
} else {
|
|
||||||
return null;
|
return this.targetRef;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,7 +73,9 @@ export class EndpointSubset implements IEndpointSubset {
|
|||||||
ports: IEndpointPort[];
|
ports: IEndpointPort[];
|
||||||
|
|
||||||
constructor(data: IEndpointSubset) {
|
constructor(data: IEndpointSubset) {
|
||||||
Object.assign(this, data);
|
this.addresses = data.addresses;
|
||||||
|
this.notReadyAddresses = data.notReadyAddresses;
|
||||||
|
this.ports = data.ports;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAddresses(): EndpointAddress[] {
|
getAddresses(): EndpointAddress[] {
|
||||||
@ -101,27 +108,24 @@ export class EndpointSubset implements IEndpointSubset {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class Endpoint extends KubeObject {
|
export class Endpoint extends KubeObject<void, void> {
|
||||||
static kind = "Endpoints";
|
static kind = "Endpoints";
|
||||||
static namespaced = true;
|
static namespaced = true;
|
||||||
static apiBase = "/api/v1/endpoints";
|
static apiBase = "/api/v1/endpoints";
|
||||||
|
|
||||||
subsets: IEndpointSubset[];
|
subsets?: IEndpointSubset[];
|
||||||
|
|
||||||
getEndpointSubsets(): EndpointSubset[] {
|
getEndpointSubsets(): EndpointSubset[] {
|
||||||
const subsets = this.subsets || [];
|
return (this.subsets ?? []).map(s => new EndpointSubset(s));
|
||||||
|
|
||||||
return subsets.map(s => new EndpointSubset(s));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toString(): string {
|
toString(): string {
|
||||||
if(this.subsets) {
|
if (this.subsets) {
|
||||||
return this.getEndpointSubsets().map(es => es.toString()).join(", ");
|
return this.getEndpointSubsets().map(String).join(", ");
|
||||||
} else {
|
|
||||||
return "<none>";
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
return "<none>";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const endpointApi = new KubeApi({
|
export const endpointApi = new KubeApi({
|
||||||
|
|||||||
@ -5,12 +5,12 @@ import { autobind } from "../../utils";
|
|||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class KubeEvent extends KubeObject {
|
export class KubeEvent extends KubeObject<void, void> {
|
||||||
static kind = "Event";
|
static kind = "Event";
|
||||||
static namespaced = true;
|
static namespaced = true;
|
||||||
static apiBase = "/api/v1/events";
|
static apiBase = "/api/v1/events";
|
||||||
|
|
||||||
involvedObject: {
|
involvedObject?: {
|
||||||
kind: string;
|
kind: string;
|
||||||
namespace: string;
|
namespace: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -19,26 +19,26 @@ export class KubeEvent extends KubeObject {
|
|||||||
resourceVersion: string;
|
resourceVersion: string;
|
||||||
fieldPath: string;
|
fieldPath: string;
|
||||||
};
|
};
|
||||||
reason: string;
|
reason?: string;
|
||||||
message: string;
|
message?: string;
|
||||||
source: {
|
source?: {
|
||||||
component: string;
|
component: string;
|
||||||
host: string;
|
host: string;
|
||||||
};
|
};
|
||||||
firstTimestamp: string;
|
firstTimestamp?: string;
|
||||||
lastTimestamp: string;
|
lastTimestamp?: string;
|
||||||
count: number;
|
count?: number;
|
||||||
type: "Normal" | "Warning" | string;
|
type?: "Normal" | "Warning" | string;
|
||||||
eventTime: null;
|
eventTime?: null;
|
||||||
reportingComponent: string;
|
reportingComponent?: string;
|
||||||
reportingInstance: string;
|
reportingInstance?: string;
|
||||||
|
|
||||||
isWarning() {
|
isWarning() {
|
||||||
return this.type === "Warning";
|
return this.type === "Warning";
|
||||||
}
|
}
|
||||||
|
|
||||||
getSource() {
|
getSource() {
|
||||||
const { component, host } = this.source;
|
const { component, host } = this.source ?? {};
|
||||||
|
|
||||||
return `${component} ${host || ""}`;
|
return `${component} ${host || ""}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,33 @@
|
|||||||
import { compile } from "path-to-regexp";
|
import { compile } from "path-to-regexp";
|
||||||
import { apiBase } from "../index";
|
import { apiBase } from "../index";
|
||||||
import { stringify } from "querystring";
|
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 type HelmChartList = Record<string, RepoHelmChartList>;
|
||||||
|
|
||||||
export interface IHelmChartDetails {
|
export interface IHelmChartDetails {
|
||||||
@ -17,31 +41,27 @@ const endpoint = compile(`/v2/charts/:repo?/:name?`) as (params?: {
|
|||||||
}) => string;
|
}) => string;
|
||||||
|
|
||||||
export const helmChartsApi = {
|
export const helmChartsApi = {
|
||||||
list() {
|
async list() {
|
||||||
return apiBase
|
const data = await apiBase.get<HelmChartList>(endpoint());
|
||||||
.get<HelmChartList>(endpoint())
|
|
||||||
.then(data => {
|
return Object.values(data)
|
||||||
return Object
|
.flatMap(chartList => Object.values(chartList)[0])
|
||||||
.values(data)
|
.filter(NotFalsy)
|
||||||
.reduce((allCharts, repoCharts) => allCharts.concat(Object.values(repoCharts)), [])
|
.map(HelmChart.create);
|
||||||
.map(([chart]) => HelmChart.create(chart));
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
get(repo: string, name: string, readmeVersion?: string) {
|
async get(repo: string, name: string, readmeVersion?: string) {
|
||||||
const path = endpoint({ repo, name });
|
const path = endpoint({ repo, name });
|
||||||
|
|
||||||
return apiBase
|
const data = await apiBase
|
||||||
.get<IHelmChartDetails>(`${path}?${stringify({ version: readmeVersion })}`)
|
.get<IHelmChartDetails>(`${path}?${stringify({ version: readmeVersion })}`);
|
||||||
.then(data => {
|
const versions = data.versions.map(HelmChart.create);
|
||||||
const versions = data.versions.map(HelmChart.create);
|
const readme = data.readme;
|
||||||
const readme = data.readme;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
readme,
|
readme,
|
||||||
versions,
|
versions,
|
||||||
};
|
};
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getValues(repo: string, name: string, version: string) {
|
getValues(repo: string, name: string, version: string) {
|
||||||
@ -51,12 +71,28 @@ export const helmChartsApi = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class HelmChart {
|
export class HelmChart implements HelmChartData {
|
||||||
constructor(data: any) {
|
constructor(data: HelmChartData) {
|
||||||
Object.assign(this, data);
|
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);
|
return new HelmChart(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -28,7 +28,7 @@ interface IReleaseRawDetails extends IReleasePayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IReleaseDetails extends IReleasePayload {
|
export interface IReleaseDetails extends IReleasePayload {
|
||||||
resources: KubeObject[];
|
resources: KubeObject<any, any>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IReleaseCreatePayload {
|
export interface IReleaseCreatePayload {
|
||||||
@ -79,7 +79,7 @@ export const helmReleasesApi = {
|
|||||||
const path = endpoint({ name, namespace });
|
const path = endpoint({ name, namespace });
|
||||||
|
|
||||||
return apiBase.get<IReleaseRawDetails>(path).then(details => {
|
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));
|
const resources = items.map(item => KubeObject.create(item));
|
||||||
|
|
||||||
return {
|
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()
|
@autobind()
|
||||||
export class HelmRelease implements ItemObject {
|
export class HelmRelease implements ItemObject {
|
||||||
constructor(data: any) {
|
constructor(data: HelmReleaseData) {
|
||||||
Object.assign(this, data);
|
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);
|
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 kind = "HorizontalPodAutoscaler";
|
||||||
static namespaced = true;
|
static namespaced = true;
|
||||||
static apiBase = "/apis/autoscaling/v2beta1/horizontalpodautoscalers";
|
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() {
|
getMaxPods() {
|
||||||
return this.spec.maxReplicas || 0;
|
return this.spec?.maxReplicas || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
getMinPods() {
|
getMinPods() {
|
||||||
return this.spec.minReplicas || 0;
|
return this.spec?.minReplicas || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
getReplicas() {
|
getReplicas() {
|
||||||
return this.status.currentReplicas;
|
return this.status?.currentReplicas;
|
||||||
}
|
}
|
||||||
|
|
||||||
getConditions() {
|
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;
|
const { message, reason, lastTransitionTime, status } = condition;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -93,23 +94,23 @@ export class HorizontalPodAutoscaler extends KubeObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getMetrics() {
|
getMetrics() {
|
||||||
return this.spec.metrics || [];
|
return this.spec?.metrics || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentMetrics() {
|
getCurrentMetrics() {
|
||||||
return this.status.currentMetrics || [];
|
return this.status?.currentMetrics || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getMetricName(metric: IHpaMetric): string {
|
protected getMetricName(metric: IHpaMetric): string | undefined {
|
||||||
const { type, resource, pods, object, external } = metric;
|
const { type, resource, pods, object, external } = metric;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case HpaMetricType.Resource:
|
case HpaMetricType.Resource:
|
||||||
return resource.name;
|
return resource?.name;
|
||||||
case HpaMetricType.Pods:
|
case HpaMetricType.Pods:
|
||||||
return pods.metricName;
|
return pods.metricName;
|
||||||
case HpaMetricType.Object:
|
case HpaMetricType.Object:
|
||||||
return object.metricName;
|
return object?.metricName;
|
||||||
case HpaMetricType.External:
|
case HpaMetricType.External:
|
||||||
return external.metricName;
|
return external.metricName;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { autobind } from "../../utils";
|
|||||||
import { IMetrics, metricsApi } from "./metrics.api";
|
import { IMetrics, metricsApi } from "./metrics.api";
|
||||||
import { KubeApi } from "../kube-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> {
|
getMetrics(ingress: string, namespace: string): Promise<IIngressMetrics> {
|
||||||
const opts = { category: "ingress", ingress };
|
const opts = { category: "ingress", ingress };
|
||||||
|
|
||||||
@ -61,44 +61,45 @@ export const getBackendServiceNamePort = (backend: IIngressBackend) => {
|
|||||||
return { serviceName, servicePort };
|
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()
|
@autobind()
|
||||||
export class Ingress extends KubeObject {
|
export class Ingress extends KubeObject<IngressSpec, IngressStatus> {
|
||||||
static kind = "Ingress";
|
static kind = "Ingress";
|
||||||
static namespaced = true;
|
static namespaced = true;
|
||||||
static apiBase = "/apis/networking.k8s.io/v1/ingresses";
|
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() {
|
getRoutes() {
|
||||||
const { spec: { tls, rules } } = this;
|
const { spec: { tls, rules } = {} } = this;
|
||||||
|
|
||||||
if (!rules) return [];
|
if (!rules) return [];
|
||||||
|
|
||||||
@ -135,7 +136,7 @@ export class Ingress extends KubeObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getHosts() {
|
getHosts() {
|
||||||
const { spec: { rules } } = this;
|
const { spec: { rules } = {} } = this;
|
||||||
|
|
||||||
if (!rules) return [];
|
if (!rules) return [];
|
||||||
|
|
||||||
@ -144,7 +145,7 @@ export class Ingress extends KubeObject {
|
|||||||
|
|
||||||
getPorts() {
|
getPorts() {
|
||||||
const ports: number[] = [];
|
const ports: number[] = [];
|
||||||
const { spec: { tls, rules, backend, defaultBackend } } = this;
|
const { spec: { tls, rules, backend, defaultBackend } = {} } = this;
|
||||||
const httpPort = 80;
|
const httpPort = 80;
|
||||||
const tlsPort = 443;
|
const tlsPort = 443;
|
||||||
// Note: not using the port name (string)
|
// Note: not using the port name (string)
|
||||||
@ -166,11 +167,9 @@ export class Ingress extends KubeObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getLoadBalancers() {
|
getLoadBalancers() {
|
||||||
const { status: { loadBalancer = { ingress: [] } } } = this;
|
return this.status?.loadBalancer?.ingress?.map(address => (
|
||||||
|
|
||||||
return (loadBalancer.ingress ?? []).map(address => (
|
|
||||||
address.hostname || address.ip
|
address.hostname || address.ip
|
||||||
));
|
)) ?? [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,5 +178,4 @@ export const ingressApi = new IngressApi({
|
|||||||
// Add fallback for Kubernetes <1.19
|
// Add fallback for Kubernetes <1.19
|
||||||
checkPreferredVersion: true,
|
checkPreferredVersion: true,
|
||||||
fallbackApiBases: ["/apis/extensions/v1beta1/ingresses"],
|
fallbackApiBases: ["/apis/extensions/v1beta1/ingresses"],
|
||||||
logStuff: true
|
});
|
||||||
} as any);
|
|
||||||
|
|||||||
@ -1,95 +1,87 @@
|
|||||||
import get from "lodash/get";
|
import get from "lodash/get";
|
||||||
import { autobind } from "../../utils";
|
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 { IPodContainer } from "./pods.api";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
import { JsonApiParams } from "../json-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()
|
@autobind()
|
||||||
export class Job extends WorkloadKubeObject {
|
export class Job extends WorkloadKubeObject<JobSpec, JobStatus> {
|
||||||
static kind = "Job";
|
static kind = "Job";
|
||||||
static namespaced = true;
|
static namespaced = true;
|
||||||
static apiBase = "/apis/batch/v1/jobs";
|
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() {
|
getDesiredCompletions() {
|
||||||
return this.spec.completions || 0;
|
return this.spec?.completions || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCompletions() {
|
getCompletions() {
|
||||||
return this.status.succeeded || 0;
|
return this.status?.succeeded || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
getParallelism() {
|
getParallelism() {
|
||||||
return this.spec.parallelism;
|
return this.spec?.parallelism;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCondition() {
|
getCondition() {
|
||||||
// Type of Job condition could be only Complete or Failed
|
// Type of Job condition could be only Complete or Failed
|
||||||
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.14/#jobcondition-v1-batch
|
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.14/#jobcondition-v1-batch
|
||||||
const { conditions } = this.status;
|
return this.status?.conditions.find(({ status }) => status === "True");
|
||||||
|
|
||||||
if (!conditions) return;
|
|
||||||
|
|
||||||
return conditions.find(({ status }) => status === "True");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getImages() {
|
getImages() {
|
||||||
|
|||||||
@ -29,26 +29,26 @@ export interface LimitRangeItem extends LimitRangeParts {
|
|||||||
type: string
|
type: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LimitRangeSpec {
|
||||||
|
limits: LimitRangeItem[];
|
||||||
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class LimitRange extends KubeObject {
|
export class LimitRange extends KubeObject<LimitRangeSpec> {
|
||||||
static kind = "LimitRange";
|
static kind = "LimitRange";
|
||||||
static namespaced = true;
|
static namespaced = true;
|
||||||
static apiBase = "/api/v1/limitranges";
|
static apiBase = "/api/v1/limitranges";
|
||||||
|
|
||||||
spec: {
|
|
||||||
limits: LimitRangeItem[];
|
|
||||||
};
|
|
||||||
|
|
||||||
getContainerLimits() {
|
getContainerLimits() {
|
||||||
return this.spec.limits.filter(limit => limit.type === LimitType.CONTAINER);
|
return this.spec?.limits.filter(limit => limit.type === LimitType.CONTAINER);
|
||||||
}
|
}
|
||||||
|
|
||||||
getPodLimits() {
|
getPodLimits() {
|
||||||
return this.spec.limits.filter(limit => limit.type === LimitType.POD);
|
return this.spec?.limits.filter(limit => limit.type === LimitType.POD);
|
||||||
}
|
}
|
||||||
|
|
||||||
getPVCLimits() {
|
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 {
|
export interface IMetricsResult {
|
||||||
metric: {
|
metric: {
|
||||||
[name: string]: string;
|
[name: string]: string | undefined;
|
||||||
instance: string;
|
instance?: string;
|
||||||
node?: string;
|
node?: string;
|
||||||
pod?: string;
|
pod?: string;
|
||||||
kubernetes?: string;
|
kubernetes?: string;
|
||||||
@ -111,12 +111,11 @@ export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics {
|
|||||||
return metrics;
|
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);
|
return Object.values(metrics).every(metric => !metric?.data?.result?.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getItemMetrics(metrics: { [key: string]: IMetrics }, itemName: string): { [key: string]: IMetrics } {
|
export function getItemMetrics(metrics: Record<string, IMetrics> = {}, itemName: string): Record<string, IMetrics> {
|
||||||
if (!metrics) return;
|
|
||||||
const itemMetrics = { ...metrics };
|
const itemMetrics = { ...metrics };
|
||||||
|
|
||||||
for (const metric in metrics) {
|
for (const metric in metrics) {
|
||||||
@ -132,7 +131,7 @@ export function getItemMetrics(metrics: { [key: string]: IMetrics }, itemName: s
|
|||||||
return itemMetrics;
|
return itemMetrics;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMetricLastPoints(metrics: { [key: string]: IMetrics }) {
|
export function getMetricLastPoints(metrics: Record<string, IMetrics>) {
|
||||||
const result: Partial<{ [metric: string]: number }> = {};
|
const result: Partial<{ [metric: string]: number }> = {};
|
||||||
|
|
||||||
Object.keys(metrics).forEach(metricName => {
|
Object.keys(metrics).forEach(metricName => {
|
||||||
|
|||||||
@ -7,18 +7,18 @@ export enum NamespaceStatus {
|
|||||||
TERMINATING = "Terminating",
|
TERMINATING = "Terminating",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NamespaceKubeStatus {
|
||||||
|
phase: string;
|
||||||
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class Namespace extends KubeObject {
|
export class Namespace extends KubeObject<void, NamespaceKubeStatus> {
|
||||||
static kind = "Namespace";
|
static kind = "Namespace";
|
||||||
static namespaced = false;
|
static namespaced = false;
|
||||||
static apiBase = "/api/v1/namespaces";
|
static apiBase = "/api/v1/namespaces";
|
||||||
|
|
||||||
status?: {
|
|
||||||
phase: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
getStatus() {
|
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()
|
@autobind()
|
||||||
export class NetworkPolicy extends KubeObject {
|
export class NetworkPolicy extends KubeObject<NetworkPolicySpec> {
|
||||||
static kind = "NetworkPolicy";
|
static kind = "NetworkPolicy";
|
||||||
static namespaced = true;
|
static namespaced = true;
|
||||||
static apiBase = "/apis/networking.k8s.io/v1/networkpolicies";
|
static apiBase = "/apis/networking.k8s.io/v1/networkpolicies";
|
||||||
|
|
||||||
spec: {
|
|
||||||
podSelector: {
|
|
||||||
matchLabels: {
|
|
||||||
[label: string]: string;
|
|
||||||
role: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
policyTypes: string[];
|
|
||||||
ingress: IPolicyIngress[];
|
|
||||||
egress: IPolicyEgress[];
|
|
||||||
};
|
|
||||||
|
|
||||||
getMatchLabels(): string[] {
|
getMatchLabels(): string[] {
|
||||||
if (!this.spec.podSelector || !this.spec.podSelector.matchLabels) return [];
|
|
||||||
|
|
||||||
return Object
|
return Object
|
||||||
.entries(this.spec.podSelector.matchLabels)
|
.entries(this.spec?.podSelector?.matchLabels ?? {})
|
||||||
.map(data => data.join(":"));
|
.map(data => data.join(":"));
|
||||||
}
|
}
|
||||||
|
|
||||||
getTypes(): string[] {
|
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 { KubeObject } from "../kube-object";
|
||||||
import { autobind, cpuUnitsToNumber, unitsToBytes } from "../../utils";
|
import { autobind, cpuUnitsToNumber, NotFalsy, unitsToBytes } from "../../utils";
|
||||||
import { IMetrics, metricsApi } from "./metrics.api";
|
import { IMetrics, metricsApi } from "./metrics.api";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
|
|
||||||
export class NodesApi extends KubeApi<Node> {
|
export class NodesApi extends KubeApi<NodeSpec, NodeStatus, Node> {
|
||||||
getMetrics(): Promise<INodeMetrics> {
|
getMetrics(): Promise<INodeMetrics> {
|
||||||
const opts = { category: "nodes"};
|
const opts = { category: "nodes"};
|
||||||
|
|
||||||
@ -28,85 +28,86 @@ export interface INodeMetrics<T = IMetrics> {
|
|||||||
fsSize: T;
|
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()
|
@autobind()
|
||||||
export class Node extends KubeObject {
|
export class Node extends KubeObject<NodeSpec, NodeStatus> {
|
||||||
static kind = "Node";
|
static kind = "Node";
|
||||||
static namespaced = false;
|
static namespaced = false;
|
||||||
static apiBase = "/api/v1/nodes";
|
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() {
|
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 "";
|
||||||
|
|
||||||
return conditions.reduce((types, current) => {
|
|
||||||
if (current.status !== "True") return "";
|
|
||||||
|
|
||||||
return types += ` ${current.type}`;
|
|
||||||
}, "");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getTaints() {
|
getTaints() {
|
||||||
return this.spec.taints || [];
|
return this.spec?.taints ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getRoleLabels() {
|
getRoleLabels() {
|
||||||
const roleLabels = Object.keys(this.metadata.labels).filter(key =>
|
const roleLabels = Object.keys(this.metadata.labels ?? {})
|
||||||
key.includes("node-role.kubernetes.io")
|
.filter(key => key.includes("node-role.kubernetes.io"))
|
||||||
).map(key => key.match(/([^/]+$)/)[0]); // all after last slash
|
.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"]);
|
roleLabels.push(this.metadata.labels["kubernetes.io/role"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,19 +115,19 @@ export class Node extends KubeObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getCpuCapacity() {
|
getCpuCapacity() {
|
||||||
if (!this.status.capacity || !this.status.capacity.cpu) return 0;
|
if (!this.status?.capacity.cpu) return 0;
|
||||||
|
|
||||||
return cpuUnitsToNumber(this.status.capacity.cpu);
|
return cpuUnitsToNumber(this.status.capacity.cpu);
|
||||||
}
|
}
|
||||||
|
|
||||||
getMemoryCapacity() {
|
getMemoryCapacity() {
|
||||||
if (!this.status.capacity || !this.status.capacity.memory) return 0;
|
if (!this.status?.capacity.memory) return 0;
|
||||||
|
|
||||||
return unitsToBytes(this.status.capacity.memory);
|
return unitsToBytes(this.status.capacity.memory);
|
||||||
}
|
}
|
||||||
|
|
||||||
getConditions() {
|
getConditions() {
|
||||||
const conditions = this.status.conditions || [];
|
const conditions = this.status?.conditions ?? [];
|
||||||
|
|
||||||
if (this.isUnschedulable()) {
|
if (this.isUnschedulable()) {
|
||||||
return [{ type: "SchedulingDisabled", status: "True" }, ...conditions];
|
return [{ type: "SchedulingDisabled", status: "True" }, ...conditions];
|
||||||
@ -148,21 +149,17 @@ export class Node extends KubeObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getKubeletVersion() {
|
getKubeletVersion() {
|
||||||
return this.status.nodeInfo.kubeletVersion;
|
return this.status?.nodeInfo.kubeletVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
getOperatingSystem(): string {
|
getOperatingSystem(): string {
|
||||||
const label = this.getLabels().find(label => label.startsWith("kubernetes.io/os="));
|
const label = this.getLabels().find(label => label.startsWith("kubernetes.io/os="));
|
||||||
|
|
||||||
if (label) {
|
return label?.split("=", 2)[1] ?? "linux";
|
||||||
return label.split("=", 2)[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
return "linux";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isUnschedulable() {
|
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 { Pod } from "./pods.api";
|
||||||
import { KubeApi } from "../kube-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> {
|
getMetrics(pvcName: string, namespace: string): Promise<IPvcMetrics> {
|
||||||
return metricsApi.getMetrics({
|
return metricsApi.getMetrics({
|
||||||
diskUsage: { category: "pvc", pvc: pvcName },
|
diskUsage: { category: "pvc", pvc: pvcName },
|
||||||
@ -21,35 +21,36 @@ export interface IPvcMetrics<T = IMetrics> {
|
|||||||
diskCapacity: T;
|
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()
|
@autobind()
|
||||||
export class PersistentVolumeClaim extends KubeObject {
|
export class PersistentVolumeClaim extends KubeObject<PersistentVolumeClaimSpec, PersistentVolumeClaimStatus> {
|
||||||
static kind = "PersistentVolumeClaim";
|
static kind = "PersistentVolumeClaim";
|
||||||
static namespaced = true;
|
static namespaced = true;
|
||||||
static apiBase = "/api/v1/persistentvolumeclaims";
|
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[] {
|
getPods(allPods: Pod[]): Pod[] {
|
||||||
const pods = allPods.filter(pod => pod.getNs() === this.getNs());
|
const pods = allPods.filter(pod => pod.getNs() === this.getNs());
|
||||||
|
|
||||||
@ -62,28 +63,20 @@ export class PersistentVolumeClaim extends KubeObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getStorage(): string {
|
getStorage(): string {
|
||||||
if (!this.spec.resources || !this.spec.resources.requests) return "-";
|
return this.spec?.resources.requests.storage ?? "-";
|
||||||
|
|
||||||
return this.spec.resources.requests.storage;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getMatchLabels(): string[] {
|
getMatchLabels(): string[] {
|
||||||
if (!this.spec.selector || !this.spec.selector.matchLabels) return [];
|
return Object.entries(this.spec?.selector.matchLabels ?? {})
|
||||||
|
|
||||||
return Object.entries(this.spec.selector.matchLabels)
|
|
||||||
.map(([name, val]) => `${name}:${val}`);
|
.map(([name, val]) => `${name}:${val}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
getMatchExpressions() {
|
getMatchExpressions() {
|
||||||
if (!this.spec.selector || !this.spec.selector.matchExpressions) return [];
|
return this.spec?.selector.matchExpressions ?? [];
|
||||||
|
|
||||||
return this.spec.selector.matchExpressions;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatus(): string {
|
getStatus(): string {
|
||||||
if (this.status) return this.status.phase;
|
return this.status?.phase ?? "-";
|
||||||
|
|
||||||
return "-";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,76 +3,72 @@ import { unitsToBytes } from "../../utils/convertMemory";
|
|||||||
import { autobind } from "../../utils";
|
import { autobind } from "../../utils";
|
||||||
import { KubeApi } from "../kube-api";
|
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()
|
@autobind()
|
||||||
export class PersistentVolume extends KubeObject {
|
export class PersistentVolume extends KubeObject<PersistentVolumeSpec, PersistentVolumeStatus> {
|
||||||
static kind = "PersistentVolume";
|
static kind = "PersistentVolume";
|
||||||
static namespaced = false;
|
static namespaced = false;
|
||||||
static apiBase = "/api/v1/persistentvolumes";
|
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) {
|
getCapacity(inBytes = false) {
|
||||||
const capacity = this.spec.capacity;
|
const storage = this.spec?.capacity?.storage ?? "0";
|
||||||
|
|
||||||
if (capacity) {
|
if (inBytes) {
|
||||||
if (inBytes) return unitsToBytes(capacity.storage);
|
return unitsToBytes(storage);
|
||||||
|
|
||||||
return capacity.storage;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return storage;
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatus() {
|
getStatus() {
|
||||||
if (!this.status) return;
|
return this.status?.phase ?? "-";
|
||||||
|
|
||||||
return this.status.phase || "-";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getStorageClass(): string {
|
getStorageClass(): string {
|
||||||
return this.spec.storageClassName;
|
return this.spec?.storageClassName ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
getClaimRefName(): string {
|
getClaimRefName(): string {
|
||||||
return this.spec.claimRef?.name ?? "";
|
return this.spec?.claimRef?.name ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
getStorageClassName() {
|
getStorageClassName() {
|
||||||
return this.spec.storageClassName || "";
|
return this.spec?.storageClassName ?? "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import { KubeObject } from "../kube-object";
|
import { KubeObject } from "../kube-object";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
|
|
||||||
export class PodMetrics extends KubeObject {
|
export class PodMetrics extends KubeObject<void, void> {
|
||||||
static kind = "Pod";
|
static kind = "Pod";
|
||||||
static namespaced = true;
|
static namespaced = true;
|
||||||
static apiBase = "/apis/metrics.k8s.io/v1beta1/pods";
|
static apiBase = "/apis/metrics.k8s.io/v1beta1/pods";
|
||||||
|
|
||||||
timestamp: string;
|
timestamp?: string;
|
||||||
window: string;
|
window?: string;
|
||||||
containers: {
|
containers?: {
|
||||||
name: string;
|
name: string;
|
||||||
usage: {
|
usage: {
|
||||||
cpu: string;
|
cpu: string;
|
||||||
|
|||||||
@ -1,45 +1,44 @@
|
|||||||
import { autobind } from "../../utils";
|
import { autobind } from "../../utils";
|
||||||
import { KubeObject } from "../kube-object";
|
import { KubeObject } from "../kube-object";
|
||||||
import { KubeApi } from "../kube-api";
|
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()
|
@autobind()
|
||||||
export class PodDisruptionBudget extends KubeObject {
|
export class PodDisruptionBudget extends WorkloadKubeObject<PodDisruptionBudgetSpec, PodDisruptionBudgetStatus> {
|
||||||
static kind = "PodDisruptionBudget";
|
static kind = "PodDisruptionBudget";
|
||||||
static namespaced = true;
|
static namespaced = true;
|
||||||
static apiBase = "/apis/policy/v1beta1/poddisruptionbudgets";
|
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() {
|
getSelectors() {
|
||||||
const selector = this.spec.selector;
|
return KubeObject.stringifyLabels(this.spec?.selector?.matchLabels);
|
||||||
|
|
||||||
return KubeObject.stringifyLabels(selector ? selector.matchLabels : null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getMinAvailable() {
|
getMinAvailable() {
|
||||||
return this.spec.minAvailable || "N/A";
|
return this.spec?.minAvailable || "N/A";
|
||||||
}
|
}
|
||||||
|
|
||||||
getMaxUnavailable() {
|
getMaxUnavailable() {
|
||||||
return this.spec.maxUnavailable || "N/A";
|
return this.spec?.maxUnavailable || "N/A";
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentHealthy() {
|
getCurrentHealthy() {
|
||||||
return this.status.currentHealthy;
|
return this.status?.currentHealthy ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
getDesiredHealthy() {
|
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 { autobind } from "../../utils";
|
||||||
import { IMetrics, metricsApi } from "./metrics.api";
|
import { IMetrics, metricsApi } from "./metrics.api";
|
||||||
import { KubeApi } from "../kube-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> {
|
async getLogs(params: { namespace: string; name: string }, query?: IPodLogsQuery): Promise<string> {
|
||||||
const path = `${this.getUrl(params)}/log`;
|
const path = `${this.getUrl(params)}/log`;
|
||||||
|
|
||||||
@ -142,7 +143,7 @@ interface IContainerProbe {
|
|||||||
export interface IPodContainerStatus {
|
export interface IPodContainerStatus {
|
||||||
name: string;
|
name: string;
|
||||||
state?: {
|
state?: {
|
||||||
[index: string]: object;
|
[index: string]: Record<string, Primitive> | undefined;
|
||||||
running?: {
|
running?: {
|
||||||
startedAt: string;
|
startedAt: string;
|
||||||
};
|
};
|
||||||
@ -158,7 +159,7 @@ export interface IPodContainerStatus {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
lastState?: {
|
lastState?: {
|
||||||
[index: string]: object;
|
[index: string]: Record<string, Primitive> | undefined;
|
||||||
running?: {
|
running?: {
|
||||||
startedAt: string;
|
startedAt: string;
|
||||||
};
|
};
|
||||||
@ -181,92 +182,93 @@ export interface IPodContainerStatus {
|
|||||||
started?: boolean;
|
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()
|
@autobind()
|
||||||
export class Pod extends WorkloadKubeObject {
|
export class Pod extends WorkloadKubeObject<PodSpec, PodKubeStatus> {
|
||||||
static kind = "Pod";
|
static kind = "Pod";
|
||||||
static namespaced = true;
|
static namespaced = true;
|
||||||
static apiBase = "/api/v1/pods";
|
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() {
|
getInitContainers() {
|
||||||
return this.spec.initContainers || [];
|
return this.spec?.initContainers || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getContainers() {
|
getContainers() {
|
||||||
return this.spec.containers || [];
|
return this.spec?.containers || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllContainers() {
|
getAllContainers() {
|
||||||
@ -276,7 +278,7 @@ export class Pod extends WorkloadKubeObject {
|
|||||||
getRunningContainers() {
|
getRunningContainers() {
|
||||||
const runningContainerNames = new Set(
|
const runningContainerNames = new Set(
|
||||||
this.getContainerStatuses()
|
this.getContainerStatuses()
|
||||||
.filter(({ state }) => state.running)
|
.filter(({ state }) => state?.running)
|
||||||
.map(({ name }) => name)
|
.map(({ name }) => name)
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -309,7 +311,7 @@ export class Pod extends WorkloadKubeObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getPriorityClassName() {
|
getPriorityClassName() {
|
||||||
return this.spec.priorityClassName || "";
|
return this.spec?.priorityClassName || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatus(): PodStatus {
|
getStatus(): PodStatus {
|
||||||
@ -347,12 +349,12 @@ export class Pod extends WorkloadKubeObject {
|
|||||||
const statuses = this.getContainerStatuses(false); // not including initContainers
|
const statuses = this.getContainerStatuses(false); // not including initContainers
|
||||||
|
|
||||||
for (const { state } of statuses.reverse()) {
|
for (const { state } of statuses.reverse()) {
|
||||||
if (state.waiting) {
|
if (state?.waiting) {
|
||||||
return state.waiting.reason || "Waiting";
|
return state?.waiting.reason || "Waiting";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.terminated) {
|
if (state?.terminated) {
|
||||||
return state.terminated.reason || "Terminated";
|
return state?.terminated.reason || "Terminated";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -368,7 +370,7 @@ export class Pod extends WorkloadKubeObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getVolumes() {
|
getVolumes() {
|
||||||
return this.spec.volumes || [];
|
return this.spec?.volumes || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getSecrets(): string[] {
|
getSecrets(): string[] {
|
||||||
@ -378,17 +380,16 @@ export class Pod extends WorkloadKubeObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getNodeSelectors(): string[] {
|
getNodeSelectors(): string[] {
|
||||||
const { nodeSelector = {} } = this.spec;
|
return Object.entries(this.spec?.nodeSelector ?? {})
|
||||||
|
.map(values => values.join(": "));
|
||||||
return Object.entries(nodeSelector).map(values => values.join(": "));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getTolerations() {
|
getTolerations() {
|
||||||
return this.spec.tolerations || [];
|
return this.spec?.tolerations || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getAffinity(): IAffinity {
|
getAffinity(): IAffinity {
|
||||||
return this.spec.affinity;
|
return this.spec?.affinity ?? {};
|
||||||
}
|
}
|
||||||
|
|
||||||
hasIssues() {
|
hasIssues() {
|
||||||
@ -419,8 +420,11 @@ export class Pod extends WorkloadKubeObject {
|
|||||||
return this.getProbe(container.startupProbe);
|
return this.getProbe(container.startupProbe);
|
||||||
}
|
}
|
||||||
|
|
||||||
getProbe(probeData: IContainerProbe) {
|
getProbe(probeData?: IContainerProbe) {
|
||||||
if (!probeData) return [];
|
if (!probeData) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
httpGet, exec, tcpSocket, initialDelaySeconds, timeoutSeconds,
|
httpGet, exec, tcpSocket, initialDelaySeconds, timeoutSeconds,
|
||||||
periodSeconds, successThreshold, failureThreshold
|
periodSeconds, successThreshold, failureThreshold
|
||||||
@ -458,11 +462,11 @@ export class Pod extends WorkloadKubeObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getNodeName() {
|
getNodeName() {
|
||||||
return this.spec.nodeName;
|
return this.spec?.nodeName;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSelectedNodeOs(): string | undefined {
|
getSelectedNodeOs(): string | undefined {
|
||||||
return this.spec.nodeSelector?.["kubernetes.io/os"] || this.spec.nodeSelector?.["beta.kubernetes.io/os"];
|
return this.spec?.nodeSelector?.["kubernetes.io/os"] || this.spec?.nodeSelector?.["beta.kubernetes.io/os"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,89 +2,101 @@ import { autobind } from "../../utils";
|
|||||||
import { KubeObject } from "../kube-object";
|
import { KubeObject } from "../kube-object";
|
||||||
import { KubeApi } from "../kube-api";
|
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()
|
@autobind()
|
||||||
export class PodSecurityPolicy extends KubeObject {
|
export class PodSecurityPolicy extends KubeObject<PodSecurityPolicySpec> {
|
||||||
static kind = "PodSecurityPolicy";
|
static kind = "PodSecurityPolicy";
|
||||||
static namespaced = false;
|
static namespaced = false;
|
||||||
static apiBase = "/apis/policy/v1beta1/podsecuritypolicies";
|
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() {
|
isPrivileged() {
|
||||||
return !!this.spec.privileged;
|
return this.spec?.privileged ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
getVolumes() {
|
getVolumes() {
|
||||||
return this.spec.volumes || [];
|
return this.spec?.volumes ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getRules() {
|
getRules() {
|
||||||
const { fsGroup, runAsGroup, runAsUser, supplementalGroups, seLinux } = this.spec;
|
const { fsGroup, runAsGroup, runAsUser, supplementalGroups, seLinux } = this.spec ?? {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fsGroup: fsGroup ? fsGroup.rule : "",
|
fsGroup: fsGroup?.rule ?? "",
|
||||||
runAsGroup: runAsGroup ? runAsGroup.rule : "",
|
runAsGroup: runAsGroup?.rule ?? "",
|
||||||
runAsUser: runAsUser ? runAsUser.rule : "",
|
runAsUser: runAsUser?.rule ?? "",
|
||||||
supplementalGroups: supplementalGroups ? supplementalGroups.rule : "",
|
supplementalGroups: supplementalGroups?.rule ?? "",
|
||||||
seLinux: seLinux ? seLinux.rule : "",
|
seLinux: seLinux?.rule ?? "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import get from "lodash/get";
|
|
||||||
import { autobind } from "../../utils";
|
import { autobind } from "../../utils";
|
||||||
import { WorkloadKubeObject } from "../workload-kube-object";
|
import { WorkloadKubeObject, WorkloadSpec } from "../workload-kube-object";
|
||||||
import { IPodContainer, Pod } from "./pods.api";
|
import { Pod } from "./pods.api";
|
||||||
import { KubeApi } from "../kube-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 }) {
|
protected getScaleApiUrl(params: { namespace: string; name: string }) {
|
||||||
return `${this.getUrl(params)}/scale`;
|
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()
|
@autobind()
|
||||||
export class ReplicaSet extends WorkloadKubeObject {
|
export class ReplicaSet extends WorkloadKubeObject<ReplicaSetSpec, ReplicaSetStatus> {
|
||||||
static kind = "ReplicaSet";
|
static kind = "ReplicaSet";
|
||||||
static namespaced = true;
|
static namespaced = true;
|
||||||
static apiBase = "/apis/apps/v1/replicasets";
|
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() {
|
getDesired() {
|
||||||
return this.spec.replicas || 0;
|
return this.spec?.replicas ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrent() {
|
getCurrent() {
|
||||||
return this.status.availableReplicas || 0;
|
return this.status?.availableReplicas ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
getReady() {
|
getReady() {
|
||||||
return this.status.readyReplicas || 0;
|
return this.status?.readyReplicas ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
getImages() {
|
getImages() {
|
||||||
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []);
|
return this.spec?.template?.spec?.containers?.map(container => container.image) ?? [];
|
||||||
|
|
||||||
return [...containers].map(container => container.image);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import jsYaml from "js-yaml";
|
import jsYaml from "js-yaml";
|
||||||
import { KubeObject } from "../kube-object";
|
import { KubeObject } from "../kube-object";
|
||||||
import { KubeJsonApiData } from "../kube-json-api";
|
|
||||||
import { apiBase } from "../index";
|
import { apiBase } from "../index";
|
||||||
import { apiManager } from "../api-manager";
|
import { apiManager } from "../api-manager";
|
||||||
|
|
||||||
@ -9,25 +8,22 @@ export const resourceApplierApi = {
|
|||||||
"kubectl.kubernetes.io/last-applied-configuration"
|
"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") {
|
if (typeof resource === "string") {
|
||||||
resource = jsYaml.safeLoad(resource);
|
resource = jsYaml.safeLoad(resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
return apiBase
|
const data = await apiBase.post<D[]>("/stack", { data: resource });
|
||||||
.post<KubeJsonApiData[]>("/stack", { data: resource })
|
const items = data.map(obj => {
|
||||||
.then(data => {
|
const api = apiManager.getApiByKind(obj.kind, obj.apiVersion);
|
||||||
const items = data.map(obj => {
|
|
||||||
const api = apiManager.getApiByKind(obj.kind, obj.apiVersion);
|
|
||||||
|
|
||||||
if (api) {
|
if (api) {
|
||||||
return new api.objectConstructor(obj);
|
return new api.objectConstructor(obj) as D;
|
||||||
} else {
|
}
|
||||||
return new KubeObject(obj);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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 { KubeObject } from "../kube-object";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
import { KubeJsonApiData } from "../kube-json-api";
|
|
||||||
|
|
||||||
export interface IResourceQuotaValues {
|
export interface IResourceQuotaValues {
|
||||||
[quota: string]: string;
|
[quota: string]: string | undefined;
|
||||||
|
|
||||||
// Compute Resource Quota
|
// Compute Resource Quota
|
||||||
"limits.cpu"?: string;
|
"limits.cpu"?: string;
|
||||||
@ -30,36 +29,29 @@ export interface IResourceQuotaValues {
|
|||||||
"count/deployments.extensions"?: string;
|
"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 kind = "ResourceQuota";
|
||||||
static namespaced = true;
|
static namespaced = true;
|
||||||
static apiBase = "/api/v1/resourcequotas";
|
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() {
|
getScopeSelector() {
|
||||||
const { matchExpressions = [] } = this.spec.scopeSelector || {};
|
return this.spec?.scopeSelector?.matchExpressions ?? [];
|
||||||
|
|
||||||
return matchExpressions;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,13 +10,13 @@ export interface IRoleBindingSubject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class RoleBinding extends KubeObject {
|
export class RoleBinding extends KubeObject<void, void> {
|
||||||
static kind = "RoleBinding";
|
static kind = "RoleBinding";
|
||||||
static namespaced = true;
|
static namespaced = true;
|
||||||
static apiBase = "/apis/rbac.authorization.k8s.io/v1/rolebindings";
|
static apiBase = "/apis/rbac.authorization.k8s.io/v1/rolebindings";
|
||||||
|
|
||||||
subjects?: IRoleBindingSubject[];
|
subjects?: IRoleBindingSubject[];
|
||||||
roleRef: {
|
roleRef?: {
|
||||||
kind: string;
|
kind: string;
|
||||||
name: string;
|
name: string;
|
||||||
apiGroup?: string;
|
apiGroup?: string;
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import { KubeObject } from "../kube-object";
|
import { KubeObject } from "../kube-object";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
|
|
||||||
export class Role extends KubeObject {
|
export class Role extends KubeObject<void, void> {
|
||||||
static kind = "Role";
|
static kind = "Role";
|
||||||
static namespaced = true;
|
static namespaced = true;
|
||||||
static apiBase = "/apis/rbac.authorization.k8s.io/v1/roles";
|
static apiBase = "/apis/rbac.authorization.k8s.io/v1/roles";
|
||||||
|
|
||||||
rules: {
|
rules?: {
|
||||||
verbs: string[];
|
verbs: string[];
|
||||||
apiGroups: string[];
|
apiGroups: string[];
|
||||||
resources: string[];
|
resources: string[];
|
||||||
@ -14,7 +14,7 @@ export class Role extends KubeObject {
|
|||||||
}[];
|
}[];
|
||||||
|
|
||||||
getRules() {
|
getRules() {
|
||||||
return this.rules || [];
|
return this.rules ?? [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { KubeObject } from "../kube-object";
|
import { KubeObject } from "../kube-object";
|
||||||
import { KubeJsonApiData } from "../kube-json-api";
|
|
||||||
import { autobind } from "../../utils";
|
import { autobind } from "../../utils";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
|
|
||||||
@ -20,21 +19,16 @@ export interface ISecretRef {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class Secret extends KubeObject {
|
export class Secret extends KubeObject<void, void> {
|
||||||
static kind = "Secret";
|
static kind = "Secret";
|
||||||
static namespaced = true;
|
static namespaced = true;
|
||||||
static apiBase = "/api/v1/secrets";
|
static apiBase = "/api/v1/secrets";
|
||||||
|
|
||||||
type: SecretType;
|
type?: SecretType;
|
||||||
data: {
|
data: {
|
||||||
[prop: string]: string;
|
[prop: string]: string | undefined;
|
||||||
token?: string;
|
token?: string;
|
||||||
};
|
} = {};
|
||||||
|
|
||||||
constructor(data: KubeJsonApiData) {
|
|
||||||
super(data);
|
|
||||||
this.data = this.data || {};
|
|
||||||
}
|
|
||||||
|
|
||||||
getKeys(): string[] {
|
getKeys(): string[] {
|
||||||
return Object.keys(this.data);
|
return Object.keys(this.data);
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
import { KubeObject } from "../kube-object";
|
import { KubeObject } from "../kube-object";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
|
|
||||||
export class SelfSubjectRulesReviewApi extends KubeApi<SelfSubjectRulesReview> {
|
export class SelfSubjectRulesReviewApi extends KubeApi<SelfSubjectRulesReviewSpec, SelfSubjectRulesReviewStatus, SelfSubjectRulesReview> {
|
||||||
create({ namespace = "default" }): Promise<SelfSubjectRulesReview> {
|
create({ namespace = "default" }): Promise<SelfSubjectRulesReview> {
|
||||||
return super.create({}, {
|
return super.create({}, {
|
||||||
spec: {
|
spec: {
|
||||||
namespace
|
namespace
|
||||||
},
|
},
|
||||||
}
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,32 +19,28 @@ export interface ISelfSubjectReviewRule {
|
|||||||
nonResourceURLs?: string[];
|
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 kind = "SelfSubjectRulesReview";
|
||||||
static namespaced = false;
|
static namespaced = false;
|
||||||
static apiBase = "/apis/authorization.k8s.io/v1/selfsubjectrulesreviews";
|
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() {
|
getResourceRules() {
|
||||||
const rules = this.status && this.status.resourceRules || [];
|
return this.status?.resourceRules.map(rule => this.normalize(rule)) ?? [];
|
||||||
|
|
||||||
return rules.map(rule => this.normalize(rule));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getNonResourceRules() {
|
getNonResourceRules() {
|
||||||
const rules = this.status && this.status.nonResourceRules || [];
|
return this.status?.nonResourceRules.map(rule => this.normalize(rule)) ?? [];
|
||||||
|
|
||||||
return rules.map(rule => this.normalize(rule));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected normalize(rule: ISelfSubjectReviewRule): ISelfSubjectReviewRule {
|
protected normalize(rule: ISelfSubjectReviewRule): ISelfSubjectReviewRule {
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { KubeObject } from "../kube-object";
|
|||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class ServiceAccount extends KubeObject {
|
export class ServiceAccount extends KubeObject<void, void> {
|
||||||
static kind = "ServiceAccount";
|
static kind = "ServiceAccount";
|
||||||
static namespaced = true;
|
static namespaced = true;
|
||||||
static apiBase = "/api/v1/serviceaccounts";
|
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,
|
objectConstructor: ServiceAccount,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -7,6 +7,7 @@ export interface IServicePort {
|
|||||||
protocol: string;
|
protocol: string;
|
||||||
port: number;
|
port: number;
|
||||||
targetPort: number;
|
targetPort: number;
|
||||||
|
nodePort?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ServicePort implements IServicePort {
|
export class ServicePort implements IServicePort {
|
||||||
@ -17,7 +18,11 @@ export class ServicePort implements IServicePort {
|
|||||||
nodePort?: number;
|
nodePort?: number;
|
||||||
|
|
||||||
constructor(data: IServicePort) {
|
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() {
|
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()
|
@autobind()
|
||||||
export class Service extends KubeObject {
|
export class Service extends KubeObject<ServiceSpec, ServiceStatus> {
|
||||||
static kind = "Service";
|
static kind = "Service";
|
||||||
static namespaced = true;
|
static namespaced = true;
|
||||||
static apiBase = "/api/v1/services";
|
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() {
|
getClusterIp() {
|
||||||
return this.spec.clusterIP;
|
return this.spec?.clusterIP;
|
||||||
}
|
}
|
||||||
|
|
||||||
getExternalIps() {
|
getExternalIps() {
|
||||||
const lb = this.getLoadBalancer();
|
return this.getLoadBalancer()
|
||||||
|
?.ingress
|
||||||
if (lb && lb.ingress) {
|
?.map(val => val.ip || val.hostname)
|
||||||
return lb.ingress.map(val => val.ip || val.hostname);
|
?? this.spec?.externalIPs
|
||||||
}
|
?? [];
|
||||||
|
|
||||||
return this.spec.externalIPs || [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getType() {
|
getType() {
|
||||||
return this.spec.type || "-";
|
return this.spec?.type || "-";
|
||||||
}
|
}
|
||||||
|
|
||||||
getSelector(): string[] {
|
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[] {
|
getPorts(): ServicePort[] {
|
||||||
const ports = this.spec.ports || [];
|
return this.spec?.ports.map(p => new ServicePort(p)) ?? [];
|
||||||
|
|
||||||
return ports.map(p => new ServicePort(p));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getLoadBalancer() {
|
getLoadBalancer() {
|
||||||
return this.status.loadBalancer;
|
return this.status?.loadBalancer ?? {};
|
||||||
}
|
}
|
||||||
|
|
||||||
isActive() {
|
isActive() {
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import get from "lodash/get";
|
import { IAffinity, WorkloadKubeObject, WorkloadSpec } from "../workload-kube-object";
|
||||||
import { IPodContainer } from "./pods.api";
|
|
||||||
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
|
|
||||||
import { autobind } from "../../utils";
|
import { autobind } from "../../utils";
|
||||||
import { KubeApi } from "../kube-api";
|
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 }) {
|
protected getScaleApiUrl(params: { namespace: string; name: string }) {
|
||||||
return `${this.getUrl(params)}/scale`;
|
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()
|
@autobind()
|
||||||
export class StatefulSet extends WorkloadKubeObject {
|
export class StatefulSet extends WorkloadKubeObject<StatefulSetSpec, StatefulSetStatus> {
|
||||||
static kind = "StatefulSet";
|
static kind = "StatefulSet";
|
||||||
static namespaced = true;
|
static namespaced = true;
|
||||||
static apiBase = "/apis/apps/v1/statefulsets";
|
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() {
|
getReplicas() {
|
||||||
return this.spec.replicas || 0;
|
return this.spec?.replicas ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
getImages() {
|
getImages() {
|
||||||
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []);
|
return this.spec?.template.spec.containers.map(container => container.image) ?? [];
|
||||||
|
|
||||||
return [...containers].map(container => container.image);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,16 +3,16 @@ import { KubeObject } from "../kube-object";
|
|||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class StorageClass extends KubeObject {
|
export class StorageClass extends KubeObject<void, void> {
|
||||||
static kind = "StorageClass";
|
static kind = "StorageClass";
|
||||||
static namespaced = false;
|
static namespaced = false;
|
||||||
static apiBase = "/apis/storage.k8s.io/v1/storageclasses";
|
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[];
|
mountOptions?: string[];
|
||||||
volumeBindingMode: string;
|
volumeBindingMode?: string;
|
||||||
reclaimPolicy: string;
|
reclaimPolicy?: string;
|
||||||
parameters: {
|
parameters?: {
|
||||||
[param: string]: string; // every provisioner has own set of these parameters
|
[param: string]: string; // every provisioner has own set of these parameters
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export const apiBase = new JsonApi({
|
|||||||
apiBase: apiPrefix,
|
apiBase: apiPrefix,
|
||||||
debug: isDevelopment || isDebugging,
|
debug: isDevelopment || isDebugging,
|
||||||
});
|
});
|
||||||
export const apiKube = new KubeJsonApi({
|
export const apiKube = new KubeJsonApi<any, any>({
|
||||||
apiBase: apiKubePrefix,
|
apiBase: apiKubePrefix,
|
||||||
debug: isDevelopment,
|
debug: isDevelopment,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -114,7 +114,7 @@ export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
|
|||||||
reqUrl += (reqUrl.includes("?") ? "&" : "?") + queryString;
|
reqUrl += (reqUrl.includes("?") ? "&" : "?") + queryString;
|
||||||
}
|
}
|
||||||
const infoLog: JsonApiLog = {
|
const infoLog: JsonApiLog = {
|
||||||
method: reqInit.method.toUpperCase(),
|
method: reqInit.method?.toUpperCase() ?? "<unknown>",
|
||||||
reqUrl,
|
reqUrl,
|
||||||
reqInit,
|
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