Skip to content

Commit b2fa381

Browse files
authored
Merge pull request #975 from negz/extra-required
Document using required (fka extra) resources in compositions
2 parents 0314e64 + 6d648d6 commit b2fa381

File tree

2 files changed

+364
-10
lines changed

2 files changed

+364
-10
lines changed

content/master/composition/compositions.md

Lines changed: 182 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -606,15 +606,15 @@ Most composition functions read the observed state of the composite resource,
606606
and use it to add composed resources to the desired state. This tells Crossplane
607607
which composed resources it should create or update.
608608

609-
If the function needs __extra resources__ to determine the desired state it can
610-
request any cluster-scoped resource Crossplane already has access to, either by
609+
If the function needs __required resources__ to determine the desired state it can
610+
request any cluster-scoped or namespaced resource Crossplane already has access to, either by
611611
name or labels through the returned RunFunctionResponse. Crossplane then calls
612-
the function again including the requested __extra resources__ and the
612+
the function again including the requested __required resources__ and the
613613
__context__ returned by the Function itself alongside the same __input__,
614614
__observed__ and __desired state__ of the previous RunFunctionRequest. Functions
615-
can iteratively request __extra resources__ if needed, but to avoid endlessly
615+
can iteratively request __required resources__ if needed, but to avoid endlessly
616616
looping Crossplane limits the number of iterations to 5. Crossplane considers
617-
the function satisfied as soon as the __extra resources__ requests become
617+
the function satisfied as soon as the __required resources__ requests become
618618
stable, so the Function returns the same exact request two times in a row.
619619
Crossplane errors if stability isn't reached after 5 iterations.
620620

@@ -767,6 +767,183 @@ Crossplane doesn't validate function input. It's a good idea for a function to
767767
validate its own input.
768768
{{</hint>}}
769769

