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

Merge pull request #1450 from lensapp/feature/enforce-semicolons

Enforce semicolons in eslint
This commit is contained in:
Panu Horsmalahti 2020-11-19 18:52:14 +02:00 committed by GitHub
commit c94c599cdd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
588 changed files with 4909 additions and 4901 deletions

View File

@ -20,6 +20,7 @@ module.exports = {
rules: { rules: {
"indent": ["error", 2], "indent": ["error", 2],
"no-unused-vars": "off", "no-unused-vars": "off",
"semi": ["error", "always"],
} }
}, },
{ {
@ -47,7 +48,9 @@ module.exports = {
"@typescript-eslint/ban-types": "off", "@typescript-eslint/ban-types": "off",
"@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-empty-interface": "off", "@typescript-eslint/no-empty-interface": "off",
"indent": ["error", 2] "indent": ["error", 2],
"semi": "off",
"@typescript-eslint/semi": ["error"],
}, },
}, },
{ {
@ -75,7 +78,9 @@ module.exports = {
"@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/ban-types": "off", "@typescript-eslint/ban-types": "off",
"@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-empty-function": "off",
"indent": ["error", 2] "indent": ["error", 2],
"semi": "off",
"@typescript-eslint/semi": ["error"],
}, },
} }
] ]

View File

@ -5,7 +5,7 @@ module.exports = {
getVersion: jest.fn().mockReturnValue("3.0.0"), getVersion: jest.fn().mockReturnValue("3.0.0"),
getLocale: jest.fn().mockRejectedValue("en"), getLocale: jest.fn().mockRejectedValue("en"),
getPath: jest.fn((name: string) => { getPath: jest.fn((name: string) => {
return "tmp" return "tmp";
}), }),
}, },
remote: { remote: {

View File

@ -1,9 +1,9 @@
// Generate tray icons from SVG to PNG + different sizes and colors (B&W) // Generate tray icons from SVG to PNG + different sizes and colors (B&W)
// Command: `yarn build:tray-icons` // Command: `yarn build:tray-icons`
import path from "path" import path from "path";
import sharp from "sharp"; import sharp from "sharp";
import jsdom from "jsdom" import jsdom from "jsdom";
import fs from "fs-extra" import fs from "fs-extra";
export async function generateTrayIcon( export async function generateTrayIcon(
{ {
@ -14,15 +14,15 @@ export async function generateTrayIcon(
pixelSize = 32, pixelSize = 32,
shouldUseDarkColors = false, // managed by electron.nativeTheme.shouldUseDarkColors shouldUseDarkColors = false, // managed by electron.nativeTheme.shouldUseDarkColors
} = {}) { } = {}) {
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

View File

@ -1,3 +1,3 @@
import { helmCli } from "../src/main/helm/helm-cli" import { helmCli } from "../src/main/helm/helm-cli";
helmCli.ensureBinary() helmCli.ensureBinary();

View File

@ -1,20 +1,20 @@
import packageInfo from "../package.json" import packageInfo from "../package.json";
import fs from "fs" import fs from "fs";
import request from "request" import request from "request";
import md5File from "md5-file" import md5File from "md5-file";
import requestPromise from "request-promise-native" import requestPromise from "request-promise-native";
import { ensureDir, pathExists } from "fs-extra" import { ensureDir, pathExists } from "fs-extra";
import path from "path" import path from "path";
class KubectlDownloader { class KubectlDownloader {
public kubectlVersion: string public kubectlVersion: string;
protected url: string protected url: string;
protected path: string; protected path: string;
protected dirname: string protected dirname: string;
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;
@ -25,83 +25,85 @@ class KubectlDownloader {
method: "HEAD", method: "HEAD",
uri: this.url, uri: this.url,
resolveWithFullResponse: true resolveWithFullResponse: true
}).catch((error) => { console.log(error) }) }).catch((error) => { console.log(error); });
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;
} }
console.log("Kubectl md5sum " + hash + " does not match the remote etag " + etag + ", unlinking and downloading again") console.log("Kubectl md5sum " + hash + " does not match the remote etag " + etag + ", unlinking and downloading again");
await fs.promises.unlink(this.path) await fs.promises.unlink(this.path);
} }
return false return false;
} }
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,
gzip: true gzip: true
} };
const stream = request(requestOpts) const stream = request(requestOpts);
stream.on("complete", () => { stream.on("complete", () => {
console.log("kubectl binary download finished") console.log("kubectl binary download finished");
file.end(() => {}) // eslint-disable-next-line @typescript-eslint/no-empty-function
}) file.end(() => {});
});
stream.on("error", (error) => { stream.on("error", (error) => {
console.log(error) console.log(error);
fs.unlink(this.path, () => {}) // eslint-disable-next-line @typescript-eslint/no-empty-function
throw(error) fs.unlink(this.path, () => {});
}) 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");
fs.chmod(this.path, 0o755, (err) => { fs.chmod(this.path, 0o755, (err) => {
if (err) reject(err); if (err) reject(err);
}) });
resolve() resolve();
}) });
stream.pipe(file) stream.pipe(file);
}) });
} }
} }
const downloadVersion = packageInfo.config.bundledKubectlVersion; const downloadVersion = packageInfo.config.bundledKubectlVersion;
const baseDir = path.join(process.env.INIT_CWD, 'binaries', 'client') const baseDir = path.join(process.env.INIT_CWD, 'binaries', 'client');
const downloads = [ const downloads = [
{ platform: 'linux', arch: 'amd64', target: path.join(baseDir, 'linux', 'x64', 'kubectl') }, { platform: 'linux', arch: 'amd64', target: path.join(baseDir, 'linux', 'x64', 'kubectl') },
{ platform: 'darwin', arch: 'amd64', target: path.join(baseDir, 'darwin', 'x64', 'kubectl') }, { platform: 'darwin', arch: 'amd64', target: path.join(baseDir, 'darwin', 'x64', 'kubectl') },
{ platform: 'windows', arch: 'amd64', target: path.join(baseDir, 'windows', 'x64', 'kubectl.exe') }, { platform: 'windows', arch: 'amd64', target: path.join(baseDir, 'windows', 'x64', 'kubectl.exe') },
{ platform: 'windows', arch: '386', target: path.join(baseDir, 'windows', 'ia32', 'kubectl.exe') } { platform: 'windows', arch: '386', target: path.join(baseDir, 'windows', 'ia32', 'kubectl.exe') }
] ];
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

@ -1,4 +1,4 @@
const { notarize } = require('electron-notarize') 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;

View File

@ -1,9 +1,9 @@
import * as fs from "fs" import * as fs from "fs";
import * as path from "path" import * as path from "path";
import packageInfo from "../src/extensions/npm/extensions/package.json" import packageInfo from "../src/extensions/npm/extensions/package.json";
import appInfo from "../package.json" import appInfo from "../package.json";
const packagePath = path.join(__dirname, "../src/extensions/npm/extensions/package.json") const packagePath = path.join(__dirname, "../src/extensions/npm/extensions/package.json");
packageInfo.version = appInfo.version packageInfo.version = appInfo.version;
fs.writeFileSync(packagePath, JSON.stringify(packageInfo, null, 2) + "\n") fs.writeFileSync(packagePath, JSON.stringify(packageInfo, null, 2) + "\n");

View File

@ -1,10 +1,10 @@
import { LensRendererExtension, Component } from "@k8slens/extensions"; import { LensRendererExtension, Component } from "@k8slens/extensions";
import { CoffeeDoodle } from "react-open-doodles"; import { CoffeeDoodle } from "react-open-doodles";
import path from "path"; import path from "path";
import React from "react" import React from "react";
export function ExampleIcon(props: Component.IconProps) { export function ExampleIcon(props: Component.IconProps) {
return <Component.Icon {...props} material="pages" tooltip={path.basename(__filename)}/> return <Component.Icon {...props} material="pages" tooltip={path.basename(__filename)}/>;
} }
export class ExamplePage extends React.Component<{ extension: LensRendererExtension }> { export class ExamplePage extends React.Component<{ extension: LensRendererExtension }> {
@ -16,7 +16,7 @@ export class ExamplePage extends React.Component<{ extension: LensRendererExtens
render() { render() {
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>
@ -24,6 +24,6 @@ export class ExamplePage extends React.Component<{ extension: LensRendererExtens
<p>File: <i>{__filename}</i></p> <p>File: <i>{__filename}</i></p>
<Component.Button accent label="Deactivate" onClick={this.deactivate}/> <Component.Button accent label="Deactivate" onClick={this.deactivate}/>
</div> </div>
) );
} }
} }

View File

@ -1,6 +1,6 @@
import { LensRendererExtension } from "@k8slens/extensions"; import { LensRendererExtension } from "@k8slens/extensions";
import { ExampleIcon, ExamplePage } from "./page" import { ExampleIcon, ExamplePage } from "./page";
import React from "react" import React from "react";
export default class ExampleExtension extends LensRendererExtension { export default class ExampleExtension extends LensRendererExtension {
clusterPages = [ clusterPages = [

View File

@ -1,5 +1,5 @@
import { LensRendererExtension, K8sApi } from "@k8slens/extensions"; import { LensRendererExtension, K8sApi } from "@k8slens/extensions";
import { resolveStatus, resolveStatusForCronJobs, resolveStatusForPods } from "./src/resolver" import { resolveStatus, resolveStatusForCronJobs, resolveStatusForPods } from "./src/resolver";
export default class EventResourceStatusRendererExtension extends LensRendererExtension { export default class EventResourceStatusRendererExtension extends LensRendererExtension {
kubeObjectStatusTexts = [ kubeObjectStatusTexts = [

View File

@ -1,9 +1,9 @@
import { K8sApi } from "@k8slens/extensions"; import { K8sApi } from "@k8slens/extensions";
export function resolveStatus(object: K8sApi.KubeObject): K8sApi.KubeObjectStatus { export function resolveStatus(object: K8sApi.KubeObject): K8sApi.KubeObjectStatus {
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);
let 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;
} }
@ -12,16 +12,16 @@ export function resolveStatus(object: K8sApi.KubeObject): K8sApi.KubeObjectStatu
level: K8sApi.KubeObjectStatusLevel.WARNING, level: K8sApi.KubeObjectStatusLevel.WARNING,
text: `${event.message}`, text: `${event.message}`,
timestamp: event.metadata.creationTimestamp timestamp: event.metadata.creationTimestamp
} };
} }
export function resolveStatusForPods(pod: K8sApi.Pod): K8sApi.KubeObjectStatus { export function resolveStatusForPods(pod: K8sApi.Pod): K8sApi.KubeObjectStatus {
if (!pod.hasIssues()) { if (!pod.hasIssues()) {
return null return null;
} }
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);
let 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;
} }
@ -30,13 +30,13 @@ export function resolveStatusForPods(pod: K8sApi.Pod): K8sApi.KubeObjectStatus {
level: K8sApi.KubeObjectStatusLevel.WARNING, level: K8sApi.KubeObjectStatusLevel.WARNING,
text: `${event.message}`, text: `${event.message}`,
timestamp: event.metadata.creationTimestamp timestamp: event.metadata.creationTimestamp
} };
} }
export function resolveStatusForCronJobs(cronJob: K8sApi.CronJob): K8sApi.KubeObjectStatus { export function resolveStatusForCronJobs(cronJob: K8sApi.CronJob): K8sApi.KubeObjectStatus {
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);
let 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");
} }
@ -48,5 +48,5 @@ export function resolveStatusForCronJobs(cronJob: K8sApi.CronJob): K8sApi.KubeOb
level: K8sApi.KubeObjectStatusLevel.WARNING, level: K8sApi.KubeObjectStatusLevel.WARNING,
text: `${event.message}`, text: `${event.message}`,
timestamp: event.metadata.creationTimestamp timestamp: event.metadata.creationTimestamp
} };
} }

View File

@ -6,7 +6,7 @@ export default class LicenseLensMainExtension extends LensMainExtension {
parentId: "help", parentId: "help",
label: "License", label: "License",
async click() { async click() {
Util.openExternal("https://k8slens.dev/licenses/eula.md") Util.openExternal("https://k8slens.dev/licenses/eula.md");
} }
} }
] ]

View File

@ -1,4 +1,4 @@
import path from "path" import path from "path";
const outputPath = path.resolve(__dirname, 'dist'); const outputPath = path.resolve(__dirname, 'dist');

View File

@ -1,6 +1,6 @@
import { LensRendererExtension } from "@k8slens/extensions" import { LensRendererExtension } from "@k8slens/extensions";
import { MetricsFeature } from "./src/metrics-feature" import { MetricsFeature } from "./src/metrics-feature";
import React from "react" import React from "react";
export default class ClusterMetricsFeatureExtension extends LensRendererExtension { export default class ClusterMetricsFeatureExtension extends LensRendererExtension {
clusterFeatures = [ clusterFeatures = [
@ -13,8 +13,8 @@ export default class ClusterMetricsFeatureExtension extends LensRendererExtensio
Enable timeseries data visualization (Prometheus stack) for your cluster. Enable timeseries data visualization (Prometheus stack) for your cluster.
Install this only if you don't have existing Prometheus stack installed. Install this only if you don't have existing Prometheus stack installed.
You can see preview of manifests <a href="https://github.com/lensapp/lens/tree/master/extensions/lens-metrics/resources" target="_blank">here</a>. You can see preview of manifests <a href="https://github.com/lensapp/lens/tree/master/extensions/lens-metrics/resources" target="_blank">here</a>.
</span> </span>
) );
} }
}, },
feature: new MetricsFeature() feature: new MetricsFeature()

View File

@ -1,6 +1,6 @@
import { ClusterFeature, Store, K8sApi } from "@k8slens/extensions" import { ClusterFeature, Store, K8sApi } from "@k8slens/extensions";
import semver from "semver" import semver from "semver";
import * as path from "path" import * as path from "path";
export interface MetricsConfiguration { export interface MetricsConfiguration {
// Placeholder for Metrics config structure // Placeholder for Metrics config structure
@ -51,46 +51,46 @@ export class MetricsFeature extends ClusterFeature.Feature {
async install(cluster: Store.Cluster): Promise<void> { async install(cluster: Store.Cluster): Promise<void> {
// 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.config.persistence.enabled = scs.some(sc => ( this.config.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'
)); ));
super.applyResources(cluster, super.renderTemplates(path.join(__dirname, "../resources/"))) super.applyResources(cluster, super.renderTemplates(path.join(__dirname, "../resources/")));
} }
async upgrade(cluster: Store.Cluster): Promise<void> { async upgrade(cluster: Store.Cluster): Promise<void> {
return this.install(cluster) return this.install(cluster);
} }
async updateStatus(cluster: Store.Cluster): Promise<ClusterFeature.FeatureStatus> { async updateStatus(cluster: Store.Cluster): Promise<ClusterFeature.FeatureStatus> {
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];
this.status.canUpgrade = semver.lt(this.status.currentVersion, this.latestVersion, true); this.status.canUpgrade = semver.lt(this.status.currentVersion, this.latestVersion, true);
} else { } else {
this.status.installed = false this.status.installed = false;
} }
} catch(e) { } catch(e) {
if (e?.error?.code === 404) { if (e?.error?.code === 404) {
this.status.installed = false this.status.installed = false;
} }
} }
return this.status return this.status;
} }
async uninstall(cluster: Store.Cluster): Promise<void> { async uninstall(cluster: Store.Cluster): Promise<void> {
const namespaceApi = K8sApi.forCluster(cluster, K8sApi.Namespace) const namespaceApi = K8sApi.forCluster(cluster, K8sApi.Namespace);
const clusterRoleBindingApi = K8sApi.forCluster(cluster, K8sApi.ClusterRoleBinding) const clusterRoleBindingApi = K8sApi.forCluster(cluster, K8sApi.ClusterRoleBinding);
const clusterRoleApi = K8sApi.forCluster(cluster, K8sApi.ClusterRole) const clusterRoleApi = K8sApi.forCluster(cluster, K8sApi.ClusterRole);
await namespaceApi.delete({name: "lens-metrics"}) await namespaceApi.delete({name: "lens-metrics"});
await clusterRoleBindingApi.delete({name: "lens-prometheus"}) await clusterRoleBindingApi.delete({name: "lens-prometheus"});
await clusterRoleApi.delete({name: "lens-prometheus"}) } await clusterRoleApi.delete({name: "lens-prometheus"}); }
} }

View File

