Введение в магию jq

jq — это волшебный инструмент для работы с JSON в CLI. Как и его знаменитые предшественники, sed и awk, он недружелюбен к новичкам (или, как сказали бы старые бородачи, он просто разборчив в выборе друзей).

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

Хватит разглагольствовать, давайте покажем немного магии jq.

Задача, которую я хотел решить, заключалась в сравнении двух списков. Сначала немного предыстории: у нас есть группа безопасности на Scaleway, которая ограничивает входящие соединения с нашими экземплярами сервера API. Единственная организация, к которой мы хотим иметь возможность открывать соединения, это Cloudflare, которая обеспечивает балансировку нагрузки, на которую разрешается api.ente.io, а затем обратное проксирование входящих соединений к нашим экземплярам API-серверов. Поэтому диапазоны IP-адресов, с которых Cloudflare может подключаться к нашим экземплярам, должны быть внесены в белый список в нашей группе безопасности Scaleway.

Поэтому моя цель — убедиться, что эти два списка — разрешенные диапазоны IP-адресов в группе безопасности и диапазоны IP-адресов Cloudflare — совпадают.

Это идеальный случай для демонстрации возможностей jq. Мы увидим фильтрацию, объединение, сортировку и многое другое. Мы надеемся, что на реальном примере (это то, что мне действительно нужно было сделать) полезность jq станет еще более очевидной.

Во-первых, давайте начнем с получения ID нашей группы безопасности. Мы будем использовать инструмент Scaleway CLI (называется scw).

$ scw instance security-group list name=scg-prod
ID                                    NAME      DESCRIPTION
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx  scg-prod  Security group - ...
Вход в полноэкранный режим Выход из полноэкранного режима

Нам нужен вывод в формате JSON, для чего команда scw предоставляет удобный переключатель --output json.

