diff --git a/doc/generate_configpanel_doc.py b/doc/generate_configpanel_doc.py index e29a80dbc..1eb7b5ebb 100644 --- a/doc/generate_configpanel_doc.py +++ b/doc/generate_configpanel_doc.py @@ -88,41 +88,41 @@ print( """ ## Full example -We supposed we have an upstream app with this simple config.yml file: +Let's imagine that the upstream app is configured using this simple `config.yml` file stored in the app's install directory (typically `/var/www/$app/config.yml`): ```yaml -title: 'My dummy apps' +title: 'My dummy app' theme: 'white' max_rate: 10 max_age: 365 ``` -We could for example create a simple configuration panel for it like this one, by following the syntax `\[PANEL.SECTION.QUESTION\]`: +We could for example create a simple configuration panel for it like this one, by following the syntax `[PANEL.SECTION.QUESTION]`: ```toml version = "1.0" [main] -[main.main] -[main.main.title] -ask.en = "Title" -type = "string" -bind = ":__INSTALL_DIR__/config.yml" + [main.main] + [main.main.title] + ask.en = "Title" + type = "string" + bind = ":__INSTALL_DIR__/config.yml" -[main.main.theme] -ask.en = "Theme" -type = "select" -choices = ["white", "dark"] -bind = ":__INSTALL_DIR__/config.yml" + [main.main.theme] + ask.en = "Theme" + type = "select" + choices = ["white", "dark"] + bind = ":__INSTALL_DIR__/config.yml" -[main.limits] -[main.limits.max_rate] -ask.en = "Maximum display rate" -type = "number" -bind = ":__INSTALL_DIR__/config.yml" + [main.limits] + [main.limits.max_rate] + ask.en = "Maximum display rate" + type = "number" + bind = ":__INSTALL_DIR__/config.yml" -[main.limits.max_age] -ask.en = "Duration of a dummy" -type = "number" -bind = ":__INSTALL_DIR__/config.yml" + [main.limits.max_age] + ask.en = "Duration of a dummy" + type = "number" + bind = ":__INSTALL_DIR__/config.yml" ``` Here we have created one `main` panel, containing the `main` and `limits` sections, containing questions according to params name of our `config.yml` file. Thanks to the `bind` properties, all those questions are bind to their values in the `config.yml` file. @@ -147,13 +147,13 @@ ynh_app_config_apply() { } ``` -List of main configuration helpers -* ynh_app_config_get -* ynh_app_config_show -* ynh_app_config_validate -* ynh_app_config_apply -* ynh_app_config_run +List of main configuration helpers: + * `ynh_app_config_get` + * `ynh_app_config_show` + * `ynh_app_config_validate` + * `ynh_app_config_apply` + * `ynh_app_config_run` -More info on this could be found by reading [vpnclient_ynh config script](https://github.com/YunoHost-Apps/vpnclient_ynh/blob/master/scripts/config) +More info on this can be found by reading [vpnclient_ynh config script](https://github.com/YunoHost-Apps/vpnclient_ynh/blob/master/scripts/config) """ ) diff --git a/doc/generate_options_doc.py b/doc/generate_options_doc.py index 88f6deb20..ea7febe6d 100644 --- a/doc/generate_options_doc.py +++ b/doc/generate_options_doc.py @@ -132,62 +132,70 @@ print( """ ---------------- -## Read and write values: the `bind` property +## Reading and writing values ! Config panels only You can read and write values with 2 mechanisms: the `bind` property in the `config_panel.toml` and for complex use cases the getter/setter in a `config` script. -`bind` allows us to alter the default behavior of applying option's values, which is: get from and set in the app `settings.yml`. +If you did not define a specific getter/setter (see below), and no `bind` argument was defined, YunoHost will read/write the value from/to the app's `/etc/yunohost/$app/settings.yml` file. -We can: +With `bind`, we can: - alter the source the value comes from with binds to file or custom getters. -- alter the destination with binds to file or settings. +- alter the destination with binds to file or custom setters. - parse/validate the value before destination with validators -! IMPORTANT: with the exception of `bind = "null"` options, options ids should almost **always** correspond to an app setting initialized / reused during install/upgrade. +! IMPORTANT: with the exception of `bind = "null"` options, options ids should almost **always** correspond to an app setting initialized/reused during install/upgrade. Not doing so may result in inconsistencies between the config panel mechanism and the use of ynh_add_config -### Read / write into a var of a configuration file +### Read / write into a var of an actual configuration file Settings usually correspond to key/values in actual app configurations. Hence, a more useful mode is to have `bind = ":FILENAME"` with a colon `:` before. In that case, YunoHost will automagically find a line with `KEY=VALUE` in `FILENAME` (with the adequate separator between `KEY` and `VALUE`). YunoHost will then use this value for the read/get operation. During write/set operations, YunoHost will overwrite the value in **both** FILENAME and in the app's settings.yml -Configuration file format supported: `yaml`, `toml`, `json`, `ini`, `env`, `php`, `python`. +Configuration file format supported: `YAML`, `TOML`, `JSON`, `INI`, `PHP`, `.env`-like, `.py`. The feature probably works with others formats, but should be tested carefully. -Note that this feature only works with relatively simple cases such as `KEY: VALUE`, but won't properly work with complex data structures like multiline array/lists or dictionnaries. -It also doesn't work with XML format, custom config function call, php define(), … -If you need to save complex/multiline content in a configuration variable, you should do it via a specific getter/setter. - ```toml -[panel.section.config_value] +[main.main.theme] # Do not use `file` for this since we only want to insert/save a value type = "string" -bind = ":__FINALPATH__/config.ini" -default = "" +bind = ":__INSTALL_DIR__/config.yml" +``` +In which case, YunoHost will look for something like a key/value, with the key being `theme`. + +If the question id in the config panel (here, `theme`) differs from the key in the actual conf file (let's say it's not `theme` but `css_theme`), then you can write: +```toml +[main.main.theme] +type = "string" +bind = "css_theme:__FINALPATH__/config.yml" ``` -By default, `bind = ":FILENAME"` will use the option id as `KEY` but the option id may sometime not be the exact same `KEY` name in the configuration file. -For example, [In pepettes app](https://github.com/YunoHost-Apps/pepettes_ynh/blob/5cc2d3ffd6529cc7356ff93af92dbb6785c3ab9a/conf/settings.py##L11), the python variable is `name` and not `project_name`. In that case, the key name can be specified before the colon `:`. - -```toml -[panel.section.project_name] -bind = "name:__FINALPATH__/config.ini" +!!!! Note: This mechanism is quasi language agnostic and will use regexes to find something that looks like a key=value or common variants. However, it does assume that the key and value are stored on the same line. It doesn't support multiline text or file in a variable with this method. If you need to save multiline content in a configuration variable, you should create a custom getter/setter (see below). +Nested syntax is also supported, which may be useful for example to remove ambiguities about stuff looking like: +```json +{ + "foo": { + "max": 123 + }, + "bar": { + "max": 456 + } +} ``` -Sometimes, you want to read and save a value in a variable name that appears several time in the configuration file (for example variables called `max`). The `bind` property allows you to change the value on the variable following a regex in a the file: +which we can `bind` to using: ```toml -bind = "importExportRateLimiting>max:__INSTALL_DIR__/conf.json" +bind = "foo>max:__INSTALL_DIR__/conf.json" ``` ### Read / write an entire file -You can bind a `text` or directly a `file` to a specific file by using `bind = "FILEPATH`. +Useful when using a question `file` or `text` for which you want to save the raw content directly as a file on the system. ```toml [panel.section.config_file] @@ -207,17 +215,14 @@ default = "key: 'value'" Sometimes the `bind` mechanism is not enough: * the config file format is not supported (e.g. xml, csv) * the data is not contained in a config file (e.g. database, directory, web resources...) - * the data should be writen but not read (e.g. password) - * the data should be read but not writen (e.g. status information) + * the data should be written but not read (e.g. password) + * the data should be read but not written (e.g. fetching status information) * we want to change other things than the value (e.g. the choices list of a select) * the question answer contains several values to dispatch in several places * and so on -For all of those use cases, there are the specific getter or setter mechanism for an option! +You can create specific getter/setters functions inside the `scripts/config` of your app to customize how the information is read/written. -To create specific getter/setter, you first need to create a `config` script inside the `scripts` directory - -`scripts/config` ```bash #!/bin/bash source /usr/share/yunohost/helpers @@ -232,54 +237,65 @@ ynh_app_config_run $1 ### Getters -Define an option's custom getter in a bash script `script/config`. -It has to be named after an option's `id` prepended by `get__`. +A question's getter is the function used to read the current value/state. Custom getters are defined using bash functions called `getter__QUESTION_SHORT_KEY()` which returns data through stdout. -The function should returns one of these two formats: - * a raw format, in this case the return is binded directly to the value of the question - * a yaml format, in this case you can rewrite several properties of your option (like the `style` of an `alert`, the list of `choices` of a `select`, etc.) +Stdout can generated using one of those formats: + 1) either a raw format, in which case the return is binded directly to the value of the question + 2) or a yaml format, in this case you dynamically provide properties for your question (for example the `style` of an `alert`, the list of available `choices` of a `select`, etc.) -[details summary="Basic example : Get the login inside the first line of a file " class="helper-card-subtitle text-muted"] -scripts/config -```bash -get__login_user() { - if [ -s /etc/openvpn/keys/credentials ] - then - echo "$(sed -n 1p /etc/openvpn/keys/credentials)" - else - echo "" - fi -} +[details summary="Basic example with raw stdout: get the timezone on the system" class="helper-card-subtitle text-muted"] + +`config_panel.toml` + +```toml +[main.main.timezone] +ask = "Timezone" +type = "string" ``` -config_panel.toml -```toml -[main.auth.login_user] -ask = "Username" -type = "string" +`scripts/config` + +```bash +get__timezone() { + echo "$(cat /etc/timezone)" +} ``` [/details] -[details summary="Advanced example 1 : Display a list of available plugins" class="helper-card-subtitle text-muted"] -scripts/config -```bash -get__plugins() { - echo "choices: [$(ls $install_dir/plugins/ | tr '\n' ',')]" -} -``` +[details summary="Basic example with yaml-formated stdout : Display a list of available plugins" class="helper-card-subtitle text-muted"] -config_panel.toml +`config_panel.toml` ```toml [main.plugins.plugins] ask = "Plugin to activate" type = "tags" choices = [] ``` + +`scripts/config` + +```bash +get__plugins() { + echo "choices: [$(ls $install_dir/plugins/ | tr '\n' ',')]" +} +``` + [/details] -[details summary="Example 2 : Display the status of a VPN" class="helper-card-subtitle text-muted"] -scripts/config +[details summary="Advanced example with yaml-formated stdout : Display the status of a VPN" class="helper-card-subtitle text-muted"] + +`config_panel.toml` + +```toml +[main.cube.status] +ask = "Custom getter alert" +type = "alert" +style = "info" +bind = "null" # no behaviour on +``` + +`scripts/config` ```bash get__status() { if [ -f "/sys/class/net/tun0/operstate" ] && [ "$(cat /sys/class/net/tun0/operstate)" == "up" ] @@ -298,50 +314,38 @@ EOF fi } ``` - -config_panel.toml -```toml -[main.cube.status] -ask = "Custom getter alert" -type = "alert" -style = "info" -bind = "null" # no behaviour on -``` [/details] ### Setters -Define an option's custom setter in a bash script `script/config`. -It has to be named after an option's id prepended by `set__`. +A question's setter is the function used to set new value/state. Custom setters are defined using bash functions called `setter__QUESTION_SHORT_KEY()`. In the context of the setter function, variables named with the various quetion's short keys are avaible ... for example the user-specified date for question `[main.main.theme]` is available as `$theme`. -This function could access new values defined by the users by using bash variable with the same name as the short key of a question. - -You probably should use `ynh_print_info` in order to display info for user about change that has been made to help them to understand a bit what's going. +When doing non-trivial operations to set a value, you may want to use `ynh_print_info` to inform the admin about what's going on. -[details summary="Basic example : Set the login into the first line of a file " class="helper-card-subtitle text-muted"] -scripts/config -```bash -set__login_user() { - if [ -z "${login_user}" ] - then - echo "${login_user}" > /etc/openvpn/keys/credentials - ynh_print_info "The user login has been registered in /etc/openvpn/keys/credentials" - fi -} +[details summary="Basic example : Set the system timezone" class="helper-card-subtitle text-muted"] + +`config_panel.toml` + +```toml +[main.main.timezone] +ask = "Timezone" +type = "string" ``` -config_panel.toml -```toml -[main.auth.login_user] -ask = "Username" -type = "string" +`scripts/config` + +```bash +set__timezone() { + echo "$timezone" > /etc/timezone + ynh_print_info "The timezone has been changed to $timezone" +} ``` [/details] -#### Validation +### Validation You will often need to validate data answered by the user before to save it somewhere. @@ -353,9 +357,9 @@ pattern.error = 'An email is required for this field' You can also restrict several types with a choices list. ```toml -choices.option1 = "Plop1" -choices.option2 = "Plop2" -choices.option3 = "Plop3" +choices.foo = "Foo (some explanation)" +choices.bar = "Bar (moar explanation)" +choices.loremipsum = "Lorem Ipsum Dolor Sit Amet" ``` Some other type specific argument exist like @@ -366,15 +370,12 @@ Some other type specific argument exist like | `boolean` | `yes` `no` | -If you need more control over validation, you can use custom validators. -Define an option's custom validator in a bash script `script/config`. -It has to be named after an option's id prepended by `validate__`. - +Finally, if you need specific or multi variable validation, you can use custom validators function. Validators allows us to return custom error messages depending on the value. ```bash validate__login_user() { - if [[ "${#login_user}" -lt 4 ]]; then echo 'Too short user login'; fi + if [[ "${#login_user}" -lt 4 ]]; then echo 'User login is too short, should be at least 4 chars'; fi } ``` diff --git a/src/utils/configpanel.py b/src/utils/configpanel.py index 4f333cc5a..47c97a808 100644 --- a/src/utils/configpanel.py +++ b/src/utils/configpanel.py @@ -217,30 +217,30 @@ class PanelModel(ContainerModel): class ConfigPanelModel(BaseModel): """ - Configuration panels allows instances adminitrators to manage some parameters or runs some actions for which the app's upstream doesn't provide any configuration panels itself. It's a good way to reduce manual change on config files and avoid conflicts on it. + Configuration panels allows admins to manage parameters or runs actions for which the upstream's app doesn't provide any appropriate UI itself. It's a good way to reduce manual change on config files and avoid conflicts on it. - Those panels could also be used as interface generator to extend quickly capabilities of YunoHost (e.g. VPN Client, Hotspost, Borg, etc.). + Those panels can also be used to quickly create interfaces that extend the capabilities of YunoHost (e.g. VPN Client, Hotspost, Borg, etc.). From a packager perspective, this `config_panel.toml` is coupled to the `scripts/config` script, which may be used to define custom getters/setters/validations/actions. However, most use cases should be covered automagically by the core, thus it may not be necessary to define a scripts/config at all! - ! IMPORTANT: Please: Keep in mind the YunoHost spirit, and try to build your panels in such a way as to expose only really useful parameters, and if there are many of them, to relegate those corresponding to rarer use cases to "Advanced" sub-sections. + ! Please: Keep in mind the YunoHost spirit, and try to build your panels in such a way as to expose only really useful, "high-level" parameters, and if there are many of them, to relegate those corresponding to rarer use cases to "Advanced" sub-sections. Keep it simple, focus on common needs, don't expect the admins to have 3 PhDs in computer science. - ### How does `config_panel.toml` work - Basically, configuration panels for apps uses at least a `config_panel.toml` at the root of your package. For advanced usecases, this TOML file could also be paired with a `scripts/config` to define custom getters/setters/validators/actions. However, most use cases should be covered automagically by the core, thus it may not be necessary to define a `scripts/config` at all! + ### `config_panel.toml`'s principle and general format + To create configuration panels for apps, you should at least create a `config_panel.toml` at the root of the package. For more complex cases, this TOML file can be paired with a `config` script inside the scripts directory of your package, which will handle specific controller logic. - The `config_panel.toml` file describes one or several panels, containing some sections, containing some options generally binded to a params in a configuration file. + The `config_panel.toml` describes one or several panels, containing sections, each containing questions generally binded to a params in the app's actual configuration files. ### Options short keys have to be unique - For performance reasons, questions short keys should be unique in all the `config_panel.toml` file, not just inside its panel or its section. - - So you can't have + For performance reasons, questions short keys have to be unique in all the `config_panel.toml` file, not just inside its panel or its section. Hence it's not possible to have: ```toml [manual.vpn.server_ip] [advanced.dns.server_ip] ``` - Indeed the real variable name is server_ip and here you have a conflict. + In which two questions have "real variable name" `is server_ip` and therefore conflict with each other. - ### Options + ! Some short keys are forbidden cause it can interfer with config scripts (`old`, `file_hash`, `types`, `binds`, `formats`, `changed`) and you probably should avoid to use common settings name to avoid to bind your question to this settings (e.g. `id`, `install_time`, `mysql_pwd`, `path`, `domain`, `port`, `db_name`, `current_revision`, `admin`) + + ### Supported questions types and properties [Learn more about Options](/dev/forms) in their dedicated doc page as those are also used in app install forms and core config panels.