mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Add eslint rule padding-line-between-statements (#1593)
Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>
This commit is contained in:
parent
7b77f18376
commit
dcf253e7d5
27
.eslintrc.js
27
.eslintrc.js
@ -49,6 +49,15 @@ module.exports = {
|
|||||||
"object-shorthand": "error",
|
"object-shorthand": "error",
|
||||||
"prefer-template": "error",
|
"prefer-template": "error",
|
||||||
"template-curly-spacing": "error",
|
"template-curly-spacing": "error",
|
||||||
|
"padding-line-between-statements": [
|
||||||
|
"error",
|
||||||
|
{ "blankLine": "always", "prev": "*", "next": "return" },
|
||||||
|
{ "blankLine": "always", "prev": "*", "next": "block-like" },
|
||||||
|
{ "blankLine": "always", "prev": "*", "next": "function" },
|
||||||
|
{ "blankLine": "always", "prev": "*", "next": "class" },
|
||||||
|
{ "blankLine": "always", "prev": ["const", "let", "var"], "next": "*" },
|
||||||
|
{ "blankLine": "any", "prev": ["const", "let", "var"], "next": ["const", "let", "var"]},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -94,6 +103,15 @@ module.exports = {
|
|||||||
"object-shorthand": "error",
|
"object-shorthand": "error",
|
||||||
"prefer-template": "error",
|
"prefer-template": "error",
|
||||||
"template-curly-spacing": "error",
|
"template-curly-spacing": "error",
|
||||||
|
"padding-line-between-statements": [
|
||||||
|
"error",
|
||||||
|
{ "blankLine": "always", "prev": "*", "next": "return" },
|
||||||
|
{ "blankLine": "always", "prev": "*", "next": "block-like" },
|
||||||
|
{ "blankLine": "always", "prev": "*", "next": "function" },
|
||||||
|
{ "blankLine": "always", "prev": "*", "next": "class" },
|
||||||
|
{ "blankLine": "always", "prev": ["const", "let", "var"], "next": "*" },
|
||||||
|
{ "blankLine": "any", "prev": ["const", "let", "var"], "next": ["const", "let", "var"]},
|
||||||
|
]
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -146,6 +164,15 @@ module.exports = {
|
|||||||
"object-shorthand": "error",
|
"object-shorthand": "error",
|
||||||
"prefer-template": "error",
|
"prefer-template": "error",
|
||||||
"template-curly-spacing": "error",
|
"template-curly-spacing": "error",
|
||||||
|
"padding-line-between-statements": [
|
||||||
|
"error",
|
||||||
|
{ "blankLine": "always", "prev": "*", "next": "return" },
|
||||||
|
{ "blankLine": "always", "prev": "*", "next": "block-like" },
|
||||||
|
{ "blankLine": "always", "prev": "*", "next": "function" },
|
||||||
|
{ "blankLine": "always", "prev": "*", "next": "class" },
|
||||||
|
{ "blankLine": "always", "prev": ["const", "let", "var"], "next": "*" },
|
||||||
|
{ "blankLine": "any", "prev": ["const", "let", "var"], "next": ["const", "let", "var"]},
|
||||||
|
]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -17,14 +17,15 @@ export async function generateTrayIcon(
|
|||||||
outputFilename += shouldUseDarkColors ? "_dark" : "";
|
outputFilename += shouldUseDarkColors ? "_dark" : "";
|
||||||
dpiSuffix = dpiSuffix !== "1x" ? `@${dpiSuffix}` : "";
|
dpiSuffix = dpiSuffix !== "1x" ? `@${dpiSuffix}` : "";
|
||||||
const pngIconDestPath = path.resolve(outputFolder, `${outputFilename}${dpiSuffix}.png`);
|
const pngIconDestPath = path.resolve(outputFolder, `${outputFilename}${dpiSuffix}.png`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Modify .SVG colors
|
// Modify .SVG colors
|
||||||
const trayIconColor = shouldUseDarkColors ? "white" : "black";
|
const trayIconColor = shouldUseDarkColors ? "white" : "black";
|
||||||
const svgDom = await jsdom.JSDOM.fromFile(svgIconPath);
|
const svgDom = await jsdom.JSDOM.fromFile(svgIconPath);
|
||||||
const svgRoot = svgDom.window.document.body.getElementsByTagName("svg")[0];
|
const svgRoot = svgDom.window.document.body.getElementsByTagName("svg")[0];
|
||||||
|
|
||||||
svgRoot.innerHTML += `<style>* {fill: ${trayIconColor} !important;}</style>`;
|
svgRoot.innerHTML += `<style>* {fill: ${trayIconColor} !important;}</style>`;
|
||||||
const svgIconBuffer = Buffer.from(svgRoot.outerHTML);
|
const svgIconBuffer = Buffer.from(svgRoot.outerHTML);
|
||||||
|
|
||||||
// Resize and convert to .PNG
|
// Resize and convert to .PNG
|
||||||
const pngIconBuffer: Buffer = await sharp(svgIconBuffer)
|
const pngIconBuffer: Buffer = await sharp(svgIconBuffer)
|
||||||
.resize({ width: pixelSize, height: pixelSize })
|
.resize({ width: pixelSize, height: pixelSize })
|
||||||
@ -45,6 +46,7 @@ const iconSizes: Record<string, number> = {
|
|||||||
"2x": 32,
|
"2x": 32,
|
||||||
"3x": 48,
|
"3x": 48,
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.entries(iconSizes).forEach(([dpiSuffix, pixelSize]) => {
|
Object.entries(iconSizes).forEach(([dpiSuffix, pixelSize]) => {
|
||||||
generateTrayIcon({ dpiSuffix, pixelSize, shouldUseDarkColors: false });
|
generateTrayIcon({ dpiSuffix, pixelSize, shouldUseDarkColors: false });
|
||||||
generateTrayIcon({ dpiSuffix, pixelSize, shouldUseDarkColors: true });
|
generateTrayIcon({ dpiSuffix, pixelSize, shouldUseDarkColors: true });
|
||||||
|
|||||||
@ -15,6 +15,7 @@ class KubectlDownloader {
|
|||||||
constructor(clusterVersion: string, platform: string, arch: string, target: string) {
|
constructor(clusterVersion: string, platform: string, arch: string, target: string) {
|
||||||
this.kubectlVersion = clusterVersion;
|
this.kubectlVersion = clusterVersion;
|
||||||
const binaryName = platform === "windows" ? "kubectl.exe" : "kubectl";
|
const binaryName = platform === "windows" ? "kubectl.exe" : "kubectl";
|
||||||
|
|
||||||
this.url = `https://storage.googleapis.com/kubernetes-release/release/v${this.kubectlVersion}/bin/${platform}/${arch}/${binaryName}`;
|
this.url = `https://storage.googleapis.com/kubernetes-release/release/v${this.kubectlVersion}/bin/${platform}/${arch}/${binaryName}`;
|
||||||
this.dirname = path.dirname(target);
|
this.dirname = path.dirname(target);
|
||||||
this.path = target;
|
this.path = target;
|
||||||
@ -30,16 +31,20 @@ class KubectlDownloader {
|
|||||||
if (response.headers["etag"]) {
|
if (response.headers["etag"]) {
|
||||||
return response.headers["etag"].replace(/"/g, "");
|
return response.headers["etag"].replace(/"/g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
public async checkBinary() {
|
public async checkBinary() {
|
||||||
const exists = await pathExists(this.path);
|
const exists = await pathExists(this.path);
|
||||||
|
|
||||||
if (exists) {
|
if (exists) {
|
||||||
const hash = md5File.sync(this.path);
|
const hash = md5File.sync(this.path);
|
||||||
const etag = await this.urlEtag();
|
const etag = await this.urlEtag();
|
||||||
|
|
||||||
if(hash == etag) {
|
if(hash == etag) {
|
||||||
console.log("Kubectl md5sum matches the remote etag");
|
console.log("Kubectl md5sum matches the remote etag");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,13 +57,16 @@ class KubectlDownloader {
|
|||||||
|
|
||||||
public async downloadKubectl() {
|
public async downloadKubectl() {
|
||||||
const exists = await this.checkBinary();
|
const exists = await this.checkBinary();
|
||||||
|
|
||||||
if(exists) {
|
if(exists) {
|
||||||
console.log("Already exists and is valid");
|
console.log("Already exists and is valid");
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await ensureDir(path.dirname(this.path), 0o755);
|
await ensureDir(path.dirname(this.path), 0o755);
|
||||||
|
|
||||||
const file = fs.createWriteStream(this.path);
|
const file = fs.createWriteStream(this.path);
|
||||||
|
|
||||||
console.log(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`);
|
console.log(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`);
|
||||||
const requestOpts: request.UriOptions & request.CoreOptions = {
|
const requestOpts: request.UriOptions & request.CoreOptions = {
|
||||||
uri: this.url,
|
uri: this.url,
|
||||||
@ -78,6 +86,7 @@ class KubectlDownloader {
|
|||||||
fs.unlink(this.path, () => {});
|
fs.unlink(this.path, () => {});
|
||||||
throw(error);
|
throw(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
file.on("close", () => {
|
file.on("close", () => {
|
||||||
console.log("kubectl binary download closed");
|
console.log("kubectl binary download closed");
|
||||||
@ -103,6 +112,7 @@ const downloads = [
|
|||||||
downloads.forEach((dlOpts) => {
|
downloads.forEach((dlOpts) => {
|
||||||
console.log(dlOpts);
|
console.log(dlOpts);
|
||||||
const downloader = new KubectlDownloader(downloadVersion, dlOpts.platform, dlOpts.arch, dlOpts.target);
|
const downloader = new KubectlDownloader(downloadVersion, dlOpts.platform, dlOpts.arch, dlOpts.target);
|
||||||
|
|
||||||
console.log(`Downloading: ${JSON.stringify(dlOpts)}`);
|
console.log(`Downloading: ${JSON.stringify(dlOpts)}`);
|
||||||
downloader.downloadKubectl().then(() => downloader.checkBinary().then(() => console.log("Download complete")));
|
downloader.downloadKubectl().then(() => downloader.checkBinary().then(() => console.log("Download complete")));
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,9 +2,11 @@ const { notarize } = require("electron-notarize");
|
|||||||
|
|
||||||
exports.default = async function notarizing(context) {
|
exports.default = async function notarizing(context) {
|
||||||
const { electronPlatformName, appOutDir } = context;
|
const { electronPlatformName, appOutDir } = context;
|
||||||
|
|
||||||
if (electronPlatformName !== "darwin") {
|
if (electronPlatformName !== "darwin") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!process.env.APPLEID || !process.env.APPLEIDPASS) {
|
if (!process.env.APPLEID || !process.env.APPLEIDPASS) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ export function ExampleIcon(props: Component.IconProps) {
|
|||||||
export class ExamplePage extends React.Component<{ extension: LensRendererExtension }> {
|
export class ExamplePage extends React.Component<{ extension: LensRendererExtension }> {
|
||||||
deactivate = () => {
|
deactivate = () => {
|
||||||
const { extension } = this.props;
|
const { extension } = this.props;
|
||||||
|
|
||||||
extension.disable();
|
extension.disable();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -17,6 +18,7 @@ export class ExamplePage extends React.Component<{ extension: LensRendererExtens
|
|||||||
const doodleStyle = {
|
const doodleStyle = {
|
||||||
width: "200px"
|
width: "200px"
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex column gaps align-flex-start">
|
<div className="flex column gaps align-flex-start">
|
||||||
<div style={doodleStyle}><CoffeeDoodle accent="#3d90ce" /></div>
|
<div style={doodleStyle}><CoffeeDoodle accent="#3d90ce" /></div>
|
||||||
|
|||||||
@ -4,10 +4,12 @@ export function resolveStatus(object: K8sApi.KubeObject): K8sApi.KubeObjectStatu
|
|||||||
const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi);
|
const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi);
|
||||||
const events = (eventStore as K8sApi.EventStore).getEventsByObject(object);
|
const events = (eventStore as K8sApi.EventStore).getEventsByObject(object);
|
||||||
const warnings = events.filter(evt => evt.isWarning());
|
const warnings = events.filter(evt => evt.isWarning());
|
||||||
|
|
||||||
if (!events.length || !warnings.length) {
|
if (!events.length || !warnings.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const event = [...warnings, ...events][0]; // get latest event
|
const event = [...warnings, ...events][0]; // get latest event
|
||||||
|
|
||||||
return {
|
return {
|
||||||
level: K8sApi.KubeObjectStatusLevel.WARNING,
|
level: K8sApi.KubeObjectStatusLevel.WARNING,
|
||||||
text: `${event.message}`,
|
text: `${event.message}`,
|
||||||
@ -22,10 +24,12 @@ export function resolveStatusForPods(pod: K8sApi.Pod): K8sApi.KubeObjectStatus {
|
|||||||
const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi);
|
const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi);
|
||||||
const events = (eventStore as K8sApi.EventStore).getEventsByObject(pod);
|
const events = (eventStore as K8sApi.EventStore).getEventsByObject(pod);
|
||||||
const warnings = events.filter(evt => evt.isWarning());
|
const warnings = events.filter(evt => evt.isWarning());
|
||||||
|
|
||||||
if (!events.length || !warnings.length) {
|
if (!events.length || !warnings.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const event = [...warnings, ...events][0]; // get latest event
|
const event = [...warnings, ...events][0]; // get latest event
|
||||||
|
|
||||||
return {
|
return {
|
||||||
level: K8sApi.KubeObjectStatusLevel.WARNING,
|
level: K8sApi.KubeObjectStatusLevel.WARNING,
|
||||||
text: `${event.message}`,
|
text: `${event.message}`,
|
||||||
@ -37,13 +41,16 @@ export function resolveStatusForCronJobs(cronJob: K8sApi.CronJob): K8sApi.KubeOb
|
|||||||
const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi);
|
const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi);
|
||||||
let events = (eventStore as K8sApi.EventStore).getEventsByObject(cronJob);
|
let events = (eventStore as K8sApi.EventStore).getEventsByObject(cronJob);
|
||||||
const warnings = events.filter(evt => evt.isWarning());
|
const warnings = events.filter(evt => evt.isWarning());
|
||||||
|
|
||||||
if (cronJob.isNeverRun()) {
|
if (cronJob.isNeverRun()) {
|
||||||
events = events.filter(event => event.reason != "FailedNeedsStart");
|
events = events.filter(event => event.reason != "FailedNeedsStart");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!events.length || !warnings.length) {
|
if (!events.length || !warnings.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const event = [...warnings, ...events][0]; // get latest event
|
const event = [...warnings, ...events][0]; // get latest event
|
||||||
|
|
||||||
return {
|
return {
|
||||||
level: K8sApi.KubeObjectStatusLevel.WARNING,
|
level: K8sApi.KubeObjectStatusLevel.WARNING,
|
||||||
text: `${event.message}`,
|
text: `${event.message}`,
|
||||||
|
|||||||
@ -53,6 +53,7 @@ export class MetricsFeature extends ClusterFeature.Feature {
|
|||||||
// Check if there are storageclasses
|
// Check if there are storageclasses
|
||||||
const storageClassApi = K8sApi.forCluster(cluster, K8sApi.StorageClass);
|
const storageClassApi = K8sApi.forCluster(cluster, K8sApi.StorageClass);
|
||||||
const scs = await storageClassApi.list();
|
const scs = await storageClassApi.list();
|
||||||
|
|
||||||
this.templateContext.persistence.enabled = scs.some(sc => (
|
this.templateContext.persistence.enabled = scs.some(sc => (
|
||||||
sc.metadata?.annotations?.["storageclass.kubernetes.io/is-default-class"] === "true" ||
|
sc.metadata?.annotations?.["storageclass.kubernetes.io/is-default-class"] === "true" ||
|
||||||
sc.metadata?.annotations?.["storageclass.beta.kubernetes.io/is-default-class"] === "true"
|
sc.metadata?.annotations?.["storageclass.beta.kubernetes.io/is-default-class"] === "true"
|
||||||
@ -69,6 +70,7 @@ export class MetricsFeature extends ClusterFeature.Feature {
|
|||||||
try {
|
try {
|
||||||
const statefulSet = K8sApi.forCluster(cluster, K8sApi.StatefulSet);
|
const statefulSet = K8sApi.forCluster(cluster, K8sApi.StatefulSet);
|
||||||
const prometheus = await statefulSet.get({name: "prometheus", namespace: "lens-metrics"});
|
const prometheus = await statefulSet.get({name: "prometheus", namespace: "lens-metrics"});
|
||||||
|
|
||||||
if (prometheus?.kind) {
|
if (prometheus?.kind) {
|
||||||
this.status.installed = true;
|
this.status.installed = true;
|
||||||
this.status.currentVersion = prometheus.spec.template.spec.containers[0].image.split(":")[1];
|
this.status.currentVersion = prometheus.spec.template.spec.containers[0].image.split(":")[1];
|
||||||
|
|||||||
@ -6,6 +6,7 @@ export interface NodeMenuProps extends Component.KubeObjectMenuProps<K8sApi.Node
|
|||||||
|
|
||||||
export function NodeMenu(props: NodeMenuProps) {
|
export function NodeMenu(props: NodeMenuProps) {
|
||||||
const { object: node, toolbar } = props;
|
const { object: node, toolbar } = props;
|
||||||
|
|
||||||
if (!node) return null;
|
if (!node) return null;
|
||||||
const nodeName = node.getName();
|
const nodeName = node.getName();
|
||||||
|
|
||||||
@ -35,6 +36,7 @@ export function NodeMenu(props: NodeMenuProps) {
|
|||||||
|
|
||||||
const drain = () => {
|
const drain = () => {
|
||||||
const command = `kubectl drain ${nodeName} --delete-local-data --ignore-daemonsets --force`;
|
const command = `kubectl drain ${nodeName} --delete-local-data --ignore-daemonsets --force`;
|
||||||
|
|
||||||
Component.ConfirmDialog.open({
|
Component.ConfirmDialog.open({
|
||||||
ok: () => sendToTerminal(command),
|
ok: () => sendToTerminal(command),
|
||||||
labelOk: `Drain Node`,
|
labelOk: `Drain Node`,
|
||||||
|
|||||||
@ -8,6 +8,7 @@ export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
|
|||||||
showLogs(container: K8sApi.IPodContainer) {
|
showLogs(container: K8sApi.IPodContainer) {
|
||||||
Navigation.hideDetails();
|
Navigation.hideDetails();
|
||||||
const pod = this.props.object;
|
const pod = this.props.object;
|
||||||
|
|
||||||
Component.createPodLogsTab({
|
Component.createPodLogsTab({
|
||||||
pod,
|
pod,
|
||||||
containers: pod.getContainers(),
|
containers: pod.getContainers(),
|
||||||
@ -22,7 +23,9 @@ export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
|
|||||||
const { object: pod, toolbar } = this.props;
|
const { object: pod, toolbar } = this.props;
|
||||||
const containers = pod.getAllContainers();
|
const containers = pod.getAllContainers();
|
||||||
const statuses = pod.getContainerStatuses();
|
const statuses = pod.getContainerStatuses();
|
||||||
|
|
||||||
if (!containers.length) return null;
|
if (!containers.length) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component.MenuItem onClick={Util.prevDefault(() => this.showLogs(containers[0]))}>
|
<Component.MenuItem onClick={Util.prevDefault(() => this.showLogs(containers[0]))}>
|
||||||
<Component.Icon material="subject" title="Logs" interactive={toolbar}/>
|
<Component.Icon material="subject" title="Logs" interactive={toolbar}/>
|
||||||
@ -40,6 +43,7 @@ export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
|
|||||||
className={Util.cssNames(Object.keys(status.state)[0], { ready: status.ready })}
|
className={Util.cssNames(Object.keys(status.state)[0], { ready: status.ready })}
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component.MenuItem key={name} onClick={Util.prevDefault(() => this.showLogs(container))} className="flex align-center">
|
<Component.MenuItem key={name} onClick={Util.prevDefault(() => this.showLogs(container))} className="flex align-center">
|
||||||
{brick}
|
{brick}
|
||||||
|
|||||||
@ -12,9 +12,11 @@ export class PodShellMenu extends React.Component<PodShellMenuProps> {
|
|||||||
const { object: pod } = this.props;
|
const { object: pod } = this.props;
|
||||||
const containerParam = container ? `-c ${container}` : "";
|
const containerParam = container ? `-c ${container}` : "";
|
||||||
let command = `kubectl exec -i -t -n ${pod.getNs()} ${pod.getName()} ${containerParam} "--"`;
|
let command = `kubectl exec -i -t -n ${pod.getNs()} ${pod.getName()} ${containerParam} "--"`;
|
||||||
|
|
||||||
if (window.navigator.platform !== "Win32") {
|
if (window.navigator.platform !== "Win32") {
|
||||||
command = `exec ${command}`;
|
command = `exec ${command}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pod.getSelectedNodeOs() === "windows") {
|
if (pod.getSelectedNodeOs() === "windows") {
|
||||||
command = `${command} powershell`;
|
command = `${command} powershell`;
|
||||||
} else {
|
} else {
|
||||||
@ -34,7 +36,9 @@ export class PodShellMenu extends React.Component<PodShellMenuProps> {
|
|||||||
render() {
|
render() {
|
||||||
const { object, toolbar } = this.props;
|
const { object, toolbar } = this.props;
|
||||||
const containers = object.getRunningContainers();
|
const containers = object.getRunningContainers();
|
||||||
|
|
||||||
if (!containers.length) return null;
|
if (!containers.length) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component.MenuItem onClick={Util.prevDefault(() => this.execShell(containers[0].name))}>
|
<Component.MenuItem onClick={Util.prevDefault(() => this.execShell(containers[0].name))}>
|
||||||
<Component.Icon svg="ssh" interactive={toolbar} title="Pod shell"/>
|
<Component.Icon svg="ssh" interactive={toolbar} title="Pod shell"/>
|
||||||
@ -46,6 +50,7 @@ export class PodShellMenu extends React.Component<PodShellMenuProps> {
|
|||||||
{
|
{
|
||||||
containers.map(container => {
|
containers.map(container => {
|
||||||
const { name } = container;
|
const { name } = container;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component.MenuItem key={name} onClick={Util.prevDefault(() => this.execShell(name))} className="flex align-center">
|
<Component.MenuItem key={name} onClick={Util.prevDefault(() => this.execShell(name))} className="flex align-center">
|
||||||
<Component.StatusBrick/>
|
<Component.StatusBrick/>
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { TelemetryPreferencesStore } from "./telemetry-preferences-store";
|
|||||||
export class TelemetryPreferenceInput extends React.Component<{telemetry: TelemetryPreferencesStore}, {}> {
|
export class TelemetryPreferenceInput extends React.Component<{telemetry: TelemetryPreferencesStore}, {}> {
|
||||||
render() {
|
render() {
|
||||||
const { telemetry } = this.props;
|
const { telemetry } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component.Checkbox
|
<Component.Checkbox
|
||||||
label="Allow telemetry & usage tracking"
|
label="Allow telemetry & usage tracking"
|
||||||
|
|||||||
@ -29,6 +29,7 @@ export class Tracker extends Util.Singleton {
|
|||||||
this.anonymousId = machineIdSync();
|
this.anonymousId = machineIdSync();
|
||||||
this.os = this.resolveOS();
|
this.os = this.resolveOS();
|
||||||
this.userAgent = `Lens ${App.version} (${this.os})`;
|
this.userAgent = `Lens ${App.version} (${this.os})`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.visitor = ua(Tracker.GA_ID, this.anonymousId, { strictCidFormat: false });
|
this.visitor = ua(Tracker.GA_ID, this.anonymousId, { strictCidFormat: false });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -49,18 +50,22 @@ export class Tracker extends Util.Singleton {
|
|||||||
const handler = (ev: EventBus.AppEvent) => {
|
const handler = (ev: EventBus.AppEvent) => {
|
||||||
this.event(ev.name, ev.action, ev.params);
|
this.event(ev.name, ev.action, ev.params);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.eventHandlers.push(handler);
|
this.eventHandlers.push(handler);
|
||||||
EventBus.appEventBus.addListener(handler);
|
EventBus.appEventBus.addListener(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
watchExtensions() {
|
watchExtensions() {
|
||||||
let previousExtensions = App.getEnabledExtensions();
|
let previousExtensions = App.getEnabledExtensions();
|
||||||
|
|
||||||
this.disposers.push(reaction(() => App.getEnabledExtensions(), (currentExtensions) => {
|
this.disposers.push(reaction(() => App.getEnabledExtensions(), (currentExtensions) => {
|
||||||
const removedExtensions = previousExtensions.filter(x => !currentExtensions.includes(x));
|
const removedExtensions = previousExtensions.filter(x => !currentExtensions.includes(x));
|
||||||
|
|
||||||
removedExtensions.forEach(ext => {
|
removedExtensions.forEach(ext => {
|
||||||
this.event("extension", "disable", { extension: ext });
|
this.event("extension", "disable", { extension: ext });
|
||||||
});
|
});
|
||||||
const newExtensions = currentExtensions.filter(x => !previousExtensions.includes(x));
|
const newExtensions = currentExtensions.filter(x => !previousExtensions.includes(x));
|
||||||
|
|
||||||
newExtensions.forEach(ext => {
|
newExtensions.forEach(ext => {
|
||||||
this.event("extension", "enable", { extension: ext });
|
this.event("extension", "enable", { extension: ext });
|
||||||
});
|
});
|
||||||
@ -82,6 +87,7 @@ export class Tracker extends Util.Singleton {
|
|||||||
for (const handler of this.eventHandlers) {
|
for (const handler of this.eventHandlers) {
|
||||||
EventBus.appEventBus.removeListener(handler);
|
EventBus.appEventBus.removeListener(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.reportInterval) {
|
if (this.reportInterval) {
|
||||||
clearInterval(this.reportInterval);
|
clearInterval(this.reportInterval);
|
||||||
}
|
}
|
||||||
@ -125,12 +131,14 @@ export class Tracker extends Util.Singleton {
|
|||||||
|
|
||||||
protected resolveOS() {
|
protected resolveOS() {
|
||||||
let os = "";
|
let os = "";
|
||||||
|
|
||||||
if (App.isMac) {
|
if (App.isMac) {
|
||||||
os = "MacOS";
|
os = "MacOS";
|
||||||
} else if(App.isWindows) {
|
} else if(App.isWindows) {
|
||||||
os = "Windows";
|
os = "Windows";
|
||||||
} else if (App.isLinux) {
|
} else if (App.isLinux) {
|
||||||
os = "Linux";
|
os = "Linux";
|
||||||
|
|
||||||
if (App.isSnap) {
|
if (App.isSnap) {
|
||||||
os += "; Snap";
|
os += "; Snap";
|
||||||
} else {
|
} else {
|
||||||
@ -139,12 +147,14 @@ export class Tracker extends Util.Singleton {
|
|||||||
} else {
|
} else {
|
||||||
os = "Unknown";
|
os = "Unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
return os;
|
return os;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async event(eventCategory: string, eventAction: string, otherParams = {}) {
|
protected async event(eventCategory: string, eventAction: string, otherParams = {}) {
|
||||||
try {
|
try {
|
||||||
const allowed = await this.isTelemetryAllowed();
|
const allowed = await this.isTelemetryAllowed();
|
||||||
|
|
||||||
if (!allowed) {
|
if (!allowed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,9 +14,7 @@ jest.setTimeout(60000);
|
|||||||
describe("Lens integration tests", () => {
|
describe("Lens integration tests", () => {
|
||||||
const TEST_NAMESPACE = "integration-tests";
|
const TEST_NAMESPACE = "integration-tests";
|
||||||
const BACKSPACE = "\uE003";
|
const BACKSPACE = "\uE003";
|
||||||
|
|
||||||
let app: Application;
|
let app: Application;
|
||||||
|
|
||||||
const appStart = async () => {
|
const appStart = async () => {
|
||||||
app = util.setup();
|
app = util.setup();
|
||||||
await app.start();
|
await app.start();
|
||||||
@ -25,19 +23,19 @@ describe("Lens integration tests", () => {
|
|||||||
await app.client.windowByIndex(0);
|
await app.client.windowByIndex(0);
|
||||||
await app.client.waitUntilWindowLoaded();
|
await app.client.waitUntilWindowLoaded();
|
||||||
};
|
};
|
||||||
|
|
||||||
const clickWhatsNew = async (app: Application) => {
|
const clickWhatsNew = async (app: Application) => {
|
||||||
await app.client.waitUntilTextExists("h1", "What's new?");
|
await app.client.waitUntilTextExists("h1", "What's new?");
|
||||||
await app.client.click("button.primary");
|
await app.client.click("button.primary");
|
||||||
await app.client.waitUntilTextExists("h1", "Welcome");
|
await app.client.waitUntilTextExists("h1", "Welcome");
|
||||||
};
|
};
|
||||||
|
|
||||||
const minikubeReady = (): boolean => {
|
const minikubeReady = (): boolean => {
|
||||||
// determine if minikube is running
|
// determine if minikube is running
|
||||||
{
|
{
|
||||||
const { status } = spawnSync("minikube status", { shell: true });
|
const { status } = spawnSync("minikube status", { shell: true });
|
||||||
|
|
||||||
if (status !== 0) {
|
if (status !== 0) {
|
||||||
console.warn("minikube not running");
|
console.warn("minikube not running");
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -45,6 +43,7 @@ describe("Lens integration tests", () => {
|
|||||||
// Remove TEST_NAMESPACE if it already exists
|
// Remove TEST_NAMESPACE if it already exists
|
||||||
{
|
{
|
||||||
const { status } = spawnSync(`minikube kubectl -- get namespace ${TEST_NAMESPACE}`, { shell: true });
|
const { status } = spawnSync(`minikube kubectl -- get namespace ${TEST_NAMESPACE}`, { shell: true });
|
||||||
|
|
||||||
if (status === 0) {
|
if (status === 0) {
|
||||||
console.warn(`Removing existing ${TEST_NAMESPACE} namespace`);
|
console.warn(`Removing existing ${TEST_NAMESPACE} namespace`);
|
||||||
|
|
||||||
@ -52,8 +51,10 @@ describe("Lens integration tests", () => {
|
|||||||
`minikube kubectl -- delete namespace ${TEST_NAMESPACE}`,
|
`minikube kubectl -- delete namespace ${TEST_NAMESPACE}`,
|
||||||
{ shell: true },
|
{ shell: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (status !== 0) {
|
if (status !== 0) {
|
||||||
console.warn(`Error removing ${TEST_NAMESPACE} namespace: ${stderr.toString()}`);
|
console.warn(`Error removing ${TEST_NAMESPACE} namespace: ${stderr.toString()}`);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,6 +87,7 @@ describe("Lens integration tests", () => {
|
|||||||
describe("preferences page", () => {
|
describe("preferences page", () => {
|
||||||
it('shows "preferences"', async () => {
|
it('shows "preferences"', async () => {
|
||||||
const appName: string = process.platform === "darwin" ? "Lens" : "File";
|
const appName: string = process.platform === "darwin" ? "Lens" : "File";
|
||||||
|
|
||||||
await app.electron.ipcRenderer.send("test-menu-item-click", appName, "Preferences");
|
await app.electron.ipcRenderer.send("test-menu-item-click", appName, "Preferences");
|
||||||
await app.client.waitUntilTextExists("h2", "Preferences");
|
await app.client.waitUntilTextExists("h2", "Preferences");
|
||||||
});
|
});
|
||||||
@ -153,13 +155,13 @@ describe("Lens integration tests", () => {
|
|||||||
await app.client.waitUntilTextExists("div", "Select kubeconfig file");
|
await app.client.waitUntilTextExists("div", "Select kubeconfig file");
|
||||||
await app.client.click("div.Select__control"); // show the context drop-down list
|
await app.client.click("div.Select__control"); // show the context drop-down list
|
||||||
await app.client.waitUntilTextExists("div", "minikube");
|
await app.client.waitUntilTextExists("div", "minikube");
|
||||||
|
|
||||||
if (!await app.client.$("button.primary").isEnabled()) {
|
if (!await app.client.$("button.primary").isEnabled()) {
|
||||||
await app.client.click("div.minikube"); // select minikube context
|
await app.client.click("div.minikube"); // select minikube context
|
||||||
} // else the only context, which must be 'minikube', is automatically selected
|
} // else the only context, which must be 'minikube', is automatically selected
|
||||||
await app.client.click("div.Select__control"); // hide the context drop-down list (it might be obscuring the Add cluster(s) button)
|
await app.client.click("div.Select__control"); // hide the context drop-down list (it might be obscuring the Add cluster(s) button)
|
||||||
await app.client.click("button.primary"); // add minikube cluster
|
await app.client.click("button.primary"); // add minikube cluster
|
||||||
};
|
};
|
||||||
|
|
||||||
const waitForMinikubeDashboard = async (app: Application) => {
|
const waitForMinikubeDashboard = async (app: Application) => {
|
||||||
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started");
|
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started");
|
||||||
await app.client.waitForExist(`iframe[name="minikube"]`);
|
await app.client.waitForExist(`iframe[name="minikube"]`);
|
||||||
@ -169,7 +171,6 @@ describe("Lens integration tests", () => {
|
|||||||
|
|
||||||
util.describeIf(ready)("cluster tests", () => {
|
util.describeIf(ready)("cluster tests", () => {
|
||||||
let clusterAdded = false;
|
let clusterAdded = false;
|
||||||
|
|
||||||
const addCluster = async () => {
|
const addCluster = async () => {
|
||||||
await clickWhatsNew(app);
|
await clickWhatsNew(app);
|
||||||
await addMinikubeCluster(app);
|
await addMinikubeCluster(app);
|
||||||
@ -443,6 +444,7 @@ describe("Lens integration tests", () => {
|
|||||||
expectedText: "Custom Resources"
|
expectedText: "Custom Resources"
|
||||||
}]
|
}]
|
||||||
}];
|
}];
|
||||||
|
|
||||||
tests.forEach(({ drawer = "", drawerId = "", pages }) => {
|
tests.forEach(({ drawer = "", drawerId = "", pages }) => {
|
||||||
if (drawer !== "") {
|
if (drawer !== "") {
|
||||||
it(`shows ${drawer} drawer`, async () => {
|
it(`shows ${drawer} drawer`, async () => {
|
||||||
@ -458,6 +460,7 @@ describe("Lens integration tests", () => {
|
|||||||
await app.client.waitUntilTextExists(expectedSelector, expectedText);
|
await app.client.waitUntilTextExists(expectedSelector, expectedText);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (drawer !== "") {
|
if (drawer !== "") {
|
||||||
// hide the drawer
|
// hide the drawer
|
||||||
it(`hides ${drawer} drawer`, async () => {
|
it(`hides ${drawer} drawer`, async () => {
|
||||||
|
|||||||
@ -30,7 +30,9 @@ type AsyncPidGetter = () => Promise<number>;
|
|||||||
|
|
||||||
export async function tearDown(app: Application) {
|
export async function tearDown(app: Application) {
|
||||||
const pid = await (app.mainProcess.pid as any as AsyncPidGetter)();
|
const pid = await (app.mainProcess.pid as any as AsyncPidGetter)();
|
||||||
|
|
||||||
await app.stop();
|
await app.stop();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
process.kill(pid, "SIGKILL");
|
process.kill(pid, "SIGKILL");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@ -31,8 +31,10 @@ describe("empty config", () => {
|
|||||||
"lens-cluster-store.json": JSON.stringify({})
|
"lens-cluster-store.json": JSON.stringify({})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||||
|
|
||||||
return clusterStore.load();
|
return clusterStore.load();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -59,6 +61,7 @@ describe("empty config", () => {
|
|||||||
|
|
||||||
it("adds new cluster to store", async () => {
|
it("adds new cluster to store", async () => {
|
||||||
const storedCluster = clusterStore.getById("foo");
|
const storedCluster = clusterStore.getById("foo");
|
||||||
|
|
||||||
expect(storedCluster.id).toBe("foo");
|
expect(storedCluster.id).toBe("foo");
|
||||||
expect(storedCluster.preferences.terminalCWD).toBe("/tmp");
|
expect(storedCluster.preferences.terminalCWD).toBe("/tmp");
|
||||||
expect(storedCluster.preferences.icon).toBe("data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5");
|
expect(storedCluster.preferences.icon).toBe("data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5");
|
||||||
@ -67,6 +70,7 @@ describe("empty config", () => {
|
|||||||
|
|
||||||
it("adds cluster to default workspace", () => {
|
it("adds cluster to default workspace", () => {
|
||||||
const storedCluster = clusterStore.getById("foo");
|
const storedCluster = clusterStore.getById("foo");
|
||||||
|
|
||||||
expect(storedCluster.workspace).toBe("default");
|
expect(storedCluster.workspace).toBe("default");
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -114,6 +118,7 @@ describe("empty config", () => {
|
|||||||
it("gets clusters by workspaces", () => {
|
it("gets clusters by workspaces", () => {
|
||||||
const wsClusters = clusterStore.getByWorkspaceId("workstation");
|
const wsClusters = clusterStore.getByWorkspaceId("workstation");
|
||||||
const defaultClusters = clusterStore.getByWorkspaceId("default");
|
const defaultClusters = clusterStore.getByWorkspaceId("default");
|
||||||
|
|
||||||
expect(defaultClusters.length).toBe(0);
|
expect(defaultClusters.length).toBe(0);
|
||||||
expect(wsClusters.length).toBe(2);
|
expect(wsClusters.length).toBe(2);
|
||||||
expect(wsClusters[0].id).toBe("prod");
|
expect(wsClusters[0].id).toBe("prod");
|
||||||
@ -122,6 +127,7 @@ describe("empty config", () => {
|
|||||||
|
|
||||||
it("check if cluster's kubeconfig file saved", () => {
|
it("check if cluster's kubeconfig file saved", () => {
|
||||||
const file = ClusterStore.embedCustomKubeConfig("boo", "kubeconfig");
|
const file = ClusterStore.embedCustomKubeConfig("boo", "kubeconfig");
|
||||||
|
|
||||||
expect(fs.readFileSync(file, "utf8")).toBe("kubeconfig");
|
expect(fs.readFileSync(file, "utf8")).toBe("kubeconfig");
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -129,6 +135,7 @@ describe("empty config", () => {
|
|||||||
clusterStore.swapIconOrders("workstation", 1, 1);
|
clusterStore.swapIconOrders("workstation", 1, 1);
|
||||||
|
|
||||||
const clusters = clusterStore.getByWorkspaceId("workstation");
|
const clusters = clusterStore.getByWorkspaceId("workstation");
|
||||||
|
|
||||||
expect(clusters[0].id).toBe("prod");
|
expect(clusters[0].id).toBe("prod");
|
||||||
expect(clusters[0].preferences.iconOrder).toBe(0);
|
expect(clusters[0].preferences.iconOrder).toBe(0);
|
||||||
expect(clusters[1].id).toBe("dev");
|
expect(clusters[1].id).toBe("dev");
|
||||||
@ -139,6 +146,7 @@ describe("empty config", () => {
|
|||||||
clusterStore.swapIconOrders("workstation", 0, 1);
|
clusterStore.swapIconOrders("workstation", 0, 1);
|
||||||
|
|
||||||
const clusters = clusterStore.getByWorkspaceId("workstation");
|
const clusters = clusterStore.getByWorkspaceId("workstation");
|
||||||
|
|
||||||
expect(clusters[0].id).toBe("dev");
|
expect(clusters[0].id).toBe("dev");
|
||||||
expect(clusters[0].preferences.iconOrder).toBe(0);
|
expect(clusters[0].preferences.iconOrder).toBe(0);
|
||||||
expect(clusters[1].id).toBe("prod");
|
expect(clusters[1].id).toBe("prod");
|
||||||
@ -192,8 +200,10 @@ describe("config with existing clusters", () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||||
|
|
||||||
return clusterStore.load();
|
return clusterStore.load();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -203,6 +213,7 @@ describe("config with existing clusters", () => {
|
|||||||
|
|
||||||
it("allows to retrieve a cluster", () => {
|
it("allows to retrieve a cluster", () => {
|
||||||
const storedCluster = clusterStore.getById("cluster1");
|
const storedCluster = clusterStore.getById("cluster1");
|
||||||
|
|
||||||
expect(storedCluster.id).toBe("cluster1");
|
expect(storedCluster.id).toBe("cluster1");
|
||||||
expect(storedCluster.preferences.terminalCWD).toBe("/foo");
|
expect(storedCluster.preferences.terminalCWD).toBe("/foo");
|
||||||
});
|
});
|
||||||
@ -210,13 +221,16 @@ describe("config with existing clusters", () => {
|
|||||||
it("allows to delete a cluster", () => {
|
it("allows to delete a cluster", () => {
|
||||||
clusterStore.removeById("cluster2");
|
clusterStore.removeById("cluster2");
|
||||||
const storedCluster = clusterStore.getById("cluster1");
|
const storedCluster = clusterStore.getById("cluster1");
|
||||||
|
|
||||||
expect(storedCluster).toBeTruthy();
|
expect(storedCluster).toBeTruthy();
|
||||||
const storedCluster2 = clusterStore.getById("cluster2");
|
const storedCluster2 = clusterStore.getById("cluster2");
|
||||||
|
|
||||||
expect(storedCluster2).toBeUndefined();
|
expect(storedCluster2).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows getting all of the clusters", async () => {
|
it("allows getting all of the clusters", async () => {
|
||||||
const storedClusters = clusterStore.clustersList;
|
const storedClusters = clusterStore.clustersList;
|
||||||
|
|
||||||
expect(storedClusters.length).toBe(3);
|
expect(storedClusters.length).toBe(3);
|
||||||
expect(storedClusters[0].id).toBe("cluster1");
|
expect(storedClusters[0].id).toBe("cluster1");
|
||||||
expect(storedClusters[0].preferences.terminalCWD).toBe("/foo");
|
expect(storedClusters[0].preferences.terminalCWD).toBe("/foo");
|
||||||
@ -227,6 +241,7 @@ describe("config with existing clusters", () => {
|
|||||||
|
|
||||||
it("marks owned cluster disabled by default", () => {
|
it("marks owned cluster disabled by default", () => {
|
||||||
const storedClusters = clusterStore.clustersList;
|
const storedClusters = clusterStore.clustersList;
|
||||||
|
|
||||||
expect(storedClusters[0].enabled).toBe(true);
|
expect(storedClusters[0].enabled).toBe(true);
|
||||||
expect(storedClusters[2].enabled).toBe(false);
|
expect(storedClusters[2].enabled).toBe(false);
|
||||||
});
|
});
|
||||||
@ -247,8 +262,10 @@ describe("pre 2.0 config with an existing cluster", () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||||
|
|
||||||
return clusterStore.load();
|
return clusterStore.load();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -258,6 +275,7 @@ describe("pre 2.0 config with an existing cluster", () => {
|
|||||||
|
|
||||||
it("migrates to modern format with kubeconfig in a file", async () => {
|
it("migrates to modern format with kubeconfig in a file", async () => {
|
||||||
const config = clusterStore.clustersList[0].kubeConfigPath;
|
const config = clusterStore.clustersList[0].kubeConfigPath;
|
||||||
|
|
||||||
expect(fs.readFileSync(config, "utf8")).toBe("kubeconfig content");
|
expect(fs.readFileSync(config, "utf8")).toBe("kubeconfig content");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -279,8 +297,10 @@ describe("pre 2.6.0 config with a cluster that has arrays in auth config", () =>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||||
|
|
||||||
return clusterStore.load();
|
return clusterStore.load();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -292,6 +312,7 @@ describe("pre 2.6.0 config with a cluster that has arrays in auth config", () =>
|
|||||||
const file = clusterStore.clustersList[0].kubeConfigPath;
|
const file = clusterStore.clustersList[0].kubeConfigPath;
|
||||||
const config = fs.readFileSync(file, "utf8");
|
const config = fs.readFileSync(file, "utf8");
|
||||||
const kc = yaml.safeLoad(config);
|
const kc = yaml.safeLoad(config);
|
||||||
|
|
||||||
expect(kc.users[0].user["auth-provider"].config["access-token"]).toBe("should be string");
|
expect(kc.users[0].user["auth-provider"].config["access-token"]).toBe("should be string");
|
||||||
expect(kc.users[0].user["auth-provider"].config["expiry"]).toBe("should be string");
|
expect(kc.users[0].user["auth-provider"].config["expiry"]).toBe("should be string");
|
||||||
});
|
});
|
||||||
@ -319,8 +340,10 @@ describe("pre 2.6.0 config with a cluster icon", () => {
|
|||||||
"icon_path": testDataIcon,
|
"icon_path": testDataIcon,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||||
|
|
||||||
return clusterStore.load();
|
return clusterStore.load();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -330,6 +353,7 @@ describe("pre 2.6.0 config with a cluster icon", () => {
|
|||||||
|
|
||||||
it("moves the icon into preferences", async () => {
|
it("moves the icon into preferences", async () => {
|
||||||
const storedClusterData = clusterStore.clustersList[0];
|
const storedClusterData = clusterStore.clustersList[0];
|
||||||
|
|
||||||
expect(storedClusterData.hasOwnProperty("icon")).toBe(false);
|
expect(storedClusterData.hasOwnProperty("icon")).toBe(false);
|
||||||
expect(storedClusterData.preferences.hasOwnProperty("icon")).toBe(true);
|
expect(storedClusterData.preferences.hasOwnProperty("icon")).toBe(true);
|
||||||
expect(storedClusterData.preferences.icon.startsWith("data:;base64,")).toBe(true);
|
expect(storedClusterData.preferences.icon.startsWith("data:;base64,")).toBe(true);
|
||||||
@ -356,8 +380,10 @@ describe("for a pre 2.7.0-beta.0 config without a workspace", () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||||
|
|
||||||
return clusterStore.load();
|
return clusterStore.load();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -367,6 +393,7 @@ describe("for a pre 2.7.0-beta.0 config without a workspace", () => {
|
|||||||
|
|
||||||
it("adds cluster to default workspace", async () => {
|
it("adds cluster to default workspace", async () => {
|
||||||
const storedClusterData = clusterStore.clustersList[0];
|
const storedClusterData = clusterStore.clustersList[0];
|
||||||
|
|
||||||
expect(storedClusterData.workspace).toBe("default");
|
expect(storedClusterData.workspace).toBe("default");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -396,8 +423,10 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
|
|||||||
"icon_path": testDataIcon,
|
"icon_path": testDataIcon,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||||
|
|
||||||
return clusterStore.load();
|
return clusterStore.load();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -407,11 +436,13 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
|
|||||||
|
|
||||||
it("migrates to modern format with kubeconfig in a file", async () => {
|
it("migrates to modern format with kubeconfig in a file", async () => {
|
||||||
const config = clusterStore.clustersList[0].kubeConfigPath;
|
const config = clusterStore.clustersList[0].kubeConfigPath;
|
||||||
|
|
||||||
expect(fs.readFileSync(config, "utf8")).toBe("kubeconfig content");
|
expect(fs.readFileSync(config, "utf8")).toBe("kubeconfig content");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("migrates to modern format with icon not in file", async () => {
|
it("migrates to modern format with icon not in file", async () => {
|
||||||
const { icon } = clusterStore.clustersList[0].preferences;
|
const { icon } = clusterStore.clustersList[0].preferences;
|
||||||
|
|
||||||
expect(icon.startsWith("data:;base64,")).toBe(true);
|
expect(icon.startsWith("data:;base64,")).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,6 +4,7 @@ describe("event bus tests", () => {
|
|||||||
describe("emit", () => {
|
describe("emit", () => {
|
||||||
it("emits an event", () => {
|
it("emits an event", () => {
|
||||||
let event: AppEvent = null;
|
let event: AppEvent = null;
|
||||||
|
|
||||||
appEventBus.addListener((data) => {
|
appEventBus.addListener((data) => {
|
||||||
event = data;
|
event = data;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -5,7 +5,6 @@
|
|||||||
import { SearchStore } from "../search-store";
|
import { SearchStore } from "../search-store";
|
||||||
|
|
||||||
let searchStore: SearchStore = null;
|
let searchStore: SearchStore = null;
|
||||||
|
|
||||||
const logs = [
|
const logs = [
|
||||||
"1:M 30 Oct 2020 16:17:41.553 # Connection with replica 172.17.0.12:6379 lost",
|
"1:M 30 Oct 2020 16:17:41.553 # Connection with replica 172.17.0.12:6379 lost",
|
||||||
"1:M 30 Oct 2020 16:17:41.623 * Replica 172.17.0.12:6379 asks for synchronization",
|
"1:M 30 Oct 2020 16:17:41.623 * Replica 172.17.0.12:6379 asks for synchronization",
|
||||||
@ -64,6 +63,7 @@ describe("search store tests", () => {
|
|||||||
|
|
||||||
it("escapes string for using in regex", () => {
|
it("escapes string for using in regex", () => {
|
||||||
const regex = searchStore.escapeRegex("some.interesting-query\\#?()[]");
|
const regex = searchStore.escapeRegex("some.interesting-query\\#?()[]");
|
||||||
|
|
||||||
expect(regex).toBe("some\\.interesting\\-query\\\\\\#\\?\\(\\)\\[\\]");
|
expect(regex).toBe("some\\.interesting\\-query\\\\\\#\\?\\(\\)\\[\\]");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -59,6 +59,7 @@ describe("user store tests", () => {
|
|||||||
|
|
||||||
it("correctly resets theme to default value", async () => {
|
it("correctly resets theme to default value", async () => {
|
||||||
const us = UserStore.getInstance<UserStore>();
|
const us = UserStore.getInstance<UserStore>();
|
||||||
|
|
||||||
us.isLoaded = true;
|
us.isLoaded = true;
|
||||||
|
|
||||||
us.preferences.colorTheme = "some other theme";
|
us.preferences.colorTheme = "some other theme";
|
||||||
|
|||||||
@ -44,7 +44,6 @@ describe("workspace store tests", () => {
|
|||||||
|
|
||||||
it("can update workspace description", () => {
|
it("can update workspace description", () => {
|
||||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
||||||
|
|
||||||
const workspace = ws.addWorkspace(new Workspace({
|
const workspace = ws.addWorkspace(new Workspace({
|
||||||
id: "foobar",
|
id: "foobar",
|
||||||
name: "foobar",
|
name: "foobar",
|
||||||
@ -65,6 +64,7 @@ describe("workspace store tests", () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const workspace = ws.getById("123");
|
const workspace = ws.getById("123");
|
||||||
|
|
||||||
expect(workspace.name).toBe("foobar");
|
expect(workspace.name).toBe("foobar");
|
||||||
expect(workspace.enabled).toBe(true);
|
expect(workspace.enabled).toBe(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -55,6 +55,7 @@ export abstract class BaseStore<T = any> extends Singleton {
|
|||||||
if (this.params.autoLoad) {
|
if (this.params.autoLoad) {
|
||||||
await this.load();
|
await this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.params.syncEnabled) {
|
if (this.params.syncEnabled) {
|
||||||
await this.whenLoaded;
|
await this.whenLoaded;
|
||||||
this.enableSync();
|
this.enableSync();
|
||||||
@ -63,6 +64,7 @@ export abstract class BaseStore<T = any> extends Singleton {
|
|||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
const { autoLoad, syncEnabled, ...confOptions } = this.params;
|
const { autoLoad, syncEnabled, ...confOptions } = this.params;
|
||||||
|
|
||||||
this.storeConfig = new Config({
|
this.storeConfig = new Config({
|
||||||
...confOptions,
|
...confOptions,
|
||||||
projectName: "lens",
|
projectName: "lens",
|
||||||
@ -90,19 +92,23 @@ export abstract class BaseStore<T = any> extends Singleton {
|
|||||||
this.syncDisposers.push(
|
this.syncDisposers.push(
|
||||||
reaction(() => this.toJSON(), model => this.onModelChange(model), this.params.syncOptions),
|
reaction(() => this.toJSON(), model => this.onModelChange(model), this.params.syncOptions),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (ipcMain) {
|
if (ipcMain) {
|
||||||
const callback = (event: IpcMainEvent, model: T) => {
|
const callback = (event: IpcMainEvent, model: T) => {
|
||||||
logger.silly(`[STORE]: SYNC ${this.name} from renderer`, { model });
|
logger.silly(`[STORE]: SYNC ${this.name} from renderer`, { model });
|
||||||
this.onSync(model);
|
this.onSync(model);
|
||||||
};
|
};
|
||||||
|
|
||||||
subscribeToBroadcast(this.syncMainChannel, callback);
|
subscribeToBroadcast(this.syncMainChannel, callback);
|
||||||
this.syncDisposers.push(() => unsubscribeFromBroadcast(this.syncMainChannel, callback));
|
this.syncDisposers.push(() => unsubscribeFromBroadcast(this.syncMainChannel, callback));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ipcRenderer) {
|
if (ipcRenderer) {
|
||||||
const callback = (event: IpcRendererEvent, model: T) => {
|
const callback = (event: IpcRendererEvent, model: T) => {
|
||||||
logger.silly(`[STORE]: SYNC ${this.name} from main`, { model });
|
logger.silly(`[STORE]: SYNC ${this.name} from main`, { model });
|
||||||
this.onSyncFromMain(model);
|
this.onSyncFromMain(model);
|
||||||
};
|
};
|
||||||
|
|
||||||
subscribeToBroadcast(this.syncRendererChannel, callback);
|
subscribeToBroadcast(this.syncRendererChannel, callback);
|
||||||
this.syncDisposers.push(() => unsubscribeFromBroadcast(this.syncRendererChannel, callback));
|
this.syncDisposers.push(() => unsubscribeFromBroadcast(this.syncRendererChannel, callback));
|
||||||
}
|
}
|
||||||
@ -127,6 +133,7 @@ export abstract class BaseStore<T = any> extends Singleton {
|
|||||||
protected applyWithoutSync(callback: () => void) {
|
protected applyWithoutSync(callback: () => void) {
|
||||||
this.disableSync();
|
this.disableSync();
|
||||||
runInAction(callback);
|
runInAction(callback);
|
||||||
|
|
||||||
if (this.params.syncEnabled) {
|
if (this.params.syncEnabled) {
|
||||||
this.enableSync();
|
this.enableSync();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all";
|
|||||||
if (ipcMain) {
|
if (ipcMain) {
|
||||||
handleRequest(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
|
handleRequest(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
|
||||||
const cluster = clusterStore.getById(clusterId);
|
const cluster = clusterStore.getById(clusterId);
|
||||||
|
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
return cluster.activate(force);
|
return cluster.activate(force);
|
||||||
}
|
}
|
||||||
@ -22,20 +23,24 @@ if (ipcMain) {
|
|||||||
|
|
||||||
handleRequest(clusterSetFrameIdHandler, (event, clusterId: ClusterId, frameId: number) => {
|
handleRequest(clusterSetFrameIdHandler, (event, clusterId: ClusterId, frameId: number) => {
|
||||||
const cluster = clusterStore.getById(clusterId);
|
const cluster = clusterStore.getById(clusterId);
|
||||||
|
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
clusterFrameMap.set(cluster.id, frameId);
|
clusterFrameMap.set(cluster.id, frameId);
|
||||||
|
|
||||||
return cluster.pushState();
|
return cluster.pushState();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
handleRequest(clusterRefreshHandler, (event, clusterId: ClusterId) => {
|
handleRequest(clusterRefreshHandler, (event, clusterId: ClusterId) => {
|
||||||
const cluster = clusterStore.getById(clusterId);
|
const cluster = clusterStore.getById(clusterId);
|
||||||
|
|
||||||
if (cluster) return cluster.refresh({ refreshMetadata: true });
|
if (cluster) return cluster.refresh({ refreshMetadata: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
handleRequest(clusterDisconnectHandler, (event, clusterId: ClusterId) => {
|
handleRequest(clusterDisconnectHandler, (event, clusterId: ClusterId) => {
|
||||||
appEventBus.emit({name: "cluster", action: "stop"});
|
appEventBus.emit({name: "cluster", action: "stop"});
|
||||||
const cluster = clusterStore.getById(clusterId);
|
const cluster = clusterStore.getById(clusterId);
|
||||||
|
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
cluster.disconnect();
|
cluster.disconnect();
|
||||||
clusterFrameMap.delete(cluster.id);
|
clusterFrameMap.delete(cluster.id);
|
||||||
@ -45,8 +50,10 @@ if (ipcMain) {
|
|||||||
handleRequest(clusterKubectlApplyAllHandler, (event, clusterId: ClusterId, resources: string[]) => {
|
handleRequest(clusterKubectlApplyAllHandler, (event, clusterId: ClusterId, resources: string[]) => {
|
||||||
appEventBus.emit({name: "cluster", action: "kubectl-apply-all"});
|
appEventBus.emit({name: "cluster", action: "kubectl-apply-all"});
|
||||||
const cluster = clusterStore.getById(clusterId);
|
const cluster = clusterStore.getById(clusterId);
|
||||||
|
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
const applier = new ResourceApplier(cluster);
|
const applier = new ResourceApplier(cluster);
|
||||||
|
|
||||||
applier.kubectlApplyAll(resources);
|
applier.kubectlApplyAll(resources);
|
||||||
} else {
|
} else {
|
||||||
throw `${clusterId} is not a valid cluster id`;
|
throw `${clusterId} is not a valid cluster id`;
|
||||||
|
|||||||
@ -98,7 +98,9 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
static embedCustomKubeConfig(clusterId: ClusterId, kubeConfig: KubeConfig | string): string {
|
static embedCustomKubeConfig(clusterId: ClusterId, kubeConfig: KubeConfig | string): string {
|
||||||
const filePath = ClusterStore.getCustomKubeConfigPath(clusterId);
|
const filePath = ClusterStore.getCustomKubeConfigPath(clusterId);
|
||||||
const fileContents = typeof kubeConfig == "string" ? kubeConfig : dumpConfigYaml(kubeConfig);
|
const fileContents = typeof kubeConfig == "string" ? kubeConfig : dumpConfigYaml(kubeConfig);
|
||||||
|
|
||||||
saveToAppFiles(filePath, fileContents, { mode: 0o600 });
|
saveToAppFiles(filePath, fileContents, { mode: 0o600 });
|
||||||
|
|
||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,11 +129,14 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
id: string;
|
id: string;
|
||||||
state: ClusterState;
|
state: ClusterState;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (ipcRenderer) {
|
if (ipcRenderer) {
|
||||||
logger.info("[CLUSTER-STORE] requesting initial state sync");
|
logger.info("[CLUSTER-STORE] requesting initial state sync");
|
||||||
const clusterStates: clusterStateSync[] = await requestMain(ClusterStore.stateRequestChannel);
|
const clusterStates: clusterStateSync[] = await requestMain(ClusterStore.stateRequestChannel);
|
||||||
|
|
||||||
clusterStates.forEach((clusterState) => {
|
clusterStates.forEach((clusterState) => {
|
||||||
const cluster = this.getById(clusterState.id);
|
const cluster = this.getById(clusterState.id);
|
||||||
|
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
cluster.setState(clusterState.state);
|
cluster.setState(clusterState.state);
|
||||||
}
|
}
|
||||||
@ -139,12 +144,14 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
} else {
|
} else {
|
||||||
handleRequest(ClusterStore.stateRequestChannel, (): clusterStateSync[] => {
|
handleRequest(ClusterStore.stateRequestChannel, (): clusterStateSync[] => {
|
||||||
const states: clusterStateSync[] = [];
|
const states: clusterStateSync[] = [];
|
||||||
|
|
||||||
this.clustersList.forEach((cluster) => {
|
this.clustersList.forEach((cluster) => {
|
||||||
states.push({
|
states.push({
|
||||||
state: cluster.getState(),
|
state: cluster.getState(),
|
||||||
id: cluster.id
|
id: cluster.id
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return states;
|
return states;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -207,6 +214,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
@action
|
@action
|
||||||
setActive(id: ClusterId) {
|
setActive(id: ClusterId) {
|
||||||
const clusterId = this.clusters.has(id) ? id : null;
|
const clusterId = this.clusters.has(id) ? id : null;
|
||||||
|
|
||||||
this.activeCluster = clusterId;
|
this.activeCluster = clusterId;
|
||||||
workspaceStore.setLastActiveClusterId(clusterId);
|
workspaceStore.setLastActiveClusterId(clusterId);
|
||||||
}
|
}
|
||||||
@ -214,11 +222,13 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
@action
|
@action
|
||||||
swapIconOrders(workspace: WorkspaceId, from: number, to: number) {
|
swapIconOrders(workspace: WorkspaceId, from: number, to: number) {
|
||||||
const clusters = this.getByWorkspaceId(workspace);
|
const clusters = this.getByWorkspaceId(workspace);
|
||||||
|
|
||||||
if (from < 0 || to < 0 || from >= clusters.length || to >= clusters.length || isNaN(from) || isNaN(to)) {
|
if (from < 0 || to < 0 || from >= clusters.length || to >= clusters.length || isNaN(from) || isNaN(to)) {
|
||||||
throw new Error(`invalid from<->to arguments`);
|
throw new Error(`invalid from<->to arguments`);
|
||||||
}
|
}
|
||||||
|
|
||||||
move.mutate(clusters, from, to);
|
move.mutate(clusters, from, to);
|
||||||
|
|
||||||
for (const i in clusters) {
|
for (const i in clusters) {
|
||||||
// This resets the iconOrder to the current display order
|
// This resets the iconOrder to the current display order
|
||||||
clusters[i].preferences.iconOrder = +i;
|
clusters[i].preferences.iconOrder = +i;
|
||||||
@ -236,12 +246,14 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
getByWorkspaceId(workspaceId: string): Cluster[] {
|
getByWorkspaceId(workspaceId: string): Cluster[] {
|
||||||
const clusters = Array.from(this.clusters.values())
|
const clusters = Array.from(this.clusters.values())
|
||||||
.filter(cluster => cluster.workspace === workspaceId);
|
.filter(cluster => cluster.workspace === workspaceId);
|
||||||
|
|
||||||
return _.sortBy(clusters, cluster => cluster.preferences.iconOrder);
|
return _.sortBy(clusters, cluster => cluster.preferences.iconOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
addClusters(...models: ClusterModel[]): Cluster[] {
|
addClusters(...models: ClusterModel[]): Cluster[] {
|
||||||
const clusters: Cluster[] = [];
|
const clusters: Cluster[] = [];
|
||||||
|
|
||||||
models.forEach(model => {
|
models.forEach(model => {
|
||||||
clusters.push(this.addCluster(model));
|
clusters.push(this.addCluster(model));
|
||||||
});
|
});
|
||||||
@ -253,13 +265,16 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
addCluster(model: ClusterModel | Cluster): Cluster {
|
addCluster(model: ClusterModel | Cluster): Cluster {
|
||||||
appEventBus.emit({ name: "cluster", action: "add" });
|
appEventBus.emit({ name: "cluster", action: "add" });
|
||||||
let cluster = model as Cluster;
|
let cluster = model as Cluster;
|
||||||
|
|
||||||
if (!(model instanceof Cluster)) {
|
if (!(model instanceof Cluster)) {
|
||||||
cluster = new Cluster(model);
|
cluster = new Cluster(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cluster.isManaged) {
|
if (!cluster.isManaged) {
|
||||||
cluster.enabled = true;
|
cluster.enabled = true;
|
||||||
}
|
}
|
||||||
this.clusters.set(model.id, cluster);
|
this.clusters.set(model.id, cluster);
|
||||||
|
|
||||||
return cluster;
|
return cluster;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,11 +286,14 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
async removeById(clusterId: ClusterId) {
|
async removeById(clusterId: ClusterId) {
|
||||||
appEventBus.emit({ name: "cluster", action: "remove" });
|
appEventBus.emit({ name: "cluster", action: "remove" });
|
||||||
const cluster = this.getById(clusterId);
|
const cluster = this.getById(clusterId);
|
||||||
|
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
this.clusters.delete(clusterId);
|
this.clusters.delete(clusterId);
|
||||||
|
|
||||||
if (this.activeCluster === clusterId) {
|
if (this.activeCluster === clusterId) {
|
||||||
this.setActive(null);
|
this.setActive(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove only custom kubeconfigs (pasted as text)
|
// remove only custom kubeconfigs (pasted as text)
|
||||||
if (cluster.kubeConfigPath == ClusterStore.getCustomKubeConfigPath(clusterId)) {
|
if (cluster.kubeConfigPath == ClusterStore.getCustomKubeConfigPath(clusterId)) {
|
||||||
unlink(cluster.kubeConfigPath).catch(() => null);
|
unlink(cluster.kubeConfigPath).catch(() => null);
|
||||||
@ -299,10 +317,12 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
// update new clusters
|
// update new clusters
|
||||||
for (const clusterModel of clusters) {
|
for (const clusterModel of clusters) {
|
||||||
let cluster = currentClusters.get(clusterModel.id);
|
let cluster = currentClusters.get(clusterModel.id);
|
||||||
|
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
cluster.updateModel(clusterModel);
|
cluster.updateModel(clusterModel);
|
||||||
} else {
|
} else {
|
||||||
cluster = new Cluster(clusterModel);
|
cluster = new Cluster(clusterModel);
|
||||||
|
|
||||||
if (!cluster.isManaged) {
|
if (!cluster.isManaged) {
|
||||||
cluster.enabled = true;
|
cluster.enabled = true;
|
||||||
}
|
}
|
||||||
@ -336,6 +356,7 @@ export const clusterStore = ClusterStore.getInstance<ClusterStore>();
|
|||||||
|
|
||||||
export function getClusterIdFromHost(hostname: string): ClusterId {
|
export function getClusterIdFromHost(hostname: string): ClusterId {
|
||||||
const subDomains = hostname.split(":")[0].split(".");
|
const subDomains = hostname.split(":")[0].split(".");
|
||||||
|
|
||||||
return subDomains.slice(-2)[0]; // e.g host == "%clusterId.localhost:45345"
|
return subDomains.slice(-2)[0]; // e.g host == "%clusterId.localhost:45345"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ export class ExecValidationNotFoundError extends Error {
|
|||||||
constructor(execPath: string, isAbsolute: boolean) {
|
constructor(execPath: string, isAbsolute: boolean) {
|
||||||
super(`User Exec command "${execPath}" not found on host.`);
|
super(`User Exec command "${execPath}" not found on host.`);
|
||||||
let message = `User Exec command "${execPath}" not found on host.`;
|
let message = `User Exec command "${execPath}" not found on host.`;
|
||||||
|
|
||||||
if (!isAbsolute) {
|
if (!isAbsolute) {
|
||||||
message += ` Please ensure binary is found in PATH or use absolute path to binary in Kubeconfig`;
|
message += ` Please ensure binary is found in PATH or use absolute path to binary in Kubeconfig`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ export class EventEmitter<D extends [...any[]]> {
|
|||||||
addListener(callback: Callback<D>, options: Options = {}) {
|
addListener(callback: Callback<D>, options: Options = {}) {
|
||||||
if (options.prepend) {
|
if (options.prepend) {
|
||||||
const listeners = [...this.listeners];
|
const listeners = [...this.listeners];
|
||||||
|
|
||||||
listeners.unshift([callback, options]);
|
listeners.unshift([callback, options]);
|
||||||
this.listeners = new Map(listeners);
|
this.listeners = new Map(listeners);
|
||||||
}
|
}
|
||||||
@ -33,7 +34,9 @@ export class EventEmitter<D extends [...any[]]> {
|
|||||||
[...this.listeners].every(([callback, options]) => {
|
[...this.listeners].every(([callback, options]) => {
|
||||||
if (options.once) this.removeListener(callback);
|
if (options.once) this.removeListener(callback);
|
||||||
const result = callback(...data);
|
const result = callback(...data);
|
||||||
|
|
||||||
if (result === false) return; // break cycle
|
if (result === false) return; // break cycle
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,18 +16,22 @@ export async function requestMain(channel: string, ...args: any[]) {
|
|||||||
|
|
||||||
async function getSubFrames(): Promise<number[]> {
|
async function getSubFrames(): Promise<number[]> {
|
||||||
const subFrames: number[] = [];
|
const subFrames: number[] = [];
|
||||||
|
|
||||||
clusterFrameMap.forEach(frameId => {
|
clusterFrameMap.forEach(frameId => {
|
||||||
subFrames.push(frameId);
|
subFrames.push(frameId);
|
||||||
});
|
});
|
||||||
|
|
||||||
return subFrames;
|
return subFrames;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function broadcastMessage(channel: string, ...args: any[]) {
|
export function broadcastMessage(channel: string, ...args: any[]) {
|
||||||
const views = (webContents || remote?.webContents)?.getAllWebContents();
|
const views = (webContents || remote?.webContents)?.getAllWebContents();
|
||||||
|
|
||||||
if (!views) return;
|
if (!views) return;
|
||||||
|
|
||||||
views.forEach(webContent => {
|
views.forEach(webContent => {
|
||||||
const type = webContent.getType();
|
const type = webContent.getType();
|
||||||
|
|
||||||
logger.silly(`[IPC]: broadcasting "${channel}" to ${type}=${webContent.id}`, { args });
|
logger.silly(`[IPC]: broadcasting "${channel}" to ${type}=${webContent.id}`, { args });
|
||||||
webContent.send(channel, ...args);
|
webContent.send(channel, ...args);
|
||||||
getSubFrames().then((frames) => {
|
getSubFrames().then((frames) => {
|
||||||
@ -36,6 +40,7 @@ export function broadcastMessage(channel: string, ...args: any[]) {
|
|||||||
});
|
});
|
||||||
}).catch((e) => e);
|
}).catch((e) => e);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (ipcRenderer) {
|
if (ipcRenderer) {
|
||||||
ipcRenderer.send(channel, ...args);
|
ipcRenderer.send(channel, ...args);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -13,6 +13,7 @@ function resolveTilde(filePath: string) {
|
|||||||
if (filePath[0] === "~" && (filePath[1] === "/" || filePath.length === 1)) {
|
if (filePath[0] === "~" && (filePath[1] === "/" || filePath.length === 1)) {
|
||||||
return filePath.replace("~", os.homedir());
|
return filePath.replace("~", os.homedir());
|
||||||
}
|
}
|
||||||
|
|
||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,12 +41,15 @@ export function validateConfig(config: KubeConfig | string): KubeConfig {
|
|||||||
config = loadConfig(config);
|
config = loadConfig(config);
|
||||||
}
|
}
|
||||||
logger.debug(`validating kube config: ${JSON.stringify(config)}`);
|
logger.debug(`validating kube config: ${JSON.stringify(config)}`);
|
||||||
|
|
||||||
if (!config.users || config.users.length == 0) {
|
if (!config.users || config.users.length == 0) {
|
||||||
throw new Error("No users provided in config");
|
throw new Error("No users provided in config");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.clusters || config.clusters.length == 0) {
|
if (!config.clusters || config.clusters.length == 0) {
|
||||||
throw new Error("No clusters provided in config");
|
throw new Error("No clusters provided in config");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.contexts || config.contexts.length == 0) {
|
if (!config.contexts || config.contexts.length == 0) {
|
||||||
throw new Error("No contexts provided in config");
|
throw new Error("No contexts provided in config");
|
||||||
}
|
}
|
||||||
@ -58,11 +62,13 @@ export function validateConfig(config: KubeConfig | string): KubeConfig {
|
|||||||
*/
|
*/
|
||||||
export function splitConfig(kubeConfig: KubeConfig): KubeConfig[] {
|
export function splitConfig(kubeConfig: KubeConfig): KubeConfig[] {
|
||||||
const configs: KubeConfig[] = [];
|
const configs: KubeConfig[] = [];
|
||||||
|
|
||||||
if (!kubeConfig.contexts) {
|
if (!kubeConfig.contexts) {
|
||||||
return configs;
|
return configs;
|
||||||
}
|
}
|
||||||
kubeConfig.contexts.forEach(ctx => {
|
kubeConfig.contexts.forEach(ctx => {
|
||||||
const kc = new KubeConfig();
|
const kc = new KubeConfig();
|
||||||
|
|
||||||
kc.clusters = [kubeConfig.getCluster(ctx.cluster)].filter(n => n);
|
kc.clusters = [kubeConfig.getCluster(ctx.cluster)].filter(n => n);
|
||||||
kc.users = [kubeConfig.getUser(ctx.user)].filter(n => n);
|
kc.users = [kubeConfig.getUser(ctx.user)].filter(n => n);
|
||||||
kc.contexts = [kubeConfig.getContextObject(ctx.name)].filter(n => n);
|
kc.contexts = [kubeConfig.getContextObject(ctx.name)].filter(n => n);
|
||||||
@ -70,6 +76,7 @@ export function splitConfig(kubeConfig: KubeConfig): KubeConfig[] {
|
|||||||
|
|
||||||
configs.push(kc);
|
configs.push(kc);
|
||||||
});
|
});
|
||||||
|
|
||||||
return configs;
|
return configs;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,11 +160,13 @@ export function validateKubeConfig (config: KubeConfig) {
|
|||||||
logger.debug(`validateKubeConfig: validating kubeconfig - ${JSON.stringify(config)}`);
|
logger.debug(`validateKubeConfig: validating kubeconfig - ${JSON.stringify(config)}`);
|
||||||
|
|
||||||
// Validate the User Object
|
// Validate the User Object
|
||||||
const user = config.getCurrentUser();
|
const user = config.getCurrentUser();
|
||||||
|
|
||||||
if (user.exec) {
|
if (user.exec) {
|
||||||
const execCommand = user.exec["command"];
|
const execCommand = user.exec["command"];
|
||||||
// check if the command is absolute or not
|
// check if the command is absolute or not
|
||||||
const isAbsolute = path.isAbsolute(execCommand);
|
const isAbsolute = path.isAbsolute(execCommand);
|
||||||
|
|
||||||
// validate the exec struct in the user object, start with the command field
|
// validate the exec struct in the user object, start with the command field
|
||||||
logger.debug(`validateKubeConfig: validating user exec command - ${JSON.stringify(execCommand)}`);
|
logger.debug(`validateKubeConfig: validating user exec command - ${JSON.stringify(execCommand)}`);
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { PrometheusProviderRegistry } from "../main/prometheus/provider-registry
|
|||||||
|
|
||||||
[PrometheusLens, PrometheusHelm, PrometheusOperator, PrometheusStacklight].forEach(providerClass => {
|
[PrometheusLens, PrometheusHelm, PrometheusOperator, PrometheusStacklight].forEach(providerClass => {
|
||||||
const provider = new providerClass();
|
const provider = new providerClass();
|
||||||
|
|
||||||
PrometheusProviderRegistry.registerProvider(provider.id, provider);
|
PrometheusProviderRegistry.registerProvider(provider.id, provider);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -42,10 +42,12 @@ export function isAllowedResource(resources: KubeResource | KubeResource[]) {
|
|||||||
resources = [resources];
|
resources = [resources];
|
||||||
}
|
}
|
||||||
const { allowedResources = [] } = getHostedCluster() || {};
|
const { allowedResources = [] } = getHostedCluster() || {};
|
||||||
|
|
||||||
for (const resource of resources) {
|
for (const resource of resources) {
|
||||||
if (!allowedResources.includes(resource)) {
|
if (!allowedResources.includes(resource)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ export function registerFileProtocol(name: string, basePath: string) {
|
|||||||
protocol.registerFileProtocol(name, (request, callback) => {
|
protocol.registerFileProtocol(name, (request, callback) => {
|
||||||
const filePath = request.url.replace(`${name}://`, "");
|
const filePath = request.url.replace(`${name}://`, "");
|
||||||
const absPath = path.resolve(basePath, filePath);
|
const absPath = path.resolve(basePath, filePath);
|
||||||
|
|
||||||
callback({ path: absPath });
|
callback({ path: absPath });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { userStore } from "./user-store";
|
|||||||
|
|
||||||
function getDefaultRequestOpts(): Partial<request.Options> {
|
function getDefaultRequestOpts(): Partial<request.Options> {
|
||||||
const { httpsProxy, allowUntrustedCAs } = userStore.preferences;
|
const { httpsProxy, allowUntrustedCAs } = userStore.preferences;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
proxy: httpsProxy || undefined,
|
proxy: httpsProxy || undefined,
|
||||||
rejectUnauthorized: !allowUntrustedCAs,
|
rejectUnauthorized: !allowUntrustedCAs,
|
||||||
|
|||||||
@ -14,8 +14,10 @@ export class SearchStore {
|
|||||||
@action
|
@action
|
||||||
onSearch(text: string[], query = this.searchQuery) {
|
onSearch(text: string[], query = this.searchQuery) {
|
||||||
this.searchQuery = query;
|
this.searchQuery = query;
|
||||||
|
|
||||||
if (!query) {
|
if (!query) {
|
||||||
this.reset();
|
this.reset();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.occurrences = this.findOccurences(text, query);
|
this.occurrences = this.findOccurences(text, query);
|
||||||
@ -36,11 +38,14 @@ export class SearchStore {
|
|||||||
findOccurences(text: string[], query: string) {
|
findOccurences(text: string[], query: string) {
|
||||||
if (!text) return [];
|
if (!text) return [];
|
||||||
const occurences: number[] = [];
|
const occurences: number[] = [];
|
||||||
|
|
||||||
text.forEach((line, index) => {
|
text.forEach((line, index) => {
|
||||||
const regex = new RegExp(this.escapeRegex(query), "gi");
|
const regex = new RegExp(this.escapeRegex(query), "gi");
|
||||||
const matches = [...line.matchAll(regex)];
|
const matches = [...line.matchAll(regex)];
|
||||||
|
|
||||||
matches.forEach(() => occurences.push(index));
|
matches.forEach(() => occurences.push(index));
|
||||||
});
|
});
|
||||||
|
|
||||||
return occurences;
|
return occurences;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,9 +56,11 @@ export class SearchStore {
|
|||||||
*/
|
*/
|
||||||
getNextOverlay(loopOver = false) {
|
getNextOverlay(loopOver = false) {
|
||||||
const next = this.activeOverlayIndex + 1;
|
const next = this.activeOverlayIndex + 1;
|
||||||
|
|
||||||
if (next > this.occurrences.length - 1) {
|
if (next > this.occurrences.length - 1) {
|
||||||
return loopOver ? 0 : this.activeOverlayIndex;
|
return loopOver ? 0 : this.activeOverlayIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,9 +71,11 @@ export class SearchStore {
|
|||||||
*/
|
*/
|
||||||
getPrevOverlay(loopOver = false) {
|
getPrevOverlay(loopOver = false) {
|
||||||
const prev = this.activeOverlayIndex - 1;
|
const prev = this.activeOverlayIndex - 1;
|
||||||
|
|
||||||
if (prev < 0) {
|
if (prev < 0) {
|
||||||
return loopOver ? this.occurrences.length - 1 : this.activeOverlayIndex;
|
return loopOver ? this.occurrences.length - 1 : this.activeOverlayIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,6 +113,7 @@ export class SearchStore {
|
|||||||
@autobind()
|
@autobind()
|
||||||
isActiveOverlay(line: number, occurence: number) {
|
isActiveOverlay(line: number, occurence: number) {
|
||||||
const firstLineIndex = this.occurrences.findIndex(item => item === line);
|
const firstLineIndex = this.occurrences.findIndex(item => item === line);
|
||||||
|
|
||||||
return firstLineIndex + occurence === this.activeOverlayIndex;
|
return firstLineIndex + occurence === this.activeOverlayIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,9 +6,11 @@ import logger from "../main/logger";
|
|||||||
if (isMac) {
|
if (isMac) {
|
||||||
for (const crt of macca.all()) {
|
for (const crt of macca.all()) {
|
||||||
const attributes = crt.issuer?.attributes?.map((a: any) => `${a.name}=${a.value}`);
|
const attributes = crt.issuer?.attributes?.map((a: any) => `${a.name}=${a.value}`);
|
||||||
|
|
||||||
logger.debug(`Using host CA: ${attributes.join(",")}`);
|
logger.debug(`Using host CA: ${attributes.join(",")}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
winca.inject("+"); // see: https://github.com/ukoloff/win-ca#caveats
|
winca.inject("+"); // see: https://github.com/ukoloff/win-ca#caveats
|
||||||
}
|
}
|
||||||
|
|||||||
@ -102,6 +102,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
|||||||
protected refreshNewContexts = async () => {
|
protected refreshNewContexts = async () => {
|
||||||
try {
|
try {
|
||||||
const kubeConfig = await readFile(this.kubeConfigPath, "utf8");
|
const kubeConfig = await readFile(this.kubeConfigPath, "utf8");
|
||||||
|
|
||||||
if (kubeConfig) {
|
if (kubeConfig) {
|
||||||
this.newContexts.clear();
|
this.newContexts.clear();
|
||||||
loadConfig(kubeConfig).getContexts()
|
loadConfig(kubeConfig).getContexts()
|
||||||
@ -118,6 +119,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
|||||||
@action
|
@action
|
||||||
markNewContextsAsSeen() {
|
markNewContextsAsSeen() {
|
||||||
const { seenContexts, newContexts } = this;
|
const { seenContexts, newContexts } = this;
|
||||||
|
|
||||||
this.seenContexts.replace([...seenContexts, ...newContexts]);
|
this.seenContexts.replace([...seenContexts, ...newContexts]);
|
||||||
this.newContexts.clear();
|
this.newContexts.clear();
|
||||||
}
|
}
|
||||||
@ -133,9 +135,11 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
|||||||
@action
|
@action
|
||||||
protected async fromStore(data: Partial<UserStoreModel> = {}) {
|
protected async fromStore(data: Partial<UserStoreModel> = {}) {
|
||||||
const { lastSeenAppVersion, seenContexts = [], preferences, kubeConfigPath } = data;
|
const { lastSeenAppVersion, seenContexts = [], preferences, kubeConfigPath } = data;
|
||||||
|
|
||||||
if (lastSeenAppVersion) {
|
if (lastSeenAppVersion) {
|
||||||
this.lastSeenAppVersion = lastSeenAppVersion;
|
this.lastSeenAppVersion = lastSeenAppVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (kubeConfigPath) {
|
if (kubeConfigPath) {
|
||||||
this.kubeConfigPath = kubeConfigPath;
|
this.kubeConfigPath = kubeConfigPath;
|
||||||
}
|
}
|
||||||
@ -150,6 +154,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
|||||||
seenContexts: Array.from(this.seenContexts),
|
seenContexts: Array.from(this.seenContexts),
|
||||||
preferences: this.preferences,
|
preferences: this.preferences,
|
||||||
};
|
};
|
||||||
|
|
||||||
return toJS(model, {
|
return toJS(model, {
|
||||||
recurseEverything: true,
|
recurseEverything: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -12,7 +12,6 @@ export function autobind() {
|
|||||||
function bindClass<T extends Constructor>(constructor: T) {
|
function bindClass<T extends Constructor>(constructor: T) {
|
||||||
const proto = constructor.prototype;
|
const proto = constructor.prototype;
|
||||||
const descriptors = Object.getOwnPropertyDescriptors(proto);
|
const descriptors = Object.getOwnPropertyDescriptors(proto);
|
||||||
|
|
||||||
const skipMethod = (methodName: string) => {
|
const skipMethod = (methodName: string) => {
|
||||||
return methodName === "constructor"
|
return methodName === "constructor"
|
||||||
|| typeof descriptors[methodName].value !== "function";
|
|| typeof descriptors[methodName].value !== "function";
|
||||||
@ -21,6 +20,7 @@ function bindClass<T extends Constructor>(constructor: T) {
|
|||||||
Object.keys(descriptors).forEach(prop => {
|
Object.keys(descriptors).forEach(prop => {
|
||||||
if (skipMethod(prop)) return;
|
if (skipMethod(prop)) return;
|
||||||
const boundDescriptor = bindMethod(proto, prop, descriptors[prop]);
|
const boundDescriptor = bindMethod(proto, prop, descriptors[prop]);
|
||||||
|
|
||||||
Object.defineProperty(proto, prop, boundDescriptor);
|
Object.defineProperty(proto, prop, boundDescriptor);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -38,6 +38,7 @@ function bindMethod(target: object, prop?: string, descriptor?: PropertyDescript
|
|||||||
get() {
|
get() {
|
||||||
if (this === target) return func; // direct access from prototype
|
if (this === target) return func; // direct access from prototype
|
||||||
if (!boundFunc.has(this)) boundFunc.set(this, func.bind(this));
|
if (!boundFunc.has(this)) boundFunc.set(this, func.bind(this));
|
||||||
|
|
||||||
return boundFunc.get(this);
|
return boundFunc.get(this);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -7,8 +7,10 @@ export interface IURLParams<P extends object = {}, Q extends object = {}> {
|
|||||||
|
|
||||||
export function buildURL<P extends object = {}, Q extends object = {}>(path: string | any) {
|
export function buildURL<P extends object = {}, Q extends object = {}>(path: string | any) {
|
||||||
const pathBuilder = compile(String(path));
|
const pathBuilder = compile(String(path));
|
||||||
|
|
||||||
return function ({ params, query }: IURLParams<P, Q> = {}) {
|
return function ({ params, query }: IURLParams<P, Q> = {}) {
|
||||||
const queryParams = query ? new URLSearchParams(Object.entries(query)).toString() : "";
|
const queryParams = query ? new URLSearchParams(Object.entries(query)).toString() : "";
|
||||||
|
|
||||||
return pathBuilder(params) + (queryParams ? `?${queryParams}` : "");
|
return pathBuilder(params) + (queryParams ? `?${queryParams}` : "");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,9 @@ export function toCamelCase(obj: Record<string, any>): any {
|
|||||||
else if (isPlainObject(obj)) {
|
else if (isPlainObject(obj)) {
|
||||||
return Object.keys(obj).reduce((result, key) => {
|
return Object.keys(obj).reduce((result, key) => {
|
||||||
const value = obj[key];
|
const value = obj[key];
|
||||||
|
|
||||||
result[camelCase(key)] = typeof value === "object" ? toCamelCase(value) : value;
|
result[camelCase(key)] = typeof value === "object" ? toCamelCase(value) : value;
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}, {} as any);
|
}, {} as any);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
export function debouncePromise<T, F extends any[]>(func: (...args: F) => T | Promise<T>, timeout = 0): (...args: F) => Promise<T> {
|
export function debouncePromise<T, F extends any[]>(func: (...args: F) => T | Promise<T>, timeout = 0): (...args: F) => Promise<T> {
|
||||||
let timer: NodeJS.Timeout;
|
let timer: NodeJS.Timeout;
|
||||||
|
|
||||||
return (...params: any[]) => new Promise(resolve => {
|
return (...params: any[]) => new Promise(resolve => {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
timer = global.setTimeout(() => resolve(func.apply(this, params)), timeout);
|
timer = global.setTimeout(() => resolve(func.apply(this, params)), timeout);
|
||||||
|
|||||||
@ -26,6 +26,7 @@ export function downloadFile({ url, timeout, gzip = true }: DownloadFileOptions)
|
|||||||
resolve(Buffer.concat(fileChunks));
|
resolve(Buffer.concat(fileChunks));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url,
|
url,
|
||||||
promise,
|
promise,
|
||||||
|
|||||||
@ -2,5 +2,6 @@
|
|||||||
|
|
||||||
export function getRandId({ prefix = "", suffix = "", sep = "_" } = {}) {
|
export function getRandId({ prefix = "", suffix = "", sep = "_" } = {}) {
|
||||||
const randId = () => Math.random().toString(16).substr(2);
|
const randId = () => Math.random().toString(16).substr(2);
|
||||||
|
|
||||||
return [prefix, randId(), suffix].filter(s => s).join(sep);
|
return [prefix, randId(), suffix].filter(s => s).join(sep);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,9 @@ import { WriteFileOptions } from "fs";
|
|||||||
|
|
||||||
export function saveToAppFiles(filePath: string, contents: any, options?: WriteFileOptions): string {
|
export function saveToAppFiles(filePath: string, contents: any, options?: WriteFileOptions): string {
|
||||||
const absPath = path.resolve((app || remote.app).getPath("userData"), filePath);
|
const absPath = path.resolve((app || remote.app).getPath("userData"), filePath);
|
||||||
|
|
||||||
ensureDirSync(path.dirname(absPath));
|
ensureDirSync(path.dirname(absPath));
|
||||||
writeFileSync(absPath, contents, options);
|
writeFileSync(absPath, contents, options);
|
||||||
|
|
||||||
return absPath;
|
return absPath;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ class Singleton {
|
|||||||
if (!Singleton.instances.has(this)) {
|
if (!Singleton.instances.has(this)) {
|
||||||
Singleton.instances.set(this, Reflect.construct(this, args));
|
Singleton.instances.set(this, Reflect.construct(this, args));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Singleton.instances.get(this) as T;
|
return Singleton.instances.get(this) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,8 +12,10 @@
|
|||||||
*/
|
*/
|
||||||
export function splitArray<T>(array: T[], element: T): [T[], T[], boolean] {
|
export function splitArray<T>(array: T[], element: T): [T[], T[], boolean] {
|
||||||
const index = array.indexOf(element);
|
const index = array.indexOf(element);
|
||||||
|
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
return [array, [], false];
|
return [array, [], false];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [array.slice(0, index), array.slice(index + 1, array.length), true];
|
return [array.slice(0, index), array.slice(index + 1, array.length), true];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,7 @@ export function readFileFromTar<R = Buffer>({ tarPath, filePath, parseJson }: Re
|
|||||||
entry.once("end", () => {
|
entry.once("end", () => {
|
||||||
const data = Buffer.concat(fileChunks);
|
const data = Buffer.concat(fileChunks);
|
||||||
const result = parseJson ? JSON.parse(data.toString("utf8")) : data;
|
const result = parseJson ? JSON.parse(data.toString("utf8")) : data;
|
||||||
|
|
||||||
resolve(result);
|
resolve(result);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -39,12 +40,14 @@ export function readFileFromTar<R = Buffer>({ tarPath, filePath, parseJson }: Re
|
|||||||
|
|
||||||
export async function listTarEntries(filePath: string): Promise<string[]> {
|
export async function listTarEntries(filePath: string): Promise<string[]> {
|
||||||
const entries: string[] = [];
|
const entries: string[] = [];
|
||||||
|
|
||||||
await tar.list({
|
await tar.list({
|
||||||
file: filePath,
|
file: filePath,
|
||||||
onentry: (entry: FileStat) => {
|
onentry: (entry: FileStat) => {
|
||||||
entries.push(path.normalize(entry.path as any as string));
|
entries.push(path.normalize(entry.path as any as string));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -30,6 +30,7 @@ defineGlobal("__static", {
|
|||||||
if (isDevelopment) {
|
if (isDevelopment) {
|
||||||
return path.resolve(contextDir, "static");
|
return path.resolve(contextDir, "static");
|
||||||
}
|
}
|
||||||
|
|
||||||
return path.resolve(process.resourcesPath, "static");
|
return path.resolve(process.resourcesPath, "static");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -125,11 +125,14 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
|
|||||||
id: string;
|
id: string;
|
||||||
state: WorkspaceState;
|
state: WorkspaceState;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (ipcRenderer) {
|
if (ipcRenderer) {
|
||||||
logger.info("[WORKSPACE-STORE] requesting initial state sync");
|
logger.info("[WORKSPACE-STORE] requesting initial state sync");
|
||||||
const workspaceStates: workspaceStateSync[] = await requestMain(WorkspaceStore.stateRequestChannel);
|
const workspaceStates: workspaceStateSync[] = await requestMain(WorkspaceStore.stateRequestChannel);
|
||||||
|
|
||||||
workspaceStates.forEach((workspaceState) => {
|
workspaceStates.forEach((workspaceState) => {
|
||||||
const workspace = this.getById(workspaceState.id);
|
const workspace = this.getById(workspaceState.id);
|
||||||
|
|
||||||
if (workspace) {
|
if (workspace) {
|
||||||
workspace.setState(workspaceState.state);
|
workspace.setState(workspaceState.state);
|
||||||
}
|
}
|
||||||
@ -137,12 +140,14 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
|
|||||||
} else {
|
} else {
|
||||||
handleRequest(WorkspaceStore.stateRequestChannel, (): workspaceStateSync[] => {
|
handleRequest(WorkspaceStore.stateRequestChannel, (): workspaceStateSync[] => {
|
||||||
const states: workspaceStateSync[] = [];
|
const states: workspaceStateSync[] = [];
|
||||||
|
|
||||||
this.workspacesList.forEach((workspace) => {
|
this.workspacesList.forEach((workspace) => {
|
||||||
states.push({
|
states.push({
|
||||||
state: workspace.getState(),
|
state: workspace.getState(),
|
||||||
id: workspace.id
|
id: workspace.id
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return states;
|
return states;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -202,6 +207,7 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
|
|||||||
@action
|
@action
|
||||||
setActive(id = WorkspaceStore.defaultId) {
|
setActive(id = WorkspaceStore.defaultId) {
|
||||||
if (id === this.currentWorkspaceId) return;
|
if (id === this.currentWorkspaceId) return;
|
||||||
|
|
||||||
if (!this.getById(id)) {
|
if (!this.getById(id)) {
|
||||||
throw new Error(`workspace ${id} doesn't exist`);
|
throw new Error(`workspace ${id} doesn't exist`);
|
||||||
}
|
}
|
||||||
@ -211,15 +217,18 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
|
|||||||
@action
|
@action
|
||||||
addWorkspace(workspace: Workspace) {
|
addWorkspace(workspace: Workspace) {
|
||||||
const { id, name } = workspace;
|
const { id, name } = workspace;
|
||||||
|
|
||||||
if (!name.trim() || this.getByName(name.trim())) {
|
if (!name.trim() || this.getByName(name.trim())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.workspaces.set(id, workspace);
|
this.workspaces.set(id, workspace);
|
||||||
|
|
||||||
if (!workspace.isManaged) {
|
if (!workspace.isManaged) {
|
||||||
workspace.enabled = true;
|
workspace.enabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
appEventBus.emit({name: "workspace", action: "add"});
|
appEventBus.emit({name: "workspace", action: "add"});
|
||||||
|
|
||||||
return workspace;
|
return workspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,10 +246,13 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
|
|||||||
@action
|
@action
|
||||||
removeWorkspaceById(id: WorkspaceId) {
|
removeWorkspaceById(id: WorkspaceId) {
|
||||||
const workspace = this.getById(id);
|
const workspace = this.getById(id);
|
||||||
|
|
||||||
if (!workspace) return;
|
if (!workspace) return;
|
||||||
|
|
||||||
if (this.isDefault(id)) {
|
if (this.isDefault(id)) {
|
||||||
throw new Error("Cannot remove default workspace");
|
throw new Error("Cannot remove default workspace");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.currentWorkspaceId === id) {
|
if (this.currentWorkspaceId === id) {
|
||||||
this.currentWorkspaceId = WorkspaceStore.defaultId; // reset to default
|
this.currentWorkspaceId = WorkspaceStore.defaultId; // reset to default
|
||||||
}
|
}
|
||||||
@ -259,10 +271,12 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
|
|||||||
if (currentWorkspace) {
|
if (currentWorkspace) {
|
||||||
this.currentWorkspaceId = currentWorkspace;
|
this.currentWorkspaceId = currentWorkspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (workspaces.length) {
|
if (workspaces.length) {
|
||||||
this.workspaces.clear();
|
this.workspaces.clear();
|
||||||
workspaces.forEach(ws => {
|
workspaces.forEach(ws => {
|
||||||
const workspace = new Workspace(ws);
|
const workspace = new Workspace(ws);
|
||||||
|
|
||||||
if (!workspace.isManaged) {
|
if (!workspace.isManaged) {
|
||||||
workspace.enabled = true;
|
workspace.enabled = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -108,12 +108,15 @@ export abstract class ClusterFeature {
|
|||||||
*/
|
*/
|
||||||
protected renderTemplates(folderPath: string): string[] {
|
protected renderTemplates(folderPath: string): string[] {
|
||||||
const resources: string[] = [];
|
const resources: string[] = [];
|
||||||
|
|
||||||
logger.info(`[FEATURE]: render templates from ${folderPath}`);
|
logger.info(`[FEATURE]: render templates from ${folderPath}`);
|
||||||
fs.readdirSync(folderPath).forEach(filename => {
|
fs.readdirSync(folderPath).forEach(filename => {
|
||||||
const file = path.join(folderPath, filename);
|
const file = path.join(folderPath, filename);
|
||||||
const raw = fs.readFileSync(file);
|
const raw = fs.readFileSync(file);
|
||||||
|
|
||||||
if (filename.endsWith(".hb")) {
|
if (filename.endsWith(".hb")) {
|
||||||
const template = hb.compile(raw.toString());
|
const template = hb.compile(raw.toString());
|
||||||
|
|
||||||
resources.push(template(this.templateContext));
|
resources.push(template(this.templateContext));
|
||||||
} else {
|
} else {
|
||||||
resources.push(raw.toString());
|
resources.push(raw.toString());
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { extensionsStore } from "../extensions-store";
|
|||||||
|
|
||||||
export const version = getAppVersion();
|
export const version = getAppVersion();
|
||||||
export { isSnap, isWindows, isMac, isLinux, appName, slackUrl, issuesTrackerUrl } from "../../common/vars";
|
export { isSnap, isWindows, isMac, isLinux, appName, slackUrl, issuesTrackerUrl } from "../../common/vars";
|
||||||
|
|
||||||
export function getEnabledExtensions(): string[] {
|
export function getEnabledExtensions(): string[] {
|
||||||
return extensionsStore.enabledExtensions;
|
return extensionsStore.enabledExtensions;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,6 +25,7 @@ export interface InstalledExtension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const logModule = "[EXTENSION-DISCOVERY]";
|
const logModule = "[EXTENSION-DISCOVERY]";
|
||||||
|
|
||||||
export const manifestFilename = "package.json";
|
export const manifestFilename = "package.json";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -133,7 +134,6 @@ export class ExtensionDiscovery {
|
|||||||
if (path.basename(filePath) === manifestFilename) {
|
if (path.basename(filePath) === manifestFilename) {
|
||||||
try {
|
try {
|
||||||
const absPath = path.dirname(filePath);
|
const absPath = path.dirname(filePath);
|
||||||
|
|
||||||
// this.loadExtensionFromPath updates this.packagesJson
|
// this.loadExtensionFromPath updates this.packagesJson
|
||||||
const extension = await this.loadExtensionFromPath(absPath);
|
const extension = await this.loadExtensionFromPath(absPath);
|
||||||
|
|
||||||
@ -251,6 +251,7 @@ export class ExtensionDiscovery {
|
|||||||
|
|
||||||
manifestJson = __non_webpack_require__(manifestPath);
|
manifestJson = __non_webpack_require__(manifestPath);
|
||||||
const installedManifestPath = path.join(this.nodeModulesPath, manifestJson.name, "package.json");
|
const installedManifestPath = path.join(this.nodeModulesPath, manifestJson.name, "package.json");
|
||||||
|
|
||||||
this.packagesJson.dependencies[manifestJson.name] = path.dirname(manifestPath);
|
this.packagesJson.dependencies[manifestJson.name] = path.dirname(manifestPath);
|
||||||
const isEnabled = isBundled || extensionsStore.isEnabled(installedManifestPath);
|
const isEnabled = isBundled || extensionsStore.isEnabled(installedManifestPath);
|
||||||
|
|
||||||
@ -272,6 +273,7 @@ export class ExtensionDiscovery {
|
|||||||
async loadExtensions(): Promise<Map<LensExtensionId, InstalledExtension>> {
|
async loadExtensions(): Promise<Map<LensExtensionId, InstalledExtension>> {
|
||||||
const bundledExtensions = await this.loadBundledExtensions();
|
const bundledExtensions = await this.loadBundledExtensions();
|
||||||
const localExtensions = await this.loadFromFolder(this.localFolderPath);
|
const localExtensions = await this.loadFromFolder(this.localFolderPath);
|
||||||
|
|
||||||
await this.installPackages();
|
await this.installPackages();
|
||||||
const extensions = bundledExtensions.concat(localExtensions);
|
const extensions = bundledExtensions.concat(localExtensions);
|
||||||
|
|
||||||
@ -333,12 +335,14 @@ export class ExtensionDiscovery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const extension = await this.loadExtensionFromPath(absPath);
|
const extension = await this.loadExtensionFromPath(absPath);
|
||||||
|
|
||||||
if (extension) {
|
if (extension) {
|
||||||
extensions.push(extension);
|
extensions.push(extension);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`${logModule}: ${extensions.length} extensions loaded`, { folderPath, extensions });
|
logger.debug(`${logModule}: ${extensions.length} extensions loaded`, { folderPath, extensions });
|
||||||
|
|
||||||
return extensions;
|
return extensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -37,6 +37,7 @@ export class ExtensionInstaller {
|
|||||||
cwd: extensionPackagesRoot(),
|
cwd: extensionPackagesRoot(),
|
||||||
silent: true
|
silent: true
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on("close", () => {
|
child.on("close", () => {
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -176,6 +176,7 @@ export class ExtensionLoader {
|
|||||||
loadOnClusterRenderer() {
|
loadOnClusterRenderer() {
|
||||||
logger.info(`${logModule}: load on cluster renderer (dashboard)`);
|
logger.info(`${logModule}: load on cluster renderer (dashboard)`);
|
||||||
const cluster = getHostedCluster();
|
const cluster = getHostedCluster();
|
||||||
|
|
||||||
this.autoInitExtensions(async (extension: LensRendererExtension) => {
|
this.autoInitExtensions(async (extension: LensRendererExtension) => {
|
||||||
if (await extension.isEnabledForCluster(cluster) === false) {
|
if (await extension.isEnabledForCluster(cluster) === false) {
|
||||||
return [];
|
return [];
|
||||||
@ -209,11 +210,13 @@ export class ExtensionLoader {
|
|||||||
if (ext.isEnabled && !alreadyInit) {
|
if (ext.isEnabled && !alreadyInit) {
|
||||||
try {
|
try {
|
||||||
const LensExtensionClass = this.requireExtension(ext);
|
const LensExtensionClass = this.requireExtension(ext);
|
||||||
|
|
||||||
if (!LensExtensionClass) {
|
if (!LensExtensionClass) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const instance = new LensExtensionClass(ext);
|
const instance = new LensExtensionClass(ext);
|
||||||
|
|
||||||
instance.whenEnabled(() => register(instance));
|
instance.whenEnabled(() => register(instance));
|
||||||
instance.enable();
|
instance.enable();
|
||||||
this.instances.set(extId, instance);
|
this.instances.set(extId, instance);
|
||||||
@ -231,12 +234,14 @@ export class ExtensionLoader {
|
|||||||
|
|
||||||
protected requireExtension(extension: InstalledExtension): LensExtensionConstructor {
|
protected requireExtension(extension: InstalledExtension): LensExtensionConstructor {
|
||||||
let extEntrypoint = "";
|
let extEntrypoint = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (ipcRenderer && extension.manifest.renderer) {
|
if (ipcRenderer && extension.manifest.renderer) {
|
||||||
extEntrypoint = path.resolve(path.join(path.dirname(extension.manifestPath), extension.manifest.renderer));
|
extEntrypoint = path.resolve(path.join(path.dirname(extension.manifestPath), extension.manifest.renderer));
|
||||||
} else if (!ipcRenderer && extension.manifest.main) {
|
} else if (!ipcRenderer && extension.manifest.main) {
|
||||||
extEntrypoint = path.resolve(path.join(path.dirname(extension.manifestPath), extension.manifest.main));
|
extEntrypoint = path.resolve(path.join(path.dirname(extension.manifestPath), extension.manifest.main));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extEntrypoint !== "") {
|
if (extEntrypoint !== "") {
|
||||||
return __non_webpack_require__(extEntrypoint).default;
|
return __non_webpack_require__(extEntrypoint).default;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,11 +7,13 @@ export abstract class ExtensionStore<T> extends BaseStore<T> {
|
|||||||
|
|
||||||
async loadExtension(extension: LensExtension) {
|
async loadExtension(extension: LensExtension) {
|
||||||
this.extension = extension;
|
this.extension = extension;
|
||||||
|
|
||||||
return super.load();
|
return super.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
if (!this.extension) { return; }
|
if (!this.extension) { return; }
|
||||||
|
|
||||||
return super.load();
|
return super.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -30,11 +30,13 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
|
|||||||
|
|
||||||
protected getState(extensionLoader: ExtensionLoader) {
|
protected getState(extensionLoader: ExtensionLoader) {
|
||||||
const state: Record<LensExtensionId, LensExtensionState> = {};
|
const state: Record<LensExtensionId, LensExtensionState> = {};
|
||||||
|
|
||||||
return Array.from(extensionLoader.userExtensions).reduce((state, [extId, ext]) => {
|
return Array.from(extensionLoader.userExtensions).reduce((state, [extId, ext]) => {
|
||||||
state[extId] = {
|
state[extId] = {
|
||||||
enabled: ext.isEnabled,
|
enabled: ext.isEnabled,
|
||||||
name: ext.manifest.name,
|
name: ext.manifest.name,
|
||||||
};
|
};
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}, state);
|
}, state);
|
||||||
}
|
}
|
||||||
@ -47,6 +49,7 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
|
|||||||
reaction(() => this.state.toJS(), extensionsState => {
|
reaction(() => this.state.toJS(), extensionsState => {
|
||||||
extensionsState.forEach((state, extId) => {
|
extensionsState.forEach((state, extId) => {
|
||||||
const ext = extensionLoader.getExtension(extId);
|
const ext = extensionLoader.getExtension(extId);
|
||||||
|
|
||||||
if (ext && !ext.isBundled) {
|
if (ext && !ext.isBundled) {
|
||||||
ext.isEnabled = state.enabled;
|
ext.isEnabled = state.enabled;
|
||||||
}
|
}
|
||||||
@ -61,6 +64,7 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
|
|||||||
|
|
||||||
isEnabled(extId: LensExtensionId) {
|
isEnabled(extId: LensExtensionId) {
|
||||||
const state = this.state.get(extId);
|
const state = this.state.get(extId);
|
||||||
|
|
||||||
return state && state.enabled; // by default false
|
return state && state.enabled; // by default false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -86,6 +86,7 @@ export class LensExtension {
|
|||||||
const cancelReaction = reaction(() => this.isEnabled, async (isEnabled) => {
|
const cancelReaction = reaction(() => this.isEnabled, async (isEnabled) => {
|
||||||
if (isEnabled) {
|
if (isEnabled) {
|
||||||
const handlerDisposers = await handlers();
|
const handlerDisposers = await handlers();
|
||||||
|
|
||||||
disposers.push(...handlerDisposers);
|
disposers.push(...handlerDisposers);
|
||||||
} else {
|
} else {
|
||||||
unregisterHandlers();
|
unregisterHandlers();
|
||||||
@ -93,6 +94,7 @@ export class LensExtension {
|
|||||||
}, {
|
}, {
|
||||||
fireImmediately: true
|
fireImmediately: true
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unregisterHandlers();
|
unregisterHandlers();
|
||||||
cancelReaction();
|
cancelReaction();
|
||||||
|
|||||||
@ -13,6 +13,7 @@ export class LensMainExtension extends LensExtension {
|
|||||||
pageId,
|
pageId,
|
||||||
params: params ?? {}, // compile to url with params
|
params: params ?? {}, // compile to url with params
|
||||||
});
|
});
|
||||||
|
|
||||||
await windowManager.navigate(pageUrl, frameId);
|
await windowManager.navigate(pageUrl, frameId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,7 @@ export class LensRendererExtension extends LensExtension {
|
|||||||
pageId,
|
pageId,
|
||||||
params: params ?? {}, // compile to url with params
|
params: params ?? {}, // compile to url with params
|
||||||
});
|
});
|
||||||
|
|
||||||
navigate(pageUrl);
|
navigate(pageUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -73,6 +73,7 @@ describe("globalPageRegistry", () => {
|
|||||||
describe("getByPageMenuTarget", () => {
|
describe("getByPageMenuTarget", () => {
|
||||||
it("matching to first registered page without id", () => {
|
it("matching to first registered page without id", () => {
|
||||||
const page = globalPageRegistry.getByPageMenuTarget({ extensionId: ext.name });
|
const page = globalPageRegistry.getByPageMenuTarget({ extensionId: ext.name });
|
||||||
|
|
||||||
expect(page.id).toEqual(undefined);
|
expect(page.id).toEqual(undefined);
|
||||||
expect(page.extensionId).toEqual(ext.name);
|
expect(page.extensionId).toEqual(ext.name);
|
||||||
expect(page.routePath).toEqual(getExtensionPageUrl({ extensionId: ext.name }));
|
expect(page.routePath).toEqual(getExtensionPageUrl({ extensionId: ext.name }));
|
||||||
@ -83,6 +84,7 @@ describe("globalPageRegistry", () => {
|
|||||||
pageId: "test-page",
|
pageId: "test-page",
|
||||||
extensionId: ext.name
|
extensionId: ext.name
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(page.id).toEqual("test-page");
|
expect(page.id).toEqual("test-page");
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -91,6 +93,7 @@ describe("globalPageRegistry", () => {
|
|||||||
pageId: "wrong-page",
|
pageId: "wrong-page",
|
||||||
extensionId: ext.name
|
extensionId: ext.name
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(page).toBeNull();
|
expect(page).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -14,7 +14,9 @@ export class BaseRegistry<T> {
|
|||||||
@action
|
@action
|
||||||
add(items: T | T[]) {
|
add(items: T | T[]) {
|
||||||
const itemArray = rectify(items);
|
const itemArray = rectify(items);
|
||||||
|
|
||||||
this.items.push(...itemArray);
|
this.items.push(...itemArray);
|
||||||
|
|
||||||
return () => this.remove(...itemArray);
|
return () => this.remove(...itemArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,8 +20,10 @@ export class KubeObjectDetailRegistry extends BaseRegistry<KubeObjectDetailRegis
|
|||||||
if (item.priority === null) {
|
if (item.priority === null) {
|
||||||
item.priority = 50;
|
item.priority = 50;
|
||||||
}
|
}
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
});
|
});
|
||||||
|
|
||||||
return items.sort((a, b) => b.priority - a.priority);
|
return items.sort((a, b) => b.priority - a.priority);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,8 +35,10 @@ export class GlobalPageMenuRegistry extends BaseRegistry<PageMenuRegistration> {
|
|||||||
extensionId: ext.name,
|
extensionId: ext.name,
|
||||||
...(menuItem.target || {}),
|
...(menuItem.target || {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
return menuItem;
|
return menuItem;
|
||||||
});
|
});
|
||||||
|
|
||||||
return super.add(normalizedItems);
|
return super.add(normalizedItems);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -49,8 +51,10 @@ export class ClusterPageMenuRegistry extends BaseRegistry<ClusterPageMenuRegistr
|
|||||||
extensionId: ext.name,
|
extensionId: ext.name,
|
||||||
...(menuItem.target || {}),
|
...(menuItem.target || {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
return menuItem;
|
return menuItem;
|
||||||
});
|
});
|
||||||
|
|
||||||
return super.add(normalizedItems);
|
return super.add(normalizedItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -45,9 +45,11 @@ export function getExtensionPageUrl<P extends object>({ extensionId, pageId = ""
|
|||||||
name: sanitizeExtensionName(extensionId), // compile only with extension-id first and define base path
|
name: sanitizeExtensionName(extensionId), // compile only with extension-id first and define base path
|
||||||
});
|
});
|
||||||
const extPageRoutePath = path.join(extensionBaseUrl, pageId); // page-id might contain route :param-s, so don't compile yet
|
const extPageRoutePath = path.join(extensionBaseUrl, pageId); // page-id might contain route :param-s, so don't compile yet
|
||||||
|
|
||||||
if (params) {
|
if (params) {
|
||||||
return compile(extPageRoutePath)(params); // might throw error when required params not passed
|
return compile(extPageRoutePath)(params); // might throw error when required params not passed
|
||||||
}
|
}
|
||||||
|
|
||||||
return extPageRoutePath;
|
return extPageRoutePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,6 +58,7 @@ export class PageRegistry extends BaseRegistry<RegisteredPage> {
|
|||||||
add(items: PageRegistration | PageRegistration[], ext: LensExtension) {
|
add(items: PageRegistration | PageRegistration[], ext: LensExtension) {
|
||||||
const itemArray = rectify(items);
|
const itemArray = rectify(items);
|
||||||
let registeredPages: RegisteredPage[] = [];
|
let registeredPages: RegisteredPage[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
registeredPages = itemArray.map(page => ({
|
registeredPages = itemArray.map(page => ({
|
||||||
...page,
|
...page,
|
||||||
@ -69,6 +72,7 @@ export class PageRegistry extends BaseRegistry<RegisteredPage> {
|
|||||||
error: String(err),
|
error: String(err),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.add(registeredPages);
|
return super.add(registeredPages);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,8 +82,10 @@ export class PageRegistry extends BaseRegistry<RegisteredPage> {
|
|||||||
|
|
||||||
getByPageMenuTarget(target: PageMenuTarget = {}): RegisteredPage | null {
|
getByPageMenuTarget(target: PageMenuTarget = {}): RegisteredPage | null {
|
||||||
const targetUrl = getExtensionPageUrl(target);
|
const targetUrl = getExtensionPageUrl(target);
|
||||||
|
|
||||||
return this.getItems().find(({ id: pageId, extensionId }) => {
|
return this.getItems().find(({ id: pageId, extensionId }) => {
|
||||||
const pageUrl = getExtensionPageUrl({ extensionId, pageId, params: target.params }); // compiled with provided params
|
const pageUrl = getExtensionPageUrl({ extensionId, pageId, params: target.params }); // compiled with provided params
|
||||||
|
|
||||||
return targetUrl === pageUrl;
|
return targetUrl === pageUrl;
|
||||||
}) || null;
|
}) || null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,6 +40,7 @@ export class ClusterStore extends Singleton {
|
|||||||
if (!this.activeClusterId) {
|
if (!this.activeClusterId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.getById(this.activeClusterId);
|
return this.getById(this.activeClusterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -75,6 +75,7 @@ describe("create clusters", () => {
|
|||||||
preferences: {},
|
preferences: {},
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValue(Promise.resolve(true));
|
jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValue(Promise.resolve(true));
|
||||||
c = new Cluster({
|
c = new Cluster({
|
||||||
@ -112,6 +113,7 @@ describe("create clusters", () => {
|
|||||||
|
|
||||||
it("activating cluster should try to connect to cluster and do a refresh", async () => {
|
it("activating cluster should try to connect to cluster and do a refresh", async () => {
|
||||||
const port = await getFreePort();
|
const port = await getFreePort();
|
||||||
|
|
||||||
jest.spyOn(ContextHandler.prototype, "ensureServer");
|
jest.spyOn(ContextHandler.prototype, "ensureServer");
|
||||||
|
|
||||||
const mockListNSs = jest.fn();
|
const mockListNSs = jest.fn();
|
||||||
@ -122,17 +124,20 @@ describe("create clusters", () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
jest.spyOn(Cluster.prototype, "isClusterAdmin").mockReturnValue(Promise.resolve(true));
|
jest.spyOn(Cluster.prototype, "isClusterAdmin").mockReturnValue(Promise.resolve(true));
|
||||||
jest.spyOn(Cluster.prototype, "canI")
|
jest.spyOn(Cluster.prototype, "canI")
|
||||||
.mockImplementationOnce((attr: V1ResourceAttributes): Promise<boolean> => {
|
.mockImplementationOnce((attr: V1ResourceAttributes): Promise<boolean> => {
|
||||||
expect(attr.namespace).toBe("default");
|
expect(attr.namespace).toBe("default");
|
||||||
expect(attr.resource).toBe("pods");
|
expect(attr.resource).toBe("pods");
|
||||||
expect(attr.verb).toBe("list");
|
expect(attr.verb).toBe("list");
|
||||||
|
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
})
|
})
|
||||||
.mockImplementation((attr: V1ResourceAttributes): Promise<boolean> => {
|
.mockImplementation((attr: V1ResourceAttributes): Promise<boolean> => {
|
||||||
expect(attr.namespace).toBe("default");
|
expect(attr.namespace).toBe("default");
|
||||||
expect(attr.verb).toBe("list");
|
expect(attr.verb).toBe("list");
|
||||||
|
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
});
|
});
|
||||||
jest.spyOn(Cluster.prototype, "getProxyKubeconfig").mockReturnValue(mockKC as any);
|
jest.spyOn(Cluster.prototype, "getProxyKubeconfig").mockReturnValue(mockKC as any);
|
||||||
@ -148,6 +153,7 @@ describe("create clusters", () => {
|
|||||||
|
|
||||||
mockedRequest.mockImplementationOnce(((uri: any) => {
|
mockedRequest.mockImplementationOnce(((uri: any) => {
|
||||||
expect(uri).toBe(`http://localhost:${port}/api-kube/version`);
|
expect(uri).toBe(`http://localhost:${port}/api-kube/version`);
|
||||||
|
|
||||||
return Promise.resolve({ gitVersion: "1.2.3" });
|
return Promise.resolve({ gitVersion: "1.2.3" });
|
||||||
}) as any);
|
}) as any);
|
||||||
|
|
||||||
@ -165,6 +171,7 @@ describe("create clusters", () => {
|
|||||||
kubeConfigPath: "minikube-config.yml",
|
kubeConfigPath: "minikube-config.yml",
|
||||||
workspace: workspaceStore.currentWorkspaceId
|
workspace: workspaceStore.currentWorkspaceId
|
||||||
});
|
});
|
||||||
|
|
||||||
await c.init(port);
|
await c.init(port);
|
||||||
await c.activate();
|
await c.activate();
|
||||||
|
|
||||||
|
|||||||
@ -49,6 +49,7 @@ describe("kube auth proxy tests", () => {
|
|||||||
it("calling exit multiple times shouldn't throw", async () => {
|
it("calling exit multiple times shouldn't throw", async () => {
|
||||||
const port = await getFreePort();
|
const port = await getFreePort();
|
||||||
const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" }), port, {});
|
const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" }), port, {});
|
||||||
|
|
||||||
kap.exit();
|
kap.exit();
|
||||||
kap.exit();
|
kap.exit();
|
||||||
kap.exit();
|
kap.exit();
|
||||||
@ -69,24 +70,29 @@ describe("kube auth proxy tests", () => {
|
|||||||
jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValueOnce(Promise.resolve(false));
|
jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValueOnce(Promise.resolve(false));
|
||||||
mockedCP.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): ChildProcess => {
|
mockedCP.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): ChildProcess => {
|
||||||
listeners[event] = listener;
|
listeners[event] = listener;
|
||||||
|
|
||||||
return mockedCP;
|
return mockedCP;
|
||||||
});
|
});
|
||||||
mockedCP.stderr = mock<Readable>();
|
mockedCP.stderr = mock<Readable>();
|
||||||
mockedCP.stderr.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => {
|
mockedCP.stderr.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => {
|
||||||
listeners[`stderr/${event}`] = listener;
|
listeners[`stderr/${event}`] = listener;
|
||||||
|
|
||||||
return mockedCP.stderr;
|
return mockedCP.stderr;
|
||||||
});
|
});
|
||||||
mockedCP.stdout = mock<Readable>();
|
mockedCP.stdout = mock<Readable>();
|
||||||
mockedCP.stdout.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => {
|
mockedCP.stdout.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => {
|
||||||
listeners[`stdout/${event}`] = listener;
|
listeners[`stdout/${event}`] = listener;
|
||||||
|
|
||||||
return mockedCP.stdout;
|
return mockedCP.stdout;
|
||||||
});
|
});
|
||||||
mockSpawn.mockImplementationOnce((command: string): ChildProcess => {
|
mockSpawn.mockImplementationOnce((command: string): ChildProcess => {
|
||||||
expect(command).toBe(bundledKubectlPath());
|
expect(command).toBe(bundledKubectlPath());
|
||||||
|
|
||||||
return mockedCP;
|
return mockedCP;
|
||||||
});
|
});
|
||||||
mockWaitUntilUsed.mockReturnValueOnce(Promise.resolve());
|
mockWaitUntilUsed.mockReturnValueOnce(Promise.resolve());
|
||||||
const cluster = new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" });
|
const cluster = new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" });
|
||||||
|
|
||||||
jest.spyOn(cluster, "apiUrl", "get").mockReturnValue("https://fake.k8s.internal");
|
jest.spyOn(cluster, "apiUrl", "get").mockReturnValue("https://fake.k8s.internal");
|
||||||
proxy = new KubeAuthProxy(cluster, port, {});
|
proxy = new KubeAuthProxy(cluster, port, {});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -64,6 +64,7 @@ describe("kubeconfig manager tests", () => {
|
|||||||
preferences: {},
|
preferences: {},
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -86,6 +87,7 @@ describe("kubeconfig manager tests", () => {
|
|||||||
expect(kubeConfManager.getPath()).toBe("tmp/kubeconfig-foo");
|
expect(kubeConfManager.getPath()).toBe("tmp/kubeconfig-foo");
|
||||||
const file = await fse.readFile(kubeConfManager.getPath());
|
const file = await fse.readFile(kubeConfManager.getPath());
|
||||||
const yml = loadYaml<any>(file.toString());
|
const yml = loadYaml<any>(file.toString());
|
||||||
|
|
||||||
expect(yml["current-context"]).toBe("minikube");
|
expect(yml["current-context"]).toBe("minikube");
|
||||||
expect(yml["clusters"][0]["cluster"]["server"]).toBe(`http://127.0.0.1:${port}/foo`);
|
expect(yml["clusters"][0]["cluster"]["server"]).toBe(`http://127.0.0.1:${port}/foo`);
|
||||||
expect(yml["users"][0]["name"]).toBe("proxy");
|
expect(yml["users"][0]["name"]).toBe("proxy");
|
||||||
@ -101,8 +103,8 @@ describe("kubeconfig manager tests", () => {
|
|||||||
const contextHandler = new ContextHandler(cluster);
|
const contextHandler = new ContextHandler(cluster);
|
||||||
const port = await getFreePort();
|
const port = await getFreePort();
|
||||||
const kubeConfManager = await KubeconfigManager.create(cluster, contextHandler, port);
|
const kubeConfManager = await KubeconfigManager.create(cluster, contextHandler, port);
|
||||||
|
|
||||||
const configPath = kubeConfManager.getPath();
|
const configPath = kubeConfManager.getPath();
|
||||||
|
|
||||||
expect(await fse.pathExists(configPath)).toBe(true);
|
expect(await fse.pathExists(configPath)).toBe(true);
|
||||||
await kubeConfManager.unlink();
|
await kubeConfManager.unlink();
|
||||||
expect(await fse.pathExists(configPath)).toBe(false);
|
expect(await fse.pathExists(configPath)).toBe(false);
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export class AppUpdater {
|
|||||||
|
|
||||||
public start() {
|
public start() {
|
||||||
setInterval(AppUpdater.checkForUpdates, this.updateInterval);
|
setInterval(AppUpdater.checkForUpdates, this.updateInterval);
|
||||||
|
|
||||||
return AppUpdater.checkForUpdates();
|
return AppUpdater.checkForUpdates();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,7 @@ export class BaseClusterDetector {
|
|||||||
|
|
||||||
protected async k8sRequest<T = any>(path: string, options: RequestPromiseOptions = {}): Promise<T> {
|
protected async k8sRequest<T = any>(path: string, options: RequestPromiseOptions = {}): Promise<T> {
|
||||||
const apiUrl = this.cluster.kubeProxyUrl + path;
|
const apiUrl = this.cluster.kubeProxyUrl + path;
|
||||||
|
|
||||||
return request(apiUrl, {
|
return request(apiUrl, {
|
||||||
json: true,
|
json: true,
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
|
|||||||
@ -7,17 +7,20 @@ export class ClusterIdDetector extends BaseClusterDetector {
|
|||||||
|
|
||||||
public async detect() {
|
public async detect() {
|
||||||
let id: string;
|
let id: string;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
id = await this.getDefaultNamespaceId();
|
id = await this.getDefaultNamespaceId();
|
||||||
} catch(_) {
|
} catch(_) {
|
||||||
id = this.cluster.apiUrl;
|
id = this.cluster.apiUrl;
|
||||||
}
|
}
|
||||||
const value = createHash("sha256").update(id).digest("hex");
|
const value = createHash("sha256").update(id).digest("hex");
|
||||||
|
|
||||||
return { value, accuracy: 100 };
|
return { value, accuracy: 100 };
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getDefaultNamespaceId() {
|
protected async getDefaultNamespaceId() {
|
||||||
const response = await this.k8sRequest("/api/v1/namespaces/default");
|
const response = await this.k8sRequest("/api/v1/namespaces/default");
|
||||||
|
|
||||||
return response.metadata.uid;
|
return response.metadata.uid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -17,12 +17,16 @@ export class DetectorRegistry {
|
|||||||
|
|
||||||
async detectForCluster(cluster: Cluster): Promise<ClusterMetadata> {
|
async detectForCluster(cluster: Cluster): Promise<ClusterMetadata> {
|
||||||
const results: {[key: string]: ClusterDetectionResult } = {};
|
const results: {[key: string]: ClusterDetectionResult } = {};
|
||||||
|
|
||||||
for (const detectorClass of this.registry) {
|
for (const detectorClass of this.registry) {
|
||||||
const detector = new detectorClass(cluster);
|
const detector = new detectorClass(cluster);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await detector.detect();
|
const data = await detector.detect();
|
||||||
|
|
||||||
if (!data) continue;
|
if (!data) continue;
|
||||||
const existingValue = results[detector.key];
|
const existingValue = results[detector.key];
|
||||||
|
|
||||||
if (existingValue && existingValue.accuracy > data.accuracy) continue; // previous value exists and is more accurate
|
if (existingValue && existingValue.accuracy > data.accuracy) continue; // previous value exists and is more accurate
|
||||||
results[detector.key] = data;
|
results[detector.key] = data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -30,9 +34,11 @@ export class DetectorRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const metadata: ClusterMetadata = {};
|
const metadata: ClusterMetadata = {};
|
||||||
|
|
||||||
for (const [key, result] of Object.entries(results)) {
|
for (const [key, result] of Object.entries(results)) {
|
||||||
metadata[key] = result.value;
|
metadata[key] = result.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return metadata;
|
return metadata;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,30 +7,39 @@ export class DistributionDetector extends BaseClusterDetector {
|
|||||||
|
|
||||||
public async detect() {
|
public async detect() {
|
||||||
this.version = await this.getKubernetesVersion();
|
this.version = await this.getKubernetesVersion();
|
||||||
|
|
||||||
if (await this.isRancher()) {
|
if (await this.isRancher()) {
|
||||||
return { value: "rancher", accuracy: 80};
|
return { value: "rancher", accuracy: 80};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isGKE()) {
|
if (this.isGKE()) {
|
||||||
return { value: "gke", accuracy: 80};
|
return { value: "gke", accuracy: 80};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isEKS()) {
|
if (this.isEKS()) {
|
||||||
return { value: "eks", accuracy: 80};
|
return { value: "eks", accuracy: 80};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isIKS()) {
|
if (this.isIKS()) {
|
||||||
return { value: "iks", accuracy: 80};
|
return { value: "iks", accuracy: 80};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isAKS()) {
|
if (this.isAKS()) {
|
||||||
return { value: "aks", accuracy: 80};
|
return { value: "aks", accuracy: 80};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isDigitalOcean()) {
|
if (this.isDigitalOcean()) {
|
||||||
return { value: "digitalocean", accuracy: 90};
|
return { value: "digitalocean", accuracy: 90};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isMinikube()) {
|
if (this.isMinikube()) {
|
||||||
return { value: "minikube", accuracy: 80};
|
return { value: "minikube", accuracy: 80};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isCustom()) {
|
if (this.isCustom()) {
|
||||||
return { value: "custom", accuracy: 10};
|
return { value: "custom", accuracy: 10};
|
||||||
}
|
}
|
||||||
|
|
||||||
return { value: "unknown", accuracy: 10};
|
return { value: "unknown", accuracy: 10};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,6 +47,7 @@ export class DistributionDetector extends BaseClusterDetector {
|
|||||||
if (this.cluster.version) return this.cluster.version;
|
if (this.cluster.version) return this.cluster.version;
|
||||||
|
|
||||||
const response = await this.k8sRequest("/version");
|
const response = await this.k8sRequest("/version");
|
||||||
|
|
||||||
return response.gitVersion;
|
return response.gitVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,6 +82,7 @@ export class DistributionDetector extends BaseClusterDetector {
|
|||||||
protected async isRancher() {
|
protected async isRancher() {
|
||||||
try {
|
try {
|
||||||
const response = await this.k8sRequest("");
|
const response = await this.k8sRequest("");
|
||||||
|
|
||||||
return response.data.find((api: any) => api?.apiVersion?.group === "meta.cattle.io") !== undefined;
|
return response.data.find((api: any) => api?.apiVersion?.group === "meta.cattle.io") !== undefined;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@ -8,6 +8,7 @@ export class LastSeenDetector extends BaseClusterDetector {
|
|||||||
if (!this.cluster.accessible) return null;
|
if (!this.cluster.accessible) return null;
|
||||||
|
|
||||||
await this.k8sRequest("/version");
|
await this.k8sRequest("/version");
|
||||||
|
|
||||||
return { value: new Date().toJSON(), accuracy: 100 };
|
return { value: new Date().toJSON(), accuracy: 100 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -7,11 +7,13 @@ export class NodesCountDetector extends BaseClusterDetector {
|
|||||||
public async detect() {
|
public async detect() {
|
||||||
if (!this.cluster.accessible) return null;
|
if (!this.cluster.accessible) return null;
|
||||||
const nodeCount = await this.getNodeCount();
|
const nodeCount = await this.getNodeCount();
|
||||||
|
|
||||||
return { value: nodeCount, accuracy: 100};
|
return { value: nodeCount, accuracy: 100};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getNodeCount(): Promise<number> {
|
protected async getNodeCount(): Promise<number> {
|
||||||
const response = await this.k8sRequest("/api/v1/nodes");
|
const response = await this.k8sRequest("/api/v1/nodes");
|
||||||
|
|
||||||
return response.items.length;
|
return response.items.length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -7,11 +7,13 @@ export class VersionDetector extends BaseClusterDetector {
|
|||||||
|
|
||||||
public async detect() {
|
public async detect() {
|
||||||
const version = await this.getKubernetesVersion();
|
const version = await this.getKubernetesVersion();
|
||||||
|
|
||||||
return { value: version, accuracy: 100};
|
return { value: version, accuracy: 100};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getKubernetesVersion() {
|
public async getKubernetesVersion() {
|
||||||
const response = await this.k8sRequest("/version");
|
const response = await this.k8sRequest("/version");
|
||||||
|
|
||||||
return response.gitVersion;
|
return response.gitVersion;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -24,8 +24,10 @@ export class ClusterManager extends Singleton {
|
|||||||
// auto-stop removed clusters
|
// auto-stop removed clusters
|
||||||
autorun(() => {
|
autorun(() => {
|
||||||
const removedClusters = Array.from(clusterStore.removedClusters.values());
|
const removedClusters = Array.from(clusterStore.removedClusters.values());
|
||||||
|
|
||||||
if (removedClusters.length > 0) {
|
if (removedClusters.length > 0) {
|
||||||
const meta = removedClusters.map(cluster => cluster.getMeta());
|
const meta = removedClusters.map(cluster => cluster.getMeta());
|
||||||
|
|
||||||
logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta);
|
logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta);
|
||||||
removedClusters.forEach(cluster => cluster.disconnect());
|
removedClusters.forEach(cluster => cluster.disconnect());
|
||||||
clusterStore.removedClusters.clear();
|
clusterStore.removedClusters.clear();
|
||||||
@ -70,7 +72,9 @@ export class ClusterManager extends Singleton {
|
|||||||
// lens-server is connecting to 127.0.0.1:<port>/<uid>
|
// lens-server is connecting to 127.0.0.1:<port>/<uid>
|
||||||
if (req.headers.host.startsWith("127.0.0.1")) {
|
if (req.headers.host.startsWith("127.0.0.1")) {
|
||||||
const clusterId = req.url.split("/")[1];
|
const clusterId = req.url.split("/")[1];
|
||||||
|
|
||||||
cluster = clusterStore.getById(clusterId);
|
cluster = clusterStore.getById(clusterId);
|
||||||
|
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
// we need to swap path prefix so that request is proxied to kube api
|
// we need to swap path prefix so that request is proxied to kube api
|
||||||
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix);
|
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix);
|
||||||
@ -79,6 +83,7 @@ export class ClusterManager extends Singleton {
|
|||||||
cluster = clusterStore.getById(req.headers["x-cluster-id"].toString());
|
cluster = clusterStore.getById(req.headers["x-cluster-id"].toString());
|
||||||
} else {
|
} else {
|
||||||
const clusterId = getClusterIdFromHost(req.headers.host);
|
const clusterId = getClusterIdFromHost(req.headers.host);
|
||||||
|
|
||||||
cluster = clusterStore.getById(clusterId);
|
cluster = clusterStore.getById(clusterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -91,6 +91,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
|
|
||||||
@computed get prometheusPreferences(): ClusterPrometheusPreferences {
|
@computed get prometheusPreferences(): ClusterPrometheusPreferences {
|
||||||
const { prometheus, prometheusProvider } = this.preferences;
|
const { prometheus, prometheusProvider } = this.preferences;
|
||||||
|
|
||||||
return toJS({ prometheus, prometheusProvider }, {
|
return toJS({ prometheus, prometheusProvider }, {
|
||||||
recurseEverything: true,
|
recurseEverything: true,
|
||||||
});
|
});
|
||||||
@ -103,6 +104,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
constructor(model: ClusterModel) {
|
constructor(model: ClusterModel) {
|
||||||
this.updateModel(model);
|
this.updateModel(model);
|
||||||
const kubeconfig = this.getKubeconfig();
|
const kubeconfig = this.getKubeconfig();
|
||||||
|
|
||||||
if (kubeconfig.getContextObject(this.contextName)) {
|
if (kubeconfig.getContextObject(this.contextName)) {
|
||||||
this.apiUrl = kubeconfig.getCluster(kubeconfig.getContextObject(this.contextName).cluster).server;
|
this.apiUrl = kubeconfig.getCluster(kubeconfig.getContextObject(this.contextName).cluster).server;
|
||||||
}
|
}
|
||||||
@ -167,13 +169,16 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
}
|
}
|
||||||
logger.info(`[CLUSTER]: activate`, this.getMeta());
|
logger.info(`[CLUSTER]: activate`, this.getMeta());
|
||||||
await this.whenInitialized;
|
await this.whenInitialized;
|
||||||
|
|
||||||
if (!this.eventDisposers.length) {
|
if (!this.eventDisposers.length) {
|
||||||
this.bindEvents();
|
this.bindEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.disconnected || !this.accessible) {
|
if (this.disconnected || !this.accessible) {
|
||||||
await this.reconnect();
|
await this.reconnect();
|
||||||
}
|
}
|
||||||
await this.refreshConnectionStatus();
|
await this.refreshConnectionStatus();
|
||||||
|
|
||||||
if (this.accessible) {
|
if (this.accessible) {
|
||||||
await this.refreshAllowedResources();
|
await this.refreshAllowedResources();
|
||||||
this.isAdmin = await this.isClusterAdmin();
|
this.isAdmin = await this.isClusterAdmin();
|
||||||
@ -181,11 +186,13 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
this.ensureKubectl();
|
this.ensureKubectl();
|
||||||
}
|
}
|
||||||
this.activated = true;
|
this.activated = true;
|
||||||
|
|
||||||
return this.pushState();
|
return this.pushState();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async ensureKubectl() {
|
protected async ensureKubectl() {
|
||||||
this.kubeCtl = new Kubectl(this.version);
|
this.kubeCtl = new Kubectl(this.version);
|
||||||
|
|
||||||
return this.kubeCtl.ensureKubectl(); // download kubectl in background, so it's not blocking dashboard
|
return this.kubeCtl.ensureKubectl(); // download kubectl in background, so it's not blocking dashboard
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,9 +222,11 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
logger.info(`[CLUSTER]: refresh`, this.getMeta());
|
logger.info(`[CLUSTER]: refresh`, this.getMeta());
|
||||||
await this.whenInitialized;
|
await this.whenInitialized;
|
||||||
await this.refreshConnectionStatus();
|
await this.refreshConnectionStatus();
|
||||||
|
|
||||||
if (this.accessible) {
|
if (this.accessible) {
|
||||||
this.isAdmin = await this.isClusterAdmin();
|
this.isAdmin = await this.isClusterAdmin();
|
||||||
await this.refreshAllowedResources();
|
await this.refreshAllowedResources();
|
||||||
|
|
||||||
if (opts.refreshMetadata) {
|
if (opts.refreshMetadata) {
|
||||||
this.refreshMetadata();
|
this.refreshMetadata();
|
||||||
}
|
}
|
||||||
@ -231,12 +240,14 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta());
|
logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta());
|
||||||
const metadata = await detectorRegistry.detectForCluster(this);
|
const metadata = await detectorRegistry.detectForCluster(this);
|
||||||
const existingMetadata = this.metadata;
|
const existingMetadata = this.metadata;
|
||||||
|
|
||||||
this.metadata = Object.assign(existingMetadata, metadata);
|
this.metadata = Object.assign(existingMetadata, metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async refreshConnectionStatus() {
|
async refreshConnectionStatus() {
|
||||||
const connectionStatus = await this.getConnectionStatus();
|
const connectionStatus = await this.getConnectionStatus();
|
||||||
|
|
||||||
this.online = connectionStatus > ClusterStatus.Offline;
|
this.online = connectionStatus > ClusterStatus.Offline;
|
||||||
this.accessible = connectionStatus == ClusterStatus.AccessGranted;
|
this.accessible = connectionStatus == ClusterStatus.AccessGranted;
|
||||||
}
|
}
|
||||||
@ -271,6 +282,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
getMetrics(prometheusPath: string, queryParams: IMetricsReqParams & { query: string }) {
|
getMetrics(prometheusPath: string, queryParams: IMetricsReqParams & { query: string }) {
|
||||||
const prometheusPrefix = this.preferences.prometheus?.prefix || "";
|
const prometheusPrefix = this.preferences.prometheus?.prefix || "";
|
||||||
const metricsPath = `/api/v1/namespaces/${prometheusPath}/proxy${prometheusPrefix}/api/v1/query_range`;
|
const metricsPath = `/api/v1/namespaces/${prometheusPath}/proxy${prometheusPrefix}/api/v1/query_range`;
|
||||||
|
|
||||||
return this.k8sRequest(metricsPath, {
|
return this.k8sRequest(metricsPath, {
|
||||||
timeout: 0,
|
timeout: 0,
|
||||||
resolveWithFullResponse: false,
|
resolveWithFullResponse: false,
|
||||||
@ -283,43 +295,54 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
try {
|
try {
|
||||||
const versionDetector = new VersionDetector(this);
|
const versionDetector = new VersionDetector(this);
|
||||||
const versionData = await versionDetector.detect();
|
const versionData = await versionDetector.detect();
|
||||||
|
|
||||||
this.metadata.version = versionData.value;
|
this.metadata.version = versionData.value;
|
||||||
|
|
||||||
return ClusterStatus.AccessGranted;
|
return ClusterStatus.AccessGranted;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to connect cluster "${this.contextName}": ${error}`);
|
logger.error(`Failed to connect cluster "${this.contextName}": ${error}`);
|
||||||
|
|
||||||
if (error.statusCode) {
|
if (error.statusCode) {
|
||||||
if (error.statusCode >= 400 && error.statusCode < 500) {
|
if (error.statusCode >= 400 && error.statusCode < 500) {
|
||||||
this.failureReason = "Invalid credentials";
|
this.failureReason = "Invalid credentials";
|
||||||
|
|
||||||
return ClusterStatus.AccessDenied;
|
return ClusterStatus.AccessDenied;
|
||||||
} else {
|
} else {
|
||||||
this.failureReason = error.error || error.message;
|
this.failureReason = error.error || error.message;
|
||||||
|
|
||||||
return ClusterStatus.Offline;
|
return ClusterStatus.Offline;
|
||||||
}
|
}
|
||||||
} else if (error.failed === true) {
|
} else if (error.failed === true) {
|
||||||
if (error.timedOut === true) {
|
if (error.timedOut === true) {
|
||||||
this.failureReason = "Connection timed out";
|
this.failureReason = "Connection timed out";
|
||||||
|
|
||||||
return ClusterStatus.Offline;
|
return ClusterStatus.Offline;
|
||||||
} else {
|
} else {
|
||||||
this.failureReason = "Failed to fetch credentials";
|
this.failureReason = "Failed to fetch credentials";
|
||||||
|
|
||||||
return ClusterStatus.AccessDenied;
|
return ClusterStatus.AccessDenied;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.failureReason = error.message;
|
this.failureReason = error.message;
|
||||||
|
|
||||||
return ClusterStatus.Offline;
|
return ClusterStatus.Offline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async canI(resourceAttributes: V1ResourceAttributes): Promise<boolean> {
|
async canI(resourceAttributes: V1ResourceAttributes): Promise<boolean> {
|
||||||
const authApi = this.getProxyKubeconfig().makeApiClient(AuthorizationV1Api);
|
const authApi = this.getProxyKubeconfig().makeApiClient(AuthorizationV1Api);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const accessReview = await authApi.createSelfSubjectAccessReview({
|
const accessReview = await authApi.createSelfSubjectAccessReview({
|
||||||
apiVersion: "authorization.k8s.io/v1",
|
apiVersion: "authorization.k8s.io/v1",
|
||||||
kind: "SelfSubjectAccessReview",
|
kind: "SelfSubjectAccessReview",
|
||||||
spec: { resourceAttributes }
|
spec: { resourceAttributes }
|
||||||
});
|
});
|
||||||
|
|
||||||
return accessReview.body.status.allowed;
|
return accessReview.body.status.allowed;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`failed to request selfSubjectAccessReview: ${error}`);
|
logger.error(`failed to request selfSubjectAccessReview: ${error}`);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -343,6 +366,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
ownerRef: this.ownerRef,
|
ownerRef: this.ownerRef,
|
||||||
accessibleNamespaces: this.accessibleNamespaces,
|
accessibleNamespaces: this.accessibleNamespaces,
|
||||||
};
|
};
|
||||||
|
|
||||||
return toJS(model, {
|
return toJS(model, {
|
||||||
recurseEverything: true
|
recurseEverything: true
|
||||||
});
|
});
|
||||||
@ -363,6 +387,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
allowedNamespaces: this.allowedNamespaces,
|
allowedNamespaces: this.allowedNamespaces,
|
||||||
allowedResources: this.allowedResources,
|
allowedResources: this.allowedResources,
|
||||||
};
|
};
|
||||||
|
|
||||||
return toJS(state, {
|
return toJS(state, {
|
||||||
recurseEverything: true
|
recurseEverything: true
|
||||||
});
|
});
|
||||||
@ -397,6 +422,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const api = this.getProxyKubeconfig().makeApiClient(CoreV1Api);
|
const api = this.getProxyKubeconfig().makeApiClient(CoreV1Api);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const namespaceList = await api.listNamespace();
|
const namespaceList = await api.listNamespace();
|
||||||
const nsAccessStatuses = await Promise.all(
|
const nsAccessStatuses = await Promise.all(
|
||||||
@ -406,12 +432,15 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
verb: "list",
|
verb: "list",
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
return namespaceList.body.items
|
return namespaceList.body.items
|
||||||
.filter((ns, i) => nsAccessStatuses[i])
|
.filter((ns, i) => nsAccessStatuses[i])
|
||||||
.map(ns => ns.metadata.name);
|
.map(ns => ns.metadata.name);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const ctx = this.getProxyKubeconfig().getContextObject(this.contextName);
|
const ctx = this.getProxyKubeconfig().getContextObject(this.contextName);
|
||||||
|
|
||||||
if (ctx.namespace) return [ctx.namespace];
|
if (ctx.namespace) return [ctx.namespace];
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -429,6 +458,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
namespace: this.allowedNamespaces[0]
|
namespace: this.allowedNamespaces[0]
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
return apiResources
|
return apiResources
|
||||||
.filter((resource, i) => resourceAccessStatuses[i])
|
.filter((resource, i) => resourceAccessStatuses[i])
|
||||||
.map(apiResource => apiResource.resource);
|
.map(apiResource => apiResource.resource);
|
||||||
|
|||||||
@ -25,28 +25,34 @@ export class ContextHandler {
|
|||||||
public setupPrometheus(preferences: ClusterPrometheusPreferences = {}) {
|
public setupPrometheus(preferences: ClusterPrometheusPreferences = {}) {
|
||||||
this.prometheusProvider = preferences.prometheusProvider?.type;
|
this.prometheusProvider = preferences.prometheusProvider?.type;
|
||||||
this.prometheusPath = null;
|
this.prometheusPath = null;
|
||||||
|
|
||||||
if (preferences.prometheus) {
|
if (preferences.prometheus) {
|
||||||
const { namespace, service, port } = preferences.prometheus;
|
const { namespace, service, port } = preferences.prometheus;
|
||||||
|
|
||||||
this.prometheusPath = `${namespace}/services/${service}:${port}`;
|
this.prometheusPath = `${namespace}/services/${service}:${port}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async resolvePrometheusPath(): Promise<string> {
|
protected async resolvePrometheusPath(): Promise<string> {
|
||||||
const prometheusService = await this.getPrometheusService();
|
const prometheusService = await this.getPrometheusService();
|
||||||
|
|
||||||
if (!prometheusService) return null;
|
if (!prometheusService) return null;
|
||||||
const { service, namespace, port } = prometheusService;
|
const { service, namespace, port } = prometheusService;
|
||||||
|
|
||||||
return `${namespace}/services/${service}:${port}`;
|
return `${namespace}/services/${service}:${port}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPrometheusProvider() {
|
async getPrometheusProvider() {
|
||||||
if (!this.prometheusProvider) {
|
if (!this.prometheusProvider) {
|
||||||
const service = await this.getPrometheusService();
|
const service = await this.getPrometheusService();
|
||||||
|
|
||||||
if (!service) {
|
if (!service) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
logger.info(`using ${service.id} as prometheus provider`);
|
logger.info(`using ${service.id} as prometheus provider`);
|
||||||
this.prometheusProvider = service.id;
|
this.prometheusProvider = service.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
return prometheusProviders.find(p => p.id === this.prometheusProvider);
|
return prometheusProviders.find(p => p.id === this.prometheusProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,9 +60,11 @@ export class ContextHandler {
|
|||||||
const providers = this.prometheusProvider ? prometheusProviders.filter(provider => provider.id == this.prometheusProvider) : prometheusProviders;
|
const providers = this.prometheusProvider ? prometheusProviders.filter(provider => provider.id == this.prometheusProvider) : prometheusProviders;
|
||||||
const prometheusPromises: Promise<PrometheusService>[] = providers.map(async (provider: PrometheusProvider): Promise<PrometheusService> => {
|
const prometheusPromises: Promise<PrometheusService>[] = providers.map(async (provider: PrometheusProvider): Promise<PrometheusService> => {
|
||||||
const apiClient = this.cluster.getProxyKubeconfig().makeApiClient(CoreV1Api);
|
const apiClient = this.cluster.getProxyKubeconfig().makeApiClient(CoreV1Api);
|
||||||
|
|
||||||
return await provider.getPrometheusService(apiClient);
|
return await provider.getPrometheusService(apiClient);
|
||||||
});
|
});
|
||||||
const resolvedPrometheusServices = await Promise.all(prometheusPromises);
|
const resolvedPrometheusServices = await Promise.all(prometheusPromises);
|
||||||
|
|
||||||
return resolvedPrometheusServices.filter(n => n)[0];
|
return resolvedPrometheusServices.filter(n => n)[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,12 +72,14 @@ export class ContextHandler {
|
|||||||
if (!this.prometheusPath) {
|
if (!this.prometheusPath) {
|
||||||
this.prometheusPath = await this.resolvePrometheusPath();
|
this.prometheusPath = await this.resolvePrometheusPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.prometheusPath;
|
return this.prometheusPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
async resolveAuthProxyUrl() {
|
async resolveAuthProxyUrl() {
|
||||||
const proxyPort = await this.ensurePort();
|
const proxyPort = await this.ensurePort();
|
||||||
const path = this.clusterUrl.path !== "/" ? this.clusterUrl.path : "";
|
const path = this.clusterUrl.path !== "/" ? this.clusterUrl.path : "";
|
||||||
|
|
||||||
return `http://127.0.0.1:${proxyPort}${path}`;
|
return `http://127.0.0.1:${proxyPort}${path}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,14 +89,17 @@ export class ContextHandler {
|
|||||||
}
|
}
|
||||||
const timeout = isWatchRequest ? 4 * 60 * 60 * 1000 : 30000; // 4 hours for watch request, 30 seconds for the rest
|
const timeout = isWatchRequest ? 4 * 60 * 60 * 1000 : 30000; // 4 hours for watch request, 30 seconds for the rest
|
||||||
const apiTarget = await this.newApiTarget(timeout);
|
const apiTarget = await this.newApiTarget(timeout);
|
||||||
|
|
||||||
if (!isWatchRequest) {
|
if (!isWatchRequest) {
|
||||||
this.apiTarget = apiTarget;
|
this.apiTarget = apiTarget;
|
||||||
}
|
}
|
||||||
|
|
||||||
return apiTarget;
|
return apiTarget;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async newApiTarget(timeout: number): Promise<httpProxy.ServerOptions> {
|
protected async newApiTarget(timeout: number): Promise<httpProxy.ServerOptions> {
|
||||||
const proxyUrl = await this.resolveAuthProxyUrl();
|
const proxyUrl = await this.resolveAuthProxyUrl();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
target: proxyUrl,
|
target: proxyUrl,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
@ -101,6 +114,7 @@ export class ContextHandler {
|
|||||||
if (!this.proxyPort) {
|
if (!this.proxyPort) {
|
||||||
this.proxyPort = await getFreePort();
|
this.proxyPort = await getFreePort();
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.proxyPort;
|
return this.proxyPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,6 +122,7 @@ export class ContextHandler {
|
|||||||
if (!this.kubeAuthProxy) {
|
if (!this.kubeAuthProxy) {
|
||||||
await this.ensurePort();
|
await this.ensurePort();
|
||||||
const proxyEnv = Object.assign({}, process.env);
|
const proxyEnv = Object.assign({}, process.env);
|
||||||
|
|
||||||
if (this.cluster.preferences.httpsProxy) {
|
if (this.cluster.preferences.httpsProxy) {
|
||||||
proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy;
|
proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import logger from "./logger";
|
|||||||
export function exitApp() {
|
export function exitApp() {
|
||||||
const windowManager = WindowManager.getInstance<WindowManager>();
|
const windowManager = WindowManager.getInstance<WindowManager>();
|
||||||
const clusterManager = ClusterManager.getInstance<ClusterManager>();
|
const clusterManager = ClusterManager.getInstance<ClusterManager>();
|
||||||
|
|
||||||
appEventBus.emit({ name: "service", action: "close" });
|
appEventBus.emit({ name: "service", action: "close" });
|
||||||
windowManager.hide();
|
windowManager.hide();
|
||||||
clusterManager.stop();
|
clusterManager.stop();
|
||||||
|
|||||||
@ -32,11 +32,14 @@ export class FilesystemProvisionerStore extends BaseStore<FSProvisionModel> {
|
|||||||
const salt = randomBytes(32).toString("hex");
|
const salt = randomBytes(32).toString("hex");
|
||||||
const hashedName = SHA256(`${extensionName}/${salt}`).toString();
|
const hashedName = SHA256(`${extensionName}/${salt}`).toString();
|
||||||
const dirPath = path.resolve(app.getPath("userData"), "extension_data", hashedName);
|
const dirPath = path.resolve(app.getPath("userData"), "extension_data", hashedName);
|
||||||
|
|
||||||
this.registeredExtensions.set(extensionName, dirPath);
|
this.registeredExtensions.set(extensionName, dirPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dirPath = this.registeredExtensions.get(extensionName);
|
const dirPath = this.registeredExtensions.get(extensionName);
|
||||||
|
|
||||||
await fse.ensureDir(dirPath);
|
await fse.ensureDir(dirPath);
|
||||||
|
|
||||||
return dirPath;
|
return dirPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,32 +20,39 @@ export class HelmChartManager {
|
|||||||
|
|
||||||
public async chart(name: string) {
|
public async chart(name: string) {
|
||||||
const charts = await this.charts();
|
const charts = await this.charts();
|
||||||
|
|
||||||
return charts[name];
|
return charts[name];
|
||||||
}
|
}
|
||||||
|
|
||||||
public async charts(): Promise<any> {
|
public async charts(): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const cachedYaml = await this.cachedYaml();
|
const cachedYaml = await this.cachedYaml();
|
||||||
|
|
||||||
return cachedYaml["entries"];
|
return cachedYaml["entries"];
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getReadme(name: string, version = "") {
|
public async getReadme(name: string, version = "") {
|
||||||
const helm = await helmCli.binaryPath();
|
const helm = await helmCli.binaryPath();
|
||||||
|
|
||||||
if(version && version != "") {
|
if(version && version != "") {
|
||||||
const { stdout } = await promiseExec(`"${helm}" show readme ${this.repo.name}/${name} --version ${version}`).catch((error) => { throw(error.stderr);});
|
const { stdout } = await promiseExec(`"${helm}" show readme ${this.repo.name}/${name} --version ${version}`).catch((error) => { throw(error.stderr);});
|
||||||
|
|
||||||
return stdout;
|
return stdout;
|
||||||
} else {
|
} else {
|
||||||
const { stdout } = await promiseExec(`"${helm}" show readme ${this.repo.name}/${name}`).catch((error) => { throw(error.stderr);});
|
const { stdout } = await promiseExec(`"${helm}" show readme ${this.repo.name}/${name}`).catch((error) => { throw(error.stderr);});
|
||||||
|
|
||||||
return stdout;
|
return stdout;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getValues(name: string, version = "") {
|
public async getValues(name: string, version = "") {
|
||||||
const helm = await helmCli.binaryPath();
|
const helm = await helmCli.binaryPath();
|
||||||
|
|
||||||
if(version && version != "") {
|
if(version && version != "") {
|
||||||
const { stdout } = await promiseExec(`"${helm}" show values ${this.repo.name}/${name} --version ${version}`).catch((error) => { throw(error.stderr);});
|
const { stdout } = await promiseExec(`"${helm}" show values ${this.repo.name}/${name} --version ${version}`).catch((error) => { throw(error.stderr);});
|
||||||
|
|
||||||
@ -61,6 +68,7 @@ export class HelmChartManager {
|
|||||||
if (!(this.repo.name in this.cache)) {
|
if (!(this.repo.name in this.cache)) {
|
||||||
const cacheFile = await fs.promises.readFile(this.repo.cacheFilePath, "utf-8");
|
const cacheFile = await fs.promises.readFile(this.repo.cacheFilePath, "utf-8");
|
||||||
const data = yaml.safeLoad(cacheFile);
|
const data = yaml.safeLoad(cacheFile);
|
||||||
|
|
||||||
for(const key in data["entries"]) {
|
for(const key in data["entries"]) {
|
||||||
data["entries"][key].forEach((version: any) => {
|
data["entries"][key].forEach((version: any) => {
|
||||||
version["repo"] = this.repo.name;
|
version["repo"] = this.repo.name;
|
||||||
@ -69,6 +77,7 @@ export class HelmChartManager {
|
|||||||
}
|
}
|
||||||
this.cache[this.repo.name] = Buffer.from(JSON.stringify(data));
|
this.cache[this.repo.name] = Buffer.from(JSON.stringify(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
return JSON.parse(this.cache[this.repo.name].toString());
|
return JSON.parse(this.cache[this.repo.name].toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ export class HelmCli extends LensBinary {
|
|||||||
originalBinaryName: "helm",
|
originalBinaryName: "helm",
|
||||||
newBinaryName: "helm3"
|
newBinaryName: "helm3"
|
||||||
};
|
};
|
||||||
|
|
||||||
super(opts);
|
super(opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,14 +12,15 @@ export class HelmReleaseManager {
|
|||||||
const helm = await helmCli.binaryPath();
|
const helm = await helmCli.binaryPath();
|
||||||
const namespaceFlag = namespace ? `-n ${namespace}` : "--all-namespaces";
|
const namespaceFlag = namespace ? `-n ${namespace}` : "--all-namespaces";
|
||||||
const { stdout } = await promiseExec(`"${helm}" ls --output json ${namespaceFlag} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
|
const { stdout } = await promiseExec(`"${helm}" ls --output json ${namespaceFlag} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
|
||||||
|
|
||||||
const output = JSON.parse(stdout);
|
const output = JSON.parse(stdout);
|
||||||
|
|
||||||
if (output.length == 0) {
|
if (output.length == 0) {
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
output.forEach((release: any, index: number) => {
|
output.forEach((release: any, index: number) => {
|
||||||
output[index] = toCamelCase(release);
|
output[index] = toCamelCase(release);
|
||||||
});
|
});
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,15 +28,19 @@ export class HelmReleaseManager {
|
|||||||
public async installChart(chart: string, values: any, name: string, namespace: string, version: string, pathToKubeconfig: string){
|
public async installChart(chart: string, values: any, name: string, namespace: string, version: string, pathToKubeconfig: string){
|
||||||
const helm = await helmCli.binaryPath();
|
const helm = await helmCli.binaryPath();
|
||||||
const fileName = tempy.file({name: "values.yaml"});
|
const fileName = tempy.file({name: "values.yaml"});
|
||||||
|
|
||||||
await fs.promises.writeFile(fileName, yaml.safeDump(values));
|
await fs.promises.writeFile(fileName, yaml.safeDump(values));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let generateName = "";
|
let generateName = "";
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
generateName = "--generate-name";
|
generateName = "--generate-name";
|
||||||
name = "";
|
name = "";
|
||||||
}
|
}
|
||||||
const { stdout } = await promiseExec(`"${helm}" install ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} ${generateName}`).catch((error) => { throw(error.stderr);});
|
const { stdout } = await promiseExec(`"${helm}" install ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} ${generateName}`).catch((error) => { throw(error.stderr);});
|
||||||
const releaseName = stdout.split("\n")[0].split(" ")[1].trim();
|
const releaseName = stdout.split("\n")[0].split(" ")[1].trim();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
log: stdout,
|
log: stdout,
|
||||||
release: {
|
release: {
|
||||||
@ -51,10 +56,12 @@ export class HelmReleaseManager {
|
|||||||
public async upgradeRelease(name: string, chart: string, values: any, namespace: string, version: string, cluster: Cluster){
|
public async upgradeRelease(name: string, chart: string, values: any, namespace: string, version: string, cluster: Cluster){
|
||||||
const helm = await helmCli.binaryPath();
|
const helm = await helmCli.binaryPath();
|
||||||
const fileName = tempy.file({name: "values.yaml"});
|
const fileName = tempy.file({name: "values.yaml"});
|
||||||
|
|
||||||
await fs.promises.writeFile(fileName, yaml.safeDump(values));
|
await fs.promises.writeFile(fileName, yaml.safeDump(values));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { stdout } = await promiseExec(`"${helm}" upgrade ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${cluster.getProxyKubeconfigPath()}`).catch((error) => { throw(error.stderr);});
|
const { stdout } = await promiseExec(`"${helm}" upgrade ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${cluster.getProxyKubeconfigPath()}`).catch((error) => { throw(error.stderr);});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
log: stdout,
|
log: stdout,
|
||||||
release: this.getRelease(name, namespace, cluster)
|
release: this.getRelease(name, namespace, cluster)
|
||||||
@ -68,7 +75,9 @@ export class HelmReleaseManager {
|
|||||||
const helm = await helmCli.binaryPath();
|
const helm = await helmCli.binaryPath();
|
||||||
const { stdout } = await promiseExec(`"${helm}" status ${name} --output json --namespace ${namespace} --kubeconfig ${cluster.getProxyKubeconfigPath()}`).catch((error) => { throw(error.stderr);});
|
const { stdout } = await promiseExec(`"${helm}" status ${name} --output json --namespace ${namespace} --kubeconfig ${cluster.getProxyKubeconfigPath()}`).catch((error) => { throw(error.stderr);});
|
||||||
const release = JSON.parse(stdout);
|
const release = JSON.parse(stdout);
|
||||||
|
|
||||||
release.resources = await this.getResources(name, namespace, cluster);
|
release.resources = await this.getResources(name, namespace, cluster);
|
||||||
|
|
||||||
return release;
|
return release;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,18 +91,21 @@ export class HelmReleaseManager {
|
|||||||
public async getValues(name: string, namespace: string, pathToKubeconfig: string) {
|
public async getValues(name: string, namespace: string, pathToKubeconfig: string) {
|
||||||
const helm = await helmCli.binaryPath();
|
const helm = await helmCli.binaryPath();
|
||||||
const { stdout, } = await promiseExec(`"${helm}" get values ${name} --all --output yaml --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
|
const { stdout, } = await promiseExec(`"${helm}" get values ${name} --all --output yaml --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
|
||||||
|
|
||||||
return stdout;
|
return stdout;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getHistory(name: string, namespace: string, pathToKubeconfig: string) {
|
public async getHistory(name: string, namespace: string, pathToKubeconfig: string) {
|
||||||
const helm = await helmCli.binaryPath();
|
const helm = await helmCli.binaryPath();
|
||||||
const { stdout } = await promiseExec(`"${helm}" history ${name} --output json --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
|
const { stdout } = await promiseExec(`"${helm}" history ${name} --output json --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
|
||||||
|
|
||||||
return JSON.parse(stdout);
|
return JSON.parse(stdout);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async rollback(name: string, namespace: string, revision: number, pathToKubeconfig: string) {
|
public async rollback(name: string, namespace: string, revision: number, pathToKubeconfig: string) {
|
||||||
const helm = await helmCli.binaryPath();
|
const helm = await helmCli.binaryPath();
|
||||||
const { stdout } = await promiseExec(`"${helm}" rollback ${name} ${revision} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
|
const { stdout } = await promiseExec(`"${helm}" rollback ${name} ${revision} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
|
||||||
|
|
||||||
return stdout;
|
return stdout;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,6 +116,7 @@ export class HelmReleaseManager {
|
|||||||
const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectl}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`).catch(() => {
|
const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectl}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`).catch(() => {
|
||||||
return { stdout: JSON.stringify({items: []})};
|
return { stdout: JSON.stringify({items: []})};
|
||||||
});
|
});
|
||||||
|
|
||||||
return stdout;
|
return stdout;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,12 +42,14 @@ export class HelmRepoManager extends Singleton {
|
|||||||
resolveWithFullResponse: true,
|
resolveWithFullResponse: true,
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
});
|
});
|
||||||
|
|
||||||
return orderBy<HelmRepo>(res.body, repo => repo.name);
|
return orderBy<HelmRepo>(res.body, repo => repo.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
helmCli.setLogger(logger);
|
helmCli.setLogger(logger);
|
||||||
await helmCli.ensureBinary();
|
await helmCli.ensureBinary();
|
||||||
|
|
||||||
if (!this.initialized) {
|
if (!this.initialized) {
|
||||||
this.helmEnv = await this.parseHelmEnv();
|
this.helmEnv = await this.parseHelmEnv();
|
||||||
await this.update();
|
await this.update();
|
||||||
@ -62,12 +64,15 @@ export class HelmRepoManager extends Singleton {
|
|||||||
});
|
});
|
||||||
const lines = stdout.split(/\r?\n/); // split by new line feed
|
const lines = stdout.split(/\r?\n/); // split by new line feed
|
||||||
const env: HelmEnv = {};
|
const env: HelmEnv = {};
|
||||||
|
|
||||||
lines.forEach((line: string) => {
|
lines.forEach((line: string) => {
|
||||||
const [key, value] = line.split("=");
|
const [key, value] = line.split("=");
|
||||||
|
|
||||||
if (key && value) {
|
if (key && value) {
|
||||||
env[key] = value.replace(/"/g, ""); // strip quotas
|
env[key] = value.replace(/"/g, ""); // strip quotas
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,6 +80,7 @@ export class HelmRepoManager extends Singleton {
|
|||||||
if (!this.initialized) {
|
if (!this.initialized) {
|
||||||
await this.init();
|
await this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const repoConfigFile = this.helmEnv.HELM_REPOSITORY_CONFIG;
|
const repoConfigFile = this.helmEnv.HELM_REPOSITORY_CONFIG;
|
||||||
const { repositories }: HelmRepoConfig = await readFile(repoConfigFile, "utf8")
|
const { repositories }: HelmRepoConfig = await readFile(repoConfigFile, "utf8")
|
||||||
@ -82,22 +88,27 @@ export class HelmRepoManager extends Singleton {
|
|||||||
.catch(() => ({
|
.catch(() => ({
|
||||||
repositories: []
|
repositories: []
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (!repositories.length) {
|
if (!repositories.length) {
|
||||||
await this.addRepo({ name: "bitnami", url: "https://charts.bitnami.com/bitnami" });
|
await this.addRepo({ name: "bitnami", url: "https://charts.bitnami.com/bitnami" });
|
||||||
|
|
||||||
return await this.repositories();
|
return await this.repositories();
|
||||||
}
|
}
|
||||||
|
|
||||||
return repositories.map(repo => ({
|
return repositories.map(repo => ({
|
||||||
...repo,
|
...repo,
|
||||||
cacheFilePath: `${this.helmEnv.HELM_REPOSITORY_CACHE}/${repo.name}-index.yaml`
|
cacheFilePath: `${this.helmEnv.HELM_REPOSITORY_CACHE}/${repo.name}-index.yaml`
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[HELM]: repositories listing error "${error}"`);
|
logger.error(`[HELM]: repositories listing error "${error}"`);
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async repository(name: string) {
|
public async repository(name: string) {
|
||||||
const repositories = await this.repositories();
|
const repositories = await this.repositories();
|
||||||
|
|
||||||
return repositories.find(repo => repo.name == name);
|
return repositories.find(repo => repo.name == name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,6 +117,7 @@ export class HelmRepoManager extends Singleton {
|
|||||||
const { stdout } = await promiseExec(`"${helm}" repo update`).catch((error) => {
|
const { stdout } = await promiseExec(`"${helm}" repo update`).catch((error) => {
|
||||||
return { stdout: error.stdout };
|
return { stdout: error.stdout };
|
||||||
});
|
});
|
||||||
|
|
||||||
return stdout;
|
return stdout;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,6 +127,7 @@ export class HelmRepoManager extends Singleton {
|
|||||||
const { stdout } = await promiseExec(`"${helm}" repo add ${name} ${url}`).catch((error) => {
|
const { stdout } = await promiseExec(`"${helm}" repo add ${name} ${url}`).catch((error) => {
|
||||||
throw(error.stderr);
|
throw(error.stderr);
|
||||||
});
|
});
|
||||||
|
|
||||||
return stdout;
|
return stdout;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,6 +137,7 @@ export class HelmRepoManager extends Singleton {
|
|||||||
const { stdout } = await promiseExec(`"${helm}" repo remove ${name}`).catch((error) => {
|
const { stdout } = await promiseExec(`"${helm}" repo remove ${name}`).catch((error) => {
|
||||||
throw(error.stderr);
|
throw(error.stderr);
|
||||||
});
|
});
|
||||||
|
|
||||||
return stdout;
|
return stdout;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,18 +11,23 @@ class HelmService {
|
|||||||
|
|
||||||
public async listCharts() {
|
public async listCharts() {
|
||||||
const charts: any = {};
|
const charts: any = {};
|
||||||
|
|
||||||
await repoManager.init();
|
await repoManager.init();
|
||||||
const repositories = await repoManager.repositories();
|
const repositories = await repoManager.repositories();
|
||||||
|
|
||||||
for (const repo of repositories) {
|
for (const repo of repositories) {
|
||||||
charts[repo.name] = {};
|
charts[repo.name] = {};
|
||||||
const manager = new HelmChartManager(repo);
|
const manager = new HelmChartManager(repo);
|
||||||
let entries = await manager.charts();
|
let entries = await manager.charts();
|
||||||
|
|
||||||
entries = this.excludeDeprecated(entries);
|
entries = this.excludeDeprecated(entries);
|
||||||
|
|
||||||
for (const key in entries) {
|
for (const key in entries) {
|
||||||
entries[key] = entries[key][0];
|
entries[key] = entries[key][0];
|
||||||
}
|
}
|
||||||
charts[repo.name] = entries;
|
charts[repo.name] = entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
return charts;
|
return charts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,50 +39,60 @@ class HelmService {
|
|||||||
const repo = await repoManager.repository(repoName);
|
const repo = await repoManager.repository(repoName);
|
||||||
const chartManager = new HelmChartManager(repo);
|
const chartManager = new HelmChartManager(repo);
|
||||||
const chart = await chartManager.chart(chartName);
|
const chart = await chartManager.chart(chartName);
|
||||||
|
|
||||||
result.readme = await chartManager.getReadme(chartName, version);
|
result.readme = await chartManager.getReadme(chartName, version);
|
||||||
result.versions = chart;
|
result.versions = chart;
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getChartValues(repoName: string, chartName: string, version = "") {
|
public async getChartValues(repoName: string, chartName: string, version = "") {
|
||||||
const repo = await repoManager.repository(repoName);
|
const repo = await repoManager.repository(repoName);
|
||||||
const chartManager = new HelmChartManager(repo);
|
const chartManager = new HelmChartManager(repo);
|
||||||
|
|
||||||
return chartManager.getValues(chartName, version);
|
return chartManager.getValues(chartName, version);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async listReleases(cluster: Cluster, namespace: string = null) {
|
public async listReleases(cluster: Cluster, namespace: string = null) {
|
||||||
await repoManager.init();
|
await repoManager.init();
|
||||||
|
|
||||||
return await releaseManager.listReleases(cluster.getProxyKubeconfigPath(), namespace);
|
return await releaseManager.listReleases(cluster.getProxyKubeconfigPath(), namespace);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getRelease(cluster: Cluster, releaseName: string, namespace: string) {
|
public async getRelease(cluster: Cluster, releaseName: string, namespace: string) {
|
||||||
logger.debug("Fetch release");
|
logger.debug("Fetch release");
|
||||||
|
|
||||||
return await releaseManager.getRelease(releaseName, namespace, cluster);
|
return await releaseManager.getRelease(releaseName, namespace, cluster);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getReleaseValues(cluster: Cluster, releaseName: string, namespace: string) {
|
public async getReleaseValues(cluster: Cluster, releaseName: string, namespace: string) {
|
||||||
logger.debug("Fetch release values");
|
logger.debug("Fetch release values");
|
||||||
|
|
||||||
return await releaseManager.getValues(releaseName, namespace, cluster.getProxyKubeconfigPath());
|
return await releaseManager.getValues(releaseName, namespace, cluster.getProxyKubeconfigPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getReleaseHistory(cluster: Cluster, releaseName: string, namespace: string) {
|
public async getReleaseHistory(cluster: Cluster, releaseName: string, namespace: string) {
|
||||||
logger.debug("Fetch release history");
|
logger.debug("Fetch release history");
|
||||||
|
|
||||||
return await releaseManager.getHistory(releaseName, namespace, cluster.getProxyKubeconfigPath());
|
return await releaseManager.getHistory(releaseName, namespace, cluster.getProxyKubeconfigPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteRelease(cluster: Cluster, releaseName: string, namespace: string) {
|
public async deleteRelease(cluster: Cluster, releaseName: string, namespace: string) {
|
||||||
logger.debug("Delete release");
|
logger.debug("Delete release");
|
||||||
|
|
||||||
return await releaseManager.deleteRelease(releaseName, namespace, cluster.getProxyKubeconfigPath());
|
return await releaseManager.deleteRelease(releaseName, namespace, cluster.getProxyKubeconfigPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateRelease(cluster: Cluster, releaseName: string, namespace: string, data: { chart: string; values: {}; version: string }) {
|
public async updateRelease(cluster: Cluster, releaseName: string, namespace: string, data: { chart: string; values: {}; version: string }) {
|
||||||
logger.debug("Upgrade release");
|
logger.debug("Upgrade release");
|
||||||
|
|
||||||
return await releaseManager.upgradeRelease(releaseName, data.chart, data.values, namespace, data.version, cluster);
|
return await releaseManager.upgradeRelease(releaseName, data.chart, data.values, namespace, data.version, cluster);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async rollback(cluster: Cluster, releaseName: string, namespace: string, revision: number) {
|
public async rollback(cluster: Cluster, releaseName: string, namespace: string, revision: number) {
|
||||||
logger.debug("Rollback release");
|
logger.debug("Rollback release");
|
||||||
const output = await releaseManager.rollback(releaseName, namespace, revision, cluster.getProxyKubeconfigPath());
|
const output = await releaseManager.rollback(releaseName, namespace, revision, cluster.getProxyKubeconfigPath());
|
||||||
|
|
||||||
return { message: output };
|
return { message: output };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,9 +102,11 @@ class HelmService {
|
|||||||
if (Array.isArray(entry)) {
|
if (Array.isArray(entry)) {
|
||||||
return entry[0]["deprecated"] != true;
|
return entry[0]["deprecated"] != true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return entry["deprecated"] != true;
|
return entry["deprecated"] != true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -34,11 +34,13 @@ let clusterManager: ClusterManager;
|
|||||||
let windowManager: WindowManager;
|
let windowManager: WindowManager;
|
||||||
|
|
||||||
app.setName(appName);
|
app.setName(appName);
|
||||||
|
|
||||||
if (!process.env.CICD) {
|
if (!process.env.CICD) {
|
||||||
app.setPath("userData", workingDir);
|
app.setPath("userData", workingDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
mangleProxyEnv();
|
mangleProxyEnv();
|
||||||
|
|
||||||
if (app.commandLine.getSwitchValue("proxy-server") !== "") {
|
if (app.commandLine.getSwitchValue("proxy-server") !== "") {
|
||||||
process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server");
|
process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server");
|
||||||
}
|
}
|
||||||
@ -48,6 +50,7 @@ app.on("ready", async () => {
|
|||||||
await shellSync();
|
await shellSync();
|
||||||
|
|
||||||
const updater = new AppUpdater();
|
const updater = new AppUpdater();
|
||||||
|
|
||||||
updater.start();
|
updater.start();
|
||||||
|
|
||||||
registerFileProtocol("static", __static);
|
registerFileProtocol("static", __static);
|
||||||
@ -110,6 +113,7 @@ app.on("ready", async () => {
|
|||||||
|
|
||||||
app.on("activate", (event, hasVisibleWindows) => {
|
app.on("activate", (event, hasVisibleWindows) => {
|
||||||
logger.info("APP:ACTIVATE", { hasVisibleWindows });
|
logger.info("APP:ACTIVATE", { hasVisibleWindows });
|
||||||
|
|
||||||
if (!hasVisibleWindows) {
|
if (!hasVisibleWindows) {
|
||||||
windowManager.initMainWindow();
|
windowManager.initMainWindow();
|
||||||
}
|
}
|
||||||
@ -121,6 +125,7 @@ app.on("will-quit", (event) => {
|
|||||||
appEventBus.emit({name: "app", action: "close"});
|
appEventBus.emit({name: "app", action: "close"});
|
||||||
event.preventDefault(); // prevent app's default shutdown (e.g. required for telemetry, etc.)
|
event.preventDefault(); // prevent app's default shutdown (e.g. required for telemetry, etc.)
|
||||||
clusterManager?.stop(); // close cluster connections
|
clusterManager?.stop(); // close cluster connections
|
||||||
|
|
||||||
return; // skip exit to make tray work, to quit go to app's global menu or tray's menu
|
return; // skip exit to make tray work, to quit go to app's global menu or tray's menu
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -45,6 +45,7 @@ export class KubeAuthProxy {
|
|||||||
"--accept-hosts", this.acceptHosts,
|
"--accept-hosts", this.acceptHosts,
|
||||||
"--reject-paths", "^[^/]"
|
"--reject-paths", "^[^/]"
|
||||||
];
|
];
|
||||||
|
|
||||||
if (process.env.DEBUG_PROXY === "true") {
|
if (process.env.DEBUG_PROXY === "true") {
|
||||||
args.push("-v", "9");
|
args.push("-v", "9");
|
||||||
}
|
}
|
||||||
@ -62,6 +63,7 @@ export class KubeAuthProxy {
|
|||||||
|
|
||||||
this.proxyProcess.stdout.on("data", (data) => {
|
this.proxyProcess.stdout.on("data", (data) => {
|
||||||
let logItem = data.toString();
|
let logItem = data.toString();
|
||||||
|
|
||||||
if (logItem.startsWith("Starting to serve on")) {
|
if (logItem.startsWith("Starting to serve on")) {
|
||||||
logItem = "Authentication proxy started\n";
|
logItem = "Authentication proxy started\n";
|
||||||
}
|
}
|
||||||
@ -80,19 +82,23 @@ export class KubeAuthProxy {
|
|||||||
const error = data.split("http: proxy error:").slice(1).join("").trim();
|
const error = data.split("http: proxy error:").slice(1).join("").trim();
|
||||||
let errorMsg = error;
|
let errorMsg = error;
|
||||||
const jsonError = error.split("Response: ")[1];
|
const jsonError = error.split("Response: ")[1];
|
||||||
|
|
||||||
if (jsonError) {
|
if (jsonError) {
|
||||||
try {
|
try {
|
||||||
const parsedError = JSON.parse(jsonError);
|
const parsedError = JSON.parse(jsonError);
|
||||||
|
|
||||||
errorMsg = parsedError.error_description || parsedError.error || jsonError;
|
errorMsg = parsedError.error_description || parsedError.error || jsonError;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
errorMsg = jsonError.trim();
|
errorMsg = jsonError.trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return errorMsg;
|
return errorMsg;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async sendIpcLogMessage(res: KubeAuthProxyLog) {
|
protected async sendIpcLogMessage(res: KubeAuthProxyLog) {
|
||||||
const channel = `kube-auth:${this.cluster.id}`;
|
const channel = `kube-auth:${this.cluster.id}`;
|
||||||
|
|
||||||
logger.info(`[KUBE-AUTH]: out-channel "${channel}"`, { ...res, meta: this.cluster.getMeta() });
|
logger.info(`[KUBE-AUTH]: out-channel "${channel}"`, { ...res, meta: this.cluster.getMeta() });
|
||||||
broadcastMessage(channel, res);
|
broadcastMessage(channel, res);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,9 @@ export class KubeconfigManager {
|
|||||||
|
|
||||||
static async create(cluster: Cluster, contextHandler: ContextHandler, port: number) {
|
static async create(cluster: Cluster, contextHandler: ContextHandler, port: number) {
|
||||||
const kcm = new KubeconfigManager(cluster, contextHandler, port);
|
const kcm = new KubeconfigManager(cluster, contextHandler, port);
|
||||||
|
|
||||||
await kcm.init();
|
await kcm.init();
|
||||||
|
|
||||||
return kcm;
|
return kcm;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,13 +68,14 @@ export class KubeconfigManager {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
// write
|
// write
|
||||||
const configYaml = dumpConfigYaml(proxyConfig);
|
const configYaml = dumpConfigYaml(proxyConfig);
|
||||||
|
|
||||||
fs.ensureDir(path.dirname(tempFile));
|
fs.ensureDir(path.dirname(tempFile));
|
||||||
fs.writeFileSync(tempFile, configYaml, { mode: 0o600 });
|
fs.writeFileSync(tempFile, configYaml, { mode: 0o600 });
|
||||||
this.tempFile = tempFile;
|
this.tempFile = tempFile;
|
||||||
logger.debug(`Created temp kubeconfig "${contextName}" at "${tempFile}": \n${configYaml}`);
|
logger.debug(`Created temp kubeconfig "${contextName}" at "${tempFile}": \n${configYaml}`);
|
||||||
|
|
||||||
return tempFile;
|
return tempFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -27,12 +27,10 @@ const kubectlMap: Map<string, string> = new Map([
|
|||||||
["1.18", "1.18.8"],
|
["1.18", "1.18.8"],
|
||||||
["1.19", "1.19.0"]
|
["1.19", "1.19.0"]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const packageMirrors: Map<string, string> = new Map([
|
const packageMirrors: Map<string, string> = new Map([
|
||||||
["default", "https://storage.googleapis.com/kubernetes-release/release"],
|
["default", "https://storage.googleapis.com/kubernetes-release/release"],
|
||||||
["china", "https://mirror.azure.cn/kubernetes/kubectl"]
|
["china", "https://mirror.azure.cn/kubernetes/kubectl"]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let bundledPath: string;
|
let bundledPath: string;
|
||||||
const initScriptVersionString = "# lens-initscript v3\n";
|
const initScriptVersionString = "# lens-initscript v3\n";
|
||||||
|
|
||||||
@ -41,6 +39,7 @@ export function bundledKubectlPath(): string {
|
|||||||
|
|
||||||
if (isDevelopment || isTestEnv) {
|
if (isDevelopment || isTestEnv) {
|
||||||
const platformName = isWindows ? "windows" : process.platform;
|
const platformName = isWindows ? "windows" : process.platform;
|
||||||
|
|
||||||
bundledPath = path.join(process.cwd(), "binaries", "client", platformName, process.arch, "kubectl");
|
bundledPath = path.join(process.cwd(), "binaries", "client", platformName, process.arch, "kubectl");
|
||||||
} else {
|
} else {
|
||||||
bundledPath = path.join(process.resourcesPath, process.arch, "kubectl");
|
bundledPath = path.join(process.resourcesPath, process.arch, "kubectl");
|
||||||
@ -71,12 +70,14 @@ export class Kubectl {
|
|||||||
// Returns the single bundled Kubectl instance
|
// Returns the single bundled Kubectl instance
|
||||||
public static bundled() {
|
public static bundled() {
|
||||||
if (!Kubectl.bundledInstance) Kubectl.bundledInstance = new Kubectl(Kubectl.bundledKubectlVersion);
|
if (!Kubectl.bundledInstance) Kubectl.bundledInstance = new Kubectl(Kubectl.bundledKubectlVersion);
|
||||||
|
|
||||||
return Kubectl.bundledInstance;
|
return Kubectl.bundledInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(clusterVersion: string) {
|
constructor(clusterVersion: string) {
|
||||||
const versionParts = /^v?(\d+\.\d+)(.*)/.exec(clusterVersion);
|
const versionParts = /^v?(\d+\.\d+)(.*)/.exec(clusterVersion);
|
||||||
const minorVersion = versionParts[1];
|
const minorVersion = versionParts[1];
|
||||||
|
|
||||||
/* minorVersion is the first two digits of kube server version
|
/* minorVersion is the first two digits of kube server version
|
||||||
if the version map includes that, use that version, if not, fallback to the exact x.y.z of kube version */
|
if the version map includes that, use that version, if not, fallback to the exact x.y.z of kube version */
|
||||||
if (kubectlMap.has(minorVersion)) {
|
if (kubectlMap.has(minorVersion)) {
|
||||||
@ -134,18 +135,22 @@ export class Kubectl {
|
|||||||
// return binary name if bundled path is not functional
|
// return binary name if bundled path is not functional
|
||||||
if (!await this.checkBinary(this.getBundledPath(), false)) {
|
if (!await this.checkBinary(this.getBundledPath(), false)) {
|
||||||
Kubectl.invalidBundle = true;
|
Kubectl.invalidBundle = true;
|
||||||
|
|
||||||
return path.basename(this.getBundledPath());
|
return path.basename(this.getBundledPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!await this.ensureKubectl()) {
|
if (!await this.ensureKubectl()) {
|
||||||
logger.error("Failed to ensure kubectl, fallback to the bundled version");
|
logger.error("Failed to ensure kubectl, fallback to the bundled version");
|
||||||
|
|
||||||
return this.getBundledPath();
|
return this.getBundledPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.path;
|
return this.path;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error("Failed to ensure kubectl, fallback to the bundled version");
|
logger.error("Failed to ensure kubectl, fallback to the bundled version");
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
|
|
||||||
return this.getBundledPath();
|
return this.getBundledPath();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -154,28 +159,35 @@ export class Kubectl {
|
|||||||
try {
|
try {
|
||||||
await this.ensureKubectl();
|
await this.ensureKubectl();
|
||||||
await this.writeInitScripts();
|
await this.writeInitScripts();
|
||||||
|
|
||||||
return this.dirname;
|
return this.dirname;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async checkBinary(path: string, checkVersion = true) {
|
public async checkBinary(path: string, checkVersion = true) {
|
||||||
const exists = await pathExists(path);
|
const exists = await pathExists(path);
|
||||||
|
|
||||||
if (exists) {
|
if (exists) {
|
||||||
try {
|
try {
|
||||||
const { stdout } = await promiseExec(`"${path}" version --client=true -o json`);
|
const { stdout } = await promiseExec(`"${path}" version --client=true -o json`);
|
||||||
const output = JSON.parse(stdout);
|
const output = JSON.parse(stdout);
|
||||||
|
|
||||||
if (!checkVersion) {
|
if (!checkVersion) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
let version: string = output.clientVersion.gitVersion;
|
let version: string = output.clientVersion.gitVersion;
|
||||||
|
|
||||||
if (version[0] === "v") {
|
if (version[0] === "v") {
|
||||||
version = version.slice(1);
|
version = version.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (version === this.kubectlVersion) {
|
if (version === this.kubectlVersion) {
|
||||||
logger.debug(`Local kubectl is version ${this.kubectlVersion}`);
|
logger.debug(`Local kubectl is version ${this.kubectlVersion}`);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
logger.error(`Local kubectl is version ${version}, expected ${this.kubectlVersion}, unlinking`);
|
logger.error(`Local kubectl is version ${version}, expected ${this.kubectlVersion}, unlinking`);
|
||||||
@ -184,6 +196,7 @@ export class Kubectl {
|
|||||||
}
|
}
|
||||||
await fs.promises.unlink(this.path);
|
await fs.promises.unlink(this.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,13 +204,16 @@ export class Kubectl {
|
|||||||
if (this.kubectlVersion === Kubectl.bundledKubectlVersion) {
|
if (this.kubectlVersion === Kubectl.bundledKubectlVersion) {
|
||||||
try {
|
try {
|
||||||
const exist = await pathExists(this.path);
|
const exist = await pathExists(this.path);
|
||||||
|
|
||||||
if (!exist) {
|
if (!exist) {
|
||||||
await fs.promises.copyFile(this.getBundledPath(), this.path);
|
await fs.promises.copyFile(this.getBundledPath(), this.path);
|
||||||
await fs.promises.chmod(this.path, 0o755);
|
await fs.promises.chmod(this.path, 0o755);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`Could not copy the bundled kubectl to app-data: ${err}`);
|
logger.error(`Could not copy the bundled kubectl to app-data: ${err}`);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -209,35 +225,44 @@ export class Kubectl {
|
|||||||
if (userStore.preferences?.downloadKubectlBinaries === false) {
|
if (userStore.preferences?.downloadKubectlBinaries === false) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Kubectl.invalidBundle) {
|
if (Kubectl.invalidBundle) {
|
||||||
logger.error(`Detected invalid bundle binary, returning ...`);
|
logger.error(`Detected invalid bundle binary, returning ...`);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
await ensureDir(this.dirname, 0o755);
|
await ensureDir(this.dirname, 0o755);
|
||||||
|
|
||||||
return lockFile.lock(this.dirname).then(async (release) => {
|
return lockFile.lock(this.dirname).then(async (release) => {
|
||||||
logger.debug(`Acquired a lock for ${this.kubectlVersion}`);
|
logger.debug(`Acquired a lock for ${this.kubectlVersion}`);
|
||||||
const bundled = await this.checkBundled();
|
const bundled = await this.checkBundled();
|
||||||
let isValid = await this.checkBinary(this.path, !bundled);
|
let isValid = await this.checkBinary(this.path, !bundled);
|
||||||
|
|
||||||
if (!isValid && !bundled) {
|
if (!isValid && !bundled) {
|
||||||
await this.downloadKubectl().catch((error) => {
|
await this.downloadKubectl().catch((error) => {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
logger.debug(`Releasing lock for ${this.kubectlVersion}`);
|
logger.debug(`Releasing lock for ${this.kubectlVersion}`);
|
||||||
release();
|
release();
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
isValid = !await this.checkBinary(this.path, false);
|
isValid = !await this.checkBinary(this.path, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
logger.debug(`Releasing lock for ${this.kubectlVersion}`);
|
logger.debug(`Releasing lock for ${this.kubectlVersion}`);
|
||||||
release();
|
release();
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
logger.debug(`Releasing lock for ${this.kubectlVersion}`);
|
logger.debug(`Releasing lock for ${this.kubectlVersion}`);
|
||||||
release();
|
release();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
logger.error(`Failed to get a lock for ${this.kubectlVersion}`);
|
logger.error(`Failed to get a lock for ${this.kubectlVersion}`);
|
||||||
logger.error(e);
|
logger.error(e);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -246,12 +271,14 @@ export class Kubectl {
|
|||||||
await ensureDir(path.dirname(this.path), 0o755);
|
await ensureDir(path.dirname(this.path), 0o755);
|
||||||
|
|
||||||
logger.info(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`);
|
logger.info(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`);
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const stream = customRequest({
|
const stream = customRequest({
|
||||||
url: this.url,
|
url: this.url,
|
||||||
gzip: true,
|
gzip: true,
|
||||||
});
|
});
|
||||||
const file = fs.createWriteStream(this.path);
|
const file = fs.createWriteStream(this.path);
|
||||||
|
|
||||||
stream.on("complete", () => {
|
stream.on("complete", () => {
|
||||||
logger.debug("kubectl binary download finished");
|
logger.debug("kubectl binary download finished");
|
||||||
file.end();
|
file.end();
|
||||||
@ -279,8 +306,8 @@ export class Kubectl {
|
|||||||
const helmPath = helmCli.getBinaryDir();
|
const helmPath = helmCli.getBinaryDir();
|
||||||
const fsPromises = fs.promises;
|
const fsPromises = fs.promises;
|
||||||
const bashScriptPath = path.join(this.dirname, ".bash_set_path");
|
const bashScriptPath = path.join(this.dirname, ".bash_set_path");
|
||||||
|
|
||||||
let bashScript = `${initScriptVersionString}`;
|
let bashScript = `${initScriptVersionString}`;
|
||||||
|
|
||||||
bashScript += "tempkubeconfig=\"$KUBECONFIG\"\n";
|
bashScript += "tempkubeconfig=\"$KUBECONFIG\"\n";
|
||||||
bashScript += "test -f \"/etc/profile\" && . \"/etc/profile\"\n";
|
bashScript += "test -f \"/etc/profile\" && . \"/etc/profile\"\n";
|
||||||
bashScript += "if test -f \"$HOME/.bash_profile\"; then\n";
|
bashScript += "if test -f \"$HOME/.bash_profile\"; then\n";
|
||||||
@ -302,7 +329,6 @@ export class Kubectl {
|
|||||||
await fsPromises.writeFile(bashScriptPath, bashScript.toString(), { mode: 0o644 });
|
await fsPromises.writeFile(bashScriptPath, bashScript.toString(), { mode: 0o644 });
|
||||||
|
|
||||||
const zshScriptPath = path.join(this.dirname, ".zlogin");
|
const zshScriptPath = path.join(this.dirname, ".zlogin");
|
||||||
|
|
||||||
let zshScript = `${initScriptVersionString}`;
|
let zshScript = `${initScriptVersionString}`;
|
||||||
|
|
||||||
zshScript += "tempkubeconfig=\"$KUBECONFIG\"\n";
|
zshScript += "tempkubeconfig=\"$KUBECONFIG\"\n";
|
||||||
@ -335,9 +361,11 @@ export class Kubectl {
|
|||||||
|
|
||||||
protected getDownloadMirror() {
|
protected getDownloadMirror() {
|
||||||
const mirror = packageMirrors.get(userStore.preferences?.downloadMirror);
|
const mirror = packageMirrors.get(userStore.preferences?.downloadMirror);
|
||||||
|
|
||||||
if (mirror) {
|
if (mirror) {
|
||||||
return mirror;
|
return mirror;
|
||||||
}
|
}
|
||||||
|
|
||||||
return packageMirrors.get("default"); // MacOS packages are only available from default
|
return packageMirrors.get("default"); // MacOS packages are only available from default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,12 +8,14 @@ jest.mock("../common/user-store");
|
|||||||
describe("kubectlVersion", () => {
|
describe("kubectlVersion", () => {
|
||||||
it("returns bundled version if exactly same version used", async () => {
|
it("returns bundled version if exactly same version used", async () => {
|
||||||
const kubectl = new Kubectl(Kubectl.bundled().kubectlVersion);
|
const kubectl = new Kubectl(Kubectl.bundled().kubectlVersion);
|
||||||
|
|
||||||
expect(kubectl.kubectlVersion).toBe(Kubectl.bundled().kubectlVersion);
|
expect(kubectl.kubectlVersion).toBe(Kubectl.bundled().kubectlVersion);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns bundled version if same major.minor version is used", async () => {
|
it("returns bundled version if same major.minor version is used", async () => {
|
||||||
const { bundledKubectlVersion } = packageInfo.config;
|
const { bundledKubectlVersion } = packageInfo.config;
|
||||||
const kubectl = new Kubectl(bundledKubectlVersion);
|
const kubectl = new Kubectl(bundledKubectlVersion);
|
||||||
|
|
||||||
expect(kubectl.kubectlVersion).toBe(Kubectl.bundled().kubectlVersion);
|
expect(kubectl.kubectlVersion).toBe(Kubectl.bundled().kubectlVersion);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -24,19 +26,23 @@ describe("getPath()", () => {
|
|||||||
const kubectl = new Kubectl(bundledKubectlVersion);
|
const kubectl = new Kubectl(bundledKubectlVersion);
|
||||||
const kubectlPath = await kubectl.getPath();
|
const kubectlPath = await kubectl.getPath();
|
||||||
let binaryName = "kubectl";
|
let binaryName = "kubectl";
|
||||||
|
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
binaryName += ".exe";
|
binaryName += ".exe";
|
||||||
}
|
}
|
||||||
const expectedPath = path.join(Kubectl.kubectlDir, Kubectl.bundledKubectlVersion, binaryName);
|
const expectedPath = path.join(Kubectl.kubectlDir, Kubectl.bundledKubectlVersion, binaryName);
|
||||||
|
|
||||||
expect(kubectlPath).toBe(expectedPath);
|
expect(kubectlPath).toBe(expectedPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns plain binary name if bundled kubectl is non-functional", async () => {
|
it("returns plain binary name if bundled kubectl is non-functional", async () => {
|
||||||
const { bundledKubectlVersion } = packageInfo.config;
|
const { bundledKubectlVersion } = packageInfo.config;
|
||||||
const kubectl = new Kubectl(bundledKubectlVersion);
|
const kubectl = new Kubectl(bundledKubectlVersion);
|
||||||
|
|
||||||
jest.spyOn(kubectl, "getBundledPath").mockReturnValue("/invalid/path/kubectl");
|
jest.spyOn(kubectl, "getBundledPath").mockReturnValue("/invalid/path/kubectl");
|
||||||
const kubectlPath = await kubectl.getPath();
|
const kubectlPath = await kubectl.getPath();
|
||||||
let binaryName = "kubectl";
|
let binaryName = "kubectl";
|
||||||
|
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
binaryName += ".exe";
|
binaryName += ".exe";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,6 +31,7 @@ export class LensBinary {
|
|||||||
|
|
||||||
constructor(opts: LensBinaryOpts) {
|
constructor(opts: LensBinaryOpts) {
|
||||||
const baseDir = opts.baseDir;
|
const baseDir = opts.baseDir;
|
||||||
|
|
||||||
this.originalBinaryName = opts.originalBinaryName;
|
this.originalBinaryName = opts.originalBinaryName;
|
||||||
this.binaryName = opts.newBinaryName || opts.originalBinaryName;
|
this.binaryName = opts.newBinaryName || opts.originalBinaryName;
|
||||||
this.binaryVersion = opts.version;
|
this.binaryVersion = opts.version;
|
||||||
@ -50,11 +51,13 @@ export class LensBinary {
|
|||||||
this.arch = arch;
|
this.arch = arch;
|
||||||
this.platformName = isWindows ? "windows" : process.platform;
|
this.platformName = isWindows ? "windows" : process.platform;
|
||||||
this.dirname = path.normalize(path.join(baseDir, this.binaryName));
|
this.dirname = path.normalize(path.join(baseDir, this.binaryName));
|
||||||
|
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
this.binaryName = `${this.binaryName}.exe`;
|
this.binaryName = `${this.binaryName}.exe`;
|
||||||
this.originalBinaryName = `${this.originalBinaryName}.exe`;
|
this.originalBinaryName = `${this.originalBinaryName}.exe`;
|
||||||
}
|
}
|
||||||
const tarName = this.getTarName();
|
const tarName = this.getTarName();
|
||||||
|
|
||||||
if (tarName) {
|
if (tarName) {
|
||||||
this.tarPath = path.join(this.dirname, tarName);
|
this.tarPath = path.join(this.dirname, tarName);
|
||||||
}
|
}
|
||||||
@ -70,6 +73,7 @@ export class LensBinary {
|
|||||||
|
|
||||||
public async binaryPath() {
|
public async binaryPath() {
|
||||||
await this.ensureBinary();
|
await this.ensureBinary();
|
||||||
|
|
||||||
return this.getBinaryPath();
|
return this.getBinaryPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,20 +100,24 @@ export class LensBinary {
|
|||||||
public async binDir() {
|
public async binDir() {
|
||||||
try {
|
try {
|
||||||
await this.ensureBinary();
|
await this.ensureBinary();
|
||||||
|
|
||||||
return this.dirname;
|
return this.dirname;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error(err);
|
this.logger.error(err);
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async checkBinary() {
|
protected async checkBinary() {
|
||||||
const exists = await pathExists(this.getBinaryPath());
|
const exists = await pathExists(this.getBinaryPath());
|
||||||
|
|
||||||
return exists;
|
return exists;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ensureBinary() {
|
public async ensureBinary() {
|
||||||
const isValid = await this.checkBinary();
|
const isValid = await this.checkBinary();
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
await this.downloadBinary().catch((error) => {
|
await this.downloadBinary().catch((error) => {
|
||||||
this.logger.error(error);
|
this.logger.error(error);
|
||||||
@ -148,6 +156,7 @@ export class LensBinary {
|
|||||||
|
|
||||||
protected async downloadBinary() {
|
protected async downloadBinary() {
|
||||||
const binaryPath = this.tarPath || this.getBinaryPath();
|
const binaryPath = this.tarPath || this.getBinaryPath();
|
||||||
|
|
||||||
await ensureDir(this.getBinaryDir(), 0o755);
|
await ensureDir(this.getBinaryDir(), 0o755);
|
||||||
|
|
||||||
const file = fs.createWriteStream(binaryPath);
|
const file = fs.createWriteStream(binaryPath);
|
||||||
@ -159,7 +168,6 @@ export class LensBinary {
|
|||||||
gzip: true,
|
gzip: true,
|
||||||
...this.requestOpts
|
...this.requestOpts
|
||||||
};
|
};
|
||||||
|
|
||||||
const stream = request(requestOpts);
|
const stream = request(requestOpts);
|
||||||
|
|
||||||
stream.on("complete", () => {
|
stream.on("complete", () => {
|
||||||
@ -174,6 +182,7 @@ export class LensBinary {
|
|||||||
});
|
});
|
||||||
throw(error);
|
throw(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
file.on("close", () => {
|
file.on("close", () => {
|
||||||
this.logger.debug(`${this.originalBinaryName} binary download closed`);
|
this.logger.debug(`${this.originalBinaryName} binary download closed`);
|
||||||
|
|||||||
@ -30,6 +30,7 @@ export class LensProxy {
|
|||||||
listen(port = this.port): this {
|
listen(port = this.port): this {
|
||||||
this.proxyServer = this.buildCustomProxy().listen(port);
|
this.proxyServer = this.buildCustomProxy().listen(port);
|
||||||
logger.info(`LensProxy server has started at ${this.origin}`);
|
logger.info(`LensProxy server has started at ${this.origin}`);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,6 +50,7 @@ export class LensProxy {
|
|||||||
}, (req: http.IncomingMessage, res: http.ServerResponse) => {
|
}, (req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||||
this.handleRequest(proxy, req, res);
|
this.handleRequest(proxy, req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
spdyProxy.on("upgrade", (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => {
|
spdyProxy.on("upgrade", (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => {
|
||||||
if (req.url.startsWith(`${apiPrefix}?`)) {
|
if (req.url.startsWith(`${apiPrefix}?`)) {
|
||||||
this.handleWsUpgrade(req, socket, head);
|
this.handleWsUpgrade(req, socket, head);
|
||||||
@ -59,22 +61,27 @@ export class LensProxy {
|
|||||||
spdyProxy.on("error", (err) => {
|
spdyProxy.on("error", (err) => {
|
||||||
logger.error("proxy error", err);
|
logger.error("proxy error", err);
|
||||||
});
|
});
|
||||||
|
|
||||||
return spdyProxy;
|
return spdyProxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async handleProxyUpgrade(proxy: httpProxy, req: http.IncomingMessage, socket: net.Socket, head: Buffer) {
|
protected async handleProxyUpgrade(proxy: httpProxy, req: http.IncomingMessage, socket: net.Socket, head: Buffer) {
|
||||||
const cluster = this.clusterManager.getClusterForRequest(req);
|
const cluster = this.clusterManager.getClusterForRequest(req);
|
||||||
|
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "");
|
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "");
|
||||||
const apiUrl = url.parse(cluster.apiUrl);
|
const apiUrl = url.parse(cluster.apiUrl);
|
||||||
const pUrl = url.parse(proxyUrl);
|
const pUrl = url.parse(proxyUrl);
|
||||||
const connectOpts = { port: parseInt(pUrl.port), host: pUrl.hostname };
|
const connectOpts = { port: parseInt(pUrl.port), host: pUrl.hostname };
|
||||||
const proxySocket = new net.Socket();
|
const proxySocket = new net.Socket();
|
||||||
|
|
||||||
proxySocket.connect(connectOpts, () => {
|
proxySocket.connect(connectOpts, () => {
|
||||||
proxySocket.write(`${req.method} ${pUrl.path} HTTP/1.1\r\n`);
|
proxySocket.write(`${req.method} ${pUrl.path} HTTP/1.1\r\n`);
|
||||||
proxySocket.write(`Host: ${apiUrl.host}\r\n`);
|
proxySocket.write(`Host: ${apiUrl.host}\r\n`);
|
||||||
|
|
||||||
for (let i = 0; i < req.rawHeaders.length; i += 2) {
|
for (let i = 0; i < req.rawHeaders.length; i += 2) {
|
||||||
const key = req.rawHeaders[i];
|
const key = req.rawHeaders[i];
|
||||||
|
|
||||||
if (key !== "Host" && key !== "Authorization") {
|
if (key !== "Host" && key !== "Authorization") {
|
||||||
proxySocket.write(`${req.rawHeaders[i]}: ${req.rawHeaders[i+1]}\r\n`);
|
proxySocket.write(`${req.rawHeaders[i]}: ${req.rawHeaders[i+1]}\r\n`);
|
||||||
}
|
}
|
||||||
@ -112,16 +119,20 @@ export class LensProxy {
|
|||||||
|
|
||||||
protected createProxy(): httpProxy {
|
protected createProxy(): httpProxy {
|
||||||
const proxy = httpProxy.createProxyServer();
|
const proxy = httpProxy.createProxyServer();
|
||||||
|
|
||||||
proxy.on("error", (error, req, res, target) => {
|
proxy.on("error", (error, req, res, target) => {
|
||||||
if (this.closed) {
|
if (this.closed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (target) {
|
if (target) {
|
||||||
logger.debug(`Failed proxy to target: ${JSON.stringify(target, null, 2)}`);
|
logger.debug(`Failed proxy to target: ${JSON.stringify(target, null, 2)}`);
|
||||||
|
|
||||||
if (req.method === "GET" && (!res.statusCode || res.statusCode >= 500)) {
|
if (req.method === "GET" && (!res.statusCode || res.statusCode >= 500)) {
|
||||||
const reqId = this.getRequestId(req);
|
const reqId = this.getRequestId(req);
|
||||||
const retryCount = this.retryCounters.get(reqId) || 0;
|
const retryCount = this.retryCounters.get(reqId) || 0;
|
||||||
const timeoutMs = retryCount * 250;
|
const timeoutMs = retryCount * 250;
|
||||||
|
|
||||||
if (retryCount < 20) {
|
if (retryCount < 20) {
|
||||||
logger.debug(`Retrying proxy request to url: ${reqId}`);
|
logger.debug(`Retrying proxy request to url: ${reqId}`);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -131,6 +142,7 @@ export class LensProxy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
res.writeHead(500).end("Oops, something went wrong.");
|
res.writeHead(500).end("Oops, something went wrong.");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -143,9 +155,11 @@ export class LensProxy {
|
|||||||
|
|
||||||
protected createWsListener(): WebSocket.Server {
|
protected createWsListener(): WebSocket.Server {
|
||||||
const ws = new WebSocket.Server({ noServer: true });
|
const ws = new WebSocket.Server({ noServer: true });
|
||||||
|
|
||||||
return ws.on("connection", ((socket: WebSocket, req: http.IncomingMessage) => {
|
return ws.on("connection", ((socket: WebSocket, req: http.IncomingMessage) => {
|
||||||
const cluster = this.clusterManager.getClusterForRequest(req);
|
const cluster = this.clusterManager.getClusterForRequest(req);
|
||||||
const nodeParam = url.parse(req.url, true).query["node"]?.toString();
|
const nodeParam = url.parse(req.url, true).query["node"]?.toString();
|
||||||
|
|
||||||
openShell(socket, cluster, nodeParam);
|
openShell(socket, cluster, nodeParam);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@ -155,6 +169,7 @@ export class LensProxy {
|
|||||||
delete req.headers.authorization;
|
delete req.headers.authorization;
|
||||||
req.url = req.url.replace(apiKubePrefix, "");
|
req.url = req.url.replace(apiKubePrefix, "");
|
||||||
const isWatchRequest = req.url.includes("watch=");
|
const isWatchRequest = req.url.includes("watch=");
|
||||||
|
|
||||||
return await contextHandler.getApiTarget(isWatchRequest);
|
return await contextHandler.getApiTarget(isWatchRequest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -165,11 +180,14 @@ export class LensProxy {
|
|||||||
|
|
||||||
protected async handleRequest(proxy: httpProxy, req: http.IncomingMessage, res: http.ServerResponse) {
|
protected async handleRequest(proxy: httpProxy, req: http.IncomingMessage, res: http.ServerResponse) {
|
||||||
const cluster = this.clusterManager.getClusterForRequest(req);
|
const cluster = this.clusterManager.getClusterForRequest(req);
|
||||||
|
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler);
|
const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler);
|
||||||
|
|
||||||
if (proxyTarget) {
|
if (proxyTarget) {
|
||||||
// allow to fetch apis in "clusterId.localhost:port" from "localhost:port"
|
// allow to fetch apis in "clusterId.localhost:port" from "localhost:port"
|
||||||
res.setHeader("Access-Control-Allow-Origin", this.origin);
|
res.setHeader("Access-Control-Allow-Origin", this.origin);
|
||||||
|
|
||||||
return proxy.web(req, res, proxyTarget);
|
return proxy.web(req, res, proxyTarget);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -178,6 +196,7 @@ export class LensProxy {
|
|||||||
|
|
||||||
protected async handleWsUpgrade(req: http.IncomingMessage, socket: net.Socket, head: Buffer) {
|
protected async handleWsUpgrade(req: http.IncomingMessage, socket: net.Socket, head: Buffer) {
|
||||||
const wsServer = this.createWsListener();
|
const wsServer = this.createWsListener();
|
||||||
|
|
||||||
wsServer.handleUpgrade(req, socket, head, (con) => {
|
wsServer.handleUpgrade(req, socket, head, (con) => {
|
||||||
wsServer.emit("connection", con, req);
|
wsServer.emit("connection", con, req);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,12 +3,10 @@ import winston from "winston";
|
|||||||
import { isDebugging } from "../common/vars";
|
import { isDebugging } from "../common/vars";
|
||||||
|
|
||||||
const logLevel = process.env.LOG_LEVEL ? process.env.LOG_LEVEL : isDebugging ? "debug" : "info";
|
const logLevel = process.env.LOG_LEVEL ? process.env.LOG_LEVEL : isDebugging ? "debug" : "info";
|
||||||
|
|
||||||
const consoleOptions: winston.transports.ConsoleTransportOptions = {
|
const consoleOptions: winston.transports.ConsoleTransportOptions = {
|
||||||
handleExceptions: false,
|
handleExceptions: false,
|
||||||
level: logLevel,
|
level: logLevel,
|
||||||
};
|
};
|
||||||
|
|
||||||
const fileOptions: winston.transports.FileTransportOptions = {
|
const fileOptions: winston.transports.FileTransportOptions = {
|
||||||
handleExceptions: false,
|
handleExceptions: false,
|
||||||
level: logLevel,
|
level: logLevel,
|
||||||
@ -18,7 +16,6 @@ const fileOptions: winston.transports.FileTransportOptions = {
|
|||||||
maxFiles: 16,
|
maxFiles: 16,
|
||||||
tailable: true,
|
tailable: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const logger = winston.createLogger({
|
const logger = winston.createLogger({
|
||||||
format: winston.format.combine(
|
format: winston.format.combine(
|
||||||
winston.format.colorize(),
|
winston.format.colorize(),
|
||||||
|
|||||||
@ -27,6 +27,7 @@ export function showAbout(browserWindow: BrowserWindow) {
|
|||||||
`Node: ${process.versions.node}`,
|
`Node: ${process.versions.node}`,
|
||||||
`Copyright 2020 Mirantis, Inc.`,
|
`Copyright 2020 Mirantis, Inc.`,
|
||||||
];
|
];
|
||||||
|
|
||||||
dialog.showMessageBoxSync(browserWindow, {
|
dialog.showMessageBoxSync(browserWindow, {
|
||||||
title: `${isWindows ? " ".repeat(2) : ""}${appName}`,
|
title: `${isWindows ? " ".repeat(2) : ""}${appName}`,
|
||||||
type: "info",
|
type: "info",
|
||||||
@ -39,6 +40,7 @@ export function showAbout(browserWindow: BrowserWindow) {
|
|||||||
export function buildMenu(windowManager: WindowManager) {
|
export function buildMenu(windowManager: WindowManager) {
|
||||||
function ignoreOnMac(menuItems: MenuItemConstructorOptions[]) {
|
function ignoreOnMac(menuItems: MenuItemConstructorOptions[]) {
|
||||||
if (isMac) return [];
|
if (isMac) return [];
|
||||||
|
|
||||||
return menuItems;
|
return menuItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,6 +50,7 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
item.enabled = false;
|
item.enabled = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return menuItems;
|
return menuItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,7 +99,6 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
const fileMenu: MenuItemConstructorOptions = {
|
const fileMenu: MenuItemConstructorOptions = {
|
||||||
label: "File",
|
label: "File",
|
||||||
submenu: [
|
submenu: [
|
||||||
@ -154,7 +156,6 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
])
|
])
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
const editMenu: MenuItemConstructorOptions = {
|
const editMenu: MenuItemConstructorOptions = {
|
||||||
label: "Edit",
|
label: "Edit",
|
||||||
submenu: [
|
submenu: [
|
||||||
@ -169,7 +170,6 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
{ role: "selectAll" },
|
{ role: "selectAll" },
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
const viewMenu: MenuItemConstructorOptions = {
|
const viewMenu: MenuItemConstructorOptions = {
|
||||||
label: "View",
|
label: "View",
|
||||||
submenu: [
|
submenu: [
|
||||||
@ -203,7 +203,6 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
{ role: "togglefullscreen" }
|
{ role: "togglefullscreen" }
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
const helpMenu: MenuItemConstructorOptions = {
|
const helpMenu: MenuItemConstructorOptions = {
|
||||||
role: "help",
|
role: "help",
|
||||||
submenu: [
|
submenu: [
|
||||||
@ -235,7 +234,6 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
])
|
])
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
// Prepare menu items order
|
// Prepare menu items order
|
||||||
const appMenu: Record<MenuTopId, MenuItemConstructorOptions> = {
|
const appMenu: Record<MenuTopId, MenuItemConstructorOptions> = {
|
||||||
mac: macAppMenu,
|
mac: macAppMenu,
|
||||||
@ -249,6 +247,7 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
menuRegistry.getItems().forEach(({ parentId, ...menuItem }) => {
|
menuRegistry.getItems().forEach(({ parentId, ...menuItem }) => {
|
||||||
try {
|
try {
|
||||||
const topMenu = appMenu[parentId as MenuTopId].submenu as MenuItemConstructorOptions[];
|
const topMenu = appMenu[parentId as MenuTopId].submenu as MenuItemConstructorOptions[];
|
||||||
|
|
||||||
topMenu.push(menuItem);
|
topMenu.push(menuItem);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`[MENU]: can't register menu item, parentId=${parentId}`, { menuItem });
|
logger.error(`[MENU]: can't register menu item, parentId=${parentId}`, { menuItem });
|
||||||
@ -260,6 +259,7 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const menu = Menu.buildFromTemplate(Object.values(appMenu));
|
const menu = Menu.buildFromTemplate(Object.values(appMenu));
|
||||||
|
|
||||||
Menu.setApplicationMenu(menu);
|
Menu.setApplicationMenu(menu);
|
||||||
|
|
||||||
if (isTestEnv) {
|
if (isTestEnv) {
|
||||||
@ -273,6 +273,7 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
for (const name of names) {
|
for (const name of names) {
|
||||||
parentLabels.push(name);
|
parentLabels.push(name);
|
||||||
menuItem = menu?.items?.find(item => item.label === name);
|
menuItem = menu?.items?.find(item => item.label === name);
|
||||||
|
|
||||||
if (!menuItem) {
|
if (!menuItem) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -280,14 +281,18 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const menuPath: string = parentLabels.join(" -> ");
|
const menuPath: string = parentLabels.join(" -> ");
|
||||||
|
|
||||||
if (!menuItem) {
|
if (!menuItem) {
|
||||||
logger.info(`[MENU:test-menu-item-click] Cannot find menu item ${menuPath}`);
|
logger.info(`[MENU:test-menu-item-click] Cannot find menu item ${menuPath}`);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { enabled, visible, click } = menuItem;
|
const { enabled, visible, click } = menuItem;
|
||||||
|
|
||||||
if (enabled === false || visible === false || typeof click !== "function") {
|
if (enabled === false || visible === false || typeof click !== "function") {
|
||||||
logger.info(`[MENU:test-menu-item-click] Menu item ${menuPath} not clickable`);
|
logger.info(`[MENU:test-menu-item-click] Menu item ${menuPath} not clickable`);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -23,6 +23,7 @@ export class NodeShellSession extends ShellSession {
|
|||||||
public async open() {
|
public async open() {
|
||||||
const shell = await this.kubectl.getPath();
|
const shell = await this.kubectl.getPath();
|
||||||
let args = [];
|
let args = [];
|
||||||
|
|
||||||
if (this.createNodeShellPod(this.podId, this.nodeName)) {
|
if (this.createNodeShellPod(this.podId, this.nodeName)) {
|
||||||
await this.waitForRunningPod(this.podId).catch(() => {
|
await this.waitForRunningPod(this.podId).catch(() => {
|
||||||
this.exit(1001);
|
this.exit(1001);
|
||||||
@ -31,6 +32,7 @@ export class NodeShellSession extends ShellSession {
|
|||||||
args = ["exec", "-i", "-t", "-n", "kube-system", this.podId, "--", "sh", "-c", "((clear && bash) || (clear && ash) || (clear && sh))"];
|
args = ["exec", "-i", "-t", "-n", "kube-system", this.podId, "--", "sh", "-c", "((clear && bash) || (clear && ash) || (clear && sh))"];
|
||||||
|
|
||||||
const shellEnv = await this.getCachedShellEnv();
|
const shellEnv = await this.getCachedShellEnv();
|
||||||
|
|
||||||
this.shellProcess = pty.spawn(shell, args, {
|
this.shellProcess = pty.spawn(shell, args, {
|
||||||
cols: 80,
|
cols: 80,
|
||||||
cwd: this.cwd() || shellEnv["HOME"],
|
cwd: this.cwd() || shellEnv["HOME"],
|
||||||
@ -85,10 +87,13 @@ export class NodeShellSession extends ShellSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} as k8s.V1Pod;
|
} as k8s.V1Pod;
|
||||||
|
|
||||||
await k8sApi.createNamespacedPod("kube-system", pod).catch((error) => {
|
await k8sApi.createNamespacedPod("kube-system", pod).catch((error) => {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,6 +103,7 @@ export class NodeShellSession extends ShellSession {
|
|||||||
}
|
}
|
||||||
this.kc = new k8s.KubeConfig();
|
this.kc = new k8s.KubeConfig();
|
||||||
this.kc.loadFromFile(this.kubeconfigPath);
|
this.kc.loadFromFile(this.kubeconfigPath);
|
||||||
|
|
||||||
return this.kc;
|
return this.kc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,7 +111,6 @@ export class NodeShellSession extends ShellSession {
|
|||||||
return new Promise<boolean>(async (resolve, reject) => {
|
return new Promise<boolean>(async (resolve, reject) => {
|
||||||
const kc = this.getKubeConfig();
|
const kc = this.getKubeConfig();
|
||||||
const watch = new k8s.Watch(kc);
|
const watch = new k8s.Watch(kc);
|
||||||
|
|
||||||
const req = await watch.watch(`/api/v1/namespaces/kube-system/pods`, {},
|
const req = await watch.watch(`/api/v1/namespaces/kube-system/pods`, {},
|
||||||
// callback is called for each received object.
|
// callback is called for each received object.
|
||||||
(type, obj) => {
|
(type, obj) => {
|
||||||
@ -119,6 +124,7 @@ export class NodeShellSession extends ShellSession {
|
|||||||
reject(false);
|
reject(false);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
req.abort();
|
req.abort();
|
||||||
reject(false);
|
reject(false);
|
||||||
@ -129,17 +135,20 @@ export class NodeShellSession extends ShellSession {
|
|||||||
protected deleteNodeShellPod() {
|
protected deleteNodeShellPod() {
|
||||||
const kc = this.getKubeConfig();
|
const kc = this.getKubeConfig();
|
||||||
const k8sApi = kc.makeApiClient(k8s.CoreV1Api);
|
const k8sApi = kc.makeApiClient(k8s.CoreV1Api);
|
||||||
|
|
||||||
k8sApi.deleteNamespacedPod(this.podId, "kube-system");
|
k8sApi.deleteNamespacedPod(this.podId, "kube-system");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openShell(socket: WebSocket, cluster: Cluster, nodeName?: string): Promise<ShellSession> {
|
export async function openShell(socket: WebSocket, cluster: Cluster, nodeName?: string): Promise<ShellSession> {
|
||||||
let shell: ShellSession;
|
let shell: ShellSession;
|
||||||
|
|
||||||
if (nodeName) {
|
if (nodeName) {
|
||||||
shell = new NodeShellSession(socket, cluster, nodeName);
|
shell = new NodeShellSession(socket, cluster, nodeName);
|
||||||
} else {
|
} else {
|
||||||
shell = new ShellSession(socket, cluster);
|
shell = new ShellSession(socket, cluster);
|
||||||
}
|
}
|
||||||
shell.open();
|
shell.open();
|
||||||
|
|
||||||
return shell;
|
return shell;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,11 +5,14 @@ import logger from "./logger";
|
|||||||
|
|
||||||
export async function getFreePort(): Promise<number> {
|
export async function getFreePort(): Promise<number> {
|
||||||
logger.debug("Lookup new free port..");
|
logger.debug("Lookup new free port..");
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const server = net.createServer();
|
const server = net.createServer();
|
||||||
|
|
||||||
server.unref();
|
server.unref();
|
||||||
server.on("listening", () => {
|
server.on("listening", () => {
|
||||||
const port = (server.address() as AddressInfo).port;
|
const port = (server.address() as AddressInfo).port;
|
||||||
|
|
||||||
server.close(() => resolve(port));
|
server.close(() => resolve(port));
|
||||||
logger.debug(`New port found: ${port}`);
|
logger.debug(`New port found: ${port}`);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -9,10 +9,12 @@ jest.mock("net", () => {
|
|||||||
return new class MockServer extends EventEmitter {
|
return new class MockServer extends EventEmitter {
|
||||||
listen = jest.fn(() => {
|
listen = jest.fn(() => {
|
||||||
this.emit("listening");
|
this.emit("listening");
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
});
|
});
|
||||||
address = () => {
|
address = () => {
|
||||||
newPort = Math.round(Math.random() * 10000);
|
newPort = Math.round(Math.random() * 10000);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
port: newPort
|
port: newPort
|
||||||
};
|
};
|
||||||
|
|||||||
@ -10,9 +10,11 @@ export class PrometheusHelm extends PrometheusLens {
|
|||||||
|
|
||||||
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService> {
|
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService> {
|
||||||
const labelSelector = "app=prometheus,component=server,heritage=Helm";
|
const labelSelector = "app=prometheus,component=server,heritage=Helm";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const serviceList = await client.listServiceForAllNamespaces(false, "", null, labelSelector);
|
const serviceList = await client.listServiceForAllNamespaces(false, "", null, labelSelector);
|
||||||
const service = serviceList.body.items[0];
|
const service = serviceList.body.items[0];
|
||||||
|
|
||||||
if (!service) return;
|
if (!service) return;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -23,6 +25,7 @@ export class PrometheusHelm extends PrometheusLens {
|
|||||||
};
|
};
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
logger.warn(`PrometheusHelm: failed to list services: ${error.toString()}`);
|
logger.warn(`PrometheusHelm: failed to list services: ${error.toString()}`);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ export class PrometheusLens implements PrometheusProvider {
|
|||||||
try {
|
try {
|
||||||
const resp = await client.readNamespacedService("prometheus", "lens-metrics");
|
const resp = await client.readNamespacedService("prometheus", "lens-metrics");
|
||||||
const service = resp.body;
|
const service = resp.body;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
namespace: service.metadata.namespace,
|
namespace: service.metadata.namespace,
|
||||||
@ -72,6 +73,7 @@ export class PrometheusLens implements PrometheusProvider {
|
|||||||
case "ingress":
|
case "ingress":
|
||||||
const bytesSent = (ingress: string, statuses: string) =>
|
const bytesSent = (ingress: string, statuses: string) =>
|
||||||
`sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}", status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress)`;
|
`sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}", status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress)`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
bytesSentSuccess: bytesSent(opts.igress, "^2\\\\d*"),
|
bytesSentSuccess: bytesSent(opts.igress, "^2\\\\d*"),
|
||||||
bytesSentFailure: bytesSent(opts.ingres, "^5\\\\d*"),
|
bytesSentFailure: bytesSent(opts.ingres, "^5\\\\d*"),
|
||||||
|
|||||||
@ -10,9 +10,11 @@ export class PrometheusOperator implements PrometheusProvider {
|
|||||||
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService> {
|
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService> {
|
||||||
try {
|
try {
|
||||||
let service: V1Service;
|
let service: V1Service;
|
||||||
|
|
||||||
for (const labelSelector of ["operated-prometheus=true", "self-monitor=true"]) {
|
for (const labelSelector of ["operated-prometheus=true", "self-monitor=true"]) {
|
||||||
if (!service) {
|
if (!service) {
|
||||||
const serviceList = await client.listServiceForAllNamespaces(null, null, null, labelSelector);
|
const serviceList = await client.listServiceForAllNamespaces(null, null, null, labelSelector);
|
||||||
|
|
||||||
service = serviceList.body.items[0];
|
service = serviceList.body.items[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -26,6 +28,7 @@ export class PrometheusOperator implements PrometheusProvider {
|
|||||||
};
|
};
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
logger.warn(`PrometheusOperator: failed to list services: ${error.toString()}`);
|
logger.warn(`PrometheusOperator: failed to list services: ${error.toString()}`);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -80,6 +83,7 @@ export class PrometheusOperator implements PrometheusProvider {
|
|||||||
case "ingress":
|
case "ingress":
|
||||||
const bytesSent = (ingress: string, statuses: string) =>
|
const bytesSent = (ingress: string, statuses: string) =>
|
||||||
`sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}", status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress)`;
|
`sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}", status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress)`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
bytesSentSuccess: bytesSent(opts.igress, "^2\\\\d*"),
|
bytesSentSuccess: bytesSent(opts.igress, "^2\\\\d*"),
|
||||||
bytesSentFailure: bytesSent(opts.ingres, "^5\\\\d*"),
|
bytesSentFailure: bytesSent(opts.ingres, "^5\\\\d*"),
|
||||||
|
|||||||
@ -77,6 +77,7 @@ export class PrometheusProviderRegistry {
|
|||||||
if (!this.prometheusProviders[type]) {
|
if (!this.prometheusProviders[type]) {
|
||||||
throw "Unknown Prometheus provider";
|
throw "Unknown Prometheus provider";
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.prometheusProviders[type];
|
return this.prometheusProviders[type];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@ export class PrometheusStacklight implements PrometheusProvider {
|
|||||||
try {
|
try {
|
||||||
const resp = await client.readNamespacedService("prometheus-server", "stacklight");
|
const resp = await client.readNamespacedService("prometheus-server", "stacklight");
|
||||||
const service = resp.body;
|
const service = resp.body;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
namespace: service.metadata.namespace,
|
namespace: service.metadata.namespace,
|
||||||
@ -72,6 +73,7 @@ export class PrometheusStacklight implements PrometheusProvider {
|
|||||||
case "ingress":
|
case "ingress":
|
||||||
const bytesSent = (ingress: string, statuses: string) =>
|
const bytesSent = (ingress: string, statuses: string) =>
|
||||||
`sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}", status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress)`;
|
`sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}", status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress)`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
bytesSentSuccess: bytesSent(opts.igress, "^2\\\\d*"),
|
bytesSentSuccess: bytesSent(opts.igress, "^2\\\\d*"),
|
||||||
bytesSentFailure: bytesSent(opts.ingres, "^5\\\\d*"),
|
bytesSentFailure: bytesSent(opts.ingres, "^5\\\\d*"),
|
||||||
|
|||||||
@ -16,19 +16,24 @@ export class ResourceApplier {
|
|||||||
async apply(resource: KubernetesObject | any): Promise<string> {
|
async apply(resource: KubernetesObject | any): Promise<string> {
|
||||||
resource = this.sanitizeObject(resource);
|
resource = this.sanitizeObject(resource);
|
||||||
appEventBus.emit({name: "resource", action: "apply"});
|
appEventBus.emit({name: "resource", action: "apply"});
|
||||||
|
|
||||||
return await this.kubectlApply(yaml.safeDump(resource));
|
return await this.kubectlApply(yaml.safeDump(resource));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async kubectlApply(content: string): Promise<string> {
|
protected async kubectlApply(content: string): Promise<string> {
|
||||||
const { kubeCtl } = this.cluster;
|
const { kubeCtl } = this.cluster;
|
||||||
const kubectlPath = await kubeCtl.getPath();
|
const kubectlPath = await kubeCtl.getPath();
|
||||||
|
|
||||||
return new Promise<string>((resolve, reject) => {
|
return new Promise<string>((resolve, reject) => {
|
||||||
const fileName = tempy.file({ name: "resource.yaml" });
|
const fileName = tempy.file({ name: "resource.yaml" });
|
||||||
|
|
||||||
fs.writeFileSync(fileName, content);
|
fs.writeFileSync(fileName, content);
|
||||||
const cmd = `"${kubectlPath}" apply --kubeconfig "${this.cluster.getProxyKubeconfigPath()}" -o json -f "${fileName}"`;
|
const cmd = `"${kubectlPath}" apply --kubeconfig "${this.cluster.getProxyKubeconfigPath()}" -o json -f "${fileName}"`;
|
||||||
|
|
||||||
logger.debug(`shooting manifests with: ${cmd}`);
|
logger.debug(`shooting manifests with: ${cmd}`);
|
||||||
const execEnv: NodeJS.ProcessEnv = Object.assign({}, process.env);
|
const execEnv: NodeJS.ProcessEnv = Object.assign({}, process.env);
|
||||||
const httpsProxy = this.cluster.preferences?.httpsProxy;
|
const httpsProxy = this.cluster.preferences?.httpsProxy;
|
||||||
|
|
||||||
if (httpsProxy) {
|
if (httpsProxy) {
|
||||||
execEnv["HTTPS_PROXY"] = httpsProxy;
|
execEnv["HTTPS_PROXY"] = httpsProxy;
|
||||||
}
|
}
|
||||||
@ -37,6 +42,7 @@ export class ResourceApplier {
|
|||||||
if (stderr != "") {
|
if (stderr != "") {
|
||||||
fs.unlinkSync(fileName);
|
fs.unlinkSync(fileName);
|
||||||
reject(stderr);
|
reject(stderr);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fs.unlinkSync(fileName);
|
fs.unlinkSync(fileName);
|
||||||
@ -48,20 +54,25 @@ export class ResourceApplier {
|
|||||||
public async kubectlApplyAll(resources: string[]): Promise<string> {
|
public async kubectlApplyAll(resources: string[]): Promise<string> {
|
||||||
const { kubeCtl } = this.cluster;
|
const { kubeCtl } = this.cluster;
|
||||||
const kubectlPath = await kubeCtl.getPath();
|
const kubectlPath = await kubeCtl.getPath();
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const tmpDir = tempy.directory();
|
const tmpDir = tempy.directory();
|
||||||
|
|
||||||
// Dump each resource into tmpDir
|
// Dump each resource into tmpDir
|
||||||
resources.forEach((resource, index) => {
|
resources.forEach((resource, index) => {
|
||||||
fs.writeFileSync(path.join(tmpDir, `${index}.yaml`), resource);
|
fs.writeFileSync(path.join(tmpDir, `${index}.yaml`), resource);
|
||||||
});
|
});
|
||||||
const cmd = `"${kubectlPath}" apply --kubeconfig "${this.cluster.getProxyKubeconfigPath()}" -o json -f "${tmpDir}"`;
|
const cmd = `"${kubectlPath}" apply --kubeconfig "${this.cluster.getProxyKubeconfigPath()}" -o json -f "${tmpDir}"`;
|
||||||
|
|
||||||
console.log("shooting manifests with:", cmd);
|
console.log("shooting manifests with:", cmd);
|
||||||
exec(cmd, (error, stdout, stderr) => {
|
exec(cmd, (error, stdout, stderr) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(`Error applying manifests:${error}`);
|
reject(`Error applying manifests:${error}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stderr != "") {
|
if (stderr != "") {
|
||||||
reject(stderr);
|
reject(stderr);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
resolve(stdout);
|
resolve(stdout);
|
||||||
@ -74,9 +85,11 @@ export class ResourceApplier {
|
|||||||
delete resource.status;
|
delete resource.status;
|
||||||
delete resource.metadata?.resourceVersion;
|
delete resource.metadata?.resourceVersion;
|
||||||
const annotations = resource.metadata?.annotations;
|
const annotations = resource.metadata?.annotations;
|
||||||
|
|
||||||
if (annotations) {
|
if (annotations) {
|
||||||
delete annotations["kubectl.kubernetes.io/last-applied-configuration"];
|
delete annotations["kubectl.kubernetes.io/last-applied-configuration"];
|
||||||
}
|
}
|
||||||
|
|
||||||
return resource;
|
return resource;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user