Задача – запустить 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" } } }
Запускам, проверяем:
Окей – работает.
Но тут возникает проблема:
- Jenkins запущен в Docker-контейнере в сети jenkins_jenkins
- SonarQube запущен в Docker-контейнере в сети jenkins_jenkins
- 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" } } }
Готово.