Skip to main content

function_sdk_rust/
response.rs

1//! Helpers for working with RunFunctionResponses.
2
3use std::collections::HashMap;
4use std::time::Duration;
5
6use serde::Serialize;
7
8use crate::Error;
9use crate::proto::v1::{
10    Condition, MatchLabels, Requirements, Resource, ResourceSelector, Result as FnResult,
11    RunFunctionRequest, RunFunctionResponse, SchemaSelector, Severity, Status, Target,
12    resource_selector,
13};
14use crate::resource::{json_to_pb, json_to_struct};
15
16/// The default TTL for which a RunFunctionResponse may be cached.
17pub const DEFAULT_TTL: Duration = Duration::from_secs(60);
18
19/// Creates a response to the supplied request.
20///
21/// The request's tag, desired state, and context are copied to the response.
22/// Crossplane deletes any previously desired field or resource that is not
23/// copied forward, so always start from this function rather than from an
24/// empty response.
25pub fn to(req: &RunFunctionRequest, ttl: Duration) -> RunFunctionResponse {
26    RunFunctionResponse {
27        meta: Some(crate::proto::v1::ResponseMeta {
28            tag: req.meta.as_ref().map(|m| m.tag.clone()).unwrap_or_default(),
29            ttl: Some(pbjson_types::Duration {
30                seconds: ttl.as_secs() as i64,
31                nanos: ttl.subsec_nanos() as i32,
32            }),
33        }),
34        desired: req.desired.clone(),
35        context: req.context.clone(),
36        ..Default::default()
37    }
38}
39
40/// Gets the desired composite resource (XR), inserting an empty one when
41/// the response has none.
42///
43/// A function may only set the desired XR's status; Crossplane ignores its
44/// desired metadata and spec. Write it with [`crate::resource::update_status`].
45pub fn desired_composite(rsp: &mut RunFunctionResponse) -> &mut Resource {
46    rsp.desired
47        .get_or_insert_default()
48        .composite
49        .get_or_insert_default()
50}
51
52/// Gets a desired composed resource by name, inserting an empty one when
53/// the response has none.
54///
55/// The resource is modified in place, so whatever earlier pipeline steps
56/// desired for it, copied forward by [`to`], is kept unless overwritten.
57/// Write it with [`crate::resource::update`].
58pub fn desired_composed(rsp: &mut RunFunctionResponse, name: impl Into<String>) -> &mut Resource {
59    rsp.desired
60        .get_or_insert_default()
61        .resources
62        .entry(name.into())
63        .or_default()
64}
65
66/// Adds a normal result to the response.
67///
68/// Returns the added result so a reason or target can be set on it:
69///
70/// ```
71/// # use function_sdk_rust::proto::v1::{RunFunctionResponse, Target};
72/// # use function_sdk_rust::response;
73/// # let mut rsp = RunFunctionResponse::default();
74/// let result = response::normal(&mut rsp, "created the bucket");
75/// result.reason = Some("CreatedBucket".to_string());
76/// result.target = Some(Target::CompositeAndClaim as i32);
77/// ```
78pub fn normal(rsp: &mut RunFunctionResponse, message: impl Into<String>) -> &mut FnResult {
79    add_result(rsp, Severity::Normal, message)
80}
81
82/// Adds a warning result to the response.
83pub fn warning(rsp: &mut RunFunctionResponse, message: impl Into<String>) -> &mut FnResult {
84    add_result(rsp, Severity::Warning, message)
85}
86
87/// Adds a fatal result to the response.
88///
89/// Crossplane stops the pipeline at the first fatal result: later steps do
90/// not run, and for that reconcile Crossplane applies no desired state and
91/// garbage-collects nothing. The reconcile error carries the message:
92/// `pipeline step "<step>" returned a fatal result: <message>`.
93pub fn fatal(rsp: &mut RunFunctionResponse, message: impl Into<String>) -> &mut FnResult {
94    add_result(rsp, Severity::Fatal, message)
95}
96
97fn add_result(
98    rsp: &mut RunFunctionResponse,
99    severity: Severity,
100    message: impl Into<String>,
101) -> &mut FnResult {
102    rsp.results.push(FnResult {
103        severity: severity as i32,
104        message: message.into(),
105        reason: None,
106        // Explicitly target the XR by default, like function-sdk-go does.
107        target: Some(Target::Composite as i32),
108    });
109    rsp.results.last_mut().expect("a result was just pushed")
110}
111
112/// Adds a True status condition to be applied to the XR.
113///
114/// Returns the added condition so a message or target can be set on it. Do
115/// not set the Ready condition type; Crossplane manages it from resource
116/// readiness.
117pub fn condition_true(
118    rsp: &mut RunFunctionResponse,
119    typ: impl Into<String>,
120    reason: impl Into<String>,
121) -> &mut Condition {
122    add_condition(rsp, Status::ConditionTrue, typ, reason)
123}
124
125/// Adds a False status condition to be applied to the XR.
126pub fn condition_false(
127    rsp: &mut RunFunctionResponse,
128    typ: impl Into<String>,
129    reason: impl Into<String>,
130) -> &mut Condition {
131    add_condition(rsp, Status::ConditionFalse, typ, reason)
132}
133
134/// Adds an Unknown status condition to be applied to the XR.
135pub fn condition_unknown(
136    rsp: &mut RunFunctionResponse,
137    typ: impl Into<String>,
138    reason: impl Into<String>,
139) -> &mut Condition {
140    add_condition(rsp, Status::ConditionUnknown, typ, reason)
141}
142
143fn add_condition(
144    rsp: &mut RunFunctionResponse,
145    status: Status,
146    typ: impl Into<String>,
147    reason: impl Into<String>,
148) -> &mut Condition {
149    rsp.conditions.push(Condition {
150        r#type: typ.into(),
151        status: status as i32,
152        reason: reason.into(),
153        message: None,
154        target: None,
155    });
156    rsp.conditions
157        .last_mut()
158        .expect("a condition was just pushed")
159}
160
161/// How a resource requirement matches resources.
162#[derive(Clone, Debug)]
163pub enum ResourceMatch {
164    /// Match the resource with this name.
165    Name(String),
166    /// Match all resources with these labels.
167    Labels(HashMap<String, String>),
168}
169
170/// Adds a resource requirement to the response.
171///
172/// Crossplane fetches the matching resources and calls the function again
173/// with them in `req.required_resources[name]`. Matching None selects all
174/// resources of the given API version and kind. Omit the namespace to match
175/// cluster scoped resources, or to match namespaced resources by labels
176/// across all namespaces.
177pub fn require_resources(
178    rsp: &mut RunFunctionResponse,
179    name: impl Into<String>,
180    api_version: impl Into<String>,
181    kind: impl Into<String>,
182    r#match: Option<ResourceMatch>,
183    namespace: Option<String>,
184) {
185    let selector = ResourceSelector {
186        api_version: api_version.into(),
187        kind: kind.into(),
188        r#match: r#match.map(|m| match m {
189            ResourceMatch::Name(name) => resource_selector::Match::MatchName(name),
190            ResourceMatch::Labels(labels) => {
191                resource_selector::Match::MatchLabels(MatchLabels { labels })
192            }
193        }),
194        namespace,
195    };
196    requirements(rsp).resources.insert(name.into(), selector);
197}
198
199/// Adds a schema requirement to the response.
200///
201/// Crossplane fetches the OpenAPI v3 schema for the resource kind and calls
202/// the function again with it in `req.required_schemas[name]`. Read it with
203/// [`crate::request::get_required_schema`].
204pub fn require_schema(
205    rsp: &mut RunFunctionResponse,
206    name: impl Into<String>,
207    api_version: impl Into<String>,
208    kind: impl Into<String>,
209) {
210    let selector = SchemaSelector {
211        api_version: api_version.into(),
212        kind: kind.into(),
213    };
214    requirements(rsp).schemas.insert(name.into(), selector);
215}
216
217fn requirements(rsp: &mut RunFunctionResponse) -> &mut Requirements {
218    rsp.requirements.get_or_insert_default()
219}
220
221/// Sets a function context value by key.
222///
223/// Crossplane passes the response's context to subsequent functions in the
224/// pipeline. [`to`] copies the request's context forward, so setting a key
225/// adds to what earlier pipeline steps wrote. See [`crate::context`] for
226/// well-known context keys.
227pub fn set_context_key<T: Serialize + ?Sized>(
228    rsp: &mut RunFunctionResponse,
229    key: impl Into<String>,
230    value: &T,
231) -> Result<(), Error> {
232    let v = serde_json::to_value(value)?;
233    rsp.context
234        .get_or_insert_default()
235        .fields
236        .insert(key.into(), json_to_pb(&v));
237    Ok(())
238}
239
240/// Sets the output of an operation function.
241///
242/// The source must serialize to a JSON object. Output is written to the
243/// Operation's status.pipeline field; XRs discard function output.
244pub fn set_output<T: Serialize + ?Sized>(
245    rsp: &mut RunFunctionResponse,
246    output: &T,
247) -> Result<(), Error> {
248    let value = serde_json::to_value(output)?;
249    let serde_json::Value::Object(map) = value else {
250        return Err(Error::NotAnObject);
251    };
252    rsp.output = Some(json_to_struct(&map));
253    Ok(())
254}