yq to konsolowe narzędzie do manipulacji YAML-em. Czemu warto je znać?
YAML jest teraz wszędzie – konfiguruje się nim pipeline’y CI, sposób deploymentu, pojedyncze aplikacje. Przewagą tego języka nad na przykład JSON-em jest możliwość unikania duplikacji przez zdefiniowanie powtarzającego się kodu w jednym miejscu i „dołączanie” go wielokrotnie. Mając konfigurację CI GitLaba:
.jvm_job: &jvm_job image: openjdk:11-jdk-slim .gradle_job: &gradle_job <<: *jvm_job tags: - medium check: stage: test image: openjdk:11-jdk script: - ./gradlew check <<: *gradle_job deploy_cit: <<: *cit_variables <<: *deploy_job # a dookoła jeszcze tak z 50 różnego rodzaju jobów
możliwości YAML-a umożliwiają nam dzielenie konfiguracji joba na reużywalne kawałki i komponowanie z nich ostatecznych jobów. Z drugiej strony dużo trudniejsze staje się dla czytającego odpowiedzenie na pytanie, co tak właściwie robi dany job: we fragmencie wyżej mamy zarówno coś w rodzaju dłuższej hierarchii dziedziczenia (check) jak i coś w rodzaju wielokrotnego dziedziczenia (deploy_cit). Konfiguracja GitLaba definiuje prawie 30 parametrów konfiguracyjnych jobów, więc rzeczywiste pliki są zazwyczaj dużo bardziej skomplikowane niż przykład wyżej. Nagle od tego, czy umiem w głowie merge’ować spore struktury danych zależy to, czy wprowadzę błąd w konfiguracji, czy nie.
Idealnie byłoby, gdyby jakieś narzędzie wypluwało mi ostateczną postać pliku konfigurowaniu, po wstawieniu wszystkiego na miejsce i zmerge’owaniu. Tyle, że jest z tym bieda: taki GitLab na przykład chwali się w tym miesiącu wizualizacją pipeline’u generowaną z edytowanej konfiguracji, ale konkretnych parametrów tam nie ma. Jeśli ciekawi mnie, jaki image będzie użyty w jobie check
, wizualizacja GitLaba nie przybliży mnie ani o krok do odpowiedzi. Tak w ogóle to odpowiedź brzmi: nie ten image, jaki zamierzył sobie autor kodu, więc pewnie wszystko nie działa.
Ogólnie: systemy przyjmujące YAML-ową konfigurację często nie oferują dobrej możliwości zrozumienia skutków zaaplikowania tej konfiguracji. Dobrze jest znać więc coś niskopoziomowego, nieprzywiązanego do tej czy tamtej aplikacji, coś, co potrafi operować na dowolnym YAML-u, niezależnie od jego przeznaczenia.
Wstęp do yq
yq to taki awk do YAML-a – mały konsolowy program ze swoim językiem wyrażeń (inspirowanym podobnym narzędziem jq pracującym na JSON-ie – swoją drogą też je mocno polecam). Do użycia interaktywnie w terminalu albo automatycznie w skryptach shellowych.
Jest dostępny na wiele platform i nie ma zależności – wystarczy sama binarka. Mając Ubuntu nie trzeba nawet szukać w Internecie, jak go zdobyć – ja wpisałem po prostu w konsoli „yq” i w odpowiedzi dostałem komendę do zainstalowania ze snapa.
% yq Command 'yq' not found, but can be installed with: sudo snap install yq % sudo snap install yq yq 4.3.2 from Mike Farah (mikefarah) installed
yq domyślnie drukuje wejściowy YAML, ujednolicając formatowanie i dodając kolorowanie składni. Mogę na przykład ładnie wyświetlić YAML trzymany w schowku:
Komenda „e” to skrót od „evaluate” i uruchamia podane operacje na wejściowym YAML-u. Tutaj akurat operacji nie podałem, więc wejście będzie wydrukowane bez zmian. Dalej trzeba podać źródło YAML-a: kreska oznacza czytanie ze standardowego wejścia zamiast z pliku. Program xclip umożliwia dostawanie się do schowka systemowego (pisałem o nim osobny artykuł).
Najprostszym operatorem jest wyciągnięcie klucza i wypisanie go – robi się to podając od kropki ścieżkę. Załóżmy, że mam monstrualną konfigurację CI GitLaba, nieposortowaną w żaden sensowny sposób, a chcę zobaczyć, co jest w środku joba check:
% yq e '.check' .gitlab-ci.yml stage: test image: openjdk:11-jdk script: - ./gradlew check !!merge <<: *gradle_job
Jeśli chcę zobaczyć, jaką wartość przyjmuje ostatecznie image dla joba check, muszę najpierw uruchomić operacje merge (symulując to, co robi GitLab wczytując moją konfigurację) – służy do tego operator explode.
% yq e 'explode(.) | .check' .gitlab-ci.yml stage: test image: openjdk:11-jdk-slim script: - ./gradlew check tags: - medium
Pozwala mi to zauważyć, że konfiguracja joba check jest bez sensu – klucz image jest zdefiniowany, ale jego wartość będzie zawsze nadpisana przez merge.
Operatory można łączyć znakiem pipe, tak jakby były to polecenia w linii komend. Mogę na przykład sprawdzić, ile różnych obrazów dockerowych jest używanych w danym pipelinie, gwiazdka pozwala mi dopasować job o dowolnej nazwie:
% yq e 'explode(.) | .*.image' .gitlab-ci.yml | sort | uniq ansible/ansible openjdk:11-jdk openjdk:11-jdk-slim
Normalizacja
Możemy używać różnych sztuczek, by poprawić czytelność plików YAML: dzielić wartości na kilka linii albo dodawać komentarze. Ale czy robimy to dobrze?
Weźmy plik:
image: gradle variables: # important note GRADLE_OPTS: > -DlongOption1 -DlongOption2 -DlongOption3
Czy GRADLE_OPTS to string jedno- czy wielolinijkowy? Jeśli masz specyfikację YAML w pamięci, nie pomylisz ze sobą >
i |
. Jeśli jednak wolisz się upewnić, yq oferuje kilka opcji. Można wyciągnąć konkretny klucz:
% cat .gitlab-ci.yml | yq e '.variables.GRADLE_OPTS' -DlongOption1 -DlongOption2 -DlongOption3
Nie jest to jednak konieczne – domyślnie yq wykonuje pewną normalizację i usuwa łamanie linii:
% cat .gitlab-ci.yml| yq e '.' image: gradle variables: # important note GRADLE_OPTS: > -DlongOption1 -DlongOption2 -DlongOption3
Dodatkową normalizację uruchomimy przekazując opcję -P czyli –prettyPrint:
% cat .gitlab-ci.yml| yq e -P '.' image: gradle variables: # important note GRADLE_OPTS: -DlongOption1 -DlongOption2 -DlongOption3
Trochę bardziej skomplikowane jest usuwanie komentarzy:
% cat .gitlab-ci.yml| yq e '... comments=""' image: gradle variables: GRADLE_OPTS: > -DlongOption1 -DlongOption2 -DlongOption3
Aby wyciągnąć zagnieżdżony element bez komentarzy, trzeba operację pobrania uruchomić po usuwaniu komentarzy, łącząc operacje pipe’em:
% cat .gitlab-ci.yml| yq e '... comments="" | .variables' GRADLE_OPTS: >- -DlongOption1 -DlongOption2 -DlongOption3
Krótko o pozostałych operacjach
W skryptach shellowych yq może się przydać do modyfikowania plików YAML: można między innymi zmieniać wartość kluczy, dopisywać elementy do list:
% yq e '.check.image = "openjdk:13" | .check.script += "./gradlew --version"' .gitlab-ci.yml .gradle_job: &gradle_job !!merge <<: *jvm_job tags: - medium check: !!merge <<: *gradle_job stage: test image: openjdk:13 script: - ./gradlew check - ./gradlew --version
Można też łączyć dane z kilku plików. Należy użyć komendy eval-all do załadowania wszystkich plików na raz zamiast jednego po drugim i operatora fileIndex, żeby wskazać, z którego pliku brać klucz. Z każdego pliku produkujemy nowy dokument z wyciągniętymi danymi, a następnie łączymy dokumenty w jeden za pomocą operatora gwiazdki.
% yq eval-all '(select(fi == 0) | {"env": .deploy_cit.variables.ENVIRONMENT_NAME}) * (select(fi == 1) | {"profiles": .spring.profiles.active})' .gitlab-ci.yml application.yml env: cit profiles: dev
Więcej można dowiedzieć się z dokumentacji.