diff --git a/cloudformation/lamb-status.yml b/cloudformation/lamb-status.yml
index bc77b1c9..3335d6be 100644
--- a/cloudformation/lamb-status.yml
+++ b/cloudformation/lamb-status.yml
@@ -18,65 +18,117 @@ Mappings:
Version: 0.2.1
Resources:
LambdaRole:
- Type: "AWS::IAM::Role"
+ Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- - Effect: "Allow"
+ - Effect: Allow
Principal:
Service:
- - "lambda.amazonaws.com"
+ - lambda.amazonaws.com
Action:
- - "sts:AssumeRole"
- Path: "/"
- LambdaRoleInstanceProfile:
- Type: "AWS::IAM::InstanceProfile"
- Properties:
- Path: "/"
- Roles:
- - Ref: "LambdaRole"
- LambStatusIAMPolicy:
- Type: "AWS::IAM::Policy"
- Properties:
- PolicyName: "LambStatus"
- PolicyDocument:
- Statement:
- - Action:
- - "logs:CreateLogGroup"
- - "logs:CreateLogStream"
- - "logs:PutLogEvents"
- Effect: "Allow"
- Resource: "arn:aws:logs:*:*:*"
- - Effect: "Allow"
- Action: "dynamodb:*"
- Resource: !Sub |-
- arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${ServiceComponentTable}
- - Effect: "Allow"
- Action: "dynamodb:*"
- Resource: !Sub |-
- arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${IncidentTable}
- - Effect: "Allow"
- Action: "dynamodb:*"
- Resource: !Sub |-
- arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${IncidentUpdateTable}
- - Effect: "Allow"
- Action: "dynamodb:*"
- Resource: !Sub |-
- arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${MaintenanceTable}
- - Effect: "Allow"
- Action: "dynamodb:*"
- Resource: !Sub |-
- arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${MaintenanceUpdateTable}
- - Effect: "Allow"
- Action: "dynamodb:*"
- Resource: !Sub |-
- arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${MetricsTable}
- - Effect: "Allow"
- Action: "dynamodb:*"
- Resource: !Sub |-
- arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${SettingsTable}
- Roles:
- - Ref: "LambdaRole"
+ - sts:AssumeRole
+ Policies:
+ - PolicyName: CloudWatchLogs
+ PolicyDocument:
+ Statement:
+ - Action:
+ - logs:CreateLogGroup
+ - logs:CreateLogStream
+ - logs:PutLogEvents
+ Effect: Allow
+ Resource: arn:aws:logs:*:*:*
+ - PolicyName: DynamoDB
+ PolicyDocument:
+ Statement:
+ - Effect: Allow
+ Action: dynamodb:*
+ Resource: !Sub |-
+ arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${ServiceComponentTable}
+ - Effect: Allow
+ Action: dynamodb:*
+ Resource: !Sub |-
+ arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${IncidentTable}
+ - Effect: Allow
+ Action: dynamodb:*
+ Resource: !Sub |-
+ arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${IncidentUpdateTable}
+ - Effect: Allow
+ Action: dynamodb:*
+ Resource: !Sub |-
+ arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${MaintenanceTable}
+ - Effect: Allow
+ Action: dynamodb:*
+ Resource: !Sub |-
+ arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${MaintenanceUpdateTable}
+ - Effect: Allow
+ Action: dynamodb:*
+ Resource: !Sub |-
+ arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${MetricsTable}
+ - Effect: Allow
+ Action: dynamodb:*
+ Resource: !Sub |-
+ arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${SettingsTable}
+ - PolicyName: CloudFormation
+ PolicyDocument:
+ Statement:
+ - Effect: Allow
+ Action: cloudformation:DescribeStacks
+ Resource: !Sub |-
+ arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/${AWS::StackName}/*
+ - PolicyName: S3
+ PolicyDocument:
+ Statement:
+ - Effect: Allow
+ Action:
+ - s3:ListBucket
+ Resource:
+ - !Sub |-
+ arn:aws:s3:::${StatusPageS3}
+ - !Sub |-
+ arn:aws:s3:::${AdminPageS3}
+ - Effect: Allow
+ Action:
+ - s3:DeleteObject
+ - s3:PutObject
+ - s3:GetObject
+ Resource:
+ - !Sub |-
+ arn:aws:s3:::${StatusPageS3}/*
+ - !Sub |-
+ arn:aws:s3:::${AdminPageS3}/*
+ - PolicyName: CloudWatch
+ PolicyDocument:
+ Statement:
+ - Effect: Allow
+ Action:
+ - cloudwatch:GetMetricData
+ - cloudwatch:GetMetricStatistics
+ - cloudwatch:ListMetrics
+ Resource: "*"
+ - PolicyName: Cognito
+ PolicyDocument:
+ Statement:
+ - Effect: Allow
+ Action:
+ - cognito-idp:*
+ - iam:PassRole
+ Resource: '*'
+ - PolicyName: APIGateway
+ PolicyDocument:
+ Statement:
+ - Effect: Allow
+ Action:
+ - apigateway:POST
+ Resource: !Sub |-
+ arn:aws:apigateway:${AWS::Region}::/restapis/${RestApi}/deployments
+ - PolicyName: SNS
+ PolicyDocument:
+ Statement:
+ - Effect: Allow
+ Action: sns:publish
+ Resource:
+ Ref: IncidentNotificationTopic
GetComponentsLambdaFunction:
Type: "AWS::Lambda::Function"
Properties:
@@ -623,7 +675,7 @@ Resources:
MemorySize: 128
Role:
Fn::GetAtt:
- - CognitoHandleFunctionRole
+ - LambdaRole
- Arn
Runtime: "nodejs4.3"
Timeout: 30
@@ -656,7 +708,7 @@ Resources:
MemorySize: 128
Role:
Fn::GetAtt:
- - "MetricsFunctionRole"
+ - "LambdaRole"
- "Arn"
Runtime: "nodejs4.3"
Timeout: 30
@@ -788,7 +840,7 @@ Resources:
MemorySize: 128
Role:
Fn::GetAtt:
- - "MetricsFunctionRole"
+ - "LambdaRole"
- "Arn"
Runtime: "nodejs4.3"
Timeout: 30
@@ -821,7 +873,7 @@ Resources:
MemorySize: 128
Role:
Fn::GetAtt:
- - "MetricsFunctionRole"
+ - "LambdaRole"
- "Arn"
Runtime: "nodejs4.3"
Timeout: 30
@@ -854,7 +906,7 @@ Resources:
MemorySize: 512
Role:
Fn::GetAtt:
- - "MetricsFunctionRole"
+ - "LambdaRole"
- "Arn"
Runtime: "nodejs4.3"
Timeout: 60
@@ -869,54 +921,6 @@ Resources:
Principal: "events.amazonaws.com"
SourceArn: !Sub |-
arn:aws:events:${AWS::Region}:${AWS::AccountId}:rule/*
- MetricsFunctionRole:
- Type: AWS::IAM::Role
- Properties:
- AssumeRolePolicyDocument:
- Statement:
- - Effect: Allow
- Principal:
- Service:
- - lambda.amazonaws.com
- Action:
- - sts:AssumeRole
- Policies:
- - PolicyName: WriteS3
- PolicyDocument:
- Statement:
- - Effect: Allow
- Action:
- - s3:DeleteObject
- - s3:ListBucket
- - s3:PutObject
- - s3:GetObject
- Resource: arn:aws:s3:::*
- - PolicyName: WriteCloudWatchLogs
- PolicyDocument:
- Statement:
- - Effect: Allow
- Action:
- - logs:CreateLogGroup
- - logs:CreateLogStream
- - logs:PutLogEvents
- Resource: arn:aws:logs:*:*:*
- - PolicyName: ReadMetricsTable
- PolicyDocument:
- Statement:
- - Effect: "Allow"
- Action:
- - "dynamodb:*"
- Resource: !Sub |-
- arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${MetricsTable}
- - PolicyName: ReadMetricData
- PolicyDocument:
- Statement:
- - Effect: Allow
- Action:
- - cloudwatch:GetMetricData
- - cloudwatch:GetMetricStatistics
- - cloudwatch:ListMetrics
- Resource: "*"
CollectMetricsDataEvent:
Type: AWS::Events::Rule
Properties:
@@ -930,6 +934,52 @@ Resources:
Id: CollectMetricsDataFunction
Input: !Sub |-
{"StatusPageS3BucketName": "${StatusPageS3}"}
+ UpdateFeedsLambdaFunction:
+ Type: "AWS::Lambda::Function"
+ Properties:
+ Code:
+ S3Bucket: !Sub |-
+ lambstatus-${AWS::Region}
+ S3Key: !Sub
+ - fn/${Version}/UpdateFeeds.zip
+ - Version:
+ Fn::FindInMap: [ Constants, LambStatus, Version ]
+ Description: "Update feeds"
+ # The prefix of function name must be stack name
+ FunctionName: !Sub |-
+ ${AWS::StackName}-UpdateFeeds
+ Handler: "_apex_index.handle"
+ MemorySize: 128
+ Role:
+ Fn::GetAtt:
+ - "LambdaRole"
+ - "Arn"
+ Runtime: "nodejs4.3"
+ Timeout: 30
+ UpdateFeedsLambdaInvokePermission:
+ Type: "AWS::Lambda::Permission"
+ Properties:
+ FunctionName:
+ Fn::GetAtt:
+ - "UpdateFeedsLambdaFunction"
+ - "Arn"
+ Action: "lambda:InvokeFunction"
+ Principal: "sns.amazonaws.com"
+ SourceArn:
+ Ref: "IncidentNotificationTopic"
+ IncidentNotificationTopic:
+ Type: "AWS::SNS::Topic"
+ Properties:
+ TopicName: !Sub |-
+ ${AWS::StackName}-IncidentNotification
+ IncidentSubscriptionByUpdateFeeds:
+ Type: "AWS::SNS::Subscription"
+ Properties:
+ Endpoint: !Sub |-
+ ${UpdateFeedsLambdaFunction.Arn}
+ Protocol: "lambda"
+ TopicArn:
+ Ref: "IncidentNotificationTopic"
ApiDeployment:
Type: "AWS::ApiGateway::Deployment"
Properties:
@@ -2656,37 +2706,6 @@ Resources:
ProvisionedThroughput:
ReadCapacityUnits: "1"
WriteCapacityUnits: "1"
- S3HandleFunctionRole:
- Type: AWS::IAM::Role
- Properties:
- AssumeRolePolicyDocument:
- Statement:
- - Effect: Allow
- Principal:
- Service:
- - lambda.amazonaws.com
- Action:
- - sts:AssumeRole
- Policies:
- - PolicyName: WriteS3
- PolicyDocument:
- Statement:
- - Effect: Allow
- Action:
- - s3:DeleteObject
- - s3:ListBucket
- - s3:PutObject
- - s3:GetObject
- Resource: arn:aws:s3:::*
- - PolicyName: WriteCloudWatchLogs
- PolicyDocument:
- Statement:
- - Effect: Allow
- Action:
- - logs:CreateLogGroup
- - logs:CreateLogStream
- - logs:PutLogEvents
- Resource: arn:aws:logs:*:*:*
S3PutObjectFunction:
Type: AWS::Lambda::Function
Properties:
@@ -2704,7 +2723,7 @@ Resources:
Handler: "_apex_index.handle"
Role:
Fn::GetAtt:
- - S3HandleFunctionRole
+ - LambdaRole
- Arn
Runtime: nodejs4.3
Timeout: 30
@@ -2725,46 +2744,10 @@ Resources:
Handler: "_apex_index.handle"
Role:
Fn::GetAtt:
- - S3HandleFunctionRole
+ - LambdaRole
- Arn
Runtime: nodejs4.3
Timeout: 30
- CognitoHandleFunctionRole:
- Type: AWS::IAM::Role
- Properties:
- AssumeRolePolicyDocument:
- Statement:
- - Effect: Allow
- Principal:
- Service:
- - lambda.amazonaws.com
- Action:
- - sts:AssumeRole
- Policies:
- - PolicyName: WriteCloudWatchLogs
- PolicyDocument:
- Statement:
- - Effect: Allow
- Action:
- - logs:CreateLogGroup
- - logs:CreateLogStream
- - logs:PutLogEvents
- Resource: arn:aws:logs:*:*:*
- - PolicyName: CreateUserPool
- PolicyDocument:
- Statement:
- - Effect: Allow
- Action:
- - cognito-idp:*
- - iam:PassRole
- Resource: '*'
- - PolicyName: HandleSettings
- PolicyDocument:
- Statement:
- - Effect: "Allow"
- Action: "dynamodb:*"
- Resource: !Sub |-
- arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${SettingsTable}
CognitoCreateUserPoolFunction:
Type: AWS::Lambda::Function
Properties:
@@ -2782,7 +2765,7 @@ Resources:
Handler: "_apex_index.handle"
Role:
Fn::GetAtt:
- - CognitoHandleFunctionRole
+ - LambdaRole
- Arn
Runtime: nodejs4.3
Timeout: 30
@@ -2803,7 +2786,7 @@ Resources:
Handler: "_apex_index.handle"
Role:
Fn::GetAtt:
- - CognitoHandleFunctionRole
+ - LambdaRole
- Arn
Runtime: nodejs4.3
Timeout: 30
@@ -2824,7 +2807,7 @@ Resources:
Handler: "_apex_index.handle"
Role:
Fn::GetAtt:
- - CognitoHandleFunctionRole
+ - LambdaRole
- Arn
Runtime: nodejs4.3
Timeout: 30
@@ -2845,39 +2828,10 @@ Resources:
Handler: "_apex_index.handle"
Role:
Fn::GetAtt:
- - APIGatewayHandleFunctionRole
+ - LambdaRole
- Arn
Runtime: nodejs4.3
Timeout: 30
- APIGatewayHandleFunctionRole:
- Type: AWS::IAM::Role
- Properties:
- AssumeRolePolicyDocument:
- Statement:
- - Effect: Allow
- Principal:
- Service:
- - lambda.amazonaws.com
- Action:
- - sts:AssumeRole
- Policies:
- - PolicyName: WriteCloudWatchLogs
- PolicyDocument:
- Statement:
- - Effect: Allow
- Action:
- - logs:CreateLogGroup
- - logs:CreateLogStream
- - logs:PutLogEvents
- Resource: arn:aws:logs:*:*:*
- - PolicyName: APIGateway
- PolicyDocument:
- Statement:
- - Effect: Allow
- Action:
- - apigateway:POST
- Resource: !Sub |-
- arn:aws:apigateway:${AWS::Region}::/restapis/${RestApi}/deployments
DBCreateItemsLambdaFunction:
Type: "AWS::Lambda::Function"
Properties:
@@ -2896,7 +2850,7 @@ Resources:
MemorySize: 128
Role:
Fn::GetAtt:
- - CognitoHandleFunctionRole
+ - LambdaRole
- Arn
Runtime: "nodejs4.3"
Timeout: 30
@@ -3049,18 +3003,6 @@ Outputs:
LambdaRoleArn:
Value:
!GetAtt LambdaRole.Arn
- MetricsFunctionRoleArn:
- Value:
- !GetAtt MetricsFunctionRole.Arn
- S3HandleFunctionRoleArn:
- Value:
- !GetAtt S3HandleFunctionRole.Arn
- CognitoHandleFunctionRoleArn:
- Value:
- !GetAtt CognitoHandleFunctionRole.Arn
- APIGatewayHandleFunctionRoleArn:
- Value:
- !GetAtt APIGatewayHandleFunctionRole.Arn
AdminPageS3BucketURL:
Value:
Fn::GetAtt:
@@ -3097,3 +3039,6 @@ Outputs:
Version:
Value:
Fn::FindInMap: [ Constants, LambStatus, Version ]
+ IncidentNotificationTopic:
+ Value:
+ Ref: "IncidentNotificationTopic"
diff --git a/packages/frontend/src/components/statusPage/History/History.js b/packages/frontend/src/components/statusPage/History/History.js
index 80448552..fdbbab92 100644
--- a/packages/frontend/src/components/statusPage/History/History.js
+++ b/packages/frontend/src/components/statusPage/History/History.js
@@ -2,6 +2,7 @@ import React, { PropTypes } from 'react'
import classnames from 'classnames'
import ModestLink from 'components/common/ModestLink'
import Title from 'components/statusPage/Title'
+import SubscribeButton from 'components/statusPage/SubscribeButton'
import IncidentItem from 'components/statusPage/IncidentItem'
import MaintenanceItem from 'components/statusPage/MaintenanceItem'
import { getDateTimeFormat } from 'utils/datetime'
@@ -91,16 +92,21 @@ export default class History extends React.Component {
const events = incidents.concat(maintenances)
const eventsByMonth = this.renderEventsByMonth(events)
- return (
-
-
-
Incident History
-
-
- {eventsByMonth}
+ return (
+
+
+
+
+
+
+
Incident History
+
+
+ {eventsByMonth}
+
+
-
-
)
+ )
}
}
diff --git a/packages/frontend/src/components/statusPage/History/History.scss b/packages/frontend/src/components/statusPage/History/History.scss
index 8343c934..e5efb0b1 100644
--- a/packages/frontend/src/components/statusPage/History/History.scss
+++ b/packages/frontend/src/components/statusPage/History/History.scss
@@ -1,3 +1,9 @@
+.top {
+ display: flex;
+ justify-content: space-between;
+ width: 100%;
+}
+
.date_item {
height: auto;
}
diff --git a/packages/frontend/src/components/statusPage/Statuses/Statuses.js b/packages/frontend/src/components/statusPage/Statuses/Statuses.js
index 35c3d9da..281593bb 100644
--- a/packages/frontend/src/components/statusPage/Statuses/Statuses.js
+++ b/packages/frontend/src/components/statusPage/Statuses/Statuses.js
@@ -4,6 +4,7 @@ import Button from 'components/common/Button'
import ModestLink from 'components/common/ModestLink'
import MetricsGraph from 'components/common/MetricsGraph'
import Title from 'components/statusPage/Title'
+import SubscribeButton from 'components/statusPage/SubscribeButton'
import Components from 'components/statusPage/Components'
import Incidents from 'components/statusPage/Incidents'
import ScheduledMaintenances from 'components/statusPage/ScheduledMaintenances'
@@ -95,15 +96,20 @@ export default class Statuses extends React.Component {
)
}
- return (
-
- {components}
- {maintenances}
- {metricsTitle}
- {metricsContent}
- {incidents}
-
- )
+ return (
+
+
+
+
+
+ {components}
+ {maintenances}
+ {metricsTitle}
+ {metricsContent}
+ {incidents}
+
+
+ )
}
}
diff --git a/packages/frontend/src/components/statusPage/Statuses/Statuses.scss b/packages/frontend/src/components/statusPage/Statuses/Statuses.scss
index 53004346..1e3bce5c 100644
--- a/packages/frontend/src/components/statusPage/Statuses/Statuses.scss
+++ b/packages/frontend/src/components/statusPage/Statuses/Statuses.scss
@@ -1,3 +1,9 @@
+.top {
+ display: flex;
+ justify-content: space-between;
+ width: 100%;
+}
+
.title {
margin-top: 32px;
margin-bottom: -8px;
diff --git a/packages/frontend/src/components/statusPage/SubscribeButton/SubscribeButton.scss b/packages/frontend/src/components/statusPage/SubscribeButton/SubscribeButton.scss
new file mode 100644
index 00000000..cdefeeef
--- /dev/null
+++ b/packages/frontend/src/components/statusPage/SubscribeButton/SubscribeButton.scss
@@ -0,0 +1,10 @@
+.rss-icon {
+ margin-top: 28px;
+ margin-bottom: 16px;
+ height: 24px;
+ line-height: 24px;
+
+ border-radius: 20%;
+ background-color: #f57c00;
+ color: #fff;
+}
diff --git a/packages/frontend/src/components/statusPage/SubscribeButton/index.js b/packages/frontend/src/components/statusPage/SubscribeButton/index.js
new file mode 100644
index 00000000..047d7ab1
--- /dev/null
+++ b/packages/frontend/src/components/statusPage/SubscribeButton/index.js
@@ -0,0 +1,13 @@
+import React from 'react'
+import classnames from 'classnames'
+import classes from './SubscribeButton.scss'
+
+export default class SubscribeButton extends React.Component {
+ render () {
+ return (
+
+ rss_feed
+
+ )
+ }
+}
diff --git a/packages/lambda/bin/setup-apex.js b/packages/lambda/bin/setup-apex.js
index ac1eaf97..5f9a42fc 100644
--- a/packages/lambda/bin/setup-apex.js
+++ b/packages/lambda/bin/setup-apex.js
@@ -49,27 +49,6 @@ const createFunctionJSON = (role, timeout, memory, targetDirs) => {
console.log(`${dir}/function.json created`)
})
}
-const metricsFunctionRoleArn = getArn(awsResourceIDs, 'MetricsFunctionRoleArn')
-createFunctionJSON(metricsFunctionRoleArn, 60, 512, [
+createFunctionJSON(lambdaRoleArn, 60, 512, [
buildDir + '/functions/CollectMetricsData'
])
-createFunctionJSON(metricsFunctionRoleArn, 30, 128, [
- buildDir + '/functions/GetExternalMetrics'
-])
-const s3HandleFunctionRoleArn = getArn(awsResourceIDs, 'S3HandleFunctionRoleArn')
-createFunctionJSON(s3HandleFunctionRoleArn, 30, 128, [
- buildDir + '/functions/S3PutObject',
- buildDir + '/functions/S3SyncObjects'
-])
-const cognitoHandleFunctionRoleArn = getArn(awsResourceIDs, 'CognitoHandleFunctionRoleArn')
-createFunctionJSON(cognitoHandleFunctionRoleArn, 30, 128, [
- buildDir + '/functions/CognitoCreateUser',
- buildDir + '/functions/CognitoCreateUserPool',
- buildDir + '/functions/CognitoCreateUserPoolClient',
- buildDir + '/functions/DBCreateItems',
- buildDir + '/functions/PatchSettings'
-])
-const apiGatewayHandleFunctionRoleArn = getArn(awsResourceIDs, 'APIGatewayHandleFunctionRoleArn')
-createFunctionJSON(apiGatewayHandleFunctionRoleArn, 30, 128, [
- buildDir + '/functions/APIGatewayDeploy'
-])
diff --git a/packages/lambda/config/webpack.config.es6.js b/packages/lambda/config/webpack.config.es6.js
index 4c54d7c3..510540d5 100644
--- a/packages/lambda/config/webpack.config.es6.js
+++ b/packages/lambda/config/webpack.config.es6.js
@@ -128,6 +128,10 @@ export default {
DBCreateItems: [
'babel-polyfill',
'./src/api/dbCreateItems/index.js'
+ ],
+ UpdateFeeds: [
+ 'babel-polyfill',
+ './src/api/updateFeeds/index.js'
]
},
output: {
diff --git a/packages/lambda/package.json b/packages/lambda/package.json
index 07884f45..1a8cbb60 100644
--- a/packages/lambda/package.json
+++ b/packages/lambda/package.json
@@ -39,6 +39,7 @@
"json-loader": "^0.5.4",
"mime": "^1.3.4",
"mkdirp": "^0.5.1",
+ "moment": "^2.18.1",
"rimraf": "^2.5.4",
"verror": "^1.8.1",
"webpack": "^1.13.2"
diff --git a/packages/lambda/src/api/deleteIncidents/index.js b/packages/lambda/src/api/deleteIncidents/index.js
index 18215a9a..d33ad009 100644
--- a/packages/lambda/src/api/deleteIncidents/index.js
+++ b/packages/lambda/src/api/deleteIncidents/index.js
@@ -1,10 +1,13 @@
import { Incidents } from 'model/incidents'
+import SNS from 'aws/sns'
export async function handle (event, context, callback) {
try {
const incidents = new Incidents()
const incident = await incidents.lookup(event.params.incidentid)
await incident.delete()
+
+ await new SNS().notifyIncident(incident)
} catch (error) {
console.log(error.message)
console.log(error.stack)
diff --git a/packages/lambda/src/api/deleteMaintenances/index.js b/packages/lambda/src/api/deleteMaintenances/index.js
index 50a588d1..1e419e4b 100644
--- a/packages/lambda/src/api/deleteMaintenances/index.js
+++ b/packages/lambda/src/api/deleteMaintenances/index.js
@@ -1,10 +1,13 @@
import { Maintenances } from 'model/maintenances'
+import SNS from 'aws/sns'
export async function handle (event, context, callback) {
try {
const maintenances = new Maintenances()
const maintenance = await maintenances.lookup(event.params.maintenanceid)
await maintenance.delete()
+
+ await new SNS().notifyIncident(maintenance)
} catch (error) {
console.log(error.message)
console.log(error.stack)
diff --git a/packages/lambda/src/api/patchIncidents/index.js b/packages/lambda/src/api/patchIncidents/index.js
index 971be738..39c1664a 100644
--- a/packages/lambda/src/api/patchIncidents/index.js
+++ b/packages/lambda/src/api/patchIncidents/index.js
@@ -1,4 +1,5 @@
import { Incident } from 'model/incidents'
+import SNS from 'aws/sns'
export async function handle (event, context, callback) {
try {
@@ -7,6 +8,8 @@ export async function handle (event, context, callback) {
await incident.validate()
await incident.save()
+ await new SNS().notifyIncident(incident)
+
const obj = incident.objectify()
const comps = obj.components
delete obj.components
diff --git a/packages/lambda/src/api/patchMaintenances/index.js b/packages/lambda/src/api/patchMaintenances/index.js
index 9dda071e..59a21402 100644
--- a/packages/lambda/src/api/patchMaintenances/index.js
+++ b/packages/lambda/src/api/patchMaintenances/index.js
@@ -1,4 +1,5 @@
import { Maintenance } from 'model/maintenances'
+import SNS from 'aws/sns'
export async function handle (event, context, callback) {
try {
@@ -8,6 +9,8 @@ export async function handle (event, context, callback) {
await maintenance.validate()
await maintenance.save()
+ await new SNS().notifyIncident(maintenance)
+
const obj = maintenance.objectify()
const comps = obj.components
delete obj.components
diff --git a/packages/lambda/src/api/postIncidents/index.js b/packages/lambda/src/api/postIncidents/index.js
index 9829cb40..1a45b33c 100644
--- a/packages/lambda/src/api/postIncidents/index.js
+++ b/packages/lambda/src/api/postIncidents/index.js
@@ -1,4 +1,5 @@
import { Incident } from 'model/incidents'
+import SNS from 'aws/sns'
export async function handle (event, context, callback) {
try {
@@ -7,6 +8,8 @@ export async function handle (event, context, callback) {
await incident.validate()
await incident.save()
+ await new SNS().notifyIncident(incident)
+
const obj = incident.objectify()
const comps = obj.components
delete obj.components
diff --git a/packages/lambda/src/api/postMaintenances/index.js b/packages/lambda/src/api/postMaintenances/index.js
index 2ab75c80..06136451 100644
--- a/packages/lambda/src/api/postMaintenances/index.js
+++ b/packages/lambda/src/api/postMaintenances/index.js
@@ -1,4 +1,5 @@
import { Maintenance } from 'model/maintenances'
+import SNS from 'aws/sns'
export async function handle (event, context, callback) {
try {
@@ -7,6 +8,8 @@ export async function handle (event, context, callback) {
await maintenance.validate()
await maintenance.save()
+ await new SNS().notifyIncident(maintenance)
+
const obj = maintenance.objectify()
const comps = obj.components
delete obj.components
diff --git a/packages/lambda/src/api/updateFeeds/index.js b/packages/lambda/src/api/updateFeeds/index.js
new file mode 100644
index 00000000..c5b90bc2
--- /dev/null
+++ b/packages/lambda/src/api/updateFeeds/index.js
@@ -0,0 +1,87 @@
+import Feed from 'feed'
+import { Settings } from 'model/settings'
+import { Incidents } from 'model/incidents'
+import { Maintenances } from 'model/maintenances'
+import S3 from 'aws/s3'
+import CloudFormation from 'aws/cloudFormation'
+import { stackName } from 'utils/const'
+import { getDateTimeFormat } from 'utils/datetime'
+
+export async function handle (event, context, callback) {
+ try {
+ const settings = new Settings()
+ const statusPageURL = await settings.getStatusPageURL()
+ const serviceName = await settings.getServiceName()
+ const feed = new Feed({
+ id: `tag:${statusPageURL},2017:/history`,
+ link: statusPageURL,
+ title: `${serviceName} Status - Incident History`,
+ author: {
+ name: serviceName
+ }
+ })
+
+ let events = (await new Incidents().all()).concat(await new Maintenances().all())
+ events.sort(latestToOldest)
+ const maxItems = 25
+ for (let i = 0; i < maxItems; i++) {
+ feed.addItem(await buildItem(events[i], statusPageURL))
+ }
+
+ const { AWS_REGION: region } = process.env
+ const bucket = await new CloudFormation(stackName).getStatusPageBucketName()
+ const s3 = new S3()
+ await s3.putObject(region, bucket, 'history.atom', feed.atom1())
+ await s3.putObject(region, bucket, 'history.rss', feed.rss2())
+ callback(null)
+ } catch (error) {
+ console.log(error.message)
+ console.log(error.stack)
+ callback('Error: failed to update the feeds')
+ }
+}
+
+const latestToOldest = (a, b) => {
+ if (a.updatedAt < b.updatedAt) return 1
+ else if (b.updatedAt < a.updatedAt) return -1
+ return 0
+}
+
+const buildItem = async (event, statusPageURL) => {
+ let id, link, eventUpdates
+ if (event.hasOwnProperty('incidentID')) {
+ const incidentUpdates = await event.getIncidentUpdates()
+ incidentUpdates.sort(latestToOldest)
+
+ id = `tag:${statusPageURL},2017:Incident/${event.incidentID}`
+ link = `${statusPageURL}/incidents/${event.incidentID}`
+ eventUpdates = incidentUpdates.map(update => {
+ update.status = update.incidentStatus
+ return update
+ })
+ } else if (event.hasOwnProperty('maintenanceID')) {
+ const maintenanceUpdates = await event.getMaintenanceUpdates()
+ maintenanceUpdates.sort(latestToOldest)
+
+ id = `tag:${statusPageURL},2017:Maintenance/${event.maintenanceID}`
+ link = `${statusPageURL}/maintenances/${event.maintenanceID}`
+ eventUpdates = maintenanceUpdates.map(update => {
+ update.status = update.maintenanceStatus
+ return update
+ })
+ } else {
+ throw new Error('Unknown event: ', event)
+ }
+
+ const content = eventUpdates.map(update => {
+ return `
${getDateTimeFormat(update.updatedAt)}
${update.status} - ${update.message}
`
+ }).join('')
+ return {
+ id,
+ link,
+ content,
+ published: new Date(eventUpdates[0].updatedAt),
+ date: new Date(event.updatedAt),
+ title: event.name
+ }
+}
diff --git a/packages/lambda/src/aws/cloudFormation.js b/packages/lambda/src/aws/cloudFormation.js
new file mode 100644
index 00000000..77f299c7
--- /dev/null
+++ b/packages/lambda/src/aws/cloudFormation.js
@@ -0,0 +1,62 @@
+import AWS from 'aws-sdk'
+
+export default class CloudFormation {
+ constructor (stackName) {
+ const { AWS_REGION: region } = process.env
+ this.cloudFormation = new AWS.CloudFormation({ region })
+ this.stackName = stackName
+ }
+
+ describe () {
+ const params = { StackName: this.stackName }
+ return new Promise((resolve, reject) => {
+ this.cloudFormation.describeStacks(params, (error, data) => {
+ if (error) {
+ return reject(error)
+ }
+ if (!data) {
+ return reject(new Error('describeStacks returned no data'))
+ }
+ const { Stacks: stacks } = data
+ if (!stacks || stacks.length !== 1) {
+ return reject(new Error('describeStacks unexpected number of stacks'))
+ }
+ const stack = stacks[0]
+ resolve(stack)
+ })
+ })
+ }
+
+ async getOutputValue (key) {
+ if (!this.stack) {
+ this.stack = await this.describe()
+ }
+
+ let value
+ this.stack.Outputs.some(output => {
+ if (output.OutputKey === key) {
+ value = output.OutputValue
+ return true
+ }
+ })
+ if (!value) {
+ throw new Error(`failed to get output value (key: ${key}`)
+ }
+ return value
+ }
+
+ async getAdminPageBucketName () {
+ const key = 'AdminPageS3BucketName'
+ return await this.getOutputValue(key)
+ }
+
+ async getStatusPageBucketName () {
+ const key = 'StatusPageS3BucketName'
+ return await this.getOutputValue(key)
+ }
+
+ async getIncidentNotificationTopic () {
+ const key = 'IncidentNotificationTopic'
+ return await this.getOutputValue(key)
+ }
+}
diff --git a/packages/lambda/src/aws/sns.js b/packages/lambda/src/aws/sns.js
new file mode 100644
index 00000000..6b59f3c2
--- /dev/null
+++ b/packages/lambda/src/aws/sns.js
@@ -0,0 +1,31 @@
+import AWS from 'aws-sdk'
+import CloudFormation from 'aws/cloudFormation'
+import { stackName } from 'utils/const'
+
+export default class SNS {
+ constructor () {
+ const { AWS_REGION: region } = process.env
+ this.sns = new AWS.SNS({ apiVersion: '2010-03-31', region })
+ }
+
+ async notifyIncident (message) {
+ const topic = await new CloudFormation(stackName).getIncidentNotificationTopic()
+ return await this.publish(topic, message)
+ }
+
+ publish (topic, message) {
+ const params = {
+ Message: JSON.stringify({default: JSON.stringify(message)}),
+ MessageStructure: 'json',
+ TopicArn: topic
+ }
+ return new Promise((resolve, reject) => {
+ this.sns.publish(params, (error, data) => {
+ if (error) {
+ return reject(error)
+ }
+ resolve(data)
+ })
+ })
+ }
+}
diff --git a/packages/lambda/src/utils/const.js b/packages/lambda/src/utils/const.js
index 5f2c3cb4..f1ed4de5 100644
--- a/packages/lambda/src/utils/const.js
+++ b/packages/lambda/src/utils/const.js
@@ -1,4 +1,4 @@
-const stackName = process.env.AWS_LAMBDA_FUNCTION_NAME.replace(/-[^-]*$/, '')
+export const stackName = process.env.AWS_LAMBDA_FUNCTION_NAME.replace(/-[^-]*$/, '')
export const ServiceComponentTable = `${stackName}-ServiceComponentTable`
export const IncidentTable = `${stackName}-IncidentTable`
export const IncidentUpdateTable = `${stackName}-IncidentUpdateTable`
diff --git a/packages/lambda/src/utils/datetime.js b/packages/lambda/src/utils/datetime.js
new file mode 100644
index 00000000..2f5e3f53
--- /dev/null
+++ b/packages/lambda/src/utils/datetime.js
@@ -0,0 +1,5 @@
+import moment from 'moment'
+
+export const getDateTimeFormat = (datetime, fmt = 'MMM D, YYYY, HH:mm UTC') => {
+ return moment(datetime).format(fmt)
+}
diff --git a/packages/lambda/test/api/deleteMaintenances/index.js b/packages/lambda/test/api/deleteMaintenances/index.js
index 2dcd5b5b..70e20202 100644
--- a/packages/lambda/test/api/deleteMaintenances/index.js
+++ b/packages/lambda/test/api/deleteMaintenances/index.js
@@ -2,28 +2,34 @@ import assert from 'assert'
import sinon from 'sinon'
import { handle } from 'api/deleteMaintenances'
import { Maintenances, Maintenance } from 'model/maintenances'
+import SNS from 'aws/sns'
describe('deleteMaintenances', () => {
afterEach(() => {
Maintenance.prototype.delete.restore()
Maintenances.prototype.lookup.restore()
+ SNS.prototype.notifyIncident.restore()
})
it('should delete the maintenance', async () => {
const maint = new Maintenance('1', undefined, undefined, undefined, undefined, undefined, [], '1')
const lookupStub = sinon.stub(Maintenances.prototype, 'lookup').returns(maint)
const deleteStub = sinon.stub(Maintenance.prototype, 'delete').returns('')
+ const snsStub = sinon.stub(SNS.prototype, 'notifyIncident').returns()
await handle({ params: { maintenanceid: '1' } }, null, (error) => {
assert(error === null)
})
assert(lookupStub.calledOnce)
assert(deleteStub.calledOnce)
+ assert(snsStub.calledOnce)
})
it('should return error on exception thrown', async () => {
sinon.stub(Maintenances.prototype, 'lookup').throws()
- sinon.stub(Maintenance.prototype, 'delete').throws()
+ sinon.stub(Maintenance.prototype, 'delete').returns()
+ sinon.stub(SNS.prototype, 'notifyIncident').returns()
+
return await handle({ components: [] }, null, (error, result) => {
assert(error.match(/Error/))
})
diff --git a/packages/lambda/test/api/patchMaintenances/index.js b/packages/lambda/test/api/patchMaintenances/index.js
index 845ce5b4..e85edc8b 100644
--- a/packages/lambda/test/api/patchMaintenances/index.js
+++ b/packages/lambda/test/api/patchMaintenances/index.js
@@ -2,16 +2,19 @@ import assert from 'assert'
import sinon from 'sinon'
import { handle } from 'api/patchMaintenances'
import { Maintenance } from 'model/maintenances'
+import SNS from 'aws/sns'
describe('patchMaintenances', () => {
afterEach(() => {
Maintenance.prototype.validate.restore()
Maintenance.prototype.save.restore()
+ SNS.prototype.notifyIncident.restore()
})
it('should update the maintenance', async () => {
const validateStub = sinon.stub(Maintenance.prototype, 'validate').returns('')
const saveStub = sinon.stub(Maintenance.prototype, 'save').returns('')
+ const snsStub = sinon.stub(SNS.prototype, 'notifyIncident').returns()
await handle({ params: { maintenanceid: '1' }, body: { components: [] } }, null, (error, result) => {
assert(error === null)
@@ -21,11 +24,14 @@ describe('patchMaintenances', () => {
})
assert(validateStub.calledOnce)
assert(saveStub.calledOnce)
+ assert(snsStub.calledOnce)
})
it('should return error on exception thrown', async () => {
sinon.stub(Maintenance.prototype, 'validate').throws()
- sinon.stub(Maintenance.prototype, 'save').throws()
+ sinon.stub(Maintenance.prototype, 'save').returns()
+ sinon.stub(SNS.prototype, 'notifyIncident').returns()
+
return await handle({ params: { maintenanceid: '1' }, body: { components: [] } }, null, (error, result) => {
assert(error.match(/Error/))
})
diff --git a/packages/lambda/test/api/postMaintenances/index.js b/packages/lambda/test/api/postMaintenances/index.js
index 398ada03..3fa4b97d 100644
--- a/packages/lambda/test/api/postMaintenances/index.js
+++ b/packages/lambda/test/api/postMaintenances/index.js
@@ -2,16 +2,19 @@ import assert from 'assert'
import sinon from 'sinon'
import { handle } from 'api/postMaintenances'
import { Maintenance } from 'model/maintenances'
+import SNS from 'aws/sns'
describe('postMaintenances', () => {
afterEach(() => {
Maintenance.prototype.validate.restore()
Maintenance.prototype.save.restore()
+ SNS.prototype.notifyIncident.restore()
})
it('should update the maintenance', async () => {
const validateStub = sinon.stub(Maintenance.prototype, 'validate').returns('')
const saveStub = sinon.stub(Maintenance.prototype, 'save').returns('')
+ const snsStub = sinon.stub(SNS.prototype, 'notifyIncident').returns()
await handle({ components: [] }, null, (error, result) => {
assert(error === null)
@@ -21,11 +24,14 @@ describe('postMaintenances', () => {
})
assert(validateStub.calledOnce)
assert(saveStub.calledOnce)
+ assert(snsStub.calledOnce)
})
it('should return error on exception thrown', async () => {
sinon.stub(Maintenance.prototype, 'validate').throws()
- sinon.stub(Maintenance.prototype, 'save').throws()
+ sinon.stub(Maintenance.prototype, 'save').returns()
+ sinon.stub(SNS.prototype, 'notifyIncident').returns()
+
return await handle({ components: [] }, null, (error, result) => {
assert(error.match(/Error/))
})