Update UI to automatically set SCRIPT_NAME and ABSOLUTE_URI

This commit is contained in:
Théophile Diot 2023-06-01 19:33:09 -04:00
parent b27958a19c
commit 9829ef7525
No known key found for this signature in database
GPG Key ID: E752C80DB72BB014
3 changed files with 168 additions and 51 deletions

View File

@ -11,7 +11,7 @@ from os.path import basename, dirname, join
from pathlib import Path
from re import compile as re_compile
from sys import _getframe, path as sys_path
from typing import Any, Dict, List, Optional, Tuple, Union
from typing import Any, Dict, List, Literal, Optional, Tuple, Union
from time import sleep
from traceback import format_exc
@ -55,7 +55,13 @@ install_as_MySQLdb()
class Database:
def __init__(self, logger: Logger, sqlalchemy_string: Optional[str] = None) -> None:
def __init__(
self,
logger: Logger,
sqlalchemy_string: Optional[str] = None,
*,
ui: bool = False,
) -> None:
"""Initialize the database"""
self.__logger = logger
self.__sql_session = None
@ -67,10 +73,14 @@ class Database:
)
if sqlalchemy_string.startswith("sqlite"):
with suppress(FileExistsError):
Path(dirname(sqlalchemy_string.split("///")[1])).mkdir(
parents=True, exist_ok=True
)
if ui:
while not Path(sep, "var", "lib", "bunkerweb", "db.sqlite3"):
sleep(1)
else:
with suppress(FileExistsError):
Path(dirname(sqlalchemy_string.split("///")[1])).mkdir(
parents=True, exist_ok=True
)
elif "+" in sqlalchemy_string and "+pymysql" not in sqlalchemy_string:
splitted = sqlalchemy_string.split("+")
sqlalchemy_string = f"{splitted[0]}:{':'.join(splitted[1].split(':')[1:])}"
@ -254,31 +264,44 @@ class Database:
return ""
def check_changes(self) -> Union[Dict[str, bool], str]:
def check_changes(
self, _type: Union[Literal["scheduler"], Literal["ui"]] = "scheduler"
) -> Union[Dict[str, bool], bool, str]:
"""Check if either the config, the custom configs or plugins have changed inside the database"""
with self.__db_session() as session:
try:
metadata = (
session.query(Metadata)
.with_entities(
if _type == "scheduler":
entities = (
Metadata.custom_configs_changed,
Metadata.external_plugins_changed,
Metadata.config_changed,
)
else:
entities = (Metadata.ui_config_changed,)
metadata = (
session.query(Metadata)
.with_entities(*entities)
.filter_by(id=1)
.first()
)
return dict(
custom_configs_changed=metadata is not None
and metadata.custom_configs_changed,
external_plugins_changed=metadata is not None
and metadata.external_plugins_changed,
config_changed=metadata is not None and metadata.config_changed,
)
if _type == "scheduler":
return dict(
custom_configs_changed=metadata is not None
and metadata.custom_configs_changed,
external_plugins_changed=metadata is not None
and metadata.external_plugins_changed,
config_changed=metadata is not None and metadata.config_changed,
)
else:
return metadata is not None and metadata.ui_config_changed
except BaseException:
return format_exc()
def checked_changes(self) -> str:
def checked_changes(
self, _type: Union[Literal["scheduler"], Literal["ui"]] = "scheduler"
) -> str:
"""Set that the config, the custom configs and the plugins didn't change"""
with self.__db_session() as session:
try:
@ -287,9 +310,12 @@ class Database:
if not metadata:
return "The metadata are not set yet, try again"
metadata.config_changed = False
metadata.custom_configs_changed = False
metadata.external_plugins_changed = False
if _type == "scheduler":
metadata.config_changed = False
metadata.custom_configs_changed = False
metadata.external_plugins_changed = False
else:
metadata.ui_config_changed = False
session.commit()
except BaseException:
return format_exc()
@ -658,6 +684,7 @@ class Database:
if not metadata.first_config_saved:
metadata.first_config_saved = True
metadata.config_changed = bool(to_put)
metadata.ui_config_changed = bool(to_put)
try:
session.add_all(to_put)

View File

@ -282,5 +282,6 @@ class Metadata(Base):
custom_configs_changed = Column(Boolean, default=False, nullable=True)
external_plugins_changed = Column(Boolean, default=False, nullable=True)
config_changed = Column(Boolean, default=False, nullable=True)
ui_config_changed = Column(Boolean, default=False, nullable=True)
integration = Column(INTEGRATIONS_ENUM, default="Unknown", nullable=False)
version = Column(String(32), default="1.5.0", nullable=False)

View File

@ -1,12 +1,14 @@
#!/usr/bin/python3
from os import _exit, getenv, getpid, listdir, sep
from os import _exit, environ, getenv, listdir, sep
from os.path import basename, dirname, join
from sys import path as sys_path, modules as sys_modules
from pathlib import Path
os_release_path = Path(sep, "etc", "os-release")
if os_release_path.is_file() and "Alpine" not in os_release_path.read_text():
if os_release_path.is_file() and "Alpine" not in os_release_path.read_text(
encoding="utf-8"
):
sys_path.append(join(sep, "usr", "share", "bunkerweb", "deps", "python"))
del os_release_path
@ -18,7 +20,7 @@ for deps_path in [
if deps_path not in sys_path:
sys_path.append(deps_path)
from gevent import monkey
from gevent import monkey, spawn
monkey.patch_all()
@ -96,10 +98,10 @@ def stop_gunicorn():
call(["kill", "-SIGTERM", pid])
def stop(status, stop=True):
def stop(status, _stop=True):
Path(sep, "var", "run", "bunkerweb", "ui.pid").unlink(missing_ok=True)
Path(sep, "var", "tmp", "bunkerweb", "ui.healthy").unlink(missing_ok=True)
if stop is True:
if _stop is True:
stop_gunicorn()
_exit(status)
@ -127,10 +129,7 @@ app.wsgi_app = ReverseProxied(app.wsgi_app)
# Set variables and instantiate objects
vars = get_variables()
if "ABSOLUTE_URI" not in vars:
logger.error("ABSOLUTE_URI is not set")
stop(1)
elif "ADMIN_USERNAME" not in vars:
if "ADMIN_USERNAME" not in vars:
logger.error("ADMIN_USERNAME is not set")
stop(1)
elif "ADMIN_PASSWORD" not in vars:
@ -146,14 +145,6 @@ if not vars.get("FLASK_DEBUG", False) and not regex_match(
)
stop(1)
if not vars["ABSOLUTE_URI"].endswith("/"):
vars["ABSOLUTE_URI"] += "/"
if not vars.get("FLASK_DEBUG", False) and vars["ABSOLUTE_URI"].endswith("/changeme/"):
logger.error("Please change the default URL.")
stop(1)
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = "login"
@ -167,33 +158,44 @@ PLUGIN_KEYS = [
"settings",
]
integration = "Linux"
INTEGRATION = "Linux"
integration_path = Path(sep, "usr", "share", "bunkerweb", "INTEGRATION")
if getenv("KUBERNETES_MODE", "no").lower() == "yes":
integration = "Kubernetes"
INTEGRATION = "Kubernetes"
elif getenv("SWARM_MODE", "no").lower() == "yes":
integration = "Swarm"
INTEGRATION = "Swarm"
elif getenv("AUTOCONF_MODE", "no").lower() == "yes":
integration = "Autoconf"
INTEGRATION = "Autoconf"
elif integration_path.is_file():
integration = integration_path.read_text().strip()
INTEGRATION = integration_path.read_text(encoding="utf-8").strip()
del integration_path
docker_client = None
kubernetes_client = None
if integration in ("Docker", "Swarm", "Autoconf"):
if INTEGRATION in ("Docker", "Swarm", "Autoconf"):
try:
docker_client: DockerClient = DockerClient(
base_url=vars.get("DOCKER_HOST", "unix:///var/run/docker.sock")
)
except (docker_APIError, DockerException):
logger.warning("No docker host found")
elif integration == "Kubernetes":
elif INTEGRATION == "Kubernetes":
kube_config.load_incluster_config()
kubernetes_client = kube_client.CoreV1Api()
db = Database(logger)
db = Database(logger, ui=True)
if INTEGRATION in (
"Swarm",
"Kubernetes",
"Autoconf",
):
while not db.is_autoconf_loaded():
logger.warning(
"Autoconf is not loaded yet in the database, retrying in 5s ...",
)
sleep(5)
while not db.is_initialized():
logger.warning(
@ -209,6 +211,8 @@ while not db.is_first_config_saved() or not env:
sleep(5)
env = db.get_config()
del env
logger.info("Database is ready")
Path(sep, "var", "tmp", "bunkerweb", "ui.healthy").write_text("ok", encoding="utf-8")
bw_version = (
@ -217,16 +221,101 @@ bw_version = (
.strip()
)
ABSOLUTE_URI = vars.get("ABSOLUTE_URI")
CONFIG = Config(db)
def update_config():
global ABSOLUTE_URI
ret = db.checked_changes("ui")
if ret:
logger.error(
f"An error occurred when setting the changes to checked in the database : {ret}"
)
stop(1)
ssl = False
server_name = None
endpoint = None
for service in CONFIG.get_services():
if service.get("USE_UI", "no") == "no":
continue
server_name = service.get("SERVER_NAME", {"value": None})["value"]
endpoint = service.get("REVERSE_PROXY_URL", {"value": "/"})["value"]
logger.warning(service.get("AUTO_LETS_ENCRYPT", {"value": "no"}))
logger.warning(service.get("GENERATE_SELF_SIGNED_SSL", {"value": "no"}))
logger.warning(service.get("USE_CUSTOM_SSL", {"value": "no"}))
if any(
[
service.get("AUTO_LETS_ENCRYPT", {"value": "no"})["value"] == "yes",
service.get("GENERATE_SELF_SIGNED_SSL", {"value": "no"})["value"]
== "yes",
service.get("USE_CUSTOM_SSL", {"value": "no"})["value"] == "yes",
]
):
ssl = True
break
if not server_name:
logger.error("No service found with USE_UI=yes")
stop(1)
ABSOLUTE_URI = f"http{'s' if ssl else ''}://{server_name}{endpoint}"
SCRIPT_NAME = f"/{basename(ABSOLUTE_URI[:-1] if ABSOLUTE_URI.endswith('/') and ABSOLUTE_URI != '/' else ABSOLUTE_URI)}"
if not ABSOLUTE_URI.endswith("/"):
ABSOLUTE_URI += "/"
if ABSOLUTE_URI != app.config.get("ABSOLUTE_URI"):
app.config["ABSOLUTE_URI"] = ABSOLUTE_URI
app.config["SESSION_COOKIE_DOMAIN"] = server_name
logger.info(f"The ABSOLUTE_URI is now {ABSOLUTE_URI}")
else:
logger.info(f"The ABSOLUTE_URI is still {ABSOLUTE_URI}")
if SCRIPT_NAME != getenv("SCRIPT_NAME"):
environ["SCRIPT_NAME"] = f"/{basename(ABSOLUTE_URI[:-1])}"
logger.info(f"The script name is now {environ['SCRIPT_NAME']}")
else:
logger.info(f"The script name is still {environ['SCRIPT_NAME']}")
def check_config_changes():
while True:
changes = db.check_changes("ui")
if isinstance(changes, str):
continue
if changes:
logger.info(
"Config changed in the database, updating ABSOLUTE_URI and SCRIPT_NAME ..."
)
update_config()
sleep(1)
update_config()
spawn(check_config_changes)
try:
app.config.update(
DEBUG=True,
SECRET_KEY=vars["FLASK_SECRET"],
ABSOLUTE_URI=vars["ABSOLUTE_URI"],
INSTANCES=Instances(docker_client, kubernetes_client, integration),
INSTANCES=Instances(docker_client, kubernetes_client, INTEGRATION),
CONFIG=Config(db),
CONFIGFILES=ConfigFiles(logger, db),
SESSION_COOKIE_DOMAIN=vars["ABSOLUTE_URI"]
.replace("http://", "")
SESSION_COOKIE_DOMAIN=ABSOLUTE_URI.replace("http://", "")
.replace("https://", "")
.split("/")[0],
WTF_CSRF_SSL_STRICT=False,
@ -1346,7 +1435,7 @@ def logs_container(container_id):
tmp_logs = []
if docker_client:
try:
if integration != "Swarm":
if INTEGRATION != "Swarm":
docker_logs = docker_client.containers.get(container_id).logs(
stdout=True,
stderr=True,