Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions .github/workflows/python-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
name: Python CI & Docker

on:
push:
branches: [ main, master ]
tags: [ 'v*' ]
pull_request:
branches: [ main, master ]

env:
APP_DIR: app_python

jobs:
lint-and-test:
name: Lint, Test and Snyk
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Cache pip
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r ${{ env.APP_DIR }}/requirements.txt
pip install -r ${{ env.APP_DIR }}/requirements-dev.txt

- name: Lint
run: |
flake8 ${{ env.APP_DIR }}

- name: Run tests
run: |
cd ${{ env.APP_DIR }}
pytest -q

- name: Install Snyk
run: |
npm install -g snyk

- name: Snyk test
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
run: |
snyk test --file=${{ env.APP_DIR }}/requirements.txt --severity-threshold=high || true

docker-build-push:
name: Build and Push Docker Image
needs: lint-and-test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set version variables
id: vars
run: |
echo "GITHUB_REF=$GITHUB_REF"
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
VERSION=${GITHUB_REF#refs/tags/v}
else
VERSION="0.0.0-dev-${GITHUB_RUN_NUMBER}"
fi
echo "VERSION=$VERSION" >> $GITHUB_ENV
MAJOR_MINOR=$(echo $VERSION | awk -F. '{print $1"."$2}')
echo "MAJOR_MINOR=$MAJOR_MINOR" >> $GITHUB_ENV

- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Build and push
uses: docker/build-push-action@v4
with:
context: ./app_python
push: true
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/devops-info-service:${{ env.VERSION }}
${{ secrets.DOCKERHUB_USERNAME }}/devops-info-service:${{ env.MAJOR_MINOR }}
${{ secrets.DOCKERHUB_USERNAME }}/devops-info-service:latest
9 changes: 9 additions & 0 deletions app_python/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
venv/
__pycache__/
*.pyc
.pytest_cache/
.git/
.github/
tests/
docs/
*.md
17 changes: 17 additions & 0 deletions app_python/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Python
__pycache__/
*.py[cod]
venv/
*.log

# IDE
.vscode/
.idea/

# OS
.DS_Store

# pytest
.pytest_cache/
.coverage
coverage.xml
16 changes: 16 additions & 0 deletions app_python/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .

RUN groupadd -r app && useradd -r -g app app && chown -R app:app /app

USER app

EXPOSE 5000

CMD ["python", "app.py"]
82 changes: 82 additions & 0 deletions app_python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# DevOps Info Service

![Python CI](https://github.com/iu-capstone-ad/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg)

## Overview

Python Flask service with endpoints for checking system information and health.

## Prerequisites

Python version 3.12 or higher, Flask 3.1.0.

Project has been tested with python 3.12 and Flask 3.1.0 on Ubuntu 24.04

## Installation

```bash
# clone repo
git clone https://github.com/iu-capstone-ad/DevOps-Core-Course
# cd into the app directory
cd app_python
# create and activate a new venv
python3 -m venv venv
source venv/bin/activate
# install dependencies from requirements.txt
pip install -r requirements.txt
```

## Running tests

Install dev requirements and run pytest:

```bash
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt -r requirements-dev.txt
pytest -q
```

## Running the Application

```bash
python app.py
# or with custom config
PORT=8080 python app.py
```

## API Endpoints

- `GET /` - Show system information.
- `GET /health` - Show health information (service uptime).

## Configuration

Environment Variables table

| Variable | Default | Description |
|----------|-----------|--------------------------------------|
| `HOST` | `0.0.0.0` | Address for the service to listen on |
| `PORT` | `5000` | Port for the service to listen on |
| `DEBUG` | `False` | Enable Flask debug mode |

## Docker

### Building the image locally

```bash
cd app_python
docker build -t iucapstonead/devops-info-service:lab02 .
```

### Running the container

```bash
docker run -p 5000:5000 iucapstonead/devops-info-service:lab02
```

### Pulling and running from docker hub

```bash
docker pull iucapstonead/devops-info-service:lab02
```
146 changes: 146 additions & 0 deletions app_python/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import os
import socket
import platform
import logging
from datetime import datetime, timezone

from flask import Flask, jsonify, request

logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)

app = Flask(__name__)

HOST = os.getenv("HOST", "0.0.0.0")
PORT = int(os.getenv("PORT", 5000))
DEBUG = os.getenv("DEBUG", "False").lower() == "true"

START_TIME = datetime.now()


def get_uptime():
delta = datetime.now() - START_TIME
seconds = int(delta.total_seconds())
hours = seconds // 3600
minutes = (seconds % 3600) // 60
return {"seconds": seconds, "human": f"{hours} hours, {minutes} minutes"}


def get_platform_version():
system = platform.system()
if system == "Linux":
return platform.freedesktop_os_release()["PRETTY_NAME"]
elif system == "Darwin":
return str(platform.mac_ver()[0])
elif system == "Windows":
return platform.version()
return platform.release()


def get_system_info():
return {
"hostname": socket.gethostname(),
"platform": platform.system(),
"platform_version": get_platform_version(),
"architecture": platform.machine(),
"cpu_count": os.cpu_count(),
"python_version": platform.python_version(),
}


def get_service_info():
return {
"name": "devops-info-service",
"version": "1.0.0",
"description": "DevOps course info service",
"framework": "Flask",
}


def get_runtime_info():
uptime = get_uptime()
return {
"uptime_seconds": uptime["seconds"],
"uptime_human": uptime["human"],
"current_time": datetime.now(timezone.utc).isoformat(),
"timezone": "UTC",
}


def get_request_info():
return {
"client_ip": request.remote_addr,
"user_agent": request.headers.get("User-Agent", "unknown"),
"method": request.method,
"path": request.path,
}


def get_endpoints():
return [
{"path": "/", "method": "GET", "description": "Service information"},
{"path": "/health", "method": "GET", "description": "Health check"},
]


@app.route("/")
def index():
logger.info(
f"Request: {request.method} {request.path} from {request.remote_addr}"
)

return jsonify(
{
"service": get_service_info(),
"system": get_system_info(),
"runtime": get_runtime_info(),
"request": get_request_info(),
"endpoints": get_endpoints(),
}
)


@app.route("/health")
def health():
return jsonify(
{
"status": "healthy",
"timestamp": datetime.now(timezone.utc).isoformat(),
"uptime_seconds": get_uptime()["seconds"],
}
)


@app.errorhandler(404)
def not_found(error):
return (
jsonify(
{
"error": "Not Found",
"message": "Endpoint does not exist",
}
),
404,
)


@app.errorhandler(500)
def internal_error(error):
logger.error(f"Internal server error: {error}")
return (
jsonify(
{
"error": "Internal Server Error",
"message": "An unexpected error occurred",
}
),
500,
)


if __name__ == "__main__":
logger.info("Starting...")
app.run(host=HOST, port=PORT, debug=DEBUG)
Loading