UMGUM.COM 

Webhook + FCGI + Bash + Git ( Автоматизация доставки кода простейших web-приложений к серверам тестирования и публикации по событию изменения содержимого Git-репозитория. )

20 декабря 2018  (обновлено 14 июня 2019)

OS: "Linux Debian 8/9", "Linux Ubuntu 16/18 LTS".
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-сервер публикации.

Последовательность дальнейших действий:

1. Настраиваем уведомление об изменениях на стороне Git-репозитория.
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

Пример для 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

Все остальные сервисы 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 ${?}

Пример использования (будет создан пользователь "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.

Скрипт добавления локального 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 ${?}

Пример использования (будет создан 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

По умолчанию 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;
  }
}

Пример конфигурационного файла подключения 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;

Активируем конфигурацию, проверяем её средствами 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

Подготовка конфигурации сопоставления 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

##
# (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

#!/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 ${?}

Выносим в подключаемый файл подборку функций, предназначенных для обработки входящих переменных, подтверждения подлинности уведомления и подключения локального файла конфигурации связи "репозиторий - сайт":

# 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

#!/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 ${?}
}

Выносим в подключаемый файл подборку функций, предназначенных для загрузки данных с удалённого 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

#!/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 ${?}
}

Для порядка закрываем директорию скриптов обработки "webhook"-ов от посторонних:

# chown -R www-data:www-data /var/www/cgi-bin/webhook
# chmod -R o-rwx /var/www/cgi-bin/webhook

Пояснение к публикации.

Прочитавшим с интересом или пролиставшим до конца с целью оставить комментарий о никчёмности затеи автоматизации посредством Bash-скриптов сообщу - да, я осознаю крайнюю неэффективность этого подхода. Я знаю о специально созданных для целей автоматизации "деплоя" программах вроде "Bamboo", "Jenkins", "Travis" или "GitLab Runner". Мне просто хотелось разобраться в основах, сделать что-то крайне простое, прежде чем задействовать в работе более сложные специализированные программные комплексы.


Заметки и комментарии к публикации:


Оставьте свой комментарий ( выразите мнение относительно публикации, поделитесь дополнительными сведениями или укажите на ошибку )