@ -1,6 +1,6 @@
import { LensRendererExtension } from "@k8slens/extensions"; import { LensRendererExtension } from "@k8slens/extensions";
import React from "react" import React from "react";
import { NodeMenu, NodeMenuProps } from "./src/node-menu" import { NodeMenu, NodeMenuProps } from "./src/node-menu";
export default class NodeMenuRendererExtension extends LensRendererExtension { export default class NodeMenuRendererExtension extends LensRendererExtension {
kubeObjectMenuItems = [ kubeObjectMenuItems = [

View File

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { Component, K8sApi, Navigation} from "@k8slens/extensions" import { Component, K8sApi, Navigation} from "@k8slens/extensions";
export interface NodeMenuProps extends Component.KubeObjectMenuProps<K8sApi.Node> { export interface NodeMenuProps extends Component.KubeObjectMenuProps<K8sApi.Node> {
} }
@ -15,7 +15,7 @@ export function NodeMenu(props: NodeMenuProps) {
newTab: true, newTab: true,
}); });
Navigation.hideDetails(); Navigation.hideDetails();
} };
const shell = () => { const shell = () => {
Component.createTerminalTab({ Component.createTerminalTab({
@ -23,15 +23,15 @@ export function NodeMenu(props: NodeMenuProps) {
node: nodeName, node: nodeName,
}); });
Navigation.hideDetails(); Navigation.hideDetails();
} };
const cordon = () => { const cordon = () => {
sendToTerminal(`kubectl cordon ${nodeName}`); sendToTerminal(`kubectl cordon ${nodeName}`);
} };
const unCordon = () => { const unCordon = () => {
sendToTerminal(`kubectl uncordon ${nodeName}`) sendToTerminal(`kubectl uncordon ${nodeName}`);
} };
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`;
@ -43,8 +43,8 @@ export function NodeMenu(props: NodeMenuProps) {
Are you sure you want to drain <b>{nodeName}</b>? Are you sure you want to drain <b>{nodeName}</b>?
</p> </p>
), ),
}) });
} };
return ( return (
<> <>

View File

@ -1,7 +1,7 @@
import { LensRendererExtension } from "@k8slens/extensions"; import { LensRendererExtension } from "@k8slens/extensions";
import { PodShellMenu, PodShellMenuProps } from "./src/shell-menu" import { PodShellMenu, PodShellMenuProps } from "./src/shell-menu";
import { PodLogsMenu, PodLogsMenuProps } from "./src/logs-menu" import { PodLogsMenu, PodLogsMenuProps } from "./src/logs-menu";
import React from "react" import React from "react";
export default class PodMenuRendererExtension extends LensRendererExtension { export default class PodMenuRendererExtension extends LensRendererExtension {
kubeObjectMenuItems = [ kubeObjectMenuItems = [

View File

@ -19,7 +19,7 @@ export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
} }
render() { render() {
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;
@ -33,25 +33,25 @@ export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
<Component.SubMenu> <Component.SubMenu>
{ {
containers.map(container => { containers.map(container => {
const { name } = container const { name } = container;
const status = statuses.find(status => status.name === name); const status = statuses.find(status => status.name === name);
const brick = status ? ( const brick = status ? (
<Component.StatusBrick <Component.StatusBrick
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}
{name} {name}
</Component.MenuItem> </Component.MenuItem>
) );
}) })
} }
</Component.SubMenu> </Component.SubMenu>
</> </>
)} )}
</Component.MenuItem> </Component.MenuItem>
) );
} }
} }

View File

@ -9,16 +9,16 @@ export interface PodShellMenuProps extends Component.KubeObjectMenuProps<K8sApi.
export class PodShellMenu extends React.Component<PodShellMenuProps> { export class PodShellMenu extends React.Component<PodShellMenuProps> {
async execShell(container?: string) { async execShell(container?: string) {
Navigation.hideDetails(); Navigation.hideDetails();
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 {
command = `${command} sh -c "clear; (bash || ash || sh)"` command = `${command} sh -c "clear; (bash || ash || sh)"`;
} }
const shell = Component.createTerminalTab({ const shell = Component.createTerminalTab({
@ -32,7 +32,7 @@ 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 (
@ -51,13 +51,13 @@ export class PodShellMenu extends React.Component<PodShellMenuProps> {
<Component.StatusBrick/> <Component.StatusBrick/>
{name} {name}
</Component.MenuItem> </Component.MenuItem>
) );
}) })
} }
</Component.SubMenu> </Component.SubMenu>
</> </>
)} )}
</Component.MenuItem> </Component.MenuItem>
) );
} }
} }

View File

@ -1,8 +1,8 @@
// TODO: support localization / figure out how to extract / consume i18n strings // TODO: support localization / figure out how to extract / consume i18n strings
import "./support.scss" import "./support.scss";
import React from "react" import React from "react";
import { observer } from "mobx-react" import { observer } from "mobx-react";
import { App, Component } from "@k8slens/extensions"; import { App, Component } from "@k8slens/extensions";
@observer @observer

View File

@ -1,4 +1,4 @@
import path from "path" import path from "path";
const outputPath = path.resolve(__dirname, 'dist'); const outputPath = path.resolve(__dirname, 'dist');

View File

@ -1,18 +1,18 @@
import { LensMainExtension } from "@k8slens/extensions"; import { LensMainExtension } from "@k8slens/extensions";
import { telemetryPreferencesStore } from "./src/telemetry-preferences-store" import { telemetryPreferencesStore } from "./src/telemetry-preferences-store";
import { tracker } from "./src/tracker"; import { tracker } from "./src/tracker";
export default class TelemetryMainExtension extends LensMainExtension { export default class TelemetryMainExtension extends LensMainExtension {
async onActivate() { async onActivate() {
console.log("telemetry main extension activated") console.log("telemetry main extension activated");
tracker.start() tracker.start();
tracker.reportPeriodically() tracker.reportPeriodically();
await telemetryPreferencesStore.loadExtension(this) await telemetryPreferencesStore.loadExtension(this);
} }
onDeactivate() { onDeactivate() {
tracker.stop() tracker.stop();
console.log("telemetry main extension deactivated") console.log("telemetry main extension deactivated");
} }
} }

View File

@ -1,8 +1,8 @@
import { LensRendererExtension } from "@k8slens/extensions"; import { LensRendererExtension } from "@k8slens/extensions";
import { telemetryPreferencesStore } from "./src/telemetry-preferences-store" import { telemetryPreferencesStore } from "./src/telemetry-preferences-store";
import { TelemetryPreferenceHint, TelemetryPreferenceInput } from "./src/telemetry-preference" import { TelemetryPreferenceHint, TelemetryPreferenceInput } from "./src/telemetry-preference";
import { tracker } from "./src/tracker" import { tracker } from "./src/tracker";
import React from "react" import React from "react";
export default class TelemetryRendererExtension extends LensRendererExtension { export default class TelemetryRendererExtension extends LensRendererExtension {
appPreferences = [ appPreferences = [
@ -16,8 +16,8 @@ export default class TelemetryRendererExtension extends LensRendererExtension {
]; ];
async onActivate() { async onActivate() {
console.log("telemetry extension activated") console.log("telemetry extension activated");
tracker.start() tracker.start();
await telemetryPreferencesStore.loadExtension(this) await telemetryPreferencesStore.loadExtension(this);
} }
} }

View File

@ -1,19 +1,19 @@
import { Component } from "@k8slens/extensions" import { Component } from "@k8slens/extensions";
import React from "react" import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { TelemetryPreferencesStore } from "./telemetry-preferences-store" import { TelemetryPreferencesStore } from "./telemetry-preferences-store";
@observer @observer
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"
value={telemetry.enabled} value={telemetry.enabled}
onChange={v => { telemetry.enabled = v; }} onChange={v => { telemetry.enabled = v; }}
/> />
) );
} }
} }
@ -21,6 +21,6 @@ export class TelemetryPreferenceHint extends React.Component {
render() { render() {
return ( return (
<span>Telemetry & usage data is collected to continuously improve the Lens experience.</span> <span>Telemetry & usage data is collected to continuously improve the Lens experience.</span>
) );
} }
} }

View File

@ -1,5 +1,5 @@
import { Store } from "@k8slens/extensions"; import { Store } from "@k8slens/extensions";
import { toJS } from "mobx" import { toJS } from "mobx";
export type TelemetryPreferencesModel = { export type TelemetryPreferencesModel = {
enabled: boolean; enabled: boolean;
@ -14,11 +14,11 @@ export class TelemetryPreferencesStore extends Store.ExtensionStore<TelemetryPre
defaults: { defaults: {
enabled: true enabled: true
} }
}) });
} }
protected fromStore({ enabled }: TelemetryPreferencesModel): void { protected fromStore({ enabled }: TelemetryPreferencesModel): void {
this.enabled = enabled this.enabled = enabled;
} }
toJSON(): TelemetryPreferencesModel { toJSON(): TelemetryPreferencesModel {
@ -26,8 +26,8 @@ export class TelemetryPreferencesStore extends Store.ExtensionStore<TelemetryPre
enabled: this.enabled enabled: this.enabled
}, { }, {
recurseEverything: true recurseEverything: true
}) });
} }
} }
export const telemetryPreferencesStore = TelemetryPreferencesStore.getInstance<TelemetryPreferencesStore>() export const telemetryPreferencesStore = TelemetryPreferencesStore.getInstance<TelemetryPreferencesStore>();

View File

@ -1,8 +1,8 @@
import { EventBus, Util, Store, App } from "@k8slens/extensions" import { EventBus, Util, Store, App } from "@k8slens/extensions";
import ua from "universal-analytics" import ua from "universal-analytics";
import Analytics from "analytics-node" import Analytics from "analytics-node";
import { machineIdSync } from "node-machine-id" import { machineIdSync } from "node-machine-id";
import { telemetryPreferencesStore } from "./telemetry-preferences-store" import { telemetryPreferencesStore } from "./telemetry-preferences-store";
export class Tracker extends Util.Singleton { export class Tracker extends Util.Singleton {
static readonly GA_ID = "UA-159377374-1" static readonly GA_ID = "UA-159377374-1"
@ -23,69 +23,69 @@ export class Tracker extends Util.Singleton {
private constructor() { private constructor() {
super(); super();
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) {
this.visitor = ua(Tracker.GA_ID) this.visitor = ua(Tracker.GA_ID);
} }
this.analytics = new Analytics(Tracker.SEGMENT_KEY, { flushAt: 1 }) this.analytics = new Analytics(Tracker.SEGMENT_KEY, { flushAt: 1 });
this.visitor.set("dl", "https://telemetry.k8slens.dev") this.visitor.set("dl", "https://telemetry.k8slens.dev");
this.visitor.set("ua", this.userAgent) this.visitor.set("ua", this.userAgent);
} }
start() { start() {
if (this.started === true) { return } if (this.started === true) { return; }
this.started = true this.started = true;
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);
} }
reportPeriodically() { reportPeriodically() {
this.reportInterval = setInterval(() => { this.reportInterval = setInterval(() => {
this.reportData() this.reportData();
}, 60 * 60 * 1000) // report every 1h }, 60 * 60 * 1000); // report every 1h
} }
stop() { stop() {
if (!this.started) { return } if (!this.started) { return; }
this.started = false this.started = false;
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);
} }
} }
protected async isTelemetryAllowed(): Promise<boolean> { protected async isTelemetryAllowed(): Promise<boolean> {
return telemetryPreferencesStore.enabled return telemetryPreferencesStore.enabled;
} }
protected reportData() { protected reportData() {
const clustersList = Store.clusterStore.enabledClustersList const clustersList = Store.clusterStore.enabledClustersList;
this.event("generic-data", "report", { this.event("generic-data", "report", {
appVersion: App.version, appVersion: App.version,
os: this.os, os: this.os,
clustersCount: clustersList.length, clustersCount: clustersList.length,
workspacesCount: Store.workspaceStore.enabledWorkspacesList.length workspacesCount: Store.workspaceStore.enabledWorkspacesList.length
}) });
clustersList.forEach((cluster) => { clustersList.forEach((cluster) => {
if (!cluster?.metadata.lastSeen) { return } if (!cluster?.metadata.lastSeen) { return; }
this.reportClusterData(cluster) this.reportClusterData(cluster);
}) });
} }
protected reportClusterData(cluster: Store.ClusterModel) { protected reportClusterData(cluster: Store.ClusterModel) {
@ -96,26 +96,26 @@ export class Tracker extends Util.Singleton {
distribution: cluster.metadata.distribution, distribution: cluster.metadata.distribution,
nodesCount: cluster.metadata.nodes, nodesCount: cluster.metadata.nodes,
lastSeen: cluster.metadata.lastSeen lastSeen: cluster.metadata.lastSeen
}) });
} }
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 {
os += "; AppImage" os += "; AppImage";
} }
} 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 = {}) {
@ -128,7 +128,7 @@ export class Tracker extends Util.Singleton {
ec: eventCategory, ec: eventCategory,
ea: eventAction, ea: eventAction,
...otherParams, ...otherParams,
}).send() }).send();
this.analytics.track({ this.analytics.track({
anonymousId: this.anonymousId, anonymousId: this.anonymousId,
@ -141,9 +141,9 @@ export class Tracker extends Util.Singleton {
...otherParams, ...otherParams,
}, },
}) });
} catch (err) { } catch (err) {
console.error(`Failed to track "${eventCategory}:${eventAction}"`, err) console.error(`Failed to track "${eventCategory}:${eventAction}"`, err);
} }
} }
} }

View File

@ -4,159 +4,159 @@
TEST_NAMESPACE namespace. This is done to minimize destructive impact of the cluster tests on an existing minikube TEST_NAMESPACE namespace. This is done to minimize destructive impact of the cluster tests on an existing minikube
cluster and vice versa. cluster and vice versa.
*/ */
import { Application } from "spectron" import { Application } from "spectron";
import * as util from "../helpers/utils" import * as util from "../helpers/utils";
import { spawnSync } from "child_process" import { spawnSync } from "child_process";
const describeif = (condition: boolean) => condition ? describe : describe.skip const describeif = (condition: boolean) => condition ? describe : describe.skip;
const itif = (condition: boolean) => condition ? it : it.skip const itif = (condition: boolean) => condition ? it : it.skip;
jest.setTimeout(60000) jest.setTimeout(60000);
// FIXME (!): improve / simplify all css-selectors + use [data-test-id="some-id"] (already used in some tests below) // FIXME (!): improve / simplify all css-selectors + use [data-test-id="some-id"] (already used in some tests below)
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();
// Wait for splash screen to be closed // Wait for splash screen to be closed
while (await app.client.getWindowCount() > 1); while (await app.client.getWindowCount() > 1);
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");
} };
describe("app start", () => { describe("app start", () => {
beforeAll(appStart, 20000) beforeAll(appStart, 20000);
afterAll(async () => { afterAll(async () => {
if (app && app.isRunning()) { if (app && app.isRunning()) {
return util.tearDown(app) return util.tearDown(app);
} }
}) });
it('shows "whats new"', async () => { it('shows "whats new"', async () => {
await clickWhatsNew(app) await clickWhatsNew(app);
}) });
it('shows "add cluster"', async () => { it('shows "add cluster"', async () => {
await app.electron.ipcRenderer.send('test-menu-item-click', "File", "Add Cluster") await app.electron.ipcRenderer.send('test-menu-item-click', "File", "Add Cluster");
await app.client.waitUntilTextExists("h2", "Add Cluster") await app.client.waitUntilTextExists("h2", "Add Cluster");
}) });
describe("preferences page", () => { describe("preferences page", () => {
it('shows "preferences"', async () => { it('shows "preferences"', async () => {
let 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");
}) });
it('ensures helm repos', async () => { it('ensures helm repos', async () => {
await app.client.waitUntilTextExists("div.repos #message-stable", "stable") // wait for the helm-cli to fetch the stable repo await app.client.waitUntilTextExists("div.repos #message-stable", "stable"); // wait for the helm-cli to fetch the stable repo
await app.client.click("#HelmRepoSelect") // click the repo select to activate the drop-down await app.client.click("#HelmRepoSelect"); // click the repo select to activate the drop-down
await app.client.waitUntilTextExists("div.Select__option", "") // wait for at least one option to appear (any text) await app.client.waitUntilTextExists("div.Select__option", ""); // wait for at least one option to appear (any text)
}) });
}) });
it.skip('quits Lens"', async () => { it.skip('quits Lens"', async () => {
await app.client.keys(['Meta', 'Q']) await app.client.keys(['Meta', 'Q']);
await app.client.keys('Meta') await app.client.keys('Meta');
}) });
}) });
const minikubeReady = (): boolean => { const minikubeReady = (): boolean => {
// determine if minikube is running // determine if minikube is running
let status = spawnSync("minikube status", { shell: true }) let status = spawnSync("minikube status", { shell: true });
if (status.status !== 0) { if (status.status !== 0) {
console.warn("minikube not running") console.warn("minikube not running");
return false return false;
} }
// Remove TEST_NAMESPACE if it already exists // Remove TEST_NAMESPACE if it already exists
status = spawnSync(`minikube kubectl -- get namespace ${TEST_NAMESPACE}`, { shell: true }) status = spawnSync(`minikube kubectl -- get namespace ${TEST_NAMESPACE}`, { shell: true });
if (status.status === 0) { if (status.status === 0) {
console.warn(`Removing existing ${TEST_NAMESPACE} namespace`) console.warn(`Removing existing ${TEST_NAMESPACE} namespace`);
status = spawnSync(`minikube kubectl -- delete namespace ${TEST_NAMESPACE}`, { shell: true }) status = spawnSync(`minikube kubectl -- delete namespace ${TEST_NAMESPACE}`, { shell: true });
if (status.status !== 0) { if (status.status !== 0) {
console.warn(`Error removing ${TEST_NAMESPACE} namespace: ${status.stderr.toString()}`) console.warn(`Error removing ${TEST_NAMESPACE} namespace: ${status.stderr.toString()}`);
return false return false;
} }
console.log(status.stdout.toString()) console.log(status.stdout.toString());
} }
return true return true;
} };
const ready = minikubeReady() const ready = minikubeReady();
const addMinikubeCluster = async (app: Application) => { const addMinikubeCluster = async (app: Application) => {
await app.client.click("div.add-cluster") await app.client.click("div.add-cluster");
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"]`);
await app.client.frame("minikube") await app.client.frame("minikube");
await app.client.waitUntilTextExists("span.link-text", "Cluster") await app.client.waitUntilTextExists("span.link-text", "Cluster");
} };
describeif(ready)("cluster tests", () => { 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);
await waitForMinikubeDashboard(app) await waitForMinikubeDashboard(app);
await app.client.click('a[href="/nodes"]') await app.client.click('a[href="/nodes"]');
await app.client.waitUntilTextExists("div.TableCell", "Ready") await app.client.waitUntilTextExists("div.TableCell", "Ready");
} };
describe("cluster add", () => { describe("cluster add", () => {
beforeAll(appStart, 20000) beforeAll(appStart, 20000);
afterAll(async () => { afterAll(async () => {
if (app && app.isRunning()) { if (app && app.isRunning()) {
return util.tearDown(app) return util.tearDown(app);
} }
}) });
it('allows to add a cluster', async () => { it('allows to add a cluster', async () => {
await addCluster() await addCluster();
clusterAdded = true clusterAdded = true;
}) });
}) });
const appStartAddCluster = async () => { const appStartAddCluster = async () => {
if (clusterAdded) { if (clusterAdded) {
await appStart() await appStart();
await addCluster() await addCluster();
} }
} };
describe("cluster pages", () => { describe("cluster pages", () => {
beforeAll(appStartAddCluster, 40000) beforeAll(appStartAddCluster, 40000);
afterAll(async () => { afterAll(async () => {
if (app && app.isRunning()) { if (app && app.isRunning()) {
return util.tearDown(app) return util.tearDown(app);
} }
}) });
const tests: { const tests: {
drawer?: string drawer?: string
@ -394,119 +394,119 @@ describe("Lens integration tests", () => {
tests.forEach(({ drawer = "", drawerId = "", pages }) => { tests.forEach(({ drawer = "", drawerId = "", pages }) => {
if (drawer !== "") { if (drawer !== "") {
it(`shows ${drawer} drawer`, async () => { it(`shows ${drawer} drawer`, async () => {
expect(clusterAdded).toBe(true) expect(clusterAdded).toBe(true);
await app.client.click(`.sidebar-nav [data-test-id="${drawerId}"] span.link-text`) await app.client.click(`.sidebar-nav [data-test-id="${drawerId}"] span.link-text`);
await app.client.waitUntilTextExists(`a[href^="/${pages[0].href}"]`, pages[0].name) await app.client.waitUntilTextExists(`a[href^="/${pages[0].href}"]`, pages[0].name);
}) });
} }
pages.forEach(({ name, href, expectedSelector, expectedText }) => { pages.forEach(({ name, href, expectedSelector, expectedText }) => {
it(`shows ${drawer}->${name} page`, async () => { it(`shows ${drawer}->${name} page`, async () => {
expect(clusterAdded).toBe(true) expect(clusterAdded).toBe(true);
await app.client.click(`a[href^="/${href}"]`) await app.client.click(`a[href^="/${href}"]`);
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 () => {
expect(clusterAdded).toBe(true) expect(clusterAdded).toBe(true);
await app.client.click(`.sidebar-nav [data-test-id="${drawerId}"] span.link-text`) await app.client.click(`.sidebar-nav [data-test-id="${drawerId}"] span.link-text`);
await expect(app.client.waitUntilTextExists(`a[href^="/${pages[0].href}"]`, pages[0].name, 100)).rejects.toThrow() await expect(app.client.waitUntilTextExists(`a[href^="/${pages[0].href}"]`, pages[0].name, 100)).rejects.toThrow();
}) });
} }
}) });
}) });
describe("viewing pod logs", () => { describe("viewing pod logs", () => {
beforeEach(appStartAddCluster, 40000) beforeEach(appStartAddCluster, 40000);
afterEach(async () => { afterEach(async () => {
if (app && app.isRunning()) { if (app && app.isRunning()) {
return util.tearDown(app) return util.tearDown(app);
} }
}) });
it(`shows a logs for a pod`, async () => { it(`shows a logs for a pod`, async () => {
expect(clusterAdded).toBe(true) expect(clusterAdded).toBe(true);
// Go to Pods page // Go to Pods page
await app.client.click(".sidebar-nav [data-test-id='workloads'] span.link-text") await app.client.click(".sidebar-nav [data-test-id='workloads'] span.link-text");
await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods") await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods");
await app.client.click('a[href^="/pods"]') await app.client.click('a[href^="/pods"]');
await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver") await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver");
// Open logs tab in dock // Open logs tab in dock
await app.client.click(".list .TableRow:first-child") await app.client.click(".list .TableRow:first-child");
await app.client.waitForVisible(".Drawer") await app.client.waitForVisible(".Drawer");
await app.client.click(".drawer-title .Menu li:nth-child(2)") await app.client.click(".drawer-title .Menu li:nth-child(2)");
// Check if controls are available // Check if controls are available
await app.client.waitForVisible(".PodLogs .VirtualList") await app.client.waitForVisible(".PodLogs .VirtualList");
await app.client.waitForVisible(".PodLogControls") await app.client.waitForVisible(".PodLogControls");
await app.client.waitForVisible(".PodLogControls .SearchInput") await app.client.waitForVisible(".PodLogControls .SearchInput");
await app.client.waitForVisible(".PodLogControls .SearchInput input") await app.client.waitForVisible(".PodLogControls .SearchInput input");
// Search for semicolon // Search for semicolon
await app.client.keys(":") await app.client.keys(":");
await app.client.waitForVisible(".PodLogs .list span.active") await app.client.waitForVisible(".PodLogs .list span.active");
// Click through controls // Click through controls
await app.client.click(".PodLogControls .timestamps-icon") await app.client.click(".PodLogControls .timestamps-icon");
await app.client.click(".PodLogControls .undo-icon") await app.client.click(".PodLogControls .undo-icon");
}) });
}) });
describe("cluster operations", () => { describe("cluster operations", () => {
beforeEach(appStartAddCluster, 40000) beforeEach(appStartAddCluster, 40000);
afterEach(async () => { afterEach(async () => {
if (app && app.isRunning()) { if (app && app.isRunning()) {
return util.tearDown(app) return util.tearDown(app);
} }
}) });
it('shows default namespace', async () => { it('shows default namespace', async () => {
expect(clusterAdded).toBe(true) expect(clusterAdded).toBe(true);
await app.client.click('a[href="/namespaces"]') await app.client.click('a[href="/namespaces"]');
await app.client.waitUntilTextExists("div.TableCell", "default") await app.client.waitUntilTextExists("div.TableCell", "default");
await app.client.waitUntilTextExists("div.TableCell", "kube-system") await app.client.waitUntilTextExists("div.TableCell", "kube-system");
}) });
it(`creates ${TEST_NAMESPACE} namespace`, async () => { it(`creates ${TEST_NAMESPACE} namespace`, async () => {
expect(clusterAdded).toBe(true) expect(clusterAdded).toBe(true);
await app.client.click('a[href="/namespaces"]') await app.client.click('a[href="/namespaces"]');
await app.client.waitUntilTextExists("div.TableCell", "default") await app.client.waitUntilTextExists("div.TableCell", "default");
await app.client.waitUntilTextExists("div.TableCell", "kube-system") await app.client.waitUntilTextExists("div.TableCell", "kube-system");
await app.client.click("button.add-button") await app.client.click("button.add-button");
await app.client.waitUntilTextExists("div.AddNamespaceDialog", "Create Namespace") await app.client.waitUntilTextExists("div.AddNamespaceDialog", "Create Namespace");
await app.client.keys(`${TEST_NAMESPACE}\n`) await app.client.keys(`${TEST_NAMESPACE}\n`);
await app.client.waitForExist(`.name=${TEST_NAMESPACE}`) await app.client.waitForExist(`.name=${TEST_NAMESPACE}`);
}) });
it(`creates a pod in ${TEST_NAMESPACE} namespace`, async () => { it(`creates a pod in ${TEST_NAMESPACE} namespace`, async () => {
expect(clusterAdded).toBe(true) expect(clusterAdded).toBe(true);
await app.client.click(".sidebar-nav [data-test-id='workloads'] span.link-text") await app.client.click(".sidebar-nav [data-test-id='workloads'] span.link-text");
await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods") await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods");
await app.client.click('a[href^="/pods"]') await app.client.click('a[href^="/pods"]');
await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver") await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver");
await app.client.click('.Icon.new-dock-tab') await app.client.click('.Icon.new-dock-tab');
await app.client.waitUntilTextExists("li.MenuItem.create-resource-tab", "Create resource") await app.client.waitUntilTextExists("li.MenuItem.create-resource-tab", "Create resource");
await app.client.click("li.MenuItem.create-resource-tab") await app.client.click("li.MenuItem.create-resource-tab");
await app.client.waitForVisible(".CreateResource div.ace_content") await app.client.waitForVisible(".CreateResource div.ace_content");
// Write pod manifest to editor // Write pod manifest to editor
await app.client.keys("apiVersion: v1\n") await app.client.keys("apiVersion: v1\n");
await app.client.keys("kind: Pod\n") await app.client.keys("kind: Pod\n");
await app.client.keys("metadata:\n") await app.client.keys("metadata:\n");
await app.client.keys(" name: nginx-create-pod-test\n") await app.client.keys(" name: nginx-create-pod-test\n");
await app.client.keys(`namespace: ${TEST_NAMESPACE}\n`) await app.client.keys(`namespace: ${TEST_NAMESPACE}\n`);
await app.client.keys(BACKSPACE + "spec:\n") await app.client.keys(BACKSPACE + "spec:\n");
await app.client.keys(" containers:\n") await app.client.keys(" containers:\n");
await app.client.keys("- name: nginx-create-pod-test\n") await app.client.keys("- name: nginx-create-pod-test\n");
await app.client.keys(" image: nginx:alpine\n") await app.client.keys(" image: nginx:alpine\n");
// Create deployment // Create deployment
await app.client.waitForEnabled("button.Button=Create & Close") await app.client.waitForEnabled("button.Button=Create & Close");
await app.client.click("button.Button=Create & Close") await app.client.click("button.Button=Create & Close");
// Wait until first bits of pod appears on dashboard // Wait until first bits of pod appears on dashboard
await app.client.waitForExist(".name=nginx-create-pod-test") await app.client.waitForExist(".name=nginx-create-pod-test");
// Open pod details // Open pod details
await app.client.click(".name=nginx-create-pod-test") await app.client.click(".name=nginx-create-pod-test");
await app.client.waitUntilTextExists("div.drawer-title-text", "Pod: nginx-create-pod-test") await app.client.waitUntilTextExists("div.drawer-title-text", "Pod: nginx-create-pod-test");
}) });
}) });
}) });
}) });

