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

View file

@ -1052,6 +1052,21 @@ class UserQuestion(Question):
break 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): class NumberQuestion(Question):
argument_type = "number" argument_type = "number"
default_value = None default_value = None
@ -1204,6 +1219,7 @@ ARGUMENTS_TYPE_PARSERS = {
"boolean": BooleanQuestion, "boolean": BooleanQuestion,
"domain": DomainQuestion, "domain": DomainQuestion,
"user": UserQuestion, "user": UserQuestion,
"group": GroupQuestion,
"number": NumberQuestion, "number": NumberQuestion,
"range": NumberQuestion, "range": NumberQuestion,
"display_text": DisplayTextQuestion, "display_text": DisplayTextQuestion,
@ -1243,9 +1259,9 @@ def ask_questions_and_parse_answers(
out = [] 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")] 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) question = question_class(raw_question, context=answers)
answers[question.name] = question.ask_if_needed() answers[question.name] = question.ask_if_needed()
out.append(question) 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 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(): for key, value in self.default_properties.items():
setattr(self, key, value) setattr(self, key, value)
for key, value in properties: for key, value in properties.items():
setattr(self. key, value) 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 M = 1024 ** 2
@ -68,19 +90,16 @@ class DiskAppResource(AppResource):
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(self, *args, **kwargs) super().__init__(*args, **kwargs)
# FIXME: better error handling # FIXME: better error handling
assert self.space in sizes 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] \ if free_space_in_directory("/") <= sizes[self.space] \
or free_space_in_directory("/var") <= sizes[self.space]: or free_space_in_directory("/var") <= sizes[self.space]:
raise YunohostValidationError("Not enough disk space") # FIXME: i18n / better messaging raise YunohostValidationError("Not enough disk space") # FIXME: i18n / better messaging
def deprovision(self, context: Dict):
pass
class RamAppResource(AppResource): class RamAppResource(AppResource):
type = "ram" type = "ram"
@ -92,13 +111,13 @@ class RamAppResource(AppResource):
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(self, *args, **kwargs) super().__init__(*args, **kwargs)
# FIXME: better error handling # FIXME: better error handling
assert self.build in sizes assert self.build in sizes
assert self.runtime in sizes assert self.runtime in sizes
assert isinstance(self.include_swap, bool) assert isinstance(self.include_swap, bool)
def provision_or_update(self, context: Dict): def assert_availability(self, context: Dict):
memory = psutil.virtual_memory().available memory = psutil.virtual_memory().available
if self.include_swap: if self.include_swap:
@ -109,37 +128,78 @@ class RamAppResource(AppResource):
if memory <= max_size: if memory <= max_size:
raise YunohostValidationError("Not enough RAM/swap") # FIXME: i18n / better messaging 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 pass
class WebpathAppResource(AppResource): class RoutesAppResource(AppResource):
type = "webpath" type = "routes"
default_properties = { 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 from yunohost.app import _assert_no_conflicting_apps
# Check the url is available app_settings = self.get_app_settings()
domain = context["app_settings"]["domain"] domain = app_settings["domain"]
path = context["app_settings"]["path"] or "/" path = app_settings["path"] if not self.full_domain else "/"
_assert_no_conflicting_apps(domain, path, ignore_app=context["app"]) _assert_no_conflicting_apps(domain, path, ignore_app=self.app_id)
context["app_settings"]["path"] = path
def provision_or_update(self, context: Dict):
if context["app_action"] == "install": if context["app_action"] == "install":
pass # FIXME
# Initially, the .main permission is created with no url at all associated # 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 '/' # When the app register/books its web url, we also add the url '/'
# (meaning the root of the app, domain.tld/path/) # (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 # 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 # For more specific cases, the app is free to change / add urls or disable
# the tile using the permission helpers. # the tile using the permission helpers.
permission_url(app + ".main", url="/", sync_perm=False) #permission_create(
user_permission_update(app + ".main", show_tile=True, sync_perm=False) # self.app_id + ".main",
permission_sync_to_user() # 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): def deprovision(self, context: Dict):
del context["app_settings"]["domain"] del context["app_settings"]["domain"]
@ -177,8 +237,8 @@ class PortAppResource(AppResource):
raise NotImplementedError() raise NotImplementedError()
class UserAppResource(AppResource): class SystemuserAppResource(AppResource):
type = "user" type = "system_user"
default_properties = { default_properties = {
"username": "__APP__", "username": "__APP__",
@ -187,6 +247,12 @@ class UserAppResource(AppResource):
"groups": [] "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): def provision_or_update(self, context: str):
raise NotImplementedError() raise NotImplementedError()
@ -195,13 +261,19 @@ class UserAppResource(AppResource):
class InstalldirAppResource(AppResource): class InstalldirAppResource(AppResource):
type = "installdir" type = "install_dir"
default_properties = { default_properties = {
"dir": "/var/www/__APP__", "dir": "/var/www/__APP__", # FIXME or choose to move this elsewhere nowadays idk...
"alias": "final_path" "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): def provision_or_update(self, context: Dict):
if context["app_action"] in ["install", "restore"]: if context["app_action"] in ["install", "restore"]:
@ -218,12 +290,16 @@ class InstalldirAppResource(AppResource):
class DatadirAppResource(AppResource): class DatadirAppResource(AppResource):
type = "datadir" type = "data_dir"
default_properties = { 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): def provision_or_update(self, context: Dict):
if "datadir" not in context["app_settings"]: if "datadir" not in context["app_settings"]:
context["app_settings"]["datadir"] = self.dir context["app_settings"]["datadir"] = self.dir
@ -240,8 +316,16 @@ class DBAppResource(AppResource):
"type": "mysql" "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): def provision_or_update(self, context: str):
raise NotImplementedError() raise NotImplementedError()
def deprovision(self, context: Dict): def deprovision(self, context: Dict):
raise NotImplementedError() raise NotImplementedError()
AppResourceClassesByType = {c.type: c for c in AppResource.__subclasses__()}