Skip to content

Commit e6922c1

Browse files
committed
Directly query OCI repository for chart info
1 parent ac27491 commit e6922c1

File tree

5 files changed

+99
-92
lines changed

5 files changed

+99
-92
lines changed

.github/workflows/build-publish-staging-anvilops.yml

Lines changed: 0 additions & 24 deletions
This file was deleted.

Dockerfile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ ENTRYPOINT ["/tini", "--", "/nodejs/bin/node", "--experimental-strip-types"]
8484
CMD ["/app/src/index.ts"]
8585

8686
WORKDIR /app
87-
COPY --chown=65532:65532 --from=alpine/helm:3.19.0 /usr/bin/helm /usr/local/bin/helm
8887
COPY --chown=65532:65532 --from=regclient/regctl:v0.11.1-alpine /usr/local/bin/regctl /usr/local/bin/regctl
8988
COPY --chown=65532:65532 --from=swagger_build /app/dist ./public/openapi
9089
COPY --chown=65532:65532 --from=frontend_build /app/dist ./public

backend/src/lib/helm.ts

Lines changed: 83 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,106 @@
11
import { V1Pod } from "@kubernetes/client-node";
2-
import { spawn } from "child_process";
32
import { randomBytes } from "node:crypto";
4-
import { parse as yamlParse } from "yaml";
53
import type { App, Deployment, HelmConfig } from "../db/models.ts";
64
import { svcK8s } from "./cluster/kubernetes.ts";
75
import { shouldImpersonate } from "./cluster/rancher.ts";
86
import { getNamespace } from "./cluster/resources.ts";
97
import { wrapWithLogExporter } from "./cluster/resources/logs.ts";
108
import { env } from "./env.ts";
119

