hammerkit 1.7.0 → 1.9.0

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 (53) hide show
  1. package/README.md +3 -3
  2. package/build.schema.json +19 -13
  3. package/dist/docker/package.js +13 -3
  4. package/dist/docker/remove-orphaned-containers.js +53 -0
  5. package/dist/docker/run-labels.js +14 -0
  6. package/dist/docker/using-container.js +25 -26
  7. package/dist/executer/check-readiness.js +4 -4
  8. package/dist/executer/container-task-state.js +34 -0
  9. package/dist/executer/docker-service.js +23 -6
  10. package/dist/executer/docker-task.js +26 -4
  11. package/dist/executer/event-cache.js +8 -0
  12. package/dist/executer/execute-command.js +36 -6
  13. package/dist/executer/execute-docker.js +18 -2
  14. package/dist/executer/execute-work-service.js +4 -0
  15. package/dist/executer/execute-work-task.js +17 -2
  16. package/dist/executer/execution-steps.js +3 -8
  17. package/dist/executer/get-container-binds.js +13 -1
  18. package/dist/executer/main-process-user.js +52 -0
  19. package/dist/index.js +15 -3
  20. package/dist/kubernetes/apply.js +6 -7
  21. package/dist/kubernetes/await-running-state.js +48 -44
  22. package/dist/kubernetes/ensure-http-route.js +7 -10
  23. package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +27 -6
  24. package/dist/kubernetes/ensure-namespace.js +9 -10
  25. package/dist/kubernetes/ensure-persistent-data.js +6 -5
  26. package/dist/kubernetes/get-service-ip.js +2 -2
  27. package/dist/kubernetes/kubernetes-instance.js +0 -4
  28. package/dist/kubernetes/remove-persistent-data.js +16 -6
  29. package/dist/kubernetes/resolve-pod-name.js +7 -7
  30. package/dist/planner/unreferenced-build-file-envs.js +35 -0
  31. package/dist/planner/utils/append-work-environment.js +1 -1
  32. package/dist/planner/utils/append-work-service.js +26 -2
  33. package/dist/planner/utils/get-outputs-to-reset.js +20 -0
  34. package/dist/planner/utils/parse-work-generate.js +2 -2
  35. package/dist/planner/validate.js +10 -0
  36. package/dist/planner/work-healthcheck.js +8 -0
  37. package/dist/planner/work-instance-id.js +3 -3
  38. package/dist/planner/work-runtime-docker.js +19 -19
  39. package/dist/planner/work-runtime-kubernetes.js +66 -48
  40. package/dist/planner/work-runtime-local.js +22 -5
  41. package/dist/planner/work-service-id.js +3 -0
  42. package/dist/planner/work-service.js +27 -1
  43. package/dist/program.js +6 -6
  44. package/dist/schema/build-file-container-service-schema.js +8 -0
  45. package/dist/schema/build-file-environment-schema-http-route.js +15 -0
  46. package/dist/schema/build-file-environment-schema.js +8 -2
  47. package/dist/schema/build-file-schema.js +1 -1
  48. package/dist/schema/build-file-service-container-healthcheck.js +4 -0
  49. package/dist/schema/envs-schema.js +1 -1
  50. package/dist/utils/abort-on-signals.js +17 -0
  51. package/package.json +9 -9
  52. package/dist/kubernetes/ensure-ingress.js +0 -54
  53. package/dist/schema/build-file-environment-schema-ingress.js +0 -14
@@ -51,6 +51,15 @@ function withDeadline(parent, timeout) {
51
51
  },
52
52
  };
53
53
  }
