function.request
Utilities for working with RunFunctionRequests.
1# Copyright 2025 The Crossplane Authors. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""Utilities for working with RunFunctionRequests.""" 16 17import dataclasses 18 19import crossplane.function.proto.v1.run_function_pb2 as fnv1 20from crossplane.function import resource 21 22 23@dataclasses.dataclass 24class Credentials: 25 """Credentials.""" 26 27 type: str 28 data: dict 29 30 31def get_required_resources(req: fnv1.RunFunctionRequest, name: str) -> list[dict]: 32 """Get required resources by name from the request. 33 34 Args: 35 req: The RunFunctionRequest containing required resources. 36 name: The name of the required resource set to get. 37 38 Returns: 39 A list of resources as dictionaries. Empty list if not found. 40 41 Required resources are previously called "extra resources" in composition 42 functions. For operation functions, there are no observed resources, so 43 all resources are "required" resources that the function requested. 44 45 Note: This returns an empty list both when the requirement hasn't been 46 resolved yet, and when it was resolved but no resources matched. To 47 distinguish between these cases, check whether Crossplane resolved the 48 requirement using `name in req.required_resources`: 49 50 # Always declare requirements - Crossplane considers them satisfied 51 # when they stabilize across calls. 52 response.require_resources(rsp, name, ...) 53 54 if name in req.required_resources: 55 resources = request.get_required_resources(req, name) 56 if not resources: 57 # Crossplane resolved the requirement, but found no matches 58 response.fatal(rsp, "no matching resources found") 59 """ 60 if name not in req.required_resources: 61 return [] 62 63 return [ 64 resource.struct_to_dict(item.resource) 65 for item in req.required_resources[name].items 66 ] 67 68 69def get_watched_resource(req: fnv1.RunFunctionRequest) -> dict | None: 70 """Get the watched resource that triggered this operation. 71 72 Args: 73 req: The RunFunctionRequest to check for a watched resource. 74 75 Returns: 76 The watched resource as a dictionary, or None if not found. 77 78 When a WatchOperation creates an Operation, it injects the resource that 79 changed using the special requirement name 'ops.crossplane.io/watched-resource'. 80 This helper makes it easy to access that resource. 81 """ 82 watched = get_required_resources(req, "ops.crossplane.io/watched-resource") 83 return watched[0] if watched else None 84 85 86def get_required_resource(req: fnv1.RunFunctionRequest, name: str) -> dict | None: 87 """Get a single required resource by name from the request. 88 89 Args: 90 req: The RunFunctionRequest containing required resources. 91 name: The name of the required resource to get. 92 93 Returns: 94 The first resource as a dictionary, or None if not found. 95 96 This is a convenience function for when you know there should be exactly 97 one resource with the given requirement name. 98 """ 99 resources = get_required_resources(req, name) 100 return resources[0] if resources else None 101 102 103def get_credentials(req: fnv1.RunFunctionRequest, name: str) -> Credentials: 104 """Get the supplied credentials from the request. 105 106 Args: 107 req: The RunFunctionRequest containing credentials. 108 name: The name of the credentials to get. 109 110 Returns: 111 The requested credentials with type and data. 112 113 If the credentials don't exist, returns empty credentials with type "data" 114 and empty data dictionary. 115 """ 116 empty = Credentials(type="data", data={}) 117 118 if not req or name not in req.credentials: 119 return empty 120 121 cred = req.credentials[name] 122 123 # Use WhichOneof to determine which field in the oneof is set 124 source_type = cred.WhichOneof("source") 125 if source_type == "credential_data": 126 # Convert bytes data to string data for backward compatibility 127 data = {} 128 for key, value in cred.credential_data.data.items(): 129 data[key] = value.decode("utf-8") 130 return Credentials(type="credential_data", data=data) 131 132 # If no recognized source type is set, return empty 133 return empty 134 135 136def advertises_capabilities(req: fnv1.RunFunctionRequest) -> bool: 137 """Check whether Crossplane advertises its capabilities. 138 139 Args: 140 req: The RunFunctionRequest to check. 141 142 Returns: 143 True if Crossplane advertises its capabilities. 144 145 Crossplane v2.2 and later advertise their capabilities in the request 146 metadata. If this returns False, the calling Crossplane predates capability 147 advertisement and has_capability will always return False, even for features 148 the older Crossplane does support. 149 150 if not request.advertises_capabilities(req): 151 # Pre-v2.2 Crossplane, capabilities are unknown. 152 ... 153 elif request.has_capability(req, fnv1.CAPABILITY_REQUIRED_SCHEMAS): 154 response.require_schema(rsp, "xr", xr_api_version, xr_kind) 155 """ 156 return fnv1.CAPABILITY_CAPABILITIES in req.meta.capabilities 157 158 159def has_capability( 160 req: fnv1.RunFunctionRequest, 161 cap: fnv1.Capability.ValueType, 162) -> bool: 163 """Check whether Crossplane advertises a particular capability. 164 165 Args: 166 req: The RunFunctionRequest to check. 167 cap: The capability to check for, e.g. fnv1.CAPABILITY_REQUIRED_SCHEMAS. 168 169 Returns: 170 True if the capability is present in the request metadata. 171 172 Crossplane sends its capabilities in the request metadata. Functions can use 173 this to determine whether Crossplane will honor certain fields in their 174 response, or populate certain fields in their request. 175 176 Use advertises_capabilities to check whether Crossplane advertises its 177 capabilities at all. If it doesn't, has_capability always returns False even 178 for features the older Crossplane does support. 179 180 if request.has_capability(req, fnv1.CAPABILITY_REQUIRED_SCHEMAS): 181 response.require_schema(rsp, "xr", xr_api_version, xr_kind) 182 """ 183 return cap in req.meta.capabilities 184 185 186def get_required_schema(req: fnv1.RunFunctionRequest, name: str) -> dict | None: 187 """Get a required OpenAPI schema by name from the request. 188 189 Args: 190 req: The RunFunctionRequest containing required schemas. 191 name: The name of the required schema to get. 192 193 Returns: 194 The OpenAPI v3 schema as a dictionary, or None if not found. 195 196 Note: This returns None both when the requirement hasn't been resolved yet, 197 and when it was resolved but the schema wasn't found. To distinguish between 198 these cases, check whether Crossplane resolved the requirement using 199 `name in req.required_schemas`: 200 201 # Always declare requirements - Crossplane considers them satisfied 202 # when they stabilize across calls. 203 response.require_schema(rsp, name, "example.org/v1", "MyKind") 204 205 if name in req.required_schemas: 206 schema = request.get_required_schema(req, name) 207 if schema is None: 208 # Crossplane resolved the requirement, but couldn't find it 209 response.fatal(rsp, "schema not found") 210 211 The returned schema can be used with libraries like openapi-schema-validator 212 or jsonschema for validation: 213 214 schema = request.get_required_schema(req, "my-schema") 215 if schema: 216 from openapi_schema_validator import validate 217 validate(resource, schema) 218 """ 219 if name not in req.required_schemas: 220 return None 221 222 schema = req.required_schemas[name] 223 if not schema.HasField("openapi_v3"): 224 return None 225 226 return resource.struct_to_dict(schema.openapi_v3)
Credentials.
32def get_required_resources(req: fnv1.RunFunctionRequest, name: str) -> list[dict]: 33 """Get required resources by name from the request. 34 35 Args: 36 req: The RunFunctionRequest containing required resources. 37 name: The name of the required resource set to get. 38 39 Returns: 40 A list of resources as dictionaries. Empty list if not found. 41 42 Required resources are previously called "extra resources" in composition 43 functions. For operation functions, there are no observed resources, so 44 all resources are "required" resources that the function requested. 45 46 Note: This returns an empty list both when the requirement hasn't been 47 resolved yet, and when it was resolved but no resources matched. To 48 distinguish between these cases, check whether Crossplane resolved the 49 requirement using `name in req.required_resources`: 50 51 # Always declare requirements - Crossplane considers them satisfied 52 # when they stabilize across calls. 53 response.require_resources(rsp, name, ...) 54 55 if name in req.required_resources: 56 resources = request.get_required_resources(req, name) 57 if not resources: 58 # Crossplane resolved the requirement, but found no matches 59 response.fatal(rsp, "no matching resources found") 60 """ 61 if name not in req.required_resources: 62 return [] 63 64 return [ 65 resource.struct_to_dict(item.resource) 66 for item in req.required_resources[name].items 67 ]
Get required resources by name from the request.
Arguments:
- req: The RunFunctionRequest containing required resources.
- name: The name of the required resource set to get.
Returns:
A list of resources as dictionaries. Empty list if not found.
Required resources are previously called "extra resources" in composition functions. For operation functions, there are no observed resources, so all resources are "required" resources that the function requested.
Note: This returns an empty list both when the requirement hasn't been
resolved yet, and when it was resolved but no resources matched. To
distinguish between these cases, check whether Crossplane resolved the
requirement using name in req.required_resources:
# Always declare requirements - Crossplane considers them satisfied
# when they stabilize across calls.
response.require_resources(rsp, name, ...)
if name in req.required_resources:
resources = request.get_required_resources(req, name)
if not resources:
# Crossplane resolved the requirement, but found no matches
response.fatal(rsp, "no matching resources found")
70def get_watched_resource(req: fnv1.RunFunctionRequest) -> dict | None: 71 """Get the watched resource that triggered this operation. 72 73 Args: 74 req: The RunFunctionRequest to check for a watched resource. 75 76 Returns: 77 The watched resource as a dictionary, or None if not found. 78 79 When a WatchOperation creates an Operation, it injects the resource that 80 changed using the special requirement name 'ops.crossplane.io/watched-resource'. 81 This helper makes it easy to access that resource. 82 """ 83 watched = get_required_resources(req, "ops.crossplane.io/watched-resource") 84 return watched[0] if watched else None
Get the watched resource that triggered this operation.
Arguments:
- req: The RunFunctionRequest to check for a watched resource.
Returns:
The watched resource as a dictionary, or None if not found.
When a WatchOperation creates an Operation, it injects the resource that changed using the special requirement name 'ops.crossplane.io/watched-resource'. This helper makes it easy to access that resource.
87def get_required_resource(req: fnv1.RunFunctionRequest, name: str) -> dict | None: 88 """Get a single required resource by name from the request. 89 90 Args: 91 req: The RunFunctionRequest containing required resources. 92 name: The name of the required resource to get. 93 94 Returns: 95 The first resource as a dictionary, or None if not found. 96 97 This is a convenience function for when you know there should be exactly 98 one resource with the given requirement name. 99 """ 100 resources = get_required_resources(req, name) 101 return resources[0] if resources else None
Get a single required resource by name from the request.
Arguments:
- req: The RunFunctionRequest containing required resources.
- name: The name of the required resource to get.
Returns:
The first resource as a dictionary, or None if not found.
This is a convenience function for when you know there should be exactly one resource with the given requirement name.
104def get_credentials(req: fnv1.RunFunctionRequest, name: str) -> Credentials: 105 """Get the supplied credentials from the request. 106 107 Args: 108 req: The RunFunctionRequest containing credentials. 109 name: The name of the credentials to get. 110 111 Returns: 112 The requested credentials with type and data. 113 114 If the credentials don't exist, returns empty credentials with type "data" 115 and empty data dictionary. 116 """ 117 empty = Credentials(type="data", data={}) 118 119 if not req or name not in req.credentials: 120 return empty 121 122 cred = req.credentials[name] 123 124 # Use WhichOneof to determine which field in the oneof is set 125 source_type = cred.WhichOneof("source") 126 if source_type == "credential_data": 127 # Convert bytes data to string data for backward compatibility 128 data = {} 129 for key, value in cred.credential_data.data.items(): 130 data[key] = value.decode("utf-8") 131 return Credentials(type="credential_data", data=data) 132 133 # If no recognized source type is set, return empty 134 return empty
Get the supplied credentials from the request.
Arguments:
- req: The RunFunctionRequest containing credentials.
- name: The name of the credentials to get.
Returns:
The requested credentials with type and data.
If the credentials don't exist, returns empty credentials with type "data" and empty data dictionary.
137def advertises_capabilities(req: fnv1.RunFunctionRequest) -> bool: 138 """Check whether Crossplane advertises its capabilities. 139 140 Args: 141 req: The RunFunctionRequest to check. 142 143 Returns: 144 True if Crossplane advertises its capabilities. 145 146 Crossplane v2.2 and later advertise their capabilities in the request 147 metadata. If this returns False, the calling Crossplane predates capability 148 advertisement and has_capability will always return False, even for features 149 the older Crossplane does support. 150 151 if not request.advertises_capabilities(req): 152 # Pre-v2.2 Crossplane, capabilities are unknown. 153 ... 154 elif request.has_capability(req, fnv1.CAPABILITY_REQUIRED_SCHEMAS): 155 response.require_schema(rsp, "xr", xr_api_version, xr_kind) 156 """ 157 return fnv1.CAPABILITY_CAPABILITIES in req.meta.capabilities
Check whether Crossplane advertises its capabilities.
Arguments:
- req: The RunFunctionRequest to check.
Returns:
True if Crossplane advertises its capabilities.
Crossplane v2.2 and later advertise their capabilities in the request metadata. If this returns False, the calling Crossplane predates capability advertisement and has_capability will always return False, even for features the older Crossplane does support.
if not request.advertises_capabilities(req):
# Pre-v2.2 Crossplane, capabilities are unknown.
...
elif request.has_capability(req, fnv1.CAPABILITY_REQUIRED_SCHEMAS):
response.require_schema(rsp, "xr", xr_api_version, xr_kind)
160def has_capability( 161 req: fnv1.RunFunctionRequest, 162 cap: fnv1.Capability.ValueType, 163) -> bool: 164 """Check whether Crossplane advertises a particular capability. 165 166 Args: 167 req: The RunFunctionRequest to check. 168 cap: The capability to check for, e.g. fnv1.CAPABILITY_REQUIRED_SCHEMAS. 169 170 Returns: 171 True if the capability is present in the request metadata. 172 173 Crossplane sends its capabilities in the request metadata. Functions can use 174 this to determine whether Crossplane will honor certain fields in their 175 response, or populate certain fields in their request. 176 177 Use advertises_capabilities to check whether Crossplane advertises its 178 capabilities at all. If it doesn't, has_capability always returns False even 179 for features the older Crossplane does support. 180 181 if request.has_capability(req, fnv1.CAPABILITY_REQUIRED_SCHEMAS): 182 response.require_schema(rsp, "xr", xr_api_version, xr_kind) 183 """ 184 return cap in req.meta.capabilities
Check whether Crossplane advertises a particular capability.
Arguments:
- req: The RunFunctionRequest to check.
- cap: The capability to check for, e.g. fnv1.CAPABILITY_REQUIRED_SCHEMAS.
Returns:
True if the capability is present in the request metadata.
Crossplane sends its capabilities in the request metadata. Functions can use this to determine whether Crossplane will honor certain fields in their response, or populate certain fields in their request.
Use advertises_capabilities to check whether Crossplane advertises its capabilities at all. If it doesn't, has_capability always returns False even for features the older Crossplane does support.
if request.has_capability(req, fnv1.CAPABILITY_REQUIRED_SCHEMAS):
response.require_schema(rsp, "xr", xr_api_version, xr_kind)
187def get_required_schema(req: fnv1.RunFunctionRequest, name: str) -> dict | None: 188 """Get a required OpenAPI schema by name from the request. 189 190 Args: 191 req: The RunFunctionRequest containing required schemas. 192 name: The name of the required schema to get. 193 194 Returns: 195 The OpenAPI v3 schema as a dictionary, or None if not found. 196 197 Note: This returns None both when the requirement hasn't been resolved yet, 198 and when it was resolved but the schema wasn't found. To distinguish between 199 these cases, check whether Crossplane resolved the requirement using 200 `name in req.required_schemas`: 201 202 # Always declare requirements - Crossplane considers them satisfied 203 # when they stabilize across calls. 204 response.require_schema(rsp, name, "example.org/v1", "MyKind") 205 206 if name in req.required_schemas: 207 schema = request.get_required_schema(req, name) 208 if schema is None: 209 # Crossplane resolved the requirement, but couldn't find it 210 response.fatal(rsp, "schema not found") 211 212 The returned schema can be used with libraries like openapi-schema-validator 213 or jsonschema for validation: 214 215 schema = request.get_required_schema(req, "my-schema") 216 if schema: 217 from openapi_schema_validator import validate 218 validate(resource, schema) 219 """ 220 if name not in req.required_schemas: 221 return None 222 223 schema = req.required_schemas[name] 224 if not schema.HasField("openapi_v3"): 225 return None 226 227 return resource.struct_to_dict(schema.openapi_v3)
Get a required OpenAPI schema by name from the request.
Arguments:
- req: The RunFunctionRequest containing required schemas.
- name: The name of the required schema to get.
Returns:
The OpenAPI v3 schema as a dictionary, or None if not found.
Note: This returns None both when the requirement hasn't been resolved yet,
and when it was resolved but the schema wasn't found. To distinguish between
these cases, check whether Crossplane resolved the requirement using
name in req.required_schemas:
# Always declare requirements - Crossplane considers them satisfied
# when they stabilize across calls.
response.require_schema(rsp, name, "example.org/v1", "MyKind")
if name in req.required_schemas:
schema = request.get_required_schema(req, name)
if schema is None:
# Crossplane resolved the requirement, but couldn't find it
response.fatal(rsp, "schema not found")
The returned schema can be used with libraries like openapi-schema-validator or jsonschema for validation:
schema = request.get_required_schema(req, "my-schema")
if schema:
from openapi_schema_validator import validate
validate(resource, schema)