Skip to content

Commit 3f6f1f7

Browse files
committed
Interactive fleet Sample
Signed-off-by: Luke Roy <luke.roy@ibm.com>
1 parent 215b721 commit 3f6f1f7

5 files changed

Lines changed: 1269 additions & 0 deletions

File tree

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
# interactive-fleets
2+
3+
A general-purpose **submit-and-poll** pattern for IBM Code Engine Fleets.
4+
A Code Engine **Function** acts as a synchronous HTTP endpoint. It accepts any JSON data,
5+
dispatches it as a fleet task, polls COS for the result, and returns the enriched output —
6+
or a retrieval URL if the task is still running when the poll window closes.
7+
8+
This sample ships a simple **data enrichment** worker (adds timestamp, hostname,
9+
word/character counts, and reversed key list) as the fleet task. Swap out the
10+
enrichment logic in [`task-container/entrypoint.sh`](task-container/entrypoint.sh)
11+
with any workload: ML inference, document processing, data transformation, etc.
12+
13+
---
14+
15+
## Architecture
16+
17+
```mermaid
18+
flowchart TD
19+
CallerPost["Caller — POST /\nJSON body with arbitrary data"]
20+
CallerGet["Caller — GET /\n?key=value&... query params"]
21+
CallerResult["Caller — GET /result/{task_id}\nRetrieve previously submitted result"]
22+
23+
Fn["Code Engine Function · function/__main__.py\n─────────────────────────────────────\nGenerates task_id (UUID)\nSerialises input dict as JSON string\nObtains IAM token via CE_API_KEY → submits task\n add_tasks: {idx, args: ['<json-string>']}\nPolls COS for {COS_PREFIX}/{task_id}.json\n via IAM Bearer token up to POLL_TIMEOUT s\nHTTP 200 — result returned synchronously\nHTTP 202 — timeout; returns task_id + retrieve_url\nGET /result/{task_id} → direct COS lookup (IAM)"]
24+
25+
subgraph Fleet["Code Engine Fleet · cx3-4x8 (4 vCPU · 8 GiB)"]
26+
Task["Task Container · task-container/entrypoint.sh\n─────────────────────────────────────\n1 vCPU / 2 GiB\nReceives input as JSON string in $1\nEnriches payload:\n + timestamp, processing_node (hostname)\n + word_count, char_count\n + reversed_keys, summary\nWrites result to /data/results/interactive-fleet-result/{task_id}.json"]
27+
end
28+
29+
COS["COS Persistent Data Store · fleet-output-store\ninteractive-fleet-result/{task_id}.json\n{task_id, status, elapsed_seconds,\n enriched_at, processing_node,\n input: {...}, output: {...}}"]
30+
31+
CallerPost -->|"HTTP POST — submit + poll"| Fn
32+
CallerGet -->|"HTTP GET — submit + poll"| Fn
33+
CallerResult-->|"HTTP GET — retrieve only"| Fn
34+
Fn -->|"CE v2 REST API POST .../add_tasks"| Fleet
35+
Fn -.->|"S3 REST API GET /{bucket}/interactive-fleet-result/{task_id}.json\n(IAM Bearer — polls up to POLL_TIMEOUT seconds)"| COS
36+
Task -->|"COS mount /data/results/interactive-fleet-result/{task_id}.json"| COS
37+
```
38+
39+
---
40+
41+
## Files
42+
43+
| File | Purpose |
44+
|------|---------|
45+
| [`deploy`](deploy) | Single deploy script — builds image, creates fleet, creates secret, deploys function |
46+
| [`task-container/Dockerfile`](task-container/Dockerfile) | Fleet task image — ubi-minimal + bash + jq |
47+
| [`task-container/entrypoint.sh`](task-container/entrypoint.sh) | Enrichment worker — receives argv key-value pairs, writes `/data/results/interactive-fleet-result/{task_id}.json` |
48+
| [`function/__main__.py`](function/__main__.py) | CE Function — routes requests, submits tasks, polls COS, returns results |
49+
50+
---
51+
52+
## Prerequisites
53+
54+
- IBM Cloud CLI with Code Engine plugin
55+
```bash
56+
ibmcloud plugin install code-engine
57+
```
58+
- A Code Engine project selected
59+
```bash
60+
ibmcloud ce project select --name <project>
61+
```
62+
- Persistent data stores provisioned (e.g. via `init-fleet-sandbox`):
63+
```bash
64+
ibmcloud ce pds create --name fleet-task-store ...
65+
ibmcloud ce pds create --name fleet-output-store ...
66+
```
67+
- Subnetpool `fleet-subnetpool` provisioned
68+
- Container registry secret `ce-auto-icr-private-eu-de` (or set `FLEET_REGISTRY_SECRET`)
69+
- Your IBM Container Registry namespace:
70+
```bash
71+
export ICR_NAMESPACE=<your-icr-namespace>
72+
```
73+
- An IBM Cloud API key with **Code Engine Writer** and **COS Reader** access:
74+
```bash
75+
export CE_API_KEY=<your-api-key>
76+
```
77+
78+
---
79+
80+
## Deployment
81+
82+
```bash
83+
export ICR_NAMESPACE=<your-namespace>
84+
export CE_API_KEY=<your-api-key>
85+
86+
./deploy
87+
```
88+
89+
The script prints the function endpoint at the end. Copy it for the examples below.
90+
91+
```bash
92+
export FN_URL=https://<function-name>.<subdomain>.<region>.codeengine.appdomain.cloud
93+
```
94+
95+
### Deploy options
96+
97+
| Variable | Default | Description |
98+
|---|---|---|
99+
| `SKIP_BUILD=1` | — | Skip image build (reuse existing image) |
100+
| `SKIP_FLEET=1` | — | Skip fleet creation; `FLEET_ID` must be set |
101+
| `SKIP_SECRET=1` | — | Skip secret creation/update |
102+
| `FLEET_MAX_SCALE` | `2` | Maximum fleet worker nodes |
103+
| `POLL_TIMEOUT` | `30` | Seconds the function polls COS before returning 202 |
104+
105+
---
106+
107+
## Usage
108+
109+
### 1. Submit a task and wait for the result (synchronous)
110+
111+
The function submits the task to the fleet, polls COS for up to `POLL_TIMEOUT` seconds,
112+
and returns the enriched result directly when it is ready.
113+
114+
**POST with a JSON body:**
115+
116+
```bash
117+
curl -s -X POST "${FN_URL}" \
118+
-H "Content-Type: application/json" \
119+
-d '{
120+
"name": "Alice",
121+
"city": "Berlin",
122+
"message": "Hello from interactive-fleets"
123+
}' | jq .
124+
```
125+
126+
**GET with query parameters:**
127+
128+
```bash
129+
curl -s "${FN_URL}?name=Alice&city=Berlin&message=Hello+from+interactive-fleets" | jq .
130+
```
131+
132+
**HTTP 200 — result available within poll window:**
133+
134+
```json
135+
{
136+
"task_id": "a3f5b8c2-4d1e-4b5a-9c3f-1e2d3c4b5a6f",
137+
"status": "completed",
138+
"result": {
139+
"task_id": "a3f5b8c2-4d1e-4b5a-9c3f-1e2d3c4b5a6f",
140+
"status": "completed",
141+
"elapsed_seconds": 0.0021,
142+
"enriched_at": "2025-01-15T12:00:00.123456Z",
143+
"processing_node": "worker-abc123",
144+
"input": {
145+
"name": "Alice",
146+
"city": "Berlin",
147+
"message": "Hello from interactive-fleets"
148+
},
149+
"output": {
150+
"word_count": 5,
151+
"char_count": 29,
152+
"reversed_keys": ["message", "city", "name"],
153+
"summary": "Enriched 3 field(s) from input."
154+
}
155+
}
156+
}
157+
```
158+
159+
---
160+
161+
### 2. Submit a task asynchronously (fire and forget)
162+
163+
Set `poll_timeout=0` to skip polling entirely and get the retrieval URL immediately.
164+
165+
```bash
166+
curl -s -X POST "${FN_URL}" \
167+
-H "Content-Type: application/json" \
168+
-d '{
169+
"name": "Bob",
170+
"city": "London",
171+
"poll_timeout": 0
172+
}' | jq .
173+
```
174+
175+
**HTTP 202 — task accepted, result not yet available:**
176+
177+
```json
178+
{
179+
"message": "Task submitted and is being processed",
180+
"task_id": "b4e6c9d2-5e2f-4c6b-8d4e-2f3e4d5c6b7e",
181+
"status": "pending",
182+
"fleet_id": "1a2b3c4d-...",
183+
"poll_timeout_seconds": 0,
184+
"retrieve_url": "https://<fn-url>/result/b4e6c9d2-5e2f-4c6b-8d4e-2f3e4d5c6b7e"
185+
}
186+
```
187+
188+
---
189+
190+
### 3. Retrieve a result by task ID
191+
192+
Use the `retrieve_url` from the 202 response, or construct it manually:
193+
194+
```bash
195+
curl -s "${FN_URL}/result/b4e6c9d2-5e2f-4c6b-8d4e-2f3e4d5c6b7e" | jq .
196+
```
197+
198+
**HTTP 200** — result is ready:
199+
200+
```json
201+
{
202+
"task_id": "b4e6c9d2-...",
203+
"status": "completed",
204+
"elapsed_seconds": 0.0018,
205+
"enriched_at": "2025-01-15T12:01:15.000000Z",
206+
"processing_node": "worker-def456",
207+
"input": { "name": "Bob", "city": "London" },
208+
"output": {
209+
"word_count": 2,
210+
"char_count": 10,
211+
"reversed_keys": ["city", "name"],
212+
"summary": "Enriched 2 field(s) from input."
213+
}
214+
}
215+
```
216+
217+
**HTTP 404** — task is still running or ID is invalid:
218+
219+
```json
220+
{
221+
"task_id": "b4e6c9d2-...",
222+
"status": "pending_or_not_found",
223+
"retrieve_url": "https://<fn-url>/result/b4e6c9d2-..."
224+
}
225+
```
226+
227+
---
228+
229+
### 4. Provide a custom task ID
230+
231+
```bash
232+
curl -s -X POST "${FN_URL}" \
233+
-H "Content-Type: application/json" \
234+
-d '{
235+
"task_id": "my-custom-task-001",
236+
"product": "widget",
237+
"quantity": "42"
238+
}' | jq .
239+
```
240+
241+
The result will be stored at `interactive-fleet-result/my-custom-task-001.json` in the COS bucket.
242+
243+
---
244+
245+
## Result JSON schema
246+
247+
The fleet task writes the following structure to `interactive-fleet-result/{task_id}.json`:
248+
249+
```json
250+
{
251+
"task_id": "<uuid or custom id>",
252+
"status": "completed",
253+
"elapsed_seconds": 0.0021,
254+
"enriched_at": "ISO-8601 UTC timestamp",
255+
"processing_node": "hostname of the fleet worker",
256+
"input": {
257+
"...": "all key/value pairs from the original request (control keys stripped)"
258+
},
259+
"output": {
260+
"word_count": 42,
261+
"char_count": 210,
262+
"reversed_keys": ["key3", "key2", "key1"],
263+
"summary": "Enriched N field(s) from input."
264+
}
265+
}
266+
```
267+
268+
---
269+
270+
## Configuration reference
271+
272+
### Function environment variables
273+
274+
| Variable | Default | Description |
275+
|---|---|---|
276+
| `FLEET_ID` | **required** | Target fleet UUID or name |
277+
| `CE_API_KEY` | **required** (from `interactive-fleet-secrets`) | IBM Cloud API key — IAM token used for CE Fleet API **and** COS access |
278+
| `COS_BUCKET` | `fleet-output-store` | COS bucket where results are stored |
279+
| `COS_PREFIX` | `interactive-fleet-result` | Folder prefix inside the COS bucket |
280+
| `COS_ENDPOINT` | `https://s3.direct.eu-de.cloud-object-storage.appdomain.cloud` | COS S3 endpoint |
281+
| `POLL_TIMEOUT` | `30` | Max seconds to poll COS before returning HTTP 202 |
282+
| `POLL_INTERVAL` | `2` | Seconds between COS poll attempts |
283+
284+
### Task container environment variables
285+
286+
| Variable | Default | Description |
287+
|---|---|---|
288+
| `RESULTS_DIR` | `/data/results` | Fleet COS mount root; script writes `interactive-fleet-result/<task_id>.json` inside it |
289+
290+
### Deploy script environment variables
291+
292+
| Variable | Default | Description |
293+
|---|---|---|
294+
| `ICR_NAMESPACE` | **required** | Your IBM Container Registry namespace |
295+
| `CE_API_KEY` | **required** | IBM Cloud API key (Code Engine Writer + COS Reader — fleet API and COS) |
296+
| `NAME_PREFIX` | `interactive-fleets` | Prefix for all created resource names |
297+
| `FLEET_IMAGE` | `private.de.icr.io/<ICR_NAMESPACE>/interactive-fleets-task:latest` | Task image |
298+
| `FLEET_REGISTRY_SECRET` | `ce-auto-icr-private-eu-de` | Registry pull secret |
299+
| `FLEET_MAX_SCALE` | `2` | Maximum worker nodes |
300+
| `FLEET_SCALE_DOWN_DELAY` | `300` | Idle seconds before scaling down (5 min) |
301+
| `FLEET_CPU` | `1` | vCPUs per task container |
302+
| `FLEET_MEMORY` | `2G` | Memory per task container |
303+
| `FN_NAME` | `interactive-fleet-fn` | CE Function name |
304+
| `SECRET_NAME` | `interactive-fleet-secrets` | Secret holding `CE_API_KEY` |
305+
| `COS_PREFIX` | `interactive-fleet-result` | Folder prefix in COS bucket for results |
306+
| `POLL_TIMEOUT` | `30` | Max poll seconds (function env var) |
307+
308+
---
309+
310+
## Customising the workload
311+
312+
The enrichment logic lives entirely in [`task-container/entrypoint.sh`](task-container/entrypoint.sh)
313+
in the enrichment section (steps 3–6). Replace it with any processing you need:
314+
315+
- ML model inference
316+
- External API calls
317+
- File or document transformation
318+
- Database writes
319+
320+
The contract the function and worker share:
321+
1. The function submits one task per request with:
322+
```json
323+
{ "idx": "<task_id>", "args": ["<json-string-of-input>"] }
324+
```
325+
2. The worker receives the JSON string as `$1` (or `sys.argv[1]` for Python) and reads it directly — no key-value parsing needed.
326+
3. The worker writes the result JSON to `{RESULTS_DIR}/interactive-fleet-result/{task_id}.json`.
327+
328+
After changing the worker, rebuild and redeploy with:
329+
330+
```bash
331+
./deploy
332+
# or, to skip fleet re-creation:
333+
SKIP_FLEET=1 FLEET_ID=<existing-id> ./deploy
334+
```

0 commit comments

Comments
 (0)