770+
### Required resources
771+
772+
{{<hint "note">}}
773+
Crossplane v1 called this feature "extra resources." The v2 API
774+
uses the name "required resources" and adds support for bootstrap requirements.
775+
{{</hint>}}
776+
777+
Functions can request access to existing Kubernetes resources to help determine
778+
the desired state. Functions use this capability to read configuration from
779+
ConfigMaps, select the status of other resources, or make decisions based on
780+
existing cluster state.
781+
782+
Functions can receive required resources in two ways:
783+
784+
#### Bootstrap requirements
785+
786+
You can provide required resources in the Composition pipeline step. This
787+
approach performs better than requesting resources during function runtime:
788+
789+
```yaml
790+
apiVersion: apiextensions.crossplane.io/v1
791+
kind: Composition
792+
metadata:
793+
name: app-with-config
794+
spec:
795+
compositeTypeRef:
796+
apiVersion: example.crossplane.io/v1
797+
kind: App
798+
mode: Pipeline
799+
pipeline:
800+
- step: create-deployment-from-config
801+
functionRef:
802+
name: crossplane-contrib-function-python
803+
requirements:
804+
requiredResources:
805+
- requirementName: app-config
806+
apiVersion: v1
807+
kind: ConfigMap
808+
name: app-configuration
809+
namespace: default
810+
input:
811+
apiVersion: python.fn.crossplane.io/v1beta1
812+
kind: Script
813+
script: |
814+
from crossplane.function import request
815+
816+
def compose(req, rsp):
817+
observed_xr = req.observed.composite.resource
818+
819+
# Access the required ConfigMap using the helper function
820+
config_map = request.get_required_resource(req, "app-config")
821+
822+
if not config_map:
823+
# Fallback image if ConfigMap not found
824+
image = "nginx:latest"
825+
else:
826+
# Read image from ConfigMap data
827+
image = config_map.get("data", {}).get("image", "nginx:latest")
828+
829+
# Create deployment with the configured image
830+
rsp.desired.resources["deployment"].resource.update({
831+
"apiVersion": "apps/v1",
832+
"kind": "Deployment",
833+
"metadata": {
834+
"labels": {"example.crossplane.io/app": observed_xr["metadata"]["name"]},
835+
},
836+
"spec": {
837+
"replicas": 2,
838+
"selector": {"matchLabels": {"example.crossplane.io/app": observed_xr["metadata"]["name"]}},
839+
"template": {
840+
"metadata": {
841+
"labels": {"example.crossplane.io/app": observed_xr["metadata"]["name"]},
842+
},
843+
"spec": {
844+
"containers": [{
845+
"name": "app",
846+
"image": image,
847+
"ports": [{"containerPort": 80}]
848+
}],
849+
},
850+
},
851+
},
852+
})
853+
```
854+
855+
#### Dynamic resource requests
856+
857+
Functions can also request resources during runtime through the
858+
RunFunctionResponse. Crossplane calls the function again with the requested
859+
resources:
860+
861+
```yaml
862+
apiVersion: apiextensions.crossplane.io/v1
863+
kind: Composition
864+
metadata:
865+
name: app-dynamic-config
866+
spec:
867+
compositeTypeRef:
868+
apiVersion: example.crossplane.io/v1
869+
kind: App
870+
mode: Pipeline
871+
pipeline:
872+
- step: create-deployment-from-dynamic-config
873+
functionRef:
874+
name: crossplane-contrib-function-python
875+
input:
876+
apiVersion: python.fn.crossplane.io/v1beta1
877+
kind: Script
878+
script: |
879+
from crossplane.function import request, response
880+
881+
def compose(req, rsp):
882+
observed_xr = req.observed.composite.resource
883+
884+
# Always request the ConfigMap to ensure stable requirements
885+
config_name = observed_xr["spec"].get("configName", "default-config")
886+
namespace = observed_xr["metadata"].get("namespace", "default")
887+
888+
response.require_resources(
889+
rsp,
890+
name="dynamic-config",
891+
api_version="v1",
892+
kind="ConfigMap",
893+
match_name=config_name,
894+
namespace=namespace
895+
)
896+
897+
# Check if we have the required ConfigMap
898+
config_map = request.get_required_resource(req, "dynamic-config")
899+
900+
if not config_map:
901+
# ConfigMap not found yet - Crossplane will call us again
902+
return
903+
904+
# ConfigMap found - use the image data to create deployment
905+
image = config_map.get("data", {}).get("image", "nginx:latest")
906+
907+
rsp.desired.resources["deployment"].resource.update({
908+
"apiVersion": "apps/v1",
909+
"kind": "Deployment",
910+
"metadata": {
911+
"labels": {"example.crossplane.io/app": observed_xr["metadata"]["name"]},
912+
},
913+
"spec": {
914+
"replicas": 2,
915+
"selector": {"matchLabels": {"example.crossplane.io/app": observed_xr["metadata"]["name"]}},
916+
"template": {
917+
"metadata": {
918+
"labels": {"example.crossplane.io/app": observed_xr["metadata"]["name"]},
919+
},
920+
"spec": {
921+
"containers": [{
922+
"name": "app",
923+
"image": image,
924+
"ports": [{"containerPort": 80}]
925+
}],
926+
},
927+
},
928+
},
929+
})
930+
```
931+
932+
{{<hint "tip">}}
933+
Use bootstrap requirements when possible for better performance. Dynamic requests
934+
require more function calls and work best when the
935+
required resources depend on the observed state or earlier function results.
936+
{{</hint>}}
937+
938+
Functions can request resources by:
939+
- **Name**: `name: "my-configmap"` for a specific resource
940+
- **Labels**: `matchLabels: {"env": "prod"}` for multiple resources
941+
- **Namespace**: Include `namespace: "production"` for namespaced resources
942+
943+
Crossplane limits dynamic resource requests to 5 iterations to prevent infinite
944+
loops. The function signals completion by returning the same resource requirements
945+
two iterations in a row.
946+
770947
### Function pipeline context
771948

772949
Sometimes two functions in a pipeline want to share information with each other

0 commit comments

Comments
 (0)