hammerkit 1.26.0 → 1.27.0-next.10
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 +62 -1
- package/dist/cache/backends/local-cache-backend.js +10 -2
- package/dist/cache/backends/registry-cache-backend.js +2 -2
- package/dist/cache/explain.js +13 -11
- package/dist/cli.js +3 -0
- package/dist/docker/item-network.js +471 -0
- package/dist/docker/pull.js +4 -2
- package/dist/docker/remove-orphaned-containers.js +13 -6
- package/dist/environment/replace-env-variables.js +8 -6
- package/dist/executer/await-completed-dependencies.js +2 -1
- package/dist/executer/check-readiness.js +2 -2
- package/dist/executer/docker-service.js +24 -13
- package/dist/executer/docker-task.js +18 -29
- package/dist/executer/event-cache.js +24 -2
- package/dist/executer/execute-command.js +18 -5
- package/dist/executer/execute-docker.js +4 -1
- package/dist/executer/execution-steps.js +33 -7
- package/dist/executer/get-docker-executor.js +14 -7
- package/dist/executer/scheduler/enqueue-next.js +17 -3
- package/dist/executer/watch-loop.js +2 -2
- package/dist/executer/watch-state-key.js +1 -1
- package/dist/file/within-root.js +42 -0
- package/dist/harness/connect.js +99 -0
- package/dist/harness/format.js +73 -0
- package/dist/harness/mcp-command.js +59 -0
- package/dist/harness/mcp.js +432 -0
- package/dist/harness/opencode.js +492 -0
- package/dist/harness/pi.js +217 -0
- package/dist/harness/sandbox-client.js +411 -0
- package/dist/index.js +33 -36
- package/dist/kubernetes/apply.js +42 -1
- package/dist/kubernetes/await-running-state.js +33 -1
- 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 +46 -28
- package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +132 -5
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +27 -11
- package/dist/kubernetes/ensure-persistent-data.js +75 -24
- package/dist/kubernetes/get-service-ip.js +16 -2
- package/dist/kubernetes/job-placement.js +99 -0
- package/dist/kubernetes/job-secrets.js +320 -0
- package/dist/kubernetes/kept-claims.js +203 -0
- package/dist/kubernetes/kubernetes-instance.js +10 -2
- package/dist/kubernetes/network-gate.js +71 -0
- package/dist/kubernetes/network-policy.js +583 -0
- package/dist/kubernetes/remove-persistent-data.js +3 -1
- package/dist/kubernetes/resources.js +37 -4
- package/dist/kubernetes/restore-kubernetes-data.js +28 -7
- package/dist/kubernetes/secrets.js +7 -9
- package/dist/kubernetes/store-kubernetes-data.js +1 -1
- package/dist/kubernetes/volumes.js +104 -12
- package/dist/log.js +1 -1
- package/dist/main.js +41 -0
- package/dist/network/egress-proxy.js +407 -0
- package/dist/network/port-relay.js +35 -0
- package/dist/optimizer/get-work-cache-stats.js +16 -0
- package/dist/optimizer/work-task-cache-description.js +5 -2
- package/dist/planner/service-definition.js +5 -0
- package/dist/planner/utils/append-work-dependencies.js +5 -2
- package/dist/planner/utils/append-work-environment.js +2 -0
- package/dist/planner/utils/append-work-service.js +5 -0
- package/dist/planner/utils/append-work-task.js +20 -1
- package/dist/planner/utils/assert-item-paths.js +50 -0
- package/dist/planner/utils/hosted-volumes.js +35 -0
- package/dist/planner/utils/task-arguments.js +3 -0
- package/dist/planner/work-network.js +193 -0
- package/dist/planner/work-runtime-docker.js +43 -1
- package/dist/planner/work-runtime-kubernetes.js +370 -111
- package/dist/planner/work-runtime-local.js +2 -1
- package/dist/program.js +33 -5
- package/dist/remote/annotations.js +115 -0
- package/dist/remote/bundle-text.js +24 -0
- package/dist/remote/bundle.js +9 -1
- package/dist/remote/remote-commands.js +15 -3
- package/dist/remote/remote-work-commands.js +272 -71
- package/dist/remote/server-client.js +18 -5
- package/dist/remote/source-manifest.js +15 -2
- package/dist/remote/token-source.js +67 -0
- package/dist/schema/build-file-base-task-schema.js +7 -0
- package/dist/schema/build-file-container-service-schema.js +8 -0
- package/dist/schema/build-file-container-task-schema.js +3 -0
- package/dist/schema/build-file-environment-schema.js +9 -0
- package/dist/schema/network-schema.js +78 -0
- package/dist/schema/reference-parser.js +2 -0
- package/dist/schema/schema-parser.js +16 -3
- package/dist/schema/work-scope-parser.js +5 -0
- package/dist/secrets/command-secret-provider.js +73 -10
- package/dist/secrets/github-app-token-command.js +26 -0
- package/dist/secrets/github-app-token.js +191 -0
- package/dist/server/accounts-api.js +9 -2
- package/dist/server/definitions-api.js +69 -47
- package/dist/server/definitions.js +76 -51
- package/dist/server/executor.js +156 -41
- package/dist/server/git-checkout.js +72 -8
- package/dist/server/kubernetes-backend.js +603 -0
- package/dist/server/kubernetes-sandbox-storage.js +997 -0
- package/dist/server/oauth.js +12 -0
- package/dist/server/plan.js +88 -2
- package/dist/server/policy.js +104 -15
- package/dist/server/proposals-api.js +24 -40
- package/dist/server/proposals.js +12 -25
- package/dist/server/repo-access.js +37 -6
- package/dist/server/run-logs.js +217 -0
- package/dist/server/run-plan.js +48 -2
- package/dist/server/run-request.js +29 -10
- package/dist/server/run-store.js +5 -0
- package/dist/server/run-worker-command.js +13 -1
- package/dist/server/run-worker.js +121 -24
- package/dist/server/runs.js +191 -10
- package/dist/server/sandbox-docker.js +62 -0
- package/dist/server/sandbox-files-api.js +269 -0
- package/dist/server/sandbox-files.js +1054 -0
- package/dist/server/sandbox-patch.js +481 -0
- package/dist/server/sandbox-pod-command.js +15 -0
- package/dist/server/sandbox-pod.js +146 -0
- package/dist/server/sandbox-storage.js +462 -0
- package/dist/server/sandboxes-api.js +216 -0
- package/dist/server/sandboxes.js +519 -0
- package/dist/server/server-command.js +39 -20
- package/dist/server/server-config.js +443 -22
- package/dist/server/server.js +219 -40
- package/dist/utils/abort-on-signals.js +46 -6
- package/dist/utils/run-id.js +12 -0
- package/dist/utils/tar.js +25 -0
- package/harness/opencode.ts +11 -0
- package/harness/pi.ts +7 -0
- package/package.json +8 -2
- package/dist/remote/text-diff.js +0 -89
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The egress proxy of a task or service with `network: { allow: [hosts] }`.
|
|
3
|
+
// It runs in its own container (a pod on Kubernetes) next to the item, on a
|
|
4
|
+
// network only the item and the proxy join, and is the item's only way out: HTTP requests and
|
|
5
|
+
// `CONNECT` tunnels to an allowed host pass, everything else gets a 403 and a
|
|
6
|
+
// `refused` event on stdout. For each `host:port` entry it also listens on that
|
|
7
|
+
// port and forwards raw TCP to that host, which the item reaches under the
|
|
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
|
|
11
|
+
// needs no image of its own, only a stock Node image.
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
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;
|
|
14
|
+
const work_network_1 = require("../planner/work-network");
|
|
15
|
+
const network_schema_1 = require("../schema/network-schema");
|
|
16
|
+
// Pinned, so the code behind the boundary is the same on every host. To bump
|
|
17
|
+
// it, take the index digest of the tag (`docker buildx imagetools inspect
|
|
18
|
+
// node:24-alpine`). The port relay runs in it too.
|
|
19
|
+
exports.EGRESS_PROXY_IMAGE = 'node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401c70e0e762c5b53302f905ec1c1';
|
|
20
|
+
exports.EGRESS_PROXY_PORT = network_schema_1.PROXY_PORT;
|
|
21
|
+
// the name the item reaches the proxy under
|
|
22
|
+
exports.EGRESS_PROXY_HOST = 'hammerkit-proxy';
|
|
23
|
+
// Kept free of anything but Node's own modules. The host rule is the one of
|
|
24
|
+
// matchesHost in planner/work-network.ts. A name a wildcard allows must not
|
|
25
|
+
// resolve to the host or a private network, and none to link-local.
|
|
26
|
+
exports.EGRESS_PROXY_SCRIPT = `
|
|
27
|
+
const http = require('http')
|
|
28
|
+
const net = require('net')
|
|
29
|
+
const dns = require('dns')
|
|
30
|
+
const crypto = require('crypto')
|
|
31
|
+
const entries = JSON.parse(process.env.HAMMERKIT_ALLOW || '[]').map((h) => h.toLowerCase())
|
|
32
|
+
// names and patterns for HTTP(S)
|
|
33
|
+
const allow = entries.filter((e) => !e.includes(':'))
|
|
34
|
+
// names and ports for raw TCP, each forwarded on its port
|
|
35
|
+
const forwards = entries
|
|
36
|
+
.filter((e) => e.includes(':'))
|
|
37
|
+
.map((e) => ({ host: e.substring(0, e.lastIndexOf(':')), port: parseInt(e.substring(e.lastIndexOf(':') + 1), 10) }))
|
|
38
|
+
// entries of both kinds that a ceiling allows only through a pattern: they
|
|
39
|
+
// reach no private address, like a pattern
|
|
40
|
+
const publicOnly = JSON.parse(process.env.HAMMERKIT_PUBLIC_ONLY || '[]').map((h) => h.toLowerCase())
|
|
41
|
+
// the names of the needed services, for tools that ignore NO_PROXY: exact
|
|
42
|
+
// names only, never patterns
|
|
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 || ''
|
|
47
|
+
const port = parseInt(process.env.HAMMERKIT_PROXY_PORT || '${exports.EGRESS_PROXY_PORT}', 10)
|
|
48
|
+
// the name whose address the proxy listens on, every address when unset
|
|
49
|
+
const bind = process.env.HAMMERKIT_PROXY_BIND
|
|
50
|
+
const refused = new Set()
|
|
51
|
+
const httpsUrls = new Set()
|
|
52
|
+
function emit(event) {
|
|
53
|
+
process.stdout.write(JSON.stringify(event) + '\\n')
|
|
54
|
+
}
|
|
55
|
+
// the rule that allows a host and port: its exact name, a wildcard, or none
|
|
56
|
+
function allowedBy(host, port) {
|
|
57
|
+
const name = host.toLowerCase().replace(/\\.$/, '')
|
|
58
|
+
if (needs.includes(name)) {
|
|
59
|
+
return 'exact'
|
|
60
|
+
}
|
|
61
|
+
const named = []
|
|
62
|
+
if (allow.includes(name)) {
|
|
63
|
+
named.push(name)
|
|
64
|
+
}
|
|
65
|
+
if (forwards.some((f) => f.host === name && f.port === port)) {
|
|
66
|
+
named.push(name + ':' + port)
|
|
67
|
+
}
|
|
68
|
+
if (named.length > 0) {
|
|
69
|
+
return named.some((entry) => !publicOnly.includes(entry)) ? 'exact' : 'wildcard'
|
|
70
|
+
}
|
|
71
|
+
return allow.some((rule) => rule.startsWith('*.') && name.endsWith(rule.substring(1))) ? 'wildcard' : null
|
|
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
|
+
}
|
|
98
|
+
function ipv4(address) {
|
|
99
|
+
const mapped = /^::ffff:(\\d+\\.\\d+\\.\\d+\\.\\d+)$/i.exec(address)
|
|
100
|
+
const value = mapped ? mapped[1] : address
|
|
101
|
+
return net.isIPv4(value) ? value.split('.').map(Number) : null
|
|
102
|
+
}
|
|
103
|
+
// never allowed: the cloud metadata service lives there
|
|
104
|
+
function isLinkLocal(address) {
|
|
105
|
+
const v4 = ipv4(address)
|
|
106
|
+
return v4 ? v4[0] === 169 && v4[1] === 254 : /^fe[89ab]/i.test(address)
|
|
107
|
+
}
|
|
108
|
+
// the host, its private networks (Docker's bridges among them) and the
|
|
109
|
+
// cluster: only for a name listed exactly, such as a needed service
|
|
110
|
+
function isInternal(address) {
|
|
111
|
+
const v4 = ipv4(address)
|
|
112
|
+
if (v4) {
|
|
113
|
+
const [a, b] = v4
|
|
114
|
+
return (
|
|
115
|
+
a === 0 ||
|
|
116
|
+
a === 10 ||
|
|
117
|
+
a === 127 ||
|
|
118
|
+
(a === 100 && b >= 64 && b < 128) ||
|
|
119
|
+
(a === 172 && b >= 16 && b < 32) ||
|
|
120
|
+
(a === 192 && b === 168)
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
return address === '::' || address === '::1' || /^f[cd]/i.test(address)
|
|
124
|
+
}
|
|
125
|
+
// The proxy resolves the name itself and connects only to the addresses it
|
|
126
|
+
// checked, each in turn (a name can have an IPv6 address the network has no
|
|
127
|
+
// route to).
|
|
128
|
+
function resolve(host, rule, callback) {
|
|
129
|
+
dns.lookup(host, { all: true }, (err, addresses) => {
|
|
130
|
+
if (err || addresses.length === 0) {
|
|
131
|
+
callback(err || new Error('no address'), null)
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
const blocked = addresses.some((a) => isLinkLocal(a.address) || (rule !== 'exact' && isInternal(a.address)))
|
|
135
|
+
callback(null, blocked ? null : addresses)
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
// connection options that reach the checked addresses and nothing else
|
|
139
|
+
function connectTo(host, addresses) {
|
|
140
|
+
const lookup = (hostname, options, callback) => {
|
|
141
|
+
const cb = typeof options === 'function' ? options : callback
|
|
142
|
+
if (typeof options === 'object' && options && options.all) {
|
|
143
|
+
cb(null, addresses)
|
|
144
|
+
} else {
|
|
145
|
+
cb(null, addresses[0].address, addresses[0].family)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { host, lookup, autoSelectFamily: true }
|
|
149
|
+
}
|
|
150
|
+
function refuse(host) {
|
|
151
|
+
if (!refused.has(host)) {
|
|
152
|
+
refused.add(host)
|
|
153
|
+
emit({ type: 'refused', host })
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function httpsUrl(host) {
|
|
157
|
+
if (!httpsUrls.has(host)) {
|
|
158
|
+
httpsUrls.add(host)
|
|
159
|
+
emit({ type: 'https-url', host })
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function parseAuthority(value) {
|
|
163
|
+
const match = /^\\[([^\\]]+)\\](?::(\\d+))?$/.exec(value) || /^([^:]+)(?::(\\d+))?$/.exec(value)
|
|
164
|
+
return match ? { host: match[1], port: match[2] ? parseInt(match[2], 10) : 443 } : null
|
|
165
|
+
}
|
|
166
|
+
const server = http.createServer((req, res) => {
|
|
167
|
+
let url
|
|
168
|
+
try {
|
|
169
|
+
url = new URL(req.url)
|
|
170
|
+
} catch (e) {
|
|
171
|
+
res.writeHead(400, { 'content-type': 'text/plain' })
|
|
172
|
+
res.end('hammerkit: only proxy requests are served\\n')
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
// BusyBox wget sends an https URL to the proxy as it is: TLS belongs between
|
|
176
|
+
// the client and the host, so only a CONNECT tunnel reaches an https host
|
|
177
|
+
if (url.protocol === 'https:' && allowedBy(url.hostname, parseInt(url.port || '443', 10))) {
|
|
178
|
+
httpsUrl(url.hostname)
|
|
179
|
+
res.writeHead(501, { 'content-type': 'text/plain' })
|
|
180
|
+
res.end('hammerkit: ' + url.hostname + ' is allowed through a CONNECT tunnel only, not by its https URL\\n')
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
const rule = url.protocol === 'http:' ? allowedBy(url.hostname, parseInt(url.port || '80', 10)) : null
|
|
184
|
+
if (!rule) {
|
|
185
|
+
refuse(url.hostname)
|
|
186
|
+
res.writeHead(403, { 'content-type': 'text/plain' })
|
|
187
|
+
res.end('hammerkit: network access to ' + url.hostname + ' is not allowed\\n')
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
resolve(url.hostname, rule, (err, addresses) => {
|
|
191
|
+
if (err) {
|
|
192
|
+
res.writeHead(502, { 'content-type': 'text/plain' })
|
|
193
|
+
res.end()
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
if (!addresses) {
|
|
197
|
+
refuse(url.hostname)
|
|
198
|
+
res.writeHead(403, { 'content-type': 'text/plain' })
|
|
199
|
+
res.end('hammerkit: ' + url.hostname + ' resolves to an address that is not allowed\\n')
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
const headers = Object.assign({}, req.headers)
|
|
203
|
+
delete headers['proxy-connection']
|
|
204
|
+
delete headers['proxy-authorization']
|
|
205
|
+
// the absolute URL wins over a Host header that disagrees
|
|
206
|
+
headers.host = url.host
|
|
207
|
+
const upstream = http.request(
|
|
208
|
+
Object.assign(connectTo(url.hostname, addresses), {
|
|
209
|
+
port: url.port || 80,
|
|
210
|
+
method: req.method,
|
|
211
|
+
path: url.pathname + url.search,
|
|
212
|
+
headers,
|
|
213
|
+
}),
|
|
214
|
+
(response) => {
|
|
215
|
+
res.writeHead(response.statusCode || 502, response.headers)
|
|
216
|
+
response.pipe(res)
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
upstream.on('error', () => {
|
|
220
|
+
if (!res.headersSent) {
|
|
221
|
+
res.writeHead(502, { 'content-type': 'text/plain' })
|
|
222
|
+
}
|
|
223
|
+
res.end()
|
|
224
|
+
})
|
|
225
|
+
req.pipe(upstream)
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
server.on('connect', (req, socket, head) => {
|
|
229
|
+
const target = parseAuthority(req.url || '')
|
|
230
|
+
socket.on('error', () => socket.destroy())
|
|
231
|
+
const rule = target ? allowedBy(target.host, target.port) || allowedForSecrets(req, target.host, target.port) : null
|
|
232
|
+
if (!rule) {
|
|
233
|
+
refuse(target ? target.host : String(req.url))
|
|
234
|
+
socket.end('HTTP/1.1 403 Forbidden\\r\\ncontent-type: text/plain\\r\\n\\r\\nhammerkit: network access is not allowed\\n')
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
resolve(target.host, rule, (err, addresses) => {
|
|
238
|
+
if (err) {
|
|
239
|
+
socket.end('HTTP/1.1 502 Bad Gateway\\r\\n\\r\\n')
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
if (!addresses) {
|
|
243
|
+
refuse(target.host)
|
|
244
|
+
socket.end('HTTP/1.1 403 Forbidden\\r\\ncontent-type: text/plain\\r\\n\\r\\nhammerkit: network access is not allowed\\n')
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
let established = false
|
|
248
|
+
const upstream = net.connect(Object.assign(connectTo(target.host, addresses), { port: target.port }), () => {
|
|
249
|
+
established = true
|
|
250
|
+
socket.write('HTTP/1.1 200 Connection Established\\r\\n\\r\\n')
|
|
251
|
+
if (head && head.length > 0) {
|
|
252
|
+
upstream.write(head)
|
|
253
|
+
}
|
|
254
|
+
upstream.pipe(socket)
|
|
255
|
+
socket.pipe(upstream)
|
|
256
|
+
})
|
|
257
|
+
upstream.on('error', () => {
|
|
258
|
+
if (established) {
|
|
259
|
+
socket.destroy()
|
|
260
|
+
} else {
|
|
261
|
+
socket.end('HTTP/1.1 502 Bad Gateway\\r\\n\\r\\n')
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
socket.on('close', () => upstream.destroy())
|
|
265
|
+
})
|
|
266
|
+
})
|
|
267
|
+
// A TCP forward: each connection goes to the checked addresses of its host,
|
|
268
|
+
// on the same port.
|
|
269
|
+
function forward(target) {
|
|
270
|
+
return net.createServer({ allowHalfOpen: true }, (client) => {
|
|
271
|
+
client.on('error', () => client.destroy())
|
|
272
|
+
resolve(target.host, allowedBy(target.host, target.port), (err, addresses) => {
|
|
273
|
+
if (err || !addresses) {
|
|
274
|
+
if (!err) {
|
|
275
|
+
refuse(target.host)
|
|
276
|
+
}
|
|
277
|
+
client.destroy()
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
const upstream = net.connect(Object.assign(connectTo(target.host, addresses), { port: target.port }))
|
|
281
|
+
upstream.on('error', () => client.destroy())
|
|
282
|
+
client.on('close', () => upstream.destroy())
|
|
283
|
+
client.pipe(upstream)
|
|
284
|
+
upstream.pipe(client)
|
|
285
|
+
})
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
function listen(address) {
|
|
289
|
+
const servers = [{ server, port }].concat(forwards.map((f) => ({ server: forward(f), port: f.port })))
|
|
290
|
+
let pending = servers.length
|
|
291
|
+
for (const entry of servers) {
|
|
292
|
+
entry.server.on('error', (err) => {
|
|
293
|
+
process.stderr.write('hammerkit: unable to listen on port ' + entry.port + ': ' + err.message + '\\n')
|
|
294
|
+
process.exit(1)
|
|
295
|
+
})
|
|
296
|
+
entry.server.listen(entry.port, address, () => {
|
|
297
|
+
pending--
|
|
298
|
+
if (pending === 0) {
|
|
299
|
+
emit({ type: 'ready' })
|
|
300
|
+
}
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (bind) {
|
|
305
|
+
dns.lookup(bind, { family: 4 }, (err, address) => {
|
|
306
|
+
if (err) {
|
|
307
|
+
process.stderr.write('hammerkit: unable to resolve ' + bind + ': ' + err.message + '\\n')
|
|
308
|
+
process.exit(1)
|
|
309
|
+
}
|
|
310
|
+
listen(address)
|
|
311
|
+
})
|
|
312
|
+
} else {
|
|
313
|
+
listen(undefined)
|
|
314
|
+
}
|
|
315
|
+
process.on('SIGTERM', () => process.exit(0))
|
|
316
|
+
`;
|
|
317
|
+
function getEgressProxyCommand() {
|
|
318
|
+
return ['node', '-e', exports.EGRESS_PROXY_SCRIPT];
|
|
319
|
+
}
|
|
320
|
+
exports.getEgressProxyCommand = getEgressProxyCommand;
|
|
321
|
+
// `needs` are the names of the needed services, allowed as exactly those
|
|
322
|
+
// names. `bind` is the name the proxy listens on alone, so that nothing on its
|
|
323
|
+
// other networks reaches it. `publicOnly` are the entries of `hosts` that reach
|
|
324
|
+
// no private address (see WorkNetwork). The endpoints of secret providers and
|
|
325
|
+
// the token they need are set by the caller (getSecretProxyEnvironment).
|
|
326
|
+
function getEgressProxyEnvironment(hosts, needs, bind, publicOnly = []) {
|
|
327
|
+
return {
|
|
328
|
+
HAMMERKIT_ALLOW: JSON.stringify(hosts),
|
|
329
|
+
...(publicOnly.length > 0 ? { HAMMERKIT_PUBLIC_ONLY: JSON.stringify(publicOnly) } : {}),
|
|
330
|
+
HAMMERKIT_NEEDS: JSON.stringify(needs),
|
|
331
|
+
HAMMERKIT_PROXY_PORT: `${exports.EGRESS_PROXY_PORT}`,
|
|
332
|
+
...(bind ? { HAMMERKIT_PROXY_BIND: bind } : {}),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
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;
|
|
342
|
+
// The proxy of a task or service: the hosts of its network in this run (under
|
|
343
|
+
// the run's ceiling), and the names of the services it needs, on every runtime.
|
|
344
|
+
function getItemProxyEnvironment(item, bind) {
|
|
345
|
+
const network = (0, work_network_1.getEffectiveNetwork)(item.data);
|
|
346
|
+
return getEgressProxyEnvironment(network.type === 'allow' ? network.hosts : [], item.needs.map((need) => need.name), bind, network.type === 'allow' ? network.publicOnly ?? [] : []);
|
|
347
|
+
}
|
|
348
|
+
exports.getItemProxyEnvironment = getItemProxyEnvironment;
|
|
349
|
+
// The variables that send the item's HTTP(S) through the proxy, both spellings
|
|
350
|
+
// since tools read either. The services it needs are reached directly.
|
|
351
|
+
// The ports the proxy of a network listens on: its own, and one per TCP forward.
|
|
352
|
+
function getEgressProxyPorts(network) {
|
|
353
|
+
return [exports.EGRESS_PROXY_PORT, ...(0, work_network_1.getTcpForwards)(network).map((forward) => forward.port)];
|
|
354
|
+
}
|
|
355
|
+
exports.getEgressProxyPorts = getEgressProxyPorts;
|
|
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}`;
|
|
359
|
+
const noProxy = ['localhost', '127.0.0.1', ...directHosts].join(',');
|
|
360
|
+
return {
|
|
361
|
+
HTTP_PROXY: proxy,
|
|
362
|
+
HTTPS_PROXY: proxy,
|
|
363
|
+
NO_PROXY: noProxy,
|
|
364
|
+
http_proxy: proxy,
|
|
365
|
+
https_proxy: proxy,
|
|
366
|
+
no_proxy: noProxy,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
exports.getProxyClientEnvironment = getProxyClientEnvironment;
|
|
370
|
+
// The proxy variables emptied: a healthcheck checks the service itself, and
|
|
371
|
+
// tools that ignore NO_PROXY (busybox wget) would ask the proxy for localhost.
|
|
372
|
+
function getProxylessEnvironment(env) {
|
|
373
|
+
const proxyKeys = Object.keys(env).filter((key) => /^(https?|no)_proxy$/i.test(key));
|
|
374
|
+
return proxyKeys.length > 0 ? Object.fromEntries(proxyKeys.map((key) => [key, ''])) : undefined;
|
|
375
|
+
}
|
|
376
|
+
exports.getProxylessEnvironment = getProxylessEnvironment;
|
|
377
|
+
// One line of the proxy's stdout, null for anything else.
|
|
378
|
+
function parseEgressProxyEvent(line) {
|
|
379
|
+
try {
|
|
380
|
+
const value = JSON.parse(line);
|
|
381
|
+
if (value?.type === 'ready') {
|
|
382
|
+
return { type: 'ready' };
|
|
383
|
+
}
|
|
384
|
+
if ((value?.type === 'refused' || value?.type === 'https-url') && typeof value.host === 'string') {
|
|
385
|
+
return { type: value.type, host: value.host };
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
// not an event
|
|
390
|
+
}
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
exports.parseEgressProxyEvent = parseEgressProxyEvent;
|
|
394
|
+
function formatRefusedHost(host) {
|
|
395
|
+
return `network: refused ${host}`;
|
|
396
|
+
}
|
|
397
|
+
exports.formatRefusedHost = formatRefusedHost;
|
|
398
|
+
// The line of an event other than `ready` in the item's log.
|
|
399
|
+
function formatEgressProxyEvent(event) {
|
|
400
|
+
if (event.type === 'https-url') {
|
|
401
|
+
return (`network: ${event.host} is allowed, but the client asked the proxy for its https URL instead of ` +
|
|
402
|
+
'tunneling with CONNECT (BusyBox wget does), use curl or GNU wget');
|
|
403
|
+
}
|
|
404
|
+
return formatRefusedHost(event.host);
|
|
405
|
+
}
|
|
406
|
+
exports.formatEgressProxyEvent = formatEgressProxyEvent;
|
|
407
|
+
//# sourceMappingURL=egress-proxy.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The published ports of a service with a limited network. Docker publishes
|
|
3
|
+
// ports only on a network with a way out, which a limited service must not
|
|
4
|
+
// have, so a relay container on the default bridge publishes them and passes
|
|
5
|
+
// each connection on to the service over the service's internal network.
|
|
6
|
+
// Like the egress proxy, it is a script for `node -e` in a stock Node image.
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.getPortRelayEnvironment = exports.getPortRelayCommand = exports.PORT_RELAY_SCRIPT = void 0;
|
|
9
|
+
// Kept free of anything but Node's own modules.
|
|
10
|
+
exports.PORT_RELAY_SCRIPT = `
|
|
11
|
+
const net = require('net')
|
|
12
|
+
const targets = JSON.parse(process.env.HAMMERKIT_RELAY || '[]')
|
|
13
|
+
for (const target of targets) {
|
|
14
|
+
// a client that ends its side still gets the answer
|
|
15
|
+
net
|
|
16
|
+
.createServer({ allowHalfOpen: true }, (client) => {
|
|
17
|
+
const upstream = net.connect(target.targetPort, target.host)
|
|
18
|
+
client.on('error', () => upstream.destroy())
|
|
19
|
+
upstream.on('error', () => client.destroy())
|
|
20
|
+
client.pipe(upstream)
|
|
21
|
+
upstream.pipe(client)
|
|
22
|
+
})
|
|
23
|
+
.listen(target.port)
|
|
24
|
+
}
|
|
25
|
+
process.on('SIGTERM', () => process.exit(0))
|
|
26
|
+
`;
|
|
27
|
+
function getPortRelayCommand() {
|
|
28
|
+
return ['node', '-e', exports.PORT_RELAY_SCRIPT];
|
|
29
|
+
}
|
|
30
|
+
exports.getPortRelayCommand = getPortRelayCommand;
|
|
31
|
+
function getPortRelayEnvironment(targets) {
|
|
32
|
+
return { HAMMERKIT_RELAY: JSON.stringify(targets) };
|
|
33
|
+
}
|
|
34
|
+
exports.getPortRelayEnvironment = getPortRelayEnvironment;
|
|
35
|
+
//# sourceMappingURL=port-relay.js.map
|
|
@@ -4,6 +4,7 @@ exports.getStateKey = exports.getWorkItemCacheStats = exports.getWorkCacheStats
|
|
|
4
4
|
const calculate_checksum_1 = require("./calculate-checksum");
|
|
5
5
|
const path_1 = require("path");
|
|
6
6
|
const crypto_1 = require("crypto");
|
|
7
|
+
const promises_1 = require("fs/promises");
|
|
7
8
|
function isExcluded(path, excluded) {
|
|
8
9
|
return excluded.some((excludedPath) => path === excludedPath || path.startsWith(excludedPath + path_1.sep));
|
|
9
10
|
}
|
|
@@ -11,6 +12,21 @@ async function addWorkCacheStats(result, cwd, path, matcher, context, root, excl
|
|
|
11
12
|
if (isExcluded(path, excluded)) {
|
|
12
13
|
return 0;
|
|
13
14
|
}
|
|
15
|
+
// On a server the files were written by a commit or a task: a link is a source as it is,
|
|
16
|
+
// its target, and never followed. A `**` would walk `x -> /` through the server's disk.
|
|
17
|
+
if (context.hosted) {
|
|
18
|
+
const link = await (0, promises_1.lstat)(path).catch(() => undefined);
|
|
19
|
+
if (link?.isSymbolicLink()) {
|
|
20
|
+
if (!matcher(path)) {
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
const checksum = (0, crypto_1.createHash)('sha1')
|
|
24
|
+
.update(`link:${await (0, promises_1.readlink)(path)}`)
|
|
25
|
+
.digest('hex');
|
|
26
|
+
result.files[(0, path_1.relative)(cwd, path).split(path_1.sep).join(path_1.posix.sep)] = { lastModified: link.mtimeMs, checksum };
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
14
30
|
const exists = await context.file.exists(path);
|
|
15
31
|
if (!exists) {
|
|
16
32
|
return 0;
|
|
@@ -6,6 +6,7 @@ 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");
|
|
9
10
|
// Every path is made project-relative (portablePath) so the description, and the
|
|
10
11
|
// task id hashed from it, is identical wherever the project is checked out.
|
|
11
12
|
//
|
|
@@ -24,8 +25,9 @@ function getWorkTaskCacheDescription(item) {
|
|
|
24
25
|
shell: task.shell ?? undefined,
|
|
25
26
|
platform: task.type === 'container-task' ? task.image : (0, os_1.platform)(),
|
|
26
27
|
// outputs hold native binaries, and docker pulls images for the host
|
|
27
|
-
// architecture, so container tasks are keyed by it too
|
|
28
|
-
|
|
28
|
+
// architecture, so container tasks are keyed by it too; on a cluster it is the
|
|
29
|
+
// one of the nodes the task runs on
|
|
30
|
+
arch: (task.type === 'container-task' && task.arch) || process.arch,
|
|
29
31
|
generates: task.generates.map((g) => portable(g.path)).sort() ?? undefined,
|
|
30
32
|
src: task.src.map((s) => s.source).sort() ?? undefined,
|
|
31
33
|
envs: Object.keys(envs)
|
|
@@ -39,6 +41,7 @@ function getWorkTaskCacheDescription(item) {
|
|
|
39
41
|
cwd: portable(task.cwd),
|
|
40
42
|
deps: item.deps.length > 0 ? item.deps.map((dep) => dep.id()).sort() : undefined,
|
|
41
43
|
secrets: cachedSecrets.length > 0 ? cachedSecrets : undefined,
|
|
44
|
+
claimLayout: (0, claim_layout_1.hasPathsEndingAlike)(item) || (0, claim_layout_1.hasOutputsFlatteningAlike)(item) ? 2 : undefined,
|
|
42
45
|
};
|
|
43
46
|
}
|
|
44
47
|
exports.getWorkTaskCacheDescription = getWorkTaskCacheDescription;
|
|
@@ -4,6 +4,7 @@ exports.getServiceDefinitionHash = void 0;
|
|
|
4
4
|
const crypto_1 = require("crypto");
|
|
5
5
|
const work_secret_1 = require("./work-secret");
|
|
6
6
|
const work_service_1 = require("./work-service");
|
|
7
|
+
const work_network_1 = require("./work-network");
|
|
7
8
|
const work_cache_id_1 = require("./work-cache-id");
|
|
8
9
|
// Everything a running container of the service was created from, plus the
|
|
9
10
|
// definitions of the services it needs: a dependent links to the container it
|
|
@@ -12,6 +13,7 @@ const work_cache_id_1 = require("./work-cache-id");
|
|
|
12
13
|
// service whose definition changed instead of silently reusing it.
|
|
13
14
|
function getServiceDefinitionHash(item) {
|
|
14
15
|
const service = item.data;
|
|
16
|
+
const network = (0, work_network_1.getEffectiveNetwork)(service);
|
|
15
17
|
const definition = {
|
|
16
18
|
image: service.image,
|
|
17
19
|
cmd: service.cmd?.cmd ?? null,
|
|
@@ -38,6 +40,9 @@ function getServiceDefinitionHash(item) {
|
|
|
38
40
|
.map((need) => `${need.name}=${getServiceDefinitionHash(need.service)}`),
|
|
39
41
|
// only when declared, so a service without keeps its hash
|
|
40
42
|
...(service.resources ? { resources: service.resources } : {}),
|
|
43
|
+
// in this run, under its `--network` ceiling: a service started open is
|
|
44
|
+
// recreated for a run that limits it
|
|
45
|
+
...((0, work_network_1.isLimitedNetwork)(network) ? { network } : {}),
|
|
41
46
|
};
|
|
42
47
|
return (0, crypto_1.createHash)('sha1').update(JSON.stringify(definition)).digest('hex');
|
|
43
48
|
}
|
|
@@ -19,7 +19,10 @@ function appendWorkDependencies(workTree, referenced, item, environment, context
|
|
|
19
19
|
}
|
|
20
20
|
if (((0, work_task_1.isContainerWorkTask)(depNode.data) || (0, work_service_1.isContainerWorkService)(depNode.data)) &&
|
|
21
21
|
((0, work_task_1.isContainerWorkTask)(item.data) || (0, work_service_1.isContainerWorkService)(item.data))) {
|
|
22
|
+
// the outputs of the dep's own deps too, which stay those of the task that made them
|
|
23
|
+
// (on Kubernetes, they are in its claim)
|
|
22
24
|
for (const generate of depNode.data.generates) {
|
|
25
|
+
const inherited = generate.inherited ?? depNode;
|
|
23
26
|
if (generate.isFile) {
|
|
24
27
|
// TODO check if thats correct
|
|
25
28
|
continue;
|
|
@@ -31,7 +34,7 @@ function appendWorkDependencies(workTree, referenced, item, environment, context
|
|
|
31
34
|
export: false,
|
|
32
35
|
resetOnChange: false,
|
|
33
36
|
containerPath: generate.path,
|
|
34
|
-
inherited
|
|
37
|
+
inherited,
|
|
35
38
|
readOnly: false,
|
|
36
39
|
});
|
|
37
40
|
}
|
|
@@ -44,7 +47,7 @@ function appendWorkDependencies(workTree, referenced, item, environment, context
|
|
|
44
47
|
exportAlways: false,
|
|
45
48
|
resetOnChange: false,
|
|
46
49
|
path: generate.path,
|
|
47
|
-
inherited
|
|
50
|
+
inherited,
|
|
48
51
|
isFile: false,
|
|
49
52
|
});
|
|
50
53
|
}
|
|
@@ -15,6 +15,8 @@ function appendWorkEnvironment(referenced) {
|
|
|
15
15
|
kubeConfig: referenced.schema.kubernetes.kubeconfig,
|
|
16
16
|
httpRoutes: referenced.schema.kubernetes.httpRoutes || [],
|
|
17
17
|
namespace: referenced.schema.kubernetes.namespace ?? 'default',
|
|
18
|
+
networkPolicyEnforced: referenced.schema.kubernetes.networkPolicy === 'enforced',
|
|
19
|
+
...(referenced.schema.kubernetes.volumeSize ? { volumeSize: referenced.schema.kubernetes.volumeSize } : {}),
|
|
18
20
|
};
|
|
19
21
|
}
|
|
20
22
|
}
|
|
@@ -23,6 +23,7 @@ const resolve_cache_1 = require("../../cache/resolve-cache");
|
|
|
23
23
|
const units_1 = require("../../utils/units");
|
|
24
24
|
const log_1 = require("../../log");
|
|
25
25
|
const work_resources_1 = require("../work-resources");
|
|
26
|
+
const work_network_1 = require("../work-network");
|
|
26
27
|
function appendWorkService(workTree, service, environment, context) {
|
|
27
28
|
const workService = parseService(workTree, service, environment, context);
|
|
28
29
|
if (!workTree.services[workService.name]) {
|
|
@@ -116,6 +117,10 @@ function parseService(workTree, service, environment, context) {
|
|
|
116
117
|
init: null,
|
|
117
118
|
secrets: (0, work_secret_1.parseWorkSecrets)(service.cwd, context.projectRoot, service.schema.secrets, envs, environment, context.secrets, service.schema.account),
|
|
118
119
|
resources: (0, work_resources_1.parseWorkResources)(service.schema.resources),
|
|
120
|
+
volumeSize: service.schema.volumeSize ?? null,
|
|
121
|
+
network: (0, work_network_1.parseWorkNetwork)(service.schema.network),
|
|
122
|
+
networkCeiling: null,
|
|
123
|
+
args: service.schema.args ?? null,
|
|
119
124
|
};
|
|
120
125
|
}
|
|
121
126
|
}
|
|
@@ -19,8 +19,17 @@ const get_work_runtime_1 = require("./get-work-runtime");
|
|
|
19
19
|
const resolve_cache_1 = require("../../cache/resolve-cache");
|
|
20
20
|
const units_1 = require("../../utils/units");
|
|
21
21
|
const work_resources_1 = require("../work-resources");
|
|
22
|
+
const job_placement_1 = require("../../kubernetes/job-placement");
|
|
23
|
+
const work_network_1 = require("../work-network");
|
|
22
24
|
function appendWorkTask(workTree, cwd, referenceTask, environment, context) {
|
|
23
|
-
const
|
|
25
|
+
const parsed = parseTask(cwd, referenceTask, environment, context);
|
|
26
|
+
// on a server's cluster a local task runs in a tool image like a container task
|
|
27
|
+
const server = workTree.environment.type === 'kubernetes' ? workTree.environment.server : undefined;
|
|
28
|
+
const task = !server
|
|
29
|
+
? parsed
|
|
30
|
+
: parsed.type === 'local-task'
|
|
31
|
+
? (0, job_placement_1.runInToolImage)(parsed, server, referenceTask.schema.platform)
|
|
32
|
+
: (0, job_placement_1.placeOnPool)(parsed, server);
|
|
24
33
|
if (!workTree.tasks[task.name]) {
|
|
25
34
|
const workItem = {
|
|
26
35
|
// A dependency cycle is rejected before anything runs (checkForLoop), but
|
|
@@ -72,6 +81,9 @@ function parseTask(cwd, task, environment, context) {
|
|
|
72
81
|
timeout: task.schema.timeout ? (0, units_1.parseDuration)(task.schema.timeout) : null,
|
|
73
82
|
secrets: (0, work_secret_1.parseWorkSecrets)(cwd, context.projectRoot, task.schema.secrets, envs, environment, context.secrets, task.schema.account),
|
|
74
83
|
resources: (0, work_resources_1.parseWorkResources)(task.schema.resources),
|
|
84
|
+
network: (0, work_network_1.parseWorkNetwork)(task.schema.network),
|
|
85
|
+
networkCeiling: null,
|
|
86
|
+
args: task.schema.args ?? null,
|
|
75
87
|
runner: task.schema.runner ?? task.scope.schema.runner ?? null,
|
|
76
88
|
};
|
|
77
89
|
if ((0, build_file_task_schema_1.isBuildFileContainerTaskSchema)(task.schema)) {
|
|
@@ -81,6 +93,7 @@ function parseTask(cwd, task, environment, context) {
|
|
|
81
93
|
user: (0, get_container_user_1.getContainerUser)(),
|
|
82
94
|
image: (0, template_value_1.templateValue)(task.schema.image, envs),
|
|
83
95
|
mounts: (0, parse_work_mounts_1.parseWorkMounts)(cwd, task.schema, envs),
|
|
96
|
+
volumeSize: task.schema.volumeSize ?? null,
|
|
84
97
|
};
|
|
85
98
|
}
|
|
86
99
|
else {
|
|
@@ -88,6 +101,12 @@ function parseTask(cwd, task, environment, context) {
|
|
|
88
101
|
if (fileSecret) {
|
|
89
102
|
throw new Error(`task ${task.relativeName}: a local task takes secrets as env only (${(0, work_secret_1.getSecretName)(fileSecret)})`);
|
|
90
103
|
}
|
|
104
|
+
// on a server's cluster it runs in a pod of a pool's tool image (runInToolImage)
|
|
105
|
+
const contained = !!environment.hosted?.kubernetes || !!environment.hosted?.localTasksInContainers;
|
|
106
|
+
if (baseWorkNode.network.type !== 'open' && !contained) {
|
|
107
|
+
throw new Error(`task ${task.relativeName}: network ${(0, work_network_1.formatNetwork)(baseWorkNode.network)} needs a container, ` +
|
|
108
|
+
`hammerkit can't limit the network of a local task`);
|
|
109
|
+
}
|
|
91
110
|
return {
|
|
92
111
|
...baseWorkNode,
|
|
93
112
|
type: 'local-task',
|