Merge branch 'feature/restart-pleroma-from-outside-application' into 'develop'

Restarting pleroma from outside application

See merge request pleroma/pleroma!2144
This commit is contained in:
rinpatch 2020-02-05 16:59:21 +00:00
commit 49e80a1537
13 changed files with 318 additions and 42 deletions

View File

@ -665,6 +665,19 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- 404 Not Found `"Not found"`
- On success: 200 OK `{}`
## `GET /api/pleroma/admin/restart`
### Restarts pleroma application
- Params: none
- Response:
- On failure:
- 400 Bad Request `"To use this endpoint you need to enable configuration from database."`
```json
{}
```
## `GET /api/pleroma/admin/config/migrate_from_db`
### Run mix task pleroma.config migrate_from_db

View File

@ -6,11 +6,7 @@ config :pleroma, configurable_from_database: true
```
## How it works
Settings are stored in database and are applied in `runtime` after each change. Most of the settings take effect immediately, except some, which need instance reboot. These settings are needed in `compile time`, that's why settings are duplicated to the file.
File with duplicated settings is located in `config/{env}.exported_from_db.exs` if pleroma is runned from source. For prod env it will be `config/prod.exported_from_db.exs`.
For releases: `/etc/pleroma/prod.exported_from_db.secret.exs` or `PLEROMA_CONFIG_PATH/prod.exported_from_db.exs`.
Settings are stored in database and are applied in `runtime` after each change. Most of the settings take effect immediately, except some, which need instance reboot.
## How to set it up
You need to migrate your existing settings to the database. This task will migrate only added by user settings.
@ -25,7 +21,7 @@ You can do this with mix task (all config files will remain untouched):
mix pleroma.config migrate_to_db
```
Now you can change settings in admin interface. After each save, settings from database are duplicated to the `config/{env}.exported_from_db.exs` file.
Now you can change settings in admin interface. If `reboot time` settings were changed, pleroma must be rebooted.
<span style="color:red">**ATTENTION**</span>
@ -35,10 +31,19 @@ Now you can change settings in admin interface. After each save, settings from d
- all settings inside these keys:
- `:hackney_pools`
- `:chat`
- `Oban`
- `:rate_limit`
- `:markup`
- `:streamer`
- partially settings inside these keys:
- `:seconds_valid` in `Pleroma.Captcha`
- `:proxy_remote` in `Pleroma.Upload`
- `:upload_limit` in `:instance`
- `:digest` in `:email_notifications`
- `:clean_expired_tokens` in `:oauth2`
- `:enabled` in `Pleroma.ActivityExpiration`
- `:enabled` in `Pleroma.ScheduledActivity`
- `:enabled` in `:gopher`
## How to dump settings from database to file
@ -59,7 +64,7 @@ mix pleroma.config migrate_from_db [-d]
```sql
TRUNCATE TABLE config;
```
2. Delete `config/{env}.exported_from_db.exs`.
2. If migrate_from_db task was runned, backup and delete `config/{env}.exported_from_db.exs`.
For `prod` env:
```bash

View File

@ -3,8 +3,6 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config.Loader do
@paths ["config/config.exs", "config/#{Mix.env()}.exs"]
@reject_keys [
Pleroma.Repo,
Pleroma.Web.Endpoint,
@ -35,8 +33,8 @@ defp do_merge(conf1, conf2), do: Mix.Config.merge(conf1, conf2)
def load_and_merge do
all_paths =
if Pleroma.Config.get(:release),
do: @paths ++ ["config/releases.exs"],
else: @paths
do: ["config/config.exs", "config/releases.exs"],
else: ["config/config.exs"]
all_paths
|> Enum.map(&load(&1))

View File

@ -10,6 +10,30 @@ defmodule Pleroma.Config.TransferTask do
require Logger
@type env() :: :test | :benchmark | :dev | :prod
@reboot_time_keys [
{:pleroma, :hackney_pools},
{:pleroma, :chat},
{:pleroma, Oban},
{:pleroma, :rate_limit},
{:pleroma, :markup},
{:plerome, :streamer}
]
@reboot_time_subkeys [
{:pleroma, Pleroma.Captcha, [:seconds_valid]},
{:pleroma, Pleroma.Upload, [:proxy_remote]},
{:pleroma, :instance, [:upload_limit]},
{:pleroma, :email_notifications, [:digest]},
{:pleroma, :oauth2, [:clean_expired_tokens]},
{:pleroma, Pleroma.ActivityExpiration, [:enabled]},
{:pleroma, Pleroma.ScheduledActivity, [:enabled]},
{:pleroma, :gopher, [:enabled]}
]
@reject [nil, :prometheus]
def start_link(_) do
load_and_update_env()
if Pleroma.Config.get(:env) == :test, do: Ecto.Adapters.SQL.Sandbox.checkin(Repo)
@ -17,21 +41,34 @@ def start_link(_) do
end
@spec load_and_update_env([ConfigDB.t()]) :: :ok | false
def load_and_update_env(deleted \\ []) do
def load_and_update_env(deleted \\ [], restart_pleroma? \\ true) do
with true <- Pleroma.Config.get(:configurable_from_database),
true <- Ecto.Adapters.SQL.table_exists?(Repo, "config"),
started_applications <- Application.started_applications() do
# We need to restart applications for loaded settings take effect
in_db = Repo.all(ConfigDB)
with_deleted = in_db ++ deleted
reject_for_restart = if restart_pleroma?, do: @reject, else: [:pleroma | @reject]
applications =
with_deleted
|> Enum.map(&merge_and_update(&1))
|> Enum.uniq()
# TODO: some problem with prometheus after restart!
|> Enum.reject(&(&1 in [:pleroma, nil, :prometheus]))
|> Enum.each(&restart(started_applications, &1))
|> Enum.reject(&(&1 in reject_for_restart))
# to be ensured that pleroma will be restarted last
applications =
if :pleroma in applications do
List.delete(applications, :pleroma) ++ [:pleroma]
else
applications
end
Enum.each(applications, &restart(started_applications, &1, Pleroma.Config.get(:env)))
:ok
end
@ -43,12 +80,25 @@ defp merge_and_update(setting) do
group = ConfigDB.from_string(setting.group)
default = Pleroma.Config.Holder.config(group, key)
merged_value = merge_value(setting, default, group, key)
value = ConfigDB.from_binary(setting.value)
merged_value =
if Ecto.get_meta(setting, :state) == :deleted do
default
else
if can_be_merged?(default, value) do
ConfigDB.merge_group(group, key, default, value)
else
value
end
end
:ok = update_env(group, key, merged_value)
if group != :logger do
if group != :pleroma or pleroma_need_restart?(group, key, value) do
group
end
else
# change logger configuration in runtime, without restart
if Keyword.keyword?(merged_value) and
@ -76,22 +126,31 @@ defp merge_and_update(setting) do
end
end
defp merge_value(%{__meta__: %{state: :deleted}}, default, _group, _key), do: default
defp merge_value(setting, default, group, key) do
value = ConfigDB.from_binary(setting.value)
if can_be_merged?(default, value) do
ConfigDB.merge_group(group, key, default, value)
else
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 update_env(group, key, nil), do: Application.delete_env(group, key)
defp update_env(group, key, value), do: Application.put_env(group, key, value)
defp restart(started_applications, app) do
defp restart(_, :pleroma, :test), do: Logger.warn("pleroma restarted")
defp restart(_, :pleroma, _), do: send(Restarter.Pleroma, :after_boot)
defp restart(started_applications, app, _) do
with {^app, _, _} <- List.keyfind(started_applications, app, 0),
:ok <- Application.stop(app) do
:ok = Application.start(app)

View File

@ -890,17 +890,36 @@ def config_update(conn, %{"configs" => configs}) do
Ecto.get_meta(config, :state) == :deleted
end)
Pleroma.Config.TransferTask.load_and_update_env(deleted)
Pleroma.Config.TransferTask.load_and_update_env(deleted, false)
Mix.Tasks.Pleroma.Config.run([
"migrate_from_db",
"--env",
to_string(Pleroma.Config.get(:env))
])
need_reboot? =
Enum.any?(updated, fn config ->
group = ConfigDB.from_string(config.group)
key = ConfigDB.from_string(config.key)
value = ConfigDB.from_binary(config.value)
Pleroma.Config.TransferTask.pleroma_need_restart?(group, key, value)
end)
response = %{configs: updated}
response =
if need_reboot?, do: Map.put(response, :need_reboot, need_reboot?), else: response
conn
|> put_view(ConfigView)
|> render("index.json", %{configs: updated})
|> render("index.json", response)
end
end
def restart(conn, _params) do
with :ok <- configurable_from_database(conn) do
if Pleroma.Config.get(:env) == :test do
Logger.warn("pleroma restarted")
else
send(Restarter.Pleroma, {:restart, 50})
end
json(conn, %{})
end
end

View File

@ -5,10 +5,16 @@
defmodule Pleroma.Web.AdminAPI.ConfigView do
use Pleroma.Web, :view
def render("index.json", %{configs: configs}) do
%{
def render("index.json", %{configs: configs} = params) do
map = %{
configs: render_many(configs, __MODULE__, "show.json", as: :config)
}
if params[:need_reboot] do
Map.put(map, :need_reboot, true)
else
map
end
end
def render("show.json", %{config: config}) do

View File

@ -197,6 +197,7 @@ defmodule Pleroma.Web.Router do
post("/config", AdminAPIController, :config_update)
get("/config/descriptions", AdminAPIController, :config_descriptions)
get("/config/migrate_from_db", AdminAPIController, :migrate_from_db)
get("/restart", AdminAPIController, :restart)
get("/moderation_log", AdminAPIController, :list_log)

10
mix.exs
View File

@ -8,7 +8,7 @@ def project do
elixir: "~> 1.8",
elixirc_paths: elixirc_paths(Mix.env()),
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
elixirc_options: [warnings_as_errors: true],
elixirc_options: [warnings_as_errors: warnings_as_errors(Mix.env())],
xref: [exclude: [:eldap]],
start_permanent: Mix.env() == :prod,
aliases: aliases(),
@ -73,6 +73,11 @@ defp elixirc_paths(:benchmark), do: ["lib", "benchmarks"]
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
defp warnings_as_errors(:prod), do: false
# Uncomment this if you need testing configurable_from_database logic
# defp warnings_as_errors(:dev), do: false
defp warnings_as_errors(_), do: true
# Specifies OAuth dependencies.
defp oauth_deps do
oauth_strategy_packages =
@ -166,7 +171,8 @@ defp deps do
{:captcha,
git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git",
ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"},
{:mox, "~> 0.5", only: :test}
{:mox, "~> 0.5", only: :test},
{:restarter, path: "./restarter"}
] ++ oauth_deps()
end

28
restarter/lib/pleroma.ex Normal file
View File

@ -0,0 +1,28 @@
defmodule Restarter.Pleroma do
use GenServer
def start_link(_) do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init(_), do: {:ok, %{}}
def handle_info(:after_boot, %{after_boot: true} = state), do: {:noreply, state}
def handle_info(:after_boot, state) do
restart(:pleroma)
{:noreply, Map.put(state, :after_boot, true)}
end
def handle_info({:restart, delay}, state) do
Process.sleep(delay)
restart(:pleroma)
{:noreply, state}
end
defp restart(app) do
:ok = Application.ensure_started(app)
:ok = Application.stop(app)
:ok = Application.start(app)
end
end

View File

@ -0,0 +1,8 @@
defmodule Restarter do
use Application
def start(_, _) do
opts = [strategy: :one_for_one, name: Restarter.Supervisor]
Supervisor.start_link([Restarter.Pleroma], opts)
end
end

21
restarter/mix.exs Normal file
View File

@ -0,0 +1,21 @@
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

@ -5,6 +5,8 @@
defmodule Pleroma.Config.TransferTaskTest do
use Pleroma.DataCase
import ExUnit.CaptureLog
alias Pleroma.Config.TransferTask
alias Pleroma.ConfigDB
@ -105,4 +107,75 @@ test "transfer config values with full subkey update" do
Application.put_env(:pleroma, :assets, assets)
end)
end
describe "pleroma restart" do
test "don't restart if no reboot time settings were changed" do
emoji = Application.get_env(:pleroma, :emoji)
on_exit(fn -> Application.put_env(:pleroma, :emoji, emoji) end)
ConfigDB.create(%{
group: ":pleroma",
key: ":emoji",
value: [groups: [a: 1, b: 2]]
})
refute String.contains?(
capture_log(fn -> TransferTask.start_link([]) end),
"pleroma restarted"
)
end
test "restart pleroma on reboot time key" do
chat = Application.get_env(:pleroma, :chat)
on_exit(fn -> Application.put_env(:pleroma, :chat, chat) end)
ConfigDB.create(%{
group: ":pleroma",
key: ":chat",
value: [enabled: false]
})
assert capture_log(fn -> TransferTask.start_link([]) end) =~ "pleroma restarted"
end
test "restart pleroma on reboot time subkey" do
captcha = Application.get_env(:pleroma, Pleroma.Captcha)
on_exit(fn -> Application.put_env(:pleroma, Pleroma.Captcha, captcha) end)
ConfigDB.create(%{
group: ":pleroma",
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
chat = Application.get_env(:pleroma, :chat)
captcha = Application.get_env(:pleroma, Pleroma.Captcha)
on_exit(fn ->
Application.put_env(:pleroma, :chat, chat)
Application.put_env(:pleroma, Pleroma.Captcha, captcha)
end)
ConfigDB.create(%{
group: ":pleroma",
key: ":chat",
value: [enabled: false]
})
ConfigDB.create(%{
group: ":pleroma",
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

@ -2043,7 +2043,6 @@ test "POST /api/pleroma/admin/config error", %{conn: conn} do
Application.delete_env(:pleroma, Pleroma.Captcha.NotReal)
Application.put_env(:pleroma, :http, http)
Application.put_env(:tesla, :adapter, Tesla.Mock)
:ok = File.rm("config/test.exported_from_db.secret.exs")
end)
end
@ -2170,7 +2169,7 @@ test "create new config setting in db", %{conn: conn} do
assert Application.get_env(:idna, :key5) == {"string", Pleroma.Captcha.NotReal, []}
end
test "save config setting without key", %{conn: conn} do
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)
@ -2256,6 +2255,34 @@ test "saving config with partial update", %{conn: conn} do
}
end
test "saving config which need pleroma reboot", %{conn: conn} do
chat = Pleroma.Config.get(:chat)
on_exit(fn -> Pleroma.Config.put(:chat, chat) end)
conn =
post(
conn,
"/api/pleroma/admin/config",
%{
configs: [
%{group: ":pleroma", key: ":chat", value: [%{"tuple" => [":enabled", true]}]}
]
}
)
assert json_response(conn, 200) == %{
"configs" => [
%{
"db" => [":enabled"],
"group" => ":pleroma",
"key" => ":chat",
"value" => [%{"tuple" => [":enabled", true]}]
}
],
"need_reboot" => true
}
end
test "saving config with nested merge", %{conn: conn} do
config =
insert(:config, key: ":key1", value: :erlang.term_to_binary(key1: 1, key2: [k1: 1, k2: 2]))
@ -3001,6 +3028,18 @@ test "returns error if configuration from database is off", %{conn: conn} do
end
end
describe "GET /api/pleroma/admin/restart" do
clear_config(:configurable_from_database) do
Pleroma.Config.put(:configurable_from_database, true)
end
test "pleroma restarts", %{conn: conn} do
ExUnit.CaptureLog.capture_log(fn ->
assert conn |> get("/api/pleroma/admin/restart") |> json_response(200) == %{}
end) =~ "pleroma restarted"
end
end
describe "GET /api/pleroma/admin/users/:nickname/statuses" do
setup do
user = insert(:user)