From 1413873fa7849d5c630b9c33ab90ac0fa81af996 Mon Sep 17 00:00:00 2001 From: Nic Cope Date: Thu, 14 Aug 2025 14:12:03 -0700 Subject: [PATCH 1/2] Document using required (fka extra) resources in compositions Right now AFAIK only function-python supports this. Signed-off-by: Nic Cope --- content/master/composition/compositions.md | 187 ++++++++++++++++++++- 1 file changed, 182 insertions(+), 5 deletions(-) diff --git a/content/master/composition/compositions.md b/content/master/composition/compositions.md index 8fc380a00..e933ad2ec 100644 --- a/content/master/composition/compositions.md +++ b/content/master/composition/compositions.md @@ -606,15 +606,15 @@ Most composition functions read the observed state of the composite resource, and use it to add composed resources to the desired state. This tells Crossplane which composed resources it should create or update. -If the function needs __extra resources__ to determine the desired state it can -request any cluster-scoped resource Crossplane already has access to, either by +If the function needs __required resources__ to determine the desired state it can +request any cluster-scoped or namespaced resource Crossplane already has access to, either by name or labels through the returned RunFunctionResponse. Crossplane then calls -the function again including the requested __extra resources__ and the +the function again including the requested __required resources__ and the __context__ returned by the Function itself alongside the same __input__, __observed__ and __desired state__ of the previous RunFunctionRequest. Functions -can iteratively request __extra resources__ if needed, but to avoid endlessly +can iteratively request __required resources__ if needed, but to avoid endlessly looping Crossplane limits the number of iterations to 5. Crossplane considers -the function satisfied as soon as the __extra resources__ requests become +the function satisfied as soon as the __required resources__ requests become stable, so the Function returns the same exact request two times in a row. Crossplane errors if stability isn't reached after 5 iterations. @@ -767,6 +767,183 @@ Crossplane doesn't validate function input. It's a good idea for a function to validate its own input. {{}} +### Required resources + +{{}} +Crossplane v1 called this feature "extra resources." The v2 API +uses the name "required resources" and adds support for bootstrap requirements. +{{}} + +Functions can request access to existing Kubernetes resources to help determine +the desired state. Functions use this capability to read configuration from +ConfigMaps, select the status of other resources, or make decisions based on +existing cluster state. + +Functions can receive required resources in two ways: + +#### Bootstrap requirements + +You can provide required resources in the Composition pipeline step. This +approach performs better than requesting resources during function runtime: + +```yaml +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: app-with-config +spec: + compositeTypeRef: + apiVersion: example.crossplane.io/v1 + kind: App + mode: Pipeline + pipeline: + - step: create-deployment-from-config + functionRef: + name: crossplane-contrib-function-python + requirements: + requiredResources: + - requirementName: app-config + apiVersion: v1 + kind: ConfigMap + name: app-configuration + namespace: default + input: + apiVersion: python.fn.crossplane.io/v1beta1 + kind: Script + script: | + from crossplane.function import request + + def compose(req, rsp): + observed_xr = req.observed.composite.resource + + # Access the required ConfigMap using the helper function + config_map = request.get_required_resource(req, "app-config") + + if not config_map: + # Fallback image if ConfigMap not found + image = "nginx:latest" + else: + # Read image from ConfigMap data + image = config_map.get("data", {}).get("image", "nginx:latest") + + # Create deployment with the configured image + rsp.desired.resources["deployment"].resource.update({ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "labels": {"example.crossplane.io/app": observed_xr["metadata"]["name"]}, + }, + "spec": { + "replicas": 2, + "selector": {"matchLabels": {"example.crossplane.io/app": observed_xr["metadata"]["name"]}}, + "template": { + "metadata": { + "labels": {"example.crossplane.io/app": observed_xr["metadata"]["name"]}, + }, + "spec": { + "containers": [{ + "name": "app", + "image": image, + "ports": [{"containerPort": 80}] + }], + }, + }, + }, + }) +``` + +#### Dynamic resource requests + +Functions can also request resources during runtime through the +RunFunctionResponse. Crossplane calls the function again with the requested +resources: + +```yaml +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: app-dynamic-config +spec: + compositeTypeRef: + apiVersion: example.crossplane.io/v1 + kind: App + mode: Pipeline + pipeline: + - step: create-deployment-from-dynamic-config + functionRef: + name: crossplane-contrib-function-python + input: + apiVersion: python.fn.crossplane.io/v1beta1 + kind: Script + script: | + from crossplane.function import request, response + + def compose(req, rsp): + observed_xr = req.observed.composite.resource + + # Always request the ConfigMap to ensure stable requirements + config_name = observed_xr["spec"].get("configName", "default-config") + namespace = observed_xr["metadata"].get("namespace", "default") + + response.require_resources( + rsp, + name="dynamic-config", + api_version="v1", + kind="ConfigMap", + match_name=config_name, + namespace=namespace + ) + + # Check if we have the required ConfigMap + config_map = request.get_required_resource(req, "dynamic-config") + + if not config_map: + # ConfigMap not found yet - Crossplane will call us again + return + + # ConfigMap found - use the image data to create deployment + image = config_map.get("data", {}).get("image", "nginx:latest") + + rsp.desired.resources["deployment"].resource.update({ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "labels": {"example.crossplane.io/app": observed_xr["metadata"]["name"]}, + }, + "spec": { + "replicas": 2, + "selector": {"matchLabels": {"example.crossplane.io/app": observed_xr["metadata"]["name"]}}, + "template": { + "metadata": { + "labels": {"example.crossplane.io/app": observed_xr["metadata"]["name"]}, + }, + "spec": { + "containers": [{ + "name": "app", + "image": image, + "ports": [{"containerPort": 80}] + }], + }, + }, + }, + }) +``` + +{{}} +Use bootstrap requirements when possible for better performance. Dynamic requests +require more function calls and work best when the +required resources depend on the observed state or earlier function results. +{{}} + +Functions can request resources by: +- **Name**: `name: "my-configmap"` for a specific resource +- **Labels**: `matchLabels: {"env": "prod"}` for multiple resources +- **Namespace**: Include `namespace: "production"` for namespaced resources + +Crossplane limits dynamic resource requests to 5 iterations to prevent infinite +loops. The function signals completion by returning the same resource requirements +two iterations in a row. + ### Function pipeline context Sometimes two functions in a pipeline want to share information with each other From 6d648d6bf4c5f28febdfbb5d8e634856848b4cd5 Mon Sep 17 00:00:00 2001 From: Nic Cope Date: Thu, 14 Aug 2025 15:29:52 -0700 Subject: [PATCH 2/2] Sync content/master -> content/v2.0 Signed-off-by: Nic Cope --- content/v2.0/composition/compositions.md | 187 ++++++++++++++++++++++- 1 file changed, 182 insertions(+), 5 deletions(-) diff --git a/content/v2.0/composition/compositions.md b/content/v2.0/composition/compositions.md index 8fc380a00..e933ad2ec 100644 --- a/content/v2.0/composition/compositions.md +++ b/content/v2.0/composition/compositions.md @@ -606,15 +606,15 @@ Most composition functions read the observed state of the composite resource, and use it to add composed resources to the desired state. This tells Crossplane which composed resources it should create or update. -If the function needs __extra resources__ to determine the desired state it can -request any cluster-scoped resource Crossplane already has access to, either by +If the function needs __required resources__ to determine the desired state it can +request any cluster-scoped or namespaced resource Crossplane already has access to, either by name or labels through the returned RunFunctionResponse. Crossplane then calls -the function again including the requested __extra resources__ and the +the function again including the requested __required resources__ and the __context__ returned by the Function itself alongside the same __input__, __observed__ and __desired state__ of the previous RunFunctionRequest. Functions -can iteratively request __extra resources__ if needed, but to avoid endlessly +can iteratively request __required resources__ if needed, but to avoid endlessly looping Crossplane limits the number of iterations to 5. Crossplane considers -the function satisfied as soon as the __extra resources__ requests become +the function satisfied as soon as the __required resources__ requests become stable, so the Function returns the same exact request two times in a row. Crossplane errors if stability isn't reached after 5 iterations. @@ -767,6 +767,183 @@ Crossplane doesn't validate function input. It's a good idea for a function to validate its own input. {{}} +### Required resources + +{{}} +Crossplane v1 called this feature "extra resources." The v2 API +uses the name "required resources" and adds support for bootstrap requirements. +{{}} + +Functions can request access to existing Kubernetes resources to help determine +the desired state. Functions use this capability to read configuration from +ConfigMaps, select the status of other resources, or make decisions based on +existing cluster state. + +Functions can receive required resources in two ways: + +#### Bootstrap requirements + +You can provide required resources in the Composition pipeline step. This +approach performs better than requesting resources during function runtime: + +```yaml +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: app-with-config +spec: + compositeTypeRef: + apiVersion: example.crossplane.io/v1 + kind: App + mode: Pipeline + pipeline: + - step: create-deployment-from-config + functionRef: + name: crossplane-contrib-function-python + requirements: + requiredResources: + - requirementName: app-config + apiVersion: v1 + kind: ConfigMap + name: app-configuration + namespace: default + input: + apiVersion: python.fn.crossplane.io/v1beta1 + kind: Script + script: | + from crossplane.function import request + + def compose(req, rsp): + observed_xr = req.observed.composite.resource + + # Access the required ConfigMap using the helper function + config_map = request.get_required_resource(req, "app-config") + + if not config_map: + # Fallback image if ConfigMap not found + image = "nginx:latest" + else: + # Read image from ConfigMap data + image = config_map.get("data", {}).get("image", "nginx:latest") + + # Create deployment with the configured image + rsp.desired.resources["deployment"].resource.update({ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "labels": {"example.crossplane.io/app": observed_xr["metadata"]["name"]}, + }, + "spec": { + "replicas": 2, + "selector": {"matchLabels": {"example.crossplane.io/app": observed_xr["metadata"]["name"]}}, + "template": { + "metadata": { + "labels": {"example.crossplane.io/app": observed_xr["metadata"]["name"]}, + }, + "spec": { + "containers": [{ + "name": "app", + "image": image, + "ports": [{"containerPort": 80}] + }], + }, + }, + }, + }) +``` + +#### Dynamic resource requests + +Functions can also request resources during runtime through the +RunFunctionResponse. Crossplane calls the function again with the requested +resources: + +```yaml +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: app-dynamic-config +spec: + compositeTypeRef: + apiVersion: example.crossplane.io/v1 + kind: App + mode: Pipeline + pipeline: + - step: create-deployment-from-dynamic-config + functionRef: + name: crossplane-contrib-function-python + input: + apiVersion: python.fn.crossplane.io/v1beta1 + kind: Script + script: | + from crossplane.function import request, response + + def compose(req, rsp): + observed_xr = req.observed.composite.resource + + # Always request the ConfigMap to ensure stable requirements + config_name = observed_xr["spec"].get("configName", "default-config") + namespace = observed_xr["metadata"].get("namespace", "default") + + response.require_resources( + rsp, + name="dynamic-config", + api_version="v1", + kind="ConfigMap", + match_name=config_name, + namespace=namespace + ) + + # Check if we have the required ConfigMap + config_map = request.get_required_resource(req, "dynamic-config") + + if not config_map: + # ConfigMap not found yet - Crossplane will call us again + return + + # ConfigMap found - use the image data to create deployment + image = config_map.get("data", {}).get("image", "nginx:latest") + + rsp.desired.resources["deployment"].resource.update({ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "labels": {"example.crossplane.io/app": observed_xr["metadata"]["name"]}, + }, + "spec": { + "replicas": 2, + "selector": {"matchLabels": {"example.crossplane.io/app": observed_xr["metadata"]["name"]}}, + "template": { + "metadata": { + "labels": {"example.crossplane.io/app": observed_xr["metadata"]["name"]}, + }, + "spec": { + "containers": [{ + "name": "app", + "image": image, + "ports": [{"containerPort": 80}] + }], + }, + }, + }, + }) +``` + +{{}} +Use bootstrap requirements when possible for better performance. Dynamic requests +require more function calls and work best when the +required resources depend on the observed state or earlier function results. +{{}} + +Functions can request resources by: +- **Name**: `name: "my-configmap"` for a specific resource +- **Labels**: `matchLabels: {"env": "prod"}` for multiple resources +- **Namespace**: Include `namespace: "production"` for namespaced resources + +Crossplane limits dynamic resource requests to 5 iterations to prevent infinite +loops. The function signals completion by returning the same resource requirements +two iterations in a row. + ### Function pipeline context Sometimes two functions in a pipeline want to share information with each other