Apps: "Nginx", FCGI, "Bash", SSH и "Git".
Задача: автоматизировать доставку и развёртывание кода простейших web-приложений к серверам тестирования и публикации на примере поддержки web-сервиса, считая последний установленным следуя инструкции по сборке и развёртыванию "LNPMM + Multisite" на этом же сайте, запуская процедуры по событию изменения содержимого Git-репозитория, с информированием о таковом посредством "webhook" POST-запроса.
В качестве Git-репозитория хранения может использоваться практически любой современный сервис - все они поддерживают уведомление о событиях посредством "webhooks" - но в примере рассматриваются только три из них: "GitLab (Standalone)", "BitBucket (Standalone)" и "GitHub".
Исходим из того, что процесс разработки ведётся по классической простейшей схеме перехода от ветке к ветке: "feature" -> "develop" -> "testing" -> "staging" -> "master". Пример организации "workflow" на стороне сервисов хранения Git-репозиториев приводится в отдельной публикации. Из этих этапов к нашему сервису имеет отношение только пара-тройка последних.
Основные положения, рассматриваемые в этой публикации:
1. Запись в "testing" вызывает процедуру безусловной выгрузки этой ветки репозитория на web-сервер оперативного тестирования.
2. Запись (только посредством одобренного "pull request"-а) в "staging" вызывает процедуру безусловной выгрузки этой ветки репозитория на web-сервер предварительного расширенного тестирования.
3. Запись (только посредством одобренного "pull request"-а) в "master" вызывает процедуру безусловной выгрузки этой ветки репозитория на web-сервер публикации.
2. Запись (только посредством одобренного "pull request"-а) в "staging" вызывает процедуру безусловной выгрузки этой ветки репозитория на web-сервер предварительного расширенного тестирования.
3. Запись (только посредством одобренного "pull request"-а) в "master" вызывает процедуру безусловной выгрузки этой ветки репозитория на web-сервер публикации.
Последовательность дальнейших действий:
1. Настраиваем уведомление об изменениях на стороне Git-репозитория.
2. Подготавливаем локальный репозиторий и пользовательское окружение.
3. Подготавливаем среду автоматизации обработки входящих "webhook"-ов.
4. Пишем Bash-скрипты обработки "webhook"-ов и запуска процедур доставки данных.
2. Подготавливаем локальный репозиторий и пользовательское окружение.
3. Подготавливаем среду автоматизации обработки входящих "webhook"-ов.
4. Пишем Bash-скрипты обработки "webhook"-ов и запуска процедур доставки данных.
Создание задачи уведомления сторонних web-серверов об изменениях в репозиториях.
В web-интерфейсе "BitBucket (Standalone) Server" для репозитория, изменения которого нужно отслеживать, создаём "Webhook":
Repositories -> Repository settings -> Webhooks -> Create webhook:
Name: Notify Repo Push
URL: https://webhost.example.net/webhook/notice-repo-update.cgi
Secret: keyForPOSTverify
Events: Push
Name: Notify Repo Push
URL: https://webhost.example.net/webhook/notice-repo-update.cgi
Secret: keyForPOSTverify
Events: Push
Пример для web-интерфейса "GitLab (Standalone) Server":
Group -> Repository -> Settings -> Integrations:
URL: https://webhost.example.net/webhook/notice-repo-update.cgi
Secret Token: keyForPOSTverify
Trigger: Push events
SSL verification: true
URL: https://webhost.example.net/webhook/notice-repo-update.cgi
Secret Token: keyForPOSTverify
Trigger: Push events
SSL verification: true
Все остальные сервисы Git-репозиториев конфигурируются схожим с приведёнными выше примерами образом.
Теперь на каждое событие добавления данных в Git-репозитории будет вызываться указанный выше URL и удалённому web-сервису будет отправляться POST-запрос с JSON-массивом перечня параметров, позволяющих определить, интересуют ли изменения в репозитории целевой сервер и стоит их оттуда забирать.
Подготовка локального репозитория и пользовательского окружения.
Устанавливаем необходимое прикладное ПО и вспомогательные утилиты:
# aptitude install git patch tree acl libcap2-bin jq
Будем размещать инструменты в отдельной от системного окружения директории "/usr/local/etc" - создаём директорию для Bash-скриптов и шаблонов конфигурационных файлов для таковых:
# mkdir -p /usr/local/etc/gitman
Здесь мы об автоматизации, так что сразу приступим к написанию скриптов, создающих все необходимые структуры и производящие потребные действия.
Напоминаю, что в этой публикации рассматривается автоматизация в среде созданной по инструкции развёртывания и настройки web-сервера "LNPMM + Multisite" - отсюда обязательная адресация ресурсов через сочетание "groupname - sitename - reponame".
Скрипт создания пользователя для обращений к Git-репозиториям.
# vi /usr/local/etc/gitman/addgituser.sh && chmod ug+x /usr/local/etc/gitman/addgituser.sh
#!/bin/bash
GROUP=${1}
WEBROOT="/var/www"
# Включаем дублирование вывода в журнальный файл
exec &> >(col -bp | tee -a "$(dirname ${0})/add.log")
# Фиксируем дату и время запуска процедуры
echo; echo $(date +"%Y-%m-%d.%H:%M:%S:")
# Проверяем наличие ожидаемых утилит
[ -x "$(command -v setfacl)" ] || { echo "Не обнаружен необходимый для работы набор утилит. Операция создания пользователя прервана."; exit 1; }
# Проверяем корректность вводимых данных и даём подсказку в случае необходимости
[ ! "${1}" ] && { echo "Ожидаемый формат: ${0} \"group\""; echo "Не указано название \"группы сайтов\". Операция создания пользователя прервана."; exit 1; }
[ ! -d "${WEBROOT}/${GROUP}" ] && { echo "Указанная \"группа сайтов\" отсутствует. Операция создания пользователя прервана."; exit 1; }
# Проверяем наличие специализированного Git-пользователя, от имени которого будут осуществляться обращения к Git-репозиториями
id git-${GROUP} > /dev/null 2>&1
if [ "${?}" -ne "0" ]; then
# Добавляем пользователя, с предварительным профилактическим созданием группы, объединяющей ресурсы набора web-проектов
groupadd --force www-${GROUP} > /dev/null 2>&1
useradd --shell /usr/bin/git-shell --create-home --home-dir /home/git-${GROUP} --gid www-${GROUP} git-${GROUP} > /dev/null 2>&1
if [ "${?}" -eq "0" ]; then
echo "Успешно создан специализированный пользователь \"git-${GROUP}\"."
else
echo "На этапе добавления специализированного пользователя \"git-${GROUP}\" случился сбой. Операция добавления пользователя прервана."
exit 1
fi
else
echo "Создаваемый пользователь \"git-${GROUP}\" уже существует."
fi
# Генерируем набор SSH-ключей для аутентификации, если таковые отсутствуют
mkdir -p /home/git-${GROUP}/.ssh
if [ ! -f "/home/git-${GROUP}/.ssh/id_rsa" ]; then
ssh-keygen -t rsa -b 2048 -f /home/git-${GROUP}/.ssh/id_rsa -P "" -C "User git-${GROUP}." > /dev/null 2>&1
if [ "${?}" -eq "0" ]; then
echo "Успешно сгенерирован набор SSH-ключей \"/home/git-${GROUP}/.ssh/id_rsa\" для аутентификации Git-пользователя \"git-${GROUP}\"."
else
echo "На этапе генерирования набора SSH-ключей \"/home/git-${GROUP}/.ssh/id_rsa\" случился сбой. Операция добавления пользователя прервана."
exit 1
fi
else
echo "Обнаруженный имеющийся набор SSH-ключей \"/home/git-${GROUP}/.ssh/id_rsa\" оставлен без изменений."
fi
# Проверяем в конфигурации SSH наличие правила игнорирования подтверждения "fingerprint" и добавляем их при необходимости
touch "/home/git-${GROUP}/.ssh/config"
if [[ "$(grep -i -c -e "StrictHostKeyChecking\b\sno" "/home/git-${GROUP}/.ssh/config")" -eq "0" ]]; then
echo "StrictHostKeyChecking no" >> /home/git-${GROUP}/.ssh/config
fi
# Защищаем директорию пользователя от посторонних
chown -R git-${GROUP}:www-${GROUP} /home/git-${GROUP}
chmod -R go-rwx /home/git-${GROUP}
exit ${?}
GROUP=${1}
WEBROOT="/var/www"
# Включаем дублирование вывода в журнальный файл
exec &> >(col -bp | tee -a "$(dirname ${0})/add.log")
# Фиксируем дату и время запуска процедуры
echo; echo $(date +"%Y-%m-%d.%H:%M:%S:")
# Проверяем наличие ожидаемых утилит
[ -x "$(command -v setfacl)" ] || { echo "Не обнаружен необходимый для работы набор утилит. Операция создания пользователя прервана."; exit 1; }
# Проверяем корректность вводимых данных и даём подсказку в случае необходимости
[ ! "${1}" ] && { echo "Ожидаемый формат: ${0} \"group\""; echo "Не указано название \"группы сайтов\". Операция создания пользователя прервана."; exit 1; }
[ ! -d "${WEBROOT}/${GROUP}" ] && { echo "Указанная \"группа сайтов\" отсутствует. Операция создания пользователя прервана."; exit 1; }
# Проверяем наличие специализированного Git-пользователя, от имени которого будут осуществляться обращения к Git-репозиториями
id git-${GROUP} > /dev/null 2>&1
if [ "${?}" -ne "0" ]; then
# Добавляем пользователя, с предварительным профилактическим созданием группы, объединяющей ресурсы набора web-проектов
groupadd --force www-${GROUP} > /dev/null 2>&1
useradd --shell /usr/bin/git-shell --create-home --home-dir /home/git-${GROUP} --gid www-${GROUP} git-${GROUP} > /dev/null 2>&1
if [ "${?}" -eq "0" ]; then
echo "Успешно создан специализированный пользователь \"git-${GROUP}\"."
else
echo "На этапе добавления специализированного пользователя \"git-${GROUP}\" случился сбой. Операция добавления пользователя прервана."
exit 1
fi
else
echo "Создаваемый пользователь \"git-${GROUP}\" уже существует."
fi
# Генерируем набор SSH-ключей для аутентификации, если таковые отсутствуют
mkdir -p /home/git-${GROUP}/.ssh
if [ ! -f "/home/git-${GROUP}/.ssh/id_rsa" ]; then
ssh-keygen -t rsa -b 2048 -f /home/git-${GROUP}/.ssh/id_rsa -P "" -C "User git-${GROUP}." > /dev/null 2>&1
if [ "${?}" -eq "0" ]; then
echo "Успешно сгенерирован набор SSH-ключей \"/home/git-${GROUP}/.ssh/id_rsa\" для аутентификации Git-пользователя \"git-${GROUP}\"."
else
echo "На этапе генерирования набора SSH-ключей \"/home/git-${GROUP}/.ssh/id_rsa\" случился сбой. Операция добавления пользователя прервана."
exit 1
fi
else
echo "Обнаруженный имеющийся набор SSH-ключей \"/home/git-${GROUP}/.ssh/id_rsa\" оставлен без изменений."
fi
# Проверяем в конфигурации SSH наличие правила игнорирования подтверждения "fingerprint" и добавляем их при необходимости
touch "/home/git-${GROUP}/.ssh/config"
if [[ "$(grep -i -c -e "StrictHostKeyChecking\b\sno" "/home/git-${GROUP}/.ssh/config")" -eq "0" ]]; then
echo "StrictHostKeyChecking no" >> /home/git-${GROUP}/.ssh/config
fi
# Защищаем директорию пользователя от посторонних
chown -R git-${GROUP}:www-${GROUP} /home/git-${GROUP}
chmod -R go-rwx /home/git-${GROUP}
exit ${?}
Пример использования (будет создан пользователь "git-group0"):
# /usr/local/etc/gitman/addgituser.sh group0
Практически все сервисы хранения Git-репозиториев поддерживают способ обеспечения доступа к проектам и репозиториям без заведения в них специального пользователя, довольствуясь проверкой заранее размещённого SSH-ключа.
Например, в случае использования в качестве центрального репозитория "BitBucket", получаем содержимое открытого (публичного) ключа демонстрационного пользователя "git-group0" на исходящем web-сервере командой "cat /home/git-group0/.ssh/id_rsa.pub" и добавляем его в перечень ключей доступа посредством web-интерфейса на сервере Git-репозиториев:
Project -> Project settings -> Acces keys -> Add key:
Add public key:
Permission: Read
Key: ssh-rsa ... git-group0.
Add public key:
Permission: Read
Key: ssh-rsa ... git-group0.
Скрипт добавления локального Git-репозитория.
Скрипт добавления Git-репозитория разработки, тестирования, предварительного тестирования или публикации (они идентичны с точки зрения начального конфигурирования) на web-сервере:
# vi /usr/local/etc/gitman/addgitrepo.sh && chmod ug+x /usr/local/etc/gitman/addgitrepo.sh
#!/bin/bash
GROUP=${1}
SITE=${2}
WORK_DIR="/var/www/${GROUP}/${SITE}"
#
GIT_USER_SNAME="User Name"
GIT_USER_MAIL="email@example.net"
# Включаем дублирование вывода в журнальный файл
exec &> >(col -bp | tee -a "$(dirname ${0})/add.log")
# Фиксируем дату и время запуска процедуры
echo; echo $(date +"%Y-%m-%d.%H:%M:%S:")
# Проверяем наличие ожидаемых утилит
[ -x "$(command -v git)" ] && [ -x "$(command -v setfacl)" ] || { echo "Не обнаружен необходимый для работы набор утилит. Операция создания Git-репозитория прервана."; exit 1; }
# Проверяем корректность вводимых данных и даём подсказку в случае необходимости
# (если репозиторий уже имеется - ничего страшного - профилактически обновим структуру директорий и разрешения доступа)
#
[ ! "${1}" ] && { echo "Ожидаемый формат: ${0} \"group\" \"domain.name\""; echo "Не указано название \"группы сайтов\". Операция создания репозитория сайта прервана."; exit 1; }
#
[ ! "${2}" ] && { echo "Ожидаемый формат: ${0} \"${GROUP}\" \"domain.name\""; echo "Не указано имя сайта. Операция создания репозитория сайта прервана."; exit 1; }
[ $(echo "${SITE}" | grep -i -c -e "^www\.") -ne "0" ] && { echo "Имя сайта необходимо указывать без префикса \"www.\". Операция создания репозитория сайта прервана."; exit 1; }
# Проверяем наличие корневой директории сайта, для которого создаётся репозиторий и прерываем работу при отсутствии таковой
[ ! -d "${WORK_DIR}" ] && { echo "Корневая директория \"${WORK_DIR}\" сайта, для которого запрошено создание репозитория, отсутствует. Операция создания репозитория сайта прервана."; exit 1; }
# Проверяем наличие выделенной для указанной "группы сайтов" группы пользователей
grep -q -E "^www-${GROUP}:" /etc/group > /dev/null 2>&1
[ "${?}" -ne "0" ] && { echo "Группа пользователей \"www-${GROUP}\" не найдена! Операция добавления репозитория \"${WORK_DIR}\" прервана."; exit 1; }
# Проверяем наличие выделенного для фоновых операций с Git-репозиториями пользователя
id git-${GROUP} > /dev/null 2>&1
[ "${?}" -ne "0" ] && { echo "Пользователь \"git-${GROUP}\" для фоновых операций с Git-репозиториями не найден! Операция добавления репозитория \"${WORK_DIR}\" прервана."; exit 1; }
# Переходим в директорию репозитория
cd "${WORK_DIR}"
# Перестраховываясь, зачищаем переменную месторасположения репозитория предыдущего сеанса работы
unset GIT_DIR
# Инициализируем репозиторий (это безопасно для уже имеющегося репозитория - ничего не затирается)
git init --shared
# Задаём ряд параметров Git-репозитория
git config user.name "${GIT_USER_SNAME}"
git config user.email "${GIT_USER_MAIL}"
git config core.sharedrepository true
# Разрешаем выделенному для операций с репозиториями пользователю запись в корневую директорию сайта
setfacl --modify user:git-${GROUP}:rwX "${WORK_DIR}"
# Переводим ресурсы репозитория во владение соответствующей ему группе пользователей сайтов
chown -R root:www-${GROUP} "${WORK_DIR}/.git"
chmod -R ug+rw "${WORK_DIR}/.git"
chmod -R o-rw "${WORK_DIR}/.git"
#
setfacl --recursive --default --modify group:www-${GROUP}:rwX "${WORK_DIR}/.git"
#
chown -R root:www-${GROUP} "${WORK_DIR}/.gitignore"
chmod -R ug+rw "${WORK_DIR}/.gitignore"
chmod -R o-rw "${WORK_DIR}/.gitignore"
exit ${?}
GROUP=${1}
SITE=${2}
WORK_DIR="/var/www/${GROUP}/${SITE}"
#
GIT_USER_SNAME="User Name"
GIT_USER_MAIL="email@example.net"
# Включаем дублирование вывода в журнальный файл
exec &> >(col -bp | tee -a "$(dirname ${0})/add.log")
# Фиксируем дату и время запуска процедуры
echo; echo $(date +"%Y-%m-%d.%H:%M:%S:")
# Проверяем наличие ожидаемых утилит
[ -x "$(command -v git)" ] && [ -x "$(command -v setfacl)" ] || { echo "Не обнаружен необходимый для работы набор утилит. Операция создания Git-репозитория прервана."; exit 1; }
# Проверяем корректность вводимых данных и даём подсказку в случае необходимости
# (если репозиторий уже имеется - ничего страшного - профилактически обновим структуру директорий и разрешения доступа)
#
[ ! "${1}" ] && { echo "Ожидаемый формат: ${0} \"group\" \"domain.name\""; echo "Не указано название \"группы сайтов\". Операция создания репозитория сайта прервана."; exit 1; }
#
[ ! "${2}" ] && { echo "Ожидаемый формат: ${0} \"${GROUP}\" \"domain.name\""; echo "Не указано имя сайта. Операция создания репозитория сайта прервана."; exit 1; }
[ $(echo "${SITE}" | grep -i -c -e "^www\.") -ne "0" ] && { echo "Имя сайта необходимо указывать без префикса \"www.\". Операция создания репозитория сайта прервана."; exit 1; }
# Проверяем наличие корневой директории сайта, для которого создаётся репозиторий и прерываем работу при отсутствии таковой
[ ! -d "${WORK_DIR}" ] && { echo "Корневая директория \"${WORK_DIR}\" сайта, для которого запрошено создание репозитория, отсутствует. Операция создания репозитория сайта прервана."; exit 1; }
# Проверяем наличие выделенной для указанной "группы сайтов" группы пользователей
grep -q -E "^www-${GROUP}:" /etc/group > /dev/null 2>&1
[ "${?}" -ne "0" ] && { echo "Группа пользователей \"www-${GROUP}\" не найдена! Операция добавления репозитория \"${WORK_DIR}\" прервана."; exit 1; }
# Проверяем наличие выделенного для фоновых операций с Git-репозиториями пользователя
id git-${GROUP} > /dev/null 2>&1
[ "${?}" -ne "0" ] && { echo "Пользователь \"git-${GROUP}\" для фоновых операций с Git-репозиториями не найден! Операция добавления репозитория \"${WORK_DIR}\" прервана."; exit 1; }
# Переходим в директорию репозитория
cd "${WORK_DIR}"
# Перестраховываясь, зачищаем переменную месторасположения репозитория предыдущего сеанса работы
unset GIT_DIR
# Инициализируем репозиторий (это безопасно для уже имеющегося репозитория - ничего не затирается)
git init --shared
# Задаём ряд параметров Git-репозитория
git config user.name "${GIT_USER_SNAME}"
git config user.email "${GIT_USER_MAIL}"
git config core.sharedrepository true
# Разрешаем выделенному для операций с репозиториями пользователю запись в корневую директорию сайта
setfacl --modify user:git-${GROUP}:rwX "${WORK_DIR}"
# Переводим ресурсы репозитория во владение соответствующей ему группе пользователей сайтов
chown -R root:www-${GROUP} "${WORK_DIR}/.git"
chmod -R ug+rw "${WORK_DIR}/.git"
chmod -R o-rw "${WORK_DIR}/.git"
#
setfacl --recursive --default --modify group:www-${GROUP}:rwX "${WORK_DIR}/.git"
#
chown -R root:www-${GROUP} "${WORK_DIR}/.gitignore"
chmod -R ug+rw "${WORK_DIR}/.gitignore"
chmod -R o-rw "${WORK_DIR}/.gitignore"
exit ${?}
Пример использования (будет создан Git-репозиторий: "/var/www/group0/site.example.net/.git"):
# /usr/local/etc/gitman/addgitrepo.sh group0 site.example.net
Разрешение web-серверу запускать процедуры "деплоя".
Учитывая то, что уведомления о событиях изменения содержимого Git-репозитория мы будем получать через web-сервер посредством POST-запроса, проще всего (в отрыве от абстрактных рассуждений о концепции изолирования сред исполнения) организовать запуск CI/CD-процедур в контексте обработки информационного сообщения. Для этого нам понадобится разрешить запуск ограниченного перечня приложений от имени web-сервера.
Используя функционал расширения конфигурации SUDO добавляем правила в отдельные файлы конфигурации, разрешая пользователю, от имени которого запущен web-сервер, простую прямую загрузку данных из Git-репозитория непосредственно в директории web-сайтов:
# vi /etc/sudoers.d/web-deploy-users
www-data ALL=(root:root) NOPASSWD: /usr/bin/git
Проверяем синтаксическую корректность вносимых изменений:
# visudo -cf /etc/sudoers.d/web-deploy-users
Ограничиваем доступ к файлу конфигурации:
# chown root:root /etc/sudoers.d/web-deploy-users && chmod -R go-rwx /etc/sudoers.d/web-deploy-users
Подготовка среды автоматизации обработки входящих "Webhook"-ов.
Приём поступающих POST-запросов и запуск соответствующего им Bash-скрипта будем производить в рамках самой простой схемы:
"web-сервер (Nginx)" -> "CGI-шлюз (Spawn-FCGI)" -> "скрипт анализа (Bash)" -> "CD-скрипт (Bash)".
Устанавливаем легковесный web-сервер "Nginx" и программную обёртку "FCGI-Wrap" для передачи CGI-запросов произвольным скриптам:
# aptitude install nginx-light fcgiwrap
Сразу после установки запускаются два сервиса: приложение "fcgiwrap" как таковое и "сервис сокета" - это такой способ создать файловый сокет средствами "Systemd" (скрипт старого образца "/etc/init.d/fcgiwrap" запускал сервис и создавал для него "сокет" в один проход):
# systemctl status fcgiwrap.service
# systemctl status fcgiwrap.socket
# systemctl status fcgiwrap.socket
По умолчанию CGI-"обёртка" запускается в контексте пользователя "www-data", в котором также работает и web-сервер "Nginx" - меня это полностью устраивает и что-либо менять не вижу необходимости.
Настройка связи web-сервера и CGI-обёртки.
Для приёма обращений и передаче их CGI-шлюзу проще всего завести отдельный сайт (но можно и через "location" в любом имеющемся, конечно):
# vi /etc/nginx/sites-available/webhooks.example.net.conf
# Перехватываем все запросы по протоколу без шифрования и перенаправляем их на HTTPS
server {
listen 80;
server_name webhooks.example.net;
location / {
rewrite ^ https://$host$request_uri permanent;
}
}
# Описывем рабочее окружение web-сайта как такового
server {
listen 443 ssl http2;
server_name webhooks.example.net;
# Принудительно переводим сайт на работу только через SSL
ssl on;
include /etc/nginx/ssl_wildcard.conf;
# Отправляем неудовлетворяемые запросы "в никуда"
location @blackhole {return 444;}
error_page 500 502 503 504 =444 @blackhole;
try_files $uri =444; # =403
# Отлавливаем все обращения к CGI-скриптам и направляем их в FCGI-Wrap
location ~ ^/webhook/.*\.cgi$ {
# Не сжимаем обращения к CGI-скриптам
gzip off;
default_type "text/plain";
# Указываем корневую директорию ресурса
root /var/www/cgi-bin;
# Блокируем доступ к "закрытым" ресурсам
location ~* /(bin|conf|lib|log)/ { deny all; }
# Указываем параметры и точку обращений к обработчику CGI-запросов
include /etc/nginx/fastcgi_params;
fastcgi_pass unix:/var/run/fcgiwrap.socket;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
server {
listen 80;
server_name webhooks.example.net;
location / {
rewrite ^ https://$host$request_uri permanent;
}
}
# Описывем рабочее окружение web-сайта как такового
server {
listen 443 ssl http2;
server_name webhooks.example.net;
# Принудительно переводим сайт на работу только через SSL
ssl on;
include /etc/nginx/ssl_wildcard.conf;
# Отправляем неудовлетворяемые запросы "в никуда"
location @blackhole {return 444;}
error_page 500 502 503 504 =444 @blackhole;
try_files $uri =444; # =403
# Отлавливаем все обращения к CGI-скриптам и направляем их в FCGI-Wrap
location ~ ^/webhook/.*\.cgi$ {
# Не сжимаем обращения к CGI-скриптам
gzip off;
default_type "text/plain";
# Указываем корневую директорию ресурса
root /var/www/cgi-bin;
# Блокируем доступ к "закрытым" ресурсам
location ~* /(bin|conf|lib|log)/ { deny all; }
# Указываем параметры и точку обращений к обработчику CGI-запросов
include /etc/nginx/fastcgi_params;
fastcgi_pass unix:/var/run/fcgiwrap.socket;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
Пример конфигурационного файла подключения SSL-сертификатов:
# vi /etc/nginx/ssl_wildcard.conf
ssl_dhparam /etc/ssl/nginx/dhparam.pem;
ssl_certificate /etc/ssl/nginx/wildcard.example.net.crt;
ssl_certificate_key /etc/ssl/nginx/wildcard.example.net.key.decrypt;
ssl_protocols SSLv2 SSLv3 TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl on;
ssl_certificate /etc/ssl/nginx/wildcard.example.net.crt;
ssl_certificate_key /etc/ssl/nginx/wildcard.example.net.key.decrypt;
ssl_protocols SSLv2 SSLv3 TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl on;
Активируем конфигурацию, проверяем её средствами web-сервер и применяем:
# ln -s /etc/nginx/sites-available/webhooks.example.net.conf /etc/nginx/sites-enabled/webhooks.example.net.conf
# nginx -t && /etc/init.d/nginx reload
# nginx -t && /etc/init.d/nginx reload
Подготовка конфигурации сопоставления Git-репозитория и сайта на web-сервере.
Создаём структуру директорий для размещения в ней скриптов обработки запросов, конфигурационных файлов обрабатываемых пар "репозиторий - сайт" и журналов событий:
# mkdir -p /var/www/cgi-bin/webhook /var/www/cgi-bin/webhook/lib /var/www/cgi-bin/webhook/conf /var/www/cgi-bin/webhook/log
Прежде чем разрабатывать скрипты, определимся с параметрами, в достаточной мере описывающими связь между внешним Git-репозиторием, уведомления о событиях в котором мы будем получать, и обслуживаемым web-сервером сайтом, в которые предполагается загружать данные из внешнего Git-репозитория.
Пример конфигурационного файла:
# cd /var/www/cgi-bin/webhook/conf
# vi ./test-test-testing.conf && chown www-data:www-data ./test-test-testing.conf && chmod o-rwx ./test-test-testing.conf
# vi ./test-test-testing.conf && chown www-data:www-data ./test-test-testing.conf && chmod o-rwx ./test-test-testing.conf
##
# (required file name format: ./groupname-reponame-branch.conf) - in lowercase! #
# (supported branches: {master|staging|testing|develop}) - in lowercase! #
##
# Секретный ключ, которым подписывается тело уведомления от удалённого репозитория, или пароль простой аутентификации
WEBHOOK_SECRET_KEY="keyForPOSTverify"
# Задаём метод доставки и применения данных
CD_MODE="direct" # {direct|external}
# Адрес удалённого Git-репозитория, из которого будут загружаться данные
GIT_REMOTE="ssh://git@gitlab.example.net/test/test.git"
# Пользователь, от имени которого следует обращаться к удалённому репозиторию
GIT_USER="git-test"
# Месторасположение локального репозитория
GIT_DIR="/var/www/test/test"
# (required file name format: ./groupname-reponame-branch.conf) - in lowercase! #
# (supported branches: {master|staging|testing|develop}) - in lowercase! #
##
# Секретный ключ, которым подписывается тело уведомления от удалённого репозитория, или пароль простой аутентификации
WEBHOOK_SECRET_KEY="keyForPOSTverify"
# Задаём метод доставки и применения данных
CD_MODE="direct" # {direct|external}
# Адрес удалённого Git-репозитория, из которого будут загружаться данные
GIT_REMOTE="ssh://git@gitlab.example.net/test/test.git"
# Пользователь, от имени которого следует обращаться к удалённому репозиторию
GIT_USER="git-test"
# Месторасположение локального репозитория
GIT_DIR="/var/www/test/test"
Обращаю внимание на то, что web-сервисы "GitHub" и "BitBucket Clowd" выдают ссылку на подключение к Git-репозиторию посредством SSH с разделителем между доменным именем и адресом репозитория в виде символа двоеточия ":" (примерно так: "git@github.com:test/test.git") и без явного указания используемого протокола в виде "ssh://" - это так называемый SCP-совместимый синтаксис, который можно использовать как есть, а можно для унификации преобразовать в формат строки с указанием протокола (примерно так: "ssh://git@github.com/test/test.git") и заменой двоеточия на "слэш".
Автоматизация обработки входящих "webhook"-ов.
Пишем Bash-скрипт приёма "webhook"-уведомлений от Git-репозитория и запуска процедур доставки данных или передачи управления внешним инструментам:
# cd /var/www/cgi-bin/webhook
# vi ./notice-repo-update.cgi && chmod +x ./notice-repo-update.cgi && chown www-data:www-data ./notice-repo-update.cgi && chmod o-rwx ./notice-repo-update.cgi
# vi ./notice-repo-update.cgi && chmod +x ./notice-repo-update.cgi && chown www-data:www-data ./notice-repo-update.cgi && chmod o-rwx ./notice-repo-update.cgi
#!/bin/bash
DATE=$(date +"%Y-%m-%d %H:%M:%S")
LOG="$(dirname ${0})/log/$(basename $0).log"
FORTH=true
INITIATOR=""
STATUS=""
# Запрещаем запускать скрипт от имени суперпользователя
[ "`id --user`" = "0" ] && { echo "Status: 500 Internal Server Error"; echo "Content-type: text/plain"; echo ""; exit 500; }
# Принимаем запросы только через HTTPS-соединение
# (GitLab не подписывает свои уведомления, высылая пароль в открытом виде - взаимодействуем по шифрованному соединению)
[ "${HTTPS}" != "on" ] && { echo "Status: 406 Not Acceptable"; echo "Content-type: text/plain"; echo ""; exit 406; }
# Проверяем наличие ожидаемых утилит
[ -x "$(command -v git)" ] && [ -x "$(command -v jq)" ] || { echo "Status: 500 Internal Server Error"; echo "Content-type: text/plain"; echo ""; exit 500; }
# Первым делом читаем прикреплённые к запросу POST-данные
# (от BitBucket и GitLab ожидается JSON)
read IN_POST_STRING
# Первое ветвление логики, в зависимости от типа запроса
# (отвечаем на запрос проверки связи от BitBucket)
if [ "${HTTP_X_EVENT_KEY}" = "diagnostics:ping" ] ; then
STATUS="200 Ok"
# (отвечаем на запрос проверки связи от GitHub)
elif [ "${HTTP_X_GITHUB_EVENT}" = "ping" ] ; then
STATUS="200 Ok"
# (во всех остальных услучаях ожидается наличие POST-запроса)
elif [ ! -z "${IN_POST_STRING}" ] ; then
# Подключаем функцию обработки входящих переменных
source ${PWD}/lib/preset-inc-var.sh.snippet
if [ "${HTTP_X_EVENT_KEY}" = "repo:refs_changed" ] ; then
# (BitBucket Standalone)
INITIATOR="bitbucket"
declare -f preset-inc-bitbucket >/dev/null && preset-inc-bitbucket || { FORTH=false; }
elif [ "${HTTP_X_GITLAB_EVENT}" = "Push Hook" ] ; then
# (GitLab Standalone)
INITIATOR="gitlab"
declare -f preset-inc-gitlab >/dev/null && preset-inc-gitlab || { FORTH=false; }
elif [ "${HTTP_X_EVENT_KEY}" = "repo:push" ] ; then
# (BitBucket Cloud)
INITIATOR="bitbucket-cloud"
declare -f preset-inc-bitbucket-cloud >/dev/null && preset-inc-bitbucket-cloud || { FORTH=false; }
elif [ "${HTTP_X_GITHUB_EVENT}" = "push" ] ; then
# (GitHub Cloud)
INITIATOR="github"
declare -f preset-inc-github >/dev/null && preset-inc-github || { FORTH=false; }
else
FORTH=false
STATUS="400 Bad Request"
echo "${DATE}: Некорректный запрос, неопределённый тип источника." >> "${LOG}"
fi
# Приступаем к исполнению логики обработки события
if [ "${FORTH}" = "true" ] ; then
# Реагируем только на событие "Push"
if [[ "${IN_EVENT}" = "repo:refs_changed" || "${IN_EVENT}" = "push" ]] ; then
# В зависимости от заданного способа "деплоя" переходим к соответствующим модулям
if [[ -z "${CD_MODE}" || "${CD_MODE}" = "direct" ]] ; then
source ${PWD}/lib/cd-direct-worker.sh.snippet 2>/dev/null
declare -f cd-direct >/dev/null && cd-direct || { FORTH=false; }
elif [[ "${CD_MODE}" = "external" ]] ; then
# (заготовка для функционала, здесь не рассматриваемого)
source ${PWD}/lib/cd-external-wrapper.sh.snippet 2>/dev/null
declare -f cd-external >/dev/null && cd-external || { FORTH=false; }
fi
else
STATUS="200 Ok"
echo "${DATE}: Запрос игнорируется: обработка события \"${IN_EVENT}\" этим локальным репозиторием не поддерживается." >> "${LOG}"
fi
fi
else
STATUS="400 Bad Request"
echo "${DATE}: Некорректный запрос, не содержащий POST-данные." >> "${LOG}"
fi
# Формальный ненулевой ответ для корректного завершения HTTP-сеанса
# (обязательно для работы схемы: "Nginx -> FastCGI -> FCGIWraper -> Script")
echo "Status: ${STATUS}"
echo "Content-type: text/plain"
echo ""
exit ${?}
DATE=$(date +"%Y-%m-%d %H:%M:%S")
LOG="$(dirname ${0})/log/$(basename $0).log"
FORTH=true
INITIATOR=""
STATUS=""
# Запрещаем запускать скрипт от имени суперпользователя
[ "`id --user`" = "0" ] && { echo "Status: 500 Internal Server Error"; echo "Content-type: text/plain"; echo ""; exit 500; }
# Принимаем запросы только через HTTPS-соединение
# (GitLab не подписывает свои уведомления, высылая пароль в открытом виде - взаимодействуем по шифрованному соединению)
[ "${HTTPS}" != "on" ] && { echo "Status: 406 Not Acceptable"; echo "Content-type: text/plain"; echo ""; exit 406; }
# Проверяем наличие ожидаемых утилит
[ -x "$(command -v git)" ] && [ -x "$(command -v jq)" ] || { echo "Status: 500 Internal Server Error"; echo "Content-type: text/plain"; echo ""; exit 500; }
# Первым делом читаем прикреплённые к запросу POST-данные
# (от BitBucket и GitLab ожидается JSON)
read IN_POST_STRING
# Первое ветвление логики, в зависимости от типа запроса
# (отвечаем на запрос проверки связи от BitBucket)
if [ "${HTTP_X_EVENT_KEY}" = "diagnostics:ping" ] ; then
STATUS="200 Ok"
# (отвечаем на запрос проверки связи от GitHub)
elif [ "${HTTP_X_GITHUB_EVENT}" = "ping" ] ; then
STATUS="200 Ok"
# (во всех остальных услучаях ожидается наличие POST-запроса)
elif [ ! -z "${IN_POST_STRING}" ] ; then
# Подключаем функцию обработки входящих переменных
source ${PWD}/lib/preset-inc-var.sh.snippet
if [ "${HTTP_X_EVENT_KEY}" = "repo:refs_changed" ] ; then
# (BitBucket Standalone)
INITIATOR="bitbucket"
declare -f preset-inc-bitbucket >/dev/null && preset-inc-bitbucket || { FORTH=false; }
elif [ "${HTTP_X_GITLAB_EVENT}" = "Push Hook" ] ; then
# (GitLab Standalone)
INITIATOR="gitlab"
declare -f preset-inc-gitlab >/dev/null && preset-inc-gitlab || { FORTH=false; }
elif [ "${HTTP_X_EVENT_KEY}" = "repo:push" ] ; then
# (BitBucket Cloud)
INITIATOR="bitbucket-cloud"
declare -f preset-inc-bitbucket-cloud >/dev/null && preset-inc-bitbucket-cloud || { FORTH=false; }
elif [ "${HTTP_X_GITHUB_EVENT}" = "push" ] ; then
# (GitHub Cloud)
INITIATOR="github"
declare -f preset-inc-github >/dev/null && preset-inc-github || { FORTH=false; }
else
FORTH=false
STATUS="400 Bad Request"
echo "${DATE}: Некорректный запрос, неопределённый тип источника." >> "${LOG}"
fi
# Приступаем к исполнению логики обработки события
if [ "${FORTH}" = "true" ] ; then
# Реагируем только на событие "Push"
if [[ "${IN_EVENT}" = "repo:refs_changed" || "${IN_EVENT}" = "push" ]] ; then
# В зависимости от заданного способа "деплоя" переходим к соответствующим модулям
if [[ -z "${CD_MODE}" || "${CD_MODE}" = "direct" ]] ; then
source ${PWD}/lib/cd-direct-worker.sh.snippet 2>/dev/null
declare -f cd-direct >/dev/null && cd-direct || { FORTH=false; }
elif [[ "${CD_MODE}" = "external" ]] ; then
# (заготовка для функционала, здесь не рассматриваемого)
source ${PWD}/lib/cd-external-wrapper.sh.snippet 2>/dev/null
declare -f cd-external >/dev/null && cd-external || { FORTH=false; }
fi
else
STATUS="200 Ok"
echo "${DATE}: Запрос игнорируется: обработка события \"${IN_EVENT}\" этим локальным репозиторием не поддерживается." >> "${LOG}"
fi
fi
else
STATUS="400 Bad Request"
echo "${DATE}: Некорректный запрос, не содержащий POST-данные." >> "${LOG}"
fi
# Формальный ненулевой ответ для корректного завершения HTTP-сеанса
# (обязательно для работы схемы: "Nginx -> FastCGI -> FCGIWraper -> Script")
echo "Status: ${STATUS}"
echo "Content-type: text/plain"
echo ""
exit ${?}
Выносим в подключаемый файл подборку функций, предназначенных для обработки входящих переменных, подтверждения подлинности уведомления и подключения локального файла конфигурации связи "репозиторий - сайт":
# cd /var/www/cgi-bin/webhook/webhook/lib
# vi ./preset-inc-var.sh.snippet && chown www-data:www-data ./preset-inc-var.sh.snippet && chmod o-rwx ./preset-inc-var.sh.snippet
# vi ./preset-inc-var.sh.snippet && chown www-data:www-data ./preset-inc-var.sh.snippet && chmod o-rwx ./preset-inc-var.sh.snippet
#!/bin/bash
# This file contains the code snippet for the shell Bash v.4 (Bourne again shell)
# Файл содержит фрагмент кода для командного интерпретатора Bash v.4 (Bourne again shell)
# Функция разбора входящих переменных от "BitBucket (Standalone) Server"
function preset-inc-bitbucket {
# Вычленяем параметры состояния и адресации удалённого репозитория
IN_EVENT=$(echo "${IN_POST_STRING}" | jq -r '.eventKey' 2>/dev/null)
IN_GROUPNAME=$(echo "${IN_POST_STRING}" | jq -r '.repository.project.key' 2>/dev/null | tr '[:upper:]' '[:lower:]')
IN_REPONAME=$(echo "${IN_POST_STRING}" | jq -r '.repository.name' 2>/dev/null | tr '[:upper:]' '[:lower:]')
IN_BRANCH=$(echo "${IN_POST_STRING}" | jq -r '.changes[].ref.displayId' 2>/dev/null)
# Извлекаем (если имеется) HMAC-подпись (sha256) тела POST-запроса, поступившую от BitBucket
IN_SIGNATURE=$(echo "${HTTP_X_HUB_SIGNATURE}" | awk -F "=" '{print $2}')
# Проверяем получение необходимых сведений об изменённом репозитории
if [[ ! -z "${IN_GROUPNAME}" && ! -z "${IN_REPONAME}" && ! -z "${IN_BRANCH}" ]] ; then
# Запрашиваем набор переменных локальной конфигурации
declare -f preset-var >/dev/null && preset-var || { FORTH=false; }
else
FORTH=false
STATUS="422 Unprocessable Entity"
echo "${DATE}: Некорректный запрос: не все ожидаемые входные параметры \"${IN_GROUPNAME}\", \"${IN_REPONAME}\" и \"${IN_BRANCH}\" присутствуют." >> "${LOG}"
fi
# Если какой-то из сторон обмена данными затребована HMAC-подпись тела POST-запроса, то проверяем её
if [ "${FORTH}" = "true" ] && [[ ! -z "${IN_SIGNATURE}" || ! -z "${WEBHOOK_SECRET_KEY}" ]] ; then
# Вычисляем сигнатуру HMAC-SHA256 тела POST-запроса и сверяем с предоставленной подписью
HMAC_SIGNATURE=$(echo -n "${IN_POST_STRING}" | openssl dgst -sha256 -hmac "${WEBHOOK_SECRET_KEY}" -r | awk '{print $1}')
if [ "${IN_SIGNATURE}" != "${HMAC_SIGNATURE}" ] ; then
FORTH=false
STATUS="401 Unauthorized"
echo "${DATE}: Сигнатура HMAC-SHA256 подписи тела POST-запроса не соответствует указанному \"секретному ключу\"." >> "${LOG}"
fi
fi
return ${?}
}
# Функция разбора входящих переменных от "GitLab (Standalone)"
function preset-inc-gitlab {
# Вычленяем параметры состояния и адресации удалённого репозитория
IN_EVENT=$(echo "${IN_POST_STRING}" | jq -r '.object_kind' 2>/dev/null)
IN_GROUPNAME=$(echo "${IN_POST_STRING}" | jq -r '.project.namespace' 2>/dev/null | tr '[:upper:]' '[:lower:]')
IN_REPONAME=$(echo "${IN_POST_STRING}" | jq -r '.project.name' 2>/dev/null | tr '[:upper:]' '[:lower:]')
IN_BRANCH=$(echo "${IN_POST_STRING}" | jq -r '.ref' 2>/dev/null | awk -F "/" '{print $3}')
# Извлекаем (если имеется) пароль (передаваемый в открытом виде!) простейшей аутентификации, поступивший от GitLab
IN_TOKEN="${HTTP_X_GITLAB_TOKEN}"
# Проверяем получение необходимых сведений об изменённом репозитории
if [[ ! -z "${IN_GROUPNAME}" && ! -z "${IN_REPONAME}" && ! -z "${IN_BRANCH}" ]] ; then
# Запрашиваем набор переменных локальной конфигурации
declare -f preset-var >/dev/null && preset-var || { FORTH=false; }
else
FORTH=false
STATUS="422 Unprocessable Entity"
echo "${DATE}: Некорректный запрос: не все ожидаемые входные параметры \"${IN_GROUPNAME}\", \"${IN_REPONAME}\" и \"${IN_BRANCH}\" присутствуют." >> "${LOG}"
fi
# Если какой-то из сторон обмена данными затребована аутентификация "секретным ключём", то проводим её
if [ "${FORTH}" = "true" ] && [[ ! -z "${IN_TOKEN}" || ! -z "${WEBHOOK_SECRET_KEY}" ]] ; then
# Сверяем "секретный ключ" с предоставленным в POST-запросе
# (удивительно небезопасно, но "GitLab" шлёт пароль сверки открытым текстом)
if [ "${IN_TOKEN}" != "${WEBHOOK_SECRET_KEY}" ] ; then
FORTH=false
STATUS="401 Unauthorized"
echo "${DATE}: Предоставленный в POST-запросе \"секретный ключ\" не соответствует заданному." >> "${LOG}"
fi
fi
return ${?}
}
# Функция разбора входящих переменных от "BitBucket Cloud"
function preset-inc-bitbucket-cloud {
# Вычленяем параметры состояния и адресации удалённого репозитория
IN_EVENT=$(echo "${HTTP_X_EVENT_KEY}" | awk -F ":" '{print $2}')
IN_GROUPNAME=$(echo "${IN_POST_STRING}" | jq -r '.repository.owner.username' 2>/dev/null | tr '[:upper:]' '[:lower:]')
IN_REPONAME=$(echo "${IN_POST_STRING}" | jq -r '.repository.name' 2>/dev/null | tr '[:upper:]' '[:lower:]')
IN_BRANCH=$(echo "${IN_POST_STRING}" | jq -r '.push.changes[].new.name' 2>/dev/null)
# Проверяем получение необходимых сведений об изменённом репозитории
if [[ ! -z "${IN_GROUPNAME}" && ! -z "${IN_REPONAME}" && ! -z "${IN_BRANCH}" ]] ; then
# Запрашиваем набор переменных локальной конфигурации
declare -f preset-var >/dev/null && preset-var || { FORTH=false; }
else
FORTH=false
STATUS="422 Unprocessable Entity"
echo "${DATE}: Некорректный запрос: не все ожидаемые входные параметры \"${IN_GROUPNAME}\", \"${IN_REPONAME}\" и \"${IN_BRANCH}\" присутствуют." >> "${LOG}"
fi
# Web-cервис "BitBucket Cloud" не поддерживает проверки подлинности "webhook"-а (ни пароля , ни HMAC-подписи тела POST-запроса).
return ${?}
}
# Функция разбора входящих переменных от "GitHub (Cloud)"
function preset-inc-github {
# Вычленяем параметры состояния и адресации удалённого репозитория
IN_EVENT="${HTTP_X_GITHUB_EVENT}"
IN_GROUPNAME=$(echo "${IN_POST_STRING}" | jq -r '.repository.owner.name' 2>/dev/null | tr '[:upper:]' '[:lower:]')
IN_REPONAME=$(echo "${IN_POST_STRING}" | jq -r '.repository.name' 2>/dev/null | tr '[:upper:]' '[:lower:]')
IN_BRANCH=$(echo "${IN_POST_STRING}" | jq -r '.ref' 2>/dev/null | awk -F "/" '{print $3}')
# Извлекаем (если имеется) HMAC-подпись (sha1) тела POST-запроса, поступившую от GitLab
IN_SIGNATURE=$(echo "${HTTP_X_HUB_SIGNATURE}" | awk -F "=" '{print $2}')
# Проверяем получение необходимых сведений об изменённом репозитории
if [[ ! -z "${IN_GROUPNAME}" && ! -z "${IN_REPONAME}" && ! -z "${IN_BRANCH}" ]] ; then
# Запрашиваем набор переменных локальной конфигурации
declare -f preset-var >/dev/null && preset-var || { FORTH=false; }
else
FORTH=false
STATUS="422 Unprocessable Entity"
echo "${DATE}: Некорректный запрос: не все ожидаемые входные параметры \"${IN_GROUPNAME}\", \"${IN_REPONAME}\" и \"${IN_BRANCH}\" присутствуют." >> "${LOG}"
fi
# Если какой-то из сторон обмена данными затребована HMAC-подпись тела POST-запроса, то проверяем её
if [ "${FORTH}" = "true" ] && [[ ! -z "${IN_SIGNATURE}" || ! -z "${WEBHOOK_SECRET_KEY}" ]] ; then
# Вычисляем сигнатуру HMAC-SHA1 тела POST-запроса и сверяем с предоставленной подписью
HMAC_SIGNATURE=$(echo -n "${IN_POST_STRING}" | openssl dgst -sha1 -hmac "${WEBHOOK_SECRET_KEY}" -r | awk '{print $1}')
if [ "${IN_SIGNATURE}" != "${HMAC_SIGNATURE}" ] ; then
FORTH=false
STATUS="401 Unauthorized"
echo "${DATE}: Сигнатура HMAC-SHA256 подписи тела POST-запроса не соответствует указанному \"секретному ключу\"." >> "${LOG}"
fi
fi
return ${?}
}
# Функция загрузки локальных переменных
function preset-var {
# Проверяем наличие соответствующего запросу конфигурационного файла
if [ -f "${PWD}/conf/${IN_GROUPNAME}-${IN_REPONAME}-${IN_BRANCH}.conf" ] ; then
# Читаем соответствующий запросу конфигурационный файл с параметрами связки удалённого и локального репозиториев
source "${PWD}/conf/${IN_GROUPNAME}-${IN_REPONAME}-${IN_BRANCH}.conf" 2>/dev/null
if [[ "${?}" -eq "0" && ! -z "${GIT_REMOTE}" ]] ; then
# Для очевидности в дальнейшей логике задаём явно переменную имени задействуемой ветви репозитория
GIT_BRANCH="${IN_BRANCH}"
else
FORTH=false
STATUS="422 Unprocessable Entity"
echo "${DATE}: Некорректно содержимое конфигурационного файла \"${PWD}/conf/${IN_GROUPNAME}-${IN_REPONAME}-${IN_BRANCH}.conf\" или не все обязательные параметры присутствуют." >> "${LOG}"
fi
else
FORTH=false
STATUS="200 Ok"
echo "${DATE}: Запрос игнорируется: обработка событий в ветви \"${IN_BRANCH}\" этим локальным репозиторием не поддерживается." >> "${LOG}"
fi
return ${?}
}
# This file contains the code snippet for the shell Bash v.4 (Bourne again shell)
# Файл содержит фрагмент кода для командного интерпретатора Bash v.4 (Bourne again shell)
# Функция разбора входящих переменных от "BitBucket (Standalone) Server"
function preset-inc-bitbucket {
# Вычленяем параметры состояния и адресации удалённого репозитория
IN_EVENT=$(echo "${IN_POST_STRING}" | jq -r '.eventKey' 2>/dev/null)
IN_GROUPNAME=$(echo "${IN_POST_STRING}" | jq -r '.repository.project.key' 2>/dev/null | tr '[:upper:]' '[:lower:]')
IN_REPONAME=$(echo "${IN_POST_STRING}" | jq -r '.repository.name' 2>/dev/null | tr '[:upper:]' '[:lower:]')
IN_BRANCH=$(echo "${IN_POST_STRING}" | jq -r '.changes[].ref.displayId' 2>/dev/null)
# Извлекаем (если имеется) HMAC-подпись (sha256) тела POST-запроса, поступившую от BitBucket
IN_SIGNATURE=$(echo "${HTTP_X_HUB_SIGNATURE}" | awk -F "=" '{print $2}')
# Проверяем получение необходимых сведений об изменённом репозитории
if [[ ! -z "${IN_GROUPNAME}" && ! -z "${IN_REPONAME}" && ! -z "${IN_BRANCH}" ]] ; then
# Запрашиваем набор переменных локальной конфигурации
declare -f preset-var >/dev/null && preset-var || { FORTH=false; }
else
FORTH=false
STATUS="422 Unprocessable Entity"
echo "${DATE}: Некорректный запрос: не все ожидаемые входные параметры \"${IN_GROUPNAME}\", \"${IN_REPONAME}\" и \"${IN_BRANCH}\" присутствуют." >> "${LOG}"
fi
# Если какой-то из сторон обмена данными затребована HMAC-подпись тела POST-запроса, то проверяем её
if [ "${FORTH}" = "true" ] && [[ ! -z "${IN_SIGNATURE}" || ! -z "${WEBHOOK_SECRET_KEY}" ]] ; then
# Вычисляем сигнатуру HMAC-SHA256 тела POST-запроса и сверяем с предоставленной подписью
HMAC_SIGNATURE=$(echo -n "${IN_POST_STRING}" | openssl dgst -sha256 -hmac "${WEBHOOK_SECRET_KEY}" -r | awk '{print $1}')
if [ "${IN_SIGNATURE}" != "${HMAC_SIGNATURE}" ] ; then
FORTH=false
STATUS="401 Unauthorized"
echo "${DATE}: Сигнатура HMAC-SHA256 подписи тела POST-запроса не соответствует указанному \"секретному ключу\"." >> "${LOG}"
fi
fi
return ${?}
}
# Функция разбора входящих переменных от "GitLab (Standalone)"
function preset-inc-gitlab {
# Вычленяем параметры состояния и адресации удалённого репозитория
IN_EVENT=$(echo "${IN_POST_STRING}" | jq -r '.object_kind' 2>/dev/null)
IN_GROUPNAME=$(echo "${IN_POST_STRING}" | jq -r '.project.namespace' 2>/dev/null | tr '[:upper:]' '[:lower:]')
IN_REPONAME=$(echo "${IN_POST_STRING}" | jq -r '.project.name' 2>/dev/null | tr '[:upper:]' '[:lower:]')
IN_BRANCH=$(echo "${IN_POST_STRING}" | jq -r '.ref' 2>/dev/null | awk -F "/" '{print $3}')
# Извлекаем (если имеется) пароль (передаваемый в открытом виде!) простейшей аутентификации, поступивший от GitLab
IN_TOKEN="${HTTP_X_GITLAB_TOKEN}"
# Проверяем получение необходимых сведений об изменённом репозитории
if [[ ! -z "${IN_GROUPNAME}" && ! -z "${IN_REPONAME}" && ! -z "${IN_BRANCH}" ]] ; then
# Запрашиваем набор переменных локальной конфигурации
declare -f preset-var >/dev/null && preset-var || { FORTH=false; }
else
FORTH=false
STATUS="422 Unprocessable Entity"
echo "${DATE}: Некорректный запрос: не все ожидаемые входные параметры \"${IN_GROUPNAME}\", \"${IN_REPONAME}\" и \"${IN_BRANCH}\" присутствуют." >> "${LOG}"
fi
# Если какой-то из сторон обмена данными затребована аутентификация "секретным ключём", то проводим её
if [ "${FORTH}" = "true" ] && [[ ! -z "${IN_TOKEN}" || ! -z "${WEBHOOK_SECRET_KEY}" ]] ; then
# Сверяем "секретный ключ" с предоставленным в POST-запросе
# (удивительно небезопасно, но "GitLab" шлёт пароль сверки открытым текстом)
if [ "${IN_TOKEN}" != "${WEBHOOK_SECRET_KEY}" ] ; then
FORTH=false
STATUS="401 Unauthorized"
echo "${DATE}: Предоставленный в POST-запросе \"секретный ключ\" не соответствует заданному." >> "${LOG}"
fi
fi
return ${?}
}
# Функция разбора входящих переменных от "BitBucket Cloud"
function preset-inc-bitbucket-cloud {
# Вычленяем параметры состояния и адресации удалённого репозитория
IN_EVENT=$(echo "${HTTP_X_EVENT_KEY}" | awk -F ":" '{print $2}')
IN_GROUPNAME=$(echo "${IN_POST_STRING}" | jq -r '.repository.owner.username' 2>/dev/null | tr '[:upper:]' '[:lower:]')
IN_REPONAME=$(echo "${IN_POST_STRING}" | jq -r '.repository.name' 2>/dev/null | tr '[:upper:]' '[:lower:]')
IN_BRANCH=$(echo "${IN_POST_STRING}" | jq -r '.push.changes[].new.name' 2>/dev/null)
# Проверяем получение необходимых сведений об изменённом репозитории
if [[ ! -z "${IN_GROUPNAME}" && ! -z "${IN_REPONAME}" && ! -z "${IN_BRANCH}" ]] ; then
# Запрашиваем набор переменных локальной конфигурации
declare -f preset-var >/dev/null && preset-var || { FORTH=false; }
else
FORTH=false
STATUS="422 Unprocessable Entity"
echo "${DATE}: Некорректный запрос: не все ожидаемые входные параметры \"${IN_GROUPNAME}\", \"${IN_REPONAME}\" и \"${IN_BRANCH}\" присутствуют." >> "${LOG}"
fi
# Web-cервис "BitBucket Cloud" не поддерживает проверки подлинности "webhook"-а (ни пароля , ни HMAC-подписи тела POST-запроса).
return ${?}
}
# Функция разбора входящих переменных от "GitHub (Cloud)"
function preset-inc-github {
# Вычленяем параметры состояния и адресации удалённого репозитория
IN_EVENT="${HTTP_X_GITHUB_EVENT}"
IN_GROUPNAME=$(echo "${IN_POST_STRING}" | jq -r '.repository.owner.name' 2>/dev/null | tr '[:upper:]' '[:lower:]')
IN_REPONAME=$(echo "${IN_POST_STRING}" | jq -r '.repository.name' 2>/dev/null | tr '[:upper:]' '[:lower:]')
IN_BRANCH=$(echo "${IN_POST_STRING}" | jq -r '.ref' 2>/dev/null | awk -F "/" '{print $3}')
# Извлекаем (если имеется) HMAC-подпись (sha1) тела POST-запроса, поступившую от GitLab
IN_SIGNATURE=$(echo "${HTTP_X_HUB_SIGNATURE}" | awk -F "=" '{print $2}')
# Проверяем получение необходимых сведений об изменённом репозитории
if [[ ! -z "${IN_GROUPNAME}" && ! -z "${IN_REPONAME}" && ! -z "${IN_BRANCH}" ]] ; then
# Запрашиваем набор переменных локальной конфигурации
declare -f preset-var >/dev/null && preset-var || { FORTH=false; }
else
FORTH=false
STATUS="422 Unprocessable Entity"
echo "${DATE}: Некорректный запрос: не все ожидаемые входные параметры \"${IN_GROUPNAME}\", \"${IN_REPONAME}\" и \"${IN_BRANCH}\" присутствуют." >> "${LOG}"
fi
# Если какой-то из сторон обмена данными затребована HMAC-подпись тела POST-запроса, то проверяем её
if [ "${FORTH}" = "true" ] && [[ ! -z "${IN_SIGNATURE}" || ! -z "${WEBHOOK_SECRET_KEY}" ]] ; then
# Вычисляем сигнатуру HMAC-SHA1 тела POST-запроса и сверяем с предоставленной подписью
HMAC_SIGNATURE=$(echo -n "${IN_POST_STRING}" | openssl dgst -sha1 -hmac "${WEBHOOK_SECRET_KEY}" -r | awk '{print $1}')
if [ "${IN_SIGNATURE}" != "${HMAC_SIGNATURE}" ] ; then
FORTH=false
STATUS="401 Unauthorized"
echo "${DATE}: Сигнатура HMAC-SHA256 подписи тела POST-запроса не соответствует указанному \"секретному ключу\"." >> "${LOG}"
fi
fi
return ${?}
}
# Функция загрузки локальных переменных
function preset-var {
# Проверяем наличие соответствующего запросу конфигурационного файла
if [ -f "${PWD}/conf/${IN_GROUPNAME}-${IN_REPONAME}-${IN_BRANCH}.conf" ] ; then
# Читаем соответствующий запросу конфигурационный файл с параметрами связки удалённого и локального репозиториев
source "${PWD}/conf/${IN_GROUPNAME}-${IN_REPONAME}-${IN_BRANCH}.conf" 2>/dev/null
if [[ "${?}" -eq "0" && ! -z "${GIT_REMOTE}" ]] ; then
# Для очевидности в дальнейшей логике задаём явно переменную имени задействуемой ветви репозитория
GIT_BRANCH="${IN_BRANCH}"
else
FORTH=false
STATUS="422 Unprocessable Entity"
echo "${DATE}: Некорректно содержимое конфигурационного файла \"${PWD}/conf/${IN_GROUPNAME}-${IN_REPONAME}-${IN_BRANCH}.conf\" или не все обязательные параметры присутствуют." >> "${LOG}"
fi
else
FORTH=false
STATUS="200 Ok"
echo "${DATE}: Запрос игнорируется: обработка событий в ветви \"${IN_BRANCH}\" этим локальным репозиторием не поддерживается." >> "${LOG}"
fi
return ${?}
}
Выносим в подключаемый файл подборку функций, предназначенных для загрузки данных с удалённого Git-репозитория непосредственно в директорию сайта на web-сервере:
# cd /var/www/cgi-bin/webhook/webhook/lib
# vi ./cd-direct-worker.sh.snippet && chown www-data:www-data ./cd-direct-worker.sh.snippet && chmod o-rwx ./cd-direct-worker.sh.snippet
# vi ./cd-direct-worker.sh.snippet && chown www-data:www-data ./cd-direct-worker.sh.snippet && chmod o-rwx ./cd-direct-worker.sh.snippet
#!/bin/bash
# This file contains the code snippet for the shell Bash v.4 (Bourne again shell).
# Файл содержит фрагмент кода для командного интерпретатора Bash v.4 (Bourne again shell).
# Функция простейшей загрузки содержимого репозитория в указанную директорию
function cd-direct {
# Проверяем наличие обязательных переменных
if [[ -z "${GIT_DIR}" || -z "${GIT_USER}" ]] ; then
FORTH=false
STATUS="422 Unprocessable Entity"
echo "${DATE}: Некорректный запрос: не все обязательные параметры локального репозитория \"${GIT_DIR}\" присутствуют." >> "${LOG}"
fi
# Пробуем перейти в директорию локального репозитория
cd "${GIT_DIR}"
if [ "${?}" -eq "0" ] ; then
# Проверяем наличие (создаём, если отсутствует) и доступность выделенному пользователю целевой ветви локального репозитория
sudo -u "${GIT_USER}" git checkout -B "${GIT_BRANCH}" > /dev/null 2>&1
if [ "${?}" -eq "0" ] ; then
# Пробуем загрузить изменения из удалённого репозитория в локальный
sudo -u "${GIT_USER}" git fetch "${GIT_REMOTE}" "${GIT_BRANCH}" >> "${LOG}" 2>&1
if [ "${?}" -eq "0" ] ; then
# Сразу после загрузки данных "перематываем" указатель состояния к последнему "коммиту" в ветке
sudo -u "${GIT_USER}" git reset --hard FETCH_HEAD >> "${LOG}" 2>&1
# ( хорошо бы здесь добавить проверку успешности операции)
# Явно активируем изменённую (текущую) ветку проекта
sudo -u "${GIT_USER}" git checkout --force "${GIT_BRANCH}" > /dev/null 2>&1
if [ "$(sudo -u ${GIT_USER} git rev-parse --abbrev-ref HEAD)" = "${GIT_BRANCH}" ] ; then
# Информируем клиента об успешной обработке запроса
STATUS="200 Ok"
echo "${DATE}: Успешно завершена загрузка данных ветви \"${GIT_BRANCH}\" из удалённого репозитория \"${GIT_REMOTE}\" в локальный репозиторий в директории \"${GIT_DIR}\"." >> "${LOG}"
# Записываем в журнал событий сведения о текущем состоянии локального репозитория
CUR_HEAD=$(sudo -u "${GIT_USER}" git rev-parse HEAD 2>/dev/null)
echo "${DATE}:" >> "${LOG}"
echo " Remote repository: ${GIT_REMOTE}" >> "${LOG}"
echo " Local repository: ${GIT_DIR}" >> "${LOG}"
echo " Branch: ${GIT_BRANCH}" >> "${LOG}"
echo " Commit: ${CUR_HEAD}" >> "${LOG}"
else
FORTH=false
STATUS="422 Unprocessable Entity"
echo "${DATE}: Не удалось от имени \"${GIT_USER}\" активировать ветку \"${GIT_BRANCH}\" локального репозитория в директории \"${GIT_DIR}\"." >> "${LOG}"
fi
else
FORTH=false
STATUS="422 Unprocessable Entity"
echo "${DATE}: Не удалось от имени \"${GIT_USER}\" загрузить данные ветви \"${GIT_BRANCH}\" из удалённого репозитория \"${GIT_REMOTE}\" в локальный репозиторий в директории \"${GIT_DIR}\"." >> "${LOG}"
fi
else
FORTH=false
STATUS="422 Unprocessable Entity"
echo "${DATE}: Не удалось от имени \"${GIT_USER}\" проверить статус локального репозитория в директории \"${GIT_DIR}\"." >> "${LOG}"
fi
else
FORTH=false
STATUS="422 Unprocessable Entity"
echo "${DATE}: Не удалось переместиться в директорию локального репозитория \"${GIT_DIR}\"." >> "${LOG}"
fi
return ${?}
}
# This file contains the code snippet for the shell Bash v.4 (Bourne again shell).
# Файл содержит фрагмент кода для командного интерпретатора Bash v.4 (Bourne again shell).
# Функция простейшей загрузки содержимого репозитория в указанную директорию
function cd-direct {
# Проверяем наличие обязательных переменных
if [[ -z "${GIT_DIR}" || -z "${GIT_USER}" ]] ; then
FORTH=false
STATUS="422 Unprocessable Entity"
echo "${DATE}: Некорректный запрос: не все обязательные параметры локального репозитория \"${GIT_DIR}\" присутствуют." >> "${LOG}"
fi
# Пробуем перейти в директорию локального репозитория
cd "${GIT_DIR}"
if [ "${?}" -eq "0" ] ; then
# Проверяем наличие (создаём, если отсутствует) и доступность выделенному пользователю целевой ветви локального репозитория
sudo -u "${GIT_USER}" git checkout -B "${GIT_BRANCH}" > /dev/null 2>&1
if [ "${?}" -eq "0" ] ; then
# Пробуем загрузить изменения из удалённого репозитория в локальный
sudo -u "${GIT_USER}" git fetch "${GIT_REMOTE}" "${GIT_BRANCH}" >> "${LOG}" 2>&1
if [ "${?}" -eq "0" ] ; then
# Сразу после загрузки данных "перематываем" указатель состояния к последнему "коммиту" в ветке
sudo -u "${GIT_USER}" git reset --hard FETCH_HEAD >> "${LOG}" 2>&1
# ( хорошо бы здесь добавить проверку успешности операции)
# Явно активируем изменённую (текущую) ветку проекта
sudo -u "${GIT_USER}" git checkout --force "${GIT_BRANCH}" > /dev/null 2>&1
if [ "$(sudo -u ${GIT_USER} git rev-parse --abbrev-ref HEAD)" = "${GIT_BRANCH}" ] ; then
# Информируем клиента об успешной обработке запроса
STATUS="200 Ok"
echo "${DATE}: Успешно завершена загрузка данных ветви \"${GIT_BRANCH}\" из удалённого репозитория \"${GIT_REMOTE}\" в локальный репозиторий в директории \"${GIT_DIR}\"." >> "${LOG}"
# Записываем в журнал событий сведения о текущем состоянии локального репозитория
CUR_HEAD=$(sudo -u "${GIT_USER}" git rev-parse HEAD 2>/dev/null)
echo "${DATE}:" >> "${LOG}"
echo " Remote repository: ${GIT_REMOTE}" >> "${LOG}"
echo " Local repository: ${GIT_DIR}" >> "${LOG}"
echo " Branch: ${GIT_BRANCH}" >> "${LOG}"
echo " Commit: ${CUR_HEAD}" >> "${LOG}"
else
FORTH=false
STATUS="422 Unprocessable Entity"
echo "${DATE}: Не удалось от имени \"${GIT_USER}\" активировать ветку \"${GIT_BRANCH}\" локального репозитория в директории \"${GIT_DIR}\"." >> "${LOG}"
fi
else
FORTH=false
STATUS="422 Unprocessable Entity"
echo "${DATE}: Не удалось от имени \"${GIT_USER}\" загрузить данные ветви \"${GIT_BRANCH}\" из удалённого репозитория \"${GIT_REMOTE}\" в локальный репозиторий в директории \"${GIT_DIR}\"." >> "${LOG}"
fi
else
FORTH=false
STATUS="422 Unprocessable Entity"
echo "${DATE}: Не удалось от имени \"${GIT_USER}\" проверить статус локального репозитория в директории \"${GIT_DIR}\"." >> "${LOG}"
fi
else
FORTH=false
STATUS="422 Unprocessable Entity"
echo "${DATE}: Не удалось переместиться в директорию локального репозитория \"${GIT_DIR}\"." >> "${LOG}"
fi
return ${?}
}
Для порядка закрываем директорию скриптов обработки "webhook"-ов от посторонних:
# chown -R www-data:www-data /var/www/cgi-bin/webhook
# chmod -R o-rwx /var/www/cgi-bin/webhook
# chmod -R o-rwx /var/www/cgi-bin/webhook
Пояснение к публикации.
Прочитавшим с интересом или пролиставшим до конца с целью оставить комментарий о никчёмности затеи автоматизации посредством Bash-скриптов сообщу - да, я осознаю крайнюю неэффективность этого подхода. Я знаю о специально созданных для целей автоматизации "деплоя" программах вроде "Bamboo", "Jenkins", "Travis" или "GitLab Runner". Мне просто хотелось разобраться в основах, сделать что-то крайне простое, прежде чем задействовать в работе более сложные специализированные программные комплексы.