#![deny(missing_docs)]
extern crate failure;
extern crate futures;
extern crate reqwest;
#[macro_use]
extern crate serde;
#[cfg_attr(test, macro_use)]
extern crate serde_json;
use failure::Error;
use futures::{Future, IntoFuture};
use serde::de::{Deserialize, Deserializer};
use serde::ser::Serialize;
pub trait PhysicalResourceIdSuffixProvider {
fn physical_resource_id_suffix(&self) -> String;
}
impl<T> PhysicalResourceIdSuffixProvider for Option<T>
where
T: PhysicalResourceIdSuffixProvider,
{
fn physical_resource_id_suffix(&self) -> String {
match self {
Some(value) => value.physical_resource_id_suffix(),
None => String::new(),
}
}
}
impl PhysicalResourceIdSuffixProvider for () {
fn physical_resource_id_suffix(&self) -> String {
String::new()
}
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(tag = "RequestType")]
pub enum CfnRequest<P>
where
P: Clone,
{
#[serde(rename_all = "PascalCase")]
Create {
request_id: String,
#[serde(rename = "ResponseURL")]
response_url: String,
resource_type: String,
logical_resource_id: String,
stack_id: String,
resource_properties: P,
},
#[serde(rename_all = "PascalCase")]
Delete {
request_id: String,
#[serde(rename = "ResponseURL")]
response_url: String,
resource_type: String,
logical_resource_id: String,
stack_id: String,
physical_resource_id: String,
resource_properties: P,
},
#[serde(rename_all = "PascalCase")]
Update {
request_id: String,
#[serde(rename = "ResponseURL")]
response_url: String,
resource_type: String,
logical_resource_id: String,
stack_id: String,
physical_resource_id: String,
resource_properties: P,
old_resource_properties: P,
},
}
impl<P> CfnRequest<P>
where
P: PhysicalResourceIdSuffixProvider + Clone,
{
#[inline(always)]
pub fn request_id(&self) -> String {
match self {
CfnRequest::Create { request_id, .. } => request_id.to_owned(),
CfnRequest::Delete { request_id, .. } => request_id.to_owned(),
CfnRequest::Update { request_id, .. } => request_id.to_owned(),
}
}
#[inline(always)]
pub fn response_url(&self) -> String {
match self {
CfnRequest::Create { response_url, .. } => response_url.to_owned(),
CfnRequest::Delete { response_url, .. } => response_url.to_owned(),
CfnRequest::Update { response_url, .. } => response_url.to_owned(),
}
}
#[inline(always)]
pub fn resource_type(&self) -> String {
match self {
CfnRequest::Create { resource_type, .. } => resource_type.to_owned(),
CfnRequest::Delete { resource_type, .. } => resource_type.to_owned(),
CfnRequest::Update { resource_type, .. } => resource_type.to_owned(),
}
}
#[inline(always)]
pub fn logical_resource_id(&self) -> String {
match self {
CfnRequest::Create {
logical_resource_id,
..
} => logical_resource_id.to_owned(),
CfnRequest::Delete {
logical_resource_id,
..
} => logical_resource_id.to_owned(),
CfnRequest::Update {
logical_resource_id,
..
} => logical_resource_id.to_owned(),
}
}
#[inline(always)]
pub fn stack_id(&self) -> String {
match self {
CfnRequest::Create { stack_id, .. } => stack_id.to_owned(),
CfnRequest::Delete { stack_id, .. } => stack_id.to_owned(),
CfnRequest::Update { stack_id, .. } => stack_id.to_owned(),
}
}
#[inline(always)]
pub fn physical_resource_id(&self) -> String {
match self {
CfnRequest::Create {
logical_resource_id,
stack_id,
resource_properties,
..
}
| CfnRequest::Update {
logical_resource_id,
stack_id,
resource_properties,
..
} => {
let suffix = resource_properties.physical_resource_id_suffix();
format!(
"arn:custom:cfn-resource-provider:::{stack_id}-{logical_resource_id}{suffix_separator}{suffix}",
stack_id = stack_id.rsplit('/').next().expect("failed to get GUID from stack ID"),
logical_resource_id = logical_resource_id,
suffix_separator = if suffix.is_empty() { "" } else { "/" },
suffix = suffix,
)
}
CfnRequest::Delete {
physical_resource_id,
..
} => physical_resource_id.to_owned(),
}
}
#[inline(always)]
pub fn resource_properties(&self) -> &P {
match self {
CfnRequest::Create {
resource_properties,
..
} => resource_properties,
CfnRequest::Delete {
resource_properties,
..
} => resource_properties,
CfnRequest::Update {
resource_properties,
..
} => resource_properties,
}
}
pub fn into_response<S>(self, result: &Result<Option<S>, Error>) -> CfnResponse
where
S: Serialize,
{
match result {
Ok(data) => CfnResponse::Success {
request_id: self.request_id(),
logical_resource_id: self.logical_resource_id(),
stack_id: self.stack_id(),
physical_resource_id: self.physical_resource_id(),
no_echo: None,
data: data
.as_ref()
.and_then(|value| serde_json::to_value(value).ok()),
},
Err(e) => CfnResponse::Failed {
reason: format!("{}", e),
request_id: self.request_id(),
logical_resource_id: self.logical_resource_id(),
stack_id: self.stack_id(),
physical_resource_id: self.physical_resource_id(),
},
}
}
}
#[derive(Debug, Clone, Default, Copy, PartialEq)]
pub struct Ignored;
impl<'de> Deserialize<'de> for Ignored {
fn deserialize<D>(_deserializer: D) -> Result<Ignored, D::Error>
where
D: Deserializer<'de>,
{
Ok(Ignored)
}
}
impl PhysicalResourceIdSuffixProvider for Ignored {
fn physical_resource_id_suffix(&self) -> String {
String::new()
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(tag = "Status", rename_all = "SCREAMING_SNAKE_CASE")]
pub enum CfnResponse {
#[serde(rename_all = "PascalCase")]
Success {
request_id: String,
logical_resource_id: String,
stack_id: String,
physical_resource_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
no_echo: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
data: Option<serde_json::Value>,
},
#[serde(rename_all = "PascalCase")]
Failed {
reason: String,
request_id: String,
logical_resource_id: String,
stack_id: String,
physical_resource_id: String,
},
}
pub fn process<F, R, P, S>(
f: F,
) -> impl Fn(CfnRequest<P>) -> Box<dyn Future<Item = Option<S>, Error = Error> + Send>
where
F: Fn(CfnRequest<P>) -> R + Send + Sync + 'static,
R: IntoFuture<Item = Option<S>, Error = Error> + Send + 'static,
R::Future: Send,
S: Serialize + Send + 'static,
P: PhysicalResourceIdSuffixProvider + Clone + Send + 'static,
{
move |request: CfnRequest<P>| {
let response_url = request.response_url();
Box::new(f(request.clone()).into_future().then(|request_result| {
let cfn_response = request.into_response(&request_result);
serde_json::to_string(&cfn_response)
.map_err(Into::into)
.into_future()
.and_then(|cfn_response| {
reqwest::async::Client::builder()
.build()
.into_future()
.and_then(move |client| {
client
.put(&response_url)
.header("Content-Type", "")
.body(cfn_response)
.send()
})
.and_then(reqwest::async::Response::error_for_status)
.map_err(Into::into)
})
.and_then(move |_| request_result)
}))
}
}
#[cfg(test)]
mod test {
use super::*;
#[derive(Debug, Clone)]
struct Empty;
impl PhysicalResourceIdSuffixProvider for Empty {
fn physical_resource_id_suffix(&self) -> String {
String::new()
}
}
#[derive(Debug, Clone)]
struct StaticSuffix;
impl PhysicalResourceIdSuffixProvider for StaticSuffix {
fn physical_resource_id_suffix(&self) -> String {
"STATIC-SUFFIX".to_owned()
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "PascalCase")]
struct ExampleProperties {
example_property_1: String,
example_property_2: Option<bool>,
}
impl PhysicalResourceIdSuffixProvider for ExampleProperties {
fn physical_resource_id_suffix(&self) -> String {
self.example_property_1.to_owned()
}
}
#[test]
fn empty_suffix_has_no_trailing_slash() {
let request: CfnRequest<Empty> = CfnRequest::Create {
request_id: String::new(),
response_url: String::new(),
resource_type: String::new(),
logical_resource_id: String::new(),
stack_id: String::new(),
resource_properties: Empty,
};
assert!(!request.physical_resource_id().ends_with('/'));
}
#[test]
fn static_suffix_is_correctly_appended() {
let request: CfnRequest<StaticSuffix> = CfnRequest::Create {
request_id: String::new(),
response_url: String::new(),
resource_type: String::new(),
logical_resource_id: String::new(),
stack_id: String::new(),
resource_properties: StaticSuffix,
};
assert!(request.physical_resource_id().ends_with("/STATIC-SUFFIX"));
}
#[test]
fn cfnrequest_generic_type_required() {
let request: CfnRequest<Empty> = CfnRequest::Create {
request_id: String::new(),
response_url: String::new(),
resource_type: String::new(),
logical_resource_id: String::new(),
stack_id: String::new(),
resource_properties: Empty,
};
assert!(!request.physical_resource_id().is_empty());
}
#[test]
fn cfnrequest_generic_type_optional() {
let mut request: CfnRequest<Option<Empty>> = CfnRequest::Create {
request_id: String::new(),
response_url: String::new(),
resource_type: String::new(),
logical_resource_id: String::new(),
stack_id: String::new(),
resource_properties: None,
};
assert!(!request.physical_resource_id().is_empty());
assert!(!request.physical_resource_id().ends_with('/'));
request = CfnRequest::Create {
request_id: String::new(),
response_url: String::new(),
resource_type: String::new(),
logical_resource_id: String::new(),
stack_id: String::new(),
resource_properties: Some(Empty),
};
assert!(!request.physical_resource_id().is_empty());
assert!(!request.physical_resource_id().ends_with('/'));
}
#[test]
fn cfnrequest_generic_type_optional_unit() {
let mut request: CfnRequest<Option<()>> = CfnRequest::Create {
request_id: String::new(),
response_url: String::new(),
resource_type: String::new(),
logical_resource_id: String::new(),
stack_id: String::new(),
resource_properties: None,
};
assert!(!request.physical_resource_id().is_empty());
assert!(!request.physical_resource_id().ends_with('/'));
request = CfnRequest::Create {
request_id: String::new(),
response_url: String::new(),
resource_type: String::new(),
logical_resource_id: String::new(),
stack_id: String::new(),
resource_properties: Some(()),
};
assert!(!request.physical_resource_id().is_empty());
assert!(!request.physical_resource_id().ends_with('/'));
}
#[test]
fn cfnrequest_type_present() {
let expected_request: CfnRequest<ExampleProperties> = CfnRequest::Create {
request_id: "unique id for this create request".to_owned(),
response_url: "pre-signed-url-for-create-response".to_owned(),
resource_type: "Custom::MyCustomResourceType".to_owned(),
logical_resource_id: "name of resource in template".to_owned(),
stack_id: "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid".to_owned(),
resource_properties: ExampleProperties {
example_property_1: "example property 1".to_owned(),
example_property_2: None,
},
};
let actual_request: CfnRequest<ExampleProperties> = serde_json::from_value(json!({
"RequestType" : "Create",
"RequestId" : "unique id for this create request",
"ResponseURL" : "pre-signed-url-for-create-response",
"ResourceType" : "Custom::MyCustomResourceType",
"LogicalResourceId" : "name of resource in template",
"StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid",
"ResourceProperties": {
"ExampleProperty1": "example property 1"
}
}))
.unwrap();
assert_eq!(expected_request, actual_request);
}
#[test]
#[should_panic]
fn cfnrequest_type_absent() {
serde_json::from_value::<CfnRequest<ExampleProperties>>(json!({
"RequestType" : "Create",
"RequestId" : "unique id for this create request",
"ResponseURL" : "pre-signed-url-for-create-response",
"ResourceType" : "Custom::MyCustomResourceType",
"LogicalResourceId" : "name of resource in template",
"StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid"
}))
.unwrap();
}
#[test]
#[should_panic]
fn cfnrequest_type_malformed() {
serde_json::from_value::<CfnRequest<ExampleProperties>>(json!({
"RequestType" : "Create",
"RequestId" : "unique id for this create request",
"ResponseURL" : "pre-signed-url-for-create-response",
"ResourceType" : "Custom::MyCustomResourceType",
"LogicalResourceId" : "name of resource in template",
"StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid",
"ResourceProperties": {
"UnknownProperty": null
}
}))
.unwrap();
}
#[test]
fn cfnrequest_type_option_present() {
let expected_request: CfnRequest<Option<ExampleProperties>> = CfnRequest::Create {
request_id: "unique id for this create request".to_owned(),
response_url: "pre-signed-url-for-create-response".to_owned(),
resource_type: "Custom::MyCustomResourceType".to_owned(),
logical_resource_id: "name of resource in template".to_owned(),
stack_id: "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid".to_owned(),
resource_properties: Some(ExampleProperties {
example_property_1: "example property 1".to_owned(),
example_property_2: None,
}),
};
let actual_request: CfnRequest<Option<ExampleProperties>> = serde_json::from_value(json!({
"RequestType" : "Create",
"RequestId" : "unique id for this create request",
"ResponseURL" : "pre-signed-url-for-create-response",
"ResourceType" : "Custom::MyCustomResourceType",
"LogicalResourceId" : "name of resource in template",
"StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid",
"ResourceProperties": {
"ExampleProperty1": "example property 1"
}
}))
.unwrap();
assert_eq!(expected_request, actual_request);
}
#[test]
fn cfnrequest_type_option_absent() {
let expected_request: CfnRequest<Option<ExampleProperties>> = CfnRequest::Create {
request_id: "unique id for this create request".to_owned(),
response_url: "pre-signed-url-for-create-response".to_owned(),
resource_type: "Custom::MyCustomResourceType".to_owned(),
logical_resource_id: "name of resource in template".to_owned(),
stack_id: "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid".to_owned(),
resource_properties: None,
};
let actual_request: CfnRequest<Option<ExampleProperties>> = serde_json::from_value(json!({
"RequestType" : "Create",
"RequestId" : "unique id for this create request",
"ResponseURL" : "pre-signed-url-for-create-response",
"ResourceType" : "Custom::MyCustomResourceType",
"LogicalResourceId" : "name of resource in template",
"StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid"
}))
.unwrap();
assert_eq!(expected_request, actual_request);
}
#[test]
#[should_panic]
fn cfnrequest_type_option_malformed() {
serde_json::from_value::<CfnRequest<Option<ExampleProperties>>>(json!({
"RequestType" : "Create",
"RequestId" : "unique id for this create request",
"ResponseURL" : "pre-signed-url-for-create-response",
"ResourceType" : "Custom::MyCustomResourceType",
"LogicalResourceId" : "name of resource in template",
"StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid",
"ResourceProperties": {
"UnknownProperty": null
}
}))
.unwrap();
}
#[test]
fn cfnrequest_type_option_unit() {
let expected_request: CfnRequest<Option<()>> = CfnRequest::Create {
request_id: "unique id for this create request".to_owned(),
response_url: "pre-signed-url-for-create-response".to_owned(),
resource_type: "Custom::MyCustomResourceType".to_owned(),
logical_resource_id: "name of resource in template".to_owned(),
stack_id: "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid".to_owned(),
resource_properties: None,
};
let mut actual_request: CfnRequest<Option<()>> = serde_json::from_value(json!({
"RequestType" : "Create",
"RequestId" : "unique id for this create request",
"ResponseURL" : "pre-signed-url-for-create-response",
"ResourceType" : "Custom::MyCustomResourceType",
"LogicalResourceId" : "name of resource in template",
"StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid",
"ResourceProperties" : null
}))
.unwrap();
assert_eq!(expected_request, actual_request);
actual_request = serde_json::from_value(json!({
"RequestType" : "Create",
"RequestId" : "unique id for this create request",
"ResponseURL" : "pre-signed-url-for-create-response",
"ResourceType" : "Custom::MyCustomResourceType",
"LogicalResourceId" : "name of resource in template",
"StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid"
}))
.unwrap();
assert_eq!(expected_request, actual_request);
}
#[test]
#[should_panic]
fn cfnrequest_type_option_unit_data_provided() {
serde_json::from_value::<CfnRequest<Option<()>>>(json!({
"RequestType" : "Create",
"RequestId" : "unique id for this create request",
"ResponseURL" : "pre-signed-url-for-create-response",
"ResourceType" : "Custom::MyCustomResourceType",
"LogicalResourceId" : "name of resource in template",
"StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid",
"ResourceProperties" : {
"key1" : "string",
"key2" : [ "list" ],
"key3" : { "key4" : "map" }
}
}))
.unwrap();
}
#[test]
fn cfnrequest_type_ignored() {
let expected_request: CfnRequest<Ignored> = CfnRequest::Create {
request_id: "unique id for this create request".to_owned(),
response_url: "pre-signed-url-for-create-response".to_owned(),
resource_type: "Custom::MyCustomResourceType".to_owned(),
logical_resource_id: "name of resource in template".to_owned(),
stack_id: "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid".to_owned(),
resource_properties: Ignored,
};
let mut actual_request: CfnRequest<Ignored> = serde_json::from_value(json!({
"RequestType" : "Create",
"RequestId" : "unique id for this create request",
"ResponseURL" : "pre-signed-url-for-create-response",
"ResourceType" : "Custom::MyCustomResourceType",
"LogicalResourceId" : "name of resource in template",
"StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid",
"ResourceProperties" : {
"key1" : "string",
"key2" : [ "list" ],
"key3" : { "key4" : "map" }
}
}))
.unwrap();
assert_eq!(expected_request, actual_request);
actual_request = serde_json::from_value(json!({
"RequestType" : "Create",
"RequestId" : "unique id for this create request",
"ResponseURL" : "pre-signed-url-for-create-response",
"ResourceType" : "Custom::MyCustomResourceType",
"LogicalResourceId" : "name of resource in template",
"StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid"
}))
.unwrap();
assert_eq!(expected_request, actual_request);
}
#[test]
fn cfnrequest_create_example() {
#[derive(Debug, Clone, PartialEq, Deserialize)]
struct ExampleProperties {
key1: String,
key2: Vec<String>,
key3: serde_json::Value,
}
let expected_request = CfnRequest::Create {
request_id: "unique id for this create request".to_owned(),
response_url: "pre-signed-url-for-create-response".to_owned(),
resource_type: "Custom::MyCustomResourceType".to_owned(),
logical_resource_id: "name of resource in template".to_owned(),
stack_id: "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid".to_owned(),
resource_properties: ExampleProperties {
key1: "string".to_owned(),
key2: vec!["list".to_owned()],
key3: json!({ "key4": "map" }),
},
};
let actual_request = serde_json::from_value(json!({
"RequestType" : "Create",
"RequestId" : "unique id for this create request",
"ResponseURL" : "pre-signed-url-for-create-response",
"ResourceType" : "Custom::MyCustomResourceType",
"LogicalResourceId" : "name of resource in template",
"StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid",
"ResourceProperties" : {
"key1" : "string",
"key2" : [ "list" ],
"key3" : { "key4" : "map" }
}
}))
.unwrap();
assert_eq!(expected_request, actual_request);
}
#[test]
fn cfnresponse_create_success_example() {
let expected_response = json!({
"Status" : "SUCCESS",
"RequestId" : "unique id for this create request (copied from request)",
"LogicalResourceId" : "name of resource in template (copied from request)",
"StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid (copied from request)",
"PhysicalResourceId" : "required vendor-defined physical id that is unique for that vendor",
"Data" : {
"keyThatCanBeUsedInGetAtt1" : "data for key 1",
"keyThatCanBeUsedInGetAtt2" : "data for key 2"
}
});
let actual_response = serde_json::to_value(CfnResponse::Success {
request_id: "unique id for this create request (copied from request)".to_owned(),
logical_resource_id: "name of resource in template (copied from request)".to_owned(),
stack_id: "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid (copied from request)".to_owned(),
physical_resource_id: "required vendor-defined physical id that is unique for that vendor".to_owned(),
no_echo: None,
data: Some(json!({
"keyThatCanBeUsedInGetAtt1" : "data for key 1",
"keyThatCanBeUsedInGetAtt2" : "data for key 2"
})),
}).unwrap();
assert_eq!(expected_response, actual_response);
}
#[test]
fn cfnresponse_create_failed_example() {
let expected_response = json!({
"Status" : "FAILED",
"Reason" : "Required failure reason string",
"RequestId" : "unique id for this create request (copied from request)",
"LogicalResourceId" : "name of resource in template (copied from request)",
"StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid (copied from request)",
"PhysicalResourceId" : "required vendor-defined physical id that is unique for that vendor"
});
let actual_response = serde_json::to_value(CfnResponse::Failed {
reason: "Required failure reason string".to_owned(),
request_id: "unique id for this create request (copied from request)".to_owned(),
logical_resource_id: "name of resource in template (copied from request)".to_owned(),
stack_id: "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid (copied from request)".to_owned(),
physical_resource_id: "required vendor-defined physical id that is unique for that vendor".to_owned(),
}).unwrap();
assert_eq!(expected_response, actual_response);
}
#[test]
fn cfnrequest_delete_example() {
#[derive(Debug, PartialEq, Clone, Deserialize)]
struct ExampleProperties {
key1: String,
key2: Vec<String>,
key3: serde_json::Value,
}
let expected_request = CfnRequest::Delete {
request_id: "unique id for this delete request".to_owned(),
response_url: "pre-signed-url-for-delete-response".to_owned(),
resource_type: "Custom::MyCustomResourceType".to_owned(),
logical_resource_id: "name of resource in template".to_owned(),
stack_id: "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid".to_owned(),
physical_resource_id: "custom resource provider-defined physical id".to_owned(),
resource_properties: ExampleProperties {
key1: "string".to_owned(),
key2: vec!["list".to_owned()],
key3: json!({ "key4": "map" }),
},
};
let actual_request = serde_json::from_value(json!({
"RequestType" : "Delete",
"RequestId" : "unique id for this delete request",
"ResponseURL" : "pre-signed-url-for-delete-response",
"ResourceType" : "Custom::MyCustomResourceType",
"LogicalResourceId" : "name of resource in template",
"StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid",
"PhysicalResourceId" : "custom resource provider-defined physical id",
"ResourceProperties" : {
"key1" : "string",
"key2" : [ "list" ],
"key3" : { "key4" : "map" }
}
}))
.unwrap();
assert_eq!(expected_request, actual_request);
}
#[test]
fn cfnresponse_delete_success_example() {
let expected_response = json!({
"Status" : "SUCCESS",
"RequestId" : "unique id for this delete request (copied from request)",
"LogicalResourceId" : "name of resource in template (copied from request)",
"StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid (copied from request)",
"PhysicalResourceId" : "custom resource provider-defined physical id"
});
let actual_response = serde_json::to_value(CfnResponse::Success {
request_id: "unique id for this delete request (copied from request)".to_owned(),
logical_resource_id: "name of resource in template (copied from request)".to_owned(),
stack_id: "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid (copied from request)".to_owned(),
physical_resource_id: "custom resource provider-defined physical id".to_owned(),
no_echo: None,
data: None,
}).unwrap();
assert_eq!(expected_response, actual_response);
}
#[test]
fn cfnresponse_delete_failed_example() {
let expected_response = json!({
"Status" : "FAILED",
"Reason" : "Required failure reason string",
"RequestId" : "unique id for this delete request (copied from request)",
"LogicalResourceId" : "name of resource in template (copied from request)",
"StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid (copied from request)",
"PhysicalResourceId" : "custom resource provider-defined physical id"
});
let actual_response = serde_json::to_value(CfnResponse::Failed {
reason: "Required failure reason string".to_owned(),
request_id: "unique id for this delete request (copied from request)".to_owned(),
logical_resource_id: "name of resource in template (copied from request)".to_owned(),
stack_id: "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid (copied from request)".to_owned(),
physical_resource_id: "custom resource provider-defined physical id".to_owned(),
}).unwrap();
assert_eq!(expected_response, actual_response);
}
#[test]
fn cfnrequest_update_example() {
#[derive(Debug, PartialEq, Clone, Deserialize)]
struct ExampleProperties {
key1: String,
key2: Vec<String>,
key3: serde_json::Value,
}
let expected_request = CfnRequest::Update {
request_id: "unique id for this update request".to_owned(),
response_url: "pre-signed-url-for-update-response".to_owned(),
resource_type: "Custom::MyCustomResourceType".to_owned(),
logical_resource_id: "name of resource in template".to_owned(),
stack_id: "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid".to_owned(),
physical_resource_id: "custom resource provider-defined physical id".to_owned(),
resource_properties: ExampleProperties {
key1: "new-string".to_owned(),
key2: vec!["new-list".to_owned()],
key3: json!({ "key4": "new-map" }),
},
old_resource_properties: ExampleProperties {
key1: "string".to_owned(),
key2: vec!["list".to_owned()],
key3: json!({ "key4": "map" }),
},
};
let actual_request: CfnRequest<ExampleProperties> = serde_json::from_value(json!({
"RequestType" : "Update",
"RequestId" : "unique id for this update request",
"ResponseURL" : "pre-signed-url-for-update-response",
"ResourceType" : "Custom::MyCustomResourceType",
"LogicalResourceId" : "name of resource in template",
"StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid",
"PhysicalResourceId" : "custom resource provider-defined physical id",
"ResourceProperties" : {
"key1" : "new-string",
"key2" : [ "new-list" ],
"key3" : { "key4" : "new-map" }
},
"OldResourceProperties" : {
"key1" : "string",
"key2" : [ "list" ],
"key3" : { "key4" : "map" }
}
}))
.unwrap();
assert_eq!(expected_request, actual_request);
}
#[test]
fn cfnresponse_update_success_example() {
let expected_response = json!({
"Status" : "SUCCESS",
"RequestId" : "unique id for this update request (copied from request)",
"LogicalResourceId" : "name of resource in template (copied from request)",
"StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid (copied from request)",
"PhysicalResourceId" : "custom resource provider-defined physical id",
"Data" : {
"keyThatCanBeUsedInGetAtt1" : "data for key 1",
"keyThatCanBeUsedInGetAtt2" : "data for key 2"
}
});
let actual_response = serde_json::to_value(CfnResponse::Success {
request_id: "unique id for this update request (copied from request)".to_owned(),
logical_resource_id: "name of resource in template (copied from request)".to_owned(),
physical_resource_id: "custom resource provider-defined physical id".to_owned(),
stack_id: "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid (copied from request)".to_owned(),
no_echo: None,
data: Some(json!({
"keyThatCanBeUsedInGetAtt1" : "data for key 1",
"keyThatCanBeUsedInGetAtt2" : "data for key 2"
})),
}).unwrap();
assert_eq!(expected_response, actual_response);
}
#[test]
fn cfnresponse_update_failed_example() {
let expected_response = json!({
"Status" : "FAILED",
"Reason" : "Required failure reason string",
"RequestId" : "unique id for this update request (copied from request)",
"LogicalResourceId" : "name of resource in template (copied from request)",
"StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid (copied from request)",
"PhysicalResourceId" : "custom resource provider-defined physical id"
});
let actual_response = serde_json::to_value(CfnResponse::Failed {
reason: "Required failure reason string".to_owned(),
request_id: "unique id for this update request (copied from request)".to_owned(),
logical_resource_id: "name of resource in template (copied from request)".to_owned(),
stack_id: "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid (copied from request)".to_owned(),
physical_resource_id: "custom resource provider-defined physical id".to_owned(),
}).unwrap();
assert_eq!(expected_response, actual_response);
}
#[test]
fn cfnresponse_from_cfnrequest_unit() {
let actual_request: CfnRequest<Ignored> = CfnRequest::Create {
request_id: "unique id for this create request".to_owned(),
response_url: "pre-signed-url-for-create-response".to_owned(),
resource_type: "Custom::MyCustomResourceType".to_owned(),
logical_resource_id: "name of resource in template".to_owned(),
stack_id: "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid".to_owned(),
resource_properties: Ignored,
};
let actual_response =
serde_json::to_value(actual_request.into_response(&Ok(None::<()>))).unwrap();
let expected_response = json!({
"Status": "SUCCESS",
"RequestId": "unique id for this create request",
"LogicalResourceId": "name of resource in template",
"StackId": "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid",
"PhysicalResourceId": "arn:custom:cfn-resource-provider:::guid-name of resource in template"
});
assert_eq!(actual_response, expected_response)
}
#[test]
fn cfnresponse_from_cfnrequest_serializable() {
let actual_request: CfnRequest<Ignored> = CfnRequest::Create {
request_id: "unique id for this create request".to_owned(),
response_url: "pre-signed-url-for-create-response".to_owned(),
resource_type: "Custom::MyCustomResourceType".to_owned(),
logical_resource_id: "name of resource in template".to_owned(),
stack_id: "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid".to_owned(),
resource_properties: Ignored,
};
let actual_response =
serde_json::to_value(actual_request.into_response(&Ok(Some(ExampleProperties {
example_property_1: "example return property 1".to_owned(),
example_property_2: None,
}))))
.unwrap();
let expected_response = json!({
"Status": "SUCCESS",
"RequestId": "unique id for this create request",
"LogicalResourceId": "name of resource in template",
"StackId": "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid",
"PhysicalResourceId": "arn:custom:cfn-resource-provider:::guid-name of resource in template",
"Data": {
"ExampleProperty1": "example return property 1",
"ExampleProperty2": null,
}
});
assert_eq!(actual_response, expected_response)
}
}