Restructure to monorepo (#6907)

* wip: restructure to monorepo

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

* refactor create-release-pr to a package

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

* build fixes

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

* github workflow fixes

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

* fix typo

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

* add webpack-env types to core

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

* fix github workflows

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

* refactor/fix integration tests

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

* lint fix

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

* yarn run dev

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

* eslint settings for vscode

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

* move templates to right package

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

* open-lens build fixes

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

* integration test fix

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

* fix nx task dependencies

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

* use bash shell for unit tests in test workflow

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

* fix test:unit for windows

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

* fix win-ca webpack error in open-lens

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

* fix win-ca webpack error in open-lens

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

* fix build:app on windows

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

* remove ELECTRON_BUILDER_EXTRA_ARGS

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

* sync src/ from master

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

* remove Makefile from core

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

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen authored and GitHub committed 2023-01-24 10:46:26 -08:00
1 parent 0066ca4ea2
commit 2657df2293
3321 files changed
+28699 -11447

No files matched your search

@@ -0,0 +1,355 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import listHelmChartsInjectable from "../helm-service/list-helm-charts.injectable";
import getActiveHelmRepositoriesInjectable from "../repositories/get-active-helm-repositories/get-active-helm-repositories.injectable";
import type { AsyncResult } from "../../../common/utils/async-result";
import type { HelmRepo } from "../../../common/helm/helm-repo";
import { sortBySemverVersion } from "../../../common/utils";
import helmChartManagerInjectable from "../helm-chart-manager.injectable";
describe("Helm Service tests", () => {
let listHelmCharts: () => Promise<any>;
let getActiveHelmRepositoriesMock: jest.Mock<Promise<AsyncResult<HelmRepo[]>>>;
beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
getActiveHelmRepositoriesMock = jest.fn();
di.override(
helmChartManagerInjectable,
(di, repo) => new HelmChartManagerFake(repo) as unknown,
);
di.override(getActiveHelmRepositoriesInjectable, () => getActiveHelmRepositoriesMock);
di.unoverride(listHelmChartsInjectable);
di.permitSideEffects(listHelmChartsInjectable);
listHelmCharts = di.inject(listHelmChartsInjectable);
});
afterEach(() => {
jest.resetAllMocks();
});
it("list charts with deprecated entries", async () => {
getActiveHelmRepositoriesMock.mockReturnValue(
Promise.resolve({
callWasSuccessful: true,
response: [
{
name: "stable",
url: "stableurl",
cacheFilePath: "/some-cache-file-path-for-stable",
},
{
name: "experiment",
url: "experimenturl",
cacheFilePath: "/some-cache-file-path-for-experiment",
},
],
}),
);
const charts = await listHelmCharts();
expect(charts).toEqual({
stable: {
"apm-server": [
{
apiVersion: "3.0.0",
name: "apm-server",
version: "2.1.7",
repo: "stable",
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "apm-server",
version: "2.1.6",
repo: "stable",
digest: "test",
created: "now",
},
],
"invalid-semver": [
{
apiVersion: "3.0.0",
name: "weird-versioning",
version: "v4.4.0",
repo: "stable",
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "weird-versioning",
version: "v4.3.0",
repo: "stable",
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "weird-versioning",
version: "I am not semver",
repo: "stable",
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "weird-versioning",
version: "I am not semver but more",
repo: "stable",
digest: "test",
created: "now",
},
],
"redis": [
{
apiVersion: "3.0.0",
name: "apm-server",
version: "1.0.0",
repo: "stable",
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "apm-server",
version: "0.0.9",
repo: "stable",
digest: "test",
created: "now",
},
],
},
experiment: {
"fairwind": [
{
apiVersion: "3.0.0",
name: "fairwind",
version: "0.0.2",
repo: "experiment",
digest: "test",
deprecated: true,
created: "now",
},
{
apiVersion: "3.0.0",
name: "fairwind",
version: "0.0.1",
repo: "experiment",
digest: "test",
created: "now",
},
],
},
});
});
it("list charts sorted by version in descending order", async () => {
getActiveHelmRepositoriesMock.mockReturnValue(
Promise.resolve({
callWasSuccessful: true,
response: [{
name: "bitnami",
url: "bitnamiurl",
cacheFilePath: "/some-cache-file-path-for-bitnami",
}],
}),
);
const charts = await listHelmCharts();
expect(charts).toEqual({
bitnami: {
"hotdog": [
{
apiVersion: "3.0.0",
name: "hotdog",
version: "1.0.2",
repo: "bitnami",
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "hotdog",
version: "1.0.1",
repo: "bitnami",
digest: "test",
created: "now",
},
],
"pretzel": [
{
apiVersion: "3.0.0",
name: "pretzel",
version: "1.0.1",
repo: "bitnami",
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "pretzel",
version: "1.0",
repo: "bitnami",
digest: "test",
created: "now",
},
],
},
});
});
});
const charts = new Map([
["stable", {
"invalid-semver": sortBySemverVersion([
{
apiVersion: "3.0.0",
name: "weird-versioning",
version: "I am not semver",
repo: "stable",
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "weird-versioning",
version: "v4.3.0",
repo: "stable",
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "weird-versioning",
version: "I am not semver but more",
repo: "stable",
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "weird-versioning",
version: "v4.4.0",
repo: "stable",
digest: "test",
created: "now",
},
]),
"apm-server": sortBySemverVersion([
{
apiVersion: "3.0.0",
name: "apm-server",
version: "2.1.7",
repo: "stable",
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "apm-server",
version: "2.1.6",
repo: "stable",
digest: "test",
created: "now",
},
]),
"redis": sortBySemverVersion([
{
apiVersion: "3.0.0",
name: "apm-server",
version: "1.0.0",
repo: "stable",
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "apm-server",
version: "0.0.9",
repo: "stable",
digest: "test",
created: "now",
},
]),
}],
["experiment", {
"fairwind": sortBySemverVersion([
{
apiVersion: "3.0.0",
name: "fairwind",
version: "0.0.1",
repo: "experiment",
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "fairwind",
version: "0.0.2",
repo: "experiment",
digest: "test",
deprecated: true,
created: "now",
},
]),
}],
["bitnami", {
"hotdog": sortBySemverVersion([
{
apiVersion: "3.0.0",
name: "hotdog",
version: "1.0.1",
repo: "bitnami",
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "hotdog",
version: "1.0.2",
repo: "bitnami",
digest: "test",
created: "now",
},
]),
"pretzel": sortBySemverVersion([
{
apiVersion: "3.0.0",
name: "pretzel",
version: "1.0",
repo: "bitnami",
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "pretzel",
version: "1.0.1",
repo: "bitnami",
digest: "test",
created: "now",
},
]),
}],
]);
class HelmChartManagerFake {
constructor(private repo: HelmRepo){ }
public async charts(): Promise<any> {
return charts.get(this.repo.name) ?? {};
}
}
@@ -0,0 +1,37 @@
/**
* 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 execHelmInjectable from "./exec-helm/exec-helm.injectable";
export interface DeleteHelmReleaseData {
name: string;
namespace: string;
}
export type DeleteHelmRelease = (kubeconfigPath: string, data: DeleteHelmReleaseData) => Promise<string>;
const deleteHelmReleaseInjectable = getInjectable({
id: "delete-helm-release",
instantiate: (di): DeleteHelmRelease => {
const execHelm = di.inject(execHelmInjectable);
return async (kubeconfigPath, { name, namespace }) => {
const result = await execHelm([
"delete",
name,
"--namespace", namespace,
"--kubeconfig", kubeconfigPath,
]);
if (result.callWasSuccessful) {
return result.response;
}
throw result.error;
};
},
});
export default deleteHelmReleaseInjectable;
@@ -0,0 +1,10 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { computed } from "mobx";
import { getGlobalOverride } from "../../../common/test-utils/get-global-override";
import execHelmEnvInjectable from "./exec-env.injectable";
export default getGlobalOverride(execHelmEnvInjectable, () => computed(() => ({})));
@@ -0,0 +1,26 @@
/**
* 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 { computed } from "mobx";
import httpsProxyConfigurationInjectable from "../../../common/user-store/https-proxy.injectable";
const execHelmEnvInjectable = getInjectable({
id: "exec-helm-env",
instantiate: (di) => {
const httpsProxyConfiguration = di.inject(httpsProxyConfigurationInjectable);
return computed(() => {
const {
HTTPS_PROXY = httpsProxyConfiguration.get(),
...env
} = process.env;
return { HTTPS_PROXY, ...env } as Partial<Record<string, string>>;
});
},
causesSideEffects: true,
});
export default execHelmEnvInjectable;
@@ -0,0 +1,30 @@
/**
* 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 { ExecFileException } from "child_process";
import execFileInjectable from "../../../common/fs/exec-file.injectable";
import type { AsyncResult } from "../../../common/utils/async-result";
import helmBinaryPathInjectable from "../helm-binary-path.injectable";
import execHelmEnvInjectable from "./exec-env.injectable";
export type ExecHelm = (args: string[]) => Promise<AsyncResult<string, ExecFileException & { stderr: string }>>;
const execHelmInjectable = getInjectable({
id: "exec-helm",
instantiate: (di): ExecHelm => {
const execFile = di.inject(execFileInjectable);
const execHelmEnv = di.inject(execHelmEnvInjectable);
const helmBinaryPath = di.inject(helmBinaryPathInjectable);
return async (args) => execFile(helmBinaryPath, args, {
maxBuffer: 32 * 1024 * 1024 * 1024, // 32 MiB
env: execHelmEnv.get(),
});
},
});
export default execHelmInjectable;
@@ -0,0 +1,43 @@
/**
* 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 execHelmInjectable from "../exec-helm/exec-helm.injectable";
import type { AsyncResult } from "../../../common/utils/async-result";
export type HelmEnv = Record<string, string> & {
HELM_REPOSITORY_CACHE?: string;
HELM_REPOSITORY_CONFIG?: string;
};
const getHelmEnvInjectable = getInjectable({
id: "get-helm-env",
instantiate: (di) => {
const execHelm = di.inject(execHelmInjectable);
return async (): Promise<AsyncResult<HelmEnv>> => {
const result = await execHelm(["env"]);
if (!result.callWasSuccessful) {
return { callWasSuccessful: false, error: result.error.stderr };
}
const lines = result.response.split(/\r?\n/); // split by new line feed
const env: HelmEnv = {};
lines.forEach((line: string) => {
const [key, value] = line.split("=");
if (key && value) {
env[key] = value.replace(/"/g, ""); // strip quotas
}
});
return { callWasSuccessful: true, response: env };
};
},
});
export default getHelmEnvInjectable;
@@ -0,0 +1,39 @@
/**
* 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 { HelmReleaseRevision } from "../../common/k8s-api/endpoints/helm-releases.api/request-history.injectable";
import execHelmInjectable from "./exec-helm/exec-helm.injectable";
export interface GetHelmReleaseHistoryData {
name: string;
namespace: string;
}
export type GetHelmReleaseHistory = (kubeconfigPath: string, data: GetHelmReleaseHistoryData) => Promise<HelmReleaseRevision[]>;
const getHelmReleaseHistoryInjectable = getInjectable({
id: "get-helm-release-history",
instantiate: (di): GetHelmReleaseHistory => {
const execHelm = di.inject(execHelmInjectable);
return async (kubeconfigPath, { name, namespace }) => {
const result = await execHelm([
"history",
name,
"--output", "json",
"--namespace", namespace,
"--kubeconfig", kubeconfigPath,
]);
if (result.callWasSuccessful) {
return JSON.parse(result.response);
}
throw result.error;
};
},
});
export default getHelmReleaseHistoryInjectable;
@@ -0,0 +1,49 @@
/**
* 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 execHelmInjectable from "./exec-helm/exec-helm.injectable";
export interface GetHelmReleaseValuesData {
name: string;
namespace: string;
all?: boolean;
}
export type GetHelmReleaseValues = (kubeconfigPath: string, data: GetHelmReleaseValuesData) => Promise<string>;
const getHelmReleaseValuesInjectable = getInjectable({
id: "get-helm-release-values",
instantiate: (di): GetHelmReleaseValues => {
const execHelm = di.inject(execHelmInjectable);
return async (kubeconfigPath, { name, namespace, all = false }) => {
const args = [
"get",
"values",
name,
];
if (all) {
args.push("--all");
}
args.push(
"--output", "yaml",
"--namespace", namespace,
"--kubeconfig", kubeconfigPath,
);
const result = await execHelm(args);
if (result.callWasSuccessful) {
return result.response;
}
throw result.error;
};
},
});
export default getHelmReleaseValuesInjectable;
@@ -0,0 +1,13 @@
/**
* 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 bundledBinaryPathInjectable from "../../common/utils/bundled-binary-path.injectable";
const helmBinaryPathInjectable = getInjectable({
id: "helm-binary-path",
instantiate: (di) => di.inject(bundledBinaryPathInjectable, "helm"),
});
export default helmBinaryPathInjectable;
@@ -0,0 +1,19 @@
/**
* 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";
export interface ChartCacheEntry {
data: string; // serialized JSON
mtimeMs: number;
}
export type HelmChartManagerCache = Map<string, ChartCacheEntry>;
const helmChartManagerCacheInjectable = getInjectable({
id: "helm-chart-manager-cache",
instantiate: (): HelmChartManagerCache => new Map(),
});
export default helmChartManagerCacheInjectable;
@@ -0,0 +1,30 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { HelmRepo } from "../../common/helm/helm-repo";
import { HelmChartManager } from "./helm-chart-manager";
import helmChartManagerCacheInjectable from "./helm-chart-manager-cache.injectable";
import loggerInjectable from "../../common/logger.injectable";
import execHelmInjectable from "./exec-helm/exec-helm.injectable";
import readFileInjectable from "../../common/fs/read-file.injectable";
import statInjectable from "../../common/fs/stat.injectable";
const helmChartManagerInjectable = getInjectable({
id: "helm-chart-manager",
instantiate: (di, repo: HelmRepo) => new HelmChartManager({
cache: di.inject(helmChartManagerCacheInjectable),
logger: di.inject(loggerInjectable),
execHelm: di.inject(execHelmInjectable),
readFile: di.inject(readFileInjectable),
stat: di.inject(statInjectable),
}, repo),
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, repo: HelmRepo) => repo.name,
}),
});
export default helmChartManagerInjectable;
@@ -0,0 +1,132 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import * as yaml from "js-yaml";
import { iter, put, sortBySemverVersion } from "../../common/utils";
import type { HelmRepo } from "../../common/helm/helm-repo";
import type { HelmChartManagerCache } from "./helm-chart-manager-cache.injectable";
import type { Logger } from "../../common/logger";
import type { RepoHelmChartList } from "../../common/k8s-api/endpoints/helm-charts.api/request-charts.injectable";
import type { ExecHelm } from "./exec-helm/exec-helm.injectable";
import type { ReadFile } from "../../common/fs/read-file.injectable";
import type { Stat } from "../../common/fs/stat.injectable";
export interface HelmCacheFile {
apiVersion: string;
entries: RepoHelmChartList;
}
interface Dependencies {
readonly cache: HelmChartManagerCache;
readonly logger: Logger;
execHelm: ExecHelm;
readFile: ReadFile;
stat: Stat;
}
export class HelmChartManager {
constructor(
private readonly dependencies: Dependencies,
protected readonly repo: HelmRepo,
) {}
public async chartVersions(name: string) {
const charts = await this.charts();
return charts[name];
}
public async charts(): Promise<RepoHelmChartList> {
try {
return await this.cachedYaml();
} catch(error) {
this.dependencies.logger.error("HELM-CHART-MANAGER]: failed to list charts", { error });
return {};
}
}
private async executeCommand(args: string[], name: string, version?: string) {
args.push(`${this.repo.name}/${name}`);
if (version) {
args.push("--version", version);
}
return this.dependencies.execHelm(args);
}
public async getReadme(name: string, version?: string) {
return this.executeCommand(["show", "readme"], name, version);
}
public async getValues(name: string, version?: string) {
return this.executeCommand(["show", "values"], name, version);
}
protected async updateYamlCache() {
const cacheFile = await this.dependencies.readFile(this.repo.cacheFilePath);
const cacheFileStats = await this.dependencies.stat(this.repo.cacheFilePath);
const data = yaml.load(cacheFile) as string | number | HelmCacheFile;
if (!data || typeof data !== "object" || typeof data.entries !== "object") {
throw Object.assign(new TypeError("Helm Cache file does not parse correctly"), { file: this.repo.cacheFilePath, data });
}
const normalized = normalizeHelmCharts(this.repo.name, data.entries);
return put(
this.dependencies.cache,
this.repo.name,
{
data: JSON.stringify(normalized),
mtimeMs: cacheFileStats.mtimeMs,
},
);
}
protected async cachedYaml(): Promise<RepoHelmChartList> {
let cacheEntry = this.dependencies.cache.get(this.repo.name);
if (!cacheEntry) {
cacheEntry = await this.updateYamlCache();
} else {
const newStats = await this.dependencies.stat(this.repo.cacheFilePath);
if (cacheEntry.mtimeMs < newStats.mtimeMs) {
cacheEntry = await this.updateYamlCache();
}
}
return JSON.parse(cacheEntry.data);
}
}
/**
* Do some initial preprocessing on the data, so as to avoid needing to do it later
* 1. Set the repo name
* 2. Normalize the created date
* 3. Filter out charts that only have deprecated entries
*/
function normalizeHelmCharts(repoName: string, entries: RepoHelmChartList): RepoHelmChartList {
return Object.fromEntries(
iter.filter(
iter.map(
Object.entries(entries),
([name, charts]) => [
name,
sortBySemverVersion(
charts.map(chart => ({
...chart,
created: Date.parse(chart.created).toString(),
repo: repoName,
})),
),
] as const,
),
([, charts]) => !charts.every(chart => chart.deprecated),
),
);
}
@@ -0,0 +1,28 @@
/**
* 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 { Cluster } from "../../../common/cluster/cluster";
import loggerInjectable from "../../../common/logger.injectable";
import type { DeleteHelmReleaseData } from "../delete-helm-release.injectable";
import deleteHelmReleaseInjectable from "../delete-helm-release.injectable";
const deleteClusterHelmReleaseInjectable = getInjectable({
id: "delete-cluster-helm-release",
instantiate: (di) => {
const logger = di.inject(loggerInjectable);
const deleteHelmRelease = di.inject(deleteHelmReleaseInjectable);
return async (cluster: Cluster, data: DeleteHelmReleaseData) => {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
logger.debug(`[CLUSTER]: Delete helm release`, data);
return deleteHelmRelease(proxyKubeconfig, data);
};
},
});
export default deleteClusterHelmReleaseInjectable;
@@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../../../common/test-utils/get-global-override";
import getHelmChartReadmeInjectable from "./get-helm-chart-readme.injectable";
export default getGlobalOverride(getHelmChartReadmeInjectable, () => () => {
throw new Error("tried to get a helm chart's readme without overriding");
});
@@ -0,0 +1,31 @@
/**
* 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 getActiveHelmRepositoryInjectable from "../repositories/get-active-helm-repository.injectable";
import type { HelmRepo } from "../../../common/helm/helm-repo";
import helmChartManagerInjectable from "../helm-chart-manager.injectable";
const getHelmChartReadmeInjectable = getInjectable({
id: "get-helm-chart-readme",
instantiate: (di) => {
const getActiveHelmRepository = di.inject(getActiveHelmRepositoryInjectable);
const getChartManager = (repo: HelmRepo) => di.inject(helmChartManagerInjectable, repo);
return async (repoName: string, chartName: string, version = "") => {
const repo = await getActiveHelmRepository(repoName);
if (!repo) {
return undefined;
}
return getChartManager(repo).getReadme(chartName, version);
};
},
causesSideEffects: true,
});
export default getHelmChartReadmeInjectable;
@@ -0,0 +1,29 @@
/**
* 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 getActiveHelmRepositoryInjectable from "../repositories/get-active-helm-repository.injectable";
import type { HelmRepo } from "../../../common/helm/helm-repo";
import helmChartManagerInjectable from "../helm-chart-manager.injectable";
const getHelmChartValuesInjectable = getInjectable({
id: "get-helm-chart-values",
instantiate: (di) => {
const getActiveHelmRepository = di.inject(getActiveHelmRepositoryInjectable);
const getChartManager = (repo: HelmRepo) => di.inject(helmChartManagerInjectable, repo);
return async (repoName: string, chartName: string, version = "") => {
const repo = await getActiveHelmRepository(repoName);
if (!repo) {
return undefined;
}
return getChartManager(repo).getValues(chartName, version);
};
},
});
export default getHelmChartValuesInjectable;
@@ -0,0 +1,29 @@
/**
* 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 { HelmRepo } from "../../../common/helm/helm-repo";
import helmChartManagerInjectable from "../helm-chart-manager.injectable";
import getActiveHelmRepositoryInjectable from "../repositories/get-active-helm-repository.injectable";
const getHelmChartVersionsInjectable = getInjectable({
id: "get-helm-chart-versions",
instantiate: (di) => {
const getActiveHelmRepository = di.inject(getActiveHelmRepositoryInjectable);
const getChartManager = (repo: HelmRepo) => di.inject(helmChartManagerInjectable, repo);
return async (repoName: string, chartName: string) => {
const repo = await getActiveHelmRepository(repoName);
if (!repo) {
return undefined;
}
return getChartManager(repo).chartVersions(chartName);
};
},
});
export default getHelmChartVersionsInjectable;
@@ -0,0 +1,28 @@
/**
* 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 { Cluster } from "../../../common/cluster/cluster";
import loggerInjectable from "../../../common/logger.injectable";
import type { GetHelmReleaseHistoryData } from "../get-helm-release-history.injectable";
import getHelmReleaseHistoryInjectable from "../get-helm-release-history.injectable";
const getClusterHelmReleaseHistoryInjectable = getInjectable({
id: "get-cluster-helm-release-history",
instantiate: (di) => {
const logger = di.inject(loggerInjectable);
const getHelmReleaseHistory = di.inject(getHelmReleaseHistoryInjectable);
return async (cluster: Cluster, data: GetHelmReleaseHistoryData) => {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
logger.debug(`[CLUSTER]: Fetch release history for clusterId=${cluster.id}`, data);
return getHelmReleaseHistory(proxyKubeconfig, data);
};
},
});
export default getClusterHelmReleaseHistoryInjectable;
@@ -0,0 +1,47 @@
/**
* 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 { AsyncResult } from "../../../../../common/utils/async-result";
import execHelmInjectable from "../../../exec-helm/exec-helm.injectable";
import yaml from "js-yaml";
import type { KubeJsonApiData, KubeJsonApiDataList } from "../../../../../common/k8s-api/kube-json-api";
const callForHelmManifestInjectable = getInjectable({
id: "call-for-helm-manifest",
instantiate: (di) => {
const execHelm = di.inject(execHelmInjectable);
return async (
name: string,
namespace: string,
kubeconfigPath: string,
): Promise<AsyncResult<(KubeJsonApiData | KubeJsonApiDataList)[]>> => {
const result = await execHelm([
"get",
"manifest",
name,
"--namespace",
namespace,
"--kubeconfig",
kubeconfigPath,
]);
if (!result.callWasSuccessful) {
return { callWasSuccessful: false, error: result.error.message };
}
return {
callWasSuccessful: true,
response: yaml
.loadAll(result.response)
.filter((manifest) => !!manifest) as KubeJsonApiData[],
};
};
},
});
export default callForHelmManifestInjectable;
@@ -0,0 +1,73 @@
/**
* 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 { JsonObject } from "type-fest";
import { json } from "../../../../../common/utils";
import yaml from "js-yaml";
import execFileWithInputInjectable from "./exec-file-with-input/exec-file-with-input.injectable";
import { getErrorMessage } from "../../../../../common/utils/get-error-message";
import { map } from "lodash/fp";
import { pipeline } from "@ogre-tools/fp";
import type { KubeJsonApiData } from "../../../../../common/k8s-api/kube-json-api";
export type CallForKubeResourcesByManifest = (
namespace: string,
kubeconfigPath: string,
kubectlPath: string,
resourceManifests: KubeJsonApiData[]
) => Promise<JsonObject[]>;
const callForKubeResourcesByManifestInjectable = getInjectable({
id: "call-for-kube-resources-by-manifest",
instantiate: (di): CallForKubeResourcesByManifest => {
const execFileWithInput = di.inject(execFileWithInputInjectable);
return async (
namespace,
kubeconfigPath,
kubectlPath,
resourceManifests,
) => {
const input = pipeline(
resourceManifests,
map((manifest) => yaml.dump(manifest)),
wideJoin("---\n"),
);
const result = await execFileWithInput({
filePath: kubectlPath,
input,
commandArguments: [
"get",
"--kubeconfig",
kubeconfigPath,
"-f",
"-",
"--namespace",
namespace,
"--output",
"json",
],
});
if (!result.callWasSuccessful) {
const errorMessage = getErrorMessage(result.error);
throw new Error(errorMessage);
}
const output = json.parse(result.response) as { items: JsonObject[] };
return output.items;
};
},
});
export default callForKubeResourcesByManifestInjectable;
const wideJoin = (joiner: string) => (items: string[]) =>
`${joiner}${items.join(joiner)}${joiner}`;
@@ -0,0 +1,12 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../../../../../../common/test-utils/get-global-override";
import execFileWithInputInjectable from "./exec-file-with-input.injectable";
export default getGlobalOverride(execFileWithInputInjectable, () => () => {
throw new Error(
"Tried to call exec file with input without explicit override",
);
});
@@ -0,0 +1,92 @@
/**
* 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 { AsyncResult } from "../../../../../../common/utils/async-result";
import nonPromiseExecFileInjectable from "./non-promise-exec-file.injectable";
import { isNumber } from "../../../../../../common/utils";
import assert from "assert";
import type { ChildProcess } from "child_process";
export type ExecFileWithInput = (options: {
filePath: string;
commandArguments: string[];
input: string;
}) => Promise<AsyncResult<string, unknown>>;
const execFileWithInputInjectable = getInjectable({
id: "exec-file-with-input",
instantiate: (di): ExecFileWithInput => {
const execFile = di.inject(nonPromiseExecFileInjectable);
return async ({ filePath, commandArguments, input }) =>
new Promise((resolve) => {
let execution: ChildProcess;
try {
execution = execFile(filePath, commandArguments, {
maxBuffer: 8 * 1024 * 1024 * 1024, // 8 MiB
});
} catch (e) {
resolve({ callWasSuccessful: false, error: e });
return;
}
assert(execution.stdout, "stdout is not defined");
assert(execution.stderr, "stderr is not defined");
assert(execution.stdin, "stdin is not defined");
let stdout = "";
let stderr = "";
execution.stdout.on("data", (data) => {
stdout += data;
});
execution.stderr.on("data", (data) => {
stderr += data;
});
execution.on("error", (error) =>
resolve({ callWasSuccessful: false, error }),
);
execution.on("exit", (code, signal) => {
if (!isNumber(code)) {
/**
* According to https://nodejs.org/api/child_process.html#class-childprocess (section about the "exit" event)
* it says the following:
*
* If the process exited, code is the final exit code of the process, otherwise null.
* If the process terminated due to receipt of a signal, signal is the string name of the signal, otherwise null.
* One of the two will always be non-null.
*/
resolve({
callWasSuccessful: false,
error: `Exited via ${signal}`,
});
return;
}
if (code !== 0) {
resolve({
callWasSuccessful: false,
error: stderr ? stderr : `Failed with error: ${signal}`,
});
return;
}
resolve({ callWasSuccessful: true, response: stdout });
});
execution.stdin.end(input);
});
},
});
export default execFileWithInputInjectable;
@@ -0,0 +1,215 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getDiForUnitTesting } from "../../../../../getDiForUnitTesting";
import type { ExecFileWithInput } from "./exec-file-with-input.injectable";
import execFileWithInputInjectable from "./exec-file-with-input.injectable";
import type { AsyncResult } from "../../../../../../common/utils/async-result";
import nonPromiseExecFileInjectable from "./non-promise-exec-file.injectable";
import { getPromiseStatus } from "../../../../../../common/test-utils/get-promise-status";
import EventEmitter from "events";
describe("exec-file-with-input", () => {
let execFileWithInput: ExecFileWithInput;
let execFileMock: jest.Mock;
let executionStub: EventEmitter & {
stdin: { end: (chunk: any) => void };
stdout: EventEmitter;
stderr: EventEmitter;
};
beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
di.unoverride(execFileWithInputInjectable);
executionStub = Object.assign(new EventEmitter(), {
stdin: { end: jest.fn() },
stdout: new EventEmitter(),
stderr: new EventEmitter(),
});
execFileMock = jest.fn(() => executionStub);
di.override(nonPromiseExecFileInjectable, () => execFileMock as any);
execFileWithInput = di.inject(execFileWithInputInjectable);
});
it("given call, when throws synchronously, resolves with failure", async () => {
execFileMock.mockImplementation(() => {
throw new Error("some-error");
});
const actual = await execFileWithInput({
filePath: "./irrelevant",
commandArguments: ["irrelevant"],
input: "irrelevant",
});
expect(actual).toEqual({
callWasSuccessful: false,
error: expect.any(Error),
});
});
describe("when called", () => {
let actualPromise: Promise<AsyncResult<string, unknown>>;
beforeEach(() => {
actualPromise = execFileWithInput({
filePath: "./some-file-path",
commandArguments: ["some-arg", "some-other-arg"],
input: "some-input",
});
});
it("calls for file with arguments", () => {
expect(execFileMock).toHaveBeenCalledWith(
"./some-file-path",
[
"some-arg",
"some-other-arg",
],
{ "maxBuffer": 8589934592 },
);
});
it("calls with input", () => {
expect(executionStub.stdin.end).toHaveBeenCalledWith("some-input");
});
it("does not resolve yet", async () => {
const promiseStatus = await getPromiseStatus(actualPromise);
expect(promiseStatus.fulfilled).toBe(false);
});
describe("when stdout receives data", () => {
beforeEach(() => {
executionStub.stdout.emit("data", "some-data");
});
it("does not resolve yet", async () => {
const promiseStatus = await getPromiseStatus(actualPromise);
expect(promiseStatus.fulfilled).toBe(false);
});
describe("when stdout receives more data", () => {
beforeEach(() => {
executionStub.stdout.emit("data", "some-other-data");
});
it("does not resolve yet", async () => {
const promiseStatus = await getPromiseStatus(actualPromise);
expect(promiseStatus.fulfilled).toBe(false);
});
it("when execution exits with success, resolves with result", async () => {
executionStub.emit("exit", 0);
const actual = await actualPromise;
expect(actual).toEqual({
callWasSuccessful: true,
response: "some-datasome-other-data",
});
});
it("when execution exits without exit code, resolves with failure", async () => {
executionStub.emit("exit", null, "SIGKILL");
const actual = await actualPromise;
expect(actual).toEqual({
callWasSuccessful: false,
error: "Exited via SIGKILL",
});
});
it("when execution exits with failure, resolves with failure", async () => {
executionStub.emit("exit", 42, "some-signal");
const actual = await actualPromise;
expect(actual).toEqual({
callWasSuccessful: false,
error: "Failed with error: some-signal",
});
});
describe("when stderr receives data", () => {
beforeEach(() => {
executionStub.stderr.emit("data", "some-error");
});
it("does not resolve yet", async () => {
const promiseStatus = await getPromiseStatus(actualPromise);
expect(promiseStatus.fulfilled).toBe(false);
});
describe("when stderr receives more data", () => {
beforeEach(() => {
executionStub.stderr.emit("data", "some-other-error");
});
it("does not resolve yet", async () => {
const promiseStatus = await getPromiseStatus(actualPromise);
expect(promiseStatus.fulfilled).toBe(false);
});
it("when execution exits with success, resolves with result", async () => {
executionStub.emit("exit", 0);
const actual = await actualPromise;
expect(actual).toEqual({
callWasSuccessful: true,
response: "some-datasome-other-data",
});
});
it("when execution exits without exit code, resolves with failure", async () => {
executionStub.emit("exit", null, "some-signal");
const actual = await actualPromise;
expect(actual).toEqual({
callWasSuccessful: false,
error: "Exited via some-signal",
});
});
it("when execution exits with failure, resolves with errors", async () => {
executionStub.emit("exit", 42, "irrelevant");
const actual = await actualPromise;
expect(actual).toEqual({
callWasSuccessful: false,
error: "some-errorsome-other-error",
});
});
});
});
});
});
it("when execution receives error, resolves with error", async () => {
executionStub.emit("error", new Error("some-error"));
const actual = await actualPromise;
expect(actual).toEqual({
callWasSuccessful: false,
error: expect.any(Error),
});
});
});
});
@@ -0,0 +1,14 @@
/**
* 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 { execFile } from "child_process";
const nonPromiseExecFileInjectable = getInjectable({
id: "non-promise-exec-file",
instantiate: () => execFile,
causesSideEffects: true,
});
export default nonPromiseExecFileInjectable;
@@ -0,0 +1,41 @@
/**
* 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 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,
) => Promise<AsyncResult<KubeJsonApiData[], string>>;
const getHelmReleaseResourcesInjectable = getInjectable({
id: "get-helm-release-resources",
instantiate: (di): GetHelmReleaseResources => {
const callForHelmManifest = di.inject(callForHelmManifestInjectable);
return async (name, namespace, kubeconfigPath) => {
const result = await callForHelmManifest(name, namespace, kubeconfigPath);
if (!result.callWasSuccessful) {
return result;
}
return {
callWasSuccessful: true,
response: result.response.flatMap(item => (
Array.isArray(item.items)
? (item as KubeJsonApiDataList).items
: item as KubeJsonApiData
)),
};
};
},
});
export default getHelmReleaseResourcesInjectable;
@@ -0,0 +1,145 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import type { GetHelmReleaseResources } from "./get-helm-release-resources.injectable";
import getHelmReleaseResourcesInjectable from "./get-helm-release-resources.injectable";
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 { 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;
let execHelmMock: AsyncFnMock<ExecHelm>;
let execFileWithStreamInputMock: AsyncFnMock<ExecFileWithInput>;
beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
execHelmMock = asyncFn();
execFileWithStreamInputMock = asyncFn();
di.override(execHelmInjectable, () => execHelmMock);
di.override(
execFileWithInputInjectable,
() => execFileWithStreamInputMock,
);
getHelmReleaseResources = di.inject(getHelmReleaseResourcesInjectable);
});
describe("when called", () => {
let actualPromise: Promise<AsyncResult<KubeJsonApiData[], string>>;
beforeEach(() => {
actualPromise = getHelmReleaseResources(
"some-release",
"some-namespace",
"/some-kubeconfig-path",
);
});
it("calls for release manifest", () => {
expect(execHelmMock).toHaveBeenCalledWith([
"get", "manifest", "some-release", "--namespace", "some-namespace", "--kubeconfig", "/some-kubeconfig-path",
]);
});
it("does not call for resources yet", () => {
expect(execFileWithStreamInputMock).not.toHaveBeenCalled();
});
it("when call for manifest resolves without resources, resolves without resources", async () => {
await execHelmMock.resolve({
callWasSuccessful: true,
response: "",
});
const actual = await actualPromise;
expect(actual).toEqual({
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:
name: some-resource-with-same-namespace
namespace: some-namespace
---
apiVersion: v1
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
---
`,
});
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",
},
},
],
});
});
});
});
@@ -0,0 +1,28 @@
/**
* 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 loggerInjectable from "../../../common/logger.injectable";
import type { Cluster } from "../../../common/cluster/cluster";
import type { GetHelmReleaseValuesData } from "../get-helm-release-values.injectable";
import getHelmReleaseValuesInjectable from "../get-helm-release-values.injectable";
const getClusterHelmReleaseValuesInjectable = getInjectable({
id: "get-cluster-helm-release-values",
instantiate: (di) => {
const logger = di.inject(loggerInjectable);
const getHelmReleaseValues = di.inject(getHelmReleaseValuesInjectable);
return async (cluster: Cluster, data: GetHelmReleaseValuesData) => {
const pathToKubeconfig = await cluster.getProxyKubeconfigPath();
logger.debug(`[CLUSTER]: getting helm release values`, data);
return getHelmReleaseValues(pathToKubeconfig, data);
};
},
});
export default getClusterHelmReleaseValuesInjectable;
@@ -0,0 +1,10 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../../../common/test-utils/get-global-override";
import getHelmReleaseInjectable from "./get-helm-release.injectable";
export default getGlobalOverride(getHelmReleaseInjectable, () => () => {
throw new Error("Tried to get helm release without explicit override");
});
@@ -0,0 +1,70 @@
/**
* 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 { Cluster } from "../../../common/cluster/cluster";
import loggerInjectable from "../../../common/logger.injectable";
import { isObject, json } from "../../../common/utils";
import execHelmInjectable from "../exec-helm/exec-helm.injectable";
import getHelmReleaseResourcesInjectable from "./get-helm-release-resources/get-helm-release-resources.injectable";
const getHelmReleaseInjectable = getInjectable({
id: "get-helm-release",
instantiate: (di) => {
const logger = di.inject(loggerInjectable);
const execHelm = di.inject(execHelmInjectable);
const getHelmReleaseResources = di.inject(getHelmReleaseResourcesInjectable);
return async (cluster: Cluster, releaseName: string, namespace: string) => {
const kubeconfigPath = await cluster.getProxyKubeconfigPath();
logger.debug("Fetch release");
const result = await execHelm([
"status",
releaseName,
"--namespace",
namespace,
"--kubeconfig",
kubeconfigPath,
"--output",
"json",
]);
if (!result.callWasSuccessful) {
logger.warn(`Failed to exectute helm: ${result.error}`);
return undefined;
}
const release = json.parse(result.response);
if (!isObject(release) || Array.isArray(release)) {
return undefined;
}
const resourcesResult = await getHelmReleaseResources(
releaseName,
namespace,
kubeconfigPath,
);
if (!resourcesResult.callWasSuccessful) {
logger.warn(`Failed to get helm release resources: ${resourcesResult.error}`);
return undefined;
}
return {
...release,
resources: resourcesResult.response,
};
};
},
causesSideEffects: true,
});
export default getHelmReleaseInjectable;
@@ -0,0 +1,35 @@
/**
* 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 { JsonObject } from "type-fest";
import type { Cluster } from "../../../common/cluster/cluster";
import installHelmChartInjectable from "../install-helm-chart.injectable";
export interface InstallChartArgs {
chart: string;
values: JsonObject;
name: string;
namespace: string;
version: string;
}
const installClusterHelmChartInjectable = getInjectable({
id: "install-cluster-helm-chart",
instantiate: (di) => {
const installHelmChart = di.inject(installHelmChartInjectable);
return async (cluster: Cluster, data: InstallChartArgs) => {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
return installHelmChart({
...data,
kubeconfigPath: proxyKubeconfig,
});
};
},
});
export default installClusterHelmChartInjectable;
@@ -0,0 +1,41 @@
/**
* 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 assert from "assert";
import { object } from "../../../common/utils";
import getActiveHelmRepositoriesInjectable from "../repositories/get-active-helm-repositories/get-active-helm-repositories.injectable";
import type { HelmRepo } from "../../../common/helm/helm-repo";
import helmChartManagerInjectable from "../helm-chart-manager.injectable";
const listHelmChartsInjectable = getInjectable({
id: "list-helm-charts",
instantiate: (di) => {
const getActiveHelmRepositories = di.inject(getActiveHelmRepositoriesInjectable);
const getChartManager = (repo: HelmRepo) => di.inject(helmChartManagerInjectable, repo);
return async () => {
const result = await getActiveHelmRepositories();
assert(result.callWasSuccessful);
const repositories = result.response;
return object.fromEntries(
await Promise.all(
repositories.map(
async (repo) =>
[
repo.name,
await getChartManager(repo).charts(),
] as const,
),
),
);
};
},
});
export default listHelmChartsInjectable;
@@ -0,0 +1,27 @@
/**
* 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 { Cluster } from "../../../common/cluster/cluster";
import loggerInjectable from "../../../common/logger.injectable";
import listHelmReleasesInjectable from "../list-helm-releases.injectable";
const listClusterHelmReleasesInjectable = getInjectable({
id: "list-cluster-helm-releases",
instantiate: (di) => {
const logger = di.inject(loggerInjectable);
const listHelmReleases = di.inject(listHelmReleasesInjectable);
return async (cluster: Cluster, namespace?: string) => {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
logger.debug(`[CLUSTER]: listing helm releases for clusterId=${cluster.id}`, { namespace });
return listHelmReleases(proxyKubeconfig, namespace);
};
},
});
export default listClusterHelmReleasesInjectable;
@@ -0,0 +1,28 @@
/**
* 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 { Cluster } from "../../../common/cluster/cluster";
import loggerInjectable from "../../../common/logger.injectable";
import type { RollbackHelmReleaseData } from "../rollback-helm-release.injectable";
import rollbackHelmReleaseInjectable from "../rollback-helm-release.injectable";
const rollbackClusterHelmReleaseInjectable = getInjectable({
id: "rollback-cluster-helm-release",
instantiate: (di) => {
const logger = di.inject(loggerInjectable);
const rollbackHelmRelease = di.inject(rollbackHelmReleaseInjectable);
return async (cluster: Cluster, data: RollbackHelmReleaseData) => {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
logger.debug(`[CLUSTER]: rolling back helm release for clusterId=${cluster.id}`, data);
await rollbackHelmRelease(proxyKubeconfig, data);
};
},
});
export default rollbackClusterHelmReleaseInjectable;
@@ -0,0 +1,10 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../../../common/test-utils/get-global-override";
import updateHelmReleaseInjectable from "./update-helm-release.injectable";
export default getGlobalOverride(updateHelmReleaseInjectable, () => () => {
throw new Error("Tried to update helm release without explicit override");
});
@@ -0,0 +1,65 @@
/**
* 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 { Cluster } from "../../../common/cluster/cluster";
import loggerInjectable from "../../../common/logger.injectable";
import tempy from "tempy";
import getHelmReleaseInjectable from "./get-helm-release.injectable";
import writeFileInjectable from "../../../common/fs/write-file.injectable";
import removePathInjectable from "../../../common/fs/remove.injectable";
import execHelmInjectable from "../exec-helm/exec-helm.injectable";
export interface UpdateChartArgs {
chart: string;
values: string;
version: string;
}
const updateHelmReleaseInjectable = getInjectable({
id: "update-helm-release",
instantiate: (di) => {
const logger = di.inject(loggerInjectable);
const getHelmRelease = di.inject(getHelmReleaseInjectable);
const writeFile = di.inject(writeFileInjectable);
const removePath = di.inject(removePathInjectable);
const execHelm = di.inject(execHelmInjectable);
return async (cluster: Cluster, releaseName: string, namespace: string, data: UpdateChartArgs) => {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
const valuesFilePath = tempy.file({ name: "values.yaml" });
logger.debug(`[HELM]: upgrading "${releaseName}" in "${namespace}" to ${data.version}`);
try {
await writeFile(valuesFilePath, data.values);
const result = await execHelm([
"upgrade",
releaseName,
data.chart,
"--version", data.version,
"--values", valuesFilePath,
"--namespace", namespace,
"--kubeconfig", proxyKubeconfig,
]);
if (result.callWasSuccessful === false) {
throw result.error; // keep the same interface
}
return {
log: result.response,
release: await getHelmRelease(cluster, releaseName, namespace),
};
} finally {
await removePath(valuesFilePath);
}
};
},
});
export default updateHelmReleaseInjectable;
@@ -0,0 +1,93 @@
/**
* 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 { dump } from "js-yaml";
import tempy from "tempy";
import type { JsonValue } from "type-fest";
import removePathInjectable from "../../common/fs/remove.injectable";
import writeFileInjectable from "../../common/fs/write-file.injectable";
import execHelmInjectable from "./exec-helm/exec-helm.injectable";
export interface InstallHelmChartData {
chart: string;
values: JsonValue;
name: string;
namespace: string;
version: string;
kubeconfigPath: string;
}
export interface InstallHelmChartResult {
log: string;
release: {
name: string;
namespace: string;
};
}
export type InstallHelmChart = (data: InstallHelmChartData) => Promise<InstallHelmChartResult>;
const installHelmChartInjectable = getInjectable({
id: "install-helm-chart",
instantiate: (di): InstallHelmChart => {
const writeFile = di.inject(writeFileInjectable);
const removePath = di.inject(removePathInjectable);
const execHelm = di.inject(execHelmInjectable);
return async ({
chart,
kubeconfigPath,
name,
namespace,
values,
version,
}) => {
const valuesFilePath = tempy.file({ name: "values.yaml" });
await writeFile(valuesFilePath, dump(values));
const args = ["install"];
if (name) {
args.push(name);
}
args.push(
chart,
"--version", version,
"--values", valuesFilePath,
"--namespace", namespace,
"--kubeconfig", kubeconfigPath,
);
if (!name) {
args.push("--generate-name");
}
try {
const result = await execHelm(args);
if (!result.callWasSuccessful) {
throw result.error;
}
const output = result.response;
const releaseName = output.split("\n")[0].split(" ")[1].trim();
return {
log: output,
release: {
name: releaseName,
namespace,
},
};
} finally {
await removePath(valuesFilePath);
}
};
},
});
export default installHelmChartInjectable;
@@ -0,0 +1,48 @@
/**
* 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 execHelmInjectable from "./exec-helm/exec-helm.injectable";
import { isObject, json, toCamelCase } from "../../common/utils";
export type ListHelmReleases = (pathToKubeconfig: string, namespace?: string) => Promise<Record<string, any>[]>;
const listHelmReleasesInjectable = getInjectable({
id: "list-helm-releases",
instantiate: (di): ListHelmReleases => {
const execHelm = di.inject(execHelmInjectable);
return async (pathToKubeconfig, namespace) => {
const args = [
"ls",
"--all",
"--output", "json",
];
if (namespace) {
args.push("-n", namespace);
} else {
args.push("--all-namespaces");
}
args.push("--kubeconfig", pathToKubeconfig);
const result = await execHelm(args);
if (!result.callWasSuccessful) {
throw result.error;
}
const output = json.parse(result.response);
if (!Array.isArray(output) || output.length == 0) {
return [];
}
return output.filter(isObject).map(toCamelCase);
};
},
});
export default listHelmReleasesInjectable;
@@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { addHelmRepositoryChannel } from "../../../../common/helm/add-helm-repository-channel";
import { getRequestChannelListenerInjectable } from "../../../utils/channel/channel-listeners/listener-tokens";
import addHelmRepositoryInjectable from "./add-helm-repository.injectable";
const addHelmRepositoryChannelListenerInjectable = getRequestChannelListenerInjectable({
channel: addHelmRepositoryChannel,
handler: (di) => di.inject(addHelmRepositoryInjectable),
});
export default addHelmRepositoryChannelListenerInjectable;
@@ -0,0 +1,74 @@
/**
* 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 execHelmInjectable from "../../exec-helm/exec-helm.injectable";
import loggerInjectable from "../../../../common/logger.injectable";
import type { AddHelmRepositoryChannel } from "../../../../common/helm/add-helm-repository-channel";
import type { RequestChannelHandler } from "../../../utils/channel/channel-listeners/listener-tokens";
const addHelmRepositoryInjectable = getInjectable({
id: "add-helm-repository",
instantiate: (di): RequestChannelHandler<AddHelmRepositoryChannel> => {
const execHelm = di.inject(execHelmInjectable);
const logger = di.inject(loggerInjectable);
return async (repo) => {
const {
name,
url,
insecureSkipTlsVerify,
username,
password,
caFile,
keyFile,
certFile,
} = repo;
logger.info(`[HELM]: adding repo ${name} from ${url}`);
const args = ["repo", "add", name, url];
if (insecureSkipTlsVerify) {
args.push("--insecure-skip-tls-verify");
}
if (username) {
args.push("--username", username);
}
if (password) {
args.push("--password", password);
}
if (caFile) {
args.push("--ca-file", caFile);
}
if (keyFile) {
args.push("--key-file", keyFile);
}
if (certFile) {
args.push("--cert-file", certFile);
}
const result = await execHelm(args);
if (result.callWasSuccessful) {
return {
callWasSuccessful: true,
};
}
return {
callWasSuccessful: false,
error: result.error.stderr || result.error.message,
};
};
},
});
export default addHelmRepositoryInjectable;
@@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getActiveHelmRepositoriesChannel } from "../../../../common/helm/get-active-helm-repositories-channel";
import { getRequestChannelListenerInjectable } from "../../../utils/channel/channel-listeners/listener-tokens";
import getActiveHelmRepositoriesInjectable from "./get-active-helm-repositories.injectable";
const getActiveHelmRepositoriesChannelListenerInjectable = getRequestChannelListenerInjectable({
channel: getActiveHelmRepositoriesChannel,
handler: (di) => di.inject(getActiveHelmRepositoriesInjectable),
});
export default getActiveHelmRepositoriesChannelListenerInjectable;
@@ -0,0 +1,131 @@
/**
* 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 { HelmRepo } from "../../../../common/helm/helm-repo";
import type { ReadYamlFile } from "../../../../common/fs/read-yaml-file.injectable";
import readYamlFileInjectable from "../../../../common/fs/read-yaml-file.injectable";
import getHelmEnvInjectable from "../../get-helm-env/get-helm-env.injectable";
import execHelmInjectable from "../../exec-helm/exec-helm.injectable";
import loggerInjectable from "../../../../common/logger.injectable";
import type { AsyncResult } from "../../../../common/utils/async-result";
interface HelmRepositoryFromYaml {
name: string;
url: string;
caFile: string;
certFile: string;
insecure_skip_tls_verify: boolean;
keyFile: string;
pass_credentials_all: boolean;
password: string;
username: string;
}
export interface HelmRepositoriesFromYaml {
repositories: HelmRepositoryFromYaml[];
}
const getActiveHelmRepositoriesInjectable = getInjectable({
id: "get-helm-repositories",
instantiate: (di) => {
const readYamlFile = di.inject(readYamlFileInjectable);
const execHelm = di.inject(execHelmInjectable);
const getHelmEnv = di.inject(getHelmEnvInjectable);
const logger = di.inject(loggerInjectable);
const getRepositories = getRepositoriesFor(readYamlFile);
return async (): Promise<AsyncResult<HelmRepo[]>> => {
const envResult = await getHelmEnv();
if (!envResult.callWasSuccessful) {
return {
callWasSuccessful: false,
error: `Error getting Helm configuration: ${envResult.error}`,
};
}
const {
HELM_REPOSITORY_CONFIG: repositoryConfigFilePath,
HELM_REPOSITORY_CACHE: helmRepositoryCacheDirPath,
} = envResult.response;
if (!repositoryConfigFilePath) {
const errorMessage = "Tried to get Helm repositories, but HELM_REPOSITORY_CONFIG was not present in `$ helm env`.";
logger.error(errorMessage);
return {
callWasSuccessful: false,
error: `Error getting Helm configuration: ${errorMessage}`,
};
}
if (!helmRepositoryCacheDirPath) {
const errorMessage = "Tried to get Helm repositories, but HELM_REPOSITORY_CACHE was not present in `$ helm env`.";
logger.error(errorMessage);
return {
callWasSuccessful: false,
error: `Error getting Helm configuration: ${errorMessage}`,
};
}
const updateResult = await execHelm(["repo", "update"]);
if (!updateResult.callWasSuccessful) {
if (!updateResult.error.stderr.includes(internalHelmErrorForNoRepositoriesFound)) {
return {
callWasSuccessful: false,
error: `Error updating Helm repositories: ${updateResult.error.stderr}`,
};
}
const resultOfAddingDefaultRepository = await execHelm(["repo", "add", "bitnami", "https://charts.bitnami.com/bitnami"]);
if (!resultOfAddingDefaultRepository.callWasSuccessful) {
return {
callWasSuccessful: false,
error: `Error when adding default Helm repository: ${resultOfAddingDefaultRepository.error.stderr}`,
};
}
}
return {
callWasSuccessful: true,
response: await getRepositories(
repositoryConfigFilePath,
helmRepositoryCacheDirPath,
),
};
};
},
});
export default getActiveHelmRepositoriesInjectable;
const getRepositoriesFor =
(readYamlFile: ReadYamlFile) =>
async (repositoryConfigFilePath: string, helmRepositoryCacheDirPath: string): Promise<HelmRepo[]> => {
const { repositories } = (await readYamlFile(
repositoryConfigFilePath,
)) as HelmRepositoriesFromYaml;
return repositories.map((repository) => ({
name: repository.name,
url: repository.url,
caFile: repository.caFile,
certFile: repository.certFile,
insecureSkipTlsVerify: repository.insecure_skip_tls_verify,
keyFile: repository.keyFile,
username: repository.username,
password: repository.password,
cacheFilePath: `${helmRepositoryCacheDirPath}/${repository.name}-index.yaml`,
}));
};
const internalHelmErrorForNoRepositoriesFound = "no repositories found. You must add one before updating";
@@ -0,0 +1,27 @@
/**
* 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 assert from "assert";
import getActiveHelmRepositoriesInjectable from "./get-active-helm-repositories/get-active-helm-repositories.injectable";
const getActiveHelmRepositoryInjectable = getInjectable({
id: "get-active-helm-repository",
instantiate: (di) => {
const getActiveHelmRepositories = di.inject(getActiveHelmRepositoriesInjectable);
return async (name: string) => {
const activeHelmRepositories = await getActiveHelmRepositories();
assert(activeHelmRepositories.callWasSuccessful);
return activeHelmRepositories.response.find(
(repository) => repository.name === name,
);
};
},
});
export default getActiveHelmRepositoryInjectable;
@@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import removeHelmRepositoryInjectable from "./remove-helm-repository.injectable";
import { removeHelmRepositoryChannel } from "../../../../common/helm/remove-helm-repository-channel";
import { getRequestChannelListenerInjectable } from "../../../utils/channel/channel-listeners/listener-tokens";
const removeHelmRepositoryChannelListenerInjectable = getRequestChannelListenerInjectable({
channel: removeHelmRepositoryChannel,
handler: (di) => di.inject(removeHelmRepositoryInjectable),
});
export default removeHelmRepositoryChannelListenerInjectable;
@@ -0,0 +1,41 @@
/**
* 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 execHelmInjectable from "../../exec-helm/exec-helm.injectable";
import type { HelmRepo } from "../../../../common/helm/helm-repo";
import loggerInjectable from "../../../../common/logger.injectable";
import type { AsyncResult } from "../../../../common/utils/async-result";
const removeHelmRepositoryInjectable = getInjectable({
id: "remove-helm-repository",
instantiate: (di) => {
const execHelm = di.inject(execHelmInjectable);
const logger = di.inject(loggerInjectable);
return async (repo: HelmRepo): Promise<AsyncResult<void, string>> => {
logger.info(`[HELM]: removing repo ${repo.name} (${repo.url})`);
const result = await execHelm([
"repo",
"remove",
repo.name,
]);
if (result.callWasSuccessful) {
return {
callWasSuccessful: true,
};
}
return {
callWasSuccessful: false,
error: result.error.stderr,
};
};
},
});
export default removeHelmRepositoryInjectable;
@@ -0,0 +1,37 @@
/**
* 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 execHelmInjectable from "./exec-helm/exec-helm.injectable";
export interface RollbackHelmReleaseData {
name: string;
namespace: string;
revision: number;
}
export type RollbackHelmRelease = (kubeconfigPath: string, data: RollbackHelmReleaseData) => Promise<void>;
const rollbackHelmReleaseInjectable = getInjectable({
id: "rollback-helm-release",
instantiate: (di): RollbackHelmRelease => {
const execHelm = di.inject(execHelmInjectable);
return async (kubeconfigPath, { name, namespace, revision }) => {
const result = await execHelm([
"rollback",
name,
`${revision}`,
"--namespace", namespace,
"--kubeconfig", kubeconfigPath,
]);
if (!result.callWasSuccessful) {
throw result.error;
}
};
},
});
export default rollbackHelmReleaseInjectable;