Skip to content
Open
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
88 changes: 69 additions & 19 deletions serverless-fleets/tutorials/clef-on-fleets/3_deploy_function
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ print_cmd() { echo -e " $1"; }
# ==============================
FN_NAME="${FN_NAME:=clef-classify-fn}"
FLEET_ID="${1:-${FLEET_ID:-}}"
SECRET_NAME="${SECRET_NAME:=clef-fn-secrets}"
CE_API_KEY="${CE_API_KEY:=}"
TRUSTED_PROFILE_NAME="${TRUSTED_PROFILE_NAME:=clef-fn-access}"
COS_PREFIX="${COS_PREFIX:=clef-result}"
FILE_PREFIX="${FILE_PREFIX:=result}"
PDS_NAME="${PDS_NAME:=fleet-output-store}"
Expand Down Expand Up @@ -51,23 +50,71 @@ if [[ -z "${FLEET_ID}" ]]; then
exit 1
fi

# Ensure secret exists or create it if CE_API_KEY was supplied in env
if [[ -n "${CE_API_KEY}" ]]; then
print_msg "Creating/updating secret '${SECRET_NAME}'..."
if ibmcloud ce secret get --name "${SECRET_NAME}" >/dev/null 2>&1; then
ibmcloud ce secret update --name "${SECRET_NAME}" --from-literal CE_API_KEY="${CE_API_KEY}"
# ==============================
# TRUSTED PROFILE (replaces the former CE_API_KEY secret)
# The function exchanges its Code Engine compute resource token for an IAM
# token of this profile. The profile is linked to the function by name and is
# granted: Writer on the Code Engine project (add fleet tasks) and
# Object Reader on the results bucket (poll results).
# ==============================
# 'project current' does not return the CRN, 'project get' does.
CE_PROJECT_NAME=$(ibmcloud ce project current --output json | jq -r '.name')
CE_PROJECT_JSON=$(ibmcloud ce project get --name "${CE_PROJECT_NAME}" --output json)
CE_PROJECT_CRN=$(echo "${CE_PROJECT_JSON}" | jq -r '.crn // empty')
CE_PROJECT_GUID=$(echo "${CE_PROJECT_JSON}" | jq -r '.guid // empty')
if [[ -z "${CE_PROJECT_CRN}" || -z "${CE_PROJECT_GUID}" ]]; then
print_error "Could not determine CRN/GUID of Code Engine project '${CE_PROJECT_NAME}'."
exit 1
fi

# COS instance hosting the results bucket: explicit CRN, explicit name, or the
# only COS instance in the targeted resource group.
if [[ -z "${COS_INSTANCE_ID:-}" ]]; then
if [[ -n "${COS_INSTANCE_NAME:-}" ]]; then
COS_INSTANCE_ID=$(ibmcloud resource service-instance "${COS_INSTANCE_NAME}" --output json 2>/dev/null | jq -r '.[0].id // empty' || echo "")
else
ibmcloud ce secret create --name "${SECRET_NAME}" --from-literal CE_API_KEY="${CE_API_KEY}"
fi
else
if ! ibmcloud ce secret get --name "${SECRET_NAME}" >/dev/null 2>&1; then
print_error "Secret '${SECRET_NAME}' does not exist and CE_API_KEY environment variable is not set."
echo "Create the secret with:"
echo " ibmcloud ce secret create --name ${SECRET_NAME} --from-literal CE_API_KEY=<your-ibmcloud-api-key>"
echo "or set export CE_API_KEY=<key> before running."
exit 1
COS_INSTANCES=$(ibmcloud resource service-instances --service-name cloud-object-storage --output json 2>/dev/null || echo "[]")
if [[ "$(echo "${COS_INSTANCES}" | jq 'length')" == "1" ]]; then
COS_INSTANCE_ID=$(echo "${COS_INSTANCES}" | jq -r '.[0].id')
fi
fi
fi
if [[ -z "${COS_INSTANCE_ID:-}" ]]; then
print_error "Could not determine the COS instance of bucket '${COS_BUCKET}'."
echo "Set export COS_INSTANCE_NAME=<name> or COS_INSTANCE_ID=<crn>."
exit 1
fi

print_msg "Ensuring trusted profile '${TRUSTED_PROFILE_NAME}'..."
if ! ibmcloud iam trusted-profile "${TRUSTED_PROFILE_NAME}" >/dev/null 2>&1; then
ibmcloud iam trusted-profile-create "${TRUSTED_PROFILE_NAME}"
fi

