Mój Jenkinsfile pod Symfony / PHP

O zaletach stosowania i czym w ogóle jest Continuous integration(CI) nie będę się w tym wpisie rozwodził. Wydaje się, że tyle już zostało powiedziane w tym temacie, że to wszystko staje się aż do bólu oczywiste w samej teorii. Dlatego dzisiejszy wpis chce by miał bardziej praktyczne podejście do tego. Od dłuższego czasu szukam jakiejś w miarę sensownej konfiguracji Jenkinsa pod projekty pisane w PHP. Niestety od momentu wprowadzenia Pipeline, wiele rozszerzeń dedykowanych pod PHP po prostu z tym nie współgra. Poniżej prezentuje co udało mi się ustalić i jak to wygląda w moim projekcie gdzie na pokładzie jest Symfony4 z testami pisanym w PHPUnit. Jednakże dla osób niezaznajomionych  lub wciąż głodnych dodatkowych informacji, z całego serca mogę polecić książkę Continuous Integration: Improving Software Quality and Reducing Risk.

Na świeżej instalacji Jenkinsa 2.176.1 musiałem dodatkowo zainstalować następujące pluginy:

  • PHP Built-in Web Server
  • HTML Publisher – umożliwia publikowanie raportu z pokrycia kodu testami
  • Clover – ustawia zachowania build’a na podstawie wyników z pokrycia kodu
  • Clover PHP – PHP’owy wrapper na Clover
  • Javadoc – prezentuje dokumentacje z phpdoc
  • Static Analysis Utilities
  • Checkstyle – przechwytuje raport z phpcs
  • PMD – przechwytuje raport z phpmd
  • DRY – przechwytuje raport z phpcpd
  • Bitbucket Plugin – umożliwia przechwycenie webhook’a z Bitbucket (więcej o tym w dalszej części wpisu)

Poniżej Jenkinsfile

#!/usr/bin/env groovy

node {
    
    stage('Get code from SCM') {
        checkout(
                [$class: 'GitSCM', branches: [[name: 'master']],
                 doGenerateSubmoduleConfigurations: false,
                 extensions: [],
                 submoduleCfg: [],
                 userRemoteConfigs: [[url: 'git@adresrDoRepo.git',  credentialsId: 'user']]]
        )
    }

    stage('Prepare') {
        sh 'composer install'
        sh 'bin/console assets:install'
        sh 'bin/console cache:clear'
        sh 'bin/console doctrine:database:create'
        sh 'bin/console doctrine:migrations:migrate --no-interaction'
    }

    stage('PHP Syntax check') { 
        sh 'vendor/bin/parallel-lint --exclude vendor/ --exclude ./bin .'
    }
    
    stage('Symfony Lint') {
        sh 'bin/console lint:yaml src'
        sh 'bin/console lint:yaml tests'
        sh 'bin/console lint:twig src'
        sh 'bin/console lint:twig tests'
    }

    stage("PHPUnit") {
        sh 'bin/phpunit --coverage-html build/coverage --coverage-clover build/coverage/index.xml'
    }

    stage("Publish Coverage") {
        publishHTML (target: [
                allowMissing: false,
                alwaysLinkToLastBuild: false,
                keepAll: true,
                reportDir: 'build/coverage',
                reportFiles: 'index.html',
                reportName: "Coverage Report"

        ])
    }

    stage("Publish Clover") {
        step([
            $class: 'CloverPublisher',
            cloverReportDir: 'build/coverage',
            cloverReportFileName: 'index.xml',
            healthyTarget: [methodCoverage: 70, conditionalCoverage: 80, statementCoverage: 80], // optional, default is: method=70, conditional=80, statement=80
            unhealthyTarget: [methodCoverage: 50, conditionalCoverage: 50, statementCoverage: 50], // optional, default is none
            failingTarget: [methodCoverage: 0, conditionalCoverage: 0, statementCoverage: 0]     // optional, default is none
        ])
    }

    stage('Checkstyle Report') {
        sh 'vendor/bin/phpcs --report=checkstyle --report-file=build/logs/checkstyle.xml --standard=phpcs.xml --extensions=php,inc -wp || exit 0'
        checkstyle pattern: 'build/logs/checkstyle.xml'
    }

    stage('Mess Detection Report') {
        sh 'vendor/bin/phpmd . xml phpmd.xml --reportfile build/logs/pmd.xml || exit 0'
        pmd canRunOnFailed: true, pattern: 'build/logs/pmd.xml'
    }

    stage('CPD Report') {
        sh 'vendor/bin/phpcpd --log-pmd build/logs/pmd-cpd.xml --exclude bin --exclude vendor --exclude src/Migrations --exclude var . --progress || exit 0'
        dry canRunOnFailed: true, pattern: 'build/logs/pmd-cpd.xml'
    }

    stage('Lines of Code') {
        sh ' vendor/bin/phploc --count-tests --log-csv build/logs/phploc.csv --log-xml build/logs/phploc.xml . --exclude vendor --exclude src/Migrations --exclude var .'
    }

    stage('Software metrics') {
        sh 'vendor/bin/pdepend --jdepend-xml=build/logs/jdepend.xml --jdepend-chart=build/dependencies.svg --overview-pyramid=build/overview-pyramid.svg --ignore=vendor,var,bin,build .'
    }

    stage('Generate documentation') {
        sh 'vendor/bin/phpdox -f phpdox.xml'
    }
    stage('Publish Documentation') {
        publishHTML (target: [
                allowMissing: false,
                alwaysLinkToLastBuild: false,
                keepAll: true,
                reportDir: 'docs/html',
                reportFiles: 'index.xhtml',
                reportName: "PHPDox Documentation"

        ])
    }

}

