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.
This commit is contained in:
Alexander Strizhakov 2020-07-16 19:57:27 +03:00
parent 745375bdcf
commit 2538c741c0
No known key found for this signature in database
GPG key ID: 022896A53AEF1381
60 changed files with 4279 additions and 2204 deletions

5
.gitignore vendored
View file

@ -56,4 +56,7 @@ pleroma.iml
# Editor temp files
/*~
/*#
/*#
# local iex
.iex.exs

View file

@ -6,19 +6,38 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased
### Changed
- 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.
- HTTPSecurityPlug now sends a response header to opt out of Google's FLoC (Federated Learning of Cohorts) targeted advertising.
### 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.
- MRF (`FollowBotPolicy`): New MRF Policy which makes a designated local Bot account attempt to follow all users in public Notes received by your instance. Users who require approving follower requests or have #nobot in their profile are excluded.
- HTTPSecurityPlug now sends a response header to opt out of Google's FLoC (Federated Learning of Cohorts) targeted advertising.
- Config Versioning.
- MRF (`FollowBotPolicy`): New MRF Policy which makes a designated local Bot account attempt to follow all users in public Notes received by your instance. Users who require approving follower requests or have #nobot in their profile are excluded.
- Return OAuth token `id` (primary key) in POST `/oauth/token`.
<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>
### Fixed
- Don't crash so hard when email settings are invalid.
### 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)
### Fixed
@ -71,7 +90,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

View file

@ -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: :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: :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: :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,

View file

@ -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"

View file

@ -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:
@ -154,4 +154,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]
```

View file

@ -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`
- 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:
- 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.)

View file

@ -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.

View file

@ -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)]}
]
cachex_children = Enum.map(@cachex_children, &Pleroma.Application.build_cachex(&1, []))
children = [Pleroma.Application.StartUpDependencies.adapter_module() | children]
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

View file

@ -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
def run(["delete", group]), do: delete(group, force: false)
def run(["delete", "--force", group]), do: delete(group, force: true)
def run(["delete", group, key]), do: delete(group, key, force: false)
def run(["delete", "--force", group, key]), do: delete(group, key, force: true)
def run(["rollback" | options]) do
check_configdb(fn ->
start_pleroma()
{opts, _} = OptionParser.parse!(options, strict: [steps: :integer], aliases: [s: :steps])
do_rollback(opts)
end)
end
defp delete(group, opts) do
start_pleroma()
group = maybe_atomize(group)
configs = ConfigDB.get_all_by_group(group)
if configs != [] do
shell_info("The following settings will be removed from ConfigDB:\n")
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.")
end
end
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)
delete_key(group, key)
else
_ ->
shell_error("No settings in ConfigDB for #{inspect(group)}, #{inspect(key)}. Aborting.")
end
end
def run(["delete", "--force", group]) do
start_pleroma()
group = maybe_atomize(group)
with true <- group_exists?(group) do
shell_info("The following settings will be removed from ConfigDB:\n")
dump_group(group)
delete_group(group)
else
_ -> shell_error("No settings in ConfigDB for #{inspect(group)}. Aborting.")
end
end
def run(["delete", group, key]) do
start_pleroma()
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")
group
|> ConfigDB.get_by_group_and_key(key)
|> dump()
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
@ -296,48 +274,47 @@ defmodule Mix.Tasks.Pleroma.Config do
end
defp write_config(file, path, opts) do
IO.write(file, config_header())
IO.write(file, Pleroma.Config.Loader.config_header())
ConfigDB
|> Repo.all()
|> Enum.each(&write_and_delete(&1, file, opts[:delete]))
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
if opts[:delete] do
shell_info(msg)
change =
config
|> Map.take([:group, :key])
|> Map.put(:delete, true)
[change | acc]
else
acc
end
end)
if Keyword.get(opts, :delete, false) and changes != [] do
Pleroma.Config.Versioning.new_version(changes)
end
:ok = File.close(file)
System.cmd("mix", ["format", path])
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
defp write_and_delete(config, file, delete?) do
config
|> write(file)
|> delete(delete?)
end
defp write(config, file) do
value = inspect(config.value, limit: :infinity)
IO.write(file, "config #{inspect(config.group)}, #{inspect(config.key)}, #{value}\r\n\r\n")
config
end
defp delete(config, true) do
{:ok, _} = Repo.delete(config)
shell_info(
"config #{inspect(config.group)}, #{inspect(config.key)} was deleted from the ConfigDB."
)
end
defp delete(_config, _), do: :ok
defp dump(%ConfigDB{} = config) do
value = inspect(config.value, limit: :infinity)
@ -346,31 +323,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)
@ -387,23 +345,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

View file

@ -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,

View file

@ -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)
Pleroma.Telemetry.Logger.attach()
Config.Holder.save_default()
# compilation in runtime
Pleroma.HTML.compile_scrubbers()
Pleroma.Config.Oban.warn()
compile_custom_modules()
Pleroma.Docs.JSON.compile()
# telemetry and prometheus
Pleroma.Telemetry.Logger.attach()
setup_instrumenters()
Config.Holder.save_default()
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)
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, [])

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
@ -164,9 +167,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 =
@ -217,7 +220,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
@ -231,4 +234,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

View file

@ -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

View file

@ -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", "ConcurrentLimiter"]
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

View file

@ -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

View file

@ -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()
config =
"config/config.exs"
|> read!()
|> filter()
logger_config =
:logger
|> Application.get_all_env()
|> Enum.filter(fn {key, _} -> key in [:backends, :console, :ex_syslogger] end)
merge(config, logger: logger_config)
end
defp filter(configs) do
configs
|> Keyword.keys()
|> Enum.reduce([], &Keyword.put(&2, &1, filter_group(&1, configs)))
end
@spec filter(keyword()) :: keyword()
def filter(configs) do
Enum.reduce(configs, [], fn
{group, _settings}, group_acc when group in @reject_groups ->
group_acc
@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)
{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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,53 +107,104 @@ 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
@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
search_opts = Map.take(params, [:group, :key])
Enum.map(keys, &to_json_types(&1))
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
nil ->
create(params)
end
end
@spec merge_group(atom(), atom(), keyword(), keyword()) :: keyword()
def merge_group(group, key, old_value, new_value) do
@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)
@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) do
do_delete_or_update(config, params[:subkeys])
else
_ -> {:ok, nil}
end
end
def delete_or_update(%{group: group}) do
query = from(c in ConfigDB, where: c.group == ^group)
with {num, _} <- Repo.delete_all(query) do
{:ok, num}
end
end
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
defp do_delete_or_update(%ConfigDB{} = config, _), do: delete(config)
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)
intersect_keys = old_value |> to_mapset() |> MapSet.intersection(new_keys) |> MapSet.to_list()
merged_value = ConfigDB.merge(old_value, new_value)
merged_value = deep_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: []
|> 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)
|> List.flatten()
|> Enum.reduce(merged_value, &Keyword.put(&2, &1, new_value[&1]))
end
defp to_mapset(keyword) do
defp merge_group(_group, _key, _old_value, new_value) when is_list(new_value), do: new_value
defp merge_group(:pleroma, key, _old_value, new_value)
when key in @pleroma_not_keyword_values do
new_value
end
defp to_mapset(keyword) when is_list(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
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)
@ -138,255 +222,99 @@ defmodule Pleroma.ConfigDB do
end
end
@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])
@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
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
new_value = merge_group(config.group, config.key, config.value, params[:value])
update(config, %{value: new_value})
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
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)
remaining_defaults =
if remaining_group_defaults == [] do
Keyword.delete(defaults, group)
else
Keyword.put(defaults, group, remaining_group_defaults)
end
change = merge_change_with_default(change, default)
{[change | acc], remaining_defaults}
else
{reason, false, config} when reason in [:partial_update, :can_be_merged] ->
update(config, params)
nil ->
create(params)
{[change | acc], defaults}
end
end
defp can_be_partially_updated?(%ConfigDB{} = config), do: not only_full_update?(config)
@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
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}
]
defp reduce_to_structs({group, config}, group_acc) when group in @groups_without_keys do
[struct(%ConfigDB{}, to_map(group, config)) | group_acc]
end
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)
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
@spec delete(ConfigDB.t() | map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
def delete(%ConfigDB{} = config), do: Repo.delete(config)
@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 delete(params) do
search_opts = Map.delete(params, :subkeys)
defp reduce_to_maps({group, config}, group_acc) when group in @groups_without_keys do
[to_map(group, config) | group_acc]
end
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})
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 to_map(group, config), do: %{group: group, value: config}
defp to_map(group, key, value), do: %{group: group, key: key, value: value}
@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])
%{group: group, key: key} = change ->
merge_change_with_default(change, defaults[group][key])
end)
end
defp merge_change_with_default(change, default) do
%{change | value: merge_change_value_with_default(change, default)}
end
@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
{:partial_remove, config, []} ->
Repo.delete(config)
{config, nil} ->
Repo.delete(config)
nil ->
err =
dgettext("errors", "Config with params %{params} not found", params: inspect(params))
{:error, err}
merge_group(change.group, change.key, default, change.value)
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)
end
@spec groups_without_keys() :: [atom()]
def groups_without_keys, do: @groups_without_keys
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)
@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
@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 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
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", "ConcurrentLimiter"]
end
@spec pleroma_not_keyword_values() :: [atom()]
def pleroma_not_keyword_values, do: @pleroma_not_keyword_values
end

View file

@ -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

View file

@ -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)}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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()
defaults = Config.Holder.default_config()
changes = ConfigDB.all_with_db()
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
{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)
else
value
end
%ConfigDB{
group: group,
key: key,
value: merged_value
}
|> Pleroma.Maps.put_if_present(:db, db)
end)
end)
|> List.flatten()
render(conn, "index.json", %{
configs: merged,
need_reboot: Restarter.Pleroma.need_reboot?()
})
end
render(conn, "index.json", %{
configs: changes_merged_with_defaults,
need_reboot: Application.ConfigDependentDeps.need_reboot?()
})
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]})
result =
configs
|> Enum.filter(&whitelisted_config?/1)
|> Enum.map(&Config.Converter.to_elixir_types/1)
|> Config.Versioning.new_version()
%{group: group, key: key, value: value} ->
ConfigDB.update_or_create(%{group: group, key: key, value: value})
end)
|> Enum.reject(fn {result, _} -> result == :error end)
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
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
{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))
_, acc ->
acc
end)
Config.TransferTask.load_and_update_env(deleted, false)
Application.Environment.update(inserts_and_deletions, only_update: true)
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))
render(conn, "index.json", %{
configs: Enum.reject(inserts_and_deletions, &(Ecto.get_meta(&1, :state) == :deleted)),
need_reboot: Application.ConfigDependentDeps.need_reboot?()
})
if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot()
end
{:error, error} ->
{:error, "Updating config failed: #{inspect(error)}"}
render(conn, "index.json", %{
configs: updated,
need_reboot: Restarter.Pleroma.need_reboot?()
})
{:error, _, {error, operation}, _} ->
{:error,
"Updating config failed: #{inspect(error)}, group: #{operation[:group]}, key: #{
operation[:key]
}, value: #{inspect(operation[:value])}"}
end
end
defp configurable_from_database do
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
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

View file

@ -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
if config.db != [] do
Map.put(map, :db, config.db)
else
map
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
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

View file

@ -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()
}
}

View file

@ -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)

View file

@ -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"},

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
@ -206,7 +275,7 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
setup 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(:quack, nil, level: :info)
path = "test/instance_static"
file_path = Path.join(path, "temp.exported_from_db.secret.exs")
@ -222,7 +291,7 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
file = File.read!(file_path)
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 "release", %{file_path: file_path} do
@ -234,7 +303,7 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
file = File.read!(file_path)
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
end
@ -242,7 +311,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",
@ -272,8 +341,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"])
@ -285,8 +354,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"])
@ -302,7 +371,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"])
@ -317,8 +386,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
@ -347,4 +416,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

View file

@ -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

View file

@ -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

View file

@ -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 "warns 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 capture_log(fn ->
assert Pleroma.ApplicationRequirements.verify!() == :ok
assert Requirements.verify!() == :ok
end) =~ "Welcome emails will NOT be sent"
end
end
describe "check_confirmation_accounts!" do
setup_with_mocks([
{Pleroma.ApplicationRequirements, [:passthrough],
{Requirements, [:passthrough],
[
check_migrations_applied!: fn _ -> :ok end
]}
@ -59,30 +59,29 @@ defmodule Pleroma.ApplicationRequirementsTest do
test "warns if account confirmation is required but mailer isn't enabled" do
clear_config([:instance, :account_activation_required], true)
clear_config([Pleroma.Emails.Mailer, :enabled], false)
clear_config([Mailer, :enabled], false)
assert capture_log(fn ->
assert Pleroma.ApplicationRequirements.verify!() == :ok
assert Requirements.verify!() == :ok
end) =~ "Users will NOT be able to confirm their accounts"
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
@ -93,10 +92,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
@ -105,10 +104,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
@ -117,7 +116,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
@ -125,12 +124,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, [],
[
@ -150,17 +149,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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
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
describe "to_elixir_types/1" do
test "string" do
assert ConfigDB.to_elixir_types("value as string") == "value as string"
test "all/0" do
config = insert(:config)
assert [^config] = ConfigDB.all()
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]
]
config1 = insert(:config, key: :key1, value: [k1: 4, k2: 4])
config2 = insert(:config, key: :key2, value: [k1: 5, k2: 5])
{changes, [logger: [k1: 3, k2: 3]]} =
ConfigDB.reduce_defaults_and_merge_with_changes([config1, config2], defaults)
Enum.each(changes, fn
%{key: :key1, value: value} ->
assert value == [k3: 1, k1: 4, k2: 4]
%{key: :key2, value: value} ->
assert value == [k3: 2, k1: 5, k2: 5]
end)
end
test "boolean" do
assert ConfigDB.to_elixir_types(false) == false
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 "nil" do
assert ConfigDB.to_elixir_types(nil) == nil
end
config = insert(:config, group: :cors_plug, key: nil, value: [max_age: 60_000])
test "integer" do
assert ConfigDB.to_elixir_types(150) == 150
end
{[change], [pleroma: [key1: [k1: 1, k2: 1, k3: 1]]]} =
ConfigDB.reduce_defaults_and_merge_with_changes([config], defaults)
test "atom" do
assert ConfigDB.to_elixir_types(":atom") == :atom
end
test "ssl options" do
assert ConfigDB.to_elixir_types([":tlsv1", ":tlsv1.1", ":tlsv1.2"]) == [
:tlsv1,
:"tlsv1.1",
:"tlsv1.2"
assert change.value == [
methods: ["POST", "PUT", "DELETE", "GET", "PATCH", "OPTIONS"],
max_age: 60_000
]
end
test "pleroma module" do
assert ConfigDB.to_elixir_types("Pleroma.Bookmark") == Pleroma.Bookmark
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]
]
logger =
insert(:config,
group: :logger,
key: nil,
value: [ex_syslogger: [k1: 3, k2: 4], backends: [:console]]
)
{[change], [pleroma: [key1: 1, key2: 2]]} =
ConfigDB.reduce_defaults_and_merge_with_changes([logger], defaults)
assert change.value == [
console: [k1: 2, k2: 2],
key: 1,
ex_syslogger: [k1: 3, k2: 4],
backends: [:console]
]
end
test "pleroma string" do
assert ConfigDB.to_elixir_types("Pleroma") == "Pleroma"
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 "phoenix module" do
assert ConfigDB.to_elixir_types("Phoenix.Socket.V1.JSONSerializer") ==
Phoenix.Socket.V1.JSONSerializer
end
logger =
insert(:config,
group: :logger,
key: nil,
value: [console: [k1: 4, k2: 4], k1: 3, k2: 4, backends: [:console]]
)
test "tesla module" do
assert ConfigDB.to_elixir_types("Tesla.Adapter.Hackney") == Tesla.Adapter.Hackney
end
{[change], [pleroma: [key1: 1, key2: 2]]} =
ConfigDB.reduce_defaults_and_merge_with_changes([logger], defaults)
test "ExSyslogger module" do
assert ConfigDB.to_elixir_types("ExSyslogger") == ExSyslogger
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 "Quack.Logger module" do
assert ConfigDB.to_elixir_types("Quack.Logger") == Quack.Logger
end
test "all_with_db/0" do
config = insert(:config)
[change] = ConfigDB.all_with_db()
assert change.db == Keyword.keys(config.value)
end
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
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]]
]
test "sigil" do
assert ConfigDB.to_elixir_types("~r[comp[lL][aA][iI][nN]er]") == ~r/comp[lL][aA][iI][nN]er/
end
changes = ConfigDB.from_keyword_to_structs(keyword)
test "link sigil" do
assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/") == ~r/https:\/\/example.com/
end
Enum.each(changes, fn
%{key: :key1} = change ->
assert change.group == :pleroma
assert change.value == [k1: 1, k2: 1, k3: 1]
test "link sigil with um modifiers" do
assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/um") ==
~r/https:\/\/example.com/um
end
%{key: :key2} = change ->
assert change.group == :pleroma
assert change.value == [k1: 2, k2: 2, k3: 2]
test "link sigil with i modifier" do
assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/i") == ~r/https:\/\/example.com/i
end
%{key: nil} = change ->
assert change.group == :logger
test "link sigil with s modifier" do
assert ConfigDB.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 ->
ConfigDB.to_elixir_types("~r/https://[]{}<>\"'()|example.com/s")
end
end
test "2 child tuple" do
assert ConfigDB.to_elixir_types(%{"tuple" => ["v1", ":v2"]}) == {"v1", :v2}
end
test "proxy tuple with localhost" do
assert ConfigDB.to_elixir_types(%{
"tuple" => [":proxy_url", %{"tuple" => [":socks5", "localhost", 1234]}]
}) == {:proxy_url, {:socks5, :localhost, 1234}}
end
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}}
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 "tuple with n childs" do
assert ConfigDB.to_elixir_types(%{
"tuple" => [
"v1",
":v2",
"Pleroma.Bookmark",
150,
false,
"Phoenix.Socket.V1.JSONSerializer"
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
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]
]
config1 = insert(:config, key: :key1, value: [k1: 4, k2: 4])
config2 = insert(:config, key: :key2, value: [k1: 5, k2: 5])
changes = ConfigDB.merge_changes_with_defaults([config1, config2], defaults)
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 "map with string key" do
assert ConfigDB.to_elixir_types(%{"key" => "value"}) == %{"key" => "value"}
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 "map with atom key" do
assert ConfigDB.to_elixir_types(%{":key" => "value"}) == %{key: "value"}
end
config =
insert(:config,
group: :pleroma,
key: :assets,
value: [mascots: [1, 2], subkey: [key1: [key: :val1, key2: :val1], key2: :val1]]
)
test "list of strings" do
assert ConfigDB.to_elixir_types(["v1", "v2", "v3"]) == ["v1", "v2", "v3"]
end
[merged] = ConfigDB.merge_changes_with_defaults([config], defaults)
test "list of modules" do
assert ConfigDB.to_elixir_types(["Pleroma.Repo", "Pleroma.Activity"]) == [
Pleroma.Repo,
Pleroma.Activity
assert merged.value == [
mascots: [1, 2],
key: 5,
subkey: [key1: [key: :val1, key2: :val1], key2: :val1]
]
end
test "list of atoms" do
assert ConfigDB.to_elixir_types([":v1", ":v2", ":v3"]) == [:v1, :v2, :v3]
test "merge for other subkeys" do
defaults = [pleroma: [assets: [key: 5]]]
config =
insert(:config,
group: :pleroma,
key: :assets,
value: [subkey: 3, default_mascot: :test_mascot]
)
[merged] = ConfigDB.merge_changes_with_defaults([config], defaults)
assert merged.value == [key: 5, subkey: 3, default_mascot: :test_mascot]
end
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
test "with change deletion" do
defaults = [pleroma: [assets: [key: 5]]]
test "simple keyword" do
assert ConfigDB.to_elixir_types([%{"tuple" => [":key", "value"]}]) == [key: "value"]
end
config =
insert(:config,
group: :pleroma,
key: :assets,
value: [subkey: 3, default_mascot: :test_mascot]
)
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"
]
end
test "trandformed keyword" do
assert ConfigDB.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 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
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"
]
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 "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, []}}
]}
]
]
]
{:ok, config} = ConfigDB.delete(config)
[merged] = ConfigDB.merge_changes_with_defaults([config], defaults)
assert merged.value == [key: 5]
end
end
end

View file

@ -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

View file

@ -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
assert length(configs) == 3
db_keys =
config3.value
|> Keyword.keys()
|> ConfigDB.to_json_types()
keys = Enum.map(saved_configs -- [config3], &inspect(&1.key))
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}
]
assert Application.get_env(:pleroma, :key4) == %{
"endpoint" => "https://example.com",
nested_5: :upload
}
assert Application.get_env(:pleroma, :key3) == [
key: :nested_3,
nested_33: "nested_33",
key: true
]
assert Application.get_env(:idna, :key5) == {"string", Pleroma.Captcha.NotReal, []}
assert Application.get_env(:pleroma, :key4) == [
nested_5: :upload,
endpoint: "https://example.com"
]
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)
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"])
insert(:config,
group: :logger,
key: :backends,
value: []
)
Pleroma.Config.TransferTask.load_and_update_env([], false)
assert Application.get_env(:logger, :backends) == []
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" => [
%{
"group" => ":pleroma",
"key" => ":keyaa1",
"value" => "another_value",
"db" => [":keyaa1"]
%{"tuple" => [":another_key", "somevalue"]},
%{"tuple" => [":another", "somevalue"]}
]
},
%{
"group" => ":pleroma",
"key" => ":keyaa2",
"value" => "another_value",
"db" => [":keyaa2"]
"value" => [
%{"tuple" => [":key", "value2"]}
],
"db" => [":key"]
},
%{
"group" => ":pleroma",
"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
@ -1445,14 +1387,83 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
"need_reboot" => false
}
_res =
assert conn
|> get("/api/v1/instance")
|> json_response_and_validate_schema(200)
assert conn
|> get("/api/v1/instance")
|> json_response_and_validate_schema(200)
assert res = %{"thumbnail" => "https://example.com/media/new_thumbnail.jpg"}
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
test "Concurrent Limiter", %{conn: conn} do
clear_config([ConcurrentLimiter])
@ -1529,4 +1540,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

View file

@ -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

View file

@ -504,7 +504,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)
@ -536,4 +536,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