diff --git a/.github/workflows/stage.yml b/.github/workflows/stage.yml index 9a8c57ab..893adb2e 100644 --- a/.github/workflows/stage.yml +++ b/.github/workflows/stage.yml @@ -3,7 +3,7 @@ name: Publish upgrade artifacts to staging on: push: branches: - - main + - release_stage env: INCLUDE_ERTS: true MIX_ENV: prod @@ -16,7 +16,14 @@ jobs: packages: write id-token: write steps: - - uses: actions/checkout@v4 + - name: Checkout code + uses: actions/checkout@v4 + - name: Set upgrade variables + run: | + elixir ./deploy/upgrade/handler.exs all_keys ./deploy/upgrade/staging.config + echo "RELEASE_FROM=$(elixir ./deploy/upgrade/handler.exs upgrade_from ./deploy/upgrade/staging.config)" >> $GITHUB_ENV + echo "RELEASE_TO=$(elixir ./deploy/upgrade/handler.exs upgrade_to ./deploy/upgrade/staging.config)" >> $GITHUB_ENV + echo "NAME=$(elixir ./deploy/upgrade/handler.exs name ./deploy/upgrade/staging.config)" >> $GITHUB_ENV - name: Setup Elixir run: | . ~/.asdf/asdf.sh @@ -26,7 +33,27 @@ jobs: - name: Set up Rust uses: ATiltedTree/setup-rust@v1 with: - rust-version: stable + rust-version: stable + - name: Get git tags + run: git fetch --tags origin + - name: Checkout RELEASE_FROM + run: git checkout v${{ env.RELEASE_FROM }} + - name: Cache Mix + uses: actions/cache@v3 + with: + path: deps + key: ${{ runner.os }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} + restore-keys: | + ${{ runner.os }}-mix- + - name: Install dependencies + run: | + mix local.hex --force + mix local.rebar --force + mix deps.get + - name: Make old release + run: mix release supavisor + - name: Checkout RELEASE_TO + run: git checkout v${{ env.RELEASE_TO }} - name: Cache Mix uses: actions/cache@v3 with: @@ -39,10 +66,15 @@ jobs: mix local.hex --force mix local.rebar --force mix deps.get - - name: Make release - run: mix release supavisor + - name: Clean up old release + run: | + rm -rf ./_build/${{ env.MIX_ENV }}/lib/supavisor + rm ./_build/${{ env.MIX_ENV }}/rel/supavisor/releases/COOKIE + rm ./_build/${{ env.MIX_ENV }}/supavisor-${{ env.RELEASE_FROM }}.tar.gz + - name: Make upgrade release + run: RELEASE_COOKIE=${{ secrets.RELEASE_COOKIE_STAGE }} UPGRADE_FROM=${{ env.RELEASE_FROM }} mix release supavisor - name: Create tarball - run: cd _build/prod/rel/ && tar -czvf ${{ secrets.TARBALL_REGIONS_STAGE }}_supavisor_v$(cat ../../../VERSION)_$(date "+%s").tar.gz supavisor + run: cd _build/${{ env.MIX_ENV }} && mv supavisor-${{ env.RELEASE_TO }}.tar.gz "${{ env.NAME }}_$(date "+%s").tar.gz" - name: configure aws credentials - staging uses: aws-actions/configure-aws-credentials@v4 with: @@ -50,4 +82,4 @@ jobs: aws-region: "us-east-1" - name: Deploy to S3 shell: bash - run: aws s3 sync ./_build/prod/rel/ ${{ secrets.TARBALLS_PATH_STAGE }} --exclude '*' --include '*tar.gz' + run: aws s3 sync ./_build/${{ env.MIX_ENV }} ${{ secrets.TARBALLS_PATH_STAGE }} --exclude '*' --include '*tar.gz' diff --git a/VERSION b/VERSION index 2818446a..a2f3bf51 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.40 +1.1.41 diff --git a/config/config.exs b/config/config.exs index 5eb444bb..7255b2ec 100644 --- a/config/config.exs +++ b/config/config.exs @@ -9,7 +9,8 @@ import Config config :supavisor, ecto_repos: [Supavisor.Repo], - version: Mix.Project.config()[:version] + version: Mix.Project.config()[:version], + env: Mix.env() # Configures the endpoint config :supavisor, SupavisorWeb.Endpoint, diff --git a/deploy/upgrade/handler.exs b/deploy/upgrade/handler.exs new file mode 100644 index 00000000..3ce1ac23 --- /dev/null +++ b/deploy/upgrade/handler.exs @@ -0,0 +1,23 @@ +defmodule Handler do + def main([key, config]) do + {:ok, [config]} = :file.consult(config) + + case key do + "all_keys" -> + IO.write(inspect(config, pretty: true)) + "name" -> + regions = Enum.join(config[:regions], "&") + + IO.write( + "supavisor_#{config[:type]}_#{regions}_#{config[:upgrade_from]}_#{config[:upgrade_to]}" + ) + + key -> + IO.write(config[String.to_atom(key)]) + end + + end +end + + +Handler.main(System.argv()) diff --git a/deploy/upgrade/prod.config b/deploy/upgrade/prod.config new file mode 100644 index 00000000..46424d5e --- /dev/null +++ b/deploy/upgrade/prod.config @@ -0,0 +1,9 @@ +[ + {upgrade_from, "x.x.x"}, + {upgrade_to, "x.x.x"}, + % list() | [:all] + {regions, ["ap-southeast-1"]}, + % :soft | :hard + {type, soft} +]. +% supavisor_soft_all-1_1.1.13_1.1.39 \ No newline at end of file diff --git a/deploy/upgrade/staging.config b/deploy/upgrade/staging.config new file mode 100644 index 00000000..46424d5e --- /dev/null +++ b/deploy/upgrade/staging.config @@ -0,0 +1,9 @@ +[ + {upgrade_from, "x.x.x"}, + {upgrade_to, "x.x.x"}, + % list() | [:all] + {regions, ["ap-southeast-1"]}, + % :soft | :hard + {type, soft} +]. +% supavisor_soft_all-1_1.1.13_1.1.39 \ No newline at end of file diff --git a/lib/supavisor/client_handler.ex b/lib/supavisor/client_handler.ex index 4bf51ed4..c0f78851 100644 --- a/lib/supavisor/client_handler.ex +++ b/lib/supavisor/client_handler.ex @@ -76,7 +76,12 @@ defmodule Supavisor.ClientHandler do @impl true def handle_event(:info, {_proto, _, <<"GET", _::binary>>}, :exchange, data) do Logger.debug("ClientHandler: Client is trying to request HTTP") - HH.sock_send(data.sock, "HTTP/1.1 204 OK\r\n\r\n") + + HH.sock_send( + data.sock, + "HTTP/1.1 204 OK\r\nx-app-version: #{Application.spec(:supavisor, :vsn)}\r\n\r\n" + ) + {:stop, {:shutdown, :http_request}} end diff --git a/lib/supavisor/hot_upgrade.ex b/lib/supavisor/hot_upgrade.ex new file mode 100644 index 00000000..5f9a316f --- /dev/null +++ b/lib/supavisor/hot_upgrade.ex @@ -0,0 +1,59 @@ +defmodule Supavisor.HotUpgrade do + @moduledoc false + + @type app :: atom + @type version_str :: String.t() + @type path_str :: String.t() + @type change :: :soft | {:advanced, [term]} + @type dep_mods :: [module] + @type appup_ver :: charlist | binary + @type instruction :: + {:add_module, module} + | {:delete_module, module} + | {:update, module, :supervisor | change} + | {:update, module, change, dep_mods} + | {:load_module, module} + | {:load_module, module, dep_mods} + | {:apply, {module, atom, [term]}} + | {:add_application, atom} + | {:remove_application, atom} + | {:restart_application, atom} + | :restart_new_emulator + | :restart_emulator + @type upgrade_instructions :: [{appup_ver, instruction}] + @type downgrade_instructions :: [{appup_ver, instruction}] + @type appup :: {appup_ver, upgrade_instructions, downgrade_instructions} + + @spec up(app(), version_str(), version_str(), [appup()], any()) :: [appup()] + def up(_app, _from_vsn, to_vsn, appup, _transform), + do: [{:apply, {Supavisor.HotUpgrade, :apply_runtime_config, [to_vsn]}} | appup] + + @spec down(app(), version_str(), version_str(), [appup()], any()) :: [appup()] + def down(_app, from_vsn, _to_vsn, appup, _transform), + do: [{:apply, {Supavisor.HotUpgrade, :apply_runtime_config, [from_vsn]}} | appup] + + @spec apply_runtime_config(version_str()) :: any() + def apply_runtime_config(vsn) do + path = + if System.get_env("DEBUG_LOAD_RUNTIME_CONFIG"), + do: "config/runtime.exs", + else: "releases/#{vsn}/runtime.exs" + + if File.exists?(path) do + IO.write("Loading runtime.exs from releases/#{vsn}") + + for {app, config} <- + Config.Reader.read!(path, env: Application.get_env(:supavisor, :env)) do + updated_config = + Config.Reader.merge( + [{app, Application.get_all_env(app)}], + [{app, config}] + ) + + Application.put_all_env(updated_config) + end + else + IO.write("No runtime.exs found in releases/#{vsn}") + end + end +end diff --git a/lib/supavisor/monitoring/prom_ex.ex b/lib/supavisor/monitoring/prom_ex.ex index afccdefa..72f92f9a 100644 --- a/lib/supavisor/monitoring/prom_ex.ex +++ b/lib/supavisor/monitoring/prom_ex.ex @@ -37,7 +37,7 @@ defmodule Supavisor.Monitoring.PromEx do |> :ets.select_delete([{{{:_, meta}, :_}, [], [true]}]) end - @spec set_metrics_tags() :: :ok + @spec set_metrics_tags() :: map() def set_metrics_tags() do [_, host] = node() |> Atom.to_string() |> String.split("@") @@ -53,6 +53,7 @@ defmodule Supavisor.Monitoring.PromEx do end Application.put_env(:supavisor, :metrics_tags, metrics_tags) + metrics_tags end @spec short_node_id() :: String.t() | nil @@ -68,9 +69,13 @@ defmodule Supavisor.Monitoring.PromEx do @spec get_metrics() :: String.t() def get_metrics() do - def_tags = - Application.fetch_env!(:supavisor, :metrics_tags) - |> Enum.map_join(",", fn {k, v} -> "#{k}=\"#{v}\"" end) + metrics_tags = + case Application.fetch_env(:supavisor, :metrics_tags) do + :error -> set_metrics_tags() + {:ok, tags} -> tags + end + + def_tags = Enum.map_join(metrics_tags, ",", fn {k, v} -> "#{k}=\"#{v}\"" end) metrics = PromEx.get_metrics(__MODULE__) diff --git a/lib/tasks/gen.appup.ex b/lib/tasks/gen.appup.ex index 005206ed..692e04fe 100644 --- a/lib/tasks/gen.appup.ex +++ b/lib/tasks/gen.appup.ex @@ -32,7 +32,9 @@ defmodule Mix.Tasks.Supavisor.Gen.Appup do path_to = Path.join(lib_path, "supavisor-#{to_vsn}") appup_path = Path.join([path_to, "ebin", "supavisor.appup"]) - case Appup.make(:supavisor, from_vsn, to_vsn, path_from, path_to) do + transforms = [Supavisor.HotUpgrade] + + case Appup.make(:supavisor, from_vsn, to_vsn, path_from, path_to, transforms) do {:ok, appup} -> IO.puts("Writing appup to #{appup_path}") diff --git a/mix.exs b/mix.exs index 31d17286..66b539a4 100644 --- a/mix.exs +++ b/mix.exs @@ -77,7 +77,8 @@ defmodule Supavisor.MixProject do [ supavisor: [ steps: [:assemble, &upgrade/1, :tar], - include_erts: System.get_env("INCLUDE_ERTS", "true") == "true" + include_erts: System.get_env("INCLUDE_ERTS", "true") == "true", + cookie: System.get_env("RELEASE_COOKIE", Base.url_encode64(:crypto.strong_rand_bytes(30))) ], supavisor_bin: [ steps: [:assemble, &Burrito.wrap/1], @@ -118,7 +119,7 @@ defmodule Supavisor.MixProject do defp upgrade(release) do from = System.get_env("UPGRADE_FROM") - if from do + if from && from != "" do vsn = release.version path = Path.join([release.path, "releases", "supavisor-#{vsn}.rel"]) rel_content = File.read!(Path.join(release.version_path, "supavisor.rel")) diff --git a/test/integration/proxy_test.exs b/test/integration/proxy_test.exs index d842131b..2c12a9ac 100644 --- a/test/integration/proxy_test.exs +++ b/test/integration/proxy_test.exs @@ -170,7 +170,9 @@ defmodule Supavisor.Integration.ProxyTest do assert :httpc.request( "http://localhost:#{Application.get_env(:supavisor, :proxy_port_transaction)}" ) == - {:ok, {{'HTTP/1.1', 204, 'OK'}, [], []}} + {:ok, + {{'HTTP/1.1', 204, 'OK'}, [{'x-app-version', Application.spec(:supavisor, :vsn)}], + []}} end test "checks that client_handler is idle and db_pid is nil for transaction mode" do