Зависимости наших зависимостей или несколько слов об уязвимости наших проектов
Зависимости наших зависимостей
Эта история началась 30 ноября, утром. Когда вполне обычный билд на Test environment внезапно упал. Наверное, какой-то линтер отвалился, не проснувшись подумал я и был не прав. Кому интересно чем закончилась эта история и на какие мысли навела — прошу под кат.
Открыв билд-лог, я увидел, что упал npm install. Странно подумал, я. Еще вчера вечером все работало отлично. После некоторого изучения логов, была найдена подозрительная строка:
node --eval 'if (require("./package.json").name === "coffee-script") { var red, yellow, cyan, reset; red = yellow = cyan = reset = ""; if (!process.env.NODE_DISABLE_COLORS) { red = "\x1b[31m"; yellow = "\x1b[33m"; cyan = "\x1b[36m"; reset = "\x1b[0m"; } console.warn(red + "CoffeeScript has moved!" + reset + " Please update references to " + yellow + "\"coffee-script\"" + reset + " to use " + yellow + "\"coffeescript\"" + reset + " (no hyphen) instead."); console.warn("Also, a new major version has been released under the " + yellow + "coffeescript" + reset + " name on NPM. This new release targets modern JavaScript, with minimal breaking changes. Learn more at " + cyan + "http://coffeescript.org" + reset + "."); console.warn(""); }
Здесь я опять удивился. Опять же, еще вчера мы coffee-script на проекте не использовали и за ночь вряд ли что-то сильно изменилось. Быстрый просмотр package.json подтвердил, что никакой супостат ничего нового туда не добавлял. Значит, наверное, у нас обновилась какая-то зависимость, которая использует coffee-script. Но против этой идеи говорило то, что уже довольно давно я выставил строгие версии для всех зависимостей проекта и, как мне казалось, такого случиться не могло. Бесплодно поискав похожую проблему в интернете, я опять вернулся к мысли об обновившейся зависимости. Поэтому на коленке был набросан скрипт который обошел все package.json-ы в node_modules в поисках coffee-script. Таких зависимостей оказалось около 5–6 штук. Это еще больше укрепило мои подозрения, и не сильно долго размышляя я снес весь node_modules, а заодно и все dependencies кроме одной в локальном репозитории и запустил npm install снова. Процесс прошел успешно. Дальше, шаг за шагом была найдена та зависимость, которая и валила install.
Это оказалась karma-typescript у которой в транзитивной зависимости оказался «pad», который в свою очередь зависел от coffee-script. И тут я снова приуныл. Вариантов было немного. Или временно отключать тесты, или ждать фикса, или делать форк и чинить самому (причем не очень понятно, что же конкретно нужно чинить). Без особой надежды я отправился на Github создавать issue. Каково же было мое удивление, когда мне ответили буквально через 20 минут. Оказалось, что некоторый товарищ, решил обновить coffee-script пакет в npm и вместо того, чтобы объявить старый пакет устаревшим он просто сделал ему unpublish.
На мое счастье, пользователь @ondrejbase уже предложил костыль временное решение которое мне помогло. И вся эта история закончилась относительно дешево. Но могло быть совсем не так.
Это была присказка. А теперь я предлагаю поговорить о зависимостях наших проектов и проблемах, которые они нам приносят.
Давайте начнём с простого.
Глобальные зависимости
Недавно я в очередной раз наткнулся на статью, для новичков которая начиналась со строки
npm install -g typescript
И не выдержал. По моему мнению это один из самых плохих советов который вы можете дать начинающему разработчику. Я серьезно. Вот проблемы, к которым приводит этот совет:
- Конфликт локальных репозиториев. Собирая весь код с помощью глобальных зависимостей (tsc, например) или валидируя его глобальным линтерами мы уже не можем так легко обновлять эти зависимости. Ведь обновив что-то глобальное, мы ставим под угрозу все репозитории в которых это используется. В итоге, придется или тратить время на то что бы обновить эти зависимости во всех репозиториях, или не обновлять их совсем.
- Конфликт с членами команды. Использование глобальных зависимостей в командной работе приводит к их рассинхронизации. И, как следствие, к тому, что мой код не соберется или не пройдет проверку у моего коллеги.
- Конфликт с билд-машиной. То, что соберется у меня, не гарантировано соберется на билд машине. Или, что гораздо хуже и коварнее — соберется не так, как у меня. А это гарантированный ад дебага на ближайший час или больше.
И все это происходит потому, большинство пособий начинается с npm install abc -g
. Хотя можно поставить все локально и подключить в package.json как ./node_modules/.bin/tsc
Если вы соберетесь писать свой гайд, пожалуйста, вспомните эту статью и спасите мои нервы, а также нервы всех тех, кто будет ее использовать на практике.
N.B. Глобально устанавливать генераторы кода (create-react-app, create-angular-app) это нормально. Они один раз отработают и все. Кроме того, вам не понадобится ставить их заново, когда вы решите создать следующий репозиторий.
Нестрогие зависимости
Давайте идти дальше. Установим create-react-app, и создадим базовое приложение. Заходим в package.json и что мы там видим?
"react": "^16.2.0"
Все (или почти все) знают, что значит символ ^. Он значит, что npm может установить любую версию старше или равной указанной, в пределах мажорного релиза. И что в этом плохого? Не хочется быть категоричным, но, как по мне этот подход тоже «не очень», и вот почему:
- Потеря контроля. Как только вы оставили ^ или ~ в своем package.json вы потеряли контроль над своим кодом. Конечно я утрирую, но подумайте. Вы, грузите в проект версию сторонней библиотеки о которой не знаете даже ее версии. Только то, что она лежит в репозитории от какого-то издателя. И вдруг что случится, остается только надеяться, что у этого пакета достаточно организованное сообщество что бы это заметить.
- Изменения ломающие обратную совместимость. Конечно, публикуя мажорную версию, разработчики обещают, что в ее рамках breaking changes не будет. Но, господа, давайте быть реалистами. Это интернет, и это open source: вам никто ничего не должен. То, что работало час назад, может отвалиться с комментарием типа: «This new release targets modern JavaScript, with minimal breaking changes».
- Неочевидность обновлений. Допустим есть у меня библиотека вида abc: ^2.1.1. И, получается, я не знаю какая на самом деле версия библиотеки у меня стоит. Может быть 2.1.1, а может быть 2.9.9. И вроде бы это не проблема, наоборот. Мне не нужно искать документацию для определенной версии, достаточно всегда смотреть самую свежую. И вот я ее смотрю, и вижу новую фишку, и она не работает. А не работает она потому что я просто забыл обновить библиотеку. Я ее обновляю, комичу код, и через 30 минут ко мне прибегает мой коллега потому что у него приложение не работает! А за ним приходят еще трое и делают мне грустно. Кому это нужно?
- И наконец: зависимости зависимостей. Это вишенка на торте и это то, о чем я бы хотел, чтобы вы задумались.
Зависимости наших зависимостей или ЗНЗ
Давайте начнем с того, почему у нас сломался билд. Все зависимости были заданы жестко и тем не менее мы свалились. Несмотря на то, что версии наших основных зависимостей оставались прежними (напомню, никаких ^, ~ в package.json) их зависимости были не такими строгими. Мы не контролировали зависимости наших зависимостей, хотя и пытались. И, что самое неприятное такое поведение поощряется по умолчанию. Я не знаю кто и зачем это сделал, но он подложил большую свинью всем нам, а особенно тем, кто практикует continues-integration.
Конечно, конкретно эту проблему исправить легко. Достаточно создать lock-file (например, с помощью команды npm shrinkwrap) или использовать yarn — пакетный менеджер который по умолчанию фиксирует все зависимости вашего проекта.
Однако это решает только часть проблемы. Остается еще одна, гораздо более опасная ее часть и имя ей unpublish. Если вы не сталкивались с этой проблемой до этого, то вот тут прекрасная статья которая показывает всю уязвимость современной веб разработки. В любой момент, умышленно или по неосторожности, ваш проект может перестать собираться только потому, что кто-то удалил свой пакет из npm. И сделать это отнюдь не сложно. Достаточно просто ввести команду unpublish. С этой бедой можно бороться. Но давайте будем честны перед собой? У кого из нас стоит свой локальный npm? А кто хотя бы задумывался об этом? Боюсь, что не так много. И я лишь надеюсь, что теперь вы предупреждены.
Кстати, если вы зайдете в мой репозиторий (не делайте этого, прошу), то увидите, что почти все мои проекты нарушают все, что было сказано выше. И это еще одно доказательство того, насколько проблема распространена.
Выводы
- Используйте флаг глобальной установки с умом. Не стоит использовать его для тех зависимостей от которых будет постоянно зависеть ваш проект.
- Если вы работаете не один или на нескольких физических машинах, подумайте о том, чтобы задать зависимости строго. Это позволит вам иметь одинаковые основные зависимости проекта во всех репозиториях и у коллег. Кроме того, вы увеличите контроль над собственным кодом и процессом его обновления.
- Используйте lock-файл. Особенно если вы собираетесь использовать continues-integration.
- Задумайтесь о частном регистре пакетов. Это не только убережет вас от внезапного удаления пакетов, но и ускорит установку зависимостей на билд машине. А это сэкономит ваше время и количество кофе, которое выпивается пока проходит билд во время пулл реквеста.
На этом у меня все, надеюсь было интересно и полезно. По странной традиции здесь должны быть какая-то реклама, но ее у меня нет, поэтому я просто передам спасибо коллегам, которые вместе со мной тушили этот пожарчик.
И приношу свои извинения за английские слова, но в русском эквиваленте они сильно режут глаз.