Skip to content

Commit fd255ed

Browse files
test worker for certification
1 parent a751eba commit fd255ed

12 files changed

Lines changed: 825 additions & 0 deletions

File tree

Dockerfile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,19 @@ ENV CONDUCTOR_AUTH_SECRET ${CONDUCTOR_AUTH_SECRET}
4141
ENV CONDUCTOR_SERVER_URL ${CONDUCTOR_SERVER_URL}
4242
RUN python3 ./tests/integration/main.py
4343

44+
FROM python_test_base AS harness-build
45+
COPY /harness /package/harness
46+
47+
FROM python:3.12-alpine AS harness
48+
RUN adduser -D -u 65532 nonroot
49+
WORKDIR /app
50+
COPY --from=harness-build /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
51+
COPY --from=harness-build /package/src /app/src
52+
COPY --from=harness-build /package/harness /app/harness
53+
ENV PYTHONPATH=/app/src
54+
USER nonroot
55+
ENTRYPOINT ["python", "-u", "harness/main.py"]
56+
4457
FROM python:3.12-alpine AS publish
4558
RUN apk add --no-cache tk curl
4659
WORKDIR /package

harness/README.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Python SDK Docker Harness
2+
3+
Two Docker targets built from the root `Dockerfile`: an **SDK build** and a **long-running worker harness**.
4+
5+
## Worker Harness
6+
7+
A self-feeding worker that runs indefinitely. On startup it registers five simulated tasks (`python_worker_0` through `python_worker_4`) and the `python_simulated_tasks_workflow`, then runs two background services:
8+
9+
- **WorkflowGovernor** -- starts a configurable number of `python_simulated_tasks_workflow` instances per second (default 2), indefinitely.
10+
- **SimulatedTaskWorkers** -- five task handlers, each with a codename and a default sleep duration. Each worker supports configurable delay types, failure simulation, and output generation via task input parameters. The workflow chains them in sequence: quickpulse (1s) → whisperlink (2s) → shadowfetch (3s) → ironforge (4s) → deepcrawl (5s).
11+
12+
### Building Locally
13+
14+
```bash
15+
docker build --target harness -t python-sdk-harness .
16+
```
17+
18+
### Multiplatform Build and Push
19+
20+
To build for both `linux/amd64` and `linux/arm64` and push to GHCR:
21+
22+
```bash
23+
# One-time: create a buildx builder if you don't have one
24+
docker buildx create --name multiarch --use --bootstrap
25+
26+
# Build and push
27+
docker buildx build \
28+
--platform linux/amd64,linux/arm64 \
29+
--target harness \
30+
-t ghcr.io/conductor-oss/python-sdk/harness-worker:latest \
31+
--push .
32+
```
33+
34+
> **Note:** Multi-platform builds require `docker buildx` and a builder that supports cross-compilation. On macOS this works out of the box with Docker Desktop. On Linux you may need to install QEMU user-space emulators:
35+
>
36+
> ```bash
37+
> docker run --privileged --rm tonistiigi/binfmt --install all
38+
> ```
39+
40+
### Running
41+
42+
```bash
43+
docker run -d \
44+
-e CONDUCTOR_SERVER_URL=https://your-cluster.example.com/api \
45+
-e CONDUCTOR_AUTH_KEY=$CONDUCTOR_AUTH_KEY \
46+
-e CONDUCTOR_AUTH_SECRET=$CONDUCTOR_AUTH_SECRET \
47+
-e HARNESS_WORKFLOWS_PER_SEC=4 \
48+
python-sdk-harness
49+
```
50+
51+
You can also run the harness locally without Docker (from the repo root):
52+
53+
```bash
54+
# Install the SDK in development mode (one-time)
55+
pip3 install -e .
56+
57+
export CONDUCTOR_SERVER_URL=https://your-cluster.example.com/api
58+
export CONDUCTOR_AUTH_KEY=$CONDUCTOR_AUTH_KEY
59+
export CONDUCTOR_AUTH_SECRET=$CONDUCTOR_AUTH_SECRET
60+
61+
python3 harness/main.py
62+
```
63+
64+
Override defaults with environment variables as needed:
65+
66+
```bash
67+
HARNESS_WORKFLOWS_PER_SEC=4 HARNESS_BATCH_SIZE=10 python3 harness/main.py
68+
```
69+
70+
All resource names use a `python_` prefix so multiple SDK harnesses (C#, JS, Go, Java, etc.) can coexist on the same cluster.
71+
72+
### Environment Variables
73+
74+
| Variable | Required | Default | Description |
75+
|---|---|---|---|
76+
| `CONDUCTOR_SERVER_URL` | yes | -- | Conductor API base URL |
77+
| `CONDUCTOR_AUTH_KEY` | no | -- | Orkes auth key |
78+
| `CONDUCTOR_AUTH_SECRET` | no | -- | Orkes auth secret |
79+
| `HARNESS_WORKFLOWS_PER_SEC` | no | 2 | Workflows to start per second |
80+
| `HARNESS_BATCH_SIZE` | no | 20 | Number of tasks each worker polls per batch |
81+
| `HARNESS_POLL_INTERVAL_MS` | no | 100 | Milliseconds between poll cycles |

harness/main.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
from __future__ import annotations
2+
3+
import os
4+
import signal
5+
import sys
6+
7+
from conductor.client.automator.task_handler import TaskHandler
8+
from conductor.client.configuration.configuration import Configuration
9+
from conductor.client.http.models.task_def import TaskDef
10+
from conductor.client.orkes_clients import OrkesClients
11+
from conductor.client.workflow.conductor_workflow import ConductorWorkflow
12+
from conductor.client.workflow.task.simple_task import SimpleTask
13+
14+
from simulated_task_worker import SimulatedTaskWorker
15+
from workflow_governor import WorkflowGovernor
16+
17+
WORKFLOW_NAME = "python_simulated_tasks_workflow"
18+
19+
SIMULATED_WORKERS = [
20+
("python_worker_0", "quickpulse", 1),
21+
("python_worker_1", "whisperlink", 2),
22+
("python_worker_2", "shadowfetch", 3),
23+
("python_worker_3", "ironforge", 4),
24+
("python_worker_4", "deepcrawl", 5),
25+
]
26+
27+
28+
def env_int_or_default(key: str, default: int) -> int:
29+
s = os.environ.get(key, "")
30+
if not s:
31+
return default
32+
try:
33+
return int(s)
34+
except ValueError:
35+
return default
36+
37+
38+
def register_metadata(clients: OrkesClients) -> None:
39+
metadata_client = clients.get_metadata_client()
40+
workflow_executor = clients.get_workflow_executor()
41+
42+
for task_name, codename, sleep_seconds in SIMULATED_WORKERS:
43+
task_def = TaskDef(
44+
name=task_name,
45+
description=f"Python SDK harness simulated task ({codename}, default delay {sleep_seconds}s)",
46+
retry_count=1,
47+
timeout_seconds=300,
48+
response_timeout_seconds=300,
49+
)
50+
metadata_client.register_task_def(task_def)
51+
52+
wf = ConductorWorkflow(
53+
executor=workflow_executor,
54+
name=WORKFLOW_NAME,
55+
version=1,
56+
description="Python SDK harness simulated task workflow",
57+
)
58+
wf.owner_email("python-sdk-harness@conductor.io")
59+
60+
for task_name, codename, _ in SIMULATED_WORKERS:
61+
wf.add(SimpleTask(task_def_name=task_name, task_reference_name=codename))
62+
63+
wf.register(overwrite=True)
64+
print(f"Registered workflow {WORKFLOW_NAME} with {len(SIMULATED_WORKERS)} tasks")
65+
66+
67+
def main() -> None:
68+
configuration = Configuration()
69+
clients = OrkesClients(configuration)
70+
71+
register_metadata(clients)
72+
73+
workflows_per_sec = env_int_or_default("HARNESS_WORKFLOWS_PER_SEC", 2)
74+
batch_size = env_int_or_default("HARNESS_BATCH_SIZE", 20)
75+
poll_interval_ms = env_int_or_default("HARNESS_POLL_INTERVAL_MS", 100)
76+
77+
workers = []
78+
for task_name, codename, sleep_seconds in SIMULATED_WORKERS:
79+
worker = SimulatedTaskWorker(task_name, codename, sleep_seconds, batch_size, poll_interval_ms)
80+
workers.append(worker)
81+
82+
task_handler = TaskHandler(
83+
workers=workers,
84+
configuration=configuration,
85+
scan_for_annotated_workers=False,
86+
)
87+
88+
workflow_executor = clients.get_workflow_executor()
89+
governor = WorkflowGovernor(workflow_executor, WORKFLOW_NAME, workflows_per_sec)
90+
governor.start()
91+
92+
main_pid = os.getpid()
93+
shutting_down = False
94+
95+
def shutdown(signum, frame):
96+
nonlocal shutting_down
97+
if os.getpid() != main_pid or shutting_down:
98+
return
99+
shutting_down = True
100+
print("Shutting down...")
101+
governor.stop()
102+
task_handler.stop_processes()
103+
sys.exit(0)
104+
105+
signal.signal(signal.SIGINT, shutdown)
106+
signal.signal(signal.SIGTERM, shutdown)
107+
108+
task_handler.start_processes()
109+
task_handler.join_processes()
110+
111+
112+
if __name__ == "__main__":
113+
main()

harness/manifests/README.md

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# Kubernetes Manifests
2+
3+
This directory contains Kubernetes manifests for deploying the Python SDK harness worker to the certification clusters.
4+
5+
## Prerequisites
6+
7+
**Set your namespace environment variable:**
8+
```bash
9+
export NS=your-namespace-here
10+
```
11+
12+
All kubectl commands below use `-n $NS` to specify the namespace. The manifests intentionally do not include hardcoded namespaces.
13+
14+
**Note:** The harness worker images are published as public packages on GHCR and do not require authentication to pull. No image pull secrets are needed.
15+
16+
## Files
17+
18+
| File | Description |
19+
|---|---|
20+
| `deployment.yaml` | Deployment (single file, works on all clusters) |
21+
| `configmap-aws.yaml` | Conductor URL + auth key for certification-aws |
22+
| `configmap-azure.yaml` | Conductor URL + auth key for certification-az |
23+
| `configmap-gcp.yaml` | Conductor URL + auth key for certification-gcp |
24+
| `secret-conductor.yaml` | Conductor auth secret (placeholder template) |
25+
26+
## Quick Start
27+
28+
### 1. Create the Conductor Auth Secret
29+
30+
The `CONDUCTOR_AUTH_SECRET` must be created as a Kubernetes secret before deploying.
31+
32+
```bash
33+
kubectl create secret generic conductor-credentials \
34+
--from-literal=auth-secret=YOUR_AUTH_SECRET \
35+
-n $NS
36+
```
37+
38+
If the `conductor-credentials` secret already exists in the namespace (e.g. from the e2e-testrunner-worker), it can be reused as-is.
39+
40+
See `secret-conductor.yaml` for more details.
41+
42+
### 2. Apply the ConfigMap for Your Cluster
43+
44+
```bash
45+
# AWS
46+
kubectl apply -f manifests/configmap-aws.yaml -n $NS
47+
48+
# Azure
49+
kubectl apply -f manifests/configmap-azure.yaml -n $NS
50+
51+
# GCP
52+
kubectl apply -f manifests/configmap-gcp.yaml -n $NS
53+
```
54+
55+
### 3. Deploy
56+
57+
```bash
58+
kubectl apply -f manifests/deployment.yaml -n $NS
59+
```
60+
61+
### 4. Verify
62+
63+
```bash
64+
# Check pod status
65+
kubectl get pods -n $NS -l app=python-sdk-harness-worker
66+
67+
# Watch logs
68+
kubectl logs -n $NS -l app=python-sdk-harness-worker -f
69+
```
70+
71+
## Building and Pushing the Image
72+
73+
From the repository root:
74+
75+
```bash
76+
# Build the harness target and push to GHCR
77+
docker buildx build \
78+
--platform linux/amd64,linux/arm64 \
79+
--target harness \
80+
-t ghcr.io/conductor-oss/python-sdk/harness-worker:latest \
81+
--push .
82+
```
83+
84+
After pushing a new image with the same tag, restart the deployment to pull it:
85+
86+
```bash
87+
kubectl rollout restart deployment/python-sdk-harness-worker -n $NS
88+
kubectl rollout status deployment/python-sdk-harness-worker -n $NS
89+
```
90+
91+
## Tuning
92+
93+
The harness worker accepts these optional environment variables (set in `deployment.yaml`):
94+
95+
| Variable | Default | Description |
96+
|---|---|---|
97+
| `HARNESS_WORKFLOWS_PER_SEC` | 2 | Workflows to start per second |
98+
| `HARNESS_BATCH_SIZE` | 20 | Tasks each worker polls per batch |
99+
| `HARNESS_POLL_INTERVAL_MS` | 100 | Milliseconds between poll cycles |
100+
101+
Edit `deployment.yaml` to change these, then re-apply:
102+
103+
```bash
104+
kubectl apply -f manifests/deployment.yaml -n $NS
105+
```
106+
107+
## Troubleshooting
108+
109+
### Pod not starting
110+
111+
```bash
112+
kubectl describe pod -n $NS -l app=python-sdk-harness-worker
113+
kubectl logs -n $NS -l app=python-sdk-harness-worker --tail=100
114+
```
115+
116+
### Secret not found
117+
118+
```bash
119+
kubectl get secret conductor-credentials -n $NS
120+
```
121+
122+
## Resource Limits
123+
124+
Default resource allocation:
125+
- **Memory**: 256Mi (request) / 512Mi (limit)
126+
- **CPU**: 100m (request) / 500m (limit)
127+
128+
Adjust in `deployment.yaml` based on workload. Higher `HARNESS_WORKFLOWS_PER_SEC` values may need more CPU/memory.
129+
130+
## Service
131+
132+
The harness worker does **not** need a Service or Ingress. It connects to Conductor via outbound HTTP polling. All communication is outbound.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
# ConfigMap for certification-aws cluster
3+
# Contains Conductor connection details specific to this cluster
4+
apiVersion: v1
5+
kind: ConfigMap
6+
metadata:
7+
name: python-sdk-harness-config
8+
# namespace: xxxxx # supply this in kubectl command
9+
labels:
10+
app: python-sdk-harness-worker
11+
data:
12+
CONDUCTOR_SERVER_URL: "https://certification-aws.orkesconductor.io/api"
13+
CONDUCTOR_AUTH_KEY: "7ba9d0ec-247b-11f1-8d42-ea3efeda41b2"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
# ConfigMap for certification-az cluster
3+
# Contains Conductor connection details specific to this cluster
4+
apiVersion: v1
5+
kind: ConfigMap
6+
metadata:
7+
name: python-sdk-harness-config
8+
# namespace: xxxxx # supply this in kubectl command
9+
labels:
10+
app: python-sdk-harness-worker
11+
data:
12+
CONDUCTOR_SERVER_URL: "https://certification-az.orkesconductor.io/api"
13+
CONDUCTOR_AUTH_KEY: "bf170d61-2797-11f1-833e-4ae04d100a03"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
# ConfigMap for certification-gcp cluster
3+
# Contains Conductor connection details specific to this cluster
4+
apiVersion: v1
5+
kind: ConfigMap
6+
metadata:
7+
name: python-sdk-harness-config
8+
# namespace: xxxxx # supply this in kubectl command
9+
labels:
10+
app: python-sdk-harness-worker
11+
data:
12+
CONDUCTOR_SERVER_URL: "https://certification-gcp.orkesconductor.com/api"
13+
CONDUCTOR_AUTH_KEY: "e6c1ac61-286b-11f1-be01-c682b5750c3a"

0 commit comments

Comments
 (0)