Skip to content

Commit

Permalink
Merge pull request #11 from gematik/feature/ci-cd-concept
Browse files Browse the repository at this point in the history
Feature/ci cd concept
  • Loading branch information
gem-cp authored Aug 22, 2024
2 parents 700c3f9 + e87688a commit 98416a3
Show file tree
Hide file tree
Showing 25 changed files with 1,543 additions and 72 deletions.
84 changes: 84 additions & 0 deletions .github/workflows/automatic_image_genration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: Automatic Image Generation

on:
push:
paths:
- 'src/plantuml/**'
- 'src/drawio/**'
workflow_dispatch:

jobs:
generate_images:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4 # Updated to Node.js 20 compatible version
with:
fetch-depth: 2 # Ensures at least two commits are fetched

- name: Set up JDK 11
uses: actions/[email protected] # Updated to Node.js 20 compatible version
with:
distribution: 'temurin'
java-version: '11'

- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y graphviz xdotool xvfb
sudo snap install drawio --classic
- name: Download PlantUML jar
run: |
wget https://github.com/plantuml/plantuml/releases/latest/download/plantuml.jar -O /usr/local/bin/plantuml.jar
- name: Check for changed files
id: changed_files
run: |
BASE_SHA=$(git rev-parse HEAD^1)
CHANGED_FILES=$(git diff --name-only $BASE_SHA HEAD | grep -E 'src/plantuml/.*\.puml|src/drawio/.*\.drawio' || true)
echo "changed_files=$CHANGED_FILES" >> $GITHUB_ENV
echo "::set-output name=changed_files::$CHANGED_FILES"
- name: Debug changed files
run: |
echo "Debug: Changed files are ${{ steps.changed_files.outputs.changed_files }}"
- name: Generate images from changed PlantUML files
if: ${{ steps.changed_files.outputs.changed_files }} != ''
run: |
for file in $(echo "${{ steps.changed_files.outputs.changed_files }}" | grep 'src/plantuml/.*\.puml' || true); do
echo "Processing PlantUML file: $file"
output_dir=$(dirname "$file" | sed 's|^src/plantuml|images|')
mkdir -p "$output_dir"
java -jar /usr/local/bin/plantuml.jar -tpng "$file" -o "${PWD}/$output_dir"
java -jar /usr/local/bin/plantuml.jar -tsvg "$file" -o "${PWD}/$output_dir"
done
- name: Generate images from changed drawio files
if: ${{ steps.changed_files.outputs.changed_files }} != ''
run: |
for file in $(echo "${{ steps.changed_files.outputs.changed_files }}" | grep 'src/drawio/.*\.drawio' || true); do
echo "Processing drawio file: $file"
output_dir=$(dirname "$file" | sed 's|^src/drawio|images|')
mkdir -p "$output_dir"
xvfb-run -a drawio -x -f png -o "${PWD}/$output_dir/$(basename "$file" .drawio).png" "$file"
xvfb-run -a drawio -x -f svg -o "${PWD}/$output_dir/$(basename "$file" .drawio).svg" "$file"
done
- name: Debug images folder
run: |
echo "Contents of the images folder:"
ls -R images
- name: Commit and push generated images
if: ${{ steps.changed_files.outputs.changed_files }} != ''
run: |
git config --global user.name 'github-actions'
git config --global user.email '[email protected]'
git add -A images
git commit -m "Generate images from changed PlantUML and drawio files"
git push
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
*.tar.gz
*.pyc
__pycache__/
var/
var/
console
4 changes: 2 additions & 2 deletions components/fastapi-pip-pap/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ poetry install
poetry shell
----

The files to start the service in the `/components/fastapi-pip-pap` directory of the project.
The files to start the service are in the `/components/fastapi-pip-pap` directory of the project.
You need to `create bundle.tar.gz` files and place them in the `<config.yaml bundle_storage_path>/<application>/<version>/<bundleType>` directory.
Example: `/var/tmp/fastapi-pip-pap/KIM/2.0/pap/bundle.tar.gz`

The following command starts pip-pap-service on localhost:8200.
The following command starts pip-pap-service on localhost:8080.
[source,sh]
----
python fastapi-pip-pap.py
Expand Down
8 changes: 4 additions & 4 deletions components/fastapi-pip-pap/config.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# pip-pap-service configuration file

