commit ed6a8560c53fb33e8fe4d68e56e2ef1a14a9ef5e Author: devops Date: Wed Sep 16 15:23:03 2026 +0000 devops-platform shared library: reusable pipeline for any module diff --git a/README.md b/README.md new file mode 100644 index 0000000..e04d48b --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# devops-platform — Jenkins Shared Library + +Kisi bhi module ke liye ek hi pipeline. Module ki `Jenkinsfile` sirf itni: + +```groovy +@Library('devops-platform') _ +devopsPipeline(module: 'demo-app') +``` + +## Kya-kya milta hai (sab default ON) + +| Stage | Tool | Fail hoga jab | +|---|---|---| +| Secret scan | gitleaks | poori git history me koi secret mila | +| Test | module ka test image | test fail | +| SAST | SonarQube | quality gate (Sonar side) | +| IaC/Dockerfile scan | trivy config | HIGH/CRITICAL misconfig | +| CVE gate | trivy image | HIGH/CRITICAL fixable CVE | +| SBOM | syft | — (artifact archive hota hai) | +| Smoke test | container | `/version` galat | +| Push | docker | — | +| Sign | cosign | — | +| Verify signature | cosign | signature nahi mili | +| Deploy + Verify | ansible | playbook fail ya version mismatch | + +## Options + +| Key | Default | Kaam | +|---|---|---| +| `module` | **zaroori** | module ka naam | +| `imageName` | `swim/` | registry me image ka naam | +| `vaultMount` | `swim` | Vault KV mount jahan `registry` secret hai | +| `testImage` | `python:3.12-slim` | test kis image me chalein | +| `testCommand` | pytest | test command | +| `trivySeverity` | `HIGH,CRITICAL` | CVE gate ki severity | +| `appPort` / `healthPath` | `8000` / `/version` | verify endpoint | +| `sonarKey` | `swim-` | SonarQube project key | +| `inventory` / `playbook` | `deploy/inventory.ini` / `deploy/deploy.yml` | Ansible | +| `runSonar` / `runDeploy` / `approval` | `true` | stage on/off | + +## Branch ka niyam +- `main`/`master` → poora raasta (staging → approval → production) +- `PR-*` aur baaki branches → **sirf CI gates**, koi deploy nahi + +## Rollback +`ROLLBACK_TAG=7` do → build/test/scan skip, seedha us image par deploy. + +## Kahan chalta hai +- **CI** → `builder-vm-01` (label `builder`). Yahan deploy key **nahi** hai. +- **CD + signing** → Jenkins controller (label `built-in`). Yahan docker.sock **nahi** hai. diff --git a/vars/ansibleDeploy.groovy b/vars/ansibleDeploy.groovy new file mode 100644 index 0000000..c0b08cf --- /dev/null +++ b/vars/ansibleDeploy.groovy @@ -0,0 +1,23 @@ +// Ansible se deploy. Deploy key sirf yahan bind hoti hai aur log me masked rehti hai. +def call(String target) { + checkout scm + withCredentials([ + sshUserPrivateKey(credentialsId: 'deploy-key', keyFileVariable: 'DEPLOY_KEY', usernameVariable: 'DEPLOY_USER'), + string(credentialsId: 'vault-deployer-token', variable: 'VAULT_TOKEN') + ]) { + withEnv(["TARGET=${target}"]) { + sh ''' + set +x + curl -sf -H "X-Vault-Token: $VAULT_TOKEN" \ + "$VAULT_ADDR/v1/$VAULT_MOUNT/data/registry" > /tmp/reg.$$.json + export REG_USER=$(python3 -c "import json;print(json.load(open('/tmp/reg.$$.json'))['data']['data']['username'])") + export REG_PASS=$(python3 -c "import json;print(json.load(open('/tmp/reg.$$.json'))['data']['data']['password'])") + rm -f /tmp/reg.$$.json + export ANSIBLE_HOST_KEY_CHECKING=False + ansible-galaxy collection install community.docker --force -q || true + ansible-playbook -i "$INVENTORY" "$PLAYBOOK" \ + -u "$DEPLOY_USER" --private-key "$DEPLOY_KEY" -e target="$TARGET" + ''' + } + } +} diff --git a/vars/cosignSign.groovy b/vars/cosignSign.groovy new file mode 100644 index 0000000..a7b1734 --- /dev/null +++ b/vars/cosignSign.groovy @@ -0,0 +1,34 @@ +// Image ko sign karta hai. Key Vault se aati hai aur sirf controller par rehti hai. +// Signature registry me ek alag tag ban ke jaati hai. +def call() { + withCredentials([string(credentialsId: 'vault-deployer-token', variable: 'VAULT_TOKEN')]) { + sh ''' + set +x + umask 077 + WORK=$(mktemp -d); trap "rm -rf $WORK" EXIT + curl -sf -H "X-Vault-Token: $VAULT_TOKEN" "$VAULT_ADDR/v1/platform/data/cosign" > $WORK/c.json + python3 - "$WORK" <<'PY' +import json, sys, os +w = sys.argv[1] +d = json.load(open(w + '/c.json'))['data']['data'] +for k, f in (('private_key','cosign.key'), ('public_key','cosign.pub'), ('password','pw')): + open(os.path.join(w, f), 'w').write(d[k]) +PY + curl -sf -H "X-Vault-Token: $VAULT_TOKEN" "$VAULT_ADDR/v1/$VAULT_MOUNT/data/registry" > $WORK/r.json + python3 - "$WORK" "$REGISTRY" <<'PY' +import base64, json, os, sys +w, reg = sys.argv[1], sys.argv[2] +d = json.load(open(w + '/r.json'))['data']['data'] +auth = base64.b64encode(('%s:%s' % (d['username'], d['password'])).encode()).decode() +os.makedirs(os.path.expanduser('~/.docker'), exist_ok=True) +json.dump({'auths': {reg: {'auth': auth}}}, open(os.path.expanduser('~/.docker/config.json'), 'w')) +PY + # tlog-upload=false: private registry ke digests public transparency log + # me nahi bhejte. Verify bhi isi tarah offline hoti hai. + COSIGN_PASSWORD=$(cat $WORK/pw) cosign sign --key $WORK/cosign.key \ + --yes --tlog-upload=false "$FULL_IMAGE" + rm -f ~/.docker/config.json + echo " signed: $FULL_IMAGE" + ''' + } +} diff --git a/vars/cosignVerify.groovy b/vars/cosignVerify.groovy new file mode 100644 index 0000000..c80ad05 --- /dev/null +++ b/vars/cosignVerify.groovy @@ -0,0 +1,28 @@ +// Deploy se pehle GATE: signature nahi mili to yahin ruk jao. +// Isse koi manually push kiya hua image production me nahi ja sakta. +def call() { + withCredentials([string(credentialsId: 'vault-deployer-token', variable: 'VAULT_TOKEN')]) { + sh ''' + set +x + umask 077 + WORK=$(mktemp -d); trap "rm -rf $WORK" EXIT + curl -sf -H "X-Vault-Token: $VAULT_TOKEN" "$VAULT_ADDR/v1/platform/data/cosign" > $WORK/c.json + python3 -c " +import json,sys +d=json.load(open('$WORK/c.json'))['data']['data'] +open('$WORK/cosign.pub','w').write(d['public_key'])" + curl -sf -H "X-Vault-Token: $VAULT_TOKEN" "$VAULT_ADDR/v1/$VAULT_MOUNT/data/registry" > $WORK/r.json + python3 - "$WORK" "$REGISTRY" <<'PY' +import base64, json, os, sys +w, reg = sys.argv[1], sys.argv[2] +d = json.load(open(w + '/r.json'))['data']['data'] +auth = base64.b64encode(('%s:%s' % (d['username'], d['password'])).encode()).decode() +os.makedirs(os.path.expanduser('~/.docker'), exist_ok=True) +json.dump({'auths': {reg: {'auth': auth}}}, open(os.path.expanduser('~/.docker/config.json'), 'w')) +PY + cosign verify --key $WORK/cosign.pub --insecure-ignore-tlog=true "$FULL_IMAGE" \ + && echo " signature OK: $FULL_IMAGE" + rm -f ~/.docker/config.json + ''' + } +} diff --git a/vars/devopsPipeline.groovy b/vars/devopsPipeline.groovy new file mode 100644 index 0000000..26ffdb5 --- /dev/null +++ b/vars/devopsPipeline.groovy @@ -0,0 +1,252 @@ +// Ek hi pipeline, har module ke liye. Module ki Jenkinsfile sirf itni hoti hai: +// +// @Library('devops-platform') _ +// devopsPipeline(module: 'demo-app') +// +// Poora raasta: secret scan -> test -> SAST -> build -> IaC scan -> CVE gate -> +// SBOM -> smoke -> push -> sign -> verify -> staging -> approval -> production. +// +// CI builder VM par chalta hai (wahan deploy key nahi hai), +// CD controller par (wahan docker.sock nahi hai). Dono taraf blast radius chhota. + +def call(Map cfg = [:]) { + + def P = platformConfig() + if (!cfg.module) { error "devopsPipeline: 'module' dena zaroori hai" } + + // ---- defaults: module kuch na de to sensible value ---- + cfg.imageName = cfg.imageName ?: "swim/${cfg.module}" + cfg.vaultMount = cfg.vaultMount ?: 'swim' + cfg.testImage = cfg.testImage ?: 'python:3.12-slim' + cfg.testCommand = cfg.testCommand ?: 'pip install --quiet -r requirements-dev.txt && python -m pytest tests/ -v --junitxml=test-results.xml' + cfg.trivySeverity = cfg.trivySeverity ?: 'HIGH,CRITICAL' + cfg.healthPath = cfg.healthPath ?: '/version' + cfg.appPort = cfg.appPort ?: '8000' + cfg.sonarKey = cfg.sonarKey ?: "swim-${cfg.module}" + cfg.inventory = cfg.inventory ?: 'deploy/inventory.ini' + cfg.playbook = cfg.playbook ?: 'deploy/deploy.yml' + cfg.runSonar = cfg.containsKey('runSonar') ? cfg.runSonar : true + cfg.runDeploy = cfg.containsKey('runDeploy') ? cfg.runDeploy : true + cfg.approval = cfg.containsKey('approval') ? cfg.approval : true + + properties([ + parameters([ + string(name: 'ROLLBACK_TAG', defaultValue: '', + description: 'Khaali = normal build. Purana tag (jaise 7) = bina rebuild seedha us image par rollback.') + ]), + buildDiscarder(logRotator(numToKeepStr: '30')), + disableConcurrentBuilds() + ]) + + def branch = env.BRANCH_NAME ?: 'main' + def isPR = branch.startsWith('PR-') + // PR par sirf CI. Production sirf main/master se jaata hai. + def deployable = cfg.runDeploy && !isPR && (branch == 'main' || branch == 'master') + + def rollbackTag = (env.ROLLBACK_TAG ?: '').trim() + def isRollback = rollbackTag != '' + def imageTag = isRollback ? rollbackTag : "${env.BUILD_NUMBER}" + def fullImage = "${P.registry}/${cfg.imageName}:${imageTag}" + + def commonEnv = [ + "MODULE=${cfg.module}", + "REGISTRY=${P.registry}", + "IMAGE_NAME=${cfg.imageName}", + "IMAGE_TAG=${imageTag}", + "FULL_IMAGE=${fullImage}", + "VAULT_ADDR=${P.vaultAddr}", + "VAULT_MOUNT=${cfg.vaultMount}", + "SONAR_URL=${P.sonarUrl}", + "SONAR_KEY=${cfg.sonarKey}", + "BRIDGE_IP=${P.bridgeIp}", + "TRIVY_SEVERITY=${cfg.trivySeverity}", + "APP_PORT=${cfg.appPort}", + "HEALTH_PATH=${cfg.healthPath}", + "INVENTORY=${cfg.inventory}", + "PLAYBOOK=${cfg.playbook}", + "IMG_GITLEAKS=${P.gitleaks}", + "IMG_TRIVY=${P.trivy}", + "IMG_SYFT=${P.syft}", + "IMG_SCANNER=${P.sonarScanner}", + "TEST_IMAGE=${cfg.testImage}" + ] + + timestamps { + timeout(time: 60, unit: 'MINUTES') { + try { + + // ===================== CI: builder VM ===================== + node(P.builderLabel) { + withEnv(commonEnv) { + try { + stage('Init') { + echo "module=${cfg.module} node=${env.NODE_NAME} branch=${branch} " + + "mode=${isRollback ? 'ROLLBACK' : 'BUILD'} tag=${imageTag} deploy=${deployable}" + } + + stage('Checkout') { + checkout scm + sh 'echo "commit: $(git rev-parse --short HEAD)"' + } + + stage('Secret scan') { + // Poori git history scan hoti hai, sirf latest commit nahi - + // purana leak bhi pakda jaata hai. Findings = build fail. + sh ''' + docker run --rm -v "$PWD":/repo "$IMG_GITLEAKS" \ + detect --source=/repo --redact --no-banner -v + ''' + } + + if (!isRollback) { + stage('Test') { + sh """ + docker run --rm -v "\$PWD":/src -w /src "\$TEST_IMAGE" sh -c '${cfg.testCommand}' + """ + junit allowEmptyResults: true, testResults: 'test-results.xml' + } + + if (cfg.runSonar) { + stage('SAST (SonarQube)') { + withCredentials([string(credentialsId: 'sonar-token', variable: 'SONAR_TOKEN')]) { + sh ''' + set +x + docker run --rm \ + --add-host "ns31240276.ip-51-195-4.eu:$BRIDGE_IP" \ + -e SONAR_HOST_URL="$SONAR_URL" \ + -e SONAR_TOKEN="$SONAR_TOKEN" \ + -v "$PWD":/usr/src "$IMG_SCANNER" \ + -Dsonar.projectKey="$SONAR_KEY" + ''' + } + } + } + + stage('IaC / Dockerfile scan') { + // Dockerfile, compose, k8s manifests ki misconfig pakadta hai + // (root user, missing healthcheck, privileged, etc.) + sh ''' + docker run --rm -v "$PWD":/work "$IMG_TRIVY" config /work \ + --severity "$TRIVY_SEVERITY" --exit-code 1 --no-progress + ''' + } + + stage('Build image') { + sh ''' + docker build --build-arg APP_VERSION="$IMAGE_TAG" \ + -t "$FULL_IMAGE" -t "$REGISTRY/$IMAGE_NAME:latest" . + ''' + } + + stage('CVE gate (Trivy)') { + sh ''' + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v trivy-cache:/root/.cache/ \ + -v "$PWD/.trivyignore":/.trivyignore:ro \ + "$IMG_TRIVY" image \ + --severity "$TRIVY_SEVERITY" --ignore-unfixed \ + --ignorefile /.trivyignore --exit-code 1 \ + --format table --no-progress "$FULL_IMAGE" + ''' + } + + stage('SBOM') { + // Audit ka pehla sawaal: "is image me kya-kya hai?" + sh ''' + docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \ + "$IMG_SYFT" "docker:$FULL_IMAGE" -o spdx-json > sbom.spdx.json + docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \ + "$IMG_SYFT" "docker:$FULL_IMAGE" -o cyclonedx-json > sbom.cyclonedx.json + echo " SBOM packages: $(python3 -c "import json;print(len(json.load(open('sbom.spdx.json'))['packages']))")" + ''' + archiveArtifacts artifacts: 'sbom.*.json', fingerprint: true + } + + stage('Smoke test') { + sh ''' + CID=$(docker run -d -P "$FULL_IMAGE") + trap "docker rm -f $CID >/dev/null 2>&1" EXIT + sleep 5 + docker exec $CID python -c " +import urllib.request, json +r = json.loads(urllib.request.urlopen('http://127.0.0.1:${APP_PORT}${HEALTH_PATH}').read()) +print(' live:', r) +assert r['version'] == '${IMAGE_TAG}', 'version mismatch' +" + ''' + } + + stage('Push') { + withCredentials([string(credentialsId: 'vault-builder-token', variable: 'VAULT_TOKEN')]) { + sh ''' + set +x + curl -sf -H "X-Vault-Token: $VAULT_TOKEN" \ + "$VAULT_ADDR/v1/$VAULT_MOUNT/data/registry" > /tmp/reg.$$.json + U=$(python3 -c "import json;print(json.load(open('/tmp/reg.$$.json'))['data']['data']['username'])") + P=$(python3 -c "import json;print(json.load(open('/tmp/reg.$$.json'))['data']['data']['password'])") + rm -f /tmp/reg.$$.json + echo "$P" | docker login "$REGISTRY" -u "$U" --password-stdin + docker push "$FULL_IMAGE" + docker push "$REGISTRY/$IMAGE_NAME:latest" + docker logout "$REGISTRY" + ''' + } + } + } + } finally { + sh 'docker image prune -f --filter "until=24h" >/dev/null 2>&1 || true' + cleanWs() + } + } + } + + // ============ Sign: controller par (key builder ko nahi milti) ============ + if (!isRollback) { + node(P.deployLabel) { + withEnv(commonEnv) { + stage('Sign image') { cosignSign() } + } + } + } + + if (!deployable) { + echo isPR ? "PR build - deploy nahi hoga, sirf CI gates." : "Branch '${branch}' deployable nahi hai." + return + } + + // ===================== CD: controller ===================== + node(P.deployLabel) { + withEnv(commonEnv) { + stage('Verify signature') { cosignVerify() } + stage('Deploy STAGING') { ansibleDeploy('staging') } + stage('Verify STAGING') { verifyEnv('staging') } + } + } + + if (cfg.approval) { + stage('Production approval') { + // Node ke bahar hai - intezaar me koi executor block nahi hota. + timeout(time: 30, unit: 'MINUTES') { + input message: "${cfg.module}:${imageTag} staging par verified hai. PRODUCTION par bhejein?", + ok: 'Deploy to Production' + } + } + } + + node(P.deployLabel) { + withEnv(commonEnv) { + stage('Deploy PRODUCTION') { ansibleDeploy('production') } + stage('Verify PRODUCTION') { verifyEnv('production') } + } + } + + currentBuild.description = "${cfg.module}:${imageTag}${isRollback ? ' (rollback)' : ''}" + + } catch (e) { + currentBuild.result = 'FAILURE' + throw e + } + } + } +} diff --git a/vars/platformConfig.groovy b/vars/platformConfig.groovy new file mode 100644 index 0000000..eb7665d --- /dev/null +++ b/vars/platformConfig.groovy @@ -0,0 +1,20 @@ +// Platform ke sab endpoints ek jagah. Kuch badle to sirf yahan badlo, +// har module ki pipeline apne aap nayi value uthayegi. +def call() { + return [ + host : 'ns31240276.ip-51-195-4.eu', + registry : 'ns31240276.ip-51-195-4.eu:5000', + giteaUrl : 'https://ns31240276.ip-51-195-4.eu', + vaultAddr : 'https://ns31240276.ip-51-195-4.eu:8444', + sonarUrl : 'https://ns31240276.ip-51-195-4.eu:8446', + // VM ke andar se host is IP par milta hai (libvirt bridge) + bridgeIp : '192.168.124.1', + builderLabel: 'builder', + deployLabel : 'built-in', + // pinned versions - "latest" kabhi nahi, warna build reproducible nahi rehti + gitleaks : 'zricethezav/gitleaks:v8.21.2', + trivy : 'aquasec/trivy:0.58.1', + syft : 'anchore/syft:v1.18.1', + sonarScanner: 'sonarsource/sonar-scanner-cli:11' + ] +} diff --git a/vars/verifyEnv.groovy b/vars/verifyEnv.groovy new file mode 100644 index 0000000..7289936 --- /dev/null +++ b/vars/verifyEnv.groovy @@ -0,0 +1,18 @@ +// Deploy ke baad sach me chal raha hai ya nahi - live endpoint se check. +def call(String target) { + withEnv(["TARGET=${target}"]) { + sh ''' + HOSTIP=$(awk -v t="[$TARGET]" ' + $0 == t {f=1; next} + /^\\[/ {f=0} + f && /ansible_host=/ {sub(/.*ansible_host=/,""); print $1; exit} + ' "$INVENTORY") + [ -n "$HOSTIP" ] || { echo "$TARGET ka host inventory me nahi mila"; exit 1; } + GOT=$(curl -sf --max-time 15 "http://$HOSTIP:$APP_PORT$HEALTH_PATH" \ + | python3 -c "import sys,json;print(json.load(sys.stdin)['version'])") + echo " $TARGET ($HOSTIP) expected=$IMAGE_TAG live=$GOT" + [ "$GOT" = "$IMAGE_TAG" ] || { echo " $TARGET MISMATCH"; exit 1; } + echo " $TARGET VERIFIED" + ''' + } +}