Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve ec2_instance_metadata and create ec2_instance_region #349

Merged
merged 4 commits into from
Apr 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ XMLDict = "228000da-037f-5747-90a9-8195ccbf91a5"
[compat]
Compat = "3"
GitHub = "5"
HTTP = "0.8, 0.9"
HTTP = "0.9.6"
IniFile = "0.5"
JSON = "0.18, 0.19, 0.20, 0.21"
MbedTLS = "0.6, 0.7, 1"
Expand Down
1 change: 1 addition & 0 deletions src/AWS.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import URIs

export @service
export _merge, AbstractAWSConfig, AWSConfig, AWSExceptions, AWSServices, Request
export ec2_instance_metadata, ec2_instance_region
export global_aws_config, generate_service_url, set_user_agent, sign!, sign_aws2!, sign_aws4!
export JSONService, RestJSONService, RestXMLService, QueryService

Expand Down
47 changes: 28 additions & 19 deletions src/AWSCredentials.jl
Original file line number Diff line number Diff line change
Expand Up @@ -291,48 +291,57 @@ end


"""
_ec2_metadata(metadata_endpoint::String) -> Union{String, Nothing}
ec2_instance_metadata(path::AbstractString) -> Union{String, Nothing}

Retrieve the EC2 meta data from the local AWS endpoint. Return the EC2 metadata request
body, or `nothing` if not running on an EC2 instance.
Retrieve the AWS EC2 instance metadata as a string using the provided `path`. If no instance
metadata is available (typically due to not running within an EC2 instance) then `nothing`
will be returned. See the AWS documentation for details on what metadata is available:
https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html

# Arguments
- `metadata_endpoint::String`: AWS internal meta data endpoint to hit
https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html#instance-metadata-ex-1

# Throws
- `StatusError`: If the response status is >= 300, except for 404
- `ParsingError`: Invalid HTTP request target
- `path`: The URI path to used to specify that metadata to return
"""
function _ec2_metadata(metadata_endpoint::String)
try
request = @mock HTTP.request("GET", "http://169.254.169.254/latest/meta-data/$metadata_endpoint")

return request === nothing ? nothing : String(request.body)
function ec2_instance_metadata(path::AbstractString)
mattBrzezinski marked this conversation as resolved.
Show resolved Hide resolved
uri = HTTP.URI(scheme="http", host="169.254.169.254", path=path)
request = try
@mock HTTP.request("GET", uri; connect_timeout=1)
mattBrzezinski marked this conversation as resolved.
Show resolved Hide resolved
catch e
e isa HTTP.IOError || e isa HTTP.StatusError && e.status == 404 || rethrow(e)
if e isa HTTP.ConnectionPool.ConnectTimeout
nothing
else
rethrow()
end
end

return nothing
return request !== nothing ? String(request.body) : nothing
end


"""
ec2_instance_region() -> Union{String, Nothing}

Determine the AWS region of the machine executing this code if running inside of an EC2
instance, otherwise `nothing` is returned.
"""
ec2_instance_region() = ec2_instance_metadata("/latest/meta-data/placement/region")


"""
ec2_instance_credentials() -> AWSCredentials

Parse the EC2 metadata to retrieve AWSCredentials.
"""
function ec2_instance_credentials()
info = _ec2_metadata("iam/info")
info = ec2_instance_metadata("/latest/meta-data/iam/info")

if info === nothing
return nothing
end

info = JSON.parse(info)

name = _ec2_metadata("iam/security-credentials/")
creds = _ec2_metadata("iam/security-credentials/$name")
name = ec2_instance_metadata("/latest/meta-data/iam/security-credentials/")
creds = ec2_instance_metadata("/latest/meta-data/iam/security-credentials/$name")
new_creds = JSON.parse(creds)

expiry = DateTime(rstrip(new_creds["Expiration"], 'Z'))
Expand Down
61 changes: 38 additions & 23 deletions test/AWSCredentials.jl
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,32 @@ end
@test AWS._role_session_name("a" ^ 22, "b" ^ 22, "c" ^ 22) == "a" ^ 22 * "b" ^ 20 * "c" ^ 22
end

@testset "ec2_instance_metadata" begin
@testset "connect timeout" begin
apply(Patches._instance_metadata_timeout_patch) do
@test AWS.ec2_instance_metadata("/latest/meta-data") === nothing
end
end
end

@testset "ec2_instance_region" begin
@testset "available" begin
patch = @patch function HTTP.request(method::String, url; kwargs...)
return HTTP.Response("ap-atlantis-1") # Made up region
end

apply(patch) do
@test AWS.ec2_instance_region() == "ap-atlantis-1"
end
end

@testset "unavailable" begin
apply(Patches._instance_metadata_timeout_patch) do
@test AWS.ec2_instance_region() === nothing
end
end
end

@testset "AWSCredentials" begin
@testset "Defaults" begin
creds = AWSCredentials("access_key_id" ,"secret_key")
Expand Down Expand Up @@ -328,9 +354,10 @@ end
"Security-Credentials" => "Test-Security-Credentials"
)

_http_request_patch = @patch function HTTP.request(method::String, url::String)
_http_request_patch = @patch function HTTP.request(method::String, url; kwargs...)
security_credentials = test_values["Security-Credentials"]
uri = test_values["URI"]
url = string(url)

if url == "http://169.254.169.254/latest/meta-data/iam/info"
instance_profile_arn = test_values["InstanceProfileArn"]
Expand Down Expand Up @@ -533,31 +560,19 @@ end
end

@testset "Credentials Not Found" begin
_http_request_patch = @patch function HTTP.request(method::String, url::String)
return nothing
end

ACCESS_KEY = "AWS_ACCESS_KEY_ID"
CONTAINER_URI = "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"

old_aws_access_key_id = get(ENV, ACCESS_KEY, "")
old_container_uri = get(ENV, CONTAINER_URI, "")

try
delete!(ENV, "AWS_ACCESS_KEY_ID")
delete!(ENV, "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")
patches = [
@patch HTTP.request(method::String, url; kwargs...) = nothing
Patches._cred_file_patch
Patches._config_file_patch
]

apply([_http_request_patch, Patches._cred_file_patch, Patches._config_file_patch]) do
withenv(
"AWS_ACCESS_KEY_ID" => nothing,
"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" => nothing,
) do
apply(patches) do
@test_throws NoCredentials AWSConfig()
end
finally
if !isempty(old_aws_access_key_id)
ENV[ACCESS_KEY] = old_aws_access_key_id
end

if !isempty(old_container_uri)
ENV[CONTAINER_URI] = old_container_uri
end
end
end

Expand Down
4 changes: 4 additions & 0 deletions test/patch.jl
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,8 @@ _github_tree_patch = @patch function tree(repo, tree_obj; kwargs...)
end
end

_instance_metadata_timeout_patch = @patch function HTTP.request(method::String, url; kwargs...)
throw(HTTP.ConnectionPool.ConnectTimeout("169.254.169.254", "80"))
end

end