diff --git a/.github/workflows/kind.yml b/.github/workflows/kind.yml new file mode 100644 index 00000000..8362c670 --- /dev/null +++ b/.github/workflows/kind.yml @@ -0,0 +1,46 @@ +name: deploy k8s +on: + workflow_run: + workflows: [docker build] + types: + - completed + +jobs: + kind: + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} + steps: + - uses: actions/checkout@v3 + - name: Create a k8s Kind Cluster + uses: helm/kind-action@v1.4.0 + - name: Testing + run: | + kubectl wait nodes --for condition=Ready --all + kubectl cluster-info + kubectl get pods -n kube-system + echo "current-context:" $(kubectl config current-context) + echo "environment-kubeconfig:" ${KUBECONFIG} + - name: Deploy Application + run: | + kubectl create deployment rps --image=${{ secrets.DOCKERHUB_USERNAME }}/rps:latest + kubectl wait pods --for condition=Ready --timeout=90s --all + kubectl expose deployment/rps --type=NodePort --port 5000 + - name: install dependencies for integration testing + run: | + pip install pytest + pip install pytest-cov + - name: Test Deployment + run: | + export NODE_PORT=$(kubectl get services/rps -o go-template='{{(index .spec.ports 0).nodePort}}') + echo NODE_PORT=${NODE_PORT} + kubectl describe services/rps + export POD_NAME=$(kubectl get pods -o go-template --template '{{range .items}}{{.metadata.name}}{{"\n"}}{{end}}') + kubectl port-forward service/rps 5000:5000 > /dev/null & + export URL=http://localhost:5000 + pytest tests/integration + PYTHONPATH="${PYTHONPATH}:./src" coverage run -m pytest -v tests/integration + coverage report -m + - name: Cleanup + if: always() + run: | + kind delete cluster --name test-cd diff --git a/.github/workflows/workflow1.yml b/.github/workflows/workflow1.yml new file mode 100644 index 00000000..5030fbe4 --- /dev/null +++ b/.github/workflows/workflow1.yml @@ -0,0 +1,37 @@ +name: Pylint + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10"] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint + pip install black + pip install pytest-cov + pip install requests + export URL=http://localhost:5000 + pip install -r $(git ls-files 'requirements.txt') + - name: using black to format + run: | + black $(git ls-files '*.py') + - name: testing with coverage + run: | + PYTHONPATH="${PYTHONPATH}:./src" coverage run -m pytest -v tests/unit + PYTHONPATH="${PYTHONPATH}:./src" coverage run -m pytest -v tests/functional + - name: Get coverage report + run: coverage report -m + - name: Analysing the code with pylint + run: | + PYTHONPATH="${PYTHONPATH}:./src" pylint $(git ls-files '*.py') diff --git a/.github/workflows/workflow2.yml b/.github/workflows/workflow2.yml new file mode 100644 index 00000000..dbf08980 --- /dev/null +++ b/.github/workflows/workflow2.yml @@ -0,0 +1,32 @@ +name: docker build + +on: [push, pull_request] + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v3 + - + name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - + name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + push: true + tags: | + ${{ secrets.DOCKERHUB_USERNAME }}/rps:latest + ${{ secrets.DOCKERHUB_USERNAME }}/rps:${{ github.sha }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..804f81eb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.10.10-slim + +WORKDIR /app + +COPY ./src ./src + +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +CMD ["flask", "--app", "src/rock_paper_scissors/app", "run"] diff --git a/src/rock_paper_scissors/app.py b/src/rock_paper_scissors/app.py index bdfa7579..7a7635ce 100644 --- a/src/rock_paper_scissors/app.py +++ b/src/rock_paper_scissors/app.py @@ -26,25 +26,33 @@ class InvalidMove(Exception): Invalid Move """ + app = Flask(__name__) @app.route("/health") def health(): + """ + returns the health of the server + """ return "OK" -@app.route("/rps", methods = ['POST']) + +@app.route("/rps", methods=["POST"]) def rps(): + """ + returns results of rock paper scissors game. + """ # 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}") + except ValueError as exc: + raise InvalidMove(f"{move} is invalid. Valid moves: {mapping}") from exc - 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: @@ -52,6 +60,6 @@ def rps(): 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} + ) diff --git a/src/rock_paper_scissors/rps.py b/src/rock_paper_scissors/rps.py index 568bf696..8c0671e1 100644 --- a/src/rock_paper_scissors/rps.py +++ b/src/rock_paper_scissors/rps.py @@ -22,9 +22,10 @@ def rock_paper_scissors(user_choice: int) -> int: """ # Generate computer choice pc_choice = random.randint(0, 2) - if pc_choice == user_choice: + if pc_choice == user_choice: # pylint: disable=R1705 return 0, pc_choice elif (user_choice + 1) % 3 == pc_choice % 3: return -1, pc_choice else: return 1, pc_choice + diff --git a/tests/functional/test_app.py b/tests/functional/test_app.py index 6aca6f34..127d0ed1 100644 --- a/tests/functional/test_app.py +++ b/tests/functional/test_app.py @@ -1,16 +1,28 @@ +""" +Importing modules +""" import json - from rock_paper_scissors.app import app def test_rps(): """ - Test Flask Application and API for Rock Paper Scissors + Test Flask Application and API for Rock Paper Scissors.. """ - - with app.test_client() as test_client: - response = test_client.post('/rps', - data=json.dumps(dict(move='Rock')), - content_type='application/json') - assert response.status_code == 200 + moves = ["Rock", "Paper", "Scissors"] + for move in moves: + response = test_client.post( + "/rps", data=json.dumps({"move": move}), content_type="application/json" + ) + assert response.status_code == 200 + assert test_client.get("/health").data.decode("utf-8") == "OK" + + try: + response = test_client.post( + "/rps", + data=json.dumps({"move": "wrong"}), + content_type="application/json", + ) + except ValueError: + pass diff --git a/tests/integration/integration_test.py b/tests/integration/integration_test.py new file mode 100644 index 00000000..33e3baf9 --- /dev/null +++ b/tests/integration/integration_test.py @@ -0,0 +1,25 @@ +""" +Importing dependencies +""" + +import os +import requests + +URL = os.environ["URL"] + + +def test_health(): # pylint: disable=W3101 + """ + Testing the /health endpoint + """ + response = requests.get(f"{URL}/health", timeout=60) + assert response.status_code == 200 + assert response.content == b"OK" + + +def test_rps(): + """ + Testing deployed application for Rock Paper Scissors.. + """ + res = requests.post(URL + "/rps", json={"move": "rock"}, timeout=60) + assert res.status_code == 200 diff --git a/tests/unit/test_rps.py b/tests/unit/test_rps.py index 27a9e4a8..08a78d6b 100644 --- a/tests/unit/test_rps.py +++ b/tests/unit/test_rps.py @@ -1,3 +1,6 @@ +""" +importing modules +""" from rock_paper_scissors.rps import rock_paper_scissors @@ -7,3 +10,14 @@ def test_rps(): """ assert rock_paper_scissors(1) is not None + assert rock_paper_scissors(3) in [ + (-1, 1), + (-1, 0), + (-1, 2), + (1, 0), + (1, 1), + (1, -1), + (1, 2), + (2, 1), + (0, 1), + ]