Wycinek z composer.json

"require-dev": {
        "jakub-onderka/php-parallel-lint": "^1.0",
        "pdepend/pdepend": "@stable",
        "phploc/phploc": "^5.0",
        "phpmd/phpmd": "@stable",
        "sebastian/phpcpd": "^4.1",
        "squizlabs/php_codesniffer": "*",
        "symfony/debug-pack": "*",
        "symfony/maker-bundle": "^1.0",
        "symfony/phpunit-bridge": "^5.0",
        "symfony/profiler-pack": "*",
        "symfony/test-pack": "*",
        "symfony/web-server-bundle": "4.3.*",
        "theseer/phpdox": "^0.12.0"
    },

A tak wygląda mój dashboard po tych buildach

W trakcie implementacji natknąłem się na dwa problemy. Pierwszy dotyczył ustawienia w Jenkinsie Credentials dla wygenerowanego klucza z dostępem do repozytorium Git. Pamiętajmy, że trzeba taki klucz publiczny utworzyć i zapisać w ustawieniach Jenkinsa – Credentials. Następnie w pliku Jenkinsfile w parametrze userRemoteConfigs należy ustawić credentialsId z identyfikatorem podanym w zakładce Credentials. Ciekawą opcją w przypadku używania akurat Bitbucket jest możliwość wygenerowania klucza z dostępem tylko do odczytu repozytorium. Zdecydowanie polecam tą opcję dla przyznania dostępu dla Jenkinsa. Inną problematyczną kwestią z jaką się spotkałem było przechwycenie webhooka z Bitbucket. Albo występował problem z uprawnieniami, albo serwer Jenkinsa klasyfikował żądanie jako nieprawidłowe. Gdy się z tym uporałem okazało się, że build dla gałęzi master idzie także wtedy gdy zostanie zrobiony push do innego brancha. Koszmar! Rozwiązanie, użyć pluginu bitbucket w Jenkinsie.

A Wy jaką macie konfiguracje ? 😉

Kiedy aplikacja nie udostępnia czytelnego API

Cześć, ostatnio pracując nad nowym projektem w PHP zderzyłem się z koniecznością integracji mojej aplikacji z zewnętrzną usługą. Niestety owa usługa nie udostępnia żadnego sensownego interfejsu ala REST. Dlatego też musiałem się trochę napocić by otrzymać interesujące mnie dane. Dzisiejszy wpis będzie o tych zmaganiach i wykorzystaniu w tym celu bibliotek CURL (dla połączenia), fabpot/goutte (do swobodnego przechodzenia po drzewie DOM) i kegi/netscape-cookie-file-handler (do manipulacją plikami cookie lokalnie).

Zanim zaczniemy, uwaga na CSRF

By mógł otrzymywać interesujące mnie informacje, muszę się wcześniej zalogować do aplikacji. Pierwszym wymaganym krokiem jest otrzymanie tokenu ze zdalnej witryny, który służy zabezpieczeniu przed atakiem CSRF. Przykładowy kod wykorzystujący bibliotekę fabpot/goutte prezentuje poniżej.

private function getCsrfToken(): string
    {
        $crawler = $this->client->request('GET', self::SERVICE_LOGIN_URL);
        $token = $crawler->filter('input[name=csrftoken]')->attr('value');
        return $token;
    }

gdzie SERVICE_LOGIN_URL zawiera adres URL do zdalnej witryny, która z kolei zwraca swój token w inpucie o nazwie csrftoken. Tak przechwycony token będziemy musieli później odsyłać do tej witryny (zgodnie z jej protokołem).

