@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.
- package/README.md +2 -2
- package/dist/.env +2 -0
- package/dist/package.json +9 -6
- package/dist/{src/NodeManager/provider/containerOrchestration → packages/container-orchestrator/src}/docker/createDockerRunOptions.d.ts +1 -1
- package/dist/{src/NodeManager/provider/containerOrchestration → packages/container-orchestrator/src}/docker/createDockerRunOptions.js +5 -1
- package/dist/{src/NodeManager/provider/containerOrchestration → packages/container-orchestrator/src}/docker/dockerExtended/index.d.ts +3 -0
- package/dist/{src/NodeManager/provider/containerOrchestration → packages/container-orchestrator/src}/docker/dockerExtended/index.js +12 -5
- package/dist/{src/NodeManager/provider/containerOrchestration → packages/container-orchestrator/src}/docker/index.d.ts +11 -10
- package/dist/{src/NodeManager/provider/containerOrchestration → packages/container-orchestrator/src}/docker/index.js +18 -37
- package/dist/packages/container-orchestrator/src/hooks.d.ts +32 -0
- package/dist/packages/container-orchestrator/src/index.d.ts +17 -0
- package/dist/packages/container-orchestrator/src/index.js +16 -0
- package/dist/{src/NodeManager/provider/containerOrchestration → packages/container-orchestrator/src}/interface.d.ts +14 -5
- package/dist/packages/container-orchestrator/src/interface.js +1 -0
- package/dist/{src/NodeManager/provider/containerOrchestration → packages/container-orchestrator/src}/labels.d.ts +2 -0
- package/dist/{src/NodeManager/provider/containerOrchestration → packages/container-orchestrator/src}/labels.js +2 -0
- package/dist/{src/NodeManager/provider/containerOrchestration → packages/container-orchestrator/src}/podman/index.d.ts +3 -3
- package/dist/{src/NodeManager/provider/containerOrchestration → packages/container-orchestrator/src}/podman/index.js +6 -17
- package/dist/{src/NodeManager/provider/containerOrchestration → packages/container-orchestrator/src}/podman/utils/createPodmanRunOptions.d.ts +12 -0
- package/dist/{src/NodeManager/provider/containerOrchestration → packages/container-orchestrator/src}/podman/utils/createPodmanRunOptions.js +4 -2
- package/dist/packages/container-orchestrator/src/selectContainerOrchestration.d.ts +3 -0
- package/dist/{src/NodeManager/provider/containerOrchestration → packages/container-orchestrator/src}/selectContainerOrchestration.js +3 -3
- package/dist/packages/container-orchestrator/src/types.d.ts +15 -0
- package/dist/packages/container-orchestrator/src/types.js +1 -0
- package/dist/packages/container-orchestrator/src/utils/ifStringCastToArray.d.ts +2 -0
- package/dist/packages/container-orchestrator/src/utils/ifStringCastToArray.js +2 -0
- package/dist/src/NodeManager/configs/configs.d.ts +6 -0
- package/dist/src/NodeManager/configs/configs.js +6 -0
- package/dist/src/NodeManager/index.js +7 -0
- package/dist/src/NodeManager/node/Node.d.ts +5 -0
- package/dist/src/NodeManager/node/Node.js +23 -2
- package/dist/src/NodeManager/node/health/healthHandler.d.ts +1 -1
- package/dist/src/NodeManager/node/resource/definition/index.d.ts +0 -1
- package/dist/src/NodeManager/node/resource/definition/index.js +0 -1
- package/dist/src/NodeManager/node/resource/helpers/createHFArgs.d.ts +1 -1
- package/dist/src/NodeManager/node/resource/helpers/createHFArgs.js +2 -3
- package/dist/src/NodeManager/node/resource/helpers/createOllamaArgs.d.ts +1 -1
- package/dist/src/NodeManager/node/resource/helpers/createOllamaArgs.js +2 -3
- package/dist/src/NodeManager/node/resource/helpers/createS3Args.d.ts +1 -1
- package/dist/src/NodeManager/node/resource/helpers/createS3Args.js +3 -3
- package/dist/src/NodeManager/node/resource/image/imageManager.d.ts +1 -1
- package/dist/src/NodeManager/node/resource/resourceManager.d.ts +1 -1
- package/dist/src/NodeManager/node/resource/volume/volumeManager.d.ts +1 -1
- package/dist/src/NodeManager/node/resource/volume/volumeManager.js +11 -8
- package/dist/src/NodeManager/node/task/TaskManager.js +25 -0
- package/dist/src/NodeManager/provider/ContainerStateManager.js +1 -1
- package/dist/src/NodeManager/provider/Provider.d.ts +34 -10
- package/dist/src/NodeManager/provider/Provider.js +217 -93
- package/dist/src/NodeManager/provider/orchestrationHooks.d.ts +8 -0
- package/dist/src/NodeManager/provider/orchestrationHooks.js +13 -0
- package/dist/src/NodeManager/provider/sshSidecar.d.ts +2 -2
- package/dist/src/NodeManager/provider/sshSidecar.js +2 -2
- package/dist/src/NodeManager/provider/types.d.ts +0 -9
- package/dist/src/NodeManager/provider/webTerminal.d.ts +1 -1
- package/dist/src/NodeManager/provider/webTerminal.js +1 -1
- package/dist/src/NodeManager/utils/utils.d.ts +1 -7
- package/dist/src/NodeManager/utils/utils.js +1 -7
- package/dist/src/cli/commands/prune/action.js +3 -2
- package/dist/src/cli/commands/prune/command.js +1 -1
- package/dist/src/cli/commands/run/action.js +3 -2
- package/dist/src/cli/commands/run/command.js +1 -1
- package/dist/src/cli/commands/start/command.js +1 -1
- package/npm-shrinkwrap.json +21 -2
- package/package.json +9 -6
- package/dist/src/NodeManager/provider/containerOrchestration/docker/utils/createServerObject.d.ts +0 -5
- package/dist/src/NodeManager/provider/containerOrchestration/docker/utils/createServerObject.js +0 -15
- package/dist/src/NodeManager/provider/containerOrchestration/docker/utils/deadline.d.ts +0 -1
- package/dist/src/NodeManager/provider/containerOrchestration/docker/utils/deadline.js +0 -15
- package/dist/src/NodeManager/provider/containerOrchestration/selectContainerOrchestration.d.ts +0 -3
- /package/dist/{src/NodeManager/provider/containerOrchestration → packages/container-orchestrator/src}/docker/utils/repoTagsContainsImage.d.ts +0 -0
- /package/dist/{src/NodeManager/provider/containerOrchestration → packages/container-orchestrator/src}/docker/utils/repoTagsContainsImage.js +0 -0
- /package/dist/{src/NodeManager/provider/containerOrchestration/interface.js → packages/container-orchestrator/src/hooks.js} +0 -0
- /package/dist/{src/NodeManager → packages/container-orchestrator/src}/utils/liveAbortSignal.d.ts +0 -0
- /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 {
|
|
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 '
|
|
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 '
|
|
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 {
|
|
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)
|
|
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.
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
306
|
-
this.
|
|
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 '
|
|
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
|
-
|
|
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 '
|
|
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.
|
|
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 '
|
|
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 '
|
|
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,
|
|
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,
|
|
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 '
|
|
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 <
|
|
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 '
|
|
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 <
|
|
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 <
|
|
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)
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nosana/node",
|
|
3
|
-
"version": "1.1.
|
|
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.
|
|
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
|
}
|