hammerkit 1.27.0-next.3 → 1.27.0-next.31
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 +188 -0
- package/dist/cache/backends/local-cache-backend.js +6 -0
- package/dist/cache/backends/registry/registry-client.js +1 -0
- package/dist/cache/backends/registry-cache-backend.js +2 -2
- package/dist/cache/explain.js +17 -0
- package/dist/cli.js +2 -0
- package/dist/dashboard/assets/index-CjnKm5-3.css +2 -0
- package/dist/dashboard/assets/index-zPXa4klZ.js +13 -0
- package/dist/dashboard/favicon.svg +1 -0
- package/dist/dashboard/index.html +15 -0
- package/dist/docker/item-network.js +13 -5
- package/dist/executer/deadline.js +17 -3
- package/dist/executer/deferred-restore.js +36 -0
- package/dist/executer/docker-service.js +30 -8
- package/dist/executer/docker-task.js +21 -1
- package/dist/executer/event-cache.js +20 -2
- package/dist/executer/execute-work-service.js +2 -0
- package/dist/executer/execute-work-task.js +10 -3
- package/dist/executer/execution-steps.js +25 -4
- package/dist/executer/reset-work-tree.js +4 -0
- package/dist/executer/scheduler/enqueue-next.js +57 -6
- package/dist/executer/scheduler/state-key.js +42 -5
- package/dist/executer/watch-loop.js +4 -1
- package/dist/harness/connect.js +20 -6
- package/dist/harness/mcp-command.js +22 -6
- package/dist/harness/mcp.js +63 -114
- package/dist/harness/opencode.js +158 -50
- package/dist/harness/pi.js +29 -0
- package/dist/harness/sandbox-client.js +168 -17
- package/dist/harness/service-tools.js +141 -0
- package/dist/harness/task-tools.js +221 -0
- package/dist/kubernetes/apply.js +27 -16
- package/dist/kubernetes/await-running-state.js +59 -13
- package/dist/kubernetes/claim-layout.js +71 -0
- package/dist/kubernetes/cluster-state.js +245 -0
- package/dist/kubernetes/ensure-http-route.js +1 -1
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +35 -12
- package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +1 -1
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +6 -9
- package/dist/kubernetes/ensure-persistent-data.js +30 -20
- package/dist/kubernetes/follow-pod-logs.js +182 -0
- package/dist/kubernetes/get-service-ip.js +2 -2
- package/dist/kubernetes/job-placement.js +36 -12
- package/dist/kubernetes/job-secrets.js +33 -19
- package/dist/kubernetes/kept-claims.js +22 -10
- package/dist/kubernetes/kubernetes-instance.js +1 -0
- package/dist/kubernetes/network-policy.js +85 -27
- package/dist/kubernetes/remove-persistent-data.js +3 -1
- package/dist/kubernetes/resources.js +25 -3
- package/dist/kubernetes/restore-kubernetes-data.js +9 -3
- package/dist/kubernetes/sandbox-runner.js +628 -0
- package/dist/kubernetes/secrets.js +7 -11
- package/dist/kubernetes/volumes.js +6 -3
- package/dist/main.js +2 -0
- package/dist/network/egress-proxy.js +45 -6
- package/dist/optimizer/calculate-checksum.js +3 -2
- package/dist/optimizer/check-key.js +94 -0
- package/dist/optimizer/check-keys.js +64 -0
- package/dist/optimizer/get-work-cache-stats.js +17 -3
- package/dist/optimizer/work-task-cache-description.js +51 -1
- package/dist/planner/utils/append-work-dependencies.js +10 -5
- package/dist/planner/utils/append-work-service.js +19 -1
- package/dist/planner/utils/append-work-task.js +18 -3
- package/dist/planner/utils/get-work-runtime.js +6 -1
- package/dist/planner/utils/normalize-path.js +19 -1
- package/dist/planner/utils/parse-work-mount.js +7 -1
- package/dist/planner/utils/parse-work-port.js +4 -1
- package/dist/planner/utils/task-arguments.js +3 -0
- package/dist/planner/validate.js +22 -0
- package/dist/planner/work-network.js +3 -1
- package/dist/planner/work-runtime-aggregate.js +32 -0
- package/dist/planner/work-runtime-docker.js +2 -1
- package/dist/planner/work-runtime-kubernetes.js +124 -48
- package/dist/planner/work-runtime-local.js +2 -1
- package/dist/planner/work-task.js +6 -1
- package/dist/program.js +31 -10
- package/dist/remote/device-login.js +7 -2
- package/dist/remote/host-config.js +69 -3
- package/dist/remote/remote-commands.js +33 -7
- package/dist/remote/remote-status.js +112 -0
- package/dist/remote/remote-work-commands.js +545 -45
- package/dist/remote/server-client.js +15 -2
- package/dist/remote/token-source.js +64 -26
- package/dist/run-program.js +9 -0
- package/dist/schema/build-file-base-task-schema.js +8 -1
- package/dist/schema/build-file-container-service-schema.js +12 -0
- package/dist/schema/build-file-container-task-schema.js +16 -1
- package/dist/schema/build-file-include-schema.js +18 -4
- package/dist/schema/build-file-kubernetes-service-schema.js +2 -0
- package/dist/schema/build-file-task-schema.js +5 -1
- package/dist/schema/capabilities-schema.js +56 -0
- package/dist/schema/reference-parser.js +120 -11
- package/dist/schema/schema-parser.js +92 -41
- package/dist/schema/work-scope-parser.js +33 -10
- package/dist/secrets/command-secret-provider.js +3 -1
- package/dist/secrets/github-app-token-command.js +3 -1
- package/dist/secrets/secret-catalog.js +43 -19
- package/dist/server/accounts-api.js +4 -1
- package/dist/server/approvals-api.js +1 -1
- package/dist/server/capacity-format.js +36 -0
- package/dist/server/capacity.js +300 -0
- package/dist/server/checks-api.js +83 -0
- package/dist/server/checks.js +116 -0
- package/dist/server/dashboard.js +196 -0
- package/dist/server/definitions-api.js +66 -3
- package/dist/server/endpoints.js +793 -0
- package/dist/server/executor.js +57 -5
- package/dist/server/kubernetes-backend.js +392 -65
- package/dist/server/kubernetes-sandbox-storage.js +48 -3
- package/dist/server/oauth.js +12 -1
- package/dist/server/overview.js +271 -0
- package/dist/server/plan.js +188 -103
- package/dist/server/policy.js +34 -2
- package/dist/server/proposals-api.js +17 -8
- package/dist/server/repo-access.js +5 -0
- package/dist/server/run-logs.js +4 -0
- package/dist/server/run-plan.js +50 -19
- package/dist/server/run-request.js +58 -3
- package/dist/server/run-worker.js +245 -12
- package/dist/server/runs.js +532 -35
- package/dist/server/sandbox-pod.js +28 -0
- package/dist/server/sandbox-storage.js +23 -0
- package/dist/server/sandboxes-api.js +10 -4
- package/dist/server/sandboxes.js +8 -0
- package/dist/server/server-command.js +39 -20
- package/dist/server/server-config.js +277 -14
- package/dist/server/server.js +402 -33
- package/dist/server/service-runs.js +82 -0
- package/dist/server/timings.js +132 -0
- package/dist/server/wait-reason.js +21 -0
- package/dist/utils/abort-on-signals.js +46 -6
- package/dist/utils/tar.js +36 -0
- package/dist/utils/terminal-prompt.js +53 -0
- package/package.json +8 -2
package/dist/kubernetes/apply.js
CHANGED
|
@@ -36,28 +36,39 @@ exports.apply = apply;
|
|
|
36
36
|
* of a policy).
|
|
37
37
|
*/
|
|
38
38
|
async function replace(instance, spec) {
|
|
39
|
-
let
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
+
}
|
|
46
57
|
}
|
|
47
58
|
try {
|
|
48
|
-
return await instance.objectApi.
|
|
59
|
+
return await instance.objectApi.replace({
|
|
60
|
+
...spec,
|
|
61
|
+
metadata: { ...spec.metadata, resourceVersion: current.metadata?.resourceVersion },
|
|
62
|
+
});
|
|
49
63
|
}
|
|
50
|
-
catch (
|
|
51
|
-
|
|
52
|
-
|
|
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;
|
|
53
69
|
}
|
|
54
|
-
current = await instance.objectApi.read(spec);
|
|
55
70
|
}
|
|
56
71
|
}
|
|
57
|
-
return await instance.objectApi.replace({
|
|
58
|
-
...spec,
|
|
59
|
-
metadata: { ...spec.metadata, resourceVersion: current.metadata?.resourceVersion },
|
|
60
|
-
});
|
|
61
72
|
}
|
|
62
73
|
exports.replace = replace;
|
|
63
74
|
//# sourceMappingURL=apply.js.map
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.stopJob = exports.deleteJobAndWait = exports.awaitDeployRunningState = exports.deleteJob = exports.awaitJobCompletion = exports.awaitRunningState = void 0;
|
|
3
|
+
exports.stopJob = exports.deleteJobAndWait = exports.awaitDeployRunningState = exports.PASSING_FAILURE_MS = 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");
|
|
7
7
|
const sleep_1 = require("../utils/sleep");
|
|
8
|
+
const cluster_state_1 = require("./cluster-state");
|
|
8
9
|
// The waits below poll instead of using the client's Watch: Watch aborts every
|
|
9
10
|
// request after 30s, which a pod or deployment still pulling its image outlasts.
|
|
10
11
|
async function awaitRunningState(instance, env, name, phase, pollInterval = 1000) {
|
|
@@ -76,29 +77,74 @@ async function deleteJob(instance, env, name) {
|
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
exports.deleteJob = deleteJob;
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
|
|
80
|
+
// how long a reason that may still pass (an image pull a registry refused for now, a
|
|
81
|
+
// container the runtime could not create) may last before it fails the service
|
|
82
|
+
exports.PASSING_FAILURE_MS = 60000;
|
|
83
|
+
// Waits for an available replica: a ready one, for a service without a healthcheck one that
|
|
84
|
+
// stayed ready for a while. The deadline only starts once the container runs, and again
|
|
85
|
+
// at each start of it, so an image pull or a back-off doesn't count against it (as on
|
|
86
|
+
// docker, where it starts after the container started). A pod that won't get there without a change (an image
|
|
87
|
+
// that isn't there, a container that crashed once more than its
|
|
88
|
+
// service's `restarts`, 2 by default) fails it at once, with
|
|
89
|
+
// the reason the cluster gives; one that may still pass fails once it lasted a minute.
|
|
90
|
+
// Only the pods of this Deployment count: not the ones of one deleted before it was
|
|
91
|
+
// created again, which may still be there, terminating or not yet.
|
|
82
92
|
async function awaitDeployRunningState(instance, env, name, readiness, pollInterval = 1000, now = Date.now) {
|
|
83
93
|
let runningSince = null;
|
|
94
|
+
// how often the container was restarted when last looked at
|
|
95
|
+
let restarts = 0;
|
|
96
|
+
let said;
|
|
97
|
+
// since when a pod has had a failure that may still pass, by pod
|
|
98
|
+
const failingSince = new Map();
|
|
84
99
|
for (;;) {
|
|
85
100
|
const deployment = await instance.appsApi.readNamespacedDeployment({ name, namespace: env.namespace });
|
|
86
|
-
|
|
101
|
+
// available: ready for the Deployment's minReadySeconds
|
|
102
|
+
if ((deployment.status?.availableReplicas ?? 0) > 0) {
|
|
87
103
|
return;
|
|
88
104
|
}
|
|
105
|
+
const created = deployment.metadata?.creationTimestamp;
|
|
106
|
+
const listed = await instance.coreApi.listNamespacedPod({
|
|
107
|
+
namespace: env.namespace,
|
|
108
|
+
labelSelector: readiness.podSelector,
|
|
109
|
+
});
|
|
110
|
+
const pods = listed.items.filter((pod) => !pod.metadata?.deletionTimestamp &&
|
|
111
|
+
!(created && pod.metadata?.creationTimestamp && pod.metadata.creationTimestamp < created));
|
|
112
|
+
for (const [index, pod] of pods.entries()) {
|
|
113
|
+
const key = pod.metadata?.name ?? `${index}`;
|
|
114
|
+
const failure = (0, cluster_state_1.getPodFailure)(pod, readiness.container, readiness.restarts);
|
|
115
|
+
if (!failure) {
|
|
116
|
+
failingSince.delete(key);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const since = failingSince.get(key) ?? now();
|
|
120
|
+
failingSince.set(key, since);
|
|
121
|
+
if (failure.final || now() - since >= exports.PASSING_FAILURE_MS) {
|
|
122
|
+
throw new Error(`${readiness.subject ?? name} can not become ready, ${failure.text}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const waiting = pods.map((pod) => (0, cluster_state_1.getPodWaiting)(pod, readiness.container)).find((text) => !!text);
|
|
126
|
+
if (waiting && waiting !== said) {
|
|
127
|
+
readiness.onWaiting?.(waiting);
|
|
128
|
+
}
|
|
129
|
+
said = waiting;
|
|
89
130
|
if (readiness.timeout !== null) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
131
|
+
// the deadline bounds each start of the container, as on Docker: a crash is counted by
|
|
132
|
+
// `restarts`, and the back-off before the next start doesn't count against it
|
|
133
|
+
const statuses = pods.flatMap((pod) => (pod.status?.containerStatuses ?? []).filter((status) => !readiness.container || status.name === readiness.container));
|
|
134
|
+
const running = statuses.length > 0
|
|
135
|
+
? statuses.some((status) => status.state?.running)
|
|
136
|
+
: pods.some((pod) => pod.status?.phase === 'Running');
|
|
137
|
+
const restarted = Math.max(0, ...statuses.map((status) => status.restartCount ?? 0));
|
|
138
|
+
if (!running) {
|
|
139
|
+
runningSince = null;
|
|
140
|
+
}
|
|
141
|
+
else if (runningSince === null || restarted !== restarts) {
|
|
142
|
+
runningSince = now();
|
|
98
143
|
}
|
|
99
144
|
else if (now() - runningSince >= readiness.timeout) {
|
|
100
145
|
throw new Error(readiness.timeoutMessage);
|
|
101
146
|
}
|
|
147
|
+
restarts = restarted;
|
|
102
148
|
}
|
|
103
149
|
await (0, sleep_1.sleep)(pollInterval);
|
|
104
150
|
}
|
|
@@ -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
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readClusterState = exports.describeEvent = exports.describePod = exports.getPodWaiting = exports.describeLastExit = exports.getPodFailure = exports.CRASHES_TO_FAIL = void 0;
|
|
4
|
+
const log_1 = require("../log");
|
|
5
|
+
const apply_1 = require("./apply");
|
|
6
|
+
const job_placement_1 = require("./job-placement");
|
|
7
|
+
const network_gate_1 = require("./network-gate");
|
|
8
|
+
const resources_1 = require("./resources");
|
|
9
|
+
// what a pod's or event's message is cut to
|
|
10
|
+
const MAX_MESSAGE = 500;
|
|
11
|
+
// how many events of a run are said, the newest
|
|
12
|
+
const MAX_EVENTS = 20;
|
|
13
|
+
// events the result of a task says better: a Job of a failed command
|
|
14
|
+
const SAID_ELSEWHERE = ['BackoffLimitExceeded'];
|
|
15
|
+
// A container restarted this often (ended three times) is taken as one that keeps crashing,
|
|
16
|
+
// unless its service sets `restarts`: once may be a race with a dependency or a slow first start
|
|
17
|
+
exports.CRASHES_TO_FAIL = 2;
|
|
18
|
+
// what a pull error says when the image is not there to pull, or not for this cluster:
|
|
19
|
+
// waiting does not change it. Others (a registry that limits, a token not there yet) may pass.
|
|
20
|
+
const IMAGE_GONE = /not found|manifest unknown|unauthorized|denied|forbidden|invalid reference|no such host/i;
|
|
21
|
+
/**
|
|
22
|
+
* Why a pod of a service will not become ready without a change, as the cluster says it:
|
|
23
|
+
* an image that can not be pulled, a container that keeps crashing or can not be created,
|
|
24
|
+
* an init container that failed. Null while it may still get there. A container fails once
|
|
25
|
+
* it was restarted `restarts` times and crashed again.
|
|
26
|
+
*
|
|
27
|
+
* Only what the cluster wrote is quoted: the termination message of a container is its
|
|
28
|
+
* own output (and may hold a value it read), except for the network gate's, which says why
|
|
29
|
+
* the policy did not take hold.
|
|
30
|
+
*/
|
|
31
|
+
function getPodFailure(pod, container, restarts = exports.CRASHES_TO_FAIL) {
|
|
32
|
+
for (const status of pod.status?.initContainerStatuses ?? []) {
|
|
33
|
+
const terminated = status.state?.terminated ?? status.lastState?.terminated;
|
|
34
|
+
if (!terminated || terminated.exitCode === 0) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (status.name === network_gate_1.NETWORK_GATE_CONTAINER) {
|
|
38
|
+
return {
|
|
39
|
+
text: `the network policy did not take hold: ${cut(terminated.message?.trim() || 'the gate failed')}`,
|
|
40
|
+
final: true,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (status.restartCount >= exports.CRASHES_TO_FAIL) {
|
|
44
|
+
return { text: `its init container ${status.name} failed with exit code ${terminated.exitCode}`, final: true };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const status of mainStatuses(pod, container)) {
|
|
48
|
+
const failure = getContainerFailure(status, restartsEnded(pod, status), restarts);
|
|
49
|
+
if (failure) {
|
|
50
|
+
return failure;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
exports.getPodFailure = getPodFailure;
|
|
56
|
+
function getContainerFailure(status, restartsEnded, restarts) {
|
|
57
|
+
const waiting = status.state?.waiting;
|
|
58
|
+
const reason = waiting?.reason;
|
|
59
|
+
const said = waiting?.message ? `: ${cut(waiting.message)}` : '';
|
|
60
|
+
switch (reason) {
|
|
61
|
+
// the first pull: final only when the registry said the image is not there, as older
|
|
62
|
+
// kubelets leave that out of the ImagePullBackOff that follows
|
|
63
|
+
case 'ErrImagePull':
|
|
64
|
+
return IMAGE_GONE.test(waiting?.message ?? '')
|
|
65
|
+
? { text: `its image ${status.image} could not be pulled (${reason})${said}`, final: true }
|
|
66
|
+
: null;
|
|
67
|
+
case 'ImagePullBackOff':
|
|
68
|
+
return {
|
|
69
|
+
text: `its image ${status.image} could not be pulled (${reason})${said}`,
|
|
70
|
+
final: IMAGE_GONE.test(waiting?.message ?? ''),
|
|
71
|
+
};
|
|
72
|
+
case 'ErrImageNeverPull':
|
|
73
|
+
case 'InvalidImageName':
|
|
74
|
+
return { text: `its image ${status.image} could not be pulled (${reason})${said}`, final: true };
|
|
75
|
+
case 'CreateContainerConfigError':
|
|
76
|
+
case 'CreateContainerError':
|
|
77
|
+
case 'RunContainerError':
|
|
78
|
+
return { text: `its container could not be started (${reason})${said}`, final: false };
|
|
79
|
+
case 'CrashLoopBackOff':
|
|
80
|
+
return status.restartCount >= restarts
|
|
81
|
+
? { text: `it keeps crashing: ${describeLastExit(status)}`, final: true }
|
|
82
|
+
: null;
|
|
83
|
+
}
|
|
84
|
+
// a kubelet may leave an instance that ended as it is until its back-off is over, and say
|
|
85
|
+
// CrashLoopBackOff only when it looks at the pod again, a minute later: the same crash
|
|
86
|
+
if (restartsEnded && status.state?.terminated && status.restartCount >= restarts) {
|
|
87
|
+
return { text: `it keeps crashing: ${describeLastExit(status)}`, final: true };
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
// How the last instance of a container ended, as the cluster says it.
|
|
92
|
+
function describeLastExit(status) {
|
|
93
|
+
// the instance that ended now, else the one before the one that waits or runs
|
|
94
|
+
const last = status.state?.terminated ?? status.lastState?.terminated;
|
|
95
|
+
const restarts = `, restarted ${status.restartCount} time${status.restartCount === 1 ? '' : 's'}`;
|
|
96
|
+
if (!last) {
|
|
97
|
+
return `it ended${restarts}`;
|
|
98
|
+
}
|
|
99
|
+
if (last.reason === 'OOMKilled') {
|
|
100
|
+
return `it was killed for using more memory than its limit (OOMKilled)${restarts}`;
|
|
101
|
+
}
|
|
102
|
+
// the kubelet kills a container whose liveness probe (its healthcheck) keeps failing
|
|
103
|
+
const killed = last.exitCode === 137 || last.exitCode === 143 ? ', killed as when its healthcheck kept failing' : '';
|
|
104
|
+
return `it exited with code ${last.exitCode}${last.reason ? ` (${last.reason})` : ''}${killed}${restarts}`;
|
|
105
|
+
}
|
|
106
|
+
exports.describeLastExit = describeLastExit;
|
|
107
|
+
/**
|
|
108
|
+
* What a pod of a service waits for, in a few words, while it is not ready: a node, its
|
|
109
|
+
* image, its container. Undefined when it waits for nothing the cluster names.
|
|
110
|
+
*/
|
|
111
|
+
function getPodWaiting(pod, container) {
|
|
112
|
+
const scheduled = pod.status?.conditions?.find((condition) => condition.type === 'PodScheduled');
|
|
113
|
+
if (scheduled?.status === 'False') {
|
|
114
|
+
return `waits for a node: ${cut(scheduled.message ?? scheduled.reason ?? 'not scheduled yet')}`;
|
|
115
|
+
}
|
|
116
|
+
for (const status of mainStatuses(pod, container)) {
|
|
117
|
+
const waiting = status.state?.waiting;
|
|
118
|
+
const said = waiting?.message ? `: ${cut(waiting.message)}` : '';
|
|
119
|
+
switch (waiting?.reason) {
|
|
120
|
+
case 'ErrImagePull':
|
|
121
|
+
case 'ImagePullBackOff':
|
|
122
|
+
return `can not pull its image ${status.image} yet${said}`;
|
|
123
|
+
case 'CreateContainerConfigError':
|
|
124
|
+
case 'CreateContainerError':
|
|
125
|
+
case 'RunContainerError':
|
|
126
|
+
return `can not start its container yet (${waiting.reason})${said}`;
|
|
127
|
+
case 'CrashLoopBackOff':
|
|
128
|
+
return `crashed and starts again: ${describeLastExit(status)}`;
|
|
129
|
+
}
|
|
130
|
+
const ended = status.state?.terminated;
|
|
131
|
+
if (ended && restartsEnded(pod, status)) {
|
|
132
|
+
return `${ended.exitCode === 0 ? 'ended' : 'crashed'} and starts again: ${describeLastExit(status)}`;
|
|
133
|
+
}
|
|
134
|
+
if (status.restartCount > 0 && status.state?.running && !status.ready) {
|
|
135
|
+
return `restarted: ${describeLastExit(status)}`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
exports.getPodWaiting = getPodWaiting;
|
|
141
|
+
// Whether the kubelet starts a container that ended again: not one of a pod that finished (a
|
|
142
|
+
// task's Job, an evicted pod, which its ReplicaSet replaces) or that never restarts.
|
|
143
|
+
function restartsEnded(pod, status) {
|
|
144
|
+
const phase = pod.status?.phase;
|
|
145
|
+
if (phase === 'Succeeded' || phase === 'Failed') {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
const policy = pod.spec?.restartPolicy ?? 'Always';
|
|
149
|
+
return policy === 'Always' || (policy === 'OnFailure' && status.state?.terminated?.exitCode !== 0);
|
|
150
|
+
}
|
|
151
|
+
function mainStatuses(pod, container) {
|
|
152
|
+
return (pod.status?.containerStatuses ?? []).filter((status) => !container || status.name === container);
|
|
153
|
+
}
|
|
154
|
+
// One line of a pod that is not as it should be, or null for one that runs, ready, or ended well.
|
|
155
|
+
function describePod(pod) {
|
|
156
|
+
const name = pod.metadata?.name ?? '?';
|
|
157
|
+
const phase = pod.status?.phase ?? 'Unknown';
|
|
158
|
+
const failure = getPodFailure(pod)?.text;
|
|
159
|
+
const waiting = getPodWaiting(pod);
|
|
160
|
+
const unready = (pod.status?.containerStatuses ?? [])
|
|
161
|
+
.filter((status) => !status.ready && !status.state?.terminated)
|
|
162
|
+
.map((status) => status.name);
|
|
163
|
+
const crashed = (pod.status?.containerStatuses ?? []).filter((status) => (status.state?.terminated && status.state.terminated.exitCode !== 0) || status.restartCount > 0);
|
|
164
|
+
if (!failure && !waiting && crashed.length === 0 && (phase === 'Succeeded' || unready.length === 0)) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
const said = failure ?? waiting;
|
|
168
|
+
const parts = [phase, ...(said ? [said] : [])];
|
|
169
|
+
for (const status of crashed) {
|
|
170
|
+
if (!said?.includes(describeLastExit(status))) {
|
|
171
|
+
parts.push(`${status.name}: ${describeLastExit(status)}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (!said && unready.length > 0) {
|
|
175
|
+
parts.push(`not ready: ${unready.join(', ')}`);
|
|
176
|
+
}
|
|
177
|
+
return `pod ${name}: ${parts.join(', ')}`;
|
|
178
|
+
}
|
|
179
|
+
exports.describePod = describePod;
|
|
180
|
+
/**
|
|
181
|
+
* The message of a warning event, as far as it is the cluster's: a failed probe says the
|
|
182
|
+
* output of the command, which is the service's, and is left out.
|
|
183
|
+
*/
|
|
184
|
+
function describeEvent(event) {
|
|
185
|
+
const object = event.involvedObject;
|
|
186
|
+
const message = (event.message ?? '').replace(/^((Readiness|Liveness|Startup) probe (failed|errored)).*$/s, '$1');
|
|
187
|
+
const count = (event.count ?? 1) > 1 ? ` (${event.count} times)` : '';
|
|
188
|
+
return `${event.reason ?? 'Warning'} ${object.kind?.toLowerCase() ?? 'object'}/${object.name ?? '?'}: ${cut(message.trim())}${count}`;
|
|
189
|
+
}
|
|
190
|
+
exports.describeEvent = describeEvent;
|
|
191
|
+
/**
|
|
192
|
+
* What the cluster said of a run that failed, into its log before the server removes its
|
|
193
|
+
* objects: its pods that are not as they should be, and the warning events of its objects.
|
|
194
|
+
* In the namespace of a service run everything is the run's; in a caller's namespace, which
|
|
195
|
+
* runs share, only the objects of the run's items (named by their key) are.
|
|
196
|
+
*/
|
|
197
|
+
async function readClusterState(instance, kubernetes, workTree, ownNamespace) {
|
|
198
|
+
const namespace = kubernetes.namespace;
|
|
199
|
+
const keys = [...Object.values(workTree.tasks), ...Object.values(workTree.services)].map((item) => (0, resources_1.getItemKey)(item, kubernetes));
|
|
200
|
+
const ours = (name) => ownNamespace || keys.some((key) => name?.includes(key));
|
|
201
|
+
const lines = [];
|
|
202
|
+
try {
|
|
203
|
+
const labels = (0, job_placement_1.getRunLabels)(kubernetes);
|
|
204
|
+
const labelSelector = Object.entries(labels)
|
|
205
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
206
|
+
.join(',');
|
|
207
|
+
const pods = await instance.coreApi.listNamespacedPod({
|
|
208
|
+
namespace,
|
|
209
|
+
...(labelSelector && !ownNamespace ? { labelSelector } : {}),
|
|
210
|
+
});
|
|
211
|
+
for (const pod of pods.items) {
|
|
212
|
+
const line = ours(pod.metadata?.name) ? describePod(pod) : null;
|
|
213
|
+
if (line) {
|
|
214
|
+
lines.push(line);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch (e) {
|
|
219
|
+
lines.push(`its pods could not be read: ${(0, log_1.getErrorMessage)(e)}`);
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
const events = await instance.coreApi.listNamespacedEvent({ namespace, fieldSelector: 'type=Warning' });
|
|
223
|
+
const warnings = events.items
|
|
224
|
+
.filter((event) => ours(event.involvedObject.name) && !SAID_ELSEWHERE.includes(event.reason ?? ''))
|
|
225
|
+
.sort((a, b) => eventTime(a) - eventTime(b))
|
|
226
|
+
.slice(-MAX_EVENTS);
|
|
227
|
+
lines.push(...warnings.map((event) => `event ${describeEvent(event)}`));
|
|
228
|
+
}
|
|
229
|
+
catch (e) {
|
|
230
|
+
lines.push((0, apply_1.statusCodeOf)(e) === 403
|
|
231
|
+
? `its events could not be read: the runRole of the server needs list on events (see the backend docs)`
|
|
232
|
+
: `its events could not be read: ${(0, log_1.getErrorMessage)(e)}`);
|
|
233
|
+
}
|
|
234
|
+
return lines;
|
|
235
|
+
}
|
|
236
|
+
exports.readClusterState = readClusterState;
|
|
237
|
+
function eventTime(event) {
|
|
238
|
+
const time = event.lastTimestamp ?? event.eventTime ?? event.firstTimestamp ?? event.metadata?.creationTimestamp;
|
|
239
|
+
return time ? new Date(time).getTime() : 0;
|
|
240
|
+
}
|
|
241
|
+
function cut(text) {
|
|
242
|
+
const line = text.replace(/\s+/g, ' ');
|
|
243
|
+
return line.length > MAX_MESSAGE ? `${line.slice(0, MAX_MESSAGE)}…` : line;
|
|
244
|
+
}
|
|
245
|
+
//# sourceMappingURL=cluster-state.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,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ensureKubernetesDeploymentExists = exports.DEFINITION_LABEL = void 0;
|
|
3
|
+
exports.getServiceContainerName = exports.MIN_READY_SECONDS = exports.ensureKubernetesDeploymentExists = exports.DEFINITION_LABEL = void 0;
|
|
4
4
|
const service_definition_1 = require("../planner/service-definition");
|
|
5
5
|
const work_service_1 = require("../planner/work-service");
|
|
6
6
|
const volumes_1 = require("./volumes");
|
|
@@ -17,11 +17,13 @@ const network_policy_1 = require("./network-policy");
|
|
|
17
17
|
const egress_proxy_1 = require("../network/egress-proxy");
|
|
18
18
|
// what the service was created from, to tell a changed one
|
|
19
19
|
exports.DEFINITION_LABEL = 'hammerkit.dev/definition';
|
|
20
|
+
// how often a starting container is checked: as the liveness probe
|
|
21
|
+
const STARTUP_PERIOD_SECONDS = 5;
|
|
20
22
|
async function ensureKubernetesDeploymentExists(instance, env, service, persistence, secrets, stateKey,
|
|
21
23
|
// the needed services by name, and the proxy of a limited network
|
|
22
24
|
network) {
|
|
23
25
|
const envs = (0, replace_env_variables_1.getEnvironmentVariables)(service.data.envs);
|
|
24
|
-
const name = (0, resources_1.getResourceName)(service);
|
|
26
|
+
const name = (0, resources_1.getResourceName)(service, env);
|
|
25
27
|
const healthcheckCommand = (0, work_service_1.getHealthcheckCommand)(service.data);
|
|
26
28
|
const serviceCommand = (0, work_service_1.getServiceCommand)(service.data, { kubernetes: true });
|
|
27
29
|
const probe = healthcheckCommand
|
|
@@ -34,6 +36,16 @@ network) {
|
|
|
34
36
|
failureThreshold: 3,
|
|
35
37
|
}
|
|
36
38
|
: undefined;
|
|
39
|
+
// the healthcheck's `timeout` for each start: until it passed once, the liveness probe
|
|
40
|
+
// doesn't kill a container that starts slowly, and the wait fails it with the timeout
|
|
41
|
+
const startupProbe = probe && service.data.healthcheck
|
|
42
|
+
? {
|
|
43
|
+
...probe,
|
|
44
|
+
// a check more than the timeout holds, and one more for the first check's offset: the
|
|
45
|
+
// wait fails it with the timeout before the kubelet restarts it
|
|
46
|
+
failureThreshold: Math.ceil(service.data.healthcheck.timeout / (STARTUP_PERIOD_SECONDS * 1000)) + 2,
|
|
47
|
+
}
|
|
48
|
+
: undefined;
|
|
37
49
|
const server = env.server;
|
|
38
50
|
const placement = server ? (0, job_placement_1.getPodPlacement)(server, undefined) : null;
|
|
39
51
|
const workspace = (0, volumes_1.getWorkspacePodSpec)(env, persistence);
|
|
@@ -47,25 +59,24 @@ network) {
|
|
|
47
59
|
'hammerkit.dev/version': (0, version_1.getVersion)(),
|
|
48
60
|
},
|
|
49
61
|
labels: {
|
|
50
|
-
|
|
62
|
+
...(0, resources_1.getItemLabels)(service, env),
|
|
51
63
|
'hammerkit.dev/state': stateKey,
|
|
52
|
-
...(0, job_placement_1.getRunLabels)(env),
|
|
53
64
|
[exports.DEFINITION_LABEL]: (0, service_definition_1.getServiceDefinitionHash)(service),
|
|
54
65
|
},
|
|
55
66
|
},
|
|
56
67
|
spec: {
|
|
57
68
|
selector: {
|
|
58
|
-
matchLabels:
|
|
59
|
-
'hammerkit.dev/id': service.id(),
|
|
60
|
-
},
|
|
69
|
+
matchLabels: (0, resources_1.getItemLabels)(service, env),
|
|
61
70
|
},
|
|
62
71
|
replicas: 1,
|
|
72
|
+
// without a healthcheck a pod is ready as soon as its container runs, also one that
|
|
73
|
+
// exits a second later: it counts once it ran this long without a restart
|
|
74
|
+
minReadySeconds: probe ? 0 : exports.MIN_READY_SECONDS,
|
|
63
75
|
template: {
|
|
64
76
|
metadata: {
|
|
65
77
|
labels: {
|
|
66
|
-
|
|
78
|
+
...(0, resources_1.getItemLabels)(service, env),
|
|
67
79
|
'hammerkit.dev/state': stateKey,
|
|
68
|
-
...(0, job_placement_1.getRunLabels)(env),
|
|
69
80
|
...workspace.labels,
|
|
70
81
|
...(0, network_policy_1.getNetworkLabels)(env, service),
|
|
71
82
|
},
|
|
@@ -87,7 +98,7 @@ network) {
|
|
|
87
98
|
: {}),
|
|
88
99
|
containers: [
|
|
89
100
|
{
|
|
90
|
-
name: service
|
|
101
|
+
name: getServiceContainerName(service),
|
|
91
102
|
image: service.data.image,
|
|
92
103
|
workingDir: (0, work_service_1.getServiceWorkingDir)(service.data),
|
|
93
104
|
env: [
|
|
@@ -104,6 +115,7 @@ network) {
|
|
|
104
115
|
})),
|
|
105
116
|
readinessProbe: probe,
|
|
106
117
|
livenessProbe: probe,
|
|
118
|
+
startupProbe,
|
|
107
119
|
...(0, container_resources_1.getKubernetesResources)(service.data.resources),
|
|
108
120
|
volumeMounts: [...persistence.mounts.map((m) => m.mount), ...secrets.mounts],
|
|
109
121
|
...(0, job_placement_1.getContainerSecurity)(env),
|
|
@@ -123,18 +135,29 @@ network) {
|
|
|
123
135
|
},
|
|
124
136
|
};
|
|
125
137
|
const deploy = await (0, apply_1.apply)(instance, deployment);
|
|
126
|
-
if (deploy.status?.
|
|
138
|
+
if ((deploy.status?.availableReplicas ?? 0) > 0) {
|
|
127
139
|
return;
|
|
128
140
|
}
|
|
129
141
|
await (0, await_running_state_1.awaitDeployRunningState)(instance, env, name, {
|
|
130
|
-
podSelector:
|
|
142
|
+
podSelector: (0, resources_1.getItemSelector)(service, env),
|
|
131
143
|
timeout: service.data.healthcheck?.timeout ?? null,
|
|
132
144
|
timeoutMessage: service.data.healthcheck
|
|
133
145
|
? (0, work_healthcheck_1.getHealthcheckTimeoutMessage)(service.name, service.data.healthcheck)
|
|
134
146
|
: '',
|
|
147
|
+
container: getServiceContainerName(service),
|
|
148
|
+
subject: `service ${service.name}`,
|
|
149
|
+
onWaiting: (text) => service.status.write('warn', `${service.name} ${text}`),
|
|
150
|
+
...(service.data.restarts !== null ? { restarts: service.data.restarts } : {}),
|
|
135
151
|
});
|
|
136
152
|
}
|
|
137
153
|
exports.ensureKubernetesDeploymentExists = ensureKubernetesDeploymentExists;
|
|
154
|
+
// how long the container of a service without a healthcheck has to run before it counts as up
|
|
155
|
+
exports.MIN_READY_SECONDS = 10;
|
|
156
|
+
// The container of a service's pod that runs its command, beside the `debug` one.
|
|
157
|
+
function getServiceContainerName(service) {
|
|
158
|
+
return service.name.replace(/:/, '-');
|
|
159
|
+
}
|
|
160
|
+
exports.getServiceContainerName = getServiceContainerName;
|
|
138
161
|
// Kubernetes `command` replaces the image entrypoint and `args` its command.
|
|
139
162
|
// A shell service sets both; an exec-form `cmd` keeps the established mapping
|
|
140
163
|
// (first token as `command`, the rest as `args`).
|
|
@@ -65,7 +65,7 @@ async function ensureKubernetesPersistentVolumeClaimExists(instance, env, volume
|
|
|
65
65
|
},
|
|
66
66
|
},
|
|
67
67
|
};
|
|
68
|
-
service.status.
|
|
68
|
+
service.status.write('debug', `ensure volume ${volume.name}`);
|
|
69
69
|
// Don't block on the PVC reaching "Bound". Storage classes with
|
|
70
70
|
// volumeBindingMode WaitForFirstConsumer (docker-desktop's "standard",
|
|
71
71
|
// k3d/k3s local-path, most cloud defaults) keep the claim Pending until a pod
|
|
@@ -4,11 +4,10 @@ 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
|
-
const job_placement_1 = require("./job-placement");
|
|
8
7
|
// A service without ports gets a headless Service: a ClusterIP one needs a port,
|
|
9
8
|
// and its tasks reach every port of the pod, as on docker.
|
|
10
9
|
async function ensureKubernetesServiceExists(instance, env, service) {
|
|
11
|
-
const name = (0, resources_1.getResourceName)(service);
|
|
10
|
+
const name = (0, resources_1.getResourceName)(service, env);
|
|
12
11
|
const headless = service.data.ports.length === 0;
|
|
13
12
|
const svc = {
|
|
14
13
|
kind: 'Service',
|
|
@@ -19,21 +18,19 @@ async function ensureKubernetesServiceExists(instance, env, service) {
|
|
|
19
18
|
annotations: {
|
|
20
19
|
'hammerkit.dev/version': (0, version_1.getVersion)(),
|
|
21
20
|
},
|
|
22
|
-
labels:
|
|
23
|
-
'hammerkit.dev/id': service.id(),
|
|
24
|
-
...(0, job_placement_1.getRunLabels)(env),
|
|
25
|
-
},
|
|
21
|
+
labels: (0, resources_1.getItemLabels)(service, env),
|
|
26
22
|
},
|
|
27
23
|
spec: {
|
|
28
24
|
type: 'ClusterIP',
|
|
29
25
|
...(headless ? { clusterIP: 'None' } : {}),
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
},
|
|
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),
|
|
33
28
|
...(headless
|
|
34
29
|
? {}
|
|
35
30
|
: {
|
|
31
|
+
// a Service of more than one port needs a name for each
|
|
36
32
|
ports: service.data.ports.map((port) => ({
|
|
33
|
+
name: `tcp-${port.hostPort ?? port.containerPort}`,
|
|
37
34
|
port: port.hostPort ?? port.containerPort,
|
|
38
35
|
targetPort: port.containerPort,
|
|
39
36
|
})),
|