second exercice

This commit is contained in:
suhail 2022-05-01 20:50:28 +05:30
parent f4b832c197
commit 2c89566c69
78 changed files with 32924 additions and 0 deletions

121
z2/README.md.txt Normal file
View File

@ -0,0 +1,121 @@
# 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')
## Requirements
The basic system requirements are as follows
- Any OS, preferably Linux
- Docker
- minikube
### Technology used
- Flask (Backend Rest API)
- Angular (Frontend)
- MySQL - as persistent database
- kubernetes
- Docker
## Kubernetes environment
For shipping and deploying the application kubernetes(minikube) is used.
Key points of the kubernetes objects is given below.
### Namespace
- **Namespace**: We used namespace to group our project resources together and isolate theme.
### Deployment
- **backend:** Deployment used to run the backend image
- **front** Deployment used to run the frontend image
### Statefulset:
Mysql database is deployed as a statefulset kuebernetes object. StatefulSet is used to manage stateful applications with persistent storage. Storage stays associated with replacement pods. Volumes persist when pods are deleted.. You can find more information about statefulset [here](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/)
### Services
The kubernetes services are used to expose our application as a network service.
- **backend-service:** service used to communicate with the backend APO.
- **mysql-service** runs the mysql database service required for the backend API
- **front-service** this is the entrypoint for the front-end application
### ConfigMap:
A ConfigMap is used to store nginx configuration file. The front pod will consume the ConfigMaps as the default configuration files for nginx web server. This ConfigMap contains the rules for mapping the `/api` request to the backend service.
### PersistentVolumeClaim:
We used a persitent volume claim to bind the data volume (persistent volume) and the Pod (mysql pod).
### PersistentVolume:
We used a persitent volume in order to save the database data in a persistent volume which is independent of the lifecycle of the Pods. It means that data represented by a PV continue to exist as the cluster changes and as Pods are deleted and recreated.
## A description of the container configuration performed.
The most important part is the connection to between database and the backend and between the frontend and the backend. The credentials for mysql in the statefulset.yml file should match the ones in the deployment.yml file for the backend deployment.
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.
### Running minikube:
First we should run the minikube and expose the dashboard
```
minikube start
minikube dashboard
```
### Prepare the app
As the local docker registry is not reachable for minikube, we shoud proxy the docker daemon to the minikube registry before building our images, for this we have to run the following command.
```
eval $(minikube docker-env)
```
Now to prepare the application you have to run the command :
```
sh prepare-app.sh
```
The script will build the frontend and the backend images.
### Run the app
To run the app you have to run:
```
sh start-app.sh
```
the script which will create a namespace and all the required kubernetes object of the application.
### To stop the app
The `stop-app.sh` script will remove the namespace and its resources also it will remove the persistent volume we created in the run script.
## How to view the application on the web.
To expose the application service outside the kubernetes we used the `kubectl port-forwading` to forward the front end service localy .
```
POD_NAME=$(kubectl get pods --namespace suhailahamed -l "app=fruitapp-frontend" -o jsonpath="{.items[0].metadata.name}")
kubectl port-forward $POD_NAME 8000:80 --namespace suhailahamed
```
Now the application is accessible through: `http://localhost:8000`

113
z2/deployment.yml Normal file
View File

@ -0,0 +1,113 @@
#
# Backend deployment
#
apiVersion: apps/v1
kind: Deployment
metadata:
name: fruitapp-backend-deployment
namespace: suhailahamed
labels:
app: fruitapp-backend
spec:
replicas: 1
selector:
matchLabels:
app: fruitapp-backend
template:
metadata:
labels:
app: fruitapp-backend
spec:
initContainers:
- name: init-cont
image: busybox:1.31
command: ['sh', '-c',
'echo -e "Checking for the availability of MySQL Server deployment"; while ! nc -z mysql-service 3306; do sleep 1; printf "-"; done; echo -e " >> MySQL DB Server has started";']
containers:
- name: fruitapp-backend
image: fruitapp-backend:latest
imagePullPolicy: IfNotPresent
command: ['bash']
args: ['scripts/entrypoint.sh']
ports:
- containerPort: 5000
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
---
#
# Frontend deployment
#
apiVersion: apps/v1
kind: Deployment
metadata:
name: fruitapp-frontend-deployment
namespace: suhailahamed
labels:
app: fruitapp-frontend
spec:
replicas: 1
selector:
matchLabels:
app: fruitapp-frontend
template:
metadata:
labels:
app: fruitapp-frontend
spec:
containers:
- name: fruitapp-frontend
image: fruitapp-frontend:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
volumeMounts:
- mountPath: /etc/nginx/conf.d/ # mount nginx-conf volumn to /etc/nginx
readOnly: true
name: nginx-conf
volumes:
- name: nginx-conf
configMap:
name: nginx-conf # place ConfigMap `nginx-conf` on /etc/nginx
items:
- key: default.conf
path: default.conf
---
##
## ConfigMap for nginx
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-conf
namespace: suhailahamed
data:
default.conf: |
upstream Backend {
# hello is the internal DNS name used by the backend Service inside Kubernetes
server backend-service:5000;
}
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;
}
}

