Compare commits

..

23 Commits

Author SHA1 Message Date
c8fecc52d6 Update docs 2024-01-26 11:48:35 +01:00
e97cbc6ad2 Update docs 2024-01-26 11:27:24 +01:00
9fbca2677b Update readme 2024-01-26 11:05:20 +01:00
6daa4c284d Update readme 2024-01-26 11:01:19 +01:00
1c1eaa7ee7 Update docs 2024-01-26 10:50:45 +01:00
96501f712c Update diagram 2024-01-25 17:51:32 +01:00
ce52b5aff5 Update diagram 2024-01-25 17:48:44 +01:00
b9ad5fd922 Update diagrams 2024-01-23 23:04:23 +01:00
f6b1a7eede Merge branch 'product-details' 2024-01-23 23:01:14 +01:00
c1b2a8cbdd Merge branch 'main' into product-details 2024-01-23 23:00:53 +01:00
aa22b48746 Frontend env arguments 2024-01-23 23:00:03 +01:00
ea7c54a5dc Product detail modal 2024-01-23 23:00:03 +01:00
0a344ed1ce DC in local network 2024-01-23 22:59:30 +01:00
3d6e4e6f34 Move apps to different ports 2024-01-23 22:59:02 +01:00
2cdbebdfa1 Frontend env arguments 2024-01-23 22:55:21 +01:00
5db0026aa7 Product detail modal 2024-01-23 22:32:11 +01:00
7b213dd33a DC in local network 2024-01-23 21:40:57 +01:00
b69f26027d Move apps to different ports 2024-01-23 20:14:01 +01:00
95b0c5f1ad FE tweaks 2024-01-18 13:17:45 +01:00
4dc99b740b Merge branch 'networking' 2024-01-16 22:47:04 +01:00
248364a686 Update diagrams 2024-01-16 22:46:43 +01:00
cd57e11404 Isolate networks 2024-01-16 21:37:32 +01:00
99ea80c93c Remove debug print 2024-01-16 17:11:03 +01:00
28 changed files with 6877 additions and 483 deletions

View File

