hammerkit 2.0.0-next.7 → 2.0.0-next.8
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/dist/kubernetes/await-running-state.js +69 -5
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +10 -1
- package/dist/kubernetes/ensure-persistent-data.js +4 -5
- package/dist/kubernetes/quota-wait.js +97 -0
- package/dist/planner/work-runtime-kubernetes.js +4 -1
- package/dist/server/capacity.js +10 -43
- package/dist/utils/units.js +36 -1
- package/package.json +1 -1
|
@@ -6,6 +6,7 @@ const abort_event_1 = require("../utils/abort-event");
|
|
|
6
6
|
const apply_1 = require("./apply");
|
|
7
7
|
const sleep_1 = require("../utils/sleep");
|
|
8
8
|
const cluster_state_1 = require("./cluster-state");
|
|
9
|
+
const quota_wait_1 = require("./quota-wait");
|
|
9
10
|
// The waits below poll instead of using the client's Watch: Watch aborts every
|
|
10
11
|
// request after 30s, which a pod or deployment still pulling its image outlasts.
|
|
11
12
|
async function awaitRunningState(instance, env, name, phase, pollInterval = 1000) {
|
|
@@ -22,11 +23,16 @@ async function awaitRunningState(instance, env, name, phase, pollInterval = 1000
|
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
exports.awaitRunningState = awaitRunningState;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
async function awaitJobCompletion(instance, env, name, abort, pollInterval = 1000, quota) {
|
|
27
|
+
const wait = quota
|
|
28
|
+
? new quota_wait_1.QuotaWait(instance, env, {
|
|
29
|
+
subject: `the pod of job ${name}`,
|
|
30
|
+
timeout: quota.timeout,
|
|
31
|
+
onWaiting: quota.onWaiting,
|
|
32
|
+
})
|
|
33
|
+
: null;
|
|
34
|
+
// when the job last had a pod: a refusal before it is not one of the pod it waits for now
|
|
35
|
+
let hadPod = 0;
|
|
30
36
|
for (;;) {
|
|
31
37
|
if (abort.aborted) {
|
|
32
38
|
await deleteJob(instance, env, name);
|
|
@@ -41,10 +47,50 @@ async function awaitJobCompletion(instance, env, name, abort, pollInterval = 100
|
|
|
41
47
|
if (failed) {
|
|
42
48
|
throw new Error(`job ${name} failed: ${failed.message ?? failed.reason ?? 'unknown reason'}`);
|
|
43
49
|
}
|
|
50
|
+
if (wait) {
|
|
51
|
+
let refusal = null;
|
|
52
|
+
if (status?.active || status?.ready) {
|
|
53
|
+
hadPod = Date.now();
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
// the job controller can't make the pod: no pod to wait on, only its events say why
|
|
57
|
+
refusal = await readJobRefusal(instance, env, name, job, hadPod);
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
await wait.check(refusal);
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
await deleteJob(instance, env, name);
|
|
64
|
+
throw e;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
44
67
|
await sleep(pollInterval, abort);
|
|
45
68
|
}
|
|
46
69
|
}
|
|
47
70
|
exports.awaitJobCompletion = awaitJobCompletion;
|
|
71
|
+
// The last `FailedCreate` of the job for its quota after `after`. Events the server may not list
|
|
72
|
+
// leave the wait as it was.
|
|
73
|
+
async function readJobRefusal(instance, env, name, job, after) {
|
|
74
|
+
try {
|
|
75
|
+
const events = await instance.coreApi.listNamespacedEvent({
|
|
76
|
+
namespace: env.namespace,
|
|
77
|
+
fieldSelector: `involvedObject.kind=Job,involvedObject.name=${name},reason=FailedCreate`,
|
|
78
|
+
});
|
|
79
|
+
const uid = job.metadata?.uid;
|
|
80
|
+
const latest = events.items
|
|
81
|
+
.filter((event) => !uid || !event.involvedObject?.uid || event.involvedObject.uid === uid)
|
|
82
|
+
.filter((event) => eventTime(event) > after && (0, quota_wait_1.getQuotaRefusal)(event.message))
|
|
83
|
+
.sort((a, b) => eventTime(b) - eventTime(a))[0];
|
|
84
|
+
return (0, quota_wait_1.getQuotaRefusal)(latest?.message);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function eventTime(event) {
|
|
91
|
+
const at = event.lastTimestamp ?? event.eventTime ?? event.firstTimestamp ?? event.metadata?.creationTimestamp;
|
|
92
|
+
return at ? new Date(at).getTime() : 0;
|
|
93
|
+
}
|
|
48
94
|
function sleep(ms, abort) {
|
|
49
95
|
if (abort.aborted) {
|
|
50
96
|
return Promise.resolve();
|
|
@@ -77,6 +123,20 @@ async function deleteJob(instance, env, name) {
|
|
|
77
123
|
}
|
|
78
124
|
}
|
|
79
125
|
exports.deleteJob = deleteJob;
|
|
126
|
+
// The Deployment says when its ReplicaSet can't make a pod: `ReplicaFailure`, `FailedCreate`.
|
|
127
|
+
function getDeploymentRefusal(deployment) {
|
|
128
|
+
const failure = deployment.status?.conditions?.find((condition) => condition.type === 'ReplicaFailure' && condition.status === 'True');
|
|
129
|
+
return (0, quota_wait_1.getQuotaRefusal)(failure?.message);
|
|
130
|
+
}
|
|
131
|
+
function getDeployQuotaWait(instance, env, name, readiness, now) {
|
|
132
|
+
return new quota_wait_1.QuotaWait(instance, env, {
|
|
133
|
+
subject: readiness.subject ?? name,
|
|
134
|
+
// without a limit of its own it waits as long as the run does
|
|
135
|
+
timeout: readiness.quotaTimeout ?? Number.POSITIVE_INFINITY,
|
|
136
|
+
onWaiting: (text) => readiness.onWaiting?.(text),
|
|
137
|
+
now,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
80
140
|
// how long a reason that may still pass (an image pull a registry refused for now, a
|
|
81
141
|
// container the runtime could not create) may last before it fails the service
|
|
82
142
|
exports.PASSING_FAILURE_MS = 60000;
|
|
@@ -96,12 +156,14 @@ async function awaitDeployRunningState(instance, env, name, readiness, pollInter
|
|
|
96
156
|
let said;
|
|
97
157
|
// since when a pod has had a failure that may still pass, by pod
|
|
98
158
|
const failingSince = new Map();
|
|
159
|
+
const quota = getDeployQuotaWait(instance, env, name, readiness, now);
|
|
99
160
|
for (;;) {
|
|
100
161
|
const deployment = await instance.appsApi.readNamespacedDeployment({ name, namespace: env.namespace });
|
|
101
162
|
// available: ready for the Deployment's minReadySeconds
|
|
102
163
|
if ((deployment.status?.availableReplicas ?? 0) > 0) {
|
|
103
164
|
return;
|
|
104
165
|
}
|
|
166
|
+
await quota.check(getDeploymentRefusal(deployment));
|
|
105
167
|
const created = deployment.metadata?.creationTimestamp;
|
|
106
168
|
const listed = await instance.coreApi.listNamespacedPod({
|
|
107
169
|
namespace: env.namespace,
|
|
@@ -163,6 +225,7 @@ async function awaitDeployRollout(instance, env, name, readiness, pollInterval =
|
|
|
163
225
|
let runningSince = null;
|
|
164
226
|
let said;
|
|
165
227
|
const failingSince = new Map();
|
|
228
|
+
const quota = getDeployQuotaWait(instance, env, name, readiness, now);
|
|
166
229
|
for (;;) {
|
|
167
230
|
if (readiness.signal?.aborted) {
|
|
168
231
|
throw new abort_1.AbortError();
|
|
@@ -179,6 +242,7 @@ async function awaitDeployRollout(instance, env, name, readiness, pollInterval =
|
|
|
179
242
|
if (stalled && (status?.observedGeneration ?? 0) >= readiness.generation) {
|
|
180
243
|
throw new Error(`${timeoutMessage}: ${stalled.message ?? 'its rollout made no progress'}`);
|
|
181
244
|
}
|
|
245
|
+
await quota.check(getDeploymentRefusal(deployment));
|
|
182
246
|
const listed = await instance.coreApi.listNamespacedPod({
|
|
183
247
|
namespace: env.namespace,
|
|
184
248
|
labelSelector: readiness.podSelector,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getServiceContainerName = exports.MIN_READY_SECONDS = exports.getPodTemplateHash = exports.POD_ANNOTATION = exports.ROLLOUT_DEADLINE_SECONDS = exports.ensureKubernetesDeploymentExists = exports.DEFINITION_LABEL = void 0;
|
|
3
|
+
exports.getServiceContainerName = exports.MIN_READY_SECONDS = exports.getPodTemplateHash = exports.POD_ANNOTATION = exports.ROLLOUT_DEADLINE_SECONDS = exports.ensureKubernetesDeploymentExists = exports.DEBUG_CONTAINER_RESOURCES = 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,10 +17,15 @@ const job_placement_1 = require("./job-placement");
|
|
|
17
17
|
const network_policy_1 = require("./network-policy");
|
|
18
18
|
const egress_proxy_1 = require("../network/egress-proxy");
|
|
19
19
|
const service_rollouts_1 = require("./service-rollouts");
|
|
20
|
+
const quota_wait_1 = require("./quota-wait");
|
|
20
21
|
// what the service was created from, to tell a changed one
|
|
21
22
|
exports.DEFINITION_LABEL = 'hammerkit.dev/definition';
|
|
22
23
|
// how often a starting container is checked: as the liveness probe
|
|
23
24
|
const STARTUP_PERIOD_SECONDS = 5;
|
|
25
|
+
exports.DEBUG_CONTAINER_RESOURCES = {
|
|
26
|
+
requests: { cpu: '10m', memory: '16Mi' },
|
|
27
|
+
limits: { cpu: '100m', memory: '256Mi' },
|
|
28
|
+
};
|
|
24
29
|
async function ensureKubernetesDeploymentExists(instance, env, service, persistence, secrets, stateKey,
|
|
25
30
|
// the needed services by name, and the proxy of a limited network
|
|
26
31
|
network,
|
|
@@ -131,6 +136,9 @@ rollout = {}) {
|
|
|
131
136
|
command: ['sleep'],
|
|
132
137
|
args: ['3600'],
|
|
133
138
|
volumeMounts: persistence.mounts.map((m) => m.mount),
|
|
139
|
+
// a shell to look at the service's files: without a size of its own it would get
|
|
140
|
+
// the namespace's default, and count as much against its quota as the service
|
|
141
|
+
resources: exports.DEBUG_CONTAINER_RESOURCES,
|
|
134
142
|
...(0, job_placement_1.getContainerSecurity)(env),
|
|
135
143
|
},
|
|
136
144
|
],
|
|
@@ -149,6 +157,7 @@ rollout = {}) {
|
|
|
149
157
|
subject: `service ${service.name}`,
|
|
150
158
|
onWaiting: (text) => service.status.write('warn', `${service.name} ${text}`),
|
|
151
159
|
...(service.data.restarts !== null ? { restarts: service.data.restarts } : {}),
|
|
160
|
+
quotaTimeout: quota_wait_1.QUOTA_TIMEOUT,
|
|
152
161
|
};
|
|
153
162
|
if (env.server?.serviceRun) {
|
|
154
163
|
await rollOut(instance, env, service, deployment, readiness, rollout);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ensurePersistentData = exports.packSource = exports.clearStateFiles = exports.ENSURE_OUTPUT_FILES = exports.restoreLiveFiles = exports.syncLiveFiles = exports.SYNC_FILES = exports.getStateDirsInUse = exports.REMOVE_OTHER_STATES = exports.execInPod = exports.clearClaim = exports.getPodForPersistence = exports.applyWithinQuota = exports.QUOTA_TIMEOUT = exports.TRANSFER_POD_RESOURCES = void 0;
|
|
4
|
+
const quota_wait_1 = require("./quota-wait");
|
|
5
|
+
Object.defineProperty(exports, "QUOTA_TIMEOUT", { enumerable: true, get: function () { return quota_wait_1.QUOTA_TIMEOUT; } });
|
|
4
6
|
const volumes_1 = require("./volumes");
|
|
5
7
|
const apply_1 = require("./apply");
|
|
6
8
|
const path_1 = require("path");
|
|
@@ -50,12 +52,9 @@ exports.TRANSFER_POD_RESOURCES = {
|
|
|
50
52
|
requests: { cpu: '50m', memory: '64Mi' },
|
|
51
53
|
limits: { memory: '512Mi' },
|
|
52
54
|
};
|
|
53
|
-
// how long a transfer pod waits for room under the quota of its namespace, such as the
|
|
54
|
-
// pods of other tasks of the run ending, before its task fails
|
|
55
|
-
exports.QUOTA_TIMEOUT = 10 * 60 * 1000;
|
|
56
55
|
// A Job waits for room under a full quota, a pod is refused: so a transfer pod is
|
|
57
56
|
// created again until the pods of the tasks it runs beside are done and make room
|
|
58
|
-
async function applyWithinQuota(instance, podSpec, service, timeout =
|
|
57
|
+
async function applyWithinQuota(instance, podSpec, service, timeout = quota_wait_1.QUOTA_TIMEOUT, pollInterval = 2000,
|
|
59
58
|
// a run stopped while it waits: it stops waiting
|
|
60
59
|
signal) {
|
|
61
60
|
const deadline = Date.now() + timeout;
|
|
@@ -155,7 +154,7 @@ signal, fn) {
|
|
|
155
154
|
};
|
|
156
155
|
service.status.write('debug', `create pod ${name}`);
|
|
157
156
|
await removeStalePod(instance, env.namespace, name);
|
|
158
|
-
const pod = await applyWithinQuota(instance, podSpec, service,
|
|
157
|
+
const pod = await applyWithinQuota(instance, podSpec, service, quota_wait_1.QUOTA_TIMEOUT, 2000, signal);
|
|
159
158
|
if (pod.status?.phase !== 'Running') {
|
|
160
159
|
service.status.write('debug', `await running state ${name}`);
|
|
161
160
|
await (0, await_running_state_1.awaitRunningState)(instance, env, name, 'Running');
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.QuotaWait = exports.exceedsQuota = exports.getQuotaRefusal = exports.QUOTA_TIMEOUT = void 0;
|
|
4
|
+
const units_1 = require("../utils/units");
|
|
5
|
+
// How long a pod the quota of its namespace refuses waits for room before what needs it fails:
|
|
6
|
+
// a transfer pod in any case, a task's or service's only when no pod that will end holds the
|
|
7
|
+
// room (see QuotaWait)
|
|
8
|
+
exports.QUOTA_TIMEOUT = 10 * 60 * 1000;
|
|
9
|
+
// What a refusal of the quota of the namespace says, from where it starts: `exceeded quota:
|
|
10
|
+
// hammerkit, requested: limits.memory=4Gi, used: limits.memory=24Gi, limited: limits.memory=26Gi`
|
|
11
|
+
function getQuotaRefusal(message) {
|
|
12
|
+
const index = message ? message.search(/exceeded quota/i) : -1;
|
|
13
|
+
return message && index >= 0 ? message.slice(index) : null;
|
|
14
|
+
}
|
|
15
|
+
exports.getQuotaRefusal = getQuotaRefusal;
|
|
16
|
+
// A pod that asks for more of a resource than the quota allows at all never fits.
|
|
17
|
+
function exceedsQuota(refusal) {
|
|
18
|
+
const requested = readAmounts(/requested: (\S+?)(?:,\s|$)/.exec(refusal)?.[1]);
|
|
19
|
+
const limited = readAmounts(/limited: (\S+?)(?:,\s|$)/.exec(refusal)?.[1]);
|
|
20
|
+
return [...requested].some(([resource, amount]) => {
|
|
21
|
+
const limit = limited.get(resource);
|
|
22
|
+
return limit !== undefined && amount > limit;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
exports.exceedsQuota = exceedsQuota;
|
|
26
|
+
// `limits.memory=4Gi,requests.cpu=1` → limits.memory 4Gi, requests.cpu 1
|
|
27
|
+
function readAmounts(list = '') {
|
|
28
|
+
const amounts = new Map();
|
|
29
|
+
for (const entry of list.split(',')) {
|
|
30
|
+
const [resource, value] = entry.split('=');
|
|
31
|
+
const amount = (0, units_1.parseQuantity)(value);
|
|
32
|
+
if (resource && amount !== undefined) {
|
|
33
|
+
amounts.set(resource, amount);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return amounts;
|
|
37
|
+
}
|
|
38
|
+
// A pod that will end and free its room: one of a Job (a task) or a pod of its own (the copy of
|
|
39
|
+
// a task's files). The pods of a Deployment (a service) stay as long as what needs them.
|
|
40
|
+
function willEnd(pod) {
|
|
41
|
+
if (pod.metadata?.deletionTimestamp || (pod.status?.phase !== 'Pending' && pod.status?.phase !== 'Running')) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return !(pod.metadata?.ownerReferences ?? []).some((owner) => owner.kind === 'ReplicaSet');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The wait of a pod the quota of its namespace refuses. It is told what the quota refused,
|
|
48
|
+
* fails at once when the pod asks for more than the quota allows at all, and after `timeout`
|
|
49
|
+
* goes on waiting only while a pod that will end (a task's, another run's) holds room in the
|
|
50
|
+
* namespace: when only services hold it, they wait for what waits for them, and it fails.
|
|
51
|
+
*/
|
|
52
|
+
class QuotaWait {
|
|
53
|
+
constructor(instance, env, options) {
|
|
54
|
+
this.instance = instance;
|
|
55
|
+
this.env = env;
|
|
56
|
+
this.options = options;
|
|
57
|
+
this.since = null;
|
|
58
|
+
this.told = '';
|
|
59
|
+
}
|
|
60
|
+
// Called on each poll with what the quota refused now, or null when nothing is refused.
|
|
61
|
+
async check(refusal) {
|
|
62
|
+
const now = this.options.now ?? Date.now;
|
|
63
|
+
if (!refusal) {
|
|
64
|
+
this.since = null;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (refusal !== this.told) {
|
|
68
|
+
this.told = refusal;
|
|
69
|
+
this.options.onWaiting?.(`waits for room under the quota of the namespace: ${refusal}`);
|
|
70
|
+
}
|
|
71
|
+
if (exceedsQuota(refusal)) {
|
|
72
|
+
throw new Error(`${this.options.subject} asks for more than the quota of the namespace allows: ${refusal}`);
|
|
73
|
+
}
|
|
74
|
+
this.since = this.since ?? now();
|
|
75
|
+
if (now() - this.since < this.options.timeout) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (await this.othersWillEnd()) {
|
|
79
|
+
// checked again after another `timeout`
|
|
80
|
+
this.since = now();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
throw new Error(`${this.options.subject} did not fit the quota of the namespace within ${Math.round(this.options.timeout / 60000)} minutes, and only services hold its room: ${refusal}`);
|
|
84
|
+
}
|
|
85
|
+
async othersWillEnd() {
|
|
86
|
+
try {
|
|
87
|
+
const pods = await this.instance.coreApi.listNamespacedPod({ namespace: this.env.namespace });
|
|
88
|
+
return pods.items.some(willEnd);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// what it can not see may still end
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
exports.QuotaWait = QuotaWait;
|
|
97
|
+
//# sourceMappingURL=quota-wait.js.map
|
|
@@ -385,7 +385,10 @@ function kubernetesTaskRuntime(task, kubernetes) {
|
|
|
385
385
|
try {
|
|
386
386
|
if (!pod.status?.succeeded) {
|
|
387
387
|
try {
|
|
388
|
-
await (0, await_running_state_1.awaitJobCompletion)(instance, kubernetes, spec.metadata.name, options.abort
|
|
388
|
+
await (0, await_running_state_1.awaitJobCompletion)(instance, kubernetes, spec.metadata.name, options.abort, 1000, {
|
|
389
|
+
onWaiting: (text) => task.status.write('warn', `${task.name} ${text}`),
|
|
390
|
+
timeout: ensure_persistent_data_1.QUOTA_TIMEOUT,
|
|
391
|
+
});
|
|
389
392
|
}
|
|
390
393
|
catch (e) {
|
|
391
394
|
if (!(e instanceof abort_1.AbortError)) {
|
package/dist/server/capacity.js
CHANGED
|
@@ -7,41 +7,8 @@ const apply_1 = require("../kubernetes/apply");
|
|
|
7
7
|
const job_placement_1 = require("../kubernetes/job-placement");
|
|
8
8
|
const volumes_1 = require("../kubernetes/volumes");
|
|
9
9
|
const network_policy_1 = require("../kubernetes/network-policy");
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
u: 1e-6,
|
|
13
|
-
m: 1e-3,
|
|
14
|
-
'': 1,
|
|
15
|
-
k: 1e3,
|
|
16
|
-
K: 1e3,
|
|
17
|
-
M: 1e6,
|
|
18
|
-
G: 1e9,
|
|
19
|
-
T: 1e12,
|
|
20
|
-
P: 1e15,
|
|
21
|
-
E: 1e18,
|
|
22
|
-
Ki: 1024,
|
|
23
|
-
Mi: 1024 ** 2,
|
|
24
|
-
Gi: 1024 ** 3,
|
|
25
|
-
Ti: 1024 ** 4,
|
|
26
|
-
Pi: 1024 ** 5,
|
|
27
|
-
Ei: 1024 ** 6,
|
|
28
|
-
};
|
|
29
|
-
// A quantity of Kubernetes (`250m`, `1.5`, `512Mi`, `12345678n`, `1e3`), as a number.
|
|
30
|
-
function parseQuantity(value) {
|
|
31
|
-
if (value === undefined) {
|
|
32
|
-
return undefined;
|
|
33
|
-
}
|
|
34
|
-
if (typeof value === 'number') {
|
|
35
|
-
return value;
|
|
36
|
-
}
|
|
37
|
-
const match = /^([+-]?[0-9.]+(?:[eE][+-]?\d+)?)([a-zA-Z]*)$/.exec(value.trim());
|
|
38
|
-
if (!match || !(match[2] in UNITS)) {
|
|
39
|
-
return undefined;
|
|
40
|
-
}
|
|
41
|
-
const number = Number(match[1]);
|
|
42
|
-
return Number.isFinite(number) ? number * UNITS[match[2]] : undefined;
|
|
43
|
-
}
|
|
44
|
-
exports.parseQuantity = parseQuantity;
|
|
10
|
+
const units_1 = require("../utils/units");
|
|
11
|
+
Object.defineProperty(exports, "parseQuantity", { enumerable: true, get: function () { return units_1.parseQuantity; } });
|
|
45
12
|
function add(into, cpu, memory) {
|
|
46
13
|
if (cpu !== undefined) {
|
|
47
14
|
into.cpu = (into.cpu ?? 0) + cpu;
|
|
@@ -182,8 +149,8 @@ class KubernetesCapacity {
|
|
|
182
149
|
use.pods++;
|
|
183
150
|
for (const container of pod.spec?.containers ?? []) {
|
|
184
151
|
const requests = container.resources?.requests ?? {};
|
|
185
|
-
add(group.requested, parseQuantity(requests.cpu) ?? 0, parseQuantity(requests.memory) ?? 0);
|
|
186
|
-
add(use.requested, parseQuantity(requests.cpu) ?? 0, parseQuantity(requests.memory) ?? 0);
|
|
152
|
+
add(group.requested, (0, units_1.parseQuantity)(requests.cpu) ?? 0, (0, units_1.parseQuantity)(requests.memory) ?? 0);
|
|
153
|
+
add(use.requested, (0, units_1.parseQuantity)(requests.cpu) ?? 0, (0, units_1.parseQuantity)(requests.memory) ?? 0);
|
|
187
154
|
}
|
|
188
155
|
}
|
|
189
156
|
}
|
|
@@ -196,8 +163,8 @@ class KubernetesCapacity {
|
|
|
196
163
|
namespace,
|
|
197
164
|
});
|
|
198
165
|
const hard = quota.spec?.hard ?? {};
|
|
199
|
-
const cpu = parseQuantity(hard['requests.cpu'] ?? hard.cpu);
|
|
200
|
-
const memory = parseQuantity(hard['requests.memory'] ?? hard.memory);
|
|
166
|
+
const cpu = (0, units_1.parseQuantity)(hard['requests.cpu'] ?? hard.cpu);
|
|
167
|
+
const memory = (0, units_1.parseQuantity)(hard['requests.memory'] ?? hard.memory);
|
|
201
168
|
if (cpu !== undefined || memory !== undefined) {
|
|
202
169
|
group.quota = { ...(cpu !== undefined ? { cpu } : {}), ...(memory !== undefined ? { memory } : {}) };
|
|
203
170
|
}
|
|
@@ -215,10 +182,10 @@ class KubernetesCapacity {
|
|
|
215
182
|
for (const pod of metrics.items) {
|
|
216
183
|
const use = usesOfPods.get(pod.metadata?.name ?? '');
|
|
217
184
|
for (const container of pod.containers) {
|
|
218
|
-
add(used, parseQuantity(container.usage.cpu), parseQuantity(container.usage.memory));
|
|
185
|
+
add(used, (0, units_1.parseQuantity)(container.usage.cpu), (0, units_1.parseQuantity)(container.usage.memory));
|
|
219
186
|
if (use) {
|
|
220
187
|
use.used ?? (use.used = { cpu: 0, memory: 0 });
|
|
221
|
-
add(use.used, parseQuantity(container.usage.cpu), parseQuantity(container.usage.memory));
|
|
188
|
+
add(use.used, (0, units_1.parseQuantity)(container.usage.cpu), (0, units_1.parseQuantity)(container.usage.memory));
|
|
222
189
|
}
|
|
223
190
|
}
|
|
224
191
|
}
|
|
@@ -248,7 +215,7 @@ class KubernetesCapacity {
|
|
|
248
215
|
const allocatable = {};
|
|
249
216
|
for (const node of nodes.items) {
|
|
250
217
|
const of = node.status?.allocatable ?? {};
|
|
251
|
-
add(allocatable, parseQuantity(of.cpu), parseQuantity(of.memory));
|
|
218
|
+
add(allocatable, (0, units_1.parseQuantity)(of.cpu), (0, units_1.parseQuantity)(of.memory));
|
|
252
219
|
}
|
|
253
220
|
machines = { count: nodes.items.length, allocatable };
|
|
254
221
|
if (this.instance.metrics) {
|
|
@@ -256,7 +223,7 @@ class KubernetesCapacity {
|
|
|
256
223
|
const metrics = await this.instance.metrics.getNodeMetrics();
|
|
257
224
|
const nodesUsed = {};
|
|
258
225
|
for (const node of metrics.items) {
|
|
259
|
-
add(nodesUsed, parseQuantity(node.usage.cpu), parseQuantity(node.usage.memory));
|
|
226
|
+
add(nodesUsed, (0, units_1.parseQuantity)(node.usage.cpu), (0, units_1.parseQuantity)(node.usage.memory));
|
|
260
227
|
}
|
|
261
228
|
machines.used = nodesUsed;
|
|
262
229
|
}
|
package/dist/utils/units.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.parseCpus = exports.formatSize = exports.parseSize = exports.formatDuration = exports.parseDuration = void 0;
|
|
3
|
+
exports.parseQuantity = exports.parseCpus = exports.formatSize = exports.parseSize = exports.formatDuration = exports.parseDuration = void 0;
|
|
4
4
|
const DURATION_UNITS = {
|
|
5
5
|
ms: 1,
|
|
6
6
|
s: 1000,
|
|
@@ -97,4 +97,39 @@ function parseCpus(value) {
|
|
|
97
97
|
return millicores / 1000;
|
|
98
98
|
}
|
|
99
99
|
exports.parseCpus = parseCpus;
|
|
100
|
+
const QUANTITY_UNITS = {
|
|
101
|
+
n: 1e-9,
|
|
102
|
+
u: 1e-6,
|
|
103
|
+
m: 1e-3,
|
|
104
|
+
'': 1,
|
|
105
|
+
k: 1e3,
|
|
106
|
+
K: 1e3,
|
|
107
|
+
M: 1e6,
|
|
108
|
+
G: 1e9,
|
|
109
|
+
T: 1e12,
|
|
110
|
+
P: 1e15,
|
|
111
|
+
E: 1e18,
|
|
112
|
+
Ki: 1024,
|
|
113
|
+
Mi: 1024 ** 2,
|
|
114
|
+
Gi: 1024 ** 3,
|
|
115
|
+
Ti: 1024 ** 4,
|
|
116
|
+
Pi: 1024 ** 5,
|
|
117
|
+
Ei: 1024 ** 6,
|
|
118
|
+
};
|
|
119
|
+
// A quantity of Kubernetes (`250m`, `1.5`, `512Mi`, `12345678n`, `1e3`), as a number.
|
|
120
|
+
function parseQuantity(value) {
|
|
121
|
+
if (value === undefined) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
if (typeof value === 'number') {
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
const match = /^([+-]?[0-9.]+(?:[eE][+-]?\d+)?)([a-zA-Z]*)$/.exec(value.trim());
|
|
128
|
+
if (!match || !(match[2] in QUANTITY_UNITS)) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
const number = Number(match[1]);
|
|
132
|
+
return Number.isFinite(number) ? number * QUANTITY_UNITS[match[2]] : undefined;
|
|
133
|
+
}
|
|
134
|
+
exports.parseQuantity = parseQuantity;
|
|
100
135
|
//# sourceMappingURL=units.js.map
|
package/package.json
CHANGED