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.
@@ -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
- // Wait for a job to finish by polling its status: resolves once it succeeded,
26
- // rejects when it failed (its `Failed` condition, i.e. retries are exhausted).
27
- // On abort the job is deleted — with its pods — and an AbortError is thrown, so
28
- // a cancelled or timed-out task leaves nothing running on the cluster.
29
- async function awaitJobCompletion(instance, env, name, abort, pollInterval = 1000) {
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 = exports.QUOTA_TIMEOUT, pollInterval = 2000,
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, exports.QUOTA_TIMEOUT, 2000, signal);
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)) {
@@ -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 UNITS = {
11
- n: 1e-9,
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
  }
@@ -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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hammerkit",
3
3
  "description": "Containerized build tool with incremental caching that runs the same build file locally, in Docker and on Kubernetes.",
4
- "version": "2.0.0-next.7",
4
+ "version": "2.0.0-next.8",
5
5
  "keywords": [
6
6
  "build",
7
7
  "build-tool",