![Logo](img/recipe.png)
An end to end process to develop, test, and deploy a small scale application using CI/CD, GitHub Actions, and Docker. The application is built using two repositories: one for the frontend and one for the backend.
See the Backend»
·
See the project»
·
How to use this repo
·
Report Bug
·
Request Feature
Table of Contents
This project is to practice end-to-end development using best practice implementations of early functional as well as unit testing, CI/CD and Dockerization of applicatins.
I am also making this repo explicity detailed so that anyone can follow it from start to finish.
Implement an anonymous web application for managing cooking recipes. Single page application.
Functional requirements:
- The user must be able to create, read, update, and delete recipes (CRUD).
- The user must be able to rate recipes from 1 to 5 stars.
- The user must be able to mark recipes as Favorite with a checkbox.
- The user must be able to create and edit recipes in a modal view.
- User authentication is not needed.
-
REST (Representational State Transfer) is a software architectural style that defines a set of constraints to be used for creating web services.
-
A REST API (Application Programming Interface) is a set of rules that defines how two systems can interact over the internet, using the HTTP protocol.
-
A REST API defines a set of functions that a developer can use to send requests and receive responses. The requests and responses are typically in the form of JSON (JavaScript Object Notation) or XML (Extensible Markup Language) messages.
Flask is a web (micro)framework for Python:
- Multiple extensions available to enhance features
- It makes programming a website much easier than usingplain HTTPServer.
- Integrated support for unit testing
- Contains development server and debugger
- Relies on Jinja templating engine to ease HTML creation
The Database:
- PGAdmin is a PostgreSQL Management Tool used to interact with the Postgres database sessions, both locally and remote servers as well. You can use PGAdmin to perform any sort of database administration required for a Postgres database.
- PostgreSQL, also known as Postgres, is a free and open-source relational database management system emphasizing extensibility and SQL compliance. It is designed to handle a range of workloads, from single machines to data warehouses or Web services with many concurrent users. It is the default database for macOS Server, and is also available for Linux, FreeBSD, OpenBSD, and Windows.
You will need the following software downloaded on your workstation:
- Clone the backend repo
git clone https://github.com/vtwoptwo/ie-backend.git
- Clone the frontend repo
git clone https://github.com/vtwoptwo/ie-frontend.git
Make sure you have everything downloaded from the pre-requisites before continuing.
- Create project repository in Github
Name: Backend | Access: Public | Add .gitignore template: None
- Open VSCode and select "Clone Github Repository"
- Install Virtualenv:
pip install virtualenv
#I had an error for so run this command just in case before starting
python -m pip install psycopg2
- Create new virtualenv:
py –m venv .venv
- Activate new virtual environment
.venv/scripts/activate
- Install Flask
pip install flask, flask_sqlalchemy, python-dotenv, flask_cors
- In your PGAdmin right click on databases and create a new database. The following is the table we will create in the dabase using code-first architecture.
Attribute | Type | Description |
---|---|---|
Name | String | Name of recipe |
Ingredients | String | Required Ingredients |
Steps | String | Instructions on creating the recipe |
Rating | Int | 1-5 Star Rating |
Favorite | bool | Marked as either a favorite or not |
- In your requirements make sure you have the following requirements set:
Flask==2.2.2
Flask-Cors==3.0.10
Flask-SQLAlchemy==2.5.1
psycopg2==2.9.3
python-dotenv==0.21.0
SQLAlchemy==1.4.41
pytest
pytest-cov
pip install -r ./requirements.txt
- Implementing the "Code-First Approach" with the DB
Creating a table model for our recipe tracker (in models.py)
class Recipe(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(32), nullable=False)
ingredients = db.Column(db.String(500), nullable=False)
instructions = db.Column(db.String(500), nullable=False)
favorite = db.Column(db.Boolean, default=False)
rating = db.Column(db.Integer, default=0)
date_created = db.Column(db.DateTime, default=datetime.utcnow)
def __repr__(self):
return f'{self.name}'
def __init__(self, name, ingredients, instructions, favorite, rating):
self.name = name
self.ingredients = ingredients
self.instructions = instructions
self.favorite = favorite
self.rating = rating
In the python CLI to test the code/database:
from app import app,db,Recipe
with app.app_context():
db.create_all()
r1 = Recipe(name="Soup", ingredients="veggies,vegeta,water,chicken", instructions=("Boil all ingredients for a few hours"), favorite=True, rating=5)
>>> with app.app_context():
db.session.add(r1)
db.session.commit()
run the backend
py ./run.py
Implementing vue.js
- In your frontend folder run the following:
$ npm install -g @vue/cli
$ vue create frontend
#Manually select features (Babel, Router)
#Version: 2.x
#History mode for router: yes
#Config: dedicated config files
#Save for future projects: No
to run the vue application run:
cd frontend
npm run serve
To create a production build, run
npm run build
To quit running it
crtl + c
- Install the following libraries
npm i axios
npm install [email protected] --save
- Testing the API by opening up POSTMAN and sending in requests to http://127.0.0.1:5000/recipes
Request = GET
{
"name": "Tortilla de patata",
"ingredients": "potatoes,onions,salt,eggs",
"instructions": "if u dont know how to make it google it",
"favorite": false,
"rating": 4
}
Request = POST
{
"name": "Tortilla de patata",
"ingredients": "potatoes,onions,salt,eggs",
"instructions": "if u dont know how to make it google it",
"favorite": True,
"rating": 4
}
- Ensure that your index folder has the same routes as your backend_api/routes.py
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/skull',
name: 'Skull',
component: Skull
},
{
path: '/recipes',
name: 'AppRecipes',
component: AppRecipes
}
]
- Your main.js initiates the App.vue app: ensure that any extra components frome vue that you use are imported in the main.js file
More documentation is available here:
import Vue from 'vue'
import 'bootstrap-vue'
import App from './App.vue'
import router from './router'
import 'bootstrap/dist/css/bootstrap.css'
import { BootstrapVue } from 'bootstrap-vue'
import { BFormRating } from 'bootstrap-vue'
Vue.component('b-form-rating', BFormRating)
import { IconsPlugin } from 'bootstrap-vue'
Vue.use(IconsPlugin)
Vue.use(BootstrapVue)
- Understanding the routing in VUE using CRUD
Description | GET | POST | PUT | DELETE |
---|---|---|---|---|
Functions | RESTgetRecipes() | RESTcreateRecipe(payload) | RESTupdateRecipe(payload, recipeId) | RESTdeleteRecipe(id) |
Action | Reads all recipes | Creates new recipes by using a payload variable. These are variables which are mapped to the attribute variables in the database in order to create a new instance. | Updates an existing recipe by using the recipe id to identify it and the payload to change specific values. Any values not changed remain the same. | Deletes a recipe using the recipe id |
Responses* | 200 | 200 | 200 | 200 |
- Understanding the routing between pages or components in the app
Function | Description | ||||
---|---|---|---|---|---|
gotohome() | initForm() | gonSubmit(evt) | onSubmitUpdate(evt) | deleteRecipe(recipe) | editRecipe(recipe) |
Switches back to the Home.vue page | Initialized the forms using the HTML Modal Code | event tracking of Submit Button to create a Recipe | event tracking of Submit Button to edit a Recipe | event tracking of Delete Button to delete a Recipe | event tracking of Update to initialize update form |
- Workflows stored as yml files
- fullly integrated with GitHub
- Respond to GitHub events
- Live logs and visualized workflow execution
- community powered workflows
- GitHub hosted or self-hosted runners
- Built-in secret store
To understand the infrastructure see the following diagram:
To see the infrastructure as code written in Bicep, refer to: IaC
Within the bicep file, in order to launch two webapps at once I implemented the following for loop:
param names array = [ 'FE', 'BE' ]
module appService 'modules/appModule.bicep' = [ for i in range(0,2): {
name: 'appService${names[i]}'
params: {
location: location
appServiceAppName:'${appServiceAppName}${names[i]}'
appServicePlanName: appServicePlanName
environmentType: environmentType
dbhost: dbhost
dbuser: dbuser
dbpass: dbpass
dbname: dbname
}
}]
// output using loop
output appServiceAppHostName array = [ for i in range(0,2): {
name: 'appService${names[i]}'
value: appService[i].outputs.appServiceAppHostName
}]
See the IaC Repository for more info.
For the Frontend Workflow, I had to figure out a way to push different environment variables to the .env file that vue.js reads automatically.
Since both webapps were being deployed as a production slot, the dev
webapp did not recognize the correct environment file. To fix this I implemented the following code in the yml workflow:
- name: Create .env for development
if: github.ref == 'refs/heads/dev'
run: |
echo "${{ secrets.ENV_DEV_FILE }}" > .env.production
- name: Create .env for production
if: github.ref == 'refs/heads/main'
run: |
echo "${{ secrets.ENV_PROD_FILE }}" > .env.production
I set the VUE_ENV_ROOT_URL={url of backend} as two separate repository secrets in GitHub and call either file to be the official .env.production file based on which branched was pushed.
To increase the deployment efficiency, I deployed using release.zip format.
For the Backend Workflow, I implemented a pytest job in the build of the application, and then conditional deployments depending on which branch was pushed.
The testing was implemented as follows:
- name: Test with pytest
run: |
python -m pytest --cov=backend_api -v
See the open issues for a full list of proposed features (and known issues).
The idea is to create a nice and simple repo for CPP Basic Rules, information, definitions, best practices etc. Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.
If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement".
Don't forget to give the project a star! Thanks again!
- Fork the Project
- Create your Feature Branch (
git checkout -b feature/AmazingFeature
) - Commit your Changes (
git commit -m 'Add some AmazingFeature'
) - Push to the Branch (
git push origin feature/AmazingFeature
) - Open a Pull Request
Distributed under the MIT License. See LICENSE.txt
for more information.