View File

@ -4,7 +4,7 @@ const AppPaths: Partial<Record<NodeJS.Platform, string>> = {
"win32": "./dist/win-unpacked/Lens.exe", "win32": "./dist/win-unpacked/Lens.exe",
"linux": "./dist/linux-unpacked/kontena-lens", "linux": "./dist/linux-unpacked/kontena-lens",
"darwin": "./dist/mac/Lens.app/Contents/MacOS/Lens", "darwin": "./dist/mac/Lens.app/Contents/MacOS/Lens",
} };
export function setup(): Application { export function setup(): Application {
return new Application({ return new Application({
@ -16,16 +16,16 @@ export function setup(): Application {
env: { env: {
CICD: "true" CICD: "true"
} }
}) });
} }
export async function tearDown(app: Application) { export async function tearDown(app: Application) {
let mpid: any = app.mainProcess.pid const mpid: any = app.mainProcess.pid;
let pid = await mpid() const pid = await mpid();
await app.stop() await app.stop();
try { try {
process.kill(pid, "SIGKILL"); process.kill(pid, "SIGKILL");
} catch (e) { } catch (e) {
console.error(e) console.error(e);
} }
} }

View File

@ -37,7 +37,8 @@
"download:kubectl": "yarn run ts-node build/download_kubectl.ts", "download:kubectl": "yarn run ts-node build/download_kubectl.ts",
"download:helm": "yarn run ts-node build/download_helm.ts", "download:helm": "yarn run ts-node build/download_helm.ts",
"build:tray-icons": "yarn run ts-node build/build_tray_icon.ts", "build:tray-icons": "yarn run ts-node build/build_tray_icon.ts",
"lint": "yarn run eslint $@ --ext js,ts,tsx --max-warnings=0 src/", "lint": "yarn run eslint $@ --ext js,ts,tsx --max-warnings=0 src/ integration/ __mocks__/ build/",
"lint:fix": "yarn run lint --fix",
"mkdocs-serve-local": "docker build -t mkdocs-serve-local:latest mkdocs/ && docker run --rm -it -p 8000:8000 -v ${PWD}:/docs mkdocs-serve-local:latest", "mkdocs-serve-local": "docker build -t mkdocs-serve-local:latest mkdocs/ && docker run --rm -it -p 8000:8000 -v ${PWD}:/docs mkdocs-serve-local:latest",
"typedocs-extensions-api": "yarn run typedoc --ignoreCompilerErrors --readme docs/extensions/typedoc-readme.md.tpl --name @k8slens/extensions --out docs/extensions/api --mode library --excludePrivate --hideBreadcrumbs --includes src/ src/extensions/extension-api.ts" "typedocs-extensions-api": "yarn run typedoc --ignoreCompilerErrors --readme docs/extensions/typedoc-readme.md.tpl --name @k8slens/extensions --out docs/extensions/api --mode library --excludePrivate --hideBreadcrumbs --includes src/ src/extensions/extension-api.ts"
}, },

View File

@ -5,9 +5,9 @@ import { Cluster } from "../../main/cluster";
import { ClusterStore } from "../cluster-store"; import { ClusterStore } from "../cluster-store";
import { workspaceStore } from "../workspace-store"; import { workspaceStore } from "../workspace-store";
const testDataIcon = fs.readFileSync("test-data/cluster-store-migration-icon.png") const testDataIcon = fs.readFileSync("test-data/cluster-store-migration-icon.png");
console.log("") // fix bug console.log(""); // fix bug
let clusterStore: ClusterStore; let clusterStore: ClusterStore;
@ -18,15 +18,15 @@ describe("empty config", () => {
'tmp': { 'tmp': {
'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();
}) });
afterEach(() => { afterEach(() => {
mockFs.restore(); mockFs.restore();
}) });
describe("with foo cluster added", () => { describe("with foo cluster added", () => {
beforeEach(() => { beforeEach(() => {
@ -43,30 +43,30 @@ describe("empty config", () => {
workspace: workspaceStore.currentWorkspaceId workspace: workspaceStore.currentWorkspaceId
}) })
); );
}) });
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");
}) });
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");
}) });
it("removes cluster from store", async () => { it("removes cluster from store", async () => {
await clusterStore.removeById("foo"); await clusterStore.removeById("foo");
expect(clusterStore.getById("foo")).toBeUndefined(); expect(clusterStore.getById("foo")).toBeUndefined();
}) });
it("sets active cluster", () => { it("sets active cluster", () => {
clusterStore.setActive("foo"); clusterStore.setActive("foo");
expect(clusterStore.active.id).toBe("foo"); expect(clusterStore.active.id).toBe("foo");
}) });
}) });
describe("with prod and dev clusters added", () => { describe("with prod and dev clusters added", () => {
beforeEach(() => { beforeEach(() => {
@ -89,8 +89,8 @@ describe("empty config", () => {
kubeConfigPath: ClusterStore.embedCustomKubeConfig("dev", "fancy config"), kubeConfigPath: ClusterStore.embedCustomKubeConfig("dev", "fancy config"),
workspace: "workstation" workspace: "workstation"
}) })
) );
}) });
it("check if store can contain multiple clusters", () => { it("check if store can contain multiple clusters", () => {
expect(clusterStore.hasClusters()).toBeTruthy(); expect(clusterStore.hasClusters()).toBeTruthy();
@ -104,42 +104,42 @@ describe("empty config", () => {
expect(wsClusters.length).toBe(2); expect(wsClusters.length).toBe(2);
expect(wsClusters[0].id).toBe("prod"); expect(wsClusters[0].id).toBe("prod");
expect(wsClusters[1].id).toBe("dev"); expect(wsClusters[1].id).toBe("dev");
}) });
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");
}) });
it("check if reorderring works for same from and to", () => { it("check if reorderring works for same from and to", () => {
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");
expect(clusters[1].preferences.iconOrder).toBe(1) expect(clusters[1].preferences.iconOrder).toBe(1);
}) });
it("check if reorderring works for different from and to", () => { it("check if reorderring works for different from and to", () => {
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");
expect(clusters[1].preferences.iconOrder).toBe(1) expect(clusters[1].preferences.iconOrder).toBe(1);
}) });
it("check if after icon reordering, changing workspaces still works", () => { it("check if after icon reordering, changing workspaces still works", () => {
clusterStore.swapIconOrders("workstation", 1, 1) clusterStore.swapIconOrders("workstation", 1, 1);
clusterStore.getById("prod").workspace = "default" clusterStore.getById("prod").workspace = "default";
expect(clusterStore.getByWorkspaceId("workstation").length).toBe(1); expect(clusterStore.getByWorkspaceId("workstation").length).toBe(1);
expect(clusterStore.getByWorkspaceId("default").length).toBe(1); expect(clusterStore.getByWorkspaceId("default").length).toBe(1);
}) });
}) });
}) });
describe("config with existing clusters", () => { describe("config with existing clusters", () => {
beforeEach(() => { beforeEach(() => {
@ -176,21 +176,21 @@ describe("config with existing clusters", () => {
] ]
}) })
} }
} };
mockFs(mockOpts); mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>(); clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load(); return clusterStore.load();
}) });
afterEach(() => { afterEach(() => {
mockFs.restore(); mockFs.restore();
}) });
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');
}) });
it("allows to delete a cluster", () => { it("allows to delete a cluster", () => {
clusterStore.removeById('cluster2'); clusterStore.removeById('cluster2');
@ -198,18 +198,18 @@ describe("config with existing clusters", () => {
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');
expect(storedClusters[1].id).toBe('cluster2') expect(storedClusters[1].id).toBe('cluster2');
expect(storedClusters[1].preferences.terminalCWD).toBe('/foo2') expect(storedClusters[1].preferences.terminalCWD).toBe('/foo2');
expect(storedClusters[2].id).toBe('cluster3') expect(storedClusters[2].id).toBe('cluster3');
}) });
}) });
describe("pre 2.0 config with an existing cluster", () => { describe("pre 2.0 config with an existing cluster", () => {
beforeEach(() => { beforeEach(() => {
@ -229,17 +229,17 @@ 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();
}) });
afterEach(() => { afterEach(() => {
mockFs.restore(); mockFs.restore();
}) });
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");
}) });
}) });
describe("pre 2.6.0 config with a cluster that has arrays in auth config", () => { describe("pre 2.6.0 config with a cluster that has arrays in auth config", () => {
beforeEach(() => { beforeEach(() => {
@ -257,15 +257,15 @@ 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();
}) });
afterEach(() => { afterEach(() => {
mockFs.restore(); mockFs.restore();
}) });
it("replaces array format access token and expiry into string", async () => { it("replaces array format access token and expiry into string", async () => {
const file = clusterStore.clustersList[0].kubeConfigPath; const file = clusterStore.clustersList[0].kubeConfigPath;
@ -273,8 +273,8 @@ describe("pre 2.6.0 config with a cluster that has arrays in auth config", () =>
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");
}) });
}) });
describe("pre 2.6.0 config with a cluster icon", () => { describe("pre 2.6.0 config with a cluster icon", () => {
beforeEach(() => { beforeEach(() => {
@ -297,23 +297,23 @@ 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();
}) });
afterEach(() => { afterEach(() => {
mockFs.restore(); mockFs.restore();
}) });
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);
}) });
}) });
describe("for a pre 2.7.0-beta.0 config without a workspace", () => { describe("for a pre 2.7.0-beta.0 config without a workspace", () => {
beforeEach(() => { beforeEach(() => {
@ -334,21 +334,21 @@ 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();
}) });
afterEach(() => { afterEach(() => {
mockFs.restore(); mockFs.restore();
}) });
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');
}) });
}) });
describe("pre 3.6.0-beta.1 config with an existing cluster", () => { describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
beforeEach(() => { beforeEach(() => {
@ -378,19 +378,19 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
mockFs(mockOpts); mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>(); clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load(); return clusterStore.load();
}) });
afterEach(() => { afterEach(() => {
mockFs.restore(); mockFs.restore();
}) });
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

@ -1,15 +1,15 @@
import { appEventBus, AppEvent } from "../event-bus" import { appEventBus, AppEvent } from "../event-bus";
describe("event bus tests", () => { 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;
}) });
appEventBus.emit({name: "foo", action: "bar"}) appEventBus.emit({name: "foo", action: "bar"});
expect(event.name).toBe("foo") expect(event.name).toBe("foo");
}) });
}) });
}) });

View File

@ -2,7 +2,7 @@
* @jest-environment jsdom * @jest-environment jsdom
*/ */
import { SearchStore } from "../search-store" import { SearchStore } from "../search-store";
let searchStore: SearchStore = null; let searchStore: SearchStore = null;
@ -10,17 +10,17 @@ 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",
"1:M 30 Oct 2020 16:17:41.623 * Starting Partial resynchronization request from 172.17.0.12:6379 accepted. Sending 0 bytes of backlog starting from offset 14407." "1:M 30 Oct 2020 16:17:41.623 * Starting Partial resynchronization request from 172.17.0.12:6379 accepted. Sending 0 bytes of backlog starting from offset 14407."
] ];
describe("search store tests", () => { describe("search store tests", () => {
beforeEach(async () => { beforeEach(async () => {
searchStore = new SearchStore(); searchStore = new SearchStore();
}) });
it("does nothing with empty search query", () => { it("does nothing with empty search query", () => {
searchStore.onSearch([], ""); searchStore.onSearch([], "");
expect(searchStore.occurrences).toEqual([]); expect(searchStore.occurrences).toEqual([]);
}) });
it("doesn't break if no text provided", () => { it("doesn't break if no text provided", () => {
searchStore.onSearch(null, "replica"); searchStore.onSearch(null, "replica");
@ -28,53 +28,53 @@ describe("search store tests", () => {
searchStore.onSearch([], "replica"); searchStore.onSearch([], "replica");
expect(searchStore.occurrences).toEqual([]); expect(searchStore.occurrences).toEqual([]);
}) });
it("find 3 occurences across 3 lines", () => { it("find 3 occurences across 3 lines", () => {
searchStore.onSearch(logs, "172"); searchStore.onSearch(logs, "172");
expect(searchStore.occurrences).toEqual([0, 1, 2]); expect(searchStore.occurrences).toEqual([0, 1, 2]);
}) });
it("find occurences within 1 line (case-insensitive)", () => { it("find occurences within 1 line (case-insensitive)", () => {
searchStore.onSearch(logs, "Starting"); searchStore.onSearch(logs, "Starting");
expect(searchStore.occurrences).toEqual([2, 2]); expect(searchStore.occurrences).toEqual([2, 2]);
}) });
it("sets overlay index equal to first occurence", () => { it("sets overlay index equal to first occurence", () => {
searchStore.onSearch(logs, "Replica"); searchStore.onSearch(logs, "Replica");
expect(searchStore.activeOverlayIndex).toBe(0); expect(searchStore.activeOverlayIndex).toBe(0);
}) });
it("set overlay index to next occurence", () => { it("set overlay index to next occurence", () => {
searchStore.onSearch(logs, "172"); searchStore.onSearch(logs, "172");
searchStore.setNextOverlayActive(); searchStore.setNextOverlayActive();
expect(searchStore.activeOverlayIndex).toBe(1); expect(searchStore.activeOverlayIndex).toBe(1);
}) });
it("sets overlay to last occurence", () => { it("sets overlay to last occurence", () => {
searchStore.onSearch(logs, "172"); searchStore.onSearch(logs, "172");
searchStore.setPrevOverlayActive(); searchStore.setPrevOverlayActive();
expect(searchStore.activeOverlayIndex).toBe(2); expect(searchStore.activeOverlayIndex).toBe(2);
}) });
it("gets line index where overlay is located", () => { it("gets line index where overlay is located", () => {
searchStore.onSearch(logs, "synchronization"); searchStore.onSearch(logs, "synchronization");
expect(searchStore.activeOverlayLine).toBe(1); expect(searchStore.activeOverlayLine).toBe(1);
}) });
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\\\\\\#\\?\\(\\)\\[\\]");
}) });
it("gets active find number", () => { it("gets active find number", () => {
searchStore.onSearch(logs, "172"); searchStore.onSearch(logs, "172");
searchStore.setNextOverlayActive(); searchStore.setNextOverlayActive();
expect(searchStore.activeFind).toBe(2); expect(searchStore.activeFind).toBe(2);
}) });
it("gets total finds number", () => { it("gets total finds number", () => {
searchStore.onSearch(logs, "Starting"); searchStore.onSearch(logs, "Starting");
expect(searchStore.totalFinds).toBe(2); expect(searchStore.totalFinds).toBe(2);
}) });
}) });

View File

@ -1,4 +1,4 @@
import mockFs from "mock-fs" import mockFs from "mock-fs";
jest.mock("electron", () => { jest.mock("electron", () => {
return { return {
@ -7,55 +7,55 @@ jest.mock("electron", () => {
getPath: () => 'tmp', getPath: () => 'tmp',
getLocale: () => 'en' getLocale: () => 'en'
} }
} };
}) });
import { UserStore } from "../user-store" import { UserStore } from "../user-store";
import { SemVer } from "semver" import { SemVer } from "semver";
import electron from "electron" import electron from "electron";
describe("user store tests", () => { describe("user store tests", () => {
describe("for an empty config", () => { describe("for an empty config", () => {
beforeEach(() => { beforeEach(() => {
UserStore.resetInstance() UserStore.resetInstance();
mockFs({ tmp: { 'config.json': "{}" } }) mockFs({ tmp: { 'config.json': "{}" } });
}) });
afterEach(() => { afterEach(() => {
mockFs.restore() mockFs.restore();
}) });
it("allows setting and retrieving lastSeenAppVersion", () => { it("allows setting and retrieving lastSeenAppVersion", () => {
const us = UserStore.getInstance<UserStore>(); const us = UserStore.getInstance<UserStore>();
us.lastSeenAppVersion = "1.2.3"; us.lastSeenAppVersion = "1.2.3";
expect(us.lastSeenAppVersion).toBe("1.2.3"); expect(us.lastSeenAppVersion).toBe("1.2.3");
}) });
it("allows adding and listing seen contexts", () => { it("allows adding and listing seen contexts", () => {
const us = UserStore.getInstance<UserStore>(); const us = UserStore.getInstance<UserStore>();
us.seenContexts.add('foo') us.seenContexts.add('foo');
expect(us.seenContexts.size).toBe(1) expect(us.seenContexts.size).toBe(1);
us.seenContexts.add('foo') us.seenContexts.add('foo');
us.seenContexts.add('bar') us.seenContexts.add('bar');
expect(us.seenContexts.size).toBe(2) // check 'foo' isn't added twice expect(us.seenContexts.size).toBe(2); // check 'foo' isn't added twice
expect(us.seenContexts.has('foo')).toBe(true) expect(us.seenContexts.has('foo')).toBe(true);
expect(us.seenContexts.has('bar')).toBe(true) expect(us.seenContexts.has('bar')).toBe(true);
}) });
it("allows setting and getting preferences", () => { it("allows setting and getting preferences", () => {
const us = UserStore.getInstance<UserStore>(); const us = UserStore.getInstance<UserStore>();
us.preferences.httpsProxy = 'abcd://defg'; us.preferences.httpsProxy = 'abcd://defg';
expect(us.preferences.httpsProxy).toBe('abcd://defg') expect(us.preferences.httpsProxy).toBe('abcd://defg');
expect(us.preferences.colorTheme).toBe(UserStore.defaultTheme) expect(us.preferences.colorTheme).toBe(UserStore.defaultTheme);
us.preferences.colorTheme = "light"; us.preferences.colorTheme = "light";
expect(us.preferences.colorTheme).toBe('light') expect(us.preferences.colorTheme).toBe('light');
}) });
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>();
@ -64,7 +64,7 @@ describe("user store tests", () => {
us.preferences.colorTheme = "some other theme"; us.preferences.colorTheme = "some other theme";
await us.resetTheme(); await us.resetTheme();
expect(us.preferences.colorTheme).toBe(UserStore.defaultTheme); expect(us.preferences.colorTheme).toBe(UserStore.defaultTheme);
}) });
it("correctly calculates if the last seen version is an old release", () => { it("correctly calculates if the last seen version is an old release", () => {
const us = UserStore.getInstance<UserStore>(); const us = UserStore.getInstance<UserStore>();
@ -73,12 +73,12 @@ describe("user store tests", () => {
us.lastSeenAppVersion = (new SemVer(electron.app.getVersion())).inc("major").format(); us.lastSeenAppVersion = (new SemVer(electron.app.getVersion())).inc("major").format();
expect(us.isNewVersion).toBe(false); expect(us.isNewVersion).toBe(false);
}) });
}) });
describe("migrations", () => { describe("migrations", () => {
beforeEach(() => { beforeEach(() => {
UserStore.resetInstance() UserStore.resetInstance();
mockFs({ mockFs({
'tmp': { 'tmp': {
'config.json': JSON.stringify({ 'config.json': JSON.stringify({
@ -87,17 +87,17 @@ describe("user store tests", () => {
lastSeenAppVersion: '1.2.3' lastSeenAppVersion: '1.2.3'
}) })
} }
}) });
}) });
afterEach(() => { afterEach(() => {
mockFs.restore() mockFs.restore();
}) });
it("sets last seen app version to 0.0.0", () => { it("sets last seen app version to 0.0.0", () => {
const us = UserStore.getInstance<UserStore>(); const us = UserStore.getInstance<UserStore>();
expect(us.lastSeenAppVersion).toBe('0.0.0') expect(us.lastSeenAppVersion).toBe('0.0.0');
}) });
}) });
}) });

View File

@ -1,4 +1,4 @@
import mockFs from "mock-fs" import mockFs from "mock-fs";
jest.mock("electron", () => { jest.mock("electron", () => {
return { return {
@ -7,36 +7,36 @@ jest.mock("electron", () => {
getPath: () => 'tmp', getPath: () => 'tmp',
getLocale: () => 'en' getLocale: () => 'en'
} }
} };
}) });
import { Workspace, WorkspaceStore } from "../workspace-store" import { Workspace, WorkspaceStore } from "../workspace-store";
describe("workspace store tests", () => { describe("workspace store tests", () => {
describe("for an empty config", () => { describe("for an empty config", () => {
beforeEach(async () => { beforeEach(async () => {
WorkspaceStore.resetInstance() WorkspaceStore.resetInstance();
mockFs({ tmp: { 'lens-workspace-store.json': "{}" } }) mockFs({ tmp: { 'lens-workspace-store.json': "{}" } });
await WorkspaceStore.getInstance<WorkspaceStore>().load(); await WorkspaceStore.getInstance<WorkspaceStore>().load();
}) });
afterEach(() => { afterEach(() => {
mockFs.restore() mockFs.restore();
}) });
it("default workspace should always exist", () => { it("default workspace should always exist", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>(); const ws = WorkspaceStore.getInstance<WorkspaceStore>();
expect(ws.workspaces.size).toBe(1); expect(ws.workspaces.size).toBe(1);
expect(ws.getById(WorkspaceStore.defaultId)).not.toBe(null); expect(ws.getById(WorkspaceStore.defaultId)).not.toBe(null);
}) });
it("cannot remove the default workspace", () => { it("cannot remove the default workspace", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>(); const ws = WorkspaceStore.getInstance<WorkspaceStore>();
expect(() => ws.removeWorkspaceById(WorkspaceStore.defaultId)).toThrowError("Cannot remove"); expect(() => ws.removeWorkspaceById(WorkspaceStore.defaultId)).toThrowError("Cannot remove");
}) });
it("can update workspace description", () => { it("can update workspace description", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>(); const ws = WorkspaceStore.getInstance<WorkspaceStore>();
@ -50,7 +50,7 @@ describe("workspace store tests", () => {
ws.updateWorkspace(workspace); ws.updateWorkspace(workspace);
expect(ws.getById("foobar").description).toBe("Foobar description"); expect(ws.getById("foobar").description).toBe("Foobar description");
}) });
it("can add workspaces", () => { it("can add workspaces", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>(); const ws = WorkspaceStore.getInstance<WorkspaceStore>();
@ -61,13 +61,13 @@ describe("workspace store tests", () => {
})); }));
expect(ws.getById("123").name).toBe("foobar"); expect(ws.getById("123").name).toBe("foobar");
}) });
it("cannot set a non-existent workspace to be active", () => { it("cannot set a non-existent workspace to be active", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>(); const ws = WorkspaceStore.getInstance<WorkspaceStore>();
expect(() => ws.setActive("abc")).toThrow("doesn't exist"); expect(() => ws.setActive("abc")).toThrow("doesn't exist");
}) });
it("can set a existent workspace to be active", () => { it("can set a existent workspace to be active", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>(); const ws = WorkspaceStore.getInstance<WorkspaceStore>();
@ -78,7 +78,7 @@ describe("workspace store tests", () => {
})); }));
expect(() => ws.setActive("abc")).not.toThrowError(); expect(() => ws.setActive("abc")).not.toThrowError();
}) });
it("can remove a workspace", () => { it("can remove a workspace", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>(); const ws = WorkspaceStore.getInstance<WorkspaceStore>();
@ -94,7 +94,7 @@ describe("workspace store tests", () => {
ws.removeWorkspaceById("123"); ws.removeWorkspaceById("123");
expect(ws.workspaces.size).toBe(2); expect(ws.workspaces.size).toBe(2);
}) });
it("cannot create workspace with existent name", () => { it("cannot create workspace with existent name", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>(); const ws = WorkspaceStore.getInstance<WorkspaceStore>();
@ -105,7 +105,7 @@ describe("workspace store tests", () => {
})); }));
expect(ws.workspacesList.length).toBe(1); // default workspace only expect(ws.workspacesList.length).toBe(1); // default workspace only
}) });
it("cannot create workspace with empty name", () => { it("cannot create workspace with empty name", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>(); const ws = WorkspaceStore.getInstance<WorkspaceStore>();
@ -116,7 +116,7 @@ describe("workspace store tests", () => {
})); }));
expect(ws.workspacesList.length).toBe(1); // default workspace only expect(ws.workspacesList.length).toBe(1); // default workspace only
}) });
it("cannot create workspace with ' ' name", () => { it("cannot create workspace with ' ' name", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>(); const ws = WorkspaceStore.getInstance<WorkspaceStore>();
@ -127,7 +127,7 @@ describe("workspace store tests", () => {
})); }));
expect(ws.workspacesList.length).toBe(1); // default workspace only expect(ws.workspacesList.length).toBe(1); // default workspace only
}) });
it("trim workspace name", () => { it("trim workspace name", () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>(); const ws = WorkspaceStore.getInstance<WorkspaceStore>();
@ -138,12 +138,12 @@ describe("workspace store tests", () => {
})); }));
expect(ws.workspacesList.length).toBe(1); // default workspace only expect(ws.workspacesList.length).toBe(1); // default workspace only
}) });
}) });
describe("for a non-empty config", () => { describe("for a non-empty config", () => {
beforeEach(async () => { beforeEach(async () => {
WorkspaceStore.resetInstance() WorkspaceStore.resetInstance();
mockFs({ mockFs({
tmp: { tmp: {
'lens-workspace-store.json': JSON.stringify({ 'lens-workspace-store.json': JSON.stringify({
@ -157,19 +157,19 @@ describe("workspace store tests", () => {
}] }]
}) })
} }
}) });
await WorkspaceStore.getInstance<WorkspaceStore>().load(); await WorkspaceStore.getInstance<WorkspaceStore>().load();
}) });
afterEach(() => { afterEach(() => {
mockFs.restore() mockFs.restore();
}) });
it("doesn't revert to default workspace", async () => { it("doesn't revert to default workspace", async () => {
const ws = WorkspaceStore.getInstance<WorkspaceStore>(); const ws = WorkspaceStore.getInstance<WorkspaceStore>();
expect(ws.currentWorkspaceId).toBe("abc"); expect(ws.currentWorkspaceId).toBe("abc");
}) });
}) });
}) });

