hammerkit 1.25.0 → 1.27.0-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build.schema.json +53 -1
- package/dist/cache/backends/local-cache-backend.js +10 -2
- package/dist/cache/explain.js +13 -11
- package/dist/cli.js +3 -0
- package/dist/docker/item-network.js +471 -0
- package/dist/docker/pull.js +4 -2
- package/dist/docker/remove-orphaned-containers.js +13 -6
- package/dist/environment/replace-env-variables.js +8 -6
- package/dist/executer/await-completed-dependencies.js +2 -1
- package/dist/executer/check-readiness.js +2 -2
- package/dist/executer/container-task-state.js +8 -6
- package/dist/executer/docker-service.js +24 -13
- package/dist/executer/docker-task.js +18 -29
- package/dist/executer/event-cache.js +4 -0
- package/dist/executer/execute-command.js +18 -5
- package/dist/executer/execute-docker.js +4 -1
- package/dist/executer/execution-steps.js +33 -7
- package/dist/executer/get-docker-executor.js +14 -7
- package/dist/executer/scheduler/enqueue-next.js +17 -3
- package/dist/executer/watch-loop.js +2 -2
- package/dist/executer/watch-state-key.js +1 -1
- package/dist/file/within-root.js +42 -0
- package/dist/harness/connect.js +99 -0
- package/dist/harness/format.js +73 -0
- package/dist/harness/mcp-command.js +59 -0
- package/dist/harness/mcp.js +432 -0
- package/dist/harness/opencode.js +492 -0
- package/dist/harness/pi.js +217 -0
- package/dist/harness/sandbox-client.js +373 -0
- package/dist/index.js +33 -36
- package/dist/kubernetes/apply.js +31 -1
- package/dist/kubernetes/await-running-state.js +33 -1
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +44 -22
- package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +131 -4
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +25 -4
- package/dist/kubernetes/ensure-persistent-data.js +48 -7
- package/dist/kubernetes/get-service-ip.js +15 -1
- package/dist/kubernetes/job-placement.js +99 -0
- package/dist/kubernetes/job-secrets.js +306 -0
- package/dist/kubernetes/kept-claims.js +191 -0
- package/dist/kubernetes/kubernetes-instance.js +10 -2
- package/dist/kubernetes/network-gate.js +71 -0
- package/dist/kubernetes/network-policy.js +526 -0
- package/dist/kubernetes/resources.js +13 -2
- package/dist/kubernetes/restore-kubernetes-data.js +19 -4
- package/dist/kubernetes/secrets.js +2 -0
- package/dist/kubernetes/store-kubernetes-data.js +1 -1
- package/dist/kubernetes/volumes.js +100 -11
- package/dist/main.js +41 -0
- package/dist/network/egress-proxy.js +368 -0
- package/dist/network/port-relay.js +35 -0
- package/dist/optimizer/get-work-cache-stats.js +16 -0
- package/dist/optimizer/work-task-cache-description.js +3 -2
- package/dist/planner/service-definition.js +8 -2
- package/dist/planner/utils/append-work-environment.js +2 -0
- package/dist/planner/utils/append-work-service.js +4 -0
- package/dist/planner/utils/append-work-task.js +17 -1
- package/dist/planner/utils/assert-item-paths.js +50 -0
- package/dist/planner/utils/hosted-volumes.js +35 -0
- package/dist/planner/utils/task-arguments.js +155 -0
- package/dist/planner/work-cache-id.js +15 -1
- package/dist/planner/work-network.js +193 -0
- package/dist/planner/work-runtime-docker.js +43 -0
- package/dist/planner/work-runtime-kubernetes.js +336 -102
- package/dist/planner/work-runtime-local.js +16 -8
- package/dist/planner/work-service.js +7 -3
- package/dist/planner/work-state-record.js +30 -0
- package/dist/program.js +80 -7
- package/dist/remote/annotations.js +103 -0
- package/dist/remote/bundle-text.js +24 -0
- package/dist/remote/bundle.js +9 -1
- package/dist/remote/remote-commands.js +4 -1
- package/dist/remote/remote-work-commands.js +182 -69
- package/dist/remote/server-client.js +18 -5
- package/dist/remote/source-manifest.js +15 -2
- package/dist/remote/token-source.js +67 -0
- package/dist/schema/build-file-base-task-schema.js +3 -0
- package/dist/schema/build-file-container-service-schema.js +6 -0
- package/dist/schema/build-file-container-task-schema.js +3 -0
- package/dist/schema/build-file-environment-schema.js +9 -0
- package/dist/schema/network-schema.js +78 -0
- package/dist/schema/reference-parser.js +1 -0
- package/dist/schema/schema-parser.js +16 -3
- package/dist/schema/work-scope-parser.js +13 -0
- package/dist/secrets/command-secret-provider.js +73 -10
- package/dist/secrets/github-app-token-command.js +23 -0
- package/dist/secrets/github-app-token.js +191 -0
- package/dist/server/accounts-api.js +9 -2
- package/dist/server/definitions-api.js +69 -47
- package/dist/server/definitions.js +76 -51
- package/dist/server/executor.js +156 -41
- package/dist/server/git-checkout.js +72 -8
- package/dist/server/kubernetes-backend.js +554 -0
- package/dist/server/kubernetes-sandbox-storage.js +996 -0
- package/dist/server/oauth.js +1 -0
- package/dist/server/plan.js +51 -1
- package/dist/server/policy.js +104 -15
- package/dist/server/proposals-api.js +24 -40
- package/dist/server/proposals.js +12 -25
- package/dist/server/repo-access.js +37 -6
- package/dist/server/run-logs.js +217 -0
- package/dist/server/run-plan.js +51 -2
- package/dist/server/run-request.js +23 -10
- package/dist/server/run-store.js +5 -0
- package/dist/server/run-worker-command.js +13 -1
- package/dist/server/run-worker.js +121 -24
- package/dist/server/runs.js +184 -10
- package/dist/server/sandbox-docker.js +62 -0
- package/dist/server/sandbox-files-api.js +269 -0
- package/dist/server/sandbox-files.js +1054 -0
- package/dist/server/sandbox-patch.js +481 -0
- package/dist/server/sandbox-pod-command.js +15 -0
- package/dist/server/sandbox-pod.js +146 -0
- package/dist/server/sandbox-storage.js +462 -0
- package/dist/server/sandboxes-api.js +210 -0
- package/dist/server/sandboxes.js +519 -0
- package/dist/server/server-config.js +412 -22
- package/dist/server/server.js +204 -39
- package/dist/utils/run-id.js +12 -0
- package/harness/opencode.ts +11 -0
- package/harness/pi.ts +7 -0
- package/package.json +8 -2
- package/dist/remote/text-diff.js +0 -89
|
@@ -3,18 +3,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.kubernetesForwardRuntime = exports.kubernetesServiceRuntime = exports.kubernetesTaskRuntime = void 0;
|
|
6
|
+
exports.kubernetesForwardRuntime = exports.kubernetesServiceRuntime = exports.kubernetesTaskRuntime = exports.describeFailedJob = void 0;
|
|
7
7
|
const abort_1 = require("../executer/abort");
|
|
8
|
-
const get_service_ip_1 = require("../kubernetes/get-service-ip");
|
|
9
|
-
const work_item_1 = require("./work-item");
|
|
10
8
|
const kubernetes_service_1 = require("../executer/kubernetes-service");
|
|
11
9
|
const kubernetes_instance_1 = require("../kubernetes/kubernetes-instance");
|
|
12
10
|
const replace_env_variables_1 = require("../environment/replace-env-variables");
|
|
13
11
|
const apply_1 = require("../kubernetes/apply");
|
|
14
12
|
const ensure_kubernetes_service_exists_1 = require("../kubernetes/ensure-kubernetes-service-exists");
|
|
15
13
|
const ensure_kubernetes_deployment_exists_1 = require("../kubernetes/ensure-kubernetes-deployment-exists");
|
|
14
|
+
const service_definition_1 = require("./service-definition");
|
|
16
15
|
const ensure_namespace_1 = require("../kubernetes/ensure-namespace");
|
|
17
16
|
const ensure_persistent_data_1 = require("../kubernetes/ensure-persistent-data");
|
|
17
|
+
const network_gate_1 = require("../kubernetes/network-gate");
|
|
18
18
|
const await_running_state_1 = require("../kubernetes/await-running-state");
|
|
19
19
|
const volumes_1 = require("../kubernetes/volumes");
|
|
20
20
|
const resources_1 = require("../kubernetes/resources");
|
|
@@ -22,11 +22,19 @@ const ensure_http_route_1 = require("../kubernetes/ensure-http-route");
|
|
|
22
22
|
const find_process_1 = __importDefault(require("find-process"));
|
|
23
23
|
const log_1 = require("../log");
|
|
24
24
|
const version_1 = require("../version");
|
|
25
|
+
const task_arguments_1 = require("./utils/task-arguments");
|
|
25
26
|
const restore_kubernetes_data_1 = require("../kubernetes/restore-kubernetes-data");
|
|
26
27
|
const store_kubernetes_data_1 = require("../kubernetes/store-kubernetes-data");
|
|
27
28
|
const remove_persistent_data_1 = require("../kubernetes/remove-persistent-data");
|
|
29
|
+
const kept_claims_1 = require("../kubernetes/kept-claims");
|
|
28
30
|
const container_resources_1 = require("../kubernetes/container-resources");
|
|
29
31
|
const secrets_1 = require("../kubernetes/secrets");
|
|
32
|
+
const job_secrets_1 = require("../kubernetes/job-secrets");
|
|
33
|
+
const job_placement_1 = require("../kubernetes/job-placement");
|
|
34
|
+
const network_policy_1 = require("../kubernetes/network-policy");
|
|
35
|
+
const run_id_1 = require("../utils/run-id");
|
|
36
|
+
const work_network_1 = require("./work-network");
|
|
37
|
+
const work_secret_1 = require("./work-secret");
|
|
30
38
|
async function listJobNames(instance, kubernetes, task) {
|
|
31
39
|
const jobs = await instance.batchApi.listNamespacedJob({
|
|
32
40
|
namespace: kubernetes.namespace,
|
|
@@ -34,138 +42,334 @@ async function listJobNames(instance, kubernetes, task) {
|
|
|
34
42
|
});
|
|
35
43
|
return jobs.items.flatMap((job) => (job.metadata?.name ? [job.metadata.name] : []));
|
|
36
44
|
}
|
|
45
|
+
// The Secret of the values the server hands over and the one of the credentials the
|
|
46
|
+
// pod reads its own secrets with
|
|
47
|
+
async function removeTaskSecrets(instance, kubernetes, task) {
|
|
48
|
+
await (0, secrets_1.removeKubernetesSecret)(instance, kubernetes, task);
|
|
49
|
+
await (0, job_secrets_1.removeJobCredentials)(instance, kubernetes, task);
|
|
50
|
+
}
|
|
51
|
+
// The newest pod of a Job
|
|
52
|
+
async function getJobPod(instance, kubernetes, job) {
|
|
53
|
+
const pods = await instance.coreApi.listNamespacedPod({
|
|
54
|
+
namespace: kubernetes.namespace,
|
|
55
|
+
labelSelector: `job-name=${job}`,
|
|
56
|
+
});
|
|
57
|
+
return pods.items
|
|
58
|
+
.slice()
|
|
59
|
+
.sort((a, b) => (a.metadata?.creationTimestamp?.toISOString() ?? '').localeCompare(b.metadata?.creationTimestamp?.toISOString() ?? ''))
|
|
60
|
+
.pop();
|
|
61
|
+
}
|
|
62
|
+
// a task's log is kept to this, the run's own cap is smaller
|
|
63
|
+
const MAX_JOB_LOG_BYTES = 4 * 1024 * 1024;
|
|
64
|
+
// What the command printed, as the task's output: read from the cluster once the Job
|
|
65
|
+
// has ended. Without a pod (it never started) there is nothing to say.
|
|
66
|
+
async function writeJobLogs(instance, kubernetes, task, job, container) {
|
|
67
|
+
try {
|
|
68
|
+
const pod = await getJobPod(instance, kubernetes, job);
|
|
69
|
+
if (!pod?.metadata?.name) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const log = await instance.coreApi.readNamespacedPodLog({
|
|
73
|
+
name: pod.metadata.name,
|
|
74
|
+
namespace: kubernetes.namespace,
|
|
75
|
+
container,
|
|
76
|
+
limitBytes: MAX_JOB_LOG_BYTES,
|
|
77
|
+
});
|
|
78
|
+
for (const line of String(log ?? '').split(/\r?\n/)) {
|
|
79
|
+
if (line.length > 0) {
|
|
80
|
+
task.status.console('stdout', line);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
task.status.write('debug', `the log of ${job} could not be read: ${(0, log_1.getErrorMessage)(e)}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// the command of a Job exited with this code, as a crash on docker or a host
|
|
89
|
+
class JobCrash extends Error {
|
|
90
|
+
constructor(exitCode) {
|
|
91
|
+
super(`exit code ${exitCode}`);
|
|
92
|
+
this.exitCode = exitCode;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Why a Job failed: the network gate, an init container that could not read a secret, which
|
|
96
|
+
// names the secret, never its output (which is the provider's and can hold the value), or the
|
|
97
|
+
// exit code of the command.
|
|
98
|
+
async function describeFailedJob(instance, kubernetes, task, job, container, secrets) {
|
|
99
|
+
const pod = await getJobPod(instance, kubernetes, job).catch(() => undefined);
|
|
100
|
+
for (const status of pod?.status?.initContainerStatuses ?? []) {
|
|
101
|
+
const terminated = status.state?.terminated ?? status.lastState?.terminated;
|
|
102
|
+
if (!terminated || terminated.exitCode === 0) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (status.name === network_gate_1.NETWORK_GATE_CONTAINER) {
|
|
106
|
+
// the gate says why in its termination message
|
|
107
|
+
return {
|
|
108
|
+
type: 'error',
|
|
109
|
+
message: `${task}: ${terminated.message?.trim() || 'the network policy did not take hold'}`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const secret = secrets.find((s) => `secret-${s.index}` === status.name);
|
|
113
|
+
if (secret) {
|
|
114
|
+
return {
|
|
115
|
+
type: 'error',
|
|
116
|
+
message: `secret ${(0, work_secret_1.getSecretName)(secret.secret)} (${(0, work_secret_1.describeSecretSource)(secret.secret)}) could not be read: ${terminated.reason ?? 'exit code'} ${terminated.exitCode}`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const command = pod?.status?.containerStatuses?.find((status) => status.name === container);
|
|
121
|
+
const terminated = command?.state?.terminated ?? command?.lastState?.terminated;
|
|
122
|
+
if (terminated && terminated.exitCode !== 0) {
|
|
123
|
+
return { type: 'crash', exitCode: terminated.exitCode };
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
exports.describeFailedJob = describeFailedJob;
|
|
37
128
|
function kubernetesTaskRuntime(task, kubernetes) {
|
|
38
129
|
const instance = (0, kubernetes_instance_1.createKubernetesInstances)(kubernetes);
|
|
130
|
+
// on a server that keeps them, the task's claim stays after the run with the state of its
|
|
131
|
+
// outputs, and the next run of the same source mounts them as they are (see kept-claims)
|
|
132
|
+
const kept = (0, kept_claims_1.isKeptClaim)(task, kubernetes);
|
|
133
|
+
const claim = (0, volumes_1.getVolumeName)(task, kubernetes);
|
|
39
134
|
return {
|
|
40
135
|
initialize() {
|
|
41
136
|
return Promise.resolve();
|
|
42
137
|
},
|
|
43
|
-
async restore(environment, path) {
|
|
44
|
-
|
|
138
|
+
async restore(environment, path, stateKey) {
|
|
139
|
+
if (!kept) {
|
|
140
|
+
await (0, restore_kubernetes_data_1.restoreKubernetesData)(task, kubernetes, instance, environment, path);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const reset = await (0, kept_claims_1.forgetClaimState)(instance, kubernetes, claim);
|
|
144
|
+
await (0, restore_kubernetes_data_1.restoreKubernetesData)(task, kubernetes, instance, environment, path, { reset });
|
|
145
|
+
if (stateKey) {
|
|
146
|
+
await (0, kept_claims_1.recordClaimState)(instance, kubernetes, claim, stateKey);
|
|
147
|
+
}
|
|
45
148
|
},
|
|
46
149
|
async archive(environment, path) {
|
|
47
150
|
await (0, store_kubernetes_data_1.storeKubernetesData)(task, kubernetes, instance, environment, path);
|
|
48
151
|
},
|
|
49
152
|
async execute(environment, options) {
|
|
50
153
|
const envs = (0, replace_env_variables_1.getEnvironmentVariables)(task.data.envs);
|
|
51
|
-
const persistence = await (0, volumes_1.getKubernetesPersistence)(task);
|
|
154
|
+
const persistence = await (0, volumes_1.getKubernetesPersistence)(task, kubernetes);
|
|
155
|
+
const workspace = (0, volumes_1.getWorkspacePodSpec)(kubernetes, persistence, { name: task.name, pool: task.data.pool });
|
|
52
156
|
await (0, ensure_namespace_1.ensureNamespace)(instance, kubernetes.namespace);
|
|
53
|
-
await (0, ensure_persistent_data_1.ensurePersistentData)(instance, kubernetes, environment, task, persistence);
|
|
54
|
-
const secrets = (0, secrets_1.getKubernetesSecretRefs)(task, task.data.secrets);
|
|
55
157
|
try {
|
|
56
|
-
|
|
158
|
+
// the task writes to it now: what it holds stops being the outputs of a state, and
|
|
159
|
+
// goes, as a new claim would hold nothing
|
|
160
|
+
const reset = kept && (await (0, kept_claims_1.forgetClaimState)(instance, kubernetes, claim));
|
|
161
|
+
await (0, ensure_persistent_data_1.ensurePersistentData)(instance, kubernetes, environment, task, persistence, { reset });
|
|
162
|
+
}
|
|
163
|
+
catch (e) {
|
|
164
|
+
options.state.set({ stateKey: options.stateKey, type: 'error', errorMessage: (0, log_1.getErrorMessage)(e) });
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const server = kubernetes.server;
|
|
168
|
+
const { delivered, inJob } = (0, job_secrets_1.splitJobSecrets)(task.data.secrets, server);
|
|
169
|
+
const secrets = (0, secrets_1.getKubernetesSecretRefs)(task, delivered);
|
|
170
|
+
const admitted = environment.hosted?.accounts;
|
|
171
|
+
try {
|
|
172
|
+
(0, job_secrets_1.assertSecretsReachable)(task, inJob);
|
|
173
|
+
if (server) {
|
|
174
|
+
(0, job_secrets_1.assertJobAccounts)(inJob, server, admitted);
|
|
175
|
+
}
|
|
176
|
+
await (0, secrets_1.ensureKubernetesSecret)(instance, kubernetes, task, delivered, environment);
|
|
57
177
|
}
|
|
58
178
|
catch (e) {
|
|
179
|
+
await removeTaskSecrets(instance, kubernetes, task).catch(() => undefined);
|
|
59
180
|
options.state.set({ stateKey: options.stateKey, type: 'error', errorMessage: (0, log_1.getErrorMessage)(e) });
|
|
60
181
|
return;
|
|
61
182
|
}
|
|
183
|
+
// the run its pods and limits belong to: on a server, the server's run; on a
|
|
184
|
+
// sandbox, its proxy stays with the sandbox for the next runs
|
|
185
|
+
const scope = {
|
|
186
|
+
run: server ? (0, job_placement_1.getRunLabelValue)(server.run) : (0, run_id_1.getRunId)(),
|
|
187
|
+
...(server?.workspace
|
|
188
|
+
? {
|
|
189
|
+
sandbox: {
|
|
190
|
+
label: server.workspace.label,
|
|
191
|
+
...(server.workspace.pod ? { pod: server.workspace.pod } : {}),
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
: {}),
|
|
195
|
+
};
|
|
196
|
+
const labels = {
|
|
197
|
+
'hammerkit.dev/id': task.id(),
|
|
198
|
+
'hammerkit.dev/state': options.stateKey,
|
|
199
|
+
...workspace.labels,
|
|
200
|
+
[network_policy_1.RUN_LABEL]: scope.run,
|
|
201
|
+
...(0, network_policy_1.getNetworkLabels)(kubernetes, task),
|
|
202
|
+
};
|
|
203
|
+
const placement = server ? (0, job_placement_1.getPodPlacement)(server, task.data.pool) : null;
|
|
62
204
|
// The last job of a run stays: its state label is what currentStateKey
|
|
63
205
|
// reads. Jobs from an earlier run of this task go before this one starts.
|
|
64
206
|
for (const name of await listJobNames(instance, kubernetes, task)) {
|
|
65
207
|
await (0, await_running_state_1.deleteJobAndWait)(instance, kubernetes, name, options.abort);
|
|
66
208
|
}
|
|
67
|
-
// the services the task needs
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
const ip = await (0, get_service_ip_1.getServiceIp)(instance, kubernetes, need.service);
|
|
74
|
-
if (!ip) {
|
|
75
|
-
throw new Error(`unable to get service ip for ${need.name}`);
|
|
76
|
-
}
|
|
77
|
-
hostAliases.push({ ip, hostnames: [need.name] });
|
|
209
|
+
// the services the task needs by the names it lists them under, and the
|
|
210
|
+
// NetworkPolicy and proxy of a limited network, both of this run
|
|
211
|
+
let network;
|
|
212
|
+
try {
|
|
213
|
+
network = await (0, network_policy_1.ensureKubernetesItemNetwork)(instance, kubernetes, task, { followLog: true, ...scope });
|
|
78
214
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
215
|
+
catch (e) {
|
|
216
|
+
await removeTaskSecrets(instance, kubernetes, task).catch(() => undefined);
|
|
217
|
+
options.state.set({ stateKey: options.stateKey, type: 'error', errorMessage: (0, log_1.getErrorMessage)(e) });
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
const podName = (0, resources_1.getResourceName)(task, '-job');
|
|
222
|
+
let i = 0;
|
|
223
|
+
for (const cmd of task.data.cmds) {
|
|
224
|
+
const container = `cmd-${++i}`;
|
|
225
|
+
let secretSpec;
|
|
226
|
+
try {
|
|
227
|
+
// made for each Job right before it, so a later command's token is as fresh
|
|
228
|
+
const credentials = await (0, job_secrets_1.ensureJobCredentials)(instance, kubernetes, task, inJob, admitted);
|
|
229
|
+
secretSpec = (0, job_secrets_1.getJobSecretSpec)(task, inJob, server, credentials, environment);
|
|
230
|
+
}
|
|
231
|
+
catch (e) {
|
|
232
|
+
await removeTaskSecrets(instance, kubernetes, task).catch(() => undefined);
|
|
233
|
+
options.state.set({ stateKey: options.stateKey, type: 'error', errorMessage: (0, log_1.getErrorMessage)(e) });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
// the arguments after `--` keep their `$` as text, Kubernetes expands `$(NAME)`
|
|
237
|
+
const command = secretSpec.command(task.data.shell, (0, task_arguments_1.getKubernetesCommandText)(cmd));
|
|
238
|
+
const spec = {
|
|
239
|
+
kind: 'Job',
|
|
240
|
+
apiVersion: 'batch/v1',
|
|
241
|
+
metadata: {
|
|
242
|
+
namespace: kubernetes.namespace,
|
|
243
|
+
name: podName,
|
|
244
|
+
annotations: {
|
|
245
|
+
'hammerkit.dev/version': (0, version_1.getVersion)(),
|
|
246
|
+
},
|
|
247
|
+
labels,
|
|
94
248
|
},
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
249
|
+
spec: {
|
|
250
|
+
template: {
|
|
251
|
+
metadata: {
|
|
252
|
+
annotations: {
|
|
253
|
+
'hammerkit.dev/version': (0, version_1.getVersion)(),
|
|
254
|
+
},
|
|
255
|
+
labels,
|
|
101
256
|
},
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
257
|
+
spec: {
|
|
258
|
+
// the network gate first: nothing runs before the item's policy holds
|
|
259
|
+
...(network.initContainers.length + secretSpec.initContainers.length > 0
|
|
260
|
+
? { initContainers: [...network.initContainers, ...secretSpec.initContainers] }
|
|
261
|
+
: {}),
|
|
262
|
+
containers: [
|
|
263
|
+
{
|
|
264
|
+
image: task.data.image,
|
|
265
|
+
// through the task's shell, as on Docker
|
|
266
|
+
command: command.command,
|
|
267
|
+
args: command.args,
|
|
268
|
+
workingDir: cmd.cwd,
|
|
269
|
+
env: [
|
|
270
|
+
...Object.entries(envs).map(([key, value]) => ({ name: key, value })),
|
|
271
|
+
...secrets.env,
|
|
272
|
+
...network.env,
|
|
273
|
+
...command.env,
|
|
274
|
+
],
|
|
275
|
+
name: container,
|
|
276
|
+
volumeMounts: [
|
|
277
|
+
...persistence.mounts.map((m) => m.mount),
|
|
278
|
+
...secrets.mounts,
|
|
279
|
+
...secretSpec.mounts,
|
|
280
|
+
],
|
|
281
|
+
...(0, container_resources_1.getKubernetesResources)(task.data.resources),
|
|
282
|
+
...(0, job_placement_1.getContainerSecurity)(kubernetes),
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
volumes: [
|
|
286
|
+
...persistence.volumes,
|
|
287
|
+
...persistence.workspace,
|
|
288
|
+
...secrets.volumes,
|
|
289
|
+
...secretSpec.volumes,
|
|
290
|
+
],
|
|
291
|
+
...(workspace.affinity ? { affinity: workspace.affinity } : {}),
|
|
292
|
+
hostAliases: network.hostAliases,
|
|
293
|
+
restartPolicy: 'Never',
|
|
294
|
+
// on a server the pod is the build's: no token of the namespace, and the
|
|
295
|
+
// nodes of its pool
|
|
296
|
+
...(placement
|
|
297
|
+
? {
|
|
298
|
+
automountServiceAccountToken: false,
|
|
299
|
+
nodeSelector: placement.nodeSelector,
|
|
300
|
+
tolerations: placement.tolerations,
|
|
301
|
+
}
|
|
302
|
+
: {}),
|
|
105
303
|
},
|
|
106
304
|
},
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
{
|
|
110
|
-
image: task.data.image,
|
|
111
|
-
// through the task's shell, as on Docker
|
|
112
|
-
command: [task.data.shell, '-c'],
|
|
113
|
-
args: [cmd.cmd],
|
|
114
|
-
workingDir: cmd.cwd,
|
|
115
|
-
env: [
|
|
116
|
-
...Object.entries(envs).map(([key, value]) => ({ name: key, value })),
|
|
117
|
-
...secrets.env,
|
|
118
|
-
],
|
|
119
|
-
name: `cmd-${++i}`,
|
|
120
|
-
volumeMounts: [...persistence.mounts.map((m) => m.mount), ...secrets.mounts],
|
|
121
|
-
...(0, container_resources_1.getKubernetesResources)(task.data.resources),
|
|
122
|
-
},
|
|
123
|
-
],
|
|
124
|
-
volumes: [...persistence.volumes, ...secrets.volumes],
|
|
125
|
-
hostAliases,
|
|
126
|
-
restartPolicy: 'Never',
|
|
127
|
-
},
|
|
305
|
+
backoffLimit: server ? 0 : 1,
|
|
306
|
+
parallelism: 1,
|
|
128
307
|
},
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
308
|
+
};
|
|
309
|
+
try {
|
|
310
|
+
const pod = await (0, apply_1.apply)(instance, spec);
|
|
311
|
+
if (!pod.status?.succeeded) {
|
|
312
|
+
try {
|
|
313
|
+
await (0, await_running_state_1.awaitJobCompletion)(instance, kubernetes, spec.metadata.name, options.abort);
|
|
314
|
+
}
|
|
315
|
+
catch (e) {
|
|
316
|
+
if (!(e instanceof abort_1.AbortError)) {
|
|
317
|
+
await writeJobLogs(instance, kubernetes, task, podName, container);
|
|
318
|
+
const failure = await describeFailedJob(instance, kubernetes, task.name, podName, container, inJob);
|
|
319
|
+
if (failure?.type === 'crash') {
|
|
320
|
+
throw new JobCrash(failure.exitCode);
|
|
321
|
+
}
|
|
322
|
+
throw new Error(failure?.message ?? (0, log_1.getErrorMessage)(e));
|
|
323
|
+
}
|
|
324
|
+
throw e;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
await writeJobLogs(instance, kubernetes, task, podName, container);
|
|
328
|
+
task.status.write('debug', 'pod completed');
|
|
329
|
+
// its init containers are done with the credentials
|
|
330
|
+
await (0, job_secrets_1.removeJobCredentials)(instance, kubernetes, task);
|
|
331
|
+
if (i < task.data.cmds.length) {
|
|
332
|
+
await (0, await_running_state_1.deleteJobAndWait)(instance, kubernetes, podName, options.abort);
|
|
333
|
+
}
|
|
137
334
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
335
|
+
catch (e) {
|
|
336
|
+
if (e instanceof abort_1.AbortError) {
|
|
337
|
+
// at once, so its policy can go (in finally) once the pod ended
|
|
338
|
+
await (0, await_running_state_1.stopJob)(instance, kubernetes, podName).catch((e) => task.status.write('error', `the job could not be stopped: ${(0, log_1.getErrorMessage)(e)}`));
|
|
339
|
+
await removeTaskSecrets(instance, kubernetes, task).catch(() => undefined);
|
|
340
|
+
options.state.set({ stateKey: options.stateKey, type: 'canceled' });
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
await (0, await_running_state_1.deleteJob)(instance, kubernetes, podName).catch(() => undefined);
|
|
344
|
+
options.state.set(e instanceof JobCrash
|
|
345
|
+
? { stateKey: options.stateKey, type: 'crash', exitCode: e.exitCode }
|
|
346
|
+
: { stateKey: options.stateKey, type: 'error', errorMessage: (0, log_1.getErrorMessage)(e) });
|
|
347
|
+
await removeTaskSecrets(instance, kubernetes, task).catch(() => undefined);
|
|
146
348
|
return;
|
|
147
349
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
350
|
+
}
|
|
351
|
+
// the finished job stays for its state, the values it read need not
|
|
352
|
+
await removeTaskSecrets(instance, kubernetes, task);
|
|
353
|
+
if (kept) {
|
|
354
|
+
await (0, kept_claims_1.recordClaimState)(instance, kubernetes, claim, options.stateKey);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
finally {
|
|
358
|
+
network.close();
|
|
359
|
+
// once the run's pods ended: a cancelled job's pod is still terminating
|
|
360
|
+
if ((0, work_network_1.isLimitedNetwork)((0, work_network_1.getEffectiveNetwork)(task.data))) {
|
|
361
|
+
await (0, network_policy_1.releaseKubernetesItemNetwork)(instance, kubernetes, task, scope).catch((e) => task.status.write('error', `remove of network policy failed ${(0, log_1.getErrorMessage)(e)}`));
|
|
156
362
|
}
|
|
157
363
|
}
|
|
158
|
-
// the finished job stays for its state, the values it read need not
|
|
159
|
-
await (0, secrets_1.removeKubernetesSecret)(instance, kubernetes, task);
|
|
160
364
|
},
|
|
161
365
|
async stop() {
|
|
162
366
|
const jobs = await instance.batchApi.listNamespacedJob({
|
|
163
367
|
namespace: kubernetes.namespace,
|
|
164
368
|
labelSelector: `hammerkit.dev/id=${task.id()}`,
|
|
165
369
|
});
|
|
166
|
-
for (const
|
|
167
|
-
if (
|
|
168
|
-
await
|
|
370
|
+
for (const job of jobs.items) {
|
|
371
|
+
if (job.metadata?.name) {
|
|
372
|
+
await (0, await_running_state_1.stopJob)(instance, kubernetes, job.metadata.name);
|
|
169
373
|
}
|
|
170
374
|
}
|
|
171
375
|
const deployments = await instance.appsApi.listNamespacedDeployment({
|
|
@@ -189,13 +393,19 @@ function kubernetesTaskRuntime(task, kubernetes) {
|
|
|
189
393
|
await instance.coreApi.deleteNamespacedPod({ name: pod.metadata.name, namespace: kubernetes.namespace });
|
|
190
394
|
}
|
|
191
395
|
}
|
|
396
|
+
// the limits go last, once no pod of the task runs
|
|
397
|
+
await (0, network_policy_1.removeAllKubernetesItemNetworks)(instance, kubernetes, task);
|
|
192
398
|
},
|
|
193
399
|
async remove() {
|
|
194
400
|
await this.stop();
|
|
195
401
|
await (0, remove_persistent_data_1.removePersistentData)(instance, kubernetes, task);
|
|
196
|
-
await (
|
|
402
|
+
await removeTaskSecrets(instance, kubernetes, task);
|
|
197
403
|
},
|
|
198
404
|
async currentStateKey() {
|
|
405
|
+
if (kept) {
|
|
406
|
+
// the server removes the Jobs of a run at its end, the claim says
|
|
407
|
+
return (0, kept_claims_1.useClaimState)(instance, kubernetes, claim);
|
|
408
|
+
}
|
|
199
409
|
const jobs = await instance.batchApi.listNamespacedJob({
|
|
200
410
|
namespace: kubernetes.namespace,
|
|
201
411
|
labelSelector: `hammerkit.dev/id=${task.id()}`,
|
|
@@ -210,11 +420,11 @@ function kubernetesTaskRuntime(task, kubernetes) {
|
|
|
210
420
|
// outputs live in the task's claim; without it the finished job left
|
|
211
421
|
// nothing to reuse
|
|
212
422
|
try {
|
|
213
|
-
const
|
|
214
|
-
name:
|
|
423
|
+
const found = await instance.coreApi.readNamespacedPersistentVolumeClaim({
|
|
424
|
+
name: claim,
|
|
215
425
|
namespace: kubernetes.namespace,
|
|
216
426
|
});
|
|
217
|
-
return
|
|
427
|
+
return found.metadata?.deletionTimestamp ? null : stateKey;
|
|
218
428
|
}
|
|
219
429
|
catch (e) {
|
|
220
430
|
if ((0, apply_1.statusCodeOf)(e) === 404) {
|
|
@@ -229,8 +439,29 @@ exports.kubernetesTaskRuntime = kubernetesTaskRuntime;
|
|
|
229
439
|
function kubernetesServiceRuntime(service, kubernetes) {
|
|
230
440
|
const instance = (0, kubernetes_instance_1.createKubernetesInstances)(kubernetes);
|
|
231
441
|
return {
|
|
232
|
-
|
|
233
|
-
|
|
442
|
+
// A Deployment left by an earlier `up` is the running service, so `down`
|
|
443
|
+
// stops it and a run reuses it, as with a container on Docker. One whose
|
|
444
|
+
// definition changed goes, with its limits, and is created anew.
|
|
445
|
+
async initialize(state) {
|
|
446
|
+
const deployments = await instance.appsApi.listNamespacedDeployment({
|
|
447
|
+
namespace: kubernetes.namespace,
|
|
448
|
+
labelSelector: `hammerkit.dev/id=${service.id()}`,
|
|
449
|
+
});
|
|
450
|
+
const deployment = deployments.items[0];
|
|
451
|
+
if (!deployment?.metadata?.name) {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (deployment.metadata.labels?.[ensure_kubernetes_deployment_exists_1.DEFINITION_LABEL] !== (0, service_definition_1.getServiceDefinitionHash)(service)) {
|
|
455
|
+
service.status.write('info', `${service.name} changed since it was started, recreating it`);
|
|
456
|
+
await this.stop();
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
state.set({
|
|
460
|
+
type: 'running',
|
|
461
|
+
dns: { containerId: deployment.metadata.name },
|
|
462
|
+
stateKey: deployment.metadata.labels?.['hammerkit.dev/state'] ?? '',
|
|
463
|
+
remote: null,
|
|
464
|
+
});
|
|
234
465
|
},
|
|
235
466
|
async restore(environment, path) {
|
|
236
467
|
await (0, restore_kubernetes_data_1.restoreKubernetesData)(service, kubernetes, instance, environment, path);
|
|
@@ -252,14 +483,17 @@ function kubernetesServiceRuntime(service, kubernetes) {
|
|
|
252
483
|
}
|
|
253
484
|
}
|
|
254
485
|
await (0, secrets_1.removeKubernetesSecret)(instance, kubernetes, service);
|
|
486
|
+
// the limits go last, once the service's pods ended
|
|
487
|
+
await (0, network_policy_1.removeAllKubernetesItemNetworks)(instance, kubernetes, service);
|
|
255
488
|
},
|
|
256
489
|
async execute(environment, options) {
|
|
257
|
-
const persistence = await (0, volumes_1.getKubernetesPersistence)(service);
|
|
490
|
+
const persistence = await (0, volumes_1.getKubernetesPersistence)(service, kubernetes);
|
|
258
491
|
await (0, ensure_namespace_1.ensureNamespace)(instance, kubernetes.namespace);
|
|
259
492
|
await (0, ensure_kubernetes_service_exists_1.ensureKubernetesServiceExists)(instance, kubernetes, service);
|
|
260
493
|
await (0, ensure_persistent_data_1.ensurePersistentData)(instance, kubernetes, environment, service, persistence);
|
|
261
494
|
await (0, secrets_1.ensureKubernetesSecret)(instance, kubernetes, service, service.data.secrets, environment);
|
|
262
|
-
await (0,
|
|
495
|
+
const network = await (0, network_policy_1.ensureKubernetesItemNetwork)(instance, kubernetes, service, { followLog: false, run: null });
|
|
496
|
+
await (0, ensure_kubernetes_deployment_exists_1.ensureKubernetesDeploymentExists)(instance, kubernetes, service, persistence, (0, secrets_1.getKubernetesSecretRefs)(service, service.data.secrets), options.stateKey, network);
|
|
263
497
|
const name = (0, resources_1.getResourceName)(service);
|
|
264
498
|
options.state.set({
|
|
265
499
|
type: 'running',
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.getLocalWorkRuntime = void 0;
|
|
6
|
+
exports.getLocalWorkRuntime = exports.getStateFilename = void 0;
|
|
7
7
|
const tar_1 = require("tar");
|
|
8
8
|
const path_1 = require("path");
|
|
9
9
|
const local_task_1 = require("../executer/local-task");
|
|
@@ -11,9 +11,11 @@ const event_cache_1 = require("../executer/event-cache");
|
|
|
11
11
|
const get_outputs_to_reset_1 = require("./utils/get-outputs-to-reset");
|
|
12
12
|
const find_process_1 = __importDefault(require("find-process"));
|
|
13
13
|
const stream_1 = require("stream");
|
|
14
|
+
const work_state_record_1 = require("./work-state-record");
|
|
14
15
|
function getStateFilename(task) {
|
|
15
|
-
return (0, path_1.join)(task.data.cwd, '.hammerkit',
|
|
16
|
+
return (0, path_1.join)(task.data.cwd, '.hammerkit', 'state', (0, work_state_record_1.getStateRecordName)(task));
|
|
16
17
|
}
|
|
18
|
+
exports.getStateFilename = getStateFilename;
|
|
17
19
|
function getPidFilename(task) {
|
|
18
20
|
return (0, path_1.join)(task.data.cwd, '.hammerkit', `${task.id()}.pid`);
|
|
19
21
|
}
|
|
@@ -39,6 +41,8 @@ function getLocalWorkRuntime(task) {
|
|
|
39
41
|
// pidfile check happens in execute (initialize has no Environment in scope)
|
|
40
42
|
},
|
|
41
43
|
async restore(environment, path) {
|
|
44
|
+
// the restored outputs aren't the ones the record names any more
|
|
45
|
+
await removeStateFile(environment, task);
|
|
42
46
|
await restoreLocal(environment, task.data, path);
|
|
43
47
|
},
|
|
44
48
|
async archive(environment, path) {
|
|
@@ -54,7 +58,7 @@ function getLocalWorkRuntime(task) {
|
|
|
54
58
|
});
|
|
55
59
|
return;
|
|
56
60
|
}
|
|
57
|
-
await environment.file.createDirectory((0, path_1.join)(task.data.cwd, '.hammerkit'));
|
|
61
|
+
await environment.file.createDirectory((0, path_1.join)(task.data.cwd, '.hammerkit', 'state'));
|
|
58
62
|
const pidFile = getPidFilename(task);
|
|
59
63
|
await environment.file.writeFile(pidFile, `${process.pid}`);
|
|
60
64
|
try {
|
|
@@ -62,16 +66,14 @@ function getLocalWorkRuntime(task) {
|
|
|
62
66
|
// fails overwrites outputs, so the previous record goes first, and only
|
|
63
67
|
// a run that succeeded writes a new one.
|
|
64
68
|
const stateFile = getStateFilename(task);
|
|
65
|
-
|
|
66
|
-
await environment.file.remove(stateFile);
|
|
67
|
-
}
|
|
69
|
+
await removeStateFile(environment, task);
|
|
68
70
|
for (const generate of (0, get_outputs_to_reset_1.getOutputsToReset)(task.data)) {
|
|
69
71
|
await environment.file.remove(generate.path);
|
|
70
72
|
}
|
|
71
73
|
await (0, local_task_1.localTask)(task, environment, options);
|
|
72
74
|
const failed = ['crash', 'error', 'canceled'].includes(options.state.current.type);
|
|
73
75
|
if (!failed) {
|
|
74
|
-
await environment.file.writeFile(stateFile, options.stateKey);
|
|
76
|
+
await environment.file.writeFile(stateFile, (0, work_state_record_1.formatStateRecord)(task, options.stateKey));
|
|
75
77
|
}
|
|
76
78
|
}
|
|
77
79
|
finally {
|
|
@@ -95,7 +97,7 @@ function getLocalWorkRuntime(task) {
|
|
|
95
97
|
return null;
|
|
96
98
|
}
|
|
97
99
|
}
|
|
98
|
-
return await environment.file.read(stateFileName);
|
|
100
|
+
return (0, work_state_record_1.parseStateRecord)(task, await environment.file.read(stateFileName));
|
|
99
101
|
},
|
|
100
102
|
async remove(environment) {
|
|
101
103
|
for (const generate of task.data.generates) {
|
|
@@ -109,6 +111,12 @@ function getLocalWorkRuntime(task) {
|
|
|
109
111
|
};
|
|
110
112
|
}
|
|
111
113
|
exports.getLocalWorkRuntime = getLocalWorkRuntime;
|
|
114
|
+
async function removeStateFile(environment, task) {
|
|
115
|
+
const stateFile = getStateFilename(task);
|
|
116
|
+
if (await environment.file.exists(stateFile)) {
|
|
117
|
+
await environment.file.remove(stateFile);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
112
120
|
async function archiveLocal(environment, task, path) {
|
|
113
121
|
for (const generatedArchive of (0, event_cache_1.getArchivePaths)(task.data, path)) {
|
|
114
122
|
await environment.file.writeStream(generatedArchive.filename,
|