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