hammerkit 1.27.0-next.1 → 1.27.0-next.11
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 +9 -0
- package/dist/cache/backends/registry-cache-backend.js +2 -2
- package/dist/executer/event-cache.js +20 -2
- package/dist/harness/sandbox-client.js +104 -66
- 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 +4 -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 +83 -26
- 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/secrets.js +7 -11
- package/dist/kubernetes/volumes.js +6 -3
- package/dist/log.js +1 -1
- package/dist/network/egress-proxy.js +45 -6
- package/dist/optimizer/work-task-cache-description.js +2 -0
- package/dist/planner/utils/append-work-dependencies.js +5 -2
- package/dist/planner/utils/append-work-service.js +1 -0
- package/dist/planner/utils/append-work-task.js +4 -1
- package/dist/planner/utils/task-arguments.js +3 -0
- package/dist/planner/work-runtime-docker.js +2 -1
- package/dist/planner/work-runtime-kubernetes.js +74 -48
- package/dist/planner/work-runtime-local.js +2 -1
- package/dist/program.js +12 -5
- package/dist/remote/annotations.js +13 -1
- package/dist/remote/remote-commands.js +11 -2
- package/dist/remote/remote-work-commands.js +89 -1
- 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/reference-parser.js +1 -0
- package/dist/secrets/github-app-token-command.js +4 -1
- package/dist/server/executor.js +1 -1
- package/dist/server/kubernetes-backend.js +80 -31
- package/dist/server/kubernetes-sandbox-storage.js +2 -1
- package/dist/server/oauth.js +12 -1
- package/dist/server/plan.js +46 -10
- package/dist/server/run-plan.js +12 -15
- package/dist/server/run-request.js +6 -0
- package/dist/server/runs.js +10 -3
- package/dist/server/sandboxes-api.js +10 -4
- package/dist/server/server-command.js +39 -20
- package/dist/server/server-config.js +35 -4
- package/dist/server/server.js +16 -2
- package/dist/utils/abort-on-signals.js +46 -6
- package/dist/utils/tar.js +36 -0
- package/package.json +1 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getJobSecretSpec = exports.removeJobCredentials = exports.assertJobAccounts = exports.ensureJobCredentials = exports.getCredentialsName = exports.
|
|
3
|
+
exports.getJobSecretSpec = exports.removeJobCredentials = exports.assertJobAccounts = exports.ensureJobCredentials = exports.getCredentialsName = exports.getSecretHosts = exports.splitJobSecrets = exports.ACCOUNT_PATH = exports.SECRET_VALUES_PATH = void 0;
|
|
4
4
|
const work_secret_1 = require("../planner/work-secret");
|
|
5
5
|
const command_secret_provider_1 = require("../secrets/command-secret-provider");
|
|
6
|
+
const task_arguments_1 = require("../planner/utils/task-arguments");
|
|
6
7
|
const version_1 = require("../version");
|
|
7
8
|
const apply_1 = require("./apply");
|
|
8
9
|
const resources_1 = require("./resources");
|
|
@@ -42,25 +43,30 @@ function splitJobSecrets(secrets, server) {
|
|
|
42
43
|
exports.splitJobSecrets = splitJobSecrets;
|
|
43
44
|
/**
|
|
44
45
|
* The init containers that read secrets share the pod, and so the NetworkPolicy, of
|
|
45
|
-
* the task: under a limited network they
|
|
46
|
-
*
|
|
47
|
-
*
|
|
46
|
+
* the task: under a limited network they reach the secret manager only through the
|
|
47
|
+
* task's egress proxy, which lets them reach the endpoints the server names for the
|
|
48
|
+
* provider (`backend.providers.<name>.allow`). A task that reads a secret of a provider
|
|
49
|
+
* without them keeps an open network. Answers the endpoints the proxy lets through.
|
|
48
50
|
*/
|
|
49
|
-
function
|
|
50
|
-
if (secrets.length === 0 || network.type === 'open') {
|
|
51
|
-
return;
|
|
51
|
+
function getSecretHosts(task, secrets, server, network = (0, work_network_1.getEffectiveNetwork)(task.data)) {
|
|
52
|
+
if (!server || secrets.length === 0 || network.type === 'open') {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
const unreachable = secrets.find((secret) => !server.providers[secret.source.provider]?.allow?.length);
|
|
56
|
+
if (unreachable) {
|
|
57
|
+
throw new Error(`task ${task.name}: secret ${(0, work_secret_1.getSecretName)(unreachable.secret)} is read from ${(0, work_secret_1.describeSecretSource)(unreachable.secret)} in the Job of the task, which network ${(0, work_network_1.formatNetwork)(network)} would cut off from it; ` +
|
|
58
|
+
`on this cluster a task that reads secrets of ${unreachable.source.provider} keeps an open network, ` +
|
|
59
|
+
`unless the server names its endpoints in backend.providers.${unreachable.source.provider}.allow`);
|
|
52
60
|
}
|
|
53
|
-
|
|
54
|
-
throw new Error(`task ${task.name}: secret ${(0, work_secret_1.getSecretName)(secret.secret)} is read from ${(0, work_secret_1.describeSecretSource)(secret.secret)} in the Job of the task, which network ${(0, work_network_1.formatNetwork)(network)} would cut off from it; ` +
|
|
55
|
-
'on this cluster a task that reads secrets of a provider keeps an open network');
|
|
61
|
+
return [...new Set(secrets.flatMap((secret) => server.providers[secret.source.provider].allow ?? []))].sort();
|
|
56
62
|
}
|
|
57
|
-
exports.
|
|
63
|
+
exports.getSecretHosts = getSecretHosts;
|
|
58
64
|
// The accounts of the secrets, in the order they are first read
|
|
59
65
|
function accountsOf(secrets) {
|
|
60
66
|
return [...new Set(secrets.map((s) => s.source.account))];
|
|
61
67
|
}
|
|
62
|
-
function getCredentialsName(item) {
|
|
63
|
-
return (0, resources_1.getResourceName)(item, '-credentials');
|
|
68
|
+
function getCredentialsName(item, kubernetes) {
|
|
69
|
+
return (0, resources_1.getResourceName)(item, kubernetes, '-credentials');
|
|
64
70
|
}
|
|
65
71
|
exports.getCredentialsName = getCredentialsName;
|
|
66
72
|
// The keys of the credentials Secret: `<account>.token` and `<account>.file.<key>`
|
|
@@ -130,9 +136,9 @@ async function ensureJobCredentials(instance, kubernetes, item, secrets, admitte
|
|
|
130
136
|
apiVersion: 'v1',
|
|
131
137
|
metadata: {
|
|
132
138
|
namespace: kubernetes.namespace,
|
|
133
|
-
name: getCredentialsName(item),
|
|
139
|
+
name: getCredentialsName(item, kubernetes),
|
|
134
140
|
annotations: { 'hammerkit.dev/version': (0, version_1.getVersion)() },
|
|
135
|
-
labels:
|
|
141
|
+
labels: (0, resources_1.getItemLabels)(item, kubernetes),
|
|
136
142
|
},
|
|
137
143
|
type: 'Opaque',
|
|
138
144
|
data,
|
|
@@ -163,7 +169,10 @@ async function removeJobCredentials(instance, kubernetes, item) {
|
|
|
163
169
|
return;
|
|
164
170
|
}
|
|
165
171
|
try {
|
|
166
|
-
await instance.coreApi.deleteNamespacedSecret({
|
|
172
|
+
await instance.coreApi.deleteNamespacedSecret({
|
|
173
|
+
name: getCredentialsName(item, kubernetes),
|
|
174
|
+
namespace: kubernetes.namespace,
|
|
175
|
+
});
|
|
167
176
|
}
|
|
168
177
|
catch (e) {
|
|
169
178
|
if ((0, apply_1.statusCodeOf)(e) !== 404) {
|
|
@@ -186,7 +195,9 @@ function quote(value) {
|
|
|
186
195
|
* from it, and the task's output is masked before it leaves the pod, since the
|
|
187
196
|
* server, which masks everything else, never sees these values.
|
|
188
197
|
*/
|
|
189
|
-
function getJobSecretSpec(item, secrets, server, credentials, environment
|
|
198
|
+
function getJobSecretSpec(item, secrets, server, credentials, environment,
|
|
199
|
+
// under a limited network, what sends their requests through the egress proxy
|
|
200
|
+
proxyEnv = []) {
|
|
190
201
|
if (!server || secrets.length === 0) {
|
|
191
202
|
return {
|
|
192
203
|
initContainers: [],
|
|
@@ -205,7 +216,7 @@ function getJobSecretSpec(item, secrets, server, credentials, environment) {
|
|
|
205
216
|
}
|
|
206
217
|
const name = `hammerkit-account-${index}`;
|
|
207
218
|
accountVolume.set(account, name);
|
|
208
|
-
volumes.push({ name, secret: { secretName: getCredentialsName(item), items, defaultMode: 0o444 } });
|
|
219
|
+
volumes.push({ name, secret: { secretName: getCredentialsName(item, { server }), items, defaultMode: 0o444 } });
|
|
209
220
|
});
|
|
210
221
|
const initContainers = secrets.map(({ index, secret, source }) => {
|
|
211
222
|
const label = `secret ${(0, work_secret_1.getSecretName)(secret)} (${(0, work_secret_1.describeSecretSource)(secret)})`;
|
|
@@ -229,8 +240,11 @@ function getJobSecretSpec(item, secrets, server, credentials, environment) {
|
|
|
229
240
|
'-c',
|
|
230
241
|
'umask 022 && "$@" > "$HAMMERKIT_VALUE.part" && mv "$HAMMERKIT_VALUE.part" "$HAMMERKIT_VALUE"',
|
|
231
242
|
],
|
|
232
|
-
|
|
243
|
+
// Kubernetes would expand a `$(NAME)` of the container's env in them, the proxy's token
|
|
244
|
+
// among them: the reference is text, as on any other backend
|
|
245
|
+
args: ['sh', command, ...args].map(task_arguments_1.escapeKubernetesText),
|
|
233
246
|
env: [
|
|
247
|
+
...proxyEnv,
|
|
234
248
|
...Object.entries(env).map(([name, value]) => ({ name, value })),
|
|
235
249
|
{ name: 'HAMMERKIT_VALUE', value: `${exports.SECRET_VALUES_PATH}/secret-${index}` },
|
|
236
250
|
],
|
|
@@ -5,6 +5,7 @@ const crypto_1 = require("crypto");
|
|
|
5
5
|
const apply_1 = require("./apply");
|
|
6
6
|
const sleep_1 = require("../utils/sleep");
|
|
7
7
|
const job_placement_1 = require("./job-placement");
|
|
8
|
+
const resources_1 = require("./resources");
|
|
8
9
|
// On a server with `backend.keepVolumes`, the claim of a task stays after the run, so the
|
|
9
10
|
// next run of the same source finds the outputs in it and mounts them without restoring
|
|
10
11
|
// them from the cache. A kept claim has no run label (the end of the run removes what has
|
|
@@ -18,11 +19,15 @@ exports.STATE_ANNOTATION = 'hammerkit.dev/state';
|
|
|
18
19
|
// when it goes, set by the end of the run that mounted it last
|
|
19
20
|
exports.EXPIRES_ANNOTATION = 'hammerkit.dev/expires';
|
|
20
21
|
// The claim of a work item: a task's on a server that keeps them carries the scope, so a
|
|
21
|
-
// run of another source never mounts it
|
|
22
|
+
// run of another source never mounts it, and the runs of one scope go one at a time
|
|
23
|
+
// (KubernetesExecutor.exclusive). Any other claim on a server is the run's own, as the
|
|
24
|
+
// runs of a caller share their namespace. 63 characters at most, a pod's volume name.
|
|
22
25
|
function getClaimName(work, kubernetes) {
|
|
23
|
-
const scope = kubernetes
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
const scope = kubernetes.server?.volumeScope;
|
|
27
|
+
if (scope && isTask(work)) {
|
|
28
|
+
return `hammerkit-${work.id()}-${(0, crypto_1.createHash)('sha256').update(scope).digest('hex').slice(0, 12)}`;
|
|
29
|
+
}
|
|
30
|
+
return 'hammerkit-' + (0, resources_1.getItemKey)(work, kubernetes);
|
|
26
31
|
}
|
|
27
32
|
exports.getClaimName = getClaimName;
|
|
28
33
|
// whether the claim of the work item stays after the run
|
|
@@ -156,14 +161,17 @@ async function expireKeptClaims(instance, namespace, runLabel, expires) {
|
|
|
156
161
|
}
|
|
157
162
|
}
|
|
158
163
|
exports.expireKeptClaims = expireKeptClaims;
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
// the others expires.
|
|
164
|
+
// The kept claims that expired, and those a run left without an expiry because the server
|
|
165
|
+
// stopped before its end (all of them when the server keeps none any more), but those of
|
|
166
|
+
// the scopes `inUse`: a run of the scope executes and mounts them, or may in a moment.
|
|
167
|
+
// The names of those removed, and when the next of the others expires.
|
|
163
168
|
async function sweepKeptClaims(instance, namespace, options) {
|
|
164
169
|
const expired = [];
|
|
165
170
|
let next = null;
|
|
166
171
|
for (const claim of await listKeptClaims(instance, namespace)) {
|
|
172
|
+
if (options.inUse?.has(claim.metadata?.labels?.[exports.VOLUME_SCOPE_LABEL] ?? '')) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
167
175
|
const expires = Date.parse(claim.metadata?.annotations?.[exports.EXPIRES_ANNOTATION] ?? '');
|
|
168
176
|
if (!options.keep || !(expires > options.now.getTime())) {
|
|
169
177
|
expired.push(claim);
|
|
@@ -178,14 +186,18 @@ async function sweepKeptClaims(instance, namespace, options) {
|
|
|
178
186
|
exports.sweepKeptClaims = sweepKeptClaims;
|
|
179
187
|
// When a run needs room under the quota of its namespace: the kept claims it does not use
|
|
180
188
|
// go, those of the other sources and those of its own a task of this run did not take (one
|
|
181
|
-
// renamed or changed since).
|
|
189
|
+
// renamed or changed since). Of another scope, only one its last run released, which has an
|
|
190
|
+
// expiry: one without is mounted by a run of the caller that executes now (or was left by
|
|
191
|
+
// a server that stopped, which the next sweep removes). The names of those removed.
|
|
182
192
|
async function evictKeptClaims(instance, kubernetes, timeout) {
|
|
183
193
|
const server = kubernetes.server;
|
|
184
194
|
if (!server?.volumeScope) {
|
|
185
195
|
return [];
|
|
186
196
|
}
|
|
187
197
|
const unused = await listKeptClaims(instance, kubernetes.namespace, `${exports.VOLUME_SCOPE_LABEL},${exports.KEPT_BY_LABEL}!=${(0, job_placement_1.getRunLabelValue)(server.run)}`);
|
|
188
|
-
|
|
198
|
+
const released = unused.filter((claim) => claim.metadata?.labels?.[exports.VOLUME_SCOPE_LABEL] === server.volumeScope ||
|
|
199
|
+
!!claim.metadata?.annotations?.[exports.EXPIRES_ANNOTATION]);
|
|
200
|
+
return removeClaims(instance, kubernetes.namespace, released, timeout);
|
|
189
201
|
}
|
|
190
202
|
exports.evictKeptClaims = evictKeptClaims;
|
|
191
203
|
//# sourceMappingURL=kept-claims.js.map
|
|
@@ -17,12 +17,15 @@ const sleep_1 = require("../utils/sleep");
|
|
|
17
17
|
const network_gate_1 = require("./network-gate");
|
|
18
18
|
const job_placement_1 = require("./job-placement");
|
|
19
19
|
Object.defineProperty(exports, "RUN_LABEL", { enumerable: true, get: function () { return job_placement_1.RUN_LABEL; } });
|
|
20
|
-
const
|
|
20
|
+
const resources_1 = require("./resources");
|
|
21
21
|
const PROXY_LABEL = 'hammerkit.dev/proxy-for';
|
|
22
22
|
const NETWORK_OF_LABEL = 'hammerkit.dev/network-of';
|
|
23
23
|
// on the proxy of a task on a sandbox: the sandbox, whose pod of files it stays with
|
|
24
24
|
const PROXY_OF_SANDBOX_LABEL = 'hammerkit.dev/proxy-of-sandbox';
|
|
25
25
|
const PROXY_READY_TIMEOUT_MS = 60000;
|
|
26
|
+
// the token of the endpoints of secret providers, in a Secret named like the proxy
|
|
27
|
+
const SECRET_TOKEN_KEY = 'token';
|
|
28
|
+
const SECRET_TOKEN_VARIABLE = 'HAMMERKIT_PROXY_TOKEN';
|
|
26
29
|
// how long the pods of an item may take to end before their policy goes
|
|
27
30
|
const POD_END_TIMEOUT_MS = 120000;
|
|
28
31
|
const POD_END_POLL_MS = 500;
|
|
@@ -83,7 +86,7 @@ function getNetworkPolicyName(item, scope) {
|
|
|
83
86
|
exports.getNetworkPolicyName = getNetworkPolicyName;
|
|
84
87
|
// the pods of the item, of the run when the scope has one
|
|
85
88
|
function getItemPodLabels(item, scope) {
|
|
86
|
-
return { [ID_LABEL]: item.id(), ...(scope.run ? { [job_placement_1.RUN_LABEL]: scope.run } : {}) };
|
|
89
|
+
return { [resources_1.ID_LABEL]: item.id(), ...(scope.run ? { [job_placement_1.RUN_LABEL]: scope.run } : {}) };
|
|
87
90
|
}
|
|
88
91
|
function toSelector(labels) {
|
|
89
92
|
return Object.entries(labels)
|
|
@@ -104,15 +107,20 @@ function getIngress(env, item) {
|
|
|
104
107
|
if (ids.length === 0) {
|
|
105
108
|
return [];
|
|
106
109
|
}
|
|
107
|
-
|
|
110
|
+
// on a server, of this run: the runs of a caller share their namespace
|
|
111
|
+
const run = (0, job_placement_1.getRunLabels)(env);
|
|
112
|
+
const from = {
|
|
113
|
+
matchExpressions: [{ key: resources_1.ID_LABEL, operator: 'In', values: ids }],
|
|
114
|
+
...(Object.keys(run).length > 0 ? { matchLabels: run } : {}),
|
|
115
|
+
};
|
|
108
116
|
// the client's name for `from`, a reserved word
|
|
109
117
|
return [{ _from: [{ podSelector: from }] }];
|
|
110
118
|
}
|
|
111
119
|
// The NetworkPolicy of a limited item: egress to the pods of the services it
|
|
112
|
-
// needs and, under `allow
|
|
113
|
-
// and the proxy are host aliases). Ingress
|
|
114
|
-
// item has none.
|
|
115
|
-
function buildNetworkPolicy(env, item, network, scope, proxy = getRunProxy(item, scope)) {
|
|
120
|
+
// needs and, under `allow` or with secrets read through it, to its proxy;
|
|
121
|
+
// nothing else, DNS included (needs and the proxy are host aliases). Ingress
|
|
122
|
+
// only from what needs it. An open item has none.
|
|
123
|
+
function buildNetworkPolicy(env, item, network, scope, proxy = getRunProxy(item, scope), proxied = network.type === 'allow') {
|
|
116
124
|
const needs = item.needs.filter((need) => (0, work_item_1.isContainerWorkServiceItem)(need.service));
|
|
117
125
|
return {
|
|
118
126
|
apiVersion: 'networking.k8s.io/v1',
|
|
@@ -129,9 +137,9 @@ function buildNetworkPolicy(env, item, network, scope, proxy = getRunProxy(item,
|
|
|
129
137
|
ingress: getIngress(env, item),
|
|
130
138
|
egress: [
|
|
131
139
|
...(needs.length > 0
|
|
132
|
-
? [{ to: needs.map((need) => ({ podSelector: { matchLabels:
|
|
140
|
+
? [{ to: needs.map((need) => ({ podSelector: { matchLabels: (0, resources_1.getItemLabels)(need.service, env) } })) }]
|
|
133
141
|
: []),
|
|
134
|
-
...(
|
|
142
|
+
...(proxied
|
|
135
143
|
? [
|
|
136
144
|
{
|
|
137
145
|
to: [{ podSelector: { matchLabels: { [PROXY_LABEL]: proxy.key } } }],
|
|
@@ -179,6 +187,9 @@ exports.buildProxyPolicy = buildProxyPolicy;
|
|
|
179
187
|
// left goes.
|
|
180
188
|
// A task follows the proxy's log while its jobs run. A service outlives the
|
|
181
189
|
// run on a cluster, so its refused hosts are in the proxy's own log.
|
|
190
|
+
// `secretHosts` are the endpoints of the providers whose secrets the init
|
|
191
|
+
// containers of a task on a server read: a limited task gets a proxy for them
|
|
192
|
+
// (under `none` too), which passes them to those containers only.
|
|
182
193
|
async function ensureKubernetesItemNetwork(instance, env, item, options) {
|
|
183
194
|
const network = (0, work_network_1.getEffectiveNetwork)(item.data);
|
|
184
195
|
if (network.type !== 'open' && !env.networkPolicyEnforced) {
|
|
@@ -191,22 +202,30 @@ async function ensureKubernetesItemNetwork(instance, env, item, options) {
|
|
|
191
202
|
// without the rights to manage NetworkPolicies, hammerkit never made one
|
|
192
203
|
await removeKubernetesItemNetwork(instance, env, item, { run: null }, { ignoreForbidden: true });
|
|
193
204
|
}
|
|
194
|
-
return { env: [], hostAliases, initContainers: [], close() { } };
|
|
205
|
+
return { env: [], secretEnv: [], hostAliases, initContainers: [], close() { } };
|
|
206
|
+
}
|
|
207
|
+
const secretHosts = options.secretHosts ?? [];
|
|
208
|
+
if (secretHosts.length > 0 && options.sandbox) {
|
|
209
|
+
// a run on a sandbox uses no account, so it reads no secret of a provider
|
|
210
|
+
throw new Error(`${item.name}: a task on a sandbox reads no secret of a provider`);
|
|
195
211
|
}
|
|
196
212
|
const proxy = network.type === 'allow' && options.sandbox
|
|
197
213
|
? getSandboxProxy(env, item, network, hostAliases, options.sandbox)
|
|
198
214
|
: getRunProxy(item, options);
|
|
199
|
-
|
|
215
|
+
const proxied = network.type === 'allow' || secretHosts.length > 0;
|
|
216
|
+
await (0, apply_1.apply)(instance, buildNetworkPolicy(env, item, network, options, proxy, proxied));
|
|
200
217
|
const initContainers = [(0, network_gate_1.getNetworkGateContainer)()];
|
|
201
|
-
if (
|
|
218
|
+
if (!proxied) {
|
|
202
219
|
if (options.sandbox) {
|
|
203
220
|
await removeSandboxProxies(instance, env.namespace, { sandbox: options.sandbox.label, item });
|
|
204
221
|
}
|
|
205
222
|
else {
|
|
206
223
|
await removeProxy(instance, env.namespace, proxy.name, [404]);
|
|
207
224
|
}
|
|
208
|
-
return { env: [], hostAliases, initContainers, close() { } };
|
|
225
|
+
return { env: [], secretEnv: [], hostAliases, initContainers, close() { } };
|
|
209
226
|
}
|
|
227
|
+
// under `none`, a proxy for the secrets only: it allows the item no host
|
|
228
|
+
const proxyNetwork = network.type === 'allow' ? network : { type: 'allow', hosts: [] };
|
|
210
229
|
let started;
|
|
211
230
|
try {
|
|
212
231
|
if (proxy.sandbox) {
|
|
@@ -214,7 +233,7 @@ async function ensureKubernetesItemNetwork(instance, env, item, options) {
|
|
|
214
233
|
await removeSandboxProxies(instance, env.namespace, { sandbox: proxy.sandbox.label, item }, (deployment) => deployment.metadata?.labels?.[PROXY_LABEL] === proxy.key);
|
|
215
234
|
proxy.owner = await getFilesPodOwner(instance, env, proxy.sandbox);
|
|
216
235
|
}
|
|
217
|
-
started = await ensureProxy(instance, env, item,
|
|
236
|
+
started = await ensureProxy(instance, env, item, proxyNetwork, hostAliases, options, proxy, secretHosts);
|
|
218
237
|
}
|
|
219
238
|
catch (e) {
|
|
220
239
|
await removeKubernetesItemNetwork(instance, env, item, options).catch(() => undefined);
|
|
@@ -223,11 +242,17 @@ async function ensureKubernetesItemNetwork(instance, env, item, options) {
|
|
|
223
242
|
const follow = options.followLog
|
|
224
243
|
? await followProxyLog(instance, env, item, proxy, { tailLines: started.reused ? 0 : undefined })
|
|
225
244
|
: null;
|
|
245
|
+
const needs = item.needs.map((need) => need.name);
|
|
246
|
+
const toEnv = (variables) => Object.entries(variables).map(([name, value]) => ({ name, value }));
|
|
226
247
|
return {
|
|
227
|
-
env:
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
248
|
+
env: network.type === 'allow' ? toEnv((0, egress_proxy_1.getProxyClientEnvironment)(needs)) : [],
|
|
249
|
+
// the token from the proxy's Secret, which the item's own container never mounts
|
|
250
|
+
secretEnv: secretHosts.length > 0
|
|
251
|
+
? [
|
|
252
|
+
{ name: SECRET_TOKEN_VARIABLE, valueFrom: { secretKeyRef: { name: proxy.name, key: SECRET_TOKEN_KEY } } },
|
|
253
|
+
...toEnv((0, egress_proxy_1.getProxyClientEnvironment)(needs, `$(${SECRET_TOKEN_VARIABLE})`)),
|
|
254
|
+
]
|
|
255
|
+
: [],
|
|
231
256
|
// the item reaches each TCP forward under its host's name, at the proxy
|
|
232
257
|
hostAliases: [
|
|
233
258
|
...hostAliases,
|
|
@@ -286,12 +311,14 @@ async function releaseKubernetesItemNetwork(instance, env, item, scope) {
|
|
|
286
311
|
}
|
|
287
312
|
exports.releaseKubernetesItemNetwork = releaseKubernetesItemNetwork;
|
|
288
313
|
// Every policy and proxy of the item, of any run, once none of its pods runs:
|
|
289
|
-
// what `down` and `clean` remove.
|
|
314
|
+
// what `down` and `clean` remove. On a server, those of the server's run only: the
|
|
315
|
+
// runs of a caller share their namespace, and the pods of another run of the same item
|
|
316
|
+
// would run unlimited without their policy.
|
|
290
317
|
async function removeAllKubernetesItemNetworks(instance, env, item) {
|
|
291
|
-
if (!(await awaitPodsEnded(instance, env,
|
|
318
|
+
if (!(await awaitPodsEnded(instance, env, (0, resources_1.getItemLabels)(item, env)))) {
|
|
292
319
|
throw new Error(`pods of ${item.name} are still running, so its network policy stays`);
|
|
293
320
|
}
|
|
294
|
-
const labelSelector =
|
|
321
|
+
const labelSelector = toSelector({ [NETWORK_OF_LABEL]: item.id(), ...(0, job_placement_1.getRunLabels)(env) });
|
|
295
322
|
let policies;
|
|
296
323
|
try {
|
|
297
324
|
const list = await instance.objectApi.list('networking.k8s.io/v1', 'NetworkPolicy', env.namespace, undefined, undefined, undefined, undefined, labelSelector);
|
|
@@ -318,6 +345,7 @@ async function removeAllKubernetesItemNetworks(instance, env, item) {
|
|
|
318
345
|
if (name) {
|
|
319
346
|
await ignoreStatus(instance.appsApi.deleteNamespacedDeployment({ name, namespace: env.namespace }), [404]);
|
|
320
347
|
await ignoreStatus(instance.coreApi.deleteNamespacedService({ name, namespace: env.namespace }), [404]);
|
|
348
|
+
await ignoreStatus(instance.coreApi.deleteNamespacedSecret({ name, namespace: env.namespace }), [404]);
|
|
321
349
|
}
|
|
322
350
|
}
|
|
323
351
|
}
|
|
@@ -349,7 +377,7 @@ function getProxyReadinessScript(ports) {
|
|
|
349
377
|
`.on('error', () => process.exit(1))`);
|
|
350
378
|
}
|
|
351
379
|
exports.getProxyReadinessScript = getProxyReadinessScript;
|
|
352
|
-
async function ensureProxy(instance, env, item, network, hostAliases, scope, proxy) {
|
|
380
|
+
async function ensureProxy(instance, env, item, network, hostAliases, scope, proxy, secretHosts = []) {
|
|
353
381
|
const name = proxy.name;
|
|
354
382
|
const labels = { [PROXY_LABEL]: proxy.key };
|
|
355
383
|
const metadata = {
|
|
@@ -388,7 +416,17 @@ async function ensureProxy(instance, env, item, network, hostAliases, scope, pro
|
|
|
388
416
|
name: 'proxy',
|
|
389
417
|
image: egress_proxy_1.EGRESS_PROXY_IMAGE,
|
|
390
418
|
command: (0, egress_proxy_1.getEgressProxyCommand)(),
|
|
391
|
-
env:
|
|
419
|
+
env: [
|
|
420
|
+
...Object.entries({ ...(0, egress_proxy_1.getItemProxyEnvironment)(item), ...(0, egress_proxy_1.getSecretProxyEnvironment)(secretHosts) }).map(([key, value]) => ({ name: key, value })),
|
|
421
|
+
...(secretHosts.length > 0
|
|
422
|
+
? [
|
|
423
|
+
{
|
|
424
|
+
name: 'HAMMERKIT_SECRET_TOKEN',
|
|
425
|
+
valueFrom: { secretKeyRef: { name, key: SECRET_TOKEN_KEY } },
|
|
426
|
+
},
|
|
427
|
+
]
|
|
428
|
+
: []),
|
|
429
|
+
],
|
|
392
430
|
ports: (0, egress_proxy_1.getEgressProxyPorts)(network).map((port) => ({ containerPort: port })),
|
|
393
431
|
// in the pod, since its ingress policy may keep the kubelet out;
|
|
394
432
|
// ready once it listens on every port, its TCP forwards' too
|
|
@@ -422,6 +460,21 @@ async function ensureProxy(instance, env, item, network, hostAliases, scope, pro
|
|
|
422
460
|
? await instance.objectApi.read(deployment).then((current) => !current.metadata?.deletionTimestamp, (e) => ((0, apply_1.statusCodeOf)(e) === 404 ? false : Promise.reject(e)))
|
|
423
461
|
: false;
|
|
424
462
|
await (0, apply_1.apply)(instance, buildProxyPolicy(env, item, network, scope, proxy));
|
|
463
|
+
if (secretHosts.length > 0) {
|
|
464
|
+
// new with each run's proxy, and kept while it is there: its pod read it when it started.
|
|
465
|
+
// The init containers that read secrets get it, the task not.
|
|
466
|
+
const secret = {
|
|
467
|
+
apiVersion: 'v1',
|
|
468
|
+
kind: 'Secret',
|
|
469
|
+
metadata: { ...metadata, annotations: { 'hammerkit.dev/version': (0, version_1.getVersion)() } },
|
|
470
|
+
type: 'Opaque',
|
|
471
|
+
data: { [SECRET_TOKEN_KEY]: Buffer.from((0, crypto_1.randomBytes)(32).toString('hex')).toString('base64') },
|
|
472
|
+
};
|
|
473
|
+
const exists = await instance.objectApi.read(secret).then(() => true, (e) => ((0, apply_1.statusCodeOf)(e) === 404 ? false : Promise.reject(e)));
|
|
474
|
+
if (!exists) {
|
|
475
|
+
await instance.objectApi.create(secret);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
425
478
|
await (0, apply_1.apply)(instance, deployment);
|
|
426
479
|
const created = await (0, apply_1.apply)(instance, service);
|
|
427
480
|
await (0, await_running_state_1.awaitDeployRunningState)(instance, env, name, {
|
|
@@ -433,7 +486,7 @@ async function ensureProxy(instance, env, item, network, hostAliases, scope, pro
|
|
|
433
486
|
if (!ip) {
|
|
434
487
|
throw new Error(`the egress proxy of ${item.name} has no cluster ip`);
|
|
435
488
|
}
|
|
436
|
-
item.status.write('debug', `${reused ? 'egress proxy of the sandbox' : 'egress proxy'} allows ${network.hosts.join(', ') || 'no host'}`);
|
|
489
|
+
item.status.write('debug', `${reused ? 'egress proxy of the sandbox' : 'egress proxy'} allows ${network.hosts.join(', ') || 'no host'}${secretHosts.length > 0 ? `, and ${secretHosts.join(', ')} to the reads of secrets` : ''}`);
|
|
437
490
|
return { ip, reused };
|
|
438
491
|
}
|
|
439
492
|
// Refused hosts show up in the item's log, as on Docker; of a proxy from an
|
|
@@ -468,9 +521,13 @@ async function followProxyLog(instance, env, item, proxy, options) {
|
|
|
468
521
|
return null;
|
|
469
522
|
}
|
|
470
523
|
}
|
|
471
|
-
|
|
524
|
+
// The token of a proxy for secrets goes with it; a proxy of a sandbox has none.
|
|
525
|
+
async function removeProxy(instance, namespace, name, ignored, options = { token: true }) {
|
|
472
526
|
await ignoreStatus(instance.appsApi.deleteNamespacedDeployment({ name, namespace }), ignored);
|
|
473
527
|
await ignoreStatus(instance.coreApi.deleteNamespacedService({ name, namespace }), ignored);
|
|
528
|
+
if (options.token) {
|
|
529
|
+
await ignoreStatus(instance.coreApi.deleteNamespacedSecret({ name, namespace }), ignored);
|
|
530
|
+
}
|
|
474
531
|
await ignoreStatus(instance.objectApi.delete({
|
|
475
532
|
apiVersion: 'networking.k8s.io/v1',
|
|
476
533
|
kind: 'NetworkPolicy',
|
|
@@ -492,7 +549,7 @@ async function removeSandboxProxies(instance, namespace, of, keep = () => false)
|
|
|
492
549
|
for (const deployment of deployments.items) {
|
|
493
550
|
const name = deployment.metadata?.name;
|
|
494
551
|
if (name && !keep(deployment)) {
|
|
495
|
-
await removeProxy(instance, namespace, name, [404]);
|
|
552
|
+
await removeProxy(instance, namespace, name, [404], { token: false });
|
|
496
553
|
}
|
|
497
554
|
}
|
|
498
555
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.removePersistentData = void 0;
|
|
4
|
+
const resources_1 = require("./resources");
|
|
4
5
|
async function removePersistentData(instance, kubernetes, task) {
|
|
5
|
-
|
|
6
|
+
// on a server, the run's: a claim it kept has no run label and goes once it expired
|
|
7
|
+
const labelSelector = (0, resources_1.getItemSelector)(task, kubernetes);
|
|
6
8
|
const configmaps = await instance.coreApi.listNamespacedConfigMap({ namespace: kubernetes.namespace, labelSelector });
|
|
7
9
|
for (const configMap of configmaps.items) {
|
|
8
10
|
if (configMap.metadata?.name) {
|
|
@@ -1,12 +1,34 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.removeInvalidCharacters = exports.getResourceName = void 0;
|
|
3
|
+
exports.removeInvalidCharacters = exports.getResourceName = exports.getItemSelector = exports.getItemLabels = exports.getItemKey = exports.ID_LABEL = void 0;
|
|
4
|
+
const job_placement_1 = require("./job-placement");
|
|
4
5
|
// An object name is a DNS label: lowercase letters, digits and `-`, at most 63
|
|
5
6
|
// characters, starting and ending with a letter or digit. The id makes a name
|
|
6
7
|
// unique; the item's name is there for people and is cut to fit.
|
|
7
8
|
const MAX_NAME = 63;
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
exports.ID_LABEL = 'hammerkit.dev/id';
|
|
10
|
+
// What tells the objects of an item apart in its namespace: its id, and on a server the
|
|
11
|
+
// run's label too. The runs of a caller execute side by side in their namespace, and two
|
|
12
|
+
// of them often run the same task (the same command on two sandboxes of a repository),
|
|
13
|
+
// so each has objects of its own, which nothing of the other run finds or removes. The
|
|
14
|
+
// first 8 digits of the run label tell runs apart, as they do for a run's proxies.
|
|
15
|
+
function getItemKey(item, env) {
|
|
16
|
+
return env.server ? `${item.id().substring(0, 32)}-${(0, job_placement_1.getRunLabelValue)(env.server.run).substring(0, 8)}` : item.id();
|
|
17
|
+
}
|
|
18
|
+
exports.getItemKey = getItemKey;
|
|
19
|
+
// The labels the objects of an item carry, by which they are found again
|
|
20
|
+
function getItemLabels(item, env) {
|
|
21
|
+
return { [exports.ID_LABEL]: item.id(), ...(0, job_placement_1.getRunLabels)(env) };
|
|
22
|
+
}
|
|
23
|
+
exports.getItemLabels = getItemLabels;
|
|
24
|
+
function getItemSelector(item, env) {
|
|
25
|
+
return Object.entries(getItemLabels(item, env))
|
|
26
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
27
|
+
.join(',');
|
|
28
|
+
}
|
|
29
|
+
exports.getItemSelector = getItemSelector;
|
|
30
|
+
function getResourceName(item, env, suffix) {
|
|
31
|
+
const tail = `-${getItemKey(item, env)}${suffix ?? ''}`;
|
|
10
32
|
const name = removeInvalidCharacters(item.name)
|
|
11
33
|
.slice(0, Math.max(0, MAX_NAME - tail.length))
|
|
12
34
|
.replace(/-+$/, '');
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.restoreKubernetesData = void 0;
|
|
3
|
+
exports.restoreKubernetesData = exports.getRestoreTarget = void 0;
|
|
4
4
|
const event_cache_1 = require("../executer/event-cache");
|
|
5
5
|
const log_1 = require("../log");
|
|
6
|
-
const path_1 = require("path");
|
|
7
6
|
const volumes_1 = require("./volumes");
|
|
8
7
|
const ensure_persistent_data_1 = require("./ensure-persistent-data");
|
|
8
|
+
const claim_layout_1 = require("./claim-layout");
|
|
9
|
+
// Where the upload pod (which mounts the work's claim at /dev/hammerkit/<id>) unpacks the
|
|
10
|
+
// archive of an output: the output's place in the claim, which its mount reads
|
|
11
|
+
function getRestoreTarget(work, path) {
|
|
12
|
+
return `/dev/hammerkit/${work.id()}/${(0, claim_layout_1.getClaimSubPath)(work, path)}`;
|
|
13
|
+
}
|
|
14
|
+
exports.getRestoreTarget = getRestoreTarget;
|
|
9
15
|
async function restoreKubernetesData(work, kubernetes, instance, environment, path,
|
|
10
16
|
// the kept claim of the work holds what an earlier run left, which goes first
|
|
11
17
|
options = {}) {
|
|
@@ -18,7 +24,7 @@ options = {}) {
|
|
|
18
24
|
if (!(await environment.file.exists(generatedArchive.filename))) {
|
|
19
25
|
continue;
|
|
20
26
|
}
|
|
21
|
-
const targetPath =
|
|
27
|
+
const targetPath = getRestoreTarget(work, generatedArchive.path);
|
|
22
28
|
// a claim made for this run (a server removes the claims of a run when it
|
|
23
29
|
// ends) does not have the directory of the output yet
|
|
24
30
|
let failure = null;
|
|
@@ -5,19 +5,18 @@ const work_secret_1 = require("../planner/work-secret");
|
|
|
5
5
|
const apply_1 = require("./apply");
|
|
6
6
|
const resources_1 = require("./resources");
|
|
7
7
|
const version_1 = require("../version");
|
|
8
|
-
const job_placement_1 = require("./job-placement");
|
|
9
8
|
const SECRET_VOLUME_NAME = 'hammerkit-secrets';
|
|
10
|
-
function getSecretResourceName(item) {
|
|
11
|
-
return (0, resources_1.getResourceName)(item, '-secrets');
|
|
9
|
+
function getSecretResourceName(item, kubernetes) {
|
|
10
|
+
return (0, resources_1.getResourceName)(item, kubernetes, '-secrets');
|
|
12
11
|
}
|
|
13
12
|
exports.getSecretResourceName = getSecretResourceName;
|
|
14
13
|
// The values go into a Secret of the item; the pod only references it, so no
|
|
15
14
|
// value is part of a Job or Deployment spec.
|
|
16
|
-
function getKubernetesSecretRefs(item, secrets) {
|
|
15
|
+
function getKubernetesSecretRefs(item, kubernetes, secrets) {
|
|
17
16
|
if (secrets.length === 0) {
|
|
18
17
|
return { env: [], mounts: [], volumes: [] };
|
|
19
18
|
}
|
|
20
|
-
const secretName = getSecretResourceName(item);
|
|
19
|
+
const secretName = getSecretResourceName(item, kubernetes);
|
|
21
20
|
const refs = {
|
|
22
21
|
env: [],
|
|
23
22
|
mounts: [],
|
|
@@ -50,14 +49,11 @@ async function ensureKubernetesSecret(instance, kubernetes, item, secrets, envir
|
|
|
50
49
|
apiVersion: 'v1',
|
|
51
50
|
metadata: {
|
|
52
51
|
namespace: kubernetes.namespace,
|
|
53
|
-
name: getSecretResourceName(item),
|
|
52
|
+
name: getSecretResourceName(item, kubernetes),
|
|
54
53
|
annotations: {
|
|
55
54
|
'hammerkit.dev/version': (0, version_1.getVersion)(),
|
|
56
55
|
},
|
|
57
|
-
labels:
|
|
58
|
-
'hammerkit.dev/id': item.id(),
|
|
59
|
-
...(0, job_placement_1.getRunLabels)(kubernetes),
|
|
60
|
-
},
|
|
56
|
+
labels: (0, resources_1.getItemLabels)(item, kubernetes),
|
|
61
57
|
},
|
|
62
58
|
type: 'Opaque',
|
|
63
59
|
stringData,
|
|
@@ -68,7 +64,7 @@ exports.ensureKubernetesSecret = ensureKubernetesSecret;
|
|
|
68
64
|
async function removeKubernetesSecret(instance, kubernetes, item) {
|
|
69
65
|
try {
|
|
70
66
|
await instance.coreApi.deleteNamespacedSecret({
|
|
71
|
-
name: getSecretResourceName(item),
|
|
67
|
+
name: getSecretResourceName(item, kubernetes),
|
|
72
68
|
namespace: kubernetes.namespace,
|
|
73
69
|
});
|
|
74
70
|
}
|
|
@@ -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/log.js
CHANGED
|
@@ -216,7 +216,7 @@ function writeWorkItemLogToConsole(env, log, maxNodeNameLength) {
|
|
|
216
216
|
exports.writeWorkItemLogToConsole = writeWorkItemLogToConsole;
|
|
217
217
|
function formatDate(date) {
|
|
218
218
|
return colors_1.default.grey(`[${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date
|
|
219
|
-
.
|
|
219
|
+
.getDate()
|
|
220
220
|
.toString()
|
|
221
221
|
.padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date
|
|
222
222
|
.getMinutes()
|