diff --git a/CHANGELOG.md b/CHANGELOG.md index 510ad7ff5..4dafa0df5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - [MongooseIM](https://github.com/esl/MongooseIM) http authentication support. - LDAP authentication - External OAuth provider authentication +- Support for building a release using [`mix release`](https://hexdocs.pm/mix/master/Mix.Tasks.Release.html) - A [job queue](https://git.pleroma.social/pleroma/pleroma_job_queue) for federation, emails, web push, etc. - [Prometheus](https://prometheus.io/) metrics - Support for Mastodon's remote interaction diff --git a/config/prod.exs b/config/prod.exs index 1179cf3b0..bf1a97de0 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -17,6 +17,8 @@ http: [port: 4000], protocol: "http" +config :phoenix, serve_endpoints: true + # Do not print debug messages in production config :logger, level: :warn diff --git a/config/releases.exs b/config/releases.exs new file mode 100644 index 000000000..becde7693 --- /dev/null +++ b/config/releases.exs @@ -0,0 +1 @@ +import Config diff --git a/lib/mix/tasks/pleroma/common.ex b/lib/mix/tasks/pleroma/common.ex index 25977f656..7d50605af 100644 --- a/lib/mix/tasks/pleroma/common.ex +++ b/lib/mix/tasks/pleroma/common.ex @@ -10,19 +10,53 @@ def start_pleroma do end def get_option(options, opt, prompt, defval \\ nil, defname \\ nil) do - Keyword.get(options, opt) || - case Mix.shell().prompt("#{prompt} [#{defname || defval}]") do - "\n" -> - case defval do - nil -> get_option(options, opt, prompt, defval) - defval -> defval - end - - opt -> - opt |> String.trim() - end + Keyword.get(options, opt) || shell_prompt(prompt, defval, defname) end + def shell_prompt(prompt, defval \\ nil, defname \\ nil) do + prompt_message = "#{prompt} [#{defname || defval}]" + + input = + if mix_shell?(), + do: Mix.shell().prompt(prompt_message), + else: :io.get_line(prompt_message) + + case input do + "\n" -> + case defval do + nil -> + shell_prompt(prompt, defval, defname) + + defval -> + defval + end + + input -> + String.trim(input) + end + end + + def shell_yes?(message) do + if mix_shell?(), + do: Mix.shell().yes?("Continue?"), + else: shell_prompt(message, "Continue?") in ~w(Yn Y y) + end + + def shell_info(message) do + if mix_shell?(), + do: Mix.shell().info(message), + else: IO.puts(message) + end + + def shell_error(message) do + if mix_shell?(), + do: Mix.shell().error(message), + else: IO.puts(:stderr, message) + end + + @doc "Performs a safe check whether `Mix.shell/0` is available (does not raise if Mix is not loaded)" + def mix_shell?, do: :erlang.function_exported(Mix, :shell, 0) + def escape_sh_path(path) do ~S(') <> String.replace(path, ~S('), ~S(\')) <> ~S(') end diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex index 6cee8d630..88925dbaf 100644 --- a/lib/mix/tasks/pleroma/instance.ex +++ b/lib/mix/tasks/pleroma/instance.ex @@ -155,17 +155,17 @@ def run(["gen" | rest]) do dbpass: dbpass ) - Mix.shell().info( + Common.shell_info( "Writing config to #{config_path}. You should rename it to config/prod.secret.exs or config/dev.secret.exs." ) File.write(config_path, result_config) - Mix.shell().info("Writing #{psql_path}.") + Common.shell_info("Writing #{psql_path}.") File.write(psql_path, result_psql) write_robots_txt(indexable) - Mix.shell().info( + Common.shell_info( "\n" <> """ To get started: @@ -179,7 +179,7 @@ def run(["gen" | rest]) do end ) else - Mix.shell().error( + Common.shell_error( "The task would have overwritten the following files:\n" <> (Enum.map(paths, &"- #{&1}\n") |> Enum.join("")) <> "Rerun with `--force` to overwrite them." @@ -204,10 +204,10 @@ defp write_robots_txt(indexable) do if File.exists?(robots_txt_path) do File.cp!(robots_txt_path, "#{robots_txt_path}.bak") - Mix.shell().info("Backing up existing robots.txt to #{robots_txt_path}.bak") + Common.shell_info("Backing up existing robots.txt to #{robots_txt_path}.bak") end File.write(robots_txt_path, robots_txt) - Mix.shell().info("Writing #{robots_txt_path}.") + Common.shell_info("Writing #{robots_txt_path}.") end end diff --git a/lib/mix/tasks/pleroma/relay.ex b/lib/mix/tasks/pleroma/relay.ex index fbec473c5..213ae24d2 100644 --- a/lib/mix/tasks/pleroma/relay.ex +++ b/lib/mix/tasks/pleroma/relay.ex @@ -30,7 +30,7 @@ def run(["follow", target]) do # put this task to sleep to allow the genserver to push out the messages :timer.sleep(500) else - {:error, e} -> Mix.shell().error("Error while following #{target}: #{inspect(e)}") + {:error, e} -> Common.shell_error("Error while following #{target}: #{inspect(e)}") end end @@ -41,7 +41,7 @@ def run(["unfollow", target]) do # put this task to sleep to allow the genserver to push out the messages :timer.sleep(500) else - {:error, e} -> Mix.shell().error("Error while following #{target}: #{inspect(e)}") + {:error, e} -> Common.shell_error("Error while following #{target}: #{inspect(e)}") end end end diff --git a/lib/mix/tasks/pleroma/uploads.ex b/lib/mix/tasks/pleroma/uploads.ex index 106fcf443..8855b5538 100644 --- a/lib/mix/tasks/pleroma/uploads.ex +++ b/lib/mix/tasks/pleroma/uploads.ex @@ -38,10 +38,10 @@ def run(["migrate_local", target_uploader | args]) do Pleroma.Config.put([Upload, :uploader], uploader) end - Mix.shell().info("Migrating files from local #{local_path} to #{to_string(uploader)}") + Common.shell_info("Migrating files from local #{local_path} to #{to_string(uploader)}") if delete? do - Mix.shell().info( + Common.shell_info( "Attention: uploaded files will be deleted, hope you have backups! (--delete ; cancel with ^C)" ) @@ -78,7 +78,7 @@ def run(["migrate_local", target_uploader | args]) do |> Enum.filter(& &1) total_count = length(uploads) - Mix.shell().info("Found #{total_count} uploads") + Common.shell_info("Found #{total_count} uploads") uploads |> Task.async_stream( @@ -90,7 +90,7 @@ def run(["migrate_local", target_uploader | args]) do :ok error -> - Mix.shell().error("failed to upload #{inspect(upload.path)}: #{inspect(error)}") + Common.shell_error("failed to upload #{inspect(upload.path)}: #{inspect(error)}") end end, timeout: 150_000 @@ -99,10 +99,10 @@ def run(["migrate_local", target_uploader | args]) do # credo:disable-for-next-line Credo.Check.Warning.UnusedEnumOperation |> Enum.reduce(0, fn done, count -> count = count + length(done) - Mix.shell().info("Uploaded #{count}/#{total_count} files") + Common.shell_info("Uploaded #{count}/#{total_count} files") count end) - Mix.shell().info("Done!") + Common.shell_info("Done!") end end diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index 25fc40ea7..7eaa49836 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -115,7 +115,7 @@ def run(["new", nickname, email | rest]) do admin? = Keyword.get(options, :admin, false) assume_yes? = Keyword.get(options, :assume_yes, false) - Mix.shell().info(""" + Common.shell_info(""" A user will be created with the following information: - nickname: #{nickname} - email: #{email} @@ -128,7 +128,7 @@ def run(["new", nickname, email | rest]) do - admin: #{if(admin?, do: "true", else: "false")} """) - proceed? = assume_yes? or Mix.shell().yes?("Continue?") + proceed? = assume_yes? or Common.shell_yes?("Continue?") if proceed? do Common.start_pleroma() @@ -145,7 +145,7 @@ def run(["new", nickname, email | rest]) do changeset = User.register_changeset(%User{}, params, need_confirmation: false) {:ok, _user} = User.register(changeset) - Mix.shell().info("User #{nickname} created") + Common.shell_info("User #{nickname} created") if moderator? do run(["set", nickname, "--moderator"]) @@ -159,7 +159,7 @@ def run(["new", nickname, email | rest]) do run(["reset_password", nickname]) end else - Mix.shell().info("User will not be created.") + Common.shell_info("User will not be created.") end end @@ -168,10 +168,10 @@ def run(["rm", nickname]) do with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do User.perform(:delete, user) - Mix.shell().info("User #{nickname} deleted.") + Common.shell_info("User #{nickname} deleted.") else _ -> - Mix.shell().error("No local user #{nickname}") + Common.shell_error("No local user #{nickname}") end end @@ -181,12 +181,12 @@ def run(["toggle_activated", nickname]) do with %User{} = user <- User.get_cached_by_nickname(nickname) do {:ok, user} = User.deactivate(user, !user.info.deactivated) - Mix.shell().info( + Common.shell_info( "Activation status of #{nickname}: #{if(user.info.deactivated, do: "de", else: "")}activated" ) else _ -> - Mix.shell().error("No user #{nickname}") + Common.shell_error("No user #{nickname}") end end @@ -195,7 +195,7 @@ def run(["reset_password", nickname]) do with %User{local: true} = user <- User.get_cached_by_nickname(nickname), {:ok, token} <- Pleroma.PasswordResetToken.create_token(user) do - Mix.shell().info("Generated password reset token for #{user.nickname}") + Common.shell_info("Generated password reset token for #{user.nickname}") IO.puts( "URL: #{ @@ -208,7 +208,7 @@ def run(["reset_password", nickname]) do ) else _ -> - Mix.shell().error("No local user #{nickname}") + Common.shell_error("No local user #{nickname}") end end @@ -216,7 +216,7 @@ def run(["unsubscribe", nickname]) do Common.start_pleroma() with %User{} = user <- User.get_cached_by_nickname(nickname) do - Mix.shell().info("Deactivating #{user.nickname}") + Common.shell_info("Deactivating #{user.nickname}") User.deactivate(user) {:ok, friends} = User.get_friends(user) @@ -224,7 +224,7 @@ def run(["unsubscribe", nickname]) do Enum.each(friends, fn friend -> user = User.get_cached_by_id(user.id) - Mix.shell().info("Unsubscribing #{friend.nickname} from #{user.nickname}") + Common.shell_info("Unsubscribing #{friend.nickname} from #{user.nickname}") User.unfollow(user, friend) end) @@ -233,11 +233,11 @@ def run(["unsubscribe", nickname]) do user = User.get_cached_by_id(user.id) if Enum.empty?(user.following) do - Mix.shell().info("Successfully unsubscribed all followers from #{user.nickname}") + Common.shell_info("Successfully unsubscribed all followers from #{user.nickname}") end else _ -> - Mix.shell().error("No user #{nickname}") + Common.shell_error("No user #{nickname}") end end @@ -274,7 +274,7 @@ def run(["set", nickname | rest]) do end else _ -> - Mix.shell().error("No local user #{nickname}") + Common.shell_error("No local user #{nickname}") end end @@ -284,10 +284,10 @@ def run(["tag", nickname | tags]) do with %User{} = user <- User.get_cached_by_nickname(nickname) do user = user |> User.tag(tags) - Mix.shell().info("Tags of #{user.nickname}: #{inspect(tags)}") + Common.shell_info("Tags of #{user.nickname}: #{inspect(tags)}") else _ -> - Mix.shell().error("Could not change user tags for #{nickname}") + Common.shell_error("Could not change user tags for #{nickname}") end end @@ -297,10 +297,10 @@ def run(["untag", nickname | tags]) do with %User{} = user <- User.get_cached_by_nickname(nickname) do user = user |> User.untag(tags) - Mix.shell().info("Tags of #{user.nickname}: #{inspect(tags)}") + Common.shell_info("Tags of #{user.nickname}: #{inspect(tags)}") else _ -> - Mix.shell().error("Could not change user tags for #{nickname}") + Common.shell_error("Could not change user tags for #{nickname}") end end @@ -326,7 +326,7 @@ def run(["invite" | rest]) do with {:ok, val} <- options[:expires_at], options = Map.put(options, :expires_at, val), {:ok, invite} <- UserInviteToken.create_invite(options) do - Mix.shell().info( + Common.shell_info( "Generated user invite token " <> String.replace(invite.invite_type, "_", " ") ) @@ -340,14 +340,14 @@ def run(["invite" | rest]) do IO.puts(url) else error -> - Mix.shell().error("Could not create invite token: #{inspect(error)}") + Common.shell_error("Could not create invite token: #{inspect(error)}") end end def run(["invites"]) do Common.start_pleroma() - Mix.shell().info("Invites list:") + Common.shell_info("Invites list:") UserInviteToken.list_invites() |> Enum.each(fn invite -> @@ -361,7 +361,7 @@ def run(["invites"]) do " | Max use: #{max_use} Left use: #{max_use - invite.uses}" end - Mix.shell().info( + Common.shell_info( "ID: #{invite.id} | Token: #{invite.token} | Token type: #{invite.invite_type} | Used: #{ invite.used }#{expire_info}#{using_info}" @@ -374,9 +374,9 @@ def run(["revoke_invite", token]) do with {:ok, invite} <- UserInviteToken.find_by_token(token), {:ok, _} <- UserInviteToken.update_invite(invite, %{used: true}) do - Mix.shell().info("Invite for token #{token} was revoked.") + Common.shell_info("Invite for token #{token} was revoked.") else - _ -> Mix.shell().error("No invite found with token #{token}") + _ -> Common.shell_error("No invite found with token #{token}") end end @@ -385,10 +385,10 @@ def run(["delete_activities", nickname]) do with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do {:ok, _} = User.delete_user_activities(user) - Mix.shell().info("User #{nickname} statuses deleted.") + Common.shell_info("User #{nickname} statuses deleted.") else _ -> - Mix.shell().error("No local user #{nickname}") + Common.shell_error("No local user #{nickname}") end end @@ -400,10 +400,10 @@ def run(["toggle_confirmed", nickname]) do message = if user.info.confirmation_pending, do: "needs", else: "doesn't need" - Mix.shell().info("#{nickname} #{message} confirmation.") + Common.shell_info("#{nickname} #{message} confirmation.") else _ -> - Mix.shell().error("No local user #{nickname}") + Common.shell_error("No local user #{nickname}") end end @@ -416,7 +416,7 @@ defp set_moderator(user, value) do {:ok, user} = User.update_and_set_cache(user_cng) - Mix.shell().info("Moderator status of #{user.nickname}: #{user.info.is_moderator}") + Common.shell_info("Moderator status of #{user.nickname}: #{user.info.is_moderator}") user end @@ -429,7 +429,7 @@ defp set_admin(user, value) do {:ok, user} = User.update_and_set_cache(user_cng) - Mix.shell().info("Admin status of #{user.nickname}: #{user.info.is_admin}") + Common.shell_info("Admin status of #{user.nickname}: #{user.info.is_admin}") user end @@ -442,7 +442,7 @@ defp set_locked(user, value) do {:ok, user} = User.update_and_set_cache(user_cng) - Mix.shell().info("Locked status of #{user.nickname}: #{user.info.locked}") + Common.shell_info("Locked status of #{user.nickname}: #{user.info.locked}") user end end diff --git a/lib/pleroma/release_tasks.ex b/lib/pleroma/release_tasks.ex new file mode 100644 index 000000000..7726bc635 --- /dev/null +++ b/lib/pleroma/release_tasks.ex @@ -0,0 +1,64 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ReleaseTasks do + @repo Pleroma.Repo + + def run(args) do + Mix.Tasks.Pleroma.Common.start_pleroma() + [task | args] = String.split(args) + + case task do + "migrate" -> migrate() + "create" -> create() + "rollback" -> rollback(String.to_integer(Enum.at(args, 0))) + task -> mix_task(task, args) + end + end + + defp mix_task(task, args) do + {:ok, modules} = :application.get_key(:pleroma, :modules) + + module = + Enum.find(modules, fn module -> + module = Module.split(module) + + match?(["Mix", "Tasks", "Pleroma" | _], module) and + String.downcase(List.last(module)) == task + end) + + if module do + module.run(args) + else + IO.puts("The task #{task} does not exist") + end + end + + def migrate do + {:ok, _, _} = Ecto.Migrator.with_repo(@repo, &Ecto.Migrator.run(&1, :up, all: true)) + end + + def rollback(version) do + {:ok, _, _} = Ecto.Migrator.with_repo(@repo, &Ecto.Migrator.run(&1, :down, to: version)) + end + + def create do + case @repo.__adapter__.storage_up(@repo.config) do + :ok -> + IO.puts("The database for #{inspect(@repo)} has been created") + + {:error, :already_up} -> + IO.puts("The database for #{inspect(@repo)} has already been created") + + {:error, term} when is_binary(term) -> + IO.puts(:stderr, "The database for #{inspect(@repo)} couldn't be created: #{term}") + + {:error, term} -> + IO.puts( + :stderr, + "The database for #{inspect(@repo)} couldn't be created: #{inspect(term)}" + ) + end + end +end diff --git a/mix.exs b/mix.exs index 9447a2e4f..a6481bab6 100644 --- a/mix.exs +++ b/mix.exs @@ -32,10 +32,22 @@ def project do ], main: "readme", output: "priv/static/doc" + ], + releases: [ + pleroma: [ + include_executables_for: [:unix], + applications: [ex_syslogger: :load, syslog: :load], + steps: [:assemble, ©_pleroma_ctl/1] + ] ] ] end + def copy_pleroma_ctl(%{path: target_path} = release) do + File.cp!("./rel/pleroma_ctl", Path.join([target_path, "bin", "pleroma_ctl"])) + release + end + # Configuration for the OTP application. # # Type `mix help compile.app` for more information. diff --git a/rel/env.sh.eex b/rel/env.sh.eex new file mode 100644 index 000000000..a4ce25295 --- /dev/null +++ b/rel/env.sh.eex @@ -0,0 +1,12 @@ +#!/bin/sh + +# Sets and enables heart (recommended only in daemon mode) +# if [ "$RELEASE_COMMAND" = "daemon" ] || [ "$RELEASE_COMMAND" = "daemon_iex" ]; then +# HEART_COMMAND="$RELEASE_ROOT/bin/$RELEASE_NAME $RELEASE_COMMAND" +# export HEART_COMMAND +# export ELIXIR_ERL_OPTIONS="-heart" +# fi + +# Set the release to work across nodes +export RELEASE_DISTRIBUTION=name +export RELEASE_NODE=<%= @release.name %>@127.0.0.1 diff --git a/rel/pleroma_ctl b/rel/pleroma_ctl new file mode 100755 index 000000000..ef2717c44 --- /dev/null +++ b/rel/pleroma_ctl @@ -0,0 +1,19 @@ +#!/bin/sh +# XXX: This should be removed when elixir's releases get custom command support +if [ -z "$1" ] || [ "$1" == "help" ]; then + echo "Usage: $(basename "$0") COMMAND [ARGS] + + The known commands are: + + create Create database schema (needs to be executed only once) + migrate Execute database migrations (needs to be done after updates) + rollback [VERSION] Rollback database migrations (needs to be done before downgrading) + + and any mix tasks under Pleroma namespace, for example \`mix pleroma.user COMMAND\` is + equivalent to \`$(basename "$0") user COMMAND\` +" +else + SCRIPT=$(readlink -f "$0") + SCRIPTPATH=$(dirname "$SCRIPT") + $SCRIPTPATH/pleroma eval 'Pleroma.ReleaseTasks.run("'"$*"'")' +fi diff --git a/rel/vm.args.eex b/rel/vm.args.eex new file mode 100644 index 000000000..71e803264 --- /dev/null +++ b/rel/vm.args.eex @@ -0,0 +1,11 @@ +## Customize flags given to the VM: http://erlang.org/doc/man/erl.html +## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here + +## Number of dirty schedulers doing IO work (file, sockets, etc) +##+SDio 5 + +## Increase number of concurrent ports/sockets +##+Q 65536 + +## Tweak GC to run more often +##-env ERL_FULLSWEEP_AFTER 10