You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
317 lines
13 KiB
Groovy
317 lines
13 KiB
Groovy
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('Security: Verify Dependencies') {
|
|
steps {
|
|
script {
|
|
sh """#!/bin/bash
|
|
set -e
|
|
|
|
WORKSPACE_DIR="\$(pwd)"
|
|
echo "Working directory: \${WORKSPACE_DIR}"
|
|
|
|
echo "1. Checking Python dependencies..."
|
|
if [ -f "\${WORKSPACE_DIR}/requirements.txt" ]; then
|
|
echo " - Found requirements.txt"
|
|
|
|
echo ""
|
|
echo "2. Running security checks inside Python container..."
|
|
|
|
# Buat container, copy file, jalankan check
|
|
CONTAINER_ID=\$(docker create python:3.11-slim bash -c '
|
|
set -e
|
|
|
|
echo "Installing security tools..."
|
|
pip install --upgrade pip setuptools wheel > /dev/null 2>&1
|
|
pip install pip-audit safety > /dev/null 2>&1
|
|
|
|
echo ""
|
|
echo "Installing dependencies from requirements.txt..."
|
|
pip install -r /tmp/requirements.txt
|
|
|
|
echo ""
|
|
echo "Running pip-audit for known vulnerabilities..."
|
|
pip-audit --desc || {
|
|
echo "Security vulnerabilities detected"
|
|
exit 1
|
|
}
|
|
|
|
echo ""
|
|
echo "Running safety check..."
|
|
safety check || {
|
|
echo "Safety check found issues"
|
|
exit 1
|
|
}
|
|
|
|
echo ""
|
|
echo "Checking for outdated packages..."
|
|
pip list --outdated || echo "All packages are up to date"
|
|
|
|
echo ""
|
|
echo "Verifying package integrity..."
|
|
pip check || {
|
|
echo "Dependency conflicts detected"
|
|
exit 1
|
|
}
|
|
|
|
echo "All Python dependencies verified successfully"
|
|
')
|
|
|
|
echo " - Container ID: \${CONTAINER_ID}"
|
|
|
|
# Copy requirements.txt langsung ke container (tanpa volume mount)
|
|
docker cp "\${WORKSPACE_DIR}/requirements.txt" "\${CONTAINER_ID}:/tmp/requirements.txt"
|
|
|
|
# Copy Pipfile jika ada
|
|
if [ -f "\${WORKSPACE_DIR}/Pipfile.lock" ]; then
|
|
docker cp "\${WORKSPACE_DIR}/Pipfile.lock" "\${CONTAINER_ID}:/tmp/Pipfile.lock"
|
|
fi
|
|
|
|
# Jalankan container dan pastikan cleanup
|
|
docker start --attach "\${CONTAINER_ID}" || {
|
|
EXIT_CODE=\$?
|
|
docker rm "\${CONTAINER_ID}" > /dev/null 2>&1 || true
|
|
exit \${EXIT_CODE}
|
|
}
|
|
|
|
docker rm "\${CONTAINER_ID}" > /dev/null 2>&1 || true
|
|
|
|
echo "✓ All Python dependencies are verified and up to date"
|
|
else
|
|
echo " - requirements.txt not found, skipping dependency check"
|
|
echo " Directory contents:"
|
|
ls -la "\${WORKSPACE_DIR}/"
|
|
fi
|
|
"""
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
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"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|