doc:config: add ljf's advanced config panel doc

This commit is contained in:
axolotle 2023-10-23 19:05:23 +02:00
parent d676348d35
commit ee72d2f463
4 changed files with 329 additions and 160 deletions

View file

@ -82,3 +82,78 @@ for c in OptionClasses:
print("")
print(doc)
print("")
print(
"""
## Full example
We supposed we have an upstream app with this simple config.yml file:
```yaml
title: 'My dummy apps'
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\]`:
```toml
version = "1.0"
[main]
[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.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"
```
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.
## Overwrite config panel mechanism
All main configuration helpers are overwritable, example:
```bash
ynh_app_config_apply() {
# Stop vpn client
touch /tmp/.ynh-vpnclient-stopped
systemctl stop ynh-vpnclient
_ynh_app_config_apply
# Start vpn client
systemctl start ynh-vpnclient
rm -f /tmp/.ynh-vpnclient-stopped
}
```
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)
"""
)

View file

@ -132,83 +132,60 @@ print(
"""
----------------
## Advanced use cases
## Read and write values: the `bind` property
### `visible` & `enabled` expression evaluation
! Config panels only
Sometimes we may want to conditionaly display a message or prompt for a value, for this we have the `visible` prop.
And we may want to allow a user to trigger an action only if some condition are met, for this we have the `enabled` prop.
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.
Expressions are evaluated against a context containing previous values of the current section's options. This quite limited current design exists because on the web-admin or on the CLI we cannot guarantee that a value will be present in the form if the user queried only a single panel/section/option.
In the case of an action, the user will be shown or asked for each of the options of the section in which the button is present.
The expression has to be written in javascript (this has been designed for the web-admin first and is converted to python on the fly on the cli).
Available operators are: `==`, `!=`, `>`, `>=`, `<`, `<=`, `!`, `&&`, `||`, `+`, `-`, `*`, `/`, `%` and `match()`.
##### Examples
```toml
# simple "my_option_id" is thruthy/falsy
visible = "my_option_id"
visible = "!my_option_id"
# misc
visible = "my_value >= 10"
visible = "-(my_value + 1) < 0"
visible = "!!my_value || my_other_value"
```
For a more complete set of examples, [check the tests at the end of the file](https://github.com/YunoHost/yunohost/blob/dev/src/tests/test_questions.py).
##### match()
For more complex evaluation we can use regex matching.
```toml
[my_string]
default = "Lorem ipsum dolor et si qua met!"
[my_boolean]
type = "boolean"
visible = "my_string && match(my_string, '^Lorem [ia]psumE?')"
```
Match the content of a file.
```toml
[my_file]
type = "file"
accept = ".txt"
bind = "/etc/random/lorem.txt"
[my_boolean]
type = "boolean"
visible = "my_file && match(my_file, '^Lorem [ia]psumE?')"
```
with a file with content like:
```txt
Lorem ipsum dolor et si qua met!
```
### `bind`
Config panels only
`bind` allows us to alter the generic behavior of option's values which is: get from and set in the app `settings.yml`.
`bind` allows us to alter the default behavior of applying option's values, which is: get from and set in the app `settings.yml`.
We can:
- alter the source the value comes from with getters or binds to file.
- alter the destination with setters or binds to file.
- alter the source the value comes from wit binds to file or custom getters.
- alter the destination with binds to file or settings.
- 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
----------------
##### bind to file
### Read / write into a var of a 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`.
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]
# Do not use `file` for this since we only want to insert/save a value
type = "string"
bind = ":__FINALPATH__/config.ini"
default = ""
```
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"
```
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:
```toml
bind = "importExportRateLimiting>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`.
@ -225,142 +202,183 @@ bind = "__FINALPATH__/config.ini"
default = "key: 'value'"
```
##### bind a value inside a file
## Advanced use cases
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`).
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)
* 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
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
For all of those use cases, there are the specific getter or setter mechanism for an option!
Configuration file format supported: `yaml`, `toml`, `json`, `ini`, `env`, `php`, `python`.
The feature probably works with others formats, but should be tested carefully.
To create specific getter/setter, you first need to create a `config` script inside the `scripts` directory
Note that this feature only works with relatively simple cases such as `KEY: VALUE`, but won't properly work with complex data structures like multilin array/lists or dictionnaries.
It also doesn't work with XML format, custom config function call, php define(), …
`scripts/config`
```bash
#!/bin/bash
source /usr/share/yunohost/helpers
ynh_abort_if_errors
```toml
[panel.section.config_value]
# Do not use `file` for this since we only want to insert/save a value
type = "string"
bind = ":__FINALPATH__/config.ini"
default = ""
# Put your getter, setter, validator or action here
# Keep this last line
ynh_app_config_run $1
```
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"
```
##### Getters
### 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__`.
It has to be named after an option's `id` prepended by `get__`.
To display a custom alert message for example. We setup a base option in `config_panel.toml`.
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.)
```toml
[panel.section.alert]
type = "alert"
# bind to "null" to inform there's something in `scripts/config`
bind = "null"
# `ask` & `style` will be injected by a custom getter
[details summary="<i>Basic example : Get the login inside the first line of a file </i>" 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
}
```
Then add a custom getter that output yaml, every properties defined here will override any property previously declared.
config_panel.toml
```toml
[main.auth.login_user]
ask = "Username"
type = "string"
```
[/details]
[details summary="<i>Advanced example 1 : Display a list of available plugins</i>" class="helper-card-subtitle text-muted"]
scripts/config
```bash
get__alert() {
if [ "$whatever" ]; then
get__plugins() {
echo "choices: [$(ls $install_dir/plugins/ | tr '\n' ',')]"
}
```
config_panel.toml
```toml
[main.plugins.plugins]
ask = "Plugin to activate"
type = "tags"
choices = []
```
[/details]
[details summary="<i>Example 2 : Display the status of a VPN</i>" class="helper-card-subtitle text-muted"]
scripts/config
```bash
get__status() {
if [ -f "/sys/class/net/tun0/operstate" ] && [ "$(cat /sys/class/net/tun0/operstate)" == "up" ]
then
cat << EOF
style: success
ask: Your VPN is running :)
ask:
en: Your VPN is running :)
EOF
else
cat << EOF
style: danger
ask: Your VPN is down
ask:
en: Your VPN is down
EOF
fi
}
```
Or to inject a custom value:
config_panel.toml
```toml
[panel.section.my_hidden_value]
type = "number"
bind = "null"
# option will act as an hidden variable that can be used in context evaluation
# (ie: `visible` or `enabled`)
readonly = true
visible = false
# `default` injected by a custom getter
[main.cube.status]
ask = "Custom getter alert"
type = "alert"
style = "info"
bind = "null" # no behaviour on
```
[/details]
```bash
get__my_hidden_value() {
if [ "$whatever" ]; then
# if only a value is needed
echo "10"
else
# or if we need to override some other props
# (use `default` or `value` to inject the value)
cat << EOF
ask: Here's a number
visible: true
default: 0
EOF
fi
}
```
##### Setters
### 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__`.
```toml
[panel.section.my_value]
type = "string"
bind = "null"
ask = "gimme complex string"
```
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.
[details summary="<i>Basic example : Set the login into the first line of a file </i>" class="helper-card-subtitle text-muted"]
scripts/config
```bash
set__my_value() {
if [ -n "$my_value" ]; then
# split the string into multiple elements or idk
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
# To save the value or modified value as a setting:
ynh_app_setting_set --app=$app --key=my_value --value="$my_value"
}
```
##### Validators
config_panel.toml
```toml
[main.auth.login_user]
ask = "Username"
type = "string"
```
[/details]
#### Validation
You will often need to validate data answered by the user before to save it somewhere.
Validation can be made with regex through `pattern` argument
```toml
pattern.regexp = '^.+@.+$'
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"
```
Some other type specific argument exist like
| type | validation arguments |
| ----- | --------------------------- |
| `number`, `range` | `min`, `max`, `step` |
| `file` | `accept` |
| `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__`.
Validators allows us to return custom error messages depending on the value.
```toml
[panel.section.my_value]
type = "string"
bind = "null"
ask = "Gimme a long string"
default = "too short"
```
```bash
validate__my_value() {
if [[ "${#my_value}" -lt 12 ]]; then echo 'Too short!'; fi
validate__login_user() {
if [[ "${#login_user}" -lt 4 ]]; then echo 'Too short user login'; fi
}
```
##### Actions
### Actions
Define an option's action in a bash script `script/config`.
It has to be named after a `button`'s id prepended by `run__`.
@ -407,5 +425,61 @@ run__my_action() {
git clone "$my_repo" "$my_repo_name"
}
```
### `visible` & `enabled` expression evaluation
Sometimes we may want to conditionaly display a message or prompt for a value, for this we have the `visible` prop.
And we may want to allow a user to trigger an action only if some condition are met, for this we have the `enabled` prop.
Expressions are evaluated against a context containing previous values of the current section's options. This quite limited current design exists because on the web-admin or on the CLI we cannot guarantee that a value will be present in the form if the user queried only a single panel/section/option.
In the case of an action, the user will be shown or asked for each of the options of the section in which the button is present.
The expression has to be written in javascript (this has been designed for the web-admin first and is converted to python on the fly on the cli).
Available operators are: `==`, `!=`, `>`, `>=`, `<`, `<=`, `!`, `&&`, `||`, `+`, `-`, `*`, `/`, `%` and `match()`.
#### Examples
```toml
# simple "my_option_id" is thruthy/falsy
visible = "my_option_id"
visible = "!my_option_id"
# misc
visible = "my_value >= 10"
visible = "-(my_value + 1) < 0"
visible = "!!my_value || my_other_value"
```
For a more complete set of examples, [check the tests at the end of the file](https://github.com/YunoHost/yunohost/blob/dev/src/tests/test_questions.py).
#### match()
For more complex evaluation we can use regex matching.
```toml
[my_string]
default = "Lorem ipsum dolor et si qua met!"
[my_boolean]
type = "boolean"
visible = "my_string && match(my_string, '^Lorem [ia]psumE?')"
```
Match the content of a file.
```toml
[my_file]
type = "file"
accept = ".txt"
bind = "/etc/random/lorem.txt"
[my_boolean]
type = "boolean"
visible = "my_file && match(my_file, '^Lorem [ia]psumE?')"
```
with a file with content like:
```txt
Lorem ipsum dolor et si qua met!
```
"""
)

