mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
Absolutely yolo iteration on app resources draft
This commit is contained in:
parent
8a71fae732
commit
0a750b7b61
3 changed files with 160 additions and 52 deletions
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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__()}
|
||||
|
|
Loading…
Add table
Reference in a new issue