@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.
- package/README.md +2 -0
- package/manifest.schema.json +22 -0
- package/package.json +4 -2
- package/src/assets.d.ts +2 -2
- package/src/build/build.ts +111 -43
- package/src/build/cli.ts +28 -21
- package/src/build/elf.ts +8 -7
- package/src/build/hashes.ts +61 -28
- package/src/build/project.ts +69 -30
- package/src/builder/pipeline.ts +288 -102
- package/src/builder/refs.ts +12 -6
- package/src/core/brands.ts +6 -6
- package/src/core/config-refs.ts +46 -20
- package/src/core/config.ts +110 -33
- package/src/core/external.ts +30 -12
- package/src/core/handlers.ts +54 -14
- package/src/core/models.ts +29 -14
- package/src/core/origin.ts +7 -4
- package/src/core/secrets.ts +2 -2
- package/src/core/steps.ts +39 -23
- package/src/core/types.ts +5 -1
- package/src/core/webhooks.ts +21 -7
- package/src/ctx/files.ts +111 -40
- package/src/ctx/index.ts +75 -34
- package/src/ctx/sh.ts +104 -44
- package/src/ctx/types.ts +4 -4
- package/src/describe/describe.ts +558 -194
- package/src/describe/manifest-schema.ts +217 -79
- package/src/describe/mapper-source.ts +249 -104
- package/src/describe/record.ts +67 -37
- package/src/dev/cli.ts +64 -34
- package/src/harness/env.ts +33 -27
- package/src/harness/execute.ts +55 -29
- package/src/harness/index.ts +21 -8
- package/src/harness/main.ts +197 -74
- package/src/harness/result.ts +38 -22
- package/src/harness/validate.ts +62 -19
- package/src/index.ts +123 -14
- package/src/library/checks.ts +11 -6
- package/src/library/compose-deploy.ts +40 -23
- package/src/library/github-source.ts +69 -39
- package/src/library/helm-deploy-ssh.ts +213 -76
- package/src/library/helm-deploy.ts +263 -85
- package/src/library/http-health.ts +7 -5
- package/src/library/image-build.ts +293 -117
- package/src/library/index.ts +47 -15
- package/src/library/models.ts +17 -9
- package/src/library/ssh.ts +31 -14
- package/src/testing/index.ts +63 -24
- package/src/util/canonical.ts +7 -4
- package/src/util/handler-hash.ts +15 -5
- package/src/util/hash.ts +5 -5
- package/src/util/tar.ts +74 -37
|
@@ -1,51 +1,96 @@
|
|
|
1
1
|
// Helm deploys to a target from config (`helmTarget()`): steps.helmDeployInCluster runs helm in the step
|
|
2
2
|
// pod against its own cluster; steps.helmDeployOverSsh({ target, … }) runs it on a k3s host. Pick one
|
|
3
3
|
// per target kind with `select`. Images come from a registry, pinned by digest.
|
|
4
|
-
import { existsSync, writeFileSync } from
|
|
5
|
-
import { join } from
|
|
6
|
-
import { z } from
|
|
7
|
-
import type { Ctx } from
|
|
8
|
-
import type { Configurable } from
|
|
9
|
-
import { resolveConfigRefs } from
|
|
10
|
-
import { implement, type Implementation } from
|
|
11
|
-
import type { FileRef } from
|
|
12
|
-
import { optional } from
|
|
13
|
-
import type { Secret } from
|
|
14
|
-
import { secretRef } from
|
|
15
|
-
import { step, type AnyStep, type LiteralName, type Step } from
|
|
16
|
-
import type { Simplify } from
|
|
17
|
-
import { serviceUrl } from
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
4
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import type { Ctx } from '../ctx/types.ts';
|
|
8
|
+
import type { Configurable } from '../core/config.ts';
|
|
9
|
+
import { resolveConfigRefs } from '../core/config-refs.ts';
|
|
10
|
+
import { implement, type Implementation } from '../core/handlers.ts';
|
|
11
|
+
import type { FileRef } from '../core/models.ts';
|
|
12
|
+
import { optional } from '../core/models.ts';
|
|
13
|
+
import type { Secret } from '../core/secrets.ts';
|
|
14
|
+
import { secretRef } from '../core/config.ts';
|
|
15
|
+
import { step, type AnyStep, type LiteralName, type Step } from '../core/steps.ts';
|
|
16
|
+
import type { Simplify } from '../core/types.ts';
|
|
17
|
+
import { serviceUrl } from './checks.ts';
|
|
18
|
+
import {
|
|
19
|
+
chartDirectory,
|
|
20
|
+
deployOverSsh,
|
|
21
|
+
DEFAULT_ROLLOUT_TIMEOUT_SECONDS,
|
|
22
|
+
HELM_DEPLOY_IMAGE,
|
|
23
|
+
helmFlags,
|
|
24
|
+
type HelmImageFlags,
|
|
25
|
+
type HelmImageRefFlags,
|
|
26
|
+
} from './helm-deploy-ssh.ts';
|
|
27
|
+
import { httpHealthCheck } from './http-health.ts';
|
|
28
|
+
import { HelmRelease, PushedImage, Source } from './models.ts';
|
|
21
29
|
|
|
22
30
|
/** helm 3 on Alpine, for in-cluster deploys (the health check runs in the harness). */
|
|
23
|
-
export const HELM_IMAGE =
|
|
31
|
+
export const HELM_IMAGE = 'alpine/helm:3.16.2';
|
|
24
32
|
|
|
25
33
|
/** Where the step pod finds its ServiceAccount token (mounted only with the instance's step ServiceAccount). */
|
|
26
|
-
export const SERVICE_ACCOUNT_TOKEN =
|
|
34
|
+
export const SERVICE_ACCOUNT_TOKEN =
|
|
35
|
+
'/var/run/secrets/kubernetes.io/serviceaccount/token';
|
|
27
36
|
|
|
28
|
-
function targetVariants<const E extends z.ZodRawShape, const K extends string>(
|
|
29
|
-
|
|
37
|
+
function targetVariants<const E extends z.ZodRawShape, const K extends string>(
|
|
38
|
+
o: Omit<DeployTargetOptions<E, K>, 'composeToken'> & { composeToken?: Secret<K> },
|
|
39
|
+
) {
|
|
40
|
+
const name = (d: string | undefined) =>
|
|
41
|
+
d === undefined ? z.string() : z.string().default(d);
|
|
30
42
|
const common = {
|
|
31
|
-
namespace: name(o.namespace).describe(
|
|
32
|
-
release: name(o.release).describe(
|
|
33
|
-
values: z
|
|
34
|
-
|
|
35
|
-
|
|
43
|
+
namespace: name(o.namespace).describe('Namespace the release is installed into.'),
|
|
44
|
+
release: name(o.release).describe('Helm release name.'),
|
|
45
|
+
values: z
|
|
46
|
+
.array(z.string())
|
|
47
|
+
.default([])
|
|
48
|
+
.describe('Values files, relative to the chart directory (-f, in order).'),
|
|
49
|
+
set: z.record(z.string(), z.string()).default({}).describe('Extra --set flags.'),
|
|
50
|
+
url: z
|
|
51
|
+
.string()
|
|
52
|
+
.optional()
|
|
53
|
+
.describe(
|
|
54
|
+
'Base URL step pods reach the release at (health checks, smoke tests); default http://<release>.<namespace>.svc.cluster.local.',
|
|
55
|
+
),
|
|
36
56
|
...(o.extend ?? ({} as E)),
|
|
37
57
|
};
|
|
38
|
-
const inCluster = z.object({
|
|
39
|
-
|
|
58
|
+
const inCluster = z.object({
|
|
59
|
+
kind: z
|
|
60
|
+
.literal('inCluster')
|
|
61
|
+
.describe(
|
|
62
|
+
"helm runs in the step pod against its own cluster (needs the instance's step ServiceAccount).",
|
|
63
|
+
),
|
|
64
|
+
...common,
|
|
65
|
+
});
|
|
66
|
+
const ssh = z.object({
|
|
67
|
+
kind: z.literal('ssh').describe('helm runs on a k3s host over SSH.'),
|
|
68
|
+
host: z.string().describe('user@host'),
|
|
69
|
+
...common,
|
|
70
|
+
});
|
|
40
71
|
const compose = z.object({
|
|
41
|
-
kind: z
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
72
|
+
kind: z
|
|
73
|
+
.literal('compose')
|
|
74
|
+
.describe('A Docker Compose deployer recreates a service with the image.'),
|
|
75
|
+
deployerUrl: z
|
|
76
|
+
.string()
|
|
77
|
+
.describe("The deployer's base URL (POST <deployerUrl>/v1/deploy)."),
|
|
78
|
+
service: z.string().describe('The compose service to recreate.'),
|
|
79
|
+
token: secretRef(
|
|
80
|
+
o.composeToken ??
|
|
81
|
+
({ kind: 'secret', name: 'DEPLOYER_TOKEN', description: '' } as Secret<K>),
|
|
82
|
+
).describe("The deployer's bearer token."),
|
|
83
|
+
healthUrl: z
|
|
84
|
+
.string()
|
|
85
|
+
.optional()
|
|
86
|
+
.describe(
|
|
87
|
+
"Checked by the step after the deployer's own health check, when the step can reach it.",
|
|
88
|
+
),
|
|
46
89
|
...(o.extend ?? ({} as E)),
|
|
47
90
|
});
|
|
48
|
-
const none = z.object({
|
|
91
|
+
const none = z.object({
|
|
92
|
+
kind: z.literal('none').describe("Not deployed: the target's steps are left out."),
|
|
93
|
+
});
|
|
49
94
|
return { inCluster, ssh, compose, none };
|
|
50
95
|
}
|
|
51
96
|
|
|
@@ -67,21 +112,42 @@ export interface DeployTargetOptions<E extends z.ZodRawShape, K extends string>
|
|
|
67
112
|
* `compose` (a Docker Compose deployer), and with `optional: true` also `none` (not deployed).
|
|
68
113
|
* `config("target", "…", z.object({ beta: deployTarget({ optional: true, composeToken: T }), prod: deployTarget({ composeToken: T }) }))`
|
|
69
114
|
*/
|
|
70
|
-
export function deployTarget<const K extends string, const E extends z.ZodRawShape = {}>(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
]
|
|
115
|
+
export function deployTarget<const K extends string, const E extends z.ZodRawShape = {}>(
|
|
116
|
+
o: DeployTargetOptions<E, K> & { optional: true },
|
|
117
|
+
): z.ZodDiscriminatedUnion<
|
|
118
|
+
[
|
|
119
|
+
ReturnType<typeof targetVariants<E, K>>['inCluster'],
|
|
120
|
+
ReturnType<typeof targetVariants<E, K>>['ssh'],
|
|
121
|
+
ReturnType<typeof targetVariants<E, K>>['compose'],
|
|
122
|
+
ReturnType<typeof targetVariants<E, K>>['none'],
|
|
123
|
+
]
|
|
124
|
+
>;
|
|
125
|
+
export function deployTarget<const K extends string, const E extends z.ZodRawShape = {}>(
|
|
126
|
+
o: DeployTargetOptions<E, K>,
|
|
127
|
+
): z.ZodDiscriminatedUnion<
|
|
128
|
+
[
|
|
129
|
+
ReturnType<typeof targetVariants<E, K>>['inCluster'],
|
|
130
|
+
ReturnType<typeof targetVariants<E, K>>['ssh'],
|
|
131
|
+
ReturnType<typeof targetVariants<E, K>>['compose'],
|
|
132
|
+
]
|
|
133
|
+
>;
|
|
76
134
|
export function deployTarget(o: DeployTargetOptions<z.ZodRawShape, string>): unknown {
|
|
77
135
|
const v = targetVariants(o);
|
|
78
|
-
return z.discriminatedUnion(
|
|
136
|
+
return z.discriminatedUnion(
|
|
137
|
+
'kind',
|
|
138
|
+
o.optional
|
|
139
|
+
? [v.inCluster, v.ssh, v.compose, v.none]
|
|
140
|
+
: [v.inCluster, v.ssh, v.compose],
|
|
141
|
+
);
|
|
79
142
|
}
|
|
80
143
|
|
|
81
144
|
/** A helm-only deploy target (`inCluster | ssh`). */
|
|
82
|
-
export function helmTarget<const E extends z.ZodRawShape = {}>(
|
|
145
|
+
export function helmTarget<const E extends z.ZodRawShape = {}>(
|
|
146
|
+
defaults: { namespace?: string; release?: string } = {},
|
|
147
|
+
extend?: E,
|
|
148
|
+
) {
|
|
83
149
|
const v = targetVariants({ ...defaults, extend });
|
|
84
|
-
return z.discriminatedUnion(
|
|
150
|
+
return z.discriminatedUnion('kind', [v.inCluster, v.ssh]);
|
|
85
151
|
}
|
|
86
152
|
|
|
87
153
|
interface TargetCommon {
|
|
@@ -92,19 +158,28 @@ interface TargetCommon {
|
|
|
92
158
|
url?: string | undefined;
|
|
93
159
|
}
|
|
94
160
|
/** A resolved `kind: inCluster` target. */
|
|
95
|
-
export type InClusterTargetValue = TargetCommon & { kind:
|
|
161
|
+
export type InClusterTargetValue = TargetCommon & { kind: 'inCluster' };
|
|
96
162
|
/** A resolved `kind: ssh` target. */
|
|
97
|
-
export type SshTargetValue = TargetCommon & { kind:
|
|
163
|
+
export type SshTargetValue = TargetCommon & { kind: 'ssh'; host: string };
|
|
98
164
|
/** A resolved deploy target (the output of `helmTarget()`). */
|
|
99
165
|
export type HelmTargetValue = InClusterTargetValue | SshTargetValue;
|
|
100
166
|
|
|
101
|
-
type DefaultImages = { readonly image:
|
|
102
|
-
export type HelmTargetDeployInput<
|
|
103
|
-
|
|
167
|
+
type DefaultImages = { readonly image: 'image' };
|
|
168
|
+
export type HelmTargetDeployInput<
|
|
169
|
+
Im extends Record<string, string> = DefaultImages,
|
|
170
|
+
Rf extends Record<string, string> = {},
|
|
171
|
+
> = Simplify<
|
|
172
|
+
{ readonly source: typeof Source } & {
|
|
173
|
+
readonly [K in keyof Im]: typeof PushedImage;
|
|
174
|
+
} & { readonly [K in keyof Rf]: ReturnType<typeof optional<typeof PushedImage>> }
|
|
104
175
|
>;
|
|
105
176
|
|
|
106
177
|
/** What both target deploys take (besides `target`). */
|
|
107
|
-
export interface HelmTargetDeployDef<
|
|
178
|
+
export interface HelmTargetDeployDef<
|
|
179
|
+
N extends string,
|
|
180
|
+
Im extends Record<string, string> = DefaultImages,
|
|
181
|
+
Rf extends Record<string, string> = {},
|
|
182
|
+
> {
|
|
108
183
|
name: LiteralName<N>;
|
|
109
184
|
/** What the step does, shown in the UI. Required; may be empty. */
|
|
110
185
|
description: string;
|
|
@@ -134,14 +209,21 @@ export interface HelmTargetDeployDef<N extends string, Im extends Record<string,
|
|
|
134
209
|
timeoutSeconds?: number;
|
|
135
210
|
}
|
|
136
211
|
|
|
137
|
-
export interface HelmDeployInClusterDef<
|
|
138
|
-
extends
|
|
212
|
+
export interface HelmDeployInClusterDef<
|
|
213
|
+
N extends string,
|
|
214
|
+
Im extends Record<string, string> = DefaultImages,
|
|
215
|
+
Rf extends Record<string, string> = {},
|
|
216
|
+
> extends HelmTargetDeployDef<N, Im, Rf> {
|
|
139
217
|
/** An in-cluster target, usually a `select` case's reference (`(p, prod) => … target: prod`). */
|
|
140
218
|
target: Configurable<InClusterTargetValue>;
|
|
141
219
|
}
|
|
142
220
|
|
|
143
|
-
export interface HelmDeploySshTargetDef<
|
|
144
|
-
extends
|
|
221
|
+
export interface HelmDeploySshTargetDef<
|
|
222
|
+
N extends string,
|
|
223
|
+
K extends string,
|
|
224
|
+
Im extends Record<string, string> = DefaultImages,
|
|
225
|
+
Rf extends Record<string, string> = {},
|
|
226
|
+
> extends HelmTargetDeployDef<N, Im, Rf> {
|
|
145
227
|
/** An ssh target, usually a `select` case's reference. */
|
|
146
228
|
target: Configurable<SshTargetValue>;
|
|
147
229
|
/** Private key for the target's host. */
|
|
@@ -159,13 +241,31 @@ export function helmDeployInCluster<
|
|
|
159
241
|
const N extends string,
|
|
160
242
|
const Im extends Record<string, string> = DefaultImages,
|
|
161
243
|
const Rf extends Record<string, string> = {},
|
|
162
|
-
>(
|
|
163
|
-
|
|
244
|
+
>(
|
|
245
|
+
def: HelmDeployInClusterDef<N, Im, Rf>,
|
|
246
|
+
): Implementation<Step<N, HelmTargetDeployInput<Im, Rf>, typeof HelmRelease, never>> {
|
|
247
|
+
return targetDeploy(
|
|
248
|
+
'helmDeployInCluster',
|
|
249
|
+
def as unknown as AnyTargetDef,
|
|
250
|
+
undefined,
|
|
251
|
+
) as never;
|
|
164
252
|
}
|
|
165
253
|
|
|
166
254
|
/** The `target` form of `steps.helmDeployOverSsh`: an ssh target from config, registry images by digest. */
|
|
167
|
-
export function helmDeployOverSshTarget(
|
|
168
|
-
|
|
255
|
+
export function helmDeployOverSshTarget(
|
|
256
|
+
def: HelmDeploySshTargetDef<
|
|
257
|
+
string,
|
|
258
|
+
string,
|
|
259
|
+
Record<string, string>,
|
|
260
|
+
Record<string, string>
|
|
261
|
+
>,
|
|
262
|
+
): Implementation {
|
|
263
|
+
return targetDeploy(
|
|
264
|
+
'helmDeployOverSsh',
|
|
265
|
+
def as unknown as AnyTargetDef,
|
|
266
|
+
def.sshKey,
|
|
267
|
+
def.deployments,
|
|
268
|
+
) as never;
|
|
169
269
|
}
|
|
170
270
|
|
|
171
271
|
interface AnyTargetDef {
|
|
@@ -181,17 +281,27 @@ interface AnyTargetDef {
|
|
|
181
281
|
timeoutSeconds?: number;
|
|
182
282
|
}
|
|
183
283
|
|
|
184
|
-
function targetDeploy(
|
|
185
|
-
|
|
284
|
+
function targetDeploy(
|
|
285
|
+
kind: 'helmDeployInCluster' | 'helmDeployOverSsh',
|
|
286
|
+
def: AnyTargetDef,
|
|
287
|
+
sshKey: Secret | undefined,
|
|
288
|
+
deployments?: readonly string[],
|
|
289
|
+
): Implementation {
|
|
290
|
+
const images: Record<string, string> = { ...(def.images ?? { image: 'image' }) };
|
|
186
291
|
const imageEntries = Object.entries(images);
|
|
187
292
|
const imageRefs: Record<string, string> = { ...(def.imageRefs ?? {}) };
|
|
188
293
|
const refEntries = Object.entries(imageRefs);
|
|
189
294
|
const rolloutTimeout = def.rolloutTimeoutSeconds ?? DEFAULT_ROLLOUT_TIMEOUT_SECONDS;
|
|
190
|
-
if (
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
295
|
+
if ('source' in images || 'source' in imageRefs)
|
|
296
|
+
throw new Error(`${kind} '${def.name}': 'source' can't be an image input`);
|
|
297
|
+
for (const [key] of refEntries)
|
|
298
|
+
if (key in images)
|
|
299
|
+
throw new Error(`${kind} '${def.name}': '${key}' is both in images and imageRefs`);
|
|
300
|
+
if (deployments?.length === 0)
|
|
301
|
+
throw new Error(`${kind} '${def.name}': deployments must not be empty`);
|
|
302
|
+
if (def.healthPath !== undefined && !def.healthPath.startsWith('/'))
|
|
303
|
+
throw new Error(`${kind} '${def.name}': healthPath must start with '/'`);
|
|
304
|
+
const inCluster = kind === 'helmDeployInCluster';
|
|
195
305
|
const input = {
|
|
196
306
|
source: Source,
|
|
197
307
|
...Object.fromEntries(imageEntries.map(([key]) => [key, PushedImage])),
|
|
@@ -217,15 +327,26 @@ function targetDeploy(kind: "helmDeployInCluster" | "helmDeployOverSsh", def: An
|
|
|
217
327
|
images: { ...images },
|
|
218
328
|
...(refEntries.length > 0 ? { imageRefs: { ...imageRefs } } : {}),
|
|
219
329
|
...(deployments ? { deployments: [...deployments] } : {}),
|
|
220
|
-
...(def.rolloutTimeoutSeconds !== undefined
|
|
330
|
+
...(def.rolloutTimeoutSeconds !== undefined
|
|
331
|
+
? { rolloutTimeoutSeconds: def.rolloutTimeoutSeconds }
|
|
332
|
+
: {}),
|
|
221
333
|
...(def.healthPath !== undefined ? { healthPath: def.healthPath } : {}),
|
|
222
334
|
},
|
|
223
335
|
} as never) as AnyStep;
|
|
224
336
|
return implement(contract, async (stepInput, ctx: Ctx) => {
|
|
225
|
-
const input = stepInput as unknown as Record<
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
337
|
+
const input = stepInput as unknown as Record<
|
|
338
|
+
string,
|
|
339
|
+
{ repository: string; tag: string; digest: string } | undefined
|
|
340
|
+
> & { source: { tree: FileRef } };
|
|
341
|
+
const { target, inlineValues } = resolveConfigRefs(
|
|
342
|
+
configurable,
|
|
343
|
+
ctx.config ?? {},
|
|
344
|
+
) as { target: HelmTargetValue; inlineValues: Record<string, unknown>[] };
|
|
345
|
+
const want = inCluster ? 'inCluster' : 'ssh';
|
|
346
|
+
if (target?.kind !== want)
|
|
347
|
+
throw new Error(
|
|
348
|
+
`${kind} '${def.name}': the target is kind '${String(target?.kind)}', not '${want}'`,
|
|
349
|
+
);
|
|
229
350
|
const chartDir = chartDirectory(await ctx.files.fetch(input.source.tree), def.chart);
|
|
230
351
|
|
|
231
352
|
const generated = inlineValues.map((values, i) => {
|
|
@@ -235,37 +356,94 @@ function targetDeploy(kind: "helmDeployInCluster" | "helmDeployOverSsh", def: An
|
|
|
235
356
|
});
|
|
236
357
|
const flags: HelmImageFlags[] = imageEntries.map(([key, prefix]) => {
|
|
237
358
|
const image = input[key]!;
|
|
238
|
-
return {
|
|
359
|
+
return {
|
|
360
|
+
prefix,
|
|
361
|
+
repository: image.repository,
|
|
362
|
+
tag: image.tag,
|
|
363
|
+
digest: image.digest,
|
|
364
|
+
pullPolicy: 'IfNotPresent',
|
|
365
|
+
};
|
|
239
366
|
});
|
|
240
367
|
const refs: HelmImageRefFlags[] = refEntries.flatMap(([key, prefix]) => {
|
|
241
368
|
const image = input[key];
|
|
242
|
-
return image
|
|
369
|
+
return image
|
|
370
|
+
? [{ prefix, repository: image.repository, tag: image.tag, digest: image.digest }]
|
|
371
|
+
: [];
|
|
243
372
|
});
|
|
244
|
-
const helm = {
|
|
373
|
+
const helm = {
|
|
374
|
+
values: [...target.values, ...generated],
|
|
375
|
+
set: target.set,
|
|
376
|
+
images: flags,
|
|
377
|
+
imageRefs: refs,
|
|
378
|
+
};
|
|
245
379
|
const { release, namespace } = target;
|
|
246
380
|
|
|
247
|
-
if (target.kind ===
|
|
381
|
+
if (target.kind === 'ssh') {
|
|
248
382
|
const key = ctx.secrets[sshKey!.name];
|
|
249
383
|
if (!key) throw new Error(`secret ${sshKey!.name} (sshKey) is not set`);
|
|
250
384
|
await deployOverSsh(ctx, {
|
|
251
|
-
host: target.host,
|
|
252
|
-
|
|
253
|
-
|
|
385
|
+
host: target.host,
|
|
386
|
+
key,
|
|
387
|
+
chartDir,
|
|
388
|
+
release,
|
|
389
|
+
namespace,
|
|
390
|
+
hostImages: flags.map((f) => ({
|
|
391
|
+
repository: f.repository,
|
|
392
|
+
tag: f.tag,
|
|
393
|
+
digest: f.digest,
|
|
394
|
+
})),
|
|
395
|
+
flags: helm,
|
|
396
|
+
deployments: deployments ?? (imageEntries.length ? [release] : []),
|
|
397
|
+
rolloutTimeoutSeconds: rolloutTimeout,
|
|
254
398
|
});
|
|
255
399
|
} else {
|
|
256
400
|
if (!existsSync(SERVICE_ACCOUNT_TOKEN))
|
|
257
|
-
throw new Error(
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
401
|
+
throw new Error(
|
|
402
|
+
`no ServiceAccount token in the step pod: the instance grants no step ServiceAccount (Kubernetes:StepServiceAccount), so an inCluster target can't be deployed`,
|
|
403
|
+
);
|
|
404
|
+
const home = join(ctx.tmpDir, 'helm-home');
|
|
405
|
+
ctx.log('helm upgrade', {
|
|
406
|
+
release,
|
|
407
|
+
namespace,
|
|
408
|
+
images: flags.map((f) => `${f.repository}@${f.digest}`).join(' '),
|
|
263
409
|
});
|
|
410
|
+
await ctx.exec(
|
|
411
|
+
[
|
|
412
|
+
'helm',
|
|
413
|
+
'upgrade',
|
|
414
|
+
'--install',
|
|
415
|
+
release,
|
|
416
|
+
'.',
|
|
417
|
+
'-n',
|
|
418
|
+
namespace,
|
|
419
|
+
...helmFlags(helm),
|
|
420
|
+
'--wait',
|
|
421
|
+
`--timeout=${rolloutTimeout}s`,
|
|
422
|
+
],
|
|
423
|
+
{
|
|
424
|
+
cwd: chartDir,
|
|
425
|
+
env: {
|
|
426
|
+
HOME: home,
|
|
427
|
+
HELM_CACHE_HOME: join(home, 'cache'),
|
|
428
|
+
HELM_CONFIG_HOME: join(home, 'config'),
|
|
429
|
+
HELM_DATA_HOME: join(home, 'data'),
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
);
|
|
264
433
|
}
|
|
265
434
|
|
|
266
|
-
const url = (target.url ?? serviceUrl({ release, namespace })).replace(/\/+$/,
|
|
267
|
-
if (def.healthPath !== undefined)
|
|
435
|
+
const url = (target.url ?? serviceUrl({ release, namespace })).replace(/\/+$/, '');
|
|
436
|
+
if (def.healthPath !== undefined)
|
|
437
|
+
await httpHealthCheck(ctx, `${url}${def.healthPath}`, {
|
|
438
|
+
attempts: 12,
|
|
439
|
+
delaySeconds: 5,
|
|
440
|
+
});
|
|
268
441
|
const main = flags[0];
|
|
269
|
-
return {
|
|
442
|
+
return {
|
|
443
|
+
release,
|
|
444
|
+
namespace,
|
|
445
|
+
...(main ? { image: `${main.repository}:${main.tag}@${main.digest}` } : {}),
|
|
446
|
+
...(target.url ? { url } : {}),
|
|
447
|
+
} as never;
|
|
270
448
|
}) as never;
|
|
271
449
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// The general HTTP health check (ported from v1's deploy-beta.sh curl loop). Library steps use it
|
|
4
4
|
// (helmDeployOverSsh's `healthUrl`), and it is exported for custom handlers, which get it with
|
|
5
5
|
// their own retry policy. Runs in the harness (Bun fetch), so the image needs no curl.
|
|
6
|
-
import type { Ctx } from
|
|
6
|
+
import type { Ctx } from '../ctx/types.ts';
|
|
7
7
|
|
|
8
8
|
export interface HttpHealthCheckOptions {
|
|
9
9
|
/** Total attempts (v1: 5). */
|
|
@@ -22,13 +22,15 @@ export const HTTP_HEALTH_DEFAULTS = { attempts: 5, delaySeconds: 5 } as const;
|
|
|
22
22
|
* non-2xx statuses. Throws the last error (status + URL, query redacted) once the attempts run out.
|
|
23
23
|
*/
|
|
24
24
|
export async function httpHealthCheck(
|
|
25
|
-
ctx: Pick<Ctx,
|
|
25
|
+
ctx: Pick<Ctx, 'http' | 'retry' | 'log'>,
|
|
26
26
|
url: string,
|
|
27
27
|
opts: HttpHealthCheckOptions = {},
|
|
28
28
|
): Promise<void> {
|
|
29
29
|
const attempts = opts.attempts ?? HTTP_HEALTH_DEFAULTS.attempts;
|
|
30
30
|
const delaySeconds = opts.delaySeconds ?? HTTP_HEALTH_DEFAULTS.delaySeconds;
|
|
31
|
-
ctx.log(
|
|
32
|
-
await ctx.retry({ attempts, delaySeconds }, () =>
|
|
33
|
-
|
|
31
|
+
ctx.log('checking health', { url: url.split('?')[0]!, attempts });
|
|
32
|
+
await ctx.retry({ attempts, delaySeconds }, () =>
|
|
33
|
+
ctx.http.expectOk(url, opts.headers ? { headers: opts.headers } : undefined),
|
|
34
|
+
);
|
|
35
|
+
ctx.log('health OK', { url: url.split('?')[0]! });
|
|
34
36
|
}
|