Absolutely yolo iteration on app resources draft

This commit is contained in:
Alexandre Aubin 2021-11-03 16:36:38 +01:00
parent 8a71fae732
commit 0a750b7b61
3 changed files with 160 additions and 52 deletions

View file

@ -59,6 +59,7 @@ from yunohost.utils.config import (
DomainQuestion,
PathQuestion,
)
from yunohost.utils.resources import AppResourceSet
from yunohost.utils.i18n import _value_for_locale
from yunohost.utils.error import YunohostError, YunohostValidationError
from yunohost.utils.filesystem import free_space_in_directory
@ -803,6 +804,7 @@ def app_install(
confirm_install(app)
manifest, extracted_app_folder = _extract_app(app)
packaging_format = manifest["packaging_format"]
# Check ID
if "id" not in manifest or "__" in manifest["id"] or "." in manifest["id"]:
@ -827,7 +829,7 @@ def app_install(
app_instance_name = app_id
# Retrieve arguments list for install script
raw_questions = manifest.get("arguments", {}).get("install", {})
raw_questions = manifest["install"]
questions = ask_questions_and_parse_answers(raw_questions, prefilled_answers=args)
args = {
question.name: question.value
@ -836,11 +838,12 @@ def app_install(
}
# Validate domain / path availability for webapps
path_requirement = _guess_webapp_path_requirement(extracted_app_folder)
_validate_webpath_requirement(args, path_requirement)
if packaging_format < 2:
path_requirement = _guess_webapp_path_requirement(extracted_app_folder)
_validate_webpath_requirement(args, path_requirement)
# Attempt to patch legacy helpers ...
_patch_legacy_helpers(extracted_app_folder)
# Attempt to patch legacy helpers ...
_patch_legacy_helpers(extracted_app_folder)
# Apply dirty patch to make php5 apps compatible with php7
_patch_legacy_php_versions(extracted_app_folder)
@ -870,7 +873,6 @@ def app_install(
}
# If packaging_format v2+, save all install questions as settings
packaging_format = int(manifest.get("packaging_format", 0))
if packaging_format >= 2:
for arg_name, arg_value in args.items():
@ -891,17 +893,23 @@ def app_install(
recursive=True,
)
# Initialize the main permission for the app
# The permission is initialized with no url associated, and with tile disabled
# For web app, the root path of the app will be added as url and the tile
# will be enabled during the app install. C.f. 'app_register_url()' below.
permission_create(
app_instance_name + ".main",
allowed=["all_users"],
label=label,
show_tile=False,
protected=False,
)
resources = AppResourceSet(manifest["resources"], app_instance_name)
resources.check_availability()
resources.provision()
if packaging_format < 2:
# Initialize the main permission for the app
# The permission is initialized with no url associated, and with tile disabled
# For web app, the root path of the app will be added as url and the tile
# will be enabled during the app install. C.f. 'app_register_url()' below.
permission_create(
app_instance_name + ".main",
allowed=["all_users"],
label=label,
show_tile=False,
protected=False,
)
# Prepare env. var. to pass to script
env_dict = _make_environment_for_app_script(
@ -2354,13 +2362,13 @@ def _guess_webapp_path_requirement(app_folder: str) -> str:
# is an available url and normalize the path.
manifest = _get_manifest_of_app(app_folder)
raw_questions = manifest.get("arguments", {}).get("install", {})
raw_questions = manifest["install"]
domain_questions = [
question for question in raw_questions if question.get("type") == "domain"
question for question in raw_questions.values() if question.get("type") == "domain"
]
path_questions = [
question for question in raw_questions if question.get("type") == "path"
question for question in raw_questions.values() if question.get("type") == "path"
]
if len(domain_questions) == 0 and len(path_questions) == 0:

View file

@ -1052,6 +1052,21 @@ class UserQuestion(Question):
break
class GroupQuestion(Question):
argument_type = "group"
def __init__(self, question, context: Mapping[str, Any] = {}):
from yunohost.user import user_group_list
super().__init__(question, context)
self.choices = list(user_group_list(short=True)["groups"])
if self.default is None:
self.default = "all_users"
class NumberQuestion(Question):
argument_type = "number"
default_value = None
@ -1204,6 +1219,7 @@ ARGUMENTS_TYPE_PARSERS = {
"boolean": BooleanQuestion,
"domain": DomainQuestion,
"user": UserQuestion,
"group": GroupQuestion,
"number": NumberQuestion,
"range": NumberQuestion,
"display_text": DisplayTextQuestion,
@ -1243,9 +1259,9 @@ def ask_questions_and_parse_answers(
out = []
for raw_question in raw_questions:
for name, raw_question in raw_questions.items():
question_class = ARGUMENTS_TYPE_PARSERS[raw_question.get("type", "string")]
raw_question["value"] = answers.get(raw_question["name"])
raw_question["value"] = answers.get(name)
question = question_class(raw_question, context=answers)
answers[question.name] = question.ask_if_needed()
out.append(question)

View file

@ -27,15 +27,37 @@ from yunohost.utils.error import YunohostError, YunohostValidationError
from yunohost.utils.filesystem import free_space_in_directory
class AppResource:
class AppResource(object):
def __init__(self, properties: Dict[str, Any], app_id: str, app_settings):
def __init__(self, properties: Dict[str, Any], app_id: str):
self.app_id = app_id
for key, value in self.default_properties.items():
setattr(self, key, value)
for key, value in properties:
setattr(self. key, value)
for key, value in properties.items():
setattr(self, key, value)
def get_app_settings(self):
from yunohost.app import _get_app_settings
return _get_app_settings(self.app_id)
def check_availability(self, context: Dict):
pass
class AppResourceSet:
def __init__(self, resources_dict: Dict[str, Dict[str, Any]], app_id: str):
self.set = {name: AppResourceClassesByType[name](infos, app_id)
for name, infos in resources_dict.items()}
def check_availability(self):
for name, resource in self.set.items():
resource.check_availability(context={})
M = 1024 ** 2
@ -68,19 +90,16 @@ class DiskAppResource(AppResource):
}
def __init__(self, *args, **kwargs):
super().__init__(self, *args, **kwargs)
super().__init__(*args, **kwargs)
# FIXME: better error handling
assert self.space in sizes
def provision_or_update(self, context: Dict):
def assert_availability(self, context: Dict):
if free_space_in_directory("/") <= sizes[self.space] \
or free_space_in_directory("/var") <= sizes[self.space]:
raise YunohostValidationError("Not enough disk space") # FIXME: i18n / better messaging
def deprovision(self, context: Dict):
pass
class RamAppResource(AppResource):
type = "ram"
@ -92,13 +111,13 @@ class RamAppResource(AppResource):
}
def __init__(self, *args, **kwargs):
super().__init__(self, *args, **kwargs)
super().__init__(*args, **kwargs)
# FIXME: better error handling
assert self.build in sizes
assert self.runtime in sizes
assert isinstance(self.include_swap, bool)
def provision_or_update(self, context: Dict):
def assert_availability(self, context: Dict):
memory = psutil.virtual_memory().available
if self.include_swap:
@ -109,37 +128,78 @@ class RamAppResource(AppResource):
if memory <= max_size:
raise YunohostValidationError("Not enough RAM/swap") # FIXME: i18n / better messaging
def deprovision(self, context: Dict):
class AptDependenciesAppResource(AppResource):
type = "apt"
default_properties = {
"packages": [],
"extras": {}
}
def check_availability(self, context):
# ? FIXME
# call helpers idk ...
pass
class SourcesAppResource(AppResource):
type = "sources"
default_properties = {
"main": {"url": "?", "sha256sum": "?", "predownload": True}
}
def check_availability(self, context):
# ? FIXME
# call request.head on the url idk
pass
class WebpathAppResource(AppResource):
type = "webpath"
class RoutesAppResource(AppResource):
type = "routes"
default_properties = {
"url": "__DOMAIN____PATH__"
"full_domain": False,
"main": {
"url": "/",
"additional_urls": [],
"init_allowed": "__FIXME__",
"show_tile": True,
"protected": False,
"auth_header": True,
"label": "FIXME",
}
}
def provision_or_update(self, context: Dict):
def check_availability(self, context):
from yunohost.app import _assert_no_conflicting_apps
# Check the url is available
domain = context["app_settings"]["domain"]
path = context["app_settings"]["path"] or "/"
_assert_no_conflicting_apps(domain, path, ignore_app=context["app"])
context["app_settings"]["path"] = path
app_settings = self.get_app_settings()
domain = app_settings["domain"]
path = app_settings["path"] if not self.full_domain else "/"
_assert_no_conflicting_apps(domain, path, ignore_app=self.app_id)
def provision_or_update(self, context: Dict):
if context["app_action"] == "install":
pass # FIXME
# Initially, the .main permission is created with no url at all associated
# When the app register/books its web url, we also add the url '/'
# (meaning the root of the app, domain.tld/path/)
# and enable the tile to the SSO, and both of this should match 95% of apps
# For more specific cases, the app is free to change / add urls or disable
# the tile using the permission helpers.
permission_url(app + ".main", url="/", sync_perm=False)
user_permission_update(app + ".main", show_tile=True, sync_perm=False)
permission_sync_to_user()
#permission_create(
# self.app_id + ".main",
# allowed=["all_users"],
# label=label,
# show_tile=False,
# protected=False,
#)
#permission_url(app + ".main", url="/", sync_perm=False)
#user_permission_update(app + ".main", show_tile=True, sync_perm=False)
#permission_sync_to_user()
def deprovision(self, context: Dict):
del context["app_settings"]["domain"]
@ -177,8 +237,8 @@ class PortAppResource(AppResource):
raise NotImplementedError()
class UserAppResource(AppResource):
type = "user"
class SystemuserAppResource(AppResource):
type = "system_user"
default_properties = {
"username": "__APP__",
@ -187,6 +247,12 @@ class UserAppResource(AppResource):
"groups": []
}
def check_availability(self, context):
if os.system(f"getent passwd {self.username} &>/dev/null") != 0:
raise YunohostValidationError(f"User {self.username} already exists")
if os.system(f"getent group {self.username} &>/dev/null") != 0:
raise YunohostValidationError(f"Group {self.username} already exists")
def provision_or_update(self, context: str):
raise NotImplementedError()
@ -195,13 +261,19 @@ class UserAppResource(AppResource):
class InstalldirAppResource(AppResource):
type = "installdir"
type = "install_dir"
default_properties = {
"dir": "/var/www/__APP__",
"dir": "/var/www/__APP__", # FIXME or choose to move this elsewhere nowadays idk...
"alias": "final_path"
}
# FIXME: change default dir to /opt/stuff if app ain't a webapp ...
def check_availability(self, context):
if os.path.exists(self.dir):
raise YunohostValidationError(f"Folder {self.dir} already exists")
def provision_or_update(self, context: Dict):
if context["app_action"] in ["install", "restore"]:
@ -218,12 +290,16 @@ class InstalldirAppResource(AppResource):
class DatadirAppResource(AppResource):
type = "datadir"
type = "data_dir"
default_properties = {
"dir": "/home/yunohost.app/__APP__",
"dir": "/home/yunohost.app/__APP__", # FIXME or choose to move this elsewhere nowadays idk...
}
def check_availability(self, context):
if os.path.exists(self.dir):
raise YunohostValidationError(f"Folder {self.dir} already exists")
def provision_or_update(self, context: Dict):
if "datadir" not in context["app_settings"]:
context["app_settings"]["datadir"] = self.dir
@ -240,8 +316,16 @@ class DBAppResource(AppResource):
"type": "mysql"
}
def check_availability(self, context):
# FIXME : checking availability sort of imply that mysql / postgresql is installed
# or we gotta make sure mariadb-server or postgresql is gonna be installed (apt resource)
pass
def provision_or_update(self, context: str):
raise NotImplementedError()
def deprovision(self, context: Dict):
raise NotImplementedError()
AppResourceClassesByType = {c.type: c for c in AppResource.__subclasses__()}