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 # 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: build:
context: ./database context: ./database
dockerfile: Dockerfile dockerfile: Dockerfile
ports: networks:
- 55432:5432 - backend-net
environment: environment:
- POSTGRES_USER=postgres - POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres - POSTGRES_PASSWORD=postgres
@ -19,8 +19,10 @@ services:
build: build:
context: ./machines context: ./machines
dockerfile: Dockerfile dockerfile: Dockerfile
networks:
- backend-net
environment: environment:
- APPPORT=3000 - APPPORT=4000
- DBHOST=db - DBHOST=db
- DBPORT=5432 - DBPORT=5432
- DBNAME=komponiranje - DBNAME=komponiranje
@ -37,8 +39,10 @@ services:
deploy: deploy:
mode: replicated mode: replicated
replicas: 2 replicas: 2
networks:
- backend-net
environment: environment:
- APPPORT=3000 - APPPORT=4001
- DBHOST=db - DBHOST=db
- DBPORT=5432 - DBPORT=5432
- DBNAME=komponiranje - DBNAME=komponiranje
@ -49,6 +53,9 @@ services:
condition: service_healthy condition: service_healthy
proxy: proxy:
image: envoyproxy/envoy:v1.28-latest image: envoyproxy/envoy:v1.28-latest
networks:
- frontend-net
- backend-net
ports: ports:
- "10000:10000" - "10000:10000"
volumes: volumes:
@ -60,9 +67,20 @@ services:
build: build:
context: ./frontend context: ./frontend
dockerfile: Dockerfile dockerfile: Dockerfile
environment: args:
- REACT_APP_BACKEND_API_URL="http://localhost:10000" - REACT_APP_MACHINES_API_URL=http://localhost:10000
- REACT_APP_PRODUCTS_API_URL=http://localhost:10000
networks:
- frontend-net
ports: ports:
- "8080:80" - "8080:80"
depends_on: depends_on:
- proxy - 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 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.json .
COPY ./package-lock.json . COPY ./package-lock.json .
COPY ./public ./public COPY ./public ./public
@ -17,4 +23,3 @@ RUN \
FROM nginx:1.25-alpine FROM nginx:1.25-alpine
COPY --from=node-builder /node-builder/build/. /usr/share/nginx/html 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> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta name="description" content="Komponiranje frontend demo" />
name="description" <title>Komponiranje frontend demo</title>
content="Web site created using create-react-app" </head>
/>
<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.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will <body>
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>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root"></div>
<!-- </body>
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
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> </html>

View File

