diff --git a/Jenkinsfile b/Jenkinsfile index 33f7e67..18263fc 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -6,16 +6,22 @@ pipeline { // This creates DOCKER_AUTH_USR and DOCKER_AUTH_PSW DOCKER_AUTH = credentials('aimodocker') IMAGE_NAME = 'oh-service' - SERVICE_NAME = 'ahm-app' + 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 { @@ -50,12 +56,104 @@ pipeline { } } + 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}" } @@ -65,34 +163,66 @@ pipeline { stage('Push to Docker Hub') { steps { - retry(3) { - script { - def fullImageName = "${DOCKER_HUB_USERNAME}/${IMAGE_NAME}" - sh "docker push ${fullImageName}:${IMAGE_TAG}" - - if (SECONDARY_TAG) { - sh "docker push ${fullImageName}:${SECONDARY_TAG}" - } + 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 { - sh 'docker logout' + // 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}" - // Clean up images to save agent disk space sh "docker rmi ${fullImageName}:${IMAGE_TAG} || true" if (SECONDARY_TAG) { sh "docker rmi ${fullImageName}:${SECONDARY_TAG} || true" } } } - success { - echo "Successfully processed ${env.BRANCH_NAME}." - } } -} \ No newline at end of file +}