Skryptologia – shim on you

Tym razem trochę o tym jak działa zmienna $PATH i co to jest shim. Zanim jednak przejdę do mięska, to kilka słów o wzorcach projektowych.

Wśród wzorców mamy trzy, które działają podobnie i często są mylone.

  • Adapter – pozwala na podmianę jednego API na inne. Pełni rolę tłumacza pomiędzy API. Całkowicie zmienia oryginalne API.
  • Fasada – ogranicza API do wybranych elementów. Zubaża je, „podpowiadając”, z czego w danym kontekście należy korzystać.
  • Proxy – opakowuje API w celu ochrony lub zasłonięcia dodatkowych czynności potrzebnych przy wywołaniu (np. wywołaniu zdalnym). Nie zmienia jednak samego API (sygnatur).

Jak widać, różnica polega na tym, ile z pierwotnego API zostaje. Pomyłki związane z pomyleniem tych wzorców wynikają zazwyczaj z potrzeb językowych. Jeżeli mamy cztery klasy-adaptery ogarniające nam komunikację ze starym API, to przecież nazwanie piątej klasy fasadą czy proxy spowoduje tylko zamieszanie. W dodatku na CR mogą pojawić się niewygodne pytania, które będą prowadzić do jałowej dyskusji o niuansach w kodzie. Szczególnie że istnieje jeszcze wzorzec mostu, który pozwala na separowanie separację interfejsu i implementacji. Zatem lepiej użyć niewłaściwego słowa i mieć spokój niż wojować o to właściwe opóźniając św. Wartość Biznesową.

A teraz dołożę wam jeszcze jeden wzorzec – shim. Nie ma on ładnej polskiej nazwy, a bezpośrednie tłumaczenie „podkładka” nie oddaje do końca sensu tego wzorca. Najprościej można go opisać jako odmianę adaptera, którego zadaniem jest kompensowanie błędów i różnic w API. Zazwyczaj używa się go w celu zapewnienia kompatybilności wstecznej, w tym kompatybilności z wszystkimi bugami, albo w celu naprawy istniejącego kodu, do którego nie możemy dobrać się bezpośrednio. Można go też użyć w celu zapewnienia kompatybilności w przód, ograniczając nowsze API, w taki sposób, by stare rozwiązania mogły z niego korzystać.
Shim działa zatem trochę jak adapter, trochę jak proxy, trochę jak fasada, czerpiąc z każdego tych wzorców pewne elementy funkcjonalne. Nic więc dziwnego, że głównym użytkownikiem tego rozwiązania są ofiary Windowsa, gdzie mamy cały zestaw narzędzi w ramach Application Compatibility Toolkit.

A teraz mięsko.

Mechanizm tego wzorca można wykorzystać w bardzo ciekawy sposób. Mam kilka projektów, które korzystają z mavena jako głównego narzędzia do budowania. Problem polega na tym, że pewne projekty korzystają z prywatnych repozytoriów, a te w mavenie konfigurujemy w pliku settings.xml. Nie chcę też dopuścić do sytuacji, gdzie w głównym pliku ustawień będę miał widoczne repozytoria „z różnych parafii”. Po pierwsze klienci nie powinni o sobie wiedzieć. Po drugie znacznie łatwiej jest wtedy zarządzać ustawieniami. Kończę projekt, usuwam kod, nie muszę nic usuwać z innych miejsc.

Rozwiązaniem jest zatem użycie lokalnego pliku settings.xml i uruchamianie mavena z przełącznikiem -s. Dodatkowo zazwyczaj korzystam z -U, który pozwala na dociągniecie aktualnych snapshotów. Bardzo przydatne w projektach z wieloma modułami i zależnościami wewnątrzkorporacyjnymi. Użycie to wiąże się z każdorazowym wpisywaniem -U -s settings.xml. Mocno niewygodne. W dodatku trzeba pamiętać, że może to zepsuć skrypty, bo tu się zapomni, tam się nie doda i tragedia gotowa.

Rozwiązania są teoretycznie trzy.

Po pierwsze możemy stworzyć alias:

Listing 1. Aliasowanie mavena

alias mvn='mvn -U -s settings.xml'

Ale to nie zadziała w momencie gdy pliku nie ma w danym katalogu. Można oczywiście alias rozwijać do mikroskryptu z IFem na pokładzie, ale to jest mega niewygodne.

Po drugie możemy stworzyć funkcję, która będzie miała identyczną nazwę i będzie wywoływała właściwy program.
Po trzecie możemy stworzyć skrypt, który będzie tak jak wymieniona wyżej funkcja.
Zarówno skrypt jak i funkcja będą realizować wzorzec shim. Co wybierzemy, to zależy, poniekąd, od naszych preferencji. Ja preferuję skrypt, ponieważ ma on być samodzielnym programem i nie jest przeznaczony do używania w innych programach/skryptach.

Listing 1. Shim mavena

#!/bin/bash

args="$@"
added=0

