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