hammerkit 1.26.0 → 1.27.0-next.10
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 +62 -1
- package/dist/cache/backends/local-cache-backend.js +10 -2
- package/dist/cache/backends/registry-cache-backend.js +2 -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 +24 -2
- 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 +411 -0
- package/dist/index.js +33 -36
- package/dist/kubernetes/apply.js +42 -1
- package/dist/kubernetes/await-running-state.js +33 -1
- package/dist/kubernetes/claim-layout.js +71 -0
- package/dist/kubernetes/ensure-http-route.js +1 -1
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +46 -28
- package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +132 -5
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +27 -11
- package/dist/kubernetes/ensure-persistent-data.js +75 -24
- package/dist/kubernetes/get-service-ip.js +16 -2
- package/dist/kubernetes/job-placement.js +99 -0
- package/dist/kubernetes/job-secrets.js +320 -0
- package/dist/kubernetes/kept-claims.js +203 -0
- package/dist/kubernetes/kubernetes-instance.js +10 -2
- package/dist/kubernetes/network-gate.js +71 -0
- package/dist/kubernetes/network-policy.js +583 -0
- package/dist/kubernetes/remove-persistent-data.js +3 -1
- package/dist/kubernetes/resources.js +37 -4
- package/dist/kubernetes/restore-kubernetes-data.js +28 -7
- package/dist/kubernetes/secrets.js +7 -9
- package/dist/kubernetes/store-kubernetes-data.js +1 -1
- package/dist/kubernetes/volumes.js +104 -12
- package/dist/log.js +1 -1
- package/dist/main.js +41 -0
- package/dist/network/egress-proxy.js +407 -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 +5 -2
- package/dist/planner/service-definition.js +5 -0
- package/dist/planner/utils/append-work-dependencies.js +5 -2
- package/dist/planner/utils/append-work-environment.js +2 -0
- package/dist/planner/utils/append-work-service.js +5 -0
- package/dist/planner/utils/append-work-task.js +20 -1
- package/dist/planner/utils/assert-item-paths.js +50 -0
- package/dist/planner/utils/hosted-volumes.js +35 -0
- package/dist/planner/utils/task-arguments.js +3 -0
- package/dist/planner/work-network.js +193 -0
- package/dist/planner/work-runtime-docker.js +43 -1
- package/dist/planner/work-runtime-kubernetes.js +370 -111
- package/dist/planner/work-runtime-local.js +2 -1
- package/dist/program.js +33 -5
- package/dist/remote/annotations.js +115 -0
- package/dist/remote/bundle-text.js +24 -0
- package/dist/remote/bundle.js +9 -1
- package/dist/remote/remote-commands.js +15 -3
- package/dist/remote/remote-work-commands.js +272 -71
- 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 +7 -0
- package/dist/schema/build-file-container-service-schema.js +8 -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 +2 -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 +26 -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 +603 -0
- package/dist/server/kubernetes-sandbox-storage.js +997 -0
- package/dist/server/oauth.js +12 -0
- package/dist/server/plan.js +88 -2
- 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 +48 -2
- package/dist/server/run-request.js +29 -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 +191 -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 +216 -0
- package/dist/server/sandboxes.js +519 -0
- package/dist/server/server-command.js +39 -20
- package/dist/server/server-config.js +443 -22
- package/dist/server/server.js +219 -40
- package/dist/utils/abort-on-signals.js +46 -6
- package/dist/utils/run-id.js +12 -0
- package/dist/utils/tar.js +25 -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
package/dist/index.js
CHANGED
|
@@ -1,41 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
-
Object.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const empty_writable_1 = require("./utils/empty-writable");
|
|
9
|
-
const run_program_1 = require("./run-program");
|
|
10
|
-
const abort_on_signals_1 = require("./utils/abort-on-signals");
|
|
11
|
-
const abortCtrl = new AbortController();
|
|
12
|
-
(0, abort_on_signals_1.abortOnSignals)(process, abortCtrl);
|
|
13
|
-
let settled = false;
|
|
14
|
-
// A run that never settles (a task waiting on something that can't happen)
|
|
15
|
-
// lets the event loop drain, and Node then exits 0: CI would report success
|
|
16
|
-
// for a build that never finished. Fail loudly instead.
|
|
17
|
-
process.on('beforeExit', () => {
|
|
18
|
-
if (!settled) {
|
|
19
|
-
process.stderr.write('hammerkit stopped before the run finished\n');
|
|
20
|
-
process.exitCode = 1;
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
21
8
|
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
22
18
|
});
|
|
23
|
-
|
|
24
|
-
(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
19
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
20
|
+
if (mod && mod.__esModule) return mod;
|
|
21
|
+
var result = {};
|
|
22
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
23
|
+
__setModuleDefault(result, mod);
|
|
24
|
+
return result;
|
|
25
|
+
};
|
|
26
|
+
// A server on a cluster runs `hammerkit server-sandbox <directory>` in the pod of a
|
|
27
|
+
// sandbox's files for every request on them. It loads the code of sandboxes only: the
|
|
28
|
+
// whole CLI takes most of a second to load, and more on a pod with little CPU.
|
|
29
|
+
if (process.argv[2] === 'server-sandbox' && process.argv.length === 4) {
|
|
30
|
+
void Promise.resolve().then(() => __importStar(require('./server/sandbox-pod-command'))).then(({ runSandboxPodCommand }) => runSandboxPodCommand(process.argv[3], process.stdin, process.stdout).catch((e) => {
|
|
31
|
+
process.stderr.write(`${e instanceof Error ? e.message : String(e)}\n`);
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
void Promise.resolve().then(() => __importStar(require('./main')));
|
|
37
|
+
}
|
|
41
38
|
//# sourceMappingURL=index.js.map
|
package/dist/kubernetes/apply.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.apply = exports.statusCodeOf = void 0;
|
|
3
|
+
exports.replace = exports.apply = exports.statusCodeOf = void 0;
|
|
4
4
|
// The client throws an ApiException carrying the HTTP status as `code`; watch
|
|
5
5
|
// errors carry it as `statusCode`.
|
|
6
6
|
function statusCodeOf(e) {
|
|
@@ -30,4 +30,45 @@ async function apply(instance, spec) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
exports.apply = apply;
|
|
33
|
+
/**
|
|
34
|
+
* Makes the object exactly as given: replaced as a whole when it exists, so what was
|
|
35
|
+
* dropped from it goes too, which a merge patch would keep (a key of a quota, a rule
|
|
36
|
+
* of a policy).
|
|
37
|
+
*/
|
|
38
|
+
async function replace(instance, spec) {
|
|
39
|
+
for (let attempt = 1;; attempt++) {
|
|
40
|
+
let current;
|
|
41
|
+
try {
|
|
42
|
+
current = await instance.objectApi.read(spec);
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
if (statusCodeOf(e) !== 404) {
|
|
46
|
+
throw e;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
return await instance.objectApi.create(spec);
|
|
50
|
+
}
|
|
51
|
+
catch (createError) {
|
|
52
|
+
if (statusCodeOf(createError) !== 409) {
|
|
53
|
+
throw createError;
|
|
54
|
+
}
|
|
55
|
+
current = await instance.objectApi.read(spec);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
return await instance.objectApi.replace({
|
|
60
|
+
...spec,
|
|
61
|
+
metadata: { ...spec.metadata, resourceVersion: current.metadata?.resourceVersion },
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
// replaced by someone else since it was read (the server sets up a caller's
|
|
66
|
+
// namespace for a run and for a sandbox at the same time): read it again
|
|
67
|
+
if (statusCodeOf(e) !== 409 || attempt >= 3) {
|
|
68
|
+
throw e;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
exports.replace = replace;
|
|
33
74
|
//# sourceMappingURL=apply.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.deleteJobAndWait = exports.awaitDeployRunningState = exports.deleteJob = exports.awaitJobCompletion = exports.awaitRunningState = void 0;
|
|
3
|
+
exports.stopJob = exports.deleteJobAndWait = exports.awaitDeployRunningState = exports.deleteJob = exports.awaitJobCompletion = exports.awaitRunningState = void 0;
|
|
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");
|
|
@@ -126,4 +126,36 @@ async function deleteJobAndWait(instance, env, name, abort, pollInterval = 500)
|
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
exports.deleteJobAndWait = deleteJobAndWait;
|
|
129
|
+
// Stops a Job at once: its pods go without a grace period, and this returns once they
|
|
130
|
+
// are gone, so what limited them (the item's NetworkPolicy) can go after them.
|
|
131
|
+
async function stopJob(instance, env, name, timeout = 30000, pollInterval = 500) {
|
|
132
|
+
await deleteJob(instance, env, name);
|
|
133
|
+
const deadline = Date.now() + timeout;
|
|
134
|
+
for (;;) {
|
|
135
|
+
const pods = await instance.coreApi.listNamespacedPod({
|
|
136
|
+
namespace: env.namespace,
|
|
137
|
+
labelSelector: `job-name=${name}`,
|
|
138
|
+
});
|
|
139
|
+
if (pods.items.length === 0) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (Date.now() > deadline) {
|
|
143
|
+
throw new Error(`the pods of ${name} were still there after ${Math.round(timeout / 1000)}s`);
|
|
144
|
+
}
|
|
145
|
+
for (const pod of pods.items) {
|
|
146
|
+
// also one the Job's deletion already ends, with the default grace period
|
|
147
|
+
if (pod.metadata?.name) {
|
|
148
|
+
await instance.coreApi
|
|
149
|
+
.deleteNamespacedPod({ name: pod.metadata.name, namespace: env.namespace, gracePeriodSeconds: 0 })
|
|
150
|
+
.catch((e) => {
|
|
151
|
+
if ((0, apply_1.statusCodeOf)(e) !== 404) {
|
|
152
|
+
throw e;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
await (0, sleep_1.sleep)(pollInterval);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
exports.stopJob = stopJob;
|
|
129
161
|
//# sourceMappingURL=await-running-state.js.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hasOutputsFlatteningAlike = exports.hasPathsEndingAlike = exports.byDepth = exports.getClaimSubPath = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const path_1 = require("path");
|
|
6
|
+
const portable_path_1 = require("../planner/utils/portable-path");
|
|
7
|
+
// The paths a work item keeps in its claim. Its consumers mount its outputs by the same
|
|
8
|
+
// paths, so they find the same folders.
|
|
9
|
+
function getClaimPaths(work) {
|
|
10
|
+
const data = work.data;
|
|
11
|
+
const paths = [];
|
|
12
|
+
if ('mounts' in data) {
|
|
13
|
+
paths.push(...data.mounts.map((mount) => mount.containerPath));
|
|
14
|
+
}
|
|
15
|
+
if ('src' in data) {
|
|
16
|
+
paths.push(...data.src.filter((src) => !src.inherited).map((src) => src.absolutePath));
|
|
17
|
+
}
|
|
18
|
+
if ('generates' in data) {
|
|
19
|
+
paths.push(...data.generates.filter((generate) => !generate.inherited).map((generate) => generate.path));
|
|
20
|
+
}
|
|
21
|
+
if ('volumes' in data) {
|
|
22
|
+
paths.push(...data.volumes.filter((volume) => !volume.inherited).map((volume) => volume.containerPath));
|
|
23
|
+
}
|
|
24
|
+
return paths;
|
|
25
|
+
}
|
|
26
|
+
// Where `path` is in the claim of `work`, relative to its root. A path is named after its
|
|
27
|
+
// last segment, so claims without two such paths are laid out as they always were (a
|
|
28
|
+
// service's data among them). Of the paths with the same last segment, the least deep one
|
|
29
|
+
// (then the first in sorted order) keeps the name, and the others also carry a hash of
|
|
30
|
+
// their path. Both go by the path
|
|
31
|
+
// relative to the project, as the task's id does, so a run in another checkout directory
|
|
32
|
+
// (each run of a server has its own) or with its `generates` in another order finds the
|
|
33
|
+
// same folders in a kept claim.
|
|
34
|
+
function getClaimSubPath(work, path) {
|
|
35
|
+
const name = (0, path_1.basename)(path) || 'root';
|
|
36
|
+
const alike = getClaimPaths(work).filter((other) => other !== path && (0, path_1.basename)(other) === (0, path_1.basename)(path));
|
|
37
|
+
if (alike.length === 0) {
|
|
38
|
+
return name;
|
|
39
|
+
}
|
|
40
|
+
const own = (0, portable_path_1.portablePath)(work.data.projectRoot, path);
|
|
41
|
+
const first = [own, ...alike.map((other) => (0, portable_path_1.portablePath)(work.data.projectRoot, other))].sort(byDepth)[0];
|
|
42
|
+
if (first === own) {
|
|
43
|
+
return name;
|
|
44
|
+
}
|
|
45
|
+
return `${name}-${(0, crypto_1.createHash)('sha256').update(own).digest('hex').slice(0, 12)}`;
|
|
46
|
+
}
|
|
47
|
+
exports.getClaimSubPath = getClaimSubPath;
|
|
48
|
+
function byDepth(a, b) {
|
|
49
|
+
return a.split('/').length - b.split('/').length || (a < b ? -1 : a > b ? 1 : 0);
|
|
50
|
+
}
|
|
51
|
+
exports.byDepth = byDepth;
|
|
52
|
+
// Whether two different paths of the claim of `work` end alike. Before getClaimSubPath
|
|
53
|
+
// named them apart, they shared one folder, and outputs made then are mixed.
|
|
54
|
+
function hasPathsEndingAlike(work) {
|
|
55
|
+
const paths = new Set(getClaimPaths(work));
|
|
56
|
+
return new Set([...paths].map((path) => (0, path_1.basename)(path))).size < paths.size;
|
|
57
|
+
}
|
|
58
|
+
exports.hasPathsEndingAlike = hasPathsEndingAlike;
|
|
59
|
+
// Whether two outputs of `work` flatten to the same name of a cache archive (`a-b` and
|
|
60
|
+
// `a/b`, see getArchivePaths). Before they were named apart, one archive held both.
|
|
61
|
+
function hasOutputsFlatteningAlike(work) {
|
|
62
|
+
const data = work.data;
|
|
63
|
+
if (!('generates' in data) || !('cwd' in data)) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const paths = new Set(data.generates.filter((generate) => !generate.inherited).map((generate) => generate.path));
|
|
67
|
+
const names = new Set([...paths].map((path) => (0, path_1.relative)(data.cwd, path).split(path_1.sep).join('-')));
|
|
68
|
+
return names.size < paths.size;
|
|
69
|
+
}
|
|
70
|
+
exports.hasOutputsFlatteningAlike = hasOutputsFlatteningAlike;
|
|
71
|
+
//# sourceMappingURL=claim-layout.js.map
|
|
@@ -36,7 +36,7 @@ async function ensureHttpRoute(instance, env, route, service) {
|
|
|
36
36
|
],
|
|
37
37
|
backendRefs: [
|
|
38
38
|
{
|
|
39
|
-
name: (0, resources_1.getResourceName)(service),
|
|
39
|
+
name: (0, resources_1.getResourceName)(service, env),
|
|
40
40
|
port: route.servicePort ?? service.data.ports[0].containerPort,
|
|
41
41
|
},
|
|
42
42
|
],
|
|
@@ -1,45 +1,42 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ensureKubernetesDeploymentExists = void 0;
|
|
4
|
-
const
|
|
3
|
+
exports.ensureKubernetesDeploymentExists = exports.DEFINITION_LABEL = void 0;
|
|
4
|
+
const service_definition_1 = require("../planner/service-definition");
|
|
5
5
|
const work_service_1 = require("../planner/work-service");
|
|
6
|
+
const volumes_1 = require("./volumes");
|
|
6
7
|
const service_hostname_1 = require("../planner/utils/service-hostname");
|
|
7
8
|
const apply_1 = require("./apply");
|
|
8
|
-
const get_service_ip_1 = require("./get-service-ip");
|
|
9
9
|
const version_1 = require("../version");
|
|
10
10
|
const replace_env_variables_1 = require("../environment/replace-env-variables");
|
|
11
11
|
const await_running_state_1 = require("./await-running-state");
|
|
12
12
|
const resources_1 = require("./resources");
|
|
13
13
|
const work_healthcheck_1 = require("../planner/work-healthcheck");
|
|
14
14
|
const container_resources_1 = require("./container-resources");
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
throw new Error(`unable to get service ip for ${need.name}`);
|
|
24
|
-
}
|
|
25
|
-
hostAliases.push({
|
|
26
|
-
ip,
|
|
27
|
-
hostnames: [need.name],
|
|
28
|
-
});
|
|
29
|
-
}
|
|
15
|
+
const job_placement_1 = require("./job-placement");
|
|
16
|
+
const network_policy_1 = require("./network-policy");
|
|
17
|
+
const egress_proxy_1 = require("../network/egress-proxy");
|
|
18
|
+
// what the service was created from, to tell a changed one
|
|
19
|
+
exports.DEFINITION_LABEL = 'hammerkit.dev/definition';
|
|
20
|
+
async function ensureKubernetesDeploymentExists(instance, env, service, persistence, secrets, stateKey,
|
|
21
|
+
// the needed services by name, and the proxy of a limited network
|
|
22
|
+
network) {
|
|
30
23
|
const envs = (0, replace_env_variables_1.getEnvironmentVariables)(service.data.envs);
|
|
31
|
-
const name = (0, resources_1.getResourceName)(service);
|
|
24
|
+
const name = (0, resources_1.getResourceName)(service, env);
|
|
32
25
|
const healthcheckCommand = (0, work_service_1.getHealthcheckCommand)(service.data);
|
|
33
26
|
const serviceCommand = (0, work_service_1.getServiceCommand)(service.data, { kubernetes: true });
|
|
34
27
|
const probe = healthcheckCommand
|
|
35
28
|
? {
|
|
36
29
|
exec: {
|
|
37
|
-
|
|
30
|
+
// the service itself, not through the proxy (see getProxylessEnvironment)
|
|
31
|
+
command: withoutProxy(healthcheckCommand, network.env),
|
|
38
32
|
},
|
|
39
33
|
periodSeconds: 5,
|
|
40
34
|
failureThreshold: 3,
|
|
41
35
|
}
|
|
42
36
|
: undefined;
|
|
37
|
+
const server = env.server;
|
|
38
|
+
const placement = server ? (0, job_placement_1.getPodPlacement)(server, undefined) : null;
|
|
39
|
+
const workspace = (0, volumes_1.getWorkspacePodSpec)(env, persistence);
|
|
43
40
|
const deployment = {
|
|
44
41
|
kind: 'Deployment',
|
|
45
42
|
apiVersion: 'apps/v1',
|
|
@@ -50,28 +47,40 @@ async function ensureKubernetesDeploymentExists(instance, env, service, persiste
|
|
|
50
47
|
'hammerkit.dev/version': (0, version_1.getVersion)(),
|
|
51
48
|
},
|
|
52
49
|
labels: {
|
|
53
|
-
|
|
50
|
+
...(0, resources_1.getItemLabels)(service, env),
|
|
54
51
|
'hammerkit.dev/state': stateKey,
|
|
52
|
+
[exports.DEFINITION_LABEL]: (0, service_definition_1.getServiceDefinitionHash)(service),
|
|
55
53
|
},
|
|
56
54
|
},
|
|
57
55
|
spec: {
|
|
58
56
|
selector: {
|
|
59
|
-
matchLabels:
|
|
60
|
-
'hammerkit.dev/id': service.id(),
|
|
61
|
-
},
|
|
57
|
+
matchLabels: (0, resources_1.getItemLabels)(service, env),
|
|
62
58
|
},
|
|
63
59
|
replicas: 1,
|
|
64
60
|
template: {
|
|
65
61
|
metadata: {
|
|
66
62
|
labels: {
|
|
67
|
-
|
|
63
|
+
...(0, resources_1.getItemLabels)(service, env),
|
|
68
64
|
'hammerkit.dev/state': stateKey,
|
|
65
|
+
...workspace.labels,
|
|
66
|
+
...(0, network_policy_1.getNetworkLabels)(env, service),
|
|
69
67
|
},
|
|
70
68
|
},
|
|
71
69
|
spec: {
|
|
70
|
+
...(workspace.affinity ? { affinity: workspace.affinity } : {}),
|
|
72
71
|
// the pod resolves its own service name, as with Docker's Hostname
|
|
73
72
|
hostname: (0, service_hostname_1.getServiceHostname)(service.name),
|
|
74
|
-
hostAliases: hostAliases,
|
|
73
|
+
hostAliases: network.hostAliases,
|
|
74
|
+
initContainers: network.initContainers,
|
|
75
|
+
// on a server the pod is the build's: no token of the namespace, and the
|
|
76
|
+
// nodes of the first pool
|
|
77
|
+
...(placement
|
|
78
|
+
? {
|
|
79
|
+
automountServiceAccountToken: false,
|
|
80
|
+
nodeSelector: placement.nodeSelector,
|
|
81
|
+
tolerations: placement.tolerations,
|
|
82
|
+
}
|
|
83
|
+
: {}),
|
|
75
84
|
containers: [
|
|
76
85
|
{
|
|
77
86
|
name: service.name.replace(/:/, '-'),
|
|
@@ -83,6 +92,7 @@ async function ensureKubernetesDeploymentExists(instance, env, service, persiste
|
|
|
83
92
|
value: value,
|
|
84
93
|
})),
|
|
85
94
|
...secrets.env,
|
|
95
|
+
...network.env,
|
|
86
96
|
],
|
|
87
97
|
...getKubernetesCommand(serviceCommand),
|
|
88
98
|
ports: service.data.ports.map((p) => ({
|
|
@@ -92,6 +102,7 @@ async function ensureKubernetesDeploymentExists(instance, env, service, persiste
|
|
|
92
102
|
livenessProbe: probe,
|
|
93
103
|
...(0, container_resources_1.getKubernetesResources)(service.data.resources),
|
|
94
104
|
volumeMounts: [...persistence.mounts.map((m) => m.mount), ...secrets.mounts],
|
|
105
|
+
...(0, job_placement_1.getContainerSecurity)(env),
|
|
95
106
|
},
|
|
96
107
|
{
|
|
97
108
|
name: 'debug',
|
|
@@ -99,9 +110,10 @@ async function ensureKubernetesDeploymentExists(instance, env, service, persiste
|
|
|
99
110
|
command: ['sleep'],
|
|
100
111
|
args: ['3600'],
|
|
101
112
|
volumeMounts: persistence.mounts.map((m) => m.mount),
|
|
113
|
+
...(0, job_placement_1.getContainerSecurity)(env),
|
|
102
114
|
},
|
|
103
115
|
],
|
|
104
|
-
volumes: [...persistence.volumes, ...secrets.volumes],
|
|
116
|
+
volumes: [...persistence.volumes, ...persistence.workspace, ...secrets.volumes],
|
|
105
117
|
},
|
|
106
118
|
},
|
|
107
119
|
},
|
|
@@ -111,7 +123,7 @@ async function ensureKubernetesDeploymentExists(instance, env, service, persiste
|
|
|
111
123
|
return;
|
|
112
124
|
}
|
|
113
125
|
await (0, await_running_state_1.awaitDeployRunningState)(instance, env, name, {
|
|
114
|
-
podSelector:
|
|
126
|
+
podSelector: (0, resources_1.getItemSelector)(service, env),
|
|
115
127
|
timeout: service.data.healthcheck?.timeout ?? null,
|
|
116
128
|
timeoutMessage: service.data.healthcheck
|
|
117
129
|
? (0, work_healthcheck_1.getHealthcheckTimeoutMessage)(service.name, service.data.healthcheck)
|
|
@@ -131,4 +143,10 @@ function getKubernetesCommand(command) {
|
|
|
131
143
|
}
|
|
132
144
|
return {};
|
|
133
145
|
}
|
|
146
|
+
// A probe runs with the container's variables, so it empties the proxy ones
|
|
147
|
+
// through `env`, as a Docker healthcheck does with the exec's own variables.
|
|
148
|
+
function withoutProxy(command, env) {
|
|
149
|
+
const proxyless = (0, egress_proxy_1.getProxylessEnvironment)(Object.fromEntries(env.map((e) => [e.name, e.value ?? ''])));
|
|
150
|
+
return proxyless ? ['env', ...Object.keys(proxyless).map((key) => `${key}=`), ...command] : command;
|
|
151
|
+
}
|
|
134
152
|
//# sourceMappingURL=ensure-kubernetes-deployment-exists.js.map
|
|
@@ -1,9 +1,46 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ensureKubernetesPersistentVolumeClaimExists = void 0;
|
|
3
|
+
exports.ensureKubernetesPersistentVolumeClaimExists = exports.getClaimSize = exports.DEFAULT_VOLUME_SIZE = void 0;
|
|
4
4
|
const apply_1 = require("./apply");
|
|
5
5
|
const version_1 = require("../version");
|
|
6
|
-
|
|
6
|
+
const job_placement_1 = require("./job-placement");
|
|
7
|
+
const log_1 = require("../log");
|
|
8
|
+
const units_1 = require("../utils/units");
|
|
9
|
+
const sleep_1 = require("../utils/sleep");
|
|
10
|
+
const kept_claims_1 = require("./kept-claims");
|
|
11
|
+
// how long a claim that is being removed (a kept one that expired) gets to go before one
|
|
12
|
+
// of its name is made again, and the kept claims of other sources when the quota is full
|
|
13
|
+
const REMOVAL_TIMEOUT = 60000;
|
|
14
|
+
// what the claim of a task or service requests when neither it nor the environment says
|
|
15
|
+
exports.DEFAULT_VOLUME_SIZE = '1Gi';
|
|
16
|
+
// The size of a claim: the work's `volumeSize`, else the environment's, at most what the
|
|
17
|
+
// environment allows (a server's `maxVolumeSize`).
|
|
18
|
+
function getClaimSize(env, size, name) {
|
|
19
|
+
const requested = size ?? env.volumeSize ?? exports.DEFAULT_VOLUME_SIZE;
|
|
20
|
+
if (env.maxVolumeSize && (0, units_1.parseSize)(requested) > (0, units_1.parseSize)(env.maxVolumeSize)) {
|
|
21
|
+
throw new Error(`${name} asks for a claim of ${requested}, the server allows at most ${env.maxVolumeSize}`);
|
|
22
|
+
}
|
|
23
|
+
return requested;
|
|
24
|
+
}
|
|
25
|
+
exports.getClaimSize = getClaimSize;
|
|
26
|
+
// The size a claim requests, in bytes, as the cluster may write it back (`1Gi`, bytes)
|
|
27
|
+
function getRequestedBytes(claim) {
|
|
28
|
+
const storage = claim.spec?.resources?.requests?.storage;
|
|
29
|
+
try {
|
|
30
|
+
return storage === undefined ? null : (0, units_1.parseSize)(String(storage));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// The claim of a work item, of `size` (see getClaimSize). One a run before made smaller
|
|
37
|
+
// grows, where its storage class allows that; a larger one stays as it is, since a claim
|
|
38
|
+
// never shrinks. A `kept` claim (see kept-claims) carries the scope of the run instead of
|
|
39
|
+
// its label, and does not expire while the run mounts it.
|
|
40
|
+
async function ensureKubernetesPersistentVolumeClaimExists(instance, env, volume, service, size, kept = false) {
|
|
41
|
+
const storage = getClaimSize(env, size, service.name);
|
|
42
|
+
const scope = kept ? env.server?.volumeScope : undefined;
|
|
43
|
+
const runLabel = env.server ? (0, job_placement_1.getRunLabelValue)(env.server.run) : undefined;
|
|
7
44
|
const pvc = {
|
|
8
45
|
kind: 'PersistentVolumeClaim',
|
|
9
46
|
apiVersion: 'v1',
|
|
@@ -15,6 +52,7 @@ async function ensureKubernetesPersistentVolumeClaimExists(instance, env, volume
|
|
|
15
52
|
},
|
|
16
53
|
labels: {
|
|
17
54
|
'hammerkit.dev/id': service.id(),
|
|
55
|
+
...(scope && runLabel ? { [kept_claims_1.VOLUME_SCOPE_LABEL]: scope, [kept_claims_1.KEPT_BY_LABEL]: runLabel } : (0, job_placement_1.getRunLabels)(env)),
|
|
18
56
|
},
|
|
19
57
|
},
|
|
20
58
|
spec: {
|
|
@@ -22,19 +60,108 @@ async function ensureKubernetesPersistentVolumeClaimExists(instance, env, volume
|
|
|
22
60
|
storageClassName: env.storageClass,
|
|
23
61
|
resources: {
|
|
24
62
|
requests: {
|
|
25
|
-
storage
|
|
63
|
+
storage,
|
|
26
64
|
},
|
|
27
65
|
},
|
|
28
66
|
},
|
|
29
67
|
};
|
|
30
|
-
service.status.
|
|
68
|
+
service.status.write('debug', `ensure volume ${volume.name}`);
|
|
31
69
|
// Don't block on the PVC reaching "Bound". Storage classes with
|
|
32
70
|
// volumeBindingMode WaitForFirstConsumer (docker-desktop's "standard",
|
|
33
71
|
// k3d/k3s local-path, most cloud defaults) keep the claim Pending until a pod
|
|
34
72
|
// mounts it — and hammerkit creates that pod (upload pod / deployment) only
|
|
35
73
|
// after this call. Waiting here deadlocks until the test times out. k8s binds
|
|
36
74
|
// the claim when the consuming pod is scheduled, so applying it is enough.
|
|
37
|
-
|
|
75
|
+
let existing = await readClaim(instance, pvc);
|
|
76
|
+
if (existing?.metadata?.deletionTimestamp) {
|
|
77
|
+
// a kept claim that expired: one of the same name is made once it is gone
|
|
78
|
+
existing = await untilRemoved(instance, pvc);
|
|
79
|
+
}
|
|
80
|
+
if (!existing) {
|
|
81
|
+
return await createClaim(instance, env, pvc);
|
|
82
|
+
}
|
|
83
|
+
if (scope) {
|
|
84
|
+
if (existing.metadata?.labels?.[kept_claims_1.VOLUME_SCOPE_LABEL] !== scope) {
|
|
85
|
+
throw new Error(`the claim ${volume.name} is not one this run may mount, it was made for another source`);
|
|
86
|
+
}
|
|
87
|
+
if (existing.metadata?.labels?.[kept_claims_1.KEPT_BY_LABEL] !== runLabel ||
|
|
88
|
+
existing.metadata?.annotations?.[kept_claims_1.EXPIRES_ANNOTATION]) {
|
|
89
|
+
// mounted by this run now: it expires at the run's end
|
|
90
|
+
existing = await instance.objectApi.patch({
|
|
91
|
+
apiVersion: pvc.apiVersion,
|
|
92
|
+
kind: pvc.kind,
|
|
93
|
+
metadata: {
|
|
94
|
+
name: pvc.metadata.name,
|
|
95
|
+
namespace: pvc.metadata.namespace,
|
|
96
|
+
labels: { [kept_claims_1.KEPT_BY_LABEL]: runLabel },
|
|
97
|
+
// null removes it
|
|
98
|
+
annotations: { [kept_claims_1.EXPIRES_ANNOTATION]: null },
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const current = getRequestedBytes(existing);
|
|
104
|
+
if (current === null || current >= (0, units_1.parseSize)(storage)) {
|
|
105
|
+
return existing;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
// the requested size is all of a bound claim's spec that may change
|
|
109
|
+
return await instance.objectApi.patch({
|
|
110
|
+
...pvc,
|
|
111
|
+
spec: { resources: { requests: { storage } } },
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
throw new Error(`the claim ${volume.name} has ${(0, units_1.formatSize)(current)} and ${service.name} needs ${storage}, ` +
|
|
116
|
+
`which its storage class could not grow it to (${(0, log_1.getErrorMessage)(e)}): ` +
|
|
117
|
+
'remove it with hammerkit clean and run again');
|
|
118
|
+
}
|
|
38
119
|
}
|
|
39
120
|
exports.ensureKubernetesPersistentVolumeClaimExists = ensureKubernetesPersistentVolumeClaimExists;
|
|
121
|
+
async function createClaim(instance, env, pvc) {
|
|
122
|
+
for (let evicted = false;; evicted = true) {
|
|
123
|
+
try {
|
|
124
|
+
return await instance.objectApi.create(pvc);
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
// Two work items can make the same claim concurrently (e.g. a volume
|
|
128
|
+
// shared between a task and a service that inherits its generates)
|
|
129
|
+
if ((0, apply_1.statusCodeOf)(e) === 409) {
|
|
130
|
+
return await instance.objectApi.read(pvc);
|
|
131
|
+
}
|
|
132
|
+
// the claims a server keeps for other sources make way under the quota
|
|
133
|
+
if (evicted || (0, apply_1.statusCodeOf)(e) !== 403 || !/exceeded quota/i.test((0, log_1.getErrorMessage)(e))) {
|
|
134
|
+
throw e;
|
|
135
|
+
}
|
|
136
|
+
const removed = await (0, kept_claims_1.evictKeptClaims)(instance, env, REMOVAL_TIMEOUT);
|
|
137
|
+
if (removed.length === 0) {
|
|
138
|
+
throw e;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function untilRemoved(instance, pvc) {
|
|
144
|
+
const deadline = Date.now() + REMOVAL_TIMEOUT;
|
|
145
|
+
for (;;) {
|
|
146
|
+
const claim = await readClaim(instance, pvc);
|
|
147
|
+
if (!claim) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
if (Date.now() > deadline) {
|
|
151
|
+
throw new Error(`the claim ${pvc.metadata.name} is being removed and still there after ${REMOVAL_TIMEOUT / 1000}s`);
|
|
152
|
+
}
|
|
153
|
+
await (0, sleep_1.sleep)(1000);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async function readClaim(instance, pvc) {
|
|
157
|
+
try {
|
|
158
|
+
return await instance.objectApi.read(pvc);
|
|
159
|
+
}
|
|
160
|
+
catch (e) {
|
|
161
|
+
if ((0, apply_1.statusCodeOf)(e) === 404) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
throw e;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
40
167
|
//# sourceMappingURL=ensure-kubernetes-persistent-volume-claim-exists.js.map
|
|
@@ -4,8 +4,11 @@ exports.ensureKubernetesServiceExists = void 0;
|
|
|
4
4
|
const apply_1 = require("./apply");
|
|
5
5
|
const resources_1 = require("./resources");
|
|
6
6
|
const version_1 = require("../version");
|
|
7
|
+
// A service without ports gets a headless Service: a ClusterIP one needs a port,
|
|
8
|
+
// and its tasks reach every port of the pod, as on docker.
|
|
7
9
|
async function ensureKubernetesServiceExists(instance, env, service) {
|
|
8
|
-
const name = (0, resources_1.getResourceName)(service);
|
|
10
|
+
const name = (0, resources_1.getResourceName)(service, env);
|
|
11
|
+
const headless = service.data.ports.length === 0;
|
|
9
12
|
const svc = {
|
|
10
13
|
kind: 'Service',
|
|
11
14
|
apiVersion: 'v1',
|
|
@@ -15,21 +18,34 @@ async function ensureKubernetesServiceExists(instance, env, service) {
|
|
|
15
18
|
annotations: {
|
|
16
19
|
'hammerkit.dev/version': (0, version_1.getVersion)(),
|
|
17
20
|
},
|
|
18
|
-
labels:
|
|
19
|
-
'hammerkit.dev/id': service.id(),
|
|
20
|
-
},
|
|
21
|
+
labels: (0, resources_1.getItemLabels)(service, env),
|
|
21
22
|
},
|
|
22
23
|
spec: {
|
|
23
24
|
type: 'ClusterIP',
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
...(headless ? { clusterIP: 'None' } : {}),
|
|
26
|
+
// the pods of the service of this run, on a server where runs share the namespace
|
|
27
|
+
selector: (0, resources_1.getItemLabels)(service, env),
|
|
28
|
+
...(headless
|
|
29
|
+
? {}
|
|
30
|
+
: {
|
|
31
|
+
ports: service.data.ports.map((port) => ({
|
|
32
|
+
port: port.hostPort ?? port.containerPort,
|
|
33
|
+
targetPort: port.containerPort,
|
|
34
|
+
})),
|
|
35
|
+
}),
|
|
31
36
|
},
|
|
32
37
|
};
|
|
38
|
+
// the clusterIP of a Service is fixed, so one from before its ports changed goes first
|
|
39
|
+
const existing = await instance.coreApi
|
|
40
|
+
.readNamespacedService({ name, namespace: env.namespace })
|
|
41
|
+
.catch((e) => ((0, apply_1.statusCodeOf)(e) === 404 ? null : Promise.reject(e)));
|
|
42
|
+
if (existing && (existing.spec?.clusterIP === 'None') !== headless) {
|
|
43
|
+
await instance.coreApi.deleteNamespacedService({ name, namespace: env.namespace }).catch((e) => {
|
|
44
|
+
if ((0, apply_1.statusCodeOf)(e) !== 404) {
|
|
45
|
+
throw e;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
33
49
|
return await (0, apply_1.apply)(instance, svc);
|
|
34
50
|
}
|
|
35
51
|
exports.ensureKubernetesServiceExists = ensureKubernetesServiceExists;
|