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. The one exception is if you need to configure the :kernel
application, you will need to do so via sys.config
, config.exs
, or vm.args
.
Implementing A Provider
To implement a new configuration provider, there are two callbacks you need to
implement currently, init/1
and get/1
.
The init/1
function is called during startup, before any applications are
started, with the exception of core applications required for things to work,
namely :kernel
, :stdlib
, :compiler
, and :elixir
. Everything is loaded in
a release, but you may still want to be explicit about loading applications just
in case the provider ends up running in a relaxed code loading environment.
Currently, get/1
is not used by anything directly, but you can use it yourself
to ask for a configured value, simply by calling
Mix.Releases.Config.Provider.get/1
. This will try all of the config providers
until one returns a non-nil value, or nil if no value was found. I am
considering some features which may take advantage of this API in the future,
but currently it is just a placeholder.
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
, and before returning frominit
, you must stop them withApplication.stop/1
. If you do not do this, the boot script will fail when attempting to start the release post-configuration. - 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.
In general, it is highly recommended to avoid starting applications as part of
your provider. The one exception to this is providers which may make HTTP
requests, as some applications are necessary to start in that case if using :httpc
,
namely :inets
, and possibly :crypto
and :ssl
. In a future release,
Distillery may make it possible to express application dependencies like this,
so that configuration can either be moved to take place after they are started,
or have Distillery manage their temporary lifecycle for you.
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:
use Mix.Config
config :myapp,
port: 8080
config :myapp, :settings,
foo: "bar"
The JSON representation expected by this provider would be:
{
"myapp": {
"port": 8080,
"settings": {
"foo": "bar"
}
}
}
The actual implementation looks like this:
defmodule JsonConfigProvider do
use Mix.Releases.Config.Provider
def init([config_path]) do
# 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.