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

on:
push:
paths:
- "app_python/**"
- ".github/workflows/python-ci.yml"
pull_request:
paths:
- "app_python/**"
- ".github/workflows/python-ci.yml"

permissions:
contents: read

jobs:
test:
name: Dependencies, Linter and Tests
runs-on: ubuntu-latest

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

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"
cache-dependency-path: |
app_python/requirements.txt
app_python/requirements-dev.txt

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

- name: Run linter
run: ruff check app_python

- name: Run tests
run: python -m pytest app_python/tests -q

- name: Set up Snyk
uses: snyk/actions/setup@master

- name: Run Snyk vulnerability check
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
run: snyk test --file=app_python/requirements.txt --package-manager=pip --severity-threshold=high

docker:
name: Docker Build and Push
runs-on: ubuntu-latest
needs: test

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

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

- name: Login to Docker Hub
if: github.event_name == 'push'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: ./app_python
file: ./app_python/Dockerfile
push: ${{ github.event_name == 'push' }}
tags: ${{ secrets.DOCKERHUB_USERNAME }}/moscow-time-app:latest
cache-from: type=gha
cache-to: type=gha,mode=max
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
venv/
__pycache__/
*.pyc
gunicorn.ctl
.pytest_cache/
.ruff_cache/
11 changes: 11 additions & 0 deletions app_python/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
venv/
__pycache__/
*.pyc
gunicorn.ctl
.pytest_cache/
.ruff_cache/
tests/
requirements-dev.txt
CI.md
PYTHON.md
README.md
67 changes: 67 additions & 0 deletions app_python/CI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Continuous Integration

## Overview

This project uses GitHub Actions to run automated checks for the Python web application.

The CI workflow runs on push and pull request events when files inside the `app_python` directory or the workflow file are changed.

## Workflow Steps

The workflow includes the following stages:

1. Checkout repository.
2. Set up Python.
3. Install dependencies.
4. Run linter.
5. Run unit tests.
6. Run Snyk vulnerability checks.
7. Set up Docker Buildx.
8. Login to Docker Hub.
9. Build and push Docker image.

## CI Best Practices Used

### Explicit Python Version

The workflow uses a fixed Python version to make builds reproducible.

### Dependency Caching

The workflow uses pip cache to speed up dependency installation.

### Docker Build Cache

Docker Buildx cache is used to speed up repeated image builds.

### Separate Test and Docker Jobs

The Docker image is built only after the test job has completed successfully.

### Path Filters

The workflow runs only when files in the `app_python` directory or the workflow file itself are changed.

### Secrets

Docker Hub credentials and the Snyk token are stored in GitHub Actions secrets.

Used secrets:

- `DOCKERHUB_USERNAME`
- `DOCKERHUB_TOKEN`
- `SNYK_TOKEN`

### Pull Request Validation

The workflow runs on pull requests to validate code before merging.

### Linting

Ruff is used to check code quality before tests and Docker publishing.

## Snyk

Snyk is used to check Python dependencies for known vulnerabilities.

The workflow fails if Snyk finds vulnerabilities with high severity or higher.
101 changes: 101 additions & 0 deletions app_python/DOCKER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Docker

## Dockerfile

The application is containerized with Docker using the following image:

```dockerfile
FROM python:3.12-alpine3.20
```

A fixed Python and Alpine version is used instead of a floating tag.

## Best practices used

### Non-root user

The container runs the application as `appuser`, not as `root`:

```dockerfile
RUN addgroup -S appgroup \
&& adduser -D -h /home/appuser -G appgroup appuser

USER appuser
```

The user's home directory is used as the working directory:

```dockerfile
WORKDIR /home/appuser
```

### Specific files are copied

The image does not copy the whole project directory. Only files required to run the application are copied:

```dockerfile
COPY --chown=appuser:appgroup requirements.txt .
COPY --chown=appuser:appgroup app.py .
```

### Layer order

Dependencies are copied and installed before the application code:

```dockerfile
COPY --chown=appuser:appgroup requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY --chown=appuser:appgroup app.py .
```

This keeps dependency installation in a separate layer.

### No pip cache

Python packages are installed without storing the pip cache:

```dockerfile
RUN pip install --no-cache-dir -r requirements.txt
```

### .dockerignore

The `.dockerignore` file excludes files that are not required for building the image:

```dockerignore
venv/
__pycache__/
*.pyc
gunicorn.ctl
```

## Build

```bash
docker build -t moscow-time-app:1.0.0 .
```

## Run

```bash
docker run --rm -d --name moscow-time-app -p 8080:8080 moscow-time-app:1.0.0
```

## Check

```bash
curl http://localhost:8080
```

```bash
docker exec moscow-time-app id
```

The `id` command must show that the user id is not `0`.

## Stop

```bash
docker stop moscow-time-app
```
17 changes: 17 additions & 0 deletions app_python/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
FROM python:3.12-alpine3.20

RUN addgroup -S appgroup \
&& adduser -D -h /home/appuser -G appgroup appuser

WORKDIR /home/appuser

COPY --chown=appuser:appgroup requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY --chown=appuser:appgroup app.py .

EXPOSE 8080

USER appuser

CMD ["gunicorn", "-b", "0.0.0.0:8080", "app:app"]
54 changes: 54 additions & 0 deletions app_python/PYTHON.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Python Web Application

## Framework Choice

For this lab I used Flask via gunicorn because it is a simple and lightweight Python web framework.
It is a good choice for a small application that only needs to display the current time.

## Best Practices Used

- Dependencies are kept minimal.
- Runtime dependencies are separated from development dependencies.
- The application logic for Moscow time generation is separated into a dedicated function.
- The project structure is simple and clear.
- Tests are automated and can be executed locally and in CI.

## Coding Standards

- Code is written in PEP 8 format.
- Ruff is used as a linter.

## Testing

The application includes automated unit tests written with pytest.

Implemented tests:

- `test_get_moscow_time_format` checks that the Moscow time function returns time in the expected format.
- `test_index_returns_success_status_code` checks that the main route returns HTTP 200.
- `test_index_contains_expected_content` checks that the response contains the expected page text.
- `test_index_contains_time_value` checks that the page contains a generated time value.

Testing best practices used:

- Tests are isolated and do not require a running external server.
- Flask test client is used instead of manual browser checks.
- The time generation logic is separated into a function to make it easier to test.
- Tests validate both application logic and HTTP response behavior.
- Tests import the application as a normal Python package: `app_python.app`.

Run tests from the repository root:

```bash
python -m pytest app_python/tests -q
```

## Code Quality

Code quality is checked automatically in CI with Ruff.

Run linting from the repository root:

```bash
ruff check app_python
```
Loading
Loading