From 857d626128d20d8385b3c3fb86c04f291871fe93 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 19 Dec 2025 11:22:56 +0000 Subject: [PATCH] Add comprehensive README and import error handlers Co-authored-by: naveenkumar.s43 --- README.md | 459 ++++++++++++++++++++++++++++++++++++++++++++ service/__init__.py | 1 + 2 files changed, 460 insertions(+) diff --git a/README.md b/README.md index 791eb9cd8..ad5195158 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,12 @@ This repository contains the practice code for the labs in **IBM-CD0215EN-SkillsNetwork Introduction to CI/CD** +## Services in this repo + +This repo includes a small Flask service used throughout the labs: + +- **Hit Counter Service**: an in-memory counter API (see [HTTP API](#http-api-hit-counter-service)). + ## Contents - Lab 1: [Build an empty Pipeline](labs/01_base_pipeline/README.md) @@ -14,6 +20,459 @@ This repository contains the practice code for the labs in **IBM-CD0215EN-Skills - Lab 5: [Building an Image](labs/05_build_an_image/README.md) - Lab 6: [Deploy to Kubernetes](labs/06_deploy_to_kubernetes/README.md) +## Quickstart (local development) + +### Prerequisites + +- Python **3.9** +- `pip` + +### Install + +```bash +python -m venv .venv +source .venv/bin/activate +python -m pip install -U pip +pip install -r requirements.txt +``` + +### Run (Flask dev server) + +```bash +export FLASK_APP=service +flask run --host=0.0.0.0 --port=8000 +``` + +### Run (Gunicorn, production-style) + +```bash +gunicorn --bind 0.0.0.0:8000 service:app +``` + +### Run (Procfile via honcho) + +```bash +export PORT=8000 +honcho start +``` + +### Run (Docker) + +```bash +docker build -t hit-counter-service . +docker run --rm -p 8000:8000 hit-counter-service +``` + +### Test + +```bash +nosetests +``` + +Note: this repository targets **Python 3.9**. If you are running Python 3.12+ locally (where `nose` may not work), you can run the suite with the standard library test runner: + +```bash +python -m unittest discover -s tests -p "test*.py" -v +``` + +## HTTP API (Hit Counter Service) + +### Base URL + +The service listens on port **8000** by default. + +- **Local**: `http://localhost:8000` + +### Data model + +- A **counter** is identified by a string `name`. +- Each counter has a numeric value `counter` starting at `0`. +- Storage is **in-memory** (a Python dict). Counters reset when the process restarts. + +### Content types + +- Successful responses are JSON (`application/json`) except `DELETE`, which returns an empty body. +- Error responses are JSON when the service is imported as `service` (this repo registers JSON error handlers on import). + +### Error format + +Errors use this JSON shape: + +```json +{ + "status": 404, + "error": "Not Found", + "message": "404 Not Found: Counter foo does not exist" +} +``` + +### Endpoints + +#### `GET /health` + +Health check endpoint. + +- **Response 200** + +```json +{ "status": "OK" } +``` + +Example: + +```bash +curl -sS http://localhost:8000/health +``` + +#### `GET /` + +Service index and discovery document. + +- **Response 200** + +```json +{ + "status": 200, + "message": "Hit Counter Service", + "version": "1.0.0", + "url": "http://localhost:8000/counters" +} +``` + +Example: + +```bash +curl -sS http://localhost:8000/ +``` + +#### `GET /counters` + +List all counters. + +- **Response 200** + +```json +[ + { "name": "foo", "counter": 3 }, + { "name": "bar", "counter": 0 } +] +``` + +Example: + +```bash +curl -sS http://localhost:8000/counters +``` + +#### `POST /counters/` + +Create a new counter with value `0`. + +- **Response 201** + - Body: `{ "name": "", "counter": 0 }` + - Header: `Location: >` +- **Response 409** if the counter already exists + +Examples: + +```bash +curl -i -X POST http://localhost:8000/counters/foo +``` + +Using `httpie` (also included in `requirements.txt`): + +```bash +http -v POST :8000/counters/foo +``` + +#### `GET /counters/` + +Read a single counter. + +- **Response 200** + +```json +{ "name": "foo", "counter": 0 } +``` + +- **Response 404** if the counter does not exist + +Example: + +```bash +curl -sS http://localhost:8000/counters/foo +``` + +#### `PUT /counters/` + +Increment a counter by `1`. + +Notes: + +- This endpoint is intentionally an “increment” operation (it does not accept a request body). + +- **Response 200** + +```json +{ "name": "foo", "counter": 1 } +``` + +- **Response 404** if the counter does not exist + +Example: + +```bash +curl -sS -X PUT http://localhost:8000/counters/foo +``` + +#### `DELETE /counters/` + +Delete a counter. + +Notes: + +- This operation is **idempotent**: deleting a non-existent counter still returns `204`. + +- **Response 204** (empty body) + +Example: + +```bash +curl -i -X DELETE http://localhost:8000/counters/foo +``` + +### End-to-end usage example + +```bash +# Create +curl -sS -X POST http://localhost:8000/counters/foo + +# Read +curl -sS http://localhost:8000/counters/foo + +# Increment twice +curl -sS -X PUT http://localhost:8000/counters/foo +curl -sS -X PUT http://localhost:8000/counters/foo + +# List +curl -sS http://localhost:8000/counters + +# Delete +curl -i -X DELETE http://localhost:8000/counters/foo +``` + +## Python API (modules and public functions) + +This repo’s Python code is organized under the `service/` package. + +### `service:app` (Flask application) + +- **Import path**: `from service import app` +- Used by: + - **Gunicorn**: `gunicorn service:app` + - **Flask CLI**: `FLASK_APP=service flask run ...` + +### `service.routes` (HTTP route handlers) + +- **Public functions** + - `health()`: implements `GET /health` + - `index()`: implements `GET /` + - `list_counters()`: implements `GET /counters` + - `create_counters(name)`: implements `POST /counters/` + - `read_counters(name)`: implements `GET /counters/` + - `update_counters(name)`: implements `PUT /counters/` (increments by 1) + - `delete_counters(name)`: implements `DELETE /counters/` + - `reset_counters()`: **testing utility** that clears all counters only when `app.testing` is `True` + +### `service.common.status` (HTTP status constants) + +Descriptive constants for HTTP codes, e.g.: + +- `status.HTTP_200_OK` +- `status.HTTP_201_CREATED` +- `status.HTTP_404_NOT_FOUND` +- `status.HTTP_409_CONFLICT` + +Import: + +```python +from service.common import status +``` + +### `service.common.log_handlers` (logging) + +- `init_logging(app, logger_name: str)`: configures the Flask app logger to use an existing logger’s handlers (commonly `gunicorn.error`) and a consistent log formatter. + +Import: + +```python +from service.common.log_handlers import init_logging +``` + +### `service.common.error_handlers` (JSON error responses) + +This module registers JSON error handlers for common error statuses (400, 404, 405, 409, 415, 500). It is registered via import side effects when the `service` package is imported. + +## Tekton CI/CD components (labs) + +The `labs/` folders contain Tekton resources (Pipelines, Tasks, Triggers, and PVCs) that evolve across the course. These YAML files are intended to be applied to a Kubernetes/OpenShift cluster with Tekton installed. + +### Common concepts used in these labs + +- **Pipeline**: a sequence of Tasks with explicit dependencies (`runAfter`) +- **Task**: a reusable unit of work (one or more container steps) +- **Workspace + PVC**: shared filesystem between Tasks, backed by a `PersistentVolumeClaim` +- **Catalog Tasks / ClusterTasks**: some Pipelines reference Tasks that are not defined in this repo (e.g., `git-clone`, `flake8`) and must exist in the cluster (often installed from Tekton Hub / Tekton Catalog) + +### Lab 1: Base Pipeline (`labs/01_base_pipeline/`) + +Files: + +- `pipeline.yaml`: **template** skeleton (contains ``) +- `tasks.yaml`: **template** skeleton (contains ``) + +Intended outcome: a minimal Pipeline/Task YAML structure you can fill in during the lab. + +### Lab 2: Add Git trigger (`labs/02_add_git_trigger/`) + +#### Components + +- **Pipeline**: `cd-pipeline` (`pipeline.yaml`) + - Params: + - `repo-url` (required) + - `branch` (default `"master"`) + - Task graph: + - `checkout` (Task: `checkout`) → `lint` (Task: `echo`) → `tests` (Task: `echo`) → `build` (Task: `echo`) → `deploy` (Task: `echo`) +- **Tasks**: (`tasks.yaml`) + - `echo`: prints a provided message + - `checkout`: `git clone --branch ` using `bitnami/git:latest` +- **Triggers**: templates (placeholders to fill in during lab) + - `eventlistener.yaml` (contains ``) + - `triggerbinding.yaml` (contains ``) + - `triggertemplate.yaml` (contains ``) + +#### Example: apply + start a PipelineRun manually + +```bash +kubectl apply -f labs/02_add_git_trigger/tasks.yaml +kubectl apply -f labs/02_add_git_trigger/pipeline.yaml + +# Example (requires Tekton CLI 'tkn'): +tkn pipeline start cd-pipeline \ + -p repo-url=https://github.com//.git \ + -p branch=main \ + --showlog +``` + +### Lab 3: Use Tekton Catalog (`labs/03_use_tekton_catalog/`) + +#### Components + +- **Pipeline**: `cd-pipeline` (`pipeline.yaml`) + - Workspaces: + - `pipeline-workspace` + - Params: + - `repo-url` (required) + - `branch` (default `master`) + - Task graph: + - `init` (Task: `cleanup`, clears workspace) + - `clone` (Task: `git-clone`, **catalog task**, writes to workspace) + - `lint/tests/build/deploy` (Task: `echo`, placeholders) +- **Tasks**: (`tasks.yaml`) + - `cleanup`: deletes all files in a workspace safely + - `echo`: prints a provided message +- **PVC**: `pipelinerun-pvc` (`pvc.yaml`) + - Uses `storageClassName: skills-network-learner` (may need changing for your cluster) + +#### Example: apply + start + +```bash +kubectl apply -f labs/03_use_tekton_catalog/pvc.yaml +kubectl apply -f labs/03_use_tekton_catalog/tasks.yaml +kubectl apply -f labs/03_use_tekton_catalog/pipeline.yaml + +tkn pipeline start cd-pipeline \ + -p repo-url=https://github.com//.git \ + -p branch=main \ + -w name=pipeline-workspace,claimName=pipelinerun-pvc \ + --showlog +``` + +#### Required cluster tasks + +This Pipeline references `git-clone` which is not defined in this repo; install it in your cluster (commonly via Tekton Hub / Catalog). + +### Lab 4: Unit test automation (`labs/04_unit_test_automation/`) + +#### Components + +- **Pipeline**: `cd-pipeline` (`pipeline.yaml`) + - Adds real `lint` + `tests` stages using Tasks: + - `flake8` (**catalog task**, not defined in this repo) + - `nose` (defined in this repo’s `tasks.yaml`) +- **Tasks**: (`tasks.yaml`) + - `nose`: installs `requirements.txt` then runs `nosetests` inside `python:3.9-slim` + - `cleanup`, `echo` (same as earlier labs) +- **PVC**: `pipelinerun-pvc` (`pvc.yaml`) + +#### Example: apply + start + +```bash +kubectl apply -f labs/04_unit_test_automation/pvc.yaml +kubectl apply -f labs/04_unit_test_automation/tasks.yaml +kubectl apply -f labs/04_unit_test_automation/pipeline.yaml + +tkn pipeline start cd-pipeline \ + -p repo-url=https://github.com//.git \ + -p branch=main \ + -w name=pipeline-workspace,claimName=pipelinerun-pvc \ + --showlog +``` + +### Lab 5: Build an image (`labs/05_build_an_image/`) + +#### Components + +- **Pipeline**: `cd-pipeline` (`pipeline.yaml`) + - Adds `build-image` param and builds a container image via: + - `build` (ClusterTask: `buildah`) + - Params: + - `build-image` (required; e.g., `quay.io//hit-counter:latest`) + - `repo-url` (required) + - `branch` (default `master`) +- **PVC**: `pipelinerun-pvc` (`pvc.yaml`) +- **Tasks**: (`tasks.yaml`) includes `nose`, `cleanup`, `echo` + +#### Example: apply + start + +```bash +kubectl apply -f labs/05_build_an_image/pvc.yaml +kubectl apply -f labs/05_build_an_image/tasks.yaml +kubectl apply -f labs/05_build_an_image/pipeline.yaml + +tkn pipeline start cd-pipeline \ + -p repo-url=https://github.com//.git \ + -p branch=main \ + -p build-image=quay.io//hit-counter:latest \ + -w name=pipeline-workspace,claimName=pipelinerun-pvc \ + --showlog +``` + +#### Required cluster tasks + +- `git-clone` and `flake8` Tasks (catalog) +- `buildah` ClusterTask + +### Lab 6: Deploy to Kubernetes (`labs/06_deploy_to_kubernetes/`) + +This lab keeps the same pipeline shape as Lab 5 (clone → lint → tests → build) and leaves the final `deploy` stage as an `echo` placeholder that you implement during the lab. + +Files: + +- `pipeline.yaml`: `cd-pipeline` including `buildah` and an `echo` deploy placeholder +- `tasks.yaml`: `cleanup`, `nose`, `echo` +- `pvc.yaml`: `pipelinerun-pvc` + ## Instructor John Rofrano, Senior Technical Staff Member, DevOps Champion, @ IBM Research diff --git a/service/__init__.py b/service/__init__.py index 98896d1ff..1536c734d 100644 --- a/service/__init__.py +++ b/service/__init__.py @@ -7,6 +7,7 @@ # This must be imported after the Flask app is created from service import routes # pylint: disable=wrong-import-position,cyclic-import +from service.common import error_handlers # pylint: disable=wrong-import-position,unused-import from service.common import log_handlers # pylint: disable=wrong-import-position log_handlers.init_logging(app, "gunicorn.error")