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

Add eslint rule padding-line-between-statements (#1593)

Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>
This commit is contained in:
Panu Horsmalahti 2020-12-02 09:55:52 +02:00 committed by GitHub
parent 7b77f18376
commit dcf253e7d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
401 changed files with 2018 additions and 40 deletions

View File

@ -49,6 +49,15 @@ module.exports = {
"object-shorthand": "error", "object-shorthand": "error",
"prefer-template": "error", "prefer-template": "error",
"template-curly-spacing": "error", "template-curly-spacing": "error",
"padding-line-between-statements": [
"error",
{ "blankLine": "always", "prev": "*", "next": "return" },
{ "blankLine": "always", "prev": "*", "next": "block-like" },
{ "blankLine": "always", "prev": "*", "next": "function" },
{ "blankLine": "always", "prev": "*", "next": "class" },
{ "blankLine": "always", "prev": ["const", "let", "var"], "next": "*" },
{ "blankLine": "any", "prev": ["const", "let", "var"], "next": ["const", "let", "var"]},
]
} }
}, },
{ {
@ -94,6 +103,15 @@ module.exports = {
"object-shorthand": "error", "object-shorthand": "error",
"prefer-template": "error", "prefer-template": "error",
"template-curly-spacing": "error", "template-curly-spacing": "error",
"padding-line-between-statements": [
"error",
{ "blankLine": "always", "prev": "*", "next": "return" },
{ "blankLine": "always", "prev": "*", "next": "block-like" },
{ "blankLine": "always", "prev": "*", "next": "function" },
{ "blankLine": "always", "prev": "*", "next": "class" },
{ "blankLine": "always", "prev": ["const", "let", "var"], "next": "*" },
{ "blankLine": "any", "prev": ["const", "let", "var"], "next": ["const", "let", "var"]},
]
}, },
}, },
{ {
@ -146,6 +164,15 @@ module.exports = {
"object-shorthand": "error", "object-shorthand": "error",
"prefer-template": "error", "prefer-template": "error",
"template-curly-spacing": "error", "template-curly-spacing": "error",
"padding-line-between-statements": [
"error",
{ "blankLine": "always", "prev": "*", "next": "return" },
{ "blankLine": "always", "prev": "*", "next": "block-like" },
{ "blankLine": "always", "prev": "*", "next": "function" },
{ "blankLine": "always", "prev": "*", "next": "class" },
{ "blankLine": "always", "prev": ["const", "let", "var"], "next": "*" },
{ "blankLine": "any", "prev": ["const", "let", "var"], "next": ["const", "let", "var"]},
]
}, },
} }
] ]

View File

