diff --git a/package.json b/package.json index 8944c663c2..4dcf5a5c16 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "productName": "OpenLens", "description": "OpenLens - Open Source IDE for Kubernetes", "homepage": "https://github.com/lensapp/lens", - "version": "5.3.2", + "version": "5.3.3", "main": "static/build/main.js", "copyright": "© 2021 OpenLens Authors", "license": "MIT", @@ -131,6 +131,11 @@ } ] }, + "rpm": { + "fpm": [ + "--rpm-rpmbuild-define=%define _build_id_links none" + ] + }, "mac": { "hardenedRuntime": true, "gatekeeperAssess": false, @@ -380,7 +385,6 @@ "webpack-cli": "^3.3.12", "webpack-dev-server": "^3.11.2", "webpack-node-externals": "^1.7.2", - "what-input": "^5.2.10", "xterm": "^4.14.1", "xterm-addon-fit": "^0.5.0" } diff --git a/src/common/app-paths.ts b/src/common/app-paths.ts index 883b7053a4..d23cc8c1a4 100644 --- a/src/common/app-paths.ts +++ b/src/common/app-paths.ts @@ -78,7 +78,17 @@ export class AppPaths { app.setPath("userData", path.join(app.getPath("appData"), app.getName())); - AppPaths.paths.set(fromEntries(pathNames.map(pathName => [pathName, app.getPath(pathName)]))); + const getPath = (pathName: PathName) => { + try { + return app.getPath(pathName); + } catch { + logger.debug(`[APP-PATHS] No path found for ${pathName}`); + + return ""; + } + }; + + AppPaths.paths.set(fromEntries(pathNames.map(pathName => [pathName, getPath(pathName)] as const).filter(([, path]) => path))); ipcMain.handle(AppPaths.ipcChannel, () => toJS(AppPaths.paths.get())); } diff --git a/src/common/k8s-api/__tests__/kube-api.test.ts b/src/common/k8s-api/__tests__/kube-api.test.ts index 2ae2015437..23bd9155cb 100644 --- a/src/common/k8s-api/__tests__/kube-api.test.ts +++ b/src/common/k8s-api/__tests__/kube-api.test.ts @@ -26,6 +26,12 @@ import { KubeObject } from "../kube-object"; import AbortController from "abort-controller"; import { delay } from "../../utils/delay"; import { PassThrough } from "stream"; +import { ApiManager, apiManager } from "../api-manager"; +import { Ingress, Pod } from "../endpoints"; + +jest.mock("../api-manager"); + +const mockApiManager = apiManager as jest.Mocked; class TestKubeObject extends KubeObject { static kind = "Pod"; @@ -33,7 +39,11 @@ class TestKubeObject extends KubeObject { static apiBase = "/api/v1/pods"; } -class TestKubeApi extends KubeApi { } +class TestKubeApi extends KubeApi { + public async checkPreferredVersion() { + return super.checkPreferredVersion(); + } +} describe("forRemoteCluster", () => { it("builds api client for KubeObject", async () => { @@ -184,6 +194,94 @@ describe("KubeApi", () => { expect(kubeApi.apiGroup).toEqual("extensions"); }); + describe("checkPreferredVersion", () => { + it("registers with apiManager if checkPreferredVersion changes apiVersionPreferred", async () => { + expect.hasAssertions(); + + const api = new TestKubeApi({ + objectConstructor: Ingress, + checkPreferredVersion: true, + fallbackApiBases: ["/apis/extensions/v1beta1/ingresses"], + request: { + get: jest.fn() + .mockImplementationOnce((path: string) => { + expect(path).toBe("/apis/networking.k8s.io/v1"); + + throw new Error("no"); + }) + .mockImplementationOnce((path: string) => { + expect(path).toBe("/apis/extensions/v1beta1"); + + return { + resources: [ + { + name: "ingresses", + }, + ], + }; + }) + .mockImplementationOnce((path: string) => { + expect(path).toBe("/apis/extensions"); + + return { + preferredVersion: { + version: "v1beta1", + }, + }; + }), + } as any, + }); + + await api.checkPreferredVersion(); + + expect(api.apiVersionPreferred).toBe("v1beta1"); + expect(mockApiManager.registerApi).toBeCalledWith("/apis/extensions/v1beta1/ingresses", expect.anything()); + }); + + it("registers with apiManager if checkPreferredVersion changes apiVersionPreferred with non-grouped apis", async () => { + expect.hasAssertions(); + + const api = new TestKubeApi({ + objectConstructor: Pod, + checkPreferredVersion: true, + fallbackApiBases: ["/api/v1beta1/pods"], + request: { + get: jest.fn() + .mockImplementationOnce((path: string) => { + expect(path).toBe("/api/v1"); + + throw new Error("no"); + }) + .mockImplementationOnce((path: string) => { + expect(path).toBe("/api/v1beta1"); + + return { + resources: [ + { + name: "pods", + }, + ], + }; + }) + .mockImplementationOnce((path: string) => { + expect(path).toBe("/api"); + + return { + preferredVersion: { + version: "v1beta1", + }, + }; + }), + } as any, + }); + + await api.checkPreferredVersion(); + + expect(api.apiVersionPreferred).toBe("v1beta1"); + expect(mockApiManager.registerApi).toBeCalledWith("/api/v1beta1/pods", expect.anything()); + }); + }); + describe("patch", () => { let api: TestKubeApi; diff --git a/src/common/k8s-api/kube-api-parse.ts b/src/common/k8s-api/kube-api-parse.ts index 57c835ed6b..a5d137be02 100644 --- a/src/common/k8s-api/kube-api-parse.ts +++ b/src/common/k8s-api/kube-api-parse.ts @@ -37,7 +37,7 @@ export interface IKubeApiLinkRef { apiPrefix?: string; apiVersion: string; resource: string; - name: string; + name?: string; namespace?: string; } @@ -145,15 +145,18 @@ function _parseKubeApi(path: string): IKubeApiParsed { }; } -export function createKubeApiURL(ref: IKubeApiLinkRef): string { - const { apiPrefix = "/apis", resource, apiVersion, name } = ref; - let { namespace } = ref; +export function createKubeApiURL({ apiPrefix = "/apis", resource, apiVersion, name, namespace }: IKubeApiLinkRef): string { + const parts = [apiPrefix, apiVersion]; if (namespace) { - namespace = `namespaces/${namespace}`; + parts.push("namespaces", namespace); } - return [apiPrefix, apiVersion, namespace, resource, name] - .filter(v => v) - .join("/"); + parts.push(resource); + + if (name) { + parts.push(name); + } + + return parts.join("/"); } diff --git a/src/common/k8s-api/kube-api.ts b/src/common/k8s-api/kube-api.ts index 33cecf3db1..9304cb1485 100644 --- a/src/common/k8s-api/kube-api.ts +++ b/src/common/k8s-api/kube-api.ts @@ -38,9 +38,14 @@ import AbortController from "abort-controller"; import { Agent, AgentOptions } from "https"; import type { Patch } from "rfc6902"; +/** + * The options used for creating a `KubeApi` + */ export interface IKubeApiOptions { /** * base api-path for listing all resources, e.g. "/api/v1/pods" + * + * If not specified then will be the one on the `objectConstructor` */ apiBase?: string; @@ -52,11 +57,33 @@ export interface IKubeApiOptions { */ fallbackApiBases?: string[]; - objectConstructor: KubeObjectConstructor; - request?: KubeJsonApi; - isNamespaced?: boolean; - kind?: string; + /** + * If `true` then will check all declared apiBases against the kube api server + * for the first accepted one. + */ checkPreferredVersion?: boolean; + + /** + * The constructor for the kube objects returned from the API + */ + objectConstructor: KubeObjectConstructor; + + /** + * The api instance to use for making requests + * + * @default apiKube + */ + request?: KubeJsonApi; + + /** + * @deprecated should be specified by `objectConstructor` + */ + isNamespaced?: boolean; + + /** + * @deprecated should be specified by `objectConstructor` + */ + kind?: string; } export interface IKubeApiQueryParams { @@ -249,11 +276,11 @@ export interface DeleteResourceDescriptor extends ResourceDescriptor { export class KubeApi { readonly kind: string; - readonly apiBase: string; - readonly apiPrefix: string; - readonly apiGroup: string; readonly apiVersion: string; - readonly apiVersionPreferred?: string; + apiBase: string; + apiPrefix: string; + apiGroup: string; + apiVersionPreferred?: string; readonly apiResource: string; readonly isNamespaced: boolean; @@ -264,23 +291,18 @@ export class KubeApi { private watchId = 1; constructor(protected options: IKubeApiOptions) { - const { - objectConstructor, - request = apiKube, - kind = options.objectConstructor?.kind, - isNamespaced = options.objectConstructor?.namespaced, - } = options || {}; - + const { objectConstructor, request, kind, isNamespaced } = options; const { apiBase, apiPrefix, apiGroup, apiVersion, resource } = parseKubeApi(options.apiBase || objectConstructor.apiBase); - this.kind = kind; - this.isNamespaced = isNamespaced; + this.options = options; + this.kind = kind ?? objectConstructor.kind; + this.isNamespaced = isNamespaced ?? objectConstructor.namespaced ?? false; this.apiBase = apiBase; this.apiPrefix = apiPrefix; this.apiGroup = apiGroup; this.apiVersion = apiVersion; this.apiResource = resource; - this.request = request; + this.request = request ?? apiKube; this.objectConstructor = objectConstructor; this.parseResponse = this.parseResponse.bind(this); @@ -353,21 +375,16 @@ export class KubeApi { const { apiPrefix, apiGroup } = await this.getPreferredVersionPrefixGroup(); // The apiPrefix and apiGroup might change due to fallbackApiBases, so we must override them - Object.defineProperty(this, "apiPrefix", { - value: apiPrefix, - }); - Object.defineProperty(this, "apiGroup", { - value: apiGroup, - }); + this.apiPrefix = apiPrefix; + this.apiGroup = apiGroup; - const res = await this.request.get(`${this.apiPrefix}/${this.apiGroup}`); + const url = [apiPrefix, apiGroup].filter(Boolean).join("/"); + const res = await this.request.get(url); - Object.defineProperty(this, "apiVersionPreferred", { - value: res?.preferredVersion?.version ?? null, - }); + this.apiVersionPreferred = res?.preferredVersion?.version ?? null; if (this.apiVersionPreferred) { - Object.defineProperty(this, "apiBase", { value: this.getUrl() }); + this.apiBase = this.computeApiBase(); apiManager.registerApi(this.apiBase, this); } } @@ -385,7 +402,15 @@ export class KubeApi { return this.list(params, { limit: 1 }); } - getUrl({ name, namespace = "default" }: Partial = {}, query?: Partial) { + private computeApiBase(): string { + return createKubeApiURL({ + apiPrefix: this.apiPrefix, + apiVersion: this.apiVersionWithGroup, + resource: this.apiResource, + }); + } + + getUrl({ name, namespace }: Partial = {}, query?: Partial) { const resourcePath = createKubeApiURL({ apiPrefix: this.apiPrefix, apiVersion: this.apiVersionWithGroup, diff --git a/src/main/index.ts b/src/main/index.ts index e5b7f3d9f2..1c5b80042c 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -90,15 +90,23 @@ if (process.env.LENS_DISABLE_GPU) { app.disableHardwareAcceleration(); } +logger.debug("[APP-MAIN] initializing remote"); initializeRemote(); + +logger.debug("[APP-MAIN] configuring packages"); configurePackages(); + mangleProxyEnv(); + +logger.debug("[APP-MAIN] initializing ipc main handlers"); initializers.initIpcMainHandlers(); if (app.commandLine.getSwitchValue("proxy-server") !== "") { process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server"); } +logger.debug("[APP-MAIN] Lens protocol routing main"); + if (!app.requestSingleInstanceLock()) { app.exit(); } else { @@ -112,6 +120,8 @@ if (!app.requestSingleInstanceLock()) { } app.on("second-instance", (event, argv) => { + logger.debug("second-instance message"); + const lprm = LensProtocolRouterMain.createInstance(); for (const arg of argv) { @@ -295,6 +305,8 @@ autoUpdater.on("before-quit-for-update", () => { }); app.on("will-quit", (event) => { + logger.debug("will-quit message"); + // This is called when the close button of the main window is clicked const lprm = LensProtocolRouterMain.getInstance(false); @@ -324,6 +336,8 @@ app.on("will-quit", (event) => { }); app.on("open-url", (event, rawUrl) => { + logger.debug("open-url message"); + // lens:// protocol handler event.preventDefault(); LensProtocolRouterMain.getInstance().route(rawUrl); @@ -343,3 +357,5 @@ export { Mobx, LensExtensions, }; + +logger.debug("[APP-MAIN] waiting for 'ready' and other messages"); diff --git a/src/main/shell-session/shell-session.ts b/src/main/shell-session/shell-session.ts index b171986d56..677a637cc0 100644 --- a/src/main/shell-session/shell-session.ts +++ b/src/main/shell-session/shell-session.ts @@ -158,6 +158,8 @@ export abstract class ShellSession { cwd, env, name: "xterm-256color", + // TODO: Something else is broken here so we need to force the use of winPty on windows + useConpty: false, })); } diff --git a/src/renderer/cluster-frame.tsx b/src/renderer/cluster-frame.tsx index 734e6c8486..b5ea54dad0 100755 --- a/src/renderer/cluster-frame.tsx +++ b/src/renderer/cluster-frame.tsx @@ -38,7 +38,6 @@ import { ClusterPageRegistry, getExtensionPageUrl } from "../extensions/registri import { ExtensionLoader } from "../extensions/extension-loader"; import { appEventBus } from "../common/event-bus"; import { requestMain } from "../common/ipc"; -import whatInput from "what-input"; import { clusterSetFrameIdHandler } from "../common/cluster-ipc"; import { ClusterPageMenuRegistration, ClusterPageMenuRegistry } from "../extensions/registries"; import { StatefulSetScaleDialog } from "./components/+workloads-statefulsets/statefulset-scale-dialog"; @@ -122,8 +121,6 @@ export class ClusterFrame extends React.Component { unmountComponentAtNode(rootElem); }; - whatInput.ask(); // Start to monitor user input device - const clusterContext = new FrameContext(cluster); // Setup hosted cluster context diff --git a/src/renderer/components/+custom-resources/crd-resources.tsx b/src/renderer/components/+custom-resources/crd-resources.tsx index d0eb4779c7..1c8f55aaab 100644 --- a/src/renderer/components/+custom-resources/crd-resources.tsx +++ b/src/renderer/components/+custom-resources/crd-resources.tsx @@ -57,9 +57,7 @@ export class CrdResources extends React.Component { } @computed get store() { - if (!this.crd) return null; - - return apiManager.getStore(this.crd.getResourceApiBase()); + return apiManager.getStore(this.crd?.getResourceApiBase()); } render() { diff --git a/src/renderer/components/+custom-resources/crd.store.ts b/src/renderer/components/+custom-resources/crd.store.ts index 83c8b5cdb6..2cdcbee55b 100644 --- a/src/renderer/components/+custom-resources/crd.store.ts +++ b/src/renderer/components/+custom-resources/crd.store.ts @@ -29,15 +29,14 @@ import { CRDResourceStore } from "./crd-resource.store"; import { KubeObject } from "../../../common/k8s-api/kube-object"; function initStore(crd: CustomResourceDefinition) { - const apiBase = crd.getResourceApiBase(); - const kind = crd.getResourceKind(); - const isNamespaced = crd.isNamespaced(); - const api = apiManager.getApi(apiBase) ?? new KubeApi({ - objectConstructor: KubeObject, - apiBase, - kind, - isNamespaced, - }); + const objectConstructor = class extends KubeObject { + static readonly kind = crd.getResourceKind(); + static readonly namespaced = crd.isNamespaced(); + static readonly apiBase = crd.getResourceApiBase(); + }; + + const api = apiManager.getApi(objectConstructor.apiBase) + ?? new KubeApi({ objectConstructor }); if (!apiManager.getStore(api)) { apiManager.registerStore(new CRDResourceStore(api)); diff --git a/src/renderer/components/cluster-manager/cluster-status.tsx b/src/renderer/components/cluster-manager/cluster-status.tsx index e926a75d84..79df15c412 100644 --- a/src/renderer/components/cluster-manager/cluster-status.tsx +++ b/src/renderer/components/cluster-manager/cluster-status.tsx @@ -153,7 +153,7 @@ export class ClusterStatus extends React.Component { return (
-

{this.entity.getName()}

+

{this.entity?.getName() ?? this.cluster.name}

{this.renderStatusIcon()} {this.renderAuthenticationOutput()} {this.renderReconnectionHelp()} diff --git a/src/renderer/components/icon/icon.scss b/src/renderer/components/icon/icon.scss index 49d4b9c192..a1fcab8a31 100644 --- a/src/renderer/components/icon/icon.scss +++ b/src/renderer/components/icon/icon.scss @@ -127,7 +127,7 @@ } &.active { - color: var(--color-active); + color: var(--textColorAccent); box-shadow: 0 0 0 2px var(--iconActiveBackground); background-color: var(--iconActiveBackground); } @@ -137,16 +137,8 @@ transition: 250ms color, 250ms opacity, 150ms background-color, 150ms box-shadow; border-radius: var(--border-radius); - &.focusable:focus:not(:hover) { + &.focusable:focus-visible { box-shadow: 0 0 0 2px var(--focus-color); - - [data-whatintent='mouse'] & { - box-shadow: none; - - &.active { - box-shadow: 0 0 0 2px var(--iconActiveBackground); - } - } } &:hover { diff --git a/src/renderer/components/layout/sidebar-cluster.module.css b/src/renderer/components/layout/sidebar-cluster.module.css index efe4455169..635443500d 100644 --- a/src/renderer/components/layout/sidebar-cluster.module.css +++ b/src/renderer/components/layout/sidebar-cluster.module.css @@ -32,7 +32,6 @@ &:focus-visible { .dropdown { box-shadow: 0 0 0 2px var(--focus-color); - color: white; } } diff --git a/src/renderer/initializers/catalog-category-registry.tsx b/src/renderer/initializers/catalog-category-registry.tsx index 0c1fd6b9b3..294c5883fa 100644 --- a/src/renderer/initializers/catalog-category-registry.tsx +++ b/src/renderer/initializers/catalog-category-registry.tsx @@ -61,11 +61,11 @@ export function initCatalogCategoryRegistryEntries() { ctx.menuItems.push( { icon: "create_new_folder", - title: "Sync kubeconfig folders(s)", + title: "Sync kubeconfig folder(s)", defaultAction: true, onClick: async () => { await PathPicker.pick({ - label: "Sync folders(s)", + label: "Sync folder(s)", buttonLabel: "Sync", properties: ["showHiddenFiles", "multiSelections", "openDirectory"], onPick: addSyncEntries, diff --git a/yarn.lock b/yarn.lock index cf9a2cf0c0..f618779abd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14370,11 +14370,6 @@ websocket-extensions@>=0.1.1: resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== -what-input@^5.2.10: - version "5.2.10" - resolved "https://registry.yarnpkg.com/what-input/-/what-input-5.2.10.tgz#f79f5b65cf95d75e55e6d580bb0a6b98174cad4e" - integrity sha512-7AQoIMGq7uU8esmKniOtZG3A+pzlwgeyFpkS3f/yzRbxknSL68tvn5gjE6bZ4OMFxCPjpaBd2udUTqlZ0HwrXQ== - whatwg-encoding@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"