diff --git a/lib/mix/tasks/phx.gen.release.ex b/lib/mix/tasks/phx.gen.release.ex index 402de20134..b55d8a46f5 100644 --- a/lib/mix/tasks/phx.gen.release.ex +++ b/lib/mix/tasks/phx.gen.release.ex @@ -35,6 +35,8 @@ defmodule Mix.Tasks.Phx.Gen.Release do use Mix.Task + require Logger + @doc false def run(args) do opts = parse_args(args) @@ -54,8 +56,6 @@ defmodule Mix.Tasks.Phx.Gen.Release do binding = [ app_namespace: app_namespace, otp_app: app, - elixir_vsn: System.version(), - otp_vsn: otp_vsn(), assets_dir_exists?: File.dir?("assets") ] @@ -73,10 +73,7 @@ defmodule Mix.Tasks.Phx.Gen.Release do end if opts.docker do - Mix.Phoenix.copy_from(paths(), "priv/templates/phx.gen.release", binding, [ - {:eex, "Dockerfile.eex", "Dockerfile"}, - {:eex, "dockerignore.eex", ".dockerignore"} - ]) + gen_docker(binding) end File.chmod!("rel/overlays/bin/server", 0o755) @@ -189,6 +186,90 @@ defmodule Mix.Tasks.Phx.Gen.Release do end end + defp ecto_sql_installed?, do: Mix.Project.deps_paths() |> Map.has_key?(:ecto_sql) + + @debian "bullseye" + defp gen_docker(binding) do + elixir_vsn = System.version() + otp_vsn = otp_vsn() + + url = + "https://hub.docker.com/v2/namespaces/hexpm/repositories/elixir/tags?name=#{elixir_vsn}-erlang-#{otp_vsn}-debian-#{@debian}-" + + debian_vsn = + fetch_body!(url) + |> Phoenix.json_library().decode!() + |> Map.fetch!("results") + |> Enum.find_value(:error, fn %{"name" => name} -> + if String.ends_with?(name, "-slim") do + %{"vsn" => vsn} = Regex.named_captures(~r/.*debian-#{@debian}-(?.*)-slim/, name) + {:ok, vsn} + end + end) + + case debian_vsn do + {:ok, debian_vsn} -> + binding = + Keyword.merge(binding, + debian: @debian, + debian_vsn: debian_vsn, + elixir_vsn: elixir_vsn, + otp_vsn: otp_vsn + ) + + Mix.Phoenix.copy_from(paths(), "priv/templates/phx.gen.release", binding, [ + {:eex, "Dockerfile.eex", "Dockerfile"}, + {:eex, "dockerignore.eex", ".dockerignore"} + ]) + + :error -> + raise "unable to fetch supported Docker image for Elixir #{elixir_vsn} and Erlang #{otp_vsn}" + end + end + + defp fetch_body!(url) do + url = String.to_charlist(url) + Logger.debug("Fetching latest image information from #{url}") + + {:ok, _} = Application.ensure_all_started(:inets) + {:ok, _} = Application.ensure_all_started(:ssl) + + if proxy = System.get_env("HTTP_PROXY") || System.get_env("http_proxy") do + Logger.debug("Using HTTP_PROXY: #{proxy}") + %{host: host, port: port} = URI.parse(proxy) + :httpc.set_options([{:proxy, {{String.to_charlist(host), port}, []}}]) + end + + if proxy = System.get_env("HTTPS_PROXY") || System.get_env("https_proxy") do + Logger.debug("Using HTTPS_PROXY: #{proxy}") + %{host: host, port: port} = URI.parse(proxy) + :httpc.set_options([{:https_proxy, {{String.to_charlist(host), port}, []}}]) + end + + # https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/inets + http_options = [ + ssl: [ + verify: :verify_peer, + cacertfile: String.to_charlist(CAStore.file_path()), + depth: 3, + customize_hostname_check: [ + match_fun: :public_key.pkix_verify_hostname_match_fun(:https) + ], + versions: protocol_versions() + ] + ] + + case :httpc.request(:get, {url, []}, http_options, body_format: :binary) do + {:ok, {{_, 200, _}, _headers, body}} -> body + other -> raise "couldn't fetch #{url}: #{inspect(other)}" + end + end + + defp protocol_versions do + otp_major_vsn = :erlang.system_info(:otp_release) |> List.to_integer() + if otp_major_vsn < 25, do: [:"tlsv1.2"], else: [:"tlsv1.2", :"tlsv1.3"] + end + def otp_vsn do major = to_string(:erlang.system_info(:otp_release)) path = Path.join([:code.root_dir(), "releases", major, "OTP_VERSION"]) @@ -202,6 +283,4 @@ defmodule Mix.Tasks.Phx.Gen.Release do "#{major}.0" end end - - defp ecto_sql_installed?, do: Mix.Project.deps_paths() |> Map.has_key?(:ecto_sql) end diff --git a/mix.exs b/mix.exs index 560a757cb1..57420885e6 100644 --- a/mix.exs +++ b/mix.exs @@ -32,7 +32,9 @@ defmodule Phoenix.MixProject do :ranch, :cowboy_req, Plug.Cowboy.Conn, - Plug.Cowboy + Plug.Cowboy, + :httpc, + :public_key ] ], elixirc_paths: elixirc_paths(Mix.env()), @@ -73,6 +75,8 @@ defmodule Phoenix.MixProject do {:telemetry, "~> 0.4 or ~> 1.0"}, {:phoenix_pubsub, "~> 2.0"}, {:phoenix_view, "~> 1.0"}, + # TODO drop castore when we require OTP 25+ + {:castore, ">= 0.0.0"}, # Optional deps {:plug_cowboy, "~> 2.2", optional: true}, diff --git a/priv/templates/phx.gen.release/Dockerfile.eex b/priv/templates/phx.gen.release/Dockerfile.eex index a50d4e923d..003c62ec90 100644 --- a/priv/templates/phx.gen.release/Dockerfile.eex +++ b/priv/templates/phx.gen.release/Dockerfile.eex @@ -8,13 +8,13 @@ # This file is based on these images: # # - https://hub.docker.com/r/hexpm/elixir/tags - for the build image -# - https://hub.docker.com/_/debian?tab=tags&page=1&name=bullseye-20220801-slim - for the release image +# - https://hub.docker.com/_/debian?tab=tags&page=1&name=<%= debian %>-<%= debian_vsn %>-slim - for the release image # - https://pkgs.org/ - resource for finding needed packages -# - Ex: hexpm/elixir:<%= elixir_vsn %>-erlang-<%= otp_vsn %>-debian-bullseye-20210902-slim +# - Ex: hexpm/elixir:<%= elixir_vsn %>-erlang-<%= otp_vsn %>-debian-<%= debian %>-<%= debian_vsn %>-slim # ARG ELIXIR_VERSION=<%= elixir_vsn %> ARG OTP_VERSION=<%= otp_vsn %> -ARG DEBIAN_VERSION=bullseye-20220801-slim +ARG DEBIAN_VERSION=<%= debian %>-<%= debian_vsn %>-slim ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}" ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}" diff --git a/test/mix/tasks/phx.gen.release_test.exs b/test/mix/tasks/phx.gen.release_test.exs index 5f162b74c4..30386eba21 100644 --- a/test/mix/tasks/phx.gen.release_test.exs +++ b/test/mix/tasks/phx.gen.release_test.exs @@ -5,6 +5,8 @@ defmodule Mix.Tasks.Phx.Gen.ReleaseTest do import MixHelper alias Mix.Tasks.Phx.Gen + @moduletag :capture_log + setup do Mix.Task.clear() :ok