diff --git a/src/aws_credentials_eks.erl b/src/aws_credentials_eks.erl new file mode 100644 index 0000000..653b0ec --- /dev/null +++ b/src/aws_credentials_eks.erl @@ -0,0 +1,49 @@ +%% @doc This provider looks up credential information from EKS Pod Identity +%% @end +-module(aws_credentials_eks). +-behaviour(aws_credentials_provider). + +-define(AUTHORIZATION_HEADER, "authorization"). + +-export([fetch/1]). + +-spec fetch(aws_credentials_provider:options()) -> + {error, _} + | {ok, aws_credentials:credentials(), aws_credentials_provider:expiration()}. +fetch(_Options) -> + FullUri = os:getenv("AWS_CONTAINER_CREDENTIALS_FULL_URI"), + TokenFile = os:getenv("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE"), + AuthToken = read_token(TokenFile), + Response = fetch_session_token(FullUri, AuthToken), + make_map(Response). + +-spec read_token(false | string()) -> {error, _} | {ok, binary()}. +read_token(false) -> {error, no_credentials}; +read_token(Path) -> file:read_file(Path). + +-spec fetch_session_token(false | string(), {error, _} | {ok, string()}) -> + {error, _} + | {ok, aws_credentials_httpc:status_code(), + aws_credentials_httpc:body(), + aws_credentials_httpc:headers()}. +fetch_session_token(false, _AuthToken) -> {error, no_credentials}; +fetch_session_token(_FullUri, {error, _Error} = Error) -> Error; +fetch_session_token(FullUri, {ok, AuthToken}) -> + aws_credentials_httpc:request(get, FullUri, [{?AUTHORIZATION_HEADER, AuthToken}]). + + +-spec make_map({error, _} + | {ok, aws_credentials_httpc:status_code(), + aws_credentials_httpc:body(), + aws_credentials_httpc:headers()}) -> + {error, _} + | {ok, aws_credentials:credentials(), aws_credentials_provider:expiration()}. +make_map({error, _Error} = Error) -> Error; +make_map({ok, _Status, Body, _Headers}) -> + #{ <<"AccessKeyId">> := AccessKeyId + , <<"SecretAccessKey">> := SecretAccessKey + , <<"Token">> := Token + , <<"Expiration">> := Expiration + } = jsx:decode(Body), + Creds = aws_credentials:make_map(?MODULE, AccessKeyId, SecretAccessKey, Token), + {ok, Creds, Expiration}. diff --git a/src/aws_credentials_httpc.erl b/src/aws_credentials_httpc.erl index d41bb70..800f5c1 100644 --- a/src/aws_credentials_httpc.erl +++ b/src/aws_credentials_httpc.erl @@ -21,7 +21,7 @@ -type httpc_result() :: {status_line(), [header()], body()}. -type status_line() :: {http_version(), status_code(), reason_phrase()}. --type header() :: {string(), string()}. +-type header() :: {string(), binary() | string()}. -type body() :: binary(). -type http_version() :: string(). -type status_code() :: non_neg_integer(). diff --git a/src/aws_credentials_provider.erl b/src/aws_credentials_provider.erl index a3d95c9..cf6f062 100644 --- a/src/aws_credentials_provider.erl +++ b/src/aws_credentials_provider.erl @@ -36,6 +36,7 @@ | aws_credentials_file | aws_credentials_ecs | aws_credentials_ec2 + | aws_credentials_eks | module(). -type error_log() :: [{provider(), term()}]. -export_type([ options/0, expiration/0, provider/0, error_log/0 ]). @@ -48,7 +49,8 @@ -define(DEFAULT_PROVIDERS, [aws_credentials_env, aws_credentials_file, aws_credentials_ecs, - aws_credentials_ec2]). + aws_credentials_ec2, + aws_credentials_eks]). -spec fetch() -> {ok, aws_credentials:credentials(), expiration()} | diff --git a/test/aws_credentials_providers_SUITE.erl b/test/aws_credentials_providers_SUITE.erl index 5129c88..25715c7 100644 --- a/test/aws_credentials_providers_SUITE.erl +++ b/test/aws_credentials_providers_SUITE.erl @@ -35,6 +35,7 @@ all() -> , {group, env} , {group, application_env} , {group, ecs} + , {group, eks} , {group, credential_process} ]. @@ -48,6 +49,7 @@ groups() -> , {env, [], all_testcases()} , {application_env, [], all_testcases()} , {ecs, [], all_testcases()} + , {eks, [], all_testcases()} , {credential_process, [], all_testcases()} ]. @@ -118,6 +120,9 @@ assert_test(profile_env) -> assert_test(credential_process) -> Provider = provider(file), assert_values(?DUMMY_ACCESS_KEY2, ?DUMMY_SECRET_ACCESS_KEY2, Provider); +assert_test(eks) -> + Provider = provider(eks), + assert_values(?DUMMY_ACCESS_KEY, ?DUMMY_SECRET_ACCESS_KEY, Provider); assert_test(GroupName) -> Provider = provider(GroupName), assert_values(?DUMMY_ACCESS_KEY, ?DUMMY_SECRET_ACCESS_KEY, Provider). @@ -196,6 +201,18 @@ setup_provider(ecs, _Config) -> #{ mocks => [httpc] , env => [{"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", OldUri}] }; +setup_provider(eks, Config) -> + OldFullUri = os:getenv("AWS_CONTAINER_CREDENTIALS_FULL_URI"), + OldTokenFile = os:getenv("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE"), + os:putenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", "https://eks.amazonaws.com/dummy-uri"), + os:putenv("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE", ?config(data_dir, Config) ++ "eks/token"), + meck:new(httpc, [no_link, passthrough]), + meck:expect(httpc, request, fun mock_httpc_request_eks/5), + #{ mocks => [httpc] + , env => [ {"AWS_CONTAINER_CREDENTIALS_FULL_URI", OldFullUri} + , {"AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE", OldTokenFile} + ] + }; setup_provider(config_env, Config) -> Old = os:getenv("AWS_CONFIG_FILE"), os:putenv("AWS_CONFIG_FILE", ?config(data_dir, Config) ++ "env/config"), @@ -258,6 +275,14 @@ mock_httpc_request_ecs(Method, Request, HTTPOptions, Options, Profile) -> meck:passthrough([Method, Request, HTTPOptions, Options, Profile]) end. +mock_httpc_request_eks(Method, Request, HTTPOptions, Options, Profile) -> + case Request of + {"https://eks.amazonaws.com/dummy-uri", [{"authorization", <<"dummy-authorization-token">>}]} -> + {ok, response('eks-credentials')}; + _ -> + meck:passthrough([Method, Request, HTTPOptions, Options, Profile]) + end. + response(BodyTag) -> StatusLine = {unused, 200, unused}, Headers = [], @@ -277,6 +302,12 @@ body('dummy-role') -> body('document') -> jsx:encode(#{ 'region' => unused }); body('dummy-uri') -> + jsx:encode(#{ 'AccessKeyId' => ?DUMMY_ACCESS_KEY + , 'SecretAccessKey' => ?DUMMY_SECRET_ACCESS_KEY + , 'Expiration' => <<"2025-09-25T23:43:56Z">> + , 'Token' => unused + }); +body('eks-credentials') -> jsx:encode(#{ 'AccessKeyId' => ?DUMMY_ACCESS_KEY , 'SecretAccessKey' => ?DUMMY_SECRET_ACCESS_KEY , 'Expiration' => <<"2025-09-25T23:43:56Z">> diff --git a/test/aws_credentials_providers_SUITE_data/eks/token b/test/aws_credentials_providers_SUITE_data/eks/token new file mode 100644 index 0000000..1f712d8 --- /dev/null +++ b/test/aws_credentials_providers_SUITE_data/eks/token @@ -0,0 +1 @@ +dummy-authorization-token \ No newline at end of file