Add draft back end logic for services

This commit is contained in:
Théophile Diot 2024-01-30 15:33:35 +01:00
parent fa014cfef9
commit b0e5eacbbf
No known key found for this signature in database
GPG key ID: 248FEA4BAE400D06
5 changed files with 97 additions and 85 deletions

View file

@ -621,23 +621,42 @@ class Database:
if config:
config.pop("DATABASE_URI", None)
db_services = session.query(Services).with_entities(Services.id, Services.method).all()
db_ids = [service.id for service in db_services]
db_services = session.query(Services).with_entities(Services.id, Services.method, Services.is_draft).all()
db_ids: Dict[str, dict] = {service.id: {"method": service.method, "is_draft": service.is_draft} for service in db_services}
missing_ids = []
services = config.get("SERVER_NAME", [])
if isinstance(services, str):
services = services.split(" ")
if db_services:
missing_ids = [service.id for service in db_services if (service.method == method) and service.id not in services]
missing_ids = [service.id for service in db_services if service.method == method and service.id not in services]
if missing_ids:
# Remove services that are no longer in the list
session.query(Services).filter(Services.id.in_(missing_ids)).delete()
drafts = {service for service in services if config.pop(f"{service}_IS_DRAFT", "no") == "yes"}
db_drafts = {service.id for service in db_services if service.is_draft}
if db_drafts:
missing_drafts = [service.id for service in db_services if service.method == method and service.id not in drafts and service.id not in missing_ids]
if missing_drafts:
# Remove drafts that are no longer in the list
session.query(Services).filter(Services.id.in_(missing_drafts)).update({Services.is_draft: False})
for draft in drafts:
if draft not in db_drafts:
if draft not in db_ids:
to_put.append(Services(id=draft, method=method, is_draft=True))
db_ids[draft] = {"method": method, "is_draft": True}
elif method == db_ids[draft]["method"]:
session.query(Services).filter(Services.id == draft).update({Services.is_draft: True})
if config.get("MULTISITE", "no") == "yes":
global_values = []
for key, value in deepcopy(config).items():
for key, value in config.copy().items():
suffix = 0
original_key = deepcopy(key)
if self.suffix_rx.search(key):
@ -653,8 +672,8 @@ class Database:
continue
if server_name not in db_ids:
to_put.append(Services(id=server_name, method=method))
db_ids.append(server_name)
to_put.append(Services(id=server_name, method=method, is_draft=server_name in drafts))
db_ids[server_name] = {"method": method, "is_draft": server_name in drafts}
key = key.replace(f"{server_name}_", "")
setting = session.query(Settings).with_entities(Settings.default).filter_by(id=key).first()
@ -886,7 +905,7 @@ class Database:
return message
def get_config(self, methods: bool = False) -> Dict[str, Any]:
def get_config(self, methods: bool = False, with_drafts: bool = False) -> Dict[str, Any]:
"""Get the config from the database"""
with self.__db_session() as session:
config = {}
@ -922,8 +941,14 @@ class Database:
is_multisite = config.get("MULTISITE", {"value": "no"})["value"] == "yes" if methods else config.get("MULTISITE", "no") == "yes"
if with_drafts:
services = session.query(Services).with_entities(Services.id, Services.is_draft).all()
else:
services = session.query(Services).with_entities(Services.id, Services.is_draft).filter_by(is_draft=False).all()
if is_multisite:
for service in session.query(Services).with_entities(Services.id).all():
for service in services:
config[f"{service.id}_IS_DRAFT"] = "yes" if service.is_draft else "no"
checked_settings = []
for key, value in deepcopy(config).items():
original_key = key
@ -962,7 +987,7 @@ class Database:
}
)
servers = " ".join(service.id for service in session.query(Services).all())
servers = " ".join(service.id for service in services)
config["SERVER_NAME"] = servers if not methods else {"value": servers, "global": True, "method": "default"}
return config
@ -991,35 +1016,26 @@ class Database:
)
]
def get_services_settings(self, methods: bool = False) -> List[Dict[str, Any]]:
def get_services_settings(self, methods: bool = False, with_drafts: bool = False) -> List[Dict[str, Any]]:
"""Get the services' configs from the database"""
services = []
config = self.get_config(methods=methods)
with self.__db_session() as session:
service_names = [service.id for service in session.query(Services).with_entities(Services.id).all()]
for service in service_names:
service_settings = []
tmp_config = deepcopy(config)
config = self.get_config(methods=methods, with_drafts=with_drafts)
service_names = config["SERVER_NAME"]["value"].split(" ") if methods else config["SERVER_NAME"].split(" ")
for service in service_names:
service_settings = []
tmp_config = deepcopy(config)
for key, value in deepcopy(tmp_config).items():
if key.startswith(f"{service}_"):
setting = key.replace(f"{service}_", "")
service_settings.append(setting)
tmp_config[setting] = tmp_config.pop(key)
elif any(key.startswith(f"{s}_") for s in service_names):
tmp_config.pop(key)
elif key not in service_settings:
tmp_config[key] = (
{
"value": value["value"],
"global": value["global"],
"method": value["method"],
}
if methods
else value
)
for key, value in deepcopy(tmp_config).items():
if key.startswith(f"{service}_"):
setting = key.replace(f"{service}_", "")
service_settings.append(setting)
tmp_config[setting] = tmp_config.pop(key)
elif any(key.startswith(f"{s}_") for s in service_names):
tmp_config.pop(key)
elif key not in service_settings:
tmp_config[key] = {"value": value["value"], "global": value["global"], "method": value["method"]} if methods else value
services.append(tmp_config)
services.append(tmp_config)
return services

