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:
27 files changed
+374
-510
No files matched your search
@@ -17,6 +17,7 @@ export function keys<K extends keyof any>(obj: Record<K, any>): 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: Record<K, V> | null | undefined): [K, V][];
|
||||
|
||||
|
||||
-60
@@ -5523,11 +5523,6 @@ exports[`showing details for helm release given application is started when navi
|
||||
>
|
||||
Namespace
|
||||
</div>
|
||||
<div
|
||||
class="TableCell age"
|
||||
>
|
||||
Age
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="TableRow"
|
||||
@@ -5542,11 +5537,6 @@ exports[`showing details for helm release given application is started when navi
|
||||
>
|
||||
some-namespace
|
||||
</div>
|
||||
<div
|
||||
class="TableCell age"
|
||||
>
|
||||
0s
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -6747,11 +6737,6 @@ exports[`showing details for helm release given application is started when navi
|
||||
>
|
||||
Namespace
|
||||
</div>
|
||||
<div
|
||||
class="TableCell age"
|
||||
>
|
||||
Age
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="TableRow"
|
||||
@@ -6766,11 +6751,6 @@ exports[`showing details for helm release given application is started when navi
|
||||
>
|
||||
some-namespace
|
||||
</div>
|
||||
<div
|
||||
class="TableCell age"
|
||||
>
|
||||
0s
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -7971,11 +7951,6 @@ exports[`showing details for helm release given application is started when navi
|
||||
>
|
||||
Namespace
|
||||
</div>
|
||||
<div
|
||||
class="TableCell age"
|
||||
>
|
||||
Age
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="TableRow"
|
||||
@@ -7990,11 +7965,6 @@ exports[`showing details for helm release given application is started when navi
|
||||
>
|
||||
some-namespace
|
||||
</div>
|
||||
<div
|
||||
class="TableCell age"
|
||||
>
|
||||
0s
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -9022,11 +8992,6 @@ exports[`showing details for helm release given application is started when navi
|
||||
>
|
||||
Namespace
|
||||
</div>
|
||||
<div
|
||||
class="TableCell age"
|
||||
>
|
||||
Age
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="TableRow"
|
||||
@@ -9041,11 +9006,6 @@ exports[`showing details for helm release given application is started when navi
|
||||
>
|
||||
some-namespace
|
||||
</div>
|
||||
<div
|
||||
class="TableCell age"
|
||||
>
|
||||
0s
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -10075,11 +10035,6 @@ exports[`showing details for helm release given application is started when navi
|
||||
>
|
||||
Namespace
|
||||
</div>
|
||||
<div
|
||||
class="TableCell age"
|
||||
>
|
||||
Age
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="TableRow"
|
||||
@@ -10094,11 +10049,6 @@ exports[`showing details for helm release given application is started when navi
|
||||
>
|
||||
some-namespace
|
||||
</div>
|
||||
<div
|
||||
class="TableCell age"
|
||||
>
|
||||
0s
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -11299,11 +11249,6 @@ exports[`showing details for helm release given application is started when navi
|
||||
>
|
||||
Namespace
|
||||
</div>
|
||||
<div
|
||||
class="TableCell age"
|
||||
>
|
||||
Age
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="TableRow"
|
||||
@@ -11318,11 +11263,6 @@ exports[`showing details for helm release given application is started when navi
|
||||
>
|
||||
some-namespace
|
||||
</div>
|
||||
<div
|
||||
class="TableCell age"
|
||||
>
|
||||
0s
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { AsyncResult } from "../../../../../common/utils/async-result";
|
||||
import execHelmInjectable from "../../../exec-helm/exec-helm.injectable";
|
||||
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({
|
||||
id: "call-for-helm-manifest",
|
||||
@@ -18,7 +18,7 @@ const callForHelmManifestInjectable = getInjectable({
|
||||
name: string,
|
||||
namespace: string,
|
||||
kubeconfigPath: string,
|
||||
): Promise<AsyncResult<KubeJsonApiData[]>> => {
|
||||
): Promise<AsyncResult<(KubeJsonApiData | KubeJsonApiDataList)[]>> => {
|
||||
const result = await execHelm([
|
||||
"get",
|
||||
"manifest",
|
||||
|
||||
+13
-24
@@ -3,48 +3,37 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
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 type { KubeJsonApiData, KubeJsonApiDataList } from "../../../../common/k8s-api/kube-json-api";
|
||||
import type { AsyncResult } from "../../../../common/utils/async-result";
|
||||
|
||||
export type GetHelmReleaseResources = (
|
||||
name: string,
|
||||
namespace: string,
|
||||
kubeconfigPath: string,
|
||||
kubectlPath: string
|
||||
) => Promise<JsonObject[]>;
|
||||
) => Promise<AsyncResult<KubeJsonApiData[], string>>;
|
||||
|
||||
const getHelmReleaseResourcesInjectable = getInjectable({
|
||||
id: "get-helm-release-resources",
|
||||
|
||||
instantiate: (di): GetHelmReleaseResources => {
|
||||
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);
|
||||
|
||||
if (!result.callWasSuccessful) {
|
||||
throw new Error(result.error);
|
||||
return result;
|
||||
}
|
||||
|
||||
const results = await pipeline(
|
||||
result.response,
|
||||
|
||||
groupBy((item) => item.metadata.namespace || namespace),
|
||||
|
||||
(x) => Object.entries(x),
|
||||
|
||||
map(([namespace, manifest]) =>
|
||||
callForKubeResourcesByManifest(namespace, kubeconfigPath, kubectlPath, manifest),
|
||||
),
|
||||
|
||||
promises => Promise.all(promises),
|
||||
);
|
||||
|
||||
return results.flat(1);
|
||||
return {
|
||||
callWasSuccessful: true,
|
||||
response: result.response.flatMap(item => (
|
||||
Array.isArray(item.items)
|
||||
? (item as KubeJsonApiDataList).items
|
||||
: item as KubeJsonApiData
|
||||
)),
|
||||
};
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
+49
-119
@@ -10,9 +10,10 @@ import type { ExecHelm } from "../../exec-helm/exec-helm.injectable";
|
||||
import execHelmInjectable from "../../exec-helm/exec-helm.injectable";
|
||||
import type { AsyncFnMock } 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 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", () => {
|
||||
let getHelmReleaseResources: GetHelmReleaseResources;
|
||||
@@ -36,14 +37,13 @@ describe("get helm release resources", () => {
|
||||
});
|
||||
|
||||
describe("when called", () => {
|
||||
let actualPromise: Promise<JsonObject[]>;
|
||||
let actualPromise: Promise<AsyncResult<KubeJsonApiData[], string>>;
|
||||
|
||||
beforeEach(() => {
|
||||
actualPromise = getHelmReleaseResources(
|
||||
"some-release",
|
||||
"some-namespace",
|
||||
"/some-kubeconfig-path",
|
||||
"/some-kubectl-path",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -65,14 +65,16 @@ describe("get helm release resources", () => {
|
||||
|
||||
const actual = await actualPromise;
|
||||
|
||||
expect(actual).toEqual([]);
|
||||
expect(actual).toEqual({
|
||||
callWasSuccessful: true,
|
||||
response: [],
|
||||
});
|
||||
});
|
||||
|
||||
describe("when call for manifest resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await execHelmMock.resolve({
|
||||
callWasSuccessful: true,
|
||||
response: `---
|
||||
it("when call to manifest resolves with resources, resolves with resources", async () => {
|
||||
await execHelmMock.resolve({
|
||||
callWasSuccessful: true,
|
||||
response: `---
|
||||
apiVersion: v1
|
||||
kind: SomeKind
|
||||
metadata:
|
||||
@@ -80,135 +82,63 @@ metadata:
|
||||
namespace: some-namespace
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: SomeKind
|
||||
kind: SomeOtherKind
|
||||
metadata:
|
||||
name: some-resource-without-namespace
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: List
|
||||
items:
|
||||
- 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("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
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: SomeKind
|
||||
metadata:
|
||||
name: some-resource-without-namespace
|
||||
---
|
||||
`,
|
||||
expect(await actualPromise).toEqual({
|
||||
callWasSuccessful: true,
|
||||
response: [
|
||||
{
|
||||
apiVersion: "v1",
|
||||
kind: "SomeKind",
|
||||
metadata: {
|
||||
name: "some-resource-with-same-namespace",
|
||||
namespace: "some-namespace",
|
||||
},
|
||||
],
|
||||
|
||||
[
|
||||
{
|
||||
filePath: "/some-kubectl-path",
|
||||
commandArguments: ["get", "--kubeconfig", "/some-kubeconfig-path", "-f", "-", "--namespace", "some-other-namespace", "--output", "json"],
|
||||
input: `---
|
||||
apiVersion: v1
|
||||
kind: SomeKind
|
||||
metadata:
|
||||
name: some-resource-with-different-namespace
|
||||
namespace: some-other-namespace
|
||||
---
|
||||
`,
|
||||
},
|
||||
{
|
||||
apiVersion: "v1",
|
||||
kind: "SomeOtherKind",
|
||||
metadata: {
|
||||
name: "some-resource-without-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: "",
|
||||
},
|
||||
}),
|
||||
apiVersion: "monitoring.coreos.com/v1",
|
||||
kind: "ServiceMonitor",
|
||||
metadata: {
|
||||
name: "collection-sumologic-fluentd-logs",
|
||||
namespace: "some-namespace",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
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: "",
|
||||
},
|
||||
}),
|
||||
apiVersion: "v1",
|
||||
kind: "SomeKind",
|
||||
metadata: {
|
||||
name: "some-resource-with-different-namespace",
|
||||
namespace: "some-other-namespace",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
execFileWithStreamInputMock.resolveSpecific(
|
||||
([{ commandArguments }]) =>
|
||||
commandArguments.includes("some-other-namespace"),
|
||||
|
||||
{
|
||||
callWasSuccessful: false,
|
||||
error: "some-error",
|
||||
},
|
||||
);
|
||||
|
||||
return expect(actualPromise).rejects.toEqual(expect.any(Error));
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,12 +19,10 @@ const getHelmReleaseInjectable = getInjectable({
|
||||
|
||||
return async (cluster: Cluster, releaseName: string, namespace: string) => {
|
||||
const kubeconfigPath = await cluster.getProxyKubeconfigPath();
|
||||
const kubectl = await cluster.ensureKubectl();
|
||||
const kubectlPath = await kubectl.getPath();
|
||||
|
||||
logger.debug("Fetch release");
|
||||
|
||||
const args = [
|
||||
const result = await execHelm([
|
||||
"status",
|
||||
releaseName,
|
||||
"--namespace",
|
||||
@@ -33,11 +31,11 @@ const getHelmReleaseInjectable = getInjectable({
|
||||
kubeconfigPath,
|
||||
"--output",
|
||||
"json",
|
||||
];
|
||||
|
||||
const result = await execHelm(args);
|
||||
]);
|
||||
|
||||
if (!result.callWasSuccessful) {
|
||||
logger.warn(`Failed to exectute helm: ${result.error}`);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -47,14 +45,22 @@ const getHelmReleaseInjectable = getInjectable({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
release.resources = await getHelmReleaseResources(
|
||||
const resourcesResult = await getHelmReleaseResources(
|
||||
releaseName,
|
||||
namespace,
|
||||
kubeconfigPath,
|
||||
kubectlPath,
|
||||
);
|
||||
|
||||
return release;
|
||||
if (!resourcesResult.callWasSuccessful) {
|
||||
logger.warn(`Failed to get helm release resources: ${resourcesResult.error}`);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
...release,
|
||||
resources: resourcesResult.response,
|
||||
};
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { promiseExecFile } from "../../common/utils/promise-exec";
|
||||
import logger from "../logger";
|
||||
import { ensureDir, pathExists } from "fs-extra";
|
||||
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 got from "got/dist/source";
|
||||
import { promisify } from "util";
|
||||
@@ -45,11 +45,12 @@ export class Kubectl {
|
||||
|
||||
constructor(protected readonly dependencies: KubectlDependencies, clusterVersion: string) {
|
||||
let version: SemVer;
|
||||
const bundledVersion = new SemVer(this.dependencies.bundledKubectlVersion);
|
||||
|
||||
try {
|
||||
version = new SemVer(clusterVersion);
|
||||
} catch {
|
||||
version = new SemVer(this.dependencies.bundledKubectlVersion);
|
||||
version = bundledVersion;
|
||||
}
|
||||
|
||||
const fromMajorMinor = this.dependencies.kubectlVersionMap.get(`${version.major}.${version.minor}`);
|
||||
@@ -62,7 +63,10 @@ export class Kubectl {
|
||||
this.kubectlVersion = fromMajorMinor;
|
||||
logger.debug(`Set kubectl version ${this.kubectlVersion} for cluster version ${clusterVersion} using version map`);
|
||||
} 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`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -7,6 +7,7 @@ import { getInjectable, getInjectionToken, lifecycleEnum } from "@ogre-tools/inj
|
||||
import { Router } from "./router";
|
||||
import parseRequestInjectable from "./parse-request.injectable";
|
||||
import type { Route } from "./route";
|
||||
import createHandlerForRouteInjectable from "./create-handler-for-route.injectable";
|
||||
|
||||
export const routeInjectionToken = getInjectionToken<Route<unknown, string>>({
|
||||
id: "route-injection-token",
|
||||
@@ -28,13 +29,11 @@ export function getRouteInjectable<
|
||||
const routerInjectable = getInjectable({
|
||||
id: "router",
|
||||
|
||||
instantiate: (di) => {
|
||||
const routes = di.injectMany(routeInjectionToken);
|
||||
|
||||
return new Router(routes, {
|
||||
parseRequest: di.inject(parseRequestInjectable),
|
||||
});
|
||||
},
|
||||
instantiate: (di) => new Router({
|
||||
parseRequest: di.inject(parseRequestInjectable),
|
||||
routes: di.injectMany(routeInjectionToken),
|
||||
createHandlerForRoute: di.inject(createHandlerForRouteInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
export default routerInjectable;
|
||||
@@ -5,12 +5,11 @@
|
||||
|
||||
import Call from "@hapi/call";
|
||||
import type http from "http";
|
||||
import { toPairs } from "lodash/fp";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import { contentTypes } from "./router-content-types";
|
||||
import type { LensApiRequest, LensApiResult, Route } from "./route";
|
||||
import type { LensApiRequest, Route } from "./route";
|
||||
import type { ServerIncomingMessage } from "../lens-proxy/lens-proxy";
|
||||
import type { ParseRequest } from "./parse-request.injectable";
|
||||
import type { CreateHandlerForRoute, RouteHandler } from "./create-handler-for-route.injectable";
|
||||
|
||||
export interface RouterRequestOpts {
|
||||
req: http.IncomingMessage;
|
||||
@@ -22,15 +21,17 @@ export interface RouterRequestOpts {
|
||||
|
||||
interface Dependencies {
|
||||
parseRequest: ParseRequest;
|
||||
createHandlerForRoute: CreateHandlerForRoute;
|
||||
readonly routes: Route<unknown, string>[];
|
||||
}
|
||||
|
||||
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) {
|
||||
routes.forEach(route => {
|
||||
this.router.add({ method: route.method, path: route.path }, handleRoute(route));
|
||||
});
|
||||
constructor(private readonly dependencies: Dependencies) {
|
||||
for (const route of this.dependencies.routes) {
|
||||
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> {
|
||||
@@ -52,7 +53,6 @@ export class Router {
|
||||
|
||||
protected async getRequest(opts: RouterRequestOpts): Promise<LensApiRequest<string>> {
|
||||
const { req, res, url, cluster, params } = opts;
|
||||
|
||||
const { payload } = await this.dependencies.parseRequest(req, null, {
|
||||
parse: true,
|
||||
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();
|
||||
}
|
||||
};
|
||||
@@ -10,7 +10,7 @@ import { shellEnv } from "../utils/shell-env";
|
||||
import { app } from "electron";
|
||||
import { clearKubeconfigEnvVars } from "../utils/clear-kube-env-vars";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import os, { userInfo } from "os";
|
||||
import { isMac, isWindows } from "../../common/vars";
|
||||
import { UserStore } from "../../common/user-store";
|
||||
import * as pty from "node-pty";
|
||||
@@ -313,9 +313,9 @@ export abstract class ShellSession {
|
||||
}
|
||||
|
||||
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 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
|
||||
|
||||
|
||||
@@ -3,11 +3,85 @@
|
||||
* 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";
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
@@ -16,20 +90,18 @@ let shellSyncFailed = false;
|
||||
* Subsequent calls after such a timeout simply log an error message without trying
|
||||
* to get the environment, unless forceRetry is true.
|
||||
* @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
|
||||
* returned if the call fails.
|
||||
*/
|
||||
export async function shellEnv(shell?: string, forceRetry = false) : Promise<EnvironmentVariables> {
|
||||
if (forceRetry) {
|
||||
shellSyncFailed = false;
|
||||
export async function shellEnv(shell: string) : Promise<EnvironmentVariables> {
|
||||
if (isWindows) {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
if (!shellSyncFailed) {
|
||||
try {
|
||||
return await Promise.race([
|
||||
shellEnvironment(shell),
|
||||
unixShellEnvironment(shell),
|
||||
new Promise<EnvironmentVariables>((_resolve, reject) => setTimeout(() => {
|
||||
reject(new Error("Resolving shell environment is taking very long. Please review your shell configuration."));
|
||||
}, 30_000)),
|
||||
|
||||
@@ -19,7 +19,6 @@ import { kebabCase } from "lodash/fp";
|
||||
import { Badge } from "../../badge";
|
||||
import { SubTitle } from "../../layout/sub-title";
|
||||
import { Table, TableCell, TableHead, TableRow } from "../../table";
|
||||
import { ReactiveDuration } from "../../duration/reactive-duration";
|
||||
import { Checkbox } from "../../checkbox";
|
||||
import { MonacoEditor } from "../../monaco-editor";
|
||||
import { Spinner } from "../../spinner";
|
||||
@@ -131,12 +130,10 @@ const ResourceGroup = ({
|
||||
<TableCell className="name">Name</TableCell>
|
||||
|
||||
{isNamespaced && <TableCell className="namespace">Namespace</TableCell>}
|
||||
|
||||
<TableCell className="age">Age</TableCell>
|
||||
</TableHead>
|
||||
|
||||
{resources.map(
|
||||
({ creationTimestamp, detailsUrl, name, namespace, uid }) => (
|
||||
({ detailsUrl, name, namespace, uid }) => (
|
||||
<TableRow key={uid}>
|
||||
<TableCell className="name">
|
||||
{detailsUrl ? <Link to={detailsUrl}>{name}</Link> : name}
|
||||
@@ -145,10 +142,6 @@ const ResourceGroup = ({
|
||||
{isNamespaced && (
|
||||
<TableCell className="namespace">{namespace}</TableCell>
|
||||
)}
|
||||
|
||||
<TableCell className="age">
|
||||
<ReactiveDuration timestamp={creationTimestamp} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
),
|
||||
)}
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import type { AsyncResult } from "../../../../../../common/utils/async-result";
|
||||
|
||||
export interface DetailedHelmRelease {
|
||||
release: HelmReleaseDto;
|
||||
details: HelmReleaseDetails;
|
||||
details?: HelmReleaseDetails;
|
||||
}
|
||||
|
||||
export type CallForHelmRelease = (
|
||||
|
||||
+3
-6
@@ -228,12 +228,12 @@ export class ReleaseDetailsModel {
|
||||
}
|
||||
|
||||
@computed get notes() {
|
||||
return this.details.info.notes;
|
||||
return this.details?.info.notes ?? "";
|
||||
}
|
||||
|
||||
@computed get groupedResources(): MinimalResourceGroup[] {
|
||||
return pipeline(
|
||||
this.details.resources,
|
||||
this.details?.resources ?? [],
|
||||
groupBy((resource) => resource.kind),
|
||||
(grouped) => Object.entries(grouped),
|
||||
|
||||
@@ -277,20 +277,17 @@ export interface MinimalResource {
|
||||
name: string;
|
||||
namespace: string | undefined;
|
||||
detailsUrl: string | undefined;
|
||||
creationTimestamp: string | undefined;
|
||||
}
|
||||
|
||||
const toMinimalResourceFor =
|
||||
(getResourceDetailsUrl: GetResourceDetailsUrl, kind: string) =>
|
||||
(resource: KubeJsonApiData): MinimalResource => {
|
||||
const { creationTimestamp, name, namespace, uid } = resource.metadata;
|
||||
const { name, namespace, uid } = resource.metadata;
|
||||
|
||||
return {
|
||||
uid,
|
||||
name,
|
||||
namespace,
|
||||
creationTimestamp,
|
||||
|
||||
detailsUrl: getResourceDetailsUrl(
|
||||
kind,
|
||||
resource.apiVersion,
|
||||
|
||||
Reference in new issue
Block a user