hammerkit 1.27.0-next.3 → 1.27.0-next.30
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/build.schema.json +188 -0
- package/dist/cache/backends/local-cache-backend.js +6 -0
- package/dist/cache/backends/registry-cache-backend.js +2 -2
- package/dist/cache/explain.js +17 -0
- package/dist/cli.js +2 -0
- package/dist/docker/item-network.js +13 -5
- package/dist/executer/deadline.js +17 -3
- package/dist/executer/deferred-restore.js +36 -0
- package/dist/executer/docker-service.js +30 -8
- package/dist/executer/docker-task.js +21 -1
- package/dist/executer/event-cache.js +20 -2
- package/dist/executer/execute-work-service.js +2 -0
- package/dist/executer/execute-work-task.js +10 -3
- package/dist/executer/execution-steps.js +25 -4
- package/dist/executer/reset-work-tree.js +4 -0
- package/dist/executer/scheduler/enqueue-next.js +57 -6
- package/dist/executer/scheduler/state-key.js +42 -5
- package/dist/executer/watch-loop.js +4 -1
- package/dist/harness/connect.js +20 -6
- package/dist/harness/mcp-command.js +22 -6
- package/dist/harness/mcp.js +63 -114
- package/dist/harness/opencode.js +158 -50
- package/dist/harness/pi.js +29 -0
- package/dist/harness/sandbox-client.js +168 -17
- package/dist/harness/service-tools.js +141 -0
- package/dist/harness/task-tools.js +221 -0
- package/dist/kubernetes/apply.js +27 -16
- package/dist/kubernetes/await-running-state.js +59 -13
- package/dist/kubernetes/claim-layout.js +71 -0
- package/dist/kubernetes/cluster-state.js +245 -0
- package/dist/kubernetes/ensure-http-route.js +1 -1
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +35 -12
- package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +1 -1
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +6 -9
- package/dist/kubernetes/ensure-persistent-data.js +30 -20
- package/dist/kubernetes/follow-pod-logs.js +182 -0
- package/dist/kubernetes/get-service-ip.js +2 -2
- package/dist/kubernetes/job-placement.js +36 -12
- package/dist/kubernetes/job-secrets.js +33 -19
- package/dist/kubernetes/kept-claims.js +22 -10
- package/dist/kubernetes/network-policy.js +85 -27
- package/dist/kubernetes/remove-persistent-data.js +3 -1
- package/dist/kubernetes/resources.js +25 -3
- package/dist/kubernetes/restore-kubernetes-data.js +9 -3
- package/dist/kubernetes/sandbox-runner.js +628 -0
- package/dist/kubernetes/secrets.js +7 -11
- package/dist/kubernetes/volumes.js +6 -3
- package/dist/main.js +2 -0
- package/dist/network/egress-proxy.js +45 -6
- package/dist/optimizer/calculate-checksum.js +3 -2
- package/dist/optimizer/check-key.js +94 -0
- package/dist/optimizer/check-keys.js +64 -0
- package/dist/optimizer/get-work-cache-stats.js +17 -3
- package/dist/optimizer/work-task-cache-description.js +51 -1
- package/dist/planner/utils/append-work-dependencies.js +10 -5
- package/dist/planner/utils/append-work-service.js +19 -1
- package/dist/planner/utils/append-work-task.js +18 -3
- package/dist/planner/utils/get-work-runtime.js +6 -1
- package/dist/planner/utils/normalize-path.js +19 -1
- package/dist/planner/utils/parse-work-mount.js +7 -1
- package/dist/planner/utils/parse-work-port.js +4 -1
- package/dist/planner/utils/task-arguments.js +3 -0
- package/dist/planner/validate.js +22 -0
- package/dist/planner/work-network.js +3 -1
- package/dist/planner/work-runtime-aggregate.js +32 -0
- package/dist/planner/work-runtime-docker.js +2 -1
- package/dist/planner/work-runtime-kubernetes.js +124 -48
- package/dist/planner/work-runtime-local.js +2 -1
- package/dist/planner/work-task.js +6 -1
- package/dist/program.js +31 -10
- package/dist/remote/device-login.js +7 -2
- package/dist/remote/host-config.js +69 -3
- package/dist/remote/remote-commands.js +30 -7
- package/dist/remote/remote-work-commands.js +426 -45
- package/dist/remote/server-client.js +15 -2
- package/dist/remote/token-source.js +64 -26
- package/dist/run-program.js +9 -0
- package/dist/schema/build-file-base-task-schema.js +8 -1
- package/dist/schema/build-file-container-service-schema.js +12 -0
- package/dist/schema/build-file-container-task-schema.js +16 -1
- package/dist/schema/build-file-include-schema.js +18 -4
- package/dist/schema/build-file-kubernetes-service-schema.js +2 -0
- package/dist/schema/build-file-task-schema.js +5 -1
- package/dist/schema/capabilities-schema.js +56 -0
- package/dist/schema/reference-parser.js +120 -11
- package/dist/schema/schema-parser.js +92 -41
- package/dist/schema/work-scope-parser.js +33 -10
- package/dist/secrets/command-secret-provider.js +3 -1
- package/dist/secrets/github-app-token-command.js +3 -1
- package/dist/secrets/secret-catalog.js +43 -19
- package/dist/server/accounts-api.js +3 -1
- package/dist/server/approvals-api.js +1 -1
- package/dist/server/checks-api.js +83 -0
- package/dist/server/checks.js +116 -0
- package/dist/server/definitions-api.js +66 -3
- package/dist/server/endpoints.js +739 -0
- package/dist/server/executor.js +57 -5
- package/dist/server/kubernetes-backend.js +367 -65
- package/dist/server/kubernetes-sandbox-storage.js +48 -3
- package/dist/server/oauth.js +12 -1
- package/dist/server/plan.js +188 -103
- package/dist/server/policy.js +32 -2
- package/dist/server/repo-access.js +5 -0
- package/dist/server/run-plan.js +50 -19
- package/dist/server/run-request.js +58 -3
- package/dist/server/run-worker.js +245 -12
- package/dist/server/runs.js +410 -27
- package/dist/server/sandbox-pod.js +28 -0
- package/dist/server/sandbox-storage.js +23 -0
- package/dist/server/sandboxes-api.js +10 -4
- package/dist/server/sandboxes.js +8 -0
- package/dist/server/server-command.js +39 -20
- package/dist/server/server-config.js +255 -14
- package/dist/server/server.js +269 -15
- package/dist/server/service-runs.js +82 -0
- package/dist/utils/abort-on-signals.js +46 -6
- package/dist/utils/tar.js +36 -0
- package/dist/utils/terminal-prompt.js +53 -0
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ exports.getSandboxAffinity = exports.SANDBOX_LABEL = exports.getWorkspacePodSpec
|
|
|
4
4
|
const path_1 = require("path");
|
|
5
5
|
const fs_1 = require("fs");
|
|
6
6
|
const kept_claims_1 = require("./kept-claims");
|
|
7
|
+
const claim_layout_1 = require("./claim-layout");
|
|
7
8
|
function getVolumeName(service, kubernetes) {
|
|
8
9
|
return (0, kept_claims_1.getClaimName)(service, kubernetes);
|
|
9
10
|
}
|
|
@@ -50,7 +51,7 @@ async function getKubernetesPersistence(work, kubernetes) {
|
|
|
50
51
|
mounts: [],
|
|
51
52
|
initCommands: [],
|
|
52
53
|
};
|
|
53
|
-
const workspace = kubernetes
|
|
54
|
+
const workspace = kubernetes.server?.workspace;
|
|
54
55
|
for (const mount of work.data.mounts) {
|
|
55
56
|
if (workspace &&
|
|
56
57
|
appendWorkspaceMount(workspace, persistence, mount.containerPath, mount.localPath, mount.isFile, mount.readOnly)) {
|
|
@@ -83,6 +84,7 @@ async function getKubernetesPersistence(work, kubernetes) {
|
|
|
83
84
|
exports.getKubernetesPersistence = getKubernetesPersistence;
|
|
84
85
|
function appendVolume(sourceWork, persistence, containerPath, localPath, isFile, readOnly, kubernetes) {
|
|
85
86
|
const volumeName = getVolumeName(sourceWork, kubernetes);
|
|
87
|
+
const subPath = (0, claim_layout_1.getClaimSubPath)(sourceWork, containerPath);
|
|
86
88
|
const existVolume = persistence.volumes.some((v) => v.name === volumeName);
|
|
87
89
|
if (!existVolume) {
|
|
88
90
|
persistence.volumes.push({
|
|
@@ -92,7 +94,7 @@ function appendVolume(sourceWork, persistence, containerPath, localPath, isFile,
|
|
|
92
94
|
},
|
|
93
95
|
});
|
|
94
96
|
persistence.sizes[volumeName] = 'volumeSize' in sourceWork.data ? sourceWork.data.volumeSize : null;
|
|
95
|
-
if (
|
|
97
|
+
if ((0, kept_claims_1.isKeptClaim)(sourceWork, kubernetes)) {
|
|
96
98
|
persistence.kept.push(volumeName);
|
|
97
99
|
}
|
|
98
100
|
}
|
|
@@ -101,7 +103,7 @@ function appendVolume(sourceWork, persistence, containerPath, localPath, isFile,
|
|
|
101
103
|
readOnly,
|
|
102
104
|
mountPath: containerPath,
|
|
103
105
|
name: volumeName,
|
|
104
|
-
subPath
|
|
106
|
+
subPath,
|
|
105
107
|
},
|
|
106
108
|
uploadPath: `/dev/hammerkit/${sourceWork.id()}`,
|
|
107
109
|
cwd: isFile ? (0, path_1.dirname)(containerPath) : containerPath,
|
|
@@ -111,6 +113,7 @@ function appendVolume(sourceWork, persistence, containerPath, localPath, isFile,
|
|
|
111
113
|
localPath: localPath,
|
|
112
114
|
matcher: () => true, // TODO
|
|
113
115
|
containerPath,
|
|
116
|
+
subPath,
|
|
114
117
|
stateKey: '', // TODO
|
|
115
118
|
});
|
|
116
119
|
}
|
package/dist/main.js
CHANGED
|
@@ -8,6 +8,7 @@ const redact_1 = require("./utils/redact");
|
|
|
8
8
|
const empty_writable_1 = require("./utils/empty-writable");
|
|
9
9
|
const run_program_1 = require("./run-program");
|
|
10
10
|
const abort_on_signals_1 = require("./utils/abort-on-signals");
|
|
11
|
+
const terminal_prompt_1 = require("./utils/terminal-prompt");
|
|
11
12
|
const abortCtrl = new AbortController();
|
|
12
13
|
(0, abort_on_signals_1.abortOnSignals)(process, abortCtrl);
|
|
13
14
|
let settled = false;
|
|
@@ -32,6 +33,7 @@ const secrets = (0, redact_1.createSecretRegistry)();
|
|
|
32
33
|
stdout: process.stdout,
|
|
33
34
|
stderr: process.stderr,
|
|
34
35
|
stdoutColumns: process.stdout.columns,
|
|
36
|
+
prompt: (0, terminal_prompt_1.getTerminalPrompt)(process.env, abortCtrl.signal),
|
|
35
37
|
}, process.argv, false).then(() => {
|
|
36
38
|
settled = true;
|
|
37
39
|
}, () => {
|
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
// `CONNECT` tunnels to an allowed host pass, everything else gets a 403 and a
|
|
6
6
|
// `refused` event on stdout. For each `host:port` entry it also listens on that
|
|
7
7
|
// port and forwards raw TCP to that host, which the item reaches under the
|
|
8
|
-
// host's name.
|
|
8
|
+
// host's name. On a server's cluster it also lets the init containers that read
|
|
9
|
+
// the task's secrets reach the endpoints of their providers: a tunnel to port 443
|
|
10
|
+
// that carries the proxy's token, which those containers get and the task not. The script is passed to `node -e`, so the proxy
|
|
9
11
|
// needs no image of its own, only a stock Node image.
|
|
10
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
-
exports.formatEgressProxyEvent = exports.formatRefusedHost = exports.parseEgressProxyEvent = exports.getProxylessEnvironment = exports.getProxyClientEnvironment = exports.getEgressProxyPorts = exports.getItemProxyEnvironment = exports.getEgressProxyEnvironment = exports.getEgressProxyCommand = exports.EGRESS_PROXY_SCRIPT = exports.EGRESS_PROXY_HOST = exports.EGRESS_PROXY_PORT = exports.EGRESS_PROXY_IMAGE = void 0;
|
|
13
|
+
exports.formatEgressProxyEvent = exports.formatRefusedHost = exports.parseEgressProxyEvent = exports.getProxylessEnvironment = exports.getProxyClientEnvironment = exports.getEgressProxyPorts = exports.getItemProxyEnvironment = exports.getSecretProxyEnvironment = exports.getEgressProxyEnvironment = exports.getEgressProxyCommand = exports.EGRESS_PROXY_SCRIPT = exports.EGRESS_PROXY_HOST = exports.EGRESS_PROXY_PORT = exports.EGRESS_PROXY_IMAGE = void 0;
|
|
12
14
|
const work_network_1 = require("../planner/work-network");
|
|
13
15
|
const network_schema_1 = require("../schema/network-schema");
|
|
14
16
|
// Pinned, so the code behind the boundary is the same on every host. To bump
|
|
@@ -25,6 +27,7 @@ exports.EGRESS_PROXY_SCRIPT = `
|
|
|
25
27
|
const http = require('http')
|
|
26
28
|
const net = require('net')
|
|
27
29
|
const dns = require('dns')
|
|
30
|
+
const crypto = require('crypto')
|
|
28
31
|
const entries = JSON.parse(process.env.HAMMERKIT_ALLOW || '[]').map((h) => h.toLowerCase())
|
|
29
32
|
// names and patterns for HTTP(S)
|
|
30
33
|
const allow = entries.filter((e) => !e.includes(':'))
|
|
@@ -38,6 +41,9 @@ const publicOnly = JSON.parse(process.env.HAMMERKIT_PUBLIC_ONLY || '[]').map((h)
|
|
|
38
41
|
// the names of the needed services, for tools that ignore NO_PROXY: exact
|
|
39
42
|
// names only, never patterns
|
|
40
43
|
const needs = JSON.parse(process.env.HAMMERKIT_NEEDS || '[]').map((h) => h.toLowerCase())
|
|
44
|
+
// the endpoints of secret providers, for a request with the token only
|
|
45
|
+
const secretAllow = JSON.parse(process.env.HAMMERKIT_SECRET_ALLOW || '[]').map((h) => h.toLowerCase())
|
|
46
|
+
const secretToken = process.env.HAMMERKIT_SECRET_TOKEN || ''
|
|
41
47
|
const port = parseInt(process.env.HAMMERKIT_PROXY_PORT || '${exports.EGRESS_PROXY_PORT}', 10)
|
|
42
48
|
// the name whose address the proxy listens on, every address when unset
|
|
43
49
|
const bind = process.env.HAMMERKIT_PROXY_BIND
|
|
@@ -64,6 +70,31 @@ function allowedBy(host, port) {
|
|
|
64
70
|
}
|
|
65
71
|
return allow.some((rule) => rule.startsWith('*.') && name.endsWith(rule.substring(1))) ? 'wildcard' : null
|
|
66
72
|
}
|
|
73
|
+
// Whether the request carries the token as the password of Proxy-Authorization (Basic).
|
|
74
|
+
function hasSecretToken(req) {
|
|
75
|
+
if (secretToken.length === 0) {
|
|
76
|
+
return false
|
|
77
|
+
}
|
|
78
|
+
const match = /^basic\\s+(\\S+)$/i.exec(String(req.headers['proxy-authorization'] || ''))
|
|
79
|
+
if (!match) {
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
const credentials = Buffer.from(match[1], 'base64').toString('utf8')
|
|
83
|
+
const colon = credentials.indexOf(':')
|
|
84
|
+
if (colon < 0) {
|
|
85
|
+
return false
|
|
86
|
+
}
|
|
87
|
+
const given = Buffer.from(credentials.substring(colon + 1), 'utf8')
|
|
88
|
+
const expected = Buffer.from(secretToken, 'utf8')
|
|
89
|
+
return given.length === expected.length && crypto.timingSafeEqual(given, expected)
|
|
90
|
+
}
|
|
91
|
+
// the rule of a tunnel to an endpoint of a secret provider: like a wildcard, it reaches
|
|
92
|
+
// no private address
|
|
93
|
+
function allowedForSecrets(req, host, port) {
|
|
94
|
+
const name = host.toLowerCase().replace(/\\.$/, '')
|
|
95
|
+
const listed = secretAllow.some((rule) => (rule.startsWith('*.') ? name.endsWith(rule.substring(1)) : name === rule))
|
|
96
|
+
return listed && port === 443 && hasSecretToken(req) ? 'wildcard' : null
|
|
97
|
+
}
|
|
67
98
|
function ipv4(address) {
|
|
68
99
|
const mapped = /^::ffff:(\\d+\\.\\d+\\.\\d+\\.\\d+)$/i.exec(address)
|
|
69
100
|
const value = mapped ? mapped[1] : address
|
|
@@ -197,7 +228,7 @@ const server = http.createServer((req, res) => {
|
|
|
197
228
|
server.on('connect', (req, socket, head) => {
|
|
198
229
|
const target = parseAuthority(req.url || '')
|
|
199
230
|
socket.on('error', () => socket.destroy())
|
|
200
|
-
const rule = target ? allowedBy(target.host, target.port) : null
|
|
231
|
+
const rule = target ? allowedBy(target.host, target.port) || allowedForSecrets(req, target.host, target.port) : null
|
|
201
232
|
if (!rule) {
|
|
202
233
|
refuse(target ? target.host : String(req.url))
|
|
203
234
|
socket.end('HTTP/1.1 403 Forbidden\\r\\ncontent-type: text/plain\\r\\n\\r\\nhammerkit: network access is not allowed\\n')
|
|
@@ -290,7 +321,8 @@ exports.getEgressProxyCommand = getEgressProxyCommand;
|
|
|
290
321
|
// `needs` are the names of the needed services, allowed as exactly those
|
|
291
322
|
// names. `bind` is the name the proxy listens on alone, so that nothing on its
|
|
292
323
|
// other networks reaches it. `publicOnly` are the entries of `hosts` that reach
|
|
293
|
-
// no private address (see WorkNetwork).
|
|
324
|
+
// no private address (see WorkNetwork). The endpoints of secret providers and
|
|
325
|
+
// the token they need are set by the caller (getSecretProxyEnvironment).
|
|
294
326
|
function getEgressProxyEnvironment(hosts, needs, bind, publicOnly = []) {
|
|
295
327
|
return {
|
|
296
328
|
HAMMERKIT_ALLOW: JSON.stringify(hosts),
|
|
@@ -301,6 +333,12 @@ function getEgressProxyEnvironment(hosts, needs, bind, publicOnly = []) {
|
|
|
301
333
|
};
|
|
302
334
|
}
|
|
303
335
|
exports.getEgressProxyEnvironment = getEgressProxyEnvironment;
|
|
336
|
+
// The endpoints of secret providers the proxy lets a tunnel with its token reach; the
|
|
337
|
+
// token itself is set where the proxy runs, see HAMMERKIT_SECRET_TOKEN.
|
|
338
|
+
function getSecretProxyEnvironment(hosts) {
|
|
339
|
+
return hosts.length > 0 ? { HAMMERKIT_SECRET_ALLOW: JSON.stringify(hosts) } : {};
|
|
340
|
+
}
|
|
341
|
+
exports.getSecretProxyEnvironment = getSecretProxyEnvironment;
|
|
304
342
|
// The proxy of a task or service: the hosts of its network in this run (under
|
|
305
343
|
// the run's ceiling), and the names of the services it needs, on every runtime.
|
|
306
344
|
function getItemProxyEnvironment(item, bind) {
|
|
@@ -315,8 +353,9 @@ function getEgressProxyPorts(network) {
|
|
|
315
353
|
return [exports.EGRESS_PROXY_PORT, ...(0, work_network_1.getTcpForwards)(network).map((forward) => forward.port)];
|
|
316
354
|
}
|
|
317
355
|
exports.getEgressProxyPorts = getEgressProxyPorts;
|
|
318
|
-
|
|
319
|
-
|
|
356
|
+
// `token` is sent as the password, for the endpoints of secret providers.
|
|
357
|
+
function getProxyClientEnvironment(directHosts, token) {
|
|
358
|
+
const proxy = `http://${token !== undefined ? `hammerkit:${token}@` : ''}${exports.EGRESS_PROXY_HOST}:${exports.EGRESS_PROXY_PORT}`;
|
|
320
359
|
const noProxy = ['localhost', '127.0.0.1', ...directHosts].join(',');
|
|
321
360
|
return {
|
|
322
361
|
HTTP_PROXY: proxy,
|
|
@@ -4,9 +4,10 @@ exports.calculateChecksum = void 0;
|
|
|
4
4
|
const crypto_1 = require("crypto");
|
|
5
5
|
// Hashes the raw bytes: decoding to text first would map every invalid UTF-8
|
|
6
6
|
// sequence to U+FFFD, so two different binary files could share a checksum.
|
|
7
|
-
// For UTF-8 text the result is unchanged.
|
|
7
|
+
// For UTF-8 text the result is unchanged. sha256, which nobody can make two files share:
|
|
8
|
+
// a cache hit is also what a verified check counts (check-key.ts).
|
|
8
9
|
async function calculateChecksum(environment, path) {
|
|
9
|
-
const hash = (0, crypto_1.createHash)('
|
|
10
|
+
const hash = (0, crypto_1.createHash)('sha256');
|
|
10
11
|
for await (const chunk of environment.file.readStream(path)) {
|
|
11
12
|
hash.update(chunk);
|
|
12
13
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getCheckKey = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const state_key_1 = require("../executer/scheduler/state-key");
|
|
6
|
+
const get_work_cache_stats_1 = require("./get-work-cache-stats");
|
|
7
|
+
const work_task_cache_description_1 = require("./work-task-cache-description");
|
|
8
|
+
/**
|
|
9
|
+
* What a passed check is recorded under, and what `remote verify` looks it up by: the
|
|
10
|
+
* inputs of the task's cache key, so a cached result counts for the same key it was
|
|
11
|
+
* stored under. Its definition (the services it needs included), the files of its `src`,
|
|
12
|
+
* the keys of its dependencies, and the sources of the services it needs and of the tasks
|
|
13
|
+
* they depend on.
|
|
14
|
+
*
|
|
15
|
+
* It is computed on a checkout before anything ran in it, on both ends, so it also covers
|
|
16
|
+
* the files a commit holds where a dependency generates (the cache leaves those out, as
|
|
17
|
+
* they change once the dependency ran), and it leaves out `.git`, which is no file of a
|
|
18
|
+
* commit. Two checkouts of the same files give the same key wherever they are.
|
|
19
|
+
* Null for a task that can not be proven up to date (no `src` of its own, or none that
|
|
20
|
+
* matches a file, here or in a dependency): its result says nothing about any files.
|
|
21
|
+
*/
|
|
22
|
+
async function getCheckKey(item, environment, memo = new Map(), path = new Set()) {
|
|
23
|
+
return memoized(memo, path, `task\0${item.id()}`, (inner) => computeCheckKey(item, environment, memo, inner));
|
|
24
|
+
}
|
|
25
|
+
exports.getCheckKey = getCheckKey;
|
|
26
|
+
async function computeCheckKey(item, environment, memo, path) {
|
|
27
|
+
const { stats, unmatched } = await (0, get_work_cache_stats_1.getWorkItemCheckoutStats)(item, environment);
|
|
28
|
+
if (!(0, state_key_1.isOwnProvable)(item, unmatched)) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const inputs = await getInputKeys(item, environment, memo, path);
|
|
32
|
+
if (inputs === null) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return hash({ task: (0, work_task_cache_description_1.getWorkTaskCacheDescription)(item), files: commitFiles(stats), ...inputs });
|
|
36
|
+
}
|
|
37
|
+
// The sources of a needed service, with the keys of what it depends on and needs; its
|
|
38
|
+
// definition is in the description of the task that needs it.
|
|
39
|
+
function getNeedKey(item, environment, memo, path) {
|
|
40
|
+
return memoized(memo, path, `need\0${item.name}`, async (inner) => {
|
|
41
|
+
const inputs = await getInputKeys(item, environment, memo, inner);
|
|
42
|
+
if (inputs === null) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return hash({ files: commitFiles(await (0, get_work_cache_stats_1.getWorkCacheStats)(item.data, environment)), ...inputs });
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async function getInputKeys(item, environment, memo, path) {
|
|
49
|
+
const deps = [];
|
|
50
|
+
for (const dep of item.deps) {
|
|
51
|
+
const key = await getCheckKey(dep, environment, memo, path);
|
|
52
|
+
if (key === null) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
deps.push(key);
|
|
56
|
+
}
|
|
57
|
+
const needs = [];
|
|
58
|
+
for (const need of item.needs) {
|
|
59
|
+
const key = await getNeedKey(need.service, environment, memo, path);
|
|
60
|
+
if (key === null) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
needs.push(`${need.name}=${key}`);
|
|
64
|
+
}
|
|
65
|
+
return { deps: deps.sort(), needs: needs.sort() };
|
|
66
|
+
}
|
|
67
|
+
// a commit never holds `.git`: what a checkout keeps there differs between a CI job's clone,
|
|
68
|
+
// the server's fetch and a sandbox's copy, which has none
|
|
69
|
+
function commitFiles(stats) {
|
|
70
|
+
return Object.keys(stats.files)
|
|
71
|
+
.filter((path) => !path.split('/').includes('.git'))
|
|
72
|
+
.sort()
|
|
73
|
+
.map((path) => [path, stats.files[path].checksum]);
|
|
74
|
+
}
|
|
75
|
+
function hash(value) {
|
|
76
|
+
return (0, crypto_1.createHash)('sha256').update(JSON.stringify(value)).digest('hex');
|
|
77
|
+
}
|
|
78
|
+
// `path` holds the keys being computed on the way here: one coming back is a loop (services
|
|
79
|
+
// that need each other), which can not be proven and fails to plan anyway. Waiting on its
|
|
80
|
+
// pending promise would never end. The keys are computed one after another, so every pending
|
|
81
|
+
// promise in the memo is on the path.
|
|
82
|
+
function memoized(memo, path, key, compute) {
|
|
83
|
+
if (path.has(key)) {
|
|
84
|
+
return Promise.resolve(null);
|
|
85
|
+
}
|
|
86
|
+
const known = memo.get(key);
|
|
87
|
+
if (known) {
|
|
88
|
+
return known;
|
|
89
|
+
}
|
|
90
|
+
const computed = compute(new Set([...path, key]));
|
|
91
|
+
memo.set(key, computed);
|
|
92
|
+
return computed;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=check-key.js.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getCheckKeys = void 0;
|
|
4
|
+
const plan_work_tasks_1 = require("../planner/utils/plan-work-tasks");
|
|
5
|
+
const reference_parser_1 = require("../schema/reference-parser");
|
|
6
|
+
const schema_parser_1 = require("../schema/schema-parser");
|
|
7
|
+
const work_scope_parser_1 = require("../schema/work-scope-parser");
|
|
8
|
+
const log_1 = require("../log");
|
|
9
|
+
const check_key_1 = require("./check-key");
|
|
10
|
+
/**
|
|
11
|
+
* The check keys of the tasks of a scope, as both ends compute them: the process of a run on
|
|
12
|
+
* the server before it executes anything, and `remote verify` in a CI job's checkout. So the
|
|
13
|
+
* build file is read the same way on both, as a server reads it (no `.env`, links are files)
|
|
14
|
+
* but placed nowhere: where a server runs a task (a pool, its tool image, the arch of its
|
|
15
|
+
* nodes) is the server's, and a result counts for the server it passed on. The secrets'
|
|
16
|
+
* providers and accounts are the server's too, and no part of a key, so any is taken here.
|
|
17
|
+
*/
|
|
18
|
+
async function getCheckKeys(entry, environment, scope) {
|
|
19
|
+
const keyed = { ...environment, hosted: { catalog: anyCatalog(), localTasksInContainers: true } };
|
|
20
|
+
const { ctx, scope: parsed } = await (0, schema_parser_1.createParseContext)(entry, keyed);
|
|
21
|
+
const workTree = (0, work_scope_parser_1.getWorkContext)(await (0, reference_parser_1.parseReferences)(ctx, parsed, keyed), scope, keyed);
|
|
22
|
+
const memo = new Map();
|
|
23
|
+
const keys = {};
|
|
24
|
+
for (const task of (0, plan_work_tasks_1.iterateWorkTasks)(workTree)) {
|
|
25
|
+
try {
|
|
26
|
+
const key = await (0, check_key_1.getCheckKey)(task, keyed, memo);
|
|
27
|
+
keys[task.name] = key
|
|
28
|
+
? { key }
|
|
29
|
+
: {
|
|
30
|
+
key: null,
|
|
31
|
+
reason: 'it, or a task it or a service it needs depends on, declares no src that matches a file',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
// a secret whose value is part of the key (`cache: true`) is not known here
|
|
36
|
+
keys[task.name] = { key: null, reason: (0, log_1.getErrorMessage)(e) };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return keys;
|
|
40
|
+
}
|
|
41
|
+
exports.getCheckKeys = getCheckKeys;
|
|
42
|
+
// Every provider and account name resolves, to a definition nothing runs.
|
|
43
|
+
function anyCatalog() {
|
|
44
|
+
const provider = (name) => ({
|
|
45
|
+
name,
|
|
46
|
+
type: 'command',
|
|
47
|
+
command: [],
|
|
48
|
+
env: {},
|
|
49
|
+
timeoutMs: 0,
|
|
50
|
+
declaredIn: 'the server',
|
|
51
|
+
});
|
|
52
|
+
const account = (name) => ({
|
|
53
|
+
name,
|
|
54
|
+
default: false,
|
|
55
|
+
providers: new Proxy({}, { get: () => ({ env: {} }) }),
|
|
56
|
+
declaredIn: 'the server',
|
|
57
|
+
});
|
|
58
|
+
return {
|
|
59
|
+
providers: new Proxy({}, { get: (_target, name) => (typeof name === 'string' ? provider(name) : undefined) }),
|
|
60
|
+
accounts: new Proxy({}, { get: (_target, name) => (typeof name === 'string' ? account(name) : undefined) }),
|
|
61
|
+
explicitAccounts: true,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=check-keys.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getStateKey = exports.getWorkItemCacheStats = exports.getWorkCacheStats = void 0;
|
|
3
|
+
exports.stateKeyDigest = exports.getStateKey = exports.getWorkItemCheckoutStats = exports.getWorkItemCacheStats = exports.getWorkCacheStats = void 0;
|
|
4
4
|
const calculate_checksum_1 = require("./calculate-checksum");
|
|
5
5
|
const path_1 = require("path");
|
|
6
6
|
const crypto_1 = require("crypto");
|
|
@@ -20,7 +20,7 @@ async function addWorkCacheStats(result, cwd, path, matcher, context, root, excl
|
|
|
20
20
|
if (!matcher(path)) {
|
|
21
21
|
return 0;
|
|
22
22
|
}
|
|
23
|
-
const checksum = (0, crypto_1.createHash)('
|
|
23
|
+
const checksum = (0, crypto_1.createHash)('sha256')
|
|
24
24
|
.update(`link:${await (0, promises_1.readlink)(path)}`)
|
|
25
25
|
.digest('hex');
|
|
26
26
|
result.files[(0, path_1.relative)(cwd, path).split(path_1.sep).join(path_1.posix.sep)] = { lastModified: link.mtimeMs, checksum };
|
|
@@ -85,6 +85,13 @@ function getWorkItemCacheStats(item, environment) {
|
|
|
85
85
|
return collectSourceStats(item.data, environment, dependencyOutputs(item));
|
|
86
86
|
}
|
|
87
87
|
exports.getWorkItemCacheStats = getWorkItemCacheStats;
|
|
88
|
+
// The source stats of a task with what its dependencies generate: on a checkout nothing ran
|
|
89
|
+
// in, those are the files a commit holds there, see check-key.ts.
|
|
90
|
+
async function getWorkItemCheckoutStats(item, environment) {
|
|
91
|
+
const { stats } = await collectSourceStats(item.data, environment, []);
|
|
92
|
+
return { stats, unmatched: (await getWorkItemCacheStats(item, environment)).unmatched };
|
|
93
|
+
}
|
|
94
|
+
exports.getWorkItemCheckoutStats = getWorkItemCheckoutStats;
|
|
88
95
|
function dependencyOutputs(item) {
|
|
89
96
|
const outputs = new Set();
|
|
90
97
|
const visited = new Set();
|
|
@@ -113,7 +120,14 @@ function getStateKey(stats, cacheMethod) {
|
|
|
113
120
|
contents.push(`${key}:${stats.files[key].lastModified}`);
|
|
114
121
|
}
|
|
115
122
|
}
|
|
116
|
-
return (
|
|
123
|
+
return stateKeyDigest(contents.join(','));
|
|
117
124
|
}
|
|
118
125
|
exports.getStateKey = getStateKey;
|
|
126
|
+
// A state key is also the value of the `hammerkit.dev/state` label on Kubernetes, which
|
|
127
|
+
// takes at most 63 characters: the sha256 in base 36 has 50.
|
|
128
|
+
function stateKeyDigest(content) {
|
|
129
|
+
const hex = (0, crypto_1.createHash)('sha256').update(content).digest('hex');
|
|
130
|
+
return BigInt(`0x${hex}`).toString(36).padStart(50, '0');
|
|
131
|
+
}
|
|
132
|
+
exports.stateKeyDigest = stateKeyDigest;
|
|
119
133
|
//# sourceMappingURL=get-work-cache-stats.js.map
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.storedCacheDescription = exports.getWorkTaskCacheDescription = void 0;
|
|
3
|
+
exports.storedCacheDescription = exports.getWorkServiceCacheIdentity = exports.getWorkTaskCacheDescription = void 0;
|
|
4
4
|
const os_1 = require("os");
|
|
5
5
|
const crypto_1 = require("crypto");
|
|
6
6
|
const replace_env_variables_1 = require("../environment/replace-env-variables");
|
|
7
7
|
const portable_path_1 = require("../planner/utils/portable-path");
|
|
8
8
|
const work_secret_1 = require("../planner/work-secret");
|
|
9
|
+
const claim_layout_1 = require("../kubernetes/claim-layout");
|
|
10
|
+
const work_service_1 = require("../planner/work-service");
|
|
11
|
+
const work_service_id_1 = require("../planner/work-service-id");
|
|
12
|
+
const work_cache_id_1 = require("../planner/work-cache-id");
|
|
9
13
|
// Every path is made project-relative (portablePath) so the description, and the
|
|
10
14
|
// task id hashed from it, is identical wherever the project is checked out.
|
|
11
15
|
//
|
|
@@ -40,9 +44,55 @@ function getWorkTaskCacheDescription(item) {
|
|
|
40
44
|
cwd: portable(task.cwd),
|
|
41
45
|
deps: item.deps.length > 0 ? item.deps.map((dep) => dep.id()).sort() : undefined,
|
|
42
46
|
secrets: cachedSecrets.length > 0 ? cachedSecrets : undefined,
|
|
47
|
+
claimLayout: (0, claim_layout_1.hasPathsEndingAlike)(item) || (0, claim_layout_1.hasOutputsFlatteningAlike)(item) ? 2 : undefined,
|
|
48
|
+
needs: item.needs.length > 0 ? getNeedsIdentities(item) : undefined,
|
|
43
49
|
};
|
|
44
50
|
}
|
|
45
51
|
exports.getWorkTaskCacheDescription = getWorkTaskCacheDescription;
|
|
52
|
+
// `visiting` holds the services on the way here: services that need each other in a loop
|
|
53
|
+
// fail to plan (checkForLoop), the identity only has to come back to say so
|
|
54
|
+
function getNeedsIdentities(item, visiting = new Set()) {
|
|
55
|
+
return (item.needs
|
|
56
|
+
// an init task needs the service it initializes, whose identity holds the init's id
|
|
57
|
+
.filter((need) => !isInitOf(item, need.service))
|
|
58
|
+
.map((need) => visiting.has(need.service)
|
|
59
|
+
? `${need.name}=loop`
|
|
60
|
+
: `${need.name}=${getWorkServiceCacheIdentity(need.service, visiting)}`)
|
|
61
|
+
.sort());
|
|
62
|
+
}
|
|
63
|
+
function isInitOf(item, service) {
|
|
64
|
+
return (0, work_service_1.isContainerWorkService)(service.data) && service.data.init?.task.data === item.data;
|
|
65
|
+
}
|
|
66
|
+
// Everything that makes a service what a task needing it talks to: its definition, the ids of
|
|
67
|
+
// the tasks it depends on and of its init task, and the services it needs in turn. The source
|
|
68
|
+
// of those tasks and of the service is in the state key (computeStateKey), like a dependency's.
|
|
69
|
+
// Not the service id, which leaves out what a service runs (getWorkServiceId).
|
|
70
|
+
function getWorkServiceCacheIdentity(item, visiting = new Set()) {
|
|
71
|
+
const service = item.data;
|
|
72
|
+
const definition = (0, work_service_1.isContainerWorkService)(service)
|
|
73
|
+
? {
|
|
74
|
+
cmd: service.cmd?.cmd ?? null,
|
|
75
|
+
envs: (0, replace_env_variables_1.getEnvironmentVariables)(service.envs),
|
|
76
|
+
healthcheck: service.healthcheck?.cmd.cmd ?? null,
|
|
77
|
+
secrets: service.secrets
|
|
78
|
+
.filter((secret) => secret.cache)
|
|
79
|
+
.map((secret) => `${(0, work_secret_1.getPortableSecretName)(service.projectRoot, secret)}=${secret.digest()}`)
|
|
80
|
+
.sort(),
|
|
81
|
+
// as declared: arguments after -- of a run of the init leave the service as it is
|
|
82
|
+
init: service.init ? (0, work_cache_id_1.getDeclaredWorkTaskId)(service.init.task) : null,
|
|
83
|
+
}
|
|
84
|
+
: { namespace: service.namespace };
|
|
85
|
+
return (0, crypto_1.createHash)('sha256')
|
|
86
|
+
.update(JSON.stringify({
|
|
87
|
+
id: (0, work_service_id_1.getWorkServiceId)(service),
|
|
88
|
+
...definition,
|
|
89
|
+
ports: service.ports.map((port) => `${port.hostPort ?? ''}:${port.containerPort}`).sort(),
|
|
90
|
+
deps: item.deps.map((dep) => dep.id()).sort(),
|
|
91
|
+
needs: getNeedsIdentities(item, new Set([...visiting, item])),
|
|
92
|
+
}))
|
|
93
|
+
.digest('hex');
|
|
94
|
+
}
|
|
95
|
+
exports.getWorkServiceCacheIdentity = getWorkServiceCacheIdentity;
|
|
46
96
|
// The form written to disk: description.json in a cache entry (pushed to remote
|
|
47
97
|
// backends, readable by everyone who can pull) and the local explain record.
|
|
48
98
|
// Env is where tokens get passed, so each value is stored as a digest; the task
|
|
@@ -17,9 +17,14 @@ function appendWorkDependencies(workTree, referenced, item, environment, context
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
// an aggregate hands on what its dependencies generate, as if what depends on it
|
|
21
|
+
// depended on them
|
|
22
|
+
if (((0, work_task_1.isContainerWorkTask)(depNode.data) || (0, work_service_1.isContainerWorkService)(depNode.data) || (0, work_task_1.isAggregateTask)(depNode.data)) &&
|
|
23
|
+
((0, work_task_1.isContainerWorkTask)(item.data) || (0, work_service_1.isContainerWorkService)(item.data) || (0, work_task_1.isAggregateTask)(item.data))) {
|
|
24
|
+
// the outputs of the dep's own deps too, which stay those of the task that made them
|
|
25
|
+
// (on Kubernetes, they are in its claim)
|
|
22
26
|
for (const generate of depNode.data.generates) {
|
|
27
|
+
const inherited = generate.inherited ?? depNode;
|
|
23
28
|
if (generate.isFile) {
|
|
24
29
|
// TODO check if thats correct
|
|
25
30
|
continue;
|
|
@@ -31,12 +36,12 @@ function appendWorkDependencies(workTree, referenced, item, environment, context
|
|
|
31
36
|
export: false,
|
|
32
37
|
resetOnChange: false,
|
|
33
38
|
containerPath: generate.path,
|
|
34
|
-
inherited
|
|
39
|
+
inherited,
|
|
35
40
|
readOnly: false,
|
|
36
41
|
});
|
|
37
42
|
}
|
|
38
43
|
}
|
|
39
|
-
else {
|
|
44
|
+
else if ((0, work_task_1.isWorkTask)(item.data)) {
|
|
40
45
|
if (!item.data.generates.some((v) => v.volumeName === generate.volumeName)) {
|
|
41
46
|
item.data.generates.push({
|
|
42
47
|
volumeName: generate.volumeName,
|
|
@@ -44,7 +49,7 @@ function appendWorkDependencies(workTree, referenced, item, environment, context
|
|
|
44
49
|
exportAlways: false,
|
|
45
50
|
resetOnChange: false,
|
|
46
51
|
path: generate.path,
|
|
47
|
-
inherited
|
|
52
|
+
inherited,
|
|
48
53
|
isFile: false,
|
|
49
54
|
});
|
|
50
55
|
}
|
|
@@ -118,8 +118,10 @@ function parseService(workTree, service, environment, context) {
|
|
|
118
118
|
secrets: (0, work_secret_1.parseWorkSecrets)(service.cwd, context.projectRoot, service.schema.secrets, envs, environment, context.secrets, service.schema.account),
|
|
119
119
|
resources: (0, work_resources_1.parseWorkResources)(service.schema.resources),
|
|
120
120
|
volumeSize: service.schema.volumeSize ?? null,
|
|
121
|
+
restarts: service.schema.restarts ?? null,
|
|
121
122
|
network: (0, work_network_1.parseWorkNetwork)(service.schema.network),
|
|
122
123
|
networkCeiling: null,
|
|
124
|
+
args: service.schema.args ?? null,
|
|
123
125
|
};
|
|
124
126
|
}
|
|
125
127
|
}
|
|
@@ -142,7 +144,11 @@ function appendServiceInit(workTree, service, item, environment, context) {
|
|
|
142
144
|
task.needs[selfNeed] = { name: task.needs[selfNeed].name, service: view };
|
|
143
145
|
}
|
|
144
146
|
else {
|
|
145
|
-
|
|
147
|
+
const name = getInitNeedName(service);
|
|
148
|
+
if (task.needs.some((need) => need.name === name)) {
|
|
149
|
+
throw new Error(`${task.name} needs two services as ${name}, give one another name with name:`);
|
|
150
|
+
}
|
|
151
|
+
task.needs.push({ name, service: view });
|
|
146
152
|
}
|
|
147
153
|
removeRequirer(item, task.name);
|
|
148
154
|
// explicit, so neither the default method nor `--cache` turns it back on
|
|
@@ -152,6 +158,18 @@ function appendServiceInit(workTree, service, item, environment, context) {
|
|
|
152
158
|
}
|
|
153
159
|
item.data.init = { task, state };
|
|
154
160
|
}
|
|
161
|
+
// The host name the init reaches its service by: the name its task declares the
|
|
162
|
+
// need by, else the service's name in its own build file. Planning the task
|
|
163
|
+
// skips that need (it is the init's), and the full name of a service of an
|
|
164
|
+
// include, `services:db`, is no host name: Docker and DNS take no colon.
|
|
165
|
+
function getInitNeedName(service) {
|
|
166
|
+
const declared = service.init?.task.needs.find((need) => need.service.relativeName === service.relativeName);
|
|
167
|
+
if (declared) {
|
|
168
|
+
return declared.relativeName;
|
|
169
|
+
}
|
|
170
|
+
const prefix = service.scope.namePrefix;
|
|
171
|
+
return prefix.length > 0 ? service.relativeName.slice(prefix.length + 1) : service.relativeName;
|
|
172
|
+
}
|
|
155
173
|
const initViews = new WeakMap();
|
|
156
174
|
// The service itself for the view its init task needs, else the item.
|
|
157
175
|
function getPlannedService(item) {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.appendWorkTask = void 0;
|
|
4
4
|
const work_secret_1 = require("../work-secret");
|
|
5
|
+
const work_task_1 = require("../work-task");
|
|
5
6
|
const parse_work_generate_1 = require("./parse-work-generate");
|
|
6
7
|
const template_value_1 = require("./template-value");
|
|
7
8
|
const parse_work_mounts_1 = require("./parse-work-mounts");
|
|
@@ -25,7 +26,8 @@ function appendWorkTask(workTree, cwd, referenceTask, environment, context) {
|
|
|
25
26
|
const parsed = parseTask(cwd, referenceTask, environment, context);
|
|
26
27
|
// on a server's cluster a local task runs in a tool image like a container task
|
|
27
28
|
const server = workTree.environment.type === 'kubernetes' ? workTree.environment.server : undefined;
|
|
28
|
-
|
|
29
|
+
// an aggregate runs nothing, so it needs no pod either
|
|
30
|
+
const task = !server || (0, work_task_1.isAggregateTask)(parsed)
|
|
29
31
|
? parsed
|
|
30
32
|
: parsed.type === 'local-task'
|
|
31
33
|
? (0, job_placement_1.runInToolImage)(parsed, server, referenceTask.schema.platform)
|
|
@@ -83,16 +85,19 @@ function parseTask(cwd, task, environment, context) {
|
|
|
83
85
|
resources: (0, work_resources_1.parseWorkResources)(task.schema.resources),
|
|
84
86
|
network: (0, work_network_1.parseWorkNetwork)(task.schema.network),
|
|
85
87
|
networkCeiling: null,
|
|
88
|
+
args: task.schema.args ?? null,
|
|
86
89
|
runner: task.schema.runner ?? task.scope.schema.runner ?? null,
|
|
87
90
|
};
|
|
88
91
|
if ((0, build_file_task_schema_1.isBuildFileContainerTaskSchema)(task.schema)) {
|
|
89
92
|
return {
|
|
90
93
|
...baseWorkNode,
|
|
91
94
|
type: 'container-task',
|
|
92
|
-
|
|
95
|
+
// a capability holds for root only: a task that adds some runs as its image's user
|
|
96
|
+
user: task.schema.capAdd?.length ? null : (0, get_container_user_1.getContainerUser)(),
|
|
93
97
|
image: (0, template_value_1.templateValue)(task.schema.image, envs),
|
|
94
98
|
mounts: (0, parse_work_mounts_1.parseWorkMounts)(cwd, task.schema, envs),
|
|
95
99
|
volumeSize: task.schema.volumeSize ?? null,
|
|
100
|
+
capAdd: task.schema.capAdd ?? [],
|
|
96
101
|
};
|
|
97
102
|
}
|
|
98
103
|
else {
|
|
@@ -100,13 +105,23 @@ function parseTask(cwd, task, environment, context) {
|
|
|
100
105
|
if (fileSecret) {
|
|
101
106
|
throw new Error(`task ${task.relativeName}: a local task takes secrets as env only (${(0, work_secret_1.getSecretName)(fileSecret)})`);
|
|
102
107
|
}
|
|
103
|
-
|
|
108
|
+
// on a server's cluster it runs in a pod of a pool's tool image (runInToolImage)
|
|
109
|
+
const contained = !!environment.hosted?.kubernetes || !!environment.hosted?.localTasksInContainers;
|
|
110
|
+
if (baseWorkNode.network.type !== 'open' && !contained) {
|
|
104
111
|
throw new Error(`task ${task.relativeName}: network ${(0, work_network_1.formatNetwork)(baseWorkNode.network)} needs a container, ` +
|
|
105
112
|
`hammerkit can't limit the network of a local task`);
|
|
106
113
|
}
|
|
114
|
+
const aggregate = task.deps.length > 0 &&
|
|
115
|
+
task.needs.length === 0 &&
|
|
116
|
+
baseWorkNode.src.length === 0 &&
|
|
117
|
+
!baseWorkNode.continuous &&
|
|
118
|
+
baseWorkNode.cmds.length === 0 &&
|
|
119
|
+
baseWorkNode.generates.length === 0 &&
|
|
120
|
+
baseWorkNode.secrets.length === 0;
|
|
107
121
|
return {
|
|
108
122
|
...baseWorkNode,
|
|
109
123
|
type: 'local-task',
|
|
124
|
+
...(aggregate ? { aggregate } : {}),
|
|
110
125
|
};
|
|
111
126
|
}
|
|
112
127
|
}
|
|
@@ -4,6 +4,8 @@ exports.getWorkTaskRuntime = exports.getWorkServiceRuntime = void 0;
|
|
|
4
4
|
const work_runtime_docker_1 = require("../work-runtime-docker");
|
|
5
5
|
const work_runtime_kubernetes_1 = require("../work-runtime-kubernetes");
|
|
6
6
|
const work_runtime_local_1 = require("../work-runtime-local");
|
|
7
|
+
const work_runtime_aggregate_1 = require("../work-runtime-aggregate");
|
|
8
|
+
const work_task_1 = require("../work-task");
|
|
7
9
|
const work_item_1 = require("../work-item");
|
|
8
10
|
function getWorkServiceRuntime(workTree, service) {
|
|
9
11
|
if ((0, work_item_1.isContainerWorkServiceItem)(service)) {
|
|
@@ -24,7 +26,10 @@ function getWorkServiceRuntime(workTree, service) {
|
|
|
24
26
|
}
|
|
25
27
|
exports.getWorkServiceRuntime = getWorkServiceRuntime;
|
|
26
28
|
function getWorkTaskRuntime(workTree, task) {
|
|
27
|
-
if ((0,
|
|
29
|
+
if ((0, work_task_1.isAggregateTask)(task.data)) {
|
|
30
|
+
return (0, work_runtime_aggregate_1.aggregateWorkRuntime)();
|
|
31
|
+
}
|
|
32
|
+
else if ((0, work_item_1.isLocalWorkTaskItem)(task)) {
|
|
28
33
|
return (0, work_runtime_local_1.getLocalWorkRuntime)(task);
|
|
29
34
|
}
|
|
30
35
|
else if ((0, work_item_1.isContainerWorkTaskItem)(task)) {
|