if ! ibmcloud iam trusted-profile-links "${TRUSTED_PROFILE_NAME}" --output json 2>/dev/null \
| jq -e --arg n "ce-fn-${FN_NAME}" '.[] | select(.name == $n)' >/dev/null 2>&1; then
ibmcloud iam trusted-profile-link-create "${TRUSTED_PROFILE_NAME}" \
--name "ce-fn-${FN_NAME}" \
--cr-type CE \
--link-crn "${CE_PROJECT_CRN}" \
--link-component-type function \
--link-component-name "${FN_NAME}"
fi

PROFILE_POLICIES=$(ibmcloud iam trusted-profile-policies "${TRUSTED_PROFILE_NAME}" --output json 2>/dev/null || echo "[]")
if ! echo "${PROFILE_POLICIES}" | jq -e --arg s "${CE_PROJECT_GUID}" '.[] | select(.resources[]?.attributes[]? | .value == $s)' >/dev/null 2>&1; then
ibmcloud iam trusted-profile-policy-create "${TRUSTED_PROFILE_NAME}" \
--roles "Writer" \
--service-name codeengine \
--service-instance "${CE_PROJECT_GUID}"
fi
if ! echo "${PROFILE_POLICIES}" | jq -e --arg b "${COS_BUCKET}" '.[] | select(.resources[]?.attributes[]? | .value == $b)' >/dev/null 2>&1; then
ibmcloud iam trusted-profile-policy-create "${TRUSTED_PROFILE_NAME}" \
--roles "Object Reader" \
--service-name cloud-object-storage \
--service-instance "${COS_INSTANCE_ID}" \
--resource-type bucket \
--resource "${COS_BUCKET}"
fi

print_msg "\nDeploying Code Engine function '${FN_NAME}' from ${FUNCTION_DIR} ..."
echo ""
Expand All @@ -82,7 +129,8 @@ echo " --env COS_PREFIX=${COS_PREFIX}"
echo " --env FILE_PREFIX=${FILE_PREFIX}"
echo " --env COS_ENDPOINT=${COS_ENDPOINT}"
echo " --env POLL_TIMEOUT=${POLL_TIMEOUT}"
echo " --env-from-secret ${SECRET_NAME}"
echo " --env TRUSTED_PROFILE_NAME=${TRUSTED_PROFILE_NAME}"
echo " --trusted-profiles-enabled=true"
echo ""

if ibmcloud ce fn get --name "${FN_NAME}" >/dev/null 2>&1; then
Expand All @@ -98,7 +146,8 @@ if ibmcloud ce fn get --name "${FN_NAME}" >/dev/null 2>&1; then
--env FILE_PREFIX="${FILE_PREFIX}" \
--env COS_ENDPOINT="${COS_ENDPOINT}" \
--env POLL_TIMEOUT="${POLL_TIMEOUT}" \
--env-from-secret "${SECRET_NAME}"
--env TRUSTED_PROFILE_NAME="${TRUSTED_PROFILE_NAME}" \
--trusted-profiles-enabled=true
else
ibmcloud ce fn create \
--name "${FN_NAME}" \
Expand All @@ -111,7 +160,8 @@ else
--env FILE_PREFIX="${FILE_PREFIX}" \
--env COS_ENDPOINT="${COS_ENDPOINT}" \
--env POLL_TIMEOUT="${POLL_TIMEOUT}" \
--env-from-secret "${SECRET_NAME}"
--env TRUSTED_PROFILE_NAME="${TRUSTED_PROFILE_NAME}" \
--trusted-profiles-enabled=true
fi

FN_ENDPOINT=$(ibmcloud ce fn get --name "${FN_NAME}" --output json | jq -r '.endpoint // empty' 2>/dev/null || echo "")
Expand Down
25 changes: 18 additions & 7 deletions serverless-fleets/tutorials/clef-on-fleets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ flowchart TD
CallerGet["Caller (Poll/Get) · HTTPS GET\n?task_id=uuid"]
CallerCLI["Caller · ibmcloud ce fleet task add\n--tasks-from-local-file\n{idx: task_id, args: [key, val, ...]}"]

Fn["Code Engine Function · function/__main__.py\n──────────────────────────────────\nGenerates / receives task_id\nValidates + assembles task spec\nGets IAM token via CE_API_KEY\nSubmits task to CE Fleet\nPolls COS (up to poll_timeout e.g. 60s)\nReturns 200 (completed) or 202 (pending with task_id)\nAlso handles GET/POST {task_id} to fetch result"]
Fn["Code Engine Function · function/__main__.py\n──────────────────────────────────\nGenerates / receives task_id\nValidates + assembles task spec\nGets IAM token via trusted profile (CR token)\nSubmits task to CE Fleet\nPolls COS (up to poll_timeout e.g. 60s)\nReturns 200 (completed) or 202 (pending with task_id)\nAlso handles GET/POST {task_id} to fetch result"]

