CloudRun Python -Installing Sonarqube and Adding Tests

Published: Oct 1, 2022 by Isaac Johnson

Now that we have a simple cloud run function deployed that sends notifications, we should improve things by adding some semblance of test coverage.

And as long as we intend to capture test coverage, we might as well capture other metrics using Sonarqube.

Today we’ll start by setting up Sonarqube in our cluster then do the steps necessary to add it as an “App” in our user account.

We’ll then add Python tests with pytests and lastly integration with test coverage results sent to Sonarqube.

Installing with Helm

The easiest way to setup Sonarqube is to use Helm

$ helm repo add sonarqube https://SonarSource.github.io/helm-chart-sonarqube
"sonarqube" has been added to your repositories

$ helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "longhorn" chart repository
...Successfully got an update from the "confluentinc" chart repository
...Successfully got an update from the "dapr" chart repository
...Successfully got an update from the "adwerx" chart repository
...Successfully got an update from the "novum-rgi-helm" chart repository
...Successfully got an update from the "uptime-kuma" chart repository
...Successfully got an update from the "hashicorp" chart repository
...Successfully got an update from the "rhcharts" chart repository
...Successfully got an update from the "kuma" chart repository
...Successfully got an update from the "actions-runner-controller" chart repository
...Successfully got an update from the "sonarqube" chart repository
...Successfully got an update from the "kubecost" chart repository
...Successfully got an update from the "sumologic" chart repository
...Successfully got an update from the "epsagon" chart repository
...Successfully got an update from the "azure-samples" chart repository
...Successfully got an update from the "nginx-stable" chart repository
...Successfully got an update from the "crossplane-stable" chart repository
...Successfully got an update from the "datadog" chart repository
...Successfully got an update from the "argo-cd" chart repository
...Successfully got an update from the "lifen-charts" chart repository
...Successfully got an update from the "harbor" chart repository
...Successfully got an update from the "rook-release" chart repository
...Successfully got an update from the "rancher-latest" chart repository
...Successfully got an update from the "incubator" chart repository
...Successfully got an update from the "myharbor" chart repository
...Successfully got an update from the "newrelic" chart repository
...Successfully got an update from the "gitlab" chart repository
...Successfully got an update from the "bitnami" chart repository
Update Complete. ⎈Happy Helming!⎈

$ helm upgrade --install sonarqube sonarqube/sonarqube
Release "sonarqube" does not exist. Installing it now.
NAME: sonarqube
LAST DEPLOYED: Fri Sep 23 06:18:03 2022
NAMESPACE: default
STATUS: deployed
REVISION: 1
NOTES:
1. Get the application URL by running these commands:
  export POD_NAME=$(kubectl get pods --namespace default -l "app=sonarqube,release=sonarqube" -o jsonpath="{.items[0].metadata.name}")
  echo "Visit http://127.0.0.1:8080 to use your application"
  kubectl port-forward $POD_NAME 8080:9000 -n default

However, this did not install the PostgreSQL database (as it should).

try again

$ helm uninstall sonarqube
WARNING: Kubernetes configuration file is group-readable. This is insecure. Location: /home/builder/.kube/config
WARNING: Kubernetes configuration file is world-readable. This is insecure. Location: /home/builder/.kube/config
release "sonarqube" uninstalled

builder@DESKTOP-QADGF36:~$ helm install sonarqube-ce bitnami/sonarqube
WARNING: Kubernetes configuration file is group-readable. This is insecure. Location: /home/builder/.kube/config
WARNING: Kubernetes configuration file is world-readable. This is insecure. Location: /home/builder/.kube/config
NAME: sonarqube-ce
LAST DEPLOYED: Fri Sep 23 06:26:43 2022
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
** Please be patient while the chart is being deployed **

Your SonarQube site can be accessed through the following DNS name from within your cluster:

    sonarqube-ce.default.svc.cluster.local (port 80)

To access your SonarQube site from outside the cluster follow the steps below:

