From 86d9e60dd6c33c48ee8713668094fb0c8c3ae379 Mon Sep 17 00:00:00 2001 From: Patrick de Ruiter Date: Mon, 17 Nov 2025 08:25:38 +0100 Subject: [PATCH] feat: Add CI/CD pipeline and SonarQube configuration Added comprehensive Gitea Actions pipeline with: - TFLint for Terraform linting - Tfsec for security scanning - Checkov for policy validation - Terraform validate for syntax checking - SonarQube integration for code quality analysis - Terraform plan/apply workflow with MinIO artifact storage - Terraform destroy workflow with manual approval Pipeline Features: - Runs on push to main and pull requests - Sequential job execution with proper dependencies - Secure secrets management for Vault, MinIO, and Renovate - Plan artifact storage in MinIO for apply jobs - Production environment protection for apply - Destroy approval environment for safety - Support for destroy via PR label SonarQube Configuration: - Project metadata and version tracking - Terraform-specific exclusions - Proper source encoding - Documentation links to Gitea repository Required Secrets: - VAULT_ROLE_ID, VAULT_SECRET_ID, VAULT_ADDR - MINIO_ACCESS_KEY, MINIO_SECRET_KEY, MINIO_ENDPOINT, MINIO_BUCKET - RENOVATE_ENDPOINT, RENOVATE_TOKEN - SONARQUBE_HOST, SONARQUBE_TOKEN --- .gitea/workflows/sonarqube.yaml | 312 ++++++++++++++++++++++++++++++++ sonar-project.properties | 25 +++ 2 files changed, 337 insertions(+) create mode 100644 .gitea/workflows/sonarqube.yaml create mode 100644 sonar-project.properties diff --git a/.gitea/workflows/sonarqube.yaml b/.gitea/workflows/sonarqube.yaml new file mode 100644 index 0000000..ff5cf6a --- /dev/null +++ b/.gitea/workflows/sonarqube.yaml @@ -0,0 +1,312 @@ +on: + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened] + +name: Code Quality & Security Scan +jobs: + tflint: + name: TFLint + runs-on: ubuntu-latest + steps: + - name: Checking out + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup TFLint + uses: terraform-linters/setup-tflint@v4 + with: + tflint_version: latest + + - name: Initialize TFLint + run: tflint --init + + - name: Run TFLint + run: tflint --format compact + + tfsec: + name: Tfsec Security Scan + runs-on: ubuntu-latest + needs: tflint + steps: + - name: Checking out + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Run Tfsec + uses: aquasecurity/tfsec-action@v1.0.3 + with: + format: default + soft_fail: false + + checkov: + name: Checkov Security Scan + runs-on: ubuntu-latest + needs: tfsec + steps: + - name: Checking out + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Run Checkov + uses: bridgecrewio/checkov-action@v12 + with: + directory: . + framework: terraform + output_format: cli + soft_fail: false + + terraform-validate: + name: Terraform Validate + runs-on: ubuntu-latest + needs: checkov + steps: + - name: Checking out + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: latest + + - name: Terraform Format Check + run: terraform fmt -check -recursive + + - name: Terraform Init (validation only) + env: + TF_VAR_role_id: ${{ secrets.VAULT_ROLE_ID }} + TF_VAR_secret_id: ${{ secrets.VAULT_SECRET_ID }} + TF_VAR_renovate_endpoint: "https://gitea.example.com/api/v1/" + TF_VAR_renovate_token: "dummy-token-for-validation" + VAULT_ADDR: ${{ secrets.VAULT_ADDR }} + run: terraform init -backend=false + + - name: Terraform Validate + env: + TF_VAR_role_id: ${{ secrets.VAULT_ROLE_ID }} + TF_VAR_secret_id: ${{ secrets.VAULT_SECRET_ID }} + TF_VAR_renovate_endpoint: "https://gitea.example.com/api/v1/" + TF_VAR_renovate_token: "dummy-token-for-validation" + VAULT_ADDR: ${{ secrets.VAULT_ADDR }} + run: terraform validate + + sonarqube: + name: SonarQube Trigger + runs-on: ubuntu-latest + needs: terraform-validate + steps: + - name: Checking out + uses: actions/checkout@v4 + with: + # Disabling shallow clone is recommended for improving relevancy of reporting + fetch-depth: 0 + - name: SonarQube Scan + uses: sonarsource/sonarqube-scan-action@v6 + env: + SONAR_HOST_URL: ${{ secrets.SONARQUBE_HOST }} + SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }} + + terraform-plan: + name: Terraform Plan + runs-on: ubuntu-latest + needs: sonarqube + steps: + - name: Checking out + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: latest + + - name: Terraform Init + env: + AWS_ACCESS_KEY_ID: ${{ secrets.MINIO_ACCESS_KEY }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.MINIO_SECRET_KEY }} + TF_BACKEND_ENDPOINT: ${{ secrets.MINIO_ENDPOINT }} + TF_VAR_role_id: ${{ secrets.VAULT_ROLE_ID }} + TF_VAR_secret_id: ${{ secrets.VAULT_SECRET_ID }} + TF_VAR_renovate_endpoint: ${{ secrets.RENOVATE_ENDPOINT }} + TF_VAR_renovate_token: ${{ secrets.RENOVATE_TOKEN }} + VAULT_ADDR: ${{ secrets.VAULT_ADDR }} + run: | + terraform init -input=false + + - name: Terraform Plan + env: + AWS_ACCESS_KEY_ID: ${{ secrets.MINIO_ACCESS_KEY }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.MINIO_SECRET_KEY }} + TF_VAR_role_id: ${{ secrets.VAULT_ROLE_ID }} + TF_VAR_secret_id: ${{ secrets.VAULT_SECRET_ID }} + TF_VAR_renovate_endpoint: ${{ secrets.RENOVATE_ENDPOINT }} + TF_VAR_renovate_token: ${{ secrets.RENOVATE_TOKEN }} + VAULT_ADDR: ${{ secrets.VAULT_ADDR }} + run: | + terraform plan -input=false -out=tfplan + terraform show -no-color tfplan > tfplan.txt + + - name: Upload Terraform Plan to MinIO + env: + AWS_ACCESS_KEY_ID: ${{ secrets.MINIO_ACCESS_KEY }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.MINIO_SECRET_KEY }} + MINIO_ENDPOINT: ${{ secrets.MINIO_ENDPOINT }} + MINIO_BUCKET: ${{ secrets.MINIO_BUCKET }} + run: | + # Install AWS CLI for S3-compatible operations + curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" + unzip -q awscliv2.zip + sudo ./aws/install + + # Upload plan files to MinIO + PLAN_PATH="terraform-plans/${{ github.repository }}/${{ github.run_number }}" + aws s3 cp tfplan "s3://${MINIO_BUCKET}/${PLAN_PATH}/tfplan" \ + --endpoint-url="${MINIO_ENDPOINT}" + aws s3 cp tfplan.txt "s3://${MINIO_BUCKET}/${PLAN_PATH}/tfplan.txt" \ + --endpoint-url="${MINIO_ENDPOINT}" + + echo "Plan uploaded to: s3://${MINIO_BUCKET}/${PLAN_PATH}/" + + terraform-apply: + name: Terraform Apply + runs-on: ubuntu-latest + needs: terraform-plan + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + environment: + name: production + steps: + - name: Checking out + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: latest + + - name: Install AWS CLI + run: | + curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" + unzip -q awscliv2.zip + sudo ./aws/install + + - name: Terraform Init + env: + AWS_ACCESS_KEY_ID: ${{ secrets.MINIO_ACCESS_KEY }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.MINIO_SECRET_KEY }} + TF_VAR_role_id: ${{ secrets.VAULT_ROLE_ID }} + TF_VAR_secret_id: ${{ secrets.VAULT_SECRET_ID }} + TF_VAR_renovate_endpoint: ${{ secrets.RENOVATE_ENDPOINT }} + TF_VAR_renovate_token: ${{ secrets.RENOVATE_TOKEN }} + VAULT_ADDR: ${{ secrets.VAULT_ADDR }} + run: terraform init + + - name: Download Terraform Plan from MinIO + env: + AWS_ACCESS_KEY_ID: ${{ secrets.MINIO_ACCESS_KEY }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.MINIO_SECRET_KEY }} + MINIO_ENDPOINT: ${{ secrets.MINIO_ENDPOINT }} + MINIO_BUCKET: ${{ secrets.MINIO_BUCKET }} + run: | + PLAN_PATH="terraform-plans/${{ github.repository }}/${{ github.run_number }}" + aws s3 cp "s3://${MINIO_BUCKET}/${PLAN_PATH}/tfplan" tfplan \ + --endpoint-url="${MINIO_ENDPOINT}" + echo "Plan downloaded from: s3://${MINIO_BUCKET}/${PLAN_PATH}/tfplan" + + - name: Terraform Apply + env: + AWS_ACCESS_KEY_ID: ${{ secrets.MINIO_ACCESS_KEY }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.MINIO_SECRET_KEY }} + TF_VAR_role_id: ${{ secrets.VAULT_ROLE_ID }} + TF_VAR_secret_id: ${{ secrets.VAULT_SECRET_ID }} + TF_VAR_renovate_endpoint: ${{ secrets.RENOVATE_ENDPOINT }} + TF_VAR_renovate_token: ${{ secrets.RENOVATE_TOKEN }} + VAULT_ADDR: ${{ secrets.VAULT_ADDR }} + run: terraform apply -input=false -auto-approve tfplan + + terraform-destroy: + name: Terraform Destroy + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'destroy') + environment: + name: destroy-approval + steps: + - name: Verify Destroy Authorization + run: | + echo "⚠️ CRITICAL: INFRASTRUCTURE DESTRUCTION REQUESTED" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "PR: ${{ github.event.pull_request.html_url }}" + echo "Requested by: ${{ github.actor }}" + echo "Repository: ${{ github.repository }}" + echo "Branch: ${{ github.head_ref }}" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + echo "This action will PERMANENTLY DESTROY the Renovate container" + echo "and all associated resources managed by this Terraform configuration." + echo "" + echo "Waiting for manual approval via environment protection rules..." + + - name: Checking out + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: latest + + - name: Terraform Init (Fresh - No Cache) + env: + AWS_ACCESS_KEY_ID: ${{ secrets.MINIO_ACCESS_KEY }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.MINIO_SECRET_KEY }} + TF_VAR_role_id: ${{ secrets.VAULT_ROLE_ID }} + TF_VAR_secret_id: ${{ secrets.VAULT_SECRET_ID }} + TF_VAR_renovate_endpoint: ${{ secrets.RENOVATE_ENDPOINT }} + TF_VAR_renovate_token: ${{ secrets.RENOVATE_TOKEN }} + VAULT_ADDR: ${{ secrets.VAULT_ADDR }} + run: | + echo "Performing fresh terraform init (no cache for safety)..." + terraform init + + - name: Terraform Destroy Plan + env: + AWS_ACCESS_KEY_ID: ${{ secrets.MINIO_ACCESS_KEY }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.MINIO_SECRET_KEY }} + TF_VAR_role_id: ${{ secrets.VAULT_ROLE_ID }} + TF_VAR_secret_id: ${{ secrets.VAULT_SECRET_ID }} + TF_VAR_renovate_endpoint: ${{ secrets.RENOVATE_ENDPOINT }} + TF_VAR_renovate_token: ${{ secrets.RENOVATE_TOKEN }} + VAULT_ADDR: ${{ secrets.VAULT_ADDR }} + run: | + echo "Generating destroy plan..." + terraform plan -input=false -destroy -out=destroy.tfplan + echo "" + echo "Destroy plan generated. Review the plan above carefully." + + - name: Terraform Destroy Execute + env: + AWS_ACCESS_KEY_ID: ${{ secrets.MINIO_ACCESS_KEY }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.MINIO_SECRET_KEY }} + TF_VAR_role_id: ${{ secrets.VAULT_ROLE_ID }} + TF_VAR_secret_id: ${{ secrets.VAULT_SECRET_ID }} + TF_VAR_renovate_endpoint: ${{ secrets.RENOVATE_ENDPOINT }} + TF_VAR_renovate_token: ${{ secrets.RENOVATE_TOKEN }} + VAULT_ADDR: ${{ secrets.VAULT_ADDR }} + run: | + echo "🔥 DESTROYING INFRASTRUCTURE..." + echo "This cannot be undone!" + echo "" + terraform apply -input=false -auto-approve destroy.tfplan + echo "" + echo "✅ Renovate infrastructure has been destroyed" + echo "State file updated in MinIO" diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..8e696e2 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,25 @@ +sonar.projectKey=terraform-docker-renovate +sonar.projectName=Terraform Docker Renovate Module +sonar.projectVersion=2.0.0 + +# Source code location +sonar.sources=. +sonar.exclusions=**/.terraform/**,**/.git/**,**/files/**,**/*.md,**/.gitea/** + +# File encoding +sonar.sourceEncoding=UTF-8 + +# Terraform specific settings +sonar.language=terraform + +# Coverage exclusions +sonar.coverage.exclusions=**/*.tf,**/*.tfvars + +# Test exclusions (if you add tests later) +sonar.test.exclusions=**/tests/** + +# Documentation +sonar.links.homepage=https://git.bsdserver.nl/gitea-admin/terraform-docker-renovate +sonar.links.ci=https://git.bsdserver.nl/gitea-admin/terraform-docker-renovate/actions +sonar.links.scm=https://git.bsdserver.nl/gitea-admin/terraform-docker-renovate +sonar.links.issue=https://git.bsdserver.nl/gitea-admin/terraform-docker-renovate/issues