while [ $# -gt 0 ]
do
        case "$1" in
                (-s) added=1; break;;
                (*);;
        esac
        shift
done

if [[ $added == 0 ]]  # nie dodano argumentu ręcznie
then
        if [[ -f "settings.xml" ]]
        then
                exec /home/koziolek/.sdkman/candidates/maven/current/bin/mvn $args -s settings.xml
        else
                exec /home/koziolek/.sdkman/candidates/maven/current/bin/mvn $args
        fi
else
        exec /home/koziolek/.sdkman/candidates/maven/current/bin/mvn $args
fi

Jak widać mam tutaj dwie „sekcje”. Pierwsza sprawdza, czy już nie przekazano pliku z konfiguracją. Druga uruchamia właściwego mavena. Teraz trzeba TYLKO dodać to do zmiennej $PATH.

Listing 3. Konfiguracja $PATH

export PATH=$HOME/.bin/:$PATH

Ścieżkę do naszego skryptu dodałem na początku zmiennej $PATH, ponieważ ta jest ewaluowana od lewej do prawej i uruchamiany jest pierwszy pasujący program. Należy o tym pamiętać, ponieważ w przeciwnym wypadku, gdy dodamy skrypt na końcu, pierwszym „trafieniem” będzie maven z SDKmana i nasz skrypt nie zostanie uruchomiony.

Dwie lekcje do zapamiętania:

  • shim – czyli wzorzec projektowy, służący do „maskowania niedociągnięć” kodu.
  • $PATH – i kolejność ewaluacji.

Koniec 😀

Skryptologia – czy korzeń płonie?

Będzie trochę krótkich wpisów „tips & tricks” w skryptach powłoki. Nie są one jakieś pr0 czy coś, ale chcę stworzyć sobie taki mały prywatno-publiczny zasób różnych sztuczek.

Na pierwszy ogień, skrypty, które powinny być uruchamiane z sudo.

Całe zamieszanie polega na tym, że nie wszystkie skrypty powinny być uruchamiane z sudo, a z drugiej strony pamiętanie co powinno być uruchamiane, jest do mało efektywne. W dodatku czasami mamy już tego root-a przyznanego i każdorazowe wpisywanie sudo polecenie jest upierdliwe. Jak to ugryźć.

Po pierwsze należy przypomnieć sobie, że root, podobnie jak inne nazwy użytkowników, to tylko aliasy przyjazne dla interfejsów białkowych. System operuje identyfikatorami numerycznymi, a nasz korzeń ma identyfikator równy 0. Zatem uruchamiając skrypt, możemy sprawdzić, czy w ogóle uruchamiamy go jako root:

Listing 1. Proste sprawdzenie, czy jesteśmy root-em

#!/bin/bash

if (( $EUID != 0 )); then
    echo "Spierdalaj…"
    return 1
fi

Takie coś nie rozwiązuje jednak naszego problemu. Co prawda przypomina nam, że warto wklepać te dodatkowe znaczki na początku, ale nie jest to rozwiązanie globalne. Wklepywanie tego kawałka w każdym skrypcie, który tego wymaga, jest mało zabawne.

Zamknijmy zatem ten kawałek w funkcji, którą umieścimy w skrypcie startowym powłoki (zazwyczaj .bashrc, .zshrc, lub co tam w ENV macie).

Listing 2. Konar zapłonięty, w funkcji zamknięty


function make_me_sudo () {
  export SUDO=''
  if (( $EUID != 0 )); then
     SUDO='sudo'
  fi
  return 0
}

# funkcja będzie dostępna globalnie
export -f make_me_sudo

Jest prawie dobrze, bo po wywołaniu tej funkcji mamy dostęp do sudo, ale czasami warto go ograniczyć. Mówiąc inaczej, chcemy posprzątać. W tym celu potrzebujemy dodatkowej funkcji, która nam to ogarnie:

Listing 3. Funkcja właściwa i funkcja sprzątająca

function make_me_sudo () {
  export SUDO=''
  export RESET_SUDO=0
  if (( $EUID != 0 )); then
     SUDO='sudo'
     RESET_SUDO=1
  fi
  return 0
}

function unmake_me_sudo () {
  if (( $RESET_SUDO !=0 )); then
      $SUDO -K
  fi
  unset SUDO
  unset RESET_SUDO
}

# funkcja będzie dostępna globalnie
export -f make_me_sudo
export -f unmake_me_sudo

I teraz mały myk. Zmienna RESET_SUDO, przechowuje informacje o stanie początkowym. Jeżeli uruchamiając skrypt, byliśmy już root-em, wcześniej wywołaliśmy sudo, to nie zmieniamy tego stanu. Bez takiego sprawdzenia każdorazowe uruchomienie skryptu kończyłoby się zmianą stanu „świata”.

Przykład użycia poniżej

Listing 4. Przykład użycia – aktualizacja modułu jądra

#!/bin/bash

make_me_sudo

$SUDO depmod -ae
$SUDO modprobe v4l2loopback_dc

unmake_me_sudo

Tyle.

TDD vs BDD

Tak trochę w nawiązaniu do ostatniego wpisu Michała Bartyzela na temat świętej trójcy Szybko-Dobrze-Tanio. I tego co w czasie tegorocznej Confitury padło w czasie bodajże pierwszego keynota, że:

Soft tworzymy przede wszystkim dla biznesu

czy jakoś tak (sorry nie pamiętam dokładnie, będzie nagranie, to poprawię).

Otóż to tak do końca nie działa. Moim zdaniem, można wyróżnić dwie osie przy tworzeniu projektów. Wszystko to, co robimy, będzie gdzieś na płaszczyźnie przez nie wyznaczonej.

TDD – Technology Driven Development

Jak sama nazwa wskazuje na tej osi „mierzymy”, jak bardzo potrzeby techniczne są ważne w projekcie. To tutaj będą siedziały wszystkie problemy związane z pisaniem testów, infrastrukturą CI/CD, ogólną jakością kodu (cokolwiek to znaczy), czy wykorzystywanymi rozwiązaniami.

Projekty, które bardzo mocno stawiają na zagadnienia technologiczne, najprawdopodobniej będą miały bardzo wysoką kulturę inżynierską. Dobrze zdefiniowane, poukładane i przemyślane procesy związane z produkcją i wdrażaniem kolejnych wersji. Do tego worka trafią też wszystkie projekty, które wymagają wysokich standardów out-of-box, bo np. muszą spełniać bardzo wyśrubowane normy. Mogą to być projekty bardzo proste, gdzie łatwo zadbać o jakość, ale mogą to też być bardzo skomplikowane i złożone systemy.

Na drugim końcu tej osi są projekty, w których nie tyle nie dba się o technologię, ile nie jest ona aż tak istotna. To tutaj wylądują wszystkie projekty, które ograniczają się do zakupu i instalacji gotowego rozwiązania. W tym przypadku jakość techniczna jest rzeczą wtórną. Zazwyczaj jest oddelegowana do dostawcy.

Gdzieś pomiędzy leży „ciemna strefa za dwunastnicą”, czyli typowe korpo-projekty. Niekoniecznie są złe, ale technologia nie jest w nich najważniejsza. To oznacza, że będą tam używane narzędzia, które co prawda są nieodpowiednie, ale pasują do korporacyjnego standardu. Może tam nie być dobrze zdefiniowanych procesów (ręczne wrzuty na PROD), albo brakuje odpowiedniego dbania o jakość.

BDD – Business Driven Development

Na drugiej osi znajduje się „wartość biznesowa”, a tak naprawdę to ile „biznes” ma do gadania. Przy czym biznes, to sfera finansowa/ekonomiczna projektu, a nie „domena biznesowa”.

Projekty mocno stawiające na biznes będą chciały sprzedawać. Tutaj liczą się przewaga konkurencyjna i dotarcie do klienta. Znajdziemy tutaj zarówno startupy, jak i „pewne inwestycje”. Celem jest zarobienie (normalne przedsiębiorstwa) lub eliminacja konkurencji (startupy).

Drugi koniec osi okupują badania podstawowe i projekty społeczne. Takie projekty z natury nie mają zarabiać. W przypadku niektórych z nich jest szansa, że wykluje się z nich jakiś produkt. To przede wszystkim projekty badawcze. Niestety w ostatnich dziesięcioleciach system grantów zmienia podejście do badań podstawowych. W efekcie największe wsparcie dostają projekty mające szansę na zarobek, a nie na odkrycie czegoś sensownego. Co z projektami, które nie mają szans na zarobek? To projekty społeczne. Ich cele są poza finansowe i trudno mierzalne. Jednak są albo konieczne z punktu widzenia prawidłowego funkcjonowania i rozwoju społeczeństwa, albo z powodów etycznych.

Pomiędzy nimi będą wszystkie te „małe i średnie, ale stabilne biznesy”, które oferują usługi ograniczone geograficznie (lokalne jedzenie), kulturowo (wydłużanie szyi) czy skierowane do specyficznego odbiorcy (np. kolekcjonerzy Lamborghini). Oczywiście na swoim lokalnym rynku mogą one konkurować, tak jak firmy z pierwszej grupy, ale zazwyczaj lokalny rynek w przypadku takich usług jest stabilny.

Szybko, Tanio, Dobrze

A jak ma się to do św. Trójcy Projektowej? To zależy. Odpowiednio ustawiając swój projekt w obu tych osiach, można spróbować oszacować, w jakim stopniu będziemy mieli Szybko, Tanio, Dobrze.

Projekty czysto biznesowe, o niskim poziomie technologii, będą mocno ciągnęły w kierunku „Szybko”. To czy będzie „Tanio” i „Dobrze”, zależy od tego jak bardzo zaangażujemy się w dobór technologii.

Projekty czysto techniczne, będą kierowały się ku „Dobrze”. A ich „Tanio” zależy od tego czy są to badania, które mają szansę na zwrot, czy też projekty społeczne, które nie dają szansy na zarobek. „Szybko” jest uzależnione od celu. W przypadku projektów społecznych zazwyczaj są jakieś deadline’y.

Projekty silnie biznesowe i techniczne zarazem będą na pewno drogie i powolne. Sama ich specyfika powoduje, że „Szybko” skończy się katastrofą (patrz katastrofy Boeingów 737 MAX).

Z drugiej strony projekty mało biznesowe i mało techniczne, mogą być bardzo szybkie w realizacji. Ich koszt i jakość będzie uzależniona od ich specyfiki, ale mówimy to o projektach w stylu kanał komunikacyjny do umawiania się na piwo z sąsiadami.

Tyle.

Docker jest jak koza

Zeżre każdą ilość dysku, jaką będziemy mieli.

Wpis dedykuję programistom, którzy nie muszą na co dzień ogarniać dockera i problemów bliższych administracji, a czasami walą głową w mur.

Problemacik

Taki mały, maleńki, maciupci można by powiedzieć. Przy próbie wystartowania kontenera ten się zbiesił i stwierdził, że nie ma zamiaru startować, ponieważ nie ma miejsca na dysku. W tym momencie miałem takie małe „kurwa, ale jak to nie ma miejsca”. W dużym uproszczeniu rozbiłem się o klasyczny błąd związany z instalacją dowolnego Linuxa.

Mając do dyspozycji dysk o pojemności 1TB warto podzielić go na mniejsze partycje. Wszystkie tutoriale koncentrują się jednak na podziale pod standardowego użytkownika. W efekcie mamy wydzieloną partycje dla /boot, /, /boot/efi, /var, /home i swap. Wszystkie poza /home są stosunkowo małe. Od 1GB dla partycji /boot i /boot/efi, po 25GB dla / i /var. Ta ostatnia partycja jest kluczowa dla działania dockera, ale o tym za chwilę. Przestrzeń wymiany jest w proporcji 1:1 do RAMu. Katalogi domowe zajmują całą resztę.

Taki układ generalnie działa. Nie będzie się psuł i wynika z doświadczenia pokoleń administratorów. Problemem jest jednak docker.

Źródło

W domyślnej konfiguracji Docker przechowuje wszystkie swoje dane, tzn. obrazy, kontenery, wolumeny itd. w katalogu /var/lib/docker.. Trafia tam wszystko, więc w naturalną koleją rzeczy jest, że katalog ten będzie puchł. Z czasem zbierają się w nim stare obrazy i nieużywane wolumeny. Koniec końców przy 25GB przestrzeni na dysku osiągniemy koniec. Co z tym fantem zrobić?

Quickfix – który nie działa

Po pierwsze dbać o higienę. Regularnie usuwać nieużywane kontenery, obrazy, wolumeny i sieci. Proste polecenie, które pozwala na sprzątniecie wszystkiego:

$ docker volume prune;  docker system prune; docker image prune; docker container prune; docker network prune
WARNING! This will remove all local volumes not used by at least one container.
Are you sure you want to continue? [y/N] y
Total reclaimed space: 0B
WARNING! This will remove:
  - all stopped containers
  - all networks not used by at least one container
  - all dangling images
  - all dangling build cache

Are you sure you want to continue? [y/N] y
Total reclaimed space: 0B
WARNING! This will remove all dangling images.
Are you sure you want to continue? [y/N] y
Total reclaimed space: 0B
WARNING! This will remove all stopped containers.
Are you sure you want to continue? [y/N] y
Total reclaimed space: 0B
WARNING! This will remove all custom networks not used by at least one container.
Are you sure you want to continue? [y/N] y

Tylko że jest jeden mały problem. Nie odzyskaliśmy miejsca. Oczywiście przy odrobinie szczęścia uda Ci się odzyskać 1-2GB. Jednak to jest quickfix, który rzadko działa, a w dodatku, o ile zadziała, to niewiele daje. Jeszcze gorzej będzie jeżeli symbolicznie zalinkujesz wyżej wymieniony folder do miejsca, w którym jest dużo miejsca. Tu mogą zacząć się dziać naprawdę dziwne rzeczy.

Podejście prawidłowe

Prawidłowe podejście do rozwiązania tego problemu jest trochę bardziej złożone. Po pierwsze zatrzymaj i usuń wszystkie kontenery. Nie ma co płakać nad danymi. Co najwyżej potrenujesz przywracanie backupów. Następnie utwórz plik /etc/docker/daemon.json, w którym umieścisz następujący wpis:

{
   "data-root": "DOCELOWA ŚCIEŻKA"
}

Zrestartuj dockera:

$ sudo systemctl restart docker.socket 
$ sudo systemctl restart docker

I teraz ważna uwaga. Usługa docker.socket zarządza całym tym dockerowym burdelem i to właśnie na niej operujemy w pierwszej kolejności. Inaczej zaczną się pojawiał jakieś głupie błędy związane z zależnościami pomiędzy usługami.

Podsumowanie

Szukam jakiegoś dobrego dysku na m.2 PCIE co najmniej 6TB pojemności. Potrzebuję dwie sztuki.

Confitura 2022 – jak mi tego brakowało

Tytuł mówi wszystko 😉

Ostatni raz byłem na Confiturze w 2018 roku. I wtedy też trochę pomarudziłem na miejsce i warunki. W 2019 nie mogłem dotrzeć, a potem była pandemia…

Miejsce

Hale EXPO XXI na Prądzyńskiego. Od 2018 dużo się zmieniło. Remont linii kolejowej zakończył się już dawno temu, jest kładka od strony al. Prymasa Tysiąclecia i całkiem wygodnie można dojść od strony Dworca Zachodniego. Poszczególne ścieżki dostały swoje sale, które nie zagłuszały się nawzajem. Widziałem też odsłuchy na scenie więc chyba na plus.
Spoina tradycyjnie była w kręgielni 🙂

Prelekcje

Nie będę się rozpisywał co i jak w szczegółach, ale tak sobie dobrałem kolejność, że było samo mięsko. Jeżeli miałbym wskazać jakie elementy poszczególnych prezentacji dały mi do myślenia, i za co je cenię:

  • Maciej Próchniak – No dobra, czas zsynchronizować zegarki… – za naprawdę głębokie wejście w problem pomiaru czasu w Linuxie.
  • Jarosław Pałka – Bare metal Java – za humor i skondensowaną dawkę wiedzy. Prezentacje Jarka zawsze są za krótkie.
  • Tomasz Nurkiewicz – Loom: rewolucja czy szczegół implementacyjny? – w końcu rozumiem, na czym polega Loom.
  • Grzegorz Piwowarek – Trzymaj Springa na dystans! – za sensowne pokazanie jak należy pracować z frameworkami.

Resztę prezentacji obejrzę online, jak już się pojawią.

Ludzie

Na konferencje jeździ się dla ludzi (i gadżetów). Paradoksalnie to nie prezentacje, ale możliwość podyskutowania, nawiązania kontaktów, czy zwykłego „pogadania o dupie Maryni” jest ważniejsza. Skrzętnie wykorzystałem nadarzającą się okazję 🙂 Poza tym, zauważyłem, że Confitura dzięki swojemu modelowi biznesowemu (bilety – cegiełki) przyciąga bardzo dużo, bardzo młodych ludzi. Krótka pogadanka z parą tegorocznych maturzystów była ciekawym doświadczeniem.
Bardzo się cieszę, że mogłem spotkać się osobiście z przyjaciółmi z naszej JVMowej sterty 🙂

Na zakończenie

Z różnych powodów nie mogłem wystartować w C4P, ale obiecuję, że wrócę na scenę i mam nadzieję, że za rok znowu się spotkamy w komplecie, na kolejnej Confiturze.

Najszybsza metoda naprawy uszkodzonej sieci w Ubuntu 20.04

Uwaga przedstawiona poniżej metoda nie jest uniwersalna, ale może być użyta jako „szybki hack” w przypadku niespodziewanego fakapu z VPNem.

Fakap – źródło problemu

Ogólna miłość korporacji do wykorzystania OSS jest znana. Oczywiście korpy biorą tyle, ile się da, a jak coś nie działa, to ścigają twórców, żeby naprawiali soft. Jest jednak pewna dziedzina, gdzie korporacje unikają OSS jak ognia – VPN. Zamiast po ludzku, użyć OpenVPN każde korpo kupuje jakiegoś VPNa. Oczywiście nie wykupuje wsparcia dla użytkowników, którzy korzystają z czegoś innego niż korpo-windows. Każdy taki soft ma własnego klienta, który działa po swojemu i po swojemu ingeruje w tabele routingu, opakowuje interfejsy sieciowe czy tworzy reguły firewalla.

W ogólności przepływ pracy jest taki sam dla każdego klienta. W momencie zalogowania dodawane są odpowiednie konfiguracje, które powinny zostać usunięte gdy wylogujemy się z VPNa. Problem pojawi się w momencie, gdy z jakiegoś powodu klient VPN nie zostanie prawidłowo zamknięty. W moim przypadku było to „szybki reset” po kolejnej awarii kamery (temat na osobny wpis). Po restarcie sieć po prostu nie się podniosła. Przy czym był to dość zwodniczy objaw.

Po pierwsze interfejs sieciowy działał i mogłem połączyć się z routerem. Mogłem też dobić się do drukarki sieciowej, a Factorio widziało serwer w LANie. Po drugie inni użytkownicy korzystający z tego routera widzieli mój komputer oraz mogli normalnie korzystać z internetu. Po trzecie, tethering przez USB działał normalnie i miałem dostęp do sieci. Zatem coś się było zjebało między routerem, kartą sieciową i konfiguracją interfejsu.

Naprawa

W pierwszym odruchu połączyłem się przez USB, odpaliłem VPNa, rozłączyłem i taka akcja powinna pomóc. Klient powinien wyczyścić swoją konfigurację i pozwolić mi na normalną pracę. Niestety nie 🙁 I tu mnie nic nie tknęło, że to jest duperela z profilem połączenia… pół nocy w dupe.

Reset tabeli routingu, restart managera sieciowego, wyczyszczenie /etc/hosts, wyczyszczenie reguł firewalla i ich ponowna instalacja (tylko ptorzebnych). Nic nie dało rady. Lekka załamka.

Oświecenie

Koniec końców poszedłem do /etc/NetworkManager/system-connections. W tym miejscu składowane są informacje o profilach połączeń. Zrobiłem sobie kopię, po czym wywaliłem profil połączenia kablowego i stworzyłem nowy, pod inną nazwą. Bangla!

Co się było zjebało

Jak wspomniałem w /etc/NetworkManager/system-connections przechowywane są profile połączeń. Co dokładnie można tam ustawić opisuje manual. W moim przypadku krytyczne okazały się dwa wpisy zmieniane przez klienta VPN. Pierwszy z nich to autoconnect-priority, który został ustawiony na -999. Dzięki temu połączenie VPN jest ważniejsze i w razie krótkotrwałej (sekundowej) awarii połączenia sieciowego nie nastąpi całkowite zerwanie połączenia VPN. Sieć po chwili wróci, a klient VPN zostanie wybrany jako domyślny kanał komunikacji. Drugim jest para dns, dns-search. Pierwszy do tablica IP serwerów DNS, drugi to nazwy domenowe. Klient VPN ustawił serwer DNS na IP w sieci wirtualnej (10.x.x.x) na którym „wisi” jego własny serwer DNS i własną listę domen. W efekcie po restarcie nie dość, że interfejs kablowy miał najniższy priorytet, to jeszcze nie mógł znaleźć żadnego DNSa.

To wyjaśnia, dlaczego mogłem połączyć się z routerem, bo łączę się po IP, a nie mogłem wyjść w świat. Po prostu miałem internet bez DNSa. Przy okazji można to naprawić w prostszy sposób, ręcznie ustawiając IP DNSów i usuwając.

Wpis ku pamięci, bo znając życie za pół roku znowu coś popusuję.

Terminalowe potyczki – by żyło się lepiej

Prawie rok nic nie napisałem. Po prostu nie mam na to siły, nie widzę sensu (blog nie przynosi korzyści i nie chodzi o zarabianie) i co gorsza, wszystkie pomysły wydają mi się takie banalne. Nie chodzi nawet o prostotę, ale o banalność właśnie. Rozwiązanie problemu możesz znaleźć w sieci w przeciągu kilku minut. Pisać po raz setny to samo? Not hacker way. Wtórność. Ale dziś mam pomysł, więc coś opiszę 🙂

Problematy

Problem trochę „licealny”, czyli porównywania wielkości swoich pokemonów. Chciałbym mieć spersonalizowany „ekran powitalny” terminala, ale tak, żeby było widać kilka podstawowych informacji o komputerze. Czasami nie potrzebuję nic ponad wersję kernela czy uptime. Samo odpalenie terminala niech da mi te informacje.
Kolejnym elementem jest moje lenistwo. Chcę, żeby okno terminala startowało zmaksymalizowane (fullscreen jak po naciśnięciu F11).

Część pierwsza

Rozwiązaniem pierwszej części zadania jest neofetch. Ta prosta biblioteka, którą można zainstalować za pomocą zwykłego

apt get install neofetch

Wyświetli mi wszystkie podstawowe informacje. W praktyce wystarczy zatem gdzieś pod koniec .bashrc dodać wywołanie neofetch i gotowe.

Część druga

Paradoksalnie znacznie bardziej „cwana”. Okazuje się, że w Ubuntu 20.04 można skonfigurować skrót do uruchamiania terminala, ale nie można skonfigurować polecenia. To znaczy, można dodać własne polecenie uruchomienia terminala i wyłączyć to domyślne. Jednak bez grzebania w głębokiej konfiguracji nie ma możliwości przedefiniowania tego polecenia. Można też pokombinować z konfiguracją samego terminala i ustawić odpowiedni rozmiar w kolumnach i wierszach… ale nie chcecie tego robić. No ale mamy .bashrc.

W pierwszej kolejności musimy zainstalować sobie xdotool. Jest to takie zabawne narzędzie, które pozwala na emulowanie naciśnięcia przycisków klawiatury z poziomu linii poleceń. Tutaj rozwiązanie wydaje się proste:

xdotool key F11

Umieszczone pod koniec .bashrc powinno rozwiązać problem…

I tu wchodzi tmux

Jako że używam tmuxa, to .bashrc będzie uruchamiany za każdym odpaleniem nowego tmuxowego okna (myśl o nim jak o zakładce w przeglądarce). Zatem za każdym razem będzie naciśnięty klawisz F11 i okno przeskoczy do/z pełnego ekranu. Rozwiązanie tego problemu jest trochę „prymitywne”:

CURRENT_SIZE=$(echo $(xwininfo -id $(xdotool getactivewindow) -stats | egrep '(Width|Height):' | awk '{print $NF}') | sed -e 's/ /x/')
MAX_SIZE="1920x1080"

if [ "$CURRENT_SIZE" != "$MAX_SIZE" ]; then
   xdotool key F11
fi 

Definiujemy dwie zmienne. Pierwsza, CURRENT_SIZE, jest obliczona na podstawie wielkości aktualnego okna. Tak, święta trójca egrep, awk, sed znowu robi robotę. Druga, MAX_SIZE, określa maksymalny rozmiar okna. Jeżeli nie są sobie równe, to naciskamy F11.

Co można jeszcze?

Można oczywiście trochę się pobawić. Na przykład instalując ascii-image-converter można wygenerować ascii art z obrazka i podmienić logo neofetch.

Kolejnym rozszerzeniem jest zmiana zahardkodowanej, maksymalnej rozdzielczości okna w drugiej części problemu na:

MAX_SIZE=$(xrandr | egrep \\* | awk '{print $1}' | head -n 1)

Generalnie powinno to „robić robotę”, bo jedynym przypadkiem gdy to nie będzie działać, jest ten, gdy:

  • Mamy 2+ monitorów o różnych rozdzielczościach;
  • Okno terminala otworzyło się w domyślnym rozmiarze równym rozdzielczości jednego z monitorów;
  • Monitor ten jest pierwszym na liście xrandr.

Zapobiec temu można konfigurując rozmiar okna terminala w ustawieniach na 10×10 (kolumny/wiersze) czy coś podobnie niedorzecznego.

Multihooki w git

W poprzednim wpisie poruszyłem problem konfigurowania hooków w gitcie. Dzisiaj przyjrzymy się jak zapanować nad hookami, w których chcemy wykonać wiele zadań. Będzie trochę basha, będzie trochę „magii” i w końcu będzie trochę porad jak sobie ułatwić życie.

Hooki – podstawy

Git, podobnie jak wiele innych narzędzi, pozwala na uruchomienie dodatkowych skryptów lub programów w wyniku zaistnienia pewnych zdarzeń. Jest to model reaktywny, to znaczy, że w systemie są zdefiniowane „punkty obserwacyjne”. Po osiągnięciu takiego punktu uruchamiane są wszystkie skrypty „obserwujące” dany punkt.

Jakie to są punkty?

W gitcie jest wiele punktów zaczepienia dla hooków, ale można je podzielić na te po stronie klienta i te po stronie serwera (repozytorium). Te po stronie serwera służą przede wszystkim do weryfikacji poprawności samych commitów. Czy nie ma gdzieś nadużywanego fast-forward, czy nie commitujemy do „chronionej” gałęzi (czyli takiej, do które powinny być dopisywane tylko MR/PR), czy w końcu do rozgłaszania zmian w repozytorium. Ten ostatni hook jest często wykorzystywany, do powiadamiania systemów CI o zmianach. Podsumowując po stronie serwera mamy:

  • pre-recive – odpalany raz gdy przychodzi kod (robisz git push).
  • update – odpalany raz dla każdej gałęzi zmienianej w przychodzącym kodzie.
  • post-recive – odpalany raz po zakończeniu procesowania zmian.

Co i jak można tutaj wykorzystać, to temat na osobny artykuł.

Po stronie klienta jest jednak znacznie więcej opcji. Przede wszystkim dlatego, że po stronie klienta dzieje się znacznie więcej rzeczy. Commitujemy, łączymy gałęzie, rebaseujemy, wypychamy, ciągniemy… a do tego niektóre z tych operacji są wieloetapowe. Dlatego też mamy tutaj większe pole do popisu. Oznacza to też, że wraz z rozwojem naszej bazy hooków zaczną pojawiać się pewne problemy. Zanim jednak o nich opowiem i pokażę, jak je rozwiązać zobaczmy, w jakiej kolejności wykonywane są skrypty. Po prostu w katalogu .git/hooks (lub tam, gdzie wskazuje core.hooksPath) umieśćmy pliki takie jak:

  • applypatch-msg
  • commit-msg
  • fsmonitor-watchman
  • post-checkout
  • post-commit
  • post-merge
  • post-update
  • post-receive
  • pre-applypatch
  • pre-commit
  • prepare-commit-msg
  • pre-push
  • pre-rebase
  • pre-receive
  • update

A w każdym z nich umieśćmy poniższy kod:

Listing 1. gdzie ja jestem

#!/usr/bin/env bash

hook_type=${BASH_SOURCE##*/}

echo "Hello from $hook_type"
echo Params
echo $@
echo -------------

Następnie wystarczy coś zmienić w repozytorium i scommitować zmiany. Naszym oczom ukaże się coś w stylu:

Listing 2. Kolejność hooków przy commicie

$ git commit -am "chore: Some example changes"
pre-commit
Params

-------------
prepare-commit-msg
Params
.git/COMMIT_EDITMSG message
-------------
commit-msg
Params
.git/COMMIT_EDITMSG
-------------
Commit message meets Conventional Commit standards...
post-commit
Params

-------------
[master a1cc644] chore: Some example changes
 1 file changed, 1 insertion(+), 1 deletion(-)

Następnie wypchnijmy te zmiany do repozytorium:

Listing 3. Kolejność hooków przy push

$ git push
pre-push
Params
origin git@github.com:Koziolek/multihooks.git
-------------
Enumerating objects: 23, done.
Counting objects: 100% (23/23), done.
Delta compression using up to 8 threads
Compressing objects: 100% (15/15), done.
Writing objects: 100% (20/20), 2.24 KiB | 327.00 KiB/s, done.
Total 20 (delta 7), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (7/7), completed with 2 local objects.
To github.com:Koziolek/multihooks.git
   1452343..4bbecdc  master -> master

Jak widać w normalnej codziennej pracy tylko niektóre z hooków będą nam potrzebne. Oczywiście nie oznacza to, że inne nie będą nam potrzebne. W pewnych sytuacjach na pewno. Jednak nie o tym chciałem dzisiaj mówić. Skupmy się na najpopularniejszym, jak sądzę, hooku, czyli commit-msg.

Multihooki

Jak wspomniałem na początku hooki działają w modelu reaktywnym. Przy czym jest on ograniczony. Dla jednego rodzaju hooków możemy mieć tylko jeden skrypt. Co to oznacza w praktyce? Chcą wykonać wiele operacji w reakcji na jedno zdarzenie, musimy umieścić wszystko w jednym pliku. Oczywiście istnieją funkcje, można użyć polecenia source, by zmodularyzować nasz skrypt, ale chyba nie o to chodzi.

Typową i powszechnie stosowaną techniką w systemach *-xiowych jest umieszczenie wszystkich skryptów, które chcemy wykonać, w katalogu. Nazwa katalogu odpowiada „głównej” nazwie skryptu zakończonej na .d. Tak samo też możemy podejść do naszego problemu. Poniższy skrypt jest rozwiązaniem problemu:

Listing 4. Multihook – uruchamia wszystkie pliki w katalogu NAZWA_HOOKA.d

#!/usr/bin/env bash

# Allow multiple hooks.
#
# To use it copy file to ./git/hooks directory or to directory pointed in git config core.hooksPath.
# Next create symbolic links from this file to hook files. This script should have execution rights.
# Put your scripts in <>.d folders in your hook directory. Scripts will be executed in
# alphabetical order.
#
# Original code https://gist.github.com/damienrg/411f63a5120206bb887929f4830ad0d0
#

hook_type=${BASH_SOURCE##*/}
hook_dir="${BASH_SOURCE[0]}.d"

case "$hook_type" in
  applypatch-msg | \
  commit-msg | \
  fsmonitor-watchman | \
  post-checkout | \
  post-commit | \
  post-merge | \
  post-update | \
  post-receive | \
  pre-applypatch | \
  pre-commit | \
  prepare-commit-msg | \
  pre-push | \
  pre-rebase | \
  pre-receive | \
  update)
  IFS= read -rd '' stdin
  if [[ -d $hook_dir && "$(ls -I .gitkeep -A ${hook_dir})" ]]; then
      for file in "${hook_dir}"/*; do
        "./$file" "$@" <<<"$stdin" || exit 2
      done
  fi
  exit 0
  ;;
*)
  echo "unknown hook type: $hook_type"
  exit 2
  ;;
esac

Jest tu trochę magii. Niedużo 🙂 Najpierw w zmiennej hook_type zapisujemy nazwę hooka, a tak naprawdę jest to nazwa pliku lub linku, który został uruchomiony. Szczegóły notacji i co oznaczają magiczne znaczki na końcu znajdziecie w manualu lub tutaj. Następnie tworzymy sobie nazwę katalogu ze skryptami dla danego hooka.
Później jest switch, który technicznie rzecz biorąc, jest średnio potrzebny. Jednak ma sens w przypadku, gdyby pojawiły się nowe hooki, a my byśmy na pałę podpięli ten skrypt. Zabezpiecza on przed „przykrymi” niespodziankami w przyszłości. Teraz czas na mięsko. Przedefiniujemy sobie IFS na pusty ciąg znaków, by sensowniej obsługiwać wywołanie naszego skryptu. If sprawdza, czy mamy co wywołać. Przy czym jeżeli commitujemy puste katalogi (zawierające jedynie .gitkeep), to ten .gitkeep jest ignorowany. W pętli wywołujemy wszystkie skrypty (w kolejności alfabetycznej), przekazując im wejście, jakie dostał nasz skrypt. To wejście to parametry, z jakimi klient gita wywołał nasz hook. Dodatkowo przekazujemy im całe wejście, jakie dostaliśmy. Jak coś się nie powiedzie, to całość się wywali.

Naszym zadaniem teraz jest utworzenie odpowiednich katalogów i wrzucenie do nich skryptów. Jeden skrypt, to jedna niezależna funkcjonalność.

Multihooki - kolejność wywołania

Na koniec jeszcze jedna uwaga. Jeżeli chcemy zapewnić kolejność wywołania skryptów, to warto użyć konwencji nazewniczej dla skryptów na zasadzie 000-nazwa_skryptu.sh. Bash będzie wywoływać skrypty w kolejności alfabetycznej, a dzięki nazwom zaczynającym się od cyfr, mamy możliwość sterowania kolejnością. Nie warto jednak numerować skryptów po kolei. Co dziesięć jest ok, ponieważ mamy dzięki temu miejsce na przyszłe „wtrącenia”.

Podsumowanie

Skryptologia jest bardzo przydatna przy pracy z gitem. Sam git potrafi dość dużo, ale ma też swoje ograniczenia. Wynikają one z jego natury. Skrypty pozwalają na eleganckie obudowanie skomplikowanych operacji. Hooki stanowią znowuż pierwszą linię obrony w przypadku prostych błędów, które mogą pojawić się w codziennej pracy.

Prawdziwy greenfield – Conventional Commits i jak zmusić do używania standardów

Mam masę rozgrzebanych tematów. Ego się pisze, ale brakuje mi trochę czasu i trochę więcej motywacji. Gdzieś tam jest jeszcze temat drzewek Merkle. Jeszcze książka się pisze…

Po prostu czasami nie da się wszystkiego naraz. Dlatego dzisiaj temat trochę z pogranicza programowania i pożycia projektowego.

Czym jest greenfield?

Jeżeli ktoś w naszej branżuni mówi, że „dostał mu się greenfield”, to oznacza jedno. Ma to szczęście, że zaczyna z pustym repozytorium. Zespół zaczyna od zera. Może swobodnie kształtować architekturę, dobierać narzędzia i tworzyć kod według Najlepszych Praktyk Rynkowych™. Niby idealnie, ale nie do końca. Jeżeli projekt jest tworzony na potrzeby klienta, który nie ma doświadczenia ani zaplecza, to rzeczywiście mamy dużą swobodę. Jedynym ogranicznikiem jest tak naprawdę budżet klienta i nasz profesjonalizm. Profesjonalizm rozumiem tutaj jako pewną etykę pracy, czyli niewciskanie klientowi rozwiązań, których nie potrzebuje.

A co w przypadku gdy greenfield jest realizowany u dojrzałego klienta? Wtedy pojawia się więcej ograniczeń. Ma on wypracowane m.in. standardy kodu, architekturę i procedury. Do tego dodać należy całą masę drobnostek jak np. nazewnictwo gałęzi w repozytorium albo szablon wiadomości wypychanego kodu. W tym przypadku nie mamy do czynienia z greenfieldem. Raczej jest to taki brownfield. Niby robimy coś nowego, ale nie do końca. Przed nami nie ma łąki, a ugór.

// offtopic

Tomek Nurkiewicz ma swoje repozytorium Polski w IT, w którym zbiera odpowiedniki angielskojęzycznych nazw dla różnych bytów. Brakuje tam słówka commit. Rzecz w tym, że w języku angielskim commit jest czasownikiem, a w polskojęzycznym IT mówiąc commit, myślimy o rzeczowniku. Dlatego jest to nie do przetłumaczenia.

// offtopic

Jest jednak jeszcze trzecia możliwość. Klient może chcieć zdefiniować od nowa swoje zasady. Ma przestarzałą infrastrukturę, istniejące rozwiązania osiągnęły kres możliwości technologicznych, a organizacja rozrosła się i musi szybko znaleźć nową drogę. W tym przypadku możemy mówić o „prawdziwym” greenfieldzie. Organizacja jest na tyle duża i zmotywowana (lub zdesperowana), że praktycznie znika ograniczenie budżetu. Oczywiście nie oznacza to, że możemy robić wszystko, ale mamy naprawdę duże pole do popisu.

Po raz drugi robię tego typu projekt. I dzisiaj będzie o „otoczce”.

Co każdy projekt mieć powinien, a niewiele ma?

No właśnie… Projekt jest co do zasady zadaniem zespołowym. Zespół tworzą nie tylko osoby techniczne, ale też tzw. „biznesowi”, którzy poza głównym celem, jakim jest realizacja projektu, muszą też „zaliczać” kolejne cele cząstkowe. Te cząstkowe cele, to oddawanie kolejnych elementów aplikacji lub systemu. Sama praca programistyczna w tym miejscu jest drugoplanowa, ponieważ liczą się artefakty. Te znowuż najłatwiej jest opisać metrykami. Ergo, projekt powinien mieć pewne metryki.

Metryki mogą być różne, począwszy od prymitywnego pokrycia kodu testami, poprzez złożoność cyklomatyczną, a np. na regresji kończąc. Trochę tego jest. Jednak wszystkie te metryki można stosunkowo łatwo zebrać i na upartego łatwo też je spełnić. Nawet na chama.

Poza metrykami istnieje też pewna klasa wymagań, które są trudniejsze w opisaniu. Istnieją pewne formalizmy, których zadaniem jest zapewnienie, że projekt będzie w prawidłowy sposób funkcjonował w ramach organizacji. Bełkot? A co powiesz np. na schemat nazywania commitów, by można było z nich generować listę zmian? Albo sławetny „JĘZYK WSZECHOBECNY”, którego zadaniem jest zapewnienie, że wszyscy zaangażowani w projekt mają taką samą definicję różnych bytów?

Conventional Commits

Zajmę się jednym, dla mnie obecnie najciekawszym w tym momencie zagadnieniem. Jako osoba odpowiedzialna za zespół (do czego to doszło) chcę mieć sformalizowany dziennik zmian. Po każdym sprincie chcę mieć możliwość wygenerowania dokumentu, który będzie można we w miarę tani sposób porównać z wymaganiami, specyfikacją, backlogiem, czy zamówieniem klienta i odhaczyć zakończone zadania. Najłatwiej jest wziąć historię zmian w kodzie i na tej podstawie wygenerować odpowiedni dokument. Jednak taki generator musiałby być albo bardzo cwany, by zrozumieć wiadomości i jakoś je obrobić, albo należy wypracować jakiś standard wiadomości. Co do zasady reguły pracy powinien uzgadniać zespół, ale znacznie łatwiej jest posłużyć się pewną sztuczką.

Gdy dochodzi do dyskusji na temat jakiegoś standardu pracy, to chcemy szybko ją zakończyć (bo zazwyczaj są to dyskusje jałowe jak sędzina Pawłowicz). Jak to zrobić? Dać zespołowi wybór mamy standard A lub B, lub siedzimy i opracowujemy własny. Ostatnia opcja choć kusząca, bo pozwala na prokrastynacjęW, to wymaga przygotowania narzędzi. Jakieś integracje, jakieś klikanie w CI, GitLabie czy innej JIRZe. Gotowe rozwiązania zazwyczaj mają gotowe narzędzia i integracje. Jednym takich gotowców jest Conventional Commits. Obok wersjonowania semantycznego jest to rozwiązanie dające najwięcej „standardowego kopa” w projekcie.

CC – kilka słów o regułach

Ogólna zasada jest taka, że wiadomość przy wypychaniu zmian wpisuje się w szablon:

Listing 1. Szablon wiadomości

(Typ)[zakres][!]: (Tytuł)

[Długi opis]

[Stopka]

Elementy w [] są opcjonalne, w () obowiązkowe. Ponadto Typ jest zestandaryzowany i jest zawężony do kilkunastu wartości takich jak feat, fix, chore, test, itp. Zakres odpowiada za uszczegółowienie obszaru, którego dotyczy zmiana. Może to być na przykład security, api, front, konfiguracja, itp. Przydatne jeżeli projekt ma naturę monolityczną lub jeden zespół utrzymuje wiele modułów w ramach jednego repozytorium. Następnie mamy Tytuł, czyli krótki opis zmian. Po nim następuje opcjonalny dłuższy opis, który może być wymagany w pewnych sytuacjach. Przykładowo możemy tam zamieścić linki do dokumentacji, czy też niektóre organizacje wymagają jakiegoś dłuższego opisu w konkretnym formacie. Na koniec mamy stopkę, która spełnia dwa zadania. Jeżeli zaczyna się od frazy BREAKING CHANGE:, to mamy do czynienia z czymś co łamie kontrakt. Identyczną rolę pełni wykrzyknik, który można umieścić po Typie i Zakresie. Stopka jest też miejscem, gdzie można umieścić gitowe trailery. Ciągi te będą interpretowane w specyficzny sposób. Próbkę tego typu zachowania mamy na githubie, gdzie można zamykać zadnia umieszczając ich numer poprzedzony słówkiem close.

Jak widać, możemy wykorzystać istniejący standard, by znormalizować wiadomości. Ale jak wymusić jego użycie?

Kontrola podstawą zaufania

Powyższe słowa są przypisywane Leninowi, choć historycy nie są co do tego zgodni. Podobne słowa przypisuje się też Stalinowi i stąd wiele problemów, ze źródłem. Maksyma to choć wydaje się sprzeczna, to dość dobrze oddaje ludzką naturę. Ufamy ludziom, że będą zachowywać się odpowiedzialnie i nie chcą sobie ani bliźnim zrobić krzywdy, a jednocześnie staramy się projektować rzeczy w bezpieczny sposób. Ludzie mogą popełniać błędy, a naszym zadaniem jest tak projektować procesy, urządzenia czy interfejsy, by minimalizowały szansę popełnienia błędu, a w przypadku jego wystąpienia ograniczały negatywne skutki.
Pracując z gitem podstawowym narzędziem, które może posłużyć do zapewnienia nam bezpieczeństwa, są wyzwalacze – hooks.

CC – walidacja wiadomości

Poniższy kod pozwalana sprawdzenie wiadomości i w razie czego wyświetla odpowiednią informację:

Listing 2. hook sprawdzający wiadomość

#!/usr/bin/env bash

set -eou pipefail

# Create a regex for a conventional commit.
convetional_commit_regex="^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z \-]+\))?!?: .+$"

# Get the commit message (the parameter we're given is just the path to the
# temporary file which holds the message).
commit_message=$(cat "$1")

# Check the message, if we match, all good baby.
if [[ "$commit_message" =~ $convetional_commit_regex ]]; then
   echo -e "\e[32mCommit message meets Conventional Commit standards...\e[0m"
   exit 0
fi

# Uh-oh, this is not a conventional commit, show an example and link to the spec.
echo -e "\e[31mThe commit message does not meet the Conventional Commit standard\e[0m"
echo "An example of a valid message is: "
echo "  feat(login): add the 'remember me' button"
echo "More details at: https://www.conventionalcommits.org/en/v1.0.0/#summary"

exit 1

źródło

Jeżeli wiadomość nie pasuje do formatu, to całość zwróci błąd, a proces commitowania zostanie przerwany. No dobra, ale jak wymusić użycie tego rozwiązania?

Współdzielone hooki

Git przechowuje hooki w katalogu .git/hooks. Zatem nie są one współdzielone w zespole. Wystarczy jednak utworzyć katalog w projekcie i tam umieścić skrypty. Następnie katalog ten będziemy współdzielić jak inne pliki w projekcie. Ale to oczywiście nie wszystko. Kolejnym krokiem jest wskazanie gitowi nowego katalogu. W tym celu należ zmienić konfigurację w następujący sposób:

Listing 3. Nowa konfiguracja gita

git config core.hooksPath .hooks

W tym przypadku nasze skrypty znajdują się w katalogu .hooks. Ale czy to wszystko? Nie do końca. Jeżeli ktoś nie zmieni konfiguracji gita, to oczywiście nie będzie podlegać weryfikacji (pomijam włączenie weryfikacji na serwerze) i będzie nam bruździł w repo. Pozostaje wymusić taką konfigurację w inny sposób.

Wymuszenie konfiguracji

Opisana tutaj metoda jest „krzywa” tzn. opiera się o zewnętrzne narzędzia, w tym przypadku służące do budowania projektów. Na czym polega? Skonfigurujmy nasz projekt tak by każde uruchomienie mavena/mixa/npma/sbt czy czego tam używasz spowodowało wykonanie powyższego polecenia konfiguracyjnego gita. Przykładowo w mavenie będzie to wyglądać tak:

Listing 4. Dodatkowa konfiguracja mavena


  exec-maven-plugin
  org.codehaus.mojo
  
    
      Git setup
      generate-sources
      
        exec
      
      
        ${basedir}/.hooks/setup.sh
      
    
  

Przy czym skrypt setup.sh może ustawiać też inne elementy w projekcie. O tym kiedy indziej (na św. Nigdy pewno). Dodatkowo należy tę konfigurację zamknąć w dodatkowym profilu, który nie będzie uruchamiany np. przy tworzeniu kontenerów dockera.

Podsumowanie

Tworząc projekty greenfield zazwyczaj wpadamy w środowisko, w którym są już ustalone pewne zasady. Mamy narzucony stos technologiczny, jakiś szablon architektury i procedury. Niezykle rzadko możemy jednak trafić na projekt, który poza budową rozwiązania od zera, ma też za zadanie ustanowić (nowe) reguły dla całej organizacji. W tym momencie musimy podejmować decyzje o tzw. duperelach. Przy czym, jak zwykle, trudniejszym elementem w tej zabawie jest wymuszenie przestrzegania reguł. Można, a wręcz należy, zaufać ludziom. Jednocześnie warto wprowadzić elementy (automatycznej) kontroli. Ich zadaniem jednak nie jest jebanie po ludziach (dlatego mechanizmy te nie mogą zbierać metryk), ale ograniczenie potencjalnych strat.

Na dłuższą metę będziemy w stanie znacznie sprawniej zarządzać techniczna stroną projektu. Skupić się na ciekawszych aspektach niż klepanie raportów dla kierownictwa i klientów.

Testuj na produkcji, serio

Zeszły tydzień zakończyliśmy eleganckim fakapem ekipy mBanku, która to ekipa „testowała na produkcji” powiadomienia push aplikacji mobilnej. W dużym skrócie, dla tych, co czytają ten tekst jakiś czas później, o poranku klienci mBanku otrzymali trzy powiadomienia push, w tym takie o treści „ęśąćż”. Ogólnie mało ciekawe zdarzenie i ktoś zapewne oberwał po uszach. O możliwych przyczynach na koniec. Teraz o testowaniu na produkcji.

Zanim zaczniemy

Załóżmy na chwilę, że jesteś architektem. Jednak nie takim od stawiania kloców w UML-u, ale takim prawdziwym, który projektuje budynki. Nie jesteś jednak zwykłym architektem, od projektowania kostki mazowieckiej. Jesteś architektem-urbanistą, czyli poza budynkami projektujesz też ich grupy (systemy). Dostajesz zadanie zaprojektowania budynków, które będą spełniać pewne określone warunki. Ich dokładna treść nie jest istotna. Istotne jest jednak, że już na samym początku masz kilka różnych pomysłów. Każdy z nich będzie spełniał część warunków. W dodatku zbiór warunków, które spełniają wymyślone przez ciebie projekty, jest, mniej więcej, wspólny dla wszystkich pomysłów. Istnieje też pewna grupa warunków, których nie możesz przetestować na etapie projektowania. Co robisz?

Sytuacja wydaje się beznadziejna. Co prawda możesz odwołać się do swojego doświadczenia i spróbować intuicyjnie wybrać najlepsze rozwiązania. W przypadku małych elementów w projekcie, takich jak elementy mieszkania, możesz spróbować użyć wzorców projektowych AlexandraW. Czy jednak samo mieszkanie będzie dobrze zaprojektowane?

Na to pytanie próbowali odpowiedzieć Szwedzi z Instytut Badań DomowychW. Przeprowadzili oni badania nad zachowaniem gospodyń domowych w obrębie kuchni. Celem było zaprojektowanie kuchni funkcjonalnej. Do dziś wyniki tych badań są widoczne w projektach kuchni IKEA. Swoją drogą, badania te stały się też przedmiotem żartu w postaci filmu Historie kuchenneW, który to film gorąco polecam.

Można jednak podejść do problemu trochę inaczej. Pamiętaj, że mamy do opracowania NOWY projekt, a nie bazujemy na starych. Zrealizujmy więc większość naszych pomysłów na niewielką skalę. Zbadajmy, który spełnia wszystkie założenia, a następnie zrealizujmy go na większą skalę. Taki sposób myślenia przyświecał twórcom Osiedla Prototypów, na warszawskim Mokotowie. Więcej na temat samego osiedla tutaj. W dużym skrócie. W latach 60-tych wybudowano osiedle składające się kilkunastu budynków, które były budowane zgodnie z różnymi projektami. Były to zarówno pojedyncze budynki, jak i niewielkie zespoły urbanistyczne. Niektóre projekty różniły się szczegółami w rodzaju lustrzanego odbicia mieszkań albo innego ustawienia budynku w stosunku do stron świata.

Dzisiaj osiedle ma się chyba nieźle. Co prawda w latach 90-tych bardzo mocno dotknął je kryzys związany z transformacją, ale obecnie następuje już zmiana pokoleniowa i idzie ku lepszemu.

Testy na produkcji – oczywistości

Po przydługim wstępnie czas na mięsko. Po pierwsze testy na produkcji nie są czymś niezwykłym. Najpopularniejszym przykładem tego rodzaju działań są test A/B. Pozwalają one na porównanie różnych wersji elementów UI. Są stosunkowo tanie w porównaniu z testami laboratoryjnymi. Można też prowadzić je we w miarę bezpieczny sposób z punktu widzenia produktu. Nic więc dziwnego, że tego typu testy są często stosowane przy ocenie np. reklam. Przygotowujemy kilka wersji reklamy i patrzymy, która jest efektywna. Ba! Testy te, dla reklam, prowadzą nie tylko reklamodawcy, ale też (tak naprawdę, przede wszystkim) autorzy stron, gdzie reklamy są umieszczane. W końcu celem jest maksymalizacja kliknięć w reklamę. Jednak testy te nie są do końca tym, czym chcemy się tutaj zająć.

Drugim obszarem „na produkcji” są testy bliskie krzemu. Jak przetestować bootloader? Na emulatorze. Jednak koniec końców i tak trzeba przeprowadzić testy na fizycznym sprzęcie. Tak też dzieje się w przypadku ww. bootloadera. Ktoś odpala go na konkretnym sprzęcie i patrzy czy działa. Jest to czasochłonne i drogie, ale innej drogi nie ma. Zresztą dlatego tak ważna jest współpraca pomiędzy programistami kernela, a producentami sprzętu. Niektóre rzeczy można testować wcześniej. Oczywiście te testy są ograniczone. Tak naprawdę nie odbywają się „na produkcji”, a jedynie na środowisku takim jak produkcyjne. Czym to się różni? Fajny przykład podała Alicja Kubera na SegFaulcie:

Jak widzicie testy na kopii produkcji to jedno, a produkcja to drugie.

Część wspólna

Co łączy powyższe przykłady i przykład ze wstępu?

Łączy je trudność w wyborze rozwiązania, przy uwzględnieniu pewnych wymagań, których spełnienie nie może zostać zweryfikowane za pomocą testów w warunkach laboratoryjnych.

Czym są warunki laboratoryjne?

W przypadku testów jednostkowych mówimy, o ich powtarzalności, wzajemnej niezależności, niezależności wobec otoczenia, szybkości i możliwości automatyzacji. Dobre testy powinny ponadto być łatwe w implementacji, możliwe do uruchomienia przez każdego i niezmienne w stosunku do API (tylko zmiana API zmienia test). W przypadku testów wyższego poziomu, integracyjnych, e2e, rozluźniamy niektóre z powyższych warunków, ponieważ charakter testów tego rodzaju tego wymaga. Przykładowo trudno mówić o szybkich (trwających milisekundy) testach integracyjnych dla dużej liczby modułów.
Jednak wspomniane warunki laboratoryjne oznaczają, że testy można prowadzić w kontrolowanym i (w miarę) stabilnym środowisku. Pozwalają one na uzyskanie powtarzalnych wyników.

Mocki i inne zaślepki

Przy okazji warto wspomnieć o mockach i różnego rodzaju zaślepkach. Nie służą one do testowania naszego kodu. Ich zadaniem jest zapewnienie właśnie takiego stabilnego i w pewnym sensie sterylnego środowiska. Co prawda istnieje pewna niewielka nisza, gdzie weryfikacja stanu naszej zaślepki jest clou testu. Jednak w 99% przypadków „z życia” tak naprawdę testujemy bibliotekę, a nie nasz kod. Bardzo fajnie mówił kiedyś o tym Jose Valim, że funkcja powinna dostać wszystko, czego potrzebuje. Dzięki temu nie ma potrzeby korzystania z mocków.

Testy na produkcji – kiedy

Testy na produkcji prędzej czy później okazują się koniecznością. Co ciekawe mam pewną hipotezę na temat tego, kto wykonuje tego typu testy. Otóż są dwie grupy organizacji, które robią tego typu testy. Pierwsza grupa, to bardzo małe organizacje, które dostarczają produkty o niewielkim znaczeniu dla użytkownika końcowego. W takim przypadku taniej jest dostarczyć coś, co zostało przetestowane jednostkowo, a użytkownik końcowy przeklika sobie produkt i w razie czego zgłosi błąd. Szczególnie że błędy w takich produktach wynikają w dużej mierze z niedostatków w procesie projektowania. Dotyczą też rzeczy bliskich zagadnieniom, które obejmują testy A/B. Klasyką gatunku jest tu hasło „większe logo”. Czasami chodzi o inne umieszczenie przycisków, albo lekkie zmiany w procesie. Proces projektowania w takich produktach jest potraktowany po macoszemu nie ze względu na lenistwo czy niechęć do robienia porządnie, ale ze względu na oszczędność. Szkoda czasu na projektowanie hello worlda, choć i są fani tego typu zadań
Druga grupa to organizacje, które są po prostu gigantyczne. W ich przypadku nie opłaca się testować niektórych rzeczy, ponieważ ze względu na specyfikę klientów i tak nie pokryjemy wszystkich przypadków. W dodatku taniej wychodzi zrobienie dobrze niezadowolonemu klientowi na poziomie supportu niż przepalanie czasu testerów. Co ciekawe wielu błędów i tak nikt nigdy nie wykryje. Przy czym organizacje te mają dobrze zorganizowany proces wdrożenia i łatwo mogą wycofać wadliwy kod oraz mają sprawnie działający dział wsparcia, który potrafi (i może) identyfikować problemy z funkcjonalnościami.
Średniej wielkości organizacje zazwyczaj nie testują na produkcji. Z jednej strony mają już wystarczająco dużo środków, by testować laboratoryjnie. Z drugiej strony ich produkty nie są na tyle duże, by testerzy nie byli w stanie ogarnąć wszystkich zagadnień.

Osobną grupę stanowią dostawcy różnych krytycznych rozwiązań, którzy mają zupełnie inne podejście do problemu testowania, ale to temat na osobny wpis.

Podsumowanie

Trochę się rozwlekłem, więc szybkie podsumowanie. Testy na produkcji nie są czymś złym. Jeżeli potrafimy je wykonać, czyli mówiąc prościej, umiemy odkręcić ich rezultaty, to mogą okazać się niezłym narzędziem. Ważne jest też wypracowanie i przestrzeganie zasad tego typu testów. Inaczej może okazać się, że mamy problem jak mBank. A co tam padło? Najprawdopodobniej w narzędziu do wysyłania powiadomień tester nie zaznaczył grupy docelowej i poszło do wszystkich. Zapewne narzędzie było niedostatecznie przetestowane.