CI/CD f'Azzjonijiet Github għal proġett Flask+Angular

CI/CD f'Azzjonijiet Github għal proġett Flask+Angular
F'dan l-artikolu, se naqsam l-esperjenza tiegħi tat-twaqqif ta 'CI/CD billi tuża l-Plesk Control Panel u l-Azzjonijiet Github. Illum se nitgħallmu kif niskjeraw proġett sempliċi bl-isem mhux ikkumplikat "Helloworld". Huwa miktub fil-qafas tal-Flask Python, b'ħaddiema tal-Kfus u frontend Angular 8.

Links għar-repożitorji: backend, il-parti ta 'quddiem.

Fl-ewwel parti tal-artiklu, se nħarsu lejn il-proġett tagħna u l-partijiet tiegħu. Fit-tieni, aħna ser insemmu kif nistabbilixxu Plesk u ninstallaw l-estensjonijiet u l-komponenti meħtieġa (DB, RabbitMQ, Redis, Docker, eċċ.).

Fit-tielet parti, fl-aħħar ser insemmu kif nistabbilixxu pipeline għall-iskjerament tal-proġett tagħna għal server f'ambjent dev u prod. U mbagħad inniedu s-sit fuq is-server.

U iva, insejt nintroduċi ruħi. Jisimni Oleg Borzov, jien żviluppatur fullstack fit-tim tas-CRM għall-maniġers tal-ipoteki f'Domclick.

Ħarsa ġenerali tal-proġett

L-ewwel, ejja nħarsu lejn żewġ repożitorji tal-proġett - backend u front - u nimxu fuq il-kodiċi.

Backend: Garafina + Karfus

Għall-parti ta 'wara, ħadt mazz li huwa pjuttost popolari fost l-iżviluppaturi ta' Python: il-qafas tal-Flask (għall-API) u Karfus (għall-kju tal-kompiti). SQLAchemy jintuża bħala ORM. L-alambik jintuża għall-migrazzjoni. Għall-validazzjoni JSON fil-pumi - Marshmallow.

В repożitorji hemm fajl Readme.md b'deskrizzjoni dettaljata tal-istruttura u struzzjonijiet għat-tmexxija tal-proġett.

Web Part API pjuttost mhux ikkumplikat, jikkonsisti minn 6 pinen:

  • /ping - biex tiċċekkja d-disponibbiltà;
  • mankijiet għar-reġistrazzjoni, awtorizzazzjoni, de-awtorizzazzjoni u l-kisba ta 'utent awtorizzat;
  • manku tal-email li jpoġġi kompitu fil-kju tal-karfus.

Parti tal-karfus saħansitra aktar faċli, hemm problema waħda biss send_mail_task.

Fil-folder /konf hemm żewġ sottofolders:

  • docker b'żewġ Dockerfiles (base.dockerfile biex tibni immaġini bażi li rarament tinbidel u Dockerfile għall-assemblaġġi prinċipali);
  • .env_files - b'fajls b'varjabbli ambjentali għal ambjenti differenti.

Hemm erba 'fajls docker-compose fl-għerq tal-proġett:

  • docker-compose.local.db.yml li titqajjem database lokali għall-iżvilupp;
  • docker-compose.local.workers.yml għat-trobbija lokali tal-ħaddiem, database, Redis u RabbitMQ;
  • docker-compose.test.yml biex iwettaq testijiet waqt l-iskjerament;
  • docker-compose.yml għall-iskjerament.

U l-aħħar folder li aħna interessati fih - .ci-cd. Fiha skripts tal-qoxra għall-iskjerament:

  • deploy.sh — it-tnedija tal-migrazzjoni u l-iskjerament. Jiġi fuq is-server wara li tibni u tmexxi testijiet f'Azzjonijiet Github;
  • rollback.sh - rollback tal-kontenituri għall-verżjoni preċedenti tal-assemblaġġ;
  • curl_tg.sh - tibgħat notifiki tal-iskjerament lil Telegram.

Frontend fuq Angular

Repożitorju bil-quddiem ferm aktar sempliċi minn Beck's. Il-faċċata tikkonsisti fi tliet paġni:

  • Paġna prinċipali b'formola biex tintbagħat email u buttuna tal-ħruġ.
  • Il-paġna tal-login.
  • Paġna tar-reġistrazzjoni.

