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
106 changes: 106 additions & 0 deletions .github/workflows/python-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
name: Python CI & Docker Build

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

permissions:
contents: read
packages: write

jobs:
test:
name: Test & Lint
runs-on: ubuntu-latest

strategy:
matrix:
python-version: ["3.9", "3.10", "3.11"]

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r app_python/requirements.txt
pip install ruff pytest

- name: Lint with Ruff
run: ruff check .

- name: Run tests
run: pytest app_python/tests/ --verbose -v

- name: Format check
run: ruff format --check .

security:
name: Snyk Security Scan
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
defaults:
run:
working-directory: ./app_python
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Snyk CLI
uses: snyk/actions/python-3.11@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=critical --skip-unresolved
continue-on-error: true


docker:
name: Build & Push Docker
needs: [ test, security ] # Runs only if test & security passed
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' # Dont push pr to docker hub

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ secrets.DOCKER_USERNAME }}/testiks
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=raw,value={{date 'YYYY.MM'}},enable={{is_default_branch}}
type=ref,event=branch


- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

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

- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: ./app_python/
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
24 changes: 24 additions & 0 deletions app_python/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Python
__pycache__/
*.pyc
*.pyo

# Virtual environments
venv/
.venv/

# Git
.git/
.gitignore

# IDE
.vscode/
.idea/

# OS
.DS_Store

# Docs & tests (если не нужны в контейнере)
README.md
tests/
docs/
4 changes: 4 additions & 0 deletions app_python/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
**/myenv/
**/__pycache__/
*cache*
**/*cache/
22 changes: 22 additions & 0 deletions app_python/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
FROM python:3.12-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1

# Non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser

# Install deps first
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt


COPY app.py .
RUN chown -R appuser:appuser /app
USER appuser

EXPOSE 8000

# Run app finally
CMD ["python", "app.py"]
55 changes: 55 additions & 0 deletions app_python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# DevOps Info Service
A lightweight demo Python web application that system information via HTTP endpoints

### Prerequisites
Python 3.10+
Flask 3.1.0

### Installation
```bash
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```

### Running the Application
```bash
python3 app.py
# Or with custom config
PORT=8080 python3 app.py
```

### API Endpoints
There are few main endpoints:
- `GET /` - Service and system information
- `GET /health` - Health check.

### Configuration

| Variable | Value | Purpose |
| -------- | ------ | ------------------------------------ |
| Host | string | A host to run app on |
| Port | int | A port to assign for web application |
| Debug | bool | Should debug output be enabled |

## Docker
This application can be run in a containerized environment with Docker

### Build the image locally
To build the Docker image, use the Docker build command from the project directory, specifying the Dockerfile and an image name with a tag
```bash
cd app_python
docker build -t <image-name> .
```

### Run a container
To run the application, start a container from the built image and map the container port to a port on the host machine so the application can be accessed locally
```bash
docker run -p<any-port-on-your-machine>:5000 <created-image-name>
```

### Pull from Docker Hub
The pre-built image is also available on Docker Hub and can be pulled using the standard Docker pull command with the repository name and desired tag
```bash
docker pull cacucoh/testiks:1.0
```
112 changes: 112 additions & 0 deletions app_python/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
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(timezone.utc)


def get_uptime():
delta = datetime.now(timezone.utc) - 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_system_info():
return {
"hostname": socket.gethostname(),
"platform": platform.system(),
"platform_version": platform.version(),
"architecture": platform.machine(),
"cpu_count": os.cpu_count(),
"python_version": platform.python_version(),
}


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


@app.route("/", methods=["GET"])
def default_route():
logger.info(f"Request: {request.method} {request.path}")
uptime = get_uptime()

response = {
"service": {
"name": "devops-info-service",
"version": "1.0.0",
"description": "DevOps course info service",
"framework": "Flask",
},
"system": get_system_info(),
"runtime": {
"uptime_seconds": uptime["seconds"],
"uptime_human": uptime["human"],
"current_time": datetime.now(timezone.utc).isoformat(),
"timezone": "UTC",
},
"request": {
"client_ip": request.remote_addr,
"user_agent": request.headers.get("User-Agent"),
"method": request.method,
"path": request.path,
},
"endpoints": [
{"path": "/", "method": "GET", "description": "Service information"},
{"path": "/health", "method": "GET", "description": "Health check"},
],
}

return jsonify(response)


@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):
return (
jsonify(
{
"error": "Internal Server Error",
"message": "An unexpected error occurred",
}
),
500,
)


if __name__ == "__main__":
logger.info("[+] Starting...")
try:
app.run(host=HOST, port=PORT, debug=DEBUG)
finally:
logger.info("[i] Shutting down")
2 changes: 2 additions & 0 deletions app_python/docs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
**/__pycache__/
myvenv/
Loading