View File

@ -1,7 +1,7 @@
import path from "path" import path from "path";
import Config from "conf" import Config from "conf";
import { Options as ConfOptions } from "conf/dist/source/types" import { Options as ConfOptions } from "conf/dist/source/types";
import { app, ipcMain, IpcMainEvent, ipcRenderer, IpcRendererEvent, remote } from "electron" import { app, ipcMain, IpcMainEvent, ipcRenderer, IpcRendererEvent, remote } from "electron";
import { action, IReactionOptions, observable, reaction, runInAction, toJS, when } from "mobx"; import { action, IReactionOptions, observable, reaction, runInAction, toJS, when } from "mobx";
import Singleton from "./utils/singleton"; import Singleton from "./utils/singleton";
import { getAppVersion } from "./utils/app-version"; import { getAppVersion } from "./utils/app-version";
@ -32,7 +32,7 @@ export abstract class BaseStore<T = any> extends Singleton {
autoLoad: false, autoLoad: false,
syncEnabled: true, syncEnabled: true,
...params, ...params,
} };
this.init(); this.init();
} }
@ -41,11 +41,11 @@ export abstract class BaseStore<T = any> extends Singleton {
} }
protected get syncRendererChannel() { protected get syncRendererChannel() {
return `store-sync-renderer:${this.path}` return `store-sync-renderer:${this.path}`;
} }
protected get syncMainChannel() { protected get syncMainChannel() {
return `store-sync-main:${this.path}` return `store-sync-main:${this.path}`;
} }
get path() { get path() {
@ -76,7 +76,7 @@ export abstract class BaseStore<T = any> extends Singleton {
} }
protected cwd() { protected cwd() {
return (app || remote.app).getPath("userData") return (app || remote.app).getPath("userData");
} }
protected async saveToFile(model: T) { protected async saveToFile(model: T) {
@ -96,7 +96,7 @@ export abstract class BaseStore<T = any> extends Singleton {
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) {
@ -104,20 +104,20 @@ export abstract class BaseStore<T = any> extends Singleton {
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));
} }
} }
protected onSyncFromMain(model: T) { protected onSyncFromMain(model: T) {
this.applyWithoutSync(() => { this.applyWithoutSync(() => {
this.onSync(model) this.onSync(model);
}) });
} }
unregisterIpcListener() { unregisterIpcListener() {
ipcRenderer.removeAllListeners(this.syncMainChannel) ipcRenderer.removeAllListeners(this.syncMainChannel);
ipcRenderer.removeAllListeners(this.syncRendererChannel) ipcRenderer.removeAllListeners(this.syncRendererChannel);
} }
disableSync() { disableSync() {
@ -143,9 +143,9 @@ export abstract class BaseStore<T = any> extends Singleton {
protected async onModelChange(model: T) { protected async onModelChange(model: T) {
if (ipcMain) { if (ipcMain) {
this.saveToFile(model); // save config file this.saveToFile(model); // save config file
broadcastMessage(this.syncRendererChannel, model) broadcastMessage(this.syncRendererChannel, model);
} else { } else {
broadcastMessage(this.syncMainChannel, model) broadcastMessage(this.syncMainChannel, model);
} }
} }

View File

@ -1,3 +1,3 @@
import { observable } from "mobx" import { observable } from "mobx";
export const clusterFrameMap = observable.map<string, number>(); export const clusterFrameMap = observable.map<string, number>();

View File

@ -1,15 +1,15 @@
import { handleRequest } from "./ipc"; import { handleRequest } from "./ipc";
import { ClusterId, clusterStore } from "./cluster-store"; import { ClusterId, clusterStore } from "./cluster-store";
import { appEventBus } from "./event-bus" import { appEventBus } from "./event-bus";
import { ResourceApplier } from "../main/resource-applier"; import { ResourceApplier } from "../main/resource-applier";
import { ipcMain } from "electron"; import { ipcMain } from "electron";
import { clusterFrameMap } from "./cluster-frames" import { clusterFrameMap } from "./cluster-frames";
export const clusterActivateHandler = "cluster:activate" export const clusterActivateHandler = "cluster:activate";
export const clusterSetFrameIdHandler = "cluster:set-frame-id" export const clusterSetFrameIdHandler = "cluster:set-frame-id";
export const clusterRefreshHandler = "cluster:refresh" export const clusterRefreshHandler = "cluster:refresh";
export const clusterDisconnectHandler = "cluster:disconnect" export const clusterDisconnectHandler = "cluster:disconnect";
export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all" export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all";
if (ipcMain) { if (ipcMain) {
@ -18,38 +18,38 @@ if (ipcMain) {
if (cluster) { if (cluster) {
return cluster.activate(force); return cluster.activate(force);
} }
}) });
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);
} }
}) });
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

@ -5,9 +5,9 @@ import { unlink } from "fs-extra";
import { action, computed, observable, reaction, toJS } from "mobx"; import { action, computed, observable, reaction, toJS } from "mobx";
import { BaseStore } from "./base-store"; import { BaseStore } from "./base-store";
import { Cluster, ClusterState } from "../main/cluster"; import { Cluster, ClusterState } from "../main/cluster";
import migrations from "../migrations/cluster-store" import migrations from "../migrations/cluster-store";
import logger from "../main/logger"; import logger from "../main/logger";
import { appEventBus } from "./event-bus" import { appEventBus } from "./event-bus";
import { dumpConfigYaml } from "./kube-helpers"; import { dumpConfigYaml } from "./kube-helpers";
import { saveToAppFiles } from "./utils/saveToAppFiles"; import { saveToAppFiles } from "./utils/saveToAppFiles";
import { KubeConfig } from "@kubernetes/client-node"; import { KubeConfig } from "@kubernetes/client-node";
@ -86,38 +86,38 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
migrations: migrations, migrations: migrations,
}); });
this.pushStateToViewsAutomatically() this.pushStateToViewsAutomatically();
} }
protected pushStateToViewsAutomatically() { protected pushStateToViewsAutomatically() {
if (!ipcRenderer) { if (!ipcRenderer) {
reaction(() => this.connectedClustersList, () => { reaction(() => this.connectedClustersList, () => {
this.pushState() this.pushState();
}) });
} }
} }
registerIpcListener() { registerIpcListener() {
logger.info(`[CLUSTER-STORE] start to listen (${webFrame.routingId})`) logger.info(`[CLUSTER-STORE] start to listen (${webFrame.routingId})`);
subscribeToBroadcast("cluster:state", (event, clusterId: string, state: ClusterState) => { subscribeToBroadcast("cluster:state", (event, clusterId: string, state: ClusterState) => {
logger.silly(`[CLUSTER-STORE]: received push-state at ${location.host} (${webFrame.routingId})`, clusterId, state); logger.silly(`[CLUSTER-STORE]: received push-state at ${location.host} (${webFrame.routingId})`, clusterId, state);
this.getById(clusterId)?.setState(state) this.getById(clusterId)?.setState(state);
}) });
} }
unregisterIpcListener() { unregisterIpcListener() {
super.unregisterIpcListener() super.unregisterIpcListener();
unsubscribeAllFromBroadcast("cluster:state") unsubscribeAllFromBroadcast("cluster:state");
} }
pushState() { pushState() {
this.clusters.forEach((c) => { this.clusters.forEach((c) => {
c.pushState() c.pushState();
}) });
} }
get activeClusterId() { get activeClusterId() {
return this.activeCluster return this.activeCluster;
} }
@computed get clustersList(): Cluster[] { @computed get clustersList(): Cluster[] {
@ -125,7 +125,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
} }
@computed get enabledClustersList(): Cluster[] { @computed get enabledClustersList(): Cluster[] {
return this.clustersList.filter((c) => c.enabled) return this.clustersList.filter((c) => c.enabled);
} }
@computed get active(): Cluster | null { @computed get active(): Cluster | null {
@ -133,7 +133,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
} }
@computed get connectedClustersList(): Cluster[] { @computed get connectedClustersList(): Cluster[] {
return this.clustersList.filter((c) => !c.disconnected) return this.clustersList.filter((c) => !c.disconnected);
} }
isActive(id: ClusterId) { isActive(id: ClusterId) {
@ -149,7 +149,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
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);
@ -170,37 +170,37 @@ 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));
}) });
return clusters return clusters;
} }
@action @action
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);
} }
this.clusters.set(model.id, cluster); this.clusters.set(model.id, cluster);
return cluster return cluster;
} }
async removeCluster(model: ClusterModel) { async removeCluster(model: ClusterModel) {
await this.removeById(model.id) await this.removeById(model.id);
} }
@action @action
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);
@ -217,8 +217,8 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
@action @action
removeByWorkspaceId(workspaceId: string) { removeByWorkspaceId(workspaceId: string) {
this.getByWorkspaceId(workspaceId).forEach(cluster => { this.getByWorkspaceId(workspaceId).forEach(cluster => {
this.removeById(cluster.id) this.removeById(cluster.id);
}) });
} }
@action @action
@ -235,7 +235,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
} else { } else {
cluster = new Cluster(clusterModel); cluster = new Cluster(clusterModel);
if (!cluster.isManaged) { if (!cluster.isManaged) {
cluster.enabled = true cluster.enabled = true;
} }
} }
newClusters.set(clusterModel.id, cluster); newClusters.set(clusterModel.id, cluster);
@ -259,7 +259,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
clusters: this.clustersList.map(cluster => cluster.toJSON()), clusters: this.clustersList.map(cluster => cluster.toJSON()),
}, { }, {
recurseEverything: true recurseEverything: true
}) });
} }
} }

View File

@ -1,9 +1,9 @@
import { EventEmitter } from "./event-emitter" import { EventEmitter } from "./event-emitter";
export type AppEvent = { export type AppEvent = {
name: string; name: string;
action: string; action: string;
params?: object; params?: object;
} };
export const appEventBus = new EventEmitter<[AppEvent]>() export const appEventBus = new EventEmitter<[AppEvent]>();

View File

@ -35,6 +35,6 @@ export class EventEmitter<D extends [...any[]]> {
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

@ -7,24 +7,24 @@ import logger from "../main/logger";
import { clusterFrameMap } from "./cluster-frames"; import { clusterFrameMap } from "./cluster-frames";
export function handleRequest(channel: string, listener: (...args: any[]) => any) { export function handleRequest(channel: string, listener: (...args: any[]) => any) {
ipcMain.handle(channel, listener) ipcMain.handle(channel, listener);
} }
export async function requestMain(channel: string, ...args: any[]) { export async function requestMain(channel: string, ...args: any[]) {
return ipcRenderer.invoke(channel, ...args) return ipcRenderer.invoke(channel, ...args);
} }
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();
@ -32,39 +32,39 @@ export function broadcastMessage(channel: string, ...args: any[]) {
webContent.send(channel, ...args); webContent.send(channel, ...args);
getSubFrames().then((frames) => { getSubFrames().then((frames) => {
frames.map((frameId) => { frames.map((frameId) => {
webContent.sendToFrame(frameId, channel, ...args) webContent.sendToFrame(frameId, channel, ...args);
}) });
}).catch((e) => e) }).catch((e) => e);
}) });
if (ipcRenderer) { if (ipcRenderer) {
ipcRenderer.send(channel, ...args) ipcRenderer.send(channel, ...args);
} else { } else {
ipcMain.emit(channel, ...args) ipcMain.emit(channel, ...args);
} }
} }
export function subscribeToBroadcast(channel: string, listener: (...args: any[]) => any) { export function subscribeToBroadcast(channel: string, listener: (...args: any[]) => any) {
if (ipcRenderer) { if (ipcRenderer) {
ipcRenderer.on(channel, listener) ipcRenderer.on(channel, listener);
} else { } else {
ipcMain.on(channel, listener) ipcMain.on(channel, listener);
} }
return listener return listener;
} }
export function unsubscribeFromBroadcast(channel: string, listener: (...args: any[]) => any) { export function unsubscribeFromBroadcast(channel: string, listener: (...args: any[]) => any) {
if (ipcRenderer) { if (ipcRenderer) {
ipcRenderer.off(channel, listener) ipcRenderer.off(channel, listener);
} else { } else {
ipcMain.off(channel, listener) ipcMain.off(channel, listener);
} }
} }
export function unsubscribeAllFromBroadcast(channel: string) { export function unsubscribeAllFromBroadcast(channel: string) {
if (ipcRenderer) { if (ipcRenderer) {
ipcRenderer.removeAllListeners(channel) ipcRenderer.removeAllListeners(channel);
} else { } else {
ipcMain.removeAllListeners(channel) ipcMain.removeAllListeners(channel);
} }
} }

View File