1. Get the SonarQube URL by running these commands:

  NOTE: It may take a few minutes for the LoadBalancer IP to be available.
        Watch the status with: 'kubectl get svc --namespace default -w sonarqube-ce'

   export SERVICE_IP=$(kubectl get svc --namespace default sonarqube-ce --template "{{ range (index .status.loadBalancer.ingress 0) }}{{ . }}{{ end }}")
   echo "SonarQube URL: http://$SERVICE_IP/"

2. Open a browser and access SonarQube using the obtained URL.

3. Login with the following credentials below:

  echo Username: user
  echo Password: $(kubectl get secret --namespace default sonarqube-ce -o jsonpath="{.data.sonarqube-password}" | base64 -d)

In a little bit we see the postgresql pods come up

$ kubectl get pods
NAME                            READY   STATUS            RESTARTS   AGE
hello-openshift-pod             1/1     Running           0          4d
www                             1/1     Running           0          3d23h
www2                            1/1     Running           0          3d23h
sonarqube-ce-74d48f97b8-sj65z   0/1     PodInitializing   0          86s
sonarqube-ce-postgresql-0       1/1     Running           0          86s

To login we need our user (user) and first time password:

$ kubectl get secret --namespace default sonarqube-ce -o jsonpath="{.data.sonarqube-password}" | base64 -d && echo
1ewjA0Mnwl

In a normal cluster, we would get an external-ip immediately satisfied

$ kubectl get svc --namespace default sonarqube-ce
NAME           TYPE           CLUSTER-IP    EXTERNAL-IP   PORT(S)                       AGE
sonarqube-ce   LoadBalancer   10.43.2.101   <pending>     80:32259/TCP,9001:30534/TCP   4m38s

However, for testing, I’ll just port-forward to the service

$ kubectl port-forward svc/sonarqube-ce 8080:80

/content/images/2022/09/sonarqube-01.png

trying adding a mmapfs statement

builder@DESKTOP-QADGF36:~$ diff t.yaml t.yaml.bak
100,101d99
<         - name: SONARQUBE_ELASTICSEARCH_JAVA_ADD_OPTS
<           value: "-Dnode.store.allow_mmapfs=false"
builder@DESKTOP-QADGF36:~$ kubectl apply -f t.yaml
Warning: resource deployments/sonarqube-ce2 is missing the kubectl.kubernetes.io/last-applied-configuration annotation which is required by kubectl apply. kubectl apply should only be used on resources created declaratively by either kubectl create --save-config or kubectl apply. The missing annotation will be patched automatically.
deployment.apps/sonarqube-ce2 configured

That failed then i tried

diff t.yaml t.yaml.bak
103c103
<           value: -Dnode.store.allow_mmap=false
---
>           value: -Dnode.store.allow_mmapfs=false

It was at this time I realized it eventually did come up the first time

builder@DESKTOP-QADGF36:~$ kubectl get pods
NAME                             READY   STATUS    RESTARTS        AGE
hello-openshift-pod              1/1     Running   0               4d
www                              1/1     Running   0               4d
www2                             1/1     Running   0               3d23h
sonarqube-ce2-postgresql-0       1/1     Running   0               7m40s
sonarqube-ce2-79b45589b7-wtj6d   1/1     Running   1 (3m47s ago)   7m40s
sonarqube-ce2-5fd7889df9-wck9k   0/1     Running   0               16s

We can now port-forward to configure the service

$ kubectl port-forward svc/sonarqube-ce 8080:80
Forwarding from 127.0.0.1:8080 -> 9000
Forwarding from [::1]:8080 -> 9000
Handling connection for 8080
Handling connection for 8080

/content/images/2022/09/sonarqube-02.png

At this point, I’ll pause to switch to my primary cluster. I would rather install Sonarqube as an externally accessible system.

This means after I run the helm install there

$ helm install sonarqube-ce --set service.type=ClusterIP bitnami/sonarqube
NAME: sonarqube-ce
LAST DEPLOYED: Fri Sep 23 07:05:14 2022
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
** Please be patient while the chart is being deployed **

Your SonarQube site can be accessed through the following DNS name from within your cluster:

    sonarqube-ce.default.svc.cluster.local (port 80)

To access your SonarQube site from outside the cluster follow the steps below:

1. Get the SonarQube URL by running these commands:

   kubectl port-forward --namespace default svc/sonarqube-ce 80:80 &
   echo "SonarQube URL: http://127.0.0.1/"