@ -1,25 +1,8 @@
{ {
"short_name": "React App", "short_name": "React App",
"name": "Create React App Sample", "name": "Create React App Sample",
"icons": [ "start_url": "./index.html",
{ "display": "standalone",
"src": "favicon.ico", "theme_color": "#000000",
"sizes": "64x64 32x32 24x24 16x16", "background_color": "#ffffff"
"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"
} }

View File

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

View File

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

View File

@ -1,14 +1,17 @@
import { axiosInstance, ApiBase } from "./common"; import { axiosInstance, ApiBase } from "./common";
import { MACHINES_API_URL } from "../const";
const baseUrl = `${MACHINES_API_URL}/machines`;
class MachinesApi extends ApiBase { class MachinesApi extends ApiBase {
list = async () => { list = async () => {
return axiosInstance.get(`/machines`, {}); return axiosInstance.get(`${baseUrl}`, {});
}; };
get = async (machineId) => { get = async (machineId) => {
return axiosInstance.get(`/machines/${machineId}`, {}); return axiosInstance.get(`${baseUrl}/${machineId}`, {});
}; };
listProducts = async (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 { CardActionArea } from "@mui/material";
import { PRODUCT_IMAGE_DIR } from "../const"; import { PRODUCT_IMAGE_DIR } from "../const";
function ProductCard({ product }) { function ProductCard({ product, onClick }) {
const productImg = `${PRODUCT_IMAGE_DIR}/${product.image}`; const productImg = `${PRODUCT_IMAGE_DIR}/${product.image}`;
return ( return (
<Card> <Card sx={{ width: "100%" }}>
<CardActionArea> <CardActionArea
<CardMedia component="img" height="140" image={productImg} alt={product.name} /> onClick={() => {
onClick(product.id);
}}
>
<CardMedia component="img" height="200" image={productImg} alt={product.name} />
<CardContent> <CardContent>
<Typography gutterBottom variant="h5" component="div"> <Typography gutterBottom variant="h5" component="div" sx={{ marginBottom: 0 }}>
{product.name} {product.name}
</Typography> </Typography>
<Typography variant="body2" color="text.secondary">
{product.description}
</Typography>
</CardContent> </CardContent>
</CardActionArea> </CardActionArea>
</Card> </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 Grid from "@mui/material/Unstable_Grid2";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import { ProductCard } from "./ProductCard"; import { ProductCard } from "./ProductCard";
import { ProductModal } from "./ProductModal";
import { productsApi } from "../api";
function Products({ machineName, products, onSelect }) { 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) => { const productItems = products.map((product) => {
return ( return (
<Grid md={6} key={product.id} sx={{ display: "flex" }}> <Grid md={6} key={product.id} sx={{ display: "flex" }}>
<ProductCard product={product} /> <ProductCard product={product} onClick={onProductSelect} />
</Grid> </Grid>
); );
}); });
@ -21,6 +41,8 @@ function Products({ machineName, products, onSelect }) {
<Grid container spacing={2}> <Grid container spacing={2}>
{productItems} {productItems}
</Grid> </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/"; export const PRODUCT_IMAGE_DIR = "/static/products/";

View File

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

View File

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

View File

@ -6,13 +6,13 @@ import (
type configStruct struct { type configStruct struct {
AppHost string `default:"0.0.0.0"` AppHost string `default:"0.0.0.0"`
AppPort int `default:"3000"` AppPort int `default:"4000"`
DbHost string `default:"localhost"` DbHost string `default:"localhost"`
DbPort int `default:"55432"` DbPort int `default:"55432"`
DbName string `default:"komponiranje"` DbName string `default:"komponiranje"`
DbUser string `default:"pero"` DbUser string `default:"pero"`
DbPassword string `default:"pero.000"` DbPassword string `default:"pero.000"`
ProductsAppUrl string `default:"http://localhost:3001"` ProductsAppUrl string `default:"http://localhost:4001"`
} }
const ENV_PREFIX = "" 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:
@docker run \ @docker run \
--name $(CONTAINER_NAME) \ --name $(CONTAINER_NAME) \
--publish 3000:3000 \ --publish 4001:4001 \
--env CONTAINER_NAME="Awesome API server" \
--env DBPORT=55432 \ --env DBPORT=55432 \
--detach \ --detach \
$(IMAGE_NAME) $(IMAGE_NAME)

View File

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

View File

@ -6,7 +6,7 @@ import (
type configStruct struct { type configStruct struct {
AppHost string `default:"0.0.0.0"` AppHost string `default:"0.0.0.0"`
AppPort int `default:"3001"` AppPort int `default:"4001"`
DbHost string `default:"localhost"` DbHost string `default:"localhost"`
DbPort int `default:"55432"` DbPort int `default:"55432"`
DbName string `default:"komponiranje"` 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: address:
socket_address: socket_address:
address: machines-app address: machines-app
port_value: 3000 port_value: 4000
- name: products-app - name: products-app
connect_timeout: 0.25s connect_timeout: 0.25s
type: strict_dns type: strict_dns
@ -56,10 +56,4 @@ static_resources:
address: address:
socket_address: socket_address:
address: products-app address: products-app
port_value: 3000 port_value: 4001
admin:
access_log_path: "/dev/null"
address:
socket_address:
address: 0.0.0.0
port_value: 800