subgraph Fleet["Code Engine Fleet · gx3-16x80x1l4 (16 vCPU · 80 GiB · 1× L4 GPU)"]
Prehook["Worker Prehook (runs once at startup)\n──────────────────────────────────\npodman run ollama (GPU-aware)\nskip if container already exists/stopped\nollama pull clef (skip if present)\nverify in /api/tags via jq\nwarm-up /v1/systemone → VRAM loaded"]
Expand Down Expand Up @@ -69,7 +69,7 @@ flowchart TD
`ibmcloud ce pds create --name fleet-output-store ...`
- Subnetpool `fleet-subnetpool` provisioned
- Container registry secret `ce-auto-icr-private-eu-de` (or set `FLEET_REGISTRY_SECRET`)
- An IBM Cloud API key with Code Engine Writer and COS access (set as `CE_API_KEY` or stored in `clef-fn-secrets`)
- Permission to create IAM trusted profiles and policies (used by `3_deploy_function`; no API key is needed)
- Your IBM Container Registry namespace exported:
`export ICR_NAMESPACE=<your-icr-namespace>`

Expand Down Expand Up @@ -108,15 +108,25 @@ export FLEET_ID=<uuid-printed-by-script>
### 3 — Deploy the Code Engine function

```bash
# Set your API key if the secret doesn't exist yet
export CE_API_KEY="<your-ibmcloud-api-key>"

# Pass FLEET_ID as $1 or set via environment variable:
./3_deploy_function "${FLEET_ID}"
```

*(If `FLEET_ID` is not supplied as `$1` or exported in the environment, `3_deploy_function` attempts to auto-discover the latest clef fleet in the project.)*

The function authenticates with an [IAM trusted profile](../../../trusted-profiles/README.md)
instead of an API key. `3_deploy_function` idempotently:

1. creates the trusted profile `clef-fn-access` (`TRUSTED_PROFILE_NAME`),
2. links it to the function (`--cr-type CE --link-component-type function`),
3. grants it **Writer** on the Code Engine project (to add fleet tasks) and **Object Reader** on the results bucket,
4. deploys the function with `--trusted-profiles-enabled=true` and `TRUSTED_PROFILE_NAME` as env var.

At runtime the function reads the compute resource token mounted by Code Engine and exchanges it
at IAM (`grant_type=urn:ibm:params:oauth:grant-type:cr-token`) for a bearer token used for both
the Code Engine and COS calls. The COS instance is auto-discovered if it is the only one in the
targeted resource group; otherwise set `COS_INSTANCE_NAME` or `COS_INSTANCE_ID` (CRN).

---

