hammerkit 1.27.0-next.13 → 1.27.0-next.15

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.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.workerMain = exports.taskCauses = exports.runTasks = exports.getRunNetwork = exports.workerInputSchema = void 0;
3
+ exports.workerMain = exports.readInput = exports.taskCauses = exports.runTasks = exports.getEndpointTargets = exports.publishEndpoints = exports.getRunNetwork = exports.workerInputSchema = void 0;
4
4
  const readline_1 = require("readline");
5
5
  const zod_1 = require("zod");
6
6
  const cli_1 = require("../cli");
@@ -21,6 +21,7 @@ const network_schema_1 = require("../schema/network-schema");
21
21
  const work_network_1 = require("../planner/work-network");
22
22
  const run_plan_1 = require("./run-plan");
23
23
  const runs_1 = require("./runs");
24
+ const resources_1 = require("../kubernetes/resources");
24
25
  // one log line of a task is cut here, the registry caps how many events a run keeps
25
26
  const MAX_LOG_TEXT = 8 * 1024;
26
27
  /**
@@ -59,6 +60,15 @@ exports.workerInputSchema = zod_1.z
59
60
  .string()
60
61
  .regex(/^[a-z]+-[0-9a-f]{24}$/)
61
62
  .optional(),
63
+ // a service run: the services it keeps up instead of tasks, and the ports the server
64
+ // reaches as endpoints
65
+ up: zod_1.z
66
+ .object({
67
+ services: zod_1.z.array(zod_1.z.string()).min(1),
68
+ endpoints: zod_1.z.array(zod_1.z.object({ service: zod_1.z.string(), port: zod_1.z.number().int() }).strict()),
69
+ })
70
+ .strict()
71
+ .optional(),
62
72
  })
63
73
  .strict();
64
74
  // The ceiling of the run, combined here and not in the server: the lower of two allow lists
@@ -68,6 +78,60 @@ function getRunNetwork(input) {
68
78
  return input.sandboxNetwork ? (0, work_network_1.limitNetwork)((0, work_network_1.parseWorkNetwork)(input.sandboxNetwork), runs) : runs;
69
79
  }
70
80
  exports.getRunNetwork = getRunNetwork;
81
+ /**
82
+ * The ports of the services of a service run as the server publishes them: on a host the
83
+ * ports of endpoints go to a free port of the loopback interface, where only the server
84
+ * reaches them, and no other port goes to the host, whatever the build file says (`up`
85
+ * would publish them on every interface, where two runs collide). On a cluster nothing is
86
+ * published, the server reaches the Service of each.
87
+ */
88
+ function publishEndpoints(workTree, endpoints, cluster) {
89
+ for (const service of Object.values(workTree.services)) {
90
+ if (!('ports' in service.data)) {
91
+ continue;
92
+ }
93
+ service.data.ports = service.data.ports.map((port) => !cluster &&
94
+ endpoints.some((endpoint) => endpoint.service === service.name && endpoint.port === port.containerPort)
95
+ ? { containerPort: port.containerPort, hostPort: 0, hostIp: '127.0.0.1' }
96
+ : { containerPort: port.containerPort, hostPort: null });
97
+ }
98
+ }
99
+ exports.publishEndpoints = publishEndpoints;
100
+ // Where the server reaches each endpoint, once every service the run keeps up is ready;
101
+ // null until then.
102
+ function getEndpointTargets(workTree, up, kubernetes) {
103
+ const services = Object.values(workTree.services).filter((service) => up.services.includes(service.name));
104
+ if (services.length < up.services.length || services.some((service) => service.state.current.type !== 'running')) {
105
+ return null;
106
+ }
107
+ const targets = [];
108
+ for (const endpoint of up.endpoints) {
109
+ const service = services.find((candidate) => candidate.name === endpoint.service);
110
+ const state = service?.state.current;
111
+ if (!service || state?.type !== 'running') {
112
+ return null;
113
+ }
114
+ if (kubernetes) {
115
+ targets.push({
116
+ service: endpoint.service,
117
+ port: endpoint.port,
118
+ address: { host: `${(0, resources_1.getResourceName)(service, kubernetes)}.${kubernetes.namespace}.svc`, port: endpoint.port },
119
+ });
120
+ continue;
121
+ }
122
+ const published = state.published?.find((candidate) => candidate.containerPort === endpoint.port);
123
+ if (!published) {
124
+ throw new Error(`${endpoint.service} is up, but its port ${endpoint.port} was not published`);
125
+ }
126
+ targets.push({
127
+ service: endpoint.service,
128
+ port: endpoint.port,
129
+ address: { host: published.host, port: published.port },
130
+ });
131
+ }
132
+ return targets;
133
+ }
134
+ exports.getEndpointTargets = getEndpointTargets;
71
135
  /**
72
136
  * Executes the tasks of a run with the engine of `hammerkit run`. The caller says which
73
137
  * process this is in: it reports what happens through `emit` and stops when `abortCtrl`
@@ -75,7 +139,9 @@ exports.getRunNetwork = getRunNetwork;
75
139
  */
76
140
  async function runTasks(input, cwd, processEnvs, abortCtrl,
77
141
  // whether the run was stopped from outside; the engine aborts too when a task fails
78
- cancelled, emit) {
142
+ cancelled, emit,
143
+ // a service run: its services are ready, at these addresses
144
+ up) {
79
145
  const secrets = (0, redact_1.createSecretRegistry)();
80
146
  const status = (0, work_item_status_1.statusConsole)((0, empty_writable_1.emptyWritable)(), secrets);
81
147
  // the process starts in a directory of the server, the tasks work on the run's files
@@ -126,7 +192,7 @@ cancelled, emit) {
126
192
  // why a task was not taken from the cache goes into its result
127
193
  explain: true,
128
194
  };