# Base path for bundle storage
bundle_storage_path: /var/tmp/fastapi-pip-pap
# GitHub repository for bundle storage
github_repo: https://raw.githubusercontent.com/gem-cp/zt-opa-bundles/main

# logging configuration
logging:
loglevel: DEBUG
loglevel: INFO
log_to_console: True
log_to_file: True
# If log _to_file is True then the logfile is: <service_name>.log
# If log_to_file is True then the logfile is: <service_name>.log
service_name: fastapi-pip-pap
3 changes: 0 additions & 3 deletions components/fastapi-pip-pap/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,5 @@ services:
- "8080:8080"
volumes:
- ./config.yaml:/app/config.yaml # Mount the config file
# Mount the bundle directory;
# From config.yaml: bundle_storage_path: /var/tmp/fastapi-pip-pap
- /var/tmp/fastapi-pip-pap:/var/tmp/fastapi-pip-pap
environment:
- PYTHONPATH=/app
199 changes: 155 additions & 44 deletions components/fastapi-pip-pap/fastapi-pip-pap.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,83 +5,194 @@
import argparse
import yaml
import hashlib
import requests
import jwcrypto.jwk as jwk
from fastapi import FastAPI, HTTPException, Header, Response
from fastapi.responses import FileResponse
from fastapi.responses import FileResponse, JSONResponse
from typing import Optional
from tempfile import NamedTemporaryFile
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import serialization
from jwcrypto import jwk, jwt
import tarfile
import json
import io
import time

app = FastAPI()

class Bundles:
"""Class to handle the bundle storage and retrieval."""
def __init__(self, bundle_storage_path, policies, application, label, filename):
"""Class to handle the bundle storage and retrieval from GitHub."""
def __init__(self, github_repo, application, label, filename):
"""Initialize the Bundles class."""
self.bundle_storage_path = bundle_storage_path
self.policies = policies
self.github_repo = github_repo
self.application = application
self.label = label
self.filename = filename
self.bundle_path = self.get_bundle_path()

def get_bundle_path(self):
"""Return the path to the bundle."""
return os.path.join(
self.bundle_storage_path,
self.policies,
self.application,
self.label,
self.filename
)

def get_etag(self):
"""Calculates the ETag header value based on the file content hash.
Returns:
str: The ETag header value.
"""
# Simulate reading file content
with open(self.bundle_path, 'rb') as f:
content = f.read()
# Calculate hash of the content
hasher = hashlib.sha256(content)
etag = hasher.hexdigest()
return etag
self.bundle_url = self.get_bundle_url()

def get_bundle_url(self):
"""Return the URL to the bundle."""
return f"{self.github_repo}/opa-bundles/{self.application}/{self.label}/{self.filename}"

def download_bundle(self):
"""Download the bundle from GitHub."""
response = requests.get(self.bundle_url)
if response.status_code != 200:
raise HTTPException(status_code=response.status_code, detail=response.text)

temp_file = NamedTemporaryFile(delete=False)
with open(temp_file.name, 'wb') as f:
f.write(response.content)

return temp_file.name

def get_etag(self, file_path):
"""Calculates the ETag header value based on the file content hash."""
hasher = hashlib.sha256()
with open(file_path, 'rb') as f:
while chunk := f.read(8192):
hasher.update(chunk)
etag = hasher.hexdigest()
return etag

def calculate_hash(self, file_content, algorithm='SHA-256'):
"""Calculate the hash of the file content."""
hasher = hashlib.new(algorithm.lower().replace("-", ""))
hasher.update(file_content)
return hasher.hexdigest()

def sign_bundle(self, file_hashes, private_key):
"""Signs the bundle using the given private key."""
claims = {
"files": file_hashes,
"iat": int(time.time()),
"iss": "JWTSercice"
}

header = {
"alg": "ES256",
"typ": "JWT",
"kid": "myPublicKey"
}

token = jwt.JWT(header=header, claims=claims)
token.make_signed_token(private_key)
return token.serialize()

def create_signed_tarball(self, original_bundle_file, signature):
"""Creates a new tarball including the original files and the signature."""
signed_bundle_file = NamedTemporaryFile(delete=False)