View file

@ -109,6 +109,7 @@ class Services(Base):
id = Column(String(64), primary_key=True)
method = Column(METHODS_ENUM, nullable=False)
is_draft = Column(Boolean, default=False, nullable=False)
settings = relationship("Services_settings", back_populates="service", cascade="all")
custom_configs = relationship("Custom_configs", back_populates="service", cascade="all")

View file

@ -3,8 +3,8 @@
"context": "global",
"default": "no",
"help": "Internal use : set to yes when BW is loading.",
"id": "internal-use",
"label": "internal use",
"id": "internal-use-loading",
"label": "internal use loading",
"regex": "^(yes|no)$",
"type": "check"
},
@ -288,5 +288,14 @@
"label": "Use IPv6",
"regex": "^(yes|no)$",
"type": "check"
},
"IS_DRAFT": {
"context": "multisite",
"default": "no",
"help": "Internal use : set to yes when the service is in draft mode.",
"id": "internal-use-draft",
"label": "internal use draft",
"regex": "^(yes|no)$",
"type": "check"
}
}

View file

@ -215,7 +215,7 @@ LOG_RX = re_compile(r"^(?P<date>\d+/\d+/\d+\s\d+:\d+:\d+)\s\[(?P<level>[a-z]+)\]
REVERSE_PROXY_PATH = re_compile(r"^(?P<host>https?://.{1,255}(:((6553[0-5])|(655[0-2]\d)|(65[0-4]\d{2})|(6[0-4]\d{3})|([1-5]\d{4})|([0-5]{0,5})|(\d{1,4})))?)$")
def manage_bunkerweb(method: str, *args, operation: str = "reloads"):
def manage_bunkerweb(method: str, *args, operation: str = "reloads", is_draft: bool = False):
# Do the operation
error = False
if method == "services":
@ -224,27 +224,27 @@ def manage_bunkerweb(method: str, *args, operation: str = "reloads"):
deleted = False
if operation == "new":
operation, error = app.config["CONFIG"].new_service(args[0])
operation, error = app.config["CONFIG"].new_service(args[0], is_draft=is_draft)
elif operation == "edit":
if args[1].split(" ")[0] != args[2].split(" ")[0] and service_custom_confs:
for service_custom_conf in service_custom_confs:
if listdir(service_custom_conf):
move(service_custom_conf, service_custom_conf.replace(f"{sep}{args[1].split(' ')[0]}", f"{sep}{args[2].split(' ')[0]}"))
moved = True
operation, error = app.config["CONFIG"].edit_service(args[1], args[0], check_changes=not moved)
operation, error = app.config["CONFIG"].edit_service(args[1], args[0], check_changes=not is_draft and not moved, is_draft=is_draft)
elif operation == "delete":
for service_custom_conf in glob(join(sep, "etc", "bunkerweb", "configs", "*", args[2].split(" ")[0])):
if listdir(service_custom_conf):
rmtree(service_custom_conf, ignore_errors=True)
deleted = True
operation, error = app.config["CONFIG"].delete_service(args[2], check_changes=not deleted)
operation, error = app.config["CONFIG"].delete_service(args[2], check_changes=not is_draft and not deleted)
if error:
app.config["TO_FLASH"].append({"content": operation, "type": "error"})
else:
app.config["TO_FLASH"].append({"content": operation, "type": "success"})
if moved or deleted:
if not is_draft and (moved or deleted):
changes = ["config", "custom_configs"]
error = app.config["CONFIGFILES"].save_configs(check_changes=False)
if error:
@ -693,17 +693,17 @@ def instances():
def services():
if request.method == "POST":
# Check operation
if "operation" not in request.form or request.form["operation"] not in (
"new",
"edit",
"delete",
):
if "operation" not in request.form or request.form["operation"] not in ("new", "edit", "delete"):
flash("Missing operation parameter on /services.", "error")
return redirect(url_for("loading", next=url_for("services")))
elif "is_draft" not in request.form or request.form["is_draft"] not in ("yes", "no"):
flash("Missing is_draft parameter on /services.", "error")
return redirect(url_for("loading", next=url_for("services")))
# Check variables
variables = deepcopy(request.form.to_dict())
del variables["csrf_token"]
is_draft = variables.pop("is_draft") == "yes"
if "OLD_SERVER_NAME" not in request.form and request.form["operation"] == "edit":
flash("Missing OLD_SERVER_NAME parameter.", "error")
@ -717,7 +717,7 @@ def services():
del variables["OLD_SERVER_NAME"]
# Edit check fields and remove already existing ones
config = app.config["CONFIG"].get_config(methods=False)
config = app.config["CONFIG"].get_config(methods=False, with_drafts=True)
server_name = variables["SERVER_NAME"].split(" ")[0]
for variable, value in deepcopy(variables).items():
if variable.endswith("SCHEMA"):
@ -732,11 +732,8 @@ def services():
if variable in variables and variable != "SERVER_NAME" and value == config.get(f"{server_name}_{variable}" if request.form["operation"] == "edit" else variable, None):
del variables[variable]
if request.form["operation"] == "edit" and len(variables) == 1 and "SERVER_NAME" in variables and variables["SERVER_NAME"] == request.form.get("OLD_SERVER_NAME", ""):
flash(
"The service was not edited because no values were changed.",
"error",
)
if (config.get(f"{server_name}_IS_DRAFT", "no") == "yes") == is_draft and request.form["operation"] == "edit" and len(variables) == 1 and "SERVER_NAME" in variables and variables["SERVER_NAME"] == request.form.get("OLD_SERVER_NAME", ""):
flash("The service was not edited because no values were changed.", "error")
return redirect(url_for("loading", next=url_for("services")))
elif request.form["operation"] == "new" and not variables:
flash("The service was not created because all values had the default value.", "error")
@ -767,22 +764,22 @@ def services():
target=manage_bunkerweb,
name="Reloading instances",
args=("services", variables, request.form.get("OLD_SERVER_NAME", ""), variables.get("SERVER_NAME", "")),
kwargs={"operation": request.form["operation"]},
kwargs={"operation": request.form["operation"], "is_draft": is_draft},
).start()
message = ""
if request.form["operation"] == "new":
message = f"Creating service {variables.get('SERVER_NAME', '').split(' ')[0]}"
message = f"Creating {'draft ' if is_draft else ''}service {variables.get('SERVER_NAME', '').split(' ')[0]}"
elif request.form["operation"] == "edit":
message = f"Saving configuration for service {request.form.get('OLD_SERVER_NAME', '').split(' ')[0]}"
message = f"Saving configuration for {'draft ' if is_draft else ''}service {request.form.get('OLD_SERVER_NAME', '').split(' ')[0]}"
elif request.form["operation"] == "delete":
message = f"Deleting service {request.form.get('SERVER_NAME', '').split(' ')[0]}"
message = f"Deleting {'draft ' if is_draft else ''}service {request.form.get('SERVER_NAME', '').split(' ')[0]}"
return redirect(url_for("loading", next=url_for("services"), message=message))
# Display services
services = app.config["CONFIG"].get_services()
services = app.config["CONFIG"].get_services(with_drafts=True)
return render_template(
"services.html",
services=[
@ -792,6 +789,7 @@ def services():
"full_value": service["SERVER_NAME"]["value"],
"method": service["SERVER_NAME"]["method"],
},
"IS_DRAFT": service.pop("IS_DRAFT", "no"),
"USE_REVERSE_PROXY": service["USE_REVERSE_PROXY"],
"SERVE_FILES": service["SERVE_FILES"],
"REMOTE_PHP": service["REMOTE_PHP"],

View file

@ -98,7 +98,7 @@ class Config:
def get_settings(self) -> dict:
return self.__settings
def get_config(self, methods: bool = True) -> dict:
def get_config(self, methods: bool = True, with_drafts: bool = False) -> dict:
"""Get the nginx variables env file and returns it as a dict
Returns
@ -106,9 +106,9 @@ class Config:
dict
The nginx variables env file as a dict
"""
return self.__db.get_config(methods=methods)
return self.__db.get_config(methods=methods, with_drafts=with_drafts)
def get_services(self, methods: bool = True) -> list[dict]:
def get_services(self, methods: bool = True, with_drafts: bool = False) -> list[dict]:
"""Get nginx's services
Returns
@ -116,7 +116,7 @@ class Config:
list
The services
"""
return self.__db.get_services_settings(methods=methods)
return self.__db.get_services_settings(methods=methods, with_drafts=with_drafts)
def check_variables(self, variables: dict, _global: bool = False) -> int:
"""Testify that the variables passed are valid
@ -163,7 +163,7 @@ class Config:
def reload_config(self) -> None:
self.__gen_conf(self.get_config(methods=False), self.get_services(methods=False))
def new_service(self, variables: dict) -> Tuple[str, int]:
def new_service(self, variables: dict, is_draft: bool = False) -> Tuple[str, int]:
"""Creates a new service from the given variables
Parameters
@ -181,23 +181,17 @@ class Config:
Exception
raise this if the service already exists
"""
services = self.get_services(methods=False)
services = self.get_services(methods=False, with_drafts=True)
server_name_splitted = variables["SERVER_NAME"].split(" ")
for service in services:
if service["SERVER_NAME"] == variables["SERVER_NAME"] or service["SERVER_NAME"] in server_name_splitted:
return (
f"Service {service['SERVER_NAME'].split(' ')[0]} already exists.",
1,
)
return f"Service {service['SERVER_NAME'].split(' ')[0]} already exists.", 1
services.append(variables)
self.__gen_conf(self.get_config(methods=False), services)
return (
f"Configuration for {variables['SERVER_NAME'].split(' ')[0]} has been generated.",
0,
)
services.append(variables | {"IS_DRAFT": "yes" if is_draft else "no"})
self.__gen_conf(self.get_config(methods=False), services, check_changes=not is_draft)
return f"Configuration for {variables['SERVER_NAME'].split(' ')[0]} has been generated.", 0
def edit_service(self, old_server_name: str, variables: dict, *, check_changes: bool = True) -> Tuple[str, int]:
def edit_service(self, old_server_name: str, variables: dict, *, check_changes: bool = True, is_draft: bool = False) -> Tuple[str, int]:
"""Edits a service
Parameters
@ -212,22 +206,19 @@ class Config:
str
the confirmation message
"""
services = self.get_services(methods=False)
services = self.get_services(methods=False, with_drafts=True)
changed_server_name = old_server_name != variables["SERVER_NAME"]
server_name_splitted = variables["SERVER_NAME"].split(" ")
old_server_name_splitted = old_server_name.split(" ")
for i, service in enumerate(deepcopy(services)):
if service["SERVER_NAME"] == variables["SERVER_NAME"] or service["SERVER_NAME"] in server_name_splitted:
if changed_server_name and service["SERVER_NAME"].split(" ")[0] != old_server_name_splitted[0]:
return (
f"Service {service['SERVER_NAME'].split(' ')[0]} already exists.",
1,
)
return f"Service {service['SERVER_NAME'].split(' ')[0]} already exists.", 1
services.pop(i)
elif changed_server_name and (service["SERVER_NAME"] == old_server_name or service["SERVER_NAME"] in old_server_name_splitted):
services.pop(i)
services.append(variables)
services.append(variables | {"IS_DRAFT": "yes" if is_draft else "no"})
config = self.get_config(methods=False)
if changed_server_name and server_name_splitted[0] != old_server_name_splitted[0]:
@ -236,10 +227,7 @@ class Config:
config.pop(k)
self.__gen_conf(config, services, check_changes=check_changes)
return (
f"Configuration for {old_server_name_splitted[0]} has been edited.",
0,
)
return f"Configuration for {old_server_name_splitted[0]} has been edited.", 0
def edit_global_conf(self, variables: dict) -> str:
"""Edits the global conf
@ -277,7 +265,7 @@ class Config:
"""
service_name = service_name.split(" ")[0]
full_env = self.get_config(methods=False)
services = self.get_services(methods=False)
services = self.get_services(methods=False, with_drafts=True)
new_services = []
found = False