How to access files in a Distillery release?

I asked a question here (Mariaex.Error) failed to upgraded socket: :enotconn yesterday, and finally made it work under dev environment with this config in dev.exs:

config :app, App.Repo,
  adapter: Ecto.Adapters.MySQL,
  ssl: true,
  ssl_opts: [cacertfile: Path.join(["priv", "ssl", "ca.pem"]), certfile: Path.join(["priv", "ssl", "client-cert.pem"]), keyfile: Path.join(["priv", "ssl", "client-key.pem"])]

But then I got another problem deploying my app with Distillery, since the path would be wrong in prod environment.

Here is what I do.

First, I build my app with distillery in docker https://github.com/bitwalker/alpine-elixir-phoenix, then deploy it in docker https://github.com/bitwalker/alpine-erlang. My app will run from /opt/app/bin/app start, then I got these error logs:

app        | 10:21:32.387 [error] Mariaex.Protocol (#PID<0.1286.0>) failed to connect: ** (Mariaex.Error) failed to upgraded socket: {:options, {:cacertfile, '/opt/app/priv/ssl/ca.pem', {:error, :enoent}}}

The application try to read those ca.pem files from /opt/app/priv/ssl/ folder, but they are not there. The /opt/app only has these:

I can find the ssl folder under /opt/app/lib/app-0.0.1/priv/, but I have no idea how to access it in my prod.secret.exs config file.

I have tried File.cwd!, it won’t work.

The other post Including data files in a Distillery release said I can access priv either with Application.app_dir(app_name, "priv/path/to/file") or :code.priv_dir(app):

"#{Application.app_dir(:app)}/priv/ssl/ca.pem"

But I would got error:

** (Mix.Config.LoadError) could not load config config/dev.exs
    ** (ArgumentError) unknown application: :app
    (elixir) lib/application.ex:428: Application.app_dir/1
    (stdlib) erl_eval.erl:670: :erl_eval.do_apply/6
    (stdlib) erl_eval.erl:269: :erl_eval.expr/5
    (stdlib) eval_bits.erl:81: :eval_bits.eval_field/3
    (stdlib) eval_bits.erl:65: :eval_bits.expr_grp/4
    (stdlib) erl_eval.erl:474: :erl_eval.expr/5
    (stdlib) erl_eval.erl:878: :erl_eval.expr_list/6
    (stdlib) erl_eval.erl:236: :erl_eval.expr/5

Seems like I can’t use :app here. I’m still new to elixir, and google won’t help anymore, so any help here are welcome. Thanks very much.

Distillery will evaluate config.exs and produce a static sys.config that is packaged with the release.

In this case, you want Application.app_dir(:app) to evaluate at runtime, not when generating the release. You could try passing the ssl_opts when starting the Repo under your supervisor, with.

cert_path = Path.join([Application.app_dir(:app), "priv/ssl"])
mysql_ssl_opts = [ssl_opts: [cacertfile: Path.join([cert_path, "ca.pem"]), certfile: Path.join([cert_path, "client-cert.pem"]), keyfile: Path.join([cert_path, "client-key.pem"])]]

children = [
...
supervisor(App.Repo, [mysql_ssl_opts])
...
] 

The custom opts will be merged with whatever is declared in config.exs: https://github.com/elixir-ecto/ecto/blob/master/lib/ecto/repo/supervisor.ex#L18

Thank you very much! It works. But it also apply to dev environment. I could work around this, but still wondering if there’s some better methods to work out this use case :grin:.

Though I never used it, this lib might address your need: https://github.com/mrluc/deferred_config

1 Like

The easiest way to configure this is using the init/2 repo callback introduced in ecto 2.1. This allows you to configure repo dynamically at runtime.

In your repo module, define the init/2 callback:

def init(_type, config) do
  cert_dir = Application.app_dir(:my_app, "priv/ssl")
  ssl_opts = [cacertfile: Path.join(cert_dir, "ca.pem"), 
              certfile: Path.join(cert_dir, "client-cert.pem"), 
              keyfile: Path.join(cert_dir, "client_key.pem")]
  {:ok, [ssl_opts: ssl_opts] ++ config}
end

Thanks, but I think it would apply to dev environment too? I only need secure database connection when code running in prod environment.

I could define an environment variable in prod.exs:

config :app, App.SSL, active: true

Then in repo.ex:

  def init(_type, config) do
    if Application.get_env(:app, App.SSL) |> Keyword.get(:active) do
      cert_dir = Application.app_dir(:app, "priv/ssl")
      ssl_opts = [cacertfile: Path.join(cert_dir, "ca.pem"), 
                certfile: Path.join(cert_dir, "client-cert.pem"), 
                keyfile: Path.join(cert_dir, "client_key.pem")]
      {:ok, [ssl: true, ssl_opts: ssl_opts] ++ config}
    else
      {:ok, config}
    end
  end

Do you think this is all right? I feel the code is a little mess now.

You could define it in the repo config:

config :app, Repo,
  # other config values
  ssl: true
def init(_type, config) do
  if config[:ssl] do
      cert_dir = Application.app_dir(:app, "priv/ssl")
      ssl_opts = [cacertfile: Path.join(cert_dir, "ca.pem"), 
                  certfile: Path.join(cert_dir, "client-cert.pem"), 
                  keyfile: Path.join(cert_dir, "client_key.pem")]
      {:ok, [ssl_opts: ssl_opts] ++ config}
  else
    {:ok, config}
  end
end
1 Like