diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | config/config.exs | 2 | ||||
-rw-r--r-- | docs/administration/CLI_tasks/frontend.md | 21 | ||||
-rw-r--r-- | lib/mix/tasks/pleroma/frontend.ex | 80 | ||||
-rw-r--r-- | lib/pleroma/frontend.ex | 143 | ||||
-rw-r--r-- | lib/pleroma/web/admin_api/controllers/frontend_controller.ex | 10 | ||||
-rw-r--r-- | test/mix/tasks/pleroma/frontend_test.exs | 50 | ||||
-rw-r--r-- | test/pleroma/frontend_test.exs | 132 |
8 files changed, 375 insertions, 64 deletions
diff --git a/.gitignore b/.gitignore index da73b6f36..849f3955f 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ /test/fixtures/test_tmp.txt /test/fixtures/image_tmp.jpg /test/tmp/ +/test/frontend_static_test/ /doc /instance /priv/ssh_keys diff --git a/config/config.exs b/config/config.exs index 2f8a18788..cda5d1c6f 100644 --- a/config/config.exs +++ b/config/config.exs @@ -752,7 +752,7 @@ config :pleroma, :frontends, "git" => "https://gitlab.com/soapbox-pub/soapbox-fe", "build_url" => "https://gitlab.com/soapbox-pub/soapbox-fe/-/jobs/artifacts/${ref}/download?job=build-production", - "ref" => "v1.0.0", + "ref" => "develop", "build_dir" => "static" } } diff --git a/docs/administration/CLI_tasks/frontend.md b/docs/administration/CLI_tasks/frontend.md index d4a48cb56..6a330975d 100644 --- a/docs/administration/CLI_tasks/frontend.md +++ b/docs/administration/CLI_tasks/frontend.md @@ -3,13 +3,15 @@ === "OTP" ```sh - ./bin/pleroma_ctl frontend install <frontend> [--ref <ref>] [--file <file>] [--build-url <build-url>] [--path <path>] [--build-dir <build-dir>] + ./bin/pleroma_ctl frontend install <frontend> [--ref <ref>] [--file <file>] [--build-url <build-url>] [--path <path>] [--build-dir <build-dir>] [--primary] [--admin] + ./bin/pleroma_ctl frontend enable <frontend> [--ref <ref>] [--file <file>] [--build-url <build-url>] [--path <path>] [--build-dir <build-dir>] [--primary] [--admin] ``` === "From Source" ```sh - mix pleroma.frontend install <frontend> [--ref <ref>] [--file <file>] [--build-url <build-url>] [--path <path>] [--build-dir <build-dir>] + mix pleroma.frontend install <frontend> [--ref <ref>] [--file <file>] [--build-url <build-url>] [--path <path>] [--build-dir <build-dir>] [--primary] [--admin] + mix pleroma.frontend enable <frontend> [--ref <ref>] [--file <file>] [--build-url <build-url>] [--path <path>] [--build-dir <build-dir>] [--primary] [--admin] ``` Frontend can be installed either from local zip file, or automatically downloaded from the web. @@ -94,3 +96,18 @@ The installation process is the same, but you will have to give all the needed o If you don't have a zip file but just want to install a frontend from a local path, you can simply copy the files over a folder of this template: `${instance_static}/frontends/${name}/${ref}`. +## Enabling a frontend + +Once installed, a frontend can be enabled with the `enable` command: + +=== "OTP" + + ```sh + ./bin/pleroma_ctl frontend enable gensokyo --primary + ``` + +=== "From Source" + + ```sh + mix pleroma.frontend enable gensokyo --primary + ``` diff --git a/lib/mix/tasks/pleroma/frontend.ex b/lib/mix/tasks/pleroma/frontend.ex index 8334e0049..229ef10bd 100644 --- a/lib/mix/tasks/pleroma/frontend.ex +++ b/lib/mix/tasks/pleroma/frontend.ex @@ -7,6 +7,8 @@ defmodule Mix.Tasks.Pleroma.Frontend do import Mix.Pleroma + alias Pleroma.Frontend + @shortdoc "Manages bundled Pleroma frontends" @moduledoc File.read!("docs/administration/CLI_tasks/frontend.md") @@ -16,7 +18,42 @@ defmodule Mix.Tasks.Pleroma.Frontend do "none" end - def run(["install", frontend | args]) do + def run(["install", name | args]) do + start_pleroma() + + {options, [], []} = + OptionParser.parse( + args, + strict: [ + ref: :string, + build_url: :string, + build_dir: :string, + file: :string, + admin: :boolean, + primary: :boolean + ] + ) + + shell_info("Installing frontend #{name}...") + + with %Frontend{} = fe <- + options + |> Keyword.put(:name, name) + |> opts_to_frontend() + |> Frontend.install() do + shell_info("Frontend #{fe.name} installed") + + if get_frontend_type(options) do + run(["enable", name] ++ args) + end + else + error -> + shell_error("Failed to install frontend") + exit(inspect(error)) + end + end + + def run(["enable", name | args]) do start_pleroma() {options, [], []} = @@ -24,13 +61,48 @@ defmodule Mix.Tasks.Pleroma.Frontend do args, strict: [ ref: :string, - static_dir: :string, build_url: :string, build_dir: :string, - file: :string + file: :string, + admin: :boolean, + primary: :boolean ] ) - Pleroma.Frontend.install(frontend, options) + frontend_type = get_frontend_type(options) || :primary + + shell_info("Enabling frontend #{name}...") + + with %Frontend{} = fe <- + options + |> Keyword.put(:name, name) + |> opts_to_frontend() + |> Frontend.enable(frontend_type) do + shell_info("Frontend #{fe.name} enabled") + else + error -> + shell_error("Failed to enable frontend") + exit(inspect(error)) + end + end + + defp opts_to_frontend(opts) do + struct(Frontend, opts) + end + + defp get_frontend_type(opts) do + case Enum.into(opts, %{}) do + %{admin: true, primary: true} -> + raise "Invalid command. Only one frontend type may be selected." + + %{admin: true} -> + :admin + + %{primary: true} -> + :primary + + _ -> + nil + end end end diff --git a/lib/pleroma/frontend.ex b/lib/pleroma/frontend.ex index 34b7befb8..18bb6c488 100644 --- a/lib/pleroma/frontend.ex +++ b/lib/pleroma/frontend.ex @@ -4,41 +4,45 @@ defmodule Pleroma.Frontend do alias Pleroma.Config + alias Pleroma.ConfigDB + alias Pleroma.Frontend require Logger - def install(name, opts \\ []) do - frontend_info = %{ - "ref" => opts[:ref], - "build_url" => opts[:build_url], - "build_dir" => opts[:build_dir] - } - - frontend_info = - [:frontends, :available, name] - |> Config.get(%{}) - |> Map.merge(frontend_info, fn _key, config, cmd -> - # This only overrides things that are actually set - cmd || config - end) + @unknown_name "unknown" + @frontend_types [:admin, :primary] - ref = frontend_info["ref"] + defstruct [:name, :ref, :git, :build_url, :build_dir, :file, :"custom-http-headers"] - unless ref do - raise "No ref given or configured" - end + def install(%Frontend{} = frontend) do + frontend + |> maybe_put_name() + |> hydrate() + |> validate!() + |> do_install() + end + + defp maybe_put_name(%{name: nil} = fe), do: Map.put(fe, :name, @unknown_name) + defp maybe_put_name(fe), do: fe + # Merges a named frontend with the provided one + defp hydrate(%Frontend{name: name} = frontend) do + get_named_frontend(name) + |> merge(frontend) + end + + defp do_install(%Frontend{ref: ref, name: name} = frontend) do dest = Path.join([dir(), name, ref]) label = "#{name} (#{ref})" tmp_dir = Path.join(dir(), "tmp") - with {_, :ok} <- - {:download_or_unzip, download_or_unzip(frontend_info, tmp_dir, opts[:file])}, + with {_, :ok} <- {:download_or_unzip, download_or_unzip(frontend, tmp_dir)}, Logger.info("Installing #{label} to #{dest}"), - :ok <- install_frontend(frontend_info, tmp_dir, dest) do + :ok <- install_frontend(frontend, tmp_dir, dest) do File.rm_rf!(tmp_dir) Logger.info("Frontend #{label} installed to #{dest}") + frontend else {:download_or_unzip, _} -> Logger.info("Could not download or unzip the frontend") @@ -50,21 +54,53 @@ defmodule Pleroma.Frontend do end end - def dir(opts \\ []) do - if is_nil(opts[:static_dir]) do - Pleroma.Config.get!([:instance, :static_dir]) + def enable(%Frontend{} = frontend, frontend_type) when frontend_type in @frontend_types do + with {:config_db, true} <- {:config_db, Config.get(:configurable_from_database)} do + frontend + |> maybe_put_name() + |> hydrate() + |> validate!() + |> do_enable(frontend_type) else - opts[:static_dir] + {:config_db, _} -> + map = to_map(frontend) + + raise """ + Can't enable frontend; database configuration is disabled. + Enable the frontend by manually adding this line to your config: + + config :pleroma, :frontends, #{to_string(frontend_type)}: #{inspect(map)} + + Alternatively, enable database configuration: + + config :pleroma, configurable_from_database: true + """ + end + end + + def do_enable(%Frontend{name: name} = frontend, frontend_type) do + value = Keyword.put([], frontend_type, to_map(frontend)) + params = %{group: :pleroma, key: :frontends, value: value} + + with {:ok, _} <- ConfigDB.update_or_create(params), + :ok <- Config.TransferTask.load_and_update_env([], false) do + Logger.info("Frontend #{name} successfully enabled") + frontend end + end + + def dir do + Config.get!([:instance, :static_dir]) |> Path.join("frontends") end - defp download_or_unzip(frontend_info, temp_dir, nil), - do: download_build(frontend_info, temp_dir) + defp download_or_unzip(%Frontend{build_url: build_url} = frontend, dest) + when is_binary(build_url), + do: download_build(frontend, dest) - defp download_or_unzip(_frontend_info, temp_dir, file) do + defp download_or_unzip(%Frontend{file: file}, dest) when is_binary(file) do with {:ok, zip} <- File.read(Path.expand(file)) do - unzip(zip, temp_dir) + unzip(zip, dest) end end @@ -87,9 +123,13 @@ defmodule Pleroma.Frontend do end end - defp download_build(frontend_info, dest) do - Logger.info("Downloading pre-built bundle for #{frontend_info["name"]}") - url = String.replace(frontend_info["build_url"], "${ref}", frontend_info["ref"]) + def parse_build_url(%Frontend{ref: ref, build_url: build_url}) do + String.replace(build_url, "${ref}", ref) + end + + defp download_build(%Frontend{name: name} = frontend, dest) do + Logger.info("Downloading pre-built bundle for #{name}") + url = parse_build_url(frontend) with {:ok, %{status: 200, body: zip_body}} <- Pleroma.HTTP.get(url, [], pool: :media, recv_timeout: 120_000) do @@ -100,11 +140,46 @@ defmodule Pleroma.Frontend do end end - defp install_frontend(frontend_info, source, dest) do - from = frontend_info["build_dir"] || "dist" + defp install_frontend(%Frontend{} = frontend, source, dest) do + from = frontend.build_dir || "dist" File.rm_rf!(dest) File.mkdir_p!(dest) File.cp_r!(Path.join([source, from]), dest) :ok end + + # Converts a named frontend into a %Frontend{} struct + def get_named_frontend(name) do + [:frontends, :available, name] + |> Config.get(%{}) + |> from_map() + end + + def merge(%Frontend{} = fe1, %Frontend{} = fe2) do + Map.merge(fe1, fe2, fn _key, v1, v2 -> + # This only overrides things that are actually set + v1 || v2 + end) + end + + def validate!(%Frontend{ref: ref} = fe) when is_binary(ref), do: fe + def validate!(_), do: raise("No ref given or configured") + + def from_map(frontend) when is_map(frontend) do + struct(Frontend, atomize_keys(frontend)) + end + + def to_map(%Frontend{} = frontend) do + frontend + |> Map.from_struct() + |> stringify_keys() + end + + defp atomize_keys(map) do + Map.new(map, fn {k, v} -> {String.to_existing_atom(k), v} end) + end + + defp stringify_keys(map) do + Map.new(map, fn {k, v} -> {to_string(k), v} end) + end end diff --git a/lib/pleroma/web/admin_api/controllers/frontend_controller.ex b/lib/pleroma/web/admin_api/controllers/frontend_controller.ex index 722f51bd2..c1332bdcd 100644 --- a/lib/pleroma/web/admin_api/controllers/frontend_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/frontend_controller.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.AdminAPI.FrontendController do use Pleroma.Web, :controller alias Pleroma.Config + alias Pleroma.Frontend alias Pleroma.Web.Plugs.OAuthScopesPlug plug(Pleroma.Web.ApiSpec.CastAndValidate) @@ -29,12 +30,17 @@ defmodule Pleroma.Web.AdminAPI.FrontendController do end def install(%{body_params: params} = conn, _params) do - with :ok <- Pleroma.Frontend.install(params.name, Map.delete(params, :name)) do + with %Frontend{} = frontend <- params_to_frontend(params), + %Frontend{} <- Frontend.install(frontend) do index(conn, %{}) end end defp installed do - File.ls!(Pleroma.Frontend.dir()) + File.ls!(Frontend.dir()) + end + + defp params_to_frontend(params) when is_map(params) do + struct(Frontend, params) end end diff --git a/test/mix/tasks/pleroma/frontend_test.exs b/test/mix/tasks/pleroma/frontend_test.exs index aa4b25ebb..3443f8958 100644 --- a/test/mix/tasks/pleroma/frontend_test.exs +++ b/test/mix/tasks/pleroma/frontend_test.exs @@ -39,6 +39,28 @@ defmodule Mix.Tasks.Pleroma.FrontendTest do assert File.exists?(Path.join([@dir, "frontends", "pleroma", "fantasy", "test.txt"])) end + test "it enables a frontend with the --primary flag" do + frontend = %Pleroma.Frontend{ + ref: "fantasy", + name: "pleroma", + build_url: "http://gensokyo.2hu/builds/${ref}" + } + + map = Pleroma.Frontend.to_map(frontend) + clear_config(:configurable_from_database, true) + clear_config([:frontends, :available], %{"pleroma" => map}) + + Tesla.Mock.mock(fn %{url: "http://gensokyo.2hu/builds/fantasy"} -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/frontend_dist.zip")} + end) + + capture_io(fn -> + Frontend.run(["install", "pleroma", "--primary"]) + end) + + assert Pleroma.Config.get([:frontends, :primary]) == map + end + test "it also works given a file" do clear_config([:frontends, :available], %{ "pleroma" => %{ @@ -82,4 +104,32 @@ defmodule Mix.Tasks.Pleroma.FrontendTest do assert File.exists?(Path.join([@dir, "frontends", "unknown", "baka", "test.txt"])) end + + describe "enable" do + setup do + clear_config(:configurable_from_database, true) + end + + test "enabling a primary frontend" do + capture_io(fn -> Frontend.run(["enable", "soapbox-fe"]) end) + + primary = Pleroma.Config.get([:frontends, :primary]) + assert primary["name"] == "soapbox-fe" + end + + test "enabling an admin frontend" do + capture_io(fn -> Frontend.run(["enable", "soapbox-fe", "--admin"]) end) + + primary = Pleroma.Config.get([:frontends, :admin]) + assert primary["name"] == "soapbox-fe" + end + + test "raise if configurable_from_database is disabled" do + clear_config(:configurable_from_database, false) + + assert_raise(RuntimeError, fn -> + capture_io(fn -> Frontend.run(["enable", "soapbox-fe"]) end) + end) + end + end end diff --git a/test/pleroma/frontend_test.exs b/test/pleroma/frontend_test.exs index 1b50a031d..3cd30d121 100644 --- a/test/pleroma/frontend_test.exs +++ b/test/pleroma/frontend_test.exs @@ -18,31 +18,32 @@ defmodule Pleroma.FrontendTest do end test "it downloads and unzips a known frontend" do - clear_config([:frontends, :available], %{ - "pleroma" => %{ - "ref" => "fantasy", - "name" => "pleroma", - "build_url" => "http://gensokyo.2hu/builds/${ref}" - } - }) + frontend = %Frontend{ + ref: "fantasy", + name: "pleroma", + build_url: "http://gensokyo.2hu/builds/${ref}" + } + + clear_config([:frontends, :available], %{"pleroma" => Frontend.to_map(frontend)}) Tesla.Mock.mock(fn %{url: "http://gensokyo.2hu/builds/fantasy"} -> %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/frontend_dist.zip")} end) - Frontend.install("pleroma") + Frontend.install(frontend) assert File.exists?(Path.join([@dir, "frontends", "pleroma", "fantasy", "test.txt"])) end test "it also works given a file" do - clear_config([:frontends, :available], %{ - "pleroma" => %{ - "ref" => "fantasy", - "name" => "pleroma", - "build_dir" => "" - } - }) + frontend = %Frontend{ + ref: "fantasy", + name: "pleroma", + build_dir: "", + file: "test/fixtures/tesla_mock/frontend.zip" + } + + clear_config([:frontends, :available], %{"pleroma" => Frontend.to_map(frontend)}) folder = Path.join([@dir, "frontends", "pleroma", "fantasy"]) previously_existing = Path.join([folder, "temp"]) @@ -50,23 +51,112 @@ defmodule Pleroma.FrontendTest do File.write!(previously_existing, "yey") assert File.exists?(previously_existing) - Frontend.install("pleroma", file: "test/fixtures/tesla_mock/frontend.zip") + Frontend.install(frontend) assert File.exists?(Path.join([folder, "test.txt"])) refute File.exists?(previously_existing) end test "it downloads and unzips unknown frontends" do + frontend = %Frontend{ + ref: "baka", + build_url: "http://gensokyo.2hu/madeup.zip", + build_dir: "" + } + Tesla.Mock.mock(fn %{url: "http://gensokyo.2hu/madeup.zip"} -> %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/frontend.zip")} end) - Frontend.install("unknown", - ref: "baka", - build_url: "http://gensokyo.2hu/madeup.zip", - build_dir: "" - ) + Frontend.install(frontend) assert File.exists?(Path.join([@dir, "frontends", "unknown", "baka", "test.txt"])) end + + test "merge/2 only overrides nil values" do + fe1 = %Frontend{name: "pleroma"} + fe2 = %Frontend{name: "soapbox", ref: "fantasy"} + expected = %Frontend{name: "pleroma", ref: "fantasy"} + assert Frontend.merge(fe1, fe2) == expected + end + + test "validate!/1 raises if :ref isn't set" do + fe = %Frontend{name: "pleroma"} + assert_raise(RuntimeError, fn -> Frontend.validate!(fe) end) + end + + test "validate!/1 returns the frontend" do + fe = %Frontend{name: "pleroma", ref: "fantasy"} + assert Frontend.validate!(fe) == fe + end + + test "from_map/1 parses a map into a %Frontend{} struct" do + map = %{"name" => "pleroma", "ref" => "fantasy"} + expected = %Frontend{name: "pleroma", ref: "fantasy"} + assert Frontend.from_map(map) == expected + end + + test "to_map/1 returns the frontend as a map with string keys" do + frontend = %Frontend{name: "pleroma", ref: "fantasy"} + + expected = %{ + "name" => "pleroma", + "ref" => "fantasy", + "build_dir" => nil, + "build_url" => nil, + "custom-http-headers" => nil, + "file" => nil, + "git" => nil + } + + assert Frontend.to_map(frontend) == expected + end + + test "parse_build_url/1 replaces ${ref}" do + frontend = %Frontend{ + name: "pleroma", + ref: "fantasy", + build_url: "http://gensokyo.2hu/builds/${ref}" + } + + expected = "http://gensokyo.2hu/builds/fantasy" + assert Frontend.parse_build_url(frontend) == expected + end + + test "dir/0 returns the frontend dir" do + assert Frontend.dir() == "test/frontend_static_test/frontends" + end + + test "get_named_frontend/1 returns a frontend from the config" do + frontend = %Frontend{name: "pleroma", ref: "fantasy"} + clear_config([:frontends, :available], %{"pleroma" => Frontend.to_map(frontend)}) + + assert Frontend.get_named_frontend("pleroma") == frontend + end + + describe "enable/2" do + setup do + clear_config(:configurable_from_database, true) + end + + test "enables a primary frontend" do + frontend = %Frontend{name: "soapbox", ref: "v1.2.3"} + map = Frontend.to_map(frontend) + + clear_config([:frontends, :available], %{"soapbox" => map}) + Frontend.enable(frontend, :primary) + + assert Pleroma.Config.get([:frontends, :primary]) == map + end + + test "enables an admin frontend" do + frontend = %Frontend{name: "admin-fe", ref: "develop"} + map = Frontend.to_map(frontend) + + clear_config([:frontends, :available], %{"admin-fe" => map}) + Frontend.enable(frontend, :admin) + + assert Pleroma.Config.get([:frontends, :admin]) == map + end + end end |