hammerkit 1.27.0-next.9 → 2.0.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 +176 -6
- package/dist/cache/backends/local-cache-backend.js +6 -0
- package/dist/cache/backends/registry/registry-client.js +1 -0
- package/dist/cache/cache-sync.js +1 -1
- package/dist/cache/explain.js +17 -0
- package/dist/cli.js +2 -0
- package/dist/dashboard/assets/index-CjnKm5-3.css +2 -0
- package/dist/dashboard/assets/index-zPXa4klZ.js +13 -0
- package/dist/dashboard/favicon.svg +1 -0
- package/dist/dashboard/index.html +15 -0
- package/dist/docker/item-network.js +13 -5
- package/dist/executer/archive-task-entry.js +4 -2
- package/dist/executer/deadline.js +17 -3
- package/dist/executer/deferred-restore.js +36 -0
- package/dist/executer/docker-service.js +30 -8
- package/dist/executer/docker-task.js +21 -1
- package/dist/executer/execute-work-service.js +2 -0
- package/dist/executer/execute-work-task.js +11 -4
- package/dist/executer/execution-steps.js +25 -4
- package/dist/executer/reset-work-tree.js +4 -0
- package/dist/executer/scheduler/enqueue-next.js +57 -6
- package/dist/executer/scheduler/state-key.js +42 -5
- package/dist/executer/watch-loop.js +6 -3
- 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 +194 -23
- package/dist/harness/service-tools.js +141 -0
- package/dist/harness/task-tools.js +221 -0
- package/dist/kubernetes/apply.js +49 -1
- package/dist/kubernetes/await-running-state.js +172 -13
- package/dist/kubernetes/cluster-state.js +245 -0
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +133 -9
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +2 -0
- package/dist/kubernetes/ensure-persistent-data.js +211 -6
- package/dist/kubernetes/follow-pod-logs.js +182 -0
- package/dist/kubernetes/job-placement.js +36 -12
- package/dist/kubernetes/kubernetes-instance.js +1 -0
- package/dist/kubernetes/network-policy.js +2 -1
- package/dist/kubernetes/remove-stale-items.js +46 -0
- package/dist/kubernetes/restore-kubernetes-data.js +9 -5
- package/dist/kubernetes/sandbox-runner.js +628 -0
- package/dist/kubernetes/service-rollouts.js +74 -0
- package/dist/kubernetes/store-kubernetes-data.js +5 -2
- package/dist/kubernetes/volumes.js +94 -9
- package/dist/main.js +2 -0
- 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 +49 -1
- package/dist/planner/service-definition.js +9 -4
- package/dist/planner/utils/append-work-dependencies.js +5 -3
- package/dist/planner/utils/append-work-service.js +18 -1
- package/dist/planner/utils/append-work-task.js +13 -3
- 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/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-kubernetes.js +136 -13
- package/dist/planner/work-task.js +6 -1
- package/dist/program.js +27 -9
- package/dist/remote/device-login.js +7 -2
- package/dist/remote/host-config.js +69 -3
- package/dist/remote/remote-commands.js +33 -7
- package/dist/remote/remote-status.js +112 -0
- package/dist/remote/remote-work-commands.js +562 -45
- 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 +3 -3
- package/dist/schema/build-file-container-service-schema.js +10 -0
- package/dist/schema/build-file-container-task-schema.js +16 -1
- package/dist/schema/build-file-include-schema.js +18 -4
- package/dist/schema/build-file-kubernetes-service-schema.js +2 -0
- package/dist/schema/build-file-task-schema.js +5 -1
- package/dist/schema/capabilities-schema.js +56 -0
- package/dist/schema/reference-parser.js +119 -11
- package/dist/schema/removed-properties.js +20 -0
- package/dist/schema/schema-parser.js +94 -41
- package/dist/schema/work-scope-parser.js +33 -10
- package/dist/secrets/command-secret-provider.js +3 -1
- package/dist/secrets/github-app-token-command.js +3 -1
- package/dist/secrets/secret-catalog.js +43 -19
- package/dist/server/accounts-api.js +4 -1
- package/dist/server/approvals-api.js +1 -1
- package/dist/server/capacity-format.js +36 -0
- package/dist/server/capacity.js +300 -0
- package/dist/server/checks-api.js +83 -0
- package/dist/server/checks.js +116 -0
- package/dist/server/dashboard.js +196 -0
- package/dist/server/definitions-api.js +66 -3
- package/dist/server/endpoints.js +794 -0
- package/dist/server/executor.js +164 -9
- package/dist/server/kubernetes-backend.js +415 -48
- package/dist/server/kubernetes-sandbox-storage.js +46 -2
- package/dist/server/overview.js +271 -0
- package/dist/server/plan.js +170 -121
- package/dist/server/policy.js +34 -2
- package/dist/server/proposals-api.js +17 -8
- package/dist/server/repo-access.js +5 -0
- package/dist/server/run-logs.js +4 -0
- package/dist/server/run-plan.js +38 -4
- package/dist/server/run-request.js +58 -3
- package/dist/server/run-worker.js +262 -13
- package/dist/server/runs.js +962 -36
- package/dist/server/sandbox-pod.js +28 -0
- package/dist/server/sandbox-storage.js +23 -0
- package/dist/server/sandboxes.js +8 -0
- package/dist/server/server-config.js +246 -10
- package/dist/server/server.js +429 -32
- package/dist/server/service-runs.js +82 -0
- package/dist/server/timings.js +132 -0
- package/dist/server/wait-reason.js +21 -0
- package/dist/utils/tar.js +28 -1
- package/dist/utils/terminal-prompt.js +53 -0
- package/package.json +8 -2
|
@@ -118,6 +118,7 @@ function parseService(workTree, service, environment, context) {
|
|
|
118
118
|
secrets: (0, work_secret_1.parseWorkSecrets)(service.cwd, context.projectRoot, service.schema.secrets, envs, environment, context.secrets, service.schema.account),
|
|
119
119
|
resources: (0, work_resources_1.parseWorkResources)(service.schema.resources),
|
|
120
120
|
volumeSize: service.schema.volumeSize ?? null,
|
|
121
|
+
restarts: service.schema.restarts ?? null,
|
|
121
122
|
network: (0, work_network_1.parseWorkNetwork)(service.schema.network),
|
|
122
123
|
networkCeiling: null,
|
|
123
124
|
args: service.schema.args ?? null,
|
|
@@ -143,7 +144,11 @@ function appendServiceInit(workTree, service, item, environment, context) {
|
|
|
143
144
|
task.needs[selfNeed] = { name: task.needs[selfNeed].name, service: view };
|
|
144
145
|
}
|
|
145
146
|
else {
|
|
146
|
-
|
|
147
|
+
const name = getInitNeedName(service);
|
|
148
|
+
if (task.needs.some((need) => need.name === name)) {
|
|
149
|
+
throw new Error(`${task.name} needs two services as ${name}, give one another name with name:`);
|
|
150
|
+
}
|
|
151
|
+
task.needs.push({ name, service: view });
|
|
147
152
|
}
|
|
148
153
|
removeRequirer(item, task.name);
|
|
149
154
|
// explicit, so neither the default method nor `--cache` turns it back on
|
|
@@ -153,6 +158,18 @@ function appendServiceInit(workTree, service, item, environment, context) {
|
|
|
153
158
|
}
|
|
154
159
|
item.data.init = { task, state };
|
|
155
160
|
}
|
|
161
|
+
// The host name the init reaches its service by: the name its task declares the
|
|
162
|
+
// need by, else the service's name in its own build file. Planning the task
|
|
163
|
+
// skips that need (it is the init's), and the full name of a service of an
|
|
164
|
+
// include, `services:db`, is no host name: Docker and DNS take no colon.
|
|
165
|
+
function getInitNeedName(service) {
|
|
166
|
+
const declared = service.init?.task.needs.find((need) => need.service.relativeName === service.relativeName);
|
|
167
|
+
if (declared) {
|
|
168
|
+
return declared.relativeName;
|
|
169
|
+
}
|
|
170
|
+
const prefix = service.scope.namePrefix;
|
|
171
|
+
return prefix.length > 0 ? service.relativeName.slice(prefix.length + 1) : service.relativeName;
|
|
172
|
+
}
|
|
156
173
|
const initViews = new WeakMap();
|
|
157
174
|
// The service itself for the view its init task needs, else the item.
|
|
158
175
|
function getPlannedService(item) {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.appendWorkTask = void 0;
|
|
4
4
|
const work_secret_1 = require("../work-secret");
|
|
5
|
+
const work_task_1 = require("../work-task");
|
|
5
6
|
const parse_work_generate_1 = require("./parse-work-generate");
|
|
6
7
|
const template_value_1 = require("./template-value");
|
|
7
8
|
const parse_work_mounts_1 = require("./parse-work-mounts");
|
|
@@ -25,7 +26,8 @@ function appendWorkTask(workTree, cwd, referenceTask, environment, context) {
|
|
|
25
26
|
const parsed = parseTask(cwd, referenceTask, environment, context);
|
|
26
27
|
// on a server's cluster a local task runs in a tool image like a container task
|
|
27
28
|
const server = workTree.environment.type === 'kubernetes' ? workTree.environment.server : undefined;
|
|
28
|
-
|
|
29
|
+
// an aggregate runs nothing, so it needs no pod either
|
|
30
|
+
const task = !server || (0, work_task_1.isAggregateTask)(parsed)
|
|
29
31
|
? parsed
|
|
30
32
|
: parsed.type === 'local-task'
|
|
31
33
|
? (0, job_placement_1.runInToolImage)(parsed, server, referenceTask.schema.platform)
|
|
@@ -77,7 +79,6 @@ function parseTask(cwd, task, environment, context) {
|
|
|
77
79
|
labels: task.labels,
|
|
78
80
|
caching: (0, resolve_cache_1.resolveCache)(task.schema.cache ?? null, context.caches, task.relativeName),
|
|
79
81
|
shell: task.schema.shell ? (0, template_value_1.templateValue)(task.schema.shell, envs) : '/bin/sh',
|
|
80
|
-
continuous: task.schema.continuous ?? false,
|
|
81
82
|
timeout: task.schema.timeout ? (0, units_1.parseDuration)(task.schema.timeout) : null,
|
|
82
83
|
secrets: (0, work_secret_1.parseWorkSecrets)(cwd, context.projectRoot, task.schema.secrets, envs, environment, context.secrets, task.schema.account),
|
|
83
84
|
resources: (0, work_resources_1.parseWorkResources)(task.schema.resources),
|
|
@@ -90,10 +91,12 @@ function parseTask(cwd, task, environment, context) {
|
|
|
90
91
|
return {
|
|
91
92
|
...baseWorkNode,
|
|
92
93
|
type: 'container-task',
|
|
93
|
-
|
|
94
|
+
// a capability holds for root only: a task that adds some runs as its image's user
|
|
95
|
+
user: task.schema.capAdd?.length ? null : (0, get_container_user_1.getContainerUser)(),
|
|
94
96
|
image: (0, template_value_1.templateValue)(task.schema.image, envs),
|
|
95
97
|
mounts: (0, parse_work_mounts_1.parseWorkMounts)(cwd, task.schema, envs),
|
|
96
98
|
volumeSize: task.schema.volumeSize ?? null,
|
|
99
|
+
capAdd: task.schema.capAdd ?? [],
|
|
97
100
|
};
|
|
98
101
|
}
|
|
99
102
|
else {
|
|
@@ -107,9 +110,16 @@ function parseTask(cwd, task, environment, context) {
|
|
|
107
110
|
throw new Error(`task ${task.relativeName}: network ${(0, work_network_1.formatNetwork)(baseWorkNode.network)} needs a container, ` +
|
|
108
111
|
`hammerkit can't limit the network of a local task`);
|
|
109
112
|
}
|
|
113
|
+
const aggregate = task.deps.length > 0 &&
|
|
114
|
+
task.needs.length === 0 &&
|
|
115
|
+
baseWorkNode.src.length === 0 &&
|
|
116
|
+
baseWorkNode.cmds.length === 0 &&
|
|
117
|
+
baseWorkNode.generates.length === 0 &&
|
|
118
|
+
baseWorkNode.secrets.length === 0;
|
|
110
119
|
return {
|
|
111
120
|
...baseWorkNode,
|
|
112
121
|
type: 'local-task',
|
|
122
|
+
...(aggregate ? { aggregate } : {}),
|
|
113
123
|
};
|
|
114
124
|
}
|
|
115
125
|
}
|
|
@@ -4,6 +4,8 @@ exports.getWorkTaskRuntime = exports.getWorkServiceRuntime = void 0;
|
|
|
4
4
|
const work_runtime_docker_1 = require("../work-runtime-docker");
|
|
5
5
|
const work_runtime_kubernetes_1 = require("../work-runtime-kubernetes");
|
|
6
6
|
const work_runtime_local_1 = require("../work-runtime-local");
|
|
7
|
+
const work_runtime_aggregate_1 = require("../work-runtime-aggregate");
|
|
8
|
+
const work_task_1 = require("../work-task");
|
|
7
9
|
const work_item_1 = require("../work-item");
|
|
8
10
|
function getWorkServiceRuntime(workTree, service) {
|
|
9
11
|
if ((0, work_item_1.isContainerWorkServiceItem)(service)) {
|
|
@@ -24,7 +26,10 @@ function getWorkServiceRuntime(workTree, service) {
|
|
|
24
26
|
}
|
|
25
27
|
exports.getWorkServiceRuntime = getWorkServiceRuntime;
|
|
26
28
|
function getWorkTaskRuntime(workTree, task) {
|
|
27
|
-
if ((0,
|
|
29
|
+
if ((0, work_task_1.isAggregateTask)(task.data)) {
|
|
30
|
+
return (0, work_runtime_aggregate_1.aggregateWorkRuntime)();
|
|
31
|
+
}
|
|
32
|
+
else if ((0, work_item_1.isLocalWorkTaskItem)(task)) {
|
|
28
33
|
return (0, work_runtime_local_1.getLocalWorkRuntime)(task);
|
|
29
34
|
}
|
|
30
35
|
else if ((0, work_item_1.isContainerWorkTaskItem)(task)) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.normalizePath = void 0;
|
|
3
|
+
exports.normalizeHostPath = exports.getHomeReference = exports.normalizePath = void 0;
|
|
4
4
|
const path_1 = require("path");
|
|
5
5
|
function normalizePath(cwd, pwd, path) {
|
|
6
6
|
if (path.startsWith('/')) {
|
|
@@ -14,4 +14,22 @@ function normalizePath(cwd, pwd, path) {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
exports.normalizePath = normalizePath;
|
|
17
|
+
// What a path of the host starts with to name the home directory: `~` and `$HOME`, and
|
|
18
|
+
// `$PWD`, which in a mount has always meant the home directory, not the project it reads like
|
|
19
|
+
function getHomeReference(path) {
|
|
20
|
+
for (const reference of ['~', '$HOME']) {
|
|
21
|
+
if (path === reference || path.startsWith(`${reference}/`) || path.startsWith(`${reference}\\`)) {
|
|
22
|
+
return reference;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return path.startsWith('$PWD') ? '$PWD' : null;
|
|
26
|
+
}
|
|
27
|
+
exports.getHomeReference = getHomeReference;
|
|
28
|
+
// A path of the host in a mount: in the home directory when it starts with one of the
|
|
29
|
+
// references above, else in the directory of the build file unless it is absolute
|
|
30
|
+
function normalizeHostPath(cwd, home, path) {
|
|
31
|
+
const reference = getHomeReference(path);
|
|
32
|
+
return reference ? (0, path_1.join)(home, path.substring(reference.length)) : normalizePath(cwd, home, path);
|
|
33
|
+
}
|
|
34
|
+
exports.normalizeHostPath = normalizeHostPath;
|
|
17
35
|
//# sourceMappingURL=normalize-path.js.map
|
|
@@ -7,6 +7,12 @@ const path_1 = require("path");
|
|
|
7
7
|
function parseWorkMount(cwd, dir) {
|
|
8
8
|
const parts = dir.split(':');
|
|
9
9
|
if (parts.length === 1) {
|
|
10
|
+
const home = (0, normalize_path_1.getHomeReference)(dir);
|
|
11
|
+
if (home === '~' || home === '$HOME') {
|
|
12
|
+
// the container has no such directory: the same path on both sides would be one of the
|
|
13
|
+
// project there (`$PWD` has always been, so it stays)
|
|
14
|
+
throw new Error(`mount ${dir} is in the home directory, name where it goes in the container (${dir}:/path)`);
|
|
15
|
+
}
|
|
10
16
|
return parseLocalMount(cwd, dir, dir, dir, false);
|
|
11
17
|
}
|
|
12
18
|
else if (parts.length === 2) {
|
|
@@ -22,7 +28,7 @@ function parseWorkMount(cwd, dir) {
|
|
|
22
28
|
exports.parseWorkMount = parseWorkMount;
|
|
23
29
|
function parseLocalMount(cwd, localPath, containerPath, mount, readOnly) {
|
|
24
30
|
return {
|
|
25
|
-
localPath: (0, normalize_path_1.
|
|
31
|
+
localPath: (0, normalize_path_1.normalizeHostPath)(cwd, (0, os_1.homedir)(), localPath),
|
|
26
32
|
containerPath: (0, normalize_path_1.normalizePath)(cwd, cwd, containerPath),
|
|
27
33
|
isFile: (0, path_1.extname)(localPath).length > 1,
|
|
28
34
|
mount,
|
|
@@ -8,7 +8,10 @@ function parseWorkPort(port) {
|
|
|
8
8
|
return { containerPort: port, hostPort: port };
|
|
9
9
|
}
|
|
10
10
|
else if (parts.length === 2) {
|
|
11
|
-
|
|
11
|
+
// `0:80` is not published, as an empty host port: 0 (a free port) is for the endpoints
|
|
12
|
+
// of a server only
|
|
13
|
+
const hostPort = parsePort(parts[0], true);
|
|
14
|
+
return { containerPort: parsePort(parts[1], false), hostPort: hostPort === 0 ? null : hostPort };
|
|
12
15
|
}
|
|
13
16
|
else {
|
|
14
17
|
throw new Error(`invalid port ${port}`);
|
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
|
|
@@ -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.getTaskJobContainers = exports.describeFailedJob = void 0;
|
|
6
|
+
exports.kubernetesForwardRuntime = exports.isRolledOut = 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");
|
|
@@ -11,12 +11,14 @@ const replace_env_variables_1 = require("../environment/replace-env-variables");
|
|
|
11
11
|
const apply_1 = require("../kubernetes/apply");
|
|
12
12
|
const ensure_kubernetes_service_exists_1 = require("../kubernetes/ensure-kubernetes-service-exists");
|
|
13
13
|
const ensure_kubernetes_deployment_exists_1 = require("../kubernetes/ensure-kubernetes-deployment-exists");
|
|
14
|
+
const follow_pod_logs_1 = require("../kubernetes/follow-pod-logs");
|
|
14
15
|
const service_definition_1 = require("./service-definition");
|
|
15
16
|
const ensure_namespace_1 = require("../kubernetes/ensure-namespace");
|
|
16
17
|
const ensure_persistent_data_1 = require("../kubernetes/ensure-persistent-data");
|
|
17
18
|
const network_gate_1 = require("../kubernetes/network-gate");
|
|
18
19
|
const await_running_state_1 = require("../kubernetes/await-running-state");
|
|
19
20
|
const volumes_1 = require("../kubernetes/volumes");
|
|
21
|
+
const service_rollouts_1 = require("../kubernetes/service-rollouts");
|
|
20
22
|
const resources_1 = require("../kubernetes/resources");
|
|
21
23
|
const ensure_http_route_1 = require("../kubernetes/ensure-http-route");
|
|
22
24
|
const find_process_1 = __importDefault(require("find-process"));
|
|
@@ -34,6 +36,7 @@ const job_placement_1 = require("../kubernetes/job-placement");
|
|
|
34
36
|
const network_policy_1 = require("../kubernetes/network-policy");
|
|
35
37
|
const run_id_1 = require("../utils/run-id");
|
|
36
38
|
const work_network_1 = require("./work-network");
|
|
39
|
+
const sandbox_runner_1 = require("../kubernetes/sandbox-runner");
|
|
37
40
|
const work_secret_1 = require("./work-secret");
|
|
38
41
|
async function listJobNames(instance, kubernetes, task) {
|
|
39
42
|
const jobs = await instance.batchApi.listNamespacedJob({
|
|
@@ -129,7 +132,8 @@ exports.describeFailedJob = describeFailedJob;
|
|
|
129
132
|
* The containers of the Job of a command: the network gate first, nothing runs before the
|
|
130
133
|
* item's policy holds, then the init containers that read its secrets, then the command. The
|
|
131
134
|
* proxy's token (`network.secretEnv`) reaches the init containers through `secretSpec` only:
|
|
132
|
-
* the command's container gets the proxy variables of a limited network without it.
|
|
135
|
+
* the command's container gets the proxy variables of a limited network without it. A task that
|
|
136
|
+
* adds capabilities gets them in the sandbox's RuntimeClass only.
|
|
133
137
|
*/
|
|
134
138
|
function getTaskJobContainers(options) {
|
|
135
139
|
const { task, cmd, envs, secrets, network, secretSpec, kubernetes } = options;
|
|
@@ -154,9 +158,10 @@ function getTaskJobContainers(options) {
|
|
|
154
158
|
name: options.container,
|
|
155
159
|
volumeMounts: [...options.mounts, ...secrets.mounts, ...secretSpec.mounts],
|
|
156
160
|
...(0, container_resources_1.getKubernetesResources)(task.data.resources),
|
|
157
|
-
...(0, job_placement_1.getContainerSecurity)(kubernetes),
|
|
161
|
+
...(0, job_placement_1.getContainerSecurity)(kubernetes, task.data.capAdd),
|
|
158
162
|
},
|
|
159
163
|
],
|
|
164
|
+
...(0, job_placement_1.getPodRuntimeClass)({ name: task.name, capAdd: task.data.capAdd }, kubernetes),
|
|
160
165
|
};
|
|
161
166
|
}
|
|
162
167
|
exports.getTaskJobContainers = getTaskJobContainers;
|
|
@@ -171,29 +176,67 @@ function kubernetesTaskRuntime(task, kubernetes) {
|
|
|
171
176
|
return Promise.resolve();
|
|
172
177
|
},
|
|
173
178
|
async restore(environment, path, stateKey) {
|
|
179
|
+
// a cache hit of a service run at the state that services of the run mount (a move whose
|
|
180
|
+
// dep did not change): its outputs are there, and stay as they are while pods read them
|
|
181
|
+
if (kubernetes.server?.serviceRun &&
|
|
182
|
+
stateKey &&
|
|
183
|
+
(await (0, ensure_persistent_data_1.getStateDirsInUse)(instance, kubernetes.namespace, claim)).has((0, volumes_1.getStateFilesDir)(stateKey))) {
|
|
184
|
+
task.status.write('info', `${task.name}: services of the run use its outputs of this state, they stay`);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
174
187
|
if (!kept) {
|
|
175
|
-
await (0, restore_kubernetes_data_1.restoreKubernetesData)(task, kubernetes, instance, environment, path);
|
|
188
|
+
await (0, restore_kubernetes_data_1.restoreKubernetesData)(task, kubernetes, instance, environment, path, { stateKey });
|
|
176
189
|
return;
|
|
177
190
|
}
|
|
178
191
|
const reset = await (0, kept_claims_1.forgetClaimState)(instance, kubernetes, claim);
|
|
179
|
-
await (0, restore_kubernetes_data_1.restoreKubernetesData)(task, kubernetes, instance, environment, path, { reset });
|
|
192
|
+
await (0, restore_kubernetes_data_1.restoreKubernetesData)(task, kubernetes, instance, environment, path, { reset, stateKey });
|
|
180
193
|
if (stateKey) {
|
|
181
194
|
await (0, kept_claims_1.recordClaimState)(instance, kubernetes, claim, stateKey);
|
|
182
195
|
}
|
|
183
196
|
},
|
|
184
|
-
async archive(environment, path) {
|
|
185
|
-
await (0, store_kubernetes_data_1.storeKubernetesData)(task, kubernetes, instance, environment, path);
|
|
197
|
+
async archive(environment, path, stateKey) {
|
|
198
|
+
await (0, store_kubernetes_data_1.storeKubernetesData)(task, kubernetes, instance, environment, path, stateKey);
|
|
186
199
|
},
|
|
187
200
|
async execute(environment, options) {
|
|
188
201
|
const envs = (0, replace_env_variables_1.getEnvironmentVariables)(task.data.envs);
|
|
189
|
-
const persistence = await (0, volumes_1.getKubernetesPersistence)(task, kubernetes);
|
|
202
|
+
const persistence = await (0, volumes_1.getKubernetesPersistence)(task, kubernetes, options.stateKey);
|
|
190
203
|
const workspace = (0, volumes_1.getWorkspacePodSpec)(kubernetes, persistence, { name: task.name, pool: task.data.pool });
|
|
204
|
+
// a shell of a sandbox: its commands go into the sandbox's runner, when it has one now
|
|
205
|
+
if ((0, sandbox_runner_1.canUseRunner)(task, kubernetes, persistence)) {
|
|
206
|
+
try {
|
|
207
|
+
const result = await (0, sandbox_runner_1.executeInRunner)(instance, task, kubernetes, persistence, options.abort);
|
|
208
|
+
if (result.type === 'crash') {
|
|
209
|
+
options.state.set({ stateKey: options.stateKey, type: 'crash', exitCode: result.exitCode });
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
// stopped while the runner started: no Job for it either
|
|
213
|
+
if (result.type === 'canceled' || options.abort.aborted) {
|
|
214
|
+
options.state.set({ stateKey: options.stateKey, type: 'canceled' });
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (result.type === 'done') {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch (e) {
|
|
222
|
+
options.state.set({ stateKey: options.stateKey, type: 'error', errorMessage: (0, log_1.getErrorMessage)(e) });
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
191
226
|
await (0, ensure_namespace_1.ensureNamespace)(instance, kubernetes.namespace);
|
|
227
|
+
// a service run writes the outputs of each state into a folder of their own (see
|
|
228
|
+
// getStateFilesDir), which pods of its services may mount: of the commit before during
|
|
229
|
+
// a move, or of this state after a move that went back
|
|
230
|
+
const files = kubernetes.server?.serviceRun ? { dir: (0, volumes_1.getStateFilesDir)(options.stateKey) } : undefined;
|
|
192
231
|
try {
|
|
232
|
+
if (files && (await (0, ensure_persistent_data_1.getStateDirsInUse)(instance, kubernetes.namespace, claim)).has(files.dir)) {
|
|
233
|
+
task.status.write('info', `${task.name}: services of the run use its outputs of this state, they stay`);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
193
236
|
// the task writes to it now: what it holds stops being the outputs of a state, and
|
|
194
237
|
// goes, as a new claim would hold nothing
|
|
195
238
|
const reset = kept && (await (0, kept_claims_1.forgetClaimState)(instance, kubernetes, claim));
|
|
196
|
-
await (0, ensure_persistent_data_1.ensurePersistentData)(instance, kubernetes, environment, task, persistence, { reset });
|
|
239
|
+
await (0, ensure_persistent_data_1.ensurePersistentData)(instance, kubernetes, environment, task, persistence, { reset, files });
|
|
197
240
|
}
|
|
198
241
|
catch (e) {
|
|
199
242
|
options.state.set({ stateKey: options.stateKey, type: 'error', errorMessage: (0, log_1.getErrorMessage)(e) });
|
|
@@ -465,6 +508,10 @@ function kubernetesServiceRuntime(service, kubernetes) {
|
|
|
465
508
|
// stops it and a run reuses it, as with a container on Docker. One whose
|
|
466
509
|
// definition changed goes, with its limits, and is created anew.
|
|
467
510
|
async initialize(state) {
|
|
511
|
+
if (kubernetes.server?.serviceRun) {
|
|
512
|
+
// a service run starts every service: one that did not change stays as it is (execute)
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
468
515
|
const deployments = await instance.appsApi.listNamespacedDeployment({
|
|
469
516
|
namespace: kubernetes.namespace,
|
|
470
517
|
labelSelector: (0, resources_1.getItemSelector)(service, kubernetes),
|
|
@@ -509,18 +556,70 @@ function kubernetesServiceRuntime(service, kubernetes) {
|
|
|
509
556
|
await (0, network_policy_1.removeAllKubernetesItemNetworks)(instance, kubernetes, service);
|
|
510
557
|
},
|
|
511
558
|
async execute(environment, options) {
|
|
512
|
-
const
|
|
559
|
+
const name = (0, resources_1.getResourceName)(service, kubernetes);
|
|
560
|
+
// a service run that moves to another commit: a service of the same definition and
|
|
561
|
+
// state is left running, another one is rolled from the Deployment it has now
|
|
562
|
+
let previous;
|
|
563
|
+
if (kubernetes.server?.serviceRun) {
|
|
564
|
+
previous = await readDeployment(instance, kubernetes.namespace, name);
|
|
565
|
+
if (previous && isRolledOut(previous, (0, service_definition_1.getServiceDefinitionHash)(service), options.stateKey)) {
|
|
566
|
+
service.status.write('info', `${service.name} did not change, it stays up`);
|
|
567
|
+
options.state.set({ type: 'running', dns: { containerId: name }, stateKey: options.stateKey, remote: null });
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
const persistence = await (0, volumes_1.getKubernetesPersistence)(service, kubernetes, options.stateKey);
|
|
572
|
+
// the files of a service run's service go into a folder of their state, so the pod that
|
|
573
|
+
// runs keeps its own until the new one is up. One that watches its files (`continuous`)
|
|
574
|
+
// gets them synced into the folder its pod mounts: the pod that runs sees the changes.
|
|
575
|
+
const live = (0, volumes_1.isLiveService)(service, kubernetes);
|
|
576
|
+
// the files of the state it runs at now, to put back when the move does not get up
|
|
577
|
+
const before = live ? previous?.metadata?.labels?.['hammerkit.dev/state'] : undefined;
|
|
578
|
+
const files = kubernetes.server?.serviceRun
|
|
579
|
+
? {
|
|
580
|
+
dir: (0, volumes_1.getStateFilesDir)(options.stateKey),
|
|
581
|
+
live,
|
|
582
|
+
keep: before ? [(0, volumes_1.getStateFilesDir)(before)] : [],
|
|
583
|
+
}
|
|
584
|
+
: undefined;
|
|
585
|
+
if (live && previous && before) {
|
|
586
|
+
const from = (0, volumes_1.getStateFilesDir)(before);
|
|
587
|
+
service_rollouts_1.serviceRollouts.add(kubernetes.namespace, name, previous, () => (0, ensure_persistent_data_1.restoreLiveFiles)(instance, kubernetes, service, persistence, from));
|
|
588
|
+
}
|
|
513
589
|
await (0, ensure_namespace_1.ensureNamespace)(instance, kubernetes.namespace);
|
|
514
590
|
await (0, ensure_kubernetes_service_exists_1.ensureKubernetesServiceExists)(instance, kubernetes, service);
|
|
515
|
-
await (0, ensure_persistent_data_1.ensurePersistentData)(instance, kubernetes, environment, service, persistence);
|
|
591
|
+
await (0, ensure_persistent_data_1.ensurePersistentData)(instance, kubernetes, environment, service, persistence, { files });
|
|
516
592
|
await (0, secrets_1.ensureKubernetesSecret)(instance, kubernetes, service, service.data.secrets, environment);
|
|
517
593
|
// on a server, the run's: its runs share the namespace, and the service goes with the run
|
|
518
594
|
const network = await (0, network_policy_1.ensureKubernetesItemNetwork)(instance, kubernetes, service, {
|
|
519
595
|
followLog: false,
|
|
520
596
|
run: kubernetes.server ? (0, job_placement_1.getRunLabelValue)(kubernetes.server.run) : null,
|
|
521
597
|
});
|
|
522
|
-
|
|
523
|
-
|
|
598
|
+
// its output, as on Docker: while it starts, and while it runs unless it runs on
|
|
599
|
+
// without hammerkit (`up --daemon`)
|
|
600
|
+
const logs = (0, follow_pod_logs_1.followPodLogs)(instance, kubernetes.namespace, (0, resources_1.getItemSelector)(service, kubernetes), (0, ensure_kubernetes_deployment_exists_1.getServiceContainerName)(service), (line) => service.status.console('stdout', line), { onError: (text) => service.status.write('debug', text) });
|
|
601
|
+
try {
|
|
602
|
+
await (0, ensure_kubernetes_deployment_exists_1.ensureKubernetesDeploymentExists)(instance, kubernetes, service, persistence, (0, secrets_1.getKubernetesSecretRefs)(service, kubernetes, service.data.secrets), options.stateKey, network, { previous, signal: options.abort });
|
|
603
|
+
}
|
|
604
|
+
catch (e) {
|
|
605
|
+
// the last words of a container that crashed, before why it failed
|
|
606
|
+
await logs.settle().catch(() => undefined);
|
|
607
|
+
logs.stop();
|
|
608
|
+
throw e;
|
|
609
|
+
}
|
|
610
|
+
if (options.daemon || options.abort.aborted || environment.abortCtrl.signal.aborted) {
|
|
611
|
+
logs.stop();
|
|
612
|
+
}
|
|
613
|
+
else {
|
|
614
|
+
// either ends it, and takes the other listener with it
|
|
615
|
+
const listening = new AbortController();
|
|
616
|
+
const stop = () => {
|
|
617
|
+
logs.stop();
|
|
618
|
+
listening.abort();
|
|
619
|
+
};
|
|
620
|
+
options.abort.addEventListener('abort', stop, { once: true, signal: listening.signal });
|
|
621
|
+
environment.abortCtrl.signal.addEventListener('abort', stop, { once: true, signal: listening.signal });
|
|
622
|
+
}
|
|
524
623
|
options.state.set({
|
|
525
624
|
type: 'running',
|
|
526
625
|
dns: { containerId: name },
|
|
@@ -576,6 +675,30 @@ function kubernetesServiceRuntime(service, kubernetes) {
|
|
|
576
675
|
};
|
|
577
676
|
}
|
|
578
677
|
exports.kubernetesServiceRuntime = kubernetesServiceRuntime;
|
|
678
|
+
async function readDeployment(instance, namespace, name) {
|
|
679
|
+
try {
|
|
680
|
+
return await instance.appsApi.readNamespacedDeployment({ name, namespace });
|
|
681
|
+
}
|
|
682
|
+
catch (e) {
|
|
683
|
+
if ((0, apply_1.statusCodeOf)(e) === 404) {
|
|
684
|
+
return undefined;
|
|
685
|
+
}
|
|
686
|
+
throw e;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
// A Deployment of this definition and state whose pods are all of it and ready
|
|
690
|
+
function isRolledOut(deployment, definition, stateKey) {
|
|
691
|
+
const labels = deployment.metadata?.labels ?? {};
|
|
692
|
+
const status = deployment.status;
|
|
693
|
+
return (!deployment.metadata?.deletionTimestamp &&
|
|
694
|
+
labels[ensure_kubernetes_deployment_exists_1.DEFINITION_LABEL] === definition &&
|
|
695
|
+
labels['hammerkit.dev/state'] === stateKey &&
|
|
696
|
+
(status?.observedGeneration ?? 0) >= (deployment.metadata?.generation ?? 0) &&
|
|
697
|
+
(status?.updatedReplicas ?? 0) >= 1 &&
|
|
698
|
+
(status?.availableReplicas ?? 0) >= 1 &&
|
|
699
|
+
(status?.replicas ?? 0) === (status?.updatedReplicas ?? 0));
|
|
700
|
+
}
|
|
701
|
+
exports.isRolledOut = isRolledOut;
|
|
579
702
|
function kubernetesForwardRuntime(service) {
|
|
580
703
|
return {
|
|
581
704
|
async initialize(item) {
|
|
@@ -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` or `needs`. 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
|
@@ -111,6 +111,18 @@ function getListJson(tasks, services) {
|
|
|
111
111
|
})),
|
|
112
112
|
};
|
|
113
113
|
}
|
|
114
|
+
// `up` and `down` select by name or by label, not both: a name outside the labels would
|
|
115
|
+
// silently select nothing
|
|
116
|
+
function parseWorkNameScope(names, options) {
|
|
117
|
+
const scope = parseWorkLabelScope(options);
|
|
118
|
+
if (names.length === 0) {
|
|
119
|
+
return scope;
|
|
120
|
+
}
|
|
121
|
+
if (scope.filterLabels || scope.excludeLabels) {
|
|
122
|
+
throw new Error('name tasks and services or select them with --filter/--exclude, not both');
|
|
123
|
+
}
|
|
124
|
+
return { ...scope, names };
|
|
125
|
+
}
|
|
114
126
|
function parseWorkLabelScope(options) {
|
|
115
127
|
const scope = {
|
|
116
128
|
environmentName: null,
|
|
@@ -587,9 +599,9 @@ async function getProgram(environment, argv) {
|
|
|
587
599
|
}
|
|
588
600
|
});
|
|
589
601
|
program
|
|
590
|
-
.command('up')
|
|
591
|
-
.description("start services(s), with the arguments after -- appended to the selected service's cmd")
|
|
592
|
-
.usage('[options] [-- args...]')
|
|
602
|
+
.command('up [names...]')
|
|
603
|
+
.description("start services(s), all or the named tasks and services, with the arguments after -- appended to the selected service's cmd")
|
|
604
|
+
.usage('[options] [names...] [-- args...]')
|
|
593
605
|
.addOption(new commander_1.Option('-f, --filter <labels...>', 'filter task and services with labels'))
|
|
594
606
|
.addOption(new commander_1.Option('-e, --exclude <labels...>', 'exclude task and services with labels'))
|
|
595
607
|
.addOption(new commander_1.Option('-c, --concurrency <number>', 'parallel worker count (default 4, unbounded when every task and service requests cpus and memory)').argParser(parseInt))
|
|
@@ -608,11 +620,12 @@ async function getProgram(environment, argv) {
|
|
|
608
620
|
.choices(['checksum', 'modify-date', 'none']))
|
|
609
621
|
.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))
|
|
610
622
|
.addOption(new commander_1.Option('--timeout <duration>', 'fail tasks without their own timeout after this long (e.g. 10m)').argParser(units_1.parseDuration))
|
|
623
|
+
.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))
|
|
611
624
|
.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))
|
|
612
|
-
.action(async (options, command) => {
|
|
625
|
+
.action(async (names, options, command) => {
|
|
613
626
|
try {
|
|
614
627
|
const scope = {
|
|
615
|
-
...
|
|
628
|
+
...parseWorkNameScope(names, options),
|
|
616
629
|
args: { values: passthrough, services: true, cache: keepsCache(command) },
|
|
617
630
|
};
|
|
618
631
|
const cli = await createCli(fileName, environment, scope);
|
|
@@ -626,6 +639,7 @@ async function getProgram(environment, argv) {
|
|
|
626
639
|
wait: options.wait,
|
|
627
640
|
cacheReadOnly: (0, read_only_1.isCacheReadOnly)(options.cacheReadOnly, environment.processEnvs),
|
|
628
641
|
timeout: options.timeout ?? null,
|
|
642
|
+
maxTimeout: options.maxTimeout ?? null,
|
|
629
643
|
network: options.network ?? null,
|
|
630
644
|
});
|
|
631
645
|
if (!result.success) {
|
|
@@ -644,14 +658,14 @@ async function getProgram(environment, argv) {
|
|
|
644
658
|
}
|
|
645
659
|
});
|
|
646
660
|
program
|
|
647
|
-
.command('down')
|
|
648
|
-
.description('stop services(s)')
|
|
661
|
+
.command('down [names...]')
|
|
662
|
+
.description('stop services(s), all or the named services and the services the named tasks need')
|
|
649
663
|
.addOption(new commander_1.Option('-f, --filter <labels...>', 'filter task and services with labels'))
|
|
650
664
|
.addOption(new commander_1.Option('-e, --exclude <labels...>', 'exclude task and services with labels'))
|
|
651
665
|
.addOption(new commander_1.Option('--env <name>', 'environment'))
|
|
652
|
-
.action(async (options) => {
|
|
666
|
+
.action(async (names, options) => {
|
|
653
667
|
try {
|
|
654
|
-
const scope =
|
|
668
|
+
const scope = { ...parseWorkNameScope(names, options), stop: true };
|
|
655
669
|
const cli = await createCli(fileName, environment, scope);
|
|
656
670
|
const result = await cli.runDown();
|
|
657
671
|
if (!result.success) {
|
|
@@ -688,8 +702,10 @@ async function getProgram(environment, argv) {
|
|
|
688
702
|
.addOption(new commander_1.Option('--dry-run', 'print the execution plan with predicted cache hits/misses without running').default(false))
|
|
689
703
|
.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))
|
|
690
704
|
.addOption(new commander_1.Option('--timeout <duration>', 'fail tasks without their own timeout after this long (e.g. 10m)').argParser(units_1.parseDuration))
|
|
705
|
+
.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))
|
|
691
706
|
.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))
|
|
692
707
|
.addOption(new commander_1.Option('--no-skip-deps', 'run dependencies even when nothing needing them has to run'))
|
|
708
|
+
.addOption(new commander_1.Option('--no-restore', 'only look cache hits up, download their outputs only when a task that runs needs them'))
|
|
693
709
|
.action(async (task, options, command) => {
|
|
694
710
|
try {
|
|
695
711
|
// For machine-readable JSON, suppress the human progress logger (which
|
|
@@ -722,8 +738,10 @@ async function getProgram(environment, argv) {
|
|
|
722
738
|
explain: options.explain,
|
|
723
739
|
cacheReadOnly: (0, read_only_1.isCacheReadOnly)(options.cacheReadOnly, environment.processEnvs),
|
|
724
740
|
timeout: options.timeout ?? null,
|
|
741
|
+
maxTimeout: options.maxTimeout ?? null,
|
|
725
742
|
network: options.network ?? null,
|
|
726
743
|
skipDeps: options.skipDeps !== false,
|
|
744
|
+
restore: options.restore !== false,
|
|
727
745
|
});
|
|
728
746
|
// Reporting only: the summary never changes the exit code or behavior.
|
|
729
747
|
if (!options.watch) {
|