@shepherdjerred/helm-types 1.3.0-dev.3866 → 1.3.0-dev.3954
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +102 -80
- package/package.json +1 -1
- package/src/chart-fetcher.ts +72 -23
- package/src/config.ts +67 -16
- package/src/type-converter-helpers.ts +1 -73
- package/src/type-converter.ts +21 -10
- package/src/types.ts +3 -0
package/dist/cli.js
CHANGED
|
@@ -21877,27 +21877,61 @@ async function runCommand(command, args) {
|
|
|
21877
21877
|
throw new Error(`Failed to spawn command "${command} ${args.join(" ")}": ${errorMessage}`, { cause: error51 });
|
|
21878
21878
|
}
|
|
21879
21879
|
}
|
|
21880
|
+
async function resolveUntarredChartDir(tempDir, fallbackName) {
|
|
21881
|
+
const fallback = `${tempDir}/${fallbackName}`;
|
|
21882
|
+
if (await Bun.file(`${fallback}/Chart.yaml`).exists()) {
|
|
21883
|
+
return fallback;
|
|
21884
|
+
}
|
|
21885
|
+
const lsOutput = await runCommand("ls", ["-1", tempDir]);
|
|
21886
|
+
for (const entry of lsOutput.split(`
|
|
21887
|
+
`).map((s) => s.trim())) {
|
|
21888
|
+
if (entry === "") {
|
|
21889
|
+
continue;
|
|
21890
|
+
}
|
|
21891
|
+
if (await Bun.file(`${tempDir}/${entry}/Chart.yaml`).exists()) {
|
|
21892
|
+
return `${tempDir}/${entry}`;
|
|
21893
|
+
}
|
|
21894
|
+
}
|
|
21895
|
+
return fallback;
|
|
21896
|
+
}
|
|
21880
21897
|
async function fetchHelmChart(chart) {
|
|
21881
21898
|
const pwd = Bun.env["PWD"] ?? process.cwd();
|
|
21882
21899
|
const tempDir = `${pwd}/temp/helm-${chart.name}`;
|
|
21883
21900
|
const repoName = `temp-repo-${chart.name}-${String(Date.now())}`;
|
|
21884
21901
|
try {
|
|
21885
21902
|
await Bun.$`mkdir -p ${tempDir}`.quiet();
|
|
21886
|
-
|
|
21887
|
-
|
|
21888
|
-
|
|
21889
|
-
|
|
21890
|
-
|
|
21891
|
-
|
|
21892
|
-
|
|
21893
|
-
|
|
21894
|
-
|
|
21895
|
-
|
|
21896
|
-
|
|
21897
|
-
|
|
21898
|
-
|
|
21899
|
-
|
|
21900
|
-
|
|
21903
|
+
let chartDir;
|
|
21904
|
+
if (chart.oci === true) {
|
|
21905
|
+
const ociRef = `oci://${chart.repoUrl}/${chart.chartName}`;
|
|
21906
|
+
console.log(` ⬇️ Pulling OCI chart ${ociRef}:${chart.version}...`);
|
|
21907
|
+
await runCommand("helm", [
|
|
21908
|
+
"pull",
|
|
21909
|
+
ociRef,
|
|
21910
|
+
"--version",
|
|
21911
|
+
chart.version,
|
|
21912
|
+
"--destination",
|
|
21913
|
+
tempDir,
|
|
21914
|
+
"--untar"
|
|
21915
|
+
]);
|
|
21916
|
+
chartDir = await resolveUntarredChartDir(tempDir, chart.name);
|
|
21917
|
+
} else {
|
|
21918
|
+
console.log(` \uD83D\uDCE6 Adding Helm repo: ${chart.repoUrl}`);
|
|
21919
|
+
await runCommand("helm", ["repo", "add", repoName, chart.repoUrl]);
|
|
21920
|
+
console.log(` \uD83D\uDD04 Updating Helm repo ${repoName}...`);
|
|
21921
|
+
await runCommand("helm", ["repo", "update", repoName]);
|
|
21922
|
+
console.log(` ⬇️ Pulling chart ${chart.chartName}:${chart.version}...`);
|
|
21923
|
+
await runCommand("helm", [
|
|
21924
|
+
"pull",
|
|
21925
|
+
`${repoName}/${chart.chartName}`,
|
|
21926
|
+
"--version",
|
|
21927
|
+
chart.version,
|
|
21928
|
+
"--destination",
|
|
21929
|
+
tempDir,
|
|
21930
|
+
"--untar"
|
|
21931
|
+
]);
|
|
21932
|
+
chartDir = `${tempDir}/${chart.chartName}`;
|
|
21933
|
+
}
|
|
21934
|
+
const valuesPath = `${chartDir}/values.yaml`;
|
|
21901
21935
|
console.log(` \uD83D\uDCD6 Reading values.yaml from ${valuesPath}`);
|
|
21902
21936
|
try {
|
|
21903
21937
|
const valuesContent = await Bun.file(valuesPath).text();
|
|
@@ -21914,8 +21948,7 @@ async function fetchHelmChart(chart) {
|
|
|
21914
21948
|
return { values: {}, schema: null, yamlComments: new Map };
|
|
21915
21949
|
}
|
|
21916
21950
|
const parseResult = HelmValueSchema.safeParse(recordParseResult.data);
|
|
21917
|
-
const
|
|
21918
|
-
const schema = await loadJSONSchema(chartPath);
|
|
21951
|
+
const schema = await loadJSONSchema(chartDir);
|
|
21919
21952
|
if (parseResult.success) {
|
|
21920
21953
|
console.log(` ✅ Zod validation successful`);
|
|
21921
21954
|
return { values: parseResult.data, schema, yamlComments };
|
|
@@ -21932,7 +21965,9 @@ async function fetchHelmChart(chart) {
|
|
|
21932
21965
|
} finally {
|
|
21933
21966
|
try {
|
|
21934
21967
|
console.log(` \uD83E\uDDF9 Cleaning up...`);
|
|
21935
|
-
|
|
21968
|
+
if (chart.oci !== true) {
|
|
21969
|
+
await runCommand("helm", ["repo", "remove", repoName]);
|
|
21970
|
+
}
|
|
21936
21971
|
await Bun.$`rm -rf ${tempDir}`.quiet();
|
|
21937
21972
|
} catch (cleanupError) {
|
|
21938
21973
|
console.warn(`Cleanup failed for ${chart.name}:`, String(cleanupError));
|
|
@@ -21941,12 +21976,40 @@ async function fetchHelmChart(chart) {
|
|
|
21941
21976
|
}
|
|
21942
21977
|
|
|
21943
21978
|
// src/config.ts
|
|
21944
|
-
var
|
|
21945
|
-
|
|
21946
|
-
|
|
21979
|
+
var K8S_WELL_KNOWN_FIELDS = {
|
|
21980
|
+
resources: {
|
|
21981
|
+
type: "{ requests?: Record<string, string | number>; limits?: Record<string, string | number> }",
|
|
21982
|
+
allowedShapes: ["object"],
|
|
21983
|
+
description: "Kubernetes container resources (standard ResourceRequirements: arbitrary resource names, string or numeric quantities)"
|
|
21984
|
+
},
|
|
21985
|
+
nodeselector: {
|
|
21986
|
+
type: "Record<string, string>",
|
|
21987
|
+
allowedShapes: ["object"],
|
|
21988
|
+
description: "Kubernetes nodeSelector (arbitrary label key/value pairs)"
|
|
21989
|
+
},
|
|
21990
|
+
tolerations: {
|
|
21991
|
+
type: "unknown[]",
|
|
21992
|
+
allowedShapes: ["array"],
|
|
21993
|
+
description: "Kubernetes tolerations (standard Toleration objects)"
|
|
21994
|
+
},
|
|
21995
|
+
affinity: {
|
|
21996
|
+
type: "Record<string, unknown>",
|
|
21997
|
+
allowedShapes: ["object"],
|
|
21998
|
+
description: "Kubernetes affinity (standard Affinity object)"
|
|
21999
|
+
}
|
|
21947
22000
|
};
|
|
21948
|
-
function
|
|
21949
|
-
|
|
22001
|
+
function getWellKnownK8sFieldType(propertyName, value) {
|
|
22002
|
+
const field = K8S_WELL_KNOWN_FIELDS[propertyName.toLowerCase()];
|
|
22003
|
+
if (!field) {
|
|
22004
|
+
return;
|
|
22005
|
+
}
|
|
22006
|
+
if (value != null) {
|
|
22007
|
+
const shape = Array.isArray(value) ? "array" : typeof value === "object" ? "object" : "primitive";
|
|
22008
|
+
if (shape === "primitive" || !field.allowedShapes.includes(shape)) {
|
|
22009
|
+
return;
|
|
22010
|
+
}
|
|
22011
|
+
}
|
|
22012
|
+
return { type: field.type, description: field.description };
|
|
21950
22013
|
}
|
|
21951
22014
|
var EXTENSIBLE_TYPE_PATTERNS = {
|
|
21952
22015
|
"argo-cd": [
|
|
@@ -21989,6 +22052,12 @@ var EXTENSIBLE_TYPE_PATTERNS = {
|
|
|
21989
22052
|
],
|
|
21990
22053
|
seaweedfs: [
|
|
21991
22054
|
"volume.dataDirs"
|
|
22055
|
+
],
|
|
22056
|
+
"dagger-helm": [
|
|
22057
|
+
"engine"
|
|
22058
|
+
],
|
|
22059
|
+
"agent-stack-k8s": [
|
|
22060
|
+
"config"
|
|
21992
22061
|
]
|
|
21993
22062
|
};
|
|
21994
22063
|
function shouldAllowArbitraryProps(keyPath, chartName, propertyName, yamlComment) {
|
|
@@ -22150,60 +22219,6 @@ function inferPrimitiveType(value, yamlComment) {
|
|
|
22150
22219
|
console.warn(`Unrecognized value type for: ${String(value)}, using 'unknown'`);
|
|
22151
22220
|
return { type: "unknown", optional: true, description: yamlComment };
|
|
22152
22221
|
}
|
|
22153
|
-
function augmentK8sResourceSpec(iface) {
|
|
22154
|
-
const hasRequests = "requests" in iface.properties;
|
|
22155
|
-
const hasLimits = "limits" in iface.properties;
|
|
22156
|
-
if (hasRequests && !hasLimits) {
|
|
22157
|
-
const requestsProp = iface.properties["requests"];
|
|
22158
|
-
if (requestsProp) {
|
|
22159
|
-
const limitsTypeName = requestsProp.type.replace("Requests", "Limits");
|
|
22160
|
-
if (requestsProp.nested) {
|
|
22161
|
-
const limitsNested = {
|
|
22162
|
-
name: limitsTypeName,
|
|
22163
|
-
properties: { ...requestsProp.nested.properties },
|
|
22164
|
-
allowArbitraryProps: requestsProp.nested.allowArbitraryProps
|
|
22165
|
-
};
|
|
22166
|
-
iface.properties["limits"] = {
|
|
22167
|
-
type: limitsTypeName,
|
|
22168
|
-
optional: true,
|
|
22169
|
-
nested: limitsNested,
|
|
22170
|
-
description: "Kubernetes resource limits (memory, cpu, etc.)"
|
|
22171
|
-
};
|
|
22172
|
-
} else {
|
|
22173
|
-
iface.properties["limits"] = {
|
|
22174
|
-
type: requestsProp.type,
|
|
22175
|
-
optional: true,
|
|
22176
|
-
description: "Kubernetes resource limits (memory, cpu, etc.)"
|
|
22177
|
-
};
|
|
22178
|
-
}
|
|
22179
|
-
}
|
|
22180
|
-
}
|
|
22181
|
-
if (hasLimits && !hasRequests) {
|
|
22182
|
-
const limitsProp = iface.properties["limits"];
|
|
22183
|
-
if (limitsProp) {
|
|
22184
|
-
const requestsTypeName = limitsProp.type.replace("Limits", "Requests");
|
|
22185
|
-
if (limitsProp.nested) {
|
|
22186
|
-
const requestsNested = {
|
|
22187
|
-
name: requestsTypeName,
|
|
22188
|
-
properties: { ...limitsProp.nested.properties },
|
|
22189
|
-
allowArbitraryProps: limitsProp.nested.allowArbitraryProps
|
|
22190
|
-
};
|
|
22191
|
-
iface.properties["requests"] = {
|
|
22192
|
-
type: requestsTypeName,
|
|
22193
|
-
optional: true,
|
|
22194
|
-
nested: requestsNested,
|
|
22195
|
-
description: "Kubernetes resource requests (memory, cpu, etc.)"
|
|
22196
|
-
};
|
|
22197
|
-
} else {
|
|
22198
|
-
iface.properties["requests"] = {
|
|
22199
|
-
type: limitsProp.type,
|
|
22200
|
-
optional: true,
|
|
22201
|
-
description: "Kubernetes resource requests (memory, cpu, etc.)"
|
|
22202
|
-
};
|
|
22203
|
-
}
|
|
22204
|
-
}
|
|
22205
|
-
}
|
|
22206
|
-
}
|
|
22207
22222
|
|
|
22208
22223
|
// src/type-converter.ts
|
|
22209
22224
|
function jsonSchemaToTypeScript(schema) {
|
|
@@ -22484,6 +22499,16 @@ function convertValueToProperty(opts) {
|
|
|
22484
22499
|
if (NullSchema.safeParse(value).success || UndefinedSchema.safeParse(value).success) {
|
|
22485
22500
|
return { type: "unknown", optional: true };
|
|
22486
22501
|
}
|
|
22502
|
+
if (propertyName != null && propertyName !== "") {
|
|
22503
|
+
const wellKnown = getWellKnownK8sFieldType(propertyName, value);
|
|
22504
|
+
if (wellKnown) {
|
|
22505
|
+
return {
|
|
22506
|
+
type: wellKnown.type,
|
|
22507
|
+
optional: true,
|
|
22508
|
+
description: yamlComment ?? wellKnown.description
|
|
22509
|
+
};
|
|
22510
|
+
}
|
|
22511
|
+
}
|
|
22487
22512
|
const arrayResult = ArraySchema.safeParse(value);
|
|
22488
22513
|
if (arrayResult.success) {
|
|
22489
22514
|
return inferArrayType(opts, arrayResult.data);
|
|
@@ -22497,9 +22522,6 @@ function convertValueToProperty(opts) {
|
|
|
22497
22522
|
keyPrefix: fullKey,
|
|
22498
22523
|
chartName
|
|
22499
22524
|
});
|
|
22500
|
-
if (propertyName != null && propertyName !== "" && isK8sResourceSpec(propertyName)) {
|
|
22501
|
-
augmentK8sResourceSpec(nestedInterface);
|
|
22502
|
-
}
|
|
22503
22525
|
return {
|
|
22504
22526
|
type: nestedTypeName,
|
|
22505
22527
|
optional: true,
|
package/package.json
CHANGED
package/src/chart-fetcher.ts
CHANGED
|
@@ -62,6 +62,32 @@ async function runCommand(command: string, args: string[]): Promise<string> {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Locate the directory `helm pull --untar` created. helm names it after the
|
|
67
|
+
* Chart.yaml `name`, which can differ from the OCI artifact path (e.g.
|
|
68
|
+
* `kueue/charts/kueue` untars to `kueue/`), so prefer the version-key fallback
|
|
69
|
+
* but fall back to scanning for the extracted Chart.yaml.
|
|
70
|
+
*/
|
|
71
|
+
async function resolveUntarredChartDir(
|
|
72
|
+
tempDir: string,
|
|
73
|
+
fallbackName: string,
|
|
74
|
+
): Promise<string> {
|
|
75
|
+
const fallback = `${tempDir}/${fallbackName}`;
|
|
76
|
+
if (await Bun.file(`${fallback}/Chart.yaml`).exists()) {
|
|
77
|
+
return fallback;
|
|
78
|
+
}
|
|
79
|
+
const lsOutput = await runCommand("ls", ["-1", tempDir]);
|
|
80
|
+
for (const entry of lsOutput.split("\n").map((s) => s.trim())) {
|
|
81
|
+
if (entry === "") {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (await Bun.file(`${tempDir}/${entry}/Chart.yaml`).exists()) {
|
|
85
|
+
return `${tempDir}/${entry}`;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return fallback;
|
|
89
|
+
}
|
|
90
|
+
|
|
65
91
|
/**
|
|
66
92
|
* Fetch a Helm chart and extract its values.yaml and optional schema
|
|
67
93
|
*/
|
|
@@ -78,28 +104,49 @@ export async function fetchHelmChart(chart: ChartInfo): Promise<{
|
|
|
78
104
|
// Ensure temp directory exists
|
|
79
105
|
await Bun.$`mkdir -p ${tempDir}`.quiet();
|
|
80
106
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
chart.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
107
|
+
let chartDir: string;
|
|
108
|
+
if (chart.oci === true) {
|
|
109
|
+
// OCI registry: pull directly, no `helm repo add` needed.
|
|
110
|
+
const ociRef = `oci://${chart.repoUrl}/${chart.chartName}`;
|
|
111
|
+
console.log(` ⬇️ Pulling OCI chart ${ociRef}:${chart.version}...`);
|
|
112
|
+
await runCommand("helm", [
|
|
113
|
+
"pull",
|
|
114
|
+
ociRef,
|
|
115
|
+
"--version",
|
|
116
|
+
chart.version,
|
|
117
|
+
"--destination",
|
|
118
|
+
tempDir,
|
|
119
|
+
"--untar",
|
|
120
|
+
]);
|
|
121
|
+
chartDir = await resolveUntarredChartDir(tempDir, chart.name);
|
|
122
|
+
} else {
|
|
123
|
+
console.log(` 📦 Adding Helm repo: ${chart.repoUrl}`);
|
|
124
|
+
// Add the helm repo
|
|
125
|
+
await runCommand("helm", ["repo", "add", repoName, chart.repoUrl]);
|
|
126
|
+
|
|
127
|
+
console.log(` 🔄 Updating Helm repo ${repoName}...`);
|
|
128
|
+
// Update ONLY the repo we just added. `helm repo update` with no args
|
|
129
|
+
// refreshes every repo in the local helm config — including unrelated
|
|
130
|
+
// stale entries (e.g. the retired public bitnami repo) whose failure
|
|
131
|
+
// would abort an otherwise-fine fetch.
|
|
132
|
+
await runCommand("helm", ["repo", "update", repoName]);
|
|
133
|
+
|
|
134
|
+
console.log(` ⬇️ Pulling chart ${chart.chartName}:${chart.version}...`);
|
|
135
|
+
// Pull the chart
|
|
136
|
+
await runCommand("helm", [
|
|
137
|
+
"pull",
|
|
138
|
+
`${repoName}/${chart.chartName}`,
|
|
139
|
+
"--version",
|
|
140
|
+
chart.version,
|
|
141
|
+
"--destination",
|
|
142
|
+
tempDir,
|
|
143
|
+
"--untar",
|
|
144
|
+
]);
|
|
145
|
+
chartDir = `${tempDir}/${chart.chartName}`;
|
|
146
|
+
}
|
|
100
147
|
|
|
101
148
|
// Read values.yaml
|
|
102
|
-
const valuesPath = `${
|
|
149
|
+
const valuesPath = `${chartDir}/values.yaml`;
|
|
103
150
|
console.log(` 📖 Reading values.yaml from ${valuesPath}`);
|
|
104
151
|
|
|
105
152
|
try {
|
|
@@ -137,8 +184,7 @@ export async function fetchHelmChart(chart: ChartInfo): Promise<{
|
|
|
137
184
|
const parseResult = HelmValueSchema.safeParse(recordParseResult.data);
|
|
138
185
|
|
|
139
186
|
// Try to load JSON schema
|
|
140
|
-
const
|
|
141
|
-
const schema = await loadJSONSchema(chartPath);
|
|
187
|
+
const schema = await loadJSONSchema(chartDir);
|
|
142
188
|
|
|
143
189
|
if (parseResult.success) {
|
|
144
190
|
console.log(` ✅ Zod validation successful`);
|
|
@@ -163,7 +209,10 @@ export async function fetchHelmChart(chart: ChartInfo): Promise<{
|
|
|
163
209
|
// Cleanup
|
|
164
210
|
try {
|
|
165
211
|
console.log(` 🧹 Cleaning up...`);
|
|
166
|
-
|
|
212
|
+
// OCI charts never added a named repo, so only remove for HTTP repos.
|
|
213
|
+
if (chart.oci !== true) {
|
|
214
|
+
await runCommand("helm", ["repo", "remove", repoName]);
|
|
215
|
+
}
|
|
167
216
|
await Bun.$`rm -rf ${tempDir}`.quiet();
|
|
168
217
|
} catch (cleanupError) {
|
|
169
218
|
console.warn(`Cleanup failed for ${chart.name}:`, String(cleanupError));
|
package/src/config.ts
CHANGED
|
@@ -1,25 +1,68 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Well-known Kubernetes
|
|
3
|
-
*
|
|
2
|
+
* Well-known Kubernetes fields whose shape is defined by the Kubernetes API,
|
|
3
|
+
* not by whatever subset a chart's values.yaml happens to set as defaults.
|
|
4
|
+
*
|
|
5
|
+
* Inferring these from defaults produces types that are too narrow — e.g. a
|
|
6
|
+
* chart defaulting `resources: {requests: {cpu: 0.2}}` would otherwise forbid
|
|
7
|
+
* setting a memory request at all. When a property matches one of these names
|
|
8
|
+
* AND its default value has a compatible shape, the canonical permissive type
|
|
9
|
+
* is emitted instead of a defaults-derived interface.
|
|
4
10
|
*/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
const K8S_WELL_KNOWN_FIELDS: Record<
|
|
12
|
+
string,
|
|
13
|
+
{ type: string; allowedShapes: ("object" | "array")[]; description: string }
|
|
14
|
+
> = {
|
|
15
|
+
resources: {
|
|
16
|
+
type: "{ requests?: Record<string, string | number>; limits?: Record<string, string | number> }",
|
|
17
|
+
// RBAC rules also use a key named `resources`, but as an array of strings —
|
|
18
|
+
// the object guard keeps those out.
|
|
19
|
+
allowedShapes: ["object"],
|
|
20
|
+
description:
|
|
21
|
+
"Kubernetes container resources (standard ResourceRequirements: arbitrary resource names, string or numeric quantities)",
|
|
22
|
+
},
|
|
23
|
+
nodeselector: {
|
|
24
|
+
type: "Record<string, string>",
|
|
25
|
+
allowedShapes: ["object"],
|
|
26
|
+
description: "Kubernetes nodeSelector (arbitrary label key/value pairs)",
|
|
27
|
+
},
|
|
28
|
+
tolerations: {
|
|
29
|
+
type: "unknown[]",
|
|
30
|
+
allowedShapes: ["array"],
|
|
31
|
+
description: "Kubernetes tolerations (standard Toleration objects)",
|
|
32
|
+
},
|
|
33
|
+
affinity: {
|
|
34
|
+
type: "Record<string, unknown>",
|
|
35
|
+
allowedShapes: ["object"],
|
|
36
|
+
description: "Kubernetes affinity (standard Affinity object)",
|
|
37
|
+
},
|
|
14
38
|
};
|
|
15
39
|
|
|
16
40
|
/**
|
|
17
|
-
*
|
|
41
|
+
* Return the canonical type for a well-known Kubernetes field, or undefined if
|
|
42
|
+
* the property name doesn't match or the default value's shape is incompatible
|
|
43
|
+
* (e.g. an RBAC `resources: ["secrets"]` array, which must NOT become
|
|
44
|
+
* ResourceRequirements).
|
|
18
45
|
*/
|
|
19
|
-
export function
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
46
|
+
export function getWellKnownK8sFieldType(
|
|
47
|
+
propertyName: string,
|
|
48
|
+
value: unknown,
|
|
49
|
+
): { type: string; description: string } | undefined {
|
|
50
|
+
const field = K8S_WELL_KNOWN_FIELDS[propertyName.toLowerCase()];
|
|
51
|
+
if (!field) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
// A null/undefined default carries no shape signal — trust the name.
|
|
55
|
+
if (value != null) {
|
|
56
|
+
const shape: "object" | "array" | "primitive" = Array.isArray(value)
|
|
57
|
+
? "array"
|
|
58
|
+
: typeof value === "object"
|
|
59
|
+
? "object"
|
|
60
|
+
: "primitive";
|
|
61
|
+
if (shape === "primitive" || !field.allowedShapes.includes(shape)) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { type: field.type, description: field.description };
|
|
23
66
|
}
|
|
24
67
|
|
|
25
68
|
/**
|
|
@@ -72,6 +115,14 @@ export const EXTENSIBLE_TYPE_PATTERNS: Record<string, string[]> = {
|
|
|
72
115
|
seaweedfs: [
|
|
73
116
|
"volume.dataDirs", // dataDirs elements support size, storageClass when type is persistentVolumeClaim
|
|
74
117
|
],
|
|
118
|
+
// OCI charts that document config keys only as commented-out examples in
|
|
119
|
+
// values.yaml, so inference from active defaults misses valid keys.
|
|
120
|
+
"dagger-helm": [
|
|
121
|
+
"engine", // engine.port / engine.configJson / engine.config are commented-out chart examples
|
|
122
|
+
],
|
|
123
|
+
"agent-stack-k8s": [
|
|
124
|
+
"config", // config.queue / max-in-flight / empty-job-grace-period / default-checkout-params are valid but not defaulted
|
|
125
|
+
],
|
|
75
126
|
};
|
|
76
127
|
|
|
77
128
|
/**
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
JSONSchemaProperty,
|
|
3
|
-
TypeScriptInterface,
|
|
4
|
-
TypeProperty,
|
|
5
|
-
} from "./types.ts";
|
|
1
|
+
import type { JSONSchemaProperty, TypeProperty } from "./types.ts";
|
|
6
2
|
import {
|
|
7
3
|
StringSchema,
|
|
8
4
|
ActualNumberSchema,
|
|
@@ -110,71 +106,3 @@ export function inferPrimitiveType(
|
|
|
110
106
|
);
|
|
111
107
|
return { type: "unknown", optional: true, description: yamlComment };
|
|
112
108
|
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Augment a Kubernetes resource spec interface with both requests and limits.
|
|
116
|
-
* If only one is present, copy its type structure to the other.
|
|
117
|
-
*/
|
|
118
|
-
export function augmentK8sResourceSpec(iface: TypeScriptInterface): void {
|
|
119
|
-
const hasRequests = "requests" in iface.properties;
|
|
120
|
-
const hasLimits = "limits" in iface.properties;
|
|
121
|
-
|
|
122
|
-
// If we have requests but not limits, add limits with the same structure
|
|
123
|
-
if (hasRequests && !hasLimits) {
|
|
124
|
-
const requestsProp = iface.properties["requests"];
|
|
125
|
-
if (requestsProp) {
|
|
126
|
-
// Create limits property with the same type but different name for the nested interface
|
|
127
|
-
const limitsTypeName = requestsProp.type.replace("Requests", "Limits");
|
|
128
|
-
|
|
129
|
-
// If there's a nested interface, create a copy for limits
|
|
130
|
-
if (requestsProp.nested) {
|
|
131
|
-
const limitsNested: TypeScriptInterface = {
|
|
132
|
-
name: limitsTypeName,
|
|
133
|
-
properties: { ...requestsProp.nested.properties },
|
|
134
|
-
allowArbitraryProps: requestsProp.nested.allowArbitraryProps,
|
|
135
|
-
};
|
|
136
|
-
iface.properties["limits"] = {
|
|
137
|
-
type: limitsTypeName,
|
|
138
|
-
optional: true,
|
|
139
|
-
nested: limitsNested,
|
|
140
|
-
description: "Kubernetes resource limits (memory, cpu, etc.)",
|
|
141
|
-
};
|
|
142
|
-
} else {
|
|
143
|
-
// No nested interface, just copy the type
|
|
144
|
-
iface.properties["limits"] = {
|
|
145
|
-
type: requestsProp.type,
|
|
146
|
-
optional: true,
|
|
147
|
-
description: "Kubernetes resource limits (memory, cpu, etc.)",
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// If we have limits but not requests, add requests with the same structure
|
|
154
|
-
if (hasLimits && !hasRequests) {
|
|
155
|
-
const limitsProp = iface.properties["limits"];
|
|
156
|
-
if (limitsProp) {
|
|
157
|
-
const requestsTypeName = limitsProp.type.replace("Limits", "Requests");
|
|
158
|
-
|
|
159
|
-
if (limitsProp.nested) {
|
|
160
|
-
const requestsNested: TypeScriptInterface = {
|
|
161
|
-
name: requestsTypeName,
|
|
162
|
-
properties: { ...limitsProp.nested.properties },
|
|
163
|
-
allowArbitraryProps: limitsProp.nested.allowArbitraryProps,
|
|
164
|
-
};
|
|
165
|
-
iface.properties["requests"] = {
|
|
166
|
-
type: requestsTypeName,
|
|
167
|
-
optional: true,
|
|
168
|
-
nested: requestsNested,
|
|
169
|
-
description: "Kubernetes resource requests (memory, cpu, etc.)",
|
|
170
|
-
};
|
|
171
|
-
} else {
|
|
172
|
-
iface.properties["requests"] = {
|
|
173
|
-
type: limitsProp.type,
|
|
174
|
-
optional: true,
|
|
175
|
-
description: "Kubernetes resource requests (memory, cpu, etc.)",
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
package/src/type-converter.ts
CHANGED
|
@@ -14,7 +14,10 @@ import {
|
|
|
14
14
|
ActualNumberSchema,
|
|
15
15
|
StringBooleanSchema,
|
|
16
16
|
} from "./schemas.ts";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
shouldAllowArbitraryProps,
|
|
19
|
+
getWellKnownK8sFieldType,
|
|
20
|
+
} from "./config.ts";
|
|
18
21
|
import {
|
|
19
22
|
sanitizePropertyName,
|
|
20
23
|
sanitizeTypeName,
|
|
@@ -24,7 +27,6 @@ import type { PropertyConversionContext } from "./type-converter-helpers.ts";
|
|
|
24
27
|
import {
|
|
25
28
|
mergeDescriptions,
|
|
26
29
|
inferPrimitiveType,
|
|
27
|
-
augmentK8sResourceSpec,
|
|
28
30
|
} from "./type-converter-helpers.ts";
|
|
29
31
|
|
|
30
32
|
/**
|
|
@@ -470,6 +472,23 @@ function convertValueToProperty(opts: PropertyConversionContext): TypeProperty {
|
|
|
470
472
|
return { type: "unknown", optional: true };
|
|
471
473
|
}
|
|
472
474
|
|
|
475
|
+
// Well-known Kubernetes fields (resources, nodeSelector, tolerations,
|
|
476
|
+
// affinity) have an API-defined shape. Inferring them from a chart's default
|
|
477
|
+
// subset produces types that are too narrow (e.g. resources.requests that
|
|
478
|
+
// only allow cpu). Emit the canonical permissive type instead — but only
|
|
479
|
+
// when the default value's shape is compatible, so RBAC `resources:
|
|
480
|
+
// ["secrets"]` arrays stay arrays.
|
|
481
|
+
if (propertyName != null && propertyName !== "") {
|
|
482
|
+
const wellKnown = getWellKnownK8sFieldType(propertyName, value);
|
|
483
|
+
if (wellKnown) {
|
|
484
|
+
return {
|
|
485
|
+
type: wellKnown.type,
|
|
486
|
+
optional: true,
|
|
487
|
+
description: yamlComment ?? wellKnown.description,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
473
492
|
// Check for array (before coercion checks)
|
|
474
493
|
const arrayResult = ArraySchema.safeParse(value);
|
|
475
494
|
if (arrayResult.success) {
|
|
@@ -487,14 +506,6 @@ function convertValueToProperty(opts: PropertyConversionContext): TypeProperty {
|
|
|
487
506
|
chartName,
|
|
488
507
|
});
|
|
489
508
|
|
|
490
|
-
if (
|
|
491
|
-
propertyName != null &&
|
|
492
|
-
propertyName !== "" &&
|
|
493
|
-
isK8sResourceSpec(propertyName)
|
|
494
|
-
) {
|
|
495
|
-
augmentK8sResourceSpec(nestedInterface);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
509
|
return {
|
|
499
510
|
type: nestedTypeName,
|
|
500
511
|
optional: true,
|
package/src/types.ts
CHANGED
|
@@ -5,6 +5,9 @@ export type ChartInfo = {
|
|
|
5
5
|
repoUrl: string;
|
|
6
6
|
version: string;
|
|
7
7
|
chartName: string; // The actual chart name (may differ from versions.ts key)
|
|
8
|
+
// When true, the chart is served from an OCI registry: fetch via
|
|
9
|
+
// `helm pull oci://<repoUrl>/<chartName>` instead of `helm repo add` + pull.
|
|
10
|
+
oci?: boolean;
|
|
8
11
|
};
|
|
9
12
|
|
|
10
13
|
export type JSONSchemaProperty = {
|