with tarfile.open(original_bundle_file, "r:gz") as tar:
with tarfile.open(signed_bundle_file.name, "w:gz") as signed_tar:
for member in tar.getmembers():
file_data = tar.extractfile(member).read()
tarinfo = tarfile.TarInfo(name=member.name)
tarinfo.size = len(file_data)
signed_tar.addfile(tarinfo, fileobj=io.BytesIO(file_data))

# Add signature file
signature_info = tarfile.TarInfo(name=".signatures.json")
signature_info.size = len(signature)
signed_tar.addfile(signature_info, io.BytesIO(signature.encode()))

return signed_bundle_file.name

def generate_keys():
"""Generate a new ECC key pair."""
private_key = ec.generate_private_key(ec.SECP256R1())
public_key = private_key.public_key()
return private_key, public_key

#def get_jwks(public_key):
# """Generate a JWKS representation of the given public key."""
# Create a JWK object from ECC public key
#jwk_key = jwk.JWK()
#jwk_key.import_key(public_key)

# Create a JWKSet containing the JWK object
#jwks = jwk.JWKSet(keys=[jwk_key])

# Export the JWKS as JSON string
#return jwks.export(private_keys=False)

#private_key, public_key = generate_keys()
#jwks = get_jwks(public_key)

@app.options("/policies/{application}/{label}")
async def options_bundle(
application: str,
label: str
):
"""Handle OPTIONS request for bundle endpoint."""
return Response(status_code=200, headers={"Allow": "GET, HEAD, OPTIONS"})
return Response(status_code=200, headers={"Allow": "GET, OPTIONS"})

@app.get("/policies/{application}/{label}")
async def get_bundle(
application: str,
label: str,
if_none_match: Optional[str] = Header(None)
):

filename = "bundle.tar.gz"
"""Get the requested bundle."""
bundle_storage_path = app.state.config["bundle_storage_path"]
bundle = Bundles(bundle_storage_path, "policies", application, label, filename)
bundle_path = bundle.get_bundle_path()
filename = "bundle.tar.gz"
github_repo = app.state.config["github_repo"]
bundle = Bundles(github_repo, application, label, filename)

try:
bundle_file = bundle.download_bundle()
except HTTPException as e:
# Handle HTTP errors from download
raise e

if not os.path.exists(bundle_path):
raise HTTPException(status_code=404, detail="The requested bundle does not exist.")
if bundle_file is None:
# Handle scenario where download_bundle() did not return a valid file path
raise HTTPException(status_code=500, detail="Failed to download bundle")

etag = bundle.get_etag()
# Calculate ETag header value
etag = bundle.get_etag(bundle_file)

if if_none_match == etag:
return Response(status_code=304)

# Extract files and calculate their hashes
file_hashes = []
with tarfile.open(bundle_file, "r:gz") as tar:
for member in tar.getmembers():
file_data = tar.extractfile(member).read()
file_hash = bundle.calculate_hash(file_data)
file_hashes.append({
"name": member.name,
"hash": file_hash,
"algorithm": "SHA-256"
})

# Sign the bundle
signature = bundle.sign_bundle(file_hashes, private_key)

# Create a new tarball including the original files and the signature
signed_bundle_file = bundle.create_signed_tarball(bundle_file, signature)

# Add Content-Disposition header
return FileResponse(bundle_path, media_type="application/gzip", headers={
"ETag": etag,
#"Content-Disposition": f'attachment; filename="{os.path.basename(bundle_path)}"'
"Content-Disposition": f"attachment; filename={filename}"
response = FileResponse(signed_bundle_file, media_type="application/gzip", headers={
"ETag": etag,
"Content-Disposition": f"attachment; filename={filename}"
})

# Clean up the temporary files after sending the response
if bundle_file:
response.background = lambda: os.remove(bundle_file)
if signed_bundle_file:
response.background = lambda: os.remove(signed_bundle_file)

return response

#@app.get("/jwks")
#async def get_jwks_endpoint():
# """Serve the JWKS."""
# return JSONResponse(content=jwks)

def load_config(filename):
"""Load the configuration from the given file."""
try:
Expand Down Expand Up @@ -132,4 +243,4 @@ def setup_logging(log_config):

import uvicorn
# Start the server
uvicorn.run(app, host=args.servername, port=int(args.port), log_level=logger.level)
uvicorn.run(app, host=args.servername, port=int(args.port), log_level=logger.level)
Loading

0 comments on commit 98416a3

Please sign in to comment.