@olivervea/pipelines 2.0.0-beta.17 → 2.0.0-beta.19

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.
Files changed (53) hide show
  1. package/README.md +2 -0
  2. package/manifest.schema.json +22 -0
  3. package/package.json +4 -2
  4. package/src/assets.d.ts +2 -2
  5. package/src/build/build.ts +111 -43
  6. package/src/build/cli.ts +28 -21
  7. package/src/build/elf.ts +8 -7
  8. package/src/build/hashes.ts +61 -28
  9. package/src/build/project.ts +69 -30
  10. package/src/builder/pipeline.ts +288 -102
  11. package/src/builder/refs.ts +12 -6
  12. package/src/core/brands.ts +6 -6
  13. package/src/core/config-refs.ts +46 -20
  14. package/src/core/config.ts +110 -33
  15. package/src/core/external.ts +30 -12
  16. package/src/core/handlers.ts +54 -14
  17. package/src/core/models.ts +29 -14
  18. package/src/core/origin.ts +7 -4
  19. package/src/core/secrets.ts +2 -2
  20. package/src/core/steps.ts +39 -23
  21. package/src/core/types.ts +5 -1
  22. package/src/core/webhooks.ts +21 -7
  23. package/src/ctx/files.ts +111 -40
  24. package/src/ctx/index.ts +75 -34
  25. package/src/ctx/sh.ts +104 -44
  26. package/src/ctx/types.ts +4 -4
  27. package/src/describe/describe.ts +558 -194
  28. package/src/describe/manifest-schema.ts +217 -79
  29. package/src/describe/mapper-source.ts +249 -104
  30. package/src/describe/record.ts +67 -37
  31. package/src/dev/cli.ts +64 -34
  32. package/src/harness/env.ts +33 -27
  33. package/src/harness/execute.ts +55 -29
  34. package/src/harness/index.ts +21 -8
  35. package/src/harness/main.ts +197 -74
  36. package/src/harness/result.ts +38 -22
  37. package/src/harness/validate.ts +62 -19
  38. package/src/index.ts +123 -14
  39. package/src/library/checks.ts +11 -6
  40. package/src/library/compose-deploy.ts +40 -23
  41. package/src/library/github-source.ts +69 -39
  42. package/src/library/helm-deploy-ssh.ts +213 -76
  43. package/src/library/helm-deploy.ts +263 -85
  44. package/src/library/http-health.ts +7 -5
  45. package/src/library/image-build.ts +293 -117
  46. package/src/library/index.ts +47 -15
  47. package/src/library/models.ts +17 -9
  48. package/src/library/ssh.ts +31 -14
  49. package/src/testing/index.ts +63 -24
  50. package/src/util/canonical.ts +7 -4
  51. package/src/util/handler-hash.ts +15 -5
  52. package/src/util/hash.ts +5 -5
  53. package/src/util/tar.ts +74 -37
@@ -1,28 +1,36 @@
1
1
  // steps.imageBuild — build a container image from a `Source` with kaniko: to an image tarball that
2
2
  // travels as a file ref (a deploy imports it on the host), or pushed to a registry (`push: true`).
3
3
  // Ported from v1's olve-lib.sh (olve_kaniko_build + build.sh's layer cache); footguns kept below.
4
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
5
- import { join } from "node:path";
6
- import type { Ctx, RunInfo } from "../ctx/types.ts";
7
- import type { FileRef } from "../core/models.ts";
8
- import type { Secret } from "../core/secrets.ts";
9
- import { isConfigRef, type ConfigRef, type Configurable } from "../core/config.ts";
10
- import { isLiteral, resolveConfigRefs } from "../core/config-refs.ts";
11
- import { implement, type Implementation } from "../core/handlers.ts";
12
- import { step, type LiteralName, type Step } from "../core/steps.ts";
13
- import { sha256File } from "../util/hash.ts";
14
- import { dirEntries, writeTar } from "../util/tar.ts";
15
- import { BuiltImage, PushedImage, Source } from "./models.ts";
4
+ import {
5
+ existsSync,
6
+ mkdirSync,
7
+ mkdtempSync,
8
+ readFileSync,
9
+ rmSync,
10
+ statSync,
11
+ writeFileSync,
12
+ } from 'node:fs';
13
+ import { join } from 'node:path';
14
+ import type { Ctx, RunInfo } from '../ctx/types.ts';
15
+ import type { FileRef } from '../core/models.ts';
16
+ import type { Secret } from '../core/secrets.ts';
17
+ import { isConfigRef, type ConfigRef, type Configurable } from '../core/config.ts';
18
+ import { isLiteral, resolveConfigRefs } from '../core/config-refs.ts';
19
+ import { implement, type Implementation } from '../core/handlers.ts';
20
+ import { step, type LiteralName, type Step } from '../core/steps.ts';
21
+ import { sha256File } from '../util/hash.ts';
22
+ import { dirEntries, writeTar } from '../util/tar.ts';
23
+ import { BuiltImage, PushedImage, Source } from './models.ts';
16
24
 
