Packages
aws
1.0.9
1.0.14
1.0.13
1.0.12
1.0.11
1.0.10
1.0.9
1.0.8
1.0.7
1.0.6
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
0.14.1
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.0
0.11.0
0.10.1
0.10.0
0.9.2
0.9.1
0.9.0
0.8.0
0.7.0
0.6.0
0.5.0
0.4.0
0.3.0
0.2.0
0.1.0
0.0.12
0.0.11
0.0.10
0.0.9
0.0.8
0.0.7
0.0.6
0.0.5
0.0.4
0.0.3
0.0.2
0.0.1
AWS clients for Elixir
Current section
Files
Jump to
Current section
Files
lib/aws/client.ex
defmodule AWS.Client do
@moduledoc """
Provides credentials and connection details for making requests to AWS services.
You can configure `access_key_id` and `secret_access_key` which are the credentials
needed by [IAM](https://aws.amazon.com/iam), and also the `region` for your services.
The list of regions can be found in the [AWS service endpoints](https://docs.aws.amazon.com/general/latest/gr/rande.html)
documentation. You can also use "local" to make requests to your localhost.
## Custom HTTP client
The option `http_client` accepts a tuple with a module and a list of options.
The module must implement the callback `c:AWS.HTTPClient.request/5`.
## Custom JSON or XML parsers
You can configure a custom JSON parser by using the option `json_module`. This
option accepts a tuple with a module and options. The given module must
implement the callbacks from `AWS.JSON`.
Similarly, there is a `xml_module` option that configures the XML parser. The
XML module must implement the callbacks from `AWS.XML`.
## Additional options
* `:session_token` - an option to set the `X-Amz-Security-Token` when performing
the requests.
* `:port` - is the port to use when making requests. Defaults to `443`.
* `:proto` - is the protocol to use. It can be "http" or "https". Defaults to `"https"`.
* `:endpoint` - the AWS endpoint. Defaults to `"amazonaws.com"`. You can configure this
using `put_endpoint/2` for AWS compatible APIs.
The `service` option is overwritten by each service with its signing name from metadata.
"""
@derive {Inspect, except: [:access_key_id, :secret_access_key, :session_token]}
defstruct access_key_id: nil,
secret_access_key: nil,
session_token: nil,
region: nil,
service: nil,
endpoint: nil,
proto: "https",
port: 443,
http_client: {AWS.HTTPClient.Hackney, []},
json_module: {AWS.JSON, []},
xml_module: {AWS.XML, []}
@typedoc """
The endpoint configuration.
Check `put_endpoint/2` for more details.
"""
@type endpoint_config :: binary() | {:keep_prefixes, binary()} | (map() -> binary()) | nil
@type t :: %__MODULE__{
access_key_id: binary() | nil,
secret_access_key: binary() | nil,
session_token: binary() | nil,
region: binary() | nil,
service: binary() | nil,
endpoint: endpoint_config(),
proto: binary(),
port: non_neg_integer(),
http_client: {module(), keyword()},
json_module: {module(), keyword()},
xml_module: {module(), keyword()}
}
@aws_default_endpoint "amazonaws.com"
@aws_access_key_id "AWS_ACCESS_KEY_ID"
@aws_secret_access_key "AWS_SECRET_ACCESS_KEY"
@aws_session_token "AWS_SESSION_TOKEN"
@aws_default_region "AWS_DEFAULT_REGION"
@doc """
The default endpoint.
Check `put_endpoint/2` for more details on how to configure
a custom endpoint.
"""
def default_endpoint, do: @aws_default_endpoint
def create() do
case System.get_env(@aws_default_region) do
nil -> raise RuntimeError, "missing default region"
region -> create(region)
end
end
def create(region) do
case {System.get_env(@aws_access_key_id), System.get_env(@aws_secret_access_key),
System.get_env(@aws_session_token)} do
{nil, _, _} ->
raise RuntimeError, "missing access key id"
{_, nil, _} ->
raise RuntimeError, "missing secret access key"
{access_key_id, secret_access_key, nil} ->
create(access_key_id, secret_access_key, region)
{access_key_id, secret_access_key, token} ->
create(access_key_id, secret_access_key, token, region)
end
end
def create(access_key_id, secret_access_key, region) do
%AWS.Client{
access_key_id: access_key_id,
secret_access_key: secret_access_key,
region: region
}
end
def create(access_key_id, secret_access_key, token, region) do
%AWS.Client{
access_key_id: access_key_id,
secret_access_key: secret_access_key,
session_token: token,
region: region
}
end
@doc """
Configures the endpoint to a custom one.
This is useful to set a custom endpoint when working with AWS
compatible APIs.
The following configuration is valid:
* `"example.com"`: this will be used as it is, without considering
regions or endpoint prefixes or account id.
* `{:keep_prefixes, "example.com"}`: it will keep the same rules from
AWS to build the prefixes.
For example, if region is "us-east-1" and the service prefix is "s3",
then the final endpoint will be `"s3.us-east-1.example.com"`
* `fn options -> "example.com" end`: a function that will be invoked
with options for that request. `options` is a map with the following
shape:
```
%{
endpoint: endpoint_config(),
region: binary() | nil,
service: binary(),
global?: boolean(),
endpoint_prefix: binary(),
account_id: binary() | nil
}
```
## Examples
iex> put_endpoint(%Client{}, "example.com")
%Client{endpoint: "example.com}
iex> put_endpoint(%Client{}, {:keep_prefixes, "example.com"})
%Client{endpoint: {:keep_prefixes, "example.com}}
iex> put_endpoint(%Client{}, fn opts -> Enum.join(["baz", opts.region, "foo.com"], ".") end)
%Client{endpoint: #Function<>}
"""
def put_endpoint(%__MODULE__{} = client, endpoint_config)
when is_binary(endpoint_config) or is_function(endpoint_config, 1) do
%{client | endpoint: endpoint_config}
end
def put_endpoint(%__MODULE__{} = client, {:keep_prefixes, endpoint} = endpoint_config)
when is_binary(endpoint) do
%{client | endpoint: endpoint_config}
end
@doc """
Configures the HTTP client used by a given client.
Switches to a different HTTP client, defined by a `{module, opts}` tuple. See
`AWS.HTTPClient` behavior for more details.
### Examples
iex> AWS.Client.put_http_client(%AWS.Client{}, {MyHTTPClient, []})
%AWS.Client{http_client: {MyHTTPClient, []}}
iex> AWS.Client.put_http_client(%AWS.Client{}, {AWS.HTTPClient.Finch, finch_name: AWS.Finch})
%AWS.Client{http_client: {AWS.HTTPClient.Finch, finch_name: AWS.Finch}}
"""
def put_http_client(%__MODULE__{} = client, http_client) do
%{client | http_client: http_client}
end
@doc """
Makes a HTTP request using the specified client.
## Retries and options.
The option `:enable_retries?` enables request retries on known errors such as 500s.
* `enable_retries?` - Defaults to `false`.
* `retry_opts` - the options to configure retries in case of errors. This uses exponential backoff with jitter.
* `:max_retries` - the maximum number of retries (plus the initial request). Defaults to `10`.
* `:base_sleep_time` - the base sleep time in milliseconds. Defaults to `5`.
* `:cap_sleep_time` - the maximum sleep time between attempts. Defaults to `5_000`.
See "FullJitter" at: https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
## Examples
iex> AWS.Client.request(client, :post, url, payload, headers, options)
{:ok, %{status_code: 200, body: body}}
iex> AWS.Client.request(client, :post, url, payload, headers, enable_retries?: true)
{:ok, %{status_code: 200, body: body}}
iex> AWS.Client.request(client, :post, url, payload, headers, enable_retries?: true, retry_opts: [max_retries: 3])
{:ok, %{status_code: 200, body: body}}
"""
def request(client, method, url, body, headers, opts \\ []) do
# Pop off all retry-related options from opts, so they aren't passed to the HTTP client.
{enable_retries?, opts} = Keyword.pop(opts, :enable_retries?, false)
{retry_num, opts} = Keyword.pop(opts, :retry_num, 0)
{retry_opts, opts} = Keyword.pop(opts, :retry_opts, [])
# Defaults for retry_opts
retry_opts =
Keyword.merge([max_retries: 10, base_sleep_time: 5, cap_sleep_time: 5_000], retry_opts)
# HTTP Client options
{mod, options} = Map.fetch!(client, :http_client)
options = Keyword.merge(options, opts)
resp = apply(mod, :request, [method, url, body, headers, options])
retriable?(resp)
|> case do
:ok ->
resp
:retry ->
if enable_retries? and should_retry?(retry_num, retry_opts) do
updated_opts =
Keyword.merge(opts,
retry_num: retry_num + 1,
enable_retries?: enable_retries?,
retry_opts: retry_opts
)
request(client, method, url, body, headers, updated_opts)
else
resp
end
:error ->
resp
end
end
def should_retry?(retry_num, retry_opts) do
max_retries = Keyword.fetch!(retry_opts, :max_retries)
if retry_num >= max_retries do
# The max-limit of retries has been reached. Give up.
false
else
# Sleep and retry
base_sleep_time = Keyword.fetch!(retry_opts, :base_sleep_time)
cap_sleep_time = Keyword.fetch!(retry_opts, :cap_sleep_time)
# This equivalent to "FullJitter" in https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
max_sleep_time =
min(cap_sleep_time, base_sleep_time * :math.pow(2, retry_num))
|> round()
Enum.random(0..max_sleep_time)
|> Process.sleep()
true
end
end
# Retry on 500 or 429
defp retriable?({:ok, %{status_code: status}}) when status >= 500 or status == 429, do: :retry
# Hackney specific
defp retriable?({:error, :closed}), do: :retry
defp retriable?({:error, :connect_timeout}), do: :retry
defp retriable?({:error, :checkout_timeout}), do: :retry
# Finch/Mint specific
defp retriable?({:error, %{reason: :closed}}), do: :retry
defp retriable?({:error, %{reason: :timeout}}), do: :retry
# Do not retry on other erors
defp retriable?({:error, _}), do: :error
defp retriable?({:ok, _}), do: :ok
defp retriable?({:ok, _, _}), do: :ok
def encode!(_client, payload, :query), do: AWS.Util.encode_query(payload)
def encode!(client, payload, format) do
{mod, opts} = Map.fetch!(client, String.to_existing_atom("#{format}_module"))
apply(mod, :encode_to_iodata!, [payload, opts])
end
def decode!(client, payload, format) do
{mod, opts} = Map.fetch!(client, String.to_existing_atom("#{format}_module"))
apply(mod, :decode!, [payload, opts])
end
end