diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml new file mode 100644 index 0000000000..ac2a67a5d8 --- /dev/null +++ b/.github/workflows/python-ci.yml @@ -0,0 +1,58 @@ +name: Python CI + +on: + push: + branches: [ master, lab03 ] + paths: + - 'app_python/**' + - '.github/workflows/python-ci.yml' + pull_request: + paths: + - 'app_python/**' + +jobs: + test-build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + cache: "pip" + + - name: Install dependencies + run: | + pip install -r app_python/requirements.txt + pip install -r app_python/requirements-dev.txt + + - name: Lint + run: ruff check app_python + + - name: Run tests + run: pytest app_python/tests + + - name: Generate version + run: echo "VERSION=$(date +%Y.%m.%d)" >> $GITHUB_ENV + + - name: Login Docker + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Snyk Scan + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + run: snyk test --file=app_python/requirements.txt + + - name: Build and Push Docker Image + uses: docker/build-push-action@v6 + with: + context: ./app_python + push: true + tags: | + maksimmenshikh/devops-info-service:${{ env.VERSION }} + maksimmenshikh/devops-info-service:latest diff --git a/app_python/README.md b/app_python/README.md index 9c088a0c2b..e1613427b9 100644 --- a/app_python/README.md +++ b/app_python/README.md @@ -123,3 +123,11 @@ docker run -p 5000:5000 maksimmenshikh/devops-info-service ### Pull from Docker Hub docker pull maksimmenshikh/devops-info-service + +## Running Tests + +cd app_python +pip install -r requirements-dev.txt +pytest -v + +![CI](https://github.com/MMenshikh/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg) diff --git a/app_python/docs/LAB03.md b/app_python/docs/LAB03.md new file mode 100644 index 0000000000..3939f0754c --- /dev/null +++ b/app_python/docs/LAB03.md @@ -0,0 +1,93 @@ +# LAB03 --- CI/CD Pipeline with GitHub Actions + +## CI/CD Practices Applied + +### Automated Builds + +Docker image is automatically built on push and pull requests using +GitHub Actions. + +### Secure Secrets Management + +Sensitive data stored in GitHub Repository Secrets instead of hardcoding +credentials. + +### Container Security Scanning + +Snyk integrated into pipeline to scan Docker image for vulnerabilities. + +### Continuous Delivery + +Docker image automatically pushed to DockerHub after successful pipeline +execution. + +------------------------------------------------------------------------ + +## Pipeline Information & Decisions + +CI Platform: GitHub Actions\ +Container Registry: DockerHub\ +Security Scanner: Snyk + +Triggers: - push to main - pull requests + +Pipeline Stages: 1. Repository checkout 2. Docker image build 3. +Security scan with Snyk 4. DockerHub login 5. Image push to registry + +------------------------------------------------------------------------ + +## Workflow Execution Process + +### Automatic Trigger + +Pipeline runs when: + +- code is pushed +- pull request is created + +### Build Stage + +Docker image built using: + +docker build -t devops-info-service . + +### Security Scan + +Snyk scans Docker image for known vulnerabilities before publishing. + +### Push Stage + +Image pushed to: + +maksimmenshikh/devops-info-service + +------------------------------------------------------------------------ + +## Technical Analysis + +Automated CI reduces manual deployment steps and human error. + +Secrets stored in GitHub prevent credential exposure in repository code. + +Security scanning ensures vulnerabilities are detected early in +development. + +Automated publishing guarantees consistent container versions. + +------------------------------------------------------------------------ + +## Challenges & Solutions + +### Missing Secrets + +Pipeline failed due to missing repository secrets.\ +Solved by adding DOCKERHUB_TOKEN, DOCKERHUB_USERNAME and SNYK_TOKEN. + +### Docker Authentication Failure + +Fixed by generating DockerHub access token and configuring +docker/login-action. + +### Failed Security Scan + +Resolved by rebuilding image and ensuring dependencies were up to date. diff --git a/app_python/docs/screenshots/Lab03/tests-local-passed.png b/app_python/docs/screenshots/Lab03/tests-local-passed.png new file mode 100644 index 0000000000..df8a49a77e Binary files /dev/null and b/app_python/docs/screenshots/Lab03/tests-local-passed.png differ diff --git a/app_python/requirements-dev.txt b/app_python/requirements-dev.txt new file mode 100644 index 0000000000..eebbc87319 --- /dev/null +++ b/app_python/requirements-dev.txt @@ -0,0 +1,3 @@ +pytest==8.3.5 +pytest-cov==5.0.0 +ruff==0.9.0 diff --git a/app_python/tests/test_app.py b/app_python/tests/test_app.py new file mode 100644 index 0000000000..a4fe2af9d7 --- /dev/null +++ b/app_python/tests/test_app.py @@ -0,0 +1,40 @@ +import pytest +from app import app + + +@pytest.fixture +def client(): + app.config["TESTING"] = True + with app.test_client() as client: + yield client + + +def test_index_ok(client): + response = client.get("/") + assert response.status_code == 200 + + data = response.get_json() + + assert "service" in data + assert "system" in data + assert "runtime" in data + assert "request" in data + assert "endpoints" in data + + +def test_health_ok(client): + response = client.get("/health") + assert response.status_code == 200 + + data = response.get_json() + assert data["status"] == "healthy" + assert "timestamp" in data + assert "uptime_seconds" in data + + +def test_404(client): + response = client.get("/unknown") + assert response.status_code == 404 + + data = response.get_json() + assert data["error"] == "Not Found"