@nosana/node 1.1.71 → 1.1.72-rc

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.
Files changed (74) hide show
  1. package/README.md +2 -2
  2. package/dist/.env +2 -0
  3. package/dist/package.json +9 -6
  4. package/dist/{src/NodeManager/provider/containerOrchestration → packages/container-orchestrator/src}/docker/createDockerRunOptions.d.ts +1 -1
  5. package/dist/{src/NodeManager/provider/containerOrchestration → packages/container-orchestrator/src}/docker/createDockerRunOptions.js +5 -1
  6. package/dist/{src/NodeManager/provider/containerOrchestration → packages/container-orchestrator/src}/docker/dockerExtended/index.d.ts +3 -0
  7. package/dist/{src/NodeManager/provider/containerOrchestration → packages/container-orchestrator/src}/docker/dockerExtended/index.js +12 -5
  8. package/dist/{src/NodeManager/provider/containerOrchestration → packages/container-orchestrator/src}/docker/index.d.ts +11 -10
  9. package/dist/{src/NodeManager/provider/containerOrchestration → packages/container-orchestrator/src}/docker/index.js +18 -37
  10. package/dist/packages/container-orchestrator/src/hooks.d.ts +32 -0
  11. package/dist/packages/container-orchestrator/src/index.d.ts +17 -0
  12. package/dist/packages/container-orchestrator/src/index.js +16 -0
  13. package/dist/{src/NodeManager/provider/containerOrchestration → packages/container-orchestrator/src}/interface.d.ts +14 -5
  14. package/dist/packages/container-orchestrator/src/interface.js +1 -0
  15. package/dist/{src/NodeManager/provider/containerOrchestration → packages/container-orchestrator/src}/labels.d.ts +2 -0
  16. package/dist/{src/NodeManager/provider/containerOrchestration → packages/container-orchestrator/src}/labels.js +2 -0
  17. package/dist/{src/NodeManager/provider/containerOrchestration → packages/container-orchestrator/src}/podman/index.d.ts +3 -3
  18. package/dist/{src/NodeManager/provider/containerOrchestration → packages/container-orchestrator/src}/podman/index.js +6 -17
  19. package/dist/{src/NodeManager/provider/containerOrchestration → packages/container-orchestrator/src}/podman/utils/createPodmanRunOptions.d.ts +12 -0
  20. package/dist/{src/NodeManager/provider/containerOrchestration → packages/container-orchestrator/src}/podman/utils/createPodmanRunOptions.js +4 -2
  21. package/dist/packages/container-orchestrator/src/selectContainerOrchestration.d.ts +3 -0
  22. package/dist/{src/NodeManager/provider/containerOrchestration → packages/container-orchestrator/src}/selectContainerOrchestration.js +3 -3
  23. package/dist/packages/container-orchestrator/src/types.d.ts +15 -0
  24. package/dist/packages/container-orchestrator/src/types.js +1 -0
  25. package/dist/packages/container-orchestrator/src/utils/ifStringCastToArray.d.ts +2 -0
  26. package/dist/packages/container-orchestrator/src/utils/ifStringCastToArray.js +2 -0
  27. package/dist/src/NodeManager/configs/configs.d.ts +6 -0
  28. package/dist/src/NodeManager/configs/configs.js +6 -0
  29. package/dist/src/NodeManager/index.js +7 -0
  30. package/dist/src/NodeManager/node/Node.d.ts +5 -0
  31. package/dist/src/NodeManager/node/Node.js +23 -2
  32. package/dist/src/NodeManager/node/health/healthHandler.d.ts +1 -1
  33. package/dist/src/NodeManager/node/resource/definition/index.d.ts +0 -1
  34. package/dist/src/NodeManager/node/resource/definition/index.js +0 -1
  35. package/dist/src/NodeManager/node/resource/helpers/createHFArgs.d.ts +1 -1
  36. package/dist/src/NodeManager/node/resource/helpers/createHFArgs.js +2 -3
  37. package/dist/src/NodeManager/node/resource/helpers/createOllamaArgs.d.ts +1 -1
  38. package/dist/src/NodeManager/node/resource/helpers/createOllamaArgs.js +2 -3
  39. package/dist/src/NodeManager/node/resource/helpers/createS3Args.d.ts +1 -1
  40. package/dist/src/NodeManager/node/resource/helpers/createS3Args.js +3 -3
  41. package/dist/src/NodeManager/node/resource/image/imageManager.d.ts +1 -1
  42. package/dist/src/NodeManager/node/resource/resourceManager.d.ts +1 -1
  43. package/dist/src/NodeManager/node/resource/volume/volumeManager.d.ts +1 -1
  44. package/dist/src/NodeManager/node/resource/volume/volumeManager.js +11 -8
  45. package/dist/src/NodeManager/node/task/TaskManager.js +25 -0
  46. package/dist/src/NodeManager/provider/ContainerStateManager.js +1 -1
  47. package/dist/src/NodeManager/provider/Provider.d.ts +34 -10
  48. package/dist/src/NodeManager/provider/Provider.js +217 -93
  49. package/dist/src/NodeManager/provider/orchestrationHooks.d.ts +8 -0
  50. package/dist/src/NodeManager/provider/orchestrationHooks.js +13 -0
  51. package/dist/src/NodeManager/provider/sshSidecar.d.ts +2 -2
  52. package/dist/src/NodeManager/provider/sshSidecar.js +2 -2
  53. package/dist/src/NodeManager/provider/types.d.ts +0 -9
  54. package/dist/src/NodeManager/provider/webTerminal.d.ts +1 -1
  55. package/dist/src/NodeManager/provider/webTerminal.js +1 -1
  56. package/dist/src/NodeManager/utils/utils.d.ts +1 -7
  57. package/dist/src/NodeManager/utils/utils.js +1 -7
  58. package/dist/src/cli/commands/prune/action.js +3 -2
  59. package/dist/src/cli/commands/prune/command.js +1 -1
  60. package/dist/src/cli/commands/run/action.js +3 -2
  61. package/dist/src/cli/commands/run/command.js +1 -1
  62. package/dist/src/cli/commands/start/command.js +1 -1
  63. package/npm-shrinkwrap.json +21 -2
  64. package/package.json +9 -6
  65. package/dist/src/NodeManager/provider/containerOrchestration/docker/utils/createServerObject.d.ts +0 -5
  66. package/dist/src/NodeManager/provider/containerOrchestration/docker/utils/createServerObject.js +0 -15
  67. package/dist/src/NodeManager/provider/containerOrchestration/docker/utils/deadline.d.ts +0 -1
  68. package/dist/src/NodeManager/provider/containerOrchestration/docker/utils/deadline.js +0 -15
  69. package/dist/src/NodeManager/provider/containerOrchestration/selectContainerOrchestration.d.ts +0 -3
  70. /package/dist/{src/NodeManager/provider/containerOrchestration → packages/container-orchestrator/src}/docker/utils/repoTagsContainsImage.d.ts +0 -0
  71. /package/dist/{src/NodeManager/provider/containerOrchestration → packages/container-orchestrator/src}/docker/utils/repoTagsContainsImage.js +0 -0
  72. /package/dist/{src/NodeManager/provider/containerOrchestration/interface.js → packages/container-orchestrator/src/hooks.js} +0 -0
  73. /package/dist/{src/NodeManager → packages/container-orchestrator/src}/utils/liveAbortSignal.d.ts +0 -0
  74. /package/dist/{src/NodeManager → packages/container-orchestrator/src}/utils/liveAbortSignal.js +0 -0
@@ -2,22 +2,27 @@ import { createHash, getExposePorts, isOpExposed, } from '@nosana/sdk';
2
2
  import { configs } from '../configs/configs.js';
3
3
  import { NodeConfigsSingleton } from '../configs/NodeConfigs.js';
4
4
  import { ExposedPortHealthCheck, } from './ExposedPortHealthCheck.js';
5
- import { s3HelperImage } from '../node/resource/definition/index.js';
6
- import { applyLoggingProxyToClass } from '../monitoring/proxy/loggingProxy.js';
5
+ import { applyLoggingProxyToClass, } from '../monitoring/proxy/loggingProxy.js';
7
6
  import { promiseTimeoutWrapper } from '../utils/timeoutPromiseWrapper.js';
8
- import { liveAbortSignal } from '../utils/liveAbortSignal.js';
7
+ import { liveAbortSignal } from '../../../packages/container-orchestrator/src/index.js';
9
8
  import { generateProxies, generateUrlSecretObject, } from '../utils/expose-util.js';
10
9
  import { ContainerStateManager } from './ContainerStateManager.js';
11
10
  import { sshSidecarFilters, withSshSidecar } from './sshSidecar.js';
12
- import { JOB_LABEL, OP_INDEX_LABEL, OP_LABEL, } from './containerOrchestration/labels.js';
11
+ import { GROUP_LABEL, JOB_LABEL, OP_INDEX_LABEL, OP_LABEL, SERVICE_LABEL, } from '../../../packages/container-orchestrator/src/index.js';
13
12
  import { createJobTerminalExec, } from './webTerminal.js';
