Skip to content

Commit 29ddd63

Browse files
authored
Merge pull request #23 from fossamagna/add-slack-app
feat: add slack app as selection to receive amplify notification
2 parents e725f59 + 4a13420 commit 29ddd63

File tree

11 files changed

+342
-34
lines changed

11 files changed

+342
-34
lines changed

packages/amplify-category-console-notification/amplify-plugin.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,7 @@
77
"version",
88
"help"
99
],
10-
"eventHandlers": []
10+
"eventHandlers": [
11+
"PostPush"
12+
]
1113
}

packages/amplify-category-console-notification/resources/awscloudformation/cloudformation-templates/amplify-console-notification-cloudformation-template.json.ejs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,32 @@
131131
"Principal" : "sns.amazonaws.com",
132132
"SourceArn" : { "Ref" : "SNSTopic" }
133133
}
134-
}
134+
}<% if (props.useFunctionUrl) { %>,
135+
"FunctionUrl": {
136+
"Type" : "AWS::Lambda::Url",
137+
"Properties" : {
138+
"AuthType" : "NONE",
139+
"Cors" : {
140+
"AllowCredentials" : false,
141+
"AllowMethods" : [ "POST" ],
142+
"AllowOrigins" : [ "*" ]
143+
},
144+
"TargetFunctionArn" : {
145+
"Ref": "function<%= props.functionName %>Arn"
146+
}
147+
}
148+
},
149+
"PermissionForURLInvoke": {
150+
"Type": "AWS::Lambda::Permission",
151+
"Properties": {
152+
"Action": "lambda:InvokeFunctionUrl",
153+
"FunctionName": {
154+
"Ref": "function<%= props.functionName %>Arn"
155+
},
156+
"Principal": "*",
157+
"FunctionUrlAuthType": "NONE"
158+
}
159+
}<% } %>
135160
},
136161
"Outputs": {
137162
"SNSTopicArn": {
@@ -154,6 +179,10 @@
154179
"Value": {
155180
"Ref": "AWS::Region"
156181
}
182+
}<% if (props.useFunctionUrl) { %>,
183+
"FunctionUrl": {
184+
"Value": {"Fn::GetAtt": ["FunctionUrl", "FunctionUrl"]}
157185
}
186+
<% } %>
158187
}
159188
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { $TSContext, $TSMeta } from 'amplify-cli-core';
2+
import { category } from '../constants';
3+
4+
export const run = async (context: $TSContext) => {
5+
const previousFunctionUrlByServiceName = getFunctionUrlBySerivceName(context.exeInfo.amplifyMeta);
6+
const { outputsByCategory } = context.amplify.getResourceOutputs();
7+
Object.entries<Record<string, string>>(outputsByCategory[category])
8+
.filter(([_, output]) => !!output.LambdaFunctionUrl)
9+
.filter(([serviceName, output]) => previousFunctionUrlByServiceName[serviceName] !== output.LambdaFunctionUrl)
10+
.forEach(([serviceName, output]) => {
11+
showFunctionUrlInformation(context, serviceName, output.LambdaFunctionUrl);
12+
});
13+
};
14+
15+
function showFunctionUrlInformation(context: $TSContext, serviceName: string, functionUrl: string) {
16+
context.print.success(`Successfully added resource ${functionUrl}.`);
17+
context.print.info("");
18+
context.print.info('Next steps:');
19+
context.print.info("1. Open https://api.slack.com/apps in your browser.");
20+
context.print.info(`2. Update settings.interactivity.request_url with ${functionUrl} in Slack App Manifest for ${serviceName}.`);
21+
context.print.info("");
22+
}
23+
24+
function getFunctionUrlBySerivceName(amplifyMeta: $TSMeta): Record<string, string> {
25+
return Object.entries<Record<string, any>>(amplifyMeta.consolenotification)
26+
.reduce((all, [serviceName, service]) => {
27+
all[serviceName] = service.output.LambdaFunctionUrl;
28+
return all;
29+
}, {} as Record<string, string>);
30+
}

