Continuous integration i sprawdzanie jakości kodu przy użyciu Liferaya 7

Przez Rafał Pydyniak | 2020-08-22

Cześć! Dzisiaj chciałbym przedstawić jak można wykorzystać Jenkinsa do procesu "Continuous integration" czyli ciągłej integracji naszego kodu a także do procesu ciągłego sprawdzania jakości kodu z wykorzystaniem narzędzia SonarQube.

Postanowiłem napisać ten post gdyż jakiś czas temu spotkałem się z takim właśnie zadaniem i jak się okazało, nie ma zbyt wielu pomocnych materiałów odnośnie tego tematu.

Wymagania wstępne

Aby rozpocząć potrzebujemy kilku rzeczy:

  • Projektu Liferaya - na potrzeby tego postu stworzyłem prosty projekt, z którego również możesz skorzystać. Zawiera on zaledwie kilka klas, aczkolwiek powinien być wystarczający do celów "naukowych". Kod tego projektu możesz znaleźć na moim githubie.
  • Jenkins
  • SonarQube

Jenkins

Zacznijmy od Jenkinsa. Większość z czytających pewnie o tym narzędziu słyszała, ale gdybyś jednak nie spotkał się z nim wcześniej to już tłumaczę -  Jenkins jest to open sourcowe narzędzie służące automatyzacji. Używany jest bardzo często do automatyzacji wszelakich zadań około programistycznych takich jak budowanie aplikacji, uruchamiania testów, a także wrzucaniu aplikacji na serwer, aczkolwiek nie ogranicza się tylko do tego i równie dobrze może zostać użyty np. do cyklicznego uruchamiania jakiegoś skryptu czy aplikacji.

Instalacja Jenkinsa nie powinna sprawić Ci żadnego problemu, a wszelkie niezbędne informacje można znaleźć na oficjalnej stronie. Jenkinsa można zainstalować zarówno lokalnie (podobnie jak każdą inną aplikacje) lub wykorzystać do tego celu Dockera. Osobiście polecam Dockera gdyż jest to świetne narzędzie, aczkolwiek możesz wybrać dowolną opcje.

SonarQube

Drugim narzędziem, którego będziemy potrzebowali jest SonarQube czyli open sourcowe narzędzie służące automatycznemu sprawdzaniu jakości naszego kodu. SonarQube może wykryć tak zwane "code smells", bugi, duplikacje kodu i inne rzeczy tego typu. Instrukcje instalacji, podobnie jak w przypadku Jenkinsa, można znaleźć na oficjalnej stronie. Po raz kolejny możesz wybrać między standardową instalacją, a Dockerem.

Integracja

Gdy już mamy wspomniane narzędzia możemy rozpocząć konfiguracje naszej integracji między Jenkinsem, SonarQubem a naszym projektem.

Generalnie to co chcemy zrobić to:

  • Skonfigurować projekt w SonarQube
  • Dodać plugin "SonarQube Scanner" w Jenkinsie, tak aby móć odpalić Sonara z poziomu Jenkinsa
  • Skonfigurować wyżej wymienione plugin
  • Stworzyć zadanie w Jenkinsa służące budowaniu naszego projektu oraz skanowaniu kodu za pomocą Sonara

Konfiguracja projektu w SonarQube

Zanim rozpoczniemy integracje potrzebujemy stworzyć projekt w SonarQubie. Aby to zrobić musimy odpalić nasz serwer Sonara (najprawdopodobniej dostępny pod adresem localhost:9000), zalogować się (domyślne dane to admin/admin) oraz kliknąć przycisk "plusika" w prawym górnym rogu.

Następnie generujemy nasz token - po prostu podajemy jakąś nazwe i klikamy "Generate". Token zostanie dla nas wygenerowany i będziemy mogli go użyć do integracji np. właśnie z Jenkinsem.

Wygenerowany token w SonarQube
Wygenerowany token w SonarQube

Zapisz sobie wygenerowany token oraz nazwę projektu, przydadzą się one później. Na ekranie wygenerowanego tokenu możesz także kliknąć "Continue", aby zobaczyć przykłady jak można uruchomić skanowanie projektu w Gradle lub Mavenie, aczkolwiek w naszym przypadku nie będzie to potrzebne.

Dodawanie pluginu "SonarQube scanner" do Jenkinsa i jego konfiguracja