@ -1 +1,83 @@
# Komponiranje
## How to run
Clone repository
```
git clone https://gitea.ekirin.com/Intis/prezentacija-komponiranje.git
cd prezentacija-komponiranje
```
Start docker compose with all containers:
```
docker compose up -d
```
Wait until docker images are built and containers starts.
Browse to [Local frontend application on port 8080](http://localhost:8080)
List running containers:
```
docker compose ps
```
Attach to docker compose output:
```
docker compose logs -f
```
Stop running containers:
```
docker compose down
```
### Run on host network
Start docker compose:
```
docker compose -f docker-compose-local.yml up -d
```
Containers will listen to the following local ports:
| Service | Port |
| ------------- | ----- |
| FE / nginx | 80 |
| Envoy proxy | 10000 |
| Machines app | 4000 |
| Products app | 4001 |
| Database | 55432 |
Browse to [Local frontend application on port 80](http://localhost:80)
Stop running containers:
```
docker compose -f docker-compose-local.yml down
```
## Media
### Life without Docker Compose
![Life without Docker Compose](media/life-without-docker-compose.png)
### Containers Architecture
![Containers Architecture](media/containers-architecture.png)
### Networking - docker-compose.yml
![Networking - docker-compose.yml](media/networking.png)
### Networking - host network - docker-compose-local.yml
![Networking - host network - docker-compose-local.yml](media/networking-host-network.png)
### DCCT - Docker Compose Cloud Tester
- [DCCT repository](https://gitlab.televendcloud.com/cloud/dc-cloud-tester)
![DCCT - Docker Compose Cloud Tester](media/dcct.png)

66
docker-compose-local.yml Normal file
View File

@ -0,0 +1,66 @@
version: "3.8"
services:
db:
build:
context: ./database
dockerfile: Dockerfile
network_mode: "host"
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
command: -p 55432
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -p 55432"]
interval: 1s
timeout: 5s
retries: 10
machines-app:
build:
context: ./machines
dockerfile: Dockerfile
network_mode: "host"
environment:
- APPPORT=4000
- DBHOST=localhost
- DBPORT=55432
- DBNAME=komponiranje
- DBUSER=pero
- DBPASSWORD=pero.000
- PRODUCTSAPPURL=http://localhost:10000
depends_on:
db:
condition: service_healthy
products-app:
build:
context: ./products
dockerfile: Dockerfile
network_mode: "host"
environment:
- APPPORT=4001
- DBHOST=localhost
- DBPORT=55432
- DBNAME=komponiranje
- DBUSER=pero
- DBPASSWORD=pero.000
depends_on:
db:
condition: service_healthy
proxy:
image: envoyproxy/envoy:v1.28-latest
network_mode: "host"
volumes:
- ./proxy/envoy-local.yaml:/etc/envoy/envoy.yaml
depends_on:
- machines-app
- products-app
frontend-app:
build:
context: ./frontend
dockerfile: Dockerfile
args:
- REACT_APP_MACHINES_API_URL=http://localhost:10000
- REACT_APP_PRODUCTS_API_URL=http://localhost:10000
network_mode: "host"
depends_on:
- proxy

View File

@ -5,8 +5,8 @@ services:
build:
context: ./database
dockerfile: Dockerfile
ports:
- 55432:5432
networks:
- backend-net
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
@ -19,8 +19,10 @@ services:
build:
context: ./machines
dockerfile: Dockerfile
networks:
- backend-net
environment:
- APPPORT=3000
- APPPORT=4000
- DBHOST=db
- DBPORT=5432
- DBNAME=komponiranje
@ -37,8 +39,10 @@ services:
deploy:
mode: replicated
replicas: 2
networks:
- backend-net
environment:
- APPPORT=3000
- APPPORT=4001
- DBHOST=db
- DBPORT=5432
- DBNAME=komponiranje
@ -49,6 +53,9 @@ services:
condition: service_healthy
proxy:
image: envoyproxy/envoy:v1.28-latest
networks:
- frontend-net
- backend-net
ports:
- "10000:10000"
volumes:
@ -60,9 +67,20 @@ services:
build:
context: ./frontend
dockerfile: Dockerfile
environment:
- REACT_APP_BACKEND_API_URL="http://localhost:10000"
args:
- REACT_APP_MACHINES_API_URL=http://localhost:10000
- REACT_APP_PRODUCTS_API_URL=http://localhost:10000
networks:
- frontend-net
ports:
- "8080:80"
depends_on:
- proxy
networks:
frontend-net:
name: frontend-net
internal: false
backend-net:
name: backend-net
internal: true

View File

@ -3,6 +3,12 @@ FROM node:21 as node-builder
WORKDIR /node-builder
ARG REACT_APP_MACHINES_API_URL
ARG REACT_APP_PRODUCTS_API_URL
ENV REACT_APP_MACHINES_API_URL $REACT_APP_MACHINES_API_URL
ENV REACT_APP_PRODUCTS_API_URL $REACT_APP_PRODUCTS_API_URL
COPY ./package.json .
COPY ./package-lock.json .
COPY ./public ./public
@ -17,4 +23,3 @@ RUN \
FROM nginx:1.25-alpine
COPY --from=node-builder /node-builder/build/. /usr/share/nginx/html
RUN ls -alF /usr/share/nginx/html

View File

@ -1,43 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
<meta name="description" content="Komponiranje frontend demo" />
<title>Komponiranje frontend demo</title>
</head>
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
</body>
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@ -1,25 +1,8 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
"short_name": "React App",
"name": "Create React App Sample",
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -1,5 +1,4 @@
import axios from "axios";
import { API_URL } from "../const";
export class ApiBase {}
@ -12,7 +11,6 @@ const commonHeaders = {
};
export const axiosUnauthorizedInstance = axios.create({
baseURL: API_URL,
timeout: 5000,
headers: commonHeaders,
});
@ -25,7 +23,6 @@ axiosUnauthorizedInstance.interceptors.response.use(
);
export const axiosInstance = axios.create({
baseURL: API_URL,
timeout: 5000,
headers: {
...commonHeaders,

View File

@ -1 +1,2 @@
export { machinesApi } from "./machines";
export { productsApi } from "./products";

View File

@ -1,14 +1,17 @@
import { axiosInstance, ApiBase } from "./common";
import { MACHINES_API_URL } from "../const";
const baseUrl = `${MACHINES_API_URL}/machines`;
class MachinesApi extends ApiBase {
list = async () => {
return axiosInstance.get(`/machines`, {});
return axiosInstance.get(`${baseUrl}`, {});
};
get = async (machineId) => {
return axiosInstance.get(`/machines/${machineId}`, {});
return axiosInstance.get(`${baseUrl}/${machineId}`, {});
};
listProducts = async (machineId) => {
return axiosInstance.get(`/machines/${machineId}/products`, {});
return axiosInstance.get(`${baseUrl}/${machineId}/products`, {});
};
}

View File

@ -0,0 +1,12 @@
import { axiosInstance, ApiBase } from "./common";
import { PRODUCTS_API_URL } from "../const";
const baseUrl = `${PRODUCTS_API_URL}/products`;
class ProductsApi extends ApiBase {
get = async (productId) => {
return axiosInstance.get(`${baseUrl}/${productId}`, {});
};
}
export const productsApi = new ProductsApi();

View File

@ -6,20 +6,21 @@ import Typography from "@mui/material/Typography";
import { CardActionArea } from "@mui/material";
import { PRODUCT_IMAGE_DIR } from "../const";
function ProductCard({ product }) {
function ProductCard({ product, onClick }) {
const productImg = `${PRODUCT_IMAGE_DIR}/${product.image}`;
return (
<Card>
<CardActionArea>
<CardMedia component="img" height="140" image={productImg} alt={product.name} />
<Card sx={{ width: "100%" }}>
<CardActionArea
onClick={() => {
onClick(product.id);
}}
>
<CardMedia component="img" height="200" image={productImg} alt={product.name} />
<CardContent>
<Typography gutterBottom variant="h5" component="div">
<Typography gutterBottom variant="h5" component="div" sx={{ marginBottom: 0 }}>
{product.name}
</Typography>
<Typography variant="body2" color="text.secondary">
{product.description}
</Typography>
</CardContent>
</CardActionArea>
</Card>

View File

@ -0,0 +1,36 @@
import * as React from "react";
import Button from "@mui/material/Button";
import Dialog from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@mui/material/DialogContentText";
import DialogTitle from "@mui/material/DialogTitle";
import CardMedia from "@mui/material/CardMedia";
import { PRODUCT_IMAGE_DIR } from "../const";
function ProductModal({ product, onClose }) {
const productImg = `${PRODUCT_IMAGE_DIR}/${product.image}`;
return (
<Dialog open={true} onClose={onClose}>
<DialogTitle>{product.name}</DialogTitle>
<DialogContent>
<CardMedia
component="img"
height="300"
image={productImg}
alt={product.name}
sx={{ marginBottom: "1rem" }}
/>
<DialogContentText>{product.description}</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onClose} variant="contained" autoFocus>
Close
</Button>
</DialogActions>
</Dialog>
);
}
export { ProductModal };

View File

@ -2,12 +2,32 @@ import * as React from "react";
import Grid from "@mui/material/Unstable_Grid2";
import Typography from "@mui/material/Typography";
import { ProductCard } from "./ProductCard";
import { ProductModal } from "./ProductModal";
import { productsApi } from "../api";
function Products({ machineName, products, onSelect }) {
const [productModal, setProductModal] = React.useState({ isOpen: false, product: null });
const onProductSelect = (productId) => {
productsApi.get(productId).then((response) => {
setProductModal({
isOpen: true,
product: response.data,
});
});
};
const onProductModalClose = () => {
setProductModal({
isOpen: false,
productId: null,
});
};
const productItems = products.map((product) => {
return (
<Grid md={6} key={product.id} sx={{ display: "flex" }}>
<ProductCard product={product} />
<ProductCard product={product} onClick={onProductSelect} />
</Grid>
);
});
@ -21,6 +41,8 @@ function Products({ machineName, products, onSelect }) {
<Grid container spacing={2}>
{productItems}
</Grid>
{productModal.isOpen && <ProductModal product={productModal.product} onClose={onProductModalClose} />}
</>
);
}

View File

@ -1,2 +1,3 @@
export const API_URL = process.env.REACT_APP_BACKEND_API_URL || "http://localhost:10000";
export const MACHINES_API_URL = process.env.REACT_APP_MACHINES_API_URL || "http://localhost:4000";
export const PRODUCTS_API_URL = process.env.REACT_APP_PRODUCTS_API_URL || "http://localhost:4001";
export const PRODUCT_IMAGE_DIR = "/static/products/";

View File

@ -18,9 +18,7 @@ function Home() {
}, []);
const onMachineSelect = (machineName, machineId) => {
console.log("selected:", machineName);
machinesApi.listProducts(machineId).then((response) => {
console.log(response.data.products);
setProductsData({
machineName,
products: response.data.products,

View File

@ -26,8 +26,7 @@ docker-build: clean
docker-run:
@docker run \
--name $(CONTAINER_NAME) \
--publish 3000:3000 \
--env CONTAINER_NAME="Awesome API server" \
--publish 4000:4000 \
--env DBPORT=55432 \
--detach \
$(IMAGE_NAME)

View File

@ -6,13 +6,13 @@ import (
type configStruct struct {
AppHost string `default:"0.0.0.0"`
AppPort int `default:"3000"`
AppPort int `default:"4000"`
DbHost string `default:"localhost"`
DbPort int `default:"55432"`
DbName string `default:"komponiranje"`
DbUser string `default:"pero"`
DbPassword string `default:"pero.000"`
ProductsAppUrl string `default:"http://localhost:3001"`
ProductsAppUrl string `default:"http://localhost:4001"`
}
const ENV_PREFIX = ""

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
media/dcct.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 688 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

BIN
media/networking.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 KiB

View File

@ -26,8 +26,7 @@ docker-build: clean
docker-run:
@docker run \
--name $(CONTAINER_NAME) \
--publish 3000:3000 \
--env CONTAINER_NAME="Awesome API server" \
--publish 4001:4001 \
--env DBPORT=55432 \
--detach \
$(IMAGE_NAME)

View File

@ -39,6 +39,10 @@ func handleGetProducts(dbConn *gorm.DB) gin.HandlerFunc {
products := db.GetProducts(dbConn, machineId)
// for i := 0; i < len(*products); i++ {
// (*products)[i].Name = fmt.Sprintf("[local] %s", (*products)[i].Name)
// }
c.JSON(
http.StatusOK,
GetProductsResponse{

View File

@ -6,7 +6,7 @@ import (
type configStruct struct {
AppHost string `default:"0.0.0.0"`
AppPort int `default:"3001"`
AppPort int `default:"4001"`
DbHost string `default:"localhost"`
DbPort int `default:"55432"`
DbName string `default:"komponiranje"`

59
proxy/envoy-local.yaml Normal file
View File

@ -0,0 +1,59 @@
static_resources:
listeners:
- address:
socket_address:
address: 127.0.0.1
port_value: 10000
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
codec_type: auto
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: backend
domains:
- "*"
routes:
- match:
prefix: "/machines"
route:
cluster: machines-app
- match:
prefix: "/products"
route:
cluster: products-app
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: machines-app
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
load_assignment:
cluster_name: machines-app
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 127.0.0.1
port_value: 4000
- name: products-app
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
load_assignment:
cluster_name: products-app
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 127.0.0.1
port_value: 4001

View File

@ -43,7 +43,7 @@ static_resources:
address:
socket_address:
address: machines-app
port_value: 3000
port_value: 4000
- name: products-app
connect_timeout: 0.25s
type: strict_dns
@ -56,10 +56,4 @@ static_resources:
address:
socket_address:
address: products-app
port_value: 3000
admin:
access_log_path: "/dev/null"
address:
socket_address:
address: 0.0.0.0
port_value: 800
port_value: 4001