Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flexport Take Home - Omair Khan #20

Open
wants to merge 75 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
68fcb41
try to test docker workflow
Apr 20, 2023
365f501
try to test docker workflow
Apr 20, 2023
6185572
add k8s deploy workflow and more test
Apr 20, 2023
64c1d48
fixed errors
Apr 20, 2023
fbaf83c
edits to deploy workflow
Apr 20, 2023
d9aae2e
edits to deploy workflow
Apr 20, 2023
457cba4
edits to deploy workflow
Apr 20, 2023
e678ab9
edits to deploy workflow
Apr 20, 2023
372b725
edits to deploy workflow
Apr 20, 2023
e90ad90
edits to deploy workflow
Apr 20, 2023
0623655
trying another way
Apr 20, 2023
1cbb07e
trying another way
Apr 20, 2023
2d854d8
trying another way
Apr 20, 2023
0ab4368
trying another way
Apr 20, 2023
8eb870e
trying another way
Apr 20, 2023
fb2c71b
trying another way
Apr 20, 2023
3d2a250
trying another way
Apr 20, 2023
34806b6
trying another way
Apr 20, 2023
6c0f31a
trying another way
Apr 20, 2023
0c01288
new k8s
Apr 20, 2023
336b180
new k8s
Apr 20, 2023
00f320e
new k8s
Apr 20, 2023
555b1a4
new k8s
Apr 20, 2023
c5b095d
new k8s
Apr 20, 2023
daed47e
new k8s
Apr 20, 2023
a0d7cac
new k8s
Apr 20, 2023
2dffc14
new k8s
Apr 20, 2023
408bc0b
combine build and deploy
Apr 20, 2023
5b5021f
combine build and deploy
Apr 20, 2023
d8e2332
seperate build and deploy
Apr 20, 2023
f09656a
seperate build and deploy
Apr 20, 2023
d78a1bd
seperate build and deploy
Apr 20, 2023
2b2805f
seperate build and deploy
Apr 20, 2023
744aa23
seperate build and deploy
Apr 20, 2023
dcf6a41
seperate build and deploy
Apr 20, 2023
197cde6
seperate build and deploy
Apr 20, 2023
9885276
seperate build and deploy
Apr 20, 2023
4d46215
seperate build and deploy
Apr 20, 2023
8ced4e5
seperate build and deploy
Apr 20, 2023
4b35724
seperate build and deploy
Apr 20, 2023
384ac6f
seperate build and deploy
Apr 20, 2023
70b2a40
seperate new kind yaml
Apr 20, 2023
bb6627a
seperate new kind yaml
Apr 20, 2023
4bbdaa0
seperate new kind yaml
Apr 20, 2023
ed3d496
seperate new kind yaml
Apr 20, 2023
32f3ac4
seperate new kind yaml
Apr 20, 2023
20b2d68
seperate new kind yaml
Apr 20, 2023
abfd370
seperate new kind yaml
Apr 20, 2023
b764689
seperate new kind yaml
Apr 20, 2023
886f86c
seperate new kind yaml
Apr 20, 2023
bfe325b
seperate new kind yaml
Apr 20, 2023
e56c968
seperate new kind yaml
Apr 20, 2023
c525061
seperate new kind yaml
Apr 20, 2023
a66bd25
seperate new kind yaml
Apr 20, 2023
1fea5e6
seperate new kind yaml
Apr 20, 2023
f00460d
seperate new kind yaml
Apr 20, 2023
6dfef22
seperate new kind yaml
Apr 20, 2023
694a911
health check
Apr 20, 2023
bf75140
health check
Apr 20, 2023
bc21477
game test
Apr 20, 2023
3a691c6
cleanup repo
Apr 20, 2023
59ec8e9
rm kind-config
Apr 20, 2023
6aeb63a
cleanup repo
Apr 20, 2023
5392260
security issue
Apr 20, 2023
b6b8797
security issue
Apr 20, 2023
57a7672
security issue
Apr 20, 2023
c559cb6
security issue
Apr 20, 2023
1e3ce26
security issue
Apr 20, 2023
983d1e5
security issue
Apr 20, 2023
206ef27
security issue
Apr 20, 2023
23cb39c
revert security issues
Apr 20, 2023
faf6181
added integration test
Apr 20, 2023
19224d4
fixed error
Apr 20, 2023
f68b75f
added doc strings
Apr 20, 2023
900afdf
added doc strings
Apr 20, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/build-docker-image.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Build Docker Image

