Last active
January 27, 2025 00:38
-
-
Save James-E-A/c296b416339cf5ce1edb7c94eb84c84e to your computer and use it in GitHub Desktop.
Elixir lazy upsert storing SECRET_KEY_BASE in Ecto
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule Foo.Application do | |
@moduledoc false | |
use Application | |
@impl true | |
def start(_type, _args) do | |
children = [ | |
FooWeb.Telemetry, | |
Foo.Repo, | |
{Ecto.Migrator, | |
repos: Application.fetch_env!(:fooApp, :ecto_repos), | |
skip: skip_migrations?()}, | |
#{DNSCluster, query: Application.get_env(:fooApp, :dns_cluster_query) || :ignore}, | |
#{Phoenix.PubSub, name: Foo.PubSub}, | |
#{DynamicSupervisor, name: Foo.RoomSupervisor}, | |
FooWeb.Endpoint |> Foo.Util.defer_spec( | |
secret_key_base: {:ecto_simple, Foo.Repo, Foo.Repo.Schemas.Secret, &FooWeb.Util.phx_gen_secret/0}) | |
] | |
opts = [strategy: :one_for_one, name: Foo.Supervisor] | |
Supervisor.start_link(children, opts) | |
end | |
@impl true | |
def config_change(changed, _new, removed) do | |
FooWeb.Endpoint.config_change(changed, removed) | |
:ok | |
end | |
defp skip_migrations?() do | |
# By default, sqlite migrations are run when using a release | |
System.get_env("RELEASE_NAME") != nil | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule Foo.Util do | |
require Ecto.Query | |
# https://github.com/elixir-ecto/ecto/blob/v3.12.5/lib/ecto/repo/queryable.ex#L161 | |
@spec one_or_insert_lazy!(Ecto.Repo.t(), %Ecto.Query{from: s, select: t}, (-> s)) :: t when s: Ecto.Schema.t(), t: term() | |
def one_or_insert_lazy!(repo, query, fun) do | |
repo.transaction(fn r -> | |
case r.all(query) do | |
[value] -> | |
value | |
[] -> | |
new_record = fun.() | |
r.insert!(new_record) | |
case r.all(query) do | |
[value] -> | |
value | |
[] -> | |
raise ArgumentError, "changeset doesn't fulfill query (will be rolled back): #{inspect [struct_or_changeset: new_record, query: query]}" | |
other -> | |
raise ArgumentError, "changeset produced multiple results for query (will be rolled back): #{inspect [struct_or_changeset: new_record, query: query, result: other]}" | |
end | |
other -> | |
# https://github.com/elixir-ecto/ecto/blob/v3.12.5/lib/ecto/repo/queryable.ex#L165 | |
raise Ecto.MultipleResultsError, queryable: query, count: length(other) | |
end | |
end) | |
end | |
# https://elixirforum.com/t/managing-secret-key-base-without-kubernetes-docker-etc/67926/20?u=james_e | |
@spec defer_spec(Supervisor.module_spec() | Supervisor.child_spec(), keyword()) :: Supervisor.child_spec() | |
def defer_spec(child_spec, deferred_opts) | |
def defer_spec(module, deferred_opts) when is_atom(module) do | |
module.child_spec([]) | |
|> defer_spec(deferred_opts) | |
end | |
def defer_spec({module, arg}, deferred_opts) when is_atom(module) do | |
module.child_spec(arg) | |
|> defer_spec(deferred_opts) | |
end | |
def defer_spec(%{id: _, start: {_, _, _} = start} = child_spec, deferred_opts) do | |
%{child_spec | start: { | |
__MODULE__, | |
:_start_with_deferred_opts, | |
[start, deferred_opts] | |
}} | |
end | |
def defer_spec(_, _) do | |
raise ArgumentError, "child_spec" | |
end | |
defp eval_deferred_opts(opts \\ [], deferred_opts) do | |
access = if is_list(opts), do: Keyword, else: Access | |
Enum.reduce(deferred_opts, opts, fn | |
{key, fun}, acc when is_function(fun, 0) -> | |
value = fun.() | |
access.put(acc, key, value) | |
{key, fun}, acc when is_function(fun, 1) -> | |
access.get_and_update(acc, key, &{&1, fun.(&1)}) | |
|> elem(1) | |
{key, {:get_and_update, fun}}, acc when is_function(fun, 1) -> | |
access.get_and_update(acc, key, fun) | |
|> elem(1) | |
{key, {:ecto, repo, query, fun}}, acc when is_function(fun, 0) -> | |
access.get_and_update(acc, key, fn | |
value when not is_nil(value) -> | |
{value, value} | |
nil -> | |
value = one_or_insert_lazy!(repo, query, fun) | |
{nil, value} | |
end) | |
|> elem(1) | |
{key, {:ecto_simple, repo, schema, fun}}, acc when is_function(fun, 0) -> | |
access.get_and_update(acc, key, fn | |
value when not is_nil(value) -> | |
{value, value} | |
nil -> | |
key = Atom.to_string(key) | |
value = one_or_insert_lazy!( | |
repo, | |
Ecto.Query.from( | |
s in schema, | |
where: s.name == ^key, | |
select: s.value | |
), | |
fn -> struct( | |
schema, | |
name: key, | |
value: fun.() | |
) end | |
) | |
{nil, value} | |
end) | |
|> elem(1) | |
end) | |
end | |
def _start_with_deferred_opts({m, f, a}, deferred_opts) do | |
a = case a do | |
[_ | _] = a -> List.update_at(a, -1, &eval_deferred_opts(&1, deferred_opts)) | |
[] -> [eval_deferred_opts(deferred_opts)] | |
end | |
# https://github.com/elixir-lang/elixir/blob/v1.17.3/lib/elixir/lib/task/supervisor.ex#L539 | |
apply(m, f, a) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment