DEV Community

Gaurav Tayade
Gaurav Tayade

Posted on

Building a Production-Ready SonarQube Scanner Plugin for Devtron CI/CD

Building a Production-Ready SonarQube Scanner Plugin for Devtron CI/CD

How to build a reusable, secure, and language-agnostic SonarQube scanner plugin that enforces code quality gates across all application pipelines in Devtron.

TL;DR

  • Built a custom Devtron plugin for SonarQube scanning
  • Works across all languages (Java, Python, JS, TS, Go)
  • Enforces Quality Gates and blocks deployments on failure
  • Zero token exposure in pipeline logs
  • One plugin reusable across all apps with minimal config

Introduction

When managing multiple application pipelines using Devtron an open-source Kubernetes-native CI/CD platform, teams often need a consistent way to enforce code quality standards across all applications without asking every team to set up SonarQube from scratch.

The challenge? Devtron's native SonarQube plugin can have limitations. This post walks through building a custom production-ready, reusable plugin that solves these challenges.


Why Build a Custom Plugin

  1. Native plugin limitations - The built-in Devtron SonarQube plugin may have authentication issues
  2. No modification access - You cannot modify Devtron's native plugins
  3. Reusability - One plugin all teams can use without writing their own scripts
  4. Security - Proper secret handling and token validation before every scan

Architecture Overview

Devtron CI Pipeline
        |
        |-- Pre-build Stage
        |   |-- SonarQube Scanner Plugin (custom)
        |       |-- Container Image  <- sonar-scanner binary
        |       |-- Shell Script     <- logic
        |       |-- sonar-project.properties <- per app repo
        |
        |-- Build Stage
        |   |-- Docker Image Build
        |
        |-- Post-build Stage
            |-- Deploy
Enter fullscreen mode Exit fullscreen mode

Key Insight: Separate the binary from the logic. The container image carries the sonar-scanner binary, while the shell script mounted at runtime by Devtron contains all the logic. This means you can update the script without rebuilding the image!


The Container Image

FROM alpine:3.20

RUN apk add --no-cache \
    curl unzip openjdk17-jre bash \
    nodejs npm libstdc++ libgcc \
    && rm -rf /var/cache/apk/*

RUN curl -sSLo /tmp/sonar.zip \
    "https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-6.2.1.4610-linux-x64.zip" && \
    unzip -q /tmp/sonar.zip -d /opt && \
    mv /opt/sonar-scanner-6.2.1.4610-linux-x64 /opt/sonar-scanner && \
    rm /tmp/sonar.zip && \
    rm -rf /opt/sonar-scanner/jre && \
    sed -i 's/use_embedded_jre=true/use_embedded_jre=false/' \
    /opt/sonar-scanner/bin/sonar-scanner

ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk
ENV PATH="/opt/sonar-scanner/bin:/usr/lib/jvm/java-17-openjdk/bin:$PATH"
ENV SONAR_SCANNER_OPTS="-Dsonar.nodejs.executable=/usr/bin/node"
Enter fullscreen mode Exit fullscreen mode

Common Gotchas

1. Alpine needs BOTH libstdc++ AND libgcc

Missing either causes:

Error loading shared library libstdc++.so.6: No such file or directory
Enter fullscreen mode Exit fullscreen mode

2. Never use $(which node) in ENV instruction

# Wrong
ENV SONAR_SCANNER_OPTS="-Dsonar.nodejs.executable=$(which node)"

# Correct
ENV SONAR_SCANNER_OPTS="-Dsonar.nodejs.executable=/usr/bin/node"
Enter fullscreen mode Exit fullscreen mode

The Shell Script

Script Flow

1. Strip trailing slash from URL
2. Validate required variables
3. Security checks (token format, HTTPS)
4. Connectivity check
5. Token authentication via API
6. Source code validation
7. Run sonar-scanner
8. Check Quality Gate via API
9. Report status in logs
Enter fullscreen mode Exit fullscreen mode

Security Handling

# Never print the token
set +x

# Validate token BEFORE scan
AUTH=$(curl --silent -u "$SONAR_TOKEN:" \
  "$SONAR_HOST_URL/api/authentication/validate")

if ! echo "$AUTH" | grep -q '"valid":true'; then
  echo "Token authentication FAILED!"
  exit 1
fi

# Filter token from scanner output
/opt/sonar-scanner/bin/sonar-scanner $SCANNER_ARGS 2>&1 | \
  grep -v "sonar.token" | \
  grep -v "sonar.login"
Enter fullscreen mode Exit fullscreen mode

Fix Trailing Slash Issue

SonarQube returns HTML instead of JSON when the URL has a trailing slash causing auth to fail even with a valid token!

# Always strip trailing slash first
SONAR_HOST_URL=$(echo "$SONAR_HOST_URL" | sed 's:/*$::')
Enter fullscreen mode Exit fullscreen mode

Quality Gate Control

# FAIL_ON_QUALITY_GATE=false is useful for onboarding legacy projects
FAIL_ON_QUALITY_GATE="${FAIL_ON_QUALITY_GATE:-true}"

if [ "$QG_STATUS" = "ERROR" ]; then
  if [ "$FAIL_ON_QUALITY_GATE" = "false" ]; then
    echo "WARNING: Quality Gate failed but pipeline continues"
  else
    exit 1
  fi
fi
Enter fullscreen mode Exit fullscreen mode

Devtron Plugin Configuration

Plugin Form Settings

Field Value
Task Name SonarQube Scanner with Quality Gate
Plugin ID sonarqube-scanner-quality-gate
Version 1.0.0
Mount Code At /scripts/sonar-scan.sh
Command /bin/sh
Args /scripts/sonar-scan.sh
Mount Code to Container Yes

Input Variables

Variable Type Required Default
SONAR_TOKEN Secret Yes -
SONAR_HOST_URL String Yes -
SONAR_PROJECT_KEY String Yes -
SONAR_PROJECT_NAME String No Same as key
SONAR_SOURCES String No /sourcecode
SONAR_JAVA_BINARIES String No .
SONAR_EXCLUSIONS String No -
BRANCH_NAME String No -
QUALITY_GATE_TIMEOUT String No 300
FAIL_ON_QUALITY_GATE String No true

Always use Secret type for SONAR_TOKEN - tokens appear in plain text in logs when using String type!


Per-App Configuration

Each app just needs one file added to its repo root.

JavaScript/TypeScript:

sonar.projectKey=my-app
sonar.projectName=My App
sonar.sources=src
sonar.exclusions=**/node_modules/**,**/coverage/**,**/*.test.js
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.sourceEncoding=UTF-8
Enter fullscreen mode Exit fullscreen mode