17
25
  /** kaniko's debug image (it has a busybox shell for debugging; the executor itself needs none). */
18
- export const KANIKO_IMAGE = "gcr.io/kaniko-project/executor:debug";
26
+ export const KANIKO_IMAGE = 'gcr.io/kaniko-project/executor:debug';
19
27
 
20
28
  /**
21
29
  * Footgun: the build context must live under /kaniko, NOT /workspace (or another plain directory in
22
30
  * the container): kaniko wipes the filesystem root between the stages of a multi-stage build, which
23
31
  * breaks a later stage's COPY from the context. /kaniko survives stage transitions.
24
32
  */
25
- export const KANIKO_CONTEXT = "/kaniko/build-context";
33
+ export const KANIKO_CONTEXT = '/kaniko/build-context';
26
34
 
27
35
  const input = { source: Source };
28
36
  export type ImageBuildInput = typeof input;
@@ -84,7 +92,10 @@ export interface ImageBuildDef<N extends string, K extends string = never> {
84
92
  }
85
93
 
86
94
  /** Tarball mode (default): output `BuiltImage` with the image tar as a file. */
87
- export interface ImageBuildTarballDef<N extends string, K extends string = never> extends ImageBuildDef<N, K> {
95
+ export interface ImageBuildTarballDef<
96
+ N extends string,
97
+ K extends string = never,
98
+ > extends ImageBuildDef<N, K> {
88
99
  push?: false;
89
100
  contentTag?: never;
90
101
  }
@@ -93,7 +104,10 @@ export interface ImageBuildTarballDef<N extends string, K extends string = never
93
104
  * Registry mode: push to `repository` and output `PushedImage`, for images too big to travel as a
94
105
  * tarball (the step's scratch space is ~1 GiB).
95
106
  */
96
- export interface ImageBuildPushDef<N extends string, K extends string = never> extends ImageBuildDef<N, K> {
107
+ export interface ImageBuildPushDef<
108
+ N extends string,
109
+ K extends string = never,
110
+ > extends ImageBuildDef<N, K> {
97
111
  push: true;
98
112
  /**
99
113
  * Tag the image `src-<hash>`, the hash of the build context's content, and skip the build when the
@@ -110,14 +124,22 @@ export const CACHE_PROBE_TIMEOUT_MS = 5000;
110
124
 
111
125
  /** The registry host of an image reference or mirror prefix (`registry.ovea.pro/a/b` → `registry.ovea.pro`). */
112
126
  export function registryHost(ref: string): string {
113
- return ref.split("/")[0]!;
127
+ return ref.split('/')[0]!;
114
128
  }
115
129
 
116
130
  /** Whether a registry answers its API root: 200, or 401 (it wants a token). Never throws. */
117
- export async function registryUp(http: Ctx["http"], host: string, signal?: AbortSignal, timeoutMs = CACHE_PROBE_TIMEOUT_MS, insecure = false): Promise<boolean> {
131
+ export async function registryUp(
132
+ http: Ctx['http'],
133
+ host: string,
134
+ signal?: AbortSignal,
135
+ timeoutMs = CACHE_PROBE_TIMEOUT_MS,
136
+ insecure = false,
137
+ ): Promise<boolean> {
118
138
  try {
119
139
  const timeout = AbortSignal.timeout(timeoutMs);
120
- const res = await http.fetch(`${insecure ? "http" : "https"}://${host}/v2/`, { signal: signal ? AbortSignal.any([signal, timeout]) : timeout });
140
+ const res = await http.fetch(`${insecure ? 'http' : 'https'}://${host}/v2/`, {
141
+ signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
142
+ });
121
143
  await res.body?.cancel().catch(() => {});
122
144
  return res.status === 200 || res.status === 401;
123
145
  } catch {
@@ -127,12 +149,28 @@ export async function registryUp(http: Ctx["http"], host: string, signal?: Abort
127
149
 
128
150
  /** The registry map without the mirrors that don't answer, so a mirror outage (GC, maintenance) never blocks a build. */
129
151
  export async function reachableMirrors(
130
- ctx: Pick<Ctx, "http" | "log" | "signal">,
152
+ ctx: Pick<Ctx, 'http' | 'log' | 'signal'>,
131
153
  registryMap: Record<string, string>,
132
154
  insecureHosts: readonly string[] = [],
133
155
  ): Promise<Record<string, string>> {
134
156
  const hosts = new Set(Object.values(registryMap).map(registryHost));
135
- const up = new Map(await Promise.all([...hosts].map(async (h) => [h, await registryUp(ctx.http, h, ctx.signal, undefined, insecureHosts.includes(h))] as const)));
157
+ const up = new Map(
158
+ await Promise.all(
159
+ [...hosts].map(
160
+ async (h) =>
161
+ [
162
+ h,
163
+ await registryUp(
164
+ ctx.http,
165
+ h,
166
+ ctx.signal,
167
+ undefined,
168
+ insecureHosts.includes(h),
169
+ ),
170
+ ] as const,
171
+ ),
172
+ ),
173
+ );
136
174
  const reachable: Record<string, string> = {};
137
175
  for (const [from, to] of Object.entries(registryMap)) {
138
176
  if (up.get(registryHost(to))) reachable[from] = to;
@@ -152,33 +190,38 @@ export const DEFAULT_STALL_TIMEOUT_SECONDS = 300;
152
190
  * must not reuse the tag: deploys import it with `pullPolicy=Never`, so an unchanged tag would leave
153
191
  * the old pods running.
154
192
  */
155
- export function imageTag(run: Pick<RunInfo, "version" | "attempt">): string {
193
+ export function imageTag(run: Pick<RunInfo, 'version' | 'attempt'>): string {
156
194
  const tag = run.attempt > 1 ? `${run.version}-attempt${run.attempt}` : run.version;
157
- if (!/^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/.test(tag)) throw new Error(`'${tag}' (from the run's version stamp) is not a valid image tag`);
195
+ if (!/^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/.test(tag))
196
+ throw new Error(`'${tag}' (from the run's version stamp) is not a valid image tag`);
158
197
  return tag;
159
198
  }
160
199
 
161
200
  /** The name a container runtime gives an image after loading it (`x` → `docker.io/library/x`). */
162
201
  export function qualifyRepository(repository: string): string {
163
- const first = repository.split("/")[0]!;
164
- if (!repository.includes("/")) return `docker.io/library/${repository}`;
165
- if (first.includes(".") || first.includes(":") || first === "localhost") return repository;
202
+ const first = repository.split('/')[0]!;
203
+ if (!repository.includes('/')) return `docker.io/library/${repository}`;
204
+ if (first.includes('.') || first.includes(':') || first === 'localhost')
205
+ return repository;
166
206
  return `docker.io/${repository}`;
167
207
  }
168
208
 
169
209
  /** A relative path inside the source tree (no `..`, not absolute). */
170
210
  function checkRelative(what: string, path: string): string {
171
- const clean = path.replace(/^\.\/+/, "").replace(/\/+$/, "");
172
- if (clean.startsWith("/") || clean.split("/").includes("..")) throw new Error(`imageBuild ${what} '${path}' must be a relative path inside the source tree`);
173
- return clean === "." ? "" : clean;
211
+ const clean = path.replace(/^\.\/+/, '').replace(/\/+$/, '');
212
+ if (clean.startsWith('/') || clean.split('/').includes('..'))
213
+ throw new Error(
214
+ `imageBuild ${what} '${path}' must be a relative path inside the source tree`,
215
+ );
216
+ return clean === '.' ? '' : clean;
174
217
  }
175
218
 
176
219
  /** The `src-<12 hex>` tag of a directory: the sha256 of its deterministic tar (as `ctx.output.dir` writes it). */
177
220
  export function contentTagOf(dir: string, tmpDir: string): string {
178
221
  mkdirSync(tmpDir, { recursive: true });
179
- const work = mkdtempSync(join(tmpDir, "content-tag-"));
222
+ const work = mkdtempSync(join(tmpDir, 'content-tag-'));
180
223
  try {
181
- const tar = join(work, "context.tar");
224
+ const tar = join(work, 'context.tar');
182
225
  writeTar(tar, dirEntries(dir));
183
226
  return `src-${sha256File(tar).slice(0, 12)}`;
184
227
  } finally {
@@ -187,11 +230,11 @@ export function contentTagOf(dir: string, tmpDir: string): string {
187
230
  }
188
231
 
189
232
  const MANIFEST_TYPES = [
190
- "application/vnd.oci.image.index.v1+json",
191
- "application/vnd.oci.image.manifest.v1+json",
192
- "application/vnd.docker.distribution.manifest.list.v2+json",
193
- "application/vnd.docker.distribution.manifest.v2+json",
194
- ].join(", ");
233
+ 'application/vnd.oci.image.index.v1+json',
234
+ 'application/vnd.oci.image.manifest.v1+json',
235
+ 'application/vnd.docker.distribution.manifest.list.v2+json',
236
+ 'application/vnd.docker.distribution.manifest.v2+json',
237
+ ].join(', ');
195
238
 
196
239
  /** Options of a registry API call: login (`username`/`password`) and plain HTTP. */
197
240
  export interface RegistryAccess {
@@ -200,13 +243,18 @@ export interface RegistryAccess {
200
243
  insecure?: boolean;
201
244
  }
202
245
 
203
- const basic = (username: string, password: string) => `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
246
+ const basic = (username: string, password: string) =>
247
+ `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
204
248
 
205
249
  /** `Bearer realm="…",service="…",scope="…"` → its parameters. */
206
- export function parseChallenge(header: string): { scheme: string; params: Record<string, string> } {
250
+ export function parseChallenge(header: string): {
251
+ scheme: string;
252
+ params: Record<string, string>;
253
+ } {
207
254
  const scheme = header.trim().split(/\s+/, 1)[0]!.toLowerCase();
208
255
  const params: Record<string, string> = {};
209
- for (const m of header.slice(scheme.length).matchAll(/([A-Za-z]+)="([^"]*)"/g)) params[m[1]!.toLowerCase()] = m[2]!;
256
+ for (const m of header.slice(scheme.length).matchAll(/([A-Za-z]+)="([^"]*)"/g))
257
+ params[m[1]!.toLowerCase()] = m[2]!;
210
258
  return { scheme, params };
211
259
  }
212
260
 
@@ -214,92 +262,157 @@ export function parseChallenge(header: string): { scheme: string; params: Record
214
262
  * `HEAD`/`GET` against a registry API path, answering its auth challenge: Basic (a `registry:2` with
215
263
  * htpasswd) or a Bearer token from the realm (ghcr.io, Docker Hub; anonymous for public images).
216
264
  */
217
- async function registryFetch(http: Ctx["http"], host: string, path: string, init: RequestInit, access: RegistryAccess): Promise<Response> {
218
- const url = `${access.insecure ? "http" : "https"}://${host}${path}`;
265
+ async function registryFetch(
266
+ http: Ctx['http'],
267
+ host: string,
268
+ path: string,
269
+ init: RequestInit,
270
+ access: RegistryAccess,
271
+ ): Promise<Response> {
272
+ const url = `${access.insecure ? 'http' : 'https'}://${host}${path}`;
219
273
  const res = await http.fetch(url, init);
220
274
  if (res.status !== 401) return res;
221
275
  await res.body?.cancel().catch(() => {});
222
- const { scheme, params } = parseChallenge(res.headers.get("www-authenticate") ?? "");
223
- const login = access.username !== undefined && access.password !== undefined ? basic(access.username, access.password) : undefined;
276
+ const { scheme, params } = parseChallenge(res.headers.get('www-authenticate') ?? '');
277
+ const login =
278
+ access.username !== undefined && access.password !== undefined
279
+ ? basic(access.username, access.password)
280
+ : undefined;
224
281
  let authorization: string;
225
- if (scheme === "basic") {
282
+ if (scheme === 'basic') {
226
283
  if (!login) return new Response(null, { status: 401 });
227
284
  authorization = login;
228
- } else if (scheme === "bearer" && params.realm) {
285
+ } else if (scheme === 'bearer' && params.realm) {
229
286
  const q = new URLSearchParams();
230
- if (params.service) q.set("service", params.service);
231
- if (params.scope) q.set("scope", params.scope);
232
- const tokenRes = await http.fetch(`${params.realm}?${q}`, { headers: login ? { Authorization: login } : {} });
287
+ if (params.service) q.set('service', params.service);
288
+ if (params.scope) q.set('scope', params.scope);
289
+ const tokenRes = await http.fetch(`${params.realm}?${q}`, {
290
+ headers: login ? { Authorization: login } : {},
291
+ });
233
292
  if (!tokenRes.ok) {
234
293
  await tokenRes.body?.cancel().catch(() => {});
235
- return new Response(null, { status: tokenRes.status === 404 ? 401 : tokenRes.status });
294
+ return new Response(null, {
295
+ status: tokenRes.status === 404 ? 401 : tokenRes.status,
296
+ });
236
297
  }
237
298
  const body = (await tokenRes.json()) as { token?: string; access_token?: string };
238
299
  const token = body.token ?? body.access_token;
239
- if (!token) throw new Error(`registry token endpoint ${params.realm} returned no token`);
300
+ if (!token)
301
+ throw new Error(`registry token endpoint ${params.realm} returned no token`);
240
302
  authorization = `Bearer ${token}`;
241
303
  } else {
242
304
  return new Response(null, { status: 401 });
243
305
  }
244
306
  const headers = new Headers(init.headers);
245
- headers.set("Authorization", authorization);
307
+ headers.set('Authorization', authorization);
246
308
  return http.fetch(url, { ...init, headers });
247
309
  }
248
310
 
249
311
  /** The digest of `repository:tag` in its registry (`HEAD …/manifests/<tag>`), or undefined if it has none. */
250
- export async function registryDigest(http: Ctx["http"], repository: string, tag: string, access: RegistryAccess = {}): Promise<string | undefined> {
251
- const [host, ...path] = repository.split("/");
252
- const res = await registryFetch(http, host!, `/v2/${path.join("/")}/manifests/${tag}`, { method: "HEAD", headers: { Accept: MANIFEST_TYPES } }, access);
312
+ export async function registryDigest(
313
+ http: Ctx['http'],
314
+ repository: string,
315
+ tag: string,
316
+ access: RegistryAccess = {},
317
+ ): Promise<string | undefined> {
318
+ const [host, ...path] = repository.split('/');
319
+ const res = await registryFetch(
320
+ http,
321
+ host!,
322
+ `/v2/${path.join('/')}/manifests/${tag}`,
323
+ { method: 'HEAD', headers: { Accept: MANIFEST_TYPES } },
324
+ access,
325
+ );
253
326
  await res.body?.cancel().catch(() => {});
254
327
  if (res.status === 404) return undefined;
255
328
  // An anonymous lookup of a repository that doesn't exist yet (or is private) is denied, not 404.
256
- if ((res.status === 401 || res.status === 403) && access.password === undefined) return undefined;
257
- if (!res.ok) throw new Error(`registry lookup of ${repository}:${tag} answered HTTP ${res.status}`);
258
- const digest = res.headers.get("docker-content-digest") ?? "";
329
+ if ((res.status === 401 || res.status === 403) && access.password === undefined)
330
+ return undefined;
331
+ if (!res.ok)
332
+ throw new Error(
333
+ `registry lookup of ${repository}:${tag} answered HTTP ${res.status}`,
334
+ );
335
+ const digest = res.headers.get('docker-content-digest') ?? '';
259
336
  return /^sha256:[0-9a-f]{64}$/.test(digest) ? digest : undefined;
260
337
  }
261
338
 
262
339
  /** kaniko reads registry logins from `$DOCKER_CONFIG/config.json` (`/kaniko/.docker` in its image). */
263
- export const KANIKO_DOCKER_CONFIG = "/kaniko/.docker";
340
+ export const KANIKO_DOCKER_CONFIG = '/kaniko/.docker';
264
341
 
265
342
  /** A docker `config.json` that logs in to `host`. */
266
- export function dockerConfigJson(host: string, username: string, password: string): string {
267
- return JSON.stringify({ auths: { [host]: { auth: Buffer.from(`${username}:${password}`).toString("base64") } } });
343
+ export function dockerConfigJson(
344
+ host: string,
345
+ username: string,
346
+ password: string,
347
+ ): string {
348
+ return JSON.stringify({
349
+ auths: {
350
+ [host]: { auth: Buffer.from(`${username}:${password}`).toString('base64') },
351
+ },
352
+ });
268
353
  }
269
354
 
270
355
  /** Adds the login for `host` to `<dir>/config.json` (owner-only), keeping what is there (kaniko's credHelpers). */
271
- export function writeDockerConfig(dir: string, host: string, username: string, password: string): void {
356
+ export function writeDockerConfig(
357
+ dir: string,
358
+ host: string,
359
+ username: string,
360
+ password: string,
361
+ ): void {
272
362
  mkdirSync(dir, { recursive: true });
273
- const file = join(dir, "config.json");
363
+ const file = join(dir, 'config.json');
274
364
  let config: { auths?: Record<string, unknown> } = {};
275
365
  try {
276
- config = JSON.parse(readFileSync(file, "utf8"));
366
+ config = JSON.parse(readFileSync(file, 'utf8'));
277
367
  } catch {
278
368
  // absent or unreadable: start fresh
279
369
  }
280
- const login = JSON.parse(dockerConfigJson(host, username, password)) as { auths: Record<string, unknown> };
281
- writeFileSync(file, JSON.stringify({ ...config, auths: { ...config.auths, ...login.auths } }), { mode: 0o600 });
370
+ const login = JSON.parse(dockerConfigJson(host, username, password)) as {
371
+ auths: Record<string, unknown>;
372
+ };
373
+ writeFileSync(
374
+ file,
375
+ JSON.stringify({ ...config, auths: { ...config.auths, ...login.auths } }),
376
+ { mode: 0o600 },
377
+ );
282
378
  }
283
379
 
284
380
  /**
285
381
  * Builds an image with kaniko from `<context>/Dockerfile` in the source tree (default: the root). Default: into an image tarball, output `BuiltImage { repository, tag, digest, tar }`.
286
382
  * With `push: true`: pushed to `repository`, output `PushedImage { repository, tag, digest }`.
287
383
  */
288
- export function imageBuild<const N extends string, const K extends string = never>(def: ImageBuildTarballDef<N, K>): Implementation<Step<N, ImageBuildInput, typeof BuiltImage, K>>;
289
- export function imageBuild<const N extends string, const K extends string = never>(def: ImageBuildPushDef<N, K>): Implementation<Step<N, ImageBuildInput, typeof PushedImage, K>>;
290
- export function imageBuild<const N extends string, const K extends string = never>(def: ImageBuildTarballDef<N, K> | ImageBuildPushDef<N, K>): AnyImageBuild {
384
+ export function imageBuild<const N extends string, const K extends string = never>(
385
+ def: ImageBuildTarballDef<N, K>,
386
+ ): Implementation<Step<N, ImageBuildInput, typeof BuiltImage, K>>;
387
+ export function imageBuild<const N extends string, const K extends string = never>(
388
+ def: ImageBuildPushDef<N, K>,
389
+ ): Implementation<Step<N, ImageBuildInput, typeof PushedImage, K>>;
390
+ export function imageBuild<const N extends string, const K extends string = never>(
391
+ def: ImageBuildTarballDef<N, K> | ImageBuildPushDef<N, K>,
392
+ ): AnyImageBuild {
291
393
  const push = def.push === true;
292
394
  const contentTag = push && def.contentTag === true;
293
- const context = checkRelative("context", def.context ?? "");
294
- const authSecret = def.registryAuth && !isConfigRef(def.registryAuth) ? def.registryAuth : undefined;
295
- if (isLiteral([def.repository, def.registryAuth, def.insecureRegistry])) checkRegistryHost(def.name, def.repository as string, push, def.registryAuth !== undefined, def.insecureRegistry === true);
296
- const secrets = [...(authSecret ? [authSecret.password] : []), ...(def.secrets ?? [])].filter((s, i, all) => all.findIndex((x) => x.name === s.name) === i);
395
+ const context = checkRelative('context', def.context ?? '');
396
+ const authSecret =
397
+ def.registryAuth && !isConfigRef(def.registryAuth) ? def.registryAuth : undefined;
398
+ if (isLiteral([def.repository, def.registryAuth, def.insecureRegistry]))
399
+ checkRegistryHost(
400
+ def.name,
401
+ def.repository as string,
402
+ push,
403
+ def.registryAuth !== undefined,
404
+ def.insecureRegistry === true,
405
+ );
406
+ const secrets = [
407
+ ...(authSecret ? [authSecret.password] : []),
408
+ ...(def.secrets ?? []),
409
+ ].filter((s, i, all) => all.findIndex((x) => x.name === s.name) === i);
297
410
  const registryMap = def.registryMap ?? DEFAULT_REGISTRY_MAP;
298
411
  const mapped = isConfigRef(registryMap) || Object.keys(registryMap).length > 0;
299
412
  const contract = step({
300
413
  name: def.name,
301
414
  description: def.description,
302
- kind: "imageBuild",
415
+ kind: 'imageBuild',
303
416
  image: KANIKO_IMAGE,
304
417
  input,
305
418
  output: push ? PushedImage : BuiltImage,
@@ -312,101 +425,164 @@ export function imageBuild<const N extends string, const K extends string = neve
312
425
  ...(push ? { push: true } : {}),
313
426
  ...(contentTag ? { contentTag: true } : {}),
314
427
  ...(mapped ? { registryMap } : {}),
315
- ...(authSecret ? { registryAuth: { username: authSecret.username, password: authSecret.password.name } } : {}),
316
- ...(def.registryAuth && isConfigRef(def.registryAuth) ? { registryAuth: def.registryAuth } : {}),
317
- ...(def.insecureRegistry !== undefined && def.insecureRegistry !== false ? { insecureRegistry: def.insecureRegistry } : {}),
428
+ ...(authSecret
429
+ ? {
430
+ registryAuth: {
431
+ username: authSecret.username,
432
+ password: authSecret.password.name,
433
+ },
434
+ }
435
+ : {}),
436
+ ...(def.registryAuth && isConfigRef(def.registryAuth)
437
+ ? { registryAuth: def.registryAuth }
438
+ : {}),
439
+ ...(def.insecureRegistry !== undefined && def.insecureRegistry !== false
440
+ ? { insecureRegistry: def.insecureRegistry }
441
+ : {}),
318
442
  ...(def.enabled !== undefined ? { enabled: def.enabled } : {}),
319
443
  },
320
444
  });
321
445
  return implement(contract, async ({ source }, ctx: Ctx) => {
322
- const d = resolveConfigRefs({
323
- repository: def.repository, registryMap: def.registryMap, insecureRegistry: def.insecureRegistry,
324
- registryAuth: def.registryAuth, enabled: def.enabled,
325
- }, ctx.config ?? {});
446
+ const d = resolveConfigRefs(
447
+ {
448
+ repository: def.repository,
449
+ registryMap: def.registryMap,
450
+ insecureRegistry: def.insecureRegistry,
451
+ registryAuth: def.registryAuth,
452
+ enabled: def.enabled,
453
+ },
454
+ ctx.config ?? {},
455
+ );
326
456
  const repository = d.repository as string;
327
457
  if (d.enabled === false) return ctx.skip(`publishing ${repository} is turned off`);
328
458
  const insecure = d.insecureRegistry === true;
329
- const auth = registryLogin(d.registryAuth as RegistryAuth<string> | RegistryAuthConfig | undefined, ctx, secrets.map((s) => s.name));
459
+ const auth = registryLogin(
460
+ d.registryAuth as RegistryAuth<string> | RegistryAuthConfig | undefined,
461
+ ctx,
462
+ secrets.map((s) => s.name),
463
+ );
330
464
  checkRegistryHost(def.name, repository, push, auth !== undefined, insecure);
331
465
  const host = registryHost(repository);
332
466
 
333
467
  const tree = await ctx.files.fetch(source.tree);
334
468
  const contextDir = context ? join(tree, context) : tree;
335
- if (!existsSync(contextDir) || !statSync(contextDir).isDirectory()) throw new Error(`build context '${context}' not found in the source tree`);
336
- if (!existsSync(join(contextDir, "Dockerfile"))) throw new Error(`Dockerfile not found in the build context${context ? ` '${context}'` : ""}`);
469
+ if (!existsSync(contextDir) || !statSync(contextDir).isDirectory())
470
+ throw new Error(`build context '${context}' not found in the source tree`);
471
+ if (!existsSync(join(contextDir, 'Dockerfile')))
472
+ throw new Error(
473
+ `Dockerfile not found in the build context${context ? ` '${context}'` : ''}`,
474
+ );
337
475
 
338
476
  const access: RegistryAccess = { insecure, ...(auth ?? {}) };
339
- if (auth) writeDockerConfig(process.env.DOCKER_CONFIG || KANIKO_DOCKER_CONFIG, host, auth.username, auth.password);
477
+ if (auth)
478
+ writeDockerConfig(
479
+ process.env.DOCKER_CONFIG || KANIKO_DOCKER_CONFIG,
480
+ host,
481
+ auth.username,
482
+ auth.password,
483
+ );
340
484
 
341
485
  const tag = contentTag ? contentTagOf(contextDir, ctx.tmpDir) : imageTag(ctx.run);
342
486
  if (contentTag) {
343
487
  const digest = await registryDigest(ctx.http, repository, tag, access);
344
488
  if (digest) {
345
- ctx.log("image already in the registry", { image: `${repository}:${tag}`, digest });
489
+ ctx.log('image already in the registry', {
490
+ image: `${repository}:${tag}`,
491
+ digest,
492
+ });
346
493
  return { repository, tag, digest };
347
494
  }
348
495
  }
349
496
 
350
- const mirrors = await reachableMirrors(ctx, (d.registryMap as Record<string, string> | undefined) ?? DEFAULT_REGISTRY_MAP, insecure ? [host] : []);
351
- await ctx.exec(["mkdir", "-p", KANIKO_CONTEXT]);
352
- await ctx.exec(["cp", "-r", `${tree}/.`, KANIKO_CONTEXT]);
497
+ const mirrors = await reachableMirrors(
498
+ ctx,
499
+ (d.registryMap as Record<string, string> | undefined) ?? DEFAULT_REGISTRY_MAP,
500
+ insecure ? [host] : [],
501
+ );
502
+ await ctx.exec(['mkdir', '-p', KANIKO_CONTEXT]);
503
+ await ctx.exec(['cp', '-r', `${tree}/.`, KANIKO_CONTEXT]);
353
504
  const kanikoContext = context ? `${KANIKO_CONTEXT}/${context}` : KANIKO_CONTEXT;
354
505
 
355
506
  // Outputs go to the harness's scratch dir (/olve/tmp, an emptyDir mount kaniko leaves alone;
356
507
  // kaniko images have no /tmp), then into the step's files.
357
508
  mkdirSync(ctx.tmpDir, { recursive: true });
358
- const out = mkdtempSync(join(ctx.tmpDir, "image-build-"));
359
- const tarPath = join(out, "image.tar");
360
- const digestPath = join(out, "digest");
361
- const output = push ? [] : ["--no-push", `--tar-path=${tarPath}`];
509
+ const out = mkdtempSync(join(ctx.tmpDir, 'image-build-'));
510
+ const tarPath = join(out, 'image.tar');
511
+ const digestPath = join(out, 'digest');
512
+ const output = push ? [] : ['--no-push', `--tar-path=${tarPath}`];
362
513
  // No --single-snapshot: it is incompatible with multi-stage Dockerfiles whose later stages need
363
514
  // the build context intact.
364
- await ctx.exec([
365
- "/kaniko/executor",
366
- `--context=${kanikoContext}`,
367
- `--dockerfile=${kanikoContext}/Dockerfile`,
368
- ...output,
369
- `--digest-file=${digestPath}`,
370
- `--destination=${repository}:${tag}`,
371
- ...(insecure ? ["--insecure", `--insecure-registry=${host}`] : []),
372
- "--snapshot-mode=redo",
373
- "--use-new-run",
374
- "--compressed-caching=false",
375
- ...Object.entries(mirrors).map(([from, to]) => `--registry-map=${from}=${to}`),
376
- ], { idleTimeoutSeconds: def.stallTimeoutSeconds ?? DEFAULT_STALL_TIMEOUT_SECONDS });
515
+ await ctx.exec(
516
+ [
517
+ '/kaniko/executor',
518
+ `--context=${kanikoContext}`,
519
+ `--dockerfile=${kanikoContext}/Dockerfile`,
520
+ ...output,
521
+ `--digest-file=${digestPath}`,
522
+ `--destination=${repository}:${tag}`,
523
+ ...(insecure ? ['--insecure', `--insecure-registry=${host}`] : []),
524
+ '--snapshot-mode=redo',
525
+ '--use-new-run',
526
+ '--compressed-caching=false',
527
+ ...Object.entries(mirrors).map(([from, to]) => `--registry-map=${from}=${to}`),
528
+ ],
529
+ { idleTimeoutSeconds: def.stallTimeoutSeconds ?? DEFAULT_STALL_TIMEOUT_SECONDS },
530
+ );
377
531
 
378
532
  let digest: string;
379
533
  try {
380
- digest = readFileSync(digestPath, "utf8").trim();
534
+ digest = readFileSync(digestPath, 'utf8').trim();
381
535
  } catch {
382
536
  throw new Error(`kaniko did not write the image digest to ${digestPath}`);
383
537
  }
384
- if (!/^sha256:[0-9a-f]{64}$/.test(digest)) throw new Error(`kaniko wrote an unexpected digest: '${digest}'`);
538
+ if (!/^sha256:[0-9a-f]{64}$/.test(digest))
539
+ throw new Error(`kaniko wrote an unexpected digest: '${digest}'`);
385
540
  if (push) {
386
- ctx.log("image pushed", { image: `${repository}:${tag}`, digest });
541
+ ctx.log('image pushed', { image: `${repository}:${tag}`, digest });
387
542
  return { repository, tag, digest };
388
543
  }
389
- const tar = await ctx.output.file(tarPath, { name: "image.tar" });
544
+ const tar = await ctx.output.file(tarPath, { name: 'image.tar' });
390
545
  rmSync(tarPath); // the copy in the step's files is the one that counts; don't hold the image twice in /olve
391
- ctx.log("image built", { image: `${repository}:${tag}`, digest });
546
+ ctx.log('image built', { image: `${repository}:${tag}`, digest });
392
547
  return { repository: qualifyRepository(repository), tag, digest, tar };
393
548
  }) as never;
394
549
  }
395
550
 
396
- function checkRegistryHost(name: string, repository: string, push: boolean, auth: boolean, insecure: boolean): void {
551
+ function checkRegistryHost(
552
+ name: string,
553
+ repository: string,
554
+ push: boolean,
555
+ auth: boolean,
556
+ insecure: boolean,
557
+ ): void {
397
558
  const host = registryHost(repository);
398
- if (push && !/[.:]|^localhost$/.test(host)) throw new Error(`imageBuild '${name}': push needs a repository with a registry host, got '${repository}'`);
399
- if ((auth || insecure) && !/[.:]|^localhost$/.test(host)) throw new Error(`imageBuild '${name}': registryAuth and insecureRegistry need a repository with a registry host, got '${repository}'`);
559
+ if (push && !/[.:]|^localhost$/.test(host))
560
+ throw new Error(
561
+ `imageBuild '${name}': push needs a repository with a registry host, got '${repository}'`,
562
+ );
563
+ if ((auth || insecure) && !/[.:]|^localhost$/.test(host))
564
+ throw new Error(
565
+ `imageBuild '${name}': registryAuth and insecureRegistry need a repository with a registry host, got '${repository}'`,
566
+ );
400
567
  }
401
568
 
402
569
  /** The login for `registryAuth` (a literal secret or a config `secretRef`), or undefined without one. */
403
- function registryLogin(auth: RegistryAuth<string> | RegistryAuthConfig | undefined, ctx: Pick<Ctx, "secrets">, declared: readonly string[]): { username: string; password: string } | undefined {
570
+ function registryLogin(
571
+ auth: RegistryAuth<string> | RegistryAuthConfig | undefined,
572
+ ctx: Pick<Ctx, 'secrets'>,
573
+ declared: readonly string[],
574
+ ): { username: string; password: string } | undefined {
404
575
  if (!auth) return undefined;
405
- const name = "name" in auth.password ? auth.password.name : auth.password.secret;
406
- if (!declared.includes(name)) throw new Error(`registryAuth names secret ${name}, which the step does not declare (add it to secrets)`);
576
+ const name = 'name' in auth.password ? auth.password.name : auth.password.secret;
577
+ if (!declared.includes(name))
578
+ throw new Error(
579
+ `registryAuth names secret ${name}, which the step does not declare (add it to secrets)`,
580
+ );
407
581
  const password = (ctx.secrets as Record<string, string | undefined>)[name];
408
582
  if (!password) throw new Error(`secret ${name} (registryAuth) is not set`);
409
583
  return { username: auth.username, password };
410
584
  }
411
585
 
412
- type AnyImageBuild = Implementation<Step<string, ImageBuildInput, typeof BuiltImage | typeof PushedImage, any>>;
586
+ type AnyImageBuild = Implementation<
587
+ Step<string, ImageBuildInput, typeof BuiltImage | typeof PushedImage, any>
588
+ >;