Kolejnym krokiem niezbędnym do naszej integracji jest zainstalowanie pluginu "SonarQube Scanner" w Jenkinsie. Możemy to zrobić wchodząc w (ze strony głównej):

Manage Jenkins -> Manage plugins -> Available -> Search for "SonarQube Scanner" -> zaznacz pierwszą dostępną opcje i zaznacz "Install with restart"

Następnie, po instalacji pluginu, idziemy do (z strony głównej):

Manage Jenkins -> Global tool Configuration -> W części "SonarQube Scanner" klikamy "Add SonarQube Scanner" i wpisujemy jakąs nazwę. Dla reszty opcji zostawiamy wartości domyślne. Następnie potwierdzamy naszą konfiguracje poprzez kliknięcie "Save".

Konfiguracja SonarQube Scannera w Jenkinsie
Konfiguracja SonarQube Scannera w Jenkinsie

Wypełniona konfiguracja pluginu SonarQube Scanner

Kolejny krok to konfiguracja serwera SonarQube. Aby to zrobić przechodzimy do (ze strony głównej Jenkinsa):

Manage Jenkins -> Configure System -> Szukamy sekcji"SonarQube servers" i klikamy "Add SonarQube". Nazywamy nasz serwer (nazwa jedynie dla nas) oraz wprowadzamy adres URL.

Stworzenie projektu (zadania) w Jenkinsie

Jenkins oferuje nam kilka typów "projektów". Opis tych typów można znaleźć chociażby w dokumentacji lub w innych artykułach. My skupimy się na tak zwanym "Freestyle Project", który umożliwia nam zdefiniowanie różnych akcji (takich jak budowanie, testy, deployment itp), które wykonają się po kolei. Aby stworzyć taki projekt klikamy na "New item" z strony głównej i wybieramy "Freestyle project". Nazywamy go wedle uznania, a następnie klikamy "ok".

Przycisk do stworzenia nowego projektu w Jenkinsie
Przycisk do stworzenia nowego projektu w Jenkinsie

Nowe zadanie w Jenkinsie

Opcje nowego projektu w Jenkinsie
Opcje nowego projektu w Jenkinsie

Ekran tworzenia nowego projektu

Gdy klikniemy ok zostaniemy przekierowani do konfiguracji stworzonego zadania. Nie będę opisywał wszystkich dostępnych ustawień, gdyż jest ich całkiem sporo a jednocześnie większość z nich jest albo prosta i jasna albo służy do bardzo specyficznych zastosowań i nie będą Ci nigdy (albo prawie nigdy) potrzebne. Gdybyś jednak chciał się zapoznać z możliwościami to przy każdej z opcji dostępny jest "Znak zapytania", na który można kliknąć, aby dowiedzieć się więcej o danym polu.

Mając stworzone zadanie warto zastanowić się co właściwie chcemy tym zadaniem osiągnąc. W naszym przypadku będziemy mieli następujące kroki:

  • Ściągnięcie kodu z naszego repozytorium - w moim przypadku będzie to GIT, ale równie dobrze możesz użyć np. serwera SVN
  • Po ściągnięciu najnowszego kodu chcemy go zbudować używająć gradle. Oczywiście równie dobrze mógłby to być maven, kroki są analogiczne i na pewno nie sprawią Ci większego problemu
  • Na koniec chcemy przeskanować nasz kod SonarQubem w celu sprawdzenia jego jakości

W naszym przykładzie to będzie wszystko, aczkolwiek w rzeczywistości można by zrobić dużo więcej. Przykładowo po zbudowaniu kodu można uruchomić testy (a nawet należy - bo testy zawsze warto pisać!) albo nawet pójść o krok dalej i automatycznie deployować naszą aplikacje na serwer (Continious deployment).

Ściągnięcie kodu z repozytorium

W celu automatycznego pullowania kodu z naszego repozytorium przechodzimy do sekcji "Source code management". Wybieramy rodzaj repozytorium, którego będziemy używać - w moim przypadku GIT. Musimy jeszcze dodać dane autoryzacyjne. Aby to zrobić klikamy "Add" (zakładając, że robimy to po raz pierwszy) i wybieramy "Jenkins". W panelu, który się nam pokaże wpisujemy login oraz hasło i zatwierdzamy przyciskiem add. Dane autoryzacyjne zostaną zapisane i następnym razem będziemy mogli ich użyć wybierając odpowiedni wpis z listy przy polu "Credentials".

