-
Notifications
You must be signed in to change notification settings - Fork 113
Added Cloudwatch Event #48
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
b42f452
6b16b30
2ef1a42
fd09e25
fcc1a1d
198d7ae
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the SwiftAWSLambdaRuntime open source project | ||
// | ||
// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors | ||
// Licensed under Apache License v2.0 | ||
// | ||
// See LICENSE.txt for license information | ||
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
// list all available regions using aws cli: | ||
// $ aws ssm get-parameters-by-path --path /aws/service/global-infrastructure/services/lambda/regions --output json | ||
|
||
/// Enumeration of the AWS Regions. | ||
public enum AWSRegion: String, Codable { | ||
case ap_northeast_1 = "ap-northeast-1" | ||
case ap_northeast_2 = "ap-northeast-2" | ||
case ap_east_1 = "ap-east-1" | ||
case ap_southeast_1 = "ap-southeast-1" | ||
case ap_southeast_2 = "ap-southeast-2" | ||
case ap_south_1 = "ap-south-1" | ||
|
||
case cn_north_1 = "cn-north-1" | ||
case cn_northwest_1 = "cn-northwest-1" | ||
|
||
case eu_north_1 = "eu-north-1" | ||
case eu_west_1 = "eu-west-1" | ||
case eu_west_2 = "eu-west-2" | ||
case eu_west_3 = "eu-west-3" | ||
case eu_central_1 = "eu-central-1" | ||
|
||
case us_east_1 = "us-east-1" | ||
case us_east_2 = "us-east-2" | ||
case us_west_1 = "us-west-1" | ||
case us_west_2 = "us-west-2" | ||
case us_gov_east_1 = "us-gov-east-1" | ||
case us_gov_west_1 = "us-gov-west-1" | ||
|
||
case ca_central_1 = "ca-central-1" | ||
case sa_east_1 = "sa-east-1" | ||
case me_south_1 = "me-south-1" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the SwiftAWSLambdaRuntime open source project | ||
// | ||
// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors | ||
// Licensed under Apache License v2.0 | ||
// | ||
// See LICENSE.txt for license information | ||
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
import struct Foundation.Date | ||
|
||
/// EventBridge has the same payloads/notification types as CloudWatch | ||
typealias EventBridge = Cloudwatch | ||
|
||
public protocol CloudwatchDetail: Decodable { | ||
static var name: String { get } | ||
} | ||
|
||
public extension CloudwatchDetail { | ||
var detailType: String { | ||
Self.name | ||
} | ||
} | ||
|
||
public enum Cloudwatch { | ||
/// CloudWatch.Event is the outer structure of an event sent via CloudWatch Events. | ||
/// | ||
/// **NOTE**: For examples of events that come via CloudWatch Events, see | ||
/// https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/EventTypes.html | ||
/// https://docs.aws.amazon.com/eventbridge/latest/userguide/event-types.html | ||
public struct Event<Detail: CloudwatchDetail>: Decodable { | ||
public let id: String | ||
public let source: String | ||
public let accountId: String | ||
public let time: Date | ||
public let region: AWSRegion | ||
public let resources: [String] | ||
public let detail: Detail | ||
|
||
enum CodingKeys: String, CodingKey { | ||
case id | ||
case source | ||
case accountId = "account" | ||
case time | ||
case region | ||
case resources | ||
case detailType = "detail-type" | ||
case detail | ||
} | ||
|
||
public init(from decoder: Decoder) throws { | ||
let container = try decoder.container(keyedBy: CodingKeys.self) | ||
|
||
self.id = try container.decode(String.self, forKey: .id) | ||
self.source = try container.decode(String.self, forKey: .source) | ||
self.accountId = try container.decode(String.self, forKey: .accountId) | ||
self.time = (try container.decode(ISO8601Coding.self, forKey: .time)).wrappedValue | ||
self.region = try container.decode(AWSRegion.self, forKey: .region) | ||
self.resources = try container.decode([String].self, forKey: .resources) | ||
|
||
let detailType = try container.decode(String.self, forKey: .detailType) | ||
guard detailType == Detail.name else { | ||
throw PayloadTypeMismatch(name: detailType, type: Detail.self) | ||
} | ||
|
||
self.detail = try container.decode(Detail.self, forKey: .detail) | ||
} | ||
} | ||
|
||
// MARK: - Common Event Types | ||
|
||
public typealias ScheduledEvent = Event<Scheduled> | ||
public struct Scheduled: CloudwatchDetail { | ||
public static let name = "Scheduled Event" | ||
} | ||
|
||
public enum EC2 { | ||
public typealias InstanceStateChangeNotificationEvent = Event<InstanceStateChangeNotification> | ||
public struct InstanceStateChangeNotification: CloudwatchDetail { | ||
public static let name = "EC2 Instance State-change Notification" | ||
|
||
public enum State: String, Codable { | ||
case running | ||
case shuttingDown = "shutting-down" | ||
case stopped | ||
case stopping | ||
case terminated | ||
} | ||
|
||
public let instanceId: String | ||
public let state: State | ||
|
||
enum CodingKeys: String, CodingKey { | ||
case instanceId = "instance-id" | ||
case state | ||
} | ||
} | ||
|
||
public typealias SpotInstanceInterruptionNoticeEvent = Event<SpotInstanceInterruptionNotice> | ||
public struct SpotInstanceInterruptionNotice: CloudwatchDetail { | ||
public static let name = "EC2 Spot Instance Interruption Warning" | ||
|
||
public enum Action: String, Codable { | ||
case hibernate | ||
case stop | ||
case terminate | ||
} | ||
|
||
public let instanceId: String | ||
public let action: Action | ||
|
||
enum CodingKeys: String, CodingKey { | ||
case instanceId = "instance-id" | ||
case action = "instance-action" | ||
} | ||
} | ||
} | ||
|
||
struct UnknownPayload: Error { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. remove now? |
||
let name: String | ||
} | ||
|
||
struct PayloadTypeMismatch: Error { | ||
let name: String | ||
let type: Any | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,6 +15,27 @@ | |
import struct Foundation.Date | ||
import class Foundation.ISO8601DateFormatter | ||
|
||
@propertyWrapper | ||
public struct ISO8601Coding: Decodable { | ||
public let wrappedValue: Date | ||
|
||
public init(wrappedValue: Date) { | ||
self.wrappedValue = wrappedValue | ||
} | ||
|
||
public init(from decoder: Decoder) throws { | ||
let container = try decoder.singleValueContainer() | ||
let dateString = try container.decode(String.self) | ||
guard let date = Self.dateFormatter.date(from: dateString) else { | ||
throw DecodingError.dataCorruptedError(in: container, debugDescription: | ||
"Expected date to be in iso8601 date format, but `\(dateString)` does not forfill format") | ||
} | ||
self.wrappedValue = date | ||
} | ||
|
||
private static let dateFormatter = ISO8601DateFormatter() | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we need a unit test for this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Those are included in the test-events. So I don't think so? If you want me to I'm happy to provide extra tests though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. would nice to have a small unit test for each PW There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. tests added. |
||
@propertyWrapper | ||
public struct ISO8601WithFractionalSecondsCoding: Decodable { | ||
public let wrappedValue: Date | ||
|
@@ -28,7 +49,7 @@ public struct ISO8601WithFractionalSecondsCoding: Decodable { | |
let dateString = try container.decode(String.self) | ||
guard let date = Self.dateFormatter.date(from: dateString) else { | ||
throw DecodingError.dataCorruptedError(in: container, debugDescription: | ||
"Expected date to be in iso8601 date format with fractional seconds, but `\(dateString) does not forfill format`") | ||
"Expected date to be in iso8601 date format with fractional seconds, but `\(dateString)` does not forfill format") | ||
} | ||
self.wrappedValue = date | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the SwiftAWSLambdaRuntime open source project | ||
// | ||
// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors | ||
// Licensed under Apache License v2.0 | ||
// | ||
// See LICENSE.txt for license information | ||
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
@testable import AWSLambdaEvents | ||
import XCTest | ||
|
||
class CloudwatchTests: XCTestCase { | ||
static func eventPayload(type: String, details: String) -> String { | ||
""" | ||
{ | ||
"id": "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c", | ||
"detail-type": "\(type)", | ||
"source": "aws.events", | ||
"account": "123456789012", | ||
"time": "1970-01-01T00:00:00Z", | ||
"region": "us-east-1", | ||
"resources": [ | ||
"arn:aws:events:us-east-1:123456789012:rule/ExampleRule" | ||
], | ||
"detail": \(details) | ||
} | ||
""" | ||
} | ||
|
||
func testScheduledEventFromJSON() { | ||
let payload = CloudwatchTests.eventPayload(type: Cloudwatch.Scheduled.name, details: "{}") | ||
let data = payload.data(using: .utf8)! | ||
var maybeEvent: Cloudwatch.ScheduledEvent? | ||
XCTAssertNoThrow(maybeEvent = try JSONDecoder().decode(Cloudwatch.ScheduledEvent.self, from: data)) | ||
|
||
guard let event = maybeEvent else { | ||
return XCTFail("Expected to have an event") | ||
} | ||
|
||
XCTAssertEqual(event.id, "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c") | ||
XCTAssertEqual(event.source, "aws.events") | ||
XCTAssertEqual(event.accountId, "123456789012") | ||
XCTAssertEqual(event.time, Date(timeIntervalSince1970: 0)) | ||
XCTAssertEqual(event.region, .us_east_1) | ||
XCTAssertEqual(event.resources, ["arn:aws:events:us-east-1:123456789012:rule/ExampleRule"]) | ||
} | ||
|
||
func testEC2InstanceStateChangeNotificationEventFromJSON() { | ||
let payload = CloudwatchTests.eventPayload(type: Cloudwatch.EC2.InstanceStateChangeNotification.name, | ||
details: "{ \"instance-id\": \"0\", \"state\": \"stopping\" }") | ||
let data = payload.data(using: .utf8)! | ||
var maybeEvent: Cloudwatch.EC2.InstanceStateChangeNotificationEvent? | ||
XCTAssertNoThrow(maybeEvent = try JSONDecoder().decode(Cloudwatch.EC2.InstanceStateChangeNotificationEvent.self, from: data)) | ||
|
||
guard let event = maybeEvent else { | ||
return XCTFail("Expected to have an event") | ||
} | ||
|
||
XCTAssertEqual(event.id, "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c") | ||
XCTAssertEqual(event.source, "aws.events") | ||
XCTAssertEqual(event.accountId, "123456789012") | ||
XCTAssertEqual(event.time, Date(timeIntervalSince1970: 0)) | ||
XCTAssertEqual(event.region, .us_east_1) | ||
XCTAssertEqual(event.resources, ["arn:aws:events:us-east-1:123456789012:rule/ExampleRule"]) | ||
XCTAssertEqual(event.detail.instanceId, "0") | ||
XCTAssertEqual(event.detail.state, .stopping) | ||
} | ||
|
||
func testEC2SpotInstanceInterruptionNoticeEventFromJSON() { | ||
let payload = CloudwatchTests.eventPayload(type: Cloudwatch.EC2.SpotInstanceInterruptionNotice.name, | ||
details: "{ \"instance-id\": \"0\", \"instance-action\": \"terminate\" }") | ||
let data = payload.data(using: .utf8)! | ||
var maybeEvent: Cloudwatch.EC2.SpotInstanceInterruptionNoticeEvent? | ||
XCTAssertNoThrow(maybeEvent = try JSONDecoder().decode(Cloudwatch.EC2.SpotInstanceInterruptionNoticeEvent.self, from: data)) | ||
|
||
guard let event = maybeEvent else { | ||
return XCTFail("Expected to have an event") | ||
} | ||
|
||
XCTAssertEqual(event.id, "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c") | ||
XCTAssertEqual(event.source, "aws.events") | ||
XCTAssertEqual(event.accountId, "123456789012") | ||
XCTAssertEqual(event.time, Date(timeIntervalSince1970: 0)) | ||
XCTAssertEqual(event.region, .us_east_1) | ||
XCTAssertEqual(event.resources, ["arn:aws:events:us-east-1:123456789012:rule/ExampleRule"]) | ||
XCTAssertEqual(event.detail.instanceId, "0") | ||
XCTAssertEqual(event.detail.action, .terminate) | ||
} | ||
|
||
func testCustomEventFromJSON() { | ||
struct Custom: CloudwatchDetail { | ||
public static let name = "Custom" | ||
|
||
let name: String | ||
} | ||
|
||
let payload = CloudwatchTests.eventPayload(type: Custom.name, details: "{ \"name\": \"foo\" }") | ||
let data = payload.data(using: .utf8)! | ||
var maybeEvent: Cloudwatch.Event<Custom>? | ||
XCTAssertNoThrow(maybeEvent = try JSONDecoder().decode(Cloudwatch.Event<Custom>.self, from: data)) | ||
|
||
guard let event = maybeEvent else { | ||
return XCTFail("Expected to have an event") | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so I spoke with the XCTest team and they told me that XCTest has improved the way they handle errors so that throwing tests are no longer considered "harmful". their current recommendation is to make the test throwable and drop the XCTAssertNoThrow and do not use try/catch(XCTFail) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh hmmm, they only fixed in on Darwin, looking to get the fix ported to Linux too |
||
|
||
XCTAssertEqual(event.id, "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c") | ||
XCTAssertEqual(event.source, "aws.events") | ||
XCTAssertEqual(event.accountId, "123456789012") | ||
XCTAssertEqual(event.time, Date(timeIntervalSince1970: 0)) | ||
XCTAssertEqual(event.region, .us_east_1) | ||
XCTAssertEqual(event.resources, ["arn:aws:events:us-east-1:123456789012:rule/ExampleRule"]) | ||
XCTAssertEqual(event.detail.name, "foo") | ||
} | ||
|
||
func testUnregistredType() { | ||
let payload = CloudwatchTests.eventPayload(type: UUID().uuidString, details: "{}") | ||
let data = payload.data(using: .utf8)! | ||
XCTAssertThrowsError(try JSONDecoder().decode(Cloudwatch.ScheduledEvent.self, from: data)) { error in | ||
XCTAssert(error is Cloudwatch.PayloadTypeMismatch, "expected PayloadTypeMismatch but received \(error)") | ||
} | ||
} | ||
|
||
func testTypeMismatch() { | ||
let payload = CloudwatchTests.eventPayload(type: Cloudwatch.EC2.InstanceStateChangeNotification.name, | ||
details: "{ \"instance-id\": \"0\", \"state\": \"stopping\" }") | ||
let data = payload.data(using: .utf8)! | ||
XCTAssertThrowsError(try JSONDecoder().decode(Cloudwatch.ScheduledEvent.self, from: data)) { error in | ||
XCTAssert(error is Cloudwatch.PayloadTypeMismatch, "expected PayloadTypeMismatch but received \(error)") | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
make this case insensitive?