From 822034525dcfe8b5923005deba3b026de545ba7b Mon Sep 17 00:00:00 2001 From: mohamedtaee Date: Thu, 23 May 2024 19:55:57 -0500 Subject: [PATCH 1/4] Beginning the changes for newer versions. --- Dockerfile | 19 ++++++++----------- docker-compose.yml | 10 +++++----- requirements.txt | 10 +--------- 3 files changed, 14 insertions(+), 25 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1173d01..baff500 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,12 @@ -FROM python:3.7-alpine +FROM python:3.11.2-slim -ADD requirements.txt . +EXPOSE 8080 -RUN apk add python3-dev build-base linux-headers pcre-dev && pip install --no-cache-dir -r requirements.txt +WORKDIR /app -# adding application files -ADD . /webapp +RUN pip install --upgrade pip +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . -# configure path /webapp to HOME-dir -ENV HOME /webapp -WORKDIR /webapp - -ENTRYPOINT ["uwsgi"] -CMD ["--http", "0.0.0.0:8080", "--wsgi-file", "wsgi.py", "--callable", "app", "--processes", "1", "--threads", "8"] \ No newline at end of file +CMD ["gunicorn", "--workers", "2", "--timeoute", "5000", "--preload", "--bind", "0.0.0.0:8080", "app:app"] diff --git a/docker-compose.yml b/docker-compose.yml index 9707ef5..e055ee0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,19 +1,19 @@ version: '3' services: nginx-ui: - container_name: nginx-ui + container_name: flask-nginx-ui build: . - image: nginx-ui:latest + image: flask-nginx-ui:latest ports: - - 8080:8080 + - "8080:8080" volumes: - nginx:/etc/nginx nginx: container_name: nginx - image: nginx:1.18.0-alpine + image: nginx:alpine3.19 ports: - - 80:80 + - "443:443" volumes: - nginx:/etc/nginx diff --git a/requirements.txt b/requirements.txt index a8aa493..a32a733 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1 @@ -click==7.1.2 -Flask==1.1.2 -Flask-Moment==0.9.0 -itsdangerous==1.1.0 -Jinja2==2.11.3 -MarkupSafe==1.1.1 -pytz==2020.1 -uWSGI==2.0.18 -Werkzeug==1.0.1 +Flask~=3.0.3 \ No newline at end of file From 70173ffefa2fa9a384f95fb3c38e89806a6da15b Mon Sep 17 00:00:00 2001 From: mohamedtaee Date: Fri, 24 May 2024 15:15:53 -0500 Subject: [PATCH 2/4] Restructured to use updated Flask and best practices. --- Dockerfile | 14 +++++++++----- app/__init__.py | 11 ++++++----- app/api/__init__.py | 5 ----- app/api/endpoints.py | 18 +++++++++--------- app/ui/__init__.py | 5 ----- app/ui/views.py | 11 ++++++----- docker-compose.yml | 2 +- requirements.txt | 4 +++- 8 files changed, 34 insertions(+), 36 deletions(-) diff --git a/Dockerfile b/Dockerfile index baff500..b6536d9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,16 @@ FROM python:3.11.2-slim -EXPOSE 8080 - WORKDIR /app +COPY . /app + RUN pip install --upgrade pip -COPY requirements.txt . + RUN pip install --no-cache-dir -r requirements.txt -COPY . . -CMD ["gunicorn", "--workers", "2", "--timeoute", "5000", "--preload", "--bind", "0.0.0.0:8080", "app:app"] +EXPOSE 8080 + +ENV FLASK_APP=app +ENV FLASK_ENV=production + +CMD ["gunicorn", "--workers", "1", "--timeout", "5000", "--preload", "--bind", "0.0.0.0:8080", "wsgi:app"] diff --git a/app/__init__.py b/app/__init__.py index 7d8039c..7e6832a 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -2,7 +2,6 @@ from config import config from flask_moment import Moment - moment = Moment() @@ -10,13 +9,15 @@ def create_app(config_name): app = Flask(__name__) app.config.from_object(config[config_name]) + config[config_name].init_app(app) + config[config_name].init_app(app) moment.init_app(app) - from app.ui import ui as ui_blueprint - app.register_blueprint(ui_blueprint) + from app.ui import views as ui_views + app.register_blueprint(ui_views.bp) - from app.api import api as api_blueprint - app.register_blueprint(api_blueprint, url_prefix='/api') + from app.api import endpoints as api_endpoints + app.register_blueprint(api_endpoints.bp) return app diff --git a/app/api/__init__.py b/app/api/__init__.py index f94274e..e69de29 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -1,5 +0,0 @@ -from flask import Blueprint - -api = Blueprint('api', __name__) - -from . import endpoints diff --git a/app/api/endpoints.py b/app/api/endpoints.py index df8f9e1..cdfe720 100644 --- a/app/api/endpoints.py +++ b/app/api/endpoints.py @@ -3,10 +3,10 @@ import os import flask -from app.api import api +bp = flask.Blueprint('api', __name__, url_prefix='/api') -@api.route('/config/', methods=['GET']) +@bp.route('/config/', methods=['GET']) def get_config(name: str): """ Reads the file with the corresponding name that was passed. @@ -25,7 +25,7 @@ def get_config(name: str): return flask.render_template('config.html', name=name, file=_file), 200 -@api.route('/config/', methods=['POST']) +@bp.route('/config/', methods=['POST']) def post_config(name: str): """ Accepts the customized configuration and saves it in the configuration file with the supplied name. @@ -45,7 +45,7 @@ def post_config(name: str): return flask.make_response({'success': True}), 200 -@api.route('/domains', methods=['GET']) +@bp.route('/domains', methods=['GET']) def get_domains(): """ Reads all files from the configuration file directory and checks the state of the site configuration. @@ -83,7 +83,7 @@ def get_domains(): return flask.render_template('domains.html', sites_available=sites_available, sites_enabled=sites_enabled), 200 -@api.route('/domain/', methods=['GET']) +@bp.route('/domain/', methods=['GET']) def get_domain(name: str): """ Takes the name of the domain configuration file and @@ -116,7 +116,7 @@ def get_domain(name: str): return flask.render_template('domain.html', name=name, file=_file, enabled=enabled), 200 -@api.route('/domain/', methods=['POST']) +@bp.route('/domain/', methods=['POST']) def post_domain(name: str): """ Creates the configuration file of the domain. @@ -141,7 +141,7 @@ def post_domain(name: str): return response -@api.route('/domain/', methods=['DELETE']) +@bp.route('/domain/', methods=['DELETE']) def delete_domain(name: str): """ Deletes the configuration file of the corresponding domain. @@ -168,7 +168,7 @@ def delete_domain(name: str): return flask.jsonify({'success': False}), 400 -@api.route('/domain/', methods=['PUT']) +@bp.route('/domain/', methods=['PUT']) def put_domain(name: str): """ Updates the configuration file with the corresponding domain name. @@ -191,7 +191,7 @@ def put_domain(name: str): return flask.make_response({'success': True}), 200 -@api.route('/domain//enable', methods=['POST']) +@bp.route('/domain//enable', methods=['POST']) def enable_domain(name: str): """ Activates the domain in Nginx so that the configuration is applied. diff --git a/app/ui/__init__.py b/app/ui/__init__.py index d5fdb35..e69de29 100644 --- a/app/ui/__init__.py +++ b/app/ui/__init__.py @@ -1,5 +0,0 @@ -from flask import Blueprint - -ui = Blueprint('ui', __name__) - -from . import views diff --git a/app/ui/views.py b/app/ui/views.py index 2696a92..3ac7259 100644 --- a/app/ui/views.py +++ b/app/ui/views.py @@ -1,9 +1,10 @@ -from app.ui import ui -import flask +from flask import Blueprint, current_app, render_template import os +bp = Blueprint('ui', __name__) -@ui.route('/', methods=['GET']) + +@bp.route('/', methods=['GET']) def index(): """ Delivers the home page of Nginx UI. @@ -11,6 +12,6 @@ def index(): :return: Rendered HTML document. :rtype: str """ - nginx_path = flask.current_app.config['NGINX_PATH'] + nginx_path = current_app.config['NGINX_PATH'] config = [f for f in os.listdir(nginx_path) if os.path.isfile(os.path.join(nginx_path, f))] - return flask.render_template('index.html', config=config) + return render_template('index.html', config=config) diff --git a/docker-compose.yml b/docker-compose.yml index e055ee0..8163dc5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ version: '3' services: - nginx-ui: + flask-nginx-ui: container_name: flask-nginx-ui build: . image: flask-nginx-ui:latest diff --git a/requirements.txt b/requirements.txt index a32a733..5093808 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,3 @@ -Flask~=3.0.3 \ No newline at end of file +Flask~=3.0.3 +gunicorn~=22.0.0 +Flask-Moment~=1.0.5 \ No newline at end of file From 83a2ac57e48f05a1b7584a083faff214903ba1c3 Mon Sep 17 00:00:00 2001 From: mohamedtaee Date: Sun, 26 May 2024 00:30:04 -0500 Subject: [PATCH 3/4] Added restart button and method to interact with container. Resolves #14. --- app/api/endpoints.py | 47 +++++++++++++++++- app/static/custom.css | 93 +++++++++++++++++++++++++++++++---- app/static/nginxController.js | 63 ++++++++++++++++++++++++ app/templates/index.html | 12 ++++- docker-compose.yml | 1 + requirements.txt | 3 +- 6 files changed, 207 insertions(+), 12 deletions(-) create mode 100644 app/static/nginxController.js diff --git a/app/api/endpoints.py b/app/api/endpoints.py index cdfe720..dc72257 100644 --- a/app/api/endpoints.py +++ b/app/api/endpoints.py @@ -2,11 +2,13 @@ import io import os import flask +import platform +import docker bp = flask.Blueprint('api', __name__, url_prefix='/api') -@bp.route('/config/', methods=['GET']) +@bp.route('/config/', methods=['GET']) def get_config(name: str): """ Reads the file with the corresponding name that was passed. @@ -215,3 +217,46 @@ def enable_domain(name: str): os.rename(os.path.join(config_path, _), os.path.join(config_path, _ + '.disabled')) return flask.make_response({'success': True}), 200 + + +def is_rhel(): + try: + with open('/etc/os-release') as f: + os_release = f.read() + return 'rhel' in os_release.lower() + except FileNotFoundError: + return False + + +USE_SUDO_PODMAN = is_rhel() + + +def get_docker_client(): + print("Platform: ", platform.system()) + if USE_SUDO_PODMAN: + base_url = f'unix:///run/user/{os.getuid()}/podman/podman.sock' + elif platform.system() == "Windows": + base_url = 'npipe:////./pipe/docker_engine' + else: + base_url = 'unix://var/run/docker.sock' + return docker.DockerClient(base_url=base_url) + + +client = get_docker_client() + + +@bp.route('/restart-nginx', methods=['POST']) +def restart_nginx(): + try: + container = client.containers.get('nginx') + container.restart() + except Exception as e: + return flask.jsonify({"message": f"Error: {e}"}), 500 + return flask.jsonify({"message": "Nginx container restarted"}), 200 + + +@bp.route('/status-nginx', methods=['GET']) +def status_nginx(): + container = client.containers.get('nginx') + status = container.status + return flask.jsonify({"container": "nginx", "status": status}), 200 diff --git a/app/static/custom.css b/app/static/custom.css index 49b12e7..98d332b 100644 --- a/app/static/custom.css +++ b/app/static/custom.css @@ -14,17 +14,92 @@ textarea { #main-container { margin-top: 5em; } + #domain { display: none; } @media only screen and (max-width: 666px) { - [class*="mobile hidden"], - [class*="tablet only"]:not(.mobile), - [class*="computer only"]:not(.mobile), - [class*="large monitor only"]:not(.mobile), - [class*="widescreen monitor only"]:not(.mobile), - [class*="or lower hidden"] { - display: none !important; - } -} \ No newline at end of file + [class*="mobile hidden"], + [class*="tablet only"]:not(.mobile), + [class*="computer only"]:not(.mobile), + [class*="large monitor only"]:not(.mobile), + [class*="widescreen monitor only"]:not(.mobile), + [class*="or lower hidden"] { + display: none !important; + } +} + +.status-circle { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 50%; + margin-right: 5px; +} + +.status-running { + background-color: limegreen; +} + +.status-exited { + background-color: red; +} + +.status-loading { + background-color: orange; +} + +.status-paused { + background-color: grey; +} + +.status-black { + background-color: black; +} + +.restart-button { + margin-left: 10px; + padding: 5px 10px; + background-color: #007bff; + color: white; + border: none; + border-radius: 3px; + cursor: pointer; +} + +.restart-button:hover { + background-color: #0056b3; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.custom-spinner { + display: inline-block; + width: 2rem; + height: 2rem; + vertical-align: text-bottom; + border: 0.25em solid currentColor; + border-right-color: transparent; + border-radius: 50%; + animation: spin 0.75s linear infinite; +} + +.custom-spinner-sm { + width: 1rem; + height: 1rem; + border-width: 0.2em; +} + +.custom-spinner-lg { + width: 3rem; + height: 3rem; + border-width: 0.3em; +} diff --git a/app/static/nginxController.js b/app/static/nginxController.js new file mode 100644 index 0000000..ff9b96c --- /dev/null +++ b/app/static/nginxController.js @@ -0,0 +1,63 @@ +document.addEventListener('DOMContentLoaded', function () { + setNginxStatusCircle(); +}); + +function setNginxStatusCircle() { + const nginxStatusCircle = document.getElementById('nginx-status-circle'); + + fetch('/api/status-nginx') + .then(response => response.json()) + .then(data => { + console.log(data); + + const containerStatus = data.status; + console.log("Container status: " + containerStatus); + nginxStatusCircle.className = 'status-circle status-' + containerStatus; + return containerStatus; + }); +} + +let restartProcessComplete = true; + +function restartStatusInterval() { + const nginxStatusCircle = document.getElementById('nginx-status-circle'); + nginxStatusCircle.className = 'status-circle status-loading'; + + const intervalId = setInterval(() => { + if (restartProcessComplete) { + clearInterval(intervalId); + setNginxStatusCircle(); + } else { + if (nginxStatusCircle.className === 'status-circle status-loading') { + nginxStatusCircle.className = 'status-circle status-black'; + } else { + nginxStatusCircle.className = 'status-circle status-loading'; + } + } + }, 300); +} + + +function restartNginx() { + restartProcessComplete = false; + restartStatusInterval(); + + const restartBtn = document.getElementById('nginx-restart-btn'); + restartBtn.disabled = true; + + const restartSpinner = document.getElementById('nginx-restart-btn-spinner'); + restartSpinner.className = 'custom-spinner custom-spinner-sm'; + + + fetch('/api/restart-nginx', {method: "POST"}) + .then(response => response.json()) + .then(data => { + console.log(data); + }) + .finally(() => { + setNginxStatusCircle(); + restartProcessComplete = true; + restartSpinner.className = ''; + restartBtn.disabled = false; + }); +} \ No newline at end of file diff --git a/app/templates/index.html b/app/templates/index.html index 9109245..fa6215b 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -12,7 +12,8 @@ {{ moment.include_moment() }} - + # update custom.js to minified version + @@ -40,6 +41,15 @@ Domains + + + Nginx Status + + +