@ -17,14 +17,15 @@ export async function generateTrayIcon(
outputFilename += shouldUseDarkColors ? "_dark" : ""; outputFilename += shouldUseDarkColors ? "_dark" : "";
dpiSuffix = dpiSuffix !== "1x" ? `@${dpiSuffix}` : ""; dpiSuffix = dpiSuffix !== "1x" ? `@${dpiSuffix}` : "";
const pngIconDestPath = path.resolve(outputFolder, `${outputFilename}${dpiSuffix}.png`); const pngIconDestPath = path.resolve(outputFolder, `${outputFilename}${dpiSuffix}.png`);
try { try {
// Modify .SVG colors // Modify .SVG colors
const trayIconColor = shouldUseDarkColors ? "white" : "black"; const trayIconColor = shouldUseDarkColors ? "white" : "black";
const svgDom = await jsdom.JSDOM.fromFile(svgIconPath); const svgDom = await jsdom.JSDOM.fromFile(svgIconPath);
const svgRoot = svgDom.window.document.body.getElementsByTagName("svg")[0]; const svgRoot = svgDom.window.document.body.getElementsByTagName("svg")[0];
svgRoot.innerHTML += `<style>* {fill: ${trayIconColor} !important;}</style>`; svgRoot.innerHTML += `<style>* {fill: ${trayIconColor} !important;}</style>`;
const svgIconBuffer = Buffer.from(svgRoot.outerHTML); const svgIconBuffer = Buffer.from(svgRoot.outerHTML);
// Resize and convert to .PNG // Resize and convert to .PNG
const pngIconBuffer: Buffer = await sharp(svgIconBuffer) const pngIconBuffer: Buffer = await sharp(svgIconBuffer)
.resize({ width: pixelSize, height: pixelSize }) .resize({ width: pixelSize, height: pixelSize })
@ -45,6 +46,7 @@ const iconSizes: Record<string, number> = {
"2x": 32, "2x": 32,
"3x": 48, "3x": 48,
}; };
Object.entries(iconSizes).forEach(([dpiSuffix, pixelSize]) => { Object.entries(iconSizes).forEach(([dpiSuffix, pixelSize]) => {
generateTrayIcon({ dpiSuffix, pixelSize, shouldUseDarkColors: false }); generateTrayIcon({ dpiSuffix, pixelSize, shouldUseDarkColors: false });
generateTrayIcon({ dpiSuffix, pixelSize, shouldUseDarkColors: true }); generateTrayIcon({ dpiSuffix, pixelSize, shouldUseDarkColors: true });

View File

@ -15,6 +15,7 @@ class KubectlDownloader {
constructor(clusterVersion: string, platform: string, arch: string, target: string) { constructor(clusterVersion: string, platform: string, arch: string, target: string) {
this.kubectlVersion = clusterVersion; this.kubectlVersion = clusterVersion;
const binaryName = platform === "windows" ? "kubectl.exe" : "kubectl"; const binaryName = platform === "windows" ? "kubectl.exe" : "kubectl";
this.url = `https://storage.googleapis.com/kubernetes-release/release/v${this.kubectlVersion}/bin/${platform}/${arch}/${binaryName}`; this.url = `https://storage.googleapis.com/kubernetes-release/release/v${this.kubectlVersion}/bin/${platform}/${arch}/${binaryName}`;
this.dirname = path.dirname(target); this.dirname = path.dirname(target);
this.path = target; this.path = target;
@ -30,16 +31,20 @@ class KubectlDownloader {
if (response.headers["etag"]) { if (response.headers["etag"]) {
return response.headers["etag"].replace(/"/g, ""); return response.headers["etag"].replace(/"/g, "");
} }
return ""; return "";
} }
public async checkBinary() { public async checkBinary() {
const exists = await pathExists(this.path); const exists = await pathExists(this.path);
if (exists) { if (exists) {
const hash = md5File.sync(this.path); const hash = md5File.sync(this.path);
const etag = await this.urlEtag(); const etag = await this.urlEtag();
if(hash == etag) { if(hash == etag) {
console.log("Kubectl md5sum matches the remote etag"); console.log("Kubectl md5sum matches the remote etag");
return true; return true;
} }
@ -52,13 +57,16 @@ class KubectlDownloader {
public async downloadKubectl() { public async downloadKubectl() {
const exists = await this.checkBinary(); const exists = await this.checkBinary();
if(exists) { if(exists) {
console.log("Already exists and is valid"); console.log("Already exists and is valid");
return; return;
} }
await ensureDir(path.dirname(this.path), 0o755); await ensureDir(path.dirname(this.path), 0o755);
const file = fs.createWriteStream(this.path); const file = fs.createWriteStream(this.path);
console.log(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`); console.log(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`);
const requestOpts: request.UriOptions & request.CoreOptions = { const requestOpts: request.UriOptions & request.CoreOptions = {
uri: this.url, uri: this.url,
@ -78,6 +86,7 @@ class KubectlDownloader {
fs.unlink(this.path, () => {}); fs.unlink(this.path, () => {});
throw(error); throw(error);
}); });
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
file.on("close", () => { file.on("close", () => {
console.log("kubectl binary download closed"); console.log("kubectl binary download closed");
@ -103,6 +112,7 @@ const downloads = [
downloads.forEach((dlOpts) => { downloads.forEach((dlOpts) => {
console.log(dlOpts); console.log(dlOpts);
const downloader = new KubectlDownloader(downloadVersion, dlOpts.platform, dlOpts.arch, dlOpts.target); const downloader = new KubectlDownloader(downloadVersion, dlOpts.platform, dlOpts.arch, dlOpts.target);
console.log(`Downloading: ${JSON.stringify(dlOpts)}`); console.log(`Downloading: ${JSON.stringify(dlOpts)}`);
downloader.downloadKubectl().then(() => downloader.checkBinary().then(() => console.log("Download complete"))); downloader.downloadKubectl().then(() => downloader.checkBinary().then(() => console.log("Download complete")));
}); });

View File

@ -2,9 +2,11 @@ const { notarize } = require("electron-notarize");
exports.default = async function notarizing(context) { exports.default = async function notarizing(context) {
const { electronPlatformName, appOutDir } = context; const { electronPlatformName, appOutDir } = context;
if (electronPlatformName !== "darwin") { if (electronPlatformName !== "darwin") {
return; return;
} }
if (!process.env.APPLEID || !process.env.APPLEIDPASS) { if (!process.env.APPLEID || !process.env.APPLEIDPASS) {
return; return;
} }

View File

@ -10,6 +10,7 @@ export function ExampleIcon(props: Component.IconProps) {
export class ExamplePage extends React.Component<{ extension: LensRendererExtension }> { export class ExamplePage extends React.Component<{ extension: LensRendererExtension }> {
deactivate = () => { deactivate = () => {
const { extension } = this.props; const { extension } = this.props;
extension.disable(); extension.disable();
}; };
@ -17,6 +18,7 @@ export class ExamplePage extends React.Component<{ extension: LensRendererExtens
const doodleStyle = { const doodleStyle = {
width: "200px" width: "200px"
}; };
return ( return (
<div className="flex column gaps align-flex-start"> <div className="flex column gaps align-flex-start">
<div style={doodleStyle}><CoffeeDoodle accent="#3d90ce" /></div> <div style={doodleStyle}><CoffeeDoodle accent="#3d90ce" /></div>

View File

@ -4,10 +4,12 @@ export function resolveStatus(object: K8sApi.KubeObject): K8sApi.KubeObjectStatu
const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi); const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi);
const events = (eventStore as K8sApi.EventStore).getEventsByObject(object); const events = (eventStore as K8sApi.EventStore).getEventsByObject(object);
const warnings = events.filter(evt => evt.isWarning()); const warnings = events.filter(evt => evt.isWarning());
if (!events.length || !warnings.length) { if (!events.length || !warnings.length) {
return null; return null;
} }
const event = [...warnings, ...events][0]; // get latest event const event = [...warnings, ...events][0]; // get latest event
return { return {
level: K8sApi.KubeObjectStatusLevel.WARNING, level: K8sApi.KubeObjectStatusLevel.WARNING,
text: `${event.message}`, text: `${event.message}`,
@ -22,10 +24,12 @@ export function resolveStatusForPods(pod: K8sApi.Pod): K8sApi.KubeObjectStatus {
const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi); const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi);
const events = (eventStore as K8sApi.EventStore).getEventsByObject(pod); const events = (eventStore as K8sApi.EventStore).getEventsByObject(pod);
const warnings = events.filter(evt => evt.isWarning()); const warnings = events.filter(evt => evt.isWarning());
if (!events.length || !warnings.length) { if (!events.length || !warnings.length) {
return null; return null;
} }
const event = [...warnings, ...events][0]; // get latest event const event = [...warnings, ...events][0]; // get latest event
return { return {
level: K8sApi.KubeObjectStatusLevel.WARNING, level: K8sApi.KubeObjectStatusLevel.WARNING,
text: `${event.message}`, text: `${event.message}`,
@ -37,13 +41,16 @@ export function resolveStatusForCronJobs(cronJob: K8sApi.CronJob): K8sApi.KubeOb
const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi); const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi);
let events = (eventStore as K8sApi.EventStore).getEventsByObject(cronJob); let events = (eventStore as K8sApi.EventStore).getEventsByObject(cronJob);
const warnings = events.filter(evt => evt.isWarning()); const warnings = events.filter(evt => evt.isWarning());
if (cronJob.isNeverRun()) { if (cronJob.isNeverRun()) {
events = events.filter(event => event.reason != "FailedNeedsStart"); events = events.filter(event => event.reason != "FailedNeedsStart");
} }
if (!events.length || !warnings.length) { if (!events.length || !warnings.length) {
return null; return null;
} }
const event = [...warnings, ...events][0]; // get latest event const event = [...warnings, ...events][0]; // get latest event
return { return {
level: K8sApi.KubeObjectStatusLevel.WARNING, level: K8sApi.KubeObjectStatusLevel.WARNING,
text: `${event.message}`, text: `${event.message}`,

View File

@ -53,6 +53,7 @@ export class MetricsFeature extends ClusterFeature.Feature {
// Check if there are storageclasses // Check if there are storageclasses
const storageClassApi = K8sApi.forCluster(cluster, K8sApi.StorageClass); const storageClassApi = K8sApi.forCluster(cluster, K8sApi.StorageClass);
const scs = await storageClassApi.list(); const scs = await storageClassApi.list();
this.templateContext.persistence.enabled = scs.some(sc => ( this.templateContext.persistence.enabled = scs.some(sc => (
sc.metadata?.annotations?.["storageclass.kubernetes.io/is-default-class"] === "true" || sc.metadata?.annotations?.["storageclass.kubernetes.io/is-default-class"] === "true" ||
sc.metadata?.annotations?.["storageclass.beta.kubernetes.io/is-default-class"] === "true" sc.metadata?.annotations?.["storageclass.beta.kubernetes.io/is-default-class"] === "true"
@ -69,6 +70,7 @@ export class MetricsFeature extends ClusterFeature.Feature {
try { try {
const statefulSet = K8sApi.forCluster(cluster, K8sApi.StatefulSet); const statefulSet = K8sApi.forCluster(cluster, K8sApi.StatefulSet);
const prometheus = await statefulSet.get({name: "prometheus", namespace: "lens-metrics"}); const prometheus = await statefulSet.get({name: "prometheus", namespace: "lens-metrics"});
if (prometheus?.kind) { if (prometheus?.kind) {
this.status.installed = true; this.status.installed = true;
this.status.currentVersion = prometheus.spec.template.spec.containers[0].image.split(":")[1]; this.status.currentVersion = prometheus.spec.template.spec.containers[0].image.split(":")[1];

View File

@ -6,6 +6,7 @@ export interface NodeMenuProps extends Component.KubeObjectMenuProps<K8sApi.Node
export function NodeMenu(props: NodeMenuProps) { export function NodeMenu(props: NodeMenuProps) {
const { object: node, toolbar } = props; const { object: node, toolbar } = props;
if (!node) return null; if (!node) return null;
const nodeName = node.getName(); const nodeName = node.getName();
@ -35,6 +36,7 @@ export function NodeMenu(props: NodeMenuProps) {
const drain = () => { const drain = () => {
const command = `kubectl drain ${nodeName} --delete-local-data --ignore-daemonsets --force`; const command = `kubectl drain ${nodeName} --delete-local-data --ignore-daemonsets --force`;
Component.ConfirmDialog.open({ Component.ConfirmDialog.open({
ok: () => sendToTerminal(command), ok: () => sendToTerminal(command),
labelOk: `Drain Node`, labelOk: `Drain Node`,

View File

@ -8,6 +8,7 @@ export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
showLogs(container: K8sApi.IPodContainer) { showLogs(container: K8sApi.IPodContainer) {
Navigation.hideDetails(); Navigation.hideDetails();
const pod = this.props.object; const pod = this.props.object;
Component.createPodLogsTab({ Component.createPodLogsTab({
pod, pod,
containers: pod.getContainers(), containers: pod.getContainers(),
@ -22,7 +23,9 @@ export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
const { object: pod, toolbar } = this.props; const { object: pod, toolbar } = this.props;
const containers = pod.getAllContainers(); const containers = pod.getAllContainers();
const statuses = pod.getContainerStatuses(); const statuses = pod.getContainerStatuses();
if (!containers.length) return null; if (!containers.length) return null;
return ( return (
<Component.MenuItem onClick={Util.prevDefault(() => this.showLogs(containers[0]))}> <Component.MenuItem onClick={Util.prevDefault(() => this.showLogs(containers[0]))}>
<Component.Icon material="subject" title="Logs" interactive={toolbar}/> <Component.Icon material="subject" title="Logs" interactive={toolbar}/>
@ -40,6 +43,7 @@ export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
className={Util.cssNames(Object.keys(status.state)[0], { ready: status.ready })} className={Util.cssNames(Object.keys(status.state)[0], { ready: status.ready })}
/> />
) : null; ) : null;
return ( return (
<Component.MenuItem key={name} onClick={Util.prevDefault(() => this.showLogs(container))} className="flex align-center"> <Component.MenuItem key={name} onClick={Util.prevDefault(() => this.showLogs(container))} className="flex align-center">
{brick} {brick}

View File

@ -12,9 +12,11 @@ export class PodShellMenu extends React.Component<PodShellMenuProps> {
const { object: pod } = this.props; const { object: pod } = this.props;
const containerParam = container ? `-c ${container}` : ""; const containerParam = container ? `-c ${container}` : "";
let command = `kubectl exec -i -t -n ${pod.getNs()} ${pod.getName()} ${containerParam} "--"`; let command = `kubectl exec -i -t -n ${pod.getNs()} ${pod.getName()} ${containerParam} "--"`;
if (window.navigator.platform !== "Win32") { if (window.navigator.platform !== "Win32") {
command = `exec ${command}`; command = `exec ${command}`;
} }
if (pod.getSelectedNodeOs() === "windows") { if (pod.getSelectedNodeOs() === "windows") {
command = `${command} powershell`; command = `${command} powershell`;
} else { } else {
@ -34,7 +36,9 @@ export class PodShellMenu extends React.Component<PodShellMenuProps> {
render() { render() {
const { object, toolbar } = this.props; const { object, toolbar } = this.props;
const containers = object.getRunningContainers(); const containers = object.getRunningContainers();
if (!containers.length) return null; if (!containers.length) return null;
return ( return (
<Component.MenuItem onClick={Util.prevDefault(() => this.execShell(containers[0].name))}> <Component.MenuItem onClick={Util.prevDefault(() => this.execShell(containers[0].name))}>
<Component.Icon svg="ssh" interactive={toolbar} title="Pod shell"/> <Component.Icon svg="ssh" interactive={toolbar} title="Pod shell"/>
@ -46,6 +50,7 @@ export class PodShellMenu extends React.Component<PodShellMenuProps> {
{ {
containers.map(container => { containers.map(container => {
const { name } = container; const { name } = container;
return ( return (
<Component.MenuItem key={name} onClick={Util.prevDefault(() => this.execShell(name))} className="flex align-center"> <Component.MenuItem key={name} onClick={Util.prevDefault(() => this.execShell(name))} className="flex align-center">
<Component.StatusBrick/> <Component.StatusBrick/>

View File

@ -7,6 +7,7 @@ import { TelemetryPreferencesStore } from "./telemetry-preferences-store";
export class TelemetryPreferenceInput extends React.Component<{telemetry: TelemetryPreferencesStore}, {}> { export class TelemetryPreferenceInput extends React.Component<{telemetry: TelemetryPreferencesStore}, {}> {
render() { render() {
const { telemetry } = this.props; const { telemetry } = this.props;
return ( return (
<Component.Checkbox <Component.Checkbox
label="Allow telemetry & usage tracking" label="Allow telemetry & usage tracking"

View File

@ -29,6 +29,7 @@ export class Tracker extends Util.Singleton {
this.anonymousId = machineIdSync(); this.anonymousId = machineIdSync();
this.os = this.resolveOS(); this.os = this.resolveOS();
this.userAgent = `Lens ${App.version} (${this.os})`; this.userAgent = `Lens ${App.version} (${this.os})`;
try { try {
this.visitor = ua(Tracker.GA_ID, this.anonymousId, { strictCidFormat: false }); this.visitor = ua(Tracker.GA_ID, this.anonymousId, { strictCidFormat: false });
} catch (error) { } catch (error) {
@ -49,18 +50,22 @@ export class Tracker extends Util.Singleton {
const handler = (ev: EventBus.AppEvent) => { const handler = (ev: EventBus.AppEvent) => {
this.event(ev.name, ev.action, ev.params); this.event(ev.name, ev.action, ev.params);
}; };
this.eventHandlers.push(handler); this.eventHandlers.push(handler);
EventBus.appEventBus.addListener(handler); EventBus.appEventBus.addListener(handler);
} }
watchExtensions() { watchExtensions() {
let previousExtensions = App.getEnabledExtensions(); let previousExtensions = App.getEnabledExtensions();
this.disposers.push(reaction(() => App.getEnabledExtensions(), (currentExtensions) => { this.disposers.push(reaction(() => App.getEnabledExtensions(), (currentExtensions) => {
const removedExtensions = previousExtensions.filter(x => !currentExtensions.includes(x)); const removedExtensions = previousExtensions.filter(x => !currentExtensions.includes(x));
removedExtensions.forEach(ext => { removedExtensions.forEach(ext => {
this.event("extension", "disable", { extension: ext }); this.event("extension", "disable", { extension: ext });
}); });
const newExtensions = currentExtensions.filter(x => !previousExtensions.includes(x)); const newExtensions = currentExtensions.filter(x => !previousExtensions.includes(x));
newExtensions.forEach(ext => { newExtensions.forEach(ext => {
this.event("extension", "enable", { extension: ext }); this.event("extension", "enable", { extension: ext });
}); });
@ -82,6 +87,7 @@ export class Tracker extends Util.Singleton {
for (const handler of this.eventHandlers) { for (const handler of this.eventHandlers) {
EventBus.appEventBus.removeListener(handler); EventBus.appEventBus.removeListener(handler);
} }
if (this.reportInterval) { if (this.reportInterval) {
clearInterval(this.reportInterval); clearInterval(this.reportInterval);
} }
@ -125,12 +131,14 @@ export class Tracker extends Util.Singleton {
protected resolveOS() { protected resolveOS() {
let os = ""; let os = "";
if (App.isMac) { if (App.isMac) {
os = "MacOS"; os = "MacOS";
} else if(App.isWindows) { } else if(App.isWindows) {
os = "Windows"; os = "Windows";
} else if (App.isLinux) { } else if (App.isLinux) {
os = "Linux"; os = "Linux";
if (App.isSnap) { if (App.isSnap) {
os += "; Snap"; os += "; Snap";
} else { } else {
@ -139,12 +147,14 @@ export class Tracker extends Util.Singleton {
} else { } else {
os = "Unknown"; os = "Unknown";
} }
return os; return os;
} }
protected async event(eventCategory: string, eventAction: string, otherParams = {}) { protected async event(eventCategory: string, eventAction: string, otherParams = {}) {
try { try {
const allowed = await this.isTelemetryAllowed(); const allowed = await this.isTelemetryAllowed();
if (!allowed) { if (!allowed) {
return; return;
} }

View File

@ -14,9 +14,7 @@ jest.setTimeout(60000);
describe("Lens integration tests", () => { describe("Lens integration tests", () => {
const TEST_NAMESPACE = "integration-tests"; const TEST_NAMESPACE = "integration-tests";
const BACKSPACE = "\uE003"; const BACKSPACE = "\uE003";
let app: Application; let app: Application;
const appStart = async () => { const appStart = async () => {
app = util.setup(); app = util.setup();
await app.start(); await app.start();
@ -25,19 +23,19 @@ describe("Lens integration tests", () => {
await app.client.windowByIndex(0); await app.client.windowByIndex(0);
await app.client.waitUntilWindowLoaded(); await app.client.waitUntilWindowLoaded();
}; };
const clickWhatsNew = async (app: Application) => { const clickWhatsNew = async (app: Application) => {
await app.client.waitUntilTextExists("h1", "What's new?"); await app.client.waitUntilTextExists("h1", "What's new?");
await app.client.click("button.primary"); await app.client.click("button.primary");
await app.client.waitUntilTextExists("h1", "Welcome"); await app.client.waitUntilTextExists("h1", "Welcome");
}; };
const minikubeReady = (): boolean => { const minikubeReady = (): boolean => {
// determine if minikube is running // determine if minikube is running
{ {
const { status } = spawnSync("minikube status", { shell: true }); const { status } = spawnSync("minikube status", { shell: true });
if (status !== 0) { if (status !== 0) {
console.warn("minikube not running"); console.warn("minikube not running");
return false; return false;
} }
} }
@ -45,6 +43,7 @@ describe("Lens integration tests", () => {
// Remove TEST_NAMESPACE if it already exists // Remove TEST_NAMESPACE if it already exists
{ {
const { status } = spawnSync(`minikube kubectl -- get namespace ${TEST_NAMESPACE}`, { shell: true }); const { status } = spawnSync(`minikube kubectl -- get namespace ${TEST_NAMESPACE}`, { shell: true });
if (status === 0) { if (status === 0) {
console.warn(`Removing existing ${TEST_NAMESPACE} namespace`); console.warn(`Removing existing ${TEST_NAMESPACE} namespace`);
@ -52,8 +51,10 @@ describe("Lens integration tests", () => {
`minikube kubectl -- delete namespace ${TEST_NAMESPACE}`, `minikube kubectl -- delete namespace ${TEST_NAMESPACE}`,
{ shell: true }, { shell: true },
); );
if (status !== 0) { if (status !== 0) {
console.warn(`Error removing ${TEST_NAMESPACE} namespace: ${stderr.toString()}`); console.warn(`Error removing ${TEST_NAMESPACE} namespace: ${stderr.toString()}`);
return false; return false;
} }
@ -86,6 +87,7 @@ describe("Lens integration tests", () => {
describe("preferences page", () => { describe("preferences page", () => {
it('shows "preferences"', async () => { it('shows "preferences"', async () => {
const appName: string = process.platform === "darwin" ? "Lens" : "File"; const appName: string = process.platform === "darwin" ? "Lens" : "File";
await app.electron.ipcRenderer.send("test-menu-item-click", appName, "Preferences"); await app.electron.ipcRenderer.send("test-menu-item-click", appName, "Preferences");
await app.client.waitUntilTextExists("h2", "Preferences"); await app.client.waitUntilTextExists("h2", "Preferences");
}); });
@ -153,13 +155,13 @@ describe("Lens integration tests", () => {
await app.client.waitUntilTextExists("div", "Select kubeconfig file"); await app.client.waitUntilTextExists("div", "Select kubeconfig file");
await app.client.click("div.Select__control"); // show the context drop-down list await app.client.click("div.Select__control"); // show the context drop-down list
await app.client.waitUntilTextExists("div", "minikube"); await app.client.waitUntilTextExists("div", "minikube");
if (!await app.client.$("button.primary").isEnabled()) { if (!await app.client.$("button.primary").isEnabled()) {
await app.client.click("div.minikube"); // select minikube context await app.client.click("div.minikube"); // select minikube context
} // else the only context, which must be 'minikube', is automatically selected } // else the only context, which must be 'minikube', is automatically selected
await app.client.click("div.Select__control"); // hide the context drop-down list (it might be obscuring the Add cluster(s) button) await app.client.click("div.Select__control"); // hide the context drop-down list (it might be obscuring the Add cluster(s) button)
await app.client.click("button.primary"); // add minikube cluster await app.client.click("button.primary"); // add minikube cluster
}; };
const waitForMinikubeDashboard = async (app: Application) => { const waitForMinikubeDashboard = async (app: Application) => {
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started"); await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started");
await app.client.waitForExist(`iframe[name="minikube"]`); await app.client.waitForExist(`iframe[name="minikube"]`);
@ -169,7 +171,6 @@ describe("Lens integration tests", () => {
util.describeIf(ready)("cluster tests", () => { util.describeIf(ready)("cluster tests", () => {
let clusterAdded = false; let clusterAdded = false;
const addCluster = async () => { const addCluster = async () => {
await clickWhatsNew(app); await clickWhatsNew(app);
await addMinikubeCluster(app); await addMinikubeCluster(app);
@ -443,6 +444,7 @@ describe("Lens integration tests", () => {
expectedText: "Custom Resources" expectedText: "Custom Resources"
}] }]
}]; }];
tests.forEach(({ drawer = "", drawerId = "", pages }) => { tests.forEach(({ drawer = "", drawerId = "", pages }) => {
if (drawer !== "") { if (drawer !== "") {
it(`shows ${drawer} drawer`, async () => { it(`shows ${drawer} drawer`, async () => {
@ -458,6 +460,7 @@ describe("Lens integration tests", () => {
await app.client.waitUntilTextExists(expectedSelector, expectedText); await app.client.waitUntilTextExists(expectedSelector, expectedText);
}); });
}); });
if (drawer !== "") { if (drawer !== "") {
// hide the drawer // hide the drawer
it(`hides ${drawer} drawer`, async () => { it(`hides ${drawer} drawer`, async () => {

View File

@ -30,7 +30,9 @@ type AsyncPidGetter = () => Promise<number>;
export async function tearDown(app: Application) { export async function tearDown(app: Application) {
const pid = await (app.mainProcess.pid as any as AsyncPidGetter)(); const pid = await (app.mainProcess.pid as any as AsyncPidGetter)();
await app.stop(); await app.stop();
try { try {
process.kill(pid, "SIGKILL"); process.kill(pid, "SIGKILL");
} catch (e) { } catch (e) {

View File

@ -31,8 +31,10 @@ describe("empty config", () => {
"lens-cluster-store.json": JSON.stringify({}) "lens-cluster-store.json": JSON.stringify({})
} }
}; };
mockFs(mockOpts); mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>(); clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load(); return clusterStore.load();
}); });
@ -59,6 +61,7 @@ describe("empty config", () => {
it("adds new cluster to store", async () => { it("adds new cluster to store", async () => {
const storedCluster = clusterStore.getById("foo"); const storedCluster = clusterStore.getById("foo");
expect(storedCluster.id).toBe("foo"); expect(storedCluster.id).toBe("foo");
expect(storedCluster.preferences.terminalCWD).toBe("/tmp"); expect(storedCluster.preferences.terminalCWD).toBe("/tmp");
expect(storedCluster.preferences.icon).toBe("data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5"); expect(storedCluster.preferences.icon).toBe("data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5");
@ -67,6 +70,7 @@ describe("empty config", () => {
it("adds cluster to default workspace", () => { it("adds cluster to default workspace", () => {
const storedCluster = clusterStore.getById("foo"); const storedCluster = clusterStore.getById("foo");
expect(storedCluster.workspace).toBe("default"); expect(storedCluster.workspace).toBe("default");
}); });
@ -114,6 +118,7 @@ describe("empty config", () => {
it("gets clusters by workspaces", () => { it("gets clusters by workspaces", () => {
const wsClusters = clusterStore.getByWorkspaceId("workstation"); const wsClusters = clusterStore.getByWorkspaceId("workstation");
const defaultClusters = clusterStore.getByWorkspaceId("default"); const defaultClusters = clusterStore.getByWorkspaceId("default");
expect(defaultClusters.length).toBe(0); expect(defaultClusters.length).toBe(0);
expect(wsClusters.length).toBe(2); expect(wsClusters.length).toBe(2);
expect(wsClusters[0].id).toBe("prod"); expect(wsClusters[0].id).toBe("prod");
@ -122,6 +127,7 @@ describe("empty config", () => {
it("check if cluster's kubeconfig file saved", () => { it("check if cluster's kubeconfig file saved", () => {
const file = ClusterStore.embedCustomKubeConfig("boo", "kubeconfig"); const file = ClusterStore.embedCustomKubeConfig("boo", "kubeconfig");
expect(fs.readFileSync(file, "utf8")).toBe("kubeconfig"); expect(fs.readFileSync(file, "utf8")).toBe("kubeconfig");
}); });
@ -129,6 +135,7 @@ describe("empty config", () => {
clusterStore.swapIconOrders("workstation", 1, 1); clusterStore.swapIconOrders("workstation", 1, 1);
const clusters = clusterStore.getByWorkspaceId("workstation"); const clusters = clusterStore.getByWorkspaceId("workstation");
expect(clusters[0].id).toBe("prod"); expect(clusters[0].id).toBe("prod");
expect(clusters[0].preferences.iconOrder).toBe(0); expect(clusters[0].preferences.iconOrder).toBe(0);
expect(clusters[1].id).toBe("dev"); expect(clusters[1].id).toBe("dev");
@ -139,6 +146,7 @@ describe("empty config", () => {
clusterStore.swapIconOrders("workstation", 0, 1); clusterStore.swapIconOrders("workstation", 0, 1);
const clusters = clusterStore.getByWorkspaceId("workstation"); const clusters = clusterStore.getByWorkspaceId("workstation");
expect(clusters[0].id).toBe("dev"); expect(clusters[0].id).toBe("dev");
expect(clusters[0].preferences.iconOrder).toBe(0); expect(clusters[0].preferences.iconOrder).toBe(0);
expect(clusters[1].id).toBe("prod"); expect(clusters[1].id).toBe("prod");
@ -192,8 +200,10 @@ describe("config with existing clusters", () => {
}) })
} }
}; };
mockFs(mockOpts); mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>(); clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load(); return clusterStore.load();
}); });
@ -203,6 +213,7 @@ describe("config with existing clusters", () => {
it("allows to retrieve a cluster", () => { it("allows to retrieve a cluster", () => {
const storedCluster = clusterStore.getById("cluster1"); const storedCluster = clusterStore.getById("cluster1");
expect(storedCluster.id).toBe("cluster1"); expect(storedCluster.id).toBe("cluster1");
expect(storedCluster.preferences.terminalCWD).toBe("/foo"); expect(storedCluster.preferences.terminalCWD).toBe("/foo");
}); });
@ -210,13 +221,16 @@ describe("config with existing clusters", () => {
it("allows to delete a cluster", () => { it("allows to delete a cluster", () => {
clusterStore.removeById("cluster2"); clusterStore.removeById("cluster2");
const storedCluster = clusterStore.getById("cluster1"); const storedCluster = clusterStore.getById("cluster1");
expect(storedCluster).toBeTruthy(); expect(storedCluster).toBeTruthy();
const storedCluster2 = clusterStore.getById("cluster2"); const storedCluster2 = clusterStore.getById("cluster2");
expect(storedCluster2).toBeUndefined(); expect(storedCluster2).toBeUndefined();
}); });
it("allows getting all of the clusters", async () => { it("allows getting all of the clusters", async () => {
const storedClusters = clusterStore.clustersList; const storedClusters = clusterStore.clustersList;
expect(storedClusters.length).toBe(3); expect(storedClusters.length).toBe(3);
expect(storedClusters[0].id).toBe("cluster1"); expect(storedClusters[0].id).toBe("cluster1");
expect(storedClusters[0].preferences.terminalCWD).toBe("/foo"); expect(storedClusters[0].preferences.terminalCWD).toBe("/foo");
@ -227,6 +241,7 @@ describe("config with existing clusters", () => {
it("marks owned cluster disabled by default", () => { it("marks owned cluster disabled by default", () => {
const storedClusters = clusterStore.clustersList; const storedClusters = clusterStore.clustersList;
expect(storedClusters[0].enabled).toBe(true); expect(storedClusters[0].enabled).toBe(true);
expect(storedClusters[2].enabled).toBe(false); expect(storedClusters[2].enabled).toBe(false);
}); });
@ -247,8 +262,10 @@ describe("pre 2.0 config with an existing cluster", () => {
}) })
} }
}; };
mockFs(mockOpts); mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>(); clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load(); return clusterStore.load();
}); });
@ -258,6 +275,7 @@ describe("pre 2.0 config with an existing cluster", () => {
it("migrates to modern format with kubeconfig in a file", async () => { it("migrates to modern format with kubeconfig in a file", async () => {
const config = clusterStore.clustersList[0].kubeConfigPath; const config = clusterStore.clustersList[0].kubeConfigPath;
expect(fs.readFileSync(config, "utf8")).toBe("kubeconfig content"); expect(fs.readFileSync(config, "utf8")).toBe("kubeconfig content");
}); });
}); });
@ -279,8 +297,10 @@ describe("pre 2.6.0 config with a cluster that has arrays in auth config", () =>
}) })
} }
}; };
mockFs(mockOpts); mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>(); clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load(); return clusterStore.load();
}); });
@ -292,6 +312,7 @@ describe("pre 2.6.0 config with a cluster that has arrays in auth config", () =>
const file = clusterStore.clustersList[0].kubeConfigPath; const file = clusterStore.clustersList[0].kubeConfigPath;
const config = fs.readFileSync(file, "utf8"); const config = fs.readFileSync(file, "utf8");
const kc = yaml.safeLoad(config); const kc = yaml.safeLoad(config);
expect(kc.users[0].user["auth-provider"].config["access-token"]).toBe("should be string"); expect(kc.users[0].user["auth-provider"].config["access-token"]).toBe("should be string");
expect(kc.users[0].user["auth-provider"].config["expiry"]).toBe("should be string"); expect(kc.users[0].user["auth-provider"].config["expiry"]).toBe("should be string");
}); });
@ -319,8 +340,10 @@ describe("pre 2.6.0 config with a cluster icon", () => {
"icon_path": testDataIcon, "icon_path": testDataIcon,
} }
}; };
mockFs(mockOpts); mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>(); clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load(); return clusterStore.load();
}); });
@ -330,6 +353,7 @@ describe("pre 2.6.0 config with a cluster icon", () => {
it("moves the icon into preferences", async () => { it("moves the icon into preferences", async () => {
const storedClusterData = clusterStore.clustersList[0]; const storedClusterData = clusterStore.clustersList[0];
expect(storedClusterData.hasOwnProperty("icon")).toBe(false); expect(storedClusterData.hasOwnProperty("icon")).toBe(false);
expect(storedClusterData.preferences.hasOwnProperty("icon")).toBe(true); expect(storedClusterData.preferences.hasOwnProperty("icon")).toBe(true);
expect(storedClusterData.preferences.icon.startsWith("data:;base64,")).toBe(true); expect(storedClusterData.preferences.icon.startsWith("data:;base64,")).toBe(true);
@ -356,8 +380,10 @@ describe("for a pre 2.7.0-beta.0 config without a workspace", () => {
}) })
} }
}; };
mockFs(mockOpts); mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>(); clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load(); return clusterStore.load();
}); });
@ -367,6 +393,7 @@ describe("for a pre 2.7.0-beta.0 config without a workspace", () => {
it("adds cluster to default workspace", async () => { it("adds cluster to default workspace", async () => {
const storedClusterData = clusterStore.clustersList[0]; const storedClusterData = clusterStore.clustersList[0];
expect(storedClusterData.workspace).toBe("default"); expect(storedClusterData.workspace).toBe("default");
}); });
}); });
@ -396,8 +423,10 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
"icon_path": testDataIcon, "icon_path": testDataIcon,
} }
}; };
mockFs(mockOpts); mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>(); clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load(); return clusterStore.load();
}); });
@ -407,11 +436,13 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
it("migrates to modern format with kubeconfig in a file", async () => { it("migrates to modern format with kubeconfig in a file", async () => {
const config = clusterStore.clustersList[0].kubeConfigPath; const config = clusterStore.clustersList[0].kubeConfigPath;
expect(fs.readFileSync(config, "utf8")).toBe("kubeconfig content"); expect(fs.readFileSync(config, "utf8")).toBe("kubeconfig content");
}); });
it("migrates to modern format with icon not in file", async () => { it("migrates to modern format with icon not in file", async () => {
const { icon } = clusterStore.clustersList[0].preferences; const { icon } = clusterStore.clustersList[0].preferences;
expect(icon.startsWith("data:;base64,")).toBe(true); expect(icon.startsWith("data:;base64,")).toBe(true);
}); });
}); });

View File

@ -4,6 +4,7 @@ describe("event bus tests", () => {
describe("emit", () => { describe("emit", () => {
it("emits an event", () => { it("emits an event", () => {
let event: AppEvent = null; let event: AppEvent = null;
appEventBus.addListener((data) => { appEventBus.addListener((data) => {
event = data; event = data;
}); });

View File

@ -5,7 +5,6 @@
import { SearchStore } from "../search-store"; import { SearchStore } from "../search-store";
let searchStore: SearchStore = null; let searchStore: SearchStore = null;
const logs = [ const logs = [
"1:M 30 Oct 2020 16:17:41.553 # Connection with replica 172.17.0.12:6379 lost", "1:M 30 Oct 2020 16:17:41.553 # Connection with replica 172.17.0.12:6379 lost",
"1:M 30 Oct 2020 16:17:41.623 * Replica 172.17.0.12:6379 asks for synchronization", "1:M 30 Oct 2020 16:17:41.623 * Replica 172.17.0.12:6379 asks for synchronization",
@ -64,6 +63,7 @@ describe("search store tests", () => {
it("escapes string for using in regex", () => { it("escapes string for using in regex", () => {
const regex = searchStore.escapeRegex("some.interesting-query\\#?()[]"); const regex = searchStore.escapeRegex("some.interesting-query\\#?()[]");
expect(regex).toBe("some\\.interesting\\-query\\\\\\#\\?\\(\\)\\[\\]"); expect(regex).toBe("some\\.interesting\\-query\\\\\\#\\?\\(\\)\\[\\]");
}); });

View File

@ -59,6 +59,7 @@ describe("user store tests", () => {
it("correctly resets theme to default value", async () => { it("correctly resets theme to default value", async () => {
const us = UserStore.getInstance<UserStore>(); const us = UserStore.getInstance<UserStore>();
us.isLoaded = true; us.isLoaded = true;
us.preferences.colorTheme = "some other theme"; us.preferences.colorTheme = "some other theme";

View File

@ -44,7 +44,6 @@ describe("workspace store tests", () => {
it("can update workspace description", () => { it("can update workspace description", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>(); const ws = WorkspaceStore.getInstance<WorkspaceStore>();
const workspace = ws.addWorkspace(new Workspace({ const workspace = ws.addWorkspace(new Workspace({
id: "foobar", id: "foobar",
name: "foobar", name: "foobar",
@ -65,6 +64,7 @@ describe("workspace store tests", () => {
})); }));
const workspace = ws.getById("123"); const workspace = ws.getById("123");
expect(workspace.name).toBe("foobar"); expect(workspace.name).toBe("foobar");
expect(workspace.enabled).toBe(true); expect(workspace.enabled).toBe(true);
}); });

View File

@ -55,6 +55,7 @@ export abstract class BaseStore<T = any> extends Singleton {
if (this.params.autoLoad) { if (this.params.autoLoad) {
await this.load(); await this.load();
} }
if (this.params.syncEnabled) { if (this.params.syncEnabled) {
await this.whenLoaded; await this.whenLoaded;
this.enableSync(); this.enableSync();
@ -63,6 +64,7 @@ export abstract class BaseStore<T = any> extends Singleton {
async load() { async load() {
const { autoLoad, syncEnabled, ...confOptions } = this.params; const { autoLoad, syncEnabled, ...confOptions } = this.params;
this.storeConfig = new Config({ this.storeConfig = new Config({
...confOptions, ...confOptions,
projectName: "lens", projectName: "lens",
@ -90,19 +92,23 @@ export abstract class BaseStore<T = any> extends Singleton {
this.syncDisposers.push( this.syncDisposers.push(
reaction(() => this.toJSON(), model => this.onModelChange(model), this.params.syncOptions), reaction(() => this.toJSON(), model => this.onModelChange(model), this.params.syncOptions),
); );
if (ipcMain) { if (ipcMain) {
const callback = (event: IpcMainEvent, model: T) => { const callback = (event: IpcMainEvent, model: T) => {
logger.silly(`[STORE]: SYNC ${this.name} from renderer`, { model }); logger.silly(`[STORE]: SYNC ${this.name} from renderer`, { model });
this.onSync(model); this.onSync(model);
}; };
subscribeToBroadcast(this.syncMainChannel, callback); subscribeToBroadcast(this.syncMainChannel, callback);
this.syncDisposers.push(() => unsubscribeFromBroadcast(this.syncMainChannel, callback)); this.syncDisposers.push(() => unsubscribeFromBroadcast(this.syncMainChannel, callback));
} }
if (ipcRenderer) { if (ipcRenderer) {
const callback = (event: IpcRendererEvent, model: T) => { const callback = (event: IpcRendererEvent, model: T) => {
logger.silly(`[STORE]: SYNC ${this.name} from main`, { model }); logger.silly(`[STORE]: SYNC ${this.name} from main`, { model });
this.onSyncFromMain(model); this.onSyncFromMain(model);
}; };
subscribeToBroadcast(this.syncRendererChannel, callback); subscribeToBroadcast(this.syncRendererChannel, callback);
this.syncDisposers.push(() => unsubscribeFromBroadcast(this.syncRendererChannel, callback)); this.syncDisposers.push(() => unsubscribeFromBroadcast(this.syncRendererChannel, callback));
} }
@ -127,6 +133,7 @@ export abstract class BaseStore<T = any> extends Singleton {
protected applyWithoutSync(callback: () => void) { protected applyWithoutSync(callback: () => void) {
this.disableSync(); this.disableSync();
runInAction(callback); runInAction(callback);
if (this.params.syncEnabled) { if (this.params.syncEnabled) {
this.enableSync(); this.enableSync();
} }

View File

@ -15,6 +15,7 @@ export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all";
if (ipcMain) { if (ipcMain) {
handleRequest(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => { handleRequest(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
const cluster = clusterStore.getById(clusterId); const cluster = clusterStore.getById(clusterId);
if (cluster) { if (cluster) {
return cluster.activate(force); return cluster.activate(force);
} }
@ -22,20 +23,24 @@ if (ipcMain) {
handleRequest(clusterSetFrameIdHandler, (event, clusterId: ClusterId, frameId: number) => { handleRequest(clusterSetFrameIdHandler, (event, clusterId: ClusterId, frameId: number) => {
const cluster = clusterStore.getById(clusterId); const cluster = clusterStore.getById(clusterId);
if (cluster) { if (cluster) {
clusterFrameMap.set(cluster.id, frameId); clusterFrameMap.set(cluster.id, frameId);
return cluster.pushState(); return cluster.pushState();
} }
}); });
handleRequest(clusterRefreshHandler, (event, clusterId: ClusterId) => { handleRequest(clusterRefreshHandler, (event, clusterId: ClusterId) => {
const cluster = clusterStore.getById(clusterId); const cluster = clusterStore.getById(clusterId);
if (cluster) return cluster.refresh({ refreshMetadata: true }); if (cluster) return cluster.refresh({ refreshMetadata: true });
}); });
handleRequest(clusterDisconnectHandler, (event, clusterId: ClusterId) => { handleRequest(clusterDisconnectHandler, (event, clusterId: ClusterId) => {
appEventBus.emit({name: "cluster", action: "stop"}); appEventBus.emit({name: "cluster", action: "stop"});
const cluster = clusterStore.getById(clusterId); const cluster = clusterStore.getById(clusterId);
if (cluster) { if (cluster) {
cluster.disconnect(); cluster.disconnect();
clusterFrameMap.delete(cluster.id); clusterFrameMap.delete(cluster.id);
@ -45,8 +50,10 @@ if (ipcMain) {
handleRequest(clusterKubectlApplyAllHandler, (event, clusterId: ClusterId, resources: string[]) => { handleRequest(clusterKubectlApplyAllHandler, (event, clusterId: ClusterId, resources: string[]) => {
appEventBus.emit({name: "cluster", action: "kubectl-apply-all"}); appEventBus.emit({name: "cluster", action: "kubectl-apply-all"});
const cluster = clusterStore.getById(clusterId); const cluster = clusterStore.getById(clusterId);
if (cluster) { if (cluster) {
const applier = new ResourceApplier(cluster); const applier = new ResourceApplier(cluster);
applier.kubectlApplyAll(resources); applier.kubectlApplyAll(resources);
} else { } else {
throw `${clusterId} is not a valid cluster id`; throw `${clusterId} is not a valid cluster id`;

View File

@ -98,7 +98,9 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
static embedCustomKubeConfig(clusterId: ClusterId, kubeConfig: KubeConfig | string): string { static embedCustomKubeConfig(clusterId: ClusterId, kubeConfig: KubeConfig | string): string {
const filePath = ClusterStore.getCustomKubeConfigPath(clusterId); const filePath = ClusterStore.getCustomKubeConfigPath(clusterId);
const fileContents = typeof kubeConfig == "string" ? kubeConfig : dumpConfigYaml(kubeConfig); const fileContents = typeof kubeConfig == "string" ? kubeConfig : dumpConfigYaml(kubeConfig);
saveToAppFiles(filePath, fileContents, { mode: 0o600 }); saveToAppFiles(filePath, fileContents, { mode: 0o600 });
return filePath; return filePath;
} }
@ -127,11 +129,14 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
id: string; id: string;
state: ClusterState; state: ClusterState;
}; };
if (ipcRenderer) { if (ipcRenderer) {
logger.info("[CLUSTER-STORE] requesting initial state sync"); logger.info("[CLUSTER-STORE] requesting initial state sync");
const clusterStates: clusterStateSync[] = await requestMain(ClusterStore.stateRequestChannel); const clusterStates: clusterStateSync[] = await requestMain(ClusterStore.stateRequestChannel);
clusterStates.forEach((clusterState) => { clusterStates.forEach((clusterState) => {
const cluster = this.getById(clusterState.id); const cluster = this.getById(clusterState.id);
if (cluster) { if (cluster) {
cluster.setState(clusterState.state); cluster.setState(clusterState.state);
} }
@ -139,12 +144,14 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
} else { } else {
handleRequest(ClusterStore.stateRequestChannel, (): clusterStateSync[] => { handleRequest(ClusterStore.stateRequestChannel, (): clusterStateSync[] => {
const states: clusterStateSync[] = []; const states: clusterStateSync[] = [];
this.clustersList.forEach((cluster) => { this.clustersList.forEach((cluster) => {
states.push({ states.push({
state: cluster.getState(), state: cluster.getState(),
id: cluster.id id: cluster.id
}); });
}); });
return states; return states;
}); });
} }
@ -207,6 +214,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
@action @action
setActive(id: ClusterId) { setActive(id: ClusterId) {
const clusterId = this.clusters.has(id) ? id : null; const clusterId = this.clusters.has(id) ? id : null;
this.activeCluster = clusterId; this.activeCluster = clusterId;
workspaceStore.setLastActiveClusterId(clusterId); workspaceStore.setLastActiveClusterId(clusterId);
} }
@ -214,11 +222,13 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
@action @action
swapIconOrders(workspace: WorkspaceId, from: number, to: number) { swapIconOrders(workspace: WorkspaceId, from: number, to: number) {
const clusters = this.getByWorkspaceId(workspace); const clusters = this.getByWorkspaceId(workspace);
if (from < 0 || to < 0 || from >= clusters.length || to >= clusters.length || isNaN(from) || isNaN(to)) { if (from < 0 || to < 0 || from >= clusters.length || to >= clusters.length || isNaN(from) || isNaN(to)) {
throw new Error(`invalid from<->to arguments`); throw new Error(`invalid from<->to arguments`);
} }
move.mutate(clusters, from, to); move.mutate(clusters, from, to);
for (const i in clusters) { for (const i in clusters) {
// This resets the iconOrder to the current display order // This resets the iconOrder to the current display order
clusters[i].preferences.iconOrder = +i; clusters[i].preferences.iconOrder = +i;
@ -236,12 +246,14 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
getByWorkspaceId(workspaceId: string): Cluster[] { getByWorkspaceId(workspaceId: string): Cluster[] {
const clusters = Array.from(this.clusters.values()) const clusters = Array.from(this.clusters.values())
.filter(cluster => cluster.workspace === workspaceId); .filter(cluster => cluster.workspace === workspaceId);
return _.sortBy(clusters, cluster => cluster.preferences.iconOrder); return _.sortBy(clusters, cluster => cluster.preferences.iconOrder);
} }
@action @action
addClusters(...models: ClusterModel[]): Cluster[] { addClusters(...models: ClusterModel[]): Cluster[] {
const clusters: Cluster[] = []; const clusters: Cluster[] = [];
models.forEach(model => { models.forEach(model => {
clusters.push(this.addCluster(model)); clusters.push(this.addCluster(model));
}); });
@ -253,13 +265,16 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
addCluster(model: ClusterModel | Cluster): Cluster { addCluster(model: ClusterModel | Cluster): Cluster {
appEventBus.emit({ name: "cluster", action: "add" }); appEventBus.emit({ name: "cluster", action: "add" });
let cluster = model as Cluster; let cluster = model as Cluster;
if (!(model instanceof Cluster)) { if (!(model instanceof Cluster)) {
cluster = new Cluster(model); cluster = new Cluster(model);
} }
if (!cluster.isManaged) { if (!cluster.isManaged) {
cluster.enabled = true; cluster.enabled = true;
} }
this.clusters.set(model.id, cluster); this.clusters.set(model.id, cluster);
return cluster; return cluster;
} }
@ -271,11 +286,14 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
async removeById(clusterId: ClusterId) { async removeById(clusterId: ClusterId) {
appEventBus.emit({ name: "cluster", action: "remove" }); appEventBus.emit({ name: "cluster", action: "remove" });
const cluster = this.getById(clusterId); const cluster = this.getById(clusterId);
if (cluster) { if (cluster) {
this.clusters.delete(clusterId); this.clusters.delete(clusterId);
if (this.activeCluster === clusterId) { if (this.activeCluster === clusterId) {
this.setActive(null); this.setActive(null);
} }
// remove only custom kubeconfigs (pasted as text) // remove only custom kubeconfigs (pasted as text)
if (cluster.kubeConfigPath == ClusterStore.getCustomKubeConfigPath(clusterId)) { if (cluster.kubeConfigPath == ClusterStore.getCustomKubeConfigPath(clusterId)) {
unlink(cluster.kubeConfigPath).catch(() => null); unlink(cluster.kubeConfigPath).catch(() => null);
@ -299,10 +317,12 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
// update new clusters // update new clusters
for (const clusterModel of clusters) { for (const clusterModel of clusters) {
let cluster = currentClusters.get(clusterModel.id); let cluster = currentClusters.get(clusterModel.id);
if (cluster) { if (cluster) {
cluster.updateModel(clusterModel); cluster.updateModel(clusterModel);
} else { } else {
cluster = new Cluster(clusterModel); cluster = new Cluster(clusterModel);
if (!cluster.isManaged) { if (!cluster.isManaged) {
cluster.enabled = true; cluster.enabled = true;
} }
@ -336,6 +356,7 @@ export const clusterStore = ClusterStore.getInstance<ClusterStore>();
export function getClusterIdFromHost(hostname: string): ClusterId { export function getClusterIdFromHost(hostname: string): ClusterId {
const subDomains = hostname.split(":")[0].split("."); const subDomains = hostname.split(":")[0].split(".");
return subDomains.slice(-2)[0]; // e.g host == "%clusterId.localhost:45345" return subDomains.slice(-2)[0]; // e.g host == "%clusterId.localhost:45345"
} }

View File

@ -2,6 +2,7 @@ export class ExecValidationNotFoundError extends Error {
constructor(execPath: string, isAbsolute: boolean) { constructor(execPath: string, isAbsolute: boolean) {
super(`User Exec command "${execPath}" not found on host.`); super(`User Exec command "${execPath}" not found on host.`);
let message = `User Exec command "${execPath}" not found on host.`; let message = `User Exec command "${execPath}" not found on host.`;
if (!isAbsolute) { if (!isAbsolute) {
message += ` Please ensure binary is found in PATH or use absolute path to binary in Kubeconfig`; message += ` Please ensure binary is found in PATH or use absolute path to binary in Kubeconfig`;
} }

View File

@ -13,6 +13,7 @@ export class EventEmitter<D extends [...any[]]> {
addListener(callback: Callback<D>, options: Options = {}) { addListener(callback: Callback<D>, options: Options = {}) {
if (options.prepend) { if (options.prepend) {
const listeners = [...this.listeners]; const listeners = [...this.listeners];
listeners.unshift([callback, options]); listeners.unshift([callback, options]);
this.listeners = new Map(listeners); this.listeners = new Map(listeners);
} }
@ -33,7 +34,9 @@ export class EventEmitter<D extends [...any[]]> {
[...this.listeners].every(([callback, options]) => { [...this.listeners].every(([callback, options]) => {
if (options.once) this.removeListener(callback); if (options.once) this.removeListener(callback);
const result = callback(...data); const result = callback(...data);
if (result === false) return; // break cycle if (result === false) return; // break cycle
return true; return true;
}); });
} }

View File

@ -16,18 +16,22 @@ export async function requestMain(channel: string, ...args: any[]) {
async function getSubFrames(): Promise<number[]> { async function getSubFrames(): Promise<number[]> {
const subFrames: number[] = []; const subFrames: number[] = [];
clusterFrameMap.forEach(frameId => { clusterFrameMap.forEach(frameId => {
subFrames.push(frameId); subFrames.push(frameId);
}); });
return subFrames; return subFrames;
} }
export function broadcastMessage(channel: string, ...args: any[]) { export function broadcastMessage(channel: string, ...args: any[]) {
const views = (webContents || remote?.webContents)?.getAllWebContents(); const views = (webContents || remote?.webContents)?.getAllWebContents();
if (!views) return; if (!views) return;
views.forEach(webContent => { views.forEach(webContent => {
const type = webContent.getType(); const type = webContent.getType();
logger.silly(`[IPC]: broadcasting "${channel}" to ${type}=${webContent.id}`, { args }); logger.silly(`[IPC]: broadcasting "${channel}" to ${type}=${webContent.id}`, { args });
webContent.send(channel, ...args); webContent.send(channel, ...args);
getSubFrames().then((frames) => { getSubFrames().then((frames) => {
@ -36,6 +40,7 @@ export function broadcastMessage(channel: string, ...args: any[]) {
}); });
}).catch((e) => e); }).catch((e) => e);
}); });
if (ipcRenderer) { if (ipcRenderer) {
ipcRenderer.send(channel, ...args); ipcRenderer.send(channel, ...args);
} else { } else {

View File

@ -13,6 +13,7 @@ function resolveTilde(filePath: string) {
if (filePath[0] === "~" && (filePath[1] === "/" || filePath.length === 1)) { if (filePath[0] === "~" && (filePath[1] === "/" || filePath.length === 1)) {
return filePath.replace("~", os.homedir()); return filePath.replace("~", os.homedir());
} }
return filePath; return filePath;
} }
@ -40,12 +41,15 @@ export function validateConfig(config: KubeConfig | string): KubeConfig {
config = loadConfig(config); config = loadConfig(config);
} }
logger.debug(`validating kube config: ${JSON.stringify(config)}`); logger.debug(`validating kube config: ${JSON.stringify(config)}`);
if (!config.users || config.users.length == 0) { if (!config.users || config.users.length == 0) {
throw new Error("No users provided in config"); throw new Error("No users provided in config");
} }
if (!config.clusters || config.clusters.length == 0) { if (!config.clusters || config.clusters.length == 0) {
throw new Error("No clusters provided in config"); throw new Error("No clusters provided in config");
} }
if (!config.contexts || config.contexts.length == 0) { if (!config.contexts || config.contexts.length == 0) {
throw new Error("No contexts provided in config"); throw new Error("No contexts provided in config");
} }
@ -58,11 +62,13 @@ export function validateConfig(config: KubeConfig | string): KubeConfig {
*/ */
export function splitConfig(kubeConfig: KubeConfig): KubeConfig[] { export function splitConfig(kubeConfig: KubeConfig): KubeConfig[] {
const configs: KubeConfig[] = []; const configs: KubeConfig[] = [];
if (!kubeConfig.contexts) { if (!kubeConfig.contexts) {
return configs; return configs;
} }
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(n => n);
kc.users = [kubeConfig.getUser(ctx.user)].filter(n => n); kc.users = [kubeConfig.getUser(ctx.user)].filter(n => n);
kc.contexts = [kubeConfig.getContextObject(ctx.name)].filter(n => n); kc.contexts = [kubeConfig.getContextObject(ctx.name)].filter(n => n);
@ -70,6 +76,7 @@ export function splitConfig(kubeConfig: KubeConfig): KubeConfig[] {
configs.push(kc); configs.push(kc);
}); });
return configs; return configs;
} }
@ -154,10 +161,12 @@ export function validateKubeConfig (config: KubeConfig) {
// Validate the User Object // Validate the User Object
const user = config.getCurrentUser(); const user = config.getCurrentUser();
if (user.exec) { if (user.exec) {
const execCommand = user.exec["command"]; const execCommand = user.exec["command"];
// check if the command is absolute or not // check if the command is absolute or not
const isAbsolute = path.isAbsolute(execCommand); const isAbsolute = path.isAbsolute(execCommand);
// validate the exec struct in the user object, start with the command field // validate the exec struct in the user object, start with the command field
logger.debug(`validateKubeConfig: validating user exec command - ${JSON.stringify(execCommand)}`); logger.debug(`validateKubeConfig: validating user exec command - ${JSON.stringify(execCommand)}`);

View File

@ -6,6 +6,7 @@ import { PrometheusProviderRegistry } from "../main/prometheus/provider-registry
[PrometheusLens, PrometheusHelm, PrometheusOperator, PrometheusStacklight].forEach(providerClass => { [PrometheusLens, PrometheusHelm, PrometheusOperator, PrometheusStacklight].forEach(providerClass => {
const provider = new providerClass(); const provider = new providerClass();
PrometheusProviderRegistry.registerProvider(provider.id, provider); PrometheusProviderRegistry.registerProvider(provider.id, provider);
}); });

View File

@ -42,10 +42,12 @@ export function isAllowedResource(resources: KubeResource | KubeResource[]) {
resources = [resources]; resources = [resources];
} }
const { allowedResources = [] } = getHostedCluster() || {}; const { allowedResources = [] } = getHostedCluster() || {};
for (const resource of resources) { for (const resource of resources) {
if (!allowedResources.includes(resource)) { if (!allowedResources.includes(resource)) {
return false; return false;
} }
} }
return true; return true;
} }

View File

@ -7,6 +7,7 @@ export function registerFileProtocol(name: string, basePath: string) {
protocol.registerFileProtocol(name, (request, callback) => { protocol.registerFileProtocol(name, (request, callback) => {
const filePath = request.url.replace(`${name}://`, ""); const filePath = request.url.replace(`${name}://`, "");
const absPath = path.resolve(basePath, filePath); const absPath = path.resolve(basePath, filePath);
callback({ path: absPath }); callback({ path: absPath });
}); });
} }

View File

@ -7,6 +7,7 @@ import { userStore } from "./user-store";
function getDefaultRequestOpts(): Partial<request.Options> { function getDefaultRequestOpts(): Partial<request.Options> {
const { httpsProxy, allowUntrustedCAs } = userStore.preferences; const { httpsProxy, allowUntrustedCAs } = userStore.preferences;
return { return {
proxy: httpsProxy || undefined, proxy: httpsProxy || undefined,
rejectUnauthorized: !allowUntrustedCAs, rejectUnauthorized: !allowUntrustedCAs,

View File

@ -14,8 +14,10 @@ export class SearchStore {
@action @action
onSearch(text: string[], query = this.searchQuery) { onSearch(text: string[], query = this.searchQuery) {
this.searchQuery = query; this.searchQuery = query;
if (!query) { if (!query) {
this.reset(); this.reset();
return; return;
} }
this.occurrences = this.findOccurences(text, query); this.occurrences = this.findOccurences(text, query);
@ -36,11 +38,14 @@ export class SearchStore {
findOccurences(text: string[], query: string) { findOccurences(text: string[], query: string) {
if (!text) return []; if (!text) return [];
const occurences: number[] = []; const occurences: number[] = [];
text.forEach((line, index) => { text.forEach((line, index) => {
const regex = new RegExp(this.escapeRegex(query), "gi"); const regex = new RegExp(this.escapeRegex(query), "gi");
const matches = [...line.matchAll(regex)]; const matches = [...line.matchAll(regex)];
matches.forEach(() => occurences.push(index)); matches.forEach(() => occurences.push(index));
}); });
return occurences; return occurences;
} }
@ -51,9 +56,11 @@ export class SearchStore {
*/ */
getNextOverlay(loopOver = false) { getNextOverlay(loopOver = false) {
const next = this.activeOverlayIndex + 1; const next = this.activeOverlayIndex + 1;
if (next > this.occurrences.length - 1) { if (next > this.occurrences.length - 1) {
return loopOver ? 0 : this.activeOverlayIndex; return loopOver ? 0 : this.activeOverlayIndex;
} }
return next; return next;
} }
@ -64,9 +71,11 @@ export class SearchStore {
*/ */
getPrevOverlay(loopOver = false) { getPrevOverlay(loopOver = false) {
const prev = this.activeOverlayIndex - 1; const prev = this.activeOverlayIndex - 1;
if (prev < 0) { if (prev < 0) {
return loopOver ? this.occurrences.length - 1 : this.activeOverlayIndex; return loopOver ? this.occurrences.length - 1 : this.activeOverlayIndex;
} }
return prev; return prev;
} }
@ -104,6 +113,7 @@ export class SearchStore {
@autobind() @autobind()
isActiveOverlay(line: number, occurence: number) { isActiveOverlay(line: number, occurence: number) {
const firstLineIndex = this.occurrences.findIndex(item => item === line); const firstLineIndex = this.occurrences.findIndex(item => item === line);
return firstLineIndex + occurence === this.activeOverlayIndex; return firstLineIndex + occurence === this.activeOverlayIndex;
} }

View File

@ -6,9 +6,11 @@ import logger from "../main/logger";
if (isMac) { if (isMac) {
for (const crt of macca.all()) { for (const crt of macca.all()) {
const attributes = crt.issuer?.attributes?.map((a: any) => `${a.name}=${a.value}`); const attributes = crt.issuer?.attributes?.map((a: any) => `${a.name}=${a.value}`);
logger.debug(`Using host CA: ${attributes.join(",")}`); logger.debug(`Using host CA: ${attributes.join(",")}`);
} }
} }
if (isWindows) { if (isWindows) {
winca.inject("+"); // see: https://github.com/ukoloff/win-ca#caveats winca.inject("+"); // see: https://github.com/ukoloff/win-ca#caveats
} }

View File

@ -102,6 +102,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
protected refreshNewContexts = async () => { protected refreshNewContexts = async () => {
try { try {
const kubeConfig = await readFile(this.kubeConfigPath, "utf8"); const kubeConfig = await readFile(this.kubeConfigPath, "utf8");
if (kubeConfig) { if (kubeConfig) {
this.newContexts.clear(); this.newContexts.clear();
loadConfig(kubeConfig).getContexts() loadConfig(kubeConfig).getContexts()
@ -118,6 +119,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
@action @action
markNewContextsAsSeen() { markNewContextsAsSeen() {
const { seenContexts, newContexts } = this; const { seenContexts, newContexts } = this;
this.seenContexts.replace([...seenContexts, ...newContexts]); this.seenContexts.replace([...seenContexts, ...newContexts]);
this.newContexts.clear(); this.newContexts.clear();
} }
@ -133,9 +135,11 @@ export class UserStore extends BaseStore<UserStoreModel> {
@action @action
protected async fromStore(data: Partial<UserStoreModel> = {}) { protected async fromStore(data: Partial<UserStoreModel> = {}) {
const { lastSeenAppVersion, seenContexts = [], preferences, kubeConfigPath } = data; const { lastSeenAppVersion, seenContexts = [], preferences, kubeConfigPath } = data;
if (lastSeenAppVersion) { if (lastSeenAppVersion) {
this.lastSeenAppVersion = lastSeenAppVersion; this.lastSeenAppVersion = lastSeenAppVersion;
} }
if (kubeConfigPath) { if (kubeConfigPath) {
this.kubeConfigPath = kubeConfigPath; this.kubeConfigPath = kubeConfigPath;
} }
@ -150,6 +154,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
seenContexts: Array.from(this.seenContexts), seenContexts: Array.from(this.seenContexts),
preferences: this.preferences, preferences: this.preferences,
}; };
return toJS(model, { return toJS(model, {
recurseEverything: true, recurseEverything: true,
}); });

View File

@ -12,7 +12,6 @@ export function autobind() {
function bindClass<T extends Constructor>(constructor: T) { function bindClass<T extends Constructor>(constructor: T) {
const proto = constructor.prototype; const proto = constructor.prototype;
const descriptors = Object.getOwnPropertyDescriptors(proto); const descriptors = Object.getOwnPropertyDescriptors(proto);
const skipMethod = (methodName: string) => { const skipMethod = (methodName: string) => {
return methodName === "constructor" return methodName === "constructor"
|| typeof descriptors[methodName].value !== "function"; || typeof descriptors[methodName].value !== "function";
@ -21,6 +20,7 @@ function bindClass<T extends Constructor>(constructor: T) {
Object.keys(descriptors).forEach(prop => { Object.keys(descriptors).forEach(prop => {
if (skipMethod(prop)) return; if (skipMethod(prop)) return;
const boundDescriptor = bindMethod(proto, prop, descriptors[prop]); const boundDescriptor = bindMethod(proto, prop, descriptors[prop]);
Object.defineProperty(proto, prop, boundDescriptor); Object.defineProperty(proto, prop, boundDescriptor);
}); });
} }
@ -38,6 +38,7 @@ function bindMethod(target: object, prop?: string, descriptor?: PropertyDescript
get() { get() {
if (this === target) return func; // direct access from prototype if (this === target) return func; // direct access from prototype
if (!boundFunc.has(this)) boundFunc.set(this, func.bind(this)); if (!boundFunc.has(this)) boundFunc.set(this, func.bind(this));
return boundFunc.get(this); return boundFunc.get(this);
} }
}); });

View File

@ -7,8 +7,10 @@ export interface IURLParams<P extends object = {}, Q extends object = {}> {
export function buildURL<P extends object = {}, Q extends object = {}>(path: string | any) { export function buildURL<P extends object = {}, Q extends object = {}>(path: string | any) {
const pathBuilder = compile(String(path)); const pathBuilder = compile(String(path));
return function ({ params, query }: IURLParams<P, Q> = {}) { return function ({ params, query }: IURLParams<P, Q> = {}) {
const queryParams = query ? new URLSearchParams(Object.entries(query)).toString() : ""; const queryParams = query ? new URLSearchParams(Object.entries(query)).toString() : "";
return pathBuilder(params) + (queryParams ? `?${queryParams}` : ""); return pathBuilder(params) + (queryParams ? `?${queryParams}` : "");
}; };
} }

View File

@ -8,7 +8,9 @@ export function toCamelCase(obj: Record<string, any>): any {
else if (isPlainObject(obj)) { else if (isPlainObject(obj)) {
return Object.keys(obj).reduce((result, key) => { return Object.keys(obj).reduce((result, key) => {
const value = obj[key]; const value = obj[key];
result[camelCase(key)] = typeof value === "object" ? toCamelCase(value) : value; result[camelCase(key)] = typeof value === "object" ? toCamelCase(value) : value;
return result; return result;
}, {} as any); }, {} as any);
} }

View File

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

View File

@ -26,6 +26,7 @@ export function downloadFile({ url, timeout, gzip = true }: DownloadFileOptions)
resolve(Buffer.concat(fileChunks)); resolve(Buffer.concat(fileChunks));
}); });
}); });
return { return {
url, url,
promise, promise,

View File

@ -2,5 +2,6 @@
export function getRandId({ prefix = "", suffix = "", sep = "_" } = {}) { export function getRandId({ prefix = "", suffix = "", sep = "_" } = {}) {
const randId = () => Math.random().toString(16).substr(2); const randId = () => Math.random().toString(16).substr(2);
return [prefix, randId(), suffix].filter(s => s).join(sep); return [prefix, randId(), suffix].filter(s => s).join(sep);
} }

View File

@ -6,7 +6,9 @@ import { WriteFileOptions } from "fs";
export function saveToAppFiles(filePath: string, contents: any, options?: WriteFileOptions): string { export function saveToAppFiles(filePath: string, contents: any, options?: WriteFileOptions): string {
const absPath = path.resolve((app || remote.app).getPath("userData"), filePath); const absPath = path.resolve((app || remote.app).getPath("userData"), filePath);
ensureDirSync(path.dirname(absPath)); ensureDirSync(path.dirname(absPath));
writeFileSync(absPath, contents, options); writeFileSync(absPath, contents, options);
return absPath; return absPath;
} }

View File

@ -16,6 +16,7 @@ class Singleton {
if (!Singleton.instances.has(this)) { if (!Singleton.instances.has(this)) {
Singleton.instances.set(this, Reflect.construct(this, args)); Singleton.instances.set(this, Reflect.construct(this, args));
} }
return Singleton.instances.get(this) as T; return Singleton.instances.get(this) as T;
} }

View File

@ -12,8 +12,10 @@
*/ */
export function splitArray<T>(array: T[], element: T): [T[], T[], boolean] { export function splitArray<T>(array: T[], element: T): [T[], T[], boolean] {
const index = array.indexOf(element); const index = array.indexOf(element);
if (index < 0) { if (index < 0) {
return [array, [], false]; return [array, [], false];
} }
return [array.slice(0, index), array.slice(index + 1, array.length), true]; return [array.slice(0, index), array.slice(index + 1, array.length), true];
} }

View File

@ -26,6 +26,7 @@ export function readFileFromTar<R = Buffer>({ tarPath, filePath, parseJson }: Re
entry.once("end", () => { entry.once("end", () => {
const data = Buffer.concat(fileChunks); const data = Buffer.concat(fileChunks);
const result = parseJson ? JSON.parse(data.toString("utf8")) : data; const result = parseJson ? JSON.parse(data.toString("utf8")) : data;
resolve(result); resolve(result);
}); });
}, },
@ -39,12 +40,14 @@ export function readFileFromTar<R = Buffer>({ tarPath, filePath, parseJson }: Re
export async function listTarEntries(filePath: string): Promise<string[]> { export async function listTarEntries(filePath: string): Promise<string[]> {
const entries: string[] = []; const entries: string[] = [];
await tar.list({ await tar.list({
file: filePath, file: filePath,
onentry: (entry: FileStat) => { onentry: (entry: FileStat) => {
entries.push(path.normalize(entry.path as any as string)); entries.push(path.normalize(entry.path as any as string));
}, },
}); });
return entries; return entries;
} }

View File

@ -30,6 +30,7 @@ defineGlobal("__static", {
if (isDevelopment) { if (isDevelopment) {
return path.resolve(contextDir, "static"); return path.resolve(contextDir, "static");
} }
return path.resolve(process.resourcesPath, "static"); return path.resolve(process.resourcesPath, "static");
} }
}); });

View File

@ -125,11 +125,14 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
id: string; id: string;
state: WorkspaceState; state: WorkspaceState;
}; };
if (ipcRenderer) { if (ipcRenderer) {
logger.info("[WORKSPACE-STORE] requesting initial state sync"); logger.info("[WORKSPACE-STORE] requesting initial state sync");
const workspaceStates: workspaceStateSync[] = await requestMain(WorkspaceStore.stateRequestChannel); const workspaceStates: workspaceStateSync[] = await requestMain(WorkspaceStore.stateRequestChannel);
workspaceStates.forEach((workspaceState) => { workspaceStates.forEach((workspaceState) => {
const workspace = this.getById(workspaceState.id); const workspace = this.getById(workspaceState.id);
if (workspace) { if (workspace) {
workspace.setState(workspaceState.state); workspace.setState(workspaceState.state);
} }
@ -137,12 +140,14 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
} else { } else {
handleRequest(WorkspaceStore.stateRequestChannel, (): workspaceStateSync[] => { handleRequest(WorkspaceStore.stateRequestChannel, (): workspaceStateSync[] => {
const states: workspaceStateSync[] = []; const states: workspaceStateSync[] = [];
this.workspacesList.forEach((workspace) => { this.workspacesList.forEach((workspace) => {
states.push({ states.push({
state: workspace.getState(), state: workspace.getState(),
id: workspace.id id: workspace.id
}); });
}); });
return states; return states;
}); });
} }
@ -202,6 +207,7 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
@action @action
setActive(id = WorkspaceStore.defaultId) { setActive(id = WorkspaceStore.defaultId) {
if (id === this.currentWorkspaceId) return; if (id === this.currentWorkspaceId) return;
if (!this.getById(id)) { if (!this.getById(id)) {
throw new Error(`workspace ${id} doesn't exist`); throw new Error(`workspace ${id} doesn't exist`);
} }
@ -211,15 +217,18 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
@action @action
addWorkspace(workspace: Workspace) { addWorkspace(workspace: Workspace) {
const { id, name } = workspace; const { id, name } = workspace;
if (!name.trim() || this.getByName(name.trim())) { if (!name.trim() || this.getByName(name.trim())) {
return; return;
} }
this.workspaces.set(id, workspace); this.workspaces.set(id, workspace);
if (!workspace.isManaged) { if (!workspace.isManaged) {
workspace.enabled = true; workspace.enabled = true;
} }
appEventBus.emit({name: "workspace", action: "add"}); appEventBus.emit({name: "workspace", action: "add"});
return workspace; return workspace;
} }
@ -237,10 +246,13 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
@action @action
removeWorkspaceById(id: WorkspaceId) { removeWorkspaceById(id: WorkspaceId) {
const workspace = this.getById(id); const workspace = this.getById(id);
if (!workspace) return; if (!workspace) return;
if (this.isDefault(id)) { if (this.isDefault(id)) {
throw new Error("Cannot remove default workspace"); throw new Error("Cannot remove default workspace");
} }
if (this.currentWorkspaceId === id) { if (this.currentWorkspaceId === id) {
this.currentWorkspaceId = WorkspaceStore.defaultId; // reset to default this.currentWorkspaceId = WorkspaceStore.defaultId; // reset to default
} }
@ -259,10 +271,12 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
if (currentWorkspace) { if (currentWorkspace) {
this.currentWorkspaceId = currentWorkspace; this.currentWorkspaceId = currentWorkspace;
} }
if (workspaces.length) { if (workspaces.length) {
this.workspaces.clear(); this.workspaces.clear();
workspaces.forEach(ws => { workspaces.forEach(ws => {
const workspace = new Workspace(ws); const workspace = new Workspace(ws);
if (!workspace.isManaged) { if (!workspace.isManaged) {
workspace.enabled = true; workspace.enabled = true;
} }

View File

@ -108,12 +108,15 @@ export abstract class ClusterFeature {
*/ */
protected renderTemplates(folderPath: string): string[] { protected renderTemplates(folderPath: string): string[] {
const resources: string[] = []; const resources: string[] = [];
logger.info(`[FEATURE]: render templates from ${folderPath}`); logger.info(`[FEATURE]: render templates from ${folderPath}`);
fs.readdirSync(folderPath).forEach(filename => { fs.readdirSync(folderPath).forEach(filename => {
const file = path.join(folderPath, filename); const file = path.join(folderPath, filename);
const raw = fs.readFileSync(file); const raw = fs.readFileSync(file);
if (filename.endsWith(".hb")) { if (filename.endsWith(".hb")) {
const template = hb.compile(raw.toString()); const template = hb.compile(raw.toString());
resources.push(template(this.templateContext)); resources.push(template(this.templateContext));
} else { } else {
resources.push(raw.toString()); resources.push(raw.toString());

View File

@ -3,6 +3,7 @@ import { extensionsStore } from "../extensions-store";
export const version = getAppVersion(); export const version = getAppVersion();
export { isSnap, isWindows, isMac, isLinux, appName, slackUrl, issuesTrackerUrl } from "../../common/vars"; export { isSnap, isWindows, isMac, isLinux, appName, slackUrl, issuesTrackerUrl } from "../../common/vars";
export function getEnabledExtensions(): string[] { export function getEnabledExtensions(): string[] {
return extensionsStore.enabledExtensions; return extensionsStore.enabledExtensions;
} }

View File

@ -25,6 +25,7 @@ export interface InstalledExtension {
} }
const logModule = "[EXTENSION-DISCOVERY]"; const logModule = "[EXTENSION-DISCOVERY]";
export const manifestFilename = "package.json"; export const manifestFilename = "package.json";
/** /**
@ -133,7 +134,6 @@ export class ExtensionDiscovery {
if (path.basename(filePath) === manifestFilename) { if (path.basename(filePath) === manifestFilename) {
try { try {
const absPath = path.dirname(filePath); const absPath = path.dirname(filePath);
// this.loadExtensionFromPath updates this.packagesJson // this.loadExtensionFromPath updates this.packagesJson
const extension = await this.loadExtensionFromPath(absPath); const extension = await this.loadExtensionFromPath(absPath);
@ -251,6 +251,7 @@ export class ExtensionDiscovery {
manifestJson = __non_webpack_require__(manifestPath); manifestJson = __non_webpack_require__(manifestPath);
const installedManifestPath = path.join(this.nodeModulesPath, manifestJson.name, "package.json"); const installedManifestPath = path.join(this.nodeModulesPath, manifestJson.name, "package.json");
this.packagesJson.dependencies[manifestJson.name] = path.dirname(manifestPath); this.packagesJson.dependencies[manifestJson.name] = path.dirname(manifestPath);
const isEnabled = isBundled || extensionsStore.isEnabled(installedManifestPath); const isEnabled = isBundled || extensionsStore.isEnabled(installedManifestPath);
@ -272,6 +273,7 @@ export class ExtensionDiscovery {
async loadExtensions(): Promise<Map<LensExtensionId, InstalledExtension>> { async loadExtensions(): Promise<Map<LensExtensionId, InstalledExtension>> {
const bundledExtensions = await this.loadBundledExtensions(); const bundledExtensions = await this.loadBundledExtensions();
const localExtensions = await this.loadFromFolder(this.localFolderPath); const localExtensions = await this.loadFromFolder(this.localFolderPath);
await this.installPackages(); await this.installPackages();
const extensions = bundledExtensions.concat(localExtensions); const extensions = bundledExtensions.concat(localExtensions);
@ -333,12 +335,14 @@ export class ExtensionDiscovery {
} }
const extension = await this.loadExtensionFromPath(absPath); const extension = await this.loadExtensionFromPath(absPath);
if (extension) { if (extension) {
extensions.push(extension); extensions.push(extension);
} }
} }
logger.debug(`${logModule}: ${extensions.length} extensions loaded`, { folderPath, extensions }); logger.debug(`${logModule}: ${extensions.length} extensions loaded`, { folderPath, extensions });
return extensions; return extensions;
} }

View File

@ -37,6 +37,7 @@ export class ExtensionInstaller {
cwd: extensionPackagesRoot(), cwd: extensionPackagesRoot(),
silent: true silent: true
}); });
child.on("close", () => { child.on("close", () => {
resolve(); resolve();
}); });

View File

@ -176,6 +176,7 @@ export class ExtensionLoader {
loadOnClusterRenderer() { loadOnClusterRenderer() {
logger.info(`${logModule}: load on cluster renderer (dashboard)`); logger.info(`${logModule}: load on cluster renderer (dashboard)`);
const cluster = getHostedCluster(); const cluster = getHostedCluster();
this.autoInitExtensions(async (extension: LensRendererExtension) => { this.autoInitExtensions(async (extension: LensRendererExtension) => {
if (await extension.isEnabledForCluster(cluster) === false) { if (await extension.isEnabledForCluster(cluster) === false) {
return []; return [];
@ -209,11 +210,13 @@ export class ExtensionLoader {
if (ext.isEnabled && !alreadyInit) { if (ext.isEnabled && !alreadyInit) {
try { try {
const LensExtensionClass = this.requireExtension(ext); const LensExtensionClass = this.requireExtension(ext);
if (!LensExtensionClass) { if (!LensExtensionClass) {
continue; continue;
} }
const instance = new LensExtensionClass(ext); const instance = new LensExtensionClass(ext);
instance.whenEnabled(() => register(instance)); instance.whenEnabled(() => register(instance));
instance.enable(); instance.enable();
this.instances.set(extId, instance); this.instances.set(extId, instance);
@ -231,12 +234,14 @@ export class ExtensionLoader {
protected requireExtension(extension: InstalledExtension): LensExtensionConstructor { protected requireExtension(extension: InstalledExtension): LensExtensionConstructor {
let extEntrypoint = ""; let extEntrypoint = "";
try { try {
if (ipcRenderer && extension.manifest.renderer) { if (ipcRenderer && extension.manifest.renderer) {
extEntrypoint = path.resolve(path.join(path.dirname(extension.manifestPath), extension.manifest.renderer)); extEntrypoint = path.resolve(path.join(path.dirname(extension.manifestPath), extension.manifest.renderer));
} else if (!ipcRenderer && extension.manifest.main) { } else if (!ipcRenderer && extension.manifest.main) {
extEntrypoint = path.resolve(path.join(path.dirname(extension.manifestPath), extension.manifest.main)); extEntrypoint = path.resolve(path.join(path.dirname(extension.manifestPath), extension.manifest.main));
} }
if (extEntrypoint !== "") { if (extEntrypoint !== "") {
return __non_webpack_require__(extEntrypoint).default; return __non_webpack_require__(extEntrypoint).default;
} }

View File

@ -7,11 +7,13 @@ export abstract class ExtensionStore<T> extends BaseStore<T> {
async loadExtension(extension: LensExtension) { async loadExtension(extension: LensExtension) {
this.extension = extension; this.extension = extension;
return super.load(); return super.load();
} }
async load() { async load() {
if (!this.extension) { return; } if (!this.extension) { return; }
return super.load(); return super.load();
} }

View File

@ -30,11 +30,13 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
protected getState(extensionLoader: ExtensionLoader) { protected getState(extensionLoader: ExtensionLoader) {
const state: Record<LensExtensionId, LensExtensionState> = {}; const state: Record<LensExtensionId, LensExtensionState> = {};
return Array.from(extensionLoader.userExtensions).reduce((state, [extId, ext]) => { return Array.from(extensionLoader.userExtensions).reduce((state, [extId, ext]) => {
state[extId] = { state[extId] = {
enabled: ext.isEnabled, enabled: ext.isEnabled,
name: ext.manifest.name, name: ext.manifest.name,
}; };
return state; return state;
}, state); }, state);
} }
@ -47,6 +49,7 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
reaction(() => this.state.toJS(), extensionsState => { reaction(() => this.state.toJS(), extensionsState => {
extensionsState.forEach((state, extId) => { extensionsState.forEach((state, extId) => {
const ext = extensionLoader.getExtension(extId); const ext = extensionLoader.getExtension(extId);
if (ext && !ext.isBundled) { if (ext && !ext.isBundled) {
ext.isEnabled = state.enabled; ext.isEnabled = state.enabled;
} }
@ -61,6 +64,7 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
isEnabled(extId: LensExtensionId) { isEnabled(extId: LensExtensionId) {
const state = this.state.get(extId); const state = this.state.get(extId);
return state && state.enabled; // by default false return state && state.enabled; // by default false
} }

View File

@ -86,6 +86,7 @@ export class LensExtension {
const cancelReaction = reaction(() => this.isEnabled, async (isEnabled) => { const cancelReaction = reaction(() => this.isEnabled, async (isEnabled) => {
if (isEnabled) { if (isEnabled) {
const handlerDisposers = await handlers(); const handlerDisposers = await handlers();
disposers.push(...handlerDisposers); disposers.push(...handlerDisposers);
} else { } else {
unregisterHandlers(); unregisterHandlers();
@ -93,6 +94,7 @@ export class LensExtension {
}, { }, {
fireImmediately: true fireImmediately: true
}); });
return () => { return () => {
unregisterHandlers(); unregisterHandlers();
cancelReaction(); cancelReaction();

View File

@ -13,6 +13,7 @@ export class LensMainExtension extends LensExtension {
pageId, pageId,
params: params ?? {}, // compile to url with params params: params ?? {}, // compile to url with params
}); });
await windowManager.navigate(pageUrl, frameId); await windowManager.navigate(pageUrl, frameId);
} }
} }

View File

@ -23,6 +23,7 @@ export class LensRendererExtension extends LensExtension {
pageId, pageId,
params: params ?? {}, // compile to url with params params: params ?? {}, // compile to url with params
}); });
navigate(pageUrl); navigate(pageUrl);
} }

View File

@ -73,6 +73,7 @@ describe("globalPageRegistry", () => {
describe("getByPageMenuTarget", () => { describe("getByPageMenuTarget", () => {
it("matching to first registered page without id", () => { it("matching to first registered page without id", () => {
const page = globalPageRegistry.getByPageMenuTarget({ extensionId: ext.name }); const page = globalPageRegistry.getByPageMenuTarget({ extensionId: ext.name });
expect(page.id).toEqual(undefined); expect(page.id).toEqual(undefined);
expect(page.extensionId).toEqual(ext.name); expect(page.extensionId).toEqual(ext.name);
expect(page.routePath).toEqual(getExtensionPageUrl({ extensionId: ext.name })); expect(page.routePath).toEqual(getExtensionPageUrl({ extensionId: ext.name }));
@ -83,6 +84,7 @@ describe("globalPageRegistry", () => {
pageId: "test-page", pageId: "test-page",
extensionId: ext.name extensionId: ext.name
}); });
expect(page.id).toEqual("test-page"); expect(page.id).toEqual("test-page");
}); });
@ -91,6 +93,7 @@ describe("globalPageRegistry", () => {
pageId: "wrong-page", pageId: "wrong-page",
extensionId: ext.name extensionId: ext.name
}); });
expect(page).toBeNull(); expect(page).toBeNull();
}); });
}); });

View File

@ -14,7 +14,9 @@ export class BaseRegistry<T> {
@action @action
add(items: T | T[]) { add(items: T | T[]) {
const itemArray = rectify(items); const itemArray = rectify(items);
this.items.push(...itemArray); this.items.push(...itemArray);
return () => this.remove(...itemArray); return () => this.remove(...itemArray);
} }

View File

@ -20,8 +20,10 @@ export class KubeObjectDetailRegistry extends BaseRegistry<KubeObjectDetailRegis
if (item.priority === null) { if (item.priority === null) {
item.priority = 50; item.priority = 50;
} }
return item; return item;
}); });
return items.sort((a, b) => b.priority - a.priority); return items.sort((a, b) => b.priority - a.priority);
} }
} }

View File

@ -35,8 +35,10 @@ export class GlobalPageMenuRegistry extends BaseRegistry<PageMenuRegistration> {
extensionId: ext.name, extensionId: ext.name,
...(menuItem.target || {}), ...(menuItem.target || {}),
}; };
return menuItem; return menuItem;
}); });
return super.add(normalizedItems); return super.add(normalizedItems);
} }
} }
@ -49,8 +51,10 @@ export class ClusterPageMenuRegistry extends BaseRegistry<ClusterPageMenuRegistr
extensionId: ext.name, extensionId: ext.name,
...(menuItem.target || {}), ...(menuItem.target || {}),
}; };
return menuItem; return menuItem;
}); });
return super.add(normalizedItems); return super.add(normalizedItems);
} }

View File

@ -45,9 +45,11 @@ export function getExtensionPageUrl<P extends object>({ extensionId, pageId = ""
name: sanitizeExtensionName(extensionId), // compile only with extension-id first and define base path name: sanitizeExtensionName(extensionId), // compile only with extension-id first and define base path
}); });
const extPageRoutePath = path.join(extensionBaseUrl, pageId); // page-id might contain route :param-s, so don't compile yet const extPageRoutePath = path.join(extensionBaseUrl, pageId); // page-id might contain route :param-s, so don't compile yet
if (params) { if (params) {
return compile(extPageRoutePath)(params); // might throw error when required params not passed return compile(extPageRoutePath)(params); // might throw error when required params not passed
} }
return extPageRoutePath; return extPageRoutePath;
} }
@ -56,6 +58,7 @@ export class PageRegistry extends BaseRegistry<RegisteredPage> {
add(items: PageRegistration | PageRegistration[], ext: LensExtension) { add(items: PageRegistration | PageRegistration[], ext: LensExtension) {
const itemArray = rectify(items); const itemArray = rectify(items);
let registeredPages: RegisteredPage[] = []; let registeredPages: RegisteredPage[] = [];
try { try {
registeredPages = itemArray.map(page => ({ registeredPages = itemArray.map(page => ({
...page, ...page,
@ -69,6 +72,7 @@ export class PageRegistry extends BaseRegistry<RegisteredPage> {
error: String(err), error: String(err),
}); });
} }
return super.add(registeredPages); return super.add(registeredPages);
} }
@ -78,8 +82,10 @@ export class PageRegistry extends BaseRegistry<RegisteredPage> {
getByPageMenuTarget(target: PageMenuTarget = {}): RegisteredPage | null { getByPageMenuTarget(target: PageMenuTarget = {}): RegisteredPage | null {
const targetUrl = getExtensionPageUrl(target); const targetUrl = getExtensionPageUrl(target);
return this.getItems().find(({ id: pageId, extensionId }) => { return this.getItems().find(({ id: pageId, extensionId }) => {
const pageUrl = getExtensionPageUrl({ extensionId, pageId, params: target.params }); // compiled with provided params const pageUrl = getExtensionPageUrl({ extensionId, pageId, params: target.params }); // compiled with provided params
return targetUrl === pageUrl; return targetUrl === pageUrl;
}) || null; }) || null;
} }

View File

@ -40,6 +40,7 @@ export class ClusterStore extends Singleton {
if (!this.activeClusterId) { if (!this.activeClusterId) {
return null; return null;
} }
return this.getById(this.activeClusterId); return this.getById(this.activeClusterId);
} }

View File

@ -75,6 +75,7 @@ describe("create clusters", () => {
preferences: {}, preferences: {},
}) })
}; };
mockFs(mockOpts); mockFs(mockOpts);
jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValue(Promise.resolve(true)); jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValue(Promise.resolve(true));
c = new Cluster({ c = new Cluster({
@ -112,6 +113,7 @@ describe("create clusters", () => {
it("activating cluster should try to connect to cluster and do a refresh", async () => { it("activating cluster should try to connect to cluster and do a refresh", async () => {
const port = await getFreePort(); const port = await getFreePort();
jest.spyOn(ContextHandler.prototype, "ensureServer"); jest.spyOn(ContextHandler.prototype, "ensureServer");
const mockListNSs = jest.fn(); const mockListNSs = jest.fn();
@ -122,17 +124,20 @@ describe("create clusters", () => {
}; };
} }
}; };
jest.spyOn(Cluster.prototype, "isClusterAdmin").mockReturnValue(Promise.resolve(true)); jest.spyOn(Cluster.prototype, "isClusterAdmin").mockReturnValue(Promise.resolve(true));
jest.spyOn(Cluster.prototype, "canI") jest.spyOn(Cluster.prototype, "canI")
.mockImplementationOnce((attr: V1ResourceAttributes): Promise<boolean> => { .mockImplementationOnce((attr: V1ResourceAttributes): Promise<boolean> => {
expect(attr.namespace).toBe("default"); expect(attr.namespace).toBe("default");
expect(attr.resource).toBe("pods"); expect(attr.resource).toBe("pods");
expect(attr.verb).toBe("list"); expect(attr.verb).toBe("list");
return Promise.resolve(true); return Promise.resolve(true);
}) })
.mockImplementation((attr: V1ResourceAttributes): Promise<boolean> => { .mockImplementation((attr: V1ResourceAttributes): Promise<boolean> => {
expect(attr.namespace).toBe("default"); expect(attr.namespace).toBe("default");
expect(attr.verb).toBe("list"); expect(attr.verb).toBe("list");
return Promise.resolve(true); return Promise.resolve(true);
}); });
jest.spyOn(Cluster.prototype, "getProxyKubeconfig").mockReturnValue(mockKC as any); jest.spyOn(Cluster.prototype, "getProxyKubeconfig").mockReturnValue(mockKC as any);
@ -148,6 +153,7 @@ describe("create clusters", () => {
mockedRequest.mockImplementationOnce(((uri: any) => { mockedRequest.mockImplementationOnce(((uri: any) => {
expect(uri).toBe(`http://localhost:${port}/api-kube/version`); expect(uri).toBe(`http://localhost:${port}/api-kube/version`);
return Promise.resolve({ gitVersion: "1.2.3" }); return Promise.resolve({ gitVersion: "1.2.3" });
}) as any); }) as any);
@ -165,6 +171,7 @@ describe("create clusters", () => {
kubeConfigPath: "minikube-config.yml", kubeConfigPath: "minikube-config.yml",
workspace: workspaceStore.currentWorkspaceId workspace: workspaceStore.currentWorkspaceId
}); });
await c.init(port); await c.init(port);
await c.activate(); await c.activate();

View File

@ -49,6 +49,7 @@ describe("kube auth proxy tests", () => {
it("calling exit multiple times shouldn't throw", async () => { it("calling exit multiple times shouldn't throw", async () => {
const port = await getFreePort(); const port = await getFreePort();
const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" }), port, {}); const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" }), port, {});
kap.exit(); kap.exit();
kap.exit(); kap.exit();
kap.exit(); kap.exit();
@ -69,24 +70,29 @@ describe("kube auth proxy tests", () => {
jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValueOnce(Promise.resolve(false)); jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValueOnce(Promise.resolve(false));
mockedCP.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): ChildProcess => { mockedCP.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): ChildProcess => {
listeners[event] = listener; listeners[event] = listener;
return mockedCP; return mockedCP;
}); });
mockedCP.stderr = mock<Readable>(); mockedCP.stderr = mock<Readable>();
mockedCP.stderr.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => { mockedCP.stderr.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => {
listeners[`stderr/${event}`] = listener; listeners[`stderr/${event}`] = listener;
return mockedCP.stderr; return mockedCP.stderr;
}); });
mockedCP.stdout = mock<Readable>(); mockedCP.stdout = mock<Readable>();
mockedCP.stdout.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => { mockedCP.stdout.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => {
listeners[`stdout/${event}`] = listener; listeners[`stdout/${event}`] = listener;
return mockedCP.stdout; return mockedCP.stdout;
}); });
mockSpawn.mockImplementationOnce((command: string): ChildProcess => { mockSpawn.mockImplementationOnce((command: string): ChildProcess => {
expect(command).toBe(bundledKubectlPath()); expect(command).toBe(bundledKubectlPath());
return mockedCP; return mockedCP;
}); });
mockWaitUntilUsed.mockReturnValueOnce(Promise.resolve()); mockWaitUntilUsed.mockReturnValueOnce(Promise.resolve());
const cluster = new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" }); const cluster = new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" });
jest.spyOn(cluster, "apiUrl", "get").mockReturnValue("https://fake.k8s.internal"); jest.spyOn(cluster, "apiUrl", "get").mockReturnValue("https://fake.k8s.internal");
proxy = new KubeAuthProxy(cluster, port, {}); proxy = new KubeAuthProxy(cluster, port, {});
}); });

View File

@ -64,6 +64,7 @@ describe("kubeconfig manager tests", () => {
preferences: {}, preferences: {},
}) })
}; };
mockFs(mockOpts); mockFs(mockOpts);
}); });
@ -86,6 +87,7 @@ describe("kubeconfig manager tests", () => {
expect(kubeConfManager.getPath()).toBe("tmp/kubeconfig-foo"); expect(kubeConfManager.getPath()).toBe("tmp/kubeconfig-foo");
const file = await fse.readFile(kubeConfManager.getPath()); const file = await fse.readFile(kubeConfManager.getPath());
const yml = loadYaml<any>(file.toString()); const yml = loadYaml<any>(file.toString());
expect(yml["current-context"]).toBe("minikube"); expect(yml["current-context"]).toBe("minikube");
expect(yml["clusters"][0]["cluster"]["server"]).toBe(`http://127.0.0.1:${port}/foo`); expect(yml["clusters"][0]["cluster"]["server"]).toBe(`http://127.0.0.1:${port}/foo`);
expect(yml["users"][0]["name"]).toBe("proxy"); expect(yml["users"][0]["name"]).toBe("proxy");
@ -101,8 +103,8 @@ describe("kubeconfig manager tests", () => {
const contextHandler = new ContextHandler(cluster); const contextHandler = new ContextHandler(cluster);
const port = await getFreePort(); const port = await getFreePort();
const kubeConfManager = await KubeconfigManager.create(cluster, contextHandler, port); const kubeConfManager = await KubeconfigManager.create(cluster, contextHandler, port);
const configPath = kubeConfManager.getPath(); const configPath = kubeConfManager.getPath();
expect(await fse.pathExists(configPath)).toBe(true); expect(await fse.pathExists(configPath)).toBe(true);
await kubeConfManager.unlink(); await kubeConfManager.unlink();
expect(await fse.pathExists(configPath)).toBe(false); expect(await fse.pathExists(configPath)).toBe(false);

View File

@ -14,6 +14,7 @@ export class AppUpdater {
public start() { public start() {
setInterval(AppUpdater.checkForUpdates, this.updateInterval); setInterval(AppUpdater.checkForUpdates, this.updateInterval);
return AppUpdater.checkForUpdates(); return AppUpdater.checkForUpdates();
} }
} }

View File

@ -20,6 +20,7 @@ export class BaseClusterDetector {
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; const apiUrl = this.cluster.kubeProxyUrl + path;
return request(apiUrl, { return request(apiUrl, {
json: true, json: true,
timeout: 30000, timeout: 30000,

View File

@ -7,17 +7,20 @@ export class ClusterIdDetector extends BaseClusterDetector {
public async detect() { public async detect() {
let id: string; let id: string;
try { try {
id = await this.getDefaultNamespaceId(); id = await this.getDefaultNamespaceId();
} catch(_) { } catch(_) {
id = this.cluster.apiUrl; id = this.cluster.apiUrl;
} }
const value = createHash("sha256").update(id).digest("hex"); const value = createHash("sha256").update(id).digest("hex");
return { value, accuracy: 100 }; return { value, accuracy: 100 };
} }
protected async getDefaultNamespaceId() { protected async getDefaultNamespaceId() {
const response = await this.k8sRequest("/api/v1/namespaces/default"); const response = await this.k8sRequest("/api/v1/namespaces/default");
return response.metadata.uid; return response.metadata.uid;
} }
} }

View File

@ -17,12 +17,16 @@ export class DetectorRegistry {
async detectForCluster(cluster: Cluster): Promise<ClusterMetadata> { async detectForCluster(cluster: Cluster): Promise<ClusterMetadata> {
const results: {[key: string]: ClusterDetectionResult } = {}; const results: {[key: string]: ClusterDetectionResult } = {};
for (const detectorClass of this.registry) { for (const detectorClass of this.registry) {
const detector = new detectorClass(cluster); const detector = new detectorClass(cluster);
try { try {
const data = await detector.detect(); const data = await detector.detect();
if (!data) continue; if (!data) continue;
const existingValue = results[detector.key]; const existingValue = results[detector.key];
if (existingValue && existingValue.accuracy > data.accuracy) continue; // previous value exists and is more accurate if (existingValue && existingValue.accuracy > data.accuracy) continue; // previous value exists and is more accurate
results[detector.key] = data; results[detector.key] = data;
} catch (e) { } catch (e) {
@ -30,9 +34,11 @@ export class DetectorRegistry {
} }
} }
const metadata: ClusterMetadata = {}; const metadata: ClusterMetadata = {};
for (const [key, result] of Object.entries(results)) { for (const [key, result] of Object.entries(results)) {
metadata[key] = result.value; metadata[key] = result.value;
} }
return metadata; return metadata;
} }
} }

View File

@ -7,30 +7,39 @@ export class DistributionDetector extends BaseClusterDetector {
public async detect() { public async detect() {
this.version = await this.getKubernetesVersion(); this.version = await this.getKubernetesVersion();
if (await this.isRancher()) { if (await this.isRancher()) {
return { value: "rancher", accuracy: 80}; return { value: "rancher", accuracy: 80};
} }
if (this.isGKE()) { if (this.isGKE()) {
return { value: "gke", accuracy: 80}; return { value: "gke", accuracy: 80};
} }
if (this.isEKS()) { if (this.isEKS()) {
return { value: "eks", accuracy: 80}; return { value: "eks", accuracy: 80};
} }
if (this.isIKS()) { if (this.isIKS()) {
return { value: "iks", accuracy: 80}; return { value: "iks", accuracy: 80};
} }
if (this.isAKS()) { if (this.isAKS()) {
return { value: "aks", accuracy: 80}; return { value: "aks", accuracy: 80};
} }
if (this.isDigitalOcean()) { if (this.isDigitalOcean()) {
return { value: "digitalocean", accuracy: 90}; return { value: "digitalocean", accuracy: 90};
} }
if (this.isMinikube()) { if (this.isMinikube()) {
return { value: "minikube", accuracy: 80}; return { value: "minikube", accuracy: 80};
} }
if (this.isCustom()) { if (this.isCustom()) {
return { value: "custom", accuracy: 10}; return { value: "custom", accuracy: 10};
} }
return { value: "unknown", accuracy: 10}; return { value: "unknown", accuracy: 10};
} }
@ -38,6 +47,7 @@ export class DistributionDetector extends BaseClusterDetector {
if (this.cluster.version) return this.cluster.version; if (this.cluster.version) return this.cluster.version;
const response = await this.k8sRequest("/version"); const response = await this.k8sRequest("/version");
return response.gitVersion; return response.gitVersion;
} }
@ -72,6 +82,7 @@ export class DistributionDetector extends BaseClusterDetector {
protected async isRancher() { protected async isRancher() {
try { try {
const response = await this.k8sRequest(""); const response = await this.k8sRequest("");
return response.data.find((api: any) => api?.apiVersion?.group === "meta.cattle.io") !== undefined; return response.data.find((api: any) => api?.apiVersion?.group === "meta.cattle.io") !== undefined;
} catch (e) { } catch (e) {
return false; return false;

View File

@ -8,6 +8,7 @@ export class LastSeenDetector extends BaseClusterDetector {
if (!this.cluster.accessible) return null; if (!this.cluster.accessible) return null;
await this.k8sRequest("/version"); await this.k8sRequest("/version");
return { value: new Date().toJSON(), accuracy: 100 }; return { value: new Date().toJSON(), accuracy: 100 };
} }
} }

View File

@ -7,11 +7,13 @@ export class NodesCountDetector extends BaseClusterDetector {
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};
} }
protected async getNodeCount(): Promise<number> { protected async getNodeCount(): Promise<number> {
const response = await this.k8sRequest("/api/v1/nodes"); const response = await this.k8sRequest("/api/v1/nodes");
return response.items.length; return response.items.length;
} }
} }

View File

@ -7,11 +7,13 @@ export class VersionDetector extends BaseClusterDetector {
public async detect() { public async detect() {
const version = await this.getKubernetesVersion(); const version = await this.getKubernetesVersion();
return { value: version, accuracy: 100}; return { value: version, accuracy: 100};
} }
public async getKubernetesVersion() { public async getKubernetesVersion() {
const response = await this.k8sRequest("/version"); const response = await this.k8sRequest("/version");
return response.gitVersion; return response.gitVersion;
} }
} }

View File

@ -24,8 +24,10 @@ export class ClusterManager extends Singleton {
// auto-stop removed clusters // auto-stop removed clusters
autorun(() => { autorun(() => {
const removedClusters = Array.from(clusterStore.removedClusters.values()); const removedClusters = Array.from(clusterStore.removedClusters.values());
if (removedClusters.length > 0) { if (removedClusters.length > 0) {
const meta = removedClusters.map(cluster => cluster.getMeta()); const meta = removedClusters.map(cluster => cluster.getMeta());
logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta); logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta);
removedClusters.forEach(cluster => cluster.disconnect()); removedClusters.forEach(cluster => cluster.disconnect());
clusterStore.removedClusters.clear(); clusterStore.removedClusters.clear();
@ -70,7 +72,9 @@ export class ClusterManager extends Singleton {
// 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);
@ -79,6 +83,7 @@ export class ClusterManager extends Singleton {
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);
} }

View File

@ -91,6 +91,7 @@ export class Cluster implements ClusterModel, ClusterState {
@computed get prometheusPreferences(): ClusterPrometheusPreferences { @computed get prometheusPreferences(): ClusterPrometheusPreferences {
const { prometheus, prometheusProvider } = this.preferences; const { prometheus, prometheusProvider } = this.preferences;
return toJS({ prometheus, prometheusProvider }, { return toJS({ prometheus, prometheusProvider }, {
recurseEverything: true, recurseEverything: true,
}); });
@ -103,6 +104,7 @@ export class Cluster implements ClusterModel, ClusterState {
constructor(model: ClusterModel) { constructor(model: ClusterModel) {
this.updateModel(model); this.updateModel(model);
const kubeconfig = this.getKubeconfig(); const kubeconfig = this.getKubeconfig();
if (kubeconfig.getContextObject(this.contextName)) { if (kubeconfig.getContextObject(this.contextName)) {
this.apiUrl = kubeconfig.getCluster(kubeconfig.getContextObject(this.contextName).cluster).server; this.apiUrl = kubeconfig.getCluster(kubeconfig.getContextObject(this.contextName).cluster).server;
} }
@ -167,13 +169,16 @@ export class Cluster implements ClusterModel, ClusterState {
} }
logger.info(`[CLUSTER]: activate`, this.getMeta()); logger.info(`[CLUSTER]: activate`, this.getMeta());
await this.whenInitialized; await this.whenInitialized;
if (!this.eventDisposers.length) { if (!this.eventDisposers.length) {
this.bindEvents(); this.bindEvents();
} }
if (this.disconnected || !this.accessible) { if (this.disconnected || !this.accessible) {
await this.reconnect(); await this.reconnect();
} }
await this.refreshConnectionStatus(); await this.refreshConnectionStatus();
if (this.accessible) { if (this.accessible) {
await this.refreshAllowedResources(); await this.refreshAllowedResources();
this.isAdmin = await this.isClusterAdmin(); this.isAdmin = await this.isClusterAdmin();
@ -181,11 +186,13 @@ export class Cluster implements ClusterModel, ClusterState {
this.ensureKubectl(); this.ensureKubectl();
} }
this.activated = true; this.activated = true;
return this.pushState(); return this.pushState();
} }
protected async ensureKubectl() { protected async ensureKubectl() {
this.kubeCtl = new Kubectl(this.version); this.kubeCtl = new Kubectl(this.version);
return this.kubeCtl.ensureKubectl(); // download kubectl in background, so it's not blocking dashboard return this.kubeCtl.ensureKubectl(); // download kubectl in background, so it's not blocking dashboard
} }
@ -215,9 +222,11 @@ export class Cluster implements ClusterModel, ClusterState {
logger.info(`[CLUSTER]: refresh`, this.getMeta()); logger.info(`[CLUSTER]: refresh`, this.getMeta());
await this.whenInitialized; await this.whenInitialized;
await this.refreshConnectionStatus(); await this.refreshConnectionStatus();
if (this.accessible) { if (this.accessible) {
this.isAdmin = await this.isClusterAdmin(); this.isAdmin = await this.isClusterAdmin();
await this.refreshAllowedResources(); await this.refreshAllowedResources();
if (opts.refreshMetadata) { if (opts.refreshMetadata) {
this.refreshMetadata(); this.refreshMetadata();
} }
@ -231,12 +240,14 @@ export class Cluster implements ClusterModel, ClusterState {
logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta()); logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta());
const metadata = await detectorRegistry.detectForCluster(this); const metadata = await detectorRegistry.detectForCluster(this);
const existingMetadata = this.metadata; const existingMetadata = this.metadata;
this.metadata = Object.assign(existingMetadata, metadata); this.metadata = Object.assign(existingMetadata, metadata);
} }
@action @action
async refreshConnectionStatus() { async refreshConnectionStatus() {
const connectionStatus = await this.getConnectionStatus(); const connectionStatus = await this.getConnectionStatus();
this.online = connectionStatus > ClusterStatus.Offline; this.online = connectionStatus > ClusterStatus.Offline;
this.accessible = connectionStatus == ClusterStatus.AccessGranted; this.accessible = connectionStatus == ClusterStatus.AccessGranted;
} }
@ -271,6 +282,7 @@ export class Cluster implements ClusterModel, ClusterState {
getMetrics(prometheusPath: string, queryParams: IMetricsReqParams & { query: string }) { getMetrics(prometheusPath: string, queryParams: IMetricsReqParams & { query: string }) {
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,
@ -283,43 +295,54 @@ export class Cluster implements ClusterModel, ClusterState {
try { try {
const versionDetector = new VersionDetector(this); const versionDetector = new VersionDetector(this);
const versionData = await versionDetector.detect(); const versionData = await versionDetector.detect();
this.metadata.version = versionData.value; this.metadata.version = versionData.value;
return ClusterStatus.AccessGranted; return ClusterStatus.AccessGranted;
} catch (error) { } catch (error) {
logger.error(`Failed to connect cluster "${this.contextName}": ${error}`); logger.error(`Failed to connect cluster "${this.contextName}": ${error}`);
if (error.statusCode) { if (error.statusCode) {
if (error.statusCode >= 400 && error.statusCode < 500) { if (error.statusCode >= 400 && error.statusCode < 500) {
this.failureReason = "Invalid credentials"; this.failureReason = "Invalid credentials";
return ClusterStatus.AccessDenied; return ClusterStatus.AccessDenied;
} else { } else {
this.failureReason = error.error || error.message; this.failureReason = error.error || error.message;
return ClusterStatus.Offline; return ClusterStatus.Offline;
} }
} else if (error.failed === true) { } else if (error.failed === true) {
if (error.timedOut === true) { if (error.timedOut === true) {
this.failureReason = "Connection timed out"; this.failureReason = "Connection timed out";
return ClusterStatus.Offline; return ClusterStatus.Offline;
} else { } else {
this.failureReason = "Failed to fetch credentials"; this.failureReason = "Failed to fetch credentials";
return ClusterStatus.AccessDenied; return ClusterStatus.AccessDenied;
} }
} }
this.failureReason = error.message; this.failureReason = error.message;
return ClusterStatus.Offline; return ClusterStatus.Offline;
} }
} }
async canI(resourceAttributes: V1ResourceAttributes): Promise<boolean> { async canI(resourceAttributes: V1ResourceAttributes): Promise<boolean> {
const authApi = this.getProxyKubeconfig().makeApiClient(AuthorizationV1Api); const authApi = this.getProxyKubeconfig().makeApiClient(AuthorizationV1Api);
try { try {
const accessReview = await authApi.createSelfSubjectAccessReview({ const accessReview = await authApi.createSelfSubjectAccessReview({
apiVersion: "authorization.k8s.io/v1", apiVersion: "authorization.k8s.io/v1",
kind: "SelfSubjectAccessReview", kind: "SelfSubjectAccessReview",
spec: { resourceAttributes } spec: { resourceAttributes }
}); });
return accessReview.body.status.allowed; return accessReview.body.status.allowed;
} catch (error) { } catch (error) {
logger.error(`failed to request selfSubjectAccessReview: ${error}`); logger.error(`failed to request selfSubjectAccessReview: ${error}`);
return false; return false;
} }
} }
@ -343,6 +366,7 @@ export class Cluster implements ClusterModel, ClusterState {
ownerRef: this.ownerRef, ownerRef: this.ownerRef,
accessibleNamespaces: this.accessibleNamespaces, accessibleNamespaces: this.accessibleNamespaces,
}; };
return toJS(model, { return toJS(model, {
recurseEverything: true recurseEverything: true
}); });
@ -363,6 +387,7 @@ export class Cluster implements ClusterModel, ClusterState {
allowedNamespaces: this.allowedNamespaces, allowedNamespaces: this.allowedNamespaces,
allowedResources: this.allowedResources, allowedResources: this.allowedResources,
}; };
return toJS(state, { return toJS(state, {
recurseEverything: true recurseEverything: true
}); });
@ -397,6 +422,7 @@ export class Cluster implements ClusterModel, ClusterState {
} }
const api = this.getProxyKubeconfig().makeApiClient(CoreV1Api); const api = this.getProxyKubeconfig().makeApiClient(CoreV1Api);
try { try {
const namespaceList = await api.listNamespace(); const namespaceList = await api.listNamespace();
const nsAccessStatuses = await Promise.all( const nsAccessStatuses = await Promise.all(
@ -406,12 +432,15 @@ export class Cluster implements ClusterModel, ClusterState {
verb: "list", verb: "list",
})) }))
); );
return namespaceList.body.items return namespaceList.body.items
.filter((ns, i) => nsAccessStatuses[i]) .filter((ns, i) => nsAccessStatuses[i])
.map(ns => ns.metadata.name); .map(ns => ns.metadata.name);
} catch (error) { } catch (error) {
const ctx = this.getProxyKubeconfig().getContextObject(this.contextName); const ctx = this.getProxyKubeconfig().getContextObject(this.contextName);
if (ctx.namespace) return [ctx.namespace]; if (ctx.namespace) return [ctx.namespace];
return []; return [];
} }
} }
@ -429,6 +458,7 @@ export class Cluster implements ClusterModel, ClusterState {
namespace: this.allowedNamespaces[0] namespace: this.allowedNamespaces[0]
})) }))
); );
return apiResources return apiResources
.filter((resource, i) => resourceAccessStatuses[i]) .filter((resource, i) => resourceAccessStatuses[i])
.map(apiResource => apiResource.resource); .map(apiResource => apiResource.resource);

View File

@ -25,28 +25,34 @@ export class ContextHandler {
public setupPrometheus(preferences: ClusterPrometheusPreferences = {}) { public setupPrometheus(preferences: ClusterPrometheusPreferences = {}) {
this.prometheusProvider = preferences.prometheusProvider?.type; this.prometheusProvider = preferences.prometheusProvider?.type;
this.prometheusPath = null; 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}`;
} }
} }
protected async resolvePrometheusPath(): Promise<string> { protected async resolvePrometheusPath(): Promise<string> {
const prometheusService = await this.getPrometheusService(); const prometheusService = await this.getPrometheusService();
if (!prometheusService) return null; if (!prometheusService) return null;
const { service, namespace, port } = prometheusService; const { service, namespace, port } = prometheusService;
return `${namespace}/services/${service}:${port}`; return `${namespace}/services/${service}:${port}`;
} }
async getPrometheusProvider() { async getPrometheusProvider() {
if (!this.prometheusProvider) { if (!this.prometheusProvider) {
const service = await this.getPrometheusService(); const service = await this.getPrometheusService();
if (!service) { if (!service) {
return null; return null;
} }
logger.info(`using ${service.id} as prometheus provider`); logger.info(`using ${service.id} as prometheus provider`);
this.prometheusProvider = service.id; this.prometheusProvider = service.id;
} }
return prometheusProviders.find(p => p.id === this.prometheusProvider); return prometheusProviders.find(p => p.id === this.prometheusProvider);
} }
@ -54,9 +60,11 @@ export class ContextHandler {
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 prometheusPromises: Promise<PrometheusService>[] = providers.map(async (provider: PrometheusProvider): Promise<PrometheusService> => {
const apiClient = this.cluster.getProxyKubeconfig().makeApiClient(CoreV1Api); const apiClient = this.cluster.getProxyKubeconfig().makeApiClient(CoreV1Api);
return await provider.getPrometheusService(apiClient); return await provider.getPrometheusService(apiClient);
}); });
const resolvedPrometheusServices = await Promise.all(prometheusPromises); const resolvedPrometheusServices = await Promise.all(prometheusPromises);
return resolvedPrometheusServices.filter(n => n)[0]; return resolvedPrometheusServices.filter(n => n)[0];
} }
@ -64,12 +72,14 @@ export class ContextHandler {
if (!this.prometheusPath) { if (!this.prometheusPath) {
this.prometheusPath = await this.resolvePrometheusPath(); this.prometheusPath = await this.resolvePrometheusPath();
} }
return this.prometheusPath; return this.prometheusPath;
} }
async resolveAuthProxyUrl() { async resolveAuthProxyUrl() {
const proxyPort = await this.ensurePort(); const proxyPort = await this.ensurePort();
const path = this.clusterUrl.path !== "/" ? this.clusterUrl.path : ""; const path = this.clusterUrl.path !== "/" ? this.clusterUrl.path : "";
return `http://127.0.0.1:${proxyPort}${path}`; return `http://127.0.0.1:${proxyPort}${path}`;
} }
@ -79,14 +89,17 @@ export class ContextHandler {
} }
const timeout = isWatchRequest ? 4 * 60 * 60 * 1000 : 30000; // 4 hours for watch request, 30 seconds for the rest const timeout = isWatchRequest ? 4 * 60 * 60 * 1000 : 30000; // 4 hours for watch request, 30 seconds for the rest
const apiTarget = await this.newApiTarget(timeout); const apiTarget = await this.newApiTarget(timeout);
if (!isWatchRequest) { if (!isWatchRequest) {
this.apiTarget = apiTarget; this.apiTarget = apiTarget;
} }
return apiTarget; return apiTarget;
} }
protected async newApiTarget(timeout: number): Promise<httpProxy.ServerOptions> { protected async newApiTarget(timeout: number): Promise<httpProxy.ServerOptions> {
const proxyUrl = await this.resolveAuthProxyUrl(); const proxyUrl = await this.resolveAuthProxyUrl();
return { return {
target: proxyUrl, target: proxyUrl,
changeOrigin: true, changeOrigin: true,
@ -101,6 +114,7 @@ export class ContextHandler {
if (!this.proxyPort) { if (!this.proxyPort) {
this.proxyPort = await getFreePort(); this.proxyPort = await getFreePort();
} }
return this.proxyPort; return this.proxyPort;
} }
@ -108,6 +122,7 @@ export class ContextHandler {
if (!this.kubeAuthProxy) { if (!this.kubeAuthProxy) {
await this.ensurePort(); await this.ensurePort();
const proxyEnv = Object.assign({}, process.env); const proxyEnv = Object.assign({}, process.env);
if (this.cluster.preferences.httpsProxy) { if (this.cluster.preferences.httpsProxy) {
proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy; proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy;
} }

View File

@ -8,6 +8,7 @@ import logger from "./logger";
export function exitApp() { export function exitApp() {
const windowManager = WindowManager.getInstance<WindowManager>(); const windowManager = WindowManager.getInstance<WindowManager>();
const clusterManager = ClusterManager.getInstance<ClusterManager>(); const clusterManager = ClusterManager.getInstance<ClusterManager>();
appEventBus.emit({ name: "service", action: "close" }); appEventBus.emit({ name: "service", action: "close" });
windowManager.hide(); windowManager.hide();
clusterManager.stop(); clusterManager.stop();

View File

@ -32,11 +32,14 @@ export class FilesystemProvisionerStore extends BaseStore<FSProvisionModel> {
const salt = randomBytes(32).toString("hex"); const salt = randomBytes(32).toString("hex");
const hashedName = SHA256(`${extensionName}/${salt}`).toString(); const hashedName = SHA256(`${extensionName}/${salt}`).toString();
const dirPath = path.resolve(app.getPath("userData"), "extension_data", hashedName); const dirPath = path.resolve(app.getPath("userData"), "extension_data", hashedName);
this.registeredExtensions.set(extensionName, dirPath); this.registeredExtensions.set(extensionName, dirPath);
} }
const dirPath = this.registeredExtensions.get(extensionName); const dirPath = this.registeredExtensions.get(extensionName);
await fse.ensureDir(dirPath); await fse.ensureDir(dirPath);
return dirPath; return dirPath;
} }

View File

@ -20,32 +20,39 @@ export class HelmChartManager {
public async chart(name: string) { public async chart(name: string) {
const charts = await this.charts(); const charts = await this.charts();
return charts[name]; return charts[name];
} }
public async charts(): Promise<any> { public async charts(): Promise<any> {
try { try {
const cachedYaml = await this.cachedYaml(); const cachedYaml = await this.cachedYaml();
return cachedYaml["entries"]; return cachedYaml["entries"];
} catch(error) { } catch(error) {
logger.error(error); logger.error(error);
return []; return [];
} }
} }
public async getReadme(name: string, version = "") { public async getReadme(name: string, version = "") {
const helm = await helmCli.binaryPath(); const helm = await helmCli.binaryPath();
if(version && version != "") { if(version && version != "") {
const { stdout } = await promiseExec(`"${helm}" show readme ${this.repo.name}/${name} --version ${version}`).catch((error) => { throw(error.stderr);}); const { stdout } = await promiseExec(`"${helm}" show readme ${this.repo.name}/${name} --version ${version}`).catch((error) => { throw(error.stderr);});
return stdout; return stdout;
} else { } else {
const { stdout } = await promiseExec(`"${helm}" show readme ${this.repo.name}/${name}`).catch((error) => { throw(error.stderr);}); const { stdout } = await promiseExec(`"${helm}" show readme ${this.repo.name}/${name}`).catch((error) => { throw(error.stderr);});
return stdout; return stdout;
} }
} }
public async getValues(name: string, version = "") { public async getValues(name: string, version = "") {
const helm = await helmCli.binaryPath(); const helm = await helmCli.binaryPath();
if(version && version != "") { if(version && version != "") {
const { stdout } = await promiseExec(`"${helm}" show values ${this.repo.name}/${name} --version ${version}`).catch((error) => { throw(error.stderr);}); const { stdout } = await promiseExec(`"${helm}" show values ${this.repo.name}/${name} --version ${version}`).catch((error) => { throw(error.stderr);});
@ -61,6 +68,7 @@ export class HelmChartManager {
if (!(this.repo.name in this.cache)) { if (!(this.repo.name in this.cache)) {
const cacheFile = await fs.promises.readFile(this.repo.cacheFilePath, "utf-8"); const cacheFile = await fs.promises.readFile(this.repo.cacheFilePath, "utf-8");
const data = yaml.safeLoad(cacheFile); const data = yaml.safeLoad(cacheFile);
for(const key in data["entries"]) { for(const key in data["entries"]) {
data["entries"][key].forEach((version: any) => { data["entries"][key].forEach((version: any) => {
version["repo"] = this.repo.name; version["repo"] = this.repo.name;
@ -69,6 +77,7 @@ export class HelmChartManager {
} }
this.cache[this.repo.name] = Buffer.from(JSON.stringify(data)); this.cache[this.repo.name] = Buffer.from(JSON.stringify(data));
} }
return JSON.parse(this.cache[this.repo.name].toString()); return JSON.parse(this.cache[this.repo.name].toString());
} }
} }

View File

@ -12,6 +12,7 @@ export class HelmCli extends LensBinary {
originalBinaryName: "helm", originalBinaryName: "helm",
newBinaryName: "helm3" newBinaryName: "helm3"
}; };
super(opts); super(opts);
} }

View File

@ -12,14 +12,15 @@ export class HelmReleaseManager {
const helm = await helmCli.binaryPath(); const helm = await helmCli.binaryPath();
const namespaceFlag = namespace ? `-n ${namespace}` : "--all-namespaces"; const namespaceFlag = namespace ? `-n ${namespace}` : "--all-namespaces";
const { stdout } = await promiseExec(`"${helm}" ls --output json ${namespaceFlag} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);}); const { stdout } = await promiseExec(`"${helm}" ls --output json ${namespaceFlag} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
const output = JSON.parse(stdout); const output = JSON.parse(stdout);
if (output.length == 0) { if (output.length == 0) {
return output; return output;
} }
output.forEach((release: any, index: number) => { output.forEach((release: any, index: number) => {
output[index] = toCamelCase(release); output[index] = toCamelCase(release);
}); });
return output; return output;
} }
@ -27,15 +28,19 @@ export class HelmReleaseManager {
public async installChart(chart: string, values: any, name: string, namespace: string, version: string, pathToKubeconfig: string){ public async installChart(chart: string, values: any, name: string, namespace: string, version: string, pathToKubeconfig: string){
const helm = await helmCli.binaryPath(); const helm = await helmCli.binaryPath();
const fileName = tempy.file({name: "values.yaml"}); const fileName = tempy.file({name: "values.yaml"});
await fs.promises.writeFile(fileName, yaml.safeDump(values)); await fs.promises.writeFile(fileName, yaml.safeDump(values));
try { try {
let generateName = ""; let generateName = "";
if (!name) { if (!name) {
generateName = "--generate-name"; generateName = "--generate-name";
name = ""; name = "";
} }
const { stdout } = await promiseExec(`"${helm}" install ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} ${generateName}`).catch((error) => { throw(error.stderr);}); const { stdout } = await promiseExec(`"${helm}" install ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} ${generateName}`).catch((error) => { throw(error.stderr);});
const releaseName = stdout.split("\n")[0].split(" ")[1].trim(); const releaseName = stdout.split("\n")[0].split(" ")[1].trim();
return { return {
log: stdout, log: stdout,
release: { release: {
@ -51,10 +56,12 @@ export class HelmReleaseManager {
public async upgradeRelease(name: string, chart: string, values: any, namespace: string, version: string, cluster: Cluster){ public async upgradeRelease(name: string, chart: string, values: any, namespace: string, version: string, cluster: Cluster){
const helm = await helmCli.binaryPath(); const helm = await helmCli.binaryPath();
const fileName = tempy.file({name: "values.yaml"}); const fileName = tempy.file({name: "values.yaml"});
await fs.promises.writeFile(fileName, yaml.safeDump(values)); await fs.promises.writeFile(fileName, yaml.safeDump(values));
try { try {
const { stdout } = await promiseExec(`"${helm}" upgrade ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${cluster.getProxyKubeconfigPath()}`).catch((error) => { throw(error.stderr);}); const { stdout } = await promiseExec(`"${helm}" upgrade ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${cluster.getProxyKubeconfigPath()}`).catch((error) => { throw(error.stderr);});
return { return {
log: stdout, log: stdout,
release: this.getRelease(name, namespace, cluster) release: this.getRelease(name, namespace, cluster)
@ -68,7 +75,9 @@ export class HelmReleaseManager {
const helm = await helmCli.binaryPath(); const helm = await helmCli.binaryPath();
const { stdout } = await promiseExec(`"${helm}" status ${name} --output json --namespace ${namespace} --kubeconfig ${cluster.getProxyKubeconfigPath()}`).catch((error) => { throw(error.stderr);}); const { stdout } = await promiseExec(`"${helm}" status ${name} --output json --namespace ${namespace} --kubeconfig ${cluster.getProxyKubeconfigPath()}`).catch((error) => { throw(error.stderr);});
const release = JSON.parse(stdout); const release = JSON.parse(stdout);
release.resources = await this.getResources(name, namespace, cluster); release.resources = await this.getResources(name, namespace, cluster);
return release; return release;
} }
@ -82,18 +91,21 @@ export class HelmReleaseManager {
public async getValues(name: string, namespace: string, pathToKubeconfig: string) { public async getValues(name: string, namespace: string, pathToKubeconfig: string) {
const helm = await helmCli.binaryPath(); const helm = await helmCli.binaryPath();
const { stdout, } = await promiseExec(`"${helm}" get values ${name} --all --output yaml --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);}); const { stdout, } = await promiseExec(`"${helm}" get values ${name} --all --output yaml --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
return stdout; return stdout;
} }
public async getHistory(name: string, namespace: string, pathToKubeconfig: string) { public async getHistory(name: string, namespace: string, pathToKubeconfig: string) {
const helm = await helmCli.binaryPath(); const helm = await helmCli.binaryPath();
const { stdout } = await promiseExec(`"${helm}" history ${name} --output json --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);}); const { stdout } = await promiseExec(`"${helm}" history ${name} --output json --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
return JSON.parse(stdout); return JSON.parse(stdout);
} }
public async rollback(name: string, namespace: string, revision: number, pathToKubeconfig: string) { public async rollback(name: string, namespace: string, revision: number, pathToKubeconfig: string) {
const helm = await helmCli.binaryPath(); const helm = await helmCli.binaryPath();
const { stdout } = await promiseExec(`"${helm}" rollback ${name} ${revision} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);}); const { stdout } = await promiseExec(`"${helm}" rollback ${name} ${revision} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
return stdout; return stdout;
} }
@ -104,6 +116,7 @@ export class HelmReleaseManager {
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: []})};
}); });
return stdout; return stdout;
} }
} }

View File

@ -42,12 +42,14 @@ export class HelmRepoManager extends Singleton {
resolveWithFullResponse: true, resolveWithFullResponse: true,
timeout: 10000, timeout: 10000,
}); });
return orderBy<HelmRepo>(res.body, repo => repo.name); return orderBy<HelmRepo>(res.body, repo => repo.name);
} }
async init() { async init() {
helmCli.setLogger(logger); helmCli.setLogger(logger);
await helmCli.ensureBinary(); await helmCli.ensureBinary();
if (!this.initialized) { if (!this.initialized) {
this.helmEnv = await this.parseHelmEnv(); this.helmEnv = await this.parseHelmEnv();
await this.update(); await this.update();
@ -62,12 +64,15 @@ export class HelmRepoManager extends Singleton {
}); });
const lines = stdout.split(/\r?\n/); // split by new line feed const lines = stdout.split(/\r?\n/); // split by new line feed
const env: HelmEnv = {}; const env: HelmEnv = {};
lines.forEach((line: string) => { lines.forEach((line: string) => {
const [key, value] = line.split("="); const [key, value] = line.split("=");
if (key && value) { if (key && value) {
env[key] = value.replace(/"/g, ""); // strip quotas env[key] = value.replace(/"/g, ""); // strip quotas
} }
}); });
return env; return env;
} }
@ -75,6 +80,7 @@ export class HelmRepoManager extends Singleton {
if (!this.initialized) { if (!this.initialized) {
await this.init(); await this.init();
} }
try { try {
const repoConfigFile = this.helmEnv.HELM_REPOSITORY_CONFIG; const repoConfigFile = this.helmEnv.HELM_REPOSITORY_CONFIG;
const { repositories }: HelmRepoConfig = await readFile(repoConfigFile, "utf8") const { repositories }: HelmRepoConfig = await readFile(repoConfigFile, "utf8")
@ -82,22 +88,27 @@ export class HelmRepoManager extends Singleton {
.catch(() => ({ .catch(() => ({
repositories: [] repositories: []
})); }));
if (!repositories.length) { if (!repositories.length) {
await this.addRepo({ name: "bitnami", url: "https://charts.bitnami.com/bitnami" }); await this.addRepo({ name: "bitnami", url: "https://charts.bitnami.com/bitnami" });
return await this.repositories(); return await this.repositories();
} }
return repositories.map(repo => ({ return repositories.map(repo => ({
...repo, ...repo,
cacheFilePath: `${this.helmEnv.HELM_REPOSITORY_CACHE}/${repo.name}-index.yaml` cacheFilePath: `${this.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}"`);
return []; return [];
} }
} }
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);
} }
@ -106,6 +117,7 @@ export class HelmRepoManager extends Singleton {
const { stdout } = await promiseExec(`"${helm}" repo update`).catch((error) => { const { stdout } = await promiseExec(`"${helm}" repo update`).catch((error) => {
return { stdout: error.stdout }; return { stdout: error.stdout };
}); });
return stdout; return stdout;
} }
@ -115,6 +127,7 @@ export class HelmRepoManager extends Singleton {
const { stdout } = await promiseExec(`"${helm}" repo add ${name} ${url}`).catch((error) => { const { stdout } = await promiseExec(`"${helm}" repo add ${name} ${url}`).catch((error) => {
throw(error.stderr); throw(error.stderr);
}); });
return stdout; return stdout;
} }
@ -124,6 +137,7 @@ export class HelmRepoManager extends Singleton {
const { stdout } = await promiseExec(`"${helm}" repo remove ${name}`).catch((error) => { const { stdout } = await promiseExec(`"${helm}" repo remove ${name}`).catch((error) => {
throw(error.stderr); throw(error.stderr);
}); });
return stdout; return stdout;
} }
} }

View File

@ -11,18 +11,23 @@ class HelmService {
public async listCharts() { public async listCharts() {
const charts: any = {}; const charts: any = {};
await repoManager.init(); await repoManager.init();
const repositories = await repoManager.repositories(); const repositories = await repoManager.repositories();
for (const repo of repositories) { for (const repo of repositories) {
charts[repo.name] = {}; charts[repo.name] = {};
const manager = new HelmChartManager(repo); const manager = new HelmChartManager(repo);
let entries = await manager.charts(); let entries = await manager.charts();
entries = this.excludeDeprecated(entries); entries = this.excludeDeprecated(entries);
for (const key in entries) { for (const key in entries) {
entries[key] = entries[key][0]; entries[key] = entries[key][0];
} }
charts[repo.name] = entries; charts[repo.name] = entries;
} }
return charts; return charts;
} }
@ -34,50 +39,60 @@ class HelmService {
const repo = await repoManager.repository(repoName); const repo = await repoManager.repository(repoName);
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); 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 = "") {
const repo = await repoManager.repository(repoName); const repo = await repoManager.repository(repoName);
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 = null) {
await repoManager.init(); await repoManager.init();
return await releaseManager.listReleases(cluster.getProxyKubeconfigPath(), namespace); return await releaseManager.listReleases(cluster.getProxyKubeconfigPath(), namespace);
} }
public async getRelease(cluster: Cluster, releaseName: string, namespace: string) { public async getRelease(cluster: Cluster, releaseName: string, namespace: string) {
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) {
logger.debug("Fetch release values"); logger.debug("Fetch release values");
return await releaseManager.getValues(releaseName, namespace, cluster.getProxyKubeconfigPath()); return await releaseManager.getValues(releaseName, namespace, cluster.getProxyKubeconfigPath());
} }
public async getReleaseHistory(cluster: Cluster, releaseName: string, namespace: string) { public async getReleaseHistory(cluster: Cluster, releaseName: string, namespace: string) {
logger.debug("Fetch release history"); logger.debug("Fetch release history");
return await releaseManager.getHistory(releaseName, namespace, cluster.getProxyKubeconfigPath()); return await releaseManager.getHistory(releaseName, namespace, cluster.getProxyKubeconfigPath());
} }
public async deleteRelease(cluster: Cluster, releaseName: string, namespace: string) { public async deleteRelease(cluster: Cluster, releaseName: string, namespace: string) {
logger.debug("Delete release"); logger.debug("Delete release");
return await releaseManager.deleteRelease(releaseName, namespace, cluster.getProxyKubeconfigPath()); return await releaseManager.deleteRelease(releaseName, namespace, cluster.getProxyKubeconfigPath());
} }
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 }) {
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) {
logger.debug("Rollback release"); logger.debug("Rollback release");
const output = await releaseManager.rollback(releaseName, namespace, revision, cluster.getProxyKubeconfigPath()); const output = await releaseManager.rollback(releaseName, namespace, revision, cluster.getProxyKubeconfigPath());
return { message: output }; return { message: output };
} }
@ -87,9 +102,11 @@ class HelmService {
if (Array.isArray(entry)) { if (Array.isArray(entry)) {
return entry[0]["deprecated"] != true; return entry[0]["deprecated"] != true;
} }
return entry["deprecated"] != true; return entry["deprecated"] != true;
}); });
} }
return entries; return entries;
} }

View File

@ -34,11 +34,13 @@ let clusterManager: ClusterManager;
let windowManager: WindowManager; let windowManager: WindowManager;
app.setName(appName); app.setName(appName);
if (!process.env.CICD) { if (!process.env.CICD) {
app.setPath("userData", workingDir); app.setPath("userData", workingDir);
} }
mangleProxyEnv(); mangleProxyEnv();
if (app.commandLine.getSwitchValue("proxy-server") !== "") { if (app.commandLine.getSwitchValue("proxy-server") !== "") {
process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server"); process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server");
} }
@ -48,6 +50,7 @@ app.on("ready", async () => {
await shellSync(); await shellSync();
const updater = new AppUpdater(); const updater = new AppUpdater();
updater.start(); updater.start();
registerFileProtocol("static", __static); registerFileProtocol("static", __static);
@ -110,6 +113,7 @@ app.on("ready", async () => {
app.on("activate", (event, hasVisibleWindows) => { app.on("activate", (event, hasVisibleWindows) => {
logger.info("APP:ACTIVATE", { hasVisibleWindows }); logger.info("APP:ACTIVATE", { hasVisibleWindows });
if (!hasVisibleWindows) { if (!hasVisibleWindows) {
windowManager.initMainWindow(); windowManager.initMainWindow();
} }
@ -121,6 +125,7 @@ app.on("will-quit", (event) => {
appEventBus.emit({name: "app", action: "close"}); appEventBus.emit({name: "app", action: "close"});
event.preventDefault(); // prevent app's default shutdown (e.g. required for telemetry, etc.) event.preventDefault(); // prevent app's default shutdown (e.g. required for telemetry, etc.)
clusterManager?.stop(); // close cluster connections clusterManager?.stop(); // close cluster connections
return; // skip exit to make tray work, to quit go to app's global menu or tray's menu return; // skip exit to make tray work, to quit go to app's global menu or tray's menu
}); });

View File

@ -45,6 +45,7 @@ export class KubeAuthProxy {
"--accept-hosts", this.acceptHosts, "--accept-hosts", this.acceptHosts,
"--reject-paths", "^[^/]" "--reject-paths", "^[^/]"
]; ];
if (process.env.DEBUG_PROXY === "true") { if (process.env.DEBUG_PROXY === "true") {
args.push("-v", "9"); args.push("-v", "9");
} }
@ -62,6 +63,7 @@ export class KubeAuthProxy {
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")) {
logItem = "Authentication proxy started\n"; logItem = "Authentication proxy started\n";
} }
@ -80,19 +82,23 @@ export class KubeAuthProxy {
const error = data.split("http: proxy error:").slice(1).join("").trim(); const error = data.split("http: proxy error:").slice(1).join("").trim();
let errorMsg = error; let errorMsg = error;
const jsonError = error.split("Response: ")[1]; const jsonError = error.split("Response: ")[1];
if (jsonError) { if (jsonError) {
try { try {
const parsedError = JSON.parse(jsonError); const parsedError = JSON.parse(jsonError);
errorMsg = parsedError.error_description || parsedError.error || jsonError; errorMsg = parsedError.error_description || parsedError.error || jsonError;
} catch (_) { } catch (_) {
errorMsg = jsonError.trim(); errorMsg = jsonError.trim();
} }
} }
return errorMsg; return errorMsg;
} }
protected async sendIpcLogMessage(res: KubeAuthProxyLog) { protected async sendIpcLogMessage(res: KubeAuthProxyLog) {
const channel = `kube-auth:${this.cluster.id}`; const channel = `kube-auth:${this.cluster.id}`;
logger.info(`[KUBE-AUTH]: out-channel "${channel}"`, { ...res, meta: this.cluster.getMeta() }); logger.info(`[KUBE-AUTH]: out-channel "${channel}"`, { ...res, meta: this.cluster.getMeta() });
broadcastMessage(channel, res); broadcastMessage(channel, res);
} }

View File

@ -15,7 +15,9 @@ export class KubeconfigManager {
static async create(cluster: Cluster, contextHandler: ContextHandler, port: number) { static async create(cluster: Cluster, contextHandler: ContextHandler, port: number) {
const kcm = new KubeconfigManager(cluster, contextHandler, port); const kcm = new KubeconfigManager(cluster, contextHandler, port);
await kcm.init(); await kcm.init();
return kcm; return kcm;
} }
@ -66,13 +68,14 @@ export class KubeconfigManager {
} }
] ]
}; };
// write // write
const configYaml = dumpConfigYaml(proxyConfig); const configYaml = dumpConfigYaml(proxyConfig);
fs.ensureDir(path.dirname(tempFile)); fs.ensureDir(path.dirname(tempFile));
fs.writeFileSync(tempFile, configYaml, { mode: 0o600 }); fs.writeFileSync(tempFile, configYaml, { mode: 0o600 });
this.tempFile = tempFile; this.tempFile = tempFile;
logger.debug(`Created temp kubeconfig "${contextName}" at "${tempFile}": \n${configYaml}`); logger.debug(`Created temp kubeconfig "${contextName}" at "${tempFile}": \n${configYaml}`);
return tempFile; return tempFile;
} }

View File

@ -27,12 +27,10 @@ const kubectlMap: Map<string, string> = new Map([
["1.18", "1.18.8"], ["1.18", "1.18.8"],
["1.19", "1.19.0"] ["1.19", "1.19.0"]
]); ]);
const packageMirrors: Map<string, string> = new Map([ const packageMirrors: Map<string, string> = new Map([
["default", "https://storage.googleapis.com/kubernetes-release/release"], ["default", "https://storage.googleapis.com/kubernetes-release/release"],
["china", "https://mirror.azure.cn/kubernetes/kubectl"] ["china", "https://mirror.azure.cn/kubernetes/kubectl"]
]); ]);
let bundledPath: string; let bundledPath: string;
const initScriptVersionString = "# lens-initscript v3\n"; const initScriptVersionString = "# lens-initscript v3\n";
@ -41,6 +39,7 @@ export function bundledKubectlPath(): string {
if (isDevelopment || isTestEnv) { if (isDevelopment || isTestEnv) {
const platformName = isWindows ? "windows" : process.platform; const platformName = isWindows ? "windows" : process.platform;
bundledPath = path.join(process.cwd(), "binaries", "client", platformName, process.arch, "kubectl"); bundledPath = path.join(process.cwd(), "binaries", "client", platformName, process.arch, "kubectl");
} else { } else {
bundledPath = path.join(process.resourcesPath, process.arch, "kubectl"); bundledPath = path.join(process.resourcesPath, process.arch, "kubectl");
@ -71,12 +70,14 @@ export class Kubectl {
// Returns the single bundled Kubectl instance // Returns the single bundled Kubectl instance
public static bundled() { public static bundled() {
if (!Kubectl.bundledInstance) Kubectl.bundledInstance = new Kubectl(Kubectl.bundledKubectlVersion); if (!Kubectl.bundledInstance) Kubectl.bundledInstance = new Kubectl(Kubectl.bundledKubectlVersion);
return Kubectl.bundledInstance; return Kubectl.bundledInstance;
} }
constructor(clusterVersion: string) { constructor(clusterVersion: string) {
const versionParts = /^v?(\d+\.\d+)(.*)/.exec(clusterVersion); const versionParts = /^v?(\d+\.\d+)(.*)/.exec(clusterVersion);
const minorVersion = versionParts[1]; const minorVersion = 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 (kubectlMap.has(minorVersion)) {
@ -134,18 +135,22 @@ export class Kubectl {
// return binary name if bundled path is not functional // return binary name if bundled path is not functional
if (!await this.checkBinary(this.getBundledPath(), false)) { if (!await this.checkBinary(this.getBundledPath(), false)) {
Kubectl.invalidBundle = true; Kubectl.invalidBundle = true;
return path.basename(this.getBundledPath()); return path.basename(this.getBundledPath());
} }
try { try {
if (!await this.ensureKubectl()) { if (!await this.ensureKubectl()) {
logger.error("Failed to ensure kubectl, fallback to the bundled version"); logger.error("Failed to ensure kubectl, fallback to the bundled version");
return this.getBundledPath(); return this.getBundledPath();
} }
return this.path; return this.path;
} catch (err) { } catch (err) {
logger.error("Failed to ensure kubectl, fallback to the bundled version"); logger.error("Failed to ensure kubectl, fallback to the bundled version");
logger.error(err); logger.error(err);
return this.getBundledPath(); return this.getBundledPath();
} }
} }
@ -154,28 +159,35 @@ export class Kubectl {
try { try {
await this.ensureKubectl(); await this.ensureKubectl();
await this.writeInitScripts(); await this.writeInitScripts();
return this.dirname; return this.dirname;
} catch (err) { } catch (err) {
logger.error(err); logger.error(err);
return ""; return "";
} }
} }
public async checkBinary(path: string, checkVersion = true) { public async checkBinary(path: string, checkVersion = true) {
const exists = await pathExists(path); const exists = await pathExists(path);
if (exists) { if (exists) {
try { try {
const { stdout } = await promiseExec(`"${path}" version --client=true -o json`); const { stdout } = await promiseExec(`"${path}" version --client=true -o json`);
const output = JSON.parse(stdout); const output = JSON.parse(stdout);
if (!checkVersion) { if (!checkVersion) {
return true; return true;
} }
let version: string = output.clientVersion.gitVersion; let version: string = output.clientVersion.gitVersion;
if (version[0] === "v") { if (version[0] === "v") {
version = version.slice(1); version = version.slice(1);
} }
if (version === this.kubectlVersion) { if (version === this.kubectlVersion) {
logger.debug(`Local kubectl is version ${this.kubectlVersion}`); logger.debug(`Local kubectl is version ${this.kubectlVersion}`);
return true; return true;
} }
logger.error(`Local kubectl is version ${version}, expected ${this.kubectlVersion}, unlinking`); logger.error(`Local kubectl is version ${version}, expected ${this.kubectlVersion}, unlinking`);
@ -184,6 +196,7 @@ export class Kubectl {
} }
await fs.promises.unlink(this.path); await fs.promises.unlink(this.path);
} }
return false; return false;
} }
@ -191,13 +204,16 @@ export class Kubectl {
if (this.kubectlVersion === Kubectl.bundledKubectlVersion) { if (this.kubectlVersion === Kubectl.bundledKubectlVersion) {
try { try {
const exist = await pathExists(this.path); const exist = await pathExists(this.path);
if (!exist) { if (!exist) {
await fs.promises.copyFile(this.getBundledPath(), this.path); await fs.promises.copyFile(this.getBundledPath(), this.path);
await fs.promises.chmod(this.path, 0o755); await fs.promises.chmod(this.path, 0o755);
} }
return true; return true;
} catch (err) { } catch (err) {
logger.error(`Could not copy the bundled kubectl to app-data: ${err}`); logger.error(`Could not copy the bundled kubectl to app-data: ${err}`);
return false; return false;
} }
} else { } else {
@ -209,35 +225,44 @@ export class Kubectl {
if (userStore.preferences?.downloadKubectlBinaries === false) { if (userStore.preferences?.downloadKubectlBinaries === false) {
return true; return true;
} }
if (Kubectl.invalidBundle) { if (Kubectl.invalidBundle) {
logger.error(`Detected invalid bundle binary, returning ...`); logger.error(`Detected invalid bundle binary, returning ...`);
return false; return false;
} }
await ensureDir(this.dirname, 0o755); await ensureDir(this.dirname, 0o755);
return lockFile.lock(this.dirname).then(async (release) => { return lockFile.lock(this.dirname).then(async (release) => {
logger.debug(`Acquired a lock for ${this.kubectlVersion}`); logger.debug(`Acquired a lock for ${this.kubectlVersion}`);
const bundled = await this.checkBundled(); const bundled = await this.checkBundled();
let isValid = await this.checkBinary(this.path, !bundled); let isValid = await this.checkBinary(this.path, !bundled);
if (!isValid && !bundled) { if (!isValid && !bundled) {
await this.downloadKubectl().catch((error) => { await this.downloadKubectl().catch((error) => {
logger.error(error); logger.error(error);
logger.debug(`Releasing lock for ${this.kubectlVersion}`); logger.debug(`Releasing lock for ${this.kubectlVersion}`);
release(); release();
return false; return false;
}); });
isValid = !await this.checkBinary(this.path, false); isValid = !await this.checkBinary(this.path, false);
} }
if (!isValid) { if (!isValid) {
logger.debug(`Releasing lock for ${this.kubectlVersion}`); logger.debug(`Releasing lock for ${this.kubectlVersion}`);
release(); release();
return false; return false;
} }
logger.debug(`Releasing lock for ${this.kubectlVersion}`); logger.debug(`Releasing lock for ${this.kubectlVersion}`);
release(); release();
return true; return true;
}).catch((e) => { }).catch((e) => {
logger.error(`Failed to get a lock for ${this.kubectlVersion}`); logger.error(`Failed to get a lock for ${this.kubectlVersion}`);
logger.error(e); logger.error(e);
return false; return false;
}); });
} }
@ -246,12 +271,14 @@ export class Kubectl {
await ensureDir(path.dirname(this.path), 0o755); await ensureDir(path.dirname(this.path), 0o755);
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((resolve, reject) => {
const stream = customRequest({ const stream = customRequest({
url: this.url, url: this.url,
gzip: true, gzip: true,
}); });
const file = fs.createWriteStream(this.path); const file = fs.createWriteStream(this.path);
stream.on("complete", () => { stream.on("complete", () => {
logger.debug("kubectl binary download finished"); logger.debug("kubectl binary download finished");
file.end(); file.end();
@ -279,8 +306,8 @@ export class Kubectl {
const helmPath = helmCli.getBinaryDir(); const helmPath = helmCli.getBinaryDir();
const fsPromises = fs.promises; const fsPromises = fs.promises;
const bashScriptPath = path.join(this.dirname, ".bash_set_path"); const bashScriptPath = path.join(this.dirname, ".bash_set_path");
let bashScript = `${initScriptVersionString}`; let bashScript = `${initScriptVersionString}`;
bashScript += "tempkubeconfig=\"$KUBECONFIG\"\n"; bashScript += "tempkubeconfig=\"$KUBECONFIG\"\n";
bashScript += "test -f \"/etc/profile\" && . \"/etc/profile\"\n"; bashScript += "test -f \"/etc/profile\" && . \"/etc/profile\"\n";
bashScript += "if test -f \"$HOME/.bash_profile\"; then\n"; bashScript += "if test -f \"$HOME/.bash_profile\"; then\n";
@ -302,7 +329,6 @@ export class Kubectl {
await fsPromises.writeFile(bashScriptPath, bashScript.toString(), { mode: 0o644 }); await fsPromises.writeFile(bashScriptPath, bashScript.toString(), { mode: 0o644 });
const zshScriptPath = path.join(this.dirname, ".zlogin"); const zshScriptPath = path.join(this.dirname, ".zlogin");
let zshScript = `${initScriptVersionString}`; let zshScript = `${initScriptVersionString}`;
zshScript += "tempkubeconfig=\"$KUBECONFIG\"\n"; zshScript += "tempkubeconfig=\"$KUBECONFIG\"\n";
@ -335,9 +361,11 @@ export class Kubectl {
protected getDownloadMirror() { protected getDownloadMirror() {
const mirror = packageMirrors.get(userStore.preferences?.downloadMirror); const mirror = packageMirrors.get(userStore.preferences?.downloadMirror);
if (mirror) { if (mirror) {
return mirror; return mirror;
} }
return packageMirrors.get("default"); // MacOS packages are only available from default return packageMirrors.get("default"); // MacOS packages are only available from default
} }
} }

View File

@ -8,12 +8,14 @@ jest.mock("../common/user-store");
describe("kubectlVersion", () => { describe("kubectlVersion", () => {
it("returns bundled version if exactly same version used", async () => { it("returns bundled version if exactly same version used", async () => {
const kubectl = new Kubectl(Kubectl.bundled().kubectlVersion); const kubectl = new Kubectl(Kubectl.bundled().kubectlVersion);
expect(kubectl.kubectlVersion).toBe(Kubectl.bundled().kubectlVersion); expect(kubectl.kubectlVersion).toBe(Kubectl.bundled().kubectlVersion);
}); });
it("returns bundled version if same major.minor version is used", async () => { it("returns bundled version if same major.minor version is used", async () => {
const { bundledKubectlVersion } = packageInfo.config; const { bundledKubectlVersion } = packageInfo.config;
const kubectl = new Kubectl(bundledKubectlVersion); const kubectl = new Kubectl(bundledKubectlVersion);
expect(kubectl.kubectlVersion).toBe(Kubectl.bundled().kubectlVersion); expect(kubectl.kubectlVersion).toBe(Kubectl.bundled().kubectlVersion);
}); });
}); });
@ -24,19 +26,23 @@ describe("getPath()", () => {
const kubectl = new Kubectl(bundledKubectlVersion); const kubectl = new Kubectl(bundledKubectlVersion);
const kubectlPath = await kubectl.getPath(); const kubectlPath = await kubectl.getPath();
let binaryName = "kubectl"; let binaryName = "kubectl";
if (isWindows) { if (isWindows) {
binaryName += ".exe"; binaryName += ".exe";
} }
const expectedPath = path.join(Kubectl.kubectlDir, Kubectl.bundledKubectlVersion, binaryName); const expectedPath = path.join(Kubectl.kubectlDir, Kubectl.bundledKubectlVersion, binaryName);
expect(kubectlPath).toBe(expectedPath); expect(kubectlPath).toBe(expectedPath);
}); });
it("returns plain binary name if bundled kubectl is non-functional", async () => { it("returns plain binary name if bundled kubectl is non-functional", async () => {
const { bundledKubectlVersion } = packageInfo.config; const { bundledKubectlVersion } = packageInfo.config;
const kubectl = new Kubectl(bundledKubectlVersion); const kubectl = new Kubectl(bundledKubectlVersion);
jest.spyOn(kubectl, "getBundledPath").mockReturnValue("/invalid/path/kubectl"); jest.spyOn(kubectl, "getBundledPath").mockReturnValue("/invalid/path/kubectl");
const kubectlPath = await kubectl.getPath(); const kubectlPath = await kubectl.getPath();
let binaryName = "kubectl"; let binaryName = "kubectl";
if (isWindows) { if (isWindows) {
binaryName += ".exe"; binaryName += ".exe";
} }

View File

@ -31,6 +31,7 @@ export class LensBinary {
constructor(opts: LensBinaryOpts) { constructor(opts: LensBinaryOpts) {
const baseDir = opts.baseDir; const baseDir = opts.baseDir;
this.originalBinaryName = opts.originalBinaryName; this.originalBinaryName = opts.originalBinaryName;
this.binaryName = opts.newBinaryName || opts.originalBinaryName; this.binaryName = opts.newBinaryName || opts.originalBinaryName;
this.binaryVersion = opts.version; this.binaryVersion = opts.version;
@ -50,11 +51,13 @@ export class LensBinary {
this.arch = arch; this.arch = arch;
this.platformName = isWindows ? "windows" : process.platform; this.platformName = isWindows ? "windows" : process.platform;
this.dirname = path.normalize(path.join(baseDir, this.binaryName)); this.dirname = path.normalize(path.join(baseDir, this.binaryName));
if (isWindows) { if (isWindows) {
this.binaryName = `${this.binaryName}.exe`; this.binaryName = `${this.binaryName}.exe`;
this.originalBinaryName = `${this.originalBinaryName}.exe`; this.originalBinaryName = `${this.originalBinaryName}.exe`;
} }
const tarName = this.getTarName(); const tarName = this.getTarName();
if (tarName) { if (tarName) {
this.tarPath = path.join(this.dirname, tarName); this.tarPath = path.join(this.dirname, tarName);
} }
@ -70,6 +73,7 @@ export class LensBinary {
public async binaryPath() { public async binaryPath() {
await this.ensureBinary(); await this.ensureBinary();
return this.getBinaryPath(); return this.getBinaryPath();
} }
@ -96,20 +100,24 @@ export class LensBinary {
public async binDir() { public async binDir() {
try { try {
await this.ensureBinary(); await this.ensureBinary();
return this.dirname; return this.dirname;
} catch (err) { } catch (err) {
this.logger.error(err); this.logger.error(err);
return ""; return "";
} }
} }
protected async checkBinary() { protected async checkBinary() {
const exists = await pathExists(this.getBinaryPath()); const exists = await pathExists(this.getBinaryPath());
return exists; return exists;
} }
public async ensureBinary() { public async ensureBinary() {
const isValid = await this.checkBinary(); const isValid = await this.checkBinary();
if (!isValid) { if (!isValid) {
await this.downloadBinary().catch((error) => { await this.downloadBinary().catch((error) => {
this.logger.error(error); this.logger.error(error);
@ -148,6 +156,7 @@ export class LensBinary {
protected async downloadBinary() { protected async downloadBinary() {
const binaryPath = this.tarPath || this.getBinaryPath(); const binaryPath = this.tarPath || this.getBinaryPath();
await ensureDir(this.getBinaryDir(), 0o755); await ensureDir(this.getBinaryDir(), 0o755);
const file = fs.createWriteStream(binaryPath); const file = fs.createWriteStream(binaryPath);
@ -159,7 +168,6 @@ export class LensBinary {
gzip: true, gzip: true,
...this.requestOpts ...this.requestOpts
}; };
const stream = request(requestOpts); const stream = request(requestOpts);
stream.on("complete", () => { stream.on("complete", () => {
@ -174,6 +182,7 @@ export class LensBinary {
}); });
throw(error); throw(error);
}); });
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
file.on("close", () => { file.on("close", () => {
this.logger.debug(`${this.originalBinaryName} binary download closed`); this.logger.debug(`${this.originalBinaryName} binary download closed`);

View File

@ -30,6 +30,7 @@ export class LensProxy {
listen(port = this.port): this { listen(port = this.port): this {
this.proxyServer = this.buildCustomProxy().listen(port); this.proxyServer = this.buildCustomProxy().listen(port);
logger.info(`LensProxy server has started at ${this.origin}`); logger.info(`LensProxy server has started at ${this.origin}`);
return this; return this;
} }
@ -49,6 +50,7 @@ export class LensProxy {
}, (req: http.IncomingMessage, res: http.ServerResponse) => { }, (req: http.IncomingMessage, res: http.ServerResponse) => {
this.handleRequest(proxy, req, res); this.handleRequest(proxy, req, res);
}); });
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);
@ -59,22 +61,27 @@ export class LensProxy {
spdyProxy.on("error", (err) => { spdyProxy.on("error", (err) => {
logger.error("proxy error", err); logger.error("proxy error", err);
}); });
return spdyProxy; return spdyProxy;
} }
protected async handleProxyUpgrade(proxy: httpProxy, req: http.IncomingMessage, socket: net.Socket, head: Buffer) { protected async handleProxyUpgrade(proxy: httpProxy, req: http.IncomingMessage, socket: net.Socket, head: Buffer) {
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 proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "");
const apiUrl = url.parse(cluster.apiUrl); const apiUrl = url.parse(cluster.apiUrl);
const pUrl = url.parse(proxyUrl); const pUrl = url.parse(proxyUrl);
const connectOpts = { port: parseInt(pUrl.port), host: pUrl.hostname }; const connectOpts = { port: parseInt(pUrl.port), host: pUrl.hostname };
const proxySocket = new net.Socket(); const proxySocket = new net.Socket();
proxySocket.connect(connectOpts, () => { proxySocket.connect(connectOpts, () => {
proxySocket.write(`${req.method} ${pUrl.path} HTTP/1.1\r\n`); proxySocket.write(`${req.method} ${pUrl.path} HTTP/1.1\r\n`);
proxySocket.write(`Host: ${apiUrl.host}\r\n`); proxySocket.write(`Host: ${apiUrl.host}\r\n`);
for (let i = 0; i < req.rawHeaders.length; i += 2) { for (let i = 0; i < req.rawHeaders.length; i += 2) {
const key = req.rawHeaders[i]; const key = req.rawHeaders[i];
if (key !== "Host" && key !== "Authorization") { if (key !== "Host" && key !== "Authorization") {
proxySocket.write(`${req.rawHeaders[i]}: ${req.rawHeaders[i+1]}\r\n`); proxySocket.write(`${req.rawHeaders[i]}: ${req.rawHeaders[i+1]}\r\n`);
} }
@ -112,16 +119,20 @@ export class LensProxy {
protected createProxy(): httpProxy { protected createProxy(): httpProxy {
const proxy = httpProxy.createProxyServer(); const proxy = httpProxy.createProxyServer();
proxy.on("error", (error, req, res, target) => { proxy.on("error", (error, req, res, target) => {
if (this.closed) { if (this.closed) {
return; return;
} }
if (target) { if (target) {
logger.debug(`Failed proxy to target: ${JSON.stringify(target, null, 2)}`); logger.debug(`Failed proxy to target: ${JSON.stringify(target, null, 2)}`);
if (req.method === "GET" && (!res.statusCode || res.statusCode >= 500)) { if (req.method === "GET" && (!res.statusCode || res.statusCode >= 500)) {
const reqId = this.getRequestId(req); const reqId = this.getRequestId(req);
const retryCount = this.retryCounters.get(reqId) || 0; const retryCount = this.retryCounters.get(reqId) || 0;
const timeoutMs = retryCount * 250; const timeoutMs = retryCount * 250;
if (retryCount < 20) { if (retryCount < 20) {
logger.debug(`Retrying proxy request to url: ${reqId}`); logger.debug(`Retrying proxy request to url: ${reqId}`);
setTimeout(() => { setTimeout(() => {
@ -131,6 +142,7 @@ export class LensProxy {
} }
} }
} }
try { try {
res.writeHead(500).end("Oops, something went wrong."); res.writeHead(500).end("Oops, something went wrong.");
} catch (e) { } catch (e) {
@ -143,9 +155,11 @@ export class LensProxy {
protected createWsListener(): WebSocket.Server { protected createWsListener(): WebSocket.Server {
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 = this.clusterManager.getClusterForRequest(req);
const nodeParam = url.parse(req.url, true).query["node"]?.toString(); const nodeParam = url.parse(req.url, true).query["node"]?.toString();
openShell(socket, cluster, nodeParam); openShell(socket, cluster, nodeParam);
})); }));
} }
@ -155,6 +169,7 @@ export class LensProxy {
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=");
return await contextHandler.getApiTarget(isWatchRequest); return await contextHandler.getApiTarget(isWatchRequest);
} }
} }
@ -165,11 +180,14 @@ export class LensProxy {
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 proxyTarget = await this.getProxyTarget(req, cluster.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"
res.setHeader("Access-Control-Allow-Origin", this.origin); res.setHeader("Access-Control-Allow-Origin", this.origin);
return proxy.web(req, res, proxyTarget); return proxy.web(req, res, proxyTarget);
} }
} }
@ -178,6 +196,7 @@ export class LensProxy {
protected async handleWsUpgrade(req: http.IncomingMessage, socket: net.Socket, head: Buffer) { protected async handleWsUpgrade(req: http.IncomingMessage, socket: net.Socket, head: Buffer) {
const wsServer = this.createWsListener(); const wsServer = this.createWsListener();
wsServer.handleUpgrade(req, socket, head, (con) => { wsServer.handleUpgrade(req, socket, head, (con) => {
wsServer.emit("connection", con, req); wsServer.emit("connection", con, req);
}); });

View File

@ -3,12 +3,10 @@ import winston from "winston";
import { isDebugging } from "../common/vars"; import { isDebugging } from "../common/vars";
const logLevel = process.env.LOG_LEVEL ? process.env.LOG_LEVEL : isDebugging ? "debug" : "info"; const logLevel = process.env.LOG_LEVEL ? process.env.LOG_LEVEL : isDebugging ? "debug" : "info";
const consoleOptions: winston.transports.ConsoleTransportOptions = { const consoleOptions: winston.transports.ConsoleTransportOptions = {
handleExceptions: false, handleExceptions: false,
level: logLevel, level: logLevel,
}; };
const fileOptions: winston.transports.FileTransportOptions = { const fileOptions: winston.transports.FileTransportOptions = {
handleExceptions: false, handleExceptions: false,
level: logLevel, level: logLevel,
@ -18,7 +16,6 @@ const fileOptions: winston.transports.FileTransportOptions = {
maxFiles: 16, maxFiles: 16,
tailable: true, tailable: true,
}; };
const logger = winston.createLogger({ const logger = winston.createLogger({
format: winston.format.combine( format: winston.format.combine(
winston.format.colorize(), winston.format.colorize(),

View File

@ -27,6 +27,7 @@ export function showAbout(browserWindow: BrowserWindow) {
`Node: ${process.versions.node}`, `Node: ${process.versions.node}`,
`Copyright 2020 Mirantis, Inc.`, `Copyright 2020 Mirantis, Inc.`,
]; ];
dialog.showMessageBoxSync(browserWindow, { dialog.showMessageBoxSync(browserWindow, {
title: `${isWindows ? " ".repeat(2) : ""}${appName}`, title: `${isWindows ? " ".repeat(2) : ""}${appName}`,
type: "info", type: "info",
@ -39,6 +40,7 @@ export function showAbout(browserWindow: BrowserWindow) {
export function buildMenu(windowManager: WindowManager) { export function buildMenu(windowManager: WindowManager) {
function ignoreOnMac(menuItems: MenuItemConstructorOptions[]) { function ignoreOnMac(menuItems: MenuItemConstructorOptions[]) {
if (isMac) return []; if (isMac) return [];
return menuItems; return menuItems;
} }
@ -48,6 +50,7 @@ export function buildMenu(windowManager: WindowManager) {
item.enabled = false; item.enabled = false;
}); });
} }
return menuItems; return menuItems;
} }
@ -96,7 +99,6 @@ export function buildMenu(windowManager: WindowManager) {
} }
] ]
}; };
const fileMenu: MenuItemConstructorOptions = { const fileMenu: MenuItemConstructorOptions = {
label: "File", label: "File",
submenu: [ submenu: [
@ -154,7 +156,6 @@ export function buildMenu(windowManager: WindowManager) {
]) ])
] ]
}; };
const editMenu: MenuItemConstructorOptions = { const editMenu: MenuItemConstructorOptions = {
label: "Edit", label: "Edit",
submenu: [ submenu: [
@ -169,7 +170,6 @@ export function buildMenu(windowManager: WindowManager) {
{ role: "selectAll" }, { role: "selectAll" },
] ]
}; };
const viewMenu: MenuItemConstructorOptions = { const viewMenu: MenuItemConstructorOptions = {
label: "View", label: "View",
submenu: [ submenu: [
@ -203,7 +203,6 @@ export function buildMenu(windowManager: WindowManager) {
{ role: "togglefullscreen" } { role: "togglefullscreen" }
] ]
}; };
const helpMenu: MenuItemConstructorOptions = { const helpMenu: MenuItemConstructorOptions = {
role: "help", role: "help",
submenu: [ submenu: [
@ -235,7 +234,6 @@ export function buildMenu(windowManager: WindowManager) {
]) ])
] ]
}; };
// Prepare menu items order // Prepare menu items order
const appMenu: Record<MenuTopId, MenuItemConstructorOptions> = { const appMenu: Record<MenuTopId, MenuItemConstructorOptions> = {
mac: macAppMenu, mac: macAppMenu,
@ -249,6 +247,7 @@ export function buildMenu(windowManager: WindowManager) {
menuRegistry.getItems().forEach(({ parentId, ...menuItem }) => { menuRegistry.getItems().forEach(({ parentId, ...menuItem }) => {
try { try {
const topMenu = appMenu[parentId as MenuTopId].submenu as MenuItemConstructorOptions[]; const topMenu = appMenu[parentId as MenuTopId].submenu as MenuItemConstructorOptions[];
topMenu.push(menuItem); topMenu.push(menuItem);
} catch (err) { } catch (err) {
logger.error(`[MENU]: can't register menu item, parentId=${parentId}`, { menuItem }); logger.error(`[MENU]: can't register menu item, parentId=${parentId}`, { menuItem });
@ -260,6 +259,7 @@ export function buildMenu(windowManager: WindowManager) {
} }
const menu = Menu.buildFromTemplate(Object.values(appMenu)); const menu = Menu.buildFromTemplate(Object.values(appMenu));
Menu.setApplicationMenu(menu); Menu.setApplicationMenu(menu);
if (isTestEnv) { if (isTestEnv) {
@ -273,6 +273,7 @@ export function buildMenu(windowManager: WindowManager) {
for (const name of names) { for (const name of names) {
parentLabels.push(name); parentLabels.push(name);
menuItem = menu?.items?.find(item => item.label === name); menuItem = menu?.items?.find(item => item.label === name);
if (!menuItem) { if (!menuItem) {
break; break;
} }
@ -280,14 +281,18 @@ export function buildMenu(windowManager: WindowManager) {
} }
const menuPath: string = parentLabels.join(" -> "); const menuPath: string = parentLabels.join(" -> ");
if (!menuItem) { if (!menuItem) {
logger.info(`[MENU:test-menu-item-click] Cannot find menu item ${menuPath}`); logger.info(`[MENU:test-menu-item-click] Cannot find menu item ${menuPath}`);
return; return;
} }
const { enabled, visible, click } = menuItem; const { enabled, visible, click } = menuItem;
if (enabled === false || visible === false || typeof click !== "function") { if (enabled === false || visible === false || typeof click !== "function") {
logger.info(`[MENU:test-menu-item-click] Menu item ${menuPath} not clickable`); logger.info(`[MENU:test-menu-item-click] Menu item ${menuPath} not clickable`);
return; return;
} }

View File

@ -23,6 +23,7 @@ export class NodeShellSession extends ShellSession {
public async open() { public async open() {
const shell = await this.kubectl.getPath(); const shell = await this.kubectl.getPath();
let args = []; let args = [];
if (this.createNodeShellPod(this.podId, this.nodeName)) { if (this.createNodeShellPod(this.podId, this.nodeName)) {
await this.waitForRunningPod(this.podId).catch(() => { await this.waitForRunningPod(this.podId).catch(() => {
this.exit(1001); this.exit(1001);
@ -31,6 +32,7 @@ export class NodeShellSession extends ShellSession {
args = ["exec", "-i", "-t", "-n", "kube-system", this.podId, "--", "sh", "-c", "((clear && bash) || (clear && ash) || (clear && sh))"]; args = ["exec", "-i", "-t", "-n", "kube-system", this.podId, "--", "sh", "-c", "((clear && bash) || (clear && ash) || (clear && sh))"];
const shellEnv = await this.getCachedShellEnv(); const shellEnv = await this.getCachedShellEnv();
this.shellProcess = pty.spawn(shell, args, { this.shellProcess = pty.spawn(shell, args, {
cols: 80, cols: 80,
cwd: this.cwd() || shellEnv["HOME"], cwd: this.cwd() || shellEnv["HOME"],
@ -85,10 +87,13 @@ export class NodeShellSession extends ShellSession {
} }
} }
} as k8s.V1Pod; } as k8s.V1Pod;
await k8sApi.createNamespacedPod("kube-system", pod).catch((error) => { await k8sApi.createNamespacedPod("kube-system", pod).catch((error) => {
logger.error(error); logger.error(error);
return false; return false;
}); });
return true; return true;
} }
@ -98,6 +103,7 @@ export class NodeShellSession extends ShellSession {
} }
this.kc = new k8s.KubeConfig(); this.kc = new k8s.KubeConfig();
this.kc.loadFromFile(this.kubeconfigPath); this.kc.loadFromFile(this.kubeconfigPath);
return this.kc; return this.kc;
} }
@ -105,7 +111,6 @@ export class NodeShellSession extends ShellSession {
return new Promise<boolean>(async (resolve, reject) => { return new Promise<boolean>(async (resolve, reject) => {
const kc = this.getKubeConfig(); const kc = this.getKubeConfig();
const watch = new k8s.Watch(kc); const watch = new k8s.Watch(kc);
const req = await watch.watch(`/api/v1/namespaces/kube-system/pods`, {}, const req = await watch.watch(`/api/v1/namespaces/kube-system/pods`, {},
// callback is called for each received object. // callback is called for each received object.
(type, obj) => { (type, obj) => {
@ -119,6 +124,7 @@ export class NodeShellSession extends ShellSession {
reject(false); reject(false);
} }
); );
setTimeout(() => { setTimeout(() => {
req.abort(); req.abort();
reject(false); reject(false);
@ -129,17 +135,20 @@ export class NodeShellSession extends ShellSession {
protected deleteNodeShellPod() { protected deleteNodeShellPod() {
const kc = this.getKubeConfig(); const kc = this.getKubeConfig();
const k8sApi = kc.makeApiClient(k8s.CoreV1Api); const k8sApi = kc.makeApiClient(k8s.CoreV1Api);
k8sApi.deleteNamespacedPod(this.podId, "kube-system"); k8sApi.deleteNamespacedPod(this.podId, "kube-system");
} }
} }
export async function openShell(socket: WebSocket, cluster: Cluster, nodeName?: string): Promise<ShellSession> { export async function openShell(socket: WebSocket, cluster: Cluster, nodeName?: string): Promise<ShellSession> {
let shell: ShellSession; let shell: ShellSession;
if (nodeName) { if (nodeName) {
shell = new NodeShellSession(socket, cluster, nodeName); shell = new NodeShellSession(socket, cluster, nodeName);
} else { } else {
shell = new ShellSession(socket, cluster); shell = new ShellSession(socket, cluster);
} }
shell.open(); shell.open();
return shell; return shell;
} }

View File

@ -5,11 +5,14 @@ import logger from "./logger";
export async function getFreePort(): Promise<number> { export async function getFreePort(): Promise<number> {
logger.debug("Lookup new free port.."); logger.debug("Lookup new free port..");
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const server = net.createServer(); const server = net.createServer();
server.unref(); server.unref();
server.on("listening", () => { server.on("listening", () => {
const port = (server.address() as AddressInfo).port; const port = (server.address() as AddressInfo).port;
server.close(() => resolve(port)); server.close(() => resolve(port));
logger.debug(`New port found: ${port}`); logger.debug(`New port found: ${port}`);
}); });

View File

@ -9,10 +9,12 @@ jest.mock("net", () => {
return new class MockServer extends EventEmitter { return new class MockServer extends EventEmitter {
listen = jest.fn(() => { listen = jest.fn(() => {
this.emit("listening"); this.emit("listening");
return this; return this;
}); });
address = () => { address = () => {
newPort = Math.round(Math.random() * 10000); newPort = Math.round(Math.random() * 10000);
return { return {
port: newPort port: newPort
}; };

View File

@ -10,9 +10,11 @@ export class PrometheusHelm extends PrometheusLens {
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService> { public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService> {
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, "", null, labelSelector);
const service = serviceList.body.items[0]; const service = serviceList.body.items[0];
if (!service) return; if (!service) return;
return { return {
@ -23,6 +25,7 @@ export class PrometheusHelm extends PrometheusLens {
}; };
} catch(error) { } catch(error) {
logger.warn(`PrometheusHelm: failed to list services: ${error.toString()}`); logger.warn(`PrometheusHelm: failed to list services: ${error.toString()}`);
return; return;
} }
} }

View File

@ -11,6 +11,7 @@ export class PrometheusLens implements PrometheusProvider {
try { try {
const resp = await client.readNamespacedService("prometheus", "lens-metrics"); const resp = await client.readNamespacedService("prometheus", "lens-metrics");
const service = resp.body; const service = resp.body;
return { return {
id: this.id, id: this.id,
namespace: service.metadata.namespace, namespace: service.metadata.namespace,
@ -72,6 +73,7 @@ export class PrometheusLens implements PrometheusProvider {
case "ingress": case "ingress":
const bytesSent = (ingress: string, statuses: string) => const bytesSent = (ingress: string, statuses: string) =>
`sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}", status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress)`; `sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}", status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress)`;
return { return {
bytesSentSuccess: bytesSent(opts.igress, "^2\\\\d*"), bytesSentSuccess: bytesSent(opts.igress, "^2\\\\d*"),
bytesSentFailure: bytesSent(opts.ingres, "^5\\\\d*"), bytesSentFailure: bytesSent(opts.ingres, "^5\\\\d*"),

View File

@ -10,9 +10,11 @@ export class PrometheusOperator implements PrometheusProvider {
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService> { public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService> {
try { try {
let service: V1Service; let service: V1Service;
for (const labelSelector of ["operated-prometheus=true", "self-monitor=true"]) { for (const labelSelector of ["operated-prometheus=true", "self-monitor=true"]) {
if (!service) { if (!service) {
const serviceList = await client.listServiceForAllNamespaces(null, null, null, labelSelector); const serviceList = await client.listServiceForAllNamespaces(null, null, null, labelSelector);
service = serviceList.body.items[0]; service = serviceList.body.items[0];
} }
} }
@ -26,6 +28,7 @@ export class PrometheusOperator implements PrometheusProvider {
}; };
} catch(error) { } catch(error) {
logger.warn(`PrometheusOperator: failed to list services: ${error.toString()}`); logger.warn(`PrometheusOperator: failed to list services: ${error.toString()}`);
return; return;
} }
} }
@ -80,6 +83,7 @@ export class PrometheusOperator implements PrometheusProvider {
case "ingress": case "ingress":
const bytesSent = (ingress: string, statuses: string) => const bytesSent = (ingress: string, statuses: string) =>
`sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}", status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress)`; `sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}", status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress)`;
return { return {
bytesSentSuccess: bytesSent(opts.igress, "^2\\\\d*"), bytesSentSuccess: bytesSent(opts.igress, "^2\\\\d*"),
bytesSentFailure: bytesSent(opts.ingres, "^5\\\\d*"), bytesSentFailure: bytesSent(opts.ingres, "^5\\\\d*"),

View File

@ -77,6 +77,7 @@ export class PrometheusProviderRegistry {
if (!this.prometheusProviders[type]) { if (!this.prometheusProviders[type]) {
throw "Unknown Prometheus provider"; throw "Unknown Prometheus provider";
} }
return this.prometheusProviders[type]; return this.prometheusProviders[type];
} }

View File

@ -11,6 +11,7 @@ export class PrometheusStacklight implements PrometheusProvider {
try { try {
const resp = await client.readNamespacedService("prometheus-server", "stacklight"); const resp = await client.readNamespacedService("prometheus-server", "stacklight");
const service = resp.body; const service = resp.body;
return { return {
id: this.id, id: this.id,
namespace: service.metadata.namespace, namespace: service.metadata.namespace,
@ -72,6 +73,7 @@ export class PrometheusStacklight implements PrometheusProvider {
case "ingress": case "ingress":
const bytesSent = (ingress: string, statuses: string) => const bytesSent = (ingress: string, statuses: string) =>
`sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}", status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress)`; `sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}", status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress)`;
return { return {
bytesSentSuccess: bytesSent(opts.igress, "^2\\\\d*"), bytesSentSuccess: bytesSent(opts.igress, "^2\\\\d*"),
bytesSentFailure: bytesSent(opts.ingres, "^5\\\\d*"), bytesSentFailure: bytesSent(opts.ingres, "^5\\\\d*"),

View File

@ -16,19 +16,24 @@ export class ResourceApplier {
async apply(resource: KubernetesObject | any): Promise<string> { async apply(resource: KubernetesObject | any): Promise<string> {
resource = this.sanitizeObject(resource); resource = this.sanitizeObject(resource);
appEventBus.emit({name: "resource", action: "apply"}); appEventBus.emit({name: "resource", action: "apply"});
return await this.kubectlApply(yaml.safeDump(resource)); return await this.kubectlApply(yaml.safeDump(resource));
} }
protected async kubectlApply(content: string): Promise<string> { protected async kubectlApply(content: string): Promise<string> {
const { kubeCtl } = this.cluster; const { kubeCtl } = this.cluster;
const kubectlPath = await kubeCtl.getPath(); const kubectlPath = await kubeCtl.getPath();
return new Promise<string>((resolve, reject) => { return new Promise<string>((resolve, reject) => {
const fileName = tempy.file({ name: "resource.yaml" }); const fileName = tempy.file({ name: "resource.yaml" });
fs.writeFileSync(fileName, content); fs.writeFileSync(fileName, content);
const cmd = `"${kubectlPath}" apply --kubeconfig "${this.cluster.getProxyKubeconfigPath()}" -o json -f "${fileName}"`; const cmd = `"${kubectlPath}" apply --kubeconfig "${this.cluster.getProxyKubeconfigPath()}" -o json -f "${fileName}"`;
logger.debug(`shooting manifests with: ${cmd}`); logger.debug(`shooting manifests with: ${cmd}`);
const execEnv: NodeJS.ProcessEnv = Object.assign({}, process.env); const execEnv: NodeJS.ProcessEnv = Object.assign({}, process.env);
const httpsProxy = this.cluster.preferences?.httpsProxy; const httpsProxy = this.cluster.preferences?.httpsProxy;
if (httpsProxy) { if (httpsProxy) {
execEnv["HTTPS_PROXY"] = httpsProxy; execEnv["HTTPS_PROXY"] = httpsProxy;
} }
@ -37,6 +42,7 @@ export class ResourceApplier {
if (stderr != "") { if (stderr != "") {
fs.unlinkSync(fileName); fs.unlinkSync(fileName);
reject(stderr); reject(stderr);
return; return;
} }
fs.unlinkSync(fileName); fs.unlinkSync(fileName);
@ -48,20 +54,25 @@ export class ResourceApplier {
public async kubectlApplyAll(resources: string[]): Promise<string> { public async kubectlApplyAll(resources: string[]): Promise<string> {
const { kubeCtl } = this.cluster; const { kubeCtl } = this.cluster;
const kubectlPath = await kubeCtl.getPath(); const kubectlPath = await kubeCtl.getPath();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const tmpDir = tempy.directory(); const tmpDir = tempy.directory();
// Dump each resource into tmpDir // Dump each resource into tmpDir
resources.forEach((resource, index) => { resources.forEach((resource, index) => {
fs.writeFileSync(path.join(tmpDir, `${index}.yaml`), resource); fs.writeFileSync(path.join(tmpDir, `${index}.yaml`), resource);
}); });
const cmd = `"${kubectlPath}" apply --kubeconfig "${this.cluster.getProxyKubeconfigPath()}" -o json -f "${tmpDir}"`; const cmd = `"${kubectlPath}" apply --kubeconfig "${this.cluster.getProxyKubeconfigPath()}" -o json -f "${tmpDir}"`;
console.log("shooting manifests with:", cmd); console.log("shooting manifests with:", cmd);
exec(cmd, (error, stdout, stderr) => { exec(cmd, (error, stdout, stderr) => {
if (error) { if (error) {
reject(`Error applying manifests:${error}`); reject(`Error applying manifests:${error}`);
} }
if (stderr != "") { if (stderr != "") {
reject(stderr); reject(stderr);
return; return;
} }
resolve(stdout); resolve(stdout);
@ -74,9 +85,11 @@ export class ResourceApplier {
delete resource.status; delete resource.status;
delete resource.metadata?.resourceVersion; delete resource.metadata?.resourceVersion;
const annotations = resource.metadata?.annotations; const annotations = resource.metadata?.annotations;
if (annotations) { if (annotations) {
delete annotations["kubectl.kubernetes.io/last-applied-configuration"]; delete annotations["kubectl.kubernetes.io/last-applied-configuration"];
} }
return resource; return resource;
} }
} }

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