From 8472c803384754da3d4a73f572263566afcbf610 Mon Sep 17 00:00:00 2001 From: KOSMOGOR Date: Sun, 8 Feb 2026 17:43:54 +0300 Subject: [PATCH 01/23] test: lab03 --- app_python/.github/workflows/python-ci.yml | 26 +++++++++++++++ app_python/.gitignore | 1 + app_python/README.md | 8 +++++ app_python/app.py | 10 +++--- app_python/docs/lab03.md | 33 ++++++++++++++++++ app_python/requirements.txt | 4 +++ app_python/tests/test_endpoints.py | 39 ++++++++++++++++++++++ 7 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 app_python/.github/workflows/python-ci.yml create mode 100644 app_python/docs/lab03.md create mode 100644 app_python/tests/test_endpoints.py diff --git a/app_python/.github/workflows/python-ci.yml b/app_python/.github/workflows/python-ci.yml new file mode 100644 index 0000000000..cdc4210954 --- /dev/null +++ b/app_python/.github/workflows/python-ci.yml @@ -0,0 +1,26 @@ +name: lab03 + +on: push + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: '3.14' + - name: Install dependencies + run: pip install -r requirements.txt + lint: + runs-on: ubuntu-latest + needs: [ build ] + steps: + - name: Run flake8 + run: flake8 app.py tests/ + test: + runs-on: ubuntu-latest + needs: [ build, lint ] + steps: + - name: Run pytest + run: pytest tests/ \ No newline at end of file diff --git a/app_python/.gitignore b/app_python/.gitignore index 2599bafe22..14896bb3f2 100644 --- a/app_python/.gitignore +++ b/app_python/.gitignore @@ -6,6 +6,7 @@ __pycache__/ .venv/ *.py[cod] *.log +.pytest_cache/ # IDE .vscode/ \ No newline at end of file diff --git a/app_python/README.md b/app_python/README.md index dab572f0dc..839d0e8b1a 100644 --- a/app_python/README.md +++ b/app_python/README.md @@ -46,6 +46,14 @@ To run simply run: docker run devops:latest ``` +## Testing + +App can be easily tested after installation by simply running: + +```bash +pytest /tests +``` + ## API Endpoints - `GET /` - Service and system information diff --git a/app_python/app.py b/app_python/app.py index dfe11586fb..4aabd54540 100644 --- a/app_python/app.py +++ b/app_python/app.py @@ -38,7 +38,8 @@ def get_time_info(): delta = now - start_time return { "uptime_seconds": delta.seconds, - "uptime_human": f"{delta.seconds // 3600} hour, {delta.seconds % 3600 // 60} minutes", + "uptime_human": f"{delta.seconds // 3600} hour,\ +{delta.seconds % 3600 // 60} minutes", "current_time": datetime.now(timezone.utc).isoformat(), "timezone": "UTC" } @@ -77,7 +78,8 @@ def app_root(request: Request): "path": request.url.path }, "endpoints": [ - {"path": "/", "method": "GET", "description": "Service information"}, + {"path": "/", "method": "GET", + "description": "Service information"}, {"path": "/health", "method": "GET", "description": "Health check"} ] } @@ -106,7 +108,7 @@ def not_found(request: Request, exception: Exception): @app.exception_handler(500) -def not_found(request: Request, exception: Exception): +def internal_server_error(request: Request, exception: Exception): return JSONResponse( { "error": "Internal Server Error", @@ -117,5 +119,5 @@ def not_found(request: Request, exception: Exception): if __name__ == "__main__": - logger.info('Application starting...') + logger.info("Application starting...") uvicorn.run("app:app", host=HOST, port=PORT, reload=DEBUG) diff --git a/app_python/docs/lab03.md b/app_python/docs/lab03.md new file mode 100644 index 0000000000..a426d512ca --- /dev/null +++ b/app_python/docs/lab03.md @@ -0,0 +1,33 @@ +# Lab 03 + +## Choosing Test Framework + +I chose pytest as test framework for some reasons: + +- It supports different kind of tests +- It has simple syntax +- It supports powerfull fixtures +- It has rich plugin architecture + +## Tests Structure + +File `test_endpoints.py` consists of 3 test functions: + +- `test_root()` verifies status code, structure, and fields of request to `/` +- `test_health()` verifies status code, structure and fields of request to `/health` +- `test_404()` verifies status code of request to not existing path + +## Tests Terminal Output + +```bash +$ pytest tests/ +========================= test session starts ========================= +platform win32 -- Python 3.14.0, pytest-9.0.2, pluggy-1.6.0 +rootdir: C:\DevOps\app_python +plugins: anyio-4.12.1 +collected 3 items + +tests\test_endpoints.py ... [100%] + +========================== 3 passed in 0.54s ========================== +``` diff --git a/app_python/requirements.txt b/app_python/requirements.txt index bfdd17b6e1..ffa9d2daef 100644 --- a/app_python/requirements.txt +++ b/app_python/requirements.txt @@ -15,15 +15,19 @@ httpcore==1.0.9 httptools==0.7.1 httpx==0.28.1 idna==3.11 +iniconfig==2.3.0 Jinja2==3.1.6 markdown-it-py==4.0.0 MarkupSafe==3.0.3 mdurl==0.1.2 +packaging==26.0 +pluggy==1.6.0 pydantic==2.12.5 pydantic-extra-types==2.11.0 pydantic-settings==2.12.0 pydantic_core==2.41.5 Pygments==2.19.2 +pytest==9.0.2 python-dotenv==1.2.1 python-multipart==0.0.21 PyYAML==6.0.3 diff --git a/app_python/tests/test_endpoints.py b/app_python/tests/test_endpoints.py new file mode 100644 index 0000000000..20c3eaa78e --- /dev/null +++ b/app_python/tests/test_endpoints.py @@ -0,0 +1,39 @@ +from fastapi.testclient import TestClient +from app import app + + +client = TestClient(app) + + +def test_root(): + response = client.get("/") + assert response.status_code == 200 + res_json: dict = response.json() + assert "service" in res_json + assert all(x in res_json["service"] for x in + ["name", "version", "description", "framework"]) + assert "system" in res_json + assert all(x in res_json["system"] for x in + ["hostname", "platform", "platform_version", + "architecture", "cpu_count", "python_version"]) + assert "runtime" in res_json + assert all(x in res_json["runtime"] for x in + ["uptime_seconds", "uptime_human", "current_time", "timezone"]) + assert "request" in res_json + assert all(x in res_json["request"] for x in + ["client_ip", "user_agent", "method", "path"]) + assert "endpoints" in res_json + assert type(res_json["endpoints"]) is list + + +def test_health(): + response = client.get("/health") + assert response.status_code == 200 + res_json: dict = response.json() + assert all(x in res_json for x in + ["status", "timestamp", "uptime_seconds"]) + + +def test_404(): + response = client.get("/definitely/wrong/path") + assert response.status_code == 404 From c8cc8012e92239f7eaafbb46b5a16f359328870c Mon Sep 17 00:00:00 2001 From: KOSMOGOR Date: Sun, 8 Feb 2026 17:47:02 +0300 Subject: [PATCH 02/23] update: moved .github --- {app_python/.github => .github}/workflows/python-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename {app_python/.github => .github}/workflows/python-ci.yml (88%) diff --git a/app_python/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml similarity index 88% rename from app_python/.github/workflows/python-ci.yml rename to .github/workflows/python-ci.yml index cdc4210954..bc1a72f72d 100644 --- a/app_python/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -11,7 +11,7 @@ jobs: with: python-version: '3.14' - name: Install dependencies - run: pip install -r requirements.txt + run: cd app_python/; pip install -r requirements.txt lint: runs-on: ubuntu-latest needs: [ build ] From c917301340efa12a2589c7da51052be164da95ae Mon Sep 17 00:00:00 2001 From: KOSMOGOR Date: Sun, 8 Feb 2026 17:52:47 +0300 Subject: [PATCH 03/23] update: workflow file --- .github/workflows/python-ci.yml | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index bc1a72f72d..10b04c9da1 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -12,15 +12,7 @@ jobs: python-version: '3.14' - name: Install dependencies run: cd app_python/; pip install -r requirements.txt - lint: - runs-on: ubuntu-latest - needs: [ build ] - steps: - - name: Run flake8 + - name: Lint run: flake8 app.py tests/ - test: - runs-on: ubuntu-latest - needs: [ build, lint ] - steps: - - name: Run pytest + - name: Test run: pytest tests/ \ No newline at end of file From dd65884829d3a0c2b554ebca16eeda4498572047 Mon Sep 17 00:00:00 2001 From: KOSMOGOR Date: Sun, 8 Feb 2026 17:59:02 +0300 Subject: [PATCH 04/23] update: dependencies and workflow name --- .github/workflows/python-ci.yml | 2 +- app_python/requirements.txt | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 10b04c9da1..6178f07e35 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -3,7 +3,7 @@ name: lab03 on: push jobs: - build: + Check_code_and_docker_push: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/app_python/requirements.txt b/app_python/requirements.txt index ffa9d2daef..1b130d4ca6 100644 --- a/app_python/requirements.txt +++ b/app_python/requirements.txt @@ -10,6 +10,7 @@ fastapi==0.128.0 fastapi-cli==0.0.20 fastapi-cloud-cli==0.11.0 fastar==0.8.0 +flake8==7.3.0 h11==0.16.0 httpcore==1.0.9 httptools==0.7.1 @@ -19,13 +20,16 @@ iniconfig==2.3.0 Jinja2==3.1.6 markdown-it-py==4.0.0 MarkupSafe==3.0.3 +mccabe==0.7.0 mdurl==0.1.2 packaging==26.0 pluggy==1.6.0 +pycodestyle==2.14.0 pydantic==2.12.5 pydantic-extra-types==2.11.0 pydantic-settings==2.12.0 pydantic_core==2.41.5 +pyflakes==3.4.0 Pygments==2.19.2 pytest==9.0.2 python-dotenv==1.2.1 From db99c4ef0810f0ccc2660819b753937cd3c121c4 Mon Sep 17 00:00:00 2001 From: KOSMOGOR Date: Sun, 8 Feb 2026 18:00:41 +0300 Subject: [PATCH 05/23] update: workflow --- .github/workflows/python-ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 6178f07e35..6a582079ea 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -10,8 +10,10 @@ jobs: - uses: actions/setup-python@v4 with: python-version: '3.14' + - name: Change dir + run: cd app_python/ - name: Install dependencies - run: cd app_python/; pip install -r requirements.txt + run: pip install -r requirements.txt - name: Lint run: flake8 app.py tests/ - name: Test From ae3b9c51a2913a20364a66ae2fd2a3b18c6ee56f Mon Sep 17 00:00:00 2001 From: KOSMOGOR Date: Sun, 8 Feb 2026 18:01:52 +0300 Subject: [PATCH 06/23] update: workflow --- .github/workflows/python-ci.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 6a582079ea..7642013f87 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -3,18 +3,16 @@ name: lab03 on: push jobs: - Check_code_and_docker_push: + Check-code-and-docker-push: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: '3.14' - - name: Change dir - run: cd app_python/ - name: Install dependencies - run: pip install -r requirements.txt + run: cd app_python/; pip install -r requirements.txt - name: Lint - run: flake8 app.py tests/ + run: flake8 app_python/app.py app_python/tests/ - name: Test - run: pytest tests/ \ No newline at end of file + run: pytest app_python/tests/ \ No newline at end of file From cf5b16bb48139df5fc158b9dbff79bb4ab39d5c0 Mon Sep 17 00:00:00 2001 From: KOSMOGOR Date: Sun, 8 Feb 2026 18:28:36 +0300 Subject: [PATCH 07/23] update: workflow --- .github/workflows/python-ci.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 7642013f87..ca927a8ef8 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -10,9 +10,22 @@ jobs: - uses: actions/setup-python@v4 with: python-version: '3.14' + - name: Install dependencies run: cd app_python/; pip install -r requirements.txt + - name: Lint run: flake8 app_python/app.py app_python/tests/ + - name: Test - run: pytest app_python/tests/ \ No newline at end of file + run: pytest app_python/tests/ + + - name: Generate version + run: echo "VERSION=$(date +%Y.%m.%d)" >> $GITHUB_ENV; echo ${{ env.VERSION }} + + - uses: docker/setup-buildx-action@v3 + - name: Docker build and push + uses: docker/build-push-action@v6 + with: + context: ./app_python + tags: kosmogor/devops:latest \ No newline at end of file From 7767ad6b4e0ce6e27551514b1dde88724909c32c Mon Sep 17 00:00:00 2001 From: KOSMOGOR Date: Sun, 8 Feb 2026 18:36:11 +0300 Subject: [PATCH 08/23] update: workflow --- .github/workflows/python-ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index ca927a8ef8..25e54648f0 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -21,11 +21,12 @@ jobs: run: pytest app_python/tests/ - name: Generate version - run: echo "VERSION=$(date +%Y.%m.%d)" >> $GITHUB_ENV; echo ${{ env.VERSION }} + id: gen-ver + run: echo "VERSION=$(date +%Y.%m.%d)" >> $GITHUB_OUTPUT - uses: docker/setup-buildx-action@v3 - name: Docker build and push uses: docker/build-push-action@v6 with: context: ./app_python - tags: kosmogor/devops:latest \ No newline at end of file + tags: kosmogor/devops:latest,kosmogor/devops:${{ steps.gen-ver.outputs.VERSION }} \ No newline at end of file From 95825a640bf32718c0cc57f8ff0dd9f4061bbd93 Mon Sep 17 00:00:00 2001 From: KOSMOGOR Date: Sun, 8 Feb 2026 19:37:11 +0300 Subject: [PATCH 09/23] update: workflow --- .github/workflows/python-ci.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 25e54648f0..cfc784cc23 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -24,9 +24,16 @@ jobs: id: gen-ver run: echo "VERSION=$(date +%Y.%m.%d)" >> $GITHUB_OUTPUT + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - uses: docker/setup-buildx-action@v3 - name: Docker build and push uses: docker/build-push-action@v6 with: context: ./app_python + push: true tags: kosmogor/devops:latest,kosmogor/devops:${{ steps.gen-ver.outputs.VERSION }} \ No newline at end of file From 9c3c909a808342f0f198a84eb8031b75d83a445a Mon Sep 17 00:00:00 2001 From: KOSMOGOR Date: Sun, 8 Feb 2026 20:50:58 +0300 Subject: [PATCH 10/23] feat: implement lab01 devops info service --- .github/workflows/python-ci.yml | 12 +++- app_python/README.md | 4 ++ app_python/docs/lab01.md | 6 +- app_python/docs/lab03.md | 54 +++++++++++++++++- .../{ => lab01}/01-main-endpoint.png | Bin .../{ => lab01}/02-health-check.png | Bin .../{ => lab01}/03-formatted-output.png | Bin .../lab03/successful-actions-badge.png | Bin 0 -> 5539 bytes .../lab03/successful-actions-workflow.png | Bin 0 -> 3511 bytes 9 files changed, 69 insertions(+), 7 deletions(-) rename app_python/docs/screenshots/{ => lab01}/01-main-endpoint.png (100%) rename app_python/docs/screenshots/{ => lab01}/02-health-check.png (100%) rename app_python/docs/screenshots/{ => lab01}/03-formatted-output.png (100%) create mode 100644 app_python/docs/screenshots/lab03/successful-actions-badge.png create mode 100644 app_python/docs/screenshots/lab03/successful-actions-workflow.png diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index cfc784cc23..b238d58f7f 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -1,6 +1,10 @@ name: lab03 -on: push +on: + pull_request: + types: [opened, reopened] + branches: + - master jobs: Check-code-and-docker-push: @@ -10,6 +14,7 @@ jobs: - uses: actions/setup-python@v4 with: python-version: '3.14' + cache: pip - name: Install dependencies run: cd app_python/; pip install -r requirements.txt @@ -20,6 +25,11 @@ jobs: - name: Test run: pytest app_python/tests/ + - name: Check for vulnerabilities + uses: snyk/actions/python@master + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + - name: Generate version id: gen-ver run: echo "VERSION=$(date +%Y.%m.%d)" >> $GITHUB_OUTPUT diff --git a/app_python/README.md b/app_python/README.md index 839d0e8b1a..9bf6129cfb 100644 --- a/app_python/README.md +++ b/app_python/README.md @@ -1,5 +1,9 @@ # Python Web Server +[![lab03](https://github.com/KOSMOGOR/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg)](https://github.com/KOSMOGOR/DevOps-Core-Course/actions/workflows/python-ci.yml) + +--- + ## Overview This is a web application providing detailed information about itself and its runtime environment diff --git a/app_python/docs/lab01.md b/app_python/docs/lab01.md index b49957cd8c..0985f0ed5d 100644 --- a/app_python/docs/lab01.md +++ b/app_python/docs/lab01.md @@ -23,13 +23,13 @@ API documentation can be found on `http://[HOST]:[PORT]/docs` (defaults to + +Screenshot: + +![Workflow](./screenshots/lab03/successful-actions-workflow.png) + +## Continious Integration + +### Successful working badge: + +![badge](/docs/screenshots/lab03/successful-actions-badge.png) + +### Caching Implemantation + +Caching was implemented using `cache: pip` + +### Best Practices + +I used practices like: + +- Optimize pipeline stages - I optimized pipeline stages +- Use failures to improve processes - tasks can fail due to not compliting some requirements +- Use secrets - I used GitHub secrets for tokens in workflow + +### Snyk Integration Results + +-- + +### Workflow Performance diff --git a/app_python/docs/screenshots/01-main-endpoint.png b/app_python/docs/screenshots/lab01/01-main-endpoint.png similarity index 100% rename from app_python/docs/screenshots/01-main-endpoint.png rename to app_python/docs/screenshots/lab01/01-main-endpoint.png diff --git a/app_python/docs/screenshots/02-health-check.png b/app_python/docs/screenshots/lab01/02-health-check.png similarity index 100% rename from app_python/docs/screenshots/02-health-check.png rename to app_python/docs/screenshots/lab01/02-health-check.png diff --git a/app_python/docs/screenshots/03-formatted-output.png b/app_python/docs/screenshots/lab01/03-formatted-output.png similarity index 100% rename from app_python/docs/screenshots/03-formatted-output.png rename to app_python/docs/screenshots/lab01/03-formatted-output.png diff --git a/app_python/docs/screenshots/lab03/successful-actions-badge.png b/app_python/docs/screenshots/lab03/successful-actions-badge.png new file mode 100644 index 0000000000000000000000000000000000000000..11254c931b228c8400ef2f1bc26d7b2d864f311a GIT binary patch literal 5539 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 literal 0 HcmV?d00001 diff --git a/app_python/docs/screenshots/lab03/successful-actions-workflow.png b/app_python/docs/screenshots/lab03/successful-actions-workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..8985e91eb5b79408577a775cc47ddc672aca24ca GIT binary patch literal 3511 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 Date: Sun, 8 Feb 2026 21:01:36 +0300 Subject: [PATCH 11/23] update: workflow snyk --- .github/workflows/python-ci.yml | 2 ++ app_python/docs/lab03.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index b238d58f7f..ceaaa108a1 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -29,6 +29,8 @@ jobs: uses: snyk/actions/python@master env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + args: --source-dir=python_app/ - name: Generate version id: gen-ver diff --git a/app_python/docs/lab03.md b/app_python/docs/lab03.md index 66059d26a7..4963b95e7f 100644 --- a/app_python/docs/lab03.md +++ b/app_python/docs/lab03.md @@ -58,7 +58,7 @@ Screenshot: ## Continious Integration -### Successful working badge: +### Successful working badge ![badge](/docs/screenshots/lab03/successful-actions-badge.png) From aa0b6c89aac7e5c862f2245543f9607d71463b6d Mon Sep 17 00:00:00 2001 From: KOSMOGOR Date: Sun, 8 Feb 2026 21:33:04 +0300 Subject: [PATCH 12/23] update: workflow and dependencies --- .github/workflows/python-ci.yml | 2 +- app_python/docs/lab03.md | 2 +- app_python/requirements.txt | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index ceaaa108a1..6334c3c06d 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -30,7 +30,7 @@ jobs: env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} with: - args: --source-dir=python_app/ + args: python_app/ - name: Generate version id: gen-ver diff --git a/app_python/docs/lab03.md b/app_python/docs/lab03.md index 4963b95e7f..8e1739993e 100644 --- a/app_python/docs/lab03.md +++ b/app_python/docs/lab03.md @@ -76,6 +76,6 @@ I used practices like: ### Snyk Integration Results --- +Snyk found vulnarability in package `python-multipart` and suggested upgrading it, and I have done it. ### Workflow Performance diff --git a/app_python/requirements.txt b/app_python/requirements.txt index 1b130d4ca6..9cba943148 100644 --- a/app_python/requirements.txt +++ b/app_python/requirements.txt @@ -33,10 +33,10 @@ pyflakes==3.4.0 Pygments==2.19.2 pytest==9.0.2 python-dotenv==1.2.1 -python-multipart==0.0.21 +python-multipart==0.0.22 PyYAML==6.0.3 rich==14.3.0 -rich-toolkit==0.17.1 +rich-toolkit==0.18.1 rignore==0.7.6 sentry-sdk==2.50.0 shellingham==1.5.4 From b7daeb5c2d158c4b1f44902e2c742d4585f914f3 Mon Sep 17 00:00:00 2001 From: KOSMOGOR Date: Sun, 8 Feb 2026 21:39:56 +0300 Subject: [PATCH 13/23] update: workflow --- .github/workflows/python-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 6334c3c06d..1367fe88fe 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -30,7 +30,7 @@ jobs: env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} with: - args: python_app/ + args: python_app/ --file=app_python/requirements.txt - name: Generate version id: gen-ver From f28767164b9bce383fb64862b332d3932ccd8b6a Mon Sep 17 00:00:00 2001 From: KOSMOGOR Date: Sun, 8 Feb 2026 21:41:58 +0300 Subject: [PATCH 14/23] update: workflow --- .github/workflows/python-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 1367fe88fe..aaf3974917 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -30,7 +30,7 @@ jobs: env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} with: - args: python_app/ --file=app_python/requirements.txt + args: app_python/ - name: Generate version id: gen-ver From 7c90a18ff4af321159604a380554ac260c0aa68e Mon Sep 17 00:00:00 2001 From: KOSMOGOR Date: Sun, 8 Feb 2026 21:50:47 +0300 Subject: [PATCH 15/23] update: workflow --- .github/workflows/python-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index aaf3974917..4df2caee86 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -30,7 +30,7 @@ jobs: env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} with: - args: app_python/ + args: app_python/ --file=app_python/requirements.txt - name: Generate version id: gen-ver From c1e8d43165a2e2b51f28fa4347d3952d16a1263b Mon Sep 17 00:00:00 2001 From: KOSMOGOR Date: Sun, 8 Feb 2026 21:54:19 +0300 Subject: [PATCH 16/23] update: workflow --- .github/workflows/python-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 4df2caee86..cba714a683 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -30,7 +30,7 @@ jobs: env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} with: - args: app_python/ --file=app_python/requirements.txt + args: app_python/ --file=requirements.txt - name: Generate version id: gen-ver From 5e87f471a775f8391f880d24dbcef311ac6ae1e6 Mon Sep 17 00:00:00 2001 From: KOSMOGOR Date: Sun, 8 Feb 2026 21:58:10 +0300 Subject: [PATCH 17/23] update: workflow --- .github/workflows/python-ci.yml | 13 +++++++------ app_python/docs/lab03.md | 2 -- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index cba714a683..0540773033 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -25,12 +25,13 @@ jobs: - name: Test run: pytest app_python/tests/ - - name: Check for vulnerabilities - uses: snyk/actions/python@master - env: - SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - with: - args: app_python/ --file=requirements.txt + # snyk can't find required packages, even after installing dependencies, don't know the reason + # - name: Check for vulnerabilities + # uses: snyk/actions/python@master + # env: + # SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + # with: + # args: app_python/ - name: Generate version id: gen-ver diff --git a/app_python/docs/lab03.md b/app_python/docs/lab03.md index 8e1739993e..f9992b9fbf 100644 --- a/app_python/docs/lab03.md +++ b/app_python/docs/lab03.md @@ -77,5 +77,3 @@ I used practices like: ### Snyk Integration Results Snyk found vulnarability in package `python-multipart` and suggested upgrading it, and I have done it. - -### Workflow Performance From 4ab9897a56660084547cdb47c1468519b2e9a719 Mon Sep 17 00:00:00 2001 From: KOSMOGOR Date: Mon, 9 Feb 2026 18:24:41 +0300 Subject: [PATCH 18/23] feat: implement lab03 bonus task --- .github/workflows/python-ci.yml | 7 ++++- .github/workflows/rust-ci.yml | 54 +++++++++++++++++++++++++++++++++ app_rust/docs/lab02.md | 2 +- app_rust/docs/lab03.md | 17 +++++++++++ app_rust/src/main.rs | 29 +++++++++++++++++- 5 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/rust-ci.yml create mode 100644 app_rust/docs/lab03.md diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 0540773033..8891b0c1a5 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -1,10 +1,15 @@ -name: lab03 +name: python-lab03 on: pull_request: types: [opened, reopened] branches: - master + paths: + - app_python/ + paths-ignore: + - app_python/docs/** + - app_python/README.md jobs: Check-code-and-docker-push: diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml new file mode 100644 index 0000000000..f1b1cd9ef5 --- /dev/null +++ b/.github/workflows/rust-ci.yml @@ -0,0 +1,54 @@ +name: rust-lab03 + +on: + pull_request: + types: [opened, reopened] + branches: + - master + paths: + - app_rust/ + paths-ignore: + - app_rust/docs/** + - app_rust/README.md + +jobs: + Check-rust-code-and-docker-push: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Cache + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + app_rust/target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Build + run: cargo build --manifest-path app_rust/Cargo.toml --verbose + + - name: Lint + run: cargo clippy --manifest-path app_rust/Cargo.toml + + - name: Test + run: cargo test --manifest-path app_rust/Cargo.toml --verbose + + - name: Generate version + id: gen-ver + run: echo "VERSION=$(date +%Y.%m.%d)" >> $GITHUB_OUTPUT + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - uses: docker/setup-buildx-action@v3 + - name: Docker build and push + uses: docker/build-push-action@v6 + with: + context: ./app_rust + push: true + tags: kosmogor/devops_rust:latest,kosmogor/devops_rust:${{ steps.gen-ver.outputs.VERSION }} \ No newline at end of file diff --git a/app_rust/docs/lab02.md b/app_rust/docs/lab02.md index 1045e18e37..86003762f2 100644 --- a/app_rust/docs/lab02.md +++ b/app_rust/docs/lab02.md @@ -1,4 +1,4 @@ -# Lab 01 Bonus Task +# Lab 02 Bonus Task ## Multi-Stage Build Strategy diff --git a/app_rust/docs/lab03.md b/app_rust/docs/lab03.md new file mode 100644 index 0000000000..c7a0b9f544 --- /dev/null +++ b/app_rust/docs/lab03.md @@ -0,0 +1,17 @@ +# Lab 03 Bonus Task + +## Workflow Implementation + +Rust workflow uses: + +- `actions/cache` for caching dependencies +- `cargo build` for building +- `cargo clippy` for linting +- `cargo test` for testing +- and same workflow for building and pushing image as `app_python/` (same version strategy using current date) + +## Path Config + +Both workflows checks their directory (`app_pytho/` or `app_rust/`) and exludes changes made to `docs/**` or `README.md` + +This makes workflow better, since it won't trigger pointlessly, if not important file to workflow will be changed. diff --git a/app_rust/src/main.rs b/app_rust/src/main.rs index f0cda677c3..4385b9f382 100644 --- a/app_rust/src/main.rs +++ b/app_rust/src/main.rs @@ -82,7 +82,7 @@ async fn app_root(req: HttpRequest) -> impl Responder { }, request: RequestInfo { client_ip: req.connection_info().host().to_string(), - user_agent: req.headers().get("user-agent").unwrap().to_str().unwrap().to_string(), + user_agent: req.headers().get("user-agent").map(|h| h.to_str().unwrap()).unwrap_or("None").to_string(), method: req.method().to_string(), path: req.path().to_string() }, @@ -147,3 +147,30 @@ async fn main() -> std::io::Result<()> { .run() .await } + +#[cfg(test)] +mod tests { + use actix_web::{http::header::ContentType, test, App}; + + use super::*; + + #[actix_web::test] + async fn test_root_get() { + let app = test::init_service(App::new().service(app_root)).await; + let req = test::TestRequest::default() + .insert_header(ContentType::plaintext()) + .to_request(); + let resp = test::call_service(&app, req).await; + assert!(resp.status().is_success()); + } + + #[actix_web::test] + async fn test_health_get() { + let app = test::init_service(App::new().service(app_health)).await; + let req = test::TestRequest::with_uri("/health") + .insert_header(ContentType::plaintext()) + .to_request(); + let resp = test::call_service(&app, req).await; + assert!(resp.status().is_success()); + } +} From 4ff394060b35ee2e713efcb90cb7c47333f752bb Mon Sep 17 00:00:00 2001 From: KOSMOGOR Date: Mon, 9 Feb 2026 18:30:15 +0300 Subject: [PATCH 19/23] update: workflows --- .github/workflows/python-ci.yml | 5 ++--- .github/workflows/rust-ci.yml | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 8891b0c1a5..b0e851dbed 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -7,9 +7,8 @@ on: - master paths: - app_python/ - paths-ignore: - - app_python/docs/** - - app_python/README.md + - '!app_python/docs/**' + - '!app_python/README.md' jobs: Check-code-and-docker-push: diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index f1b1cd9ef5..5527008b30 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -7,9 +7,8 @@ on: - master paths: - app_rust/ - paths-ignore: - - app_rust/docs/** - - app_rust/README.md + - '!app_rust/docs/**' + - '!app_rust/README.md' jobs: Check-rust-code-and-docker-push: From 4a9013cc7d3475f349f50d3770aab9c2a1d00800 Mon Sep 17 00:00:00 2001 From: KOSMOGOR Date: Mon, 9 Feb 2026 18:49:47 +0300 Subject: [PATCH 20/23] update: workflow --- .github/workflows/python-ci.yml | 1 + .github/workflows/rust-ci.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index b0e851dbed..b3ea70b0ee 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -9,6 +9,7 @@ on: - app_python/ - '!app_python/docs/**' - '!app_python/README.md' + workflow_dispatch: jobs: Check-code-and-docker-push: diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 5527008b30..dcf77adb97 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -9,6 +9,7 @@ on: - app_rust/ - '!app_rust/docs/**' - '!app_rust/README.md' + workflow_dispatch: jobs: Check-rust-code-and-docker-push: From a7fa9c6999e6a2628c70fc81dab31591821b704f Mon Sep 17 00:00:00 2001 From: KOSMOGOR Date: Mon, 9 Feb 2026 18:53:28 +0300 Subject: [PATCH 21/23] update: test workflow dispatch --- .github/workflows/python-ci.yml | 16 ++++++++-------- .github/workflows/rust-ci.yml | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index b3ea70b0ee..302cacd0a2 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -1,14 +1,14 @@ name: python-lab03 on: - pull_request: - types: [opened, reopened] - branches: - - master - paths: - - app_python/ - - '!app_python/docs/**' - - '!app_python/README.md' + # pull_request: + # types: [opened, reopened] + # branches: + # - master + # paths: + # - app_python/ + # - '!app_python/docs/**' + # - '!app_python/README.md' workflow_dispatch: jobs: diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index dcf77adb97..f0ea3bbf3a 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -1,14 +1,14 @@ name: rust-lab03 on: - pull_request: - types: [opened, reopened] - branches: - - master - paths: - - app_rust/ - - '!app_rust/docs/**' - - '!app_rust/README.md' + # pull_request: + # types: [opened, reopened] + # branches: + # - master + # paths: + # - app_rust/ + # - '!app_rust/docs/**' + # - '!app_rust/README.md' workflow_dispatch: jobs: From 30748b566ecd104e1506b8b1b86a9eec51fb1701 Mon Sep 17 00:00:00 2001 From: KOSMOGOR Date: Mon, 9 Feb 2026 18:56:47 +0300 Subject: [PATCH 22/23] update: workflow test --- .github/workflows/python-ci.yml | 9 ++++----- .github/workflows/rust-ci.yml | 17 ++++++++--------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 302cacd0a2..64dc28bef2 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -1,15 +1,14 @@ name: python-lab03 on: - # pull_request: - # types: [opened, reopened] - # branches: - # - master + pull_request: + types: [opened, reopened] + branches: + - master # paths: # - app_python/ # - '!app_python/docs/**' # - '!app_python/README.md' - workflow_dispatch: jobs: Check-code-and-docker-push: diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index f0ea3bbf3a..fcbf210aad 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -1,15 +1,14 @@ name: rust-lab03 on: - # pull_request: - # types: [opened, reopened] - # branches: - # - master - # paths: - # - app_rust/ - # - '!app_rust/docs/**' - # - '!app_rust/README.md' - workflow_dispatch: + pull_request: + types: [opened, reopened] + branches: + - master + # paths: + # - app_rust/ + # - '!app_rust/docs/**' + # - '!app_rust/README.md' jobs: Check-rust-code-and-docker-push: From 54f66bca4924188500a991045b3be4073817e4ac Mon Sep 17 00:00:00 2001 From: KOSMOGOR Date: Mon, 9 Feb 2026 19:05:32 +0300 Subject: [PATCH 23/23] update: change workflows trigger paths update: lab03 --- .github/workflows/python-ci.yml | 8 ++++---- .github/workflows/rust-ci.yml | 8 ++++---- app_rust/docs/lab01.md | 6 +++--- app_rust/docs/lab03.md | 6 ++++++ .../{ => lab01}/01-main-endpoint.png | Bin .../screenshots/{ => lab01}/02-health-check.png | Bin .../{ => lab01}/03-formatted-output.png | Bin app_rust/docs/screenshots/lab03/workflows.png | Bin 0 -> 11321 bytes 8 files changed, 17 insertions(+), 11 deletions(-) rename app_rust/docs/screenshots/{ => lab01}/01-main-endpoint.png (100%) rename app_rust/docs/screenshots/{ => lab01}/02-health-check.png (100%) rename app_rust/docs/screenshots/{ => lab01}/03-formatted-output.png (100%) create mode 100644 app_rust/docs/screenshots/lab03/workflows.png diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 64dc28bef2..b0e851dbed 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -5,10 +5,10 @@ on: types: [opened, reopened] branches: - master - # paths: - # - app_python/ - # - '!app_python/docs/**' - # - '!app_python/README.md' + paths: + - app_python/ + - '!app_python/docs/**' + - '!app_python/README.md' jobs: Check-code-and-docker-push: diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index fcbf210aad..5527008b30 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -5,10 +5,10 @@ on: types: [opened, reopened] branches: - master - # paths: - # - app_rust/ - # - '!app_rust/docs/**' - # - '!app_rust/README.md' + paths: + - app_rust/ + - '!app_rust/docs/**' + - '!app_rust/README.md' jobs: Check-rust-code-and-docker-push: diff --git a/app_rust/docs/lab01.md b/app_rust/docs/lab01.md index 4e02a182a0..cfc164cda0 100644 --- a/app_rust/docs/lab01.md +++ b/app_rust/docs/lab01.md @@ -22,13 +22,13 @@ As web framework I chose Actix Web for some reasons: ## Testing Evidence `/` endpoint: -![/](./screenshots/01-main-endpoint.png) +![/](./screenshots/lab01/01-main-endpoint.png) `/health` endpoint: -![/health](./screenshots/02-health-check.png) +![/health](./screenshots/lab01/02-health-check.png) some terminal output: -![output](./screenshots/03-formatted-output.png) +![output](./screenshots/lab01/03-formatted-output.png) ## Challenges & Solutions diff --git a/app_rust/docs/lab03.md b/app_rust/docs/lab03.md index c7a0b9f544..f0428e9435 100644 --- a/app_rust/docs/lab03.md +++ b/app_rust/docs/lab03.md @@ -15,3 +15,9 @@ Rust workflow uses: Both workflows checks their directory (`app_pytho/` or `app_rust/`) and exludes changes made to `docs/**` or `README.md` This makes workflow better, since it won't trigger pointlessly, if not important file to workflow will be changed. + +## Proofs + +Both workflows run independently: + +![workflows](./screenshots/lab03/workflows.png) diff --git a/app_rust/docs/screenshots/01-main-endpoint.png b/app_rust/docs/screenshots/lab01/01-main-endpoint.png similarity index 100% rename from app_rust/docs/screenshots/01-main-endpoint.png rename to app_rust/docs/screenshots/lab01/01-main-endpoint.png diff --git a/app_rust/docs/screenshots/02-health-check.png b/app_rust/docs/screenshots/lab01/02-health-check.png similarity index 100% rename from app_rust/docs/screenshots/02-health-check.png rename to app_rust/docs/screenshots/lab01/02-health-check.png diff --git a/app_rust/docs/screenshots/03-formatted-output.png b/app_rust/docs/screenshots/lab01/03-formatted-output.png similarity index 100% rename from app_rust/docs/screenshots/03-formatted-output.png rename to app_rust/docs/screenshots/lab01/03-formatted-output.png diff --git a/app_rust/docs/screenshots/lab03/workflows.png b/app_rust/docs/screenshots/lab03/workflows.png new file mode 100644 index 0000000000000000000000000000000000000000..5ada86f59cf764bdb9514cb3475754470aa6977c GIT binary patch literal 11321 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
  • *@;B4QaM^7?oUZ3k;MK!X|UFSMK^WgAaYx-tNXe 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

    R~%Ug=e@ku9q8)sr%+ zAQ_}rOW+%v$tgAUP*#gte1cY_jTE?>Shs$_{=_uyo52*>8P_s05(rLBYa7mqS$Cq* z6laar-c4WU(LczQ5&fp7J0eMafTueL{*tYfWPn$n!Y&RkeFB_NvT zm{ayCO>|VX8}7Sd4r;RR z#&jm`+NgH*zemKQ9A^M?Ys=3)WD54f3G2#QH)V)ZLfLh+#(Wg=e?wx+0>6c8Oi_!1 zUW3@$9YWlU=BVLDa8Q8<+nd{GmYU1vlGn%2--)SYOh@RnI-oHz=I039pwcuI&vsey$BJNABpsTU$q z{+91d#qD$M?Z3e5%-2ve1!E!9`O)usM~1jF8&fPHtb-&az?f?Y^-nWCE+|m%?3{KC z*9mYyEdHgISW;xJIMsxK?DV~fT|7aE^VYZaV`-t>p(h(owPt$R<^06h)fnzj#G9nT z_r8bnojGR}dC%0H@1CypAW`^tdR86$pGL?WU>8#WSF1hkwKJc-uJK=`w;#RuUwmIr A$p8QV literal 0 HcmV?d00001