Il-paġna ewlenija tidher aċetika:

CI/CD f'Azzjonijiet Github għal proġett Flask+Angular
Hemm żewġ fajls fl-għerq Dockerfile и docker-compose.yml, kif ukoll il-folder familjari .ci-cd bi ftit inqas skripts milli fir-repożitorju ta 'wara (skripts imneħħija għat-tmexxija tat-testijiet).

Nibdew proġett fi Plesk

Nibdew billi nwaqqfu Plesk u noħolqu abbonament għas-sit tagħna.

Installazzjoni ta' estensjonijiet

Fi Plesk, għandna bżonn erba' estensjonijiet:

  • Docker biex timmaniġġja u turi viżwalment l-istatus tal-kontenituri fil-pannell tal-amministrazzjoni Plesk;
  • Git biex tikkonfigura l-pass tal-iskjerament fuq is-server;
  • Let's Encrypt biex tiġġenera (u tiġġedded awtomatikament) ċertifikati TLS b'xejn;
  • Firewall biex jiġi kkonfigurat l-iffiltrar tat-traffiku li jkun dieħel.

Tista' tinstallahom permezz tal-pannell tal-amministrazzjoni tal-Plesk fit-taqsima tal-Estensjonijiet:

CI/CD f'Azzjonijiet Github għal proġett Flask+Angular
Aħna mhux se nikkunsidraw is-settings dettaljati għall-estensjonijiet, is-settings default se jagħmlu għall-finijiet tad-demo tagħna.

Oħloq abbonament u sit

Sussegwentement, irridu noħolqu abbonament għall-websajt tagħna helloworld.ru u nżidu s-sottodominju dev.helloworld.ru hemmhekk.

  1. Oħloq abbonament għad-dominju helloworld.ru u speċifika l-password tal-login għall-utent tas-sistema:

    CI/CD f'Azzjonijiet Github għal proġett Flask+Angular
    Iċċekkja l-kaxxa fil-qiegħ tal-paġna Sikura d-dominju b'Ejja Encryptjekk irridu nwaqqfu HTTPS għas-sit:

    CI/CD f'Azzjonijiet Github għal proġett Flask+Angular

  2. Sussegwentement, f'dan l-abbonament, oħloq sottodominju dev.helloworld.ru (li għalih tista' wkoll toħroġ ċertifikat TLS b'xejn):

    CI/CD f'Azzjonijiet Github għal proġett Flask+Angular

Installazzjoni ta' Komponenti tas-Server

Għandna server ma OS Debian Stretch 9.12 u pannell tal-kontroll installat Plesk Obsidian 18.0.27.