@ -1,8 +1,8 @@
import { KubeConfig, V1Node, V1Pod } from "@kubernetes/client-node" import { KubeConfig, V1Node, V1Pod } from "@kubernetes/client-node";
import fse from "fs-extra"; import fse from "fs-extra";
import path from "path" import path from "path";
import os from "os" import os from "os";
import yaml from "js-yaml" import yaml from "js-yaml";
import logger from "../main/logger"; import logger from "../main/logger";
import commandExists from "command-exists"; import commandExists from "command-exists";
import { ExecValidationNotFoundError } from "./custom-errors"; import { ExecValidationNotFoundError } from "./custom-errors";
@ -25,7 +25,7 @@ export function loadConfig(pathOrContent?: string): KubeConfig {
kc.loadFromString(pathOrContent); kc.loadFromString(pathOrContent);
} }
return kc return kc;
} }
/** /**
@ -39,33 +39,33 @@ export function validateConfig(config: KubeConfig | string): KubeConfig {
if (typeof config == "string") { if (typeof config == "string") {
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");
} }
return config return config;
} }
/** /**
* Breaks kube config into several configs. Each context as it own KubeConfig object * Breaks kube config into several configs. Each context as it own KubeConfig object
*/ */
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);
kc.setCurrentContext(ctx.name); kc.setCurrentContext(ctx.name);
configs.push(kc); configs.push(kc);
@ -88,7 +88,7 @@ export function dumpConfigYaml(kubeConfig: Partial<KubeConfig>): string {
server: cluster.server, server: cluster.server,
'insecure-skip-tls-verify': cluster.skipTLSVerify 'insecure-skip-tls-verify': cluster.skipTLSVerify
} }
} };
}), }),
contexts: kubeConfig.contexts.map(context => { contexts: kubeConfig.contexts.map(context => {
return { return {
@ -98,7 +98,7 @@ export function dumpConfigYaml(kubeConfig: Partial<KubeConfig>): string {
user: context.user, user: context.user,
namespace: context.namespace namespace: context.namespace
} }
} };
}), }),
users: kubeConfig.users.map(user => { users: kubeConfig.users.map(user => {
return { return {
@ -114,9 +114,9 @@ export function dumpConfigYaml(kubeConfig: Partial<KubeConfig>): string {
username: user.username, username: user.username,
password: user.password password: user.password
} }
} };
}) })
} };
logger.debug("Dumping KubeConfig:", config); logger.debug("Dumping KubeConfig:", config);
@ -127,20 +127,20 @@ export function dumpConfigYaml(kubeConfig: Partial<KubeConfig>): string {
export function podHasIssues(pod: V1Pod) { export function podHasIssues(pod: V1Pod) {
// Logic adapted from dashboard // Logic adapted from dashboard
const notReady = !!pod.status.conditions.find(condition => { const notReady = !!pod.status.conditions.find(condition => {
return condition.type == "Ready" && condition.status !== "True" return condition.type == "Ready" && condition.status !== "True";
}); });
return ( return (
notReady || notReady ||
pod.status.phase !== "Running" || pod.status.phase !== "Running" ||
pod.spec.priority > 500000 // We're interested in high prio pods events regardless of their running status pod.spec.priority > 500000 // We're interested in high prio pods events regardless of their running status
) );
} }
export function getNodeWarningConditions(node: V1Node) { export function getNodeWarningConditions(node: V1Node) {
return node.status.conditions.filter(c => return node.status.conditions.filter(c =>
c.status.toLowerCase() === "true" && c.type !== "Ready" && c.type !== "HostUpgrades" c.status.toLowerCase() === "true" && c.type !== "Ready" && c.type !== "HostUpgrades"
) );
} }
/** /**

View File

@ -5,8 +5,8 @@ import { PrometheusStacklight } from "../main/prometheus/stacklight";
import { PrometheusProviderRegistry } from "../main/prometheus/provider-registry"; 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);
}); });
export const prometheusProviders = PrometheusProviderRegistry.getProviders() export const prometheusProviders = PrometheusProviderRegistry.getProviders();

View File

@ -4,7 +4,7 @@ export type KubeResource =
"namespaces" | "nodes" | "events" | "resourcequotas" | "namespaces" | "nodes" | "events" | "resourcequotas" |
"services" | "secrets" | "configmaps" | "ingresses" | "networkpolicies" | "persistentvolumes" | "storageclasses" | "services" | "secrets" | "configmaps" | "ingresses" | "networkpolicies" | "persistentvolumes" | "storageclasses" |
"pods" | "daemonsets" | "deployments" | "statefulsets" | "replicasets" | "jobs" | "cronjobs" | "pods" | "daemonsets" | "deployments" | "statefulsets" | "replicasets" | "jobs" | "cronjobs" |
"endpoints" | "customresourcedefinitions" | "horizontalpodautoscalers" | "podsecuritypolicies" | "poddisruptionbudgets" "endpoints" | "customresourcedefinitions" | "horizontalpodautoscalers" | "podsecuritypolicies" | "poddisruptionbudgets";
export interface KubeApiResource { export interface KubeApiResource {
resource: KubeResource; // valid resource name resource: KubeResource; // valid resource name

View File

@ -1,6 +1,6 @@
// Register custom protocols // Register custom protocols
import { protocol } from "electron" import { protocol } from "electron";
import path from "path"; import path from "path";
export function registerFileProtocol(name: string, basePath: string) { export function registerFileProtocol(name: string, basePath: string) {
@ -8,5 +8,5 @@ export function registerFileProtocol(name: string, basePath: string) {
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

@ -1,28 +1,28 @@
import request from "request" import request from "request";
import requestPromise from "request-promise-native" import requestPromise from "request-promise-native";
import { userStore } from "./user-store" import { userStore } from "./user-store";
// todo: get rid of "request" (deprecated) // todo: get rid of "request" (deprecated)
// https://github.com/lensapp/lens/issues/459 // https://github.com/lensapp/lens/issues/459
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,
} };
} }
/** /**
* @deprecated * @deprecated
*/ */
export function customRequest(opts: request.Options) { export function customRequest(opts: request.Options) {
return request.defaults(getDefaultRequestOpts())(opts) return request.defaults(getDefaultRequestOpts())(opts);
} }
/** /**
* @deprecated * @deprecated
*/ */
export function customRequestPromise(opts: requestPromise.Options) { export function customRequestPromise(opts: requestPromise.Options) {
return requestPromise.defaults(getDefaultRequestOpts())(opts) return requestPromise.defaults(getDefaultRequestOpts())(opts);
} }

View File

@ -1,14 +1,14 @@
import { isMac, isWindows } from "./vars"; import { isMac, isWindows } from "./vars";
import winca from "win-ca" import winca from "win-ca";
import macca from "mac-ca" import macca from "mac-ca";
import logger from "../main/logger" 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

@ -1,13 +1,13 @@
import type { ThemeId } from "../renderer/theme.store"; import type { ThemeId } from "../renderer/theme.store";
import { app, remote } from 'electron'; import { app, remote } from 'electron';
import semver from "semver" import semver from "semver";
import { readFile } from "fs-extra" import { readFile } from "fs-extra";
import { action, observable, reaction, toJS } from "mobx"; import { action, observable, reaction, toJS } from "mobx";
import { BaseStore } from "./base-store"; import { BaseStore } from "./base-store";
import migrations from "../migrations/user-store" import migrations from "../migrations/user-store";
import { getAppVersion } from "./utils/app-version"; import { getAppVersion } from "./utils/app-version";
import { kubeConfigDefaultPath, loadConfig } from "./kube-helpers"; import { kubeConfigDefaultPath, loadConfig } from "./kube-helpers";
import { appEventBus } from "./event-bus" import { appEventBus } from "./event-bus";
import logger from "../main/logger"; import logger from "../main/logger";
import path from 'path'; import path from 'path';
@ -31,7 +31,7 @@ export interface UserPreferences {
} }
export class UserStore extends BaseStore<UserStoreModel> { export class UserStore extends BaseStore<UserStoreModel> {
static readonly defaultTheme: ThemeId = "lens-dark" static readonly defaultTheme: ThemeId = "lens-dark";
private constructor() { private constructor() {
super({ super({
@ -42,7 +42,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
this.handleOnLoad(); this.handleOnLoad();
} }
@observable lastSeenAppVersion = "0.0.0" @observable lastSeenAppVersion = "0.0.0";
@observable kubeConfigPath = kubeConfigDefaultPath; // used in add-cluster page for providing context @observable kubeConfigPath = kubeConfigDefaultPath; // used in add-cluster page for providing context
@observable seenContexts = observable.set<string>(); @observable seenContexts = observable.set<string>();
@observable newContexts = observable.set<string>(); @observable newContexts = observable.set<string>();
@ -66,7 +66,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
if (app) { if (app) {
// track telemetry availability // track telemetry availability
reaction(() => this.preferences.allowTelemetry, allowed => { reaction(() => this.preferences.allowTelemetry, allowed => {
appEventBus.emit({name: "telemetry", action: allowed ? "enabled" : "disabled"}) appEventBus.emit({name: "telemetry", action: allowed ? "enabled" : "disabled"});
}); });
// open at system start-up // open at system start-up
@ -95,7 +95,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
@action @action
saveLastSeenAppVersion() { saveLastSeenAppVersion() {
appEventBus.emit({name: "app", action: "whats-new-seen"}) appEventBus.emit({name: "app", action: "whats-new-seen"});
this.lastSeenAppVersion = getAppVersion(); this.lastSeenAppVersion = getAppVersion();
} }
@ -113,7 +113,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
logger.error(err); logger.error(err);
this.resetKubeConfigPath(); this.resetKubeConfigPath();
} }
} };
@action @action
markNewContextsAsSeen() { markNewContextsAsSeen() {
@ -127,12 +127,12 @@ export class UserStore extends BaseStore<UserStoreModel> {
* @returns string * @returns string
*/ */
getDefaultKubectlPath(): string { getDefaultKubectlPath(): string {
return path.join((app || remote.app).getPath("userData"), "binaries") return path.join((app || remote.app).getPath("userData"), "binaries");
} }
@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;
} }
@ -149,10 +149,10 @@ export class UserStore extends BaseStore<UserStoreModel> {
lastSeenAppVersion: this.lastSeenAppVersion, lastSeenAppVersion: this.lastSeenAppVersion,
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

@ -1,4 +1,4 @@
import packageInfo from "../../../package.json" import packageInfo from "../../../package.json";
export function getAppVersion(): string { export function getAppVersion(): string {
return packageInfo.version; return packageInfo.version;
@ -9,5 +9,5 @@ export function getBundledKubectlVersion(): string {
} }
export function getBundledExtensions(): string[] { export function getBundledExtensions(): string[] {
return packageInfo.lens?.extensions || [] return packageInfo.lens?.extensions || [];
} }

View File

@ -6,7 +6,7 @@ export function autobind() {
return function (target: Constructor | object, prop?: string, descriptor?: PropertyDescriptor) { return function (target: Constructor | object, prop?: string, descriptor?: PropertyDescriptor) {
if (target instanceof Function) return bindClass(target); if (target instanceof Function) return bindClass(target);
else return bindMethod(target, prop, descriptor); else return bindMethod(target, prop, descriptor);
} };
} }
function bindClass<T extends Constructor>(constructor: T) { function bindClass<T extends Constructor>(constructor: T) {
@ -22,12 +22,12 @@ function bindClass<T extends Constructor>(constructor: T) {
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);
}) });
} }
function bindMethod(target: object, prop?: string, descriptor?: PropertyDescriptor) { function bindMethod(target: object, prop?: string, descriptor?: PropertyDescriptor) {
if (!descriptor || typeof descriptor.value !== "function") { if (!descriptor || typeof descriptor.value !== "function") {
throw new Error(`@autobind() must be used on class or method only`) throw new Error(`@autobind() must be used on class or method only`);
} }
const { value: func, enumerable, configurable } = descriptor; const { value: func, enumerable, configurable } = descriptor;
const boundFunc = new WeakMap<object, Function>(); const boundFunc = new WeakMap<object, Function>();

View File

@ -1,4 +1,4 @@
import { compile } from "path-to-regexp" import { compile } from "path-to-regexp";
export interface IURLParams<P extends object = {}, Q extends object = {}> { export interface IURLParams<P extends object = {}, Q extends object = {}> {
params?: P; params?: P;
@ -8,7 +8,7 @@ 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

@ -5,7 +5,7 @@
export function defineGlobal(propName: string, descriptor: PropertyDescriptor) { export function defineGlobal(propName: string, descriptor: PropertyDescriptor) {
const scope = typeof global !== "undefined" ? global : window; const scope = typeof global !== "undefined" ? global : window;
if (scope.hasOwnProperty(propName)) { if (scope.hasOwnProperty(propName)) {
console.info(`Global variable "${propName}" already exists. Skipping.`) console.info(`Global variable "${propName}" already exists. Skipping.`);
return; return;
} }
Object.defineProperty(scope, propName, descriptor); Object.defineProperty(scope, propName, descriptor);

View File

@ -1,14 +1,14 @@
// Common utils (main OR renderer) // Common utils (main OR renderer)
export * from "./app-version" export * from "./app-version";
export * from "./autobind" export * from "./autobind";
export * from "./base64" export * from "./base64";
export * from "./camelCase" export * from "./camelCase";
export * from "./cloneJson" export * from "./cloneJson";
export * from "./debouncePromise" export * from "./debouncePromise";
export * from "./defineGlobal" export * from "./defineGlobal";
export * from "./getRandId" export * from "./getRandId";
export * from "./splitArray" export * from "./splitArray";
export * from "./saveToAppFiles" export * from "./saveToAppFiles";
export * from "./singleton" export * from "./singleton";
export * from "./openExternal" export * from "./openExternal";

View File

@ -1,5 +1,5 @@
// Opens a link in external browser // Opens a link in external browser
import { shell } from "electron" import { shell } from "electron";
export function openExternal(url: string) { export function openExternal(url: string) {
return shell.openExternal(url); return shell.openExternal(url);

View File

@ -2,7 +2,7 @@
import path from "path"; import path from "path";
import { app, remote } from "electron"; import { app, remote } from "electron";
import { ensureDirSync, writeFileSync } from "fs-extra"; import { ensureDirSync, writeFileSync } from "fs-extra";
import { WriteFileOptions } from "fs" 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);

View File

@ -24,5 +24,5 @@ class Singleton {
} }
} }
export { Singleton } export { Singleton };
export default Singleton; export default Singleton;

View File

@ -15,5 +15,5 @@ export function splitArray<T>(array: T[], element: T): [T[], T[], boolean] {
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

@ -1,19 +1,19 @@
// App's common configuration for any process (main, renderer, build pipeline, etc.) // App's common configuration for any process (main, renderer, build pipeline, etc.)
import path from "path"; import path from "path";
import packageInfo from "../../package.json" import packageInfo from "../../package.json";
import { defineGlobal } from "./utils/defineGlobal"; import { defineGlobal } from "./utils/defineGlobal";
export const isMac = process.platform === "darwin" export const isMac = process.platform === "darwin";
export const isWindows = process.platform === "win32" export const isWindows = process.platform === "win32";
export const isLinux = process.platform === "linux" export const isLinux = process.platform === "linux";
export const isDebugging = process.env.DEBUG === "true"; export const isDebugging = process.env.DEBUG === "true";
export const isSnap = !!process.env["SNAP"] export const isSnap = !!process.env["SNAP"];
export const isProduction = process.env.NODE_ENV === "production" export const isProduction = process.env.NODE_ENV === "production";
export const isTestEnv = !!process.env.JEST_WORKER_ID; export const isTestEnv = !!process.env.JEST_WORKER_ID;
export const isDevelopment = !isTestEnv && !isProduction; export const isDevelopment = !isTestEnv && !isProduction;
export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}` export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}`;
export const publicPath = "/build/" export const publicPath = "/build/";
// Webpack build paths // Webpack build paths
export const contextDir = process.cwd(); export const contextDir = process.cwd();
@ -22,7 +22,7 @@ export const mainDir = path.join(contextDir, "src/main");
export const rendererDir = path.join(contextDir, "src/renderer"); export const rendererDir = path.join(contextDir, "src/renderer");
export const htmlTemplate = path.resolve(rendererDir, "template.html"); export const htmlTemplate = path.resolve(rendererDir, "template.html");
export const sassCommonVars = path.resolve(rendererDir, "components/vars.scss"); export const sassCommonVars = path.resolve(rendererDir, "components/vars.scss");
export const webpackDevServerPort = 9009 export const webpackDevServerPort = 9009;
// Special runtime paths // Special runtime paths
defineGlobal("__static", { defineGlobal("__static", {
@ -30,14 +30,14 @@ 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");
} }
}) });
// Apis // Apis
export const apiPrefix = "/api" // local router apis export const apiPrefix = "/api"; // local router apis
export const apiKubePrefix = "/api-kube" // k8s cluster apis export const apiKubePrefix = "/api-kube"; // k8s cluster apis
// Links // Links
export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues" export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues";
export const slackUrl = "https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI" export const slackUrl = "https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI";

View File

@ -1,7 +1,7 @@
import { ipcRenderer } from "electron"; import { ipcRenderer } from "electron";
import { action, computed, observable, toJS, reaction } from "mobx"; import { action, computed, observable, toJS, reaction } from "mobx";
import { BaseStore } from "./base-store"; import { BaseStore } from "./base-store";
import { clusterStore } from "./cluster-store" import { clusterStore } from "./cluster-store";
import { appEventBus } from "./event-bus"; import { appEventBus } from "./event-bus";
import { broadcastMessage } from "../common/ipc"; import { broadcastMessage } from "../common/ipc";
import logger from "../main/logger"; import logger from "../main/logger";
@ -25,40 +25,40 @@ export interface WorkspaceState {
} }
export class Workspace implements WorkspaceModel, WorkspaceState { export class Workspace implements WorkspaceModel, WorkspaceState {
@observable id: WorkspaceId @observable id: WorkspaceId;
@observable name: string @observable name: string;
@observable description?: string @observable description?: string;
@observable ownerRef?: string @observable ownerRef?: string;
@observable enabled: boolean @observable enabled: boolean;
constructor(data: WorkspaceModel) { constructor(data: WorkspaceModel) {
Object.assign(this, data) Object.assign(this, data);
if (!ipcRenderer) { if (!ipcRenderer) {
reaction(() => this.getState(), () => { reaction(() => this.getState(), () => {
this.pushState() this.pushState();
}) });
} }
} }
get isManaged(): boolean { get isManaged(): boolean {
return !!this.ownerRef return !!this.ownerRef;
} }
getState(): WorkspaceState { getState(): WorkspaceState {
return { return {
enabled: this.enabled enabled: this.enabled
} };
} }
pushState(state = this.getState()) { pushState(state = this.getState()) {
logger.silly("[WORKSPACE] pushing state", {...state, id: this.id}) logger.silly("[WORKSPACE] pushing state", {...state, id: this.id});
broadcastMessage("workspace:state", this.id, toJS(state)) broadcastMessage("workspace:state", this.id, toJS(state));
} }
@action @action
setState(state: WorkspaceState) { setState(state: WorkspaceState) {
Object.assign(this, state) Object.assign(this, state);
} }
toJSON(): WorkspaceModel { toJSON(): WorkspaceModel {
@ -67,12 +67,12 @@ export class Workspace implements WorkspaceModel, WorkspaceState {
name: this.name, name: this.name,
description: this.description, description: this.description,
ownerRef: this.ownerRef ownerRef: this.ownerRef
}) });
} }
} }
export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> { export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
static readonly defaultId: WorkspaceId = "default" static readonly defaultId: WorkspaceId = "default";
private constructor() { private constructor() {
super({ super({
@ -81,21 +81,21 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
if (!ipcRenderer) { if (!ipcRenderer) {
setInterval(() => { setInterval(() => {
this.pushState() this.pushState();
}, 5000) }, 5000);
} }
} }
registerIpcListener() { registerIpcListener() {
logger.info("[WORKSPACE-STORE] starting to listen state events") logger.info("[WORKSPACE-STORE] starting to listen state events");
ipcRenderer.on("workspace:state", (event, workspaceId: string, state: WorkspaceState) => { ipcRenderer.on("workspace:state", (event, workspaceId: string, state: WorkspaceState) => {
this.getById(workspaceId)?.setState(state) this.getById(workspaceId)?.setState(state);
}) });
} }
unregisterIpcListener() { unregisterIpcListener() {
super.unregisterIpcListener() super.unregisterIpcListener();
ipcRenderer.removeAllListeners("workspace:state") ipcRenderer.removeAllListeners("workspace:state");
} }
@observable currentWorkspaceId = WorkspaceStore.defaultId; @observable currentWorkspaceId = WorkspaceStore.defaultId;
@ -121,8 +121,8 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
pushState() { pushState() {
this.workspaces.forEach((w) => { this.workspaces.forEach((w) => {
w.pushState() w.pushState();
}) });
} }
isDefault(id: WorkspaceId) { isDefault(id: WorkspaceId) {
@ -154,7 +154,7 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
return; return;
} }
this.workspaces.set(id, workspace); this.workspaces.set(id, workspace);
appEventBus.emit({name: "workspace", action: "add"}) appEventBus.emit({name: "workspace", action: "add"});
return workspace; return workspace;
} }
@ -166,7 +166,7 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
@action @action
removeWorkspace(workspace: Workspace) { removeWorkspace(workspace: Workspace) {
this.removeWorkspaceById(workspace.id) this.removeWorkspaceById(workspace.id);
} }
@action @action
@ -180,24 +180,24 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
this.currentWorkspaceId = WorkspaceStore.defaultId; // reset to default this.currentWorkspaceId = WorkspaceStore.defaultId; // reset to default
} }
this.workspaces.delete(id); this.workspaces.delete(id);
appEventBus.emit({name: "workspace", action: "remove"}) appEventBus.emit({name: "workspace", action: "remove"});
clusterStore.removeByWorkspaceId(id) clusterStore.removeByWorkspaceId(id);
} }
@action @action
protected fromStore({ currentWorkspace, workspaces = [] }: WorkspaceStoreModel) { protected fromStore({ currentWorkspace, workspaces = [] }: 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;
} }
this.workspaces.set(workspace.id, workspace) this.workspaces.set(workspace.id, workspace);
}) });
} }
} }
@ -207,8 +207,8 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
workspaces: this.workspacesList.map((w) => w.toJSON()), workspaces: this.workspacesList.map((w) => w.toJSON()),
}, { }, {
recurseEverything: true recurseEverything: true
}) });
} }
} }
export const workspaceStore = WorkspaceStore.getInstance<WorkspaceStore>() export const workspaceStore = WorkspaceStore.getInstance<WorkspaceStore>();

View File

@ -1,6 +1,6 @@
import { LensExtension } from "../lens-extension" import { LensExtension } from "../lens-extension";
let ext: LensExtension = null let ext: LensExtension = null;
describe("lens extension", () => { describe("lens extension", () => {
beforeEach(async () => { beforeEach(async () => {
@ -12,12 +12,12 @@ describe("lens extension", () => {
manifestPath: "/this/is/fake/package.json", manifestPath: "/this/is/fake/package.json",
isBundled: false, isBundled: false,
isEnabled: true isEnabled: true
}) });
}) });
describe("name", () => { describe("name", () => {
it("returns name", () => { it("returns name", () => {
expect(ext.name).toBe("foo-bar") expect(ext.name).toBe("foo-bar");
}) });
}) });
}) });

View File