packages/amplify-category-console-notification/src/provider-utils/awscloudformation/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export interface Options {
66
providerPlugin: string;
77
functionName?: string;
88
webhookUrl?: string;
9+
useFunctionUrl?: boolean;
910
depensOn?: any;
1011
}
1112

Original file line numberDiff line numberDiff line change
@@ -1,60 +1,103 @@
1-
import { $TSContext, AmplifyCategories, AmplifySupportedService } from "amplify-cli-core";
2-
import { FunctionParameters } from 'amplify-function-plugin-interface';
1+
import {
2+
$TSContext,
3+
AmplifyCategories,
4+
AmplifySupportedService,
5+
open,
6+
} from "amplify-cli-core";
7+
import {
8+
FunctionParameters,
9+
} from "amplify-function-plugin-interface";
310
import { Options } from "..";
4-
import * as path from 'path';
11+
import * as path from "path";
12+
import inquirer from "inquirer";
513

6-
const templateFileName = 'amplify-console-notification-cloudformation-template.json.ejs';
14+
const templateFileName =
15+
"amplify-console-notification-cloudformation-template.json.ejs";
16+
17+
const amplifySlackAppTemplateName = "amplifyslackapp";
718

819
/**
920
* Starting point for CLI walkthrough that generates a Amplify Console Notification
1021
* @param context The Amplify Context object
1122
* @param category The resource category (should always be 'consolenotification')
1223
* @param options The options for Amplify Console Notification
1324
*/
14-
export async function createWalkthrough(context: $TSContext, category: string, options: Options) {
15-
const functionName = await addNewLambdaFunction(context);
25+
export async function createWalkthrough(
26+
context: $TSContext,
27+
category: string,
28+
options: Options
29+
) {
30+
const params = await askSlackAppType(context);
31+
options.useFunctionUrl = params.template === amplifySlackAppTemplateName;
32+
33+
if (options.useFunctionUrl) {
34+
await showCreateSlacAppSteps(context);
35+
}
36+
37+
const functionName = await addNewLambdaFunction(context, options, params);
1638
options.functionName = functionName;
1739

18-
await copyCfnTemplate(context, category, options);
40+
const resourceName = functionName; // use functionName as resourceName of consolenotification category
41+
42+
await copyCfnTemplate(context, category, resourceName, options);
1943

2044
const backendConfigs = {
2145
service: options.service,
22-
providerPlugin: 'awscloudformation',
46+
providerPlugin: options.providerPlugin,
2347
dependsOn: [
2448
{
2549
category: "function",
2650
resourceName: functionName,
27-
attributes: [
28-
"Arn"
29-
]
30-
}
51+
attributes: ["Arn"],
52+
},
3153
],
3254
};
33-
await context.amplify.updateamplifyMetaAfterResourceAdd(category, 'slack', backendConfigs);
55+
await context.amplify.updateamplifyMetaAfterResourceAdd(
56+
category,
57+
resourceName,
58+
backendConfigs
59+
);
60+
}
61+
62+
async function showCreateSlacAppSteps(context: $TSContext) {
63+
context.print.info("");
64+
context.print.info("Next Steps:");
65+
context.print.info("1. Create a Slack App following setup document.");
66+
context.print.info(" https://github.com/fossamagna/amplify-category-console-notification/blob/main/packages/amplify-slack-app/docs/SETUP.md")
67+
context.print.info("2. Provide an AWS Lambda function name for Slack App.");
68+
context.print.info("3. Input \"Signing Secret\" and \"Slack Bot User OAuth Token\" of Slack App to this prompt.");
69+
context.print.info("4. Input Slack Channel ID to send notification message.");
70+
context.print.info("5. Deploy Slack App as execute `amplify push` command.");
71+
context.print.info("6. Update settings.interactivity.request_url with FunctionUrl in Slack App Manifest.");
72+
context.print.info("");
73+
if (await context.amplify.confirmPrompt('Do you want to open setup document in your browser?', true)) {
74+
open("https://github.com/fossamagna/amplify-category-console-notification/blob/main/packages/amplify-slack-app/docs/SETUP.md", {});
75+
}
3476
}
3577