Uwierzytelnienie

Mając pobrany token, możemy przystąpić do logowania. Oczywiście będziemy musieli posiadać login i hasło. Sposób logowania zależy od konkretnej witryny, w moim przypadku wygląda to tak.

private function auth(string $token): string
    {
        $postdata = http_build_query(
            array(
                'csrftoken' => $token,
                'username' => $this->login,
                'password' => $this->password,
            )
        );

        $headerArray = [
            'Content-Type: application/x-www-form-urlencoded',
            'referer: http://' . self::SERVICE_LOGIN_URL,
            'Accept: */*',
            "Cookie: csrftoken=$token;",
        ];

        $ckfile = tempnam(self::COOKIE_DIR, "CURLCOOKIE");
        try {
            $ch = curl_init(self::SERVICE_LOGIN_URL);
            curl_setopt($ch, CURLOPT_COOKIEJAR, $ckfile);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_POST, 1);
            curl_setopt($ch, CURLOPT_HTTPHEADER, $headerArray);
            curl_setopt($ch, CURLOPT_POSTFIELDS, $postdata);
            curl_setopt($ch, CURLOPT_COOKIEFILE, $ckfile);
            curl_setopt($ch, CURLOPT_VERBOSE, true);
            $output = curl_exec($ch);

            if ($output === false) {
                throw new \Exception(sprintf('No connection with service, msg: %s', curl_error($ch)));
            }
            $filenameArray = explode(DIRECTORY_SEPARATOR, $ckfile);
            return end($filenameArray);

        } finally {
            curl_close($ch);
        }
    }

Zatem token, login i hasło zostaną wysłane metodą POST pod URL ze stałej SERVICE_LOGIN_URL (opcja CURLOPT_POST z danymi z klucza CURLOPT_POSTFIELDS ). Do poprawnego obsłużenia żądania konieczne jest wysyłanie dodatkowo kilku nagłówków w tym cookie zawierającego token (CURLOPT_HTTPHEADER). Opcjonalne pozostaje użycie CURLOPT_RETURNTRANSFER i CURLOPT_VERBOSE – ułatwiają śledzenie błędów.

