diff --git a/src/main/helm/__mocks__/helm-chart-manager.ts b/src/main/helm/__mocks__/helm-chart-manager.ts new file mode 100644 index 0000000000..e832a937cb --- /dev/null +++ b/src/main/helm/__mocks__/helm-chart-manager.ts @@ -0,0 +1,108 @@ +import { HelmRepo, HelmRepoManager } from "../helm-repo-manager"; + +export class HelmChartManager { + private cache: any = {}; + private repo: HelmRepo; + + constructor(repo: HelmRepo){ + this.cache = HelmRepoManager.cache; + this.repo = repo; + } + + public async charts(): Promise { + switch (this.repo.name) { + case "stable": + return Promise.resolve({ + "apm-server": [ + { + apiVersion: "3.0.0", + name: "apm-server", + version: "2.1.7", + repo: "stable", + digest: "test" + }, + { + apiVersion: "3.0.0", + name: "apm-server", + version: "2.1.6", + repo: "stable", + digest: "test" + } + ], + "redis": [ + { + apiVersion: "3.0.0", + name: "apm-server", + version: "1.0.0", + repo: "stable", + digest: "test" + }, + { + apiVersion: "3.0.0", + name: "apm-server", + version: "0.0.9", + repo: "stable", + digest: "test" + } + ] + }); + case "experiment": + return Promise.resolve({ + "fairwind": [ + { + apiVersion: "3.0.0", + name: "fairwind", + version: "0.0.1", + repo: "experiment", + digest: "test" + }, + { + apiVersion: "3.0.0", + name: "fairwind", + version: "0.0.2", + repo: "experiment", + digest: "test", + deprecated: true + } + ] + }); + case "bitnami": + return Promise.resolve({ + "hotdog": [ + { + apiVersion: "3.0.0", + name: "hotdog", + version: "1.0.1", + repo: "bitnami", + digest: "test" + }, + { + apiVersion: "3.0.0", + name: "hotdog", + version: "1.0.2", + repo: "bitnami", + digest: "test", + } + ], + "pretzel": [ + { + apiVersion: "3.0.0", + name: "pretzel", + version: "1.0", + repo: "bitnami", + digest: "test", + }, + { + apiVersion: "3.0.0", + name: "pretzel", + version: "1.0.1", + repo: "bitnami", + digest: "test" + } + ] + }); + default: + return Promise.resolve({}); + } + } +} diff --git a/src/main/helm/__tests__/helm-service.test.ts b/src/main/helm/__tests__/helm-service.test.ts new file mode 100644 index 0000000000..8c1e82ef0a --- /dev/null +++ b/src/main/helm/__tests__/helm-service.test.ts @@ -0,0 +1,104 @@ +import { helmService } from "../helm-service"; +import { repoManager } from "../helm-repo-manager"; + +jest.spyOn(repoManager, "init").mockImplementation(); + +jest.mock("../helm-chart-manager"); + +describe("Helm Service tests", () => { + test("list charts without deprecated ones", async () => { + jest.spyOn(repoManager, "repositories").mockImplementation(async () => { + return [ + { name: "stable", url: "stableurl" }, + { name: "experiment", url: "experimenturl" } + ]; + }); + + const charts = await helmService.listCharts(); + + expect(charts).toEqual({ + stable: { + "apm-server": [ + { + apiVersion: "3.0.0", + name: "apm-server", + version: "2.1.7", + repo: "stable", + digest: "test" + }, + { + apiVersion: "3.0.0", + name: "apm-server", + version: "2.1.6", + repo: "stable", + digest: "test" + } + ], + "redis": [ + { + apiVersion: "3.0.0", + name: "apm-server", + version: "1.0.0", + repo: "stable", + digest: "test" + }, + { + apiVersion: "3.0.0", + name: "apm-server", + version: "0.0.9", + repo: "stable", + digest: "test" + } + ] + }, + experiment: {} + }); + }); + + test("list charts sorted by version in descending order", async () => { + jest.spyOn(repoManager, "repositories").mockImplementation(async () => { + return [ + { name: "bitnami", url: "bitnamiurl" } + ]; + }); + + const charts = await helmService.listCharts(); + + expect(charts).toEqual({ + bitnami: { + "hotdog": [ + { + apiVersion: "3.0.0", + name: "hotdog", + version: "1.0.2", + repo: "bitnami", + digest: "test", + }, + { + apiVersion: "3.0.0", + name: "hotdog", + version: "1.0.1", + repo: "bitnami", + digest: "test" + }, + ], + "pretzel": [ + { + apiVersion: "3.0.0", + name: "pretzel", + version: "1.0.1", + repo: "bitnami", + digest: "test", + }, + { + apiVersion: "3.0.0", + name: "pretzel", + version: "1.0", + repo: "bitnami", + digest: "test" + } + ] + } + }); + }); +}); diff --git a/src/main/helm/helm-chart-manager.ts b/src/main/helm/helm-chart-manager.ts index cf4a8e5ace..69619a56d4 100644 --- a/src/main/helm/helm-chart-manager.ts +++ b/src/main/helm/helm-chart-manager.ts @@ -4,9 +4,10 @@ import { HelmRepo, HelmRepoManager } from "./helm-repo-manager"; import logger from "../logger"; import { promiseExec } from "../promise-exec"; import { helmCli } from "./helm-cli"; +import type { RepoHelmChartList } from "../../renderer/api/endpoints/helm-charts.api"; type CachedYaml = { - entries: any; // todo: types + entries: RepoHelmChartList }; export class HelmChartManager { @@ -24,15 +25,15 @@ export class HelmChartManager { return charts[name]; } - public async charts(): Promise { + public async charts(): Promise { try { const cachedYaml = await this.cachedYaml(); return cachedYaml["entries"]; } catch(error) { - logger.error(error); + logger.error("HELM-CHART-MANAGER]: failed to list charts", { error }); - return []; + return {}; } } diff --git a/src/main/helm/helm-service.ts b/src/main/helm/helm-service.ts index 1918268075..f7445cebd4 100644 --- a/src/main/helm/helm-service.ts +++ b/src/main/helm/helm-service.ts @@ -1,8 +1,10 @@ +import semver from "semver"; import { Cluster } from "../cluster"; import logger from "../logger"; import { repoManager } from "./helm-repo-manager"; import { HelmChartManager } from "./helm-chart-manager"; import { releaseManager } from "./helm-release-manager"; +import { HelmChartList, RepoHelmChartList } from "../../renderer/api/endpoints/helm-charts.api"; class HelmService { public async installChart(cluster: Cluster, data: { chart: string; values: {}; name: string; namespace: string; version: string }) { @@ -10,7 +12,7 @@ class HelmService { } public async listCharts() { - const charts: any = {}; + const charts: HelmChartList = {}; await repoManager.init(); const repositories = await repoManager.repositories(); @@ -18,14 +20,10 @@ class HelmService { for (const repo of repositories) { charts[repo.name] = {}; const manager = new HelmChartManager(repo); - let entries = await manager.charts(); + const sortedCharts = this.sortChartsByVersion(await manager.charts()); + const enabledCharts = this.excludeDeprecatedChartGroups(sortedCharts); - entries = this.excludeDeprecated(entries); - - for (const key in entries) { - entries[key] = entries[key][0]; - } - charts[repo.name] = entries; + charts[repo.name] = enabledCharts; } return charts; @@ -96,20 +94,30 @@ class HelmService { return { message: output }; } - protected excludeDeprecated(entries: any) { - for (const key in entries) { - entries[key] = entries[key].filter((entry: any) => { - if (Array.isArray(entry)) { - return entry[0]["deprecated"] != true; - } + private excludeDeprecatedChartGroups(chartGroups: RepoHelmChartList) { + const groups = new Map(Object.entries(chartGroups)); - return entry["deprecated"] != true; + for (const [chartName, charts] of groups) { + if (charts[0].deprecated) { + groups.delete(chartName); + } + } + + return Object.fromEntries(groups); + } + + private sortChartsByVersion(chartGroups: RepoHelmChartList) { + for (const key in chartGroups) { + chartGroups[key] = chartGroups[key].sort((first, second) => { + const firstVersion = semver.coerce(first.version || 0); + const secondVersion = semver.coerce(second.version || 0); + + return semver.compare(secondVersion, firstVersion); }); } - return entries; + return chartGroups; } - } export const helmService = new HelmService(); diff --git a/src/renderer/api/endpoints/helm-charts.api.ts b/src/renderer/api/endpoints/helm-charts.api.ts index 8beff01779..02b5b0dbee 100644 --- a/src/renderer/api/endpoints/helm-charts.api.ts +++ b/src/renderer/api/endpoints/helm-charts.api.ts @@ -3,11 +3,8 @@ import { apiBase } from "../index"; import { stringify } from "querystring"; import { autobind } from "../../utils"; -interface IHelmChartList { - [repo: string]: { - [name: string]: HelmChart; - }; -} +export type RepoHelmChartList = Record; +export type HelmChartList = Record; export interface IHelmChartDetails { readme: string; @@ -22,12 +19,12 @@ const endpoint = compile(`/v2/charts/:repo?/:name?`) as (params?: { export const helmChartsApi = { list() { return apiBase - .get(endpoint()) + .get(endpoint()) .then(data => { return Object .values(data) .reduce((allCharts, repoCharts) => allCharts.concat(Object.values(repoCharts)), []) - .map(HelmChart.create); + .map(([chart]) => HelmChart.create(chart)); }); }, diff --git a/src/renderer/components/+apps-helm-charts/helm-charts.tsx b/src/renderer/components/+apps-helm-charts/helm-charts.tsx index 22d5ba3a2f..8e4afe8d83 100644 --- a/src/renderer/components/+apps-helm-charts/helm-charts.tsx +++ b/src/renderer/components/+apps-helm-charts/helm-charts.tsx @@ -72,9 +72,6 @@ export class HelmCharts extends Component { (chart: HelmChart) => chart.getAppVersion(), (chart: HelmChart) => chart.getKeywords(), ]} - filterItems={[ - (items: HelmChart[]) => items.filter(item => !item.deprecated) - ]} customizeHeader={() => ( )}