@ -1,11 +1,11 @@
import fs from "fs"; import fs from "fs";
import path from "path" import path from "path";
import hb from "handlebars" import hb from "handlebars";
import { observable } from "mobx" import { observable } from "mobx";
import { ResourceApplier } from "../main/resource-applier" import { ResourceApplier } from "../main/resource-applier";
import { Cluster } from "../main/cluster"; import { Cluster } from "../main/cluster";
import logger from "../main/logger"; import logger from "../main/logger";
import { app } from "electron" import { app } from "electron";
import { requestMain } from "../common/ipc"; import { requestMain } from "../common/ipc";
import { clusterKubectlApplyAllHandler } from "../common/cluster-ipc"; import { clusterKubectlApplyAllHandler } from "../common/cluster-ipc";
@ -26,7 +26,7 @@ export abstract class ClusterFeature {
installed: false, installed: false,
latestVersion: null, latestVersion: null,
canUpgrade: false canUpgrade: false
} };
abstract async install(cluster: Cluster): Promise<void>; abstract async install(cluster: Cluster): Promise<void>;
@ -38,9 +38,9 @@ export abstract class ClusterFeature {
protected async applyResources(cluster: Cluster, resources: string[]) { protected async applyResources(cluster: Cluster, resources: string[]) {
if (app) { if (app) {
await new ResourceApplier(cluster).kubectlApplyAll(resources) await new ResourceApplier(cluster).kubectlApplyAll(resources);
} else { } else {
await requestMain(clusterKubectlApplyAllHandler, cluster.id, resources) await requestMain(clusterKubectlApplyAllHandler, cluster.id, resources);
} }
} }

View File

@ -1,4 +1,4 @@
import { getAppVersion } from "../../common/utils"; import { getAppVersion } from "../../common/utils";
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";

View File

@ -1,2 +1,2 @@
export { ClusterFeature as Feature } from "../cluster-feature" export { ClusterFeature as Feature } from "../cluster-feature";
export type { ClusterFeatureStatus as FeatureStatus } from "../cluster-feature" export type { ClusterFeatureStatus as FeatureStatus } from "../cluster-feature";

View File

@ -1,2 +1,2 @@
export { appEventBus } from "../../common/event-bus" export { appEventBus } from "../../common/event-bus";
export type { AppEvent } from "../../common/event-bus" export type { AppEvent } from "../../common/event-bus";

View File

@ -1,14 +1,14 @@
// Lens-extensions api developer's kit // Lens-extensions api developer's kit
export * from "../lens-main-extension" export * from "../lens-main-extension";
export * from "../lens-renderer-extension" export * from "../lens-renderer-extension";
// APIs // APIs
import * as App from "./app" import * as App from "./app";
import * as EventBus from "./event-bus" import * as EventBus from "./event-bus";
import * as Store from "./stores" import * as Store from "./stores";
import * as Util from "./utils" import * as Util from "./utils";
import * as ClusterFeature from "./cluster-feature" import * as ClusterFeature from "./cluster-feature";
import * as Interface from "../interfaces" import * as Interface from "../interfaces";
export { export {
App, App,
@ -17,4 +17,4 @@ export {
Interface, Interface,
Store, Store,
Util, Util,
} };

View File

@ -1,6 +1,6 @@
export { ExtensionStore } from "../extension-store" export { ExtensionStore } from "../extension-store";
export { clusterStore } from "../../common/cluster-store" export { clusterStore } from "../../common/cluster-store";
export type { ClusterModel } from "../../common/cluster-store" export type { ClusterModel } from "../../common/cluster-store";
export { Cluster } from "../../main/cluster" export { Cluster } from "../../main/cluster";
export { workspaceStore, Workspace } from "../../common/workspace-store" export { workspaceStore, Workspace } from "../../common/workspace-store";
export type { WorkspaceModel } from "../../common/workspace-store" export type { WorkspaceModel } from "../../common/workspace-store";

View File

@ -1,3 +1,3 @@
export { Singleton, openExternal } from "../../common/utils" export { Singleton, openExternal } from "../../common/utils";
export { prevDefault, stopPropagation } from "../../renderer/utils/prevDefault" export { prevDefault, stopPropagation } from "../../renderer/utils/prevDefault";
export { cssNames } from "../../renderer/utils/cssNames" export { cssNames } from "../../renderer/utils/cssNames";

View File

@ -1,4 +1,4 @@
// Extension-api types generation bundle // Extension-api types generation bundle
export * from "./core-api" export * from "./core-api";
export * from "./renderer-api" export * from "./renderer-api";

View File

@ -1,24 +1,24 @@
import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "./lens-extension" import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "./lens-extension";
import type { LensMainExtension } from "./lens-main-extension" import type { LensMainExtension } from "./lens-main-extension";
import type { LensRendererExtension } from "./lens-renderer-extension" import type { LensRendererExtension } from "./lens-renderer-extension";
import type { InstalledExtension } from "./extension-manager"; import type { InstalledExtension } from "./extension-manager";
import path from "path" import path from "path";
import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc" import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc";
import { action, computed, observable, reaction, toJS, when } from "mobx" import { action, computed, observable, reaction, toJS, when } from "mobx";
import logger from "../main/logger" import logger from "../main/logger";
import { app, ipcRenderer, remote } from "electron" import { app, ipcRenderer, remote } from "electron";
import * as registries from "./registries"; import * as registries from "./registries";
import { extensionsStore } from "./extensions-store"; import { extensionsStore } from "./extensions-store";
// lazy load so that we get correct userData // lazy load so that we get correct userData
export function extensionPackagesRoot() { export function extensionPackagesRoot() {
return path.join((app || remote.app).getPath("userData")) return path.join((app || remote.app).getPath("userData"));
} }
export class ExtensionLoader { export class ExtensionLoader {
protected extensions = observable.map<LensExtensionId, InstalledExtension>(); protected extensions = observable.map<LensExtensionId, InstalledExtension>();
protected instances = observable.map<LensExtensionId, LensExtension>(); protected instances = observable.map<LensExtensionId, LensExtension>();
protected readonly requestExtensionsChannel = "extensions:loaded" protected readonly requestExtensionsChannel = "extensions:loaded";
@observable isLoaded = false; @observable isLoaded = false;
whenLoaded = when(() => this.isLoaded); whenLoaded = when(() => this.isLoaded);
@ -29,22 +29,22 @@ export class ExtensionLoader {
if (ext.isBundled) { if (ext.isBundled) {
extensions.delete(extId); extensions.delete(extId);
} }
}) });
return extensions; return extensions;
} }
@action @action
async init() { async init() {
if (ipcRenderer) { if (ipcRenderer) {
this.initRenderer() this.initRenderer();
} else { } else {
this.initMain() this.initMain();
} }
extensionsStore.manageState(this); extensionsStore.manageState(this);
} }
initExtensions(extensions?: Map<LensExtensionId, InstalledExtension>) { initExtensions(extensions?: Map<LensExtensionId, InstalledExtension>) {
this.extensions.replace(extensions) this.extensions.replace(extensions);
} }
protected async initMain() { protected async initMain() {
@ -53,12 +53,12 @@ export class ExtensionLoader {
this.broadcastExtensions(); this.broadcastExtensions();
reaction(() => this.extensions.toJS(), () => { reaction(() => this.extensions.toJS(), () => {
this.broadcastExtensions() this.broadcastExtensions();
}) });
handleRequest(this.requestExtensionsChannel, () => { handleRequest(this.requestExtensionsChannel, () => {
return Array.from(this.toJSON()) return Array.from(this.toJSON());
}) });
} }
protected async initRenderer() { protected async initRenderer() {
@ -66,25 +66,25 @@ export class ExtensionLoader {
this.isLoaded = true; this.isLoaded = true;
extensions.forEach(([extId, ext]) => { extensions.forEach(([extId, ext]) => {
if (!this.extensions.has(extId)) { if (!this.extensions.has(extId)) {
this.extensions.set(extId, ext) this.extensions.set(extId, ext);
} }
}) });
} };
requestMain(this.requestExtensionsChannel).then(extensionListHandler) requestMain(this.requestExtensionsChannel).then(extensionListHandler);
subscribeToBroadcast(this.requestExtensionsChannel, (event, extensions: [LensExtensionId, InstalledExtension][]) => { subscribeToBroadcast(this.requestExtensionsChannel, (event, extensions: [LensExtensionId, InstalledExtension][]) => {
extensionListHandler(extensions) extensionListHandler(extensions);
}); });
} }
loadOnMain() { loadOnMain() {
logger.info('[EXTENSIONS-LOADER]: load on main') logger.info('[EXTENSIONS-LOADER]: load on main');
this.autoInitExtensions((ext: LensMainExtension) => [ this.autoInitExtensions((ext: LensMainExtension) => [
registries.menuRegistry.add(ext.appMenus) registries.menuRegistry.add(ext.appMenus)
]); ]);
} }
loadOnClusterManagerRenderer() { loadOnClusterManagerRenderer() {
logger.info('[EXTENSIONS-LOADER]: load on main renderer (cluster manager)') logger.info('[EXTENSIONS-LOADER]: load on main renderer (cluster manager)');
this.autoInitExtensions((ext: LensRendererExtension) => [ this.autoInitExtensions((ext: LensRendererExtension) => [
registries.globalPageRegistry.add(ext.globalPages, ext), registries.globalPageRegistry.add(ext.globalPages, ext),
registries.globalPageMenuRegistry.add(ext.globalPageMenus, ext), registries.globalPageMenuRegistry.add(ext.globalPageMenus, ext),
@ -95,14 +95,14 @@ export class ExtensionLoader {
} }
loadOnClusterRenderer() { loadOnClusterRenderer() {
logger.info('[EXTENSIONS-LOADER]: load on cluster renderer (dashboard)') logger.info('[EXTENSIONS-LOADER]: load on cluster renderer (dashboard)');
this.autoInitExtensions((ext: LensRendererExtension) => [ this.autoInitExtensions((ext: LensRendererExtension) => [
registries.clusterPageRegistry.add(ext.clusterPages, ext), registries.clusterPageRegistry.add(ext.clusterPages, ext),
registries.clusterPageMenuRegistry.add(ext.clusterPageMenus, ext), registries.clusterPageMenuRegistry.add(ext.clusterPageMenus, ext),
registries.kubeObjectMenuRegistry.add(ext.kubeObjectMenuItems), registries.kubeObjectMenuRegistry.add(ext.kubeObjectMenuItems),
registries.kubeObjectDetailRegistry.add(ext.kubeObjectDetailItems), registries.kubeObjectDetailRegistry.add(ext.kubeObjectDetailItems),
registries.kubeObjectStatusRegistry.add(ext.kubeObjectStatusTexts) registries.kubeObjectStatusRegistry.add(ext.kubeObjectStatusTexts)
]) ]);
} }
protected autoInitExtensions(register: (ext: LensExtension) => Function[]) { protected autoInitExtensions(register: (ext: LensExtension) => Function[]) {
@ -111,43 +111,43 @@ export class ExtensionLoader {
let instance = this.instances.get(extId); let instance = this.instances.get(extId);
if (ext.isEnabled && !instance) { if (ext.isEnabled && !instance) {
try { try {
const LensExtensionClass: LensExtensionConstructor = this.requireExtension(ext) const LensExtensionClass: LensExtensionConstructor = this.requireExtension(ext);
if (!LensExtensionClass) continue; if (!LensExtensionClass) continue;
instance = new LensExtensionClass(ext); 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);
} catch (err) { } catch (err) {
logger.error(`[EXTENSION-LOADER]: activation extension error`, { ext, err }) logger.error(`[EXTENSION-LOADER]: activation extension error`, { ext, err });
} }
} else if (!ext.isEnabled && instance) { } else if (!ext.isEnabled && instance) {
try { try {
instance.disable(); instance.disable();
this.instances.delete(extId); this.instances.delete(extId);
} catch (err) { } catch (err) {
logger.error(`[EXTENSION-LOADER]: deactivation extension error`, { ext, err }) logger.error(`[EXTENSION-LOADER]: deactivation extension error`, { ext, err });
} }
} }
} }
}, { }, {
fireImmediately: true, fireImmediately: true,
}) });
} }
protected requireExtension(extension: InstalledExtension) { protected requireExtension(extension: InstalledExtension) {
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;
} }
} catch (err) { } catch (err) {
console.error(`[EXTENSION-LOADER]: can't load extension main at ${extEntrypoint}: ${err}`, { extension }); console.error(`[EXTENSION-LOADER]: can't load extension main at ${extEntrypoint}: ${err}`, { extension });
console.trace(err) console.trace(err);
} }
} }
@ -159,11 +159,11 @@ export class ExtensionLoader {
return toJS(this.extensions, { return toJS(this.extensions, {
exportMapsAsObjects: false, exportMapsAsObjects: false,
recurseEverything: true, recurseEverything: true,
}) });
} }
broadcastExtensions() { broadcastExtensions() {
broadcastMessage(this.requestExtensionsChannel, Array.from(this.toJSON())) broadcastMessage(this.requestExtensionsChannel, Array.from(this.toJSON()));
} }
} }

View File

@ -1,11 +1,11 @@
import type { LensExtensionId, LensExtensionManifest } from "./lens-extension" import type { LensExtensionId, LensExtensionManifest } from "./lens-extension";
import path from "path" import path from "path";
import os from "os" import os from "os";
import fs from "fs-extra" import fs from "fs-extra";
import child_process from "child_process"; import child_process from "child_process";
import logger from "../main/logger" import logger from "../main/logger";
import { extensionPackagesRoot } from "./extension-loader" import { extensionPackagesRoot } from "./extension-loader";
import { getBundledExtensions } from "../common/utils/app-version" import { getBundledExtensions } from "../common/utils/app-version";
export interface InstalledExtension { export interface InstalledExtension {
readonly manifest: LensExtensionManifest; readonly manifest: LensExtensionManifest;
@ -16,26 +16,26 @@ export interface InstalledExtension {
type Dependencies = { type Dependencies = {
[name: string]: string; [name: string]: string;
} };
type PackageJson = { type PackageJson = {
dependencies: Dependencies; dependencies: Dependencies;
} };
export class ExtensionManager { export class ExtensionManager {
protected bundledFolderPath: string protected bundledFolderPath: string;
protected packagesJson: PackageJson = { protected packagesJson: PackageJson = {
dependencies: {} dependencies: {}
} };
get extensionPackagesRoot() { get extensionPackagesRoot() {
return extensionPackagesRoot() return extensionPackagesRoot();
} }
get inTreeTargetPath() { get inTreeTargetPath() {
return path.join(this.extensionPackagesRoot, "extensions") return path.join(this.extensionPackagesRoot, "extensions");
} }
get inTreeFolderPath(): string { get inTreeFolderPath(): string {
@ -43,7 +43,7 @@ export class ExtensionManager {
} }
get nodeModulesPath(): string { get nodeModulesPath(): string {
return path.join(this.extensionPackagesRoot, "node_modules") return path.join(this.extensionPackagesRoot, "node_modules");
} }
get localFolderPath(): string { get localFolderPath(): string {
@ -51,30 +51,30 @@ export class ExtensionManager {
} }
get npmPath() { get npmPath() {
return __non_webpack_require__.resolve('npm/bin/npm-cli') return __non_webpack_require__.resolve('npm/bin/npm-cli');
} }
get packageJsonPath() { get packageJsonPath() {
return path.join(this.extensionPackagesRoot, "package.json") return path.join(this.extensionPackagesRoot, "package.json");
} }
async load(): Promise<Map<LensExtensionId, InstalledExtension>> { async load(): Promise<Map<LensExtensionId, InstalledExtension>> {
logger.info("[EXTENSION-MANAGER] loading extensions from " + this.extensionPackagesRoot) logger.info("[EXTENSION-MANAGER] loading extensions from " + this.extensionPackagesRoot);
if (fs.existsSync(path.join(this.extensionPackagesRoot, "package-lock.json"))) { if (fs.existsSync(path.join(this.extensionPackagesRoot, "package-lock.json"))) {
await fs.remove(path.join(this.extensionPackagesRoot, "package-lock.json")) await fs.remove(path.join(this.extensionPackagesRoot, "package-lock.json"));
} }
try { try {
await fs.access(this.inTreeFolderPath, fs.constants.W_OK) await fs.access(this.inTreeFolderPath, fs.constants.W_OK);
this.bundledFolderPath = this.inTreeFolderPath this.bundledFolderPath = this.inTreeFolderPath;
} catch { } catch {
// we need to copy in-tree extensions so that we can symlink them properly on "npm install" // we need to copy in-tree extensions so that we can symlink them properly on "npm install"
await fs.remove(this.inTreeTargetPath) await fs.remove(this.inTreeTargetPath);
await fs.ensureDir(this.inTreeTargetPath) await fs.ensureDir(this.inTreeTargetPath);
await fs.copy(this.inTreeFolderPath, this.inTreeTargetPath) await fs.copy(this.inTreeFolderPath, this.inTreeTargetPath);
this.bundledFolderPath = this.inTreeTargetPath this.bundledFolderPath = this.inTreeTargetPath;
} }
await fs.ensureDir(this.nodeModulesPath) await fs.ensureDir(this.nodeModulesPath);
await fs.ensureDir(this.localFolderPath) await fs.ensureDir(this.localFolderPath);
return await this.loadExtensions(); return await this.loadExtensions();
} }
@ -82,16 +82,16 @@ export class ExtensionManager {
let manifestJson: LensExtensionManifest; let manifestJson: LensExtensionManifest;
try { try {
fs.accessSync(manifestPath, fs.constants.F_OK); // check manifest file for existence fs.accessSync(manifestPath, fs.constants.F_OK); // check manifest file for existence
manifestJson = __non_webpack_require__(manifestPath) manifestJson = __non_webpack_require__(manifestPath);
this.packagesJson.dependencies[manifestJson.name] = path.dirname(manifestPath) this.packagesJson.dependencies[manifestJson.name] = path.dirname(manifestPath);
logger.info("[EXTENSION-MANAGER] installed extension " + manifestJson.name) logger.info("[EXTENSION-MANAGER] installed extension " + manifestJson.name);
return { return {
manifestPath: path.join(this.nodeModulesPath, manifestJson.name, "package.json"), manifestPath: path.join(this.nodeModulesPath, manifestJson.name, "package.json"),
manifest: manifestJson, manifest: manifestJson,
isBundled: isBundled, isBundled: isBundled,
isEnabled: isBundled, isEnabled: isBundled,
} };
} catch (err) { } catch (err) {
logger.error(`[EXTENSION-MANAGER]: can't install extension at ${manifestPath}: ${err}`, { manifestJson }); logger.error(`[EXTENSION-MANAGER]: can't install extension at ${manifestPath}: ${err}`, { manifestJson });
} }
@ -102,65 +102,65 @@ export class ExtensionManager {
const child = child_process.fork(this.npmPath, ["install", "--silent", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock"], { const child = child_process.fork(this.npmPath, ["install", "--silent", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock"], {
cwd: extensionPackagesRoot(), cwd: extensionPackagesRoot(),
silent: true silent: true
}) });
child.on("close", () => { child.on("close", () => {
resolve() resolve();
}) });
child.on("error", (err) => { child.on("error", (err) => {
reject(err) reject(err);
}) });
}) });
} }
async loadExtensions() { async loadExtensions() {
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 fs.writeFile(path.join(this.packageJsonPath), JSON.stringify(this.packagesJson, null, 2), { mode: 0o600 }) await fs.writeFile(path.join(this.packageJsonPath), JSON.stringify(this.packagesJson, null, 2), { mode: 0o600 });
await this.installPackages() await this.installPackages();
const extensions = bundledExtensions.concat(localExtensions) const extensions = bundledExtensions.concat(localExtensions);
return new Map(extensions.map(ext => [ext.manifestPath, ext])); return new Map(extensions.map(ext => [ext.manifestPath, ext]));
} }
async loadBundledExtensions() { async loadBundledExtensions() {
const extensions: InstalledExtension[] = [] const extensions: InstalledExtension[] = [];
const folderPath = this.bundledFolderPath const folderPath = this.bundledFolderPath;
const bundledExtensions = getBundledExtensions() const bundledExtensions = getBundledExtensions();
const paths = await fs.readdir(folderPath); const paths = await fs.readdir(folderPath);
for (const fileName of paths) { for (const fileName of paths) {
if (!bundledExtensions.includes(fileName)) { if (!bundledExtensions.includes(fileName)) {
continue continue;
} }
const absPath = path.resolve(folderPath, fileName); const absPath = path.resolve(folderPath, fileName);
const manifestPath = path.resolve(absPath, "package.json"); const manifestPath = path.resolve(absPath, "package.json");
const ext = await this.getByManifest(manifestPath, { isBundled: true }).catch(() => null) const ext = await this.getByManifest(manifestPath, { isBundled: true }).catch(() => null);
if (ext) { if (ext) {
extensions.push(ext) extensions.push(ext);
} }
} }
logger.debug(`[EXTENSION-MANAGER]: ${extensions.length} extensions loaded`, { folderPath, extensions }); logger.debug(`[EXTENSION-MANAGER]: ${extensions.length} extensions loaded`, { folderPath, extensions });
return extensions return extensions;
} }
async loadFromFolder(folderPath: string): Promise<InstalledExtension[]> { async loadFromFolder(folderPath: string): Promise<InstalledExtension[]> {
const bundledExtensions = getBundledExtensions() const bundledExtensions = getBundledExtensions();
const extensions: InstalledExtension[] = [] const extensions: InstalledExtension[] = [];
const paths = await fs.readdir(folderPath); const paths = await fs.readdir(folderPath);
for (const fileName of paths) { for (const fileName of paths) {
if (bundledExtensions.includes(fileName)) { // do no allow to override bundled extensions if (bundledExtensions.includes(fileName)) { // do no allow to override bundled extensions
continue continue;
} }
const absPath = path.resolve(folderPath, fileName); const absPath = path.resolve(folderPath, fileName);
if (!fs.existsSync(absPath)) { if (!fs.existsSync(absPath)) {
continue continue;
} }
const lstat = await fs.lstat(absPath) const lstat = await fs.lstat(absPath);
if (!lstat.isDirectory() && !lstat.isSymbolicLink()) { // skip non-directories if (!lstat.isDirectory() && !lstat.isSymbolicLink()) { // skip non-directories
continue continue;
} }
const manifestPath = path.resolve(absPath, "package.json"); const manifestPath = path.resolve(absPath, "package.json");
const ext = await this.getByManifest(manifestPath).catch(() => null) const ext = await this.getByManifest(manifestPath).catch(() => null);
if (ext) { if (ext) {
extensions.push(ext) extensions.push(ext);
} }
} }
@ -169,4 +169,4 @@ export class ExtensionManager {
} }
} }
export const extensionManager = new ExtensionManager() export const extensionManager = new ExtensionManager();

View File

@ -1,21 +1,21 @@
import { BaseStore } from "../common/base-store" import { BaseStore } from "../common/base-store";
import * as path from "path" import * as path from "path";
import { LensExtension } from "./lens-extension" import { LensExtension } from "./lens-extension";
export abstract class ExtensionStore<T> extends BaseStore<T> { export abstract class ExtensionStore<T> extends BaseStore<T> {
protected extension: LensExtension protected extension: LensExtension;
async loadExtension(extension: LensExtension) { async loadExtension(extension: LensExtension) {
this.extension = extension this.extension = extension;
return super.load() return super.load();
} }
async load() { async load() {
if (!this.extension) { return } if (!this.extension) { return; }
return super.load() return super.load();
} }
protected cwd() { protected cwd() {
return path.join(super.cwd(), "extension-store", this.extension.name) return path.join(super.cwd(), "extension-store", this.extension.name);
} }
} }

View File

@ -1,6 +1,6 @@
import type { LensExtensionId } from "./lens-extension"; import type { LensExtensionId } from "./lens-extension";
import type { ExtensionLoader } from "./extension-loader"; import type { ExtensionLoader } from "./extension-loader";
import { BaseStore } from "../common/base-store" import { BaseStore } from "../common/base-store";
import { action, observable, reaction, toJS } from "mobx"; import { action, observable, reaction, toJS } from "mobx";
export interface LensExtensionsStoreModel { export interface LensExtensionsStoreModel {
@ -25,9 +25,9 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
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,
} };
return state; return state;
}, state) }, state);
} }
async manageState(extensionLoader: ExtensionLoader) { async manageState(extensionLoader: ExtensionLoader) {
@ -46,13 +46,13 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
if (ext && !ext.isBundled) { if (ext && !ext.isBundled) {
ext.isEnabled = state.enabled; ext.isEnabled = state.enabled;
} }
}) });
}) });
// save state on change `extension.isEnabled` // save state on change `extension.isEnabled`
reaction(() => this.getState(extensionLoader), extensionsState => { reaction(() => this.getState(extensionLoader), extensionsState => {
this.state.merge(extensionsState) this.state.merge(extensionsState);
}) });
} }
isEnabled(extId: LensExtensionId) { isEnabled(extId: LensExtensionId) {
@ -70,7 +70,7 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
extensions: this.state.toJSON(), extensions: this.state.toJSON(),
}, { }, {
recurseEverything: true recurseEverything: true
}) });
} }
} }

