/
config.ex
231 lines (186 loc) · 6.48 KB
/
config.ex
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
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.
## Refreshable fields
Some fields are marked as refreshable. These fields will be fetched through
the auth cache even if they are passed in as overrides. This is so stale
credentials aren't used, for example, with long running streams.
This behaviour must be explicitly enabled by passing `refreshable: true` as an option
to Config.new/2
"""
# 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
]
@instance_role_config [
:access_key_id,
:secret_access_key,
:security_token
]
@awscli_config [
:source_profile,
:role_arn,
:access_key_id,
:secret_access_key,
:region,
:security_token,
:role_session_name,
:external_id
]
@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)
config =
defaults
|> Map.merge(common_config)
|> Map.merge(service_config)
|> add_refreshable_metadata(overrides)
# (Maybe) do not allow overrides for refreshable config.
overrides =
if refreshable = config[:refreshable] do
Enum.reduce(refreshable, overrides, fn
:awscli, overrides -> Map.drop(overrides, @awscli_config)
:instance_role, overrides -> Map.drop(overrides, @instance_role_config)
end)
else
overrides
end
Map.merge(config, overrides)
end
# :awscli and :instance_role both read creds from ExAws.Config.AuthCache which
# is "refreshable". This is useful for long running streams where the creds can
# change while the stream is still running.
defp add_refreshable_metadata(config, %{refreshable: true}) do
refreshable =
Enum.flat_map(config, fn {_k, v} -> List.wrap(v) end)
|> Enum.reduce([], fn
{:awscli, _, _}, acc -> [:awscli | acc]
:instance_role, acc -> [:instance_role | acc]
_, acc -> acc
end)
|> Enum.uniq()
if refreshable != [] do
Map.put(config, :refreshable, refreshable)
else
config
end
end
defp add_refreshable_metadata(config, _overrides) do
config
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)
{:refreshable, refreshable}, config ->
Map.put(config, :refreshable, refreshable)
{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(@instance_role_config)
|> valid_map_or_nil
end
def retrieve_runtime_value({:awscli, profile, expiration}, _) do
ExAws.Config.AuthCache.get(profile, expiration * 1000)
|> Map.take(@awscli_config)
|> 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