2. Open a browser and access SonarQube using the obtained URL.

3. Login with the following credentials below:

  echo Username: user
  echo Password: $(kubectl get secret --namespace default sonarqube-ce -o jsonpath="{.data.sonarqube-password}" | base64 -d)

I’ll wait to see them startup

$ kubectl get pods -l app.kubernetes.io/instance=sonarqube-ce
NAME                           READY   STATUS    RESTARTS   AGE
sonarqube-ce-postgresql-0      1/1     Running   0          106s
sonarqube-ce-c9c856d9c-cxvzf   0/1     Running   0          106s

$ kubectl get svc -l app.kubernetes.io/instance=sonarqube-ce
NAME                         TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)                       AGE
sonarqube-ce-postgresql-hl   ClusterIP      None            <none>        5432/TCP                      2m16s
sonarqube-ce-postgresql      ClusterIP      10.43.131.54    <none>        5432/TCP                      2m16s
sonarqube-ce                 LoadBalancer   10.43.235.116   <pending>     80:31220/TCP,9001:30671/TCP   2m16s

I’ll add an Ingress

$ cat sonarqubeIngress.yml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/tls-acme: "true"
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    nginx.ingress.kubernetes.io/proxy-body-size: "0"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.org/client-max-body-size: "0"
    nginx.org/proxy-connect-timeout: "600"
    nginx.org/proxy-read-timeout: "600"
  name: sonarqube
  namespace: default
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - sonarqube.freshbrewed.science
    secretName: sonarqube-tls
  rules:
  - host: sonarqube.freshbrewed.science
    http:
      paths:
      - backend:
          service:
            name: sonarqube-sonarqube
            port:
              number: 9000
        path: /
        pathType: ImplementationSpecific
$ kubectl apply -f sonarqubeIngress.yml
ingress.networking.k8s.io/sonarqube created

The database pod came up but the Sonarqube application pod continued to crash

sonarqube-ce-postgresql-0                              1/1     Running            0                131m
sonarqube-ce-6c79fc565b-rb9c4                          0/1     CrashLoopBackOff   24 (13s ago)     123m
builder@DESKTOP-QADGF36:~$ kubectl exec -it sonarqube-ce-6c79fc565b-rb9c4 -- /bin/bash
Defaulted container "sonarqube" out of: sonarqube, sysctl (init)
error: unable to upgrade connection: container not found ("sonarqube")
builder@DESKTOP-QADGF36:~$ kubectl exec -it sonarqube-ce-6c79fc565b-rb9c4 -- /bin/bash
Defaulted container "sonarqube" out of: sonarqube, sysctl (init)
error: unable to upgrade connection: container not found ("sonarqube")
builder@DESKTOP-QADGF36:~$ kubectl exec -it sonarqube-ce-6c79fc565b-rb9c4 -- /bin/bash
Defaulted container "sonarqube" out of: sonarqube, sysctl (init)
error: unable to upgrade connection: container not found ("sonarqube")

I used an interactive shell to test, but again, none of the passwords worked. Clearly the step to configure PostgreSQL was not occurring

$ kubectl run my-shell --rm -i --tty --image ubuntu -- bash
If you don't see a command prompt, try pressing enter.
root@my-shell:/# apt update && apt install -y postgresdb-client

Root@my-shell:/# psql -h sonarqube-ce-postgresql.svc.default.cluster.local -d postgres -U bn_sonarqube
psql: error: could not translate host name "sonarqube-ce-postgresql.svc.default.cluster.local" to address: Name or service not known
root@my-shell:/# psql -h 10.43.18.19 -d postgres -U bn_sonarqube
Password for user bn_sonarqube:
psql: error: connection to server at "10.43.18.19", port 5432 failed: FATAL:  password authentication failed for user "bn_sonarqube"
root@my-shell:/# psql -h 10.43.18.19 -d postgres -U bn_sonarqube
Password for user bn_sonarqube:
psql: error: connection to server at "10.43.18.19", port 5432 failed: fe_sendauth: no password supplied
root@my-shell:/# psql -h 10.43.18.19 -d postgres -U bn_sonarqube
Password for user bn_sonarqube:
psql: error: connection to server at "10.43.18.19", port 5432 failed: FATAL:  password authentication failed for user "bn_sonarqube"