54
+ // A task without cmds only groups its dependencies (`ci: { deps: [test, build] }`).
55
+ // Having no src of its own it is never a cache hit, so its dependencies are
56
+ // always checked and their outputs restored. When every one of them was a hit,
57
+ // nothing ran: report the group as cached too. Reporting only, the cache
58
+ // decision is untouched.
59
+ function onlyGroupsCacheHits(work) {
60
+ return (work.data.cmds.length === 0 &&
61
+ work.deps.every((dep) => dep.state.current.type === 'completed' && dep.state.current.cached));
62
+ }
54
63
  async function executeWorkTask(work, environment, options,
55
64
  // when set, the task was not requested and only runs if one of these tasks
56
65
  // depending on it misses the cache (see executeWorkTree)
@@ -139,12 +148,13 @@ dependents = null) {
139
148
  if (work.state.current.type === 'running') {
140
149
  work.status.write('debug', 'completed for state key ' + cacheState.stateKey);
141
150
  await pushToBackend(work, environment, cacheState, options);
151
+ const cached = onlyGroupsCacheHits(work);
142
152
  work.state.set({
143
153
  stateKey: cacheState.stateKey,
144
154
  type: 'completed',
145
- cached: false,
155
+ cached,
146
156
  duration: (0, states_1.getDuration)(started),
147
- missCauses,
157
+ missCauses: cached ? undefined : missCauses,
148
158
  });
149
159
  }
150
160
  else {
@@ -170,6 +180,11 @@ dependents = null) {
170
180
  errorMessage: (0, log_1.getErrorMessage)(e),
171
181
  stateKey: null,
172
182
  });
183
+ // Stop the run like a failing command does: dependents wait for this task
184
+ // to complete, which it now never will.
185
+ if (!options.watch) {
186
+ environment.abortCtrl.abort();
187
+ }
173
188
  }
174
189
  }
175
190
  }
@@ -26,18 +26,13 @@ async function prepareVolume(item, docker) {
26
26
  }
27
27
  }
28
28
  if ((0, work_task_1.isContainerWorkTask)(item.data)) {
29
+ // a task's outputs are emptied inside its container (see dockerTask)
29
30
  for (const generate of item.data.generates) {
30
31
  if (generate.isFile) {
31
32
  continue;
32
33
  }
33
- if (generate.resetOnChange && !generate.inherited) {
34
- item.status.write('debug', 'recreate volume');
35
- await (0, get_docker_executor_1.recreateVolume)(docker, item.status, generate.volumeName);
36
- }
37
- else {
38
- item.status.write('debug', 'ensure volume exists');
39
- await (0, get_docker_executor_1.ensureVolumeExists)(docker, item.status, generate.volumeName);
40
- }
34
+ item.status.write('debug', 'ensure volume exists');
35
+ await (0, get_docker_executor_1.ensureVolumeExists)(docker, item.status, generate.volumeName);
41
36
  }
42
37
  }
43
38
  }
