From 2116bc2d0db0bb1c8d218701e2ccdb09067d866e Mon Sep 17 00:00:00 2001 From: Brandyn Bennett Date: Thu, 25 May 2023 10:27:00 -0600 Subject: [PATCH] POD13-65 polyn.gen.release task (#30) * gen release task and test * add polyn.gen.release task and don't assume which otp_app * inject polyn migrate functions into existing release file * update changelog * gross fix for flaky test --- polyn_elixir_client/CHANGELOG.md | 3 +- polyn_elixir_client/README.md | 35 ++++--- polyn_elixir_client/config/dev.exs | 1 - polyn_elixir_client/config/test.exs | 1 - .../lib/mix/tasks/polyn.gen.release.ex | 98 +++++++++++++++++++ .../lib/polyn/migration/migrator.ex | 6 +- polyn_elixir_client/lib/polyn/release.ex | 35 ------- .../test/mix/tasks/polyn.gen.release_test.exs | 76 ++++++++++++++ .../test/polyn/migration/bucket_test.exs | 4 + 9 files changed, 204 insertions(+), 55 deletions(-) create mode 100644 polyn_elixir_client/lib/mix/tasks/polyn.gen.release.ex delete mode 100644 polyn_elixir_client/lib/polyn/release.ex create mode 100644 polyn_elixir_client/test/mix/tasks/polyn.gen.release_test.exs diff --git a/polyn_elixir_client/CHANGELOG.md b/polyn_elixir_client/CHANGELOG.md index 3c02be5..091a2be 100644 --- a/polyn_elixir_client/CHANGELOG.md +++ b/polyn_elixir_client/CHANGELOG.md @@ -2,8 +2,7 @@ ## 0.6.1 -* Adds Polyn.Release module for working with `mix release`. -* Requires otp_app config +* Adds `mix polyn.gen.release` task for working with `mix release`. ## 0.6.0 diff --git a/polyn_elixir_client/README.md b/polyn_elixir_client/README.md index ccc7ac2..155ee54 100644 --- a/polyn_elixir_client/README.md +++ b/polyn_elixir_client/README.md @@ -58,14 +58,6 @@ The [Cloud Event Spec](https://github.com/cloudevents/spec/blob/v1.0.2/cloudeven config :polyn, :source_root, "orders.payments" ``` -### OTP App - -Polyn needs to know the name of the otp app that's using it - -```elixir -config :polyn, :otp_app, :my_app -``` - ### NATS Connection You will need to provide the connection settings for your NATS connection. This will differ in-between environments. More settings options can be seen [here](https://hexdocs.pm/gnat/Gnat.ConnectionSupervisor.html#content) @@ -122,14 +114,35 @@ Polyn uses a shared Key-Value bucket in NATS to avoid re-running migrations. It When using `mix release` to deploy, `mix` and Mix Tasks are not available, so you can't use `mix polyn.migrate` to do your migrations. -Instead you can use a built-in `Polyn.Release` module to execute migrations in the compiled application +Instead you'll need to run `mix polyn.gen.release` which will add a `lib/my_app/release.ex` file to your app (if you already have this file it will append to it). The file will look something like this: + +```elixir +defmodule MyApp.Release do + @app :my_app + + def polyn_migrate do + load_app() + {:ok, _apps} = Application.ensure_all_started(:polyn) -You can use it from the release like this: + dir = Path.join([:code.priv_dir(@app), "polyn", "migrations"]) + Polyn.Migration.Migrator.run(migrations_dir: dir) + end + + defp load_app do + Application.load(@app) + end +end +``` + +You can use the `polyn_migrate` function from this module to execute migrations in the compiled release like this: ``` -_build/prod/rel/my_app/bin/my_app eval "Polyn.Release.migrate" +_build/prod/rel/my_app/bin/my_app eval "MyApp.Release.polyn_migrate" ``` +Sometimes multiple OTP apps are part of a single application, so Polyn doesn't assume which app to use for accessing and running migration files. This is why you need to generate the `release.ex` file yourself and pass in the OTP app you want. + + ## Usage ### Publishing Messages diff --git a/polyn_elixir_client/config/dev.exs b/polyn_elixir_client/config/dev.exs index 503b5e6..b3f63ce 100644 --- a/polyn_elixir_client/config/dev.exs +++ b/polyn_elixir_client/config/dev.exs @@ -2,7 +2,6 @@ import Config config :polyn, :domain, "com.acme" config :polyn, :source_root, "user.backend" -config :polyn, :otp_app, :polyn config :polyn, :nats, %{ name: :gnat, diff --git a/polyn_elixir_client/config/test.exs b/polyn_elixir_client/config/test.exs index d58d3f4..6c4d19c 100644 --- a/polyn_elixir_client/config/test.exs +++ b/polyn_elixir_client/config/test.exs @@ -2,7 +2,6 @@ import Config config :polyn, :domain, "com.test" config :polyn, :source_root, "user.backend" -config :polyn, :otp_app, :polyn config :polyn, :nats, %{ name: :gnat, diff --git a/polyn_elixir_client/lib/mix/tasks/polyn.gen.release.ex b/polyn_elixir_client/lib/mix/tasks/polyn.gen.release.ex new file mode 100644 index 0000000..091756c --- /dev/null +++ b/polyn_elixir_client/lib/mix/tasks/polyn.gen.release.ex @@ -0,0 +1,98 @@ +defmodule Mix.Tasks.Polyn.Gen.Release do + @moduledoc """ + Use `mix polyn.gen.release` to generate a new polyn release module for your application + """ + @shortdoc "Generates a new polyn release file" + + use Mix.Task + require Mix.Generator + + def run(args) do + {options, []} = OptionParser.parse!(args, strict: [dir: :string]) + path = Path.join([dir(options), Atom.to_string(app_name()), "release.ex"]) + assigns = [mod: module_name(), app: app_name()] + + if File.exists?(path) do + check_existing(path) + else + Mix.Generator.create_file(path, release_file_template(assigns)) + end + + inject_into_existing(path) + end + + defp dir(options) do + Keyword.get(options, :dir, Path.join(File.cwd!(), "lib")) + end + + defp app_name do + Mix.Project.config() |> Keyword.fetch!(:app) + end + + defp module_name do + prefix = app_name() |> Atom.to_string() |> Macro.camelize() + Module.concat([prefix, Release]) + end + + defp check_existing(path) do + unless prompt_allow_injection(path) do + System.halt() + end + end + + defp prompt_allow_injection(path) do + Mix.shell().yes?( + "#{path} already exists. Would you like to inject Polyn release functions into it?" + ) + end + + defp inject_into_existing(path) do + file = File.read!(path) + + lines = + String.trim_trailing(file) + |> String.trim_trailing("end") + |> String.split(["\n", "\r\n"]) + + lines = List.insert_at(lines, first_private_func_index(lines), polyn_migrate_text()) + + injected = Enum.join(lines, "\n") <> "end\n" + File.write!(path, injected) + end + + defp first_private_func_index(lines) do + result = + Enum.find_index(lines, fn line -> + String.contains?(line, "defp ") + end) + + # Use the very end of the file if there are no private functions + case result do + nil -> -1 + index -> index + end + end + + Mix.Generator.embed_template(:release_file, """ + defmodule <%= inspect @mod %> do + @app <%= inspect @app %> + + defp load_app do + Application.load(@app) + end + end + """) + + Mix.Generator.embed_text( + :polyn_migrate, + """ + def polyn_migrate do + load_app() + {:ok, _apps} = Application.ensure_all_started(:polyn) + + dir = Path.join([:code.priv_dir(@app), "polyn", "migrations"]) + Polyn.Migration.Migrator.run(migrations_dir: dir) + end + """ + ) +end diff --git a/polyn_elixir_client/lib/polyn/migration/migrator.ex b/polyn_elixir_client/lib/polyn/migration/migrator.ex index ea39129..f6a97a2 100644 --- a/polyn_elixir_client/lib/polyn/migration/migrator.ex +++ b/polyn_elixir_client/lib/polyn/migration/migrator.ex @@ -52,7 +52,7 @@ defmodule Polyn.Migration.Migrator do Path of migration files """ def migrations_dir do - Path.join([:code.priv_dir(otp_app()), "polyn", "migrations"]) + Path.join([File.cwd!(), "priv", "polyn", "migrations"]) end @doc """ @@ -174,8 +174,4 @@ defmodule Polyn.Migration.Migrator do state end - - defp otp_app do - Application.fetch_env!(:polyn, :otp_app) - end end diff --git a/polyn_elixir_client/lib/polyn/release.ex b/polyn_elixir_client/lib/polyn/release.ex deleted file mode 100644 index 42ab03c..0000000 --- a/polyn_elixir_client/lib/polyn/release.ex +++ /dev/null @@ -1,35 +0,0 @@ -defmodule Polyn.Release do - @moduledoc """ - Utilities for working with Polyn and mix releases. - `mix` and mix tasks aren't available in a release. This module - provides a way to run migrations in a release. - """ - - alias Polyn.Migration - - # Using a compile-time env to avoid the chicken-egg problem - # of needing the application to be loaded to get the env - # to load the application - @otp_app Application.compile_env!(:polyn, :otp_app) - - @doc """ - Run migrations in a release - - ## Examples - - ``` - mix release - _build/prod/rel/my_app/bin/my_app eval "Polyn.Release.migrate" - ``` - """ - def migrate do - load_app() - - Migration.Migrator.run() - end - - defp load_app do - Application.load(@otp_app) - {:ok, _apps} = Application.ensure_all_started(:polyn) - end -end diff --git a/polyn_elixir_client/test/mix/tasks/polyn.gen.release_test.exs b/polyn_elixir_client/test/mix/tasks/polyn.gen.release_test.exs new file mode 100644 index 0000000..d988a37 --- /dev/null +++ b/polyn_elixir_client/test/mix/tasks/polyn.gen.release_test.exs @@ -0,0 +1,76 @@ +defmodule Mix.Tasks.Polyn.Gen.ReleaseTest do + use ExUnit.Case, async: true + + alias Mix.Tasks.Polyn.Gen + + @moduletag :tmp_dir + + # send output to test process rather than stdio + Mix.shell(Mix.Shell.Process) + + test "makes release file", %{tmp_dir: tmp_dir} do + Gen.Release.run(["--dir", tmp_dir]) + + path = Path.join([tmp_dir, "polyn", "release.ex"]) + + file = File.read!(path) + + assert [{Polyn.Release, _binary}] = Code.compile_string(file) + + assert file == + """ + defmodule Polyn.Release do + @app :polyn + + def polyn_migrate do + load_app() + {:ok, _apps} = Application.ensure_all_started(:polyn) + + dir = Path.join([:code.priv_dir(@app), "polyn", "migrations"]) + Polyn.Migration.Migrator.run(migrations_dir: dir) + end + + defp load_app do + Application.load(@app) + end + end + """ + end + + test "injects polyn_migrate function if release_file exists already", %{tmp_dir: tmp_dir} do + File.mkdir!(Path.join(tmp_dir, "polyn")) + + path = Path.join([tmp_dir, "polyn", "release.ex"]) + + File.write!(path, """ + defmodule Polyn.Release do + def do_other_stuff do + end + end + """) + + send(self(), {:mix_shell_input, :yes?, true}) + Gen.Release.run(["--dir", tmp_dir]) + + prompt = "#{path} already exists. Would you like to inject Polyn release functions into it?" + assert_received {:mix_shell, :yes?, [^prompt]} + + file = File.read!(path) + + assert file == + """ + defmodule Polyn.Release do + def do_other_stuff do + end + + def polyn_migrate do + load_app() + {:ok, _apps} = Application.ensure_all_started(:polyn) + + dir = Path.join([:code.priv_dir(@app), "polyn", "migrations"]) + Polyn.Migration.Migrator.run(migrations_dir: dir) + end + end + """ + end +end diff --git a/polyn_elixir_client/test/polyn/migration/bucket_test.exs b/polyn_elixir_client/test/polyn/migration/bucket_test.exs index 07f575a..03452b9 100644 --- a/polyn_elixir_client/test/polyn/migration/bucket_test.exs +++ b/polyn_elixir_client/test/polyn/migration/bucket_test.exs @@ -29,6 +29,10 @@ defmodule Polyn.Migration.BucketTest do Application.get_env(:polyn, :source_root) ) + # wait for purge/delete to complete. The delete is async right now. + # Can remove this once it is fixed in Jetstream lib + :timer.sleep(500) + assert [] = Migration.Bucket.already_run_migrations(@bucket_name) end