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,583 @@
|
|
|
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 resources_1 = require("./resources");
|
|
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
|
+
// 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';
|
|
29
|
+
// how long the pods of an item may take to end before their policy goes
|
|
30
|
+
const POD_END_TIMEOUT_MS = 120000;
|
|
31
|
+
const POD_END_POLL_MS = 500;
|
|
32
|
+
// on the pods of a limited item on a server: its own policy governs their egress, the
|
|
33
|
+
// one of the caller's namespace (see getNamespacePolicies) leaves them out
|
|
34
|
+
// (`limited`), and on its egress proxy (`proxy`), whose ingress only its own policy lets in
|
|
35
|
+
exports.NETWORK_LABEL = 'hammerkit.dev/network';
|
|
36
|
+
exports.NETWORK_LIMITED = 'limited';
|
|
37
|
+
exports.NETWORK_PROXY = 'proxy';
|
|
38
|
+
function getNetworkLabels(env, item) {
|
|
39
|
+
return env.server && (0, work_network_1.getEffectiveNetwork)(item.data).type !== 'open' ? { [exports.NETWORK_LABEL]: exports.NETWORK_LIMITED } : {};
|
|
40
|
+
}
|
|
41
|
+
exports.getNetworkLabels = getNetworkLabels;
|
|
42
|
+
// A key short enough for a Service name (63 characters) with its prefix. A server's run
|
|
43
|
+
// label is longer than a run id of the CLI, and its first 8 digits tell runs apart.
|
|
44
|
+
function getNetworkKey(item, scope) {
|
|
45
|
+
return scope.run ? `${item.id().substring(0, 32)}-${scope.run.substring(0, 8)}` : item.id();
|
|
46
|
+
}
|
|
47
|
+
function getProxyName(item, scope) {
|
|
48
|
+
return `hammerkit-proxy-${getNetworkKey(item, scope)}`;
|
|
49
|
+
}
|
|
50
|
+
exports.getProxyName = getProxyName;
|
|
51
|
+
function getRunProxy(item, scope) {
|
|
52
|
+
return { key: getNetworkKey(item, scope), name: getProxyName(item, scope) };
|
|
53
|
+
}
|
|
54
|
+
// A proxy that outlives its run is told apart by all it is made of: a run whose task,
|
|
55
|
+
// network, needed services, server or pod of files changed gets one of its own, and the
|
|
56
|
+
// one before goes (removeSandboxProxies).
|
|
57
|
+
function getSandboxProxy(env, item, network, hostAliases, sandbox) {
|
|
58
|
+
const key = (0, crypto_1.createHash)('sha256')
|
|
59
|
+
.update(JSON.stringify({
|
|
60
|
+
sandbox: sandbox.label,
|
|
61
|
+
pod: sandbox.pod?.uid ?? null,
|
|
62
|
+
item: item.id(),
|
|
63
|
+
network,
|
|
64
|
+
env: (0, egress_proxy_1.getItemProxyEnvironment)(item),
|
|
65
|
+
hostAliases,
|
|
66
|
+
placement: env.server ? (0, job_placement_1.getPodPlacement)(env.server, undefined) : null,
|
|
67
|
+
image: egress_proxy_1.EGRESS_PROXY_IMAGE,
|
|
68
|
+
version: (0, version_1.getVersion)(),
|
|
69
|
+
}))
|
|
70
|
+
.digest('hex')
|
|
71
|
+
.slice(0, 20);
|
|
72
|
+
return { key, name: `hammerkit-proxy-${key}`, sandbox };
|
|
73
|
+
}
|
|
74
|
+
exports.getSandboxProxy = getSandboxProxy;
|
|
75
|
+
// What the proxy's objects are labeled with: removed with the run, or, on a sandbox,
|
|
76
|
+
// with the pod of its files.
|
|
77
|
+
function getProxyLabels(env, item, proxy) {
|
|
78
|
+
return {
|
|
79
|
+
[NETWORK_OF_LABEL]: item.id(),
|
|
80
|
+
...(proxy.sandbox ? { [PROXY_OF_SANDBOX_LABEL]: proxy.sandbox.label } : (0, job_placement_1.getRunLabels)(env)),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function getNetworkPolicyName(item, scope) {
|
|
84
|
+
return `hammerkit-network-${getNetworkKey(item, scope)}`;
|
|
85
|
+
}
|
|
86
|
+
exports.getNetworkPolicyName = getNetworkPolicyName;
|
|
87
|
+
// the pods of the item, of the run when the scope has one
|
|
88
|
+
function getItemPodLabels(item, scope) {
|
|
89
|
+
return { [resources_1.ID_LABEL]: item.id(), ...(scope.run ? { [job_placement_1.RUN_LABEL]: scope.run } : {}) };
|
|
90
|
+
}
|
|
91
|
+
function toSelector(labels) {
|
|
92
|
+
return Object.entries(labels)
|
|
93
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
94
|
+
.join(',');
|
|
95
|
+
}
|
|
96
|
+
// Who may connect to a limited item: the items that need it, and for a
|
|
97
|
+
// service a Gateway API route of the environment, the cluster's equivalent
|
|
98
|
+
// of a published port. A task takes no connections.
|
|
99
|
+
function getIngress(env, item) {
|
|
100
|
+
if (!(0, work_service_1.isContainerWorkService)(item.data)) {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
if (env.httpRoutes.some((route) => route.service === item.name)) {
|
|
104
|
+
return [{}];
|
|
105
|
+
}
|
|
106
|
+
const ids = [...new Set(item.requiredBy.map((requirer) => requirer.id()))];
|
|
107
|
+
if (ids.length === 0) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
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
|
+
};
|
|
116
|
+
// the client's name for `from`, a reserved word
|
|
117
|
+
return [{ _from: [{ podSelector: from }] }];
|
|
118
|
+
}
|
|
119
|
+
// The NetworkPolicy of a limited item: egress to the pods of the services it
|
|
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') {
|
|
124
|
+
const needs = item.needs.filter((need) => (0, work_item_1.isContainerWorkServiceItem)(need.service));
|
|
125
|
+
return {
|
|
126
|
+
apiVersion: 'networking.k8s.io/v1',
|
|
127
|
+
kind: 'NetworkPolicy',
|
|
128
|
+
metadata: {
|
|
129
|
+
namespace: env.namespace,
|
|
130
|
+
name: getNetworkPolicyName(item, scope),
|
|
131
|
+
annotations: { 'hammerkit.dev/version': (0, version_1.getVersion)() },
|
|
132
|
+
labels: { [NETWORK_OF_LABEL]: item.id(), ...(0, job_placement_1.getRunLabels)(env) },
|
|
133
|
+
},
|
|
134
|
+
spec: {
|
|
135
|
+
podSelector: { matchLabels: getItemPodLabels(item, scope) },
|
|
136
|
+
policyTypes: ['Ingress', 'Egress'],
|
|
137
|
+
ingress: getIngress(env, item),
|
|
138
|
+
egress: [
|
|
139
|
+
...(needs.length > 0
|
|
140
|
+
? [{ to: needs.map((need) => ({ podSelector: { matchLabels: (0, resources_1.getItemLabels)(need.service, env) } })) }]
|
|
141
|
+
: []),
|
|
142
|
+
...(proxied
|
|
143
|
+
? [
|
|
144
|
+
{
|
|
145
|
+
to: [{ podSelector: { matchLabels: { [PROXY_LABEL]: proxy.key } } }],
|
|
146
|
+
ports: (0, egress_proxy_1.getEgressProxyPorts)(network).map((port) => ({ protocol: 'TCP', port })),
|
|
147
|
+
},
|
|
148
|
+
]
|
|
149
|
+
: []),
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
exports.buildNetworkPolicy = buildNetworkPolicy;
|
|
155
|
+
// The proxy takes connections from its item's pods only, so no other pod
|
|
156
|
+
// uses its allow list or reaches the item's services through it; a proxy that
|
|
157
|
+
// outlives its run, from the pods of the current run only.
|
|
158
|
+
function buildProxyPolicy(env, item, network, scope, proxy = getRunProxy(item, scope)) {
|
|
159
|
+
return {
|
|
160
|
+
apiVersion: 'networking.k8s.io/v1',
|
|
161
|
+
kind: 'NetworkPolicy',
|
|
162
|
+
metadata: {
|
|
163
|
+
namespace: env.namespace,
|
|
164
|
+
name: proxy.name,
|
|
165
|
+
annotations: { 'hammerkit.dev/version': (0, version_1.getVersion)() },
|
|
166
|
+
labels: getProxyLabels(env, item, proxy),
|
|
167
|
+
...(proxy.owner ? { ownerReferences: [proxy.owner] } : {}),
|
|
168
|
+
},
|
|
169
|
+
spec: {
|
|
170
|
+
podSelector: { matchLabels: { [PROXY_LABEL]: proxy.key } },
|
|
171
|
+
policyTypes: ['Ingress'],
|
|
172
|
+
ingress: [
|
|
173
|
+
{
|
|
174
|
+
_from: [{ podSelector: { matchLabels: getItemPodLabels(item, scope) } }],
|
|
175
|
+
ports: (0, egress_proxy_1.getEgressProxyPorts)(network).map((port) => ({ protocol: 'TCP', port })),
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
exports.buildProxyPolicy = buildProxyPolicy;
|
|
182
|
+
// What the item's pods are created with, after its policy (and proxy) are in
|
|
183
|
+
// place, so a pod never runs unlimited. A limited item needs an environment
|
|
184
|
+
// that states its cluster enforces NetworkPolicy: hammerkit can't tell from
|
|
185
|
+
// the API, and a policy nobody enforces would run the item open.
|
|
186
|
+
// An open service gets what it had before: whatever an earlier limited run
|
|
187
|
+
// left goes.
|
|
188
|
+
// A task follows the proxy's log while its jobs run. A service outlives the
|
|
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.
|
|
193
|
+
async function ensureKubernetesItemNetwork(instance, env, item, options) {
|
|
194
|
+
const network = (0, work_network_1.getEffectiveNetwork)(item.data);
|
|
195
|
+
if (network.type !== 'open' && !env.networkPolicyEnforced) {
|
|
196
|
+
throw new Error(`${item.name}: network ${network.type} needs a cluster that enforces NetworkPolicy, ` +
|
|
197
|
+
'state it with networkPolicy: enforced on the kubernetes environment');
|
|
198
|
+
}
|
|
199
|
+
const hostAliases = await getNeedHostAliases(instance, env, item);
|
|
200
|
+
if (network.type === 'open') {
|
|
201
|
+
if (!options.run) {
|
|
202
|
+
// without the rights to manage NetworkPolicies, hammerkit never made one
|
|
203
|
+
await removeKubernetesItemNetwork(instance, env, item, { run: null }, { ignoreForbidden: true });
|
|
204
|
+
}
|
|
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`);
|
|
211
|
+
}
|
|
212
|
+
const proxy = network.type === 'allow' && options.sandbox
|
|
213
|
+
? getSandboxProxy(env, item, network, hostAliases, options.sandbox)
|
|
214
|
+
: getRunProxy(item, options);
|
|
215
|
+
const proxied = network.type === 'allow' || secretHosts.length > 0;
|
|
216
|
+
await (0, apply_1.apply)(instance, buildNetworkPolicy(env, item, network, options, proxy, proxied));
|
|
217
|
+
const initContainers = [(0, network_gate_1.getNetworkGateContainer)()];
|
|
218
|
+
if (!proxied) {
|
|
219
|
+
if (options.sandbox) {
|
|
220
|
+
await removeSandboxProxies(instance, env.namespace, { sandbox: options.sandbox.label, item });
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
await removeProxy(instance, env.namespace, proxy.name, [404]);
|
|
224
|
+
}
|
|
225
|
+
return { env: [], secretEnv: [], hostAliases, initContainers, close() { } };
|
|
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: [] };
|
|
229
|
+
let started;
|
|
230
|
+
try {
|
|
231
|
+
if (proxy.sandbox) {
|
|
232
|
+
// a proxy of the sandbox from before, made of something else, goes
|
|
233
|
+
await removeSandboxProxies(instance, env.namespace, { sandbox: proxy.sandbox.label, item }, (deployment) => deployment.metadata?.labels?.[PROXY_LABEL] === proxy.key);
|
|
234
|
+
proxy.owner = await getFilesPodOwner(instance, env, proxy.sandbox);
|
|
235
|
+
}
|
|
236
|
+
started = await ensureProxy(instance, env, item, proxyNetwork, hostAliases, options, proxy, secretHosts);
|
|
237
|
+
}
|
|
238
|
+
catch (e) {
|
|
239
|
+
await removeKubernetesItemNetwork(instance, env, item, options).catch(() => undefined);
|
|
240
|
+
throw e;
|
|
241
|
+
}
|
|
242
|
+
const follow = options.followLog
|
|
243
|
+
? await followProxyLog(instance, env, item, proxy, { tailLines: started.reused ? 0 : undefined })
|
|
244
|
+
: null;
|
|
245
|
+
const needs = item.needs.map((need) => need.name);
|
|
246
|
+
const toEnv = (variables) => Object.entries(variables).map(([name, value]) => ({ name, value }));
|
|
247
|
+
return {
|
|
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
|
+
: [],
|
|
256
|
+
// the item reaches each TCP forward under its host's name, at the proxy
|
|
257
|
+
hostAliases: [
|
|
258
|
+
...hostAliases,
|
|
259
|
+
{ ip: started.ip, hostnames: [egress_proxy_1.EGRESS_PROXY_HOST, ...(0, work_network_1.getTcpForwards)(network).map((forward) => forward.host)] },
|
|
260
|
+
],
|
|
261
|
+
initContainers,
|
|
262
|
+
close: () => follow?.abort(),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
exports.ensureKubernetesItemNetwork = ensureKubernetesItemNetwork;
|
|
266
|
+
// The policy and proxy of the scope go only once no pod of it runs anymore:
|
|
267
|
+
// a pod that is still terminating (cancel, timeout, `down`) would otherwise
|
|
268
|
+
// run without limits. When pods outlast the wait, the policy stays.
|
|
269
|
+
// The proxy of a task on a sandbox stays while the pod of the sandbox's files
|
|
270
|
+
// runs, which owns it; one it does not own goes with the run.
|
|
271
|
+
async function removeKubernetesItemNetwork(instance, env, item, scope, options = { ignoreForbidden: false }) {
|
|
272
|
+
const ignored = options.ignoreForbidden ? [403, 404] : [404];
|
|
273
|
+
await ignoreStatus(instance.objectApi.delete({
|
|
274
|
+
apiVersion: 'networking.k8s.io/v1',
|
|
275
|
+
kind: 'NetworkPolicy',
|
|
276
|
+
metadata: { name: getNetworkPolicyName(item, scope), namespace: env.namespace },
|
|
277
|
+
}), ignored);
|
|
278
|
+
if (scope.sandbox) {
|
|
279
|
+
const owner = await getFilesPodOwner(instance, env, scope.sandbox);
|
|
280
|
+
await removeSandboxProxies(instance, env.namespace, { sandbox: scope.sandbox.label, item }, (deployment) => (deployment.metadata?.ownerReferences ?? []).some((reference) => !!owner && reference.uid === owner.uid));
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
await removeProxy(instance, env.namespace, getProxyName(item, scope), ignored);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
exports.removeKubernetesItemNetwork = removeKubernetesItemNetwork;
|
|
287
|
+
// Waits until no pod with the labels runs or waits to run.
|
|
288
|
+
async function awaitPodsEnded(instance, env, labels, timeout = POD_END_TIMEOUT_MS) {
|
|
289
|
+
const deadline = Date.now() + timeout;
|
|
290
|
+
for (;;) {
|
|
291
|
+
const pods = await instance.coreApi.listNamespacedPod({
|
|
292
|
+
namespace: env.namespace,
|
|
293
|
+
labelSelector: toSelector(labels),
|
|
294
|
+
});
|
|
295
|
+
if (pods.items.every((pod) => pod.status?.phase === 'Succeeded' || pod.status?.phase === 'Failed')) {
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
if (Date.now() >= deadline) {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
await (0, sleep_1.sleep)(POD_END_POLL_MS);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
exports.awaitPodsEnded = awaitPodsEnded;
|
|
305
|
+
// Removes the scope's policy and proxy once its pods ended.
|
|
306
|
+
async function releaseKubernetesItemNetwork(instance, env, item, scope) {
|
|
307
|
+
if (!(await awaitPodsEnded(instance, env, getItemPodLabels(item, scope)))) {
|
|
308
|
+
throw new Error(`pods of ${item.name} are still running, so its network policy stays`);
|
|
309
|
+
}
|
|
310
|
+
await removeKubernetesItemNetwork(instance, env, item, scope);
|
|
311
|
+
}
|
|
312
|
+
exports.releaseKubernetesItemNetwork = releaseKubernetesItemNetwork;
|
|
313
|
+
// Every policy and proxy of the item, of any run, once none of its pods runs:
|
|
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.
|
|
317
|
+
async function removeAllKubernetesItemNetworks(instance, env, item) {
|
|
318
|
+
if (!(await awaitPodsEnded(instance, env, (0, resources_1.getItemLabels)(item, env)))) {
|
|
319
|
+
throw new Error(`pods of ${item.name} are still running, so its network policy stays`);
|
|
320
|
+
}
|
|
321
|
+
const labelSelector = toSelector({ [NETWORK_OF_LABEL]: item.id(), ...(0, job_placement_1.getRunLabels)(env) });
|
|
322
|
+
let policies;
|
|
323
|
+
try {
|
|
324
|
+
const list = await instance.objectApi.list('networking.k8s.io/v1', 'NetworkPolicy', env.namespace, undefined, undefined, undefined, undefined, labelSelector);
|
|
325
|
+
policies = list.items;
|
|
326
|
+
}
|
|
327
|
+
catch (e) {
|
|
328
|
+
// without the rights to manage NetworkPolicies, hammerkit never made one
|
|
329
|
+
if ((0, apply_1.statusCodeOf)(e) === 403) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
throw e;
|
|
333
|
+
}
|
|
334
|
+
// the items of a list carry no kind of their own
|
|
335
|
+
for (const name of policies.map((policy) => policy.metadata?.name).filter((name) => !!name)) {
|
|
336
|
+
await ignoreStatus(instance.objectApi.delete({
|
|
337
|
+
apiVersion: 'networking.k8s.io/v1',
|
|
338
|
+
kind: 'NetworkPolicy',
|
|
339
|
+
metadata: { name, namespace: env.namespace },
|
|
340
|
+
}), [404]);
|
|
341
|
+
}
|
|
342
|
+
const deployments = await instance.appsApi.listNamespacedDeployment({ namespace: env.namespace, labelSelector });
|
|
343
|
+
for (const deployment of deployments.items) {
|
|
344
|
+
const name = deployment.metadata?.name;
|
|
345
|
+
if (name) {
|
|
346
|
+
await ignoreStatus(instance.appsApi.deleteNamespacedDeployment({ name, namespace: env.namespace }), [404]);
|
|
347
|
+
await ignoreStatus(instance.coreApi.deleteNamespacedService({ name, namespace: env.namespace }), [404]);
|
|
348
|
+
await ignoreStatus(instance.coreApi.deleteNamespacedSecret({ name, namespace: env.namespace }), [404]);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
exports.removeAllKubernetesItemNetworks = removeAllKubernetesItemNetworks;
|
|
353
|
+
// the services the item needs, by the names it lists them under
|
|
354
|
+
async function getNeedHostAliases(instance, env, item) {
|
|
355
|
+
const hostAliases = [];
|
|
356
|
+
for (const need of item.needs) {
|
|
357
|
+
if (!(0, work_item_1.isContainerWorkServiceItem)(need.service)) {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const ip = await (0, get_service_ip_1.getServiceIp)(instance, env, need.service);
|
|
361
|
+
if (!ip) {
|
|
362
|
+
throw new Error(`unable to get service ip for ${need.name}`);
|
|
363
|
+
}
|
|
364
|
+
hostAliases.push({ ip, hostnames: [need.name] });
|
|
365
|
+
}
|
|
366
|
+
return hostAliases;
|
|
367
|
+
}
|
|
368
|
+
// The proxy as a Deployment and a Service; its pods reach out without an
|
|
369
|
+
// egress policy of their own, and reach the needed services by name for tools
|
|
370
|
+
// that ignore NO_PROXY. Unprivileged, from a pinned image. Its Service carries
|
|
371
|
+
// the port of each TCP forward too.
|
|
372
|
+
// Exits 0 once every port answers on the pod's loopback.
|
|
373
|
+
function getProxyReadinessScript(ports) {
|
|
374
|
+
return (`const net = require('net'); let open = ${ports.length}; ` +
|
|
375
|
+
`for (const port of ${JSON.stringify(ports)}) net.connect(port, '127.0.0.1')` +
|
|
376
|
+
`.on('connect', function () { this.destroy(); if (--open === 0) process.exit(0) })` +
|
|
377
|
+
`.on('error', () => process.exit(1))`);
|
|
378
|
+
}
|
|
379
|
+
exports.getProxyReadinessScript = getProxyReadinessScript;
|
|
380
|
+
async function ensureProxy(instance, env, item, network, hostAliases, scope, proxy, secretHosts = []) {
|
|
381
|
+
const name = proxy.name;
|
|
382
|
+
const labels = { [PROXY_LABEL]: proxy.key };
|
|
383
|
+
const metadata = {
|
|
384
|
+
namespace: env.namespace,
|
|
385
|
+
name,
|
|
386
|
+
annotations: { 'hammerkit.dev/version': (0, version_1.getVersion)() },
|
|
387
|
+
labels: { ...labels, ...getProxyLabels(env, item, proxy) },
|
|
388
|
+
...(proxy.owner ? { ownerReferences: [proxy.owner] } : {}),
|
|
389
|
+
};
|
|
390
|
+
// on a server the proxy runs on the nodes of the first pool
|
|
391
|
+
const placement = env.server ? (0, job_placement_1.getPodPlacement)(env.server, undefined) : null;
|
|
392
|
+
const deployment = {
|
|
393
|
+
apiVersion: 'apps/v1',
|
|
394
|
+
kind: 'Deployment',
|
|
395
|
+
metadata,
|
|
396
|
+
spec: {
|
|
397
|
+
replicas: 1,
|
|
398
|
+
selector: { matchLabels: labels },
|
|
399
|
+
template: {
|
|
400
|
+
metadata: {
|
|
401
|
+
labels: {
|
|
402
|
+
...labels,
|
|
403
|
+
...(proxy.sandbox ? {} : (0, job_placement_1.getRunLabels)(env)),
|
|
404
|
+
...(env.server ? { [exports.NETWORK_LABEL]: exports.NETWORK_PROXY } : {}),
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
spec: {
|
|
408
|
+
hostAliases,
|
|
409
|
+
automountServiceAccountToken: false,
|
|
410
|
+
...(placement ? { nodeSelector: placement.nodeSelector, tolerations: placement.tolerations } : {}),
|
|
411
|
+
// unprivileged, it still listens on the port of a TCP forward below
|
|
412
|
+
// 1024; a sysctl every Pod Security level allows
|
|
413
|
+
securityContext: { sysctls: [{ name: 'net.ipv4.ip_unprivileged_port_start', value: '0' }] },
|
|
414
|
+
containers: [
|
|
415
|
+
{
|
|
416
|
+
name: 'proxy',
|
|
417
|
+
image: egress_proxy_1.EGRESS_PROXY_IMAGE,
|
|
418
|
+
command: (0, egress_proxy_1.getEgressProxyCommand)(),
|
|
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
|
+
],
|
|
430
|
+
ports: (0, egress_proxy_1.getEgressProxyPorts)(network).map((port) => ({ containerPort: port })),
|
|
431
|
+
// in the pod, since its ingress policy may keep the kubelet out;
|
|
432
|
+
// ready once it listens on every port, its TCP forwards' too
|
|
433
|
+
readinessProbe: {
|
|
434
|
+
exec: {
|
|
435
|
+
command: ['node', '-e', getProxyReadinessScript((0, egress_proxy_1.getEgressProxyPorts)(network))],
|
|
436
|
+
},
|
|
437
|
+
periodSeconds: 2,
|
|
438
|
+
},
|
|
439
|
+
securityContext: network_gate_1.HELPER_SECURITY_CONTEXT,
|
|
440
|
+
resources: { limits: { memory: '256Mi' } },
|
|
441
|
+
},
|
|
442
|
+
],
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
const service = {
|
|
448
|
+
apiVersion: 'v1',
|
|
449
|
+
kind: 'Service',
|
|
450
|
+
metadata,
|
|
451
|
+
spec: {
|
|
452
|
+
type: 'ClusterIP',
|
|
453
|
+
selector: labels,
|
|
454
|
+
ports: (0, egress_proxy_1.getEgressProxyPorts)(network).map((port) => ({ name: `tcp-${port}`, port, targetPort: port })),
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
// on a sandbox, the proxy of an earlier run may still be there: the item's policy
|
|
458
|
+
// of this run lets its pods reach it, its own policy lets them in
|
|
459
|
+
const reused = proxy.sandbox
|
|
460
|
+
? await instance.objectApi.read(deployment).then((current) => !current.metadata?.deletionTimestamp, (e) => ((0, apply_1.statusCodeOf)(e) === 404 ? false : Promise.reject(e)))
|
|
461
|
+
: false;
|
|
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
|
+
}
|
|
478
|
+
await (0, apply_1.apply)(instance, deployment);
|
|
479
|
+
const created = await (0, apply_1.apply)(instance, service);
|
|
480
|
+
await (0, await_running_state_1.awaitDeployRunningState)(instance, env, name, {
|
|
481
|
+
podSelector: toSelector(labels),
|
|
482
|
+
timeout: PROXY_READY_TIMEOUT_MS,
|
|
483
|
+
timeoutMessage: `the egress proxy of ${item.name} did not start`,
|
|
484
|
+
});
|
|
485
|
+
const ip = created.spec?.clusterIP;
|
|
486
|
+
if (!ip) {
|
|
487
|
+
throw new Error(`the egress proxy of ${item.name} has no cluster ip`);
|
|
488
|
+
}
|
|
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` : ''}`);
|
|
490
|
+
return { ip, reused };
|
|
491
|
+
}
|
|
492
|
+
// Refused hosts show up in the item's log, as on Docker; of a proxy from an
|
|
493
|
+
// earlier run (`tailLines: 0`), only what this run asks of it.
|
|
494
|
+
async function followProxyLog(instance, env, item, proxy, options) {
|
|
495
|
+
try {
|
|
496
|
+
const pods = await instance.coreApi.listNamespacedPod({
|
|
497
|
+
namespace: env.namespace,
|
|
498
|
+
labelSelector: `${PROXY_LABEL}=${proxy.key}`,
|
|
499
|
+
});
|
|
500
|
+
const pod = pods.items.find((p) => p.status?.phase === 'Running' && !p.metadata?.deletionTimestamp);
|
|
501
|
+
if (!pod?.metadata?.name) {
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
const output = new stream_1.PassThrough();
|
|
505
|
+
// close() aborts the request, which errors the response stream the
|
|
506
|
+
// client pipes in here; nothing listens to it there
|
|
507
|
+
output.on('pipe', (source) => source.on('error', () => undefined));
|
|
508
|
+
(0, readline_1.createInterface)({ input: output }).on('line', (line) => {
|
|
509
|
+
const event = (0, egress_proxy_1.parseEgressProxyEvent)(line);
|
|
510
|
+
if (event && event.type !== 'ready') {
|
|
511
|
+
item.status.write('warn', (0, egress_proxy_1.formatEgressProxyEvent)(event));
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
return await instance.log.log(env.namespace, pod.metadata.name, 'proxy', output, {
|
|
515
|
+
follow: true,
|
|
516
|
+
...(options.tailLines !== undefined ? { tailLines: options.tailLines } : {}),
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
catch (e) {
|
|
520
|
+
item.status.write('debug', `unable to follow the egress proxy log: ${(0, log_1.getErrorMessage)(e)}`);
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
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 }) {
|
|
526
|
+
await ignoreStatus(instance.appsApi.deleteNamespacedDeployment({ name, namespace }), ignored);
|
|
527
|
+
await ignoreStatus(instance.coreApi.deleteNamespacedService({ name, namespace }), ignored);
|
|
528
|
+
if (options.token) {
|
|
529
|
+
await ignoreStatus(instance.coreApi.deleteNamespacedSecret({ name, namespace }), ignored);
|
|
530
|
+
}
|
|
531
|
+
await ignoreStatus(instance.objectApi.delete({
|
|
532
|
+
apiVersion: 'networking.k8s.io/v1',
|
|
533
|
+
kind: 'NetworkPolicy',
|
|
534
|
+
metadata: { name, namespace },
|
|
535
|
+
}), ignored);
|
|
536
|
+
}
|
|
537
|
+
// The proxies on the sandbox, of the item when one is given, but those `keep` takes.
|
|
538
|
+
// The cluster removes those the pod of the sandbox's files owns when the pod goes; the
|
|
539
|
+
// server removes all when it took the pod away while a run worked (see
|
|
540
|
+
// KubernetesSandboxStorage.makeWayForRun).
|
|
541
|
+
async function removeSandboxProxies(instance, namespace, of, keep = () => false) {
|
|
542
|
+
const deployments = await instance.appsApi.listNamespacedDeployment({
|
|
543
|
+
namespace,
|
|
544
|
+
labelSelector: [
|
|
545
|
+
`${PROXY_OF_SANDBOX_LABEL}=${of.sandbox}`,
|
|
546
|
+
...(of.item ? [`${NETWORK_OF_LABEL}=${of.item.id()}`] : []),
|
|
547
|
+
].join(','),
|
|
548
|
+
});
|
|
549
|
+
for (const deployment of deployments.items) {
|
|
550
|
+
const name = deployment.metadata?.name;
|
|
551
|
+
if (name && !keep(deployment)) {
|
|
552
|
+
await removeProxy(instance, namespace, name, [404], { token: false });
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
exports.removeSandboxProxies = removeSandboxProxies;
|
|
557
|
+
// The pod of the sandbox's files the run holds, as the owner of the proxy: the cluster
|
|
558
|
+
// removes the proxy when that pod goes. None when the pod is no longer the one the run
|
|
559
|
+
// holds (it made way for a pod of the run): the proxy then goes with the run.
|
|
560
|
+
async function getFilesPodOwner(instance, env, sandbox) {
|
|
561
|
+
if (!sandbox.pod) {
|
|
562
|
+
return undefined;
|
|
563
|
+
}
|
|
564
|
+
const { name, uid } = sandbox.pod;
|
|
565
|
+
const current = await instance.coreApi
|
|
566
|
+
.readNamespacedPod({ name, namespace: env.namespace })
|
|
567
|
+
.catch((e) => ((0, apply_1.statusCodeOf)(e) === 404 ? undefined : Promise.reject(e)));
|
|
568
|
+
if (!current || current.metadata?.uid !== uid || current.metadata?.deletionTimestamp) {
|
|
569
|
+
return undefined;
|
|
570
|
+
}
|
|
571
|
+
return { apiVersion: 'v1', kind: 'Pod', name, uid };
|
|
572
|
+
}
|
|
573
|
+
async function ignoreStatus(promise, statusCodes) {
|
|
574
|
+
try {
|
|
575
|
+
await promise;
|
|
576
|
+
}
|
|
577
|
+
catch (e) {
|
|
578
|
+
if (!statusCodes.includes((0, apply_1.statusCodeOf)(e) ?? 0)) {
|
|
579
|
+
throw e;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
//# sourceMappingURL=network-policy.js.map
|
|
@@ -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,45 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.removeInvalidCharacters = exports.getResourceName = void 0;
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
exports.removeInvalidCharacters = exports.getResourceName = exports.getItemSelector = exports.getItemLabels = exports.getItemKey = exports.ID_LABEL = void 0;
|
|
4
|
+
const job_placement_1 = require("./job-placement");
|
|
5
|
+
// An object name is a DNS label: lowercase letters, digits and `-`, at most 63
|
|
6
|
+
// characters, starting and ending with a letter or digit. The id makes a name
|
|
7
|
+
// unique; the item's name is there for people and is cut to fit.
|
|
8
|
+
const MAX_NAME = 63;
|
|
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 ?? ''}`;
|
|
32
|
+
const name = removeInvalidCharacters(item.name)
|
|
33
|
+
.slice(0, Math.max(0, MAX_NAME - tail.length))
|
|
34
|
+
.replace(/-+$/, '');
|
|
35
|
+
return name ? name + tail : tail.slice(1);
|
|
6
36
|
}
|
|
7
37
|
exports.getResourceName = getResourceName;
|
|
8
38
|
function removeInvalidCharacters(val) {
|
|
9
|
-
return val
|
|
39
|
+
return val
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.replace(/[^a-z0-9-]+/g, '-')
|
|
42
|
+
.replace(/^-+/, '');
|
|
10
43
|
}
|
|
11
44
|
exports.removeInvalidCharacters = removeInvalidCharacters;
|
|
12
45
|
//# sourceMappingURL=resources.js.map
|