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}