Введение
CIS - это инструмент для разработчиков программного обеспечения.
CIS является системой непрерывной интеграции и предназначен для автоматизации рутинных операций, которые присутствуют в жизненном цикле ПО таких как: сборка, развертывание, автотестирование, диагностика состояния систем и т.д.
В отличии от других систем непрерывной интеграции ядро CIS можно запускать локально без остальных компонентов, что может быть удобно для отладки разрабатываемых скриптов автоматизации. Все логи, настройки проектов и переменных доступны в виде обычных текстовых файлов, что позволяет хранить конфигурацию CI в системах контроля версий, а так же переносить проекты вместе со всей историей при помощи обычного копирования. Веб интерфейс позволяет создавать задачи, изменять их, следить за их исполнением в реальном времени, просматривать логи и скачивать полученные артефакты. Так как CIS использует исполняемые файлы в качестве скриптов, то возможные сценарии использования ограничены лишь возможностями ОС, правами пользователя и фантазией разработчика. Например пользователь может использовать docker или другие средства контейнеризации для сборки. Механизм задач, в свою очередь, позволяет реализовать набор базовых компонентов для переиспользования подобных приемов.
Скриншоты работы системы:
Подробное описание системы и документация представлены в отдельном репозитории.
Описание системы
Разрабатываемая система состоит из трех основных частей:
- FrontEnd
- WebUI
- Core
Схема их взаимодействия представлена ниже.
┌────────┐ <─[exec]─ ┌───────┐ ┌──────────┐
│ │ <─[env]── │ │ <[HTTP]> │ │
│ Core │ │ WebUI │ │ FrontEnd │
│ │ ─[TCP]──> │ │ <─[WS]─> │ │
└────────┘ <[files]> └───────┘ └──────────┘
Ядро является независимой от остальной системы частью системы и может работать отдельно. В данный момент для корректной работы необходимо устанавливать все компоненты на один сервер или в один контейнер. При этом существует возможность разработки небольшой прослойки, которая позволит запускать ядро и webui на отдельных серверах.
Core
Ядро является ключевым компонентом системы и содержит в себе логику запуска задач, работы со сборками, логами и планировщиком. Ядро не зависит от остальных компонентов и может работать отдельно, как из терминала, так и из других систем, например задачи ядра могут запускаться напрямую из GitLab CI Runner'а.
Задачи
Базовой единицей в CIS является задача. Каждая задача представлена отдельной директорией, содержащей конфигурацию и исполняемый файл (которой не обязательно является скриптом). У каждой задачи присутствуют параметры, которые считываются интерактивно, или, могут быть заданы с помощью аргументов. В рамках сессии параметры задачи могут быть установлены и считаны с помощью утилит setparam
и getparam
.
Сессии
При старте первой задачи создается новая сессия, в рамках сессии все задачи выполняются синхронно, образуя дерево задач. При этом существует возможность в задаче создать новую сессию. Такую сессию можно выполнять как синхронно, так и асинхронно, но в асинхронном варианте все задачи синхронизации ложатся на пользователя. Вся информация о событиях в сессии логируется в соответствующий файл, и, если webui доступен, то также отправляется в него посредством TCP. Служебная информация, такая как пользователь, внутренний адрес webui сервера, имя самой задачи, номер сборки и другие параметры передаются посредством переменных среды. Так же существует механизм пользовательских глобальных переменных, которые видны в рамках одной сессии и могут быть установлены с помощью setvalue
и получены с помощью getvalue
.
Пример выполнения задач в сессии. Сессия начинается в (а)
и завершается в (б)
:
───────────────────────────────────────────────
WebUI или терминал
───────────────────────────────────────────────
↓ (а) ↑ (б)
┌────────────────────────────────────┐
| startjob |
└────────────────────────────────────┘
↓ ↑
┌────────────────────────────────────┐
| job |
└────────────────────────────────────┘
↓ ↑ ↓ ↑
┌──────────┐ ┌──────────────────────┐
| startjob | | startjob |
└──────────┘ └──────────────────────┘
↓ ↑ ↓ ↑
┌──────────┐ ┌──────────────────────┐
| job | | job |
└──────────┘ └──────────────────────┘
↓ ↑
┌────────────┐
| startjob |
└────────────┘
↓ ↑
┌────────────┐
| job |
└────────────┘
Логи
Ядро ведет несколько логов:
cis.log
- служебные события CIS, в основном ошибки.${session_id}.${n}.log
- события в рамках сессии.output.txt
в директории каждой сборки - вывод самого скрипта сборки.${session_id}.combined.log
- полный лог всех событий сессии, информация идентична той, что отправляется в WebUI посредством TCP.
Прочее
В задачи ядра входит очистка директории задачи после сборки, если это необходимо, так же очистку задачи можно провести вручную при помощи утилиты maintenance
.
Помимо этого ядро позволяет запускать задачи по расписанию аналогично утилите cron
.
Существует репозиторий с вспомогательными скриптами используемых для упрощения написания задач - ci-py-lib.
WebUI
Предоставляет API для удаленного управления ядром, просмотра процесса исполнения задач. Также добавляет функционал управления пользователями, правами, аутентификации и авторизации. Основной функционал реализован посредством собственного протокола поверх WebSocket. Дополнительно часть функций, а именно загрузка и скачивание файлов и вебхуки используют HTTP. Помимо управления ядром так же реализовано управление файловой системой, т.к. именно на неё завязано много функций в ядре.
FrontEnd
Реализует интерфейс для API WebUI. Даёт возможность создавать пользовательские сценарии.
Техническая реализация
Зависимости
core
- boost 1.69
- sqlite_orm 1.3
- croncpp 1.0
- sc_logger 1.0.3
webui
- boost 1.69
- rapidjson 1.1.0
- sqlite_orm 1.3
- croncpp 1.0
- sc_logger 1.0.3
Протокол WebUI API
Для взаимодействия с фронтендом или другими внешними системами, а так же для получения логов из ядра был реализован протокол похожий на JSON-RPC или WAMP.
Каждое сообщение содержит поле event
являющееся уникальным идентификатором события.
Каждое сообщение содержит поле transactionId
- идентификатор транзакции, все ответы сервера на какой-либо запрос устанавливают значение данного поля равное установленному в запросе. Позволяет асинхронно взаимодействовать с API по модели RequestReply.
Сообщение может содержать поле errorMessage
- отправляется только сервером и содержит дополнительное текстовое описание ошибки.
Поле data
содержит произвольные данные специфичные для каждого отдельного события.
Сериализация и десериализация
Для реализации протоколов были реализованы вспомогательные библиотеки, позволяющие сереализовать и десериализовать JSON на основе добавления метаописания к C++-типам.
Пример типа с метаописанием:
struct auth_logout
{
std::string token;
static constexpr auto get_converter()
{
using namespace reflect;
return make_meta_converter<auth_logout>()
.set_name(
CT_STRING("auth"),
CT_STRING("logout"))
.add_field(
CT_STRING("token"),
ptr_v<&auth_logout::token>{})
.done();
}
};
И соотвествующего ему JSON объекта:
{
"event": "auth.logout",
"data": {
"token": string
}
}
При этом при десериализации происходит автоматическая валидация типа, так же присуствует возможность задать дополнительные функции валидаторы для каждого типа.
Т.к. при данном подходе описание типа отделено от описания протокола, присутствует возможность перехода на бинарный протокол.
Транспортный протокол между WebUI и Core
Для передачи информации о активной сессии в реально времени был реализован простой бинарный ltv-протокол поверх TCP. В нём присутствует всего 3 типа сообщений - ping
, pong
и regular
.
Первые два используются для проверки того, что соединение активно (в случае если вторая сторона не отвечает на ping соединение просто обрывается). Сообщение типа regular
может иметь произвольное содержимое.
Данный протокол реализован отдельный библиотекой, с использованием Boost.Asio.
Core
Ядро состоит из нескольких исполняемых файлов, каждый из которых выполняет отдельную задачу. Для запуска исполняемых файлов используется библиотека Boost.Process.
Если задача является корневой для сессии, то она считывает параметры интерактивно, в ином случае она считывает их из job.params в папке сборки, который формируется в момент подготовки сборки из параметров заданных ранее при помощи setparam и параметров задачи по-умолчанию.
Планировщик задач использует два исполняемых файла - один для непосредственно запуска задач, второй для добавления, удаления и изменения запланированных задач. Для оповещения процесса о изменении списка задач используется системная условная переменная из Boost.Interprocess.
Логирование реализовано при помощи библиотеки scf::logger.
Ядро покрыто модульными тестами с применением библиотеки GTest.
WebUI
Событийный цикл приложения и сетевая часть реализованы при помощи библиотеки Boost.Asio.
Публичный API представлен WebSocket с протоколом описанным выше, а так же дополнительным HTTP интерфейсом для загрузки и скачивания файлов, запуска задач посредством вебхук или curl. Данный API реализован с помощью Boost.Beast.
Для работы с ядром используется библиотека Boost.Process.
Для хранения информации о пользователях и правах доступа использована СУБД SQLite и библиотека sqlite_orm.
Результат
Система успешно запущена в эксплуатацию на сайтах tomsksoft.