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

Release 6.1.3 (#6354)

* Release 6.1.3

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* ignore prerelease tag for kubectl version to download (#6299)

Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com>

Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com>

* Split integration tests into seperate jobs from unit tests for faster CI (#6310)

* Split integration tests into seperate jobs from unit tests for faster CI

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add logging

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Simplify the matrix

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove steps that are part of Makefile

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix yml decl

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Switch to using single quotes

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Further clarify the test job names

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix invocation

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Attempt to fix traking stdout

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix lint

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* And handling for tests failing to start

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add check for app early exiting

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add more logging to help with debugging

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Cleanup attemptStart code

Signed-off-by: Sebastian Malton <sebastian@malton.name>

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix loading helm release details (#6318)

* Fix loading helm release details

- The helm manifest can sometimes contain KubeJsonApiDataLists
  instead of just KubeJsonApiData entries

- Add additional logging to main for when a route handler throws so that
  we can gain more context in the future

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix usage of getHelmReleaseResources

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add test to verify handling of Lists being returned

Signed-off-by: Sebastian Malton <sebastian@malton.name>

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* improve shellEnv (#6351)

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Make creating releases automatic on merge of release PRs (#6353)

* Remove unused bundled-extensions file

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove unused release drafter workflow

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Tag releases created using create-release-pr with the release label

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove the unneeded tag-release script

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add workflow for creating release on the merging of a release PR

Signed-off-by: Sebastian Malton <sebastian@malton.name>

Signed-off-by: Sebastian Malton <sebastian@malton.name>

Signed-off-by: Sebastian Malton <sebastian@malton.name>
Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com>
Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
Co-authored-by: Jim Ehrismann <40840436+jim-docker@users.noreply.github.com>
Co-authored-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Sebastian Malton 2022-10-07 10:32:16 -04:00 committed by GitHub
parent 7efe974966
commit af47379b54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 374 additions and 510 deletions

View File

@ -1,8 +0,0 @@
{
"extensions": [
"pod-menu",
"node-menu",
"metrics-cluster-feature",
"kube-object-event-status"
]
}

View File

@ -1,16 +0,0 @@
name: Release Drafter
on:
push:
# branches to consider in the event; optional, defaults to all
branches:
- master
jobs:
update_release_draft:
runs-on: ubuntu-latest
steps:
# Drafts your next Release notes as Pull Requests are merged into "master"
- uses: release-drafter/release-drafter@v5
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}

30
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,30 @@
name: Release Open Lens
on:
pull_request:
types:
- closed
branches:
- master
- release/v*.*
jobs:
release:
name: Release
if: ${{ github.event.pull_request.merged == 'true' && contains(github.event.pull_request.labels.*.name, 'release') }}
runs-on: ubuntu-latest
steps:
- name: Checkout Release from lens
uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: butlerlogic/action-autotag@stable
id: tagger
with:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
tag_prefix: "v"
- uses: ncipollo/release-action@v1
if: ${{ needs.tag.outputs.tagname != '' }}
with:
name: v${{ steps.open-lens-version.outputs.VERSION }}
commit: master
tag: v${{ steps.open-lens-version.outputs.VERSION }}
body: ${{ github.event.pull_request.body }}

View File

@ -7,13 +7,14 @@ on:
branches: branches:
- master - master
jobs: jobs:
build: test:
name: Test name: ${{ matrix.type }} tests on ${{ matrix.os }}
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-20.04, macos-11, windows-2019] os: [ubuntu-20.04, macos-11, windows-2019]
type: [unit, smoke]
node-version: [16.x] node-version: [16.x]
steps: steps:
- name: Checkout Release from lens - name: Checkout Release from lens
@ -51,25 +52,16 @@ jobs:
retry_on: error retry_on: error
command: make node_modules command: make node_modules
- run: make build-npm
name: Generate npm package
- uses: nick-fields/retry@v2
name: Build bundled extensions
with:
timeout_minutes: 15
max_attempts: 3
retry_on: error
command: make -j2 build-extensions
- run: make test - run: make test
name: Run tests name: Run tests
if: ${{ matrix.type == 'unit' }}
- run: make test-extensions - run: make test-extensions
name: Run In-tree Extension tests name: Run In-tree Extension tests
if: ${{ matrix.type == 'unit' }}
- run: make ci-validate-dev - run: make ci-validate-dev
if: contains(github.event.pull_request.labels.*.name, 'dependencies') if: ${{ contains(github.event.pull_request.labels.*.name, 'dependencies') && matrix.type == 'unit' }}
name: Validate dev mode will work name: Validate dev mode will work
- name: Install integration test dependencies - name: Install integration test dependencies
@ -77,7 +69,7 @@ jobs:
uses: medyagh/setup-minikube@master uses: medyagh/setup-minikube@master
with: with:
minikube-version: latest minikube-version: latest
if: runner.os == 'Linux' if: ${{ runner.os == 'Linux' && matrix.type == 'smoke' }}
- run: xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' make integration - run: xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' make integration
name: Run Linux integration tests name: Run Linux integration tests
@ -88,11 +80,11 @@ jobs:
shell: bash shell: bash
env: env:
ELECTRON_BUILDER_EXTRA_ARGS: "--x64 --arm64" ELECTRON_BUILDER_EXTRA_ARGS: "--x64 --arm64"
if: runner.os == 'macOS' if: ${{ runner.os == 'macOS' && matrix.type == 'smoke' }}
- run: make integration - run: make integration
name: Run Windows integration tests name: Run Windows integration tests
shell: bash shell: bash
env: env:
ELECTRON_BUILDER_EXTRA_ARGS: "--x64 --ia32" ELECTRON_BUILDER_EXTRA_ARGS: "--x64 --ia32"
if: runner.os == 'Windows' if: ${{ runner.os == 'Windows' && matrix.type == 'smoke' }}

View File

@ -13,7 +13,8 @@ import type { ElectronApplication, Page } from "playwright";
import * as utils from "../helpers/utils"; import * as utils from "../helpers/utils";
describe("preferences page tests", () => { describe("preferences page tests", () => {
let window: Page, cleanup: () => Promise<void>; let window: Page;
let cleanup: undefined | (() => Promise<void>);
beforeEach(async () => { beforeEach(async () => {
let app: ElectronApplication; let app: ElectronApplication;
@ -30,7 +31,7 @@ describe("preferences page tests", () => {
}, 10*60*1000); }, 10*60*1000);
afterEach(async () => { afterEach(async () => {
await cleanup(); await cleanup?.();
}, 10*60*1000); }, 10*60*1000);
it('shows "preferences" and can navigate through the tabs', async () => { it('shows "preferences" and can navigate through the tabs', async () => {

View File

@ -18,7 +18,9 @@ import { pipeline } from "@ogre-tools/fp";
const TEST_NAMESPACE = "integration-tests"; const TEST_NAMESPACE = "integration-tests";
utils.describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => { utils.describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => {
let window: Page, cleanup: () => Promise<void>, frame: Frame; let window: Page;
let cleanup: undefined | (() => Promise<void>);
let frame: Frame;
beforeEach(async () => { beforeEach(async () => {
({ window, cleanup } = await utils.start()); ({ window, cleanup } = await utils.start());
@ -28,7 +30,7 @@ utils.describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => {
}, 10 * 60 * 1000); }, 10 * 60 * 1000);
afterEach(async () => { afterEach(async () => {
await cleanup(); await cleanup?.();
}, 10 * 60 * 1000); }, 10 * 60 * 1000);
it("shows cluster context menu in sidebar", async () => { it("shows cluster context menu in sidebar", async () => {

View File

@ -7,7 +7,9 @@ import type { ElectronApplication, Page } from "playwright";
import * as utils from "../helpers/utils"; import * as utils from "../helpers/utils";
describe("Lens command palette", () => { describe("Lens command palette", () => {
let window: Page, cleanup: () => Promise<void>, app: ElectronApplication; let window: Page;
let cleanup: undefined | (() => Promise<void>);
let app: ElectronApplication;
beforeEach(async () => { beforeEach(async () => {
({ window, cleanup, app } = await utils.start()); ({ window, cleanup, app } = await utils.start());
@ -15,7 +17,7 @@ describe("Lens command palette", () => {
}, 10*60*1000); }, 10*60*1000);
afterEach(async () => { afterEach(async () => {
await cleanup(); await cleanup?.();
}, 10*60*1000); }, 10*60*1000);
describe("menu", () => { describe("menu", () => {

View File

@ -10,6 +10,7 @@ import * as uuid from "uuid";
import type { ElectronApplication, Frame, Page } from "playwright"; import type { ElectronApplication, Frame, Page } from "playwright";
import { _electron as electron } from "playwright"; import { _electron as electron } from "playwright";
import { noop } from "lodash"; import { noop } from "lodash";
import { disposer } from "../../src/common/utils";
export const appPaths: Partial<Record<NodeJS.Platform, string>> = { export const appPaths: Partial<Record<NodeJS.Platform, string>> = {
"win32": "./dist/win-unpacked/OpenLens.exe", "win32": "./dist/win-unpacked/OpenLens.exe",
@ -26,19 +27,46 @@ export function describeIf(condition: boolean) {
} }
async function getMainWindow(app: ElectronApplication, timeout = 50_000): Promise<Page> { async function getMainWindow(app: ElectronApplication, timeout = 50_000): Promise<Page> {
const deadline = Date.now() + timeout; return new Promise((resolve, reject) => {
const cleanup = disposer();
let stdoutBuf = "";
const onWindow = (page: Page) => {
console.log(`Page opened: ${page.url()}`);
for (; Date.now() < deadline;) {
for (const page of app.windows()) {
if (page.url().startsWith("http://localhost")) { if (page.url().startsWith("http://localhost")) {
return page; cleanup();
} console.log(stdoutBuf);
resolve(page);
} }
};
await new Promise(resolve => setTimeout(resolve, 2_000)); app.on("window", onWindow);
} cleanup.push(() => app.off("window", onWindow));
throw new Error(`Lens did not open the main window within ${timeout}ms`); const onClose = () => {
cleanup();
reject(new Error("App has unnexpectedly closed"));
};
app.on("close", onClose);
cleanup.push(() => app.off("close", onClose));
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const stdout = app.process().stdout!;
const onData = (chunk: any) => stdoutBuf += chunk.toString();
stdout.on("data", onData);
cleanup.push(() => stdout.off("data", onData));
const timeoutId = setTimeout(() => {
cleanup();
console.log(stdoutBuf);
reject(new Error(`Lens did not open the main window within ${timeout}ms`));
}, timeout);
cleanup.push(() => clearTimeout(timeoutId));
});
} }
async function attemptStart() { async function attemptStart() {
@ -57,7 +85,7 @@ async function attemptStart() {
...process.env, ...process.env,
}, },
timeout: 100_000, timeout: 100_000,
} as Parameters<typeof electron["launch"]>[0]); });
try { try {
const window = await getMainWindow(app); const window = await getMainWindow(app);
@ -78,6 +106,8 @@ async function attemptStart() {
} }
export async function start() { export async function start() {
console.log(process.env);
// this is an attempted workaround for an issue with playwright not always getting the main window when using Electron 14.2.4 (observed on windows) // this is an attempted workaround for an issue with playwright not always getting the main window when using Electron 14.2.4 (observed on windows)
for (let i = 0; ; i++) { for (let i = 0; ; i++) {
try { try {

View File

@ -3,7 +3,7 @@
"productName": "OpenLens", "productName": "OpenLens",
"description": "OpenLens - Open Source IDE for Kubernetes", "description": "OpenLens - Open Source IDE for Kubernetes",
"homepage": "https://github.com/lensapp/lens", "homepage": "https://github.com/lensapp/lens",
"version": "6.1.2", "version": "6.1.3",
"main": "static/build/main.js", "main": "static/build/main.js",
"copyright": "© 2022 OpenLens Authors", "copyright": "© 2022 OpenLens Authors",
"license": "MIT", "license": "MIT",
@ -279,8 +279,7 @@
"request-promise-native": "^1.0.9", "request-promise-native": "^1.0.9",
"rfc6902": "^4.0.2", "rfc6902": "^4.0.2",
"selfsigned": "^2.1.1", "selfsigned": "^2.1.1",
"semver": "^7.3.7", "semver": "^7.3.8",
"shell-env": "^3.0.1",
"spdy": "^4.0.2", "spdy": "^4.0.2",
"tar": "^6.1.11", "tar": "^6.1.11",
"tcp-port-used": "^1.0.2", "tcp-port-used": "^1.0.2",

View File

@ -296,6 +296,7 @@ const createPrArgs = [
"--base", prBase, "--base", prBase,
"--title", `Release ${newVersion.format()}`, "--title", `Release ${newVersion.format()}`,
"--label", "skip-changelog", "--label", "skip-changelog",
"--label", "release",
"--body-file", "-", "--body-file", "-",
]; ];

View File

@ -1,21 +0,0 @@
#!/bin/bash
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
-f|--force)
FORCE="--force"
shift # past argument
;;
esac
done
if [[ `git branch --show-current` =~ ^release/v ]]
then
VERSION_STRING=$(cat package.json | jq '.version' -r | xargs printf "v%s")
git tag ${VERSION_STRING} ${FORCE}
git push ${GIT_REMOTE:-origin} ${VERSION_STRING} ${FORCE}
else
echo "You must be in a release branch"
fi

View File

@ -17,6 +17,7 @@ export function keys<K extends keyof any>(obj: Record<K, any>): K[] {
return Object.keys(obj) as K[]; return Object.keys(obj) as K[];
} }
export function entries<K extends string, V>(obj: Partial<Record<K, V>> | null | undefined): [K, V][];
export function entries<K extends string | number | symbol, V>(obj: Partial<Record<K, V>> | null | undefined): [K, V][]; export function entries<K extends string | number | symbol, V>(obj: Partial<Record<K, V>> | null | undefined): [K, V][];
export function entries<K extends string | number | symbol, V>(obj: Record<K, V> | null | undefined): [K, V][]; export function entries<K extends string | number | symbol, V>(obj: Record<K, V> | null | undefined): [K, V][];

View File

@ -5523,11 +5523,6 @@ exports[`showing details for helm release given application is started when navi
> >
Namespace Namespace
</div> </div>
<div
class="TableCell age"
>
Age
</div>
</div> </div>
<div <div
class="TableRow" class="TableRow"
@ -5542,11 +5537,6 @@ exports[`showing details for helm release given application is started when navi
> >
some-namespace some-namespace
</div> </div>
<div
class="TableCell age"
>
0s
</div>
</div> </div>
</div> </div>
</div> </div>
@ -6747,11 +6737,6 @@ exports[`showing details for helm release given application is started when navi
> >
Namespace Namespace
</div> </div>
<div
class="TableCell age"
>
Age
</div>
</div> </div>
<div <div
class="TableRow" class="TableRow"
@ -6766,11 +6751,6 @@ exports[`showing details for helm release given application is started when navi
> >
some-namespace some-namespace
</div> </div>
<div
class="TableCell age"
>
0s
</div>
</div> </div>
</div> </div>
</div> </div>
@ -7971,11 +7951,6 @@ exports[`showing details for helm release given application is started when navi
> >
Namespace Namespace
</div> </div>
<div
class="TableCell age"
>
Age
</div>
</div> </div>
<div <div
class="TableRow" class="TableRow"
@ -7990,11 +7965,6 @@ exports[`showing details for helm release given application is started when navi
> >
some-namespace some-namespace
</div> </div>
<div
class="TableCell age"
>
0s
</div>
</div> </div>
</div> </div>
</div> </div>
@ -9022,11 +8992,6 @@ exports[`showing details for helm release given application is started when navi
> >
Namespace Namespace
</div> </div>
<div
class="TableCell age"
>
Age
</div>
</div> </div>
<div <div
class="TableRow" class="TableRow"
@ -9041,11 +9006,6 @@ exports[`showing details for helm release given application is started when navi
> >
some-namespace some-namespace
</div> </div>
<div
class="TableCell age"
>
0s
</div>
</div> </div>
</div> </div>
</div> </div>
@ -10075,11 +10035,6 @@ exports[`showing details for helm release given application is started when navi
> >
Namespace Namespace
</div> </div>
<div
class="TableCell age"
>
Age
</div>
</div> </div>
<div <div
class="TableRow" class="TableRow"
@ -10094,11 +10049,6 @@ exports[`showing details for helm release given application is started when navi
> >
some-namespace some-namespace
</div> </div>
<div
class="TableCell age"
>
0s
</div>
</div> </div>
</div> </div>
</div> </div>
@ -11299,11 +11249,6 @@ exports[`showing details for helm release given application is started when navi
> >
Namespace Namespace
</div> </div>
<div
class="TableCell age"
>
Age
</div>
</div> </div>
<div <div
class="TableRow" class="TableRow"
@ -11318,11 +11263,6 @@ exports[`showing details for helm release given application is started when navi
> >
some-namespace some-namespace
</div> </div>
<div
class="TableCell age"
>
0s
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -6,7 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import type { AsyncResult } from "../../../../../common/utils/async-result"; import type { AsyncResult } from "../../../../../common/utils/async-result";
import execHelmInjectable from "../../../exec-helm/exec-helm.injectable"; import execHelmInjectable from "../../../exec-helm/exec-helm.injectable";
import yaml from "js-yaml"; import yaml from "js-yaml";
import type { KubeJsonApiData } from "../../../../../common/k8s-api/kube-json-api"; import type { KubeJsonApiData, KubeJsonApiDataList } from "../../../../../common/k8s-api/kube-json-api";
const callForHelmManifestInjectable = getInjectable({ const callForHelmManifestInjectable = getInjectable({
id: "call-for-helm-manifest", id: "call-for-helm-manifest",
@ -18,7 +18,7 @@ const callForHelmManifestInjectable = getInjectable({
name: string, name: string,
namespace: string, namespace: string,
kubeconfigPath: string, kubeconfigPath: string,
): Promise<AsyncResult<KubeJsonApiData[]>> => { ): Promise<AsyncResult<(KubeJsonApiData | KubeJsonApiDataList)[]>> => {
const result = await execHelm([ const result = await execHelm([
"get", "get",
"manifest", "manifest",

View File

@ -3,48 +3,37 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import callForKubeResourcesByManifestInjectable from "./call-for-kube-resources-by-manifest/call-for-kube-resources-by-manifest.injectable";
import { groupBy, map } from "lodash/fp";
import type { JsonObject } from "type-fest";
import { pipeline } from "@ogre-tools/fp";
import callForHelmManifestInjectable from "./call-for-helm-manifest/call-for-helm-manifest.injectable"; import callForHelmManifestInjectable from "./call-for-helm-manifest/call-for-helm-manifest.injectable";
import type { KubeJsonApiData, KubeJsonApiDataList } from "../../../../common/k8s-api/kube-json-api";
import type { AsyncResult } from "../../../../common/utils/async-result";
export type GetHelmReleaseResources = ( export type GetHelmReleaseResources = (
name: string, name: string,
namespace: string, namespace: string,
kubeconfigPath: string, kubeconfigPath: string,
kubectlPath: string ) => Promise<AsyncResult<KubeJsonApiData[], string>>;
) => Promise<JsonObject[]>;
const getHelmReleaseResourcesInjectable = getInjectable({ const getHelmReleaseResourcesInjectable = getInjectable({
id: "get-helm-release-resources", id: "get-helm-release-resources",
instantiate: (di): GetHelmReleaseResources => { instantiate: (di): GetHelmReleaseResources => {
const callForHelmManifest = di.inject(callForHelmManifestInjectable); const callForHelmManifest = di.inject(callForHelmManifestInjectable);
const callForKubeResourcesByManifest = di.inject(callForKubeResourcesByManifestInjectable);
return async (name, namespace, kubeconfigPath, kubectlPath) => { return async (name, namespace, kubeconfigPath) => {
const result = await callForHelmManifest(name, namespace, kubeconfigPath); const result = await callForHelmManifest(name, namespace, kubeconfigPath);
if (!result.callWasSuccessful) { if (!result.callWasSuccessful) {
throw new Error(result.error); return result;
} }
const results = await pipeline( return {
result.response, callWasSuccessful: true,
response: result.response.flatMap(item => (
groupBy((item) => item.metadata.namespace || namespace), Array.isArray(item.items)
? (item as KubeJsonApiDataList).items
(x) => Object.entries(x), : item as KubeJsonApiData
)),
map(([namespace, manifest]) => };
callForKubeResourcesByManifest(namespace, kubeconfigPath, kubectlPath, manifest),
),
promises => Promise.all(promises),
);
return results.flat(1);
}; };
}, },
}); });

View File

@ -10,9 +10,10 @@ import type { ExecHelm } from "../../exec-helm/exec-helm.injectable";
import execHelmInjectable from "../../exec-helm/exec-helm.injectable"; import execHelmInjectable from "../../exec-helm/exec-helm.injectable";
import type { AsyncFnMock } from "@async-fn/jest"; import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest"; import asyncFn from "@async-fn/jest";
import type { JsonObject } from "type-fest";
import type { ExecFileWithInput } from "./call-for-kube-resources-by-manifest/exec-file-with-input/exec-file-with-input.injectable"; import type { ExecFileWithInput } from "./call-for-kube-resources-by-manifest/exec-file-with-input/exec-file-with-input.injectable";
import execFileWithInputInjectable from "./call-for-kube-resources-by-manifest/exec-file-with-input/exec-file-with-input.injectable"; import execFileWithInputInjectable from "./call-for-kube-resources-by-manifest/exec-file-with-input/exec-file-with-input.injectable";
import type { AsyncResult } from "../../../../common/utils/async-result";
import type { KubeJsonApiData } from "../../../../common/k8s-api/kube-json-api";
describe("get helm release resources", () => { describe("get helm release resources", () => {
let getHelmReleaseResources: GetHelmReleaseResources; let getHelmReleaseResources: GetHelmReleaseResources;
@ -36,14 +37,13 @@ describe("get helm release resources", () => {
}); });
describe("when called", () => { describe("when called", () => {
let actualPromise: Promise<JsonObject[]>; let actualPromise: Promise<AsyncResult<KubeJsonApiData[], string>>;
beforeEach(() => { beforeEach(() => {
actualPromise = getHelmReleaseResources( actualPromise = getHelmReleaseResources(
"some-release", "some-release",
"some-namespace", "some-namespace",
"/some-kubeconfig-path", "/some-kubeconfig-path",
"/some-kubectl-path",
); );
}); });
@ -65,11 +65,13 @@ describe("get helm release resources", () => {
const actual = await actualPromise; const actual = await actualPromise;
expect(actual).toEqual([]); expect(actual).toEqual({
callWasSuccessful: true,
response: [],
});
}); });
describe("when call for manifest resolves", () => { it("when call to manifest resolves with resources, resolves with resources", async () => {
beforeEach(async () => {
await execHelmMock.resolve({ await execHelmMock.resolve({
callWasSuccessful: true, callWasSuccessful: true,
response: `--- response: `---
@ -80,135 +82,63 @@ metadata:
namespace: some-namespace namespace: some-namespace
--- ---
apiVersion: v1 apiVersion: v1
kind: SomeKind kind: SomeOtherKind
metadata: metadata:
name: some-resource-without-namespace name: some-resource-without-namespace
--- ---
apiVersion: v1 apiVersion: v1
kind: SomeKind kind: List
items:
- apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata: metadata:
name: some-resource-with-different-namespace name: collection-sumologic-fluentd-logs
namespace: some-other-namespace
---
`,
});
});
it("calls for resources from each namespace separately using the manifest as input", () => {
expect(execFileWithStreamInputMock.mock.calls).toEqual([
[
{
filePath: "/some-kubectl-path",
commandArguments: ["get", "--kubeconfig", "/some-kubeconfig-path", "-f", "-", "--namespace", "some-namespace", "--output", "json"],
input: `---
apiVersion: v1
kind: SomeKind
metadata:
name: some-resource-with-same-namespace
namespace: some-namespace namespace: some-namespace
--- ---
apiVersion: v1 apiVersion: v1
kind: SomeKind kind: SomeKind
metadata:
name: some-resource-without-namespace
---
`,
},
],
[
{
filePath: "/some-kubectl-path",
commandArguments: ["get", "--kubeconfig", "/some-kubeconfig-path", "-f", "-", "--namespace", "some-other-namespace", "--output", "json"],
input: `---
apiVersion: v1
kind: SomeKind
metadata: metadata:
name: some-resource-with-different-namespace name: some-resource-with-different-namespace
namespace: some-other-namespace namespace: some-other-namespace
--- ---
`, `,
});
expect(await actualPromise).toEqual({
callWasSuccessful: true,
response: [
{
apiVersion: "v1",
kind: "SomeKind",
metadata: {
name: "some-resource-with-same-namespace",
namespace: "some-namespace",
},
},
{
apiVersion: "v1",
kind: "SomeOtherKind",
metadata: {
name: "some-resource-without-namespace",
},
},
{
apiVersion: "monitoring.coreos.com/v1",
kind: "ServiceMonitor",
metadata: {
name: "collection-sumologic-fluentd-logs",
namespace: "some-namespace",
},
},
{
apiVersion: "v1",
kind: "SomeKind",
metadata: {
name: "some-resource-with-different-namespace",
namespace: "some-other-namespace",
},
}, },
], ],
]);
});
it("when all calls for resources resolve, resolves with combined result", async () => {
await execFileWithStreamInputMock.resolveSpecific(
([{ commandArguments }]) =>
commandArguments.includes("some-namespace"),
{
callWasSuccessful: true,
response: JSON.stringify({
items: [{ some: "item" }],
kind: "List",
metadata: {
resourceVersion: "",
selfLink: "",
},
}),
},
);
await execFileWithStreamInputMock.resolveSpecific(
([{ commandArguments }]) =>
commandArguments.includes("some-other-namespace"),
{
callWasSuccessful: true,
response: JSON.stringify({
items: [{ some: "other-item" }],
kind: "List",
metadata: {
resourceVersion: "",
selfLink: "",
},
}),
},
);
const actual = await actualPromise;
expect(actual).toEqual([{ some: "item" }, { some: "other-item" }]);
});
it("given some call fails, when all calls have finished, rejects with failure", async () => {
await execFileWithStreamInputMock.resolveSpecific(
([{ commandArguments }]) =>
commandArguments.includes("some-namespace"),
{
callWasSuccessful: true,
response: JSON.stringify({
items: [{ some: "item" }],
kind: "List",
metadata: {
resourceVersion: "",
selfLink: "",
},
}),
},
);
execFileWithStreamInputMock.resolveSpecific(
([{ commandArguments }]) =>
commandArguments.includes("some-other-namespace"),
{
callWasSuccessful: false,
error: "some-error",
},
);
return expect(actualPromise).rejects.toEqual(expect.any(Error));
}); });
}); });
}); });

View File

@ -19,12 +19,10 @@ const getHelmReleaseInjectable = getInjectable({
return async (cluster: Cluster, releaseName: string, namespace: string) => { return async (cluster: Cluster, releaseName: string, namespace: string) => {
const kubeconfigPath = await cluster.getProxyKubeconfigPath(); const kubeconfigPath = await cluster.getProxyKubeconfigPath();
const kubectl = await cluster.ensureKubectl();
const kubectlPath = await kubectl.getPath();
logger.debug("Fetch release"); logger.debug("Fetch release");
const args = [ const result = await execHelm([
"status", "status",
releaseName, releaseName,
"--namespace", "--namespace",
@ -33,11 +31,11 @@ const getHelmReleaseInjectable = getInjectable({
kubeconfigPath, kubeconfigPath,
"--output", "--output",
"json", "json",
]; ]);
const result = await execHelm(args);
if (!result.callWasSuccessful) { if (!result.callWasSuccessful) {
logger.warn(`Failed to exectute helm: ${result.error}`);
return undefined; return undefined;
} }
@ -47,14 +45,22 @@ const getHelmReleaseInjectable = getInjectable({
return undefined; return undefined;
} }
release.resources = await getHelmReleaseResources( const resourcesResult = await getHelmReleaseResources(
releaseName, releaseName,
namespace, namespace,
kubeconfigPath, kubeconfigPath,
kubectlPath,
); );
return release; if (!resourcesResult.callWasSuccessful) {
logger.warn(`Failed to get helm release resources: ${resourcesResult.error}`);
return undefined;
}
return {
...release,
resources: resourcesResult.response,
};
}; };
}, },

View File

@ -9,7 +9,7 @@ import { promiseExecFile } from "../../common/utils/promise-exec";
import logger from "../logger"; import logger from "../logger";
import { ensureDir, pathExists } from "fs-extra"; import { ensureDir, pathExists } from "fs-extra";
import * as lockFile from "proper-lockfile"; import * as lockFile from "proper-lockfile";
import { SemVer } from "semver"; import { SemVer, coerce } from "semver";
import { defaultPackageMirror, packageMirrors } from "../../common/user-store/preferences-helpers"; import { defaultPackageMirror, packageMirrors } from "../../common/user-store/preferences-helpers";
import got from "got/dist/source"; import got from "got/dist/source";
import { promisify } from "util"; import { promisify } from "util";
@ -45,11 +45,12 @@ export class Kubectl {
constructor(protected readonly dependencies: KubectlDependencies, clusterVersion: string) { constructor(protected readonly dependencies: KubectlDependencies, clusterVersion: string) {
let version: SemVer; let version: SemVer;
const bundledVersion = new SemVer(this.dependencies.bundledKubectlVersion);
try { try {
version = new SemVer(clusterVersion); version = new SemVer(clusterVersion);
} catch { } catch {
version = new SemVer(this.dependencies.bundledKubectlVersion); version = bundledVersion;
} }
const fromMajorMinor = this.dependencies.kubectlVersionMap.get(`${version.major}.${version.minor}`); const fromMajorMinor = this.dependencies.kubectlVersionMap.get(`${version.major}.${version.minor}`);
@ -62,7 +63,10 @@ export class Kubectl {
this.kubectlVersion = fromMajorMinor; this.kubectlVersion = fromMajorMinor;
logger.debug(`Set kubectl version ${this.kubectlVersion} for cluster version ${clusterVersion} using version map`); logger.debug(`Set kubectl version ${this.kubectlVersion} for cluster version ${clusterVersion} using version map`);
} else { } else {
this.kubectlVersion = version.format(); /* this is the version (without possible prelease tag) to get from the download mirror */
const ver = coerce(version.format()) ?? bundledVersion;
this.kubectlVersion = ver.format();
logger.debug(`Set kubectl version ${this.kubectlVersion} for cluster version ${clusterVersion} using fallback`); logger.debug(`Set kubectl version ${this.kubectlVersion} for cluster version ${clusterVersion} using fallback`);
} }

View File

@ -0,0 +1,80 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { ServerResponse } from "http";
import loggerInjectable from "../../common/logger.injectable";
import { object } from "../../common/utils";
import type { LensApiRequest, Route } from "./route";
import { contentTypes } from "./router-content-types";
export type RouteHandler = (request: LensApiRequest<string>, response: ServerResponse) => Promise<void>;
export type CreateHandlerForRoute = (route: Route<unknown, string>) => RouteHandler;
interface LensServerResponse {
statusCode: number;
content: any;
headers: {
[name: string]: string;
};
}
const writeServerResponseFor = (serverResponse: ServerResponse) => ({
statusCode,
content,
headers,
}: LensServerResponse) => {
serverResponse.statusCode = statusCode;
for (const [name, value] of object.entries(headers)) {
serverResponse.setHeader(name, value);
}
if (content instanceof Buffer) {
serverResponse.write(content);
serverResponse.end();
} else if (content) {
serverResponse.end(content);
} else {
serverResponse.end();
}
};
const createHandlerForRouteInjectable = getInjectable({
id: "create-handler-for-route",
instantiate: (di): CreateHandlerForRoute => {
const logger = di.inject(loggerInjectable);
return (route) => async (request, response) => {
const writeServerResponse = writeServerResponseFor(response);
try {
const result = await route.handler(request);
if (!result) {
const mappedResult = contentTypes.txt.resultMapper({
statusCode: 204,
response: undefined,
});
writeServerResponse(mappedResult);
} else if (!result.proxy) {
const contentType = result.contentType || contentTypes.json;
writeServerResponse(contentType.resultMapper(result));
}
} catch(error) {
const mappedResult = contentTypes.txt.resultMapper({
statusCode: 500,
error: error ? String(error) : "unknown error",
});
logger.error(`[ROUTER]: route ${route.path}, called with ${request.path}, threw an error`, error);
writeServerResponse(mappedResult);
}
};
},
});
export default createHandlerForRouteInjectable;

View File

@ -7,6 +7,7 @@ import { getInjectable, getInjectionToken, lifecycleEnum } from "@ogre-tools/inj
import { Router } from "./router"; import { Router } from "./router";
import parseRequestInjectable from "./parse-request.injectable"; import parseRequestInjectable from "./parse-request.injectable";
import type { Route } from "./route"; import type { Route } from "./route";
import createHandlerForRouteInjectable from "./create-handler-for-route.injectable";
export const routeInjectionToken = getInjectionToken<Route<unknown, string>>({ export const routeInjectionToken = getInjectionToken<Route<unknown, string>>({
id: "route-injection-token", id: "route-injection-token",
@ -28,13 +29,11 @@ export function getRouteInjectable<
const routerInjectable = getInjectable({ const routerInjectable = getInjectable({
id: "router", id: "router",
instantiate: (di) => { instantiate: (di) => new Router({
const routes = di.injectMany(routeInjectionToken);
return new Router(routes, {
parseRequest: di.inject(parseRequestInjectable), parseRequest: di.inject(parseRequestInjectable),
}); routes: di.injectMany(routeInjectionToken),
}, createHandlerForRoute: di.inject(createHandlerForRouteInjectable),
}),
}); });
export default routerInjectable; export default routerInjectable;

View File

@ -5,12 +5,11 @@
import Call from "@hapi/call"; import Call from "@hapi/call";
import type http from "http"; import type http from "http";
import { toPairs } from "lodash/fp";
import type { Cluster } from "../../common/cluster/cluster"; import type { Cluster } from "../../common/cluster/cluster";
import { contentTypes } from "./router-content-types"; import type { LensApiRequest, Route } from "./route";
import type { LensApiRequest, LensApiResult, Route } from "./route";
import type { ServerIncomingMessage } from "../lens-proxy/lens-proxy"; import type { ServerIncomingMessage } from "../lens-proxy/lens-proxy";
import type { ParseRequest } from "./parse-request.injectable"; import type { ParseRequest } from "./parse-request.injectable";
import type { CreateHandlerForRoute, RouteHandler } from "./create-handler-for-route.injectable";
export interface RouterRequestOpts { export interface RouterRequestOpts {
req: http.IncomingMessage; req: http.IncomingMessage;
@ -22,15 +21,17 @@ export interface RouterRequestOpts {
interface Dependencies { interface Dependencies {
parseRequest: ParseRequest; parseRequest: ParseRequest;
createHandlerForRoute: CreateHandlerForRoute;
readonly routes: Route<unknown, string>[];
} }
export class Router { export class Router {
protected router = new Call.Router<ReturnType<typeof handleRoute>>(); private readonly router = new Call.Router<RouteHandler>();
constructor(routes: Route<unknown, string>[], private dependencies: Dependencies) { constructor(private readonly dependencies: Dependencies) {
routes.forEach(route => { for (const route of this.dependencies.routes) {
this.router.add({ method: route.method, path: route.path }, handleRoute(route)); this.router.add({ method: route.method, path: route.path }, this.dependencies.createHandlerForRoute(route));
}); }
} }
public async route(cluster: Cluster | undefined, req: ServerIncomingMessage, res: http.ServerResponse): Promise<boolean> { public async route(cluster: Cluster | undefined, req: ServerIncomingMessage, res: http.ServerResponse): Promise<boolean> {
@ -52,7 +53,6 @@ export class Router {
protected async getRequest(opts: RouterRequestOpts): Promise<LensApiRequest<string>> { protected async getRequest(opts: RouterRequestOpts): Promise<LensApiRequest<string>> {
const { req, res, url, cluster, params } = opts; const { req, res, url, cluster, params } = opts;
const { payload } = await this.dependencies.parseRequest(req, null, { const { payload } = await this.dependencies.parseRequest(req, null, {
parse: true, parse: true,
output: "data", output: "data",
@ -70,77 +70,3 @@ export class Router {
}; };
} }
} }
const handleRoute = (route: Route<unknown, string>) => async (request: LensApiRequest<string>, response: http.ServerResponse) => {
let result: LensApiResult<any> | void;
const writeServerResponse = writeServerResponseFor(response);
try {
result = await route.handler(request);
} catch(error) {
const mappedResult = contentTypes.txt.resultMapper({
statusCode: 500,
error: error ? String(error) : "unknown error",
});
writeServerResponse(mappedResult);
return;
}
if (!result) {
const mappedResult = contentTypes.txt.resultMapper({
statusCode: 204,
response: undefined,
});
writeServerResponse(mappedResult);
return;
}
if (result.proxy) {
return;
}
const contentType = result.contentType || contentTypes.json;
const mappedResult = contentType.resultMapper(result);
writeServerResponse(mappedResult);
};
const writeServerResponseFor =
(serverResponse: http.ServerResponse) =>
({
statusCode,
content,
headers,
}: {
statusCode: number;
content: any;
headers: { [name: string]: string };
}) => {
serverResponse.statusCode = statusCode;
const headerPairs = toPairs<string>(headers);
headerPairs.forEach(([name, value]) => {
serverResponse.setHeader(name, value);
});
if (content instanceof Buffer) {
serverResponse.write(content);
serverResponse.end();
return;
}
if (content) {
serverResponse.end(content);
} else {
serverResponse.end();
}
};

View File

@ -10,7 +10,7 @@ import { shellEnv } from "../utils/shell-env";
import { app } from "electron"; import { app } from "electron";
import { clearKubeconfigEnvVars } from "../utils/clear-kube-env-vars"; import { clearKubeconfigEnvVars } from "../utils/clear-kube-env-vars";
import path from "path"; import path from "path";
import os from "os"; import os, { userInfo } from "os";
import { isMac, isWindows } from "../../common/vars"; import { isMac, isWindows } from "../../common/vars";
import { UserStore } from "../../common/user-store"; import { UserStore } from "../../common/user-store";
import * as pty from "node-pty"; import * as pty from "node-pty";
@ -313,9 +313,9 @@ export abstract class ShellSession {
} }
protected async getShellEnv() { protected async getShellEnv() {
const env = clearKubeconfigEnvVars(JSON.parse(JSON.stringify(await shellEnv())));
const pathStr = [await this.kubectlBinDirP, ...this.getPathEntries(), process.env.PATH].join(path.delimiter);
const shell = UserStore.getInstance().resolvedShell; const shell = UserStore.getInstance().resolvedShell;
const env = clearKubeconfigEnvVars(JSON.parse(JSON.stringify(await shellEnv(shell || userInfo().shell))));
const pathStr = [await this.kubectlBinDirP, ...this.getPathEntries(), process.env.PATH].join(path.delimiter);
delete env.DEBUG; // don't pass DEBUG into shells delete env.DEBUG; // don't pass DEBUG into shells

View File

@ -3,11 +3,85 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import shellEnvironment from "shell-env"; import { spawn } from "child_process";
import { randomUUID } from "crypto";
import { basename } from "path";
import { isWindows } from "../../common/vars";
import logger from "../logger"; import logger from "../logger";
export type EnvironmentVariables = Partial<Record<string, string>>; export type EnvironmentVariables = Partial<Record<string, string>>;
async function unixShellEnvironment(shell: string): Promise<EnvironmentVariables> {
const runAsNode = process.env["ELECTRON_RUN_AS_NODE"];
const noAttach = process.env["ELECTRON_NO_ATTACH_CONSOLE"];
const env = {
...process.env,
ELECTRON_RUN_AS_NODE: "1",
ELECTRON_NO_ATTACH_CONSOLE: "1",
};
const mark = randomUUID().replace(/-/g, "");
const regex = new RegExp(`${mark}(.*)${mark}`);
const shellName = basename(shell);
let command: string;
let shellArgs: string[];
if (/^pwsh(-preview)?$/.test(shellName)) {
// Older versions of PowerShell removes double quotes sometimes so we use "double single quotes" which is how
// you escape single quotes inside of a single quoted string.
command = `& '${process.execPath}' -p '''${mark}'' + JSON.stringify(process.env) + ''${mark}'''`;
shellArgs = ["-Login", "-Command"];
} else {
command = `'${process.execPath}' -p '"${mark}" + JSON.stringify(process.env) + "${mark}"'`;
if (shellName === "tcsh") {
shellArgs = ["-ic"];
} else {
shellArgs = ["-ilc"];
}
}
return new Promise((resolve, reject) => {
const shellProcess = spawn(shell, [...shellArgs, command], {
detached: true,
stdio: ["ignore", "pipe", "pipe"],
env,
});
const stdout: Buffer[] = [];
shellProcess.on("error", (err) => reject(err));
shellProcess.stdout.on("data", b => stdout.push(b));
shellProcess.on("close", (code, signal) => {
if (code || signal) {
return reject(new Error(`Unexpected return code from spawned shell (code: ${code}, signal: ${signal})`));
}
try {
const rawOutput = Buffer.concat(stdout).toString("utf-8");
const match = regex.exec(rawOutput);
const strippedRawOutput = match ? match[1] : "{}";
const resolvedEnv = JSON.parse(strippedRawOutput);
if (runAsNode) {
resolvedEnv["ELECTRON_RUN_AS_NODE"] = runAsNode;
} else {
delete resolvedEnv["ELECTRON_RUN_AS_NODE"];
}
if (noAttach) {
resolvedEnv["ELECTRON_NO_ATTACH_CONSOLE"] = noAttach;
} else {
delete resolvedEnv["ELECTRON_NO_ATTACH_CONSOLE"];
}
resolve(resolvedEnv);
} catch(err) {
reject(err);
}
});
});
}
let shellSyncFailed = false; let shellSyncFailed = false;
/** /**
@ -16,20 +90,18 @@ let shellSyncFailed = false;
* Subsequent calls after such a timeout simply log an error message without trying * Subsequent calls after such a timeout simply log an error message without trying
* to get the environment, unless forceRetry is true. * to get the environment, unless forceRetry is true.
* @param shell the shell to get the environment from * @param shell the shell to get the environment from
* @param forceRetry if true will always try to get the environment, otherwise if
* a previous call to this function failed then this call will fail too.
* @returns object containing the shell's environment variables. An empty object is * @returns object containing the shell's environment variables. An empty object is
* returned if the call fails. * returned if the call fails.
*/ */
export async function shellEnv(shell?: string, forceRetry = false) : Promise<EnvironmentVariables> { export async function shellEnv(shell: string) : Promise<EnvironmentVariables> {
if (forceRetry) { if (isWindows) {
shellSyncFailed = false; return {};
} }
if (!shellSyncFailed) { if (!shellSyncFailed) {
try { try {
return await Promise.race([ return await Promise.race([
shellEnvironment(shell), unixShellEnvironment(shell),
new Promise<EnvironmentVariables>((_resolve, reject) => setTimeout(() => { new Promise<EnvironmentVariables>((_resolve, reject) => setTimeout(() => {
reject(new Error("Resolving shell environment is taking very long. Please review your shell configuration.")); reject(new Error("Resolving shell environment is taking very long. Please review your shell configuration."));
}, 30_000)), }, 30_000)),

View File

@ -19,7 +19,6 @@ import { kebabCase } from "lodash/fp";
import { Badge } from "../../badge"; import { Badge } from "../../badge";
import { SubTitle } from "../../layout/sub-title"; import { SubTitle } from "../../layout/sub-title";
import { Table, TableCell, TableHead, TableRow } from "../../table"; import { Table, TableCell, TableHead, TableRow } from "../../table";
import { ReactiveDuration } from "../../duration/reactive-duration";
import { Checkbox } from "../../checkbox"; import { Checkbox } from "../../checkbox";
import { MonacoEditor } from "../../monaco-editor"; import { MonacoEditor } from "../../monaco-editor";
import { Spinner } from "../../spinner"; import { Spinner } from "../../spinner";
@ -131,12 +130,10 @@ const ResourceGroup = ({
<TableCell className="name">Name</TableCell> <TableCell className="name">Name</TableCell>
{isNamespaced && <TableCell className="namespace">Namespace</TableCell>} {isNamespaced && <TableCell className="namespace">Namespace</TableCell>}
<TableCell className="age">Age</TableCell>
</TableHead> </TableHead>
{resources.map( {resources.map(
({ creationTimestamp, detailsUrl, name, namespace, uid }) => ( ({ detailsUrl, name, namespace, uid }) => (
<TableRow key={uid}> <TableRow key={uid}>
<TableCell className="name"> <TableCell className="name">
{detailsUrl ? <Link to={detailsUrl}>{name}</Link> : name} {detailsUrl ? <Link to={detailsUrl}>{name}</Link> : name}
@ -145,10 +142,6 @@ const ResourceGroup = ({
{isNamespaced && ( {isNamespaced && (
<TableCell className="namespace">{namespace}</TableCell> <TableCell className="namespace">{namespace}</TableCell>
)} )}
<TableCell className="age">
<ReactiveDuration timestamp={creationTimestamp} />
</TableCell>
</TableRow> </TableRow>
), ),
)} )}

View File

@ -11,7 +11,7 @@ import type { AsyncResult } from "../../../../../../common/utils/async-result";
export interface DetailedHelmRelease { export interface DetailedHelmRelease {
release: HelmReleaseDto; release: HelmReleaseDto;
details: HelmReleaseDetails; details?: HelmReleaseDetails;
} }
export type CallForHelmRelease = ( export type CallForHelmRelease = (

View File

@ -228,12 +228,12 @@ export class ReleaseDetailsModel {
} }
@computed get notes() { @computed get notes() {
return this.details.info.notes; return this.details?.info.notes ?? "";
} }
@computed get groupedResources(): MinimalResourceGroup[] { @computed get groupedResources(): MinimalResourceGroup[] {
return pipeline( return pipeline(
this.details.resources, this.details?.resources ?? [],
groupBy((resource) => resource.kind), groupBy((resource) => resource.kind),
(grouped) => Object.entries(grouped), (grouped) => Object.entries(grouped),
@ -277,20 +277,17 @@ export interface MinimalResource {
name: string; name: string;
namespace: string | undefined; namespace: string | undefined;
detailsUrl: string | undefined; detailsUrl: string | undefined;
creationTimestamp: string | undefined;
} }
const toMinimalResourceFor = const toMinimalResourceFor =
(getResourceDetailsUrl: GetResourceDetailsUrl, kind: string) => (getResourceDetailsUrl: GetResourceDetailsUrl, kind: string) =>
(resource: KubeJsonApiData): MinimalResource => { (resource: KubeJsonApiData): MinimalResource => {
const { creationTimestamp, name, namespace, uid } = resource.metadata; const { name, namespace, uid } = resource.metadata;
return { return {
uid, uid,
name, name,
namespace, namespace,
creationTimestamp,
detailsUrl: getResourceDetailsUrl( detailsUrl: getResourceDetailsUrl(
kind, kind,
resource.apiVersion, resource.apiVersion,

107
yarn.lock
View File

@ -4518,17 +4518,6 @@ cross-fetch@^3.0.4:
dependencies: dependencies:
node-fetch "2.6.7" node-fetch "2.6.7"
cross-spawn@^6.0.0:
version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
dependencies:
nice-try "^1.0.4"
path-key "^2.0.1"
semver "^5.5.0"
shebang-command "^1.2.0"
which "^1.2.9"
cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
version "7.0.3" version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@ -4808,11 +4797,6 @@ default-gateway@^6.0.3:
dependencies: dependencies:
execa "^5.0.0" execa "^5.0.0"
default-shell@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/default-shell/-/default-shell-1.0.1.tgz#752304bddc6174f49eb29cb988feea0b8813c8bc"
integrity sha1-dSMEvdxhdPSespy5iP7qC4gTyLw=
defaults@^1.0.3: defaults@^1.0.3:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d"
@ -5845,19 +5829,6 @@ execa@5.0.0:
signal-exit "^3.0.3" signal-exit "^3.0.3"
strip-final-newline "^2.0.0" strip-final-newline "^2.0.0"
execa@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==
dependencies:
cross-spawn "^6.0.0"
get-stream "^4.0.0"
is-stream "^1.1.0"
npm-run-path "^2.0.0"
p-finally "^1.0.0"
signal-exit "^3.0.0"
strip-eof "^1.0.0"
execa@^5.0.0: execa@^5.0.0:
version "5.1.1" version "5.1.1"
resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
@ -6416,7 +6387,7 @@ get-package-type@^0.1.0:
resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a"
integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==
get-stream@^4.0.0, get-stream@^4.1.0: get-stream@^4.1.0:
version "4.1.0" version "4.1.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==
@ -7428,11 +7399,6 @@ is-shared-array-buffer@^1.0.2:
dependencies: dependencies:
call-bind "^1.0.2" call-bind "^1.0.2"
is-stream@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
is-stream@^2.0.0: is-stream@^2.0.0:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
@ -9305,11 +9271,6 @@ neo-async@^2.6.0, neo-async@^2.6.2:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
nice-try@^1.0.4:
version "1.0.5"
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
no-case@^3.0.4: no-case@^3.0.4:
version "3.0.4" version "3.0.4"
resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d"
@ -9574,13 +9535,6 @@ npm-registry-fetch@^13.0.0, npm-registry-fetch@^13.0.1, npm-registry-fetch@^13.3
npm-package-arg "^9.0.1" npm-package-arg "^9.0.1"
proc-log "^2.0.0" proc-log "^2.0.0"
npm-run-path@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=
dependencies:
path-key "^2.0.0"
npm-run-path@^4.0.1: npm-run-path@^4.0.1:
version "4.0.1" version "4.0.1"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
@ -9605,7 +9559,6 @@ npm@^8.19.2:
"@npmcli/fs" "^2.1.0" "@npmcli/fs" "^2.1.0"
"@npmcli/map-workspaces" "^2.0.3" "@npmcli/map-workspaces" "^2.0.3"
"@npmcli/package-json" "^2.0.0" "@npmcli/package-json" "^2.0.0"
"@npmcli/promise-spawn" "^3.0.0"
"@npmcli/run-script" "^4.2.1" "@npmcli/run-script" "^4.2.1"
abbrev "~1.1.1" abbrev "~1.1.1"
archy "~1.0.0" archy "~1.0.0"
@ -9616,7 +9569,6 @@ npm@^8.19.2:
cli-table3 "^0.6.2" cli-table3 "^0.6.2"
columnify "^1.6.0" columnify "^1.6.0"
fastest-levenshtein "^1.0.12" fastest-levenshtein "^1.0.12"
fs-minipass "^2.1.0"
glob "^8.0.1" glob "^8.0.1"
graceful-fs "^4.2.10" graceful-fs "^4.2.10"
hosted-git-info "^5.1.0" hosted-git-info "^5.1.0"
@ -9636,7 +9588,6 @@ npm@^8.19.2:
libnpmteam "^4.0.4" libnpmteam "^4.0.4"
libnpmversion "^3.0.7" libnpmversion "^3.0.7"
make-fetch-happen "^10.2.0" make-fetch-happen "^10.2.0"
minimatch "^5.1.0"
minipass "^3.1.6" minipass "^3.1.6"
minipass-pipeline "^1.2.4" minipass-pipeline "^1.2.4"
mkdirp "^1.0.4" mkdirp "^1.0.4"
@ -9896,11 +9847,6 @@ p-cancelable@^2.0.0:
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf"
integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg== integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==
p-finally@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=
p-limit@^1.1.0: p-limit@^1.1.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8"
@ -10094,11 +10040,6 @@ path-is-absolute@^1.0.0:
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
path-key@^2.0.0, path-key@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
path-key@^3.0.0, path-key@^3.1.0: path-key@^3.0.0, path-key@^3.1.0:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
@ -11346,7 +11287,7 @@ semver-diff@^3.1.1:
dependencies: dependencies:
semver "^6.3.0" semver "^6.3.0"
semver@^5.5.0, semver@^5.6.0, semver@^5.7.1: semver@^5.6.0, semver@^5.7.1:
version "5.7.1" version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
@ -11363,6 +11304,13 @@ semver@^7.0.0, semver@^7.1.1, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semve
dependencies: dependencies:
lru-cache "^6.0.0" lru-cache "^6.0.0"
semver@^7.3.8:
version "7.3.8"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798"
integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==
dependencies:
lru-cache "^6.0.0"
semver@~7.0.0: semver@~7.0.0:
version "7.0.0" version "7.0.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e"
@ -11486,13 +11434,6 @@ sharp@^0.31.0:
tar-fs "^2.1.1" tar-fs "^2.1.1"
tunnel-agent "^0.6.0" tunnel-agent "^0.6.0"
shebang-command@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=
dependencies:
shebang-regex "^1.0.0"
shebang-command@^2.0.0: shebang-command@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
@ -11500,25 +11441,11 @@ shebang-command@^2.0.0:
dependencies: dependencies:
shebang-regex "^3.0.0" shebang-regex "^3.0.0"
shebang-regex@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
shebang-regex@^3.0.0: shebang-regex@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
shell-env@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/shell-env/-/shell-env-3.0.1.tgz#515a62f6cbd5e139365be2535745e8e53438ce77"
integrity sha512-b09fpMipAQ9ObwvIeKoQFLDXcEcCpYUUZanlad4OYQscw2I49C/u97OPQg9jWYo36bRDn62fbe07oWYqovIvKA==
dependencies:
default-shell "^1.0.1"
execa "^1.0.0"
strip-ansi "^5.2.0"
shell-quote@^1.7.3: shell-quote@^1.7.3:
version "1.7.3" version "1.7.3"
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123" resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123"
@ -11551,7 +11478,7 @@ side-channel@^1.0.4:
get-intrinsic "^1.0.2" get-intrinsic "^1.0.2"
object-inspect "^1.9.0" object-inspect "^1.9.0"
signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7:
version "3.0.7" version "3.0.7"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
@ -11952,7 +11879,7 @@ strip-ansi@^3.0.0:
dependencies: dependencies:
ansi-regex "^2.0.0" ansi-regex "^2.0.0"
strip-ansi@^5.1.0, strip-ansi@^5.2.0: strip-ansi@^5.1.0:
version "5.2.0" version "5.2.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
@ -11981,11 +11908,6 @@ strip-color@^0.1.0:
resolved "https://registry.yarnpkg.com/strip-color/-/strip-color-0.1.0.tgz#106f65d3d3e6a2d9401cac0eb0ce8b8a702b4f7b" resolved "https://registry.yarnpkg.com/strip-color/-/strip-color-0.1.0.tgz#106f65d3d3e6a2d9401cac0eb0ce8b8a702b4f7b"
integrity sha512-p9LsUieSjWNNAxVCXLeilaDlmuUOrDS5/dF9znM1nZc7EGX5+zEFC0bEevsNIaldjlks+2jns5Siz6F9iK6jwA== integrity sha512-p9LsUieSjWNNAxVCXLeilaDlmuUOrDS5/dF9znM1nZc7EGX5+zEFC0bEevsNIaldjlks+2jns5Siz6F9iK6jwA==
strip-eof@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=
strip-final-newline@^2.0.0: strip-final-newline@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
@ -13161,13 +13083,6 @@ which-boxed-primitive@^1.0.2:
is-string "^1.0.5" is-string "^1.0.5"
is-symbol "^1.0.3" is-symbol "^1.0.3"
which@^1.2.9:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
dependencies:
isexe "^2.0.0"
which@^2.0.1, which@^2.0.2: which@^2.0.1, which@^2.0.2:
version "2.0.2" version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"