%%------------------------------------------------------------------------------ %% @doc erllambda_util %% %% Utilities for the Erllambda Environment %% %% @copyright 2018 Alert Logic, Inc. %%------------------------------------------------------------------------------ -module(erllambda_util). -export([region/0, environ/0, accountid/0, config/0, config/1, config/2]). -export([to_list/1, to_binary/1]). %%****************************************************************************** %% Includes %%****************************************************************************** -include("erllambda.hrl"). -include_lib("erlcloud/include/erlcloud_aws.hrl"). -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). -endif. %%****************************************************************************** %% API Functions %%****************************************************************************** %%------------------------------------------------------------------------------ -spec region() -> region() | undefined. %%------------------------------------------------------------------------------ %% @doc Access current AWS region %% %% This function will resolve the region in which we are currently executing %% for all IWS execution environments, including AWS EC2 instance, IWS ECS %% Containers, AWS Lambda Functions, and Jenkins/development tests. The %% region value is located in the following locations, in order: %% %% %% region() -> case erllambda_config_srv:get( region ) of {ok, Result} -> Result; {error, not_found} -> Generate = fun() -> region_() end, erllambda_config_srv:serialize( region, Generate ) end. region_() -> Functions = [fun region_devenv/0, fun region_config/0, fun region_env/0, fun region_meta/0, fun region_cli/0], Region = lists:foldl( fun( Function, undefined ) -> Function(); ( _, Result ) -> Result end, undefined, Functions ), erllambda_config_srv:set( region, Region, undefined ), Region. region_devenv() -> case os:getenv( "REGION" ) of false -> undefined; Region -> list_to_binary(Region) end. region_config() -> case application:get_env( erllambda, region ) of {ok, undefined} -> undefined; {ok, Region} when is_binary(Region) -> Region; {ok, Region} when is_list(Region) -> list_to_binary(Region) end. region_env() -> case os:getenv( "AWS_REGION" ) of false -> undefined; Region -> list_to_binary(Region) end. region_meta() -> case instance_doc() of {ok, #{<<"region">> := Region}} -> Region; Otherwise -> Otherwise end. region_cli() -> case string:tokens( os:cmd( "aws configure get region" ), "\n" ) of [Region] -> list_to_binary(Region); _ -> undefined end. %%------------------------------------------------------------------------------ -spec accountid() -> accountid() | undefined. %%------------------------------------------------------------------------------ %% @doc Access current AWS accountid %% %% This function will resolve the AWS AccountId in which we are currently %% executing for all IWS execution environments, including AWS EC2 instance, %% IWS ECS Containers, AWS Lambda Functions, and Jenkins/development tests. %% The accountid value is located in the following locations, in order: %% %% %% accountid() -> case erllambda_config_srv:get( accountid ) of {ok, Result} -> Result; {error, not_found} -> Generate = fun() -> accountid_() end, erllambda_config_srv:serialize( accountid, Generate ) end. accountid_() -> Functions = [fun accountid_config/0, fun accountid_meta/0, fun accountid_sts/0], AccountId = lists:foldl( fun( Function, undefined ) -> Function(); ( _, Result ) -> Result end, undefined, Functions ), erllambda_config_srv:set( accountid, AccountId, undefined ), AccountId. accountid_config() -> case application:get_env( erllambda, accountid ) of {ok, undefined} -> undefined; {ok, Accountid} when is_binary(Accountid) -> Accountid; {ok, Accountid} when is_list(Accountid) -> list_to_binary(Accountid) end. accountid_meta() -> case instance_doc() of {ok, #{<<"accountId">> := Accountid}} -> Accountid; Otherwise -> Otherwise end. accountid_sts() -> accountid_sts( config([sts]) ). accountid_sts( Config ) -> {ok, Proplists} = erlcloud_sts:get_caller_identity( Config ), to_binary( proplists:get_value( account, Proplists ) ). %%------------------------------------------------------------------------------ -spec environ() -> environ() | undefined. %%------------------------------------------------------------------------------ %% @doc Create a erllambba context. %% environ() -> case erllambda_config_srv:get( environ ) of {ok, Result} -> Result; {error, not_found} -> Generate = fun() -> environ_() end, erllambda_config_srv:serialize( environ, Generate ) end. environ_() -> Functions = [fun environ_env/0, fun environ_config/0], Environ = lists:foldl( fun( Function, undefined ) -> Function(); ( _, Result ) -> Result end, undefined, Functions ), erllambda_config_srv:set( environ, Environ, undefined ), Environ. environ_env() -> case os:getenv( "ENVIRON" ) of false -> undefined; Environ -> list_to_binary(Environ) end. environ_config() -> case application:get_env( erllambda, environ ) of {ok, undefined} -> undefined; {ok, Environ} when is_binary(Environ) -> Environ; {ok, Environ} when is_list(Environ) -> list_to_binary(Environ) end. %%------------------------------------------------------------------------------ -spec config() -> #aws_config{} | {error, term()}. %%------------------------------------------------------------------------------ %% @doc Generate and cache an erlcloud configuration %% %% This function will generate and cache an {@link %% erlcloud_aws:config(). erlcloud #aws_config{} record} for %% the current AWS execution by calling {@link region/0}. %% %% @see config/2 %% config() -> config( region(), [] ). %%------------------------------------------------------------------------------ -spec config( Services :: [service()] ) -> #aws_config{} | {error, term()}. %%------------------------------------------------------------------------------ %% @doc Generate and cache an erlcloud configuration %% %% This function will generate and cache an {@link %% erlcloud_aws:config(). erlcloud #aws_config{} record} for %% the current AWS execution by calling {@link region/0}, and will setup the %% regional endpoints for the list of services provided in the %% Services parameter. %% %% @see config/2 %% config( Services ) -> config( region(), [{services, Services}] ). %%------------------------------------------------------------------------------ -spec config( Region :: region(), Options :: [option()] ) -> #aws_config{} | undefined | {error, term()}. %%------------------------------------------------------------------------------ %% @doc Generate and cache an erlcloud configuration %% %% This function will generate and cache an {@link %% erlcloud_aws:config(). erlcloud #aws_config{} record} for %% the AWS Region specified. This value will be cached and %% automatically refreshed in the background, so it is safe to call this %% whenever a config is needed. %% %% Whether or not the config ultimately assumes an alternative role or not, %% the base credential information is sourced from %% erlcloud_aws:auto_config/1. These base credentials are then %% used directly, or as the basis for the assumed role. This function will %% conditionally source the profile name used from the environment variable %% PROFILE, and if not present, will request the %% default profile. %% %% The Options parameter is a property list containing tuple %% pairs that can be used to influence the construction of the {@link %% erlcloud_aws:config(). erlcloud #aws_config{} record}. The %% possible properties are as follows: %% %% %% config( Region, Options ) -> Key = erlang:phash2( {Region, Options} ), case erllambda_config_srv:get( Key ) of {ok, #aws_config{expiration = Expiration} = Result} when is_integer(Expiration) -> GraceExp = Expiration - ?AWSEVICT_SECS, case GraceExp > os:system_time(seconds) of true -> Result; false -> erllambda_config_srv:evict(Key), config(Region, Options) end; % unsure if config is expired {ok, Result} -> Result; {error, not_found} -> Generate = config_generate( Key, Region, Options ), erllambda_config_srv:serialize( Key, Generate ) end. config_generate( Key, Region, Options ) -> fun() -> Refresh = config_refresh( Key, Region, Options ), config_generate( Key, Region, Options, Refresh ) end. config_refresh( Key, Region, Options ) -> fun Refresh() -> config_generate( Key, Region, Options, Refresh ) end. config_generate( Key, Region, Options, Refresh ) -> Profile = list_to_atom( os:getenv( "PROFILE", "default" ) ), {ok, Config} = erlcloud_aws:auto_config( [{profile, Profile}] ), config_finalize( Key, Region, Config, Options, Refresh ). config_finalize( Key, Region, Config, Options, Refresh ) -> case {proplists:get_value( iam_role, Options ), proplists:get_value( iam_extid, Options )} of {undefined, undefined} -> config_finalize_( Key, Region, Config, Options, Refresh ); {undefined, _ExtId} -> {error, iam_role_required}; {Role, ExtId} -> config_assume( Role, ExtId, Key, Region, Config, Options, Refresh ) end. config_assume( Role, ExtId, Key, Region, Config, Options, Refresh ) -> Name = proplists:get_value( role_session_name, Options, "erllambda" ), {ok, DefaultDur} = application:get_env(erllambda, default_role_duration_sec), Secs = proplists:get_value( role_duration_secs, Options, DefaultDur), StsConfig = erlcloud_aws:service_config(<<"sts">>, Region, Config), try erlcloud_sts:assume_role( StsConfig, to_list(Role), Name, Secs, to_list(ExtId) ) of {NewConfig, _Creds} -> config_finalize_( Key, Region, NewConfig, Options, Refresh ) catch error:{aws_error, Reason} -> {error, Reason} end. config_finalize_( Key, Region, Config, Options, Refresh ) -> Services = proplists:get_value( services, Options, [] ), RegionConfig = config_regionalize_(Config, Region, Services), NewConfig = erlcloud_aws:default_config_override(RegionConfig), ExpireTsSec = context_config_expire( Config ), RefreshTsMs = (ExpireTsSec * 1000) - ?AWSEVICT_MSECS, erllambda_config_srv:set( Key, NewConfig, ExpireTsSec ), erllambda_config_srv:schedule( RefreshTsMs, Refresh ), NewConfig. config_regionalize_(Config, Region, []) -> erlcloud_aws:default_config_region(Config, Region); config_regionalize_(Config, Region, Services) -> lists:foldl( fun( Service, Cfg ) -> erlcloud_aws:service_config( Service, Region, Cfg ) end, Config, Services ). context_config_expire( #aws_config{expiration = undefined} ) -> {ok, DefaultDur} = application:get_env(erllambda, default_role_duration_sec), erlang:system_time( second ) + DefaultDur; context_config_expire( #aws_config{expiration = ExpirationTs} ) -> ExpirationTs. %%****************************************************************************** %% Internal Functions %%****************************************************************************** instance_doc() -> case erllambda_config_srv:get( instance_doc ) of {ok, Result} -> Result; {error, not_found} -> Generate = instance_doc_generate(), erllambda_config_srv:serialize( instance_doc, Generate ) end. instance_doc_generate() -> fun() -> case erllambda_config_srv:get( instance_doc ) of {ok, Result} -> Result; {error, not_found} -> instance_doc_() end end. instance_doc_refresh() -> fun() -> instance_doc_() end. instance_doc_() -> Url = "http://169.254.169.254/latest/dynamic/instance-identity/document", Headers = [{"accept", "*/*"}], case lhttpc:request( Url, get, Headers, <<>>, (10 * 1000), [] ) of {ok, {{200, _}, _RespHeaders, Body}} -> instance_doc( Body ); _Otherwise -> undefined end. instance_doc( Body ) -> try jsone:decode( Body ) of Json -> Result = {ok, Json}, ExpireTsMsec = erlang:system_time( millisecond ) + (900 * 1000), RefreshTsMsec = ExpireTsMsec - ?AWSEVICT_MSECS, erllambda_config_srv:set( instance_doc, Result, ExpireTsMsec ), erllambda_config_srv:schedule( RefreshTsMsec, instance_doc_refresh() ), Result catch _:_ -> undefined end. to_list( V ) when is_list(V) -> V; to_list( V ) when is_binary(V) -> binary_to_list(V); to_list( V ) when is_atom(V) -> atom_to_list(V); to_list( V ) when is_integer(V) -> integer_to_list(V); to_list( V ) when is_float(V) -> float_to_list(V); to_list( V ) -> V. to_binary(T) when is_pid(T) -> iolist_to_binary(pid_to_list(T)); to_binary(T) when is_binary(T) -> T; to_binary(T) when is_list(T) -> list_to_binary(T); to_binary(T) when is_atom(T) -> atom_to_binary(T, latin1). %%****************************************************************************** %% Test functions %%****************************************************************************** -ifdef(TEST). -endif.