diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index f44647ef5..39c62398c 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -544,6 +544,31 @@ app: full: --file help: Folder or tarball for upgrade + ### app_change_url() + change-url: + action_help: Change app's URL + api: PUT /apps//changeurl + configuration: + authenticate: all + authenticator: ldap-anonymous + lock: false + arguments: + app: + help: Target app instance name + -d: + full: --domain + help: New app domain on which the application will be moved + extra: + ask: ask_main_domain + pattern: *pattern_domain + required: True + -p: + full: --path + help: New path at which the application will be moved + extra: + ask: ask_path + required: True + ### app_setting() setting: action_help: Set or get an app setting value diff --git a/locales/en.json b/locales/en.json index 9b5acd87f..2e85d6d4b 100644 --- a/locales/en.json +++ b/locales/en.json @@ -8,6 +8,11 @@ "app_argument_choice_invalid": "Invalid choice for argument '{name:s}', it must be one of {choices:s}", "app_argument_invalid": "Invalid value for argument '{name:s}': {error:s}", "app_argument_required": "Argument '{name:s}' is required", + "app_change_no_change_url_script": "The application {app_name:s} doesn't support changing it's URL yet, you might need to upgrade it.", + "app_change_url_failed_nginx_reload": "Failed to reload nginx. Here is the output of 'nginx -t':\n{nginx_errors:s}", + "app_change_url_identical_domains": "The old and new domain/url_path are identical ('{domain:s}{path:s}'), nothing to do.", + "app_change_url_no_script": "This application '{app_name:s}' doesn't support url modification yet. Maybe you should upgrade the application.", + "app_change_url_success": "Successfully changed {app:s} url to {domain:s}{path:s}", "app_extraction_failed": "Unable to extract installation files", "app_id_invalid": "Invalid app id", "app_incompatible": "The app is incompatible with your YunoHost version", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index f25d35b72..10f617228 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -41,7 +41,7 @@ from collections import OrderedDict from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger -from yunohost.service import service_log +from yunohost.service import service_log, _run_service_command from yunohost.utils import packages logger = getActionLogger('yunohost.app') @@ -419,6 +419,105 @@ def app_map(app=None, raw=False, user=None): return result +def app_change_url(auth, app, domain, path): + """ + Modify the URL at which an application is installed. + + Keyword argument: + app -- Taget app instance name + domain -- New app domain on which the application will be moved + path -- New path at which the application will be move + + """ + from yunohost.hook import hook_exec + + installed = _is_installed(app) + if not installed: + raise MoulinetteError(errno.ENOPKG, + m18n.n('app_not_installed', app=app)) + + if not os.path.exists(os.path.join(APPS_SETTING_PATH, app, "scripts", "change_url")): + raise MoulinetteError(errno.EINVAL, m18n.n("app_change_no_change_url_script", app_name=app)) + + old_domain = app_setting(app, "domain") + old_path = app_setting(app, "path") + + # Normalize path and domain format + domain = domain.strip().lower() + old_path = '/' + old_path.strip("/").strip() + '/' + path = '/' + path.strip("/").strip() + '/' + + if (domain, path) == (old_domain, old_path): + raise MoulinetteError(errno.EINVAL, m18n.n("app_change_url_identical_domains", domain=domain, path=path)) + + # WARNING / FIXME : checkurl will modify the settings + # (this is a non intuitive behavior that should be changed) + # (or checkurl renamed in reserve_url) + app_checkurl(auth, '%s%s' % (domain, path), app) + + manifest = json.load(open(os.path.join(APPS_SETTING_PATH, app, "manifest.json"))) + + # Retrieve arguments list for change_url script + # TODO: Allow to specify arguments + args_odict = _parse_args_from_manifest(manifest, 'change_url', auth=auth) + args_list = args_odict.values() + args_list.append(app) + + # Prepare env. var. to pass to script + env_dict = _make_environment_dict(args_odict) + app_id, app_instance_nb = _parse_app_instance_name(app) + env_dict["YNH_APP_ID"] = app_id + env_dict["YNH_APP_INSTANCE_NAME"] = app + env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) + + env_dict["YNH_APP_OLD_DOMAIN"] = old_domain + env_dict["YNH_APP_OLD_PATH"] = old_path.rstrip("/") + env_dict["YNH_APP_NEW_DOMAIN"] = domain + env_dict["YNH_APP_NEW_PATH"] = path.rstrip("/") + + if os.path.exists(os.path.join(APP_TMP_FOLDER, "scripts")): + shutil.rmtree(os.path.join(APP_TMP_FOLDER, "scripts")) + + shutil.copytree(os.path.join(APPS_SETTING_PATH, app, "scripts"), + os.path.join(APP_TMP_FOLDER, "scripts")) + + # Execute App change_url script + os.system('chown -R admin: %s' % INSTALL_TMP) + os.system('chmod +x %s' % os.path.join(os.path.join(APP_TMP_FOLDER, "scripts"))) + os.system('chmod +x %s' % os.path.join(os.path.join(APP_TMP_FOLDER, "scripts", "change_url"))) + + # XXX journal + if hook_exec(os.path.join(APP_TMP_FOLDER, 'scripts/change_url'), args=args_list, env=env_dict) != 0: + logger.error("Failed to change '%s' url." % app) + + # restore values modified by app_checkurl + # see begining of the function + app_setting(app, "domain", value=old_domain) + app_setting(app, "path", value=old_path) + + return + + # this should idealy be done in the change_url script but let's avoid common mistakes + app_setting(app, 'domain', value=domain) + app_setting(app, 'path', value=path) + + app_ssowatconf(auth) + + # avoid common mistakes + if _run_service_command("reload", "nginx") == False: + # grab nginx errors + # the "exit 0" is here to avoid check_output to fail because 'nginx -t' + # will return != 0 since we are in a failed state + nginx_errors = subprocess.check_output("nginx -t; exit 0", + stderr=subprocess.STDOUT, + shell=True).rstrip() + + raise MoulinetteError(errno.EINVAL, m18n.n("app_change_url_failed_nginx_reload", nginx_errors=nginx_errors)) + + logger.success(m18n.n("app_change_url_success", + app=app, domain=domain, path=path)) + + def app_upgrade(auth, app=[], url=None, file=None): """ Upgrade app diff --git a/src/yunohost/tests/test_changeurl.py b/src/yunohost/tests/test_changeurl.py new file mode 100644 index 000000000..506060cbb --- /dev/null +++ b/src/yunohost/tests/test_changeurl.py @@ -0,0 +1,61 @@ +import pytest +import time +import requests + +from moulinette.core import init_authenticator +from yunohost.app import app_install, app_change_url, app_remove, app_map +from yunohost.domain import _get_maindomain + +from moulinette.core import MoulinetteError + +# Instantiate LDAP Authenticator +AUTH_IDENTIFIER = ('ldap', 'ldap-anonymous') +AUTH_PARAMETERS = {'uri': 'ldap://localhost:389', 'base_dn': 'dc=yunohost,dc=org'} + +auth = init_authenticator(AUTH_IDENTIFIER, AUTH_PARAMETERS) + +# Get main domain +maindomain = _get_maindomain() + + +def setup_function(function): + pass + + +def teardown_function(function): + app_remove(auth, "change_url_app") + + +def install_changeurl_app(path): + app_install(auth, "./tests/apps/change_url_app_ynh", + args="domain=%s&path=%s" % (maindomain, path)) + + +def check_changeurl_app(path): + appmap = app_map(raw=True) + + assert path + "/" in appmap[maindomain].keys() + + assert appmap[maindomain][path + "/"]["id"] == "change_url_app" + + r = requests.get("https://%s%s/" % (maindomain, path)) + assert r.status_code == 200 + + +def test_appchangeurl(): + install_changeurl_app("/changeurl") + check_changeurl_app("/changeurl") + + app_change_url(auth, "change_url_app", maindomain, "/newchangeurl") + + # For some reason the nginx reload can take some time to propagate ...? + time.sleep(2) + + check_changeurl_app("/newchangeurl") + +def test_appchangeurl_sameurl(): + install_changeurl_app("/changeurl") + check_changeurl_app("/changeurl") + + with pytest.raises(MoulinetteError): + app_change_url(auth, "change_url_app", maindomain, "changeurl")