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

Merge remote-tracking branch 'origin/master' into feature/tray

# Conflicts:
#	locales/en/messages.po
#	locales/fi/messages.po
#	locales/ru/messages.po
This commit is contained in:
Roman 2020-10-15 15:26:53 +03:00
commit b0d192309a
20 changed files with 89 additions and 67 deletions

View File

@ -0,0 +1,9 @@
// Debouncing promise evaluation
export function debouncePromise<T, F extends any[]>(func: (...args: F) => T | Promise<T>, timeout = 0): (...args: F) => Promise<T> {
let timer: NodeJS.Timeout;
return (...params: any[]) => new Promise((resolve, reject) => {
clearTimeout(timer);
timer = setTimeout(() => resolve(func.apply(this, params)), timeout);
});
}

View File

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

View File

@ -32,7 +32,7 @@ import { KubeAuthProxy } from "../kube-auth-proxy"
import { getFreePort } from "../port" import { getFreePort } from "../port"
import { broadcastIpc } from "../../common/ipc" import { broadcastIpc } from "../../common/ipc"
import { ChildProcess, spawn, SpawnOptions } from "child_process" import { ChildProcess, spawn, SpawnOptions } from "child_process"
import { Kubectl } from "../kubectl" import { bundledKubectlPath, Kubectl } from "../kubectl"
import { mock, MockProxy } from 'jest-mock-extended'; import { mock, MockProxy } from 'jest-mock-extended';
import { waitUntilUsed } from 'tcp-port-used'; import { waitUntilUsed } from 'tcp-port-used';
import { Readable } from "stream" import { Readable } from "stream"
@ -81,7 +81,7 @@ describe("kube auth proxy tests", () => {
return mockedCP.stdout return mockedCP.stdout
}) })
mockSpawn.mockImplementationOnce((command: string, args: readonly string[], options: SpawnOptions): ChildProcess => { mockSpawn.mockImplementationOnce((command: string, args: readonly string[], options: SpawnOptions): ChildProcess => {
expect(command).toBe(Kubectl.bundledKubectlPath) expect(command).toBe(bundledKubectlPath())
return mockedCP return mockedCP
}) })
mockWaitUntilUsed.mockReturnValueOnce(Promise.resolve()) mockWaitUntilUsed.mockReturnValueOnce(Promise.resolve())

View File

@ -2,7 +2,7 @@ import { ChildProcess, spawn } from "child_process"
import { waitUntilUsed } from "tcp-port-used"; import { waitUntilUsed } from "tcp-port-used";
import { broadcastIpc } from "../common/ipc"; import { broadcastIpc } from "../common/ipc";
import type { Cluster } from "./cluster" import type { Cluster } from "./cluster"
import { bundledKubectl, Kubectl } from "./kubectl" import { Kubectl } from "./kubectl"
import logger from "./logger" import logger from "./logger"
export interface KubeAuthProxyLog { export interface KubeAuthProxyLog {
@ -23,7 +23,7 @@ export class KubeAuthProxy {
this.env = env this.env = env
this.port = port this.port = port
this.cluster = cluster this.cluster = cluster
this.kubectl = bundledKubectl this.kubectl = Kubectl.bundled()
} }
public async run(): Promise<void> { public async run(): Promise<void> {

View File

@ -36,15 +36,21 @@ const packageMirrors: Map<string, string> = new Map([
let bundledPath: string let bundledPath: string
const initScriptVersionString = "# lens-initscript v3\n" const initScriptVersionString = "# lens-initscript v3\n"
if (isDevelopment || isTestEnv) { export function bundledKubectlPath(): string {
if (bundledPath) { return bundledPath }
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")
} }
if (isWindows) { if (isWindows) {
bundledPath = `${bundledPath}.exe` bundledPath = `${bundledPath}.exe`
}
return bundledPath
} }
export class Kubectl { export class Kubectl {
@ -58,7 +64,6 @@ export class Kubectl {
return path.join((app || remote.app).getPath("userData"), "binaries", "kubectl") return path.join((app || remote.app).getPath("userData"), "binaries", "kubectl")
} }
public static readonly bundledKubectlPath = bundledPath
public static readonly bundledKubectlVersion: string = bundledVersion public static readonly bundledKubectlVersion: string = bundledVersion
public static invalidBundle = false public static invalidBundle = false
private static bundledInstance: Kubectl; private static bundledInstance: Kubectl;
@ -102,7 +107,7 @@ export class Kubectl {
} }
public getBundledPath() { public getBundledPath() {
return Kubectl.bundledKubectlPath return bundledKubectlPath()
} }
public getPathFromPreferences() { public getPathFromPreferences() {
@ -125,19 +130,19 @@ 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(bundledPath) 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 Kubectl.bundledKubectlPath 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 Kubectl.bundledKubectlPath return this.getBundledPath()
} }
} }
@ -183,7 +188,7 @@ export class Kubectl {
try { try {
const exist = await pathExists(this.path) const exist = await pathExists(this.path)
if (!exist) { if (!exist) {
await fs.promises.copyFile(Kubectl.bundledKubectlPath, 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
@ -332,6 +337,3 @@ export class Kubectl {
return packageMirrors.get("default") // MacOS packages are only available from default return packageMirrors.get("default") // MacOS packages are only available from default
} }
} }
const bundledKubectl = Kubectl.bundled()
export { bundledKubectl }

View File

@ -1,20 +1,20 @@
import packageInfo from "../../package.json" import packageInfo from "../../package.json"
import path from "path" import path from "path"
import { bundledKubectl, Kubectl } from "../../src/main/kubectl"; import { Kubectl } from "../../src/main/kubectl";
import { isWindows } from "../common/vars"; import { isWindows } from "../common/vars";
jest.mock("../common/user-store"); 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(bundledKubectl.kubectlVersion) const kubectl = new Kubectl(Kubectl.bundled().kubectlVersion)
expect(kubectl.kubectlVersion).toBe(bundledKubectl.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(bundledKubectl.kubectlVersion) expect(kubectl.kubectlVersion).toBe(Kubectl.bundled().kubectlVersion)
}) })
}) })

