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,
|
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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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__()}
|
||||||
|
|
Loading…
Add table
Reference in a new issue