Fix filtering deprecated charts (#3635)

This commit is contained in:
Sebastian Malton authored and GitHub committed 2021-08-24 08:29:36 -04:00
1 parent 54ac311933
commit 5f89b3e31f
10 files changed
+324 -284

No files matched your search

+140 -130
View File
@@ -19,141 +19,151 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { HelmRepo, HelmRepoManager } from "../helm-repo-manager";
import { sortCharts } from "../../../common/utils";
import type { HelmRepo } from "../helm-repo-manager";
const charts = new Map([
["stable", {
"invalid-semver": sortCharts([
{
apiVersion: "3.0.0",
name: "weird-versioning",
version: "I am not semver",
repo: "stable",
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "weird-versioning",
version: "v4.3.0",
repo: "stable",
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "weird-versioning",
version: "I am not semver but more",
repo: "stable",
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "weird-versioning",
version: "v4.4.0",
repo: "stable",
digest: "test",
created: "now",
},
]),
"apm-server": sortCharts([
{
apiVersion: "3.0.0",
name: "apm-server",
version: "2.1.7",
repo: "stable",
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "apm-server",
version: "2.1.6",
repo: "stable",
digest: "test",
created: "now",
}
]),
"redis": sortCharts([
{
apiVersion: "3.0.0",
name: "apm-server",
version: "1.0.0",
repo: "stable",
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "apm-server",
version: "0.0.9",
repo: "stable",
digest: "test",
created: "now",
}
]),
}],
["experiment", {
"fairwind": sortCharts([
{
apiVersion: "3.0.0",
name: "fairwind",
version: "0.0.1",
repo: "experiment",
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "fairwind",
version: "0.0.2",
repo: "experiment",
digest: "test",
deprecated: true,
created: "now",
}
]),
}],
["bitnami", {
"hotdog": sortCharts([
{
apiVersion: "3.0.0",
name: "hotdog",
version: "1.0.1",
repo: "bitnami",
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "hotdog",
version: "1.0.2",
repo: "bitnami",
digest: "test",
created: "now",
}
]),
"pretzel": sortCharts([
{
apiVersion: "3.0.0",
name: "pretzel",
version: "1.0",
repo: "bitnami",
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "pretzel",
version: "1.0.1",
repo: "bitnami",
digest: "test",
created: "now",
}
]),
}],
]);
export class HelmChartManager {
cache: any = {};
private repo: HelmRepo;
constructor(private repo: HelmRepo){ }
constructor(repo: HelmRepo){
this.cache = HelmRepoManager.cache;
this.repo = repo;
static forRepo(repo: HelmRepo) {
return new this(repo);
}
public async charts(): Promise<any> {
switch (this.repo.name) {
case "stable":
return Promise.resolve({
"invalid-semver": [
{
apiVersion: "3.0.0",
name: "weird-versioning",
version: "I am not semver",
repo: "stable",
digest: "test"
},
{
apiVersion: "3.0.0",
name: "weird-versioning",
version: "v4.3.0",
repo: "stable",
digest: "test"
},
{
apiVersion: "3.0.0",
name: "weird-versioning",
version: "I am not semver but more",
repo: "stable",
digest: "test"
},
{
apiVersion: "3.0.0",
name: "weird-versioning",
version: "v4.4.0",
repo: "stable",
digest: "test"
},
],
"apm-server": [
{
apiVersion: "3.0.0",
name: "apm-server",
version: "2.1.7",
repo: "stable",
digest: "test"
},
{
apiVersion: "3.0.0",
name: "apm-server",
version: "2.1.6",
repo: "stable",
digest: "test"
}
],
"redis": [
{
apiVersion: "3.0.0",
name: "apm-server",
version: "1.0.0",
repo: "stable",
digest: "test"
},
{
apiVersion: "3.0.0",
name: "apm-server",
version: "0.0.9",
repo: "stable",
digest: "test"
}
]
});
case "experiment":
return Promise.resolve({
"fairwind": [
{
apiVersion: "3.0.0",
name: "fairwind",
version: "0.0.1",
repo: "experiment",
digest: "test"
},
{
apiVersion: "3.0.0",
name: "fairwind",
version: "0.0.2",
repo: "experiment",
digest: "test",
deprecated: true
}
]
});
case "bitnami":
return Promise.resolve({
"hotdog": [
{
apiVersion: "3.0.0",
name: "hotdog",
version: "1.0.1",
repo: "bitnami",
digest: "test"
},
{
apiVersion: "3.0.0",
name: "hotdog",
version: "1.0.2",
repo: "bitnami",
digest: "test",
}
],
"pretzel": [
{
apiVersion: "3.0.0",
name: "pretzel",
version: "1.0",
repo: "bitnami",
digest: "test",
},
{
apiVersion: "3.0.0",
name: "pretzel",
version: "1.0.1",
repo: "bitnami",
digest: "test"
}
]
});
default:
return Promise.resolve({});
}
return charts.get(this.repo.name) ?? {};
}
}
+44 -12
View File
@@ -31,7 +31,7 @@ describe("Helm Service tests", () => {
jest.resetAllMocks();
});
it("list charts without deprecated ones", async () => {
it("list charts with deprecated entries", async () => {
mockHelmRepoManager.mockReturnValue({
init: jest.fn(),
repositories: jest.fn().mockImplementation(async () => {
@@ -52,14 +52,16 @@ describe("Helm Service tests", () => {
name: "apm-server",
version: "2.1.7",
repo: "stable",
digest: "test"
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "apm-server",
version: "2.1.6",
repo: "stable",
digest: "test"
digest: "test",
created: "now",
}
],
"invalid-semver": [
@@ -68,28 +70,32 @@ describe("Helm Service tests", () => {
name: "weird-versioning",
version: "v4.4.0",
repo: "stable",
digest: "test"
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "weird-versioning",
version: "v4.3.0",
repo: "stable",
digest: "test"
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "weird-versioning",
version: "I am not semver",
repo: "stable",
digest: "test"
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "weird-versioning",
version: "I am not semver but more",
repo: "stable",
digest: "test"
digest: "test",
created: "now",
},
],
"redis": [
@@ -98,18 +104,40 @@ describe("Helm Service tests", () => {
name: "apm-server",
version: "1.0.0",
repo: "stable",
digest: "test"
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "apm-server",
version: "0.0.9",
repo: "stable",
digest: "test"
digest: "test",
created: "now",
}
]
},
experiment: {}
experiment: {
"fairwind": [
{
apiVersion: "3.0.0",
name: "fairwind",
version: "0.0.2",
repo: "experiment",
digest: "test",
deprecated: true,
created: "now",
},
{
apiVersion: "3.0.0",
name: "fairwind",
version: "0.0.1",
repo: "experiment",
digest: "test",
created: "now",
},
]
}
});
});
@@ -134,13 +162,15 @@ describe("Helm Service tests", () => {
version: "1.0.2",
repo: "bitnami",
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "hotdog",
version: "1.0.1",
repo: "bitnami",
digest: "test"
digest: "test",
created: "now",
},
],
"pretzel": [
@@ -150,13 +180,15 @@ describe("Helm Service tests", () => {
version: "1.0.1",
repo: "bitnami",
digest: "test",
created: "now",
},
{
apiVersion: "3.0.0",
name: "pretzel",
version: "1.0",
repo: "bitnami",
digest: "test"
digest: "test",
created: "now",
}
]
}
+52 -43
View File
@@ -20,27 +20,25 @@
*/
import fs from "fs";
import v8 from "v8";
import * as yaml from "js-yaml";
import { HelmRepo, HelmRepoManager } from "./helm-repo-manager";
import type { HelmRepo } from "./helm-repo-manager";
import logger from "../logger";
import { promiseExec } from "../promise-exec";
import { helmCli } from "./helm-cli";
import type { RepoHelmChartList } from "../../common/k8s-api/endpoints/helm-charts.api";
type CachedYaml = {
entries: RepoHelmChartList
};
import { sortCharts } from "../../common/utils";
export class HelmChartManager {
protected cache: any = {};
protected repo: HelmRepo;
static #cache = new Map<string, Buffer>();
constructor(repo: HelmRepo){
this.cache = HelmRepoManager.cache;
this.repo = repo;
private constructor(protected repo: HelmRepo) {}
static forRepo(repo: HelmRepo) {
return new this(repo);
}
public async chart(name: string) {
public async chartVersions(name: string) {
const charts = await this.charts();
return charts[name];
@@ -48,9 +46,7 @@ export class HelmChartManager {
public async charts(): Promise<RepoHelmChartList> {
try {
const cachedYaml = await this.cachedYaml();
return cachedYaml["entries"];
return await this.cachedYaml();
} catch(error) {
logger.error("HELM-CHART-MANAGER]: failed to list charts", { error });
@@ -58,48 +54,61 @@ export class HelmChartManager {
}
}
public async getReadme(name: string, version = "") {
private async executeCommand(action: string, name: string, version?: string) {
const helm = await helmCli.binaryPath();
const cmd = [`"${helm}" ${action} ${this.repo.name}/${name}`];
if(version && version != "") {
const { stdout } = await promiseExec(`"${helm}" show readme ${this.repo.name}/${name} --version ${version}`).catch((error) => { throw(error.stderr);});
if (version) {
cmd.push("--version", version);
}
return stdout;
} else {
const { stdout } = await promiseExec(`"${helm}" show readme ${this.repo.name}/${name}`).catch((error) => { throw(error.stderr);});
try {
const { stdout } = await promiseExec(cmd.join(" "));
return stdout;
} catch (error) {
throw error.stderr || error;
}
}
public async getValues(name: string, version = "") {
const helm = await helmCli.binaryPath();
if(version && version != "") {
const { stdout } = await promiseExec(`"${helm}" show values ${this.repo.name}/${name} --version ${version}`).catch((error) => { throw(error.stderr);});
return stdout;
} else {
const { stdout } = await promiseExec(`"${helm}" show values ${this.repo.name}/${name}`).catch((error) => { throw(error.stderr);});
return stdout;
}
public async getReadme(name: string, version?: string) {
return this.executeCommand("show readme", name, version);
}
protected async cachedYaml(): Promise<CachedYaml> {
if (!(this.repo.name in this.cache)) {
public async getValues(name: string, version?: string) {
return this.executeCommand("show values", name, version);
}
protected async cachedYaml(): Promise<RepoHelmChartList> {
if (!HelmChartManager.#cache.has(this.repo.name)) {
const cacheFile = await fs.promises.readFile(this.repo.cacheFilePath, "utf-8");
const data = yaml.safeLoad(cacheFile);
const { entries } = yaml.safeLoad(cacheFile) as { entries: RepoHelmChartList };
for(const key in data["entries"]) {
data["entries"][key].forEach((version: any) => {
version["repo"] = this.repo.name;
version["created"] = Date.parse(version.created).toString();
});
}
this.cache[this.repo.name] = Buffer.from(JSON.stringify(data));
/**
* Do some initial preprocessing on the data, so as to avoid needing to do it later
* 1. Set the repo name
* 2. Normalize the created date
* 3. Filter out deprecated items
*/
const normalized = Object.fromEntries(
Object.entries(entries)
.map(([name, charts]) => [
name,
sortCharts(
charts.map(chart => ({
...chart,
created: Date.parse(chart.created).toString(),
repo: this.repo.name,
})),
),
] as const)
.filter(([, charts]) => !charts.every(chart => chart.deprecated))
);
HelmChartManager.#cache.set(this.repo.name, v8.serialize(normalized));
}
return JSON.parse(this.cache[this.repo.name].toString());
return v8.deserialize(HelmChartManager.#cache.get(this.repo.name));
}
}
+6 -2
View File
@@ -50,8 +50,6 @@ export interface HelmRepo {
}
export class HelmRepoManager extends Singleton {
static cache = {}; // todo: remove implicit updates in helm-chart-manager.ts
protected repos: HelmRepo[];
protected helmEnv: HelmEnv;
protected initialized: boolean;
@@ -97,6 +95,12 @@ export class HelmRepoManager extends Singleton {
return env;
}
public async repo(name: string): Promise<HelmRepo> {
const repos = await this.repositories();
return repos.find(repo => repo.name === name);
}
public async repositories(): Promise<HelmRepo[]> {
try {
if (!this.initialized) {
+11 -82
View File
@@ -19,14 +19,11 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import semver, { SemVer } from "semver";
import type { Cluster } from "../cluster";
import logger from "../logger";
import { HelmRepoManager } from "./helm-repo-manager";
import { HelmChartManager } from "./helm-chart-manager";
import type { HelmChart, HelmChartList, RepoHelmChartList } from "../../common/k8s-api/endpoints/helm-charts.api";
import { deleteRelease, getHistory, getRelease, getValues, installChart, listReleases, rollback, upgradeRelease } from "./helm-release-manager";
import { iter, sortCompareChartVersions } from "../../common/utils";
interface GetReleaseValuesArgs {
cluster: Cluster;
@@ -42,43 +39,27 @@ class HelmService {
}
public async listCharts() {
const charts: HelmChartList = {};
const repositories = await HelmRepoManager.getInstance().repositories();
for (const repo of repositories) {
charts[repo.name] = {};
const manager = new HelmChartManager(repo);
const sortedCharts = this.sortChartsByVersion(await manager.charts());
const enabledCharts = this.excludeDeprecatedChartGroups(sortedCharts);
charts[repo.name] = enabledCharts;
}
return charts;
return Object.fromEntries(
await Promise.all(repositories.map(async repo => [repo.name, await HelmChartManager.forRepo(repo).charts()]))
);
}
public async getChart(repoName: string, chartName: string, version = "") {
const result = {
readme: "",
versions: {}
const repo = await HelmRepoManager.getInstance().repo(repoName);
const chartManager = HelmChartManager.forRepo(repo);
return {
readme: await chartManager.getReadme(chartName, version),
versions: await chartManager.chartVersions(chartName),
};
const repos = await HelmRepoManager.getInstance().repositories();
const repo = repos.find(repo => repo.name === repoName);
const chartManager = new HelmChartManager(repo);
const chart = await chartManager.chart(chartName);
result.readme = await chartManager.getReadme(chartName, version);
result.versions = chart;
return result;
}
public async getChartValues(repoName: string, chartName: string, version = "") {
const repos = await HelmRepoManager.getInstance().repositories();
const repo = repos.find(repo => repo.name === repoName);
const chartManager = new HelmChartManager(repo);
const repo = await HelmRepoManager.getInstance().repo(repoName);
return chartManager.getValues(chartName, version);
return HelmChartManager.forRepo(repo).getValues(chartName, version);
}
public async listReleases(cluster: Cluster, namespace: string = null) {
@@ -131,58 +112,6 @@ class HelmService {
return { message: output };
}
private excludeDeprecatedChartGroups(chartGroups: RepoHelmChartList) {
return Object.fromEntries(
iter.filterMap(
Object.entries(chartGroups),
([name, charts]) => {
for (const chart of charts) {
if (chart.deprecated) {
// ignore chart group if any chart is deprecated
return undefined;
}
}
return [name, charts];
}
)
);
}
private sortCharts(charts: HelmChart[]) {
interface ExtendedHelmChart extends HelmChart {
__version: SemVer;
}
const chartsWithVersion = Array.from(
iter.map(
charts,
(chart => {
const __version = semver.coerce(chart.version, { includePrerelease: true, loose: true });
if (!__version) {
logger.error(`[HELM-SERVICE]: Version from helm chart is not loosely coercable to semver.`, { name: chart.name, version: chart.version, repo: chart.repo });
}
(chart as ExtendedHelmChart).__version = __version;
return chart as ExtendedHelmChart;
})
),
);
return chartsWithVersion
.sort(sortCompareChartVersions)
.map(chart => (delete chart.__version, chart as HelmChart));
}
private sortChartsByVersion(chartGroups: RepoHelmChartList) {
return Object.fromEntries(
Object.entries(chartGroups)
.map(([name, charts]) => [name, this.sortCharts(charts)])
);
}
}
export const helmService = new HelmService();