Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 66 additions & 13 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -2084,33 +2084,86 @@ The updated `ServiceJob` (advanced `expiresAt`, new entry in `extendPayments`).

#### Description

Recreate the service container (no extra charge), keeping the same `expiresAt` and host ports.
Re-checks the environment service gate and access list; rejected if the service has expired.
Optionally pass `userData` to replace the stored env vars, and/or `dockerCmd`/`dockerEntrypoint`
to replace the stored CMD/ENTRYPOINT overrides — each is independent: supply it (even as `[]`)
to replace the stored value, or omit it to reuse what the service already has. This is the
recommended recovery path after a service lands in `Error` because its container died on its own
(the background health check leaves host ports/network/container record reserved specifically so
restart can reuse them), in addition to recovering from an explicit `serviceStop` or any other
terminal failure.
Recreate the service container (no extra charge), keeping the same `expiresAt`, resources and
host ports. Re-checks the environment service gate and access list; rejected if the service has
expired. This is the recommended recovery path after a service lands in `Error` because its
container died on its own (the background health check leaves host ports/network/container
record reserved specifically so restart can reuse them), in addition to recovering from an
explicit `serviceStop` or any other terminal failure.

**Restart is atomic — either all-old or all-new, never a mix of new params over the stored job:**

- **REUSE mode** — the request carries **none** of the container params below. The service
restarts on exactly its stored spec (image, `userData`, `dockerCmd`, `dockerEntrypoint`). Use
this to simply bounce a service back to `Running`.
- **RESPEC mode** — the request carries **any** container param. The container is rebuilt
entirely from the request: `image` becomes **required** and exactly one of `tag`/`checksum`/
`dockerfile` applies (validated exactly like `serviceStart`). `userData`/`dockerCmd`/
`dockerEntrypoint` are taken **as-sent** — anything omitted here is empty/unset, it is **not**
pulled from the stored job. This is the bug-fix flow: publish a fixed image under a new tag,
then restart re-supplying the full spec (e.g. same `image`, new `tag`).

Because `image` is mandatory whenever any container param is present, you cannot ride a new
`userData`/`dockerCmd` on top of the stored image — a partial change is rejected (400). Payment,
resources and duration are always preserved; only the container spec can change. A service whose
start payment was **never claimed** (escrow lock failed or was refunded) cannot be restarted —
start a new service instead.

#### Request Body

REUSE mode (bounce the service on its stored spec):

```json
{
"consumerAddress": "0x...",
"nonce": "123",
"signature": "0x...",
"serviceId": "0x..."
}
```

RESPEC mode (restart on a new image spec — `image` required, plus at most one of
`tag`/`checksum`/`dockerfile`):

```json
{
"consumerAddress": "0x...",
"nonce": "123",
"signature": "0x...",
"serviceId": "0x...",
"userData": "<optional ECIES-encrypted hex; replaces stored userData>",
"dockerCmd": ["<optional; replaces stored CMD override>"],
"dockerEntrypoint": ["<optional; replaces stored ENTRYPOINT override>"]
"image": "myrepo/myservice",
"tag": "v2",
"userData": "<optional ECIES-encrypted hex; the new container env — omitted ⇒ none>",
"dockerCmd": ["<optional; the new CMD override — omitted ⇒ none>"],
"dockerEntrypoint": ["<optional; the new ENTRYPOINT override — omitted ⇒ none>"]
}
```

| name | type | required | description |
| --- | --- | --- | --- |
| serviceId | string | v | the service to restart |
| image | string | RESPEC | base image name (build label when `dockerfile` is set). Required as soon as any container param is present |
| tag | string | | pull by `name:tag`; mutually exclusive with `checksum`/`dockerfile` |
| checksum | string | | pull by digest `sha256:<64 hex>`; mutually exclusive with `tag`/`dockerfile` |
| dockerfile | string | | build from an inline Dockerfile; requires `allowImageBuild` on the environment; mutually exclusive with `tag`/`checksum` |
| additionalDockerFiles | object | | extra `filename → content` files for the build context (only with `dockerfile`) |
| userData | string | | ECIES-encrypted (to the node public key) JSON → the container's env-var map |
| dockerCmd | string[] | | exact container command (Docker exec-form CMD override) |
| dockerEntrypoint | string[] | | container ENTRYPOINT override |

