Skip to content

Commit bf76d5f

Browse files
New version
1 parent c017a62 commit bf76d5f

9 files changed

Lines changed: 43 additions & 174 deletions

File tree

.github/workflows/cd.yml

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@ env:
1212
K8S_NAMESPACE: devops-platform
1313

1414
jobs:
15-
# -------------------------------------------------------
16-
# Job 1: Deploy to Staging
17-
# -------------------------------------------------------
1815
deploy-staging:
1916
name: Deploy to Staging
2017
runs-on: ubuntu-latest
@@ -65,11 +62,8 @@ jobs:
6562
echo "Smoke test failed — /health returned HTTP $STATUS"
6663
exit 1
6764
fi
68-
echo "Smoke test passed"
65+
echo "Smoke test passed"
6966
70-
# -------------------------------------------------------
71-
# Job 2: Deploy to Production (requires manual approval)
72-
# -------------------------------------------------------
7367
deploy-production:
7468
name: Deploy to Production
7569
runs-on: ubuntu-latest
@@ -119,18 +113,18 @@ jobs:
119113
kubectl rollout undo deployment/devops-platform -n ${{ env.K8S_NAMESPACE }}
120114
exit 1
121115
fi
122-
echo "Production smoke test passed"
116+
echo "Production smoke test passed"
123117
124118
- name: Notify on success
125119
if: success()
126120
run: |
127-
echo "🚀 Deployed ${{ steps.vars.outputs.IMAGE_TAG }} to production successfully"
121+
echo "Deployed ${{ steps.vars.outputs.IMAGE_TAG }} to production successfully"
128122
# Add Slack/Discord webhook here if needed
129-
# curl -X POST ${{ secrets.SLACK_WEBHOOK }} -d '{"text":"Deployed to prod "}'
123+
# curl -X POST ${{ secrets.SLACK_WEBHOOK }} -d '{"text":"Deployed to prod successfully"}'
130124
131125
- name: Rollback on failure
132126
if: failure()
133127
run: |
134-
echo "Deployment failed — rolling back"
128+
echo "Deployment failed"
135129
kubectl rollout undo deployment/devops-platform -n ${{ env.K8S_NAMESPACE }}
136130

Makefile

Lines changed: 32 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,48 @@
11
.PHONY: help up down build logs test lint clean ps shell migrate
22

