Skip to content

Commit ea1ab25

Browse files
committed
Document requirement stabilization pattern for dynamic resource discovery
Add critical guidance for WatchOperation functions that request resources dynamically. Functions must return consistent requirements across iterations for proper stabilization. Include working example with error handling. Signed-off-by: Nic Cope <[email protected]>
1 parent 91edda5 commit ea1ab25

File tree

1 file changed

+46
-13
lines changed

1 file changed

+46
-13
lines changed

content/master/operations/watchoperation.md

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,8 @@ spec:
354354
renewBefore: "720h" # 30 days
355355
```
356356
357-
The function examines the watched Ingress and dynamically requests related resources:
357+
The function examines the watched Ingress and dynamically requests related
358+
resources:
358359
359360
```python
360361
from crossplane.function import request, response
@@ -363,38 +364,70 @@ def operate(req, rsp):
363364
# Access the watched Ingress resource
364365
ingress = request.get_required_resource(req, "ops.crossplane.io/watched-resource")
365366
if not ingress:
366-
response.set_output(rsp, {"error": "No watched resource found"})
367+
response.fatal(rsp, "No watched resource found")
367368
return
368369

369370
# Extract the service name from the Ingress backend
370371
rules = ingress.get("spec", {}).get("rules", [])
371372
if not rules:
373+
response.fatal(rsp, "Could not extract service name from ingress")
372374
return
373375

374376
backend = rules[0].get("http", {}).get("paths", [{}])[0].get("backend", {})
375377
service_name = backend.get("service", {}).get("name")
376378
if not service_name:
379+
response.fatal(rsp, "Could not extract service name from ingress")
377380
return
378381

379382
ingress_namespace = ingress.get("metadata", {}).get("namespace", "default")
380383

381-
# Always declare what resources we need (requirements must be stable across calls)
382-
rsp.requirements.resources["related-service"].api_version = "v1"
383-
rsp.requirements.resources["related-service"].kind = "Service"
384-
rsp.requirements.resources["related-service"].match_name = service_name
385-
rsp.requirements.resources["related-service"].namespace = ingress_namespace
384+
# CRITICAL: Always request the same resources to ensure requirement
385+
# stabilization. Crossplane calls the function repeatedly until
386+
# requirements don't change.
387+
response.require_resources(
388+
rsp,
389+
name="related-service",
390+
api_version="v1",
391+
kind="Service",
392+
match_name=service_name,
393+
namespace=ingress_namespace
394+
)
386395

387-
# Process the service if Crossplane fetched it after our previous response
396+
# Check if the service is available and process accordingly
388397
service = request.get_required_resource(req, "related-service")
389398
if service:
390-
create_certificate_for_service(ingress, service, rsp)
399+
# Success: Both resources available
400+
response.set_output(rsp, {
401+
"status": "success",
402+
"message": "Certificate management completed",
403+
"ingress_host": ingress.get("spec", {}).get("rules", [{}])[0].get("host"),
404+
"service_name": service.get("metadata", {}).get("name")
405+
})
406+
return
407+
408+
# Waiting: Service not available yet
409+
response.set_output(rsp, {
410+
"status": "waiting",
411+
"message": f"Waiting for service '{service_name}' to be available"
412+
})
391413
```
392414

415+
{{<hint "important">}}
416+
**Critical resource stabilization pattern**: functions must return the **same
417+
requirements** in each iteration to signal completion. The function in the
418+
preceding example always calls `response.require_resources()` regardless of
419+
whether the service exists. This ensures Crossplane knows when to stop calling
420+
the function.
421+
422+
Common mistake: only requesting resources when missing breaks the stabilization
423+
contract and causes timeout errors.
424+
{{</hint>}}
425+
393426
This pattern allows functions to:
394-
1. Examine the watched resource
395-
2. Dynamically determine what other resources you need
396-
3. Request those resources in the next response
397-
4. Process the more resources in next calls
427+
1. Examine the watched resource (injected automatically)
428+
2. Dynamically determine what other resources the function needs
429+
3. Request those resources consistently using `response.require_resources()`
430+
4. Process all resources when available, or provide status when waiting
398431

399432
## Status and monitoring
400433

0 commit comments

Comments
 (0)