12-
type Dependency = {
10+
type Chart = {
1311
name: string;
1412
version: string;
15-
repository?: string;
16-
condition?: string;
17-
tags?: string[];
18-
"import-values"?: string;
19-
alias?: string;
13+
description?: string;
14+
note?: string;
15+
values: Record<string, any>;
2016
};
2117

22-
type Chart = {
23-
apiVersion: string;
18+
type ChartTagList = {
2419
name: string;
25-
version: string;
26-
kubeVersion?: string;
27-
description?: string;
28-
type?: string;
29-
keywords?: string[];
30-
home?: string;
31-
sources?: string[];
32-
dependencies?: Dependency[];
33-
maintainers?: { name: string; email: string; url: string }[];
34-
icon?: string;
35-
appVersion?: string;
36-
deprecated?: boolean;
37-
annotations?: Record<string, string>;
20+
tags: string[];
3821
};
3922

40-
const runHelm = (args: string[]) => {
41-
return new Promise((resolve, reject) => {
42-
const p = spawn("helm", args, { stdio: ["ignore", "pipe", "pipe"] });
43-
let out = "",
44-
err = "";
45-
p.stdout.on("data", (d) => (out += d));
46-
p.stderr.on("data", (d) => (err += d));
47-
p.on("close", (code) =>
48-
code === 0 ? resolve(out) : reject(new Error(err || `helm exit ${code}`)),
49-
);
50-
});
23+
export const getChartToken = async () => {
24+
return fetch(
25+
`${env.REGISTRY_PROTOCOL}://${env.REGISTRY_HOSTNAME}/v2/service/token?service=harbor-registry&scope=repository:${env.CHART_PROJECT_NAME}/charts:pull`,
26+
)
27+
.then((res) => {
28+
if (!res.ok) {
29+
console.error(res);
30+
throw new Error(res.statusText);
31+
}
32+
return res;
33+
})
34+
.then((res) => res.text())
35+
.then((res) => JSON.parse(res))
36+
.then((res) => {
37+
return res.token;
38+
});
5139
};
5240

53-
export const getChart = async (
54-
url: string,
55-
version?: string,
41+
const getChart = async (
42+
repository: string,
43+
version: string,
44+
token: string,
5645
): Promise<Chart> => {
57-
const args = ["show", "chart"];
58-
if (version) {
59-
args.push("version", version);
60-
}
61-
args.push(url);
46+
return fetch(
47+
`${env.REGISTRY_PROTOCOL}://${env.REGISTRY_HOSTNAME}/v2/${repository}/manifests/${version}`,
48+
{
49+
headers: {
50+
Authorization: `Bearer ${token}`,
51+
Accept: "application/vnd.oci.image.manifest.v1+json",
52+
},
53+
},
54+
)
55+
.then((res) => {
56+
if (!res.ok) {
57+
throw new Error(res.statusText);
58+
}
59+
return res;
60+
})
61+
.then((res) => res.text())
62+
.then((res) => JSON.parse(res))
63+
.then((res) => {
64+
const annotations = res.annotations;
65+
if ("anvilops-values" in annotations) {
66+
return {
67+
name: annotations["org.opencontainers.image.title"],
68+
version: annotations["org.opencontainers.image.version"],
69+
description: annotations["org.opencontainers.image.description"],
70+
note: annotations["anvilops-note"],
71+
values: JSON.parse(annotations["anvilops-values"]),
72+
};
73+
} else {
74+
return null;
75+
}
76+
});
77+
};
78+
79+
export const getLatestChart = async (
80+
repository: string,
81+
token: string,
82+
): Promise<Chart | null> => {
83+
const chartTagList = await fetch(
84+
`${env.REGISTRY_PROTOCOL}://${env.REGISTRY_HOSTNAME}/v2/${repository}/tags/list`,
85+
{
86+
headers: {
87+
Authorization: `Bearer ${token}`,
88+
},
89+
},
90+
)
91+
.then((res) => {
92+
if (!res.ok) {
93+
throw new Error(res.statusText);
94+
}
95+
return res;
96+
})
97+
.then((res) => res.json() as Promise<ChartTagList>);
6298

63-
const result = (await runHelm(args)) as string;
64-
const chart = (await yamlParse(result)) as Chart;
65-
return chart;
99+
return await getChart(
100+
chartTagList.name,
101+
chartTagList.tags[chartTagList.tags.length - 1],
102+
token,
103+
);
66104
};
67105

68106
export const upgrade = async (

backend/src/service/listCharts.ts

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { getOrCreate } from "../lib/cache.ts";
22
import { env } from "../lib/env.ts";
3-
import { getChart } from "../lib/helm.ts";
3+
import { getChartToken, getLatestChart } from "../lib/helm.ts";
44
import { getRepositoriesByProject } from "../lib/registry.ts";
55
import { ValidationError } from "./common/errors.ts";
66

@@ -16,28 +16,23 @@ export async function listCharts() {
1616
}
1717

1818
const listChartsFromRegistry = async () => {
19-
const repos = await getRepositoriesByProject(env.CHART_PROJECT_NAME);
19+
const [repos, token] = await Promise.all([
20+
getRepositoriesByProject(env.CHART_PROJECT_NAME),
21+
getChartToken(),
22+
]);
23+
2024
const charts = await Promise.all(
2125
repos.map(async (repo) => {
22-
const url = `oci://${env.REGISTRY_HOSTNAME}/${repo.name}`;
23-
return await getChart(url);
26+
return await getLatestChart(repo.name, token);
2427
}),
2528
);
2629

27-
if (charts.some((chart) => chart === null)) {
28-
throw new Error("Failed to get charts");
29-
}
30-
31-
return charts
32-
.filter(
33-
(chart) => chart?.annotations && "anvilops-values" in chart?.annotations,
34-
)
35-
.map((chart) => ({
36-
name: chart.name,
37-
note: chart.annotations["anvilops-note"],
38-
url: `oci://${env.REGISTRY_HOSTNAME}/${chart.name}`,
39-
urlType: "oci",
40-
version: chart.version,
41-
valueSpec: JSON.parse(chart.annotations["anvilops-values"] ?? ""),
42-
}));
30+
return charts.filter(Boolean).map((chart) => ({
31+
name: chart.name,
32+
note: chart.note,
33+
url: `oci://${env.REGISTRY_HOSTNAME}/${env.CHART_PROJECT_NAME}/${chart.name}`,
34+
urlType: "oci",
35+
version: chart.version,
36+
valueSpec: chart.values,
37+
}));
4338
};

frontend/src/components/diff/AppConfigDiff.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import type { CommonFormFields } from "@/lib/form.types";
1212
import { Cable } from "lucide-react";
1313
import { useContext } from "react";
1414
import type { App } from "../../pages/app/AppView";
15-
import { useAppConfig } from "../AppConfigProvider";
1615
import { DiffSelect } from "./DiffSelect";
1716
import { HelmConfigDiff } from "./helm/HelmConfigDiff";
1817
import { CommonWorkloadConfigDiff } from "./workload/CommonWorkloadConfigDiff";
@@ -33,7 +32,7 @@ export const AppConfigDiff = ({
3332
disabled?: boolean;
3433
}) => {
3534
const { user } = useContext(UserContext);
36-
const appConfig = useAppConfig();
35+
// const appConfig = useAppConfig();
3736
const selectedOrg = orgId
3837
? user?.orgs?.find((it) => it.id === orgId)
3938
: undefined;

0 commit comments

Comments
 (0)