Skip to main content

function_sdk_rust/
request.rs

1//! Helpers for working with RunFunctionRequests.
2
3use crate::Error;
4use crate::proto::v1::{Capability, CredentialData, Resource, RunFunctionRequest, credentials};
5use crate::resource::{pb_to_json, struct_to_json};
6
7/// The requirement name a WatchOperation uses to inject the watched resource.
8pub const WATCHED_RESOURCE_KEY: &str = "ops.crossplane.io/watched-resource";
9
10/// Gets the function's input from the request as a typed value.
11///
12/// Input is the function-defined config from the Composition's
13/// `spec.pipeline[].input` block. Crossplane never validates it, so
14/// functions must - deserializing into a typed struct is the first line of
15/// that validation. Returns [`Error::MissingInput`] when the pipeline step
16/// has no input at all.
17pub fn get_input<T: serde::de::DeserializeOwned>(req: &RunFunctionRequest) -> Result<T, Error> {
18    let input = req.input.as_ref().ok_or(Error::MissingInput)?;
19    Ok(serde_json::from_value(struct_to_json(input))?)
20}
21
22/// Gets the observed composite resource (XR), if the request carries one.
23///
24/// Read it as a typed value with [`crate::resource::get`].
25pub fn observed_composite(req: &RunFunctionRequest) -> Option<&Resource> {
26    req.observed.as_ref()?.composite.as_ref()
27}
28
29/// Gets an observed composed resource by name.
30///
31/// Returns None when Crossplane has not observed the resource, for example
32/// before it is first created. Read it as a typed value with
33/// [`crate::resource::get`].
34pub fn observed_composed<'a>(req: &'a RunFunctionRequest, name: &str) -> Option<&'a Resource> {
35    req.observed.as_ref()?.resources.get(name)
36}
37
38/// Gets a function context value by key, if present.
39///
40/// See [`crate::context`] for well-known context keys.
41pub fn get_context_key(req: &RunFunctionRequest, key: &str) -> Option<serde_json::Value> {
42    req.context.as_ref()?.fields.get(key).map(pb_to_json)
43}
44
45/// Gets required resources by requirement name from the request.
46///
47/// Returns None when Crossplane has not (yet) resolved the requirement, and
48/// an empty Vec when it resolved the requirement but found no matches.
49/// Always declare requirements with [`crate::response::require_resources`];
50/// Crossplane considers them satisfied when they stabilize across calls.
51pub fn get_required_resources(
52    req: &RunFunctionRequest,
53    name: &str,
54) -> Option<Vec<serde_json::Value>> {
55    let resources = req.required_resources.get(name)?;
56    Some(
57        resources
58            .items
59            .iter()
60            .filter_map(|r| r.resource.as_ref())
61            .map(struct_to_json)
62            .collect(),
63    )
64}
65
66/// Gets a single required resource by requirement name from the request.
67///
68/// A convenience for requirements that match exactly one resource. Returns
69/// None when the requirement is unresolved or matched nothing.
70pub fn get_required_resource(req: &RunFunctionRequest, name: &str) -> Option<serde_json::Value> {
71    get_required_resources(req, name)?.into_iter().next()
72}
73
74/// Gets required resources by requirement name as typed values.
75///
76/// Returns `Ok(None)` when Crossplane has not (yet) resolved the
77/// requirement, and an empty Vec when it resolved the requirement but found
78/// no matches. Integral numbers are restored to integers first, as in
79/// [`crate::resource::get`], so integer fields of generated models
80/// deserialize.
81pub fn get_required_resources_as<T: serde::de::DeserializeOwned>(
82    req: &RunFunctionRequest,
83    name: &str,
84) -> Result<Option<Vec<T>>, Error> {
85    get_required_resources(req, name)
86        .map(|resources| resources.into_iter().map(serde_json::from_value).collect())
87        .transpose()
88        .map_err(Error::from)
89}
90
91/// Gets a single required resource by requirement name as a typed value.
92///
93/// Returns `Ok(None)` when the requirement is unresolved or matched nothing.
94pub fn get_required_resource_as<T: serde::de::DeserializeOwned>(
95    req: &RunFunctionRequest,
96    name: &str,
97) -> Result<Option<T>, Error> {
98    get_required_resource(req, name)
99        .map(serde_json::from_value)
100        .transpose()
101        .map_err(Error::from)
102}
103
104/// Gets the watched resource that triggered this operation, if any.
105///
106/// When a WatchOperation creates an Operation it injects the resource that
107/// changed under the requirement name [`WATCHED_RESOURCE_KEY`].
108pub fn get_watched_resource(req: &RunFunctionRequest) -> Option<serde_json::Value> {
109    get_required_resource(req, WATCHED_RESOURCE_KEY)
110}
111
112/// Gets the supplied credential data from the request, if any.
113pub fn get_credential_data<'a>(
114    req: &'a RunFunctionRequest,
115    name: &str,
116) -> Option<&'a CredentialData> {
117    match req.credentials.get(name)?.source.as_ref()? {
118        credentials::Source::CredentialData(data) => Some(data),
119    }
120}
121
122/// Checks whether Crossplane advertises its capabilities at all.
123///
124/// Crossplane v2.2 and later advertise capabilities in the request metadata.
125/// If this returns false the calling Crossplane predates capability
126/// advertisement, and [`has_capability`] returns false even for features
127/// that older Crossplane does support.
128pub fn advertises_capabilities(req: &RunFunctionRequest) -> bool {
129    has_capability(req, Capability::Capabilities)
130}
131
132/// Checks whether Crossplane advertises a particular capability.
133pub fn has_capability(req: &RunFunctionRequest, cap: Capability) -> bool {
134    req.meta
135        .as_ref()
136        .is_some_and(|m| m.capabilities().any(|c| c == cap))
137}
138
139/// Gets a required OpenAPI v3 schema by requirement name from the request.
140///
141/// Returns None both when the requirement is unresolved and when Crossplane
142/// resolved it but found no schema. To distinguish the two, check
143/// `req.required_schemas.contains_key(name)`.
144pub fn get_required_schema(req: &RunFunctionRequest, name: &str) -> Option<serde_json::Value> {
145    req.required_schemas
146        .get(name)?
147        .openapi_v3
148        .as_ref()
149        .map(struct_to_json)
150}