Создание скрипта для публикации

в 5:50, , рубрики: bash, Программирование, публикация приложений, системное администрирование

Время от времени появляется задача: сделать скрипт для публикации, который нужно обновлять но невозможно изменять. Например это может быть скрипт инициализации, зашитый внутрь образа виртуальной машины или скрипт для установки движка сайта (публикуемый разработчиком движка).
В статье я расскажу о приемах, которые применяю для создания таких скриптов, они помогут избежать некоторых граблей, сохранить простоту и гибкость скриптов. Подход подойдет для тех скриптов, поведение которых должно меняться в зависимости от потребностей автора, обновлений и т.п. Подход НЕ подойдет для скриптов, которые должны работать автономно (без связи с системой автора).

Я использую такой подход в bash-скриптах, но общий принцип можно применять независимо от языка.

Короткая предистория (можно пропустить):

Несколько лет назад стал делать шаблоны VDS-серверов для массового использования клиентами. После создания шаблона поменять его уже не получится, т.е. единственный путь исправления ошибки: публикация нового шаблона, что стоит времени и много гигабайтов места (шаблон + его копии на всех серверах). За эти несколько лет получилось совершить несколько неудобных ошибок, в результате теперь еще много лет придется поддерживать шаблоны с неудобным поведением на старте.
Похожая ситуация может получиться и со скриптами установки/настройки панелей управления, движков сайтов, просто программ которые должны по одной команде скачаться, поставиться и настроиться. При том что сама программа может обновляться, а доступа к исправлению скрипта установки у всех кто его скачал раньше уже нет.
Часть из того что я использую было увидено в похожих скриптах, например установщики ispsystem, brew. Часть подсказали коллеги и часть нажита собственным горьким опытом.

Общая суть

Каждый раз загружать и выполнять весь код со своего сервера.

Общая структура кода

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

Что должен делать и что должен НЕ делать публикуемый скрипт

Публикуемый скрипт ни в коем случае не должен выполнять ту задачу для которой он публикуется и даже не должен иметь намёка на её решение. Единственная задача этого скрипта — найти способ подключения к серверу разработчика и скачать оттуда код для выполнения. Этот код должен быть максимально простым с минимумом внешних зависимостей, т.к. их придется поддерживать всё время жизни скрипта.

Вот код к которому я пришел в итоге

function try()
{
# Выполнить команду, если вернулась ошибка - продолжать пробовать столько раз, сколько указано в первом параметре
... код немного громоздкий, опущен для упрощения
}

# Execute init.sh from panel
function execute_init()
{
    local EVAL_CODE=`curl http://panel.1gb.ru/minimal/init.sh`
    if [ "${EVAL_CODE##!/bin/bash}" != "$EVAL_CODE" ] && [ "${EVAL_CODE%#BashScriptEnd}" != "$EVAL_CODE" ]; then
        eval "$EVAL_CODE"
        return 0
    else
        return 1
    fi
}

try 100000 execute_init

Этот код я помещаю в шаблон с заранее известным окружением, в частности я точно знаю что там есть curl, а много попыток нужно т.к. при старте сервера сеть может не работать или http-сервер может временно выдавать ошибку. Другие скрипты могут работать в разных окружениях и curl там может не быть. Это хорошее место чтобы попробовать подключиться к серверу разными способами, при необходимости много раз.
Обязательно нужно проверить что код для выполнения загрузился целиком — длинная строка с if делает именно проверку: проверяется что скрипт начинается с #!/bin/bash и заканчивается #BashScriptEnd. Так можно быть умеренным, что не выполнится код HTML-ошибки или полскрипта, обрезав rm -rf /tmp/my-downloads до rm -rf /

Делать более сложные проверки я в этом месте не стал намеренно — TCP в данном случае дает приемлимую защиту от повреждения данных, а любое усложнение внешнего интерфейса потом придется поддерживать вечно.
У этого скрипта только одна внешняя зависимость — URL-адрес. В дальнейшем даже его потом пришлось поменять — при усовершенствовании организации кода, но зависимость настолько простая, что поддерживать её просто. Более того в новых скриптах тоже используется именно этот путь, сложившийся исторически, а не новый «правильный». Потому что в случае изменений в будущем пришлось бы поддерживать уже два URL и т.п.

В скрипте не должно быть никаких попыток определить окружение и загрузить например init_linux.sh или init_freebsd.sh вместо init.sh — такая попытка тоже была, оказалось что это неудобно и теперь приходится поддерживать заглушки для старых версию скриптов.

Что поместить в загружаемый скрипт

Тут свободы уже больше его можно будет менять не трогая уже опубликованной части. Так что если всё просто — сюда можно поместит сразу тот код который будет выполняться. Если что-то усложнится — это легко поменять в будущем.

При сложности выполняемого кода больше, чем 1 файл я рекомендую поместить сюда:
1. Функцию для загрузки новых файлов. Она заново определит как именно подключаться к серверу и во всех скриптах нужно использовать именно её. Она не подходит для общей библиотеки, т.к. та еще не загружена.
2. Вызвать эту функцию один/несколько раз для подключения и выполнения всех нужных файлов: общая библиотека кода, специфический код для найденного окружения и т.п. Может быть посмотреть какая команда передана в аргументах и подгрузить код для выполнения этой команды.

На что обратить внимание

  • Всегда проверять загруженный код перед выполнением. Иначе можно выполнить полскрипта или какую-то ошибку
  • В публикуемом скрипте минимум (в идеале одна) внешняя зависимость — адрес с которого загружается основной код. Всегда один и тот же
  • Всегда делать несколько попыток загрузить каждый файл. Даже если работа идет в локальной сети — могут быть временные ошибки сервера когда он вернет что-то не то, например ошибку. Или вообще подключение не примет. Если у вас один загружаемый файл и команды выполняются вручную — это может быть не страшно. Но если команды будут встроены в автоматику и подключаемых файлов много — вероятность что ошибка произойдет вполне реальна. Так же реальна и загрузка файла не целиком.
  • Перед публикацией скрипта обязательно его проверить — это обидно, когда опечатался в одном символе и из-за этого нужно переделывать кучу работы по подготовке/проверке шаблона или взаимодействовать с теми, кому недавно отдал скрипт
Недостатки и точка зрения на них

  • Необходимость подключения к сети (или интернету). В контексте применения этого подхода всё равно что-то придется скачивать с сервера разработчика (в т.ч. со своего если система используется внутри) и сеть всё равно потребуется. Скрипт не сможет настроить локальную сеть если она не работает или задать пароль доступа если не сможет его из сети получить. Так же скрипт не сможет развернуть ПО/сайт с сервера разработчика если он недоступен
  • Выполнение заранее неопределенного кода. Если это свой код, то всё понятно. Если это чей-то чужой код — всё равно это будет запускаться/выполняться на том уровне доверия который ему положен. Если это установка клиенского приложения — в клиентской среде и всё равно разработчик внутри своего ПО может делать что угодно и весь код проверять никто не будет (если кто-то будет — они врядли будут вообще использовать авторазвертывание со стороних ресурсов — только свои коды со своих серверов)

Автор: rekby

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js