hammerkit 1.26.0 → 1.27.0-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build.schema.json +53 -1
- package/dist/cache/backends/local-cache-backend.js +10 -2
- package/dist/cache/explain.js +13 -11
- package/dist/cli.js +3 -0
- package/dist/docker/item-network.js +471 -0
- package/dist/docker/pull.js +4 -2
- package/dist/docker/remove-orphaned-containers.js +13 -6
- package/dist/environment/replace-env-variables.js +8 -6
- package/dist/executer/await-completed-dependencies.js +2 -1
- package/dist/executer/check-readiness.js +2 -2
- package/dist/executer/docker-service.js +24 -13
- package/dist/executer/docker-task.js +18 -29
- package/dist/executer/event-cache.js +4 -0
- package/dist/executer/execute-command.js +18 -5
- package/dist/executer/execute-docker.js +4 -1
- package/dist/executer/execution-steps.js +33 -7
- package/dist/executer/get-docker-executor.js +14 -7
- package/dist/executer/scheduler/enqueue-next.js +17 -3
- package/dist/executer/watch-loop.js +2 -2
- package/dist/executer/watch-state-key.js +1 -1
- package/dist/file/within-root.js +42 -0
- package/dist/harness/connect.js +99 -0
- package/dist/harness/format.js +73 -0
- package/dist/harness/mcp-command.js +59 -0
- package/dist/harness/mcp.js +432 -0
- package/dist/harness/opencode.js +492 -0
- package/dist/harness/pi.js +217 -0
- package/dist/harness/sandbox-client.js +373 -0
- package/dist/index.js +33 -36
- package/dist/kubernetes/apply.js +31 -1
- package/dist/kubernetes/await-running-state.js +33 -1
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +43 -21
- package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +131 -4
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +25 -4
- package/dist/kubernetes/ensure-persistent-data.js +48 -7
- package/dist/kubernetes/get-service-ip.js +15 -1
- package/dist/kubernetes/job-placement.js +99 -0
- package/dist/kubernetes/job-secrets.js +306 -0
- package/dist/kubernetes/kept-claims.js +191 -0
- package/dist/kubernetes/kubernetes-instance.js +10 -2
- package/dist/kubernetes/network-gate.js +71 -0
- package/dist/kubernetes/network-policy.js +526 -0
- package/dist/kubernetes/resources.js +13 -2
- package/dist/kubernetes/restore-kubernetes-data.js +19 -4
- package/dist/kubernetes/secrets.js +2 -0
- package/dist/kubernetes/store-kubernetes-data.js +1 -1
- package/dist/kubernetes/volumes.js +100 -11
- package/dist/main.js +41 -0
- package/dist/network/egress-proxy.js +368 -0
- package/dist/network/port-relay.js +35 -0
- package/dist/optimizer/get-work-cache-stats.js +16 -0
- package/dist/optimizer/work-task-cache-description.js +3 -2
- package/dist/planner/service-definition.js +5 -0
- package/dist/planner/utils/append-work-environment.js +2 -0
- package/dist/planner/utils/append-work-service.js +4 -0
- package/dist/planner/utils/append-work-task.js +17 -1
- package/dist/planner/utils/assert-item-paths.js +50 -0
- package/dist/planner/utils/hosted-volumes.js +35 -0
- package/dist/planner/work-network.js +193 -0
- package/dist/planner/work-runtime-docker.js +41 -0
- package/dist/planner/work-runtime-kubernetes.js +335 -102
- package/dist/program.js +21 -0
- package/dist/remote/annotations.js +103 -0
- package/dist/remote/bundle-text.js +24 -0
- package/dist/remote/bundle.js +9 -1
- package/dist/remote/remote-commands.js +4 -1
- package/dist/remote/remote-work-commands.js +182 -69
- package/dist/remote/server-client.js +18 -5
- package/dist/remote/source-manifest.js +15 -2
- package/dist/remote/token-source.js +67 -0
- package/dist/schema/build-file-base-task-schema.js +3 -0
- package/dist/schema/build-file-container-service-schema.js +6 -0
- package/dist/schema/build-file-container-task-schema.js +3 -0
- package/dist/schema/build-file-environment-schema.js +9 -0
- package/dist/schema/network-schema.js +78 -0
- package/dist/schema/reference-parser.js +1 -0
- package/dist/schema/schema-parser.js +16 -3
- package/dist/schema/work-scope-parser.js +5 -0
- package/dist/secrets/command-secret-provider.js +73 -10
- package/dist/secrets/github-app-token-command.js +23 -0
- package/dist/secrets/github-app-token.js +191 -0
- package/dist/server/accounts-api.js +9 -2
- package/dist/server/definitions-api.js +69 -47
- package/dist/server/definitions.js +76 -51
- package/dist/server/executor.js +156 -41
- package/dist/server/git-checkout.js +72 -8
- package/dist/server/kubernetes-backend.js +554 -0
- package/dist/server/kubernetes-sandbox-storage.js +996 -0
- package/dist/server/oauth.js +1 -0
- package/dist/server/plan.js +51 -1
- package/dist/server/policy.js +104 -15
- package/dist/server/proposals-api.js +24 -40
- package/dist/server/proposals.js +12 -25
- package/dist/server/repo-access.js +37 -6
- package/dist/server/run-logs.js +217 -0
- package/dist/server/run-plan.js +51 -2
- package/dist/server/run-request.js +23 -10
- package/dist/server/run-store.js +5 -0
- package/dist/server/run-worker-command.js +13 -1
- package/dist/server/run-worker.js +121 -24
- package/dist/server/runs.js +184 -10
- package/dist/server/sandbox-docker.js +62 -0
- package/dist/server/sandbox-files-api.js +269 -0
- package/dist/server/sandbox-files.js +1054 -0
- package/dist/server/sandbox-patch.js +481 -0
- package/dist/server/sandbox-pod-command.js +15 -0
- package/dist/server/sandbox-pod.js +146 -0
- package/dist/server/sandbox-storage.js +462 -0
- package/dist/server/sandboxes-api.js +210 -0
- package/dist/server/sandboxes.js +519 -0
- package/dist/server/server-config.js +412 -22
- package/dist/server/server.js +204 -39
- package/dist/utils/run-id.js +12 -0
- package/harness/opencode.ts +11 -0
- package/harness/pi.ts +7 -0
- package/package.json +8 -2
- package/dist/remote/text-diff.js +0 -89
|
@@ -4,6 +4,7 @@ exports.getServiceDefinitionHash = void 0;
|
|
|
4
4
|
const crypto_1 = require("crypto");
|
|
5
5
|
const work_secret_1 = require("./work-secret");
|
|
6
6
|
const work_service_1 = require("./work-service");
|
|
7
|
+
const work_network_1 = require("./work-network");
|
|
7
8
|
const work_cache_id_1 = require("./work-cache-id");
|
|
8
9
|
// Everything a running container of the service was created from, plus the
|
|
9
10
|
// definitions of the services it needs: a dependent links to the container it
|
|
@@ -12,6 +13,7 @@ const work_cache_id_1 = require("./work-cache-id");
|
|
|
12
13
|
// service whose definition changed instead of silently reusing it.
|
|
13
14
|
function getServiceDefinitionHash(item) {
|
|
14
15
|
const service = item.data;
|
|
16
|
+
const network = (0, work_network_1.getEffectiveNetwork)(service);
|
|
15
17
|
const definition = {
|
|
16
18
|
image: service.image,
|
|
17
19
|
cmd: service.cmd?.cmd ?? null,
|
|
@@ -38,6 +40,9 @@ function getServiceDefinitionHash(item) {
|
|
|
38
40
|
.map((need) => `${need.name}=${getServiceDefinitionHash(need.service)}`),
|
|
39
41
|
// only when declared, so a service without keeps its hash
|
|
40
42
|
...(service.resources ? { resources: service.resources } : {}),
|
|
43
|
+
// in this run, under its `--network` ceiling: a service started open is
|
|
44
|
+
// recreated for a run that limits it
|
|
45
|
+
...((0, work_network_1.isLimitedNetwork)(network) ? { network } : {}),
|
|
41
46
|
};
|
|
42
47
|
return (0, crypto_1.createHash)('sha1').update(JSON.stringify(definition)).digest('hex');
|
|
43
48
|
}
|
|
@@ -15,6 +15,8 @@ function appendWorkEnvironment(referenced) {
|
|
|
15
15
|
kubeConfig: referenced.schema.kubernetes.kubeconfig,
|
|
16
16
|
httpRoutes: referenced.schema.kubernetes.httpRoutes || [],
|
|
17
17
|
namespace: referenced.schema.kubernetes.namespace ?? 'default',
|
|
18
|
+
networkPolicyEnforced: referenced.schema.kubernetes.networkPolicy === 'enforced',
|
|
19
|
+
...(referenced.schema.kubernetes.volumeSize ? { volumeSize: referenced.schema.kubernetes.volumeSize } : {}),
|
|
18
20
|
};
|
|
19
21
|
}
|
|
20
22
|
}
|
|
@@ -23,6 +23,7 @@ const resolve_cache_1 = require("../../cache/resolve-cache");
|
|
|
23
23
|
const units_1 = require("../../utils/units");
|
|
24
24
|
const log_1 = require("../../log");
|
|
25
25
|
const work_resources_1 = require("../work-resources");
|
|
26
|
+
const work_network_1 = require("../work-network");
|
|
26
27
|
function appendWorkService(workTree, service, environment, context) {
|
|
27
28
|
const workService = parseService(workTree, service, environment, context);
|
|
28
29
|
if (!workTree.services[workService.name]) {
|
|
@@ -116,6 +117,9 @@ function parseService(workTree, service, environment, context) {
|
|
|
116
117
|
init: null,
|
|
117
118
|
secrets: (0, work_secret_1.parseWorkSecrets)(service.cwd, context.projectRoot, service.schema.secrets, envs, environment, context.secrets, service.schema.account),
|
|
118
119
|
resources: (0, work_resources_1.parseWorkResources)(service.schema.resources),
|
|
120
|
+
volumeSize: service.schema.volumeSize ?? null,
|
|
121
|
+
network: (0, work_network_1.parseWorkNetwork)(service.schema.network),
|
|
122
|
+
networkCeiling: null,
|
|
119
123
|
};
|
|
120
124
|
}
|
|
121
125
|
}
|
|
@@ -19,8 +19,17 @@ const get_work_runtime_1 = require("./get-work-runtime");
|
|
|
19
19
|
const resolve_cache_1 = require("../../cache/resolve-cache");
|
|
20
20
|
const units_1 = require("../../utils/units");
|
|
21
21
|
const work_resources_1 = require("../work-resources");
|
|
22
|
+
const job_placement_1 = require("../../kubernetes/job-placement");
|
|
23
|
+
const work_network_1 = require("../work-network");
|
|
22
24
|
function appendWorkTask(workTree, cwd, referenceTask, environment, context) {
|
|
23
|
-
const
|
|
25
|
+
const parsed = parseTask(cwd, referenceTask, environment, context);
|
|
26
|
+
// on a server's cluster a local task runs in a tool image like a container task
|
|
27
|
+
const server = workTree.environment.type === 'kubernetes' ? workTree.environment.server : undefined;
|
|
28
|
+
const task = !server
|
|
29
|
+
? parsed
|
|
30
|
+
: parsed.type === 'local-task'
|
|
31
|
+
? (0, job_placement_1.runInToolImage)(parsed, server, referenceTask.schema.platform)
|
|
32
|
+
: (0, job_placement_1.placeOnPool)(parsed, server);
|
|
24
33
|
if (!workTree.tasks[task.name]) {
|
|
25
34
|
const workItem = {
|
|
26
35
|
// A dependency cycle is rejected before anything runs (checkForLoop), but
|
|
@@ -72,6 +81,8 @@ function parseTask(cwd, task, environment, context) {
|
|
|
72
81
|
timeout: task.schema.timeout ? (0, units_1.parseDuration)(task.schema.timeout) : null,
|
|
73
82
|
secrets: (0, work_secret_1.parseWorkSecrets)(cwd, context.projectRoot, task.schema.secrets, envs, environment, context.secrets, task.schema.account),
|
|
74
83
|
resources: (0, work_resources_1.parseWorkResources)(task.schema.resources),
|
|
84
|
+
network: (0, work_network_1.parseWorkNetwork)(task.schema.network),
|
|
85
|
+
networkCeiling: null,
|
|
75
86
|
runner: task.schema.runner ?? task.scope.schema.runner ?? null,
|
|
76
87
|
};
|
|
77
88
|
if ((0, build_file_task_schema_1.isBuildFileContainerTaskSchema)(task.schema)) {
|
|
@@ -81,6 +92,7 @@ function parseTask(cwd, task, environment, context) {
|
|
|
81
92
|
user: (0, get_container_user_1.getContainerUser)(),
|
|
82
93
|
image: (0, template_value_1.templateValue)(task.schema.image, envs),
|
|
83
94
|
mounts: (0, parse_work_mounts_1.parseWorkMounts)(cwd, task.schema, envs),
|
|
95
|
+
volumeSize: task.schema.volumeSize ?? null,
|
|
84
96
|
};
|
|
85
97
|
}
|
|
86
98
|
else {
|
|
@@ -88,6 +100,10 @@ function parseTask(cwd, task, environment, context) {
|
|
|
88
100
|
if (fileSecret) {
|
|
89
101
|
throw new Error(`task ${task.relativeName}: a local task takes secrets as env only (${(0, work_secret_1.getSecretName)(fileSecret)})`);
|
|
90
102
|
}
|
|
103
|
+
if (baseWorkNode.network.type !== 'open') {
|
|
104
|
+
throw new Error(`task ${task.relativeName}: network ${(0, work_network_1.formatNetwork)(baseWorkNode.network)} needs a container, ` +
|
|
105
|
+
`hammerkit can't limit the network of a local task`);
|
|
106
|
+
}
|
|
91
107
|
return {
|
|
92
108
|
...baseWorkNode,
|
|
93
109
|
type: 'local-task',
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.assertWorkTreePathsWithinRoot = exports.assertItemPathsWithinRoot = void 0;
|
|
4
|
+
const within_root_1 = require("../../file/within-root");
|
|
5
|
+
const work_service_1 = require("../work-service");
|
|
6
|
+
const work_task_1 = require("../work-task");
|
|
7
|
+
const hosted_volumes_1 = require("./hosted-volumes");
|
|
8
|
+
/**
|
|
9
|
+
* Refuses an item whose sources, outputs or mounts leave the root of a run, or go
|
|
10
|
+
* through a symbolic link in it. The server runs build files it did not write, in a
|
|
11
|
+
* checkout it did not write either: a `src: ../..` or a link in the checkout would
|
|
12
|
+
* have the server mount or upload its own files (its credentials, other callers'
|
|
13
|
+
* caches) into a task. Checked when the run is planned and again before each item
|
|
14
|
+
* runs, since an earlier task may have left a link behind.
|
|
15
|
+
*/
|
|
16
|
+
async function assertItemPathsWithinRoot(item, root) {
|
|
17
|
+
const data = item.data;
|
|
18
|
+
for (const source of 'src' in data ? data.src : []) {
|
|
19
|
+
if (!source.inherited) {
|
|
20
|
+
await (0, within_root_1.assertWithinRoot)(root, source.absolutePath, `${item.name}: src ${source.source}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if ((0, work_task_1.isContainerWorkTask)(data) || (0, work_service_1.isContainerWorkService)(data)) {
|
|
24
|
+
for (const mount of data.mounts) {
|
|
25
|
+
await (0, within_root_1.assertWithinRoot)(root, mount.localPath, `${item.name}: mount ${mount.localPath}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if ((0, work_service_1.isContainerWorkService)(data)) {
|
|
29
|
+
for (const volume of data.volumes) {
|
|
30
|
+
if (!(0, hosted_volumes_1.isVolumeName)(volume.name)) {
|
|
31
|
+
throw new Error(`${item.name}: volume ${volume.name} is not a volume name, a path of the server can't be mounted`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if ((0, work_task_1.isWorkTask)(data)) {
|
|
36
|
+
for (const generate of data.generates) {
|
|
37
|
+
if (!generate.inherited) {
|
|
38
|
+
await (0, within_root_1.assertWithinRoot)(root, generate.path, `${item.name}: generates ${generate.path}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
exports.assertItemPathsWithinRoot = assertItemPathsWithinRoot;
|
|
44
|
+
async function assertWorkTreePathsWithinRoot(workTree, root) {
|
|
45
|
+
for (const item of [...Object.values(workTree.tasks), ...Object.values(workTree.services)]) {
|
|
46
|
+
await assertItemPathsWithinRoot(item, root);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.assertWorkTreePathsWithinRoot = assertWorkTreePathsWithinRoot;
|
|
50
|
+
//# sourceMappingURL=assert-item-paths.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.scopeServiceVolumes = exports.isVolumeName = void 0;
|
|
4
|
+
const work_service_1 = require("../work-service");
|
|
5
|
+
// What Docker takes as the name of a volume. Anything else in a service's `volumes` is a
|
|
6
|
+
// path of the host to Docker (`/var/run/docker.sock:/run/docker.sock` binds the socket).
|
|
7
|
+
const VOLUME_NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]+$/;
|
|
8
|
+
function isVolumeName(name) {
|
|
9
|
+
return VOLUME_NAME.test(name);
|
|
10
|
+
}
|
|
11
|
+
exports.isVolumeName = isVolumeName;
|
|
12
|
+
/**
|
|
13
|
+
* On a server, the docker volumes of a run's services are its caller's: each name gets the
|
|
14
|
+
* run's scope (its caller and source trust) in front, so a build file can neither open nor,
|
|
15
|
+
* with `resetOnChange`, remove a volume of another caller, of a sandbox or of the host.
|
|
16
|
+
* The outputs a service inherits from its deps keep their names: those are made from paths
|
|
17
|
+
* in the run's own files, and the task that writes them uses the same name. A Kubernetes
|
|
18
|
+
* backend keeps claims in the caller's namespace and is left as it is.
|
|
19
|
+
*/
|
|
20
|
+
function scopeServiceVolumes(workTree, scope) {
|
|
21
|
+
const scoped = new Set();
|
|
22
|
+
for (const service of Object.values(workTree.services)) {
|
|
23
|
+
if (!(0, work_service_1.isContainerWorkService)(service.data)) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
for (const volume of service.data.volumes) {
|
|
27
|
+
if (!volume.inherited && !scoped.has(volume)) {
|
|
28
|
+
scoped.add(volume);
|
|
29
|
+
volume.name = `hammerkit-${scope}-${volume.name}`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
exports.scopeServiceVolumes = scopeServiceVolumes;
|
|
35
|
+
//# sourceMappingURL=hosted-volumes.js.map
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
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
|
+
const network_schema_1 = require("../schema/network-schema");
|
|
5
|
+
exports.OPEN_NETWORK = { type: 'open' };
|
|
6
|
+
function parseWorkNetwork(schema) {
|
|
7
|
+
if (!schema || schema === 'open') {
|
|
8
|
+
return exports.OPEN_NETWORK;
|
|
9
|
+
}
|
|
10
|
+
if (schema === 'none') {
|
|
11
|
+
return { type: 'none' };
|
|
12
|
+
}
|
|
13
|
+
return { type: 'allow', hosts: normalizeHosts(schema.allow) };
|
|
14
|
+
}
|
|
15
|
+
exports.parseWorkNetwork = parseWorkNetwork;
|
|
16
|
+
// `--network none`, `--network open` or `--network allow:<host>,<host:port>`
|
|
17
|
+
function parseNetworkOption(value) {
|
|
18
|
+
if (value === 'open' || value === 'none') {
|
|
19
|
+
return { type: value };
|
|
20
|
+
}
|
|
21
|
+
if (value.startsWith('allow:')) {
|
|
22
|
+
const hosts = value
|
|
23
|
+
.substring('allow:'.length)
|
|
24
|
+
.split(',')
|
|
25
|
+
.map((host) => host.trim())
|
|
26
|
+
.filter((host) => host.length > 0);
|
|
27
|
+
const invalid = hosts.find((host) => !(0, network_schema_1.isAllowEntry)(host));
|
|
28
|
+
if (invalid) {
|
|
29
|
+
throw new Error(`invalid host "${invalid}", expected a name such as registry.npmjs.org or *.example.com, ` +
|
|
30
|
+
'or a name and port such as db.example.com:5432');
|
|
31
|
+
}
|
|
32
|
+
const error = (0, network_schema_1.getAllowListError)(hosts);
|
|
33
|
+
if (error) {
|
|
34
|
+
throw new Error(error);
|
|
35
|
+
}
|
|
36
|
+
return { type: 'allow', hosts: normalizeHosts(hosts) };
|
|
37
|
+
}
|
|
38
|
+
throw new Error(`invalid network "${value}", expected none, open or allow:<host>,<host>`);
|
|
39
|
+
}
|
|
40
|
+
exports.parseNetworkOption = parseNetworkOption;
|
|
41
|
+
// The setting under a ceiling: a more open setting is clamped down to it, and
|
|
42
|
+
// an allow list under an allow ceiling keeps the hosts both lists allow. A
|
|
43
|
+
// name one list allows only through a pattern reaches no private address.
|
|
44
|
+
function limitNetwork(network, ceiling) {
|
|
45
|
+
if (!ceiling || ceiling.type === 'open' || network.type === 'none') {
|
|
46
|
+
return network;
|
|
47
|
+
}
|
|
48
|
+
if (ceiling.type === 'none') {
|
|
49
|
+
return ceiling;
|
|
50
|
+
}
|
|
51
|
+
if (network.type === 'open') {
|
|
52
|
+
return ceiling;
|
|
53
|
+
}
|
|
54
|
+
const hosts = [
|
|
55
|
+
...network.hosts.filter((host) => ceiling.hosts.some((limit) => coversHost(limit, host))),
|
|
56
|
+
...ceiling.hosts.filter((limit) => network.hosts.some((host) => coversHost(host, limit))),
|
|
57
|
+
];
|
|
58
|
+
const normalized = normalizeHosts(hosts);
|
|
59
|
+
const publicOnly = normalized.filter((host) => !host.startsWith('*.') && !(coversByName(network, host) && coversByName(ceiling, host)));
|
|
60
|
+
return publicOnly.length > 0 ? { type: 'allow', hosts: normalized, publicOnly } : { type: 'allow', hosts: normalized };
|
|
61
|
+
}
|
|
62
|
+
exports.limitNetwork = limitNetwork;
|
|
63
|
+
// Whether a name (not a pattern) of the list covers `host` and may reach
|
|
64
|
+
// private addresses.
|
|
65
|
+
function coversByName(network, host) {
|
|
66
|
+
return network.hosts.some((entry) => !entry.startsWith('*.') && !network.publicOnly?.includes(entry) && coversHost(entry, host));
|
|
67
|
+
}
|
|
68
|
+
// The network of a task or service in this run: its own setting under the
|
|
69
|
+
// run's ceiling (`--network`).
|
|
70
|
+
function getEffectiveNetwork(item) {
|
|
71
|
+
return limitNetwork(item.network, item.networkCeiling);
|
|
72
|
+
}
|
|
73
|
+
exports.getEffectiveNetwork = getEffectiveNetwork;
|
|
74
|
+
// Whether `network` reaches more than `ceiling` allows: a host it allows that no
|
|
75
|
+
// host of the ceiling covers. Redundant hosts of the ceiling don't count.
|
|
76
|
+
function exceedsNetwork(network, ceiling) {
|
|
77
|
+
if (ceiling.type === 'open' || network.type === 'none') {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
if (network.type === 'open') {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
if (ceiling.type === 'none') {
|
|
84
|
+
return network.hosts.length > 0;
|
|
85
|
+
}
|
|
86
|
+
return network.hosts.some((host) => !ceiling.hosts.some((limit) => coversHost(limit, host)));
|
|
87
|
+
}
|
|
88
|
+
exports.exceedsNetwork = exceedsNetwork;
|
|
89
|
+
// The setting as a build file writes it.
|
|
90
|
+
function toNetworkSchema(network) {
|
|
91
|
+
return network.type === 'allow' ? { allow: [...network.hosts] } : network.type;
|
|
92
|
+
}
|
|
93
|
+
exports.toNetworkSchema = toNetworkSchema;
|
|
94
|
+
function isLimitedNetwork(network) {
|
|
95
|
+
return network.type !== 'open';
|
|
96
|
+
}
|
|
97
|
+
exports.isLimitedNetwork = isLimitedNetwork;
|
|
98
|
+
// Whether a request to `host` passes `pattern`: the same name, or a name under
|
|
99
|
+
// the domain of a `*.` pattern (the domain itself is not included).
|
|
100
|
+
function matchesHost(pattern, host) {
|
|
101
|
+
const name = host.toLowerCase().replace(/\.$/, '');
|
|
102
|
+
const rule = pattern.toLowerCase();
|
|
103
|
+
return rule.startsWith('*.') ? name.endsWith(rule.substring(1)) : name === rule;
|
|
104
|
+
}
|
|
105
|
+
exports.matchesHost = matchesHost;
|
|
106
|
+
// Whether everything `host` (a name, a `*.` pattern or a name and port)
|
|
107
|
+
// allows, `limit` allows too. A host without a port reaches every port of it
|
|
108
|
+
// through the proxy, so it covers a forward to one of them; a forward covers
|
|
109
|
+
// only itself.
|
|
110
|
+
function coversHost(limit, host) {
|
|
111
|
+
const limitEntry = (0, network_schema_1.parseAllowEntry)(limit);
|
|
112
|
+
const hostEntry = (0, network_schema_1.parseAllowEntry)(host);
|
|
113
|
+
if (!limitEntry || !hostEntry) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
if (limitEntry.port !== null) {
|
|
117
|
+
return hostEntry.port === limitEntry.port && matchesHost(limitEntry.host, hostEntry.host);
|
|
118
|
+
}
|
|
119
|
+
if (hostEntry.host.startsWith('*.')) {
|
|
120
|
+
// `*.b.example.com` is within `*.example.com`
|
|
121
|
+
return (limitEntry.host.startsWith('*.') &&
|
|
122
|
+
hostEntry.host.toLowerCase().endsWith(limitEntry.host.toLowerCase().substring(1)));
|
|
123
|
+
}
|
|
124
|
+
return matchesHost(limitEntry.host, hostEntry.host);
|
|
125
|
+
}
|
|
126
|
+
// The raw TCP forwards of a network: the entries of `allow` with a port.
|
|
127
|
+
function getTcpForwards(network) {
|
|
128
|
+
if (network.type !== 'allow') {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
return network.hosts
|
|
132
|
+
.map(network_schema_1.parseAllowEntry)
|
|
133
|
+
.filter((entry) => entry !== null && entry.port !== null);
|
|
134
|
+
}
|
|
135
|
+
exports.getTcpForwards = getTcpForwards;
|
|
136
|
+
function normalizeHosts(hosts) {
|
|
137
|
+
return [...new Set(hosts.map((host) => host.toLowerCase()))].sort();
|
|
138
|
+
}
|
|
139
|
+
function formatNetwork(network) {
|
|
140
|
+
if (network.type === 'allow') {
|
|
141
|
+
return network.hosts.length > 0 ? `allow ${network.hosts.join(', ')}` : 'allow (no hosts)';
|
|
142
|
+
}
|
|
143
|
+
return network.type;
|
|
144
|
+
}
|
|
145
|
+
exports.formatNetwork = formatNetwork;
|
|
146
|
+
// Sets the run's ceiling on every task and service of the work tree. A local
|
|
147
|
+
// task can't be limited, so a ceiling below open with one in the run is an
|
|
148
|
+
// error before anything runs. So is a limited item that needs a service under
|
|
149
|
+
// a name that is no host name: the proxy passes the names of the needed
|
|
150
|
+
// services, and `*.com` would pass every .com host. A TCP forward under the
|
|
151
|
+
// name of a needed service would hide that service, so it is an error too, as
|
|
152
|
+
// are two forwards on one port, which the ceiling can leave from two lists.
|
|
153
|
+
function applyNetworkCeiling(workTree, ceiling) {
|
|
154
|
+
const limit = ceiling && isLimitedNetwork(ceiling) ? ceiling : null;
|
|
155
|
+
if (limit) {
|
|
156
|
+
const local = Object.values(workTree.tasks).find((task) => task.data.type === 'local-task');
|
|
157
|
+
if (local) {
|
|
158
|
+
throw new Error(`--network ${formatNetwork(limit)} needs every task in a container, ` +
|
|
159
|
+
`hammerkit can't limit the network of the local task ${local.name}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
for (const task of Object.values(workTree.tasks)) {
|
|
163
|
+
task.data.networkCeiling = limit;
|
|
164
|
+
}
|
|
165
|
+
for (const service of Object.values(workTree.services)) {
|
|
166
|
+
if (service.data.type === 'container-service') {
|
|
167
|
+
service.data.networkCeiling = limit;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
for (const item of [...Object.values(workTree.tasks), ...Object.values(workTree.services)]) {
|
|
171
|
+
if (item.data.type === 'local-task' || item.data.type === 'kubernetes-service') {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const network = getEffectiveNetwork(item.data);
|
|
175
|
+
const invalid = item.needs.find((need) => !(0, network_schema_1.isNeedName)(need.name));
|
|
176
|
+
if (isLimitedNetwork(network) && invalid) {
|
|
177
|
+
throw new Error(`${item.name}: network ${formatNetwork(network)} reaches a needed service by a host name, ` +
|
|
178
|
+
`"${invalid.name}" is none (name it like db or api-v2)`);
|
|
179
|
+
}
|
|
180
|
+
const shared = network.type === 'allow' ? (0, network_schema_1.getAllowListError)(network.hosts) : null;
|
|
181
|
+
if (shared) {
|
|
182
|
+
const under = limit ? ` under --network ${formatNetwork(limit)}` : '';
|
|
183
|
+
throw new Error(`${item.name}: network ${formatNetwork(network)}${under}, ${shared}`);
|
|
184
|
+
}
|
|
185
|
+
const hidden = getTcpForwards(network).find((forward) => item.needs.some((need) => need.name.toLowerCase() === forward.host.toLowerCase()));
|
|
186
|
+
if (hidden) {
|
|
187
|
+
throw new Error(`${item.name}: network ${formatNetwork(network)} forwards ${hidden.host}:${hidden.port}, ` +
|
|
188
|
+
`which is the name of a service it needs`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
exports.applyNetworkCeiling = applyNetworkCeiling;
|
|
193
|
+
//# sourceMappingURL=work-network.js.map
|
|
@@ -17,6 +17,10 @@ const work_instance_id_1 = require("./work-instance-id");
|
|
|
17
17
|
const stream_1 = require("stream");
|
|
18
18
|
const service_definition_1 = require("./service-definition");
|
|
19
19
|
const container_task_state_1 = require("../executer/container-task-state");
|
|
20
|
+
const item_network_1 = require("../docker/item-network");
|
|
21
|
+
const execution_steps_1 = require("../executer/execution-steps");
|
|
22
|
+
const assert_item_paths_1 = require("./utils/assert-item-paths");
|
|
23
|
+
const log_1 = require("../log");
|
|
20
24
|
function dockerTaskRuntime(task, workEnvironment) {
|
|
21
25
|
const docker = (0, execute_docker_1.getContainerCli)(workEnvironment);
|
|
22
26
|
return {
|
|
@@ -56,8 +60,18 @@ function dockerTaskRuntime(task, workEnvironment) {
|
|
|
56
60
|
for (const container of containers) {
|
|
57
61
|
await (0, remove_container_1.removeContainer)(docker.getContainer(container.Id));
|
|
58
62
|
}
|
|
63
|
+
await (0, item_network_1.removeItemNetworks)(docker, task);
|
|
59
64
|
},
|
|
60
65
|
async execute(environment, options) {
|
|
66
|
+
if (environment.hosted) {
|
|
67
|
+
try {
|
|
68
|
+
await (0, assert_item_paths_1.assertItemPathsWithinRoot)(task, environment.cwd);
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
options.state.set({ stateKey: options.stateKey, type: 'error', errorMessage: (0, log_1.getErrorMessage)(e) });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
61
75
|
await (0, docker_task_1.dockerTask)(docker, task, environment, options);
|
|
62
76
|
},
|
|
63
77
|
async remove(environment) {
|
|
@@ -103,6 +117,18 @@ function isOnHost(generate) {
|
|
|
103
117
|
return generate.isFile || generate.export;
|
|
104
118
|
}
|
|
105
119
|
async function restoreContainer(docker, environment, item, path) {
|
|
120
|
+
await (0, execution_steps_1.refuseHostLinks)(item, environment);
|
|
121
|
+
// on a server the volumes the outputs are restored into carry the run's files, so they
|
|
122
|
+
// are removed with a sandbox
|
|
123
|
+
const root = (0, execution_steps_1.hostedRoot)(environment);
|
|
124
|
+
if (root) {
|
|
125
|
+
const names = item.data.type === 'container-service'
|
|
126
|
+
? item.data.volumes.filter((v) => !v.inherited).map((v) => v.name)
|
|
127
|
+
: item.data.generates.filter((v) => !v.inherited && !v.isFile).map((v) => v.volumeName);
|
|
128
|
+
for (const name of names) {
|
|
129
|
+
await (0, get_docker_executor_1.ensureVolumeExists)(docker, item.status, name, (0, get_docker_executor_1.volumeLabels)(root));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
106
132
|
await (0, using_container_1.usingContainer)(docker, item, {
|
|
107
133
|
abortSignal: environment.abortCtrl.signal,
|
|
108
134
|
Image: item.data.image,
|
|
@@ -142,6 +168,7 @@ async function restoreContainer(docker, environment, item, path) {
|
|
|
142
168
|
}
|
|
143
169
|
});
|
|
144
170
|
if (item.data.type === 'container-task') {
|
|
171
|
+
await (0, execution_steps_1.refuseHostLinks)(item, environment);
|
|
145
172
|
const onHost = new Set(item.data.generates.filter((g) => !g.inherited && isOnHost(g)).map((g) => g.path));
|
|
146
173
|
for (const generate of (0, event_cache_1.getArchivePaths)(item.data, path)) {
|
|
147
174
|
if (onHost.has(generate.path) && (await environment.file.exists(generate.filename))) {
|
|
@@ -167,6 +194,8 @@ function dockerServiceRuntime(service, workEnvironment) {
|
|
|
167
194
|
if (currentService.Labels['hammerkit-definition'] !== (0, service_definition_1.getServiceDefinitionHash)(service)) {
|
|
168
195
|
service.status.write('info', `${service.name} changed since it was started, recreating it`);
|
|
169
196
|
await (0, remove_container_1.removeContainer)(docker.getContainer(currentService.Id));
|
|
197
|
+
// its network, proxy and relay go with it, whatever its new setting
|
|
198
|
+
await (0, item_network_1.removeItemNetworks)(docker, service);
|
|
170
199
|
return;
|
|
171
200
|
}
|
|
172
201
|
const servicePid = 'hammerkit-pid' in currentService.Labels ? parseInt(currentService.Labels['hammerkit-pid']) : undefined;
|
|
@@ -190,6 +219,7 @@ function dockerServiceRuntime(service, workEnvironment) {
|
|
|
190
219
|
for (const container of containers) {
|
|
191
220
|
await (0, remove_container_1.removeContainer)(docker.getContainer(container.Id));
|
|
192
221
|
}
|
|
222
|
+
await (0, item_network_1.removeItemNetworks)(docker, service);
|
|
193
223
|
await (0, container_secrets_1.removeContainerSecrets)((0, work_instance_id_1.getWorkInstanceId)(service));
|
|
194
224
|
},
|
|
195
225
|
async remove() {
|
|
@@ -203,6 +233,16 @@ function dockerServiceRuntime(service, workEnvironment) {
|
|
|
203
233
|
}
|
|
204
234
|
},
|
|
205
235
|
async execute(environment, options) {
|
|
236
|
+
if (environment.hosted) {
|
|
237
|
+
try {
|
|
238
|
+
await (0, assert_item_paths_1.assertItemPathsWithinRoot)(service, environment.cwd);
|
|
239
|
+
}
|
|
240
|
+
catch (e) {
|
|
241
|
+
service.status.write('error', (0, log_1.getErrorMessage)(e));
|
|
242
|
+
options.state.set({ type: 'end', reason: 'crash', stateKey: options.stateKey });
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
206
246
|
await (0, docker_service_1.dockerService)(docker, service, options, environment);
|
|
207
247
|
},
|
|
208
248
|
async restore(environment, path) {
|
|
@@ -231,6 +271,7 @@ function dockerServiceRuntime(service, workEnvironment) {
|
|
|
231
271
|
}
|
|
232
272
|
exports.dockerServiceRuntime = dockerServiceRuntime;
|
|
233
273
|
async function archiveContainer(docker, environment, item, path) {
|
|
274
|
+
await (0, execution_steps_1.refuseHostLinks)(item, environment);
|
|
234
275
|
await (0, using_container_1.usingContainer)(docker, item, {
|
|
235
276
|
abortSignal: environment.abortCtrl.signal,
|
|
236
277
|
Image: item.data.image,
|