|
|
|
@ -6,16 +6,22 @@ pipeline {
|
|
|
|
// This creates DOCKER_AUTH_USR and DOCKER_AUTH_PSW
|
|
|
|
// This creates DOCKER_AUTH_USR and DOCKER_AUTH_PSW
|
|
|
|
DOCKER_AUTH = credentials('aimodocker')
|
|
|
|
DOCKER_AUTH = credentials('aimodocker')
|
|
|
|
IMAGE_NAME = 'oh-service'
|
|
|
|
IMAGE_NAME = 'oh-service'
|
|
|
|
SERVICE_NAME = 'ahm-app'
|
|
|
|
SERVICE_NAME = 'be-optimumoh'
|
|
|
|
|
|
|
|
|
|
|
|
SECURITY_PREFIX = 'security'
|
|
|
|
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
|
|
|
|
// Initialize variables to be updated in script blocks
|
|
|
|
GIT_COMMIT_HASH = ""
|
|
|
|
GIT_COMMIT_HASH = ""
|
|
|
|
IMAGE_TAG = ""
|
|
|
|
IMAGE_TAG = ""
|
|
|
|
SECONDARY_TAG = ""
|
|
|
|
SECONDARY_TAG = ""
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
stages {
|
|
|
|
stages {
|
|
|
|
stage('Checkout & Setup') {
|
|
|
|
stage('Checkout & Setup') {
|
|
|
|
steps {
|
|
|
|
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') {
|
|
|
|
stage('Build & Tag') {
|
|
|
|
steps {
|
|
|
|
steps {
|
|
|
|
script {
|
|
|
|
script {
|
|
|
|
def fullImageName = "${DOCKER_HUB_USERNAME}/${IMAGE_NAME}"
|
|
|
|
def fullImageName = "${DOCKER_HUB_USERNAME}/${IMAGE_NAME}"
|
|
|
|
|
|
|
|
// Build image TANPA vault.env - image tetap bersih
|
|
|
|
sh "docker build -t ${fullImageName}:${IMAGE_TAG} ."
|
|
|
|
sh "docker build -t ${fullImageName}:${IMAGE_TAG} ."
|
|
|
|
|
|
|
|
|
|
|
|
if (SECONDARY_TAG) {
|
|
|
|
if (SECONDARY_TAG) {
|
|
|
|
sh "docker tag ${fullImageName}:${IMAGE_TAG} ${fullImageName}:${SECONDARY_TAG}"
|
|
|
|
sh "docker tag ${fullImageName}:${IMAGE_TAG} ${fullImageName}:${SECONDARY_TAG}"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -65,34 +163,66 @@ pipeline {
|
|
|
|
|
|
|
|
|
|
|
|
stage('Push to Docker Hub') {
|
|
|
|
stage('Push to Docker Hub') {
|
|
|
|
steps {
|
|
|
|
steps {
|
|
|
|
retry(3) {
|
|
|
|
script {
|
|
|
|
script {
|
|
|
|
def fullImageName = "${DOCKER_HUB_USERNAME}/${IMAGE_NAME}"
|
|
|
|
def fullImageName = "${DOCKER_HUB_USERNAME}/${IMAGE_NAME}"
|
|
|
|
sh "docker push ${fullImageName}:${IMAGE_TAG}"
|
|
|
|
sh "docker push ${fullImageName}:${IMAGE_TAG}"
|
|
|
|
if (SECONDARY_TAG) {
|
|
|
|
|
|
|
|
sh "docker push ${fullImageName}:${SECONDARY_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 {
|
|
|
|
post {
|
|
|
|
always {
|
|
|
|
always {
|
|
|
|
script {
|
|
|
|
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}"
|
|
|
|
def fullImageName = "${DOCKER_HUB_USERNAME}/${IMAGE_NAME}"
|
|
|
|
// Clean up images to save agent disk space
|
|
|
|
|
|
|
|
sh "docker rmi ${fullImageName}:${IMAGE_TAG} || true"
|
|
|
|
sh "docker rmi ${fullImageName}:${IMAGE_TAG} || true"
|
|
|
|
if (SECONDARY_TAG) {
|
|
|
|
if (SECONDARY_TAG) {
|
|
|
|
sh "docker rmi ${fullImageName}:${SECONDARY_TAG} || true"
|
|
|
|
sh "docker rmi ${fullImageName}:${SECONDARY_TAG} || true"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
success {
|
|
|
|
|
|
|
|
echo "Successfully processed ${env.BRANCH_NAME}."
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|