Эта статья была первоначально опубликована в моем блоге здесь.
Проблема
CircleCI Orbs предоставляет директиву include
, которая может быть использована для включения скриптов в командные конфигурации орбов. К сожалению, эта директива не предоставляет очевидного способа ссылаться на другие, общие скрипты внутри включенного скрипта.
На самом деле, оказывается, что директива include
— это просто макрос, который побуждает CircleCI заменить директиву include
на текстовое тело ссылаемого скрипта. Это означает, что другие скрипты, включенные с помощью директивы include
, не могут ссылаться прямым образом, что затрудняет создание общих скриптовых модулей.
Мотивация
В последнее время я централизую инструментарий DevOps для своих проектов в пользовательской сфере CircleCI Orb. Сфера предназначена для абстрагирования от типичных действий над стандартизированными репозиториями, которые я планирую создать для текущих и будущих проектов.
Я работаю над генератором монорепо, используя плагин Nx с пользовательской предустановкой рабочего пространства. Для отчетов о тестовом покрытии с интеграцией Codecov я хотел бы динамически загружать по одному отчету о покрытии для каждого пакета в моем монорепо и присваивать ему соответствующий флаг Codecov.
К сожалению, официальный Codecov CircleCI Orb не достаточно мощный, чтобы самостоятельно справиться с этим сценарием, поэтому в итоге я взял несколько их скриптов, чтобы создать что-то, что понимает конфигурацию рабочего пространства Nx и автоматически выполняет выгрузку покрытия, сегментированного по пакетам.
В процессе я хотел получить возможность писать общие скрипты с функциями, которые можно было бы повторно использовать в других скриптах, что позволило бы мне писать более модульные и тестируемые скрипты, а также позволило бы мне сделать скрипты более «сухими».
Решение
К счастью, директива include
может использоваться в любом месте конфигурации орба, а не только в свойстве command
в step
.
Я даже могу использовать директиву include
, чтобы предоставить тело моего общего скрипта в качестве переменной окружения для какой-либо другой команды.
Не рекомендуется: eval
тело разделяемого скрипта.
Моим первым способом решения этой проблемы было использование include
для предоставления тела моего разделяемого скрипта в качестве переменной окружения, а затем eval
его содержимого.
Очевидно, что это не самый безопасный вариант, и использование eval
таким образом немного попахивает. Тем не менее, это решило проблему.
src/commands/upload-monorepo-coverage.yml
:
steps:
- run:
name: Upload Monorepo Coverage Results
command: << include(scripts/uploadMonorepoCoverageResults.sh) >>
environment:
PARSE_NX_PROJECTS_SCRIPT: << include(scripts/parseNxProjects.sh >>
src/scripts/parseNxProjects.sh
:
#! /usr/bin/env bash
# A common function I'd like to use in another file
parse_nx_projects() {
# ...
}
src/scripts/uploadMonorepoCoverageResults.sh
:
#! /usr/bin/env bash
eval "$PARSE_NX_PROJECTS_SCRIPT"
# This shared function from the `parseNxProjects.sh` is now callable
parse_nx_projects
# ...
Немного лучше: запись содержимого разделяемого скрипта на диск
Использование eval
в приведенном выше виде оскорбляет мою чувствительность. Хотя я не уверен, что это действительно более безопасно, в итоге я выбрал другой подход, вдохновленный подходом eval
. То есть, используя директиву include
, я записываю содержимое разделяемого сценария в предсказуемое место на диске, а затем предоставляю путь к разделяемому сценарию всем сценариям, которые должны его использовать.
Таким образом, я могу использовать source
для чтения моих разделяемых функций, что кажется немного лучше.
Для этого я написал специальную команду, которую назвал write-shared-script
.
Вот как выглядит исходный текст команды:
description: >
This command writes shared scripts to disk so they can be consumed by other scripts
parameters:
script-dir:
type: string
default: ~/@chiubaka/circleci-orb/scripts
description: Path to the directory to write shared scripts to.
script-name:
type: string
description: Name of the script to write
script:
type: string
description: The script to write. Should be included here using the include directive.
steps:
- run:
name: Write << parameters.script-name >> to disk
command: << include(scripts/writeSharedScript.sh) >>
environment:
SCRIPT: << parameters.script >>
SCRIPT_DIR: << parameters.script-dir >>
SCRIPT_NAME: << parameters.script-name >>
А вот writeSharedScript.sh
:
#! /usr/bin/env bash
SCRIPT_PATH="$SCRIPT_DIR/$SCRIPT_NAME"
mkdir -p "$SCRIPT_DIR"
echo "$SCRIPT" > "$SCRIPT_PATH"
chmod +x "$SCRIPT_PATH"
Теперь шаги команды, требующей общий скрипт, выглядят следующим образом:
- write-shared-script:
script-name: parseNxProjects.sh
script: << include(scripts/parseNxProjects.sh) >>
- run:
name: Upload Monorepo Coverage Results
command: << include(scripts/uploadMonorepoCoverageResults.sh) >>
environment:
PARSE_NX_PROJECTS_SCRIPT: ~/@chiubaka/circleci-orb/scripts/parseNxProjects.sh
Наконец, сценарий uploadMonorepoCoverageResults.sh
теперь выглядит следующим образом:
#! /usr/bin/env bash
source "$PARSE_NX_PROJECTS_SCRIPT"
# This shared function from the `parseNxProjects.sh` is now callable
parse_nx_projects
# ...
Соображения безопасности
Возможно, в контексте CircleCI разница между этими двумя подходами не очень велика. Реалистично, вход в eval
здесь всегда находится под моим контролем, пока CircleCI работает нормально. Если бы в CircleCI была брешь в безопасности, позволяющая злоумышленнику контролировать ввод этого оператора eval
, то здесь возникает более серьезная проблема, связанная с совершенно другой моделью угрозы.
Технически, если злоумышленник может контролировать ввод этого оператора eval
, тот же злоумышленник, вероятно, может контролировать содержимое общего скрипта на диске, что будет равносильно атаке такой же серьезности.
Тем не менее, лучше избегать кардинального греха использования eval
, и, по крайней мере, таким образом мои скрипты orb немного более отлаживаемы в производстве, поскольку общие скрипты записываются на диск.