diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 882ca3d..26b74b8 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -1,3 +1,4 @@ +--- name: Docker Image CI on: diff --git a/.github/workflows/helm-chart.yml b/.github/workflows/helm-chart.yml new file mode 100644 index 0000000..aca302b --- /dev/null +++ b/.github/workflows/helm-chart.yml @@ -0,0 +1,57 @@ +--- +name: Helm Chart CI + +on: + push: + branches: + - master + pull_request: + types: [opened, synchronize, reopened] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - env: + IMAGE_EDITION: community + - env: + IMAGE_EDITION: developer + steps: + - uses: actions/checkout@v4 + + - name: Set yaml value change dict with random generated secrets + run: | + echo "VALUE_CHANGES={\"[0].data.sonar_db_password\":\"$(echo ${RANDOM} | md5sum | head -c 16 | base64)\",\"[0].data.postgres_db_password\":\"$(echo ${RANDOM} | base64)\",\"[1].data.SONARQUBE_USERNAME\":\"$(echo admin | base64)\",\"[1].data.SONARQUBE_PASSWORD\":\"$(echo ${RANDOM} | md5sum | head -c 16 | base64)\"}" >> $GITHUB_ENV + + - name: Update values.yaml + uses: fjogeleit/yaml-update-action@v0.15.0 + with: + valueFile: "helm/deploy-ci.yaml" + commitChange: false + changes: ${{ env.VALUE_CHANGES }} + + - name: Start minikube + uses: medyagh/setup-minikube@latest + with: + driver: docker + container-runtime: containerd + timeout-minutes: 2 + + - name: Build and run chart + run: | + docker build --build-arg="IMAGE_EDITION=${{ matrix.env.IMAGE_EDITION }}" -t ci . + eval $(minikube -p minikube docker-env) + kubectl apply -f helm/deploy-ci.yaml + helm dependency build helm + helm upgrade --install --render-subchart-notes ictu-sonarqube helm + + - name: Wait for Sonar instance to start + # profile for language 'web' is the last; assume everything is working if we got this far + run: | + eval $(minikube -p minikube docker-env) + kubectl wait --all pods --timeout=4m --for=condition=Ready + kubectl wait --all statefulsets --timeout=30s --for=jsonpath=status.availableReplicas=1 + kubectl logs -f pod/ictu-sonarqube-sonarqube-0 |& sed "/Current profile for language 'web' is 'Sonar way'/ q" + timeout-minutes: 5 diff --git a/.github/workflows/helm-release.yml b/.github/workflows/helm-release.yml new file mode 100644 index 0000000..d32ca19 --- /dev/null +++ b/.github/workflows/helm-release.yml @@ -0,0 +1,23 @@ +--- +name: Release Helm chart + +on: workflow_dispatch # Only triggered manually + +jobs: + push_to_registry: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Package and push Helm chart to Docker Hub + run: | + cd helm + helm dependency build . + helm package . + helm push ictu-sonarqube-*.tgz oci://registry-1.docker.io/ictu diff --git a/.gitignore b/.gitignore index a09c56d..cffe8c4 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ /.idea +/helm/charts +/helm/Chart.lock + diff --git a/Dockerfile b/Dockerfile index b5cc3c4..bb05605 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,4 +29,6 @@ RUN chown -R sonarqube . USER sonarqube +HEALTHCHECK --start-period=5m CMD /src/health-check.sh + CMD ["/src/start-with-profile.sh"] diff --git a/MAINTENANCE.md b/MAINTENANCE.md index 8e382f0..5e3fb21 100644 --- a/MAINTENANCE.md +++ b/MAINTENANCE.md @@ -12,6 +12,9 @@ 1. `MAJOR.MINOR.PATCH` 1. `MAJOR.MINOR.PATCH-developer` 1. Build and push new images to docker hub with [CircleCI](https://app.circleci.com/pipelines/github/ICTU/sonar) +1. Update helm `Chart.yaml` with the new chart versions, corresponding with the new `appVersion` +1. Update the helm `values.yaml` with the new `ictu/sonar` image tag +1. Push the new chart as OCI artifact to docker hub `ictu/ictu-sonarqube`, with the GitHub action ## Adding plugins diff --git a/README.md b/README.md index c1d0dc1..e26ce0b 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,16 @@ The Sonar start script waits for the database to become available (only when usi Similarly `SONAR_START_TIMEOUT` (default: 600 seconds) defines how long the script should wait for Sonar to start up. +## Running on Kubernetes with the Helm chart + +The helm chart can be pulled as [ictu/ictu-sonarqube from Docker hub](https://hub.docker.com/r/ictu/ictu-sonarqube/tags) as OCI artifact: +``` +helm pull oci://registry-1.docker.io/ictu/ictu-sonarqube +``` + +As specified in the [Helm values.yaml](https://github.com/ICTU/sonar/blob/master/helm/values.yaml), two credentials `dbCredential` and `sonarCredential` can be used. +Additional environment variables can be passed to the SonarQube container through the `env` dict. + ## Get in touch Point of contact for this repository is [Dennie Bouman](https://github.com/denniebouman), who can be reached by [opening a new issue in this repository's issue tracker](https://github.com/ICTU/sonar/issues/new). diff --git a/helm/Chart.yaml b/helm/Chart.yaml new file mode 100644 index 0000000..7f1dedd --- /dev/null +++ b/helm/Chart.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: v2 +name: ictu-sonarqube +version: 10.5.1 +appVersion: "10.5.1" +description: A SonarQube helm chart with plugins, profiles and config used at ICTU +type: application +home: https://github.com/ICTU/sonar +dependencies: + - name: postgresql + version: 15.5.38 # this corresponds with appVersion 16.4.0, upstream sonarqube helm chart uses version 10.15.0 + repository: https://charts.bitnami.com/bitnami # https://github.com/bitnami/charts/blob/main/bitnami/postgresql/Chart.yaml + - name: sonarqube + version: 10.5.1 + repository: https://SonarSource.github.io/helm-chart-sonarqube # https://github.com/SonarSource/helm-chart-sonarqube/blob/master/charts/sonarqube/Chart.yaml diff --git a/helm/deploy-ci.yaml b/helm/deploy-ci.yaml new file mode 100644 index 0000000..b169391 --- /dev/null +++ b/helm/deploy-ci.yaml @@ -0,0 +1,24 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: sonarqube-postgresql-secret + labels: + app: sonarqube + release: sonarqube +data: + sonar_db_password: "" + postgres_db_password: "" +type: Opaque +--- +apiVersion: v1 +kind: Secret +metadata: + name: sonarqube-sonarqube-secret + labels: + app: sonarqube + release: sonarqube +data: + SONARQUBE_USERNAME: "" + SONARQUBE_PASSWORD: "" +type: Opaque diff --git a/helm/templates/sonarqube-env-vars-configmap.yaml b/helm/templates/sonarqube-env-vars-configmap.yaml new file mode 100644 index 0000000..30e877f --- /dev/null +++ b/helm/templates/sonarqube-env-vars-configmap.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Release.Name }}-sonarqube-env-vars + labels: + app: sonarqube + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +data: +{{- range $key, $val := .Values.env_vars }} + {{ $key }}: "{{ $val }}" +{{- end }} diff --git a/helm/values.yaml b/helm/values.yaml new file mode 100644 index 0000000..9169d50 --- /dev/null +++ b/helm/values.yaml @@ -0,0 +1,69 @@ +--- +secrets: + dbCredential: &dbCredential "sonarqube-postgresql-secret" + sonarCredential: &sonarCredential "sonarqube-sonarqube-secret" + +settings: + sonar.core.serverBaseURL: "test.local" + sonar.forceAuthentication: false + +postgresql: + audit: + logTimezone: "Europe/Amsterdam" + auth: + username: sonar + database: sonar + existingSecret: *dbCredential + secretKeys: + userPasswordKey: sonar_db_password + adminPasswordKey: postgres_db_password + primary: + persistence: + size: 1Gi + extraVolumes: + - name: tz-config + hostPath: + path: /etc/localtime + extraVolumeMounts: + - name: tz-config + mountPath: /etc/localtime + +sonarqube: + image: + repository: ictu/sonar + tag: "10.5.1" + pullPolicy: IfNotPresent + jdbcOverwrite: + enable: true + jdbcUrl: jdbc:postgresql://ictu-sonarqube-postgresql:5432/sonar?socketTimeout=1500 + jdbcUsername: sonar + jdbcSecretName: *dbCredential + jdbcSecretPasswordKey: sonar_db_password + nginx: + enabled: false + postgresql: + enabled: false + initSysctl: + enabled: false + initFs: + enabled: false + ingress: + enabled: false + + env: + - name: SONARQUBE_USERNAME + valueFrom: + secretKeyRef: + name: *sonarCredential + key: SONARQUBE_USERNAME + optional: true + - name: SONARQUBE_PASSWORD + valueFrom: + secretKeyRef: + name: *sonarCredential + key: SONARQUBE_PASSWORD + optional: true + + extraConfig: + configmaps: + - ictu-sonarqube-sonarqube-env-vars diff --git a/src/health-check.sh b/src/health-check.sh new file mode 100644 index 0000000..cdc2714 --- /dev/null +++ b/src/health-check.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# fetch api/system/status because api/system/health requires authentication +if ! status_result=$(curl -sf localhost:9000/api/system/status); then + exit 1 +fi + +# attempt to read json output data +if ! status_json=$(echo "${status_result}" | jq -r .status); then + exit 1 +fi + +# verify that the status is indeed UP +[[ "${status_json}" == "UP" ]] || exit 1 diff --git a/src/start-with-profile.sh b/src/start-with-profile.sh index 8965679..d6454d2 100644 --- a/src/start-with-profile.sh +++ b/src/start-with-profile.sh @@ -44,15 +44,21 @@ function waitForSonarUp { local count=0 local sleep=5 local timeout=${SONAR_START_TIMEOUT:-600} - # Wait for server to be up - until [[ "${status}" == "UP" ]] - do + # Wait for server status to be UP + until [[ "${status}" == "UP" ]]; do if [[ count -gt timeout ]]; then echo "ERROR: Failed to start Sonar within ${timeout} seconds" exit 1 fi - status=$(curl -s -f "${BASE_URL}/api/system/status" | jq -r ".status") - echo "Waiting for sonar to come up: ${status}" + + status=$(curl -sf "${BASE_URL}/api/system/status" | jq -r ".status") + if [[ "${status}" == "DB_MIGRATION_NEEDED" ]]; then + echo "Posting signal to migrate_db: ${status}" + curl -sf -XPOST "${BASE_URL}/api/system/migrate_db" + else + echo "Waiting for sonar to come up: ${status}" + fi + sleep $sleep count=$((count+sleep)) done