Custom Configuration Providers¶
Distillery now provides the capability to implement custom configuration
providers for loading and persisting runtime configuration during boot. These
providers are able to replace the use of sys.config
and config.exs
if
desired.
Implementing a Provider¶
To implement a new configuration provider, a module must implement the
Mix.Releases.Config.Provider
behavior, which has a single callback, init/1
.
Config providers are executed in a pre-boot phase, in their own instance of the
VM, where all applications are loaded, and only kernel, stdlib, compiler, and
elixir are started. Providers may start any application needed to do config provisioning,
e.g. inets/crypto/ssl, as long as the application is part of the release.
Providers are expected to push configuration into the application environment
(e.g. using Application.put_env/3
). Once all providers have run, the
application environment will be dumped to a final sys.config file, which is
subsequently used when booting the “real” release.
Warning
Since config providers only execute pre-boot, you must restart the release to pick up configuration changes. If using hot upgrades, the config providers will run during the upgrade, and so will pick up any config changes at that time.
There are some important properties you must keep in mind when designing and implementing a provider:
- You have access to the code of all applications in the release, but, only
:kernel
,:stdlib
,:compiler
, and:elixir
are started when the provider’sinit
callback is invoked. - If you must start an application in order to accomplish some goal, at present,
you must manually start them with
Application.start/2
orApplication.ensure_all_started/1
- If you need to start an application to run the provider, and the application
needs configuration, you have a circular dependency problem. In order to solve
this, you need to have users provide the initial configuration via
sys.config
orconfig.exs
, that way you can bootstrap the provider.
Example: JSON¶
The following is a relatively simple example, which allows one to represent the
typical config.exs
structure in JSON, so given the following Mix config file:
1 2 3 4 5 6 7 | use Mix.Config config :myapp, port: 8080 config :myapp, :settings, foo: "bar" |
The JSON representation expected by this provider would be:
1 2 3 4 5 6 7 8 | { "myapp": { "port": 8080, "settings": { "foo": "bar" } } } |
The actual implementation looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | defmodule JsonConfigProvider do use Mix.Releases.Config.Provider def init([config_path]) do # Helper which expands paths to absolute form # and expands env vars in the path of the form `${VAR}` # to their value in the system environment config_path = Provider.expand_path(config_path) # All applications are already loaded at this point if File.exists?(config_path) do config_path |> Jason.decode! |> to_keyword() |> persist() else :ok end end defp to_keyword(config) when is_map(config) do for {k, v} <- config do k = String.to_atom(k) {k, to_keyword(config)} end end defp to_keyword(config), do: config defp persist(config) when is_map(config) do config = to_keyword(config) for {app, app_config} <- config do base_config = Application.get_all_env(app) merged = merge_config(base_config, app_config) for {k, v} <- merged do Application.put_env(app, k, v, persistent: true) end end :ok end defp merge_config(a, b) do Keyword.merge(a, b, fn _, app1, app2 -> Keyword.merge(app1, app2, &merge_config/3) end) end defp merge_config(_key, val1, val2) do if Keyword.keyword?(val1) and Keyword.keyword?(val2) do Keyword.merge(val1, val2, &merge_config/3) else val2 end end end |
See the module docs for Mix.Releases.Config.Provider
if you want to know more about the behaviour of these callbacks.