@nosana/node 1.1.61-rc → 1.1.63-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 (31) hide show
  1. package/README.md +1 -0
  2. package/dist/.env +1 -0
  3. package/dist/package.json +1 -1
  4. package/dist/src/NodeManager/configs/configs.d.ts +3 -0
  5. package/dist/src/NodeManager/configs/configs.js +3 -0
  6. package/dist/src/NodeManager/index.js +1 -1
  7. package/dist/src/NodeManager/monitoring/reportPodmanDiagnostics.d.ts +3 -2
  8. package/dist/src/NodeManager/monitoring/reportPodmanDiagnostics.js +12 -8
  9. package/dist/src/NodeManager/node/Node.d.ts +3 -0
  10. package/dist/src/NodeManager/node/Node.js +32 -0
  11. package/dist/src/NodeManager/node/task/TaskManagerRegistry.d.ts +8 -0
  12. package/dist/src/NodeManager/node/task/TaskManagerRegistry.js +22 -0
  13. package/dist/src/NodeManager/provider/Provider.d.ts +14 -1
  14. package/dist/src/NodeManager/provider/Provider.js +55 -14
  15. package/dist/src/NodeManager/provider/gpuRuntime.d.ts +13 -0
  16. package/dist/src/NodeManager/provider/gpuRuntime.js +27 -0
  17. package/dist/src/NodeManager/provider/podmanManager/PodmanManager.d.ts +72 -0
  18. package/dist/src/NodeManager/provider/podmanManager/PodmanManager.js +238 -0
  19. package/dist/src/NodeManager/utils/expose-util.js +3 -1
  20. package/dist/src/cli/commands/prune/action.js +2 -1
  21. package/dist/src/cli/commands/run/action.js +14 -1
  22. package/dist/src/cli/commands/run/command.js +2 -1
  23. package/dist/src/cli/commands/run/generateDeploymentEndpointsTable.js +6 -3
  24. package/dist/src/cli/commands/sharedOptions/--managed-podman.d.ts +16 -0
  25. package/dist/src/cli/commands/sharedOptions/--managed-podman.js +25 -0
  26. package/dist/src/cli/commands/sharedOptions/index.d.ts +1 -0
  27. package/dist/src/cli/commands/sharedOptions/index.js +1 -0
  28. package/dist/src/cli/commands/start/action.js +2 -1
  29. package/dist/src/cli/commands/start/command.js +2 -1
  30. package/npm-shrinkwrap.json +2 -2
  31. package/package.json +1 -1
package/README.md CHANGED
@@ -80,6 +80,7 @@ Options:
80
80
  --provider <provider> provider used to run the job (choices: "docker", "podman", default: "podman")
81
81
  -w, --wallet <wallet> path to wallet private key (default: "~/.nosana/nosana_key.json")
82
82
  --docker, --podman <URI> Podman/Docker connection URI (default: "~/.nosana/podman/podman.sock")
83
+ --managed-podman Run jobs in a podman container the node creates over the docker socket given with --docker, and recreates when its GPU mounts go stale
83
84
  -h, --help display help for command