#### Response (200)

The `ServiceJob` with a new `containerId` (same `hostPort` and `expiresAt`).
The `ServiceJob` with a new `containerId` (same `hostPort` and `expiresAt`; the `image`/`tag`/
`checksum`/`dockerfile`/`containerImage` fields reflect the new spec in RESPEC mode).

#### Response (400)

Not found, expired, payment never claimed, or an invalid respec — a container param was sent
without `image`, or more than one of `tag`/`checksum`/`dockerfile` was provided.

#### Response (403)

`dockerfile` was supplied but the environment has `allowImageBuild=false`.

---

Expand Down
4 changes: 2 additions & 2 deletions docs/Ocean Node.postman_collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -1065,7 +1065,7 @@
],
"body": {
"mode": "raw",
"raw": "{\n \"consumerAddress\": \"{{consumerAddress}}\",\n \"nonce\": \"{{nonce}}\",\n \"signature\": \"{{signature}}\",\n \"serviceId\": \"{{serviceId}}\",\n \"userData\": \"\"\n}"
"raw": "{\n \"consumerAddress\": \"{{consumerAddress}}\",\n \"nonce\": \"{{nonce}}\",\n \"signature\": \"{{signature}}\",\n \"serviceId\": \"{{serviceId}}\"\n}"
},
"url": {
"raw": "{{baseUrl}}/api/services/serviceRestart",
Expand All @@ -1078,7 +1078,7 @@
"serviceRestart"
]
},
"description": "Recreate the service container (no extra charge), keeping the same hostPort and expiresAt. Optional userData replaces stored env vars."
"description": "Recreate the service container (no extra charge), keeping the same hostPort, resources and expiresAt. Restart is ATOMIC — all-old or all-new. REUSE mode (body above): send no container params to restart on the stored spec. RESPEC mode: send ANY container param and the container is rebuilt entirely from the request — `image` becomes required plus at most one of tag/checksum/dockerfile (validated like serviceStart), and userData/dockerCmd/dockerEntrypoint are taken as-sent (omitted means empty, NOT inherited). Bug-fix flow example: add \\\"image\\\":\\\"myrepo/myservice\\\",\\\"tag\\\":\\\"v2\\\" to the body. A container param without `image` returns 400; a dockerfile respec where allowImageBuild=false returns 403."
}
},
{
Expand Down
20 changes: 19 additions & 1 deletion docs/services.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ and `signature` as query parameters (or an auth-token `Authorization` header).
| `SERVICE_GET_STATUS` | `/api/services/serviceStatus` | GET | Read job status / endpoints — authenticated, owner-scoped (see notice below); poll this to follow a starting service |
| `SERVICE_LIST` | `/api/services/serviceList` | GET | Node-wide service listing — authenticated, **not** owner-scoped. Default: only services currently holding a resource reservation; `status=<n>` filters to one specific status, `includeAllStatuses=true` returns everything, `fromTimestamp` keeps services created at/after that moment. Output is listing-sanitized (no `userData`, no `dockerCmd`/`dockerEntrypoint`, no Dockerfile) |
| `SERVICE_EXTEND` | `/api/services/serviceExtend` | POST | Pay to push the expiry further out |
| `SERVICE_RESTART` | `/api/services/serviceRestart` | POST | Recreate the container (no extra charge); asynchronous like start — returns once the job is `Restarting`, poll `serviceStatus` |
| `SERVICE_RESTART` | `/api/services/serviceRestart` | POST | Recreate the container (no extra charge); asynchronous like start — returns once the job is `Restarting`, poll `serviceStatus`. Optionally restart on a **new image spec** (bug-fix flow) — see below |
| `SERVICE_STOP` | `/api/services/serviceStop` | POST | Tear down the container; the paid resource reservation (cpu/ram/gpu + host ports) is kept until `expiresAt`, so the service can be restarted anytime on the same endpoints |
| `SERVICE_GET_TEMPLATES` | `/api/services/serviceTemplates` | GET | List operator-published service templates |
| `SERVICE_GET_STREAMABLE_LOGS` | `/api/services/serviceStreamableLogs` | GET | Stream the container's live stdout/stderr logs — authenticated, owner-scoped; available while `Running` or `Error`; optional `since` to skip history |
Expand Down Expand Up @@ -71,6 +71,24 @@ lock failed outright (e.g. insufficient funds) or was refunded before being clai
cannot be restarted: it was never paid for, so restarting it would run the service for
free. Start a new service instead.

**Restart can change the image — atomically.** A restart is either *all-old* or *all-new*;
it never mixes new request params over the stored job:

- **REUSE mode** — send no container params and the service restarts on exactly its stored
spec (image, `userData`, `dockerCmd`, `dockerEntrypoint`).
- **RESPEC mode** — send *any* container param and the container is rebuilt entirely from the
request. `image` is then **required** and exactly one of `tag`/`checksum`/`dockerfile` applies
(validated like `serviceStart`); `userData`/`dockerCmd`/`dockerEntrypoint` are taken as-sent,
so anything omitted is empty — never inherited from the old job.

This is the fix-and-redeploy flow: you started a service on your own image, found a bug, pushed
a corrected image under a new tag, and now restart on it — same `image`, new `tag` — without
losing the paid window, resources or host ports. Requiring `image` whenever any container param
is present is what makes a partial change impossible: a lone new `dockerCmd`/`userData` (which
would silently run on top of the old image) is rejected with `400`, and a `dockerfile` respec on
an environment with `allowImageBuild=false` is rejected with `403`. Payment, resources and
duration are always preserved — only the container spec changes.

**The reservation lasts the whole paid window — only `Expired` releases it.** The consumer
paid for the resources for a time interval and may use them as they please within it:
running the service, stopping it, restarting it. An explicit `SERVICE_STOP` therefore tears
Expand Down
26 changes: 23 additions & 3 deletions src/@types/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -469,14 +469,34 @@ export interface GetServicesCommand extends Command {
updatedSince?: string
}

// Restart is ATOMIC — all-old or all-new, never a mix of new params over the stored job:
//
// • REUSE mode — the request carries NONE of the container params below. The service
// restarts on exactly its stored spec (image, userData, dockerCmd, dockerEntrypoint).
// • RESPEC mode — the request carries ANY container param. The container is rebuilt
// entirely from the request: `image` becomes REQUIRED and exactly one of
// tag/checksum/dockerfile applies (validated like SERVICE_START). userData/dockerCmd/
// dockerEntrypoint are taken as-sent; any omitted here is empty/unset — it is NOT
// pulled from the stored job. This is what makes mixing impossible: a request that
// changes only userData still has to re-supply the full image spec.
//
// Requiring `image` whenever any container param is present is the discriminator between
// the two modes, so there is no way to ride a new userData/cmd on top of the old image.
export interface ServiceRestartCommand extends Command {
consumerAddress: string
nonce: string
signature: string
serviceId: string
userData?: string // optional ECIES-encrypted userData. If provided it REPLACES the stored userData (send the complete set). If omitted, the stored userData is reused — no re-supply needed.
dockerCmd?: string[] // optional. If provided (even []) it REPLACES the stored CMD override. If omitted, the stored dockerCmd is reused.
dockerEntrypoint?: string[] // optional. If provided (even []) it REPLACES the stored ENTRYPOINT override. If omitted, the stored dockerEntrypoint is reused.
// Image spec (RESPEC mode). `image` is required as soon as any container param is sent;
// provide at most one of tag/checksum/dockerfile.
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<string, string> // extra files in the build context
userData?: string // ECIES-encrypted (to the node's public key) JSON → container env-var map
dockerCmd?: string[] // exact container command (Docker exec-form CMD override; no shell)
dockerEntrypoint?: string[] // container ENTRYPOINT override
}

export interface ServiceExtendCommand extends Command {
Expand Down
7 changes: 7 additions & 0 deletions src/components/c2d/compute_engine_base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,17 @@ export abstract class C2DEngine {
return await fn()
}

// Restart is atomic: when an image spec is supplied (RESPEC mode) the whole container is
// rebuilt from these params; when it is absent (REUSE mode) the stored spec is reused.
// eslint-disable-next-line @typescript-eslint/no-unused-vars, require-await
public async restartService(
serviceId: string,
owner: string,
newImage?: string,
newTag?: string,
newChecksum?: string,
newDockerfile?: string,
newAdditionalDockerFiles?: Record<string, string>,
newUserData?: string,
newDockerCmd?: string[],
newDockerEntrypoint?: string[]
Expand Down
Loading
Loading