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.
Files changed (117) hide show
  1. package/build.schema.json +53 -1
  2. package/dist/cache/backends/local-cache-backend.js +10 -2
  3. package/dist/cache/explain.js +13 -11
  4. package/dist/cli.js +3 -0
  5. package/dist/docker/item-network.js +471 -0
  6. package/dist/docker/pull.js +4 -2
  7. package/dist/docker/remove-orphaned-containers.js +13 -6
  8. package/dist/environment/replace-env-variables.js +8 -6
  9. package/dist/executer/await-completed-dependencies.js +2 -1
  10. package/dist/executer/check-readiness.js +2 -2
  11. package/dist/executer/docker-service.js +24 -13
  12. package/dist/executer/docker-task.js +18 -29
  13. package/dist/executer/event-cache.js +4 -0
  14. package/dist/executer/execute-command.js +18 -5
  15. package/dist/executer/execute-docker.js +4 -1
  16. package/dist/executer/execution-steps.js +33 -7
  17. package/dist/executer/get-docker-executor.js +14 -7
  18. package/dist/executer/scheduler/enqueue-next.js +17 -3
  19. package/dist/executer/watch-loop.js +2 -2
  20. package/dist/executer/watch-state-key.js +1 -1
  21. package/dist/file/within-root.js +42 -0
  22. package/dist/harness/connect.js +99 -0
  23. package/dist/harness/format.js +73 -0
  24. package/dist/harness/mcp-command.js +59 -0
  25. package/dist/harness/mcp.js +432 -0
  26. package/dist/harness/opencode.js +492 -0
  27. package/dist/harness/pi.js +217 -0
  28. package/dist/harness/sandbox-client.js +373 -0
  29. package/dist/index.js +33 -36
  30. package/dist/kubernetes/apply.js +31 -1
  31. package/dist/kubernetes/await-running-state.js +33 -1
  32. package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +43 -21
  33. package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +131 -4
  34. package/dist/kubernetes/ensure-kubernetes-service-exists.js +25 -4
  35. package/dist/kubernetes/ensure-persistent-data.js +48 -7
  36. package/dist/kubernetes/get-service-ip.js +15 -1
  37. package/dist/kubernetes/job-placement.js +99 -0
  38. package/dist/kubernetes/job-secrets.js +306 -0
  39. package/dist/kubernetes/kept-claims.js +191 -0
  40. package/dist/kubernetes/kubernetes-instance.js +10 -2
  41. package/dist/kubernetes/network-gate.js +71 -0
  42. package/dist/kubernetes/network-policy.js +526 -0
  43. package/dist/kubernetes/resources.js +13 -2
  44. package/dist/kubernetes/restore-kubernetes-data.js +19 -4
  45. package/dist/kubernetes/secrets.js +2 -0
  46. package/dist/kubernetes/store-kubernetes-data.js +1 -1
  47. package/dist/kubernetes/volumes.js +100 -11
  48. package/dist/main.js +41 -0
  49. package/dist/network/egress-proxy.js +368 -0
  50. package/dist/network/port-relay.js +35 -0
  51. package/dist/optimizer/get-work-cache-stats.js +16 -0
  52. package/dist/optimizer/work-task-cache-description.js +3 -2
  53. package/dist/planner/service-definition.js +5 -0
  54. package/dist/planner/utils/append-work-environment.js +2 -0
  55. package/dist/planner/utils/append-work-service.js +4 -0
  56. package/dist/planner/utils/append-work-task.js +17 -1
  57. package/dist/planner/utils/assert-item-paths.js +50 -0
  58. package/dist/planner/utils/hosted-volumes.js +35 -0
  59. package/dist/planner/work-network.js +193 -0
  60. package/dist/planner/work-runtime-docker.js +41 -0
  61. package/dist/planner/work-runtime-kubernetes.js +335 -102
  62. package/dist/program.js +21 -0
  63. package/dist/remote/annotations.js +103 -0
  64. package/dist/remote/bundle-text.js +24 -0
  65. package/dist/remote/bundle.js +9 -1
  66. package/dist/remote/remote-commands.js +4 -1
  67. package/dist/remote/remote-work-commands.js +182 -69
  68. package/dist/remote/server-client.js +18 -5
  69. package/dist/remote/source-manifest.js +15 -2
  70. package/dist/remote/token-source.js +67 -0
  71. package/dist/schema/build-file-base-task-schema.js +3 -0
  72. package/dist/schema/build-file-container-service-schema.js +6 -0
  73. package/dist/schema/build-file-container-task-schema.js +3 -0
  74. package/dist/schema/build-file-environment-schema.js +9 -0
  75. package/dist/schema/network-schema.js +78 -0
  76. package/dist/schema/reference-parser.js +1 -0
  77. package/dist/schema/schema-parser.js +16 -3
  78. package/dist/schema/work-scope-parser.js +5 -0
  79. package/dist/secrets/command-secret-provider.js +73 -10
  80. package/dist/secrets/github-app-token-command.js +23 -0
  81. package/dist/secrets/github-app-token.js +191 -0
  82. package/dist/server/accounts-api.js +9 -2
  83. package/dist/server/definitions-api.js +69 -47
  84. package/dist/server/definitions.js +76 -51
  85. package/dist/server/executor.js +156 -41
  86. package/dist/server/git-checkout.js +72 -8
  87. package/dist/server/kubernetes-backend.js +554 -0
  88. package/dist/server/kubernetes-sandbox-storage.js +996 -0
  89. package/dist/server/oauth.js +1 -0
  90. package/dist/server/plan.js +51 -1
  91. package/dist/server/policy.js +104 -15
  92. package/dist/server/proposals-api.js +24 -40
  93. package/dist/server/proposals.js +12 -25
  94. package/dist/server/repo-access.js +37 -6
  95. package/dist/server/run-logs.js +217 -0
  96. package/dist/server/run-plan.js +51 -2
  97. package/dist/server/run-request.js +23 -10
  98. package/dist/server/run-store.js +5 -0
  99. package/dist/server/run-worker-command.js +13 -1
  100. package/dist/server/run-worker.js +121 -24
  101. package/dist/server/runs.js +184 -10
  102. package/dist/server/sandbox-docker.js +62 -0
  103. package/dist/server/sandbox-files-api.js +269 -0
  104. package/dist/server/sandbox-files.js +1054 -0
  105. package/dist/server/sandbox-patch.js +481 -0
  106. package/dist/server/sandbox-pod-command.js +15 -0
  107. package/dist/server/sandbox-pod.js +146 -0
  108. package/dist/server/sandbox-storage.js +462 -0
  109. package/dist/server/sandboxes-api.js +210 -0
  110. package/dist/server/sandboxes.js +519 -0
  111. package/dist/server/server-config.js +412 -22
  112. package/dist/server/server.js +204 -39
  113. package/dist/utils/run-id.js +12 -0
  114. package/harness/opencode.ts +11 -0
  115. package/harness/pi.ts +7 -0
  116. package/package.json +8 -2
  117. package/dist/remote/text-diff.js +0 -89
