teraslice 2.17.3 → 3.0.0-dev.0

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.
@@ -1,414 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- // @ts-expect-error no types found
4
- import barbe from 'barbe';
5
- import { isNumber, has, get, set, merge, isEmpty } from '@terascope/utils';
6
- import { safeEncode } from '../../../../../utils/encoding_utils.js';
7
- import { setMaxOldSpaceViaEnv } from './utils.js';
8
- const resourcePath = path.join(process.cwd(), './packages/teraslice/src/lib/cluster/services/cluster/backends/kubernetes/');
9
- export class K8sResource {
10
- execution;
11
- jobLabelPrefix;
12
- jobPropertyLabelPrefix;
13
- logger;
14
- nodeType;
15
- nameInfix;
16
- terasliceConfig;
17
- templateGenerator;
18
- templateConfig;
19
- resource;
20
- /**
21
- * K8sResource allows the generation of k8s resources based on templates.
22
- * After creating the object, the k8s resource is accessible on the objects
23
- * .resource property.
24
- *
25
- * @param {String} resourceType - jobs/services/deployments
26
- * @param {String} resourceName - worker/execution_controller
27
- * @param {Object} terasliceConfig - teraslice cluster config from context
28
- * @param {Object} execution - teraslice execution
29
- */
30
- constructor(resourceType, resourceName, terasliceConfig, execution, logger) {
31
- this.execution = execution;
32
- this.jobLabelPrefix = 'job.teraslice.terascope.io';
33
- this.jobPropertyLabelPrefix = 'job-property.teraslice.terascope.io';
34
- this.logger = logger;
35
- this.nodeType = resourceName;
36
- this.terasliceConfig = terasliceConfig;
37
- if (resourceName === 'worker') {
38
- this.nameInfix = 'wkr';
39
- }
40
- else if (resourceName === 'execution_controller') {
41
- this.nameInfix = 'exc';
42
- }
43
- else {
44
- throw new Error(`Unsupported resourceName: ${resourceName}`);
45
- }
46
- this.templateGenerator = this._makeTemplate(resourceType, resourceName);
47
- this.templateConfig = this._makeConfig();
48
- this.resource = this.templateGenerator(this.templateConfig);
49
- this._setJobLabels();
50
- // Apply job `targets` setting as k8s nodeAffinity
51
- // We assume that multiple targets require both to match ...
52
- // NOTE: If you specify multiple `matchExpressions` associated with
53
- // `nodeSelectorTerms`, then the pod can be scheduled onto a node
54
- // only if *all* `matchExpressions` can be satisfied.
55
- this._setTargets();
56
- this._setResources();
57
- this._setVolumes();
58
- if (process.env.MOUNT_LOCAL_TERASLICE !== undefined) {
59
- this._mountLocalTeraslice();
60
- }
61
- this._setAssetsVolume();
62
- this._setImagePullSecret();
63
- this._setEphemeralStorage();
64
- this._setExternalPorts();
65
- this._setPriorityClassName();
66
- if (resourceName === 'worker') {
67
- this._setWorkerAntiAffinity();
68
- }
69
- // Execution controller targets are required nodeAffinities, if
70
- // required job targets are also supplied, then *all* of the matches
71
- // will have to be satisfied for the job to be scheduled. This also
72
- // adds tolerations for any specified targets
73
- if (resourceName === 'execution_controller') {
74
- this._setExecutionControllerTargets();
75
- }
76
- if (this.terasliceConfig.kubernetes_overrides_enabled) {
77
- this._mergePodSpecOverlay();
78
- }
79
- }
80
- _mountLocalTeraslice() {
81
- const devMounts = JSON.parse(Buffer.from(process.env.MOUNT_LOCAL_TERASLICE, 'base64').toString('utf-8'));
82
- this.resource.spec.template.spec.containers[0].volumeMounts.push(...devMounts.volumeMounts);
83
- this.resource.spec.template.spec.volumes.push(...devMounts.volumes);
84
- this.resource.spec.template.spec.containers[0].args = [
85
- 'node',
86
- 'service.js'
87
- ];
88
- }
89
- _makeConfig() {
90
- const clusterName = get(this.terasliceConfig, 'name');
91
- const clusterNameLabel = clusterName.replace(/[^a-zA-Z0-9_\-.]/g, '_').substring(0, 63);
92
- const configMapName = get(this.terasliceConfig, 'kubernetes_config_map_name', `${this.terasliceConfig.name}-worker`);
93
- const dockerImage = this.execution.kubernetes_image
94
- || this.terasliceConfig.kubernetes_image;
95
- // name needs to be a valid DNS name since it is used in the svc name,
96
- // so we can only permit alphanumeric and - characters. _ is forbidden.
97
- // -> regex used for validation is '[a-z]([-a-z0-9]*[a-z0-9])?'
98
- const jobNameLabel = this.execution.name
99
- .toLowerCase()
100
- .replace(/[^a-zA-Z0-9\-.]/g, '-')
101
- .replace(/^[^a-z]/, 'a')
102
- .replace(/[^a-z0-9]$/, '0')
103
- .substring(0, 63);
104
- const name = `ts-${this.nameInfix}-${jobNameLabel.substring(0, 35)}-${this.execution.job_id.substring(0, 13)}`;
105
- const shutdownTimeoutMs = get(this.terasliceConfig, 'shutdown_timeout', 60000);
106
- const shutdownTimeoutSeconds = Math.round(shutdownTimeoutMs / 1000);
107
- const config = {
108
- // assetsDirectory: get(this.terasliceConfig, 'assets_directory', ''),
109
- // assetsVolume: get(this.terasliceConfig, 'assets_volume', ''),
110
- clusterName,
111
- clusterNameLabel,
112
- configMapName,
113
- dockerImage,
114
- execution: safeEncode(this.execution),
115
- exId: this.execution.ex_id,
116
- // @ts-expect-error TODO: not sure where these come from
117
- exName: this.execution.k8sName,
118
- // @ts-expect-error TODO: not sure where these come from
119
- exUid: this.execution.k8sUid,
120
- jobId: this.execution.job_id,
121
- jobNameLabel,
122
- name,
123
- namespace: get(this.terasliceConfig, 'kubernetes_namespace', 'default'),
124
- nodeType: this.nodeType,
125
- replicas: this.execution.workers,
126
- shutdownTimeout: shutdownTimeoutSeconds
127
- };
128
- return config;
129
- }
130
- _makeTemplate(folder, fileName) {
131
- const filePath = path.join(resourcePath, folder, `${fileName}.hbs`);
132
- const templateData = fs.readFileSync(filePath, 'utf-8');
133
- const templateKeys = ['{{', '}}'];
134
- return (config) => {
135
- const templated = barbe(templateData, templateKeys, config);
136
- return JSON.parse(templated);
137
- };
138
- }
139
- _setWorkerAntiAffinity() {
140
- if (this.terasliceConfig.kubernetes_worker_antiaffinity) {
141
- const targetKey = 'spec.template.spec.affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution';
142
- if (!has(this.resource, targetKey)) {
143
- set(this.resource, targetKey, []);
144
- }
145
- this.resource.spec.template.spec.affinity
146
- .podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution.push({
147
- weight: 1,
148
- podAffinityTerm: {
149
- labelSelector: {
150
- matchExpressions: [
151
- {
152
- key: 'app.kubernetes.io/name',
153
- operator: 'In',
154
- values: [
155
- 'teraslice'
156
- ]
157
- },
158
- {
159
- key: 'app.kubernetes.io/instance',
160
- operator: 'In',
161
- values: [
162
- this.templateConfig.clusterNameLabel
163
- ]
164
- }
165
- ]
166
- },
167
- topologyKey: 'kubernetes.io/hostname'
168
- }
169
- });
170
- }
171
- }
172
- /**
173
- * Execution Controllers get tolerations and required affinities
174
- *
175
- * NOTE: We considered changing `execution_controller_targets` to be an
176
- * object but the inconsistency with `targets` made this awkward. See the
177
- * `teraslice config with execution_controller_targets and job targets set`
178
- * test for an example. If the syntax for this were to change, we should
179
- * also consider changing `execution.targets`, which is a change on the job.
180
- */
181
- _setExecutionControllerTargets() {
182
- if (this.terasliceConfig.execution_controller_targets) {
183
- this.terasliceConfig.execution_controller_targets.forEach((target) => {
184
- this._setTargetRequired(target);
185
- this._setTargetAccepted(target);
186
- });
187
- }
188
- }
189
- _setEphemeralStorage() {
190
- if (this.execution.ephemeral_storage) {
191
- this.resource.spec.template.spec.containers[0].volumeMounts.push({
192
- name: 'ephemeral-volume',
193
- mountPath: '/ephemeral0'
194
- });
195
- this.resource.spec.template.spec.volumes.push({
196
- name: 'ephemeral-volume',
197
- emptyDir: {}
198
- });
199
- }
200
- }
201
- _setExternalPorts() {
202
- if (this.execution.external_ports) {
203
- this.execution.external_ports.forEach((portValue) => {
204
- if (isNumber(portValue)) {
205
- this.resource.spec.template.spec.containers[0].ports
206
- .push({ containerPort: portValue });
207
- }
208
- else {
209
- this.resource.spec.template.spec.containers[0].ports
210
- .push({
211
- name: portValue.name,
212
- containerPort: portValue.port
213
- });
214
- }
215
- });
216
- }
217
- }
218
- _setImagePullSecret() {
219
- if (this.terasliceConfig.kubernetes_image_pull_secret) {
220
- this.resource.spec.template.spec.imagePullSecrets = [
221
- { name: this.terasliceConfig.kubernetes_image_pull_secret }
222
- ];
223
- }
224
- }
225
- _setPriorityClassName() {
226
- if (this.terasliceConfig.kubernetes_priority_class_name) {
227
- const className = this.terasliceConfig.kubernetes_priority_class_name;
228
- if (this.nodeType === 'execution_controller') {
229
- this.resource.spec.template.spec.priorityClassName = className;
230
- if (this.execution.stateful) {
231
- this.resource.spec.template.metadata.labels[`${this.jobPropertyLabelPrefix}/stateful`] = 'true';
232
- }
233
- }
234
- if (this.nodeType === 'worker' && this.execution.stateful) {
235
- this.resource.spec.template.spec.priorityClassName = className;
236
- this.resource.spec.template.metadata.labels[`${this.jobPropertyLabelPrefix}/stateful`] = 'true';
237
- }
238
- }
239
- }
240
- _setAssetsVolume() {
241
- if (this.terasliceConfig.assets_directory && this.terasliceConfig.assets_volume) {
242
- this.resource.spec.template.spec.volumes.push({
243
- name: this.terasliceConfig.assets_volume,
244
- persistentVolumeClaim: { claimName: this.terasliceConfig.assets_volume }
245
- });
246
- this.resource.spec.template.spec.containers[0].volumeMounts.push({
247
- name: this.terasliceConfig.assets_volume,
248
- mountPath: this.terasliceConfig.assets_directory
249
- });
250
- }
251
- }
252
- _setJobLabels() {
253
- if (this.execution.labels != null) {
254
- Object.entries(this.execution.labels).forEach(([k, v]) => {
255
- const key = `${this.jobLabelPrefix}/${k.replace(/[^a-zA-Z0-9\-._]/g, '-').substring(0, 63)}`;
256
- const value = v.replace(/[^a-zA-Z0-9\-._]/g, '-').substring(0, 63);
257
- this.resource.metadata.labels[key] = value;
258
- if (this.resource.kind !== 'Service') {
259
- // Services don't have templates, so if it's a service,
260
- // don't add this
261
- this.resource.spec.template.metadata.labels[key] = value;
262
- }
263
- });
264
- }
265
- }
266
- _setVolumes() {
267
- if (this.execution.volumes != null) {
268
- this.execution.volumes.forEach((volume) => {
269
- this.resource.spec.template.spec.volumes.push({
270
- name: volume.name,
271
- persistentVolumeClaim: { claimName: volume.name }
272
- });
273
- this.resource.spec.template.spec.containers[0].volumeMounts.push({
274
- name: volume.name,
275
- mountPath: volume.path
276
- });
277
- });
278
- }
279
- }
280
- _setResources() {
281
- let cpu;
282
- let memory;
283
- let maxMemory;
284
- const container = this.resource.spec.template.spec.containers[0];
285
- // use teraslice config as defaults and execution config will override it
286
- const envVars = Object.assign({}, this.terasliceConfig.env_vars, this.execution.env_vars);
287
- if (this.nodeType === 'worker') {
288
- if (this.execution.resources_requests_cpu
289
- || this.execution.resources_limits_cpu) {
290
- if (this.execution.resources_requests_cpu) {
291
- set(container, 'resources.requests.cpu', this.execution.resources_requests_cpu);
292
- }
293
- if (this.execution.resources_limits_cpu) {
294
- set(container, 'resources.limits.cpu', this.execution.resources_limits_cpu);
295
- }
296
- }
297
- else if (this.execution.cpu || this.terasliceConfig.cpu) {
298
- // The settings on the executions override the cluster configs
299
- cpu = this.execution.cpu || this.terasliceConfig.cpu || -1;
300
- set(container, 'resources.requests.cpu', cpu);
301
- set(container, 'resources.limits.cpu', cpu);
302
- }
303
- if (this.execution.resources_requests_memory
304
- || this.execution.resources_limits_memory) {
305
- set(container, 'resources.requests.memory', this.execution.resources_requests_memory);
306
- set(container, 'resources.limits.memory', this.execution.resources_limits_memory);
307
- maxMemory = this.execution.resources_limits_memory;
308
- }
309
- else if (this.execution.memory || this.terasliceConfig.memory) {
310
- // The settings on the executions override the cluster configs
311
- memory = this.execution.memory || this.terasliceConfig.memory || -1;
312
- set(container, 'resources.requests.memory', memory);
313
- set(container, 'resources.limits.memory', memory);
314
- maxMemory = memory;
315
- }
316
- }
317
- if (this.nodeType === 'execution_controller') {
318
- // The settings on the executions override the cluster configs
319
- cpu = this.execution.cpu_execution_controller
320
- || this.terasliceConfig.cpu_execution_controller || -1;
321
- memory = this.execution.memory_execution_controller
322
- || this.terasliceConfig.memory_execution_controller || -1;
323
- set(container, 'resources.requests.cpu', cpu);
324
- set(container, 'resources.limits.cpu', cpu);
325
- set(container, 'resources.requests.memory', memory);
326
- set(container, 'resources.limits.memory', memory);
327
- maxMemory = memory;
328
- }
329
- // NOTE: This sucks, this manages the memory env var but it ALSO is
330
- // responsible for doing the config and execution env var merge, which
331
- // should NOT be in this function
332
- setMaxOldSpaceViaEnv(container.env, envVars, maxMemory);
333
- }
334
- _setTargets() {
335
- if (this.execution.targets && !isEmpty(this.execution.targets)) {
336
- this.execution.targets.forEach((target) => {
337
- // `required` is the default if no `constraint` is provided for
338
- // backwards compatibility and as the most likely case
339
- if (target.constraint === 'required' || !has(target, 'constraint')) {
340
- this._setTargetRequired(target);
341
- }
342
- if (target.constraint === 'preferred') {
343
- this._setTargetPreferred(target);
344
- }
345
- if (target.constraint === 'accepted') {
346
- this._setTargetAccepted(target);
347
- }
348
- });
349
- }
350
- }
351
- _setTargetRequired(target) {
352
- const targetKey = 'spec.template.spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution';
353
- if (!has(this.resource, targetKey)) {
354
- const nodeSelectorObj = {
355
- nodeSelectorTerms: [{ matchExpressions: [] }]
356
- };
357
- set(this.resource, targetKey, nodeSelectorObj);
358
- }
359
- this.resource.spec.template.spec.affinity.nodeAffinity
360
- .requiredDuringSchedulingIgnoredDuringExecution
361
- .nodeSelectorTerms[0].matchExpressions.push({
362
- key: target.key,
363
- operator: 'In',
364
- values: [target.value]
365
- });
366
- }
367
- _setTargetPreferred(target) {
368
- const targetKey = 'spec.template.spec.affinity.nodeAffinity.preferredDuringSchedulingIgnoredDuringExecution';
369
- if (!has(this.resource, targetKey)) {
370
- set(this.resource, targetKey, []);
371
- }
372
- this.resource.spec.template.spec.affinity.nodeAffinity
373
- .preferredDuringSchedulingIgnoredDuringExecution.push({
374
- weight: 1,
375
- preference: {
376
- matchExpressions: [{
377
- key: target.key,
378
- operator: 'In',
379
- values: [target.value]
380
- }]
381
- }
382
- });
383
- }
384
- _setTargetAccepted(target) {
385
- const targetKey = 'spec.template.spec.tolerations';
386
- if (!has(this.resource, targetKey)) {
387
- set(this.resource, targetKey, []);
388
- }
389
- this.resource.spec.template.spec.tolerations.push({
390
- key: target.key,
391
- operator: 'Equal',
392
- value: target.value,
393
- effect: 'NoSchedule'
394
- });
395
- }
396
- /**
397
- * _mergePodSpecOverlay - allows the author of the job to override anything
398
- * in the pod .spec for both the execution controller and the worker pods
399
- * created in Kubernetes. This can be useful in many ways including these:
400
- *
401
- * * add `initContainers` to the pods
402
- * * add `hostAliases` to the pods
403
- *
404
- * Note that this happens at the end of the process, so anything added by
405
- * this overlay will overwrite any other setting set on the job or by the
406
- * config.
407
- *
408
- * Job setting: `pod_spec_override`
409
- */
410
- _mergePodSpecOverlay() {
411
- this.resource.spec.template.spec = merge(this.resource.spec.template.spec, this.execution.pod_spec_override);
412
- }
413
- }
414
- //# sourceMappingURL=k8sResource.js.map
@@ -1,59 +0,0 @@
1
- import { get, has, uniq, difference } from '@terascope/utils';
2
- /**
3
- * Given the k8s Pods API output generates the appropriate Teraslice cluster
4
- * state. NOTE: This assumes the pods have already been filtered to ensure they
5
- * are teraslice pods and match the cluster in question.
6
- * @param {Object} k8sPods k8s pods API object (k8s v1.10+)
7
- * @param {Object} clusterState Teraslice Cluster State
8
- * @param {String} clusterNameLabel k8s label containing clusterName
9
- */
10
- export function gen(k8sPods, clusterState) {
11
- // Make sure we clean up the old
12
- const hostIPs = uniq(k8sPods.items.map((item) => get(item, 'status.hostIP')));
13
- const oldHostIps = difference(Object.keys(clusterState), hostIPs);
14
- oldHostIps.forEach((ip) => {
15
- delete clusterState[ip];
16
- });
17
- // Loop over the nodes in clusterState and set active = [] so we can append
18
- // later
19
- Object.keys(clusterState).forEach((nodeId) => {
20
- clusterState[nodeId].active = [];
21
- });
22
- // add a worker for each pod
23
- k8sPods.items.forEach((pod) => {
24
- if (!has(clusterState, pod.status.hostIP)) {
25
- // If the node isn't in clusterState, add it
26
- clusterState[pod.status.hostIP] = {
27
- node_id: pod.status.hostIP,
28
- hostname: pod.status.hostIP,
29
- pid: 'N/A',
30
- node_version: 'N/A',
31
- teraslice_version: 'N/A',
32
- total: 'N/A',
33
- state: 'connected',
34
- available: 'N/A',
35
- active: []
36
- };
37
- }
38
- const worker = {
39
- assets: [],
40
- assignment: pod.metadata.labels['app.kubernetes.io/component'],
41
- ex_id: pod.metadata.labels['teraslice.terascope.io/exId'],
42
- // WARNING: This makes the assumption that the first container
43
- // in the pod is the teraslice container. Currently it is the
44
- // only container, so this assumption is safe for now.
45
- image: pod.spec.containers[0].image,
46
- job_id: pod.metadata.labels['teraslice.terascope.io/jobId'],
47
- pod_name: pod.metadata.name,
48
- pod_ip: pod.status.podIP,
49
- worker_id: pod.metadata.name,
50
- };
51
- // k8s pods can have status.phase = `Pending`, `Running`, `Succeeded`,
52
- // `Failed`, `Unknown`. We will only add `Running` pods to the
53
- // Teraslice cluster state.
54
- if (pod.status.phase === 'Running') {
55
- clusterState[pod.status.hostIP].active.push(worker);
56
- }
57
- });
58
- }
59
- //# sourceMappingURL=k8sState.js.map
@@ -1,43 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- // @ts-expect-error
4
- import barbe from 'barbe';
5
- import { isTest } from '@terascope/utils';
6
- const MAX_RETRIES = isTest ? 2 : 3;
7
- const RETRY_DELAY = isTest ? 50 : 1000; // time in ms
8
- const resourcePath = path.join(process.cwd(), './packages/teraslice/src/lib/cluster/services/cluster/backends/kubernetes/');
9
- export function makeTemplate(folder, fileName) {
10
- const filePath = path.join(resourcePath, folder, `${fileName}.hbs`);
11
- const templateData = fs.readFileSync(filePath, 'utf-8');
12
- const templateKeys = ['{{', '}}'];
13
- return (config) => {
14
- const templated = barbe(templateData, templateKeys, config);
15
- return JSON.parse(templated);
16
- };
17
- }
18
- // Convert bytes to MB and reduce by 10%
19
- export function getMaxOldSpace(memory) {
20
- return Math.round(0.9 * (memory / 1024 / 1024));
21
- }
22
- export function setMaxOldSpaceViaEnv(envArr, jobEnv, memory) {
23
- const envObj = {};
24
- if (memory && memory > -1) {
25
- // Set NODE_OPTIONS to override max-old-space-size
26
- const maxOldSpace = getMaxOldSpace(memory);
27
- envObj.NODE_OPTIONS = `--max-old-space-size=${maxOldSpace}`;
28
- }
29
- Object.assign(envObj, jobEnv);
30
- Object.entries(envObj).forEach(([name, value]) => {
31
- envArr.push({
32
- name,
33
- value
34
- });
35
- });
36
- }
37
- export function getRetryConfig() {
38
- return {
39
- retries: MAX_RETRIES,
40
- delay: RETRY_DELAY
41
- };
42
- }
43
- //# sourceMappingURL=utils.js.map