mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Release/v5.3.4 (#4721)
* Use electron.clipboard for all clipboard uses (#4535) Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Fix ERR_UNSAFE_PORT from LensProxy (#4558) * Fix ERR_UNSAFE_PORT from LensProxy - Use the current list of ports from chromium as it is much easier to just reject using one of those instead of trying to handle the ERR_UNSAFE_PORT laod error from a BrowserWindow.on("did-fail-load") Signed-off-by: Sebastian Malton <sebastian@malton.name> * Move all port handling into LensProxy Signed-off-by: Sebastian Malton <sebastian@malton.name> * don't use so many exceptions Signed-off-by: Sebastian Malton <sebastian@malton.name> Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Fix not being able to clear set cluster icon (#4555) Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Fix extension engine range not working for some ^ ranges (#4554) Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Fix crash on NetworkPolicy when matchLabels is missing (#4500) Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Replace all uses of promiseExec with promiseExecFile (#4514) Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Less noisy metrics-not-available error logging (#4602) Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Fix close button overflow in Preferences (#4611) * Adding basic colors to tailwind theme Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Using tailwind inline to style close button Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Make Select look similar to inputs Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Moving styles into separate module Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Convert tailwind commands to css Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Fix prometheus operator metrics work out of the box (#4617) Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com> Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Fix CRD.getPreferedVersion() to work based on apiVersion (#4553) * Fix CRD.getPreferedVersion() to work based on apiVersion Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add tests Signed-off-by: Sebastian Malton <sebastian@malton.name> Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Fix crash for KubeObjectStore.loadAll() (#4675) Signed-off-by: Sebastian Malton <sebastian@malton.name> Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * Convert CloseButton styles out from css modules (#4723) * Convert CloseButton styles out from css modules Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Fix close button styling Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * release v5.3.4 Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> Co-authored-by: Sebastian Malton <sebastian@malton.name> Co-authored-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> Co-authored-by: Alex Andreev <alex.andreev.email@gmail.com> Co-authored-by: Lauri Nevala <lauri.nevala@gmail.com>
This commit is contained in:
parent
0a6bafc1be
commit
e8d8f8f610
@ -71,7 +71,8 @@ describe("preferences page tests", () => {
|
|||||||
}
|
}
|
||||||
}, 10*60*1000);
|
}, 10*60*1000);
|
||||||
|
|
||||||
utils.itIf(process.platform !== "win32")("ensures helm repos", async () => {
|
// Skipping, but will turn it on again in the follow up PR
|
||||||
|
it.skip("ensures helm repos", async () => {
|
||||||
await window.click("[data-testid=kubernetes-tab]");
|
await window.click("[data-testid=kubernetes-tab]");
|
||||||
await window.waitForSelector("[data-testid=repository-name]", {
|
await window.waitForSelector("[data-testid=repository-name]", {
|
||||||
timeout: 140_000,
|
timeout: 140_000,
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
"productName": "OpenLens",
|
"productName": "OpenLens",
|
||||||
"description": "OpenLens - Open Source IDE for Kubernetes",
|
"description": "OpenLens - Open Source IDE for Kubernetes",
|
||||||
"homepage": "https://github.com/lensapp/lens",
|
"homepage": "https://github.com/lensapp/lens",
|
||||||
"version": "5.3.3",
|
"version": "5.3.4",
|
||||||
"main": "static/build/main.js",
|
"main": "static/build/main.js",
|
||||||
"copyright": "© 2021 OpenLens Authors",
|
"copyright": "© 2021 OpenLens Authors",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -90,7 +90,11 @@ export interface ClusterPreferences extends ClusterPrometheusPreferences {
|
|||||||
terminalCWD?: string;
|
terminalCWD?: string;
|
||||||
clusterName?: string;
|
clusterName?: string;
|
||||||
iconOrder?: number;
|
iconOrder?: number;
|
||||||
icon?: string;
|
/**
|
||||||
|
* The <img> src for the cluster. If set to `null` that means that it was
|
||||||
|
* cleared by preferences.
|
||||||
|
*/
|
||||||
|
icon?: string | null;
|
||||||
httpsProxy?: string;
|
httpsProxy?: string;
|
||||||
hiddenMetrics?: string[];
|
hiddenMetrics?: string[];
|
||||||
nodeShellImage?: string;
|
nodeShellImage?: string;
|
||||||
|
|||||||
@ -19,10 +19,10 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CustomResourceDefinition } from "../endpoints";
|
import { CustomResourceDefinition, CustomResourceDefinitionSpec } from "../endpoints";
|
||||||
|
|
||||||
describe("Crds", () => {
|
describe("Crds", () => {
|
||||||
describe("getVersion", () => {
|
describe("getVersion()", () => {
|
||||||
it("should throw if none of the versions are served", () => {
|
it("should throw if none of the versions are served", () => {
|
||||||
const crd = new CustomResourceDefinition({
|
const crd = new CustomResourceDefinition({
|
||||||
apiVersion: "apiextensions.k8s.io/v1",
|
apiVersion: "apiextensions.k8s.io/v1",
|
||||||
@ -136,7 +136,7 @@ describe("Crds", () => {
|
|||||||
expect(crd.getVersion()).toBe("123");
|
expect(crd.getVersion()).toBe("123");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should get the version name from the version field", () => {
|
it("should get the version name from the version field, ignoring versions on v1beta", () => {
|
||||||
const crd = new CustomResourceDefinition({
|
const crd = new CustomResourceDefinition({
|
||||||
apiVersion: "apiextensions.k8s.io/v1beta1",
|
apiVersion: "apiextensions.k8s.io/v1beta1",
|
||||||
kind: "CustomResourceDefinition",
|
kind: "CustomResourceDefinition",
|
||||||
@ -147,7 +147,14 @@ describe("Crds", () => {
|
|||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
version: "abc",
|
version: "abc",
|
||||||
},
|
versions: [
|
||||||
|
{
|
||||||
|
name: "foobar",
|
||||||
|
served: true,
|
||||||
|
storage: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as CustomResourceDefinitionSpec,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(crd.getVersion()).toBe("abc");
|
expect(crd.getVersion()).toBe("abc");
|
||||||
|
|||||||
@ -48,34 +48,36 @@ export interface CRDVersion {
|
|||||||
additionalPrinterColumns?: AdditionalPrinterColumnsV1[];
|
additionalPrinterColumns?: AdditionalPrinterColumnsV1[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CustomResourceDefinition {
|
export interface CustomResourceDefinitionSpec {
|
||||||
spec: {
|
group: string;
|
||||||
group: string;
|
/**
|
||||||
/**
|
* @deprecated for apiextensions.k8s.io/v1 but used in v1beta1
|
||||||
* @deprecated for apiextensions.k8s.io/v1 but used previously
|
*/
|
||||||
*/
|
version?: string;
|
||||||
version?: string;
|
names: {
|
||||||
names: {
|
plural: string;
|
||||||
plural: string;
|
singular: string;
|
||||||
singular: string;
|
kind: string;
|
||||||
kind: string;
|
listKind: string;
|
||||||
listKind: string;
|
|
||||||
};
|
|
||||||
scope: "Namespaced" | "Cluster" | string;
|
|
||||||
/**
|
|
||||||
* @deprecated for apiextensions.k8s.io/v1 but used previously
|
|
||||||
*/
|
|
||||||
validation?: object;
|
|
||||||
versions?: CRDVersion[];
|
|
||||||
conversion: {
|
|
||||||
strategy?: string;
|
|
||||||
webhook?: any;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* @deprecated for apiextensions.k8s.io/v1 but used previously
|
|
||||||
*/
|
|
||||||
additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[];
|
|
||||||
};
|
};
|
||||||
|
scope: "Namespaced" | "Cluster";
|
||||||
|
/**
|
||||||
|
* @deprecated for apiextensions.k8s.io/v1 but used in v1beta1
|
||||||
|
*/
|
||||||
|
validation?: object;
|
||||||
|
versions?: CRDVersion[];
|
||||||
|
conversion: {
|
||||||
|
strategy?: string;
|
||||||
|
webhook?: any;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* @deprecated for apiextensions.k8s.io/v1 but used in v1beta1
|
||||||
|
*/
|
||||||
|
additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomResourceDefinition {
|
||||||
|
spec: CustomResourceDefinitionSpec;
|
||||||
status: {
|
status: {
|
||||||
conditions: {
|
conditions: {
|
||||||
lastTransitionTime: string;
|
lastTransitionTime: string;
|
||||||
@ -150,27 +152,30 @@ export class CustomResourceDefinition extends KubeObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getPreferedVersion(): CRDVersion {
|
getPreferedVersion(): CRDVersion {
|
||||||
// Prefer the modern `versions` over the legacy `version`
|
const { apiVersion } = this;
|
||||||
if (this.spec.versions) {
|
|
||||||
for (const version of this.spec.versions) {
|
|
||||||
if (version.storage) {
|
|
||||||
return version;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (this.spec.version) {
|
|
||||||
const { additionalPrinterColumns: apc } = this.spec;
|
|
||||||
const additionalPrinterColumns = apc?.map(({ JSONPath, ...apc }) => ({ ...apc, jsonPath: JSONPath }));
|
|
||||||
|
|
||||||
return {
|
switch (apiVersion) {
|
||||||
name: this.spec.version,
|
case "apiextensions.k8s.io/v1":
|
||||||
served: true,
|
for (const version of this.spec.versions) {
|
||||||
storage: true,
|
if (version.storage) {
|
||||||
schema: this.spec.validation,
|
return version;
|
||||||
additionalPrinterColumns,
|
}
|
||||||
};
|
}
|
||||||
|
break;
|
||||||
|
case "apiextensions.k8s.io/v1beta1":
|
||||||
|
const { additionalPrinterColumns: apc } = this.spec;
|
||||||
|
const additionalPrinterColumns = apc?.map(({ JSONPath, ...apc }) => ({ ...apc, jsonPath: JSONPath }));
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: this.spec.version,
|
||||||
|
served: true,
|
||||||
|
storage: true,
|
||||||
|
schema: this.spec.validation,
|
||||||
|
additionalPrinterColumns,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Failed to find a version for CustomResourceDefinition ${this.metadata.name}`);
|
throw new Error(`Unknown apiVersion=${apiVersion}: Failed to find a version for CustomResourceDefinition ${this.metadata.name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
getVersion() {
|
getVersion() {
|
||||||
@ -197,7 +202,7 @@ export class CustomResourceDefinition extends KubeObject {
|
|||||||
const columns = this.getPreferedVersion().additionalPrinterColumns ?? [];
|
const columns = this.getPreferedVersion().additionalPrinterColumns ?? [];
|
||||||
|
|
||||||
return columns
|
return columns
|
||||||
.filter(column => column.name != "Age" && (ignorePriority || !column.priority));
|
.filter(column => column.name.toLowerCase() != "age" && (ignorePriority || !column.priority));
|
||||||
}
|
}
|
||||||
|
|
||||||
getValidation() {
|
getValidation() {
|
||||||
|
|||||||
@ -235,8 +235,9 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async loadAll({ namespaces = this.context.contextNamespaces, merge = true, reqInit, onLoadFailure }: KubeObjectStoreLoadAllParams = {}): Promise<void | T[]> {
|
async loadAll({ namespaces, merge = true, reqInit, onLoadFailure }: KubeObjectStoreLoadAllParams = {}): Promise<void | T[]> {
|
||||||
await this.contextReady;
|
await this.contextReady;
|
||||||
|
namespaces ??= this.context.contextNamespaces;
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -22,7 +22,7 @@
|
|||||||
import { isMac, isWindows } from "./vars";
|
import { isMac, isWindows } from "./vars";
|
||||||
import wincaAPI from "win-ca/api";
|
import wincaAPI from "win-ca/api";
|
||||||
import https from "https";
|
import https from "https";
|
||||||
import { promiseExec } from "./utils/promise-exec";
|
import { promiseExecFile } from "./utils/promise-exec";
|
||||||
|
|
||||||
// DST Root CA X3, which was expired on 9.30.2021
|
// DST Root CA X3, which was expired on 9.30.2021
|
||||||
export const DSTRootCAX3 = "-----BEGIN CERTIFICATE-----\nMIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/\nMSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT\nDkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow\nPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD\nEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O\nrz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq\nOLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b\nxiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw\n7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD\naeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV\nHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG\nSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69\nikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr\nAvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz\nR8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5\nJDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo\nOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ\n-----END CERTIFICATE-----\n";
|
export const DSTRootCAX3 = "-----BEGIN CERTIFICATE-----\nMIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/\nMSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT\nDkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow\nPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD\nEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O\nrz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq\nOLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b\nxiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw\n7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD\naeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV\nHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG\nSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69\nikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr\nAvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz\nR8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5\nJDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo\nOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ\n-----END CERTIFICATE-----\n";
|
||||||
@ -33,19 +33,25 @@ export function isCertActive(cert: string) {
|
|||||||
return !isExpired;
|
return !isExpired;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Cheatsheet#other_assertions
|
||||||
|
const certSplitPattern = /(?=-----BEGIN\sCERTIFICATE-----)/g;
|
||||||
|
|
||||||
|
async function execSecurity(...args: string[]): Promise<string[]> {
|
||||||
|
const { stdout } = await promiseExecFile("/usr/bin/security", args);
|
||||||
|
|
||||||
|
return stdout.split(certSplitPattern);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get root CA certificate from MacOSX system keychain
|
* Get root CA certificate from MacOSX system keychain
|
||||||
* Only return non-expred certificates.
|
* Only return non-expred certificates.
|
||||||
*/
|
*/
|
||||||
export async function getMacRootCA() {
|
export async function getMacRootCA() {
|
||||||
// inspired mac-ca https://github.com/jfromaniello/mac-ca
|
// inspired mac-ca https://github.com/jfromaniello/mac-ca
|
||||||
const args = "find-certificate -a -p";
|
const [trusted, rootCA] = await Promise.all([
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Cheatsheet#other_assertions
|
execSecurity("find-certificate", "-a", "-p"),
|
||||||
const splitPattern = /(?=-----BEGIN\sCERTIFICATE-----)/g;
|
execSecurity("find-certificate", "-a", "-p", "/System/Library/Keychains/SystemRootCertificates.keychain"),
|
||||||
const systemRootCertsPath = "/System/Library/Keychains/SystemRootCertificates.keychain";
|
]);
|
||||||
const bin = "/usr/bin/security";
|
|
||||||
const trusted = (await promiseExec(`${bin} ${args}`)).stdout.toString().split(splitPattern);
|
|
||||||
const rootCA = (await promiseExec(`${bin} ${args} ${systemRootCertsPath}`)).stdout.toString().split(splitPattern);
|
|
||||||
|
|
||||||
return [...new Set([...trusted, ...rootCA])].filter(isCertActive);
|
return [...new Set([...trusted, ...rootCA])].filter(isCertActive);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,7 +20,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as util from "util";
|
import * as util from "util";
|
||||||
import { exec, execFile } from "child_process";
|
import { execFile } from "child_process";
|
||||||
|
|
||||||
export const promiseExec = util.promisify(exec);
|
|
||||||
export const promiseExecFile = util.promisify(execFile);
|
export const promiseExecFile = util.promisify(execFile);
|
||||||
|
|||||||
@ -19,29 +19,24 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { isCompatibleExtension } from "../extension-compatibility";
|
import { rawIsCompatibleExtension } from "../extension-compatibility";
|
||||||
import { Console } from "console";
|
import { Console } from "console";
|
||||||
import { stdout, stderr } from "process";
|
import { stdout, stderr } from "process";
|
||||||
import type { LensExtensionManifest } from "../lens-extension";
|
import type { LensExtensionManifest } from "../lens-extension";
|
||||||
import { appSemVer } from "../../common/vars";
|
import { SemVer } from "semver";
|
||||||
|
|
||||||
console = new Console(stdout, stderr);
|
console = new Console(stdout, stderr);
|
||||||
|
|
||||||
describe("extension compatibility", () => {
|
describe("extension compatibility", () => {
|
||||||
describe("appSemVer with no prerelease tag", () => {
|
describe("appSemVer with no prerelease tag", () => {
|
||||||
beforeAll(() => {
|
const isCompatibleExtension = rawIsCompatibleExtension(new SemVer("5.0.3"));
|
||||||
appSemVer.major = 5;
|
|
||||||
appSemVer.minor = 0;
|
|
||||||
appSemVer.patch = 3;
|
|
||||||
appSemVer.prerelease = [];
|
|
||||||
});
|
|
||||||
|
|
||||||
it("has no extension comparator", () => {
|
it("has no extension comparator", () => {
|
||||||
const manifest = { name: "extensionName", version: "0.0.1" };
|
const manifest = { name: "extensionName", version: "0.0.1" };
|
||||||
|
|
||||||
expect(isCompatibleExtension(manifest)).toBe(false);
|
expect(isCompatibleExtension(manifest)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
{
|
{
|
||||||
comparator: "",
|
comparator: "",
|
||||||
@ -83,19 +78,32 @@ describe("extension compatibility", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("appSemVer with prerelease tag", () => {
|
describe("appSemVer with prerelease tag", () => {
|
||||||
beforeAll(() => {
|
const isCompatibleExtension = rawIsCompatibleExtension(new SemVer("5.0.3-beta.3"));
|
||||||
appSemVer.major = 5;
|
|
||||||
appSemVer.minor = 0;
|
it("^5.1.0 should work when lens' version is 5.1.0-latest.123456789", () => {
|
||||||
appSemVer.patch = 3;
|
const comparer = rawIsCompatibleExtension(new SemVer("5.1.0-latest.123456789"));
|
||||||
appSemVer.prerelease = ["beta", 3];
|
|
||||||
|
expect(comparer({ name: "extensionName", version: "0.0.1", engines: { lens: "^5.1.0" }})).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("^5.1.0 should not when lens' version is 5.1.0-beta.1.123456789", () => {
|
||||||
|
const comparer = rawIsCompatibleExtension(new SemVer("5.1.0-beta.123456789"));
|
||||||
|
|
||||||
|
expect(comparer({ name: "extensionName", version: "0.0.1", engines: { lens: "^5.1.0" }})).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("^5.1.0 should not when lens' version is 5.1.0-alpha.1.123456789", () => {
|
||||||
|
const comparer = rawIsCompatibleExtension(new SemVer("5.1.0-alpha.123456789"));
|
||||||
|
|
||||||
|
expect(comparer({ name: "extensionName", version: "0.0.1", engines: { lens: "^5.1.0" }})).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it("has no extension comparator", () => {
|
it("has no extension comparator", () => {
|
||||||
const manifest = { name: "extensionName", version: "0.0.1" };
|
const manifest = { name: "extensionName", version: "0.0.1" };
|
||||||
|
|
||||||
expect(isCompatibleExtension(manifest)).toBe(false);
|
expect(isCompatibleExtension(manifest)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
{
|
{
|
||||||
comparator: "",
|
comparator: "",
|
||||||
@ -130,9 +138,7 @@ describe("extension compatibility", () => {
|
|||||||
expected: false,
|
expected: false,
|
||||||
},
|
},
|
||||||
])("extension comparator test: %p", ({ comparator, expected }) => {
|
])("extension comparator test: %p", ({ comparator, expected }) => {
|
||||||
const manifest: LensExtensionManifest = { name: "extensionName", version: "0.0.1", engines: { lens: comparator }};
|
expect(isCompatibleExtension({ name: "extensionName", version: "0.0.1", engines: { lens: comparator }})).toBe(expected);
|
||||||
|
|
||||||
expect(isCompatibleExtension(manifest)).toBe(expected);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -19,19 +19,47 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import semver from "semver";
|
import semver, { SemVer } from "semver";
|
||||||
import { appSemVer, isProduction } from "../common/vars";
|
import { appSemVer, isProduction } from "../common/vars";
|
||||||
import type { LensExtensionManifest } from "./lens-extension";
|
import type { LensExtensionManifest } from "./lens-extension";
|
||||||
|
|
||||||
export function isCompatibleExtension(manifest: LensExtensionManifest): boolean {
|
export function rawIsCompatibleExtension(version: SemVer): (manifest: LensExtensionManifest) => boolean {
|
||||||
if (manifest.engines?.lens) {
|
const { major, minor, patch, prerelease: oldPrelease } = version;
|
||||||
/* include Lens's prerelease tag in the matching so the extension's compatibility is not limited by it */
|
let prerelease = "";
|
||||||
return semver.satisfies(appSemVer, manifest.engines.lens, { includePrerelease: true });
|
|
||||||
|
if (oldPrelease.length > 0) {
|
||||||
|
const [first] = oldPrelease;
|
||||||
|
|
||||||
|
if (first === "alpha" || first === "beta" || first === "rc") {
|
||||||
|
/**
|
||||||
|
* Strip the build IDs and "latest" prerelease tag as that is not really
|
||||||
|
* a part of API version
|
||||||
|
*/
|
||||||
|
prerelease = `-${oldPrelease.slice(0, 2).join(".")}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
/**
|
||||||
|
* We unfortunately have to format as string because the constructor only
|
||||||
|
* takes an instance or a string.
|
||||||
|
*/
|
||||||
|
const strippedVersion = new SemVer(`${major}.${minor}.${patch}${prerelease}`, { includePrerelease: true });
|
||||||
|
|
||||||
|
return (manifest: LensExtensionManifest): boolean => {
|
||||||
|
if (manifest.engines?.lens) {
|
||||||
|
/**
|
||||||
|
* include Lens's prerelease tag in the matching so the extension's
|
||||||
|
* compatibility is not limited by it
|
||||||
|
*/
|
||||||
|
return semver.satisfies(strippedVersion, manifest.engines.lens, { includePrerelease: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isCompatibleExtension = rawIsCompatibleExtension(appSemVer);
|
||||||
|
|
||||||
export function isCompatibleBundledExtension(manifest: LensExtensionManifest): boolean {
|
export function isCompatibleBundledExtension(manifest: LensExtensionManifest): boolean {
|
||||||
return !isProduction || manifest.version === appSemVer.raw;
|
return !isProduction || manifest.version === appSemVer.raw;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -136,11 +136,16 @@ export class ClusterManager extends Singleton {
|
|||||||
entity.spec.metrics.prometheus = prometheus;
|
entity.spec.metrics.prometheus = prometheus;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only set the icon if the preference is set. If the preference is not set
|
|
||||||
// then let the source determine if a cluster has an icon.
|
|
||||||
if (cluster.preferences.icon) {
|
if (cluster.preferences.icon) {
|
||||||
entity.spec.icon ??= {};
|
entity.spec.icon ??= {};
|
||||||
entity.spec.icon.src = cluster.preferences.icon;
|
entity.spec.icon.src = cluster.preferences.icon;
|
||||||
|
} else if (cluster.preferences.icon === null) {
|
||||||
|
/**
|
||||||
|
* NOTE: only clear the icon if set to `null` by ClusterIconSettings.
|
||||||
|
* We can then also clear that value too
|
||||||
|
*/
|
||||||
|
entity.spec.icon = undefined;
|
||||||
|
cluster.preferences.icon = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
catalogEntityRegistry.items.splice(index, 1, entity);
|
catalogEntityRegistry.items.splice(index, 1, entity);
|
||||||
@ -177,7 +182,8 @@ export class ClusterManager extends Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@action syncClustersFromCatalog(entities: KubernetesCluster[]) {
|
@action
|
||||||
|
protected syncClustersFromCatalog(entities: KubernetesCluster[]) {
|
||||||
for (const entity of entities) {
|
for (const entity of entities) {
|
||||||
const cluster = this.store.getById(entity.metadata.uid);
|
const cluster = this.store.getById(entity.metadata.uid);
|
||||||
|
|
||||||
|
|||||||
@ -22,161 +22,229 @@
|
|||||||
import * as tempy from "tempy";
|
import * as tempy from "tempy";
|
||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
import * as yaml from "js-yaml";
|
import * as yaml from "js-yaml";
|
||||||
import { promiseExec } from "../../common/utils/promise-exec";
|
import { promiseExecFile } from "../../common/utils/promise-exec";
|
||||||
import { helmCli } from "./helm-cli";
|
import { helmCli } from "./helm-cli";
|
||||||
import type { Cluster } from "../cluster";
|
|
||||||
import { toCamelCase } from "../../common/utils/camelCase";
|
import { toCamelCase } from "../../common/utils/camelCase";
|
||||||
|
import type { BaseEncodingOptions } from "fs";
|
||||||
|
import { execFile, ExecFileOptions } from "child_process";
|
||||||
|
|
||||||
export async function listReleases(pathToKubeconfig: string, namespace?: string) {
|
async function execHelm(args: string[], options?: BaseEncodingOptions & ExecFileOptions): Promise<string> {
|
||||||
const helm = await helmCli.binaryPath();
|
const helmCliPath = await helmCli.binaryPath();
|
||||||
const namespaceFlag = namespace ? `-n ${namespace}` : "--all-namespaces";
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { stdout } = await promiseExec(`"${helm}" ls --output json ${namespaceFlag} --kubeconfig ${pathToKubeconfig}`);
|
const { stdout } = await promiseExecFile(helmCliPath, args, options);
|
||||||
const output = JSON.parse(stdout);
|
|
||||||
|
|
||||||
if (output.length == 0) {
|
return stdout;
|
||||||
return output;
|
|
||||||
}
|
|
||||||
output.forEach((release: any, index: number) => {
|
|
||||||
output[index] = toCamelCase(release);
|
|
||||||
});
|
|
||||||
|
|
||||||
return output;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error?.stderr || error;
|
throw error?.stderr || error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listReleases(pathToKubeconfig: string, namespace?: string): Promise<Record<string, any>[]> {
|
||||||
|
const args = [
|
||||||
|
"ls",
|
||||||
|
"--output", "json",
|
||||||
|
];
|
||||||
|
|
||||||
export async function installChart(chart: string, values: any, name: string | undefined, namespace: string, version: string, pathToKubeconfig: string) {
|
if (namespace) {
|
||||||
const helm = await helmCli.binaryPath();
|
args.push("-n", namespace);
|
||||||
const fileName = tempy.file({ name: "values.yaml" });
|
} else {
|
||||||
|
args.push("--all-namespaces");
|
||||||
|
}
|
||||||
|
|
||||||
await fse.writeFile(fileName, yaml.dump(values));
|
args.push("--kubeconfig", pathToKubeconfig);
|
||||||
|
|
||||||
|
const output = JSON.parse(await execHelm(args));
|
||||||
|
|
||||||
|
if (!Array.isArray(output) || output.length == 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return output.map(toCamelCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function installChart(chart: string, values: any, name: string | undefined = "", namespace: string, version: string, kubeconfigPath: string) {
|
||||||
|
const valuesFilePath = tempy.file({ name: "values.yaml" });
|
||||||
|
|
||||||
|
await fse.writeFile(valuesFilePath, yaml.dump(values));
|
||||||
|
|
||||||
|
const args = ["install"];
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
args.push(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
args.push(
|
||||||
|
chart,
|
||||||
|
"--version", version,
|
||||||
|
"--values", valuesFilePath,
|
||||||
|
"--namespace", namespace,
|
||||||
|
"--kubeconfig", kubeconfigPath,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
args.push("--generate-name");
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let generateName = "";
|
const output = await execHelm(args);
|
||||||
|
const releaseName = output.split("\n")[0].split(" ")[1].trim();
|
||||||
if (!name) {
|
|
||||||
generateName = "--generate-name";
|
|
||||||
name = "";
|
|
||||||
}
|
|
||||||
const { stdout } = await promiseExec(`"${helm}" install ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} ${generateName}`);
|
|
||||||
const releaseName = stdout.split("\n")[0].split(" ")[1].trim();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
log: stdout,
|
log: output,
|
||||||
release: {
|
release: {
|
||||||
name: releaseName,
|
name: releaseName,
|
||||||
namespace,
|
namespace,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
throw error?.stderr || error;
|
|
||||||
} finally {
|
} finally {
|
||||||
await fse.unlink(fileName);
|
await fse.unlink(valuesFilePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function upgradeRelease(name: string, chart: string, values: any, namespace: string, version: string, cluster: Cluster) {
|
export async function upgradeRelease(name: string, chart: string, values: any, namespace: string, version: string, kubeconfigPath: string, kubectlPath: string) {
|
||||||
const helm = await helmCli.binaryPath();
|
const valuesFilePath = tempy.file({ name: "values.yaml" });
|
||||||
const fileName = tempy.file({ name: "values.yaml" });
|
|
||||||
|
|
||||||
await fse.writeFile(fileName, yaml.dump(values));
|
await fse.writeFile(valuesFilePath, yaml.dump(values));
|
||||||
|
|
||||||
|
const args = [
|
||||||
|
"upgrade",
|
||||||
|
name,
|
||||||
|
chart,
|
||||||
|
"--version", version,
|
||||||
|
"--values", valuesFilePath,
|
||||||
|
"--namespace", namespace,
|
||||||
|
"--kubeconfig", kubeconfigPath,
|
||||||
|
];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
const output = await execHelm(args);
|
||||||
const { stdout } = await promiseExec(`"${helm}" upgrade ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${proxyKubeconfig}`);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
log: stdout,
|
log: output,
|
||||||
release: getRelease(name, namespace, cluster),
|
release: getRelease(name, namespace, kubeconfigPath, kubectlPath),
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
throw error?.stderr || error;
|
|
||||||
} finally {
|
} finally {
|
||||||
await fse.unlink(fileName);
|
await fse.unlink(valuesFilePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getRelease(name: string, namespace: string, cluster: Cluster) {
|
export async function getRelease(name: string, namespace: string, kubeconfigPath: string, kubectlPath: string) {
|
||||||
try {
|
const args = [
|
||||||
const helm = await helmCli.binaryPath();
|
"status",
|
||||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
name,
|
||||||
|
"--namespace", namespace,
|
||||||
|
"--kubeconfig", kubeconfigPath,
|
||||||
|
"--output", "json",
|
||||||
|
];
|
||||||
|
|
||||||
const { stdout } = await promiseExec(`"${helm}" status ${name} --output json --namespace ${namespace} --kubeconfig ${proxyKubeconfig}`, {
|
const release = JSON.parse(await execHelm(args, {
|
||||||
maxBuffer: 32 * 1024 * 1024 * 1024, // 32 MiB
|
maxBuffer: 32 * 1024 * 1024 * 1024, // 32 MiB
|
||||||
});
|
}));
|
||||||
const release = JSON.parse(stdout);
|
|
||||||
|
|
||||||
release.resources = await getResources(name, namespace, cluster);
|
release.resources = await getResources(name, namespace, kubeconfigPath, kubectlPath);
|
||||||
|
|
||||||
return release;
|
return release;
|
||||||
} catch (error) {
|
|
||||||
throw error?.stderr || error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteRelease(name: string, namespace: string, pathToKubeconfig: string) {
|
export async function deleteRelease(name: string, namespace: string, kubeconfigPath: string) {
|
||||||
try {
|
return execHelm([
|
||||||
const helm = await helmCli.binaryPath();
|
"delete",
|
||||||
const { stdout } = await promiseExec(`"${helm}" delete ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
|
name,
|
||||||
|
"--namespace", namespace,
|
||||||
return stdout;
|
"--kubeconfig", kubeconfigPath,
|
||||||
} catch (error) {
|
]);
|
||||||
throw error?.stderr || error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GetValuesOptions {
|
interface GetValuesOptions {
|
||||||
namespace: string;
|
namespace: string;
|
||||||
all?: boolean;
|
all?: boolean;
|
||||||
pathToKubeconfig: string;
|
kubeconfigPath: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getValues(name: string, { namespace, all = false, pathToKubeconfig }: GetValuesOptions) {
|
export async function getValues(name: string, { namespace, all = false, kubeconfigPath }: GetValuesOptions) {
|
||||||
try {
|
const args = [
|
||||||
const helm = await helmCli.binaryPath();
|
"get",
|
||||||
const { stdout } = await promiseExec(`"${helm}" get values ${name} ${all ? "--all" : ""} --output yaml --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
|
"values",
|
||||||
|
name,
|
||||||
|
];
|
||||||
|
|
||||||
return stdout;
|
if (all) {
|
||||||
} catch (error) {
|
args.push("--all");
|
||||||
throw error?.stderr || error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
args.push(
|
||||||
|
"--output", "yaml",
|
||||||
|
"--namespace", namespace,
|
||||||
|
"--kubeconfig", kubeconfigPath,
|
||||||
|
);
|
||||||
|
|
||||||
|
return execHelm(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getHistory(name: string, namespace: string, pathToKubeconfig: string) {
|
export async function getHistory(name: string, namespace: string, kubeconfigPath: string) {
|
||||||
try {
|
return JSON.parse(await execHelm([
|
||||||
const helm = await helmCli.binaryPath();
|
"history",
|
||||||
const { stdout } = await promiseExec(`"${helm}" history ${name} --output json --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
|
name,
|
||||||
|
"--output", "json",
|
||||||
return JSON.parse(stdout);
|
"--namespace", namespace,
|
||||||
} catch (error) {
|
"--kubeconfig", kubeconfigPath,
|
||||||
throw error?.stderr || error;
|
]));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function rollback(name: string, namespace: string, revision: number, pathToKubeconfig: string) {
|
export async function rollback(name: string, namespace: string, revision: number, kubeconfigPath: string) {
|
||||||
try {
|
return JSON.parse(await execHelm([
|
||||||
const helm = await helmCli.binaryPath();
|
"rollback",
|
||||||
const { stdout } = await promiseExec(`"${helm}" rollback ${name} ${revision} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
|
name,
|
||||||
|
"--namespace", namespace,
|
||||||
return stdout;
|
"--kubeconfig", kubeconfigPath,
|
||||||
} catch (error) {
|
]));
|
||||||
throw error?.stderr || error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getResources(name: string, namespace: string, cluster: Cluster) {
|
async function getResources(name: string, namespace: string, kubeconfigPath: string, kubectlPath: string) {
|
||||||
try {
|
const helmArgs = [
|
||||||
const helm = await helmCli.binaryPath();
|
"get",
|
||||||
const kubectl = await cluster.ensureKubectl();
|
"manifest",
|
||||||
const kubectlPath = await kubectl.getPath();
|
name,
|
||||||
const pathToKubeconfig = await cluster.getProxyKubeconfigPath();
|
"--namespace", namespace,
|
||||||
const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectlPath}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`);
|
"--kubeconfig", kubeconfigPath,
|
||||||
|
];
|
||||||
|
const kubectlArgs = [
|
||||||
|
"get",
|
||||||
|
"--namespace", namespace,
|
||||||
|
"--kubeconfig", kubeconfigPath,
|
||||||
|
"-f", "-",
|
||||||
|
"--output", "json",
|
||||||
|
];
|
||||||
|
|
||||||
return JSON.parse(stdout).items;
|
try {
|
||||||
|
const helmOutput = await execHelm(helmArgs);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
const kubectl = execFile(kubectlPath, kubectlArgs);
|
||||||
|
|
||||||
|
kubectl
|
||||||
|
.on("exit", (code, signal) => {
|
||||||
|
if (typeof code === "number") {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve(JSON.parse(stdout).items);
|
||||||
|
} else {
|
||||||
|
reject(stderr);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reject(new Error(`Kubectl exited with signal ${signal}`));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on("error", reject);
|
||||||
|
|
||||||
|
kubectl.stderr.on("data", output => stderr += output);
|
||||||
|
kubectl.stdout.on("data", output => stdout += output);
|
||||||
|
kubectl.stdin.write(helmOutput);
|
||||||
|
kubectl.stdin.end();
|
||||||
|
});
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,13 +20,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
import { readFile } from "fs-extra";
|
import { BaseEncodingOptions, readFile } from "fs-extra";
|
||||||
import { promiseExec } from "../../common/utils/promise-exec";
|
import { promiseExecFile } from "../../common/utils/promise-exec";
|
||||||
import { helmCli } from "./helm-cli";
|
import { helmCli } from "./helm-cli";
|
||||||
import { Singleton } from "../../common/utils/singleton";
|
import { Singleton } from "../../common/utils/singleton";
|
||||||
import { customRequestPromise } from "../../common/request";
|
import { customRequestPromise } from "../../common/request";
|
||||||
import orderBy from "lodash/orderBy";
|
import orderBy from "lodash/orderBy";
|
||||||
import logger from "../logger";
|
import logger from "../logger";
|
||||||
|
import type { ExecFileOptions } from "child_process";
|
||||||
|
|
||||||
export type HelmEnv = Record<string, string> & {
|
export type HelmEnv = Record<string, string> & {
|
||||||
HELM_REPOSITORY_CACHE?: string;
|
HELM_REPOSITORY_CACHE?: string;
|
||||||
@ -49,6 +50,18 @@ export interface HelmRepo {
|
|||||||
password?: string,
|
password?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function execHelm(args: string[], options?: BaseEncodingOptions & ExecFileOptions): Promise<string> {
|
||||||
|
const helmCliPath = await helmCli.binaryPath();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { stdout } = await promiseExecFile(helmCliPath, args, options);
|
||||||
|
|
||||||
|
return stdout;
|
||||||
|
} catch (error) {
|
||||||
|
throw error?.stderr || error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class HelmRepoManager extends Singleton {
|
export class HelmRepoManager extends Singleton {
|
||||||
protected repos: HelmRepo[];
|
protected repos: HelmRepo[];
|
||||||
protected helmEnv: HelmEnv;
|
protected helmEnv: HelmEnv;
|
||||||
@ -77,11 +90,8 @@ export class HelmRepoManager extends Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected static async parseHelmEnv() {
|
protected static async parseHelmEnv() {
|
||||||
const helm = await helmCli.binaryPath();
|
const output = await execHelm(["env"]);
|
||||||
const { stdout } = await promiseExec(`"${helm}" env`).catch((error) => {
|
const lines = output.split(/\r?\n/); // split by new line feed
|
||||||
throw(error.stderr);
|
|
||||||
});
|
|
||||||
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) => {
|
||||||
@ -135,57 +145,73 @@ export class HelmRepoManager extends Singleton {
|
|||||||
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 static async update() {
|
public static async update() {
|
||||||
const helm = await helmCli.binaryPath();
|
return execHelm([
|
||||||
const { stdout } = await promiseExec(`"${helm}" repo update`).catch((error) => {
|
"repo",
|
||||||
return { stdout: error.stdout };
|
"update",
|
||||||
});
|
]);
|
||||||
|
|
||||||
return stdout;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async addRepo({ name, url }: HelmRepo) {
|
public static async addRepo({ name, url }: HelmRepo) {
|
||||||
logger.info(`[HELM]: adding repo "${name}" from ${url}`);
|
logger.info(`[HELM]: adding repo "${name}" from ${url}`);
|
||||||
const helm = await helmCli.binaryPath();
|
|
||||||
const { stdout } = await promiseExec(`"${helm}" repo add ${name} ${url}`).catch((error) => {
|
|
||||||
throw(error.stderr);
|
|
||||||
});
|
|
||||||
|
|
||||||
return stdout;
|
return execHelm([
|
||||||
|
"repo",
|
||||||
|
"add",
|
||||||
|
name,
|
||||||
|
url,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async addCustomRepo(repoAttributes : HelmRepo) {
|
public static async addCustomRepo({ name, url, insecureSkipTlsVerify, username, password, caFile, keyFile, certFile }: HelmRepo) {
|
||||||
logger.info(`[HELM]: adding repo "${repoAttributes.name}" from ${repoAttributes.url}`);
|
logger.info(`[HELM]: adding repo ${name} from ${url}`);
|
||||||
const helm = await helmCli.binaryPath();
|
const args = [
|
||||||
|
"repo",
|
||||||
|
"add",
|
||||||
|
name,
|
||||||
|
url,
|
||||||
|
];
|
||||||
|
|
||||||
const insecureSkipTlsVerify = repoAttributes.insecureSkipTlsVerify ? " --insecure-skip-tls-verify" : "";
|
if (insecureSkipTlsVerify) {
|
||||||
const username = repoAttributes.username ? ` --username "${repoAttributes.username}"` : "";
|
args.push("--insecure-skip-tls-verify");
|
||||||
const password = repoAttributes.password ? ` --password "${repoAttributes.password}"` : "";
|
}
|
||||||
const caFile = repoAttributes.caFile ? ` --ca-file "${repoAttributes.caFile}"` : "";
|
|
||||||
const keyFile = repoAttributes.keyFile ? ` --key-file "${repoAttributes.keyFile}"` : "";
|
|
||||||
const certFile = repoAttributes.certFile ? ` --cert-file "${repoAttributes.certFile}"` : "";
|
|
||||||
|
|
||||||
const addRepoCommand = `"${helm}" repo add ${repoAttributes.name} ${repoAttributes.url}${insecureSkipTlsVerify}${username}${password}${caFile}${keyFile}${certFile}`;
|
if (username) {
|
||||||
const { stdout } = await promiseExec(addRepoCommand).catch((error) => {
|
args.push("--username", username);
|
||||||
throw(error.stderr);
|
}
|
||||||
});
|
|
||||||
|
|
||||||
return stdout;
|
if (password) {
|
||||||
|
args.push("--password", password);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (caFile) {
|
||||||
|
args.push("--ca-file", caFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyFile) {
|
||||||
|
args.push("--key-file", keyFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (certFile) {
|
||||||
|
args.push("--cert-file", certFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
return execHelm(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async removeRepo({ name, url }: HelmRepo): Promise<string> {
|
public static async removeRepo({ name, url }: HelmRepo): Promise<string> {
|
||||||
logger.info(`[HELM]: removing repo "${name}" from ${url}`);
|
logger.info(`[HELM]: removing repo ${name} (${url})`);
|
||||||
const helm = await helmCli.binaryPath();
|
|
||||||
const { stdout } = await promiseExec(`"${helm}" repo remove ${name}`).catch((error) => {
|
|
||||||
throw(error.stderr);
|
|
||||||
});
|
|
||||||
|
|
||||||
return stdout;
|
return execHelm([
|
||||||
|
"repo",
|
||||||
|
"remove",
|
||||||
|
name,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -65,13 +65,19 @@ class HelmService {
|
|||||||
public async listReleases(cluster: Cluster, namespace: string = null) {
|
public async listReleases(cluster: Cluster, namespace: string = null) {
|
||||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||||
|
|
||||||
|
logger.debug("list releases");
|
||||||
|
|
||||||
return listReleases(proxyKubeconfig, namespace);
|
return listReleases(proxyKubeconfig, namespace);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getRelease(cluster: Cluster, releaseName: string, namespace: string) {
|
public async getRelease(cluster: Cluster, releaseName: string, namespace: string) {
|
||||||
|
const kubeconfigPath = await cluster.getProxyKubeconfigPath();
|
||||||
|
const kubectl = await cluster.ensureKubectl();
|
||||||
|
const kubectlPath = await kubectl.getPath();
|
||||||
|
|
||||||
logger.debug("Fetch release");
|
logger.debug("Fetch release");
|
||||||
|
|
||||||
return getRelease(releaseName, namespace, cluster);
|
return getRelease(releaseName, namespace, kubeconfigPath, kubectlPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getReleaseValues(releaseName: string, { cluster, namespace, all }: GetReleaseValuesArgs) {
|
public async getReleaseValues(releaseName: string, { cluster, namespace, all }: GetReleaseValuesArgs) {
|
||||||
@ -79,7 +85,7 @@ class HelmService {
|
|||||||
|
|
||||||
logger.debug("Fetch release values");
|
logger.debug("Fetch release values");
|
||||||
|
|
||||||
return getValues(releaseName, { namespace, all, pathToKubeconfig });
|
return getValues(releaseName, { namespace, all, kubeconfigPath: pathToKubeconfig });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getReleaseHistory(cluster: Cluster, releaseName: string, namespace: string) {
|
public async getReleaseHistory(cluster: Cluster, releaseName: string, namespace: string) {
|
||||||
@ -99,9 +105,13 @@ class HelmService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 }) {
|
||||||
|
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||||
|
const kubectl = await cluster.ensureKubectl();
|
||||||
|
const kubectlPath = await kubectl.getPath();
|
||||||
|
|
||||||
logger.debug("Upgrade release");
|
logger.debug("Upgrade release");
|
||||||
|
|
||||||
return upgradeRelease(releaseName, data.chart, data.values, namespace, data.version, cluster);
|
return upgradeRelease(releaseName, data.chart, data.values, namespace, data.version, proxyKubeconfig, kubectlPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async rollback(cluster: Cluster, releaseName: string, namespace: string, revision: number) {
|
public async rollback(cluster: Cluster, releaseName: string, namespace: string, revision: number) {
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { promiseExec } from "../common/utils/promise-exec";
|
import { promiseExecFile } from "../common/utils/promise-exec";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { ensureDir, pathExists } from "fs-extra";
|
import { ensureDir, pathExists } from "fs-extra";
|
||||||
import * as lockFile from "proper-lockfile";
|
import * as lockFile from "proper-lockfile";
|
||||||
@ -199,7 +199,12 @@ export class Kubectl {
|
|||||||
|
|
||||||
if (exists) {
|
if (exists) {
|
||||||
try {
|
try {
|
||||||
const { stdout } = await promiseExec(`"${path}" version --client=true -o json`);
|
const args = [
|
||||||
|
"version",
|
||||||
|
"--client", "true",
|
||||||
|
"--output", "json",
|
||||||
|
];
|
||||||
|
const { stdout } = await promiseExecFile(path, args);
|
||||||
const output = JSON.parse(stdout);
|
const output = JSON.parse(stdout);
|
||||||
|
|
||||||
if (!checkVersion) {
|
if (!checkVersion) {
|
||||||
|
|||||||
@ -50,6 +50,24 @@ export function isLongRunningRequest(reqUrl: string) {
|
|||||||
return getBoolean(url.searchParams, watchParam) || getBoolean(url.searchParams, followParam);
|
return getBoolean(url.searchParams, watchParam) || getBoolean(url.searchParams, followParam);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the list of ports that chrome considers unsafe to allow HTTP
|
||||||
|
* conntections to. Because they are the standard ports for processes that are
|
||||||
|
* too forgiving in the connection types they accept.
|
||||||
|
*
|
||||||
|
* If we get one of these ports, the easiest thing to do is to just try again.
|
||||||
|
*
|
||||||
|
* Source: https://chromium.googlesource.com/chromium/src.git/+/refs/heads/main/net/base/port_util.cc
|
||||||
|
*/
|
||||||
|
const disallowedPorts = new Set([
|
||||||
|
1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 69, 77, 79,
|
||||||
|
87, 95, 101, 102, 103, 104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 137,
|
||||||
|
139, 143, 161, 179, 389, 427, 465, 512, 513, 514, 515, 526, 530, 531, 532,
|
||||||
|
540, 548, 554, 556, 563, 587, 601, 636, 989, 990, 993, 995, 1719, 1720, 1723,
|
||||||
|
2049, 3659, 4045, 5060, 5061, 6000, 6566, 6665, 6666, 6667, 6668, 6669, 6697,
|
||||||
|
10080,
|
||||||
|
]);
|
||||||
|
|
||||||
export class LensProxy extends Singleton {
|
export class LensProxy extends Singleton {
|
||||||
protected origin: string;
|
protected origin: string;
|
||||||
protected proxyServer: http.Server;
|
protected proxyServer: http.Server;
|
||||||
@ -91,12 +109,13 @@ export class LensProxy extends Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the lens proxy.
|
* Starts to listen on an OS provided port. Will reject if the server throws
|
||||||
* @resolves After the server is listening
|
* an error.
|
||||||
* @rejects if there is an error before that happens
|
*
|
||||||
|
* Resolves with the port number that was picked
|
||||||
*/
|
*/
|
||||||
listen(): Promise<void> {
|
private attemptToListen(): Promise<number> {
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<number>((resolve, reject) => {
|
||||||
this.proxyServer.listen(0, "127.0.0.1");
|
this.proxyServer.listen(0, "127.0.0.1");
|
||||||
|
|
||||||
this.proxyServer
|
this.proxyServer
|
||||||
@ -113,7 +132,7 @@ export class LensProxy extends Singleton {
|
|||||||
|
|
||||||
this.port = port;
|
this.port = port;
|
||||||
appEventBus.emit({ name: "lens-proxy", action: "listen", params: { port }});
|
appEventBus.emit({ name: "lens-proxy", action: "listen", params: { port }});
|
||||||
resolve();
|
resolve(port);
|
||||||
})
|
})
|
||||||
.once("error", (error) => {
|
.once("error", (error) => {
|
||||||
logger.info(`[LENS-PROXY]: Proxy server failed to start: ${error}`);
|
logger.info(`[LENS-PROXY]: Proxy server failed to start: ${error}`);
|
||||||
@ -122,8 +141,40 @@ export class LensProxy extends Singleton {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the lens proxy.
|
||||||
|
* @resolves After the server is listening on a good port
|
||||||
|
* @rejects if there is an error before that happens
|
||||||
|
*/
|
||||||
|
async listen(): Promise<void> {
|
||||||
|
const seenPorts = new Set<number>();
|
||||||
|
|
||||||
|
while(true) {
|
||||||
|
this.proxyServer?.close();
|
||||||
|
const port = await this.attemptToListen();
|
||||||
|
|
||||||
|
if (!disallowedPorts.has(port)) {
|
||||||
|
// We didn't get a port that would result in an ERR_UNSAFE_PORT error, use it
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warn(`[LENS-PROXY]: Proxy server has with port known to be considered unsafe to connect to by chrome, restarting...`);
|
||||||
|
|
||||||
|
if (seenPorts.has(port)) {
|
||||||
|
/**
|
||||||
|
* Assume that if we have seen the port before, then the OS has looped
|
||||||
|
* through all the ports possible and we will not be able to get a safe
|
||||||
|
* port.
|
||||||
|
*/
|
||||||
|
throw new Error("Failed to start LensProxy due to seeing too many unsafe ports. Please restart Lens.");
|
||||||
|
} else {
|
||||||
|
seenPorts.add(port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
logger.info("Closing proxy server");
|
logger.info("[LENS-PROXY]: Closing server");
|
||||||
this.proxyServer.close();
|
this.proxyServer.close();
|
||||||
this.closed = true;
|
this.closed = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export class PrometheusOperator extends PrometheusProvider {
|
|||||||
case "cluster":
|
case "cluster":
|
||||||
switch (queryName) {
|
switch (queryName) {
|
||||||
case "memoryUsage":
|
case "memoryUsage":
|
||||||
return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes))`.replace(/_bytes/g, `_bytes{node=~"${opts.nodes}"}`);
|
return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes))`.replace(/_bytes/g, `_bytes * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"}`);
|
||||||
case "workloadMemoryUsage":
|
case "workloadMemoryUsage":
|
||||||
return `sum(container_memory_working_set_bytes{container!="", instance=~"${opts.nodes}"}) by (component)`;
|
return `sum(container_memory_working_set_bytes{container!="", instance=~"${opts.nodes}"}) by (component)`;
|
||||||
case "memoryRequests":
|
case "memoryRequests":
|
||||||
@ -50,7 +50,7 @@ export class PrometheusOperator extends PrometheusProvider {
|
|||||||
case "memoryAllocatableCapacity":
|
case "memoryAllocatableCapacity":
|
||||||
return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="memory"})`;
|
return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="memory"})`;
|
||||||
case "cpuUsage":
|
case "cpuUsage":
|
||||||
return `sum(rate(node_cpu_seconds_total{node=~"${opts.nodes}", mode=~"user|system"}[${this.rateAccuracy}]))`;
|
return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])* on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`;
|
||||||
case "cpuRequests":
|
case "cpuRequests":
|
||||||
return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"})`;
|
return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"})`;
|
||||||
case "cpuLimits":
|
case "cpuLimits":
|
||||||
@ -66,31 +66,31 @@ export class PrometheusOperator extends PrometheusProvider {
|
|||||||
case "podAllocatableCapacity":
|
case "podAllocatableCapacity":
|
||||||
return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="pods"})`;
|
return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="pods"})`;
|
||||||
case "fsSize":
|
case "fsSize":
|
||||||
return `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`;
|
return `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`;
|
||||||
case "fsUsage":
|
case "fsUsage":
|
||||||
return `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"} - node_filesystem_avail_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`;
|
return `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"} - node_filesystem_avail_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "nodes":
|
case "nodes":
|
||||||
switch (queryName) {
|
switch (queryName) {
|
||||||
case "memoryUsage":
|
case "memoryUsage":
|
||||||
return `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (node)`;
|
return `sum((node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) * on (pod, namespace) group_left(node) kube_pod_info) by (node)`;
|
||||||
case "workloadMemoryUsage":
|
case "workloadMemoryUsage":
|
||||||
return `sum(container_memory_working_set_bytes{container!=""}) by (node)`;
|
return `sum(container_memory_working_set_bytes{container!="POD", container!=""}) by (node)`;
|
||||||
case "memoryCapacity":
|
case "memoryCapacity":
|
||||||
return `sum(kube_node_status_capacity{resource="memory"}) by (node)`;
|
return `sum(kube_node_status_capacity{resource="memory"}) by (node)`;
|
||||||
case "memoryAllocatableCapacity":
|
case "memoryAllocatableCapacity":
|
||||||
return `sum(kube_node_status_allocatable{resource="memory"}) by (node)`;
|
return `sum(kube_node_status_allocatable{resource="memory"}) by (node)`;
|
||||||
case "cpuUsage":
|
case "cpuUsage":
|
||||||
return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])) by(node)`;
|
return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}]) * on (pod, namespace) group_left(node) kube_pod_info) by (node)`;
|
||||||
case "cpuCapacity":
|
case "cpuCapacity":
|
||||||
return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`;
|
return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`;
|
||||||
case "cpuAllocatableCapacity":
|
case "cpuAllocatableCapacity":
|
||||||
return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`;
|
return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`;
|
||||||
case "fsSize":
|
case "fsSize":
|
||||||
return `sum(node_filesystem_size_bytes{mountpoint="/"}) by (node)`;
|
return `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info) by (node)`;
|
||||||
case "fsUsage":
|
case "fsUsage":
|
||||||
return `sum(node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) by (node)`;
|
return `sum((node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) * on (pod, namespace) group_left(node) kube_pod_info) by (node)`;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "pods":
|
case "pods":
|
||||||
|
|||||||
@ -46,7 +46,7 @@ async function loadMetrics(promQueries: string[], cluster: Cluster, prometheusPa
|
|||||||
return await getMetrics(cluster, prometheusPath, { query, ...queryParams });
|
return await getMetrics(cluster, prometheusPath, { query, ...queryParams });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (lastAttempt || (error?.statusCode >= 400 && error?.statusCode < 500)) {
|
if (lastAttempt || (error?.statusCode >= 400 && error?.statusCode < 500)) {
|
||||||
logger.error("[Metrics]: metrics not available", error);
|
logger.error("[Metrics]: metrics not available", error?.response ? error.response?.body : error);
|
||||||
throw new Error("Metrics not available");
|
throw new Error("Metrics not available");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -53,4 +53,19 @@ describe("NetworkPolicyDetails", () => {
|
|||||||
expect(await findByTestId(container, "egress-0")).toBeInstanceOf(HTMLElement);
|
expect(await findByTestId(container, "egress-0")).toBeInstanceOf(HTMLElement);
|
||||||
expect(await findByText(container, "foo: bar")).toBeInstanceOf(HTMLElement);
|
expect(await findByText(container, "foo: bar")).toBeInstanceOf(HTMLElement);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should not crash if egress nodeSelector doesn't have matchLabels", async () => {
|
||||||
|
const spec: NetworkPolicySpec = {
|
||||||
|
egress: [{
|
||||||
|
to: [{
|
||||||
|
namespaceSelector: {},
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
podSelector: {},
|
||||||
|
};
|
||||||
|
const policy = new NetworkPolicy({ metadata: {} as any, spec } as any);
|
||||||
|
const { container } = render(<NetworkPolicyDetails object={policy} />);
|
||||||
|
|
||||||
|
expect(container).toBeInstanceOf(HTMLElement);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -29,4 +29,13 @@
|
|||||||
padding-bottom: 16px;
|
padding-bottom: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ul.policySelectorList {
|
||||||
|
list-style: disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.policySelectorList ul {
|
||||||
|
list-style: circle;
|
||||||
|
list-style-position: inside;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,13 +23,15 @@ import styles from "./network-policy-details.module.css";
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { DrawerItem, DrawerTitle } from "../drawer";
|
import { DrawerItem, DrawerTitle } from "../drawer";
|
||||||
import { IPolicyIpBlock, IPolicySelector, NetworkPolicy, NetworkPolicyPeer, NetworkPolicyPort } from "../../../common/k8s-api/endpoints/network-policy.api";
|
import { IPolicyIpBlock, NetworkPolicy, NetworkPolicyPeer, NetworkPolicyPort } from "../../../common/k8s-api/endpoints/network-policy.api";
|
||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
import { SubTitle } from "../layout/sub-title";
|
import { SubTitle } from "../layout/sub-title";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||||
import { KubeObjectMeta } from "../kube-object-meta";
|
import { KubeObjectMeta } from "../kube-object-meta";
|
||||||
import logger from "../../../common/logger";
|
import logger from "../../../common/logger";
|
||||||
|
import type { LabelMatchExpression, LabelSelector } from "../../../common/k8s-api/kube-object";
|
||||||
|
import { isEmpty } from "lodash";
|
||||||
|
|
||||||
interface Props extends KubeObjectDetailsProps<NetworkPolicy> {
|
interface Props extends KubeObjectDetailsProps<NetworkPolicy> {
|
||||||
}
|
}
|
||||||
@ -60,20 +62,57 @@ export class NetworkPolicyDetails extends React.Component<Props> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderIPolicySelector(name: string, selector: IPolicySelector | undefined) {
|
renderMatchLabels(matchLabels: Record<string, string | undefined> | undefined) {
|
||||||
|
if (!matchLabels) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.entries(matchLabels)
|
||||||
|
.map(([key, value]) => <li key={key}>{key}: {value}</li>);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMatchExpressions(matchExpressions: LabelMatchExpression[] | undefined) {
|
||||||
|
if (!matchExpressions) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchExpressions.map(expr => {
|
||||||
|
switch (expr.operator) {
|
||||||
|
case "DoesNotExist":
|
||||||
|
case "Exists":
|
||||||
|
return <li key={expr.key}>{expr.key} ({expr.operator})</li>;
|
||||||
|
case "In":
|
||||||
|
case "NotIn":
|
||||||
|
return (
|
||||||
|
<li key={expr.key}>
|
||||||
|
{expr.key}({expr.operator})
|
||||||
|
<ul>
|
||||||
|
{expr.values.map((value, index) => <li key={index}>{value}</li>)}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderIPolicySelector(name: string, selector: LabelSelector | undefined) {
|
||||||
if (!selector) {
|
if (!selector) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { matchLabels, matchExpressions } = selector;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DrawerItem name={name}>
|
<DrawerItem name={name}>
|
||||||
{
|
<ul className={styles.policySelectorList}>
|
||||||
Object
|
{this.renderMatchLabels(matchLabels)}
|
||||||
.entries(selector.matchLabels)
|
{this.renderMatchExpressions(matchExpressions)}
|
||||||
.map(data => data.join(": "))
|
{
|
||||||
.join(", ")
|
(isEmpty(matchLabels) && isEmpty(matchExpressions)) && (
|
||||||
|| "(empty)"
|
<li>(empty)</li>
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
</DrawerItem>
|
</DrawerItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -121,7 +121,7 @@ export class AddHelmRepoDialog extends React.Component<Props> {
|
|||||||
<div className="flex gaps align-center">
|
<div className="flex gaps align-center">
|
||||||
<Input
|
<Input
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
validators = {isPath}
|
validators={isPath}
|
||||||
className="box grow"
|
className="box grow"
|
||||||
value={this.getFilePath(fileType)}
|
value={this.getFilePath(fileType)}
|
||||||
onChange={v => this.setFilepath(fileType, v)}
|
onChange={v => this.setFilepath(fileType, v)}
|
||||||
@ -172,7 +172,7 @@ export class AddHelmRepoDialog extends React.Component<Props> {
|
|||||||
close={this.close}
|
close={this.close}
|
||||||
>
|
>
|
||||||
<Wizard header={header} done={this.close}>
|
<Wizard header={header} done={this.close}>
|
||||||
<WizardStep contentClass="flow column" nextLabel="Add" next={()=>{this.addCustomRepo();}}>
|
<WizardStep contentClass="flow column" nextLabel="Add" next={() => this.addCustomRepo()}>
|
||||||
<div className="flex column gaps">
|
<div className="flex column gaps">
|
||||||
<Input
|
<Input
|
||||||
autoFocus required
|
autoFocus required
|
||||||
|
|||||||
@ -1,88 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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 "./clipboard.scss";
|
|
||||||
import React from "react";
|
|
||||||
import { findDOMNode } from "react-dom";
|
|
||||||
import { boundMethod } from "../../../common/utils";
|
|
||||||
import { Notifications } from "../notifications";
|
|
||||||
import { copyToClipboard } from "../../utils/copyToClipboard";
|
|
||||||
import logger from "../../../main/logger";
|
|
||||||
import { cssNames } from "../../utils";
|
|
||||||
|
|
||||||
export interface CopyToClipboardProps {
|
|
||||||
resetSelection?: boolean;
|
|
||||||
showNotification?: boolean;
|
|
||||||
cssSelectorLimit?: string; // allows to copy partial content with css-selector in children-element context
|
|
||||||
getNotificationMessage?(copiedText: string): React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const defaultProps: Partial<CopyToClipboardProps> = {
|
|
||||||
getNotificationMessage(copiedText: string) {
|
|
||||||
return <p>Copied to clipboard: <em>{copiedText}</em></p>;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export class Clipboard extends React.Component<CopyToClipboardProps> {
|
|
||||||
static displayName = "Clipboard";
|
|
||||||
static defaultProps = defaultProps as object;
|
|
||||||
|
|
||||||
get rootElem(): HTMLElement {
|
|
||||||
// eslint-disable-next-line react/no-find-dom-node
|
|
||||||
return findDOMNode(this) as HTMLElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
get rootReactElem(): React.ReactElement<React.HTMLProps<any>> {
|
|
||||||
return React.Children.only(this.props.children) as React.ReactElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
@boundMethod
|
|
||||||
onClick(evt: React.MouseEvent) {
|
|
||||||
if (this.rootReactElem.props.onClick) {
|
|
||||||
this.rootReactElem.props.onClick(evt); // pass event to children-root-element if any
|
|
||||||
}
|
|
||||||
const { showNotification, resetSelection, getNotificationMessage, cssSelectorLimit } = this.props;
|
|
||||||
const contentElem = this.rootElem.querySelector<any>(cssSelectorLimit) || this.rootElem;
|
|
||||||
|
|
||||||
if (contentElem) {
|
|
||||||
const { copiedText, copied } = copyToClipboard(contentElem, { resetSelection });
|
|
||||||
|
|
||||||
if (copied && showNotification) {
|
|
||||||
Notifications.ok(getNotificationMessage(copiedText));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
try {
|
|
||||||
const rootElem = this.rootReactElem;
|
|
||||||
|
|
||||||
return React.cloneElement(rootElem, {
|
|
||||||
className: cssNames(Clipboard.displayName, rootElem.props.className),
|
|
||||||
onClick: this.onClick,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`Invalid usage components/CopyToClick usage. Children must contain root html element.`, { err: String(err) });
|
|
||||||
|
|
||||||
return this.rootReactElem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -65,7 +65,11 @@ export class ClusterIconSetting extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clearIcon() {
|
clearIcon() {
|
||||||
this.props.cluster.preferences.icon = undefined;
|
/**
|
||||||
|
* NOTE: this needs to be `null` rather than `undefined` so that we can
|
||||||
|
* tell the difference between it not being there and being cleared.
|
||||||
|
*/
|
||||||
|
this.props.cluster.preferences.icon = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
|
|||||||
@ -24,10 +24,10 @@ import "./logs-dialog.scss";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Dialog, DialogProps } from "../dialog";
|
import { Dialog, DialogProps } from "../dialog";
|
||||||
import { Wizard, WizardStep } from "../wizard";
|
import { Wizard, WizardStep } from "../wizard";
|
||||||
import { copyToClipboard } from "../../utils";
|
|
||||||
import { Notifications } from "../notifications";
|
import { Notifications } from "../notifications";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
|
import { clipboard } from "electron";
|
||||||
|
|
||||||
// todo: make as external BrowserWindow (?)
|
// todo: make as external BrowserWindow (?)
|
||||||
|
|
||||||
@ -40,9 +40,8 @@ export class LogsDialog extends React.Component<Props> {
|
|||||||
public logsElem: HTMLElement;
|
public logsElem: HTMLElement;
|
||||||
|
|
||||||
copyToClipboard = () => {
|
copyToClipboard = () => {
|
||||||
if (copyToClipboard(this.logsElem)) {
|
clipboard.writeText(this.props.logs);
|
||||||
Notifications.ok(`Logs copied to clipboard.`);
|
Notifications.ok(`Logs copied to clipboard.`);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@ -25,7 +25,7 @@ import { makeObservable, observable } from "mobx";
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
import type { ServiceAccount } from "../../../common/k8s-api/endpoints";
|
import type { ServiceAccount } from "../../../common/k8s-api/endpoints";
|
||||||
import { copyToClipboard, saveFileDialog } from "../../utils";
|
import { saveFileDialog } from "../../utils";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { Dialog, DialogProps } from "../dialog";
|
import { Dialog, DialogProps } from "../dialog";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
@ -33,6 +33,7 @@ import { Notifications } from "../notifications";
|
|||||||
import { Wizard, WizardStep } from "../wizard";
|
import { Wizard, WizardStep } from "../wizard";
|
||||||
import { apiBase } from "../../api";
|
import { apiBase } from "../../api";
|
||||||
import { MonacoEditor } from "../monaco-editor";
|
import { MonacoEditor } from "../monaco-editor";
|
||||||
|
import { clipboard } from "electron";
|
||||||
|
|
||||||
interface IKubeconfigDialogData {
|
interface IKubeconfigDialogData {
|
||||||
title?: React.ReactNode;
|
title?: React.ReactNode;
|
||||||
@ -49,7 +50,6 @@ const dialogState = observable.object({
|
|||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class KubeConfigDialog extends React.Component<Props> {
|
export class KubeConfigDialog extends React.Component<Props> {
|
||||||
@observable.ref configTextArea: HTMLTextAreaElement; // required for coping config text
|
|
||||||
@observable config = ""; // parsed kubeconfig in yaml format
|
@observable config = ""; // parsed kubeconfig in yaml format
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
@ -89,9 +89,8 @@ export class KubeConfigDialog extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
copyToClipboard = () => {
|
copyToClipboard = () => {
|
||||||
if (this.config && copyToClipboard(this.configTextArea)) {
|
clipboard.writeText(this.config);
|
||||||
Notifications.ok("Config copied to clipboard");
|
Notifications.ok("Config copied to clipboard");
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
download = () => {
|
download = () => {
|
||||||
@ -131,11 +130,6 @@ export class KubeConfigDialog extends React.Component<Props> {
|
|||||||
className={styles.editor}
|
className={styles.editor}
|
||||||
value={yamlConfig}
|
value={yamlConfig}
|
||||||
/>
|
/>
|
||||||
<textarea
|
|
||||||
className={styles.configCopy}
|
|
||||||
readOnly defaultValue={yamlConfig}
|
|
||||||
ref={e => this.configTextArea = e}
|
|
||||||
/>
|
|
||||||
</WizardStep>
|
</WizardStep>
|
||||||
</Wizard>
|
</Wizard>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@ -19,4 +19,36 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./clipboard";
|
.SettingsCloseButton {
|
||||||
|
.closeIcon {
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid var(--textColorDimmed);
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #72767d25;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Icon {
|
||||||
|
color: var(--textColorAccent);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.escLabel {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: var(--margin);
|
||||||
|
font-weight: bold;
|
||||||
|
user-select: none;
|
||||||
|
color: var(--textColorDimmed);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -19,6 +19,23 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.Clipboard {
|
import "./close-button.scss";
|
||||||
cursor: pointer;
|
|
||||||
}
|
import React, { HTMLAttributes } from "react";
|
||||||
|
import { Icon } from "../icon";
|
||||||
|
|
||||||
|
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CloseButton(props: Props) {
|
||||||
|
return (
|
||||||
|
<div className="SettingsCloseButton" {...props}>
|
||||||
|
<div className="closeIcon" role="button" aria-label="Close">
|
||||||
|
<Icon material="close" />
|
||||||
|
</div>
|
||||||
|
<div className="escLabel" aria-hidden="true">
|
||||||
|
ESC
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -142,41 +142,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
> .toolsRegion {
|
> .toolsRegion {
|
||||||
.fixedTools {
|
width: 45px;
|
||||||
position: fixed;
|
|
||||||
top: 60px;
|
|
||||||
|
|
||||||
.closeBtn {
|
|
||||||
width: 35px;
|
|
||||||
height: 35px;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
border: 2px solid var(--textColorDimmed);
|
|
||||||
border-radius: 50%;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: #72767d4d;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
transform: translateY(1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.Icon {
|
|
||||||
color: var(--textColorTertiary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.esc {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 4px;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--textColorDimmed);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -25,7 +25,7 @@ import React from "react";
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { cssNames, IClassName } from "../../utils";
|
import { cssNames, IClassName } from "../../utils";
|
||||||
import { navigation } from "../../navigation";
|
import { navigation } from "../../navigation";
|
||||||
import { Icon } from "../icon";
|
import { CloseButton } from "./close-button";
|
||||||
|
|
||||||
export interface SettingLayoutProps extends React.DOMAttributes<any> {
|
export interface SettingLayoutProps extends React.DOMAttributes<any> {
|
||||||
className?: IClassName;
|
className?: IClassName;
|
||||||
@ -97,13 +97,8 @@ export class SettingLayout extends React.Component<SettingLayoutProps> {
|
|||||||
<div className="toolsRegion">
|
<div className="toolsRegion">
|
||||||
{
|
{
|
||||||
this.props.provideBackButtonNavigation && (
|
this.props.provideBackButtonNavigation && (
|
||||||
<div className="fixedTools">
|
<div className="fixed top-[60px]">
|
||||||
<div className="closeBtn" role="button" aria-label="Close" onClick={back}>
|
<CloseButton onClick={back}/>
|
||||||
<Icon material="close" />
|
|
||||||
</div>
|
|
||||||
<div className="esc" aria-hidden="true">
|
|
||||||
ESC
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -228,10 +228,15 @@ html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.Select {
|
.Select {
|
||||||
|
&__value-container {
|
||||||
|
margin-top: 2px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
&__control {
|
&__control {
|
||||||
box-shadow: 0 0 0 1px var(--inputControlBorder);
|
box-shadow: 0 0 0 1px var(--inputControlBorder);
|
||||||
background: var(--inputControlBackground);
|
background: var(--inputControlBackground);
|
||||||
border-radius: 5px;
|
border-radius: var(--border-radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__single-value {
|
&__single-value {
|
||||||
|
|||||||
@ -1,52 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Helper for selecting element's text content and copy in clipboard
|
|
||||||
|
|
||||||
export function copyToClipboard(elem: HTMLElement, { resetSelection = true } = {}) {
|
|
||||||
let clearSelection: () => void;
|
|
||||||
|
|
||||||
if (isSelectable(elem)) {
|
|
||||||
elem.select();
|
|
||||||
clearSelection = () => elem.setSelectionRange(0, 0);
|
|
||||||
} else {
|
|
||||||
const selection = window.getSelection();
|
|
||||||
|
|
||||||
selection.selectAllChildren(elem);
|
|
||||||
clearSelection = () => selection.removeAllRanges();
|
|
||||||
}
|
|
||||||
const selectedText = document.getSelection().toString();
|
|
||||||
const isCopied = document.execCommand("copy");
|
|
||||||
|
|
||||||
if (resetSelection) {
|
|
||||||
clearSelection();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
copied: isCopied,
|
|
||||||
copiedText: selectedText,
|
|
||||||
clearSelection,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSelectable(elem: HTMLElement): elem is HTMLInputElement {
|
|
||||||
return !!(elem as HTMLInputElement).select;
|
|
||||||
}
|
|
||||||
@ -24,7 +24,6 @@
|
|||||||
export * from "../../common/utils";
|
export * from "../../common/utils";
|
||||||
export * from "../../common/event-emitter";
|
export * from "../../common/event-emitter";
|
||||||
|
|
||||||
export * from "./copyToClipboard";
|
|
||||||
export * from "./createStorage";
|
export * from "./createStorage";
|
||||||
export * from "./cssNames";
|
export * from "./cssNames";
|
||||||
export * from "./cssVar";
|
export * from "./cssVar";
|
||||||
|
|||||||
@ -27,7 +27,14 @@ module.exports = {
|
|||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ["Roboto", "Helvetica", "Arial", "sans-serif"],
|
sans: ["Roboto", "Helvetica", "Arial", "sans-serif"],
|
||||||
},
|
},
|
||||||
extend: {},
|
extend: {
|
||||||
|
colors: {
|
||||||
|
textAccent: "var(--textColorAccent)",
|
||||||
|
textPrimary: "var(--textColorPrimary)",
|
||||||
|
textTertiary: "var(--textColorTertiary)",
|
||||||
|
textDimmed: "var(--textColorDimmed)",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
extend: {},
|
extend: {},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user