SonarQube: запуск в Docker и вызов из Jenkins Pipeline

Автор: | 18/06/2019
 

Задача – запустить SonarQube, что бы Jenkins выполнял проверку кода.

Jenkins работает в Docker, билды запускаются тоже Docker.

Основная проблема, которая возникла во время запуска SonarQube из Docker Compose, это то, что контейнер с SonarQube внутри себя запускает процесс с Elastisearch (что, вроде как, нарушает главный принцип использования конейнеров – 1 сервис на один контейнер).

При этом Elasticsearch не может быть запущен от root, поэтому пришлось повозиться с пользователем и точками монтирования.

Docker Compose для SonarQube

Проверяем пользователя по умолчанию в образе SonarQube:

[simterm]

root@jenkins-dev:/opt/sonarqube# docker run -ti sonarqube id
uid=999(sonarqube) gid=999(sonarqube) groups=999(sonarqube)

[/simterm]

Создаём каталоги на хосте, в которых будем хранить данные Sonar:

[simterm]

root@jenkins-dev:~# mkdir -p /data/sonarqube/{conf,logs,temp,data,extensions,bundled_plugins,postgresql,postgresql_data}

[/simterm]

Создаём пользователя, и меняем владельца каталогов:

[simterm]

root@jenkins-dev:~# adduser sonarqube
root@jenkins-dev:~# usermod -aG docker sonarqube
root@jenkins-dev:~# chown -R sonarqube:sonarqube /data/sonarqube/

[/simterm]

Находим UID пользователя:

[simterm]

root@jenkins-dev:/opt/sonarqube# id sonarqube
uid=1004(sonarqube) gid=1004(sonarqube) groups=1004(sonarqube),999(docker)

[/simterm]

Пишем Compose файл, и используем этот UID в user:

version: "3"

networks:
  sonarnet:
    driver: bridge

services:
  sonarqube:
    // use UID here
    user: 1004:1004
    image: sonarqube
    ports:
      - "9000:9000"
    networks:
      - sonarnet
    environment:
      - sonar.jdbc.url=jdbc:postgresql://db:5432/sonar
    volumes:
      - /data/sonarqube/conf:/opt/sonarqube/conf
      - /data/sonarqube/logs:/opt/sonarqube/logs
      - /data/sonarqube/temp:/opt/sonarqube/temp
      - /data/sonarqube/data:/opt/sonarqube/data
      - /data/sonarqube/extensions:/opt/sonarqube/extensions
      - /data/sonarqube/bundled_plugins:/opt/sonarqube/lib/bundled-plugins

  db:
    image: postgres
    networks:
      - sonarnet
    environment:
      - POSTGRES_USER=sonar
      - POSTGRES_PASSWORD=sonar
    volumes:
      - /data/sonarqube/postgresql:/var/lib/postgresql
      - /data/sonarqube/postgresql_data:/var/lib/postgresql/data

Проверяем:

[simterm]

root@jenkins-dev:/opt/sonarqube# docker-compose -f sonarqube-compose.yml up
Starting sonarqube_db_1          ... done
Recreating sonarqube_sonarqube_1 ... done
...
sonarqube_1  | 2019.06.14 15:33:46 INFO  app[][o.s.a.SchedulerImpl] Process[ce] is up
sonarqube_1  | 2019.06.14 15:33:46 INFO  app[][o.s.a.SchedulerImpl] SonarQube is up

[/simterm]

NGINX

Тепреь настроим NGINX и SSL для работы с веб-интерфейсом SonarQube.

Останавливаем NGINX, устанавливаем Let’s Encrypt клиент, получаем сертификат (см. детальнее в посте Bitwarden: менеджер паролей организации — установка self-hosted версии на AWS EC2):

[simterm]

root@jenkins-dev:/opt/sonarqube# systemctl stop nginx
root@jenkins-dev:/opt/sonarqube# /opt/letsencrypt/letsencrypt-auto certonly -d sonar.example.com
root@jenkins-dev:/opt/sonarqube# systemctl start nginx

[/simterm]

Создаём файл настроек виртуалхоста, тут просто копируя уже существующий:

[simterm]

root@jenkins-dev:/opt/sonarqube# cp /etc/nginx/conf.d/ci.example.com.conf /etc/nginx/conf.d/sonar.example.com.conf

[/simterm]

Обновляем, приводим к примерно такому виду:

upstream sonar {
    server 127.0.0.1:9000;
}


