Рубрики
Uncategorized

Добавление уведомлений для каждого развертывания производства в осьминоге Развертывать

Вступительное руководство по использованию подписок с веб -крючками. Tagged с DevOps, Octopus, Firebase, Slack.

Оглавление

  • вступление
  • Настраивать
  • Webhook
  • Подписка
  • Полезная нагрузка
  • Webhook Revisited
  • Сообщения по маршрутизации и стилии
  • Уклоняясь
  • Авторизация
  • Обработайте событие только один раз
  • Обработка отказов
  • Заворачивать

вступление

Привет! В эпизоде Спросите осьминога Я говорил о том, как использование осьминога Подписки И пользовательские веб -крючки могут решить много проблем. Я представил использование случая отправки слабой уведомления для каждого производственного развертывания. Я хотел изучить эту идею более подробно, что заставило меня написать этот пост. Итак, давайте доберемся до этого и настроем Slack уведомления для всех наших развертываний производства.

Настраивать

У меня есть шесть существующих проектов без каких -либо уведомлений, встроенных в их процессы. Количество проектов здесь не является необходимым. Тот же подход будет работать, независимо от того, есть ли у вас один проект или сотни.

Webhook

Я собираюсь начать с настройки WebHook по двум причинам. Первая причина заключается в том, что мне нужен URL -адрес Webhook для настройки подписки. Во -вторых, я хочу использовать WebHook, чтобы осмотреть полезную нагрузку, чтобы я знал, какие данные ожидать. Я начну с простой функции, которая принимает веб -запрос и регистрирует тело этого запроса. Я использую функции Firebase Cloud в качестве своей конечной точки для этой демонстрации.

exports.logOctopusEvent = functions.https.onRequest((req, res) => {
    console.log(JSON.stringify(req.body));

    return res.status(200).end();
});

Подписка

У нас есть элементарный веб -крючок, поэтому давайте настроим эту подписку и запустим развертывание.

Я выбрал три категории событий, которые должны быть отправлены в WebHook для обработки: развертывание началось, развертывание удалось и развертывание не удалось. Я также установил фильтр среды только для производства. Когда вы впервые настраиваете веб -крюк, вы можете сначала ограничить его в средах разработки или тестирования, когда вы создаете свою логику.

Я также установил URL -адрес полезной нагрузки на URL моего веб -кхука.

Полезная нагрузка

После запуска развертывания я вытащил тело полезной нагрузки из журналов функций. Здесь много информации, включая детали подписки, которая вызвала запрос.

Информация, которая нас больше всего интересует, находится в полезной нагрузке. Раздел события. В частности, мы собираемся использовать категорию, сообщение и связанные с ними.