A little later….

A little later, to try and capture logs, I tried again these same commands on a freshly spun v1.23.10 cluster and it worked smooth. Perhaps a former PVC / Longhorn demo had affected things the first time

builder@DESKTOP-QADGF36:~$ helm install sonarqube-ce --set service.type=ClusterIP bitnami/sonarqube
NAME: sonarqube-ce
LAST DEPLOYED: Sat Sep 24 08:54:29 2022
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
** Please be patient while the chart is being deployed **

Your SonarQube site can be accessed through the following DNS name from within your cluster:

    sonarqube-ce.default.svc.cluster.local (port 80)

To access your SonarQube site from outside the cluster follow the steps below:

1. Get the SonarQube URL by running these commands:

   kubectl port-forward --namespace default svc/sonarqube-ce 80:80 &
   echo "SonarQube URL: http://127.0.0.1/"

2. Open a browser and access SonarQube using the obtained URL.

3. Login with the following credentials below:

  echo Username: user
  echo Password: $(kubectl get secret --namespace default sonarqube-ce -o jsonpath="{.data.sonarqube-password}" | base64 -d)
builder@DESKTOP-QADGF36:~$ kubectl get pods
NAME                            READY   STATUS    RESTARTS   AGE
sonarqube-ce-postgresql-0       1/1     Running   0          5m32s
sonarqube-ce-54d68ddcff-r2txr   1/1     Running   0          5m32s

That said, let’s show how you do this in two steps; first the database then the application

Install postgres

builder@DESKTOP-QADGF36:~$ helm repo add bitnami https://charts.bitnami.com/bitnami
"bitnami" already exists with the same configuration, skipping
builder@DESKTOP-QADGF36:~$ helm install mypostgres bitnami/postgresql
NAME: mypostgres
LAST DEPLOYED: Fri Sep 23 09:53:47 2022
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
CHART NAME: postgresql
CHART VERSION: 11.9.1
APP VERSION: 14.5.0

** Please be patient while the chart is being deployed **

PostgreSQL can be accessed via port 5432 on the following DNS names from within your cluster:

    mypostgres-postgresql.default.svc.cluster.local - Read/Write connection

To get the password for "postgres" run:

    export POSTGRES_PASSWORD=$(kubectl get secret --namespace default mypostgres-postgresql -o jsonpath="{.data.postgres-password}" | base64 -d)

To connect to your database run the following command:

    kubectl run mypostgres-postgresql-client --rm --tty -i --restart='Never' --namespace default --image docker.io/bitnami/postgresql:14.5.0-debian-11-r14 --env="PGPASSWORD=$POSTGRES_PASSWORD" \
      --command -- psql --host mypostgres-postgresql -U postgres -d postgres -p 5432

    > NOTE: If you access the container using bash, make sure that you execute "/opt/bitnami/scripts/postgresql/entrypoint.sh /bin/bash" in order to avoid the error "psql: local user with ID 1001} does not exist"

To connect to your database from outside the cluster execute the following commands:

    kubectl port-forward --namespace default svc/mypostgres-postgresql 5432:5432 &
    PGPASSWORD="$POSTGRES_PASSWORD" psql --host 127.0.0.1 -U postgres -d postgres -p 5432

Once PostgreSQL is installed, we need to create a database and user, and give the user privileges on that database

builder@DESKTOP-QADGF36:~$ kubectl run mypostgres-postgresql-client --rm --tty -i --restart='Never' --namespace default --image docker.io/bitnami/postgresql:14.5.0-debian-11-r14 --env="PGPASSWORD=XUq7rTf5cL" --command -- psql --host mypostgres-postgresql -U postgres -d postgres -p 5432
If you don't see a command prompt, try pressing enter.

postgres=# CREATE USER bn_sonarqube WITH PASSWORD 'mySonarPassword';
CREATE ROLE
postgres=# CREATE database sonarqube;
CREATE DATABASE
postgres=# grant all privileges on database sonarqube to bn_sonarqube;
GRANT
postgres=# exit
pod "mypostgres-postgresql-client" deleted