Zdjęcie pokazujące wypełnione pole z danymi autoryzacyjnymi
Zdjęcie pokazujące wypełnione pole z danymi autoryzacyjnymi

Budowanie aplikacji

Teraz przechodzimy do sekcji "Build". W sekcji tej możemy dodać kroki (jeden lub więcej), które zostaną kolejno wykonane. Takim krokiem może być np. uruchomienie Gradle czy Anta, uruchomienie komendy shellowej (lub CMD w windowsie) itp. W naszym wypadku klikamy "Invoke gradle", a następnie wybieramy wersje, której chcemy użyć lub zaznaczamy użycie Gradle Wrappera. Ta druga opcja jest rekomendowana, a więc jej użyjemy. Możemy także zaznaczyć "Make gradlew executable" i tak też zrobimy. Zaznaczenie tego pola oznacza, że przed uruchomieniem gradlew zostanie zrobione coś w stylu chmod +x ./gradlew. W innym wypadku nasz ./gradlew może nie być wykonywalny i zadanie się nie powiedzie.

W sekcji "Task" wpisujemy task, który chcemy wykonać:

clean build

Uruchamianie Gradle w Jenkinsie
Uruchamianie Gradle w Jenkinsie

Skanowanie kodu przy użyciu SonarQube

Po zbudowaniu projektu, zakładając, że nie zakończy się ono błędem, chcielibyśmy uruchomić skanowanie SonarQubem. Aby to zrobi dodajemy kolejny krok klikająć na "Add Build step" tuż pod zadaniem uruchomienia gradle. Wybieramy "Execute SonarQube Scanner", następnie w części "Analysis properties" musimy wprowadzić informacje niezbędne do przeskanowania naszego projektu. Dostępnych opcji jest cała masa i można je znaleźć w dokumentacji Sonara, ale nam wystarczy zaledwie kilka:

  • sonar.login - wygenerowany wcześniej token
  • sonar.projectKey - wprowadzony "projectKey" podczas tworzenia projektu
  • sonar.java.binaries - definiujemy gdzie są nasze zbudowane klasy
  • sonar.java.libraries - lokalizacja dodatkowych bibliotek, których używamy
  • sonar.java.source - wersja Javy w naszym projekcie

Poniżej screen z wprowadzonymi standardowymi wartośćiami - o ile nie masz jakichś szczególnych ścieżek albo wymagań to powinny one zadziałać także u Ciebie (oczywiście poza tokenem i projectKey):

Konfiguracja SonarQube w Jenkinsie
Konfiguracja SonarQube w Jenkinsie

Czas na testy!

Uruchomienie stworzonego zadania w Jenkinsie

Mamy spełnione wszystkie kroki potrzebne do uruchomienia naszego zadania. Po zapisaniu naszej konfiguracji w Jenkinsie możemy uruchomić stworzone zadanie klikając "Build now" (jeśli jesteśmy na stronie głównej musimy najpierw przejść do naszego zadania klikając na jego nazwe).

Czekamy chwile, a po wszystkim powinniśmy zobaczyć numer zadania z niebieską kropką, która wskazuje, że wszystko poszło ok:

Podgląd historii buildów
Podgląd historii buildów

