pipeline { agent any environment { DOCKER_HUB_USERNAME = 'aimodocker' // This creates DOCKER_AUTH_USR and DOCKER_AUTH_PSW DOCKER_AUTH = credentials('aimodocker') IMAGE_NAME = 'oh-service' SERVICE_NAME = 'be-optimumoh' SECURITY_PREFIX = 'security' VAULT_ADDR = 'http://127.0.0.1:8200' VAULT_KV_PATH = 'secret/data/Ep=X1qdWqzh+H&x&dRmV' VAULT_ENV_FILE = 'vault.env' TEMP_DOCKERFILE = 'Dockerfile.ci' // Initialize variables to be updated in script blocks GIT_COMMIT_HASH = "" IMAGE_TAG = "" SECONDARY_TAG = "" } stages { stage('Checkout & Setup') { steps { script { checkout scm GIT_COMMIT_HASH = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() // Use env.BRANCH_NAME or logic to handle detached HEAD if necessary def branch = env.BRANCH_NAME ?: 'unknown' echo "Current Branch: ${branch}" if (branch == 'main') { IMAGE_TAG = GIT_COMMIT_HASH SECONDARY_TAG = 'latest' } else if (branch == 'oh_security') { IMAGE_TAG = "${SECURITY_PREFIX}-${GIT_COMMIT_HASH}" SECONDARY_TAG = "${SECURITY_PREFIX}-latest" } else { IMAGE_TAG = "temp-${GIT_COMMIT_HASH}" SECONDARY_TAG = "" // Ensure it's empty for other branches } echo "Primary Tag: ${IMAGE_TAG}" } } } stage('Docker Login') { steps { // Fixed variable names based on the 'DOCKER_AUTH' environment key sh "echo ${DOCKER_AUTH_PSW} | docker login -u ${DOCKER_AUTH_USR} --password-stdin" } } stage('Setup Vault SSH Tunnel') { steps { sshagent(credentials: ['vault-server-ssh-key']) { sh """#!/bin/bash pkill -f 'ssh.*8200:127.0.0.1:8200' || true sleep 1 ssh -fN \ -L 8200:127.0.0.1:8200 \ -o StrictHostKeyChecking=no \ -o ServerAliveInterval=30 \ -o ExitOnForwardFailure=yes \ aimo@192.168.1.105 for i in \$(seq 1 10); do if curl -sf --connect-timeout 2 'http://127.0.0.1:8200/v1/sys/health' > /dev/null 2>&1; then echo "✓ Vault tunnel is ready" exit 0 fi echo "Waiting... (\$i/10)" sleep 2 done exit 1 """ } } } stage('Fetch Runtime Env from Vault') { steps { script { withCredentials([ string(credentialsId: 'vault-approle-role-id', variable: 'CI_VAULT_ROLE_ID'), string(credentialsId: 'vault-approle-secret-id', variable: 'CI_VAULT_SECRET_ID') ]) { def roleId = env.CI_VAULT_ROLE_ID def secretId = env.CI_VAULT_SECRET_ID def loginPayload = """{"role_id":"${roleId}","secret_id":"${secretId}"}""" def loginResponse = sh( script: '''curl -sS --fail --connect-timeout 10 \ --request POST \ --header 'Content-Type: application/json' \ --data "{\\"role_id\\":\\"${CI_VAULT_ROLE_ID}\\",\\"secret_id\\":\\"${CI_VAULT_SECRET_ID}\\"}" \ 'http://127.0.0.1:8200/v1/auth/approle/login' ''', returnStdout: true ).trim() def vaultToken = sh( script: "echo '${loginResponse}' | python3 -c \"import json,sys; print(json.load(sys.stdin)['auth']['client_token'])\"", returnStdout: true ).trim() def secretResponse = sh( script: """curl -sS --fail --connect-timeout 10 \ --header "X-Vault-Token: ${vaultToken}" \ '${VAULT_ADDR}/v1/${VAULT_KV_PATH}'""", returnStdout: true ).trim() def pythonScript = '''import json, os with open('/tmp/vault_response.json') as f: payload = json.load(f) data = payload.get("data", {}).get("data", {}) if not data: raise SystemExit("ERROR: Vault secret is empty") def render_env_line(key, value): if value is None: return key + "=" text = str(value) has_special = text == "" or " " in text or "#" in text or "\\t" in text if has_special: escaped = text.replace('"', '\\\\"') return key + "=" + '"' + escaped + '"' return key + "=" + text workspace = os.environ.get("WORKSPACE", ".") with open(workspace + "/vault.env", "w") as f: for key in sorted(data.keys()): f.write(render_env_line(key, data[key]) + "\\n") print("Generated vault.env with " + str(len(data)) + " keys") ''' writeFile(file: '/tmp/vault_response.json', text: secretResponse) writeFile(file: '/tmp/process_vault.py', text: pythonScript) sh "python3 /tmp/process_vault.py" sh "chmod 600 vault.env" sh "echo '=== Vault.env content ===' && cat vault.env | head -20" echo "✓ vault.env ready" } } } } stage('Build & Tag') { steps { script { def fullImageName = "${DOCKER_HUB_USERNAME}/${IMAGE_NAME}" // Build image TANPA vault.env - image tetap bersih sh "docker build -t ${fullImageName}:${IMAGE_TAG} ." if (SECONDARY_TAG) { sh "docker tag ${fullImageName}:${IMAGE_TAG} ${fullImageName}:${SECONDARY_TAG}" } } } } stage('Push to Docker Hub') { steps { script { def fullImageName = "${DOCKER_HUB_USERNAME}/${IMAGE_NAME}" sh "docker push ${fullImageName}:${IMAGE_TAG}" if (SECONDARY_TAG) { sh "docker push ${fullImageName}:${SECONDARY_TAG}" } } } } stage('Deploy to Server') { steps { sshagent(credentials: ['vault-server-ssh-key']) { sh """#!/bin/bash # Transfer vault.env ke server deployment via SSH # File disimpan di /tmp — tidak persistent echo "=== SCP transferring env file ===" scp -o StrictHostKeyChecking=no \ vault.env \ aimo@192.168.1.105:/tmp/${SERVICE_NAME}.env # Set permission ketat ssh -o StrictHostKeyChecking=no aimo@192.168.1.105 \ 'chmod 600 /tmp/${SERVICE_NAME}.env && \ echo "=== Env file transferred ===" && \ ls -lh /tmp/${SERVICE_NAME}.env' # Jalankan deploy dengan env-file dari /tmp ssh -o StrictHostKeyChecking=no aimo@192.168.1.105 \ 'cd ~ && \ echo "=== Checking env file ===" && \ ls -lh /tmp/${SERVICE_NAME}.env && \ echo "=== lines of env file ===" && \ cat /tmp/${SERVICE_NAME}.env && \ cp /tmp/${SERVICE_NAME}.env .env && \ echo "=== Copied to .env for docker compose ===" && \ docker compose pull ${SERVICE_NAME} && \ docker rm -f ${SERVICE_NAME} || true && \ docker compose -p digitaltwin up -d --no-deps ${SERVICE_NAME} && \ rm -f /tmp/${SERVICE_NAME}.env && \ echo "✓ ${SERVICE_NAME} deployed, env file removed"' """ } } } } post { always { script { // Hapus vault.env dari Jenkins workspace sh 'rm -f vault.env /tmp/vault_response.json /tmp/process_vault.py || true' sh 'pkill -f "ssh.*8200:127.0.0.1:8200" || true' sh 'docker logout || true' def fullImageName = "${DOCKER_HUB_USERNAME}/${IMAGE_NAME}" sh "docker rmi ${fullImageName}:${IMAGE_TAG} || true" if (SECONDARY_TAG) { sh "docker rmi ${fullImageName}:${SECONDARY_TAG} || true" } } } } }