diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..40018bf --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +node_modules +build +.dockerignore +Dockerfile \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4bdf4d9..26b05ff 100644 --- a/.gitignore +++ b/.gitignore @@ -103,3 +103,5 @@ venv.bak/ # mypy .mypy_cache/ *.db + +docker-compose.yml \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5132333 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,125 @@ +ARG REACT_APP_NAME=frontend +ARG FLASK_APP_NAME=backend +ARG REACT_APP_PATH=/opt/$REACT_APP_NAME +ARG FLASK_APP_PATH=/opt/$FLASK_APP_NAME +ARG PYTHON_VERSION=3.10.0-alpine +ARG POETRY_VERSION=1.1.13 +ARG NGINX_VERSION=1.23.0-alpine + +########## STAGE 1: STAGING ########## +FROM node:18-alpine AS node-staging +ARG REACT_APP_NAME +ARG REACT_APP_PATH + +WORKDIR $REACT_APP_PATH +COPY ./frontend ./ + +FROM python:3.10 as python-staging + +ARG FLASK_APP_NAME +ARG FLASK_APP_PATH + +ENV \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONFAULTHANDLER=1 +ENV \ + POETRY_VERSION=$POETRY_VERSION \ + POETRY_HOME="/opt/poetry" \ + POETRY_VIRTUALENVS_IN_PROJECT=true \ + POETRY_NO_INTERACTION=1 + +# Install Poetry - respects $POETRY_VERSION & $POETRY_HOME +RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py | python +ENV PATH="$POETRY_HOME/bin:$PATH" + +WORKDIR $FLASK_APP_PATH +COPY ./$FLASK_APP_NAME/poetry.lock ./$FLASK_APP_NAME/pyproject.toml ./$FLASK_APP_NAME/tests ./ ./$FLASK_APP_NAME/configmodule.py ./ +COPY ./$FLASK_APP_NAME/$FLASK_APP_NAME ./$FLASK_APP_NAME + +######### DEV STAGE 2: serve frontend ######### +FROM node-staging as frontend-dev +ARG REACT_APP_NAME +ARG REACT_APP_PATH +#ENV NODE_ENV="development" +#ENV HTTP_PROXY="http://host.docker.internal:1337" + +WORKDIR $REACT_APP_PATH +RUN npm install +CMD ["npm", "run", "start"] + +########## DEV STAGE 3: serve backend ######### +FROM python-staging as backend-dev +ARG FLASK_APP_NAME +ARG FLASK_APP_PATH + +WORKDIR $FLASK_APP_PATH +RUN poetry install + +# CONFIG VARS +# ENV DEVEL=true \ +# DB_URI="postgresql://eelbot:mysecretpassword@host.docker.internal:5432/eelbot" \ +# BACKEND_HOST="0.0.0.0" \ +# BACKEND_PORT_DEV='1337' + +CMD ["poetry", "run", "server"] + +######## PROD STAGE 1: build backend ######### +FROM python-staging as python-build-env +ARG FLASK_APP_PATH +ARG FLASK_APP_NAME + +WORKDIR $FLASK_APP_PATH +RUN poetry build --format wheel +RUN poetry export --format requirements.txt --output constraints.txt --without-hashes + +######## PROD STAGE 2: serve backend ######### +FROM python:$PYTHON_VERSION as backend-prod +ARG FLASK_APP_NAME +ARG FLASK_APP_PATH + +ENV \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONFAULTHANDLER=1 + +ENV \ + PIP_NO_CACHE_DIR=off \ + PIP_DISABLE_PIP_VERSION_CHECK=on \ + PIP_DEFAULT_TIMEOUT=100 + +# CONFIG VARS +# ENV DB_URI="postgresql://eelbot:mysecretpassword@host.docker.internal:5432/eelbot" \ +# BACKEND_HOST="0.0.0.0" \ +# BACKEND_PORT="1338" + +WORKDIR $FLASK_APP_PATH +COPY --from=python-build-env $FLASK_APP_PATH/dist/*.whl ./ +COPY --from=python-build-env $FLASK_APP_PATH/constraints.txt ./ +RUN pip install ./${FLASK_APP_NAME}*.whl --constraint constraints.txt + +ENV FLASK_APP_NAME $FLASK_APP_NAME + +WORKDIR "/usr/local/lib/python3.10/site-packages/$FLASK_APP_NAME" + +CMD python -m $FLASK_APP_NAME + +######## PROD STAGE 3: build frontend ######### +FROM node-staging as node-build-env +ARG REACT_APP_NAME +ARG REACT_APP_PATH + +WORKDIR $REACT_APP_PATH + +ENV NODE_ENV="production" + +RUN npm install --production +RUN npm run build + +######## PROD STAGE 4: serve frontend ######### +FROM nginx:$NGINX_VERSION as frontend-prod +ARG REACT_APP_NAME +ARG REACT_APP_PATH + +COPY --from=node-build-env $REACT_APP_PATH/build /usr/share/nginx/html +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/Readme.md b/Readme.md index 9c61258..d1ff6ca 100644 --- a/Readme.md +++ b/Readme.md @@ -2,4 +2,87 @@ A web based config tool to configure [Eelbot](https://github.com/Emseers/Eelbot), a discord bot for Emseers. ## Implemenation -Implemented as a RESTful API powered by flask and a React based SPA. \ No newline at end of file +Implemented as a RESTful API powered by flask and a React based SPA. + + +## Installation & Deployment + +### Frontend + +#### Setup +1. Navigate into the `frontend` directory + +2. install & resolve dependencies by using the following command: +```bash +npm install +``` + +#### Deployment + +3. To run the app in development mode: +```bash +npm run start +``` + +For production, you may build the app using: +```bash +npm run build +``` + +Note: edit the config file to add your desired ports, hosts, and database location + +### Backend + +#### Setup + +1. Navigate into the `backend` directory + +2. install & resolve dependencies & create the virtual environment using the following command: +```bash +poetry install +``` +3. To run the DEV version add the environment variable: `DEVEL = 'true'`, otherwise the production server will run. + +#### Deployment + +To run the server within the virtual environment: + +```bash +`poetry run server` +``` + +## Docker + +As an alternative to building and running eelbot-ui locally, you can build and run it in Docker. Build the images with the provided Dockerfile: + +### Development server Images +```bash +docker build --target frontend-dev -t frontend-dev:latest . +docker build --target backend-dev -t backend-dev:latest . +``` + +### Production server Images +```bash +docker build --target frontend-prod -t frontend:latest . +docker build --target backend-prod -t backend:latest . +``` + +You need to mount all required files and folders to run the containers: + +## Run Development Containers +```bash +docker run -it --name frontend -p frontend-dev:latest +``` + +```bash +docker run -it --name backend -p -v :/opt/backend/configmodule.py backend-dev:latest +``` + +## Run Production Containers +```bash +docker run -it --name backend -p -v :/usr/local/lib/python3.10/site-packages/backend/configmodule.py backend:latest +``` + +```bash +docker run -it --name frontend -p -v :/etc/nginx/conf.d/default.conf frontend:latest +``` \ No newline at end of file diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..452bc32 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,3 @@ +.dockerignore +Dockerfile +Dockerfile.prod \ No newline at end of file diff --git a/backend/README.md b/backend/README.md deleted file mode 100644 index b2f10c1..0000000 --- a/backend/README.md +++ /dev/null @@ -1,6 +0,0 @@ -To set up: -1. install & resolve dependencies & create the virtual environment using the following command: `poetry install` -3. To run the DEV version add the environment variable: `DEVEL = 'true'`. - -To run the server within the virtual environment: -1. `poetry run server` \ No newline at end of file diff --git a/backend/backend/__init__.py b/backend/backend/__init__.py index 94b463e..e69de29 100644 --- a/backend/backend/__init__.py +++ b/backend/backend/__init__.py @@ -1,7 +0,0 @@ -import os -__version__ = '0.1.0' - -def main(): - from backend.app import DevApp, ProdApp - app = DevApp() if 'DEVEL' in os.environ else ProdApp() - app.serve() \ No newline at end of file diff --git a/backend/backend/__main__.py b/backend/backend/__main__.py new file mode 100644 index 0000000..7b818b2 --- /dev/null +++ b/backend/backend/__main__.py @@ -0,0 +1,10 @@ +import os +__version__ = '0.1.0' + +def main(): + from backend.app import DevApp, ProdApp + app = DevApp() if 'DEVEL' in os.environ else ProdApp() + app.serve() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/backend/backend/app.py b/backend/backend/app.py index ca101f4..296b3ae 100644 --- a/backend/backend/app.py +++ b/backend/backend/app.py @@ -3,6 +3,9 @@ from abc import abstractmethod from backend.utils.eeljokes import db from backend.endpoints import jokes_bp + +from configmodule import Config, DevelopmentConfig, ProductionConfig + import waitress class App: @@ -10,8 +13,6 @@ class App: def __init__(self): self.app = Flask(__name__) - self.app.config['SQLALCHEMY_DATABASE_URI'] = "postgresql://eelbot:mysecretpassword@localhost:5432/eelbot" # TODO: Use environment variables instead - db.init_app(self.app) @abstractmethod def configure_app(self): @@ -29,14 +30,14 @@ def __init__(self): super().__init__() self.configure_app() self.register_app() + db.init_app(self.app) def configure_app(self): super().configure_app() - self.host = "0.0.0.0" # TODO: Replace with reading from config - self.port = "1337" # TODO: Replace with reading from config + self.app.config.from_object('configmodule.DevelopmentConfig') def serve(self): - self.app.run(host=self.host, port=self.port) + self.app.run(host=self.app.config['HOST'], port=self.app.config['PORT']) class ProdApp(App): @@ -44,11 +45,11 @@ def __init__(self): super().__init__() self.configure_app() self.register_app() + db.init_app(self.app) def configure_app(self): super().configure_app() - self.host = "0.0.0.0" # TODO: Replace with reading from config - self.port = "1338" # TODO: Replace with reading from config + self.app.config.from_object('configmodule.ProductionConfig') def serve(self): - waitress.serve(self.app, host=self.host, port=self.port) + waitress.serve(self.app, host=self.app.config['HOST'], port=self.app.config['PORT']) diff --git a/backend/configmodule.py b/backend/configmodule.py new file mode 100644 index 0000000..8ad94fa --- /dev/null +++ b/backend/configmodule.py @@ -0,0 +1,13 @@ +import os +basedir = os.path.abspath(os.path.dirname(__file__)) + +class Config: + SQLALCHEMY_DATABASE_URI = os.environ.get('DB_URI') or "postgresql://eelbot:mysecretpassword@127.0.0.1:5432/eelbot" + HOST = os.environ.get('BACKEND_HOST') or "127.0.0.1" + +class DevelopmentConfig(Config): + ENV = 'development' + PORT = os.environ.get('BACKEND_PORT_DEV') or "1337" + +class ProductionConfig(Config): + PORT = os.environ.get('BACKEND_PORT') or "1338" \ No newline at end of file diff --git a/backend/poetry.lock b/backend/poetry.lock index c750bbe..bbecf7c 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -152,6 +152,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.extras] dev = ["pre-commit", "tox"] +[[package]] +name = "psycopg2-binary" +version = "2.9.3" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +category = "main" +optional = false +python-versions = ">=3.6" + [[package]] name = "py" version = "1.11.0" @@ -267,7 +275,7 @@ watchdog = ["watchdog"] [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "1b241fea43bceb60fc822b1bcf67498736bc4014a587a10abf2cf8b9ad7dd273" +content-hash = "e2948af3b80329844afc74411c447ff35ef1535315215a883cc768b0ef6f3fe9" [metadata.files] atomicwrites = [ @@ -417,6 +425,7 @@ pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] +psycopg2-binary = [] py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, diff --git a/backend/pyproject.toml b/backend/pyproject.toml index fb295be..fff038d 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -10,12 +10,13 @@ Flask = "2.0.0" Flask-SQLAlchemy = "2.5.0" waitress = "2.1.1" Flask-Cors = "3.0.9" +psycopg2-binary = "^2.9.3" [tool.poetry.dev-dependencies] pytest = "^5.2" [tool.poetry.scripts] -server = "backend:main" +server = "backend.__main__:main" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/frontend/README.md b/frontend/README.md deleted file mode 100644 index 58beeac..0000000 --- a/frontend/README.md +++ /dev/null @@ -1,70 +0,0 @@ -# Getting Started with Create React App - -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). - -## Available Scripts - -In the project directory, you can run: - -### `npm start` - -Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in your browser. - -The page will reload when you make changes.\ -You may also see any lint errors in the console. - -### `npm test` - -Launches the test runner in the interactive watch mode.\ -See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. - -### `npm run build` - -Builds the app for production to the `build` folder.\ -It correctly bundles React in production mode and optimizes the build for the best performance. - -The build is minified and the filenames include the hashes.\ -Your app is ready to be deployed! - -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. - -### `npm run eject` - -**Note: this is a one-way operation. Once you `eject`, you can't go back!** - -If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. - -Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. - -You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. - -## Learn More - -You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). - -To learn React, check out the [React documentation](https://reactjs.org/). - -### Code Splitting - -This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) - -### Analyzing the Bundle Size - -This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) - -### Making a Progressive Web App - -This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) - -### Advanced Configuration - -This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) - -### Deployment - -This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) - -### `npm run build` fails to minify - -This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f7fba34..d0cf039 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,7 @@ "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^13.1.1", "@testing-library/user-event": "^13.5.0", + "http-proxy-middleware": "^2.0.6", "react": "^18.0.0", "react-dom": "^18.0.0", "react-scripts": "5.0.1" diff --git a/frontend/package.json b/frontend/package.json index 280f804..e0c4181 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,7 @@ "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^13.1.1", "@testing-library/user-event": "^13.5.0", + "http-proxy-middleware": "^2.0.6", "react": "^18.0.0", "react-dom": "^18.0.0", "react-scripts": "5.0.1" @@ -37,6 +38,5 @@ "last 1 firefox version", "last 1 safari version" ] - }, - "proxy": "http://127.0.0.1:1337" -} \ No newline at end of file + } +} diff --git a/frontend/src/services/jokes.js b/frontend/src/services/jokes.js index 4fb301f..3805730 100644 --- a/frontend/src/services/jokes.js +++ b/frontend/src/services/jokes.js @@ -1,11 +1,11 @@ const getPage = async (numJokesPerPage, page) => { - const response = await fetch(`/joke/?num_jokes_per_page=${numJokesPerPage}&page=${page}`); + const response = await fetch(`/api/joke/?num_jokes_per_page=${numJokesPerPage}&page=${page}`); if (!response.ok) throw new Error(`An error has occured: ${response.status}`); return response.json(); }; const get = async (id) => { - const response = await fetch(`/joke/${id}`); + const response = await fetch(`/api/joke/${id}`); if (!response.ok) throw new Error(`An error has occured: ${response.status}`); return response.json(); }; @@ -15,7 +15,7 @@ const del = async (id) => { method: 'DELETE', } - const response = await fetch(`/joke/${id}`, settings); + const response = await fetch(`/api/joke/${id}`, settings); if (!response.ok) throw new Error(`An error has occured: ${response.status}`); return response.ok; }; @@ -30,7 +30,7 @@ const create = async (joke) => { body: JSON.stringify(joke) }; - const response = await fetch('/joke', settings); + const response = await fetch('/api/joke', settings); if (!response.ok) throw new Error(`An error has occured: ${response.status}`); return response.ok; }; @@ -45,7 +45,7 @@ const put = async (id, joke) => { body: JSON.stringify(joke) }; - const response = await fetch(`/joke/${id}`, settings); + const response = await fetch(`/api/joke/${id}`, settings); if (!response.ok) throw new Error(`An error has occured: ${response.status}`); return response.ok; }; diff --git a/frontend/src/setupProxy.js b/frontend/src/setupProxy.js new file mode 100644 index 0000000..d6ef4a2 --- /dev/null +++ b/frontend/src/setupProxy.js @@ -0,0 +1,17 @@ +const { createProxyMiddleware } = require('http-proxy-middleware'); + +module.exports = function (app) { + + app.use( + '/api/', + createProxyMiddleware({ + target: process.env.HTTP_PROXY || 'http://127.0.0.1:1337', + changeOrigin: true, + pathRewrite: { + '^/api/': '/', + }, + }) + ); +}; + +