defmodule ExAws.Config do
@moduledoc """
Generates the configuration for a service.
It starts with the defaults for a given environment
and then merges in the common config from the ex_aws config root,
and then finally any config specified for the particular service.
"""
# TODO: Add proper documentation?
@common_config [
:http_client,
:http_opts,
:json_codec,
:access_key_id,
:secret_access_key,
:debug_requests,
:region,
:security_token,
:retries,
:normalize_path,
:telemetry_event,
:telemetry_options
]
@type t :: %{} | Keyword.t()
@doc """
Builds a complete set of config for an operation.
1. Defaults are pulled from `ExAws.Config.Defaults`
2. Common values set via e.g `config :ex_aws` are merged in.
3. Keys set on the individual service e.g `config :ex_aws, :s3` are merged in
4. Finally, any configuration overrides are merged in
"""
@spec new(atom, keyword) :: map()
def new(service, opts \\ []) do
overrides = Map.new(opts)
service
|> build_base(overrides)
|> retrieve_runtime_config
|> parse_host_for_region
end
@doc """
Builds a minimal HTTP configuration.
"""
def http_config(service, opts \\ []) do
overrides = Map.new(opts)
build_base(service, overrides)
|> Map.take([:http_client, :http_opts, :json_codec])
|> retrieve_runtime_config
end
def build_base(service, overrides \\ %{}) do
common_config = Application.get_all_env(:ex_aws) |> Map.new() |> Map.take(@common_config)
service_config = Application.get_env(:ex_aws, service, []) |> Map.new()
region =
(Map.get(overrides, :region) ||
Map.get(service_config, :region) ||
Map.get(common_config, :region) ||
"us-east-1")
|> retrieve_runtime_value(%{})
defaults = ExAws.Config.Defaults.get(service, region)
defaults
|> Map.merge(common_config)
|> Map.merge(service_config)
|> Map.merge(overrides)
end
def retrieve_runtime_config(config) do
Enum.reduce(config, config, fn
{:host, host}, config ->
Map.put(config, :host, retrieve_runtime_value(host, config))
{:retries, retries}, config ->
Map.put(config, :retries, retries)
{:http_opts, http_opts}, config ->
Map.put(config, :http_opts, http_opts)
{:telemetry_event, telemetry_event}, config ->
Map.put(config, :telemetry_event, telemetry_event)
{:telemetry_options, telemetry_options}, config ->
Map.put(config, :telemetry_options, telemetry_options)
{:headers, headers}, config ->
Map.put(config, :headers, headers)
{k, v}, config ->
case retrieve_runtime_value(v, config) do
%{} = result -> Map.merge(config, result)
value -> Map.put(config, k, value)
end
end)
end
def retrieve_runtime_value({:system, env_key}, _) do
System.get_env(env_key)
end
def retrieve_runtime_value(:instance_role, config) do
config
|> ExAws.Config.AuthCache.get()
|> Map.take([:access_key_id, :secret_access_key, :security_token])
|> valid_map_or_nil
end
def retrieve_runtime_value({:awscli, profile, expiration}, _) do
ExAws.Config.AuthCache.get(profile, expiration * 1000)
|> Map.take([
:source_profile,
:role_arn,
:access_key_id,
:secret_access_key,
:region,
:security_token,
:role_session_name,
:external_id
])
|> valid_map_or_nil
end
def retrieve_runtime_value(values, config) when is_list(values) do
values
|> Stream.map(&retrieve_runtime_value(&1, config))
|> Enum.find(& &1)
end
def retrieve_runtime_value(value, _), do: value
def parse_host_for_region(%{host: {stub, host}, region: region} = config) do
Map.put(config, :host, String.replace(host, stub, region))
end
def parse_host_for_region(%{host: map, region: region} = config) when is_map(map) do
case Map.fetch(map, region) do
{:ok, host} -> Map.put(config, :host, host)
:error -> "A host for region #{region} was not found in host map #{inspect(map)}"
end
end
def parse_host_for_region(config), do: config
def awscli_auth_adapter, do: Application.get_env(:ex_aws, :awscli_auth_adapter, nil)
def awscli_auth_credentials(profile, credentials_ini_provider \\ ExAws.CredentialsIni.File) do
case Application.get_env(:ex_aws, :awscli_credentials, nil) do
nil ->
case credentials_ini_provider.security_credentials(profile) do
{:ok, creds} -> creds
{:error, err} -> raise "Recieved error while retrieving security credentials: #{err}"
end
%{^profile => profile_credentials} ->
profile_credentials
_otherwise ->
raise("Missing #{profile} in provided credentials.")
end
end
defp valid_map_or_nil(map) when map == %{}, do: nil
defp valid_map_or_nil(map), do: map
end