@@ -1,12 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.removeOrphanedContainers = exports.isDockerUnreachable = exports.isProcessAlive = exports.isOrphanedContainer = void 0;
3
+ exports.removeOrphanedContainers = exports.isDockerUnreachable = exports.isProcessAlive = exports.isOfStoppedRun = exports.isOrphanedContainer = void 0;
4
4
  const os_1 = require("os");
5
5
  const remove_container_1 = require("./remove-container");
6
6
  // A hammerkit container nothing will remove any more: a task container (they
7
- // never outlive their run) or a service of a run that is gone, started by a
8
- // hammerkit process of this machine that no longer exists, and the paused
9
- // state records hammerkit kept before 1.9. Services of `up --daemon` and
7
+ // never outlive their run), a service or an egress proxy of a run that is
8
+ // gone, started by a hammerkit process of this machine that no longer exists,
9
+ // and the paused state records hammerkit kept before 1.9. Services of `up --daemon` and
10
10
  // containers of other machines or of running processes stay.
11
11
  function isOrphanedContainer(container, isProcessAlive) {
12
12
  const labels = container.Labels;
@@ -14,9 +14,16 @@ function isOrphanedContainer(container, isProcessAlive) {
14
14
  if (type === 'task' && container.State === 'paused') {
15
15
  return true;
16
16
  }
17
- if (type !== 'task' && type !== 'service') {
17
+ // a proxy goes with the task or service it serves
18
+ if (type !== 'task' && type !== 'service' && type !== 'proxy') {
18
19
  return false;
19
20
  }
21
+ return isOfStoppedRun(labels, isProcessAlive);
22
+ }
23
+ exports.isOrphanedContainer = isOrphanedContainer;
24
+ // Labeled with the run labels of a hammerkit process of this machine that no
25
+ // longer exists, and not kept for `up --daemon`.
26
+ function isOfStoppedRun(labels, isProcessAlive) {
20
27
  if (labels['hammerkit-daemon'] === 'true') {
21
28
  return false;
22
29
  }
@@ -29,7 +36,7 @@ function isOrphanedContainer(container, isProcessAlive) {
29
36
  }
30
37
  return !isProcessAlive(pid);
31
38
  }
32
- exports.isOrphanedContainer = isOrphanedContainer;
39
+ exports.isOfStoppedRun = isOfStoppedRun;
33
40
  function isProcessAlive(pid) {
34
41
  try {
35
42
  process.kill(pid, 0);
@@ -6,9 +6,10 @@ const INTERPOLATION = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
6
6
  // The shell / process environment first, then the .env files. A variable set to
7
7
  // an empty string is set, as in a shell: CI providers leave many empty (a merge
8
8
  // request id on a branch pipeline).
9
- function resolveReference(name, environment, context) {
9
+ function resolveReference(name, environment, context, fromProcess) {
10
10
  const processValue = environment.processEnvs[name];
11
11
  if (processValue !== undefined && processValue !== null) {
12
+ fromProcess?.add(name);
12
13
  return processValue;
13
14
  }
14
15
  for (const envFile of Object.values(context.envFiles)) {
@@ -22,10 +23,11 @@ function resolveReference(name, environment, context) {
22
23
  function buildEnvironmentVariables(envs, environment, context) {
23
24
  const replacements = [];
24
25
  const variables = {};
26
+ const fromProcess = new Set();
25
27
  for (const [key, value] of Object.entries(envs)) {
26
28
  if (value.startsWith('$') && !value.startsWith('${')) {
27
29
  const name = value.substring(1);
28
- const nameValue = resolveReference(name, environment, context);
30
+ const nameValue = resolveReference(name, environment, context, fromProcess);
29
31
  if (nameValue !== null) {
30
32
  replacements.push({ key, name, available: true, value: nameValue });
31
33
  }
@@ -34,7 +36,7 @@ function buildEnvironmentVariables(envs, environment, context) {
34
36
  }
35
37
  }
36
38
  else if (value.includes('${')) {
37
- const interpolated = interpolate(value, envs, environment, context);
39
+ const interpolated = interpolate(value, envs, environment, context, fromProcess);
38
40
  if (interpolated.missing) {
39
41
  replacements.push({ key, name: interpolated.missing, available: false, value: null });
40
42
  }
@@ -46,16 +48,16 @@ function buildEnvironmentVariables(envs, environment, context) {
46
48
  variables[key] = value;
47
49
  }
48
50
  }
49
- return { variables, replacements };
51
+ return { variables, replacements, processNames: [...fromProcess].sort() };
50
52
  }
51
53
  exports.buildEnvironmentVariables = buildEnvironmentVariables;
52
54
  // Each `${NAME}` comes from the process environment or a .env file, else from a
53
55
  // literal value of the same build file / task `envs`, else from its
54
56
  // `:-default`. A reference without any of them is missing.
55
- function interpolate(value, envs, environment, context) {
57
+ function interpolate(value, envs, environment, context, fromProcess) {
56
58
  let missing = null;
57
59
  const result = value.replace(INTERPOLATION, (match, name, fallback) => {
58
- const resolved = resolveReference(name, environment, context) ?? getLiteral(envs, name) ?? fallback ?? null;
60
+ const resolved = resolveReference(name, environment, context, fromProcess) ?? getLiteral(envs, name) ?? fallback ?? null;
59
61
  if (resolved === null) {
60
62
  missing = missing ?? name;
61
63
  return match;
@@ -77,7 +77,8 @@ exports.awaitCompleted = awaitCompleted;
77
77
  async function awaitRunningNeed(work, dep, abort) {
78
78
  const state = await (0, state_resolver_1.isState)('await-running-need-' + work.name, dep.state, isRunningOrDone, abort);
79
79
  if (state.type !== 'running') {
80
- throw new Error(`needed service ${dep.name} ${state.type === 'end' ? 'stopped' : 'failed'} before it was ready`);
80
+ const why = state.type === 'error' ? `: ${state.errorMessage}` : '';
81
+ throw new Error(`needed service ${dep.name} ${state.type === 'end' ? 'stopped' : 'failed'} before it was ready${why}`);
81
82
  }
82
83
  return state;
83
84
  }
@@ -2,8 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.checkReadiness = void 0;
4
4
  const execute_docker_1 = require("./execute-docker");
5
- async function checkReadiness(status, command, environment, container, user, abort) {
6
- const result = await (0, execute_docker_1.execCommand)(status, environment, container, undefined, command, user, 2000, abort);
5
+ async function checkReadiness(status, command, environment, container, user, abort, env) {
6
+ const result = await (0, execute_docker_1.execCommand)(status, environment, container, undefined, command, user, 2000, abort, env);
7
7
  if (result.type === 'timeout') {
8
8
  return false;
9
9
  }
@@ -14,13 +14,13 @@ const remove_container_1 = require("../docker/remove-container");
14
14
  const check_readiness_1 = require("./check-readiness");
15
15
  const main_process_user_1 = require("./main-process-user");
16
16
  const execution_steps_1 = require("./execution-steps");
17
- const docker_task_1 = require("./docker-task");
17
+ const item_network_1 = require("../docker/item-network");
18
18
  const container_secrets_1 = require("./container-secrets");
19
19
  const replace_env_variables_1 = require("../environment/replace-env-variables");
20
- const get_service_containers_1 = require("./get-service-containers");
21
20
  const work_instance_id_1 = require("../planner/work-instance-id");
22
21
  const work_healthcheck_1 = require("../planner/work-healthcheck");
23
22
  const docker_resources_1 = require("./docker-resources");
23
+ const egress_proxy_1 = require("../network/egress-proxy");
24
24
  const HEALTHCHECK_INTERVAL_MS = 1000;
25
25
  function buildServiceCreateOptions(item, options, network, secrets, resources = {}) {
26
26
  const envs = (0, replace_env_variables_1.getEnvironmentVariables)(item.data.envs);
@@ -28,7 +28,7 @@ function buildServiceCreateOptions(item, options, network, secrets, resources =
28
28
  return {
29
29
  Image: item.data.image,
30
30
  Hostname: (0, service_hostname_1.getServiceHostname)(item.name),
31
- Env: Object.entries({ ...envs, ...secrets.env }).map(([key, value]) => `${key}=${value}`),
31
+ Env: Object.entries({ ...envs, ...network.env, ...secrets.env }).map(([key, value]) => `${key}=${value}`),
32
32
  Labels: {
33
33
  app: 'hammerkit',
34
34
  'hammerkit-id': (0, work_instance_id_1.getWorkInstanceId)(item),
@@ -46,8 +46,7 @@ function buildServiceCreateOptions(item, options, network, secrets, resources =
46
46
  Cmd: command.cmd ?? undefined,
47
47
  WorkingDir: (0, execute_docker_1.convertToPosixPath)((0, work_service_1.getServiceWorkingDir)(item.data)),
48
48
  HostConfig: {
49
- ExtraHosts: network.hosts,
50
- Links: network.links,
49
+ ...(0, item_network_1.getNetworkHostConfig)(network),
51
50
  Binds: [
52
51
  ...item.data.src.map((s) => `${s.absolutePath}:${s.absolutePath}`),
53
52
  ...item.data.mounts.map((v) => `${v.localPath}:${(0, execute_docker_1.convertToPosixPath)(v.containerPath)}${v.readOnly ? ':ro' : ''}`),
@@ -56,8 +55,9 @@ function buildServiceCreateOptions(item, options, network, secrets, resources =
56
55
  ],
57
56
  // host ports are for `hammerkit up` and local tasks; container tasks
58
57
  // reach the service over its link, and a run publishing them would
59
- // collide with another run on the same host
60
- PortBindings: options.publishPorts
58
+ // collide with another run on the same host. A limited service's are
59
+ // published by its relay.
60
+ PortBindings: options.publishPorts && !network.relayPorts
61
61
  ? item.data.ports
62
62
  .filter((p) => !!p.hostPort)
63
63
  .reduce((map, port) => {
@@ -72,19 +72,25 @@ function buildServiceCreateOptions(item, options, network, secrets, resources =
72
72
  exports.buildServiceCreateOptions = buildServiceCreateOptions;
73
73
  async function dockerService(docker, item, options, environment) {
74
74
  let container = null;
75
+ let network = null;
75
76
  try {
76
77
  await (0, execution_steps_1.prepareMounts)(item, environment);
77
78
  (0, abort_1.checkForAbort)(options.abort);
78
79
  await (0, execution_steps_1.pullImage)(item, docker, environment);
79
80
  (0, abort_1.checkForAbort)(options.abort);
80
- await (0, execution_steps_1.prepareVolume)(item, docker);
81
+ await (0, execution_steps_1.prepareVolume)(item, docker, environment);
82
+ (0, abort_1.checkForAbort)(options.abort);
83
+ network = await (0, item_network_1.prepareItemNetwork)(docker, item, environment, {
84
+ daemon: options.daemon,
85
+ publish: options.publishPorts ? item.data.ports : [],
86
+ });
81
87
  (0, abort_1.checkForAbort)(options.abort);
82
- const serviceContainers = (0, get_service_containers_1.getServiceContainers)(item.needs);
83
- const network = (0, docker_task_1.getNeedsNetwork)(serviceContainers, item.needs);
84
88
  item.status.write('debug', `create container with image ${item.data.image}`);
85
89
  const secrets = await (0, container_secrets_1.prepareContainerSecrets)(item.data.secrets, (0, work_instance_id_1.getWorkInstanceId)(item), environment);
86
90
  const resources = await (0, docker_resources_1.resolveDockerResources)(docker, item.data.resources, item.status);
87
- container = await docker.createContainer(buildServiceCreateOptions(item, options, network, secrets, resources));
91
+ await (0, execution_steps_1.refuseHostLinks)(item, environment);
92
+ container = await docker.createContainer(buildServiceCreateOptions(item, options, network.options, secrets, resources));
93
+ await network.connect(container);
88
94
  const stream = await container.attach({ stream: true, stdout: true, stderr: true });
89
95
  (0, stream_1.logStream)(item.status, stream);
90
96
  await container.start();
@@ -108,7 +114,7 @@ async function dockerService(docker, item, options, environment) {
108
114
  break;
109
115
  }
110
116
  const user = await (0, main_process_user_1.getMainProcessUser)(container);
111
- ready = await (0, check_readiness_1.checkReadiness)(item.status, (0, work_service_1.getHealthcheckCommand)(item.data) ?? [], environment, container, user, options.abort);
117
+ ready = await (0, check_readiness_1.checkReadiness)(item.status, (0, work_service_1.getHealthcheckCommand)(item.data) ?? [], environment, container, user, options.abort, (0, egress_proxy_1.getProxylessEnvironment)(network.options.env ?? {}));
112
118
  if (!ready && Date.now() >= deadline) {
113
119
  throw new Error((0, work_healthcheck_1.getHealthcheckTimeoutMessage)(item.name, healthcheck));
114
120
  }
@@ -168,9 +174,14 @@ async function dockerService(docker, item, options, environment) {
168
174
  item.status.write('error', `remove of container failed ${(0, log_1.getErrorMessage)(e)}`);
169
175
  }
170
176
  }
171
- // a daemon keeps its secret files until `hammerkit down` stops it
177
+ // a daemon keeps its secret files and network until `hammerkit down` stops it
172
178
  if (!options.daemon) {
173
179
  await (0, container_secrets_1.removeContainerSecrets)((0, work_instance_id_1.getWorkInstanceId)(item));
180
+ if (network) {
181
+ await network
182
+ .release()
183
+ .catch((e) => item.status.write('error', `remove of network failed ${(0, log_1.getErrorMessage)(e)}`));
184
+ }
174
185
  }
175
186
  }
176
187
  }
@@ -1,8 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.dockerTask = exports.buildCreateOptions = exports.getNeedsNetwork = void 0;
3
+ exports.dockerTask = exports.buildCreateOptions = void 0;
4
4
  const run_labels_1 = require("../docker/run-labels");
5
- const service_dns_1 = require("./service-dns");
6
5
  const execute_docker_1 = require("./execute-docker");
7
6
  const abort_1 = require("./abort");
8
7
  const log_1 = require("../log");
@@ -12,32 +11,13 @@ const print_container_options_1 = require("./print-container-options");
12
11
  const tar_1 = require("tar");
13
12
  const replace_env_variables_1 = require("../environment/replace-env-variables");
14
13
  const get_container_binds_1 = require("./get-container-binds");
15
- const get_service_containers_1 = require("./get-service-containers");
14
+ const item_network_1 = require("../docker/item-network");
16
15
  const work_instance_id_1 = require("../planner/work-instance-id");
17
16
  const get_outputs_to_reset_1 = require("../planner/utils/get-outputs-to-reset");
18
17
  const container_secrets_1 = require("./container-secrets");
19
18
  const container_task_state_1 = require("./container-task-state");
20
19
  const docker_resources_1 = require("./docker-resources");
21
- function getNeedsNetwork(serviceContainers, needs) {
22
- const links = [];
23
- const hosts = [];
24
- for (const need of needs) {
25
- const dns = serviceContainers[need.name];
26
- if ((0, service_dns_1.isHostServiceDns)(dns)) {
27
- hosts.push(`${need.name}:${dns.host}`);
28
- }
29
- else {
30
- if (!dns.containerId) {
31
- throw new Error(`service ${need.name} is not running`);
32
- }
33
- links.push(`${dns.containerId}:${need.name}`);
34
- }
35
- }
36
- return { links, hosts };
37
- }
38
- exports.getNeedsNetwork = getNeedsNetwork;
39
- function buildCreateOptions(item, stateKey, serviceContainers, environment, secrets, resources = {}) {
40
- const network = getNeedsNetwork(serviceContainers, item.needs);
20
+ function buildCreateOptions(item, stateKey, network, environment, secrets, resources = {}) {
41
21
  const binds = (0, get_container_binds_1.getContainerBinds)(item);
42
22
  const envs = (0, replace_env_variables_1.getEnvironmentVariables)(item.data.envs);
43
23
  // Running as the host's uid (Linux), the user has no home in the image and
@@ -50,7 +30,7 @@ function buildCreateOptions(item, stateKey, serviceContainers, environment, secr
50
30
  Tty: true,
51
31
  Entrypoint: [item.data.shell],
52
32
  Cmd: ['-c', 'sleep 3600'],
53
- Env: Object.entries({ ...envs, ...home, ...secrets.env }).map(([key, value]) => `${key}=${value}`),
33
+ Env: Object.entries({ ...envs, ...home, ...network.env, ...secrets.env }).map(([key, value]) => `${key}=${value}`),
54
34
  WorkingDir: (0, execute_docker_1.convertToPosixPath)(item.data.cwd),
55
35
  Labels: {
56
36
  app: 'hammerkit',
@@ -64,8 +44,7 @@ function buildCreateOptions(item, stateKey, serviceContainers, environment, secr
64
44
  ...binds.map((b) => `${b.localPath}:${(0, execute_docker_1.convertToPosixPath)(b.containerPath)}${b.readOnly ? ':ro' : ''}`),
65
45
  ...secrets.binds,
66
46
  ],
67
- ExtraHosts: network.hosts,
68
- Links: network.links,
47
+ ...(0, item_network_1.getNetworkHostConfig)(network),
69
48
  AutoRemove: true,
70
49
  ...resources,
71
50
  },
@@ -74,6 +53,7 @@ function buildCreateOptions(item, stateKey, serviceContainers, environment, secr
74
53
  exports.buildCreateOptions = buildCreateOptions;
75
54
  async function dockerTask(docker, item, environment, options) {
76
55
  item.status.write('info', `execute ${item.name} in container`);
56
+ let network = null;
77
57
  try {
78
58
  // A run that fails overwrites outputs, so the previous record goes first,
79
59
  // and only a run that succeeded writes a new one.
@@ -87,14 +67,18 @@ async function dockerTask(docker, item, environment, options) {
87
67
  (0, abort_1.checkForAbort)(options.abort);
88
68
  await (0, execution_steps_1.pullImage)(item, docker, environment);
89
69
  (0, abort_1.checkForAbort)(options.abort);
90
- await (0, execution_steps_1.prepareVolume)(item, docker);
70
+ await (0, execution_steps_1.prepareVolume)(item, docker, environment);
71
+ (0, abort_1.checkForAbort)(options.abort);
72
+ network = await (0, item_network_1.prepareItemNetwork)(docker, item, environment, { daemon: false });
91
73
  (0, abort_1.checkForAbort)(options.abort);
92
- const serviceContainers = (0, get_service_containers_1.getServiceContainers)(item.needs);
93
74
  const secrets = await (0, container_secrets_1.prepareContainerSecrets)(item.data.secrets, (0, work_instance_id_1.getWorkInstanceId)(item), environment);
94
75
  const resources = await (0, docker_resources_1.resolveDockerResources)(docker, item.data.resources, item.status);
95
- const containerOptions = buildCreateOptions(item, options.stateKey, serviceContainers, environment, secrets, resources);
76
+ const containerOptions = buildCreateOptions(item, options.stateKey, network.options, environment, secrets, resources);
96
77
  (0, print_container_options_1.printContainerOptions)(item.status, containerOptions);
78
+ await (0, execution_steps_1.refuseHostLinks)(item, environment);
79
+ const itemNetwork = network;
97
80
  const succeeded = await (0, using_container_1.usingContainer)(docker, item, containerOptions, async (container) => {
81
+ await itemNetwork.connect(container);
98
82
  await (0, execution_steps_1.setUserPermissions)(item, container, environment);
99
83
  for (const generate of outputsToReset.filter((g) => !g.isFile)) {
100
84
  await (0, execute_docker_1.clearContainerDirectory)(item.status, environment, container, generate.path);
@@ -110,6 +94,7 @@ async function dockerTask(docker, item, environment, options) {
110
94
  throw new abort_1.AbortError();
111
95
  }
112
96
  if (result.result.ExitCode !== 0) {
97
+ await (0, execution_steps_1.refuseHostLinks)(item, environment);
113
98
  await exportGenerates(environment, container, item.data.generates.filter((g) => g.exportAlways));
114
99
  options.state.set({
115
100
  stateKey: options.stateKey,
@@ -119,6 +104,7 @@ async function dockerTask(docker, item, environment, options) {
119
104
  return false;
120
105
  }
121
106
  }
107
+ await (0, execution_steps_1.refuseHostLinks)(item, environment);
122
108
  await exportGenerates(environment, container, item.data.generates.filter((g) => g.export));
123
109
  return true;
124
110
  });
@@ -143,6 +129,9 @@ async function dockerTask(docker, item, environment, options) {
143
129
  }
144
130
  finally {
145
131
  await (0, container_secrets_1.removeContainerSecrets)((0, work_instance_id_1.getWorkInstanceId)(item));
132
+ if (network) {
133
+ await network.release().catch((e) => item.status.write('error', `remove of network failed ${(0, log_1.getErrorMessage)(e)}`));
134
+ }
146
135
  }
147
136
  }
148
137
  exports.dockerTask = dockerTask;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.cleanCache = exports.storeCache = exports.getArchivePaths = exports.restoreCache = void 0;
4
4
  const resolve_git_source_1 = require("../includes/resolve-git-source");
5
5
  const remove_orphaned_containers_1 = require("../docker/remove-orphaned-containers");
6
+ const item_network_1 = require("../docker/item-network");
6
7
  const execute_docker_1 = require("./execute-docker");
7
8
  const plan_work_tasks_1 = require("../planner/utils/plan-work-tasks");
8
9
  const get_cache_directory_1 = require("../optimizer/get-cache-directory");
@@ -102,6 +103,9 @@ async function cleanCache(workTree, environment, options) {
102
103
  for (const id of removed) {
103
104
  environment.console.info(`removed container ${id.substring(0, 12)} left behind by a stopped run`);
104
105
  }
106
+ for (const name of await (0, item_network_1.removeOrphanedNetworks)((0, execute_docker_1.getContainerCli)(workTree.environment))) {
107
+ environment.console.info(`removed network ${name} left behind by a stopped run`);
108
+ }
105
109
  }
106
110
  }
107
111
  exports.cleanCache = cleanCache;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.executeCommand = void 0;
3
+ exports.executeCommand = exports.processEnv = void 0;
4
4
  const child_process_1 = require("child_process");
5
5
  const os_1 = require("os");
6
6
  const log_1 = require("../log");
@@ -12,17 +12,30 @@ const environment_config_1 = require("../utils/environment-config");
12
12
  function getStopTimeout() {
13
13
  return (0, environment_config_1.getEnvironmentConfig)('HAMMERKIT_STOP_TIMEOUT_MS', 10000);
14
14
  }
15
+ // A variable of the process. Windows ignores the case of their names and calls PATH
16
+ // `Path`, which a copy of process.env (`{ ...process.env }`) keeps.
17
+ function processEnv(envs, name, windows) {
18
+ if (envs[name] !== undefined || !windows) {
19
+ return envs[name];
20
+ }
21
+ const key = Object.keys(envs).find((key) => key.toUpperCase() === name.toUpperCase());
22
+ return key !== undefined ? envs[key] : undefined;
23
+ }
24
+ exports.processEnv = processEnv;
15
25
  // powershell takes about half a minute to start when it has to work out its module
16
- // path itself, so the one of the process is passed on
26
+ // path or the PATH itself, so the ones of the process are passed on
17
27
  function shellEnvs(windows, environment) {
18
- const value = environment.processEnvs['PSModulePath'];
19
- return windows && value !== undefined ? { PSModulePath: value } : {};
28
+ const value = processEnv(environment.processEnvs, 'PSModulePath', windows);
29
+ return {
30
+ ...(windows && value !== undefined ? { PSModulePath: value } : {}),
31
+ PATH: processEnv(environment.processEnvs, 'PATH', windows),
32
+ };
20
33
  }
21
34
  async function executeCommand(status, abortSignal, cwd, command, envs, environment) {
22
35
  const windows = (0, os_1.platform)() === 'win32';
23
36
  return new Promise((resolve, reject) => {
24
37
  const ps = (0, child_process_1.spawn)(command, {
25
- env: { ...envs, ...shellEnvs(windows, environment), PATH: environment.processEnvs['PATH'] },
38
+ env: { ...envs, ...shellEnvs(windows, environment) },
26
39
  cwd,
27
40
  shell: windows ? 'powershell.exe' : true,
28
41
  // its own process group, so an abort reaches every process the command
@@ -66,10 +66,13 @@ function convertToPosixPath(path) {
66
66
  return path;
67
67
  }
68
68
  exports.convertToPosixPath = convertToPosixPath;
69
- async function execCommand(status, environment, container, cwd, cmd, user, timeout, abort) {
69
+ async function execCommand(status, environment, container, cwd, cmd, user, timeout, abort,
70
+ // set for this command only, on top of the container's
71
+ env) {
70
72
  const abortController = new AbortController();
71
73
  const exec = await container.exec({
72
74
  Cmd: cmd,
75
+ Env: env ? Object.entries(env).map(([key, value]) => `${key}=${value}`) : undefined,
73
76
  WorkingDir: cwd,
74
77
  Tty: false,
75
78
  AttachStdout: true,
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.setUserPermissions = exports.prepareMounts = exports.prepareVolume = exports.pullImage = void 0;
3
+ exports.setUserPermissions = exports.prepareMounts = exports.prepareVolume = exports.pullImage = exports.refuseHostLinks = exports.hostedRoot = void 0;
4
4
  const pull_1 = require("../docker/pull");
5
5
  const get_docker_executor_1 = require("./get-docker-executor");
6
6
  const path_1 = require("path");
@@ -8,20 +8,42 @@ const set_user_permission_1 = require("./set-user-permission");
8
8
  const work_task_1 = require("../planner/work-task");
9
9
  const work_service_1 = require("../planner/work-service");
10
10
  const get_container_binds_1 = require("./get-container-binds");
11
+ const assert_item_paths_1 = require("../planner/utils/assert-item-paths");
12
+ // The files a server run works on: its checkout, or a sandbox. Somebody else wrote them.
13
+ function hostedRoot(environment) {
14
+ return environment.hosted ? environment.cwd : undefined;
15
+ }
16
+ exports.hostedRoot = hostedRoot;
17
+ /**
18
+ * On a server, every path of the host an item binds into its container, or the engine
19
+ * reads or writes for it, has to stay inside the run's files with no link on the way:
20
+ * tasks of this or an earlier run on a sandbox, or the commit of a ref, could have left
21
+ * one pointing anywhere on the server. The runtime checks before an item runs; this is
22
+ * checked again right before a container is made and before outputs are copied, after
23
+ * other tasks of the run had their turn.
24
+ */
25
+ async function refuseHostLinks(item, environment) {
26
+ const root = hostedRoot(environment);
27
+ if (root) {
28
+ await (0, assert_item_paths_1.assertItemPathsWithinRoot)(item, root);
29
+ }
30
+ }
31
+ exports.refuseHostLinks = refuseHostLinks;
11
32
  async function pullImage(item, docker, environment) {
12
33
  await (0, pull_1.pull)(item.status, docker, item.data.image, environment);
13
34
  }
14
35
  exports.pullImage = pullImage;
15
- async function prepareVolume(item, docker) {
36
+ async function prepareVolume(item, docker, environment) {
37
+ const labels = (0, get_docker_executor_1.volumeLabels)(environment ? hostedRoot(environment) : undefined);
16
38
  if ((0, work_service_1.isContainerWorkService)(item.data)) {
17
39
  for (const volume of item.data.volumes) {
18
40
  if (volume.resetOnChange && !volume.inherited) {
19
41
  item.status.write('debug', 'recreate volume');
20
- await (0, get_docker_executor_1.recreateVolume)(docker, item.status, volume.name);
42
+ await (0, get_docker_executor_1.recreateVolume)(docker, item.status, volume.name, labels);
21
43
  }
22
44
  else {
23
45
  item.status.write('debug', 'ensure volume exists');
24
- await (0, get_docker_executor_1.ensureVolumeExists)(docker, item.status, volume.name);
46
+ await (0, get_docker_executor_1.ensureVolumeExists)(docker, item.status, volume.name, labels);
25
47
  }
26
48
  }
27
49
  }
@@ -32,7 +54,7 @@ async function prepareVolume(item, docker) {
32
54
  continue;
33
55
  }
34
56
  item.status.write('debug', 'ensure volume exists');
35
- await (0, get_docker_executor_1.ensureVolumeExists)(docker, item.status, generate.volumeName);
57
+ await (0, get_docker_executor_1.ensureVolumeExists)(docker, item.status, generate.volumeName, labels);
36
58
  }
37
59
  }
38
60
  }
@@ -66,8 +88,12 @@ async function prepareMounts(item, environment) {
66
88
  exports.prepareMounts = prepareMounts;
67
89
  async function setUserPermissions(item, container, environment) {
68
90
  if (item.data.user) {
69
- const binds = (0, get_container_binds_1.getContainerBinds)(item);
70
- await (0, set_user_permission_1.setUserPermission)(item.data.cwd, item.status, environment, container, item.data.user);
91
+ // On a server only the volumes: a chown as the container's root on a path of the host
92
+ // would follow a link a task put there after the binds were checked
93
+ const binds = (0, get_container_binds_1.getContainerBinds)(item).filter((bind) => !environment.hosted || !(0, path_1.isAbsolute)(bind.localPath));
94
+ if (!environment.hosted) {
95
+ await (0, set_user_permission_1.setUserPermission)(item.data.cwd, item.status, environment, container, item.data.user);
96
+ }
71
97
  for (const bind of binds) {
72
98
  await (0, set_user_permission_1.setUserPermission)(bind.containerPath, item.status, environment, container, item.data.user);
73
99
  }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.recreateVolume = exports.removeVolume = exports.createVolume = exports.ensureVolumeExists = exports.existsVolume = void 0;
3
+ exports.recreateVolume = exports.removeVolume = exports.createVolume = exports.ensureVolumeExists = exports.volumeLabels = exports.VOLUME_ROOT_LABEL = exports.existsVolume = void 0;
4
4
  const log_1 = require("../log");
5
5
  async function existsVolume(docker, volumeName) {
6
6
  try {
@@ -12,22 +12,29 @@ async function existsVolume(docker, volumeName) {
12
12
  }
13
13
  }
14
14
  exports.existsVolume = existsVolume;
15
- async function ensureVolumeExists(docker, scopedConsole, volumeName) {
15
+ // The label a volume gets when a server run made it for outputs in its files (a sandbox's),
16
+ // so the server finds it again to measure it and to remove it with the sandbox.
17
+ exports.VOLUME_ROOT_LABEL = 'hammerkit-root';
18
+ function volumeLabels(root) {
19
+ return root ? { [exports.VOLUME_ROOT_LABEL]: root } : {};
20
+ }
21
+ exports.volumeLabels = volumeLabels;
22
+ async function ensureVolumeExists(docker, scopedConsole, volumeName, labels = {}) {
16
23
  const volumeExists = await existsVolume(docker, volumeName);
17
24
  if (!volumeExists) {
18
- await createVolume(docker, scopedConsole, volumeName);
25
+ await createVolume(docker, scopedConsole, volumeName, labels);
19
26
  }
20
27
  else {
21
28
  scopedConsole.write('debug', 'volume exists ' + volumeName);
22
29
  }
23
30
  }
24
31
  exports.ensureVolumeExists = ensureVolumeExists;
25
- async function createVolume(docker, scopedConsole, volumeName) {
32
+ async function createVolume(docker, scopedConsole, volumeName, labels = {}) {
26
33
  scopedConsole.write('debug', 'create volume ' + volumeName);
27
34
  await docker.createVolume({
28
35
  Name: volumeName,
29
36
  Driver: 'local',
30
- Labels: { app: 'hammerkit' },
37
+ Labels: { ...labels, app: 'hammerkit' },
31
38
  });
32
39
  }
33
40
  exports.createVolume = createVolume;
@@ -44,9 +51,9 @@ async function removeVolume(docker, scopedConsole, volumeName) {
44
51
  }
45
52
  }
46
53
  exports.removeVolume = removeVolume;
47
- async function recreateVolume(docker, scopedConsole, volumeName) {
54
+ async function recreateVolume(docker, scopedConsole, volumeName, labels = {}) {
48
55
  await removeVolume(docker, scopedConsole, volumeName);
49
- await createVolume(docker, scopedConsole, volumeName);
56
+ await createVolume(docker, scopedConsole, volumeName, labels);
50
57
  }
51
58
  exports.recreateVolume = recreateVolume;
52
59
  //# sourceMappingURL=get-docker-executor.js.map
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.checkCacheState = void 0;
3
+ exports.checkCacheState = exports.UNCACHED_STATE_KEY = void 0;
4
4
  const work_item_1 = require("../../planner/work-item");
5
5
  const get_cache_directory_1 = require("../../optimizer/get-cache-directory");
6
6
  const cache_metadata_1 = require("../cache-metadata");
@@ -8,7 +8,21 @@ const work_task_cache_description_1 = require("../../optimizer/work-task-cache-d
8
8
  const log_1 = require("../../log");
9
9
  const state_key_1 = require("./state-key");
10
10
  const work_instance_id_1 = require("../../planner/work-instance-id");
11
- async function checkCacheState(item, defaultCacheMethod, environment) {
11
+ // The key a task whose cache is off runs and records its outputs under. It is
12
+ // no hash, so a later run with its cache on never takes those outputs as its own.
13
+ exports.UNCACHED_STATE_KEY = 'uncached';
14
+ async function checkCacheState(item, defaultCacheMethod, environment,
15
+ // watch mode compares keys to rerun on a change, so it needs one even when
16
+ // the cache is off
17
+ options = {}) {
18
+ // A task whose cache is off is never looked up, so hashing its sources buys
19
+ // nothing, and a `**` over a node_modules takes most of a minute (every run
20
+ // with arguments). Its dependents still hash its sources for their own key.
21
+ const declared = (0, state_key_1.resolveEffective)(item, defaultCacheMethod);
22
+ if (declared.method === 'none' && (0, work_item_1.isWorkTaskItem)(item) && !options.watch) {
23
+ item.status.write('debug', `${item.name} is skipping cache check, because caching is disabled`);
24
+ return { cached: false, stateKey: exports.UNCACHED_STATE_KEY, resolved: declared, provable: false };
25
+ }
12
26
  const { stateKey, stats: currentStats, resolved, provable, unmatched, } = await (0, state_key_1.computeStateKey)(item, defaultCacheMethod, environment);
13
27
  for (const src of unmatched) {
14
28
  item.status.write('warn', `src "${src.source}" matches no files`);
@@ -31,7 +45,7 @@ async function checkCacheState(item, defaultCacheMethod, environment) {
31
45
  const pulled = await resolved.backend.pull(item.id(), stateKey, cacheDir, environment);
32
46
  if (pulled) {
33
47
  item.status.write('info', `${item.name} pulled from cache "${resolved.name}" (${resolved.backend.type})`);
34
- await item.runtime.restore(environment, cacheDir);
48
+ await item.runtime.restore(environment, cacheDir, stateKey);
35
49
  await (0, cache_metadata_1.writeCacheMetadata)(environment, item, currentStats, (0, work_task_cache_description_1.getWorkTaskCacheDescription)(item));
36
50
  return { cached: true, stateKey, resolved, provable };
37
51
  }
@@ -8,12 +8,12 @@ const state_resolver_1 = require("./state-resolver");
8
8
  const abort_1 = require("./abort");
9
9
  const enqueue_next_1 = require("./scheduler/enqueue-next");
10
10
  async function watchLoop(work, environment, options, factory) {
11
- const currentStats = await (0, enqueue_next_1.checkCacheState)(work, options.cacheDefault, environment);
12
- (0, abort_1.checkForAbort)(environment.abortCtrl.signal);
13
11
  // continuous lives on container-services and on tasks (both task types); a
14
12
  // kubernetes-service has no such field. Read it wherever it exists.
15
13
  const continuous = 'continuous' in work.data ? work.data.continuous : false;
16
14
  const watchMode = options.watch && !options.daemon && !continuous;
15
+ const currentStats = await (0, enqueue_next_1.checkCacheState)(work, options.cacheDefault, environment, { watch: watchMode });
16
+ (0, abort_1.checkForAbort)(environment.abortCtrl.signal);
17
17
  const watchState = watchMode
18
18
  ? (0, watch_state_key_1.watchStateKey)(work, currentStats, environment, options)
19
19
  : new state_1.State(currentStats);
@@ -19,7 +19,7 @@ function watchStateKey(item, cacheStats, environment, options) {
19
19
  if (environment.abortCtrl.signal.aborted) {
20
20
  return;
21
21
  }
22
- const cacheState = await (0, enqueue_next_1.checkCacheState)(item, options.cacheDefault, environment);
22
+ const cacheState = await (0, enqueue_next_1.checkCacheState)(item, options.cacheDefault, environment, { watch: true });
23
23
  if (state.current.stateKey !== cacheState.stateKey) {
24
24
  state.set(cacheState);
25
25
  }