Зачем нам управлять временем?

В начале немного о себе, мое основное занятие — обеспечение качества на вверенных проектах, я Senior QA в компании Umbrella IT. Периодически при тестировании микросервисов приходится сталкиваться с необходимостью изменения времени для проверки работы того или иного функционала. Это может быть функционал, который срабатывает по «тику» cron, или связанный с применением системного времени как одного из условий обработки/хранения/передачи данных тестируемым микросервисом.

Если микросервис разворачивается в Docker, то время контейнера берется из системного времени хост машины и без дополнительных инструментов максимум, что мы можем поменять — это часовой пояс контейнера, но не само системное время. Проблема в том, что часто для целей тестирования просто изменения часового пояса, ровно как и изменения времени в пределах 24х часов, оказывается недостаточно. Что делать если нам нужно протестировать работу микросервиса в граничных значениях даты‑времени, например начало и конец месяца/года, или использовать замечательные даты, такие как 29 февраля, последние даты месяцев со сменой количества дней, и так далее?

Конечно можно поднять виртуальную машину, изменить на ней системное время, перезагрузить Docker и задеплоить нужный нам микросервис. Вполне вероятно, что в кейсах, когда микросервису нужно поддерживать множество сетевых подключений на разных портах (почта, БД, gRPC и тд), нам придется пробрасывать эти порты, возможно (в зависимости от выбора реализации) настраивать туннели и тд. Даже если подобная реализация заработает, то нужно принимать во внимание возможные риски снижения ценности тестирования по причине наращивания «костылей» подключения и недостатков реализации с использованием отдельной виртуальной машины. Все это отдаляет условия тестирования от реальных условий использования.

Что же делать если нам нужно одновременно:

  • максимальное приближение к стандартным тестовым условиям

  • гибкое управление временем при деплое

  • отсутствие вмешательства в код микросервиса

  • минимальное влияние на потребление ресурсов

  • сохранение стандартного сетевого взаимодействия (на сколько это возможно, опытный «сетевик» заметит сразу, те нюансы, которые будут описаны ниже в этой статье)

До описания инструкции хочу выразить благодарность Нестерович Александру за помощь в реализации и отладке ниже описанного подхода.

Инструкция к применению

Итак перед нами стоит задача развернуть в тестовом окружении (например кластер FT) контейнер с микросервисом и задать ему время, отличное от времени хост машины. Сразу перечислим основные требуемые доступы для этой реализации на FT окружении (это может быть выделенная машина для проведения тестирования, которая находится в FT кластере):

  1. создание, редактирование и запуск на исполнение файлов

  2. запуск на исполнение apt‑get, docker и docker‑compose

  3. назначение прав на чтение и исполнение созданных файлов

  4. остальные доступы опциональны и зависят от конкретных кейсов, например доступ к получению образа из репозитория, или, например, доступ к командной строке entrypoint.

  1. Создаем 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

  1. Пишем кастомный 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.

Пример работы faketime с командой date
Пример работы faketime с командой date
  1. Запускаем, проверяем

Все что нам осталось, это запустить деплой командой 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)


  1. ruomserg
    20.08.2024 19:48
    +4

    Интересное решение. Примерно для того же при разработке финансовой системы в static lint были настроены правила, которые били по рукам за любую попытку запросить системное время иначе как через предписанный класс time-provider-а. Ну а он уже мог быть настроен через переменные окружения на желаемое поведение. Потому что фальсифицировать системное время (даже в контейнере) мы не решились из-за возможных непредвиденных эффектов.


  1. atues
    20.08.2024 19:48

    А где тут часовая и минутная стрелки? Какое время показывают часы: 07:06:52 или 13:35:52? По логике - вернее первое, но надо присматриваться :)


    1. SergeiTerentev Автор
      20.08.2024 19:48

      Это визуализированная абстракция)