on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build_image:
runs-on: ubuntu-latest
steps:
- name: Get Repository
uses: actions/checkout@v2

- name: Set up Docker Build
uses: docker/setup-buildx-action@v1

- name: Log into ghcr.io
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.gh_tok}}

- name: Build and Push Image
uses: docker/build-push-action@v2
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
61 changes: 61 additions & 0 deletions .github/workflows/deployk8s.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: Deploy and Test on Kubernetes

on:
workflow_run:
workflows: ['Build Docker Image']
types:
- completed

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v2

- name: Set up Helm and Kind
uses: helm/[email protected]
with:
cluster_name: test-cd

- name: Set up QEMU
uses: docker/setup-qemu-action@v1

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1

- name: Log into ghcr.io
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.gh_tok }}

- name: Pull Docker Image
run: docker pull ghcr.io/${{ github.repository }}:${{ github.sha }}

- name: Load Docker Image into Kind
run: |
kubectl config use-context kind-test-cd
docker tag ghcr.io/${{ github.repository }}:${{ github.sha }} ghcr.io/${{ github.repository }}:local
docker save ghcr.io/${{ github.repository }}:local -o image.tar
kind load image-archive image.tar --name test-cd

- name: Create Deployment
run: |
kubectl config use-context kind-test-cd
kubectl create deployment rps-deployment --image=ghcr.io/${{ github.repository }}:local
kubectl rollout status deployment/rps-deployment

- name: Run Integration Tests
run: |
kubectl config use-context kind-test-cd
kubectl wait --for=condition=Ready pods --all --timeout=120s
export POD_NAME=$(kubectl get pods -o go-template --template '{{range .items}}{{.metadata.name}}{{"\n"}}{{end}}')
kubectl exec $POD_NAME -- pytest --cov=src --cov-report=xml tests/integration/

- name: Cleanup
if: always()
run: |
kubectl config use-context kind-test-cd
kind delete cluster --name test-cd
36 changes: 36 additions & 0 deletions .github/workflows/lint_and_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Lint and Test

on:
push:
branches:
- '*'

jobs:
lint-and-test:
runs-on: ubuntu-latest
env:
PYTHONPATH: ${{ github.workspace }}/src
steps:
- name: Get Repository
uses: actions/checkout@v2

- name: Python Setup
uses: actions/setup-python@v3
with:
python-version: '3.9'

- name: Install Requirements
run: pip3 install -r requirements.txt

- name: Code Format Check With Black
run: black --check .

- name: Lint Check with Pylint
run: |
pylint --fail-under=9 src
pylint --fail-under=9 tests

- name: Run Unit Tests
run: |
pytest --cov=src --cov-report=xml tests/unit/
pytest --cov=src --cov-report=xml tests/functional/
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,7 @@ dmypy.json

# Pyre type checker
.pyre/
.coverage
.idea/
.env
my-app-deployment.yaml
14 changes: 14 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
FROM python:3.10

WORKDIR /app

# COPY CODE TO /APP
COPY . .

# Install Dependencies
RUN pip install -r requirements.txt

ENV PYTHONPATH="./src"