@@ -1,10 +1,22 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getContainerBinds = void 0;
4
+ const path_1 = require("path");
5
+ function isInside(path, parent) {
6
+ const rel = (0, path_1.relative)(parent, path);
7
+ return rel !== '' && !rel.startsWith('..') && !(0, path_1.isAbsolute)(rel);
8
+ }
4
9
  function getContainerBinds(item) {
10
+ const sources = item.data.src.map((s) => s.absolutePath);
5
11
  const items = [
6
12
  ...item.data.mounts,
7
- ...item.data.src.map((s) => ({ localPath: s.absolutePath, containerPath: s.absolutePath })),
13
+ // A source inside another source (a dependency's `app/app.csproj` under the
14
+ // task's own `app`) is already visible through the outer bind. Binding it
15
+ // again would nest bind mounts, which Docker Desktop then refuses to let the
16
+ // host delete while the container exists.
17
+ ...sources
18
+ .filter((path) => !sources.some((other) => isInside(path, other)))
19
+ .map((path) => ({ localPath: path, containerPath: path })),
8
20
  ...item.data.generates.filter((g) => !g.isFile).map((v) => ({ localPath: v.volumeName, containerPath: v.path })),
9
21
  ...item.data.generates.filter((g) => g.isFile).map((v) => ({ localPath: v.path, containerPath: v.path })),
10
22
  ];
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getMainProcessUser = exports.parseProcessStatusUser = void 0;
4
+ const stream_1 = require("stream");
5
+ // `uid:gid` from the text of /proc/<pid>/status (effective ids), or null.
6
+ function parseProcessStatusUser(status) {
7
+ const uid = status.match(/^Uid:\s+\d+\s+(\d+)/m);
8
+ const gid = status.match(/^Gid:\s+\d+\s+(\d+)/m);
9
+ if (!uid || !gid) {
10
+ return null;
11
+ }
12
+ return `${uid[1]}:${gid[1]}`;
13
+ }
14
+ exports.parseProcessStatusUser = parseProcessStatusUser;
15
+ // The user the container's main process (PID 1) runs as, read inside the
16
+ // container so user namespaces and rootless docker report the container's ids.
17
+ // Healthchecks run as this user, like a Kubernetes exec probe runs as the
18
+ // container's user: a check run as root could otherwise create files the
19
+ // service then cannot read (RabbitMQ's Erlang cookie). Null when it can't be
20
+ // read (no `cat` in the image, a windows container); the check then runs as
21
+ // the exec default, as before.
22
+ async function getMainProcessUser(container, timeoutMs = 2000) {
23
+ try {
24
+ const exec = await container.exec({ Cmd: ['cat', '/proc/1/status'], AttachStdout: true, AttachStderr: true });
25
+ const stream = await exec.start({ hijack: true, stdin: false });
26
+ const stdout = new stream_1.PassThrough();
27
+ const chunks = [];
28
+ stdout.on('data', (chunk) => chunks.push(chunk));
29
+ container.modem.demuxStream(stream, stdout, new stream_1.PassThrough());
30
+ const finished = await Promise.race([
31
+ new Promise((resolve) => {
32
+ stream.on('end', () => resolve(true));
33
+ stream.on('error', () => resolve(false));
34
+ }),
35
+ new Promise((resolve) => setTimeout(() => resolve(false), timeoutMs)),
36
+ ]);
37
+ if (!finished) {
38
+ stream.destroy();
39
+ return null;
40
+ }
41
+ const result = await exec.inspect();
42
+ if (result.ExitCode !== 0) {
43
+ return null;
44
+ }
45
+ return parseProcessStatusUser(Buffer.concat(chunks).toString('utf8'));
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ }
51
+ exports.getMainProcessUser = getMainProcessUser;
52
+ //# sourceMappingURL=main-process-user.js.map
package/dist/index.js CHANGED
@@ -6,9 +6,18 @@ const get_file_context_1 = require("./file/get-file-context");
6
6
  const work_item_status_1 = require("./planner/work-item-status");
7
7
  const empty_writable_1 = require("./utils/empty-writable");
8
8
  const run_program_1 = require("./run-program");
9
+ const abort_on_signals_1 = require("./utils/abort-on-signals");
9
10
  const abortCtrl = new AbortController();
10
- process.on('SIGINT', function () {
11
- abortCtrl.abort();
11
+ (0, abort_on_signals_1.abortOnSignals)(process, abortCtrl);
12
+ let settled = false;
13
+ // A run that never settles (a task waiting on something that can't happen)
14
+ // lets the event loop drain, and Node then exits 0: CI would report success
15
+ // for a build that never finished. Fail loudly instead.
16
+ process.on('beforeExit', () => {
17
+ if (!settled) {
18
+ process.stderr.write('hammerkit stopped before the run finished\n');
19
+ process.exitCode = 1;
20
+ }
12
21
  });
13
22
  (0, run_program_1.runProgram)({
14
23
  cwd: process.cwd(),
@@ -20,7 +29,10 @@ process.on('SIGINT', function () {
20
29
  stdout: process.stdout,
21
30
  stderr: process.stderr,
22
31
  stdoutColumns: process.stdout.columns,
23
- }, process.argv, false).catch(() => {
32
+ }, process.argv, false).then(() => {
33
+ settled = true;
34
+ }, () => {
35
+ settled = true;
24
36
  process.exit(1);
25
37
  });
26
38
  //# sourceMappingURL=index.js.map
@@ -1,21 +1,21 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.apply = exports.statusCodeOf = void 0;
4
+ // The client throws an ApiException carrying the HTTP status as `code`; watch
5
+ // errors carry it as `statusCode`.
4
6
  function statusCodeOf(e) {
5
7
  const err = e;
6
- return err?.statusCode ?? err?.response?.statusCode ?? err?.body?.code;
8
+ return typeof err?.code === 'number' ? err.code : err?.statusCode;
7
9
  }
8
10
  exports.statusCodeOf = statusCodeOf;
9
11
  async function apply(instance, spec) {
10
12
  try {
11
13
  await instance.objectApi.read(spec);
12
- const response = await instance.objectApi.patch(spec);
13
- return response.body;
14
+ return await instance.objectApi.patch(spec);
14
15
  }
15
16
  catch (e) {
16
17
  try {
17
- const response = await instance.objectApi.create(spec);
18
- return response.body;
18
+ return await instance.objectApi.create(spec);
19
19
  }
20
20
  catch (createError) {
21
21
  // Two work items can apply the same object concurrently (e.g. a volume
@@ -23,8 +23,7 @@ async function apply(instance, spec) {
23
23
  // lost the create race, the object now exists — read it back instead of
24
24
  // failing the whole item.
25
25
  if (statusCodeOf(createError) === 409) {
26
- const response = await instance.objectApi.read(spec);
27
- return response.body;
26
+ return await instance.objectApi.read(spec);
28
27
  }
29
28
  throw createError;
30
29
  }
@@ -4,25 +4,21 @@ exports.deleteJobAndWait = exports.awaitDeployRunningState = exports.deleteJob =
4
4
  const abort_1 = require("../executer/abort");
5
5
  const abort_event_1 = require("../utils/abort-event");
6
6
  const apply_1 = require("./apply");
7
- function awaitRunningState(instance, env, name, phase) {
8
- return new Promise((resolve, reject) => {
9
- const req = instance.watch.watch(`/api/v1/namespaces/${env.namespace}/pods`, {}, (type, obj) => {
10
- if (obj.metadata.name === name && obj.metadata.namespace === env.namespace && obj.status.phase === phase) {
11
- req.then((r) => r.abort());
12
- resolve();
13
- }
14
- }, (err) => {
15
- if (err.message === 'aborted') {
16
- return;
17
- }
18
- if (err) {
19
- reject(err);
20
- }
21
- else {
22
- resolve();
23
- }
24
- });
25
- });
7
+ const sleep_1 = require("../utils/sleep");
8
+ // The waits below poll instead of using the client's Watch: Watch aborts every
9
+ // request after 30s, which a pod or deployment still pulling its image outlasts.
10
+ async function awaitRunningState(instance, env, name, phase, pollInterval = 1000) {
11
+ for (;;) {
12
+ const pod = await instance.coreApi.readNamespacedPod({ name, namespace: env.namespace });
13
+ const current = pod.status?.phase;
14
+ if (current === phase) {
15
+ return;
16
+ }
17
+ if (current === 'Succeeded' || current === 'Failed') {
18
+ throw new Error(`pod ${name} ended in phase ${current} before reaching ${phase}`);
19
+ }
20
+ await (0, sleep_1.sleep)(pollInterval);
21
+ }
26
22
  }
27
23
  exports.awaitRunningState = awaitRunningState;
28
24
  // Wait for a job to finish by polling its status: resolves once it succeeded,
@@ -35,8 +31,8 @@ async function awaitJobCompletion(instance, env, name, abort, pollInterval = 100
35
31
  await deleteJob(instance, env, name);
36
32
  throw new abort_1.AbortError();
37
33
  }
38
- const job = await instance.batchApi.readNamespacedJobStatus(name, env.namespace);
39
- const status = job.body.status;
34
+ const job = await instance.batchApi.readNamespacedJobStatus({ name, namespace: env.namespace });
35
+ const status = job.status;
40
36
  if (status?.succeeded) {
41
37
  return;
42
38
  }
@@ -66,7 +62,12 @@ function sleep(ms, abort) {
66
62
  }
67
63
  async function deleteJob(instance, env, name) {
68
64
  try {
69
- await instance.batchApi.deleteNamespacedJob(name, env.namespace, undefined, undefined, 0, undefined, 'Background');
65
+ await instance.batchApi.deleteNamespacedJob({
66
+ name,
67
+ namespace: env.namespace,
68
+ gracePeriodSeconds: 0,
69
+ propagationPolicy: 'Background',
70
+ });
70
71
  }
71
72
  catch (e) {
72
73
  if ((0, apply_1.statusCodeOf)(e) !== 404) {
@@ -75,29 +76,32 @@ async function deleteJob(instance, env, name) {
75
76
  }
76
77
  }
77
78
  exports.deleteJob = deleteJob;
78
- function awaitDeployRunningState(instance, env, name) {
79
- return new Promise((resolve, reject) => {
80
- const req = instance.watch.watch(`/apis/apps/v1/namespaces/${env.namespace}/deployments`, {}, (type, obj) => {
81
- if ((obj &&
82
- obj.metadata?.name === name &&
83
- obj.metadata?.namespace === env.namespace &&
84
- obj.status?.readyReplicas) ??
85
- 0 > 0) {
86
- req.then((r) => r.abort());
87
- resolve();
88
- }
89
- }, (err) => {
90
- if (err && err.message === 'aborted') {
91
- return;
92
- }
93
- if (err) {
94
- reject(err);
79
+ // Waits for a ready replica. The deadline only starts once a pod runs, so an
80
+ // image pull doesn't count against it (as on docker, where it starts after
81
+ // the container started).
82
+ async function awaitDeployRunningState(instance, env, name, readiness, pollInterval = 1000, now = Date.now) {
83
+ let runningSince = null;
84
+ for (;;) {
85
+ const deployment = await instance.appsApi.readNamespacedDeployment({ name, namespace: env.namespace });
86
+ if ((deployment.status?.readyReplicas ?? 0) > 0) {
87
+ return;
88
+ }
89
+ if (readiness.timeout !== null) {
90
+ if (runningSince === null) {
91
+ const pods = await instance.coreApi.listNamespacedPod({
92
+ namespace: env.namespace,
93
+ labelSelector: readiness.podSelector,
94
+ });
95
+ if (pods.items.some((pod) => pod.status?.phase === 'Running')) {
96
+ runningSince = now();
97
+ }
95
98
  }
96
- else {
97
- resolve();
99
+ else if (now() - runningSince >= readiness.timeout) {
100
+ throw new Error(readiness.timeoutMessage);
98
101
  }
99
- });
100
- });
102
+ }
103
+ await (0, sleep_1.sleep)(pollInterval);
104
+ }
101
105
  }
102
106
  exports.awaitDeployRunningState = awaitDeployRunningState;
103
107
  // Delete a job and wait until it is gone: its name is reused for the task's next
@@ -107,7 +111,7 @@ async function deleteJobAndWait(instance, env, name, abort, pollInterval = 500)
107
111
  await deleteJob(instance, env, name);
108
112
  for (;;) {
109
113
  try {
110
- await instance.batchApi.readNamespacedJob(name, env.namespace);
114
+ await instance.batchApi.readNamespacedJob({ name, namespace: env.namespace });
111
115
  }
112
116
  catch (e) {
113
117
  if ((0, apply_1.statusCodeOf)(e) === 404) {
@@ -5,15 +5,12 @@ const apply_1 = require("./apply");
5
5
  const resources_1 = require("./resources");
6
6
  exports.HTTP_ROUTE_API_VERSION = 'gateway.networking.k8s.io/v1';
7
7
  exports.HTTP_ROUTE_KIND = 'HTTPRoute';
8
- async function ensureHttpRoute(instance, env, ingress, service) {
9
- if (!ingress.gateway) {
10
- throw new Error(`httproute for ${ingress.host} requires a "gateway" (the parent Gateway name)`);
11
- }
8
+ async function ensureHttpRoute(instance, env, route, service) {
12
9
  const resource = {
13
10
  apiVersion: exports.HTTP_ROUTE_API_VERSION,
14
11
  kind: exports.HTTP_ROUTE_KIND,
15
12
  metadata: {
16
- name: ingress.host,
13
+ name: route.host,
17
14
  namespace: env.namespace,
18
15
  labels: {
19
16
  'hammerkit.dev/id': service.id(),
@@ -22,25 +19,25 @@ async function ensureHttpRoute(instance, env, ingress, service) {
22
19
  spec: {
23
20
  parentRefs: [
24
21
  {
25
- name: ingress.gateway,
26
- ...(ingress.gatewayNamespace ? { namespace: ingress.gatewayNamespace } : {}),
22
+ name: route.gateway,
23
+ ...(route.gatewayNamespace ? { namespace: route.gatewayNamespace } : {}),
27
24
  },
28
25
  ],
29
- hostnames: [ingress.host],
26
+ hostnames: [route.host],
30
27
  rules: [
31
28
  {
32
29
  matches: [
33
30
  {
34
31
  path: {
35
32
  type: 'PathPrefix',
36
- value: ingress.path ?? '/',
33
+ value: route.path ?? '/',
37
34
  },
38
35
  },
39
36
  ],
40
37
  backendRefs: [
41
38
  {
42
39
  name: (0, resources_1.getResourceName)(service),
43
- port: ingress.servicePort ?? service.data.ports[0].containerPort,
40
+ port: route.servicePort ?? service.data.ports[0].containerPort,
44
41
  },
45
42
  ],
46
43
  },
@@ -2,12 +2,14 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ensureKubernetesDeploymentExists = void 0;
4
4
  const work_item_1 = require("../planner/work-item");
5
+ const work_service_1 = require("../planner/work-service");
5
6
  const apply_1 = require("./apply");
6
7
  const get_service_ip_1 = require("./get-service-ip");
7
8
  const version_1 = require("../version");
8
9
  const replace_env_variables_1 = require("../environment/replace-env-variables");
9
10
  const await_running_state_1 = require("./await-running-state");
10
11
  const resources_1 = require("./resources");
12
+ const work_healthcheck_1 = require("../planner/work-healthcheck");
11
13
  async function ensureKubernetesDeploymentExists(instance, env, service, persistence, stateKey) {
12
14
  const hostAliases = [];
13
15
  for (const need of service.needs) {
@@ -25,10 +27,12 @@ async function ensureKubernetesDeploymentExists(instance, env, service, persiste
25
27
  }
26
28
  const envs = (0, replace_env_variables_1.getEnvironmentVariables)(service.data.envs);
27
29
  const name = (0, resources_1.getResourceName)(service);
28
- const probe = service.data.healthcheck
30
+ const healthcheckCommand = (0, work_service_1.getHealthcheckCommand)(service.data);
31
+ const serviceCommand = (0, work_service_1.getServiceCommand)(service.data);
32
+ const probe = healthcheckCommand
29
33
  ? {
30
34
  exec: {
31
- command: [service.data.healthcheck.cmd.parsed.command, ...service.data.healthcheck.cmd.parsed.args],
35
+ command: healthcheckCommand,
32
36
  },
33
37
  periodSeconds: 5,
34
38
  failureThreshold: 3,
@@ -68,13 +72,12 @@ async function ensureKubernetesDeploymentExists(instance, env, service, persiste
68
72
  {
69
73
  name: service.name.replace(/:/, '-'),
70
74
  image: service.data.image,
71
- workingDir: service.data.cwd ?? undefined,
75
+ workingDir: (0, work_service_1.getServiceWorkingDir)(service.data),
72
76
  env: Object.entries(envs).map(([key, value]) => ({
73
77
  name: key,
74
78
  value: value,
75
79
  })),
76
- command: service.data.cmd ? [service.data.cmd.parsed.command] : undefined,
77
- args: service.data.cmd ? service.data.cmd.parsed.args : undefined,
80
+ ...getKubernetesCommand(serviceCommand),
78
81
  ports: service.data.ports.map((p) => ({
79
82
  containerPort: p.containerPort,
80
83
  })),
@@ -99,7 +102,25 @@ async function ensureKubernetesDeploymentExists(instance, env, service, persiste
99
102
  if (deploy.status?.readyReplicas ?? 0 > 0) {
100
103
  return;
101
104
  }
102
- await (0, await_running_state_1.awaitDeployRunningState)(instance, env, name);
105
+ await (0, await_running_state_1.awaitDeployRunningState)(instance, env, name, {
106
+ podSelector: `hammerkit.dev/id=${service.id()}`,
107
+ timeout: service.data.healthcheck?.timeout ?? null,
108
+ timeoutMessage: service.data.healthcheck
109
+ ? (0, work_healthcheck_1.getHealthcheckTimeoutMessage)(service.name, service.data.healthcheck)
110
+ : '',
111
+ });
103
112
  }
104
113
  exports.ensureKubernetesDeploymentExists = ensureKubernetesDeploymentExists;
114
+ // Kubernetes `command` replaces the image entrypoint and `args` its command.
115
+ // A shell service sets both; an exec-form `cmd` keeps the established mapping
116
+ // (first token as `command`, the rest as `args`).
117
+ function getKubernetesCommand(command) {
118
+ if (command.entrypoint && command.cmd) {
119
+ return { command: command.entrypoint, args: command.cmd };
120
+ }
121
+ if (command.cmd) {
122
+ return { command: [command.cmd[0]], args: command.cmd.slice(1) };
123
+ }
124
+ return {};
125
+ }
105
126
  //# sourceMappingURL=ensure-kubernetes-deployment-exists.js.map
@@ -1,10 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ensureNamespace = void 0;
4
- function statusCodeOf(e) {
5
- const err = e;
6
- return err?.statusCode ?? err?.response?.statusCode ?? err?.body?.code;
7
- }
4
+ const apply_1 = require("./apply");
8
5
  /**
9
6
  * Idempotently make sure `namespace` exists. hammerkit does not otherwise
10
7
  * create namespaces — deployments/services/jobs are applied straight into
@@ -16,25 +13,27 @@ function statusCodeOf(e) {
16
13
  */
17
14
  async function ensureNamespace(instance, namespace) {
18
15
  try {
19
- await instance.coreApi.readNamespace(namespace);
16
+ await instance.coreApi.readNamespace({ name: namespace });
20
17
  return;
21
18
  }
22
19
  catch (e) {
23
- if (statusCodeOf(e) !== 404) {
20
+ if ((0, apply_1.statusCodeOf)(e) !== 404) {
24
21
  throw e;
25
22
  }
26
23
  }
27
24
  try {
28
25
  await instance.coreApi.createNamespace({
29
- metadata: {
30
- name: namespace,
31
- labels: { 'hammerkit.dev/managed': 'true' },
26
+ body: {
27
+ metadata: {
28
+ name: namespace,
29
+ labels: { 'hammerkit.dev/managed': 'true' },
30
+ },
32
31
  },
33
32
  });
34
33
  }
35
34
  catch (e) {
36
35
  // another worker created it between our read and create
37
- if (statusCodeOf(e) !== 409) {
36
+ if ((0, apply_1.statusCodeOf)(e) !== 409) {
38
37
  throw e;
39
38
  }
40
39
  }
@@ -10,6 +10,7 @@ const sleep_1 = require("../utils/sleep");
10
10
  const version_1 = require("../version");
11
11
  const log_1 = require("../log");
12
12
  const ensure_kubernetes_persistent_volume_claim_exists_1 = require("./ensure-kubernetes-persistent-volume-claim-exists");
13
+ const stream_1 = require("stream");
13
14
  // A previous run that crashed between creating an upload/download pod and its
14
15
  // `finally` cleanup leaves a pod behind under the same deterministic name. apply()
15
16
  // would then patch that stale pod in place — and if it is stuck in a terminal
@@ -17,7 +18,7 @@ const ensure_kubernetes_persistent_volume_claim_exists_1 = require("./ensure-kub
17
18
  // and wait for it to disappear before scheduling a fresh one.
18
19
  async function removeStalePod(instance, namespace, name) {
19
20
  try {
20
- await instance.coreApi.deleteNamespacedPod(name, namespace, undefined, undefined, 0);
21
+ await instance.coreApi.deleteNamespacedPod({ name, namespace, gracePeriodSeconds: 0 });
21
22
  }
22
23
  catch (e) {
23
24
  if ((0, apply_1.statusCodeOf)(e) === 404) {
@@ -27,7 +28,7 @@ async function removeStalePod(instance, namespace, name) {
27
28
  }
28
29
  for (let attempt = 0; attempt < 30; attempt++) {
29
30
  try {
30
- await instance.coreApi.readNamespacedPod(name, namespace);
31
+ await instance.coreApi.readNamespacedPod({ name, namespace });
31
32
  }
32
33
  catch (e) {
33
34
  if ((0, apply_1.statusCodeOf)(e) === 404) {
@@ -100,7 +101,7 @@ async function getPodForPersistence(instance, env, service, persistence, type, f
100
101
  }
101
102
  finally {
102
103
  service.status.console('stdout', `delete pod ${name}`);
103
- await instance.coreApi.deleteNamespacedPod(name, env.namespace);
104
+ await instance.coreApi.deleteNamespacedPod({ name, namespace: env.namespace });
104
105
  }
105
106
  }
106
107
  exports.getPodForPersistence = getPodForPersistence;
@@ -122,12 +123,12 @@ async function ensurePersistentData(instance, env, environment, service, persist
122
123
  continue;
123
124
  }
124
125
  const fileItem = (0, path_1.basename)(source.localPath);
125
- const data = (0, tar_1.create)({
126
+ const data = stream_1.Readable.from((0, tar_1.create)({
126
127
  cwd: (0, path_1.dirname)(source.localPath),
127
128
  filter(path) {
128
129
  return source.matcher(path, source.localPath);
129
130
  },
130
- }, [fileItem]);
131
+ }, [fileItem]));
131
132
  service.status.console('stdout', `upload ${source.localPath}`);
132
133
  const targetPath = `/dev/hammerkit/${service.id()}`;
133
134
  const res = await instance.exec.exec(env.namespace, name, 'volume', ['tar', 'xf', '-', '-C', targetPath], process.stdout, process.stderr, data, false, (status) => {
@@ -4,8 +4,8 @@ exports.getServiceIp = void 0;
4
4
  const resources_1 = require("./resources");
5
5
  async function getServiceIp(instance, env, workService) {
6
6
  const name = (0, resources_1.getResourceName)(workService);
7
- const svc = await instance.coreApi.readNamespacedService(name, env.namespace);
8
- return svc.body.spec?.clusterIP ?? null;
7
+ const svc = await instance.coreApi.readNamespacedService({ name, namespace: env.namespace });
8
+ return svc.spec?.clusterIP ?? null;
9
9
  }
10
10
  exports.getServiceIp = getServiceIp;
11
11
  //# sourceMappingURL=get-service-ip.js.map
@@ -16,14 +16,10 @@ function createKubernetesInstances(kubernetes) {
16
16
  const coreApi = kc.makeApiClient(client_node_1.CoreV1Api);
17
17
  const batchApi = kc.makeApiClient(client_node_1.BatchV1Api);
18
18
  const appsApi = kc.makeApiClient(client_node_1.AppsV1Api);
19
- const networkingApi = kc.makeApiClient(client_node_1.NetworkingV1Api);
20
19
  const objectApi = kc.makeApiClient(client_node_1.KubernetesObjectApi);
21
20
  const exec = new client_node_1.Exec(kc);
22
- const watch = new client_node_1.Watch(kc);
23
21
  return {
24
22
  objectApi,
25
- networkingApi,
26
- watch,
27
23
  exec,
28
24
  appsApi,
29
25
  coreApi,
@@ -2,16 +2,26 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.removePersistentData = void 0;
4
4
  async function removePersistentData(instance, kubernetes, task) {
5
- const configmaps = await instance.coreApi.listNamespacedConfigMap(kubernetes.namespace, undefined, undefined, undefined, undefined, `hammerkit.dev/id=${task.id()}`);
6
- for (const configMap of configmaps.body.items) {
5
+ const labelSelector = `hammerkit.dev/id=${task.id()}`;
6
+ const configmaps = await instance.coreApi.listNamespacedConfigMap({ namespace: kubernetes.namespace, labelSelector });
7
+ for (const configMap of configmaps.items) {
7
8
  if (configMap.metadata?.name) {
8
- await instance.coreApi.deleteNamespacedConfigMap(configMap.metadata.name, kubernetes.namespace);
9
+ await instance.coreApi.deleteNamespacedConfigMap({
10
+ name: configMap.metadata.name,
11
+ namespace: kubernetes.namespace,
12
+ });
9
13
  }
10
14
  }
11
- const pvcs = await instance.coreApi.listNamespacedPersistentVolumeClaim(kubernetes.namespace, undefined, undefined, undefined, undefined, `hammerkit.dev/id=${task.id()}`);
12
- for (const pvc of pvcs.body.items) {
15
+ const pvcs = await instance.coreApi.listNamespacedPersistentVolumeClaim({
16
+ namespace: kubernetes.namespace,
17
+ labelSelector,
18
+ });
19
+ for (const pvc of pvcs.items) {
13
20
  if (pvc.metadata?.name) {
14
- await instance.coreApi.deleteNamespacedPersistentVolumeClaim(pvc.metadata.name, kubernetes.namespace);
21
+ await instance.coreApi.deleteNamespacedPersistentVolumeClaim({
22
+ name: pvc.metadata.name,
23
+ namespace: kubernetes.namespace,
24
+ });
15
25
  }
16
26
  }
17
27
  }