diff --git a/Dockerfile b/Dockerfile index 1173d01..b6536d9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,16 @@ -FROM python:3.7-alpine +FROM python:3.11.2-slim -ADD requirements.txt . +WORKDIR /app -RUN apk add python3-dev build-base linux-headers pcre-dev && pip install --no-cache-dir -r requirements.txt +COPY . /app -# adding application files -ADD . /webapp +RUN pip install --upgrade pip -# configure path /webapp to HOME-dir -ENV HOME /webapp -WORKDIR /webapp +RUN pip install --no-cache-dir -r requirements.txt -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 +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..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 -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 +27,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 +47,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 +85,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 +118,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 +143,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 +170,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 +193,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. @@ -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..8266f64 --- /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'; + } + } + }, 100); +} + + +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..2128eb2 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -13,6 +13,7 @@ {{ moment.include_moment() }} + @@ -40,6 +41,15 @@ Domains + + + Nginx Status + + +