3678
function copyCfnTemplate(
3779
context: $TSContext,
3880
category: string,
81+
resourceName: string,
3982
options: Options
4083
) {
4184
const { amplify } = context;
4285
// @ts-ignore
4386
const targetDir = amplify.pathManager.getBackendDirPath();
44-
const pluginDir = path.join(__dirname, '..', '..', '..', '..');
87+
const pluginDir = path.join(__dirname, "..", "..", "..", "..");
4588

4689
const copyJobs = [
4790
{
4891
dir: pluginDir,
4992
template: `resources/awscloudformation/cloudformation-templates/${templateFileName}`,
50-
target: `${targetDir}/${category}/slack/slack-cloudformation-template.json`,
51-
paramsFile: path.join(targetDir, category, 'slack', 'parameters.json'),
93+
target: `${targetDir}/${category}/${resourceName}/slack-cloudformation-template.json`,
94+
paramsFile: path.join(targetDir, category, resourceName, "parameters.json"),
5295
},
5396
];
5497

5598
const params = {
56-
appId: getAppId(context)
57-
}
99+
appId: getAppId(context),
100+
};
58101

59102
// copy over the files
60103
// @ts-ignore
@@ -68,20 +111,48 @@ function getAppId(context: $TSContext) {
68111
}
69112
}
70113