4
z2/namespace.yaml Normal file
View File

@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: suhailahamed

5
z2/prepare-app.sh Normal file
View File

@ -0,0 +1,5 @@
#!/bin/sh
echo "Preparing the app.."
docker build ./src/fruitapp-backend -t fruitapp-backend
docker build ./src/fruitapp-frontend/ -f ./src/fruitapp-frontend/.docker/dev/Dockerfile -t fruitapp-frontend
echo "Preparation complete."

27
z2/service.yml Normal file
View File

@ -0,0 +1,27 @@
# Backend service
apiVersion: v1
kind: Service
metadata:
name: backend-service
namespace: suhailahamed
spec:
selector:
app: fruitapp-backend
ports:
- protocol: TCP
port: 5000
targetPort: 5000
---
# Frontend service
apiVersion: v1
kind: Service
metadata:
name: frontend-service
namespace: suhailahamed
spec:
selector:
app: fruitapp-frontend
ports:
- protocol: TCP
port: 80
targetPort: 80

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

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

104
z2/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

55
z2/src/docker-compose.yml Normal file
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.38
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@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
z2/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,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,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,28 @@
# 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
# Expose port 80
EXPOSE 80

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
z2/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

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
});
};

30732
z2/src/fruitapp-frontend/package-lock.json generated Normal file

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: ""
};
/*
* 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"
]
}
}

BIN
z2/src/images/fruitapp.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
z2/src/images/homepage.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

5
z2/src/prepare-app.sh Normal file
View File

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

14
z2/src/remove-app.sh Normal file
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."

7
z2/src/start-app.sh Normal file
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"

5
z2/src/stop-app.sh Normal file
View File

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

9
z2/start-app.sh Normal file
View File

@ -0,0 +1,9 @@
#!/bin/sh
echo 'starting the app'
kubectl apply -f namespace.yaml
kubectl apply -f statefulset.yml
kubectl apply -f statefulset-service.yml
kubectl create -f deployment.yml
kubectl create -f service.yml
kubectl create -f ingress.yml
echo 'app started'

View File

@ -0,0 +1,15 @@
# MySQL StatefulSet Service
apiVersion: v1
kind: Service
metadata:
name: mysql-service
namespace: suhailahamed
spec:
selector:
app: mysql
type: ClusterIP
clusterIP: None
ports:
- port: 3306
targetPort: mysql
protocol: TCP

63
z2/statefulset.yml Normal file
View File

@ -0,0 +1,63 @@
# PostgreSQL StatefulSet
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
namespace: suhailahamed
spec:
serviceName: mysql-service
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
imagePullPolicy: 'IfNotPresent'
volumeMounts:
- name: data
mountPath: /var/lib/mysql
env:
- name: MYSQL_DATABASE
value: 'main'
- name: MYSQL_USER
value: 'user'
- name: MYSQL_PASSWORD
value: 'password'
- name: MYSQL_ROOT_PASSWORD
value: 'root1'
ports:
- name: mysql
containerPort: 3306
protocol: TCP
volumeClaimTemplates:
- metadata:
name: data
namespace: suhailahamed
spec:
storageClassName: ''
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-volume
labels:
type: local
spec:
storageClassName: ''
capacity:
storage: 5Gi
accessModes: ['ReadWriteOnce']
hostPath:
path: '/mnt/pg'

4
z2/stop-app.sh Normal file
View File

@ -0,0 +1,4 @@
echo 'Deleting namespace'
kubectl delete namespaces suhailahamed
echo 'Deleting persistent volume'
kubectl delete pv pv-volume