14
- import { SshAccessError, SshAccessManager } from './SshAccessManager.js';
13
+ import { SshAccessManager } from './SshAccessManager.js';
14
+ import { TaskManagerRegistry } from '../node/task/TaskManagerRegistry.js';
15
+ import { getSDK } from '../sdk/index.js';
16
+ /** The port the job runtime sidecar listens on and its proxy publishes. */
17
+ const JOB_RUNTIME_PORT = '8080';
18
+ const JOB_RUNTIME_PREFIX = 'job-runtime-';
15
19
  export class Provider {
16
20
  constructor(containerOrchestration, repository, resourceManager) {
17
21
  this.containerOrchestration = containerOrchestration;
18
22
  this.repository = repository;
19
23
  this.resourceManager = resourceManager;
20
24
  this.frpcImage = configs().frp.containerImage;
25
+ this.jobRuntimeVolumesSwept = false;
21
26
  this.proxyStartupAbortController = undefined;
22
27
  this.sshAccess = new SshAccessManager(repository, (jobId) => this.getSshSidecars(jobId));
23
28
  applyLoggingProxyToClass(this);
@@ -134,50 +139,167 @@ export class Provider {
134
139
  ];
135
140
  return proxies;
136
141
  }
142
+ /**
143
+ * Starts the job's API sidecar (packages/job-runtime) and publishes it at
144
+ * https://<jobId>.<frp server>. The sidecar serves the job API's runtime
145
+ * routes from the container runtime alone; the node's own API is unchanged.
146
+ *
147
+ * Throws on failure; the task manager treats it as best effort and the job
148
+ * runs on without it. Only onchain jobs get one. The job handler registers
149
+ * those in the task manager registry, and a market benchmark or
150
+ * `nosana node run` never does.
151
+ */
152
+ async startJobRuntimeSidecar(flow) {
153
+ const { jobRuntime, frp, network } = configs();
154
+ if (!TaskManagerRegistry.getInstance().has(flow.id))
155
+ return;
156
+ const name = JOB_RUNTIME_PREFIX + flow.id;
157
+ if (!this.jobRuntimeVolumesSwept) {
158
+ await this.removeOrphanedJobRuntimeVolumes();
159
+ this.jobRuntimeVolumesSwept = true;
160
+ }
161
+ // A resumed flow may find the previous run's pair; the names must be
162
+ // free before they are created again. The volume is kept.
163
+ await this.removeJobRuntimeContainers(name);
164
+ // Never pulled here: the op loop waits on this call, and a registry round
165
+ // trip must not hold it. pullJobRuntimeImage fetches it before the job
166
+ // loop; a node whose pull failed skips the sidecar.
167
+ if (!(await this.containerOrchestration.hasImage(jobRuntime.containerImage)))
168
+ return;
169
+ this.resourceManager.images.setImage(jobRuntime.containerImage);
170
+ // The same socket path as the SSH sidecar (see withSshSidecar).
171
+ const socketDest = this.containerOrchestration.name === 'podman'
172
+ ? '/run/podman/podman.sock'
173
+ : '/var/run/docker.sock';
174
+ const sdk = getSDK();
175
+ const rpc = sdk.solana.connection?.rpcEndpoint;
176
+ await this.containerOrchestration.runFlowContainer(jobRuntime.containerImage, {
177
+ name,
178
+ // GPU stats need the device; a job without GPU ops (on a node that
179
+ // may have no GPU driver) gets a sidecar without one.
180
+ ...(jobUsesGpu(flow) ? { gpu: true } : {}),
181
+ pid_mode: 'host',
182
+ networks: { NOSANA_GATEWAY: {} },
183
+ aliases: [name],
184
+ restart_policy: 'on-failure',
185
+ env: {
186
+ JOB_ADDRESS: flow.id,
187
+ NOSANA_NETWORK: network,
188
+ ...(rpc ? { SOLANA_RPC: rpc } : {}),
189
+ NODE_ADDRESS: sdk.solana.wallet.publicKey.toString(),
190
+ DOCKER_SOCKET: socketDest,
191
+ JOB_RUNTIME_PORT,
192
+ },
193
+ volumes: [
194
+ {
195
+ type: 'bind',
196
+ name: await this.resolveHostSocketPath(),
197
+ dest: socketDest,
198
+ readonly: true,
199
+ },
200
+ { name, dest: '/nosana-job-runtime' },
201
+ ],
202
+ labels: {
203
+ [JOB_LABEL]: flow.id,
204
+ [SERVICE_LABEL]: 'job-runtime',
205
+ },
206
+ // The socket makes a compromised sidecar root on the host whatever
207
+ // runs inside it, so the container itself gets nothing it does not
208
+ // need: no capabilities (the API, nvidia-smi and /proc need none), no
209
+ // privilege escalation, and a pid budget that fits node plus its
210
+ // nvidia-smi spawns.
211
+ security: {
212
+ capDrop: ['ALL'],
213
+ noNewPrivileges: true,
214
+ pidsLimit: 256,
215
+ },
216
+ });
217
+ await this.containerOrchestration.pullImage(this.frpcImage);
218
+ this.resourceManager.images.setImage(this.frpcImage);
219
+ await this.containerOrchestration.runFlowContainer(this.frpcImage, {
220
+ ...this.generateFrpcContainerConfig(name, { NOSANA_GATEWAY: {} }, flow, [
221
+ {
222
+ name,
223
+ localIp: name,
224
+ localPorts: JOB_RUNTIME_PORT,
225
+ customDomain: `${flow.id}.${frp.serverAddr}`,
226
+ },
227
+ ], false, undefined),
228
+ restart_policy: 'on-failure',
229
+ labels: {
230
+ [JOB_LABEL]: flow.id,
231
+ [SERVICE_LABEL]: 'job-runtime-frpc',
232
+ },
233
+ });
234
+ }
235
+ /**
236
+ * Pulls the job runtime image, so the next job's sidecar finds it local.
237
+ * Runs before each job loop; a cached image costs no registry round trip.
238
+ */
239
+ async pullJobRuntimeImage() {
240
+ await this.containerOrchestration.pullImage(configs().jobRuntime.containerImage);
241
+ }
242
+ /**
243
+ * Removes the job's API sidecar, its proxy and its volume. Throws on
244
+ * failure; a container that will not go stays tracked for the next resync.
245
+ */
246
+ async stopJobRuntimeSidecar(flowId) {
247
+ if (!TaskManagerRegistry.getInstance().has(flowId))
248
+ return;
249
+ const name = JOB_RUNTIME_PREFIX + flowId;
250
+ await this.removeJobRuntimeContainers(name);
251
+ await this.containerOrchestration.deleteVolume(name);
252
+ }
253
+ /** Removes the sidecar and its proxy by name; one already gone is fine. */
254
+ async removeJobRuntimeContainers(name) {
255
+ await Promise.all([`frpc-${name}`, name].map((container) => this.containerOrchestration.stopAndDeleteContainer(container)));
256
+ }
257
+ /**
258
+ * Removes job-runtime-<jobId> volumes left by a job whose stop never ran (the
259
+ * node died mid-job). The volume resync does not know them, and a reaped
260
+ * sidecar container leaves its named volume behind. Runs once, on the first
261
+ * sidecar start after boot. A job still registered keeps its volume, and a
262
+ * volume still in use fails to delete and is kept for the next boot.
263
+ */
264
+ async removeOrphanedJobRuntimeVolumes() {
265
+ const registry = TaskManagerRegistry.getInstance();
266
+ for (const { Name } of await this.containerOrchestration.listVolumes()) {
267
+ if (!Name?.startsWith(JOB_RUNTIME_PREFIX))
268
+ continue;
269
+ if (registry.has(Name.slice(JOB_RUNTIME_PREFIX.length)))
270
+ continue;
271
+ try {
272
+ await this.containerOrchestration.deleteVolume(Name);
273
+ }
274
+ catch {
275
+ // In use or already gone.
276
+ }
277
+ }
278
+ }
137
279
  /**
138
280
  * Opens an interactive PTY exec session into a running job container for the
139
281
  * wallet-authorized web terminal. The authorization check is performed by the
140
- * caller (the terminal websocket route); this only enforces that the feature
141
- * is enabled and that we can reach the runtime over a socket.
282
+ * caller (the terminal websocket route).
142
283
  */
143
284
  async createJobTerminalSession(jobId, options = {}) {
144
- if (!this.sshEnabled) {
145
- throw new Error('Web terminal is not enabled');
146
- }
147
285
  return createJobTerminalExec(this.containerOrchestration, jobId, options);
148
286
  }
149
287
  async authorizeSshKeyForJob(jobId, publicKey, expiresAt) {
150
- this.assertSshEnabled();
151
288
  await this.sshAccess.authorize(jobId, publicKey, expiresAt);
152
289
  }
153
290
  async listSshKeysForJob(jobId) {
154
- this.assertSshEnabled();
155
291
  return this.sshAccess.list(jobId);
156
292
  }
157
293
  async revokeSshKeyForJob(jobId, publicKey) {
158
- this.assertSshEnabled();
159
294
  return this.sshAccess.revoke(jobId, publicKey);
160
295
  }
161
- /**
162
- * SSH requires the runtime to be reached over a socket the sidecar can mount
163
- * to exec into the workload. Every SSH entry point, including the web
164
- * terminal, checks this and nothing else.
165
- */
166
- get sshEnabled() {
167
- return this.containerOrchestration.getProtocol() === 'socket';
168
- }
169
- assertSshEnabled() {
170
- if (!this.sshEnabled) {
171
- throw new SshAccessError('SSH access is not enabled on this node', 503);
172
- }
173
- }
174
296
  /** The SSH sidecars of a job, one per operation, created ones included. */
175
297
  getSshSidecars(jobId) {
176
298
  return this.containerOrchestration.getContainersByLabels(sshSidecarFilters(jobId), { all: true });
177
299
  }
178
300
  /** The runtime socket as a host path, which is what a bind source must be. */
179
301
  resolveHostSocketPath() {
180
- return (this.hostSocketPath ??= this.containerOrchestration.resolveHostPath(this.containerOrchestration.host));
302
+ return (this.hostSocketPath ??= this.containerOrchestration.resolveHostPath(this.containerOrchestration.socketPath));
181
303
  }
182
304
  async taskManagerContainerRunOperation(flow, op, controller, emitter) {
183
305
  let stateManager;
@@ -233,77 +355,73 @@ export class Provider {
233
355
  networks[name] = {};
234
356
  await this.containerOrchestration.createNetwork(name, controller);
235
357
  const exposed = isOpExposed(op);
236
- const sshEnabled = this.sshEnabled;
237
- if (exposed || sshEnabled) {
238
- await this.containerOrchestration.pullImage(this.frpcImage, undefined, controller);
239
- this.resourceManager.images.setImage(this.frpcImage);
240
- const proxies = [];
241
- let isLoadBalanced = false;
242
- let deploymentHash;
243
- if (exposed) {
244
- /**
245
- * because of the introduction of deployment_id(load balancing key) instead of using flow.id we use deployment_id for more
246
- * deterministic url generation
247
- */
248
- const config = NodeConfigsSingleton.getInstance();
249
- isLoadBalanced = !!flow.jobDefinition.deployment_id;
250
- deploymentHash = isLoadBalanced
251
- ? // @ts-ignore
252
- config.options.isNodeRun
253
- ? flow.jobDefinition.deployment_id
254
- : createHash(`${flow.jobDefinition.deployment_id}:${flow.project}`, 45)
255
- : undefined;
256
- const generated = generateProxies(flow.id, op, index, ports, name, op.id, deploymentHash);
257
- idMaps = generated.idMap;
258
- proxies.push(...generated.proxies);
259
- }
260
- let sidecarConfig = this.generateFrpcContainerConfig(name, networks, flow, proxies, isLoadBalanced, deploymentHash);
261
- if (sshEnabled) {
262
- // Keys are job wide and may have been authorized before this
263
- // operation started; the seed runs between create and start.
264
- sidecarConfig = withSshSidecar(sidecarConfig, {
265
- jobId: flow.id,
266
- opIndex: index,
267
- runtime: {
268
- name: this.containerOrchestration.name,
269
- host: await this.resolveHostSocketPath(),
270
- },
271
- prepare: (sidecar) => this.sshAccess.prepare(flow.id, sidecar),
358
+ await this.containerOrchestration.pullImage(this.frpcImage, undefined, controller);
359
+ this.resourceManager.images.setImage(this.frpcImage);
360
+ const proxies = [];
361
+ let isLoadBalanced = false;
362
+ let deploymentHash;
363
+ if (exposed) {
364
+ /**
365
+ * because of the introduction of deployment_id(load balancing key) instead of using flow.id we use deployment_id for more
366
+ * deterministic url generation
367
+ */
368
+ const config = NodeConfigsSingleton.getInstance();
369
+ isLoadBalanced = !!flow.jobDefinition.deployment_id;
370
+ deploymentHash = isLoadBalanced
371
+ ? // @ts-ignore
372
+ config.options.isNodeRun
373
+ ? flow.jobDefinition.deployment_id
374
+ : createHash(`${flow.jobDefinition.deployment_id}:${flow.project}`, 45)
375
+ : undefined;
376
+ const generated = generateProxies(flow.id, op, index, ports, name, op.id, deploymentHash);
377
+ idMaps = generated.idMap;
378
+ proxies.push(...generated.proxies);
379
+ }
380
+ let sidecarConfig = this.generateFrpcContainerConfig(name, networks, flow, proxies, isLoadBalanced, deploymentHash);
381
+ // Keys are job wide and may have been authorized before this
382
+ // operation started; the seed runs between create and start.
383
+ sidecarConfig = withSshSidecar(sidecarConfig, {
384
+ jobId: flow.id,
385
+ opIndex: index,
386
+ runtime: {
387
+ name: this.containerOrchestration.name,
388
+ socketPath: await this.resolveHostSocketPath(),
389
+ },
390
+ prepare: (sidecar) => this.sshAccess.prepare(flow.id, sidecar),
391
+ });
392
+ frpcContainer = await this.containerOrchestration.runFlowContainer(this.frpcImage, sidecarConfig);
393
+ if (exposed) {
394
+ if (op.args.private) {
395
+ this.repository.updateflowStateSecret(flow.id, {
396
+ [flow.id]: generateUrlSecretObject(idMaps, op.id),
397
+ urlmode: 'private',
398
+ });
399
+ emitter.emit('flow:secrets-updated', {
400
+ flowId: flow.id,
401
+ opId: op.id,
272
402
  });
273
403
  }
274
- frpcContainer = await this.containerOrchestration.runFlowContainer(this.frpcImage, sidecarConfig);
275
- if (exposed) {
276
- if (op.args.private) {
277
- this.repository.updateflowStateSecret(flow.id, {
278
- [flow.id]: generateUrlSecretObject(idMaps, op.id),
279
- urlmode: 'private',
280
- });
281
- emitter.emit('flow:secrets-updated', {
282
- flowId: flow.id,
283
- opId: op.id,
284
- });
285
- }
286
- else {
287
- const newSecret = generateUrlSecretObject(idMaps, op.id);
288
- const currentSecrets = this.repository.getFlowSecret(flow.id, flow.id) || {};
289
- const mergedSecrets = {
290
- ...currentSecrets,
291
- ...newSecret,
292
- };
293
- this.repository.updateflowStateSecret(flow.id, {
294
- [flow.id]: mergedSecrets,
295
- urlmode: 'public',
296
- });
297
- emitter.emit('flow:secrets-updated', {
298
- flowId: flow.id,
299
- opId: op.id,
300
- });
301
- }
404
+ else {
405
+ const newSecret = generateUrlSecretObject(idMaps, op.id);
406
+ const currentSecrets = this.repository.getFlowSecret(flow.id, flow.id) || {};
407
+ const mergedSecrets = {
408
+ ...currentSecrets,
409
+ ...newSecret,
410
+ };
411
+ this.repository.updateflowStateSecret(flow.id, {
412
+ [flow.id]: mergedSecrets,
413
+ urlmode: 'public',
414
+ });
415
+ emitter.emit('flow:secrets-updated', {
416
+ flowId: flow.id,
417
+ opId: op.id,
418
+ });
302
419
  }
303
420
  }
304
421
  if (op.args.resources) {
305
- await this.containerOrchestration.pullImage(s3HelperImage, undefined, controller);
306
- this.resourceManager.images.setImage(s3HelperImage);
422
+ const resourceManagerImage = configs().resourceManager.containerImage;
423
+ await this.containerOrchestration.pullImage(resourceManagerImage, undefined, controller);
424
+ this.resourceManager.images.setImage(resourceManagerImage);
307
425
  // transformCollections has already resolved any SpreadMarker objects by this point
308
426
  const resourceVolumes = await this.resourceManager.getResourceVolumes(op.args.resources ?? [], controller, { id: flow.id, opIndex: index }, flow.project);
309
427
  volumes.push(...resourceVolumes);
@@ -329,6 +447,7 @@ export class Provider {
329
447
  labels: {
330
448
  [JOB_LABEL]: flow.id,
331
449
  [OP_LABEL]: op.id,
450
+ [GROUP_LABEL]: op.execution?.group ?? op.id,
332
451
  [OP_INDEX_LABEL]: index.toString(),
333
452
  },
334
453
  };
@@ -573,6 +692,11 @@ function getEntrypoint(arg, flow) {
573
692
  ? arg.entrypoint
574
693
  : flow.jobDefinition.global.entrypoint;
575
694
  }
695
+ /** Whether any of the job's container ops runs with a GPU. */
696
+ function jobUsesGpu(flow) {
697
+ return (flow.jobDefinition.ops ?? []).some((op) => op.type === 'container/run' &&
698
+ !!getGpu(op.args, flow));
699
+ }
576
700
  function getGpu(arg, flow) {
577
701
  return (arg.gpu || (flow.jobDefinition.global && flow.jobDefinition.global.gpu));
578
702
  }
@@ -0,0 +1,8 @@
1
+ import type { OrchestrationHooks } from '../../../packages/container-orchestrator/src/index.js';
2
+ import type { NodeRepository } from '../repository/NodeRepository.js';
3
+ /**
4
+ * What the node hands the shared orchestrators: the containers it starts are
5
+ * tracked in the node database for the resync to reap, and image pulls draw
6
+ * the terminal progress bars.
7
+ */
8
+ export declare function nodeOrchestrationHooks(repository: NodeRepository): OrchestrationHooks;
@@ -0,0 +1,13 @@
1
+ import { createLoggingProxy } from '../monitoring/proxy/loggingProxy.js';
2
+ import { MultiProgressBarReporter } from '../node/utils/multiProgressBarReporter.js';
3
+ /**
4
+ * What the node hands the shared orchestrators: the containers it starts are
5
+ * tracked in the node database for the resync to reap, and image pulls draw
6
+ * the terminal progress bars.
7
+ */
8
+ export function nodeOrchestrationHooks(repository) {
9
+ return {
10
+ tracker: repository,
11
+ pullProgress: () => createLoggingProxy(new MultiProgressBarReporter()),
12
+ };
13
+ }
@@ -3,7 +3,7 @@
3
3
  import type { JobDefinition } from '@nosana/sdk';
4
4
  import type { Container } from 'dockerode';
5
5
  import type { SshKeyGrant } from '../db/index.js';
6
- import type { RunContainerArgs } from './containerOrchestration/interface.js';
6
+ import type { RunContainerArgs } from '../../../packages/container-orchestrator/src/index.js';
7
7
  /** Marks an operation's frpc sidecar that also runs sshgw. */
8
8
  export declare const SSH_SIDECAR_SERVICE = "ssh-sidecar";
9
9
  /** The only user name sshgw accepts; the hostname identifies the job. Shared with the client through @nosana/ssh. */
@@ -26,7 +26,7 @@ export declare function withSshSidecar(config: RunContainerArgs, sidecar: {
26
26
  /** Runtime name and the socket path as the daemon sees it on the host. */
27
27
  runtime: {
28
28
  name: string;
29
- host: string;
29
+ socketPath: string;
30
30
  };
31
31
  prepare: (container: Container) => Promise<void>;
32
32
  }): RunContainerArgs;
@@ -1,6 +1,6 @@
1
1
  import { Readable } from 'stream';
2
2
  import { generateKeyPairSync } from 'crypto';
3
- import { JOB_LABEL, OP_INDEX_LABEL, SERVICE_LABEL, } from './containerOrchestration/labels.js';
3
+ import { JOB_LABEL, OP_INDEX_LABEL, SERVICE_LABEL, } from '../../../packages/container-orchestrator/src/index.js';
4
4
  import { getSshKeyIdentity, isValidSshPublicKey, MAX_SSH_PUBLIC_KEYS, MAX_SSH_PUBLIC_KEYS_BYTES, SSH_USERNAME, } from '@nosana/ssh';
5
5
  /** Marks an operation's frpc sidecar that also runs sshgw. */
6
6
  export const SSH_SIDECAR_SERVICE = 'ssh-sidecar';
@@ -41,7 +41,7 @@ export function withSshSidecar(config, sidecar) {
41
41
  ...(config.volumes ?? []),
42
42
  {
43
43
  type: 'bind',
44
- name: sidecar.runtime.host,
44
+ name: sidecar.runtime.socketPath,
45
45
  dest: socketDest,
46
46
  readonly: true,
47
47
  },
@@ -1,15 +1,6 @@
1
1
  import { type Resource } from '@nosana/sdk';
2
2
  export type RequiredResource = Omit<Resource, 'target'>;
3
3
  export type StdOptions = 'stdin' | 'stdout' | 'stderr' | 'nodeerr';
4
- export type ReturnedStatus<T = undefined> = {
5
- status: true;
6
- result?: T;
7
- error?: never;
8
- } | {
9
- status: false;
10
- error: Error | unknown;
11
- result?: never;
12
- };
13
4
  export type HealthcheckPayload = {
14
5
  id: string;
15
6
  flowId: string;
@@ -1,6 +1,6 @@
1
1
  /// <reference types="node" />
2
2
  import type { Duplex } from 'stream';
3
- import type { ContainerOrchestrationInterface } from './containerOrchestration/interface.js';
3
+ import type { ContainerOrchestrationInterface } from '../../../packages/container-orchestrator/src/index.js';
4
4
  export type TerminalSession = {
5
5
  stream: Duplex;
6
6
  resize: (size: {
@@ -1,4 +1,4 @@
1
- import { JOB_LABEL, OP_LABEL, isSafeIdentifier, } from './containerOrchestration/labels.js';
1
+ import { JOB_LABEL, OP_LABEL, isSafeIdentifier, } from '../../../packages/container-orchestrator/src/index.js';
2
2
  const DEFAULT_TERMINAL_COLS = 80;
3
3
  const DEFAULT_TERMINAL_ROWS = 24;
4
4
  const MAX_TERMINAL_DIMENSION = 1000;
@@ -18,16 +18,10 @@ declare const colors: {
18
18
  WHITE: string;
19
19
  RESET: string;
20
20
  };
21
- /**
22
- * Method to test and cast strings into string arrays
23
- * @param {string | string[]} value The string or string array being tested and casted
24
- * @returns { string[] } Returns a string array
25
- */
26
- declare const ifStringCastToArray: (value: string | string[]) => string[];
27
21
  declare function logTable(data: any): void;
28
22
  declare function isNodeOnboarded(status: string): boolean;
29
23
  declare const SECONDS_PER_DAY: number;
30
- export { logTable, now, sleep, clearLine, askYesNoQuestion, colors, ifStringCastToArray, SECONDS_PER_DAY, isNodeOnboarded, };
24
+ export { logTable, now, sleep, clearLine, askYesNoQuestion, colors, SECONDS_PER_DAY, isNodeOnboarded, };
31
25
  /**
32
26
  * Loads the configuration value for a given environment variable key.
33
27
  * Throws an error at runtime if the environment variable is not set.
@@ -53,12 +53,6 @@ const colors = {
53
53
  WHITE: '\u001b[1;38;5;231m',
54
54
  RESET: '\u001b[0m',
55
55
  };
56
- /**
57
- * Method to test and cast strings into string arrays
58
- * @param {string | string[]} value The string or string array being tested and casted
59
- * @returns { string[] } Returns a string array
60
- */
61
- const ifStringCastToArray = (value) => typeof value === 'string' ? [value] : value;
62
56
  function logTable(data) {
63
57
  if (data && data.length > 0) {
64
58
  const ts = new Transform({
@@ -89,7 +83,7 @@ function isNodeOnboarded(status) {
89
83
  return ['ONBOARDED', 'PREMIUM'].includes(status);
90
84
  }
91
85
  const SECONDS_PER_DAY = 24 * 60 * 60;
92
- export { logTable, now, sleep, clearLine, askYesNoQuestion, colors, ifStringCastToArray, SECONDS_PER_DAY, isNodeOnboarded, };
86
+ export { logTable, now, sleep, clearLine, askYesNoQuestion, colors, SECONDS_PER_DAY, isNodeOnboarded, };
93
87
  /**
94
88
  * Loads the configuration value for a given environment variable key.
95
89
  * Throws an error at runtime if the environment variable is not set.
@@ -1,6 +1,7 @@
1
1
  import { DB } from '../../../NodeManager/db/index.js';
2
2
  import { ResourceManager } from '../../../NodeManager/node/resource/resourceManager.js';
3
- import { selectContainerOrchestrationProvider } from '../../../NodeManager/provider/containerOrchestration/selectContainerOrchestration.js';
3
+ import { selectContainerOrchestrationProvider } from '../../../../packages/container-orchestrator/src/index.js';
4
+ import { nodeOrchestrationHooks } from '../../../NodeManager/provider/orchestrationHooks.js';
4
5
  import { NodeRepository } from '../../../NodeManager/repository/NodeRepository.js';
5
6
  export * from './action.js';
6
7
  export async function pruneResources(options) {
@@ -8,7 +9,7 @@ export async function pruneResources(options) {
8
9
  try {
9
10
  const db = new DB(options.config).db;
10
11
  const repository = new NodeRepository(db);
11
- const containerOrchestration = selectContainerOrchestrationProvider(options.provider, options.podman, options.gpu, undefined, repository);
12
+ const containerOrchestration = selectContainerOrchestrationProvider(options.provider, options.podman, options.gpu, undefined, nodeOrchestrationHooks(repository));
12
13
  const resourceManager = new ResourceManager(containerOrchestration, repository);
13
14
  await resourceManager.prune();
14
15
  console.log('Finished pruning system');
@@ -2,6 +2,6 @@ import { Command, Option } from 'commander';
2
2
  import { pruneResources } from './action.js';
3
3
  export const nodePruneCommand = new Command('prune')
4
4
  .description('Safely prune none required images and resources.')
5
- .addOption(new Option('--docker, --podman <URI>', 'Podman/Docker connection URI').default('~/.nosana/podman/podman.sock'))
5
+ .addOption(new Option('--docker, --podman <path>', 'Podman/Docker socket path').default('~/.nosana/podman/podman.sock'))
6
6
  .addOption(new Option('-c, --config <path>', 'Config path (to store the flows database and other config)').default('~/.nosana/'))
7
7
  .action(pruneResources);
@@ -4,7 +4,8 @@ import { Provider } from '../../../NodeManager/provider/Provider.js';
4
4
  import { ResourceManager } from '../../../NodeManager/node/resource/resourceManager.js';
5
5
  import { NodeRepository } from '../../../NodeManager/repository/NodeRepository.js';
6
6
  import { DB } from '../../../NodeManager/db/index.js';
7
- import { selectContainerOrchestrationProvider } from '../../../NodeManager/provider/containerOrchestration/selectContainerOrchestration.js';
7
+ import { selectContainerOrchestrationProvider } from '../../../../packages/container-orchestrator/src/index.js';
8
+ import { nodeOrchestrationHooks } from '../../../NodeManager/provider/orchestrationHooks.js';
8
9
  import { log } from '../../../NodeManager/monitoring/log/NodeLog.js';
9
10
  import { ConsoleLogger } from '../../../NodeManager/monitoring/log/console/ConsoleLogger.js';
10
11
  import TaskManager, { StopReasons, } from '../../../NodeManager/node/task/TaskManager.js';
@@ -29,7 +30,7 @@ export async function runJob(jobDefinitionFile, options) {
29
30
  log();
30
31
  const db = new DB(options.config).db;
31
32
  const repository = createLoggingProxy(new NodeRepository(db));
32
- const containerOrchestration = selectContainerOrchestrationProvider(options.provider, options.podman, options.gpu, options.trustedExecutionRuntime, repository);
33
+ const containerOrchestration = selectContainerOrchestrationProvider(options.provider, options.podman, options.gpu, options.trustedExecutionRuntime, nodeOrchestrationHooks(repository));
33
34
  const resourceManager = new ResourceManager(containerOrchestration, repository);
34
35
  const provider = new Provider(containerOrchestration, repository, resourceManager);
35
36
  const logger = new ConsoleLogger(false);
@@ -7,7 +7,7 @@ export const runNodeCommand = new Command('run')
7
7
  .addArgument(new Argument('<job-definition-file>', 'Job Definition File').argOptional())
8
8
  .addOption(new Option('--url <url>', 'Url path for the JSON flow'))
9
9
  .addOption(new Option('-c, --config <path>', 'Config path (to store the flows database and other config)').default('~/.nosana/'))
10
- .addOption(new Option('--docker, --podman <URI>', 'Podman/Docker connection URI').default('~/.nosana/podman/podman.sock'))
10
+ .addOption(new Option('--docker, --podman <path>', 'Podman/Docker socket path').default('~/.nosana/podman/podman.sock'))
11
11
  .addOption(walletOption)
12
12
  .addOption(gpuOption)
13
13
  .addOption(verboseOption)
@@ -8,7 +8,7 @@ export const startNodeCommand = new Command('start')
8
8
  .addOption(networkOption)
9
9
  .addOption(rpcOption)
10
10
  .addOption(walletOption)
11
- .addOption(new Option('--docker, --podman <URI>', 'Podman/Docker connection URI').default('~/.nosana/podman/podman.sock'))
11
+ .addOption(new Option('--docker, --podman <path>', 'Podman/Docker socket path').default('~/.nosana/podman/podman.sock'))
12
12
  .addOption(new Option('-c, --config <path>', 'Config path (to store the flows database and other config)').default('~/.nosana/'))
13
13
  .addOption(teeRuntimeOption)
14
14
  .addOption(gpuOption)
@@ -1,13 +1,16 @@
1
1
  {
2
2
  "name": "@nosana/node",
3
- "version": "1.1.71",
3
+ "version": "1.1.72-rc",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@nosana/node",
9
- "version": "1.1.71",
9
+ "version": "1.1.72-rc",
10
10
  "license": "ISC",
11
+ "workspaces": [
12
+ "packages/*"
13
+ ],
11
14
  "dependencies": {
12
15
  "@coral-xyz/anchor": "^0.28.1-beta.1",
13
16
  "@inquirer/prompts": "^4.3.0",
@@ -1582,6 +1585,10 @@
1582
1585
  "node": ">= 8"
1583
1586
  }
1584
1587
  },
1588
+ "node_modules/@nosana/container-orchestrator": {
1589
+ "resolved": "packages/container-orchestrator",
1590
+ "link": true
1591
+ },
1585
1592
  "node_modules/@nosana/ipfs": {
1586
1593
  "version": "2.2.6",
1587
1594
  "resolved": "https://registry.npmjs.org/@nosana/ipfs/-/ipfs-2.2.6.tgz",
@@ -1609,6 +1616,10 @@
1609
1616
  "base-x": "^4.0.0"
1610
1617
  }
1611
1618
  },
1619
+ "node_modules/@nosana/job-runtime": {
1620
+ "resolved": "packages/job-runtime",
1621
+ "link": true
1622
+ },
1612
1623
  "node_modules/@nosana/sdk": {
1613
1624
  "version": "0.4.87",
1614
1625
  "resolved": "https://registry.npmjs.org/@nosana/sdk/-/sdk-0.4.87.tgz",
@@ -10214,6 +10225,14 @@
10214
10225
  "funding": {
10215
10226
  "url": "https://github.com/sponsors/sindresorhus"
10216
10227
  }
10228
+ },
10229
+ "packages/container-orchestrator": {
10230
+ "name": "@nosana/container-orchestrator",
10231
+ "version": "0.0.0"
10232
+ },
10233
+ "packages/job-runtime": {
10234
+ "name": "@nosana/job-runtime",
10235
+ "version": "0.0.0"
10217
10236
  }
10218
10237
  }
10219
10238
  }