Зачем нам управлять временем?
В начале немного о себе, мое основное занятие — обеспечение качества на вверенных проектах, я Senior QA в компании Umbrella IT. Периодически при тестировании микросервисов приходится сталкиваться с необходимостью изменения времени для проверки работы того или иного функционала. Это может быть функционал, который срабатывает по «тику» cron, или связанный с применением системного времени как одного из условий обработки/хранения/передачи данных тестируемым микросервисом.
Если микросервис разворачивается в Docker, то время контейнера берется из системного времени хост машины и без дополнительных инструментов максимум, что мы можем поменять — это часовой пояс контейнера, но не само системное время. Проблема в том, что часто для целей тестирования просто изменения часового пояса, ровно как и изменения времени в пределах 24х часов, оказывается недостаточно. Что делать если нам нужно протестировать работу микросервиса в граничных значениях даты‑времени, например начало и конец месяца/года, или использовать замечательные даты, такие как 29 февраля, последние даты месяцев со сменой количества дней, и так далее?
Конечно можно поднять виртуальную машину, изменить на ней системное время, перезагрузить Docker и задеплоить нужный нам микросервис. Вполне вероятно, что в кейсах, когда микросервису нужно поддерживать множество сетевых подключений на разных портах (почта, БД, gRPC и тд), нам придется пробрасывать эти порты, возможно (в зависимости от выбора реализации) настраивать туннели и тд. Даже если подобная реализация заработает, то нужно принимать во внимание возможные риски снижения ценности тестирования по причине наращивания «костылей» подключения и недостатков реализации с использованием отдельной виртуальной машины. Все это отдаляет условия тестирования от реальных условий использования.
Что же делать если нам нужно одновременно:
максимальное приближение к стандартным тестовым условиям
гибкое управление временем при деплое
отсутствие вмешательства в код микросервиса
минимальное влияние на потребление ресурсов
сохранение стандартного сетевого взаимодействия (на сколько это возможно, опытный «сетевик» заметит сразу, те нюансы, которые будут описаны ниже в этой статье)
До описания инструкции хочу выразить благодарность Нестерович Александру за помощь в реализации и отладке ниже описанного подхода.
Инструкция к применению
Итак перед нами стоит задача развернуть в тестовом окружении (например кластер FT) контейнер с микросервисом и задать ему время, отличное от времени хост машины. Сразу перечислим основные требуемые доступы для этой реализации на FT окружении (это может быть выделенная машина для проведения тестирования, которая находится в FT кластере):
создание, редактирование и запуск на исполнение файлов
запуск на исполнение apt‑get, docker и docker‑compose
назначение прав на чтение и исполнение созданных файлов
остальные доступы опциональны и зависят от конкретных кейсов, например доступ к получению образа из репозитория, или, например, доступ к командной строке entrypoint.
Создаем docker-compose.yml
Для настройки деплоя нашего микросервиса создадим файл docker-compose.yml
version: '3.6'
services:
my-test-service-workerhost:
image: myimg.repo.com/project/my-test-service-workerhost:1.0.0-Ft
restart: always
environment:
ASPNETCORE_ENVIRONMENT: Staging
FAKED_TIME: '@2007-01-01 00:00:01' #Поле по теме статьи
cap_add:
- NET_ADMIN
volumes:
- "./appsettings.json:/app/appsettings.Staging.json"
- "./entrypoint.sh:/app/entrypoint.sh" #Поле по теме статьи
entrypoint: ["/app/entrypoint.sh"] #Поле по теме статьи
ports:
- 5335:80
Все поля, кроме тех, что помечены комментом “Поле по теме статьи”, не относятся к вопросу управления системным временем и добавлены только для того, чтобы наш docker-compose.yml
быль чуть более приближен к реальному. Разберем назначения отмеченных полей:
FAKED_TIME
- это переменная окружения, через которую мы будем пробрасывать значение в кастомизированный entrypoint при деплое микросервиса. Можем назвать ее любым удобным и свободным именем, если будет такое желание, главное не забыть переименовать ее в указанном ниже скрипте. “@
” перед датой означает, что указанное время не будет зафиксировано навеки для микросервиса, а будет являться точкой отсчета для системного времени. Время будет идти для микросервиса начиная с указанной точки отсчета.entrypoint
- собственно тут мы указываем, что стандартный entrypoint необходимо заменить на кастомный.volumes"./entrypoint.sh:/app/entrypoint.sh"
- тут мы указываем где будет храниться файл с кастомным скриптом entrypoint
Пишем кастомный entrypoint
В папке с файлом docker-compose.yml
(или по другому пути, который вы ранее указали в volumes
для entrypoint) создаем фаил entrypoint.sh
и сразу даем ему права на чтение и выполнение командой chmod
.
В самом файле пишем на bash следующий код:
#!/bin/sh
# Проверяем FAKED_TIME на наличие данных
if [ -z "$FAKED_TIME" ]; then
echo "Error: FAKED_TIME is empty or null with = $FAKED_TIME"
exit 1
else
echo "FAKED_TIME is $FAKED_TIME"
fi
# Устанавливаем faketime
apt-get update && apt-get install -y faketime
# Запускаем приложение с использованием faketime
exec faketime -f "$FAKED_TIME" dotnet /app/MyProject.TestApplication.WorkerHost.dll
Для изменения системного времени при запуске микросервиса мы используем faketime. Это приложение которое подменяет значение системного времени для выполняемой с помощью нее команды. Например при выполнении команды faketime -f '2007-01-01 00:00:01' date
в консоли мы увидим вывод подставленной нами даты-времени для команды date
.
Запускаем, проверяем
Все что нам осталось, это запустить деплой командой docker-compose up
и проверить логи контейнера
И вот у нас снова 2007. Как мы видим, время в контейнере не фиксированное и изменяется с его течением, что отражено в логах
Возможные проблемы и пути их решения
Сетевое взаимодействие
Каким бы нативным не был подход к изменению времени, мы можем столкнуться с тем, что при сетевом взаимодействии с использованием сертификатов (например простом https), у нас не будет установлено соединение. Причина тому - проверка сертификата, а именно связанных с ним дат. В случае возникновения таких проблем для проведения тестирования можно:
перейти на взаимодействие по не шифрованному протоколу
поднять рядом по этой же инструкции нужный вам хост/БД и тд с уже измененным временем, которое согласовано с тестируемым микросервисом
поднять рядом мок сервис по этой же инструкции с уже измененным временем, которое согласовано с тестируемым микросервисом
Выбор решения зависит от каждого конкретного случая/проекта, что будет быстрее, удобнее, меньше влиять на эффективность тестирования или просто будет выполнимо.
Отсутствие доступа на выполнение команд при деплое контейнера
Бывает так, что в целях безопасности дефолтному пользователю закрывают возможность выполнять команды при сборке контейнера. В таких случаях необходимо в entrypoint.sh
перед выполнением команд переключиться на пользователя с достаточными правами, либо добавить команды на установку и использование faketime уже внутрь Docker файла
Отсутствие faketime среди доступных к установке приложений
Если вдруг в вашем случае простая установка через apt-get install -y faketime
невозможна, можно воспользоваться библиотекой libfaketime для клонирования репозитория и его дальнейшей установки
git clone https://github.com/wolfcw/libfaketime.git
cd libfaketime
make install
Инструкцию по использованию libfaketime можно найти в readme репозитория
Заключение
Управление системным временем контейнера дает широкие возможности по тестированию функционала сервисов, которые используют эти данные в своей логике. Это позволяет увеличить тестовое покрытие, сократить время затраченное на само тестирование, гибко управлять окружением и при необходимости воспроизводить специфические кейсы.
Комментарии (3)
atues
20.08.2024 19:48А где тут часовая и минутная стрелки? Какое время показывают часы: 07:06:52 или 13:35:52? По логике - вернее первое, но надо присматриваться :)
ruomserg
Интересное решение. Примерно для того же при разработке финансовой системы в static lint были настроены правила, которые били по рукам за любую попытку запросить системное время иначе как через предписанный класс time-provider-а. Ну а он уже мог быть настроен через переменные окружения на желаемое поведение. Потому что фальсифицировать системное время (даже в контейнере) мы не решились из-за возможных непредвиденных эффектов.