From 9595404ca22305e7f7e29c2da2f5684d273b462d Mon Sep 17 00:00:00 2001 From: Curtis Vogt Date: Fri, 7 Jul 2023 23:01:11 -0500 Subject: [PATCH] Support data retrevial from Instance Metadata Service Version 2 (IMDSv2) (#647) --- Project.toml | 2 +- docs/make.jl | 1 + docs/src/imds.md | 16 +++ src/AWS.jl | 3 +- src/AWSCredentials.jl | 47 +------- src/AWSExceptions.jl | 10 +- src/IMDS.jl | 162 +++++++++++++++++++++++++ src/deprecated.jl | 3 + test/AWSCredentials.jl | 68 ++++------- test/IMDS.jl | 263 +++++++++++++++++++++++++++++++++++++++++ test/patch.jl | 16 +-- test/runtests.jl | 4 +- 12 files changed, 494 insertions(+), 101 deletions(-) create mode 100644 docs/src/imds.md create mode 100644 src/IMDS.jl create mode 100644 test/IMDS.jl diff --git a/Project.toml b/Project.toml index 51ea3388bd..2f7ad8af12 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "AWS" uuid = "fbe9abb3-538b-5e4e-ba9e-bc94f4f92ebc" license = "MIT" -version = "1.89.1" +version = "1.90.0" [deps] Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" diff --git a/docs/make.jl b/docs/make.jl index df0aa934c3..757c558125 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -71,6 +71,7 @@ makedocs(; "Home" => "index.md", "Backends" => "backends.md", "AWS" => "aws.md", + "IMDS" => "imds.md", "Services" => _generate_high_level_services_docs(), ], strict=true, diff --git a/docs/src/imds.md b/docs/src/imds.md new file mode 100644 index 0000000000..272e6f6793 --- /dev/null +++ b/docs/src/imds.md @@ -0,0 +1,16 @@ +# IMDS + +```@meta +CurrentModule = AWS +``` + +Provides a Julia interface for accessing AWS instance metadata. + +### Documentation + +```@docs +AWS.IMDS +AWS.IMDS.Session +AWS.IMDS.get +AWS.IMDS.region +``` diff --git a/src/AWS.jl b/src/AWS.jl index b774a9e70c..04d04475d2 100644 --- a/src/AWS.jl +++ b/src/AWS.jl @@ -18,7 +18,7 @@ using XMLDict export @service export _merge export AbstractAWSConfig, AWSConfig, AWSExceptions, AWSServices, Request -export ec2_instance_metadata, ec2_instance_region +export IMDS export assume_role, generate_service_url, global_aws_config, set_user_agent export sign!, sign_aws2!, sign_aws4! export JSONService, RestJSONService, RestXMLService, QueryService, set_features @@ -31,6 +31,7 @@ include("AWSExceptions.jl") include("AWSCredentials.jl") include("AWSConfig.jl") include("AWSMetadata.jl") +include("IMDS.jl") include(joinpath("utilities", "request.jl")) include(joinpath("utilities", "response.jl")) diff --git a/src/AWSCredentials.jl b/src/AWSCredentials.jl index a868f91140..2f6a883c6b 100644 --- a/src/AWSCredentials.jl +++ b/src/AWSCredentials.jl @@ -212,45 +212,6 @@ function check_credentials(aws_creds::AWSCredentials; force_refresh::Bool=false) end check_credentials(aws_creds::Nothing) = aws_creds -""" - ec2_instance_metadata(path::AbstractString) -> Union{String, Nothing} - -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 -- `path`: The URI path to used to specify that metadata to return -""" -function ec2_instance_metadata(path::AbstractString) - uri = HTTP.URI(; scheme="http", host="169.254.169.254", path=path) - request = try - @mock HTTP.request("GET", uri; connect_timeout=1) - catch e - if e isa HTTP.ConnectError - nothing - else - rethrow() - end - end - - 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() = - try - ec2_instance_metadata("/latest/meta-data/placement/region") - catch - nothing - end - """ ec2_instance_credentials(profile::AbstractString) -> AWSCredentials @@ -269,13 +230,13 @@ function ec2_instance_credentials(profile::AbstractString) source == "Ec2InstanceMetadata" || return nothing end - info = ec2_instance_metadata("/latest/meta-data/iam/info") + info = IMDS.get("/latest/meta-data/iam/info") info === nothing && return nothing info = JSON.parse(info) # Get credentials for the role associated to the instance via instance profile. - name = ec2_instance_metadata("/latest/meta-data/iam/security-credentials/") - creds = ec2_instance_metadata("/latest/meta-data/iam/security-credentials/$name") + name = IMDS.get("/latest/meta-data/iam/security-credentials/") + creds = IMDS.get("/latest/meta-data/iam/security-credentials/$name") parsed = JSON.parse(creds) instance_profile_creds = AWSCredentials( parsed["AccessKeyId"], @@ -695,7 +656,7 @@ function aws_get_region(; profile=nothing, config=nothing, default=DEFAULT_REGIO @something( get(ENV, "AWS_DEFAULT_REGION", nothing), get(_aws_profile_config(config, profile), "region", nothing), - ec2_instance_region(), + @mock(IMDS.region()), Some(default), ) end diff --git a/src/AWSExceptions.jl b/src/AWSExceptions.jl index f21631879e..a62a51733a 100644 --- a/src/AWSExceptions.jl +++ b/src/AWSExceptions.jl @@ -5,7 +5,15 @@ using JSON using XMLDict using XMLDict: XMLDictElement -export AWSException, ProtocolNotDefined, InvalidFileName, NoCredentials +export AWSException, IMDSUnavailable, ProtocolNotDefined, InvalidFileName, NoCredentials + +struct IMDSUnavailable <: Exception end + +function Base.show(io::IO, e::IMDSUnavailable) + msg = "$IMDSUnavailable: The Instance Metadata Service is unavailable on the host" + println(io, msg) + return nothing +end struct ProtocolNotDefined <: Exception message::String diff --git a/src/IMDS.jl b/src/IMDS.jl new file mode 100644 index 0000000000..a26e6d93b5 --- /dev/null +++ b/src/IMDS.jl @@ -0,0 +1,162 @@ +""" + IMDS + +Front-end for retrieving AWS instance metadata via the Instance Metadata Service (IMDS). For +details on available metadata see the official AWS documentation on: +["Instance metadata and user data"](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html). + +The IMDS module supports instances using either IMDSv1 or IMDSv2 (preferring IMDSv2 for +security reasons). +""" +module IMDS + +using ..AWSExceptions: IMDSUnavailable + +using HTTP: HTTP +using HTTP.Exceptions: ConnectError, StatusError +using Mocking + +const IPv4_ADDRESS = "169.254.169.254" +const DEFAULT_DURATION = 600 # 5 minutes, in seconds + +mutable struct Session + token::String + duration::Int16 + expiration::Int64 +end + +const _SESSION = Ref{Session}() + +function __init__() + _SESSION[] = Session() + return nothing +end + +""" + Session(; duration=$DEFAULT_DURATION) + +An IMDS `Session` which retains the IMDSv2 token over multiple requests. When IMDSv2 is +unavailable the session switches to IMDSv1 mode and avoids future requests for IMDSv2 +tokens. + +# Keywords +- `duration` (optional): Requested session duration, in seconds, for the IMDSv2 token. Can + be a minimum of one second and a maximum of six hours (21600). +""" +Session(; duration=DEFAULT_DURATION) = Session("", duration, 0) + +token_expired(session::Session; drift=10) = time() - session.expiration - drift > 0 + +function refresh_token!(session::Session, duration::Integer=session.duration) + t = floor(Int64, time()) + headers = ["X-aws-ec2-metadata-token-ttl-seconds" => string(duration)] + + # For IMDSv2, you must use `/latest/api/token` when retrieving the token instead of a + # version specific path. + # https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html#imds-considerations + uri = HTTP.URI(; scheme="http", host=IPv4_ADDRESS, path="/latest/api/token") + r = _http_request("PUT", uri, headers; status_exception=false) + + # Store the session token when we receive an HTTP 200. If we receive an HTTP 404 assume + # that the server is only supports IMDSv1. Otherwise "rethrow" the `StatusError`. + if r.status == 200 + session.token = String(r.body) + session.duration = duration + session.expiration = t + duration + elseif r.status == 404 + session.duration = 0 + session.expiration = typemax(Int64) # Use IMDSv1 indefinitely + else + # Could also populate the `StatusError` via `r.request.method` and + # `r.request.target` however `r.request` may not be populated under test scenarios. + throw(StatusError(r.status, "PUT", uri.path, r)) + end + + return session +end + +function request(session::Session, method::AbstractString, path::AbstractString; kwargs...) + # Attempt to generate token for use with IMDSv2. If we're unable to generate a token + # we'll fall back on using IMDSv1. We prefer using IMDSv2 as instances can be configured + # to disable IMDSv1 access: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-IMDS-new-instances.html#configure-IMDS-new-instances + token_expired(session) && refresh_token!(session) + headers = Pair{String,String}[] + !isempty(session.token) && push!(headers, "X-aws-ec2-metadata-token" => session.token) + + # Only using the IPv4 endpoint as the IPv6 endpoint has to be explicitly enabled and + # does not disable IPv4 support. + # https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-IMDS-new-instances.html#configure-IMDS-new-instances-ipv4-ipv6-endpoints + uri = HTTP.URI(; scheme="http", host=IPv4_ADDRESS, path) + return _http_request(method, uri, headers; kwargs...) +end + +function _http_request(args...; status_exception=true, kwargs...) + response = try + # Always throw status exceptions so we can determine if the IMDS service is available + @mock HTTP.request( + args...; connect_timeout=1, retry=false, kwargs..., status_exception=true + ) + catch e + # When running outside of an EC2 instance the link-local address will be unavailable + # and connections will fail. On EC2 instances where IMDS is disabled a HTTP 403 is + # returned. + # https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html#instance-metadata-returns + if e isa ConnectError || e isa StatusError && e.status == 403 + throw(IMDSUnavailable()) + + #! format: off + # Return the status exception when `status_exception=false`. We must always cause + # `HTTP.request` to throw status errors for our `IMDSUnavailable` check. + #! format: on + elseif !status_exception && e isa StatusError + e.response + else + rethrow() + end + end + + return response +end + +""" + get([session::Session], path::AbstractString) -> Union{String, Nothing} + +Retrieve the AWS instance metadata from the provided HTTP `path`. If no instance metadata is +available (due to the instance metadata service being disabled or not being run from within +an EC2 instance) then `nothing` will be returned. For details on available metadata see the +official AWS documentation on: +["Instance metadata and user data"](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html). + +# Arguments +- `session` (optional): The IMDS `Session` used to store the IMDSv2 token. +- `path`: The HTTP path to used to specify the metadata to return. +""" +function get(session::Session, path::AbstractString) + response = try + request(session, "GET", path) + catch e + if e isa IMDSUnavailable + nothing + else + rethrow() + end + end + + return !isnothing(response) ? String(response.body) : nothing +end + +get(path::AbstractString) = get(_SESSION[], path) + +""" + region([session::Session]) -> Union{String, Nothing} + +Determine the AWS region of the machine executing this code if running inside of an EC2 +instance, otherwise `nothing` is returned. + +# Arguments +- `session` (optional): The IMDS `Session` used to store the IMDSv2 token. +""" +region(session::Session) = get(session, "/latest/meta-data/placement/region") +region() = region(_SESSION[]) + +end diff --git a/src/deprecated.jl b/src/deprecated.jl index 1072941af1..091214c228 100644 --- a/src/deprecated.jl +++ b/src/deprecated.jl @@ -207,3 +207,6 @@ function legacy_response( return (return_headers ? (body, response.headers) : body) end end + +@deprecate ec2_instance_metadata(path::AbstractString) IMDS.get(path) +@deprecate ec2_instance_region() IMDS.region() diff --git a/test/AWSCredentials.jl b/test/AWSCredentials.jl index 15d7f1f252..3d9b2c9142 100644 --- a/test/AWSCredentials.jl +++ b/test/AWSCredentials.jl @@ -44,32 +44,6 @@ 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 "aws_get_profile_settings" begin @testset "no profile" begin @test aws_get_profile_settings("foo", Inifile()) === nothing @@ -563,6 +537,7 @@ end session_token="TOK_WEB", ), Patches.sso_service_patches("AKI_SSO", "SAK_SSO"), + Patches._imds_region_patch(nothing), ] withenv( @@ -943,7 +918,7 @@ end session_token=session_token, role_arn=role_arn, ) - ec2_metadata_patch = @patch function HTTP.request(method::String, url; kwargs...) + ec2_metadata_patch = @patch function HTTP.request(method, url, args...; kwargs...) url = string(url) security_credentials = test_values["Security-Credentials"] @@ -1310,9 +1285,12 @@ end @testset "unknown profile" begin withenv("AWS_DEFAULT_REGION" => nothing) do - @test aws_get_region(; config=ini, profile="unknown") == AWS.DEFAULT_REGION - @test aws_get_region(; config=config_file, profile="unknown") == - AWS.DEFAULT_REGION + apply(Patches._imds_region_patch(nothing)) do + @test aws_get_region(; config=ini, profile="unknown") == + AWS.DEFAULT_REGION + @test aws_get_region(; config=config_file, profile="unknown") == + AWS.DEFAULT_REGION + end end withenv( @@ -1320,18 +1298,22 @@ end "AWS_CONFIG_FILE" => config_file, "AWS_PROFILE" => "unknown", ) do - @test aws_get_region() == AWS.DEFAULT_REGION + apply(Patches._imds_region_patch(nothing)) do + @test aws_get_region() == AWS.DEFAULT_REGION + end end end @testset "default keyword" begin default = nothing withenv("AWS_DEFAULT_REGION" => nothing) do - @test aws_get_region(; config=ini, profile="unknown", default=default) === - default - @test aws_get_region(; - config=config_file, profile="unknown", default=default - ) === default + apply(Patches._imds_region_patch(nothing)) do + @test aws_get_region(; config=ini, profile="unknown", default) === + default + @test aws_get_region(; + config=config_file, profile="unknown", default + ) === default + end end withenv( @@ -1339,23 +1321,23 @@ end "AWS_CONFIG_FILE" => config_file, "AWS_PROFILE" => "unknown", ) do - @test aws_get_region(; default=default) === default + apply(Patches._imds_region_patch(nothing)) do + @test aws_get_region(; default=default) === default + end end end @testset "no such config file" begin withenv("AWS_DEFAULT_REGION" => nothing, "AWS_CONFIG_FILE" => tempname()) do - @test aws_get_region() == AWS.DEFAULT_REGION + apply(Patches._imds_region_patch(nothing)) do + @test aws_get_region() == AWS.DEFAULT_REGION + end end end @testset "instance profile" begin withenv("AWS_DEFAULT_REGION" => nothing, "AWS_CONFIG_FILE" => tempname()) do - patch = @patch function HTTP.request(method::String, url; kwargs...) - return HTTP.Response("ap-atlantis-1") # Made up region - end - - apply(patch) do + apply(Patches._imds_region_patch("ap-atlantis-1")) do @test aws_get_region() == "ap-atlantis-1" end end diff --git a/test/IMDS.jl b/test/IMDS.jl new file mode 100644 index 0000000000..ac61bcc123 --- /dev/null +++ b/test/IMDS.jl @@ -0,0 +1,263 @@ +struct Route + method::String + path::String + handler +end + +function register!(router::HTTP.Router, route::Route) + return HTTP.register!(router, route.method, route.path, route.handler) +end + +function Router(routes) + router = HTTP.Router() + for route in routes + register!(router, route) + end + return router +end + +function token_route(token) + handler = function (req::HTTP.Request) + ttl_secs = HTTP.header(req, "X-aws-ec2-metadata-token-ttl-seconds", nothing) + if !isnothing(ttl_secs) + HTTP.Response(200, token) + else + HTTP.Response(400) # Behavior when required header is missing + end + end + + return Route("PUT", "/latest/api/token", handler) +end + +function secure_route(route::Route, token) + wrapper = function (req::HTTP.Request) + if HTTP.header(req, "X-aws-ec2-metadata-token", nothing) == token + route.handler(req) + else + HTTP.Response(401) # Behavior when IMDSv2 is required + end + end + + return Route(route.method, route.path, wrapper) +end + +function response_route(method, path, response::HTTP.Response) + handler = function (req::HTTP.Request) + return HTTP.Response( + response.version, response.status, response.headers, response.body, req + ) + end + return Route(method, path, handler) +end + +# Use Mocking to re-route requests to 169.254.169.254 without having to actually start an +# HTTP.jl server. Should result in faster running tests. +function _imds_patch(router::HTTP.Router=HTTP.Router(); listening=true, enabled=true) + patch = @patch function HTTP.request( + method, url, headers=[], body=HTTP.nobody; status_exception=true, kwargs... + ) + uri = HTTP.URI(url) + if uri.host != "169.254.169.254" + error("Internal error: Unexpected HTTP call to non-IMDS service: $url") + end + + request = HTTP.Request(method, uri.path, headers, body) + response = if listening && enabled + router(request) + elseif listening && !enabled + HTTP.Response(403) + else + connect_timeout = HTTP.ConnectionPool.ConnectTimeout(uri.host, uri.port) + throw(HTTP.Exceptions.ConnectError(string(uri), connect_timeout)) + end + + if status_exception && response.status >= 300 + ex = HTTP.Exceptions.StatusError( + response.status, request.method, request.target, response + ) + throw(ex) + end + return response + end +end + +@testset "IMDS" begin + @testset "refresh_token!" begin + # Running outside of an EC2 instance + apply(_imds_patch(; listening=false)) do + session = IMDS.Session() + @test isempty(session.token) + @test session.duration == IMDS.DEFAULT_DURATION + @test IMDS.token_expired(session) + + @test_throws IMDSUnavailable IMDS.refresh_token!(session) + end + + # Running on an EC2 instance where IMDS is disabled + apply(_imds_patch(; enabled=false)) do + session = IMDS.Session() + @test_throws IMDSUnavailable IMDS.refresh_token!(session) + end + + # IMDS is non-functional + router = Router([response_route("PUT", "/latest/api/token", HTTP.Response(500))]) + apply(_imds_patch(router)) do + session = IMDS.Session() + @test_throws HTTP.Exceptions.StatusError IMDS.refresh_token!(session) + end + + # IMDSv1 is available + router = Router([response_route("PUT", "/latest/api/token", HTTP.Response(404))]) + apply(_imds_patch(router)) do + session = IMDS.Session() + @test IMDS.refresh_token!(session) === session + @test isempty(session.token) + @test session.duration == 0 + @test session.expiration == typemax(Int64) + end + + # IMDSv2 is available + token = "foo" + router = Router([token_route(token)]) + apply(_imds_patch(router)) do + session = IMDS.Session(; duration=60) + t = floor(Int64, time()) + @test IMDS.refresh_token!(session) === session + @test session.token == token + @test session.duration == 60 + @test 0 <= session.expiration - (t + session.duration) <= 5 + end + end + + @testset "request" begin + instance_id = "123" + path = "/latest/meta-data/instance-id" + + # Running outside of an EC2 instance + apply(_imds_patch(; listening=false)) do + session = IMDS.Session() + @test_throws IMDSUnavailable IMDS.request(session, "GET", path) + end + + # Running on an EC2 instance where IMDS is disabled + apply(_imds_patch(; enabled=false)) do + session = IMDS.Session() + @test_throws IMDSUnavailable IMDS.request(session, "GET", path) + end + + # Requested metadata is missing + router = Router([response_route("GET", path, HTTP.Response(500))]) + apply(_imds_patch(router)) do + session = IMDS.Session() + @test_throws HTTP.Exceptions.StatusError IMDS.request(session, "GET", path) + end + + # Requested metadata available via IMDSv1 + router = Router([response_route("GET", path, HTTP.Response(instance_id))]) + apply(_imds_patch(router)) do + session = IMDS.Session() + r = IMDS.request(session, "GET", path) + @test r isa HTTP.Response + @test r.status == 200 + @test String(r.body) == instance_id + @test isempty(session.token) + end + + # Requested metadata available via IMDSv2 + token = "token" + router = Router([ + token_route(token), + secure_route(response_route("GET", path, HTTP.Response(instance_id)), token), + ]) + apply(_imds_patch(router)) do + session = IMDS.Session() + r = IMDS.request(session, "GET", path) + @test r isa HTTP.Response + @test r.status == 200 + @test String(r.body) == instance_id + @test session.token == token + end + + # Invalid token used with IMDSv2 + router = Router([ + token_route("good"), + secure_route(response_route("GET", path, HTTP.Response(instance_id)), "bad"), + ]) + apply(_imds_patch(router)) do + session = IMDS.Session() + r = IMDS.request(session, "GET", path; status_exception=false) + @test r isa HTTP.Response + @test r.status == 401 + end + + # Unlikely scenario where the instance metadata services has switched over from + # IMDSv2 being optional to required while a long running Julia service on that + # instance has session which is set to use IMDSv1 indefinitely. + # TODO: We may want to have the code automatically attempt a token refresh when this + # occurs but I doubt this scenario will occur in scenario will occur in reality as + # instances cannot be configured to use IMDSv1 only. + token = "token" + router = Router([ + token_route(token), + secure_route(response_route("GET", path, HTTP.Response(instance_id)), token), + ]) + apply(_imds_patch(router)) do + # Emulate a pre-existing session where IMDSv2 was not available. + session = IMDS.Session("", 60, typemax(Int64)) + + # Request attempts to use IMDSv1 but now only IMDSv2 is enabled + r = IMDS.request(session, "GET", path; status_exception=false) + @test r isa HTTP.Response + @test r.status == 401 + end + end + + @testset "get" begin + instance_id = "123" + path = "/latest/meta-data/instance-id" + + # Running outside of an EC2 instancee + apply(_imds_patch(; listening=false)) do + session = IMDS.Session() + @test IMDS.get(session, path) === nothing + end + + # Requested metadata available via IMDSv1 + router = Router([response_route("GET", path, HTTP.Response(instance_id))]) + apply(_imds_patch(router)) do + session = IMDS.Session() + @test IMDS.get(session, path) == instance_id + end + end + + @testset "region" begin + region = "ap-atlantis-1" # Made up region + path = "/latest/meta-data/placement/region" + + # Running outside of an EC2 instance + apply(_imds_patch(; listening=false)) do + session = IMDS.Session() + @test IMDS.region(session) === nothing + end + + # Running on a webserver which doesn't understand our requests and returns HTTP 404. + # This exact scenario occurs in GHA CI and can be reproduced locally with the + # `aws-vault exec --ec2-server` which provides a very limited implementation of + # IMDSv1. + router = Router([ + response_route("PUT", "/**", HTTP.Response(404)), + response_route("GET", "/**", HTTP.Response(404)), + ]) + apply(_imds_patch(router)) do + session = IMDS.Session() + @test_throws HTTP.Exceptions.StatusError IMDS.region(session) + end + + # Requested metadata available via IMDSv1 + router = Router([response_route("GET", path, HTTP.Response(region))]) + apply(_imds_patch(router)) do + session = IMDS.Session() + @test IMDS.region(session) == region + end + end +end diff --git a/test/patch.jl b/test/patch.jl index 7315533f91..af72b60b68 100644 --- a/test/patch.jl +++ b/test/patch.jl @@ -147,17 +147,6 @@ _github_tree_patch = @patch function tree(repo, tree_obj; kwargs...) end end -_instance_metadata_timeout_patch = @patch function HTTP.request( - method::String, url; kwargs... -) - return throw( - HTTP.ConnectError( - "http://169.254.169.254/latest/meta-data/iam/info", - HTTP.ConnectionPool.ConnectTimeout("169.254.169.254", "80"), - ), - ) -end - # This patch causes `HTTP.request` to return all of its keyword arguments # except `require_ssl_verification` and `response_stream`. This is used to # test which other options are being passed to `HTTP.Request` inside of @@ -240,4 +229,9 @@ function sso_service_patches(access_key_id, secret_access_key) return [p, _sso_access_token_patch] end + +function _imds_region_patch(region) + return @patch IMDS.region() = region +end + end diff --git a/test/runtests.jl b/test/runtests.jl index 08f078a042..e57feba480 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,7 @@ using AWS using AWS: AWSCredentials, AWSServices, assume_role_creds -using AWS.AWSExceptions: AWSException, InvalidFileName, NoCredentials, ProtocolNotDefined +using AWS.AWSExceptions: + AWSException, IMDSUnavailable, InvalidFileName, NoCredentials, ProtocolNotDefined using AWS.AWSMetadata: ServiceFile, _clean_documentation, @@ -63,6 +64,7 @@ testset_role(role_name) = "AWS.jl-$role_name" @testset "Backend: $(nameof(backend))" for backend in backends AWS.DEFAULT_BACKEND[] = backend() include("AWS.jl") + include("IMDS.jl") include("AWSCredentials.jl") include("role.jl") include("issues.jl")