View File

@ -1,7 +1,7 @@
import { LensApiRequest } from "../router" import { LensApiRequest } from "../router"
import { LensApi } from "../lens-api" import { LensApi } from "../lens-api"
import { spawn, ChildProcessWithoutNullStreams } from "child_process" import { spawn, ChildProcessWithoutNullStreams } from "child_process"
import { bundledKubectl } from "../kubectl" import { Kubectl } from "../kubectl"
import { getFreePort } from "../port" import { getFreePort } from "../port"
import { shell } from "electron" import { shell } from "electron"
import * as tcpPortUsed from "tcp-port-used" import * as tcpPortUsed from "tcp-port-used"
@ -37,7 +37,7 @@ class PortForward {
public async start() { public async start() {
this.localPort = await getFreePort() this.localPort = await getFreePort()
const kubectlBin = await bundledKubectl.getPath() const kubectlBin = await Kubectl.bundled().getPath()
const args = [ const args = [
"--kubeconfig", this.kubeConfig, "--kubeconfig", this.kubeConfig,
"port-forward", "port-forward",

View File

@ -38,7 +38,7 @@ export class ShellSession extends EventEmitter {
public async open() { public async open() {
this.kubectlBinDir = await this.kubectl.binDir() this.kubectlBinDir = await this.kubectl.binDir()
const pathFromPreferences = userStore.preferences.kubectlBinariesPath || Kubectl.bundledKubectlPath const pathFromPreferences = userStore.preferences.kubectlBinariesPath || this.kubectl.getBundledPath()
this.kubectlPathDir = userStore.preferences.downloadKubectlBinaries ? this.kubectlBinDir : path.dirname(pathFromPreferences) this.kubectlPathDir = userStore.preferences.downloadKubectlBinaries ? this.kubectlBinDir : path.dirname(pathFromPreferences)
this.helmBinDir = helmCli.getBinaryDir() this.helmBinDir = helmCli.getBinaryDir()
const env = await this.getCachedShellEnv() const env = await this.getCachedShellEnv()

View File

@ -3,7 +3,7 @@ import type { KubeObjectDetailsProps, KubeObjectListLayoutProps, KubeObjectMenuP
import type React from "react"; import type React from "react";
import { observable } from "mobx"; import { observable } from "mobx";
import { autobind } from "../utils/autobind"; import { autobind } from "../utils";
import { KubeApi } from "./kube-api"; import { KubeApi } from "./kube-api";
export interface ApiComponents { export interface ApiComponents {

View File

@ -42,12 +42,14 @@ export interface IPodMetrics<T = IMetrics> {
networkTransmit: T; networkTransmit: T;
} }
// Reference: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#read-log-pod-v1-core
export interface IPodLogsQuery { export interface IPodLogsQuery {
container?: string; container?: string;
tailLines?: number; tailLines?: number;
timestamps?: boolean; timestamps?: boolean;
sinceTime?: string; // Date.toISOString()-format sinceTime?: string; // Date.toISOString()-format
follow?: boolean; follow?: boolean;
previous?: boolean;
} }
export enum PodStatus { export enum PodStatus {

View File

@ -6,7 +6,7 @@ import { Input } from '../input';
import { SubTitle } from '../layout/sub-title'; import { SubTitle } from '../layout/sub-title';
import { UserPreferences, userStore } from '../../../common/user-store'; import { UserPreferences, userStore } from '../../../common/user-store';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Kubectl } from '../../../main/kubectl'; import { bundledKubectlPath } from '../../../main/kubectl';
import { SelectOption, Select } from '../select'; import { SelectOption, Select } from '../select';
export const KubectlBinaries = observer(({ preferences }: { preferences: UserPreferences }) => { export const KubectlBinaries = observer(({ preferences }: { preferences: UserPreferences }) => {
@ -58,7 +58,7 @@ export const KubectlBinaries = observer(({ preferences }: { preferences: UserPre
<SubTitle title="Path to Kubectl binary" /> <SubTitle title="Path to Kubectl binary" />
<Input <Input
theme="round-black" theme="round-black"
placeholder={Kubectl.bundledKubectlPath} placeholder={bundledKubectlPath()}
value={binariesPath} value={binariesPath}
validators={isPath} validators={isPath}
onChange={setBinariesPath} onChange={setBinariesPath}

View File

@ -50,6 +50,7 @@ export class PodMenu extends React.Component<Props> {
initContainers: pod.getInitContainers(), initContainers: pod.getInitContainers(),
selectedContainer: container, selectedContainer: container,
showTimestamps: false, showTimestamps: false,
previous: false,
tailLines: 1000 tailLines: 1000
}); });
} }

View File

@ -13,6 +13,7 @@ export interface IPodLogsData {
initContainers: IPodContainer[] initContainers: IPodContainer[]
showTimestamps: boolean showTimestamps: boolean
tailLines: number tailLines: number
previous: boolean
} }
type TabId = string; type TabId = string;
@ -48,7 +49,7 @@ export class PodLogsStore extends DockTabStore<IPodLogsData> {
} }
const data = this.getData(tabId); const data = this.getData(tabId);
const { oldLogs, newLogs } = this.logs.get(tabId); const { oldLogs, newLogs } = this.logs.get(tabId);
const { selectedContainer, tailLines } = data; const { selectedContainer, tailLines, previous } = data;
const pod = new Pod(data.pod); const pod = new Pod(data.pod);
try { try {
// if logs already loaded, check the latest timestamp for getting updates only from this point // if logs already loaded, check the latest timestamp for getting updates only from this point
@ -64,14 +65,15 @@ export class PodLogsStore extends DockTabStore<IPodLogsData> {
sinceTime: lastLogDate.toISOString(), sinceTime: lastLogDate.toISOString(),
timestamps: true, // Always setting timestampt to separate old logs from new ones timestamps: true, // Always setting timestampt to separate old logs from new ones
container: selectedContainer.name, container: selectedContainer.name,
tailLines: tailLines, tailLines,
previous
}); });
if (!oldLogs) { if (!oldLogs) {
this.logs.set(tabId, { oldLogs: loadedLogs, newLogs }); this.logs.set(tabId, { oldLogs: loadedLogs, newLogs });
} else { } else {
this.logs.set(tabId, { oldLogs, newLogs: loadedLogs }); this.logs.set(tabId, { oldLogs, newLogs: loadedLogs });
} }
} catch (error) { } catch ({error}) {
this.logs.set(tabId, { this.logs.set(tabId, {
oldLogs: [ oldLogs: [
_i18n._(t`Failed to load logs: ${error.message}`), _i18n._(t`Failed to load logs: ${error.message}`),

View File

@ -94,6 +94,11 @@ export class PodLogs extends React.Component<Props> {
this.save({ showTimestamps: !this.tabData.showTimestamps }); this.save({ showTimestamps: !this.tabData.showTimestamps });
} }
togglePrevious = () => {
this.save({ previous: !this.tabData.previous });
this.reload();
}
onScroll = (evt: React.UIEvent<HTMLDivElement>) => { onScroll = (evt: React.UIEvent<HTMLDivElement>) => {
const logsArea = evt.currentTarget; const logsArea = evt.currentTarget;
const { scrollHeight, clientHeight, scrollTop } = logsArea; const { scrollHeight, clientHeight, scrollTop } = logsArea;
@ -148,7 +153,7 @@ export class PodLogs extends React.Component<Props> {
renderControls() { renderControls() {
if (!this.ready) return null; if (!this.ready) return null;
const { selectedContainer, showTimestamps, tailLines } = this.tabData; const { selectedContainer, showTimestamps, tailLines, previous } = this.tabData;
const timestamps = podLogsStore.getTimestamps(podLogsStore.logs.get(this.tabId).oldLogs); const timestamps = podLogsStore.getTimestamps(podLogsStore.logs.get(this.tabId).oldLogs);
return ( return (
<div className="controls flex gaps align-center"> <div className="controls flex gaps align-center">
@ -181,6 +186,12 @@ export class PodLogs extends React.Component<Props> {
className={cssNames("timestamps-icon", { active: showTimestamps })} className={cssNames("timestamps-icon", { active: showTimestamps })}
tooltip={(showTimestamps ? _i18n._(t`Hide`) : _i18n._(t`Show`)) + " " + _i18n._(t`timestamps`)} tooltip={(showTimestamps ? _i18n._(t`Hide`) : _i18n._(t`Show`)) + " " + _i18n._(t`timestamps`)}
/> />
<Icon
material="undo"
onClick={this.togglePrevious}
className={cssNames("undo-icon", { active: previous })}
tooltip={(previous ? _i18n._(t`Show current logs`) : _i18n._(t`Show previous terminated container logs`))}
/>
<Icon <Icon
material="get_app" material="get_app"
onClick={this.downloadLogs} onClick={this.downloadLogs}

View File

@ -5,7 +5,7 @@ import { FitAddon } from "xterm-addon-fit";
import { dockStore, TabId } from "./dock.store"; import { dockStore, TabId } from "./dock.store";
import { TerminalApi } from "../../api/terminal-api"; import { TerminalApi } from "../../api/terminal-api";
import { themeStore } from "../../theme.store"; import { themeStore } from "../../theme.store";
import { autobind } from "../../utils/autobind"; import { autobind } from "../../utils";
export class Terminal { export class Terminal {
static spawningPool: HTMLElement; static spawningPool: HTMLElement;

View File

@ -39,7 +39,7 @@ export const isNumber: Validator = {
export const isUrl: Validator = { export const isUrl: Validator = {
condition: ({ type }) => type === "url", condition: ({ type }) => type === "url",
message: () => _i18n._(t`Wrong url format`), message: () => _i18n._(t`Wrong url format`),
validate: value => !!value.match(/^http(s)?:\/\/\w+(\.\w+)*(:[0-9]+)?\/?(\/[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]*)*$/), validate: value => !!value.match(/^$|^http(s)?:\/\/\w+(\.\w+)*(:[0-9]+)?\/?(\/[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]*)*$/),
}; };
export const isPath: Validator = { export const isPath: Validator = {

View File

@ -1,5 +1,5 @@
import { computed, observable, reaction } from "mobx"; import { computed, observable, reaction } from "mobx";
import { autobind } from "./utils/autobind"; import { autobind } from "./utils";
import { userStore } from "../common/user-store"; import { userStore } from "../common/user-store";
import logger from "../main/logger"; import logger from "../main/logger";

View File

@ -1,9 +0,0 @@
// Debouncing promise evaluation
export const debouncePromise = function (promisedFunc: Function, timeout = 0) {
let timer: number;
return (...params: any[]) => new Promise((resolve, reject) => {
clearTimeout(timer);
timer = window.setTimeout(() => resolve(promisedFunc.apply(this, params)), timeout);
});
};

View File

@ -3,21 +3,18 @@
export const noop: any = Function(); export const noop: any = Function();
export const isElectron = !!navigator.userAgent.match(/Electron/); export const isElectron = !!navigator.userAgent.match(/Electron/);
export * from '../../common/utils/camelCase' export * from "../../common/utils"
export * from '../../common/utils/base64'
export * from './autobind' export * from "./cssVar"
export * from './cssVar' export * from "./cssNames"
export * from './cssNames' export * from "./eventEmitter"
export * from './eventEmitter' export * from "./downloadFile"
export * from './downloadFile' export * from "./prevDefault"
export * from './prevDefault' export * from "./createStorage"
export * from './createStorage' export * from "./interval"
export * from './interval' export * from "./copyToClipboard"
export * from './debouncePromise' export * from "./formatDuration"
export * from './copyToClipboard' export * from "./isReactNode"
export * from './formatDuration' export * from "./convertMemory"
export * from './isReactNode' export * from "./convertCpu"
export * from './convertMemory' export * from "./metricUnitsToNumber"
export * from './convertCpu'
export * from './metricUnitsToNumber'