hammerkit 1.25.0 → 1.27.0-next.1
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 +53 -1
- package/dist/cache/backends/local-cache-backend.js +10 -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/container-task-state.js +8 -6
- package/dist/executer/docker-service.js +24 -13
- package/dist/executer/docker-task.js +18 -29
- package/dist/executer/event-cache.js +4 -0
- 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 +373 -0
- package/dist/index.js +33 -36
- package/dist/kubernetes/apply.js +31 -1
- package/dist/kubernetes/await-running-state.js +33 -1
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +44 -22
- package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +131 -4
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +25 -4
- package/dist/kubernetes/ensure-persistent-data.js +48 -7
- package/dist/kubernetes/get-service-ip.js +15 -1
- package/dist/kubernetes/job-placement.js +99 -0
- package/dist/kubernetes/job-secrets.js +306 -0
- package/dist/kubernetes/kept-claims.js +191 -0
- package/dist/kubernetes/kubernetes-instance.js +10 -2
- package/dist/kubernetes/network-gate.js +71 -0
- package/dist/kubernetes/network-policy.js +526 -0
- package/dist/kubernetes/resources.js +13 -2
- package/dist/kubernetes/restore-kubernetes-data.js +19 -4
- package/dist/kubernetes/secrets.js +2 -0
- package/dist/kubernetes/store-kubernetes-data.js +1 -1
- package/dist/kubernetes/volumes.js +100 -11
- package/dist/main.js +41 -0
- package/dist/network/egress-proxy.js +368 -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 +3 -2
- package/dist/planner/service-definition.js +8 -2
- package/dist/planner/utils/append-work-environment.js +2 -0
- package/dist/planner/utils/append-work-service.js +4 -0
- package/dist/planner/utils/append-work-task.js +17 -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 +155 -0
- package/dist/planner/work-cache-id.js +15 -1
- package/dist/planner/work-network.js +193 -0
- package/dist/planner/work-runtime-docker.js +43 -0
- package/dist/planner/work-runtime-kubernetes.js +336 -102
- package/dist/planner/work-runtime-local.js +16 -8
- package/dist/planner/work-service.js +7 -3
- package/dist/planner/work-state-record.js +30 -0
- package/dist/program.js +80 -7
- package/dist/remote/annotations.js +103 -0
- package/dist/remote/bundle-text.js +24 -0
- package/dist/remote/bundle.js +9 -1
- package/dist/remote/remote-commands.js +4 -1
- package/dist/remote/remote-work-commands.js +182 -69
- 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 +3 -0
- package/dist/schema/build-file-container-service-schema.js +6 -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 +1 -0
- package/dist/schema/schema-parser.js +16 -3
- package/dist/schema/work-scope-parser.js +13 -0
- package/dist/secrets/command-secret-provider.js +73 -10
- package/dist/secrets/github-app-token-command.js +23 -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 +554 -0
- package/dist/server/kubernetes-sandbox-storage.js +996 -0
- package/dist/server/oauth.js +1 -0
- package/dist/server/plan.js +51 -1
- 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 +51 -2
- package/dist/server/run-request.js +23 -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 +184 -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 +210 -0
- package/dist/server/sandboxes.js +519 -0
- package/dist/server/server-config.js +412 -22
- package/dist/server/server.js +204 -39
- package/dist/utils/run-id.js +12 -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,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getNetworkGateContainer = exports.HELPER_SECURITY_CONTEXT = exports.NETWORK_GATE_SCRIPT = exports.NETWORK_GATE_CONTAINER = exports.NETWORK_GATE_TIMEOUT_MS = void 0;
|
|
4
|
+
const egress_proxy_1 = require("../network/egress-proxy");
|
|
5
|
+
// Some network plugins (kind's kindnet among them) enforce a NetworkPolicy a
|
|
6
|
+
// moment after a pod started, so a limited pod's first connections could
|
|
7
|
+
// still get out. The gate is the pod's first init container: it waits until
|
|
8
|
+
// the pod no longer reaches the cluster DNS, which the policy of every limited
|
|
9
|
+
// item denies, so the item's own containers start only once the policy holds.
|
|
10
|
+
// A policy that never takes hold fails the pod instead of running it open.
|
|
11
|
+
// A script for `node -e` in the proxy's image, like the proxy.
|
|
12
|
+
exports.NETWORK_GATE_TIMEOUT_MS = 60000;
|
|
13
|
+
// the name of the gate's init container
|
|
14
|
+
exports.NETWORK_GATE_CONTAINER = 'hammerkit-network';
|
|
15
|
+
// Kept free of anything but Node's own modules.
|
|
16
|
+
exports.NETWORK_GATE_SCRIPT = `
|
|
17
|
+
const fs = require('fs')
|
|
18
|
+
const net = require('net')
|
|
19
|
+
const resolvConf = process.env.HAMMERKIT_GATE_RESOLV_CONF || '/etc/resolv.conf'
|
|
20
|
+
const port = parseInt(process.env.HAMMERKIT_GATE_PORT || '53', 10)
|
|
21
|
+
const deadline = Date.now() + parseInt(process.env.HAMMERKIT_GATE_TIMEOUT_MS || '${exports.NETWORK_GATE_TIMEOUT_MS}', 10)
|
|
22
|
+
function fail(message) {
|
|
23
|
+
process.stderr.write('hammerkit: ' + message + '\\n')
|
|
24
|
+
try {
|
|
25
|
+
fs.writeFileSync('/dev/termination-log', 'hammerkit: ' + message)
|
|
26
|
+
} catch (e) {}
|
|
27
|
+
process.exit(1)
|
|
28
|
+
}
|
|
29
|
+
let conf = ''
|
|
30
|
+
try {
|
|
31
|
+
conf = fs.readFileSync(resolvConf, 'utf8')
|
|
32
|
+
} catch (e) {}
|
|
33
|
+
const match = /^nameserver\\s+(\\S+)/m.exec(conf)
|
|
34
|
+
if (!match) {
|
|
35
|
+
fail('the pod has no cluster DNS, so its network policy can not be checked')
|
|
36
|
+
}
|
|
37
|
+
const nameserver = match[1]
|
|
38
|
+
function attempt() {
|
|
39
|
+
const socket = net.connect({ host: nameserver, port, timeout: 2000 })
|
|
40
|
+
socket.once('connect', () => {
|
|
41
|
+
socket.destroy()
|
|
42
|
+
if (Date.now() > deadline) {
|
|
43
|
+
fail('the network policy of this pod is not enforced, it still reaches ' + nameserver)
|
|
44
|
+
}
|
|
45
|
+
setTimeout(attempt, 500)
|
|
46
|
+
})
|
|
47
|
+
// dropped or refused: the policy holds
|
|
48
|
+
socket.once('timeout', () => process.exit(0))
|
|
49
|
+
socket.once('error', () => process.exit(0))
|
|
50
|
+
}
|
|
51
|
+
attempt()
|
|
52
|
+
`;
|
|
53
|
+
exports.HELPER_SECURITY_CONTEXT = {
|
|
54
|
+
runAsNonRoot: true,
|
|
55
|
+
// the image's `node` user
|
|
56
|
+
runAsUser: 1000,
|
|
57
|
+
allowPrivilegeEscalation: false,
|
|
58
|
+
readOnlyRootFilesystem: true,
|
|
59
|
+
capabilities: { drop: ['ALL'] },
|
|
60
|
+
};
|
|
61
|
+
function getNetworkGateContainer() {
|
|
62
|
+
return {
|
|
63
|
+
name: exports.NETWORK_GATE_CONTAINER,
|
|
64
|
+
image: egress_proxy_1.EGRESS_PROXY_IMAGE,
|
|
65
|
+
command: ['node', '-e', exports.NETWORK_GATE_SCRIPT],
|
|
66
|
+
securityContext: exports.HELPER_SECURITY_CONTEXT,
|
|
67
|
+
resources: { limits: { memory: '64Mi' } },
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
exports.getNetworkGateContainer = getNetworkGateContainer;
|
|
71
|
+
//# sourceMappingURL=network-gate.js.map
|
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.removeSandboxProxies = exports.getProxyReadinessScript = exports.removeAllKubernetesItemNetworks = exports.releaseKubernetesItemNetwork = exports.awaitPodsEnded = exports.removeKubernetesItemNetwork = exports.ensureKubernetesItemNetwork = exports.buildProxyPolicy = exports.buildNetworkPolicy = exports.getNetworkPolicyName = exports.getSandboxProxy = exports.getProxyName = exports.getNetworkLabels = exports.NETWORK_PROXY = exports.NETWORK_LIMITED = exports.NETWORK_LABEL = exports.RUN_LABEL = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const stream_1 = require("stream");
|
|
6
|
+
const readline_1 = require("readline");
|
|
7
|
+
const work_item_1 = require("../planner/work-item");
|
|
8
|
+
const work_service_1 = require("../planner/work-service");
|
|
9
|
+
const work_network_1 = require("../planner/work-network");
|
|
10
|
+
const apply_1 = require("./apply");
|
|
11
|
+
const get_service_ip_1 = require("./get-service-ip");
|
|
12
|
+
const version_1 = require("../version");
|
|
13
|
+
const await_running_state_1 = require("./await-running-state");
|
|
14
|
+
const egress_proxy_1 = require("../network/egress-proxy");
|
|
15
|
+
const log_1 = require("../log");
|
|
16
|
+
const sleep_1 = require("../utils/sleep");
|
|
17
|
+
const network_gate_1 = require("./network-gate");
|
|
18
|
+
const job_placement_1 = require("./job-placement");
|
|
19
|
+
Object.defineProperty(exports, "RUN_LABEL", { enumerable: true, get: function () { return job_placement_1.RUN_LABEL; } });
|
|
20
|
+
const ID_LABEL = 'hammerkit.dev/id';
|
|
21
|
+
const PROXY_LABEL = 'hammerkit.dev/proxy-for';
|
|
22
|
+
const NETWORK_OF_LABEL = 'hammerkit.dev/network-of';
|
|
23
|
+
// on the proxy of a task on a sandbox: the sandbox, whose pod of files it stays with
|
|
24
|
+
const PROXY_OF_SANDBOX_LABEL = 'hammerkit.dev/proxy-of-sandbox';
|
|
25
|
+
const PROXY_READY_TIMEOUT_MS = 60000;
|
|
26
|
+
// how long the pods of an item may take to end before their policy goes
|
|
27
|
+
const POD_END_TIMEOUT_MS = 120000;
|
|
28
|
+
const POD_END_POLL_MS = 500;
|
|
29
|
+
// on the pods of a limited item on a server: its own policy governs their egress, the
|
|
30
|
+
// one of the caller's namespace (see getNamespacePolicies) leaves them out
|
|
31
|
+
// (`limited`), and on its egress proxy (`proxy`), whose ingress only its own policy lets in
|
|
32
|
+
exports.NETWORK_LABEL = 'hammerkit.dev/network';
|
|
33
|
+
exports.NETWORK_LIMITED = 'limited';
|
|
34
|
+
exports.NETWORK_PROXY = 'proxy';
|
|
35
|
+
function getNetworkLabels(env, item) {
|
|
36
|
+
return env.server && (0, work_network_1.getEffectiveNetwork)(item.data).type !== 'open' ? { [exports.NETWORK_LABEL]: exports.NETWORK_LIMITED } : {};
|
|
37
|
+
}
|
|
38
|
+
exports.getNetworkLabels = getNetworkLabels;
|
|
39
|
+
// A key short enough for a Service name (63 characters) with its prefix. A server's run
|
|
40
|
+
// label is longer than a run id of the CLI, and its first 8 digits tell runs apart.
|
|
41
|
+
function getNetworkKey(item, scope) {
|
|
42
|
+
return scope.run ? `${item.id().substring(0, 32)}-${scope.run.substring(0, 8)}` : item.id();
|
|
43
|
+
}
|
|
44
|
+
function getProxyName(item, scope) {
|
|
45
|
+
return `hammerkit-proxy-${getNetworkKey(item, scope)}`;
|
|
46
|
+
}
|
|
47
|
+
exports.getProxyName = getProxyName;
|
|
48
|
+
function getRunProxy(item, scope) {
|
|
49
|
+
return { key: getNetworkKey(item, scope), name: getProxyName(item, scope) };
|
|
50
|
+
}
|
|
51
|
+
// A proxy that outlives its run is told apart by all it is made of: a run whose task,
|
|
52
|
+
// network, needed services, server or pod of files changed gets one of its own, and the
|
|
53
|
+
// one before goes (removeSandboxProxies).
|
|
54
|
+
function getSandboxProxy(env, item, network, hostAliases, sandbox) {
|
|
55
|
+
const key = (0, crypto_1.createHash)('sha256')
|
|
56
|
+
.update(JSON.stringify({
|
|
57
|
+
sandbox: sandbox.label,
|
|
58
|
+
pod: sandbox.pod?.uid ?? null,
|
|
59
|
+
item: item.id(),
|
|
60
|
+
network,
|
|
61
|
+
env: (0, egress_proxy_1.getItemProxyEnvironment)(item),
|
|
62
|
+
hostAliases,
|
|
63
|
+
placement: env.server ? (0, job_placement_1.getPodPlacement)(env.server, undefined) : null,
|
|
64
|
+
image: egress_proxy_1.EGRESS_PROXY_IMAGE,
|
|
65
|
+
version: (0, version_1.getVersion)(),
|
|
66
|
+
}))
|
|
67
|
+
.digest('hex')
|
|
68
|
+
.slice(0, 20);
|
|
69
|
+
return { key, name: `hammerkit-proxy-${key}`, sandbox };
|
|
70
|
+
}
|
|
71
|
+
exports.getSandboxProxy = getSandboxProxy;
|
|
72
|
+
// What the proxy's objects are labeled with: removed with the run, or, on a sandbox,
|
|
73
|
+
// with the pod of its files.
|
|
74
|
+
function getProxyLabels(env, item, proxy) {
|
|
75
|
+
return {
|
|
76
|
+
[NETWORK_OF_LABEL]: item.id(),
|
|
77
|
+
...(proxy.sandbox ? { [PROXY_OF_SANDBOX_LABEL]: proxy.sandbox.label } : (0, job_placement_1.getRunLabels)(env)),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function getNetworkPolicyName(item, scope) {
|
|
81
|
+
return `hammerkit-network-${getNetworkKey(item, scope)}`;
|
|
82
|
+
}
|
|
83
|
+
exports.getNetworkPolicyName = getNetworkPolicyName;
|
|
84
|
+
// the pods of the item, of the run when the scope has one
|
|
85
|
+
function getItemPodLabels(item, scope) {
|
|
86
|
+
return { [ID_LABEL]: item.id(), ...(scope.run ? { [job_placement_1.RUN_LABEL]: scope.run } : {}) };
|
|
87
|
+
}
|
|
88
|
+
function toSelector(labels) {
|
|
89
|
+
return Object.entries(labels)
|
|
90
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
91
|
+
.join(',');
|
|
92
|
+
}
|
|
93
|
+
// Who may connect to a limited item: the items that need it, and for a
|
|
94
|
+
// service a Gateway API route of the environment, the cluster's equivalent
|
|
95
|
+
// of a published port. A task takes no connections.
|
|
96
|
+
function getIngress(env, item) {
|
|
97
|
+
if (!(0, work_service_1.isContainerWorkService)(item.data)) {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
if (env.httpRoutes.some((route) => route.service === item.name)) {
|
|
101
|
+
return [{}];
|
|
102
|
+
}
|
|
103
|
+
const ids = [...new Set(item.requiredBy.map((requirer) => requirer.id()))];
|
|
104
|
+
if (ids.length === 0) {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
const from = { matchExpressions: [{ key: ID_LABEL, operator: 'In', values: ids }] };
|
|
108
|
+
// the client's name for `from`, a reserved word
|
|
109
|
+
return [{ _from: [{ podSelector: from }] }];
|
|
110
|
+
}
|
|
111
|
+
// The NetworkPolicy of a limited item: egress to the pods of the services it
|
|
112
|
+
// needs and, under `allow`, to its proxy; nothing else, DNS included (needs
|
|
113
|
+
// and the proxy are host aliases). Ingress only from what needs it. An open
|
|
114
|
+
// item has none.
|
|
115
|
+
function buildNetworkPolicy(env, item, network, scope, proxy = getRunProxy(item, scope)) {
|
|
116
|
+
const needs = item.needs.filter((need) => (0, work_item_1.isContainerWorkServiceItem)(need.service));
|
|
117
|
+
return {
|
|
118
|
+
apiVersion: 'networking.k8s.io/v1',
|
|
119
|
+
kind: 'NetworkPolicy',
|
|
120
|
+
metadata: {
|
|
121
|
+
namespace: env.namespace,
|
|
122
|
+
name: getNetworkPolicyName(item, scope),
|
|
123
|
+
annotations: { 'hammerkit.dev/version': (0, version_1.getVersion)() },
|
|
124
|
+
labels: { [NETWORK_OF_LABEL]: item.id(), ...(0, job_placement_1.getRunLabels)(env) },
|
|
125
|
+
},
|
|
126
|
+
spec: {
|
|
127
|
+
podSelector: { matchLabels: getItemPodLabels(item, scope) },
|
|
128
|
+
policyTypes: ['Ingress', 'Egress'],
|
|
129
|
+
ingress: getIngress(env, item),
|
|
130
|
+
egress: [
|
|
131
|
+
...(needs.length > 0
|
|
132
|
+
? [{ to: needs.map((need) => ({ podSelector: { matchLabels: { [ID_LABEL]: need.service.id() } } })) }]
|
|
133
|
+
: []),
|
|
134
|
+
...(network.type === 'allow'
|
|
135
|
+
? [
|
|
136
|
+
{
|
|
137
|
+
to: [{ podSelector: { matchLabels: { [PROXY_LABEL]: proxy.key } } }],
|
|
138
|
+
ports: (0, egress_proxy_1.getEgressProxyPorts)(network).map((port) => ({ protocol: 'TCP', port })),
|
|
139
|
+
},
|
|
140
|
+
]
|
|
141
|
+
: []),
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
exports.buildNetworkPolicy = buildNetworkPolicy;
|
|
147
|
+
// The proxy takes connections from its item's pods only, so no other pod
|
|
148
|
+
// uses its allow list or reaches the item's services through it; a proxy that
|
|
149
|
+
// outlives its run, from the pods of the current run only.
|
|
150
|
+
function buildProxyPolicy(env, item, network, scope, proxy = getRunProxy(item, scope)) {
|
|
151
|
+
return {
|
|
152
|
+
apiVersion: 'networking.k8s.io/v1',
|
|
153
|
+
kind: 'NetworkPolicy',
|
|
154
|
+
metadata: {
|
|
155
|
+
namespace: env.namespace,
|
|
156
|
+
name: proxy.name,
|
|
157
|
+
annotations: { 'hammerkit.dev/version': (0, version_1.getVersion)() },
|
|
158
|
+
labels: getProxyLabels(env, item, proxy),
|
|
159
|
+
...(proxy.owner ? { ownerReferences: [proxy.owner] } : {}),
|
|
160
|
+
},
|
|
161
|
+
spec: {
|
|
162
|
+
podSelector: { matchLabels: { [PROXY_LABEL]: proxy.key } },
|
|
163
|
+
policyTypes: ['Ingress'],
|
|
164
|
+
ingress: [
|
|
165
|
+
{
|
|
166
|
+
_from: [{ podSelector: { matchLabels: getItemPodLabels(item, scope) } }],
|
|
167
|
+
ports: (0, egress_proxy_1.getEgressProxyPorts)(network).map((port) => ({ protocol: 'TCP', port })),
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
exports.buildProxyPolicy = buildProxyPolicy;
|
|
174
|
+
// What the item's pods are created with, after its policy (and proxy) are in
|
|
175
|
+
// place, so a pod never runs unlimited. A limited item needs an environment
|
|
176
|
+
// that states its cluster enforces NetworkPolicy: hammerkit can't tell from
|
|
177
|
+
// the API, and a policy nobody enforces would run the item open.
|
|
178
|
+
// An open service gets what it had before: whatever an earlier limited run
|
|
179
|
+
// left goes.
|
|
180
|
+
// A task follows the proxy's log while its jobs run. A service outlives the
|
|
181
|
+
// run on a cluster, so its refused hosts are in the proxy's own log.
|
|
182
|
+
async function ensureKubernetesItemNetwork(instance, env, item, options) {
|
|
183
|
+
const network = (0, work_network_1.getEffectiveNetwork)(item.data);
|
|
184
|
+
if (network.type !== 'open' && !env.networkPolicyEnforced) {
|
|
185
|
+
throw new Error(`${item.name}: network ${network.type} needs a cluster that enforces NetworkPolicy, ` +
|
|
186
|
+
'state it with networkPolicy: enforced on the kubernetes environment');
|
|
187
|
+
}
|
|
188
|
+
const hostAliases = await getNeedHostAliases(instance, env, item);
|
|
189
|
+
if (network.type === 'open') {
|
|
190
|
+
if (!options.run) {
|
|
191
|
+
// without the rights to manage NetworkPolicies, hammerkit never made one
|
|
192
|
+
await removeKubernetesItemNetwork(instance, env, item, { run: null }, { ignoreForbidden: true });
|
|
193
|
+
}
|
|
194
|
+
return { env: [], hostAliases, initContainers: [], close() { } };
|
|
195
|
+
}
|
|
196
|
+
const proxy = network.type === 'allow' && options.sandbox
|
|
197
|
+
? getSandboxProxy(env, item, network, hostAliases, options.sandbox)
|
|
198
|
+
: getRunProxy(item, options);
|
|
199
|
+
await (0, apply_1.apply)(instance, buildNetworkPolicy(env, item, network, options, proxy));
|
|
200
|
+
const initContainers = [(0, network_gate_1.getNetworkGateContainer)()];
|
|
201
|
+
if (network.type === 'none') {
|
|
202
|
+
if (options.sandbox) {
|
|
203
|
+
await removeSandboxProxies(instance, env.namespace, { sandbox: options.sandbox.label, item });
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
await removeProxy(instance, env.namespace, proxy.name, [404]);
|
|
207
|
+
}
|
|
208
|
+
return { env: [], hostAliases, initContainers, close() { } };
|
|
209
|
+
}
|
|
210
|
+
let started;
|
|
211
|
+
try {
|
|
212
|
+
if (proxy.sandbox) {
|
|
213
|
+
// a proxy of the sandbox from before, made of something else, goes
|
|
214
|
+
await removeSandboxProxies(instance, env.namespace, { sandbox: proxy.sandbox.label, item }, (deployment) => deployment.metadata?.labels?.[PROXY_LABEL] === proxy.key);
|
|
215
|
+
proxy.owner = await getFilesPodOwner(instance, env, proxy.sandbox);
|
|
216
|
+
}
|
|
217
|
+
started = await ensureProxy(instance, env, item, network, hostAliases, options, proxy);
|
|
218
|
+
}
|
|
219
|
+
catch (e) {
|
|
220
|
+
await removeKubernetesItemNetwork(instance, env, item, options).catch(() => undefined);
|
|
221
|
+
throw e;
|
|
222
|
+
}
|
|
223
|
+
const follow = options.followLog
|
|
224
|
+
? await followProxyLog(instance, env, item, proxy, { tailLines: started.reused ? 0 : undefined })
|
|
225
|
+
: null;
|
|
226
|
+
return {
|
|
227
|
+
env: Object.entries((0, egress_proxy_1.getProxyClientEnvironment)(item.needs.map((need) => need.name))).map(([name, value]) => ({
|
|
228
|
+
name,
|
|
229
|
+
value,
|
|
230
|
+
})),
|
|
231
|
+
// the item reaches each TCP forward under its host's name, at the proxy
|
|
232
|
+
hostAliases: [
|
|
233
|
+
...hostAliases,
|
|
234
|
+
{ ip: started.ip, hostnames: [egress_proxy_1.EGRESS_PROXY_HOST, ...(0, work_network_1.getTcpForwards)(network).map((forward) => forward.host)] },
|
|
235
|
+
],
|
|
236
|
+
initContainers,
|
|
237
|
+
close: () => follow?.abort(),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
exports.ensureKubernetesItemNetwork = ensureKubernetesItemNetwork;
|
|
241
|
+
// The policy and proxy of the scope go only once no pod of it runs anymore:
|
|
242
|
+
// a pod that is still terminating (cancel, timeout, `down`) would otherwise
|
|
243
|
+
// run without limits. When pods outlast the wait, the policy stays.
|
|
244
|
+
// The proxy of a task on a sandbox stays while the pod of the sandbox's files
|
|
245
|
+
// runs, which owns it; one it does not own goes with the run.
|
|
246
|
+
async function removeKubernetesItemNetwork(instance, env, item, scope, options = { ignoreForbidden: false }) {
|
|
247
|
+
const ignored = options.ignoreForbidden ? [403, 404] : [404];
|
|
248
|
+
await ignoreStatus(instance.objectApi.delete({
|
|
249
|
+
apiVersion: 'networking.k8s.io/v1',
|
|
250
|
+
kind: 'NetworkPolicy',
|
|
251
|
+
metadata: { name: getNetworkPolicyName(item, scope), namespace: env.namespace },
|
|
252
|
+
}), ignored);
|
|
253
|
+
if (scope.sandbox) {
|
|
254
|
+
const owner = await getFilesPodOwner(instance, env, scope.sandbox);
|
|
255
|
+
await removeSandboxProxies(instance, env.namespace, { sandbox: scope.sandbox.label, item }, (deployment) => (deployment.metadata?.ownerReferences ?? []).some((reference) => !!owner && reference.uid === owner.uid));
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
await removeProxy(instance, env.namespace, getProxyName(item, scope), ignored);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
exports.removeKubernetesItemNetwork = removeKubernetesItemNetwork;
|
|
262
|
+
// Waits until no pod with the labels runs or waits to run.
|
|
263
|
+
async function awaitPodsEnded(instance, env, labels, timeout = POD_END_TIMEOUT_MS) {
|
|
264
|
+
const deadline = Date.now() + timeout;
|
|
265
|
+
for (;;) {
|
|
266
|
+
const pods = await instance.coreApi.listNamespacedPod({
|
|
267
|
+
namespace: env.namespace,
|
|
268
|
+
labelSelector: toSelector(labels),
|
|
269
|
+
});
|
|
270
|
+
if (pods.items.every((pod) => pod.status?.phase === 'Succeeded' || pod.status?.phase === 'Failed')) {
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
if (Date.now() >= deadline) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
await (0, sleep_1.sleep)(POD_END_POLL_MS);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
exports.awaitPodsEnded = awaitPodsEnded;
|
|
280
|
+
// Removes the scope's policy and proxy once its pods ended.
|
|
281
|
+
async function releaseKubernetesItemNetwork(instance, env, item, scope) {
|
|
282
|
+
if (!(await awaitPodsEnded(instance, env, getItemPodLabels(item, scope)))) {
|
|
283
|
+
throw new Error(`pods of ${item.name} are still running, so its network policy stays`);
|
|
284
|
+
}
|
|
285
|
+
await removeKubernetesItemNetwork(instance, env, item, scope);
|
|
286
|
+
}
|
|
287
|
+
exports.releaseKubernetesItemNetwork = releaseKubernetesItemNetwork;
|
|
288
|
+
// Every policy and proxy of the item, of any run, once none of its pods runs:
|
|
289
|
+
// what `down` and `clean` remove.
|
|
290
|
+
async function removeAllKubernetesItemNetworks(instance, env, item) {
|
|
291
|
+
if (!(await awaitPodsEnded(instance, env, { [ID_LABEL]: item.id() }))) {
|
|
292
|
+
throw new Error(`pods of ${item.name} are still running, so its network policy stays`);
|
|
293
|
+
}
|
|
294
|
+
const labelSelector = `${NETWORK_OF_LABEL}=${item.id()}`;
|
|
295
|
+
let policies;
|
|
296
|
+
try {
|
|
297
|
+
const list = await instance.objectApi.list('networking.k8s.io/v1', 'NetworkPolicy', env.namespace, undefined, undefined, undefined, undefined, labelSelector);
|
|
298
|
+
policies = list.items;
|
|
299
|
+
}
|
|
300
|
+
catch (e) {
|
|
301
|
+
// without the rights to manage NetworkPolicies, hammerkit never made one
|
|
302
|
+
if ((0, apply_1.statusCodeOf)(e) === 403) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
throw e;
|
|
306
|
+
}
|
|
307
|
+
// the items of a list carry no kind of their own
|
|
308
|
+
for (const name of policies.map((policy) => policy.metadata?.name).filter((name) => !!name)) {
|
|
309
|
+
await ignoreStatus(instance.objectApi.delete({
|
|
310
|
+
apiVersion: 'networking.k8s.io/v1',
|
|
311
|
+
kind: 'NetworkPolicy',
|
|
312
|
+
metadata: { name, namespace: env.namespace },
|
|
313
|
+
}), [404]);
|
|
314
|
+
}
|
|
315
|
+
const deployments = await instance.appsApi.listNamespacedDeployment({ namespace: env.namespace, labelSelector });
|
|
316
|
+
for (const deployment of deployments.items) {
|
|
317
|
+
const name = deployment.metadata?.name;
|
|
318
|
+
if (name) {
|
|
319
|
+
await ignoreStatus(instance.appsApi.deleteNamespacedDeployment({ name, namespace: env.namespace }), [404]);
|
|
320
|
+
await ignoreStatus(instance.coreApi.deleteNamespacedService({ name, namespace: env.namespace }), [404]);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
exports.removeAllKubernetesItemNetworks = removeAllKubernetesItemNetworks;
|
|
325
|
+
// the services the item needs, by the names it lists them under
|
|
326
|
+
async function getNeedHostAliases(instance, env, item) {
|
|
327
|
+
const hostAliases = [];
|
|
328
|
+
for (const need of item.needs) {
|
|
329
|
+
if (!(0, work_item_1.isContainerWorkServiceItem)(need.service)) {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const ip = await (0, get_service_ip_1.getServiceIp)(instance, env, need.service);
|
|
333
|
+
if (!ip) {
|
|
334
|
+
throw new Error(`unable to get service ip for ${need.name}`);
|
|
335
|
+
}
|
|
336
|
+
hostAliases.push({ ip, hostnames: [need.name] });
|
|
337
|
+
}
|
|
338
|
+
return hostAliases;
|
|
339
|
+
}
|
|
340
|
+
// The proxy as a Deployment and a Service; its pods reach out without an
|
|
341
|
+
// egress policy of their own, and reach the needed services by name for tools
|
|
342
|
+
// that ignore NO_PROXY. Unprivileged, from a pinned image. Its Service carries
|
|
343
|
+
// the port of each TCP forward too.
|
|
344
|
+
// Exits 0 once every port answers on the pod's loopback.
|
|
345
|
+
function getProxyReadinessScript(ports) {
|
|
346
|
+
return (`const net = require('net'); let open = ${ports.length}; ` +
|
|
347
|
+
`for (const port of ${JSON.stringify(ports)}) net.connect(port, '127.0.0.1')` +
|
|
348
|
+
`.on('connect', function () { this.destroy(); if (--open === 0) process.exit(0) })` +
|
|
349
|
+
`.on('error', () => process.exit(1))`);
|
|
350
|
+
}
|
|
351
|
+
exports.getProxyReadinessScript = getProxyReadinessScript;
|
|
352
|
+
async function ensureProxy(instance, env, item, network, hostAliases, scope, proxy) {
|
|
353
|
+
const name = proxy.name;
|
|
354
|
+
const labels = { [PROXY_LABEL]: proxy.key };
|
|
355
|
+
const metadata = {
|
|
356
|
+
namespace: env.namespace,
|
|
357
|
+
name,
|
|
358
|
+
annotations: { 'hammerkit.dev/version': (0, version_1.getVersion)() },
|
|
359
|
+
labels: { ...labels, ...getProxyLabels(env, item, proxy) },
|
|
360
|
+
...(proxy.owner ? { ownerReferences: [proxy.owner] } : {}),
|
|
361
|
+
};
|
|
362
|
+
// on a server the proxy runs on the nodes of the first pool
|
|
363
|
+
const placement = env.server ? (0, job_placement_1.getPodPlacement)(env.server, undefined) : null;
|
|
364
|
+
const deployment = {
|
|
365
|
+
apiVersion: 'apps/v1',
|
|
366
|
+
kind: 'Deployment',
|
|
367
|
+
metadata,
|
|
368
|
+
spec: {
|
|
369
|
+
replicas: 1,
|
|
370
|
+
selector: { matchLabels: labels },
|
|
371
|
+
template: {
|
|
372
|
+
metadata: {
|
|
373
|
+
labels: {
|
|
374
|
+
...labels,
|
|
375
|
+
...(proxy.sandbox ? {} : (0, job_placement_1.getRunLabels)(env)),
|
|
376
|
+
...(env.server ? { [exports.NETWORK_LABEL]: exports.NETWORK_PROXY } : {}),
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
spec: {
|
|
380
|
+
hostAliases,
|
|
381
|
+
automountServiceAccountToken: false,
|
|
382
|
+
...(placement ? { nodeSelector: placement.nodeSelector, tolerations: placement.tolerations } : {}),
|
|
383
|
+
// unprivileged, it still listens on the port of a TCP forward below
|
|
384
|
+
// 1024; a sysctl every Pod Security level allows
|
|
385
|
+
securityContext: { sysctls: [{ name: 'net.ipv4.ip_unprivileged_port_start', value: '0' }] },
|
|
386
|
+
containers: [
|
|
387
|
+
{
|
|
388
|
+
name: 'proxy',
|
|
389
|
+
image: egress_proxy_1.EGRESS_PROXY_IMAGE,
|
|
390
|
+
command: (0, egress_proxy_1.getEgressProxyCommand)(),
|
|
391
|
+
env: Object.entries((0, egress_proxy_1.getItemProxyEnvironment)(item)).map(([key, value]) => ({ name: key, value })),
|
|
392
|
+
ports: (0, egress_proxy_1.getEgressProxyPorts)(network).map((port) => ({ containerPort: port })),
|
|
393
|
+
// in the pod, since its ingress policy may keep the kubelet out;
|
|
394
|
+
// ready once it listens on every port, its TCP forwards' too
|
|
395
|
+
readinessProbe: {
|
|
396
|
+
exec: {
|
|
397
|
+
command: ['node', '-e', getProxyReadinessScript((0, egress_proxy_1.getEgressProxyPorts)(network))],
|
|
398
|
+
},
|
|
399
|
+
periodSeconds: 2,
|
|
400
|
+
},
|
|
401
|
+
securityContext: network_gate_1.HELPER_SECURITY_CONTEXT,
|
|
402
|
+
resources: { limits: { memory: '256Mi' } },
|
|
403
|
+
},
|
|
404
|
+
],
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
const service = {
|
|
410
|
+
apiVersion: 'v1',
|
|
411
|
+
kind: 'Service',
|
|
412
|
+
metadata,
|
|
413
|
+
spec: {
|
|
414
|
+
type: 'ClusterIP',
|
|
415
|
+
selector: labels,
|
|
416
|
+
ports: (0, egress_proxy_1.getEgressProxyPorts)(network).map((port) => ({ name: `tcp-${port}`, port, targetPort: port })),
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
// on a sandbox, the proxy of an earlier run may still be there: the item's policy
|
|
420
|
+
// of this run lets its pods reach it, its own policy lets them in
|
|
421
|
+
const reused = proxy.sandbox
|
|
422
|
+
? await instance.objectApi.read(deployment).then((current) => !current.metadata?.deletionTimestamp, (e) => ((0, apply_1.statusCodeOf)(e) === 404 ? false : Promise.reject(e)))
|
|
423
|
+
: false;
|
|
424
|
+
await (0, apply_1.apply)(instance, buildProxyPolicy(env, item, network, scope, proxy));
|
|
425
|
+
await (0, apply_1.apply)(instance, deployment);
|
|
426
|
+
const created = await (0, apply_1.apply)(instance, service);
|
|
427
|
+
await (0, await_running_state_1.awaitDeployRunningState)(instance, env, name, {
|
|
428
|
+
podSelector: toSelector(labels),
|
|
429
|
+
timeout: PROXY_READY_TIMEOUT_MS,
|
|
430
|
+
timeoutMessage: `the egress proxy of ${item.name} did not start`,
|
|
431
|
+
});
|
|
432
|
+
const ip = created.spec?.clusterIP;
|
|
433
|
+
if (!ip) {
|
|
434
|
+
throw new Error(`the egress proxy of ${item.name} has no cluster ip`);
|
|
435
|
+
}
|
|
436
|
+
item.status.write('debug', `${reused ? 'egress proxy of the sandbox' : 'egress proxy'} allows ${network.hosts.join(', ') || 'no host'}`);
|
|
437
|
+
return { ip, reused };
|
|
438
|
+
}
|
|
439
|
+
// Refused hosts show up in the item's log, as on Docker; of a proxy from an
|
|
440
|
+
// earlier run (`tailLines: 0`), only what this run asks of it.
|
|
441
|
+
async function followProxyLog(instance, env, item, proxy, options) {
|
|
442
|
+
try {
|
|
443
|
+
const pods = await instance.coreApi.listNamespacedPod({
|
|
444
|
+
namespace: env.namespace,
|
|
445
|
+
labelSelector: `${PROXY_LABEL}=${proxy.key}`,
|
|
446
|
+
});
|
|
447
|
+
const pod = pods.items.find((p) => p.status?.phase === 'Running' && !p.metadata?.deletionTimestamp);
|
|
448
|
+
if (!pod?.metadata?.name) {
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
const output = new stream_1.PassThrough();
|
|
452
|
+
// close() aborts the request, which errors the response stream the
|
|
453
|
+
// client pipes in here; nothing listens to it there
|
|
454
|
+
output.on('pipe', (source) => source.on('error', () => undefined));
|
|
455
|
+
(0, readline_1.createInterface)({ input: output }).on('line', (line) => {
|
|
456
|
+
const event = (0, egress_proxy_1.parseEgressProxyEvent)(line);
|
|
457
|
+
if (event && event.type !== 'ready') {
|
|
458
|
+
item.status.write('warn', (0, egress_proxy_1.formatEgressProxyEvent)(event));
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
return await instance.log.log(env.namespace, pod.metadata.name, 'proxy', output, {
|
|
462
|
+
follow: true,
|
|
463
|
+
...(options.tailLines !== undefined ? { tailLines: options.tailLines } : {}),
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
catch (e) {
|
|
467
|
+
item.status.write('debug', `unable to follow the egress proxy log: ${(0, log_1.getErrorMessage)(e)}`);
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
async function removeProxy(instance, namespace, name, ignored) {
|
|
472
|
+
await ignoreStatus(instance.appsApi.deleteNamespacedDeployment({ name, namespace }), ignored);
|
|
473
|
+
await ignoreStatus(instance.coreApi.deleteNamespacedService({ name, namespace }), ignored);
|
|
474
|
+
await ignoreStatus(instance.objectApi.delete({
|
|
475
|
+
apiVersion: 'networking.k8s.io/v1',
|
|
476
|
+
kind: 'NetworkPolicy',
|
|
477
|
+
metadata: { name, namespace },
|
|
478
|
+
}), ignored);
|
|
479
|
+
}
|
|
480
|
+
// The proxies on the sandbox, of the item when one is given, but those `keep` takes.
|
|
481
|
+
// The cluster removes those the pod of the sandbox's files owns when the pod goes; the
|
|
482
|
+
// server removes all when it took the pod away while a run worked (see
|
|
483
|
+
// KubernetesSandboxStorage.makeWayForRun).
|
|
484
|
+
async function removeSandboxProxies(instance, namespace, of, keep = () => false) {
|
|
485
|
+
const deployments = await instance.appsApi.listNamespacedDeployment({
|
|
486
|
+
namespace,
|
|
487
|
+
labelSelector: [
|
|
488
|
+
`${PROXY_OF_SANDBOX_LABEL}=${of.sandbox}`,
|
|
489
|
+
...(of.item ? [`${NETWORK_OF_LABEL}=${of.item.id()}`] : []),
|
|
490
|
+
].join(','),
|
|
491
|
+
});
|
|
492
|
+
for (const deployment of deployments.items) {
|
|
493
|
+
const name = deployment.metadata?.name;
|
|
494
|
+
if (name && !keep(deployment)) {
|
|
495
|
+
await removeProxy(instance, namespace, name, [404]);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
exports.removeSandboxProxies = removeSandboxProxies;
|
|
500
|
+
// The pod of the sandbox's files the run holds, as the owner of the proxy: the cluster
|
|
501
|
+
// removes the proxy when that pod goes. None when the pod is no longer the one the run
|
|
502
|
+
// holds (it made way for a pod of the run): the proxy then goes with the run.
|
|
503
|
+
async function getFilesPodOwner(instance, env, sandbox) {
|
|
504
|
+
if (!sandbox.pod) {
|
|
505
|
+
return undefined;
|
|
506
|
+
}
|
|
507
|
+
const { name, uid } = sandbox.pod;
|
|
508
|
+
const current = await instance.coreApi
|
|
509
|
+
.readNamespacedPod({ name, namespace: env.namespace })
|
|
510
|
+
.catch((e) => ((0, apply_1.statusCodeOf)(e) === 404 ? undefined : Promise.reject(e)));
|
|
511
|
+
if (!current || current.metadata?.uid !== uid || current.metadata?.deletionTimestamp) {
|
|
512
|
+
return undefined;
|
|
513
|
+
}
|
|
514
|
+
return { apiVersion: 'v1', kind: 'Pod', name, uid };
|
|
515
|
+
}
|
|
516
|
+
async function ignoreStatus(promise, statusCodes) {
|
|
517
|
+
try {
|
|
518
|
+
await promise;
|
|
519
|
+
}
|
|
520
|
+
catch (e) {
|
|
521
|
+
if (!statusCodes.includes((0, apply_1.statusCodeOf)(e) ?? 0)) {
|
|
522
|
+
throw e;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
//# sourceMappingURL=network-policy.js.map
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.removeInvalidCharacters = exports.getResourceName = void 0;
|
|
4
|
+
// An object name is a DNS label: lowercase letters, digits and `-`, at most 63
|
|
5
|
+
// characters, starting and ending with a letter or digit. The id makes a name
|
|
6
|
+
// unique; the item's name is there for people and is cut to fit.
|
|
7
|
+
const MAX_NAME = 63;
|
|
4
8
|
function getResourceName(item, suffix) {
|
|
5
|
-
|
|
9
|
+
const tail = `-${item.id()}${suffix ?? ''}`;
|
|
10
|
+
const name = removeInvalidCharacters(item.name)
|
|
11
|
+
.slice(0, Math.max(0, MAX_NAME - tail.length))
|
|
12
|
+
.replace(/-+$/, '');
|
|
13
|
+
return name ? name + tail : tail.slice(1);
|
|
6
14
|
}
|
|
7
15
|
exports.getResourceName = getResourceName;
|
|
8
16
|
function removeInvalidCharacters(val) {
|
|
9
|
-
return val
|
|
17
|
+
return val
|
|
18
|
+
.toLowerCase()
|
|
19
|
+
.replace(/[^a-z0-9-]+/g, '-')
|
|
20
|
+
.replace(/^-+/, '');
|
|
10
21
|
}
|
|
11
22
|
exports.removeInvalidCharacters = removeInvalidCharacters;
|
|
12
23
|
//# sourceMappingURL=resources.js.map
|
|
@@ -6,16 +6,27 @@ const log_1 = require("../log");
|
|
|
6
6
|
const path_1 = require("path");
|
|
7
7
|
const volumes_1 = require("./volumes");
|
|
8
8
|
const ensure_persistent_data_1 = require("./ensure-persistent-data");
|
|
9
|
-
async function restoreKubernetesData(work, kubernetes, instance, environment, path
|
|
10
|
-
|
|
9
|
+
async function restoreKubernetesData(work, kubernetes, instance, environment, path,
|
|
10
|
+
// the kept claim of the work holds what an earlier run left, which goes first
|
|
11
|
+
options = {}) {
|
|
12
|
+
const persistence = await (0, volumes_1.getKubernetesPersistence)(work, kubernetes);
|
|
11
13
|
await (0, ensure_persistent_data_1.getPodForPersistence)(instance, kubernetes, work, persistence, 'write', async (name) => {
|
|
14
|
+
if (options.reset) {
|
|
15
|
+
await (0, ensure_persistent_data_1.clearClaim)(instance, kubernetes, work, name);
|
|
16
|
+
}
|
|
12
17
|
for (const generatedArchive of (0, event_cache_1.getArchivePaths)(work.data, path)) {
|
|
13
18
|
if (!(await environment.file.exists(generatedArchive.filename))) {
|
|
14
19
|
continue;
|
|
15
20
|
}
|
|
16
21
|
const targetPath = `/dev/hammerkit/${work.id()}/${(0, path_1.basename)(generatedArchive.path)}`;
|
|
17
|
-
|
|
18
|
-
|
|
22
|
+
// a claim made for this run (a server removes the claims of a run when it
|
|
23
|
+
// ends) does not have the directory of the output yet
|
|
24
|
+
let failure = null;
|
|
25
|
+
const res = await instance.exec.exec(kubernetes.namespace, name, 'volume', ['sh', '-c', 'mkdir -p "$1" && tar xzf - -C "$1"', 'sh', targetPath], process.stdout, process.stderr, environment.file.readStream(generatedArchive.filename), false, (status) => {
|
|
26
|
+
work.status.write('debug', `upload ${generatedArchive.filename} to ${name}: ${status.status}`);
|
|
27
|
+
if (status.status !== 'Success') {
|
|
28
|
+
failure = status.message ?? status.reason ?? 'unknown';
|
|
29
|
+
}
|
|
19
30
|
});
|
|
20
31
|
await new Promise((resolve, reject) => {
|
|
21
32
|
try {
|
|
@@ -27,6 +38,10 @@ async function restoreKubernetesData(work, kubernetes, instance, environment, pa
|
|
|
27
38
|
reject(err);
|
|
28
39
|
});
|
|
29
40
|
res.on('close', () => {
|
|
41
|
+
if (failure) {
|
|
42
|
+
reject(new Error(`unable to restore ${generatedArchive.path} into ${name}: ${failure}`));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
30
45
|
work.status.write('debug', `uploaded ${generatedArchive.filename} to ${name}:${targetPath}`);
|
|
31
46
|
resolve();
|
|
32
47
|
});
|