Għandna bżonn ninstallaw u kkonfiguraw għall-proġett tagħna:

  • PostgreSQL (fil-każ tagħna, se jkun hemm server wieħed b'żewġ databases għal ambjenti dev u prod).
  • RabbitMQ (l-istess, l-istess eżempju b'vhosts differenti għall-ambjenti).
  • Żewġ istanzi Redis (għall-ambjenti dev u prod).
  • Docker Reġistru (għall-ħażna lokali ta 'immaġini Docker mibnija).
  • UI għar-reġistru Docker.

PostgreSQL

Plesk diġà jiġi ma' PostgreSQL DBMS, iżda mhux l-aħħar verżjoni (fil-ħin tal-kitba Plesk Obsidian appoġġjati Verżjonijiet Postgres 8.4–10.8). Irridu l-aħħar verżjoni għall-applikazzjoni tagħna (12.3 fil-ħin tal-kitba ta 'dan), għalhekk aħna se ninstallawha manwalment.

Hemm ħafna struzzjonijiet dettaljati għall-installazzjoni ta' Postgres fuq Debian fuq in-net (eżempju), għalhekk mhux se niddeskrivihom fid-dettall, nagħti biss il-kmandi:

wget -q https://www.postgresql.org/media/keys/ACCC4CF8.asc -O - | sudo apt-key add -
sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ stretch-pgdg main" >> /etc/apt/sources.list.d/pgdg.list'

sudo apt-get update
sudo apt-get install postgresql postgresql-contrib

Meta wieħed iqis li PostgreSQL għandu settings default pjuttost medjokri, huwa meħtieġ li tikkoreġi l-konfigurazzjoni. Dan se jgħinna kalkulatur: għandek bżonn issuq fil-parametri tas-server tiegħek u tissostitwixxi s-settings fil-fajl /etc/postgresql/12/main/postgresql.conflil dawk offruti. Għandu jiġi nnutat hawnhekk li kalkolaturi bħal dawn mhumiex bullet maġiku, u l-bażi għandha tiġi rranġata b'mod aktar preċiż, ibbażat fuq il-ħardwer, l-applikazzjoni u l-kumplessità tal-mistoqsija tiegħek. Iżda dan huwa biżżejjed biex tibda.

Minbarra s-settings proposti mill-kalkulatur, nibdlu wkoll postgresql.confil-port default 5432 għal ieħor (fl-eżempju tagħna - 53983).

Wara li tbiddel il-fajl tal-konfigurazzjoni, ibda mill-ġdid postgresql-server bil-kmand:

service postgresql restart

Installajna u kkonfigurajna PostgreSQL. Issa ejja noħolqu database, utenti għal ambjenti dev u prod, u nagħtu lill-utenti drittijiet biex jimmaniġġjaw id-database:

$ su - postgres
postgres:~$ create database hw_dev_db_name;
CREATE DATABASE
postgres:~$ create user hw_dev_db_user with password 'hw_dev_db_password';
CREATE ROLE
postgres:~$ grant ALL privileges ON database hw_dev_db_name to hw_dev_db_user;
GRANT
postgres:~$ create database hw_prod_db_name;
CREATE DATABASE
postgres:~$ create user hw_prod_db_user with password 'hw_prod_db_password';
CREATE ROLE
postgres:~$ grant ALL privileges ON database hw_prod_db_name to hw_prod_db_user;
GRANT

Fenek MQ

Ejja ngħaddu għall-installazzjoni ta 'RabbitMQ, sensar ta' messaġġi għal Karfus. L-installazzjoni fuq Debian hija pjuttost sempliċi:

wget https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb
sudo dpkg -i erlang-solutions_1.0_all.deb

sudo apt-get update
sudo apt-get install erlang erlang-nox

sudo add-apt-repository 'deb http://www.rabbitmq.com/debian/ testing main'
wget -O- https://www.rabbitmq.com/rabbitmq-release-signing-key.asc | sudo apt-key add -

sudo apt-get update
sudo apt-get install rabbitmq-server

Wara l-installazzjoni, għandna bżonn noħolqu vhosts, utenti u agħti d-drittijiet meħtieġa:

sudo rabbitmqctl add_user hw_dev_amqp_user hw_dev_amqp_password 
sudo rabbitmqctl set_user_tags hw_dev_amqp_user administrator
sudo rabbitmqctl add_vhost hw_dev_vhost
sudo rabbitmqctl set_permissions -p hw_dev_vhost hw_dev_amqp_user ".*" ".*" ".*"

sudo rabbitmqctl add_user hw_prod_amqp_user hw_prod_amqp_password 
sudo rabbitmqctl set_user_tags hw_prod_amqp_user administrator
sudo rabbitmqctl add_vhost hw_prod_vhost
sudo rabbitmqctl set_permissions -p hw_prod_vhost hw_prod_amqp_user ".*" ".*" ".*"

Ddistribwit mill-

Issa ejja ninstallaw u kkonfiguraw l-aħħar komponent għall-applikazzjoni tagħna - Redis. Se jintuża bħala backend għall-ħażna tar-riżultati tal-kompiti Karfus.

Se ngħollu żewġ kontenituri Docker b'Redis għal ambjenti dev u prod bl-użu tal-estensjoni Docker għal Plesk.

  1. Immorru Plesk, immorru fit-taqsima tal-Estensjonijiet, infittxu l-estensjoni Docker u ninstallawha (neħtieġu verżjoni b'xejn):

    CI/CD f'Azzjonijiet Github għal proġett Flask+Angular

  2. Mur fl-estensjoni installata, sib l-immaġni permezz tat-tfittxija redis bitnami u installa l-aħħar verżjoni:

    CI/CD f'Azzjonijiet Github għal proġett Flask+Angular

  3. Aħna mmorru fil-kontenitur imniżżel u aġġusta l-konfigurazzjoni: speċifika l-port, id-daqs massimu ta 'RAM allokat, il-password fil-varjabbli ambjentali, u mmunta l-volum:

    CI/CD f'Azzjonijiet Github għal proġett Flask+Angular

  4. Aħna nwettqu passi 2-3 għall-kontenitur tal-prod, fis-settings nibdlu biss il-parametri: port, password, daqs tar-RAM u mogħdija għall-folder tal-volum fuq is-server:

    CI/CD f'Azzjonijiet Github għal proġett Flask+Angular

Docker Reġistru

Minbarra s-servizzi bażiċi, ikun sabiħ li tpoġġi r-repożitorju tal-immaġni Docker tiegħek stess fuq is-server. Fortunatament, l-ispazju tas-server issa huwa pjuttost irħis (ċertament irħas minn abbonament ta 'DockerHub), u l-proċess tat-twaqqif ta' repożitorju privat huwa sempliċi ħafna.

Irridu li jkollna:

Biex tagħmel dan:

  1. Ejja noħolqu żewġ sottodominji fi Plesk fl-abbonament tagħna: docker.helloworld.ru u docker-ui.helloworld.ru, u kkonfigurat Ejja Encrypt ċertifikati għalihom.
  2. Żid il-fajl fil-folder tas-sottodominju docker.helloworld.ru docker-compose.yml b'kontenut bħal dan:
    version: "3"
    
    services:
      docker-registry:
        image: "registry:2"
        restart: always
        ports:
          - "53985:5000"
        environment:
          REGISTRY_AUTH: htpasswd
          REGISTRY_AUTH_HTPASSWD_REALM: basic-realm
          REGISTRY_AUTH_HTPASSWD_PATH: /auth/.htpasswd
          REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data
        volumes:
          - ./.docker-registry.htpasswd:/auth/.htpasswd
          - ./data:/data
    
      docker-registry-ui:
        image: konradkleine/docker-registry-frontend:v2
        restart: always
        ports:
          - "53986:80"
        environment:
          VIRTUAL_HOST: '*, https://*'
          ENV_DOCKER_REGISTRY_HOST: 'docker-registry'
          ENV_DOCKER_REGISTRY_PORT: 5000
        links:
          - 'docker-registry'
    

  3. Taħt SSH, se niġġeneraw il-fajl .htpasswd għall-awtorizzazzjoni Bażika fir-repożitorju Docker:
    htpasswd -bBc .htpasswd hw_docker_admin hw_docker_password
  4. Iġbor u erfa' kontenituri:
    docker-compose up -d
  5. U rridu nidderieġu mill-ġdid Nginx għall-kontenituri tagħna. Dan jista' jsir permezz ta' Plesk.

Jeħtieġ li jsiru l-passi li ġejjin għas-sottodominji docker.helloworld.ru u docker-ui.helloworld.ru:

Fit-taqsima Għodda Dev is-sit tagħna mur Regoli tal-Prokura Docker:

CI/CD f'Azzjonijiet Github għal proġett Flask+Angular
U żid regola għall-prokura tat-traffiku deħlin fil-kontenitur tagħna:

CI/CD f'Azzjonijiet Github għal proġett Flask+Angular

  1. Aħna niċċekkjaw li nistgħu nidħlu fil-kontenitur tagħna mill-magna lokali:
    $ docker login docker.helloworld.ru -u hw_docker_admin -p hw_docker_password
    WARNING! Using --password via the CLI is insecure. Use --password-stdin.
    Login Succeeded
  2. Ejja niċċekkja wkoll l-operat tas-sottodominju docker-ui.helloworld.ru:

    CI/CD f'Azzjonijiet Github għal proġett Flask+Angular
    Meta tikklikkja fuq Ibbrawżja repożitorji, il-brawżer juri tieqa ta' awtorizzazzjoni fejn ikollok bżonn iddaħħal l-isem tal-utent u l-password għar-repożitorju. Wara dan, se nkunu trasferiti għal paġna b'lista ta' repożitorji (għalissa, se tkun vojta għalik):

    CI/CD f'Azzjonijiet Github għal proġett Flask+Angular

Ftuħ portijiet fil-Plesk Firewall

Wara l-installazzjoni u l-konfigurazzjoni tal-komponenti, għandna bżonn niftħu portijiet sabiex il-komponenti jkunu aċċessibbli mill-kontenituri Docker u min-netwerk estern.

Ejja naraw kif tagħmel dan billi tuża l-estensjoni tal-Firewall għal Plesk li installajna qabel.

  1. Mur fuq Għodda u Settings > Settings > Firewall:
    CI/CD f'Azzjonijiet Github għal proġett Flask+Angular
  2. Mur fuq Immodifika r-Regoli tal-Firewall Plesk > Żid Regola Custom u tiftaħ il-portijiet TCP li ġejjin għas-subnet Docker (172.0.0.0 / 8):
    RabbitMQ: 1883, 4369, 5671-5672, 25672, 61613-61614
    Redis: 32785, 32786

    CI/CD f'Azzjonijiet Github għal proġett Flask+Angular

  3. Aħna se nżidu wkoll regola li tiftaħ il-portijiet PostgreSQL u l-pannelli ta 'ġestjoni RabbitMQ għad-dinja ta' barra:

    CI/CD f'Azzjonijiet Github għal proġett Flask+Angular

  4. Applika r-regoli billi tuża l-buttuna Applika Bidliet:

    CI/CD f'Azzjonijiet Github għal proġett Flask+Angular

Twaqqif ta 'CI/CD f'Azzjonijiet Github

Ejja ninżlu għall-aktar parti interessanti - inwaqqfu pipeline ta 'integrazzjoni kontinwa u nwasslu l-proġett tagħna lis-server.

Dan il-pipeline se jikkonsisti f'żewġ partijiet:

  • bini ta 'immaġni u t-tmexxija ta' testijiet (għall-backend) - fuq in-naħa Github;
  • tmexxija migrazzjonijiet (għall-backend) u skjerament kontenituri - fuq is-server.

Uża għal Plesk

Ejja nittrattaw it-tieni punt l-ewwel (għax l-ewwel wieħed jiddependi minnu).

Aħna ser tikkonfigura l-proċess ta 'skjerament billi tuża l-estensjoni Git għal Plesk.

Ikkunsidra eżempju b'ambjent Prod għal repożitorju Backend.

  1. Immorru fis-sottoskrizzjoni tal-websajt Helloworld tagħna u mmorru fis-subsezzjoni Git:

    CI/CD f'Azzjonijiet Github għal proġett Flask+Angular

  2. Daħħal link għar-repożitorju Github tagħna fil-qasam "Repożitorju Remote Git" u ibdel il-folder default httpdocs lil ieħor (eż. /httpdocs/hw_back):

    CI/CD f'Azzjonijiet Github għal proġett Flask+Angular

  3. Ikkopja ċ-ċavetta pubblika SSH mill-pass preċedenti u żid huwa fis-settings ta 'Github.
  4. Ikklikkja OK fuq l-iskrin fil-pass 2, u wara se nkunu diretti mill-ġdid lejn il-paġna tar-repożitorju fi Plesk. Issa rridu nikkonfiguraw ir-repożitorju biex jiġi aġġornat fuq il-kommessi mal-fergħa prinċipali. Biex tagħmel dan, mur fuq Settings tar-Repożitorju u ħlief il-valur Webhook URL (se jkollna bżonnha aktar tard meta nwaqqfu Github Actions):

    CI/CD f'Azzjonijiet Github għal proġett Flask+Angular

  5. Fil-qasam Azzjonijiet fuq l-iskrin mill-paragrafu preċedenti, daħħal l-iskrittura biex tniedi l-iskjerament:
    cd {REPOSITORY_ABSOLUTE_PATH}
    .ci-cd/deploy.sh {ENV} {DOCKER_REGISTRY_HOST} {DOCKER_USER} {DOCKER_PASSWORD} {TG_BOT_TOKEN} {TG_CHAT_ID} 

    fejn:

    {REPOSITORY_ABSOLUTE_PATH} - mogħdija għall-prod folder tar-repożitorju backend fuq is-server;
    {ENV} - ambjent (dev / prod), fil-każ tagħna prod;
    {DOCKER_REGISTRY_HOST} - l-ospitant tar-repożitorju tad-docker tagħna
    {TG_BOT_TOKEN} — Telegram bot token;
    {TG_CHAT_ID} — ID taċ-chat/kanal biex jintbagħtu n-notifiki.

    Eżempju ta' skript:

    cd /var/www/vhosts/helloworld.ru/httpdocs/hw_back/
    .ci-cd/deploy.sh dev docker.helloworld.ru docker_user docker_password 12345678:AAbcdEfghCH1vGbCasdfSAs0K5PALDsaw -1001234567890
  6. Żid utent mill-abbonament tagħna mal-grupp Docker (sabiex ikunu jistgħu jimmaniġġjaw kontenituri):
    sudo usermod -aG docker helloworld_admin

L-ambjent dev għar-repożitorju backend u l-frontend huma stabbiliti bl-istess mod.

Pipeline ta 'skjerament f'Azzjonijiet Github

Ejja nkomplu biex inwaqqfu l-ewwel parti tal-pipeline tagħna CI/CD f'Azzjonijiet Github.

Backend

Il-pipeline huwa deskritt fi fajl deploy.yml.

Iżda qabel ma naqsbuh, ejja nimlew il-varjabbli Sigrieti li għandna bżonn f'Github. Biex tagħmel dan, mur fuq Settings -> Sigrieti:

  • DOCKER_REGISTRY - l-ospitant tar-repożitorju Docker tagħna (docker.helloworld.ru);
  • DOCKER_LOGIN - illoginja fir-repożitorju Docker;
  • DOCKER_PASSWORD - password għaliha;
  • DEPLOY_HOST — host fejn il-pannell tal-amministrazzjoni Plesk huwa disponibbli (eżempju: helloworld.com: 8443 jew 123.4.56.78:8443);
  • DEPLOY_BACK_PROD_TOKEN - token għall-iskjerament fil-prod-repożitorju fuq is-server (ksibna fl-Iskjerament fi Plesk p. 4);
  • DEPLOY_BACK_DEV_TOKEN - token għall-iskjerament fir-repożitorju tad-dev fuq is-server.

Il-proċess tal-iskjerament huwa sempliċi u jikkonsisti fi tliet passi ewlenin:

  • il-bini u l-pubblikazzjoni tal-immaġni fir-repożitorju tagħna;
  • it-twettiq ta' testijiet f'kontenitur ibbażat fuq immaġni mibnija friska;
  • skjerament għall-ambjent mixtieq skond il-fergħa (dev/master).

frontend

Il-fajl deploy.yml għar-repożitorju ta' quddiem ftit differenti minn Beck's. Huwa nieqes minn pass bit-tmexxija tat-testijiet u jibdel l-ismijiet tat-tokens għall-iskjerament. Sigrieti għar-repożitorju ta 'quddiem, mill-mod, jeħtieġ li jimtlew separatament.

Setup tas-sit

Proxy tat-traffiku permezz ta' Nginx

Ukoll, wasalna fl-aħħar. Jibqa 'biss li jiġi kkonfigurat il-proxying tat-traffiku deħlin u ħerġin għall-kontenitur tagħna permezz ta' Nginx. Aħna diġà koprejna dan il-proċess fil-pass 5 tas-setup tar-Reġistru Docker. L-istess għandu jiġi ripetut għall-partijiet ta 'wara u ta' quddiem f'ambjenti dev u prod.

Se nipprovdi screenshots tas-settings.

Backend

CI/CD f'Azzjonijiet Github għal proġett Flask+Angular

frontend

CI/CD f'Azzjonijiet Github għal proġett Flask+Angular
Kjarifika importanti. L-URLs kollha se jkunu proxy għall-kontenitur tal-frontend, ħlief dawk li jibdew bihom /api/ - se jkunu prokurati mal-kontenitur ta’ wara (hekk fil-kontenitur ta 'wara, il-handlers kollha għandhom jibdew bihom /api/).

Riżultati ta '

Issa s-sit tagħna għandu jkun disponibbli fuq helloworld.ru u dev.helloworld.ru (prod- u dev-environments, rispettivament).

B'kollox, tgħallimna kif nippreparaw applikazzjoni sempliċi f'Flask u Angular u waqqafna pipeline f'Azzjonijiet Github biex niftħuha għal server li jmexxi Plesk.

Se nidduplika l-links għar-repożitorji bil-kodiċi: backend, il-parti ta 'quddiem.

Sors: www.habr.com

Żid kumment