{
    "Timestamp": "2019-04-26T18:37:44.1581725+00:00",
    "EventType": "SubscriptionPayload",
    "Payload": {
        "ServerUri": "https://myoctopusurl",
        "ServerAuditUri": "https://myoctopusurl/#/configuration/audit?environments=Environments-246&eventCategories=DeploymentFailed&eventCategories=DeploymentStarted&eventCategories=DeploymentSucceeded&from=2019-04-26T18%3a37%3a12.%2b00%3a00&to=2019-04-26T18%3a37%3a42.%2b00%3a00",
        "BatchProcessingDate": "2019-04-26T18:37:42.7832114+00:00",
        "Subscription": {
            "Id": "Subscriptions-161",
            "Name": "Production Deployments",
            "Type": 0,
            "IsDisabled": false,
            "EventNotificationSubscription": {
                "Filter": {
                    "Users": [],
                    "Projects": [],
                    "Environments": [
                        "Environments-246"
                    ],
                    "EventGroups": [],
                    "EventCategories": [
                        "DeploymentFailed",
                        "DeploymentStarted",
                        "DeploymentSucceeded"
                    ],
                    "EventAgents": [],
                    "Tenants": [],
                    "Tags": [],
                    "DocumentTypes": []
                },
                "EmailTeams": [],
                "EmailFrequencyPeriod": "01:00:00",
                "EmailPriority": 0,
                "EmailDigestLastProcessed": null,
                "EmailDigestLastProcessedEventAutoId": null,
                "EmailShowDatesInTimeZoneId": "UTC",
                "WebhookURI": "https://mywebhookurl/logOctopusEvent",
                "WebhookTeams": [],
                "WebhookTimeout": "00:00:10",
                "WebhookHeaderKey": null,
                "WebhookHeaderValue": null,
                "WebhookLastProcessed": "2019-04-26T18:37:12.4560433+00:00",
                "WebhookLastProcessedEventAutoId": 187275
            },
            "SpaceId": "Spaces-83",
            "Links": {
                "Self": {}
            }
        },
        "Event": {
            "Id": "Events-189579",
            "RelatedDocumentIds": [
                "Deployments-15970",
                "Projects-670",
                "Releases-6856",
                "Environments-246",
                "ServerTasks-318123",
                "Channels-690",
                "ProjectGroups-302"
            ],
            "Category": "DeploymentStarted",
            "UserId": "users-system",
            "Username": "system",
            "IsService": false,
            "IdentityEstablishedWith": "",
            "UserAgent": "Server",
            "Occurred": "2019-04-26T18:37:34.3616214+00:00",
            "Message": "Deploy to Prod (#3) started  for Accounting Database release 10.33.210 to Prod",
            "MessageHtml": "Deploy to Prod (#3) started  for Accounting Database release 10.33.210 to Prod",
            "MessageReferences": [
                {
                    "ReferencedDocumentId": "Deployments-15970",
                    "StartIndex": 0,
                    "Length": 19
                },
                {
                    "ReferencedDocumentId": "Projects-670",
                    "StartIndex": 33,
                    "Length": 19
                },
                {
                    "ReferencedDocumentId": "Releases-6856",
                    "StartIndex": 61,
                    "Length": 9
                },
                {
                    "ReferencedDocumentId": "Environments-246",
                    "StartIndex": 74,
                    "Length": 4
                }
            ],
            "Comments": null,
            "Details": null,
            "SpaceId": "Spaces-83",
            "Links": {
                "Self": {}
            }
        },
        "BatchId": "e6df5aae-a42a-4bd8-8b0d-43065f82d5f0",
        "TotalEventsInBatch": 1,
        "EventNumberInBatch": 1
    }
}

Webhook Revisited

Первоначальный веб -крюк настроен. Подписка отправляет события на это. Давайте добавим некоторую реальную логику в функцию.

Сначала мы проверяем, есть ли у нас полезная нагрузка. Если у нас его нет, мы отправим ответ плохого запроса.

Затем мы извлекаем имя подписки и сообщение и используем его для создания слабых сообщений.

exports.logOctopusEvent = functions.https.onRequest((req, res) => {
    const payload = req.body.Payload;

    if (payload) {
        return sendSlackMessage({
            "text": payload.Event.Message,
            "username": `Octopus Subscription: ${payload.Subscription.Name}`
        }).then(() => {
            return res.status(200).send();
        });
    }
    else {
        console.warn('No payload provided');
        return res.status(400).send('No payload provided');
    }
});

После продвижения этого изменения и вызвать другое развертывание, мы получаем некоторые уведомления на нашем канале!

Сообщения по маршрутизации и стилии

Наш Slack Webhook отправляет сообщения на канал #Octopus-развертывания по умолчанию. Однако что, если мы хотим отправить уведомления на другой канал на основе проекта?

Мы можем вытащить идентификатор проекта из соответствующих идентификаторов документа.

const projectId = payload.Event.RelatedDocumentIds.find(id => id.startsWith('Projects-'));

Мы можем создать отображение от идентификатора проекта на канал, который следует использовать.

const projectToChannel = {
    "Projects-670": "#accounting_dev",
    "Projects-665": "#accounting_dev",
    "Projects-668": "#expense_dev",
    "Projects-667": "#expense_dev",
    "Projects-669": "#hr_dev",
    "Projects-666": "#hr_dev"
};

А затем предоставьте этот канал нашей функции Slack.

return sendSlackMessage({
    "channel": projectToChannel[projectId],
    "text": payload.Event.Message,
    "username": `Octopus Subscription: ${payload.Subscription.Name}`
}).then(() => {
    return res.status(200).send();
});

