summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexander Strizhakov <alex.strizhakov@gmail.com>2020-07-16 19:57:27 +0300
committerAlexander Strizhakov <alex.strizhakov@gmail.com>2021-03-16 19:27:24 +0300
commitbe02684579118e0cda0eb44d779116d332a0a1dd (patch)
tree8805852e37270cbc3e1ff777ad6cd123bf214054
parenta2aa309496eb98625b47cfb26858e1eee3f9b0cd (diff)
config versioning
- added DynamicSupervisor, which starts Pleroma deps and restarts config dependent deps - added versioning for in database config. New version is created from changes which are passed to config update/delete endpoint. Every version contains backup with all changes added through update. Versioning supports rollbacks with N steps. With a rollback, all versions that come after the version on which the rollback was made are deleted.
-rw-r--r--.gitignore5
-rw-r--r--CHANGELOG.md24
-rw-r--r--config/description.exs133
-rw-r--r--config/test.exs4
-rw-r--r--docs/administration/CLI_tasks/config.md22
-rw-r--r--docs/development/API/admin_api.md217
-rw-r--r--docs/development/config_versioning.md40
-rw-r--r--lib/mix/pleroma.ex53
-rw-r--r--lib/mix/tasks/pleroma/config.ex284
-rw-r--r--lib/mix/tasks/pleroma/docs.ex2
-rw-r--r--lib/pleroma/application.ex239
-rw-r--r--lib/pleroma/application/chat_supervisor.ex19
-rw-r--r--lib/pleroma/application/config_dependent_deps.ex244
-rw-r--r--lib/pleroma/application/environment.ex103
-rw-r--r--lib/pleroma/application/requirements.ex (renamed from lib/pleroma/application_requirements.ex)47
-rw-r--r--lib/pleroma/application/start_up_dependencies.ex182
-rw-r--r--lib/pleroma/config/converter.ex195
-rw-r--r--lib/pleroma/config/deprecation_warnings.ex46
-rw-r--r--lib/pleroma/config/loader.ex70
-rw-r--r--lib/pleroma/config/oban.ex38
-rw-r--r--lib/pleroma/config/transfer_task.ex201
-rw-r--r--lib/pleroma/config/version.ex25
-rw-r--r--lib/pleroma/config/versioning.ex292
-rw-r--r--lib/pleroma/config_db.ex434
-rw-r--r--lib/pleroma/docs/json.ex2
-rw-r--r--lib/pleroma/ecto_type/config/atom.ex4
-rw-r--r--lib/pleroma/ecto_type/config/binary_value.ex4
-rw-r--r--lib/pleroma/gopher/server.ex9
-rw-r--r--lib/pleroma/gun/gun_supervisor.ex19
-rw-r--r--lib/pleroma/http/hackney_supervisor.ex30
-rw-r--r--lib/pleroma/web/admin_api/controllers/admin_api_controller.ex4
-rw-r--r--lib/pleroma/web/admin_api/controllers/config_controller.ex169
-rw-r--r--lib/pleroma/web/admin_api/views/config_view.ex28
-rw-r--r--lib/pleroma/web/api_spec/operations/admin/config_operation.ex54
-rw-r--r--lib/pleroma/web/router.ex2
-rw-r--r--mix.exs6
-rw-r--r--priv/repo/migrations/20200716195806_autolinker_to_linkify.exs2
-rw-r--r--priv/repo/migrations/20200825061316_move_activity_expirations_to_oban.exs2
-rw-r--r--priv/repo/migrations/20200907092050_move_tokens_expiration_into_oban.exs2
-rw-r--r--priv/repo/migrations/20201007131955_add_config_version.exs14
-rw-r--r--priv/repo/migrations/20201007154420_change_key_in_config.exs49
-rw-r--r--priv/repo/migrations/20201101162751_combine_settings_without_key.exs29
-rw-r--r--priv/repo/migrations/20201102064359_create_base_config_version.exs15
-rw-r--r--restarter/lib/pleroma.ex94
-rw-r--r--restarter/lib/restarter.ex8
-rw-r--r--restarter/mix.exs21
-rw-r--r--test/fixtures/config/temp.secret.exs24
-rw-r--r--test/mix/tasks/pleroma/config_test.exs178
-rw-r--r--test/pleroma/application/config_dependent_deps_test.exs149
-rw-r--r--test/pleroma/application/environment_test.exs248
-rw-r--r--test/pleroma/application/requirements_test.exs (renamed from test/pleroma/application_requirements_test.exs)59
-rw-r--r--test/pleroma/config/converter_test.exs432
-rw-r--r--test/pleroma/config/loader_test.exs40
-rw-r--r--test/pleroma/config/transfer_task_test.exs120
-rw-r--r--test/pleroma/config/versioning_test.exs414
-rw-r--r--test/pleroma/config_db_test.exs538
-rw-r--r--test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs28
-rw-r--r--test/pleroma/web/admin_api/controllers/config_controller_test.exs636
-rw-r--r--test/support/data_case.ex18
-rw-r--r--test/support/factory.ex9
60 files changed, 4228 insertions, 2151 deletions
diff --git a/.gitignore b/.gitignore
index f30f4cf5f..5de548308 100644
--- a/.gitignore
+++ b/.gitignore
@@ -56,4 +56,7 @@ pleroma.iml
# Editor temp files
/*~
-/*# \ No newline at end of file
+/*#
+
+# local iex
+.iex.exs
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 87eb7b48f..2809773f5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,7 +6,29 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased
+### Added
+
- The `application` metadata returned with statuses is no longer hardcoded. Apps that want to display these details will now have valid data for new posts after this change.
+- Config Versioning.
+
+<details>
+ <summary>API Changes</summary>
+
+- Admin API: (`GET /api/pleroma/admin/config/versions`) - endpoint to get list of config versions.
+- Admin API: (`GET /api/pleroma/admin/config/versions/rollback/:id`) - endpoint to rollback config to specific version.
+
+</details>
+
+### Changed
+
+- Improved hashtag timeline performance (requires a background migration).
+
+<details>
+ <summary>API Changes</summary>
+
+- **Breaking**: AdminAPI configs can be without key parameter.
+
+</details>
## Unreleased (Patch)
@@ -50,7 +72,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Pleroma API: Reroute `/api/pleroma/*` to `/api/v1/pleroma/*`
</details>
-- Improved hashtag timeline performance (requires a background migration).
### Added
@@ -506,7 +527,6 @@ switched to a new configuration mechanism, however it was not officially removed
- Static-FE: Fix remote posts not being sanitized
### Fixed
-=======
- Rate limiter crashes when there is no explicitly specified ip in the config
- 500 errors when no `Accept` header is present if Static-FE is enabled
- Instance panel not being updated immediately due to wrong `Cache-Control` headers
diff --git a/config/description.exs b/config/description.exs
index 41e5e4056..886cd7ba6 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -1062,65 +1062,63 @@ config :pleroma, :config_description, [
description:
"Where logs will be sent, :console - send logs to stdout, { ExSyslogger, :ex_syslogger } - to syslog, Quack.Logger - to Slack.",
suggestions: [:console, {ExSyslogger, :ex_syslogger}, Quack.Logger]
- }
- ]
- },
- %{
- group: :logger,
- type: :group,
- key: :ex_syslogger,
- label: "ExSyslogger",
- description: "ExSyslogger-related settings",
- children: [
- %{
- key: :level,
- type: {:dropdown, :atom},
- description: "Log level",
- suggestions: [:debug, :info, :warn, :error]
- },
- %{
- key: :ident,
- type: :string,
- description:
- "A string that's prepended to every message, and is typically set to the app name",
- suggestions: ["pleroma"]
- },
- %{
- key: :format,
- type: :string,
- description: "Default: \"$date $time [$level] $levelpad$node $metadata $message\"",
- suggestions: ["$metadata[$level] $message"]
- },
- %{
- key: :metadata,
- type: {:list, :atom},
- suggestions: [:request_id]
- }
- ]
- },
- %{
- group: :logger,
- type: :group,
- key: :console,
- label: "Console Logger",
- description: "Console logger settings",
- children: [
- %{
- key: :level,
- type: {:dropdown, :atom},
- description: "Log level",
- suggestions: [:debug, :info, :warn, :error]
},
%{
- key: :format,
- type: :string,
- description: "Default: \"$date $time [$level] $levelpad$node $metadata $message\"",
- suggestions: ["$metadata[$level] $message"]
+ key: :ex_syslogger,
+ type: :keyword,
+ label: "ExSyslogger",
+ description: "ExSyslogger-related settings",
+ children: [
+ %{
+ key: :level,
+ type: {:dropdown, :atom},
+ description: "Log level",
+ suggestions: [:debug, :info, :warn, :error]
+ },
+ %{
+ key: :ident,
+ type: :string,
+ description:
+ "A string that's prepended to every message, and is typically set to the app name",
+ suggestions: ["pleroma"]
+ },
+ %{
+ key: :format,
+ type: :string,
+ description: "Default: \"$date $time [$level] $levelpad$node $metadata $message\"",
+ suggestions: ["$metadata[$level] $message"]
+ },
+ %{
+ key: :metadata,
+ type: {:list, :atom},
+ suggestions: [:request_id]
+ }
+ ]
},
%{
- key: :metadata,
- type: {:list, :atom},
- suggestions: [:request_id]
+ key: :console,
+ type: :keyword,
+ label: "Console Logger",
+ description: "Console logger settings",
+ children: [
+ %{
+ key: :level,
+ type: {:dropdown, :atom},
+ description: "Log level",
+ suggestions: [:debug, :info, :warn, :error]
+ },
+ %{
+ key: :format,
+ type: :string,
+ description: "Default: \"$date $time [$level] $levelpad$node $metadata $message\"",
+ suggestions: ["$metadata[$level] $message"]
+ },
+ %{
+ key: :metadata,
+ type: {:list, :atom},
+ suggestions: [:request_id]
+ }
+ ]
}
]
},
@@ -1840,19 +1838,13 @@ config :pleroma, :config_description, [
},
%{
group: :pleroma,
+ key: :admin_token,
label: "Pleroma Admin Token",
- type: :group,
+ type: :string,
description:
"Allows setting a token that can be used to authenticate requests with admin privileges without a normal user account token. Append the `admin_token` parameter to requests to utilize it. (Please reconsider using HTTP Basic Auth or OAuth-based authentication if possible)",
- children: [
- %{
- key: :admin_token,
- type: :string,
- description: "Admin token",
- suggestions: [
- "Please use a high entropy string or UUID"
- ]
- }
+ suggestions: [
+ "Please use a high entropy string or UUID"
]
},
%{
@@ -2153,16 +2145,11 @@ config :pleroma, :config_description, [
},
%{
group: :pleroma,
+ key: Pleroma.Web.Auth.Authenticator,
label: "Pleroma Authenticator",
- type: :group,
+ type: :module,
description: "Authenticator",
- children: [
- %{
- key: Pleroma.Web.Auth.Authenticator,
- type: :module,
- suggestions: [Pleroma.Web.Auth.PleromaAuthenticator, Pleroma.Web.Auth.LDAPAuthenticator]
- }
- ]
+ suggestions: [Pleroma.Web.Auth.PleromaAuthenticator, Pleroma.Web.Auth.LDAPAuthenticator]
},
%{
group: :pleroma,
diff --git a/config/test.exs b/config/test.exs
index 87396a88d..80aa89f78 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -91,7 +91,9 @@ config :pleroma, Pleroma.ScheduledActivity,
total_user_limit: 3,
enabled: false
-config :pleroma, :rate_limit, %{}
+# Hack to drop default settings from `config.exs`, because keywords are deeply merged, so there is no other way to do it.
+config :pleroma, :rate_limit, nil
+config :pleroma, :rate_limit, []
config :pleroma, :http_security, report_uri: "https://endpoint.com"
diff --git a/docs/administration/CLI_tasks/config.md b/docs/administration/CLI_tasks/config.md
index 000ed4d98..973552522 100644
--- a/docs/administration/CLI_tasks/config.md
+++ b/docs/administration/CLI_tasks/config.md
@@ -2,7 +2,7 @@
{! backend/administration/CLI_tasks/general_cli_task_info.include !}
-## Transfer config from file to DB.
+## Transfer config from file to DB
!!! note
You need to add the following to your config before executing this command:
@@ -150,4 +150,24 @@ This forcibly removes all saved values in the database.
```sh
mix pleroma.config [--force] reset
+
+## Rollback config version
+
+!!! note
+ You need to add the following to your config before executing this command:
+
+ ```elixir
+ config :pleroma, configurable_from_database: true
+ ```
+
+Rollback will restore last backup by default. If you want to restore older version use `-s` parameter.
+
+=== "OTP"
+ ```sh
+ ./bin/pleroma_ctl config rollback [-s 2]
+ ```
+
+=== "From Source"
+ ```sh
+ mix pleroma.config rollback [-s 2]
```
diff --git a/docs/development/API/admin_api.md b/docs/development/API/admin_api.md
index 8f855d251..05c07571d 100644
--- a/docs/development/API/admin_api.md
+++ b/docs/development/API/admin_api.md
@@ -282,7 +282,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- *optional* `with_reblogs`: `true`/`false` – allows to see reblogs (default is false)
- Response:
- On failure: `Not found`
- - On success: JSON, where:
+ - On success: JSON, where:
- `total`: total count of the statuses for the user
- `activities`: list of the statuses for the user
@@ -339,7 +339,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
Params: none
Response:
-* On success: JSON array of relays
+- On success: JSON array of relays
```json
[
@@ -354,11 +354,11 @@ Response:
Params:
-* `relay_url`
+- `relay_url`
Response:
-* On success: relay json object
+- On success: relay json object
```json
{"actor": "https://example.com/relay", "followed_back": true}
@@ -374,7 +374,7 @@ Response:
Response:
-* On success: URL of the unfollowed relay
+- On success: URL of the unfollowed relay
```json
{"https://example.com/relay"}
@@ -472,7 +472,6 @@ Response:
### Get a password reset token for a given nickname
-
- Params: none
- Response:
@@ -493,7 +492,7 @@ Response:
## PUT `/api/v1/pleroma/admin/users/disable_mfa`
-### Disable mfa for user's account.
+### Disable MFA for user's account
- Params:
- `nickname`
@@ -551,30 +550,30 @@ Response:
### Change the user's email, password, display and settings-related fields
-* Params:
- * `email`
- * `password`
- * `name`
- * `bio`
- * `avatar`
- * `locked`
- * `no_rich_text`
- * `default_scope`
- * `banner`
- * `hide_follows`
- * `hide_followers`
- * `hide_followers_count`
- * `hide_follows_count`
- * `hide_favorites`
- * `allow_following_move`
- * `background`
- * `show_role`
- * `skip_thread_containment`
- * `fields`
- * `is_discoverable`
- * `actor_type`
-
-* Responses:
+- Params:
+ - `email`
+ - `password`
+ - `name`
+ - `bio`
+ - `avatar`
+ - `locked`
+ - `no_rich_text`
+ - `default_scope`
+ - `banner`
+ - `hide_follows`
+ - `hide_followers`
+ - `hide_followers_count`
+ - `hide_follows_count`
+ - `hide_favorites`
+ - `allow_following_move`
+ - `background`
+ - `show_role`
+ - `skip_thread_containment`
+ - `fields`
+ - `is_discoverable`
+ - `actor_type`
+
+- Responses:
Status: 200
@@ -896,7 +895,7 @@ Status: 404
- Params: none
- Response:
- On failure:
- - 400 Bad Request `"To use this endpoint you need to enable configuration from database."`
+ - 400 Bad Request `"You must enable configurable_from_database in your config file."`
```json
{}
@@ -909,6 +908,7 @@ Status: 404
- Params: none
- Response:
- `need_reboot` - boolean
+
```json
{
"need_reboot": false
@@ -917,7 +917,7 @@ Status: 404
## `GET /api/v1/pleroma/admin/config`
-### Get list of merged default settings with saved in database.
+### Get list of merged default settings with saved in database
*If `need_reboot` is `true`, instance must be restarted, so reboot time settings can take effect.*
@@ -927,7 +927,7 @@ Status: 404
- `only_db`: true (*optional*, get only saved in database settings)
- Response:
- On failure:
- - 400 Bad Request `"To use this endpoint you need to enable configuration from database."`
+ - 400 Bad Request `"You must enable configurable_from_database in your config file."`
```json
{
@@ -952,34 +952,34 @@ Status: 404
Some modifications are necessary to save the config settings correctly:
-- strings which start with `Pleroma.`, `Phoenix.`, `Tesla.` or strings like `Oban`, `Ueberauth` will be converted to modules;
-```
-"Pleroma.Upload" -> Pleroma.Upload
-"Oban" -> Oban
-```
-- strings starting with `:` will be converted to atoms;
-```
-":pleroma" -> :pleroma
-```
-- objects with `tuple` key and array value will be converted to tuples;
-```
-{"tuple": ["string", "Pleroma.Upload", []]} -> {"string", Pleroma.Upload, []}
-```
-- arrays with *tuple objects* will be converted to keywords;
-```
-[{"tuple": [":key1", "value"]}, {"tuple": [":key2", "value"]}] -> [key1: "value", key2: "value"]
-```
+- strings which start with `Pleroma.`, `Phoenix.`, `Tesla.` or strings like `Oban`, `Ueberauth` will be converted to modules
+ - `"Pleroma.Upload"` -> `Pleroma.Upload`
+ - `"Oban"` -> `Oban`
+- strings starting with `:` will be converted to atoms
+ - `":pleroma"` -> `:pleroma`
+- objects with `tuple` key and array value will be converted to tuples
+ - `{"tuple": ["string", "Pleroma.Upload", []]}` -> `{"string", Pleroma.Upload, []}`
+- arrays with *tuple objects* will be converted to keywords
+ - `[{"tuple": [":key1", "value"]}, {"tuple": [":key2", "value"]}]` -> `[key1: "value", key2: "value"]`
-Most of the settings will be applied in `runtime`, this means that you don't need to restart the instance. But some settings are applied in `compile time` and require a reboot of the instance, such as:
-- all settings inside these keys:
- - `:hackney_pools`
- - `:connections_pool`
- - `:pools`
+Most of the settings will be applied in `runtime`, this means that changes will be applied immediately. But some settings are applied on `startup time` and will take effect after restart of the pleroma parts, such as:
+
+- all settings inside these keys
- `:chat`
+ - `Oban`
+ - `:rate_limit`
+ - `:streamer`
+ - `:pools`
+ - `:connections_pool`
+ - `:hackney_pools`
+ - `:gopher`
+ - `:eshhd`
+ - `:ex_aws`
- partially settings inside these keys:
- `:seconds_valid` in `Pleroma.Captcha`
- `:proxy_remote` in `Pleroma.Upload`
- `:upload_limit` in `:instance`
+ - `:enabled` in `:fed_sockets`
- Params:
- `configs` - array of config objects
@@ -990,34 +990,33 @@ Most of the settings will be applied in `runtime`, this means that you don't nee
- `delete` - true (*optional*, if setting must be deleted)
- `subkeys` - array of strings (*optional*, only works when `delete=true` parameter is passed, otherwise will be ignored)
-*When a value have several nested settings, you can delete only some nested settings by passing a parameter `subkeys`, without deleting all settings by key.*
-```
-[subkey: val1, subkey2: val2, subkey3: val3] \\ initial value
-{"group": ":pleroma", "key": "some_key", "delete": true, "subkeys": [":subkey", ":subkey3"]} \\ passing json for deletion
-[subkey2: val2] \\ value after deletion
-```
+#### Partial deletion
-*Most of the settings can be partially updated through merge old values with new values, except settings value of which is list or is not keyword.*
+Keys inside value can be partially deleted by passing `subkeys` parameter. If after partial deleting an empty list remains, then the entire setting will be deleted.
+
+Example:
-Example of setting without keyword in value:
```elixir
-config :tesla, :adapter, Tesla.Adapter.Hackney
+# initial value
+[subkey: :val1, subkey2: :val2, subkey3: :val3]
+```
+
+```json
+// config object for deletion
+{"group": ":pleroma", "key": "some_key", "delete": true, "subkeys": [":subkey", ":subkey3"]}
```
-List of settings which support only full update by key:
```elixir
-@full_key_update [
- {:pleroma, :ecto_repos},
- {:quack, :meta},
- {:mime, :types},
- {:cors_plug, [:max_age, :methods, :expose, :headers]},
- {:auto_linker, :opts},
- {:swarm, :node_blacklist},
- {:logger, :backends}
- ]
+# value after deletion
+[subkey2: :val2]
```
-List of settings which support only full update by subkey:
+#### Partial update
+
+Most settings can be partially updated: new values will be merged with existing ones.
+
+The following settings are exceptions and should be fully updated:
+
```elixir
@full_subkey_update [
{:pleroma, :assets, :mascots},
@@ -1028,22 +1027,24 @@ List of settings which support only full update by subkey:
]
```
-*Settings without explicit key must be sended in separate config object params.*
+#### Settings without explicit keys
+
+Settings without explicit key must be sended in one config object with null value as key.
+
```elixir
config :quack,
level: :debug,
- meta: [:all],
- ...
+ meta: [:all]
```
+
```json
{
"configs": [
- {"group": ":quack", "key": ":level", "value": ":debug"},
- {"group": ":quack", "key": ":meta", "value": [":all"]},
- ...
+ {"group": ":quack", "key": null, "value": [{"tuple": [":level", ":debug"]}, {"tuple": [":meta", ":all"]}]}
]
}
```
+
- Request:
```json
@@ -1077,7 +1078,8 @@ config :quack,
- Response:
- On failure:
- - 400 Bad Request `"To use this endpoint you need to enable configuration from database."`
+ - 400 Bad Request `"You must enable configurable_from_database in your config file."`
+
```json
{
"configs": [
@@ -1091,9 +1093,10 @@ config :quack,
}
```
-## ` GET /api/v1/pleroma/admin/config/descriptions`
+## `GET /api/v1/pleroma/admin/config/descriptions`
+
+### Get JSON with config descriptions
-### Get JSON with config descriptions.
Loads json generated from `config/descriptions.exs`.
- Params: none
@@ -1124,6 +1127,37 @@ Loads json generated from `config/descriptions.exs`.
}]
```
+## `GET /api/v1/pleroma/admin/config/versions/rollback/:id`
+
+### Rollback config changes for a given version
+
+- Params:
+ - `id` - version id for rollback
+- Response:
+ - On success: `204`, empty response
+ - On failure:
+ - 400 Bad Request `"You must enable configurable_from_database in your config file."` or endpoint error
+ - 404 Not found
+
+## `GET /api/v1/pleroma/admin/config/versions`
+
+### Get list of config versions
+
+- Params: none
+- Response:
+
+```json
+{
+ "versions": [
+ {
+ "id": 1,
+ "current": true,
+ "inserted_at": "2020-04-21T15:11:46.000Z"
+ }
+ ]
+}
+```
+
## `GET /api/v1/pleroma/admin/moderation_log`
### Get moderation log
@@ -1230,7 +1264,6 @@ Loads json generated from `config/descriptions.exs`.
}
```
-
## `POST /api/v1/pleroma/admin/oauth_app`
### Create OAuth App
@@ -1257,6 +1290,7 @@ Loads json generated from `config/descriptions.exs`.
```
- On failure:
+
```json
{
"redirect_uris": "can't be blank",
@@ -1269,11 +1303,11 @@ Loads json generated from `config/descriptions.exs`.
### Update OAuth App
- Params:
- - *optional* `name`
- - *optional* `redirect_uris`
- - *optional* `scopes`
- - *optional* `website`
- - *optional* `trusted`
+ - *optional* `name`
+ - *optional* `redirect_uris`
+ - *optional* `scopes`
+ - *optional* `website`
+ - *optional* `trusted`
- Response:
@@ -1491,6 +1525,7 @@ Returns the content of the document
```
## `PATCH /api/v1/pleroma/admin/instance_document/:document_name`
+
- Params:
- `file` (the file to be uploaded, using multipart form data.)
diff --git a/docs/development/config_versioning.md b/docs/development/config_versioning.md
new file mode 100644
index 000000000..fd5a44910
--- /dev/null
+++ b/docs/development/config_versioning.md
@@ -0,0 +1,40 @@
+# Config versioning
+
+Database configuration supports simple versioning. Every change (list of changes or only one change) through adminFE creates new version with backup from config table. It is possible to do rollback on N steps (1 by default). Rollback will recreate `config` table from backup.
+
+**IMPORTANT** Destructive operations with `Pleroma.ConfigDB` and `Pleroma.Config.Version` must be processed through `Pleroma.Config.Versioning` module for correct versioning work, especially migration changes.
+
+Example:
+
+* new config setting is added directly using `Pleroma.ConfigDB` module
+* user is doing rollback and setting is lost
+
+## Creating new version
+
+Creating new version is done with `Pleroma.Config.Versioning.new_version/1`, which accepts list of changes. Changes can include adding/updating/deleting operations in `config` table at the same time.
+
+Process of creating new version:
+
+* saving config changes in `config` table
+* saving new version with current configs
+ * `backup` - keyword with all configs from `config` table (binary)
+ * `current` - flag, which marks current version (boolean)
+
+## Version rollback
+
+Version control also supports a simple N steps back mechanism.
+
+Rollback process:
+
+* cleaning `config` table
+* splitting `backup` field into separate settings and inserting them into `config` table
+* removing subsequent versions
+
+## Config migrations
+
+Sometimes it becomes necessary to make changes to the configuration, which can be stored in the user's database. Config versioning makes this process more complicated, as we also must update this setting in versions backups.
+
+Versioning module contains two functions for migrations:
+
+* `Pleroma.Config.Versioning.migrate_namespace/2` - for simple renaming, e.g. group or key of the setting must be renamed.
+* `Pleroma.Config.Versioning.migrate_configs_and_versions/2` - abstract function for more complex migrations. Accepts two functions, the first one to make changes with configs, another to make changes with version backups.
diff --git a/lib/mix/pleroma.ex b/lib/mix/pleroma.ex
index 2b6c7d6bb..f7a52d16c 100644
--- a/lib/mix/pleroma.ex
+++ b/lib/mix/pleroma.ex
@@ -4,7 +4,6 @@
defmodule Mix.Pleroma do
@apps [
- :restarter,
:ecto,
:ecto_sql,
:postgrex,
@@ -16,11 +15,14 @@ defmodule Mix.Pleroma do
:fast_html,
:oban
]
+
@cachex_children ["object", "user", "scrubber", "web_resp"]
+
@doc "Common functions to be reused in mix tasks"
+ @spec start_pleroma() :: {:ok, pid()}
def start_pleroma do
Pleroma.Config.Holder.save_default()
- Pleroma.Config.Oban.warn()
+ Pleroma.Config.DeprecationWarnings.check_oban_config()
Pleroma.Application.limiters_setup()
Application.put_env(:phoenix, :serve_endpoints, false, persistent: true)
@@ -47,37 +49,27 @@ defmodule Mix.Pleroma do
plugins: []
]
- children =
- [
- Pleroma.Repo,
- Pleroma.Emoji,
- {Pleroma.Config.TransferTask, false},
- Pleroma.Web.Endpoint,
- {Oban, oban_config},
- {Majic.Pool,
- [name: Pleroma.MajicPool, pool_size: Pleroma.Config.get([:majic_pool, :size], 2)]}
- ] ++
- http_children(adapter)
+ children = [
+ Pleroma.Repo,
+ Supervisor.child_spec({Task, &Pleroma.Application.Environment.load_from_db_and_update/0},
+ id: :update_env
+ ),
+ Pleroma.Web.Endpoint,
+ Pleroma.Emoji,
+ {Oban, oban_config},
+ {Majic.Pool,
+ [name: Pleroma.MajicPool, pool_size: Pleroma.Config.get([:majic_pool, :size], 2)]}
+ ]
+
+ children = [Pleroma.Application.StartUpDependencies.adapter_module() | children]
- cachex_children = Enum.map(@cachex_children, &Pleroma.Application.build_cachex(&1, []))
+ cachex_children =
+ Enum.map(@cachex_children, &Pleroma.Application.StartUpDependencies.cachex_spec({&1, []}))
Supervisor.start_link(children ++ cachex_children,
strategy: :one_for_one,
name: Pleroma.Supervisor
)
-
- if Pleroma.Config.get(:env) not in [:test, :benchmark] do
- pleroma_rebooted?()
- end
- end
-
- defp pleroma_rebooted? do
- if Restarter.Pleroma.rebooted?() do
- :ok
- else
- Process.sleep(10)
- pleroma_rebooted?()
- end
end
def load_pleroma do
@@ -129,11 +121,4 @@ defmodule Mix.Pleroma do
def escape_sh_path(path) do
~S(') <> String.replace(path, ~S('), ~S(\')) <> ~S(')
end
-
- defp http_children(Tesla.Adapter.Gun) do
- Pleroma.Gun.ConnectionPool.children() ++
- [{Task, &Pleroma.HTTP.AdapterHelper.Gun.limiter_setup/0}]
- end
-
- defp http_children(_), do: []
end
diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex
index 1962154b9..391b112ee 100644
--- a/lib/mix/tasks/pleroma/config.ex
+++ b/lib/mix/tasks/pleroma/config.ex
@@ -14,10 +14,13 @@ defmodule Mix.Tasks.Pleroma.Config do
@shortdoc "Manages the location of the config"
@moduledoc File.read!("docs/administration/CLI_tasks/config.md")
- def run(["migrate_to_db"]) do
+ def run(["migrate_to_db" | options]) do
check_configdb(fn ->
start_pleroma()
- migrate_to_db()
+
+ {opts, _} = OptionParser.parse!(options, strict: [config: :string])
+
+ migrate_to_db(opts)
end)
end
@@ -39,15 +42,13 @@ defmodule Mix.Tasks.Pleroma.Config do
check_configdb(fn ->
start_pleroma()
- header = config_header()
-
settings =
ConfigDB
|> Repo.all()
|> Enum.sort()
unless settings == [] do
- shell_info("#{header}")
+ shell_info("#{Pleroma.Config.Loader.config_header()}")
Enum.each(settings, &dump(&1))
else
@@ -73,9 +74,10 @@ defmodule Mix.Tasks.Pleroma.Config do
check_configdb(fn ->
start_pleroma()
- group = maybe_atomize(group)
-
- dump_group(group)
+ group
+ |> maybe_atomize()
+ |> ConfigDB.get_all_by_group()
+ |> Enum.each(&dump/1)
end)
end
@@ -97,17 +99,11 @@ defmodule Mix.Tasks.Pleroma.Config do
end)
end
- def run(["reset", "--force"]) do
+ def run(["reset" | opts]) do
check_configdb(fn ->
start_pleroma()
- truncatedb()
- shell_info("The ConfigDB settings have been removed from the database.")
- end)
- end
- def run(["reset"]) do
- check_configdb(fn ->
- start_pleroma()
+ {opts, []} = OptionParser.parse!(opts, strict: [force: :boolean])
shell_info("The following settings will be permanently removed:")
@@ -118,8 +114,8 @@ defmodule Mix.Tasks.Pleroma.Config do
shell_error("\nTHIS CANNOT BE UNDONE!")
- if shell_prompt("Are you sure you want to continue?", "n") in ~w(Yn Y y) do
- truncatedb()
+ if opts[:force] or shell_prompt("Are you sure you want to continue?", "n") in ~w(Yn Y y) do
+ Pleroma.Config.Versioning.reset()
shell_info("The ConfigDB settings have been removed from the database.")
else
@@ -128,55 +124,65 @@ defmodule Mix.Tasks.Pleroma.Config do
end)
end
- def run(["delete", "--force", group, key]) do
- start_pleroma()
+ def run(["delete", group]), do: delete(group, force: false)
+ def run(["delete", "--force", group]), do: delete(group, force: true)
- group = maybe_atomize(group)
- key = maybe_atomize(key)
-
- with true <- key_exists?(group, key) do
- shell_info("The following settings will be removed from ConfigDB:\n")
+ def run(["delete", group, key]), do: delete(group, key, force: false)
+ def run(["delete", "--force", group, key]), do: delete(group, key, force: true)
- group
- |> ConfigDB.get_by_group_and_key(key)
- |> dump()
+ def run(["rollback" | options]) do
+ check_configdb(fn ->
+ start_pleroma()
+ {opts, _} = OptionParser.parse!(options, strict: [steps: :integer], aliases: [s: :steps])
- delete_key(group, key)
- else
- _ ->
- shell_error("No settings in ConfigDB for #{inspect(group)}, #{inspect(key)}. Aborting.")
- end
+ do_rollback(opts)
+ end)
end
- def run(["delete", "--force", group]) do
+ defp delete(group, opts) do
start_pleroma()
group = maybe_atomize(group)
- with true <- group_exists?(group) do
+ configs = ConfigDB.get_all_by_group(group)
+
+ if configs != [] do
shell_info("The following settings will be removed from ConfigDB:\n")
- dump_group(group)
- delete_group(group)
+ Enum.each(configs, &dump/1)
+
+ if opts[:force] or shell_prompt("Are you sure you want to continue?", "n") in ~w(Yn Y y) do
+ Enum.each(configs, fn config ->
+ Pleroma.Config.Versioning.new_version(%{
+ group: config.group,
+ key: config.key,
+ delete: true
+ })
+ end)
+ else
+ shell_error("No changes made.")
+ end
else
- _ -> shell_error("No settings in ConfigDB for #{inspect(group)}. Aborting.")
+ shell_error("No settings in ConfigDB for #{inspect(group)}. Aborting.")
end
end
- def run(["delete", group, key]) do
+ defp delete(group, key, opts) do
start_pleroma()
group = maybe_atomize(group)
key = maybe_atomize(key)
- with true <- key_exists?(group, key) do
+ with %ConfigDB{} = config <- ConfigDB.get_by_group_and_key(group, key) do
shell_info("The following settings will be removed from ConfigDB:\n")
- group
- |> ConfigDB.get_by_group_and_key(key)
- |> dump()
+ dump(config)
- if shell_prompt("Are you sure you want to continue?", "n") in ~w(Yn Y y) do
- delete_key(group, key)
+ if opts[:force] or shell_prompt("Are you sure you want to continue?", "n") in ~w(Yn Y y) do
+ Pleroma.Config.Versioning.new_version(%{
+ group: config.group,
+ key: config.key,
+ delete: true
+ })
else
shell_error("No changes made.")
end
@@ -186,40 +192,36 @@ defmodule Mix.Tasks.Pleroma.Config do
end
end
- def run(["delete", group]) do
- start_pleroma()
+ defp do_rollback(opts) do
+ steps = opts[:steps] || 1
- group = maybe_atomize(group)
+ case Pleroma.Config.Versioning.rollback(steps) do
+ {:ok, _} ->
+ shell_info("Success rollback")
- with true <- group_exists?(group) do
- shell_info("The following settings will be removed from ConfigDB:\n")
- dump_group(group)
+ {:error, :no_current_version} ->
+ shell_error("No version to rollback")
- if shell_prompt("Are you sure you want to continue?", "n") in ~w(Yn Y y) do
- delete_group(group)
- else
- shell_error("No changes made.")
- end
- else
- _ -> shell_error("No settings in ConfigDB for #{inspect(group)}. Aborting.")
+ {:error, :rollback_not_possible} ->
+ shell_error("Rollback not possible. Incorrect steps value.")
+
+ {:error, _, _, _} ->
+ shell_error("Problem with backup. Rollback not possible.")
+
+ error ->
+ shell_error("error occuried: #{inspect(error)}")
end
end
- @spec migrate_to_db(Path.t() | nil) :: any()
- def migrate_to_db(file_path \\ nil) do
+ defp migrate_to_db(opts) do
with :ok <- Pleroma.Config.DeprecationWarnings.warn() do
- config_file =
- if file_path do
- file_path
- else
- if Pleroma.Config.get(:release) do
- Pleroma.Config.get(:config_path)
- else
- "config/#{Pleroma.Config.get(:env)}.secret.exs"
- end
- end
+ config_file = opts[:config] || Pleroma.Application.config_path()
- do_migrate_to_db(config_file)
+ if File.exists?(config_file) do
+ do_migrate_to_db(config_file)
+ else
+ shell_info("To migrate settings, you must define custom settings in #{config_file}.")
+ end
else
_ ->
shell_error("Migration is not allowed until all deprecation warnings have been resolved.")
@@ -227,33 +229,9 @@ defmodule Mix.Tasks.Pleroma.Config do
end
defp do_migrate_to_db(config_file) do
- if File.exists?(config_file) do
- shell_info("Migrating settings from file: #{Path.expand(config_file)}")
- truncatedb()
-
- custom_config =
- config_file
- |> read_file()
- |> elem(0)
-
- custom_config
- |> Keyword.keys()
- |> Enum.each(&create(&1, custom_config))
- else
- shell_info("To migrate settings, you must define custom settings in #{config_file}.")
- end
- end
-
- defp create(group, settings) do
- group
- |> Pleroma.Config.Loader.filter_group(settings)
- |> Enum.each(fn {key, value} ->
- {:ok, _} = ConfigDB.update_or_create(%{group: group, key: key, value: value})
-
- shell_info("Settings for key #{key} migrated.")
- end)
-
- shell_info("Settings for group #{inspect(group)} migrated.")
+ shell_info("Migrating settings from file: #{Path.expand(config_file)}")
+ {:ok, _} = Pleroma.Config.Versioning.migrate(config_file)
+ shell_info("Settings migrated.")
end
defp migrate_from_db(opts) do
@@ -270,53 +248,51 @@ defmodule Mix.Tasks.Pleroma.Config do
|> Path.join("#{env}.exported_from_db.secret.exs")
file = File.open!(config_path, [:write, :utf8])
+ IO.write(file, Pleroma.Config.Loader.config_header())
- IO.write(file, config_header())
-
- ConfigDB
- |> Repo.all()
- |> Enum.each(&write_and_delete(&1, file, opts[:delete]))
-
- :ok = File.close(file)
- System.cmd("mix", ["format", config_path])
-
- shell_info(
- "Database configuration settings have been exported to config/#{env}.exported_from_db.secret.exs"
- )
- end
-
- if Code.ensure_loaded?(Config.Reader) do
- defp config_header, do: "import Config\r\n\r\n"
- defp read_file(config_file), do: Config.Reader.read_imports!(config_file)
- else
- defp config_header, do: "use Mix.Config\r\n\r\n"
- defp read_file(config_file), do: Mix.Config.eval!(config_file)
- end
+ changes =
+ ConfigDB
+ |> Repo.all()
+ |> Enum.reduce([], fn %{group: group} = config, acc ->
+ group_str = inspect(group)
+ value = inspect(config.value, limit: :infinity)
+
+ msg =
+ if group in ConfigDB.groups_without_keys() do
+ IO.write(file, "config #{group_str}, #{value}\r\n\r\n")
+ "config #{group_str} was deleted."
+ else
+ key_str = inspect(config.key)
+ IO.write(file, "config #{group_str}, #{key_str}, #{value}\r\n\r\n")
+ "config #{group_str}, #{key_str} was deleted."
+ end
- defp write_and_delete(config, file, delete?) do
- config
- |> write(file)
- |> delete(delete?)
- end
+ if opts[:delete] do
+ shell_info(msg)
- defp write(config, file) do
- value = inspect(config.value, limit: :infinity)
+ change =
+ config
+ |> Map.take([:group, :key])
+ |> Map.put(:delete, true)
- IO.write(file, "config #{inspect(config.group)}, #{inspect(config.key)}, #{value}\r\n\r\n")
+ [change | acc]
+ else
+ acc
+ end
+ end)
- config
- end
+ if opts[:delete] and changes != [] do
+ Pleroma.Config.Versioning.new_version(changes)
+ end
- defp delete(config, true) do
- {:ok, _} = Repo.delete(config)
+ :ok = File.close(file)
+ System.cmd("mix", ["format", config_path])
shell_info(
- "config #{inspect(config.group)}, #{inspect(config.key)} was deleted from the ConfigDB."
+ "Database configuration settings have been exported to config/#{env}.exported_from_db.secret.exs"
)
end
- defp delete(_config, _), do: :ok
-
defp dump(%ConfigDB{} = config) do
value = inspect(config.value, limit: :infinity)
@@ -325,31 +301,12 @@ defmodule Mix.Tasks.Pleroma.Config do
defp dump(_), do: :noop
- defp dump_group(group) when is_atom(group) do
- group
- |> ConfigDB.get_all_by_group()
- |> Enum.each(&dump/1)
- end
-
- defp group_exists?(group) do
- group
- |> ConfigDB.get_all_by_group()
- |> Enum.any?()
- end
-
- defp key_exists?(group, key) do
- group
- |> ConfigDB.get_by_group_and_key(key)
- |> is_nil
- |> Kernel.!()
- end
-
defp maybe_atomize(arg) when is_atom(arg), do: arg
defp maybe_atomize(":" <> arg), do: maybe_atomize(arg)
defp maybe_atomize(arg) when is_binary(arg) do
- if ConfigDB.module_name?(arg) do
+ if Pleroma.Config.Converter.module_name?(arg) do
String.to_existing_atom("Elixir." <> arg)
else
String.to_atom(arg)
@@ -366,23 +323,4 @@ defmodule Mix.Tasks.Pleroma.Config do
)
end
end
-
- defp delete_key(group, key) do
- check_configdb(fn ->
- ConfigDB.delete(%{group: group, key: key})
- end)
- end
-
- defp delete_group(group) do
- check_configdb(fn ->
- group
- |> ConfigDB.get_all_by_group()
- |> Enum.each(&ConfigDB.delete/1)
- end)
- end
-
- defp truncatedb do
- Ecto.Adapters.SQL.query!(Repo, "TRUNCATE config;")
- Ecto.Adapters.SQL.query!(Repo, "ALTER SEQUENCE config_id_seq RESTART;")
- end
end
diff --git a/lib/mix/tasks/pleroma/docs.ex b/lib/mix/tasks/pleroma/docs.ex
index 45cca1c74..e0e9834f4 100644
--- a/lib/mix/tasks/pleroma/docs.ex
+++ b/lib/mix/tasks/pleroma/docs.ex
@@ -32,7 +32,7 @@ defmodule Mix.Tasks.Pleroma.Docs do
defp do_run(implementation) do
start_pleroma()
- with descriptions <- Pleroma.Config.Loader.read("config/description.exs"),
+ with descriptions <- Pleroma.Config.Loader.read!("config/description.exs"),
{:ok, file_path} <-
Pleroma.Docs.Generator.process(
implementation,
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index 06d399b2e..afe605f01 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -5,8 +5,6 @@
defmodule Pleroma.Application do
use Application
- import Cachex.Spec
-
alias Pleroma.Config
require Logger
@@ -15,12 +13,17 @@ defmodule Pleroma.Application do
@version Mix.Project.config()[:version]
@repository Mix.Project.config()[:source_url]
@mix_env Mix.env()
+ @dynamic_supervisor Pleroma.Application.Supervisor
+
+ @type env() :: :test | :benchmark | :dev | :prod
def name, do: @name
def version, do: @version
def named_version, do: @name <> " " <> @version
def repository, do: @repository
+ def dynamic_supervisor, do: @dynamic_supervisor
+ @spec user_agent() :: String.t()
def user_agent do
if Process.whereis(Pleroma.Web.Endpoint) do
case Config.get([:http, :user_agent], :default) do
@@ -37,9 +40,43 @@ defmodule Pleroma.Application do
end
end
- # See http://elixir-lang.org/docs/stable/elixir/Application.html
- # for more information on OTP Applications
+ @spec config_path() :: Path.t()
+ def config_path do
+ if Config.get(:release) do
+ Config.get(:config_path)
+ else
+ Config.get(:config_path_in_test) || "config/#{@mix_env}.secret.exs"
+ end
+ end
+
+ @doc """
+ Under main supervisor is started DynamicSupervisor, which later starts pleroma startup dependencies.
+ Pleroma start is splitted into three `phases`:
+ - running prestart requirements (runtime compilation, warnings, deprecations, monitoring, etc.)
+ - loading and updating environment (if database config is used and enabled)
+ - starting dependencies
+ """
+ @impl true
def start(_type, _args) do
+ children = [
+ {DynamicSupervisor, strategy: :one_for_one, name: @dynamic_supervisor},
+ {Pleroma.Application.ConfigDependentDeps, [dynamic_supervisor: @dynamic_supervisor]},
+ Pleroma.Repo
+ ]
+
+ {:ok, main_supervisor} =
+ Supervisor.start_link(children, strategy: :one_for_one, name: Pleroma.Supervisor)
+
+ run_prestart_requirements()
+
+ Pleroma.Application.Environment.load_from_db_and_update(pleroma_start: true)
+
+ Pleroma.Application.StartUpDependencies.start_all(@mix_env)
+
+ {:ok, main_supervisor}
+ end
+
+ defp run_prestart_requirements do
# Scrubbers are compiled at runtime and therefore will cause a conflict
# every time the application is restarted, so we disable module
# conflicts at runtime
@@ -47,72 +84,26 @@ defmodule Pleroma.Application do
# Disable warnings_as_errors at runtime, it breaks Phoenix live reload
# due to protocol consolidation warnings
Code.compiler_options(warnings_as_errors: false)
+
+ # compilation in runtime
+ Pleroma.HTML.compile_scrubbers()
+ compile_custom_modules()
+ Pleroma.Docs.JSON.compile()
+
+ # telemetry and prometheus
Pleroma.Telemetry.Logger.attach()
+ setup_instrumenters()
+
Config.Holder.save_default()
- Pleroma.HTML.compile_scrubbers()
- Pleroma.Config.Oban.warn()
+
Config.DeprecationWarnings.warn()
Pleroma.Web.Plugs.HTTPSecurityPlug.warn_if_disabled()
- Pleroma.ApplicationRequirements.verify!()
- setup_instrumenters()
- load_custom_modules()
- Pleroma.Docs.JSON.compile()
- limiters_setup()
- adapter = Application.get_env(:tesla, :adapter)
-
- if adapter == Tesla.Adapter.Gun do
- if version = Pleroma.OTPVersion.version() do
- [major, minor] =
- version
- |> String.split(".")
- |> Enum.map(&String.to_integer/1)
- |> Enum.take(2)
-
- if (major == 22 and minor < 2) or major < 22 do
- raise "
- !!!OTP VERSION WARNING!!!
- You are using gun adapter with OTP version #{version}, which doesn't support correct handling of unordered certificates chains. Please update your Erlang/OTP to at least 22.2.
- "
- end
- else
- raise "
- !!!OTP VERSION WARNING!!!
- To support correct handling of unordered certificates chains - OTP version must be > 22.2.
- "
- end
- end
-
- # Define workers and child supervisors to be supervised
- children =
- [
- Pleroma.Repo,
- Config.TransferTask,
- Pleroma.Emoji,
- Pleroma.Web.Plugs.RateLimiter.Supervisor
- ] ++
- cachex_children() ++
- http_children(adapter, @mix_env) ++
- [
- Pleroma.Stats,
- Pleroma.JobQueueMonitor,
- {Majic.Pool, [name: Pleroma.MajicPool, pool_size: Config.get([:majic_pool, :size], 2)]},
- {Oban, Config.get(Oban)},
- Pleroma.Web.Endpoint
- ] ++
- task_children(@mix_env) ++
- dont_run_in_test(@mix_env) ++
- chat_child(chat_enabled?()) ++
- [Pleroma.Gopher.Server]
-
- # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
- # for other strategies and supported options
- opts = [strategy: :one_for_one, name: Pleroma.Supervisor]
- result = Supervisor.start_link(children, opts)
+ limiters_setup()
set_postgres_server_version()
- result
+ Pleroma.Application.Requirements.verify!()
end
defp set_postgres_server_version do
@@ -132,7 +123,7 @@ defmodule Pleroma.Application do
:persistent_term.put({Pleroma.Repo, :postgres_version}, version)
end
- def load_custom_modules do
+ defp compile_custom_modules do
dir = Config.get([:modules, :runtime_dir])
if dir && File.exists?(dir) do
@@ -177,128 +168,6 @@ defmodule Pleroma.Application do
PrometheusPhx.setup()
end
- defp cachex_children do
- [
- build_cachex("used_captcha", ttl_interval: seconds_valid_interval()),
- build_cachex("user", default_ttl: 25_000, ttl_interval: 1000, limit: 2500),
- build_cachex("object", default_ttl: 25_000, ttl_interval: 1000, limit: 2500),
- build_cachex("rich_media", default_ttl: :timer.minutes(120), limit: 5000),
- build_cachex("scrubber", limit: 2500),
- build_cachex("idempotency", expiration: idempotency_expiration(), limit: 2500),
- build_cachex("web_resp", limit: 2500),
- build_cachex("emoji_packs", expiration: emoji_packs_expiration(), limit: 10),
- build_cachex("failed_proxy_url", limit: 2500),
- build_cachex("banned_urls", default_ttl: :timer.hours(24 * 30), limit: 5_000),
- build_cachex("chat_message_id_idempotency_key",
- expiration: chat_message_id_idempotency_key_expiration(),
- limit: 500_000
- )
- ]
- end
-
- defp emoji_packs_expiration,
- do: expiration(default: :timer.seconds(5 * 60), interval: :timer.seconds(60))
-
- defp idempotency_expiration,
- do: expiration(default: :timer.seconds(6 * 60 * 60), interval: :timer.seconds(60))
-
- defp chat_message_id_idempotency_key_expiration,
- do: expiration(default: :timer.minutes(2), interval: :timer.seconds(60))
-
- defp seconds_valid_interval,
- do: :timer.seconds(Config.get!([Pleroma.Captcha, :seconds_valid]))
-
- @spec build_cachex(String.t(), keyword()) :: map()
- def build_cachex(type, opts),
- do: %{
- id: String.to_atom("cachex_" <> type),
- start: {Cachex, :start_link, [String.to_atom(type <> "_cache"), opts]},
- type: :worker
- }
-
- defp chat_enabled?, do: Config.get([:chat, :enabled])
-
- defp dont_run_in_test(env) when env in [:test, :benchmark], do: []
-
- defp dont_run_in_test(_) do
- [
- {Registry,
- [
- name: Pleroma.Web.Streamer.registry(),
- keys: :duplicate,
- partitions: System.schedulers_online()
- ]}
- ] ++ background_migrators()
- end
-
- defp background_migrators do
- [
- Pleroma.Migrators.HashtagsTableMigrator
- ]
- end
-
- defp chat_child(true) do
- [
- Pleroma.Web.ChatChannel.ChatChannelState,
- {Phoenix.PubSub, [name: Pleroma.PubSub, adapter: Phoenix.PubSub.PG2]}
- ]
- end
-
- defp chat_child(_), do: []
-
- defp task_children(:test) do
- [
- %{
- id: :web_push_init,
- start: {Task, :start_link, [&Pleroma.Web.Push.init/0]},
- restart: :temporary
- }
- ]
- end
-
- defp task_children(_) do
- [
- %{
- id: :web_push_init,
- start: {Task, :start_link, [&Pleroma.Web.Push.init/0]},
- restart: :temporary
- },
- %{
- id: :internal_fetch_init,
- start: {Task, :start_link, [&Pleroma.Web.ActivityPub.InternalFetchActor.init/0]},
- restart: :temporary
- }
- ]
- end
-
- # start hackney and gun pools in tests
- defp http_children(_, :test) do
- http_children(Tesla.Adapter.Hackney, nil) ++ http_children(Tesla.Adapter.Gun, nil)
- end
-
- defp http_children(Tesla.Adapter.Hackney, _) do
- pools = [:federation, :media]
-
- pools =
- if Config.get([Pleroma.Upload, :proxy_remote]) do
- [:upload | pools]
- else
- pools
- end
-
- for pool <- pools do
- options = Config.get([:hackney_pools, pool])
- :hackney_pool.child_spec(pool, options)
- end
- end
-
- defp http_children(Tesla.Adapter.Gun, _) do
- Pleroma.Gun.ConnectionPool.children() ++
- [{Task, &Pleroma.HTTP.AdapterHelper.Gun.limiter_setup/0}]
- end
-
- defp http_children(_, _), do: []
-
@spec limiters_setup() :: :ok
def limiters_setup do
config = Config.get(ConcurrentLimiter, [])
diff --git a/lib/pleroma/application/chat_supervisor.ex b/lib/pleroma/application/chat_supervisor.ex
new file mode 100644
index 000000000..4b6f0e740
--- /dev/null
+++ b/lib/pleroma/application/chat_supervisor.ex
@@ -0,0 +1,19 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Application.ChatSupervisor do
+ use Supervisor
+
+ def start_link(_) do
+ Supervisor.start_link(__MODULE__, :no_args)
+ end
+
+ def init(_) do
+ [
+ Pleroma.Web.ChatChannel.ChatChannelState,
+ {Phoenix.PubSub, [name: Pleroma.PubSub, adapter: Phoenix.PubSub.PG2]}
+ ]
+ |> Supervisor.init(strategy: :one_for_one)
+ end
+end
diff --git a/lib/pleroma/application/config_dependent_deps.ex b/lib/pleroma/application/config_dependent_deps.ex
new file mode 100644
index 000000000..c6b26affd
--- /dev/null
+++ b/lib/pleroma/application/config_dependent_deps.ex
@@ -0,0 +1,244 @@
+# # Pleroma: A lightweight social networking server
+# # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# # SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Application.ConfigDependentDeps do
+ use GenServer
+
+ require Logger
+
+ @config_path_mods_relation [
+ {{:pleroma, :chat}, Pleroma.Application.ChatSupervisor},
+ {{:pleroma, Oban}, Oban},
+ {{:pleroma, :rate_limit}, Pleroma.Web.Plugs.RateLimiter.Supervisor},
+ {{:pleroma, :streamer}, Pleroma.Web.Streamer.registry()},
+ {{:pleroma, :pools}, Pleroma.Gun.GunSupervisor},
+ {{:pleroma, :connections_pool}, Pleroma.Gun.GunSupervisor},
+ {{:pleroma, :hackney_pools}, Pleroma.HTTP.HackneySupervisor},
+ {{:pleroma, :gopher}, Pleroma.Gopher.Server},
+ {{:pleroma, Pleroma.Captcha, [:seconds_valid]}, Pleroma.Web.Endpoint},
+ {{:pleroma, Pleroma.Upload, [:proxy_remote]},
+ Pleroma.Application.StartUpDependencies.adapter_module()},
+ {{:pleroma, :instance, [:upload_limit]}, Pleroma.Web.Endpoint},
+ {{:pleroma, :fed_sockets, [:enabled]}, Pleroma.Web.Endpoint},
+ {:eshhd, :eshhd},
+ {:ex_aws, :ex_aws}
+ ]
+
+ def start_link(opts) do
+ opts = Keyword.put_new(opts, :relations, @config_path_mods_relation)
+
+ GenServer.start_link(__MODULE__, opts, name: opts[:name] || __MODULE__)
+ end
+
+ @impl true
+ def init(opts) do
+ init_state = %{
+ dynamic_supervisor: opts[:dynamic_supervisor],
+ relations: opts[:relations],
+ reboot_paths: [],
+ pids: %{}
+ }
+
+ {:ok, init_state}
+ end
+
+ def start_dependency(module, server \\ __MODULE__) do
+ GenServer.call(server, {:start_dependency, module})
+ end
+
+ def need_reboot?(server \\ __MODULE__) do
+ GenServer.call(server, :need_reboot?)
+ end
+
+ def restart_dependencies(server \\ __MODULE__) do
+ GenServer.call(server, :restart_dependencies)
+ end
+
+ def clear_state(server \\ __MODULE__) do
+ GenServer.call(server, :clear_state)
+ end
+
+ def save_config_paths_for_restart(changes, server \\ __MODULE__) do
+ GenServer.call(server, {:save_config_paths, changes})
+ end
+
+ @impl true
+ def handle_call({:start_dependency, module}, _, state) do
+ {result, state} =
+ with {pid, state} when is_pid(pid) <- start_module(module, state) do
+ {{:ok, pid}, state}
+ else
+ error -> {error, state}
+ end
+
+ {:reply, result, state}
+ end
+
+ @impl true
+ def handle_call(:need_reboot?, _, state) do
+ {:reply, state[:reboot_paths] != [], state}
+ end
+
+ @impl true
+ def handle_call(:restart_dependencies, _, state) do
+ {paths, state} = Map.get_and_update(state, :reboot_paths, &{&1, []})
+ started_apps = Application.started_applications()
+
+ {result, state} =
+ Enum.reduce_while(paths, {:ok, state}, fn
+ path, {:ok, acc} when is_tuple(path) ->
+ case restart(path, acc, acc[:pids][path], with_terminate: true) do
+ {pid, state} when is_pid(pid) ->
+ {:cont, {:ok, state}}
+
+ :ignore ->
+ Logger.info("path #{inspect(path)} is ignored.")
+ {:cont, {:ok, acc}}
+
+ error ->
+ {:halt, {error, acc}}
+ end
+
+ app, {:ok, acc}
+ when is_atom(app) and app not in [:logger, :quack, :pleroma, :prometheus, :postgrex] ->
+ restart_app(app, started_apps)
+ {:cont, {:ok, acc}}
+ end)
+
+ {:reply, result, state}
+ end
+
+ @impl true
+ def handle_call(:clear_state, _, state) do
+ state =
+ state
+ |> Map.put(:reboot_paths, [])
+ |> Map.put(:pids, %{})
+
+ {:reply, :ok, state}
+ end
+
+ @impl true
+ def handle_call({:save_config_paths, changes}, _, state) do
+ paths =
+ Enum.reduce(changes, state[:reboot_paths], fn
+ %{group: group, key: key, value: value}, acc ->
+ with {path, _} <- find_relation(state[:relations], group, key, value) do
+ if path not in acc do
+ [path | acc]
+ else
+ acc
+ end
+ else
+ _ ->
+ acc
+ end
+ end)
+
+ {:reply, paths, put_in(state[:reboot_paths], paths)}
+ end
+
+ @impl true
+ def handle_info({:DOWN, _ref, :process, pid, _reason}, state) do
+ updated_state =
+ with {path, ^pid} <-
+ Enum.find(state[:pids], fn {_, registered_pid} -> registered_pid == pid end) do
+ {_new_pid, new_state} = restart(path, state, pid)
+ new_state
+ else
+ _ -> state
+ end
+
+ {:noreply, updated_state}
+ end
+
+ defp start_module(module, state) do
+ with {:ok, relations} <- find_relations(state[:relations], module) do
+ start_module(module, relations, state)
+ end
+ end
+
+ defp start_module(module, relations, state) do
+ spec =
+ module
+ |> Pleroma.Application.StartUpDependencies.spec()
+ |> Supervisor.child_spec(restart: :temporary)
+
+ with {:ok, pid} <-
+ DynamicSupervisor.start_child(
+ state[:dynamic_supervisor],
+ spec
+ ) do
+ pids = Map.new(relations, fn {path, _} -> {path, pid} end)
+ Process.monitor(pid)
+ {pid, put_in(state[:pids], Map.merge(state[:pids], pids))}
+ end
+ end
+
+ defp restart(path, state, pid, opts \\ [])
+
+ defp restart(path, state, nil, _) do
+ with {_, module} <- find_relation(state[:relations], path) do
+ start_module(module, state)
+ end
+ end
+
+ defp restart(path, state, pid, opts) when is_pid(pid) do
+ with {_, module} <- find_relation(state[:relations], path),
+ {:ok, relations} <- find_relations(state[:relations], module) do
+ if opts[:with_terminate] do
+ :ok = DynamicSupervisor.terminate_child(state[:dynamic_supervisor], pid)
+ end
+
+ paths_for_remove = Enum.map(relations, fn {path, _} -> path end)
+ state = put_in(state[:pids], Map.drop(state[:pids], paths_for_remove))
+
+ start_module(module, relations, state)
+ end
+ end
+
+ defp restart_app(app, started_applications) do
+ with {^app, _, _} <- List.keyfind(started_applications, app, 0) do
+ :ok = Application.stop(app)
+ :ok = Application.start(app)
+ else
+ nil ->
+ Logger.info("#{app} is not started.")
+
+ error ->
+ error
+ |> inspect()
+ |> Logger.error()
+ end
+ end
+
+ defp find_relations(relations, module) do
+ case Enum.filter(relations, fn {_, mod} -> mod == module end) do
+ [] ->
+ {:error, :relations_not_found}
+
+ relations ->
+ {:ok, relations}
+ end
+ end
+
+ defp find_relation(relations, group, key, value) do
+ Enum.find(relations, fn
+ {g, _} when is_atom(g) ->
+ g == group
+
+ {{g, k}, _} ->
+ g == group and k == key
+
+ {{g, k, subkeys}, _} ->
+ g == group and k == key and Enum.any?(Keyword.keys(value), &(&1 in subkeys))
+ end)
+ end
+
+ def find_relation(relations, path) do
+ with nil <- Enum.find(relations, fn {key, _} -> key == path end) do
+ {:error, :relation_not_found}
+ end
+ end
+end
diff --git a/lib/pleroma/application/environment.ex b/lib/pleroma/application/environment.ex
new file mode 100644
index 000000000..589be4726
--- /dev/null
+++ b/lib/pleroma/application/environment.ex
@@ -0,0 +1,103 @@
+# # Pleroma: A lightweight social networking server
+# # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# # SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Application.Environment do
+ @moduledoc """
+ Overwrites environment config with settings from config file or database.
+ """
+
+ require Logger
+
+ @doc """
+ Method is called on pleroma start.
+ Config dependent parts don't require restart, because are not started yet.
+ But started apps need restart.
+ """
+ @spec load_from_db_and_update(keyword()) :: :ok
+ def load_from_db_and_update(opts \\ []) do
+ Pleroma.ConfigDB.all()
+ |> update(opts)
+ end
+
+ @spec update([Pleroma.ConfigDB.t()], keyword()) :: :ok
+ def update(changes, opts \\ []) when is_list(changes) do
+ if Pleroma.Config.get(:configurable_from_database) do
+ defaults = Pleroma.Config.Holder.default_config()
+
+ changes
+ |> filter_logger()
+ |> prepare_logger_changes(defaults)
+ |> Enum.each(&configure_logger/1)
+
+ changes
+ |> Pleroma.ConfigDB.merge_changes_with_defaults(defaults)
+ |> Enum.each(&update_env(&1))
+
+ cond do
+ opts[:pleroma_start] ->
+ # restart only apps on pleroma start
+ changes
+ |> Enum.filter(fn %{group: group} ->
+ group not in [:logger, :quack, :pleroma, :prometheus, :postgrex]
+ end)
+ |> Pleroma.Application.ConfigDependentDeps.save_config_paths_for_restart()
+
+ Pleroma.Application.ConfigDependentDeps.restart_dependencies()
+
+ opts[:only_update] ->
+ Pleroma.Application.ConfigDependentDeps.save_config_paths_for_restart(changes)
+
+ true ->
+ nil
+ end
+ end
+
+ :ok
+ end
+
+ defp filter_logger(changes) do
+ Enum.filter(changes, fn %{group: group} -> group in [:logger, :quack] end)
+ end
+
+ defp prepare_logger_changes(changes, defaults) do
+ Enum.map(changes, fn %{group: group} = change ->
+ {change, Pleroma.ConfigDB.merge_change_value_with_default(change, defaults[group])}
+ end)
+ end
+
+ defp configure_logger({%{group: :quack}, merged_value}) do
+ Logger.configure_backend(Quack.Logger, merged_value)
+ end
+
+ defp configure_logger({%{group: :logger} = change, merged_value}) do
+ if change.value[:backends] do
+ Enum.each(Application.get_env(:logger, :backends), &Logger.remove_backend/1)
+
+ Enum.each(merged_value[:backends], &Logger.add_backend/1)
+ end
+
+ if change.value[:console] do
+ console = merged_value[:console]
+ console = put_in(console[:format], console[:format] <> "\n")
+
+ Logger.configure_backend(:console, console)
+ end
+
+ if change.value[:ex_syslogger] do
+ Logger.configure_backend({ExSyslogger, :ex_syslogger}, merged_value[:ex_syslogger])
+ end
+
+ Logger.configure(merged_value)
+ end
+
+ defp update_env(%{group: group, key: key, value: nil}), do: Application.delete_env(group, key)
+
+ defp update_env(%{group: group, value: config} = change) do
+ if group in Pleroma.ConfigDB.groups_without_keys() do
+ Application.put_all_env([{group, config}])
+ else
+ Application.put_env(group, change.key, config)
+ end
+ end
+end
diff --git a/lib/pleroma/application_requirements.ex b/lib/pleroma/application/requirements.ex
index 6ef65b263..1f1429d7c 100644
--- a/lib/pleroma/application_requirements.ex
+++ b/lib/pleroma/application/requirements.ex
@@ -2,7 +2,7 @@
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.ApplicationRequirements do
+defmodule Pleroma.Application.Requirements do
@moduledoc """
The module represents the collection of validations to runs before start server.
"""
@@ -18,6 +18,8 @@ defmodule Pleroma.ApplicationRequirements do
@spec verify!() :: :ok | VerifyError.t()
def verify! do
+ adapter = Application.get_env(:tesla, :adapter)
+
:ok
|> check_system_commands!()
|> check_confirmation_accounts!()
@@ -25,11 +27,12 @@ defmodule Pleroma.ApplicationRequirements do
|> check_welcome_message_config!()
|> check_rum!()
|> check_repo_pool_size!()
- |> handle_result()
+ |> check_otp_version!(adapter)
+ |> handle_result!()
end
- defp handle_result(:ok), do: :ok
- defp handle_result({:error, message}), do: raise(VerifyError, message: message)
+ defp handle_result!(:ok), do: :ok
+ defp handle_result!({:error, message}), do: raise(VerifyError, message: message)
defp check_welcome_message_config!(:ok) do
if Pleroma.Config.get([:welcome, :email, :enabled], false) and
@@ -160,9 +163,9 @@ defmodule Pleroma.ApplicationRequirements do
defp check_system_commands!(:ok) do
filter_commands_statuses = [
- check_filter(Pleroma.Upload.Filters.Exiftool, "exiftool"),
- check_filter(Pleroma.Upload.Filters.Mogrify, "mogrify"),
- check_filter(Pleroma.Upload.Filters.Mogrifun, "mogrify")
+ check_filter!(Pleroma.Upload.Filters.Exiftool, "exiftool"),
+ check_filter!(Pleroma.Upload.Filters.Mogrify, "mogrify"),
+ check_filter!(Pleroma.Upload.Filters.Mogrifun, "mogrify")
]
preview_proxy_commands_status =
@@ -213,7 +216,7 @@ defmodule Pleroma.ApplicationRequirements do
defp check_repo_pool_size!(result), do: result
- defp check_filter(filter, command_required) do
+ defp check_filter!(filter, command_required) do
filters = Config.get([Pleroma.Upload, :filters])
if filter in filters and not Pleroma.Utils.command_available?(command_required) do
@@ -227,4 +230,32 @@ defmodule Pleroma.ApplicationRequirements do
true
end
end
+
+ defp check_otp_version!(:ok, Tesla.Adapter.Gun) do
+ if version = Pleroma.OTPVersion.version() do
+ [major, minor] =
+ version
+ |> String.split(".")
+ |> Enum.map(&String.to_integer/1)
+ |> Enum.take(2)
+
+ if (major == 22 and minor < 2) or major < 22 do
+ Logger.error("
+ !!!OTP VERSION ERROR!!!
+ You are using gun adapter with OTP version #{version}, which doesn't support correct handling of unordered certificates chains. Please update your Erlang/OTP to at least 22.2.
+ ")
+ {:error, "OTP version error"}
+ else
+ :ok
+ end
+ else
+ Logger.error("
+ !!!OTP VERSION ERROR!!!
+ To support correct handling of unordered certificates chains - OTP version must be > 22.2.
+ ")
+ {:error, "OTP version error"}
+ end
+ end
+
+ defp check_otp_version!(result, _), do: result
end
diff --git a/lib/pleroma/application/start_up_dependencies.ex b/lib/pleroma/application/start_up_dependencies.ex
new file mode 100644
index 000000000..ce554a0fc
--- /dev/null
+++ b/lib/pleroma/application/start_up_dependencies.ex
@@ -0,0 +1,182 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Application.StartUpDependencies do
+ alias Pleroma.Config
+ alias Pleroma.Web.Endpoint
+
+ require Cachex.Spec
+ require Logger
+
+ @type config_path() :: {atom(), atom()} | {atom(), atom(), [atom()]}
+ @type relation() :: {config_path(), module()}
+
+ @spec start_all(Pleroma.Application.env()) ::
+ :ok | {:error, {:already_started, pid()} | :max_children | term()}
+ def start_all(env) do
+ with :ok <- start_common_deps(env),
+ :ok <- start_config_dependent_deps(env) do
+ :ok
+ end
+ end
+
+ @spec adapter_module() :: module()
+ def adapter_module do
+ if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Gun do
+ Pleroma.Gun.GunSupervisor
+ else
+ Pleroma.HTTP.HackneySupervisor
+ end
+ end
+
+ @spec spec(module()) :: module() | {module(), keyword()}
+ def spec(Oban), do: {Oban, Config.get(Oban)}
+
+ def spec(Pleroma.Web.StreamerRegistry) do
+ {Registry,
+ [
+ name: Pleroma.Web.Streamer.registry(),
+ keys: :duplicate,
+ partitions: System.schedulers_online()
+ ]}
+ end
+
+ def spec(child), do: child
+
+ @spec cachex_spec({String.t(), keyword()}) :: :supervisor.child_spec()
+ def cachex_spec({type, opts}) do
+ %{
+ id: String.to_atom("cachex_" <> type),
+ start: {Cachex, :start_link, [String.to_atom(type <> "_cache"), opts]},
+ type: :worker
+ }
+ end
+
+ defp start_common_deps(env) do
+ fun = fn child ->
+ DynamicSupervisor.start_child(Pleroma.Application.dynamic_supervisor(), spec(child))
+ end
+
+ [
+ Pleroma.Emoji,
+ Pleroma.Stats,
+ Pleroma.JobQueueMonitor,
+ {Majic.Pool, [name: Pleroma.MajicPool, pool_size: Config.get([:majic_pool, :size], 2)]},
+ %{
+ id: :web_push_init,
+ start: {Task, :start_link, [&Pleroma.Web.Push.init/0]},
+ restart: :temporary
+ }
+ ]
+ |> add_cachex_deps()
+ |> maybe_add_init_internal_fetch_actor_task(env)
+ |> maybe_add_background_migrator(env)
+ |> start_while(fun)
+ end
+
+ defp start_config_dependent_deps(env) do
+ fun = fn child -> Pleroma.Application.ConfigDependentDeps.start_dependency(child) end
+
+ [
+ Pleroma.Web.Plugs.RateLimiter.Supervisor,
+ Oban,
+ Endpoint,
+ Pleroma.Gopher.Server
+ ]
+ |> add_http_children(env)
+ |> maybe_add(:streamer, env)
+ |> maybe_add_chat_child()
+ |> start_while(fun)
+ end
+
+ defp start_while(deps, fun) do
+ Enum.reduce_while(deps, :ok, fn child, acc ->
+ case fun.(child) do
+ {:ok, _} ->
+ {:cont, acc}
+
+ # consider this behavior is normal
+ :ignore ->
+ Logger.info("#{inspect(child)} is ignored.")
+ {:cont, acc}
+
+ error ->
+ Logger.error("Child #{inspect(child)} can't be started. #{inspect(error)}")
+ {:halt, error}
+ end
+ end)
+ end
+
+ @spec cachex_deps() :: [tuple()]
+ def cachex_deps do
+ captcha_clean_up_interval =
+ [Pleroma.Captcha, :seconds_valid]
+ |> Config.get!()
+ |> :timer.seconds()
+
+ [
+ {"used_captcha", expiration: Cachex.Spec.expiration(interval: captcha_clean_up_interval)},
+ {"user", expiration: cachex_expiration(25_000, 1000), limit: 2500},
+ {"object", expiration: cachex_expiration(25_000, 1000), limit: 2500},
+ {"rich_media",
+ expiration: Cachex.Spec.expiration(default: :timer.minutes(120)), limit: 5000},
+ {"scrubber", limit: 2500},
+ {"idempotency", expiration: cachex_expiration(21_600, 60), limit: 2500},
+ {"web_resp", limit: 2500},
+ {"emoji_packs", expiration: cachex_expiration(300, 60), limit: 10},
+ {"failed_proxy_url", limit: 2500},
+ {"banned_urls",
+ expiration: Cachex.Spec.expiration(default: :timer.hours(24 * 30)), limit: 5_000},
+ {"chat_message_id_idempotency_key",
+ expiration: cachex_expiration(:timer.minutes(2), :timer.seconds(60)), limit: 500_000}
+ ]
+ end
+
+ defp add_cachex_deps(application_deps) do
+ cachex_deps()
+ |> Enum.reduce(application_deps, fn cachex_init_args, acc ->
+ [cachex_spec(cachex_init_args) | acc]
+ end)
+ end
+
+ defp cachex_expiration(default, interval) do
+ Cachex.Spec.expiration(default: :timer.seconds(default), interval: :timer.seconds(interval))
+ end
+
+ defp maybe_add_init_internal_fetch_actor_task(children, :test), do: children
+
+ defp maybe_add_init_internal_fetch_actor_task(children, _) do
+ [
+ %{
+ id: :internal_fetch_init,
+ start: {Task, :start_link, [&Pleroma.Web.ActivityPub.InternalFetchActor.init/0]},
+ restart: :temporary
+ }
+ | children
+ ]
+ end
+
+ defp maybe_add_background_migrator(children, env) when env in [:test, :benchmark], do: children
+
+ defp maybe_add_background_migrator(children, _) do
+ [Pleroma.Migrators.HashtagsTableMigrator | children]
+ end
+
+ defp maybe_add(children, _, env) when env in [:test, :benchmark], do: children
+ defp maybe_add(children, :streamer, _), do: [Pleroma.Web.Streamer.registry() | children]
+
+ defp add_http_children(children, :test) do
+ [Pleroma.HTTP.HackneySupervisor, Pleroma.Gun.GunSupervisor | children]
+ end
+
+ defp add_http_children(children, _), do: [adapter_module() | children]
+
+ defp maybe_add_chat_child(children) do
+ if Config.get([:chat, :enabled]) do
+ [Pleroma.Application.ChatSupervisor | children]
+ else
+ children
+ end
+ end
+end
diff --git a/lib/pleroma/config/converter.ex b/lib/pleroma/config/converter.ex
new file mode 100644
index 000000000..bf3e16bf5
--- /dev/null
+++ b/lib/pleroma/config/converter.ex
@@ -0,0 +1,195 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Config.Converter do
+ @moduledoc """
+ Converts json structures into elixir structures and types and vice versa.
+ """
+ @spec to_elixir_types(boolean() | String.t() | map() | list()) :: term()
+ def to_elixir_types(%{"tuple" => [":args", args]}) when is_list(args) do
+ arguments =
+ Enum.map(args, fn arg ->
+ if String.contains?(arg, ["{", "}"]) do
+ {elem, []} = Code.eval_string(arg)
+ elem
+ else
+ to_elixir_types(arg)
+ end
+ end)
+
+ {:args, arguments}
+ end
+
+ def to_elixir_types(%{"tuple" => [":proxy_url", %{"tuple" => [type, host, port]}]}) do
+ {:proxy_url, {string_to_elixir_types!(type), parse_host(host), port}}
+ end
+
+ def to_elixir_types(%{"tuple" => [":partial_chain", entity]}) do
+ {partial_chain, []} =
+ entity
+ |> String.replace(~r/[^\w|^{:,[|^,|^[|^\]^}|^\/|^\.|^"]^\s/, "")
+ |> Code.eval_string()
+
+ {:partial_chain, partial_chain}
+ end
+
+ def to_elixir_types(%{"tuple" => entity}) do
+ Enum.reduce(entity, {}, &Tuple.append(&2, to_elixir_types(&1)))
+ end
+
+ def to_elixir_types(entity) when is_map(entity) do
+ Map.new(entity, fn {k, v} -> {to_elixir_types(k), to_elixir_types(v)} end)
+ end
+
+ def to_elixir_types(entity) when is_list(entity) do
+ Enum.map(entity, &to_elixir_types/1)
+ end
+
+ def to_elixir_types(entity) when is_binary(entity) do
+ entity
+ |> String.trim()
+ |> string_to_elixir_types!()
+ end
+
+ def to_elixir_types(entity), do: entity
+
+ defp parse_host("localhost"), do: :localhost
+
+ defp parse_host(host) do
+ charlist = to_charlist(host)
+
+ case :inet.parse_address(charlist) do
+ {:error, :einval} ->
+ charlist
+
+ {:ok, ip} ->
+ ip
+ end
+ end
+
+ @spec string_to_elixir_types!(String.t()) ::
+ atom() | Regex.t() | module() | String.t() | no_return()
+ def string_to_elixir_types!("~r" <> _pattern = regex) do
+ pattern =
+ ~r/^~r(?'delimiter'[\/|"'([{<]{1})(?'pattern'.+)[\/|"')\]}>]{1}(?'modifier'[uismxfU]*)/u
+
+ delimiters = ["/", "|", "\"", "'", {"(", ")"}, {"[", "]"}, {"{", "}"}, {"<", ">"}]
+
+ with %{"modifier" => modifier, "pattern" => pattern, "delimiter" => regex_delimiter} <-
+ Regex.named_captures(pattern, regex),
+ {:ok, {leading, closing}} <- find_valid_delimiter(delimiters, pattern, regex_delimiter),
+ {result, _} <- Code.eval_string("~r#{leading}#{pattern}#{closing}#{modifier}") do
+ result
+ end
+ end
+
+ def string_to_elixir_types!(":" <> atom), do: String.to_atom(atom)
+
+ def string_to_elixir_types!(value) do
+ if module_name?(value) do
+ String.to_existing_atom("Elixir." <> value)
+ else
+ value
+ end
+ end
+
+ defp find_valid_delimiter([], _string, _) do
+ raise(ArgumentError, message: "valid delimiter for Regex expression not found")
+ end
+
+ defp find_valid_delimiter([{leading, closing} = delimiter | others], pattern, regex_delimiter)
+ when is_tuple(delimiter) do
+ if String.contains?(pattern, closing) do
+ find_valid_delimiter(others, pattern, regex_delimiter)
+ else
+ {:ok, {leading, closing}}
+ end
+ end
+
+ defp find_valid_delimiter([delimiter | others], pattern, regex_delimiter) do
+ if String.contains?(pattern, delimiter) do
+ find_valid_delimiter(others, pattern, regex_delimiter)
+ else
+ {:ok, {delimiter, delimiter}}
+ end
+ end
+
+ @spec module_name?(String.t()) :: boolean()
+ def module_name?(string) do
+ Regex.match?(~r/^(Pleroma|Phoenix|Tesla|Quack|Ueberauth|Swoosh)\./, string) or
+ string in ["Oban", "Ueberauth", "ExSyslogger"]
+ end
+
+ @spec to_json_types(term()) :: map() | list() | boolean() | String.t() | integer()
+ def to_json_types(entity) when is_list(entity) do
+ Enum.map(entity, &to_json_types/1)
+ end
+
+ def to_json_types(%Regex{} = entity), do: inspect(entity)
+
+ def to_json_types(entity) when is_map(entity) do
+ Map.new(entity, fn {k, v} -> {to_json_types(k), to_json_types(v)} end)
+ end
+
+ def to_json_types({:args, args}) when is_list(args) do
+ arguments =
+ Enum.map(args, fn
+ arg when is_tuple(arg) -> inspect(arg)
+ arg -> to_json_types(arg)
+ end)
+
+ %{"tuple" => [":args", arguments]}
+ end
+
+ def to_json_types({:proxy_url, {type, :localhost, port}}) do
+ %{"tuple" => [":proxy_url", %{"tuple" => [to_json_types(type), "localhost", port]}]}
+ end
+
+ def to_json_types({:proxy_url, {type, host, port}}) when is_tuple(host) do
+ ip =
+ host
+ |> :inet_parse.ntoa()
+ |> to_string()
+
+ %{
+ "tuple" => [
+ ":proxy_url",
+ %{"tuple" => [to_json_types(type), ip, port]}
+ ]
+ }
+ end
+
+ def to_json_types({:proxy_url, {type, host, port}}) do
+ %{
+ "tuple" => [
+ ":proxy_url",
+ %{"tuple" => [to_json_types(type), to_string(host), port]}
+ ]
+ }
+ end
+
+ def to_json_types({:partial_chain, entity}),
+ do: %{"tuple" => [":partial_chain", inspect(entity)]}
+
+ def to_json_types(entity) when is_tuple(entity) do
+ value =
+ entity
+ |> Tuple.to_list()
+ |> to_json_types()
+
+ %{"tuple" => value}
+ end
+
+ def to_json_types(entity) when is_binary(entity), do: entity
+
+ def to_json_types(entity) when is_boolean(entity) or is_number(entity) or is_nil(entity) do
+ entity
+ end
+
+ def to_json_types(entity) when entity in [:"tlsv1.1", :"tlsv1.2", :"tlsv1.3"] do
+ ":#{entity}"
+ end
+
+ def to_json_types(entity) when is_atom(entity), do: inspect(entity)
+end
diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex
index 24aa5993b..19868d174 100644
--- a/lib/pleroma/config/deprecation_warnings.ex
+++ b/lib/pleroma/config/deprecation_warnings.ex
@@ -41,7 +41,8 @@ defmodule Pleroma.Config.DeprecationWarnings do
:ok <- check_gun_pool_options(),
:ok <- check_activity_expiration_config(),
:ok <- check_remote_ip_plug_name(),
- :ok <- check_uploders_s3_public_endpoint() do
+ :ok <- check_uploders_s3_public_endpoint(),
+ :ok <- check_oban_config() do
:ok
else
_ ->
@@ -79,7 +80,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
move_namespace_and_warn(@mrf_config_map, warning_preface)
end
- @spec move_namespace_and_warn([config_map()], String.t()) :: :ok | nil
+ @spec move_namespace_and_warn([config_map()], String.t()) :: :ok | :error
def move_namespace_and_warn(config_map, warning_preface) do
warning =
Enum.reduce(config_map, "", fn
@@ -102,7 +103,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
end
end
- @spec check_media_proxy_whitelist_config() :: :ok | nil
+ @spec check_media_proxy_whitelist_config() :: :ok | :error
def check_media_proxy_whitelist_config do
whitelist = Config.get([:media_proxy, :whitelist])
@@ -163,7 +164,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
end
end
- @spec check_activity_expiration_config() :: :ok | nil
+ @spec check_activity_expiration_config() :: :ok | :error
def check_activity_expiration_config do
warning_preface = """
!!!DEPRECATION WARNING!!!
@@ -215,4 +216,41 @@ defmodule Pleroma.Config.DeprecationWarnings do
:ok
end
end
+
+ @spec check_oban_config() :: :ok | :error
+ def check_oban_config do
+ oban_config = Config.get(Oban)
+
+ {crontab, changed?} =
+ [
+ Pleroma.Workers.Cron.StatsWorker,
+ Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker,
+ Pleroma.Workers.Cron.ClearOauthTokenWorker
+ ]
+ |> Enum.reduce({oban_config[:crontab], false}, fn removed_worker, {acc, changed?} ->
+ with acc when is_list(acc) <- acc,
+ setting when is_tuple(setting) <-
+ Enum.find(acc, fn {_, worker} -> worker == removed_worker end) do
+ """
+ !!!OBAN CONFIG WARNING!!!
+ You are using old workers in Oban crontab settings, which were removed.
+ Please, remove setting from crontab in your config file (prod.secret.exs): #{
+ inspect(setting)
+ }
+ """
+ |> Logger.warn()
+
+ {List.delete(acc, setting), true}
+ else
+ _ -> {acc, changed?}
+ end
+ end)
+
+ if changed? do
+ Config.put(Oban, Keyword.put(oban_config, :crontab, crontab))
+ :error
+ else
+ :ok
+ end
+ end
end
diff --git a/lib/pleroma/config/loader.ex b/lib/pleroma/config/loader.ex
index b64d06707..69fd458c0 100644
--- a/lib/pleroma/config/loader.ex
+++ b/lib/pleroma/config/loader.ex
@@ -3,57 +3,73 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config.Loader do
+ @reject_groups [
+ :postgrex,
+ :tesla,
+ :phoenix,
+ :tzdata,
+ :http_signatures,
+ :web_push_encryption,
+ :floki,
+ :pbkdf2_elixir
+ ]
+
@reject_keys [
Pleroma.Repo,
Pleroma.Web.Endpoint,
:env,
:configurable_from_database,
:database,
- :swarm
- ]
-
- @reject_groups [
- :postgrex,
- :tesla
+ :ecto_repos,
+ Pleroma.Gun,
+ Pleroma.ReverseProxy.Client,
+ Pleroma.Web.Auth.Authenticator
]
if Code.ensure_loaded?(Config.Reader) do
@reader Config.Reader
-
- def read(path), do: @reader.read!(path)
+ @config_header "import Config\r\n\r\n"
else
# support for Elixir less than 1.9
@reader Mix.Config
- def read(path) do
- path
- |> @reader.eval!()
- |> elem(0)
- end
+ @config_header "use Mix.Config\r\n\r\n"
end
- @spec read(Path.t()) :: keyword()
+ @spec read!(Path.t()) :: keyword()
+ def read!(path), do: @reader.read!(path)
@spec merge(keyword(), keyword()) :: keyword()
def merge(c1, c2), do: @reader.merge(c1, c2)
+ @spec config_header() :: String.t()
+ def config_header, do: @config_header
+
@spec default_config() :: keyword()
def default_config do
- "config/config.exs"
- |> read()
- |> filter()
- end
+ config =
+ "config/config.exs"
+ |> read!()
+ |> filter()
+
+ logger_config =
+ :logger
+ |> Application.get_all_env()
+ |> Enum.filter(fn {key, _} -> key in [:backends, :console, :ex_syslogger] end)
- defp filter(configs) do
- configs
- |> Keyword.keys()
- |> Enum.reduce([], &Keyword.put(&2, &1, filter_group(&1, configs)))
+ merge(config, logger: logger_config)
end
- @spec filter_group(atom(), keyword()) :: keyword()
- def filter_group(group, configs) do
- Enum.reject(configs[group], fn {key, _v} ->
- key in @reject_keys or group in @reject_groups or
- (group == :phoenix and key == :serve_endpoints)
+ @spec filter(keyword()) :: keyword()
+ def filter(configs) do
+ Enum.reduce(configs, [], fn
+ {group, _settings}, group_acc when group in @reject_groups ->
+ group_acc
+
+ {group, settings}, group_acc ->
+ Enum.reduce(settings, group_acc, fn
+ {key, _value}, acc when key in @reject_keys -> acc
+ setting, acc -> Keyword.update(acc, group, [setting], &Keyword.merge(&1, [setting]))
+ end)
end)
end
end
diff --git a/lib/pleroma/config/oban.ex b/lib/pleroma/config/oban.ex
deleted file mode 100644
index 3e63bca40..000000000
--- a/lib/pleroma/config/oban.ex
+++ /dev/null
@@ -1,38 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Config.Oban do
- require Logger
-
- def warn do
- oban_config = Pleroma.Config.get(Oban)
-
- crontab =
- [
- Pleroma.Workers.Cron.StatsWorker,
- Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker,
- Pleroma.Workers.Cron.ClearOauthTokenWorker
- ]
- |> Enum.reduce(oban_config[:crontab], fn removed_worker, acc ->
- with acc when is_list(acc) <- acc,
- setting when is_tuple(setting) <-
- Enum.find(acc, fn {_, worker} -> worker == removed_worker end) do
- """
- !!!OBAN CONFIG WARNING!!!
- You are using old workers in Oban crontab settings, which were removed.
- Please, remove setting from crontab in your config file (prod.secret.exs): #{
- inspect(setting)
- }
- """
- |> Logger.warn()
-
- List.delete(acc, setting)
- else
- _ -> acc
- end
- end)
-
- Pleroma.Config.put(Oban, Keyword.put(oban_config, :crontab, crontab))
- end
-end
diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex
deleted file mode 100644
index aad45aab8..000000000
--- a/lib/pleroma/config/transfer_task.ex
+++ /dev/null
@@ -1,201 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Config.TransferTask do
- use Task
-
- alias Pleroma.Config
- alias Pleroma.ConfigDB
- alias Pleroma.Repo
-
- require Logger
-
- @type env() :: :test | :benchmark | :dev | :prod
-
- @reboot_time_keys [
- {:pleroma, :hackney_pools},
- {:pleroma, :chat},
- {:pleroma, Oban},
- {:pleroma, :rate_limit},
- {:pleroma, :markup},
- {:pleroma, :streamer},
- {:pleroma, :pools},
- {:pleroma, :connections_pool}
- ]
-
- @reboot_time_subkeys [
- {:pleroma, Pleroma.Captcha, [:seconds_valid]},
- {:pleroma, Pleroma.Upload, [:proxy_remote]},
- {:pleroma, :instance, [:upload_limit]},
- {:pleroma, :gopher, [:enabled]}
- ]
-
- def start_link(restart_pleroma? \\ true) do
- load_and_update_env([], restart_pleroma?)
- if Config.get(:env) == :test, do: Ecto.Adapters.SQL.Sandbox.checkin(Repo)
- :ignore
- end
-
- @spec load_and_update_env([ConfigDB.t()], boolean()) :: :ok
- def load_and_update_env(deleted_settings \\ [], restart_pleroma? \\ true) do
- with {_, true} <- {:configurable, Config.get(:configurable_from_database)} do
- # We need to restart applications for loaded settings take effect
-
- {logger, other} =
- (Repo.all(ConfigDB) ++ deleted_settings)
- |> Enum.map(&merge_with_default/1)
- |> Enum.split_with(fn {group, _, _, _} -> group in [:logger, :quack] end)
-
- logger
- |> Enum.sort()
- |> Enum.each(&configure/1)
-
- started_applications = Application.started_applications()
-
- # TODO: some problem with prometheus after restart!
- reject = [nil, :prometheus, :postgrex]
-
- reject =
- if restart_pleroma? do
- reject
- else
- [:pleroma | reject]
- end
-
- other
- |> Enum.map(&update/1)
- |> Enum.uniq()
- |> Enum.reject(&(&1 in reject))
- |> maybe_set_pleroma_last()
- |> Enum.each(&restart(started_applications, &1, Config.get(:env)))
-
- :ok
- else
- {:configurable, false} -> Restarter.Pleroma.rebooted()
- end
- end
-
- defp maybe_set_pleroma_last(apps) do
- # to be ensured that pleroma will be restarted last
- if :pleroma in apps do
- apps
- |> List.delete(:pleroma)
- |> List.insert_at(-1, :pleroma)
- else
- Restarter.Pleroma.rebooted()
- apps
- end
- end
-
- defp merge_with_default(%{group: group, key: key, value: value} = setting) do
- default = Config.Holder.default_config(group, key)
-
- merged =
- cond do
- Ecto.get_meta(setting, :state) == :deleted -> default
- can_be_merged?(default, value) -> ConfigDB.merge_group(group, key, default, value)
- true -> value
- end
-
- {group, key, value, merged}
- end
-
- # change logger configuration in runtime, without restart
- defp configure({:quack, key, _, merged}) do
- Logger.configure_backend(Quack.Logger, [{key, merged}])
- :ok = update_env(:quack, key, merged)
- end
-
- defp configure({_, :backends, _, merged}) do
- # removing current backends
- Enum.each(Application.get_env(:logger, :backends), &Logger.remove_backend/1)
-
- Enum.each(merged, &Logger.add_backend/1)
-
- :ok = update_env(:logger, :backends, merged)
- end
-
- defp configure({_, key, _, merged}) when key in [:console, :ex_syslogger] do
- merged =
- if key == :console do
- put_in(merged[:format], merged[:format] <> "\n")
- else
- merged
- end
-
- backend =
- if key == :ex_syslogger,
- do: {ExSyslogger, :ex_syslogger},
- else: key
-
- Logger.configure_backend(backend, merged)
- :ok = update_env(:logger, key, merged)
- end
-
- defp configure({_, key, _, merged}) do
- Logger.configure([{key, merged}])
- :ok = update_env(:logger, key, merged)
- end
-
- defp update({group, key, value, merged}) do
- try do
- :ok = update_env(group, key, merged)
-
- if group != :pleroma or pleroma_need_restart?(group, key, value), do: group
- rescue
- error ->
- error_msg =
- "updating env causes error, group: #{inspect(group)}, key: #{inspect(key)}, value: #{
- inspect(value)
- } error: #{inspect(error)}"
-
- Logger.warn(error_msg)
-
- nil
- end
- end
-
- defp update_env(group, key, nil), do: Application.delete_env(group, key)
- defp update_env(group, key, value), do: Application.put_env(group, key, value)
-
- @spec pleroma_need_restart?(atom(), atom(), any()) :: boolean()
- def pleroma_need_restart?(group, key, value) do
- group_and_key_need_reboot?(group, key) or group_and_subkey_need_reboot?(group, key, value)
- end
-
- defp group_and_key_need_reboot?(group, key) do
- Enum.any?(@reboot_time_keys, fn {g, k} -> g == group and k == key end)
- end
-
- defp group_and_subkey_need_reboot?(group, key, value) do
- Keyword.keyword?(value) and
- Enum.any?(@reboot_time_subkeys, fn {g, k, subkeys} ->
- g == group and k == key and
- Enum.any?(Keyword.keys(value), &(&1 in subkeys))
- end)
- end
-
- defp restart(_, :pleroma, env), do: Restarter.Pleroma.restart_after_boot(env)
-
- defp restart(started_applications, app, _) do
- with {^app, _, _} <- List.keyfind(started_applications, app, 0),
- :ok <- Application.stop(app) do
- :ok = Application.start(app)
- else
- nil ->
- Logger.warn("#{app} is not started.")
-
- error ->
- error
- |> inspect()
- |> Logger.warn()
- end
- end
-
- defp can_be_merged?(val1, val2) when is_list(val1) and is_list(val2) do
- Keyword.keyword?(val1) and Keyword.keyword?(val2)
- end
-
- defp can_be_merged?(_val1, _val2), do: false
-end
diff --git a/lib/pleroma/config/version.ex b/lib/pleroma/config/version.ex
new file mode 100644
index 000000000..2f66cc039
--- /dev/null
+++ b/lib/pleroma/config/version.ex
@@ -0,0 +1,25 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Config.Version do
+ @moduledoc """
+ IMPORTANT!!!
+ Before modifying records in the database directly, please read "Config versioning" in `docs/development/config_versioning.md`.
+ """
+
+ use Ecto.Schema
+
+ import Ecto.Query, only: [from: 2]
+
+ schema "config_versions" do
+ field(:backup, Pleroma.EctoType.Config.BinaryValue)
+ field(:current, :boolean, default: true)
+
+ timestamps()
+ end
+
+ def all do
+ from(v in __MODULE__, order_by: [desc: v.id]) |> Pleroma.Repo.all()
+ end
+end
diff --git a/lib/pleroma/config/versioning.ex b/lib/pleroma/config/versioning.ex
new file mode 100644
index 000000000..b997da1db
--- /dev/null
+++ b/lib/pleroma/config/versioning.ex
@@ -0,0 +1,292 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Config.Versioning do
+ @moduledoc """
+ Module that manages versions of database configs.
+ """
+
+ import Ecto.Query, only: [from: 2]
+
+ alias Ecto.Multi
+ alias Pleroma.Config.Version
+ alias Pleroma.ConfigDB
+ alias Pleroma.Repo
+
+ @type change :: %{
+ optional(:delete) => boolean(),
+ optional(:value) => any(),
+ group: atom(),
+ key: atom() | nil
+ }
+
+ @doc """
+ Creates new config version:
+ - convert changes to elixir types
+ - splits changes by type and processes them in `config` table
+ - sets all pointers to false
+ - gets all rows from `config` table and inserts them as keyword in `backup` field
+ """
+ @spec new_version([change()] | change()) ::
+ {:ok, map()} | {:error, :no_changes} | {:error, atom() | tuple(), any(), any()}
+ def new_version([]), do: {:error, :empty_changes}
+ def new_version(change) when is_map(change), do: new_version([change])
+
+ def new_version(changes) when is_list(changes) do
+ changes
+ |> Enum.reduce(Multi.new(), fn
+ %{delete: true} = deletion, acc ->
+ Multi.run(acc, {:delete_or_update, deletion[:group], deletion[:key]}, fn _, _ ->
+ ConfigDB.delete_or_update(deletion)
+ end)
+
+ operation, acc ->
+ {name, fun} =
+ if Keyword.keyword?(operation[:value]) or
+ (operation[:group] == :pleroma and
+ operation[:key] in ConfigDB.pleroma_not_keyword_values()) do
+ {:insert_or_update,
+ fn _, _ ->
+ ConfigDB.update_or_create(operation)
+ end}
+ else
+ {:error,
+ fn _, _ ->
+ {:error, {:value_must_be_keyword, operation}}
+ end}
+ end
+
+ Multi.run(acc, {name, operation[:group], operation[:key]}, fun)
+ end)
+ |> set_current_flag_false_for_all_versions()
+ |> insert_new_version()
+ |> Repo.transaction()
+ end
+
+ def new_version(_), do: {:error, :bad_format}
+
+ defp set_current_flag_false_for_all_versions(multi) do
+ Multi.update_all(multi, :update_all_versions, Version, set: [current: false])
+ end
+
+ defp insert_new_version(multi) do
+ Multi.run(multi, :insert_version, fn repo, _ ->
+ %Version{
+ backup: ConfigDB.all_as_keyword()
+ }
+ |> repo.insert()
+ end)
+ end
+
+ @doc """
+ Rollbacks config version by N steps:
+ - checks possibility for rollback
+ - truncates config table and restarts pk
+ - inserts config settings from backup
+ - sets all pointers to false
+ - sets current pointer to true for rollback version
+ - deletes versions after current
+ """
+ @spec rollback(pos_integer()) ::
+ {:ok, map()}
+ | {:error, atom() | tuple(), any(), any()}
+ | {:error, :steps_format}
+ | {:error, :no_current_version}
+ | {:error, :rollback_not_possible}
+ def rollback(steps \\ 1)
+
+ def rollback(steps) when is_integer(steps) and steps > 0 do
+ with version_id when is_integer(version_id) <- get_current_version_id(),
+ %Version{} = version <- get_version_by_steps(steps) do
+ do_rollback(version)
+ end
+ end
+
+ def rollback(_), do: {:error, :steps_format}
+
+ @doc """
+ Same as `rollback/1`, but rollbacks for a given version id.
+ """
+ @spec rollback_by_id(pos_integer()) ::
+ {:ok, map()}
+ | {:error, atom() | tuple(), any(), any()}
+ | {:error, :not_found}
+ | {:error, :version_is_already_current}
+ def rollback_by_id(id) when is_integer(id) do
+ with %Version{current: false} = version <- get_version_by_id(id) do
+ do_rollback(version)
+ else
+ %Version{current: true} -> {:error, :version_is_already_current}
+ error -> error
+ end
+ end
+
+ defp get_current_version_id do
+ query = from(v in Version, where: v.current == true)
+
+ with nil <- Repo.aggregate(query, :max, :id) do
+ {:error, :no_current_version}
+ end
+ end
+
+ defp get_version_by_id(id) do
+ with nil <- Repo.get(Version, id) do
+ {:error, :not_found}
+ end
+ end
+
+ defp get_version_by_steps(steps) do
+ query = from(v in Version, order_by: [desc: v.id], limit: 1, offset: ^steps)
+
+ with nil <- Repo.one(query) do
+ {:error, :rollback_not_possible}
+ end
+ end
+
+ defp do_rollback(version) do
+ multi =
+ truncate_config_table()
+ |> reset_pk_in_config_table()
+
+ version.backup
+ |> ConfigDB.from_keyword_to_maps()
+ |> add_insert_commands(multi)
+ |> set_current_flag_false_for_all_versions()
+ |> Multi.update(:move_current_pointer, Ecto.Changeset.change(version, current: true))
+ |> Multi.delete_all(
+ :delete_next_versions,
+ from(v in Version, where: v.id > ^version.id)
+ )
+ |> Repo.transaction()
+ end
+
+ defp truncate_config_table(multi \\ Multi.new()) do
+ Multi.run(multi, :truncate_config_table, fn repo, _ ->
+ repo.query("TRUNCATE config;")
+ end)
+ end
+
+ defp reset_pk_in_config_table(multi) do
+ Multi.run(multi, :reset_pk, fn repo, _ ->
+ repo.query("ALTER SEQUENCE config_id_seq RESTART;")
+ end)
+ end
+
+ defp add_insert_commands(changes, multi) do
+ Enum.reduce(changes, multi, fn change, acc ->
+ Multi.run(acc, {:insert, change[:group], change[:key]}, fn _, _ ->
+ ConfigDB.update_or_create(change)
+ end)
+ end)
+ end
+
+ @doc """
+ Resets config table and creates new empty version.
+ """
+ @spec reset() :: {:ok, map()} | {:error, atom() | tuple(), any(), any()}
+ def reset do
+ truncate_config_table()
+ |> reset_pk_in_config_table()
+ |> set_current_flag_false_for_all_versions()
+ |> insert_new_version()
+ |> Repo.transaction()
+ end
+
+ @doc """
+ Migrates settings from config file into database:
+ - truncates config table and restarts pk
+ - inserts settings from config file
+ - sets all pointers to false
+ - gets all rows from `config` table and inserts them as keyword in `backup` field
+ """
+ @spec migrate(Path.t()) :: {:ok, map()} | {:error, atom() | tuple(), any(), any()}
+ def migrate(config_path) do
+ multi =
+ truncate_config_table()
+ |> reset_pk_in_config_table()
+
+ config_path
+ |> Pleroma.Config.Loader.read!()
+ |> Pleroma.Config.Loader.filter()
+ |> ConfigDB.from_keyword_to_maps()
+ |> add_insert_commands(multi)
+ |> set_current_flag_false_for_all_versions()
+ |> insert_new_version()
+ |> Repo.transaction()
+ end
+
+ @doc """
+ Common function to migrate old config namespace to the new one keeping the old value.
+ """
+ @spec migrate_namespace({atom(), atom()}, {atom(), atom()}) ::
+ {:ok, map()} | {:error, atom() | tuple(), any(), any()}
+ def migrate_namespace({o_group, o_key}, {n_group, n_key}) do
+ config = ConfigDB.get_by_params(%{group: o_group, key: o_key})
+
+ configs_changes_fun =
+ if config do
+ fn ->
+ config
+ |> Ecto.Changeset.change(group: n_group, key: n_key)
+ |> Repo.update()
+ end
+ else
+ fn -> {:ok, nil} end
+ end
+
+ versions_changes_fun = fn %{backup: backup} = version ->
+ with {value, rest} when not is_nil(value) <- pop_in(backup[o_group][o_key]) do
+ rest =
+ if rest[o_group] == [] do
+ Keyword.delete(rest, o_group)
+ else
+ rest
+ end
+
+ updated_backup =
+ if Keyword.has_key?(rest, n_group) do
+ put_in(rest[n_group][n_key], value)
+ else
+ Keyword.put(rest, n_group, [{n_key, value}])
+ end
+
+ version
+ |> Ecto.Changeset.change(backup: updated_backup)
+ |> Repo.update()
+ else
+ _ -> {:ok, nil}
+ end
+ end
+
+ migrate_configs_and_versions(configs_changes_fun, versions_changes_fun)
+ end
+
+ @doc """
+ Abstract function for config migrations to keep changes in config table and changes in versions backups in transaction.
+ Accepts two functions:
+ - first function makes changes to the configs
+ - second function makes changes to the backups in versions
+ """
+ @spec migrate_configs_and_versions(function(), function()) ::
+ {:ok, map()} | {:error, atom() | tuple(), any(), any()}
+ def migrate_configs_and_versions(configs_changes_fun, version_change_fun)
+ when is_function(configs_changes_fun, 0) and
+ is_function(version_change_fun, 1) do
+ versions = Repo.all(Version)
+
+ multi =
+ Multi.new()
+ |> Multi.run(:configs_changes, fn _, _ ->
+ configs_changes_fun.()
+ end)
+
+ versions
+ |> Enum.reduce(multi, fn version, acc ->
+ Multi.run(acc, {:version_change, version.id}, fn _, _ ->
+ version_change_fun.(version)
+ end)
+ end)
+ |> Repo.transaction()
+ end
+end
diff --git a/lib/pleroma/config_db.ex b/lib/pleroma/config_db.ex
index b874e0e37..7a29096c5 100644
--- a/lib/pleroma/config_db.ex
+++ b/lib/pleroma/config_db.ex
@@ -6,8 +6,7 @@ defmodule Pleroma.ConfigDB do
use Ecto.Schema
import Ecto.Changeset
- import Ecto.Query, only: [select: 3, from: 2]
- import Pleroma.Web.Gettext
+ import Ecto.Query, only: [from: 2]
alias __MODULE__
alias Pleroma.Repo
@@ -22,6 +21,10 @@ defmodule Pleroma.ConfigDB do
{:pleroma, :mrf_keyword, :replace}
]
+ @groups_without_keys [:quack, :mime, :cors_plug, :esshd, :ex_aws, :joken, :logger, :swoosh]
+
+ @pleroma_not_keyword_values [Pleroma.Web.Auth.Authenticator, :admin_token]
+
schema "config" do
field(:key, Pleroma.EctoType.Config.Atom)
field(:group, Pleroma.EctoType.Config.Atom)
@@ -31,13 +34,35 @@ defmodule Pleroma.ConfigDB do
timestamps()
end
- @spec get_all_as_keyword() :: keyword()
- def get_all_as_keyword do
- ConfigDB
- |> select([c], {c.group, c.key, c.value})
- |> Repo.all()
- |> Enum.reduce([], fn {group, key, value}, acc ->
- Keyword.update(acc, group, [{key, value}], &Keyword.merge(&1, [{key, value}]))
+ @spec all() :: [t()]
+ def all, do: Repo.all(ConfigDB)
+
+ @spec all_with_db() :: [t()]
+ def all_with_db do
+ all()
+ |> Enum.map(fn
+ %{group: :pleroma, key: key} = change when key in @pleroma_not_keyword_values ->
+ %{change | db: [change.key]}
+
+ %{value: value} = change ->
+ %{change | db: Keyword.keys(value)}
+ end)
+ end
+
+ @spec all_as_keyword() :: keyword()
+ def all_as_keyword do
+ all()
+ |> as_keyword()
+ end
+
+ @spec as_keyword([t()]) :: keyword()
+ def as_keyword(changes) do
+ Enum.reduce(changes, [], fn
+ %{group: group, key: nil, value: value}, acc ->
+ Keyword.update(acc, group, value, &Keyword.merge(&1, value))
+
+ %{group: group, key: key, value: value}, acc ->
+ Keyword.update(acc, group, [{key, value}], &Keyword.merge(&1, [{key, value}]))
end)
end
@@ -52,14 +77,22 @@ defmodule Pleroma.ConfigDB do
end
@spec get_by_params(map()) :: ConfigDB.t() | nil
- def get_by_params(%{group: _, key: _} = params), do: Repo.get_by(ConfigDB, params)
+ def get_by_params(%{group: group, key: key} = params)
+ when not is_nil(key) and not is_nil(group) do
+ Repo.get_by(ConfigDB, params)
+ end
+
+ def get_by_params(%{group: group}) do
+ from(c in ConfigDB, where: c.group == ^group and is_nil(c.key)) |> Repo.one()
+ end
@spec changeset(ConfigDB.t(), map()) :: Changeset.t()
def changeset(config, params \\ %{}) do
config
|> cast(params, [:key, :group, :value])
- |> validate_required([:key, :group, :value])
+ |> validate_required([:group, :value])
|> unique_constraint(:key, name: :config_group_key_index)
+ |> unique_constraint(:key, name: :config_group__key_is_null_index)
end
defp create(params) do
@@ -74,319 +107,214 @@ defmodule Pleroma.ConfigDB do
|> Repo.update()
end
- @spec get_db_keys(keyword(), any()) :: [String.t()]
- def get_db_keys(value, key) do
- keys =
- if Keyword.keyword?(value) do
- Keyword.keys(value)
- else
- [key]
- end
-
- Enum.map(keys, &to_json_types(&1))
- end
-
- @spec merge_group(atom(), atom(), keyword(), keyword()) :: keyword()
- def merge_group(group, key, old_value, new_value) do
- new_keys = to_mapset(new_value)
-
- intersect_keys = old_value |> to_mapset() |> MapSet.intersection(new_keys) |> MapSet.to_list()
-
- merged_value = ConfigDB.merge(old_value, new_value)
-
- @full_subkey_update
- |> Enum.map(fn
- {g, k, subkey} when g == group and k == key ->
- if subkey in intersect_keys, do: subkey, else: []
-
- _ ->
- []
- end)
- |> List.flatten()
- |> Enum.reduce(merged_value, &Keyword.put(&2, &1, new_value[&1]))
- end
-
- defp to_mapset(keyword) do
- keyword
- |> Keyword.keys()
- |> MapSet.new()
- end
-
- @spec sub_key_full_update?(atom(), atom(), [Keyword.key()]) :: boolean()
- def sub_key_full_update?(group, key, subkeys) do
- Enum.any?(@full_subkey_update, fn {g, k, subkey} ->
- g == group and k == key and subkey in subkeys
- end)
- end
-
- @spec merge(keyword(), keyword()) :: keyword()
- def merge(config1, config2) when is_list(config1) and is_list(config2) do
- Keyword.merge(config1, config2, fn _, app1, app2 ->
- if Keyword.keyword?(app1) and Keyword.keyword?(app2) do
- Keyword.merge(app1, app2, &deep_merge/3)
- else
- app2
- end
- end)
- end
-
- defp deep_merge(_key, value1, value2) do
- if Keyword.keyword?(value1) and Keyword.keyword?(value2) do
- Keyword.merge(value1, value2, &deep_merge/3)
- else
- value2
- end
- end
-
+ @doc """
+ IMPORTANT!!!
+ Before modifying records in the database directly, please read "Config versioning" in `docs/development/config_versioning.md`.
+ """
@spec update_or_create(map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
def update_or_create(params) do
- params = Map.put(params, :value, to_elixir_types(params[:value]))
search_opts = Map.take(params, [:group, :key])
- with %ConfigDB{} = config <- ConfigDB.get_by_params(search_opts),
- {_, true, config} <- {:partial_update, can_be_partially_updated?(config), config},
- {_, true, config} <-
- {:can_be_merged, is_list(params[:value]) and is_list(config.value), config} do
+ with %ConfigDB{} = config <- ConfigDB.get_by_params(search_opts) do
new_value = merge_group(config.group, config.key, config.value, params[:value])
+
update(config, %{value: new_value})
else
- {reason, false, config} when reason in [:partial_update, :can_be_merged] ->
- update(config, params)
-
nil ->
create(params)
end
end
- defp can_be_partially_updated?(%ConfigDB{} = config), do: not only_full_update?(config)
-
- defp only_full_update?(%ConfigDB{group: group, key: key}) do
- full_key_update = [
- {:pleroma, :ecto_repos},
- {:quack, :meta},
- {:mime, :types},
- {:cors_plug, [:max_age, :methods, :expose, :headers]},
- {:swarm, :node_blacklist},
- {:logger, :backends}
- ]
-
- Enum.any?(full_key_update, fn
- {s_group, s_key} ->
- group == s_group and ((is_list(s_key) and key in s_key) or key == s_key)
- end)
- end
-
+ @doc """
+ IMPORTANT!!!
+ Before modifying records in the database directly, please read "Config versioning" in `docs/development/config_versioning.md`.
+ """
@spec delete(ConfigDB.t() | map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
def delete(%ConfigDB{} = config), do: Repo.delete(config)
- def delete(params) do
- search_opts = Map.delete(params, :subkeys)
+ @doc """
+ IMPORTANT!!!
+ Before modifying records in the database directly, please read "Config versioning" in `docs/development/config_versioning.md`.
+ """
+ @spec delete_or_update(map()) :: {:ok, t()} | {:ok, nil} | {:error, Changeset.t()}
+ def delete_or_update(%{group: _, key: key} = params) when not is_nil(key) do
+ search_opts = Map.take(params, [:group, :key])
- with %ConfigDB{} = config <- ConfigDB.get_by_params(search_opts),
- {config, sub_keys} when is_list(sub_keys) <- {config, params[:subkeys]},
- keys <- Enum.map(sub_keys, &string_to_elixir_types(&1)),
- {_, config, new_value} when new_value != [] <-
- {:partial_remove, config, Keyword.drop(config.value, keys)} do
- update(config, %{value: new_value})
+ with %ConfigDB{} = config <- ConfigDB.get_by_params(search_opts) do
+ do_delete_or_update(config, params[:subkeys])
else
- {:partial_remove, config, []} ->
- Repo.delete(config)
-
- {config, nil} ->
- Repo.delete(config)
+ _ -> {:ok, nil}
+ end
+ end
- nil ->
- err =
- dgettext("errors", "Config with params %{params} not found", params: inspect(params))
+ def delete_or_update(%{group: group}) do
+ query = from(c in ConfigDB, where: c.group == ^group)
- {:error, err}
+ with {num, _} <- Repo.delete_all(query) do
+ {:ok, num}
end
end
- @spec to_json_types(term()) :: map() | list() | boolean() | String.t()
- def to_json_types(entity) when is_list(entity) do
- Enum.map(entity, &to_json_types/1)
+ defp do_delete_or_update(%ConfigDB{} = config, subkeys)
+ when is_list(subkeys) and subkeys != [] do
+ new_value = Keyword.drop(config.value, subkeys)
+
+ if new_value == [] do
+ delete(config)
+ else
+ update(config, %{value: new_value})
+ end
end
- def to_json_types(%Regex{} = entity), do: inspect(entity)
+ defp do_delete_or_update(%ConfigDB{} = config, _), do: delete(config)
- def to_json_types(entity) when is_map(entity) do
- Map.new(entity, fn {k, v} -> {to_json_types(k), to_json_types(v)} end)
- end
+ defp merge_group(group, key, old_value, new_value)
+ when is_list(old_value) and is_list(new_value) do
+ new_keys = to_mapset(new_value)
- def to_json_types({:args, args}) when is_list(args) do
- arguments =
- Enum.map(args, fn
- arg when is_tuple(arg) -> inspect(arg)
- arg -> to_json_types(arg)
- end)
+ intersect_keys = old_value |> to_mapset() |> MapSet.intersection(new_keys) |> MapSet.to_list()
- %{"tuple" => [":args", arguments]}
- end
+ merged_value = deep_merge(old_value, new_value)
- def to_json_types({:proxy_url, {type, :localhost, port}}) do
- %{"tuple" => [":proxy_url", %{"tuple" => [to_json_types(type), "localhost", port]}]}
+ @full_subkey_update
+ |> Enum.reduce([], fn
+ {g, k, subkey}, acc when g == group and k == key ->
+ if subkey in intersect_keys do
+ [subkey | acc]
+ else
+ acc
+ end
+
+ _, acc ->
+ acc
+ end)
+ |> Enum.reduce(merged_value, &Keyword.put(&2, &1, new_value[&1]))
end
- def to_json_types({:proxy_url, {type, host, port}}) when is_tuple(host) do
- ip =
- host
- |> :inet_parse.ntoa()
- |> to_string()
+ defp merge_group(_group, _key, _old_value, new_value) when is_list(new_value), do: new_value
- %{
- "tuple" => [
- ":proxy_url",
- %{"tuple" => [to_json_types(type), ip, port]}
- ]
- }
+ defp merge_group(:pleroma, key, _old_value, new_value)
+ when key in @pleroma_not_keyword_values do
+ new_value
end
- def to_json_types({:proxy_url, {type, host, port}}) do
- %{
- "tuple" => [
- ":proxy_url",
- %{"tuple" => [to_json_types(type), to_string(host), port]}
- ]
- }
+ defp to_mapset(keyword) when is_list(keyword) do
+ keyword
+ |> Keyword.keys()
+ |> MapSet.new()
end
- def to_json_types({:partial_chain, entity}),
- do: %{"tuple" => [":partial_chain", inspect(entity)]}
-
- def to_json_types(entity) when is_tuple(entity) do
- value =
- entity
- |> Tuple.to_list()
- |> to_json_types()
-
- %{"tuple" => value}
+ defp deep_merge(config1, config2) when is_list(config1) and is_list(config2) do
+ Keyword.merge(config1, config2, fn _, app1, app2 ->
+ if Keyword.keyword?(app1) and Keyword.keyword?(app2) do
+ Keyword.merge(app1, app2, &deep_merge/3)
+ else
+ app2
+ end
+ end)
end
- def to_json_types(entity) when is_binary(entity), do: entity
+ defp deep_merge(_key, value1, value2) do
+ if Keyword.keyword?(value1) and Keyword.keyword?(value2) do
+ Keyword.merge(value1, value2, &deep_merge/3)
+ else
+ value2
+ end
+ end
- def to_json_types(entity) when is_boolean(entity) or is_number(entity) or is_nil(entity) do
- entity
+ @spec reduce_defaults_and_merge_with_changes([t()], keyword()) :: {[t()], keyword()}
+ def reduce_defaults_and_merge_with_changes(changes, defaults) do
+ Enum.reduce(changes, {[], defaults}, &reduce_default_and_merge_with_change/2)
end
- def to_json_types(entity) when entity in [:"tlsv1.1", :"tlsv1.2", :"tlsv1.3"] do
- ":#{entity}"
+ defp reduce_default_and_merge_with_change(%{group: group} = change, {acc, defaults})
+ when group in @groups_without_keys do
+ {default, remaining_defaults} = Keyword.pop(defaults, group)
+
+ change = merge_change_with_default(change, default)
+ {[change | acc], remaining_defaults}
end
- def to_json_types(entity) when is_atom(entity), do: inspect(entity)
+ defp reduce_default_and_merge_with_change(%{group: group, key: key} = change, {acc, defaults}) do
+ if defaults[group] do
+ {default, remaining_group_defaults} = Keyword.pop(defaults[group], key)
- @spec to_elixir_types(boolean() | String.t() | map() | list()) :: term()
- def to_elixir_types(%{"tuple" => [":args", args]}) when is_list(args) do
- arguments =
- Enum.map(args, fn arg ->
- if String.contains?(arg, ["{", "}"]) do
- {elem, []} = Code.eval_string(arg)
- elem
+ remaining_defaults =
+ if remaining_group_defaults == [] do
+ Keyword.delete(defaults, group)
else
- to_elixir_types(arg)
+ Keyword.put(defaults, group, remaining_group_defaults)
end
- end)
-
- {:args, arguments}
- end
- def to_elixir_types(%{"tuple" => [":proxy_url", %{"tuple" => [type, host, port]}]}) do
- {:proxy_url, {string_to_elixir_types(type), parse_host(host), port}}
- end
-
- def to_elixir_types(%{"tuple" => [":partial_chain", entity]}) do
- {partial_chain, []} =
- entity
- |> String.replace(~r/[^\w|^{:,[|^,|^[|^\]^}|^\/|^\.|^"]^\s/, "")
- |> Code.eval_string()
+ change = merge_change_with_default(change, default)
- {:partial_chain, partial_chain}
+ {[change | acc], remaining_defaults}
+ else
+ {[change | acc], defaults}
+ end
end
- def to_elixir_types(%{"tuple" => entity}) do
- Enum.reduce(entity, {}, &Tuple.append(&2, to_elixir_types(&1)))
+ @spec from_keyword_to_structs(keyword(), [] | [t()]) :: [t()]
+ def from_keyword_to_structs(keyword, initial_acc \\ []) do
+ Enum.reduce(keyword, initial_acc, &reduce_to_structs/2)
end
- def to_elixir_types(entity) when is_map(entity) do
- Map.new(entity, fn {k, v} -> {to_elixir_types(k), to_elixir_types(v)} end)
+ defp reduce_to_structs({group, config}, group_acc) when group in @groups_without_keys do
+ [struct(%ConfigDB{}, to_map(group, config)) | group_acc]
end
- def to_elixir_types(entity) when is_list(entity) do
- Enum.map(entity, &to_elixir_types/1)
+ defp reduce_to_structs({group, config}, group_acc) do
+ Enum.reduce(config, group_acc, fn {key, value}, acc ->
+ [struct(%ConfigDB{}, to_map(group, key, value)) | acc]
+ end)
end
- def to_elixir_types(entity) when is_binary(entity) do
- entity
- |> String.trim()
- |> string_to_elixir_types()
+ @spec from_keyword_to_maps(keyword(), [] | [map()]) :: [map()]
+ def from_keyword_to_maps(keyword, initial_acc \\ []) do
+ Enum.reduce(keyword, initial_acc, &reduce_to_maps/2)
end
- def to_elixir_types(entity), do: entity
-
- @spec string_to_elixir_types(String.t()) ::
- atom() | Regex.t() | module() | String.t() | no_return()
- def string_to_elixir_types("~r" <> _pattern = regex) do
- pattern =
- ~r/^~r(?'delimiter'[\/|"'([{<]{1})(?'pattern'.+)[\/|"')\]}>]{1}(?'modifier'[uismxfU]*)/u
-
- delimiters = ["/", "|", "\"", "'", {"(", ")"}, {"[", "]"}, {"{", "}"}, {"<", ">"}]
-
- with %{"modifier" => modifier, "pattern" => pattern, "delimiter" => regex_delimiter} <-
- Regex.named_captures(pattern, regex),
- {:ok, {leading, closing}} <- find_valid_delimiter(delimiters, pattern, regex_delimiter),
- {result, _} <- Code.eval_string("~r#{leading}#{pattern}#{closing}#{modifier}") do
- result
- end
+ defp reduce_to_maps({group, config}, group_acc) when group in @groups_without_keys do
+ [to_map(group, config) | group_acc]
end
- def string_to_elixir_types(":" <> atom), do: String.to_atom(atom)
-
- def string_to_elixir_types(value) do
- if module_name?(value) do
- String.to_existing_atom("Elixir." <> value)
- else
- value
- end
+ defp reduce_to_maps({group, config}, group_acc) do
+ Enum.reduce(config, group_acc, fn {key, value}, acc ->
+ [to_map(group, key, value) | acc]
+ end)
end
- defp parse_host("localhost"), do: :localhost
+ defp to_map(group, config), do: %{group: group, value: config}
- defp parse_host(host) do
- charlist = to_charlist(host)
+ defp to_map(group, key, value), do: %{group: group, key: key, value: value}
- case :inet.parse_address(charlist) do
- {:error, :einval} ->
- charlist
+ @spec merge_changes_with_defaults([t()], keyword()) :: [t()]
+ def merge_changes_with_defaults(changes, defaults) when is_list(changes) do
+ Enum.map(changes, fn
+ %{group: group} = change when group in @groups_without_keys ->
+ merge_change_with_default(change, defaults[group])
- {:ok, ip} ->
- ip
- end
+ %{group: group, key: key} = change ->
+ merge_change_with_default(change, defaults[group][key])
+ end)
end
- defp find_valid_delimiter([], _string, _) do
- raise(ArgumentError, message: "valid delimiter for Regex expression not found")
+ defp merge_change_with_default(change, default) do
+ %{change | value: merge_change_value_with_default(change, default)}
end
- defp find_valid_delimiter([{leading, closing} = delimiter | others], pattern, regex_delimiter)
- when is_tuple(delimiter) do
- if String.contains?(pattern, closing) do
- find_valid_delimiter(others, pattern, regex_delimiter)
+ @spec merge_change_value_with_default(t(), keyword()) :: keyword()
+ def merge_change_value_with_default(change, default) do
+ if Ecto.get_meta(change, :state) == :deleted do
+ default
else
- {:ok, {leading, closing}}
+ merge_group(change.group, change.key, default, change.value)
end
end
- defp find_valid_delimiter([delimiter | others], pattern, regex_delimiter) do
- if String.contains?(pattern, delimiter) do
- find_valid_delimiter(others, pattern, regex_delimiter)
- else
- {:ok, {delimiter, delimiter}}
- end
- end
+ @spec groups_without_keys() :: [atom()]
+ def groups_without_keys, do: @groups_without_keys
- @spec module_name?(String.t()) :: boolean()
- def module_name?(string) do
- Regex.match?(~r/^(Pleroma|Phoenix|Tesla|Quack|Ueberauth|Swoosh)\./, string) or
- string in ["Oban", "Ueberauth", "ExSyslogger"]
- end
+ @spec pleroma_not_keyword_values() :: [atom()]
+ def pleroma_not_keyword_values, do: @pleroma_not_keyword_values
end
diff --git a/lib/pleroma/docs/json.ex b/lib/pleroma/docs/json.ex
index f22432ea4..42ea15549 100644
--- a/lib/pleroma/docs/json.ex
+++ b/lib/pleroma/docs/json.ex
@@ -5,7 +5,7 @@
defmodule Pleroma.Docs.JSON do
@behaviour Pleroma.Docs.Generator
@external_resource "config/description.exs"
- @raw_config Pleroma.Config.Loader.read("config/description.exs")
+ @raw_config Pleroma.Config.Loader.read!("config/description.exs")
@raw_descriptions @raw_config[:pleroma][:config_description]
@term __MODULE__.Compiled
diff --git a/lib/pleroma/ecto_type/config/atom.ex b/lib/pleroma/ecto_type/config/atom.ex
index 3bf0bca5b..35b459c06 100644
--- a/lib/pleroma/ecto_type/config/atom.ex
+++ b/lib/pleroma/ecto_type/config/atom.ex
@@ -12,13 +12,13 @@ defmodule Pleroma.EctoType.Config.Atom do
end
def cast(key) when is_binary(key) do
- {:ok, Pleroma.ConfigDB.string_to_elixir_types(key)}
+ {:ok, Pleroma.Config.Converter.string_to_elixir_types!(key)}
end
def cast(_), do: :error
def load(key) do
- {:ok, Pleroma.ConfigDB.string_to_elixir_types(key)}
+ {:ok, Pleroma.Config.Converter.string_to_elixir_types!(key)}
end
def dump(key) when is_atom(key), do: {:ok, inspect(key)}
diff --git a/lib/pleroma/ecto_type/config/binary_value.ex b/lib/pleroma/ecto_type/config/binary_value.ex
index 908220a65..19c5cda83 100644
--- a/lib/pleroma/ecto_type/config/binary_value.ex
+++ b/lib/pleroma/ecto_type/config/binary_value.ex
@@ -15,6 +15,10 @@ defmodule Pleroma.EctoType.Config.BinaryValue do
end
end
+ def cast(value) when is_map(value) or is_list(value) do
+ {:ok, Pleroma.Config.Converter.to_elixir_types(value)}
+ end
+
def cast(value), do: {:ok, value}
def load(value) when is_binary(value) do
diff --git a/lib/pleroma/gopher/server.ex b/lib/pleroma/gopher/server.ex
index 1b85c49f5..2fa85ef66 100644
--- a/lib/pleroma/gopher/server.ex
+++ b/lib/pleroma/gopher/server.ex
@@ -12,13 +12,14 @@ defmodule Pleroma.Gopher.Server do
port = Keyword.get(config, :port, 1234)
if Keyword.get(config, :enabled, false) do
- GenServer.start_link(__MODULE__, [ip, port], [])
+ GenServer.start_link(__MODULE__, [ip, port])
else
Logger.info("Gopher server disabled")
:ignore
end
end
+ @impl true
def init([ip, port]) do
Logger.info("Starting gopher server on #{port}")
@@ -31,8 +32,14 @@ defmodule Pleroma.Gopher.Server do
[]
)
+ Process.flag(:trap_exit, true)
{:ok, %{ip: ip, port: port}}
end
+
+ @impl true
+ def terminate(_reason, _state) do
+ :ranch.stop_listener(:gopher)
+ end
end
defmodule Pleroma.Gopher.Server.ProtocolHandler do
diff --git a/lib/pleroma/gun/gun_supervisor.ex b/lib/pleroma/gun/gun_supervisor.ex
new file mode 100644
index 000000000..c72dfdd24
--- /dev/null
+++ b/lib/pleroma/gun/gun_supervisor.ex
@@ -0,0 +1,19 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Gun.GunSupervisor do
+ use Supervisor
+
+ def start_link(_) do
+ Supervisor.start_link(__MODULE__, :no_args)
+ end
+
+ def init(_) do
+ children =
+ Pleroma.Gun.ConnectionPool.children() ++
+ [{Task, &Pleroma.HTTP.AdapterHelper.Gun.limiter_setup/0}]
+
+ Supervisor.init(children, strategy: :one_for_one)
+ end
+end
diff --git a/lib/pleroma/http/hackney_supervisor.ex b/lib/pleroma/http/hackney_supervisor.ex
new file mode 100644
index 000000000..0e36f0273
--- /dev/null
+++ b/lib/pleroma/http/hackney_supervisor.ex
@@ -0,0 +1,30 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.HTTP.HackneySupervisor do
+ use Supervisor
+
+ def start_link(_) do
+ Supervisor.start_link(__MODULE__, :no_arg)
+ end
+
+ def init(_) do
+ pools = [:federation, :media]
+
+ pools =
+ if Pleroma.Config.get([Pleroma.Upload, :proxy_remote]) do
+ [:upload | pools]
+ else
+ pools
+ end
+
+ children =
+ for pool <- pools do
+ options = Pleroma.Config.get([:hackney_pools, pool])
+ :hackney_pool.child_spec(pool, options)
+ end
+
+ Supervisor.init(children, strategy: :one_for_one)
+ end
+end
diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
index 839ac1a8d..daa4c451c 100644
--- a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex
@@ -392,14 +392,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
def restart(conn, _params) do
with :ok <- configurable_from_database() do
- Restarter.Pleroma.restart(Config.get(:env), 50)
+ Task.start(Pleroma.Application.ConfigDependentDeps, :restart_dependencies, [])
json(conn, %{})
end
end
def need_reboot(conn, _params) do
- json(conn, %{need_reboot: Restarter.Pleroma.need_reboot?()})
+ json(conn, %{need_reboot: Pleroma.Application.ConfigDependentDeps.need_reboot?()})
end
defp configurable_from_database do
diff --git a/lib/pleroma/web/admin_api/controllers/config_controller.ex b/lib/pleroma/web/admin_api/controllers/config_controller.ex
index a718d7b8d..88ae9ed99 100644
--- a/lib/pleroma/web/admin_api/controllers/config_controller.ex
+++ b/lib/pleroma/web/admin_api/controllers/config_controller.ex
@@ -5,19 +5,24 @@
defmodule Pleroma.Web.AdminAPI.ConfigController do
use Pleroma.Web, :controller
+ import Pleroma.Web.ControllerHelper, only: [json_response: 3]
+
+ alias Pleroma.Application
alias Pleroma.Config
alias Pleroma.ConfigDB
alias Pleroma.Web.Plugs.OAuthScopesPlug
plug(Pleroma.Web.ApiSpec.CastAndValidate)
- plug(OAuthScopesPlug, %{scopes: ["admin:write"]} when action == :update)
+ plug(OAuthScopesPlug, %{scopes: ["admin:write"]} when action in [:update, :rollback])
plug(
OAuthScopesPlug,
%{scopes: ["admin:read"]}
- when action in [:show, :descriptions]
+ when action in [:show, :descriptions, :versions]
)
+ plug(:check_possibility_configuration_from_database when action != :descriptions)
+
action_fallback(Pleroma.Web.AdminAPI.FallbackController)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.ConfigOperation
@@ -29,100 +34,110 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
end
def show(conn, %{only_db: true}) do
- with :ok <- configurable_from_database() do
- configs = Pleroma.Repo.all(ConfigDB)
+ configs = ConfigDB.all_with_db()
- render(conn, "index.json", %{
- configs: configs,
- need_reboot: Restarter.Pleroma.need_reboot?()
- })
- end
+ render(conn, "index.json", %{
+ configs: configs,
+ need_reboot: Application.ConfigDependentDeps.need_reboot?()
+ })
end
def show(conn, _params) do
- with :ok <- configurable_from_database() do
- configs = ConfigDB.get_all_as_keyword()
-
- merged =
- Config.Holder.default_config()
- |> ConfigDB.merge(configs)
- |> Enum.map(fn {group, values} ->
- Enum.map(values, fn {key, value} ->
- db =
- if configs[group][key] do
- ConfigDB.get_db_keys(configs[group][key], key)
- end
+ defaults = Config.Holder.default_config()
+ changes = ConfigDB.all_with_db()
+
+ {changes_values_merged_with_defaults, remaining_defaults} =
+ ConfigDB.reduce_defaults_and_merge_with_changes(changes, defaults)
- db_value = configs[group][key]
+ changes_merged_with_defaults =
+ ConfigDB.from_keyword_to_structs(remaining_defaults, changes_values_merged_with_defaults)
- merged_value =
- if not is_nil(db_value) and Keyword.keyword?(db_value) and
- ConfigDB.sub_key_full_update?(group, key, Keyword.keys(db_value)) do
- ConfigDB.merge_group(group, key, value, db_value)
+ render(conn, "index.json", %{
+ configs: changes_merged_with_defaults,
+ need_reboot: Application.ConfigDependentDeps.need_reboot?()
+ })
+ end
+
+ def update(%{body_params: %{configs: configs}} = conn, _) do
+ result =
+ configs
+ |> Enum.filter(&whitelisted_config?/1)
+ |> Enum.map(&Config.Converter.to_elixir_types/1)
+ |> Config.Versioning.new_version()
+
+ case result do
+ {:ok, changes} ->
+ inserts_and_deletions =
+ Enum.reduce(changes, [], fn
+ {{operation, _, _}, %ConfigDB{} = change}, acc
+ when operation in [:insert_or_update, :delete_or_update] ->
+ if Ecto.get_meta(change, :state) == :deleted do
+ [change | acc]
else
- value
+ if change.group == :pleroma and
+ change.key in ConfigDB.pleroma_not_keyword_values() do
+ [%{change | db: [change.key]} | acc]
+ else
+ [%{change | db: Keyword.keys(change.value)} | acc]
+ end
end
- %ConfigDB{
- group: group,
- key: key,
- value: merged_value
- }
- |> Pleroma.Maps.put_if_present(:db, db)
+ _, acc ->
+ acc
end)
- end)
- |> List.flatten()
- render(conn, "index.json", %{
- configs: merged,
- need_reboot: Restarter.Pleroma.need_reboot?()
- })
+ Application.Environment.update(inserts_and_deletions, only_update: true)
+
+ render(conn, "index.json", %{
+ configs: Enum.reject(inserts_and_deletions, &(Ecto.get_meta(&1, :state) == :deleted)),
+ need_reboot: Application.ConfigDependentDeps.need_reboot?()
+ })
+
+ {:error, error} ->
+ {:error, "Updating config failed: #{inspect(error)}"}
+
+ {:error, _, {error, operation}, _} ->
+ {:error,
+ "Updating config failed: #{inspect(error)}, group: #{operation[:group]}, key: #{
+ operation[:key]
+ }, value: #{inspect(operation[:value])}"}
end
end
- def update(%{body_params: %{configs: configs}} = conn, _) do
- with :ok <- configurable_from_database() do
- results =
- configs
- |> Enum.filter(&whitelisted_config?/1)
- |> Enum.map(fn
- %{group: group, key: key, delete: true} = params ->
- ConfigDB.delete(%{group: group, key: key, subkeys: params[:subkeys]})
-
- %{group: group, key: key, value: value} ->
- ConfigDB.update_or_create(%{group: group, key: key, value: value})
- end)
- |> Enum.reject(fn {result, _} -> result == :error end)
-
- {deleted, updated} =
- results
- |> Enum.map(fn {:ok, %{key: key, value: value} = config} ->
- Map.put(config, :db, ConfigDB.get_db_keys(value, key))
- end)
- |> Enum.split_with(&(Ecto.get_meta(&1, :state) == :deleted))
-
- Config.TransferTask.load_and_update_env(deleted, false)
-
- if not Restarter.Pleroma.need_reboot?() do
- changed_reboot_settings? =
- (updated ++ deleted)
- |> Enum.any?(&Config.TransferTask.pleroma_need_restart?(&1.group, &1.key, &1.value))
-
- if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot()
- end
-
- render(conn, "index.json", %{
- configs: updated,
- need_reboot: Restarter.Pleroma.need_reboot?()
- })
+ def rollback(conn, %{id: id}) do
+ case Config.Versioning.rollback_by_id(id) do
+ {:ok, _} ->
+ json_response(conn, :no_content, "")
+
+ {:error, :not_found} ->
+ {:error, :not_found}
+
+ {:error, error} ->
+ {:error, "Rollback is not possible: #{inspect(error)}"}
+
+ {:error, _, {error, operation}, _} ->
+ {:error,
+ "Rollback is not possible, backup restore error: #{inspect(error)}, operation error: #{
+ inspect(operation)
+ }"}
end
end
- defp configurable_from_database do
+ def versions(conn, _) do
+ versions = Pleroma.Config.Version.all()
+
+ render(conn, "index.json", %{versions: versions})
+ end
+
+ defp check_possibility_configuration_from_database(conn, _) do
if Config.get(:configurable_from_database) do
- :ok
+ conn
else
- {:error, "You must enable configurable_from_database in your config file."}
+ Pleroma.Web.AdminAPI.FallbackController.call(
+ conn,
+ {:error, "You must enable configurable_from_database in your config file."}
+ )
+ |> halt()
end
end
diff --git a/lib/pleroma/web/admin_api/views/config_view.ex b/lib/pleroma/web/admin_api/views/config_view.ex
index d29b4963d..6115c3405 100644
--- a/lib/pleroma/web/admin_api/views/config_view.ex
+++ b/lib/pleroma/web/admin_api/views/config_view.ex
@@ -5,8 +5,6 @@
defmodule Pleroma.Web.AdminAPI.ConfigView do
use Pleroma.Web, :view
- alias Pleroma.ConfigDB
-
def render("index.json", %{configs: configs} = params) do
%{
configs: render_many(configs, __MODULE__, "show.json", as: :config),
@@ -14,17 +12,23 @@ defmodule Pleroma.Web.AdminAPI.ConfigView do
}
end
- def render("show.json", %{config: config}) do
- map = %{
- key: ConfigDB.to_json_types(config.key),
- group: ConfigDB.to_json_types(config.group),
- value: ConfigDB.to_json_types(config.value)
+ def render("index.json", %{versions: versions}) do
+ %{
+ versions: render_many(versions, __MODULE__, "show.json", as: :version)
}
+ end
+
+ def render("show.json", %{config: config}) do
+ config
+ |> Map.take([:group, :key, :value, :db])
+ |> Map.new(fn
+ {k, v} -> {k, Pleroma.Config.Converter.to_json_types(v)}
+ end)
+ end
- if config.db != [] do
- Map.put(map, :db, config.db)
- else
- map
- end
+ def render("show.json", %{version: version}) do
+ version
+ |> Map.take([:id, :current])
+ |> Map.put(:inserted_at, Pleroma.Web.CommonAPI.Utils.to_masto_date(version.inserted_at))
end
end
diff --git a/lib/pleroma/web/api_spec/operations/admin/config_operation.ex b/lib/pleroma/web/api_spec/operations/admin/config_operation.ex
index 30c3433b7..6d22191f1 100644
--- a/lib/pleroma/web/api_spec/operations/admin/config_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/admin/config_operation.ex
@@ -53,7 +53,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ConfigOperation do
type: :object,
properties: %{
group: %Schema{type: :string},
- key: %Schema{type: :string},
+ key: %Schema{type: :string, nullable: true},
value: any(),
delete: %Schema{type: :boolean},
subkeys: %Schema{type: :array, items: %Schema{type: :string}}
@@ -107,6 +107,56 @@ defmodule Pleroma.Web.ApiSpec.Admin.ConfigOperation do
}
end
+ def rollback_operation do
+ %Operation{
+ tags: ["Admin", "Config"],
+ summary: "Rollback config changes.",
+ operationId: "AdminAPI.ConfigController.rollback",
+ security: [%{"oAuth" => ["write"]}],
+ parameters: [
+ Operation.parameter(:id, :path, %Schema{type: :integer}, "Version id to rollback",
+ required: true
+ )
+ | admin_api_params()
+ ],
+ responses: %{
+ 204 => no_content_response(),
+ 400 => Operation.response("Bad Request", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ def versions_operation do
+ %Operation{
+ tags: ["Admin", "Config"],
+ summary: "Get list with config versions.",
+ operationId: "AdminAPI.ConfigController.versions",
+ security: [%{"oAuth" => ["read"]}],
+ parameters: admin_api_params(),
+ responses: %{
+ 200 =>
+ Operation.response("Config Version", "application/json", %Schema{
+ type: :object,
+ properties: %{
+ versions: %Schema{
+ type: :array,
+ items: %Schema{
+ type: :object,
+ properties: %{
+ id: %Schema{type: :integer},
+ current: %Schema{type: :boolean},
+ inserted_at: %Schema{type: :string, format: :"date-time"}
+ }
+ }
+ }
+ }
+ }),
+ 400 => Operation.response("Bad Request", "application/json", ApiError)
+ }
+ }
+ end
+
defp any do
%Schema{
oneOf: [
@@ -129,7 +179,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ConfigOperation do
type: :object,
properties: %{
group: %Schema{type: :string},
- key: %Schema{type: :string},
+ key: %Schema{type: :string, nullable: true},
value: any()
}
}
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index de0bd27d7..26c964795 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -232,6 +232,8 @@ defmodule Pleroma.Web.Router do
get("/config", ConfigController, :show)
post("/config", ConfigController, :update)
get("/config/descriptions", ConfigController, :descriptions)
+ get("/config/versions", ConfigController, :versions)
+ get("/config/versions/rollback/:id", ConfigController, :rollback)
get("/need_reboot", AdminAPIController, :need_reboot)
get("/restart", AdminAPIController, :restart)
diff --git a/mix.exs b/mix.exs
index ae74f50a3..dfb5a4227 100644
--- a/mix.exs
+++ b/mix.exs
@@ -77,9 +77,8 @@ defmodule Pleroma.Mixfile do
:logger,
:runtime_tools,
:comeonin,
- :quack,
- :fast_sanitize,
- :ssl
+ :ssl,
+ :fast_sanitize
],
included_applications: [:ex_syslogger]
]
@@ -192,7 +191,6 @@ defmodule Pleroma.Mixfile do
{:captcha,
git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git",
ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"},
- {:restarter, path: "./restarter"},
{:majic,
git: "https://git.pleroma.social/pleroma/elixir-libraries/majic.git",
ref: "289cda1b6d0d70ccb2ba508a2b0bd24638db2880"},
diff --git a/priv/repo/migrations/20200716195806_autolinker_to_linkify.exs b/priv/repo/migrations/20200716195806_autolinker_to_linkify.exs
index 570acba84..74448d2c7 100644
--- a/priv/repo/migrations/20200716195806_autolinker_to_linkify.exs
+++ b/priv/repo/migrations/20200716195806_autolinker_to_linkify.exs
@@ -15,7 +15,7 @@ defmodule Pleroma.Repo.Migrations.AutolinkerToLinkify do
defp move_config(%{} = old, %{} = new) do
{:ok, _} = ConfigDB.update_or_create(new)
- {:ok, _} = ConfigDB.delete(old)
+ {:ok, _} = ConfigDB.delete_or_update(old)
:ok
end
diff --git a/priv/repo/migrations/20200825061316_move_activity_expirations_to_oban.exs b/priv/repo/migrations/20200825061316_move_activity_expirations_to_oban.exs
index 096ab4ce5..739ef7220 100644
--- a/priv/repo/migrations/20200825061316_move_activity_expirations_to_oban.exs
+++ b/priv/repo/migrations/20200825061316_move_activity_expirations_to_oban.exs
@@ -4,7 +4,7 @@ defmodule Pleroma.Repo.Migrations.MoveActivityExpirationsToOban do
import Ecto.Query, only: [from: 2]
def change do
- Pleroma.Config.Oban.warn()
+ Pleroma.Config.DeprecationWarnings.check_oban_config()
Application.ensure_all_started(:oban)
diff --git a/priv/repo/migrations/20200907092050_move_tokens_expiration_into_oban.exs b/priv/repo/migrations/20200907092050_move_tokens_expiration_into_oban.exs
index 725c5ab0b..78d966766 100644
--- a/priv/repo/migrations/20200907092050_move_tokens_expiration_into_oban.exs
+++ b/priv/repo/migrations/20200907092050_move_tokens_expiration_into_oban.exs
@@ -4,7 +4,7 @@ defmodule Pleroma.Repo.Migrations.MoveTokensExpirationIntoOban do
import Ecto.Query, only: [from: 2]
def change do
- Pleroma.Config.Oban.warn()
+ Pleroma.Config.DeprecationWarnings.check_oban_config()
Application.ensure_all_started(:oban)
diff --git a/priv/repo/migrations/20201007131955_add_config_version.exs b/priv/repo/migrations/20201007131955_add_config_version.exs
new file mode 100644
index 000000000..1cbfcb398
--- /dev/null
+++ b/priv/repo/migrations/20201007131955_add_config_version.exs
@@ -0,0 +1,14 @@
+defmodule Pleroma.Repo.Migrations.AddConfigVersion do
+ use Ecto.Migration
+
+ def change do
+ create_if_not_exists table(:config_versions) do
+ add(:backup, :binary)
+ add(:current, :boolean)
+
+ timestamps()
+ end
+
+ create_if_not_exists(unique_index(:config_versions, [:current], where: "current = true"))
+ end
+end
diff --git a/priv/repo/migrations/20201007154420_change_key_in_config.exs b/priv/repo/migrations/20201007154420_change_key_in_config.exs
new file mode 100644
index 000000000..844595a13
--- /dev/null
+++ b/priv/repo/migrations/20201007154420_change_key_in_config.exs
@@ -0,0 +1,49 @@
+defmodule Pleroma.Repo.Migrations.ChangeKeyInConfig do
+ use Ecto.Migration
+
+ import Ecto.Query
+
+ alias Pleroma.Repo
+
+ def up do
+ alter table(:config) do
+ modify(:key, :string, null: true)
+ end
+
+ create_if_not_exists(unique_index(:config, [:group, "(key is null)"], where: "key IS NULL"))
+ end
+
+ def down do
+ query = from(c in "config", where: is_nil(c.key))
+
+ if Repo.aggregate(query, :count) == 0 do
+ revert()
+ else
+ configs = Repo.all(query)
+
+ new_configs =
+ Enum.reduce(configs, [], fn %{group: group, value: config}, group_acc ->
+ Enum.reduce(config, group_acc, fn {key, value}, acc ->
+ [%{group: group, key: key, value: value} | acc]
+ end)
+ end)
+
+ Enum.each(new_configs, fn config ->
+ {:ok, _} = Pleroma.ConfigDB.update_or_create(config)
+ end)
+
+ Enum.each(configs, &Repo.delete!(&1))
+
+ flush()
+ revert()
+ end
+ end
+
+ defp revert do
+ alter table(:config) do
+ modify(:key, :string, null: false)
+ end
+
+ drop_if_exists(unique_index(:config, [:group, "(key is null)"]))
+ end
+end
diff --git a/priv/repo/migrations/20201101162751_combine_settings_without_key.exs b/priv/repo/migrations/20201101162751_combine_settings_without_key.exs
new file mode 100644
index 000000000..42c704b1b
--- /dev/null
+++ b/priv/repo/migrations/20201101162751_combine_settings_without_key.exs
@@ -0,0 +1,29 @@
+defmodule Pleroma.Repo.Migrations.CombineSettingsWithoutKey do
+ use Ecto.Migration
+
+ import Ecto.Query, only: [from: 2]
+
+ alias Pleroma.ConfigDB
+ alias Pleroma.Repo
+
+ def change do
+ groups = ConfigDB.groups_without_keys()
+
+ configs =
+ from(c in ConfigDB, where: c.group in ^groups)
+ |> Repo.all()
+
+ new_configs =
+ configs
+ |> Enum.reduce([], fn %{group: group, key: key, value: value}, acc ->
+ Keyword.update(acc, group, [{key, value}], &Keyword.merge(&1, [{key, value}]))
+ end)
+ |> ConfigDB.from_keyword_to_maps()
+
+ Enum.each(new_configs, fn config ->
+ {:ok, _} = ConfigDB.update_or_create(config)
+ end)
+
+ Enum.each(configs, &Repo.delete!(&1))
+ end
+end
diff --git a/priv/repo/migrations/20201102064359_create_base_config_version.exs b/priv/repo/migrations/20201102064359_create_base_config_version.exs
new file mode 100644
index 000000000..2f1faf996
--- /dev/null
+++ b/priv/repo/migrations/20201102064359_create_base_config_version.exs
@@ -0,0 +1,15 @@
+defmodule Pleroma.Repo.Migrations.CreateBaseConfigVersion do
+ use Ecto.Migration
+
+ def change do
+ configs = Pleroma.ConfigDB.all_as_keyword()
+
+ unless configs == [] do
+ %Pleroma.Config.Version{
+ backup: configs,
+ current: true
+ }
+ |> Pleroma.Repo.insert!()
+ end
+ end
+end
diff --git a/restarter/lib/pleroma.ex b/restarter/lib/pleroma.ex
deleted file mode 100644
index 149a569ce..000000000
--- a/restarter/lib/pleroma.ex
+++ /dev/null
@@ -1,94 +0,0 @@
-defmodule Restarter.Pleroma do
- use GenServer
-
- require Logger
-
- @init_state %{need_reboot: false, rebooted: false, after_boot: false}
-
- def start_link(_) do
- GenServer.start_link(__MODULE__, [], name: __MODULE__)
- end
-
- def init(_), do: {:ok, @init_state}
-
- def rebooted? do
- GenServer.call(__MODULE__, :rebooted?)
- end
-
- def rebooted do
- GenServer.cast(__MODULE__, :rebooted)
- end
-
- def need_reboot? do
- GenServer.call(__MODULE__, :need_reboot?)
- end
-
- def need_reboot do
- GenServer.cast(__MODULE__, :need_reboot)
- end
-
- def refresh do
- GenServer.cast(__MODULE__, :refresh)
- end
-
- def restart(env, delay) do
- GenServer.cast(__MODULE__, {:restart, env, delay})
- end
-
- def restart_after_boot(env) do
- GenServer.cast(__MODULE__, {:after_boot, env})
- end
-
- def handle_call(:rebooted?, _from, state) do
- {:reply, state[:rebooted], state}
- end
-
- def handle_call(:need_reboot?, _from, state) do
- {:reply, state[:need_reboot], state}
- end
-
- def handle_cast(:rebooted, state) do
- {:noreply, Map.put(state, :rebooted, true)}
- end
-
- def handle_cast(:need_reboot, %{need_reboot: true} = state), do: {:noreply, state}
-
- def handle_cast(:need_reboot, state) do
- {:noreply, Map.put(state, :need_reboot, true)}
- end
-
- def handle_cast(:refresh, _state) do
- {:noreply, @init_state}
- end
-
- def handle_cast({:restart, :test, _}, state) do
- Logger.debug("pleroma manually restarted")
- {:noreply, Map.put(state, :need_reboot, false)}
- end
-
- def handle_cast({:restart, _, delay}, state) do
- Process.sleep(delay)
- do_restart(:pleroma)
- {:noreply, Map.put(state, :need_reboot, false)}
- end
-
- def handle_cast({:after_boot, _}, %{after_boot: true} = state), do: {:noreply, state}
-
- def handle_cast({:after_boot, :test}, state) do
- Logger.debug("pleroma restarted after boot")
- state = %{state | after_boot: true, rebooted: true}
- {:noreply, state}
- end
-
- def handle_cast({:after_boot, _}, state) do
- do_restart(:pleroma)
- state = %{state | after_boot: true, rebooted: true}
- {:noreply, state}
- end
-
- defp do_restart(app) do
- :ok = Application.ensure_started(app)
- :ok = Application.stop(app)
- :ok = Application.start(app)
- end
-end
diff --git a/restarter/lib/restarter.ex b/restarter/lib/restarter.ex
deleted file mode 100644
index eadd86f89..000000000
--- a/restarter/lib/restarter.ex
+++ /dev/null
@@ -1,8 +0,0 @@
-defmodule Restarter do
- use Application
-
- def start(_, _) do
- opts = [strategy: :one_for_one, name: Restarter.Supervisor]
- Supervisor.start_link([Restarter.Pleroma], opts)
- end
-end
diff --git a/restarter/mix.exs b/restarter/mix.exs
deleted file mode 100644
index b0908aece..000000000
--- a/restarter/mix.exs
+++ /dev/null
@@ -1,21 +0,0 @@
-defmodule Restarter.MixProject do
- use Mix.Project
-
- def project do
- [
- app: :restarter,
- version: "0.1.0",
- elixir: "~> 1.8",
- start_permanent: Mix.env() == :prod,
- deps: deps()
- ]
- end
-
- def application do
- [
- mod: {Restarter, []}
- ]
- end
-
- defp deps, do: []
-end
diff --git a/test/fixtures/config/temp.secret.exs b/test/fixtures/config/temp.secret.exs
index 4b3af39ec..077b99923 100644
--- a/test/fixtures/config/temp.secret.exs
+++ b/test/fixtures/config/temp.secret.exs
@@ -12,6 +12,28 @@ config :quack, level: :info
config :pleroma, Pleroma.Repo, pool: Ecto.Adapters.SQL.Sandbox
-config :postgrex, :json_library, Poison
+config :pleroma, Pleroma.Web.Endpoint, key: :val
+
+config :pleroma, env: :test
config :pleroma, :database, rum_enabled: true
+
+config :pleroma, configurable_from_database: false
+
+config :pleroma, ecto_repos: [Pleroma.Repo]
+
+config :pleroma, Pleroma.Gun, Pleroma.GunMock
+
+config :pleroma, Pleroma.ReverseProxy.Client, Pleroma.ReverseProxy.Client
+
+config :postgrex, :json_library, Poison
+
+config :tesla, adapter: Tesla.Mock
+
+config :tzdata, http_client: Pleroma.HTTP
+
+config :http_signatures, key: :val
+
+config :web_push_encryption, key: :val
+
+config :floki, key: :val
diff --git a/test/mix/tasks/pleroma/config_test.exs b/test/mix/tasks/pleroma/config_test.exs
index 21f8f2286..df6a39dd9 100644
--- a/test/mix/tasks/pleroma/config_test.exs
+++ b/test/mix/tasks/pleroma/config_test.exs
@@ -29,24 +29,56 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
|> Enum.sort()
end
- defp insert_config_record(group, key, value) do
+ defp insert_config_record(group \\ nil, key, value) do
insert(:config,
- group: group,
+ group: group || :pleroma,
key: key,
value: value
)
end
test "error if file with custom settings doesn't exist" do
- MixTask.migrate_to_db("config/non_existent_config_file.exs")
+ MixTask.run([
+ "migrate_to_db",
+ "--config",
+ "config/not_existance_config_file.exs"
+ ])
msg =
- "To migrate settings, you must define custom settings in config/non_existent_config_file.exs."
+ "ConfigDB not enabled. Please check the value of :configurable_from_database in your configuration."
- assert_receive {:mix_shell, :info, [^msg]}, 15
+ assert_receive {:mix_shell, :error, [^msg]}, 15
end
- describe "migrate_to_db/1" do
+ test "migrate_to_db error if configurable_from_database is not enabled" do
+ clear_config(:configurable_from_database, false)
+
+ MixTask.run([
+ "migrate_to_db",
+ "--config",
+ "test/fixtures/config/temp.secret.exs"
+ ])
+
+ assert_received {:mix_shell, :error, [message]}
+
+ assert message =~
+ "ConfigDB not enabled. Please check the value of :configurable_from_database in your configuration."
+ end
+
+ test "migrate_from_db error if configurable_from_database is not enabled" do
+ clear_config(:configurable_from_database, false)
+
+ MixTask.run([
+ "migrate_from_db"
+ ])
+
+ assert_received {:mix_shell, :error, [message]}
+
+ assert message =~
+ "ConfigDB not enabled. Please check the value of :configurable_from_database in your configuration."
+ end
+
+ describe "migrate_to_db task" do
setup do
clear_config(:configurable_from_database, true)
clear_config([:quack, :level])
@@ -57,7 +89,11 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
clear_config([:media_proxy, :whitelist], ["domain_without_scheme.com"])
assert config_records() == []
- MixTask.migrate_to_db("test/fixtures/config/temp.secret.exs")
+ MixTask.run([
+ "migrate_to_db",
+ "--config",
+ "test/fixtures/config/temp.secret.exs"
+ ])
assert_received {:mix_shell, :error, [message]}
@@ -68,25 +104,45 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
test "filtered settings are migrated to db" do
assert config_records() == []
- MixTask.migrate_to_db("test/fixtures/config/temp.secret.exs")
+ MixTask.run([
+ "migrate_to_db",
+ "--config",
+ "test/fixtures/config/temp.secret.exs"
+ ])
- config1 = ConfigDB.get_by_params(%{group: ":pleroma", key: ":first_setting"})
- config2 = ConfigDB.get_by_params(%{group: ":pleroma", key: ":second_setting"})
- config3 = ConfigDB.get_by_params(%{group: ":quack", key: ":level"})
- refute ConfigDB.get_by_params(%{group: ":pleroma", key: "Pleroma.Repo"})
- refute ConfigDB.get_by_params(%{group: ":postgrex", key: ":json_library"})
- refute ConfigDB.get_by_params(%{group: ":pleroma", key: ":database"})
+ config1 = ConfigDB.get_by_params(%{group: :pleroma, key: :first_setting})
+ config2 = ConfigDB.get_by_params(%{group: :pleroma, key: :second_setting})
+ config3 = ConfigDB.get_by_params(%{group: :quack})
+ refute ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.Repo})
+ refute ConfigDB.get_by_params(%{group: :postgrex, key: :json_library})
+ refute ConfigDB.get_by_params(%{group: :pleroma, key: :database})
assert config1.value == [key: "value", key2: [Repo]]
assert config2.value == [key: "value2", key2: ["Activity"]]
- assert config3.value == :info
+ assert config3.value == [level: :info]
+
+ assert Repo.aggregate(ConfigDB, :count) == 3
+
+ [version] = Repo.all(Pleroma.Config.Version)
+
+ assert version.backup == [
+ pleroma: [
+ second_setting: [key: "value2", key2: ["Activity"]],
+ first_setting: [key: "value", key2: [Pleroma.Repo]]
+ ],
+ quack: [level: :info]
+ ]
end
test "config table is truncated before migration" do
- insert_config_record(:pleroma, :first_setting, key: "value", key2: ["Activity"])
+ insert_config_record(:first_setting, key: "value", key2: ["Activity"])
assert length(config_records()) == 1
- MixTask.migrate_to_db("test/fixtures/config/temp.secret.exs")
+ MixTask.run([
+ "migrate_to_db",
+ "--config",
+ "test/fixtures/config/temp.secret.exs"
+ ])
config = ConfigDB.get_by_params(%{group: ":pleroma", key: ":first_setting"})
assert config.value == [key: "value", key2: [Repo]]
@@ -106,9 +162,9 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
end
test "settings are migrated to file and deleted from db", %{temp_file: temp_file} do
- insert_config_record(:pleroma, :setting_first, key: "value", key2: ["Activity"])
- insert_config_record(:pleroma, :setting_second, key: "value2", key2: [Repo])
- insert_config_record(:quack, :level, :info)
+ insert_config_record(:setting_first, key: "value", key2: ["Activity"])
+ insert_config_record(:setting_second, key: "value2", key2: [Repo])
+ insert_config_record(:quack, nil, level: :info)
MixTask.run(["migrate_from_db", "--env", "temp", "-d"])
@@ -117,7 +173,20 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
file = File.read!(temp_file)
assert file =~ "config :pleroma, :setting_first,"
assert file =~ "config :pleroma, :setting_second,"
- assert file =~ "config :quack, :level, :info"
+ assert file =~ "config :quack, level: :info"
+ end
+
+ test "migrate_from_db with config path in env", %{temp_file: temp_file} do
+ clear_config(:release, true)
+ clear_config(:config_path, "config/temp.exported_from_db.secret.exs")
+
+ insert_config_record(:setting_first, key: "value", key2: ["Activity"])
+
+ MixTask.run(["migrate_from_db", "--env", "temp", "-d"])
+
+ assert Repo.all(ConfigDB) == []
+ file = File.read!(temp_file)
+ assert file =~ "config :pleroma, :setting_first,"
end
test "load a settings with large values and pass to file", %{temp_file: temp_file} do
@@ -204,7 +273,7 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
setup do: clear_config(:configurable_from_database, true)
test "dumping a specific group" do
- insert_config_record(:pleroma, :instance, name: "Pleroma Test")
+ insert_config_record(:instance, name: "Pleroma Test")
insert_config_record(:web_push_encryption, :vapid_details,
subject: "mailto:administrator@example.com",
@@ -234,8 +303,8 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
end
test "dumping a specific key in a group" do
- insert_config_record(:pleroma, :instance, name: "Pleroma Test")
- insert_config_record(:pleroma, Pleroma.Captcha, enabled: false)
+ insert_config_record(:instance, name: "Pleroma Test")
+ insert_config_record(Pleroma.Captcha, enabled: false)
MixTask.run(["dump", "pleroma", "Pleroma.Captcha"])
@@ -247,8 +316,8 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
end
test "dumps all configuration successfully" do
- insert_config_record(:pleroma, :instance, name: "Pleroma Test")
- insert_config_record(:pleroma, Pleroma.Captcha, enabled: false)
+ insert_config_record(:instance, name: "Pleroma Test")
+ insert_config_record(Pleroma.Captcha, enabled: false)
MixTask.run(["dump"])
@@ -264,7 +333,7 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
test "refuses to dump" do
clear_config(:configurable_from_database, false)
- insert_config_record(:pleroma, :instance, name: "Pleroma Test")
+ insert_config_record(:instance, name: "Pleroma Test")
MixTask.run(["dump"])
@@ -279,8 +348,8 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
setup do: clear_config(:configurable_from_database, true)
setup do
- insert_config_record(:pleroma, :instance, name: "Pleroma Test")
- insert_config_record(:pleroma, Pleroma.Captcha, enabled: false)
+ insert_config_record(:instance, name: "Pleroma Test")
+ insert_config_record(Pleroma.Captcha, enabled: false)
insert_config_record(:pleroma2, :key2, z: 1)
assert length(config_records()) == 3
@@ -309,4 +378,55 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
assert config_records() == []
end
end
+
+ describe "rollback/1" do
+ setup do: clear_config(:configurable_from_database, true)
+
+ test "configuration from database is not configured" do
+ clear_config(:configurable_from_database, false)
+
+ MixTask.run(["rollback"])
+
+ assert_received {:mix_shell, :error, [message]}
+
+ assert message =~
+ "ConfigDB not enabled. Please check the value of :configurable_from_database in your configuration."
+ end
+
+ test "error" do
+ MixTask.run(["rollback"])
+
+ assert_receive {:mix_shell, :error,
+ [
+ "No version to rollback"
+ ]},
+ 15
+ end
+
+ test "success rollback" do
+ insert(:config_version,
+ backup: [pleroma: [instance: [name: "First name", email: "email@example.com"]]]
+ )
+
+ insert(:config_version, current: true)
+
+ MixTask.run(["rollback"])
+
+ assert_received {:mix_shell, :info, ["Success rollback"]}
+
+ [config] = Repo.all(ConfigDB)
+
+ assert config.value == [name: "First name", email: "email@example.com"]
+ end
+
+ test "rollback not possible error" do
+ insert(:config_version, current: true)
+
+ MixTask.run(["rollback", "-s", "2"])
+
+ assert_received {:mix_shell, :error, [message]}
+
+ assert message =~ "Rollback not possible. Incorrect steps value."
+ end
+ end
end
diff --git a/test/pleroma/application/config_dependent_deps_test.exs b/test/pleroma/application/config_dependent_deps_test.exs
new file mode 100644
index 000000000..620da16d9
--- /dev/null
+++ b/test/pleroma/application/config_dependent_deps_test.exs
@@ -0,0 +1,149 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Application.ConfigDependentDepsTest do
+ use ExUnit.Case
+
+ alias Pleroma.Application.ConfigDependentDeps
+
+ setup do
+ {:ok, _} =
+ DynamicSupervisor.start_link(
+ strategy: :one_for_one,
+ name: Pleroma.Application.DynamicSupervisorTest
+ )
+
+ {:ok, pid} =
+ Pleroma.Application.ConfigDependentDeps.start_link(
+ dynamic_supervisor: Pleroma.Application.DynamicSupervisorTest,
+ name: Pleroma.Application.ConfigDependentDepsTesting,
+ relations: [
+ {{:pleroma, :dummy_module1}, Pleroma.DummyModule1},
+ {{:pleroma, :dummy_module2}, Pleroma.DummyModule2},
+ {:dummy_group1, :dummy_group1},
+ {:ex_aws, :ex_aws},
+ {:not_started_app, :not_started_app}
+ ]
+ )
+
+ [pid: pid]
+ end
+
+ test "start_dependency/2", %{pid: pid} do
+ {:ok, pid} = ConfigDependentDeps.start_dependency(Pleroma.DummyModule1, pid)
+ assert Process.alive?(pid)
+ end
+
+ describe "need_reboot?/1" do
+ test "apps and paths", %{pid: pid} do
+ changes = [
+ %Pleroma.ConfigDB{group: :dummy_group1},
+ %Pleroma.ConfigDB{group: :pleroma, key: :dummy_module1}
+ ]
+
+ assert ConfigDependentDeps.save_config_paths_for_restart(changes, pid) == [
+ {:pleroma, :dummy_module1},
+ :dummy_group1
+ ]
+
+ assert ConfigDependentDeps.need_reboot?(pid)
+ end
+
+ test "app and path are not duplicated", %{pid: pid} do
+ changes = [
+ %Pleroma.ConfigDB{group: :dummy_group1},
+ %Pleroma.ConfigDB{group: :dummy_group1},
+ %Pleroma.ConfigDB{group: :pleroma, key: :dummy_module1},
+ %Pleroma.ConfigDB{group: :pleroma, key: :dummy_module1}
+ ]
+
+ assert ConfigDependentDeps.save_config_paths_for_restart(changes, pid) == [
+ {:pleroma, :dummy_module1},
+ :dummy_group1
+ ]
+
+ assert ConfigDependentDeps.need_reboot?(pid)
+ end
+ end
+
+ describe "restart_dependencies/1" do
+ test "started dependency", %{pid: pid} do
+ {:ok, dummy_pid} = ConfigDependentDeps.start_dependency(Pleroma.DummyModule1, pid)
+
+ changes = [
+ %Pleroma.ConfigDB{group: :ex_aws},
+ %Pleroma.ConfigDB{group: :pleroma, key: :dummy_module1}
+ ]
+
+ assert ConfigDependentDeps.save_config_paths_for_restart(changes, pid) == [
+ {:pleroma, :dummy_module1},
+ :ex_aws
+ ]
+
+ assert :ok == ConfigDependentDeps.restart_dependencies(pid)
+
+ restarted = Process.whereis(Pleroma.DummyModule1)
+
+ refute dummy_pid == restarted
+ end
+
+ test "not started process and app", %{pid: pid} do
+ changes = [
+ %Pleroma.ConfigDB{group: :pleroma, key: :dummy_module1},
+ %Pleroma.ConfigDB{group: :not_started_app}
+ ]
+
+ assert ConfigDependentDeps.save_config_paths_for_restart(changes, pid) == [
+ :not_started_app,
+ {:pleroma, :dummy_module1}
+ ]
+
+ assert :ok == ConfigDependentDeps.restart_dependencies(pid)
+
+ started = Process.whereis(Pleroma.DummyModule1)
+
+ assert Process.alive?(started)
+ end
+
+ test "ignored dependency", %{pid: pid} do
+ changes = [
+ %Pleroma.ConfigDB{group: :pleroma, key: :dummy_module2}
+ ]
+
+ assert ConfigDependentDeps.save_config_paths_for_restart(changes, pid) == [
+ {:pleroma, :dummy_module2}
+ ]
+
+ assert :ok == ConfigDependentDeps.restart_dependencies(pid)
+
+ refute Process.whereis(Pleroma.DummyModule2)
+ end
+ end
+
+ test "process goes down", %{pid: pid} do
+ {:ok, dummy_pid} = ConfigDependentDeps.start_dependency(Pleroma.DummyModule1, pid)
+
+ Process.exit(dummy_pid, :kill)
+
+ Process.sleep(10)
+ restarted = Process.whereis(Pleroma.DummyModule1)
+ refute restarted == dummy_pid
+ end
+end
+
+defmodule Pleroma.DummyModule1 do
+ use Agent
+
+ def start_link(_) do
+ Agent.start_link(fn -> nil end, name: __MODULE__)
+ end
+end
+
+defmodule Pleroma.DummyModule2 do
+ use Agent
+
+ def start_link(_) do
+ :ignore
+ end
+end
diff --git a/test/pleroma/application/environment_test.exs b/test/pleroma/application/environment_test.exs
new file mode 100644
index 000000000..2ad27ebd8
--- /dev/null
+++ b/test/pleroma/application/environment_test.exs
@@ -0,0 +1,248 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Application.EnvironmentTest do
+ use Pleroma.DataCase
+
+ import Pleroma.Factory
+
+ alias Pleroma.Application.Environment
+
+ setup do: clear_config(:configurable_from_database, true)
+
+ describe "load_from_db_and_update/0" do
+ test "transfer config values from db to env" do
+ refute Application.get_env(:pleroma, :test_key)
+ refute Application.get_env(:idna, :test_key)
+ refute Application.get_env(:quack, :test_key)
+ refute Application.get_env(:postgrex, :test_key)
+ initial = Application.get_env(:logger, :level)
+
+ insert(:config, key: :test_key, value: [live: 2, com: 3])
+ insert(:config, group: :idna, key: :test_key, value: [live: 15, com: 35])
+
+ insert(:config,
+ group: :quack,
+ key: nil,
+ value: [test_key: [key1: :test_value1, key2: :test_value2]]
+ )
+
+ insert(:config, group: :logger, key: nil, value: [level: :debug])
+
+ Environment.load_from_db_and_update()
+
+ assert Application.get_env(:pleroma, :test_key) == [live: 2, com: 3]
+ assert Application.get_env(:idna, :test_key) == [live: 15, com: 35]
+ assert Application.get_env(:quack, :test_key) == [key1: :test_value1, key2: :test_value2]
+ assert Application.get_env(:logger, :level) == :debug
+
+ on_exit(fn ->
+ Application.delete_env(:pleroma, :test_key)
+ Application.delete_env(:idna, :test_key)
+ Application.delete_env(:quack, :test_key)
+ Application.delete_env(:postgrex, :test_key)
+ Application.put_env(:logger, :level, initial)
+ end)
+ end
+
+ test "transfer config values for 1 group and some keys" do
+ quack_env = Application.get_all_env(:quack)
+
+ insert(:config, group: :quack, key: nil, value: [level: :info, meta: [:none]])
+
+ Environment.load_from_db_and_update()
+
+ assert Application.get_env(:quack, :level) == :info
+ assert Application.get_env(:quack, :meta) == [:none]
+ default = Pleroma.Config.Holder.default_config(:quack, :webhook_url)
+ assert Application.get_env(:quack, :webhook_url) == default
+
+ on_exit(fn ->
+ Application.put_all_env(quack: quack_env)
+ end)
+ end
+
+ test "transfer config values with full subkey update" do
+ clear_config(:emoji)
+ clear_config(:assets)
+
+ insert(:config, key: :emoji, value: [groups: [a: 1, b: 2]])
+ insert(:config, key: :assets, value: [mascots: [a: 1, b: 2]])
+
+ Environment.load_from_db_and_update()
+
+ emoji_env = Application.get_env(:pleroma, :emoji)
+ assert emoji_env[:groups] == [a: 1, b: 2]
+ assets_env = Application.get_env(:pleroma, :assets)
+ assert assets_env[:mascots] == [a: 1, b: 2]
+ end
+ end
+
+ describe "update/2 :ex_syslogger" do
+ setup do
+ initial = Application.get_env(:logger, :ex_syslogger)
+
+ config =
+ insert(:config,
+ group: :logger,
+ key: nil,
+ value: [
+ ex_syslogger: [
+ level: :warn,
+ ident: "pleroma",
+ format: "$metadata[$level] $message",
+ metadata: [:request_id, :key]
+ ]
+ ]
+ )
+
+ on_exit(fn -> Application.put_env(:logger, :ex_syslogger, initial) end)
+ [config: config, initial: initial]
+ end
+
+ test "changing", %{config: config} do
+ assert Environment.update([config]) == :ok
+
+ env = Application.get_env(:logger, :ex_syslogger)
+ assert env[:level] == :warn
+ assert env[:metadata] == [:request_id, :key]
+ end
+
+ test "deletion", %{config: config, initial: initial} do
+ assert Environment.update([config]) == :ok
+
+ {:ok, config} = Pleroma.ConfigDB.delete(config)
+ assert Environment.update([config]) == :ok
+
+ env = Application.get_env(:logger, :ex_syslogger)
+
+ assert env == initial
+ end
+ end
+
+ describe "update/2 :console" do
+ setup do
+ initial = Application.get_env(:logger, :console)
+
+ config =
+ insert(:config,
+ group: :logger,
+ key: nil,
+ value: [
+ console: [
+ level: :info,
+ format: "$time $metadata[$level]",
+ metadata: [:request_id, :key]
+ ]
+ ]
+ )
+
+ on_exit(fn -> Application.put_env(:logger, :console, initial) end)
+ [config: config, initial: initial]
+ end
+
+ test "change", %{config: config} do
+ assert Environment.update([config]) == :ok
+ env = Application.get_env(:logger, :console)
+ assert env[:level] == :info
+ assert env[:format] == "$time $metadata[$level]"
+ assert env[:metadata] == [:request_id, :key]
+ end
+
+ test "deletion", %{config: config, initial: initial} do
+ assert Environment.update([config]) == :ok
+ {:ok, config} = Pleroma.ConfigDB.delete(config)
+ assert Environment.update([config]) == :ok
+
+ env = Application.get_env(:logger, :console)
+ assert env == initial
+ end
+ end
+
+ describe "update/2 :backends" do
+ setup do
+ initial = Application.get_all_env(:logger)
+
+ config =
+ insert(:config, group: :logger, key: nil, value: [backends: [:console, :ex_syslogger]])
+
+ on_exit(fn -> Application.put_all_env(logger: initial) end)
+
+ [config: config, initial: initial]
+ end
+
+ test "change", %{config: config} do
+ assert Environment.update([config]) == :ok
+ env = Application.get_all_env(:logger)
+ assert env[:backends] == [:console, :ex_syslogger]
+ end
+
+ test "deletion", %{config: config, initial: initial} do
+ assert Environment.update([config]) == :ok
+ {:ok, config} = Pleroma.ConfigDB.delete(config)
+ assert Environment.update([config])
+
+ env = Application.get_all_env(:logger)
+ assert env == initial
+ end
+ end
+
+ describe "update/2 logger settings" do
+ setup do
+ initial = Application.get_all_env(:logger)
+
+ config =
+ insert(:config,
+ group: :logger,
+ key: nil,
+ value: [
+ console: [
+ level: :info,
+ format: "$time $metadata[$level]",
+ metadata: [:request_id, :key]
+ ],
+ ex_syslogger: [
+ level: :warn,
+ ident: "pleroma",
+ format: "$metadata[$level] $message",
+ metadata: [:request_id, :key]
+ ],
+ backends: [:console, :ex_syslogger]
+ ]
+ )
+
+ on_exit(fn -> Application.put_all_env(logger: initial) end)
+ [config: config]
+ end
+
+ test "change", %{config: config} do
+ assert Environment.update([config]) == :ok
+
+ env =
+ :logger
+ |> Application.get_all_env()
+ |> Keyword.take([:backends, :console, :ex_syslogger])
+
+ assert env[:console] == config.value[:console]
+ assert env[:ex_syslogger] == config.value[:ex_syslogger]
+ assert env[:backends] == config.value[:backends]
+ end
+ end
+
+ test "update/2 for change without key :cors_plug" do
+ config =
+ insert(:config,
+ group: :cors_plug,
+ key: nil,
+ value: [max_age: 300, methods: ["GET"]]
+ )
+
+ assert Environment.update([config]) == :ok
+
+ env = Application.get_all_env(:cors_plug)
+
+ assert env[:max_age] == 300
+ assert env[:methods] == ["GET"]
+ end
+end
diff --git a/test/pleroma/application_requirements_test.exs b/test/pleroma/application/requirements_test.exs
index 683ac8c96..1e73798ab 100644
--- a/test/pleroma/application_requirements_test.exs
+++ b/test/pleroma/application/requirements_test.exs
@@ -2,24 +2,24 @@
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.ApplicationRequirementsTest do
+defmodule Pleroma.Application.RequirementsTest do
use Pleroma.DataCase
import ExUnit.CaptureLog
import Mock
- alias Pleroma.ApplicationRequirements
- alias Pleroma.Repo
+ alias Pleroma.Application.Requirements
+ alias Pleroma.Emails.Mailer
describe "check_repo_pool_size!/1" do
test "raises if the pool size is unexpected" do
clear_config([Pleroma.Repo, :pool_size], 11)
clear_config([:dangerzone, :override_repo_pool_size], false)
- assert_raise Pleroma.ApplicationRequirements.VerifyError,
+ assert_raise Requirements.VerifyError,
"Repo.pool_size different than recommended value.",
fn ->
- capture_log(&Pleroma.ApplicationRequirements.verify!/0)
+ capture_log(&Requirements.verify!/0)
end
end
@@ -27,27 +27,27 @@ defmodule Pleroma.ApplicationRequirementsTest do
clear_config([Pleroma.Repo, :pool_size], 11)
clear_config([:dangerzone, :override_repo_pool_size], true)
- assert Pleroma.ApplicationRequirements.verify!() == :ok
+ assert Requirements.verify!() == :ok
end
end
describe "check_welcome_message_config!/1" do
setup do: clear_config([:welcome])
- setup do: clear_config([Pleroma.Emails.Mailer])
+ setup do: clear_config([Mailer])
test "raises if welcome email enabled but mail disabled" do
clear_config([:welcome, :email, :enabled], true)
- clear_config([Pleroma.Emails.Mailer, :enabled], false)
+ clear_config([Mailer, :enabled], false)
- assert_raise Pleroma.ApplicationRequirements.VerifyError, "The mail disabled.", fn ->
- capture_log(&Pleroma.ApplicationRequirements.verify!/0)
+ assert_raise Requirements.VerifyError, "The mail disabled.", fn ->
+ capture_log(&Requirements.verify!/0)
end
end
end
describe "check_confirmation_accounts!" do
setup_with_mocks([
- {Pleroma.ApplicationRequirements, [:passthrough],
+ {Requirements, [:passthrough],
[
check_migrations_applied!: fn _ -> :ok end
]}
@@ -59,32 +59,31 @@ defmodule Pleroma.ApplicationRequirementsTest do
test "raises if account confirmation is required but mailer isn't enable" do
clear_config([:instance, :account_activation_required], true)
- clear_config([Pleroma.Emails.Mailer, :enabled], false)
+ clear_config([Mailer, :enabled], false)
- assert_raise Pleroma.ApplicationRequirements.VerifyError,
+ assert_raise Requirements.VerifyError,
"Account activation enabled, but Mailer is disabled. Cannot send confirmation emails.",
fn ->
- capture_log(&Pleroma.ApplicationRequirements.verify!/0)
+ capture_log(&Requirements.verify!/0)
end
end
test "doesn't do anything if account confirmation is disabled" do
clear_config([:instance, :account_activation_required], false)
- clear_config([Pleroma.Emails.Mailer, :enabled], false)
- assert Pleroma.ApplicationRequirements.verify!() == :ok
+ clear_config([Mailer, :enabled], false)
+ assert Requirements.verify!() == :ok
end
test "doesn't do anything if account confirmation is required and mailer is enabled" do
clear_config([:instance, :account_activation_required], true)
- clear_config([Pleroma.Emails.Mailer, :enabled], true)
- assert Pleroma.ApplicationRequirements.verify!() == :ok
+ clear_config([Mailer, :enabled], true)
+ assert Requirements.verify!() == :ok
end
end
describe "check_rum!" do
setup_with_mocks([
- {Pleroma.ApplicationRequirements, [:passthrough],
- [check_migrations_applied!: fn _ -> :ok end]}
+ {Requirements, [:passthrough], [check_migrations_applied!: fn _ -> :ok end]}
]) do
:ok
end
@@ -95,10 +94,10 @@ defmodule Pleroma.ApplicationRequirementsTest do
clear_config([:database, :rum_enabled], true)
with_mocks([{Repo, [:passthrough], [exists?: fn _, _ -> false end]}]) do
- assert_raise ApplicationRequirements.VerifyError,
+ assert_raise Requirements.VerifyError,
"Unapplied RUM Migrations detected",
fn ->
- capture_log(&ApplicationRequirements.verify!/0)
+ capture_log(&Requirements.verify!/0)
end
end
end
@@ -107,10 +106,10 @@ defmodule Pleroma.ApplicationRequirementsTest do
clear_config([:database, :rum_enabled], false)
with_mocks([{Repo, [:passthrough], [exists?: fn _, _ -> true end]}]) do
- assert_raise ApplicationRequirements.VerifyError,
+ assert_raise Requirements.VerifyError,
"RUM Migrations detected",
fn ->
- capture_log(&ApplicationRequirements.verify!/0)
+ capture_log(&Requirements.verify!/0)
end
end
end
@@ -119,7 +118,7 @@ defmodule Pleroma.ApplicationRequirementsTest do
clear_config([:database, :rum_enabled], true)
with_mocks([{Repo, [:passthrough], [exists?: fn _, _ -> true end]}]) do
- assert ApplicationRequirements.verify!() == :ok
+ assert Requirements.verify!() == :ok
end
end
@@ -127,12 +126,12 @@ defmodule Pleroma.ApplicationRequirementsTest do
clear_config([:database, :rum_enabled], false)
with_mocks([{Repo, [:passthrough], [exists?: fn _, _ -> false end]}]) do
- assert ApplicationRequirements.verify!() == :ok
+ assert Requirements.verify!() == :ok
end
end
end
- describe "check_migrations_applied!" do
+ describe "check_migrations_applied" do
setup_with_mocks([
{Ecto.Migrator, [],
[
@@ -152,17 +151,17 @@ defmodule Pleroma.ApplicationRequirementsTest do
setup do: clear_config([:i_am_aware_this_may_cause_data_loss, :disable_migration_check])
test "raises if it detects unapplied migrations" do
- assert_raise ApplicationRequirements.VerifyError,
+ assert_raise Requirements.VerifyError,
"Unapplied Migrations detected",
fn ->
- capture_log(&ApplicationRequirements.verify!/0)
+ capture_log(&Requirements.verify!/0)
end
end
test "doesn't do anything if disabled" do
clear_config([:i_am_aware_this_may_cause_data_loss, :disable_migration_check], true)
- assert :ok == ApplicationRequirements.verify!()
+ assert :ok == Requirements.verify!()
end
end
end
diff --git a/test/pleroma/config/converter_test.exs b/test/pleroma/config/converter_test.exs
new file mode 100644
index 000000000..a1ffab5be
--- /dev/null
+++ b/test/pleroma/config/converter_test.exs
@@ -0,0 +1,432 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Config.ConverterTest do
+ use ExUnit.Case, async: true
+
+ alias Pleroma.Config.Converter
+
+ describe "to_elixir_types/1" do
+ test "string" do
+ assert Converter.to_elixir_types("value as string") == "value as string"
+ end
+
+ test "boolean" do
+ assert Converter.to_elixir_types(false) == false
+ end
+
+ test "nil" do
+ assert Converter.to_elixir_types(nil) == nil
+ end
+
+ test "integer" do
+ assert Converter.to_elixir_types(150) == 150
+ end
+
+ test "atom" do
+ assert Converter.to_elixir_types(":atom") == :atom
+ end
+
+ test "ssl options" do
+ assert Converter.to_elixir_types([":tlsv1", ":tlsv1.1", ":tlsv1.2"]) == [
+ :tlsv1,
+ :"tlsv1.1",
+ :"tlsv1.2"
+ ]
+ end
+
+ test "pleroma module" do
+ assert Converter.to_elixir_types("Pleroma.Bookmark") == Pleroma.Bookmark
+ end
+
+ test "pleroma string" do
+ assert Converter.to_elixir_types("Pleroma") == "Pleroma"
+ end
+
+ test "phoenix module" do
+ assert Converter.to_elixir_types("Phoenix.Socket.V1.JSONSerializer") ==
+ Phoenix.Socket.V1.JSONSerializer
+ end
+
+ test "tesla module" do
+ assert Converter.to_elixir_types("Tesla.Adapter.Hackney") == Tesla.Adapter.Hackney
+ end
+
+ test "ExSyslogger module" do
+ assert Converter.to_elixir_types("ExSyslogger") == ExSyslogger
+ end
+
+ test "Quack.Logger module" do
+ assert Converter.to_elixir_types("Quack.Logger") == Quack.Logger
+ end
+
+ test "Swoosh.Adapters modules" do
+ assert Converter.to_elixir_types("Swoosh.Adapters.SMTP") == Swoosh.Adapters.SMTP
+ assert Converter.to_elixir_types("Swoosh.Adapters.AmazonSES") == Swoosh.Adapters.AmazonSES
+ end
+
+ test "sigil" do
+ assert Converter.to_elixir_types("~r[comp[lL][aA][iI][nN]er]") == ~r/comp[lL][aA][iI][nN]er/
+ end
+
+ test "link sigil" do
+ assert Converter.to_elixir_types("~r/https:\/\/example.com/") == ~r/https:\/\/example.com/
+ end
+
+ test "link sigil with um modifiers" do
+ assert Converter.to_elixir_types("~r/https:\/\/example.com/um") ==
+ ~r/https:\/\/example.com/um
+ end
+
+ test "link sigil with i modifier" do
+ assert Converter.to_elixir_types("~r/https:\/\/example.com/i") == ~r/https:\/\/example.com/i
+ end
+
+ test "link sigil with s modifier" do
+ assert Converter.to_elixir_types("~r/https:\/\/example.com/s") == ~r/https:\/\/example.com/s
+ end
+
+ test "raise if valid delimiter not found" do
+ assert_raise ArgumentError, "valid delimiter for Regex expression not found", fn ->
+ Converter.to_elixir_types("~r/https://[]{}<>\"'()|example.com/s")
+ end
+ end
+
+ test "2 child tuple" do
+ assert Converter.to_elixir_types(%{"tuple" => ["v1", ":v2"]}) == {"v1", :v2}
+ end
+
+ test "proxy tuple with localhost" do
+ assert Converter.to_elixir_types(%{
+ "tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]
+ }) == {:proxy_url, {:socks5, :localhost, 1234}}
+ end
+
+ test "proxy tuple with domain" do
+ assert Converter.to_elixir_types(%{
+ "tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]
+ }) == {:proxy_url, {:socks5, 'domain.com', 1234}}
+ end
+
+ test "proxy tuple with ip" do
+ assert Converter.to_elixir_types(%{
+ "tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]
+ }) == {:proxy_url, {:socks5, {127, 0, 0, 1}, 1234}}
+ end
+
+ test "tuple with n childs" do
+ assert Converter.to_elixir_types(%{
+ "tuple" => [
+ "v1",
+ ":v2",
+ "Pleroma.Bookmark",
+ 150,
+ false,
+ "Phoenix.Socket.V1.JSONSerializer"
+ ]
+ }) == {"v1", :v2, Pleroma.Bookmark, 150, false, Phoenix.Socket.V1.JSONSerializer}
+ end
+
+ test "map with string key" do
+ assert Converter.to_elixir_types(%{"key" => "value"}) == %{"key" => "value"}
+ end
+
+ test "map with atom key" do
+ assert Converter.to_elixir_types(%{":key" => "value"}) == %{key: "value"}
+ end
+
+ test "list of strings" do
+ assert Converter.to_elixir_types(["v1", "v2", "v3"]) == ["v1", "v2", "v3"]
+ end
+
+ test "list of modules" do
+ assert Converter.to_elixir_types(["Pleroma.Repo", "Pleroma.Activity"]) == [
+ Pleroma.Repo,
+ Pleroma.Activity
+ ]
+ end
+
+ test "list of atoms" do
+ assert Converter.to_elixir_types([":v1", ":v2", ":v3"]) == [:v1, :v2, :v3]
+ end
+
+ test "list of mixed values" do
+ assert Converter.to_elixir_types([
+ "v1",
+ ":v2",
+ "Pleroma.Repo",
+ "Phoenix.Socket.V1.JSONSerializer",
+ 15,
+ false
+ ]) == [
+ "v1",
+ :v2,
+ Pleroma.Repo,
+ Phoenix.Socket.V1.JSONSerializer,
+ 15,
+ false
+ ]
+ end
+
+ test "simple keyword" do
+ assert Converter.to_elixir_types([%{"tuple" => [":key", "value"]}]) == [key: "value"]
+ end
+
+ test "keyword" do
+ assert Converter.to_elixir_types([
+ %{"tuple" => [":types", "Pleroma.PostgresTypes"]},
+ %{"tuple" => [":telemetry_event", ["Pleroma.Repo.Instrumenter"]]},
+ %{"tuple" => [":migration_lock", nil]},
+ %{"tuple" => [":key1", 150]},
+ %{"tuple" => [":key2", "string"]}
+ ]) == [
+ types: Pleroma.PostgresTypes,
+ telemetry_event: [Pleroma.Repo.Instrumenter],
+ migration_lock: nil,
+ key1: 150,
+ key2: "string"
+ ]
+ end
+
+ test "trandformed keyword" do
+ assert Converter.to_elixir_types(a: 1, b: 2, c: "string") == [a: 1, b: 2, c: "string"]
+ end
+
+ test "complex keyword with nested mixed childs" do
+ assert Converter.to_elixir_types([
+ %{"tuple" => [":uploader", "Pleroma.Uploaders.Local"]},
+ %{"tuple" => [":filters", ["Pleroma.Upload.Filter.Dedupe"]]},
+ %{"tuple" => [":link_name", true]},
+ %{"tuple" => [":proxy_remote", false]},
+ %{"tuple" => [":common_map", %{":key" => "value"}]},
+ %{
+ "tuple" => [
+ ":proxy_opts",
+ [
+ %{"tuple" => [":redirect_on_failure", false]},
+ %{"tuple" => [":max_body_length", 1_048_576]},
+ %{
+ "tuple" => [
+ ":http",
+ [
+ %{"tuple" => [":follow_redirect", true]},
+ %{"tuple" => [":pool", ":upload"]}
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]) == [
+ uploader: Pleroma.Uploaders.Local,
+ filters: [Pleroma.Upload.Filter.Dedupe],
+ link_name: true,
+ proxy_remote: false,
+ common_map: %{key: "value"},
+ proxy_opts: [
+ redirect_on_failure: false,
+ max_body_length: 1_048_576,
+ http: [
+ follow_redirect: true,
+ pool: :upload
+ ]
+ ]
+ ]
+ end
+
+ test "common keyword" do
+ assert Converter.to_elixir_types([
+ %{"tuple" => [":level", ":warn"]},
+ %{"tuple" => [":meta", [":all"]]},
+ %{"tuple" => [":path", ""]},
+ %{"tuple" => [":val", nil]},
+ %{"tuple" => [":webhook_url", "https://hooks.slack.com/services/YOUR-KEY-HERE"]}
+ ]) == [
+ level: :warn,
+ meta: [:all],
+ path: "",
+ val: nil,
+ webhook_url: "https://hooks.slack.com/services/YOUR-KEY-HERE"
+ ]
+ end
+
+ test "complex keyword with sigil" do
+ assert Converter.to_elixir_types([
+ %{"tuple" => [":federated_timeline_removal", []]},
+ %{"tuple" => [":reject", ["~r/comp[lL][aA][iI][nN]er/"]]},
+ %{"tuple" => [":replace", []]}
+ ]) == [
+ federated_timeline_removal: [],
+ reject: [~r/comp[lL][aA][iI][nN]er/],
+ replace: []
+ ]
+ end
+
+ test "complex keyword with tuples with more than 2 values" do
+ assert Converter.to_elixir_types([
+ %{
+ "tuple" => [
+ ":http",
+ [
+ %{
+ "tuple" => [
+ ":key1",
+ [
+ %{
+ "tuple" => [
+ ":_",
+ [
+ %{
+ "tuple" => [
+ "/api/v1/streaming",
+ "Pleroma.Web.MastodonAPI.WebsocketHandler",
+ []
+ ]
+ },
+ %{
+ "tuple" => [
+ "/websocket",
+ "Phoenix.Endpoint.CowboyWebSocket",
+ %{
+ "tuple" => [
+ "Phoenix.Transports.WebSocket",
+ %{
+ "tuple" => [
+ "Pleroma.Web.Endpoint",
+ "Pleroma.Web.UserSocket",
+ []
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ %{
+ "tuple" => [
+ ":_",
+ "Phoenix.Endpoint.Cowboy2Handler",
+ %{"tuple" => ["Pleroma.Web.Endpoint", []]}
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ ]) == [
+ http: [
+ key1: [
+ {:_,
+ [
+ {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
+ {"/websocket", Phoenix.Endpoint.CowboyWebSocket,
+ {Phoenix.Transports.WebSocket,
+ {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, []}}},
+ {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
+ ]}
+ ]
+ ]
+ ]
+ end
+ end
+
+ describe "to_json_types" do
+ test "list" do
+ assert Converter.to_json_types(["0", 1, true, :atom, Pleroma.Upload]) == [
+ "0",
+ 1,
+ true,
+ ":atom",
+ "Pleroma.Upload"
+ ]
+ end
+
+ test "regex" do
+ assert Converter.to_json_types(~r/regex/i) == "~r/regex/i"
+ end
+
+ test "map" do
+ assert Converter.to_json_types(%{"a" => "b", "c" => 1, "d" => true, "e" => :atom}) == %{
+ "a" => "b",
+ "c" => 1,
+ "d" => true,
+ "e" => ":atom"
+ }
+ end
+
+ test ":args list" do
+ assert Converter.to_json_types({:args, [{1, "a"}, "string"]}) == %{
+ "tuple" => [":args", ["{1, \"a\"}", "string"]]
+ }
+ end
+
+ test ":proxy_url tuple with localhost" do
+ assert Converter.to_json_types({:proxy_url, {:socks, :localhost, 1234}}) == %{
+ "tuple" => [":proxy_url", %{"tuple" => [":socks", "localhost", 1234]}]
+ }
+ end
+
+ test ":proxy_url tuple" do
+ assert Converter.to_json_types({:proxy_url, {:socks, {127, 0, 0, 1}, 1234}}) == %{
+ "tuple" => [":proxy_url", %{"tuple" => [":socks", "127.0.0.1", 1234]}]
+ }
+ end
+
+ test ":proxy_url tuple domain" do
+ assert Converter.to_json_types({:proxy_url, {:socks5, "domain.com", 1234}}) == %{
+ "tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]
+ }
+ end
+
+ test "tuple" do
+ assert Converter.to_json_types({1, "a"}) == %{"tuple" => [1, "a"]}
+ end
+
+ test "string" do
+ assert Converter.to_json_types("string") == "string"
+ end
+
+ test "boolean" do
+ assert Converter.to_json_types(true) == true
+ end
+
+ test "integer" do
+ assert Converter.to_json_types(123) == 123
+ end
+
+ test "nil" do
+ assert Converter.to_json_types(nil) == nil
+ end
+
+ test "ssl type" do
+ assert Converter.to_json_types(:"tlsv1.1") == ":tlsv1.1"
+ end
+
+ test "atom" do
+ assert Converter.to_json_types(:atom) == ":atom"
+ end
+ end
+
+ describe "string_to_elixir_types!/1" do
+ test "atom" do
+ assert Converter.string_to_elixir_types!(":localhost") == :localhost
+ end
+
+ test "module" do
+ assert Converter.string_to_elixir_types!("Pleroma.Upload") == Pleroma.Upload
+ end
+
+ test "regex" do
+ assert Converter.string_to_elixir_types!("~r/regex/i") == ~r/regex/i
+ end
+
+ test "string" do
+ assert Converter.string_to_elixir_types!("string") == "string"
+ end
+ end
+end
diff --git a/test/pleroma/config/loader_test.exs b/test/pleroma/config/loader_test.exs
index b34fd70da..5eb29946d 100644
--- a/test/pleroma/config/loader_test.exs
+++ b/test/pleroma/config/loader_test.exs
@@ -8,22 +8,38 @@ defmodule Pleroma.Config.LoaderTest do
alias Pleroma.Config.Loader
test "read/1" do
- config = Loader.read("test/fixtures/config/temp.secret.exs")
+ config = Loader.read!("test/fixtures/config/temp.secret.exs")
assert config[:pleroma][:first_setting][:key] == "value"
assert config[:pleroma][:first_setting][:key2] == [Pleroma.Repo]
assert config[:quack][:level] == :info
end
- test "filter_group/2" do
- assert Loader.filter_group(:pleroma,
- pleroma: [
- {Pleroma.Repo, [a: 1, b: 2]},
- {Pleroma.Upload, [a: 1, b: 2]},
- {Pleroma.Web.Endpoint, []},
- env: :test,
- configurable_from_database: true,
- database: []
- ]
- ) == [{Pleroma.Upload, [a: 1, b: 2]}]
+ test "filter/1" do
+ config = Loader.read!("test/fixtures/config/temp.secret.exs")
+
+ filtered_config = Loader.filter(config)
+
+ refute filtered_config[:postgrex]
+ refute filtered_config[:tesla]
+ refute filtered_config[:phoenix]
+ refute filtered_config[:tz_data]
+ refute filtered_config[:http_signatures]
+ refute filtered_config[:web_push_encryption]
+ refute filtered_config[:floki]
+
+ refute filtered_config[:pleroma][Pleroma.Repo]
+ refute filtered_config[:pleroma][Pleroma.Web.Endpoint]
+ refute filtered_config[:pleroma][:env]
+ refute filtered_config[:pleroma][:configurable_from_database]
+ refute filtered_config[:pleroma][:database]
+ refute filtered_config[:pleroma][:ecto_repos]
+ refute filtered_config[:pleroma][Pleroma.Gun]
+ refute filtered_config[:pleroma][Pleroma.ReverseProxy.Client]
+
+ assert config[:pleroma][:first_setting][:key] == "value"
+ assert config[:pleroma][:first_setting][:key2] == [Pleroma.Repo]
+ assert config[:quack][:level] == :info
+ assert config[:pleroma][:second_setting][:key] == "value2"
+ assert config[:pleroma][:second_setting][:key2] == ["Activity"]
end
end
diff --git a/test/pleroma/config/transfer_task_test.exs b/test/pleroma/config/transfer_task_test.exs
deleted file mode 100644
index 8ae5d3b81..000000000
--- a/test/pleroma/config/transfer_task_test.exs
+++ /dev/null
@@ -1,120 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Config.TransferTaskTest do
- use Pleroma.DataCase
-
- import ExUnit.CaptureLog
- import Pleroma.Factory
-
- alias Pleroma.Config.TransferTask
-
- setup do: clear_config(:configurable_from_database, true)
-
- test "transfer config values from db to env" do
- refute Application.get_env(:pleroma, :test_key)
- refute Application.get_env(:idna, :test_key)
- refute Application.get_env(:quack, :test_key)
- refute Application.get_env(:postgrex, :test_key)
- initial = Application.get_env(:logger, :level)
-
- insert(:config, key: :test_key, value: [live: 2, com: 3])
- insert(:config, group: :idna, key: :test_key, value: [live: 15, com: 35])
- insert(:config, group: :quack, key: :test_key, value: [:test_value1, :test_value2])
- insert(:config, group: :postgrex, key: :test_key, value: :value)
- insert(:config, group: :logger, key: :level, value: :debug)
-
- TransferTask.start_link([])
-
- assert Application.get_env(:pleroma, :test_key) == [live: 2, com: 3]
- assert Application.get_env(:idna, :test_key) == [live: 15, com: 35]
- assert Application.get_env(:quack, :test_key) == [:test_value1, :test_value2]
- assert Application.get_env(:logger, :level) == :debug
- assert Application.get_env(:postgrex, :test_key) == :value
-
- on_exit(fn ->
- Application.delete_env(:pleroma, :test_key)
- Application.delete_env(:idna, :test_key)
- Application.delete_env(:quack, :test_key)
- Application.delete_env(:postgrex, :test_key)
- Application.put_env(:logger, :level, initial)
- end)
- end
-
- test "transfer config values for 1 group and some keys" do
- level = Application.get_env(:quack, :level)
- meta = Application.get_env(:quack, :meta)
-
- insert(:config, group: :quack, key: :level, value: :info)
- insert(:config, group: :quack, key: :meta, value: [:none])
-
- TransferTask.start_link([])
-
- assert Application.get_env(:quack, :level) == :info
- assert Application.get_env(:quack, :meta) == [:none]
- default = Pleroma.Config.Holder.default_config(:quack, :webhook_url)
- assert Application.get_env(:quack, :webhook_url) == default
-
- on_exit(fn ->
- Application.put_env(:quack, :level, level)
- Application.put_env(:quack, :meta, meta)
- end)
- end
-
- test "transfer config values with full subkey update" do
- clear_config(:emoji)
- clear_config(:assets)
-
- insert(:config, key: :emoji, value: [groups: [a: 1, b: 2]])
- insert(:config, key: :assets, value: [mascots: [a: 1, b: 2]])
-
- TransferTask.start_link([])
-
- emoji_env = Application.get_env(:pleroma, :emoji)
- assert emoji_env[:groups] == [a: 1, b: 2]
- assets_env = Application.get_env(:pleroma, :assets)
- assert assets_env[:mascots] == [a: 1, b: 2]
- end
-
- describe "pleroma restart" do
- setup do
- on_exit(fn -> Restarter.Pleroma.refresh() end)
- end
-
- test "don't restart if no reboot time settings were changed" do
- clear_config(:emoji)
- insert(:config, key: :emoji, value: [groups: [a: 1, b: 2]])
-
- refute String.contains?(
- capture_log(fn -> TransferTask.start_link([]) end),
- "pleroma restarted"
- )
- end
-
- test "on reboot time key" do
- clear_config(:chat)
- insert(:config, key: :chat, value: [enabled: false])
- assert capture_log(fn -> TransferTask.start_link([]) end) =~ "pleroma restarted"
- end
-
- test "on reboot time subkey" do
- clear_config(Pleroma.Captcha)
- insert(:config, key: Pleroma.Captcha, value: [seconds_valid: 60])
- assert capture_log(fn -> TransferTask.start_link([]) end) =~ "pleroma restarted"
- end
-
- test "don't restart pleroma on reboot time key and subkey if there is false flag" do
- clear_config(:chat)
- clear_config(Pleroma.Captcha)
-
- insert(:config, key: :chat, value: [enabled: false])
- insert(:config, key: Pleroma.Captcha, value: [seconds_valid: 60])
-
- refute String.contains?(
- capture_log(fn -> TransferTask.load_and_update_env([], false) end),
- "pleroma restarted"
- )
- end
- end
-end
diff --git a/test/pleroma/config/versioning_test.exs b/test/pleroma/config/versioning_test.exs
new file mode 100644
index 000000000..783952e98
--- /dev/null
+++ b/test/pleroma/config/versioning_test.exs
@@ -0,0 +1,414 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Config.VersioningTest do
+ use Pleroma.DataCase, async: true
+
+ import Pleroma.Factory
+
+ alias Pleroma.Config.Version
+ alias Pleroma.Config.Versioning
+ alias Pleroma.ConfigDB
+ alias Pleroma.Repo
+
+ @with_key %{
+ group: :pleroma,
+ key: :instance,
+ value: [name: "Instance name"]
+ }
+
+ @without_key %{
+ group: :quack,
+ key: nil,
+ value: [
+ level: :warn,
+ meta: [:all],
+ webhook_url: "https://hooks.slack.com/services/YOUR-KEY-HERE"
+ ]
+ }
+
+ @value_not_keyword %{
+ group: :pleroma,
+ key: Pleroma.Web.Auth.Authenticator,
+ value: Pleroma.Web.Auth.PleromaAuthenticator
+ }
+
+ describe "new_version/1" do
+ test "creates version" do
+ changes = [@with_key, @without_key, @value_not_keyword]
+
+ {:ok,
+ %{
+ :insert_version => version,
+ :update_all_versions => {0, nil},
+ {:insert_or_update, :pleroma, :instance} => _,
+ {:insert_or_update, :quack, nil} => _
+ }} = Versioning.new_version(changes)
+
+ assert version.current
+ assert backup_length(version) == 2
+
+ assert version.backup[:quack] == @without_key[:value]
+
+ assert version.backup[:pleroma][:instance] == @with_key[:value]
+
+ assert version.backup[:pleroma][Pleroma.Web.Auth.Authenticator] ==
+ @value_not_keyword[:value]
+
+ assert Repo.aggregate(ConfigDB, :count) == 3
+ assert Repo.aggregate(Version, :count) == 1
+ end
+
+ test "creates several versions" do
+ change1 = [@with_key]
+
+ {:ok,
+ %{
+ :insert_version => version1,
+ :update_all_versions => {0, nil},
+ {:insert_or_update, :pleroma, :instance} => _
+ }} = Versioning.new_version(change1)
+
+ change2 = [@without_key]
+
+ {:ok,
+ %{
+ :insert_version => version2,
+ :update_all_versions => {1, nil},
+ {:insert_or_update, :quack, nil} => _
+ }} = Versioning.new_version(change2)
+
+ version1 = refresh_record(version1)
+ refute version1.current
+
+ assert backup_length(version1) == 1
+
+ version2 = refresh_record(version2)
+ assert version2.current
+
+ assert backup_length(version2) == 2
+ end
+
+ test "error on empty list" do
+ assert Versioning.new_version([]) == {:error, :empty_changes}
+ end
+
+ test "error on bad format" do
+ assert Versioning.new_version(nil) == {:error, :bad_format}
+ end
+
+ test "process changes as single map" do
+ {:ok,
+ %{
+ :insert_version => _,
+ :update_all_versions => {0, nil},
+ {:insert_or_update, :pleroma, :instance} => _
+ }} = Versioning.new_version(@with_key)
+
+ assert Repo.aggregate(ConfigDB, :count) == 1
+ end
+
+ test "error if value is not keyword" do
+ assert Versioning.new_version([
+ %{group: :pleroma, key: :key, value: %{}}
+ ]) ==
+ {:error, {:error, :pleroma, :key},
+ {:value_must_be_keyword, %{group: :pleroma, key: :key, value: %{}}}, %{}}
+ end
+
+ test "error if value is list" do
+ assert Versioning.new_version([
+ %{group: :pleroma, key: :key, value: [1]}
+ ]) ==
+ {:error, {:error, :pleroma, :key},
+ {:value_must_be_keyword, %{group: :pleroma, key: :key, value: [1]}}, %{}}
+ end
+ end
+
+ describe "rollback/1" do
+ test "bad steps format" do
+ assert Versioning.rollback(nil) == {:error, :steps_format}
+ end
+
+ test "no versions" do
+ assert Versioning.rollback() == {:error, :no_current_version}
+ end
+
+ test "rollback not possible, because there is only one version" do
+ {:ok, _} = Versioning.new_version(@with_key)
+
+ assert Versioning.rollback() == {:error, :rollback_not_possible}
+ end
+
+ test "rollbacks to previous version" do
+ {:ok, _} = Versioning.new_version(@with_key)
+
+ {:ok, _} = Versioning.new_version(@value_not_keyword)
+
+ {:ok, _} = Versioning.new_version(@without_key)
+
+ {:ok, _} = Versioning.rollback()
+
+ configs = ConfigDB.all()
+
+ Enum.each(configs, fn
+ %{key: :instance} = config ->
+ config.value == @with_key[:value]
+
+ %{key: Pleroma.Web.Auth.Authenticator} = config ->
+ config.value == @value_not_keyword[:value]
+ end)
+
+ assert Repo.aggregate(Version, :count) == 2
+
+ version = Repo.get_by(Version, current: true)
+
+ assert version.backup[:pleroma][:instance] == @with_key[:value]
+
+ assert version.backup[:pleroma][Pleroma.Web.Auth.Authenticator] ==
+ @value_not_keyword[:value]
+ end
+
+ test "rollbacks with 2 steps" do
+ {:ok, _} = Versioning.new_version(@with_key)
+
+ {:ok, _} = Versioning.new_version(@without_key)
+
+ {:ok, _} =
+ Versioning.new_version(%{
+ group: :pleroma,
+ key: :instance,
+ value: [name: "New name"]
+ })
+
+ assert Repo.aggregate(ConfigDB, :count) == 2
+ assert Repo.aggregate(Version, :count) == 3
+ {:ok, _} = Versioning.rollback(2)
+
+ assert Repo.aggregate(Version, :count) == 1
+
+ [with_key] = ConfigDB.all()
+
+ assert with_key.value == @with_key[:value]
+ end
+
+ test "rollbacks with 2 steps and creates new version for new change" do
+ {:ok, _} = Versioning.new_version(@with_key)
+
+ {:ok, _} = Versioning.new_version(@without_key)
+
+ {:ok, _} =
+ Versioning.new_version(%{
+ group: :pleroma,
+ key: :instance,
+ value: [name: "New name"]
+ })
+
+ {:ok, _} = Versioning.rollback(2)
+
+ {:ok, _} =
+ Versioning.new_version(%{
+ group: :pleroma,
+ key: :instance,
+ value: [name: "Last name"]
+ })
+
+ [with_key] = ConfigDB.all()
+ assert with_key.value == [name: "Last name"]
+ end
+
+ test "properly rollbacks with settings without keys" do
+ {:ok, _} = Versioning.new_version(@with_key)
+
+ {:ok, _} = Versioning.new_version(@without_key)
+
+ {:ok, _} =
+ Versioning.new_version(%{
+ group: :pleroma,
+ key: :instance,
+ value: [name: "New name"]
+ })
+
+ {:ok, _} = Versioning.rollback()
+
+ config = ConfigDB.get_by_params(%{group: :quack})
+ assert config.value == @without_key[:value]
+ end
+
+ test "properly rollbacks with logger settings" do
+ {:ok, _} = Versioning.new_version(@with_key)
+
+ {:ok, _} =
+ Versioning.new_version([
+ %{
+ group: :logger,
+ value: [
+ console: [
+ level: :debug,
+ format: "\n$time $metadata[$level] $message\n",
+ metadata: [:request_id]
+ ],
+ backends: [:console]
+ ]
+ }
+ ])
+
+ {:ok, _} = Versioning.new_version(@without_key)
+
+ {:ok, _} = Versioning.rollback()
+
+ logger = ConfigDB.get_by_params(%{group: :logger})
+
+ assert logger.value == [
+ console: [
+ level: :debug,
+ format: "\n$time $metadata[$level] $message\n",
+ metadata: [:request_id]
+ ],
+ backends: [:console]
+ ]
+ end
+ end
+
+ describe "migrate/1" do
+ test "migrates settings from config file" do
+ {:ok, _} = Versioning.migrate("test/fixtures/config/temp.secret.exs")
+
+ assert Repo.aggregate(ConfigDB, :count) == 3
+
+ config1 = ConfigDB.get_by_params(%{group: :pleroma, key: :first_setting})
+ config2 = ConfigDB.get_by_params(%{group: :pleroma, key: :second_setting})
+ config3 = ConfigDB.get_by_params(%{group: :quack})
+
+ assert config1.value == [key: "value", key2: [Repo]]
+ assert config2.value == [key: "value2", key2: ["Activity"]]
+ assert config3.value == [level: :info]
+
+ [version] = Repo.all(Version)
+
+ assert version.backup == [
+ pleroma: [
+ second_setting: [key: "value2", key2: ["Activity"]],
+ first_setting: [key: "value", key2: [Repo]]
+ ],
+ quack: [level: :info]
+ ]
+ end
+
+ test "truncates table on migration" do
+ insert_list(4, :config)
+
+ assert Repo.aggregate(ConfigDB, :count) == 4
+
+ {:ok, _} = Versioning.migrate("test/fixtures/config/temp.secret.exs")
+
+ assert Repo.aggregate(ConfigDB, :count) == 3
+ end
+ end
+
+ describe "migrate_namespace/2" do
+ test "common namespace rename" do
+ value_before_migration = [name: "Name"]
+
+ {:ok, %{:insert_version => version1}} =
+ Versioning.new_version(%{
+ group: :pleroma,
+ key: :key1,
+ value: value_before_migration
+ })
+
+ {:ok, %{:insert_version => version2}} =
+ Versioning.new_version(%{
+ group: :pleroma,
+ key: :key2,
+ value: [name: "Name"]
+ })
+
+ {:ok, %{:insert_version => version3}} =
+ Versioning.new_version(%{
+ group: :pleroma,
+ key: :key3,
+ value: [name: "Name"]
+ })
+
+ {:ok, _} = Versioning.migrate_namespace({:pleroma, :key1}, {:ex_aws, :new_key})
+
+ version1 = refresh_record(version1)
+ assert version1.backup == [ex_aws: [new_key: [name: "Name"]]]
+ version2 = refresh_record(version2)
+
+ assert version2.backup == [
+ ex_aws: [new_key: [name: "Name"]],
+ pleroma: [key2: [name: "Name"]]
+ ]
+
+ version3 = refresh_record(version3)
+
+ assert version3.backup == [
+ ex_aws: [new_key: [name: "Name"]],
+ pleroma: [key2: [name: "Name"], key3: [name: "Name"]]
+ ]
+
+ assert Repo.aggregate(from(c in ConfigDB, where: c.group == ^:pleroma), :count, :id) == 2
+ config = ConfigDB.get_by_params(%{group: :ex_aws, key: :new_key})
+ assert config.value == value_before_migration
+
+ {:ok, _} = Versioning.migrate_namespace({:pleroma, :key2}, {:pleroma, :new_key})
+
+ version1 = refresh_record(version1)
+ assert version1.backup == [ex_aws: [new_key: [name: "Name"]]]
+ version2 = refresh_record(version2)
+
+ assert version2.backup == [
+ pleroma: [new_key: [name: "Name"]],
+ ex_aws: [new_key: [name: "Name"]]
+ ]
+
+ version3 = refresh_record(version3)
+
+ assert version3.backup == [
+ ex_aws: [new_key: [name: "Name"]],
+ pleroma: [new_key: [name: "Name"], key3: [name: "Name"]]
+ ]
+ end
+
+ test "old namespace exists in old backups" do
+ {:ok, %{:insert_version => version1}} =
+ Versioning.new_version(%{
+ group: :pleroma,
+ key: :key1,
+ value: [name: "Name"]
+ })
+
+ {:ok, %{:insert_version => version2}} =
+ Versioning.new_version([
+ %{
+ group: :pleroma,
+ key: :key2,
+ value: [name: "Name"]
+ },
+ %{group: :pleroma, key: :key1, delete: true}
+ ])
+
+ {:ok, _} = Versioning.migrate_namespace({:pleroma, :key1}, {:ex_aws, :new_key})
+
+ version1 = refresh_record(version1)
+ assert version1.backup == [ex_aws: [new_key: [name: "Name"]]]
+ version2 = refresh_record(version2)
+
+ assert version2.backup == [
+ pleroma: [key2: [name: "Name"]]
+ ]
+
+ assert Repo.aggregate(from(c in ConfigDB, where: c.group == ^:pleroma), :count, :id) == 1
+ refute ConfigDB.get_by_params(%{group: :ex_aws, key: :new_key})
+ end
+ end
+
+ defp backup_length(%{backup: backup}) do
+ backup
+ |> Keyword.keys()
+ |> length()
+ end
+end
diff --git a/test/pleroma/config_db_test.exs b/test/pleroma/config_db_test.exs
index d42123fb4..26704169a 100644
--- a/test/pleroma/config_db_test.exs
+++ b/test/pleroma/config_db_test.exs
@@ -14,7 +14,7 @@ defmodule Pleroma.ConfigDBTest do
assert config == ConfigDB.get_by_params(%{group: config.group, key: config.key})
end
- test "get_all_as_keyword/0" do
+ test "all_as_keyword/0" do
saved = insert(:config)
insert(:config, group: ":quack", key: ":level", value: :info)
insert(:config, group: ":quack", key: ":meta", value: [:none])
@@ -25,7 +25,7 @@ defmodule Pleroma.ConfigDBTest do
value: "https://hooks.slack.com/services/KEY/some_val"
)
- config = ConfigDB.get_all_as_keyword()
+ config = ConfigDB.all_as_keyword()
assert config[:pleroma] == [
{saved.key, saved.value}
@@ -38,12 +38,12 @@ defmodule Pleroma.ConfigDBTest do
describe "update_or_create/1" do
test "common" do
- config = insert(:config)
+ config1 = insert(:config, value: [])
key2 = :another_key
params = [
- %{group: :pleroma, key: key2, value: "another_value"},
- %{group: :pleroma, key: config.key, value: [a: 1, b: 2, c: "new_value"]}
+ %{group: :pleroma, key: config1.key, value: [a: 1, b: 2, c: "new_value"]},
+ %{group: :pleroma, key: key2, value: [new_val: "another_value"]}
]
assert Repo.all(ConfigDB) |> length() == 1
@@ -52,11 +52,11 @@ defmodule Pleroma.ConfigDBTest do
assert Repo.all(ConfigDB) |> length() == 2
- config1 = ConfigDB.get_by_params(%{group: config.group, key: config.key})
+ config1 = ConfigDB.get_by_params(%{group: config1.group, key: config1.key})
config2 = ConfigDB.get_by_params(%{group: :pleroma, key: key2})
assert config1.value == [a: 1, b: 2, c: "new_value"]
- assert config2.value == "another_value"
+ assert config2.value == [new_val: "another_value"]
end
test "partial update" do
@@ -95,50 +95,18 @@ defmodule Pleroma.ConfigDBTest do
assert updated.value[:key3] == :val3
end
- test "only full update for some keys" do
- config1 = insert(:config, key: :ecto_repos, value: [repo: Pleroma.Repo])
-
- config2 = insert(:config, group: :cors_plug, key: :max_age, value: 18)
-
- {:ok, _config} =
- ConfigDB.update_or_create(%{
- group: config1.group,
- key: config1.key,
- value: [another_repo: [Pleroma.Repo]]
- })
-
- {:ok, _config} =
- ConfigDB.update_or_create(%{
- group: config2.group,
- key: config2.key,
- value: 777
- })
-
- updated1 = ConfigDB.get_by_params(%{group: config1.group, key: config1.key})
- updated2 = ConfigDB.get_by_params(%{group: config2.group, key: config2.key})
-
- assert updated1.value == [another_repo: [Pleroma.Repo]]
- assert updated2.value == 777
- end
-
- test "full update if value is not keyword" do
- config =
- insert(:config,
- group: ":tesla",
- key: ":adapter",
- value: Tesla.Adapter.Hackney
- )
+ test "only full update for groups without keys" do
+ config = insert(:config, group: :cors_plug, key: nil, value: [max_age: 18])
{:ok, _config} =
ConfigDB.update_or_create(%{
group: config.group,
- key: config.key,
- value: Tesla.Adapter.Httpc
+ key: nil,
+ value: [max_age: 25, credentials: true]
})
updated = ConfigDB.get_by_params(%{group: config.group, key: config.key})
-
- assert updated.value == Tesla.Adapter.Httpc
+ assert updated.value == [max_age: 25, credentials: true]
end
test "only full update for some subkeys" do
@@ -176,15 +144,14 @@ defmodule Pleroma.ConfigDBTest do
end
end
- describe "delete/1" do
+ describe "delete_or_update/1" do
test "error on deleting non existing setting" do
- {:error, error} = ConfigDB.delete(%{group: ":pleroma", key: ":key"})
- assert error =~ "Config with params %{group: \":pleroma\", key: \":key\"} not found"
+ assert {:ok, nil} == ConfigDB.delete_or_update(%{group: :pleroma, key: :key})
end
test "full delete" do
config = insert(:config)
- {:ok, deleted} = ConfigDB.delete(%{group: config.group, key: config.key})
+ {:ok, deleted} = ConfigDB.delete_or_update(%{group: config.group, key: config.key})
assert Ecto.get_meta(deleted, :state) == :deleted
refute ConfigDB.get_by_params(%{group: config.group, key: config.key})
end
@@ -193,7 +160,7 @@ defmodule Pleroma.ConfigDBTest do
config = insert(:config, value: [groups: [a: 1, b: 2], key: [a: 1]])
{:ok, deleted} =
- ConfigDB.delete(%{group: config.group, key: config.key, subkeys: [":groups"]})
+ ConfigDB.delete_or_update(%{group: config.group, key: config.key, subkeys: [:groups]})
assert Ecto.get_meta(deleted, :state) == :loaded
@@ -208,339 +175,246 @@ defmodule Pleroma.ConfigDBTest do
config = insert(:config, value: [groups: [a: 1, b: 2]])
{:ok, deleted} =
- ConfigDB.delete(%{group: config.group, key: config.key, subkeys: [":groups"]})
+ ConfigDB.delete_or_update(%{group: config.group, key: config.key, subkeys: [:groups]})
assert Ecto.get_meta(deleted, :state) == :deleted
refute ConfigDB.get_by_params(%{group: config.group, key: config.key})
end
- end
-
- describe "to_elixir_types/1" do
- test "string" do
- assert ConfigDB.to_elixir_types("value as string") == "value as string"
- end
-
- test "boolean" do
- assert ConfigDB.to_elixir_types(false) == false
- end
- test "nil" do
- assert ConfigDB.to_elixir_types(nil) == nil
+ test "delete struct" do
+ config = insert(:config)
+ {:ok, config} = ConfigDB.delete(config)
+ assert Ecto.get_meta(config, :state) == :deleted
+ assert Pleroma.Repo.aggregate(ConfigDB, :count) == 0
end
+ end
- test "integer" do
- assert ConfigDB.to_elixir_types(150) == 150
- end
+ test "all/0" do
+ config = insert(:config)
- test "atom" do
- assert ConfigDB.to_elixir_types(":atom") == :atom
- end
+ assert [^config] = ConfigDB.all()
+ end
- test "ssl options" do
- assert ConfigDB.to_elixir_types([":tlsv1", ":tlsv1.1", ":tlsv1.2"]) == [
- :tlsv1,
- :"tlsv1.1",
- :"tlsv1.2"
- ]
- end
+ describe "reduce_defaults_and_merge_with_changes/2" do
+ test "common changes" do
+ defaults = [
+ pleroma: [
+ key1: [k1: 1, k2: 1, k3: 1],
+ key2: [k1: 2, k2: 2, k3: 2]
+ ],
+ logger: [k1: 3, k2: 3]
+ ]
- test "pleroma module" do
- assert ConfigDB.to_elixir_types("Pleroma.Bookmark") == Pleroma.Bookmark
- end
+ config1 = insert(:config, key: :key1, value: [k1: 4, k2: 4])
+ config2 = insert(:config, key: :key2, value: [k1: 5, k2: 5])
- test "pleroma string" do
- assert ConfigDB.to_elixir_types("Pleroma") == "Pleroma"
- end
+ {changes, [logger: [k1: 3, k2: 3]]} =
+ ConfigDB.reduce_defaults_and_merge_with_changes([config1, config2], defaults)
- test "phoenix module" do
- assert ConfigDB.to_elixir_types("Phoenix.Socket.V1.JSONSerializer") ==
- Phoenix.Socket.V1.JSONSerializer
- end
+ Enum.each(changes, fn
+ %{key: :key1, value: value} ->
+ assert value == [k3: 1, k1: 4, k2: 4]
- test "tesla module" do
- assert ConfigDB.to_elixir_types("Tesla.Adapter.Hackney") == Tesla.Adapter.Hackney
+ %{key: :key2, value: value} ->
+ assert value == [k3: 2, k1: 5, k2: 5]
+ end)
end
- test "ExSyslogger module" do
- assert ConfigDB.to_elixir_types("ExSyslogger") == ExSyslogger
- end
+ test "changes for group without key" do
+ defaults = [
+ cors_plug: [
+ max_age: 86_400,
+ methods: ["POST", "PUT", "DELETE", "GET", "PATCH", "OPTIONS"]
+ ],
+ pleroma: [key1: [k1: 1, k2: 1, k3: 1]]
+ ]
- test "Quack.Logger module" do
- assert ConfigDB.to_elixir_types("Quack.Logger") == Quack.Logger
- end
+ config = insert(:config, group: :cors_plug, key: nil, value: [max_age: 60_000])
- test "Swoosh.Adapters modules" do
- assert ConfigDB.to_elixir_types("Swoosh.Adapters.SMTP") == Swoosh.Adapters.SMTP
- assert ConfigDB.to_elixir_types("Swoosh.Adapters.AmazonSES") == Swoosh.Adapters.AmazonSES
- end
+ {[change], [pleroma: [key1: [k1: 1, k2: 1, k3: 1]]]} =
+ ConfigDB.reduce_defaults_and_merge_with_changes([config], defaults)
- test "sigil" do
- assert ConfigDB.to_elixir_types("~r[comp[lL][aA][iI][nN]er]") == ~r/comp[lL][aA][iI][nN]er/
+ assert change.value == [
+ methods: ["POST", "PUT", "DELETE", "GET", "PATCH", "OPTIONS"],
+ max_age: 60_000
+ ]
end
- test "link sigil" do
- assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/") == ~r/https:\/\/example.com/
- end
+ test "for logger backend setting and others" do
+ defaults = [
+ logger: [
+ ex_syslogger: [k1: 1, k2: 1],
+ console: [k1: 2, k2: 2],
+ backends: [:ex_syslogger, :console],
+ key: 1
+ ],
+ pleroma: [key1: 1, key2: 2]
+ ]
- test "link sigil with um modifiers" do
- assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/um") ==
- ~r/https:\/\/example.com/um
- end
+ logger =
+ insert(:config,
+ group: :logger,
+ key: nil,
+ value: [ex_syslogger: [k1: 3, k2: 4], backends: [:console]]
+ )
- test "link sigil with i modifier" do
- assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/i") == ~r/https:\/\/example.com/i
- end
+ {[change], [pleroma: [key1: 1, key2: 2]]} =
+ ConfigDB.reduce_defaults_and_merge_with_changes([logger], defaults)
- test "link sigil with s modifier" do
- assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/s") == ~r/https:\/\/example.com/s
+ assert change.value == [
+ console: [k1: 2, k2: 2],
+ key: 1,
+ ex_syslogger: [k1: 3, k2: 4],
+ backends: [:console]
+ ]
end
- test "raise if valid delimiter not found" do
- assert_raise ArgumentError, "valid delimiter for Regex expression not found", fn ->
- ConfigDB.to_elixir_types("~r/https://[]{}<>\"'()|example.com/s")
- end
- end
+ test "with ex_syslogger, console and backends changes" do
+ defaults = [
+ logger: [
+ ex_syslogger: [k1: 1, k2: 1],
+ console: [k1: 2, k2: 2],
+ backends: [:ex_syslogger, :console],
+ key: 1
+ ],
+ pleroma: [key1: 1, key2: 2]
+ ]
- test "2 child tuple" do
- assert ConfigDB.to_elixir_types(%{"tuple" => ["v1", ":v2"]}) == {"v1", :v2}
- end
+ logger =
+ insert(:config,
+ group: :logger,
+ key: nil,
+ value: [console: [k1: 4, k2: 4], k1: 3, k2: 4, backends: [:console]]
+ )
- test "proxy tuple with localhost" do
- assert ConfigDB.to_elixir_types(%{
- "tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]
- }) == {:proxy_url, {:socks5, :localhost, 1234}}
- end
+ {[change], [pleroma: [key1: 1, key2: 2]]} =
+ ConfigDB.reduce_defaults_and_merge_with_changes([logger], defaults)
- test "proxy tuple with domain" do
- assert ConfigDB.to_elixir_types(%{
- "tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]
- }) == {:proxy_url, {:socks5, 'domain.com', 1234}}
+ assert change.value == [
+ ex_syslogger: [k1: 1, k2: 1],
+ key: 1,
+ console: [k1: 4, k2: 4],
+ k1: 3,
+ k2: 4,
+ backends: [:console]
+ ]
end
+ end
- test "proxy tuple with ip" do
- assert ConfigDB.to_elixir_types(%{
- "tuple" => [":proxy_url", %{"tuple" => [":socks5", "127.0.0.1", 1234]}]
- }) == {:proxy_url, {:socks5, {127, 0, 0, 1}, 1234}}
- end
+ test "all_with_db/0" do
+ config = insert(:config)
+ [change] = ConfigDB.all_with_db()
+ assert change.db == Keyword.keys(config.value)
+ end
- test "tuple with n childs" do
- assert ConfigDB.to_elixir_types(%{
- "tuple" => [
- "v1",
- ":v2",
- "Pleroma.Bookmark",
- 150,
- false,
- "Phoenix.Socket.V1.JSONSerializer"
+ test "from_keyword_to_structs/2" do
+ keyword = [
+ pleroma: [
+ key1: [k1: 1, k2: 1, k3: 1],
+ key2: [k1: 2, k2: 2, k3: 2]
+ ],
+ logger: [k1: 3, k2: 3, ex_syslogger: [k1: 4, k2: 4], console: [k1: 5, k2: 5]]
+ ]
+
+ changes = ConfigDB.from_keyword_to_structs(keyword)
+
+ Enum.each(changes, fn
+ %{key: :key1} = change ->
+ assert change.group == :pleroma
+ assert change.value == [k1: 1, k2: 1, k3: 1]
+
+ %{key: :key2} = change ->
+ assert change.group == :pleroma
+ assert change.value == [k1: 2, k2: 2, k3: 2]
+
+ %{key: nil} = change ->
+ assert change.group == :logger
+
+ assert change.value == [
+ k1: 3,
+ k2: 3,
+ ex_syslogger: [k1: 4, k2: 4],
+ console: [k1: 5, k2: 5]
]
- }) == {"v1", :v2, Pleroma.Bookmark, 150, false, Phoenix.Socket.V1.JSONSerializer}
- end
+ end)
+ end
- test "map with string key" do
- assert ConfigDB.to_elixir_types(%{"key" => "value"}) == %{"key" => "value"}
- end
+ describe "merge_changes_with_defaults/2" do
+ test "with existance changes" do
+ defaults = [
+ pleroma: [
+ key1: [k1: 1, k2: 1, k3: 1],
+ key2: [k1: 2, k2: 2, k3: 2]
+ ],
+ logger: [k1: 3, k2: 3]
+ ]
- test "map with atom key" do
- assert ConfigDB.to_elixir_types(%{":key" => "value"}) == %{key: "value"}
- end
+ config1 = insert(:config, key: :key1, value: [k1: 4, k2: 4])
+ config2 = insert(:config, key: :key2, value: [k1: 5, k2: 5])
- test "list of strings" do
- assert ConfigDB.to_elixir_types(["v1", "v2", "v3"]) == ["v1", "v2", "v3"]
- end
+ changes = ConfigDB.merge_changes_with_defaults([config1, config2], defaults)
- test "list of modules" do
- assert ConfigDB.to_elixir_types(["Pleroma.Repo", "Pleroma.Activity"]) == [
- Pleroma.Repo,
- Pleroma.Activity
- ]
+ Enum.each(changes, fn
+ %{key: :key1} = change -> assert change.value == [k3: 1, k1: 4, k2: 4]
+ %{key: :key2} = change -> assert change.value == [k3: 2, k1: 5, k2: 5]
+ end)
end
- test "list of atoms" do
- assert ConfigDB.to_elixir_types([":v1", ":v2", ":v3"]) == [:v1, :v2, :v3]
- end
+ test "full subkey update and deep merge" do
+ defaults = [
+ pleroma: [
+ assets: [
+ mascots: [3, 4],
+ subkey: [key1: [key: :val2, key2: :val2], key2: :val2],
+ key: 5
+ ]
+ ]
+ ]
- test "list of mixed values" do
- assert ConfigDB.to_elixir_types([
- "v1",
- ":v2",
- "Pleroma.Repo",
- "Phoenix.Socket.V1.JSONSerializer",
- 15,
- false
- ]) == [
- "v1",
- :v2,
- Pleroma.Repo,
- Phoenix.Socket.V1.JSONSerializer,
- 15,
- false
- ]
- end
+ config =
+ insert(:config,
+ group: :pleroma,
+ key: :assets,
+ value: [mascots: [1, 2], subkey: [key1: [key: :val1, key2: :val1], key2: :val1]]
+ )
- test "simple keyword" do
- assert ConfigDB.to_elixir_types([%{"tuple" => [":key", "value"]}]) == [key: "value"]
- end
+ [merged] = ConfigDB.merge_changes_with_defaults([config], defaults)
- test "keyword" do
- assert ConfigDB.to_elixir_types([
- %{"tuple" => [":types", "Pleroma.PostgresTypes"]},
- %{"tuple" => [":telemetry_event", ["Pleroma.Repo.Instrumenter"]]},
- %{"tuple" => [":migration_lock", nil]},
- %{"tuple" => [":key1", 150]},
- %{"tuple" => [":key2", "string"]}
- ]) == [
- types: Pleroma.PostgresTypes,
- telemetry_event: [Pleroma.Repo.Instrumenter],
- migration_lock: nil,
- key1: 150,
- key2: "string"
+ assert merged.value == [
+ mascots: [1, 2],
+ key: 5,
+ subkey: [key1: [key: :val1, key2: :val1], key2: :val1]
]
end
- test "trandformed keyword" do
- assert ConfigDB.to_elixir_types(a: 1, b: 2, c: "string") == [a: 1, b: 2, c: "string"]
- end
+ test "merge for other subkeys" do
+ defaults = [pleroma: [assets: [key: 5]]]
- test "complex keyword with nested mixed childs" do
- assert ConfigDB.to_elixir_types([
- %{"tuple" => [":uploader", "Pleroma.Uploaders.Local"]},
- %{"tuple" => [":filters", ["Pleroma.Upload.Filter.Dedupe"]]},
- %{"tuple" => [":link_name", true]},
- %{"tuple" => [":proxy_remote", false]},
- %{"tuple" => [":common_map", %{":key" => "value"}]},
- %{
- "tuple" => [
- ":proxy_opts",
- [
- %{"tuple" => [":redirect_on_failure", false]},
- %{"tuple" => [":max_body_length", 1_048_576]},
- %{
- "tuple" => [
- ":http",
- [
- %{"tuple" => [":follow_redirect", true]},
- %{"tuple" => [":pool", ":upload"]}
- ]
- ]
- }
- ]
- ]
- }
- ]) == [
- uploader: Pleroma.Uploaders.Local,
- filters: [Pleroma.Upload.Filter.Dedupe],
- link_name: true,
- proxy_remote: false,
- common_map: %{key: "value"},
- proxy_opts: [
- redirect_on_failure: false,
- max_body_length: 1_048_576,
- http: [
- follow_redirect: true,
- pool: :upload
- ]
- ]
- ]
- end
+ config =
+ insert(:config,
+ group: :pleroma,
+ key: :assets,
+ value: [subkey: 3, default_mascot: :test_mascot]
+ )
- test "common keyword" do
- assert ConfigDB.to_elixir_types([
- %{"tuple" => [":level", ":warn"]},
- %{"tuple" => [":meta", [":all"]]},
- %{"tuple" => [":path", ""]},
- %{"tuple" => [":val", nil]},
- %{"tuple" => [":webhook_url", "https://hooks.slack.com/services/YOUR-KEY-HERE"]}
- ]) == [
- level: :warn,
- meta: [:all],
- path: "",
- val: nil,
- webhook_url: "https://hooks.slack.com/services/YOUR-KEY-HERE"
- ]
+ [merged] = ConfigDB.merge_changes_with_defaults([config], defaults)
+ assert merged.value == [key: 5, subkey: 3, default_mascot: :test_mascot]
end
- test "complex keyword with sigil" do
- assert ConfigDB.to_elixir_types([
- %{"tuple" => [":federated_timeline_removal", []]},
- %{"tuple" => [":reject", ["~r/comp[lL][aA][iI][nN]er/"]]},
- %{"tuple" => [":replace", []]}
- ]) == [
- federated_timeline_removal: [],
- reject: [~r/comp[lL][aA][iI][nN]er/],
- replace: []
- ]
- end
+ test "with change deletion" do
+ defaults = [pleroma: [assets: [key: 5]]]
- test "complex keyword with tuples with more than 2 values" do
- assert ConfigDB.to_elixir_types([
- %{
- "tuple" => [
- ":http",
- [
- %{
- "tuple" => [
- ":key1",
- [
- %{
- "tuple" => [
- ":_",
- [
- %{
- "tuple" => [
- "/api/v1/streaming",
- "Pleroma.Web.MastodonAPI.WebsocketHandler",
- []
- ]
- },
- %{
- "tuple" => [
- "/websocket",
- "Phoenix.Endpoint.CowboyWebSocket",
- %{
- "tuple" => [
- "Phoenix.Transports.WebSocket",
- %{
- "tuple" => [
- "Pleroma.Web.Endpoint",
- "Pleroma.Web.UserSocket",
- []
- ]
- }
- ]
- }
- ]
- },
- %{
- "tuple" => [
- ":_",
- "Phoenix.Endpoint.Cowboy2Handler",
- %{"tuple" => ["Pleroma.Web.Endpoint", []]}
- ]
- }
- ]
- ]
- }
- ]
- ]
- }
- ]
- ]
- }
- ]) == [
- http: [
- key1: [
- {:_,
- [
- {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
- {"/websocket", Phoenix.Endpoint.CowboyWebSocket,
- {Phoenix.Transports.WebSocket,
- {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, []}}},
- {:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
- ]}
- ]
- ]
- ]
+ config =
+ insert(:config,
+ group: :pleroma,
+ key: :assets,
+ value: [subkey: 3, default_mascot: :test_mascot]
+ )
+
+ {:ok, config} = ConfigDB.delete(config)
+ [merged] = ConfigDB.merge_changes_with_defaults([config], defaults)
+ assert merged.value == [key: 5]
end
end
end
diff --git a/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs b/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs
index 8cd9f939b..5871f3a9e 100644
--- a/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs
+++ b/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs
@@ -6,7 +6,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
use Pleroma.Web.ConnCase
use Oban.Testing, repo: Pleroma.Repo
- import ExUnit.CaptureLog
import Pleroma.Factory
import Swoosh.TestAssertions
@@ -322,28 +321,12 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
setup do: clear_config(:configurable_from_database, true)
test "pleroma restarts", %{conn: conn} do
- capture_log(fn ->
- assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == %{}
- end) =~ "pleroma restarted"
+ assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == %{}
- refute Restarter.Pleroma.need_reboot?()
+ refute Pleroma.Application.ConfigDependentDeps.need_reboot?()
end
end
- test "need_reboot flag", %{conn: conn} do
- assert conn
- |> get("/api/pleroma/admin/need_reboot")
- |> json_response(200) == %{"need_reboot" => false}
-
- Restarter.Pleroma.need_reboot()
-
- assert conn
- |> get("/api/pleroma/admin/need_reboot")
- |> json_response(200) == %{"need_reboot" => true}
-
- on_exit(fn -> Restarter.Pleroma.refresh() end)
- end
-
describe "GET /api/pleroma/admin/users/:nickname/statuses" do
setup do
user = insert(:user)
@@ -999,10 +982,3 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
end
end
end
-
-# Needed for testing
-defmodule Pleroma.Web.Endpoint.NotReal do
-end
-
-defmodule Pleroma.Captcha.NotReal do
-end
diff --git a/test/pleroma/web/admin_api/controllers/config_controller_test.exs b/test/pleroma/web/admin_api/controllers/config_controller_test.exs
index 578a4c914..39962e5a5 100644
--- a/test/pleroma/web/admin_api/controllers/config_controller_test.exs
+++ b/test/pleroma/web/admin_api/controllers/config_controller_test.exs
@@ -5,10 +5,9 @@
defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
use Pleroma.Web.ConnCase
- import ExUnit.CaptureLog
import Pleroma.Factory
- alias Pleroma.ConfigDB
+ alias Pleroma.Config
setup do
admin = insert(:user, is_admin: true)
@@ -18,7 +17,9 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
build_conn()
|> assign(:user, admin)
|> assign(:token, token)
+ |> put_req_header("content-type", "application/json")
+ on_exit(fn -> Pleroma.Application.ConfigDependentDeps.clear_state() end)
{:ok, %{admin: admin, token: token, conn: conn}}
end
@@ -27,9 +28,10 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
test "when configuration from database is off", %{conn: conn} do
clear_config(:configurable_from_database, false)
- conn = get(conn, "/api/pleroma/admin/config")
- assert json_response_and_validate_schema(conn, 400) ==
+ assert conn
+ |> get("/api/pleroma/admin/config")
+ |> json_response_and_validate_schema(400) ==
%{
"error" => "You must enable configurable_from_database in your config file."
}
@@ -61,7 +63,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
end
test "db is added to settings that are in db", %{conn: conn} do
- _config = insert(:config, key: ":instance", value: [name: "Some name"])
+ _config = insert(:config, key: :instance, value: [name: "Some name"])
%{"configs" => configs} =
conn
@@ -76,6 +78,27 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
assert instance_config["db"] == [":name"]
end
+ test "setting with value not keyword", %{conn: conn} do
+ _config =
+ insert(:config,
+ key: Pleroma.Web.Auth.Authenticator,
+ value: Pleroma.Web.Auth.LDAPAuthenticator
+ )
+
+ %{"configs" => configs} =
+ conn
+ |> get("/api/pleroma/admin/config")
+ |> json_response_and_validate_schema(200)
+
+ [instance_config] =
+ Enum.filter(configs, fn %{"group" => group, "key" => key} ->
+ group == ":pleroma" and key == "Pleroma.Web.Auth.Authenticator"
+ end)
+
+ assert instance_config["db"] == ["Pleroma.Web.Auth.Authenticator"]
+ assert instance_config["value"] == "Pleroma.Web.Auth.LDAPAuthenticator"
+ end
+
test "merged default setting with db settings", %{conn: conn} do
config1 = insert(:config)
config2 = insert(:config)
@@ -94,30 +117,19 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
saved_configs = [config1, config2, config3]
keys = Enum.map(saved_configs, &inspect(&1.key))
+ values = Map.new(saved_configs, fn config -> {config.key, config.value} end)
- received_configs =
- Enum.filter(configs, fn %{"group" => group, "key" => key} ->
+ configs =
+ configs
+ |> Enum.filter(fn %{"group" => group, "key" => key} ->
group == ":pleroma" and key in keys
end)
+ |> Config.Converter.to_elixir_types()
- assert length(received_configs) == 3
-
- db_keys =
- config3.value
- |> Keyword.keys()
- |> ConfigDB.to_json_types()
-
- keys = Enum.map(saved_configs -- [config3], &inspect(&1.key))
+ assert length(configs) == 3
- values = Enum.map(saved_configs, &ConfigDB.to_json_types(&1.value))
-
- mapset_keys = MapSet.new(keys ++ db_keys)
-
- Enum.each(received_configs, fn %{"value" => value, "db" => db} ->
- db = MapSet.new(db)
- assert MapSet.subset?(db, mapset_keys)
-
- assert value in values
+ Enum.each(configs, fn %{"key" => key, "value" => value} ->
+ assert values[key] == value
end)
end
@@ -145,8 +157,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
emoji = Enum.find(vals, fn %{"key" => key} -> key == ":emoji" end)
assets = Enum.find(vals, fn %{"key" => key} -> key == ":assets" end)
- emoji_val = ConfigDB.to_elixir_types(emoji["value"])
- assets_val = ConfigDB.to_elixir_types(assets["value"])
+ emoji_val = Config.Converter.to_elixir_types(emoji["value"])
+ assets_val = Config.Converter.to_elixir_types(assets["value"])
assert emoji_val[:groups] == [a: 1, b: 2]
assert assets_val[:mascots] == [a: 1, b: 2]
@@ -188,13 +200,11 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
Application.delete_env(:pleroma, Pleroma.Captcha.NotReal)
Application.put_env(:pleroma, :http, http)
Application.put_env(:tesla, :adapter, Tesla.Mock)
- Restarter.Pleroma.refresh()
end)
end
setup do: clear_config(:configurable_from_database, true)
- @tag capture_log: true
test "create new config setting in db", %{conn: conn} do
ueberauth = Application.get_env(:ueberauth, Ueberauth)
on_exit(fn -> Application.put_env(:ueberauth, Ueberauth, ueberauth) end)
@@ -204,7 +214,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|> put_req_header("content-type", "application/json")
|> post("/api/pleroma/admin/config", %{
configs: [
- %{group: ":pleroma", key: ":key1", value: "value1"},
+ %{group: ":pleroma", key: ":key1", value: [%{"tuple" => [":key", "value1"]}]},
%{
group: ":ueberauth",
key: "Ueberauth",
@@ -213,31 +223,40 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
%{
group: ":pleroma",
key: ":key2",
- value: %{
- ":nested_1" => "nested_value1",
- ":nested_2" => [
- %{":nested_22" => "nested_value222"},
- %{":nested_33" => %{":nested_44" => "nested_444"}}
- ]
- }
+ value: [
+ %{"tuple" => [":nested_1", "nested_value1"]},
+ %{
+ "tuple" => [
+ ":nested_2",
+ [
+ %{":nested_22" => "nested_value222"},
+ %{":nested_33" => %{":nested_44" => "nested_444"}}
+ ]
+ ]
+ }
+ ]
},
%{
group: ":pleroma",
key: ":key3",
value: [
- %{"nested_3" => ":nested_3", "nested_33" => "nested_33"},
- %{"nested_4" => true}
+ %{"tuple" => [":key", ":nested_3"]},
+ %{"tuple" => [":nested_33", "nested_33"]},
+ %{"tuple" => [":key", true]}
]
},
%{
group: ":pleroma",
key: ":key4",
- value: %{":nested_5" => ":upload", "endpoint" => "https://example.com"}
+ value: [
+ %{"tuple" => [":nested_5", ":upload"]},
+ %{"tuple" => [":endpoint", "https://example.com"]}
+ ]
},
%{
group: ":idna",
key: ":key5",
- value: %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]}
+ value: [%{"tuple" => [":string", "Pleroma.Captcha.NotReal"]}]
}
]
})
@@ -245,86 +264,92 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
assert json_response_and_validate_schema(conn, 200) == %{
"configs" => [
%{
- "group" => ":pleroma",
- "key" => ":key1",
- "value" => "value1",
- "db" => [":key1"]
- },
- %{
+ "db" => [":consumer_secret"],
"group" => ":ueberauth",
"key" => "Ueberauth",
- "value" => [%{"tuple" => [":consumer_secret", "aaaa"]}],
- "db" => [":consumer_secret"]
+ "value" => [%{"tuple" => [":consumer_secret", "aaaa"]}]
},
%{
+ "db" => [":nested_5", ":endpoint"],
"group" => ":pleroma",
- "key" => ":key2",
- "value" => %{
- ":nested_1" => "nested_value1",
- ":nested_2" => [
- %{":nested_22" => "nested_value222"},
- %{":nested_33" => %{":nested_44" => "nested_444"}}
- ]
- },
- "db" => [":key2"]
+ "key" => ":key4",
+ "value" => [
+ %{"tuple" => [":nested_5", ":upload"]},
+ %{"tuple" => [":endpoint", "https://example.com"]}
+ ]
},
%{
+ "db" => [":key", ":nested_33", ":key"],
"group" => ":pleroma",
"key" => ":key3",
"value" => [
- %{"nested_3" => ":nested_3", "nested_33" => "nested_33"},
- %{"nested_4" => true}
- ],
- "db" => [":key3"]
+ %{"tuple" => [":key", ":nested_3"]},
+ %{"tuple" => [":nested_33", "nested_33"]},
+ %{"tuple" => [":key", true]}
+ ]
},
%{
+ "db" => [":nested_1", ":nested_2"],
"group" => ":pleroma",
- "key" => ":key4",
- "value" => %{"endpoint" => "https://example.com", ":nested_5" => ":upload"},
- "db" => [":key4"]
+ "key" => ":key2",
+ "value" => [
+ %{"tuple" => [":nested_1", "nested_value1"]},
+ %{
+ "tuple" => [
+ ":nested_2",
+ [
+ %{":nested_22" => "nested_value222"},
+ %{":nested_33" => %{":nested_44" => "nested_444"}}
+ ]
+ ]
+ }
+ ]
+ },
+ %{
+ "db" => [":key"],
+ "group" => ":pleroma",
+ "key" => ":key1",
+ "value" => [%{"tuple" => [":key", "value1"]}]
},
%{
+ "db" => [":string"],
"group" => ":idna",
"key" => ":key5",
- "value" => %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]},
- "db" => [":key5"]
+ "value" => [%{"tuple" => [":string", "Pleroma.Captcha.NotReal"]}]
}
],
"need_reboot" => false
}
- assert Application.get_env(:pleroma, :key1) == "value1"
+ assert Application.get_env(:pleroma, :key1) == [key: "value1"]
- assert Application.get_env(:pleroma, :key2) == %{
+ assert Application.get_env(:pleroma, :key2) == [
nested_1: "nested_value1",
nested_2: [
%{nested_22: "nested_value222"},
%{nested_33: %{nested_44: "nested_444"}}
]
- }
+ ]
assert Application.get_env(:pleroma, :key3) == [
- %{"nested_3" => :nested_3, "nested_33" => "nested_33"},
- %{"nested_4" => true}
+ key: :nested_3,
+ nested_33: "nested_33",
+ key: true
]
- assert Application.get_env(:pleroma, :key4) == %{
- "endpoint" => "https://example.com",
- nested_5: :upload
- }
+ assert Application.get_env(:pleroma, :key4) == [
+ nested_5: :upload,
+ endpoint: "https://example.com"
+ ]
- assert Application.get_env(:idna, :key5) == {"string", Pleroma.Captcha.NotReal, []}
+ assert Application.get_env(:idna, :key5) == [string: Pleroma.Captcha.NotReal]
end
- test "save configs setting without explicit key", %{conn: conn} do
- level = Application.get_env(:quack, :level)
- meta = Application.get_env(:quack, :meta)
- webhook_url = Application.get_env(:quack, :webhook_url)
+ test "save configs setting without key", %{conn: conn} do
+ quack_env = Application.get_all_env(:quack)
on_exit(fn ->
- Application.put_env(:quack, :level, level)
- Application.put_env(:quack, :meta, meta)
- Application.put_env(:quack, :webhook_url, webhook_url)
+ Application.put_all_env(quack: quack_env)
end)
conn =
@@ -334,18 +359,11 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
configs: [
%{
group: ":quack",
- key: ":level",
- value: ":info"
- },
- %{
- group: ":quack",
- key: ":meta",
- value: [":none"]
- },
- %{
- group: ":quack",
- key: ":webhook_url",
- value: "https://hooks.slack.com/services/KEY"
+ value: [
+ %{"tuple" => [":level", ":info"]},
+ %{"tuple" => [":meta", [":none"]]},
+ %{"tuple" => [":webhook_url", "https://hooks.slack.com/services/KEY"]}
+ ]
}
]
})
@@ -354,21 +372,13 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
"configs" => [
%{
"group" => ":quack",
- "key" => ":level",
- "value" => ":info",
- "db" => [":level"]
- },
- %{
- "group" => ":quack",
- "key" => ":meta",
- "value" => [":none"],
- "db" => [":meta"]
- },
- %{
- "group" => ":quack",
- "key" => ":webhook_url",
- "value" => "https://hooks.slack.com/services/KEY",
- "db" => [":webhook_url"]
+ "key" => nil,
+ "value" => [
+ %{"tuple" => [":level", ":info"]},
+ %{"tuple" => [":meta", [":none"]]},
+ %{"tuple" => [":webhook_url", "https://hooks.slack.com/services/KEY"]}
+ ],
+ "db" => [":level", ":meta", ":webhook_url"]
}
],
"need_reboot" => false
@@ -380,7 +390,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
end
test "saving config with partial update", %{conn: conn} do
- insert(:config, key: ":key1", value: :erlang.term_to_binary(key1: 1, key2: 2))
+ insert(:config, key: ":key1", value: [key1: 1, key2: 2])
conn =
conn
@@ -440,10 +450,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
assert configs["need_reboot"]
- capture_log(fn ->
- assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) ==
- %{}
- end) =~ "pleroma restarted"
+ assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) ==
+ %{}
configs =
conn
@@ -499,10 +507,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
"need_reboot" => true
}
- capture_log(fn ->
- assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) ==
- %{}
- end) =~ "pleroma restarted"
+ assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) ==
+ %{}
configs =
conn
@@ -610,136 +616,86 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
]
end
- test "saving full setting if value is in full_key_update list", %{conn: conn} do
- backends = Application.get_env(:logger, :backends)
- on_exit(fn -> Application.put_env(:logger, :backends, backends) end)
-
- insert(:config,
- group: :logger,
- key: :backends,
- value: []
- )
-
- Pleroma.Config.TransferTask.load_and_update_env([], false)
-
- assert Application.get_env(:logger, :backends) == []
+ test "update config setting & delete with fallback to default value", %{conn: conn} do
+ ueberauth = Application.get_env(:ueberauth, Ueberauth)
+ insert(:config, key: :keyaa1, value: [key: "value"])
+ insert(:config, key: :keyaa2, value: [key: "value"])
- conn =
- conn
- |> put_req_header("content-type", "application/json")
- |> post("/api/pleroma/admin/config", %{
+ resp =
+ post(conn, "/api/pleroma/admin/config", %{
configs: [
%{
- group: ":logger",
- key: ":backends",
- value: [":console"]
+ group: ":pleroma",
+ key: ":keyaa1",
+ value: [
+ %{"tuple" => [":key", "value2"]},
+ %{"tuple" => [":key2", "value"]}
+ ]
+ },
+ %{group: ":pleroma", key: ":keyaa2", value: [%{"tuple" => [":key", "value2"]}]},
+ %{
+ group: ":ueberauth",
+ key: "Ueberauth",
+ value: [
+ %{"tuple" => [":another_key", "somevalue"]},
+ %{"tuple" => [":another", "somevalue"]}
+ ]
+ },
+ %{
+ group: ":pleroma",
+ key: "Pleroma.Uploaders.Local",
+ delete: true
}
]
})
- assert json_response_and_validate_schema(conn, 200) == %{
+ assert json_response_and_validate_schema(resp, 200) == %{
"configs" => [
%{
- "group" => ":logger",
- "key" => ":backends",
+ "db" => [":another_key", ":another"],
+ "group" => ":ueberauth",
+ "key" => "Ueberauth",
"value" => [
- ":console"
- ],
- "db" => [":backends"]
- }
- ],
- "need_reboot" => false
- }
-
- assert Application.get_env(:logger, :backends) == [
- :console
- ]
- end
-
- test "saving full setting if value is not keyword", %{conn: conn} do
- insert(:config,
- group: :tesla,
- key: :adapter,
- value: Tesla.Adapter.Hackey
- )
-
- conn =
- conn
- |> put_req_header("content-type", "application/json")
- |> post("/api/pleroma/admin/config", %{
- configs: [
- %{group: ":tesla", key: ":adapter", value: "Tesla.Adapter.Httpc"}
- ]
- })
-
- assert json_response_and_validate_schema(conn, 200) == %{
- "configs" => [
- %{
- "group" => ":tesla",
- "key" => ":adapter",
- "value" => "Tesla.Adapter.Httpc",
- "db" => [":adapter"]
- }
- ],
- "need_reboot" => false
- }
- end
-
- test "update config setting & delete with fallback to default value", %{
- conn: conn,
- admin: admin,
- token: token
- } do
- ueberauth = Application.get_env(:ueberauth, Ueberauth)
- insert(:config, key: :keyaa1)
- insert(:config, key: :keyaa2)
-
- config3 =
- insert(:config,
- group: :ueberauth,
- key: Ueberauth
- )
-
- conn =
- conn
- |> put_req_header("content-type", "application/json")
- |> post("/api/pleroma/admin/config", %{
- configs: [
- %{group: ":pleroma", key: ":keyaa1", value: "another_value"},
- %{group: ":pleroma", key: ":keyaa2", value: "another_value"}
- ]
- })
-
- assert json_response_and_validate_schema(conn, 200) == %{
- "configs" => [
+ %{"tuple" => [":another_key", "somevalue"]},
+ %{"tuple" => [":another", "somevalue"]}
+ ]
+ },
%{
"group" => ":pleroma",
- "key" => ":keyaa1",
- "value" => "another_value",
- "db" => [":keyaa1"]
+ "key" => ":keyaa2",
+ "value" => [
+ %{"tuple" => [":key", "value2"]}
+ ],
+ "db" => [":key"]
},
%{
"group" => ":pleroma",
- "key" => ":keyaa2",
- "value" => "another_value",
- "db" => [":keyaa2"]
+ "key" => ":keyaa1",
+ "value" => [
+ %{"tuple" => [":key", "value2"]},
+ %{"tuple" => [":key2", "value"]}
+ ],
+ "db" => [":key", ":key2"]
}
],
"need_reboot" => false
}
- assert Application.get_env(:pleroma, :keyaa1) == "another_value"
- assert Application.get_env(:pleroma, :keyaa2) == "another_value"
- assert Application.get_env(:ueberauth, Ueberauth) == config3.value
+ assert Application.get_env(:pleroma, :keyaa1) == [key: "value2", key2: "value"]
+ assert Application.get_env(:pleroma, :keyaa2) == [key: "value2"]
- conn =
- build_conn()
- |> assign(:user, admin)
- |> assign(:token, token)
- |> put_req_header("content-type", "application/json")
- |> post("/api/pleroma/admin/config", %{
+ assert Application.get_env(:ueberauth, Ueberauth) == [
+ base_path: "/oauth",
+ providers: [],
+ another_key: "somevalue",
+ another: "somevalue"
+ ]
+
+ resp =
+ post(conn, "/api/pleroma/admin/config", %{
configs: [
%{group: ":pleroma", key: ":keyaa2", delete: true},
+ %{group: ":pleroma", key: ":keyaa1", delete: true, subkeys: [":key"]},
%{
group: ":ueberauth",
key: "Ueberauth",
@@ -748,8 +704,15 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
]
})
- assert json_response_and_validate_schema(conn, 200) == %{
- "configs" => [],
+ assert json_response_and_validate_schema(resp, 200) == %{
+ "configs" => [
+ %{
+ "db" => [":key2"],
+ "group" => ":pleroma",
+ "key" => ":keyaa1",
+ "value" => [%{"tuple" => [":key2", "value"]}]
+ }
+ ],
"need_reboot" => false
}
@@ -1023,34 +986,6 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
}
end
- test "value as map", %{conn: conn} do
- conn =
- conn
- |> put_req_header("content-type", "application/json")
- |> post("/api/pleroma/admin/config", %{
- configs: [
- %{
- "group" => ":pleroma",
- "key" => ":key1",
- "value" => %{"key" => "some_val"}
- }
- ]
- })
-
- assert json_response_and_validate_schema(conn, 200) ==
- %{
- "configs" => [
- %{
- "group" => ":pleroma",
- "key" => ":key1",
- "value" => %{"key" => "some_val"},
- "db" => [":key1"]
- }
- ],
- "need_reboot" => false
- }
- end
-
test "queues key as atom", %{conn: conn} do
conn =
conn
@@ -1228,7 +1163,6 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
assert ":proxy_url" in db
end
- @tag capture_log: true
test "doesn't set keys not in the whitelist", %{conn: conn} do
clear_config(:database_config_whitelist, [
{:pleroma, :key1},
@@ -1241,21 +1175,29 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|> put_req_header("content-type", "application/json")
|> post("/api/pleroma/admin/config", %{
configs: [
- %{group: ":pleroma", key: ":key1", value: "value1"},
- %{group: ":pleroma", key: ":key2", value: "value2"},
- %{group: ":pleroma", key: ":key3", value: "value3"},
- %{group: ":pleroma", key: "Pleroma.Web.Endpoint.NotReal", value: "value4"},
- %{group: ":pleroma", key: "Pleroma.Captcha.NotReal", value: "value5"},
- %{group: ":not_real", key: ":anything", value: "value6"}
+ %{group: ":pleroma", key: ":key1", value: [%{"tuple" => [":key", "value1"]}]},
+ %{group: ":pleroma", key: ":key2", value: [%{"tuple" => [":key", "value2"]}]},
+ %{group: ":pleroma", key: ":key3", value: [%{"tuple" => [":key", "value3"]}]},
+ %{
+ group: ":pleroma",
+ key: "Pleroma.Web.Endpoint.NotReal",
+ value: [%{"tuple" => [":key", "value4"]}]
+ },
+ %{
+ group: ":pleroma",
+ key: "Pleroma.Captcha.NotReal",
+ value: [%{"tuple" => [":key", "value5"]}]
+ },
+ %{group: ":not_real", key: ":anything", value: [%{"tuple" => [":key", "value6"]}]}
]
})
- assert Application.get_env(:pleroma, :key1) == "value1"
- assert Application.get_env(:pleroma, :key2) == "value2"
+ assert Application.get_env(:pleroma, :key1) == [key: "value1"]
+ assert Application.get_env(:pleroma, :key2) == [key: "value2"]
assert Application.get_env(:pleroma, :key3) == nil
assert Application.get_env(:pleroma, Pleroma.Web.Endpoint.NotReal) == nil
- assert Application.get_env(:pleroma, Pleroma.Captcha.NotReal) == "value5"
- assert Application.get_env(:not_real, :anything) == "value6"
+ assert Application.get_env(:pleroma, Pleroma.Captcha.NotReal) == [key: "value5"]
+ assert Application.get_env(:not_real, :anything) == [key: "value6"]
end
test "args for Pleroma.Upload.Filter.Mogrify with custom tuples", %{conn: conn} do
@@ -1410,6 +1352,76 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
"need_reboot" => false
}
end
+
+ test "value bad format error", %{conn: conn} do
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/pleroma/admin/config", %{
+ configs: [
+ %{
+ group: ":quack",
+ value: %{}
+ }
+ ]
+ })
+
+ assert json_response_and_validate_schema(conn, 400) == %{
+ "error" =>
+ "Updating config failed: :value_must_be_keyword, group: quack, key: , value: %{}"
+ }
+ end
+
+ test "error when value is list", %{conn: conn} do
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/pleroma/admin/config", %{
+ configs: [
+ %{
+ group: ":quack",
+ value: [1]
+ }
+ ]
+ })
+
+ assert json_response_and_validate_schema(conn, 400) == %{
+ "error" =>
+ "Updating config failed: :value_must_be_keyword, group: quack, key: , value: [1]"
+ }
+ end
+
+ test "saving pleroma group with value not a keyword", %{conn: conn} do
+ clear_config(Pleroma.Web.Auth.Authenticator)
+
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/pleroma/admin/config", %{
+ configs: [
+ %{
+ group: ":pleroma",
+ key: "Pleroma.Web.Auth.Authenticator",
+ value: "Pleroma.Web.Auth.LDAPAuthenticator"
+ }
+ ]
+ })
+
+ assert json_response_and_validate_schema(conn, 200) == %{
+ "configs" => [
+ %{
+ "db" => ["Pleroma.Web.Auth.Authenticator"],
+ "group" => ":pleroma",
+ "key" => "Pleroma.Web.Auth.Authenticator",
+ "value" => "Pleroma.Web.Auth.LDAPAuthenticator"
+ }
+ ],
+ "need_reboot" => false
+ }
+
+ assert Application.get_env(:pleroma, Pleroma.Web.Auth.Authenticator) ==
+ Pleroma.Web.Auth.LDAPAuthenticator
+ end
end
describe "GET /api/pleroma/admin/config/descriptions" do
@@ -1453,4 +1465,94 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
assert esshd["children"]
end
end
+
+ describe "GET /api/pleroma/admin/config/versions/rollback" do
+ setup do: clear_config(:configurable_from_database, true)
+
+ test "success rollback", %{conn: conn} do
+ version = insert(:config_version)
+ insert(:config_version)
+ insert(:config_version, current: true)
+
+ conn
+ |> get("/api/pleroma/admin/config/versions/rollback/#{version.id}")
+ |> json_response_and_validate_schema(204)
+
+ [config] = Pleroma.Repo.all(Pleroma.ConfigDB)
+ assert config.value == version.backup[config.group][config.key]
+ end
+
+ test "not found error", %{conn: conn} do
+ assert conn
+ |> get("/api/pleroma/admin/config/versions/rollback/1")
+ |> json_response_and_validate_schema(404) == %{
+ "error" => "Not found"
+ }
+ end
+
+ test "on rollback to version, which is current", %{conn: conn} do
+ version = insert(:config_version, current: true)
+
+ assert conn
+ |> get("/api/pleroma/admin/config/versions/rollback/#{version.id}")
+ |> json_response_and_validate_schema(400) == %{
+ "error" => "Rollback is not possible: :version_is_already_current"
+ }
+ end
+
+ test "when configuration from database is off", %{conn: conn} do
+ clear_config(:configurable_from_database, false)
+
+ assert conn
+ |> get("/api/pleroma/admin/config/versions/rollback/1")
+ |> json_response_and_validate_schema(400) ==
+ %{
+ "error" => "You must enable configurable_from_database in your config file."
+ }
+ end
+ end
+
+ describe "GET /api/pleroma/admin/config/versions" do
+ setup do: clear_config(:configurable_from_database, true)
+
+ test "with no versions", %{conn: conn} do
+ assert conn
+ |> get("/api/pleroma/admin/config/versions")
+ |> json_response_and_validate_schema(200) == %{"versions" => []}
+ end
+
+ test "with versions", %{conn: conn} do
+ version = insert(:config_version, current: true)
+
+ assert conn
+ |> get("/api/pleroma/admin/config/versions")
+ |> json_response_and_validate_schema(200) == %{
+ "versions" => [
+ %{
+ "current" => true,
+ "id" => version.id,
+ "inserted_at" => Pleroma.Web.CommonAPI.Utils.to_masto_date(version.inserted_at)
+ }
+ ]
+ }
+ end
+
+ test "when configuration from database is off", %{conn: conn} do
+ clear_config(:configurable_from_database, false)
+
+ assert conn
+ |> get("/api/pleroma/admin/config/versions")
+ |> json_response_and_validate_schema(400) ==
+ %{
+ "error" => "You must enable configurable_from_database in your config file."
+ }
+ end
+ end
+end
+
+# Needed for testing
+defmodule Pleroma.Web.Endpoint.NotReal do
+end
+
+defmodule Pleroma.Captcha.NotReal do
end
diff --git a/test/support/data_case.ex b/test/support/data_case.ex
index 0ee2aa4a2..d4998a79c 100644
--- a/test/support/data_case.ex
+++ b/test/support/data_case.ex
@@ -48,19 +48,11 @@ defmodule Pleroma.DataCase do
end
def clear_cachex do
- Pleroma.Supervisor
- |> Supervisor.which_children()
- |> Enum.each(fn
- {name, _, _, [Cachex]} ->
- name
- |> to_string
- |> String.trim_leading("cachex_")
- |> Kernel.<>("_cache")
- |> String.to_existing_atom()
- |> Cachex.clear()
-
- _ ->
- nil
+ Pleroma.Application.StartUpDependencies.cachex_deps()
+ |> Enum.each(fn {name, _} ->
+ "#{name}_cache"
+ |> String.to_existing_atom()
+ |> Cachex.clear()
end)
end
diff --git a/test/support/factory.ex b/test/support/factory.ex
index af4fff45b..35c2da959 100644
--- a/test/support/factory.ex
+++ b/test/support/factory.ex
@@ -458,7 +458,7 @@ defmodule Pleroma.Factory do
value:
sequence(
:value,
- &%{another_key: "#{&1}somevalue", another: "#{&1}somevalue"}
+ &[another_key: "#{&1}somevalue", another: "#{&1}somevalue"]
)
}
|> merge_attributes(attrs)
@@ -490,4 +490,11 @@ defmodule Pleroma.Factory do
context: ["home"]
}
end
+
+ def config_version_factory do
+ %Pleroma.Config.Version{
+ backup: sequence(:value, &[pleroma: [instance: [name: "Instance name #{&1}"]]]),
+ current: false
+ }
+ end
end