From 660754394e901b726f3518fb1062c35893dea28f Mon Sep 17 00:00:00 2001 From: Alex Arslan Date: Fri, 1 Feb 2019 11:34:23 -0800 Subject: [PATCH 1/3] Make AWSConfig its own type Rather than defining `AWSConfig` as a `SymbolDict` with no restrictions on the contents, we can make it its own type, which gives more control over how things are stored and accessed. Fixes #53 --- src/AWSCore.jl | 201 +++++++++++++++++++++++++++--------------- src/AWSCredentials.jl | 33 +------ src/names.jl | 4 +- test/runtests.jl | 26 +++--- 4 files changed, 151 insertions(+), 113 deletions(-) diff --git a/src/AWSCore.jl b/src/AWSCore.jl index 0f66cc5..0bb8f53 100644 --- a/src/AWSCore.jl +++ b/src/AWSCore.jl @@ -22,17 +22,134 @@ using DataStructures: OrderedDict using JSON using LazyJSON +# NOTE: This needs to be defined before AWSConfig. Methods defined on AWSCredentials are +# in src/AWSCredentials.jl. +""" + AWSCredentials + +A type which holds AWS credentials. +When you interact with AWS, you specify your +[AWS Security Credentials](http://docs.aws.amazon.com/general/latest/gr/aws-security-credentials.html) +to verify who you are and whether you have permission to access the resources that you are +requesting. AWS uses the security credentials to authenticate and authorize your requests. + +The fields `access_key_id` and `secret_key` hold the access keys used to authenticate API +requests (see [Creating, Modifying, and Viewing Access +Keys](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html#Using_CreateAccessKey)). + +[Temporary Security Credentials](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp.html) +require the extra session `token` field. + +The `user_arn` and `account_number` fields are used to cache the result of the +[`aws_user_arn`](@ref) and [`aws_account_number`](@ref) functions. +The `AWSCredentials()` constructor tries to load local credentials from: + +* `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` + [environment variables](http://docs.aws.amazon.com/cli/latest/userguide/cli-environment.html), +* [`~/.aws/credentials`](http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html), or +* [EC2 Instance Credentials](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#instance-metadata-security-credentials). + +To specify the profile to use from `~/.aws/credentials`, do, for example, +`AWSCredentials(profile="profile-name")`. + +A `~/.aws/credentials` file can be created using the +[AWS CLI](https://aws.amazon.com/cli/) command `aws configrue`. +Or it can be created manually: + +```ini +[default] +aws_access_key_id = AKIAXXXXXXXXXXXXXXXX +aws_secret_access_key = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +``` + +If your `~/.aws/credentials` file contains multiple profiles you can pass the +profile name as a string to the `profile` keyword argument (`nothing` by +default) or select a profile by setting the `AWS_PROFILE` environment variable. """ -Most `AWSCore` functions take a `AWSConfig` dictionary as the first argument. -This dictionary holds [`AWSCredentials`](@ref) and AWS region configuration. +mutable struct AWSCredentials + access_key_id::String + secret_key::String + token::String + user_arn::String + account_number::String + + function AWSCredentials(access_key_id, secret_key, + token="", user_arn="", account_number="") + new(access_key_id, secret_key, token, user_arn, account_number) + end +end + +""" + AWSConfig + +Most `AWSCore` functions take an `AWSConfig` object as the first argument. +This type holds [`AWSCredentials`](@ref), region, and output configuration. + +# Constructors + + AWSConfig(; profile, creds, region, output) -```julia -aws = AWSConfig(:creds => AWSCredentials(), :region => "us-east-1")` +Construct an `AWSConfig` object with the given profile, credentials, region, and output +format. All keyword arguments have default values and are thus optional. + +* `profile`: Profile name passed to [`AWSCredentials`](@ref), or `nothing` (default) +* `creds`: `AWSCredentials` object, constructed using `profile` if not provided +* `region`: Region, read from `AWS_DEFAULT_REGION` if present, otherwise `"us-east-1"` +* `output`: Output format, defaulting to JSON (`"json"`) + +# Examples + +```julia-repl +julia> AWSConfig(profile="example", region="ap-southeast-2") +AWSConfig((AKIDEXAMPLE, wJa...) +, "ap-southeast-2", "json") + +julia> AWSConfig(creds=AWSCredentials("AKIDEXAMPLE", "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY")) +AWSConfig((AKIDEXAMPLE, wJa...) +, "us-east-1", "json") ``` """ -const AWSConfig = SymbolDict +mutable struct AWSConfig + creds::AWSCredentials + region::String + output::String +end + +function AWSConfig(; profile=nothing, + creds=AWSCredentials(profile=profile), + region=get(ENV, "AWS_DEFAULT_REGION", "us-east-1"), + output="json") + AWSConfig(creds, region, output) +end +# Relics of using SymbolDict +import Base: getindex, setindex! +Base.@deprecate AWSConfig(pairs::Pair...) AWSConfig(; pairs...) +Base.@deprecate getindex(conf::AWSConfig, x::Symbol) getfield(conf, x) +Base.@deprecate setindex!(conf::AWSConfig, val, var::Symbol) setfield!(conf, var, val) +Base.@deprecate aws_config AWSConfig +function Base.get(conf::AWSConfig, field::Symbol, alternative) + Base.depwarn("get(::AWSConf, a, b) is deprecated; access fields directly instead", :get) + if Base.fieldindex(AWSConfig, field, false) > 0 + getfield(conf, field) + else + alternative + end +end +function Base.merge(conf::AWSConfig, d::AbstractDict{Symbol,<:Any}) + Base.depwarn("merge(::AWSConf, dict) is deprecated; set fields directly instead", :merge) + for (k, v) in d + setfield!(conf, k, v) + end + conf +end +function Base.merge(d::AbstractDict{K,V}, conf::AWSConfig) where {K,V} + for f in fieldnames(AWSConfig) + d[convert(K, f)] = getfield(conf, f) + end + d +end """ The `AWSRequest` dictionary describes a single API request: @@ -57,78 +174,24 @@ include("names.jl") include("mime.jl") - #------------------------------------------------------------------------------# # Configuration. #------------------------------------------------------------------------------# -""" -The `aws_config` function provides a simple way to creates an -[`AWSConfig`](@ref) configuration dictionary. - -```julia ->aws = aws_config() ->aws = aws_config(creds = my_credentials) ->aws = aws_config(region = "ap-southeast-2") ->aws = aws_config(profile = "profile-name") -``` - -By default, the `aws_config` attempts to load AWS credentials from: - - - `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` [environemnt variables](http://docs.aws.amazon.com/cli/latest/userguide/cli-environment.html), - - [`~/.aws/credentials`](http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html) or - - [EC2 Instance Credentials](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#instance-metadata-security-credentials). - -A `~/.aws/credentials` file can be created using the -[AWS CLI](https://aws.amazon.com/cli/) command `aws configrue`. -Or it can be created manually: - -```ini -[default] -aws_access_key_id = AKIAXXXXXXXXXXXXXXXX -aws_secret_access_key = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -``` - -If your `~/.aws/credentials` file contains multiple profiles you can pass the -profile name as a string to the `profile` keyword argument (`nothing` by -default) or select a profile by setting the `AWS_PROFILE` environment variable. - -`aws_config` understands the following [AWS CLI environment -variables](http://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html#cli-environment): -`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`, -`AWS_DEFAULT_REGION`, `AWS_PROFILE` and `AWS_CONFIG_FILE`. - - -An configuration dictionary can also be created directly from a key pair -as follows. However, putting access credentials in source code is discouraged. - -```julia -aws = aws_config(creds = AWSCredentials("AKIAXXXXXXXXXXXXXXXX", - "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")) -``` +global _default_aws_config = Ref{Union{AWSConfig,Nothing}}(nothing) """ -function aws_config(;profile=nothing, - creds=AWSCredentials(profile=profile), - region=get(ENV, "AWS_DEFAULT_REGION", "us-east-1"), - args...) - @SymDict(creds, region, args...) -end + default_aws_config() - -global _default_aws_config = nothing # Union{AWSConfig,Nothing} - - -""" -`default_aws_config` returns a global shared [`AWSConfig`](@ref) object -obtained by calling [`aws_config`](@ref) with no optional arguments. +Return the global shared [`AWSConfig`](@ref) object obtained by calling +[`AWSConfig()`](@ref) with no arguments. """ function default_aws_config() global _default_aws_config - if _default_aws_config === nothing - _default_aws_config = aws_config() + if _default_aws_config[] === nothing + _default_aws_config[] = AWSConfig() end - return _default_aws_config + return _default_aws_config[] end @@ -201,7 +264,7 @@ Service endpoint URL for `request`. """ function service_url(aws, request) endpoint = get(request, :endpoint, request[:service]) - region = "." * aws[:region] + region = "." * aws.region if endpoint == "iam" || (endpoint == "sdb" && region == ".us-east-1") region = "" end @@ -220,7 +283,7 @@ function service_query(aws::AWSConfig; args...) request = Dict{Symbol,Any}(args) request[:verb] = "POST" - request[:resource] = get(aws, :resource, "/") + request[:resource] = "/" # get(aws, :resource, "/") XXX how could config ever have that request[:url] = service_url(aws, request) request[:headers] = Dict("Content-Type" => "application/x-www-form-urlencoded; charset=utf-8") @@ -230,7 +293,7 @@ function service_query(aws::AWSConfig; args...) request[:query]["Version"] = request[:version] if request[:service] == "iam" - aws = merge(aws, Dict(:region => "us-east-1")) + aws.region = "us-east-1" end if request[:service] in ["iam", "sts", "sqs", "sns"] request[:query]["ContentType"] = "JSON" diff --git a/src/AWSCredentials.jl b/src/AWSCredentials.jl index a968c9e..b8dde1f 100644 --- a/src/AWSCredentials.jl +++ b/src/AWSCredentials.jl @@ -17,32 +17,7 @@ export AWSCredentials, aws_account_number -""" -When you interact with AWS, you specify your [AWS Security Credentials](http://docs.aws.amazon.com/general/latest/gr/aws-security-credentials.html) to verify who you are and whether you have permission to access the resources that you are requesting. AWS uses the security credentials to authenticate and authorize your requests. - -The fields `access_key_id` and `secret_key` hold the access keys used to authenticate API requests (see [Creating, Modifying, and Viewing Access Keys](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html#Using_CreateAccessKey)). - -[Temporary Security Credentials](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp.html) require the extra session `token` field. - - -The `user_arn` and `account_number` fields are used to cache the result of the [`aws_user_arn`](@ref) and [`aws_account_number`](@ref) functions. - -The `AWSCredentials()` constructor tries to load local Credentials from -environment variables, `~/.aws/credentials`, `~/.aws/config` or EC2 instance credentials. -To specify the profile to use from `~/.aws/credentials`, do, for example, `AWSCredentials(profile="profile-name")`. -""" -mutable struct AWSCredentials - access_key_id::String - secret_key::String - token::String - user_arn::String - account_number::String - - function AWSCredentials(access_key_id, secret_key, - token="", user_arn="", account_number="") - new(access_key_id, secret_key, token, user_arn, account_number) - end -end +# AWSCredentials type defined in src/AWSCore.jl function Base.show(io::IO,c::AWSCredentials) println(io, string(c.user_arn, @@ -145,7 +120,7 @@ e.g. `"arn:aws:iam::account-ID-without-hyphens:user/Bob"` """ function aws_user_arn(aws::AWSConfig) - creds = aws[:creds] + creds = aws.creds if creds.user_arn == "" @@ -164,7 +139,7 @@ end 12-digit [AWS Account Number](http://docs.aws.amazon.com/general/latest/gr/acct-identifiers.html). """ function aws_account_number(aws::AWSConfig) - creds = aws[:creds] + creds = aws.creds if creds.account_number == "" aws_user_arn(aws) end @@ -322,7 +297,7 @@ function aws_get_role(role::AbstractString, ini::Inifile) end credentials = dot_aws_credentials(source_profile) - config = AWSConfig(:creds=>credentials, :region=>aws_get_region(source_profile, ini)) + config = AWSConfig(creds=credentials, region=aws_get_region(source_profile, ini)) role = Services.sts( config, diff --git a/src/names.jl b/src/names.jl index 232baa7..8a42d76 100644 --- a/src/names.jl +++ b/src/names.jl @@ -21,7 +21,7 @@ export arn, is_arn, Generate an [Amazon Resource Name](http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html) for `service` and `resource`. """ function arn(service, resource, - region=get(default_aws_config(), :region, ""), + region=default_aws_config().region, account=aws_account_number(default_aws_config())) if service == "s3" @@ -38,7 +38,7 @@ end function arn(aws::AWSConfig, service, resource, - region=get(aws, :region, ""), + region=aws.region, account=aws_account_number(aws)) arn(service, resource, region, account) diff --git a/test/runtests.jl b/test/runtests.jl index 0ad98d2..d0cf97c 100755 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -19,7 +19,7 @@ AWSCore.set_debug_level(1) @testset "AWSCore" begin -aws = aws_config() +aws = AWSConfig() @testset "Load Credentials" begin user = aws_user_arn(aws) @@ -28,7 +28,7 @@ aws = aws_config() println("Authenticated as: $user") - aws[:region] = "us-east-1" + aws.region = "us-east-1" println("Testing exceptions...") try @@ -121,32 +121,32 @@ end "AWS_ACCESS_KEY_ID" => nothing ) do - # Check credentials load - config = AWSCore.aws_config() - creds = config[:creds] + # Check credentials load + config = AWSCore.AWSConfig() + creds = config.creds @test creds.access_key_id == "TEST_ACCESS_ID" @test creds.secret_key == "TEST_ACCESS_KEY" # Check credential file takes precedence over config ENV["AWS_DEFAULT_PROFILE"] = "test2" - config = AWSCore.aws_config() - creds = config[:creds] + config = AWSCore.AWSConfig() + creds = config.creds @test creds.access_key_id == "RIGHT_ACCESS_ID2" @test creds.secret_key == "RIGHT_ACCESS_KEY2" # Check credentials take precedence over role ENV["AWS_DEFAULT_PROFILE"] = "test3" - config = AWSCore.aws_config() - creds = config[:creds] + config = AWSCore.AWSConfig() + creds = config.creds @test creds.access_key_id == "RIGHT_ACCESS_ID3" @test creds.secret_key == "RIGHT_ACCESS_KEY3" ENV["AWS_DEFAULT_PROFILE"] = "test4" - config = AWSCore.aws_config() - creds = config[:creds] + config = AWSCore.AWSConfig() + creds = config.creds @test creds.access_key_id == "RIGHT_ACCESS_ID4" @test creds.secret_key == "RIGHT_ACCESS_KEY4" @@ -155,7 +155,7 @@ end ENV["AWS_DEFAULT_PROFILE"] = "test:dev" try - AWSCore.aws_config() + AWSCore.AWSConfig() @test false catch e @test e isa AWSCore.AWSException @@ -167,7 +167,7 @@ end let oldout = stdout r,w = redirect_stdout() try - AWSCore.aws_config() + AWSCore.AWSConfig() @test false catch e @test e isa AWSCore.AWSException From 9771cd0e4c9accc67f272c57da1825452c24b742 Mon Sep 17 00:00:00 2001 From: Alex Arslan Date: Fri, 1 Feb 2019 19:07:44 -0800 Subject: [PATCH 2/3] wip --- src/AWSCore.jl | 99 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 82 insertions(+), 17 deletions(-) diff --git a/src/AWSCore.jl b/src/AWSCore.jl index 0bb8f53..0fef566 100644 --- a/src/AWSCore.jl +++ b/src/AWSCore.jl @@ -103,52 +103,117 @@ format. All keyword arguments have default values and are thus optional. ```julia-repl julia> AWSConfig(profile="example", region="ap-southeast-2") AWSConfig((AKIDEXAMPLE, wJa...) -, "ap-southeast-2", "json") +, "ap-southeast-2", "json", NamedTuple()) julia> AWSConfig(creds=AWSCredentials("AKIDEXAMPLE", "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY")) AWSConfig((AKIDEXAMPLE, wJa...) -, "us-east-1", "json") +, "us-east-1", "json", NamedTuple()) ``` """ mutable struct AWSConfig creds::AWSCredentials region::String output::String + # XXX: The `_extras` field will be removed after the deprecation period + _extras::Dict{Symbol,Any} end function AWSConfig(; profile=nothing, creds=AWSCredentials(profile=profile), - region=get(ENV, "AWS_DEFAULT_REGION", "us-east-1"), - output="json") - AWSConfig(creds, region, output) + region=get(ENV, "AWS_DEFAULT_REGION", ""), + output="json", + kwargs...) + AWSConfig(creds, region, output, kwargs) end # Relics of using SymbolDict -import Base: getindex, setindex! + +_isfield(x::Symbol) = (x === :creds || x === :region || x === :output) + Base.@deprecate AWSConfig(pairs::Pair...) AWSConfig(; pairs...) -Base.@deprecate getindex(conf::AWSConfig, x::Symbol) getfield(conf, x) -Base.@deprecate setindex!(conf::AWSConfig, val, var::Symbol) setfield!(conf, var, val) Base.@deprecate aws_config AWSConfig + +function Base.setindex!(conf::AWSConfig, val, var::Symbol) + if _isfield(var) + Base.depwarn("`setindex!(conf::AWSConfig, val, var::Symbol)` is deprecated, " * + "use `setfield!(conf, var, val)` instead.", :setindex!) + setfield!(conf, var, val) + else + Base.depwarn("storing information other than credentials, region, and output " * + "format in an `AWSConfig` object is deprecated; use another data " * + "structure to store this information.", :setindex!) + conf._extras[var] = val + end +end + +function Base.getindex(conf::AWSConfig, x::Symbol) + if _isfield(x) + Base.depwarn("`getindex(conf::AWSConfig, x::Symbol)` is deprecated, use " * + "`getfield(conf, x)` instead.", :getindex) + getfield(conf, x) + else + Base.depwarn("retrieving information other than credentials, region, and output " * + "format from an `AWSConfig` object is deprecated; use another data " * + "structure to store this information.", :getindex) + conf._extras[x] + end +end + function Base.get(conf::AWSConfig, field::Symbol, alternative) - Base.depwarn("get(::AWSConf, a, b) is deprecated; access fields directly instead", :get) - if Base.fieldindex(AWSConfig, field, false) > 0 + if _isfield(field) + Base.depwarn("`get(conf::AWSConfig, field::Symbol, alternative)` is deprecated, " * + "use `getfield(conf, field)` instead.", :get) getfield(conf, field) else - alternative + Base.depwarn("retrieving information other than credentials, region, and output " * + "format from an `AWSConfig` object is deprecated; use another data " * + "structure to store this information.", :get) + get(conf._extras, field, alternative) end end + +function Base.haskey(conf::AWSConfig, field::Symbol) + Base.depwarn("`haskey(conf::AWSConfig, field::Symbol)` is deprecated; in the future " * + "no information other than credentials, region and output format will " * + "be stored in an `AWSConfig` object.", :haskey) + _isfield(field) ? true : haskey(conf._extras, field) +end + function Base.merge(conf::AWSConfig, d::AbstractDict{Symbol,<:Any}) - Base.depwarn("merge(::AWSConf, dict) is deprecated; set fields directly instead", :merge) for (k, v) in d - setfield!(conf, k, v) + if _isfield(k) + Base.depwarn("`merge(conf::AWSConf, d::AbstractDict)` is deprecated, set fields " * + "directly instead", :merge) + setfield!(conf, k, v) + else + Base.depwarn("storing information other than credentials, region, and output " * + "format in an `AWSConfig` object is deprecated; use another data " * + "structure to store this information.", :merge) + conf._extras[k] = v + end end conf end + function Base.merge(d::AbstractDict{K,V}, conf::AWSConfig) where {K,V} - for f in fieldnames(AWSConfig) - d[convert(K, f)] = getfield(conf, f) + Base.depwarn("`merge(d::AbstractDict, conf::AWSConfig)` is deprecated; in the future " * + "no information other than credentials, region and output format will " * + "be stored in an `AWSConfig` object and it will not behave like a " * + "dictionary.", :merge) + m = merge(d, conf._extras) + for f in [:creds, :region, :output] + m[convert(K, f)] = getfield(conf, f) end - d + m +end + +function Base.iterate(conf::AWSConfig, state...) + Base.depwarn("in the future, `AWSConfig` objects will not be iterable", :iterate) + x = [:creds => conf.creds, + :region => conf.region, + :output => conf.output, + conf._extras...] + iterate(x, state...) end """ @@ -283,7 +348,7 @@ function service_query(aws::AWSConfig; args...) request = Dict{Symbol,Any}(args) request[:verb] = "POST" - request[:resource] = "/" # get(aws, :resource, "/") XXX how could config ever have that + request[:resource] = get(aws, :resource, "/") request[:url] = service_url(aws, request) request[:headers] = Dict("Content-Type" => "application/x-www-form-urlencoded; charset=utf-8") From 91943ac385905733a466489cbcea77e29edde907 Mon Sep 17 00:00:00 2001 From: Alex Arslan Date: Sat, 2 Feb 2019 13:21:52 -0800 Subject: [PATCH 3/3] Implement the AbstractDict interface for AWSConfig This temporarily makes `AWSConfig` a subtype of `AbstractDict` and defines the corresponding methods to satisfy the interface. Each method emits a deprecation warning. This should hopefully be completely non-breaking for users and provide sufficient information for them to upgrade. --- src/AWSConfig.jl | 209 +++++++++++++++++++++++++++++++++++++++++++++++ src/AWSCore.jl | 140 +------------------------------ 2 files changed, 212 insertions(+), 137 deletions(-) create mode 100644 src/AWSConfig.jl diff --git a/src/AWSConfig.jl b/src/AWSConfig.jl new file mode 100644 index 0000000..2a94755 --- /dev/null +++ b/src/AWSConfig.jl @@ -0,0 +1,209 @@ +""" + AWSConfig + +Most `AWSCore` functions take an `AWSConfig` object as the first argument. +This type holds [`AWSCredentials`](@ref), region, and output configuration. + +# Constructors + + AWSConfig(; profile, creds, region, output) + +Construct an `AWSConfig` object with the given profile, credentials, region, and output +format. All keyword arguments have default values and are thus optional. + +* `profile`: Profile name passed to [`AWSCredentials`](@ref), or `nothing` (default) +* `creds`: `AWSCredentials` object, constructed using `profile` if not provided +* `region`: Region, read from `AWS_DEFAULT_REGION` if present, otherwise `"us-east-1"` +* `output`: Output format, defaulting to JSON (`"json"`) + +# Examples + +```julia-repl +julia> AWSConfig(profile="example", region="ap-southeast-2") +AWSConfig(creds=AWSCredentials("AKIDEXAMPLE", "wJa..."), region="ap-southeast-2", output="json") +``` +""" +mutable struct AWSConfig <: AbstractDict{Symbol,Any} # XXX: Remove subtype after deprecation + creds::AWSCredentials + region::String + output::String + # XXX: The `_extras` field will be removed after the deprecation period + _extras::Dict{Symbol,Any} +end + +function AWSConfig(; profile=nothing, + creds=AWSCredentials(profile=profile), + region=get(ENV, "AWS_DEFAULT_REGION", "us-east-1"), + output="json", + kwargs...) + AWSConfig(creds, region, output, kwargs) +end + +function Base.show(io::IO, conf::AWSConfig) + print(io, "AWSConfig(creds=AWSCredentials(") + show(io, conf.creds.access_key_id) + print(io, ", \"", conf.creds.secret_key[1:3], "...\"), region=") + show(io, conf.region) + print(io, ", output=") + show(io, conf.output) + print(io, ')') + if !isempty(conf._extras) + println(io, "\n Additional contents (SHOULD BE REMOVED):") + join(io, [string(" :", k, " => ", v) for (k, v) in conf._extras], '\n') + end +end + +# Overrides needed because of the AbstractDict subtyping +Base.summary(io::IO, conf::AWSConfig) = "AWSConfig" +Base.show(io::IO, ::MIME{Symbol("text/plain")}, conf::AWSConfig) = show(io, conf) + +function Base.Dict(conf::AWSConfig) + d = copy(conf._extras) + for f in [:creds, :region, :output] + d[f] = getfield(conf, f) + end + d +end + +# TODO: Implement copy for AWSCredentials +Base.copy(conf::AWSConfig) = AWSConfig(conf.creds, conf.region, conf.output, copy(conf._extras)) + +# Relics of using `SymbolDict`. We'll implement the entire `AbstractDict` interface +# with informative deprecation messages depending on how the functions are used: +# if users are storing and accessing the information that corresponds to the fields +# of the type, pretend like we're just using `@deprecate`. If they try it with other +# information, tell them it won't be possible soon. + +_isfield(x::Symbol) = (x === :creds || x === :region || x === :output) + +function _depmsg(store::Bool) + if store + verb = "storing" + preposition = "in" + else + verb = "retrieving" + preposition = "from" + end + string(verb, " information other than credentials, region, and output format ", + preposition, " an `AWSConfig` object is deprecated; use another data ", + "structure to store this information.") +end + +function _depsig(old::String, new::String="") + s = "`" * old * "` is deprecated" + if isempty(new) + s *= "; in the future, no information other than credentials, region, and output " * + "format will be stored in an `AWSConfig` object." + else + s *= ", use `" * new * "` instead." + end + s +end + +using Base: @deprecate, depwarn +import Base: merge, merge!, keytype, valtype + +@deprecate AWSConfig(pairs::Pair...) AWSConfig(; pairs...) +@deprecate aws_config AWSConfig +@deprecate merge(d::AbstractDict, conf::AWSConfig) merge(d, Dict(conf)) +@deprecate merge!(d::AbstractDict, conf::AWSConfig) merge!(d, Dict(conf)) +@deprecate keytype(conf::AWSConfig) Symbol +@deprecate valtype(conf::AWSConfig) Any + +function Base.setindex!(conf::AWSConfig, val, var::Symbol) + if _isfield(var) + depwarn(_depsig("setindex!(conf::AWSConfig, val, var::Symbol", + "setfield!(conf, var, val)"), :setindex!) + setfield!(conf, var, val) + else + depwarn(_depmsg(true), :setindex!) + conf._extras[var] = val + end +end + +function Base.getindex(conf::AWSConfig, x::Symbol) + if _isfield(x) + depwarn(_depsig("getindex(conf::AWSConfig, x::Symbol)", + "getfield(conf, x)"), :getindex) + getfield(conf, x) + else + depwarn(_depmsg(false), :getindex) + conf._extras[x] + end +end + +function Base.get(conf::AWSConfig, field::Symbol, alternative) + if _isfield(field) + depwarn(_depsig("get(conf::AWSConfig, field::Symbol, alternative)", + "getfield(conf, field)"), :get) + getfield(conf, field) + else + depwarn(_depmsg(false), :get) + get(conf._extras, field, alternative) + end +end + +function Base.haskey(conf::AWSConfig, field::Symbol) + depwarn(_depsig("haskey(conf::AWSConfig, field::Symbol)"), :haskey) + _isfield(field) || haskey(conf._extras, field) +end + +function Base.keys(conf::AWSConfig) + depwarn(_depsig("keys(conf::AWSConfig)"), :keys) + keys(Dict(conf)) +end + +function Base.values(conf::AWSConfig) + depwarn(_depsig("values(conf::AWSConfig)"), :values) + values(Dict(conf)) +end + +function Base.merge(conf::AWSConfig, d::AbstractDict{Symbol,<:Any}) + c = copy(conf) + for (k, v) in d + if _isfield(k) + depwarn("`merge(conf::AWSConf, d::AbstractDict)` is deprecated, set fields " * + "directly instead.", :merge) + setfield!(c, k, v) + else + Base.depwarn(_depmsg(true), :merge) + c._extras[k] = v + end + end + c +end + +function Base.merge!(conf::AWSConfig, d::AbstractDict{Symbol,<:Any}) + for (k, v) in d + if _isfield(k) + depwarn("`merge!(conf::AWSConf, d::AbstractDict)` is deprecated, set fields " * + "directly instead.", :merge!) + setfield!(conf, k, v) + else + depwarn(_depmsg(true), :merge!) + conf._extras[k] = v + end + end + conf +end + +function Base.iterate(conf::AWSConfig, state...) + depwarn("in the future, `AWSConfig` objects will not be iterable.", :iterate) + iterate(Dict(conf), state...) +end + +function Base.push!(conf::AWSConfig, (k, v)::Pair{Symbol,<:Any}) + if _isfield(conf, k) + depwarn(_depsig("push!(conf::AWSConfig, p::Pair)", + "setfield!(conf, first(p), last(p))"), :push!) + setfield!(conf, k, v) + else + Base.depwarn(_depmsg(true), :push!) + push!(conf._extras, k => v) + end +end + +function Base.in((k, v)::Pair, conf::AWSConfig) + depwarn("`in(p::Pair, conf::AWSConfig)` is deprecated.", :in) + (_isfield(k) && getfield(conf, k) == v) || in(p, conf._extras) +end diff --git a/src/AWSCore.jl b/src/AWSCore.jl index 0fef566..5d79512 100644 --- a/src/AWSCore.jl +++ b/src/AWSCore.jl @@ -80,141 +80,7 @@ mutable struct AWSCredentials end end -""" - AWSConfig - -Most `AWSCore` functions take an `AWSConfig` object as the first argument. -This type holds [`AWSCredentials`](@ref), region, and output configuration. - -# Constructors - - AWSConfig(; profile, creds, region, output) - -Construct an `AWSConfig` object with the given profile, credentials, region, and output -format. All keyword arguments have default values and are thus optional. - -* `profile`: Profile name passed to [`AWSCredentials`](@ref), or `nothing` (default) -* `creds`: `AWSCredentials` object, constructed using `profile` if not provided -* `region`: Region, read from `AWS_DEFAULT_REGION` if present, otherwise `"us-east-1"` -* `output`: Output format, defaulting to JSON (`"json"`) - -# Examples - -```julia-repl -julia> AWSConfig(profile="example", region="ap-southeast-2") -AWSConfig((AKIDEXAMPLE, wJa...) -, "ap-southeast-2", "json", NamedTuple()) - -julia> AWSConfig(creds=AWSCredentials("AKIDEXAMPLE", "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY")) -AWSConfig((AKIDEXAMPLE, wJa...) -, "us-east-1", "json", NamedTuple()) -``` -""" -mutable struct AWSConfig - creds::AWSCredentials - region::String - output::String - # XXX: The `_extras` field will be removed after the deprecation period - _extras::Dict{Symbol,Any} -end - -function AWSConfig(; profile=nothing, - creds=AWSCredentials(profile=profile), - region=get(ENV, "AWS_DEFAULT_REGION", ""), - output="json", - kwargs...) - AWSConfig(creds, region, output, kwargs) -end - -# Relics of using SymbolDict - -_isfield(x::Symbol) = (x === :creds || x === :region || x === :output) - -Base.@deprecate AWSConfig(pairs::Pair...) AWSConfig(; pairs...) -Base.@deprecate aws_config AWSConfig - -function Base.setindex!(conf::AWSConfig, val, var::Symbol) - if _isfield(var) - Base.depwarn("`setindex!(conf::AWSConfig, val, var::Symbol)` is deprecated, " * - "use `setfield!(conf, var, val)` instead.", :setindex!) - setfield!(conf, var, val) - else - Base.depwarn("storing information other than credentials, region, and output " * - "format in an `AWSConfig` object is deprecated; use another data " * - "structure to store this information.", :setindex!) - conf._extras[var] = val - end -end - -function Base.getindex(conf::AWSConfig, x::Symbol) - if _isfield(x) - Base.depwarn("`getindex(conf::AWSConfig, x::Symbol)` is deprecated, use " * - "`getfield(conf, x)` instead.", :getindex) - getfield(conf, x) - else - Base.depwarn("retrieving information other than credentials, region, and output " * - "format from an `AWSConfig` object is deprecated; use another data " * - "structure to store this information.", :getindex) - conf._extras[x] - end -end - -function Base.get(conf::AWSConfig, field::Symbol, alternative) - if _isfield(field) - Base.depwarn("`get(conf::AWSConfig, field::Symbol, alternative)` is deprecated, " * - "use `getfield(conf, field)` instead.", :get) - getfield(conf, field) - else - Base.depwarn("retrieving information other than credentials, region, and output " * - "format from an `AWSConfig` object is deprecated; use another data " * - "structure to store this information.", :get) - get(conf._extras, field, alternative) - end -end - -function Base.haskey(conf::AWSConfig, field::Symbol) - Base.depwarn("`haskey(conf::AWSConfig, field::Symbol)` is deprecated; in the future " * - "no information other than credentials, region and output format will " * - "be stored in an `AWSConfig` object.", :haskey) - _isfield(field) ? true : haskey(conf._extras, field) -end - -function Base.merge(conf::AWSConfig, d::AbstractDict{Symbol,<:Any}) - for (k, v) in d - if _isfield(k) - Base.depwarn("`merge(conf::AWSConf, d::AbstractDict)` is deprecated, set fields " * - "directly instead", :merge) - setfield!(conf, k, v) - else - Base.depwarn("storing information other than credentials, region, and output " * - "format in an `AWSConfig` object is deprecated; use another data " * - "structure to store this information.", :merge) - conf._extras[k] = v - end - end - conf -end - -function Base.merge(d::AbstractDict{K,V}, conf::AWSConfig) where {K,V} - Base.depwarn("`merge(d::AbstractDict, conf::AWSConfig)` is deprecated; in the future " * - "no information other than credentials, region and output format will " * - "be stored in an `AWSConfig` object and it will not behave like a " * - "dictionary.", :merge) - m = merge(d, conf._extras) - for f in [:creds, :region, :output] - m[convert(K, f)] = getfield(conf, f) - end - m -end - -function Base.iterate(conf::AWSConfig, state...) - Base.depwarn("in the future, `AWSConfig` objects will not be iterable", :iterate) - x = [:creds => conf.creds, - :region => conf.region, - :output => conf.output, - conf._extras...] - iterate(x, state...) -end +include("AWSConfig.jl") """ The `AWSRequest` dictionary describes a single API request: @@ -348,7 +214,7 @@ function service_query(aws::AWSConfig; args...) request = Dict{Symbol,Any}(args) request[:verb] = "POST" - request[:resource] = get(aws, :resource, "/") + request[:resource] = get(aws._extras, :resource, "/") request[:url] = service_url(aws, request) request[:headers] = Dict("Content-Type" => "application/x-www-form-urlencoded; charset=utf-8") @@ -366,7 +232,7 @@ function service_query(aws::AWSConfig; args...) request[:content] = HTTP.escapeuri(flatten_query(request[:service], request[:query])) - do_request(merge(request, aws)) + do_request(merge(request, Dict(aws))) end