Большой! Теперь давайте добавим немного таланта в наши сообщения, используя категорию. Точно так же мы настроим отображение канала, мы можем настроить отображение из категории на смайлику.

const categoryToEmoji = {
    "DeploymentStarted": ":octopusdeploy:",
    "DeploymentFailed": ":fire:",
    "DeploymentSucceeded": ":tada:"
}

А затем используйте это отображение, чтобы выбрать смайлики и добавить его в наше сообщение.

const projectId = payload.Event.RelatedDocumentIds.find(id => id.startsWith('Projects-'));
const channel = projectToChannel[projectId];
const emoji = categoryToEmoji[payload.Event.Category];

return sendSlackMessage({
    "channel": channel,
    "text": `${emoji} ${payload.Event.Message} ${emoji}`,
    "username": `Octopus Subscription: ${payload.Subscription.Name}`
}).then(() => {
    return res.status(200).send();
});

Возможно, вы даже захотите взять категорию развертывания и отправить несколько прямых сообщений пользователю или отправить текстовое сообщение или электронное письмо. Мы оставим этот эксперимент на другой день.

Уклоняясь

Я храню свои отображения в базе данных и получаю их при выполнении функции. Код начинает становиться довольно занятым, поэтому давайте начнем разделить эти функции и звонить в трубопровод.

function getPayload([req, res]) {
    const payload = req.body.Payload;

    if (payload) {
        return Promise.resolve(payload);
    }

    return Promise.reject({
        code: 400,
        message: 'No payload provided'
    });
}

function loadMappings(payload) {
    if (categoryToEmojiMapping && projectToChannelMapping) {
        return Promise.resolve([payload, categoryToEmojiMapping, projectToChannelMapping]);
    }

    const collection = db.collection("mappings");
    const categoryToEmojiPromise = collection.doc('categoryToEmoji').get();
    const projectToChannelPromise = collection.doc('projectToChannel').get();

    return Promise.all([categoryToEmojiPromise, projectToChannelPromise])
        .then(([categoryToEmojiDoc, projectToChannelDoc]) => {
            categoryToEmojiMapping = categoryToEmojiDoc.data();
            projectToChannelMapping = projectToChannelDoc.data();

            return [payload, categoryToEmojiMapping, projectToChannelMapping];
        });
}

function createSlackOptions([payload, categoryToEmoji, projectToChannel]) {
    const projectId = payload.Event.RelatedDocumentIds.find(id => id.startsWith('Projects-'));
    const channel = projectToChannel[projectId];
    const emoji = categoryToEmoji[payload.Event.Category];

    return {
        "channel": channel,
        "text": `${emoji} ${payload.Event.Message} ${emoji}`,
        "username": `Octopus Subscription: ${payload.Subscription.Name}`
    };
}

function sendSlackMessage(options) {
    const slackUri = functions.config().slack.uri;

    const requestOptions = {
        method: 'POST',
        uri: slackUri,
        body: {
            "channel": options.channel,
            "username": options.username,
            "icon_emoji": ":octopusdeploy:",
            "text": options.text
        },
        json: true
    }

    return rp(requestOptions);
}

exports.logOctopusEvent = functions.https.onRequest((req, res) => {
    return getPayload([req, res])
        .then(loadMappings)
        .then(createSlackOptions)
        .then(sendSlackMessage)
        .then(() => { return res.status(200).send(); });
});

Идеальный! Этот код начинает выглядеть намного лучше.

Авторизация

Мы еще не обратились к авторизации для нашего веб -крючка. В соответствии с этим, любой может отправить запрос, который соответствует структуре, которую мы ожидаем. Это потому, что я использую общую функцию облака Firebase. Если вы используете внутреннюю сервис или что -то размещенное, но с заблокированным доступом, вы можете не беспокоиться об этой части.

Мы можем исправить это, добавив заголовок к нашему запросу, который содержит токен авторизации, которому мы доверяем на стороне веб -крючка.

Я назвал свой заголовок Octolog-Token, но вы можете назвать это все, что вам нравится. Вы даже можете использовать стандартное название заголовка, такое как авторизация. Здесь также важно отметить, что значение заголовка хранится в простом тексте. Вы захотите помнить об этом при принятии решения о том, какие значения заголовка использовать и какие команды предоставить доступ к подписке.