# Entry point to run app
CMD ["flask", "--app", "src/rock_paper_scissors/app", "run", "--host", "0.0.0.0"]
5 changes: 5 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
Flask==2.2.3
pytest==7.2.2
pytest
pytest-cov
black
pylint
requests
14 changes: 8 additions & 6 deletions src/rock_paper_scissors/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,32 +26,34 @@ class InvalidMove(Exception):
Invalid Move
"""


app = Flask(__name__)


@app.route("/health")
def health():
return "OK"

@app.route("/rps", methods = ['POST'])

@app.route("/rps", methods=["POST"])
def rps():
# Create number to choice mapping
mapping = ["Rock", "Paper", "Scissors"]

move = request.json.get('move', '')
move = request.json.get("move", "")
try:
user_choice = mapping.index(move.lower().capitalize())
except ValueError:
raise InvalidMove(f"{move} is invalid. Valid moves: {mapping}")

game_result, pc_choice = rock_paper_scissors(user_choice)
game_result, pc_choice = rock_paper_scissors(user_choice)
if game_result == 0:
result = "Tie"
elif game_result == -1:
result = f"I win, {mapping[pc_choice]} beats {move}"
elif game_result == 1:
result = f"You win, {move} beats {mapping[pc_choice]}"

return json.dumps({'result': result,
'game_result': game_result,
'pc_choice': pc_choice})
return json.dumps(
{"result": result, "game_result": game_result, "pc_choice": pc_choice}
)
5 changes: 2 additions & 3 deletions src/rock_paper_scissors/rps.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ def rock_paper_scissors(user_choice: int) -> int:
pc_choice = random.randint(0, 2)
if pc_choice == user_choice:
return 0, pc_choice
elif (user_choice + 1) % 3 == pc_choice % 3:
if (user_choice + 1) % 3 == pc_choice % 3:
return -1, pc_choice
else:
return 1, pc_choice
return 1, pc_choice
32 changes: 28 additions & 4 deletions tests/functional/test_app.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,40 @@
import json

from rock_paper_scissors.app import app


def test_rps():
"""
Test Flask Application and API for Rock Paper Scissors
"""
with app.test_client() as test_client:
response = test_client.get("/health")
assert response.status_code == 200
assert response.data.decode("utf-8") == "OK"

with app.test_client() as test_client:
response = test_client.post(
"/rps", data=json.dumps({"move": "Rock"}), content_type="application/json"
)
assert response.status_code == 200

with app.test_client() as test_client:
response = test_client.post(
"/rps", data=json.dumps({"move": "Paper"}), content_type="application/json"
)
assert response.status_code == 200

with app.test_client() as test_client:
response = test_client.post('/rps',
data=json.dumps(dict(move='Rock')),
content_type='application/json')
response = test_client.post(
"/rps",
data=json.dumps({"move": "Scissors"}),
content_type="application/json",
)
assert response.status_code == 200

with app.test_client() as test_client:
response = test_client.post(
"/rps",
data=json.dumps({"move": "dummyinvalidmove"}),
content_type="application/json",
)
assert response.status_code != 200
45 changes: 45 additions & 0 deletions tests/integration/test_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""
Tests for When RPS App is running
"""

import requests

RPS_HOST = "http://127.0.0.1:5000"


def check_result(user_choice, pc_choice, result, game_outcome):
"""
Checks result of game for when RPS App is running
"""
if game_outcome == -1:
assert result == f"I win, {pc_choice} beats {user_choice}"
elif game_outcome == 0:
assert result == "Tie"
elif game_outcome == -1:
assert result == f"You win, {user_choice} beats {pc_choice}"


def test_endpoints():
"""
Tests All Endpoints for when RPS App is running
"""
# Health Check
health_reponse = requests.get(f"{RPS_HOST}/health")
assert health_reponse.status_code == 200
assert health_reponse.content == b"OK"

# Check Invalid Option
invalid_response = requests.post(
f"{RPS_HOST}/rps", json={"move": "dummyinvalidmove"}
)
assert invalid_response.status_code != 200

# Check Options
for option in ["Rock", "Paper", "Scissors"]:
app_response = requests.post(f"{RPS_HOST}/rps", json={"move": option})
assert app_response.status_code == 200
data = app_response.json()
pc_choice = data["pc_choice"]
game_outcome = data["game_result"]
result = data["result"]
check_result(option, pc_choice, game_outcome, result)
29 changes: 29 additions & 0 deletions tests/unit/test_rps.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,33 @@ def test_rps():
Basic test for Rock Paper Scissors
"""

# 0 = Rock, 1 = Paper, 2 = Scissors

# Test Not None
assert rock_paper_scissors(0) is not None
assert rock_paper_scissors(1) is not None
assert rock_paper_scissors(2) is not None

# Check if User picks rock
result, pc_choice = rock_paper_scissors(0)
assert (
(result == 0 and pc_choice == 0)
or (result == 1 and pc_choice == 2)
or (result == -1 and pc_choice == 1)
)

# Check if User picks paper
result, pc_choice = rock_paper_scissors(1)
assert (
(result == 0 and pc_choice == 1)
or (result == 1 and pc_choice == 0)
or (result == -1 and pc_choice == 2)
)

# Check if User picks scissors
result, pc_choice = rock_paper_scissors(2)
assert (
(result == 0 and pc_choice == 2)
or (result == 1 and pc_choice == 1)
or (result == -1 and pc_choice == 0)
)