Skip to content

Commit 3fad2f5

Browse files
authored
Added Cloudwatch Event (#48)
motivation: support cloud watch events changes: * add aws region abstraction * add generic cloud watch abstraction * add common / key cloud watch events
1 parent 1685d89 commit 3fad2f5

File tree

7 files changed

+417
-3
lines changed

7 files changed

+417
-3
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAWSLambdaRuntime open source project
4+
//
5+
// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
// list all available regions using aws cli:
16+
// $ aws ssm get-parameters-by-path --path /aws/service/global-infrastructure/services/lambda/regions --output json
17+
18+
/// Enumeration of the AWS Regions.
19+
public enum AWSRegion: String, Codable {
20+
case ap_northeast_1 = "ap-northeast-1"
21+
case ap_northeast_2 = "ap-northeast-2"
22+
case ap_east_1 = "ap-east-1"
23+
case ap_southeast_1 = "ap-southeast-1"
24+
case ap_southeast_2 = "ap-southeast-2"
25+
case ap_south_1 = "ap-south-1"
26+
27+
case cn_north_1 = "cn-north-1"
28+
case cn_northwest_1 = "cn-northwest-1"
29+
30+
case eu_north_1 = "eu-north-1"
31+
case eu_west_1 = "eu-west-1"
32+
case eu_west_2 = "eu-west-2"
33+
case eu_west_3 = "eu-west-3"
34+
case eu_central_1 = "eu-central-1"
35+
36+
case us_east_1 = "us-east-1"
37+
case us_east_2 = "us-east-2"
38+
case us_west_1 = "us-west-1"
39+
case us_west_2 = "us-west-2"
40+
case us_gov_east_1 = "us-gov-east-1"
41+
case us_gov_west_1 = "us-gov-west-1"
42+
43+
case ca_central_1 = "ca-central-1"
44+
case sa_east_1 = "sa-east-1"
45+
case me_south_1 = "me-south-1"
46+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAWSLambdaRuntime open source project
4+
//
5+
// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import struct Foundation.Date
16+
17+
/// EventBridge has the same payloads/notification types as CloudWatch
18+
typealias EventBridge = Cloudwatch
19+
20+
public protocol CloudwatchDetail: Decodable {
21+
static var name: String { get }
22+
}
23+
24+
public extension CloudwatchDetail {
25+
var detailType: String {
26+
Self.name
27+
}
28+
}
29+
30+
public enum Cloudwatch {
31+
/// CloudWatch.Event is the outer structure of an event sent via CloudWatch Events.
32+
///
33+
/// **NOTE**: For examples of events that come via CloudWatch Events, see
34+
/// https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/EventTypes.html
35+
/// https://docs.aws.amazon.com/eventbridge/latest/userguide/event-types.html
36+
public struct Event<Detail: CloudwatchDetail>: Decodable {
37+
public let id: String
38+
public let source: String
39+
public let accountId: String
40+
public let time: Date
41+
public let region: AWSRegion
42+
public let resources: [String]
43+
public let detail: Detail
44+
45+
enum CodingKeys: String, CodingKey {
46+
case id
47+
case source
48+
case accountId = "account"
49+
case time
50+
case region
51+
case resources
52+
case detailType = "detail-type"
53+
case detail
54+
}
55+
56+
public init(from decoder: Decoder) throws {
57+
let container = try decoder.container(keyedBy: CodingKeys.self)
58+
59+
self.id = try container.decode(String.self, forKey: .id)
60+
self.source = try container.decode(String.self, forKey: .source)
61+
self.accountId = try container.decode(String.self, forKey: .accountId)
62+
self.time = (try container.decode(ISO8601Coding.self, forKey: .time)).wrappedValue
63+
self.region = try container.decode(AWSRegion.self, forKey: .region)
64+
self.resources = try container.decode([String].self, forKey: .resources)
65+
66+
let detailType = try container.decode(String.self, forKey: .detailType)
67+
guard detailType.lowercased() == Detail.name.lowercased() else {
68+
throw PayloadTypeMismatch(name: detailType, type: Detail.self)
69+
}
70+
71+
self.detail = try container.decode(Detail.self, forKey: .detail)
72+
}
73+
}
74+
75+
// MARK: - Common Event Types
76+
77+
public typealias ScheduledEvent = Event<Scheduled>
78+
public struct Scheduled: CloudwatchDetail {
79+
public static let name = "Scheduled Event"
80+
}
81+
82+
public enum EC2 {
83+
public typealias InstanceStateChangeNotificationEvent = Event<InstanceStateChangeNotification>
84+
public struct InstanceStateChangeNotification: CloudwatchDetail {
85+
public static let name = "EC2 Instance State-change Notification"
86+
87+
public enum State: String, Codable {
88+
case running
89+
case shuttingDown = "shutting-down"
90+
case stopped
91+
case stopping
92+
case terminated
93+
}
94+
95+
public let instanceId: String
96+
public let state: State
97+
98+
enum CodingKeys: String, CodingKey {
99+
case instanceId = "instance-id"
100+
case state
101+
}
102+
}
103+
104+
public typealias SpotInstanceInterruptionNoticeEvent = Event<SpotInstanceInterruptionNotice>
105+
public struct SpotInstanceInterruptionNotice: CloudwatchDetail {
106+
public static let name = "EC2 Spot Instance Interruption Warning"
107+
108+
public enum Action: String, Codable {
109+
case hibernate
110+
case stop
111+
case terminate
112+
}
113+
114+
public let instanceId: String
115+
public let action: Action
116+
117+
enum CodingKeys: String, CodingKey {
118+
case instanceId = "instance-id"
119+
case action = "instance-action"
120+
}
121+
}
122+
}
123+
124+
struct PayloadTypeMismatch: Error {
125+
let name: String
126+
let type: Any
127+
}
128+
}

Sources/AWSLambdaEvents/DateWrappers.swift

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,27 @@
1515
import struct Foundation.Date
1616
import class Foundation.ISO8601DateFormatter
1717

18+
@propertyWrapper
19+
public struct ISO8601Coding: Decodable {
20+
public let wrappedValue: Date
21+
22+
public init(wrappedValue: Date) {
23+
self.wrappedValue = wrappedValue
24+
}
25+
26+
public init(from decoder: Decoder) throws {
27+
let container = try decoder.singleValueContainer()
28+
let dateString = try container.decode(String.self)
29+
guard let date = Self.dateFormatter.date(from: dateString) else {
30+
throw DecodingError.dataCorruptedError(in: container, debugDescription:
31+
"Expected date to be in iso8601 date format, but `\(dateString)` does not forfill format")
32+
}
33+
self.wrappedValue = date
34+
}
35+
36+
private static let dateFormatter = ISO8601DateFormatter()
37+
}
38+
1839
@propertyWrapper
1940
public struct ISO8601WithFractionalSecondsCoding: Decodable {
2041
public let wrappedValue: Date
@@ -28,7 +49,7 @@ public struct ISO8601WithFractionalSecondsCoding: Decodable {
2849
let dateString = try container.decode(String.self)
2950
guard let date = Self.dateFormatter.date(from: dateString) else {
3051
throw DecodingError.dataCorruptedError(in: container, debugDescription:
31-
"Expected date to be in iso8601 date format with fractional seconds, but `\(dateString) does not forfill format`")
52+
"Expected date to be in iso8601 date format with fractional seconds, but `\(dateString)` does not forfill format")
3253
}
3354
self.wrappedValue = date
3455
}

Sources/AWSLambdaEvents/S3.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public enum S3 {
2020
public struct Record: Decodable {
2121
public let eventVersion: String
2222
public let eventSource: String
23-
public let awsRegion: String
23+
public let awsRegion: AWSRegion
2424

2525
@ISO8601WithFractionalSecondsCoding
2626
public var eventTime: Date
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAWSLambdaRuntime open source project
4+
//
5+
// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
@testable import AWSLambdaEvents
16+
import XCTest
17+
18+
class CloudwatchTests: XCTestCase {
19+
static func eventPayload(type: String, details: String) -> String {
20+
"""
21+
{
22+
"id": "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c",
23+
"detail-type": "\(type)",
24+
"source": "aws.events",
25+
"account": "123456789012",
26+
"time": "1970-01-01T00:00:00Z",
27+
"region": "us-east-1",
28+
"resources": [
29+
"arn:aws:events:us-east-1:123456789012:rule/ExampleRule"
30+
],
31+
"detail": \(details)
32+
}
33+
"""
34+
}
35+
36+
func testScheduledEventFromJSON() {
37+
let payload = CloudwatchTests.eventPayload(type: Cloudwatch.Scheduled.name, details: "{}")
38+
let data = payload.data(using: .utf8)!
39+
var maybeEvent: Cloudwatch.ScheduledEvent?
40+
XCTAssertNoThrow(maybeEvent = try JSONDecoder().decode(Cloudwatch.ScheduledEvent.self, from: data))
41+
42+
guard let event = maybeEvent else {
43+
return XCTFail("Expected to have an event")
44+
}
45+
46+
XCTAssertEqual(event.id, "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c")
47+
XCTAssertEqual(event.source, "aws.events")
48+
XCTAssertEqual(event.accountId, "123456789012")
49+
XCTAssertEqual(event.time, Date(timeIntervalSince1970: 0))
50+
XCTAssertEqual(event.region, .us_east_1)
51+
XCTAssertEqual(event.resources, ["arn:aws:events:us-east-1:123456789012:rule/ExampleRule"])
52+
}
53+
54+
func testEC2InstanceStateChangeNotificationEventFromJSON() {
55+
let payload = CloudwatchTests.eventPayload(type: Cloudwatch.EC2.InstanceStateChangeNotification.name,
56+
details: "{ \"instance-id\": \"0\", \"state\": \"stopping\" }")
57+
let data = payload.data(using: .utf8)!
58+
var maybeEvent: Cloudwatch.EC2.InstanceStateChangeNotificationEvent?
59+
XCTAssertNoThrow(maybeEvent = try JSONDecoder().decode(Cloudwatch.EC2.InstanceStateChangeNotificationEvent.self, from: data))
60+
61+
guard let event = maybeEvent else {
62+
return XCTFail("Expected to have an event")
63+
}
64+
65+
XCTAssertEqual(event.id, "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c")
66+
XCTAssertEqual(event.source, "aws.events")
67+
XCTAssertEqual(event.accountId, "123456789012")
68+
XCTAssertEqual(event.time, Date(timeIntervalSince1970: 0))
69+
XCTAssertEqual(event.region, .us_east_1)
70+
XCTAssertEqual(event.resources, ["arn:aws:events:us-east-1:123456789012:rule/ExampleRule"])
71+
XCTAssertEqual(event.detail.instanceId, "0")
72+
XCTAssertEqual(event.detail.state, .stopping)
73+
}
74+
75+
func testEC2SpotInstanceInterruptionNoticeEventFromJSON() {
76+
let payload = CloudwatchTests.eventPayload(type: Cloudwatch.EC2.SpotInstanceInterruptionNotice.name,
77+
details: "{ \"instance-id\": \"0\", \"instance-action\": \"terminate\" }")
78+
let data = payload.data(using: .utf8)!
79+
var maybeEvent: Cloudwatch.EC2.SpotInstanceInterruptionNoticeEvent?
80+
XCTAssertNoThrow(maybeEvent = try JSONDecoder().decode(Cloudwatch.EC2.SpotInstanceInterruptionNoticeEvent.self, from: data))
81+
82+
guard let event = maybeEvent else {
83+
return XCTFail("Expected to have an event")
84+
}
85+
86+
XCTAssertEqual(event.id, "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c")
87+
XCTAssertEqual(event.source, "aws.events")
88+
XCTAssertEqual(event.accountId, "123456789012")
89+
XCTAssertEqual(event.time, Date(timeIntervalSince1970: 0))
90+
XCTAssertEqual(event.region, .us_east_1)
91+
XCTAssertEqual(event.resources, ["arn:aws:events:us-east-1:123456789012:rule/ExampleRule"])
92+
XCTAssertEqual(event.detail.instanceId, "0")
93+
XCTAssertEqual(event.detail.action, .terminate)
94+
}
95+
96+
func testCustomEventFromJSON() {
97+
struct Custom: CloudwatchDetail {
98+
public static let name = "Custom"
99+
100+
let name: String
101+
}
102+
103+
let payload = CloudwatchTests.eventPayload(type: Custom.name, details: "{ \"name\": \"foo\" }")
104+
let data = payload.data(using: .utf8)!
105+
var maybeEvent: Cloudwatch.Event<Custom>?
106+
XCTAssertNoThrow(maybeEvent = try JSONDecoder().decode(Cloudwatch.Event<Custom>.self, from: data))
107+
108+
guard let event = maybeEvent else {
109+
return XCTFail("Expected to have an event")
110+
}
111+
112+
XCTAssertEqual(event.id, "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c")
113+
XCTAssertEqual(event.source, "aws.events")
114+
XCTAssertEqual(event.accountId, "123456789012")
115+
XCTAssertEqual(event.time, Date(timeIntervalSince1970: 0))
116+
XCTAssertEqual(event.region, .us_east_1)
117+
XCTAssertEqual(event.resources, ["arn:aws:events:us-east-1:123456789012:rule/ExampleRule"])
118+
XCTAssertEqual(event.detail.name, "foo")
119+
}
120+
121+
func testUnregistredType() {
122+
let payload = CloudwatchTests.eventPayload(type: UUID().uuidString, details: "{}")
123+
let data = payload.data(using: .utf8)!
124+
XCTAssertThrowsError(try JSONDecoder().decode(Cloudwatch.ScheduledEvent.self, from: data)) { error in
125+
XCTAssert(error is Cloudwatch.PayloadTypeMismatch, "expected PayloadTypeMismatch but received \(error)")
126+
}
127+
}
128+
129+
func testTypeMismatch() {
130+
let payload = CloudwatchTests.eventPayload(type: Cloudwatch.EC2.InstanceStateChangeNotification.name,
131+
details: "{ \"instance-id\": \"0\", \"state\": \"stopping\" }")
132+
let data = payload.data(using: .utf8)!
133+
XCTAssertThrowsError(try JSONDecoder().decode(Cloudwatch.ScheduledEvent.self, from: data)) { error in
134+
XCTAssert(error is Cloudwatch.PayloadTypeMismatch, "expected PayloadTypeMismatch but received \(error)")
135+
}
136+
}
137+
}

0 commit comments

Comments
 (0)