server {

    listen 80;
    server_name  dev.sonar.example.com;

    # Lets Encrypt Webroot
    location ~ /.well-known {

    root /var/www/html;
        allow all;
    }

    location / {
        return 301 https://dev.sonar.example.com;
    }
}

server {

    listen       443 ssl;
    server_name  dev.sonar.example.com;

    access_log  /var/log/nginx/dev.sonar.example.com-access.log proxy;
    error_log /var/log/nginx/dev.sonar.example.com-error.log warn;

    ssl_certificate /etc/letsencrypt/live/dev.sonar.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/dev.sonar.example.com/privkey.pem;

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;
    ssl_dhparam /etc/nginx/dhparams.pem;
    ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:ECDHE-RSA-AES128-GCM-SHA256:AES256+EECDH:DHE-RSA-AES128-GCM-SHA256:AES256+EDH:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
    ssl_session_timeout 1d;
    ssl_stapling on;
    ssl_stapling_verify on;

    location / {
        proxy_http_version 1.1;
        proxy_request_buffering off;
        proxy_buffering off;

        proxy_redirect          off;
        proxy_set_header        Host            $host;
        proxy_set_header        X-Real-IP       $remote_addr;
        proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header        X-Forwarded-Proto   $scheme;
        proxy_pass http://sonar$request_uri;
    }
}

Проверяем синтаксис и перезагружаем конфиги NGINX:

[simterm]

root@jenkins-dev:/home/admin# nginx -t && systemctl start nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

[/simterm]

Jenkins Docker Compose 

Теперь обновляем Docker Compose файл самого Jenkins – переносим в него SonarQube и PostgreSQL:

version: '3'

networks:
 jenkins:

services:

  jenkins:
    user: root
    image: jenkins/jenkins:2.164.3
    networks:
      - jenkins
    ports:
      - '8080:8080'
      - '50000:50000'
    volumes:
      - /data/jenkins:/var/lib/jenkins
      - /var/run/docker.sock:/var/run/docker.sock
      - /usr/bin/docker:/usr/bin/docker
      - /usr/lib/x86_64-linux-gnu/libltdl.so.7:/usr/lib/x86_64-linux-gnu/libltdl.so.7
    environment:
      - JENKINS_HOME=/var/lib/jenkins
      - JAVA_OPTS=-Duser.timezone=Europe/Kiev
      - JENKINS_JAVA_OPTIONS="-Djava.awt.headless=true -Dhudson.model.DirectoryBrowserSupport.CSP=\"default-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' 'unsafe-inline' data:;\""
    logging:
      driver: "journald"

  sonarqube:
    user: 1004:1004
    image: sonarqube
    ports:
      - "9000:9000"
    networks:
      - jenkins
    environment:
      - sonar.jdbc.url=jdbc:postgresql://db:5432/sonar
    volumes:
      - /data/sonarqube/conf:/opt/sonarqube/conf
      - /data/sonarqube/logs:/opt/sonarqube/logs
      - /data/sonarqube/temp:/opt/sonarqube/temp
      - /data/sonarqube/data:/opt/sonarqube/data
      - /data/sonarqube/extensions:/opt/sonarqube/extensions
      - /data/sonarqube/bundled_plugins:/opt/sonarqube/lib/bundled-plugins
    logging:
      driver: "journald"

  db:
    image: postgres
    networks:
      - jenkins
    environment:
      - POSTGRES_USER=sonar
      - POSTGRES_PASSWORD=sonar
    volumes:
      - /data/sonarqube/postgresql:/var/lib/postgresql
      - /data/sonarqube/postgresql_data:/var/lib/postgresql/data
    logging:
      driver: "journald"

Перезапускаем Jenkins:

[simterm]

root@jenkins-dev:/opt/jenkins# systemctl restart jenkins

[/simterm]

Проверяем контейнеры:

[simterm]

root@jenkins-dev:/home/admin# docker ps
CONTAINER ID        IMAGE                     COMMAND                  CREATED             STATUS              PORTS                                              NAMES
fc2662391c45        sonarqube                 "./bin/run.sh"           48 seconds ago      Up 46 seconds       0.0.0.0:9000->9000/tcp                             jenkins_sonarqube_1
3ac2bb5f0e87        postgres                  "docker-entrypoint.s…"   48 seconds ago      Up 46 seconds       5432/tcp                                           jenkins_db_1
113496304b0f        jenkins/jenkins:2.164.3   "/sbin/tini -- /usr/…"   48 seconds ago      Up 46 seconds       0.0.0.0:8080->8080/tcp, 0.0.0.0:50000->50000/tcp   jenkins_jenkins_1