With our database requirement now sorted out, I’ll reinstall Sonarqube with helm, this time passing the settings

builder@DESKTOP-QADGF36:~$ helm install sonarqube-ce --set service.type=ClusterIP --set externalDatabase.host=mypostgres-postgresql.default.svc.cluster.local --set externalDatabase.user=bn_sonarqube --set externalDatabase.password=mySonarPassword --set postgresql.enabled=false bitnami/sonarqube
NAME: sonarqube-ce
LAST DEPLOYED: Fri Sep 23 10:04:42 2022
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
** Please be patient while the chart is being deployed **

Your SonarQube site can be accessed through the following DNS name from within your cluster:

    sonarqube-ce.default.svc.cluster.local (port 80)

To access your SonarQube site from outside the cluster follow the steps below:

1. Get the SonarQube URL by running these commands:

   kubectl port-forward --namespace default svc/sonarqube-ce 80:80 &
   echo "SonarQube URL: http://127.0.0.1/"

2. Open a browser and access SonarQube using the obtained URL.

3. Login with the following credentials below:

  echo Username: user
  echo Password: $(kubectl get secret --namespace default sonarqube-ce -o jsonpath="{.data.sonarqube-password}" | base64 -d)

Testing, I could now access it with the Ingress

/content/images/2022/09/sonarqube-03.png

Configuring Sonarqube

One of my first activities is to create a user outside the default

/content/images/2022/09/sonarqube-04.png

Then add that user to administrators

/content/images/2022/09/sonarqube-05.png

I can now log out and log back in

/content/images/2022/09/sonarqube-06.png

Setup with Github from Sonarqube

We can back in a scan with our container image, but we can also connect it directly to Github.

To do that, we chose Github from the “create your project” page

/content/images/2022/09/sonarqube-07.png

The first two fields are just a name and API URL

/content/images/2022/09/sonarqube-08.png

Create a Github App

The next fields we need in Sonar come from Github.

To get those, we create an “app” in our account.

The first step is to go to your settings

/content/images/2022/09/sonarqube-09.png

then developer settings

/content/images/2022/09/sonarqube-10.png

We’ll then choose to create a new GH App

/content/images/2022/09/sonarqube-11.png

We will give it a name (e.g. sqfreshbrewed) and description as well as URL

/content/images/2022/09/sonarqube-12.png

We will use our Base URL for both the callback and webhook

/content/images/2022/09/sonarqube-13.png

The key permissions we need to set

Permission Access
Checks Read & write
Metadata (this setting is automatically set by GitHub) Read-only
Pull Requests Read & write
Commit statuses Read-only
Contents Read-only

e.g.

/content/images/2022/09/sonarqube-14.png

We can also add (if setting up Github Auth)

Permission Category Permission Access
User Email Addresses Read-only
Organization Members Read-only
Organization Projects Read-only

e.g.

/content/images/2022/09/sonarqube-15.png

Once we create, we need to generate a Click secret

/content/images/2022/09/sonarqube-16.png

And create a private key:

/content/images/2022/09/sonarqube-17.png

(when clicked, downloads a .pem file)

back to Sonarqube

We can paste in those details, including the contents of the PEM file

/content/images/2022/09/sonarqube-18.png

This prompts an authorization flow

/content/images/2022/09/sonarqube-19.png

Ensure the App is installed into Integrations/Applications

/content/images/2022/09/sonarqube-20.png

Using

We can now go to Sonarqube, Pick Github and select our User ID from the organization to present a list of repos, both public and private

/content/images/2022/09/sonarqube-21.png

In this case, I’ll select the Cloud Function repo and choose “Set up selected repository”

In the next window

/content/images/2022/09/sonarqube-23.png

I’ll choose Github Actions which will walk us through the steps

/content/images/2022/09/sonarqube-24.png

We’ll create Repository secrets from Secrets/Actions

/content/images/2022/09/sonarqube-25.png

When set, we can see them listed

/content/images/2022/09/sonarqube-26.png

I added the properties file then added a step in my workflow

builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ git diff
diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml
index 2fd5928..ff56e23 100644
--- a/.github/workflows/github-actions.yml
+++ b/.github/workflows/github-actions.yml
@@ -16,6 +16,10 @@ jobs:
         run: |
           sed -i 's/SENDGRIDTOKENHERE/$/g' main.py
           sed -i 's/MSTEAMSTOKENHERE/$/g' main.py
+      - uses: sonarsource/sonarqube-scan-action@master
+        env:
+          SONAR_TOKEN: $
+          SONAR_HOST_URL: $
       - uses: RafikFarhad/push-to-gcr-github-action@v4.1
         with:
           gcloud_service_key: $ # can be base64 encoded or plain text
builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ cat sonar-project.properties
sonar.projectKey=idjohnson_cloudFunctionPython_AYNxjV-p3oTU84gY6sze

This triggered a scan

/content/images/2022/09/sonarqube-27.png

Which I could see running

/content/images/2022/09/sonarqube-28.png

The moment that step completed, I could see some results in Sonarqube

/content/images/2022/09/sonarqube-29.png

This is not a large project, the bulk is in the main.py

/content/images/2022/09/sonarqube-30.png

while my code editor will show 25, sonar lets me know that yes, there are 25 lines but only 17 Lines of actual Code

/content/images/2022/09/sonarqube-31.png

Furthermore, I can get the Cyclomatic and Cognative complexity of the repo

/content/images/2022/09/sonarqube-32.png

One Problem I see right away is that I ran the scan AFTER I had substituted the secret variables. This now means anyone who could view sonar can see my tokens

/content/images/2022/09/sonarqube-33.png

If I swap the sed and the scan

/content/images/2022/09/sonarqube-34.png

That should remedy the issue

/content/images/2022/09/sonarqube-35.png

Adding coverage

Getting test coverage took a few steps

Fast-forward
 .github/workflows/github-actions.yml | 51 ++++++++++++++++++++-------------------------------
 .gitignore                           |  2 ++
 conftest.py                          | 13 +++++++++++++
 main.py                              |  2 --
 requirements.txt                     |  5 ++++-
 sonar-project.properties             |  8 +++++++-
 test_main.py                         |  8 ++++++++

The github actions was updated:

name: GitHub Actions Docker Build
on: push

jobs:
  HostedActions:
    runs-on: ubuntu-latest
    steps:
      - run: echo "🎉 The job was automatically triggered by a $ event."
      - run: echo "🐧 This job is now running on a $ server hosted by GitHub!"
      - run: echo "🔎 The name of your branch is $ and your repository is $."
      - name: Check out repository code
        uses: actions/checkout@v2
      - run: echo "💡 The $ repository has been cloned to the runner."
      - run: echo "🖥️ The workflow is now ready to test your code on the runner."
      - name: Run Tests
        run: |
          set -x
          pip3 install -r requirements.txt
          python3 -m pytest
          
          python3 -m pytest -v --cov --cov-report xml:test-results/coverage.xml --junitxml=test-results/results.xml

      - uses: sonarsource/sonarqube-scan-action@master
        env:
          SONAR_TOKEN: $
          SONAR_HOST_URL: $

file: test_main.py

import json


def test_main(app, client):
    res = client.get('/')
    assert res.status_code == 200
    expected = "Hello hello World!"
    assert expected == res.get_data(as_text=True)

and it’s conftest.py

import pytest

from main import app as flask_app


@pytest.fixture
def app():
    yield flask_app


@pytest.fixture
def client(app):
    return app.test_client()

file: sonar-project.properties

