hammerkit 1.27.0-next.2 → 1.27.0-next.21
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 +27 -0
- package/dist/cache/backends/registry-cache-backend.js +2 -2
- package/dist/cache/explain.js +17 -0
- package/dist/cli.js +1 -0
- package/dist/docker/item-network.js +13 -5
- package/dist/executer/deadline.js +17 -3
- package/dist/executer/docker-service.js +30 -8
- package/dist/executer/event-cache.js +20 -2
- package/dist/executer/execute-work-task.js +5 -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 +7 -0
- package/dist/executer/scheduler/state-key.js +42 -5
- 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 +207 -65
- 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/claim-layout.js +71 -0
- package/dist/kubernetes/ensure-http-route.js +1 -1
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +5 -9
- 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/get-service-ip.js +2 -2
- 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 +626 -0
- package/dist/kubernetes/secrets.js +7 -11
- package/dist/kubernetes/volumes.js +6 -3
- 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 +1 -0
- package/dist/planner/utils/append-work-task.js +15 -2
- 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 +97 -48
- package/dist/planner/work-runtime-local.js +2 -1
- package/dist/planner/work-task.js +6 -1
- package/dist/program.js +35 -13
- package/dist/remote/annotations.js +13 -1
- package/dist/remote/device-login.js +7 -2
- package/dist/remote/host-config.js +64 -3
- package/dist/remote/remote-commands.js +41 -9
- package/dist/remote/remote-work-commands.js +246 -1
- 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 +5 -1
- package/dist/schema/build-file-container-service-schema.js +2 -0
- package/dist/schema/build-file-include-schema.js +10 -4
- package/dist/schema/reference-parser.js +9 -1
- package/dist/schema/schema-parser.js +36 -4
- package/dist/schema/work-scope-parser.js +33 -10
- package/dist/secrets/github-app-token-command.js +3 -1
- 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 +4 -2
- package/dist/server/endpoints.js +612 -0
- package/dist/server/executor.js +56 -5
- package/dist/server/kubernetes-backend.js +303 -64
- package/dist/server/kubernetes-sandbox-storage.js +48 -3
- package/dist/server/oauth.js +12 -1
- package/dist/server/plan.js +126 -62
- 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 +64 -3
- package/dist/server/run-worker.js +146 -10
- package/dist/server/runs.js +347 -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 +213 -14
- package/dist/server/server.js +268 -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/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
|
}
|
|
@@ -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
|
}
|
|
@@ -120,6 +120,7 @@ function parseService(workTree, service, environment, context) {
|
|
|
120
120
|
volumeSize: service.schema.volumeSize ?? null,
|
|
121
121
|
network: (0, work_network_1.parseWorkNetwork)(service.schema.network),
|
|
122
122
|
networkCeiling: null,
|
|
123
|
+
args: service.schema.args ?? null,
|
|
123
124
|
};
|
|
124
125
|
}
|
|
125
126
|
}
|
|
@@ -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,6 +85,7 @@ 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)) {
|
|
@@ -100,13 +103,23 @@ function parseTask(cwd, task, environment, context) {
|
|
|
100
103
|
if (fileSecret) {
|
|
101
104
|
throw new Error(`task ${task.relativeName}: a local task takes secrets as env only (${(0, work_secret_1.getSecretName)(fileSecret)})`);
|
|
102
105
|
}
|
|
103
|
-
|
|
106
|
+
// on a server's cluster it runs in a pod of a pool's tool image (runInToolImage)
|
|
107
|
+
const contained = !!environment.hosted?.kubernetes || !!environment.hosted?.localTasksInContainers;
|
|
108
|
+
if (baseWorkNode.network.type !== 'open' && !contained) {
|
|
104
109
|
throw new Error(`task ${task.relativeName}: network ${(0, work_network_1.formatNetwork)(baseWorkNode.network)} needs a container, ` +
|
|
105
110
|
`hammerkit can't limit the network of a local task`);
|
|
106
111
|
}
|
|
112
|
+
const aggregate = task.deps.length > 0 &&
|
|
113
|
+
task.needs.length === 0 &&
|
|
114
|
+
baseWorkNode.src.length === 0 &&
|
|
115
|
+
!baseWorkNode.continuous &&
|
|
116
|
+
baseWorkNode.cmds.length === 0 &&
|
|
117
|
+
baseWorkNode.generates.length === 0 &&
|
|
118
|
+
baseWorkNode.secrets.length === 0;
|
|
107
119
|
return {
|
|
108
120
|
...baseWorkNode,
|
|
109
121
|
type: 'local-task',
|
|
122
|
+
...(aggregate ? { aggregate } : {}),
|
|
110
123
|
};
|
|
111
124
|
}
|
|
112
125
|
}
|
|
@@ -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)) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.normalizePath = void 0;
|
|
3
|
+
exports.normalizeHostPath = exports.getHomeReference = exports.normalizePath = void 0;
|
|
4
4
|
const path_1 = require("path");
|
|
5
5
|
function normalizePath(cwd, pwd, path) {
|
|
6
6
|
if (path.startsWith('/')) {
|
|
@@ -14,4 +14,22 @@ function normalizePath(cwd, pwd, path) {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
exports.normalizePath = normalizePath;
|
|
17
|
+
// What a path of the host starts with to name the home directory: `~` and `$HOME`, and
|
|
18
|
+
// `$PWD`, which in a mount has always meant the home directory, not the project it reads like
|
|
19
|
+
function getHomeReference(path) {
|
|
20
|
+
for (const reference of ['~', '$HOME']) {
|
|
21
|
+
if (path === reference || path.startsWith(`${reference}/`) || path.startsWith(`${reference}\\`)) {
|
|
22
|
+
return reference;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return path.startsWith('$PWD') ? '$PWD' : null;
|
|
26
|
+
}
|
|
27
|
+
exports.getHomeReference = getHomeReference;
|
|
28
|
+
// A path of the host in a mount: in the home directory when it starts with one of the
|
|
29
|
+
// references above, else in the directory of the build file unless it is absolute
|
|
30
|
+
function normalizeHostPath(cwd, home, path) {
|
|
31
|
+
const reference = getHomeReference(path);
|
|
32
|
+
return reference ? (0, path_1.join)(home, path.substring(reference.length)) : normalizePath(cwd, home, path);
|
|
33
|
+
}
|
|
34
|
+
exports.normalizeHostPath = normalizeHostPath;
|
|
17
35
|
//# sourceMappingURL=normalize-path.js.map
|
|
@@ -7,6 +7,12 @@ const path_1 = require("path");
|
|
|
7
7
|
function parseWorkMount(cwd, dir) {
|
|
8
8
|
const parts = dir.split(':');
|
|
9
9
|
if (parts.length === 1) {
|
|
10
|
+
const home = (0, normalize_path_1.getHomeReference)(dir);
|
|
11
|
+
if (home === '~' || home === '$HOME') {
|
|
12
|
+
// the container has no such directory: the same path on both sides would be one of the
|
|
13
|
+
// project there (`$PWD` has always been, so it stays)
|
|
14
|
+
throw new Error(`mount ${dir} is in the home directory, name where it goes in the container (${dir}:/path)`);
|
|
15
|
+
}
|
|
10
16
|
return parseLocalMount(cwd, dir, dir, dir, false);
|
|
11
17
|
}
|
|
12
18
|
else if (parts.length === 2) {
|
|
@@ -22,7 +28,7 @@ function parseWorkMount(cwd, dir) {
|
|
|
22
28
|
exports.parseWorkMount = parseWorkMount;
|
|
23
29
|
function parseLocalMount(cwd, localPath, containerPath, mount, readOnly) {
|
|
24
30
|
return {
|
|
25
|
-
localPath: (0, normalize_path_1.
|
|
31
|
+
localPath: (0, normalize_path_1.normalizeHostPath)(cwd, (0, os_1.homedir)(), localPath),
|
|
26
32
|
containerPath: (0, normalize_path_1.normalizePath)(cwd, cwd, containerPath),
|
|
27
33
|
isFile: (0, path_1.extname)(localPath).length > 1,
|
|
28
34
|
mount,
|
|
@@ -8,7 +8,10 @@ function parseWorkPort(port) {
|
|
|
8
8
|
return { containerPort: port, hostPort: port };
|
|
9
9
|
}
|
|
10
10
|
else if (parts.length === 2) {
|
|
11
|
-
|
|
11
|
+
// `0:80` is not published, as an empty host port: 0 (a free port) is for the endpoints
|
|
12
|
+
// of a server only
|
|
13
|
+
const hostPort = parsePort(parts[0], true);
|
|
14
|
+
return { containerPort: parsePort(parts[1], false), hostPort: hostPort === 0 ? null : hostPort };
|
|
12
15
|
}
|
|
13
16
|
else {
|
|
14
17
|
throw new Error(`invalid port ${port}`);
|
|
@@ -101,6 +101,9 @@ function applyArguments(selection, args, keepCache = false) {
|
|
|
101
101
|
if (item.type === 'kubernetes-service') {
|
|
102
102
|
throw new Error(`service ${item.name} is a kubernetes service and has no cmd for the arguments after --`);
|
|
103
103
|
}
|
|
104
|
+
if (item.args === false) {
|
|
105
|
+
throw new Error(`${item.type === 'container-service' ? 'service' : 'task'} ${item.name} takes no arguments after -- (args: false)`);
|
|
106
|
+
}
|
|
104
107
|
const target = getArgumentsTarget(item);
|
|
105
108
|
if (!target) {
|
|
106
109
|
throw new Error(item.type === 'container-service'
|