@olivervea/pipelines 2.0.0-beta.7 → 2.0.0-beta.9
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/README.md +36 -31
- package/manifest.schema.json +3 -3
- package/package.json +1 -1
- package/src/build/build.ts +9 -7
- package/src/build/hashes.ts +31 -2
- package/src/build/project.ts +7 -0
- package/src/core/brands.ts +2 -0
- package/src/core/config.ts +1 -1
- package/src/core/handlers.ts +14 -8
- package/src/core/origin.ts +20 -0
- package/src/core/steps.ts +28 -20
- package/src/describe/describe.ts +37 -32
- package/src/describe/manifest-schema.ts +3 -3
- package/src/dev/cli.ts +5 -5
- package/src/harness/main.ts +5 -6
- package/src/index.ts +5 -3
- package/src/library/compose-deploy.ts +42 -39
- package/src/library/github-source.ts +45 -41
- package/src/library/helm-deploy-ssh.ts +114 -60
- package/src/library/helm-deploy.ts +73 -70
- package/src/library/image-build.ts +113 -169
- package/src/library/index.ts +5 -8
- package/src/library/models.ts +2 -1
- package/src/testing/index.ts +6 -12
- package/src/util/handler-hash.ts +3 -3
- package/src/library/npm.ts +0 -33
- package/src/library/script.ts +0 -85
|
@@ -3,22 +3,21 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Ported from v1's olve-lib.sh (olve_ssh_host, olve_image_import, olve_helm_deploy) and
|
|
5
5
|
// deploy-beta.sh (rollout wait + health loop); the footguns are kept as comments below.
|
|
6
|
-
import { existsSync, statSync } from "node:fs";
|
|
6
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import type { Ctx } from "../ctx/types.ts";
|
|
9
9
|
import type { FileRef } from "../core/models.ts";
|
|
10
10
|
import type { Secret } from "../core/secrets.ts";
|
|
11
11
|
import type { Configurable } from "../core/config.ts";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
12
|
+
import { resolveConfigRefs } from "../core/config-refs.ts";
|
|
13
|
+
import { implement, type Implementation } from "../core/handlers.ts";
|
|
14
|
+
import { step, type LiteralName, type Step } from "../core/steps.ts";
|
|
14
15
|
import type { Simplify } from "../core/types.ts";
|
|
15
16
|
import { httpHealthCheck, type HttpHealthCheckOptions } from "./http-health.ts";
|
|
16
17
|
import { BuiltImage, HelmRelease, PushedImage, Source } from "./models.ts";
|
|
17
18
|
import { shq, sshSession } from "./ssh.ts";
|
|
18
19
|
import { helmDeployOverSshTarget, type HelmDeploySshTargetDef, type HelmTargetDeployInput } from "./helm-deploy.ts";
|
|
19
20
|
|
|
20
|
-
export { shq } from "./ssh.ts";
|
|
21
|
-
|
|
22
21
|
/** Alpine: `apk add openssh-client` at run time gives ssh/scp (the health check runs in the harness). */
|
|
23
22
|
export const HELM_DEPLOY_IMAGE = "alpine:3.22";
|
|
24
23
|
|
|
@@ -71,7 +70,7 @@ export interface HelmDeployOverSshDef<
|
|
|
71
70
|
/**
|
|
72
71
|
* The images to deploy: step input key → the chart's values prefix for that image. Each input is a
|
|
73
72
|
* `BuiltImage` (or `PushedImage` with `fromRegistry`). Default `{ image: "image" }`; several images
|
|
74
|
-
* need a mapper, e.g. `{ server: "image", web: "web.image" }`.
|
|
73
|
+
* need a mapper, e.g. `{ server: "image", web: "web.image" }`. `{}` deploys the chart alone.
|
|
75
74
|
*/
|
|
76
75
|
images?: Im & { readonly source?: never };
|
|
77
76
|
/**
|
|
@@ -87,10 +86,20 @@ export interface HelmDeployOverSshDef<
|
|
|
87
86
|
* next to tarball `images`.
|
|
88
87
|
*/
|
|
89
88
|
imageRefs?: Rf & { readonly source?: never };
|
|
90
|
-
/** Rollouts to wait for, in order: `deploy/<name>` for a bare name. Default: the release name. */
|
|
89
|
+
/** Rollouts to wait for, in order: `deploy/<name>` for a bare name. Default: the release name, or none without images or with helm's wait. */
|
|
91
90
|
deployments?: readonly string[];
|
|
92
|
-
/** Per rollout (`kubectl rollout status --timeout`). Default 120. */
|
|
91
|
+
/** Per rollout (`kubectl rollout status --timeout`), or for helm's own wait. Default 120. */
|
|
93
92
|
rolloutTimeoutSeconds?: number;
|
|
93
|
+
/**
|
|
94
|
+
* Pull the dependencies `Chart.yaml` lists (`charts/` is usually not committed) into the chart on the
|
|
95
|
+
* host before installing: `helm pull <name> --repo <repository> --version <version>`, or the `oci://`
|
|
96
|
+
* reference. Default false.
|
|
97
|
+
*/
|
|
98
|
+
dependencies?: boolean;
|
|
99
|
+
/** `helm upgrade --wait`: helm waits for all of the release's resources to be ready. Default false. */
|
|
100
|
+
helmWait?: boolean;
|
|
101
|
+
/** `helm upgrade --atomic`: wait like `helmWait`, and roll back when the upgrade fails. Default false. */
|
|
102
|
+
atomic?: boolean;
|
|
94
103
|
/** After the rollouts, GET this URL until it returns 2xx (`httpHealthCheck`: 5 × 5 s unless given), or fail the step. */
|
|
95
104
|
healthUrl?: Configurable<string> | ({ url: Configurable<string> } & Pick<HttpHealthCheckOptions, "attempts" | "delaySeconds">);
|
|
96
105
|
timeoutSeconds?: number;
|
|
@@ -144,15 +153,45 @@ export function helmFlags(o: HelmFlagOptions): string[] {
|
|
|
144
153
|
}
|
|
145
154
|
|
|
146
155
|
/** Remote `helm upgrade --install`, run from inside the copied chart directory. */
|
|
147
|
-
export function helmUpgradeCommand(o: HelmFlagOptions & { remoteChart: string; release: string; namespace: string }): string {
|
|
156
|
+
export function helmUpgradeCommand(o: HelmFlagOptions & { remoteChart: string; release: string; namespace: string; wait?: HelmWait }): string {
|
|
148
157
|
const flags = helmFlags(o);
|
|
158
|
+
const wait = o.wait ? [o.wait.atomic ? "--atomic" : "--wait", `--timeout=${o.wait.timeoutSeconds}s`] : [];
|
|
149
159
|
return [
|
|
150
160
|
`cd ${shq(o.remoteChart)}`,
|
|
151
|
-
["helm", "upgrade", "--install", o.release, ".", "-n", o.namespace, ...flags].map(shq).join(" "),
|
|
161
|
+
["helm", "upgrade", "--install", o.release, ".", "-n", o.namespace, ...flags, ...wait].map(shq).join(" "),
|
|
152
162
|
`cd / && rm -rf ${shq(o.remoteChart)}`,
|
|
153
163
|
].join(" && ");
|
|
154
164
|
}
|
|
155
165
|
|
|
166
|
+
/** helm's own wait (`--wait`, or `--atomic` to roll back on failure). */
|
|
167
|
+
export interface HelmWait {
|
|
168
|
+
atomic: boolean;
|
|
169
|
+
timeoutSeconds: number;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** A dependency from `Chart.yaml`. */
|
|
173
|
+
export interface ChartDependency {
|
|
174
|
+
name: string;
|
|
175
|
+
version: string;
|
|
176
|
+
repository: string;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The dependencies `<chartDir>/Chart.yaml` lists, checked to be pullable (an http(s) or oci repository). */
|
|
180
|
+
export function chartDependencies(chartDir: string): ChartDependency[] {
|
|
181
|
+
const chart = Bun.YAML.parse(readFileSync(join(chartDir, "Chart.yaml"), "utf8")) as { dependencies?: Partial<ChartDependency>[] } | null;
|
|
182
|
+
return (chart?.dependencies ?? []).map((d) => {
|
|
183
|
+
if (!d.name || !d.version || !d.repository || !/^(https?|oci):\/\//.test(d.repository))
|
|
184
|
+
throw new Error(`chart dependency ${JSON.stringify(d)}: needs a name, a version and an http(s) or oci repository`);
|
|
185
|
+
return { name: d.name, version: d.version, repository: d.repository };
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The remote `helm pull` of a dependency into `<remoteChart>/charts`. */
|
|
190
|
+
export function helmPullCommand(dep: ChartDependency, remoteChart: string): string {
|
|
191
|
+
const ref = dep.repository.startsWith("oci://") ? [`${dep.repository.replace(/\/+$/, "")}/${dep.name}`] : [dep.name, "--repo", dep.repository];
|
|
192
|
+
return ["helm", "pull", ...ref, "--version", dep.version, "--destination", `${remoteChart}/charts`].map(shq).join(" ");
|
|
193
|
+
}
|
|
194
|
+
|
|
156
195
|
/** One image to get onto the host before the upgrade. */
|
|
157
196
|
export interface HostImage {
|
|
158
197
|
repository: string;
|
|
@@ -178,6 +217,8 @@ export async function deployOverSsh(
|
|
|
178
217
|
flags: HelmFlagOptions;
|
|
179
218
|
deployments: readonly string[];
|
|
180
219
|
rolloutTimeoutSeconds: number;
|
|
220
|
+
dependencies?: readonly ChartDependency[];
|
|
221
|
+
wait?: HelmWait;
|
|
181
222
|
},
|
|
182
223
|
): Promise<void> {
|
|
183
224
|
const ssh = await sshSession(ctx, { host: o.host, key: o.key });
|
|
@@ -196,7 +237,12 @@ export async function deployOverSsh(
|
|
|
196
237
|
ctx.log("helm upgrade", { release: o.release, namespace: o.namespace, host: o.host });
|
|
197
238
|
await ssh.run(`rm -rf ${shq(remoteChart)}`);
|
|
198
239
|
await ssh.copy(o.chartDir, remoteChart);
|
|
199
|
-
await ssh.run(
|
|
240
|
+
if (o.dependencies?.length) await ssh.run(`mkdir -p ${shq(`${remoteChart}/charts`)}`);
|
|
241
|
+
for (const dep of o.dependencies ?? []) {
|
|
242
|
+
ctx.log("pulling chart dependency", { chart: `${dep.name}@${dep.version}`, repository: dep.repository });
|
|
243
|
+
await ssh.run(helmPullCommand(dep, remoteChart));
|
|
244
|
+
}
|
|
245
|
+
await ssh.run(helmUpgradeCommand({ ...o.flags, remoteChart, release: o.release, namespace: o.namespace, ...(o.wait ? { wait: o.wait } : {}) }));
|
|
200
246
|
for (const d of o.deployments) {
|
|
201
247
|
const target = d.includes("/") ? d : `deploy/${d}`;
|
|
202
248
|
ctx.log("waiting for rollout", { deployment: target });
|
|
@@ -221,26 +267,26 @@ export function helmDeployOverSsh<
|
|
|
221
267
|
const K extends string,
|
|
222
268
|
const Im extends Record<string, string> = DefaultImages,
|
|
223
269
|
const Rf extends Record<string, string> = {},
|
|
224
|
-
>(def: HelmDeploySshTargetDef<N, K, Im, Rf>): Step<N, HelmTargetDeployInput<Im, Rf>, typeof HelmRelease, K
|
|
270
|
+
>(def: HelmDeploySshTargetDef<N, K, Im, Rf>): Implementation<Step<N, HelmTargetDeployInput<Im, Rf>, typeof HelmRelease, K>>;
|
|
225
271
|
export function helmDeployOverSsh<
|
|
226
272
|
const N extends string,
|
|
227
273
|
const K extends string,
|
|
228
274
|
const Im extends Record<string, string> = DefaultImages,
|
|
229
275
|
const R extends boolean = false,
|
|
230
276
|
const Rf extends Record<string, string> = {},
|
|
231
|
-
>(def: HelmDeployOverSshDef<N, K, Im, R, Rf>): Step<N, HelmDeployOverSshInput<Im, R, Rf>, typeof HelmRelease, K
|
|
277
|
+
>(def: HelmDeployOverSshDef<N, K, Im, R, Rf>): Implementation<Step<N, HelmDeployOverSshInput<Im, R, Rf>, typeof HelmRelease, K>>;
|
|
232
278
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
233
|
-
export function helmDeployOverSsh(def: any):
|
|
279
|
+
export function helmDeployOverSsh(def: any): any {
|
|
234
280
|
return "target" in def ? helmDeployOverSshTarget(def) : helmDeployOverSshLiteral(def);
|
|
235
281
|
}
|
|
236
282
|
|
|
237
283
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
238
|
-
function helmDeployOverSshLiteral(def: HelmDeployOverSshDef<string, string, any, boolean, any>):
|
|
284
|
+
function helmDeployOverSshLiteral(def: HelmDeployOverSshDef<string, string, any, boolean, any>): Implementation {
|
|
239
285
|
const images: Record<string, string> = def.images ?? { image: "image" };
|
|
240
286
|
const imageEntries = Object.entries(images);
|
|
241
287
|
const fromRegistry = def.fromRegistry === true;
|
|
242
288
|
const rolloutTimeout = def.rolloutTimeoutSeconds ?? DEFAULT_ROLLOUT_TIMEOUT_SECONDS;
|
|
243
|
-
|
|
289
|
+
const wait: HelmWait | undefined = def.atomic || def.helmWait ? { atomic: def.atomic === true, timeoutSeconds: rolloutTimeout } : undefined;
|
|
244
290
|
if ("source" in images) throw new Error(`helmDeployOverSsh '${def.name}': 'source' can't be an image input`);
|
|
245
291
|
if (def.deployments?.length === 0) throw new Error(`helmDeployOverSsh '${def.name}': deployments must not be empty`);
|
|
246
292
|
const imageRefs: Record<string, string> = def.imageRefs ?? {};
|
|
@@ -256,49 +302,57 @@ function helmDeployOverSshLiteral(def: HelmDeployOverSshDef<string, string, any,
|
|
|
256
302
|
const isDefaultImages = imageEntries.length === 1 && images.image === "image";
|
|
257
303
|
const configurable = { host: def.host, release: def.release, namespace: def.namespace, values: def.values ?? [], set: def.set ?? {}, healthUrl: def.healthUrl };
|
|
258
304
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}),
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const hostImages: HostImage[] = [];
|
|
285
|
-
const flags: HelmImageFlags[] = [];
|
|
286
|
-
for (const [key, prefix] of imageEntries) {
|
|
287
|
-
const image = input[key]!;
|
|
288
|
-
hostImages.push({ repository: image.repository, tag: image.tag, ...(fromRegistry ? {} : { tar: await ctx.files.fetch(image.tar!) }) });
|
|
289
|
-
flags.push({ prefix, repository: image.repository, tag: image.tag, pullPolicy: fromRegistry ? "IfNotPresent" : "Never" });
|
|
290
|
-
}
|
|
291
|
-
const refs: HelmImageRefFlags[] = refEntries.map(([key, prefix]) => ({ prefix, repository: input[key]!.repository, tag: input[key]!.tag, digest: input[key]!.digest }));
|
|
292
|
-
await deployOverSsh(ctx, {
|
|
293
|
-
host: d.host, key: ctx.secrets[def.sshKey.name]!, chartDir, release: d.release, namespace: d.namespace, hostImages,
|
|
294
|
-
flags: { values: d.values, set: d.set, images: flags, imageRefs: refs },
|
|
295
|
-
deployments: def.deployments ?? [d.release], rolloutTimeoutSeconds: rolloutTimeout,
|
|
296
|
-
});
|
|
297
|
-
const health = typeof d.healthUrl === "string" ? { url: d.healthUrl } : d.healthUrl;
|
|
298
|
-
if (health) await httpHealthCheck(ctx, health.url, { attempts: health.attempts, delaySeconds: health.delaySeconds });
|
|
299
|
-
return { release: d.release, namespace: d.namespace, image: `${flags[0]!.repository}:${flags[0]!.tag}` };
|
|
300
|
-
},
|
|
305
|
+
const contract = step({
|
|
306
|
+
name: def.name,
|
|
307
|
+
description: def.description,
|
|
308
|
+
kind: "helmDeployOverSsh",
|
|
309
|
+
image: HELM_DEPLOY_IMAGE,
|
|
310
|
+
input,
|
|
311
|
+
output: HelmRelease,
|
|
312
|
+
secrets: [def.sshKey],
|
|
313
|
+
timeoutSeconds: def.timeoutSeconds,
|
|
314
|
+
settings: {
|
|
315
|
+
host: def.host,
|
|
316
|
+
sshKey: def.sshKey.name,
|
|
317
|
+
release: def.release,
|
|
318
|
+
namespace: def.namespace,
|
|
319
|
+
chart: def.chart,
|
|
320
|
+
values: configurable.values,
|
|
321
|
+
set: configurable.set,
|
|
322
|
+
...(isDefaultImages ? {} : { images: { ...images } }),
|
|
323
|
+
...(fromRegistry ? { fromRegistry: true } : {}),
|
|
324
|
+
...(refEntries.length > 0 ? { imageRefs: { ...imageRefs } } : {}),
|
|
325
|
+
...(def.deployments ? { deployments: [...def.deployments] } : {}),
|
|
326
|
+
...(def.rolloutTimeoutSeconds !== undefined ? { rolloutTimeoutSeconds: def.rolloutTimeoutSeconds } : {}),
|
|
327
|
+
...(def.dependencies ? { dependencies: true } : {}),
|
|
328
|
+
...(def.atomic ? { atomic: true } : def.helmWait ? { helmWait: true } : {}),
|
|
329
|
+
...(def.healthUrl !== undefined ? { healthUrl: def.healthUrl } : {}),
|
|
301
330
|
},
|
|
302
|
-
|
|
303
|
-
)
|
|
331
|
+
});
|
|
332
|
+
return implement(contract, async (stepInput, ctx: Ctx) => {
|
|
333
|
+
const input = stepInput as unknown as Record<string, { repository: string; tag: string; digest: string; tar?: FileRef }> & { source: { tree: FileRef } };
|
|
334
|
+
const d = resolveConfigRefs(configurable, ctx.config ?? {}) as {
|
|
335
|
+
host: string; release: string; namespace: string; values: string[]; set: Record<string, string>;
|
|
336
|
+
healthUrl?: string | ({ url: string } & Pick<HttpHealthCheckOptions, "attempts" | "delaySeconds">);
|
|
337
|
+
};
|
|
338
|
+
const chartDir = chartDirectory(await ctx.files.fetch(input.source.tree), def.chart);
|
|
339
|
+
const hostImages: HostImage[] = [];
|
|
340
|
+
const flags: HelmImageFlags[] = [];
|
|
341
|
+
for (const [key, prefix] of imageEntries) {
|
|
342
|
+
const image = input[key]!;
|
|
343
|
+
hostImages.push({ repository: image.repository, tag: image.tag, ...(fromRegistry ? {} : { tar: await ctx.files.fetch(image.tar!) }) });
|
|
344
|
+
flags.push({ prefix, repository: image.repository, tag: image.tag, pullPolicy: fromRegistry ? "IfNotPresent" : "Never" });
|
|
345
|
+
}
|
|
346
|
+
const refs: HelmImageRefFlags[] = refEntries.map(([key, prefix]) => ({ prefix, repository: input[key]!.repository, tag: input[key]!.tag, digest: input[key]!.digest }));
|
|
347
|
+
await deployOverSsh(ctx, {
|
|
348
|
+
host: d.host, key: ctx.secrets[def.sshKey.name]!, chartDir, release: d.release, namespace: d.namespace, hostImages,
|
|
349
|
+
flags: { values: d.values, set: d.set, images: flags, imageRefs: refs },
|
|
350
|
+
deployments: def.deployments ?? (imageEntries.length && !wait ? [d.release] : []), rolloutTimeoutSeconds: rolloutTimeout,
|
|
351
|
+
...(def.dependencies ? { dependencies: chartDependencies(chartDir) } : {}),
|
|
352
|
+
...(wait ? { wait } : {}),
|
|
353
|
+
});
|
|
354
|
+
const health = typeof d.healthUrl === "string" ? { url: d.healthUrl } : d.healthUrl;
|
|
355
|
+
if (health) await httpHealthCheck(ctx, health.url, { attempts: health.attempts, delaySeconds: health.delaySeconds });
|
|
356
|
+
return { release: d.release, namespace: d.namespace, ...(flags[0] ? { image: `${flags[0].repository}:${flags[0].tag}` } : {}) };
|
|
357
|
+
}) as never;
|
|
304
358
|
}
|
|
@@ -6,12 +6,13 @@ import { join } from "node:path";
|
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import type { Ctx } from "../ctx/types.ts";
|
|
8
8
|
import type { Configurable } from "../core/config.ts";
|
|
9
|
-
import {
|
|
9
|
+
import { resolveConfigRefs } from "../core/config-refs.ts";
|
|
10
|
+
import { implement, type Implementation } from "../core/handlers.ts";
|
|
10
11
|
import type { FileRef } from "../core/models.ts";
|
|
11
12
|
import { optional } from "../core/models.ts";
|
|
12
13
|
import type { Secret } from "../core/secrets.ts";
|
|
13
14
|
import { secretRef } from "../core/config.ts";
|
|
14
|
-
import {
|
|
15
|
+
import { step, type AnyStep, type LiteralName, type Step } from "../core/steps.ts";
|
|
15
16
|
import type { Simplify } from "../core/types.ts";
|
|
16
17
|
import { serviceUrl } from "./checks.ts";
|
|
17
18
|
import { chartDirectory, deployOverSsh, DEFAULT_ROLLOUT_TIMEOUT_SECONDS, HELM_DEPLOY_IMAGE, helmFlags, type HelmImageFlags, type HelmImageRefFlags } from "./helm-deploy-ssh.ts";
|
|
@@ -117,7 +118,8 @@ export interface HelmTargetDeployDef<N extends string, Im extends Record<string,
|
|
|
117
118
|
inlineValues?: readonly Readonly<Record<string, unknown>>[];
|
|
118
119
|
/**
|
|
119
120
|
* The images to deploy: step input key → the chart's values prefix. Each input is a `PushedImage`;
|
|
120
|
-
* the step sets `<prefix>.repository`, `.tag`, `.digest` and `.pullPolicy=IfNotPresent`.
|
|
121
|
+
* the step sets `<prefix>.repository`, `.tag`, `.digest` and `.pullPolicy=IfNotPresent`. `{}` deploys
|
|
122
|
+
* the chart alone.
|
|
121
123
|
*/
|
|
122
124
|
images?: Im & { readonly source?: never };
|
|
123
125
|
/**
|
|
@@ -144,7 +146,7 @@ export interface HelmDeploySshTargetDef<N extends string, K extends string, Im e
|
|
|
144
146
|
target: Configurable<SshTargetValue>;
|
|
145
147
|
/** Private key for the target's host. */
|
|
146
148
|
sshKey: Secret<K>;
|
|
147
|
-
/** Rollouts to wait for (`deploy/<name>` for a bare name). Default: the release name. */
|
|
149
|
+
/** Rollouts to wait for (`deploy/<name>` for a bare name). Default: the release name, or none without images. */
|
|
148
150
|
deployments?: readonly string[];
|
|
149
151
|
}
|
|
150
152
|
|
|
@@ -157,12 +159,12 @@ export function helmDeployInCluster<
|
|
|
157
159
|
const N extends string,
|
|
158
160
|
const Im extends Record<string, string> = DefaultImages,
|
|
159
161
|
const Rf extends Record<string, string> = {},
|
|
160
|
-
>(def: HelmDeployInClusterDef<N, Im, Rf>): Step<N, HelmTargetDeployInput<Im, Rf>, typeof HelmRelease, never
|
|
162
|
+
>(def: HelmDeployInClusterDef<N, Im, Rf>): Implementation<Step<N, HelmTargetDeployInput<Im, Rf>, typeof HelmRelease, never>> {
|
|
161
163
|
return targetDeploy("helmDeployInCluster", def as unknown as AnyTargetDef, undefined) as never;
|
|
162
164
|
}
|
|
163
165
|
|
|
164
166
|
/** The `target` form of `steps.helmDeployOverSsh`: an ssh target from config, registry images by digest. */
|
|
165
|
-
export function helmDeployOverSshTarget(def: HelmDeploySshTargetDef<string, string, Record<string, string>, Record<string, string>>):
|
|
167
|
+
export function helmDeployOverSshTarget(def: HelmDeploySshTargetDef<string, string, Record<string, string>, Record<string, string>>): Implementation {
|
|
166
168
|
return targetDeploy("helmDeployOverSsh", def as unknown as AnyTargetDef, def.sshKey, def.deployments) as never;
|
|
167
169
|
}
|
|
168
170
|
|
|
@@ -179,13 +181,12 @@ interface AnyTargetDef {
|
|
|
179
181
|
timeoutSeconds?: number;
|
|
180
182
|
}
|
|
181
183
|
|
|
182
|
-
function targetDeploy(kind:
|
|
184
|
+
function targetDeploy(kind: "helmDeployInCluster" | "helmDeployOverSsh", def: AnyTargetDef, sshKey: Secret | undefined, deployments?: readonly string[]): Implementation {
|
|
183
185
|
const images: Record<string, string> = { ...(def.images ?? { image: "image" }) };
|
|
184
186
|
const imageEntries = Object.entries(images);
|
|
185
187
|
const imageRefs: Record<string, string> = { ...(def.imageRefs ?? {}) };
|
|
186
188
|
const refEntries = Object.entries(imageRefs);
|
|
187
189
|
const rolloutTimeout = def.rolloutTimeoutSeconds ?? DEFAULT_ROLLOUT_TIMEOUT_SECONDS;
|
|
188
|
-
if (imageEntries.length === 0) throw new Error(`${kind} '${def.name}': images must not be empty`);
|
|
189
190
|
if ("source" in images || "source" in imageRefs) throw new Error(`${kind} '${def.name}': 'source' can't be an image input`);
|
|
190
191
|
for (const [key] of refEntries) if (key in images) throw new Error(`${kind} '${def.name}': '${key}' is both in images and imageRefs`);
|
|
191
192
|
if (deployments?.length === 0) throw new Error(`${kind} '${def.name}': deployments must not be empty`);
|
|
@@ -198,71 +199,73 @@ function targetDeploy(kind: StepKind, def: AnyTargetDef, sshKey: Secret | undefi
|
|
|
198
199
|
};
|
|
199
200
|
const configurable = { target: def.target, inlineValues: def.inlineValues ?? [] };
|
|
200
201
|
|
|
201
|
-
|
|
202
|
+
const contract = step({
|
|
203
|
+
name: def.name,
|
|
204
|
+
description: def.description,
|
|
202
205
|
kind,
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
+
image: inCluster ? HELM_IMAGE : HELM_DEPLOY_IMAGE,
|
|
207
|
+
input,
|
|
208
|
+
output: HelmRelease,
|
|
209
|
+
secrets: sshKey ? [sshKey] : [],
|
|
210
|
+
timeoutSeconds: def.timeoutSeconds,
|
|
211
|
+
...(inCluster ? { clusterAccess: true } : {}),
|
|
212
|
+
settings: {
|
|
213
|
+
target: def.target,
|
|
214
|
+
...(sshKey ? { sshKey: sshKey.name } : {}),
|
|
215
|
+
chart: def.chart,
|
|
216
|
+
...(def.inlineValues?.length ? { inlineValues: def.inlineValues } : {}),
|
|
217
|
+
images: { ...images },
|
|
218
|
+
...(refEntries.length > 0 ? { imageRefs: { ...imageRefs } } : {}),
|
|
219
|
+
...(deployments ? { deployments: [...deployments] } : {}),
|
|
220
|
+
...(def.rolloutTimeoutSeconds !== undefined ? { rolloutTimeoutSeconds: def.rolloutTimeoutSeconds } : {}),
|
|
221
|
+
...(def.healthPath !== undefined ? { healthPath: def.healthPath } : {}),
|
|
206
222
|
},
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
...(refEntries.length > 0 ? { imageRefs: { ...imageRefs } } : {}),
|
|
215
|
-
...(deployments ? { deployments: [...deployments] } : {}),
|
|
216
|
-
...(def.rolloutTimeoutSeconds !== undefined ? { rolloutTimeoutSeconds: def.rolloutTimeoutSeconds } : {}),
|
|
217
|
-
...(def.healthPath !== undefined ? { healthPath: def.healthPath } : {}),
|
|
218
|
-
}),
|
|
219
|
-
async run(input: Record<string, { repository: string; tag: string; digest: string } | undefined> & { source: { tree: FileRef } }, ctx: Ctx) {
|
|
220
|
-
const { target, inlineValues } = resolveConfigRefs(configurable, ctx.config ?? {}) as { target: HelmTargetValue; inlineValues: Record<string, unknown>[] };
|
|
221
|
-
const want = inCluster ? "inCluster" : "ssh";
|
|
222
|
-
if (target?.kind !== want) throw new Error(`${kind} '${def.name}': the target is kind '${String(target?.kind)}', not '${want}'`);
|
|
223
|
-
const chartDir = chartDirectory(await ctx.files.fetch(input.source.tree), def.chart);
|
|
223
|
+
} as never) as AnyStep;
|
|
224
|
+
return implement(contract, async (stepInput, ctx: Ctx) => {
|
|
225
|
+
const input = stepInput as unknown as Record<string, { repository: string; tag: string; digest: string } | undefined> & { source: { tree: FileRef } };
|
|
226
|
+
const { target, inlineValues } = resolveConfigRefs(configurable, ctx.config ?? {}) as { target: HelmTargetValue; inlineValues: Record<string, unknown>[] };
|
|
227
|
+
const want = inCluster ? "inCluster" : "ssh";
|
|
228
|
+
if (target?.kind !== want) throw new Error(`${kind} '${def.name}': the target is kind '${String(target?.kind)}', not '${want}'`);
|
|
229
|
+
const chartDir = chartDirectory(await ctx.files.fetch(input.source.tree), def.chart);
|
|
224
230
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
231
|
+
const generated = inlineValues.map((values, i) => {
|
|
232
|
+
const file = `olve-values-${i}.json`;
|
|
233
|
+
writeFileSync(join(chartDir, file), JSON.stringify(values));
|
|
234
|
+
return file;
|
|
235
|
+
});
|
|
236
|
+
const flags: HelmImageFlags[] = imageEntries.map(([key, prefix]) => {
|
|
237
|
+
const image = input[key]!;
|
|
238
|
+
return { prefix, repository: image.repository, tag: image.tag, digest: image.digest, pullPolicy: "IfNotPresent" };
|
|
239
|
+
});
|
|
240
|
+
const refs: HelmImageRefFlags[] = refEntries.flatMap(([key, prefix]) => {
|
|
241
|
+
const image = input[key];
|
|
242
|
+
return image ? [{ prefix, repository: image.repository, tag: image.tag, digest: image.digest }] : [];
|
|
243
|
+
});
|
|
244
|
+
const helm = { values: [...target.values, ...generated], set: target.set, images: flags, imageRefs: refs };
|
|
245
|
+
const { release, namespace } = target;
|
|
240
246
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
247
|
+
if (target.kind === "ssh") {
|
|
248
|
+
const key = ctx.secrets[sshKey!.name];
|
|
249
|
+
if (!key) throw new Error(`secret ${sshKey!.name} (sshKey) is not set`);
|
|
250
|
+
await deployOverSsh(ctx, {
|
|
251
|
+
host: target.host, key, chartDir, release, namespace,
|
|
252
|
+
hostImages: flags.map((f) => ({ repository: f.repository, tag: f.tag, digest: f.digest })),
|
|
253
|
+
flags: helm, deployments: deployments ?? (imageEntries.length ? [release] : []), rolloutTimeoutSeconds: rolloutTimeout,
|
|
254
|
+
});
|
|
255
|
+
} else {
|
|
256
|
+
if (!existsSync(SERVICE_ACCOUNT_TOKEN))
|
|
257
|
+
throw new Error(`no ServiceAccount token in the step pod: the instance grants no step ServiceAccount (Kubernetes:StepServiceAccount), so an inCluster target can't be deployed`);
|
|
258
|
+
const home = join(ctx.tmpDir, "helm-home");
|
|
259
|
+
ctx.log("helm upgrade", { release, namespace, images: flags.map((f) => `${f.repository}@${f.digest}`).join(" ") });
|
|
260
|
+
await ctx.exec(["helm", "upgrade", "--install", release, ".", "-n", namespace, ...helmFlags(helm), "--wait", `--timeout=${rolloutTimeout}s`], {
|
|
261
|
+
cwd: chartDir,
|
|
262
|
+
env: { HOME: home, HELM_CACHE_HOME: join(home, "cache"), HELM_CONFIG_HOME: join(home, "config"), HELM_DATA_HOME: join(home, "data") },
|
|
263
|
+
});
|
|
264
|
+
}
|
|
259
265
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
},
|
|
266
|
-
configurable,
|
|
267
|
-
) as never;
|
|
266
|
+
const url = (target.url ?? serviceUrl({ release, namespace })).replace(/\/+$/, "");
|
|
267
|
+
if (def.healthPath !== undefined) await httpHealthCheck(ctx, `${url}${def.healthPath}`, { attempts: 12, delaySeconds: 5 });
|
|
268
|
+
const main = flags[0];
|
|
269
|
+
return { release, namespace, ...(main ? { image: `${main.repository}:${main.tag}@${main.digest}` } : {}), ...(target.url ? { url } : {}) } as never;
|
|
270
|
+
}) as never;
|
|
268
271
|
}
|