Python:

sonar.projectKey=my-python-app
sonar.projectName=My Python App
sonar.language=py
sonar.python.version=3
sonar.sources=.
sonar.exclusions=**/__pycache__/**,**/*.pyc,**/venv/**
sonar.python.coverage.reportPaths=coverage.xml
sonar.sourceEncoding=UTF-8
Enter fullscreen mode Exit fullscreen mode

Java Maven:

sonar.projectKey=my-java-app
sonar.projectName=My Java App
sonar.sources=src/main/java
sonar.java.binaries=target/classes
sonar.tests=src/test/java
sonar.sourceEncoding=UTF-8
Enter fullscreen mode Exit fullscreen mode

Project Onboarding Automation

For each new project we automate everything via SonarQube API:

sh sonar-onboard.sh

# Output:
# Project created: my-app
# Group created: my-app-developers
# User added to group
# Permissions assigned
# Project set to PRIVATE
#
# CI TOKEN: sqa_xxxxxxxxxxxxx
# Add to Devtron as SONAR_TOKEN secret
Enter fullscreen mode Exit fullscreen mode

Challenges and Solutions

Challenge Solution
Native plugin auth issues Custom shell script plugin
Token exposed in logs set +x and grep filter
Trailing slash in URL Strip with sed first
Alpine missing C++ libs Add libstdc++ and libgcc
Node.js path issue Hardcode /usr/bin/node
Quality Gate blocking new projects FAIL_ON_QUALITY_GATE=false
/bin/sh vs /bin/bash Full POSIX sh compatible script

Results

  • One plugin reusable across all apps and languages
  • Zero token exposure in pipeline logs
  • Consistent quality gates across all teams
  • New app onboarded in under 10 minutes
  • Flexible gradual adoption with FAIL_ON_QUALITY_GATE

What's Next

  • Coverage reports (JaCoCo, pytest-cov, lcov)
  • Branch and PR analysis
  • Slack notifications on Quality Gate failure
  • Custom Grafana dashboard for scan trends

Resources


If this helped you drop a reaction and share with your team!

Top comments (0)