View file

@ -215,17 +215,36 @@ class PanelModel(ContainerModel):
class ConfigPanelModel(BaseModel):
"""
Configuration panels are descriptive format in TOML to bind settings to form items so that a user can alter some of the app's configuration without manually editing files from the command line.
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.
Those panels could also be used as interface generator to extend quickly 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. 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.
Config panels are structured as a series of panels (that renders as tabs in the web-admin) that contains a series of sections that contains options.
Options can be directly binded to settings, or captured by custom bash setters/getter, `button` Options can trigger "actions" (e.g. custom bash functions).
### 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!
- [Learn more about Options](/dev/forms) in their dedicated doc page as those are also used in app install forms.
- [Check the basic toml example](https://github.com/YunoHost/example_ynh/blob/master/config_panel.toml.example) and the [basic `scripts/config` example](https://github.com/YunoHost/example_ynh/blob/master/scripts/config)
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.
### 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
```toml
[manual.vpn.server_ip]
[advanced.dns.server_ip]
```
Indeed the real variable name is server_ip and here you have a conflict.
### Options
[Learn more about Options](/dev/forms) in their dedicated doc page as those are also used in app install forms and core config panels.
### YunoHost community examples
- [Check the basic example at the end of this doc](#basic-example)
- [Check the example_ynh app toml](https://github.com/YunoHost/example_ynh/blob/master/config_panel.toml.example) and the [basic `scripts/config` example](https://github.com/YunoHost/example_ynh/blob/master/scripts/config)
- [Check config panels of other apps](https://grep.app/search?q=version&filter[repo.pattern][0]=YunoHost-Apps&filter[lang][0]=TOML)
- [Check `scripts/config` of other apps](https://grep.app/search?q=ynh_app_config_apply&filter[repo.pattern][0]=YunoHost-Apps&filter[lang][0]=Shell)

View file

@ -374,8 +374,9 @@ class BaseOption(BaseModel):
- if the path starts with `:`, the value be saved as its id's variable/property counterpart
- this only works for first level variables/properties and simple types (no array)
- else the value will be stored as the whole content of the file
- you can use `__FINALPATH__` in your path to point to dynamic install paths
- you can use `__FINALPATH__` or `__INSTALL_DIR__` in your path to point to dynamic install paths
- FIXME are other global variables accessible?
- [refer to `bind` doc for explaination and examples](#read-and-write-values-the)
"""
type: OptionType