Skip to content

Commit dcc604e

Browse files
authored
Liquibase Manual Deploy Action (#687)
* liquibase manual deploy workflow * fixed lint * added cloud run files * fixed credentials typing * added exception * added cloud run config * fixed the image and updated commands * moved liquibase to Dockerfile * added workflow to trigger the schema update * added region param to workflow gcloud calls * added temp dir and cleaned code
1 parent 2cb48ee commit dcc604e

File tree

6 files changed

+194
-0
lines changed

6 files changed

+194
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: Deploy Schema Updater
2+
3+
on:
4+
workflow_dispatch:
5+
6+
7+
jobs:
8+
build image:
9+
runs-on: ubuntu-latest
10+
11+
steps:
12+
- uses: actions/checkout@v3
13+
14+
- id: "google-cloud-auth"
15+
name: "Authenticate to Google Cloud"
16+
uses: "google-github-actions/auth@v1"
17+
with:
18+
workload_identity_provider: "projects/774248915715/locations/global/workloadIdentityPools/gh-deploy-pool/providers/gh-provider"
19+
service_account: "[email protected]"
20+
21+
- id: "google-cloud-sdk-setup"
22+
name: "Set up Cloud SDK"
23+
uses: google-github-actions/setup-gcloud@v1
24+
25+
- name: Build image with Cloud Build
26+
run: |
27+
gcloud builds submit --tag gcr.io/sample-metadata/schema-updater:latest ./db/deploy
28+
29+
- name: Deploy image to Cloud Run
30+
run: |
31+
gcloud run deploy schema-updater --image gcr.io/sample-metadata/schema-updater --platform managed --region australia-southeast1 --no-allow-unauthenticated
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: Trigger Schema Updater
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
environment:
7+
description: 'Target environment (prod or dev)'
8+
required: true
9+
default: 'dev'
10+
type: choice
11+
options:
12+
- prod
13+
- dev
14+
15+
jobs:
16+
invoke-cloud-run:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- uses: actions/checkout@v3
20+
21+
- id: "google-cloud-auth"
22+
name: "Authenticate to Google Cloud"
23+
uses: "google-github-actions/auth@v1"
24+
with:
25+
workload_identity_provider: "projects/774248915715/locations/global/workloadIdentityPools/gh-deploy-pool/providers/gh-provider"
26+
service_account: "[email protected]"
27+
28+
- id: "google-cloud-sdk-setup"
29+
name: "Set up Cloud SDK"
30+
uses: google-github-actions/setup-gcloud@v1
31+
32+
- id: get_url
33+
name: Get Cloud Run service URL
34+
run: |
35+
echo "CLOUD_RUN_URL=$(gcloud run services describe schema-updater --region australia-southeast1 --format 'value(status.url)')/execute-liquibase?environment=${{ github.event.inputs.environment }}" >> $GITHUB_ENV
36+
37+
- name: Manual Trigger - Invoke Cloud Run
38+
run: |
39+
curl -X POST -H "Authorization: Bearer $(gcloud auth print-identity-token)" -H "Content-Type: application/xml" --data-binary "@db/project.xml" $CLOUD_RUN_URL
40+
env:
41+
CLOUD_RUN_URL: ${{ env.CLOUD_RUN_URL }}

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
db/postgres*.jar
22
.vscode/
33
env/
4+
venv/
45
__pycache__/
56
*.pyc
67
.DS_Store

db/deploy/Dockerfile

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Use the official lightweight Python image.
2+
FROM python:3.11-slim
3+
4+
# Set environment variables
5+
ENV PYTHONDONTWRITEBYTECODE 1
6+
ENV PYTHONUNBUFFERED 1
7+
ENV LIQUIBASE_VERSION=4.26.0
8+
ENV MARIADB_JDBC_VERSION=3.0.3
9+
10+
# Install system dependencies
11+
RUN apt-get update && apt-get install -y --no-install-recommends gcc git ssh default-jdk wget unzip
12+
13+
# Download and install Liquibase
14+
RUN wget https://github.com/liquibase/liquibase/releases/download/v${LIQUIBASE_VERSION}/liquibase-${LIQUIBASE_VERSION}.zip \
15+
&& unzip liquibase-${LIQUIBASE_VERSION}.zip -d /opt/liquibase \
16+
&& chmod +x /opt/liquibase/liquibase \
17+
# Clean up to reduce layer size
18+
&& rm liquibase-${LIQUIBASE_VERSION}.zip \
19+
&& apt-get clean \
20+
&& rm -rf /var/lib/apt/lists/*
21+
22+
# Download the MariaDB JDBC driver
23+
RUN wget https://downloads.mariadb.com/Connectors/java/connector-java-${MARIADB_JDBC_VERSION}/mariadb-java-client-${MARIADB_JDBC_VERSION}.jar
24+
RUN mv mariadb-java-client-${MARIADB_JDBC_VERSION}.jar /opt/
25+
26+
# Copy local code to the container image.
27+
ENV APP_HOME /app
28+
WORKDIR $APP_HOME
29+
COPY . ./
30+
31+
# Install Python dependencies
32+
RUN python3 -m pip install --no-cache-dir --break-system-packages -r requirements.txt
33+
34+
# Run the FastAPI app on container startup
35+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]

db/deploy/main.py

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import contextlib
2+
import json
3+
import os
4+
import subprocess
5+
import tempfile
6+
from typing import Dict, Literal
7+
8+
from fastapi import FastAPI, HTTPException, Query, Request
9+
from google.cloud import logging, secretmanager
10+
11+
app = FastAPI()
12+
13+
# Setup for Google Cloud clients and logging
14+
SECRET_CLIENT = secretmanager.SecretManagerServiceClient()
15+
LOGGING_CLIENT = logging.Client()
16+
SECRET_PROJECT = 'sample-metadata'
17+
SECRET_NAME = 'liquibase-schema-updater'
18+
log_name = 'lb_schema_update_log'
19+
logger = LOGGING_CLIENT.logger(log_name)
20+
21+
# Important to maintain this filename otherwise Liquibase fails to recognise previous migrations
22+
changelog_file = 'project.xml'
23+
24+
25+
def read_db_credentials(env: Literal['prod', 'dev']) -> Dict[Literal['dbname', 'username', 'password', 'host'], str]:
26+
"""Get database credentials from Secret Manager."""
27+
try:
28+
secret_path = SECRET_CLIENT.secret_version_path(SECRET_PROJECT, SECRET_NAME, 'latest')
29+
response = SECRET_CLIENT.access_secret_version(request={'name': secret_path})
30+
return json.loads(response.payload.data.decode('UTF-8'))[env]
31+
except Exception as e: # Broad exception for example; refine as needed
32+
text = f'Failed to retrieve or parse secrets: {e}'
33+
logger.log_text(text, severity='ERROR')
34+
raise HTTPException(status_code=500, detail=text) from e
35+
36+
37+
@app.post('/execute-liquibase')
38+
async def execute_liquibase(request: Request, environment: Literal['prod', 'dev'] = Query(default='dev', regex='^(prod|dev)$')):
39+
"""Endpoint to remotely trigger Liquibase commands on a GCP VM using XML content."""
40+
xml_content = await request.body()
41+
42+
# Clean up the local temporary file
43+
credentials = read_db_credentials(env=environment)
44+
db_username = credentials['username']
45+
db_password = credentials['password']
46+
db_hostname = credentials['host']
47+
db_name = credentials['dbname']
48+
49+
# Temporary file creation with XML content
50+
with tempfile.TemporaryDirectory() as tempdir:
51+
# Specify the file path within the temporary directory
52+
with contextlib.chdir(tempdir): # pylint: disable=E1101
53+
with open(changelog_file, 'wb') as temp_file:
54+
temp_file.write(xml_content)
55+
temp_file_path = temp_file.name # Store file path to use later
56+
remote_file_path = os.path.basename(temp_file_path)
57+
58+
# The actual command to run on the VM
59+
liquibase_command = [
60+
'/opt/liquibase/liquibase',
61+
f'--changeLogFile={remote_file_path}',
62+
f'--url=jdbc:mariadb://{db_hostname}/{db_name}',
63+
f'--driver=org.mariadb.jdbc.Driver',
64+
f'--classpath=/opt/mariadb-java-client-3.0.3.jar',
65+
'update',
66+
]
67+
68+
try:
69+
# Execute the gcloud command
70+
result = subprocess.run(liquibase_command, check=True, capture_output=True, text=True, env={'LIQUIBASE_COMMAND_PASSWORD': db_password, 'LIQUIBASE_COMMAND_USERNAME': db_username, **os.environ},)
71+
logger.log_text(f'Liquibase update successful: {result.stdout}', severity='INFO')
72+
os.remove(temp_file_path)
73+
return {'message': 'Liquibase update executed successfully', 'output': result.stdout}
74+
except subprocess.CalledProcessError as e:
75+
text = f'Failed to execute Liquibase update: {e.stderr}'
76+
logger.log_text(text, severity='ERROR')
77+
raise HTTPException(status_code=500, detail=text) from e
78+
79+
80+
if __name__ == '__main__':
81+
import uvicorn
82+
uvicorn.run(app, host='0.0.0.0', port=int(os.environ.get('PORT', 8080)))

db/deploy/requirements.txt

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
fastapi
2+
uvicorn
3+
google-cloud-secret-manager
4+
google-cloud-logging

0 commit comments

Comments
 (0)