71-
async function addNewLambdaFunction(context: $TSContext): Promise<string> {
72-
const params: Partial<FunctionParameters> = {
73-
defaultRuntime: 'nodejs',
74-
template: "webhook",
75-
};
76-
77-
const resourceName = await context.amplify.invokePluginMethod(context, AmplifyCategories.FUNCTION, undefined, 'add', [
114+
async function addNewLambdaFunction(
115+
context: $TSContext,
116+
options: Options,
117+
params: Partial<FunctionParameters>
118+
): Promise<string> {
119+
const resourceName = await context.amplify.invokePluginMethod(
78120
context,
79-
'awscloudformation',
80-
AmplifySupportedService.LAMBDA,
81-
params,
82-
]);
83-
84-
context.print.success(`Successfully added resource ${resourceName} locally`);
85-
121+
AmplifyCategories.FUNCTION,
122+
undefined,
123+
"add",
124+
[context, options.providerPlugin, AmplifySupportedService.LAMBDA, params]
125+
);
86126
return resourceName as string;
87127
}
128+
129+
async function askSlackAppType(context: $TSContext) {
130+
const templateProviders = context.pluginPlatform.plugins.functionTemplate;
131+
const selections = templateProviders
132+
.filter(
133+
(meta) =>
134+
meta.packageName === "amplify-slack-nodejs-function-template-provider"
135+
)
136+
.map(
137+
(meta) =>
138+
meta.manifest.functionTemplate.templates as {
139+
name: string;
140+
value: string;
141+
}[]
142+
)
143+
.reduce((all, value) => all.concat(value), []);
144+
const answer = await inquirer.prompt([
145+
{
146+
type: "list",
147+
name: "selection",
148+
message: "Select Slack App type",
149+
choices: selections,
150+
validate: (value) => !!value,
151+
},
152+
]);
153+
const params: Partial<FunctionParameters> = {
154+
defaultRuntime: "nodejs",
155+
template: answer.selection,
156+
};
157+
return params;
158+
}

packages/amplify-slack-nodejs-function-template-provider/amplify-plugin.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
{
1414
"name": "Amplify Console Build notification to Slack using Incoming Webhook",
1515
"value": "webhook"
16+
},
17+
{
18+
"name": "Slack App for Amplify Console Build notification and interactions",
19+
"value": "amplifyslackapp"
1620
}
1721
]
1822
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"Records": [
3+
{
4+
"EventSource": "aws:sns",
5+
"EventVersion": "1.0",
6+
"EventSubscriptionArn": "arn:aws:sns:ap-northeast-1:123456789012:amplify-appId_dev:3b84ba66-f0a2-4320-87d6-a01f8674974e",
7+
"Sns": {
8+
"Type": "Notification",
9+
"MessageId": "86886283-6134-460c-a0bf-e96404da803d",
10+
"TopicArn": "arn:aws:sns:ap-northeast-1:123456789012:amplify-appId_dev",
11+
"Subject": null,
12+
"Message": "\"Build notification from the AWS Amplify Console for app: https://dev.appId.amplifyapp.com/. Your build status is SUCCEED. Go to https://console.aws.amazon.com/amplify/home?region=ap-northeast-1#appId/dev/1 to view details on your build. \"",
13+
"Timestamp": "2019-10-24T06:33:19.479Z",
14+
"SignatureVersion": "1",
15+
"Signature": "signature",
16+
"SigningCertUrl": "https://sns.ap-northeast-1.amazonaws.com/SimpleNotificationService-xxx.pem",
17+
"UnsubscribeUrl": "https://sns.ap-northeast-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:ap-northeast-1:123456789012:amplify-appId_dev:3b84ba66-f0a2-4320-87d6-a01f8674974e",
18+
"MessageAttributes": {}
19+
}
20+
}
21+
]
22+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<%= props.topLevelComment %>
2+
3+
import { AmplifyConsoleSlackApp } from "amplify-slack-app";
4+
import { LogLevel } from "@slack/bolt";
5+
import { SSMClient, GetParametersCommand } from "@aws-sdk/client-ssm";
6+
7+
const getSecrets = async (names) => {
8+
const client = new SSMClient();
9+
const input = {
10+
Names: names,
11+
WithDecryption: true,
12+
};
13+
const command = new GetParametersCommand(input);
14+
const response = await client.send(command);
15+
return response.Parameters.reduce(
16+
(params, param) => ({
17+
...params,
18+
[param.Name]: param.Value,
19+
}),
20+
{}
21+
);
22+
};
23+
24+
const secrets = await getSecrets([
25+
process.env.SLACK_SIGNING_SECRET,
26+
process.env.SLACK_BOT_TOKEN,
27+
]);
28+
29+
const app = new AmplifyConsoleSlackApp({
30+
signingSecret: secrets[process.env.SLACK_SIGNING_SECRET],
31+
token: secrets[process.env.SLACK_BOT_TOKEN],
32+
defaultChannel: process.env.SLACK_DEFAULT_CHANNEL,
33+
logLevel: LogLevel.DEBUG,
34+
});
35+
36+
export const handler = app.createHandler();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "<%= props.functionName %>",
3+
"version": "1.0.0",
4+
"description": "Slack App for Amplify Console",
5+
"main": "index.js",
6+
"type": "module",
7+
"license": "Apache-2.0",
8+
"dependencies": {
9+
"@aws-sdk/client-ssm": "^3.150.0",
10+
"amplify-slack-app": "^1.1.0"
11+
}
12+
}

packages/amplify-slack-nodejs-function-template-provider/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { FunctionTemplateContributorFactory } from 'amplify-function-plugin-interface';
22
import { provideWehbook } from './providers/resolverWebhook';
3+
import { resolverAmplifySlackApp } from './providers/resolverAmplifySlackApp';
34

45
export const functionTemplateContributorFactory: FunctionTemplateContributorFactory = (context) => {
56
return {
@@ -8,6 +9,9 @@ export const functionTemplateContributorFactory: FunctionTemplateContributorFact
89
case 'webhook': {
910
return provideWehbook(context);
1011
}
12+
case 'amplifyslackapp': {
13+
return resolverAmplifySlackApp(context);
14+
}
1115
default: {
1216
throw new Error(`Unknown template selection [${request.selection}]`);
1317
}

0 commit comments

Comments
 (0)