diff --git a/src/activity/exe-unit/exe-unit.ts b/src/activity/exe-unit/exe-unit.ts index f32d68ca7..3569a90b2 100644 --- a/src/activity/exe-unit/exe-unit.ts +++ b/src/activity/exe-unit/exe-unit.ts @@ -23,9 +23,17 @@ import { Agreement, ProviderInfo } from "../../market"; import { TcpProxy } from "../../network/tcp-proxy"; import { ExecutionOptions, ExeScriptExecutor } from "../exe-script-executor"; import { lastValueFrom, tap, toArray } from "rxjs"; +import { DeployArgs } from "../script/command"; export type LifecycleFunction = (exe: ExeUnit) => Promise; +export type VolumeSpec = { + /** Size of the volume to mount */ + sizeGib: number; + /** Location of the volume */ + path: string; +}; + export interface ExeUnitOptions { activityDeployingTimeout?: number; storageProvider?: StorageProvider; @@ -38,6 +46,7 @@ export interface ExeUnitOptions { teardown?: LifecycleFunction; executionOptions?: ExecutionOptions; signalOrTimeout?: number | AbortSignal; + volumes?: Record; } export interface CommandOptions { @@ -158,7 +167,13 @@ export class ExeUnit { private async deployActivity() { try { const executionMetadata = await this.executor.execute( - new Script([new Deploy(this.networkNode?.getNetworkConfig?.()), new Start()]).getExeScriptRequest(), + new Script([ + new Deploy({ + ...this.networkNode?.getNetworkDeploymentArg?.(), + ...this.getVolumeDeploymentArg(), + }), + new Start(), + ]).getExeScriptRequest(), ); const result$ = this.executor.getResultsObservable(executionMetadata); // if any result is an error, throw an error @@ -422,4 +437,22 @@ export class ExeUnit { return allResults[0]; } + + private getVolumeDeploymentArg(): Pick { + if (!this.options?.volumes) { + return {}; + } + + const argument: Required> = { + volumes: {}, + }; + + for (const [, volumeSpec] of Object.entries(this.options.volumes)) { + argument.volumes[volumeSpec.path] = { + storage: { size: `${volumeSpec.sizeGib}g`, errors: "panic" }, + }; + } + + return argument; + } } diff --git a/src/activity/script/command.ts b/src/activity/script/command.ts index 012e9592b..b711acdfd 100644 --- a/src/activity/script/command.ts +++ b/src/activity/script/command.ts @@ -59,8 +59,32 @@ export class Command { } } +export type DeployArgs = { + net?: DeployNetworkArgs[]; + volumes?: DeployVolumesArgs; +}; + +type DeployNetworkArgs = { + id: string; + ip: string; + mask: string; + gateway?: string; + nodes: { [ip: string]: string }; + nodeIp: string; +}; + +type DeployVolumesArgs = { + [path: string]: { + storage: { + /** @example 7000m */ + size: string; + errors?: "panic"; + }; + }; +}; + export class Deploy extends Command { - constructor(args?: Record) { + constructor(args?: DeployArgs) { super("deploy", args); } } diff --git a/src/golem-network/golem-network.ts b/src/golem-network/golem-network.ts index aa7750617..5a35f01d4 100644 --- a/src/golem-network/golem-network.ts +++ b/src/golem-network/golem-network.ts @@ -35,7 +35,7 @@ import { DataTransferProtocol } from "../shared/types"; import { NetworkApiAdapter } from "../shared/yagna/adapters/network-api-adapter"; import { IProposalRepository } from "../market/proposal"; import { Subscription } from "rxjs"; -import { GolemConfigError } from "../shared/error/golem-error"; +import { GolemConfigError, GolemUserError } from "../shared/error/golem-error"; /** * Instance of an object or a factory function that you can call `new` on. @@ -158,6 +158,13 @@ export interface OneOfOptions { signalOrTimeout?: number | AbortSignal; setup?: ExeUnitOptions["setup"]; teardown?: ExeUnitOptions["teardown"]; + + /** + * Define additional volumes ot be mounted when the activity is deployed + * + * @experimental The Provider has to run yagna 0.17.x or newer and offer `vm` runtime 0.5.x or newer + */ + volumes?: ExeUnitOptions["volumes"]; } export interface ManyOfOptions { @@ -165,6 +172,13 @@ export interface ManyOfOptions { poolSize: PoolSize; setup?: ExeUnitOptions["setup"]; teardown?: ExeUnitOptions["teardown"]; + + /** + * Define additional volumes ot be mounted when the activity is deployed + * + * @experimental The Provider has to run yagna 0.17.x or newer and offer `vm` runtime 0.5.x or newer + */ + volumes?: ExeUnitOptions["volumes"]; } /** @@ -423,7 +437,12 @@ export class GolemNetwork { * @param options.setup - an optional function that is called as soon as the exe unit is ready * @param options.teardown - an optional function that is called before the exe unit is destroyed */ - async oneOf({ order, setup, teardown, signalOrTimeout }: OneOfOptions): Promise { + async oneOf({ order, setup, teardown, signalOrTimeout, volumes }: OneOfOptions): Promise { + this.validateSettings({ + order, + volumes, + }); + const { signal, cleanup: cleanupAbortSignals } = anyAbortSignal( createAbortSignalFromTimeout(signalOrTimeout), this.abortController.signal, @@ -458,6 +477,7 @@ export class GolemNetwork { .releaseAllocation(allocation) .catch((err) => this.logger.error("Error while releasing allocation", err)); }; + try { const proposalPool = new DraftOfferProposalPool({ logger: this.logger, @@ -492,7 +512,7 @@ export class GolemNetwork { payment: order.payment, activity: order.activity, networkNode, - exeUnit: { setup, teardown }, + exeUnit: { setup, teardown, volumes }, }); // We managed to create the activity, no need to look for more agreement candidates @@ -551,7 +571,12 @@ export class GolemNetwork { * @param options.setup - an optional function that is called as soon as the exe unit is ready * @param options.teardown - an optional function that is called before the exe unit is destroyed */ - public async manyOf({ poolSize, order, setup, teardown }: ManyOfOptions): Promise { + public async manyOf({ poolSize, order, setup, teardown, volumes }: ManyOfOptions): Promise { + this.validateSettings({ + order, + volumes, + }); + const signal = this.abortController.signal; let allocation: Allocation | undefined = undefined; let resourceRentalPool: ResourceRentalPool | undefined = undefined; @@ -605,7 +630,7 @@ export class GolemNetwork { resourceRentalOptions: { activity: order.activity, payment: order.payment, - exeUnit: { setup, teardown }, + exeUnit: { setup, teardown, volumes }, }, agreementOptions: { expirationSec: rentSeconds, @@ -668,4 +693,23 @@ export class GolemNetwork { return new NullStorageProvider(); } } + + /** + * A helper method used to check if the user provided settings and settings are reasonable + * @param settings + * @private + */ + private validateSettings(settings: { volumes?: ExeUnitOptions["volumes"]; order: MarketOrderSpec }) { + // Rule: If user specifies volumes and the min storage size, then the min storage has to be at least of the largest volume size + if (settings.volumes && settings.order.demand.workload?.minStorageGib !== undefined) { + const largestVolumeSizeGib = Math.max(...Object.values(settings.volumes).map((spec) => spec.sizeGib)); + if (settings.order.demand.workload.minStorageGib < largestVolumeSizeGib) { + throw new GolemUserError("Your minStorageGib requirement is below your expected largest volume size."); + } + } + // Rule: Require minStorageGib settings for volume users to ensure that they will get suitable providers from the market + if (settings.volumes && settings.order.demand.workload?.minStorageGib === undefined) { + throw new GolemUserError("You have specified volumes but did not specify a minStorageGib requirement."); + } + } } diff --git a/src/network/node.ts b/src/network/node.ts index ceb6a91ae..3a09152bd 100644 --- a/src/network/node.ts +++ b/src/network/node.ts @@ -1,4 +1,5 @@ import { NetworkInfo } from "./network"; +import { DeployArgs } from "../activity/script/command"; /** * Describes a node in a VPN, mapping a Golem node id to an IP address @@ -13,10 +14,10 @@ export class NetworkNode { /** * Generate a dictionary of arguments that are required for the appropriate - *`Deploy` command of an exescript in order to pass the network configuration to the runtime + *`Deploy` command of an exe-script in order to pass the network configuration to the runtime * on the provider's end. */ - getNetworkConfig() { + getNetworkDeploymentArg(): Pick { return { net: [ { diff --git a/src/resource-rental/resource-rental.ts b/src/resource-rental/resource-rental.ts index ad1c08646..38b927e5f 100644 --- a/src/resource-rental/resource-rental.ts +++ b/src/resource-rental/resource-rental.ts @@ -24,7 +24,7 @@ export interface ResourceRentalEvents { } export interface ResourceRentalOptions { - exeUnit?: Pick; + exeUnit?: Pick; activity?: ExecutionOptions; payment?: Partial; networkNode?: NetworkNode;