sonar.projectKey=idjohnson_cloudFunctionPython_AYNxjV-p3oTU84gY6sze
sonar.language=py
sonar.core.codeCoveragePlugin=cobertura
sonar.python.coverage.reportPaths=test-results/coverage.xml
sonar.python.xunit.reportPaths=test-results/results.xml
sonar.exclusions=**/*.xml,*.xml
#/home/runner/work/cloudFunctionPython/cloudFunctionPython/test-results

The updated requirements

Flask==2.1.0
gunicorn==20.1.0
apprise==1.0.0
pytest==7.1.3
coverage==6.4.4
pytest-cov==3.0.0

then I added a .gitignore

test-results/
__pycache__/

/content/images/2022/09/sonarqube-36.png

And the details show in the graph

/content/images/2022/09/sonarqube-37.png

we can see the coverage on the specific file

/content/images/2022/09/sonarqube-38.png

One last optimization I’ll make is to limit my Sonarscans to just the main branch. As we are using the Community Edition (CE), scans of non-main branches will fail.

name: GitHub Actions Docker Build
on: push

jobs:
  HostedActions:
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository code
        uses: actions/checkout@v2
      - name: Run Tests
        run: |
          set -x
          pip3 install -r requirements.txt
          python3 -m pytest
          
          # pytest --junitxml=test-results/coverage.xml
          # python3 -m pytest --cov-report xml:test-results/results.xml
          # python3 -m pytest -v -o junit_family=xunit1 --cov-report xml:test-results/coverage.xml --junitxml=test-results/results.xml
          python3 -m pytest -v --cov --cov-report xml:test-results/coverage.xml --junitxml=test-results/results.xml

          pwd
          ls -ltra
          ls -ltra ./test-results || true 
          cat ./test-results/results.xml

          ls -ltra /home/runner/work/cloudFunctionPython/cloudFunctionPython/test-results
          
          echo $
          #pytest -v  --cov --cov-report=xml --cov-report=html
        if: contains('
            refs/heads/main
           ', github.ref)
      - uses: sonarsource/sonarqube-scan-action@master
        env:
          SONAR_TOKEN: $
          SONAR_HOST_URL: $
        if: contains('
            refs/heads/main
           ', github.ref)

  build-and-push:
    runs-on: ubuntu-latest
    needs: HostedActions
    if: github.ref == 'refs/heads/main'
    steps:
      - name: Check out repository code
        uses: actions/checkout@v2
      - name: Replace Secret Tokens in Main
        run: |
          sed -i 's/SENDGRIDTOKENHERE/$/g' main.py
          sed -i 's/MSTEAMSTOKENHERE/$/g' main.py
        
      - uses: RafikFarhad/push-to-gcr-github-action@v4.1
        with:
          gcloud_service_key: $ # can be base64 encoded or plain text
          registry: gcr.io
          project_id: myanthosproject2
          image_name: pythonfunction
          image_tag: latest,$
          dockerfile: ./Dockerfile
          context: .
      - id: 'auth'
        name: 'Authenticate to Google Cloud'
        uses: 'google-github-actions/auth@v0'
        with:
          credentials_json: '$'
      #- id: 'deploy'
      #  uses: 'google-github-actions/deploy-cloudrun@v0'
      #  with:
      #    service: 'namespaces/511842454269/services/hellofresh'
      #    image: 'gcr.io/myanthosproject2/pythonfunction:$'
      - name: 'Set up Cloud SDK'
        uses: 'google-github-actions/setup-gcloud@v0'

      # Run Gcloud Run Deploy as GCP gcloud command instead
      - id: 'gcloud'
        name: 'gcloud'
        run: |-
          gcloud run deploy namespaces/511842454269/services/hellofresh --image gcr.io/myanthosproject2/pythonfunction:$ --quiet --platform managed --region us-central1 --project myanthosproject2 --format json

Coverage Thresholds

We could pay for Sonarqube to apply a coverage threshold. This would be the easiest.

However, what if we just want some basic threshold test?

We could code that into a bash check

jobs:
  HostedActions:
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository code
        uses: actions/checkout@v2
      - name: Run Tests
        run: |
          set -x
          pip3 install -r requirements.txt
          python3 -m pytest

          python3 -m pytest -v --cov --cov-report xml:test-results/coverage.xml --junitxml=test-results/results.xml

          a=`cat ./test-results/coverage.xml | grep line-rate | grep 'name="."' | sed 's/^.*line-rate="\([^"]*\)".*/\1/' | tr -d '\n'`
          b=0.98

          if (( $(bc <<<"$a > $b") )); then 
              echo "The Coverage has met the threshold of $b"
          else
              echo "The Coverage has not met the minimum threshold. This build will terminate.  Please increase your coverage to build and deploy."
              exit 1
          fi
      - uses: sonarsource/sonarqube-scan-action@master
        env:
          SONAR_TOKEN: $
          SONAR_HOST_URL: $
        if: contains('
            refs/heads/main
           ', github.ref)

Essentially the above would require 98% coverage from pytest.

Which we can see failed:

/content/images/2022/09/sonarqube-39.png

As it is a 96.43%

/content/images/2022/09/sonarqube-40.png

A more reasonable check (80%)

  a=`cat ./test-results/coverage.xml | grep line-rate | grep 'name="."' | sed 's/^.*line-rate="\([^"]*\)".*/\1/' | tr -d '\n'`
  b=0.80

  if (( $(bc <<<"$a > $b") )); then 
      echo "The Coverage has met the threshold of $b"
  else
      echo "The Coverage has not met the minimum threshold. This build will terminate.  Please increase your coverage to build and deploy."
      exit 1
  fi

/content/images/2022/09/sonarqube-41.png

Adding code

To see these changes in effect, let’s add some code

builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ git checkout -b add_some_code
Switched to a new branch 'add_some_code'

Then I’ll add some new endpoints

/content/images/2022/09/sonarqube-42.png

Then push it

builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ git add main.py 
builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ git commit -m "add some routes"
[add_some_code 5399bbf] add some routes
 1 file changed, 21 insertions(+)
builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ git push
fatal: The current branch add_some_code has no upstream branch.
To push the current branch and set the remote as upstream, use

    git push --set-upstream origin add_some_code

builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ git push --set-upstream origin add_some_code
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 16 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 652 bytes | 652.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
remote: 
remote: Create a pull request for 'add_some_code' on GitHub by visiting:
remote:      https://github.com/idjohnson/cloudFunctionPython/pull/new/add_some_code
remote: 
To https://github.com/idjohnson/cloudFunctionPython.git
 * [new branch]      add_some_code -> add_some_code
Branch 'add_some_code' set up to track remote branch 'add_some_code' from 'origin'.

As I haven’t added any tests yet, this fails the coverage

/content/images/2022/09/sonarqube-43.png

And we can see how it fails in a PR

/content/images/2022/09/sonarqube-44.png

Limitations

Because the PR build uses its own branch, trying to cheat the threshold would work

  a=`cat ./test-results/coverage.xml | grep line-rate | grep 'name="."' | sed 's/^.*line-rate="\([^"]*\)".*/\1/' | tr -d '\n'`
  b=0.50

  if (( $(bc <<<"$a > $b") )); then 
      echo "The Coverage has met the threshold of $b"
  else
      echo "The Coverage has not met the minimum threshold. This build will terminate.  Please increase your coverage to build and deploy."
      exit 1
  fi

then we push

builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ git commit -m "try cheating the coverage threshold"
[add_some_code 78fdc1b] try cheating the coverage threshold
 1 file changed, 1 insertion(+), 1 deletion(-)
builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ git push
Enumerating objects: 9, done.
Counting objects: 100% (9/9), done.
Delta compression using up to 16 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (5/5), 414 bytes | 414.00 KiB/s, done.
Total 5 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To https://github.com/idjohnson/cloudFunctionPython.git
   780a22f..78fdc1b  add_some_code -> add_some_code

/content/images/2022/09/sonarqube-46.png

And we can see we only succeeded because the threshold was dropped to 50%.

/content/images/2022/09/sonarqube-45.png

This just means we have to watch for modifications to the threshold. I would argue tight controls is one thing you pay for with the Developer and higher licenses (and the Developer is just US$150/year presently)

Summary

We setup Sonarqube locally with proper Ingress. We then tied it to our Github as a proper App. Lastly, we set configured python tests. Finally we showed a basic way to add threshold checks.

Next time we’ll focus on other ways to run this containerized function.

sonarqube python testing

Have something to add? Feedback? You can use the feedback form

Isaac Johnson

Isaac Johnson

Cloud Solutions Architect

Isaac is a CSA and DevOps engineer who focuses on cloud migrations and devops processes. He also is a dad to three wonderful daughters (hence the references to Princess King sprinkled throughout the blog).

Theme built by C.S. Rhymes