3-
# Default target
4-
help: ## Show this help
5-
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
6-
awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-18s\033[0m %s\n", $$1, $$2}'
3+
help:
4+
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
5+
awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-18s\033[0m %s\n", $$1, $$2}'
76

8-
# ── Docker ──────────────────────────────────────────────────────────────────
9-
up: ## Start all services (detached)
10-
docker compose up -d --build
7+
up:
8+
docker compose up -d --build
119

12-
down: ## Stop all services
13-
docker compose down
10+
down:
11+
docker compose down
1412

15-
build: ## Build app image only
16-
docker compose build app
13+
build:
14+
docker compose build app
1715

18-
logs: ## Follow app logs
19-
docker compose logs -f app
16+
logs:
17+
docker compose logs -f app
2018

21-
ps: ## Show running containers
22-
docker compose ps
19+
ps:
20+
docker compose ps
2321

24-
shell: ## Open shell in running app container
25-
docker compose exec app sh
22+
shell:
23+
docker compose exec app sh
2624

27-
# ── Database ─────────────────────────────────────────────────────────────────
28-
migrate: ## Run Alembic migrations
29-
docker compose exec app alembic upgrade head
25+
migrate:
26+
docker compose exec app alembic upgrade head
3027

31-
migration: ## Create a new migration (usage: make migration msg="add users table")
32-
docker compose exec app alembic revision --autogenerate -m "$(msg)"
28+
migration:
29+
docker compose exec app alembic revision --autogenerate -m "$(msg)"
3330

34-
rollback: ## Roll back last migration
35-
docker compose exec app alembic downgrade -1
31+
rollback:
32+
docker compose exec app alembic downgrade -1
3633

37-
# ── Testing ───────────────────────────────────────────────────────────────────
38-
test: ## Run tests with coverage
39-
cd app && pytest tests/ -v --cov=. --cov-report=term-missing
34+
test:
35+
cd app && pytest tests/ -v --cov=. --cov-report=term-missing
4036

41-
lint: ## Run flake8 linter
42-
flake8 app/ --max-line-length=100 --exclude=app/tests,app/migrations
37+
lint:
38+
flake8 app/ --max-line-length=100 --exclude=app/tests,app/migrations
4339

44-
# ── Ops ───────────────────────────────────────────────────────────────────────
45-
health: ## Check API health
46-
curl -s http://localhost:8000/health | python3 -m json.tool
40+
health:
41+
curl -s http://localhost:8000/health | python3 -m json.tool
4742

48-
metrics: ## Show Prometheus metrics (first 20 lines)
49-
curl -s http://localhost:9090/metrics 2>/dev/null | head -20 || \
50-
curl -s http://localhost:8000/metrics | head -20
51-
52-
clean: ## Remove volumes and containers
53-
docker compose down -v --remove-orphans
43+
metrics:
44+
curl -s http://localhost:9090/metrics 2>/dev/null | head -20 || \
45+
curl -s http://localhost:8000/metrics | head -20
5446

47+
clean:
48+
docker compose down -v --remove-orphans

app/alembic.ini

Lines changed: 4 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,119 +1,19 @@
1-
# A generic, single database configuration.
1+
22

33
[alembic]
4-
# path to migration scripts.
5-
# this is typically a path given in POSIX (e.g. forward slashes)
6-
# format, relative to the token %(here)s which refers to the location of this
7-
# ini file
4+
85
script_location = %(here)s/migrations
96

10-
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
11-
# Uncomment the line below if you want the files to be prepended with date and time
12-
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
13-
# for all available tokens
14-
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
15-
# Or organize into date-based subdirectories (requires recursive_version_locations = true)
16-
# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
17-
18-
# sys.path path, will be prepended to sys.path if present.
19-
# defaults to the current working directory. for multiple paths, the path separator
20-
# is defined by "path_separator" below.
217
prepend_sys_path = .
228

23-
24-
# timezone to use when rendering the date within the migration file
25-
# as well as the filename.
26-
# If specified, requires the tzdata library which can be installed by adding
27-
# `alembic[tz]` to the pip requirements.
28-
# string value is passed to ZoneInfo()
29-
# leave blank for localtime
30-
# timezone =
31-
32-
# max length of characters to apply to the "slug" field
33-
# truncate_slug_length = 40
34-
35-
# set to 'true' to run the environment during
36-
# the 'revision' command, regardless of autogenerate
37-
# revision_environment = false
38-
39-
# set to 'true' to allow .pyc and .pyo files without
40-
# a source .py file to be detected as revisions in the
41-
# versions/ directory
42-
# sourceless = false
43-
44-
# version location specification; This defaults
45-
# to <script_location>/versions. When using multiple version
46-
# directories, initial revisions must be specified with --version-path.
47-
# The path separator used here should be the separator specified by "path_separator"
48-
# below.
49-
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
50-
51-
# path_separator; This indicates what character is used to split lists of file
52-
# paths, including version_locations and prepend_sys_path within configparser
53-
# files such as alembic.ini.
54-
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
55-
# to provide os-dependent path splitting.
56-
#
57-
# Note that in order to support legacy alembic.ini files, this default does NOT
58-
# take place if path_separator is not present in alembic.ini. If this
59-
# option is omitted entirely, fallback logic is as follows:
60-
#
61-
# 1. Parsing of the version_locations option falls back to using the legacy
62-
# "version_path_separator" key, which if absent then falls back to the legacy
63-
# behavior of splitting on spaces and/or commas.
64-
# 2. Parsing of the prepend_sys_path option falls back to the legacy
65-
# behavior of splitting on spaces, commas, or colons.
66-
#
67-
# Valid values for path_separator are:
68-
#
69-
# path_separator = :
70-
# path_separator = ;
71-
# path_separator = space
72-
# path_separator = newline
73-
#
74-
# Use os.pathsep. Default configuration used for new projects.
759
path_separator = os
7610

77-
# set to 'true' to search source files recursively
78-
# in each "version_locations" directory
79-
# new in Alembic version 1.10
80-
# recursive_version_locations = false
81-
82-
# the output encoding used when revision files
83-
# are written from script.py.mako
84-
# output_encoding = utf-8
85-
86-
# database URL. This is consumed by the user-maintained env.py script only.
87-
# other means of configuring database URLs may be customized within the env.py
88-
# file.
8911
sqlalchemy.url = postgresql://devops:devops@localhost:5432/devopsdb
9012

9113

9214
[post_write_hooks]
93-
# post_write_hooks defines scripts or Python functions that are run
94-
# on newly generated revision scripts. See the documentation for further
95-
# detail and examples
96-
97-
# format using "black" - use the console_scripts runner, against the "black" entrypoint
98-
# hooks = black
99-
# black.type = console_scripts
100-
# black.entrypoint = black
101-
# black.options = -l 79 REVISION_SCRIPT_FILENAME
102-
103-
# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
104-
# hooks = ruff
105-
# ruff.type = module
106-
# ruff.module = ruff
107-
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
108-
109-
# Alternatively, use the exec runner to execute a binary found on your PATH
110-
# hooks = ruff
111-
# ruff.type = exec
112-
# ruff.executable = ruff
113-
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
114-
115-
# Logging configuration. This is also consumed by the user-maintained
116-
# env.py script only.
15+
16+
11717
[loggers]
11818
keys = root,sqlalchemy,alembic
11919

app/main.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ def get_db():
6161
db.close()
6262

6363

64-
# ── Health ────────────────────────────────────────────────────────────────────
6564
@app.get("/health", tags=["ops"])
6665
def health_check(db: Session = Depends(get_db)):
6766
db_ok = True
@@ -86,7 +85,6 @@ def health_check(db: Session = Depends(get_db)):
8685
}
8786

8887

89-
# ── Auth ──────────────────────────────────────────────────────────────────────
9088
@app.post("/auth/register", response_model=schemas.UserOut, status_code=201, tags=["auth"])
9189
def register(user: schemas.UserCreate, db: Session = Depends(get_db)):
9290
if crud.get_user_by_username(db, user.username):
@@ -111,7 +109,6 @@ def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depend
111109
return {"access_token": token, "token_type": "bearer"}
112110

113111

114-
# ── Items (protected) ─────────────────────────────────────────────────────────
115112
ITEMS_CACHE_KEY = "items:all"
116113

117114

@@ -162,4 +159,3 @@ def delete_item(
162159
raise HTTPException(status_code=404, detail="item not found")
163160
cache_delete(ITEMS_CACHE_KEY)
164161
logger.info("item deleted", extra={"item_id": item_id})
165-

app/migrations/env.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,13 @@
44
from sqlalchemy import engine_from_config, pool
55
from alembic import context
66

7-
# Import your models so Alembic can detect schema changes
87
import sys
98
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
109
import models # noqa: F401
1110
from database import Base
1211

1312
config = context.config
1413

15-
# Override sqlalchemy.url with environment variable if set
1614
db_url = os.getenv("DATABASE_URL", config.get_main_option("sqlalchemy.url"))
1715
config.set_main_option("sqlalchemy.url", db_url)
1816

app/schemas.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ class Item(ItemBase):
2020
created_at: datetime
2121

2222

23-
# Auth schemas
2423
class UserCreate(BaseModel):
2524
username: str
2625
password: str

app/tests/test_main.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,12 @@ def auth_headers(token=None):
5757
return {"Authorization": f"Bearer {token or get_token()}"}
5858

5959

60-
# ── Health ────────────────────────────────────────────────────────────────────
6160
def test_health_check():
6261
r = client.get("/health")
6362
assert r.status_code == 200
6463
assert r.json()["status"] == "healthy"
6564

6665

67-
# ── Auth ──────────────────────────────────────────────────────────────────────
6866
def test_register():
6967
r = client.post("/auth/register", json={"username": "alice", "password": "secret"})
7068
assert r.status_code == 201
@@ -90,7 +88,6 @@ def test_login_wrong_password():
9088
assert r.status_code == 401
9189

9290

93-
# ── Items ─────────────────────────────────────────────────────────────────────
9491
def test_items_require_auth():
9592
r = client.get("/items")
9693
assert r.status_code == 401

k8s/service-ingress-hpa.yaml

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
---
2-
# Service — internal load balancer for the pods
32
apiVersion: v1
43
kind: Service
54
metadata:
@@ -18,7 +17,7 @@ spec:
1817
type: ClusterIP
1918

2019
---
21-
# Ingress — exposes the service to the outside world via Nginx
20+
2221
apiVersion: networking.k8s.io/v1
2322
kind: Ingress
2423
metadata:
@@ -46,7 +45,6 @@ spec:
4645
number: 80
4746

4847
---
49-
# HPA — auto-scales pods based on CPU usage
5048
apiVersion: autoscaling/v2
5149
kind: HorizontalPodAutoscaler
5250
metadata:
@@ -74,9 +72,6 @@ spec:
7472
averageUtilization: 80
7573

7674
---
77-
# Secret template — in real usage, populate via CI/CD or Vault
78-
# kubectl create secret generic devops-platform-secrets \
79-
# --from-literal=DATABASE_URL="postgresql://..." -n devops-platform
8075
apiVersion: v1
8176
kind: Secret
8277
metadata:

load-tests/smoke.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ export const options = {
1616
const BASE_URL = __ENV.BASE_URL || "http://localhost:8000";
1717

1818
export function setup() {
19-
// register a test user and get a token
2019
const reg = http.post(`${BASE_URL}/auth/register`, JSON.stringify({
2120
username: "loadtest_user",
2221
password: "loadtest_pass",
@@ -35,16 +34,13 @@ export default function (data) {
3534
Authorization: `Bearer ${data.token}`,
3635
"Content-Type": "application/json",
3736
};
38-
39-
// health check (no auth needed)
4037
const health = http.get(`${BASE_URL}/health`);
4138
check(health, { "health 200": (r) => r.status === 200 });
4239

43-
// list items
4440
const list = http.get(`${BASE_URL}/items`, { headers });
4541
check(list, { "items 200": (r) => r.status === 200 });
4642

47-
// create item
43+
4844
const create = http.post(`${BASE_URL}/items`,
4945
JSON.stringify({ name: `item-${Date.now()}`, description: "load test" }),
5046
{ headers }

0 commit comments

Comments
 (0)