diff --git a/src/@types/Services.ts b/src/@types/Services.ts index 384cbe01c..bb36c8ea5 100644 --- a/src/@types/Services.ts +++ b/src/@types/Services.ts @@ -152,18 +152,35 @@ export interface ServicePayment { // so a plaintext secret can never be forwarded unencrypted. export type ServiceUserData = Record -export interface ServiceStartParams { - environment: string // required: the envId to run the service on - image: string // base image name (or build label when dockerfile is set) +// The container specification shared by serviceStart and serviceRestart. Every field is +// optional here; the consuming type decides which are required (serviceStart re-declares +// `image` as mandatory). `userData` is always ECIES-encrypted to the node before sending. +export interface ServiceContainerSpec { + image?: string // base image name (or build label when dockerfile is set) tag?: string // pull by name:tag checksum?: string // pull by digest: "sha256:<64 hex>" dockerfile?: string // build from inline Dockerfile; requires allowImageBuild on the env additionalDockerFiles?: Record + userData?: ServiceUserData dockerCmd?: string[] // exec-form CMD override (no shell) dockerEntrypoint?: string[] +} + +// Optional container-spec overrides for serviceRestart. The node treats the restart +// atomically ("all-or-nothing"): providing ANY of image/tag/checksum/dockerfile/ +// additionalDockerFiles switches it into RESPEC mode — the container is rebuilt entirely +// from these values (`image` becomes mandatory, and exactly one of tag/checksum/dockerfile +// applies) instead of being bounced on the stored spec (REUSE mode). Passing none of them +// reuses the stored container spec unchanged. `userData`/`dockerCmd`/`dockerEntrypoint` +// keep their replace-when-supplied semantics: an omitted value reuses the stored one, an +// explicit value (including `[]`) REPLACES it. +export type ServiceRestartParams = ServiceContainerSpec + +export interface ServiceStartParams extends ServiceContainerSpec { + environment: string // required: the envId to run the service on + image: string // required for start (base image name, or build label when dockerfile is set) exposedPorts?: number[] resources?: ComputeResourceRequest[] duration: number // seconds; capped by serviceOnDemand.maxDurationSeconds - userData?: ServiceUserData payment: ServicePayment } diff --git a/src/services/providers/BaseProvider.ts b/src/services/providers/BaseProvider.ts index c2529e16c..c689876cf 100644 --- a/src/services/providers/BaseProvider.ts +++ b/src/services/providers/BaseProvider.ts @@ -33,9 +33,9 @@ import { ServiceJob, ServiceJobListed, ServiceListFilters, + ServiceRestartParams, ServiceTemplatePublic, ServiceStartParams, - ServiceUserData, ServicePayment, OceanNode, NodeP2P, @@ -860,18 +860,14 @@ export class BaseProvider { nodeUri: OceanNode, signerOrAuthToken: SignerOrAuthTokenOrSignature, serviceId: string, - userData?: ServiceUserData, - dockerCmd?: string[], - dockerEntrypoint?: string[], + params?: ServiceRestartParams, signal?: AbortSignal ): Promise { return this.getImpl(nodeUri).serviceRestart( nodeUri, signerOrAuthToken, serviceId, - userData, - dockerCmd, - dockerEntrypoint, + params, signal ) } diff --git a/src/services/providers/HttpProvider.ts b/src/services/providers/HttpProvider.ts index b317595d0..f50b94d83 100644 --- a/src/services/providers/HttpProvider.ts +++ b/src/services/providers/HttpProvider.ts @@ -31,6 +31,7 @@ import { ServiceJob, ServiceJobListed, ServiceListFilters, + ServiceRestartParams, ServiceTemplatePublic, ServiceStartParams, ServiceUserData, @@ -2123,21 +2124,36 @@ export class HttpProvider { } /** - * Restarts a running service (recreates the container). When `userData`, `dockerCmd` or - * `dockerEntrypoint` is supplied it REPLACES the stored value; - * otherwise the stored one is reused. Passing a new `dockerCmd` swaps the launch command - * (e.g. a different model) while keeping the same serviceId, host port and expiry. + * Restarts a running service (recreates the container) while keeping the same serviceId, + * payment, resources, host port(s) and expiry. + * + * The node treats the restart atomically. Passing no container-spec fields in `params` + * (REUSE mode) bounces the container on its stored spec unchanged. Passing ANY of + * `image`/`tag`/`checksum`/`dockerfile`/`additionalDockerFiles` (RESPEC mode) rebuilds the + * container entirely from `params`: `image` becomes mandatory, exactly one of + * `tag`/`checksum`/`dockerfile` applies, and a `dockerfile` requires `allowImageBuild` on + * the env (else the node replies 403). `userData`/`dockerCmd`/`dockerEntrypoint` are only + * sent when supplied — an omitted override reuses the node's stored value, whereas an + * explicit value (including `[]`) REPLACES it (matches ocean-node's restartService semantics). * @return {Promise} The restarted service job (single-element array). */ public async serviceRestart( nodeUri: string, signerOrAuthToken: SignerOrAuthTokenOrSignature, serviceId: string, - userData?: ServiceUserData, - dockerCmd?: string[], - dockerEntrypoint?: string[], + params?: ServiceRestartParams, signal?: AbortSignal ): Promise { + const { + image, + tag, + checksum, + dockerfile, + additionalDockerFiles, + userData, + dockerCmd, + dockerEntrypoint + } = params ?? {} const providerEndpoints = await this.getEndpoints(nodeUri) const serviceEndpoints = await this.getServiceEndpoints(nodeUri, providerEndpoints) const route = this.resolveServiceRoute(nodeUri, serviceEndpoints, 'serviceRestart') @@ -2160,8 +2176,13 @@ export class HttpProvider { ...authPayload, serviceId, userData: await this.encryptServiceUserData(nodeUri, userData), - // Only send when supplied — an omitted override reuses the node's stored value, whereas an - // explicit [] REPLACES it with "no override" (matches ocean-node's restartService semantics). + // Only send when supplied — an omitted field reuses the node's stored value, whereas an + // explicit value REPLACES it (matches ocean-node's restartService REUSE/RESPEC semantics). + ...(image !== undefined ? { image } : {}), + ...(tag !== undefined ? { tag } : {}), + ...(checksum !== undefined ? { checksum } : {}), + ...(dockerfile !== undefined ? { dockerfile } : {}), + ...(additionalDockerFiles !== undefined ? { additionalDockerFiles } : {}), ...(dockerCmd !== undefined ? { dockerCmd } : {}), ...(dockerEntrypoint !== undefined ? { dockerEntrypoint } : {}) }), diff --git a/src/services/providers/P2pProvider.ts b/src/services/providers/P2pProvider.ts index 1690a14bb..84155a6f9 100644 --- a/src/services/providers/P2pProvider.ts +++ b/src/services/providers/P2pProvider.ts @@ -46,6 +46,7 @@ import { ServiceJob, ServiceJobListed, ServiceListFilters, + ServiceRestartParams, ServiceTemplatePublic, ServiceStartParams, ServiceUserData, @@ -1866,11 +1867,19 @@ export class P2pProvider { nodeUri: OceanNode, signerOrAuthToken: SignerOrAuthTokenOrSignature, serviceId: string, - userData?: ServiceUserData, - dockerCmd?: string[], - dockerEntrypoint?: string[], + params?: ServiceRestartParams, signal?: AbortSignal ): Promise { + const { + image, + tag, + checksum, + dockerfile, + additionalDockerFiles, + userData, + dockerCmd, + dockerEntrypoint + } = params ?? {} const authPayload = await this.getSignedCommandParams( nodeUri, signerOrAuthToken, @@ -1884,8 +1893,13 @@ export class P2pProvider { ...authPayload, serviceId, userData: await this.encryptServiceUserData(nodeUri, userData), - // Only send when supplied — an omitted override reuses the node's stored value, whereas an - // explicit [] REPLACES it with "no override" (matches ocean-node's restartService semantics). + // Only send when supplied — an omitted field reuses the node's stored value, whereas an + // explicit value REPLACES it (matches ocean-node's restartService REUSE/RESPEC semantics). + ...(image !== undefined ? { image } : {}), + ...(tag !== undefined ? { tag } : {}), + ...(checksum !== undefined ? { checksum } : {}), + ...(dockerfile !== undefined ? { dockerfile } : {}), + ...(additionalDockerFiles !== undefined ? { additionalDockerFiles } : {}), ...(dockerCmd !== undefined ? { dockerCmd } : {}), ...(dockerEntrypoint !== undefined ? { dockerEntrypoint } : {}) }, diff --git a/test/integration/Services.test.ts b/test/integration/Services.test.ts index 0ca062dc7..4e82175a4 100644 --- a/test/integration/Services.test.ts +++ b/test/integration/Services.test.ts @@ -373,8 +373,6 @@ describe('Service on Demand flow tests', () => { consumerAccount, serviceId, undefined, - undefined, - undefined, opSignal() ) const running = await pollUntil(