Serwer w przypadku poprawnego uwierzytelnienia wyśle dane (identyfikator sesji) do pliku cookie. Stąd konieczne jest jego wcześniejsze utworzenie przy użyciu funkcji tempnam() generującej losowo nazwany plik. Następnie odczyt tego pliku ułatwia biblioteka kegi/netscape-cookie-file-handler, jak poniżej.

 private function saveSessionId(string $ckfile): string
    {
        $configuration = (new Configuration())->setCookieDir(self::COOKIE_DIR);
        $cookieJar = (new CookieFileHandler($configuration))->parseFile($ckfile);
        $sessionIdObject = $cookieJar->get('sessionid');
        $sessionId = $sessionIdObject->getValue();
        $expire = $sessionIdObject->getExpire();
...

W dalszym kroku identyfikator sesji zostanie zapisany ze zmiennej $sessionId do bazy danych, tak by mógł zostać wykorzystywany dla późniejszych requestów.

Przykładowy request

Wreszcie przyszedł czas na request, który pozwoli nam na wykonanie jakiejś akcji po stronie zewnętrznej witryny. Może to wyglądać na przykład tak jak poniżej (pobranie danych do zmiennej $table zawartych w klasie .table drzewa DOM)

$crawler = $this->client->request('GET', self::SERVICE_SOME_TABLE_URL);
$table = $crawler->filter('.table')->first();

ufff….mimo to, jednak wciąż wolę REST’a 😉

Zdalna Fasada

Jako zwolennik leniwej optymalizacji, czyli nie przejmowania się wydajnością aplikacji dopóki owa aplikacja nie zacznie spowalniać – mam kłopot z dzisiejszym wpisem. Jest tak, ponieważ chciałbym tutaj naświetlić wzorzec projektowy Remote FacadeZdalna Fasada, który wymusza na nas taką optymalizacje już na etapie projektu architektury programu. Wpis ten jest rozwinięcie tematu o Data Transfer Object, a oba wzorce doskonale się uzupełniają.

Problem jaki przeważnie niesie optymalizacja to trudne do pogodzenia światy: czytelności kodu oraz jego wydajności. Stąd jest ważne by oprzeć poprawę efektywności kodu na jakimś sprawdzonym wzorcu. Zacznijmy jednak od początku, czym jest Remote Facade?

“Zapewnia ogólną fasadę dla szczegółowych obiektów, której celem jest poprawa efektywności ruchu sieciowego.”

gdzie przez “ogólną fasadę” mamy tu namyśli wzorzec Fasady z Gangu Czworga. Taki obiekt upraszcza nam interfejs, a w przypadku obiektów zdalnych ma za zadanie zmniejszyć ilość interakcji między nimi. Zdalnym obiektem może tu być nie tylko obiekt wywoływany na przeciwnym krańcu kuli ziemskiej, ale nawet obiekt wywoływany na tym samym komputerze (w ramach innego procesu).

Remote Facacde zastępuje wszystkie metody set() i get() jedną metodą set oraz jedną metodą get (zwanymi jako blokowe metody dostępu). Fasada może działać na zasadzie bramy dostępowej. Implementacja nie powinna być trudna , ważne jest tutaj by wybrać taki format przechowywania danych, aby łatwo można było te dane wysłać przez sieć. Trzeba uważać na serializacje całego obiektu, nie zawsze jest to dobry pomysł. W takim przypadku warto użyć DTO. Innym problemem wynikającym z stosowania Remote Facade jest poziom szczegółowości. Możemy mieć wiele fasad obejmujących stosunkowo niewielki zakres działania lub wprost przeciwnie ograniczyć się jedynie do kilku. To drugie podejście zaleca Fowler w swojej książce Architektura Systemów Zarządzania Przedsiębiorstwem. Wzorce projektów. Ciekawym pomysłem jest wykorzystanie Remote Facade jako mechanizmu zabezpieczeń czy obsługi transakcji. Listy dostępu mogą określać, który użytkownik ma dostęp do wywołania danej metody.

Na zakończenie, warto mieć na uwadze, że Remote Facade nie może zawierać żadnej logiki dziedziny. Jeśli konieczne jest użycie dziedziny np. do sterowania przebiegiem pracy czy koordynowania to można albo implementacje przenieść do obiektów szczegółowych lub użyć wzorca Transaction Script. Remote Facade nie użyjemy wszędzie tam gdzie potrzebujemy asynchronicznych komunikatów, zresztą takie podejście mija się z celem stosowania fasady.

Więcej informacji na ten temat w książce “Architektura Systemów Zarządzania Przedsiębiorstwem. Wzorce projektowe“. M. Fowler.

Parametryzowanie Dockerfile

Trudno sobie dzisiaj wyobrazić rozwój dużego projektu bez wykorzystywania dobrodziejstw jakie daje nam wirtualizacja. Po prostu ilość potrzebnych komponentów, relacji między nimi, całego procesu developmentu, orkiestracji jest tak olbrzymia i skomplikowana, że bez Dockera nie dalibyśmy rady.

Jednakże narzędzia takie jak docker i docker-compose nie są panaceum na wszystkie problemy. Ostatnio stanąłem przed wyzwaniem rozdzielenia logiki w trakcie budowy obrazu w pliku Dockerfile. Generalnie, potrzebna jest inna wersja na produkcji a inna w trybie deweloperskim.

Jako programista, głównie piszący w świecie obiektowym postanowiłem rozwiązać ten problem używając dziedziczenia. Jakże było to wielkie zaskoczenie dla mnie, gdy okazało się, że pliki Dockerfile nie posiadają takiego mechanizmu. Dziedziczeniu może podlegać jedynie logika zawarta w plikach docker-compose.yml…cóż, trzeba kombinować inaczej.

Rozwiązanie jakie udało mi się zaimplementować używa parametrów na etapie budowy obrazu – komenda arg [1][2]. Wygląda to następująco.

W głównym pliku budowy obrazu Dockerfile tworzymy zmienną (owy parametr) i przypisujemy domyślną wartość. Domyślna wartość może odpowiadać zachowaniu w np. trybie produkcyjnym.

ARG some_variable_name=default_value

W dalszej części pliku Dockerfile wykorzystujemy zmienną dostarczając zachowanie w zależności od wartości tego parametru. Właściwie jest to pewnego rodzaju flaga dla nas. Przykład:

RUN apt-get install $some_variable_name

Równie dobrze, można też [3]:

RUN if [ "x$some_variable_name" = "htop" ] ; then something action ; else echo other action ; fi

Pozostaje jeszcze nadpisać domyślną wartość np. dla trybu deweloperskiego. Poniżej zawartość docker-compose-dev.yml

services:
  someImage:
    build:
      context: ./
      dockerfile: Dockerfile
      args:
        some_variable_name: "htop"

Jeszcze innym rozwiązaniem tego problemu jaki się nasuwa to skonfigurowanie domyślnego obrazu po jego budowie i ponowne zapisanie stanu przy użyciu commit [4]. Ale jakoś to takie mało eleganckie posunięcie :/

[1] https://docs.docker.com/engine/reference/builder/#arg
[2] https://vsupalov.com/docker-arg-env-variable-guide/
[3] https://stackoverflow.com/questions/43654656/dockerfile-if-else-condition-with-external-arguments
[4] https://forums.docker.com/t/docker-container-not-persisting-data-on-stop-start-or-when-committing-to-another-image-on-windows/5612

Kod efektywny czy efektowny ? Alternatywa dla unset() w PHP

Jakiś czas temu zostałem zmuszony do skasowania pewnego elementu w tablicy, który oczywiście musiał być tak głęboko ukryty w całej tej hierarchii bym łatwo i przyjemnie nie mógł tego zrobić. W efekcie końcowym uzyskałem kod, który w pewnym sensie narusza prawo demeter[1], a symptomy są podobne do opisanego przez Wujka Boba problemu z wrakami pociągów (ang. Train Wrecks) [2]. O ile takie porównanie ma jakiś sens w kontekście operowania tablicami w PHP. Tak czy siak, być może poniższy kod jest efektywny, ale czy jest on efektowny ?

unset($carsArray['accessories'][0]['cockpit'][0]['mirror'])

Tak właściwie jest to częsta przypadłość języków dynamicznie typowanych. Postanowiłem przemyśleć sprawę raz jeszcze. Czy aby na pewno nie można by zrefaktoryzować tego kodu tak, by wyglądał choć odrobinę lepiej?

Struktury zmienić nie mogę, natomiast tą część logiki odpowiedzialnej za wyszukanie odpowiedniego klucza i jego skasowanie już tak. Stąd pierwszym pomysłem jaki mi wpadł do głowy to wykorzystanie pętli foreach na wielu poziomach, gdzie każda klasa znałaby strukturę tablicy wyłącznie dla swojego poziomu (zgodnie z prawem demeter). Można by też to oprzeć o kompozyt[3], ale gdybyśmy byli w świecie obiektów. Niestety funkcja unset() nie zadziała dla referencji uzyskanej z pętli foreach. Dlatego też rozwiązania musiałem szukać w wbudowanych w PHP funkcjach na tablicach [4]. W szczególności array_filter() [5].

class MirrorCleaner
{
    private $carsArray = [];

    public function __construct(array $carsArray)
    {
        $this->carsArray = $carsArray;
    }

    public function cleanMirror()
    {
        $newCarsArray = [];

        foreach ($this->carsArray as $value) {
            $accessoriesArray = $value['accessories'] ?? [];
            $newCarsArray[]['accessories'] = $this->parserAccessories($accessoriesArray);
        }

        return $newCarsArray;
    }

    private function parserAccessories(array $accessoriesArray)
    {
        $newAccessoriesArray = [];

        foreach ($accessoriesArray as $value) {
            $cockpitsArray = $value['cockpit'] ?? [];
            $newAccessoriesArray[]['cockpit'] = $this->parserCockpits($cockpitsArray);
        }

        return $newAccessoriesArray;
    }

    private function parserCockpits(array $cockpitsArray)
    {
        $newCockpitsArray = [];

        foreach ($cockpitsArray as $element) {
            $newCockpitsArray[] = $this->removeMirror($element);
        }

        return $newCockpitsArray;
    }

    private function removeMirror(array $value)
    {
        return array_filter($value, function ($key) {
            return !($key == 'mirror');
        }, ARRAY_FILTER_USE_KEY);
    }
}

Pełny kod na https://github.com/domino91/ArrayFilter

Nie jest to rozwiązanie idealne. Klasa MirrorCleaner narusza zasadę pojedynczej odpowiedzialności (parsuje tablice, kasuje elementy). Nadal złamane jest prawo demeter (klasa zna algorytm przetwarzania całej struktury). Nie jest to też rozwiązanie optymalne dla naprawdę sporych tablic czy aplikacji, w których czas wykonania ma kluczowe znaczenie (kopiujemy tu tablice). Pewnym krokiem naprzód byłoby zrefaktoryzowanie tej klasy o wydzielenie na kilka klas kompozytów i wstrzykiwanie do nich zależności…Mimo to jednak uważam obecne rozwiązanie za całkiem efektowne 😉 Co Wy o tym sądzicie ?

Źródło:
[1] https://www.tripled.io/25/08/2016/The-anemic-domain-model/
[2] https://hackerchick.com/clean-code/
[3] https://www.geeksforgeeks.org/composite-design-pattern/
[4] https://carlalexander.ca/php-array-functions-instead-loops/
[5] https://www.php.net/manual/en/function.array-filter.php

Throwable

Jakiś czas temu opisywałem działanie funkcji set_error_handler() w PHP, która pozwala nam na przechwytywanie większości błędów w naszej aplikacji. Niektóre typy takie jak: E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING itp.  nie są osiągalne. Dlatego dzisiaj chciałbym przedstawić alternatywne rozwiązanie. Od wersji PHP 7 istnieje możliwość przechwytywania błędów zarówno tych krytycznych (ang. fatal error) jak i tych możliwych do odzyskania (E_ERROR and E_RECOVERABLE_ERROR)[1]. Nie jest to oczywiście panaceum na obsługę wszystkich typów błędów (E_WARNING, E_NOTICE czy E_PARSE nie są tym objęte) także trzeba o tym pamiętać. Jednakże, to co możemy przechwycić jest zależne od klasy Error oraz całej jej rodziny, prezentowanej poniżej.

Error
      ArithmeticError
        DivisionByZeroError (tylko dla operatora % )
      AssertionError
      ParseError (tylko dla included/required file oraz eval())
      TypeError
        ArgumentCountError

Dla przykładu, weźmy dzielenie przez zero. Kod mógłby wyglądać tak

<?php

try {
    $quotient = 1 % 0;
} catch (Error $e) {
    echo $e->getMessage();
}
//Result: Modulo by zero

Ciekawym zabiegiem zastosowanym przez twórców PHP jest utworzenie nadklasy Throwable po której dziedziczy Error i Exception. Pozwala nam to łatwo zaadaptować kod pisany pod PHP 5.x z naszą aplikacją. [1][2].

<?php

try {
    // Code that may throw an Exception or Error.
} catch (Throwable $t) {
    // Executed only in PHP 7, will not match in PHP 5.x
} catch (Exception $e) {
    // Executed only in PHP 5.x, will not be reached in PHP 7
}

A czy Wy korzystacie już w swoich programach z Throwable ?

Źródło
[1] https://trowski.com/2015/06/24/throwable-exceptions-and-errors-in-php7/
[2] https://sergeyzhuk.me/2016/12/24/php7-errors-and-exceptions/
[3] https://www.php.net/manual/en/class.throwable.php

Docker i iptables

Ostatnio stanąłem przed problemem zablokowania dostępu do sieci dla kontenerów Dockera do jednego z moich projektów. Zadanie wydawało mi się trywialne, ale jakoś tak się złożyło, że strasznie je sobie zagmatwałem. Koniec końców rozwiązanie sprowadza się do jednej linijki:

iptables -A DOCKER-USER -j DROP

Jednak zanim do tego dotarłem to musiałem całkiem sporo dowiedzieć się jak działa sieć Dockerowa.

Wracając do początku, celem jest zablokowanie ruchu do Internetu, sieci lokalnej i pomiędzy kontenerami z wewnątrz systemu gościa. Gospodarz ma posiadać dostęp do usług, które serwuje kontener dockerowy. Pierwszym pomysłem jaki mi wpadł to ustawienie osobno firewalla na każdym z obrazów (przebudowa Dockerfile). Jest to karkołomne rozwiązanie. Poza czasochłonnością tego podejścia, zatrzyma nas w tym wszystkim komunikat:

iptables can’t initialize iptables table `filter’: Permission denied

tak, nawet jako root! Pewnym rozwiązaniem jest doinstalowanie sudo i utworzenie użytkownika po czym przelogowanie się na niego. Trzeba jeszcze pamiętać o uruchomieniu dockera z wywołaniem –cap-add=ALL…lecz, nigdzie w oficjalnej dokumentacji nie znalazłem wzmianek, że taka droga jest zalecana.

Wydaje się więc, że najlepiej sterować zachowaniem zapory sieciowej z poziomu systemu gospodarza, przy użyciu dostarczonych łańcuchów (docker, docker-user etc.).

Więcej na ten temat w
https://sowhatisthesolution.wordpress.com/2018/09/11/protect-docker-from-internet-while-allowing-lan-with-iptables/
i https://docs.docker.com/network/iptables/

ahh i jeszcze, jeśli to rozwiązanie Ci nie pomogło, zobacz swoje aktualne reguły (iptables -nL), być może posiadasz jakieś reguły zezwalające (można je nadpisać, zamiast parametru -A użyj -I).

Sprawna sylwetka

“Spójrzmy prawdzie w oczy: my, programiści, spędzamy większość swojego czasu w pozycji siedzącej przy biurku. Dlatego tak ważne jest, abyśmy mieli odpowiednią wiedzę na temat tego, co trzeba robić, żeby żyć zdrowo. Niestety nasza praca popycha nas w przeciwnym kierunku”

John Sonmez, “Sprawny programista”

Muszę przyznać, że długo zwlekałem z tym wpisem. Głównie z racji tego, że jest to kwestia szczególnie ważna dla mnie, ale jednocześnie wciąż bardzo mglista, niestety. Zdaje sobie też sprawę, że niemożliwym jest naświetlenie tego tematu jednym wpisem to też chciałbym rozpocząć na moim blogu serie artykułów o dbaniu o własną kondycję. To co już wiem będzie uzupełnione o to co napisali autorzy tj. cytowany już John Sonmez w “Sprawny programista” i Tomasz Saweczko w “Najpierw wiedza, potem rzeźba” (szczególnie polecam tą drugą książkę, bo jest w niej zawarta spora dawka konkretnej wiedzy, w dodatku opisana bardzo prostym, przystępnym językiem). Mam nadzieje, że coś z tego wyjdzie 😉

Nie da się ukryć, że codzienna praca przy komputerze niszczy nasze zdrowie, kondycje i po pewnym czasie samoocenę oraz samopoczucie. Dopadło to również i mnie. Choć staram się regularnie być w “ruchu”, trochę pływać, trochę biegać to jednak zaczynam na własnej skórze odczuwać rezultaty “pracy przy biurku”. To właśnie z tego powodu stale poszukuje sposobów na poprawę własnej sylwetki. Nie jest to proste. Właściwie to ciągła walka z własnymi nawykami, słabościami czy błędnymi przekonaniami. Jednakże mam też swoje własne, małe zwycięstwa. W tym wpisie przedstawię co się takiego u mnie zdarzyło, że w zeszłym roku zrzuciłem 8kg mimo pracy za biurkiem (a wpis, że w tym roku z kolei przytyłem 8kg będzie następnym razem :D).

Pierwsza zasada: Licz kalorie
Za namową autora “Najpierw wiedza, potem rzeźba” zacząłem prowadzić w ekselu dziennik tego ile kalorii dziennie dostarczam do swojego organizmu. Nie jest to zadanie jakoś wybitnie trudne, ale wymaga od nas pewnej samodyscypliny. Po pewnym czasie sami zapamiętujemy ile dane składniki mają kalorii, zatem robimy to już automatycznie. W tym miejscu uwaga, nie należy liczyć kalorii z dużą dokładnością, a wziąć pewien margines błędu. Oczywiście wcześniej musimy zmierzyć ile kalorii potrzebuje nasz organizm, tak by otrzymać różnice 7000 kcal potrzebnych do zrzucenia 1kg!

Druga zasada: Jedz tylko pełnowartościowe posiłki
Pełnowartościowe to takie, które zawierają dużo błonnika, minerałów, witamin, produktów pełnoziarnistych czy zdrowych tłuszczy. Długo nie mogłem się przekonać do większego spożywania warzyw we własnej diecie. Głównie za sprawą, że jadłem te warzywa, które mi nakazano. Tu jednak kluczem jest znalezienie takich, które Tobie smakują (na początku nie ma co przejmować się proporcjami w bilansie makroskładników, czy liczeniem kalorii dla zjedzonych warzyw).

Trzecia zasada: 10% Twojej diety, to musi być uczta dla duszy
Szczerze mówiąc nie dałbym rady utrzymać swojej diety, gdyby nie było w niej kilku produktów, za którymi szaleje, a które nie można zaklasyfikować jako “pełnowartościowe“. Oczywiście, musimy je dawkować z umiarem tak by nasycić duszę i mieć siłę do dalszej walki.

Czwarta zasada: Mierz swoją efektywność
No właśnie, nic tak nie motywuje człowieka do dalszego działania jak efekty z własnej pracy. Skrupulatne mierzenie wagi czy wykonywanie co pewien czas pomiaru składu ciała pomaga w konsekwentnym dążeniu do celu.

Piąta zasada: Trenuj to co lubisz
Nie trzeba chodzić na siłownie by zrzucić nadmierne kilogramy. Ważny jest ruch, nawet zwykły spacer na wieczór jest lepszy niż leżenie przed telewizorem. Natomiast to co mnie najbardziej zmotywowało do cotygodniowego treningu na basenie to fakt, że naprawdę to mnie relaksuje ;-).

Do tego dodałbym jeszcze, że oprócz diety, treningu, motywacji i regeneracji warto zatroszczyć się o wiedzę, czyli co i jak należy robić by osiągnąć upragnioną sylwetkę.

Ja walczę dalej i mam nadzieje, że uda mi się zebrać dla Was kolejne owocne spostrzeżenia 😉

Nowości w PHP 7.4

Premiera nowej wersji PHP 7.4 w listopadze, ale myślę, że warto już teraz zastanowić się nad tym co nas czeka tej jesieni. Może nie będzie to skok porównywalny do przejścia od wersji 5.6 do 7.0, jednak na kilka nowych “ficzerów” wyczekuje z radością. Będą to między innymi:

  • Możliwość deklaracji typów już na poziomie deklaracji klasy. Jest to rozwinięcie zapoczątkowanej od php 7.0 funkcjonalności pozwalającej zadeklarować typ dla typów prostych (string, integer, float …itp.). Zatem komentarze w kodzie takie jak: @param, @return będą mogły pójść w zapomnienie.
  • Skrócona składnia dla domknięć (ang. closures)
    tj. z function ($x) use ($arr) { return $arr[$x]; } do fn($x) => $arr[$x].
  • Null Coalescing Assignment Operator czyli umożliwienie wpisania wartości domyślnej dla zmiennej, gdy nie posiada ustawionej żadnej wartości (taki ukryty isset()).
  • Aktulizacja dla Spread operator o możliwość wypakowania całej tablicy w jednej instrukcji.
<?php
$vine_veges = ['cucumber', 'pumpkin'];
$ground_veges = ['carrots', 'potatos'];

print_r(['eggplant', ...$vine_veges, ...$ground_veges]);

/*
Array
(
    [0] => eggplant
    [1] => cucumber
    [2] => pumpkin
    [3] => carrots
    [4] => potatos
)
*/
z https://www.pixelite.co.nz/article/new-features-in-php-7-4/ 

Jak widać, jest na co czekać, więcej informacji
https://wiki.php.net/todo/php74
https://www.pixelite.co.nz/article/new-features-in-php-7-4/

Efektywne uczenie się programowania

Inspiracją do powstania tego dzisiejszego wpisu jest poniższy artykuł

Kiedy chodziłem do szkoły nauka nie wydawała się taka przyjemna, a przecież była konieczna. Brakowało mi w tym wszystkim nie tylko celu, motywacji, ale też i sensu. Z programowaniem było jednak inaczej. Już od czasów gdy mój pierwszy komputer z Windows 95 na pokładzie i modemem 56 Kbps (https://www.pcworld.pl/news/Demony-szybkosci-modemy-56-Kbps,297544.html) zagościł w domu, pojawiła się ciekawość, a za tym i motywacja. Właściwie od tego czasu, stale poszukuje sposobów na bardziej efektywne przyswajanie nowego materiału, czy to nowych technologi, idei, wzorców, algorytmów, rozwiązań etc. ..a przecież jak widać trochę tego jest, szczególnie w naszej branży IT. Jak zatem niepogubić się w tym wsystkim i się rozwijać ? Jak to ujął autor,

“nie uczy się ani zbyt głęboko, ani zbyt płytko”

To trudne, zwłaszcza w IT, gdzie musimy równoważyć teorę i praktykę. Wydaje się jednak dobrym pomysłem by z jednej strony poszerzać informacje o tym co dzieje się w całej branży, ale też jednocześnie specjalizować się w konkretnej dziedzinie, która nas kręci. Przytoczony “Warstwowy model nauki programowania” Przemka Smyrdka przyporządkowuje aktywności do konkretnych faz uczenia się programowania przy użyciu metafory lejka sprzedaży. Zaczynamy od “Świadomości“, tutaj przegląd nowinek w Social Media, udział w konferencjach, meetupach czy ogólnie rozmowach o programowaniu. Ta kategoria cechuje się niskim stopniem zaangażowania, za to wysoką ilością poznanych tematów. Później mamy takie kategorie jak “Zainteresowanie“, “Porównanie“, “Ewaluacja“, “Działanie” a lejek kończy “Promowanie“, czyli dzielenie się wiedzą z innymi. “Promowanie” jest kategorią z dużym stopniem zaangażowania i jest ograniczona do konkretnych tematów. Zdaniem twórcy taki model pozwoli nam usystematyzować nasz system uczenia się, a zatem pomoże podnieść efektywność 😉

Ja bym jeszcze do tego dorzucił odpuszczanie ( https://dominikjuszczyk.pl/2019/06/110-odpuszczanie/ ). Nie ma co robić sobie wyrzutów, że się wszystkiego nie wie.