Давайте добавим функцию для обработки авторизации и вставьте ее в наш трубопровод.

function authorizeRequest(req, res) {
    const providedToken = req.get('octolog-token');
    const token = functions.config().octolog.authtoken;

    if (!providedToken || providedToken !== token) {
        return Promise.reject({
            code: 401,
            message: 'Missing or invalid token'
        });
    }

    return Promise.resolve([req, res]);
}

exports.logOctopusEvent = functions.https.onRequest((req, res) => {
    return authorizeRequest(req, res)
        .then(getPayload)
        .then(loadMappings)
        .then(createSlackOptions)
        .then(sendSlackMessage)
        .then(() => { return res.status(200).send(); });
});

Теперь запросы без заголовка или с неверным токеном будут отклонены.

Обработайте событие только один раз

Я хочу назвать этот намек из нашей документации по подписке.

Несмотря на то, что мы прилагаем все усилия, чтобы события были отправлены только один раз в данную электронную почту или подписку на Webhook, мы не можем предложить никаких гарантий и посоветовать, что вы разработаете свой потребляющий API с учетом этого.

В этом случае дублирующее уведомление о слабых в худшем случае сбивает с толку. Это не повод игнорировать руководство в докоре! Давайте добавим логику, чтобы убедиться, что мы обрабатываем каждое событие только один раз. База данных Firestore поддерживает транзакции, поэтому мы используем их, чтобы убедиться, что мы не обрабатываем какие -либо события, уже в нашей базе данных.

function checkForDuplicate(payload) {
    return db.runTransaction((transaction) => {
        const eventReference = db.collection("deployments").doc(payload.Event.Id);

        return transaction.get(eventReference).then((eventDoc) => {
            if (eventDoc.exists) {
                return Promise.reject({
                    code: 200,
                    message: `Event ${payload.Event.Id} has already been processed.`
                });
            }

            transaction.set(eventReference, payload);
            console.log("Document written with ID: ", payload.Event.Id);

            return payload;
        });
    });
}

exports.logOctopusEvent = functions.https.onRequest((req, res) => {
    return authorizeRequest(req, res)
        .then(getPayload)
        .then(checkForDuplicate)
        .then(loadMappings)
        .then(createSlackOptions)
        .then(sendSlackMessage)
        .then(() => { return res.status(200).send(); });
});

Обработка отказов

Возможно, вы заметили некоторые призывы обещать. Derject, посыпанный по всему функциям. Возможно, вы также заметили, что мы нигде не справляемся с этими отказами.

В наших отказах мы отправляем объект с кодом и сообщением. Мы можем проверить этот формат, когда мы обрабатываем отказ. Если он соответствует, мы будем использовать этот код и сообщение. Если нет, мы отправим обратно общий ответ плохого запроса.

function handleRejection(res, reason) {
    if (reason.message) {
        console.warn(reason.message);
        return res.status(reason.code).send(reason.message);
    }

    console.warn(reason);
    return res.status(400).send();
}

exports.logOctopusEvent = functions.https.onRequest((req, res) => {
    const sendOkResponse = () => { return res.status(200).send(); };
    const callHandleRejection = (reason) => {
        return handleRejection(res, reason);
    }

    return authorizeRequest(req, res)
        .then(getPayload)
        .then(checkForDuplicate)
        .then(loadMappings)
        .then(createSlackOptions)
        .then(sendSlackMessage)
        .then(sendOkResponse)
        .catch(callHandleRejection);
});

Заворачивать

Вот и все! Мы начали с ничего и создали функцию WebHook, которая не только отправляет слабые уведомления для всех развертываний производства, но и направляет их на соответствующий канал.

Я надеюсь, что это было полезным для вас прохождением. Я сохранил образец полезной нагрузки и функционального файла AT GitHub Анкет

Пожалуйста, оставьте любые отзывы или задайте любые вопросы ниже в комментариях. Если у вас был успех с подписками и веб -крючками, поделитесь с нами своей историей!

Этот пост был первоначально опубликован в octopus.com .

Оригинал: «https://dev.to/octopus/adding-notifications-for-every-production-deployment-in-octopus-deploy-g5i»