hammerkit 1.27.0-next.2 → 1.27.0-next.20
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/cache/explain.js +17 -0
- package/dist/cli.js +1 -0
- package/dist/docker/item-network.js +13 -5
- package/dist/executer/deadline.js +17 -3
- package/dist/executer/docker-service.js +30 -8
- package/dist/executer/event-cache.js +20 -2
- package/dist/executer/execute-work-task.js +5 -3
- package/dist/executer/execution-steps.js +5 -2
- package/dist/executer/scheduler/enqueue-next.js +7 -0
- package/dist/executer/scheduler/state-key.js +42 -5
- package/dist/harness/connect.js +20 -6
- package/dist/harness/mcp-command.js +22 -6
- package/dist/harness/mcp.js +63 -114
- package/dist/harness/opencode.js +158 -50
- package/dist/harness/pi.js +29 -0
- package/dist/harness/sandbox-client.js +207 -65
- package/dist/harness/service-tools.js +141 -0
- package/dist/harness/task-tools.js +221 -0
- package/dist/kubernetes/apply.js +27 -16
- package/dist/kubernetes/claim-layout.js +71 -0
- package/dist/kubernetes/ensure-http-route.js +1 -1
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +5 -9
- package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +1 -1
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +6 -9
- package/dist/kubernetes/ensure-persistent-data.js +30 -20
- package/dist/kubernetes/get-service-ip.js +2 -2
- package/dist/kubernetes/job-secrets.js +33 -19
- package/dist/kubernetes/kept-claims.js +22 -10
- package/dist/kubernetes/network-policy.js +85 -27
- package/dist/kubernetes/remove-persistent-data.js +3 -1
- package/dist/kubernetes/resources.js +25 -3
- package/dist/kubernetes/restore-kubernetes-data.js +9 -3
- package/dist/kubernetes/sandbox-runner.js +626 -0
- package/dist/kubernetes/secrets.js +7 -11
- package/dist/kubernetes/volumes.js +6 -3
- package/dist/network/egress-proxy.js +45 -6
- package/dist/optimizer/calculate-checksum.js +3 -2
- package/dist/optimizer/check-key.js +94 -0
- package/dist/optimizer/check-keys.js +64 -0
- package/dist/optimizer/get-work-cache-stats.js +17 -3
- package/dist/optimizer/work-task-cache-description.js +51 -1
- package/dist/planner/utils/append-work-dependencies.js +10 -5
- package/dist/planner/utils/append-work-service.js +1 -0
- package/dist/planner/utils/append-work-task.js +15 -2
- package/dist/planner/utils/get-work-runtime.js +6 -1
- package/dist/planner/utils/normalize-path.js +19 -1
- package/dist/planner/utils/parse-work-mount.js +7 -1
- package/dist/planner/utils/parse-work-port.js +4 -1
- package/dist/planner/utils/task-arguments.js +3 -0
- package/dist/planner/validate.js +22 -0
- package/dist/planner/work-network.js +3 -1
- package/dist/planner/work-runtime-aggregate.js +32 -0
- package/dist/planner/work-runtime-docker.js +2 -1
- package/dist/planner/work-runtime-kubernetes.js +97 -48
- package/dist/planner/work-runtime-local.js +2 -1
- package/dist/planner/work-task.js +6 -1
- package/dist/program.js +14 -4
- package/dist/remote/annotations.js +13 -1
- package/dist/remote/device-login.js +7 -2
- package/dist/remote/host-config.js +64 -3
- package/dist/remote/remote-commands.js +41 -9
- package/dist/remote/remote-work-commands.js +246 -1
- package/dist/remote/server-client.js +15 -2
- package/dist/remote/token-source.js +64 -26
- package/dist/run-program.js +9 -0
- package/dist/schema/build-file-base-task-schema.js +5 -1
- package/dist/schema/build-file-container-service-schema.js +2 -0
- package/dist/schema/reference-parser.js +9 -1
- package/dist/schema/work-scope-parser.js +6 -0
- package/dist/secrets/github-app-token-command.js +3 -1
- package/dist/server/accounts-api.js +3 -1
- package/dist/server/approvals-api.js +1 -1
- package/dist/server/checks-api.js +83 -0
- package/dist/server/checks.js +116 -0
- package/dist/server/definitions-api.js +4 -2
- package/dist/server/endpoints.js +612 -0
- package/dist/server/executor.js +56 -5
- package/dist/server/kubernetes-backend.js +303 -64
- package/dist/server/kubernetes-sandbox-storage.js +48 -3
- package/dist/server/oauth.js +12 -1
- package/dist/server/plan.js +126 -62
- package/dist/server/policy.js +32 -2
- package/dist/server/repo-access.js +5 -0
- package/dist/server/run-plan.js +50 -19
- package/dist/server/run-request.js +64 -3
- package/dist/server/run-worker.js +146 -10
- package/dist/server/runs.js +347 -27
- package/dist/server/sandbox-pod.js +28 -0
- package/dist/server/sandbox-storage.js +23 -0
- package/dist/server/sandboxes-api.js +10 -4
- package/dist/server/sandboxes.js +8 -0
- package/dist/server/server-command.js +39 -20
- package/dist/server/server-config.js +213 -14
- package/dist/server/server.js +268 -15
- package/dist/server/service-runs.js +82 -0
- package/dist/utils/abort-on-signals.js +46 -6
- package/dist/utils/tar.js +36 -0
- package/package.json +1 -1
package/dist/planner/validate.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.checkIfContextExists = exports.hasDependencyCycle = exports.hasNeedCycle
|
|
|
4
4
|
const read_build_file_1 = require("../parser/read-build-file");
|
|
5
5
|
const plan_work_tasks_1 = require("./utils/plan-work-tasks");
|
|
6
6
|
const unreferenced_build_file_envs_1 = require("./unreferenced-build-file-envs");
|
|
7
|
+
const normalize_path_1 = require("./utils/normalize-path");
|
|
7
8
|
async function* validate(workTree, context) {
|
|
8
9
|
const cycleItems = [];
|
|
9
10
|
for (const item of (0, plan_work_tasks_1.iterateWorkServices)(workTree)) {
|
|
@@ -16,6 +17,10 @@ async function* validate(workTree, context) {
|
|
|
16
17
|
yield { type: 'warn', message: 'missing healthcheck', item: service };
|
|
17
18
|
}
|
|
18
19
|
for (const mount of service.mounts) {
|
|
20
|
+
const pwd = getPwdMountWarning(mount);
|
|
21
|
+
if (pwd) {
|
|
22
|
+
yield { type: 'warn', message: pwd, item: service };
|
|
23
|
+
}
|
|
19
24
|
if (!(await context.file.exists(mount.localPath))) {
|
|
20
25
|
yield {
|
|
21
26
|
type: 'warn',
|
|
@@ -62,6 +67,12 @@ async function* validate(workTree, context) {
|
|
|
62
67
|
item: task,
|
|
63
68
|
};
|
|
64
69
|
}
|
|
70
|
+
for (const mount of task.type === 'container-task' ? task.mounts : []) {
|
|
71
|
+
const pwd = getPwdMountWarning(mount);
|
|
72
|
+
if (pwd) {
|
|
73
|
+
yield { type: 'warn', message: pwd, item: task };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
65
76
|
for (const src of task.src) {
|
|
66
77
|
if (!(await context.file.exists(src.absolutePath))) {
|
|
67
78
|
yield {
|
|
@@ -146,4 +157,15 @@ async function checkIfContextExists(service, context) {
|
|
|
146
157
|
return false;
|
|
147
158
|
}
|
|
148
159
|
exports.checkIfContextExists = checkIfContextExists;
|
|
160
|
+
// `$PWD` in a mount is the home directory, which reads like a path of the project
|
|
161
|
+
function getPwdMountWarning(mount) {
|
|
162
|
+
const source = mount.mount.split(':')[0];
|
|
163
|
+
if ((0, normalize_path_1.getHomeReference)(source) !== '$PWD') {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
const rest = source.substring('$PWD'.length);
|
|
167
|
+
const home = rest === '' || rest.startsWith('/') || rest.startsWith('\\') ? `~${rest}` : '~/…';
|
|
168
|
+
return (`mount ${source} is ${mount.localPath}: $PWD in a mount is the home directory, not the project. ` +
|
|
169
|
+
`Write ${home} for the home directory, or a relative path for the project`);
|
|
170
|
+
}
|
|
149
171
|
//# sourceMappingURL=validate.js.map
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.applyNetworkCeiling = exports.formatNetwork = exports.getTcpForwards = exports.matchesHost = exports.isLimitedNetwork = exports.toNetworkSchema = exports.exceedsNetwork = exports.getEffectiveNetwork = exports.limitNetwork = exports.parseNetworkOption = exports.parseWorkNetwork = exports.OPEN_NETWORK = void 0;
|
|
4
4
|
const network_schema_1 = require("../schema/network-schema");
|
|
5
|
+
const work_task_1 = require("./work-task");
|
|
5
6
|
exports.OPEN_NETWORK = { type: 'open' };
|
|
6
7
|
function parseWorkNetwork(schema) {
|
|
7
8
|
if (!schema || schema === 'open') {
|
|
@@ -153,7 +154,8 @@ exports.formatNetwork = formatNetwork;
|
|
|
153
154
|
function applyNetworkCeiling(workTree, ceiling) {
|
|
154
155
|
const limit = ceiling && isLimitedNetwork(ceiling) ? ceiling : null;
|
|
155
156
|
if (limit) {
|
|
156
|
-
|
|
157
|
+
// an aggregate runs nothing, so there is nothing of it to limit
|
|
158
|
+
const local = Object.values(workTree.tasks).find((task) => task.data.type === 'local-task' && !(0, work_task_1.isAggregateTask)(task.data));
|
|
157
159
|
if (local) {
|
|
158
160
|
throw new Error(`--network ${formatNetwork(limit)} needs every task in a container, ` +
|
|
159
161
|
`hammerkit can't limit the network of the local task ${local.name}`);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.aggregateWorkRuntime = void 0;
|
|
4
|
+
// An aggregate runs nothing, so it touches nothing: no pid or state file in its
|
|
5
|
+
// directory (on a server that is a checkout it doesn't trust), no outputs.
|
|
6
|
+
function aggregateWorkRuntime() {
|
|
7
|
+
return {
|
|
8
|
+
async initialize() {
|
|
9
|
+
// nothing to prepare
|
|
10
|
+
},
|
|
11
|
+
async remove() {
|
|
12
|
+
// it has no outputs of its own
|
|
13
|
+
},
|
|
14
|
+
async execute() {
|
|
15
|
+
// its state stays running, and executeWorkTask completes it
|
|
16
|
+
},
|
|
17
|
+
async stop() {
|
|
18
|
+
// nothing is running
|
|
19
|
+
},
|
|
20
|
+
async archive() {
|
|
21
|
+
// never pushed, see pushToBackend
|
|
22
|
+
},
|
|
23
|
+
async restore() {
|
|
24
|
+
// never a hit, see checkCacheState
|
|
25
|
+
},
|
|
26
|
+
async currentStateKey() {
|
|
27
|
+
return null;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
exports.aggregateWorkRuntime = aggregateWorkRuntime;
|
|
32
|
+
//# sourceMappingURL=work-runtime-aggregate.js.map
|
|
@@ -12,6 +12,7 @@ const event_cache_1 = require("../executer/event-cache");
|
|
|
12
12
|
const get_docker_executor_1 = require("../executer/get-docker-executor");
|
|
13
13
|
const path_1 = require("path");
|
|
14
14
|
const tar_1 = require("tar");
|
|
15
|
+
const tar_2 = require("../utils/tar");
|
|
15
16
|
const plan_work_volume_1 = require("./utils/plan-work-volume");
|
|
16
17
|
const work_instance_id_1 = require("./work-instance-id");
|
|
17
18
|
const stream_1 = require("stream");
|
|
@@ -302,7 +303,7 @@ async function archiveContainer(docker, environment, item, path) {
|
|
|
302
303
|
for (const generatedArchive of (0, event_cache_1.getArchivePaths)(item.data, path)) {
|
|
303
304
|
// a file output is a bind mount of a host file, so archive it from the host
|
|
304
305
|
const readable = hostFiles.has(generatedArchive.path)
|
|
305
|
-
? stream_1.Readable.from((0,
|
|
306
|
+
? stream_1.Readable.from((0, tar_2.packArchive)({ cwd: (0, path_1.dirname)(generatedArchive.path) }, [(0, path_1.basename)(generatedArchive.path)]))
|
|
306
307
|
: await container.getArchive({
|
|
307
308
|
path: generatedArchive.path,
|
|
308
309
|
});
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.kubernetesForwardRuntime = exports.kubernetesServiceRuntime = exports.kubernetesTaskRuntime = exports.describeFailedJob = void 0;
|
|
6
|
+
exports.kubernetesForwardRuntime = exports.kubernetesServiceRuntime = exports.kubernetesTaskRuntime = exports.getTaskJobContainers = exports.describeFailedJob = void 0;
|
|
7
7
|
const abort_1 = require("../executer/abort");
|
|
8
8
|
const kubernetes_service_1 = require("../executer/kubernetes-service");
|
|
9
9
|
const kubernetes_instance_1 = require("../kubernetes/kubernetes-instance");
|
|
@@ -34,11 +34,12 @@ const job_placement_1 = require("../kubernetes/job-placement");
|
|
|
34
34
|
const network_policy_1 = require("../kubernetes/network-policy");
|
|
35
35
|
const run_id_1 = require("../utils/run-id");
|
|
36
36
|
const work_network_1 = require("./work-network");
|
|
37
|
+
const sandbox_runner_1 = require("../kubernetes/sandbox-runner");
|
|
37
38
|
const work_secret_1 = require("./work-secret");
|
|
38
39
|
async function listJobNames(instance, kubernetes, task) {
|
|
39
40
|
const jobs = await instance.batchApi.listNamespacedJob({
|
|
40
41
|
namespace: kubernetes.namespace,
|
|
41
|
-
labelSelector:
|
|
42
|
+
labelSelector: (0, resources_1.getItemSelector)(task, kubernetes),
|
|
42
43
|
});
|
|
43
44
|
return jobs.items.flatMap((job) => (job.metadata?.name ? [job.metadata.name] : []));
|
|
44
45
|
}
|
|
@@ -125,6 +126,41 @@ async function describeFailedJob(instance, kubernetes, task, job, container, sec
|
|
|
125
126
|
return undefined;
|
|
126
127
|
}
|
|
127
128
|
exports.describeFailedJob = describeFailedJob;
|
|
129
|
+
/**
|
|
130
|
+
* The containers of the Job of a command: the network gate first, nothing runs before the
|
|
131
|
+
* item's policy holds, then the init containers that read its secrets, then the command. The
|
|
132
|
+
* proxy's token (`network.secretEnv`) reaches the init containers through `secretSpec` only:
|
|
133
|
+
* the command's container gets the proxy variables of a limited network without it.
|
|
134
|
+
*/
|
|
135
|
+
function getTaskJobContainers(options) {
|
|
136
|
+
const { task, cmd, envs, secrets, network, secretSpec, kubernetes } = options;
|
|
137
|
+
// the arguments after `--` keep their `$` as text, Kubernetes expands `$(NAME)`
|
|
138
|
+
const command = secretSpec.command(task.data.shell, (0, task_arguments_1.getKubernetesCommandText)(cmd));
|
|
139
|
+
const initContainers = [...network.initContainers, ...secretSpec.initContainers];
|
|
140
|
+
return {
|
|
141
|
+
...(initContainers.length > 0 ? { initContainers } : {}),
|
|
142
|
+
containers: [
|
|
143
|
+
{
|
|
144
|
+
image: task.data.image,
|
|
145
|
+
// through the task's shell, as on Docker
|
|
146
|
+
command: command.command,
|
|
147
|
+
args: command.args,
|
|
148
|
+
workingDir: cmd.cwd,
|
|
149
|
+
env: [
|
|
150
|
+
...Object.entries(envs).map(([key, value]) => ({ name: key, value })),
|
|
151
|
+
...secrets.env,
|
|
152
|
+
...network.env,
|
|
153
|
+
...command.env,
|
|
154
|
+
],
|
|
155
|
+
name: options.container,
|
|
156
|
+
volumeMounts: [...options.mounts, ...secrets.mounts, ...secretSpec.mounts],
|
|
157
|
+
...(0, container_resources_1.getKubernetesResources)(task.data.resources),
|
|
158
|
+
...(0, job_placement_1.getContainerSecurity)(kubernetes),
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
exports.getTaskJobContainers = getTaskJobContainers;
|
|
128
164
|
function kubernetesTaskRuntime(task, kubernetes) {
|
|
129
165
|
const instance = (0, kubernetes_instance_1.createKubernetesInstances)(kubernetes);
|
|
130
166
|
// on a server that keeps them, the task's claim stays after the run with the state of its
|
|
@@ -153,6 +189,28 @@ function kubernetesTaskRuntime(task, kubernetes) {
|
|
|
153
189
|
const envs = (0, replace_env_variables_1.getEnvironmentVariables)(task.data.envs);
|
|
154
190
|
const persistence = await (0, volumes_1.getKubernetesPersistence)(task, kubernetes);
|
|
155
191
|
const workspace = (0, volumes_1.getWorkspacePodSpec)(kubernetes, persistence, { name: task.name, pool: task.data.pool });
|
|
192
|
+
// a shell of a sandbox: its commands go into the sandbox's runner, when it has one now
|
|
193
|
+
if ((0, sandbox_runner_1.canUseRunner)(task, kubernetes, persistence)) {
|
|
194
|
+
try {
|
|
195
|
+
const result = await (0, sandbox_runner_1.executeInRunner)(instance, task, kubernetes, persistence, options.abort);
|
|
196
|
+
if (result.type === 'crash') {
|
|
197
|
+
options.state.set({ stateKey: options.stateKey, type: 'crash', exitCode: result.exitCode });
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
// stopped while the runner started: no Job for it either
|
|
201
|
+
if (result.type === 'canceled' || options.abort.aborted) {
|
|
202
|
+
options.state.set({ stateKey: options.stateKey, type: 'canceled' });
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (result.type === 'done') {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch (e) {
|
|
210
|
+
options.state.set({ stateKey: options.stateKey, type: 'error', errorMessage: (0, log_1.getErrorMessage)(e) });
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
156
214
|
await (0, ensure_namespace_1.ensureNamespace)(instance, kubernetes.namespace);
|
|
157
215
|
try {
|
|
158
216
|
// the task writes to it now: what it holds stops being the outputs of a state, and
|
|
@@ -166,10 +224,11 @@ function kubernetesTaskRuntime(task, kubernetes) {
|
|
|
166
224
|
}
|
|
167
225
|
const server = kubernetes.server;
|
|
168
226
|
const { delivered, inJob } = (0, job_secrets_1.splitJobSecrets)(task.data.secrets, server);
|
|
169
|
-
const secrets = (0, secrets_1.getKubernetesSecretRefs)(task, delivered);
|
|
227
|
+
const secrets = (0, secrets_1.getKubernetesSecretRefs)(task, kubernetes, delivered);
|
|
170
228
|
const admitted = environment.hosted?.accounts;
|
|
229
|
+
let secretHosts;
|
|
171
230
|
try {
|
|
172
|
-
(0, job_secrets_1.
|
|
231
|
+
secretHosts = (0, job_secrets_1.getSecretHosts)(task, inJob, server);
|
|
173
232
|
if (server) {
|
|
174
233
|
(0, job_secrets_1.assertJobAccounts)(inJob, server, admitted);
|
|
175
234
|
}
|
|
@@ -194,7 +253,7 @@ function kubernetesTaskRuntime(task, kubernetes) {
|
|
|
194
253
|
: {}),
|
|
195
254
|
};
|
|
196
255
|
const labels = {
|
|
197
|
-
|
|
256
|
+
...(0, resources_1.getItemLabels)(task, kubernetes),
|
|
198
257
|
'hammerkit.dev/state': options.stateKey,
|
|
199
258
|
...workspace.labels,
|
|
200
259
|
[network_policy_1.RUN_LABEL]: scope.run,
|
|
@@ -210,7 +269,11 @@ function kubernetesTaskRuntime(task, kubernetes) {
|
|
|
210
269
|
// NetworkPolicy and proxy of a limited network, both of this run
|
|
211
270
|
let network;
|
|
212
271
|
try {
|
|
213
|
-
network = await (0, network_policy_1.ensureKubernetesItemNetwork)(instance, kubernetes, task, {
|
|
272
|
+
network = await (0, network_policy_1.ensureKubernetesItemNetwork)(instance, kubernetes, task, {
|
|
273
|
+
followLog: true,
|
|
274
|
+
secretHosts,
|
|
275
|
+
...scope,
|
|
276
|
+
});
|
|
214
277
|
}
|
|
215
278
|
catch (e) {
|
|
216
279
|
await removeTaskSecrets(instance, kubernetes, task).catch(() => undefined);
|
|
@@ -218,7 +281,7 @@ function kubernetesTaskRuntime(task, kubernetes) {
|
|
|
218
281
|
return;
|
|
219
282
|
}
|
|
220
283
|
try {
|
|
221
|
-
const podName = (0, resources_1.getResourceName)(task, '-job');
|
|
284
|
+
const podName = (0, resources_1.getResourceName)(task, kubernetes, '-job');
|
|
222
285
|
let i = 0;
|
|
223
286
|
for (const cmd of task.data.cmds) {
|
|
224
287
|
const container = `cmd-${++i}`;
|
|
@@ -226,15 +289,13 @@ function kubernetesTaskRuntime(task, kubernetes) {
|
|
|
226
289
|
try {
|
|
227
290
|
// made for each Job right before it, so a later command's token is as fresh
|
|
228
291
|
const credentials = await (0, job_secrets_1.ensureJobCredentials)(instance, kubernetes, task, inJob, admitted);
|
|
229
|
-
secretSpec = (0, job_secrets_1.getJobSecretSpec)(task, inJob, server, credentials, environment);
|
|
292
|
+
secretSpec = (0, job_secrets_1.getJobSecretSpec)(task, inJob, server, credentials, environment, network.secretEnv);
|
|
230
293
|
}
|
|
231
294
|
catch (e) {
|
|
232
295
|
await removeTaskSecrets(instance, kubernetes, task).catch(() => undefined);
|
|
233
296
|
options.state.set({ stateKey: options.stateKey, type: 'error', errorMessage: (0, log_1.getErrorMessage)(e) });
|
|
234
297
|
return;
|
|
235
298
|
}
|
|
236
|
-
// the arguments after `--` keep their `$` as text, Kubernetes expands `$(NAME)`
|
|
237
|
-
const command = secretSpec.command(task.data.shell, (0, task_arguments_1.getKubernetesCommandText)(cmd));
|
|
238
299
|
const spec = {
|
|
239
300
|
kind: 'Job',
|
|
240
301
|
apiVersion: 'batch/v1',
|
|
@@ -255,33 +316,17 @@ function kubernetesTaskRuntime(task, kubernetes) {
|
|
|
255
316
|
labels,
|
|
256
317
|
},
|
|
257
318
|
spec: {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
env: [
|
|
270
|
-
...Object.entries(envs).map(([key, value]) => ({ name: key, value })),
|
|
271
|
-
...secrets.env,
|
|
272
|
-
...network.env,
|
|
273
|
-
...command.env,
|
|
274
|
-
],
|
|
275
|
-
name: container,
|
|
276
|
-
volumeMounts: [
|
|
277
|
-
...persistence.mounts.map((m) => m.mount),
|
|
278
|
-
...secrets.mounts,
|
|
279
|
-
...secretSpec.mounts,
|
|
280
|
-
],
|
|
281
|
-
...(0, container_resources_1.getKubernetesResources)(task.data.resources),
|
|
282
|
-
...(0, job_placement_1.getContainerSecurity)(kubernetes),
|
|
283
|
-
},
|
|
284
|
-
],
|
|
319
|
+
...getTaskJobContainers({
|
|
320
|
+
task,
|
|
321
|
+
cmd,
|
|
322
|
+
container,
|
|
323
|
+
envs,
|
|
324
|
+
secrets,
|
|
325
|
+
network,
|
|
326
|
+
secretSpec,
|
|
327
|
+
mounts: persistence.mounts.map((m) => m.mount),
|
|
328
|
+
kubernetes,
|
|
329
|
+
}),
|
|
285
330
|
volumes: [
|
|
286
331
|
...persistence.volumes,
|
|
287
332
|
...persistence.workspace,
|
|
@@ -365,7 +410,7 @@ function kubernetesTaskRuntime(task, kubernetes) {
|
|
|
365
410
|
async stop() {
|
|
366
411
|
const jobs = await instance.batchApi.listNamespacedJob({
|
|
367
412
|
namespace: kubernetes.namespace,
|
|
368
|
-
labelSelector:
|
|
413
|
+
labelSelector: (0, resources_1.getItemSelector)(task, kubernetes),
|
|
369
414
|
});
|
|
370
415
|
for (const job of jobs.items) {
|
|
371
416
|
if (job.metadata?.name) {
|
|
@@ -374,7 +419,7 @@ function kubernetesTaskRuntime(task, kubernetes) {
|
|
|
374
419
|
}
|
|
375
420
|
const deployments = await instance.appsApi.listNamespacedDeployment({
|
|
376
421
|
namespace: kubernetes.namespace,
|
|
377
|
-
labelSelector:
|
|
422
|
+
labelSelector: (0, resources_1.getItemSelector)(task, kubernetes),
|
|
378
423
|
});
|
|
379
424
|
for (const deploy of deployments.items) {
|
|
380
425
|
if (deploy.metadata?.name) {
|
|
@@ -386,7 +431,7 @@ function kubernetesTaskRuntime(task, kubernetes) {
|
|
|
386
431
|
}
|
|
387
432
|
const pods = await instance.coreApi.listNamespacedPod({
|
|
388
433
|
namespace: kubernetes.namespace,
|
|
389
|
-
labelSelector:
|
|
434
|
+
labelSelector: (0, resources_1.getItemSelector)(task, kubernetes),
|
|
390
435
|
});
|
|
391
436
|
for (const pod of pods.items) {
|
|
392
437
|
if (pod.metadata?.name) {
|
|
@@ -408,7 +453,7 @@ function kubernetesTaskRuntime(task, kubernetes) {
|
|
|
408
453
|
}
|
|
409
454
|
const jobs = await instance.batchApi.listNamespacedJob({
|
|
410
455
|
namespace: kubernetes.namespace,
|
|
411
|
-
labelSelector:
|
|
456
|
+
labelSelector: (0, resources_1.getItemSelector)(task, kubernetes),
|
|
412
457
|
});
|
|
413
458
|
const completedStates = jobs.items
|
|
414
459
|
.filter((j) => j.status?.succeeded && j.metadata?.labels?.['hammerkit.dev/state'])
|
|
@@ -445,7 +490,7 @@ function kubernetesServiceRuntime(service, kubernetes) {
|
|
|
445
490
|
async initialize(state) {
|
|
446
491
|
const deployments = await instance.appsApi.listNamespacedDeployment({
|
|
447
492
|
namespace: kubernetes.namespace,
|
|
448
|
-
labelSelector:
|
|
493
|
+
labelSelector: (0, resources_1.getItemSelector)(service, kubernetes),
|
|
449
494
|
});
|
|
450
495
|
const deployment = deployments.items[0];
|
|
451
496
|
if (!deployment?.metadata?.name) {
|
|
@@ -472,7 +517,7 @@ function kubernetesServiceRuntime(service, kubernetes) {
|
|
|
472
517
|
async stop() {
|
|
473
518
|
const deployments = await instance.appsApi.listNamespacedDeployment({
|
|
474
519
|
namespace: kubernetes.namespace,
|
|
475
|
-
labelSelector:
|
|
520
|
+
labelSelector: (0, resources_1.getItemSelector)(service, kubernetes),
|
|
476
521
|
});
|
|
477
522
|
for (const deployment of deployments.items) {
|
|
478
523
|
if (deployment.metadata?.name) {
|
|
@@ -492,9 +537,13 @@ function kubernetesServiceRuntime(service, kubernetes) {
|
|
|
492
537
|
await (0, ensure_kubernetes_service_exists_1.ensureKubernetesServiceExists)(instance, kubernetes, service);
|
|
493
538
|
await (0, ensure_persistent_data_1.ensurePersistentData)(instance, kubernetes, environment, service, persistence);
|
|
494
539
|
await (0, secrets_1.ensureKubernetesSecret)(instance, kubernetes, service, service.data.secrets, environment);
|
|
495
|
-
|
|
496
|
-
await (0,
|
|
497
|
-
|
|
540
|
+
// on a server, the run's: its runs share the namespace, and the service goes with the run
|
|
541
|
+
const network = await (0, network_policy_1.ensureKubernetesItemNetwork)(instance, kubernetes, service, {
|
|
542
|
+
followLog: false,
|
|
543
|
+
run: kubernetes.server ? (0, job_placement_1.getRunLabelValue)(kubernetes.server.run) : null,
|
|
544
|
+
});
|
|
545
|
+
await (0, ensure_kubernetes_deployment_exists_1.ensureKubernetesDeploymentExists)(instance, kubernetes, service, persistence, (0, secrets_1.getKubernetesSecretRefs)(service, kubernetes, service.data.secrets), options.stateKey, network);
|
|
546
|
+
const name = (0, resources_1.getResourceName)(service, kubernetes);
|
|
498
547
|
options.state.set({
|
|
499
548
|
type: 'running',
|
|
500
549
|
dns: { containerId: name },
|
|
@@ -526,7 +575,7 @@ function kubernetesServiceRuntime(service, kubernetes) {
|
|
|
526
575
|
}
|
|
527
576
|
const services = await instance.coreApi.listNamespacedService({
|
|
528
577
|
namespace: kubernetes.namespace,
|
|
529
|
-
labelSelector:
|
|
578
|
+
labelSelector: (0, resources_1.getItemSelector)(service, kubernetes),
|
|
530
579
|
});
|
|
531
580
|
for (const service of services.items) {
|
|
532
581
|
if (service.metadata?.name) {
|
|
@@ -542,7 +591,7 @@ function kubernetesServiceRuntime(service, kubernetes) {
|
|
|
542
591
|
async currentStateKey() {
|
|
543
592
|
const deployments = await instance.appsApi.listNamespacedDeployment({
|
|
544
593
|
namespace: kubernetes.namespace,
|
|
545
|
-
labelSelector:
|
|
594
|
+
labelSelector: (0, resources_1.getItemSelector)(service, kubernetes),
|
|
546
595
|
});
|
|
547
596
|
const deployment = deployments.items[0];
|
|
548
597
|
return deployment?.metadata?.labels?.['hammerkit.dev/state'] ?? null;
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.getLocalWorkRuntime = exports.getStateFilename = void 0;
|
|
7
7
|
const tar_1 = require("tar");
|
|
8
|
+
const tar_2 = require("../utils/tar");
|
|
8
9
|
const path_1 = require("path");
|
|
9
10
|
const local_task_1 = require("../executer/local-task");
|
|
10
11
|
const event_cache_1 = require("../executer/event-cache");
|
|
@@ -121,7 +122,7 @@ async function archiveLocal(environment, task, path) {
|
|
|
121
122
|
for (const generatedArchive of (0, event_cache_1.getArchivePaths)(task.data, path)) {
|
|
122
123
|
await environment.file.writeStream(generatedArchive.filename,
|
|
123
124
|
// tar's stream is a Minipass stream; Readable.from adapts it to a Node one
|
|
124
|
-
stream_1.Readable.from((0,
|
|
125
|
+
stream_1.Readable.from((0, tar_2.packArchive)({ cwd: task.data.cwd, gzip: true }, [(0, path_1.relative)(task.data.cwd, generatedArchive.path)])));
|
|
125
126
|
}
|
|
126
127
|
}
|
|
127
128
|
async function restoreLocal(environment, task, path) {
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.isWorkTask = exports.isContainerWorkTask = void 0;
|
|
3
|
+
exports.isAggregateTask = exports.isWorkTask = exports.isContainerWorkTask = void 0;
|
|
4
4
|
const isContainerWorkTask = (val) => val.type === 'container-task';
|
|
5
5
|
exports.isContainerWorkTask = isContainerWorkTask;
|
|
6
6
|
const isWorkTask = (val) => val.type === 'container-task' || val.type === 'local-task';
|
|
7
7
|
exports.isWorkTask = isWorkTask;
|
|
8
|
+
// A task that only groups its dependencies: `deps`, and no `image`, `cmds`, `src`,
|
|
9
|
+
// `generates`, `secrets`, `needs` or `continuous`. It needs no runtime, hands on what its dependencies generate to what
|
|
10
|
+
// depends on it, and is up to date (or verified) when all its dependencies are.
|
|
11
|
+
const isAggregateTask = (val) => val.type === 'local-task' && val.aggregate === true;
|
|
12
|
+
exports.isAggregateTask = isAggregateTask;
|
|
8
13
|
//# sourceMappingURL=work-task.js.map
|
package/dist/program.js
CHANGED
|
@@ -84,7 +84,8 @@ function printTaskDefinition(environment, definition) {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
// `ls --json`: what a caller (an agent picking a task) needs to know, with the
|
|
87
|
-
// command that arguments after `--` are appended to (null: it takes none)
|
|
87
|
+
// command that arguments after `--` are appended to (null: it takes none) and
|
|
88
|
+
// `args` as the build file says it (false: it refuses them).
|
|
88
89
|
function getListJson(tasks, services) {
|
|
89
90
|
return {
|
|
90
91
|
tasks: tasks.map(({ item }) => ({
|
|
@@ -96,6 +97,7 @@ function getListJson(tasks, services) {
|
|
|
96
97
|
needs: item.needs.map((n) => n.name),
|
|
97
98
|
labels: item.data.labels,
|
|
98
99
|
command: (0, task_arguments_1.getArgumentsTarget)(item.data)?.cmd ?? null,
|
|
100
|
+
args: item.data.args,
|
|
99
101
|
})),
|
|
100
102
|
services: services.map(({ item }) => ({
|
|
101
103
|
name: item.name,
|
|
@@ -105,6 +107,7 @@ function getListJson(tasks, services) {
|
|
|
105
107
|
ports: item.data.ports.map((p) => ({ host: p.hostPort, container: p.containerPort })),
|
|
106
108
|
labels: item.data.labels,
|
|
107
109
|
command: (0, task_arguments_1.getArgumentsTarget)(item.data)?.cmd ?? null,
|
|
110
|
+
args: item.data.type === 'container-service' ? item.data.args : null,
|
|
108
111
|
})),
|
|
109
112
|
};
|
|
110
113
|
}
|
|
@@ -159,11 +162,14 @@ async function getProgram(environment, argv) {
|
|
|
159
162
|
if (fileIndex >= 0) {
|
|
160
163
|
args.splice(fileIndex, 2);
|
|
161
164
|
}
|
|
162
|
-
(0, remote_commands_1.registerRemoteCommands)(program, environment, fileName);
|
|
165
|
+
(0, remote_commands_1.registerRemoteCommands)(program, environment, fileName, passthrough);
|
|
163
166
|
program.hook('preAction', (_, actionCommand) => {
|
|
164
|
-
const takesArguments = actionCommand.parent === program && ['run', 'up'].includes(actionCommand.name())
|
|
167
|
+
const takesArguments = (actionCommand.parent === program && ['run', 'up'].includes(actionCommand.name())) ||
|
|
168
|
+
(actionCommand.parent?.name() === 'remote' && actionCommand.name() === 'run');
|
|
165
169
|
if (passthrough.length > 0 && !takesArguments) {
|
|
166
|
-
program.error(`${actionCommand.name()} takes no arguments after --, only run and
|
|
170
|
+
program.error(`${actionCommand.name()} takes no arguments after --, only run, up and remote run do`, {
|
|
171
|
+
exitCode: 1,
|
|
172
|
+
});
|
|
167
173
|
}
|
|
168
174
|
});
|
|
169
175
|
if (await environment.file.exists(fileName)) {
|
|
@@ -602,6 +608,7 @@ async function getProgram(environment, argv) {
|
|
|
602
608
|
.choices(['checksum', 'modify-date', 'none']))
|
|
603
609
|
.addOption(new commander_1.Option('--cache-read-only', `restore from cache backends but never push to them (or set ${read_only_1.CACHE_READ_ONLY_ENV}=1)`).default(false))
|
|
604
610
|
.addOption(new commander_1.Option('--timeout <duration>', 'fail tasks without their own timeout after this long (e.g. 10m)').argParser(units_1.parseDuration))
|
|
611
|
+
.addOption(new commander_1.Option('--max-timeout <duration>', 'fail any task after this long, also one with a longer timeout of its own (e.g. 2m)').argParser(units_1.parseDuration))
|
|
605
612
|
.addOption(new commander_1.Option('--network <level>', 'the most any task or service reaches besides its needs: none, open or allow:<host>,<host>').argParser(parseNetworkArgument))
|
|
606
613
|
.action(async (options, command) => {
|
|
607
614
|
try {
|
|
@@ -620,6 +627,7 @@ async function getProgram(environment, argv) {
|
|
|
620
627
|
wait: options.wait,
|
|
621
628
|
cacheReadOnly: (0, read_only_1.isCacheReadOnly)(options.cacheReadOnly, environment.processEnvs),
|
|
622
629
|
timeout: options.timeout ?? null,
|
|
630
|
+
maxTimeout: options.maxTimeout ?? null,
|
|
623
631
|
network: options.network ?? null,
|
|
624
632
|
});
|
|
625
633
|
if (!result.success) {
|
|
@@ -682,6 +690,7 @@ async function getProgram(environment, argv) {
|
|
|
682
690
|
.addOption(new commander_1.Option('--dry-run', 'print the execution plan with predicted cache hits/misses without running').default(false))
|
|
683
691
|
.addOption(new commander_1.Option('--cache-read-only', `restore from cache backends but never push to them (or set ${read_only_1.CACHE_READ_ONLY_ENV}=1)`).default(false))
|
|
684
692
|
.addOption(new commander_1.Option('--timeout <duration>', 'fail tasks without their own timeout after this long (e.g. 10m)').argParser(units_1.parseDuration))
|
|
693
|
+
.addOption(new commander_1.Option('--max-timeout <duration>', 'fail any task after this long, also one with a longer timeout of its own (e.g. 2m)').argParser(units_1.parseDuration))
|
|
685
694
|
.addOption(new commander_1.Option('--network <level>', 'the most any task or service reaches besides its needs: none, open or allow:<host>,<host>').argParser(parseNetworkArgument))
|
|
686
695
|
.addOption(new commander_1.Option('--no-skip-deps', 'run dependencies even when nothing needing them has to run'))
|
|
687
696
|
.action(async (task, options, command) => {
|
|
@@ -716,6 +725,7 @@ async function getProgram(environment, argv) {
|
|
|
716
725
|
explain: options.explain,
|
|
717
726
|
cacheReadOnly: (0, read_only_1.isCacheReadOnly)(options.cacheReadOnly, environment.processEnvs),
|
|
718
727
|
timeout: options.timeout ?? null,
|
|
728
|
+
maxTimeout: options.maxTimeout ?? null,
|
|
719
729
|
network: options.network ?? null,
|
|
720
730
|
skipDeps: options.skipDeps !== false,
|
|
721
731
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.applyAnnotations = exports.collectAnnotations = exports.remoteRepo = void 0;
|
|
3
|
+
exports.applyAnnotations = exports.collectAnnotations = exports.readCheckout = exports.remoteRepo = void 0;
|
|
4
4
|
const child_process_1 = require("child_process");
|
|
5
5
|
const os_1 = require("os");
|
|
6
6
|
const path_1 = require("path");
|
|
@@ -36,6 +36,18 @@ function remoteRepo(url) {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
exports.remoteRepo = remoteRepo;
|
|
39
|
+
// The checkout a directory is in: the repository of its `origin`, the branch it is on and
|
|
40
|
+
// its commit, what can be found of them.
|
|
41
|
+
async function readCheckout(cwd) {
|
|
42
|
+
if ((await git(cwd, 'rev-parse', '--is-inside-work-tree')) !== 'true') {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
const remote = await git(cwd, 'remote', 'get-url', 'origin');
|
|
46
|
+
// a detached HEAD is on no branch
|
|
47
|
+
const branch = await git(cwd, 'symbolic-ref', '--quiet', '--short', 'HEAD');
|
|
48
|
+
return { repo: remote ? remoteRepo(remote) : undefined, branch, commit: await git(cwd, 'rev-parse', 'HEAD') };
|
|
49
|
+
}
|
|
50
|
+
exports.readCheckout = readCheckout;
|
|
39
51
|
// Where a build file is published from: its path in its repository (or on this
|
|
40
52
|
// machine, outside of one), the repository and commit, this machine and its user.
|
|
41
53
|
// `files` are the paths of the bundle, relative to the build file's directory: when
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.deviceLogin = void 0;
|
|
3
|
+
exports.deviceLogin = exports.NoSignInError = void 0;
|
|
4
4
|
const server_client_1 = require("./server-client");
|
|
5
5
|
const GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';
|
|
6
6
|
const DEFAULT_INTERVAL_SECONDS = 5;
|
|
@@ -8,6 +8,11 @@ const SLOW_DOWN_SECONDS = 5;
|
|
|
8
8
|
const FETCH_TIMEOUT_MS = 10000;
|
|
9
9
|
const MIN_INTERVAL_SECONDS = 1;
|
|
10
10
|
const MAX_LIFETIME_SECONDS = 30 * 60;
|
|
11
|
+
// The server takes tokens of this issuer but offers no device sign-in for it: the token has
|
|
12
|
+
// to come from elsewhere, such as a token command.
|
|
13
|
+
class NoSignInError extends server_client_1.ServerError {
|
|
14
|
+
}
|
|
15
|
+
exports.NoSignInError = NoSignInError;
|
|
11
16
|
const isLoopbackHost = (hostname) => ['localhost', '127.0.0.1', '[::1]'].includes(hostname);
|
|
12
17
|
// An endpoint must use the scheme of the issuer: loopback http is for a test
|
|
13
18
|
// identity provider on this machine only, never beside an https issuer.
|
|
@@ -89,7 +94,7 @@ async function deviceLogin(server, options) {
|
|
|
89
94
|
throw new server_client_1.ServerError(`${server.url} does not accept tokens from ${server.issuer} (it accepts ${accepted})`);
|
|
90
95
|
}
|
|
91
96
|
if (!offered.clientId) {
|
|
92
|
-
throw new
|
|
97
|
+
throw new NoSignInError(`${server.url} offers no sign-in for ${server.issuer}, it needs a token obtained elsewhere`);
|
|
93
98
|
}
|
|
94
99
|
// What was pinned (by `remote add` or an earlier login) wins; the server can
|
|
95
100
|
// only offer the same, never something else to sign in with.
|