View File

@ -1 +1 @@
export * from "./registrations" export * from "./registrations";

View File

@ -1,8 +1,8 @@
export type { AppPreferenceRegistration, AppPreferenceComponents } from "../registries/app-preference-registry" export type { AppPreferenceRegistration, AppPreferenceComponents } from "../registries/app-preference-registry";
export type { ClusterFeatureRegistration, ClusterFeatureComponents } from "../registries/cluster-feature-registry" export type { ClusterFeatureRegistration, ClusterFeatureComponents } from "../registries/cluster-feature-registry";
export type { KubeObjectDetailRegistration, KubeObjectDetailComponents } from "../registries/kube-object-detail-registry" export type { KubeObjectDetailRegistration, KubeObjectDetailComponents } from "../registries/kube-object-detail-registry";
export type { KubeObjectMenuRegistration, KubeObjectMenuComponents } from "../registries/kube-object-menu-registry" export type { KubeObjectMenuRegistration, KubeObjectMenuComponents } from "../registries/kube-object-menu-registry";
export type { KubeObjectStatusRegistration } from "../registries/kube-object-status-registry" export type { KubeObjectStatusRegistration } from "../registries/kube-object-status-registry";
export type { PageRegistration, PageComponents } from "../registries/page-registry" export type { PageRegistration, PageComponents } from "../registries/page-registry";
export type { PageMenuRegistration, PageMenuComponents } from "../registries/page-menu-registry" export type { PageMenuRegistration, PageMenuComponents } from "../registries/page-menu-registry";
export type { StatusBarRegistration } from "../registries/status-bar-registry" export type { StatusBarRegistration } from "../registries/status-bar-registry";

View File

@ -21,9 +21,9 @@ export class LensExtension {
@observable private isEnabled = false; @observable private isEnabled = false;
constructor({ manifest, manifestPath, isBundled }: InstalledExtension) { constructor({ manifest, manifestPath, isBundled }: InstalledExtension) {
this.manifest = manifest this.manifest = manifest;
this.manifestPath = manifestPath this.manifestPath = manifestPath;
this.isBundled = !!isBundled this.isBundled = !!isBundled;
} }
get id(): LensExtensionId { get id(): LensExtensionId {
@ -31,15 +31,15 @@ export class LensExtension {
} }
get name() { get name() {
return this.manifest.name return this.manifest.name;
} }
get version() { get version() {
return this.manifest.version return this.manifest.version;
} }
get description() { get description() {
return this.manifest.description return this.manifest.description;
} }
@action @action
@ -60,18 +60,18 @@ export class LensExtension {
toggle(enable?: boolean) { toggle(enable?: boolean) {
if (typeof enable === "boolean") { if (typeof enable === "boolean") {
enable ? this.enable() : this.disable() enable ? this.enable() : this.disable();
} else { } else {
this.isEnabled ? this.disable() : this.enable() this.isEnabled ? this.disable() : this.enable();
} }
} }
async whenEnabled(handlers: () => Function[]) { async whenEnabled(handlers: () => Function[]) {
const disposers: Function[] = []; const disposers: Function[] = [];
const unregisterHandlers = () => { const unregisterHandlers = () => {
disposers.forEach(unregister => unregister()) disposers.forEach(unregister => unregister());
disposers.length = 0; disposers.length = 0;
} };
const cancelReaction = reaction(() => this.isEnabled, isEnabled => { const cancelReaction = reaction(() => this.isEnabled, isEnabled => {
if (isEnabled) { if (isEnabled) {
disposers.push(...handlers()); disposers.push(...handlers());
@ -80,11 +80,11 @@ export class LensExtension {
} }
}, { }, {
fireImmediately: true fireImmediately: true
}) });
return () => { return () => {
unregisterHandlers(); unregisterHandlers();
cancelReaction(); cancelReaction();
} };
} }
protected onActivate() { protected onActivate() {

View File

@ -1,11 +1,11 @@
import type { MenuRegistration } from "./registries/menu-registry"; import type { MenuRegistration } from "./registries/menu-registry";
import { observable } from "mobx"; import { observable } from "mobx";
import { LensExtension } from "./lens-extension" import { LensExtension } from "./lens-extension";
import { WindowManager } from "../main/window-manager"; import { WindowManager } from "../main/window-manager";
import { getExtensionPageUrl } from "./registries/page-registry" import { getExtensionPageUrl } from "./registries/page-registry";
export class LensMainExtension extends LensExtension { export class LensMainExtension extends LensExtension {
@observable.shallow appMenus: MenuRegistration[] = [] @observable.shallow appMenus: MenuRegistration[] = [];
async navigate<P extends object>(pageId?: string, params?: P, frameId?: number) { async navigate<P extends object>(pageId?: string, params?: P, frameId?: number) {
const windowManager = WindowManager.getInstance<WindowManager>(); const windowManager = WindowManager.getInstance<WindowManager>();

View File

@ -1,19 +1,19 @@
import type { AppPreferenceRegistration, ClusterFeatureRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration, KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, } from "./registries" import type { AppPreferenceRegistration, ClusterFeatureRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration, KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, } from "./registries";
import { observable } from "mobx"; import { observable } from "mobx";
import { LensExtension } from "./lens-extension" import { LensExtension } from "./lens-extension";
import { getExtensionPageUrl } from "./registries/page-registry" import { getExtensionPageUrl } from "./registries/page-registry";
export class LensRendererExtension extends LensExtension { export class LensRendererExtension extends LensExtension {
@observable.shallow globalPages: PageRegistration[] = [] @observable.shallow globalPages: PageRegistration[] = [];
@observable.shallow clusterPages: PageRegistration[] = [] @observable.shallow clusterPages: PageRegistration[] = [];
@observable.shallow globalPageMenus: PageMenuRegistration[] = [] @observable.shallow globalPageMenus: PageMenuRegistration[] = [];
@observable.shallow clusterPageMenus: PageMenuRegistration[] = [] @observable.shallow clusterPageMenus: PageMenuRegistration[] = [];
@observable.shallow kubeObjectStatusTexts: KubeObjectStatusRegistration[] = [] @observable.shallow kubeObjectStatusTexts: KubeObjectStatusRegistration[] = [];
@observable.shallow appPreferences: AppPreferenceRegistration[] = [] @observable.shallow appPreferences: AppPreferenceRegistration[] = [];
@observable.shallow clusterFeatures: ClusterFeatureRegistration[] = [] @observable.shallow clusterFeatures: ClusterFeatureRegistration[] = [];
@observable.shallow statusBarItems: StatusBarRegistration[] = [] @observable.shallow statusBarItems: StatusBarRegistration[] = [];
@observable.shallow kubeObjectDetailItems: KubeObjectDetailRegistration[] = [] @observable.shallow kubeObjectDetailItems: KubeObjectDetailRegistration[] = [];
@observable.shallow kubeObjectMenuItems: KubeObjectMenuRegistration[] = [] @observable.shallow kubeObjectMenuItems: KubeObjectMenuRegistration[] = [];
async navigate<P extends object>(pageId?: string, params?: P) { async navigate<P extends object>(pageId?: string, params?: P) {
const { navigate } = await import("../renderer/navigation"); const { navigate } = await import("../renderer/navigation");

View File

@ -1,8 +1,8 @@
import { getExtensionPageUrl, globalPageRegistry, PageRegistration } from "../page-registry" import { getExtensionPageUrl, globalPageRegistry, PageRegistration } from "../page-registry";
import { LensExtension } from "../../lens-extension" import { LensExtension } from "../../lens-extension";
import React from "react"; import React from "react";
let ext: LensExtension = null let ext: LensExtension = null;
describe("getPageUrl", () => { describe("getPageUrl", () => {
beforeEach(async () => { beforeEach(async () => {
@ -14,25 +14,25 @@ describe("getPageUrl", () => {
manifestPath: "/this/is/fake/package.json", manifestPath: "/this/is/fake/package.json",
isBundled: false, isBundled: false,
isEnabled: true isEnabled: true
}) });
}) });
it("returns a page url for extension", () => { it("returns a page url for extension", () => {
expect(getExtensionPageUrl({ extensionId: ext.name })).toBe("/extension/foo-bar") expect(getExtensionPageUrl({ extensionId: ext.name })).toBe("/extension/foo-bar");
}) });
it("allows to pass base url as parameter", () => { it("allows to pass base url as parameter", () => {
expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "/test" })).toBe("/extension/foo-bar/test") expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "/test" })).toBe("/extension/foo-bar/test");
}) });
it("removes @", () => { it("removes @", () => {
expect(getExtensionPageUrl({ extensionId: "@foo/bar" })).toBe("/extension/foo-bar") expect(getExtensionPageUrl({ extensionId: "@foo/bar" })).toBe("/extension/foo-bar");
}) });
it("adds / prefix", () => { it("adds / prefix", () => {
expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "test" })).toBe("/extension/foo-bar/test") expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "test" })).toBe("/extension/foo-bar/test");
}) });
}) });
describe("globalPageRegistry", () => { describe("globalPageRegistry", () => {
beforeEach(async () => { beforeEach(async () => {
@ -44,7 +44,7 @@ describe("globalPageRegistry", () => {
manifestPath: "/this/is/fake/package.json", manifestPath: "/this/is/fake/package.json",
isBundled: false, isBundled: false,
isEnabled: true isEnabled: true
}) });
globalPageRegistry.add([ globalPageRegistry.add([
{ {
id: "test-page", id: "test-page",
@ -63,12 +63,12 @@ describe("globalPageRegistry", () => {
Page: () => React.createElement('Default') Page: () => React.createElement('Default')
} }
}, },
], ext) ], ext);
}) });
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 }));
@ -78,16 +78,16 @@ describe("globalPageRegistry", () => {
const page = globalPageRegistry.getByPageMenuTarget({ const page = globalPageRegistry.getByPageMenuTarget({
pageId: "test-page", pageId: "test-page",
extensionId: ext.name extensionId: ext.name
}) });
expect(page.id).toEqual("test-page") expect(page.id).toEqual("test-page");
}) });
it("returns null if target not found", () => { it("returns null if target not found", () => {
const page = globalPageRegistry.getByPageMenuTarget({ const page = globalPageRegistry.getByPageMenuTarget({
pageId: "wrong-page", pageId: "wrong-page",
extensionId: ext.name extensionId: ext.name
}) });
expect(page).toBeNull() expect(page).toBeNull();
}) });
}) });
}) });

View File