W zakładce "Build history" można zobaczyć status wcześniejszych uruchomień zadania (możesz również zauważyć, że moje ma numerek #4 - niestety poprzednie 3 nie wyszły, ale mam nadzieję, że Tobie uda się za pierwszym razem ;) )Możesz także kliknąć na numerek builda, aby przejść do zakładki, w której dostępne są szczegóły zadania takie jak np. logi z konsoli, co jest bardzo przydatne (lub wręcz niezbędne) podczas sprawdzania co poszło nie tak.

Sprawdzenie rezultatu w SonarQube

Przejdźmy do naszego projektu w aplikacji SonarQube i zobaczmy wynik naszego zadania. Jeśli jesteś na stronie głównej w SonarQubie możesz przejść do "Projects" i wejść do odpowiedniego projektu poprzez kliknięcie na jego nazwe.

Uwaga: czasami musisz poczekać chwilę zanim rezultat naszego skanu pojawi się w SonarQubie. Dzieję się tak dlatego, że Jenkins wysyła dane do SonarQube i kończy zadanie, ale SonarQube dopiero wtedy zaczyna swoje procesowanie przesłanych danych. W przypadku mojego testowego projektu rezultat będzie prawdopodobnie od razu, aczkolwiek w większych projektach jest pewne opóźnienie i trzeba mieć to na uwadze.

Następnie możesz przejśc do swojego projektu, aby zobaczyć podsumowanie wykonanego skanowania kodu:

Podsumowanie błędów znalezionych w Sonarze
Podsumowanie błędów

W podsumowaniu można zobaczyć dwie zakładki:

  • New code - pokazane są statystyki dla nowgo kodu
  • Overall code - podsumowanie dla całego kodu

Główna różnica polega na tym, że jeżeli mamy np. błędy, które mamy "od zawsze" to zostaną one policzone jedynie w "Overall code", ale nie w "New code". Dlaczego? Przede wszystkim dlatego, że dla dużych projektów tych błędów mogą być setki i niekoniecznie ktokolwiek będzie ja poprawiał, możemy w takiej sytuacji śledzić jedynie czy sytuacja bieżąca jest wystarczająco dobra, a błędy w starym kodzie poprawiać stopniowo.

Kolejną przydatną (lub wręcz najważniejszą) zakładką jest "Issues", która pokazuje konkretne błędy w Twoim kodzie. Możesz w niej podejrzeć każdy znaleziony błąd, a także przefiltrować błędy po krytyczności, typie, miejscu w kodzie itp. Dodatkowo możliwe jest przypisanie poszczególnych błędów do konkretnych osób czy też edycja standardowym opcji dla błędu np. jego krytyczności. Istnieje także możliwość oznaczenia błędu jako "false positive" lub "won't fix" co może być przydatne gdyż Sonar (jak każde narzędzie) czasami popełnia błędy w swojej ocenie kodu.

Problemy z kodem z Liferayowego Service Buildera

Jeśli zaczniesz przeglądać znaleziony błędy możesz zauważyć pewien problem - Sonar znajduję naprawdę dużo błędów. Są dwie opcje - albo twój kod jest naprawdę tragiczny albo coś jest nie tak. Jeśli spojrzysz na mój kod testowy to zobaczysz, że praktycznie nie ma tam kodu, a jednak Sonar znalazł sporo błędów. Przyczyna jest dość trywialna - problemem jest automatycznie wygenerowany kod przez Service Buildera. W przypadku mojego testowego projektu mam prawie 100 błędów, mimo że dodałem kod zaledwie w trzech klasach. Dużo, nawet biorąc pod uwagę, że błędy robiłem specjalnie.

Moglibyśmy oczywiście oznaczyć te błędy jako "False positive", aczkolwiek w prawdziwym projekcie nie będzie to zbyt rozsąne - liczba błędów byłaby zbyt duża. Tutaj pojawiło się prawie 100 błędów z jednej encji bazodanowej, a w prawdziwym projekcie takich encji mogą być dziesiątki, sam pracowałem przy projekcie z 150 encjami obsługiwanymi przez Service Buildera, a w Twoim projekcie może być ich jeszcze więcej.

Na szczęście istnieje inny sposób. Możemy powiedzieć Jenkinsowi, aby ignorował niektóre ścieżki przy użyciu właściwośći sonar.exclusions. Przy użyciu tego "property" możemy wybraż ścieżki do ignorowania. Wydaję się proste, aczkolwiek są z tym dwa problemy:

  • Jeśli używasz Liferaya 7 to prawdopodobnie masz wiele modułów (a przynajmniej powinieneś bo modularność to dobra rzecz!). To oznacza, że ciężko wskazać jedną ścieżke bo takich modułów, z automatycznie wygenerowanym kodem, może być kilkanaście.
  • Moglibyśmy użyć więc jakiegoś "dynamicznego" matchowania nazw, ale to też nie jest do końca proste. W teorii moglibyśmy po prostu ignorować wszystkie ścieżki kończące się na *-api oraz *-service, ale w w praktyce w modułach *-service także jest nasz kod w klasach typu *Impl.

Logiczne wydają się więc dwa rozwiązania:

  • Jeśli nie masz aż tylu modułów to możesz po prostu ręcznie wypisać ścieżki do ignorowania
  • Wymyślić jakąś "ogólną" regułe, która zadziałałaby w Twoim projekcie. Reguła ta musi być wystarczająco dokładna, aby wyeliminować wszystkie miejsca, których nie chcemy skanować, ale jednocześnie nie powinna wykluczać Twojego kodu

Ogólna reguła wykluczenia

Osobiście nie miałem komfortu pracy przy jedynie jednym czy dwóch modułach z serwisami więc wybrałem opcje numer dwa. Poniższej reguły używam w prawdziwym projekcie i mogę powiedzieć, że działa ona dobrze. Konfiguracja to:

sonar.exclusions=**/build/resources/main/META-INF/resources/**/*.jsp,**/*-service/**/model/impl/**/*.java,**/*-service/**/service/persistence/**/**/*.java,**/*-service/**/service/base/**/*.java,**/*-service/**/service/http/**/*.java,**/*-service/**/service/util/**/*.java,**/*-api/**/dto/**/*.java,**/*-api/**/exception/**/*.java,**/*-api/**/model/**/*.java,**/*-api/**/service/**/*.java

Mamy więc zdefiniowane ścieżki rozdzielone przecinkami. Używamy także dwóch dostępnych możliwośći w określaniu ścieżek:

  • * - zastępuję jeden lub więcej znak w nazwie pliku
  • ** - zastępuje jeden lub więcej katalogów

Reguła ta ignoruje automatycznie wygenerowany kod zarówno w modułach *-service jak i *-api. Czy jest to reguła idealna? Niestety nie. Zawiera ona pewne założenia co do struktury twojego projektu, przykładowo ignoruje ona **/*-service/**/service/util/**/* ale równie dobrze możesz mieć moduł o nazwie przykładowo blogs-service a w środku posiadać katalog service/util z swoim kodem. Co wtedy? Niestety nie ma prostego rozwiązania. Możesz albo doprecyzować regułę do swojego przypadku albo wybrać opcje z ręcznym wskazywaniem katalogów. W większości przypadków powinna ona jednak zadziałać.

Kolejny test

Po dodaniu wspomnianej wyżej reguły rezultat uruchomienia naszego zadania w Jenkinsie powinien być zdecydowanie lepszy. W moim przypadku z 100 błędów zostało mi zaledwie 10 i rzeczywiście są one rzeczywistami błędami (czy też tak zwanymi "code smell"). Dodatkowo widzę, że posiadam jeden błąd w klasie *LocalServiceImpl co potwierdza, że reguła zadziałała poprawnie i nie ignoruje zbyt wiele.

Błędy SonarQube
Błędy SonarQube

Podsumowanie

Podsumowując. Stworzyliśmy zadanie w Jenkinsie, które uruchamia nasz projekt, wysyła rezultat do SonarQube. W Sonarze możemy zweryfikować "jakość" naszego kodu i sprawdzić poszczególny błędy.

W artykule tym nie poruszyłem wszystkiego bo i tak rozrósł się już za bardzo. Przykładowo nie wspominałem o tzw "Build triggers" czyli możliwości cyklicznego sprawdzania naszego repozytorium w celu sprawdzenia czy istnieje nowy kod, który można zbudować i przeskanować. W praktyce prawdopodobnie chcesz dodać taki wyzwalacz do swojego Jenkinsowego zadania. O wyzwalaczach tych możesz znaleźć wiele informacji w dokumentacji czy innych blogach.

Kolejnym krokiem automatyzacji może być automatyczne uruchomienie testów, automatyczny deployment czy np. wysyłka maili w przypadku błędów w budowaniu kodu (sekcja post-build actions w konfiguracji zadania) albo może coś jeszcze innego, specyficznego dla procesu w Twojej firmie czy twoim zespole.

Jenkins daje nam wiele możliwości i zachęcam do dokładniejszego zapoznania się z nimi, niezależnie czy do projektów w Liferayu czy jakichkolwiek innych. Warto również zwrócić uwagę na to, że istnieje masa pluginów, które pozwalają nam się integrować z różnymi narzędziami co dodatkowo poszerza możliwości Jenkinsa.

Mam nadzieję, że przydał Ci się ten artykuł i pamiętaj, że jeśli masz jakieś pytania to możesz napisać komentarz lub napisać do mnie.

Copyright: Rafał Pydyniak