diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml index 223f85518d..bf0315ad3e 100644 --- a/.azure-pipelines.yml +++ b/.azure-pipelines.yml @@ -16,8 +16,8 @@ jobs: vmImage: windows-2019 strategy: matrix: - node_12.x: - node_version: 12.x + node_14.x: + node_version: 14.x steps: - powershell: | $CI_BUILD_TAG = git describe --tags @@ -48,12 +48,6 @@ jobs: GH_TOKEN: $(LENS_IDE_GH_TOKEN) displayName: Customize config - - script: make node_modules - displayName: Install dependencies - - - script: make build-npm - displayName: Generate npm package - - script: make build condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))" env: @@ -69,8 +63,8 @@ jobs: vmImage: macOS-10.14 strategy: matrix: - node_12.x: - node_version: 12.x + node_14.x: + node_version: 14.x steps: - script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG" condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))" @@ -99,15 +93,57 @@ jobs: GH_TOKEN: $(LENS_IDE_GH_TOKEN) displayName: Customize config - - script: make node_modules - displayName: Install dependencies + - script: make build + condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))" + env: + APPLEID: $(APPLEID) + APPLEIDPASS: $(APPLEIDPASS) + CSC_LINK: $(CSC_LINK) + CSC_KEY_PASSWORD: $(CSC_KEY_PASSWORD) + AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID) + AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY) + BUILD_NUMBER: $(Build.BuildNumber) + displayName: Build - - script: make build-npm - displayName: Generate npm package + - job: macOS-arm64 + pool: + vmImage: macOS-10.14 + strategy: + matrix: + node_14.x: + node_version: 14.x + steps: + - script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG" + condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))" + displayName: Set the tag name as an environment variable + + - task: NodeTool@0 + inputs: + versionSpec: $(node_version) + displayName: Install Node.js + + - task: Cache@2 + inputs: + key: 'yarn | "$(Agent.OS)" | yarn.lock' + restoreKeys: | + yarn | "$(Agent.OS)" + path: $(YARN_CACHE_FOLDER) + displayName: Cache Yarn packages + + - bash: | + set -e + git clone "https://${GH_TOKEN}@github.com/lensapp/lens-ide.git" .lens-ide-overlay + rm -rf .lens-ide-overlay/.git + cp -r .lens-ide-overlay/* ./ + jq -s '.[0] * .[1]' package.json package.ide.json > package.custom.json && mv package.custom.json package.json + env: + GH_TOKEN: $(LENS_IDE_GH_TOKEN) + displayName: Customize config - script: make build condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))" env: + BINARY_ARCH: arm64 APPLEID: $(APPLEID) APPLEIDPASS: $(APPLEIDPASS) CSC_LINK: $(CSC_LINK) @@ -122,8 +158,8 @@ jobs: vmImage: ubuntu-16.04 strategy: matrix: - node_12.x: - node_version: 12.x + node_14.x: + node_version: 14.x steps: - script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG" condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))" @@ -152,23 +188,6 @@ jobs: GH_TOKEN: $(LENS_IDE_GH_TOKEN) displayName: Customize config - - script: make node_modules - displayName: Install dependencies - - - script: make build-npm - displayName: Generate npm package - - - bash: | - sudo chown root:root / - sudo apt-get update && sudo apt-get install -y snapd - sudo snap install snapcraft --classic - echo -n "${SNAP_LOGIN}" | base64 -i -d > snap_login - snapcraft login --with snap_login - condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))" - env: - SNAP_LOGIN: $(SNAP_LOGIN) - displayName: Setup snapcraft - - script: make build condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))" env: diff --git a/.github/workflows/check-docs.yml b/.github/workflows/check-docs.yml index eafc6befc0..b010600c6c 100644 --- a/.github/workflows/check-docs.yml +++ b/.github/workflows/check-docs.yml @@ -8,7 +8,7 @@ jobs: if: ${{ contains(github.event.pull_request.labels.*.name, 'area/documentation') }} strategy: matrix: - node-version: [12.x] + node-version: [14.x] steps: - name: Checkout Release from lens uses: actions/checkout@v2 diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index e847eab0f5..18ec977859 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [12.x] + node-version: [14.x] steps: - name: Checkout Release from lens uses: actions/checkout@v2 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index afbcc74367..84b8020605 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [12.x] + node-version: [14.x] steps: - name: Set up Python 3.7 uses: actions/setup-python@v2 @@ -43,7 +43,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [12.x] + node-version: [14.x] needs: verify-docs steps: - name: Set up Python 3.7 diff --git a/.github/workflows/mkdocs-manual.yml b/.github/workflows/mkdocs-manual.yml index 632fccfa7b..a0695d0e7d 100644 --- a/.github/workflows/mkdocs-manual.yml +++ b/.github/workflows/mkdocs-manual.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [12.x] + node-version: [14.x] steps: - name: Set up Python 3.7 uses: actions/setup-python@v2 @@ -29,7 +29,7 @@ jobs: with: fetch-depth: 0 ref: '${{ github.event.inputs.version }}' - + - name: Using Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: @@ -53,15 +53,15 @@ jobs: rm -fr ./docs/clusters ./docs/contributing ./docs/faq ./docs/getting-started ./docs/helm ./docs/support ./docs/supporting sed -i '/Protocol Handlers/d' ./mkdocs.yml sed -i '/IPC/d' ./mkdocs.yml - sed -i 's#../../clusters/adding-clusters.md#https://docs.k8slens.dev/latest/clusters/adding-clusters/#g' ./docs/extensions/get-started/your-first-extension.md + sed -i 's#../../clusters/adding-clusters.md#https://docs.k8slens.dev/latest/clusters/adding-clusters/#g' ./docs/extensions/get-started/your-first-extension.md sed -i 's#clusters/adding-clusters.md#https://docs.k8slens.dev/latest/clusters/adding-clusters/#g' ./docs/README.md - sed -i 's#../../contributing/README.md#https://docs.k8slens.dev/latest/contributing/#g' ./docs/extensions/guides/generator.md + sed -i 's#../../contributing/README.md#https://docs.k8slens.dev/latest/contributing/#g' ./docs/extensions/guides/generator.md - name: git config run: | git config --local user.email "action@github.com" git config --local user.name "GitHub Action" - + - name: mkdocs deploy new release run: | mike deploy --push --force ${{ github.event.inputs.version }} diff --git a/.github/workflows/publish-master-npm.yml b/.github/workflows/publish-master-npm.yml index 2be9721d3e..25e434c0f4 100644 --- a/.github/workflows/publish-master-npm.yml +++ b/.github/workflows/publish-master-npm.yml @@ -11,7 +11,7 @@ jobs: ${{ github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'area/extension') }} strategy: matrix: - node-version: [12.x] + node-version: [14.x] steps: - name: Checkout Release uses: actions/checkout@v2 diff --git a/.github/workflows/publish-release-npm.yml b/.github/workflows/publish-release-npm.yml index 5a8bd9aff5..0bfc06a106 100644 --- a/.github/workflows/publish-release-npm.yml +++ b/.github/workflows/publish-release-npm.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [12.x] + node-version: [14.x] steps: - name: Checkout Release uses: actions/checkout@v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2ce53e59e2..3058f28fe0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: os: [ubuntu-16.04, macos-10.15, windows-2019] - node-version: [12.x] + node-version: [14.x] steps: - name: Checkout Release from lens uses: actions/checkout@v2 diff --git a/.yarnrc b/.yarnrc index f11de8a2a9..9f7a2e491b 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,3 +1,3 @@ disturl "https://atom.io/download/electron" -target "9.4.4" +target "12.0.15" runtime "electron" diff --git a/Makefile b/Makefile index 1a8baf0d37..75f6f55fa0 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,7 @@ CMD_ARGS = $(filter-out $@,$(MAKECMDGOALS)) %: @: +BINARY_ARCH ?= x64 NPM_RELEASE_TAG ?= latest EXTENSIONS_DIR = ./extensions extensions = $(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), ${dir}) @@ -64,7 +65,7 @@ ifeq "$(DETECTED_OS)" "Windows" rm -rf node_modules/win-ca/pem yarn run electron-builder --publish onTag --x64 --ia32 else - yarn run electron-builder --publish onTag + yarn run electron-builder --publish onTag --$(BINARY_ARCH) endif $(extension_node_modules): node_modules diff --git a/build/download_kubectl.ts b/build/download_kubectl.ts index 819b73dd02..fefdb6ebb1 100644 --- a/build/download_kubectl.ts +++ b/build/download_kubectl.ts @@ -25,7 +25,8 @@ import md5File from "md5-file"; import requestPromise from "request-promise-native"; import { ensureDir, pathExists } from "fs-extra"; import path from "path"; -import { noop } from "../src/common/utils"; +import { noop } from "lodash"; +import { isLinux, isMac } from "../src/common/vars"; class KubectlDownloader { public kubectlVersion: string; @@ -115,15 +116,21 @@ class KubectlDownloader { }); } } - const downloadVersion = packageInfo.config.bundledKubectlVersion; -const baseDir = path.join(process.env.INIT_CWD, "binaries", "client"); -const downloads = [ - { platform: "linux", arch: "amd64", target: path.join(baseDir, "linux", "x64", "kubectl") }, - { platform: "darwin", arch: "amd64", target: path.join(baseDir, "darwin", "x64", "kubectl") }, - { platform: "windows", arch: "amd64", target: path.join(baseDir, "windows", "x64", "kubectl.exe") }, - { platform: "windows", arch: "386", target: path.join(baseDir, "windows", "ia32", "kubectl.exe") } -]; +const baseDir = path.join(__dirname, "..", "binaries", "client"); +const binaryArch = process.env.BINARY_ARCH || "amd64"; +const binaryTargetArch = binaryArch === "amd64" ? "x64" : "arm64"; + +const downloads = []; + +if (isMac) { + downloads.push({ platform: "darwin", arch: binaryArch, target: path.join(baseDir, "darwin", binaryTargetArch, "kubectl") }); +} else if (isLinux) { + downloads.push({ platform: "linux", arch: binaryArch, target: path.join(baseDir, "linux", binaryTargetArch, "kubectl") }); +} else { + downloads.push({ platform: "windows", arch: "amd64", target: path.join(baseDir, "windows", binaryTargetArch, "kubectl.exe") }); + downloads.push({ platform: "windows", arch: "386", target: path.join(baseDir, "windows", "ia32", "kubectl.exe") }); +} downloads.forEach((dlOpts) => { console.log(dlOpts); diff --git a/integration/__tests__/app-preferences.tests.ts b/integration/__tests__/app-preferences.tests.ts new file mode 100644 index 0000000000..fe3817bf0c --- /dev/null +++ b/integration/__tests__/app-preferences.tests.ts @@ -0,0 +1,74 @@ +/** + * 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. + */ + +/* + Cluster tests are run if there is a pre-existing minikube cluster. Before running cluster tests the TEST_NAMESPACE + namespace is removed, if it exists, from the minikube cluster. Resources are created as part of the cluster tests in the + TEST_NAMESPACE namespace. This is done to minimize destructive impact of the cluster tests on an existing minikube + cluster and vice versa. +*/ +import type { Page } from "playwright"; +import * as utils from "../helpers/utils"; + +describe("preferences page tests", () => { + let window: Page, cleanup: () => Promise; + + beforeEach(async () => { + ({ window, cleanup } = await utils.start()); + await utils.clickWelcomeButton(window); + await window.keyboard.press("Meta+,"); + }, 10*60*1000); + + afterEach(async () => { + await cleanup(); + }, 10*60*1000); + + it('shows "preferences" and can navigate through the tabs', async () => { + const pages = [ + { + id: "application", + header: "Application", + }, + { + id: "proxy", + header: "Proxy", + }, + { + id: "kubernetes", + header: "Kubernetes", + }, + ]; + + for (const { id, header } of pages) { + await window.click(`[data-testid=${id}-tab]`); + await window.waitForSelector(`[data-testid=${id}-header] >> text=${header}`); + } + }, 10*60*1000); + + utils.itIf(process.platform !== "win32")("ensures helm repos", async () => { + await window.click("[data-testid=kubernetes-tab]"); + await window.waitForSelector("[data-testid=repository-name]", { + timeout: 100_000, + }); + await window.click("#HelmRepoSelect"); + await window.waitForSelector("div.Select__option"); + }, 10*60*1000); +}); diff --git a/integration/__tests__/app.tests.ts b/integration/__tests__/app.tests.ts deleted file mode 100644 index 5bf707fb71..0000000000 --- a/integration/__tests__/app.tests.ts +++ /dev/null @@ -1,87 +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. - */ - -/* - Cluster tests are run if there is a pre-existing minikube cluster. Before running cluster tests the TEST_NAMESPACE - namespace is removed, if it exists, from the minikube cluster. Resources are created as part of the cluster tests in the - TEST_NAMESPACE namespace. This is done to minimize destructive impact of the cluster tests on an existing minikube - cluster and vice versa. -*/ -import type { Application } from "spectron"; -import * as utils from "../helpers/utils"; -import { listHelmRepositories } from "../helpers/utils"; -import { fail } from "assert"; - -jest.setTimeout(2 * 60 * 1000); // 2 minutes so that we can get better errors from spectron - -// FIXME (!): improve / simplify all css-selectors + use [data-test-id="some-id"] (already used in some tests below) -describe("Lens integration tests", () => { - let app: Application; - - describe("app start", () => { - utils.beforeAllWrapped(async () => { - app = await utils.setup(); - }); - - utils.afterAllWrapped(() => utils.tearDown(app)); - - it('shows "add cluster"', async () => { - await app.electron.ipcRenderer.send("test-menu-item-click", "File", "Add Cluster"); - await app.client.waitUntilTextExists("h2", "Add Clusters from Kubeconfig"); - }); - - describe("preferences page", () => { - it('shows "preferences"', async () => { - const appName: string = process.platform === "darwin" ? "OpenLens" : "File"; - - await app.electron.ipcRenderer.send("test-menu-item-click", appName, "Preferences"); - await app.client.waitUntilTextExists("[data-testid=application-header]", "Application"); - }); - - it.each([ - ["application", "Application"], - ["proxy", "Proxy"], - ["kubernetes", "Kubernetes"], - ])("Can click the %s tab and see the %s header", async (tab, header) => { - await app.client.click(`[data-testid=${tab}-tab]`); - await app.client.waitUntilTextExists(`[data-testid=${tab}-header]`, header); - }); - - it("ensures helm repos", async () => { - const repos = await listHelmRepositories(); - - if (repos.length === 0) { - fail("Lens failed to add any repositories"); - } - - await app.client.click("[data-testid=kubernetes-tab]"); - await app.client.waitUntilTextExists("[data-testid=repository-name]", repos[0].name); // wait for the helm-cli to fetch the repo(s) - await app.client.click("#HelmRepoSelect"); // click the repo select to activate the drop-down - await app.client.waitUntilTextExists("div.Select__option", ""); // wait for at least one option to appear (any text) - }); - }); - - it.skip('quits Lens"', async () => { - await app.client.keys(["Meta", "Q"]); - await app.client.keys("Meta"); - }); - }); -}); diff --git a/integration/__tests__/cluster-pages.tests.ts b/integration/__tests__/cluster-pages.tests.ts index 3ffb922dea..8dae67dcaf 100644 --- a/integration/__tests__/cluster-pages.tests.ts +++ b/integration/__tests__/cluster-pages.tests.ts @@ -25,450 +25,439 @@ TEST_NAMESPACE namespace. This is done to minimize destructive impact of the cluster tests on an existing minikube cluster and vice versa. */ -import type { Application } from "spectron"; import * as utils from "../helpers/utils"; -import { minikubeReady, waitForMinikubeDashboard } from "../helpers/minikube"; -import { exec } from "child_process"; -import * as util from "util"; +import { minikubeReady } from "../helpers/minikube"; +import type { Frame, Page } from "playwright"; -export const promiseExec = util.promisify(exec); +const TEST_NAMESPACE = "integration-tests"; -jest.setTimeout(2 * 60 * 1000); // 2 minutes so that we can get better errors from spectron +function getSidebarSelectors(itemId: string) { + const root = `.SidebarItem[data-test-id="${itemId}"]`; -// FIXME (!): improve / simplify all css-selectors + use [data-test-id="some-id"] (already used in some tests below) -describe("Lens cluster pages", () => { - const TEST_NAMESPACE = "integration-tests"; - const BACKSPACE = "\uE003"; - let app: Application; - const ready = minikubeReady(TEST_NAMESPACE); + return { + expandSubMenu: `${root} .nav-item`, + subMenuLink: (href: string) => `.Sidebar .sub-menu a[href^="/${href}"]`, + }; +} - utils.describeIf(ready)("test common pages", () => { - let clusterAdded = false; - const addCluster = async () => { - await app.client.waitUntilTextExists("div", "Catalog"); - await waitForMinikubeDashboard(app); - await app.client.click('a[href="/nodes"]'); - await app.client.waitUntilTextExists("div.TableCell", "Ready"); - }; +function getLoadedSelector(page: CommonPage): string { + if (page.expectedText) { + return `${page.expectedSelector} >> text='${page.expectedText}'`; + } - describe("cluster add", () => { - utils.beforeAllWrapped(async () => { - app = await utils.setup(); - }); + return page.expectedSelector; +} - utils.afterAllWrapped(() => utils.tearDown(app)); +interface CommonPage { + name: string; + href: string; + expectedSelector: string; + expectedText?: string; +} - it("allows to add a cluster", async () => { - await addCluster(); - clusterAdded = true; - }); - }); +interface TopPageTest { + page: CommonPage; +} - const appStartAddCluster = async () => { - if (clusterAdded) { - app = await utils.setup(); - await addCluster(); +interface SubPageTest { + drawerId: string; + pages: CommonPage[]; +} + +type CommonPageTest = TopPageTest | SubPageTest; + +function isTopPageTest(test: CommonPageTest): test is TopPageTest { + return typeof (test as any).page === "object"; +} + +const commonPageTests: CommonPageTest[] = [{ + page: { + name: "Cluster", + href: "cluster", + expectedSelector: "div.ClusterOverview div.label", + expectedText: "CPU" + } +}, +{ + page: { + name: "Nodes", + href: "nodes", + expectedSelector: "h5.title", + expectedText: "Nodes" + } +}, +{ + drawerId: "workloads", + pages: [{ + name: "Overview", + href: "workloads", + expectedSelector: "h5.box", + expectedText: "Overview" + }, + { + name: "Pods", + href: "pods", + expectedSelector: "h5.title", + expectedText: "Pods" + }, + { + name: "Deployments", + href: "deployments", + expectedSelector: "h5.title", + expectedText: "Deployments" + }, + { + name: "DaemonSets", + href: "daemonsets", + expectedSelector: "h5.title", + expectedText: "Daemon Sets" + }, + { + name: "StatefulSets", + href: "statefulsets", + expectedSelector: "h5.title", + expectedText: "Stateful Sets" + }, + { + name: "ReplicaSets", + href: "replicasets", + expectedSelector: "h5.title", + expectedText: "Replica Sets" + }, + { + name: "Jobs", + href: "jobs", + expectedSelector: "h5.title", + expectedText: "Jobs" + }, + { + name: "CronJobs", + href: "cronjobs", + expectedSelector: "h5.title", + expectedText: "Cron Jobs" + }] +}, +{ + drawerId: "config", + pages: [{ + name: "ConfigMaps", + href: "configmaps", + expectedSelector: "h5.title", + expectedText: "Config Maps" + }, + { + name: "Secrets", + href: "secrets", + expectedSelector: "h5.title", + expectedText: "Secrets" + }, + { + name: "Resource Quotas", + href: "resourcequotas", + expectedSelector: "h5.title", + expectedText: "Resource Quotas" + }, + { + name: "Limit Ranges", + href: "limitranges", + expectedSelector: "h5.title", + expectedText: "Limit Ranges" + }, + { + name: "HPA", + href: "hpa", + expectedSelector: "h5.title", + expectedText: "Horizontal Pod Autoscalers" + }, + { + name: "Pod Disruption Budgets", + href: "poddisruptionbudgets", + expectedSelector: "h5.title", + expectedText: "Pod Disruption Budgets" + }] +}, +{ + drawerId: "networks", + pages: [{ + name: "Services", + href: "services", + expectedSelector: "h5.title", + expectedText: "Services" + }, + { + name: "Endpoints", + href: "endpoints", + expectedSelector: "h5.title", + expectedText: "Endpoints" + }, + { + name: "Ingresses", + href: "ingresses", + expectedSelector: "h5.title", + expectedText: "Ingresses" + }, + { + name: "Network Policies", + href: "network-policies", + expectedSelector: "h5.title", + expectedText: "Network Policies" + }] +}, +{ + drawerId: "storage", + pages: [{ + name: "Persistent Volume Claims", + href: "persistent-volume-claims", + expectedSelector: "h5.title", + expectedText: "Persistent Volume Claims" + }, + { + name: "Persistent Volumes", + href: "persistent-volumes", + expectedSelector: "h5.title", + expectedText: "Persistent Volumes" + }, + { + name: "Storage Classes", + href: "storage-classes", + expectedSelector: "h5.title", + expectedText: "Storage Classes" + }] +}, +{ + page: { + name: "Namespaces", + href: "namespaces", + expectedSelector: "h5.title", + expectedText: "Namespaces" + } +}, +{ + page: { + name: "Events", + href: "events", + expectedSelector: "h5.title", + expectedText: "Events" + } +}, +{ + drawerId: "apps", + pages: [{ + name: "Charts", + href: "apps/charts", + expectedSelector: "div.HelmCharts input", + }, + { + name: "Releases", + href: "apps/releases", + expectedSelector: "h5.title", + expectedText: "Releases" + }] +}, +{ + drawerId: "users", + pages: [{ + name: "Service Accounts", + href: "service-accounts", + expectedSelector: "h5.title", + expectedText: "Service Accounts" + }, + { + name: "Roles", + href: "roles", + expectedSelector: "h5.title", + expectedText: "Roles" + }, + { + name: "Cluster Roles", + href: "cluster-roles", + expectedSelector: "h5.title", + expectedText: "Cluster Roles" + }, + { + name: "Role Bindings", + href: "role-bindings", + expectedSelector: "h5.title", + expectedText: "Role Bindings" + }, + { + name: "Cluster Role Bindings", + href: "cluster-role-bindings", + expectedSelector: "h5.title", + expectedText: "Cluster Role Bindings" + }, + { + name: "Pod Security Policies", + href: "pod-security-policies", + expectedSelector: "h5.title", + expectedText: "Pod Security Policies" + }] +}, +{ + drawerId: "custom-resources", + pages: [{ + name: "Definitions", + href: "crd/definitions", + expectedSelector: "h5.title", + expectedText: "Custom Resources" + }] +}]; + +utils.describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => { + let window: Page, cleanup: () => Promise, frame: Frame; + + beforeEach(async () => { + ({ window, cleanup } = await utils.start()); + await utils.clickWelcomeButton(window); + + frame = await utils.lauchMinikubeClusterFromCatalog(window); + }, 10*60*1000); + + afterEach(async () => { + await cleanup(); + }, 10*60*1000); + + it("should navigate around common cluster pages", async () => { + for (const test of commonPageTests) { + if (isTopPageTest(test)) { + const { href, expectedText, expectedSelector } = test.page; + const menuButton = await frame.waitForSelector(`a[href^="/${href}"]`); + + await menuButton.click(); + await frame.waitForSelector(`${expectedSelector} >> text='${expectedText}'`); + + continue; } - }; - function getSidebarSelectors(itemId: string) { - const root = `.SidebarItem[data-test-id="${itemId}"]`; + const { drawerId, pages } = test; + const selectors = getSidebarSelectors(drawerId); + const mainPageSelector = `${selectors.subMenuLink(pages[0].href)} >> text='${pages[0].name}'`; - return { - expandSubMenu: `${root} .nav-item`, - subMenuLink: (href: string) => `.Sidebar .sub-menu a[href^="/${href}"]`, - }; + await frame.click(selectors.expandSubMenu); + await frame.waitForSelector(mainPageSelector); + + for (const page of pages) { + const subPageButton = await frame.waitForSelector(selectors.subMenuLink(page.href)); + + await subPageButton.click(); + await frame.waitForSelector(getLoadedSelector(page)); + } + + await frame.click(selectors.expandSubMenu); + await frame.waitForSelector(mainPageSelector, { state: "hidden" }); + } + }, 10*60*1000); + + it("show logs and highlight the log search entries", async () => { + await frame.click(`a[href="/workloads"]`); + await frame.click(`a[href="/pods"]`); + + const namespacesSelector = await frame.waitForSelector(".NamespaceSelect"); + + await namespacesSelector.click(); + await namespacesSelector.type("kube-system"); + await namespacesSelector.press("Enter"); + await namespacesSelector.click(); + + const kubeApiServerRow = await frame.waitForSelector("div.TableCell >> text=kube-apiserver"); + + await kubeApiServerRow.click(); + await frame.waitForSelector(".Drawer", { state: "visible" }); + + const logButton = await frame.waitForSelector("ul.KubeObjectMenu li.MenuItem i.Icon span[data-icon-name='subject']"); + + await logButton.click(); + + // Check if controls are available + await frame.waitForSelector(".LogList .VirtualList"); + await frame.waitForSelector(".LogResourceSelector"); + + const logSearchInput = await frame.waitForSelector(".LogSearch .SearchInput input"); + + await logSearchInput.type(":"); + await frame.waitForSelector(".LogList .list span.active"); + + const showTimestampsButton = await frame.waitForSelector(".LogControls .show-timestamps"); + + await showTimestampsButton.click(); + + const showPreviousButton = await frame.waitForSelector(".LogControls .show-previous"); + + await showPreviousButton.click(); + }, 10*60*1000); + + it("should show the default namespaces", async () => { + await frame.click('a[href="/namespaces"]'); + await frame.waitForSelector("div.TableCell >> text='default'"); + await frame.waitForSelector("div.TableCell >> text='kube-system'"); + }, 10*60*1000); + + it(`should create the ${TEST_NAMESPACE} and a pod in the namespace`, async () => { + await frame.click('a[href="/namespaces"]'); + await frame.click("button.add-button"); + await frame.waitForSelector("div.AddNamespaceDialog >> text='Create Namespace'"); + + const namespaceNameInput = await frame.waitForSelector(".AddNamespaceDialog input"); + + await namespaceNameInput.type(TEST_NAMESPACE); + await namespaceNameInput.press("Enter"); + + await frame.waitForSelector(`div.TableCell >> text=${TEST_NAMESPACE}`); + + if ((await frame.innerText(`a[href^="/workloads"] .expand-icon`)) === "keyboard_arrow_down") { + await frame.click(`a[href^="/workloads"]`); } - describe("cluster pages", () => { - utils.beforeAllWrapped(appStartAddCluster); - utils.afterAllWrapped(() => utils.tearDown(app)); + await frame.click(`a[href^="/pods"]`); - const tests: { - drawer?: string - drawerId?: string - pages: { - name: string, - href: string, - expectedSelector: string, - expectedText: string - }[] - }[] = [{ - drawer: "", - drawerId: "", - pages: [{ - name: "Cluster", - href: "cluster", - expectedSelector: "div.ClusterOverview div.label", - expectedText: "CPU" - }] - }, - { - drawer: "", - drawerId: "", - pages: [{ - name: "Nodes", - href: "nodes", - expectedSelector: "h5.title", - expectedText: "Nodes" - }] - }, - { - drawer: "Workloads", - drawerId: "workloads", - pages: [{ - name: "Overview", - href: "workloads", - expectedSelector: "h5.box", - expectedText: "Overview" - }, - { - name: "Pods", - href: "pods", - expectedSelector: "h5.title", - expectedText: "Pods" - }, - { - name: "Deployments", - href: "deployments", - expectedSelector: "h5.title", - expectedText: "Deployments" - }, - { - name: "DaemonSets", - href: "daemonsets", - expectedSelector: "h5.title", - expectedText: "Daemon Sets" - }, - { - name: "StatefulSets", - href: "statefulsets", - expectedSelector: "h5.title", - expectedText: "Stateful Sets" - }, - { - name: "ReplicaSets", - href: "replicasets", - expectedSelector: "h5.title", - expectedText: "Replica Sets" - }, - { - name: "Jobs", - href: "jobs", - expectedSelector: "h5.title", - expectedText: "Jobs" - }, - { - name: "CronJobs", - href: "cronjobs", - expectedSelector: "h5.title", - expectedText: "Cron Jobs" - }] - }, - { - drawer: "Configuration", - drawerId: "config", - pages: [{ - name: "ConfigMaps", - href: "configmaps", - expectedSelector: "h5.title", - expectedText: "Config Maps" - }, - { - name: "Secrets", - href: "secrets", - expectedSelector: "h5.title", - expectedText: "Secrets" - }, - { - name: "Resource Quotas", - href: "resourcequotas", - expectedSelector: "h5.title", - expectedText: "Resource Quotas" - }, - { - name: "Limit Ranges", - href: "limitranges", - expectedSelector: "h5.title", - expectedText: "Limit Ranges" - }, - { - name: "HPA", - href: "hpa", - expectedSelector: "h5.title", - expectedText: "Horizontal Pod Autoscalers" - }, - { - name: "Pod Disruption Budgets", - href: "poddisruptionbudgets", - expectedSelector: "h5.title", - expectedText: "Pod Disruption Budgets" - }] - }, - { - drawer: "Network", - drawerId: "networks", - pages: [{ - name: "Services", - href: "services", - expectedSelector: "h5.title", - expectedText: "Services" - }, - { - name: "Endpoints", - href: "endpoints", - expectedSelector: "h5.title", - expectedText: "Endpoints" - }, - { - name: "Ingresses", - href: "ingresses", - expectedSelector: "h5.title", - expectedText: "Ingresses" - }, - { - name: "Network Policies", - href: "network-policies", - expectedSelector: "h5.title", - expectedText: "Network Policies" - }] - }, - { - drawer: "Storage", - drawerId: "storage", - pages: [{ - name: "Persistent Volume Claims", - href: "persistent-volume-claims", - expectedSelector: "h5.title", - expectedText: "Persistent Volume Claims" - }, - { - name: "Persistent Volumes", - href: "persistent-volumes", - expectedSelector: "h5.title", - expectedText: "Persistent Volumes" - }, - { - name: "Storage Classes", - href: "storage-classes", - expectedSelector: "h5.title", - expectedText: "Storage Classes" - }] - }, - { - drawer: "", - drawerId: "", - pages: [{ - name: "Namespaces", - href: "namespaces", - expectedSelector: "h5.title", - expectedText: "Namespaces" - }] - }, - { - drawer: "", - drawerId: "", - pages: [{ - name: "Events", - href: "events", - expectedSelector: "h5.title", - expectedText: "Events" - }] - }, - { - drawer: "Apps", - drawerId: "apps", - pages: [{ - name: "Charts", - href: "apps/charts", - expectedSelector: "div.HelmCharts input", - expectedText: "" - }, - { - name: "Releases", - href: "apps/releases", - expectedSelector: "h5.title", - expectedText: "Releases" - }] - }, - { - drawer: "Access Control", - drawerId: "users", - pages: [{ - name: "Service Accounts", - href: "service-accounts", - expectedSelector: "h5.title", - expectedText: "Service Accounts" - }, - { - name: "Role Bindings", - href: "role-bindings", - expectedSelector: "h5.title", - expectedText: "Role Bindings" - }, - { - name: "Roles", - href: "roles", - expectedSelector: "h5.title", - expectedText: "Roles" - }, - { - name: "Pod Security Policies", - href: "pod-security-policies", - expectedSelector: "h5.title", - expectedText: "Pod Security Policies" - }] - }, - { - drawer: "Custom Resources", - drawerId: "custom-resources", - pages: [{ - name: "Definitions", - href: "crd/definitions", - expectedSelector: "h5.title", - expectedText: "Custom Resources" - }] - }]; + const namespacesSelector = await frame.waitForSelector(".NamespaceSelect"); - tests.forEach(({ drawer = "", drawerId = "", pages }) => { - const selectors = getSidebarSelectors(drawerId); + await namespacesSelector.click(); + await namespacesSelector.type(TEST_NAMESPACE); + await namespacesSelector.press("Enter"); + await namespacesSelector.click(); - if (drawer !== "") { - it(`shows ${drawer} drawer`, async () => { - expect(clusterAdded).toBe(true); - await app.client.click(selectors.expandSubMenu); - await app.client.waitUntilTextExists(selectors.subMenuLink(pages[0].href), pages[0].name); - }); + await frame.click(".Icon.new-dock-tab"); - pages.forEach(({ name, href, expectedSelector, expectedText }) => { - it(`shows ${drawer}->${name} page`, async () => { - expect(clusterAdded).toBe(true); - await app.client.click(selectors.subMenuLink(href)); - await app.client.waitUntilTextExists(expectedSelector, expectedText); - }); - }); - - it(`hides ${drawer} drawer`, async () => { - expect(clusterAdded).toBe(true); - await app.client.click(selectors.expandSubMenu); - await expect(app.client.waitUntilTextExists(selectors.subMenuLink(pages[0].href), pages[0].name, 100)).rejects.toThrow(); - }); - } else { - const { href, name, expectedText, expectedSelector } = pages[0]; - - it(`shows page ${name}`, async () => { - expect(clusterAdded).toBe(true); - await app.client.click(`a[href^="/${href}"]`); - await app.client.waitUntilTextExists(expectedSelector, expectedText); - }); + try { + await frame.click("li.MenuItem.create-resource-tab", { + // NOTE: the following shouldn't be required, but is because without it a TypeError is thrown + // see: https://github.com/microsoft/playwright/issues/8229 + position: { + y: 0, + x: 0, } }); - }); + } catch (error) { + console.log(error); + await frame.waitForTimeout(100_000); + } - describe("viewing pod logs", () => { - utils.beforeEachWrapped(appStartAddCluster); - utils.afterEachWrapped(() => utils.tearDown(app)); + const inputField = await frame.waitForSelector(".CreateResource div.react-monaco-editor-container"); - it(`shows a log for a pod`, async () => { - expect(clusterAdded).toBe(true); - // Go to Pods page - await app.client.click(getSidebarSelectors("workloads").expandSubMenu); - await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods"); - await app.client.click('a[href^="/pods"]'); - await app.client.click(".NamespaceSelect"); - await app.client.keys("kube-system"); - await app.client.keys("Enter");// "\uE007" - await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver"); - let podMenuItemEnabled = false; + await inputField.click(); + await inputField.type("apiVersion: v1", { delay: 10 }); + await inputField.press("Enter", { delay: 10 }); + await inputField.type("kind: Pod", { delay: 10 }); + await inputField.press("Enter", { delay: 10 }); + await inputField.type("metadata:", { delay: 10 }); + await inputField.press("Enter", { delay: 10 }); + await inputField.type(" name: nginx-create-pod-test", { delay: 10 }); + await inputField.press("Enter", { delay: 10 }); + await inputField.type(`namespace: ${TEST_NAMESPACE}`, { delay: 10 }); + await inputField.press("Enter", { delay: 10 }); + await inputField.press("Backspace", { delay: 10 }); + await inputField.type("spec:", { delay: 10 }); + await inputField.press("Enter", { delay: 10 }); + await inputField.type(" containers:", { delay: 10 }); + await inputField.press("Enter", { delay: 10 }); + await inputField.type("- name: nginx-create-pod-test", { delay: 10 }); + await inputField.press("Enter", { delay: 10 }); + await inputField.type(" image: nginx:alpine", { delay: 10 }); + await inputField.press("Enter", { delay: 10 }); - // Wait until extensions are enabled on renderer - while (!podMenuItemEnabled) { - const logs = await app.client.getRenderProcessLogs(); - - podMenuItemEnabled = !!logs.find(entry => entry.message.includes("[EXTENSION]: enabled lens-pod-menu@")); - - if (!podMenuItemEnabled) { - await new Promise(r => setTimeout(r, 1000)); - } - } - - // Open logs tab in dock - await app.client.click(".list .TableRow:first-child"); - await app.client.waitForVisible(".Drawer"); - const logsButton = "ul.KubeObjectMenu li.MenuItem i.Icon span[data-icon-name='subject']"; - - await app.client.waitForVisible(logsButton); - await app.client.click(logsButton); - - // Check if controls are available - await app.client.waitForVisible(".LogList .VirtualList"); - await app.client.waitForVisible(".LogResourceSelector"); - //await app.client.waitForVisible(".LogSearch .SearchInput"); - await app.client.waitForVisible(".LogSearch .SearchInput input"); - // Search for semicolon - await app.client.keys(":"); - await app.client.waitForVisible(".LogList .list span.active"); - // Click through controls - await app.client.click(".LogControls .show-timestamps"); - await app.client.click(".LogControls .show-previous"); - }); - }); - - describe("cluster operations", () => { - utils.beforeEachWrapped(appStartAddCluster); - utils.afterEachWrapped(() => utils.tearDown(app)); - - it("shows default namespace", async () => { - expect(clusterAdded).toBe(true); - await app.client.click('a[href="/namespaces"]'); - await app.client.waitUntilTextExists("div.TableCell", "default"); - await app.client.waitUntilTextExists("div.TableCell", "kube-system"); - }); - - it(`creates ${TEST_NAMESPACE} namespace`, async () => { - expect(clusterAdded).toBe(true); - await app.client.click('a[href="/namespaces"]'); - await app.client.waitUntilTextExists("div.TableCell", "default"); - await app.client.waitUntilTextExists("div.TableCell", "kube-system"); - await app.client.click("button.add-button"); - await app.client.waitUntilTextExists("div.AddNamespaceDialog", "Create Namespace"); - await app.client.keys(`${TEST_NAMESPACE}\n`); - await app.client.waitForExist(`.name=${TEST_NAMESPACE}`); - }); - - it(`creates a pod in ${TEST_NAMESPACE} namespace`, async () => { - expect(clusterAdded).toBe(true); - await app.client.click(getSidebarSelectors("workloads").expandSubMenu); - await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods"); - await app.client.click('a[href^="/pods"]'); - - await app.client.click(".NamespaceSelect"); - await app.client.keys(TEST_NAMESPACE); - await app.client.keys("Enter");// "\uE007" - await app.client.click(".Icon.new-dock-tab"); - await app.client.waitUntilTextExists("li.MenuItem.create-resource-tab", "Create resource"); - await app.client.click("li.MenuItem.create-resource-tab"); - await app.client.waitForVisible(".CreateResource div.react-monaco-editor-container"); - // Write pod manifest to editor - await app.client.click(".CreateResource div.react-monaco-editor-container"); - await app.client.keys("apiVersion: v1\n"); - await app.client.keys("kind: Pod\n"); - await app.client.keys("metadata:\n"); - await app.client.keys(" name: nginx-create-pod-test\n"); - await app.client.keys(`namespace: ${TEST_NAMESPACE}\n`); - await app.client.keys(`${BACKSPACE}spec:\n`); - await app.client.keys(" containers:\n"); - await app.client.keys("- name: nginx-create-pod-test\n"); - await app.client.keys(" image: nginx:alpine\n"); - // Create deployment - await app.client.waitForEnabled("button.Button=Create & Close"); - await app.client.click("button.Button=Create & Close"); - // Wait until first bits of pod appears on dashboard - await app.client.waitForExist(".name=nginx-create-pod-test"); - // Open pod details - await app.client.click(".name=nginx-create-pod-test"); - await app.client.waitUntilTextExists("div.drawer-title-text", "Pod: nginx-create-pod-test"); - }); - }); - }); + await frame.click("button.Button >> text='Create & Close'"); + await frame.click("div.TableCell >> text=nginx-create-pod-test"); + await frame.waitForSelector("div.drawer-title-text >> text='Pod: nginx-create-pod-test'"); + }, 10*60*1000); }); diff --git a/integration/__tests__/command-palette.tests.ts b/integration/__tests__/command-palette.tests.ts index 4695f48b33..1f2e49114b 100644 --- a/integration/__tests__/command-palette.tests.ts +++ b/integration/__tests__/command-palette.tests.ts @@ -19,25 +19,25 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import type { Application } from "spectron"; +import type { Page } from "playwright"; import * as utils from "../helpers/utils"; -jest.setTimeout(2 * 60 * 1000); // 2 minutes so that we can get better errors from spectron - describe("Lens command palette", () => { - let app: Application; + let window: Page, cleanup: () => Promise; + + beforeEach(async () => { + ({ window, cleanup } = await utils.start()); + await utils.clickWelcomeButton(window); + }, 10*60*1000); + + afterEach(async () => { + await cleanup(); + }, 10*60*1000); describe("menu", () => { - utils.beforeAllWrapped(async () => { - app = await utils.setup(); - }); - - utils.afterAllWrapped(() => utils.tearDown(app)); - - it("opens command dialog from menu", async () => { - await app.electron.ipcRenderer.send("test-menu-item-click", "View", "Command Palette..."); - await app.client.waitUntilTextExists(".Select__option", "Hotbar: Switch"); - await app.client.keys("Escape"); - }); + it("opens command dialog from keyboard shortcut", async () => { + await window.keyboard.press("Meta+Shift+p"); + await window.waitForSelector(".Select__option >> text=Hotbar: Switch"); + }, 10*60*1000); }); }); diff --git a/integration/helpers/minikube.ts b/integration/helpers/minikube.ts index 8698bd6be2..b7c2356dab 100644 --- a/integration/helpers/minikube.ts +++ b/integration/helpers/minikube.ts @@ -19,7 +19,6 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { spawnSync } from "child_process"; -import type { Application } from "spectron"; export function minikubeReady(testNamespace: string): boolean { // determine if minikube is running @@ -57,18 +56,3 @@ export function minikubeReady(testNamespace: string): boolean { return true; } - -export async function waitForMinikubeDashboard(app: Application) { - await app.client.waitUntilTextExists("div.TableCell", "minikube"); - await app.client.waitForExist(".Input.SearchInput input"); - await app.client.setValue(".Input.SearchInput input", "minikube"); - await app.client.waitUntilTextExists("div.TableCell", "minikube"); - await app.client.click("div.TableRow"); - await app.client.waitUntilTextExists("div.drawer-title-text", "KubernetesCluster: minikube"); - await app.client.waitForExist("div.EntityIcon div.HotbarIcon div div.MuiAvatar-root"); - await app.client.click("div.EntityIcon div.HotbarIcon div div.MuiAvatar-root"); - await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started"); - await app.client.waitForExist(`iframe[name="minikube"]`); - await app.client.frame("minikube"); - await app.client.waitUntilTextExists("span.link-text", "Cluster"); -} diff --git a/integration/helpers/utils.ts b/integration/helpers/utils.ts index 15fec4d20b..f0e82809fb 100644 --- a/integration/helpers/utils.ts +++ b/integration/helpers/utils.ts @@ -18,49 +18,20 @@ * 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 { Application } from "spectron"; -import * as util from "util"; -import { exec } from "child_process"; +import { createHash } from "crypto"; +import { mkdirp, remove } from "fs-extra"; +import * as os from "os"; +import * as path from "path"; +import * as uuid from "uuid"; +import { ElectronApplication, Frame, Page, _electron as electron } from "playwright"; +import { noop } from "lodash"; -const AppPaths: Partial> = { +export const AppPaths: Partial> = { "win32": "./dist/win-unpacked/OpenLens.exe", "linux": "./dist/linux-unpacked/open-lens", "darwin": "./dist/mac/OpenLens.app/Contents/MacOS/OpenLens", }; -interface DoneCallback { - (...args: any[]): any; - fail(error?: string | { message: string }): any; -} - -/** - * This is necessary because Jest doesn't do this correctly. - * @param fn The function to call - */ -export function wrapJestLifecycle(fn: () => Promise | void): (done: DoneCallback) => void { - return function (done: DoneCallback) { - (async () => fn())() - .then(() => done()) - .catch(error => done.fail(error)); - }; -} - -export function beforeAllWrapped(fn: () => Promise | void): void { - beforeAll(wrapJestLifecycle(fn)); -} - -export function beforeEachWrapped(fn: () => Promise | void): void { - beforeEach(wrapJestLifecycle(fn)); -} - -export function afterAllWrapped(fn: () => Promise | void): void { - afterAll(wrapJestLifecycle(fn)); -} - -export function afterEachWrapped(fn: () => Promise | void): void { - afterEach(wrapJestLifecycle(fn)); -} - export function itIf(condition: boolean) { return condition ? it : it.skip; } @@ -69,71 +40,80 @@ export function describeIf(condition: boolean) { return condition ? describe : describe.skip; } -export const keys = { - backspace: "\uE003" -}; +async function getMainWindow(app: ElectronApplication, timeout = 50_000): Promise { + const deadline = Date.now() + timeout; -export async function setup(): Promise { - const app = new Application({ - path: AppPaths[process.platform], // path to electron app - args: [], - startTimeout: 60000, - waitTimeout: 10000, - env: { - CICD: "true" + for (; Date.now() < deadline;) { + for (const page of app.windows()) { + if (page.url().startsWith("http://localhost")) { + return page; + } } - }); - await app.start(); - // Wait for splash screen to be closed - while (await app.client.getWindowCount() > 1); - await app.client.windowByIndex(0); - await app.client.waitUntilWindowLoaded(); - await showCatalog(app); - - return app; -} - -export async function showCatalog(app: Application) { - await app.client.waitForExist("#hotbarIcon-catalog-entity .Icon"); - await app.client.click("#hotbarIcon-catalog-entity .Icon"); -} - -type AsyncPidGetter = () => Promise; - -export async function tearDown(app?: Application) { - if (!app?.isRunning()) { - return; + await new Promise(resolve => setTimeout(resolve, 2_000)); } - const pid = await (app.mainProcess.pid as any as AsyncPidGetter)(); + throw new Error(`Lens did not open the main window within ${timeout}ms`); +} - await app.stop(); +export async function start() { + const CICD = path.join(os.tmpdir(), "lens-integration-testing", uuid.v4()); + + // Make sure that the directory is clear + await remove(CICD).catch(noop); + await mkdirp(CICD); + + const app = await electron.launch({ + args: ["--integration-testing"], // this argument turns off the blocking of quit + executablePath: AppPaths[process.platform], + bypassCSP: true, + env: { + CICD, + ...process.env + }, + timeout: 100_000, + } as Parameters[0]); try { - process.kill(pid, "SIGKILL"); - } catch (e) { - console.error(e); + const window = await getMainWindow(app); + + return { + app, + window, + cleanup: async () => { + await app.close(); + await remove(CICD).catch(noop); + }, + }; + } catch (error) { + await app.close(); + await remove(CICD).catch(noop); + throw error; } } -export const promiseExec = util.promisify(exec); - -type HelmRepository = { - name: string; - url: string; -}; - -export async function listHelmRepositories(): Promise{ - for (let i = 0; i < 10; i += 1) { - try { - const { stdout } = await promiseExec("helm repo list -o json"); - - return JSON.parse(stdout); - } catch { - await new Promise(r => setTimeout(r, 2000)); // if no repositories, wait for Lens adding bitnami repository - } - } - - return []; +export async function clickWelcomeButton(window: Page) { + await window.click("#hotbarIcon-catalog-entity .Icon"); +} + +function minikubeEntityId() { + return createHash("md5").update(`${path.join(os.homedir(), ".kube", "config")}:minikube`).digest("hex"); +} + +/** + * From the catalog, click the minikube entity and wait for it to connect, returning its frame + */ +export async function lauchMinikubeClusterFromCatalog(window: Page): Promise { + await window.waitForSelector("div.TableCell"); + await window.click("div.TableCell >> text='minikube'"); + await window.waitForSelector("div.drawer-title-text >> text='KubernetesCluster: minikube'"); + await window.click("div.EntityIcon div.HotbarIcon div div.MuiAvatar-root"); + + const minikubeFrame = await window.waitForSelector(`#cluster-frame-${minikubeEntityId()}`); + + const frame = await minikubeFrame.contentFrame(); + + await frame.waitForSelector("div.Sidebar"); + + return frame; } diff --git a/package.json b/package.json index 64f940dc77..47d8ab915f 100644 --- a/package.json +++ b/package.json @@ -28,9 +28,8 @@ "build:linux": "yarn run compile && electron-builder --linux --dir", "build:mac": "yarn run compile && electron-builder --mac --dir", "build:win": "yarn run compile && electron-builder --win --dir", - "integration": "jest --runInBand integration", + "integration": "jest --runInBand --detectOpenHandles --forceExit integration", "dist": "yarn run compile && electron-builder --publish onTag", - "dist:win": "yarn run compile && electron-builder --publish onTag --x64 --ia32", "dist:dir": "yarn run dist --dir -c.compression=store -c.mac.identity=null", "download-bins": "concurrently yarn:download:*", "download:kubectl": "yarn run ts-node build/download_kubectl.ts", @@ -48,11 +47,11 @@ }, "config": { "bundledKubectlVersion": "1.21.2", - "bundledHelmVersion": "3.5.4", + "bundledHelmVersion": "3.6.3", "sentryDsn": "" }, "engines": { - "node": ">=12 <13" + "node": ">=14 <15" }, "jest": { "collectCoverage": false, @@ -141,6 +140,10 @@ "from": "binaries/client/darwin/x64/kubectl", "to": "./x64/kubectl" }, + { + "from": "binaries/client/darwin/arm64/kubectl", + "to": "./arm64/kubectl" + }, { "from": "binaries/client/helm3/helm3", "to": "./helm3/helm3" @@ -180,6 +183,7 @@ } }, "dependencies": { + "@electron/remote": "^1.2.1", "@hapi/call": "^8.0.1", "@hapi/subtext": "^7.0.3", "@kubernetes/client-node": "^0.15.1", @@ -263,7 +267,7 @@ "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^11.2.6", "@types/byline": "^4.2.32", - "@types/chart.js": "^2.9.21", + "@types/chart.js": "^2.9.34", "@types/color": "^3.0.2", "@types/crypto-js": "^3.1.47", "@types/dompurify": "^2.0.2", @@ -287,7 +291,7 @@ "@types/module-alias": "^2.0.0", "@types/node": "12.20", "@types/node-fetch": "^2.5.12", - "@types/npm": "^2.0.31", + "@types/npm": "^2.0.32", "@types/progress-bar-webpack-plugin": "^2.1.2", "@types/proper-lockfile": "^4.1.1", "@types/randomcolor": "^0.5.6", @@ -326,7 +330,7 @@ "css-loader": "^5.2.6", "deepdash": "^5.3.5", "dompurify": "^2.0.17", - "electron": "^9.4.4", + "electron": "^12.0.17", "electron-builder": "^22.10.5", "electron-notarize": "^0.3.0", "esbuild": "^0.12.12", @@ -352,8 +356,9 @@ "node-loader": "^1.0.3", "node-sass": "^4.14.1", "nodemon": "^2.0.12", + "playwright": "^1.14.0", "postcss": "^8.3.6", - "postcss-loader": "4.0.3", + "postcss-loader": "4.3.0", "postinstall-postinstall": "^2.1.0", "progress-bar-webpack-plugin": "^2.1.0", "randomcolor": "^0.6.2", @@ -367,12 +372,11 @@ "react-window": "^1.8.5", "sass-loader": "^8.0.2", "sharp": "^0.29.0", - "spectron": "11.0.0", "style-loader": "^2.0.0", "tailwindcss": "^2.2.4", "ts-jest": "26.5.6", "ts-loader": "^7.0.5", - "ts-node": "^8.10.2", + "ts-node": "^10.1.0", "type-fest": "^1.0.2", "typed-emitter": "^1.3.1", "typedoc": "0.21.0-beta.2", diff --git a/src/common/__tests__/user-store.test.ts b/src/common/__tests__/user-store.test.ts index c5a15aec28..ba31f06db6 100644 --- a/src/common/__tests__/user-store.test.ts +++ b/src/common/__tests__/user-store.test.ts @@ -37,7 +37,6 @@ import { Console } from "console"; import { SemVer } from "semver"; import electron from "electron"; import { stdout, stderr } from "process"; -import { beforeEachWrapped } from "../../../integration/helpers/utils"; import { ThemeStore } from "../../renderer/theme.store"; import type { ClusterStoreModel } from "../cluster-store"; @@ -45,7 +44,7 @@ console = new Console(stdout, stderr); describe("user store tests", () => { describe("for an empty config", () => { - beforeEachWrapped(() => { + beforeEach(() => { mockFs({ tmp: { "config.json": "{}", "kube_config": "{}" } }); (UserStore.createInstance() as any).refreshNewContexts = jest.fn(() => Promise.resolve()); @@ -94,7 +93,7 @@ describe("user store tests", () => { }); describe("migrations", () => { - beforeEachWrapped(() => { + beforeEach(() => { mockFs({ "tmp": { "config.json": JSON.stringify({ diff --git a/src/common/base-store.ts b/src/common/base-store.ts index c311bd8808..54f6d2a5a2 100644 --- a/src/common/base-store.ts +++ b/src/common/base-store.ts @@ -22,7 +22,7 @@ import path from "path"; import Config from "conf"; import type { Options as ConfOptions } from "conf/dist/source/types"; -import { app, ipcMain, ipcRenderer, remote } from "electron"; +import { ipcMain, ipcRenderer } from "electron"; import { IReactionOptions, makeObservable, reaction, runInAction } from "mobx"; import { getAppVersion, Singleton, toJS, Disposer } from "./utils"; import logger from "../main/logger"; @@ -30,6 +30,7 @@ import { broadcastMessage, ipcMainOn, ipcRendererOn } from "./ipc"; import isEqual from "lodash/isEqual"; import { isTestEnv } from "./vars"; import { kebabCase } from "lodash"; +import { getPath } from "./utils/getPath"; export interface BaseStoreParams extends ConfOptions { syncOptions?: IReactionOptions; @@ -88,7 +89,7 @@ export abstract class BaseStore extends Singleton { } protected cwd() { - return (app || remote.app).getPath("userData"); + return getPath("userData"); } protected async saveToFile(model: T) { diff --git a/src/common/ipc/index.ts b/src/common/ipc/index.ts index 13c6af2fcb..ad1cf2a76c 100644 --- a/src/common/ipc/index.ts +++ b/src/common/ipc/index.ts @@ -19,6 +19,8 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +export const dialogShowOpenDialogHandler = "dialog:show-open-dialog"; + export * from "./ipc"; export * from "./invalid-kubeconfig"; export * from "./update-available.ipc"; diff --git a/src/common/ipc/ipc.ts b/src/common/ipc/ipc.ts index 074d73176c..f93a9486aa 100644 --- a/src/common/ipc/ipc.ts +++ b/src/common/ipc/ipc.ts @@ -23,12 +23,14 @@ // https://www.electronjs.org/docs/api/ipc-main // https://www.electronjs.org/docs/api/ipc-renderer -import { ipcMain, ipcRenderer, remote, webContents } from "electron"; +import { ipcMain, ipcRenderer, webContents } from "electron"; import { toJS } from "../utils/toJS"; import logger from "../../main/logger"; import { ClusterFrameInfo, clusterFrameMap } from "../cluster-frames"; import type { Disposer } from "../utils"; +const remote = ipcMain ? null : require("@electron/remote"); + const subFramesChannel = "ipc:get-sub-frames"; export async function requestMain(channel: string, ...args: any[]) { diff --git a/src/common/k8s-api/endpoints/helm-charts.api.ts b/src/common/k8s-api/endpoints/helm-charts.api.ts index 82d486ac81..706fcdf7f9 100644 --- a/src/common/k8s-api/endpoints/helm-charts.api.ts +++ b/src/common/k8s-api/endpoints/helm-charts.api.ts @@ -26,8 +26,7 @@ import type { RequestInit } from "node-fetch"; import { autoBind, bifurcateArray } from "../../utils"; import Joi from "joi"; -export type RepoHelmChartList = Record; -export type HelmChartList = Record; +export type RepoHelmChartList = Record; export interface IHelmChartDetails { readme: string; @@ -43,7 +42,7 @@ const endpoint = compile(`/v2/charts/:repo?/:name?`) as (params?: { * Get a list of all helm charts from all saved helm repos */ export async function listCharts(): Promise { - const data = await apiBase.get(endpoint()); + const data = await apiBase.get>(endpoint()); return Object .values(data) @@ -311,11 +310,9 @@ export class HelmChart { } static create(data: RawHelmChart, { onError = "throw" }: HelmChartCreateOpts = {}): HelmChart | undefined { - const result = helmChartValidator.validate(data, { + const { value, error } = helmChartValidator.validate(data, { abortEarly: false, }); - let { error } = result; - const { value } = result; if (!error) { return new HelmChart(value); @@ -331,13 +328,13 @@ export class HelmChart { return new HelmChart(value); } - error = new Joi.ValidationError(actualErrors.map(er => er.message).join(". "), actualErrors, error._original); + const validationError = new Joi.ValidationError(actualErrors.map(er => er.message).join(". "), actualErrors, error._original); if (onError === "throw") { - throw error; + throw validationError; } - console.warn("[HELM-CHART]: failed to validate data", data, error); + console.warn("[HELM-CHART]: failed to validate data", data, validationError); return undefined; } diff --git a/src/common/logger.ts b/src/common/logger.ts index 723d43f70c..15506f7a46 100644 --- a/src/common/logger.ts +++ b/src/common/logger.ts @@ -19,19 +19,22 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { app, ipcMain, remote } from "electron"; +import { ipcMain } from "electron"; import winston, { format } from "winston"; import type Transport from "winston-transport"; import { consoleFormat } from "winston-console-format"; import { isDebugging, isTestEnv } from "./vars"; import BrowserConsole from "winston-transport-browserconsole"; import { SentryTransport } from "./logger-transports"; +import { getPath } from "./utils/getPath"; const logLevel = process.env.LOG_LEVEL ? process.env.LOG_LEVEL : isDebugging ? "debug" - : "info"; + : isTestEnv + ? "error" + : "info"; const transports: Transport[] = [ new SentryTransport("error") @@ -69,7 +72,7 @@ if (!isTestEnv) { handleExceptions: false, level: logLevel, filename: "lens.log", - dirname: (app ?? remote?.app)?.getPath("logs"), + dirname: getPath("logs"), maxsize: 16 * 1024, maxFiles: 16, tailable: true, diff --git a/src/common/user-store/user-store.ts b/src/common/user-store/user-store.ts index 75e207a2ca..ee0022da1b 100644 --- a/src/common/user-store/user-store.ts +++ b/src/common/user-store/user-store.ts @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { app, remote } from "electron"; +import { app } from "electron"; import semver from "semver"; import { action, computed, observable, reaction, makeObservable } from "mobx"; import { BaseStore } from "../base-store"; @@ -32,7 +32,8 @@ import { fileNameMigration } from "../../migrations/user-store"; import { ObservableToggleSet, toJS } from "../../renderer/utils"; import { DESCRIPTORS, KubeconfigSyncValue, UserPreferencesModel, EditorConfiguration } from "./preferences-helpers"; import logger from "../../main/logger"; -import type {monaco} from "react-monaco-editor"; +import type { monaco } from "react-monaco-editor"; +import { getPath } from "../utils/getPath"; export interface UserStoreModel { lastSeenAppVersion: string; @@ -69,7 +70,7 @@ export class UserStore extends BaseStore /* implements UserStore @observable shell?: string; @observable downloadBinariesPath?: string; @observable kubectlBinariesPath?: string; - + /** * Download kubectl binaries matching cluster version */ @@ -86,7 +87,7 @@ export class UserStore extends BaseStore /* implements UserStore * Monaco editor configs */ @observable editorConfiguration:EditorConfiguration = {tabSize: null, miniMap: null, lineNumbers: null}; - + /** * The set of file/folder paths to be synced */ @@ -115,7 +116,7 @@ export class UserStore extends BaseStore /* implements UserStore }); }, { fireImmediately: true, - }); + }); } // Returns monaco editor options for selected editor type (the place, where a particular instance of the editor is mounted) @@ -244,5 +245,5 @@ export class UserStore extends BaseStore /* implements UserStore * @returns string */ export function getDefaultKubectlDownloadPath(): string { - return path.join((app || remote.app).getPath("userData"), "binaries"); + return path.join(getPath("userData"), "binaries"); } diff --git a/src/common/utils/getPath.ts b/src/common/utils/getPath.ts new file mode 100644 index 0000000000..01df73975b --- /dev/null +++ b/src/common/utils/getPath.ts @@ -0,0 +1,37 @@ +/** + * 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 { app, ipcMain } from "electron"; + +const remote = ipcMain ? null : require("@electron/remote"); + +/** + * calls getPath either on app or on the remote's app + * + * @deprecated Use a different method for accessing the getPath function + */ +export function getPath(name: Parameters[0]): string { + if (app) { + return app.getPath(name); + } + + return remote.app.getPath(name); +} diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index 3d7f4f9838..6035b9f408 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -40,6 +40,7 @@ export * from "./downloadFile"; export * from "./formatDuration"; export * from "./escapeRegExp"; export * from "./extended-map"; +export * from "./getPath"; export * from "./getRandId"; export * from "./hash-set"; export * from "./local-kubeconfig"; diff --git a/src/common/utils/local-kubeconfig.ts b/src/common/utils/local-kubeconfig.ts index 04ab27bfa4..8af595e5ec 100644 --- a/src/common/utils/local-kubeconfig.ts +++ b/src/common/utils/local-kubeconfig.ts @@ -19,13 +19,13 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { app, remote } from "electron"; import path from "path"; import * as uuid from "uuid"; import type { ClusterId } from "../cluster-types"; +import { getPath } from "./getPath"; export function storedKubeConfigFolder(): string { - return path.resolve((app || remote.app).getPath("userData"), "kubeconfigs"); + return path.resolve(getPath("userData"), "kubeconfigs"); } export function getCustomKubeConfigPath(clusterId: ClusterId = uuid.v4()): string { diff --git a/src/common/utils/sort-compare.ts b/src/common/utils/sort-compare.ts index cd4e1b128a..041a239bb5 100644 --- a/src/common/utils/sort-compare.ts +++ b/src/common/utils/sort-compare.ts @@ -19,7 +19,10 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import semver, { SemVer } from "semver"; +import semver, { coerce, SemVer } from "semver"; +import * as iter from "./iter"; +import type { RawHelmChart } from "../k8s-api/endpoints/helm-charts.api"; +import logger from "../logger"; export function sortCompare(left: T, right: T): -1 | 0 | 1 { if (left < right) { @@ -53,3 +56,32 @@ export function sortCompareChartVersions(left: ChartVersion, right: ChartVersion return sortCompare(left.version, right.version); } + + + +export function sortCharts(charts: RawHelmChart[]) { + interface ExtendedHelmChart extends RawHelmChart { + __version: SemVer; + } + + const chartsWithVersion = Array.from( + iter.map( + charts, + (chart => { + const __version = coerce(chart.version, { includePrerelease: true, loose: true }); + + if (!__version) { + logger.warn(`[HELM-SERVICE]: Version from helm chart is not loosely coercable to semver.`, { name: chart.name, version: chart.version, repo: chart.repo }); + } + + (chart as ExtendedHelmChart).__version = __version; + + return chart as ExtendedHelmChart; + }) + ), + ); + + return chartsWithVersion + .sort(sortCompareChartVersions) + .map(chart => (delete chart.__version, chart)); +} diff --git a/src/common/vars.ts b/src/common/vars.ts index dec81f313e..6462be4db5 100644 --- a/src/common/vars.ts +++ b/src/common/vars.ts @@ -35,6 +35,9 @@ export const isTestEnv = !!process.env.JEST_WORKER_ID; export const isDevelopment = !isTestEnv && !isProduction; export const isPublishConfigured = Object.keys(packageInfo.build).includes("publish"); +export const integrationTestingArg = "--integration-testing"; +export const isIntegrationTesting = process.argv.includes(integrationTestingArg); + export const productName = packageInfo.productName; export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}`; export const publicPath = "/build/" as string; diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts index 0f860a964d..2074de1183 100644 --- a/src/extensions/extension-loader.ts +++ b/src/extensions/extension-loader.ts @@ -19,14 +19,14 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { app, ipcRenderer, remote } from "electron"; +import { ipcRenderer } from "electron"; import { EventEmitter } from "events"; import { isEqual } from "lodash"; import { action, computed, makeObservable, observable, observe, reaction, when } from "mobx"; import path from "path"; import { ClusterStore } from "../common/cluster-store"; import { broadcastMessage, ipcMainOn, ipcRendererOn, requestMain, ipcMainHandle } from "../common/ipc"; -import { Disposer, getHostedClusterId, Singleton, toJS } from "../common/utils"; +import { Disposer, getHostedClusterId, Singleton, toJS, getPath } from "../common/utils"; import logger from "../main/logger"; import type { InstalledExtension } from "./extension-discovery"; import { ExtensionsStore } from "./extensions-store"; @@ -36,7 +36,7 @@ import type { LensRendererExtension } from "./lens-renderer-extension"; import * as registries from "./registries"; export function extensionPackagesRoot() { - return path.join((app || remote.app).getPath("userData")); + return path.join(getPath("userData")); } const logModule = "[EXTENSIONS-LOADER]"; diff --git a/src/main/cluster.ts b/src/main/cluster.ts index b3eef9613f..319f2d23df 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -33,7 +33,8 @@ import { VersionDetector } from "./cluster-detectors/version-detector"; import { DetectorRegistry } from "./cluster-detectors/detector-registry"; import plimit from "p-limit"; import { toJS } from "../common/utils"; -import { initialNodeShellImage, ClusterState, ClusterMetadataKey, ClusterRefreshOptions, ClusterStatus, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel } from "../common/cluster-types"; +import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel } from "../common/cluster-types"; +import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../common/cluster-types"; /** * Cluster diff --git a/src/main/exit-app.ts b/src/main/exit-app.ts index 2f9e3b00c7..8f9f85d2a4 100644 --- a/src/main/exit-app.ts +++ b/src/main/exit-app.ts @@ -26,14 +26,9 @@ import { ClusterManager } from "./cluster-manager"; import logger from "./logger"; export function exitApp() { - console.log("before windowManager"); const windowManager = WindowManager.getInstance(false); - - console.log("before clusterManager"); const clusterManager = ClusterManager.getInstance(false); - console.log("after clusterManager"); - appEventBus.emit({ name: "service", action: "close" }); windowManager?.hide(); clusterManager?.stop(); diff --git a/src/main/extension-filesystem.ts b/src/main/extension-filesystem.ts index 46afe45d6e..d7c213e483 100644 --- a/src/main/extension-filesystem.ts +++ b/src/main/extension-filesystem.ts @@ -21,13 +21,13 @@ import { randomBytes } from "crypto"; import { SHA256 } from "crypto-js"; -import { app, remote } from "electron"; import fse from "fs-extra"; import { action, makeObservable, observable } from "mobx"; import path from "path"; import { BaseStore } from "../common/base-store"; import type { LensExtensionId } from "../extensions/lens-extension"; import { toJS } from "../common/utils"; +import { getPath } from "../common/utils/getPath"; interface FSProvisionModel { extensions: Record; // extension names to paths @@ -55,7 +55,7 @@ export class FilesystemProvisionerStore extends BaseStore { if (!this.registeredExtensions.has(extensionName)) { const salt = randomBytes(32).toString("hex"); const hashedName = SHA256(`${extensionName}/${salt}`).toString(); - const dirPath = path.resolve((app || remote.app).getPath("userData"), "extension_data", hashedName); + const dirPath = path.resolve(getPath("userData"), "extension_data", hashedName); this.registeredExtensions.set(extensionName, dirPath); } diff --git a/src/main/helm/__mocks__/helm-chart-manager.ts b/src/main/helm/__mocks__/helm-chart-manager.ts index 6a511273da..09f071f842 100644 --- a/src/main/helm/__mocks__/helm-chart-manager.ts +++ b/src/main/helm/__mocks__/helm-chart-manager.ts @@ -19,141 +19,151 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { HelmRepo, HelmRepoManager } from "../helm-repo-manager"; +import { sortCharts } from "../../../common/utils"; +import type { HelmRepo } from "../helm-repo-manager"; + +const charts = new Map([ + ["stable", { + "invalid-semver": sortCharts([ + { + apiVersion: "3.0.0", + name: "weird-versioning", + version: "I am not semver", + repo: "stable", + digest: "test", + created: "now", + }, + { + apiVersion: "3.0.0", + name: "weird-versioning", + version: "v4.3.0", + repo: "stable", + digest: "test", + created: "now", + }, + { + apiVersion: "3.0.0", + name: "weird-versioning", + version: "I am not semver but more", + repo: "stable", + digest: "test", + created: "now", + }, + { + apiVersion: "3.0.0", + name: "weird-versioning", + version: "v4.4.0", + repo: "stable", + digest: "test", + created: "now", + }, + ]), + "apm-server": sortCharts([ + { + apiVersion: "3.0.0", + name: "apm-server", + version: "2.1.7", + repo: "stable", + digest: "test", + created: "now", + }, + { + apiVersion: "3.0.0", + name: "apm-server", + version: "2.1.6", + repo: "stable", + digest: "test", + created: "now", + } + ]), + "redis": sortCharts([ + { + apiVersion: "3.0.0", + name: "apm-server", + version: "1.0.0", + repo: "stable", + digest: "test", + created: "now", + }, + { + apiVersion: "3.0.0", + name: "apm-server", + version: "0.0.9", + repo: "stable", + digest: "test", + created: "now", + } + ]), + }], + ["experiment", { + "fairwind": sortCharts([ + { + apiVersion: "3.0.0", + name: "fairwind", + version: "0.0.1", + repo: "experiment", + digest: "test", + created: "now", + }, + { + apiVersion: "3.0.0", + name: "fairwind", + version: "0.0.2", + repo: "experiment", + digest: "test", + deprecated: true, + created: "now", + } + ]), + }], + ["bitnami", { + "hotdog": sortCharts([ + { + apiVersion: "3.0.0", + name: "hotdog", + version: "1.0.1", + repo: "bitnami", + digest: "test", + created: "now", + }, + { + apiVersion: "3.0.0", + name: "hotdog", + version: "1.0.2", + repo: "bitnami", + digest: "test", + created: "now", + } + ]), + "pretzel": sortCharts([ + { + apiVersion: "3.0.0", + name: "pretzel", + version: "1.0", + repo: "bitnami", + digest: "test", + created: "now", + }, + { + apiVersion: "3.0.0", + name: "pretzel", + version: "1.0.1", + repo: "bitnami", + digest: "test", + created: "now", + } + ]), + }], +]); export class HelmChartManager { - cache: any = {}; - private repo: HelmRepo; + constructor(private repo: HelmRepo){ } - constructor(repo: HelmRepo){ - this.cache = HelmRepoManager.cache; - this.repo = repo; + static forRepo(repo: HelmRepo) { + return new this(repo); } public async charts(): Promise { - switch (this.repo.name) { - case "stable": - return Promise.resolve({ - "invalid-semver": [ - { - apiVersion: "3.0.0", - name: "weird-versioning", - version: "I am not semver", - repo: "stable", - digest: "test" - }, - { - apiVersion: "3.0.0", - name: "weird-versioning", - version: "v4.3.0", - repo: "stable", - digest: "test" - }, - { - apiVersion: "3.0.0", - name: "weird-versioning", - version: "I am not semver but more", - repo: "stable", - digest: "test" - }, - { - apiVersion: "3.0.0", - name: "weird-versioning", - version: "v4.4.0", - repo: "stable", - digest: "test" - }, - ], - "apm-server": [ - { - apiVersion: "3.0.0", - name: "apm-server", - version: "2.1.7", - repo: "stable", - digest: "test" - }, - { - apiVersion: "3.0.0", - name: "apm-server", - version: "2.1.6", - repo: "stable", - digest: "test" - } - ], - "redis": [ - { - apiVersion: "3.0.0", - name: "apm-server", - version: "1.0.0", - repo: "stable", - digest: "test" - }, - { - apiVersion: "3.0.0", - name: "apm-server", - version: "0.0.9", - repo: "stable", - digest: "test" - } - ] - }); - case "experiment": - return Promise.resolve({ - "fairwind": [ - { - apiVersion: "3.0.0", - name: "fairwind", - version: "0.0.1", - repo: "experiment", - digest: "test" - }, - { - apiVersion: "3.0.0", - name: "fairwind", - version: "0.0.2", - repo: "experiment", - digest: "test", - deprecated: true - } - ] - }); - case "bitnami": - return Promise.resolve({ - "hotdog": [ - { - apiVersion: "3.0.0", - name: "hotdog", - version: "1.0.1", - repo: "bitnami", - digest: "test" - }, - { - apiVersion: "3.0.0", - name: "hotdog", - version: "1.0.2", - repo: "bitnami", - digest: "test", - } - ], - "pretzel": [ - { - apiVersion: "3.0.0", - name: "pretzel", - version: "1.0", - repo: "bitnami", - digest: "test", - }, - { - apiVersion: "3.0.0", - name: "pretzel", - version: "1.0.1", - repo: "bitnami", - digest: "test" - } - ] - }); - default: - return Promise.resolve({}); - } + return charts.get(this.repo.name) ?? {}; } } diff --git a/src/main/helm/__tests__/helm-service.test.ts b/src/main/helm/__tests__/helm-service.test.ts index 56127c1684..7eaf865464 100644 --- a/src/main/helm/__tests__/helm-service.test.ts +++ b/src/main/helm/__tests__/helm-service.test.ts @@ -31,7 +31,7 @@ describe("Helm Service tests", () => { jest.resetAllMocks(); }); - it("list charts without deprecated ones", async () => { + it("list charts with deprecated entries", async () => { mockHelmRepoManager.mockReturnValue({ init: jest.fn(), repositories: jest.fn().mockImplementation(async () => { @@ -52,14 +52,16 @@ describe("Helm Service tests", () => { name: "apm-server", version: "2.1.7", repo: "stable", - digest: "test" + digest: "test", + created: "now", }, { apiVersion: "3.0.0", name: "apm-server", version: "2.1.6", repo: "stable", - digest: "test" + digest: "test", + created: "now", } ], "invalid-semver": [ @@ -68,28 +70,32 @@ describe("Helm Service tests", () => { name: "weird-versioning", version: "v4.4.0", repo: "stable", - digest: "test" + digest: "test", + created: "now", }, { apiVersion: "3.0.0", name: "weird-versioning", version: "v4.3.0", repo: "stable", - digest: "test" + digest: "test", + created: "now", }, { apiVersion: "3.0.0", name: "weird-versioning", version: "I am not semver", repo: "stable", - digest: "test" + digest: "test", + created: "now", }, { apiVersion: "3.0.0", name: "weird-versioning", version: "I am not semver but more", repo: "stable", - digest: "test" + digest: "test", + created: "now", }, ], "redis": [ @@ -98,18 +104,40 @@ describe("Helm Service tests", () => { name: "apm-server", version: "1.0.0", repo: "stable", - digest: "test" + digest: "test", + created: "now", }, { apiVersion: "3.0.0", name: "apm-server", version: "0.0.9", repo: "stable", - digest: "test" + digest: "test", + created: "now", } ] }, - experiment: {} + experiment: { + "fairwind": [ + { + apiVersion: "3.0.0", + name: "fairwind", + version: "0.0.2", + repo: "experiment", + digest: "test", + deprecated: true, + created: "now", + }, + { + apiVersion: "3.0.0", + name: "fairwind", + version: "0.0.1", + repo: "experiment", + digest: "test", + created: "now", + }, + ] + } }); }); @@ -134,13 +162,15 @@ describe("Helm Service tests", () => { version: "1.0.2", repo: "bitnami", digest: "test", + created: "now", }, { apiVersion: "3.0.0", name: "hotdog", version: "1.0.1", repo: "bitnami", - digest: "test" + digest: "test", + created: "now", }, ], "pretzel": [ @@ -150,13 +180,15 @@ describe("Helm Service tests", () => { version: "1.0.1", repo: "bitnami", digest: "test", + created: "now", }, { apiVersion: "3.0.0", name: "pretzel", version: "1.0", repo: "bitnami", - digest: "test" + digest: "test", + created: "now", } ] } diff --git a/src/main/helm/helm-chart-manager.ts b/src/main/helm/helm-chart-manager.ts index 5523e0f050..b8261845db 100644 --- a/src/main/helm/helm-chart-manager.ts +++ b/src/main/helm/helm-chart-manager.ts @@ -20,27 +20,25 @@ */ import fs from "fs"; +import v8 from "v8"; import * as yaml from "js-yaml"; -import { HelmRepo, HelmRepoManager } from "./helm-repo-manager"; +import type { HelmRepo } from "./helm-repo-manager"; import logger from "../logger"; import { promiseExec } from "../promise-exec"; import { helmCli } from "./helm-cli"; import type { RepoHelmChartList } from "../../common/k8s-api/endpoints/helm-charts.api"; - -type CachedYaml = { - entries: RepoHelmChartList -}; +import { sortCharts } from "../../common/utils"; export class HelmChartManager { - protected cache: any = {}; - protected repo: HelmRepo; + static #cache = new Map(); - constructor(repo: HelmRepo){ - this.cache = HelmRepoManager.cache; - this.repo = repo; + private constructor(protected repo: HelmRepo) {} + + static forRepo(repo: HelmRepo) { + return new this(repo); } - public async chart(name: string) { + public async chartVersions(name: string) { const charts = await this.charts(); return charts[name]; @@ -48,9 +46,7 @@ export class HelmChartManager { public async charts(): Promise { try { - const cachedYaml = await this.cachedYaml(); - - return cachedYaml["entries"]; + return await this.cachedYaml(); } catch(error) { logger.error("HELM-CHART-MANAGER]: failed to list charts", { error }); @@ -58,48 +54,61 @@ export class HelmChartManager { } } - public async getReadme(name: string, version = "") { + private async executeCommand(action: string, name: string, version?: string) { const helm = await helmCli.binaryPath(); + const cmd = [`"${helm}" ${action} ${this.repo.name}/${name}`]; - if(version && version != "") { - const { stdout } = await promiseExec(`"${helm}" show readme ${this.repo.name}/${name} --version ${version}`).catch((error) => { throw(error.stderr);}); - - return stdout; - } else { - const { stdout } = await promiseExec(`"${helm}" show readme ${this.repo.name}/${name}`).catch((error) => { throw(error.stderr);}); + if (version) { + cmd.push("--version", version); + } + + try { + const { stdout } = await promiseExec(cmd.join(" ")); return stdout; + } catch (error) { + throw error.stderr || error; } } - public async getValues(name: string, version = "") { - const helm = await helmCli.binaryPath(); - - if(version && version != "") { - const { stdout } = await promiseExec(`"${helm}" show values ${this.repo.name}/${name} --version ${version}`).catch((error) => { throw(error.stderr);}); - - return stdout; - } else { - const { stdout } = await promiseExec(`"${helm}" show values ${this.repo.name}/${name}`).catch((error) => { throw(error.stderr);}); - - return stdout; - } + public async getReadme(name: string, version?: string) { + return this.executeCommand("show readme", name, version); } - protected async cachedYaml(): Promise { - if (!(this.repo.name in this.cache)) { + public async getValues(name: string, version?: string) { + return this.executeCommand("show values", name, version); + } + + protected async cachedYaml(): Promise { + if (!HelmChartManager.#cache.has(this.repo.name)) { const cacheFile = await fs.promises.readFile(this.repo.cacheFilePath, "utf-8"); - const data = yaml.safeLoad(cacheFile); + const { entries } = yaml.safeLoad(cacheFile) as { entries: RepoHelmChartList }; - for(const key in data["entries"]) { - data["entries"][key].forEach((version: any) => { - version["repo"] = this.repo.name; - version["created"] = Date.parse(version.created).toString(); - }); - } - this.cache[this.repo.name] = Buffer.from(JSON.stringify(data)); + /** + * Do some initial preprocessing on the data, so as to avoid needing to do it later + * 1. Set the repo name + * 2. Normalize the created date + * 3. Filter out deprecated items + */ + + const normalized = Object.fromEntries( + Object.entries(entries) + .map(([name, charts]) => [ + name, + sortCharts( + charts.map(chart => ({ + ...chart, + created: Date.parse(chart.created).toString(), + repo: this.repo.name, + })), + ), + ] as const) + .filter(([, charts]) => !charts.every(chart => chart.deprecated)) + ); + + HelmChartManager.#cache.set(this.repo.name, v8.serialize(normalized)); } - return JSON.parse(this.cache[this.repo.name].toString()); + return v8.deserialize(HelmChartManager.#cache.get(this.repo.name)); } } diff --git a/src/main/helm/helm-repo-manager.ts b/src/main/helm/helm-repo-manager.ts index d1c904539d..9e4038a10f 100644 --- a/src/main/helm/helm-repo-manager.ts +++ b/src/main/helm/helm-repo-manager.ts @@ -50,8 +50,6 @@ export interface HelmRepo { } export class HelmRepoManager extends Singleton { - static cache = {}; // todo: remove implicit updates in helm-chart-manager.ts - protected repos: HelmRepo[]; protected helmEnv: HelmEnv; protected initialized: boolean; @@ -97,6 +95,12 @@ export class HelmRepoManager extends Singleton { return env; } + public async repo(name: string): Promise { + const repos = await this.repositories(); + + return repos.find(repo => repo.name === name); + } + public async repositories(): Promise { try { if (!this.initialized) { diff --git a/src/main/helm/helm-service.ts b/src/main/helm/helm-service.ts index 2fe531c215..41c8438cb3 100644 --- a/src/main/helm/helm-service.ts +++ b/src/main/helm/helm-service.ts @@ -19,14 +19,11 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import semver, { SemVer } from "semver"; import type { Cluster } from "../cluster"; import logger from "../logger"; import { HelmRepoManager } from "./helm-repo-manager"; import { HelmChartManager } from "./helm-chart-manager"; -import type { HelmChart, HelmChartList, RepoHelmChartList } from "../../common/k8s-api/endpoints/helm-charts.api"; import { deleteRelease, getHistory, getRelease, getValues, installChart, listReleases, rollback, upgradeRelease } from "./helm-release-manager"; -import { iter, sortCompareChartVersions } from "../../common/utils"; interface GetReleaseValuesArgs { cluster: Cluster; @@ -42,43 +39,27 @@ class HelmService { } public async listCharts() { - const charts: HelmChartList = {}; const repositories = await HelmRepoManager.getInstance().repositories(); - for (const repo of repositories) { - charts[repo.name] = {}; - const manager = new HelmChartManager(repo); - const sortedCharts = this.sortChartsByVersion(await manager.charts()); - const enabledCharts = this.excludeDeprecatedChartGroups(sortedCharts); - - charts[repo.name] = enabledCharts; - } - - return charts; + return Object.fromEntries( + await Promise.all(repositories.map(async repo => [repo.name, await HelmChartManager.forRepo(repo).charts()])) + ); } public async getChart(repoName: string, chartName: string, version = "") { - const result = { - readme: "", - versions: {} + const repo = await HelmRepoManager.getInstance().repo(repoName); + const chartManager = HelmChartManager.forRepo(repo); + + return { + readme: await chartManager.getReadme(chartName, version), + versions: await chartManager.chartVersions(chartName), }; - const repos = await HelmRepoManager.getInstance().repositories(); - const repo = repos.find(repo => repo.name === repoName); - const chartManager = new HelmChartManager(repo); - const chart = await chartManager.chart(chartName); - - result.readme = await chartManager.getReadme(chartName, version); - result.versions = chart; - - return result; } public async getChartValues(repoName: string, chartName: string, version = "") { - const repos = await HelmRepoManager.getInstance().repositories(); - const repo = repos.find(repo => repo.name === repoName); - const chartManager = new HelmChartManager(repo); + const repo = await HelmRepoManager.getInstance().repo(repoName); - return chartManager.getValues(chartName, version); + return HelmChartManager.forRepo(repo).getValues(chartName, version); } public async listReleases(cluster: Cluster, namespace: string = null) { @@ -131,58 +112,6 @@ class HelmService { return { message: output }; } - - private excludeDeprecatedChartGroups(chartGroups: RepoHelmChartList) { - return Object.fromEntries( - iter.filterMap( - Object.entries(chartGroups), - ([name, charts]) => { - for (const chart of charts) { - if (chart.deprecated) { - // ignore chart group if any chart is deprecated - return undefined; - } - } - - return [name, charts]; - } - ) - ); - } - - private sortCharts(charts: HelmChart[]) { - interface ExtendedHelmChart extends HelmChart { - __version: SemVer; - } - - const chartsWithVersion = Array.from( - iter.map( - charts, - (chart => { - const __version = semver.coerce(chart.version, { includePrerelease: true, loose: true }); - - if (!__version) { - logger.error(`[HELM-SERVICE]: Version from helm chart is not loosely coercable to semver.`, { name: chart.name, version: chart.version, repo: chart.repo }); - } - - (chart as ExtendedHelmChart).__version = __version; - - return chart as ExtendedHelmChart; - }) - ), - ); - - return chartsWithVersion - .sort(sortCompareChartVersions) - .map(chart => (delete chart.__version, chart as HelmChart)); - } - - private sortChartsByVersion(chartGroups: RepoHelmChartList) { - return Object.fromEntries( - Object.entries(chartGroups) - .map(([name, charts]) => [name, this.sortCharts(charts)]) - ); - } } export const helmService = new HelmService(); diff --git a/src/main/index.ts b/src/main/index.ts index c8a9d105d2..ce3cafd926 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -22,12 +22,12 @@ // Main process import "../common/system-ca"; +import { initialize as initializeRemote } from "@electron/remote/main"; import * as Mobx from "mobx"; import * as LensExtensionsCommonApi from "../extensions/common-api"; import * as LensExtensionsMainApi from "../extensions/main-api"; import { app, autoUpdater, dialog, powerMonitor } from "electron"; -import { appName, isMac, productName } from "../common/vars"; -import path from "path"; +import { appName, isIntegrationTesting, isMac, productName } from "../common/vars"; import { LensProxy } from "./lens-proxy"; import { WindowManager } from "./window-manager"; import { ClusterManager } from "./cluster-manager"; @@ -63,13 +63,13 @@ import { ensureDir } from "fs-extra"; import { Router } from "./router"; import { initMenu } from "./menu"; import { initTray } from "./tray"; +import * as path from "path"; import { kubeApiRequest, shellApiRequest } from "./proxy-functions"; +const onCloseCleanup = disposer(); +const onQuitCleanup = disposer(); + SentryInit(); - -const workingDir = path.join(app.getPath("appData"), appName); -const cleanup = disposer(); - app.setName(appName); logger.info(`📟 Setting ${productName} as protocol client for lens://`); @@ -80,14 +80,16 @@ if (app.setAsDefaultProtocolClient("lens")) { logger.info("📟 Protocol client register failed ❗"); } -if (!process.env.CICD) { - app.setPath("userData", workingDir); +if (process.env.CICD) { + app.setPath("appData", process.env.CICD); + app.setPath("userData", path.join(process.env.CICD, appName)); } if (process.env.LENS_DISABLE_GPU) { app.disableHardwareAcceleration(); } +initializeRemote(); configurePackages(); mangleProxyEnv(); initializers.initIpcMainHandlers(); @@ -121,7 +123,7 @@ app.on("second-instance", (event, argv) => { }); app.on("ready", async () => { - logger.info(`🚀 Starting ${productName} from "${workingDir}"`); + logger.info(`🚀 Starting ${productName} from "${app.getPath("exe")}"`); logger.info("🐚 Syncing shell environment"); await shellSync(); @@ -209,7 +211,7 @@ app.on("ready", async () => { logger.info("🖥️ Starting WindowManager"); const windowManager = WindowManager.createInstance(); - cleanup.push( + onQuitCleanup.push( initMenu(windowManager), initTray(windowManager), ); @@ -221,7 +223,7 @@ app.on("ready", async () => { } ipcMainOn(IpcRendererNavigationEvents.LOADED, async () => { - cleanup.push(pushCatalogToRenderer(catalogEntityRegistry)); + onCloseCleanup.push(pushCatalogToRenderer(catalogEntityRegistry)); await ensureDir(storedKubeConfigFolder()); KubeconfigSyncManager.getInstance().startSync(); startUpdateChecking(); @@ -269,7 +271,7 @@ app.on("activate", (event, hasVisibleWindows) => { /** * This variable should is used so that `autoUpdater.installAndQuit()` works */ -let blockQuit = true; +let blockQuit = !isIntegrationTesting; autoUpdater.on("before-quit-for-update", () => blockQuit = false); @@ -282,7 +284,7 @@ app.on("will-quit", (event) => { appEventBus.emit({ name: "app", action: "close" }); ClusterManager.getInstance(false)?.stop(); // close cluster connections KubeconfigSyncManager.getInstance(false)?.stopSync(); - cleanup(); + onCloseCleanup(); if (lprm) { // This is set to false here so that LPRM can wait to send future lens:// @@ -298,7 +300,8 @@ app.on("will-quit", (event) => { return; // skip exit to make tray work, to quit go to app's global menu or tray's menu } - LensProtocolRouterMain.getInstance(false)?.cleanup(); + lprm?.cleanup(); + onQuitCleanup(); }); app.on("open-url", (event, rawUrl) => { diff --git a/src/main/initializers/ipc.ts b/src/main/initializers/ipc.ts index efcd52a5a4..3c3e14f923 100644 --- a/src/main/initializers/ipc.ts +++ b/src/main/initializers/ipc.ts @@ -19,20 +19,21 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import type { IpcMainInvokeEvent } from "electron"; +import { BrowserWindow, dialog, IpcMainInvokeEvent } from "electron"; import { KubernetesCluster } from "../../common/catalog-entities"; import { clusterFrameMap } from "../../common/cluster-frames"; import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler } from "../../common/cluster-ipc"; import { ClusterStore } from "../../common/cluster-store"; import type { ClusterId } from "../../common/cluster-types"; import { appEventBus } from "../../common/event-bus"; -import { ipcMainHandle } from "../../common/ipc"; +import { dialogShowOpenDialogHandler, ipcMainHandle } from "../../common/ipc"; import { catalogEntityRegistry } from "../catalog"; import { ClusterManager } from "../cluster-manager"; import { bundledKubectlPath } from "../kubectl"; import logger from "../logger"; import { promiseExecFile } from "../promise-exec"; import { ResourceApplier } from "../resource-applier"; +import { WindowManager } from "../window-manager"; export function initIpcMainHandlers() { ipcMainHandle(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => { @@ -138,4 +139,10 @@ export function initIpcMainHandlers() { throw `${clusterId} is not a valid cluster id`; } }); + + ipcMainHandle(dialogShowOpenDialogHandler, async (event, dialogOpts: Electron.OpenDialogOptions) => { + await WindowManager.getInstance().ensureMainWindow(); + + return dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), dialogOpts); + }); } diff --git a/src/main/kubectl.ts b/src/main/kubectl.ts index 87166e597a..fbfd044dab 100644 --- a/src/main/kubectl.ts +++ b/src/main/kubectl.ts @@ -19,7 +19,6 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { app, remote } from "electron"; import path from "path"; import fs from "fs"; import { promiseExec } from "./promise-exec"; @@ -32,6 +31,7 @@ import { customRequest } from "../common/request"; import { getBundledKubectlVersion } from "../common/utils/app-version"; import { isDevelopment, isWindows, isTestEnv } from "../common/vars"; import { SemVer } from "semver"; +import { getPath } from "../common/utils/getPath"; const bundledVersion = getBundledKubectlVersion(); const kubectlMap: Map = new Map([ @@ -84,7 +84,7 @@ export class Kubectl { protected dirname: string; static get kubectlDir() { - return path.join((app || remote.app).getPath("userData"), "binaries", "kubectl"); + return path.join(getPath("userData"), "binaries", "kubectl"); } public static readonly bundledKubectlVersion: string = bundledVersion; diff --git a/src/main/lens-binary.ts b/src/main/lens-binary.ts index d5e9b943a7..2c52b05db5 100644 --- a/src/main/lens-binary.ts +++ b/src/main/lens-binary.ts @@ -60,13 +60,13 @@ export class LensBinary { this.logger = console; let arch = null; - if (process.arch == "x64") { + if (process.env.BINARY_ARCH) { + arch = process.env.BINARY_ARCH; + } else if (process.arch == "x64") { arch = "amd64"; - } - else if (process.arch == "x86" || process.arch == "ia32") { + } else if (process.arch == "x86" || process.arch == "ia32") { arch = "386"; - } - else { + } else { arch = process.arch; } this.arch = arch; diff --git a/src/main/menu.ts b/src/main/menu.ts index b1ffff6a57..34bd724d0a 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -83,7 +83,7 @@ export function buildMenu(windowManager: WindowManager) { accelerator: "CmdOrCtrl+,", click() { navigate(preferencesURL()); - } + }, }, { label: "Extensions", @@ -106,7 +106,7 @@ export function buildMenu(windowManager: WindowManager) { exitApp(); } } - ] + ], }; const fileMenu: MenuItemConstructorOptions = { label: "File", @@ -150,7 +150,7 @@ export function buildMenu(windowManager: WindowManager) { } } ]) - ] + ], }; const editMenu: MenuItemConstructorOptions = { label: "Edit", diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index f1715a4284..6adba1a987 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -89,7 +89,8 @@ export class WindowManager extends Singleton { nodeIntegration: true, nodeIntegrationInSubFrames: true, enableRemoteModule: true, - webviewTag: true + webviewTag: true, + contextIsolation: false, }, }); this.windowState.manage(this.mainWindow); @@ -257,7 +258,10 @@ export class WindowManager extends Singleton { resizable: false, show: false, webPreferences: { - nodeIntegration: true + nodeIntegration: true, + enableRemoteModule: true, + contextIsolation: false, + nodeIntegrationInSubFrames: true, } }); await this.splashWindow.loadURL("static://splash.html"); diff --git a/src/migrations/user-store/file-name-migration.ts b/src/migrations/user-store/file-name-migration.ts index 122cd78102..10d7a7c99b 100644 --- a/src/migrations/user-store/file-name-migration.ts +++ b/src/migrations/user-store/file-name-migration.ts @@ -20,11 +20,11 @@ */ import fse from "fs-extra"; -import { app, remote } from "electron"; import path from "path"; +import { getPath } from "../../common/utils/getPath"; export function fileNameMigration() { - const userDataPath = (app || remote.app).getPath("userData"); + const userDataPath = getPath("userData"); const configJsonPath = path.join(userDataPath, "config.json"); const lensUserStoreJsonPath = path.join(userDataPath, "lens-user-store.json"); diff --git a/src/renderer/components/+apps-helm-charts/helm-chart-details.scss b/src/renderer/components/+apps-helm-charts/helm-chart-details.scss index 13521ce583..7bee3b7f10 100644 --- a/src/renderer/components/+apps-helm-charts/helm-chart-details.scss +++ b/src/renderer/components/+apps-helm-charts/helm-chart-details.scss @@ -30,6 +30,12 @@ box-sizing: content-box; } + .Select__option { + span.deprecated { + text-decoration: line-through; + } + } + .intro-contents { .description { font-weight: bold; diff --git a/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx b/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx index 1cedc10d3f..041f7b1cbd 100644 --- a/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx +++ b/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx @@ -33,12 +33,19 @@ import { Button } from "../button"; import { Select, SelectOption } from "../select"; import { createInstallChartTab } from "../dock/install-chart.store"; import { Badge } from "../badge"; +import { Tooltip, withStyles } from "@material-ui/core"; interface Props { chart: HelmChart; hideDetails(): void; } +const LargeTooltip = withStyles({ + tooltip: { + fontSize: "var(--font-size-small)", + } +})(Tooltip); + @observer export class HelmChartDetails extends Component { @observable chartVersions: HelmChart[]; @@ -73,15 +80,15 @@ export class HelmChartDetails extends Component { }); @boundMethod - async onVersionChange({ value: version }: SelectOption) { - this.selectedChart = this.chartVersions.find(chart => chart.version === version); + async onVersionChange({ value: chart }: SelectOption) { + this.selectedChart = chart; this.readme = null; try { this.abortController?.abort(); this.abortController = new AbortController(); const { chart: { name, repo } } = this.props; - const { readme } = await getChartDetails(repo, name, { version, reqInit: { signal: this.abortController.signal }}); + const { readme } = await getChartDetails(repo, name, { version: chart.version, reqInit: { signal: this.abortController.signal }}); this.readme = readme; } catch (error) { @@ -115,7 +122,19 @@ export class HelmChartDetails extends Component {