diff --git a/.gitignore b/.gitignore index e43b0f9..090a1f0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ +.idea .DS_Store diff --git a/code/README.md b/code/README.md deleted file mode 100644 index 1e0fa1f..0000000 --- a/code/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# To Include - - - Code - - Deployment scripts - - Configuration files - - Scripts to create figures - - ...everything you have used! - -# To NOT include (please!) -- Passwords and private keys, API keys, etc. \ No newline at end of file diff --git a/deploy-charts-cluster.sh b/deploy-charts-cluster.sh new file mode 100755 index 0000000..c16a875 --- /dev/null +++ b/deploy-charts-cluster.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +helm repo add bitnami https://charts.bitnami.com/bitnami +helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx + +helm repo update + +helm install -f helm-config/redis-helm-values.yaml redis bitnami/redis +helm install -f helm-config/nginx-helm-values.yaml nginx ingress-nginx/ingress-nginx \ No newline at end of file diff --git a/deploy-charts-minikube.sh b/deploy-charts-minikube.sh new file mode 100755 index 0000000..a21276b --- /dev/null +++ b/deploy-charts-minikube.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +helm repo add bitnami https://charts.bitnami.com/bitnami +helm repo update + +helm install -f helm-config/redis-helm-values.yaml redis bitnami/redis \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ae8c96b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,44 @@ +version: "3" +services: + + gateway: + image: nginx:latest + volumes: + - ./gateway_nginx.conf:/etc/nginx/nginx.conf:ro + ports: + - "8000:80" + + order-service: + build: ./order + image: order:latest + environment: + - GATEWAY_URL=http://gateway:80 + command: gunicorn -b 0.0.0.0:5000 app:app + env_file: + - env/order_redis.env + + order-db: + image: redis:latest + command: redis-server --requirepass redis --maxmemory 512mb + + stock-service: + build: ./stock + image: stock:latest + command: gunicorn -b 0.0.0.0:5000 app:app + env_file: + - env/stock_redis.env + + stock-db: + image: redis:latest + command: redis-server --requirepass redis --maxmemory 512mb + + payment-service: + build: ./payment + image: user:latest + command: gunicorn -b 0.0.0.0:5000 app:app + env_file: + - env/payment_redis.env + + payment-db: + image: redis:latest + command: redis-server --requirepass redis --maxmemory 512mb diff --git a/env/order_redis.env b/env/order_redis.env new file mode 100644 index 0000000..1bde8a2 --- /dev/null +++ b/env/order_redis.env @@ -0,0 +1,4 @@ +REDIS_HOST=order-db +REDIS_PORT=6379 +REDIS_PASSWORD=redis +REDIS_DB=0 \ No newline at end of file diff --git a/env/payment_redis.env b/env/payment_redis.env new file mode 100644 index 0000000..566e602 --- /dev/null +++ b/env/payment_redis.env @@ -0,0 +1,4 @@ +REDIS_HOST=payment-db +REDIS_PORT=6379 +REDIS_PASSWORD=redis +REDIS_DB=0 \ No newline at end of file diff --git a/env/stock_redis.env b/env/stock_redis.env new file mode 100644 index 0000000..8351e62 --- /dev/null +++ b/env/stock_redis.env @@ -0,0 +1,4 @@ +REDIS_HOST=stock-db +REDIS_PORT=6379 +REDIS_PASSWORD=redis +REDIS_DB=0 \ No newline at end of file diff --git a/gateway_nginx.conf b/gateway_nginx.conf new file mode 100644 index 0000000..74ca952 --- /dev/null +++ b/gateway_nginx.conf @@ -0,0 +1,27 @@ +events { worker_connections 2048;} + +http { + upstream order-app { + server order-service:5000; + } + upstream payment-app { + server payment-service:5000; + } + upstream stock-app { + server stock-service:5000; + } + server { + listen 80; + location /orders/ { + proxy_pass http://order-app/; + } + location /payment/ { + proxy_pass http://payment-app/; + } + location /stock/ { + proxy_pass http://stock-app/; + } + access_log /var/log/nginx/server.access.log; + } + access_log /var/log/nginx/access.log; +} diff --git a/helm-config/nginx-helm-values.yaml b/helm-config/nginx-helm-values.yaml new file mode 100644 index 0000000..286d4c8 --- /dev/null +++ b/helm-config/nginx-helm-values.yaml @@ -0,0 +1,7 @@ +resources: + limits: + cpu: 500m + memory: 1Gi + requests: + cpu: 500m + memory: 1Gi \ No newline at end of file diff --git a/helm-config/redis-helm-values.yaml b/helm-config/redis-helm-values.yaml new file mode 100644 index 0000000..4c613c9 --- /dev/null +++ b/helm-config/redis-helm-values.yaml @@ -0,0 +1,17 @@ +auth: + password: redis +master: + persistence: + size: 4Gi + resources: + requests: + memory: 4Gi + cpu: 2 +replica: + replicaCount: 1 + persistence: + size: 4Gi + resources: + requests: + memory: 4Gi + cpu: 2 \ No newline at end of file diff --git a/k8s/ingress-service.yaml b/k8s/ingress-service.yaml new file mode 100644 index 0000000..ae48692 --- /dev/null +++ b/k8s/ingress-service.yaml @@ -0,0 +1,32 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress-service + annotations: + kubernetes.io/ingress.class: nginx + nginx.ingress.kubernetes.io/rewrite-target: /$1 +spec: + rules: + - http: + paths: + - path: /orders/?(.*) + pathType: Prefix + backend: + service: + name: order-service + port: + number: 5000 + - path: /stock/?(.*) + pathType: Prefix + backend: + service: + name: stock-service + port: + number: 5000 + - path: /payment/?(.*) + pathType: Prefix + backend: + service: + name: user-service + port: + number: 5000 \ No newline at end of file diff --git a/k8s/order-app.yaml b/k8s/order-app.yaml new file mode 100644 index 0000000..16e4d82 --- /dev/null +++ b/k8s/order-app.yaml @@ -0,0 +1,54 @@ +apiVersion: v1 +kind: Service +metadata: + name: order-service +spec: + type: ClusterIP + selector: + component: order + ports: + - port: 5000 + name: http + targetPort: 5000 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: order-deployment +spec: + replicas: 1 + selector: + matchLabels: + component: order + template: + metadata: + labels: + component: order + spec: + containers: + - name: order + image: order:latest + resources: + limits: + memory: "1Gi" + cpu: "1" + requests: + memory: "1Gi" + cpu: "1" + command: ["gunicorn"] + args: ["-b", "0.0.0.0:5000", "app:app"] + ports: + - containerPort: 5000 + env: + - name: USER_SERVICE_URL + value: "user-service" + - name: STOCK_SERVICE_URL + value: "stock-service" + - name: REDIS_HOST + value: redis-master + - name: REDIS_PORT + value: '6379' + - name: REDIS_PASSWORD + value: "redis" + - name: REDIS_DB + value: "0" \ No newline at end of file diff --git a/k8s/stock-app.yaml b/k8s/stock-app.yaml new file mode 100644 index 0000000..c2f0ec5 --- /dev/null +++ b/k8s/stock-app.yaml @@ -0,0 +1,50 @@ +apiVersion: v1 +kind: Service +metadata: + name: stock-service +spec: + type: ClusterIP + selector: + component: stock + ports: + - port: 5000 + name: http + targetPort: 5000 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: stock-deployment +spec: + replicas: 1 + selector: + matchLabels: + component: stock + template: + metadata: + labels: + component: stock + spec: + containers: + - name: stock + image: stock:latest + resources: + limits: + memory: "1Gi" + cpu: "1" + requests: + memory: "1Gi" + cpu: "1" + command: ["gunicorn"] + args: ["-b", "0.0.0.0:5000", "app:app"] + ports: + - containerPort: 5000 + env: + - name: REDIS_HOST + value: redis-master + - name: REDIS_PORT + value: '6379' + - name: REDIS_PASSWORD + value: "redis" + - name: REDIS_DB + value: "0" diff --git a/k8s/user-app.yaml b/k8s/user-app.yaml new file mode 100644 index 0000000..bf48433 --- /dev/null +++ b/k8s/user-app.yaml @@ -0,0 +1,50 @@ +apiVersion: v1 +kind: Service +metadata: + name: user-service +spec: + type: ClusterIP + selector: + component: user + ports: + - port: 5000 + name: http + targetPort: 5000 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: user-deployment +spec: + replicas: 1 + selector: + matchLabels: + component: user + template: + metadata: + labels: + component: user + spec: + containers: + - name: user + image: user:latest + resources: + limits: + memory: "1Gi" + cpu: "1" + requests: + memory: "1Gi" + cpu: "1" + command: ["gunicorn"] + args: ["-b", "0.0.0.0:5000", "app:app"] + ports: + - containerPort: 5000 + env: + - name: REDIS_HOST + value: redis-master + - name: REDIS_PORT + value: '6379' + - name: REDIS_PASSWORD + value: "redis" + - name: REDIS_DB + value: "0" diff --git a/order/Dockerfile b/order/Dockerfile new file mode 100644 index 0000000..dc99c97 --- /dev/null +++ b/order/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.10-slim + +WORKDIR /home/flask-app + +COPY ./requirements.txt . + +RUN pip install -r requirements.txt + +COPY . . + +EXPOSE 5000 \ No newline at end of file diff --git a/order/app.py b/order/app.py new file mode 100644 index 0000000..d882005 --- /dev/null +++ b/order/app.py @@ -0,0 +1,42 @@ +import os +import atexit + +from flask import Flask +import redis + + +gateway_url = os.environ['GATEWAY_URL'] + +app = Flask("order-service") + +db: redis.Redis = redis.Redis(host=os.environ['REDIS_HOST'], + port=int(os.environ['REDIS_PORT']), + password=os.environ['REDIS_PASSWORD'], + db=int(os.environ['REDIS_DB'])) + + +def close_db_connection(): + db.close() + + +atexit.register(close_db_connection) + + +@app.post('/create/') +def create_order(user_id): + pass + + +@app.post('/addItem//') +def add_item(order_id, item_id): + pass + + +@app.get('/find/') +def find_item(order_id): + pass + + +@app.post('/checkout/') +def checkout(order_id): + pass diff --git a/order/requirements.txt b/order/requirements.txt new file mode 100644 index 0000000..cb64544 --- /dev/null +++ b/order/requirements.txt @@ -0,0 +1,3 @@ +Flask==2.1.1 +redis==4.2.2 +gunicorn==20.1.0 \ No newline at end of file diff --git a/payment/Dockerfile b/payment/Dockerfile new file mode 100644 index 0000000..dc99c97 --- /dev/null +++ b/payment/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.10-slim + +WORKDIR /home/flask-app + +COPY ./requirements.txt . + +RUN pip install -r requirements.txt + +COPY . . + +EXPOSE 5000 \ No newline at end of file diff --git a/payment/app.py b/payment/app.py new file mode 100644 index 0000000..1a3c174 --- /dev/null +++ b/payment/app.py @@ -0,0 +1,40 @@ +import os +import atexit + +from flask import Flask +import redis + + +app = Flask("payment-service") + +db: redis.Redis = redis.Redis(host=os.environ['REDIS_HOST'], + port=int(os.environ['REDIS_PORT']), + password=os.environ['REDIS_PASSWORD'], + db=int(os.environ['REDIS_DB'])) + + +def close_db_connection(): + db.close() + + +atexit.register(close_db_connection) + + +@app.post('/create_user') +def create_user(): + pass + + +@app.get('/find_user/') +def find_user(user_id: str): + pass + + +@app.post('/add_funds//') +def add_credit(user_id: str, amount: int): + pass + + +@app.post('/pay///') +def remove_credit(user_id: str, order_id: str, amount: int): + pass diff --git a/payment/requirements.txt b/payment/requirements.txt new file mode 100644 index 0000000..cb64544 --- /dev/null +++ b/payment/requirements.txt @@ -0,0 +1,3 @@ +Flask==2.1.1 +redis==4.2.2 +gunicorn==20.1.0 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..83e267e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +requests==2.27.1 +Flask==2.1.1 +redis==4.2.2 \ No newline at end of file diff --git a/slides/presentation.pptx b/slides/presentation.pptx deleted file mode 100644 index ea32030..0000000 Binary files a/slides/presentation.pptx and /dev/null differ diff --git a/stock/Dockerfile b/stock/Dockerfile new file mode 100644 index 0000000..dc99c97 --- /dev/null +++ b/stock/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.10-slim + +WORKDIR /home/flask-app + +COPY ./requirements.txt . + +RUN pip install -r requirements.txt + +COPY . . + +EXPOSE 5000 \ No newline at end of file diff --git a/stock/app.py b/stock/app.py new file mode 100644 index 0000000..c81235a --- /dev/null +++ b/stock/app.py @@ -0,0 +1,40 @@ +import os +import atexit + +from flask import Flask +import redis + + +app = Flask("stock-service") + +db: redis.Redis = redis.Redis(host=os.environ['REDIS_HOST'], + port=int(os.environ['REDIS_PORT']), + password=os.environ['REDIS_PASSWORD'], + db=int(os.environ['REDIS_DB'])) + + +def close_db_connection(): + db.close() + + +atexit.register(close_db_connection) + + +@app.post('/item/create/') +def create_item(price: int): + pass + + +@app.get('/find/') +def find_item(item_id: str): + pass + + +@app.post('/add//') +def add_stock(item_id: str, amount: int): + pass + + +@app.post('/subtract//') +def remove_stock(item_id: str, amount: int): + pass diff --git a/stock/requirements.txt b/stock/requirements.txt new file mode 100644 index 0000000..cb64544 --- /dev/null +++ b/stock/requirements.txt @@ -0,0 +1,3 @@ +Flask==2.1.1 +redis==4.2.2 +gunicorn==20.1.0 \ No newline at end of file diff --git a/test/test_microservices.py b/test/test_microservices.py new file mode 100644 index 0000000..a1f1537 --- /dev/null +++ b/test/test_microservices.py @@ -0,0 +1,146 @@ +import unittest + +import utils as tu + + +class TestMicroservices(unittest.TestCase): + + def test_stock(self): + # Test /stock/item/create/ + item: dict = tu.create_item(5) + self.assertTrue('item_id' in item) + + item_id: str = item['item_id'] + + # Test /stock/find/ + item: dict = tu.find_item(item_id) + self.assertEqual(item['price'], 5) + self.assertEqual(item['stock'], 0) + + # Test /stock/add// + add_stock_response = tu.add_stock(item_id, 50) + self.assertTrue(200 <= int(add_stock_response) < 300) + + stock_after_add: int = tu.find_item(item_id)['stock'] + self.assertEqual(stock_after_add, 50) + + # Test /stock/subtract// + over_subtract_stock_response = tu.subtract_stock(item_id, 200) + self.assertTrue(tu.status_code_is_failure(int(over_subtract_stock_response))) + + subtract_stock_response = tu.subtract_stock(item_id, 15) + self.assertTrue(tu.status_code_is_success(int(subtract_stock_response))) + + stock_after_subtract: int = tu.find_item(item_id)['stock'] + self.assertEqual(stock_after_subtract, 35) + + def test_payment(self): + # Test /payment/pay// + user: dict = tu.create_user() + self.assertTrue('user_id' in user) + + user_id: str = user['user_id'] + + # Test /users/credit/add// + add_credit_response = tu.add_credit_to_user(user_id, 15) + self.assertTrue(tu.status_code_is_success(add_credit_response)) + + # add item to the stock service + item: dict = tu.create_item(5) + self.assertTrue('item_id' in item) + + item_id: str = item['item_id'] + + add_stock_response = tu.add_stock(item_id, 50) + self.assertTrue(tu.status_code_is_success(add_stock_response)) + + # create order in the order service and add item to the order + order: dict = tu.create_order(user_id) + self.assertTrue('order_id' in order) + + order_id: str = order['order_id'] + + add_item_response = tu.add_item_to_order(order_id, item_id) + self.assertTrue(tu.status_code_is_success(add_item_response)) + + add_item_response = tu.add_item_to_order(order_id, item_id) + self.assertTrue(tu.status_code_is_success(add_item_response)) + add_item_response = tu.add_item_to_order(order_id, item_id) + self.assertTrue(tu.status_code_is_success(add_item_response)) + + payment_response = tu.payment_pay(user_id, order_id, 10) + self.assertTrue(tu.status_code_is_success(payment_response)) + + credit_after_payment: int = tu.find_user(user_id)['credit'] + self.assertEqual(credit_after_payment, 5) + + def test_order(self): + # Test /payment/pay// + user: dict = tu.create_user() + self.assertTrue('user_id' in user) + + user_id: str = user['user_id'] + + # create order in the order service and add item to the order + order: dict = tu.create_order(user_id) + self.assertTrue('order_id' in order) + + order_id: str = order['order_id'] + + # add item to the stock service + item1: dict = tu.create_item(5) + self.assertTrue('item_id' in item1) + item_id1: str = item1['item_id'] + add_stock_response = tu.add_stock(item_id1, 15) + self.assertTrue(tu.status_code_is_success(add_stock_response)) + + # add item to the stock service + item2: dict = tu.create_item(5) + self.assertTrue('item_id' in item2) + item_id2: str = item2['item_id'] + add_stock_response = tu.add_stock(item_id2, 1) + self.assertTrue(tu.status_code_is_success(add_stock_response)) + + add_item_response = tu.add_item_to_order(order_id, item_id1) + self.assertTrue(tu.status_code_is_success(add_item_response)) + add_item_response = tu.add_item_to_order(order_id, item_id2) + self.assertTrue(tu.status_code_is_success(add_item_response)) + subtract_stock_response = tu.subtract_stock(item_id2, 1) + self.assertTrue(tu.status_code_is_success(subtract_stock_response)) + + checkout_response = tu.checkout_order(order_id).text + self.assertEqual(checkout_response, "Out of stock on item_id: " + str(item_id2)) + + stock_after_subtract: int = tu.find_item(item_id1)['stock'] + self.assertEqual(stock_after_subtract, 15) + + add_stock_response = tu.add_stock(item_id2, 15) + self.assertTrue(tu.status_code_is_success(int(add_stock_response))) + + credit_after_payment: int = tu.find_user(user_id)['credit'] + self.assertEqual(credit_after_payment, 0) + + checkout_response = tu.checkout_order(order_id).text + self.assertEqual(checkout_response, "User out of credit") + + add_credit_response = tu.add_credit_to_user(user_id, 15) + self.assertTrue(tu.status_code_is_success(int(add_credit_response))) + + credit: int = tu.find_user(user_id)['credit'] + self.assertEqual(credit, 15) + + stock: int = tu.find_item(item_id1)['stock'] + self.assertEqual(stock, 15) + + checkout_response = tu.checkout_order(order_id).json() + self.assertEqual(checkout_response, "Checkout successful") + + stock_after_subtract: int = tu.find_item(item_id1)['stock'] + self.assertEqual(stock_after_subtract, 14) + + credit: int = tu.find_user(user_id)['credit'] + self.assertEqual(credit, 5) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/utils.py b/test/utils.py new file mode 100644 index 0000000..5d660a9 --- /dev/null +++ b/test/utils.py @@ -0,0 +1,71 @@ +import requests + +ORDER_URL = STOCK_URL = PAYMENT_URL = "http://127.0.0.1:8000" + + +######################################################################################################################## +# STOCK MICROSERVICE FUNCTIONS +######################################################################################################################## +def create_item(price: float) -> dict: + return requests.post(f"{STOCK_URL}/stock/item/create/{price}").json() + + +def find_item(item_id: str) -> dict: + return requests.get(f"{STOCK_URL}/stock/find/{item_id}").json() + + +def add_stock(item_id: str, amount: int) -> int: + return requests.post(f"{STOCK_URL}/stock/add/{item_id}/{amount}").status_code + + +def subtract_stock(item_id: str, amount: int) -> int: + return requests.post(f"{STOCK_URL}/stock/subtract/{item_id}/{amount}").status_code + + +######################################################################################################################## +# PAYMENT MICROSERVICE FUNCTIONS +######################################################################################################################## +def payment_pay(user_id: str, order_id: str, amount: float) -> int: + return requests.post(f"{PAYMENT_URL}/payment/pay/{user_id}/{order_id}/{amount}").status_code + + +def create_user() -> dict: + return requests.post(f"{PAYMENT_URL}/payment/create_user").json() + + +def find_user(user_id: str) -> dict: + return requests.get(f"{PAYMENT_URL}/payment/find_user/{user_id}").json() + + +def add_credit_to_user(user_id: str, amount: float) -> int: + return requests.post(f"{PAYMENT_URL}/payment/add_funds/{user_id}/{amount}").status_code + + +######################################################################################################################## +# ORDER MICROSERVICE FUNCTIONS +######################################################################################################################## +def create_order(user_id: str) -> dict: + return requests.post(f"{ORDER_URL}/orders/create/{user_id}").json() + + +def add_item_to_order(order_id: str, item_id: str) -> int: + return requests.post(f"{ORDER_URL}/orders/addItem/{order_id}/{item_id}").status_code + + +def find_order(order_id: str) -> dict: + return requests.get(f"{ORDER_URL}/orders/find/{order_id}").json() + + +def checkout_order(order_id: str) -> requests.Response: + return requests.post(f"{ORDER_URL}/orders/checkout/{order_id}") + + +######################################################################################################################## +# STATUS CHECKS +######################################################################################################################## +def status_code_is_success(status_code: int) -> bool: + return 200 <= status_code < 300 + + +def status_code_is_failure(status_code: int) -> bool: + return 400 <= status_code < 500