### 4 — Test via Code Engine Function
Expand Down Expand Up @@ -278,8 +288,9 @@ Results will be stored in the COS bucket under `clef-result/result-task-001.json
| `FLEET_CPU` | `4` | vCPUs per task container |
| `FLEET_MEMORY` | `5G` | Memory per task container |
| `FN_NAME` | `clef-classify-fn` | Code Engine Function name |
| `SECRET_NAME` | `clef-fn-secrets` | Secret holding `CE_API_KEY` |
| `CE_API_KEY` | `""` | IBM Cloud API key for IAM & COS |
| `TRUSTED_PROFILE_NAME` | `clef-fn-access` | IAM trusted profile used by the function (no API key) |
| `COS_INSTANCE_NAME` | auto-discovered | COS instance hosting the results bucket (for the profile's policy); auto-discovered if it is the only one in the resource group |
| `COS_INSTANCE_ID` | auto-discovered | Optional CRN override of `COS_INSTANCE_NAME` |
| `COS_BUCKET` | `fleet-output-store` | Bucket where task results are stored |
| `COS_PREFIX` | `clef-result` | Folder in COS bucket for results |
| `FILE_PREFIX` | `result` | Filename prefix before `<idx>.json` |
Expand Down
65 changes: 45 additions & 20 deletions serverless-fleets/tutorials/clef-on-fleets/function/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,21 @@
6. Supports direct result retrieval when called with { "task_id": "<id>" } or
via HTTP query parameter / POST body without submit inputs.

Authentication:
The function authenticates with an IAM trusted profile (no API key). Code Engine
mounts a compute resource token into the function when it is created with
--trusted-profiles-enabled; that token is exchanged for an IAM access token.
The trusted profile needs: Code Engine Writer on the project and
Object Reader on the COS results bucket (see 3_deploy_function).

Required environment variables (set on the CE function):
FLEET_ID — UUID or name of the target fleet
CE_API_KEY — IBM Cloud API key used for IAM token exchange (and COS access)
COS_BUCKET — Name of the COS bucket containing results (default: fleet-output-store)
FLEET_ID — UUID or name of the target fleet
TRUSTED_PROFILE_NAME — Name of the IAM trusted profile linked to this function
COS_BUCKET — Name of the COS bucket containing results (default: fleet-output-store)

Optional environment variables:
CR_TOKEN_FILENAME — Path of the compute resource token (default:
/var/run/secrets/codeengine.cloud.ibm.com/compute-resource-token/token)
CE_API_BASE_URL — CE API base URL (injected automatically by CE runtime,
e.g. https://api.private.eu-de.codeengine.cloud.ibm.com)
CE_PROJECT_ID — CE project UUID (injected automatically by CE runtime)
Expand Down Expand Up @@ -51,7 +60,11 @@
)
CE_PROJECT_ID = os.environ.get("CE_PROJECT_ID", "")
FLEET_ID = os.environ.get("FLEET_ID", "")
CE_API_KEY = os.environ.get("CE_API_KEY", "")
TRUSTED_PROFILE_NAME = os.environ.get("TRUSTED_PROFILE_NAME", "")
CR_TOKEN_FILENAME = os.environ.get(
"CR_TOKEN_FILENAME",
"/var/run/secrets/codeengine.cloud.ibm.com/compute-resource-token/token",
)

# Self-URL — used to give callers a ready-made retrieve_url in async responses
# Format: https://{CE_FUNCTION}.{CE_SUBDOMAIN}.{CE_DOMAIN}
Expand Down Expand Up @@ -96,11 +109,22 @@ def _decode_b64(encoded: str) -> str:
return base64.urlsafe_b64decode(encoded).decode("utf-8")


def _get_iam_token(api_key: str) -> str:
"""Exchange an IBM Cloud API key for an IAM bearer token."""
def _get_iam_token() -> str:
"""Exchange the Code Engine compute resource token for an IAM bearer token
of the configured trusted profile."""
try:
with open(CR_TOKEN_FILENAME, encoding="utf-8") as fh:
cr_token = fh.read().strip()
except OSError as exc:
raise RuntimeError(
f"compute resource token not readable at {CR_TOKEN_FILENAME} "
f"(is the function deployed with --trusted-profiles-enabled?): {exc}"
) from exc

body = urllib.parse.urlencode({
"grant_type": "urn:ibm:params:oauth:grant-type:apikey",
"apikey": api_key,
"grant_type": "urn:ibm:params:oauth:grant-type:cr-token",
"cr_token": cr_token,
"profile_name": TRUSTED_PROFILE_NAME,
})
conn = http.client.HTTPSConnection(IAM_TOKEN_URL, timeout=15)
conn.request(
Expand Down Expand Up @@ -302,14 +326,14 @@ def main(params: dict):
))

if is_lookup_only:
if not CE_API_KEY:
if not TRUSTED_PROFILE_NAME:
return {
"headers": {"Content-Type": "application/json"},
"statusCode": 500,
"body": {"error": "Missing required CE_API_KEY configuration"},
"body": {"error": "Missing required TRUSTED_PROFILE_NAME configuration"},
}
try:
iam_token = _get_iam_token(CE_API_KEY)
iam_token = _get_iam_token()
except Exception as exc:
return {
"headers": {"Content-Type": "application/json"},
Expand Down Expand Up @@ -416,7 +440,7 @@ def main(params: dict):
k for k, v in {
"FLEET_ID": fleet_id,
"CE_PROJECT_ID": project_id,
"CE_API_KEY": CE_API_KEY,
"TRUSTED_PROFILE_NAME": TRUSTED_PROFILE_NAME,
}.items() if not v
]
if missing_config:
Expand All @@ -428,7 +452,7 @@ def main(params: dict):

# 4. Obtain IAM token
try:
iam_token = _get_iam_token(CE_API_KEY)
iam_token = _get_iam_token()
except Exception as exc:
return {
"headers": {"Content-Type": "application/json"},
Expand Down Expand Up @@ -501,17 +525,18 @@ def main(params: dict):
}

# 8. Timeout reached or async requested — return 202 with task_id
if FUNCTION_URL:
body_202["retrieve_url"] = f"{FUNCTION_URL}?task_id={task_id}"
return {
"headers": {"Content-Type": "application/json"},
"statusCode": 202,
"body": {
body_202 = {
"message": "Classification task submitted and in progress",
"task_id": task_id,
"status": "pending",
"fleet_id": fleet_id,
"question_name": question_name,
"poll_timeout_seconds": poll_timeout_sec,
},
}
if FUNCTION_URL:
body_202["retrieve_url"] = f"{FUNCTION_URL}?task_id={task_id}"
return {
"headers": {"Content-Type": "application/json"},
"statusCode": 202,
"body": body_202,
}
Loading