Back
2

Cloud Manager Slack Notifications with Project Firefly

May 6, 2021

Automating notifications is a common DevOps task. In this post, I'll explain how to send Adobe Cloud Manager pipeline status notifications to Slack using a Project Firefly headless application.

In the Getting Started with Project Firefly post, we created a Firefly project and used the Developer Console to add the Cloud Manager API, as well as a credential (JWT) to our project. 

Slack API

To begin this integration, follow the instructions on the Slack website to create an incoming webhook

retrieve webhook url in slack

After your Slack App is created, make a note of the webhook URL, we will use it when coding the application. 

Coding the Application

Open the AIO CLI documentation and Cloud Manager API documentation in a browser tab for quick reference. Using the terminal, navigate to your code project and run the following command:

aio app:add:action

The action creation wizard will walk you through generating a new action. Select, Generic as the action type and give the action an appropriate name (e.g. notify-slack). Agree to overwrite the package.json and manifest.yml files when prompted.

Open the code project within an editor and open the .env file. Notice the environment variables reflect the values that appear within the Developer Console. Add the following environment variables to the .env file. 

  • PRIVATE_KEY=<YOUR-PRIVATE-KEY>

These environment variables will be made available to the application in the next step when we update the manifest.yml file.

Note: It is not recommended to store your private key in an environment variable, for the sake of brevity I will in this tutorial, but in your production application you should read the private key from a file. Learn how to create Zipped Action Folder Resources in Project Firefly and Adobe I/O Runtime.

Dot Env File

Open the manifest.yml file and locate the entry for the notify-slack action. Items within the Inputs block will be made available to our application code within the params object. Environment variables (.env file) can be used within the manifest.yml file through the use of the $ symbol. Map the environment variables to the input properties as shown below.

Manifest File 
From the terminal, run the following command to install the jsrasign dependency.

npm install jsrsasign --save

Open the code project within an editor and create a new file within the actions > notify-slack folder named token.js. The token.js module will be responsible for exchanging a JWT token for an OAuth 2.0 access token which we'll need to work with the Cloud Manager API. Paste the following code into token.js. Save and close the file.

const jsrsasign = require("jsrsasign");
const fetch = require("node-fetch");
const { URLSearchParams } = require("url");

async function getAccessToken(params) {
  const EXPIRATION = 60 * 60; // 1 hour

  const header = {
    alg: "RS256",
    typ: "JWT",
  };

  const payload = {
    exp: Math.round(new Date().getTime() / 1000) + EXPIRATION,
    iss: params.IMS_ORG_ID,
    sub: params.TECHNICAL_ACCOUNT_ID,
    aud: `https://ims-na1.adobelogin.com/c/${params.SERVICE_API_KEY}`,
    "https://ims-na1.adobelogin.com/s/ent_cloudmgr_sdk": true,
  };

  const jwtToken = jsrsasign.jws.JWS.sign(
    "RS256",
    JSON.stringify(header),
    JSON.stringify(payload),
    params.PRIVATE_KEY
  );

  const response = await fetch(
    "https://ims-na1.adobelogin.com/ims/exchange/jwt",
    {
      method: "POST",
      body: new URLSearchParams({
        client_id: params.SERVICE_API_KEY,
        client_secret: params.CLIENT_SECRET,
        jwt_token: jwtToken,
      }),
    }
  );

  const json = await response.json();
  return json["access_token"];
}

module.exports = {
  getAccessToken
}

Open the index.js file within the editor and replace the contents with the following code. Update the SLACK_WEBHOOK constant variable with the appropriate value for your Slack App.

const fetch = require('node-fetch')
const { Core } = require('@adobe/aio-sdk')
const { getAccessToken } = require('./token');
const { errorResponse, getBearerToken, stringParameters, checkMissingRequestInputs } = require('../utils')

const SLACK_WEBHOOK = 'https://hooks.slack.com/services/<YOUR-WEBHOOK-URL>';

async function sendNotification (params) {
  const accessToken = await getAccessToken(params);

  let type = params.event['@type'].indexOf('started') > -1 ? 'STARTED' : 'ENDED';
  let id = params.event['activitystreams:object']['@id'];

  // Make GET Request to Cloud Manager API
  let pipelineRequest = await fetch(id, {
    'method': 'GET',
    'headers': { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'x-gw-ims-org-id': params.IMS_ORG_ID, 'x-api-key': params.SERVICE_API_KEY}
  });

  let pipeline = await pipelineRequest.json();
  let notification = `Cloud Manager Pipeline Notification \n Event Type: ${type} \n Status: ${pipeline.status} \n ID: ${id}`;

  //Make POST to Slack Webhook
  const message = await fetch(SLACK_WEBHOOK, {
    'method': 'POST',
    'headers': { 'Content-Type': 'application/json' },
    'body': JSON.stringify({
      'text': notification
    })
  })

  const response = {
    statusCode: 200,
    body: message
  }

  return response
}

async function main (params) {
  const logger = Core.Logger('main', { level: params.LOG_LEVEL || 'info' })

  try {
    const requiredParams = ['event', 'SERVICE_API_KEY', 'CLIENT_SECRET', 'IMS_ORG_ID', 'TECHNICAL_ACCOUNT_ID']
    const requiredHeaders = []
    const errorMessage = checkMissingRequestInputs(params, requiredParams, requiredHeaders)
    
    if (errorMessage) {
      return errorResponse(400, errorMessage, logger)
    }
    
    let response = await sendNotification(params);
    return response;

  } catch (error) {
    logger.error(error)
    return errorResponse(500, 'server error', logger)
  }
}

exports.main = main

You should now have two modules within your actions > notify-slack folder.

Filesystem showing notify-slack modules

From the terminal, run the following command to start the application.

aio app run

Notice that the console output provides two URLs to view the application. 

  • Local Application - Renders the application from a local HTTPS server
    • https://localhost:9080
  • Experience Cloud Shell - Renders the application within the Experience Cloud Shell with an authenticated session
    • https://experience.adobe.com/?devMode=true#/custom-apps/?localDevUrl=https://localhost:9080

Confirm that the action is available from the menu.

Firefly Developer Tool 

Registering I/O Event Listener

1) Return to the Developer Console and click the Add Event button within the production workspace of the project.

Developer Console Add Event Cloud Manager

2) Select the Experience Cloud category and then select the Cloud Manager service.

Select Cloud Manager

3) Click the Next button. Select the events you want to subscribe to. For this demo, I selected the Pipeline Execution Started and Pipeline Execution Ended events.

Developer Console Configure Event Registration

4) Click the Next button. Select the Service Account Credential to associate with the application.

Developer Console Select Service Account Credential

5) Click the Next button. Enter a unique name for this event registration (e.g. PipelineSlackNotification). Select the action that we created earlier (e.g. notify-slack) from the Runtime action drop-down menu.

Map Event Registration to Runtime Action

6) Click the Save Configurated Events button. The Event Registration should reflect an active status.

Registered Event is Active

Building Cloud Manager Pipeline

1) Initiate a build on the Cloud Manager Pipeline

Build Cloud Manager Pipeline

2) Open Slack channel and verify the notification was sent

Slack Channel Notification