84
85
  ```
85
86
 
package/dist/.env CHANGED
@@ -1,6 +1,7 @@
1
1
  BACKEND_SOLANA_ADDRESS=7rFPFnxjXHC2sfDy3qrDa9pEb4j49oivMuV7e8sYDPmB
2
2
  FRPC_CONTAINER_IMAGE=registry.hub.docker.com/nosana/frpc:v1.0.44
3
3
  FRP_SERVER_IMAGE=registry.hub.docker.com/nosana/frpc:v1.0.44
4
+ PODMAN_CONTAINER_IMAGE=docker.io/nosana/podman:v1.1.2
4
5
  FRP_SERVER_PORT=7000
5
6
  MIN_DISK_SPACE=25
6
7
  SIGN_MESSAGE="Hello Nosana Node!"
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nosana/node",
3
- "version": "1.1.61-rc",
3
+ "version": "1.1.63-rc",
4
4
  "description": "",
5
5
  "bin": {
6
6
  "nosana-node": "./dist/src/index.js"
@@ -9,6 +9,9 @@ export type configType = {
9
9
  containerImage: string;
10
10
  nodeInternalHostName: string;
11
11
  };
12
+ podman: {
13
+ containerImage: string;
14
+ };
12
15
  api: {
13
16
  port: number;
14
17
  };
@@ -13,6 +13,9 @@ export const configs = (options) => {
13
13
  containerImage: loadConfigurationValue('FRP_SERVER_IMAGE'),
14
14
  nodeInternalHostName: loadConfigurationValue('FRP_NODE_INTERNAL_HOSTNAME', "nosana-node"),
15
15
  },
16
+ podman: {
17
+ containerImage: loadConfigurationValue('PODMAN_CONTAINER_IMAGE'),
18
+ },
16
19
  api: {
17
20
  port: parseInt(loadConfigurationValue('API_PORT')),
18
21
  },
@@ -64,7 +64,7 @@ export default class NodeManager {
64
64
  * collect the diagnostics the podman container leaves in the shared
65
65
  * directory, which has no other route off the node
66
66
  */
67
- reportPodmanDiagnostics(this.configLocation);
67
+ reportPodmanDiagnostics(this.configLocation, (event) => this.node.podmanEvent(event));
68
68
  /**
69
69
  * start
70
70
  *
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Forward the events the podman container leaves in the directory the two
3
3
  * share. That container has no route to the host manager of its own, so its
4
- * event log is the only sign of whether the CDI watcher ever fired.
4
+ * event log is the only sign of whether the CDI watcher ever fired. Each
5
+ * event also goes to `onEvent`, for the node to act on.
5
6
  *
6
7
  * Nothing here may throw: this is diagnostics, and the node's uncaught handler
7
8
  * would turn a failure to read a log file into the node stopping. Watching can
@@ -9,4 +10,4 @@
9
10
  * and a watch already established still reports errors of its own if the
10
11
  * directory it covers is removed.
11
12
  */
12
- export declare function reportPodmanDiagnostics(configLocation: string): void;
13
+ export declare function reportPodmanDiagnostics(configLocation: string, onEvent?: (event: string) => void): void;
@@ -10,7 +10,8 @@ const MAX_CATCH_UP = 1024 * 1024;
10
10
  /**
11
11
  * Forward the events the podman container leaves in the directory the two
12
12
  * share. That container has no route to the host manager of its own, so its
13
- * event log is the only sign of whether the CDI watcher ever fired.
13
+ * event log is the only sign of whether the CDI watcher ever fired. Each
14
+ * event also goes to `onEvent`, for the node to act on.
14
15
  *
15
16
  * Nothing here may throw: this is diagnostics, and the node's uncaught handler
16
17
  * would turn a failure to read a log file into the node stopping. Watching can
@@ -18,7 +19,7 @@ const MAX_CATCH_UP = 1024 * 1024;
18
19
  * and a watch already established still reports errors of its own if the
19
20
  * directory it covers is removed.
20
21
  */
21
- export function reportPodmanDiagnostics(configLocation) {
22
+ export function reportPodmanDiagnostics(configLocation, onEvent) {
22
23
  if (!configLocation)
23
24
  return;
24
25
  const log = path.join(configLocation.replace(/^~/, os.homedir()), CDI_EVENTS);
@@ -75,12 +76,15 @@ export function reportPodmanDiagnostics(configLocation) {
75
76
  partial = lines.pop() ?? '';
76
77
  lines
77
78
  .filter(Boolean)
78
- .forEach((event) => void reportError({
79
- error_type: 'cdiEvent',
80
- error_name: 'Event',
81
- error_message: event,
82
- error_stack: '',
83
- }));
79
+ .forEach((event) => {
80
+ void reportError({
81
+ error_type: 'cdiEvent',
82
+ error_name: 'Event',
83
+ error_message: event,
84
+ error_stack: '',
85
+ });
86
+ onEvent?.(event);
87
+ });
84
88
  }
85
89
  finally {
86
90
  fs.closeSync(fd);
@@ -15,6 +15,7 @@ export declare class BasicNode {
15
15
  private resourceManager;
16
16
  private provider;
17
17
  private containerOrchestration;
18
+ private podmanManager?;
18
19
  private exiting;
19
20
  private sdk;
20
21
  constructor(options: {
@@ -27,6 +28,8 @@ export declare class BasicNode {
27
28
  isApiActive(): Promise<boolean>;
28
29
  node(): string;
29
30
  getSystemEnvironment(): string;
31
+ /** An event line the podman container's CDI watcher logged. */
32
+ podmanEvent(event: string): void;
30
33
  stop(): Promise<void>;
31
34
  clean(): Promise<void>;
32
35
  start(): Promise<void>;
@@ -14,6 +14,7 @@ import { ExpiryHandler } from './expiry/expiryHandler.js';
14
14
  import { GridHandler } from './grid/gridHandler.js';
15
15
  import { ResourceManager } from './resource/resourceManager.js';
16
16
  import { selectContainerOrchestrationProvider } from '../provider/containerOrchestration/selectContainerOrchestration.js';
17
+ import { DockerContainerOrchestration } from '../provider/containerOrchestration/docker/index.js';
17
18
  import { RegisterHandler } from './register/index.js';
18
19
  import { BalanceHandler } from './balance/balanceHandler.js';
19
20
  import { abortControllerSelector, nodeAbortControllerSelector, } from './abort/abortControllerSelector.js';
@@ -21,6 +22,7 @@ import { pollForRun } from './utils/poll.js';
21
22
  import { configs } from '../configs/configs.js';
22
23
  import { TaskManagerRegistry } from './task/TaskManagerRegistry.js';
23
24
  import { StopReasons } from './task/TaskManager.js';
25
+ import { isStaleCdiEvent, PodmanManager, } from '../provider/podmanManager/PodmanManager.js';
24
26
  export class BasicNode {
25
27
  constructor(options) {
26
28
  this.exiting = false;
@@ -28,8 +30,17 @@ export class BasicNode {
28
30
  const db = new DB(options.config).db;
29
31
  this.repository = new NodeRepository(db);
30
32
  this.containerOrchestration = selectContainerOrchestrationProvider(options.provider, options.podman, options.gpu, options.trustedExecutionRuntime);
33
+ if (options.managedPodman) {
34
+ this.podmanManager = new PodmanManager(new DockerContainerOrchestration(options.dockerSocket, options.gpu), options.config, this.containerOrchestration);
35
+ }
31
36
  this.resourceManager = new ResourceManager(this.containerOrchestration, this.repository);
32
37
  this.provider = new Provider(this.containerOrchestration, this.repository, this.resourceManager);
38
+ if (this.podmanManager) {
39
+ this.provider.gpuRuntime = this.podmanManager;
40
+ // the API proxy runs in docker beside the node, where replacing podman
41
+ // cannot reach it
42
+ this.provider.apiOrchestration = this.podmanManager.hostDocker;
43
+ }
33
44
  this.apiHandler = new ApiHandler(this.sdk, this.repository, this.provider, options.port);
34
45
  this.balanceHandler = new BalanceHandler(this.sdk);
35
46
  this.gridHandler = new GridHandler(this.sdk, this.repository);
@@ -46,6 +57,13 @@ export class BasicNode {
46
57
  await this.registerHandler.register();
47
58
  }
48
59
  async healthcheck() {
60
+ /**
61
+ * the podman container, when this node owns it: recreated between jobs,
62
+ * so a driver the host replaced since is injected afresh. Nothing of the
63
+ * node's runs inside it at this point, and the image and resource caches
64
+ * live on volumes that survive it.
65
+ */
66
+ await this.podmanManager?.recreate('between jobs');
49
67
  /**
50
68
  * run health check,
51
69
  */
@@ -76,6 +94,11 @@ export class BasicNode {
76
94
  getSystemEnvironment() {
77
95
  return this.repository.getNodeInfo().system_environment;
78
96
  }
97
+ /** An event line the podman container's CDI watcher logged. */
98
+ podmanEvent(event) {
99
+ if (isStaleCdiEvent(event))
100
+ this.podmanManager?.markStale(event);
101
+ }
79
102
  async stop() {
80
103
  await this.marketHandler.stop();
81
104
  await this.runHandler.stop();
@@ -98,6 +121,11 @@ export class BasicNode {
98
121
  if (await this.balanceHandler.balance()) {
99
122
  await this.balanceHandler.check(true);
100
123
  }
124
+ /**
125
+ * the podman container, when this node owns it: brought up if it is not
126
+ * running, and recreated if its GPU mounts went stale since the last job
127
+ */
128
+ await this.podmanManager?.ensure();
101
129
  /**
102
130
  * get an instance to the container
103
131
  */
@@ -119,6 +147,10 @@ export class BasicNode {
119
147
  try {
120
148
  let resolvedStatus = StopReasons.COMPLETED;
121
149
  const periodicHealthcheck = async () => {
150
+ // Leaves the queue, so the healthcheck that follows recreates podman
151
+ // before the node takes a job it cannot run.
152
+ if (this.podmanManager?.isStale())
153
+ return false;
122
154
  const { status, error } = await this.containerOrchestration.healthy();
123
155
  if (!status) {
124
156
  return false;
@@ -47,5 +47,13 @@ export declare class TaskManagerRegistry {
47
47
  /** Jobs with a task manager, which is to say the jobs this node is running. */
48
48
  size(): number;
49
49
  has(jobId: string): boolean;
50
+ /**
51
+ * Restart every operation that is running, in every job: the runtime under
52
+ * them is being replaced. Operations that finished stay finished and ones
53
+ * that have not started pick up the new runtime on their own; only what is
54
+ * running is aborted now and relaunched, which the provider holds until the
55
+ * runtime answers again.
56
+ */
57
+ restartRunningOperations(): void;
50
58
  stop(): Promise<void>;
51
59
  }
@@ -1,3 +1,4 @@
1
+ import { OperationProgressStatuses } from './TaskManager.js';
1
2
  /**
2
3
  * TaskManagerRegistry
3
4
  *
@@ -61,6 +62,27 @@ export class TaskManagerRegistry {
61
62
  has(jobId) {
62
63
  return this.registry.has(jobId);
63
64
  }
65
+ /**
66
+ * Restart every operation that is running, in every job: the runtime under
67
+ * them is being replaced. Operations that finished stay finished and ones
68
+ * that have not started pick up the new runtime on their own; only what is
69
+ * running is aborted now and relaunched, which the provider holds until the
70
+ * runtime answers again.
71
+ */
72
+ restartRunningOperations() {
73
+ for (const manager of this.registry.values()) {
74
+ const group = manager.getCurrentGroup();
75
+ if (!group)
76
+ continue;
77
+ for (const [opId, status] of Object.entries(manager.getCurrentGroupStatus())) {
78
+ if (status !== OperationProgressStatuses.RUNNING)
79
+ continue;
80
+ manager.restartTaskManagerOperation(group, opId).catch((error) => {
81
+ console.warn(`Could not restart operation ${opId} on the new runtime:`, error);
82
+ });
83
+ }
84
+ }
85
+ }
64
86
  async stop() {
65
87
  const stopPromises = [];
66
88
  for (const [jobId, manager] of this.registry.entries()) {
@@ -4,17 +4,30 @@ import EventEmitter from 'events';
4
4
  import { NodeRepository } from '../repository/NodeRepository.js';
5
5
  import { ResourceManager } from '../node/resource/resourceManager.js';
6
6
  import { ContainerOrchestrationInterface } from './containerOrchestration/interface.js';
7
+ import { type GpuRuntime } from './gpuRuntime.js';
7
8
  export declare class Provider {
8
9
  containerOrchestration: ContainerOrchestrationInterface;
9
10
  private repository;
10
11
  private resourceManager;
11
12
  private readonly frpcImage;
12
13
  private proxyStartupAbortController;
14
+ /**
15
+ * Where the node's own API proxy runs: the job runtime, unless the node
16
+ * manages podman, in which case it runs in docker beside the node so that
17
+ * replacing podman does not take the API down with it.
18
+ */
19
+ apiOrchestration: ContainerOrchestrationInterface;
20
+ /**
21
+ * The node's podman manager, when the node manages podman: operation starts
22
+ * wait out a recreate, and one whose container failed to start on the GPU
23
+ * runtime has podman recreated and is run again rather than failed.
24
+ */
25
+ gpuRuntime?: GpuRuntime;
13
26
  constructor(containerOrchestration: ContainerOrchestrationInterface, repository: NodeRepository, resourceManager: ResourceManager);
14
27
  hasTrustedExecutionEnvCapability(): Promise<boolean>;
15
28
  stopReverseProxyApi(address: string): Promise<boolean>;
16
29
  setUpReverseProxyApi(address: string, port: string): Promise<boolean>;
17
- taskManagerContainerRunOperation(flow: Flow, op: Operation<'container/run'>, controller: AbortController, emitter: EventEmitter): Promise<void>;
30
+ taskManagerContainerRunOperation(flow: Flow, op: Operation<'container/run'>, controller: AbortController, emitter: EventEmitter, retried?: boolean): Promise<void>;
18
31
  generateFrpcContainerConfig(name: string, networks: {
19
32
  [p: string]: {};
20
33
  }, flow: Flow, proxies: any[], isLoadBalanced: boolean, deploymentHash: string | undefined): {
@@ -8,6 +8,7 @@ import { promiseTimeoutWrapper } from '../utils/timeoutPromiseWrapper.js';
8
8
  import { liveAbortSignal } from '../utils/liveAbortSignal.js';
9
9
  import { generateProxies, generateUrlSecretObject, } from '../utils/expose-util.js';
10
10
  import { ContainerStateManager } from './ContainerStateManager.js';
11
+ import { isStaleGpuRuntimeError } from './gpuRuntime.js';
11
12
  export class Provider {
12
13
  constructor(containerOrchestration, repository, resourceManager) {
13
14
  this.containerOrchestration = containerOrchestration;
@@ -15,6 +16,7 @@ export class Provider {
15
16
  this.resourceManager = resourceManager;
16
17
  this.frpcImage = configs().frp.containerImage;
17
18
  this.proxyStartupAbortController = undefined;
19
+ this.apiOrchestration = containerOrchestration;
18
20
  applyLoggingProxyToClass(this);
19
21
  }
20
22
  async hasTrustedExecutionEnvCapability() {
@@ -28,13 +30,13 @@ export class Provider {
28
30
  }
29
31
  try {
30
32
  // Check if the frpc container exists and stop/delete it
31
- const doesFrpcExist = await this.containerOrchestration.doesContainerExist(frpc_name);
33
+ const doesFrpcExist = await this.apiOrchestration.doesContainerExist(frpc_name);
32
34
  if (doesFrpcExist) {
33
- await this.containerOrchestration.stopAndDeleteContainer(frpc_name);
35
+ await this.apiOrchestration.stopAndDeleteContainer(frpc_name);
34
36
  }
35
37
  // check if network then delete
36
- if (await this.containerOrchestration.hasNetwork(networkName)) {
37
- await this.containerOrchestration.deleteNetwork(networkName);
38
+ if (await this.apiOrchestration.hasNetwork(networkName)) {
39
+ await this.apiOrchestration.deleteNetwork(networkName);
38
40
  }
39
41
  }
40
42
  catch (error) {
@@ -48,21 +50,21 @@ export class Provider {
48
50
  const nodeName = configs().frp.nodeInternalHostName;
49
51
  const networkName = `api-${address}`;
50
52
  const frpc_name = `frpc-api-${address}`;
51
- await this.containerOrchestration.createNetwork(networkName);
53
+ await this.apiOrchestration.createNetwork(networkName);
52
54
  const networks = {};
53
55
  networks[networkName] = {};
54
56
  const startup = new AbortController();
55
57
  this.proxyStartupAbortController = startup;
56
- await this.containerOrchestration.pullImage(this.frpcImage, undefined, startup);
58
+ await this.apiOrchestration.pullImage(this.frpcImage, undefined, startup);
57
59
  this.resourceManager.images.setImage(this.frpcImage);
58
- const doesFrpcExist = await this.containerOrchestration.doesContainerExist(frpc_name);
60
+ const doesFrpcExist = await this.apiOrchestration.doesContainerExist(frpc_name);
59
61
  // Stopped while the image was being pulled, so the proxy is not wanted:
60
62
  // creating it now would put back what the caller has taken down.
61
63
  if (startup.signal.aborted) {
62
64
  throw new Error('API proxy setup was stopped');
63
65
  }
64
66
  if (!doesFrpcExist) {
65
- await this.containerOrchestration.runFlowContainer(this.frpcImage, {
67
+ await this.apiOrchestration.runFlowContainer(this.frpcImage, {
66
68
  name: 'frpc-api-' + address,
67
69
  cmd: ['-c', '/etc/frp/frpc.toml'],
68
70
  networks,
@@ -89,10 +91,10 @@ export class Provider {
89
91
  });
90
92
  }
91
93
  else {
92
- const hasFrpcExited = await this.containerOrchestration.isContainerExited(frpc_name);
94
+ const hasFrpcExited = await this.apiOrchestration.isContainerExited(frpc_name);
93
95
  if (hasFrpcExited) {
94
- await this.containerOrchestration.stopAndDeleteContainer(frpc_name);
95
- await this.containerOrchestration.runFlowContainer(this.frpcImage, {
96
+ await this.apiOrchestration.stopAndDeleteContainer(frpc_name);
97
+ await this.apiOrchestration.runFlowContainer(this.frpcImage, {
96
98
  name: 'frpc-api-' + address,
97
99
  cmd: ['-c', '/etc/frp/frpc.toml'],
98
100
  networks,
@@ -126,7 +128,7 @@ export class Provider {
126
128
  this.proxyStartupAbortController = undefined;
127
129
  return true;
128
130
  }
129
- async taskManagerContainerRunOperation(flow, op, controller, emitter) {
131
+ async taskManagerContainerRunOperation(flow, op, controller, emitter, retried = false) {
130
132
  let stateManager;
131
133
  let exposedPortHealthCheck;
132
134
  if (controller.signal.aborted) {
@@ -134,6 +136,9 @@ export class Provider {
134
136
  emitter.emit('end');
135
137
  return;
136
138
  }
139
+ // The runtime may be being replaced under a repair; nothing starts until
140
+ // it answers again.
141
+ await this.gpuRuntime?.ready();
137
142
  try {
138
143
  emitter.emit('start');
139
144
  let frpcContainer;
@@ -292,8 +297,44 @@ export class Provider {
292
297
  emitter.emit('exit', { exitCode: info.State.ExitCode });
293
298
  }
294
299
  catch (error) {
295
- emitter.emit('log', error, 'error');
296
- emitter.emit('error', error);
300
+ const fail = () => {
301
+ emitter.emit('log', error, 'error');
302
+ emitter.emit('error', error);
303
+ };
304
+ const runtime = this.gpuRuntime;
305
+ const message = error instanceof Error ? error.message : String(error);
306
+ // The container could not start because the GPU runtime under it went
307
+ // stale, which the node can fix: podman is recreated and the operation
308
+ // is run again from the top, sidecars included. Once only, and not when
309
+ // the recreate's own restart of this operation (the abort) relaunches it.
310
+ if (!runtime ||
311
+ controller.signal.aborted ||
312
+ !(isStaleGpuRuntimeError(error) || runtime.isStale())) {
313
+ fail();
314
+ }
315
+ else if (retried) {
316
+ // The recreate did not help: a replaced kernel module takes a reboot.
317
+ emitter.emit('log', "The container still cannot start on the GPU runtime after podman was recreated. If the host's NVIDIA driver was replaced, reboot the host.", 'info');
318
+ fail();
319
+ }
320
+ else {
321
+ emitter.emit('log', `Container could not start on the GPU runtime, recreating podman and retrying: ${message}`, 'info');
322
+ try {
323
+ await runtime.recreate(`container start failed on the GPU runtime: ${message}`);
324
+ }
325
+ catch (recreateError) {
326
+ emitter.emit('log', recreateError, 'error');
327
+ emitter.emit('error', recreateError);
328
+ emitter.emit('end');
329
+ return;
330
+ }
331
+ if (controller.signal.aborted) {
332
+ fail();
333
+ }
334
+ else {
335
+ return this.taskManagerContainerRunOperation(flow, op, controller, emitter, true);
336
+ }
337
+ }
297
338
  }
298
339
  // Clean up resources
299
340
  stateManager?.stopMonitoring();
@@ -0,0 +1,13 @@
1
+ /**
2
+ * What the provider needs of the node's podman manager, when the node
3
+ * manages podman.
4
+ */
5
+ export interface GpuRuntime {
6
+ /** Resolves once no recreate is in flight: operation starts wait on this. */
7
+ ready(): Promise<void>;
8
+ /** Whether podman's own watcher has found the GPU runtime stale. */
9
+ isStale(): boolean;
10
+ /** Replace the runtime; the operations running on it are restarted around it. */
11
+ recreate(reason: string): Promise<void>;
12
+ }
13
+ export declare function isStaleGpuRuntimeError(error: unknown): boolean;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * How a container start fails when the podman container's copy of the host's
3
+ * NVIDIA driver no longer matches the host. Recreating podman is what fixes
4
+ * it, so these are told apart from failures of the container itself.
5
+ *
6
+ * - At create, podman could not resolve or inject the CDI spec:
7
+ * "setting up CDI devices: unresolvable CDI devices nvidia.com/gpu=all".
8
+ * - At start, the OCI runtime could not bind a driver file the spec names,
9
+ * because the host replaced it: "crun: error mounting
10
+ * `/usr/lib/x86_64-linux-gnu/libcuda.so.550.120.04` to rootfs ...: No such
11
+ * file or directory". The file need not carry "nvidia" in its name.
12
+ * - The toolkit's own hook failed: "nvidia-container-cli: mount error".
13
+ *
14
+ * An image that happens to be NVIDIA's ("nvidia/cuda: image not known") is
15
+ * not a match: that is the container's failure, not the runtime's.
16
+ */
17
+ const STALE_GPU_RUNTIME = [
18
+ /setting up CDI devices|unresolvable CDI devices/i,
19
+ /nvidia-container-(cli|runtime)/i,
20
+ /\b(mount|mounting|stat)\b[^\n]*?(\/dev\/nvidia|libnvidia|libcuda|libnv|nvidia-smi|nvidia-persistenced)/i,
21
+ ];
22
+ export function isStaleGpuRuntimeError(error) {
23
+ const message = error instanceof Error
24
+ ? error.message
25
+ : String(error?.message ?? error);
26
+ return STALE_GPU_RUNTIME.some((pattern) => pattern.test(message));
27
+ }
@@ -0,0 +1,72 @@
1
+ import type { ContainerCreateOptions } from 'dockerode';
2
+ import type { DockerContainerOrchestration } from '../containerOrchestration/docker/index.js';
3
+ import type { ContainerOrchestrationInterface } from '../containerOrchestration/interface.js';
4
+ /** A CDI event line, as the podman container logs them, that asks for a recreate. */
5
+ export declare function isStaleCdiEvent(line: string): boolean;
6
+ /**
7
+ * Owns the podman container the node uses as its provider, over the host's
8
+ * docker socket. The node runs beside podman rather than inside it, so it can
9
+ * recreate podman without taking itself down.
10
+ *
11
+ * The container is a snapshot of the host's NVIDIA driver, taken when it is
12
+ * created; once the host replaces the driver underneath it, GPU containers
13
+ * inside it can no longer start. Recreating the container is the fix, and it
14
+ * is cheap: the image cache and the resource volumes live on docker volumes
15
+ * that survive it. What does not survive is whatever was running inside, so a
16
+ * recreate restarts the running operations around itself, and operation
17
+ * starts wait on `ready()` while it is under way.
18
+ */
19
+ export declare class PodmanManager {
20
+ /** The host's docker, which also runs the node's API proxy. */
21
+ readonly hostDocker: DockerContainerOrchestration;
22
+ private podman;
23
+ private docker;
24
+ private configDir;
25
+ private staleReason?;
26
+ private recreating?;
27
+ /** What a create needs and nothing changes afterwards, settled once. */
28
+ private prepared?;
29
+ /**
30
+ * Created by this process and not used by any operation since: it already
31
+ * carries the host's current driver, so a recreate would change nothing.
32
+ */
33
+ private fresh;
34
+ constructor(
35
+ /** The host's docker, which also runs the node's API proxy. */
36
+ hostDocker: DockerContainerOrchestration, configDir: string, podman: ContainerOrchestrationInterface);
37
+ /**
38
+ * Note a fault the podman container's watcher reported. Acted on by the
39
+ * next recreate, and while the node is queued it leaves the queue for one.
40
+ */
41
+ markStale(reason: string): void;
42
+ isStale(): boolean;
43
+ /** Resolves once no recreate is in flight: operation starts wait on this. */
44
+ ready(): Promise<void>;
45
+ /** The podman container exists, runs, and answers on its socket. */
46
+ ensure(): Promise<void>;
47
+ /**
48
+ * Remove the podman container and bring up a fresh one; the volumes stay.
49
+ * Operations running inside are restarted around it: aborted now, while
50
+ * their containers can still be stopped, and relaunched once the new
51
+ * socket answers, which `ready()` holds them for. A recreate already under
52
+ * way is joined rather than repeated, and a container this process created
53
+ * that nothing has used since is left as it is, unless it was marked stale.
54
+ */
55
+ recreate(reason: string): Promise<void>;
56
+ private doRecreate;
57
+ private ensureRunning;
58
+ private create;
59
+ /** @returns the host path of the node's config dir, for the bind mount. */
60
+ private prepare;
61
+ private waitForSocket;
62
+ private ensureVolume;
63
+ private ensureNetwork;
64
+ /**
65
+ * Where the node's config dir lives on the host, read off this container's
66
+ * own mounts: podman needs it at the host path, and the node only knows its
67
+ * own.
68
+ */
69
+ private hostConfigDir;
70
+ }
71
+ /** Mirrors the podman container the hosted start script creates. */
72
+ export declare function podmanContainerConfig(image: string, hostConfigDir: string): ContainerCreateOptions;
@@ -0,0 +1,238 @@
1
+ import os from 'os';
2
+ import chalk from 'chalk';
3
+ import { configs } from '../../configs/configs.js';
4
+ import { sleep } from '../../utils/utils.js';
5
+ import { TaskManagerRegistry } from '../../node/task/TaskManagerRegistry.js';
6
+ const PODMAN_CONTAINER_NAME = 'podman';
7
+ /**
8
+ * The docker network the node, the podman container and the node's API proxy
9
+ * share. It carries the name the docker provider hard-codes into every
10
+ * container it starts (see createDockerRunOptions), which is how the API
11
+ * proxy lands on it and reaches the node by container name. It is not the
12
+ * NOSANA_GATEWAY podman creates inside itself for the jobs, hence the
13
+ * different subnet.
14
+ */
15
+ const HOST_NETWORK = 'NOSANA_GATEWAY';
16
+ const CACHE_VOLUME = 'podman-cache';
17
+ const SOCKET_VOLUME = 'podman-socket';
18
+ const SOCKET_WAIT_S = 60;
19
+ const SOCKET_POLL_S = 2;
20
+ /**
21
+ * Events the podman container's CDI watcher writes when a GPU path it needs
22
+ * is gone and cannot be repaired from in there: only recreating the container
23
+ * re-injects the host's current driver.
24
+ */
25
+ const STALE_CDI_EVENTS = ['unrepairable', 'repair-failed'];
26
+ /** A CDI event line, as the podman container logs them, that asks for a recreate. */
27
+ export function isStaleCdiEvent(line) {
28
+ try {
29
+ return STALE_CDI_EVENTS.includes(JSON.parse(line).event);
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ }
35
+ /**
36
+ * Owns the podman container the node uses as its provider, over the host's
37
+ * docker socket. The node runs beside podman rather than inside it, so it can
38
+ * recreate podman without taking itself down.
39
+ *
40
+ * The container is a snapshot of the host's NVIDIA driver, taken when it is
41
+ * created; once the host replaces the driver underneath it, GPU containers
42
+ * inside it can no longer start. Recreating the container is the fix, and it
43
+ * is cheap: the image cache and the resource volumes live on docker volumes
44
+ * that survive it. What does not survive is whatever was running inside, so a
45
+ * recreate restarts the running operations around itself, and operation
46
+ * starts wait on `ready()` while it is under way.
47
+ */
48
+ export class PodmanManager {
49
+ constructor(
50
+ /** The host's docker, which also runs the node's API proxy. */
51
+ hostDocker, configDir, podman) {
52
+ this.hostDocker = hostDocker;
53
+ this.podman = podman;
54
+ /**
55
+ * Created by this process and not used by any operation since: it already
56
+ * carries the host's current driver, so a recreate would change nothing.
57
+ */
58
+ this.fresh = false;
59
+ this.docker = hostDocker.docker;
60
+ this.configDir = configDir.replace(/^~/, os.homedir()).replace(/\/+$/, '');
61
+ }
62
+ /**
63
+ * Note a fault the podman container's watcher reported. Acted on by the
64
+ * next recreate, and while the node is queued it leaves the queue for one.
65
+ */
66
+ markStale(reason) {
67
+ if (this.staleReason)
68
+ return;
69
+ this.staleReason = reason;
70
+ console.warn(chalk.yellow(`The podman container's GPU mounts are stale (${reason}); it is recreated before the next job.`));
71
+ }
72
+ isStale() {
73
+ return this.staleReason !== undefined;
74
+ }
75
+ /** Resolves once no recreate is in flight: operation starts wait on this. */
76
+ ready() {
77
+ this.fresh = false;
78
+ return this.recreating ?? Promise.resolve();
79
+ }
80
+ /** The podman container exists, runs, and answers on its socket. */
81
+ async ensure() {
82
+ await this.ensureRunning();
83
+ await this.waitForSocket();
84
+ }
85
+ /**
86
+ * Remove the podman container and bring up a fresh one; the volumes stay.
87
+ * Operations running inside are restarted around it: aborted now, while
88
+ * their containers can still be stopped, and relaunched once the new
89
+ * socket answers, which `ready()` holds them for. A recreate already under
90
+ * way is joined rather than repeated, and a container this process created
91
+ * that nothing has used since is left as it is, unless it was marked stale.
92
+ */
93
+ recreate(reason) {
94
+ if (this.recreating)
95
+ return this.recreating;
96
+ if (this.fresh && !this.staleReason)
97
+ return Promise.resolve();
98
+ this.recreating = this.doRecreate(reason).finally(() => {
99
+ this.recreating = undefined;
100
+ });
101
+ return this.recreating;
102
+ }
103
+ async doRecreate(reason) {
104
+ console.log(chalk.yellow(`Recreating the podman container: ${reason}`));
105
+ this.staleReason = undefined;
106
+ TaskManagerRegistry.getInstance().restartRunningOperations();
107
+ try {
108
+ await this.docker
109
+ .getContainer(PODMAN_CONTAINER_NAME)
110
+ .remove({ force: true });
111
+ }
112
+ catch (error) {
113
+ if (error.statusCode !== 404)
114
+ throw error;
115
+ }
116
+ await this.ensureRunning();
117
+ await this.waitForSocket();
118
+ }
119
+ async ensureRunning() {
120
+ const container = this.docker.getContainer(PODMAN_CONTAINER_NAME);
121
+ let info;
122
+ try {
123
+ info = await container.inspect();
124
+ }
125
+ catch (error) {
126
+ if (error.statusCode !== 404)
127
+ throw error;
128
+ return this.create();
129
+ }
130
+ if (!info.State.Running)
131
+ await container.start();
132
+ }
133
+ async create() {
134
+ const image = configs().podman.containerImage;
135
+ this.prepared ??= this.prepare(image).catch((error) => {
136
+ this.prepared = undefined; // so the next create tries again
137
+ throw error;
138
+ });
139
+ const hostConfigDir = await this.prepared;
140
+ console.log(chalk.cyan(`Creating the podman container from ${image}`));
141
+ const container = await this.docker.createContainer(podmanContainerConfig(image, hostConfigDir));
142
+ await container.start();
143
+ this.fresh = true;
144
+ }
145
+ /** @returns the host path of the node's config dir, for the bind mount. */
146
+ async prepare(image) {
147
+ const [hostConfigDir] = await Promise.all([
148
+ this.hostConfigDir(),
149
+ this.ensureVolume(CACHE_VOLUME),
150
+ this.ensureVolume(SOCKET_VOLUME),
151
+ this.ensureNetwork(),
152
+ this.hostDocker.pullImage(image),
153
+ ]);
154
+ return hostConfigDir;
155
+ }
156
+ async waitForSocket() {
157
+ const deadline = Date.now() + SOCKET_WAIT_S * 1000;
158
+ while (Date.now() < deadline) {
159
+ if ((await this.podman.healthy()).status)
160
+ return;
161
+ await sleep(SOCKET_POLL_S);
162
+ }
163
+ throw new Error(`The podman container is running but its socket did not answer within ${SOCKET_WAIT_S}s`);
164
+ }
165
+ async ensureVolume(name) {
166
+ if (!(await this.hostDocker.hasVolume(name))) {
167
+ await this.hostDocker.createVolume(name);
168
+ }
169
+ }
170
+ async ensureNetwork() {
171
+ if (await this.hostDocker.hasNetwork(HOST_NETWORK))
172
+ return;
173
+ await this.docker.createNetwork({
174
+ Name: HOST_NETWORK,
175
+ Driver: 'bridge',
176
+ IPAM: {
177
+ Driver: 'default',
178
+ Config: [{ Subnet: '192.168.102.0/24', Gateway: '192.168.102.1' }],
179
+ },
180
+ });
181
+ }
182
+ /**
183
+ * Where the node's config dir lives on the host, read off this container's
184
+ * own mounts: podman needs it at the host path, and the node only knows its
185
+ * own.
186
+ */
187
+ async hostConfigDir() {
188
+ let mounts = [];
189
+ try {
190
+ ({ Mounts: mounts } = await this.docker
191
+ .getContainer(os.hostname())
192
+ .inspect());
193
+ }
194
+ catch {
195
+ // Reported below, with what is needed rather than what failed.
196
+ }
197
+ const mount = mounts.find((m) => m.Destination.replace(/\/+$/, '') === this.configDir);
198
+ if (!mount?.Source) {
199
+ throw new Error(`Cannot manage podman: the node's config dir ${this.configDir} must be bind-mounted from the host, and the node must run in a docker container whose hostname is its id.`);
200
+ }
201
+ return mount.Source;
202
+ }
203
+ }
204
+ /** Mirrors the podman container the hosted start script creates. */
205
+ export function podmanContainerConfig(image, hostConfigDir) {
206
+ return {
207
+ name: PODMAN_CONTAINER_NAME,
208
+ Image: image,
209
+ Cmd: ['unix:/podman/podman.sock'],
210
+ Env: ['ENABLE_GPU=true', 'NVIDIA_DRIVER_CAPABILITIES=all'],
211
+ NetworkingConfig: { EndpointsConfig: { [HOST_NETWORK]: {} } },
212
+ HostConfig: {
213
+ Privileged: true,
214
+ DeviceRequests: [
215
+ { Driver: 'nvidia', Count: -1, Capabilities: [['gpu']] },
216
+ ],
217
+ Devices: [
218
+ {
219
+ PathOnHost: '/dev/fuse',
220
+ PathInContainer: '/dev/fuse',
221
+ CgroupPermissions: 'rwm',
222
+ },
223
+ ],
224
+ Mounts: [
225
+ { Type: 'volume', Source: CACHE_VOLUME, Target: '/var/lib/containers' },
226
+ { Type: 'volume', Source: SOCKET_VOLUME, Target: '/podman' },
227
+ { Type: 'bind', Source: hostConfigDir, Target: '/root/.nosana' },
228
+ {
229
+ Type: 'bind',
230
+ Source: '/run/nvidia-persistenced',
231
+ Target: '/run/nvidia-persistenced',
232
+ BindOptions: { Propagation: 'rslave' },
233
+ },
234
+ ],
235
+ RestartPolicy: { Name: 'unless-stopped' },
236
+ },
237
+ };
238
+ }
@@ -52,8 +52,10 @@ export const generateProxies = (flowId, op, opIndex, ports, name, operationId, d
52
52
  for (let exposedPort of ports) {
53
53
  // TODO: use ids generated in the taskmanager to make secerts
54
54
  const generatedId = generateExposeId(flowId, opIndex, exposedPort.port, op.args.private);
55
+ // When an op exposes more than one port, include the port in the id so each
56
+ // port gets a distinct deployment endpoint instead of colliding on a shared one.
55
57
  const generatedDeploymentId = deploymentId
56
- ? generateExposeId(deploymentId, op.id, 0, false)
58
+ ? generateExposeId(deploymentId, op.id, ports.length > 1 ? exposedPort.port : 0, false)
57
59
  : undefined;
58
60
  let proxyHTTPHealthCheckPath = undefined;
59
61
  if (exposedPort.health_checks && exposedPort.health_checks.length > 0) {
@@ -2,9 +2,10 @@ import { DB } from '../../../NodeManager/db/index.js';
2
2
  import { ResourceManager } from '../../../NodeManager/node/resource/resourceManager.js';
3
3
  import { selectContainerOrchestrationProvider } from '../../../NodeManager/provider/containerOrchestration/selectContainerOrchestration.js';
4
4
  import { NodeRepository } from '../../../NodeManager/repository/NodeRepository.js';
5
+ import { resolveProvider } from '../sharedOptions/index.js';
5
6
  export * from './action.js';
6
7
  export async function pruneResources(options) {
7
- options.provider = process.argv.some(arg => arg === '--docker') ? 'docker' : 'podman';
8
+ resolveProvider(options);
8
9
  try {
9
10
  const db = new DB(options.config).db;
10
11
  const repository = new NodeRepository(db);
@@ -13,8 +13,11 @@ import { generateDeploymentEndpointsTable } from './generateDeploymentEndpointsT
13
13
  import { generateRandomId } from '../../../NodeManager/node/utils/generateRandomId.js';
14
14
  import { getSDK } from '../../../NodeManager/sdk/index.js';
15
15
  import { createLoggingProxy } from '../../../NodeManager/monitoring/proxy/loggingProxy.js';
16
+ import { PodmanManager } from '../../../NodeManager/provider/podmanManager/PodmanManager.js';
17
+ import { DockerContainerOrchestration } from '../../../NodeManager/provider/containerOrchestration/docker/index.js';
18
+ import { resolveProvider } from '../sharedOptions/index.js';
16
19
  export async function runJob(jobDefinitionFile, options) {
17
- options.provider = process.argv.some(arg => arg === '--docker') ? 'docker' : 'podman';
20
+ resolveProvider(options);
18
21
  try {
19
22
  const sdk = getSDK();
20
23
  const jobDefinition = await resolveJobDefinition(options, jobDefinitionFile);
@@ -32,6 +35,16 @@ export async function runJob(jobDefinitionFile, options) {
32
35
  const containerOrchestration = selectContainerOrchestrationProvider(options.provider, options.podman, options.gpu, options.trustedExecutionRuntime);
33
36
  const resourceManager = new ResourceManager(containerOrchestration, repository);
34
37
  const provider = new Provider(containerOrchestration, repository, resourceManager);
38
+ /**
39
+ * the podman container, when this node owns it: brought up before the job
40
+ * needs it, and repaired under the job should a container fail to start on
41
+ * the GPU runtime
42
+ */
43
+ if (options.managedPodman) {
44
+ const podmanManager = new PodmanManager(new DockerContainerOrchestration(options.dockerSocket, options.gpu), options.config, containerOrchestration);
45
+ provider.gpuRuntime = podmanManager;
46
+ await podmanManager.ensure();
47
+ }
35
48
  const logger = new ConsoleLogger(false);
36
49
  logger.addObserver();
37
50
  const job = generateRandomId(32);
@@ -1,6 +1,6 @@
1
1
  import { Argument, Command, Option } from 'commander';
2
2
  import { runJob } from './action.js';
3
- import { gpuOption, verboseOption, walletOption } from '../sharedOptions/index.js';
3
+ import { gpuOption, managedPodmanOption, verboseOption, walletOption, } from '../sharedOptions/index.js';
4
4
  export * from './action.js';
5
5
  export const runNodeCommand = new Command('run')
6
6
  .description('Run Job Definition File')
@@ -8,6 +8,7 @@ export const runNodeCommand = new Command('run')
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
10
  .addOption(new Option('--docker, --podman <URI>', 'Podman/Docker connection URI').default('~/.nosana/podman/podman.sock'))
11
+ .addOption(managedPodmanOption)
11
12
  .addOption(walletOption)
12
13
  .addOption(gpuOption)
13
14
  .addOption(verboseOption)
@@ -1,5 +1,5 @@
1
1
  import { Table } from 'console-table-printer';
2
- import { isOperator, isSpreadMarker, } from '@nosana/sdk';
2
+ import { getExposePorts, isOperator, isSpreadMarker, } from '@nosana/sdk';
3
3
  import { generateExposeId } from '../../../NodeManager/utils/expose-util.js';
4
4
  import { configs } from '../../../NodeManager/configs/configs.js';
5
5
  export function generateDeploymentEndpointsTable(jobDefinition) {
@@ -14,9 +14,12 @@ export function generateDeploymentEndpointsTable(jobDefinition) {
14
14
  if (op.type === 'container/run') {
15
15
  const { expose } = op.args;
16
16
  if (expose) {
17
+ // Mirror generateProxies: only single-port ops fall back to 0, so a
18
+ // multi-port op gets a distinct endpoint per port.
19
+ const exposedPortCount = getExposePorts(op).length;
17
20
  if (typeof expose === 'number' ||
18
21
  (typeof expose === 'string' && !isOperator(expose))) {
19
- const generatedId = generateExposeId(jobDefinition.deployment_id, op.id, 0, false);
22
+ const generatedId = generateExposeId(jobDefinition.deployment_id, op.id, exposedPortCount > 1 ? expose : 0, false);
20
23
  table.addRow({
21
24
  OpId: op.id,
22
25
  Port: expose,
@@ -30,7 +33,7 @@ export function generateDeploymentEndpointsTable(jobDefinition) {
30
33
  if (typeof port === 'string' && isOperator(port))
31
34
  return; // skip dynamic
32
35
  const p = typeof port === 'object' ? port.port : port;
33
- const generatedId = generateExposeId(jobDefinition.deployment_id, op.id, 0, false);
36
+ const generatedId = generateExposeId(jobDefinition.deployment_id, op.id, exposedPortCount > 1 ? p : 0, false);
34
37
  table.addRow({
35
38
  OpId: op.id,
36
39
  Port: p,
@@ -0,0 +1,16 @@
1
+ import { Option } from 'commander';
2
+ /**
3
+ * Where the managed podman answers, inside the node: the podman-socket volume
4
+ * is mounted there, and at /podman in the podman container.
5
+ */
6
+ export declare const MANAGED_PODMAN_SOCKET = "~/.nosana/podman/podman.sock";
7
+ export declare const managedPodmanOption: Option;
8
+ /**
9
+ * Settles which provider the --docker/--podman URI names. With
10
+ * --managed-podman the URI is the host's docker socket, over which the node
11
+ * creates its podman container; that podman answers on the volume the two
12
+ * share, at the default podman socket path, and runs every job.
13
+ */
14
+ export declare function resolveProvider(options: {
15
+ [key: string]: any;
16
+ }): void;
@@ -0,0 +1,25 @@
1
+ import { Option } from 'commander';
2
+ /**
3
+ * Where the managed podman answers, inside the node: the podman-socket volume
4
+ * is mounted there, and at /podman in the podman container.
5
+ */
6
+ export const MANAGED_PODMAN_SOCKET = '~/.nosana/podman/podman.sock';
7
+ export const managedPodmanOption = new Option('--managed-podman', 'Run jobs in a podman container the node creates over the docker socket given with --docker, and recreates when its GPU mounts go stale');
8
+ /**
9
+ * Settles which provider the --docker/--podman URI names. With
10
+ * --managed-podman the URI is the host's docker socket, over which the node
11
+ * creates its podman container; that podman answers on the volume the two
12
+ * share, at the default podman socket path, and runs every job.
13
+ */
14
+ export function resolveProvider(options) {
15
+ if (options.managedPodman) {
16
+ options.provider = 'podman';
17
+ options.dockerSocket = options.podman;
18
+ options.podman = MANAGED_PODMAN_SOCKET;
19
+ }
20
+ else {
21
+ options.provider = process.argv.some((arg) => arg === '--docker')
22
+ ? 'docker'
23
+ : 'podman';
24
+ }
25
+ }
@@ -6,3 +6,4 @@ export * from './--port.js';
6
6
  export * from './--gpu.js';
7
7
  export * from './--tee-runtime.js';
8
8
  export * from './--verbose.js';
9
+ export * from './--managed-podman.js';
@@ -7,3 +7,4 @@ export * from './--port.js';
7
7
  export * from './--gpu.js';
8
8
  export * from './--tee-runtime.js';
9
9
  export * from './--verbose.js';
10
+ export * from './--managed-podman.js';
@@ -2,10 +2,11 @@ import 'rpc-websockets/dist/lib/client.js';
2
2
  import NodeManager from '../../../NodeManager/index.js';
3
3
  import { requestExit } from '../../../exitCodes.js';
4
4
  import { validateCLIVersion } from '../../../version/index.js';
5
+ import { resolveProvider } from '../sharedOptions/index.js';
5
6
  /** Failures no restart resolves: the node reports them and stops. */
6
7
  const TERMINAL_ERRORS = ['NodeBannedError', 'NodeNotQualifiedError'];
7
8
  export async function startNode(market, options) {
8
- options.provider = process.argv.some(arg => arg === '--docker') ? 'docker' : 'podman';
9
+ resolveProvider(options);
9
10
  const nodeManager = new NodeManager(options);
10
11
  while (true) {
11
12
  try {
@@ -1,6 +1,6 @@
1
1
  import { Command, Option } from 'commander';
2
2
  import { startNode } from './action.js';
3
- import { networkOption, portOption, rpcOption, walletOption, verboseOption, gpuOption, teeRuntimeOption, } from '../sharedOptions/index.js';
3
+ import { networkOption, portOption, rpcOption, walletOption, verboseOption, gpuOption, teeRuntimeOption, managedPodmanOption, } from '../sharedOptions/index.js';
4
4
  export * from './action.js';
5
5
  export const startNodeCommand = new Command('start')
6
6
  .description('Start Nosana Node')
@@ -9,6 +9,7 @@ export const startNodeCommand = new Command('start')
9
9
  .addOption(rpcOption)
10
10
  .addOption(walletOption)
11
11
  .addOption(new Option('--docker, --podman <URI>', 'Podman/Docker connection URI').default('~/.nosana/podman/podman.sock'))
12
+ .addOption(managedPodmanOption)
12
13
  .addOption(new Option('-c, --config <path>', 'Config path (to store the flows database and other config)').default('~/.nosana/'))
13
14
  .addOption(teeRuntimeOption)
14
15
  .addOption(gpuOption)
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@nosana/node",
3
- "version": "1.1.61-rc",
3
+ "version": "1.1.63-rc",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@nosana/node",
9
- "version": "1.1.61-rc",
9
+ "version": "1.1.63-rc",
10
10
  "license": "ISC",
11
11
  "dependencies": {
12
12
  "@coral-xyz/anchor": "^0.28.1-beta.1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nosana/node",
3
- "version": "1.1.61-rc",
3
+ "version": "1.1.63-rc",
4
4
  "description": "",
5
5
  "bin": {
6
6
  "nosana-node": "./dist/src/index.js"