Skip to content

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

Merged
merged 6 commits into from
Mar 30, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions Sources/AWSLambdaEvents/AWSRegion.swift
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"
}
132 changes: 132 additions & 0 deletions Sources/AWSLambdaEvents/Cloudwatch.swift
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make this case insensitive?

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The 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
}
}
23 changes: 22 additions & 1 deletion Sources/AWSLambdaEvents/DateWrappers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need a unit test for this?

Copy link
Member Author

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would nice to have a small unit test for each PW

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tests added.

@propertyWrapper
public struct ISO8601WithFractionalSecondsCoding: Decodable {
public let wrappedValue: Date
Expand All @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/AWSLambdaEvents/S3.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public enum S3 {
public struct Record: Decodable {
public let eventVersion: String
public let eventSource: String
public let awsRegion: String
public let awsRegion: AWSRegion

@ISO8601WithFractionalSecondsCoding
public var eventTime: Date
Expand Down
137 changes: 137 additions & 0 deletions Tests/AWSLambdaEventsTests/CloudwatchTests.swift
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")
}
Copy link
Contributor

@tomerd tomerd Mar 25, 2020

Choose a reason for hiding this comment

The 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)

Copy link
Contributor

Choose a reason for hiding this comment

The 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)")
}
}
}
Loading