@ -1,4 +1,4 @@
import type React from "react" import type React from "react";
import { BaseRegistry } from "./base-registry"; import { BaseRegistry } from "./base-registry";
export interface AppPreferenceComponents { export interface AppPreferenceComponents {
@ -14,4 +14,4 @@ export interface AppPreferenceRegistration {
export class AppPreferenceRegistry extends BaseRegistry<AppPreferenceRegistration> { export class AppPreferenceRegistry extends BaseRegistry<AppPreferenceRegistration> {
} }
export const appPreferenceRegistry = new AppPreferenceRegistry() export const appPreferenceRegistry = new AppPreferenceRegistry();

View File

@ -12,7 +12,7 @@ export class BaseRegistry<T = object, I extends T = T> {
add(items: T | T[], ext?: LensExtension): () => void; // allow method overloading with required "ext" add(items: T | T[], ext?: LensExtension): () => void; // allow method overloading with required "ext"
@action @action
add(items: T | T[]) { add(items: T | T[]) {
const normalizedItems = (Array.isArray(items) ? items : [items]) const normalizedItems = (Array.isArray(items) ? items : [items]);
this.items.push(...normalizedItems); this.items.push(...normalizedItems);
return () => this.remove(...normalizedItems); return () => this.remove(...normalizedItems);
} }
@ -21,6 +21,6 @@ export class BaseRegistry<T = object, I extends T = T> {
remove(...items: T[]) { remove(...items: T[]) {
items.forEach(item => { items.forEach(item => {
this.items.remove(item); // works because of {deep: false}; this.items.remove(item); // works because of {deep: false};
}) });
} }
} }

View File

@ -1,4 +1,4 @@
import type React from "react" import type React from "react";
import { BaseRegistry } from "./base-registry"; import { BaseRegistry } from "./base-registry";
import { ClusterFeature } from "../cluster-feature"; import { ClusterFeature } from "../cluster-feature";
@ -15,4 +15,4 @@ export interface ClusterFeatureRegistration {
export class ClusterFeatureRegistry extends BaseRegistry<ClusterFeatureRegistration> { export class ClusterFeatureRegistry extends BaseRegistry<ClusterFeatureRegistration> {
} }
export const clusterFeatureRegistry = new ClusterFeatureRegistry() export const clusterFeatureRegistry = new ClusterFeatureRegistry();

View File

@ -1,11 +1,11 @@
// All registries managed by extensions api // All registries managed by extensions api
export * from "./page-registry" export * from "./page-registry";
export * from "./page-menu-registry" export * from "./page-menu-registry";
export * from "./menu-registry" export * from "./menu-registry";
export * from "./app-preference-registry" export * from "./app-preference-registry";
export * from "./status-bar-registry" export * from "./status-bar-registry";
export * from "./kube-object-detail-registry"; export * from "./kube-object-detail-registry";
export * from "./kube-object-menu-registry"; export * from "./kube-object-menu-registry";
export * from "./cluster-feature-registry" export * from "./cluster-feature-registry";
export * from "./kube-object-status-registry" export * from "./kube-object-status-registry";

View File

@ -1,4 +1,4 @@
import React from "react" import React from "react";
import { BaseRegistry } from "./base-registry"; import { BaseRegistry } from "./base-registry";
export interface KubeObjectDetailComponents { export interface KubeObjectDetailComponents {
@ -15,15 +15,15 @@ export interface KubeObjectDetailRegistration {
export class KubeObjectDetailRegistry extends BaseRegistry<KubeObjectDetailRegistration> { export class KubeObjectDetailRegistry extends BaseRegistry<KubeObjectDetailRegistration> {
getItemsForKind(kind: string, apiVersion: string) { getItemsForKind(kind: string, apiVersion: string) {
const items = this.getItems().filter((item) => { const items = this.getItems().filter((item) => {
return item.kind === kind && item.apiVersions.includes(apiVersion) return item.kind === kind && item.apiVersions.includes(apiVersion);
}).map((item) => { }).map((item) => {
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);
} }
} }
export const kubeObjectDetailRegistry = new KubeObjectDetailRegistry() export const kubeObjectDetailRegistry = new KubeObjectDetailRegistry();

View File

@ -1,4 +1,4 @@
import React from "react" import React from "react";
import { BaseRegistry } from "./base-registry"; import { BaseRegistry } from "./base-registry";
export interface KubeObjectMenuComponents { export interface KubeObjectMenuComponents {
@ -14,9 +14,9 @@ export interface KubeObjectMenuRegistration {
export class KubeObjectMenuRegistry extends BaseRegistry<KubeObjectMenuRegistration> { export class KubeObjectMenuRegistry extends BaseRegistry<KubeObjectMenuRegistration> {
getItemsForKind(kind: string, apiVersion: string) { getItemsForKind(kind: string, apiVersion: string) {
return this.getItems().filter((item) => { return this.getItems().filter((item) => {
return item.kind === kind && item.apiVersions.includes(apiVersion) return item.kind === kind && item.apiVersions.includes(apiVersion);
}) });
} }
} }
export const kubeObjectMenuRegistry = new KubeObjectMenuRegistry() export const kubeObjectMenuRegistry = new KubeObjectMenuRegistry();

View File

@ -10,8 +10,8 @@ export interface KubeObjectStatusRegistration {
export class KubeObjectStatusRegistry extends BaseRegistry<KubeObjectStatusRegistration> { export class KubeObjectStatusRegistry extends BaseRegistry<KubeObjectStatusRegistration> {
getItemsForKind(kind: string, apiVersion: string) { getItemsForKind(kind: string, apiVersion: string) {
return this.getItems().filter((item) => { return this.getItems().filter((item) => {
return item.kind === kind && item.apiVersions.includes(apiVersion) return item.kind === kind && item.apiVersions.includes(apiVersion);
}) });
} }
} }

View File

@ -29,8 +29,8 @@ export class PageMenuRegistry extends BaseRegistry<PageMenuRegistration, Require
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,7 +45,7 @@ export interface PageComponents {
} }
export function sanitizeExtensionName(name: string) { export function sanitizeExtensionName(name: string) {
return name.replace("@", "").replace("/", "-") return name.replace("@", "").replace("/", "-");
} }
export function getExtensionPageUrl<P extends object>({ extensionId, pageId = "", params }: PageMenuTarget<P>): string { export function getExtensionPageUrl<P extends object>({ extensionId, pageId = "", params }: PageMenuTarget<P>): string {
@ -68,13 +68,13 @@ export class PageRegistry extends BaseRegistry<PageRegistration, RegisteredPage>
...page, ...page,
extensionId: ext.name, extensionId: ext.name,
routePath: getExtensionPageUrl({ extensionId: ext.name, pageId: page.id ?? page.routePath }), routePath: getExtensionPageUrl({ extensionId: ext.name, pageId: page.id ?? page.routePath }),
})) }));
} catch (err) { } catch (err) {
logger.error(`[EXTENSION]: page-registration failed`, { logger.error(`[EXTENSION]: page-registration failed`, {
items, items,
extension: ext, extension: ext,
error: String(err), error: String(err),
}) });
} }
return super.add(registeredPages); return super.add(registeredPages);
} }

View File

@ -1,36 +1,36 @@
// Common UI components // Common UI components
// layouts // layouts
export * from "../../renderer/components/layout/page-layout" export * from "../../renderer/components/layout/page-layout";
export * from "../../renderer/components/layout/wizard-layout" export * from "../../renderer/components/layout/wizard-layout";
export * from "../../renderer/components/layout/tab-layout" export * from "../../renderer/components/layout/tab-layout";
// form-controls // form-controls
export * from "../../renderer/components/button" export * from "../../renderer/components/button";
export * from "../../renderer/components/checkbox" export * from "../../renderer/components/checkbox";
export * from "../../renderer/components/radio" export * from "../../renderer/components/radio";
export * from "../../renderer/components/select" export * from "../../renderer/components/select";
export * from "../../renderer/components/slider" export * from "../../renderer/components/slider";
export * from "../../renderer/components/input/input" export * from "../../renderer/components/input/input";
// other components // other components
export * from "../../renderer/components/icon" export * from "../../renderer/components/icon";
export * from "../../renderer/components/tooltip" export * from "../../renderer/components/tooltip";
export * from "../../renderer/components/tabs" export * from "../../renderer/components/tabs";
export * from "../../renderer/components/table" export * from "../../renderer/components/table";
export * from "../../renderer/components/badge" export * from "../../renderer/components/badge";
export * from "../../renderer/components/drawer" export * from "../../renderer/components/drawer";
export * from "../../renderer/components/dialog" export * from "../../renderer/components/dialog";
export * from "../../renderer/components/confirm-dialog"; export * from "../../renderer/components/confirm-dialog";
export * from "../../renderer/components/line-progress" export * from "../../renderer/components/line-progress";
export * from "../../renderer/components/menu" export * from "../../renderer/components/menu";
export * from "../../renderer/components/notifications" export * from "../../renderer/components/notifications";
export * from "../../renderer/components/spinner" export * from "../../renderer/components/spinner";
export * from "../../renderer/components/stepper" export * from "../../renderer/components/stepper";
// kube helpers // kube helpers
export * from "../../renderer/components/kube-object" export * from "../../renderer/components/kube-object";
export * from "../../renderer/components/+events/kube-event-details" export * from "../../renderer/components/+events/kube-event-details";
// specific exports // specific exports
export * from "../../renderer/components/status-brick"; export * from "../../renderer/components/status-brick";

View File

@ -1,14 +1,14 @@
// Lens-extensions apis, required in renderer process runtime // Lens-extensions apis, required in renderer process runtime
// APIs // APIs
import * as Component from "./components" import * as Component from "./components";
import * as K8sApi from "./k8s-api" import * as K8sApi from "./k8s-api";
import * as Navigation from "./navigation" import * as Navigation from "./navigation";
import * as Theme from "./theming" import * as Theme from "./theming";
export { export {
Component, Component,
K8sApi, K8sApi,
Navigation, Navigation,
Theme, Theme,
} };

View File

@ -1,6 +1,6 @@
export { isAllowedResource } from "../../common/rbac" export { isAllowedResource } from "../../common/rbac";
export { apiManager } from "../../renderer/api/api-manager"; export { apiManager } from "../../renderer/api/api-manager";
export { KubeObjectStore } from "../../renderer/kube-object.store" export { KubeObjectStore } from "../../renderer/kube-object.store";
export { KubeApi, forCluster, IKubeApiCluster } from "../../renderer/api/kube-api"; export { KubeApi, forCluster, IKubeApiCluster } from "../../renderer/api/kube-api";
export { KubeObject } from "../../renderer/api/kube-object"; export { KubeObject } from "../../renderer/api/kube-object";
export { Pod, podsApi, PodsApi, IPodContainer, IPodContainerStatus } from "../../renderer/api/endpoints"; export { Pod, podsApi, PodsApi, IPodContainer, IPodContainerStatus } from "../../renderer/api/endpoints";
@ -31,33 +31,33 @@ export { RoleBinding, roleBindingApi } from "../../renderer/api/endpoints";
export { ClusterRole, clusterRoleApi } from "../../renderer/api/endpoints"; export { ClusterRole, clusterRoleApi } from "../../renderer/api/endpoints";
export { ClusterRoleBinding, clusterRoleBindingApi } from "../../renderer/api/endpoints"; export { ClusterRoleBinding, clusterRoleBindingApi } from "../../renderer/api/endpoints";
export { CustomResourceDefinition, crdApi } from "../../renderer/api/endpoints"; export { CustomResourceDefinition, crdApi } from "../../renderer/api/endpoints";
export { KubeObjectStatus, KubeObjectStatusLevel } from "./kube-object-status" export { KubeObjectStatus, KubeObjectStatusLevel } from "./kube-object-status";
// stores // stores
export type { EventStore } from "../../renderer/components/+events/event.store" export type { EventStore } from "../../renderer/components/+events/event.store";
export type { PodsStore } from "../../renderer/components/+workloads-pods/pods.store" export type { PodsStore } from "../../renderer/components/+workloads-pods/pods.store";
export type { NodesStore } from "../../renderer/components/+nodes/nodes.store" export type { NodesStore } from "../../renderer/components/+nodes/nodes.store";
export type { DeploymentStore } from "../../renderer/components/+workloads-deployments/deployments.store" export type { DeploymentStore } from "../../renderer/components/+workloads-deployments/deployments.store";
export type { DaemonSetStore } from "../../renderer/components/+workloads-daemonsets/daemonsets.store" export type { DaemonSetStore } from "../../renderer/components/+workloads-daemonsets/daemonsets.store";
export type { StatefulSetStore } from "../../renderer/components/+workloads-statefulsets/statefulset.store" export type { StatefulSetStore } from "../../renderer/components/+workloads-statefulsets/statefulset.store";
export type { JobStore } from "../../renderer/components/+workloads-jobs/job.store" export type { JobStore } from "../../renderer/components/+workloads-jobs/job.store";
export type { CronJobStore } from "../../renderer/components/+workloads-cronjobs/cronjob.store" export type { CronJobStore } from "../../renderer/components/+workloads-cronjobs/cronjob.store";
export type { ConfigMapsStore } from "../../renderer/components/+config-maps/config-maps.store" export type { ConfigMapsStore } from "../../renderer/components/+config-maps/config-maps.store";
export type { SecretsStore } from "../../renderer/components/+config-secrets/secrets.store" export type { SecretsStore } from "../../renderer/components/+config-secrets/secrets.store";
export type { ReplicaSetStore } from "../../renderer/components/+workloads-replicasets/replicasets.store" export type { ReplicaSetStore } from "../../renderer/components/+workloads-replicasets/replicasets.store";
export type { ResourceQuotasStore } from "../../renderer/components/+config-resource-quotas/resource-quotas.store" export type { ResourceQuotasStore } from "../../renderer/components/+config-resource-quotas/resource-quotas.store";
export type { HPAStore } from "../../renderer/components/+config-autoscalers/hpa.store" export type { HPAStore } from "../../renderer/components/+config-autoscalers/hpa.store";
export type { PodDisruptionBudgetsStore } from "../../renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.store" export type { PodDisruptionBudgetsStore } from "../../renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.store";
export type { ServiceStore } from "../../renderer/components/+network-services/services.store" export type { ServiceStore } from "../../renderer/components/+network-services/services.store";
export type { EndpointStore } from "../../renderer/components/+network-endpoints/endpoints.store" export type { EndpointStore } from "../../renderer/components/+network-endpoints/endpoints.store";
export type { IngressStore } from "../../renderer/components/+network-ingresses/ingress.store" export type { IngressStore } from "../../renderer/components/+network-ingresses/ingress.store";
export type { NetworkPolicyStore } from "../../renderer/components/+network-policies/network-policy.store" export type { NetworkPolicyStore } from "../../renderer/components/+network-policies/network-policy.store";
export type { PersistentVolumesStore } from "../../renderer/components/+storage-volumes/volumes.store" export type { PersistentVolumesStore } from "../../renderer/components/+storage-volumes/volumes.store";
export type { VolumeClaimStore } from "../../renderer/components/+storage-volume-claims/volume-claim.store" export type { VolumeClaimStore } from "../../renderer/components/+storage-volume-claims/volume-claim.store";
export type { StorageClassStore } from "../../renderer/components/+storage-classes/storage-class.store" export type { StorageClassStore } from "../../renderer/components/+storage-classes/storage-class.store";
export type { NamespaceStore } from "../../renderer/components/+namespaces/namespace.store" export type { NamespaceStore } from "../../renderer/components/+namespaces/namespace.store";
export type { ServiceAccountsStore } from "../../renderer/components/+user-management-service-accounts/service-accounts.store" export type { ServiceAccountsStore } from "../../renderer/components/+user-management-service-accounts/service-accounts.store";
export type { RolesStore } from "../../renderer/components/+user-management-roles/roles.store" export type { RolesStore } from "../../renderer/components/+user-management-roles/roles.store";
export type { RoleBindingsStore } from "../../renderer/components/+user-management-roles-bindings/role-bindings.store" export type { RoleBindingsStore } from "../../renderer/components/+user-management-roles-bindings/role-bindings.store";
export type { CRDStore } from "../../renderer/components/+custom-resources/crd.store" export type { CRDStore } from "../../renderer/components/+custom-resources/crd.store";
export type { CRDResourceStore } from "../../renderer/components/+custom-resources/crd-resource.store" export type { CRDResourceStore } from "../../renderer/components/+custom-resources/crd-resource.store";

View File

@ -2,7 +2,7 @@ export type KubeObjectStatus = {
level: KubeObjectStatusLevel; level: KubeObjectStatusLevel;
text: string; text: string;
timestamp?: string; timestamp?: string;
} };
export enum KubeObjectStatusLevel { export enum KubeObjectStatusLevel {
INFO = 1, INFO = 1,

View File

@ -1,3 +1,3 @@
export { navigate } from "../../renderer/navigation"; export { navigate } from "../../renderer/navigation";
export { hideDetails, showDetails, getDetailsUrl } from "../../renderer/navigation" export { hideDetails, showDetails, getDetailsUrl } from "../../renderer/navigation";
export { IURLParams } from "../../common/utils/buildUrl"; export { IURLParams } from "../../common/utils/buildUrl";

View File

@ -1,4 +1,4 @@
import fetchMock from "jest-fetch-mock" import fetchMock from "jest-fetch-mock";
// rewire global.fetch to call 'fetchMock' // rewire global.fetch to call 'fetchMock'
fetchMock.enableMocks(); fetchMock.enableMocks();

View File

@ -21,35 +21,35 @@ jest.mock("winston", () => ({
Console: jest.fn(), Console: jest.fn(),
File: jest.fn(), File: jest.fn(),
} }
})) }));
jest.mock("../../common/ipc") jest.mock("../../common/ipc");
jest.mock("../context-handler") jest.mock("../context-handler");
jest.mock("request") jest.mock("request");
jest.mock("request-promise-native") jest.mock("request-promise-native");
import { Console } from "console"; import { Console } from "console";
import mockFs from "mock-fs"; import mockFs from "mock-fs";
import { workspaceStore } from "../../common/workspace-store"; import { workspaceStore } from "../../common/workspace-store";
import { Cluster } from "../cluster" import { Cluster } from "../cluster";
import { ContextHandler } from "../context-handler"; import { ContextHandler } from "../context-handler";
import { getFreePort } from "../port"; import { getFreePort } from "../port";
import { V1ResourceAttributes } from "@kubernetes/client-node"; import { V1ResourceAttributes } from "@kubernetes/client-node";
import { apiResources } from "../../common/rbac"; import { apiResources } from "../../common/rbac";
import request from "request-promise-native" import request from "request-promise-native";
import { Kubectl } from "../kubectl"; import { Kubectl } from "../kubectl";
const mockedRequest = request as jest.MockedFunction<typeof request> const mockedRequest = request as jest.MockedFunction<typeof request>;
console = new Console(process.stdout, process.stderr) // fix mockFS console = new Console(process.stdout, process.stderr); // fix mockFS
describe("create clusters", () => { describe("create clusters", () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks() jest.clearAllMocks();
}) });
let c: Cluster let c: Cluster;
beforeEach(() => { beforeEach(() => {
const mockOpts = { const mockOpts = {
@ -74,68 +74,68 @@ describe("create clusters", () => {
kind: "Config", kind: "Config",
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({
id: "foo", id: "foo",
contextName: "minikube", contextName: "minikube",
kubeConfigPath: "minikube-config.yml", kubeConfigPath: "minikube-config.yml",
workspace: workspaceStore.currentWorkspaceId workspace: workspaceStore.currentWorkspaceId
}) });
}) });
afterEach(() => { afterEach(() => {
mockFs.restore() mockFs.restore();
}) });
it("should be able to create a cluster from a cluster model and apiURL should be decoded", () => { it("should be able to create a cluster from a cluster model and apiURL should be decoded", () => {
expect(c.apiUrl).toBe("https://192.168.64.3:8443") expect(c.apiUrl).toBe("https://192.168.64.3:8443");
}) });
it("reconnect should not throw if contextHandler is missing", () => { it("reconnect should not throw if contextHandler is missing", () => {
expect(() => c.reconnect()).not.toThrowError() expect(() => c.reconnect()).not.toThrowError();
}) });
it("disconnect should not throw if contextHandler is missing", () => { it("disconnect should not throw if contextHandler is missing", () => {
expect(() => c.disconnect()).not.toThrowError() expect(() => c.disconnect()).not.toThrowError();
}) });
it("init should not throw if everything is in order", async () => { it("init should not throw if everything is in order", async () => {
await c.init(await getFreePort()) await c.init(await getFreePort());
expect(logger.info).toBeCalledWith(expect.stringContaining("init success"), { expect(logger.info).toBeCalledWith(expect.stringContaining("init success"), {
id: "foo", id: "foo",
apiUrl: "https://192.168.64.3:8443", apiUrl: "https://192.168.64.3:8443",
context: "minikube", context: "minikube",
}) });
}) });
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();
const mockKC = { const mockKC = {
makeApiClient() { makeApiClient() {
return { return {
listNamespace: mockListNSs, listNamespace: mockListNSs,
} };
} }
} };
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);
mockListNSs.mockImplementationOnce(() => ({ mockListNSs.mockImplementationOnce(() => ({
body: { body: {
items: [{ items: [{
@ -144,36 +144,36 @@ describe("create clusters", () => {
} }
}] }]
} }
})) }));
mockedRequest.mockImplementationOnce(((uri: any, _options: any) => { mockedRequest.mockImplementationOnce(((uri: any, _options: 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);
const c = new class extends Cluster { const c = new class extends Cluster {
// only way to mock protected methods, without these we leak promises // only way to mock protected methods, without these we leak promises
protected bindEvents() { protected bindEvents() {
return return;
} }
protected async ensureKubectl() { protected async ensureKubectl() {
return Promise.resolve(true) return Promise.resolve(true);
} }
}({ }({
id: "foo", id: "foo",
contextName: "minikube", contextName: "minikube",
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();
expect(ContextHandler.prototype.ensureServer).toBeCalled() expect(ContextHandler.prototype.ensureServer).toBeCalled();
expect(mockedRequest).toBeCalled() expect(mockedRequest).toBeCalled();
expect(c.accessible).toBe(true) expect(c.accessible).toBe(true);
expect(c.allowedNamespaces.length).toBe(1) expect(c.allowedNamespaces.length).toBe(1);
expect(c.allowedResources.length).toBe(apiResources.length) expect(c.allowedResources.length).toBe(apiResources.length);
c.disconnect() c.disconnect();
jest.resetAllMocks() jest.resetAllMocks();
}) });
}) });

View File

@ -21,109 +21,109 @@ jest.mock("winston", () => ({
Console: jest.fn(), Console: jest.fn(),
File: jest.fn(), File: jest.fn(),
} }
})) }));
jest.mock("../../common/ipc") jest.mock("../../common/ipc");
jest.mock("child_process") jest.mock("child_process");
jest.mock("tcp-port-used") jest.mock("tcp-port-used");
import { Cluster } from "../cluster" import { Cluster } from "../cluster";
import { KubeAuthProxy } from "../kube-auth-proxy" import { KubeAuthProxy } from "../kube-auth-proxy";
import { getFreePort } from "../port" import { getFreePort } from "../port";
import { broadcastMessage } from "../../common/ipc" import { broadcastMessage } from "../../common/ipc";
import { ChildProcess, spawn, SpawnOptions } from "child_process" import { ChildProcess, spawn, SpawnOptions } from "child_process";
import { bundledKubectlPath, Kubectl } from "../kubectl" import { bundledKubectlPath, Kubectl } from "../kubectl";
import { mock, MockProxy } from 'jest-mock-extended'; import { mock, MockProxy } from 'jest-mock-extended';
import { waitUntilUsed } from 'tcp-port-used'; import { waitUntilUsed } from 'tcp-port-used';
import { Readable } from "stream" import { Readable } from "stream";
const mockBroadcastIpc = broadcastMessage as jest.MockedFunction<typeof broadcastMessage> const mockBroadcastIpc = broadcastMessage as jest.MockedFunction<typeof broadcastMessage>;
const mockSpawn = spawn as jest.MockedFunction<typeof spawn> const mockSpawn = spawn as jest.MockedFunction<typeof spawn>;
const mockWaitUntilUsed = waitUntilUsed as jest.MockedFunction<typeof waitUntilUsed> const mockWaitUntilUsed = waitUntilUsed as jest.MockedFunction<typeof waitUntilUsed>;
describe("kube auth proxy tests", () => { describe("kube auth proxy tests", () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks() jest.clearAllMocks();
}) });
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();
}) });
describe("spawn tests", () => { describe("spawn tests", () => {
let port: number let port: number;
let mockedCP: MockProxy<ChildProcess> let mockedCP: MockProxy<ChildProcess>;
let listeners: Record<string, (...args: any[]) => void> let listeners: Record<string, (...args: any[]) => void>;
let proxy: KubeAuthProxy let proxy: KubeAuthProxy;
beforeEach(async () => { beforeEach(async () => {
port = await getFreePort() port = await getFreePort();
mockedCP = mock<ChildProcess>() mockedCP = mock<ChildProcess>();
listeners = {} listeners = {};
jest.spyOn(Kubectl.prototype, "checkBinary").mockReturnValueOnce(Promise.resolve(true)) jest.spyOn(Kubectl.prototype, "checkBinary").mockReturnValueOnce(Promise.resolve(true));
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, args: readonly string[], options: SpawnOptions): ChildProcess => { mockSpawn.mockImplementationOnce((command: string, args: readonly string[], options: SpawnOptions): 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, {});
}) });
it("should call spawn and broadcast errors", async () => { it("should call spawn and broadcast errors", async () => {
await proxy.run() await proxy.run();
listeners["error"]({ message: "foobarbat" }) listeners["error"]({ message: "foobarbat" });
expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "foobarbat", error: true }) expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "foobarbat", error: true });
}) });
it("should call spawn and broadcast exit", async () => { it("should call spawn and broadcast exit", async () => {
await proxy.run() await proxy.run();
listeners["exit"](0) listeners["exit"](0);
expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "proxy exited with code: 0", error: false }) expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "proxy exited with code: 0", error: false });
}) });
it("should call spawn and broadcast errors from stderr", async () => { it("should call spawn and broadcast errors from stderr", async () => {
await proxy.run() await proxy.run();
listeners["stderr/data"]("an error") listeners["stderr/data"]("an error");
expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "an error", error: true }) expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "an error", error: true });
}) });
it("should call spawn and broadcast stdout serving info", async () => { it("should call spawn and broadcast stdout serving info", async () => {
await proxy.run() await proxy.run();
listeners["stdout/data"]("Starting to serve on") listeners["stdout/data"]("Starting to serve on");
expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "Authentication proxy started\n" }) expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "Authentication proxy started\n" });
}) });
it("should call spawn and broadcast stdout other info", async () => { it("should call spawn and broadcast stdout other info", async () => {
await proxy.run() await proxy.run();
listeners["stdout/data"]("some info") listeners["stdout/data"]("some info");
expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "some info" }) expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "some info" });
}) });
}) });
}) });

View File

@ -21,24 +21,24 @@ jest.mock("winston", () => ({
Console: jest.fn(), Console: jest.fn(),
File: jest.fn(), File: jest.fn(),
} }
})) }));
import { KubeconfigManager } from "../kubeconfig-manager" import { KubeconfigManager } from "../kubeconfig-manager";
import mockFs from "mock-fs" import mockFs from "mock-fs";
import { Cluster } from "../cluster"; import { Cluster } from "../cluster";
import { workspaceStore } from "../../common/workspace-store"; import { workspaceStore } from "../../common/workspace-store";
import { ContextHandler } from "../context-handler"; import { ContextHandler } from "../context-handler";
import { getFreePort } from "../port"; import { getFreePort } from "../port";
import fse from "fs-extra" import fse from "fs-extra";
import { loadYaml } from "@kubernetes/client-node"; import { loadYaml } from "@kubernetes/client-node";
import { Console } from "console"; import { Console } from "console";
console = new Console(process.stdout, process.stderr) // fix mockFS console = new Console(process.stdout, process.stderr); // fix mockFS
describe("kubeconfig manager tests", () => { describe("kubeconfig manager tests", () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks() jest.clearAllMocks();
}) });
beforeEach(() => { beforeEach(() => {
const mockOpts = { const mockOpts = {
@ -63,13 +63,13 @@ describe("kubeconfig manager tests", () => {
kind: "Config", kind: "Config",
preferences: {}, preferences: {},
}) })
} };
mockFs(mockOpts) mockFs(mockOpts);
}) });
afterEach(() => { afterEach(() => {
mockFs.restore() mockFs.restore();
}) });
it("should create 'temp' kube config with proxy", async () => { it("should create 'temp' kube config with proxy", async () => {
const cluster = new Cluster({ const cluster = new Cluster({
@ -77,19 +77,19 @@ describe("kubeconfig manager tests", () => {
contextName: "minikube", contextName: "minikube",
kubeConfigPath: "minikube-config.yml", kubeConfigPath: "minikube-config.yml",
workspace: workspaceStore.currentWorkspaceId workspace: workspaceStore.currentWorkspaceId
}) });
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);
expect(logger.error).not.toBeCalled() expect(logger.error).not.toBeCalled();
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");
}) });
it("should remove 'temp' kube config on unlink and remove reference from inside class", async () => { it("should remove 'temp' kube config on unlink and remove reference from inside class", async () => {
const cluster = new Cluster({ const cluster = new Cluster({
@ -97,16 +97,16 @@ describe("kubeconfig manager tests", () => {
contextName: "minikube", contextName: "minikube",
kubeConfigPath: "minikube-config.yml", kubeConfigPath: "minikube-config.yml",
workspace: workspaceStore.currentWorkspaceId workspace: workspaceStore.currentWorkspaceId
}) });
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);
await kubeConfManager.unlink() // doesn't throw await kubeConfManager.unlink(); // doesn't throw
expect(kubeConfManager.getPath()).toBeUndefined() expect(kubeConfManager.getPath()).toBeUndefined();
}) });
}) });

View File

@ -1,19 +1,19 @@
import { autoUpdater } from "electron-updater" import { autoUpdater } from "electron-updater";
import logger from "./logger" import logger from "./logger";
export class AppUpdater { export class AppUpdater {
static readonly defaultUpdateIntervalMs = 1000 * 60 * 60 * 24 // once a day static readonly defaultUpdateIntervalMs = 1000 * 60 * 60 * 24; // once a day
static checkForUpdates() { static checkForUpdates() {
return autoUpdater.checkForUpdatesAndNotify() return autoUpdater.checkForUpdatesAndNotify();
} }
constructor(protected updateInterval = AppUpdater.defaultUpdateIntervalMs) { constructor(protected updateInterval = AppUpdater.defaultUpdateIntervalMs) {
autoUpdater.logger = logger autoUpdater.logger = logger;
} }
public start() { public start() {
setInterval(AppUpdater.checkForUpdates, this.updateInterval) setInterval(AppUpdater.checkForUpdates, this.updateInterval);
return AppUpdater.checkForUpdates(); return AppUpdater.checkForUpdates();
} }
} }

View File

@ -1,21 +1,21 @@
import request, { RequestPromiseOptions } from "request-promise-native" import request, { RequestPromiseOptions } from "request-promise-native";
import { Cluster } from "../cluster"; import { Cluster } from "../cluster";
export type ClusterDetectionResult = { export type ClusterDetectionResult = {
value: string | number | boolean value: string | number | boolean
accuracy: number accuracy: number
} };
export class BaseClusterDetector { export class BaseClusterDetector {
cluster: Cluster cluster: Cluster;
key: string key: string;
constructor(cluster: Cluster) { constructor(cluster: Cluster) {
this.cluster = cluster this.cluster = cluster;
} }
detect(): Promise<ClusterDetectionResult> { detect(): Promise<ClusterDetectionResult> {
return null return null;
} }
protected async k8sRequest<T = any>(path: string, options: RequestPromiseOptions = {}): Promise<T> { protected async k8sRequest<T = any>(path: string, options: RequestPromiseOptions = {}): Promise<T> {
@ -28,6 +28,6 @@ export class BaseClusterDetector {
Host: `${this.cluster.id}.${new URL(this.cluster.kubeProxyUrl).host}`, // required in ClusterManager.getClusterForRequest() Host: `${this.cluster.id}.${new URL(this.cluster.kubeProxyUrl).host}`, // required in ClusterManager.getClusterForRequest()
...(options.headers || {}), ...(options.headers || {}),
}, },
}) });
} }
} }

View File

@ -1,23 +1,23 @@
import { BaseClusterDetector } from "./base-cluster-detector"; import { BaseClusterDetector } from "./base-cluster-detector";
import { createHash } from "crypto" import { createHash } from "crypto";
import { ClusterMetadataKey } from "../cluster"; import { ClusterMetadataKey } from "../cluster";
export class ClusterIdDetector extends BaseClusterDetector { export class ClusterIdDetector extends BaseClusterDetector {
key = ClusterMetadataKey.CLUSTER_ID key = ClusterMetadataKey.CLUSTER_ID;
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: value, accuracy: 100 } return { value: 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;
} }
} }

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