[/simterm]

Переходим в Sonar:

Логинимся с admin:admin.

Настройка Jenkins

Обычно в Jenkins устанавливается плагин SonarQube Scanner, но мы будем запускать сам Scanner из Docker-образа в самой джобе.

Настройка билда

Создаём Pipeline задачу:

Создаём скрипт:

node {
    
    stage('Clone repo') {
        git branch: "develop", url: "[email protected]:example-dev/example-me.git", credentialsId: "jenkins-github"
    }
    
    stage('SonarTests') {
        docker.image('newtmitch/sonar-scanner').inside('-v /var/run/docker.sock:/var/run/docker.sock') {
            sh "--version"
        }
    }
}

И снова сталкиваемся с ENTRYPOINT ошибкой:

$ docker top d2269fe30970490ba9957ac57701ec6091f3c9cbf78f957e903a3319fa1445bd -eo pid,comm
ERROR: The container started but didn’t run the expected command. Please double check your ENTRYPOINT does execute the command passed as docker run argument, as required by official docker images (see https://github.com/docker-library/official-images#consistency for entrypoint consistency requirements).
Alternatively you can force image entrypoint to be disabled by adding option `–entrypoint=”`.

Повторяем костыль из поста Jenkins: запуск PHPUnit из Codeception по Pull Reguest в Github и Allure-репорты.

Запускаем контейнер локально, переопределяем entrypoint в bash:

[simterm]

$ docker run -ti --entrypoint="bash" newtmitch/sonar-scanner 
root@20baaae7de9a:/usr/src#

[/simterm]

Находим исполняемый файл:

[simterm]

root@20baaae7de9a:/usr/src# which sonar-scanner 
/usr/local/bin/sonar-scanner

[/simterm]

Обновляем скрипт – указываем --entrypoint="", а в вызове – полный путь к файлу /usr/local/bin/sonar-scanner:

node {
    
    stage('Clone repo') {
        git branch: "develop", url: "[email protected]:example-dev/example-me.git", credentialsId: "jenkins-github"
    }
    
    stage('SonarTests') {
        docker.image('newtmitch/sonar-scanner').inside('-v /var/run/docker.sock:/var/run/docker.sock --entrypoint=""') {
            sh "/usr/local/bin/sonar-scanner --version"
        }
    }
}

Запускам, проверяем:

Окей – работает.

Но тут возникает проблема:

  1. Jenkins запущен в Docker-контейнере в сети jenkins_jenkins
  2. SonarQube запущен в Docker-контейнере в сети jenkins_jenkins
  3. Jenkins внутри запускает контейнер со sonar-scanner, который должен получить доступ к SonarQube, который работает во “внешней” для контейнера со сканером сети jenkins_jenkins

Проверяем сети на хосте с Jenkins:

[simterm]

root@jenkins-dev:/home/admin# docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
67900babbbc4        bridge              bridge              local
d51bc8ee54d0        host                host                local
30b091d801d6        jenkins_jenkins     bridge              local
16ab0c37234e        none                null                local

[/simterm]

Решение – обновляем параметры запуска контейнера – задаём ему запуск в сети jenkins_jenkins (--net jenkins_jenkins):

node {
    
    stage('Clone repo') {
        git branch: "develop", url: "[email protected]:example-dev/example-me.git", credentialsId: "jenkins-github"
    }
    
    stage('SonarTests') {
        docker.image('newtmitch/sonar-scanner').inside('-v /var/run/docker.sock:/var/run/docker.sock --entrypoint="" --net jenkins_jenkins') {
            sh "/usr/local/bin/sonar-scanner -Dsonar.host.url=http://sonarqube:9000 -Dsonar.sources=."
        }
    }
}

Запускаем:

Окей – теперь сканер запускается, к самому SonarQube подключиться может.

Осталось добавить какой-то реальный проект и тест.

Настройка проекта в SonarQube

Добавляем проект:

Создаём токен, для конкретного проекта:

Выбираем платформу, и Sonar сам подскажет опции:

Обновляем, запускаем, и:

ERROR: Error during SonarQube Scanner execution
ERROR: No quality profiles have been found, you probably don’t have any language plugin installed.

Окей…

Переходим в Sonar-е в  Administration > Marketplace, добавляем плагин SonarPHP:

Перезапускаем Sonar:

И проверяем доступные теперь плагины:

[simterm]

root@jenkins-dev:/home/admin# curl localhost:9000/api/plugins/installed
{"plugins":[{"key":"php","name":"SonarPHP","description":"Code Analyzer for PHP","version":"3.0.0.4537","license":"GNU LGPL v3","organizationName":"SonarSource and Akram Ben Aissi","editionBundled":false,"homepageUrl":"http://redirect.sonarsource.com/plugins/php.html","issueTrackerUrl":"http://jira.codehaus.org/browse/SONARPHP","implementationBuild":"026dee08c29a3689ab1228552e14bfefda9ae57e","updatedAt":1560775404315,"filename":"sonar-php-plugin-3.0.0.4537.jar","sonarLintSupported":true,"hash":"c80c0d053f074a9147d341cf1357d994"}]}

[/simterm]

“name”:”SonarPHP”,”description”:”Code Analyzer for PHP”

Хорошо, установлен, запускаем джобу:

И результаты в SonarQube:

Sonar Scanner: 0 files indexed

Но почему 0 scanned?


INFO: Indexing files…
INFO: Project configuration:
INFO: 0 files indexed

Что-то с маппингом каталогов.

Попробуем задать sonar.sources и sonar.projectBaseDir явно:

...
    stage('SonarTests') {
        docker.image('newtmitch/sonar-scanner').inside('-v /var/run/docker.sock:/var/run/docker.sock --entrypoint="" --net jenkins_jenkins') {
            sh "/usr/local/bin/sonar-scanner -Dsonar.host.url=http://sonarqube:9000 -Dsonar.sources=/var/lib/jenkins/workspace/SonarTest -Dsonar.projectBaseDir=/var/lib/jenkins/workspace/SonarTest -Dsonar.projectKey=ProjectName -Dsonar.login=91a691e7c91154d3fee69a05a8fa6e2b10bc82a6 -Dsonar.verbose=true"
        }
    }

Запускаем:

Работает.

Ещё раз результаты в самом SonarQube:

Осталось привести в нормальный вид сам билд.

Собственно, проблема с файлами, судя по всему, возникает оттого, что Jenkins мапит текущий каталог под этим же путём в контейнер, а затем задаёт его как --workdir:


$ docker run […] -w /var/lib/jenkins/workspace/SonarTest

Тогда как sonar-scanner пытается их найти по дефолтному пути для Based dir:


09:08:56.666 INFO: Base dir: /usr/src

Попробуем переопределить sonar.projectBaseDir значением “.“, т.е. текущая директория.

Теперь скрипт выглядит так:

node {
    
    stage('Clone repo') {
        git branch: "develop", url: "[email protected]:example-dev/example-me.git", credentialsId: "jenkins-github"
    }
    
    stage('SonarTests') {
        docker.image('newtmitch/sonar-scanner').inside('-v /var/run/docker.sock:/var/run/docker.sock --entrypoint="" --net jenkins_jenkins') {
            sh "/usr/local/bin/sonar-scanner -Dsonar.host.url=http://sonarqube:9000 -Dsonar.projectBaseDir=. -Dsonar.projectKey=ProjectName -Dsonar.login=91a691e7c91154d3fee69a05a8fa6e2b10bc82a6"
        }
    }
}

Запускаем:


INFO: Base dir: /var/lib/jenkins/workspace/SonarTest
INFO: Working dir: /var/lib/jenkins/workspace/SonarTest/.scannerwork …
INFO: EXECUTION SUCCESS

Что бы навести немного порядок в скрипте и разделить опции для разных проектов – все настройки Sonar можно вынести в файл sonar-project.properties в корне Github-репозитория проекта:

sonar.host.url=http://sonarqube:9000
sonar.projectBaseDir=.
sonar.projectKey=ProjectName
sonar.login=91a691e7c91154d3fee69a05a8fa6e2b10bc82a6

И обновить скрипт, убрав из него опции для SonarScanner – теперь он будет искать файл sonar-project.properties, и использовать настройки из него:

node {
    
    stage('Clone repo') {
        git branch: "develop", url: "[email protected]:example-dev/example-me.git", credentialsId: "jenkins-github"
    }
    
    stage('SonarTests') {
        docker.image('newtmitch/sonar-scanner').inside('-v /var/run/docker.sock:/var/run/docker.sock --entrypoint="" --net jenkins_jenkins') {
            sh "/usr/local/bin/sonar-scanner"
        }
    }
}

Готово.