Implement Docker containerisation (#18)

* Begin initialising User backend

Basket will be contained within the User class, perhaps....

* Resolve compile-time error

* Complete minimal implementation

* Create build and compose file

* Make docker build more modular

* IT RUNS

Now to actually write unit tests

* Attempt to debug docker file

Currently works correctly when built individually, but docker-compose
currently fails

* Project broken into multipel Dockerfiles

Docker Compose issues have been resolved in the given mount points in
both compose files.

User.java modified enough to resolve build error.

* Add comments to proxy compose

* Start unit testing

* Finish implementing unit testing

* Make build stable for both Docker and localhost

* Incremental update

Got past proxy errors, now to fix domain names

* Resolve naming convention
This commit is contained in:
Blizzard Finnegan 2023-11-12 18:45:47 -05:00 committed by GitHub
parent 12a4052582
commit 805f676321
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 306 additions and 33 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@
docs/.settings
docs/.project
docs/.classpath
backend-storage/

View file

@ -14,19 +14,38 @@ An online U-Fund system built in Java 17=> and ___ _replace with other platform
## Prerequisites
- Java 11=>17 (Make sure to have correct `JAVA_HOME` setup in your environment)
### Bare-metal configuration
- Java >=17 (Make sure to have correct `JAVA_HOME` setup in your environment)
- Maven
- NodeJS/`npm` >= 18
- Angular >=16
### Docker configuration
- Docker >=20
- Docker Compose >= 2.0
- Docker Compose may work properly on older versions, but that is not supported at this time
## How to run it
### Bare-metal Configuration
1. Clone the repository and go to the `ufund-api` directory.
2. Execute `mvn compile exec:java` in the terminal.
3. Move to the `ufund-ui` directory.
4. Execute `npm install && ng serve` in the terminal.
5. Open your browser and navigate to http://localhost:4200
### Container Configuration
In addition to the traditional stack, this project can also be run and/or deployed with a Docker containerization as well.
To start the service, simply run `docker compose up -d` in the root of this repository to start your containers.
To access the new service, open your browser and navigate to http://localhost:8080.
For security purposes, it is recommended that this be put behind a reverse proxy such as NginxProxyManager or Caddy. A simple Caddy example has been provided within `docker-compose.proxy.yml`. Note that your domain and email must both be specified to proxy to a public-facing URL.
## Known bugs and disclaimers
(It may be the case that your implementation is not perfect.)

View file

@ -1 +0,0 @@
[]

59
docker-compose.proxy.yml Normal file
View file

@ -0,0 +1,59 @@
# Docker-compose version
version: "3.8"
# By default, all services in the same docker compose file
# are on the same internal network. This means inter-container
# discovery and communication is contained to the device, and
# is relatively safe.
services:
#Backend container declaration
backend:
image: backend
container_name: ufund_backend
#Use Dockerfile, but only build the backend
build: ./ufund-api
#Tell Docker daemon to auto-start service on boot, unless manually stopped
restart: unless-stopped
volumes:
- ./backend-storage:/app/data
#Front-end container declaration
frontend:
container_name: ufund_frontend
#Use Dockerfile, but build the frontend
build:
context: ./ufund-ui
# Tell the backend where the frontend is
args:
BACKEND_API_PORT: 8080
BACKEND_API_URL: ufund_backend
#Tell Docker daemon to auto-start service on boot, unless manually stopped
restart: unless-stopped
# Caddy labels
labels:
# Domain name you want to host at
caddy: localhost
# Specifies which port in this container needs to be proxied
caddy.reverse_proxy: "{{upstreams 80}}"
#Reverse Proxy container declaration
caddy:
#Caddy is a commonly used HTTPS reverse proxy
image: docker.io/lucaslorentz/caddy-docker-proxy:2.8.9
#Both HTTP and HTTPS ports need to be publicly exposed
ports:
- 80:80
- 443:443
#To allow for quick and convenient setup, Caddy requires access to the Docker socket
volumes:
- /var/run/docker.sock:/var/run/docker.sock
#Start caddy on reboot
restart: unless-stopped
labels:
# REPLACE THIS EMAIL
# This is the email provided to LetsEncrypt, to which you will be sent an email
# when your HTTPS certificate is bound to expire.
caddy.email: your-email-here@example.com

30
docker-compose.yml Normal file
View file

@ -0,0 +1,30 @@
# Docker-compose version
version: "3.8"
services:
#Backend container declaration
backend:
image: backend
container_name: ufund-backend
#Use Dockerfile, but only build the backend
build: ./ufund-api
#Tell Docker daemon to auto-start service on boot, unless manually stopped
restart: unless-stopped
volumes:
- ./backend-storage:/app/data
#Front-end container declaration
frontend:
container_name: ufund_frontend
#Use Dockerfile, but build the frontend
build:
context: ./ufund-ui
args:
BACKEND_API_URL: "ufund-backend"
BACKEND_API_PORT: "8080"
#Tell Docker daemon to auto-start service on boot, unless manually stopped
restart: unless-stopped
#Expose Nginx port to a unique port
ports:
# Outbound port: 8080
# Container port: 80
- "8080:80"

33
ufund-api/Dockerfile Normal file
View file

@ -0,0 +1,33 @@
# Backend is built using Maven
FROM maven:3.9.5 as backend-build
#FROM maven:3.9.5
# Specify the working directory within the build docker container
WORKDIR /app
#Copy the ufund-api folder in the current directory to the docker container
COPY . /app/
# RUN ls -lah
#
# RUN mvn compile
#
# ENTRYPOINT ["mvn"]
# CMD ["exec:java"]
#Compile the project into a Java ARchive (.jar file)
RUN mvn package
#OpenJDK containers are officially deprecated as per their overview page
#Their dockerhub page suggests (among others) eclipse-temurin as an alternative
#Using OpenJDK (or similar) as the base container results in smaller shipped container,
#with lower attack surface
FROM eclipse-temurin:17-jre
#Set working directory in shipped container
WORKDIR /app
#Copy jar file from build container to the shipping container
COPY --from=backend-build /app/target/ufund-api-3.0.0.jar /app/ufund-api-3.0.0.jar
RUN ls -lah /app
#Open port 8080, to allow listening to REST communication
EXPOSE 8080
#Set runtime command to explicitly run the jar file
ENTRYPOINT ["java"]
CMD ["-jar", "/app/ufund-api-3.0.0.jar"]

34
ufund-ui/Dockerfile Normal file
View file

@ -0,0 +1,34 @@
# Frontend is built using NodeJS
FROM node:20.9.0 as frontend-build
# Docker Compose arguments
ARG BACKEND_API_URL
ARG BACKEND_API_PORT
#Explicitly set working directory in new docker container
WORKDIR /app
#Copy the frontend folder into the working directory
COPY . /app/
#Modify environment file with set values
RUN sed -i "s/localhost/$BACKEND_API_URL/g" /app/src/proxy.conf.mjs
RUN sed -i "s/8080/$BACKEND_API_PORT/g" /app/src/proxy.conf.mjs
#Install all required NodeJS packages
RUN npm install
#Build NodeJS package (executes ng build)
RUN npm run build #-- --configuration=docker
#Use Nginx as web server
FROM nginx:1.25.3 as frontend
#Copy the static files built by NodeJS into the Nginx server's proper location
COPY --from=frontend-build /app/dist/ufund-ui /usr/share/nginx/html
#Proxy out relevant parts of the URL
COPY ./default.conf /etc/nginx/conf.d/default.conf
ARG BACKEND_API_URL
ARG BACKEND_API_PORT
RUN sed -i "s/localhost:/$BACKEND_API_URL:/g" /etc/nginx/conf.d/default.conf
RUN sed -i "s/8080/$BACKEND_API_PORT/g" /etc/nginx/conf.d/default.conf
RUN cat /etc/nginx/conf.d/default.conf
# Expose the HTTP port
EXPOSE 80

View file

@ -30,6 +30,8 @@
"scripts": []
},
"configurations": {
"docker":{
},
"production": {
"budgets": [
{
@ -58,12 +60,15 @@
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"proxyConfig": "src/proxy.conf.mjs",
"configurations": {
"production": {
"browserTarget": "ufund-ui:build:production"
"browserTarget": "ufund-ui:build:production",
"proxyConfig": "src/proxy.conf.mjs"
},
"development": {
"browserTarget": "ufund-ui:build:development"
"browserTarget": "ufund-ui:build:development",
"proxyConfig": "src/proxy.conf.mjs"
}
},
"defaultConfiguration": "development"

55
ufund-ui/default.conf Normal file
View file

@ -0,0 +1,55 @@
server {
listen 80;
listen [::]:80;
server_name localhost;
#access_log /var/log/nginx/host.access.log main;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
location /users{
proxy_pass http://localhost:8080;
}
location /cupboard{
proxy_pass http://localhost:8080;
}
location /basket{
proxy_pass http://localhost:8080;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}

View file

@ -25,6 +25,7 @@
"@angular/cli": "^16.2.6",
"@angular/compiler-cli": "^16.2.0",
"@types/jasmine": "~4.3.0",
"@types/node": "^20.9.0",
"jasmine-core": "~4.6.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
@ -3320,12 +3321,12 @@
"dev": true
},
"node_modules/@types/node": {
"version": "20.8.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.4.tgz",
"integrity": "sha512-ZVPnqU58giiCjSxjVUESDtdPk4QR5WQhhINbc9UBrKLU68MX5BF6kbQzTrkwbolyr0X8ChBpXfavr5mZFKZQ5A==",
"version": "20.9.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz",
"integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==",
"dev": true,
"dependencies": {
"undici-types": "~5.25.1"
"undici-types": "~5.26.4"
}
},
"node_modules/@types/qs": {
@ -11367,9 +11368,9 @@
}
},
"node_modules/undici-types": {
"version": "5.25.3",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz",
"integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==",
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"node_modules/unicode-canonical-property-names-ecmascript": {

View file

@ -27,6 +27,7 @@
"@angular/cli": "^16.2.6",
"@angular/compiler-cli": "^16.2.0",
"@types/jasmine": "~4.3.0",
"@types/node": "^20.9.0",
"jasmine-core": "~4.6.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",

View file

@ -0,0 +1,19 @@
// https://dzone.com/articles/using-environment-variable-with-angular
const fs = require("fs");
// this will get argument from command line
let config = process.argv[2];
// read template file as string
let template_environment = fs.readFileSync("./src/environments/environment.template.ts").toString();
// for every keys you have defined on environment this will loop over them and replace the values accordingly
Object.keys(process.env).forEach(env_var => {
template_environment = template_environment.replaceAll(`\${${env_var}}`,process.env[env_var])
});
// if config is given use it on file name.
if(config)
fs.writeFileSync(`./src/environments/environment.${config}.ts`, template_environment);
else
fs.writeFileSync("./src/environments/environment.ts", template_environment);

View file

@ -2,6 +2,7 @@ import { Component } from '@angular/core';
import { AccountService } from './services';
import { User } from './models';
import { environment } from '../environments/environment';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
@ -14,6 +15,7 @@ export class AppComponent {
constructor(private accountService: AccountService){
this.accountService.user.subscribe(x => this.user = x);
//console.log(environment.apiUrl);
}
logout(){

View file

@ -17,7 +17,7 @@ export class JwtInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const user = this.accountService.userValue;
const isLoggedIn = user?.token;
const isApiUrl = request.url.startsWith(environment.apiUrl);
const isApiUrl = true;//request.url.startsWith(environment.apiUrl);
if(isLoggedIn && isApiUrl){
request = request.clone({
setHeaders: { Authorization: `Bearer ${user.token}` }

View file

@ -24,7 +24,7 @@ export class AccountService {
public get userValue(){ return this.userSubject.value; }
login(username: string, password: string){
return this.http.post<User>(`${environment.apiUrl}/users/authenticate`, { username, password})
return this.http.post<User>(`/users/authenticate`, { username, password})
.pipe(map(user => {
localStorage.setItem('user', JSON.stringify(user));
this.userSubject.next(user);
@ -39,21 +39,21 @@ export class AccountService {
}
register(user: User){
return this.http.post(`${environment.apiUrl}/users/register`,user);
return this.http.post(`/users/register`,user);
}
getAll() {
return this.http.get<User[]>(`${environment.apiUrl}/users`);
return this.http.get<User[]>(`/users`);
}
getById(id: string) {
return this.http.get<User>(`${environment.apiUrl}/users/${id}`);
return this.http.get<User>(`/users/${id}`);
}
update(id: string, params: any) {
return this.http.put(`${environment.apiUrl}/users/${id}`, params)
return this.http.put(`/users/${id}`, params)
.pipe(map(x => {
// update stored user if the logged in user updated their own record
if (id == this.userValue?.id) {
if (id === this.userValue?.id) {
// update local storage
const user = { ...this.userValue, ...params };
localStorage.setItem('user', JSON.stringify(user));
@ -66,10 +66,10 @@ export class AccountService {
}
delete(id: string) {
return this.http.delete(`${environment.apiUrl}/users/${id}`)
return this.http.delete(`/users/${id}`)
.pipe(map(x => {
// auto logout if the logged in user deleted their own record
if (id == this.userValue?.id) {
if (id === this.userValue?.id) {
this.logout();
}
return x;

View file

@ -23,31 +23,31 @@ export class BasketService {
) { }
getBasketNeeds() {
let url = `${environment.apiUrl}/basket/${this.accountService.userValue?.id}`;
let url = `/basket/${this.accountService.userValue?.id}`;
console.log("GET " + url);
return this.http.get<Needs[]>(url);
}
addNeedToBasket(id: number) {
let url = `${environment.apiUrl}/basket/${this.accountService.userValue?.id}`;
let url = `/basket/${this.accountService.userValue?.id}`;
console.log("POST " + url + " " + id);
return this.http.post<Needs>(url, id, this.httpOptions).subscribe();
}
removeNeedFromBasket(needID: number) {
let url = `${environment.apiUrl}/basket/${this.accountService.userValue?.id}/${needID}`;
let url = `/basket/${this.accountService.userValue?.id}/${needID}`;
console.log("DELETE " + url);
this.http.delete<Needs>(url).subscribe();
}
getNeedFromBasket(needID: number) {
let url = `${environment.apiUrl}/basket/${this.accountService.userValue?.id}/${needID}`;
let url = `/basket/${this.accountService.userValue?.id}/${needID}`;
console.log("GET " + url);
return this.http.get<Needs>(url);
}
checkout(needIDs: number[]) {
let url = `${environment.apiUrl}/basket/${this.accountService.userValue?.id}/checkout`;
let url = `/basket/${this.accountService.userValue?.id}/checkout`;
console.log("POST " + url + " " + needIDs);
this.http.post(url, needIDs, this.httpOptions).subscribe();
}

View file

@ -4,7 +4,7 @@ import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { environment } from 'src/environments/environment';
//import { environment } from 'src/environments/environment';
import { Needs } from 'src/app/models';
@Injectable({
@ -23,18 +23,18 @@ export class NeedsService {
public get needsValue() { return this.needsSubject.value; }
register(needs: Needs) {
return this.http.post(`${environment.apiUrl}/cupboard/need`, needs);
return this.http.post(`/cupboard/need`, needs);
}
getAll() {
return this.http.get<Needs[]>(`${environment.apiUrl}/cupboard/need`);
return this.http.get<Needs[]>(`/cupboard/need`);
}
getById(id: string) {
return this.http.get<Needs>(`${environment.apiUrl}/cupboard/need/${id}`);
return this.http.get<Needs>(`/cupboard/need/${id}`);
}
update(id: number, params: any) {
return this.http.put(`${environment.apiUrl}/cupboard/need/${id}`, params)
return this.http.put(`/cupboard/need/${id}`, params)
.pipe(map(x => {
// update stored user if the logged in user updated their own record
if (id == this.needsValue?.id) {
@ -50,6 +50,6 @@ export class NeedsService {
}
delete(id: number) {
return this.http.delete(`${environment.apiUrl}/cupboard/need/${id}`)
return this.http.delete(`/cupboard/need/${id}`)
}
}

View file

@ -0,0 +1,3 @@
export const environment = {
// apiUrl: "http://BACKEND_URL:BACKEND_PORT"
}

View file

@ -1,3 +1,3 @@
export const environment = {
apiUrl: 'http://localhost:8080'
apiUrl: '${BACKEND_URL}:${BACKEND_PORT}'
}

View file

@ -0,0 +1,12 @@
export default[
{
context: [
"/users",
"/cupboard",
"/basket",
],
target: 'http://localhost:8080',
secure: false,
changeorigin: true,
}
];

View file

@ -3,7 +3,7 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
"types": ["node"]
},
"files": [
"src/main.ts"