cfn_custom_resource/
lib.rs

1//! A Rust create to facilitate the creation of Rust Lambda Powered Custom Resources
2//! for AWS Cloudformation. It does not cast an opinion on which aws lambda custom
3//! runtime that the function is executing in.
4//!
5//!
6//! A simple example is below:
7//!
8//! ```
9//! use cfn_custom_resource::CustomResourceEvent;
10//! use serde::Deserialize;
11//!
12//! #[derive(Debug, Deserialize)]
13//! #[serde(rename_all = "PascalCase")]
14//! struct MyParameters {
15//!     value_one: i64,
16//!     value_two: i64,
17//! }
18//!
19//! async fn my_handler_func(event: CustomResourceEvent<MyParameters>) {
20//!     match event {
21//!         CustomResourceEvent::Create(data) => {
22//!             println!(
23//!                 "{}",
24//!                 data.resource_properties.value_one + data.resource_properties.value_two
25//!             );
26//!             data.respond_with_success("all done")
27//!                 .finish()
28//!                 .await
29//!                 .unwrap();
30//!         }
31//!         CustomResourceEvent::Update(data) => {
32//!             println!("got an update");
33//!             data.respond_with_success("all done")
34//!                 .finish()
35//!                 .await
36//!                 .unwrap();
37//!         }
38//!         CustomResourceEvent::Delete(data) => {
39//!             println!("got a delete");
40//!             data.respond_with_success("all done")
41//!                 .finish()
42//!                 .await
43//!                 .unwrap();
44//!         }
45//!     }
46//! }
47//! ```
48
49#![deny(clippy::all, clippy::pedantic, clippy::nursery)]
50#![deny(
51    missing_docs,
52    missing_debug_implementations,
53    missing_copy_implementations,
54    trivial_casts,
55    trivial_numeric_casts,
56    unsafe_code,
57    unstable_features,
58    unused_import_braces,
59    unused_qualifications
60)]
61#![allow(clippy::must_use_candidate, clippy::used_underscore_binding)]
62
63use serde::{Deserialize, Serialize};
64use uuid::Uuid;
65
66use std::collections::HashMap;
67
68/// Defines the data that encapsualtes the payload sent by cloudformation to a lambda function.
69/// This includes all data in the payload except the inner event type. This structure is
70/// encapsulated by the [`CustomResourceEvent`](enum.CustomResourceEvent.html) that
71/// defines that event type as well.
72#[derive(Debug, Deserialize, PartialEq)]
73#[serde(rename_all = "PascalCase")]
74pub struct CloudformationPayload<T> {
75    /// A unique ID for the request.
76    ///
77    /// Combining the StackId with the RequestId forms a value that you can use to
78    /// uniquely identify a request on a particular custom resource.
79    pub request_id: String,
80
81    /// The response URL identifies a presigned S3 bucket that receives responses from the
82    /// custom resource provider to AWS CloudFormation.
83    #[serde(rename = "ResponseURL")]
84    pub response_url: String,
85
86    /// The template developer-chosen resource type of the custom resource in the AWS CloudFormation template.
87    /// Custom resource type names can be up to 60 characters long and can
88    /// include alphanumeric and the following characters: _@-.
89    pub resource_type: String,
90
91    /// The template developer-chosen name (logical ID) of the custom resource in the AWS CloudFormation template.
92    /// This is provided to facilitate communication between the custom resource provider and the template developer.
93    pub logical_resource_id: String,
94
95    /// The Amazon Resource Name (ARN) that identifies the stack that contains the custom resource.
96    ///
97    /// Combining the StackId with the RequestId forms a value that you can use to uniquely
98    /// identify a request on a particular custom resource.
99    pub stack_id: String,
100
101    /// A required custom resource provider-defined physical ID that is unique for that provider.
102    /// Always sent with Update and Delete requests; never sent with Create.
103    pub physical_resource_id: Option<Uuid>,
104
105    /// This field contains the contents of the Properties object sent by the template developer.
106    /// Its contents are defined by the custom resource provider (you) and therefore is generic to accomedate
107    /// any data set that you might have.
108    pub resource_properties: T,
109}
110
111impl<T> CloudformationPayload<T> {
112    /// Creates a response that indicates a failure.
113    pub fn respond_with_success(self, reason: &str) -> CustomResourceResponse {
114        CustomResourceResponse {
115            status: ResponseType::Success,
116            reason: reason.to_owned(),
117            physical_resource_id: self.physical_resource_id.unwrap_or_else(Uuid::new_v4),
118            stack_id: self.stack_id,
119            request_id: self.request_id,
120            logical_resource_id: self.logical_resource_id,
121            response_url: self.response_url,
122            no_echo: None,
123            data: None,
124        }
125    }
126
127    /// Creates a response that indicates a failure.
128    pub fn respond_with_failure(self, reason: &str) -> CustomResourceResponse {
129        CustomResourceResponse {
130            status: ResponseType::Failed,
131            reason: reason.to_owned(),
132            physical_resource_id: self.physical_resource_id.unwrap_or_else(Uuid::new_v4),
133            stack_id: self.stack_id,
134            request_id: self.request_id,
135            logical_resource_id: self.logical_resource_id,
136            response_url: self.response_url,
137            no_echo: None,
138            data: None,
139        }
140    }
141
142    /// Creates a response based on the request in question as well as the result of
143    /// some operation executed prior this. If the result is Ok(_), then the response
144    /// will be a success. If the response is Err(e), the response will be a failure
145    /// where the error message is the error string.
146    pub fn respond_with<D, E>(self, result: Result<D, E>) -> CustomResourceResponse
147    where
148        E: std::error::Error,
149    {
150        match result {
151            Ok(_) => self.respond_with_success("Success"),
152            Err(e) => self.respond_with_failure(&format!("{:?}", e)),
153        }
154    }
155}
156
157/// Defines an event payload that is sent to a lambda function by
158/// AWS Cloud Formation. This payload is defined by AWS as the following:
159///
160/// ```json
161/// {
162///   "RequestType" : "Delete",
163///   "RequestId" : "unique id for this delete request",
164///   "ResponseURL" : "pre-signed-url-for-delete-response",
165///   "ResourceType" : "Custom::MyCustomResourceType",
166///   "LogicalResourceId" : "name of resource in template",
167///   "StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid",
168///   "PhysicalResourceId" : "custom resource provider-defined physical id",
169///   "ResourceProperties" : {
170///      "key1" : "string",
171///      "key2" : [ "list" ],
172///      "key3" : { "key4" : "map" }
173///   }
174///}
175///```
176///
177/// The entire payload is the same for each custom resource except for the `ResourceProperties` section, so
178/// that is available as a generic type for which we are able to create the schema for the resource properties
179/// on your own.
180#[derive(Debug, Deserialize, PartialEq)]
181#[serde(tag = "RequestType")]
182pub enum CustomResourceEvent<T> {
183    /// Defines a cloudformation payload that happens on a create of the resource.
184    Create(CloudformationPayload<T>),
185    /// Defines a cloudforamtion payload that happens on ab update to the resource.
186    Update(CloudformationPayload<T>),
187    /// Defines a cloudforamtion payload that happens on the deletion the resource.
188    Delete(CloudformationPayload<T>),
189}
190
191#[derive(Debug, Serialize)]
192#[serde(rename_all = "UPPERCASE")]
193enum ResponseType {
194    Success,
195    Failed,
196}
197
198/// Define a response payload for the execution of a cloud formation custom resource data that is
199/// sent back to cloud formation after the process of executing the custom resource is complete.
200#[derive(Debug, Serialize)]
201#[serde(rename_all = "PascalCase")]
202pub struct CustomResourceResponse {
203    status: ResponseType,
204    reason: String,
205    physical_resource_id: Uuid,
206    stack_id: String,
207    request_id: String,
208    logical_resource_id: String,
209    no_echo: Option<bool>,
210    data: Option<HashMap<String, String>>,
211    #[serde(skip)]
212    response_url: String,
213}
214
215impl CustomResourceResponse {
216    /// Indicates whether to mask the output of the custom resource when retrieved by using the `Fn::GetAtt` function.
217    /// If set to true, all returned values are masked with asterisks (*****), except for those stored in the Metadata
218    /// section of the template. Cloud Formation does not transform, modify, or redact any information you include in
219    /// the Metadata section. The default value is false.
220    pub fn set_no_echo(mut self, value: bool) -> Self {
221        self.no_echo = Some(value);
222        self
223    }
224
225    /// Writes a new key value pair to the data section of the response.
226    ///
227    /// The custom resource provider-defined name-value pairs to send with the response.
228    /// You can access the values provided here by name in the template with `Fn::GetAtt`.
229    pub fn add_data(mut self, key: &str, value: &str) -> Self {
230        if self.data.is_none() {
231            self.data = Some(HashMap::new());
232        }
233
234        self.data
235            .as_mut()
236            .unwrap()
237            .insert(key.to_owned(), value.to_owned());
238        self
239    }
240
241    /// Sends the response object (self) to the response url of the custom resource.
242    ///
243    /// This finishes the cloudformation object change (create/update/delete).
244    ///
245    /// # Errors
246    /// When the request fails to execute the request
247    pub async fn finish(self) -> Result<(), reqwest::Error> {
248        let client = reqwest::Client::new();
249        client.put(&self.response_url).json(&self).send().await?;
250        Ok(())
251    }
252}