Skip to content

Commit

Permalink
Merge pull request #23 from fossamagna/add-slack-app
Browse files Browse the repository at this point in the history
feat: add slack app as selection to receive amplify notification
  • Loading branch information
fossamagna authored Sep 1, 2022
2 parents e725f59 + 4a13420 commit 29ddd63
Show file tree
Hide file tree
Showing 11 changed files with 342 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@
"version",
"help"
],
"eventHandlers": []
"eventHandlers": [
"PostPush"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,32 @@
"Principal" : "sns.amazonaws.com",
"SourceArn" : { "Ref" : "SNSTopic" }
}
}
}<% if (props.useFunctionUrl) { %>,
"FunctionUrl": {
"Type" : "AWS::Lambda::Url",
"Properties" : {
"AuthType" : "NONE",
"Cors" : {
"AllowCredentials" : false,
"AllowMethods" : [ "POST" ],
"AllowOrigins" : [ "*" ]
},
"TargetFunctionArn" : {
"Ref": "function<%= props.functionName %>Arn"
}
}
},
"PermissionForURLInvoke": {
"Type": "AWS::Lambda::Permission",
"Properties": {
"Action": "lambda:InvokeFunctionUrl",
"FunctionName": {
"Ref": "function<%= props.functionName %>Arn"
},
"Principal": "*",
"FunctionUrlAuthType": "NONE"
}
}<% } %>
},
"Outputs": {
"SNSTopicArn": {
Expand All @@ -154,6 +179,10 @@
"Value": {
"Ref": "AWS::Region"
}
}<% if (props.useFunctionUrl) { %>,
"FunctionUrl": {
"Value": {"Fn::GetAtt": ["FunctionUrl", "FunctionUrl"]}
}
<% } %>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { $TSContext, $TSMeta } from 'amplify-cli-core';
import { category } from '../constants';

export const run = async (context: $TSContext) => {
const previousFunctionUrlByServiceName = getFunctionUrlBySerivceName(context.exeInfo.amplifyMeta);
const { outputsByCategory } = context.amplify.getResourceOutputs();
Object.entries<Record<string, string>>(outputsByCategory[category])
.filter(([_, output]) => !!output.LambdaFunctionUrl)
.filter(([serviceName, output]) => previousFunctionUrlByServiceName[serviceName] !== output.LambdaFunctionUrl)
.forEach(([serviceName, output]) => {
showFunctionUrlInformation(context, serviceName, output.LambdaFunctionUrl);
});
};

function showFunctionUrlInformation(context: $TSContext, serviceName: string, functionUrl: string) {
context.print.success(`Successfully added resource ${functionUrl}.`);
context.print.info("");
context.print.info('Next steps:');
context.print.info("1. Open https://api.slack.com/apps in your browser.");
context.print.info(`2. Update settings.interactivity.request_url with ${functionUrl} in Slack App Manifest for ${serviceName}.`);
context.print.info("");
}

function getFunctionUrlBySerivceName(amplifyMeta: $TSMeta): Record<string, string> {
return Object.entries<Record<string, any>>(amplifyMeta.consolenotification)
.reduce((all, [serviceName, service]) => {
all[serviceName] = service.output.LambdaFunctionUrl;
return all;
}, {} as Record<string, string>);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface Options {
providerPlugin: string;
functionName?: string;
webhookUrl?: string;
useFunctionUrl?: boolean;
depensOn?: any;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,60 +1,103 @@
import { $TSContext, AmplifyCategories, AmplifySupportedService } from "amplify-cli-core";
import { FunctionParameters } from 'amplify-function-plugin-interface';
import {
$TSContext,
AmplifyCategories,
AmplifySupportedService,
open,
} from "amplify-cli-core";
import {
FunctionParameters,
} from "amplify-function-plugin-interface";
import { Options } from "..";
import * as path from 'path';
import * as path from "path";
import inquirer from "inquirer";

const templateFileName = 'amplify-console-notification-cloudformation-template.json.ejs';
const templateFileName =
"amplify-console-notification-cloudformation-template.json.ejs";

const amplifySlackAppTemplateName = "amplifyslackapp";

/**
* Starting point for CLI walkthrough that generates a Amplify Console Notification
* @param context The Amplify Context object
* @param category The resource category (should always be 'consolenotification')
* @param options The options for Amplify Console Notification
*/
export async function createWalkthrough(context: $TSContext, category: string, options: Options) {
const functionName = await addNewLambdaFunction(context);
export async function createWalkthrough(
context: $TSContext,
category: string,
options: Options
) {
const params = await askSlackAppType(context);
options.useFunctionUrl = params.template === amplifySlackAppTemplateName;

if (options.useFunctionUrl) {
await showCreateSlacAppSteps(context);
}

const functionName = await addNewLambdaFunction(context, options, params);
options.functionName = functionName;

await copyCfnTemplate(context, category, options);
const resourceName = functionName; // use functionName as resourceName of consolenotification category

await copyCfnTemplate(context, category, resourceName, options);

const backendConfigs = {
service: options.service,
providerPlugin: 'awscloudformation',
providerPlugin: options.providerPlugin,
dependsOn: [
{
category: "function",
resourceName: functionName,
attributes: [
"Arn"
]
}
attributes: ["Arn"],
},
],
};
await context.amplify.updateamplifyMetaAfterResourceAdd(category, 'slack', backendConfigs);
await context.amplify.updateamplifyMetaAfterResourceAdd(
category,
resourceName,
backendConfigs
);
}

async function showCreateSlacAppSteps(context: $TSContext) {
context.print.info("");
context.print.info("Next Steps:");
context.print.info("1. Create a Slack App following setup document.");
context.print.info(" https://github.com/fossamagna/amplify-category-console-notification/blob/main/packages/amplify-slack-app/docs/SETUP.md")
context.print.info("2. Provide an AWS Lambda function name for Slack App.");
context.print.info("3. Input \"Signing Secret\" and \"Slack Bot User OAuth Token\" of Slack App to this prompt.");
context.print.info("4. Input Slack Channel ID to send notification message.");
context.print.info("5. Deploy Slack App as execute `amplify push` command.");
context.print.info("6. Update settings.interactivity.request_url with FunctionUrl in Slack App Manifest.");
context.print.info("");
if (await context.amplify.confirmPrompt('Do you want to open setup document in your browser?', true)) {
open("https://github.com/fossamagna/amplify-category-console-notification/blob/main/packages/amplify-slack-app/docs/SETUP.md", {});
}
}

function copyCfnTemplate(
context: $TSContext,
category: string,
resourceName: string,
options: Options
) {
const { amplify } = context;
// @ts-ignore
const targetDir = amplify.pathManager.getBackendDirPath();
const pluginDir = path.join(__dirname, '..', '..', '..', '..');
const pluginDir = path.join(__dirname, "..", "..", "..", "..");

const copyJobs = [
{
dir: pluginDir,
template: `resources/awscloudformation/cloudformation-templates/${templateFileName}`,
target: `${targetDir}/${category}/slack/slack-cloudformation-template.json`,
paramsFile: path.join(targetDir, category, 'slack', 'parameters.json'),
target: `${targetDir}/${category}/${resourceName}/slack-cloudformation-template.json`,
paramsFile: path.join(targetDir, category, resourceName, "parameters.json"),
},
];

const params = {
appId: getAppId(context)
}
appId: getAppId(context),
};

// copy over the files
// @ts-ignore
Expand All @@ -68,20 +111,48 @@ function getAppId(context: $TSContext) {
}
}

async function addNewLambdaFunction(context: $TSContext): Promise<string> {
const params: Partial<FunctionParameters> = {
defaultRuntime: 'nodejs',
template: "webhook",
};

const resourceName = await context.amplify.invokePluginMethod(context, AmplifyCategories.FUNCTION, undefined, 'add', [
async function addNewLambdaFunction(
context: $TSContext,
options: Options,
params: Partial<FunctionParameters>
): Promise<string> {
const resourceName = await context.amplify.invokePluginMethod(
context,
'awscloudformation',
AmplifySupportedService.LAMBDA,
params,
]);

context.print.success(`Successfully added resource ${resourceName} locally`);

AmplifyCategories.FUNCTION,
undefined,
"add",
[context, options.providerPlugin, AmplifySupportedService.LAMBDA, params]
);
return resourceName as string;
}

async function askSlackAppType(context: $TSContext) {
const templateProviders = context.pluginPlatform.plugins.functionTemplate;
const selections = templateProviders
.filter(
(meta) =>
meta.packageName === "amplify-slack-nodejs-function-template-provider"
)
.map(
(meta) =>
meta.manifest.functionTemplate.templates as {
name: string;
value: string;
}[]
)
.reduce((all, value) => all.concat(value), []);
const answer = await inquirer.prompt([
{
type: "list",
name: "selection",
message: "Select Slack App type",
choices: selections,
validate: (value) => !!value,
},
]);
const params: Partial<FunctionParameters> = {
defaultRuntime: "nodejs",
template: answer.selection,
};
return params;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
{
"name": "Amplify Console Build notification to Slack using Incoming Webhook",
"value": "webhook"
},
{
"name": "Slack App for Amplify Console Build notification and interactions",
"value": "amplifyslackapp"
}
]
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"Records": [
{
"EventSource": "aws:sns",
"EventVersion": "1.0",
"EventSubscriptionArn": "arn:aws:sns:ap-northeast-1:123456789012:amplify-appId_dev:3b84ba66-f0a2-4320-87d6-a01f8674974e",
"Sns": {
"Type": "Notification",
"MessageId": "86886283-6134-460c-a0bf-e96404da803d",
"TopicArn": "arn:aws:sns:ap-northeast-1:123456789012:amplify-appId_dev",
"Subject": null,
"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. \"",
"Timestamp": "2019-10-24T06:33:19.479Z",
"SignatureVersion": "1",
"Signature": "signature",
"SigningCertUrl": "https://sns.ap-northeast-1.amazonaws.com/SimpleNotificationService-xxx.pem",
"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",
"MessageAttributes": {}
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<%= props.topLevelComment %>

import { AmplifyConsoleSlackApp } from "amplify-slack-app";
import { LogLevel } from "@slack/bolt";
import { SSMClient, GetParametersCommand } from "@aws-sdk/client-ssm";

const getSecrets = async (names) => {
const client = new SSMClient();
const input = {
Names: names,
WithDecryption: true,
};
const command = new GetParametersCommand(input);
const response = await client.send(command);
return response.Parameters.reduce(
(params, param) => ({
...params,
[param.Name]: param.Value,
}),
{}
);
};

const secrets = await getSecrets([
process.env.SLACK_SIGNING_SECRET,
process.env.SLACK_BOT_TOKEN,
]);

const app = new AmplifyConsoleSlackApp({
signingSecret: secrets[process.env.SLACK_SIGNING_SECRET],
token: secrets[process.env.SLACK_BOT_TOKEN],
defaultChannel: process.env.SLACK_DEFAULT_CHANNEL,
logLevel: LogLevel.DEBUG,
});

export const handler = app.createHandler();
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "<%= props.functionName %>",
"version": "1.0.0",
"description": "Slack App for Amplify Console",
"main": "index.js",
"type": "module",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/client-ssm": "^3.150.0",
"amplify-slack-app": "^1.1.0"
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { FunctionTemplateContributorFactory } from 'amplify-function-plugin-interface';
import { provideWehbook } from './providers/resolverWebhook';
import { resolverAmplifySlackApp } from './providers/resolverAmplifySlackApp';

export const functionTemplateContributorFactory: FunctionTemplateContributorFactory = (context) => {
return {
Expand All @@ -8,6 +9,9 @@ export const functionTemplateContributorFactory: FunctionTemplateContributorFact
case 'webhook': {
return provideWehbook(context);
}
case 'amplifyslackapp': {
return resolverAmplifySlackApp(context);
}
default: {
throw new Error(`Unknown template selection [${request.selection}]`);
}
Expand Down
Loading

0 comments on commit 29ddd63

Please sign in to comment.