Unverified Commit 20ca4932 authored by Jim Ehrismann's avatar Jim Ehrismann Committed by GitHub
Browse files

Release/v5.2.1 (#3819)


* Use PATCH verb to scale deployments and statefulsets (#3744)
Signed-off-by: default avatarLauri Nevala <lauri.nevala@gmail.com>
Signed-off-by: default avatarJim Ehrismann <jehrismann@mirantis.com>

* Stop Sentry from capturing console.error and logger.error (#3785)
Signed-off-by: default avatarHung-Han (Henry) Chen <chenhungh@gmail.com>
Signed-off-by: default avatarJim Ehrismann <jehrismann@mirantis.com>

* Sidebar cluster avatar (#3765)
Signed-off-by: default avatarJim Ehrismann <jehrismann@mirantis.com>

* Second click on list item closes the details view (#3809)
Signed-off-by: default avatarJuho Heikka <juho.heikka@gmail.com>
Signed-off-by: default avatarJim Ehrismann <jehrismann@mirantis.com>

* Fix fallback to bundled kubectl (#3812)
Signed-off-by: default avatarJim Ehrismann <jehrismann@mirantis.com>

* Fix cluster's accessibleNamespaces being reset on restarting Lens (#3817)
Signed-off-by: default avatarSebastian Malton <sebastian@malton.name>
Signed-off-by: default avatarJim Ehrismann <jehrismann@mirantis.com>

* Don't change permissions when deleting a cluster (#3798)

Sign...
parent a06f0c1a
Showing with 189 additions and 91 deletions
+189 -91
......@@ -2,7 +2,7 @@ name: Test
on:
pull_request:
branches:
- "*"
- "**"
push:
branches:
- master
......
......@@ -36,7 +36,7 @@ function getSidebarSelectors(itemId: string) {
return {
expandSubMenu: `${root} .nav-item`,
subMenuLink: (href: string) => `.Sidebar .sub-menu a[href^="/${href}"]`,
subMenuLink: (href: string) => `[data-testid=cluster-sidebar] .sub-menu a[href^="/${href}"]`,
};
}
......
......@@ -113,7 +113,7 @@ export async function lauchMinikubeClusterFromCatalog(window: Page): Promise<Fra
const frame = await minikubeFrame.contentFrame();
await frame.waitForSelector("div.Sidebar");
await frame.waitForSelector("[data-testid=cluster-sidebar]");
return frame;
}
......@@ -3,7 +3,7 @@
"productName": "OpenLens",
"description": "OpenLens - Open Source IDE for Kubernetes",
"homepage": "https://github.com/lensapp/lens",
"version": "5.2.0",
"version": "5.2.1",
"main": "static/build/main.js",
"copyright": "© 2021 OpenLens Authors",
"license": "MIT",
......@@ -185,6 +185,7 @@
"@kubernetes/client-node": "^0.15.1",
"@sentry/electron": "^2.5.0",
"@sentry/integrations": "^6.10.0",
"@types/which": "^2.0.1",
"abort-controller": "^3.0.0",
"array-move": "^3.0.1",
"auto-bind": "^4.0.0",
......@@ -193,7 +194,6 @@
"byline": "^5.0.0",
"chalk": "^4.1.0",
"chokidar": "^3.4.3",
"command-exists": "1.2.9",
"conf": "^7.0.1",
"crypto-js": "^4.1.1",
"electron-devtools-installer": "^3.2.0",
......@@ -246,6 +246,7 @@
"tempy": "^0.5.0",
"url-parse": "^1.5.1",
"uuid": "^8.3.2",
"which": "^2.0.2",
"win-ca": "^3.2.0",
"winston": "^3.3.3",
"winston-console-format": "^1.0.8",
......@@ -347,7 +348,6 @@
"jest-canvas-mock": "^2.3.1",
"jest-fetch-mock": "^3.0.3",
"jest-mock-extended": "^1.0.16",
"json-to-pretty-yaml": "^1.2.2",
"make-plural": "^6.2.2",
"mini-css-extract-plugin": "^1.6.0",
"node-gyp": "7.1.2",
......
......@@ -51,6 +51,7 @@ export interface KubernetesClusterSpec extends CatalogEntitySpec {
material?: string;
background?: string;
};
accessibleNamespaces?: string[];
}
export interface KubernetesClusterMetadata extends CatalogEntityMetadata {
......
......@@ -19,15 +19,18 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import path from "path";
export class ExecValidationNotFoundError extends Error {
constructor(execPath: string, isAbsolute: boolean) {
super(`User Exec command "${execPath}" not found on host.`);
constructor(execPath: string) {
let message = `User Exec command "${execPath}" not found on host.`;
if (!isAbsolute) {
if (!path.isAbsolute(execPath)) {
message += ` Please ensure binary is found in PATH or use absolute path to binary in Kubeconfig`;
}
this.message = message;
super(message);
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
......
......@@ -19,38 +19,42 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
.Sidebar {
$iconSize: 24px;
$itemSpacing: floor($unit / 2.6) floor($unit / 1.6);
.sidebar-nav {
width: var(--sidebar-width);
padding-bottom: calc(var(--padding) * 3);
overflow: auto;
.Icon {
--size: #{$iconSize};
box-sizing: content-box;
padding: floor($padding / 2.6);
border-radius: 50%;
}
hr {
background-color: transparent;
}
}
.loading {
padding: $padding;
text-align: center;
}
import { Deployment, DeploymentApi } from "../endpoints/deployment.api";
import type { KubeJsonApi } from "../kube-json-api";
.cluster-name {
padding: 1.25rem;
font-weight: bold;
font-size: 1.5rem;
word-break: break-all;
color: var(--textColorAccent);
class DeploymentApiTest extends DeploymentApi {
public setRequest(request: any) {
this.request = request;
}
}
describe("DeploymentApi", () => {
describe("scale", () => {
const requestMock = {
patch: () => ({}),
} as unknown as KubeJsonApi;
const sub = new DeploymentApiTest({ objectConstructor: Deployment });
sub.setRequest(requestMock);
it("requests Kubernetes API with PATCH verb and correct amount of replicas", () => {
const patchSpy = jest.spyOn(requestMock, "patch");
sub.scale({ namespace: "default", name: "deployment-1"}, 5);
expect(patchSpy).toHaveBeenCalledWith("/apis/apps/v1/namespaces/default/deployments/deployment-1/scale", {
data: {
spec: {
replicas: 5
}
}
},
{
headers: {
"content-type": "application/merge-patch+json"
}
});
});
});
});
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { StatefulSet, StatefulSetApi } from "../endpoints/stateful-set.api";
import type { KubeJsonApi } from "../kube-json-api";
class StatefulSetApiTest extends StatefulSetApi {
public setRequest(request: any) {
this.request = request;
}
}
describe("StatefulSetApi", () => {
describe("scale", () => {
const requestMock = {
patch: () => ({}),
} as unknown as KubeJsonApi;
const sub = new StatefulSetApiTest({ objectConstructor: StatefulSet });
sub.setRequest(requestMock);
it("requests Kubernetes API with PATCH verb and correct amount of replicas", () => {
const patchSpy = jest.spyOn(requestMock, "patch");
sub.scale({ namespace: "default", name: "statefulset-1"}, 5);
expect(patchSpy).toHaveBeenCalledWith("/apis/apps/v1/namespaces/default/statefulsets/statefulset-1/scale", {
data: {
spec: {
replicas: 5
}
}
},
{
headers: {
"content-type": "application/merge-patch+json"
}
});
});
});
});
......@@ -41,13 +41,17 @@ export class DeploymentApi extends KubeApi<Deployment> {
}
scale(params: { namespace: string; name: string }, replicas: number) {
return this.request.put(this.getScaleApiUrl(params), {
return this.request.patch(this.getScaleApiUrl(params), {
data: {
metadata: params,
spec: {
replicas
}
}
},
{
headers: {
"content-type": "application/merge-patch+json"
}
});
}
......
......@@ -39,13 +39,17 @@ export class StatefulSetApi extends KubeApi<StatefulSet> {
}
scale(params: { namespace: string; name: string }, replicas: number) {
return this.request.put(this.getScaleApiUrl(params), {
return this.request.patch(this.getScaleApiUrl(params), {
data: {
metadata: params,
spec: {
replicas
}
}
},
{
headers: {
"content-type": "application/merge-patch+json"
}
});
}
}
......
......@@ -25,11 +25,11 @@ import path from "path";
import os from "os";
import yaml from "js-yaml";
import logger from "../main/logger";
import commandExists from "command-exists";
import { ExecValidationNotFoundError } from "./custom-errors";
import { Cluster, Context, newClusters, newContexts, newUsers, User } from "@kubernetes/client-node/dist/config_types";
import { resolvePath } from "./utils";
import Joi from "joi";
import which from "which";
export type KubeConfigValidationOpts = {
validateCluster?: boolean;
......@@ -295,13 +295,17 @@ export function validateKubeConfig(config: KubeConfig, contextName: string, vali
// Validate exec command if present
if (validateExec && user?.exec) {
const execCommand = user.exec["command"];
// check if the command is absolute or not
const isAbsolute = path.isAbsolute(execCommand);
// validate the exec struct in the user object, start with the command field
if (!commandExists.sync(execCommand)) {
return new ExecValidationNotFoundError(execCommand, isAbsolute);
try {
which.sync(user.exec.command);
// If this doesn't throw an error it also means that it has found the executable.
} catch (error) {
switch (error?.code) {
case "ENOENT":
return new ExecValidationNotFoundError(user.exec.command);
default:
return error;
}
}
}
......
......@@ -25,7 +25,6 @@ import type Transport from "winston-transport";
import { consoleFormat } from "winston-console-format";
import { isDebugging, isTestEnv } from "./vars";
import BrowserConsole from "winston-transport-browserconsole";
import { SentryTransport } from "./logger-transports";
import { getPath } from "./utils/getPath";
const logLevel = process.env.LOG_LEVEL
......@@ -36,9 +35,7 @@ const logLevel = process.env.LOG_LEVEL
? "error"
: "info";
const transports: Transport[] = [
new SentryTransport("error")
];
const transports: Transport[] = [];
if (ipcMain) {
transports.push(
......
......@@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { CaptureConsole, Dedupe, Offline } from "@sentry/integrations";
import { Dedupe, Offline } from "@sentry/integrations";
import * as Sentry from "@sentry/electron";
import { sentryDsn, isProduction } from "./vars";
import { UserStore } from "./user-store";
......@@ -65,7 +65,6 @@ export function SentryInit() {
},
dsn: sentryDsn,
integrations: [
new CaptureConsole({ levels: ["error"] }),
new Dedupe(),
new Offline(),
],
......
......@@ -193,7 +193,10 @@ export class ClusterManager extends Singleton {
} else {
cluster.kubeConfigPath = entity.spec.kubeconfigPath;
cluster.contextName = entity.spec.kubeconfigContext;
cluster.accessibleNamespaces = entity.spec.accessibleNamespaces ?? [];
if (entity.spec.accessibleNamespace) {
cluster.accessibleNamespaces = entity.spec.accessibleNamespaces;
}
this.updateEntityFromCluster(cluster);
}
......
......@@ -275,7 +275,7 @@ export class Kubectl {
return false;
});
isValid = !await this.checkBinary(this.path, false);
isValid = await this.checkBinary(this.path, false);
}
if (!isValid) {
......
......@@ -82,7 +82,7 @@ export class WindowManager extends Singleton {
show: false,
minWidth: 700, // accommodate 800 x 600 display minimum
minHeight: 500, // accommodate 800 x 600 display minimum
titleBarStyle: "hidden",
titleBarStyle: "hiddenInset",
backgroundColor: "#1e2124",
webPreferences: {
preload: path.join(__static, "build", "preload.js"),
......
......@@ -58,7 +58,6 @@ export class TerminalApi extends WebSocketApi {
public onReady = new EventEmitter<[]>();
@observable public isReady = false;
@observable public shellRunCommandsFinished = false;
public readonly url: string;
constructor(protected options: TerminalApiQuery) {
......@@ -92,7 +91,6 @@ export class TerminalApi extends WebSocketApi {
connect() {
this.emitStatus("Connecting ...");
this.onData.addListener(this._onReady, { prepend: true });
this.onData.addListener(this._onShellRunCommandsFinished);
super.connect(this.url);
}
......@@ -109,24 +107,6 @@ export class TerminalApi extends WebSocketApi {
this.onReady.removeAllListeners();
}
_onShellRunCommandsFinished = (data: string) => {
if (!data) {
return;
}
/**
* This is a heuistic for ditermining when a shell has finished executing
* its own rc file (or RunCommands file) such as `.bashrc` or `.zshrc`.
*
* This heuistic assumes that the prompt line of a terminal is a single line
* and ends with a whitespace character.
*/
if (data.match(/\r?\n/) === null && data.match(/\s$/)) {
this.shellRunCommandsFinished = true;
this.onData.removeListener(this._onShellRunCommandsFinished);
}
};
@boundMethod
protected _onReady(data: string) {
if (!data) return true;
......
......@@ -50,4 +50,11 @@
display: block;
padding-top: 6px;
}
.actions-panel {
.Spinner {
vertical-align: middle;
margin-left: $spacing;
}
}
}
......@@ -24,7 +24,7 @@ import "./add-cluster.scss";
import type { KubeConfig } from "@kubernetes/client-node";
import fse from "fs-extra";
import { debounce } from "lodash";
import { action, computed, observable, makeObservable } from "mobx";
import { action, computed, observable, makeObservable, runInAction } from "mobx";
import { observer } from "mobx-react";
import path from "path";
import React from "react";
......@@ -41,6 +41,7 @@ import { SettingLayout } from "../layout/setting-layout";
import MonacoEditor from "react-monaco-editor";
import { ThemeStore } from "../../theme.store";
import { UserStore } from "../../../common/user-store";
import { Spinner } from "../spinner";
interface Option {
config: KubeConfig;
......@@ -62,6 +63,7 @@ export class AddCluster extends React.Component {
@observable kubeContexts = observable.map<string, Option>();
@observable customConfig = "";
@observable isWaiting = false;
@observable isCheckingInput = false;
@observable errorText: string;
constructor(props: {}) {
......@@ -80,14 +82,35 @@ export class AddCluster extends React.Component {
].filter(Boolean);
}
@action
refreshContexts = debounce(() => {
const { config, error } = loadConfigFromString(this.customConfig.trim() || "{}");
this.kubeContexts.replace(getContexts(config));
this.errorText = error?.toString();
_refreshContexts = debounce(() => {
runInAction(() => {
try {
const text = this.customConfig.trim();
if (!text) {
return this.kubeContexts.clear();
}
const { config, error } = loadConfigFromString(text);
this.kubeContexts.replace(getContexts(config));
this.errorText = error?.toString();
} catch (error) {
this.kubeContexts.clear();
this.errorText = error?.toString() || "An error occured";
} finally {
this.isCheckingInput = false;
}
});
}, 500);
refreshContexts = () => {
// Clear the kubeContexts immediately
this.isCheckingInput = true;
this.kubeContexts.clear();
this._refreshContexts();
};
@action
addClusters = async () => {
this.isWaiting = true;
......@@ -145,6 +168,7 @@ export class AddCluster extends React.Component {
tooltip={this.kubeContexts.size === 0 || "Paste in at least one cluster to add."}
tooltipOverrideDisabled
/>
{this.isCheckingInput && <Spinner />}
</div>
</SettingLayout>
);
......
......@@ -55,6 +55,14 @@ export class HelmCharts extends Component<Props> {
return helmChartStore.getByName(chartName, repo);
}
onDetails = (chart: HelmChart) => {
if (chart === this.selectedChart) {
this.hideDetails();
} else {
this.showDetails(chart);
}
};
showDetails = (chart: HelmChart) => {
if (!chart) {
navigation.push(helmChartsURL());
......@@ -121,7 +129,7 @@ export class HelmCharts extends Component<Props> {
{ className: "menu" }
]}
detailsItem={this.selectedChart}
onDetails={this.showDetails}
onDetails={this.onDetails}
/>
{this.selectedChart && (
<HelmChartDetails
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment