final ass

This commit is contained in:
suhail 2022-05-22 16:10:18 +05:30
parent 55f7fe46a1
commit afa3cce0d3
143 changed files with 48064 additions and 0 deletions

113
z3/README.md Normal file
View File

@ -0,0 +1,113 @@
# Fruit app
## Overview of the APP
The application consist of two parts, an API and a front end. The application displays 5 fruits which are seeded to the database at the startup. The get API provides the list of fruits. Anyone can like a fruit. In this case the count of like will be increased.
![homepage](./src/images/homepage.png?raw=true 'System Architecture')
## Technology used
- Flask (Backend Rest API)
- Angular (Frontend)
- MySQL - as persistent database
- Azure
- Docker
## prerequist:
- mysql-client : https://dev.mysql.com/downloads/shell/
- az command line.
## Azure public Cloud :
Microsoft Azure is a cloud computing service operated by Microsoft, often referred to as Azure. it offers more than 200 cloud products and services designed to solve todays challenges. We used this cloud to deploy our fruit-app application in the cloud.
Here is a list of used services:
- **Resource group**: Azure Resource Groups are logical collections of azure service. it is used to group a collection of services to easy manage them.
- **Azure App Service**: categorized as Platform as a Service, provides a service that allows you to build, manage, and deploy enterprise-grade scaled web apps without managing the underlying infrastructure.
- **ACR**: Azure Container registry of Docker images to store our images.
- **Mysql Database**: One of zure services to create/manage databases (like mysql or postgresql).
- **Identity Access management(IAM)**: is a web service that helps securely control access to Azure resources. We use IAM to control who is authenticated (signed in) and authorized (has permissions) to use resources.
## Folder structure:
Description of the folder structure:
- **src**: contain the application code
- **prepare-app.sh**: Script to build the docker image for the application and deploy the app to the azure cloud using commands
- **stop-app.sh** Script to stop and remove the app
- **README.md**: Documentation file
- **az-docker-compose.yml**: Docker compose for the webapp for azure configuration.
## Methode of communication:
For the communication between:
- Mysql-backend: The credentials for mysql are passed to the backend application as environement variables.
- frontend-backend: For the connection between the frontend and backend, we used the nginx base image for our frontend image. Also ConfigMap is used to store nginx configuration file to forward all the `/api` request to the backend service.
## How to prepare, run, pause and delete the application.
### Prepare the app
Now to prepare the application you have to run the command :
```
sh prepare-app.sh
```
the script which will create a resource group named 'suhailgroup' and will add all the required services for our application to run.
### To stop the app
The `stop-app.sh` script will remove the resources group and all the created services related to that group.
## Workflow:
Here is our deployment workflow:
1 - Build our application image locally.
2- Creat the resource group to group our application service in order to be able later to manage them as unit.
3- Creat a azure container registry to store our application image.
4- Tag the previous built images and push them to the created ACR registry.
5 - Creat and grand acrpull permission to a Identity service in order to use it later to pull our images from the web app service.
6- Creating Mysql database and padate the firewall rule to allow external access to database.
7 - Creat the webapp service for our application( we used the --acr-identity arguement to pass the Identity role we created prevouisly created and a docker-compose to run for the application) and also passing the environment variables for database access to that webapp containers .
## How to view the application on the web.
The application is accessible through: `https://suhail5742.azurewebsites.net/`
## Restart the service on failure:
In order to have a heigh availibiity for our application, we added the `restart` docker option ,to restart our containers if they exits due to an error:
```
restart: on-failure:5
```
## resources:
[Deploy docker image to azure webapp](https://www.azuredevopslabs.com/labs/vstsextend/docker/)
[Create web app in azure](https://docs.microsoft.com/en-us/azure/devops/pipelines/apps/cd/deploy-docker-webapp?view=azure-devops&tabs=java%2Cyaml)
[Docker compose options for azure](https://docs.microsoft.com/en-us/azure/app-service/configure-custom-container?pivots=container-linux#docker-compose-options)
[Azure Mysql Service](https://azure.microsoft.com/en-us/services/mysql/#overview)
[Azure web app Commands](https://docs.microsoft.com/en-us/cli/azure/webapp?view=azure-cli-latest)
[Azure identity management](https://docs.microsoft.com/en-us/azure/active-directory/fundamentals/active-directory-whatis)

21
z3/az-docker-compose.yml Normal file
View File

@ -0,0 +1,21 @@
version: '3.8'
services:
web:
image: suhailregistry1.azurecr.io/fruitapp-frontend:latest
restart: on-failure:5
backend-service:
image: suhailregistry1.azurecr.io/fruitapp-backend:latest
restart: on-failure:5
env:
- name: SQLALCHEMY_DATABASE_URI
value: mysql://user:password@mysql-service/main
- name: FLASK_APP
value: app.py
- name: MYSQL_DATABASE
value: main
- name: MYSQL_USER
value: user
- name: MYSQL_PASSWORD
value: password
- name: MYSQL_ROOT_PASSWORD
value: root

2
z3/fruitapp-main/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.history
.idea

104
z3/fruitapp-main/Readme.md Normal file
View File

@ -0,0 +1,104 @@
# Overview of the APP
The application consist of two parts, an API and a front end. The application displays 5 fruits which are seeded to the database at the startup. The get API provides the list of fruits. Anyone can like a fruit. In this case the count of like will be increased.
![homepage](images/homepage.png?raw=true "System Architecture")
# Requirements
The basic system requirements are as follows
- Any OS, preferably Linux
- Docker
- Docker compose
# Architecture
![Architecture](images/fruitapp.jpg?raw=true "System Architecture")
### Technology used
- Flask (Backend Rest API)
- Angular (Frontend)
- MySQL - as persistent database
# Docker environment
For shipping and deploying the application docker-compose is used. All the configurations are in the docker-compose.yml file.
Key points of the docker-compose.yml is given below.
## services
- **backend:** runs the Flask web API.
- **db** runs the mysql database required for the backend API
- **web** this is the front-end application
## Virtual networks
One virtual network is used
- main
## list of the containers
### fruitapp-backend
This container runs under the service name backend. The dockerfile user for this container is located at `fruitapp-backend/Dockerfile`. This container is based on python:3.9 image.
### fruitapp-frontend
The frontend container for the application. The dockerfile is located at `fruitapp-frontend/.docker/dev/Dockerfile`. Base image used to build this container is `Node:14`.
### db container
The container is built using the official `mysql:5.7.22` image pulled from dockerhub.
## Preparation
To prepare the environment for the first time run the following command
```shell
bash prepare-app.sh
```
## Running
Run the app background
```shell
bash start-app.sh
```
see the logs of backend
```
docker-compose logs -f backend
```
see the logs of forntend
```
docker-compose logs -f web
```
see the logs of database
```
docker-compose logs -f db
```
## Stopping
```shell
bash stop-app.sh
```
## Removing containers
```shell
bash remove-app.sh
```
# Viewing the app
After running the app. visit [http://localhost:4200](http://localhost:4200) to view the app

View File

@ -0,0 +1,55 @@
version: "3.8"
services:
backend:
build:
context: ./fruitapp-backend
dockerfile: Dockerfile
container_name: fruitapp-backend
env_file:
- ./fruitapp-backend/.env
command: "bash scripts/entrypoint.sh"
ports:
- "8001:5000"
volumes:
- ./fruitapp-backend:/app
depends_on:
- db
restart: unless-stopped
networks:
- main
db:
image: mysql:5.7.22
container_name: fruitapp-db
restart: unless-stopped
env_file:
- ./fruitapp-backend/.env
volumes:
- msql-data:/var/lib/mysql
ports:
- "33067:3306"
networks:
- main
web:
build:
context: ./fruitapp-frontend
dockerfile: .docker/dev/Dockerfile
container_name: fruitapp-frontend
ports:
- 4200:4200
volumes:
- ./fruitapp-frontend:/app
command: >
bash -c "cp -rfu /cache/node_modules/. /app/node_modules/ && ng serve --host=0.0.0.0 --aot"
depends_on:
- db
- backend
networks:
- main
volumes:
msql-data:
networks:
main:

View File

@ -0,0 +1,8 @@
MYSQL_DATABASE=main
MYSQL_USER=root
MYSQL_PASSWORD=root
MYSQL_ROOT_PASSWORD=root
SQLALCHEMY_DATABASE_URI=mysql://root:root@db/main
FLASK_APP=app.py

View File

@ -0,0 +1,6 @@
MYSQL_DATABASE=main
MYSQL_USER=root
MYSQL_PASSWORD=root
MYSQL_ROOT_PASSWORD=root
SQLALCHEMY_DATABASE_URI=mysql://root:root@db/main

View File

@ -0,0 +1,25 @@
.DS_Store
.flaskenv
*.pyc
*.pyo
env/
venv/
.venv/
env*
dist/
build/
*.egg
*.egg-info/
_mailinglist
.tox/
.cache/
.pytest_cache/
.idea/
docs/_build/
.vscode
# Coverage reports
htmlcov/
.coverage
.coverage.*
*,cover

View File

@ -0,0 +1,8 @@
FROM python:3.9
ENV PYTHONUNBUFFERED 1
WORKDIR /app
COPY requirements.txt /app/requirements.txt
RUN pip install -r requirements.txt
COPY ./scripts/entrypoint.sh /app/scripts/entrypoint.sh
COPY . /app

View File

@ -0,0 +1,2 @@
from dotenv import load_dotenv
load_dotenv()

View File

@ -0,0 +1,54 @@
import os
from dataclasses import dataclass
from flask import Flask, jsonify, abort
from flask_cors import CORS
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
CORS(app)
SQLALCHEMY_DATABASE_URI = os.getenv('SQLALCHEMY_DATABASE_URI')
app.config["SQLALCHEMY_DATABASE_URI"] = SQLALCHEMY_DATABASE_URI
db = SQLAlchemy(app)
@dataclass
class Product(db.Model):
id: int
title: str
image: str
likes: int
id = db.Column(db.Integer, primary_key=True, autoincrement=False)
title = db.Column(db.String(200))
image = db.Column(db.TEXT(20000000))
likes = db.Column(db.Integer, default=0)
@dataclass
class ProductUser(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer)
product_id = db.Column(db.Integer)
@app.route('/api/products')
def index():
return jsonify(Product.query.all())
@app.route('/api/products/<int:id>/like', methods=['POST'])
def like(id):
# define a static user id for demo
try:
product = Product.query.get(id)
product.likes += 1
db.session.commit()
except:
abort(400, 'bad request')
return jsonify(product)
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')

View File

@ -0,0 +1,12 @@
from flask_migrate import Migrate, MigrateCommand
from flask_script import Manager
from main import app, db
migrate = Migrate(app, db, compare_type=True)
manager = Manager(app)
manager.add_command('db', MigrateCommand)
if __name__ == '__main__':
manager.run()

View File

@ -0,0 +1 @@
Generic single-database configuration.

View File

@ -0,0 +1,45 @@
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

View File

@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,40 @@
"""empty message
Revision ID: 843c810aec1f
Revises:
Create Date: 2020-12-08 08:40:37.736465
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '843c810aec1f'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('product',
sa.Column('id', sa.Integer(), autoincrement=False, nullable=False),
sa.Column('title', sa.String(length=200), nullable=True),
sa.Column('image', sa.String(length=200), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('product_user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('product_id', sa.Integer(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('product_user')
op.drop_table('product')
# ### end Alembic commands ###

View File

@ -0,0 +1,34 @@
"""empty message
Revision ID: 85749c0c4589
Revises: b3ff59df2833
Create Date: 2022-04-08 07:46:57.160792
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '85749c0c4589'
down_revision = 'b3ff59df2833'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('product', 'image',
existing_type=mysql.VARCHAR(length=20000),
type_=sa.TEXT(length=20000000),
existing_nullable=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('product', 'image',
existing_type=sa.TEXT(length=20000000),
type_=mysql.VARCHAR(length=20000),
existing_nullable=True)
# ### end Alembic commands ###

View File

@ -0,0 +1,34 @@
"""empty message
Revision ID: b3ff59df2833
Revises: fee4d1b1d192
Create Date: 2022-04-08 07:33:52.082355
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = 'b3ff59df2833'
down_revision = 'fee4d1b1d192'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('product', 'image',
existing_type=mysql.VARCHAR(length=200),
type_=sa.String(length=20000),
existing_nullable=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('product', 'image',
existing_type=sa.String(length=20000),
type_=mysql.VARCHAR(length=200),
existing_nullable=True)
# ### end Alembic commands ###

View File

@ -0,0 +1,28 @@
"""empty message
Revision ID: fee4d1b1d192
Revises: 843c810aec1f
Create Date: 2022-04-08 07:05:04.647143
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'fee4d1b1d192'
down_revision = '843c810aec1f'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('product', sa.Column('likes', sa.Integer(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('product', 'likes')
# ### end Alembic commands ###

View File

@ -0,0 +1,27 @@
import json
import os
import pika
RABBIT_ENDPOINT = os.environ['RABBIT_ENDPOINT']
class Producer:
"""Implement the producer logic."""
def __init__(self):
self.params = pika.URLParameters(RABBIT_ENDPOINT)
self.connection = pika.BlockingConnection(self.params)
def publish(self, method, body):
properties = pika.BasicProperties(method)
if not self.connection or self.connection.is_closed:
self.connection = pika.BlockingConnection(self.params)
channel = self.connection.channel()
channel.basic_publish(
exchange='',
routing_key='admin',
body=json.dumps(body),
properties=properties,
)
print('Product published in queue: MAIN')

View File

@ -0,0 +1,13 @@
Flask==1.1.2
werkzeug==1.0.1
Flask-SQLAlchemy==2.4.4
SQLAlchemy==1.3.20
Flask-Migrate==2.5.3
Flask-Script==2.0.6
Flask-Cors==3.0.9
requests==2.25.0
mysqlclient==2.0.1
pika==1.1.0
jinja2<3.1.0
itsdangerous==2.0.1
python-dotenv==0.20.0

View File

@ -0,0 +1,5 @@
#!/bin/bash
python manage.py db upgrade
echo "DB migration done."
python seeder.py
python main.py

View File

@ -0,0 +1,40 @@
from main import Product, db
def insert_data(data):
product = Product.query.get(data['id'])
if not product:
product = Product(id=data['id'], title=data['title'], image=data['image'])
db.session.add(product)
db.session.commit()
print('inserted ', data['title'])
else:
print('Product already exists.')
PRODUCTS = [
{
'id': 1,
'title': 'Apple',
'image': 'https://media.istockphoto.com/photos/red-apple-picture-id184276818?k=20&m=184276818&s=612x612&w=0&h=QxOcueqAUVTdiJ7DVoCu-BkNCIuwliPEgtAQhgvBA_g='
},
{
'id': 2,
'title': 'Orange',
'image': 'https://media.istockphoto.com/photos/tiny-tangerine-picture-id89951356?k=20&m=89951356&s=170667a&w=0&h=9oW2EBTASfaBol8qYVYB8YNYcSoNe1AImG4wqdgErEs='
},
{
'id': 3,
'title': 'Banana',
'image': 'https://media.istockphoto.com/photos/banana-picture-id1184345169?k=20&m=1184345169&s=170667a&w=0&h=t22KqOZ9EEwyRj7i35uxY-Xf6P_gAgLejd-SeReTnPY='
},
{
'id': 4,
'title': 'Avocado',
'image': 'https://media.istockphoto.com/photos/half-of-fresh-ripe-avocado-isolated-on-white-background-picture-id1278032327?b=1&k=20&m=1278032327&s=170667a&w=0&h=7eMAI9S_YBiVaDN49zk5gUZEhhA1oGDIWh3Tdst6q4Y='
}
]
for p in PRODUCTS:
insert_data(p)

View File

@ -0,0 +1,17 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.

View File

@ -0,0 +1,15 @@
FROM node:14
RUN npm install @angular/cli@11.0.0 -g
# Create and define the node_modules's cache directory.
WORKDIR /cache
COPY package.json .
COPY package-lock.json .
RUN npm install
# define the application's working directory.
WORKDIR /app
COPY . .
ENV PATH /app/node_modules/.bin:$PATH

View File

@ -0,0 +1,9 @@
node_modules
npm-debug.log
.dockerignore
.git
.gitignore
README.md
LICENSE
.vscode
.history

View File

@ -0,0 +1,16 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

View File

@ -0,0 +1,46 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out
# dependencies
/node_modules
# profiling files
chrome-profiler-events*.json
speed-measure-plugin*.json
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db

View File

@ -0,0 +1,27 @@
# DockerizeAngular
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 11.2.2.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.

View File

@ -0,0 +1,130 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"cli": {
"analytics": "d44c8c58-a3f9-4508-9e41-039ac5874480"
},
"version": 1,
"newProjectRoot": "projects",
"projects": {
"dockerize-angular": {
"projectType": "application",
"schematics": {
"@schematics/angular:application": {
"strict": true
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/dockerize-angular",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"aot": true,
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [ "./node_modules/@angular/material/prebuilt-themes/purple-green.css"],
"scripts": []
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
]
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "dockerize-angular:build"
},
"configurations": {
"production": {
"browserTarget": "dockerize-angular:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "dockerize-angular:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"./node_modules/@angular/material/prebuilt-themes/purple-green.css",
"src/styles.css"
],
"scripts": []
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"tsconfig.app.json",
"tsconfig.spec.json",
"e2e/tsconfig.json"
],
"exclude": [
"**/node_modules/**"
]
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "dockerize-angular:serve"
},
"configurations": {
"production": {
"devServerTarget": "dockerize-angular:serve:production"
}
}
}
}
}
},
"defaultProject": "dockerize-angular"
}

View File

@ -0,0 +1,37 @@
// @ts-check
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter');
/**
* @type { import("protractor").Config }
*/
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./src/**/*.e2e-spec.ts'
],
capabilities: {
browserName: 'chrome'
},
directConnect: true,
SELENIUM_PROMISE_MANAGER: false,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: require('path').join(__dirname, './tsconfig.json')
});
jasmine.getEnv().addReporter(new SpecReporter({
spec: {
displayStacktrace: StacktraceOption.PRETTY
}
}));
}
};

View File

@ -0,0 +1,23 @@
import { browser, logging } from 'protractor';
import { AppPage } from './app.po';
describe('workspace-project App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should display welcome message', async () => {
await page.navigateTo();
expect(await page.getTitleText()).toEqual('dockerize-angular app is running!');
});
afterEach(async () => {
// Assert that there are no errors emitted from the browser
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
expect(logs).not.toContain(jasmine.objectContaining({
level: logging.Level.SEVERE,
} as logging.Entry));
});
});

View File

@ -0,0 +1,11 @@
import { browser, by, element } from 'protractor';
export class AppPage {
async navigateTo(): Promise<unknown> {
return browser.get(browser.baseUrl);
}
async getTitleText(): Promise<string> {
return element(by.css('app-root .content span')).getText();
}
}

View File

@ -0,0 +1,13 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"module": "commonjs",
"target": "es2018",
"types": [
"jasmine",
"node"
]
}
}

View File

@ -0,0 +1,57 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
customLaunchers: {
ChromeHeadless: {
base: 'Chrome',
flags: [
'--no-sandbox',
'--disable-gpu',
'--headless',
'--remote-debugging-port=9222'
]
}
},
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, './coverage/dockerize-angular'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
]
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,49 @@
{
"name": "dockerize-angular",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@angular/animations": "~11.2.3",
"@angular/cdk": "^11.2.13",
"@angular/common": "~11.2.3",
"@angular/compiler": "~11.2.3",
"@angular/core": "~11.2.3",
"@angular/forms": "~11.2.3",
"@angular/localize": "~11.2.3",
"@angular/material": "^11.2.13",
"@angular/platform-browser": "~11.2.3",
"@angular/platform-browser-dynamic": "~11.2.3",
"@angular/router": "~11.2.3",
"hello-world-npm": "^1.1.1",
"rxjs": "~6.6.0",
"tslib": "^2.0.0",
"zone.js": "~0.11.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.1102.2",
"@angular/cli": "~11.2.2",
"@angular/compiler-cli": "~11.2.3",
"@types/jasmine": "~3.6.0",
"@types/node": "^12.11.1",
"codelyzer": "^6.0.0",
"jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~6.1.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.0.3",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"protractor": "~7.0.0",
"ts-node": "~8.3.0",
"tslint": "~6.1.0",
"typescript": "~4.1.2"
}
}

View File

@ -0,0 +1,14 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DashboardComponent } from './dashboard/dashboard.component';
const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent },
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

View File

@ -0,0 +1 @@
<router-outlet></router-outlet>

View File

@ -0,0 +1,35 @@
import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [
AppComponent
],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'dockerize-angular'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('dockerize-angular');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('.content span').textContent).toContain('dockerize-angular app is running!');
});
});

View File

@ -0,0 +1,10 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'dockerize-angular';
}

View File

@ -0,0 +1,35 @@
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatInputModule } from '@angular/material/input';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { HttpErrorHandler } from './service/http-error-handler.service';
import { MessageService } from './service/message.service';
import {MatIconModule} from '@angular/material/icon';
@NgModule({
declarations: [
AppComponent,
DashboardComponent,
],
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule,
BrowserAnimationsModule,
MatInputModule,
MatButtonModule,
MatIconModule,
MatCardModule
],
providers: [
HttpErrorHandler,
MessageService,
],
bootstrap: [AppComponent]
})
export class AppModule { }

View File

@ -0,0 +1,21 @@
.product-card-wrapper{
width: 1112px;
margin: 0 auto;
display: flex;
flex-wrap: wrap;
}
.card{
margin-left: 10px;
margin-right: 10px;
}
.mat-card{
background: #fff;
color: #424242;
}
.example-card {
max-width: 200px;
margin-top: 20px;
}

View File

@ -0,0 +1,16 @@
<div class="product-card-wrapper">
<div class="card" *ngFor="let p of products">
<mat-card class="example-card">
<mat-card-title style="margin-bottom: 15px; font-size: 18px;">{{ p.title }}</mat-card-title>
<mat-card-content style="margin-bottom: 0;">
<img src="{{ p.image }}" height="180" />
</mat-card-content>
<mat-card-actions>
<button mat-button (click)="likeProduct(p.id)" style="display: flex; align-items: center;">
<mat-icon aria-hidden="false" aria-label="Example home icon">thumb_up</mat-icon>
<span style="margin-left: 10px;">{{ p.likes }}</span>
</button>
</mat-card-actions>
</mat-card>
</div>
</div>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardComponent } from './dashboard.component';
describe('DashboardComponent', () => {
let component: DashboardComponent;
let fixture: ComponentFixture<DashboardComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ DashboardComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(DashboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,56 @@
import { Component, OnInit } from '@angular/core';
import { first } from 'rxjs/operators';
import { Product } from '../models/product';
import { DashboardService } from '../service/dashboard.service';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.css']
})
export class DashboardComponent implements OnInit {
products: Product[] = [];
constructor(private dashboardService: DashboardService) {
}
ngOnInit(): void {
console.log('dashboard');
this.getProducts();
}
getProducts() {
this.dashboardService.getProducts()
.pipe(first())
.subscribe(
data => {
this.products = data;
},
error => {
console.log(error);
}
);
}
likeProduct(id: number) {
console.log('like clicked ...........');
this.dashboardService.likeProduct(id)
.pipe(first())
.subscribe(
data => {
this.products.forEach(product => {
if (product.id == data.id) {
product.likes = data.likes;
}
});
console.log(data);
},
error => {
console.log(error);
}
);
}
}

View File

@ -0,0 +1,6 @@
export class Product {
id!: number;
title!: string;
image!: string;
likes!: number;
}

View File

@ -0,0 +1,33 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { environment } from '../../environments/environment';
import { HandleError, HttpErrorHandler } from './http-error-handler.service';
@Injectable({ providedIn: 'root' })
export class DashboardService {
apiUrl = environment.apiUrl;
private handleError: HandleError;
constructor(private http: HttpClient, httpErrorHandler: HttpErrorHandler) {
this.handleError = httpErrorHandler.createHandleError('DashboardService');
}
getProducts(): Observable<any> {
return this.http.get<any>(this.apiUrl + '/api/products')
.pipe(
catchError(this.handleError('getToDo', []))
);
}
likeProduct(id: number): Observable<any> {
return this.http.post<any>(this.apiUrl + `/api/products/${id}/like`, null)
.pipe(
catchError(this.handleError('like'))
);
}
}

View File

@ -0,0 +1,46 @@
import { HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, throwError } from 'rxjs';
import { MessageService } from './message.service';
/** Type of the handleError function returned by HttpErrorHandler.createHandleError */
export type HandleError =
<T> (operation?: string, result?: T) => (error: HttpErrorResponse) => Observable<T>;
/** Handles HttpClient errors */
@Injectable()
export class HttpErrorHandler {
constructor(private messageService: MessageService) {
}
/** Create curried handleError function that already knows the service name */
createHandleError = (serviceName = '') => <T>
(operation = 'operation', result = {} as T) => this.handleError(serviceName, operation, result);
/**
* Returns a function that handles Http operation failures.
* This error handler lets the app continue to run as if no error occurred.
* @param serviceName = name of the data service that attempted the operation
* @param operation - name of the operation that failed
* @param result - optional value to return as the observable result
*/
handleError<T>(serviceName = '', operation = 'operation', result = {} as T) {
return (error: HttpErrorResponse): Observable<T> => {
// TODO: send the error to remote logging infrastructure
console.log(error); // log to console instead
const message = (error.error instanceof ErrorEvent) ?
error.error.message :
error.message;
// TODO: better job of transforming error for user consumption
this.messageService.add(message);
// Let the app keep running by returning a safe result.
return throwError(error);
};
}
}

View File

@ -0,0 +1,17 @@
import { Injectable } from '@angular/core';
@Injectable()
export class MessageService {
messages: string[] = [];
messageType = '';
add(message: string) {
if (message && message !== '') {
this.messages.push(message);
}
}
clear() {
this.messages = [];
}
}

View File

@ -0,0 +1,3 @@
export const environment = {
production: true
};

View File

@ -0,0 +1,17 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false,
apiUrl: "http://localhost:8001"
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.

Binary file not shown.

After

Width:  |  Height:  |  Size: 948 B

View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Main</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body class="mat-typography">
<app-root></app-root>
</body>
</html>

View File

@ -0,0 +1,12 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));

View File

@ -0,0 +1,69 @@
/***************************************************************************************************
* Load `$localize` onto the global scope - used if i18n tags appear in Angular templates.
*/
import '@angular/localize/init';
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/**
* IE11 requires the following for NgClass support on SVG elements
*/
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/**
* Web Animations `@angular/platform-browser/animations`
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
*/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
* because those flags need to be set before `zone.js` being loaded, and webpack
* will put import in the top of bundle, so user need to create a separate file
* in this directory (for example: zone-flags.ts), and put the following flags
* into that file, and then add the following code before importing zone.js.
* import './zone-flags';
*
* The flags allowed in zone-flags.ts are listed here.
*
* The following flags will work for all browsers.
*
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*
* (window as any).__Zone_enable_cross_context_check = true;
*
*/
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/

View File

@ -0,0 +1,2 @@
/* You can add global styles to this file, and also import other style files */
@import '~@angular/material/prebuilt-themes/deeppurple-amber.css';

View File

@ -0,0 +1,25 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: {
context(path: string, deep?: boolean, filter?: RegExp): {
keys(): string[];
<T>(id: string): T;
};
};
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

View File

@ -0,0 +1,15 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": [
"src/main.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.d.ts"
]
}

View File

@ -0,0 +1,32 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"target": "es2015",
"module": "es2020",
"lib": [
"es2018",
"dom"
]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
},
"strictPropertyInitialization": false
}

View File

@ -0,0 +1,18 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
},
"files": [
"src/test.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

View File

@ -0,0 +1,152 @@
{
"extends": "tslint:recommended",
"rulesDirectory": [
"codelyzer"
],
"rules": {
"align": {
"options": [
"parameters",
"statements"
]
},
"array-type": false,
"arrow-return-shorthand": true,
"curly": true,
"deprecation": {
"severity": "warning"
},
"eofline": true,
"import-blacklist": [
true,
"rxjs/Rx"
],
"import-spacing": true,
"indent": {
"options": [
"spaces"
]
},
"max-classes-per-file": false,
"max-line-length": [
true,
140
],
"member-ordering": [
true,
{
"order": [
"static-field",
"instance-field",
"static-method",
"instance-method"
]
}
],
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-empty": false,
"no-inferrable-types": [
true,
"ignore-params"
],
"no-non-null-assertion": true,
"no-redundant-jsdoc": true,
"no-switch-case-fall-through": true,
"no-var-requires": false,
"object-literal-key-quotes": [
true,
"as-needed"
],
"quotemark": [
true,
"single"
],
"semicolon": {
"options": [
"always"
]
},
"space-before-function-paren": {
"options": {
"anonymous": "never",
"asyncArrow": "always",
"constructor": "never",
"method": "never",
"named": "never"
}
},
"typedef": [
true,
"call-signature"
],
"typedef-whitespace": {
"options": [
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
},
{
"call-signature": "onespace",
"index-signature": "onespace",
"parameter": "onespace",
"property-declaration": "onespace",
"variable-declaration": "onespace"
}
]
},
"variable-name": {
"options": [
"ban-keywords",
"check-format",
"allow-pascal-case"
]
},
"whitespace": {
"options": [
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type",
"check-typecast"
]
},
"component-class-suffix": true,
"contextual-lifecycle": true,
"directive-class-suffix": true,
"no-conflicting-lifecycle": true,
"no-host-metadata-property": true,
"no-input-rename": true,
"no-inputs-metadata-property": true,
"no-output-native": true,
"no-output-on-prefix": true,
"no-output-rename": true,
"no-outputs-metadata-property": true,
"template-banana-in-box": true,
"template-no-negated-async": true,
"use-lifecycle-interface": true,
"use-pipe-transform-interface": true,
"directive-selector": [
true,
"attribute",
"app",
"camelCase"
],
"component-selector": [
true,
"element",
"app",
"kebab-case"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

View File

@ -0,0 +1,5 @@
#!/bin/bash
echo "Preparing the app.."
docker-compose build
echo "Preparation complete."

View File

@ -0,0 +1,14 @@
#!/bin/bash
echo "Removing the app.."
echo "Removing the fruitapp-backend"
docker stop fruitapp-backend && docker rm -f fruitapp-backend
echo ""
echo "removing the fruitapp-web container"
docker stop fruitapp-frontend && docker rm -f fruitapp-frontend
echo ""
echo "removing the fruitapp-db container"
docker stop fruitapp-db && docker rm -f fruitapp-db
echo "App removed."

View File

@ -0,0 +1,7 @@
#!/bin/bash
echo "running the app"
docker-compose up -d
echo "Connecting to the database....Please wait."
sleep 10
echo "The app is available at http://localhost:4200"

View File

@ -0,0 +1,5 @@
#!/bin/bash
echo "stopping the app.."
docker-compose stop
echo "App stopped"

BIN
z3/images/homepage.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

53
z3/prepare-app.sh Normal file
View File

@ -0,0 +1,53 @@
#!/bin/sh
suhailregistry='suhailregistry1'
mysql_service='mysql-db-f39m'
app_name='suhail742'
echo "Preparing the app"
echo "Creating docker images"
docker build ./src/fruitapp-backend -t fruitapp-backend
docker build ./src/fruitapp-frontend/ -f ./src/fruitapp-frontend/.docker/dev/Dockerfile -t fruitapp-frontend
echo 'starting the app'
az group create --name suhailgroup --location westeurope
az acr create --resource-group suhailgroup --name $suhailregistry --sku basic
az acr login --name $suhailregistry
docker tag fruitapp-backend:latest $suhailregistry.azurecr.io/fruitapp-backend:latest
docker tag fruitapp-frontend:latest $suhailregistry.azurecr.io/fruitapp-frontend:latest
docker push $suhailregistry.azurecr.io/fruitapp-backend:latest
docker push $suhailregistry.azurecr.io/fruitapp-frontend:latest
az appservice plan create --name myserviceplan -g suhailgroup --sku S1 --is-linux
az webapp create -g suhailgroup -p myserviceplan --name $app_name \
--multicontainer-config-type compose --multicontainer-config-file az-docker-compose.yml
principalId=$(az webapp identity assign --resource-group suhailgroup --name $app_name --query principalId --output tsv)
subscriptionId=$(az account show --query id --output tsv)
az mysql server create -g suhailgroup --name $mysql_service --location eastus --admin-user adminuser --admin-password My5up3rStr0ngPaSw0rd! --sku-name B_Gen5_1 --version 5.7 --ssl-enforcement Disabled
myIp=$(curl -s api.ipify.org)
az mysql server firewall-rule create -g suhailgroup --server $mysql_service --name AllowMyIP --start-ip-address 0.0.0.0 --end-ip-address 0.0.0.0
az mysql server firewall-rule create -g suhailgroup --server $mysql_service --name myIP --start-ip-address $myIp --end-ip-address $myIp
mysql -h $mysql_service.mysql.database.azure.com -u adminuser@$mysql_service -pMy5up3rStr0ngPaSw0rd! -e "create database main;"
az role assignment create --assignee $principalId \
--scope /subscriptions/$subscriptionId/resourceGroups/suhailgroup/providers/Microsoft.ContainerRegistry/registries/$suhailregistry \
--role acrpull
az webapp config appsettings set --name $app_name -g suhailgroup --settings SQLALCHEMY_DATABASE_URI='mysql://adminuser@$mysql_service:My5up3rStr0ngPaSw0rd!@$mysql_service.mysql.database.azure.com/main' \
MYSQL_USER='adminuser@$mysql_service' MYSQL_PASSWORD='My5up3rStr0ngPaSw0rd!' MYSQL_ROOT_PASSWORD='My5up3rStr0ngPaSw0rd!'
az resource update --ids /subscriptions/$subscriptionId/resourceGroups/suhailgroup/providers/Microsoft.Web/sites/$app_name/config/web --set properties.acrUseManagedIdentityCreds=True
az webapp config container set --name $app_name -g suhailgroup --docker-registry-server-url https://$suhailregistry.azurecr.io
echo 'app started'

2
z3/src/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.history
.idea

104
z3/src/Readme.md Normal file
View File

@ -0,0 +1,104 @@
# Overview of the APP
The application consist of two parts, an API and a front end. The application displays 5 fruits which are seeded to the database at the startup. The get API provides the list of fruits. Anyone can like a fruit. In this case the count of like will be increased.
![homepage](images/homepage.png?raw=true "System Architecture")
# Requirements
The basic system requirements are as follows
- Any OS, preferably Linux
- Docker
- Docker compose
# Architecture
![Architecture](images/fruitapp.jpg?raw=true "System Architecture")
### Technology used
- Flask (Backend Rest API)
- Angular (Frontend)
- MySQL - as persistent database
# Docker environment
For shipping and deploying the application docker-compose is used. All the configurations are in the docker-compose.yml file.
Key points of the docker-compose.yml is given below.
## services
- **backend:** runs the Flask web API.
- **db** runs the mysql database required for the backend API
- **web** this is the front-end application
## Virtual networks
One virtual network is used
- main
## list of the containers
### fruitapp-backend
This container runs under the service name backend. The dockerfile user for this container is located at `fruitapp-backend/Dockerfile`. This container is based on python:3.9 image.
### fruitapp-frontend
The frontend container for the application. The dockerfile is located at `fruitapp-frontend/.docker/dev/Dockerfile`. Base image used to build this container is `Node:14`.
### db container
The container is built using the official `mysql:5.7.22` image pulled from dockerhub.
## Preparation
To prepare the environment for the first time run the following command
```shell
bash prepare-app.sh
```
## Running
Run the app background
```shell
bash start-app.sh
```
see the logs of backend
```
docker-compose logs -f backend
```
see the logs of forntend
```
docker-compose logs -f web
```
see the logs of database
```
docker-compose logs -f db
```
## Stopping
```shell
bash stop-app.sh
```
## Removing containers
```shell
bash remove-app.sh
```
# Viewing the app
After running the app. visit [http://localhost:4200](http://localhost:4200) to view the app

56
z3/src/docker-compose.yml Normal file
View File

@ -0,0 +1,56 @@
version: '3.8'
services:
backend:
image: fruitapp-backend:latest
container_name: fruitapp-backend
command: 'bash scripts/entrypoint.sh'
ports:
- '8001:5000'
depends_on:
- db
restart: unless-stopped
networks:
- main
env:
- name: SQLALCHEMY_DATABASE_URI
value: mysql://user:password@mysql-service/main
- name: FLASK_APP
value: app.py
- name: MYSQL_DATABASE
value: main
- name: MYSQL_USER
value: user
- name: MYSQL_PASSWORD
value: password
- name: MYSQL_ROOT_PASSWORD
value: root
db:
image: mysql:5.7.38
container_name: fruitapp-db
restart: unless-stopped
volumes:
- msql-data:/var/lib/mysql
ports:
- '33067:3306'
networks:
- main
env:
- name: MYSQL_DATABASE
value: 'main'
- name: MYSQL_USER
value: 'user'
- name: MYSQL_PASSWORD
value: 'password'
- name: MYSQL_ROOT_PASSWORD
value: 'root1'
web:
image: fruitapp-frontend:latest
container_name: fruitapp-frontend
ports:
- 80:4200
depends_on:
- db
- backend
networks:
- main

View File

@ -0,0 +1,8 @@
MYSQL_DATABASE=main
MYSQL_USER=root
MYSQL_PASSWORD=root
MYSQL_ROOT_PASSWORD=root
SQLALCHEMY_DATABASE_URI=mysql://root:root@fruitapp-backend/main
FLASK_APP=app.py

View File

@ -0,0 +1,6 @@
MYSQL_DATABASE=main
MYSQL_USER=root
MYSQL_PASSWORD=root
MYSQL_ROOT_PASSWORD=root
SQLALCHEMY_DATABASE_URI=mysql://root:root@test/main

23
z3/src/fruitapp-backend/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
.DS_Store
.flaskenv
*.pyc
*.pyo
venv/
.venv/
dist/
build/
*.egg
*.egg-info/
_mailinglist
.tox/
.cache/
.pytest_cache/
.idea/
docs/_build/
.vscode
# Coverage reports
htmlcov/
.coverage
.coverage.*
*,cover

View File

@ -0,0 +1,16 @@
FROM python:3-alpine
ENV PYTHONUNBUFFERED 1
WORKDIR /app
COPY requirements.txt /app/requirements.txt
RUN \
apk add --no-cache mariadb-dev && \
apk add --no-cache --virtual .build-deps gcc musl-dev && \
python3 -m pip install -r requirements.txt --no-cache-dir && \
apk --purge del .build-deps
COPY ./scripts/entrypoint.sh /app/scripts/entrypoint.sh
COPY . /app
CMD sh /app/scripts/entrypoint.sh
EXPOSE 5000

View File

@ -0,0 +1,2 @@
from dotenv import load_dotenv
load_dotenv()

View File

@ -0,0 +1,63 @@
import os
from dataclasses import dataclass
from flask import Flask, jsonify, abort
from flask_cors import CORS
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
CORS(app)
SQLALCHEMY_DATABASE_URI = os.getenv('SQLALCHEMY_DATABASE_URI')
app.config["SQLALCHEMY_DATABASE_URI"] = SQLALCHEMY_DATABASE_URI
# app.config["SQLALCHEMY_ENGINE_OPTIONS"] = dict(
# connect_args=dict(
# sslmode='require'
# )
# )
app.config.update({
'SQLALCHEMY_POOL_SIZE': None,
'SQLALCHEMY_POOL_TIMEOUT': None
})
db = SQLAlchemy(app)
@dataclass
class Product(db.Model):
id: int
title: str
image: str
likes: int
id = db.Column(db.Integer, primary_key=True, autoincrement=False)
title = db.Column(db.String(200))
image = db.Column(db.TEXT(20000000))
likes = db.Column(db.Integer, default=0)
@dataclass
class ProductUser(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer)
product_id = db.Column(db.Integer)
@app.route('/api/products')
def index():
return jsonify(Product.query.all())
@app.route('/api/products/<int:id>/like', methods=['POST'])
def like(id):
# define a static user id for demo
try:
product = Product.query.get(id)
product.likes += 1
db.session.commit()
except:
abort(400, 'bad request')
return jsonify(product)
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')

View File

@ -0,0 +1,12 @@
from flask_migrate import Migrate, MigrateCommand
from flask_script import Manager
from main import app, db
migrate = Migrate(app, db, compare_type=True)
manager = Manager(app)
manager.add_command('db', MigrateCommand)
if __name__ == '__main__':
manager.run()

View File

@ -0,0 +1 @@
Generic single-database configuration.

View File

@ -0,0 +1,45 @@
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

View File

@ -0,0 +1,96 @@
from __future__ import with_statement
import logging
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
from flask import current_app
config.set_main_option(
'sqlalchemy.url',
str(current_app.extensions['migrate'].db.engine.url).replace('%', '%%'))
target_metadata = current_app.extensions['migrate'].db.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=target_metadata, literal_binds=True
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix='sqlalchemy.',
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
process_revision_directives=process_revision_directives,
**current_app.extensions['migrate'].configure_args
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View File

@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,40 @@
"""empty message
Revision ID: 843c810aec1f
Revises:
Create Date: 2020-12-08 08:40:37.736465
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '843c810aec1f'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('product',
sa.Column('id', sa.Integer(), autoincrement=False, nullable=False),
sa.Column('title', sa.String(length=200), nullable=True),
sa.Column('image', sa.String(length=200), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('product_user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('product_id', sa.Integer(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('product_user')
op.drop_table('product')
# ### end Alembic commands ###

View File

@ -0,0 +1,34 @@
"""empty message
Revision ID: 85749c0c4589
Revises: b3ff59df2833
Create Date: 2022-04-08 07:46:57.160792
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '85749c0c4589'
down_revision = 'b3ff59df2833'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('product', 'image',
existing_type=mysql.VARCHAR(length=20000),
type_=sa.TEXT(length=20000000),
existing_nullable=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('product', 'image',
existing_type=sa.TEXT(length=20000000),
type_=mysql.VARCHAR(length=20000),
existing_nullable=True)
# ### end Alembic commands ###

View File

@ -0,0 +1,34 @@
"""empty message
Revision ID: b3ff59df2833
Revises: fee4d1b1d192
Create Date: 2022-04-08 07:33:52.082355
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = 'b3ff59df2833'
down_revision = 'fee4d1b1d192'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('product', 'image',
existing_type=mysql.VARCHAR(length=200),
type_=sa.String(length=20000),
existing_nullable=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('product', 'image',
existing_type=sa.String(length=20000),
type_=mysql.VARCHAR(length=200),
existing_nullable=True)
# ### end Alembic commands ###

View File

@ -0,0 +1,28 @@
"""empty message
Revision ID: fee4d1b1d192
Revises: 843c810aec1f
Create Date: 2022-04-08 07:05:04.647143
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'fee4d1b1d192'
down_revision = '843c810aec1f'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('product', sa.Column('likes', sa.Integer(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('product', 'likes')
# ### end Alembic commands ###

View File

@ -0,0 +1,27 @@
import json
import os
import pika
RABBIT_ENDPOINT = os.environ['RABBIT_ENDPOINT']
class Producer:
"""Implement the producer logic."""
def __init__(self):
self.params = pika.URLParameters(RABBIT_ENDPOINT)
self.connection = pika.BlockingConnection(self.params)
def publish(self, method, body):
properties = pika.BasicProperties(method)
if not self.connection or self.connection.is_closed:
self.connection = pika.BlockingConnection(self.params)
channel = self.connection.channel()
channel.basic_publish(
exchange='',
routing_key='admin',
body=json.dumps(body),
properties=properties,
)
print('Product published in queue: MAIN')

View File

@ -0,0 +1,13 @@
Flask==1.1.2
werkzeug==1.0.1
Flask-SQLAlchemy==2.4.4
SQLAlchemy==1.3.20
Flask-Migrate==2.5.3
Flask-Script==2.0.6
Flask-Cors==3.0.9
requests==2.25.0
mysqlclient==2.0.1
pika==1.1.0
jinja2<3.1.0
itsdangerous==2.0.1
python-dotenv==0.20.0

View File

@ -0,0 +1,5 @@
#!/bin/bash
python manage.py db upgrade
echo "DB migration done."
python seeder.py
python main.py

View File

@ -0,0 +1,40 @@
from main import Product, db
def insert_data(data):
product = Product.query.get(data['id'])
if not product:
product = Product(id=data['id'], title=data['title'], image=data['image'])
db.session.add(product)
db.session.commit()
print('inserted ', data['title'])
else:
print('Product already exists.')
PRODUCTS = [
{
'id': 1,
'title': 'Apple',
'image': 'https://media.istockphoto.com/photos/red-apple-picture-id184276818?k=20&m=184276818&s=612x612&w=0&h=QxOcueqAUVTdiJ7DVoCu-BkNCIuwliPEgtAQhgvBA_g='
},
{
'id': 2,
'title': 'Orange',
'image': 'https://media.istockphoto.com/photos/tiny-tangerine-picture-id89951356?k=20&m=89951356&s=170667a&w=0&h=9oW2EBTASfaBol8qYVYB8YNYcSoNe1AImG4wqdgErEs='
},
{
'id': 3,
'title': 'Banana',
'image': 'https://media.istockphoto.com/photos/banana-picture-id1184345169?k=20&m=1184345169&s=170667a&w=0&h=t22KqOZ9EEwyRj7i35uxY-Xf6P_gAgLejd-SeReTnPY='
},
{
'id': 4,
'title': 'Avocado',
'image': 'https://media.istockphoto.com/photos/half-of-fresh-ripe-avocado-isolated-on-white-background-picture-id1278032327?b=1&k=20&m=1278032327&s=170667a&w=0&h=7eMAI9S_YBiVaDN49zk5gUZEhhA1oGDIWh3Tdst6q4Y='
}
]
for p in PRODUCTS:
insert_data(p)

View File

@ -0,0 +1,17 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.

View File

@ -0,0 +1,45 @@
#FROM node:14
#RUN npm install @angular/cli@11.0.0 -g
# Create and define the node_modules's cache directory.
#WORKDIR /cache
#COPY package.json .
#COPY package-lock.json .
#RUN npm install
# define the application's working directory.
#WORKDIR /app
#COPY . .
#ENV PATH /app/node_modules/.bin:$PATH
# Stage 1: Compile and Build angular codebase
# Use official node image as the base image
FROM node:latest as build
# Set the working directory
WORKDIR /usr/local/app
ENV NODE_OPTIONS=--openssl-legacy-provider
# Add the source code to app
COPY ./ /usr/local/app/
# Install all the dependencies
RUN npm install --legacy-peer-deps
# Generate the build of the application
RUN npm run build
# Stage 2: Serve app with nginx server
# Use official nginx image as the base image
FROM nginx:latest
# Copy the build output to replace the default nginx contents.
COPY --from=build /usr/local/app/dist/dockerize-angular /usr/share/nginx/html
COPY .docker/dev/default.conf /etc/nginx/conf.d/
# Expose port 80
EXPOSE 80

View File

@ -0,0 +1,14 @@
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri /index.html;
}
location /api/ {
# The following statement will proxy traffic to the upstream named Backend
proxy_pass http://backend-service:5000;
}
}

View File

@ -0,0 +1,9 @@
node_modules
npm-debug.log
.dockerignore
.git
.gitignore
README.md
LICENSE
.vscode
.history

View File

@ -0,0 +1,16 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

46
z3/src/fruitapp-frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,46 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out
# dependencies
/node_modules
# profiling files
chrome-profiler-events*.json
speed-measure-plugin*.json
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db

Some files were not shown because too many files have changed in this diff Show More