diff --git a/lambda-events/Cargo.toml b/lambda-events/Cargo.toml index a21973b2..e401bfff 100644 --- a/lambda-events/Cargo.toml +++ b/lambda-events/Cargo.toml @@ -42,6 +42,7 @@ default = [ "autoscaling", "chime_bot", "clientvpn", + "cloudformation", "cloudwatch_events", "cloudwatch_logs", "code_commit", @@ -81,6 +82,7 @@ appsync = [] autoscaling = ["chrono"] chime_bot = ["chrono"] clientvpn = [] +cloudformation = [] cloudwatch_events = ["chrono"] cloudwatch_logs = ["flate2"] code_commit = ["chrono"] diff --git a/lambda-events/src/custom_serde/codebuild_time.rs b/lambda-events/src/custom_serde/codebuild_time.rs index 94d0e2f5..bd132b23 100644 --- a/lambda-events/src/custom_serde/codebuild_time.rs +++ b/lambda-events/src/custom_serde/codebuild_time.rs @@ -1,4 +1,4 @@ -use chrono::{DateTime, TimeZone, Utc}; +use chrono::{DateTime, NaiveDateTime, Utc}; use serde::ser::Serializer; use serde::{ de::{Deserializer, Error as DeError, Visitor}, @@ -18,7 +18,8 @@ impl<'de> Visitor<'de> for TimeVisitor { } fn visit_str(self, val: &str) -> Result { - Utc.datetime_from_str(val, CODEBUILD_TIME_FORMAT) + NaiveDateTime::parse_from_str(val, CODEBUILD_TIME_FORMAT) + .map(|naive| naive.and_utc()) .map_err(|e| DeError::custom(format!("Parse error {} for {}", e, val))) } } @@ -81,9 +82,9 @@ mod tests { "date": "Sep 1, 2017 4:12:29 PM" }); - let expected = Utc - .datetime_from_str("Sep 1, 2017 4:12:29 PM", CODEBUILD_TIME_FORMAT) - .unwrap(); + let expected = NaiveDateTime::parse_from_str("Sep 1, 2017 4:12:29 PM", CODEBUILD_TIME_FORMAT) + .unwrap() + .and_utc(); let decoded: Test = serde_json::from_value(data).unwrap(); assert_eq!(expected, decoded.date); } @@ -99,9 +100,9 @@ mod tests { "date": "Sep 1, 2017 4:12:29 PM" }); - let expected = Utc - .datetime_from_str("Sep 1, 2017 4:12:29 PM", CODEBUILD_TIME_FORMAT) - .unwrap(); + let expected = NaiveDateTime::parse_from_str("Sep 1, 2017 4:12:29 PM", CODEBUILD_TIME_FORMAT) + .unwrap() + .and_utc(); let decoded: Test = serde_json::from_value(data).unwrap(); assert_eq!(Some(expected), decoded.date); } diff --git a/lambda-events/src/event/cloudformation/mod.rs b/lambda-events/src/event/cloudformation/mod.rs new file mode 100644 index 00000000..e2d51745 --- /dev/null +++ b/lambda-events/src/event/cloudformation/mod.rs @@ -0,0 +1,143 @@ +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(tag = "RequestType")] +pub enum CloudFormationCustomResourceRequest +where + P1: DeserializeOwned + Serialize, + P2: DeserializeOwned + Serialize, +{ + #[serde(bound = "")] + Create(CreateRequest), + #[serde(bound = "")] + Update(UpdateRequest), + #[serde(bound = "")] + Delete(DeleteRequest), +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "PascalCase")] +#[serde(deny_unknown_fields)] +pub struct CreateRequest +where + P2: DeserializeOwned + Serialize, +{ + #[serde(default)] + pub service_token: Option, + pub request_id: String, + #[serde(rename = "ResponseURL")] + pub response_url: String, + pub stack_id: String, + pub resource_type: String, + pub logical_resource_id: String, + #[serde(bound = "")] + pub resource_properties: P2, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "PascalCase")] +#[serde(deny_unknown_fields)] +pub struct UpdateRequest +where + P1: DeserializeOwned + Serialize, + P2: DeserializeOwned + Serialize, +{ + #[serde(default)] + pub service_token: Option, + pub request_id: String, + #[serde(rename = "ResponseURL")] + pub response_url: String, + pub stack_id: String, + pub resource_type: String, + pub logical_resource_id: String, + pub physical_resource_id: String, + #[serde(bound = "")] + pub resource_properties: P2, + #[serde(bound = "")] + pub old_resource_properties: P1, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "PascalCase")] +#[serde(deny_unknown_fields)] +pub struct DeleteRequest +where + P2: DeserializeOwned + Serialize, +{ + #[serde(default)] + pub service_token: Option, + pub request_id: String, + #[serde(rename = "ResponseURL")] + pub response_url: String, + pub stack_id: String, + pub resource_type: String, + pub logical_resource_id: String, + pub physical_resource_id: String, + #[serde(bound = "")] + pub resource_properties: P2, +} + +#[cfg(test)] +mod test { + use std::collections::HashMap; + + use super::CloudFormationCustomResourceRequest::*; + use super::*; + + #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] + #[serde(rename_all = "PascalCase")] + #[serde(deny_unknown_fields)] + struct TestProperties { + key_1: String, + key_2: Vec, + key_3: HashMap, + } + + type TestRequest = CloudFormationCustomResourceRequest; + + #[test] + fn example_cloudformation_custom_resource_create_request() { + let data = include_bytes!("../../fixtures/example-cloudformation-custom-resource-create-request.json"); + let parsed: TestRequest = serde_json::from_slice(data).unwrap(); + + match parsed { + Create(_) => (), + _ => panic!("expected Create request"), + } + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: TestRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_cloudformation_custom_resource_update_request() { + let data = include_bytes!("../../fixtures/example-cloudformation-custom-resource-update-request.json"); + let parsed: TestRequest = serde_json::from_slice(data).unwrap(); + + match parsed { + Update(_) => (), + _ => panic!("expected Update request"), + } + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: TestRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_cloudformation_custom_resource_delete_request() { + let data = include_bytes!("../../fixtures/example-cloudformation-custom-resource-delete-request.json"); + let parsed: TestRequest = serde_json::from_slice(data).unwrap(); + + match parsed { + Delete(_) => (), + _ => panic!("expected Delete request"), + } + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: TestRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/mod.rs b/lambda-events/src/event/mod.rs index 1aa56697..4ce71dfc 100644 --- a/lambda-events/src/event/mod.rs +++ b/lambda-events/src/event/mod.rs @@ -25,6 +25,10 @@ pub mod chime_bot; #[cfg(feature = "clientvpn")] pub mod clientvpn; +/// AWS Lambda event definitions for cloudformation. +#[cfg(feature = "cloudformation")] +pub mod cloudformation; + /// CloudWatch Events payload #[cfg(feature = "cloudwatch_events")] pub mod cloudwatch_events; diff --git a/lambda-events/src/fixtures/example-cloudformation-custom-resource-create-request.json b/lambda-events/src/fixtures/example-cloudformation-custom-resource-create-request.json new file mode 100644 index 00000000..d35dd6f7 --- /dev/null +++ b/lambda-events/src/fixtures/example-cloudformation-custom-resource-create-request.json @@ -0,0 +1,14 @@ +{ + "ServiceToken": "arn:aws:lambda:eu-west-2:123456789012:function:custom-resource-handler", + "RequestType" : "Create", + "RequestId" : "82304eb2-bdda-469f-a33b-a3f1406d0a52", + "ResponseURL": "https://cr-response-bucket.s3.us-east-1.amazonaws.com/cr-response-key?sig-params=sig-values", + "StackId" : "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-name/16580499-7622-4a9c-b32f-4eba35da93da", + "ResourceType" : "Custom::MyCustomResourceType", + "LogicalResourceId" : "CustomResource", + "ResourceProperties" : { + "Key1" : "string", + "Key2" : [ "list" ], + "Key3" : { "Key4" : "map" } + } +} diff --git a/lambda-events/src/fixtures/example-cloudformation-custom-resource-delete-request.json b/lambda-events/src/fixtures/example-cloudformation-custom-resource-delete-request.json new file mode 100644 index 00000000..bd788c99 --- /dev/null +++ b/lambda-events/src/fixtures/example-cloudformation-custom-resource-delete-request.json @@ -0,0 +1,15 @@ +{ + "ServiceToken": "arn:aws:lambda:eu-west-2:123456789012:function:custom-resource-handler", + "RequestType" : "Delete", + "RequestId" : "ef70561d-d4ba-42a4-801b-33ad88dafc37", + "ResponseURL": "https://cr-response-bucket.s3.us-east-1.amazonaws.com/cr-response-key?sig-params=sig-values", + "StackId" : "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-name/16580499-7622-4a9c-b32f-4eba35da93da", + "ResourceType" : "Custom::MyCustomResourceType", + "LogicalResourceId" : "CustomResource", + "PhysicalResourceId" : "custom-resource-f4bd5382-3de3-4caf-b7ad-1be06b899647", + "ResourceProperties" : { + "Key1" : "string", + "Key2" : [ "list" ], + "Key3" : { "Key4" : "map" } + } +} diff --git a/lambda-events/src/fixtures/example-cloudformation-custom-resource-update-request.json b/lambda-events/src/fixtures/example-cloudformation-custom-resource-update-request.json new file mode 100644 index 00000000..4fc4378a --- /dev/null +++ b/lambda-events/src/fixtures/example-cloudformation-custom-resource-update-request.json @@ -0,0 +1,20 @@ +{ + "ServiceToken": "arn:aws:lambda:eu-west-2:123456789012:function:custom-resource-handler", + "RequestType" : "Update", + "RequestId" : "49347ca5-c603-44e5-a34b-10cf1854a887", + "ResponseURL": "https://cr-response-bucket.s3.us-east-1.amazonaws.com/cr-response-key?sig-params=sig-values", + "StackId" : "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-name/16580499-7622-4a9c-b32f-4eba35da93da", + "ResourceType" : "Custom::MyCustomResourceType", + "LogicalResourceId" : "CustomResource", + "PhysicalResourceId" : "custom-resource-f4bd5382-3de3-4caf-b7ad-1be06b899647", + "ResourceProperties" : { + "Key1" : "new-string", + "Key2" : [ "new-list" ], + "Key3" : { "Key4" : "new-map" } + }, + "OldResourceProperties" : { + "Key1" : "string", + "Key2" : [ "list" ], + "Key3" : { "Key4" : "map" } + } +} diff --git a/lambda-events/src/lib.rs b/lambda-events/src/lib.rs index 564debd7..7402a8f4 100644 --- a/lambda-events/src/lib.rs +++ b/lambda-events/src/lib.rs @@ -20,6 +20,7 @@ pub use event::activemq; /// AWS Lambda event definitions for alb. #[cfg(feature = "alb")] pub use event::alb; + /// AWS Lambda event definitions for apigw. #[cfg(feature = "apigw")] pub use event::apigw; @@ -40,6 +41,10 @@ pub use event::chime_bot; #[cfg(feature = "clientvpn")] pub use event::clientvpn; +/// AWS Lambda event definitions for cloudformation +#[cfg(feature = "cloudformation")] +pub use event::cloudformation; + /// CloudWatch Events payload #[cfg(feature = "cloudwatch_events")] pub use event::cloudwatch_events;