129
- for (const { name, scope } of (0, run_plan_1.getRunScopes)(input.tasks, options)) {
195
+ for (const { name, scope } of (0, run_plan_1.getRunScopes)(input.tasks, options, input.up?.services)) {
130
196
  const { ctx, scope: parsed } = await (0, schema_parser_1.createParseContext)(input.entry, environment, {
131
197
  directory: input.directory,
132
198
  });
@@ -135,17 +201,44 @@ cancelled, emit) {
135
201
  if (input.volumeScope && !input.kubernetes) {
136
202
  (0, hosted_volumes_1.scopeServiceVolumes)(workTree, input.volumeScope);
137
203
  }
204
+ if (input.up) {
205
+ publishEndpoints(workTree, input.up.endpoints, !!input.kubernetes);
206
+ }
138
207
  const cli = (0, cli_1.getCli)(workTree, environment);
139
- if (cli.tasks().length === 0) {
208
+ if (cli.tasks().length === 0 && (!input.up || cli.services().length === 0)) {
140
209
  throw new Error(`no tasks found for ${name}`);
141
210
  }
142
- const execution = cli.exec({ ...execOptions, workers: cli.defaultWorkers() });
211
+ const execution = input.up
212
+ ? cli.up({ ...execOptions, workers: cli.defaultWorkers(), daemon: false, wait: 'ready' })
213
+ : cli.exec({ ...execOptions, workers: cli.defaultWorkers() });
143
214
  const seen = new Map();
144
- const report = () => reportTasks(emit, execution.state.current, seen, (text) => secrets.redact(text));
215
+ let ready = false;
216
+ const report = () => {
217
+ reportTasks(emit, execution.state.current, seen, (text) => secrets.redact(text));
218
+ if (input.up && up && !ready) {
219
+ const targets = getEndpointTargets(execution.state.current, input.up, input.kubernetes);
220
+ if (targets) {
221
+ ready = true;
222
+ up(targets);
223
+ }
224
+ }
225
+ };
145
226
  const watch = execution.state.on('run-events', report);
146
227
  try {
147
228
  const result = await execution.start();
148
229
  report();
230
+ // on a cluster the services stay in their Deployments once they are ready, until
231
+ // the namespace of the run goes
232
+ if (input.up && input.kubernetes && ready && result.success && !abortCtrl.signal.aborted) {
233
+ await new Promise((resolve) => abortCtrl.signal.addEventListener('abort', () => resolve(), { once: true }));
234
+ return;
235
+ }
236
+ if (input.up && ready) {
237
+ if (!abortCtrl.signal.aborted) {
238
+ throw new Error('the services stopped on their own');
239
+ }
240
+ return;
241
+ }
149
242
  if (!result.success) {
150
243
  throw new Error(cancelled() ? 'the run was cancelled' : `${name} failed`);
151
244
  }
@@ -218,9 +311,13 @@ redact) {
218
311
  // The run is the first line of the standard input. The server keeps it open and writes
219
312
  // `cancel` on it to stop the run: that works the same on every platform, where a signal
220
313
  // on Windows ends the process without letting it stop its tasks.
314
+ // `onEnd` is told when the input ends before the run does: the server is gone.
221
315
  function readInput(stdin, cancel) {
222
316
  const lines = (0, readline_1.createInterface)({ input: stdin, crlfDelay: Infinity });
223
317
  let first;
318
+ let closing = false;
319
+ let ended = false;
320
+ let onEnd;
224
321
  const input = new Promise((resolve) => {
225
322
  lines.on('line', (line) => {
226
323
  if (first === undefined) {
@@ -231,10 +328,29 @@ function readInput(stdin, cancel) {
231
328
  cancel();
232
329
  }
233
330
  });
234
- lines.on('close', () => resolve(first ?? ''));
331
+ lines.on('close', () => {
332
+ resolve(first ?? '');
333
+ if (!closing) {
334
+ ended = true;
335
+ onEnd?.();
336
+ }
337
+ });
235
338
  });
236
- return { input, close: () => lines.close() };
339
+ return {
340
+ input,
341
+ close: () => {
342
+ closing = true;
343
+ lines.close();
344
+ },
345
+ onEnd: (listener) => {
346
+ onEnd = listener;
347
+ if (ended) {
348
+ listener();
349
+ }
350
+ },
351
+ };
237
352
  }
353
+ exports.readInput = readInput;
238
354
  /**
239
355
  * The process of one run: reads its input, executes the tasks in the current directory
240
356
  * and reports as lines of JSON. It exits 0 when it could say how the run ended, and the
@@ -261,8 +377,13 @@ async function workerMain(stdin, stdout, processEnvs, cwd, abortCtrl) {
261
377
  say({ kind: 'result', ok: false, error: `the run could not be read: ${e instanceof Error ? e.message : e}` });
262
378
  return;
263
379
  }
380
+ if (input.up) {
381
+ // services stay until they are stopped: without the server that would stop them, at
382
+ // their ttl or on a cancel, nothing would
383
+ reader.onEnd(stop);
384
+ }
264
385
  try {
265
- await runTasks(input, cwd, processEnvs, abortCtrl, () => cancelled, (event) => say({ kind: 'event', event }));
386
+ await runTasks(input, cwd, processEnvs, abortCtrl, () => cancelled, (event) => say({ kind: 'event', event }), (targets) => say({ kind: 'up', targets }));
266
387
  say({ kind: 'result', ok: true });
267
388
  }
268
389
  catch (e) {