Send a Slack message when Docker images are updated

Send a Slack message when Docker images are updated

I needed a way to send a notification to a Slack channel, whenever I push new versions of Docker images to the registry. This post, I explain how I created a function that uses a container registry webhook to call a serverless function that sends a message to a Slack channel.

The Kubernetes labs that are part of the Start Kubernetes course run as multiple Docker images inside a Kubernetes cluster. I wanted a way to notify the users when I push new Docker images to the registry. That way, they can restart the Pods and get the updated images.
The Azure container registry I use to host the images allows me to create webhooks. Whenever you push or delete an image, the container registry sends a JSON payload to a URL of my choosing.
For the course, I am using a Slack workspace. Slack also has support for apps. You can create and add apps to Slack workspaces and give them permission to post messages, for example. One of the Slack apps' features is the ability to use [incoming webhooks](https://api.slack.com/messaging/webhooks) to post messages to a Slack channel.
For example, you can configure a channel for the incoming webhook and then use a POST request like the one below to send a message to that channel:
curl -X POST -H 'Content-type: application/json' --data '{"text":"Hello, World!"}' https://hooks.slack.com/services/[somethingsomething]
Here's a diagram that shows what I wanted to achieve:
Registry webhook to Slack
Registry webhook to Slack
The issue I ran into quickly was that I couldn't control the payload that the container registry sends. You can only configure the URL and the headers you want to send. The container registry sends a payload that looks like this:
{
  "id": "673aeeaa-6493-41d3-bcdd-68242942bcb0",
  "timestamp": "2020-10-14T00:15:02.82330594Z",
  "action": "push",
  "target": {
    "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
    "size": 1778,
    "digest": "sha256:a71f5e4bf56c05a3d6264b8ef4d3bb4c90b4b0af579fedb6ccb68ea59f597435",
    "length": 1778,
    "repository": "startkubernetes/sk-web",
    "tag": "1"
  },
  "request": {
    "id": "adbc2757-8d80-49ac-af5f-ec30e5147bdf",
    "host": "myregistry.azurecr.io",
    "method": "PUT",
    "useragent": "docker/19.03.13+azure go/go1.13.15 git-commit/bd33bbf0497b2327516dc799a5e541b720822a4c kernel/5.4.0-1026-azure os/linux arch/amd64 UpstreamClient(Docker-Client/19.03.13+azure \\(linux\\))"
  }
}
However, Slack is expecting a different looking payload. In its simplest form, the Slack message payload looks like this:
{
  "text": "This is my message"
}

Note

Slack also has a Block Kit Builder that allows you to build more complex messages.
In any case, I needed an intermediary that does the following:
  • Accepts the container registry payload
  • Extracts the information (repository, tag, and a timestamp)
  • Creates payload that Slack understands
  • Sends the payload to the Slack incoming webhook URL.
Registry to Function to Slack
Registry to Function to Slack
That seemed like a perfect use case for a serverless function. The function has an endpoint, and I can use that in the container registry webhook. The webhook sends the container registry payload to my function. In the function, I can write any code I want, modify the payload, and then it to the Slack webhook.
To create the function, I downloaded the Azure Functions VS Code extension, logged in, and I was able to do everything from my editor.
Here's how the function looks like:
const axios = require('axios');
const slackUrl = 'https://hooks.slack.com/services/[somethingsomething]';

module.exports = async function (context, req) {
  let message = JSON.stringify(req.body);

  if (
    req.body.hasOwnProperty('target') &&
    req.body.hasOwnProperty('timestamp')
  ) {
    const {
      target: { repository, tag },
      timestamp,
    } = req.body;
    message = `Pushed image ${repository}:${tag} (${timestamp})`;
  } else {
    context.res = {
      status: 500,
      body: req.body,
    };
    return;
  }

  axios
    .post(
      slackUrl,
      { text: message },
      {
        headers: {
          'content-type': 'application/json',
        },
      }
    )
    .then((response) => {
      context.res = { body: response.body };
    })
    .catch((err) => {
      context.res = {
        status: 500,
        body: err,
      };
      context.done();
    });
};
I deployed the function, then updated the container registry webhook to send the payloads to the function URL, and that was it. The container registry webhook has the option of sending a Ping to the endpoint. That way you, can test out the connection. Similarly, if I wanted to test the function-to-Slack connection, I could either send a POST request directly to the Slack, or manually invoke the function.
Every time I make changes to the labs or any code, and when I merge a PR to the main branch, the Github action builds the images and pushes them to the registry. Then, registry takes over, sends the payload to the function and sends the message to a Slack channel.

Related Posts

;