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 😎