$ scw instance security-group list --output json name=scg-prod
[{"id":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","name":"scg-prod",...}]
Вход в полноэкранный режим Выход из полноэкранного режима

Мы получаем большой сгусток JSON, читаемый машиной, но нечитаемый для нас. Давайте украсим его, передав в jq.

$ scw instance security-group list --output json name=scg-prod | jq
[
  {
    "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "name": "scg-prod",
    ...
   }
]
Вход в полноэкранный режим Выход из полноэкранного режима

Если вы больше ничего не вынесли из этого сообщения, просто запомните, что | jq — это быстрый способ приукрасить JSON.

Идем дальше. Нам нужен только «id» группы безопасности (он понадобится нам для следующей команды, которая получит подробную информацию о группе безопасности). Для этого мы можем использовать .id, чтобы заставить jq выдать только значение поля «id» в объекте JSON, который мы ему передаем.

JSON, который мы видим выше, является массивом, но нас интересует только первый элемент, поэтому мы можем использовать first, чтобы получить его. Комбинируя first (чтобы получить первый элемент) и .id (чтобы получить значение «id» внутри этого элемента), мы получим следующее

$ scw instance security-group list --output json name=scg-prod | jq 'first.id'
"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
Войти в полноэкранный режим Выйти из полноэкранного режима

Нам нужно только значение без кавычек, поэтому попросим jq выдать нам необработанный результат, указав флаг «—raw-output» (или его сокращение, «-r»).

$ scw instance security-group list --output json name=scg-prod | jq -r 'first.id'
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Вход в полноэкранный режим Выход из полноэкранного режима

Отлично.

Теперь давайте узнаем подробности об этой группе безопасности. Мы могли бы скопировать вывод выше, но мы круче этого. Поэтому мы используем синтаксис $( ) для запуска
подкоманду, результат которой затем используется в качестве аргумента для команды верхнего уровня.

$ scw instance security-group get "$(scw instance security-group list --output json name=scg-prod | jq -r 'first.id')"
Security Group:
ID                     xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
...
Войти в полноэкранный режим Выйти из полноэкранного режима

И снова нам нужен вывод JSON, а чтобы его было легче проверить, мы хотим его украсить, передав в jq.

$ scw instance security-group get --output json "$(scw instance security-group list --output json name=scg-prod | jq -r 'first.id')" | jq
{
  "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "name": "scg-prod",
  ...
  "Rules": [
    {
      "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "protocol": "TCP",
      "direction": "outbound",
      "action": "drop",
      "ip_range": "0.0.0.0/0",
      "dest_port_from": 465,
      "dest_port_to": null,
      "position": 2,
      "editable": false,
      "zone": "fr-par-1"
    },
    ...
Вход в полноэкранный режим Выйти из полноэкранного режима

Итак, у нас есть объект JSON, представляющий группу безопасности. Он имеет массив под названием «Rules», каждое из которых является правилом группы безопасности, которое нужно применить.

Нас интересуют только правила, поэтому мы можем попросить jq выдать нам только массив «Rules» с помощью команды

jq '.Rules'
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь из всех правил нас интересуют только «входящие» правила (не «исходящие»). Поэтому мы попросим jq выбрать только те правила, у которых «direction» равно «inbound», сказав select(.direction == "inbound").

Что делает select, так это позволяет пройти только тем объектам, которые удовлетворяют заданным критериям, остальные отбрасываются. Однако select работает с объектами, а Rules — это массив. Поэтому нам нужен способ применить select к каждому объекту в массиве Rules
массива. Та-да! Карта!

jq '.Rules | map(select(.direction == "inbound"))'
Вход в полноэкранный режим Выход из полноэкранного режима

Мы также хотим отфильтровать только те правила, которые предназначены для порта 443 (порт HTTPS, на котором прослушиваются наши экземпляры API).

jq '.Rules | map(select(.direction == "inbound" and .dest_port_from == 443))'
Войти в полноэкранный режим Выход из полноэкранного режима

Наконец, это даст нам все объекты правил, которые удовлетворяют условию, но нам нужны только диапазоны IP-адресов из каждого правила. Поэтому давайте добавим еще один шаг в конвейер, чтобы извлечь только «ip_range» из каждого из отфильтрованных правил.

jq '.Rules | map(select(.direction == "inbound" and .dest_port_from == 443)) | map(.ip_range)'
Вход в полноэкранный режим Выход из полноэкранного режима

Последние два шага являются картами, и их можно объединить, чтобы получить тот же эффект.

jq '.Rules | map(select(.direction == "inbound" and .dest_port_from == 443) | .ip_range)'
Вход в полноэкранный режим Выход из полноэкранного режима

Давайте также отсортируем диапазоны (это упростит сравнение в дальнейшем).

jq '.Rules | map(select(.direction == "inbound" and .dest_port_from == 443) | .ip_range) | sort'
Войти в полноэкранный режим Выход из полноэкранного режима

В полном контексте,

$ scw instance security-group get --output json "$(scw instance security-group list --output json name=scg-prod | jq -r 'first.id')" | jq '.Rules | map(select(.direction == "inbound" and .dest_port_from == 443) | .ip_range) | sort'
[
  "103.21.244.0/22",
  "103.22.200.0/22",
  "103.31.4.0/22",
  "104.16.0.0/13",
  "104.24.0.0/14",
  "108.162.192.0/18",
  "131.0.72.0/22",
  "141.101.64.0/18",
  "162.158.0.0/15",
  "172.64.0.0/13",
  "173.245.48.0/20",
  "188.114.96.0/20",
  "190.93.240.0/20",
  "197.234.240.0/22",
  "198.41.128.0/17",
  "2400:cb00::/32",
  "2405:8100::/32",
  "2405:b500::/32",
  "2606:4700::/32",
  "2803:f800::/32",
  "2a06:98c0::/29",
  "2c0f:f248::/32"
]
Войти в полноэкранный режим Выйти из полноэкранного режима

Великолепно!

Теперь давайте получим вторую половину данных — список IP-адресов Cloudflare. Их можно получить из API Cloudflare.

$ curl -s 'https://api.cloudflare.com/client/v4/ips'
{"result":{"ipv4_cidrs":...
Войти в полноэкранный режим Выход из полноэкранного режима

Снова передадим его в jq, чтобы получить представление о структуре JSON.

$ curl -s 'https://api.cloudflare.com/client/v4/ips' | jq
{
  "result": {
    "ipv4_cidrs": [
      "173.245.48.0/20",
      ...
    ],
    "ipv6_cidrs": [
      "2400:cb00::/32",
      ...
    ],
    ...
  },
  "success": true,
  "errors": [],
  "messages": []
}
Вход в полноэкранный режим Выход из полноэкранного режима

Итак, нас интересуют массивы «ipv4_cidrs» и «ipv6_cidrs» внутри объекта «result» JSON ответа.

В jq есть интересный оператор запятой, который дублирует входящий поток и отправляет его на обе операции запятой. Таким образом, мы можем получить и «ipv4_cidrs» и «ipv6_cidrs» из одного и того же объекта результата.

jq '.result | .ipv4_cidrs,.ipv6_cidrs'
Вход в полноэкранный режим Выйти из полноэкранного режима

Однако это даст нам два массива — массив «ipv4_cidrs» и массив «ipv6_cidrs». Нам нужен один массив. Мы можем сплющить два массива в один, используя оператор «flatten». Однако перед сглаживанием нам нужно поместить оба массива в один массив-контейнер. Это
легко — просто оберните их в [].

jq '.result | [.ipv4_cidrs,.ipv6_cidrs] | flatten'
Вход в полноэкранный режим Выход из полноэкранного режима

Наконец, давайте также отсортируем их

jq '.result | [.ipv4_cidrs,.ipv6_cidrs] | flatten | sort'
Вход в полноэкранный режим Выход из полноэкранного режима

В полном контексте,

$ curl -s 'https://api.cloudflare.com/client/v4/ips' | jq '.result | [.ipv4_cidrs,.ipv6_cidrs] | flatten | sort'
[
  "103.21.244.0/22",
  "103.22.200.0/22",
  "103.31.4.0/22",
  "104.16.0.0/13",
  "104.24.0.0/14",
  "108.162.192.0/18",
  "131.0.72.0/22",
  "141.101.64.0/18",
  "162.158.0.0/15",
  "172.64.0.0/13",
  "173.245.48.0/20",
  "188.114.96.0/20",
  "190.93.240.0/20",
  "197.234.240.0/22",
  "198.41.128.0/17",
  "2400:cb00::/32",
  "2405:8100::/32",
  "2405:b500::/32",
  "2606:4700::/32",
  "2803:f800::/32",
  "2a06:98c0::/29",
  "2c0f:f248::/32"
]
Войти в полноэкранный режим Выйти из полноэкранного режима

Ура! Теперь осталось только сравнить два списка, что можно сделать с помощью команды diff.

Если полный текст команды, который будет приведен позже, слишком запутан, вы также можете сделать это, сохранив каждый список во временный файл, а затем сравнив временные файлы:

diff /tmp/output-of-scw-instances /tmp/output-of-cf-api
Войти в полноэкранный режим Выход из полноэкранного режима

Но мы, как рок-звезды, сделаем все в одной команде, используя синтаксис подстановки процесса «<()», который поддерживается многими оболочками. Оболочка возьмет вывод того, что находится внутри «<()», поместит его во временный файл и передаст этот файл команде верхнего уровня. Это примерно то, что мы сделали бы вручную.

Давайте попробуем запустить монстра во всей его красе.

$ diff <( scw instance security-group get --output json "$(scw instance
security-group list --output json name=scg-prod | jq -r 'first.id')" | jq
'.Rules | map(select(.direction == "inbound" and .dest_port_from == 443) |
.ip_range) | sort' ) <( curl -s 'https://api.cloudflare.com/client/v4/ips' | jq
'.result | [.ipv4_cidrs,.ipv6_cidrs] | flatten | sort' )`
$
Войти в полноэкранный режим Выйти из полноэкранного режима

Выхода не было, что хорошо! Это значит, что между двумя списками не было никакой разницы.

Итак, наша работа на этом закончена, давайте теперь отдохнем, ибо наступает день другой.


Если вам понравилось то, что вы здесь увидели, передавайте привет в Twitter или приходите потусоваться с нами в Discord. До тех пор, оставайтесь спокойными и jq 😎

Оцените статью
Procodings.ru
Добавить комментарий