Compare commits

...

7 Commits

Author SHA1 Message Date
Alex Gleason
d205d855fe
Changelog: enable frontend CLI task 2021-06-14 19:03:25 -05:00
Alex Gleason
5e2c63d97d
Merge remote-tracking branch 'pleroma/develop' into frontend-enable 2021-06-14 19:02:23 -05:00
Alex Gleason
811dcc1928
Fix FrontendController 2021-06-14 19:02:13 -05:00
Alex Gleason
d9a45175c0
Set soapbox-fe ref to develop 2021-06-14 17:56:19 -05:00
Alex Gleason
8527faecf5
Enable a frontend automatically with --primary arg 2021-06-14 17:55:21 -05:00
Alex Gleason
9a768429a3
Frontend: enable CLI task 2021-06-14 17:27:29 -05:00
Alex Gleason
a9106e4f13
Frontends: refactor with %Frontend{} struct 2021-06-14 15:00:00 -05:00
9 changed files with 375 additions and 63 deletions

1
.gitignore vendored
View File

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

View File

@ -20,6 +20,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- 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`.
- `AnalyzeMetadata` upload filter for extracting image/video attachment dimensions and generating blurhashes for images. Blurhashes for videos are not generated at this time.
- CLI task to enable a frontend: `mix pleroma.frontend enable <name>`
- Attachment dimensions and blurhashes are federated when available.
- Pinned posts federation

View File

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

View File

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

View File

@ -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,7 @@ defmodule Mix.Tasks.Pleroma.Frontend do
"none"
end
def run(["install", frontend | args]) do
def run(["install", name | args]) do
start_pleroma()
{options, [], []} =
@ -24,13 +26,83 @@ 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)
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, [], []} =
OptionParser.parse(
args,
strict: [
ref: :string,
build_url: :string,
build_dir: :string,
file: :string,
admin: :boolean,
primary: :boolean
]
)
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

View File

@ -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]
}
@unknown_name "unknown"
@frontend_types [:admin, :primary]
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)
defstruct [:name, :ref, :git, :build_url, :build_dir, :file, :"custom-http-headers"]
ref = frontend_info["ref"]
def install(%Frontend{} = frontend) do
frontend
|> maybe_put_name()
|> hydrate()
|> validate!()
|> do_install()
end
unless ref do
raise "No ref given or configured"
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

View File

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

View File

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

View File

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