diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..20788f7ce --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,26 @@ +--- +name: build +on: + push: + branches: + - master + pull_request: + branches: + - master +jobs: + ci: + name: Run checks and tests over ${{matrix.otp_vsn}} and ${{matrix.os}} + runs-on: ${{matrix.os}} + container: + image: erlang:${{matrix.otp_vsn}} + strategy: + matrix: + otp_vsn: [24, 25, 26, 27] + os: [ubuntu-latest] + steps: + - uses: actions/checkout@v4 + - run: export + - run: make rebar3-install + - run: make warnings + - run: make check + - run: make eunit diff --git a/.github/workflows/hex_publish.yml b/.github/workflows/hex_publish.yml new file mode 100644 index 000000000..a2d9d6004 --- /dev/null +++ b/.github/workflows/hex_publish.yml @@ -0,0 +1,23 @@ +--- +name: Publish to Hex.pm +on: + push: + tags: + - '*' + +jobs: + publish: + runs-on: ubuntu-latest + container: erlang:27 + + steps: + - name: Check out + uses: actions/checkout@v4 + + - name: 'Fix up git directory ownership (see https://github.com/actions/checkout/issues/1049)' + run: git config --global --add safe.directory '*' + + - name: Publish to Hex.pm + env: + HEX_API_KEY: ${{ secrets.HEX_API_KEY }} + run: rebar3 hex publish package -r hexpm --yes diff --git a/.gitignore b/.gitignore index b9000b235..c85b5f9b5 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ ebin/*.app .rebar/ rebar rebar3 +.rebar3 *.sublime-* deps/ doc/ @@ -20,3 +21,5 @@ eunit.coverage.xml ercloud.iml *[#]*[#] *[#]* +.dialyzer_plt +eunit.log diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 4bf7ed6b6..000000000 --- a/.travis.yml +++ /dev/null @@ -1,29 +0,0 @@ -sudo: false -language: erlang -otp_release: - - 17.5 - - 18.3 - - 19.3 - - 20.3 -env: - global: - - MAIN_OTP=19.3 - matrix: - - FORCE_REBAR2=true - - FORCE_REBAR2=false PATH=$PATH:$PWD -branches: - only: - - master - - /^[0-9]+\.[0-9]+\.[0-9]+/ -install: - - make travis-install -script: - - make check_warnings - - make eunit -deploy: - skip_cleanup: true - provider: script - script: make travis-publish - on: - tags: true - condition: $FORCE_REBAR2 = false && $TRAVIS_OTP_RELEASE = $MAIN_OTP diff --git a/Makefile b/Makefile index b6c556855..79672c001 100644 --- a/Makefile +++ b/Makefile @@ -1,33 +1,8 @@ -.PHONY: all get-deps clean compile run eunit check check-eunit doc +.PHONY: all get-deps clean compile run eunit check doc hex-publish rebar3-install -# determine which Rebar we want to be running -REBAR2=$(shell which rebar || echo ./rebar) -REBAR3=$(shell which rebar3) -ifeq ($(FORCE_REBAR2),true) - REBAR=$(REBAR2) - REBAR_VSN=2 -else ifeq ($(REBAR3),) - REBAR=$(REBAR2) - REBAR_VSN=2 -else - REBAR=$(REBAR3) - REBAR_VSN=3 -endif +REBAR=$(shell which rebar3 || echo ./rebar3) -CHECK_FILES=\ - ebin/*.beam - -CHECK_EUNIT_FILES=\ - $(CHECK_FILES) \ - .eunit/*.beam - - -all: get-deps compile - -get-deps: -ifeq ($(REBAR_VSN),2) - @$(REBAR) get-deps -endif +all: compile clean: @$(REBAR) clean @@ -36,79 +11,26 @@ compile: @$(REBAR) compile run: -ifeq ($(REBAR_VSN),2) - erl -pa deps/*/ebin -pa ./ebin -else $(REBAR) shell -endif check_warnings: -ifeq ($(REBAR_VSN),2) - @echo skip checking warnings -else @$(REBAR) as warnings compile -endif + +warnings: + @$(REBAR) as test compile eunit: -ifeq ($(REBAR_VSN),2) - @$(REBAR) compile - @$(REBAR) eunit skip_deps=true -else - @$(REBAR) eunit -endif + @ERL_FLAGS="-config $(PWD)/eunit" $(REBAR) eunit check: -ifeq ($(REBAR_VSN),2) - @$(REBAR) compile - dialyzer --verbose --no_check_plt --no_native --fullpath \ - $(CHECK_FILES) \ - -Wunmatched_returns \ - -Werror_handling -else - @$(REBAR) dialyzer -endif - -check-eunit: eunit -ifeq ($(REBAR_VSN),2) - dialyzer --verbose --no_check_plt --no_native --fullpath \ - $(CHECK_EUNIT_FILES) \ - -Wunmatched_returns \ - -Werror_handling -else - @$(REBAR) dialyzer -endif + @$(REBAR) as dialyzer do dialyzer --update-plt doc: -ifeq ($(REBAR_VSN),2) - @$(REBAR) doc skip_deps=true -else @$(REBAR) edoc -endif -# The "install" step for Travis -travis-install: -ifeq ($(FORCE_REBAR2),true) - rebar get-deps -else +hex-publish: + @$(REBAR) hex publish + +rebar3-install: wget https://s3.amazonaws.com/rebar3/rebar3 chmod a+x rebar3 -endif - -travis-publish: - @echo Create directories - mkdir -p ~/.hex - mkdir -p ~/.config/rebar3 - - @echo Decrypt secrets - @openssl aes-256-cbc -K $encrypted_9abc06b32f03_key -iv $encrypted_9abc06b32f03_iv -in hex.config.enc -out ~/.hex/hex.config -d - - @echo Create global config - echo '{plugins, [rebar3_hex]}.' > ~/.config/rebar3/rebar.config - - @echo Edit version tag in app.src - vi -e -c '%s/{vsn, *.*}/{vsn, "'${TRAVIS_TAG}'"}/g|w|q' src/erlcloud.app.src - - @echo Publish to Hex - echo 'Y' | ./rebar3 hex publish - - @echo Done diff --git a/README.md b/README.md index 333d62a2e..fe1adc1cf 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,17 @@ -# erlcloud: AWS APIs library for Erlang # +# erlcloud: AWS APIs library for Erlang -[![Build Status](https://secure.travis-ci.org/erlcloud/erlcloud.png?branch=master)](http://travis-ci.org/erlcloud/erlcloud) +[![Build Status](https://github.com/erlcloud/erlcloud/workflows/build/badge.svg)](https://github.com/erlcloud/erlcloud/actions/workflows/ci.yml) +[![Hex.pm](https://img.shields.io/hexpm/v/erlcloud)](https://hex.pm/packages/erlcloud) +[![Hex.pm](https://img.shields.io/hexpm/l/erlcloud)](COPYRIGHT) -This library is not developed or maintained by AWS thus lots of functionality is still missing comparing to [aws-cli](https://aws.amazon.com/cli/) or [boto](https://github.com/boto/boto). -Required functionality is being added upon request. + +This library is not developed or maintained by AWS thus lots of functionality +is still missing comparing to [aws-cli](https://aws.amazon.com/cli/) or +[boto](https://github.com/boto/boto). Required functionality is being added +upon request. Service APIs implemented: + - Amazon Elastic Compute Cloud (EC2) - Amazon EC2 Container Service (ECS) - Amazon Simple Storage Service (S3) @@ -37,17 +43,22 @@ Service APIs implemented: - Simple Notification Service (SNS) - Web Application Firewall (WAF) - AWS Cost and Usage Report API +- AWS Secrets Manager +- AWS Systems Manager (SSM) - and more to come -Majority of API functions have been implemented. -Not all functions have been thoroughly tested, so exercise care when integrating this library into production code. -Please send issues and patches. +The majority of API functions have been implemented. Not all functions have +been thoroughly tested, so exercise care when integrating this library into +production code. Please send issues and patches. -The libraries can be used two ways: -- either you can specify configuration parameters in the process dictionary. Useful for simple tasks -- you can create a configuration object and pass that to each request as the final parameter. Useful for Cross AWS Account access +The libraries can be used two ways. You can: -## Roadmap ## +- specify configuration parameters in the process dictionary. Useful for simple + tasks, or +- create a configuration object and pass that to each request as the final + parameter. Useful for Cross AWS Account access + +## Roadmap Below is the library roadmap update along with regular features and fixes. @@ -58,93 +69,141 @@ Below is the library roadmap update along with regular features and fixes. - 3.X.X - Fix dialyzer findings and make it mandatory for the library - Only SigV4 signing and generalised in one module. Keep SigV2 in SBD section only - - No more `erlang:error()` use and use of regular tuples as error API. Breaking change. + - No more `erlang:error()` use and use of regular tuples as error API. + Breaking change. ### Major API compatibility changes between 0.13.X and 2.0.x - - ELB APIs - - ... list to be filled shortly + +- ELB APIs +- ... list to be filled shortly ### Supported Erlang versions + At the moment we support the following OTP releases: - - 17.5 - - 18.1 - - 19.1 - - 20.0 -## Getting started ## -You need to clone the repository and download rebar/rebar3 (if it's not already available in your path). -``` +- 19.3 +- 20.3 +- 21.3 +- 22.3 +- 23.3 +- 24.3 +- 25.3 +- 26.2 +- 27.1 + +This list is determined by ensuring eunit tests and dialyzer checks succeed for these versions, but not all of these +versions are in active use by library authors. Please report any issues discovered in actual use. + +The Github Actions test runners only support OTP 24+ due to runtime issues, but OTP 19-23 were tested locally with unmodified, +official [Erlang docker images](https://hub.docker.com/_/erlang). Dialyzer checks run against the latest hex-published hackney +for OTP 24+, but a previous versions of hackney (1.15.0) and its dependency parse_trans (3.2.0) were used to do dialyzer checks +for OTP 19-23 due to newer versions of parse_trans requiring OTP 21+. + +## Getting started + +You need to clone the repository and download rebar/rebar3 (if it's not already +available in your path). + +```sh git clone https://github.com/erlcloud/erlcloud.git cd erlcloud wget https://s3.amazonaws.com/rebar3/rebar3 chmod a+x rebar3 ``` -To compile and run erlcloud -``` + +To compile and run erlcloud: + +```sh make make run ``` -If you're using erlcloud in your application, add it as a dependency in your application's configuration file. -To use erlcloud in the shell, you can start it by calling: +To use erlcloud in your application, add it as a dependency in your +application's configuration file. erlcloud is also available as a [Hex](https://hex.pm) package, refer to the Hex [`mix` usage docs](https://hex.pm/docs/usage) or [`rebar3` usage docs](https://hex.pm/docs/rebar3-usage) for more help including dependencies using Hex syntax. -``` +To use erlcloud in the shell, you can start +it by calling: + +```erlang application:ensure_all_started(erlcloud). ``` + ### Using Temporary Security Credentials -The access to AWS resource might be managed through [third-party identity provider](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp.html). -The access is managed using [temporary security credentials](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html). +When access to AWS resources is managed through [third-party identity +providers](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp.html) +it is performed using [temporary security +credentials](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html). -You can provide your amazon credentials in OS environmental variables +You can provide your AWS credentials in OS environment variables -``` +```sh export AWS_ACCESS_KEY_ID= export AWS_SECRET_ACCESS_KEY= export AWS_SESSION_TOKEN= export AWS_DEFAULT_REGION= ``` -If you did not provide your amazon credentials in the OS environmental variables, then you need to provide configuration read from your profile: -``` + +If you did not provide your AWS credentials in the OS environment variables, +then you need to provide configuration read from your profile: + +```erlang {ok, Conf} = erlcloud_aws:profile(). erlcloud_s3:list_buckets(Conf). ``` + Or you can provide them via `erlcloud` application environment variables. + ```erlang application:set_env(erlcloud, aws_access_key_id, "your key"), application:set_env(erlcloud, aws_secret_access_key, "your secret key"), application:set_env(erlcloud, aws_security_token, "your token"), application:set_env(erlcloud, aws_region, "your region"), ``` -### Using Access Key ### -You can provide your amazon credentials in environmental variables. -``` + +### Using Access Key + +You can provide your AWS credentials in environmental variables. + +```sh export AWS_ACCESS_KEY_ID= export AWS_SECRET_ACCESS_KEY= ``` -If you did not provide your amazon credentials in the environmental variables, then you need to provide the per-process configuration: -``` + +If you did not provide your AWS credentials in the environment variables, then +you need to provide the per-process configuration: + +```erlang erlcloud_ec2:configure(AccessKeyId, SecretAccessKey [, Hostname]). ``` -Hostname defaults to non-existing `"ec2.amazonaws.com"` intentionally to avoid mix with US-East-1 -Refer to [aws_config](https://github.com/erlcloud/erlcloud/blob/master/include/erlcloud_aws.hrl) for full description of all services configuration. + +Hostname defaults to non-existing `"ec2.amazonaws.com"` intentionally to avoid +mix with US-East-1 Refer to +[aws_config](https://github.com/erlcloud/erlcloud/blob/master/include/erlcloud_aws.hrl) +for full description of all services configuration. Configuration object usage: -``` + +```erlang EC2 = erlcloud_ec2:new(AccessKeyId, SecretAccessKey [, Hostname]) erlcloud_ec2:describe_images(EC2). ``` ### aws_config -[aws_config](https://github.com/erlcloud/erlcloud/blob/master/include/erlcloud_aws.hrl) record contains many valuable defaults, -such as protocols and ports for AWS services. You can always redefine them by making new `#aws_config{}` record and -changing particular fields, then passing the result to any erlcloud function. -But if you want to change something in runtime this might be tedious and/or not flexible enough. -Alternative approach is to set default fields within the `app.config -> erlcloud -> aws_config` section and -rely on the config, used by all functions by default. +The [aws_config](https://github.com/erlcloud/erlcloud/blob/master/include/erlcloud_aws.hrl) +record contains many valuable defaults, such as protocols and ports for AWS +services. You can always redefine them by making new `#aws_config{}` record and +changing particular fields, then passing the result to any `erlcloud` function. + +But if you want to change something in runtime this might be tedious and/or not +flexible enough. -Example of such app.config: +An alternative approach is to set default fields within the `app.config -> +erlcloud -> aws_config` section and rely on the config, used by all functions +by default. + +Example of such `app.config`: ```erlang [ @@ -157,10 +216,36 @@ Example of such app.config: ]. ``` +### VPC endpoints + +If you want to utilise AZ affinity for VPC endpoints you can configure those in +application config via: -### Basic use ### -Then you can start making api calls, like: +```erlang +{erlcloud, [ + {services_vpc_endpoints, [ + {<<"sqs">>, [<<"myAZ1.sqs-dns.amazonaws.com">>, <<"myAZ2.sqs-dns.amazonaws.com">>]}, + {<<"kinesis">>, {env, "KINESIS_VPC_ENDPOINTS"}} + ]} +]} ``` + +Two options are supported: + +- explicit list of Route53 AZ endpoints +- OS environment variable (handy for ECS deployments). The value of the + variable should be of comma-separated string like + `"myAZ1.sqs-dns.amazonaws.com,myAZ2.sqs-dns.amazonaws.com"` + +Upon config generation, `erlcloud` will check the AZ of the deployment and +match it to one of the pre-configured DNS records. First match is used and if +not match found default is used. + +## Basic use + +Then you can start making API calls, like: + +```erlang erlcloud_ec2:describe_images(). % list buckets of Account stored in config in process dict % of of the account you are running in. @@ -171,13 +256,13 @@ erlcloud_s3:list_buckets(Conf). ``` Creating an EC2 instance may look like this: + ```erlang start_instance(Ami, KeyPair, UserData, Type, Zone) -> Config = #aws_config{ access_key_id = application:get_env(aws_key), secret_access_key = application:get_env(aws_secret) }, - InstanceSpec = #ec2_instance_spec{image_id = Ami, key_name = KeyPair, instance_type = Type, @@ -186,24 +271,38 @@ start_instance(Ami, KeyPair, UserData, Type, Zone) -> erlcloud_ec2:run_instances(InstanceSpec, Config). ``` -For usage information, consult the source code and https://hexdocs.pm/erlcloud. -For detailed API description refer to the AWS references at: +For usage information, consult the source code and +[GitHub repo](https://hexdocs.pm/erlcloud). For detailed API description refer +to the AWS references at: -- http://docs.aws.amazon.com/AWSEC2/latest/APIReference/Welcome.html -- http://docs.aws.amazon.com/AmazonS3/latest/API/Welcome.html -- and other services https://aws.amazon.com/documentation/ +- [AmazonEC2](http://docs.aws.amazon.com/AWSEC2/latest/APIReference/Welcome.html) +- [Amazon S3](http://docs.aws.amazon.com/AmazonS3/latest/API/Welcome.html) +- and [other services](https://aws.amazon.com/documentation/) -## Notes ## +## Notes -Indentation in contributions should follow indentation style of surrounding text. -In general it follows default indentation rules of official erlang-mode as provided by OTP team. +Indentation in contributions should follow indentation style of surrounding +text. In general it follows default indentation rules of official erlang-mode +as provided by OTP team. -## Best Practices ## +## Best Practices - All interfaces should provide a method for working with non-default config. -- Public interfaces with paging logic should prefer `{ok, Results, Marker}` style to the `{{paged, Marker}, Results}` found in some modules. -In case of records output, tokens should be part of the record. -- Passing next page `NextToken`, `NextMarker` is preferred with `Opts` rather than a fun parameter like found in many modules. -- Public interfaces should normally expose proplists over records. All new modules are preferred to have both. -- Exposed records are to be used only for complex outputs. Examples to follow: ddb2, ecs. -- Library should not expose any long running or stateful processes - no gen_servers, no caches and etc. +- Public interfaces with paging logic should prefer `{ok, Results, Marker}` + style to the `{{paged, Marker}, Results}` found in some modules. + In case of records output, tokens should be part of the record. +- Passing next page `NextToken`, `NextMarker` is preferred with `Opts` rather + than a fun parameter like found in many modules. +- Public interfaces should normally expose proplists over records. All new + modules are preferred to have both. +- Exposed records are to be used only for complex outputs. Examples to follow: + ddb2, ecs. +- Library should not expose any long running or stateful processes - no + gen_servers, no caches and etc. + + +## Publishing to hex.pm + +erlcloud is available as a [Hex](https://hex.pm) package. A new version of the package can be published by maintainers using mix or rebar3. A `hex-publish` make target that uses rebar3 is provided for maintainers to use or reference when publishing a new version of the package. + +Github Actions will eventually be used to automatically publish new versions. \ No newline at end of file diff --git a/eunit.config b/eunit.config new file mode 100644 index 000000000..18babf022 --- /dev/null +++ b/eunit.config @@ -0,0 +1 @@ +[{kernel, [{error_logger, {file, "eunit.log"}}]}]. diff --git a/include/erlcloud.hrl b/include/erlcloud.hrl index 9872fa5fa..a924b8063 100644 --- a/include/erlcloud.hrl +++ b/include/erlcloud.hrl @@ -5,7 +5,7 @@ -type datetime() :: {{pos_integer(), 1..12, 1..31}, {0..23, 0..59, 0..60}}. -type paging_token() :: binary() | undefined. --type success_result_paged(ObjectType) :: {ok, [ObjectType]}. +-type success_result_paged(ObjectType) :: {ok, ObjectType}. -type error_result() :: {error, Reason :: term()}. -type result_paged(ObjectType) :: success_result_paged(ObjectType) | error_result(). -type result(ObjectType) :: {ok, [ObjectType]} | error_result(). diff --git a/include/erlcloud_as.hrl b/include/erlcloud_as.hrl index 91b1075aa..e2c0f83e9 100644 --- a/include/erlcloud_as.hrl +++ b/include/erlcloud_as.hrl @@ -26,23 +26,31 @@ -record(aws_autoscaling_group, { group_name :: string(), availability_zones :: undefined | list(string()), - load_balancer_names :: list(string()), + load_balancer_names :: undefined | list(string()), tags :: list(aws_autoscaling_tag()), desired_capacity :: undefined | integer(), min_size :: undefined | integer(), max_size :: undefined | integer(), launch_configuration_name :: undefined | string(), + launch_template :: undefined | aws_launch_template_spec(), vpc_zone_id :: undefined | list(string()), - instances :: list(aws_autoscaling_instance()), - status :: string() + instances :: undefined | list(aws_autoscaling_instance()), + status :: undefined | string() }). -type(aws_autoscaling_group() :: #aws_autoscaling_group{}). +-record(aws_launch_template_spec, { + id :: string(), + name :: string(), + version :: string() +}). +-type(aws_launch_template_spec() :: #aws_launch_template_spec{}). + -record(aws_launch_config, { name :: string(), image_id :: string(), instance_type :: string(), - tenancy :: string(), + tenancy :: undefined | string(), user_data :: undefined | string(), security_groups = [] :: list(string()), public_ip_address = false :: undefined | boolean(), @@ -61,7 +69,7 @@ status_code :: string(), status_msg :: string(), start_time :: datetime(), - end_time :: datetime(), + end_time :: undefined | datetime(), progress :: integer() }). -type(aws_autoscaling_activity() :: #aws_autoscaling_activity{}). diff --git a/include/erlcloud_aws.hrl b/include/erlcloud_aws.hrl index b1fff2bda..a7f9120b7 100644 --- a/include/erlcloud_aws.hrl +++ b/include/erlcloud_aws.hrl @@ -1,18 +1,30 @@ -ifndef(erlcloud_aws_hrl). -define(erlcloud_aws_hrl, 0). --record(aws_assume_role,{ - role_arn :: string() | undefined, - session_name = "erlcloud" :: string(), - duration_secs = 900 :: 900..3600, - external_id :: string() | undefined +-record(aws_assume_role, { + role_arn :: string() | undefined, + session_name = "erlcloud" :: string(), + duration_secs = 900 :: 900..43200, + external_id :: string() | undefined }). -type(aws_assume_role() :: #aws_assume_role{}). +-record(hackney_client_options, { + insecure = true :: boolean() | undefined, + proxy = undefined :: binary() | {binary(), non_neg_integer()} | {socks5, binary(), binary()} | {connect, binary(), binary()} | undefined, + proxy_auth = undefined :: {binary(), binary()} | undefined +}). + +-type(hackney_client_options() :: #hackney_client_options{}). + -record(aws_config, { + access_analyzer_host="access-analyzer.us-east-1.amazonaws.com"::string(), + securityhub_host="securityhub.us-east-1.amazonaws.com"::string(), as_host="autoscaling.amazonaws.com"::string(), + ec2_protocol="https"::string(), ec2_host="ec2.amazonaws.com"::string(), + ec2_port=undefined::non_neg_integer()|undefined, iam_host="iam.amazonaws.com"::string(), sts_host="sts.amazonaws.com"::string(), s3_scheme="https://"::string(), @@ -27,6 +39,8 @@ s3_bucket_access_method=vhost::vhost|path|auto, s3_bucket_after_host=false::boolean(), sdb_host="sdb.amazonaws.com"::string(), + cognito_user_pools_host ="cognito-idp.eu-west-1.amazonaws.com"::string(), + cognito_user_pools_scheme ="https://"::string()|undefined, elb_host="elasticloadbalancing.amazonaws.com"::string(), rds_host="rds.us-east-1.amazonaws.com"::string(), ses_host="email.us-east-1.amazonaws.com"::string(), @@ -65,7 +79,7 @@ kinesis_scheme="https://"::string(), kinesis_host="kinesis.us-east-1.amazonaws.com"::string(), kinesis_port=80::non_neg_integer(), - kinesis_retry=fun erlcloud_kinesis_impl:retry/2::erlcloud_kinesis_impl:retry_fun(), + kinesis_retry=fun erlcloud_kinesis_impl:retry/3::erlcloud_kinesis_impl:retry_fun(), glue_scheme="https://"::string(), glue_host="glue.us-east-1.amazonaws.com"::string(), glue_port=443::non_neg_integer(), @@ -88,6 +102,9 @@ autoscaling_scheme="https://"::string(), autoscaling_host="autoscaling.us-east-1.amazonaws.com"::string(), autoscaling_port=80::non_neg_integer(), + application_autoscaling_scheme="https://"::string(), + application_autoscaling_host="autoscaling.us-east-1.amazonaws.com"::string(), + application_autoscaling_port=80::non_neg_integer(), directconnect_scheme="https://"::string(), directconnect_host="directconnect.us-east-1.amazonaws.com"::string(), directconnect_port=80::non_neg_integer(), @@ -101,24 +118,39 @@ ecs_scheme="https://"::string(), ecs_host="ecs.us-east-1.amazonaws.com"::string(), ecs_port=443::non_neg_integer(), + efs_host="elasticfilesystem.us-east-1.amazonaws.com"::string(), + efs_port=443::non_neg_integer(), + efs_scheme="https://"::string(), mes_scheme="https://"::string(), mes_host="entitlement.marketplace.us-east-1.amazonaws.com"::string(), mes_port=443::non_neg_integer(), mms_scheme="https://"::string(), mms_host="metering.marketplace.us-east-1.amazonaws.com"::string(), mms_port=443::non_neg_integer(), + sm_scheme="https://"::string(), + sm_host="secretsmanager.us-east-1.amazonaws.com"::string(), + sm_port=443::non_neg_integer(), + ssm_scheme="https://"::string(), + ssm_host="ssm.us-east-1.amazonaws.com"::string(), + ssm_port=443::non_neg_integer(), + guardduty_scheme="https://"::string(), + guardduty_host="guardduty.us-east-1.amazonaws.com"::string(), + guardduty_port=443::non_neg_integer(), cur_scheme="https://"::string(), cur_host="cur.us-east-1.amazonaws.com"::string(), cur_port=443::non_neg_integer(), config_scheme="https://"::string(), config_host="config.us-east-1.amazonaws.com"::string(), config_port=443::non_neg_integer(), + workspaces_scheme="https://"::string(), + workspaces_host="workspaces.us-east-1.amazonaws.com"::string(), + workspaces_port=443::non_neg_integer(), access_key_id::string()|undefined|false, secret_access_key::string()|undefined|false, security_token=undefined::string()|undefined, %% epoch seconds when temporary credentials will expire expiration=undefined :: pos_integer()|undefined, - %% Network request timeout; if not specifed, the default timeout will be used: + %% Network request timeout; if not specified, the default timeout will be used: %% ddb: 1s for initial call, 10s for subsequence; %% s3:delete_objects_batch/{2,3}, cloudtrail: 1s; %% other services: 10s. @@ -130,20 +162,30 @@ %% Note: If the lhttpc pool does not exists it will be created one with the lhttpc %% default settings [{connection_timeout,300000},{pool_size,1000}] lhttpc_pool=undefined::atom(), - %% Default to not retry failures (for backwards compatability). + %% Default to not retry failures (for backwards compatibility). %% Recommended to be set to default_retry to provide recommended retry behavior. %% Currently only affects S3 and service modules which use erlcloud_aws %% for issuing HTTP request to AWS, but intent is to change other services to use this as well. %% If you provide a custom function be aware of this anticipated change. %% See erlcloud_retry for full documentation. retry=fun erlcloud_retry:no_retry/1::erlcloud_retry:retry_fun(), + + %% By default treat all non 2xx http error codes as errors. + %% But in some cases, like lambda call it useful to override such + %% behaviour by custom one. + retry_response_type=fun erlcloud_retry:only_http_errors/1::erlcloud_retry:response_type_fun(), %% Currently matches DynamoDB retry %% It's likely this is too many retries for other services retry_num=10::non_neg_integer(), assume_role = #aws_assume_role{} :: aws_assume_role(), %% If a role to be assumed is given %% then we will try to assume the role during the update_config %% region override for API gateway type requests - aws_region=undefined::string()|undefined + aws_region=undefined::string()|undefined, + %% http proxy support + http_proxy=undefined::string()|undefined, + hackney_client_options = #hackney_client_options{} :: hackney_client_options() %% The hackney client options + %% are used to specify the proxy, proxy_auth and insecure which is + %% used to support proxy based requests to s3. }). -type(aws_config() :: #aws_config{}). diff --git a/include/erlcloud_cloudformation.hrl b/include/erlcloud_cloudformation.hrl new file mode 100644 index 000000000..40d70e1b5 --- /dev/null +++ b/include/erlcloud_cloudformation.hrl @@ -0,0 +1,82 @@ +-ifndef(erlcloud_cloudformation_hrl). +-define(erlcloud_cloudformation_hrl, 0). + +-include("erlcloud.hrl"). + +-record(cloudformation_create_stack_input, { + capabilities = [] :: [string()], %% list containing CAPABILITY_IAM | CAPABILITY_NAMED_IAM | CAPABILITY_AUTO_EXPAND + client_request_token :: undefined | string(), + disable_rollback :: undefined | boolean(), + enable_termination_protection :: undefined | boolean(), + notification_arns = [] :: [string()], + on_failure = "ROLLBACK" :: string(), %% DO_NOTHING | ROLLBACK | DELETE + parameters = [] :: [cloudformation_parameter()], + resource_types = [] :: [string()], + role_arn :: undefined | string(), + rollback_configuration :: undefined | cloudformation_rollback_configuration(), + stack_name :: string(), + stack_policy_body :: undefined | string(), + stack_policy_url :: undefined | string(), + tags = []:: [cloudformation_tag()], + template_body :: undefined | string(), + template_url :: undefined | string(), + timeout_in_minutes :: undefined | integer() +}). + +-record(cloudformation_update_stack_input, { + capabilities = [] :: [string()], %% list containing CAPABILITY_IAM | CAPABILITY_NAMED_IAM | CAPABILITY_AUTO_EXPAND + client_request_token :: undefined | string(), + notification_arns = [] :: [string()], + parameters = [] :: [cloudformation_parameter()], + resource_types = [] :: [string()], + role_arn :: undefined | string(), + rollback_configuration :: undefined | cloudformation_rollback_configuration(), + stack_name :: string(), + stack_policy_body :: undefined | string(), + stack_policy_during_update_body :: undefined | string(), + stack_policy_during_update_url :: undefined | string(), + stack_policy_url :: undefined | string(), + tags = []:: [cloudformation_tag()], + template_body :: undefined | string(), + template_url :: undefined | string(), + use_previous_template :: undefined | boolean() +}). + +-record(cloudformation_delete_stack_input, { + client_request_token :: undefined | string(), + retain_resources = [] :: [string()], + role_arn :: undefined | string(), + stack_name :: undefined | string() +}). + +-record(cloudformation_parameter, { + parameter_key :: string(), + parameter_value :: string(), + resolved_value :: undefined | string(), + use_previous_value :: undefined | boolean() +}). + +-record(cloudformation_rollback_configuration, { + monitoring_time_in_minutes :: integer(), + rollback_triggers:: [cloudformation_rollback_trigger()] +}). + +-record(cloudformation_rollback_trigger, { + arn :: string(), + type:: string() +}). + +-record(cloudformation_tag, { + key :: string(), + value :: string() +}). + +-type(cloudformation_create_stack_input() :: #cloudformation_create_stack_input{}). +-type(cloudformation_update_stack_input() :: #cloudformation_update_stack_input{}). +-type(cloudformation_delete_stack_input() :: #cloudformation_delete_stack_input{}). +-type(cloudformation_parameter() :: #cloudformation_parameter{}). +-type(cloudformation_rollback_configuration() :: #cloudformation_rollback_configuration{}). +-type(cloudformation_rollback_trigger() :: #cloudformation_rollback_trigger{}). +-type(cloudformation_tag() :: #cloudformation_tag{}). + +-endif. diff --git a/include/erlcloud_ddb2.hrl b/include/erlcloud_ddb2.hrl index f55e5f52f..8f63d9060 100644 --- a/include/erlcloud_ddb2.hrl +++ b/include/erlcloud_ddb2.hrl @@ -14,18 +14,91 @@ response_body :: undefined | binary() }). +-record(ddb2_request, + {headers :: ddb2_req_headers(), + body :: jsx:json_text(), + json :: jsx:json_term() + }). + +-type ddb2_req_headers() :: [{string(), string()}]. -type date_time() :: number(). -type global_table_status() :: creating | active | deleting | updating. +-type replica_status() :: creating | creation_failed | updating | deleting | active. -type table_status() :: creating | updating | deleting | active. -type backup_status() :: creating | deleted | available. -type index_status() :: creating | updating | deleting | active. -type continuous_backups_status() :: enabled | disabled. -type point_in_time_recovery_status() :: enabling | enabled | disabled. +-record(ddb2_auto_scaling_target_tracking_scaling_policy_configuration_description, + {target_value :: undefined | number(), + disable_scale_in :: undefined | boolean(), + scale_in_cooldown :: undefined | number(), + scale_out_cooldown :: undefined | number()}). + +-record(ddb2_auto_scaling_policy_description, + {policy_name :: undefined | binary(), + target_tracking_scaling_policy_configuration :: undefined | #ddb2_auto_scaling_target_tracking_scaling_policy_configuration_description{}}). + +-record(ddb2_auto_scaling_settings_description, + {auto_scaling_disabled :: undefined | boolean(), + auto_scaling_role_arn :: undefined | binary(), + maximum_units :: undefined | pos_integer(), + minimum_units :: undefined | pos_integer(), + scaling_policies :: undefined | [#ddb2_auto_scaling_policy_description{}]}). + +-record(ddb2_provisioned_throughput_override, + {read_capacity_units :: undefined | pos_integer()}). + +-record(ddb2_replica_global_secondary_index_description, + {index_name :: undefined | binary(), + provisioned_throughput_override :: undefined | #ddb2_provisioned_throughput_override{}}). + +-record(ddb2_replica_global_secondary_index_auto_scaling_description, + {index_name :: undefined | binary(), + index_status :: undefined | index_status(), + provisioned_read_capacity_auto_scaling_settings :: undefined | #ddb2_auto_scaling_settings_description{}, + provisioned_write_capacity_auto_scaling_settings :: undefined | #ddb2_auto_scaling_settings_description{}}). + +-record(ddb2_replica_auto_scaling_description, + {global_secondary_indexes :: undefined | [#ddb2_replica_global_secondary_index_auto_scaling_description{}], + region_name :: undefined | binary(), + replica_provisioned_read_capacity_auto_scaling_settings :: undefined | #ddb2_auto_scaling_settings_description{}, + replica_provisioned_write_capacity_auto_scaling_settings :: undefined | #ddb2_auto_scaling_settings_description{}, + replica_status :: undefined | replica_status()}). + -record(ddb2_replica_description, - {region_name :: undefined | binary() + {global_secondary_indexes :: undefined | [#ddb2_replica_global_secondary_index_description{}], + kms_master_key_id :: undefined | binary(), + provisioned_throughput_override :: undefined | #ddb2_provisioned_throughput_override{}, + region_name :: undefined | binary(), + replica_status :: undefined | replica_status(), + replica_status_description :: undefined | binary(), + replica_status_percent_progress :: undefined | binary() }). +-record(ddb2_replica_global_secondary_index_settings_description, + {index_name :: undefined | binary(), + index_status :: undefined | index_status(), + provisioned_read_capacity_auto_scaling_settings :: undefined | #ddb2_auto_scaling_settings_description{}, + provisioned_read_capacity_units :: undefined | pos_integer(), + provisioned_write_capacity_auto_scaling_settings :: undefined | #ddb2_auto_scaling_settings_description{}, + provisioned_write_capacity_units :: undefined | pos_integer()}). + +-record(ddb2_billing_mode_summary, + {billing_mode :: undefined | erlcloud_ddb2:billing_mode(), + last_update_to_pay_per_request_date_time :: undefined | date_time()}). + +-record(ddb2_replica_settings_description, + {region_name :: undefined | binary(), + replica_billing_mode_summary :: undefined | #ddb2_billing_mode_summary{}, + replica_global_secondary_index_settings :: undefined | [#ddb2_replica_global_secondary_index_settings_description{}], + replica_provisioned_read_capacity_auto_scaling_settings :: undefined | #ddb2_auto_scaling_settings_description{}, + replica_provisioned_read_capacity_units :: undefined | number(), + replica_provisioned_write_capacity_auto_scaling_settings :: undefined | #ddb2_auto_scaling_settings_description{}, + replica_provisioned_write_capacity_units :: undefined | number(), + replica_status :: undefined | replica_status()}). + -record(ddb2_global_table_description, {creation_date_time :: undefined | number(), global_table_arn :: undefined | binary(), @@ -81,14 +154,18 @@ -record(ddb2_table_description, {attribute_definitions :: undefined | erlcloud_ddb2:attr_defs(), + billing_mode_summary :: undefined | #ddb2_billing_mode_summary{}, creation_date_time :: undefined | number(), + deletion_protection_enabled :: undefined | boolean(), global_secondary_indexes :: undefined | [#ddb2_global_secondary_index_description{}], + global_table_version :: undefined | binary(), item_count :: undefined | integer(), key_schema :: undefined | erlcloud_ddb2:key_schema(), latest_stream_arn :: undefined | binary(), latest_stream_label :: undefined | binary(), local_secondary_indexes :: undefined | [#ddb2_local_secondary_index_description{}], provisioned_throughput :: undefined | #ddb2_provisioned_throughput_description{}, + replicas :: undefined | [#ddb2_replica_description{}], restore_summary :: undefined | #ddb2_restore_summary{}, sse_description :: undefined | erlcloud_ddb2:sse_description(), stream_specification :: undefined | erlcloud_ddb2:stream_specification(), @@ -98,6 +175,11 @@ table_status :: undefined | table_status() }). +-record(ddb2_table_auto_scaling_description, + {replicas :: undefined | [#ddb2_replica_auto_scaling_description{}], + table_name :: undefined | binary(), + table_status :: undefined | table_status()}). + -record(ddb2_consumed_capacity, {capacity_units :: undefined | number(), global_secondary_indexes :: undefined | [{erlcloud_ddb2:index_name(), number()}], @@ -150,6 +232,10 @@ {global_table_description :: undefined | #ddb2_global_table_description{} }). +-record(ddb2_describe_global_table_settings, + {global_table_name :: undefined | binary(), + replica_settings :: undefined | [#ddb2_replica_settings_description{}]}). + -record(ddb2_describe_limits, {account_max_read_capacity_units :: undefined | pos_integer(), account_max_write_capacity_units :: undefined | pos_integer(), @@ -161,6 +247,9 @@ {table :: undefined | #ddb2_table_description{} }). +-record(ddb2_describe_table_replica_auto_scaling, + {table_auto_scaling_description :: undefined | #ddb2_table_auto_scaling_description{}}). + -record(ddb2_time_to_live_description, {attribute_name :: undefined | erlcloud_ddb2:attr_name(), time_to_live_status :: undefined | erlcloud_ddb2:time_to_live_status() @@ -221,12 +310,19 @@ {global_table_description :: undefined | #ddb2_global_table_description{} }). +-record(ddb2_update_global_table_settings, + {global_table_name :: undefined | binary(), + replica_settings :: undefined | [#ddb2_replica_settings_description{}]}). + -record(ddb2_update_table, {table_description :: undefined | #ddb2_table_description{} }). +-record(ddb2_update_table_replica_auto_scaling, + {table_auto_scaling_description :: undefined | #ddb2_table_auto_scaling_description{}}). + -record(ddb2_time_to_live_specification, - {attribute_name :: undefined | erlcloud_ddb2:attr_name(), + {attribute_name :: undefined | erlcloud_ddb2:attr_name(), enabled :: undefined | boolean() }). @@ -273,7 +369,7 @@ provisioned_throughput :: undefined | #ddb2_provisioned_throughput{}, table_arn :: undefined | binary(), table_creation_date_time :: undefined | number(), - table_id :: undefined | number(), + table_id :: undefined | binary(), table_name :: undefined | binary(), table_size_bytes :: undefined | integer() }). @@ -300,10 +396,28 @@ {global_secondary_indexes :: undefined | [#ddb2_global_secondary_index_info{}], local_secondary_indexes :: undefined | [#ddb2_local_secondary_index_info{}], sse_description :: undefined | erlcloud_ddb2:sse_description(), - stream_description :: undefined | #ddb2_stream_description{}, + stream_description :: undefined | #ddb2_stream_description{} | {boolean(), erlcloud_ddb2:stream_view_type()}, time_to_live_description :: undefined | #ddb2_time_to_live_description{} }). +-record(ddb2_transact_write_items, + {consumed_capacity :: undefined | [#ddb2_consumed_capacity{}], + item_collection_metrics :: undefined | [{erlcloud_ddb2:table_name(), [#ddb2_item_collection_metrics{}]}], + % AWS documentation infer that it should be possible to get old return + % values upon condition failure, but I have been unable to do so. + % Still, let's put a field so we can return {ok, []} on success. + attributes :: undefined + }). + +-record(ddb2_item_response, + {item :: erlcloud_ddb2:out_item() + }). + +-record(ddb2_transact_get_items, + {consumed_capacity :: undefined | [#ddb2_consumed_capacity{}], + responses :: undefined | [#ddb2_item_response{}] + }). + -record(ddb2_backup_description, {backup_details :: undefined | #ddb2_backup_details{}, source_table_details :: undefined | #ddb2_source_table_details{}, @@ -340,4 +454,4 @@ -record(ddb2_restore_table_to_point_in_time, {table_description :: undefined | #ddb2_table_description{} }). --endif. \ No newline at end of file +-endif. diff --git a/include/erlcloud_ddb_streams.hrl b/include/erlcloud_ddb_streams.hrl index 9fda7eebd..2ecfde30a 100644 --- a/include/erlcloud_ddb_streams.hrl +++ b/include/erlcloud_ddb_streams.hrl @@ -23,7 +23,8 @@ table_name :: undefined | erlcloud_ddb_streams:table_name() }). -record(ddb_streams_stream_record, - {keys :: undefined | erlcloud_ddb_streams:key(), + {approximate_creation_date_time :: undefined | number(), + keys :: undefined | erlcloud_ddb_streams:key(), new_image :: undefined | erlcloud_ddb_streams:item(), old_image :: undefined | erlcloud_ddb_streams:item(), sequence_number :: undefined | erlcloud_ddb_streams:sequence_number(), diff --git a/include/erlcloud_ec2.hrl b/include/erlcloud_ec2.hrl index 8da059e32..e7f837d9f 100644 --- a/include/erlcloud_ec2.hrl +++ b/include/erlcloud_ec2.hrl @@ -29,24 +29,24 @@ image_id::string(), min_count=1::pos_integer(), max_count=1::pos_integer(), - key_name::string(), + key_name::undefined | string(), group_set=["default"]::[string()], user_data::undefined|binary(), instance_type::string(), - availability_zone::string(), - placement_group::string(), - kernel_id::string(), - ramdisk_id::string(), + availability_zone::undefined | string(), + placement_group::undefined | string(), + kernel_id::undefined | string(), + ramdisk_id::undefined | string(), block_device_mapping=[]::[ec2_block_device_mapping()], monitoring_enabled=false::boolean(), subnet_id::string(), disable_api_termination=false::boolean(), - instance_initiated_shutdown_behavior::ec2_shutdown_behavior(), + instance_initiated_shutdown_behavior::undefined | ec2_shutdown_behavior(), net_if=[] :: [#ec2_net_if{}], ebs_optimized = false :: boolean(), - iam_instance_profile_name = undefined :: string(), - spot_price::string(), - weighted_capacity::number() + iam_instance_profile_name :: undefined | string(), + spot_price::undefined | string(), + weighted_capacity::undefined | number() }). -record(ec2_image_spec, { image_location::string(), @@ -70,15 +70,15 @@ }). -record(spot_fleet_request_config_spec, { allocation_strategy::undefined|lowest_price|diversified, - client_token::string(), - excess_capacity_termination_policy::no_termination|default, + client_token::undefined|string(), + excess_capacity_termination_policy::undefined|no_termination|default, iam_fleet_role::string(), launch_specification=[]::[#ec2_instance_spec{}], spot_price::string(), target_capacity::pos_integer(), - terminate_instances_with_expiration::true|false, - valid_from::datetime(), - valid_until::datetime() + terminate_instances_with_expiration::undefined|true|false, + valid_from::undefined|datetime(), + valid_until::undefined|datetime() }). -record(ec2_spot_fleet_request, { spot_fleet_request_config::#spot_fleet_request_config_spec{} diff --git a/include/erlcloud_ecs.hrl b/include/erlcloud_ecs.hrl index 327bdb575..2e428d7be 100644 --- a/include/erlcloud_ecs.hrl +++ b/include/erlcloud_ecs.hrl @@ -38,18 +38,18 @@ }). -record(ecs_deployment, { - created_at :: undefined | pos_integer(), + created_at :: undefined | number(), desired_count :: undefined | pos_integer(), id :: undefined | binary(), - pending_count :: undefined | pos_integer(), - running_count :: undefined | pos_integer(), + pending_count :: undefined | non_neg_integer(), + running_count :: undefined | non_neg_integer(), status :: undefined | binary(), task_definition:: undefined | binary(), - updated_at :: undefined | pos_integer() + updated_at :: undefined | number() }). -record(ecs_event, { - created_at :: undefined | pos_integer(), + created_at :: undefined | number(), id :: undefined | binary(), message :: undefined | binary() }). @@ -62,26 +62,26 @@ }). -record(ecs_cluster, { - active_services_count :: undefined | pos_integer(), + active_services_count :: undefined | non_neg_integer(), cluster_arn :: undefined | binary(), cluster_name :: undefined | binary(), - pending_tasks_count :: undefined | pos_integer(), - registered_container_instances_count :: undefined | pos_integer(), - running_tasks_count :: undefined | pos_integer(), + pending_tasks_count :: undefined | non_neg_integer(), + registered_container_instances_count :: undefined | non_neg_integer(), + running_tasks_count :: undefined | non_neg_integer(), status :: undefined | binary() }). -record(ecs_service, { cluster_arn :: undefined | binary(), - created_at :: undefined | pos_integer(), + created_at :: undefined | number(), deployment_configuration :: undefined | #ecs_deployment_configuration{}, deployments :: undefined | [#ecs_deployment{}], desired_count :: undefined | pos_integer(), events :: undefined | [#ecs_event{}], load_balancers :: undefined | [#ecs_load_balancer{}], - pending_count :: undefined | pos_integer(), + pending_count :: undefined | non_neg_integer(), role_arn :: undefined | binary(), - running_count :: undefined | pos_integer(), + running_count :: undefined | non_neg_integer(), service_arn :: undefined | binary(), service_name :: undefined | binary(), status :: undefined | binary(), @@ -89,11 +89,11 @@ }). -record(ecs_resource, { - double_value :: undefined | pos_integer(), - integer_value :: undefined | pos_integer(), - long_value :: undefined | pos_integer(), + double_value :: undefined | non_neg_integer(), + integer_value :: undefined | non_neg_integer(), + long_value :: undefined | non_neg_integer(), name :: undefined | binary(), - string_set_value :: undefined | binary(), + string_set_value :: undefined | [binary()], type :: undefined | binary() }). @@ -109,10 +109,10 @@ attributes :: undefined | [#ecs_attribute{}], container_instance_arn :: undefined | binary(), ec2_instance_id :: undefined | binary(), - pending_tasks_count :: undefined | pos_integer(), + pending_tasks_count :: undefined | non_neg_integer(), registered_resources :: undefined | [#ecs_resource{}], remaining_resources :: undefined | [#ecs_resource{}], - running_tasks_count :: undefined | pos_integer(), + running_tasks_count :: undefined | non_neg_integer(), status :: undefined | binary(), version_info :: undefined | #ecs_version_info{} }). @@ -210,7 +210,7 @@ -record(ecs_task_definition, { container_definitions :: undefined | [#ecs_container_definition{}], family :: undefined | binary(), - network_mode :: undefined | binary(), + network_mode :: undefined | ecs_network_mode(), requires_attributes :: undefined | [#ecs_attribute{}], revision :: undefined | pos_integer(), status :: undefined | binary(), @@ -226,14 +226,26 @@ protocol :: undefined | ecs_protocol() }). +-record(ecs_network_interface, { + attachment_id :: undefined | binary(), + private_ipv4_address :: undefined | binary() +}). + -record(ecs_container, { container_arn :: undefined | binary(), - exit_code :: undefined | pos_integer(), + cpu :: undefined | binary(), + exit_code :: undefined | integer(), + health_status :: undefined | binary(), + image :: undefined | binary(), last_status :: undefined | binary(), + memory_reservation :: undefined | binary(), name :: undefined | binary(), network_bindings :: undefined | [#ecs_network_binding{}], + network_interfaces :: undefined | [#ecs_network_interface{}], reason :: undefined | binary(), + runtime_id :: undefined | binary(), task_arn :: undefined | binary() + }). -record(ecs_container_override, { @@ -247,20 +259,51 @@ task_role_arn :: undefined | binary() }). +-record(ecs_attachment_detail, { + name :: undefined | binary(), + value :: undefined | binary() +}). + +-record(ecs_attachment, { + id :: undefined | binary(), + type :: undefined | binary(), + status :: undefined | binary(), + details :: undefined | [#ecs_attachment_detail{}] +}). + +-record(ecs_tag, { + key :: undefined | binary(), + value :: undefined | binary() +}). + -record(ecs_task, { + attachments :: undefined | [#ecs_attachment{}], + availability_zone :: undefined | binary(), cluster_arn :: undefined | binary(), + connectivity :: undefined | binary(), + connectivity_at :: undefined | number(), container_instance_arn :: undefined | binary(), containers :: undefined | [#ecs_container{}], - created_at :: undefined | pos_integer(), - desired_status:: undefined | binary(), + cpu :: undefined | binary(), + created_at :: undefined | number(), + desired_status :: undefined | binary(), + group :: undefined | binary(), + health_status :: undefined | binary(), last_status :: undefined | binary(), + launch_type :: undefined | binary(), + memory :: undefined | binary(), overrides :: undefined | #ecs_task_override{}, - started_at :: undefined | pos_integer(), + platform_version :: undefined | binary(), + pull_started_at :: undefined | number(), + pull_stopped_at :: undefined | number(), + started_at :: undefined | number(), started_by :: undefined | binary(), - stopped_at :: undefined | pos_integer(), + stopped_at :: undefined | number(), stopped_reason :: undefined | binary(), + tags :: undefined | [#ecs_tag{}], task_arn :: undefined | binary(), - task_definition_arn :: undefined | binary() + task_definition_arn :: undefined | binary(), + version :: undefined | non_neg_integer() }). -record(ecs_describe_tasks, { diff --git a/include/erlcloud_iam.hrl b/include/erlcloud_iam.hrl new file mode 100644 index 000000000..ad852fc1f --- /dev/null +++ b/include/erlcloud_iam.hrl @@ -0,0 +1,3 @@ +-type context_key() :: context_key_name | context_key_type | context_key_values. +-type context_entry() :: [{context_key(), string() | list()}]. +-type context_entries() :: list(context_entry()). diff --git a/include/erlcloud_mon.hrl b/include/erlcloud_mon.hrl index d85192647..e8c7e7276 100644 --- a/include/erlcloud_mon.hrl +++ b/include/erlcloud_mon.hrl @@ -30,6 +30,13 @@ %%------------------------------------------------------------------------------ -type unit() :: string(). +%%------------------------------------------------------------------------------ +%% @doc The statistic for the metric, other than percentiles. +%% Valid Values: SampleCount | Average | Sum | Minimum | Maximum +%% @end +%%------------------------------------------------------------------------------ +-type statistic() :: string(). + %%------------------------------------------------------------------------------ %% @doc Dimension %% The Dimension data type further expands on the identity of a metric using a Name, Value pair. @@ -73,4 +80,4 @@ }). -type metric_datum() :: #metric_datum{}. --endif. \ No newline at end of file +-endif. diff --git a/include/erlcloud_ssm.hrl b/include/erlcloud_ssm.hrl new file mode 100644 index 000000000..7a757f4cc --- /dev/null +++ b/include/erlcloud_ssm.hrl @@ -0,0 +1,43 @@ +-ifndef(erlcloud_ssm_hrl). +-define(erlcloud_ssm_hrl, 0). + +-include("erlcloud.hrl"). + +%%%------------------------------------------------------------------------------ +%% +%% Common data types +%% +%%%------------------------------------------------------------------------------ + +-record(ssm_parameter, { + arn :: undefined | binary(), + data_type :: undefined | binary(), + last_modified_date :: undefined | float(), + name :: undefined | binary(), + selector :: undefined | binary(), + source_result :: undefined | binary(), + type :: undefined | binary(), + value :: undefined | binary(), + version :: undefined | non_neg_integer() +}). + +-record(ssm_get_parameter, { + parameter :: undefined | #ssm_parameter{} +}). + +-record(ssm_get_parameters, { + invalid_parameters :: undefined | list(binary()), + parameters :: undefined | [#ssm_parameter{}] +}). + +-record(ssm_get_parameters_by_path, { + next_token :: undefined | binary(), + parameters :: undefined | [#ssm_parameter{}] +}). + +-record(ssm_put_parameter, { + tier :: undefined | binary(), + version :: undefined | non_neg_integer() +}). + +-endif. diff --git a/include/erlcloud_workspaces.hrl b/include/erlcloud_workspaces.hrl new file mode 100644 index 000000000..730f59203 --- /dev/null +++ b/include/erlcloud_workspaces.hrl @@ -0,0 +1,107 @@ +-ifndef(erlcloud_workspaces_hrl). +-define(erlcloud_workspaces_hrl, 0). + +-include("erlcloud.hrl"). + +-define(WORKSPACES_LIMIT, 25). + + +%%%------------------------------------------------------------------------------ +%% +%% Common data types +%% +%%%------------------------------------------------------------------------------ + +-record(workspace_modification_state, { + resource :: undefined | binary(), + state :: undefined | binary() +}). + +-record(workspace_properties, { + computer_type_name :: undefined | binary(), + root_volume_size_gib :: undefined | non_neg_integer(), + running_mode :: undefined | binary(), + running_mode_auto_stop_timeout_in_minutes :: undefined | non_neg_integer(), + user_volume_size_gib :: undefined | non_neg_integer() +}). + +-record(workspace, { + bundle_id :: undefined | binary(), + computer_name :: undefined | binary(), + directory_id :: undefined | binary(), + error_code :: undefined | binary(), + error_message :: undefined | binary(), + ip_address :: undefined | binary(), + modification_states :: undefined | [#workspace_modification_state{}], + root_volume_encryption_enabled :: undefined | boolean(), + state :: undefined | binary(), + subnet_id :: undefined | binary(), + user_name :: undefined | binary(), + user_volume_encryption_enabled :: undefined | boolean(), + volume_encryption_key :: undefined | binary(), + workspace_id :: undefined | binary(), + workspace_properties :: undefined | #workspace_properties{} +}). + +-record(workspaces_selfservice_permissions, { + change_compute_type :: undefined | binary(), + increase_volume_size :: undefined | binary(), + rebuild_workspace :: undefined | binary(), + restart_workspace :: undefined | binary(), + switch_running_mode :: undefined | binary() +}). + +-record(workspace_access_properties, { + device_type_android :: undefined | binary(), + device_type_chrome_os :: undefined | binary(), + device_type_ios :: undefined | binary(), + device_type_osx :: undefined | binary(), + device_type_web :: undefined | binary(), + device_type_windows :: undefined | binary(), + device_type_zero_client :: undefined | binary() +}). + +-record(workspace_creation_properties, { + custom_security_group_id :: undefined | binary(), + default_ou :: undefined | binary(), + enable_internet_access :: undefined | boolean(), + enable_maintenance_mode :: undefined | boolean(), + enable_work_docs :: undefined | boolean(), + user_enabled_as_local_administrator :: undefined | boolean() +}). + +-record(workspace_directory, { + alias :: undefined | binary(), + customer_user_name :: undefined | binary(), + directory_id :: undefined | binary(), + directory_name :: undefined | binary(), + directory_type :: undefined | binary(), + dns_ip_address :: undefined | [binary()], + iam_role_id :: undefined | binary(), + ip_group_ids :: undefined | [binary()], + registration_code :: undefined | binary(), + selfservice_permissions :: undefined | #workspaces_selfservice_permissions{}, + state :: undefined | binary(), + subnet_ids :: undefined | [binary()], + tenancy :: undefined | binary(), + workspace_access_properties :: undefined | #workspace_access_properties{}, + workspace_creation_properties :: undefined | #workspace_creation_properties{}, + workspace_security_group_id :: undefined | binary() +}). + +-record(describe_workspaces, { + next_token :: undefined | binary(), + workspaces :: undefined | [#workspace{}] +}). + +-record(describe_workspace_directories, { + next_token :: undefined | binary(), + workspace_directories :: undefined | [#workspace_directory{}] +}). + +-record(workspaces_tag, { + key :: undefined | binary(), + value :: undefined | binary() +}). + +-endif. \ No newline at end of file diff --git a/include/erlcloud_xmerl.hrl b/include/erlcloud_xmerl.hrl index c3c481af9..08228ee04 100644 --- a/include/erlcloud_xmerl.hrl +++ b/include/erlcloud_xmerl.hrl @@ -3,7 +3,7 @@ -include_lib("xmerl/include/xmerl.hrl"). --type(xmerl_xpath_doc_nodes() :: #xmlElement{} | #xmlAttribute{} | #xmlText{} | #xmlPI{} | #xmlComment{} | #xmlNsNode{}). +-type(xmerl_xpath_doc_nodes() :: #xmlElement{} | #xmlAttribute{} | #xmlText{} | #xmlPI{} | #xmlComment{} | #xmlNsNode{} | #xmlObj{}). -type(xmerl_xpath_node_entity() :: #xmlDocument{} | xmerl_xpath_doc_nodes()). -type(xmerl_xpath_doc_entity() :: #xmlDocument{} | [xmerl_xpath_doc_nodes()]). diff --git a/rebar.config b/rebar.config index 5837d3a0c..2397ce3a7 100644 --- a/rebar.config +++ b/rebar.config @@ -17,12 +17,14 @@ warn_unused_vars]}. {deps, [ - {jsx, "2.8.0"}, - {lhttpc, "1.6.1"}, - {eini, "1.2.5"}, - {base16, "1.0.0"} + {jsx, "2.11.0"}, + {lhttpc, "1.7.1"}, + {eini, "1.2.9"}, + {base16, "2.0.1"} ]}. +{project_plugins, [rebar3_hex]}. + {overrides, [ %% do not pull in the covertool plugin or repo, cause it fetches rebar and @@ -32,6 +34,17 @@ {profiles, [ - {test, [{deps, [{meck, "0.8.4"}]}]} - ,{warnings, [{erl_opts, [warnings_as_errors]}]} + {dialyzer, [{deps, [hackney]}, {erl_opts, [warnings_as_errors]}]}, + {test, [{deps, [{meck, "0.9.0"}]}, {erl_opts, [warnings_as_errors]}]}, + {warnings, [{erl_opts, [warnings_as_errors]}]} ]}. + +{post_hooks, [{clean, "rm -rf .dialyzer_plt"}]}. + +{pre_hooks, [{clean, "rm -rf erl_crash.dump *.log"}]}. + +{dialyzer, [ + {plt_location, ".dialyzer_plt"}, + {plt_apps, all_deps}, + {plt_extra_apps, [hackney]} + ]}. diff --git a/rebar.config.script b/rebar.config.script deleted file mode 100644 index 3be6f90f8..000000000 --- a/rebar.config.script +++ /dev/null @@ -1,15 +0,0 @@ -%% -*- mode: erlang; -*- -case erlang:function_exported(rebar3, main, 1) of - true -> % rebar3 - CONFIG; - false -> % rebar 2.x or older - %% Use git-based deps - %% profiles - [{deps, [{meck, ".*",{git, "https://github.com/eproxus/meck.git", {tag, "0.8.4"}}}, - {jsx, ".*", {git, "git://github.com/talentdeficit/jsx.git", {tag, "2.8.0"}}}, - %% {hackney, ".*", {git, "git://github.com/benoitc/hackney.git", {tag, "1.2.0"}}}, - {eini, ".*", {git, "https://github.com/erlcloud/eini.git", {tag, "1.2.5"}}}, - {lhttpc, ".*", {git, "git://github.com/erlcloud/lhttpc", {tag, "1.6.1"}}}, - {base16, ".*", {git, "https://github.com/goj/base16.git", {tag, "1.0.0"}}}]} - | lists:keydelete(deps, 1, CONFIG)] -end. diff --git a/rebar.lock b/rebar.lock index 15d035382..8e7b26280 100644 --- a/rebar.lock +++ b/rebar.lock @@ -1,12 +1,17 @@ -{"1.1.0", -[{<<"base16">>,{pkg,<<"base16">>,<<"1.0.0">>},0}, - {<<"eini">>,{pkg,<<"eini">>,<<"1.2.5">>},0}, - {<<"jsx">>,{pkg,<<"jsx">>,<<"2.8.0">>},0}, - {<<"lhttpc">>,{pkg,<<"lhttpc">>,<<"1.6.1">>},0}]}. +{"1.2.0", +[{<<"base16">>,{pkg,<<"base16">>,<<"2.0.1">>},0}, + {<<"eini">>,{pkg,<<"eini">>,<<"1.2.9">>},0}, + {<<"jsx">>,{pkg,<<"jsx">>,<<"2.11.0">>},0}, + {<<"lhttpc">>,{pkg,<<"lhttpc">>,<<"1.7.1">>},0}]}. [ {pkg_hash,[ - {<<"base16">>, <<"283644E2B21BD5915ACB7178BED7851FB07C6E5749B8FAD68A53C501092176D9">>}, - {<<"eini">>, <<"98E4988474FAAF821E3511579090AF989096ADBDB5F7BFAA03AA0A9AC80296B2">>}, - {<<"jsx">>, <<"749BEC6D205C694AE1786D62CEA6CC45A390437E24835FD16D12D74F07097727">>}, - {<<"lhttpc">>, <<"8592681D9E86E36E33E8B0383709F973FB7737487E013EE2624A665C41A6B8EF">>}]} + {<<"base16">>, <<"F0549F732E03BE8124ED0D19FD5EE52146CC8BE24C48CBC3F23AB44B157F11A2">>}, + {<<"eini">>, <<"FCC3CBD49BBDD9A1D9735C7365DAFFCD84481CCE81E6CB80537883AA44AC4895">>}, + {<<"jsx">>, <<"08154624050333919B4AC1B789667D5F4DB166DC50E190C4D778D1587F102EE0">>}, + {<<"lhttpc">>, <<"8522AF9877765C33318A3AE486BE69BC165E835D05C3334A8166FD7B318D446B">>}]}, +{pkg_hash_ext,[ + {<<"base16">>, <<"06EA2D48343282E712160BA89F692B471DB8B36ABE8394F3445FF9032251D772">>}, + {<<"eini">>, <<"DA64AE8DB7C2F502E6F20CDF44CD3D9BE364412B87FF49FEBF282540F673DFCB">>}, + {<<"jsx">>, <<"EED26A0D04D217F9EECEFFFB89714452556CF90EB38F290A27A4D45B9988F8C0">>}, + {<<"lhttpc">>, <<"154EEB27692482B52BE86406DCD1D18A2405CAFCE0E8DAA4A1A7BFA7FE295896">>}]} ]. diff --git a/src/erlcloud.app.src b/src/erlcloud.app.src index 06268f059..6c54e36a3 100644 --- a/src/erlcloud.app.src +++ b/src/erlcloud.app.src @@ -3,7 +3,7 @@ {application, erlcloud, [{description, "AWS APIs library for Erlang"}, - {vsn, git}, + {vsn, "git"}, {registered, []}, {applications, [stdlib, kernel, @@ -18,9 +18,13 @@ %% hackney, lhttpc]}, {modules, []}, - {env, []}, - {licenses, ["MIT"]}, - {links, [{"Github", "https://github.com/erlcloud/erlcloud"}]}, - {maintainers, []} + {env, [ + % Example [{<<"kinesis">>, [<<"myAZ1.amazonaws.com">>, <<"myAZ2.amazonaws.com">>]}] + % or via ENV [{<<"kinesis">>, {env, "KINESIS_VPC_ENDPOINTS"}] + {services_vpc_endpoints, []}, + {ec2_meta_host_port, "169.254.169.254:80"} % allows for an alternative instance metadata service + ]}, + {licenses, ["BSD-2-Clause"]}, + {links, [{"Github", "https://github.com/erlcloud/erlcloud"}]} ] }. diff --git a/src/erlcloud.erl b/src/erlcloud.erl index 11138d0e2..1e0666359 100644 --- a/src/erlcloud.erl +++ b/src/erlcloud.erl @@ -5,6 +5,8 @@ -include("erlcloud_aws.hrl"). -define(APP, erlcloud). +-export_type([aws_config/0]). + start() -> application:load(?APP), {ok, Apps} = application:get_key(?APP, applications), diff --git a/src/erlcloud_access_analyzer.erl b/src/erlcloud_access_analyzer.erl new file mode 100644 index 000000000..97094d003 --- /dev/null +++ b/src/erlcloud_access_analyzer.erl @@ -0,0 +1,240 @@ +-module(erlcloud_access_analyzer). + +-include("erlcloud.hrl"). +-include("erlcloud_aws.hrl"). + +%% API +-export([ + list_analyzers/1, + list_analyzers/2, + list_analyzers_all/1, + list_analyzers_all/2, + get_analyzer/2, + create_analyzer/2, + delete_analyzer/2 +]). + +-type param_name() :: binary() | string() | atom(). +-type param_value() :: binary() | string() | atom() | integer(). +-type params() :: [param_name() | {param_name(), param_value()}]. + +-type analyzer() :: proplist(). +-type analyzers() :: [analyzer()]. +-type token() :: binary(). + +-type create_analyzer_spec() :: proplist(). + +%% ----------------------------------------------------------------------------- +%% Exported functions +%% ----------------------------------------------------------------------------- + +-spec list_analyzers(AwsConfig) -> Result +when AwsConfig :: aws_config(), + Result :: {ok, analyzers()} | {ok, analyzers(), token()} | {error, term()}. +list_analyzers(AwsConfig) + when is_record(AwsConfig, aws_config) -> + list_analyzers(AwsConfig, _Params = []); +list_analyzers(Params) -> + AwsConfig = erlcloud_aws:default_config(), + list_analyzers(AwsConfig, Params). + +-spec list_analyzers(AwsConfig, Params) -> Result +when AwsConfig :: aws_config(), + Params :: params(), + Result :: {ok, analyzers()} | {ok, analyzers(), token()} | {error, term()}. +list_analyzers(AwsConfig, Params) -> + Path = ["analyzer"], + case request(AwsConfig, _Method = get, Path, Params) of + {ok, Response} -> + Analyzers = proplists:get_value(<<"analyzers">>, Response), + case proplists:get_value(<<"nextToken">>, Response) of + undefined -> + {ok, Analyzers}; + Token -> + {ok, Analyzers, Token} + end; + {error, Reason} -> + {error, Reason} + end. + +-spec list_analyzers_all(AwsConfig) -> Result +when AwsConfig :: aws_config(), + Result :: {ok, analyzers()} | {ok, analyzers(), token()} | {error, term()}. +list_analyzers_all(AwsConfig) + when is_record(AwsConfig, aws_config) -> + list_analyzers_all(AwsConfig, _Params = []); +list_analyzers_all(Params) -> + AwsConfig = erlcloud_aws:default_config(), + list_analyzers(AwsConfig, Params). + +-spec list_analyzers_all(AwsConfig, Params) -> Result +when AwsConfig :: aws_config(), + Params :: params(), + Result :: {ok, analyzers()} | {ok, analyzers(), token()} | {error, term()}. +list_analyzers_all(AwsConfig, Params) -> + case list_analyzers(AwsConfig, Params) of + {ok, Analyzers} -> + {ok, Analyzers}; + {ok, Analyzers, Token} -> + list_analyzers_next(AwsConfig, Params, Token, Analyzers); + {error, Reason} -> + {error, Reason} + end. + +-spec get_analyzer(AwsConfig, AnalyzerName) -> Result +when AwsConfig :: aws_config(), + AnalyzerName :: binary() | string(), + Result :: {ok, analyzer()} | {error, not_found} | {error, term()}. +get_analyzer(AwsConfig, AnalyzerName) -> + Path = ["analyzer", AnalyzerName], + case request(AwsConfig, _Method = get, Path) of + {ok, Response} -> + Analyzer = proplists:get_value(<<"analyzer">>, Response), + {ok, Analyzer}; + {error, {<<"ResourceNotFoundException">>, _Message}} -> + {error, not_found}; + {error, Reason} -> + {error, Reason} + end. + +-spec create_analyzer(AwsConfig, Spec) -> Result +when AwsConfig :: aws_config(), + Spec :: create_analyzer_spec(), + Result :: {ok, Arn :: binary()} | {error, term()}. +create_analyzer(AwsConfig, Spec) -> + Path = ["analyzer"], + RequestBody = jsx:encode(Spec), + case request(AwsConfig, _Method = put, Path, _Params = [], RequestBody) of + {ok, Response} -> + Arn = proplists:get_value(<<"arn">>, Response), + {ok, Arn}; + {error, Reason} -> + {error, Reason} + end. + +-spec delete_analyzer(AwsConfig, AnalyzerName) -> Result +when AwsConfig :: aws_config(), + AnalyzerName :: binary() | string(), + Result :: ok | {error, not_found} | {error, term()}. +delete_analyzer(AwsConfig, AnalyzerName) -> + delete_analyser(AwsConfig, AnalyzerName, _Params = []). + +-spec delete_analyser(AwsConfig, AnalyzerName, Params) -> Result +when AwsConfig :: aws_config(), + AnalyzerName :: binary() | string(), + Params :: params(), + Result :: ok | {error, not_found} | {error, term()}. +delete_analyser(AwsConfig, AnalyzerName, Params) -> + Path = ["analyzer", AnalyzerName], + case request(AwsConfig, _Method = delete, Path, Params) of + ok -> + ok; + {error, {<<"ResourceNotFoundException">>, _Message}} -> + {error, not_found}; + {error, Reason} -> + {error, Reason} + end. + +%% ----------------------------------------------------------------------------- +%% Local functions +%% ----------------------------------------------------------------------------- + +list_analyzers_next(AwsConfig, Params, Token0, Analyzers0) -> + case list_analyzers(AwsConfig, [{<<"nextToken">>, Token0} | Params]) of + {ok, Analyzers} -> + Analyzers1 = lists:append(Analyzers0, Analyzers), + {ok, Analyzers1}; + {ok, Analyzers, Token1} -> + Analyzers1 = lists:append(Analyzers0, Analyzers), + list_analyzers_next(AwsConfig, Params, Token1, Analyzers1); + {error, Reason} -> + {error, Reason} + end. + +request(AwsConfig, Method, Path) -> + request(AwsConfig, Method, Path, _Params = []). + +request(AwsConfig, Method, Path, Params) -> + request(AwsConfig, Method, Path, Params, _RequestBody = <<>>). + +request(AwsConfig0, Method, Path, Params, RequestBody) -> + case erlcloud_aws:update_config(AwsConfig0) of + {ok, AwsConfig1} -> + AwsRequest0 = init_request(AwsConfig1, Method, Path, Params, RequestBody), + AwsRequest1 = erlcloud_retry:request(AwsConfig1, AwsRequest0, fun should_retry/1), + case AwsRequest1#aws_request.response_type of + ok -> + decode_response(AwsRequest1); + error -> + decode_error(AwsRequest1) + end; + {error, Reason} -> + {error, Reason} + end. + +init_request(AwsConfig, Method, Path, Params, Payload) -> + Host = AwsConfig#aws_config.access_analyzer_host, + Service = "access-analyzer", + NormPath = norm_path(Path), + NormParams = norm_params(Params), + Region = erlcloud_aws:aws_region_from_host(Host), + Headers = [{"host", Host}, {"content-type", "application/json"}], + SignedHeaders = erlcloud_aws:sign_v4(Method, NormPath, AwsConfig, Headers, Payload, Region, Service, Params), + #aws_request{ + service = access_analyzer, + method = Method, + uri = "https://" ++ Host ++ NormPath ++ NormParams, + request_headers = SignedHeaders, + request_body = Payload + }. + +norm_path(Path) -> + binary_to_list(iolist_to_binary(["/" | lists:join("/", Path)])). + +norm_params([] = _Params) -> + ""; +norm_params(Params) -> + "?" ++ erlcloud_aws:canonical_query_string(Params). + +should_retry(Request) + when Request#aws_request.response_type == ok -> + Request; +should_retry(Request) + when Request#aws_request.response_type == error, + Request#aws_request.response_status == 429 -> + Request#aws_request{should_retry = true}; +should_retry(Request) + when Request#aws_request.response_type == error, + Request#aws_request.response_status >= 500 -> + Request#aws_request{should_retry = true}; +should_retry(Request) -> + Request#aws_request{should_retry = false}. + +decode_response(AwsRequest) -> + case AwsRequest#aws_request.response_body of + <<>> -> + ok; + ResponseBody -> + Json = jsx:decode(ResponseBody, [{return_maps, false}]), + {ok, Json} + end. + +decode_error(AwsRequest) -> + case AwsRequest#aws_request.error_type of + aws -> + Type = extract_error_type(AwsRequest), + Message = extract_error_message(AwsRequest), + {error, {Type, Message}}; + _ -> + erlcloud_aws:request_to_return(AwsRequest) + end. + +extract_error_type(AwsRequest) -> + Headers = AwsRequest#aws_request.response_headers, + Value = proplists:get_value("x-amzn-errortype", Headers), + iolist_to_binary(Value). + +extract_error_message(AwsRequest) -> + ResponseBody = AwsRequest#aws_request.response_body, + Object = jsx:decode(ResponseBody, [{return_maps, false}]), + proplists:get_value(<<"message">>, Object, <<>>). diff --git a/src/erlcloud_application_autoscaler.erl b/src/erlcloud_application_autoscaler.erl new file mode 100644 index 000000000..abe90f6af --- /dev/null +++ b/src/erlcloud_application_autoscaler.erl @@ -0,0 +1,774 @@ +-module(erlcloud_application_autoscaler). + +-include("erlcloud_aws.hrl"). +-include("erlcloud_as.hrl"). +-include("erlcloud_xmerl.hrl"). + +%% ------------------------------------------------------------------ +%% AWS Application Autoscaling Function Exports +%% ------------------------------------------------------------------ + +-export([delete_scaling_policy/5]). + +-export([delete_scheduled_action/2]). +-export([delete_scheduled_action/5]). + +-export([deregister_scalable_target/2]). +-export([deregister_scalable_target/4]). + +-export([describe_scalable_targets/2]). + +-export([describe_scaling_activities/2]). + +-export([describe_scaling_policies/2]). + +-export([describe_scheduled_actions/2]). + +-export([put_scaling_policy/2]). +-export([put_scaling_policy/5]). +-export([put_scaling_policy/7]). + +-export([put_scheduled_action/2]). +-export([put_scheduled_action/5]). +-export([put_scheduled_action/6]). +-export([put_scheduled_action/7]). +-export([put_scheduled_action/8]). + +-export([register_scalable_target/2]). +-export([register_scalable_target/4]). +-export([register_scalable_target/5]). +-export([register_scalable_target/7]). + +%% ------------------------------------------------------------------ +%% AWS Application Autoscaling Library Initialization Function Exports +%% ------------------------------------------------------------------ + +-export([configure/2, configure/3, configure/4, configure/5, + new/2, new/3, new/4, new/5]). +-export([default_config/0]). + +%% ------------------------------------------------------------------ +%% Macro Definitions +%% ------------------------------------------------------------------ + +-define(NUM_ATTEMPTS, 10). +-define(DEFAULT_MAX_RECORDS, 20). + +%% ------------------------------------------------------------------ +%% Type Definitions +%% ------------------------------------------------------------------ + +-type response_attribute() :: string() | integer(). +-type response_key() :: atom(). +-type response() :: [{response_key(), response_attribute()}]. +-type error_type() :: binary(). +-type error_message() :: binary(). +-type ok_error_response() :: {ok, jsx:json_term()} + | {error, metadata_not_available + | container_credentials_unavailable + | erlcloud_aws:httpc_result_error() + | {error_type(), error_message()}}. + +-type aws_aas_request_body() :: proplists:proplist(). + +-spec extract_alarm(J :: proplist()) -> response(). +extract_alarm(J) -> + erlcloud_json:decode([ + {alarm_name, <<"AlarmName">>, optional_string}, + {alarm_arn, <<"AlarmARN">>, optional_string} + ], J). + +-spec extract_step_adjustments(J :: proplist()) -> response(). +extract_step_adjustments(J) -> + erlcloud_json:decode([ + {metric_interval_lower_bound, <<"MetricIntervalLowerBound">>, optional_integer}, + {metric_interval_upper_bound, <<"MetricIntervalupperBound">>, optional_integer}, + {scaling_adjustment, <<"ScalingAdjustment">>, optional_integer} + ], J). + +-spec extract_step_scaling_policy(J :: proplist()) -> response(). +extract_step_scaling_policy(J) -> + Scaling = erlcloud_json:decode([ + {adjustment_type, <<"AdjustmentType">>, optional_string}, + {cooldown, <<"Cooldown">>, optional_integer}, + {metric_aggregation_type, <<"MetricAggregationType">>, optional_string}, + {min_adjustment_magnitude, <<"MinAdjustmentMagnitude">>, optional_integer} + ], J), + case proplists:get_value(<<"StepAdjustments">>, J, undefined) of + undefined -> + Scaling; + Steps -> + [{step_adjustments, [ extract_step_adjustments(Step) || Step <- Steps]} | Scaling] + end. + +-spec extract_dimensions(J :: proplist()) -> response(). +extract_dimensions(J) -> + erlcloud_json:decode([ + {name, <<"Name">>, optional_string}, + {value, <<"Value">>, optional_string} + ], J). + +-spec extract_predefined_metric_specifications(J :: proplist()) -> response(). +extract_predefined_metric_specifications(J) -> + erlcloud_json:decode([ + {predefined_metric_type, <<"PredefinedMetricType">>, optional_string}, + {resource_label, <<"ResourceLabel">>, optional_string} + ], J). + +-spec extract_customized_metric_specification(J :: proplist()) -> response(). +extract_customized_metric_specification(J) -> + CustomizedMetricSpecification = erlcloud_json:decode([ + {metric_name, <<"MetricName">>, optional_string}, + {namespace, <<"Namespace">>, optional_string}, + {statistic, <<"Statistic">>, optional_string}, + {unit, <<"Unit">>, optional_string} + ], J), + MaybeHasDimension = case proplists:get_value(<<"Dimensions">>, J, undefined) of + undefined -> + []; + Dimensions -> + [{dimension, [extract_dimensions(Dim) || Dim <- Dimensions ]}] + end, + CustomizedMetricSpecification ++ MaybeHasDimension. + +-spec extract_target_tracking_policy(J :: proplist()) -> response(). +extract_target_tracking_policy(J) -> + Target = erlcloud_json:decode([ + {disable_scale_in, <<"DisableScaleIn">>, optional_boolean}, + {scale_in_cooldown, <<"ScaleInCooldown">>, optional_integer}, + {scale_out_cooldown, <<"ScaleOutCooldown">>, optional_integer}, + {target_value, <<"TargetValue">>, optional_integer} + ], J), + MaybeHasCustomizedMetrics = case proplists:get_value(<<"CustomizedMetricsSpecification">>, J, undefined) of + undefined -> + []; + CustomMetrics -> + [{customized_metrics_specification, extract_customized_metric_specification(CustomMetrics)}] + end, + MaybeHasPredefinedMetrics = case proplists:get_value(<<"PredefinedMetricSpecification">>, J, undefined) of + undefined -> + []; + PredefinedMetrics -> + [{predefined_metric_specification, extract_predefined_metric_specifications(PredefinedMetrics)}] + end, + Target ++ MaybeHasCustomizedMetrics ++ MaybeHasPredefinedMetrics. + +-spec extract_scaling_policies(J :: proplist()) -> response(). +extract_scaling_policies(J) -> + Policy = erlcloud_json:decode([ + {creation_time, <<"CreationTime">>, optional_integer}, + {policy_arn, <<"PolicyARN">>, optional_string}, + {policy_name, <<"PolicyName">>, optional_string}, + {policy_type, <<"PolicyType">>, optional_string}, + {resource_id, <<"ResourceId">>, optional_string}, + {scalable_dimension, <<"ScalableDimension">>, optional_string}, + {service_namespace, <<"ServiceNamespace">>, optional_string} + ], J), + MaybeHasAlarms = case proplists:get_value(<<"Alarms">>, J, undefined) of + undefined -> + []; + AlarmJSON -> + [{alarms, [extract_alarm(Alarm) || Alarm <- AlarmJSON]}] + end, + MaybeHasStepScaling = case proplists:get_value(<<"StepScalingPolicyConfiguration">>, J, undefined) of + undefined -> + []; + StepScaling -> + [{step_scaling_policy_configuration, extract_step_scaling_policy(StepScaling)}] + end, + MaybeHasTargetTracking = case proplists:get_value(<<"TargetTrackingScalingPolicyConfiguration">>, J, undefined) of + undefined -> + []; + TargetTracking -> + [{target_tracking_scaling_policy_configuration, extract_target_tracking_policy(TargetTracking)}] + end, + Policy ++ MaybeHasAlarms ++ MaybeHasStepScaling ++ MaybeHasTargetTracking. + + +-spec extract_scalable_targets(J :: proplist()) -> response(). +extract_scalable_targets(J) -> + erlcloud_json:decode([ + {creation_time, <<"CreationTime">>, optional_integer}, + {max_capacity, <<"MaxCapacity">>, optional_integer}, + {min_capacity, <<"MinCapacity">>, optional_integer}, + {resource_id, <<"ResourceId">>, optional_string}, + {role_arn, <<"RoleARN">>, optional_string}, + {scalable_dimension, <<"ScalableDimension">>, optional_string}, + {service_namespace, <<"ServiceNamespace">>, optional_string} + ], J). + +-spec extract_scaling_activities(J :: proplist()) -> response(). +extract_scaling_activities(J) -> + erlcloud_json:decode([ + {activity_id, <<"ActivityId">>, optional_string}, + {cause, <<"Cause">>, optional_string}, + {description, <<"Description">>, optional_string}, + {resource_id, <<"ResourceId">>, optional_string}, + {scalable_dimension, <<"ScalableDimension">>, optional_string}, + {service_namespace, <<"ServiceNamespace">>, optional_string}, + {start_time, <<"StartTime">>, optional_integer}, + {status_code, <<"StatusCode">>, optional_string}, + {status_message, <<"StatusMessage">>, optional_string} + ], J). + +-spec extract_scheduled_action(J :: proplist()) -> response(). +extract_scheduled_action(J) -> + Res = erlcloud_json:decode([ + {creation_time, <<"CreationTime">>, optional_integer}, + {resource_id, <<"ResourceId">>, optional_string}, + {scalable_dimension, <<"ScalableDimension">>, optional_string}, + {service_namespace, <<"ServiceNamespace">>, optional_string}, + {scheduled_action_arn, <<"ScheduledActionARN">>, optional_string}, + {scheduled_action_name, <<"ScheduledActionName">>, optional_string}, + {start_time, <<"StartTime">>, optional_integer}, + {end_time, <<"EndTime">>, optional_integer}, + {schedule, <<"Schedule">>, optional_string} + ], J), + Action = proplists:get_value(<<"ScalableTargetAction">>, J), + PropAction = erlcloud_json:decode([ + {max_capacity, <<"MaxCapacity">>, optional_integer}, + {min_capacity, <<"MinCapacity">>, optional_integer} + ], Action), + [{scalable_target_action, PropAction} | Res]. + + +%%%------------------------------------------------------------------------------ +%%% Library initialization. +%%%------------------------------------------------------------------------------ + +-spec new(string(), string()) -> aws_config(). +new(AccessKeyID, SecretAccessKey) -> + #aws_config{access_key_id=AccessKeyID, + secret_access_key=SecretAccessKey}. + +-spec new(string(), string(), string()) -> aws_config(). +new(AccessKeyID, SecretAccessKey, Host) -> + #aws_config{access_key_id=AccessKeyID, + secret_access_key=SecretAccessKey, + application_autoscaling_host=Host}. + +-spec new(string(), string(), string(), non_neg_integer()) -> aws_config(). +new(AccessKeyID, SecretAccessKey, Host, Port) -> + #aws_config{access_key_id=AccessKeyID, + secret_access_key=SecretAccessKey, + application_autoscaling_host=Host, + application_autoscaling_port=Port}. + +-spec new(string(), string(), string(), non_neg_integer(), string()) -> aws_config(). +new(AccessKeyID, SecretAccessKey, Host, Port, Scheme) -> + #aws_config{access_key_id=AccessKeyID, + secret_access_key=SecretAccessKey, + application_autoscaling_host=Host, + application_autoscaling_port=Port, + application_autoscaling_scheme=Scheme}. + +-spec configure(string(), string()) -> ok. +configure(AccessKeyID, SecretAccessKey) -> + erlcloud_config:configure(AccessKeyID, SecretAccessKey, fun new/2). + +-spec configure(string(), string(), string()) -> ok. +configure(AccessKeyID, SecretAccessKey, Host) -> + erlcloud_config:configure(AccessKeyID, SecretAccessKey, Host, fun new/3). + +-spec configure(string(), string(), string(), non_neg_integer()) -> ok. +configure(AccessKeyID, SecretAccessKey, Host, Port) -> + erlcloud_config:configure(AccessKeyID, SecretAccessKey, Host, Port, fun new/4). + +-spec configure(string(), string(), string(), non_neg_integer(), string()) -> ok. +configure(AccessKeyID, SecretAccessKey, Host, Port, Scheme) -> + erlcloud_config:configure(AccessKeyID, SecretAccessKey, Host, Port, Scheme, fun new/5). + +default_config() -> erlcloud_aws:default_config(). + +%%%------------------------------------------------------------------------------ +%%% AWS Application Autoscaling functions. +%%% +%%% API Documentation on AWS: https://docs.aws.amazon.com/autoscaling/application/APIReference/Welcome.html +%%%------------------------------------------------------------------------------ + +%%------------------------------------------------------------------------------ +%% @doc. +%% DeleteScalingPolicy. +%% +%% https://docs.aws.amazon.com/autoscaling/application/APIReference/API_DeleteScalingPolicy.html +%%------------------------------------------------------------------------------ + +-spec delete_scaling_policy(Configuration :: aws_config(), + PolicyName :: binary(), + ResourceId :: binary(), + ScalableDimension :: binary(), + ServiceNamespace :: binary() + ) -> term(). +delete_scaling_policy(Configuration, PolicyName, ResourceId, ScalableDimension, ServiceNamespace) -> + BodyProps = [{<<"PolicyName">>, PolicyName}, + {<<"ResourceId">>, ResourceId}, + {<<"ScalableDimension">>, ScalableDimension}, + {<<"ServiceNamespace">>, ServiceNamespace}], + delete_scaling_policy(Configuration, BodyProps). + +-spec delete_scaling_policy(Configuration :: aws_config(), + BodyConfiguration :: aws_aas_request_body() + ) -> term(). +delete_scaling_policy(Configuration, BodyConfiguration) -> + request_with_action(Configuration, BodyConfiguration, "AnyScaleFrontendService.DescribeScalingPolicies"). + +%%------------------------------------------------------------------------------ +%% @doc. +%% DeleteScheduledAction. +%% +%% https://docs.aws.amazon.com/autoscaling/application/APIReference/API_DeleteScheduledAction.html +%%------------------------------------------------------------------------------ + +-spec delete_scheduled_action( + Configuration :: aws_config(), + ResourceId :: binary(), + ScalableDimension :: binary(), + ScheduledActionName :: binary(), + ServiceNamespace :: binary() + ) -> ok_error_response(). +delete_scheduled_action(Configuration, ResourceId, ScalableDimension, ScheduledActionName, ServiceNamespace) -> + BodyProps = [{<<"ScheduledActionName">>, ScheduledActionName}, + {<<"ResourceId">>, ResourceId}, + {<<"ScalableDimension">>, ScalableDimension}, + {<<"ServiceNamespace">>, ServiceNamespace}], + delete_scheduled_action(Configuration, BodyProps). + + +-spec delete_scheduled_action(Configuration :: aws_config(), + BodyConfiguration :: aws_aas_request_body() + ) -> ok_error_response(). +delete_scheduled_action(Configuration, BodyConfiguration) -> + request_with_action(Configuration, BodyConfiguration, "AnyScaleFrontendService.DeleteScheduledAction"). + +%%------------------------------------------------------------------------------ +%% @doc. +%% DeregisterScalableTarget. +%% +%% https://docs.aws.amazon.com/autoscaling/application/APIReference/API_DeregisterScalableTarget.html +%%------------------------------------------------------------------------------ + +-spec deregister_scalable_target( + Configuration :: aws_config(), + ResourceId :: binary(), + ScalableDimension :: binary(), + ServiceNamespace :: binary() + ) -> ok_error_response(). +deregister_scalable_target(Configuration, ResourceId, ScalableDimension, ServiceNamespace) -> + BodyProps = [{<<"ResourceId">>, ResourceId}, + {<<"ScalableDimension">>, ScalableDimension}, + {<<"ServiceNamespace">>, ServiceNamespace}], + deregister_scalable_target(Configuration, BodyProps). + +-spec deregister_scalable_target( + Configuration :: aws_config(), + BodyConfiguration :: aws_aas_request_body() + ) -> ok_error_response(). +deregister_scalable_target(Configuration, BodyConfiguration) -> + request_with_action(Configuration, BodyConfiguration, "AnyScaleFrontendService.DeregisterScalableTarget"). + +%%------------------------------------------------------------------------------ +%% @doc. +%% DescribeScalableTargets. +%% +%% https://docs.aws.amazon.com/autoscaling/application/APIReference/API_DescribeScalableTargets.html +%%------------------------------------------------------------------------------ + +-spec describe_scalable_targets( + aws_config(), + aws_aas_request_body() | binary() + ) -> ok_error_response(). +describe_scalable_targets(Configuration, ServiceNamespace) when is_binary(ServiceNamespace)-> + describe_scalable_targets(Configuration, [{<<"ServiceNamespace">>, ServiceNamespace}]); +describe_scalable_targets(Configuration, BodyConfiguration) -> + case request_with_action(Configuration, BodyConfiguration, "AnyScaleFrontendService.DescribeScalableTargets") of + {ok, Result} -> + ScalableTargets = proplists:get_value(<<"ScalableTargets">>, Result), + PropRes = [extract_scalable_targets(E) || E <- ScalableTargets], + NextToken = proplists:get_value(<<"NextToken">>, Result, undefined), + MaybeHasNext = case NextToken of + undefined -> []; + Token -> [{next_token, Token}] + end, + {ok, PropRes ++ MaybeHasNext}; + {error, Error} -> + {error, Error} + end. + +%%------------------------------------------------------------------------------ +%% @doc. +%% DescribeScalingActivities. +%% +%% https://docs.aws.amazon.com/autoscaling/application/APIReference/API_DescribeScalingActivities.html +%%------------------------------------------------------------------------------ + +-spec describe_scaling_activities( + aws_config(), + aws_aas_request_body() | binary() + ) -> ok_error_response(). +describe_scaling_activities(Configuration, ServiceNamespace) when is_binary(ServiceNamespace) -> + describe_scaling_activities(Configuration, [{<<"ServiceNamespace">>, ServiceNamespace}]); +describe_scaling_activities(Configuration, BodyConfiguration) -> + case request_with_action(Configuration, BodyConfiguration, "AnyScaleFrontendService.DescribeScalingActivities") of + {ok, Result} -> + {ok, Result} = request_with_action(Configuration, BodyConfiguration, "AnyScaleFrontendService.DescribeScalingActivities"), + ScalingActivities = proplists:get_value(<<"ScalingActivities">>, Result), + PropRes = [extract_scaling_activities(E) || E <- ScalingActivities], + NextToken = proplists:get_value(<<"NextToken">>, Result, undefined), + MaybeHasNext = case NextToken of + undefined -> []; + Token -> [{next_token, Token}] + end, + {ok, PropRes ++ MaybeHasNext}; + {error, Error} -> + {error, Error} + end. + +%%------------------------------------------------------------------------------ +%% @doc. +%% DescribeScalingPolicies. +%% +%% https://docs.aws.amazon.com/autoscaling/application/APIReference/API_DescribeScalingPolicies.html +%%------------------------------------------------------------------------------ + +-spec describe_scaling_policies( + aws_config(), + aws_aas_request_body() | binary() + ) -> ok_error_response(). +describe_scaling_policies(Configuration, ServiceNamespace) when is_binary(ServiceNamespace) -> + describe_scaling_policies(Configuration, [{<<"ServiceNamespace">>, ServiceNamespace}]); +describe_scaling_policies(Configuration, BodyConfiguration) -> + case request_with_action(Configuration, BodyConfiguration, "AnyScaleFrontendService.DescribeScalingPolicies") of + {ok, Result} -> + ScalingPolicies = proplists:get_value(<<"ScalingPolicies">>, Result), + PropRes = [extract_scaling_policies(Extracted) || Extracted <- ScalingPolicies], + NextToken = proplists:get_value(<<"NextToken">>, Result, undefined), + MaybeHasNext = case NextToken of + undefined -> []; + Token -> [{next_token, Token}] + end, + {ok, PropRes ++ MaybeHasNext}; + {error, Error} -> + {error, Error} + end. + + +%%------------------------------------------------------------------------------ +%% @doc. +%% DescribeScheduledActions. +%% +%% https://docs.aws.amazon.com/autoscaling/application/APIReference/API_DescribeScheduledActions.html +%%------------------------------------------------------------------------------ + +-spec describe_scheduled_actions( + aws_config(), + aws_aas_request_body() | binary() + ) -> ok_error_response(). +describe_scheduled_actions(Configuration, ServiceNamespace) when is_binary(ServiceNamespace) -> + describe_scheduled_actions(Configuration, [{<<"ServiceNamespace">>, ServiceNamespace}]); +describe_scheduled_actions(Configuration, BodyConfiguration) -> + case request_with_action(Configuration, BodyConfiguration, "AnyScaleFrontendService.DescribeScheduledActions") of + {ok, Result} -> + ScheduledActions = proplists:get_value(<<"ScheduledActions">>, Result), + PropRes = [extract_scheduled_action(Extracted) || Extracted <- ScheduledActions], + NextToken = proplists:get_value(<<"NextToken">>, Result, undefined), + MaybeHasNext = case NextToken of + undefined -> []; + Token -> [{next_token, Token}] + end, + {ok, PropRes ++ MaybeHasNext}; + {error, Error} -> + {error, Error} + + end. + +%%------------------------------------------------------------------------------ +%% @doc. +%% PutScalingPolicy. +%% +%% https://docs.aws.amazon.com/autoscaling/application/APIReference/API_PutScalingPolicy.html +%%------------------------------------------------------------------------------ + +-spec put_scaling_policy( + Configuration :: aws_config(), + PolicyName :: binary(), + ResourceId :: binary(), + ScalableDimension :: binary(), + ServiceNamespace :: binary() + ) -> ok_error_response(). +put_scaling_policy(Configuration, PolicyName, ResourceId, ScalableDimension, ServiceNamespace) -> + BodyProps = [{<<"PolicyName">>, PolicyName}, + {<<"ResourceId">>, ResourceId}, + {<<"ScalableDimension">>, ScalableDimension}, + {<<"ServiceNamespace">>, ServiceNamespace}], + put_scaling_policy(Configuration, BodyProps). + +-spec put_scaling_policy( + Configuration :: aws_config(), + PolicyName :: binary(), + ResourceId :: binary(), + ScalableDimension :: binary(), + ServiceNamespace :: binary(), + PolicyType :: binary(), + Policy :: [proplists:proplist()] + ) -> ok_error_response(). +put_scaling_policy(Configuration, PolicyName, ResourceId, ScalableDimension, ServiceNamespace, PolicyType, Policy) -> + BodyProps = [{<<"PolicyName">>, PolicyName}, + {<<"ResourceId">>, ResourceId}, + {<<"ScalableDimension">>, ScalableDimension}, + {<<"ServiceNamespace">>, ServiceNamespace}, + {<<"PolicyType">>, PolicyType}], + case PolicyType of + <<"StepScaling">> -> + put_scaling_policy(Configuration, [{<<"StepScalingPolicyConfiguration">>, Policy} | BodyProps]); + <<"TargetTrackingScaling">> -> + put_scaling_policy(Configuration, [{<<"TargetTrackingScalingPolicyConfiguration">>, Policy} | BodyProps]) + end. + +-spec put_scaling_policy( + Configuration :: aws_config(), + BodyConfiguration :: aws_aas_request_body() + ) -> ok_error_response(). +put_scaling_policy(Configuration, BodyConfiguration) -> + case request_with_action(Configuration, BodyConfiguration, "AnyScaleFrontendService.PutScalingPolicy") of + {ok, Result} -> + Alarms = proplists:get_value(<<"Alarms">>, Result), + PropAlarms = [extract_alarm(E) || E <- Alarms], + {ok, [{policy_arn, proplists:get_value(<<"PolicyARN">>, Result)} | PropAlarms]}; + {error, Error} -> + {error, Error} + end. + +%%------------------------------------------------------------------------------ +%% @doc. +%% PutScheduledAction. +%% +%% https://docs.aws.amazon.com/autoscaling/application/APIReference/API_PutScheduledAction.html +%%------------------------------------------------------------------------------ + +-spec put_scheduled_action( + Configuration :: aws_config(), + ResourceId :: binary(), + ScalableDimension :: binary(), + ServiceNamespace :: binary(), + ScheduledActionName :: binary() + ) -> ok_error_response(). +put_scheduled_action(Configuration, ResourceId, ScalableDimension, ServiceNamespace, ScheduledActionName) -> + BodyProps = [{<<"ScheduledActionName">>, ScheduledActionName}, + {<<"ResourceId">>, ResourceId}, + {<<"ScalableDimension">>, ScalableDimension}, + {<<"ServiceNamespace">>, ServiceNamespace}], + put_scheduled_action(Configuration, BodyProps). + +-spec put_scheduled_action( + Configuration :: aws_config(), + ResourceId :: binary(), + ScalableDimension :: binary(), + ServiceNamespace :: binary(), + ScheduledActionName :: binary(), + Schedule :: binary() + ) -> ok_error_response(). +put_scheduled_action(Configuration, ResourceId, ScalableDimension, ServiceNamespace, ScheduledActionName, Schedule) -> + BodyProps = [{<<"ScheduledActionName">>, ScheduledActionName}, + {<<"ResourceId">>, ResourceId}, + {<<"ScalableDimension">>, ScalableDimension}, + {<<"ServiceNamespace">>, ServiceNamespace}, + {<<"Schedule">>, Schedule}], + put_scheduled_action(Configuration, BodyProps). + +-spec put_scheduled_action( + Configuration :: aws_config(), + ResourceId :: binary(), + ScalableDimension :: binary(), + ServiceNamespace :: binary(), + ScheduledActionName :: binary(), + Schedule :: binary(), + StartTime :: pos_integer() + ) -> ok_error_response(). +put_scheduled_action(Configuration, ResourceId, ScalableDimension, ServiceNamespace, ScheduledActionName, Schedule, StartTime) -> + BodyProps = [{<<"ScheduledActionName">>, ScheduledActionName}, + {<<"ResourceId">>, ResourceId}, + {<<"ScalableDimension">>, ScalableDimension}, + {<<"ServiceNamespace">>, ServiceNamespace}, + {<<"Schedule">>, Schedule}, + {<<"StartTime">>, StartTime}], + put_scheduled_action(Configuration, BodyProps). + +-spec put_scheduled_action( + Configuration :: aws_config(), + ResourceId :: binary(), + ScalableDimension :: binary(), + ServiceNamespace :: binary(), + ScheduledActionName :: binary(), + Schedule :: binary(), + StartTime :: pos_integer(), + EndTime :: pos_integer() + ) -> ok_error_response(). +put_scheduled_action(Configuration, ResourceId, ScalableDimension, ServiceNamespace, ScheduledActionName, Schedule, StartTime, EndTime) -> + BodyProps = [{<<"ScheduledActionName">>, ScheduledActionName}, + {<<"ResourceId">>, ResourceId}, + {<<"ScalableDimension">>, ScalableDimension}, + {<<"ServiceNamespace">>, ServiceNamespace}, + {<<"Schedule">>, Schedule}, + {<<"StartTime">>, StartTime}, + {<<"EndTime">>, EndTime}], + put_scheduled_action(Configuration, BodyProps). + +-spec put_scheduled_action(Configuration :: aws_config(), + BodyConfiguration :: aws_aas_request_body() + ) -> ok_error_response(). +put_scheduled_action(Configuration, BodyConfiguration) -> + request_with_action(Configuration, BodyConfiguration, "AnyScaleFrontendService.PutScheduledAction"). + +%%------------------------------------------------------------------------------ +%% @doc. +%% RegisterScalableTarget. +%% +%% https://docs.aws.amazon.com/autoscaling/application/APIReference/API_RegisterScalableTarget.html +%%------------------------------------------------------------------------------ + +-spec register_scalable_target(Configuration :: aws_config(), + ResourceId :: binary(), + ScalableDimension :: binary(), + ServiceNamespace :: binary()) -> ok_error_response(). +register_scalable_target(Configuration, ResourceId, ScalableDimension, ServiceNamespace) -> + BodyProps = [{<<"ResourceId">>, ResourceId}, + {<<"ScalableDimension">>, ScalableDimension}, + {<<"ServiceNamespace">>, ServiceNamespace}], + register_scalable_target(Configuration, BodyProps). + +-spec register_scalable_target(Configuration :: aws_config(), + ResourceId :: binary(), + ScalableDimension :: binary(), + ServiceNamespace :: binary(), + ResourceARN :: binary()) -> ok_error_response(). +register_scalable_target(Configuration, ResourceId, ScalableDimension, ServiceNamespace, ResourceARN) -> + BodyProps = [{<<"ResourceId">>, ResourceId}, + {<<"ScalableDimension">>, ScalableDimension}, + {<<"ServiceNamespace">>, ServiceNamespace}, + {<<"ResourceARN">>, ResourceARN}], + register_scalable_target(Configuration, BodyProps). + +-spec register_scalable_target(Configuration :: aws_config(), + ResourceId :: binary(), + ScalableDimension :: binary(), + ServiceNamespace :: binary(), + ResourceARN :: binary(), + MinCapacity :: integer() | undefined, + MaxCapacity :: integer() | undefined) -> ok_error_response(). +register_scalable_target(Configuration, ResourceId, ScalableDimension, ServiceNamespace, ResourceARN, MinCapacity, MaxCapacity) -> + BodyProps = [{<<"ResourceId">>, ResourceId}, + {<<"ScalableDimension">>, ScalableDimension}, + {<<"ServiceNamespace">>, ServiceNamespace}, + {<<"ResourceARN">>, ResourceARN}], + MaybeBodyWithMax = case MaxCapacity of + undefined -> []; + Max -> [{<<"MaxCapacity">>, Max}] + end, + MaybeBodyWithMin = case MinCapacity of + undefined -> []; + Min -> [{<<"MinCapacity">>, Min}] + end, + register_scalable_target(Configuration, BodyProps ++ MaybeBodyWithMax ++ MaybeBodyWithMin). + +-spec register_scalable_target( + Configuration :: aws_config(), + BodyConfiguration :: aws_aas_request_body() +) -> ok_error_response(). +register_scalable_target(Configuration, BodyConfiguration) -> + request_with_action(Configuration, BodyConfiguration, "AnyScaleFrontendService.RegisterScalableTarget"). + + +%%%------------------------------------------------------------------------------ +%%% Internal functions. +%%%------------------------------------------------------------------------------ + +aas_result_fun(#aws_request{response_type = ok} = Request) -> + Request; +aas_result_fun(#aws_request{response_type = error, + error_type = aws, + response_status = 400, + response_body = Body} = Request) -> + %% Retry on ThrottlingException, ConcurrentUpdateException + try jsx:decode(Body, [{return_maps, false}]) of + Json -> + case proplists:get_value(<<"__type">>, Json) of + <<"ThrottlingException">> -> + Request#aws_request{should_retry = true}; + <<"ConcurrentUpdateException">> -> + Request#aws_request{should_retry = true}; + _Other -> + Request#aws_request{should_retry = false} + end + catch + error:badarg -> + Request#aws_request{should_retry = false} + end; +aas_result_fun(#aws_request{response_type = error, + error_type = aws, + response_status = Status} = Request) when + Status >= 500 -> + %% Retry on InternalFailure (500) or ServiceUnavailable (503). + Request#aws_request{should_retry = true}; +aas_result_fun(#aws_request{response_type = error, error_type = aws} = Request) -> + Request#aws_request{should_retry = false}; +aas_result_fun(Request) -> + Request#aws_request{should_retry = false}. + + +request_with_action(Configuration, BodyConfiguration, Action) -> + Body = jsx:encode(BodyConfiguration), + case erlcloud_aws:update_config(Configuration) of + {ok, Config} -> + HeadersPrev = headers(Config, Action, Body), + Headers = [{"content-type", "application/x-amz-json-1.1"} | HeadersPrev], + Request = prepare_record(Config, post, Headers, Body), + Response = erlcloud_retry:request(Config, Request, fun aas_result_fun/1), + case Response#aws_request.response_type of + ok -> + {ok, jsx:decode(Response#aws_request.response_body, [{return_maps, false}])}; + _ -> + decode_error(Response) + end; + {error, Reason} -> + {error, Reason} + end. + +prepare_record(Config, Method, Headers, Body) -> + %%% URI: awsConfig.Scheme + awsConfig.Host + [awsConfig.Port] + %%% URI: https://autoscaling.us-west-2.amazonaws.com/ + + RequestURI = lists:flatten([ + Config#aws_config.application_autoscaling_scheme, + Config#aws_config.application_autoscaling_host, + port_spec(Config) + ]), + + #aws_request{service = application_autoscaling, + method = Method, + request_headers = Headers, + request_body = Body, + uri = RequestURI}. + +port_spec(#aws_config{application_autoscaling_port=80}) -> + ""; +port_spec(#aws_config{application_autoscaling_port=Port}) -> + [":", erlang:integer_to_list(Port)]. + +headers(Config, Operation, Body) -> + Headers = [{"host", Config#aws_config.application_autoscaling_host}, + {"x-amz-target", Operation}], + erlcloud_aws:sign_v4_headers(Config, Headers, Body, erlcloud_aws:aws_region_from_host(Config#aws_config.application_autoscaling_host), "application-autoscaling"). + +%% Extracts and decodes the error from the response returning +%% in the format of `{error, {ErrorType, Message}}' (matching to how errors are returned in `ercloud_ddb2' module) +%% Example: {error, {<<"ConcurrentUpdateException">>, <<"You already have a pending update to an Auto Scaling resource.">>}} +decode_error(#aws_request{response_body = Body} = Response) -> + case jsx:is_json(Body) of + false -> + erlcloud_aws:request_to_return(Response); + true -> + DecodedError = jsx:decode(Body, [{return_maps, false}]), + ErrorType = proplists:get_value(<<"__type">>, DecodedError, <<>>), + ErrorMessage = proplists:get_value(<<"Message">>, DecodedError, <<>>), + {error, {ErrorType, ErrorMessage}} + end. diff --git a/src/erlcloud_as.erl b/src/erlcloud_as.erl index 05734e436..846e0aa1d 100644 --- a/src/erlcloud_as.erl +++ b/src/erlcloud_as.erl @@ -28,7 +28,8 @@ detach_instances/2, detach_instances/3, detach_instances/4, describe_lifecycle_hooks/1, describe_lifecycle_hooks/2, describe_lifecycle_hooks/3, - complete_lifecycle_action/4, complete_lifecycle_action/5 + complete_lifecycle_action/4, complete_lifecycle_action/5, + record_lifecycle_action_heartbeat/3, record_lifecycle_action_heartbeat/4 ]). -define(API_VERSION, "2011-01-01"). @@ -78,6 +79,9 @@ -define(COMPLETE_LIFECYCLE_ACTION_ACTIVITY, "/CompleteLifecycleActionResponse/ResponseMetadata/RequestId"). +-define(RECORD_LIFECYCLE_ACTION_HEARTBEAT_ACTIVITY, + "/RecordLifecycleActionHeartbeatResponse/ResponseMetadata/RequestId"). + %% -------------------------------------------------------------------- %% @doc Calls describe_groups([], default_configuration()) @@ -166,6 +170,7 @@ extract_group(G) -> min_size = erlcloud_xml:get_integer("MinSize", G), max_size = erlcloud_xml:get_integer("MaxSize", G), launch_configuration_name = get_text("LaunchConfigurationName", G), + launch_template = extract_launch_template_spec(xmerl_xpath:string("LaunchTemplate", G)), vpc_zone_id = [ erlcloud_xml:get_text(Zid) || Zid <- xmerl_xpath:string("VPCZoneIdentifier", G)], status = get_text("Status", G) }. @@ -173,6 +178,13 @@ extract_tags_from_group(G) -> [{erlcloud_xml:get_text("Key", T), erlcloud_xml:get_text("Value", T)} || T <- xmerl_xpath:string("Tags/member", G)]. +extract_launch_template_spec([]) -> undefined; +extract_launch_template_spec([L]) -> + #aws_launch_template_spec{ + id = get_text("LaunchTemplateId", L), + name = get_text("LaunchTemplateName", L), + version = get_text("Version", L) + }. %% -------------------------------------------------------------------- %% @doc set_desired_capacity(GroupName, Capacity, false, default_config()) %% @end @@ -327,18 +339,16 @@ create_launch_config(#aws_launch_config{ }, Config) -> Params = - lists:concat([ [ {"LaunchConfigurationName", LCName}, {"ImageId", ImageId}, {"InstanceType", Type} - ], - when_defined(UserData, [{"UserData", UserData}], []), - when_defined(PublicIP, [{"AssociatePublicIpAddress", atom_to_list(PublicIP)}], []), - when_defined(Monitoring, [{"InstanceMonitoring.Enabled", atom_to_list(Monitoring)}], []), - member_params("SecurityGroups.member.", SGroups), - when_defined(KeyPair, [{"KeyName", KeyPair}], []) - ]), + ] + ++ when_defined(UserData, [{"UserData", UserData}], []) + ++ when_defined(PublicIP, [{"AssociatePublicIpAddress", atom_to_list(PublicIP)}], []) + ++ when_defined(Monitoring, [{"InstanceMonitoring.Enabled", atom_to_list(Monitoring)}], []) + ++ member_params("SecurityGroups.member.", SGroups) + ++ when_defined(KeyPair, [{"KeyName", KeyPair}], []), io:format("~p ~n", [Params]), create_launch_config(Params, Config); @@ -369,23 +379,21 @@ create_auto_scaling_group(#aws_autoscaling_group{ }, Config) -> ProcessedTags = lists:flatten([tag_to_member_param(T, Idx) || {T, Idx} <- lists:zip(Tags, lists:seq(1, length(Tags)))]), - Params = lists:concat([ - [ + Params = [ {"AutoScalingGroupName", GName}, {"LaunchConfigurationName", LaunchName}, {"MaxSize", integer_to_list(MaxSize)}, {"MinSize", integer_to_list(MinSize)} - ], - case VpcZoneIds of + ] + ++ case VpcZoneIds of undefined -> []; _ ->[{"VPCZoneIdentifier", string:join(VpcZoneIds, ",")}] - end, - case AZones of + end + ++ case AZones of undefined -> []; _ -> member_params("AvailabilityZones.member.", AZones) - end, - ProcessedTags - ]), + end + ++ ProcessedTags, create_auto_scaling_group(Params, Config); create_auto_scaling_group(Params, Config) -> @@ -414,35 +422,33 @@ update_auto_scaling_group(#aws_autoscaling_group{ availability_zones = AZones }, Config) -> - Params = lists:concat([ - [ + Params = [ {"AutoScalingGroupName", GName} - ], - case LaunchName of + ] + ++ case LaunchName of undefined -> []; _ -> [{"LaunchConfigurationName", LaunchName}] - end, - case DesiredCapacity of + end + ++ case DesiredCapacity of undefined -> []; _ -> [{"DesiredCapacity", integer_to_list(DesiredCapacity)}] - end, - case MaxSize of + end + ++ case MaxSize of undefined -> []; _ -> [{"MaxSize", integer_to_list(MaxSize)}] - end, - case MinSize of + end + ++ case MinSize of undefined -> []; _ -> [{"MinSize", integer_to_list(MinSize)}] - end, - case VpcZoneIds of + end + ++ case VpcZoneIds of undefined -> []; _ ->[{"VPCZoneIdentifier", string:join(VpcZoneIds, ",")}] - end, - case AZones of + end + ++ case AZones of undefined -> []; _ -> member_params("AvailabilityZones.member.", AZones) - end - ]), + end, update_auto_scaling_group(Params, Config); update_auto_scaling_group(Params, Config) -> @@ -695,6 +701,29 @@ complete_lifecycle_action(GroupName, LifecycleActionResult, LifecycleHookName, I {error, Reason} end. +-spec record_lifecycle_action_heartbeat(string(), string(), {instance_id | token, string()}) -> {ok, string()} | {error, term()}. +record_lifecycle_action_heartbeat(GroupName, LifecycleHookName, InstanceIdOrLifecycleActionToken) -> + record_lifecycle_action_heartbeat(GroupName, LifecycleHookName, InstanceIdOrLifecycleActionToken, erlcloud_aws:default_config()). + +-spec record_lifecycle_action_heartbeat(string(), string(), {instance_id | token, string()}, aws_config()) -> {ok, string()} | {error, term()}. +record_lifecycle_action_heartbeat(GroupName, LifecycleHookName, InstanceIdOrLifecycleActionToken, Config) -> + InstanceIdOrLifecycleActionTokenParam = case InstanceIdOrLifecycleActionToken of + {instance_id,InstanceId} -> + {"InstanceId", InstanceId}; + {token,LifecycleActionToken} -> + {"LifecycleActionToken", LifecycleActionToken} + end, + Params = [{"AutoScalingGroupName", GroupName}, + {"LifecycleHookName", LifecycleHookName}, + InstanceIdOrLifecycleActionTokenParam], + case as_query(Config, "RecordLifecycleActionHeartbeat", Params, ?API_VERSION) of + {ok, Doc} -> + RequestId = erlcloud_xml:get_text(?RECORD_LIFECYCLE_ACTION_HEARTBEAT_ACTIVITY, Doc), + {ok, RequestId}; + {error, Reason} -> + {error, Reason} + end. + %% given a list of member identifiers, return a list of %% {key with prefix, member identifier} for use in autoscaling calls. %% Example pair that could be returned in a list is diff --git a/src/erlcloud_athena.erl b/src/erlcloud_athena.erl index c04e081be..9bf4ae4ae 100644 --- a/src/erlcloud_athena.erl +++ b/src/erlcloud_athena.erl @@ -10,18 +10,33 @@ batch_get_named_query/1, batch_get_named_query/2, + batch_get_prepared_statement/2, + batch_get_prepared_statement/3, + batch_get_query_execution/1, batch_get_query_execution/2, create_named_query/4, create_named_query/5, create_named_query/6, + create_named_query/7, + + create_prepared_statement/3, + create_prepared_statement/4, + create_prepared_statement/5, + delete_named_query/1, delete_named_query/2, + delete_prepared_statement/2, + delete_prepared_statement/3, + get_named_query/1, get_named_query/2, + get_prepared_statement/2, + get_prepared_statement/3, + get_query_execution/1, get_query_execution/2, @@ -32,18 +47,29 @@ list_named_queries/0, list_named_queries/1, list_named_queries/2, + list_named_queries/3, + + list_prepared_statements/1, + list_prepared_statements/2, + list_prepared_statements/3, list_query_executions/0, list_query_executions/1, list_query_executions/2, + list_query_executions/3, start_query_execution/4, start_query_execution/5, start_query_execution/6, start_query_execution/7, + start_query_execution/8, stop_query_execution/1, - stop_query_execution/2 + stop_query_execution/2, + + update_prepared_statement/3, + update_prepared_statement/4, + update_prepared_statement/5 ]). -spec new(string(), string()) -> aws_config(). @@ -111,6 +137,19 @@ batch_get_named_query(NamedQueryIds, Config) -> Request = #{<<"NamedQueryIds">> => NamedQueryIds}, request(Config, "BatchGetNamedQuery", Request). +%% @doc +%% Athena API: +%% https://docs.aws.amazon.com/athena/latest/APIReference/API_BatchGetPreparedStatement.html +%% +-spec batch_get_prepared_statement(binary(), list(binary())) -> {ok, map()} | {error, any()}. +batch_get_prepared_statement(WorkGroup, PreparedStatementNames) -> + batch_get_prepared_statement(WorkGroup, PreparedStatementNames, default_config()). + +-spec batch_get_prepared_statement(binary(), list(binary()), aws_config()) -> {ok, map()} | {error, any()}. +batch_get_prepared_statement(WorkGroup, PreparedStatementNames, Config) -> + Request = #{<<"WorkGroup">> => WorkGroup, <<"PreparedStatementNames">> => PreparedStatementNames}, + request(Config, "BatchGetPreparedStatement", Request). + %% @doc %% Athena API: %% http://docs.aws.amazon.com/athena/latest/APIReference/API_BatchGetQueryExecution.html @@ -135,45 +174,82 @@ batch_get_query_execution(QueryExecutionIds, Config) -> %% <<"db-name">>, %% <<"query-name">>, %% <<"select * from some-tbl">>, -%% <<"optional-query-description">> +%% <<"optional-query-description">>, +%% <<"optional-some-workgroup">> %% ). %% ' %% +-type create_named_query_opts() :: [create_named_query_opt()]. +-type create_named_query_opt() :: {workgroup, binary()}. + -spec create_named_query(binary(), binary(), binary(), binary()) -> {ok, binary()} | {error, any()}. create_named_query(ClientReqToken, Db, Name, Query) -> Config = default_config(), - create_named_query(ClientReqToken, Db, Name, Query, undefined, Config). + create_named_query(ClientReqToken, Db, Name, Query, undefined, [], Config). -spec create_named_query(binary(), binary(), binary(), binary(), aws_config() | binary()) -> {ok, binary()} | {error, any()}. create_named_query(ClientReqToken, Db, Name, Query, Config) when is_record(Config, aws_config) -> - create_named_query(ClientReqToken, Db, Name, Query, undefined, Config); + create_named_query(ClientReqToken, Db, Name, Query, undefined, [], Config); create_named_query(ClientReqToken, Db, Name, Query, Description) when is_binary(Description) -> Config = default_config(), - create_named_query(ClientReqToken, Db, Name, Query, Description, Config). + create_named_query(ClientReqToken, Db, Name, Query, Description, [], Config). + +-spec create_named_query(binary(), binary(), binary(), binary(), + binary(), aws_config() | create_named_query_opts()) -> + {ok, binary()} | {error, any()}. +create_named_query(ClientReqToken, Db, Name, Query, Description, Config) + when is_record(Config, aws_config) -> + create_named_query(ClientReqToken, Db, Name, Query, Description, [], Config); +create_named_query(ClientReqToken, Db, Name, Query, Description, Options) + when is_list(Options) -> + create_named_query(ClientReqToken, Db, Name, Query, Description, Options, default_config()). -spec create_named_query(binary(), binary(), binary(), binary(), binary() | undefined, + create_named_query_opts(), aws_config()) -> {ok, binary()} | {error, any()}. -create_named_query(ClientReqToken, Db, Name, Query, Description, Config) -> - Request0 = #{<<"ClientRequestToken">> => ClientReqToken, - <<"Database">> => Db, - <<"Name">> => Name, - <<"QueryString">> => Query}, - Request1 = update_query_description(Request0, Description), - case request(Config, "CreateNamedQuery", Request1) of +create_named_query(ClientReqToken, Db, Name, Query, Description, Options, Config) -> + Params = encode_params([{description, Description} | Options]), + Request = Params#{<<"ClientRequestToken">> => ClientReqToken, + <<"Database">> => Db, + <<"Name">> => Name, + <<"QueryString">> => Query}, + case request(Config, "CreateNamedQuery", Request) of {ok, Res} -> {ok, maps:get(<<"NamedQueryId">>, Res)}; Error -> Error end. -update_query_description(Request, undefined) -> Request; -update_query_description(Request, Description) -> - maps:put(<<"Description">>, Description, Request). +%% @doc +%% Athena API: +%% https://docs.aws.amazon.com/athena/latest/APIReference/API_CreatePreparedStatement.html +%% +-spec create_prepared_statement(binary(), binary(), binary()) -> ok | {error, any()}. +create_prepared_statement(WorkGroup, StatementName, QueryStatement) -> + create_prepared_statement(WorkGroup, StatementName, QueryStatement, undefined, default_config()). + +-spec create_prepared_statement(binary(), binary(), binary(), binary() | aws_config()) -> ok | {error, any()}. +create_prepared_statement(WorkGroup, StatementName, QueryStatement, Config) when is_record(Config, aws_config) -> + create_prepared_statement(WorkGroup, StatementName, QueryStatement, undefined, Config); +create_prepared_statement(WorkGroup, StatementName, QueryStatement, Description) when is_binary(Description) -> + create_prepared_statement(WorkGroup, StatementName, QueryStatement, Description, default_config()). + +-spec create_prepared_statement(binary(), binary(), binary(), binary() | undefined, aws_config()) -> ok | {error, any()}. +create_prepared_statement(WorkGroup, StatementName, QueryStatement, Description, Config) -> + Params = encode_params([{description, Description}]), + Request = Params#{<<"WorkGroup">> => WorkGroup, + <<"StatementName">> => StatementName, + <<"QueryStatement">> => QueryStatement}, + case request(Config, "CreatePreparedStatement", Request) of + {ok, _} -> ok; + Error -> Error + end. + %% @doc %% Athena API: @@ -190,6 +266,22 @@ delete_named_query(NamedQueryId, Config) -> {ok, _} -> ok; Error -> Error end. +%% @doc +%% Athena API: +%% https://docs.aws.amazon.com/athena/latest/APIReference/API_DeletePreparedStatement.html +%% + +-spec delete_prepared_statement(binary(), binary()) -> ok | {error | any}. +delete_prepared_statement(WorkGroup, StatementName) -> + delete_prepared_statement(WorkGroup, StatementName, default_config()). + +-spec delete_prepared_statement(binary(), binary(), aws_config()) -> ok | {error | any}. +delete_prepared_statement(WorkGroup, StatementName, Config) -> + Request = #{<<"WorkGroup">> => WorkGroup, <<"StatementName">> => StatementName}, + case request(Config, "DeletePreparedStatement", Request) of + {ok, _} -> ok; + Error -> Error + end. %% @doc %% Athena API: @@ -204,6 +296,19 @@ get_named_query(NamedQueryId, Config) -> Request = #{<<"NamedQueryId">> => NamedQueryId}, request(Config, "GetNamedQuery", Request). +%% @doc +%% Athena API: +%% https://docs.aws.amazon.com/athena/latest/APIReference/API_GetPreparedStatement.html +%% +-spec get_prepared_statement(binary(), binary()) -> {ok, map()} | {error, any()}. +get_prepared_statement(WorkGroup, StatementName) -> + get_prepared_statement(WorkGroup, StatementName, default_config()). + +-spec get_prepared_statement(binary(), binary(), aws_config()) -> {ok, map()} | {error, any()}. +get_prepared_statement(WorkGroup, StatementName, Config) -> + Request = #{<<"WorkGroup">> => WorkGroup, <<"StatementName">> => StatementName}, + request(Config, "GetPreparedStatement", Request). + %% @doc %% Athena API: %% http://docs.aws.amazon.com/athena/latest/APIReference/API_GetQueryExecution.html @@ -253,22 +358,61 @@ get_query_results(QueryExecutionId, PaginationMap, Config) -> %% %% ` %% erlcloud_athena:list_named_queries(#{<<"MaxResults">> => 1, -%% <<"NextToken">> => <<"some-token">>}). +%% <<"NextToken">> => <<"some-token">>, +%% <<"WorkGroup">> => <<"some-workgroup">>}). %% ' %% +-type list_named_queries_opts() :: [list_named_queries_opt()]. +-type list_named_queries_opt() :: {workgroup, binary()}. + -spec list_named_queries() -> {ok, map()} | {error, any()}. list_named_queries() -> - list_named_queries(#{}, default_config()). + list_named_queries(#{}, [], default_config()). --spec list_named_queries(map() | aws_config()) -> {ok, map()} | {error, any()}. +-spec list_named_queries(map() | aws_config()) -> + {ok, map()} | {error, any()}. list_named_queries(Config) when is_record(Config, aws_config) -> - list_named_queries(#{}, Config); + list_named_queries(#{}, [], Config); list_named_queries(PaginationMap) when is_map(PaginationMap) -> - list_named_queries(PaginationMap, default_config()). + list_named_queries(PaginationMap, [], default_config()). + +-spec list_named_queries(map(), aws_config() | list_named_queries_opts()) -> + {ok, map} | {error, any()}. +list_named_queries(PaginationMap, Config) when is_record(Config, aws_config) -> + list_named_queries(PaginationMap, [], Config); +list_named_queries(PaginationMap, Options) when is_list(Options) -> + list_named_queries(PaginationMap, Options, default_config()). + +-spec list_named_queries(map(), list_named_queries_opts(), aws_config()) -> + {ok, map} | {error, any()}. +list_named_queries(PaginationMap, Options, Config) -> + Params = encode_params(Options), + request(Config, "ListNamedQueries", maps:merge(PaginationMap, Params)). + +%% @doc +%% Athena API: +%% https://docs.aws.amazon.com/athena/latest/APIReference/API_ListPreparedStatements.html +%% +%% ` +%% erlcloud_athena:list_prepared_statements(<<"some-work-group">>, +%% #{<<"MaxResults">> => 1, +%% <<"NextToken">> => <<"some-token">>}). +%% ' +%% +- spec list_prepared_statements(binary()) -> {ok, map()} | {error, any()}. +list_prepared_statements(WorkGroup) -> + list_prepared_statements(WorkGroup, #{}, default_config()). + +- spec list_prepared_statements(binary(), map() | aws_config()) -> {ok, map()} | {error, any()}. +list_prepared_statements(WorkGroup, Config) when is_record(Config, aws_config) -> + list_prepared_statements(WorkGroup, #{}, Config); +list_prepared_statements(WorkGroup, PaginationMap) when is_map(PaginationMap) -> + list_prepared_statements(WorkGroup, PaginationMap, default_config()). --spec list_named_queries(map(), aws_config()) -> {ok, map} | {error, any()}. -list_named_queries(PaginationMap, Config) -> - request(Config, "ListNamedQueries", PaginationMap). +-spec list_prepared_statements(binary(), map(), aws_config()) -> {ok, map()} | {error, any()}. +list_prepared_statements(WorkGroup, PaginationMap, Config) -> + Request = PaginationMap#{<<"WorkGroup">> => WorkGroup}, + request(Config, "ListPreparedStatements", Request). %% @doc %% Athena API: @@ -279,21 +423,32 @@ list_named_queries(PaginationMap, Config) -> %% <<"NextToken">> => <<"some-token">>}). %% ' %% +-type list_query_executions_opts() :: [list_query_executions_opt()]. +-type list_query_executions_opt() :: {workgroup, binary()}. + -spec list_query_executions() -> {ok, map()} | {error, any()}. list_query_executions() -> - list_query_executions(#{}, default_config()). + list_query_executions(#{}, [], default_config()). -spec list_query_executions(map() | aws_config()) -> {ok, map()} | {error, any()}. list_query_executions(Config) when is_record(Config, aws_config) -> - list_query_executions(#{}, Config); + list_query_executions(#{}, [], Config); list_query_executions(PaginationMap) when is_map(PaginationMap) -> - list_query_executions(PaginationMap, default_config()). + list_query_executions(PaginationMap, [], default_config()). + +-spec list_query_executions(map(), aws_config() | list_query_executions_opts()) -> + {ok, map()} | {error, any()}. +list_query_executions(PaginationMap, Config) when is_record(Config, aws_config) -> + list_query_executions(PaginationMap, [], Config); +list_query_executions(PaginationMap, Options) when is_list(Options) -> + list_query_executions(PaginationMap, Options, default_config()). --spec list_query_executions(map(), aws_config()) -> +-spec list_query_executions(map(), list_query_executions_opts(), aws_config()) -> {ok, map()} | {error, any()}. -list_query_executions(PaginationMap, Config) -> - request(Config, "ListQueryExecutions", PaginationMap). +list_query_executions(PaginationMap, Options, Config) -> + Params = encode_params(Options), + request(Config, "ListQueryExecutions", maps:merge(PaginationMap, Params)). %% @doc %% Athena API: @@ -306,15 +461,19 @@ list_query_executions(PaginationMap, Config) -> %% <<"select * from some-tbl">>, %% <<"s3://some-bucket">>, %% <<"SSE_KMS">>, -%% <<"some-kms-key-id">>}] +%% <<"some-kms-key-id">>, +%% <<"optional-some-workgroup">>}] %% ). %% ' %% +-type start_query_execution_opts() :: [start_query_execution_opt()]. +-type start_query_execution_opt() :: {workgroup, binary()}. + -spec start_query_execution(binary(), binary(), binary(), binary()) -> {ok, binary()} | {error, any()}. start_query_execution(ClientReqToken, Db, Query, OutputLocation) -> start_query_execution(ClientReqToken, Db, Query, OutputLocation, undefined, - undefined, default_config()). + undefined, [], default_config()). -spec start_query_execution(binary(), binary(), binary(), binary(), aws_config()) -> @@ -322,7 +481,7 @@ start_query_execution(ClientReqToken, Db, Query, OutputLocation) -> start_query_execution(ClientReqToken, Db, Query, OutputLocation, Config) when is_record(Config, aws_config) -> start_query_execution(ClientReqToken, Db, Query, OutputLocation, undefined, - undefined, Config). + undefined, [], Config). -spec start_query_execution(binary(), binary(), binary(), binary(), binary() | undefined, @@ -331,22 +490,40 @@ start_query_execution(ClientReqToken, Db, Query, OutputLocation, Config) start_query_execution(ClientReqToken, Db, Query, OutputLocation, EncryptionOption, KmsKey) -> start_query_execution(ClientReqToken, Db, Query, OutputLocation, - EncryptionOption, KmsKey, default_config()). + EncryptionOption, KmsKey, [], default_config()). -spec start_query_execution(binary(), binary(), binary(), binary(), binary() | undefined, binary() | undefined, + aws_config() | start_query_execution_opts()) -> + {ok, binary()} | {error, any()}. +start_query_execution(ClientReqToken, Db, Query, OutputLocation, + EncryptionOption, KmsKey, Config) + when is_record(Config, aws_config) -> + start_query_execution(ClientReqToken, Db, Query, OutputLocation, + EncryptionOption, KmsKey, [], Config); +start_query_execution(ClientReqToken, Db, Query, OutputLocation, + EncryptionOption, KmsKey, Opts) + when is_list(Opts) -> + start_query_execution(ClientReqToken, Db, Query, OutputLocation, + EncryptionOption, KmsKey, Opts, default_config()). + +-spec start_query_execution(binary(), binary(), binary(), binary(), + binary() | undefined, + binary() | undefined, + start_query_execution_opts(), aws_config()) -> {ok, binary()} | {error, any()}. start_query_execution(ClientReqToken, Db, Query, OutputLocation, - EncryptionOption, KmsKey, Config) -> + EncryptionOption, KmsKey, Options, Config) -> + Params = encode_params(Options), EncryptConfig = get_encrypt_config(EncryptionOption, KmsKey), ResultConfig = EncryptConfig#{<<"OutputLocation">> => OutputLocation}, QueryExecCtxt = #{<<"Database">> => Db}, - Request = #{<<"ClientRequestToken">> => ClientReqToken, - <<"QueryExecutionContext">> => QueryExecCtxt, - <<"QueryString">> => Query, - <<"ResultConfiguration">> => ResultConfig}, + Request = Params#{<<"ClientRequestToken">> => ClientReqToken, + <<"QueryExecutionContext">> => QueryExecCtxt, + <<"QueryString">> => Query, + <<"ResultConfiguration">> => ResultConfig}, case request(Config, "StartQueryExecution", Request) of {ok, Res} -> {ok, maps:get(<<"QueryExecutionId">>, Res)}; Error -> Error @@ -362,6 +539,31 @@ get_encrypt_config(EncryptOption, KmsKey) -> #{<<"EncryptionOption">> => EncryptOption, <<"KmsKey">> => KmsKey}}. +%% @doc +%% Athena API: +%% https://docs.aws.amazon.com/athena/latest/APIReference/API_UpdatePreparedStatement.html +%% +-spec update_prepared_statement(binary(), binary(), binary()) -> ok | {error, any()}. +update_prepared_statement(WorkGroup, StatementName, QueryStatement) -> + update_prepared_statement(WorkGroup, StatementName, QueryStatement, undefined, default_config()). + +-spec update_prepared_statement(binary(), binary(), binary(), binary() | aws_config()) -> ok | {error, any()}. +update_prepared_statement(WorkGroup, StatementName, QueryStatement, Config) when is_record(Config, aws_config) -> + update_prepared_statement(WorkGroup, StatementName, QueryStatement, undefined, Config); +update_prepared_statement(WorkGroup, StatementName, QueryStatement, Description) when is_binary(Description) -> + update_prepared_statement(WorkGroup, StatementName, QueryStatement, Description, default_config()). + +-spec update_prepared_statement(binary(), binary(), binary(), binary() | undefined, aws_config()) -> ok | {error, any()}. +update_prepared_statement(WorkGroup, StatementName, QueryStatement, Description, Config) -> + Params = encode_params([{description, Description}]), + Request = Params#{<<"WorkGroup">> => WorkGroup, + <<"StatementName">> => StatementName, + <<"QueryStatement">> => QueryStatement}, + case request(Config, "UpdatePreparedStatement", Request) of + {ok, _} -> ok; + Error -> Error + end. + %% @doc %% Athena API: %% http://docs.aws.amazon.com/athena/latest/APIReference/API_StopQueryExecution.html @@ -429,3 +631,17 @@ get_url(#aws_config{athena_scheme = Scheme, athena_host = Host, athena_port = Port}) -> Scheme ++ Host ++ ":" ++ integer_to_list(Port). + +encode_params(Params) -> + encode_params(Params, []). + +encode_params([], Acc) -> + maps:from_list(Acc); +encode_params([{_, undefined} | T], Acc) -> + encode_params(T, Acc); +encode_params([{description, Description} | T], Acc) when is_binary(Description) -> + encode_params(T, [{<<"Description">>, Description} | Acc]); +encode_params([{workgroup, WorkGroup} | T], Acc) when is_binary(WorkGroup) -> + encode_params(T, [{<<"WorkGroup">>, WorkGroup} | Acc]); +encode_params([Option | _], _Acc) -> + error({erlcloud_athena, {invalid_parameter, Option}}). diff --git a/src/erlcloud_autoscaling.erl b/src/erlcloud_autoscaling.erl index 6c1b17101..425fed8db 100644 --- a/src/erlcloud_autoscaling.erl +++ b/src/erlcloud_autoscaling.erl @@ -131,6 +131,7 @@ extract_autoscaling_group(Item) -> {autoscaling_group_name, get_text("AutoScalingGroupName", Item)}, {autoscaling_group_arn, get_text("AutoScalingGroupARN", Item)}, {launch_configuration_name, get_text("LaunchConfigurationName", Item)}, + {launch_template, extract_launch_template(xmerl_xpath:string("LaunchTemplate", Item))}, {min_size, get_integer("MinSize", Item)}, {max_size, get_integer("MaxSize", Item)}, {create_time, erlcloud_xml:get_time("CreatedTime", Item)}, @@ -180,6 +181,14 @@ extract_launch_configuration(Item) -> {security_groups, [get_text(L) || L <- xmerl_xpath:string("SecurityGroups/member", Item)]} ]. +extract_launch_template([]) -> []; +extract_launch_template([Item]) -> + [ + {launch_template_id, get_text("LaunchTemplateId", Item)}, + {launch_template_name, get_text("LaunchTemplateName", Item)}, + {launch_template_version, get_text("Version", Item)} + ]. + autoscaling_query(Config, Action, Params) -> autoscaling_query(Config, Action, Params, ?API_VERSION). diff --git a/src/erlcloud_aws.erl b/src/erlcloud_aws.erl index 9f1732a9a..c81e20b2e 100644 --- a/src/erlcloud_aws.erl +++ b/src/erlcloud_aws.erl @@ -8,18 +8,20 @@ aws_request_xml4/6, aws_request_xml4/8, aws_region_from_host/1, aws_request_form/8, - aws_request_form_raw/8, - do_aws_request_form_raw/9, + aws_request_form_raw/8, aws_request_form_raw/9, + do_aws_request_form_raw/9, do_aws_request_form_raw/10, param_list/2, default_config/0, auto_config/0, auto_config/1, - default_config_region/2, + default_config_region/2, default_config_override/1, update_config/1,clear_config/1, clear_expired_configs/0, service_config/3, service_host/2, + get_host_vpc_endpoint/2, get_vpc_endpoints/0, configure/1, format_timestamp/1, http_headers_body/1, http_body/1, request_to_return/1, sign_v4_headers/5, sign_v4/8, + canonical_query_string/1, get_service_status/1, is_throttling_error_response/1, get_timeout/1, @@ -28,7 +30,6 @@ -include("erlcloud.hrl"). -include("erlcloud_aws.hrl"). --include_lib("lhttpc/include/lhttpc_types.hrl"). -define(ERLCLOUD_RETRY_TIMEOUT, 10000). -define(GREGORIAN_EPOCH_OFFSET, 62167219200). @@ -42,7 +43,7 @@ -define(AWS_REGION, ["AWS_DEFAULT_REGION", "AWS_REGION"]). %% types --type http_client_result() :: result(). % from lhttpc_types.hrl +-type http_client_result() :: erlcloud_httpc:result(). -type http_client_headers() :: [{string(), string()}]. -type httpc_result_ok() :: {http_client_headers(), binary()}. -type httpc_result_error() :: {http_error, Status :: pos_integer(), StatusLine :: string(), Body :: binary()} @@ -67,29 +68,32 @@ -record(profile_options, { session_name :: string(), - session_secs :: 900..3600, + session_secs :: 900..43200, external_id :: string() }). aws_request_xml(Method, Host, Path, Params, #aws_config{} = Config) -> Body = aws_request(Method, Host, Path, Params, Config), - element(1, xmerl_scan:string(binary_to_list(Body))). + raw_xml_response(Body). aws_request_xml(Method, Host, Path, Params, AccessKeyID, SecretAccessKey) -> Body = aws_request(Method, Host, Path, Params, AccessKeyID, SecretAccessKey), - element(1, xmerl_scan:string(binary_to_list(Body))). + raw_xml_response(Body). aws_request_xml(Method, Protocol, Host, Port, Path, Params, #aws_config{} = Config) -> Body = aws_request(Method, Protocol, Host, Port, Path, Params, Config), - element(1, xmerl_scan:string(binary_to_list(Body))). + raw_xml_response(Body). aws_request_xml(Method, Protocol, Host, Port, Path, Params, AccessKeyID, SecretAccessKey) -> Body = aws_request(Method, Protocol, Host, Port, Path, Params, AccessKeyID, SecretAccessKey), - element(1, xmerl_scan:string(binary_to_list(Body))). + raw_xml_response(Body). aws_request_xml2(Method, Host, Path, Params, #aws_config{} = Config) -> aws_request_xml2(Method, undefined, Host, undefined, Path, Params, Config). aws_request_xml2(Method, Protocol, Host, Port, Path, Params, #aws_config{} = Config) -> case aws_request2(Method, Protocol, Host, Port, Path, Params, Config) of {ok, Body} -> - {ok, element(1, xmerl_scan:string(binary_to_list(Body)))}; + case format_xml_response(Body) of + {ok, XML} -> {ok, XML}; + Error -> {error, Error} + end; {error, Reason} -> {error, Reason} end. @@ -99,7 +103,10 @@ aws_request_xml4(Method, Host, Path, Params, Service, #aws_config{} = Config) -> aws_request_xml4(Method, Protocol, Host, Port, Path, Params, Service, #aws_config{} = Config) -> case aws_request4(Method, Protocol, Host, Port, Path, Params, Service, Config) of {ok, Body} -> - {ok, element(1, xmerl_scan:string(binary_to_list(Body)))}; + case format_xml_response(Body) of + {ok, XML} -> {ok, XML}; + Error -> {error, Error} + end; {error, Reason} -> {error, Reason} end. @@ -155,15 +162,19 @@ aws_region_from_host(Host) -> %% we need to account for that: %% us-west-2: s3.us-west-2.amazonaws.com %% cn-north-1 (AWS China): s3.cn-north-1.amazonaws.com.cn + %% cn-northwest-1 (AWS China): s3.cn-northwest-1.amazonaws.com.cn %% it's assumed that the first element is the aws service (s3, ec2, etc), %% the second is the region identifier, the rest is ignored %% the exception (of course) is the dynamodb streams and the marketplace which follows a %% different format + %% another exception is VPC endpoints ["streams", "dynamodb", Value | _Rest] -> Value; [Prefix, "marketplace", Value | _Rest] - when Prefix =:= "metering"; Prefix =:= "entitlement" -> - Value; + when Prefix =:= "metering"; Prefix =:= "entitlement" -> + Value; + [_, _, Value, "vpce" | _Rest] -> + Value; [_, Value, _, _ | _Rest] -> Value; _ -> @@ -206,8 +217,8 @@ aws_request4_no_update(Method, Protocol, Host, Port, Path, Params, Service, -spec aws_request_form(Method :: atom(), Protocol :: undefined | string(), Host :: string(), - Port :: undefined | integer() | string(), Path :: string(), Form :: [string()], - Headers :: list(), Config :: aws_config()) -> {ok, Body :: binary()} | {error, httpc_result_error()}. + Port :: undefined | integer() | string(), Path :: string(), Form :: [string()], + Headers :: list(), Config :: aws_config()) -> {ok, Body :: binary()} | {error, httpc_result_error()}. aws_request_form(Method, Protocol, Host, Port, Path, Form, Headers, Config) -> RequestHeaders = case proplists:is_defined("content-type", Headers) of false -> [{"content-type", ?DEFAULT_CONTENT_TYPE} | Headers]; @@ -217,16 +228,27 @@ aws_request_form(Method, Protocol, Host, Port, Path, Form, Headers, Config) -> undefined -> "https://"; _ -> [Protocol, "://"] end, - aws_request_form_raw(Method, Scheme, Host, Port, Path, list_to_binary(Form), RequestHeaders, Config). + aws_request_form_raw(Method, Scheme, Host, Port, Path, list_to_binary(Form), RequestHeaders, [], Config). + -spec aws_request_form_raw(Method :: atom(), Scheme :: string() | [string()], - Host :: string(), Port :: undefined | integer() | string(), - Path :: string(), Form :: iodata(), Headers :: list(), - Config :: aws_config()) -> {ok, Body :: binary()} | {error, httpc_result_error()}. + Host :: string(), Port :: undefined | integer() | string(), + Path :: string(), Form :: iodata(), Headers :: list(), + Config :: aws_config()) -> {ok, Body :: binary()} | {error, httpc_result_error()}. aws_request_form_raw(Method, Scheme, Host, Port, Path, Form, Headers, Config) -> - do_aws_request_form_raw(Method, Scheme, Host, Port, Path, Form, Headers, Config, false). + do_aws_request_form_raw(Method, Scheme, Host, Port, Path, Form, Headers, [], Config, false). + +-spec aws_request_form_raw(Method :: atom(), Scheme :: string() | [string()], + Host :: string(), Port :: undefined | integer() | string(), + Path :: string(), Form :: iodata(), Headers :: list(), QueryString :: string(), + Config :: aws_config()) -> {ok, Body :: binary()} | {error, httpc_result_error()}. +aws_request_form_raw(Method, Scheme, Host, Port, Path, Form, Headers, QueryString, Config) -> + do_aws_request_form_raw(Method, Scheme, Host, Port, Path, Form, Headers, QueryString, Config, false). do_aws_request_form_raw(Method, Scheme, Host, Port, Path, Form, Headers, Config, ShowRespHeaders) -> + do_aws_request_form_raw(Method, Scheme, Host, Port, Path, Form, Headers, [], Config, ShowRespHeaders). + +do_aws_request_form_raw(Method, Scheme, Host, Port, Path, Form, Headers, QueryString, Config, ShowRespHeaders) -> URL = case Port of undefined -> [Scheme, Host, Path]; _ -> [Scheme, Host, $:, port_to_str(Port), Path] @@ -246,32 +268,23 @@ do_aws_request_form_raw(Method, Scheme, Host, Port, Path, Form, Headers, Config, response_status = Status} = Request) when %% Retry for 400, Bad Request is needed due to Amazon %% returns it in case of throttling - Status == 400; Status == 429 -> + Status == 400; Status == 403; Status == 429 -> ShouldRetry = is_throttling_error_response(Request), Request#aws_request{should_retry = ShouldRetry}; (#aws_request{response_type = error} = Request) -> Request#aws_request{should_retry = false} end, + {URI, Body} = aws_request_form_uri_and_body(Method, URL, Form, QueryString), + AwsRequest = #aws_request{uri = URI, + method = Method, + request_headers = Headers, + request_body = Body}, + %% Note: httpc MUST be used with {timeout, timeout()} option %% Many timeout related failures is observed at prod env %% when library is used in 24/7 manner - Response = - case Method of - M when M =:= get orelse M =:= head orelse M =:= delete -> - Req = lists:flatten([URL, $?, Form]), - AwsRequest = #aws_request{uri = Req, - method = M, - request_headers = Headers, - request_body = <<>>}, - erlcloud_retry:request(Config, AwsRequest, ResultFun); - _ -> - AwsRequest = #aws_request{uri = lists:flatten(URL), - method = Method, - request_headers = Headers, - request_body = Form}, - erlcloud_retry:request(Config, AwsRequest, ResultFun) - end, + Response = erlcloud_retry:request(Config, AwsRequest, ResultFun), show_headers(ShowRespHeaders, request_to_return(Response)). @@ -325,6 +338,20 @@ encode_params(Params, Headers) -> _ContentType -> {Params, LowerCaseHeaders} end. +raw_xml_response(Body) -> + case format_xml_response(Body) of + {ok, XML} -> XML; + Error -> erlang:error(Error) + end. + +format_xml_response(Body) -> + try + {ok, element(1, xmerl_scan:string(binary_to_list(Body)))} + catch + _:_ -> + {aws_error, {invalid_xml_response_document, Body}} + end. + %%%--------------------------------------------------------------------------- -spec default_config() -> aws_config(). %%%--------------------------------------------------------------------------- @@ -493,7 +520,7 @@ auto_config() -> %% This function works the same as {@link auto_config/0}, but if credentials %% are developed from User Profile as the source, the %% Options parameter provided will be used to control the -%% behavior. The credential profile name choosen can be controlled by +%% behavior. The credential profile name chosen can be controlled by %% providing {profile, atom()} as part of the options, and if %% not specified the default profile will be used. %% @@ -548,7 +575,7 @@ config_env() -> _ -> {error, environment_config_unavailable} end. --spec config_metadata(task_credentials | instance_metadata) -> {ok, #metadata_credentials{}} | {error, metadata_not_available | container_credentials_unavailable | httpc_result_error()}. +-spec config_metadata(task_credentials | instance_metadata) -> {ok, aws_config()} | {error, metadata_not_available | container_credentials_unavailable | httpc_result_error()}. config_metadata(Source) -> Config = #aws_config{}, case get_metadata_credentials( Source, Config ) of @@ -599,15 +626,17 @@ update_config(#aws_config{} = Config) -> security_token = Credentials#metadata_credentials.security_token}} end. +-dialyzer({no_return, clear_config/1}). -spec clear_config(aws_config()) -> ok. clear_config(#aws_config{assume_role = #aws_assume_role{role_arn = Arn, external_id = ExtId}}) -> - application:unset_env(erlcloud, {role_credentials, Arn, ExtId}). + unset_env_for_role_credentials(Arn, ExtId). +-dialyzer({no_match, clear_expired_configs/0}). -spec clear_expired_configs() -> ok. clear_expired_configs() -> Env = application:get_all_env(erlcloud), Now = calendar:datetime_to_gregorian_seconds(calendar:universal_time()), - [application:unset_env(erlcloud, {role_credentials, Arn, ExtId}) || + [unset_env_for_role_credentials(Arn, ExtId) || {{role_credentials, Arn, ExtId}, #role_credentials{expiration_gregorian_seconds = Ts}} <- Env, Ts < Now], @@ -628,7 +657,7 @@ clear_expired_configs() -> %% %% If an invalid service name is provided, then this will throw an error, %% presuming that this is just a coding error. This behavior allows the -%% chaining of calls to this interface to allow concise configuraiton of a +%% chaining of calls to this interface to allow concise configuration of a %% config for multiple services. %% service_config( Service, Region, Config ) when is_atom(Service) -> @@ -639,6 +668,14 @@ service_config( Service, Region, Config ) when is_atom(Region) -> service_config( Service, atom_to_binary(Region, latin1), Config ); service_config( Service, Region, Config ) when is_list(Region) -> service_config( Service, list_to_binary(Region), Config ); +service_config( <<"securityhub">> = Service, Region, Config ) -> + Host = service_host( Service, Region ), + Config#aws_config{securityhub_host = Host}; +service_config( <<"access_analyzer">>, Region, Config ) -> + service_config( <<"access-analyzer">>, Region, Config ); +service_config( <<"access-analyzer">> = Service, Region, Config ) -> + Host = service_host( Service, Region ), + Config#aws_config{access_analyzer_host = Host}; service_config( <<"as">>, Region, Config ) -> service_config( <<"autoscaling">>, Region, Config ); service_config( <<"autoscaling">> = Service, Region, Config ) -> @@ -675,6 +712,9 @@ service_config( <<"ec2">> = Service, Region, Config ) -> service_config( <<"ecs">> = Service, Region, Config ) -> Host = service_host( Service, Region ), Config#aws_config{ ecs_host = Host }; +service_config( <<"efs">> = Service, Region, Config ) -> + Host = service_host( Service, Region ), + Config#aws_config{ efs_host = Host }; service_config( <<"elasticloadbalancing">> = Service, Region, Config ) -> Host = service_host( Service, Region ), Config#aws_config{ elb_host = Host }; @@ -688,6 +728,9 @@ service_config( <<"emr">>, Region, Config ) -> service_config( <<"iam">> = Service, <<"cn-north-1">> = Region, Config ) -> Host = service_host( Service, Region ), Config#aws_config{ iam_host = Host }; +service_config( <<"iam">> = Service, <<"cn-northwest-1">> = Region, Config ) -> + Host = service_host( Service, Region ), + Config#aws_config{ iam_host = Host }; service_config( <<"iam">>, _Region, Config ) -> Config; service_config( <<"inspector">> = Service, Region, Config ) -> Host = service_host( Service, Region ), @@ -735,6 +778,9 @@ service_config( <<"sdb">> = Service, Region, Config ) -> service_config( <<"ses">>, Region, Config ) -> Host = service_host( <<"email">>, Region ), Config#aws_config{ ses_host = Host }; +service_config( <<"sm">>, Region, Config) -> + Host = service_host( <<"secretsmanager">>, Region ), + Config#aws_config{ sm_host = Host }; service_config( <<"sns">> = Service, Region, Config ) -> Host = service_host( Service, Region ), Config#aws_config{ sns_host = Host }; @@ -750,6 +796,9 @@ service_config( <<"glue">> = Service, Region, Config ) -> service_config( <<"athena">> = Service, Region, Config ) -> Host = service_host( Service, Region ), Config#aws_config{ athena_host = Host }; +service_config( <<"cognito_user_pools">> = Service, Region, Config ) -> + Host = service_host( Service, Region ), + Config#aws_config{ cognito_user_pools_host = Host }; service_config( <<"states">> = Service, Region, Config ) -> Host = service_host( Service, Region ), Config#aws_config{ states_host = Host }; @@ -760,9 +809,22 @@ service_config(<<"cloudwatch_logs">>, Region, Config)-> Host = service_host(<<"logs">>, Region), Config#aws_config{cloudwatch_logs_host = Host}; service_config( <<"waf">>, _Region, Config ) -> Config; +service_config( <<"guardduty">> = Service, Region, Config ) -> + Host = service_host( Service, Region ), + Config#aws_config{guardduty_host = Host}; service_config( <<"cur">>, Region, Config ) -> Host = service_host(<<"cur">>, Region), - Config#aws_config{cur_host = Host}. + Config#aws_config{cur_host = Host}; +service_config( <<"application_autoscaling">>, Region, Config ) -> + Host = service_host(<<"application_autoscaling">>, Region), + Config#aws_config{application_autoscaling_host = Host}; +service_config( <<"workspaces">> = Service, Region, Config ) -> + Host = service_host(Service, Region), + Config#aws_config{workspaces_host = Host}; +service_config( <<"ssm">> = Service, Region, Config ) -> + Host = service_host(Service, Region), + Config#aws_config{ssm_host = Host}. + %%%--------------------------------------------------------------------------- -spec service_host( Service :: binary(), @@ -774,17 +836,113 @@ service_config( <<"cur">>, Region, Config ) -> %% This function handles the special and general cases of service host %% names. %% +service_host( <<"cognito_user_pools">>, Region ) -> + binary_to_list(<<"cognito-idp.", Region/binary, ".amazonaws.com">>); service_host( <<"s3">>, <<"us-east-1">> ) -> "s3-external-1.amazonaws.com"; +service_host( <<"s3">>, <<"us-gov-west-1">> ) -> "s3-fips-us-gov-west-1.amazonaws.com"; service_host( <<"s3">>, <<"cn-north-1">> ) -> "s3.cn-north-1.amazonaws.com.cn"; -service_host( <<"s3">>, <<"us-gov-west-1">> ) -> - "s3-fips-us-gov-west-1.amazonaws.com"; +service_host( <<"s3">>, <<"cn-northwest-1">> ) -> "s3.cn-northwest-1.amazonaws.com.cn"; service_host( <<"s3">>, Region ) -> - binary_to_list( <<"s3-", Region/binary, ".amazonaws.com">> ); + binary_to_list( <<"s3.", Region/binary, ".amazonaws.com">> ); +service_host( <<"iam">>, <<"cn-north-1">> ) -> "iam.amazonaws.com.cn"; +service_host( <<"iam">>, <<"cn-northwest-1">> ) -> "iam.amazonaws.com.cn"; service_host( <<"sdb">>, <<"us-east-1">> ) -> "sdb.amazonaws.com"; -service_host( <<"states">>, Region ) -> - binary_to_list( <<"states.", Region/binary, ".amazonaws.com">> ); -service_host( Service, Region ) when is_binary(Service) -> - binary_to_list( <> ). +service_host( Service, <<"cn-north-1">> = Region ) when is_binary(Service) -> + binary_to_list( <> ); +service_host( Service, <<"cn-northwest-1">> = Region ) when is_binary(Service) -> + binary_to_list( <> ); +service_host( Service, Region ) when is_binary(Service) andalso is_binary(Region) -> + Default = <>, + binary_to_list(get_host_vpc_endpoint(Service, Default)). + +-spec get_host_vpc_endpoint(binary(), binary()) -> binary(). +% some services can have VPCe configured and we allow to mitigate cross-AZ traffic. +% It's application level decision to use VPCe and configure those. +% magic can be done via EC2 DescribeVpcEndpoints/filter by VPC/filter by AZ. +% however, permissions and describe* API throttling is not what we want to deal with here. +get_host_vpc_endpoint(Service, Default) when is_binary(Service) -> + VPCEndpointsByService = get_vpc_endpoints(), + ConfiguredEndpoints = proplists:get_value(Service, VPCEndpointsByService, []), + %% resolve through ENV if any + Endpoints = case ConfiguredEndpoints of + {env, EnvVarName} when is_list(EnvVarName) -> + % ignore "" env var or ",," cases + % also handle "zoneID:zoneName" form when it comes from CFN + Es = string_split(os:getenv(EnvVarName, ""), ","), + lists:filtermap( + fun ("") -> false; + (Value) -> + case string_split(Value, ":") of + [_Id, Name] -> {true, list_to_binary(Name)}; + [Name] -> {true, list_to_binary(Name)} + end + end, + Es + ); + EndpointsList when is_list(EndpointsList) -> + EndpointsList + end, + % now match our AZ to configured ones + pick_vpc_endpoint(Endpoints, Default). + +-ifdef(AT_LEAST_20). +string_split(String, Char) -> + string:split(String, Char, all). +-else. +string_split(String, Char) -> + Subject = list_to_binary(String), + Pattern = list_to_binary(Char), + Options = [global], + [binary_to_list(Elem) || Elem <- binary:split(Subject, Pattern, Options)]. +-endif. + +pick_vpc_endpoint([], Default) -> Default; +pick_vpc_endpoint(Endpoints, Default) -> + case get_availability_zone() of + {ok, AZ} -> + lists:foldl( + fun (E , Acc) -> + case {binary:match(E, AZ), Acc == Default} of + {nomatch, _} -> Acc; + % take only the first one if smb provided duplicates + {_, true} -> E; + % was previously set + {_, false} -> Acc + end + end, + Default, + Endpoints + ); + {error, _} -> + Default + end. + +-spec get_vpc_endpoints() -> list({binary(), binary()}). +get_vpc_endpoints() -> + application:get_env(erlcloud, services_vpc_endpoints, []). + +-spec get_availability_zone() -> {ok, binary()} | {error, term()}. +get_availability_zone() -> + case application:get_env(erlcloud, availability_zone) of + {ok, AZ} = OkResult when is_binary(AZ) -> + OkResult; + _ -> + cache_instance_metadata_availability_zone() + end. + +-spec cache_instance_metadata_availability_zone() -> {ok, binary()} | {error, term()}. +cache_instance_metadata_availability_zone() -> + % it fine to use default here - no IAM is used, only for http client + % one cannot use auto_config()/default_cfg() as it creates an infinite recursion. + Config = #aws_config{}, + IMDSv2Token = maybe_imdsv2_session_token(Config), + case erlcloud_ec2_meta:get_instance_metadata("placement/availability-zone", Config, IMDSv2Token) of + {ok, AZ} = OkResult -> + application:set_env(erlcloud, availability_zone, AZ), + OkResult; + {error, _} = Error -> + Error + end. -spec configure(aws_config()) -> {ok, aws_config()}. @@ -821,17 +979,18 @@ timestamp_to_gregorian_seconds(Timestamp) -> get_credentials_from_metadata(Config) -> %% TODO this function should retry on errors getting credentials %% First get the list of roles - case erlcloud_ec2_meta:get_instance_metadata("iam/security-credentials/", Config) of + IMDSv2Token = maybe_imdsv2_session_token(Config), + case erlcloud_ec2_meta:get_instance_metadata("iam/security-credentials/", Config, IMDSv2Token) of {error, Reason} -> {error, Reason}; {ok, Body} -> %% Always use the first role [Role | _] = binary:split(Body, <<$\n>>), - case erlcloud_ec2_meta:get_instance_metadata("iam/security-credentials/" ++ binary_to_list(Role), Config) of + case erlcloud_ec2_meta:get_instance_metadata("iam/security-credentials/" ++ binary_to_list(Role), Config, IMDSv2Token) of {error, Reason} -> {error, Reason}; {ok, Json} -> - Creds = jsx:decode(Json), + Creds = jsx:decode(Json, [{return_maps, false}]), get_credentials_from_metadata_xform( Creds ) end end. @@ -844,7 +1003,7 @@ get_credentials_from_task_metadata(Config) -> {error, Reason} -> {error, Reason}; {ok, Json} -> - Creds = jsx:decode(Json), + Creds = jsx:decode(Json, [{return_maps, false}]), get_credentials_from_metadata_xform( Creds ) end. @@ -873,12 +1032,10 @@ prop_to_list_defined( Name, Props ) -> end. +-dialyzer({no_return, get_role_credentials/1}). -spec get_role_credentials(aws_config()) -> {ok, #role_credentials{}}. get_role_credentials(#aws_config{assume_role = AssumeRole} = Config) -> - case application:get_env(erlcloud, - {role_credentials, - AssumeRole#aws_assume_role.role_arn, - AssumeRole#aws_assume_role.external_id}) of + case get_env_for_role_credentials(AssumeRole#aws_assume_role.role_arn, AssumeRole#aws_assume_role.external_id) of {ok, #role_credentials{expiration_gregorian_seconds = Expiration} = Credentials} -> Now = calendar:datetime_to_gregorian_seconds(calendar:universal_time()), %% Get new credentials if these will expire in less than 5 minutes @@ -890,6 +1047,7 @@ get_role_credentials(#aws_config{assume_role = AssumeRole} = Config) -> get_credentials_from_role(Config) end. +-compile({nowarn_unused_function, get_credentials_from_role/1}). -spec get_credentials_from_role(aws_config()) -> {ok, #role_credentials{}}. get_credentials_from_role(#aws_config{assume_role = AssumeRole} = Config) -> %% We have to reset the assume role to make sure we do not @@ -908,11 +1066,7 @@ get_credentials_from_role(#aws_config{assume_role = AssumeRole} = Config) -> secret_access_key = proplists:get_value(secret_access_key, Creds), session_token = proplists:get_value(session_token, Creds), expiration_gregorian_seconds = ExpireAt}, - application:set_env(erlcloud, - {role_credentials, - AssumeRole#aws_assume_role.role_arn, - AssumeRole#aws_assume_role.external_id}, - Record), + set_env_for_role_credentials(AssumeRole#aws_assume_role.role_arn, AssumeRole#aws_assume_role.external_id, Record), {ok, Record}. port_to_str(Port) when is_integer(Port) -> @@ -947,6 +1101,33 @@ get_timeout(#aws_config{timeout = undefined}) -> get_timeout(#aws_config{timeout = Timeout}) -> Timeout. +%% Construct the URI and body for an AWS request based on the Method, Form, and +%% QueryString: if the request is a read/delete, join Form and QueryString in +%% the URI and give an empty body; otherwise, pass the Form in the Body and +%% the QueryString in the URI. +aws_request_form_uri_and_body(Method, URL, Form, QueryString) when Method =:= delete; + Method =:= get; + Method =:= head -> + URI = make_uri(URL, join_query_strings([Form, QueryString])), + {URI, <<>>}; +aws_request_form_uri_and_body(_M, URL, Form, QueryString) -> + URI = make_uri(URL, QueryString), + {URI, Form}. + + +%% Given a URL and a QueryString, combine them together appropriately (drop the +%% QueryString if empty). +make_uri(URL, QueryString) when QueryString =:= <<>>; QueryString == [] -> + lists:flatten(URL); +make_uri(URL, QueryString) -> + lists:flatten([URL, $?, QueryString]). + + +%% Join a list of query strings together with `&', filtering out empty ones. +join_query_strings(QueryStrings) -> + lists:join($&, [QS || QS <- QueryStrings, QS /= <<>>, QS /= []]). + + %% Convert an aws_request record to return value as returned by http_headers_body request_to_return(#aws_request{response_type = ok, response_headers = Headers, @@ -965,11 +1146,11 @@ request_to_return(#aws_request{response_type = error, {error, {http_error, Status, StatusLine, Body, Headers}}. %% http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html --spec sign_v4_headers(aws_config(), headers(), string() | binary(), string(), string()) -> headers(). +-spec sign_v4_headers(aws_config(), erlcloud_httpc:headers(), string() | binary(), string(), string()) -> erlcloud_httpc:headers(). sign_v4_headers(Config, Headers, Payload, Region, Service) -> sign_v4(post, "/", Config, Headers, Payload, Region, Service, []). --spec sign_v4(atom(), list(), aws_config(), headers(), string() | binary(), string(), string(), list()) -> headers(). +-spec sign_v4(atom(), list(), aws_config(), erlcloud_httpc:headers(), string() | binary(), string(), string(), list()) -> erlcloud_httpc:headers(). sign_v4(Method, Uri, Config, Headers, Payload, Region, Service, QueryParams) -> Date = iso_8601_basic_time(), {PayloadHash, Headers1} = @@ -1026,16 +1207,26 @@ canonical_headers(Headers) -> {Canonical, Signed}. %% @doc calculate canonical query string out of query params and according to v4 documentation +-spec canonical_query_string(Params) -> String +when Params :: [{Name, Value}], + Name :: atom() | string() | binary(), + Value :: atom() | string() | binary() | integer(), + String :: string(). canonical_query_string([]) -> ""; canonical_query_string(Params) -> - Normalized = [{erlcloud_http:url_encode(Name), erlcloud_http:url_encode(erlcloud_http:value_to_string(Value))} || {Name, Value} <- Params], - Sorted = lists:keysort(1, Normalized), - string:join([case Value of - [] -> [Key, "="]; - _ -> [Key, "=", Value] - end - || {Key, Value} <- Sorted, Value =/= none, Value =/= undefined], "&"). + Encoded = [encode_param(Name, Value) || {Name, Value} <- Params], + Sorted = lists:sort(Encoded), + string:join(Sorted, "&"). + +encode_param(Name, Value) -> + EncodedName = erlcloud_http:url_encode(erlcloud_http:value_to_string(Name)), + case erlcloud_http:url_encode(erlcloud_http:value_to_string(Value)) of + "" -> + EncodedName ++ "="; + EncodedValue -> + EncodedName ++ "=" ++ EncodedValue + end. trimall(Value) -> %% TODO - remove excess internal whitespace in header values @@ -1095,7 +1286,7 @@ get_service_status(ServiceNames) when is_list(ServiceNames) -> "/data.json", "", [], default_config()), case get_filtered_statuses(ServiceNames, - proplists:get_value(<<"current">>, jsx:decode(Json))) + proplists:get_value(<<"current">>, jsx:decode(Json, [{return_maps, false}]))) of [] -> ok; ReturnStatuses -> ReturnStatuses @@ -1126,7 +1317,7 @@ is_throttling_error_response(RequestResponse) -> error_type = aws, response_body = RespBody} = RequestResponse, - case binary:match(RespBody, <<"Throttling">>) of + case binary:match(RespBody, <<"Throttl">>) of nomatch -> false; _ -> @@ -1163,7 +1354,7 @@ profile( Name ) -> -type profile_option() :: {role_session_name, string()} - | {role_session_secs, 900..3600}. + | {role_session_secs, 900..43200}. %%%--------------------------------------------------------------------------- -spec profile( Name :: atom(), Options :: [profile_option()] ) -> @@ -1189,8 +1380,8 @@ profile( Name ) -> %% source_profile = default %% %% -%% and finally, will supports the role_arn specification, and will -%% assume the role indicated using the credentials current when interpreting +%% Finally, it supports the role_arn specification, and will +%% assume the role indicated using the current credentials when interpreting %% the profile in which they it is declared: %% %%
@@ -1199,7 +1390,7 @@ profile( Name ) ->
 %%  source_profile = default
 %% 
%% -%% When using the the role_arn specification, you may supply the +%% When using the role_arn specification, you may supply the %% following two options to control the way in which the assume_role request %% is made via AWS STS service: %% @@ -1216,7 +1407,7 @@ profile( Name ) -> %% %%
  • 'external_id' %%

    The identifier that is used in the ExternalId -%% parameter. If this option is not specified, then it will default to +%% parameter. If this option isn't specified, then it will default to %% 'undefined', which will work for normal in-account roles, but will %% need to be specified for roles in external accounts.

    %%
  • @@ -1351,3 +1542,36 @@ error_msg( Message ) -> error_msg( Format, Values ) -> Error = iolist_to_binary( io_lib:format( Format, Values ) ), throw( {error, Error} ). + +-dialyzer({nowarn_function, unset_env_for_role_credentials/2}). +-spec unset_env_for_role_credentials(Arn, ExtId) -> ok + when Arn :: string() | undefined, + ExtId :: string() | undefined. +unset_env_for_role_credentials(Arn, ExtId) -> + % application:unset_env is undocumented in regards to type(Par) =/= atom() + application:unset_env(erlcloud, {role_credentials, Arn, ExtId}). + +-dialyzer({nowarn_function, get_env_for_role_credentials/2}). +-spec get_env_for_role_credentials(Arn, ExtId) -> undefined | {ok, Val} + when Arn :: string() | undefined, + ExtId :: string() | undefined, + Val :: term(). +get_env_for_role_credentials(Arn, ExtId) -> + % application:get_env is undocumented in regards to type(Par) =/= atom() + application:get_env(erlcloud, {role_credentials, Arn, ExtId}). + +-dialyzer({nowarn_function, set_env_for_role_credentials/3}). +-spec set_env_for_role_credentials(Arn, ExtId, Val) -> ok + when Arn :: string() | undefined, + ExtId :: string() | undefined, + Val :: term(). +set_env_for_role_credentials(Arn, ExtId, Val) -> + % application:set_env is undocumented in regards to type(Par) =/= atom() + application:set_env(erlcloud, {role_credentials, Arn, ExtId}, Val). + +-spec maybe_imdsv2_session_token(aws_config()) -> binary() | undefined. +maybe_imdsv2_session_token(Config) -> + case erlcloud_ec2_meta:generate_session_token(60, Config) of + {ok, Token} -> Token; + _Error -> undefined + end. diff --git a/src/erlcloud_cloudformation.erl b/src/erlcloud_cloudformation.erl index 87366a1f0..97887dd79 100644 --- a/src/erlcloud_cloudformation.erl +++ b/src/erlcloud_cloudformation.erl @@ -2,6 +2,7 @@ -include("erlcloud.hrl"). -include("erlcloud_aws.hrl"). +-include("erlcloud_cloudformation.hrl"). -define(API_VERSION, "2010-05-15"). @@ -23,10 +24,16 @@ %% Cloud Formation API Functions -export ([ + create_stack/2, + create_stack/1, list_stacks_all/1, list_stacks_all/2, + update_stack/2, + update_stack/1, list_stacks/2, list_stacks/1, + delete_stack/2, + delete_stack/1, list_stack_resources_all/2, list_stack_resources_all/3, list_stack_resources/2, @@ -74,6 +81,27 @@ new(AccessKeyID, SecretAccessKey) -> %%============================================================================== %% Cloud Formation API Functions %%============================================================================== +-spec create_stack(cloudformation_create_stack_input()) -> + {ok, string()} | {error, error_reason()}. +create_stack(Spec = #cloudformation_create_stack_input{}) -> + create_stack(Spec, default_config()). + +-spec create_stack(cloudformation_create_stack_input(), aws_config()) -> + {ok, string()} | {error, error_reason()}. +create_stack(Spec = #cloudformation_create_stack_input{}, Config = #aws_config{}) -> + + Params = create_stack_input_to_params(Spec), + case cloudformation_request(Config, "CreateStack", Params) of + {ok, XmlNode} -> + StackId = erlcloud_xml:get_text( + "/CreateStackResponse/CreateStackResult/StackId", + XmlNode, + undefined), + {ok, StackId}; + {error, Error} -> + {error, Error} + end. + -spec list_stacks_all(params()) -> {ok, cloudformation_list()} | {error, error_reason()}. list_stacks_all(Params) -> list_stacks_all(Params, default_config()). @@ -115,6 +143,44 @@ list_stacks(Params, Config = #aws_config{}) -> {error, Error} end. +-spec update_stack(cloudformation_update_stack_input()) -> + {ok, string()} | {error, error_reason()}. +update_stack(Spec = #cloudformation_update_stack_input{}) -> + update_stack(Spec, default_config()). + +-spec update_stack(cloudformation_update_stack_input(), aws_config()) -> + {ok, string()} | {error, error_reason()}. +update_stack(Spec = #cloudformation_update_stack_input{}, Config = #aws_config{}) -> + + Params = update_stack_input_to_params(Spec), + case cloudformation_request(Config, "UpdateStack", Params) of + {ok, XmlNode} -> + StackId = erlcloud_xml:get_text( + "/UpdateStackResponse/UpdateStackResult/StackId", + XmlNode, + undefined), + {ok, StackId}; + {error, Error} -> + {error, Error} + end. + +-spec delete_stack(cloudformation_delete_stack_input()) -> + {ok, string()} | {error, error_reason()}. +delete_stack(Spec = #cloudformation_delete_stack_input{}) -> + delete_stack(Spec, default_config()). + +-spec delete_stack(cloudformation_delete_stack_input(), aws_config()) -> + ok | {error, error_reason()}. +delete_stack(Spec = #cloudformation_delete_stack_input{}, Config = #aws_config{}) -> + + Params = delete_stack_input_to_params(Spec), + case cloudformation_request(Config, "DeleteStack", Params) of + {ok, _XmlNode} -> + ok; + {error, Error} -> + {error, Error} + end. + -spec list_stack_resources_all(params(), string()) -> {ok, cloudformation_list()} | {error, error_reason()}. list_stack_resources_all(Params, StackName) -> @@ -425,8 +491,9 @@ list_all(Fun, Params, Config, Acc) -> undefined -> lists:foldl(fun erlang:'++'/2, [], [Data | Acc]); _ -> - list_all(Fun, [{next_token, NextToken} | Params], - Config, [Data | Acc]) + list_all(Fun, + [convert_param({next_token, NextToken}) | Params], + Config, [Data | Acc]) end; {error, Reason} -> {error, Reason} @@ -659,6 +726,137 @@ extract_account_limit(XmlNode) -> {value, "Value", optional_text} ], XmlNode). - - +create_stack_input_to_params(Spec) -> + lists:flatten([ + base_create_stack_input_params(Spec), + erlcloud_util:encode_object_list( + "Parameters", + cloudformation_parameters_fields(Spec#cloudformation_create_stack_input.parameters)), + erlcloud_util:encode_list("Capabilities", Spec#cloudformation_create_stack_input.capabilities), + erlcloud_util:encode_list("NotificationARNs", Spec#cloudformation_create_stack_input.notification_arns), + erlcloud_util:encode_list("ResourceTypes", Spec#cloudformation_create_stack_input.resource_types), + erlcloud_util:encode_object( + "RollbackConfiguration", + cloudformation_rollback_configuration_fields(Spec#cloudformation_create_stack_input.rollback_configuration) + ), + erlcloud_util:encode_object_list( + "Tags", + cloudformation_tags_fields(Spec#cloudformation_create_stack_input.tags) + ) + ]). + +base_create_stack_input_params(Spec) -> + [ + { "ClientRequestToken", Spec#cloudformation_create_stack_input.client_request_token }, + { "DisableRollback", Spec#cloudformation_create_stack_input.disable_rollback }, + { "EnableTerminationProtection", Spec#cloudformation_create_stack_input.enable_termination_protection }, + { "OnFailure", Spec#cloudformation_create_stack_input.on_failure }, + { "RoleARN", Spec#cloudformation_create_stack_input.role_arn }, + { "StackName", Spec#cloudformation_create_stack_input.stack_name }, + { "StackPolicyBody", Spec#cloudformation_create_stack_input.stack_policy_body }, + { "StackPolicyURL", Spec#cloudformation_create_stack_input.stack_policy_url }, + { "TemplateBody", Spec#cloudformation_create_stack_input.template_body }, + { "TemplateURL", Spec#cloudformation_create_stack_input.template_url }, + { "TimeoutInMinutes", Spec#cloudformation_create_stack_input.timeout_in_minutes } + ]. + +update_stack_input_to_params(Spec) -> + lists:flatten([ + update_create_stack_input_params(Spec), + erlcloud_util:encode_list("Capabilities", Spec#cloudformation_update_stack_input.capabilities), + erlcloud_util:encode_list("NotificationARNs", Spec#cloudformation_update_stack_input.notification_arns), + erlcloud_util:encode_object_list( + "Parameters", + cloudformation_parameters_fields(Spec#cloudformation_update_stack_input.parameters) + ), + erlcloud_util:encode_list("ResourceTypes", Spec#cloudformation_update_stack_input.resource_types), + erlcloud_util:encode_object( + "RollbackConfiguration", + cloudformation_rollback_configuration_fields(Spec#cloudformation_update_stack_input.rollback_configuration) + ), + erlcloud_util:encode_object_list( + "Tags", + cloudformation_tags_fields(Spec#cloudformation_update_stack_input.tags) + ) + ]). + +update_create_stack_input_params(Spec) -> + [ + { "ClientRequestToken", Spec#cloudformation_update_stack_input.client_request_token }, + { "RoleARN", Spec#cloudformation_update_stack_input.role_arn }, + { "StackName", Spec#cloudformation_update_stack_input.stack_name }, + { "StackPolicyBody", Spec#cloudformation_update_stack_input.stack_policy_body }, + { "StackPolicyDuringUpdateBody", Spec#cloudformation_update_stack_input.stack_policy_during_update_body }, + { "StackPolicyDuringUpdateURL", Spec#cloudformation_update_stack_input.stack_policy_during_update_url}, + { "StackPolicyURL", Spec#cloudformation_update_stack_input.stack_policy_url }, + { "TemplateBody", Spec#cloudformation_update_stack_input.template_body }, + { "TemplateURL", Spec#cloudformation_update_stack_input.template_url }, + { "UsePreviousTemplate", Spec#cloudformation_update_stack_input.use_previous_template } + ]. + +delete_stack_input_to_params(#cloudformation_delete_stack_input{} = Spec) -> + lists:flatten([ + base_delete_stack_input_params(Spec), + erlcloud_util:encode_list("RetainResources", Spec#cloudformation_delete_stack_input.retain_resources) + ]). + +base_delete_stack_input_params(Spec) -> + [ + { "ClientRequestToken", Spec#cloudformation_delete_stack_input.client_request_token }, + { "RoleARN", Spec#cloudformation_delete_stack_input.role_arn }, + { "StackName", Spec#cloudformation_delete_stack_input.stack_name } + ]. + +cloudformation_parameters_fields(CloudformationParameters) -> + lists:map(fun cloudformation_parameter_fields/1, CloudformationParameters). + +cloudformation_parameter_fields(#cloudformation_parameter{} = Parameters) -> + Params = [ + { "ParameterKey", Parameters#cloudformation_parameter.parameter_key}, + { "ParameterValue", Parameters#cloudformation_parameter.parameter_value}, + { "ResolvedValue", Parameters#cloudformation_parameter.resolved_value}, + { "UsePreviousValue", Parameters#cloudformation_parameter.use_previous_value} + ], + filter_undefined(Params); +cloudformation_parameter_fields(_) -> + []. + +cloudformation_tags_fields(Tags) -> + lists:map(fun cloudformation_tag_fields/1, Tags). + +cloudformation_tag_fields(#cloudformation_tag{} = Tag) -> + Params = [ + { "Key", Tag#cloudformation_tag.key}, + { "Value", Tag#cloudformation_tag.value} + ], + filter_undefined(Params); +cloudformation_tag_fields(_) -> + []. + +cloudformation_rollback_configuration_fields(#cloudformation_rollback_configuration{} = RollbackConfig) -> + lists:flatten([ + [{"MonitoringTimeInMinutes", RollbackConfig#cloudformation_rollback_configuration.monitoring_time_in_minutes}], + erlcloud_util:encode_object_list( + "RollbackTriggers", + cloudformation_rollback_triggers_fields( + RollbackConfig#cloudformation_rollback_configuration.rollback_triggers + ) + ) + ]); +cloudformation_rollback_configuration_fields(_) -> + []. + +cloudformation_rollback_triggers_fields(RollbackTriggers) -> + lists:map(fun cloudformation_rollback_trigger_fields/1, RollbackTriggers). + +cloudformation_rollback_trigger_fields(#cloudformation_rollback_trigger{} = RollbackTrigger) -> + filter_undefined([ + {"Arn", RollbackTrigger#cloudformation_rollback_trigger.arn}, + {"Type", RollbackTrigger#cloudformation_rollback_trigger.type} + ]); +cloudformation_rollback_trigger_fields(_) -> + []. + +filter_undefined(Params) -> + [{K, V} || {K, V} <- Params, V =/= undefined]. diff --git a/src/erlcloud_cloudfront.erl b/src/erlcloud_cloudfront.erl index d48169cb5..be20478ad 100644 --- a/src/erlcloud_cloudfront.erl +++ b/src/erlcloud_cloudfront.erl @@ -20,7 +20,7 @@ -define(MAX_RESULTS, 100). --spec extract_distribution_summary(Node :: list()) -> proplist(). +-spec extract_distribution_summary(Node :: xmerl_xpath_doc_nodes()) -> proplist(). extract_distribution_summary(Node) -> erlcloud_xml:decode( [ @@ -58,7 +58,7 @@ extract_distribution(Node) -> ], Node) ++ extract_distribution_config(hd(xmerl_xpath:string("DistributionConfig", Node))). --spec extract_distribution_config(Node :: list()) -> proplist(). +-spec extract_distribution_config(Node :: xmerl_xpath_doc_nodes()) -> proplist(). extract_distribution_config(Node) -> erlcloud_xml:decode( [ @@ -79,7 +79,7 @@ extract_distribution_config(Node) -> ], Node). --spec extract_cache_behavior(Node :: list()) -> proplist(). +-spec extract_cache_behavior(Node :: xmerl_xpath_doc_nodes()) -> proplist(). extract_cache_behavior(Node) -> erlcloud_xml:decode( [ @@ -181,7 +181,7 @@ list_distributions(Config) when list_distributions(?MAX_RESULTS, undefined, Config). --spec list_distributions(integer(), string()) -> ok_error(proplist(), string()). +-spec list_distributions(integer(), undefined | string()) -> ok_error(proplist(), string()). list_distributions(MaxResults, Marker) -> list_distributions(MaxResults, Marker, erlcloud_aws:default_config()). diff --git a/src/erlcloud_cloudsearch.erl b/src/erlcloud_cloudsearch.erl index 9ed597885..44a36d88e 100644 --- a/src/erlcloud_cloudsearch.erl +++ b/src/erlcloud_cloudsearch.erl @@ -56,7 +56,7 @@ -type cloudsearch_domain_tag() :: [{TagKey :: binary(), TagValue :: string()}]. -type cloudsearch_index_field_option() :: { OptionName :: binary(), - OptionValue :: boolean() | string() | null | integer() | float() | list() + OptionValue :: boolean() | string() | null | integer() | float() | list() | binary() }. -type cloudsearch_index_field() :: [cloudsearch_index_field_option()]. @@ -721,7 +721,7 @@ cloudsearch_query(Config, Action, Params, ApiVersion) -> [{"Accept", "application/json"}], Config) of {ok, Response} -> - {ok, jsx:decode(Response)}; + {ok, jsx:decode(Response, [{return_maps, false}])}; {error, Reason} -> {error, Reason} end. @@ -734,9 +734,9 @@ cloudsearch_post_json(Host, Path, Body, case erlcloud_aws:aws_request_form_raw( post, Scheme, Host, Port, Path, Body, [{"content-type", "application/json"} | Headers], - Config) of + [], Config) of {ok, RespBody} -> - {ok, jsx:decode(RespBody)}; + {ok, jsx:decode(RespBody, [{return_maps, false}])}; {error, Reason} -> {error, Reason} end. diff --git a/src/erlcloud_cloudtrail.erl b/src/erlcloud_cloudtrail.erl index a1a2194f9..ab073d1f7 100644 --- a/src/erlcloud_cloudtrail.erl +++ b/src/erlcloud_cloudtrail.erl @@ -13,6 +13,7 @@ delete_trail/1, delete_trail/2, describe_trails/0, describe_trails/1, describe_trails/2, describe_trails/3, get_trail_status/1, get_trail_status/2, + get_event_selectors/1, get_event_selectors/2, start_logging/1, start_logging/2, stop_logging/1, stop_logging/2, update_trail/4, update_trail/5, update_trail/6, @@ -117,29 +118,38 @@ describe_trails(Trails, IncludeShadowTrails, Config) -> end, ct_request("DescribeTrails", Json, Config). --spec get_trail_status([string()] ) -> ct_return(). +-spec get_trail_status(string() ) -> ct_return(). get_trail_status(Trail) -> get_trail_status(Trail, default_config()). --spec get_trail_status([string()], aws_config()) -> ct_return(). +-spec get_trail_status(string(), aws_config()) -> ct_return(). get_trail_status(Trail, Config) -> Json = [{<<"Name">>, list_to_binary(Trail)}], ct_request("GetTrailStatus", Json, Config). --spec start_logging([string()] ) -> ct_return(). +-spec get_event_selectors(string()) -> ct_return(). +get_event_selectors(Trail) -> + get_event_selectors(Trail, default_config()). + +-spec get_event_selectors(string(), aws_config()) -> ct_return(). +get_event_selectors(Trail, Config) -> + Json = [{<<"TrailName">>, list_to_binary(Trail)}], + ct_request("GetEventSelectors", Json, Config). + +-spec start_logging(string() ) -> ct_return(). start_logging(Trail) -> start_logging(Trail, default_config()). --spec start_logging([string()], aws_config()) -> ct_return(). +-spec start_logging(string(), aws_config()) -> ct_return(). start_logging(Trail, Config) -> Json = [{<<"Name">>, list_to_binary(Trail)}], ct_request("StartLogging", Json, Config). --spec stop_logging([string()] ) -> ct_return(). +-spec stop_logging(string() ) -> ct_return(). stop_logging(Trail) -> stop_logging(Trail, default_config()). --spec stop_logging([string()], aws_config()) -> ct_return(). +-spec stop_logging(string(), aws_config()) -> ct_return(). stop_logging(Trail, Config) -> Json = [{<<"Name">>, list_to_binary(Trail)}], ct_request("StopLogging", Json, Config). @@ -191,11 +201,11 @@ request_impl(Method, Scheme, Host, Port, Path, Operation, Params, Body, #aws_con case erlcloud_aws:aws_request_form_raw( Method, Scheme, Host, Port, Path, Body, [{"content-type", "application/x-amz-json-1.1"} | Headers], - Config) of + [], Config) of {ok, RespBody} -> case Config#aws_config.cloudtrail_raw_result of true -> {ok, RespBody}; - _ -> {ok, jsx:decode(RespBody)} + _ -> {ok, jsx:decode(RespBody, [{return_maps, false}])} end; {error, Reason} -> {error, Reason} diff --git a/src/erlcloud_cloudwatch_logs.erl b/src/erlcloud_cloudwatch_logs.erl index c476c43c1..762422ac1 100644 --- a/src/erlcloud_cloudwatch_logs.erl +++ b/src/erlcloud_cloudwatch_logs.erl @@ -25,8 +25,13 @@ -type log_stream_name() :: string() | binary() | undefined. -type log_stream_prefix() :: string() | binary() | undefined. -type limit() :: pos_integer() | undefined. +-type filter_name_prefix() :: string() | binary() | undefined. +-type metric_name() :: string() | binary() | undefined. +-type metric_namespace() :: string() | binary() | undefined. -type log_stream_order() :: log_stream_name | last_event_time | undefined. -type events() :: [#{message => binary(), timestamp => pos_integer()}]. +-export_type([events/0]). +-type kms_key_id() :: string() | binary() | undefined. -type success_result_paged(ObjectType) :: {ok, [ObjectType], paging_token()}. @@ -36,10 +41,30 @@ -type log_group() :: jsx:json_term(). -type log_stream() :: jsx:json_term(). +-type metric_filters() :: jsx:json_term(). -type tag():: {binary(), binary()}. --type tags_return() :: jsx:json_term(). - +-type tags_return() :: {error, metadata_not_available + | container_credentials_unavailable + | erlcloud_aws:httpc_result_error()} + | {ok, jsx:json_term()}. + +-type query_status() :: cancelled + | complete + | failed + | running + | scheduled + | timeout + | unknown. +-export_type([query_status/0]). + +-type query_results() :: #{ results := [[#{ field := binary(), + value := binary() }]], + statistics := #{ bytes_scanned := float(), + records_matched := float(), + records_scanned := float() }, + status := query_status() }. +-export_type([query_results/0]). %% Library initialization -export([ @@ -52,6 +77,20 @@ %% CloudWatch API -export([ + create_log_group/1, + create_log_group/2, + create_log_group/3, + create_log_group/4, + + create_log_stream/2, + create_log_stream/3, + + delete_log_group/1, + delete_log_group/2, + + delete_log_stream/2, + delete_log_stream/3, + describe_log_groups/0, describe_log_groups/1, describe_log_groups/2, @@ -65,12 +104,30 @@ describe_log_streams/6, describe_log_streams/7, + describe_metric_filters/0, + describe_metric_filters/1, + describe_metric_filters/2, + describe_metric_filters/3, + describe_metric_filters/4, + describe_metric_filters/6, + describe_metric_filters/7, + + get_query_results/2, + get_query_results/3, + put_logs_events/4, put_logs_events/5, list_tags_log_group/1, list_tags_log_group/2, + start_query/4, + start_query/5, + start_query/6, + + stop_query/1, + stop_query/2, + tag_log_group/2, tag_log_group/3 @@ -115,6 +172,145 @@ new(AccessKeyID, SecretAccessKey, Host) -> %%============================================================================== +%%------------------------------------------------------------------------------ +%% @doc +%% +%% CreateLogGroup action +%% http://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_CreateLogGroup.html +%% +%% @end +%%------------------------------------------------------------------------------ +-spec create_log_group(log_group_name()) -> ok | error_result(). +create_log_group(LogGroupName) -> + create_log_group(LogGroupName, default_config()). + + +-spec create_log_group( + log_group_name(), + aws_config() +) -> ok | error_result(). +create_log_group(LogGroupName, Config) -> + create_log_group(LogGroupName, undefined, undefined, Config). + + +-spec create_log_group( + log_group_name(), + list(tag()), + aws_config() +) -> ok | error_result(). +create_log_group(LogGroupName, Tags, Config) when is_list(Tags) -> + create_log_group(LogGroupName, Tags, undefined, Config). + + +-spec create_log_group( + log_group_name(), + undefined | list(tag()), + undefined | kms_key_id(), + aws_config() +) -> ok | error_result(). +create_log_group(LogGroupName, Tags, KmsKeyId, Config) -> + case cw_request(Config, "CreateLogGroup", [ + {<<"logGroupName">>, LogGroupName}, + {<<"tags">>, Tags}, + {<<"kmsKeyId">>, KmsKeyId} + ]) + of + {ok, []} -> ok; + {error, _} = Error -> Error + end. + + +%%------------------------------------------------------------------------------ +%% @doc +%% +%% CreateLogStream action +%% http://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_CreateLogStream.html +%% +%% @end +%%------------------------------------------------------------------------------ +-spec create_log_stream( + log_group_name(), + log_stream_name() +) -> ok | error_result(). +create_log_stream(LogGroupName, LogStreamName) -> + create_log_stream(LogGroupName, LogStreamName, default_config()). + + +-spec create_log_stream( + log_group_name(), + log_stream_name(), + aws_config() +) -> ok | error_result(). +create_log_stream(LogGroupName, LogStreamName, Config) -> + case cw_request(Config, "CreateLogStream", [ + {<<"logGroupName">>, LogGroupName}, + {<<"logStreamName">>, LogStreamName} + ]) + of + {ok, []} -> ok; + {error, _} = Error -> Error + end. + + +%%------------------------------------------------------------------------------ +%% @doc +%% +%% DeleteLogGroup action +%% http://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_DeleteLogGroup.html +%% +%% @end +%%------------------------------------------------------------------------------ +-spec delete_log_group(log_group_name()) -> ok | error_result(). +delete_log_group(LogGroupName) -> + delete_log_group(LogGroupName, default_config()). + + +-spec delete_log_group( + log_group_name(), + aws_config() +) -> ok | error_result(). +delete_log_group(LogGroupName, Config) -> + case cw_request(Config, "DeleteLogGroup", [ + {<<"logGroupName">>, LogGroupName} + ]) + of + {ok, []} -> ok; + {error, _} = Error -> Error + end. + + +%%------------------------------------------------------------------------------ +%% @doc +%% +%% DeleteLogStream action +%% http://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_DeleteLogStream.html +%% +%% @end +%%------------------------------------------------------------------------------ +-spec delete_log_stream( + log_group_name(), + log_stream_name() +) -> ok | error_result(). +delete_log_stream(LogGroupName, LogStreamName) -> + delete_log_stream(LogGroupName, LogStreamName, default_config()). + + +-spec delete_log_stream( + log_group_name(), + log_stream_name(), + aws_config() +) -> ok | error_result(). +delete_log_stream(LogGroupName, LogStreamName, Config) -> + case cw_request(Config, "DeleteLogStream", [ + {<<"logGroupName">>, LogGroupName}, + {<<"logStreamName">>, LogStreamName} + ]) + of + {ok, []} -> ok; + {error, _} = Error -> Error + end. + + %%------------------------------------------------------------------------------ %% @doc %% @@ -161,14 +357,14 @@ describe_log_groups(LogGroupNamePrefix, Limit, Config) -> aws_config() ) -> result_paged(log_group()). describe_log_groups(LogGroupNamePrefix, Limit, Token, Config) -> - case + case cw_request(Config, "DescribeLogGroups", req_log_groups(LogGroupNamePrefix, Limit, Token) ) of {ok, Json} -> LogGroups = proplists:get_value(<<"logGroups">>, Json, []), - NextToken = proplists:get_value(<<"nextToken">>, Json, undefined), + NextToken = proplists:get_value(<<"nextToken">>, Json, undefined), {ok, LogGroups, NextToken}; {error, _} = Error -> Error @@ -248,7 +444,7 @@ describe_log_streams(LogGroupName, LogStreamPrefix, OrderBy, Desc, Limit, Token, of {ok, Json} -> LogStream = proplists:get_value(<<"logStreams">>, Json, []), - NextToken = proplists:get_value(<<"nextToken">>, Json, undefined), + NextToken = proplists:get_value(<<"nextToken">>, Json, undefined), {ok, LogStream, NextToken}; {error, _} = Error -> Error @@ -268,6 +464,86 @@ log_stream_order_by(undefined) -> <<"LogStreamName">>; log_stream_order_by(log_stream_name) -> <<"LogStreamName">>; log_stream_order_by(last_event_time) -> <<"LastEventTime">>. +%%------------------------------------------------------------------------------ +%% @doc +%% +%% GetQueryResults action +%% https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_GetQueryResults.html +%% +%% ===Example=== +%% +%% Returns the results from the specified query. +%% +%% ` +%% application:ensure_all_started(erlcloud). +%% {ok, Config} = erlcloud_aws:auto_config(). +%% {ok, Results} = erlcloud_cloudwatch_logs:get_query_results("12ab3456-12ab-123a-789e-1234567890ab", [], Config). +%% ` +%% +%% @end +%%------------------------------------------------------------------------------ + +-spec get_query_results(QueryId, Options) -> Results + when QueryId :: string(), + Options :: [{out, map}], + Results :: {ok, query_results() | AWSAPIReturn} | {error, erlcloud_aws:httpc_result_error()}, + AWSAPIReturn :: [{binary(), term()}]. % as per #API_GetQueryResults_ResponseSyntax +get_query_results(QueryId, Options) -> + get_query_results(QueryId, Options, default_config()). + +-spec get_query_results(QueryId, Options, Config) -> Results + when QueryId :: string(), + Options :: [{out, map}], + Config :: aws_config(), + Results :: {ok, query_results() | AWSAPIReturn} | {error, erlcloud_aws:httpc_result_error()}, + AWSAPIReturn :: [{binary(), term()}]. % as per #API_GetQueryResults_ResponseSyntax +get_query_results(QueryId, Options, Config) -> + Result0 = cw_request(Config, "GetQueryResults", [{<<"queryId">>, QueryId}]), + Out = proplists:get_value(out, Options, undefined), + case Result0 of + {error, _} = E -> E; + {ok, Result} when Out =:= map -> + Statistics = proplists:get_value(<<"statistics">>, Result), + {ok, #{ results => results_from_get_query_results(proplists:get_value(<<"results">>, Result)), + statistics => #{ bytes_scanned => proplists:get_value(<<"bytesScanned">>, Statistics), + records_matched => proplists:get_value(<<"recordsMatched">>, Statistics), + records_scanned => proplists:get_value(<<"recordsScanned">>, Statistics) + }, + status => status_from_get_query_results(proplists:get_value(<<"status">>, Result)) }}; + _ -> + Result0 + end. + +-spec results_from_get_query_results(ResultRows) -> Out + when ResultRows :: [[[{binary(), binary()}]]], + Out :: [[#{ field := binary(), + value := binary() }]]. +results_from_get_query_results([]) -> + []; +results_from_get_query_results(ResultRows) -> + [ [#{ field => proplists:get_value(<<"field">>, ResultField), + value => proplists:get_value(<<"value">>, ResultField) } + || ResultField <- ResultRow ] + || ResultRow <- ResultRows ]. + +-spec status_from_get_query_results(In) -> Out + when In :: binary(), + Out :: query_status(). +status_from_get_query_results(<<"Cancelled">>) -> + cancelled; +status_from_get_query_results(<<"Complete">>) -> + complete; +status_from_get_query_results(<<"Failed">>) -> + failed; +status_from_get_query_results(<<"Running">>) -> + running; +status_from_get_query_results(<<"Scheduled">>) -> + scheduled; +status_from_get_query_results(<<"Timeout">>) -> + timeout; +status_from_get_query_results(<<"Unknown">>) -> + unknown. + %%------------------------------------------------------------------------------ %% @doc %% @@ -275,14 +551,14 @@ log_stream_order_by(last_event_time) -> <<"LastEventTime">>. %% https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutLogEvents.html %% %% ===Example=== -%% +%% %% Put log events requires a Upload Sequence Token, it is available via DescribeLogStreams %% %% ` -%% application:ensure_all_started(erlcloud). +%% application:ensure_all_started(erlcloud). %% {ok, Config} = erlcloud_aws:auto_config(). %% {ok, Streams, _} = erlcloud_cloudwatch_logs:describe_log_streams(GroupName, StreamName, Config). -%% {_, Seq} = lists:keyfind(<<"uploadSequenceToken">>, 1, hd(Streams)). +%% {_, Seq} = lists:keyfind(<<"uploadSequenceToken">>, 1, hd(Streams)). %% %% Batch = [#{timestamp => 1526233086694, message => <<"Example Message">>}]. %% erlcloud_cloudwatch_logs:put_logs_events(GroupName, StreamName, Seq, Batch, Config). @@ -296,7 +572,7 @@ log_stream_order_by(last_event_time) -> <<"LastEventTime">>. log_stream_name(), seq_token(), events() -) -> datum:either( seq_token() ). +) -> {ok, seq_token()} | {error, erlcloud_aws:httpc_result_error()}. put_logs_events(LogGroup, LogStream, SeqToken, Events) -> put_logs_events(LogGroup, LogStream, SeqToken, Events, default_config()). @@ -308,7 +584,7 @@ put_logs_events(LogGroup, LogStream, SeqToken, Events) -> seq_token(), events(), aws_config() -) -> datum:either( seq_token() ). +) -> {ok, seq_token()} | {error, erlcloud_aws:httpc_result_error()}. put_logs_events(LogGroup, LogStream, SeqToken, Events, Config) -> case @@ -322,7 +598,7 @@ put_logs_events(LogGroup, LogStream, SeqToken, Events, Config) -> {error, _} = Error -> Error end. - + req_logs_events(LogGroup, LogStream, SeqToken, Events) -> [ {<<"logEvents">>, log_events(Events)}, @@ -335,6 +611,97 @@ log_events(Events) -> [maps:with([message, timestamp], X) || #{message := _, timestamp := _} = X <- Events]. +%%------------------------------------------------------------------------------ +%% @doc +%% +%% DescribeMetricFilters action +%% https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_DescribeMetricFilters.html +%% +%% @end +%%------------------------------------------------------------------------------ +-spec describe_metric_filters() -> result_paged(metric_filters()). +describe_metric_filters() -> + describe_metric_filters(default_config()). + + +-spec describe_metric_filters( + aws_config() | log_group_name() +) -> result_paged(metric_filters()). +describe_metric_filters(#aws_config{} = Config) -> + describe_metric_filters(undefined, Config); +describe_metric_filters(LogGroupName) -> + describe_metric_filters(LogGroupName, default_config()). + + +-spec describe_metric_filters( + log_group_name(), + aws_config() +) -> result_paged(metric_filters()). +describe_metric_filters(LogGroupName, Config) -> + describe_metric_filters(LogGroupName, ?DEFAULT_LIMIT, Config). + + +-spec describe_metric_filters( + log_group_name(), + limit(), + aws_config() +) -> result_paged(metric_filters()). +describe_metric_filters(LogGroupName, Limit, Config) -> + describe_metric_filters(LogGroupName, Limit, undefined, Config). + + +-spec describe_metric_filters( + log_group_name(), + limit(), + filter_name_prefix(), + aws_config() +) -> result_paged(metric_filters()). +describe_metric_filters(LogGroupName, Limit, FilterNamePrefix, Config) -> + describe_metric_filters(LogGroupName, Limit, FilterNamePrefix, undefined, + undefined, Config). + + +-spec describe_metric_filters( + log_group_name(), + limit(), + filter_name_prefix(), + metric_name(), + metric_namespace(), + aws_config() +) -> result_paged(metric_filters()). +describe_metric_filters(LogGroupName, Limit, FilterNamePrefix, MetricName, + MetricNamespace, Config) -> + describe_metric_filters(LogGroupName, Limit, FilterNamePrefix, MetricName, + MetricNamespace, undefined, Config). + + +-spec describe_metric_filters( + log_group_name(), + limit(), + filter_name_prefix(), + metric_name(), + metric_namespace(), + paging_token(), + aws_config() +) -> result_paged(metric_filters()). +describe_metric_filters(LogGroupName, Limit, FilterNamePrefix, MetricName, + MetricNamespace, PrevToken, Config) -> + case cw_request(Config, "DescribeMetricFilters", [ + {<<"logGroupName">>, LogGroupName}, + {<<"limit">>, Limit}, + {<<"filterNamePrefix">>, FilterNamePrefix}, + {<<"metricName">>, MetricName}, + {<<"metricNamespace">>, MetricNamespace}, + {<<"nextToken">>, PrevToken} + ]) of + {ok, Data} -> + MetricFilters = proplists:get_value(<<"metricFilters">>, Data, []), + NextToken = proplists:get_value(<<"nextToken">>, Data, undefined), + {ok, MetricFilters, NextToken}; + {error, Reason} -> + {error, Reason} + end. + %%------------------------------------------------------------------------------ %% @doc %% @@ -368,6 +735,105 @@ list_tags_log_group(LogGroup, Config) -> Error end. +%%------------------------------------------------------------------------------ +%% @doc +%% +%% StartQuery action +%% https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_StartQuery.html +%% +%% ===Example=== +%% +%% Schedules a query of log groups using CloudWatch Logs Insights. You specify the log groups +%% and time range to query, as well as the query string to use. +%% +%% ` +%% {ok, _} = application:ensure_all_started(erlcloud). +%% {ok, Config} = erlcloud_aws:auto_config(). +%% {ok, #{ query_id := QueryId }} = erlcloud_cloudwatch_logs:start_query(["LogGroupName1", "LogGroupName2", "LogGroupName3"], "stats count(*) by eventSource, eventName, awsRegion", 1546300800, 1546309800, 100). +%% ` +%% +%% @end +%%------------------------------------------------------------------------------ + +-spec start_query(LogGroupNames, QueryString, StartTime, EndTime) -> Result + when LogGroupNames :: [log_group_name()], + QueryString :: string(), + StartTime :: non_neg_integer(), + EndTime :: non_neg_integer(), + Result :: {ok, #{ query_id => string() }} | {error, erlcloud_aws:httpc_result_error()}. +start_query(LogGroupNames0, QueryString, StartTime, EndTime) -> + start_query(LogGroupNames0, QueryString, StartTime, EndTime, _Limit = 1000). + +-spec start_query(LogGroupNames, QueryString, StartTime, EndTime, Limit) -> Result + when LogGroupNames :: [log_group_name()], + QueryString :: string(), + StartTime :: non_neg_integer(), + EndTime :: non_neg_integer(), + Limit :: 1..10000, + Result :: {ok, #{ query_id => string() }} | {error, erlcloud_aws:httpc_result_error()}. +start_query(LogGroupNames0, QueryString, StartTime, EndTime, Limit) -> + start_query(LogGroupNames0, QueryString, StartTime, EndTime, Limit, default_config()). + +-spec start_query(LogGroupNames, QueryString, StartTime, EndTime, Limit, Config) -> Result + when LogGroupNames :: [log_group_name()], + QueryString :: string(), + StartTime :: non_neg_integer(), + EndTime :: non_neg_integer(), + Limit :: 1..10000, + Config :: aws_config(), + Result :: {ok, #{ query_id => string() }} | {error, erlcloud_aws:httpc_result_error()}. +start_query(LogGroupNames0, QueryString, StartTime, EndTime, Limit, Config) -> + LogGroupNames = case LogGroupNames0 of + [LogGroupName] -> + [{<<"logGroupName">>, LogGroupName}]; + _ -> + [{<<"logGroupNames">>, LogGroupNames0}] + end, + Result = cw_request(Config, "StartQuery", LogGroupNames ++ [{<<"queryString">>, QueryString}, + {<<"startTime">>, StartTime}, + {<<"endTime">>, EndTime}, + {<<"limit">>, Limit}]), + case Result of + {error, _} = E -> E; + {ok, OK} -> {ok, #{ query_id => binary_to_list(proplists:get_value(<<"queryId">>, OK)) }} + end. + +%%------------------------------------------------------------------------------ +%% @doc +%% +%% StopQuery action +%% https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_StopQuery.html +%% +%% ===Example=== +%% +%% Stops a CloudWatch Logs Insights query that is in progress. +%% +%% ` +%% {ok, _} = application:ensure_all_started(erlcloud). +%% {ok, Config} = erlcloud_aws:auto_config(). +%% {ok, QueryId} = erlcloud_cloudwatch_logs:stop_query("ecef5848-8aa7-4c12-9665-bafe422f3247"). +%% ` +%% +%% @end +%%------------------------------------------------------------------------------ + +-spec stop_query(QueryId) -> Result + when QueryId :: string(), + Result :: ok | {error, erlcloud_aws:httpc_result_error()}. +stop_query(QueryId) -> + stop_query(QueryId, default_config()). + +-spec stop_query(QueryId, Config) -> Result + when QueryId :: string(), + Config :: aws_config(), + Result :: ok | {error, erlcloud_aws:httpc_result_error()}. +stop_query(QueryId, Config) -> + Result = cw_request(Config, "StopQuery", [{<<"queryId">>, QueryId}]), + case Result of + {error, _} = E -> E; + {ok, _} -> ok + end. + %%------------------------------------------------------------------------------ %% @doc %% @@ -380,7 +846,9 @@ list_tags_log_group(LogGroup, Config) -> -spec tag_log_group( log_group_name(), list(tag()) -) -> ok. +) -> {ok, []} + | {ok, jsx:json_term()} + | {error, erlcloud_aws:httpc_result_error()}. tag_log_group(LogGroup, Tags) when is_list(Tags) -> tag_log_group(LogGroup, Tags, default_config()). @@ -390,7 +858,9 @@ tag_log_group(LogGroup, Tags) when is_list(Tags) -> log_group_name(), list(tag()), aws_config() -) -> ok. +) -> {ok, []} + | {ok, jsx:json_term()} + | {error, erlcloud_aws:httpc_result_error()}. tag_log_group(LogGroup, Tags, Config) when is_list(Tags) -> Params = [{<<"logGroupName">>, LogGroup}, @@ -421,6 +891,7 @@ maybe_cw_request({ok, Config}, Action, Params) -> "/", Request, make_request_headers(Config, Action, Request), + [], Config ) ); @@ -431,7 +902,7 @@ maybe_cw_request({error, _} = Error, _Action, _Params) -> maybe_json({ok, <<>>}) -> {ok, []}; maybe_json({ok, Response}) -> - {ok, jsx:decode(Response)}; + {ok, jsx:decode(Response, [{return_maps, false}])}; maybe_json({error, _} = Error) -> Error. diff --git a/src/erlcloud_cognito_user_pools.erl b/src/erlcloud_cognito_user_pools.erl new file mode 100644 index 000000000..11efb89b7 --- /dev/null +++ b/src/erlcloud_cognito_user_pools.erl @@ -0,0 +1,917 @@ +-module(erlcloud_cognito_user_pools). + +-include("erlcloud_aws.hrl"). + +-export([configure/2, configure/3, new/2, new/3]). + +-export([ + list_users/1, + list_users/2, + list_users/5, + list_users/6, + list_all_users/1, + list_all_users/2, + list_all_users/3, + + admin_list_groups_for_user/2, + admin_list_groups_for_user/3, + admin_list_groups_for_user/4, + admin_list_groups_for_user/5, + + admin_get_user/2, + admin_get_user/3, + + admin_create_user/2, + admin_create_user/3, + admin_create_user/4, + admin_delete_user/2, + admin_delete_user/3, + + admin_add_user_to_group/3, + admin_add_user_to_group/4, + + admin_remove_user_from_group/3, + admin_remove_user_from_group/4, + + create_group/2, + create_group/3, + create_group/5, + create_group/6, + + delete_group/2, + delete_group/3, + + admin_reset_user_password/2, + admin_reset_user_password/3, + admin_reset_user_password/4, + + admin_update_user_attributes/3, + admin_update_user_attributes/4, + admin_update_user_attributes/5, + + change_password/3, + change_password/4, + + list_user_pools/0, + list_user_pools/1, + list_user_pools/2, + list_all_user_pools/0, + list_all_user_pools/1, + + admin_set_user_password/3, + admin_set_user_password/4, + admin_set_user_password/5, + + describe_user_pool/1, + describe_user_pool/2, + + get_user_pool_mfa_config/1, + get_user_pool_mfa_config/2, + + list_identity_providers/1, + list_identity_providers/3, + list_identity_providers/4, + list_all_identity_providers/1, + list_all_identity_providers/2, + + describe_identity_provider/2, + describe_identity_provider/3, + + describe_user_pool_client/2, + describe_user_pool_client/3, + + list_user_pool_clients/1, + list_user_pool_clients/3, + list_user_pool_clients/4, + list_all_user_pool_clients/1, + list_all_user_pool_clients/2, + + admin_list_devices/2, + admin_list_devices/3, + admin_list_devices/5, + admin_list_all_devices/2, + admin_list_all_devices/3, + + admin_forget_device/3, + admin_forget_device/4, + + admin_confirm_signup/2, + admin_confirm_signup/3, + admin_confirm_signup/4, + + admin_initiate_auth/4, + admin_initiate_auth/5, + admin_initiate_auth/8, + + respond_to_auth_challenge/4, + respond_to_auth_challenge/5, + respond_to_auth_challenge/8, + + create_identity_provider/4, + create_identity_provider/5, + create_identity_provider/6, + create_identity_provider/7, + + delete_identity_provider/2, + delete_identity_provider/3, + + update_identity_provider/2, + update_identity_provider/3, + update_identity_provider/4, + update_identity_provider/5, + update_identity_provider/6, + + request/2, + request/3 +]). + +-define(MAX_RESULTS, 60). +-define(API_VERSION, "2016-04-18"). + +-spec new(string(), string()) -> aws_config(). +new(AccessKeyID, SecretAccessKey) -> + #aws_config{access_key_id = AccessKeyID, + secret_access_key = SecretAccessKey, + retry = fun erlcloud_retry:default_retry/1}. + +-spec new(string(), string(), string()) -> aws_config(). +new(AccessKeyID, SecretAccessKey, Host) -> + #aws_config{access_key_id = AccessKeyID, + secret_access_key = SecretAccessKey, + cognito_user_pools_host = Host, + retry = fun erlcloud_retry:default_retry/1}. + +-spec configure(string(), string()) -> ok. +configure(AccessKeyID, SecretAccessKey) -> + put(aws_config, new(AccessKeyID, SecretAccessKey)), + ok. + +-spec configure(string(), string(), string()) -> ok. +configure(AccessKeyID, SecretAccessKey, Host) -> + put(aws_config, new(AccessKeyID, SecretAccessKey, Host)), + ok. + +-spec list_users(binary()) -> {ok, map()} | {error, any()}. +list_users(UserPoolId) -> + list_users(UserPoolId, undefined, undefined, undefined, undefined). + +-spec list_users(binary(), aws_config()) -> {ok, map()} | {error, any()}. +list_users(UserPoolId, Config) -> + Body = #{ + <<"UserPoolId">> => unicode:characters_to_binary(UserPoolId) + }, + request(Config, "ListUsers", Body). + +-spec list_users(binary(), + [binary()] | undefined, + binary() | undefined, + number() | undefined, + binary() | undefined) -> {ok, map()} | {error, any()}. +list_users(UserPoolId, AttributesToGet, Filter, Limit, PaginationToken) -> + Config = erlcloud_aws:default_config(), + list_users(UserPoolId, AttributesToGet, Filter, Limit, PaginationToken, Config). + +list_users(UserPoolId, AttributesToGet, Filter, Limit, PaginationToken, Config) -> + BaseBody = #{ + <<"UserPoolId">> => UserPoolId, + <<"AttributesToGet">> => AttributesToGet, + <<"Filter">> => Filter, + <<"Limit">> => Limit, + <<"PaginationToken">> => PaginationToken + }, + Body = erlcloud_util:filter_undef(BaseBody), + request(Config, "ListUsers", Body). + +-spec list_all_users(binary()) -> {ok, map()} | {error, any()}. +list_all_users(UserPoolId) -> + list_all_users(UserPoolId, undefined). + +-spec list_all_users(binary(), binary() | undefined | aws_config()) -> + {ok, map()} | {error, any()}. +list_all_users(UserPoolId, Config) when is_record(Config, aws_config) -> + list_all_users(UserPoolId, undefined, Config); +list_all_users(UserPoolId, Filter) -> + Config = erlcloud_aws:default_config(), + list_all_users(UserPoolId, Filter, Config). + +-spec list_all_users(binary(), binary() | undefined, aws_config()) -> + {ok, map()} | {error, any()}. +list_all_users(UserPoolId, Filter, Config) -> + Fun = fun list_users/6, + Args = [UserPoolId, undefined, Filter], + list_all(Fun, Args, Config, <<"Users">>, <<"PaginationToken">>). + +-spec admin_list_groups_for_user(binary(), binary()) -> + {ok, map()} | {error, any()}. +admin_list_groups_for_user(UserName, UserPoolId) -> + Config = erlcloud_aws:default_config(), + admin_list_groups_for_user(UserName, UserPoolId, Config). + +-spec admin_list_groups_for_user(binary(), binary(), aws_config()) -> + {ok, map()} | {error, any()}. +admin_list_groups_for_user(UserName, UserPoolId, Config) -> + Body = #{ + <<"Username">> => UserName, + <<"UserPoolId">> => UserPoolId + }, + request(Config, "AdminListGroupsForUser", Body). + +-spec admin_list_groups_for_user(binary(), binary(), number(), + binary() | undefined) -> + {ok, map()} | {error, any()}. +admin_list_groups_for_user(UserName, UserPoolId, Limit, NextToken) -> + Config = erlcloud_aws:default_config(), + admin_list_groups_for_user(UserName, UserPoolId, Limit, NextToken, Config). + +-spec admin_list_groups_for_user(binary(), binary(), number(), + binary() | undefined, aws_config()) -> + {ok, map()} | {error, any()}. +admin_list_groups_for_user(UserName, UserPoolId, Limit, NextToken, Config) -> + Body = #{ + <<"Username">> => UserName, + <<"UserPoolId">> => UserPoolId, + <<"Limit">> => Limit, + <<"NextToken">> => NextToken + }, + request(Config, "AdminListGroupsForUser", Body). + +-spec admin_get_user(binary(), binary()) -> {ok, map()} | {error, any()}. +admin_get_user(UserName, UserPoolId) -> + Config = erlcloud_aws:default_config(), + admin_get_user(UserName, UserPoolId, Config). + +-spec admin_get_user(binary(), binary(), aws_config()) -> + {ok, map()} | {error, any()}. +admin_get_user(UserName, UserPoolId, Config) -> + Body = #{ + <<"Username">> => UserName, + <<"UserPoolId">> => UserPoolId + }, + request(Config, "AdminGetUser", Body). + +-spec admin_create_user(binary(), binary()) -> + {ok, map()} | {error, any()}. +admin_create_user(UserName, UserPoolId) -> + admin_create_user(UserName, UserPoolId, #{}). + +-spec admin_create_user(binary(), binary(), map()) -> + {ok, map()} | {error, any()}. +admin_create_user(UserName, UserPoolId, OptionalArgs) -> + Config = erlcloud_aws:default_config(), + admin_create_user(UserName, UserPoolId, OptionalArgs, Config). + +-spec admin_create_user(binary(), binary(), map(), aws_config()) -> + {ok, map()} | {error, any()}. +admin_create_user(UserName, UserPoolId, OptionalArgs, Config) -> + Body = OptionalArgs#{ + <<"Username">> => UserName, + <<"UserPoolId">> => UserPoolId + }, + request(Config, "AdminCreateUser", Body). + +-spec admin_delete_user(binary(), binary()) -> ok | {error, any()}. +admin_delete_user(UserName, UserPoolId) -> + Config = erlcloud_aws:default_config(), + admin_delete_user(UserName, UserPoolId, Config). + +-spec admin_delete_user(binary(), binary(), aws_config()) -> ok | {error, any()}. +admin_delete_user(UserName, UserPoolId, Config) -> + Body = #{ + <<"Username">> => UserName, + <<"UserPoolId">> => UserPoolId + }, + request_no_resp(Config, "AdminDeleteUser", Body). + +-spec admin_add_user_to_group(binary(), binary(), binary()) -> + ok | {error, any()}. +admin_add_user_to_group(GroupName, UserName, UserPoolId) -> + Config = erlcloud_aws:default_config(), + admin_add_user_to_group(GroupName, UserName, UserPoolId, Config). + +-spec admin_add_user_to_group(binary(), binary(), binary(), aws_config()) -> + ok | {error, any()}. +admin_add_user_to_group(GroupName, UserName, UserPoolId, Config) -> + Body = #{ + <<"Username">> => UserName, + <<"GroupName">> => GroupName, + <<"UserPoolId">> => UserPoolId + }, + request_no_resp(Config, "AdminAddUserToGroup", Body). + +-spec admin_remove_user_from_group(binary(), binary(), binary()) -> + ok | {error, any()}. +admin_remove_user_from_group(GroupName, UserName, UserPoolId) -> + Config = erlcloud_aws:default_config(), + admin_remove_user_from_group(GroupName, UserName, UserPoolId, Config). + +-spec admin_remove_user_from_group(binary(), binary(), binary(), aws_config()) -> + ok | {error, any()}. +admin_remove_user_from_group(GroupName, UserName, UserPoolId, Config) -> + Body = #{ + <<"Username">> => UserName, + <<"GroupName">> => GroupName, + <<"UserPoolId">> => UserPoolId + }, + request_no_resp(Config, "AdminRemoveUserFromGroup", Body). + +-spec create_group(binary(), binary()) -> {ok, map()} | {error, any()}. +create_group(GroupName, UserPoolId) -> + create_group(GroupName, UserPoolId, undefined, undefined, undefined). + +-spec create_group(binary(), binary(), aws_config()) -> + {ok, map()} | {error, any()}. +create_group(GroupName, UserPoolId, Config) -> + create_group(GroupName, UserPoolId, undefined, undefined, undefined, Config). + +-spec create_group(binary(), binary(), binary() | undefined, + number() | undefined, binary() | undefined) -> + {ok, map()} | {error, any()}. +create_group(GroupName, UserPoolId, Description, Precedence, RoleArn) -> + Config = erlcloud_aws:default_config(), + create_group(GroupName, UserPoolId, Description, Precedence, RoleArn, Config). + +-spec create_group(binary(), binary(), binary() | undefined, + number() | undefined, binary() | undefined, aws_config()) -> + {ok, map()} | {error, any()}. +create_group(GroupName, UserPoolId, Description, Precedence, RoleArn, Config) -> + Body0 = #{ + <<"GroupName">> => GroupName, + <<"UserPoolId">> => UserPoolId, + <<"Description">> => Description, + <<"Precedence">> => Precedence, + <<"RoleArn">> => RoleArn + }, + + Body = erlcloud_util:filter_undef(Body0), + request(Config, "CreateGroup", Body). + +-spec delete_group(binary(), binary()) -> ok | {error, any()}. +delete_group(GroupName, UserPoolId) -> + Config = erlcloud_aws:default_config(), + delete_group(GroupName, UserPoolId, Config). + +-spec delete_group(binary(), binary(), aws_config()) -> ok | {error, any()}. +delete_group(GroupName, UserPoolId, Config) -> + Body = #{ + <<"GroupName">> => unicode:characters_to_binary(GroupName), + <<"UserPoolId">> => unicode:characters_to_binary(UserPoolId) + }, + request_no_resp(Config, "DeleteGroup", Body). + +-spec admin_reset_user_password(binary(), binary()) -> + ok| {error, any()}. +admin_reset_user_password(UserName, UserPoolId) -> + admin_reset_user_password(UserName, UserPoolId, undefined). + +-spec admin_reset_user_password(binary(), binary(), map() | undefined) -> + ok | {error, any()}. +admin_reset_user_password(UserName, UserPoolId, MetaData) -> + Config = erlcloud_aws:default_config(), + admin_reset_user_password(UserName, UserPoolId, MetaData, Config). + +-spec admin_reset_user_password(binary(), binary(), + map() | undefined, aws_config()) -> + ok | {error, any()}. +admin_reset_user_password(UserName, UserPoolId, MetaData, Config) -> + BaseBody = #{ + <<"Username">> => UserName, + <<"UserPoolId">> => UserPoolId, + <<"ClientMetaData">> => MetaData + }, + Body = erlcloud_util:filter_undef(BaseBody), + request_no_resp(Config, "AdminResetUserPassword", Body). + +-spec admin_update_user_attributes(binary(), binary(), [map()]) -> + ok | {error, any()}. +admin_update_user_attributes(UserName, UserPoolId, Attributes) -> + admin_update_user_attributes(UserName, UserPoolId, Attributes, undefined). + +-spec admin_update_user_attributes(binary(), binary(), [map()], + map() | undefined) -> + ok | {error, any()}. +admin_update_user_attributes(UserName, UserPoolId, Attributes, MetaData) -> + Config = erlcloud_aws:default_config(), + admin_update_user_attributes(UserName, UserPoolId, Attributes, MetaData, Config). + +-spec admin_update_user_attributes(binary(), binary(), [map()], + map() | undefined, aws_config()) -> + ok | {error, any()}. +admin_update_user_attributes(UserName, UserPoolId, Attributes, MetaData, Config) -> + BaseBody = #{ + <<"Username">> => UserName, + <<"UserPoolId">> => UserPoolId, + <<"UserAttributes">> => Attributes, + <<"ClientMetaData">> => MetaData + }, + Body = erlcloud_util:filter_undef(BaseBody), + request_no_resp(Config, "AdminUpdateUserAttributes", Body). + +-spec change_password(binary(), binary(), binary()) -> + ok | {error, any()}. +change_password(OldPass, NewPass, AccessToken) -> + Config = erlcloud_aws:default_config(), + change_password(OldPass, NewPass, AccessToken, Config). + +-spec change_password(binary(), binary(), binary(), aws_config()) -> + ok | {error, any()}. +change_password(OldPass, NewPass, AccessToken, Config) -> + Body = #{ + <<"AccessToken">> => AccessToken, + <<"PreviousPassword">> => OldPass, + <<"ProposedPassword">> => NewPass + }, + request_no_resp(Config, "ChangePassword", Body). + +-spec list_user_pools() -> {ok, map()} | {error, any()}. +list_user_pools() -> + list_user_pools(?MAX_RESULTS, undefined). + +-spec list_user_pools(integer()) -> {ok, map()} | {error, any()}. +list_user_pools(MaxResult) -> + list_user_pools(MaxResult, undefined). + +-spec list_user_pools(integer(), binary() | undefined) -> + {ok, map()} | {error, any()}. +list_user_pools(MaxResult, NextToken) -> + Config = erlcloud_aws:default_config(), + list_user_pools(MaxResult, NextToken, Config). + +-spec list_user_pools(integer(), binary() | undefined, aws_config()) -> + {ok, map()} | {error, any()}. +list_user_pools(MaxResult, NextToken, Config) -> + Body0 = #{ + <<"MaxResults">> => MaxResult, + <<"NextToken">> => NextToken + }, + Body = erlcloud_util:filter_undef(Body0), + request(Config, "ListUserPools", Body). + +-spec list_all_user_pools() -> {ok, map()} | {error, any()}. +list_all_user_pools() -> + Config = erlcloud_aws:default_config(), + list_all_user_pools(Config). + +-spec list_all_user_pools(aws_config()) -> {ok, map()} | {error, any()}. +list_all_user_pools(Config) -> + Fun = fun list_user_pools/3, + list_all(Fun, [], Config, <<"UserPools">>, <<"NextToken">>). + +-spec admin_set_user_password(binary(), binary(), binary()) -> + {ok, map()} | {error, any()}. +admin_set_user_password(UserId, UserPoolId, Password) -> + admin_set_user_password(UserId, UserPoolId, Password, false). + +-spec admin_set_user_password(binary(), binary(), binary(), boolean()) -> + ok | {error, any()}. +admin_set_user_password(UserId, UserPoolId, Password, Permanent) -> + Config = erlcloud_aws:default_config(), + admin_set_user_password(UserId, UserPoolId, Password, Permanent, Config). + +-spec admin_set_user_password(binary(), binary(), binary(), boolean(), + aws_config()) -> + ok | {error, any()}. +admin_set_user_password(UserId, UserPoolId, Password, Permanent, Config) -> + Body = #{ + <<"Password">> => Password, + <<"Username">> => UserId, + <<"UserPoolId">> => UserPoolId, + <<"Permanent">> => Permanent + }, + request_no_resp(Config, "AdminSetUserPassword", Body). + +-spec describe_user_pool(binary()) -> {ok, map()} | {error, any()}. +describe_user_pool(UserPoolId) -> + Config = erlcloud_aws:default_config(), + describe_user_pool(UserPoolId, Config). + +-spec describe_user_pool(binary(), aws_config()) -> {ok, map()} | {error, any()}. +describe_user_pool(UserPoolId, Config) -> + Body = #{ + <<"UserPoolId">> => UserPoolId + }, + request(Config, "DescribeUserPool", Body). + +-spec get_user_pool_mfa_config(binary()) -> {ok, map()} | {error, any()}. +get_user_pool_mfa_config(UserPoolId) -> + Config = erlcloud_aws:default_config(), + get_user_pool_mfa_config(UserPoolId, Config). + +-spec get_user_pool_mfa_config(binary(), aws_config()) -> + {ok, map()} | {error, any()}. +get_user_pool_mfa_config(UserPoolId, Config) -> + Body = #{ + <<"UserPoolId">> => UserPoolId + }, + request(Config, "GetUserPoolMfaConfig", Body). + +-spec list_identity_providers(binary()) -> {ok, map()} | {error, any()}. +list_identity_providers(UserPoolId) -> + list_identity_providers(UserPoolId, ?MAX_RESULTS, undefined). + +-spec list_identity_providers(binary(), integer(), binary() | undefined) -> + {ok, map()} | {error, any()}. +list_identity_providers(UserPoolId, MaxResults, NextToken) -> + Config = erlcloud_aws:default_config(), + list_identity_providers(UserPoolId, MaxResults, NextToken, Config). + +-spec list_identity_providers(binary(), + integer(), + binary() | undefined, + aws_config()) -> + {ok, map()} | {error, any()}. +list_identity_providers(UserPoolId, MaxResults, NextToken, Config) -> + Body0 = #{ + <<"UserPoolId">> => UserPoolId, + <<"NextToken">> => NextToken, + <<"MaxResults">> => MaxResults + }, + Body = erlcloud_util:filter_undef(Body0), + request(Config, "ListIdentityProviders", Body). + +-spec list_all_identity_providers(binary()) -> + {ok, map()} | {error, any()}. +list_all_identity_providers(UserPoolId) -> + Config = erlcloud_aws:default_config(), + list_all_identity_providers(UserPoolId, Config). + +-spec list_all_identity_providers(binary(), aws_config()) -> + {ok, map()} | {error, any()}. +list_all_identity_providers(UserPoolId, Config) -> + Fun = fun list_identity_providers/4, + Args = [UserPoolId], + list_all(Fun, Args, Config, <<"Providers">>, <<"NextToken">>). + +-spec describe_identity_provider(binary(), binary()) -> + {ok, map()} | {error, any()}. +describe_identity_provider(UserPoolId, ProviderName) -> + Config = erlcloud_aws:default_config(), + describe_identity_provider(UserPoolId, ProviderName, Config). + +-spec describe_identity_provider(binary(), binary(), aws_config()) -> + {ok, map()} | {error, any()}. +describe_identity_provider(UserPoolId, ProviderName, Config) -> + Body = #{ + <<"ProviderName">> => ProviderName, + <<"UserPoolId">> => UserPoolId + }, + request(Config, "DescribeIdentityProvider", Body). + +-spec describe_user_pool_client(binary(), binary()) -> + {ok, map()} | {error, any()}. +describe_user_pool_client(UserPoolId, ClientId) -> + Config = erlcloud_aws:default_config(), + describe_user_pool_client(UserPoolId, ClientId, Config). + +describe_user_pool_client(UserPoolId, ClientId, Config) -> + Body = #{ + <<"ClientId">> => ClientId, + <<"UserPoolId">> => UserPoolId + }, + request(Config, "DescribeUserPoolClient", Body). + +-spec list_user_pool_clients(binary()) -> {ok, map()} | {error, any()}. +list_user_pool_clients(UserPoolId) -> + list_user_pool_clients(UserPoolId, ?MAX_RESULTS, undefined). + +-spec list_user_pool_clients(binary(), non_neg_integer(), binary() | undefined) -> + {ok, map()} | {error, any()}. +list_user_pool_clients(UserPoolId, MaxResults, NextToken) -> + Config = erlcloud_aws:default_config(), + list_user_pool_clients(UserPoolId, MaxResults, NextToken, Config). + +-spec list_user_pool_clients(binary(), non_neg_integer(), binary() | undefined, + aws_config()) -> + {ok, map()} | {error, any()}. +list_user_pool_clients(UserPoolId, MaxResults, NextToken, Config) -> + Body0 = #{ + <<"UserPoolId">> => UserPoolId, + <<"NextToken">> => NextToken, + <<"MaxResults">> => MaxResults + }, + Body = erlcloud_util:filter_undef(Body0), + request(Config, "ListUserPoolClients", Body). + +-spec list_all_user_pool_clients(binary()) -> + {ok, map()} | {error, any()}. +list_all_user_pool_clients(UserPoolId) -> + Config = erlcloud_aws:default_config(), + list_all_user_pool_clients(UserPoolId, Config). + +-spec list_all_user_pool_clients(binary(), aws_config()) -> + {ok, map()} | {error, any()}. +list_all_user_pool_clients(UserPoolId, Config) -> + Fun = fun list_user_pool_clients/4, + Args = [UserPoolId], + list_all(Fun, Args, Config, <<"UserPoolClients">>, <<"NextToken">>). + +-spec admin_list_devices(binary(), binary()) -> {ok, map()} | {error, any()}. +admin_list_devices(UserPoolId, Username) -> + Config = erlcloud_aws:default_config(), + admin_list_devices(UserPoolId, Username, Config). + +-spec admin_list_devices(binary(), binary(), aws_config()) -> + {ok, map()} | {error, any()}. +admin_list_devices(UserPoolId, Username, Config) -> + admin_list_devices(UserPoolId, Username, ?MAX_RESULTS, undefined, Config). + +-spec admin_list_devices(binary(), binary(), integer(), binary() | undefined, + aws_config()) -> + {ok, map()} | {error, any()}. +admin_list_devices(UserPoolId, Username, Limit, PaginationToken, Config) -> + Body0 = #{ + <<"UserPoolId">> => UserPoolId, + <<"Username">> => Username, + <<"Limit">> => Limit, + <<"PaginationToken">> => PaginationToken + }, + Body = erlcloud_util:filter_undef(Body0), + request(Config, "AdminListDevices", Body). + +-spec admin_list_all_devices(binary(), binary()) -> + {ok, map()} | {error, any()}. +admin_list_all_devices(UserPoolId, Username) -> + Config = erlcloud_aws:default_config(), + admin_list_all_devices(UserPoolId, Username, Config). + +-spec admin_list_all_devices(binary(), binary(), aws_config()) -> + {ok, map()} | {error, any()}. +admin_list_all_devices(UserPoolId, Username, Config) -> + Fun = fun admin_list_devices/5, + Args = [UserPoolId, Username], + list_all(Fun, Args, Config, <<"Devices">>, <<"PaginationToken">>). + +-spec admin_forget_device(binary(), binary(), binary()) -> + ok | {error, any()}. +admin_forget_device(UserPoolId, Username, DeviceKey) -> + Config = erlcloud_aws:default_config(), + admin_forget_device(UserPoolId, Username, DeviceKey, Config). + +-spec admin_forget_device(binary(), binary(), binary(), aws_config()) -> + ok | {error, any()}. +admin_forget_device(UserPoolId, Username, DeviceKey, Config) -> + Body = #{ + <<"UserPoolId">> => UserPoolId, + <<"Username">> => Username, + <<"DeviceKey">> => DeviceKey + }, + request_no_resp(Config, "AdminForgetDevice", Body). + +-spec admin_confirm_signup(binary(), binary()) -> + {ok, map()} | {error, any()}. +admin_confirm_signup(UserPoolId, Username) -> + admin_confirm_signup(UserPoolId, Username, #{}). + +-spec admin_confirm_signup(binary(), binary(), map()) -> + {ok, map()} | {error, any()}. +admin_confirm_signup(UserPoolId, Username, ClientMetadata) -> + Config = erlcloud_aws:default_config(), + admin_confirm_signup(UserPoolId, Username, ClientMetadata, Config). + +-spec admin_confirm_signup(binary(), binary(), map(), aws_config()) -> + {ok, map()} | {error, any()}. +admin_confirm_signup(UserPoolId, Username, ClientMetadata, Config) -> + Body = #{ + <<"UserPoolId">> => UserPoolId, + <<"Username">> => Username, + <<"ClientMetadata">> => ClientMetadata + }, + request(Config, "AdminConfirmSignUp", Body). + +-spec admin_initiate_auth(binary(), binary(), binary(), map()) -> + {ok, map()} | {error, any()}. +admin_initiate_auth(PoolId, ClientId, AuthFlow, AuthParams) -> + Cfg = erlcloud_aws:default_config(), + admin_initiate_auth(PoolId, ClientId, AuthFlow, AuthParams, Cfg). + +-spec admin_initiate_auth(binary(), binary(), binary(), + map(), aws_config()) -> + {ok, map()} | {error, any()}. +admin_initiate_auth(PoolId, ClientId, AuthFlow, AuthParams, Cfg) -> + admin_initiate_auth(PoolId, ClientId, AuthFlow, AuthParams, + #{}, #{}, #{}, Cfg). + +-spec admin_initiate_auth(binary(), binary(), binary(), map(), + map(), map(), map(), aws_config()) -> + {ok, map()} | {error, any()}. +admin_initiate_auth(PoolId, ClientId, AuthFlow, AuthParams, + AnalyticsMeta, ClientMeta, ContextData, Cfg) -> + Mandatory = #{ + <<"AuthFlow">> => AuthFlow, + <<"ClientId">> => ClientId, + <<"UserPoolId">> => PoolId + }, + Optional = #{ + <<"AnalyticsMetadata">> => AnalyticsMeta, + <<"AuthParameters">> => AuthParams, + <<"ClientMetadata">> => ClientMeta, + <<"ContextData">> => ContextData + }, + request(Cfg, "AdminInitiateAuth", make_request_body(Mandatory, Optional)). + +-spec respond_to_auth_challenge(binary(), binary(), map(), binary()) -> + {ok, map()} | {error, any()}. +respond_to_auth_challenge(ClientId, ChallengeName, ChallengeResponses, Session) -> + Cfg = erlcloud_aws:default_config(), + respond_to_auth_challenge(ClientId, ChallengeName, ChallengeResponses, + Session, Cfg). + +-spec respond_to_auth_challenge(binary(), binary(), map(), binary(), + aws_config()) -> + {ok, map()} | {error, any()}. +respond_to_auth_challenge(ClientId, ChallengeName, ChallengeResponses, + Session, Cfg) -> + respond_to_auth_challenge(ClientId, ChallengeName, ChallengeResponses, + Session, #{}, #{}, #{}, Cfg). + +-spec respond_to_auth_challenge(binary(), binary(), map(), binary(), + map(), map(), map(), + aws_config()) -> + {ok, map()} | {error, any()}. +respond_to_auth_challenge(ClientId, ChallengeName, ChallengeResponses, + Session, AnalyticsMeta, ClientMeta, ContextData, Cfg) -> + Mandatory = #{ + <<"ChallengeName">> => ChallengeName, + <<"ChallengeResponses">> => ChallengeResponses, + <<"ClientId">> => ClientId + }, + Optional = #{ + <<"AnalyticsMetadata">> => AnalyticsMeta, + <<"ClientMetadata">> => ClientMeta, + <<"Session">> => Session, + <<"UserContextData">> => ContextData + }, + request(Cfg, "RespondToAuthChallenge", make_request_body(Mandatory, Optional)). + +-spec create_identity_provider(binary(), binary(), binary(), map()) -> + {ok, map()} | {error, any()}. +create_identity_provider(UserPoolId, ProviderName, ProviderType, + ProviderDetails) -> + create_identity_provider(UserPoolId, ProviderName, ProviderType, + ProviderDetails, #{}). + +-spec create_identity_provider(binary(), binary(), binary(), map(), map()) -> + {ok, map()} | {error, any()}. +create_identity_provider(UserPoolId, ProviderName, ProviderType, + ProviderDetails, AttributeMapping) -> + create_identity_provider(UserPoolId, ProviderName, ProviderType, + ProviderDetails, AttributeMapping, []). + +-spec create_identity_provider(binary(), binary(), binary(), + map(), map(), list()) -> + {ok, map()} | {error, any()}. +create_identity_provider(UserPoolId, ProviderName, ProviderType, + ProviderDetails, AttributeMapping, IdpIdentifiers) -> + Config = erlcloud_aws:default_config(), + create_identity_provider(UserPoolId, ProviderName, ProviderType, + ProviderDetails, AttributeMapping, IdpIdentifiers, Config). + +-spec create_identity_provider(binary(), binary(), binary(), map(), map(), + list(), aws_config()) -> + {ok, map()} | {error, any()}. +create_identity_provider(UserPoolId, ProviderName, ProviderType, ProviderDetails, + AttributeMapping, IdpIdentifiers, Config) -> + Mandatory = #{ + <<"UserPoolId">> => UserPoolId, + <<"ProviderName">> => ProviderName, + <<"ProviderType">> => ProviderType, + <<"ProviderDetails">> => ProviderDetails + }, + Optional = #{ + <<"AttributeMapping">> => AttributeMapping, + <<"IdpIdentifiers">> => IdpIdentifiers + }, + request(Config, "CreateIdentityProvider", make_request_body(Mandatory, Optional)). + +-spec delete_identity_provider(binary(), binary()) -> + ok | {error, any()}. +delete_identity_provider(UserPoolId, ProviderName) -> + Config = erlcloud_aws:default_config(), + delete_identity_provider(UserPoolId, ProviderName, Config). + +-spec delete_identity_provider(binary(), binary(), aws_config()) -> + ok | {error, any()}. +delete_identity_provider(UserPoolId, ProviderName, Config) -> + Body = #{ + <<"UserPoolId">> => UserPoolId, + <<"ProviderName">> => ProviderName + }, + request_no_resp(Config, "DeleteIdentityProvider", Body). + +-spec update_identity_provider(binary(), binary()) -> + {ok, map()} | {error, any()}. +update_identity_provider(UserPoolId, ProviderName) -> + update_identity_provider(UserPoolId, ProviderName, #{}). + +-spec update_identity_provider(binary(), binary(), map()) -> + {ok, map()} | {error, any()}. +update_identity_provider(UserPoolId, ProviderName, ProviderDetails) -> + update_identity_provider(UserPoolId, ProviderName, ProviderDetails, #{}). + +-spec update_identity_provider(binary(), binary(), map(), map()) -> + {ok, map()} | {error, any()}. +update_identity_provider(UserPoolId, ProviderName, + ProviderDetails, AttributeMapping) -> + update_identity_provider(UserPoolId, ProviderName, + ProviderDetails, AttributeMapping, []). + +-spec update_identity_provider(binary(), binary(), map(), map(), list()) -> + {ok, map()} | {error, any()}. +update_identity_provider(UserPoolId, ProviderName, + ProviderDetails, AttributeMapping, IdpIdentifiers) -> + Config = erlcloud_aws:default_config(), + update_identity_provider(UserPoolId, ProviderName, ProviderDetails, + AttributeMapping, IdpIdentifiers, Config). + +-spec update_identity_provider(binary(), binary(), map(), map(), + list(), aws_config()) -> + {ok, map()} | {error, any()}. +update_identity_provider(UserPoolId, ProviderName, ProviderDetails, + AttributeMapping, IdpIdentifiers, Config) -> + Mandatory = #{ + <<"UserPoolId">> => UserPoolId, + <<"ProviderName">> => ProviderName + }, + Optional = #{ + <<"ProviderDetails">> => ProviderDetails, + <<"AttributeMapping">> => AttributeMapping, + <<"IdpIdentifiers">> => IdpIdentifiers + }, + request(Config, "UpdateIdentityProvider", make_request_body(Mandatory, Optional)). + +%%------------------------------------------------------------------------------ +%% Internal Functions +%%------------------------------------------------------------------------------ +request(Config, Request) -> + Result = erlcloud_retry:request(Config, Request, fun handle_result/1), + case erlcloud_aws:request_to_return(Result) of + {ok, {_, <<>>}} -> {ok, #{}}; + {ok, {_, RespBody}} -> {ok, jsx:decode(RespBody, [return_maps])}; + {error, _} = Error -> Error + end. + +request(Config0, OperationName, Request) -> + case erlcloud_aws:update_config(Config0) of + {ok, Config} -> + Body = jsx:encode(Request), + Operation = "AWSCognitoIdentityProviderService." ++ OperationName, + Headers = get_headers(Config, Operation, Body), + AwsRequest = #aws_request{service = 'cognito-idp', + uri = get_url(Config), + method = post, + request_headers = Headers, + request_body = Body}, + request(Config, AwsRequest); + {error, Reason} -> + {error, Reason} + end. + +request_no_resp(Config, OperationName, Request) -> + case request(Config, OperationName, Request) of + {ok, _} -> ok; + Error -> Error + end. + +make_request_body(Mandatory, Optional) -> + maps:merge(Mandatory, erlcloud_util:filter_empty_map(Optional)). + +get_headers(#aws_config{cognito_user_pools_host = Host} = Config, Operation, Body) -> + Headers = [{"host", Host}, + {"x-amz-target", Operation}, + {"version", ?API_VERSION}, + {"content-type", "application/x-amz-json-1.1"}], + Region = erlcloud_aws:aws_region_from_host(Host), + erlcloud_aws:sign_v4_headers(Config, Headers, Body, Region, "cognito-idp"). + +handle_result(#aws_request{response_type = ok} = Request) -> + Request; +handle_result(#aws_request{response_type = error, + error_type = aws, + response_status = Status} = Request) + when Status >= 500 -> + Request#aws_request{should_retry = true}; +handle_result(#aws_request{response_type = error, + error_type = aws} = Request) -> + Request#aws_request{should_retry = false}. + +get_url(#aws_config{cognito_user_pools_scheme = Scheme, + cognito_user_pools_host = Host}) -> + Scheme ++ Host. + +list_all(Fun, Args, Config, Key, TokenAlias) -> + list_all(Fun, Args, Config, Key, TokenAlias, undefined, []). + +list_all(Fun, Args, Config, Key, TokenAlias, NextToken, Acc) -> + UpdArgs = Args ++ [?MAX_RESULTS, NextToken, Config], + case erlang:apply(Fun, UpdArgs) of + {ok, Map} -> + UpdAcc = Acc ++ maps:get(Key, Map), + NewToken = maps:get(TokenAlias, Map, undefined), + case NewToken of + undefined -> + {ok, #{Key => UpdAcc}}; + _ -> + list_all(Fun, Args, Config, Key, TokenAlias, NewToken, UpdAcc) + end; + Error -> + Error + end. diff --git a/src/erlcloud_ddb.erl b/src/erlcloud_ddb.erl index 46590c89c..f1d095069 100644 --- a/src/erlcloud_ddb.erl +++ b/src/erlcloud_ddb.erl @@ -664,7 +664,7 @@ batch_get_item_record() -> end} ]}. --type batch_get_item_return() :: ddb_return(#ddb_batch_get_item{}, [erlcloud_ddb:out_item()]). +-type batch_get_item_return() :: ddb_return(#ddb_batch_get_item{}, [out_item()]). -spec batch_get_item(batch_get_item_request_items()) -> batch_get_item_return(). batch_get_item(RequestItems) -> diff --git a/src/erlcloud_ddb2.erl b/src/erlcloud_ddb2.erl index c8f6d75a4..13ac0b528 100644 --- a/src/erlcloud_ddb2.erl +++ b/src/erlcloud_ddb2.erl @@ -61,12 +61,18 @@ %% contain typed attribute values so that they may be correctly passed %% to subsequent calls. %% -%% DynamoDB errors are return in the form `{error, {ErrorCode, -%% Message}}' where `ErrorCode' and 'Message' are both binary +%% DynamoDB errors for most cases are returned in the form +%% `{error, {ErrorCode, Message}}' where `ErrorCode' and `Message' are both binary %% strings. List of error codes: %% [http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ErrorHandling.html]. So %% to handle conditional check failures, match `{error, %% {<<"ConditionalCheckFailedException">>, _}}'. +%% Note that in the case of a `TransactionCanceledException' DynamoDB error, +%% the error response has the form `{error, {<<"TransactionCanceledException">>, +%% {Message, CancellationReasons}}}' where `Message' is a binary string and +%% `CancellationReasons' is an ordered list in the form `[{Code, Message}]', +%% where `Code' is the status code of the result and `Message' is the cancellation +%% reason message description. %% %% `erlcloud_ddb_util' provides a higher level API that implements common %% operations that may require multiple DynamoDB API calls. @@ -91,15 +97,17 @@ batch_write_item/1, batch_write_item/2, batch_write_item/3, create_backup/2, create_backup/3, create_backup/4, create_global_table/2, create_global_table/3, create_global_table/4, - create_table/5, create_table/6, create_table/7, + create_table/4, create_table/5, create_table/6, create_table/7, delete_backup/1, delete_backup/2, delete_backup/3, delete_item/2, delete_item/3, delete_item/4, delete_table/1, delete_table/2, delete_table/3, describe_backup/1, describe_backup/2, describe_backup/3, describe_continuous_backups/1, describe_continuous_backups/2, describe_continuous_backups/3, describe_global_table/1, describe_global_table/2, describe_global_table/3, + describe_global_table_settings/1, describe_global_table_settings/2, describe_global_table_settings/3, describe_limits/0, describe_limits/1, describe_limits/2, describe_table/1, describe_table/2, describe_table/3, + describe_table_replica_auto_scaling/1, describe_table_replica_auto_scaling/2, describe_table_replica_auto_scaling/3, describe_time_to_live/1, describe_time_to_live/2, describe_time_to_live/3, get_item/2, get_item/3, get_item/4, list_backups/0, list_backups/1, list_backups/2, @@ -113,11 +121,15 @@ restore_table_to_point_in_time/2, restore_table_to_point_in_time/3, restore_table_to_point_in_time/4, scan/1, scan/2, scan/3, tag_resource/2, tag_resource/3, + transact_get_items/1, transact_get_items/2, transact_get_items/3, + transact_write_items/1, transact_write_items/2, transact_write_items/3, untag_resource/2, untag_resource/3, update_continuous_backups/2, update_continuous_backups/3, update_continuous_backups/4, update_item/3, update_item/4, update_item/5, update_global_table/2, update_global_table/3, update_global_table/4, + update_global_table_settings/2, update_global_table_settings/3, update_table/2, update_table/3, update_table/4, update_table/5, + update_table_replica_auto_scaling/2, update_table_replica_auto_scaling/3, update_time_to_live/2, update_time_to_live/3, update_time_to_live/4 ]). @@ -140,6 +152,7 @@ batch_write_item_request_item/0, batch_write_item_return/0, boolean_opt/1, + billing_mode/0, comparison_op/0, condition/0, conditional_op/0, @@ -155,7 +168,11 @@ delete_item_opts/0, delete_item_return/0, delete_table_return/0, + deletion_protection_enabled/0, + describe_global_table_return/0, + describe_global_table_settings_return/0, describe_table_return/0, + describe_table_replica_auto_scaling_return/0, describe_time_to_live_return/0, expected_opt/0, expression/0, @@ -211,6 +228,7 @@ sse_description/0, sse_specification/0, stream_specification/0, + stream_view_type/0, select/0, table_name/0, tag_key/0, @@ -222,7 +240,10 @@ update_item_opt/0, update_item_opts/0, update_item_return/0, + update_global_table_return/0, + update_global_table_settings_return/0, update_table_return/0, + update_table_replica_auto_scaling_return/0, update_time_to_live_return/0, write_units/0 ]). @@ -323,12 +344,15 @@ default_config() -> erlcloud_aws:default_config(). -type read_units() :: pos_integer(). -type write_units() :: pos_integer(). +-type billing_mode() :: provisioned | pay_per_request. + -type index_name() :: binary(). -type projection() :: keys_only | {include, [attr_name()]} | all. --type global_secondary_index_def() :: {index_name(), key_schema(), projection(), read_units(), write_units()}. +-type global_secondary_index_def() :: {index_name(), key_schema(), projection()} | + {index_name(), key_schema(), projection(), read_units(), write_units()}. -type sse_description_status() :: enabling | enabled | disabling | disabled. -type sse_description() :: {status, sse_description_status()}. @@ -359,6 +383,8 @@ default_config() -> erlcloud_aws:default_config(). {attr_name(), [in_attr_value(),...], in}. -type conditions() :: maybe_list(condition()). +-type deletion_protection_enabled() :: boolean_opt(deletion_protection_enabled). + -type select() :: all_attributes | all_projected_attributes | count | specific_attributes. -type return_consumed_capacity() :: none | total | indexes. @@ -374,6 +400,25 @@ default_config() -> erlcloud_aws:default_config(). -type out_attr() :: {attr_name(), out_attr_value()}. -type out_item() :: [out_attr() | in_attr()]. % in_attr in the case of typed_record -type ok_return(T) :: {ok, T} | {error, term()}. +-type client_request_token() :: binary(). + +-type auto_scaling_target_tracking_scaling_policy_configuration_update_opt() :: {disable_scale_in, boolean()}| + {scale_in_cooldown, pos_integer()}| + {scale_out_cooldown, pos_integer()}| + {target_value, number()}. +-type auto_scaling_target_tracking_scaling_policy_configuration_update_opts() :: [auto_scaling_target_tracking_scaling_policy_configuration_update_opt()]. + +-type auto_scaling_policy_opt() :: {policy_name, binary()}| + {target_tracking_scaling_policy_configuration, auto_scaling_target_tracking_scaling_policy_configuration_update_opts()}. + +-type auto_scaling_policy_opts() :: [auto_scaling_policy_opt()]. + +-type auto_scaling_settings_update_opt() :: {auto_scaling_disabled, boolean()}| + {auto_scaling_role_arn, binary()}| + {maximum_units, non_neg_integer()}| + {minimum_units, non_neg_integer()}| + {scaling_policy_update, auto_scaling_policy_opts()}. +-type auto_scaling_settings_update_opts() :: [auto_scaling_settings_update_opt()]. %%%------------------------------------------------------------------------------ %%% Shared Dynamizers @@ -490,6 +535,10 @@ dynamize_provisioned_throughput({ReadUnits, WriteUnits}) -> {<<"WriteCapacityUnits">>, WriteUnits}]. -spec dynamize_global_secondary_index(global_secondary_index_def()) -> jsx:json_term(). +dynamize_global_secondary_index({IndexName, KeySchema, Projection}) -> + [{<<"IndexName">>, IndexName}, + {<<"KeySchema">>, dynamize_key_schema(KeySchema)}, + {<<"Projection">>, dynamize_projection(Projection)}]; dynamize_global_secondary_index({IndexName, KeySchema, Projection, ReadUnits, WriteUnits}) -> [{<<"IndexName">>, IndexName}, {<<"KeySchema">>, dynamize_key_schema(KeySchema)}, @@ -625,6 +674,46 @@ dynamize_return_item_collection_metrics(none) -> dynamize_return_item_collection_metrics(size) -> <<"SIZE">>. +-spec dynamize_auto_scaling_target_tracking_scaling_policy_configuration_update_opt(auto_scaling_target_tracking_scaling_policy_configuration_update_opt()) -> json_pair(). +dynamize_auto_scaling_target_tracking_scaling_policy_configuration_update_opt({target_value, TargetValue}) -> + {<<"TargetValue">>, TargetValue}; +dynamize_auto_scaling_target_tracking_scaling_policy_configuration_update_opt({disable_scale_in, DisableScaleIn}) -> + {<<"DisableScaleIn">>, DisableScaleIn}; +dynamize_auto_scaling_target_tracking_scaling_policy_configuration_update_opt({scale_in_cooldown, ScaleInCooldown}) -> + {<<"ScaleInCooldown">>, ScaleInCooldown}; +dynamize_auto_scaling_target_tracking_scaling_policy_configuration_update_opt({scale_out_cooldown, ScaleOutCooldown}) -> + {<<"ScaleOutCooldown">>, ScaleOutCooldown}. + +-spec dynamize_auto_scaling_target_tracking_scaling_policy_configuration_update_opts(auto_scaling_policy_opts()) -> jsx:json_term(). +dynamize_auto_scaling_target_tracking_scaling_policy_configuration_update_opts(Opts) -> + dynamize_maybe_list(fun dynamize_auto_scaling_target_tracking_scaling_policy_configuration_update_opt/1, Opts). + +-spec dynamize_auto_scaling_policy_update_opt(auto_scaling_policy_opt()) -> json_pair(). +dynamize_auto_scaling_policy_update_opt({policy_name, PolicyName}) -> + {<<"PolicyName">>, PolicyName}; +dynamize_auto_scaling_policy_update_opt({target_tracking_scaling_policy_configuration, TargetTrackingScalingPolicyConfiguration}) -> + {<<"TargetTrackingScalingPolicyConfiguration">>, dynamize_auto_scaling_target_tracking_scaling_policy_configuration_update_opts(TargetTrackingScalingPolicyConfiguration)}. + +-spec dynamize_auto_scaling_policy_update_opts(auto_scaling_policy_opts()) -> jsx:json_term(). +dynamize_auto_scaling_policy_update_opts(Opts) -> + dynamize_maybe_list(fun dynamize_auto_scaling_policy_update_opt/1, Opts). + +-spec dynamize_auto_scaling_settings_update_opt(auto_scaling_settings_update_opt()) -> json_pair(). +dynamize_auto_scaling_settings_update_opt({auto_scaling_disabled, AutoScalingDisabled}) -> + {<<"AutoScalingDisabled">>, AutoScalingDisabled}; +dynamize_auto_scaling_settings_update_opt({auto_scaling_role_arn, AutoScalingRoleArn}) -> + {<<"AutoScalingRoleArn">>, AutoScalingRoleArn}; +dynamize_auto_scaling_settings_update_opt({maximum_units, MaximumUnits}) -> + {<<"MaximumUnits">>, MaximumUnits}; +dynamize_auto_scaling_settings_update_opt({minimum_units, MinimumUnits}) -> + {<<"MinimumUnits">>, MinimumUnits}; +dynamize_auto_scaling_settings_update_opt({scaling_policy_update, ScalingPolicyUpdate}) -> + {<<"ScalingPolicyUpdate">>, dynamize_auto_scaling_policy_update_opts(ScalingPolicyUpdate)}. + +-spec dynamize_auto_scaling_settings_update_opts(auto_scaling_settings_update_opts()) -> jsx:json_term(). +dynamize_auto_scaling_settings_update_opts(Opts) -> + dynamize_maybe_list(fun dynamize_auto_scaling_settings_update_opt/1, Opts). + %%%------------------------------------------------------------------------------ %%% Shared Undynamizers %%%------------------------------------------------------------------------------ @@ -799,6 +888,13 @@ undynamize_global_table_status(<<"UPDATING">>, _) -> updating; undynamize_global_table_status(<<"DELETING">>, _) -> deleting; undynamize_global_table_status(<<"ACTIVE">>, _) -> active. +-spec undynamize_replica_status(binary(), undynamize_opts()) -> replica_status(). +undynamize_replica_status(<<"CREATING">>, _) -> creating; +undynamize_replica_status(<<"CREATION_FAILED">>, _) -> creation_failed; +undynamize_replica_status(<<"UPDATING">>, _) -> updating; +undynamize_replica_status(<<"DELETING">>, _) -> deleting; +undynamize_replica_status(<<"ACTIVE">>, _) -> active. + -spec undynamize_replica_description(jsx:json_term(), undynamize_opts()) -> replica_description(). undynamize_replica_description(ReplicaDescription, _) -> #ddb2_replica_description{region_name = proplists:get_value(<<"RegionName">>, ReplicaDescription)}. @@ -866,11 +962,12 @@ id(X) -> X. -type out_type() :: json | record | typed_record | simple. -type out_opt() :: {out, out_type()}. +-type no_request_opt() :: {no_request, boolean()}. -type boolean_opt(Name) :: Name | {Name, boolean()}. -type property() :: proplists:property(). -type aws_opts() :: [json_pair()]. --type ddb_opts() :: [out_opt()]. +-type ddb_opts() :: [out_opt() | no_request_opt()]. -type opts() :: {aws_opts(), ddb_opts()}. -spec verify_ddb_opt(atom(), term()) -> ok. @@ -881,6 +978,13 @@ verify_ddb_opt(out, Value) -> false -> error({erlcloud_ddb, {invalid_opt, {out, Value}}}) end; +verify_ddb_opt(no_request, Value) -> + case is_boolean(Value) of + true -> + ok; + false -> + error({erlcloud_ddb, {invalid_opt, {no_request, Value}}}) + end; verify_ddb_opt(Name, Value) -> error({erlcloud_ddb, {invalid_opt, {Name, Value}}}). @@ -1020,6 +1124,12 @@ dynamize_expression({contains, Path, Operand}) -> return_consumed_capacity_opt() -> {return_consumed_capacity, <<"ReturnConsumedCapacity">>, fun dynamize_return_consumed_capacity/1}. +-type client_request_token_opt() :: {client_request_token, client_request_token()}. + +-spec client_request_token_opt() -> opt_table_entry(). +client_request_token_opt() -> + {client_request_token, <<"ClientRequestToken">>, fun id/1}. + -type return_item_collection_metrics_opt() :: {return_item_collection_metrics, return_item_collection_metrics()}. -spec return_item_collection_metrics_opt() -> opt_table_entry(). @@ -1034,13 +1144,15 @@ return_item_collection_metrics_opt() -> -type undynamize_fun() :: fun((jsx:json_term(), undynamize_opts()) -> tuple()). -spec out(erlcloud_ddb_impl:json_return(), undynamize_fun(), ddb_opts()) - -> {ok, jsx:json_term() | tuple()} | + -> {ok, jsx:json_term() | tuple() | #ddb2_request{}} | {simple, term()} | {error, term()}. out({error, Reason}, _, _) -> {error, Reason}; out(ok, _, _) -> {error, unexpected_empty_response}; +out({ok, #ddb2_request{}} = Request, _Undynamize, _Opts) -> + Request; out({ok, Json}, Undynamize, Opts) -> case proplists:get_value(out, Opts, simple) of json -> @@ -1078,6 +1190,20 @@ out(Result, Undynamize, Opts, Index, Default) -> %%% Shared Records %%%------------------------------------------------------------------------------ +-spec undynamize_billing_mode(binary(), undynamize_opts()) -> billing_mode(). +undynamize_billing_mode(<<"PROVISIONED">>, _) -> provisioned; +undynamize_billing_mode(<<"PAY_PER_REQUEST">>, _) -> pay_per_request. + +-spec billing_mode_summary_record() -> record_desc(). +billing_mode_summary_record() -> + {#ddb2_billing_mode_summary{}, + [{<<"BillingMode">>, #ddb2_billing_mode_summary.billing_mode, fun undynamize_billing_mode/2}, + {<<"LastUpdateToPayPerRequestDateTime">>, + #ddb2_billing_mode_summary.last_update_to_pay_per_request_date_time, fun id/2}]}. + +undynamize_billing_mode_summary(V, Opts) -> + undynamize_record(billing_mode_summary_record(), V, Opts). + undynamize_consumed_capacity_units(V, _Opts) -> {_, CapacityUnits} = lists:keyfind(<<"CapacityUnits">>, 1, V), CapacityUnits. @@ -1201,13 +1327,125 @@ restore_summary_record() -> {<<"SourceTableArn">>, #ddb2_restore_summary.source_table_arn, fun id/2} ]}. +-spec auto_scaling_target_tracking_scaling_policy_configuration_description_record() -> record_desc(). +auto_scaling_target_tracking_scaling_policy_configuration_description_record() -> + {#ddb2_auto_scaling_target_tracking_scaling_policy_configuration_description{}, + [{<<"TargetValue">>, #ddb2_auto_scaling_target_tracking_scaling_policy_configuration_description.target_value, fun id/2}, + {<<"DisableScaleIn">>, #ddb2_auto_scaling_target_tracking_scaling_policy_configuration_description.disable_scale_in, fun id/2}, + {<<"ScaleInCooldown">>, #ddb2_auto_scaling_target_tracking_scaling_policy_configuration_description.scale_in_cooldown, fun id/2}, + {<<"ScaleOutCooldown">>, #ddb2_auto_scaling_target_tracking_scaling_policy_configuration_description.scale_out_cooldown, fun id/2}]}. + +-spec auto_scaling_policy_description_record() -> record_desc(). +auto_scaling_policy_description_record() -> + {#ddb2_auto_scaling_policy_description{}, + [{<<"PolicyName">>, #ddb2_auto_scaling_policy_description.policy_name, fun id/2}, + {<<"TargetTrackingScalingPolicyConfiguration">>, #ddb2_auto_scaling_policy_description.target_tracking_scaling_policy_configuration, + fun(V, Opts) -> undynamize_record(auto_scaling_target_tracking_scaling_policy_configuration_description_record(), V, Opts) end}]}. + +-spec auto_scaling_settings_description_record() -> record_desc(). +auto_scaling_settings_description_record() -> + {#ddb2_auto_scaling_settings_description{}, + [{<<"AutoScalingDisabled">>, #ddb2_auto_scaling_settings_description.auto_scaling_disabled, fun id/2}, + {<<"AutoScalingRoleArn">>, #ddb2_auto_scaling_settings_description.auto_scaling_role_arn, fun id/2}, + {<<"MaximumUnits">>, #ddb2_auto_scaling_settings_description.maximum_units, fun id/2}, + {<<"MinimumUnits">>, #ddb2_auto_scaling_settings_description.minimum_units, fun id/2}, + {<<"ScalingPolicies">>, #ddb2_auto_scaling_settings_description.scaling_policies, + fun(V, Opts) -> [undynamize_record(auto_scaling_policy_description_record(), I, Opts) || I <- V] end}]}. + +-spec replica_global_secondary_index_settings_description_record() -> record_desc(). +replica_global_secondary_index_settings_description_record() -> + {#ddb2_replica_global_secondary_index_settings_description{}, + [{<<"IndexName">>, #ddb2_replica_global_secondary_index_settings_description.index_name, fun id/2}, + {<<"IndexStatus">>, #ddb2_replica_global_secondary_index_settings_description.index_status, fun undynamize_index_status/2}, + {<<"ProvisionedReadCapacityAutoScalingSettings">>, #ddb2_replica_global_secondary_index_settings_description.provisioned_read_capacity_auto_scaling_settings, + fun(V, Opts) -> undynamize_record(auto_scaling_settings_description_record(), V, Opts) end}, + {<<"ProvisionedReadCapacityUnits">>, #ddb2_replica_global_secondary_index_settings_description.provisioned_read_capacity_units, fun id/2}, + {<<"ProvisionedWriteCapacityAutoScalingSettings">>, #ddb2_replica_global_secondary_index_settings_description.provisioned_write_capacity_auto_scaling_settings, + fun(V, Opts) -> undynamize_record(auto_scaling_settings_description_record(), V, Opts) end}, + {<<"ProvisionedWriteCapacityUnits">>, #ddb2_replica_global_secondary_index_settings_description.provisioned_write_capacity_units, fun id/2}]}. + +-spec replica_global_secondary_index_auto_scaling_description_record() -> record_desc(). +replica_global_secondary_index_auto_scaling_description_record() -> + {#ddb2_replica_global_secondary_index_auto_scaling_description{}, + [{<<"IndexName">>, #ddb2_replica_global_secondary_index_auto_scaling_description.index_name, fun id/2}, + {<<"IndexStatus">>, #ddb2_replica_global_secondary_index_auto_scaling_description.index_status, fun undynamize_index_status/2}, + {<<"ProvisionedReadCapacityAutoScalingSettings">>, #ddb2_replica_global_secondary_index_auto_scaling_description.provisioned_read_capacity_auto_scaling_settings, + fun(V, Opts) -> undynamize_record(auto_scaling_settings_description_record(), V, Opts) end}, + {<<"ProvisionedWriteCapacityAutoScalingSettings">>, #ddb2_replica_global_secondary_index_auto_scaling_description.provisioned_write_capacity_auto_scaling_settings, + fun(V, Opts) -> undynamize_record(auto_scaling_settings_description_record(), V, Opts) end}]}. + +-spec replica_settings_description_record() -> record_desc(). +replica_settings_description_record() -> + {#ddb2_replica_settings_description{}, + [{<<"RegionName">>, #ddb2_replica_settings_description.region_name, fun id/2}, + {<<"ReplicaBillingModeSummary">>, #ddb2_replica_settings_description.replica_billing_mode_summary, fun undynamize_billing_mode_summary/2}, + {<<"ReplicaGlobalSecondaryIndexSettings">>, #ddb2_replica_settings_description.replica_global_secondary_index_settings, + fun(V, Opts) -> [undynamize_record(replica_global_secondary_index_settings_description_record(), I, Opts) || I <- V] end}, + {<<"ReplicaProvisionedReadCapacityAutoScalingSettings">>, #ddb2_replica_settings_description.replica_provisioned_read_capacity_auto_scaling_settings, + fun(V, Opts) -> undynamize_record(auto_scaling_settings_description_record(), V, Opts) end}, + {<<"ReplicaProvisionedReadCapacityUnits">>, #ddb2_replica_settings_description.replica_provisioned_read_capacity_units, fun id/2}, + {<<"ReplicaProvisionedWriteCapacityAutoScalingSettings">>, #ddb2_replica_settings_description.replica_provisioned_write_capacity_auto_scaling_settings, + fun(V, Opts) -> undynamize_record(auto_scaling_settings_description_record(), V, Opts) end}, + {<<"ReplicaProvisionedWriteCapacityUnits">>, #ddb2_replica_settings_description.replica_provisioned_write_capacity_units, fun id/2}, + {<<"ReplicaStatus">>, #ddb2_replica_settings_description.replica_status, fun undynamize_replica_status/2}]}. + +-spec provisioned_throughput_override_record() -> record_desc(). +provisioned_throughput_override_record() -> + {#ddb2_provisioned_throughput_override{}, + [{<<"ReadCapacityUnits">>, #ddb2_provisioned_throughput_override.read_capacity_units, fun id/2}]}. + +undynamize_provisioned_throughput_override(V, Opts) -> + undynamize_record(provisioned_throughput_override_record(), V, Opts). + +-spec replica_global_secondary_index_description_record() -> record_desc(). +replica_global_secondary_index_description_record() -> + {#ddb2_replica_global_secondary_index_description{}, + [{<<"IndexName">>, #ddb2_replica_global_secondary_index_description.index_name, fun id/2}, + {<<"ProvisionedThroughputOverride">>, #ddb2_replica_global_secondary_index_description.provisioned_throughput_override, + fun undynamize_provisioned_throughput_override/2}]}. + +-spec replica_description_record() -> record_desc(). +replica_description_record() -> + {#ddb2_replica_description{}, + [{<<"GlobalSecondaryIndexes">>, #ddb2_replica_description.global_secondary_indexes, + fun(V, Opts) -> [undynamize_record(replica_global_secondary_index_description_record(), I, Opts) || I <- V] end}, + {<<"KMSMasterKeyId">>, #ddb2_replica_description.kms_master_key_id, fun id/2}, + {<<"ProvisionedThroughputOverride">>, #ddb2_replica_description.provisioned_throughput_override, fun undynamize_provisioned_throughput_override/2}, + {<<"RegionName">>, #ddb2_replica_description.region_name, fun id/2}, + {<<"ReplicaStatus">>, #ddb2_replica_description.replica_status, fun undynamize_replica_status/2}, + {<<"ReplicaStatusDescription">>, #ddb2_replica_description.replica_status_description, fun id/2}, + {<<"ReplicaStatusPercentProgress">>, #ddb2_replica_description.replica_status_percent_progress, fun id/2}]}. + +-spec replica_auto_scaling_description_record() -> record_desc(). +replica_auto_scaling_description_record() -> + {#ddb2_replica_auto_scaling_description{}, + [{<<"GlobalSecondaryIndexes">>, #ddb2_replica_auto_scaling_description.global_secondary_indexes, + fun(V, Opts) -> [undynamize_record(replica_global_secondary_index_auto_scaling_description_record(), I, Opts) || I <- V] end}, + {<<"RegionName">>, #ddb2_replica_auto_scaling_description.region_name, fun id/2}, + {<<"ReplicaProvisionedReadCapacityAutoScalingSettings">>, #ddb2_replica_auto_scaling_description.replica_provisioned_read_capacity_auto_scaling_settings, + fun(V, Opts) -> undynamize_record(auto_scaling_settings_description_record(), V, Opts) end}, + {<<"ReplicaProvisionedWriteCapacityAutoScalingSettings">>, #ddb2_replica_auto_scaling_description.replica_provisioned_write_capacity_auto_scaling_settings, + fun(V, Opts) -> undynamize_record(auto_scaling_settings_description_record(), V, Opts) end}, + {<<"ReplicaStatus">>, #ddb2_replica_auto_scaling_description.replica_status, fun undynamize_replica_status/2}]}. + +-spec table_auto_scaling_description_record() -> record_desc(). +table_auto_scaling_description_record() -> + {#ddb2_table_auto_scaling_description{}, + [{<<"Replicas">>, #ddb2_table_auto_scaling_description.replicas, + fun(V, Opts) -> [undynamize_record(replica_auto_scaling_description_record(), I, Opts) || I <- V] end}, + {<<"TableName">>, #ddb2_table_auto_scaling_description.table_name, fun id/2}, + {<<"TableStatus">>, #ddb2_table_auto_scaling_description.table_status, fun undynamize_table_status/2}]}. + -spec table_description_record() -> record_desc(). table_description_record() -> {#ddb2_table_description{}, [{<<"AttributeDefinitions">>, #ddb2_table_description.attribute_definitions, fun undynamize_attr_defs/2}, + {<<"BillingModeSummary">>, #ddb2_table_description.billing_mode_summary, fun undynamize_billing_mode_summary/2}, {<<"CreationDateTime">>, #ddb2_table_description.creation_date_time, fun id/2}, + {<<"DeletionProtectionEnabled">>, #ddb2_table_description.deletion_protection_enabled, fun id/2}, {<<"GlobalSecondaryIndexes">>, #ddb2_table_description.global_secondary_indexes, fun(V, Opts) -> [undynamize_record(global_secondary_index_description_record(), I, Opts) || I <- V] end}, + {<<"GlobalTableVersion">>, #ddb2_table_description.global_table_version, fun id/2}, {<<"ItemCount">>, #ddb2_table_description.item_count, fun id/2}, {<<"KeySchema">>, #ddb2_table_description.key_schema, fun undynamize_key_schema/2}, {<<"LatestStreamArn">>, #ddb2_table_description.latest_stream_arn, fun id/2}, @@ -1216,6 +1454,8 @@ table_description_record() -> fun(V, Opts) -> [undynamize_record(local_secondary_index_description_record(), I, Opts) || I <- V] end}, {<<"ProvisionedThroughput">>, #ddb2_table_description.provisioned_throughput, fun(V, Opts) -> undynamize_record(provisioned_throughput_description_record(), V, Opts) end}, + {<<"Replicas">>, #ddb2_table_description.replicas, + fun(V, Opts) -> [undynamize_record(replica_description_record(), I, Opts) || I <- V] end}, {<<"RestoreSummary">>, #ddb2_table_description.restore_summary, fun(V, Opts) -> undynamize_record(restore_summary_record(), V, Opts) end}, {<<"SSEDescription">>, #ddb2_table_description.sse_description, fun undynamize_sse_description/2}, @@ -1231,7 +1471,8 @@ table_description_record() -> %%%------------------------------------------------------------------------------ -type batch_get_item_opt() :: return_consumed_capacity_opt() | - out_opt(). + out_opt() | + no_request_opt(). -type batch_get_item_opts() :: [batch_get_item_opt()]. -spec batch_get_item_opts() -> opt_table(). @@ -1305,7 +1546,7 @@ batch_get_item_record() -> end} ]}. --type batch_get_item_return() :: ddb_return(#ddb2_batch_get_item{}, [out_item()]). +-type batch_get_item_return() :: ddb_return(#ddb2_batch_get_item{} | #ddb2_request{}, [out_item()]). -spec batch_get_item(batch_get_item_request_items()) -> batch_get_item_return(). batch_get_item(RequestItems) -> @@ -1352,7 +1593,8 @@ batch_get_item(RequestItems, Opts, Config) -> Config, "DynamoDB_20120810.BatchGetItem", [{<<"RequestItems">>, dynamize_batch_get_item_request_items(RequestItems)}] - ++ AwsOpts), + ++ AwsOpts, + DdbOpts), case out(Return, fun(Json, UOpts) -> undynamize_record(batch_get_item_record(), Json, UOpts) end, DdbOpts) of @@ -1372,7 +1614,8 @@ batch_get_item(RequestItems, Opts, Config) -> -type batch_write_item_opt() :: return_consumed_capacity_opt() | return_item_collection_metrics_opt() | - out_opt(). + out_opt() | + no_request_opt(). -type batch_write_item_opts() :: [batch_write_item_opt()]. -spec batch_write_item_opts() -> opt_table(). @@ -1432,7 +1675,7 @@ batch_write_item_record() -> end} ]}. --type batch_write_item_return() :: ddb_return(#ddb2_batch_write_item{}, #ddb2_batch_write_item{}). +-type batch_write_item_return() :: ddb_return(#ddb2_batch_write_item{} | #ddb2_request{}, #ddb2_batch_write_item{}). -spec batch_write_item(batch_write_item_request_items()) -> batch_write_item_return(). batch_write_item(RequestItems) -> @@ -1477,7 +1720,8 @@ batch_write_item(RequestItems, Opts, Config) -> Config, "DynamoDB_20120810.BatchWriteItem", [{<<"RequestItems">>, dynamize_batch_write_item_request_items(RequestItems)}] - ++ AwsOpts), + ++ AwsOpts, + DdbOpts), case out(Return, fun(Json, UOpts) -> undynamize_record(batch_write_item_record(), Json, UOpts) end, DdbOpts) of @@ -1548,7 +1792,8 @@ create_backup(BackupName, TableName, Opts, Config) Config, "DynamoDB_20120810.CreateBackup", [{<<"TableName">>, TableName}, - {<<"BackupName">>, BackupName}]), + {<<"BackupName">>, BackupName}], + DdbOpts), out(Return, fun(Json, UOpts) -> undynamize_record(create_backup_record(), Json, UOpts) end, DdbOpts, #ddb2_create_backup.backup_details). @@ -1611,7 +1856,8 @@ create_global_table(GlobalTableName, ReplicationGroup, Opts, Config) -> Config, "DynamoDB_20120810.CreateGlobalTable", [{<<"GlobalTableName">>, GlobalTableName}, - {<<"ReplicationGroup">>, dynamize_maybe_list(fun dynamize_replica/1, ReplicationGroup)}]), + {<<"ReplicationGroup">>, dynamize_maybe_list(fun dynamize_replica/1, ReplicationGroup)}], + DdbOpts), out(Return, fun(Json, UOpts) -> undynamize_record(create_global_table_record(), Json, UOpts) end, DdbOpts, #ddb2_create_global_table.global_table_description). @@ -1623,6 +1869,10 @@ create_global_table(GlobalTableName, ReplicationGroup, Opts, Config) -> -type local_secondary_indexes() :: maybe_list(local_secondary_index_def()). -type global_secondary_indexes() :: maybe_list(global_secondary_index_def()). +-spec dynamize_billing_mode(billing_mode()) -> binary(). +dynamize_billing_mode(provisioned) -> <<"PROVISIONED">>; +dynamize_billing_mode(pay_per_request) -> <<"PAY_PER_REQUEST">>. + -spec dynamize_local_secondary_index(hash_key_name(), local_secondary_index_def()) -> jsx:json_term(). dynamize_local_secondary_index(HashKey, {IndexName, RangeKey, Projection}) -> [{<<"IndexName">>, IndexName}, @@ -1641,20 +1891,26 @@ dynamize_global_secondary_indexes(Value) -> dynamize_sse_specification({enabled, Enabled}) when is_boolean(Enabled) -> [{<<"Enabled">>, Enabled}]. --type create_table_opt() :: {local_secondary_indexes, local_secondary_indexes()} | +-type create_table_opt() :: {billing_mode, billing_mode()} | + {local_secondary_indexes, local_secondary_indexes()} | {global_secondary_indexes, global_secondary_indexes()} | + {provisioned_throughput, {read_units(), write_units()}} | {sse_specification, sse_specification()} | - {stream_specification, stream_specification()}. + {stream_specification, stream_specification()} | + boolean_opt(deletion_protection_enabled). -type create_table_opts() :: [create_table_opt()]. -spec create_table_opts(key_schema()) -> opt_table(). create_table_opts(KeySchema) -> - [{local_secondary_indexes, <<"LocalSecondaryIndexes">>, + [{billing_mode, <<"BillingMode">>, fun dynamize_billing_mode/1}, + {local_secondary_indexes, <<"LocalSecondaryIndexes">>, fun(V) -> dynamize_local_secondary_indexes(KeySchema, V) end}, {global_secondary_indexes, <<"GlobalSecondaryIndexes">>, fun dynamize_global_secondary_indexes/1}, + {provisioned_throughput, <<"ProvisionedThroughput">>, fun dynamize_provisioned_throughput/1}, {sse_specification, <<"SSESpecification">>, fun dynamize_sse_specification/1}, - {stream_specification, <<"StreamSpecification">>, fun dynamize_stream_specification/1}]. + {stream_specification, <<"StreamSpecification">>, fun dynamize_stream_specification/1}, + {deletion_protection_enabled, <<"DeletionProtectionEnabled">>, fun id/1}]. -spec create_table_record() -> record_desc(). create_table_record() -> @@ -1665,19 +1921,13 @@ create_table_record() -> -type create_table_return() :: ddb_return(#ddb2_create_table{}, #ddb2_table_description{}). --spec create_table(table_name(), attr_defs(), key_schema(), read_units(), write_units()) +-spec create_table(table_name(), attr_defs(), key_schema(), create_table_opts()) -> create_table_return(). -create_table(Table, AttrDefs, KeySchema, ReadUnits, WriteUnits) -> - create_table(Table, AttrDefs, KeySchema, ReadUnits, WriteUnits, [], default_config()). - --spec create_table(table_name(), attr_defs(), key_schema(), read_units(), write_units(), - create_table_opts()) - -> create_table_return(). -create_table(Table, AttrDefs, KeySchema, ReadUnits, WriteUnits, Opts) -> - create_table(Table, AttrDefs, KeySchema, ReadUnits, WriteUnits, Opts, default_config()). +create_table(Table, AttrDefs, KeySchema, Opts) -> + create_table(Table, AttrDefs, KeySchema, Opts, default_config()). %%------------------------------------------------------------------------------ -%% @doc +%% @doc %% DynamoDB API: %% [http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_CreateTable.html] %% @@ -1686,8 +1936,8 @@ create_table(Table, AttrDefs, KeySchema, ReadUnits, WriteUnits, Opts) -> %% Create a table with hash key "ForumName" and range key "Subject" %% with a local secondary index on "LastPostDateTime" %% and a global secondary index on "Subject" as hash key and "LastPostDateTime" -%% as range key, read and write capacity 10, projecting all fields -%% +%% as range key, read and write capacity 10, projecting all fields +%% %% ` %% {ok, Description} = %% erlcloud_ddb2:create_table( @@ -1696,33 +1946,49 @@ create_table(Table, AttrDefs, KeySchema, ReadUnits, WriteUnits, Opts) -> %% {<<"Subject">>, s}, %% {<<"LastPostDateTime">>, s}], %% {<<"ForumName">>, <<"Subject">>}, -%% 5, -%% 5, -%% [{local_secondary_indexes, +%% [{provisioned_throughput, {5, 5}}, +%% {local_secondary_indexes, %% [{<<"LastPostIndex">>, <<"LastPostDateTime">>, keys_only}]}, %% {global_secondary_indexes, [ %% {<<"SubjectTimeIndex">>, {<<"Subject">>, <<"LastPostDateTime">>}, all, 10, 10} -%% ]} +%% ]} %% ]), %% ' %% @end %%------------------------------------------------------------------------------ --spec create_table(table_name(), attr_defs(), key_schema(), read_units(), write_units(), - create_table_opts(), aws_config()) +-spec create_table(table_name(), attr_defs(), key_schema(), read_units(), write_units()) + -> create_table_return(); + (table_name(), attr_defs(), key_schema(), create_table_opts(), aws_config()) -> create_table_return(). -create_table(Table, AttrDefs, KeySchema, ReadUnits, WriteUnits, Opts, Config) -> +create_table(Table, AttrDefs, KeySchema, ReadUnits, WriteUnits) + when is_integer(ReadUnits), is_integer(WriteUnits) -> + create_table(Table, AttrDefs, KeySchema, ReadUnits, WriteUnits, [], default_config()); +create_table(Table, AttrDefs, KeySchema, Opts, Config) -> {AwsOpts, DdbOpts} = opts(create_table_opts(KeySchema), Opts), Return = erlcloud_ddb_impl:request( - Config, - "DynamoDB_20120810.CreateTable", - [{<<"TableName">>, Table}, - {<<"AttributeDefinitions">>, dynamize_attr_defs(AttrDefs)}, - {<<"KeySchema">>, dynamize_key_schema(KeySchema)}, - {<<"ProvisionedThroughput">>, dynamize_provisioned_throughput({ReadUnits, WriteUnits})}] - ++ AwsOpts), - out(Return, fun(Json, UOpts) -> undynamize_record(create_table_record(), Json, UOpts) end, + Config, + "DynamoDB_20120810.CreateTable", + [{<<"TableName">>, Table}, + {<<"AttributeDefinitions">>, dynamize_attr_defs(AttrDefs)}, + {<<"KeySchema">>, dynamize_key_schema(KeySchema)}] + ++ AwsOpts, + DdbOpts), + out(Return, fun(Json, UOpts) -> undynamize_record(create_table_record(), Json, UOpts) end, DdbOpts, #ddb2_create_table.table_description). +-spec create_table(table_name(), attr_defs(), key_schema(), read_units(), write_units(), + create_table_opts()) + -> create_table_return(). +create_table(Table, AttrDefs, KeySchema, ReadUnits, WriteUnits, Opts) -> + create_table(Table, AttrDefs, KeySchema, ReadUnits, WriteUnits, Opts, default_config()). + +-spec create_table(table_name(), attr_defs(), key_schema(), read_units(), write_units(), + create_table_opts(), aws_config()) + -> create_table_return(). +create_table(Table, AttrDefs, KeySchema, ReadUnits, WriteUnits, Opts0, Config) -> + Opts = [{provisioned_throughput, {ReadUnits, WriteUnits}} | Opts0], + create_table(Table, AttrDefs, KeySchema, Opts, Config). + %%%------------------------------------------------------------------------------ %%% DeleteBackup %%%------------------------------------------------------------------------------ @@ -1833,7 +2099,8 @@ delete_backup(BackupArn, Opts, Config) Return = erlcloud_ddb_impl:request( Config, "DynamoDB_20120810.DeleteBackup", - [{<<"BackupArn">>, BackupArn}]), + [{<<"BackupArn">>, BackupArn}], + DdbOpts), out(Return, fun(Json, UOpts) -> undynamize_record(delete_backup_record(), Json, UOpts) end, DdbOpts, #ddb2_delete_backup.backup_description). @@ -1849,7 +2116,8 @@ delete_backup(BackupArn, Opts, Config) {return_values, none | all_old} | return_consumed_capacity_opt() | return_item_collection_metrics_opt() | - out_opt(). + out_opt() | + no_request_opt(). -type delete_item_opts() :: [delete_item_opt()]. -spec delete_item_opts() -> opt_table(). @@ -1872,7 +2140,7 @@ delete_item_record() -> fun undynamize_item_collection_metrics/2} ]}. --type delete_item_return() :: ddb_return(#ddb2_delete_item{}, out_item()). +-type delete_item_return() :: ddb_return(#ddb2_delete_item{} | #ddb2_request{}, out_item()). -spec delete_item(table_name(), key()) -> delete_item_return(). delete_item(Table, Key) -> @@ -1926,7 +2194,8 @@ delete_item(Table, Key, Opts, Config) -> "DynamoDB_20120810.DeleteItem", [{<<"TableName">>, Table}, {<<"Key">>, dynamize_key(Key)}] - ++ AwsOpts), + ++ AwsOpts, + DdbOpts), out(Return, fun(Json, UOpts) -> undynamize_record(delete_item_record(), Json, UOpts) end, DdbOpts, #ddb2_delete_item.attributes, {ok, []}). @@ -1972,7 +2241,8 @@ delete_table(Table, Opts, Config) -> Return = erlcloud_ddb_impl:request( Config, "DynamoDB_20120810.DeleteTable", - [{<<"TableName">>, Table}]), + [{<<"TableName">>, Table}], + DdbOpts), out(Return, fun(Json, UOpts) -> undynamize_record(delete_table_record(), Json, UOpts) end, DdbOpts, #ddb2_delete_table.table_description). @@ -2021,7 +2291,8 @@ describe_backup(BackupArn, Opts, Config) Return = erlcloud_ddb_impl:request( Config, "DynamoDB_20120810.DescribeBackup", - [{<<"BackupArn">>, BackupArn}]), + [{<<"BackupArn">>, BackupArn}], + DdbOpts), out(Return, fun(Json, UOpts) -> undynamize_record(describe_backup_record(), Json, UOpts) end, DdbOpts, #ddb2_describe_backup.backup_description). @@ -2086,7 +2357,8 @@ describe_continuous_backups(TableName, Opts, Config) Return = erlcloud_ddb_impl:request( Config, "DynamoDB_20120810.DescribeContinuousBackups", - [{<<"TableName">>, TableName}]), + [{<<"TableName">>, TableName}], + DdbOpts), out(Return, fun(Json, UOpts) -> undynamize_record(continuous_backups_record(), Json, UOpts) end, DdbOpts, #ddb2_describe_continuous_backups.continuous_backups_description). @@ -2136,10 +2408,60 @@ describe_global_table(GlobalTableName, Opts, Config) -> Return = erlcloud_ddb_impl:request( Config, "DynamoDB_20120810.DescribeGlobalTable", - [{<<"GlobalTableName">>, GlobalTableName}]), + [{<<"GlobalTableName">>, GlobalTableName}], + DdbOpts), out(Return, fun(Json, UOpts) -> undynamize_record(describe_global_table_record(), Json, UOpts) end, DdbOpts, #ddb2_describe_table.table). +%%%------------------------------------------------------------------------------ +%%% DescribeGlobalTableSettings +%%%------------------------------------------------------------------------------ + +-type describe_global_table_settings_return() :: ddb_return(#ddb2_describe_global_table_settings{}, #ddb2_replica_settings_description{}). + +-spec describe_global_table_settings_record() -> record_desc(). +describe_global_table_settings_record() -> + {#ddb2_describe_global_table_settings{}, + [{<<"GlobalTableName">>, #ddb2_describe_global_table_settings.global_table_name, fun id/2}, + {<<"ReplicaSettings">>, #ddb2_describe_global_table_settings.replica_settings, + fun(V, Opts) -> [undynamize_record(replica_settings_description_record(), I, Opts) || I <- V] end}]}. + +-spec describe_global_table_settings(table_name()) -> describe_global_table_settings_return(). +describe_global_table_settings(GlobalTableName) -> + describe_global_table_settings(GlobalTableName, []). + +-spec describe_global_table_settings(table_name(), ddb_opts()) -> describe_global_table_settings_return(). +describe_global_table_settings(GlobalTableName, Opts) -> + describe_global_table_settings(GlobalTableName, Opts, default_config()). + +%%------------------------------------------------------------------------------ +%% @doc +%% DynamoDB API: +%% [https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DescribeGlobalTableSettings.html] +%% +%% Note: This method only applies to Version 2017.11.29 of global tables. +%% +%% ===Example=== +%% +%% Describes Region-specific settings for "Thread" global table. +%% +%% ` +%% {ok, GlobalTableSettings} = +%% erlcloud_ddb2:describe_global_table_settings(<<"Thread">>), +%% ' +%% @end +%%------------------------------------------------------------------------------ +-spec describe_global_table_settings(table_name(), ddb_opts(), aws_config()) -> describe_global_table_settings_return(). +describe_global_table_settings(GlobalTableName, Opts, Config) -> + {[], DdbOpts} = opts([], Opts), + Return = erlcloud_ddb_impl:request( + Config, + "DynamoDB_20120810.DescribeGlobalTableSettings", + [{<<"GlobalTableName">>, GlobalTableName}], + DdbOpts), + out(Return, fun(Json, UOpts) -> undynamize_record(describe_global_table_settings_record(), Json, UOpts) end, + DdbOpts, #ddb2_describe_global_table_settings.replica_settings). + %%%------------------------------------------------------------------------------ %%% DescribeLimits %%%------------------------------------------------------------------------------ @@ -2184,7 +2506,8 @@ describe_limits(Opts, Config) -> Return = erlcloud_ddb_impl:request( Config, "DynamoDB_20120810.DescribeLimits", - []), + [], + DdbOpts), case out(Return, fun(Json, UOpts) -> undynamize_record(describe_limits_record(), Json, UOpts) end, DdbOpts) of {simple, Record} -> {ok, Record}; @@ -2234,10 +2557,59 @@ describe_table(Table, Opts, Config) -> Return = erlcloud_ddb_impl:request( Config, "DynamoDB_20120810.DescribeTable", - [{<<"TableName">>, Table}]), + [{<<"TableName">>, Table}], + DdbOpts), out(Return, fun(Json, UOpts) -> undynamize_record(describe_table_record(), Json, UOpts) end, DdbOpts, #ddb2_describe_table.table). +%%%------------------------------------------------------------------------------ +%%% DescribeTableReplicaAutoScaling +%%%------------------------------------------------------------------------------ + +-type describe_table_replica_auto_scaling_return() :: ddb_return(#ddb2_describe_table_replica_auto_scaling{}, #ddb2_table_auto_scaling_description{}). + +-spec describe_table_replica_auto_scaling_record() -> record_desc(). +describe_table_replica_auto_scaling_record() -> + {#ddb2_describe_table_replica_auto_scaling{}, + [{<<"TableAutoScalingDescription">>, #ddb2_describe_table_replica_auto_scaling.table_auto_scaling_description, + fun(V, Opts) -> undynamize_record(table_auto_scaling_description_record(), V, Opts) end} + ]}. + +-spec describe_table_replica_auto_scaling(table_name()) -> describe_table_replica_auto_scaling_return(). +describe_table_replica_auto_scaling(Table) -> + describe_table_replica_auto_scaling(Table, [], default_config()). + +-spec describe_table_replica_auto_scaling(table_name(), ddb_opts()) -> describe_table_replica_auto_scaling_return(). +describe_table_replica_auto_scaling(Table, Opts) -> + describe_table_replica_auto_scaling(Table, Opts, default_config()). + +%%------------------------------------------------------------------------------ +%% @doc +%% DynamoDB API: +%% [http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DescribeTableReplicaAutoScaling.html] +%% Note: This method only applies to Version 2019.11.21 of global tables. +%% +%% ===Example=== +%% +%% Describes auto scaling settings across replicas of the global table called "Thread" at once. +%% +%% ` +%% {ok, Description} = +%% erlcloud_ddb2:describe_table_replica_auto_scaling(<<"Thread">>), +%% ' +%% @end +%%------------------------------------------------------------------------------ +-spec describe_table_replica_auto_scaling(table_name(), ddb_opts(), aws_config()) -> describe_table_replica_auto_scaling_return(). +describe_table_replica_auto_scaling(Table, Opts, Config) -> + {[], DdbOpts} = opts([], Opts), + Return = erlcloud_ddb_impl:request( + Config, + "DynamoDB_20120810.DescribeTableReplicaAutoScaling", + [{<<"TableName">>, Table}], + DdbOpts), + out(Return, fun(Json, UOpts) -> undynamize_record(describe_table_replica_auto_scaling_record(), Json, UOpts) end, + DdbOpts, #ddb2_describe_table_replica_auto_scaling.table_auto_scaling_description). + %%%------------------------------------------------------------------------------ %%% DescribeTimeToLive %%%------------------------------------------------------------------------------ @@ -2293,7 +2665,8 @@ describe_time_to_live(Table, DbOpts, Config) -> Return = erlcloud_ddb_impl:request( Config, "DynamoDB_20120810.DescribeTimeToLive", - [{<<"TableName">>, Table}]), + [{<<"TableName">>, Table}], + DbOpts), out(Return, fun(Json, UOpts) -> undynamize_record(describe_time_to_live_record(), Json, UOpts) end, DbOpts, #ddb2_describe_time_to_live.time_to_live_description). @@ -2306,7 +2679,8 @@ describe_time_to_live(Table, DbOpts, Config) -> attributes_to_get_opt() | consistent_read_opt() | return_consumed_capacity_opt() | - out_opt(). + out_opt() | + no_request_opt(). -type get_item_opts() :: [get_item_opt()]. -spec get_item_opts() -> opt_table(). @@ -2324,7 +2698,7 @@ get_item_record() -> {<<"ConsumedCapacity">>, #ddb2_get_item.consumed_capacity, fun undynamize_consumed_capacity/2} ]}. --type get_item_return() :: ddb_return(#ddb2_get_item{}, out_item()). +-type get_item_return() :: ddb_return(#ddb2_get_item{} | #ddb2_request{}, out_item()). -spec get_item(table_name(), key()) -> get_item_return(). get_item(Table, Key) -> @@ -2363,7 +2737,8 @@ get_item(Table, Key, Opts, Config) -> "DynamoDB_20120810.GetItem", [{<<"TableName">>, Table}, {<<"Key">>, dynamize_key(Key)}] - ++ AwsOpts), + ++ AwsOpts, + DdbOpts), out(Return, fun(Json, UOpts) -> undynamize_record(get_item_record(), Json, UOpts) end, DdbOpts, #ddb2_get_item.item, {ok, []}). @@ -2449,7 +2824,8 @@ list_backups(Opts, Config) -> Return = erlcloud_ddb_impl:request( Config, "DynamoDB_20120810.ListBackups", - AwsOpts), + AwsOpts, + DdbOpts), out(Return, fun(Json, UOpts) -> undynamize_record(list_backups_record(), Json, UOpts) end, DdbOpts, #ddb2_list_backups.backup_summaries). @@ -2515,7 +2891,8 @@ list_global_tables(Opts, Config) -> Return = erlcloud_ddb_impl:request( Config, "DynamoDB_20120810.ListGlobalTables", - AwsOpts), + AwsOpts, + DdbOpts), out(Return, fun(Json, UOpts) -> undynamize_record(list_global_tables_record(), Json, UOpts) end, DdbOpts, #ddb2_list_global_tables.global_tables, {ok, []}). @@ -2573,7 +2950,8 @@ list_tables(Opts, Config) -> Return = erlcloud_ddb_impl:request( Config, "DynamoDB_20120810.ListTables", - AwsOpts), + AwsOpts, + DdbOpts), out(Return, fun(Json, UOpts) -> undynamize_record(list_tables_record(), Json, UOpts) end, DdbOpts, #ddb2_list_tables.table_names, {ok, []}). @@ -2639,7 +3017,8 @@ list_tags_of_resource(ResourceArn, Opts, Config) -> Return = erlcloud_ddb_impl:request( Config, "DynamoDB_20120810.ListTagsOfResource", - [{<<"ResourceArn">>, ResourceArn} | AwsOpts]), + [{<<"ResourceArn">>, ResourceArn} | AwsOpts], + DdbOpts), out(Return, fun(Json, UOpts) -> undynamize_record(list_tags_of_resource_record(), Json, UOpts) end, DdbOpts, #ddb2_list_tags_of_resource.tags, {ok, []}). @@ -2655,7 +3034,8 @@ list_tags_of_resource(ResourceArn, Opts, Config) -> {return_values, none | all_old} | return_consumed_capacity_opt() | return_item_collection_metrics_opt() | - out_opt(). + out_opt() | + no_request_opt(). -type put_item_opts() :: [put_item_opt()]. -spec put_item_opts() -> opt_table(). @@ -2678,7 +3058,7 @@ put_item_record() -> fun undynamize_item_collection_metrics/2} ]}. --type put_item_return() :: ddb_return(#ddb2_put_item{}, out_item()). +-type put_item_return() :: ddb_return(#ddb2_put_item{} | #ddb2_request{}, out_item()). -spec put_item(table_name(), in_item()) -> put_item_return(). put_item(Table, Item) -> @@ -2742,7 +3122,8 @@ put_item(Table, Item, Opts, Config) -> "DynamoDB_20120810.PutItem", [{<<"TableName">>, Table}, {<<"Item">>, dynamize_item(Item)}] - ++ AwsOpts), + ++ AwsOpts, + DdbOpts), out(Return, fun(Json, UOpts) -> undynamize_record(put_item_record(), Json, UOpts) end, DdbOpts, #ddb2_put_item.attributes, {ok, []}). @@ -2801,7 +3182,7 @@ q_record() -> {<<"ScannedCount">>, #ddb2_q.scanned_count, fun id/2} ]}. --type q_return() :: ddb_return(#ddb2_q{}, [out_item()]). +-type q_return() :: ddb_return(#ddb2_q{} | #ddb2_request{}, [out_item()]). -spec q(table_name(), conditions() | expression()) -> q_return(). q(Table, KeyConditionsOrExpression) -> @@ -2821,7 +3202,7 @@ q(Table, KeyConditionsOrExpression, Opts) -> %% %% ===Example=== %% -%% Get up to 3 itesm from the "Thread" table with "ForumName" of +%% Get up to 3 items from the "Thread" table with "ForumName" of %% "Amazon DynamoDB" and "LastPostDateTime" between specified %% value. Use the "LastPostIndex". %% @@ -2853,7 +3234,8 @@ q(Table, KeyConditionsOrExpression, Opts, Config) -> "DynamoDB_20120810.Query", [{<<"TableName">>, Table}, dynamize_q_key_conditions_or_expression(KeyConditionsOrExpression)] - ++ AwsOpts), + ++ AwsOpts, + DdbOpts), out(Return, fun(Json, UOpts) -> undynamize_record(q_record(), Json, UOpts) end, DdbOpts, #ddb2_q.items, {ok, []}). @@ -2903,7 +3285,8 @@ restore_table_from_backup(BackupArn, TargetTableName, Opts, Config) Config, "DynamoDB_20120810.RestoreTableFromBackup", [{<<"BackupArn">>, BackupArn}, - {<<"TargetTableName">>, TargetTableName}]), + {<<"TargetTableName">>, TargetTableName}], + DdbOpts), out(Return, fun(Json, UOpts) -> undynamize_record(restore_table_from_backup_record(), Json, UOpts) end, DdbOpts, #ddb2_restore_table_from_backup.table_description). @@ -2962,7 +3345,8 @@ restore_table_to_point_in_time(SourceTableName, TargetTableName, Opts, Config) Config, "DynamoDB_20120810.RestoreTableToPointInTime", [{<<"SourceTableName">>, SourceTableName}, - {<<"TargetTableName">>, TargetTableName}] ++ AwsOpts), + {<<"TargetTableName">>, TargetTableName}] ++ AwsOpts, + DdbOpts), out(Return, fun(Json, UOpts) -> undynamize_record(restore_table_to_point_in_time(), Json, UOpts) end, DdbOpts, #ddb2_restore_table_to_point_in_time.table_description). @@ -2985,7 +3369,8 @@ restore_table_to_point_in_time(SourceTableName, TargetTableName, Opts, Config) {index_name, index_name()} | {select, select()} | return_consumed_capacity_opt() | - out_opt(). + out_opt() | + no_request_opt(). -type scan_opts() :: [scan_opt()]. -spec scan_opts() -> opt_table(). @@ -3017,7 +3402,7 @@ scan_record() -> {<<"ScannedCount">>, #ddb2_scan.scanned_count, fun id/2} ]}. --type scan_return() :: ddb_return(#ddb2_scan{}, [out_item()]). +-type scan_return() :: ddb_return(#ddb2_scan{} | #ddb2_request{}, [out_item()]). -spec scan(table_name()) -> scan_return(). scan(Table) -> @@ -3052,7 +3437,8 @@ scan(Table, Opts, Config) -> Config, "DynamoDB_20120810.Scan", [{<<"TableName">>, Table}] - ++ AwsOpts), + ++ AwsOpts, + DdbOpts), out(Return, fun(Json, UOpts) -> undynamize_record(scan_record(), Json, UOpts) end, DdbOpts, #ddb2_scan.items, {ok, []}). @@ -3101,6 +3487,259 @@ tag_resource(ResourceArn, Tags, Config) -> [{<<"ResourceArn">>, ResourceArn}, {<<"Tags">>, dynamize_tags(Tags)}]). +%%%----------------------------------------------------------------------------- +%%% TransactGetItems +%%%----------------------------------------------------------------------------- + +-type transact_get_items_transact_item_opts() :: expression_attribute_names_opt() | + projection_expression_opt() | + return_consumed_capacity_opt() | + out_opt() | + no_request_opt(). +-type transact_get_items_opts() :: [transact_get_items_transact_item_opts()]. + +-type transact_get_items_get_item() :: {table_name(), key()} + | {table_name(), key(), transact_get_items_opts()}. + +-type transact_get_items_get() :: {get, transact_get_items_get_item()}. + +-type transact_get_items_transact_item() :: transact_get_items_get(). +-type transact_get_items_transact_items() :: maybe_list(transact_get_items_transact_item()). + +-type transact_get_items_return() :: ddb_return(#ddb2_transact_get_items{} | #ddb2_request{}, out_item()). + +-spec dynamize_transact_get_items_transact_items(transact_get_items_transact_items()) + -> [jsx:json_term()]. +dynamize_transact_get_items_transact_items(TransactItems) -> + dynamize_maybe_list(fun dynamize_transact_get_items_transact_item/1, TransactItems). + +-spec transact_get_items_transact_item_opts() -> opt_table(). +transact_get_items_transact_item_opts() -> + [expression_attribute_names_opt(), + projection_expression_opt()]. + +-spec dynamize_transact_get_items_transact_item(transact_get_items_transact_item()) -> jsx:json_term(). +dynamize_transact_get_items_transact_item({get, {TableName, Key}}) -> + dynamize_transact_get_items_transact_item({get, {TableName, Key, []}}); +dynamize_transact_get_items_transact_item({get, {TableName, Key, Opts}}) -> + {AwsOpts, _DdbOpts} = opts(transact_get_items_transact_item_opts(), Opts), + [{<<"Get">>, [{<<"TableName">>, TableName}, {<<"Key">>, dynamize_key(Key)} | AwsOpts]}]. + +undynamize_transact_get_items_responses(Response, Opts) -> + lists:map(fun(R) -> + Item = proplists:get_value(<<"Item">>, R), + #ddb2_item_response{item = undynamize_item(Item, Opts)} + end, Response). + +-spec transact_get_items_record() -> record_desc(). +transact_get_items_record() -> + {#ddb2_transact_get_items{}, + [{<<"ConsumedCapacity">>, #ddb2_transact_get_items.consumed_capacity, fun undynamize_consumed_capacity_list/2}, + {<<"Responses">>, #ddb2_transact_get_items.responses, fun undynamize_transact_get_items_responses/2} + ]}. + +-spec transact_get_items_opts() -> opt_table(). +transact_get_items_opts() -> + [return_consumed_capacity_opt()]. + +-spec transact_get_items(transact_get_items_transact_items()) -> transact_get_items_return(). +transact_get_items(RequestItems) -> + transact_get_items(RequestItems, [], default_config()). + +-spec transact_get_items(transact_get_items_transact_items(), transact_get_items_opts()) -> transact_get_items_return(). +transact_get_items(RequestItems, Opts) -> + transact_get_items(RequestItems, Opts, default_config()). + +%%------------------------------------------------------------------------------ +%% @doc +%% DynamoDB API: +%% [https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactGetItems.html] +%% +%% Note that in the case of a `TransactionCanceledException' DynamoDB error, the error +%% response has the form `{error, {<<"TransactionCanceledException">>, {Message, CancellationReasons}}}' +%% where `Message' is a binary string and `CancellationReasons' is an ordered list in the form +%% `[{Code, Message}]', where `Code' is the status code of the result and `Message' is the cancellation +%% reason message description. +%% +%% ===Example=== +%% +%% Get two items in a transaction. +%% +%% ` +%% {ok, Record} = +%% erlcloud_ddb2:transact_get_items( +%% [{get, {<<"PersonalInfo">>, [{<<"Name">>, {s, <<"John Smith">>}}, +%% {<<"DOB">>, {s, <<"11/11/2011">>}}]}}, +%% {get, {<<"EmployeeRecord">>, [{<<"Name">>, {s, <<"John Smith">>}}, +%% {<<"DOH">>, {s, <<"11/11/2018">>}}]}}], +%% [{return_consumed_capacity, total}, +%% {out, record}]), +%% ' +%% @end +%%------------------------------------------------------------------------------ +-spec transact_get_items(transact_get_items_transact_items(), transact_get_items_opts(), aws_config()) -> + transact_get_items_return(). +transact_get_items(TransactItems, Opts, Config) -> + {AwsOpts, DdbOpts} = opts(transact_get_items_opts(), Opts), + Return = erlcloud_ddb_impl:request( + Config, + "DynamoDB_20120810.TransactGetItems", + [{<<"TransactItems">>, dynamize_transact_get_items_transact_items(TransactItems)}] + ++ AwsOpts, + DdbOpts), + case out(Return, + fun(Json, UOpts) -> undynamize_record(transact_get_items_record(), Json, UOpts) end, DdbOpts) of + {simple, #ddb2_transact_get_items{responses = Responses}} -> + %% Simple return for transact_get_items is all items from all tables in a single list + {ok, lists:map(fun(#ddb2_item_response{item = I}) -> I end, Responses)}; + {ok, _} = Out -> Out; + {error, _} = Out -> Out + end. + +%%%------------------------------------------------------------------------------ +%%% TransactWriteItem +%%%------------------------------------------------------------------------------ + +-type transact_write_items_opt() :: client_request_token_opt() | + return_consumed_capacity_opt() | + return_item_collection_metrics_opt() | + out_opt() | + no_request_opt(). +-type transact_write_items_opts() :: [transact_write_items_opt()]. + +-type return_value_on_condition_check_failure_opt() :: {return_values_on_condition_check_failure, return_value()}. + +-spec return_value_on_condition_check_failure_opt() -> opt_table_entry(). +return_value_on_condition_check_failure_opt() -> + {return_values_on_condition_check_failure, <<"ReturnValuesOnConditionCheckFailure">>, fun dynamize_return_value/1}. + +-type transact_write_items_transact_item_opt() :: expression_attribute_names_opt() | + expression_attribute_values_opt() | + condition_expression_opt() | + return_value_on_condition_check_failure_opt(). +-type transact_write_items_condition_check_item() :: {table_name(), key(), transact_write_items_transact_item_opts()} + | {table_name(), key(), binary(), transact_write_items_transact_item_opts()}. +-type transact_write_items_delete_item() :: {table_name(), key()} + | {table_name(), key(), transact_write_items_transact_item_opts()}. +-type transact_write_items_put_item() :: {table_name(), in_item()} + | {table_name(), in_item(), transact_write_items_transact_item_opts()}. +-type transact_write_items_update_item() :: {table_name(), key(), expression(), transact_write_items_transact_item_opts()}. + +-type transact_write_items_condition_check() :: {condition_check, transact_write_items_condition_check_item()}. +-type transact_write_items_delete() :: {delete, transact_write_items_delete_item()}. +-type transact_write_items_put() :: {put, transact_write_items_put_item()}. +-type transact_write_items_update() :: {update, transact_write_items_update_item()}. + +-type transact_write_items_transact_item() :: transact_write_items_condition_check() | transact_write_items_delete() | transact_write_items_put() | transact_write_items_update(). +-type transact_write_items_transact_items() :: maybe_list(transact_write_items_transact_item()). + +-type transact_write_items_transact_item_opts() :: [transact_write_items_transact_item_opt()]. + +-spec transact_write_items_transact_item_opts() -> opt_table(). +transact_write_items_transact_item_opts() -> + [expression_attribute_names_opt(), + expression_attribute_values_opt(), + condition_expression_opt(), + return_value_on_condition_check_failure_opt()]. + +-spec dynamize_transact_write_items_transact_item(transact_write_items_transact_item()) -> jsx:json_term(). +dynamize_transact_write_items_transact_item({condition_check, {TableName, Key, Opts}}) -> + {AwsOpts, _DdbOpts} = opts(transact_write_items_transact_item_opts(), Opts), + [{<<"ConditionCheck">>, [{<<"TableName">>, TableName}, {<<"Key">>, dynamize_key(Key)} | AwsOpts]}]; +dynamize_transact_write_items_transact_item({delete, {TableName, Key}}) -> + dynamize_transact_write_items_transact_item({delete, {TableName, Key, []}}); +dynamize_transact_write_items_transact_item({delete, {TableName, Key, Opts}}) -> + {AwsOpts, _DdbOpts} = opts(transact_write_items_transact_item_opts(), Opts), + [{<<"Delete">>, [{<<"TableName">>, TableName}, {<<"Key">>, dynamize_key(Key)} | AwsOpts]}]; +dynamize_transact_write_items_transact_item({put, {TableName, Item}}) -> + dynamize_transact_write_items_transact_item({put, {TableName, Item, []}}); +dynamize_transact_write_items_transact_item({put, {TableName, Item, Opts}}) -> + {AwsOpts, _DdbOpts} = opts(transact_write_items_transact_item_opts(), Opts), + [{<<"Put">>, [{<<"TableName">>, TableName}, {<<"Item">>, dynamize_item(Item)} | AwsOpts]}]; +dynamize_transact_write_items_transact_item({update, {TableName, Key, UpdateExpression}}) -> + dynamize_transact_write_items_transact_item({update, {TableName, Key, UpdateExpression, []}}); +dynamize_transact_write_items_transact_item({update, {TableName, Key, UpdateExpression, Opts}}) -> + {AwsOpts, _DdbOpts} = opts(transact_write_items_transact_item_opts(), Opts), + [{<<"Update">>, [{<<"TableName">>, TableName}, {<<"Key">>, dynamize_key(Key)}, {<<"UpdateExpression">>, dynamize_expression(UpdateExpression)} | AwsOpts]}]. + +-spec dynamize_transact_write_items_transact_items(transact_write_items_transact_items()) + -> [jsx:json_term()]. +dynamize_transact_write_items_transact_items(TransactItems) -> + dynamize_maybe_list(fun dynamize_transact_write_items_transact_item/1, TransactItems). + +-spec transact_write_items_opts() -> opt_table(). +transact_write_items_opts() -> + [client_request_token_opt(), + return_consumed_capacity_opt(), + return_item_collection_metrics_opt()]. + + +-spec transact_write_items_record() -> record_desc(). +transact_write_items_record() -> + {#ddb2_transact_write_items{}, + [{<<"ConsumedCapacity">>, #ddb2_transact_write_items.consumed_capacity, fun undynamize_consumed_capacity_list/2}, + {<<"ItemCollectionMetrics">>, #ddb2_transact_write_items.item_collection_metrics, + fun(V, Opts) -> undynamize_object( + fun({Table, Json}, Opts2) -> + undynamize_item_collection_metric_list(Table, Json, Opts2) + end, V, Opts) + end} + ]}. + +-type transact_write_items_return() :: ddb_return(#ddb2_transact_write_items{} | #ddb2_request{}, out_item()). + +-spec transact_write_items(transact_write_items_transact_items()) -> transact_write_items_return(). +transact_write_items(RequestItems) -> + transact_write_items(RequestItems, [], default_config()). + +-spec transact_write_items(transact_write_items_transact_items(), transact_write_items_opts()) -> transact_write_items_return(). +transact_write_items(RequestItems, Opts) -> + transact_write_items(RequestItems, Opts, default_config()). + +%%------------------------------------------------------------------------------ +%% @doc +%% DynamoDB API: +%% [https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactWriteItems.html] +%% +%% Note that in the case of a `TransactionCanceledException' DynamoDB error, the error +%% response has the form `{error, {<<"TransactionCanceledException">>, {Message, CancellationReasons}}}' +%% where `Message' is a binary string and `CancellationReasons' is an ordered list in the form +%% `[{Code, Message}]', where `Code' is the status code of the result and `Message' is the cancellation +%% reason message description. +%% +%% ===Example=== +%% +%% Put two items in a transaction. +%% +%% ` +%% {ok, Record} = +%% erlcloud_ddb2:transact_write_items( +%% [{put, {<<"PersonalInfo">>, [{<<"Name">>, {s, <<"John Smith">>}}, +%% {<<"DOB">>, {s, <<"11/11/2011">>}}]}}, +%% {put, {<<"EmployeeRecord">>, [{<<"Name">>, {s, <<"John Smith">>}}, +%% {<<"DOH">>, {s, <<"11/11/2018">>}}]}}], +%% [{return_consumed_capacity, total}, +%% {out, record}]), +%% ' +%% @end +%%------------------------------------------------------------------------------ +-spec transact_write_items(transact_write_items_transact_items(), transact_write_items_opts(), aws_config()) -> + transact_write_items_return(). +transact_write_items(TransactItems, Opts, Config) -> + {AwsOpts, DdbOpts} = opts(transact_write_items_opts(), Opts), + Return = erlcloud_ddb_impl:request( + Config, + "DynamoDB_20120810.TransactWriteItems", + [{<<"TransactItems">>, dynamize_transact_write_items_transact_items(TransactItems)}] + ++ AwsOpts, + DdbOpts), + case out(Return, + fun(Json, UOpts) -> undynamize_record(transact_write_items_record(), Json, UOpts) end, DdbOpts, + #ddb2_transact_write_items.attributes, {ok, []}) of + {ok, _} = Out -> Out; + {error, _} = Out -> Out + end. + %%%------------------------------------------------------------------------------ %%% UntagResource %%%------------------------------------------------------------------------------ @@ -3179,7 +3818,8 @@ update_continuous_backups(TableName, PointInTimeRecoveryEnabled, Opts, Config) Config, "DynamoDB_20120810.UpdateContinuousBackups", [{<<"TableName">>, TableName}, - {<<"PointInTimeRecoverySpecification">>, dynamize_point_in_time_recovery_enabled(PointInTimeRecoveryEnabled)}]), + {<<"PointInTimeRecoverySpecification">>, dynamize_point_in_time_recovery_enabled(PointInTimeRecoveryEnabled)}], + DdbOpts), out(Return, fun(Json, UOpts) -> undynamize_record(continuous_backups_record(), Json, UOpts) end, DdbOpts, #ddb2_describe_continuous_backups.continuous_backups_description). @@ -3230,7 +3870,8 @@ dynamize_update_item_updates_or_expression(Updates) -> {return_values, return_value()} | return_consumed_capacity_opt() | return_item_collection_metrics_opt() | - out_opt(). + out_opt() | + no_request_opt(). -type update_item_opts() :: [update_item_opt()]. -spec update_item_opts() -> opt_table(). @@ -3253,7 +3894,7 @@ update_item_record() -> fun undynamize_item_collection_metrics/2} ]}. --type update_item_return() :: ddb_return(#ddb2_update_item{}, out_item()). +-type update_item_return() :: ddb_return(#ddb2_update_item{} | #ddb2_request{}, out_item()). -spec update_item(table_name(), key(), in_updates() | expression()) -> update_item_return(). update_item(Table, Key, UpdatesOrExpression) -> @@ -3302,7 +3943,8 @@ update_item(Table, Key, UpdatesOrExpression, Opts, Config) -> [{<<"TableName">>, Table}, {<<"Key">>, dynamize_key(Key)}] ++ dynamize_update_item_updates_or_expression(UpdatesOrExpression) - ++ AwsOpts), + ++ AwsOpts, + DdbOpts), out(Return, fun(Json, UOpts) -> undynamize_record(update_item_record(), Json, UOpts) end, DdbOpts, #ddb2_update_item.attributes, {ok, []}). @@ -3365,10 +4007,160 @@ update_global_table(GlobalTableName, ReplicaUpdates, Opts, Config) -> Config, "DynamoDB_20120810.UpdateGlobalTable", [{<<"GlobalTableName">>, GlobalTableName}, - {<<"ReplicaUpdates">>, dynamize_maybe_list(fun dynamize_replica_update/1, ReplicaUpdates)}]), + {<<"ReplicaUpdates">>, dynamize_maybe_list(fun dynamize_replica_update/1, ReplicaUpdates)}], + DdbOpts), out(Return, fun(Json, UOpts) -> undynamize_record(update_global_table_record(), Json, UOpts) end, DdbOpts, #ddb2_update_global_table.global_table_description). +%%%------------------------------------------------------------------------------ +%%% UpdateGlobalTableSettings +%%%------------------------------------------------------------------------------ + +-type update_global_table_settings_return() :: ddb_return(#ddb2_update_global_table_settings{}, [#ddb2_replica_settings_description{}]). + +-type global_table_global_secondary_index_settings_update_opt() :: {index_name, binary()}| + {provisioned_write_capacity_auto_scaling_settings_update, auto_scaling_settings_update_opts()}| + {provisioned_read_capacity_units, read_units()}| + {provisioned_write_capacity_units, write_units()}. + +-type global_table_global_secondary_index_settings_update_opts() :: [global_table_global_secondary_index_settings_update_opt()]. + +-type replica_global_secondary_index_settings_update_opt() :: {index_name, binary()}| + {provisioned_read_capacity_auto_scaling_settings_update, auto_scaling_settings_update_opts()}| + {provisioned_read_capacity_units, read_units()}. + +-type replica_global_secondary_index_settings_update_opts() :: [replica_global_secondary_index_settings_update_opt()]. + +-type replica_settings_update_opt() :: {region_name, binary()}| + {replica_global_secondary_index_settings_update, [global_table_global_secondary_index_settings_update_opts()]}| + {replica_provisioned_read_capacity_auto_scaling_settings_update, auto_scaling_settings_update_opts()}| + {replica_provisioned_read_capacity_units, read_units()}. + +-type replica_settings_update_opts() :: [replica_settings_update_opts()]. + +-spec dynamize_global_table_global_secondary_index_settings_update_opt(global_table_global_secondary_index_settings_update_opt()) -> json_pair(). +dynamize_global_table_global_secondary_index_settings_update_opt({index_name, IndexName}) -> + {<<"IndexName">>, IndexName}; +dynamize_global_table_global_secondary_index_settings_update_opt({provisioned_write_capacity_auto_scaling_settings_update, Update}) -> + {<<"ProvisionedWriteCapacityAutoScalingSettingsUpdate">>, dynamize_auto_scaling_settings_update_opts(Update)}; +dynamize_global_table_global_secondary_index_settings_update_opt({provisioned_read_capacity_units, ProvisionedReadCapacityUnits}) -> + {<<"ProvisionedReadCapacityUnits">>, ProvisionedReadCapacityUnits}; +dynamize_global_table_global_secondary_index_settings_update_opt({provisioned_write_capacity_units, ProvisionedWriteCapacityUnits}) -> + {<<"ProvisionedWriteCapacityUnits">>, ProvisionedWriteCapacityUnits}. + +-spec dynamize_global_table_global_secondary_index_settings_update_opts(global_table_global_secondary_index_settings_update_opts()) -> jsx:json_term(). +dynamize_global_table_global_secondary_index_settings_update_opts(Opts) -> + dynamize_maybe_list(fun dynamize_global_table_global_secondary_index_settings_update_opt/1, Opts). + +-spec dynamize_global_table_global_secondary_index_settings_update([global_table_global_secondary_index_settings_update_opts()]) -> jsx:json_term(). +dynamize_global_table_global_secondary_index_settings_update(Updates) -> + [dynamize_global_table_global_secondary_index_settings_update_opts(Update) || Update <- Updates]. + +-spec dynamize_replica_global_secondary_index_settings_update_opt(replica_global_secondary_index_settings_update_opt()) -> json_pair(). +dynamize_replica_global_secondary_index_settings_update_opt({index_name, IndexName}) -> + {<<"IndexName">>, IndexName}; +dynamize_replica_global_secondary_index_settings_update_opt({provisioned_read_capacity_auto_scaling_settings_update, Update}) -> + {<<"ProvisionedReadCapacityAutoScalingSettingsUpdate">>, dynamize_auto_scaling_settings_update_opts(Update)}; +dynamize_replica_global_secondary_index_settings_update_opt({provisioned_read_capacity_units, Units}) -> + {<<"ProvisionedReadCapacityUnits">>, Units}. + +-spec dynamize_replica_global_secondary_index_settings_update_opts(replica_global_secondary_index_settings_update_opts()) -> jsx:json_term(). +dynamize_replica_global_secondary_index_settings_update_opts(Opts) -> + dynamize_maybe_list(fun dynamize_replica_global_secondary_index_settings_update_opt/1, Opts). + +-spec dynamize_replica_global_secondary_index_settings_update([replica_global_secondary_index_settings_update_opts()]) -> jsx:json_term(). +dynamize_replica_global_secondary_index_settings_update(Updates) -> + [dynamize_replica_global_secondary_index_settings_update_opts(Update) || Update <- Updates]. + +-spec dynamize_replica_settings_update_opt(replica_settings_update_opt()) -> json_pair(). +dynamize_replica_settings_update_opt({region_name, RegionName}) -> + {<<"RegionName">>, RegionName}; +dynamize_replica_settings_update_opt({replica_global_secondary_index_settings_update, Update}) -> + {<<"ReplicaGlobalSecondaryIndexSettingsUpdate">>, dynamize_replica_global_secondary_index_settings_update(Update)}; +dynamize_replica_settings_update_opt({replica_provisioned_read_capacity_auto_scaling_settings_update, Update}) -> + {<<"ReplicaProvisionedReadCapacityAutoScalingSettingsUpdate">>, dynamize_auto_scaling_settings_update_opts(Update)}; +dynamize_replica_settings_update_opt({replica_provisioned_read_capacity_units, Units}) -> + {<<"ReplicaProvisionedReadCapacityUnits">>, Units}. + +-spec dynamize_replica_settings_update_opts(replica_settings_update_opts()) -> jsx:json_term(). +dynamize_replica_settings_update_opts(Opts) -> + dynamize_maybe_list(fun dynamize_replica_settings_update_opt/1, Opts). + +-spec dynamize_replica_settings_updates([replica_settings_update_opts()]) -> jsx:json_term(). +dynamize_replica_settings_updates(Updates) -> + [dynamize_replica_settings_update_opts(Update) || Update <- Updates]. + +-type update_global_table_settings_opt() :: {global_table_billing_mode, billing_mode()}| + {global_table_global_secondary_index_settings_update, [global_table_global_secondary_index_settings_update_opts()]}| + {global_table_provisioned_write_capacity_auto_scaling_settings_update, auto_scaling_settings_update_opts()}| + {global_table_provisioned_write_capacity_units, write_units()}| + {replica_settings_update, [replica_settings_update_opts()]}| + out_opt(). + +-type update_global_table_settings_opts() :: [update_global_table_settings_opt()]. + +-spec update_global_table_settings_opts() -> opt_table(). +update_global_table_settings_opts() -> + [{global_table_billing_mode, <<"GlobalTableBillingMode">>, fun dynamize_billing_mode/1}, + {global_table_global_secondary_index_settings_update, <<"GlobalTableGlobalSecondaryIndexSettingsUpdate">>, fun dynamize_global_table_global_secondary_index_settings_update/1}, + {global_table_provisioned_write_capacity_auto_scaling_settings_update, <<"GlobalTableProvisionedWriteCapacityAutoScalingSettingsUpdate">>, fun dynamize_auto_scaling_settings_update_opts/1}, + {global_table_provisioned_write_capacity_units, <<"GlobalTableProvisionedWriteCapacityUnits">>, fun id/1}, + {replica_settings_update, <<"ReplicaSettingsUpdate">>, fun dynamize_replica_settings_updates/1}]. + +-spec update_global_table_settings_record() -> record_desc(). +update_global_table_settings_record() -> + {#ddb2_update_global_table_settings{}, + [{<<"GlobalTableName">>, #ddb2_update_global_table_settings.global_table_name, fun id/2}, + {<<"ReplicaSettings">>, #ddb2_update_global_table_settings.replica_settings, + fun(V, Opts) -> [undynamize_record(replica_settings_description_record(), I, Opts) || I <- V] end}]}. + + +-spec update_global_table_settings(table_name(), update_global_table_settings_opts()) -> update_global_table_settings_return(). +update_global_table_settings(GlobalTableName, Opts) -> + update_global_table_settings(GlobalTableName, Opts, default_config()). + +%%------------------------------------------------------------------------------ +%% @doc +%% DynamoDB API: +%% [https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateGlobalTableSettings.html] +%% +%% ===Example=== +%% +%% Update global table settings for a table called "Thread" in us-west-2 and eu-west-2. +%% +%% ` +%% ReadUnits = 10, +%% WriteUnits = 10, +%% erlcloud_ddb2:update_global_table_settings( +%% <<"Thread">>, +%% [{global_table_billing_mode, provisioned}, +%% {global_table_global_secondary_index_settings_update, [[{index_name, <<"id-index">>}, +%% {provisioned_write_capacity_units, WriteUnits}]]}, +%% {global_table_provisioned_write_capacity_units, WriteUnits}, +%% {replica_settings_update, [[{region_name, <<"us-west-2">>}, +%% {replica_global_secondary_index_settings_update, [[{index_name, <<"id-index">>}, +%% {provisioned_read_capacity_units, ReadUnits}]]}, +%% {replica_provisioned_read_capacity_units, ReadUnits}], +%% [{region_name, <<"eu-west-2">>}, +%% {replica_global_secondary_index_settings_update, [[{index_name, <<"id-index">>}, +%% {provisioned_read_capacity_units, ReadUnits}]]}, +%% {replica_provisioned_read_capacity_units, ReadUnits}]]}]) +%% ' +%% @end +%%------------------------------------------------------------------------------ + +-spec update_global_table_settings(table_name(), update_global_table_settings_opts(), aws_config()) -> update_global_table_settings_return(). +update_global_table_settings(GlobalTableName, Opts, Config) -> + {AwsOpts, DdbOpts} = opts(update_global_table_settings_opts(), Opts), + Return = erlcloud_ddb_impl:request( + Config, + "DynamoDB_20120810.UpdateGlobalTableSettings", + [{<<"GlobalTableName">>, GlobalTableName} | AwsOpts], + DdbOpts), + out(Return, fun(Json, UOpts) -> undynamize_record(update_global_table_settings_record(), Json, UOpts) end, + DdbOpts, #ddb2_update_global_table_settings.replica_settings). + + %%%------------------------------------------------------------------------------ %%% UpdateTable %%%------------------------------------------------------------------------------ @@ -3380,8 +4172,35 @@ update_global_table(GlobalTableName, ReplicaUpdates, Opts, Config) -> global_secondary_index_def(). -type global_secondary_index_updates() :: maybe_list(global_secondary_index_update()). +-type provisioned_throughput_override_opt() :: {read_capacity_units, non_neg_integer()}. +-type provisioned_throughput_override_opts() :: [provisioned_throughput_override_opt()]. + +-type create_replication_group_member_action_opt() :: {global_secondary_indexes, binary()}| + {kms_master_key_id, binary()}| + {provisioned_throughput_override, provisioned_throughput_override_opts()}| + {region_name, binary()}. + +-type create_replication_group_member_action_opts() :: [create_replication_group_member_action_opt()]. + +-type delete_replication_group_member_action_opt() :: {region_name, binary()}. + +-type delete_replication_group_member_action_opts() :: [create_replication_group_member_action_opt()]. + +-type update_replication_group_member_action_opt() :: {global_secondary_indexes, binary()}| + {kms_master_key_id, binary()}| + {provisioned_throughput_override, provisioned_throughput_override_opts()}| + {region_name, binary()}. + +-type update_replication_group_member_action_opts() :: [update_replication_group_member_action_opt()]. + +-type replication_group_update_opt() :: {create, create_replication_group_member_action_opts()}| + {delete, delete_replication_group_member_action_opts()}| + {update, update_replication_group_member_action_opts()}. +-type replication_group_update_opts() :: [replication_group_update_opt()]. + -spec dynamize_global_secondary_index_update(global_secondary_index_update()) -> jsx:json_term(). -dynamize_global_secondary_index_update({IndexName, ReadUnits, WriteUnits}) -> +dynamize_global_secondary_index_update({IndexName, ReadUnits, WriteUnits}) + when is_integer(ReadUnits), is_integer(WriteUnits) -> [{<<"Update">>, [ {<<"IndexName">>, IndexName}, {<<"ProvisionedThroughput">>, dynamize_provisioned_throughput({ReadUnits, WriteUnits})} @@ -3397,20 +4216,81 @@ dynamize_global_secondary_index_update(Index) -> dynamize_global_secondary_index_updates(Updates) -> dynamize_maybe_list(fun dynamize_global_secondary_index_update/1, Updates). --type update_table_opt() :: {provisioned_throughput, {read_units(), write_units()}} | +-spec dynamize_provisioned_throughput_override_opt(provisioned_throughput_override_opt()) -> json_pair(). +dynamize_provisioned_throughput_override_opt({read_capacity_units, ReadCapacityUnits}) -> + {<<"ReadCapacityUnits">>, ReadCapacityUnits}. + +-spec dynamize_provisioned_throughput_override_opts(provisioned_throughput_override_opts()) -> jsx:json_term(). +dynamize_provisioned_throughput_override_opts(Opts) -> + dynamize_maybe_list(fun dynamize_provisioned_throughput_override_opt/1, Opts). + +-spec dynamize_create_replication_group_member_action_opt(create_replication_group_member_action_opt()) -> json_pair(). +dynamize_create_replication_group_member_action_opt({global_secondary_indexes, GlobalSecondaryIndexes}) -> + {<<"GlobalSecondaryIndexes">>, GlobalSecondaryIndexes}; +dynamize_create_replication_group_member_action_opt({kms_master_key_id, KMSMasterKeyId}) -> + {<<"KMSMasterKeyId">>, KMSMasterKeyId}; +dynamize_create_replication_group_member_action_opt({provisioned_throughput_override, ProvisionedThroughputOverride}) -> + {<<"ProvisionedThroughputOverride">>, dynamize_provisioned_throughput_override_opts(ProvisionedThroughputOverride)}; +dynamize_create_replication_group_member_action_opt({region_name, RegionName}) -> + {<<"RegionName">>, RegionName}. + +-spec dynamize_create_replication_group_member_action_opts(create_replication_group_member_action_opts()) -> jsx:json_term(). +dynamize_create_replication_group_member_action_opts(Opts) -> + dynamize_maybe_list(fun dynamize_create_replication_group_member_action_opt/1, Opts). + +-spec dynamize_delete_replication_group_member_action_opt(delete_replication_group_member_action_opt()) -> json_pair(). +dynamize_delete_replication_group_member_action_opt({region_name, RegionName}) -> + {<<"RegionName">>, RegionName}. + +-spec dynamize_delete_replication_group_member_action_opts(delete_replication_group_member_action_opts()) -> jsx:json_term(). +dynamize_delete_replication_group_member_action_opts(Opts) -> + dynamize_maybe_list(fun dynamize_delete_replication_group_member_action_opt/1, Opts). + +-spec dynamize_update_replication_group_member_action_opt(update_replication_group_member_action_opt()) -> json_pair(). +dynamize_update_replication_group_member_action_opt({global_secondary_indexes, GlobalSecondaryIndexes}) -> + {<<"GlobalSecondaryIndexes">>, GlobalSecondaryIndexes}; +dynamize_update_replication_group_member_action_opt({kms_master_key_id, KMSMasterKeyId}) -> + {<<"KMSMasterKeyId">>, KMSMasterKeyId}; +dynamize_update_replication_group_member_action_opt({provisioned_throughput_override, ProvisionedThroughputOverride}) -> + {<<"ProvisionedThroughputOverride">>, ProvisionedThroughputOverride}; +dynamize_update_replication_group_member_action_opt({region_name, RegionName}) -> + {<<"RegionName">>, RegionName}. + +-spec dynamize_update_replication_group_member_action_opts(update_replication_group_member_action_opts()) -> jsx:json_term(). +dynamize_update_replication_group_member_action_opts(Opts) -> + dynamize_maybe_list(fun dynamize_update_replication_group_member_action_opt/1, Opts). + +-spec dynamize_replication_group_update(replication_group_update_opt()) -> json_pair(). +dynamize_replication_group_update({create, Create}) -> + {<<"Create">>, dynamize_create_replication_group_member_action_opts(Create)}; +dynamize_replication_group_update({delete, Delete}) -> + {<<"Delete">>, dynamize_delete_replication_group_member_action_opts(Delete)}; +dynamize_replication_group_update({update, Update}) -> + {<<"Update">>, dynamize_update_replication_group_member_action_opts(Update)}. + +-spec dynamize_replication_group_updates(replication_group_update_opts()) -> jsx:json_term(). +dynamize_replication_group_updates(Updates) -> + [dynamize_maybe_list(fun dynamize_replication_group_update/1, Update) || Update <- Updates]. + +-type update_table_opt() :: {billing_mode, billing_mode()} | + {provisioned_throughput, {read_units(), write_units()}} | {attribute_definitions, attr_defs()} | {global_secondary_index_updates, global_secondary_index_updates()} | {stream_specification, stream_specification()} | + boolean_opt(deletion_protection_enabled) | out_opt(). -type update_table_opts() :: [update_table_opt()]. -spec update_table_opts() -> opt_table(). update_table_opts() -> - [{provisioned_throughput, <<"ProvisionedThroughput">>, fun dynamize_provisioned_throughput/1}, + [{billing_mode, <<"BillingMode">>, fun dynamize_billing_mode/1}, + {provisioned_throughput, <<"ProvisionedThroughput">>, fun dynamize_provisioned_throughput/1}, {attribute_definitions, <<"AttributeDefinitions">>, fun dynamize_attr_defs/1}, {global_secondary_index_updates, <<"GlobalSecondaryIndexUpdates">>, fun dynamize_global_secondary_index_updates/1}, - {stream_specification, <<"StreamSpecification">>, fun dynamize_stream_specification/1}]. + {stream_specification, <<"StreamSpecification">>, fun dynamize_stream_specification/1}, + {replica_updates, <<"ReplicaUpdates">>, fun dynamize_replication_group_updates/1}, + {deletion_protection_enabled, <<"DeletionProtectionEnabled">>, fun id/1}]. -spec update_table_record() -> record_desc(). update_table_record() -> @@ -3447,7 +4327,8 @@ update_table(Table, Opts, Config) when is_list(Opts) -> Return = erlcloud_ddb_impl:request( Config, "DynamoDB_20120810.UpdateTable", - [{<<"TableName">>, Table} | AwsOpts]), + [{<<"TableName">>, Table} | AwsOpts], + DdbOpts), out(Return, fun(Json, UOpts) -> undynamize_record(update_table_record(), Json, UOpts) end, DdbOpts, #ddb2_update_table.table_description); update_table(Table, ReadUnits, WriteUnits) -> @@ -3465,6 +4346,139 @@ update_table(Table, ReadUnits, WriteUnits, Opts, Config) -> update_table(Table, [{provisioned_throughput, {ReadUnits, WriteUnits}} | Opts], Config). +%%%------------------------------------------------------------------------------ +%%% UpdateTableReplicaAutoScaling +%%%------------------------------------------------------------------------------ + +-type update_table_replica_auto_scaling_return() :: ddb_return(#ddb2_update_table_replica_auto_scaling{}, #ddb2_table_auto_scaling_description{}). + +-type global_secondary_index_auto_scaling_update_opt() :: {index_name, binary()}| + {provisioned_read_capacity_auto_scaling_update, auto_scaling_settings_update_opts()}. + +-type global_secondary_index_auto_scaling_update_opts() :: [global_secondary_index_auto_scaling_update_opt()]. + +-type replica_global_secondary_index_auto_scaling_opt() :: {index_name, binary()}| + {provisioned_read_capacity_auto_scaling_update, auto_scaling_settings_update_opts()}. + +-type replica_global_secondary_index_auto_scaling_opts() :: [replica_global_secondary_index_auto_scaling_opt()]. + +-type replica_auto_scaling_update_opt() :: {region_name, binary()}| + {replica_global_secondary_index_updates, [replica_global_secondary_index_auto_scaling_opts()]}| + {replica_provisioned_read_capacity_auto_scaling_update, auto_scaling_settings_update_opts()}. +-type replica_auto_scaling_update_opts() :: [replica_auto_scaling_update_opt()]. + +-spec dynamize_global_secondary_index_auto_scaling_update_opt(global_secondary_index_auto_scaling_update_opt()) -> json_pair(). +dynamize_global_secondary_index_auto_scaling_update_opt({index_name, IndexName}) -> + {<<"IndexName">>, IndexName}; +dynamize_global_secondary_index_auto_scaling_update_opt({provisioned_write_capacity_auto_scaling_update, ProvisionedWriteCapacityAutoScalingUpdate}) -> + {<<"ProvisionedWriteCapacityAutoScalingUpdate">>, dynamize_maybe_list(fun dynamize_auto_scaling_settings_update_opt/1, + ProvisionedWriteCapacityAutoScalingUpdate)}. + +-spec dynamize_global_secondary_index_auto_scaling_update_opts([replica_global_secondary_index_auto_scaling_opts()]) -> jsx:json_term(). +dynamize_global_secondary_index_auto_scaling_update_opts(GlobalSecondaryIndexUpdates) -> + [dynamize_maybe_list(fun dynamize_global_secondary_index_auto_scaling_update_opt/1, + Update) || Update <- GlobalSecondaryIndexUpdates]. + +-spec dynamize_replica_global_secondary_index_auto_scaling_update_opt(replica_global_secondary_index_auto_scaling_opt()) -> json_pair(). +dynamize_replica_global_secondary_index_auto_scaling_update_opt({index_name, IndexName}) -> + {<<"IndexName">>, IndexName}; +dynamize_replica_global_secondary_index_auto_scaling_update_opt({provisioned_read_capacity_auto_scaling_update, ProvisionedReadCapacityAutoScalingUpdate}) -> + {<<"ProvisionedReadCapacityAutoScalingUpdate">>, dynamize_auto_scaling_settings_update_opts(ProvisionedReadCapacityAutoScalingUpdate)}. + +-spec dynamize_replica_global_secondary_index_auto_scaling_update_opts(replica_global_secondary_index_auto_scaling_opts()) -> jsx:json_term(). +dynamize_replica_global_secondary_index_auto_scaling_update_opts(Opts) -> + dynamize_maybe_list(fun dynamize_replica_global_secondary_index_auto_scaling_update_opt/1, + Opts). + +-spec dynamize_replica_auto_scaling_update(replica_auto_scaling_update_opt()) -> json_pair(). +dynamize_replica_auto_scaling_update({region_name, RegionName}) -> + {<<"RegionName">>, RegionName}; +dynamize_replica_auto_scaling_update({replica_global_secondary_index_updates, ReplicaGlobalSecondaryIndexUpdates}) -> + {<<"ReplicaGlobalSecondaryIndexUpdates">>, [dynamize_replica_global_secondary_index_auto_scaling_update_opts(Update) || Update <- ReplicaGlobalSecondaryIndexUpdates]}; +dynamize_replica_auto_scaling_update({replica_provisioned_read_capacity_auto_scaling_update, ReplicaProvisionedReadCapacityAutoScalingUpdate}) -> + {<<"ReplicaProvisionedReadCapacityAutoScalingUpdate">>, dynamize_auto_scaling_settings_update_opts(ReplicaProvisionedReadCapacityAutoScalingUpdate)}. + +-spec dynamize_replica_auto_scaling_updates([replica_auto_scaling_update_opts()]) -> jsx:json_term(). +dynamize_replica_auto_scaling_updates(ReplicaUpdates) -> + [dynamize_maybe_list(fun dynamize_replica_auto_scaling_update/1, + Update) || Update <- ReplicaUpdates]. + +-spec maybe_update_config_timeout(aws_config(), MinDesiredTimeout :: pos_integer()) -> aws_config(). +maybe_update_config_timeout(Config, _MinDesiredTimeout) -> + UpdatedTimeout = erlcloud_aws:get_timeout(Config), + Config#aws_config{timeout = UpdatedTimeout}. + +-type update_table_replica_auto_scaling_opt() :: {global_secondary_index_updates, [global_secondary_index_auto_scaling_update_opts()]}| + {provisioned_write_capacity_auto_scaling_update, auto_scaling_settings_update_opts()}| + {replica_updates, [replica_auto_scaling_update_opts()]}| + out_opt(). +-type update_table_replica_auto_scaling_opts() :: [update_table_replica_auto_scaling_opt()]. + +-spec update_table_replica_auto_scaling_opts() -> opt_table(). +update_table_replica_auto_scaling_opts() -> + [{global_secondary_index_updates, <<"GlobalSecondaryIndexUpdates">>, + fun dynamize_global_secondary_index_auto_scaling_update_opts/1}, + {provisioned_write_capacity_auto_scaling_update, <<"ProvisionedWriteCapacityAutoScalingUpdate">>, + fun dynamize_auto_scaling_settings_update_opts/1}, + {replica_updates, <<"ReplicaUpdates">>, + fun dynamize_replica_auto_scaling_updates/1}]. + +-spec update_table_replica_auto_scaling_record() -> record_desc(). +update_table_replica_auto_scaling_record() -> + {#ddb2_update_table_replica_auto_scaling{}, + [{<<"TableAutoScalingDescription">>, #ddb2_update_table_replica_auto_scaling.table_auto_scaling_description, + fun(V, Opts) -> undynamize_record(table_auto_scaling_description_record(), V, Opts) end}]}. + +-spec update_table_replica_auto_scaling(table_name(), update_table_replica_auto_scaling_opts()) -> update_table_replica_auto_scaling_return(). +update_table_replica_auto_scaling(Table, Opts) -> + update_table_replica_auto_scaling(Table, Opts, default_config()). + +%%------------------------------------------------------------------------------ +%% @doc +%% DynamoDB API: +%% [http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateTableReplicaAutoScaling.html] +%% +%% Note: This method only applies to Version 2019.11.21 of global tables. +%% Note: +%% +%% ===Example=== +%% +%% Update table "Thread" to scale between 10 and 20 provisioned read and write units uniformly across replica regions. +%% ``` +%% AutoScalingSettingsUpdate = [{maximum_units, 20}, +%% {minimum_units, 10}, +%% {scaling_policy_update, [{target_tracking_scaling_policy_configuration, [{target_value, 60.0}]}]}], +%% erlcloud_ddb2:update_table_replica_auto_scaling( +%% <<"Thread">>, +%% [{global_secondary_index_updates, [[{index_name, <<"id-index">>}, +%% {provisioned_write_capacity_auto_scaling_update, AutoScalingSettingsUpdate}]]}, +%% {provisioned_write_capacity_auto_scaling_update, AutoScalingSettingsUpdate}, +%% {replica_updates, [[{region_name, <<"us-west-2">>}, +%% {replica_global_secondary_index_updates, [[{index_name, <<"id-index">>}, +%% {provisioned_read_capacity_auto_scaling_update, AutoScalingSettingsUpdate}]]}, +%% {replica_provisioned_read_capacity_auto_scaling_update, AutoScalingSettingsUpdate}], +%% [{region_name, <<"eu-west-2">>}, +%% {replica_global_secondary_index_updates, [[{index_name, <<"id-index">>}, +%% {provisioned_read_capacity_auto_scaling_update, AutoScalingSettingsUpdate}]]}, +%% {replica_provisioned_read_capacity_auto_scaling_update, AutoScalingSettingsUpdate}]]}]). +%% ''' +%% @end +%%------------------------------------------------------------------------------ +-spec update_table_replica_auto_scaling(table_name(), update_table_replica_auto_scaling_opts(), aws_config()) -> update_table_replica_auto_scaling_return(). +update_table_replica_auto_scaling(Table, Opts, Config) when is_list(Opts) -> + % The default timeout for this endpoint is updated to 25 seconds below due to the endpoint + % regularly taking around 20 seconds during testing. + % Any non-default timeout already written to the config will not be overridden. + UpdatedConfig = maybe_update_config_timeout(Config, 25000), + {AwsOpts, DdbOpts} = opts(update_table_replica_auto_scaling_opts(), Opts), + Return = erlcloud_ddb_impl:request( + UpdatedConfig, + "DynamoDB_20120810.UpdateTableReplicaAutoScaling", + [{<<"TableName">>, Table} | AwsOpts], + DdbOpts), + out(Return, fun(Json, UOpts) -> undynamize_record(update_table_replica_auto_scaling_record(), Json, UOpts) end, + DdbOpts, #ddb2_update_table_replica_auto_scaling.table_auto_scaling_description). + %%%------------------------------------------------------------------------------ %%% UpdateTimeToLive %%%------------------------------------------------------------------------------ @@ -3533,7 +4547,8 @@ update_time_to_live(Table, Opts, Config) when is_list(Opts) -> Return = erlcloud_ddb_impl:request( Config, "DynamoDB_20120810.UpdateTimeToLive", - Body), + Body, + DdbOpts), out(Return, fun(Json, UOpts) -> undynamize_record(update_time_to_live_record(), Json, UOpts) end, DdbOpts, #ddb2_update_time_to_live.time_to_live_specification); diff --git a/src/erlcloud_ddb_impl.erl b/src/erlcloud_ddb_impl.erl index 7ba4c03f1..a1044204e 100644 --- a/src/erlcloud_ddb_impl.erl +++ b/src/erlcloud_ddb_impl.erl @@ -27,7 +27,7 @@ %% @author Ransom Richardson %% @doc %% -%% Implementation of requests to DynamoDB. This code is shared accross +%% Implementation of requests to DynamoDB. This code is shared across %% all API versions. %% %% The pluggable retry function provides a way to customize the retry behavior, as well @@ -68,15 +68,23 @@ ]). %% Internal impl api --export([request/3]). +-export([request/3, + request/4]). -export_type([json_return/0, attempt/0, retry_fun/0]). --type json_return() :: ok | {ok, jsx:json_term()} | {error, term()}. +-type json_return() :: ok | {ok, jsx:json_term()} | {error, term()} | {ok, #ddb2_request{}}. -type operation() :: string(). + + -spec request(aws_config(), operation(), jsx:json_term()) -> json_return(). request(Config0, Operation, Json) -> + request(Config0, Operation, Json, []). + +-spec request(aws_config(), operation(), jsx:json_term(), erlcloud_ddb2:ddb_opts()) -> json_return(). +request(Config0, Operation, Json, DdbOpts) -> + NoRequest = proplists:get_value(no_request, DdbOpts, false), Body = case Json of [] -> <<"{}">>; _ -> jsx:encode(Json) @@ -84,7 +92,7 @@ request(Config0, Operation, Json) -> case erlcloud_aws:update_config(Config0) of {ok, Config} -> Headers = headers(Config, Operation, Body), - request_and_retry(Config, Headers, Body, {attempt, 1}); + maybe_request_and_retry(Config, Headers, Body, Json, {attempt, 1}, NoRequest); {error, Reason} -> {error, Reason} end. @@ -99,6 +107,12 @@ request(Config0, Operation, Json) -> %% This algorithm is similar, except that it waits a random interval up to 2^(Attempt-2)*100ms. The average %% wait time should be the same as boto. +-spec maybe_request_and_retry(aws_config(), ddb2_req_headers(), jsx:json_text(), jsx:json_term(), {attempt, non_neg_integer()}, boolean()) -> json_return(). +maybe_request_and_retry(Config, Headers, Body, _Json, Attempt, false) -> + request_and_retry(Config, Headers, Body, Attempt); +maybe_request_and_retry(_Config, Headers, Body, Json, _Attempt, true) -> + {ok, #ddb2_request{headers = Headers, body = Body, json = Json}}. + %% TODO refactor retry logic so that it can be used by all requests and move to erlcloud_aws -define(NUM_ATTEMPTS, 10). @@ -151,7 +165,7 @@ request_id_from_error(#ddb2_error{response_headers = Headers}) when is_list(Head request_id_from_error(#ddb2_error{}) -> undefined. -%% For backwards compatability the default reason does not include the request id. +%% For backwards compatibility the default reason does not include the request id. %% This function will update the error to have reason containing the request id. -spec error_reason2(#ddb2_error{}) -> #ddb2_error{}. error_reason2(#ddb2_error{error_type = http} = Error) -> @@ -176,8 +190,7 @@ retry_v1_wrap(#ddb2_error{should_retry = false} = Error, _) -> retry_v1_wrap(Error, RetryFun) -> RetryFun(Error#ddb2_error.attempt, Error#ddb2_error.reason). --type headers() :: [{string(), string()}]. --spec request_and_retry(aws_config(), headers(), jsx:json_text(), attempt()) -> +-spec request_and_retry(aws_config(), ddb2_req_headers(), jsx:json_text(), attempt()) -> ok | {ok, jsx:json_term()} | {error, term()}. request_and_retry(_, _, _, {error, Reason}) -> {error, Reason}; @@ -193,7 +206,7 @@ request_and_retry(Config, Headers, Body, {attempt, Attempt}) -> {ok, {{200, _}, _, RespBody}} -> %% TODO check crc - {ok, jsx:decode(RespBody)}; + {ok, jsx:decode(RespBody, [{return_maps, false}])}; Error -> DDBError = #ddb2_error{attempt = Attempt, @@ -230,7 +243,7 @@ client_error(Body, DDBError) -> false -> DDBError#ddb2_error{error_type = http, should_retry = false}; true -> - Json = jsx:decode(Body), + Json = jsx:decode(Body, [{return_maps, false}]), case proplists:get_value(<<"__type">>, Json) of undefined -> DDBError#ddb2_error{error_type = http, should_retry = false}; @@ -240,11 +253,19 @@ client_error(Body, DDBError) -> [_, Type] when Type =:= <<"ProvisionedThroughputExceededException">> orelse Type =:= <<"ThrottlingException">> -> - DDBError#ddb2_error{error_type = ddb, + DDBError#ddb2_error{error_type = ddb, should_retry = true, reason = {Type, Message}}; + [_, Type] when + Type =:= <<"TransactionCanceledException">> -> + CancellationReasons0 = proplists:get_value(<<"CancellationReasons">>, Json, []), + CancellationReasons = [{proplists:get_value(<<"Code">>, R), + proplists:get_value(<<"Message">>, R, null)} || R <- CancellationReasons0], + DDBError#ddb2_error{error_type = ddb, + should_retry = should_retry_canceled_transaction(CancellationReasons), + reason = {Type, {Message, CancellationReasons}}}; [_, Type] -> - DDBError#ddb2_error{error_type = ddb, + DDBError#ddb2_error{error_type = ddb, should_retry = false, reason = {Type, Message}}; _ -> @@ -253,7 +274,7 @@ client_error(Body, DDBError) -> end end. --spec headers(aws_config(), string(), binary()) -> headers(). +-spec headers(aws_config(), string(), binary()) -> ddb2_req_headers(). headers(Config, Operation, Body) -> Headers = [{"host", Config#aws_config.ddb_host}, {"x-amz-target", Operation}], @@ -268,3 +289,10 @@ port_spec(#aws_config{ddb_port=80}) -> port_spec(#aws_config{ddb_port=Port}) -> [":", erlang:integer_to_list(Port)]. +-spec should_retry_canceled_transaction(proplists:proplist()) -> boolean(). +should_retry_canceled_transaction(CancellationReasons) -> + %% Retry canceled transaction if cancellation reasons are either: + %% `None', `ThrottlingError' and/or `ProvisionedThroughputExceeded' + lists:filter(fun({Reason, _Message}) -> + not lists:member(Reason, [<<"None">>, <<"ThrottlingError">>, <<"ProvisionedThroughputExceeded">>]) + end, CancellationReasons) == []. diff --git a/src/erlcloud_ddb_streams.erl b/src/erlcloud_ddb_streams.erl index af6cde2c8..83575709b 100644 --- a/src/erlcloud_ddb_streams.erl +++ b/src/erlcloud_ddb_streams.erl @@ -65,6 +65,9 @@ list_streams/0, list_streams/1, list_streams/2 ]). +%%% Exported DynamoDB Streams Undynamizers +-export([undynamize_ddb_streams_record/1, undynamize_ddb_streams_record/2]). + -export_type( [aws_region/0, event_id/0, @@ -217,6 +220,9 @@ undynamize_value_untyped({<<"BS">>, Values}, _) -> [base64:decode(Value) || Value <- Values]; undynamize_value_untyped({<<"L">>, List}, Opts) -> [undynamize_value_untyped(Value, Opts) || [Value] <- List]; +undynamize_value_untyped({<<"M">>, [{}]}, _Opts) -> + %% jsx returns [{}] for empty objects + []; undynamize_value_untyped({<<"M">>, Map}, Opts) -> [undynamize_attr_untyped(Attr, Opts) || Attr <- Map]. @@ -269,6 +275,8 @@ undynamize_value_typed({<<"BS">>, Values}, _) -> {bs, [base64:decode(Value) || Value <- Values]}; undynamize_value_typed({<<"L">>, List}, Opts) -> {l, [undynamize_value_typed(Value, Opts) || [Value] <- List]}; +undynamize_value_typed({<<"M">>, [{}]}, _Opts) -> + {m, []}; undynamize_value_typed({<<"M">>, Map}, Opts) -> {m, [undynamize_attr_typed(Attr, Opts) || Attr <- Map]}. @@ -467,7 +475,8 @@ stream_description_record() -> -spec stream_record_record() -> record_desc(). stream_record_record() -> {#ddb_streams_stream_record{}, - [{<<"Keys">>, #ddb_streams_stream_record.keys, fun undynamize_key/2}, + [{<<"ApproximateCreationDateTime">>, #ddb_streams_stream_record.approximate_creation_date_time, fun id/2}, + {<<"Keys">>, #ddb_streams_stream_record.keys, fun undynamize_key/2}, {<<"NewImage">>, #ddb_streams_stream_record.new_image, fun undynamize_item/2}, {<<"OldImage">>, #ddb_streams_stream_record.old_image, fun undynamize_item/2}, {<<"SequenceNumber">>, #ddb_streams_stream_record.sequence_number, fun id/2}, @@ -789,6 +798,32 @@ list_streams(Opts, Config) -> fun(Json, UOpts) -> undynamize_record(list_streams_record(), Json, UOpts) end, DdbStreamsOpts, #ddb_streams_list_streams.streams). + +%%%------------------------------------------------------------------------------ +%%% Exported Undynamizers +%%%------------------------------------------------------------------------------ +-type undynamize_ddb_streams_record_return() :: ddb_streams_return(#ddb_streams_record{}, #ddb_streams_stream_record{}). + +-spec undynamize_ddb_streams_record(json_term()) -> undynamize_ddb_streams_record_return(). +undynamize_ddb_streams_record(Return) -> + undynamize_ddb_streams_record(Return, []). + +%%------------------------------------------------------------------------------ +%% @doc Undynamizes a DynamoDB streams record. +%% This function can be used to undynamize the jsx-decoded Data field of a +%% record retrieved from Kinesis, after enabling Kinesis Data Streaming for +%% a DynamoDB table. +%% +%% Change Data Capture for Kinesis Data Streams with DynamoDB: +%% [http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/kds.html] +%%------------------------------------------------------------------------------ +-spec undynamize_ddb_streams_record(json_term(), ddb_streams_opts()) -> undynamize_ddb_streams_record_return(). +undynamize_ddb_streams_record(Return, Opts) -> + out({ok, Return}, + fun(Json, UOpts) -> undynamize_record(record_record(), Json, UOpts) end, + Opts, #ddb_streams_record.dynamodb). + + %%%------------------------------------------------------------------------------ %%% Request %%%------------------------------------------------------------------------------ @@ -823,7 +858,7 @@ request2(Config, Operation, Json) -> request_to_return(#aws_request{response_type = ok, response_body = Body}) -> %% TODO check crc - {ok, jsx:decode(Body)}; + {ok, jsx:decode(Body, [{return_maps, false}])}; request_to_return(#aws_request{response_type = error, error_type = aws, httpc_error_reason = undefined, @@ -867,7 +902,7 @@ client_error(#aws_request{response_body = Body} = Request) -> false -> Request#aws_request{should_retry = false}; true -> - Json = jsx:decode(Body), + Json = jsx:decode(Body, [{return_maps, false}]), case proplists:get_value(<<"__type">>, Json) of undefined -> Request#aws_request{should_retry = false}; diff --git a/src/erlcloud_ddb_util.erl b/src/erlcloud_ddb_util.erl index 6759b1ebf..24af5b189 100644 --- a/src/erlcloud_ddb_util.erl +++ b/src/erlcloud_ddb_util.erl @@ -21,10 +21,12 @@ %%% DynamoDB Higher Layer API -export([delete_all/2, delete_all/3, delete_all/4, delete_hash_key/3, delete_hash_key/4, delete_hash_key/5, - get_all/2, get_all/3, get_all/4, + get_all/2, get_all/3, get_all/4, get_all/5, put_all/2, put_all/3, put_all/4, + list_tables_all/0, list_tables_all/1, q_all/2, q_all/3, q_all/4, scan_all/1, scan_all/2, scan_all/3, + wait_for_table_active/1, wait_for_table_active/2, wait_for_table_active/3, wait_for_table_active/4, write_all/2, write_all/3, write_all/4 ]). @@ -230,6 +232,28 @@ batch_get_retry(RequestItems, DdbOpts, Config, Acc) -> batch_get_retry(Unprocessed, DdbOpts, Config, Items ++ Acc) end. +%%%------------------------------------------------------------------------------ +%%% list_tables_all +%%%------------------------------------------------------------------------------ + +list_tables_all() -> + list_tables_all(default_config()). + +-spec list_tables_all(aws_config()) -> {ok, [table_name()]} | {error, any()}. +list_tables_all(Config) -> + do_list_tables_all(undefined, Config, []). + +do_list_tables_all(LastTable, Config, Result) -> + Options = [{exclusive_start_table_name, LastTable}, {out, record}], + case erlcloud_ddb2:list_tables(Options, Config) of + {ok, #ddb2_list_tables{table_names = TableNames, last_evaluated_table_name = undefined}} -> + {ok, flatreverse([TableNames, Result])}; + {ok, #ddb2_list_tables{table_names = TableNames, last_evaluated_table_name = LastTableName}} -> + do_list_tables_all(LastTableName, Config, flatreverse([TableNames, Result])); + {error, _} = Error -> + Error + end. + %%%------------------------------------------------------------------------------ %%% put_all %%%------------------------------------------------------------------------------ @@ -477,6 +501,51 @@ batch_write_retry(RequestItems, Config) -> batch_write_retry(Unprocessed, Config) end. +%%------------------------------------------------------------------------------ +%% @doc +%% wait until table_status==active. +%% +%% ===Example=== +%% +%% ` +%% erlcloud_ddb2:wait_for_table_active(<<"TableName">>, 3000, 40, Config) +%% ' +%% @end +%%------------------------------------------------------------------------------ + +-spec wait_for_table_active(table_name(), pos_integer() | infinity, non_neg_integer() | infinity, aws_config()) -> + ok | {error, deleting | retry_threshold_exceeded | any()}. +wait_for_table_active(Table, Interval, RetryTimes, Config) when is_binary(Table), Interval > 0, RetryTimes >= 0 -> + case erlcloud_ddb2:describe_table(Table, [{out, record}], Config) of + {ok, #ddb2_describe_table{table = #ddb2_table_description{table_status = active}}} -> + ok; + {ok, #ddb2_describe_table{table = #ddb2_table_description{table_status = deleting}}} -> + {error, deleting}; + {ok, _} -> + case RetryTimes of + infinity -> + timer:sleep(Interval), + wait_for_table_active(Table, infinity, RetryTimes, Config); + 1 -> + {error, retry_threshold_exceeded}; + _ -> + timer:sleep(Interval), + wait_for_table_active(Table, Interval, RetryTimes - 1, Config) + end; + {error, Reason} -> + {error, Reason} + end. + +wait_for_table_active(Table, Interval, RetryTimes) -> + wait_for_table_active(Table, Interval, RetryTimes, default_config()). + +wait_for_table_active(Table, AWSCfg) -> + wait_for_table_active(Table, 3000, 100, AWSCfg). + +wait_for_table_active(Table) -> + wait_for_table_active(Table, default_config()). + + write_all_result([ok | T]) -> write_all_result(T); write_all_result([{error, Reason} | _]) -> diff --git a/src/erlcloud_directconnect.erl b/src/erlcloud_directconnect.erl index eb3beaf8f..149df41e1 100644 --- a/src/erlcloud_directconnect.erl +++ b/src/erlcloud_directconnect.erl @@ -416,7 +416,7 @@ dc_query(Operation, Params, Config) -> [{<<"content-type">>, <<"application/x-amz-json-1.1">>} | Headers], Body, 1000, Config)) of {ok, {_RespHeader, RespBody}} -> - {ok, jsx:decode(RespBody)}; + {ok, jsx:decode(RespBody, [{return_maps, false}])}; {error, Reason} -> {error, Reason} end. diff --git a/src/erlcloud_ec2.erl b/src/erlcloud_ec2.erl index 87220c00b..f37b1dc93 100644 --- a/src/erlcloud_ec2.erl +++ b/src/erlcloud_ec2.erl @@ -21,7 +21,7 @@ %% Availability Zones and Regions describe_availability_zones/0, describe_availability_zones/1, describe_availability_zones/2, - describe_regions/0, describe_regions/1, describe_regions/2, + describe_regions/0, describe_regions/1, describe_regions/2, describe_regions/3, describe_regions/4, %% Elastic Block Store attach_volume/3, attach_volume/4, @@ -193,8 +193,21 @@ % VPC peering connections describe_vpc_peering_connections/0, describe_vpc_peering_connections/1, - describe_vpc_peering_connections/2, describe_vpc_peering_connections/3 - + describe_vpc_peering_connections/2, describe_vpc_peering_connections/3, + + % Launch templates + describe_launch_templates/0, describe_launch_templates/1, + describe_launch_templates/2, describe_launch_templates/3, + describe_launch_templates/4, describe_launch_templates/5, + + describe_launch_templates_all/0, describe_launch_templates_all/1, + describe_launch_templates_all/2, describe_launch_templates_all/3, + + describe_launch_template_versions/1, describe_launch_template_versions/3, + describe_launch_template_versions/4, describe_launch_template_versions/5, + describe_launch_template_versions/6, describe_launch_template_versions/7, + + describe_launch_template_versions_all/2, describe_launch_template_versions_all/3 ]). -import(erlcloud_xml, [get_text/1, get_text/2, get_text/3, get_bool/2, get_list/2, get_integer/2]). @@ -228,8 +241,10 @@ -define(NAT_GATEWAYS_MR_MAX, 1000). -define(FLOWS_MR_MIN, 1). -define(FLOWS_MR_MAX, 1000). +-define(LAUNCH_TEMPLATES_MR_MIN, 1). +-define(LAUNCH_TEMPLATES_MR_MAX, 200). --type filter_list() :: [{string() | atom(),[string()]}] | none. +-type filter_list() :: [{string() | atom(),[string()] | string()}] | none. -type ec2_param_list() :: [{string(),string()}]. -type ec2_selector() :: proplist(). -type ec2_token() :: string() | undefined. @@ -253,6 +268,7 @@ -type vpc_peering_connection_ids() :: [string()]. -type flow_id() :: string(). -type ec2_flow_ids() :: [flow_id()]. +-type launch_template_ids() :: [string()]. -spec new(string(), string()) -> aws_config(). @@ -508,7 +524,11 @@ bundle_instance(InstanceID, Bucket, Prefix, AccessKeyID, UploadPolicy, Error end. +-spec extract_bundle_task(Nodes :: [xmerl_xpath_doc_nodes()]) -> proplist(); + (Node :: xmerl_xpath_doc_nodes()) -> proplist(). extract_bundle_task([Node]) -> + extract_bundle_task(Node); +extract_bundle_task(Node) -> [ {instance_id, get_text("instanceId", Node)}, {bundle_id, get_text("bundleId", Node)}, @@ -1351,7 +1371,8 @@ extract_image(Node) -> {creation_date, erlcloud_xml:get_time("creationDate", Node)}, {platform, get_text("platform", Node)}, {block_device_mapping, [extract_block_device_mapping(Item) || Item <- xmerl_xpath:string("blockDeviceMapping/item", Node)]}, - {product_codes, [extract_product_code(Item) || Item <- xmerl_xpath:string("productCodes/item", Node)]} + {product_codes, [extract_product_code(Item) || Item <- xmerl_xpath:string("productCodes/item", Node)]}, + {tag_set, [extract_tag_item(Item) || Item <- xmerl_xpath:string("tagSet/item", Node, [])]} ]. extract_block_device_mapping(Node) -> @@ -1491,10 +1512,12 @@ extract_instance(Node) -> {dns_name, get_text("dnsName", Node)}, {reason, get_text("reason", Node, none)}, {key_name, get_text("keyName", Node, none)}, + {metadata_options, transform_item_list(Node, "metadataOptions", fun extract_metadata_options/1)}, {ami_launch_index, list_to_integer(get_text("amiLaunchIndex", Node, "0"))}, {product_codes, get_list("productCodes/item/productCode", Node)}, {instance_type, get_text("instanceType", Node)}, {launch_time, erlcloud_xml:get_time("launchTime", Node)}, + {platform, get_text("platform", Node)}, {placement, [{availability_zone, get_text("placement/availabilityZone", Node)}]}, {kernel_id, get_text("kernelId", Node)}, {ramdisk_id, get_text("ramdiskId", Node)}, @@ -1691,12 +1714,26 @@ describe_regions(RegionNames) -> -spec describe_regions([string()], aws_config()) -> ok_error(proplist()). describe_regions(RegionNames, Config) - when is_list(RegionNames) -> - case ec2_query(Config, "DescribeRegions", erlcloud_aws:param_list(RegionNames, "RegionName")) of + when is_record(Config, aws_config) -> + describe_regions(RegionNames, none, Config). + +-spec describe_regions([string()], none | filter_list(), aws_config()) -> ok_error(proplist()). +describe_regions(RegionNames, Filter, Config) + when is_list(RegionNames), is_record(Config, aws_config) -> + describe_regions(RegionNames, Filter, false, Config). + +-spec describe_regions([string()], none | filter_list(), boolean(), aws_config()) -> ok_error(proplist()). +describe_regions(RegionNames, Filter, AllRegions, Config) + when is_list(RegionNames), is_boolean(AllRegions), is_record(Config, aws_config) -> + Params = erlcloud_aws:param_list(RegionNames, "RegionName") ++ + list_to_ec2_filter(Filter) ++ + [{"AllRegions", AllRegions}], + case ec2_query(Config, "DescribeRegions", Params, ?NEW_API_VERSION) of {ok, Doc} -> Items = xmerl_xpath:string("/DescribeRegionsResponse/regionInfo/item", Doc), {ok, [[{region_name, get_text("regionName", Item)}, - {region_endpoint, get_text("regionEndpoint", Item)} + {region_endpoint, get_text("regionEndpoint", Item)}, + {opt_in_status, get_text("optInStatus", Item)} ] || Item <- Items]}; {error, _} = Error -> Error @@ -1931,21 +1968,21 @@ extract_reserved_instances_offering(Node) -> {product_description, get_text("productDescription", Node)} ]. --spec describe_route_tables() -> ok_error(proplist()). +-spec describe_route_tables() -> ok_error([proplist()]). describe_route_tables() -> describe_route_tables([], none, default_config()). --spec describe_route_tables(filter_list() | none | aws_config()) -> ok_error(proplist()). +-spec describe_route_tables(filter_list() | none | aws_config()) -> ok_error([proplist()]). describe_route_tables(Config) when is_record(Config, aws_config) -> describe_route_tables([], none, Config); describe_route_tables(Filter) -> describe_route_tables([], Filter, default_config()). --spec describe_route_tables(filter_list() | none, aws_config()) -> ok_error(proplist()). +-spec describe_route_tables(filter_list() | none, aws_config()) -> ok_error([proplist()]). describe_route_tables(Filter, Config) -> describe_route_tables([], Filter, Config). --spec describe_route_tables([string()], filter_list() | none, aws_config()) -> ok_error(proplist()). +-spec describe_route_tables([string()], filter_list() | none, aws_config()) -> ok_error([proplist()]). describe_route_tables(RouteTableIds, Filter, Config) -> Params = erlcloud_aws:param_list(RouteTableIds, "RouteTableId") ++ list_to_ec2_filter(Filter), case ec2_query(Config, "DescribeRouteTables", Params, ?NEW_API_VERSION) of @@ -1973,7 +2010,9 @@ extract_route(Node) -> extract_route_set(Node) -> [ {destination_cidr_block, get_text("destinationCidrBlock", Node)}, + {destination_ipv6_cidr_block, get_text("destinationIpv6CidrBlock", Node)}, {gateway_id, get_text("gatewayId", Node)}, + {nat_gateway_id, get_text("natGatewayId", Node)}, {instance_id, get_text("instanceId", Node)}, {vpc_peering_conn_id, get_text("vpcPeeringConnectionId", Node)}, {network_interface_id, get_text("networkInterfaceId", Node)}, @@ -2055,7 +2094,8 @@ extract_ip_permissions(Node) -> {users, get_list("groups/item/userId", Node)}, {groups, [extract_user_id_group_pair(Item) || Item <- xmerl_xpath:string("groups/item", Node)]}, - {ip_ranges, get_list("ipRanges/item/cidrIp", Node)} + {ip_ranges, get_list("ipRanges/item/cidrIp", Node)}, + {ipv6_ranges, get_list("ipv6Ranges/item/cidrIpv6", Node)} ]. extract_user_id_group_pair(Node) -> @@ -2459,10 +2499,30 @@ extract_vpc(Node) -> {dhcp_options_id, get_text("dhcpOptionsId", Node)}, {instance_tenancy, get_text("instanceTenancy", Node)}, {is_default, get_bool("isDefault", Node)}, - {tag_set, + {cidr_block_association_set, extract_cidr_block_association_set(Node)}, + {tag_set, [extract_tag_item(Item) || Item <- xmerl_xpath:string("tagSet/item", Node)]} - ]. + ]. + +extract_cidr_block_association_set(Node) -> + Items = xmerl_xpath:string("cidrBlockAssociationSet/item", Node), + [extract_cidr_block_association_item(Item) || Item <- Items]. + +extract_cidr_block_association_item(Node) -> + [ + {cidr_block, get_text("cidrBlock", Node)}, + {association_id, get_text("associationId", Node)}, + {cidr_block_state, + [{state, get_text("cidrBlockState/state", Node)}] ++ + case get_text("cidrBlockState/statusMessage", Node, undefined) of + undefined -> + []; + StatusMessage -> + [{status_message, StatusMessage}] + end + } + ]. -spec detach_internet_gateway(string(), string()) -> ok. detach_internet_gateway(GatewayID, VpcID) -> @@ -3514,7 +3574,9 @@ ec2_query(Config, Action, Params) -> ec2_query(Config, Action, Params, ApiVersion) -> QParams = [{"Action", Action}, {"Version", ApiVersion}|Params], - erlcloud_aws:aws_request_xml4(post, Config#aws_config.ec2_host, + erlcloud_aws:aws_request_xml4(post, Config#aws_config.ec2_protocol, + Config#aws_config.ec2_host, + Config#aws_config.ec2_port, "/", QParams, "ec2", Config). default_config() -> erlcloud_aws:default_config(). @@ -3858,3 +3920,467 @@ extract_unsuccesful_item(Node) -> {error, [ {code, get_text("error/code", Node)}, {message, get_text("error/message", Node)}]} ]. + +%%%------------------------------------------------------------------------------ +%% @doc +%% Launch Templates API - DescribeLaunchTemplates +%% [https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeLaunchTemplates.html] +%% +%% ===Example=== +%% +%% Describe launch templates with matching launch template IDs. +%% +%% ` +%% {ok, Results} = erlcloud_ec2:describe_launch_templates(["lt-0a20c965061f64abc", "lt-32415965061f007aa"]) +%% ' +%% @end +%%%------------------------------------------------------------------------------ +-spec describe_launch_templates() -> ok_error([proplist()]). +describe_launch_templates() -> + describe_launch_templates([]). + +-spec describe_launch_templates(launch_template_ids()) -> ok_error([proplist()]); + (aws_config()) -> ok_error([proplist()]). +describe_launch_templates(LaunchTemplateIds) + when is_list(LaunchTemplateIds) -> + describe_launch_templates(LaunchTemplateIds, []); +describe_launch_templates(Config) + when is_record(Config, aws_config) -> + describe_launch_templates([], Config). + +-spec describe_launch_templates(launch_template_ids(), filter_list()) -> ok_error([proplist()]); + (launch_template_ids(), aws_config()) -> ok_error([proplist()]). +describe_launch_templates(LaunchTemplateIds, Filter) + when is_list(LaunchTemplateIds), is_list(Filter) -> + describe_launch_templates(LaunchTemplateIds, Filter, default_config()); +describe_launch_templates(LaunchTemplateIds, Config) + when is_list(LaunchTemplateIds), is_record(Config, aws_config) -> + describe_launch_templates(LaunchTemplateIds, [], Config). + +-spec describe_launch_templates(launch_template_ids(), filter_list(), aws_config()) -> ok_error([proplist()]). +describe_launch_templates(LaunchTemplateIds, Filter, Config) + when is_list(LaunchTemplateIds), is_list(Filter), is_record(Config, aws_config) -> + Params = erlcloud_aws:param_list(LaunchTemplateIds, "LaunchTemplateId") ++ list_to_ec2_filter(Filter), + case ec2_query(Config, "DescribeLaunchTemplates", Params, ?NEW_API_VERSION) of + {ok, Doc} -> + LaunchTemplates = extract_results("DescribeLaunchTemplatesResponse", "launchTemplates", fun extract_launch_template/1, Doc), + {ok, LaunchTemplates}; + {error, _} = E -> E + end. + +-spec describe_launch_templates(launch_template_ids(), filter_list(), ec2_max_result(), ec2_token()) -> ok_error([proplist()]); + (filter_list(), ec2_max_result(), ec2_token(), aws_config()) -> ok_error([proplist()]). +describe_launch_templates(LaunchTemplateIds, Filter, MaxResults, NextToken) + when is_list(LaunchTemplateIds), is_list(Filter) orelse Filter =:= none, is_integer(MaxResults), + is_list(NextToken) orelse NextToken =:= undefined -> + describe_launch_templates(LaunchTemplateIds, Filter, MaxResults, NextToken, default_config()); +describe_launch_templates(Filter, MaxResults, NextToken, Config) + when is_list(Filter) orelse Filter =:= none, is_integer(MaxResults), + is_list(NextToken) orelse NextToken =:= undefined, is_record(Config, aws_config) -> + describe_launch_templates([], Filter, MaxResults, NextToken, Config). + +-spec describe_launch_templates(launch_template_ids(), filter_list(), ec2_max_result(), ec2_token(), aws_config()) -> ok_error([proplist()], ec2_token()). +describe_launch_templates(LaunchTemplateIds, Filter, MaxResults, NextToken, Config) + when is_list(LaunchTemplateIds), is_list(Filter) orelse Filter =:= none, + is_integer(MaxResults) andalso MaxResults >= ?LAUNCH_TEMPLATES_MR_MIN andalso MaxResults =< ?LAUNCH_TEMPLATES_MR_MAX, + is_list(NextToken) orelse NextToken =:= undefined, + is_record(Config, aws_config) -> + Params = erlcloud_aws:param_list(LaunchTemplateIds, "LaunchTemplateId") ++ list_to_ec2_filter(Filter) ++ + [{"MaxResults", MaxResults}, {"NextToken", NextToken}], + case ec2_query(Config, "DescribeLaunchTemplates", Params, ?NEW_API_VERSION) of + {ok, Doc} -> + LaunchTemplates = extract_results("DescribeLaunchTemplatesResponse", "launchTemplates", fun extract_launch_template/1, Doc), + NewNextToken = extract_next_token("DescribeLaunchTemplatesResponse", Doc), + {ok, LaunchTemplates, NewNextToken}; + {error, _} = E -> E + end. + +-spec describe_launch_templates_all() -> ok_error([proplist()]). +describe_launch_templates_all() -> + describe_launch_templates_all(default_config()). + +-spec describe_launch_templates_all(aws_config() | launch_template_ids()) -> ok_error([proplist()]). +describe_launch_templates_all(Ids) + when is_list(Ids) -> + describe_launch_templates_all(Ids, none, default_config(), undefined, []); +describe_launch_templates_all(Config) + when is_record(Config, aws_config) -> + describe_launch_templates_all([], none, Config, undefined, []). + +-spec describe_launch_templates_all(launch_template_ids(), filter_list()) -> ok_error([proplist()]). +describe_launch_templates_all(LaunchTemplateIds, FilterOpts) + when is_list(LaunchTemplateIds), is_list(FilterOpts) orelse FilterOpts =:= none -> + describe_launch_templates_all(LaunchTemplateIds, FilterOpts, default_config()). + +-spec describe_launch_templates_all(launch_template_ids(), filter_list(), aws_config()) -> ok_error([proplist()]). +describe_launch_templates_all(LaunchTemplateIds, FilterOpts, Config) + when is_list(LaunchTemplateIds), is_list(FilterOpts) orelse FilterOpts =:= none, + is_record(Config, aws_config) -> + describe_launch_templates_all(LaunchTemplateIds, FilterOpts, Config, undefined, []). + +describe_launch_templates_all(LaunchTemplateIds, FilterOpts, Config, Token, Acc) + when is_list(LaunchTemplateIds), is_list(FilterOpts) orelse FilterOpts =:= none, + is_list(Token) orelse Token =:= undefined, is_record(Config, aws_config) -> + case describe_launch_templates(LaunchTemplateIds, FilterOpts, ?LAUNCH_TEMPLATES_MR_MAX, Token, Config) of + {ok, Results, undefined} -> {ok, Results ++ Acc}; + {ok, Results, Next} -> describe_launch_templates_all(LaunchTemplateIds, FilterOpts, Config, Next, Results ++ Acc); + {error, _} = Error -> Error + end. + +launch_template_version_opts() -> + [ + {launch_template_version, "LaunchTemplateVersion"}, + {max_version, "MaxVersion"}, + {min_version, "MinVersion"} + ]. + +set_launch_template_optional_selector({_, none}, Acc) -> Acc; +set_launch_template_optional_selector({_, []}, Acc) -> Acc; +set_launch_template_optional_selector({Key, Value}, Acc) -> + [{Key, Value} | Acc]. + +set_launch_template_selectors(LaunchTemplateId, LaunchTemplateName) -> + ArgList = [{"LaunchTemplateId", LaunchTemplateId}, {"LaunchTemplateName", LaunchTemplateName}], + lists:foldl(fun set_launch_template_optional_selector/2, [], ArgList). + +set_launch_template_version_opts(Opts) -> + OptTable = launch_template_version_opts(), + Sorted = lists:ukeysort(1, Opts), + FnFolder = fun({K, Val}, Acc) -> + case lists:keyfind(K, 1, OptTable) of + {launch_template_version, ApiArg} -> erlcloud_aws:param_list(Val, ApiArg) ++ Acc; + {_, ApiArg} -> [{ApiArg, Val} | Acc]; + false -> Acc + end + end, + lists:foldl(FnFolder, [], Sorted). + +%%%------------------------------------------------------------------------------ +%% @doc +%% Launch Templates API - DescribeLaunchTemplateVersions +%% [https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeLaunchTemplateVersions.html] +%% +%% ===Example=== +%% +%% Describe launch template versions for the template described by ID. +%% +%% ` +%% {ok, Results} = erlcloud_ec2:describe_launch_template_versions("lt-0a20c965061f64abc") +%% ' +%% +%% Describe launch template versions for the template with the given name. +%% +%% ` +%% {ok, Results} = erlcloud_ec2:describe_launch_template_versions(none, "MyTemplateName") +%% ' +%% +%% Describe most recent launch template version for the template with the given ID. +%% +%% ` +%% {ok, Results} = erlcloud_ec2:describe_launch_template_versions("lt-0a20c965061f64abc", none, [{launch_template_version, ["$Latest"]}]) +%% ' +%% +%% @end +%%%------------------------------------------------------------------------------ +-spec describe_launch_template_versions(string()) -> ok_error([proplist()]). +describe_launch_template_versions(LaunchTemplateId) -> + describe_launch_template_versions(LaunchTemplateId, none, []). + +-spec describe_launch_template_versions(string() | none, string() | none, list()) -> ok_error([proplist()]); + (string() | none, string() | none, aws_config()) -> ok_error([proplist()]). +describe_launch_template_versions(LaunchTemplateId, LaunchTemplateName, Opts) + when is_list(LaunchTemplateId) orelse LaunchTemplateId =:= none, + is_list(LaunchTemplateName) orelse LaunchTemplateName =:= none, + is_list(Opts) -> + describe_launch_template_versions(LaunchTemplateId, LaunchTemplateName, Opts, default_config()); +describe_launch_template_versions(LaunchTemplateId, LaunchTemplateName, Config) + when is_list(LaunchTemplateId) orelse LaunchTemplateId =:= none, + is_list(LaunchTemplateName) orelse LaunchTemplateName =:= none, + is_record(Config, aws_config) -> + describe_launch_template_versions(LaunchTemplateId, LaunchTemplateName, [], Config). + +-spec describe_launch_template_versions(string() | none, string() | none, list(), filter_list()) -> ok_error([proplist()]); + (string() | none, string() | none, list(), aws_config()) -> ok_error([proplist()]). +describe_launch_template_versions(LaunchTemplateId, LaunchTemplateName, Opts, Filter) + when is_list(LaunchTemplateId) orelse LaunchTemplateId =:= none, + is_list(LaunchTemplateName) orelse LaunchTemplateName =:= none, + is_list(Opts), is_list(Filter) -> + describe_launch_template_versions(LaunchTemplateId, LaunchTemplateName, Opts, Filter, default_config()); +describe_launch_template_versions(LaunchTemplateId, LaunchTemplateName, Opts, Config) + when is_list(LaunchTemplateId) orelse LaunchTemplateId =:= none, + is_list(LaunchTemplateName) orelse LaunchTemplateName =:= none, + is_list(Opts), is_record(Config, aws_config) -> + describe_launch_template_versions(LaunchTemplateId, LaunchTemplateName, Opts, [], Config). + +-spec describe_launch_template_versions(string() | none, string() | none, list(), filter_list(), aws_config()) -> ok_error([proplist()]). +describe_launch_template_versions(LaunchTemplateId, LaunchTemplateName, Opts, Filter, Config) + when is_list(LaunchTemplateId) orelse LaunchTemplateId =:= none, + is_list(LaunchTemplateName) orelse LaunchTemplateName =:= none, + is_list(Opts), is_list(Filter), is_record(Config, aws_config) -> + Params = set_launch_template_selectors(LaunchTemplateId, LaunchTemplateName) ++ + set_launch_template_version_opts(Opts) ++ list_to_ec2_filter(Filter), + case ec2_query(Config, "DescribeLaunchTemplateVersions", Params, ?NEW_API_VERSION) of + {ok, Doc} -> + LaunchTemplateVersions = extract_results("DescribeLaunchTemplateVersionsResponse", "launchTemplateVersionSet", fun extract_launch_template_version/1, Doc), + {ok, LaunchTemplateVersions}; + {error, _} = E -> E + end. + +-spec describe_launch_template_versions(string() | none, string() | none, list(), filter_list(), ec2_max_result(), ec2_token()) -> ok_error([proplist()]). +describe_launch_template_versions(LaunchTemplateId, LaunchTemplateName, Opts, Filter, MaxResults, NextToken) + when is_list(LaunchTemplateId) orelse LaunchTemplateId =:= none, + is_list(LaunchTemplateName) orelse LaunchTemplateName =:= none, is_list(Opts), is_list(Filter) orelse Filter =:= none, + is_integer(MaxResults) andalso MaxResults >= ?LAUNCH_TEMPLATES_MR_MIN andalso MaxResults =< ?LAUNCH_TEMPLATES_MR_MAX, + is_list(NextToken) orelse NextToken =:= undefined -> + describe_launch_template_versions(LaunchTemplateId, LaunchTemplateName, Opts, Filter, MaxResults, NextToken, default_config()). + +-spec describe_launch_template_versions(string() | none, string() | none, list(), filter_list(), ec2_max_result(), ec2_token(), aws_config()) + -> ok_error([proplist()], ec2_token()). +describe_launch_template_versions(LaunchTemplateId, LaunchTemplateName, Opts, Filter, MaxResults, NextToken, Config) + when is_list(LaunchTemplateId) orelse LaunchTemplateId =:= none, + is_list(LaunchTemplateName) orelse LaunchTemplateName =:= none, is_list(Opts), is_list(Filter) orelse Filter =:= none, + is_list(NextToken) orelse NextToken =:= undefined, + is_integer(MaxResults) andalso MaxResults >= ?LAUNCH_TEMPLATES_MR_MIN andalso MaxResults =< ?LAUNCH_TEMPLATES_MR_MAX, + is_record(Config, aws_config) -> + Params = set_launch_template_version_opts(Opts) ++ list_to_ec2_filter(Filter) ++ + set_launch_template_selectors(LaunchTemplateId, LaunchTemplateName) ++ + [{"NextToken", NextToken}, {"MaxResults", MaxResults}], + case ec2_query(Config, "DescribeLaunchTemplateVersions", Params, ?NEW_API_VERSION) of + {ok, Doc} -> + LaunchTemplateVersions = extract_results("DescribeLaunchTemplateVersionsResponse", "launchTemplateVersionSet", fun extract_launch_template_version/1, Doc), + NewNextToken = extract_next_token("DescribeLaunchTemplateVersionsResponse", Doc), + {ok, LaunchTemplateVersions, NewNextToken}; + {error, _} = E -> E + end. + +-spec describe_launch_template_versions_all(string() | none, string() | none) -> ok_error([proplist()]). +describe_launch_template_versions_all(LaunchTemplateId, LaunchTemplateName) + when is_list(LaunchTemplateId) orelse LaunchTemplateId =:= none, + is_list(LaunchTemplateName) orelse LaunchTemplateName =:= none -> + describe_launch_template_versions_all(LaunchTemplateId, LaunchTemplateName, default_config()). + +-spec describe_launch_template_versions_all(string() | none, string() | none, aws_config()) -> ok_error([proplist()]). +describe_launch_template_versions_all(LaunchTemplateId, LaunchTemplateName, Config) + when is_list(LaunchTemplateId) orelse LaunchTemplateId =:= none, + is_list(LaunchTemplateName) orelse LaunchTemplateName =:= none, + is_record(Config, aws_config) -> + describe_launch_template_versions_all(LaunchTemplateId, LaunchTemplateName, Config, undefined, []). + +describe_launch_template_versions_all(LaunchTemplateId, LaunchTemplateName, Config, Token, Acc) -> + case describe_launch_template_versions(LaunchTemplateId, LaunchTemplateName, [], none, ?LAUNCH_TEMPLATES_MR_MAX, Token, Config) of + {ok, Results, undefined} -> {ok, Results ++ Acc}; + {ok, Results, Next} -> + describe_launch_template_versions_all(LaunchTemplateId, LaunchTemplateName, Config, Next, Results ++ Acc); + {error, _} = Error -> Error + end. + +extract_launch_template(Node) -> + [ + {created_by, get_text("createdBy", Node)}, + {create_time, erlcloud_xml:get_time("createTime", Node)}, + {default_version_number, get_integer("defaultVersionNumber", Node)}, + {launch_template_id, get_text("launchTemplateId", Node)}, + {launch_template_name, get_text("launchTemplateName", Node)}, + {tag_set, [extract_tag_item(Item) || Item <- xmerl_xpath:string("tagSet/item", Node, [])]} + ]. + +extract_launch_template_version(Node) -> + [ + {created_by, get_text("createdBy", Node)}, + {create_time, erlcloud_xml:get_time("createTime", Node)}, + {default_version, get_bool("defaultVersion", Node)}, + {launch_template_data, transform_item_list(Node, "launchTemplateData", fun extract_launch_template_data/1)}, + {launch_template_id, get_text("launchTemplateId", Node)}, + {launch_template_name, get_text("launchTemplateName", Node)}, + {version_description, get_text("versionDescription", Node)}, + {version_number, get_integer("versionNumber", Node)} + ]. + +extract_capacity_reservation_target(Node) -> + [ + {capacity_reservation_id, get_text("capacityReservationId", Node)}, + {capacity_reservation_resource_group_arn, get_text("capacityReservationResourceGroupArn", Node)} + ]. + +extract_capacity_reservation_spec(Node) -> + [ + {capacity_reservation_preference, get_text("capacityReservationPreference", Node)}, + {capacity_reservation_target, transform_item_list(Node, "capacityReservationTarget", fun extract_capacity_reservation_target/1)} + ]. + +extract_elastic_gpu_specification(Node) -> + [{type, get_text("type", Node)}]. + +extract_elastic_inference_accelerator(Node) -> + [ + {count, get_integer("count", Node)}, + {type, get_text("type", Node)} + ]. + +extract_spot_market_options(Node) -> + [ + {block_duration_minutes, get_integer("blockDurationMinutes", Node)}, + {instance_interruption_behaviour, get_text("instanceInterruptionBehavior", Node)}, + {max_price, get_text("maxPrice", Node)}, + {spot_instance_type, get_text("spotInstanceType", Node)}, + {valid_until, erlcloud_xml:get_time("validUntil", Node)} + ]. + +extract_instance_market_options(Node) -> + [ + {market_type, get_text("marketType", Node)}, + {spot_options, transform_item_list(Node, "spotOptions", fun extract_spot_market_options/1)} + ]. + +extract_license_configuration(Node) -> + [{license_configuration_arn, get_text("licenseConfigurationArn", Node)}]. + +extract_maintenance_options(Node) -> + [{auto_recovery, get_text("autoRecovery", Node)}]. + +extract_metadata_options(Node) -> + [ + {http_endpoint, get_text("httpEndpoint", Node)}, + {http_protocol_ipv6, get_text("httpProtocolIpv6", Node)}, + {http_put_response_hop_limit, get_integer("httpPutResponseHopLimit", Node)}, + {http_tokens, get_text("httpTokens", Node)}, + {instance_metadata_tags, get_text("instanceMetadataTags", Node)}, + {state, get_text("state", Node)} + ]. + +extract_launch_template_placement(Node) -> + [ + {affinity, get_text("affinity", Node)}, + {availability_zone, get_text("availabilityZone", Node)}, + {group_name, get_text("groupName", Node)}, + {host_id, get_text("hostId", Node)}, + {host_resource_group_arn, get_text("hostResourceGroupArn", Node)}, + {partition_number, get_integer("partitionNumber", Node)}, + {spread_domain, get_text("spreadDomain", Node)}, + {tenancy, get_text("tenancy", Node)} + ]. + +extract_private_dns_name_options(Node) -> + [ + {enable_resource_name_dns_aaaa_record, get_bool("enableResourceNameDnsAAAARecord", Node)}, + {enable_resource_name_dns_a_record, get_bool("enableResourceNameDnsARecord", Node)}, + {hostname_type, get_text("hostnameType", Node)} + ]. + +extract_tag_specification(Node) -> + [ + {resource_type, get_text("resourceType", Node)}, + {tag_set, transform_item_list(Node, "tagSet/item", fun extract_tag_item/1)} + ]. + +extract_cpu_options(Node) -> + [ + {core_count, get_integer("coreCount", Node)}, + {threads_per_core, get_integer("threadsPerCore", Node)} + ]. + +extract_iam_instance_profile(Node) -> + [ + {arn, get_text("arn", Node)}, + {id, get_text("id", Node)} + ]. + +extract_credit_specification(Node) -> + [ + {cpu_credits, get_text("cpuCredits", Node)} + ]. + +extract_range(Node, FnConvert) -> + [ + {max, FnConvert("max", Node)}, + {min, FnConvert("min", Node)} + ]. + +extract_int_range(Node) -> extract_range(Node, fun erlcloud_xml:get_integer/2). +extract_float_range(Node) -> extract_range(Node, fun erlcloud_xml:get_float/2). + +extract_instance_requirements(Node) -> + [ + {accelerator_count, transform_item_list(Node, "acceleratorCount", fun extract_int_range/1)}, + {accelerator_manufacturer_set, transform_item_list(Node, "acceleratorManufacturerSet/item", fun erlcloud_xml:get_text/1)}, + {accelerator_name_set, transform_item_list(Node, "acceleratorNameSet/item", fun erlcloud_xml:get_text/1)}, + {accelerator_total_memory_mib, transform_item_list(Node, "acceleratorTotalMemoryMiB", fun extract_int_range/1)}, + {accelerator_type_set, transform_item_list(Node, "acceleratorTypeSet/item", fun erlcloud_xml:get_text/1)}, + {bare_metal, get_text("bareMetal", Node)}, + {baseline_ebs_bandwidth_mbps, transform_item_list(Node, "baselineEbsBandwidthMbps", fun extract_int_range/1)}, + {burstable_performance, get_text("burstablePerformance", Node)}, + {cpu_manufacturer_set, transform_item_list(Node, "cpuManufacturerSet/item", fun erlcloud_xml:get_text/1)}, + {excluded_instance_type_set, transform_item_list(Node, "excludedInstanceTypeSet/item", fun erlcloud_xml:get_text/1)}, + {instance_generation_set, transform_item_list(Node, "instanceGenerationSet/item", fun erlcloud_xml:get_text/1)}, + {local_storage, get_text("localStorage", Node)}, + {local_storage_type_set, transform_item_list(Node, "localStorageTypeSet/item", fun erlcloud_xml:get_text/1)}, + {memory_gib_per_vcpu, transform_item_list(Node, "memoryGiBPerVCpu", fun extract_float_range/1)}, + {memory_mib, transform_item_list(Node, "memoryMiB", fun extract_int_range/1)}, + {network_interface_count, transform_item_list(Node, "networkInterfaceCount", fun extract_int_range/1)}, + {on_demand_max_price_percentage_over_lowest_price, get_integer("onDemandMaxPricePercentageOverLowestPrice", Node)}, + {require_hibernate_support, get_bool("requireHibernateSupport", Node)}, + {spot_max_price_percentage_over_lowest_price, get_integer("spotMaxPricePercentageOverLowestPrice", Node)}, + {total_local_storage_gb, transform_item_list(Node, "totalLocalStorageGB", fun extract_float_range/1)}, + {vcpu_count, transform_item_list(Node, "vCpuCount", fun extract_int_range/1)} + ]. + +extract_network_interface_specification(Node) -> + [ + {associate_carrier_ip_address, get_bool("associateCarrierIpAddress", Node)}, + {associate_public_ip_address, get_bool("associatePublicIpAddress", Node)}, + {delete_on_termination, get_bool("deleteOnTermination", Node)}, + {description, get_text("description", Node)}, + {device_index, get_integer("deviceIndex", Node)}, + {group_set, transform_item_list(Node, "groupSet", fun(N) -> {group_id, get_text("groupId", N)} end)}, + {interface_type, get_text("interfaceType", Node)}, + {ipv4_prefix_count, get_integer("ipv4PrefixCount", Node)}, + {ipv4_prefix_set, transform_item_list(Node, "ipv4PrefixSet/item", fun(N) -> {ipv4_prefix, get_text("ipv4Prefix", N)} end)}, + {ipv6_address_count, get_integer("ipv6AddressCount", Node)}, + {ipv6_addresses_set, transform_item_list(Node, "ipv6AddressesSet/item", fun(N) -> {ipv6_address, get_text("ipv6Address", N)} end)}, + {ipv6_prefix_count, get_integer("ipv6PrefixCount", Node)}, + {ipv6_prefix_set, transform_item_list(Node, "ipv6AddressesSet/item", fun(N) -> {ipv6_prefix, get_text("ipv6Prefix", N)} end)}, + {network_card_index, get_integer("networkCardIndex", Node)}, + {network_interface_id, get_text("networkInterfaceId", Node)}, + {private_ip_address, get_text("privateIpAddress", Node)}, + {private_ip_addresses_set, transform_item_list(Node, "privateIpAddressesSet/item", fun extract_private_ip_address/1)}, + {secondary_private_ip_address_count, get_integer("secondaryPrivateIpAddressCount", Node)}, + {subnet_id, get_text("subnetId", Node)} + ]. + +transform_item_list(ParentNode, Tag, FncTransform) -> + [FncTransform(Item) || Item <- xmerl_xpath:string(Tag, ParentNode)]. + +extract_launch_template_data(Node) -> + [ + {block_device_mapping_set, transform_item_list(Node, "blockDeviceMappingSet/item", fun extract_block_device_mapping/1)}, + {capacity_reservation_specification, transform_item_list(Node, "capacityReservationSpecification", fun extract_capacity_reservation_spec/1)}, + {cpu_options, transform_item_list(Node, "cpuOptions", fun extract_cpu_options/1)}, + {credit_specification, transform_item_list(Node, "creditSpecification", fun extract_credit_specification/1)}, + {disable_api_stop, get_bool("disableApiStop", Node)}, + {disable_api_termination, get_bool("disableApiTermination", Node)}, + {ebs_optimized, get_bool("ebsOptimized", Node)}, + {elastic_gpu_specification_set, transform_item_list(Node, "elasticGpuSpecificationSet/item", fun extract_elastic_gpu_specification/1)}, + {elastic_inference_accelerator_set, transform_item_list(Node, "elasticInferenceAcceleratorSet/item", fun extract_elastic_inference_accelerator/1)}, + {enclave_options, [{enabled, get_bool("enclaveOptions/enabled", Node)}]}, + {hibernation_options, [{configured, get_bool("hibernationOptions/configured", Node)}]}, + {iam_instance_profile, transform_item_list(Node, "iamInstanceProfile", fun extract_iam_instance_profile/1)}, + {image_id, get_text("imageId", Node)}, + {instance_initiated_shutdown_behavior, get_text("instanceInitiatedShutdownBehavior", Node)}, + {instance_market_options, transform_item_list(Node, "instanceMarketOptions", fun extract_instance_market_options/1)}, + {instance_requirements, transform_item_list(Node, "instanceRequirements", fun extract_instance_requirements/1)}, + {instance_type, get_text("instanceType", Node)}, + {kernel_id, get_text("kernelId", Node)}, + {key_name, get_text("keyName", Node)}, + {license_set, transform_item_list(Node, "licenseSet/item", fun extract_license_configuration/1)}, + {maintenance_options, transform_item_list(Node, "maintenanceOptions", fun extract_maintenance_options/1)}, + {metadata_options, transform_item_list(Node, "metadataOptions", fun extract_metadata_options/1)}, + {monitoring, [{enabled, get_bool("monitoring/enabled", Node)}]}, + {network_interface_set, transform_item_list(Node, "networkInterfaceSet/item", fun extract_network_interface_specification/1)}, + {placement, transform_item_list(Node, "placement", fun extract_launch_template_placement/1)}, + {private_dns_name_options, transform_item_list(Node, "privateDnsNameOptions", fun extract_private_dns_name_options/1)}, + {ram_disk_id, get_text("ramDiskId", Node)}, + {security_group_id_set, [get_text(Item) || Item <- xmerl_xpath:string("securityGroupIdSet/item", Node)]}, + {security_group_set, [get_text(Item) || Item <- xmerl_xpath:string("securityGroupSet/item", Node)]}, + {tag_specification_set, transform_item_list(Node, "tagSpecificationSet/item", fun extract_tag_specification/1)}, + {user_data, get_text("userData", Node)} + ]. diff --git a/src/erlcloud_ec2_meta.erl b/src/erlcloud_ec2_meta.erl index b40ed4422..a9d8d3e3e 100644 --- a/src/erlcloud_ec2_meta.erl +++ b/src/erlcloud_ec2_meta.erl @@ -3,11 +3,29 @@ -include("erlcloud.hrl"). -include("erlcloud_aws.hrl"). --export([get_instance_metadata/0, get_instance_metadata/1, get_instance_metadata/2, - get_instance_user_data/0, get_instance_user_data/1, - get_instance_dynamic_data/0, get_instance_dynamic_data/1, get_instance_dynamic_data/2]). +-export([generate_session_token/1, generate_session_token/2, + get_instance_metadata/0, get_instance_metadata/1, get_instance_metadata/2, get_instance_metadata/3, + get_instance_user_data/0, get_instance_user_data/1, get_instance_user_data/2, + get_instance_dynamic_data/0, get_instance_dynamic_data/1, get_instance_dynamic_data/2, get_instance_dynamic_data/3]). +-spec generate_session_token(DurationSecs :: non_neg_integer()) -> {ok, binary()} | {error, erlcloud_aws:httpc_result_error()}. +generate_session_token(DurationSecs) -> + generate_session_token(DurationSecs, erlcloud_aws:default_config()). + +%%%--------------------------------------------------------------------------- +-spec generate_session_token(DurationSecs :: non_neg_integer(), Config:: aws_config()) -> {ok, binary()} | {error, erlcloud_aws:httpc_result_error()}. +%%%--------------------------------------------------------------------------- +%% @doc Generate session token Will fail if not an EC2 instance. +%% +%% This convenience function will generate the IMDSv2 session token from the AWS metadata available at +%% http:///latest/latest/api/token +%% ItemPath allows fetching specific pieces of metadata. +%% defaults to 169.254.169.254 +generate_session_token(DurationSecs, Config) -> + Header = [{"X-aws-ec2-metadata-token-ttl-seconds", integer_to_binary(DurationSecs)}], + MetaDataPath = "http://" ++ ec2_meta_host_port() ++ "/latest/api/token", + erlcloud_aws:http_body(erlcloud_httpc:request(MetaDataPath, put, Header, <<>>, erlcloud_aws:get_timeout(Config), Config)). -spec get_instance_metadata() -> {ok, binary()} | {error, erlcloud_aws:httpc_result_error()}. @@ -18,38 +36,49 @@ get_instance_metadata() -> get_instance_metadata(Config) -> get_instance_metadata("", Config). +-spec get_instance_metadata( ItemPath :: string(), Config :: aws_config() ) -> {ok, binary()} | {error, erlcloud_aws:httpc_result_error()}. +get_instance_metadata(ItemPath, Config) -> + get_instance_metadata(ItemPath, Config, undefined). %%%--------------------------------------------------------------------------- --spec get_instance_metadata( ItemPath :: string(), Config :: aws_config() ) -> {ok, binary()} | {error, erlcloud_aws:httpc_result_error()}. +-spec get_instance_metadata( ItemPath :: string(), Config :: aws_config(), Token :: undefined | binary()) -> {ok, binary()} | {error, erlcloud_aws:httpc_result_error()}. %%%--------------------------------------------------------------------------- %% @doc Retrieve the instance meta data for the instance this code is running on. Will fail if not an EC2 instance. %% -%% This convenience function will retrieve the instance id from the AWS metadata available at -%% http://169.254.169.254/latest/meta-data/* +%% This convenience function will retrieve the instance id from the AWS metadata available at +%% http:///latest/meta-data/* %% ItemPath allows fetching specific pieces of metadata. +%% defaults to 169.254.169.254 %% %% -get_instance_metadata(ItemPath, Config) -> - MetaDataPath = "http://169.254.169.254/latest/meta-data/" ++ ItemPath, - erlcloud_aws:http_body(erlcloud_httpc:request(MetaDataPath, get, [], <<>>, erlcloud_aws:get_timeout(Config), Config)). +get_instance_metadata(ItemPath, Config, Token) -> + MetaDataPath = "http://" ++ ec2_meta_host_port() ++ "/latest/meta-data/" ++ ItemPath, + Header = maybe_token_header(Token), + erlcloud_aws:http_body(erlcloud_httpc:request(MetaDataPath, get, Header, <<>>, erlcloud_aws:get_timeout(Config), Config)). -spec get_instance_user_data() -> {ok, binary()} | {error, erlcloud_aws:httpc_result_error()}. get_instance_user_data() -> get_instance_user_data(erlcloud_aws:default_config()). -%%%--------------------------------------------------------------------------- -spec get_instance_user_data( Config :: aws_config() ) -> {ok, binary()} | {error, erlcloud_aws:httpc_result_error()}. +get_instance_user_data(Config) -> + get_instance_user_data(Config, undefined). + +%%%--------------------------------------------------------------------------- +-spec get_instance_user_data( Config :: aws_config(), Token :: undefined | binary() ) -> {ok, binary()} | {error, erlcloud_aws:httpc_result_error()}. %%%--------------------------------------------------------------------------- %% @doc Retrieve the user data for the instance this code is running on. Will fail if not an EC2 instance. %% -%% This convenience function will retrieve the user data the instance was started with, i.e. what's available at -%% http://169.254.169.254/latest/user-data +%% This convenience function will retrieve the user data the instance was started with, i.e. what's available at +%% http:///latest/user-data +%% defaults to 169.254.169.254 %% %% -get_instance_user_data(Config) -> - UserDataPath = "http://169.254.169.254/latest/user-data/", - erlcloud_aws:http_body(erlcloud_httpc:request(UserDataPath, get, [], <<>>, erlcloud_aws:get_timeout(Config), Config)). +get_instance_user_data(Config, Token) -> + UserDataPath = "http://" ++ ec2_meta_host_port() ++ "/latest/user-data/", + Header = maybe_token_header(Token), + erlcloud_aws:http_body(erlcloud_httpc:request(UserDataPath, get, Header, <<>>, erlcloud_aws:get_timeout(Config), Config)). -spec get_instance_dynamic_data() -> {ok, binary()} | {error, erlcloud_aws:httpc_result_error()}. @@ -60,12 +89,28 @@ get_instance_dynamic_data() -> get_instance_dynamic_data(Config) -> get_instance_dynamic_data("", Config). +-spec get_instance_dynamic_data( ItemPath :: string(), Config :: aws_config() ) -> {ok, binary()} | {error, erlcloud_aws:httpc_result_error()}. +get_instance_dynamic_data(ItemPath, Config) -> + get_instance_dynamic_data(ItemPath, Config, undefined). %%%--------------------------------------------------------------------------- --spec get_instance_dynamic_data( ItemPath :: string(), Config :: aws_config() ) -> {ok, binary()} | {error, erlcloud_aws:httpc_result_error()}. +-spec get_instance_dynamic_data( ItemPath :: string(), Config :: aws_config(), Token :: undefined | binary() ) -> {ok, binary()} | {error, erlcloud_aws:httpc_result_error()}. %%%--------------------------------------------------------------------------- -get_instance_dynamic_data(ItemPath, Config) -> - DynamicDataPath = "http://169.254.169.254/latest/dynamic/" ++ ItemPath, - erlcloud_aws:http_body(erlcloud_httpc:request(DynamicDataPath, get, [], <<>>, erlcloud_aws:get_timeout(Config), Config)). +get_instance_dynamic_data(ItemPath, Config, Token) -> + DynamicDataPath = "http://" ++ ec2_meta_host_port() ++ "/latest/dynamic/" ++ ItemPath, + Header = maybe_token_header(Token), + erlcloud_aws:http_body(erlcloud_httpc:request(DynamicDataPath, get, Header, <<>>, erlcloud_aws:get_timeout(Config), Config)). + +%%%------------------------------------------------------------------------------ +%%% Internal functions. +%%%------------------------------------------------------------------------------ + +ec2_meta_host_port() -> + {ok, EC2MetaHostPort} = application:get_env(erlcloud, ec2_meta_host_port), + EC2MetaHostPort. +maybe_token_header(undefined) -> + []; +maybe_token_header(Token) -> + [{"X-aws-ec2-metadata-token", Token}]. diff --git a/src/erlcloud_ecs.erl b/src/erlcloud_ecs.erl index f8b048234..920171ff9 100644 --- a/src/erlcloud_ecs.erl +++ b/src/erlcloud_ecs.erl @@ -62,6 +62,7 @@ list_clusters/0, list_clusters/1, list_clusters/2, list_container_instances/0, list_container_instances/1, list_container_instances/2, list_services/0, list_services/1, list_services/2, + list_tags_for_resource/1, list_tags_for_resource/2, list_tags_for_resource/3, list_task_definition_families/0, list_task_definition_families/1, list_task_definition_families/2, list_task_definitions/0, list_task_definitions/1, list_task_definitions/2, list_tasks/0, list_tasks/1, list_tasks/2, @@ -678,7 +679,8 @@ container_definition_opts() -> {working_directory, <<"workingDirectory">>, fun to_binary/1} ]. --spec encode_container_definitions(Defs :: container_definition_opts()) -> [aws_opts()]. +-spec encode_container_definitions(Defs :: container_definition_opts() + | [container_definition_opts()]) -> [aws_opts()]. encode_container_definitions(Defs) -> encode_maybe_list(fun container_definition_opts/0, Defs). @@ -1040,16 +1042,51 @@ network_binding_record() -> ] }. +-spec network_interface_record() -> record_desc(). +network_interface_record() -> + {#ecs_network_interface{}, + [ + {<<"attachmentId">>, #ecs_network_interface.attachment_id, fun id/2}, + {<<"privateIpv4Address">>, #ecs_network_interface.private_ipv4_address, fun id/2} + ] + }. + +-spec attachment_record() -> record_desc(). +attachment_record() -> + {#ecs_attachment{}, + [ + {<<"id">>, #ecs_attachment.id, fun id/2}, + {<<"type">>, #ecs_attachment.type, fun id/2}, + {<<"status">>, #ecs_attachment.status, fun id/2}, + {<<"details">>, #ecs_attachment.details, fun decode_attachment_details_list/2} + ] + }. + +-spec attachment_detail_record() -> record_desc(). +attachment_detail_record() -> + {#ecs_attachment_detail{}, + [ + {<<"name">>, #ecs_attachment_detail.name, fun id/2}, + {<<"value">>, #ecs_attachment_detail.value, fun id/2} + ] + }. + -spec container_record() -> record_desc(). container_record() -> {#ecs_container{}, [ {<<"containerArn">>, #ecs_container.container_arn, fun id/2}, + {<<"cpu">>, #ecs_container.cpu, fun id/2}, {<<"exitCode">>, #ecs_container.exit_code, fun id/2}, + {<<"healthStatus">>, #ecs_container.health_status, fun id/2}, + {<<"image">>, #ecs_container.image, fun id/2}, {<<"lastStatus">>, #ecs_container.last_status, fun id/2}, + {<<"memoryReservation">>, #ecs_container.memory_reservation, fun id/2}, {<<"name">>, #ecs_container.name, fun id/2}, {<<"networkBindings">>, #ecs_container.network_bindings, fun decode_network_bindings_list/2}, + {<<"networkInterfaces">>, #ecs_container.network_interfaces, fun decode_network_interfaces_list/2}, {<<"reason">>, #ecs_container.reason, fun id/2}, + {<<"runtimeId">>, #ecs_container.runtime_id, fun id/2}, {<<"taskArn">>, #ecs_container.task_arn, fun id/2} ] }. @@ -1073,23 +1110,46 @@ task_override_record() -> ] }. +-spec tag_record() -> record_desc(). +tag_record() -> + {#ecs_tag{}, + [ + {<<"key">>, #ecs_tag.key, fun id/2}, + {<<"value">>, #ecs_tag.value, fun id/2} + ] + }. + -spec task_record() -> record_desc(). task_record() -> {#ecs_task{}, [ + {<<"attachments">>, #ecs_task.attachments, fun decode_attachments_list/2}, + {<<"availabilityZone">>, #ecs_task.availability_zone, fun id/2}, {<<"clusterArn">>, #ecs_task.cluster_arn, fun id/2}, + {<<"connectivity">>, #ecs_task.connectivity, fun id/2}, + {<<"connectivityAt">>, #ecs_task.connectivity_at, fun id/2}, {<<"containerInstanceArn">>, #ecs_task.container_instance_arn, fun id/2}, {<<"containers">>, #ecs_task.containers, fun decode_containers_list/2}, + {<<"cpu">>, #ecs_task.cpu, fun id/2}, {<<"createdAt">>, #ecs_task.created_at, fun id/2}, {<<"desiredStatus">>, #ecs_task.desired_status, fun id/2}, + {<<"group">>, #ecs_task.group, fun id/2}, + {<<"healthStatus">>, #ecs_task.health_status, fun id/2}, {<<"lastStatus">>, #ecs_task.last_status, fun id/2}, + {<<"launchType">>, #ecs_task.launch_type, fun id/2}, + {<<"memory">>, #ecs_task.memory, fun id/2}, {<<"overrides">>, #ecs_task.overrides, fun decode_task_overrides/2}, + {<<"platformVersion">>, #ecs_task.platform_version, fun id/2}, + {<<"pullStartedAt">>, #ecs_task.pull_started_at, fun id/2}, + {<<"pullStoppedAt">>, #ecs_task.pull_stopped_at, fun id/2}, {<<"startedAt">>, #ecs_task.started_at, fun id/2}, {<<"startedBy">>, #ecs_task.started_by, fun id/2}, {<<"stoppedAt">>, #ecs_task.stopped_at, fun id/2}, {<<"stoppedReason">>, #ecs_task.stopped_reason, fun id/2}, + {<<"tags">>, #ecs_task.tags, fun decode_tags_list/2}, {<<"taskArn">>, #ecs_task.task_arn, fun id/2}, - {<<"taskDefinitionArn">>, #ecs_task.task_definition_arn, fun id/2} + {<<"taskDefinitionArn">>, #ecs_task.task_definition_arn, fun id/2}, + {<<"version">>, #ecs_task.version, fun id/2} ] }. @@ -1159,15 +1219,27 @@ decode_volumes_from_list(V, Opts) -> decode_tasks_list(V, Opts) -> [decode_record(task_record(), I, Opts) || I <- V]. +decode_attachments_list(V, Opts) -> + [decode_record(attachment_record(), I, Opts) || I <- V]. + +decode_attachment_details_list(V, Opts) -> + [decode_record(attachment_detail_record(), I, Opts) || I <- V]. + decode_containers_list(V, Opts) -> [decode_record(container_record(), I, Opts) || I <- V]. decode_network_bindings_list(V, Opts) -> [decode_record(network_binding_record(), I, Opts) || I <- V]. +decode_network_interfaces_list(V, Opts) -> + [decode_record(network_interface_record(), I, Opts) || I <- V]. + decode_task_overrides(V, Opts) -> decode_record(task_override_record(), V, Opts). +decode_tags_list(V, Opts) -> + [decode_record(tag_record(), I, Opts) || I <- V]. + decode_container_overrides_list(V, Opts) -> [decode_record(container_override_record(), I, Opts) || I <- V]. %%%------------------------------------------------------------------------------ @@ -1177,7 +1249,8 @@ decode_container_overrides_list(V, Opts) -> %%%------------------------------------------------------------------------------ %% CreateCluster %%%------------------------------------------------------------------------------ --type create_cluster_opt() :: {cluster_name, string_param()}. +-type create_cluster_opt() :: {cluster_name, string_param()} | + out_opt(). -type create_cluster_opts() :: [create_cluster_opt()]. -spec create_cluster_opts() -> opt_table(). @@ -1227,7 +1300,8 @@ create_cluster(Opts, #aws_config{} = Config) -> cluster_opt() | deployment_configuration() | load_balancers_opt() | - role_opt(). + role_opt() | + out_opt(). -type create_service_opts() :: [create_service_opt()]. -spec create_service_opts() -> opt_table(). @@ -1329,7 +1403,8 @@ delete_cluster(ClusterName, Opts, #aws_config{} = Config) -> %%%------------------------------------------------------------------------------ %% DeleteService %%%------------------------------------------------------------------------------ --type delete_service_opt() :: {cluster, string_param()}. +-type delete_service_opt() :: {cluster, string_param()} | + out_opt(). -type delete_service_opts() :: [delete_service_opt()]. -spec delete_service_opts() -> opt_table(). @@ -1381,7 +1456,8 @@ delete_service(ServiceName, Opts, #aws_config{} = Config) -> %% DeregisterContainerInstance %%%------------------------------------------------------------------------------ -type deregister_container_instance_opt() :: {cluster, string_param()} | - {force, boolean()}. + {force, boolean()} | + out_opt(). -type deregister_container_instance_opts() :: [deregister_container_instance_opt()]. -spec deregister_container_instance_opts() -> opt_table(). @@ -1483,7 +1559,8 @@ deregister_task_definition(TaskDefinition, Opts, Config) -> %%%------------------------------------------------------------------------------ %% DescribeClusters %%%------------------------------------------------------------------------------ --type describe_clusters_opt() :: {clusters, [string_param()]}. +-type describe_clusters_opt() :: {clusters, [string_param()]} | + out_opt(). -type describe_clusters_opts() :: [describe_clusters_opt()]. -spec describe_clusters_opts() -> opt_table(). @@ -1536,7 +1613,8 @@ describe_clusters(Opts, #aws_config{} = Config) -> %%%------------------------------------------------------------------------------ %% DescribeContainerInstances %%%------------------------------------------------------------------------------ --type describe_container_instances_opt() :: {cluster, string_param()}. +-type describe_container_instances_opt() :: {cluster, string_param()} | + out_opt(). -type describe_container_instances_opts() :: [describe_container_instances_opt()]. -spec describe_container_instances_opts() -> opt_table(). @@ -1594,7 +1672,8 @@ describe_container_instances(Instances, Opts, #aws_config{} = Config) -> %%%------------------------------------------------------------------------------ %% DescribeServices %%%------------------------------------------------------------------------------ --type describe_services_opt() :: {cluster, string_param()}. +-type describe_services_opt() :: {cluster, string_param()} | + out_opt(). -type describe_services_opts() :: [describe_services_opt()]. -spec describe_services_opts() -> opt_table(). @@ -1696,13 +1775,16 @@ describe_task_definition(TaskDefinition, Opts, #aws_config{} = Config) -> %%%------------------------------------------------------------------------------ %% DescribeTasks %%%------------------------------------------------------------------------------ --type describe_tasks_opt() :: {cluster, string_param()}. +-type describe_tasks_opt() :: {cluster, string_param()} | + {include, list(string_param())} | + out_opt(). -type describe_tasks_opts() :: [describe_tasks_opt()]. -spec describe_tasks_opts() -> opt_table(). describe_tasks_opts() -> [ - {cluster, <<"cluster">>, fun to_binary/1} + {cluster, <<"cluster">>, fun to_binary/1}, + {include, <<"include">>, fun to_binary/1} ]. -spec describe_tasks_record() -> record_desc(). @@ -1756,7 +1838,8 @@ describe_tasks(Tasks, Opts, #aws_config{} = Config) -> %% ListClusters %%%------------------------------------------------------------------------------ -type list_clusters_opt() :: {max_results, 1..100} | - {next_token, binary()}. + {next_token, binary()} | + out_opt(). -type list_clusters_opts() :: [list_clusters_opt()]. @@ -1808,7 +1891,8 @@ list_clusters(Opts, Config) -> %%%------------------------------------------------------------------------------ -type list_container_instances_opt() :: {cluster, string_param()} | {max_results, 1..100} | - {next_token, binary()}. + {next_token, binary()} | + out_opt(). -type list_container_instances_opts() :: [list_container_instances_opt()]. @@ -1861,7 +1945,8 @@ list_container_instances(Opts, Config) -> %%%------------------------------------------------------------------------------ -type list_services_opt() :: {cluster, string_param()} | {max_results, 1..100} | - {next_token, binary()}. + {next_token, binary()} | + out_opt(). -type list_services_opts() :: [list_services_opt()]. @@ -1909,13 +1994,59 @@ list_services(Opts, Config) -> out(Return, fun(Json, UOpts) -> decode_record(list_services_record(), Json, UOpts) end, EcsOpts). +%%%------------------------------------------------------------------------------ +%% ListTagsForResource +%%%------------------------------------------------------------------------------ +-spec list_tags_for_resource( + Arn :: string_param()) -> ecs_return([#ecs_tag{}]). +list_tags_for_resource(Arn) -> + list_tags_for_resource(Arn, [], default_config()). + +-spec list_tags_for_resource( + Arn :: string_param(), + Config :: aws_config()) -> ecs_return([#ecs_tag{}]). +list_tags_for_resource(Arn, Config) when is_record(Config, aws_config) -> + list_tags_for_resource(Arn, [], default_config()). + +%%%------------------------------------------------------------------------------ +%% @doc +%% ECS API +%% [https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_ListTagsForResource.html] +%% +%% ===Example=== +%% +%% List tags for a given resource +%% +%% ` +%% {ok, Result} = erlcloud_ecs:list_tags_for_resource("resource-arn"), +%% ' +%% @end +%%%------------------------------------------------------------------------------ +-spec list_tags_for_resource( + Arn :: string_param(), + Opts :: proplist(), + Config :: aws_config()) -> ecs_return([#ecs_tag{}]). +list_tags_for_resource(Arn, Opts, #aws_config{} = Config) -> + {AwsOpts, EcsOpts} = opts([], Opts), + Return = ecs_request( + Config, + "ListTagsForResource", + [{<<"resourceArn">>, to_binary(Arn)}] ++ AwsOpts), + out(Return, fun(Json, UOpts) -> + TagsList = proplists:get_value(<<"tags">>, Json), + decode_tags_list(TagsList, UOpts) + end, + EcsOpts). + + %%%------------------------------------------------------------------------------ %% ListTaskDefinitionFamilies %%%------------------------------------------------------------------------------ -type list_task_definition_families_opt() :: {family_prefix, string_param()} | {status, active | inactive | all} | {max_results, 1..100} | - {next_token, binary()}. + {next_token, binary()} | + out_opt(). -type list_task_definition_families_opts() :: [list_task_definition_families_opt()]. @@ -1975,7 +2106,8 @@ list_task_definition_families(Opts, Config) -> {status, active | inactive} | {sort, asc | desc} | {max_results, 1..100} | - {next_token, binary()}. + {next_token, binary()} | + out_opt(). -type list_task_definitions_opts() :: [list_task_definitions_opt()]. @@ -2036,7 +2168,9 @@ list_task_definitions(Opts, Config) -> {family, string_param()} | {sort, asc | desc} | {max_results, 1..100} | - {next_token, binary()}. + {next_token, binary()} | + {launch_type, string_param()} | + out_opt(). -type list_tasks_opts() :: [list_tasks_opt()]. @@ -2051,6 +2185,7 @@ list_tasks_opts() -> {service_name, <<"serviceName">>, fun to_binary/1}, {started_by, <<"startedBy">>, fun to_binary/1}, {max_results, <<"maxResults">>, fun id/1}, + {launch_type, <<"launchType">>, fun to_binary/1}, {next_token, <<"nextToken">>, fun to_binary/1} ]. @@ -2097,7 +2232,8 @@ list_tasks(Opts, Config) -> %%%------------------------------------------------------------------------------ -type register_task_definition_opt() :: {network_mode, ecs_network_mode()} | {task_role_arn, string_param()} | - volumes(). + volumes() | + out_opt(). -type register_task_definition_opts() :: [register_task_definition_opt()]. -spec register_task_definition_opts() -> opt_table(). @@ -2163,8 +2299,9 @@ register_task_definition(ContainerDefinitions, Family, Opts, Config) -> -type run_task_opt() :: {cluster, string_param()} | {count, pos_integer()} | task_overrides_opt() | - placement_strategy_opt() | - {started_by, string_param()}. + placement_strategy() | + {started_by, string_param()} | + out_opt(). -type run_task_opts() :: [run_task_opt()]. -spec run_task_opts() -> opt_table(). @@ -2227,7 +2364,8 @@ run_task(TaskDefinition, Opts, Config) -> %%%------------------------------------------------------------------------------ -type start_task_opt() :: {cluster, string_param()} | task_overrides_opt() | - {started_by, string_param()}. + {started_by, string_param()} | + out_opt(). -type start_task_opts() :: [start_task_opt()]. -spec start_task_opts() -> opt_table(). @@ -2295,7 +2433,8 @@ start_task(TaskDefinition, ContainerInstances, Opts, Config) -> %% StopTask %%%------------------------------------------------------------------------------ -type stop_task_opt() :: {cluster, string_param()} | - {reason, string_param()}. + {reason, string_param()} | + out_opt(). -type stop_task_opts() :: [stop_task_opt()]. -spec stop_task_opts() -> opt_table(). stop_task_opts() -> @@ -2342,7 +2481,7 @@ stop_task(Task, Opts, #aws_config{} = Config) -> %%%------------------------------------------------------------------------------ %% UpdateContainerAgent %%%------------------------------------------------------------------------------ --type update_container_agent_opts() :: [{cluster, string_param()}]. +-type update_container_agent_opts() :: [{cluster, string_param()} | out_opt()]. -spec update_container_agent_opts() -> opt_table(). update_container_agent_opts() -> [{cluster, <<"cluster">>, fun to_binary/1}]. @@ -2393,7 +2532,8 @@ update_container_agent(ContainerInstance, Opts, Config) -> -type update_service_opt() :: {cluster, string_param()} | deployment_configuration() | {desired_count, pos_integer()} | - {task_definition, string_param()}. + {task_definition, string_param()} | + out_opt(). -type update_service_opts() :: [update_service_opt()]. -spec update_service_opts() -> opt_table(). update_service_opts() -> @@ -2466,7 +2606,7 @@ ecs_request_no_update(Config, Operation, Body) -> request_body = Payload}, case erlcloud_aws:request_to_return(erlcloud_retry:request(Config, Request, fun ecs_result_fun/1)) of {ok, {_RespHeaders, <<>>}} -> {ok, []}; - {ok, {_RespHeaders, RespBody}} -> {ok, jsx:decode(RespBody)}; + {ok, {_RespHeaders, RespBody}} -> {ok, jsx:decode(RespBody, [{return_maps, false}])}; {error, _} = Error-> Error end. @@ -2500,6 +2640,7 @@ to_binary(undefined) -> undefined; to_binary(true) -> true; to_binary(false) -> false; to_binary(L) when is_list(L), is_list(hd(L)) -> [to_binary(V) || V <- L]; +to_binary(L) when is_list(L), is_binary(hd(L)) -> [to_binary(V) || V <- L]; to_binary(L) when is_list(L) -> list_to_binary(L); to_binary(B) when is_binary(B) -> B; to_binary(A) when is_atom(A) -> atom_to_binary(A, latin1). diff --git a/src/erlcloud_efs.erl b/src/erlcloud_efs.erl new file mode 100644 index 000000000..1cddd8035 --- /dev/null +++ b/src/erlcloud_efs.erl @@ -0,0 +1,214 @@ +-module(erlcloud_efs). + +-include("erlcloud.hrl"). +-include("erlcloud_aws.hrl"). + +%% API +-export([ + describe_file_systems/0, + describe_file_systems/1, + describe_file_systems/2, + describe_file_systems/3, + describe_file_systems_all/0, + describe_file_systems_all/1, + describe_file_systems_all/2, + describe_file_systems_all/3, + describe_file_systems_all/4 +]). + +-type params() :: [param_name() | {param_name(), param_value()}]. +-type param_name() :: binary() | string() | atom() | integer(). +-type param_value() :: binary() | string() | atom() | integer(). + +-type file_system() :: proplist(). +-type file_systems() :: [file_system()]. +-type token() :: binary() | undefined. + +-define(EFS_API_VERSION, "2015-02-01"). + +%% ----------------------------------------------------------------------------- +%% Exported functions +%% ----------------------------------------------------------------------------- + +-spec describe_file_systems() -> Result when + Result :: {ok, file_systems(), token()} | {error, term()}. +describe_file_systems() -> + AwsConfig = erlcloud_aws:default_config(), + describe_file_systems(AwsConfig). + +-spec describe_file_systems(Arg) -> Result when + Arg :: aws_config() | params(), + Result :: {ok, file_systems(), token()} | {error, term()}. +describe_file_systems(AwsConfig) when is_record(AwsConfig, aws_config) -> + describe_file_systems(AwsConfig, _Params = []); +describe_file_systems(Params) -> + AwsConfig = erlcloud_aws:default_config(), + describe_file_systems(AwsConfig, Params). + +-spec describe_file_systems(AwsConfig, Params) -> Result when + AwsConfig :: aws_config(), + Params :: params(), + Result :: {ok, file_systems(), token()} | {error, term()}. +describe_file_systems(AwsConfig, Params) when is_record(AwsConfig, aws_config) -> + Path = [?EFS_API_VERSION, "file-systems"], + case request(AwsConfig, _Method = get, Path, Params) of + {ok, Response} -> + FileSystems = proplists:get_value(<<"FileSystems">>, Response), + case proplists:get_value(<<"NextMarker">>, Response, null) of + null -> + {ok, FileSystems, undefined}; + Marker -> + {ok, FileSystems, Marker} + end; + {error, Reason} -> + {error, Reason} + end. + +-spec describe_file_systems(AwsConfig, Params, Token) -> Result when + AwsConfig :: aws_config(), + Params :: params(), + Token :: token(), + Result :: {ok, file_systems(), token()} | {error, term()}. +describe_file_systems(AwsConfig, Params, Token) when + is_record(AwsConfig, aws_config), is_binary(Token) +-> + describe_file_systems(AwsConfig, [{<<"Marker">>, Token} | Params]); +describe_file_systems(AwsConfig, Params, _Token = undefined) -> + describe_file_systems(AwsConfig, Params). + +-spec describe_file_systems_all() -> Result when + Result :: {ok, file_systems()} | {error, term()}. +describe_file_systems_all() -> + AwsConfig = erlcloud_aws:default_config(), + describe_file_systems_all(AwsConfig). + +-spec describe_file_systems_all(Arg) -> Result when + Arg :: aws_config() | params(), + Result :: {ok, file_systems()} | {error, term()}. +describe_file_systems_all(AwsConfig) when is_record(AwsConfig, aws_config) -> + describe_file_systems_all(AwsConfig, _Params = []); +describe_file_systems_all(Params) when is_list(Params) -> + AwsConfig = erlcloud_aws:default_config(), + describe_file_systems_all(AwsConfig, Params). + +-spec describe_file_systems_all(AwsConfig, Params) -> Result when + AwsConfig :: aws_config(), + Params :: params(), + Result :: {ok, file_systems()} | {error, term()}. +describe_file_systems_all(AwsConfig, Params) when is_record(AwsConfig, aws_config) -> + describe_file_systems_all(AwsConfig, Params, _Token = undefined). + +-spec describe_file_systems_all(AwsConfig, Params, Token) -> Result when + AwsConfig :: aws_config(), + Params :: params(), + Token :: token() | undefined, + Result :: {ok, file_systems()} | {error, term()}. +describe_file_systems_all(AwsConfig, Params, Token) -> + describe_file_systems_all(AwsConfig, Params, Token, _Acc = []). + +-spec describe_file_systems_all(AwsConfig, Params, Token, Acc) -> Result when + AwsConfig :: aws_config(), + Params :: params(), + Token :: token() | undefined, + Acc :: [file_systems()], + Result :: {ok, file_systems()} | {error, term()}. +describe_file_systems_all(AwsConfig, Params, Token, Acc) -> + case describe_file_systems(AwsConfig, Params, Token) of + {ok, FileSystems, undefined} -> + {ok, flatten_pages([FileSystems | Acc])}; + {ok, FileSystems, NextToken} -> + describe_file_systems_all(AwsConfig, Params, NextToken, [FileSystems | Acc]); + {error, Reason} -> + {error, Reason} + end. + +%% ----------------------------------------------------------------------------- +%% Local functions +%% ----------------------------------------------------------------------------- + +request(AwsConfig, Method, Path, Params) -> + request(AwsConfig, Method, Path, Params, _RequestBody = <<>>). + +request(AwsConfig0, Method, Path, Params, RequestBody) -> + case erlcloud_aws:update_config(AwsConfig0) of + {ok, AwsConfig1} -> + AwsRequest0 = init_request(AwsConfig1, Method, Path, Params, RequestBody), + AwsRequest1 = erlcloud_retry:request(AwsConfig1, AwsRequest0, fun should_retry/1), + case AwsRequest1#aws_request.response_type of + ok -> + decode_response(AwsRequest1); + error -> + decode_error(AwsRequest1) + end; + {error, Reason} -> + {error, Reason} + end. + +init_request(AwsConfig, Method, Path, Params, Payload) -> + Scheme = AwsConfig#aws_config.efs_scheme, + Host = AwsConfig#aws_config.efs_host, + Port = AwsConfig#aws_config.efs_port, + Service = "elasticfilesystem", + NormPath = norm_path(Path), + NormParams = norm_params(Params), + Region = erlcloud_aws:aws_region_from_host(Host), + Headers = [{"host", Host}, {"content-type", "application/json"}], + SignedHeaders = erlcloud_aws:sign_v4( + Method, NormPath, AwsConfig, Headers, Payload, Region, Service, Params + ), + #aws_request{ + service = efs, + method = Method, + uri = Scheme ++ Host ++ ":" ++ integer_to_list(Port) ++ NormPath ++ NormParams, + request_headers = SignedHeaders, + request_body = Payload + }. + +norm_path(Path) -> + binary_to_list(iolist_to_binary(["/" | lists:join("/", Path)])). + +norm_params([] = _Params) -> + ""; +norm_params(Params) -> + "?" ++ erlcloud_aws:canonical_query_string(Params). + +should_retry(#aws_request{response_type = ok} = AwsRequest) -> + AwsRequest; +should_retry(#aws_request{response_type = error, response_status = Status} = AwsRequest) when + Status == 429; Status >= 500 +-> + AwsRequest#aws_request{should_retry = true}; +should_retry(#aws_request{} = AwsRequest) -> + AwsRequest#aws_request{should_retry = false}. + +decode_response(#aws_request{response_body = <<>>}) -> + ok; +decode_response(#aws_request{response_body = ResponseBody}) -> + Json = jsx:decode(ResponseBody, [{return_maps, false}]), + {ok, Json}. + +decode_error(#aws_request{error_type = aws} = AwsRequest) -> + Type = extract_error_type(AwsRequest), + Message = extract_error_message(AwsRequest), + {error, {Type, Message}}; +decode_error(AwsRequest) -> + erlcloud_aws:request_to_return(AwsRequest). + +extract_error_type(#aws_request{response_body = ResponseBody} = AwsRequest) -> + ResponseObject = jsx:decode(ResponseBody, [{return_maps, false}]), + case proplists:get_value(<<"ErrorCode">>, ResponseObject) of + undefined -> + Headers = AwsRequest#aws_request.response_headers, + ErrorType = proplists:get_value("x-amzn-errortype", Headers), + iolist_to_binary(ErrorType); + Code -> + Code + end. + +extract_error_message(#aws_request{response_body = ResponseBody}) -> + Object = jsx:decode(ResponseBody, [{return_maps, false}]), + proplists:get_value(<<"Message">>, Object, <<>>). + +-spec flatten_pages([[any()]]) -> [any()]. +flatten_pages(Pages) -> + lists:append(lists:reverse(Pages)). diff --git a/src/erlcloud_elb.erl b/src/erlcloud_elb.erl index 9cf93025d..e7ba68f1b 100644 --- a/src/erlcloud_elb.erl +++ b/src/erlcloud_elb.erl @@ -12,39 +12,48 @@ describe_load_balancer/1, describe_load_balancer/2, - describe_load_balancers/0, describe_load_balancers/1, + describe_load_balancers/0, describe_load_balancers/1, describe_load_balancers/2, describe_load_balancers/3, describe_load_balancers/4, describe_load_balancers_all/0, describe_load_balancers_all/1, describe_load_balancers_all/2, configure_health_check/2, configure_health_check/3, - + create_load_balancer_policy/3, create_load_balancer_policy/4, create_load_balancer_policy/5, delete_load_balancer_policy/2, delete_load_balancer_policy/3, - - describe_load_balancer_policies/0, describe_load_balancer_policies/1, + + describe_load_balancer_policies/0, describe_load_balancer_policies/1, describe_load_balancer_policies/2, describe_load_balancer_policies/3, - - describe_load_balancer_policy_types/0, describe_load_balancer_policy_types/1, + + describe_load_balancer_policy_types/0, describe_load_balancer_policy_types/1, describe_load_balancer_policy_types/2, - describe_load_balancer_attributes/1, describe_load_balancer_attributes/2]). + describe_load_balancer_attributes/1, describe_load_balancer_attributes/2, + + describe_tags/2, describe_tags/1 + ]). -include("erlcloud.hrl"). -include("erlcloud_aws.hrl"). +-include("erlcloud_xmerl.hrl"). -define(API_VERSION, "2012-06-01"). -define(DEFAULT_MAX_RECORDS, 400). % xpath for elb descriptions used in describe_groups functions: --define(DESCRIBE_ELBS_PATH, +-define(DESCRIBE_ELBS_PATH, "/DescribeLoadBalancersResponse/DescribeLoadBalancersResult/LoadBalancerDescriptions/member"). --define(DESCRIBE_ELBS_NEXT_TOKEN, - "/DescribeLoadBalancersResponse/DescribeLoadBalancersResult/NextMarker"). --define(DESCRIBE_ELB_POLICIES_PATH, +-define(DESCRIBE_ELBS_NEXT_TOKEN, + "/DescribeLoadBalancersResponse/NextMarker"). +-define(DESCRIBE_ELB_POLICIES_PATH, "/DescribeLoadBalancerPoliciesResponse/DescribeLoadBalancerPoliciesResult/PolicyDescriptions/member"). --define(DESCRIBE_ELB_POLICY_TYPE_PATH, +-define(DESCRIBE_ELB_POLICY_TYPE_PATH, "/DescribeLoadBalancerPolicyTypesResponse/DescribeLoadBalancerPolicyTypesResult/PolicyTypeDescriptions/member"). +-define(DESCRIBE_ELBS_TAGS_PATH, + "/DescribeTagsResponse/DescribeTagsResult/TagDescriptions/member"). + +-type result() :: {ok, term()} | {error, metadata_not_available | container_credentials_unavailable | erlcloud_aws:httpc_result_error()}. + -import(erlcloud_xml, [get_text/2, get_integer/2, get_list/2]). @@ -106,11 +115,11 @@ delete_load_balancer(LB, Config) when is_list(LB) -> [{"LoadBalancerName", LB}]). --spec register_instance(string(), string()) -> ok | no_return(). +-spec register_instance(string(), string()) -> ok. register_instance(LB, InstanceId) -> register_instance(LB, InstanceId, default_config()). --spec register_instance(string(), string(), aws_config()) -> ok | no_return(). +-spec register_instance(string(), string(), aws_config()) -> ok. register_instance(LB, InstanceId, Config) when is_list(LB) -> elb_simple_request(Config, "RegisterInstancesWithLoadBalancer", @@ -118,11 +127,11 @@ register_instance(LB, InstanceId, Config) when is_list(LB) -> erlcloud_aws:param_list([[{"InstanceId", InstanceId}]], "Instances.member")]). --spec deregister_instance(string(), string()) -> ok | no_return(). +-spec deregister_instance(string(), string()) -> ok. deregister_instance(LB, InstanceId) -> deregister_instance(LB, InstanceId, default_config()). --spec deregister_instance(string(), string(), aws_config()) -> ok | no_return(). +-spec deregister_instance(string(), string(), aws_config()) -> ok. deregister_instance(LB, InstanceId, Config) when is_list(LB) -> elb_simple_request(Config, "DeregisterInstancesFromLoadBalancer", @@ -131,13 +140,13 @@ deregister_instance(LB, InstanceId, Config) when is_list(LB) -> --spec configure_health_check(string(), string()) -> ok | no_return(). +-spec configure_health_check(string(), string()) -> ok. configure_health_check(LB, Target) when is_list(LB), is_list(Target) -> configure_health_check(LB, Target, default_config()). --spec configure_health_check(string(), string(), aws_config()) -> ok | no_return(). +-spec configure_health_check(string(), string(), aws_config()) -> ok. configure_health_check(LB, Target, Config) when is_list(LB) -> elb_simple_request(Config, "ConfigureHealthCheck", @@ -145,8 +154,8 @@ configure_health_check(LB, Target, Config) when is_list(LB) -> {"HealthCheck.Target", Target}]). %% -------------------------------------------------------------------- -%% @doc describe_load_balancer with a specific balancer name or with a -%% specific configuration and specific balancer name. +%% @doc describe_load_balancer with a specific balancer name or with a +%% specific configuration and specific balancer name. %% @end %% -------------------------------------------------------------------- -spec describe_load_balancer(string()) -> {ok, term()} | {{paged, string()}, term()} | {error, metadata_not_available | container_credentials_unavailable | erlcloud_aws:httpc_result_error()}. @@ -163,7 +172,7 @@ describe_load_balancers() -> describe_load_balancers([], default_config()). %% -------------------------------------------------------------------- -%% @doc describe_load_balancers with specific balancer names or with a +%% @doc describe_load_balancers with specific balancer names or with a %% specific configuration. %% @end %% -------------------------------------------------------------------- @@ -176,13 +185,13 @@ describe_load_balancers(Config = #aws_config{}) -> %% @doc Get descriptions of the given load balancers. %% The account calling this function needs permission for the %% elasticloadbalancing:DescribeLoadBalancers action. -%% +%% %% Returns {{paged, NextPageId}, Results} if there are more than %% the current maximum count of results, {ok, Results} if everything %% fits and {error, Reason} if there was a problem. %% @end %% -------------------------------------------------------------------- --spec describe_load_balancers(list(string()), aws_config()) -> +-spec describe_load_balancers(list(string()), aws_config()) -> {ok, term()} | {{paged, string()}, term()} | {error, metadata_not_available | container_credentials_unavailable | erlcloud_aws:httpc_result_error()}. describe_load_balancers(Names, Config) -> describe_load_balancers(Names, ?DEFAULT_MAX_RECORDS, none, Config). @@ -192,25 +201,43 @@ describe_load_balancers(Names, Config) -> %% maximum number of results and optional paging offset. %% @end %% -------------------------------------------------------------------- --spec describe_load_balancers(list(string()), integer(), string() | none, aws_config()) -> +-spec describe_load_balancers(list(string()), integer(), string() | none, aws_config()) -> {ok, term()} | {{paged, string()}, term()} | {error, metadata_not_available | container_credentials_unavailable | erlcloud_aws:httpc_result_error()}. describe_load_balancers(Names, PageSize, none, Config) -> describe_load_balancers(Names, [{"PageSize", PageSize}], Config); describe_load_balancers(Names, PageSize, Marker, Config) -> describe_load_balancers(Names, [{"Marker", Marker}, {"PageSize", PageSize}], Config). --spec describe_load_balancers(list(string()), list({string(), term()}), aws_config()) -> +-spec describe_load_balancers(list(string()), list({string(), term()}), aws_config()) -> {ok, term()} | {{paged, string()}, term()} | {error, metadata_not_available | container_credentials_unavailable | erlcloud_aws:httpc_result_error()}. describe_load_balancers(Names, Params, Config) -> P = member_params("LoadBalancerNames.member.", Names) ++ Params, case elb_query(Config, "DescribeLoadBalancers", P) of {ok, Doc} -> - Elbs = xmerl_xpath:string(?DESCRIBE_ELBS_PATH, Doc), + Elbs = xmerl_xpath:string(?DESCRIBE_ELBS_PATH, Doc), {erlcloud_util:next_token(?DESCRIBE_ELBS_NEXT_TOKEN, Doc), [extract_elb(Elb) || Elb <- Elbs]}; {error, Reason} -> {error, Reason} end. +-spec describe_tags(ElbNames) -> result() when ElbNames :: list(string()). +describe_tags(Names) -> + describe_tags(Names, default_config()). + +-spec describe_tags(ElbNames, AwsConfig) -> result() + when + ElbNames :: list(string()), + AwsConfig :: aws_config(). +describe_tags(Names, Config) -> + P = member_params("LoadBalancerNames.member.", Names), + case elb_query(Config, "DescribeTags", P) of + {ok, Doc} -> + Elbs = xmerl_xpath:string(?DESCRIBE_ELBS_TAGS_PATH, Doc), + {ok, lists:map(fun extract_elb_tags/1, Elbs)}; + {error, Reason} -> + {error, Reason} + end. + -spec describe_load_balancers_all() -> {ok, [term()]} | {error, term()}. describe_load_balancers_all() -> @@ -219,7 +246,7 @@ describe_load_balancers_all() -> -spec describe_load_balancers_all(list(string()) | aws_config()) -> {ok, [term()]} | {error, term()}. describe_load_balancers_all(Config) when is_record(Config, aws_config) -> - describe_load_balancers_all([], default_config()); + describe_load_balancers_all([], Config); describe_load_balancers_all(Names) -> describe_load_balancers_all(Names, default_config()). @@ -232,8 +259,8 @@ describe_load_balancers_all(Names, Config) -> Names, ?DEFAULT_MAX_RECORDS, Marker, Cfg ) end, Config, none, []). - - + + extract_elb(Item) -> [ {load_balancer_name, get_text("LoadBalancerName", Item)}, @@ -270,11 +297,11 @@ extract_listener(Item) -> ]. %% -------------------------------------------------------------------- -%% @doc Calls describe_load_balancer_policies([], +%% @doc Calls describe_load_balancer_policies([], %% default_configuration()) %% @end %% -------------------------------------------------------------------- --spec describe_load_balancer_policies() -> +-spec describe_load_balancer_policies() -> {ok, term()} | {error, term()}. describe_load_balancer_policies() -> describe_load_balancer_policies([], [], default_config()). @@ -283,23 +310,23 @@ describe_load_balancer_policies() -> %% @doc describe_load_balancer_policies with specific config. %% @end %% -------------------------------------------------------------------- --spec describe_load_balancer_policies(aws_config()) -> +-spec describe_load_balancer_policies(aws_config()) -> {ok, term()} | {error, term()}. describe_load_balancer_policies(Config = #aws_config{}) -> describe_load_balancer_policies([], [], Config). %% -------------------------------------------------------------------- -%% @doc describe_load_balancer_policies for specified ELB -%% with specificied policy names using default config. +%% @doc describe_load_balancer_policies for specified ELB +%% with specified policy names using default config. %% @end %% -------------------------------------------------------------------- --spec describe_load_balancer_policies(string(), list() | aws_config()) -> +-spec describe_load_balancer_policies(string(), list() | aws_config()) -> {ok, term()} | {error, term()}. -describe_load_balancer_policies(ElbName, PolicyNames) +describe_load_balancer_policies(ElbName, PolicyNames) when is_list(ElbName), is_list(PolicyNames) -> describe_load_balancer_policies(ElbName, PolicyNames, default_config()); -describe_load_balancer_policies(PolicyNames, Config = #aws_config{}) +describe_load_balancer_policies(PolicyNames, Config = #aws_config{}) when is_list(PolicyNames) -> describe_load_balancer_policies([], PolicyNames, Config). @@ -309,7 +336,7 @@ describe_load_balancer_policies(PolicyNames, Config = #aws_config{}) %% with specified config. %% @end %% -------------------------------------------------------------------- --spec describe_load_balancer_policies(string(), list(), aws_config()) -> +-spec describe_load_balancer_policies(string(), list(), aws_config()) -> {ok, term()} | {error, term()}. describe_load_balancer_policies(ElbName, PolicyNames, Config) when is_list(ElbName), @@ -323,7 +350,7 @@ describe_load_balancer_policies(ElbName, PolicyNames, Config) Params = ElbNameParam ++ member_params("PolicyNames.member.", PolicyNames), case elb_query(Config, "DescribeLoadBalancerPolicies", Params) of {ok, Doc} -> - ElbPolicies = xmerl_xpath:string(?DESCRIBE_ELB_POLICIES_PATH, Doc), + ElbPolicies = xmerl_xpath:string(?DESCRIBE_ELB_POLICIES_PATH, Doc), {ok, [extract_elb_policy(ElbPolicy) || ElbPolicy <- ElbPolicies]}; {error, Reason} -> {error, Reason} @@ -333,8 +360,8 @@ extract_elb_policy(Item) -> [ {policy_name, get_text("PolicyName", Item)}, {policy_type_name, get_text("PolicyTypeName", Item)}, - {policy_attributes, - [extract_policy_attribute(A) || + {policy_attributes, + [extract_policy_attribute(A) || A <- xmerl_xpath:string("PolicyAttributeDescriptions/member", Item)]} ]. @@ -345,7 +372,7 @@ extract_policy_attribute(Item) -> ]. %% -------------------------------------------------------------------- -%% @doc Calls describe_load_balancer_policy_types([], +%% @doc Calls describe_load_balancer_policy_types([], %% default_configuration()) %% @end %% -------------------------------------------------------------------- @@ -353,7 +380,7 @@ describe_load_balancer_policy_types() -> describe_load_balancer_policy_types([], default_config()). %% -------------------------------------------------------------------- -%% @doc describe_load_balancer_policy_types() with specific +%% @doc describe_load_balancer_policy_types() with specific %% policy type names. %% @end %% -------------------------------------------------------------------- @@ -366,13 +393,13 @@ describe_load_balancer_policy_types(Config = #aws_config{}) -> %% @doc Get descriptions of the given load balancer policy types. %% @end %% -------------------------------------------------------------------- --spec describe_load_balancer_policy_types(list(string()), aws_config()) -> +-spec describe_load_balancer_policy_types(list(string()), aws_config()) -> {ok, term()} | {error, term()}. describe_load_balancer_policy_types(PolicyTypeNames, Config) -> P = member_params("PolicyTypeNames.member.", PolicyTypeNames), case elb_query(Config, "DescribeLoadBalancerPolicyTypes", P) of {ok, Doc} -> - ElbPolicyTypes = xmerl_xpath:string(?DESCRIBE_ELB_POLICY_TYPE_PATH, Doc), + ElbPolicyTypes = xmerl_xpath:string(?DESCRIBE_ELB_POLICY_TYPE_PATH, Doc), {ok, [extract_elb_policy_type(ElbPolicyType) || ElbPolicyType <- ElbPolicyTypes]}; {error, Reason} -> {error, Reason} @@ -382,8 +409,8 @@ extract_elb_policy_type(Item) -> [ {policy_type_name, get_text("PolicyTypeName", Item)}, {policy_type_description, get_text("Description", Item)}, - {policy_type_attributes, - [extract_policy_type_attribute(A) || + {policy_type_attributes, + [extract_policy_type_attribute(A) || A <- xmerl_xpath:string("PolicyAttributeTypeDescriptions/member", Item)]} ]. @@ -396,13 +423,13 @@ extract_policy_type_attribute(Item) -> {default_value, get_text("DefaultValue", Item)} ]. %% -------------------------------------------------------------------- -%% @doc Calls create_load_balancer_policy() without attributes and +%% @doc Calls create_load_balancer_policy() without attributes and %% with default aws config. %% @end %% -------------------------------------------------------------------- --spec create_load_balancer_policy(string(), string(), string()) -> - ok | {error, term()} | no_return(). -create_load_balancer_policy(LB, PolicyName, PolicyTypeName) +-spec create_load_balancer_policy(string(), string(), string()) -> + ok | {error, term()}. +create_load_balancer_policy(LB, PolicyName, PolicyTypeName) when is_list(LB), is_list(PolicyName), is_list(PolicyTypeName) -> @@ -412,9 +439,9 @@ create_load_balancer_policy(LB, PolicyName, PolicyTypeName) %% @doc Calls create_load_balancer_policy() with default aws config. %% @end %% -------------------------------------------------------------------- --spec create_load_balancer_policy(string(), string(), string(), list({string(), string()})) -> - ok | {error, term()} | no_return(). -create_load_balancer_policy(LB, PolicyName, PolicyTypeName, Attrs) +-spec create_load_balancer_policy(string(), string(), string(), list({string(), string()})) -> + ok | {error, term()}. +create_load_balancer_policy(LB, PolicyName, PolicyTypeName, Attrs) when is_list(LB), is_list(PolicyName), is_list(PolicyTypeName), @@ -426,9 +453,9 @@ create_load_balancer_policy(LB, PolicyName, PolicyTypeName, Attrs) %% http://docs.aws.amazon.com/ElasticLoadBalancing/latest/APIReference/API_CreateLoadBalancerPolicy.html %% @end %% -------------------------------------------------------------------- --spec create_load_balancer_policy(string(), string(), string(), list({string(), string()}), aws_config()) -> - ok | {error, term()} | no_return(). -create_load_balancer_policy(LB, PolicyName, PolicyTypeName, AttrList, Config) +-spec create_load_balancer_policy(string(), string(), string(), list({string(), string()}), aws_config()) -> + ok | {error, term()}. +create_load_balancer_policy(LB, PolicyName, PolicyTypeName, AttrList, Config) when is_list(LB), is_list(PolicyName), is_list(PolicyTypeName), @@ -439,7 +466,7 @@ create_load_balancer_policy(LB, PolicyName, PolicyTypeName, AttrList, Config) {"PolicyName", PolicyName}, {"PolicyTypeName", PolicyTypeName} | erlcloud_aws:param_list([[{"AttributeName", AttrName}, - {"AttributeValue", AttrValue}] || + {"AttributeValue", AttrValue}] || {AttrName, AttrValue} <- AttrList], "PolicyAttributes.member")]), ok. @@ -460,7 +487,7 @@ describe_load_balancer_attributes(Name, Config) -> %%%=================================================================== %%% Internal functions %%%=================================================================== --spec extract_elb_attribs(proplist()) -> proplist(). +-spec extract_elb_attribs(xmerl_xpath_doc_nodes()) -> proplist() | no_return(). extract_elb_attribs(Node) -> RootPath = "DescribeLoadBalancerAttributesResult/LoadBalancerAttributes", erlcloud_xml:decode( @@ -482,17 +509,17 @@ extract_elb_attribs(Node) -> %% @doc Calls delete_load_balancer_policy() with default aws config. %% @end %% -------------------------------------------------------------------- --spec delete_load_balancer_policy(string(), string()) -> ok | no_return(). -delete_load_balancer_policy(LB, PolicyName) when is_list(LB), +-spec delete_load_balancer_policy(string(), string()) -> ok. +delete_load_balancer_policy(LB, PolicyName) when is_list(LB), is_list(PolicyName) -> delete_load_balancer_policy(LB, PolicyName, default_config()). %% -------------------------------------------------------------------- -%% @doc Deletes the specified policy from the specified load balancer. +%% @doc Deletes the specified policy from the specified load balancer. %% This policy must not be enabled for any listeners. %% @end %% -------------------------------------------------------------------- --spec delete_load_balancer_policy(string(), string(), aws_config()) -> ok | no_return(). +-spec delete_load_balancer_policy(string(), string(), aws_config()) -> ok. delete_load_balancer_policy(LB, PolicyName, Config) when is_list(LB), is_list(PolicyName)-> elb_simple_request(Config, @@ -500,15 +527,15 @@ delete_load_balancer_policy(LB, PolicyName, Config) when is_list(LB), [{"LoadBalancerName", LB}, {"PolicyName", PolicyName}]). -%% given a list of member identifiers, return a list of +%% given a list of member identifiers, return a list of %% {key with prefix, member identifier} for use in elb calls. -%% Example pair that could be returned in a list is +%% Example pair that could be returned in a list is %% {"LoadBalancerNames.member.1", "my-elb}. -spec member_params(string(), list(string())) -> list({string(), string()}). member_params(Prefix, MemberIdentifiers) -> MemberKeys = [Prefix ++ integer_to_list(I) || I <- lists:seq(1, length(MemberIdentifiers))], [{K, V} || {K, V} <- lists:zip(MemberKeys, MemberIdentifiers)]. - + describe_all(Fun, AwsConfig, Marker, Acc) -> case Fun(Marker, AwsConfig) of @@ -545,3 +572,16 @@ elb_request(Config, Action, Params) -> {error, Reason} -> erlang:error({aws_error, Reason}) end. + + +extract_elb_tags(Item) -> + [ + {load_balancer_name, get_text("LoadBalancerName", Item)}, + {tags, [extract_tag(L) || L <- xmerl_xpath:string("Tags/member", Item)]} + ]. + +extract_tag(Item) -> + [ + {value, get_text("Value", Item)}, + {key, get_text("Key", Item)} + ]. diff --git a/src/erlcloud_emr.erl b/src/erlcloud_emr.erl index f874e714a..ed716e4df 100644 --- a/src/erlcloud_emr.erl +++ b/src/erlcloud_emr.erl @@ -202,16 +202,16 @@ request_no_update(Action, Json, Scheme, Host, Port, Service, Opts, Cfg) -> Region = erlcloud_aws:aws_region_from_host(Host), Headers = erlcloud_aws:sign_v4_headers(Cfg, H1, ReqBody, Region, Service) ++ H2, case erlcloud_aws:aws_request_form_raw(post, Scheme, Host, Port, - "/", ReqBody, Headers, Cfg) of + "/", ReqBody, Headers, [], Cfg) of {ok, Body} -> case proplists:get_value(out, Opts, json) of raw -> {ok, Body}; _ -> case Body of <<>> -> {ok, <<>>}; - _ -> {ok, jsx:decode(Body)} + _ -> {ok, jsx:decode(Body, [{return_maps, false}])} end end; {error, {http_error, _Code, _StatusLine, ErrBody}} -> - {error, {aws_error, jsx:decode(ErrBody)}}; + {error, {aws_error, jsx:decode(ErrBody, [{return_maps, false}])}}; {error, {socket_error, Reason}} -> {error, {socket_error, Reason}} end. diff --git a/src/erlcloud_guardduty.erl b/src/erlcloud_guardduty.erl new file mode 100644 index 000000000..1f8b8d9e9 --- /dev/null +++ b/src/erlcloud_guardduty.erl @@ -0,0 +1,135 @@ +-module(erlcloud_guardduty). + +-include_lib("erlcloud/include/erlcloud.hrl"). +-include_lib("erlcloud/include/erlcloud_aws.hrl"). + +%%------------------------------------------------------------------------------ +%% Library initialization. +%%------------------------------------------------------------------------------ +-export([configure/2, configure/3, new/2, new/3]). + +%%------------------------------------------------------------------------------ +%% GuardDuty API Functions +%%------------------------------------------------------------------------------ +-export([ + get_detector/1, get_detector/2, + list_detectors/0, list_detectors/1, list_detectors/2, list_detectors/3 +]). + +-import(erlcloud_util, [filter_undef/1]). + +-type gd_return() :: {ok, proplist()} | {error, term()}. + +-spec new(AccessKeyID ::string(), SecretAccessKey :: string()) -> aws_config(). +new(AccessKeyID, SecretAccessKey) -> + #aws_config{access_key_id=AccessKeyID, + secret_access_key=SecretAccessKey}. + +-spec new(AccessKeyID :: string(), SecretAccessKey :: string(), Host :: string()) -> aws_config(). +new(AccessKeyID, SecretAccessKey, Host) -> + #aws_config{access_key_id=AccessKeyID, + secret_access_key=SecretAccessKey, + guardduty_host=Host}. + +-spec configure(AccessKeyID :: string(), SecretAccessKey :: string()) -> ok. +configure(AccessKeyID, SecretAccessKey) -> + put(aws_config, new(AccessKeyID, SecretAccessKey)), + ok. + +-spec configure(AccessKeyID :: string(), SecretAccessKey :: string(), Host :: string()) -> ok. +configure(AccessKeyID, SecretAccessKey, Host) -> + put(aws_config, new(AccessKeyID, SecretAccessKey, Host)), + ok. + +%%------------------------------------------------------------------------------ +%% API +%%------------------------------------------------------------------------------ + +%%------------------------------------------------------------------------------ +%% @doc +%% GuardDuty API: +%% [https://docs.aws.amazon.com/guardduty/latest/ug/get-detector.html] +%% +%%------------------------------------------------------------------------------ + +-spec get_detector(DetectorId :: binary()) -> gd_return(). +get_detector(DetectorId) -> + get_detector(DetectorId, default_config()). + +-spec get_detector(DetectorId :: binary(), + Config :: aws_config()) -> gd_return(). +get_detector(DetectorId, Config) -> + Path = "/detector/" ++ binary_to_list(DetectorId), + guardduty_request(Config, get, Path, undefined). + + +%%------------------------------------------------------------------------------ +%% @doc +%% GuardDuty API: +%% [https://docs.aws.amazon.com/guardduty/latest/ug/list-detectors.html] +%% +%%------------------------------------------------------------------------------ + +-spec list_detectors() -> gd_return(). +list_detectors() -> list_detectors(default_config()). + +-spec list_detectors(Config :: aws_config()) -> gd_return(). +list_detectors(Config) -> + list_detectors(undefined, undefined, Config). + +-spec list_detectors(Marker :: binary(), + MaxItems :: integer()) -> gd_return(). +list_detectors(Marker, MaxItems) -> + list_detectors(Marker, MaxItems, default_config()). + +-spec list_detectors(Marker :: undefined | binary(), + MaxItems :: undefined | integer(), + Config :: aws_config()) -> gd_return(). +list_detectors(Marker, MaxItems, Config) -> + Path = "/detector", + QParams = filter_undef([{"Marker", Marker}, + {"MaxItems", MaxItems}]), + guardduty_request(Config, get, Path, undefined, QParams). + + +%%%------------------------------------------------------------------------------ +%%% Internal Functions +%%%------------------------------------------------------------------------------ + +guardduty_request(Config, Method, Path, Body) -> + guardduty_request(Config, Method, Path, Body, []). + +guardduty_request(Config, Method, Path, Body, QParam) -> + case erlcloud_aws:update_config(Config) of + {ok, Config1} -> + guardduty_request_no_update(Config1, Method, Path, Body, QParam); + {error, Reason} -> + {error, Reason} + end. + +guardduty_request_no_update(Config, Method, Path, Body, QParam) -> + Form = case encode_body(Body) of + <<>> -> erlcloud_http:make_query_string(QParam); + Value -> Value + end, + Headers = headers(Method, Path, Config, encode_body(Body), QParam), + case erlcloud_aws:aws_request_form_raw( + Method, Config#aws_config.guardduty_scheme, Config#aws_config.guardduty_host, + Config#aws_config.guardduty_port, Path, Form, Headers, [], Config) of + {ok, Data} -> + {ok, jsx:decode(Data, [{return_maps, false}])}; + E -> + E + end. + +encode_body(undefined) -> + <<>>. + +headers(Method, Uri, Config, Body, QParam) -> + Headers = [{"host", Config#aws_config.guardduty_host}, + {"content-type", "application/json"}], + Region = erlcloud_aws:aws_region_from_host(Config#aws_config.guardduty_host), + erlcloud_aws:sign_v4(Method, Uri, Config, + Headers, Body, Region, "guardduty", QParam). + +default_config() -> erlcloud_aws:default_config(). diff --git a/src/erlcloud_httpc.erl b/src/erlcloud_httpc.erl index 73dd16d93..6408e5822 100644 --- a/src/erlcloud_httpc.erl +++ b/src/erlcloud_httpc.erl @@ -25,6 +25,19 @@ {error, any()}). -export_type([request_fun/0]). +% Imported from lhttpc_types.hrl +-type body() :: binary() + | undefined % HEAD request. + | pid(). % When partial_download option is used. + +-type headers() :: [{atom() | string(), iodata()}]. % atom is of type 'Cache-Control' | 'Connection' | 'Date' | ... +-export_type([headers/0]). + +-type result() :: {ok, {{StatusCode :: pos_integer(), StatusMsg :: string()}, headers(), body()}} + | {ok, {pid(), WindowSize :: non_neg_integer() | infinity}} + | {error, atom()}. +-export_type([result/0]). + request(URL, Method, Hdrs, Body, Timeout, #aws_config{http_client = lhttpc} = Config) -> request_lhttpc(URL, Method, Hdrs, Body, Timeout, Config); @@ -43,10 +56,16 @@ request(URL, Method, Hdrs, Body, Timeout, when is_function(F, 6) -> F(URL, Method, Hdrs, Body, Timeout, Config). -request_lhttpc(URL, Method, Hdrs, Body, Timeout, #aws_config{lhttpc_pool = undefined}) -> +request_lhttpc(URL, Method, Hdrs, Body, Timeout, #aws_config{lhttpc_pool = undefined, http_proxy = undefined}) -> lhttpc:request(URL, Method, Hdrs, Body, Timeout, []); -request_lhttpc(URL, Method, Hdrs, Body, Timeout, #aws_config{lhttpc_pool = Pool}) -> +request_lhttpc(URL, Method, Hdrs, Body, Timeout, #aws_config{http_proxy = HttpProxy, lhttpc_pool = undefined}) -> + LHttpcOpts = [{proxy, HttpProxy}], + lhttpc:request(URL, Method, Hdrs, Body, Timeout, LHttpcOpts); +request_lhttpc(URL, Method, Hdrs, Body, Timeout, #aws_config{lhttpc_pool = Pool, http_proxy = undefined}) -> LHttpcOpts = [{pool, Pool}, {pool_ensure, true}], + lhttpc:request(URL, Method, Hdrs, Body, Timeout, LHttpcOpts); +request_lhttpc(URL, Method, Hdrs, Body, Timeout, #aws_config{lhttpc_pool = Pool, http_proxy = HttpProxy}) -> + LHttpcOpts = [{pool, Pool}, {pool_ensure, true}, {proxy, HttpProxy}], lhttpc:request(URL, Method, Hdrs, Body, Timeout, LHttpcOpts). %% Guard clause protects against empty bodied requests from being @@ -70,7 +89,13 @@ request_httpc(URL, Method, Hdrs, Body, Timeout, _Config) -> [{timeout, Timeout}], [{body_format, binary}])). -request_hackney(URL, Method, Hdrs, Body, Timeout, #aws_config{hackney_pool = Pool}) -> +request_hackney(URL, Method, Hdrs, Body, Timeout, + #aws_config{hackney_pool = Pool, + hackney_client_options = #hackney_client_options{ + insecure = Insecure, + proxy = Proxy, + proxy_auth = ProxyAuth}} + ) -> BinURL = to_binary(URL), BinHdrs = [{to_binary(K), to_binary(V)} || {K, V} <- Hdrs], PoolOpt = if Pool =:= undefined -> @@ -78,10 +103,12 @@ request_hackney(URL, Method, Hdrs, Body, Timeout, #aws_config{hackney_pool = Poo true -> [{pool, Pool}] end, + HttpProxyOpt = [{proxy, Proxy}, {proxy_auth, ProxyAuth}], response_hackney(hackney:request(Method, BinURL, BinHdrs, Body, - [{recv_timeout, Timeout}] ++ PoolOpt)). + [{recv_timeout, Timeout}, {insecure, Insecure}] ++ + PoolOpt ++ HttpProxyOpt)). response_httpc({ok, {{_HTTPVer, Status, StatusLine}, Headers, Body}}) -> {ok, {{Status, StatusLine}, Headers, Body}}; diff --git a/src/erlcloud_iam.erl b/src/erlcloud_iam.erl index 7df62a677..3a968871a 100644 --- a/src/erlcloud_iam.erl +++ b/src/erlcloud_iam.erl @@ -4,6 +4,7 @@ -include("erlcloud.hrl"). -include("erlcloud_aws.hrl"). +-include("erlcloud_iam.hrl"). -include_lib("xmerl/include/xmerl.hrl"). %% Library initialization. @@ -48,16 +49,24 @@ list_entities_for_policy_all/1, list_entities_for_policy_all/2, list_entities_for_policy_all/3, list_entities_for_policy_all/4, get_policy/1, get_policy/2, get_policy_version/2, get_policy_version/3, + get_server_certificate/1, get_server_certificate/2, + list_server_certificates/0, list_server_certificates/1, list_server_certificates/2, + list_server_certificates_all/0, list_server_certificates_all/1, list_server_certificates_all/2, + list_server_certificate_tags/1, list_server_certificate_tags/2, + list_server_certificate_tags_all/1, list_server_certificate_tags_all/2, list_instance_profiles/0, list_instance_profiles/1, list_instance_profiles/2, list_instance_profiles_all/0, list_instance_profiles_all/1, list_instance_profiles_all/2, get_instance_profile/1, get_instance_profile/2, - get_account_authorization_details/0, get_account_authorization_details/1, + get_account_authorization_details/0, get_account_authorization_details/1, get_account_authorization_details/2, get_account_summary/0, get_account_summary/1, get_account_password_policy/0, get_account_password_policy/1, generate_credential_report/0, generate_credential_report/1, get_credential_report/0, get_credential_report/1, simulate_principal_policy/2, simulate_principal_policy/3, - simulate_custom_policy/2, simulate_custom_policy/3 + simulate_custom_policy/2, simulate_custom_policy/3, simulate_custom_policy/4, + list_virtual_mfa_devices/0, list_virtual_mfa_devices/1, list_virtual_mfa_devices/2, + list_virtual_mfa_devices/3, list_virtual_mfa_devices/4, + list_virtual_mfa_devices_all/0, list_virtual_mfa_devices_all/1, list_virtual_mfa_devices_all/2 ]). -export([get_uri/2]). @@ -412,7 +421,7 @@ get_role(RoleName) when is_list(RoleName) -> -spec get_role(string(), aws_config()) -> {ok, proplist()} | {error, any()}. get_role(RoleName, Config) -> get_role_impl([{"RoleName", RoleName}], Config). - + get_role_impl(RoleNameParam, #aws_config{} = Config) -> ItemPath = "/GetRoleResponse/GetRoleResult/Role", case iam_query(Config, "GetRole", RoleNameParam, ItemPath, data_type("Role")) of @@ -432,7 +441,7 @@ list_roles(PathPrefix) -> list_roles(PathPrefix, #aws_config{} = Config) when is_list(PathPrefix) -> ItemPath = "/ListRolesResponse/ListRolesResult/Roles/member", - iam_query(Config, "ListRoles", [{"PathPrefix", PathPrefix}], ItemPath, data_type("Role")). + iam_query(Config, "ListRoles", [{"PathPrefix", PathPrefix}], ItemPath, data_type("RoleList")). -spec list_roles_all() -> {ok, proplist()} | {error, any()}. list_roles_all() -> list_roles([]). @@ -446,7 +455,7 @@ list_roles_all(PathPrefix) -> list_roles_all(PathPrefix, #aws_config{} = Config) when is_list(PathPrefix) -> ItemPath = "/ListRolesResponse/ListRolesResult/Roles/member", - iam_query_all(Config, "ListRoles", [{"PathPrefix", PathPrefix}], ItemPath, data_type("Role")). + iam_query_all(Config, "ListRoles", [{"PathPrefix", PathPrefix}], ItemPath, data_type("RoleList")). -spec list_role_policies(string()) -> {ok, proplist()} | {ok, proplist(), string()} | {error, any()}. list_role_policies(RoleName) -> @@ -610,6 +619,107 @@ get_policy_version(PolicyArn, VersionId, #aws_config{} = Config) ItemPath = "/GetPolicyVersionResponse/GetPolicyVersionResult/PolicyVersion", iam_query(Config, "GetPolicyVersion", [{"PolicyArn", PolicyArn}, {"VersionId", VersionId}], ItemPath, data_type("PolicyVersion")). +% +% ServerCertificate +% + +-spec get_server_certificate(string()) -> {ok, proplist()} | {error, any()}. +get_server_certificate(ServerCertificateName) -> + get_server_certificate(ServerCertificateName, default_config()). + +-spec get_server_certificate(string(), aws_config()) -> {ok, proplist()} | {error, any()}. +get_server_certificate(ServerCertificateName, #aws_config{} = Config) -> + Action = "GetServerCertificate", + Params = [{"ServerCertificateName", ServerCertificateName}], + case iam_query(Config, Action, Params) of + {ok, Doc} -> + ItemPath = "/GetServerCertificateResponse/GetServerCertificateResult/ServerCertificate", + [Item] = erlcloud_util:get_items(ItemPath, Doc), + Schema = [ + {server_certificate_metadata, "ServerCertificateMetadata", + {single, [ + {server_certificate_name, "ServerCertificateName", text}, + {server_certificate_id, "ServerCertificateId", text}, + {path, "Path", text}, + {arn, "Arn", text}, + {upload_date, "UploadDate", time}, + {expiration, "Expiration", time} + ]} + }, + {certificate_body, "CertificateBody", text}, + {certificate_chain, "CertificateChain", optional_text} + ], + {ok, erlcloud_xml:decode(Schema, Item)}; + {error, _} = Error -> + Error + end. + +-spec list_server_certificates() -> {ok, [proplist()]} | {ok, [proplist()], string()} | {error, any()}. +list_server_certificates() -> + list_server_certificates(default_config()). + +-spec list_server_certificates(string() | aws_config()) -> {ok, [proplist()]} | {ok, [proplist()], string()} | {error, any()}. +list_server_certificates(#aws_config{} = Config) -> + list_server_certificates("/", Config); +list_server_certificates(PathPrefix) -> + list_server_certificates(PathPrefix, default_config()). + +-spec list_server_certificates(string(), aws_config()) -> {ok, [proplist()]} | {ok, [proplist()], string()} | {error, any()}. +list_server_certificates(PathPrefix, #aws_config{} = Config) + when is_list(PathPrefix) -> + Action = "ListServerCertificates", + Params = [{"PathPrefix", PathPrefix}], + ItemPath = "/ListServerCertificatesResponse/ListServerCertificatesResult/ServerCertificateMetadataList/member", + DataTypeDef = data_type("ServerCertificateMetadataList"), + iam_query(Config, Action, Params, ItemPath, DataTypeDef). + +-spec list_server_certificates_all() -> {ok, [proplist()]} | {error, any()}. +list_server_certificates_all() -> + list_server_certificates_all(default_config()). + +-spec list_server_certificates_all(string() | aws_config()) -> {ok, [proplist()]} | {error, any()}. +list_server_certificates_all(#aws_config{} = Config) -> + list_server_certificates_all("/", Config); +list_server_certificates_all(PathPrefix) -> + list_server_certificates_all(PathPrefix, default_config()). + +-spec list_server_certificates_all(string(), aws_config()) -> {ok, [proplist()]} | {error, any()}. +list_server_certificates_all(PathPrefix, #aws_config{} = Config) + when is_list(PathPrefix) -> + Action = "ListServerCertificates", + Params = [{"PathPrefix", PathPrefix}], + ItemPath = "/ListServerCertificatesResponse/ListServerCertificatesResult/ServerCertificateMetadataList/member", + DataTypeDef = data_type("ServerCertificateMetadataList"), + iam_query_all(Config, Action, Params, ItemPath, DataTypeDef). + +-spec list_server_certificate_tags(string()) -> {ok, [proplist()]} | {error, any()}. +list_server_certificate_tags(ServerCertificateName) + when is_list(ServerCertificateName) -> + list_server_certificate_tags(ServerCertificateName, default_config()). + +-spec list_server_certificate_tags(string(), aws_config()) -> {ok, [proplist()]} | {error, any()}. +list_server_certificate_tags(ServerCertificateName, #aws_config{} = Config) + when is_list(ServerCertificateName) -> + Action = "ListServerCertificateTags", + Params = [{"ServerCertificateName", ServerCertificateName}], + ItemPath = "/ListServerCertificateTagsResponse/ListServerCertificateTagsResult/Tags/member", + DataTypeDef = data_type("ServerCertificateTags"), + iam_query(Config, Action, Params, ItemPath, DataTypeDef). + +-spec list_server_certificate_tags_all(string()) -> {ok, [proplist()]} | {error, any()}. +list_server_certificate_tags_all(ServerCertificateName) + when is_list(ServerCertificateName) -> + list_server_certificate_tags_all(ServerCertificateName, default_config()). + +-spec list_server_certificate_tags_all(string(), aws_config()) -> {ok, [proplist()]} | {error, any()}. +list_server_certificate_tags_all(ServerCertificateName, #aws_config{} = Config) + when is_list(ServerCertificateName) -> + Action = "ListServerCertificateTags", + Params = [{"ServerCertificateName", ServerCertificateName}], + ItemPath = "/ListServerCertificateTagsResponse/ListServerCertificateTagsResult/Tags/member", + DataTypeDef = data_type("ServerCertificateTags"), + iam_query_all(Config, Action, Params, ItemPath, DataTypeDef). + % % InstanceProfile % @@ -660,25 +770,33 @@ get_instance_profile(ProfileName, #aws_config{} = Config) -> % % Account APIs % --spec get_account_authorization_details() -> {ok, proplist()} | {error, any()}. +-spec get_account_authorization_details() -> {ok, proplist()} | {ok, proplist(), string()} | {error, any()}. get_account_authorization_details() -> get_account_authorization_details(default_config()). --spec get_account_authorization_details(aws_config()) -> {ok, proplist()} | {error, any()}. +-spec get_account_authorization_details(list() | aws_config()) -> {ok, proplist()} | {ok, proplist(), string()} | {error, any()}. get_account_authorization_details(#aws_config{} = Config) -> + get_account_authorization_details([], #aws_config{} = Config); +get_account_authorization_details(Params) -> + get_account_authorization_details(Params, default_config()). + +-spec get_account_authorization_details(list(), aws_config()) -> {ok, proplist()} | {ok, proplist(), string()} | {error, any()}. +get_account_authorization_details(Params, #aws_config{} = Config) when is_list(Params) -> ItemPath = "/GetAccountAuthorizationDetailsResponse/GetAccountAuthorizationDetailsResult", DataTypeDef = data_type("AccountAuthorizationDetails"), - case iam_query(Config, "GetAccountAuthorizationDetails", [], ItemPath, DataTypeDef) of + case iam_query(Config, "GetAccountAuthorizationDetails", Params, ItemPath, DataTypeDef) of {ok, [Summary]} -> {ok, Summary}; + {ok, [Summary], Marker} -> + {ok, Summary, Marker}; {error, _} = Error -> Error end. --spec get_account_summary() -> {ok, proplist()} | {error, any()}. +-spec get_account_summary() -> {ok, [proplist()]} | {error, any()}. get_account_summary() -> get_account_summary(default_config()). --spec get_account_summary(aws_config()) -> {ok, proplist()} | {error, any()}. +-spec get_account_summary(aws_config()) -> {ok, [proplist()]} | {error, any()}. get_account_summary(#aws_config{} = Config) -> case iam_query(Config, "GetAccountSummary", []) of {ok, Doc} -> @@ -737,18 +855,101 @@ simulate_principal_policy(PolicySourceArn, ActionNames, #aws_config{} = Config) simulate_custom_policy(ActionNames, PolicyInputList) -> simulate_custom_policy(ActionNames, PolicyInputList, default_config()). +-spec simulate_custom_policy(list(), list(), aws_config() | context_entries()) -> {ok, proplist()} | {error, any()}. simulate_custom_policy(ActionNames, PolicyInputList, #aws_config{} = Config) when is_list(ActionNames), is_list(PolicyInputList) -> ItemPath = "/SimulateCustomPolicyResponse/SimulateCustomPolicyResult/" "EvaluationResults/member", Params = erlcloud_util:encode_list("ActionNames", ActionNames) ++ erlcloud_util:encode_list("PolicyInputList", PolicyInputList), + iam_query_all(Config, "SimulateCustomPolicy", Params, + ItemPath, data_type("EvaluationResult")); +simulate_custom_policy(ActionNames, PolicyInputList, ContextEntries) -> + simulate_custom_policy(ActionNames, PolicyInputList, ContextEntries, default_config()). + +simulate_custom_policy(ActionNames, PolicyInputList, ContextEntries, #aws_config{} = Config) + when is_list(ActionNames), is_list(PolicyInputList) -> + ItemPath = "/SimulateCustomPolicyResponse/SimulateCustomPolicyResult/" + "EvaluationResults/member", + + Params = erlcloud_util:encode_list("ActionNames", ActionNames) ++ + erlcloud_util:encode_list("PolicyInputList", PolicyInputList) ++ + encode_context_entries(ContextEntries), iam_query_all(Config, "SimulateCustomPolicy", Params, ItemPath, data_type("EvaluationResult")). +-spec list_virtual_mfa_devices() -> {ok, [proplist()]} | {ok, [proplist()], string()} | {error, any()}. +list_virtual_mfa_devices() -> + list_virtual_mfa_devices(default_config()). + +-spec list_virtual_mfa_devices(string() | aws_config()) -> {ok, [proplist()]} | {ok, [proplist()], string()} | {error, any()}. +list_virtual_mfa_devices(#aws_config{} = Config) -> + list_virtual_mfa_devices(undefined, undefined, undefined, Config); +list_virtual_mfa_devices(AssignmentStatus) -> + list_virtual_mfa_devices(AssignmentStatus, undefined, undefined, default_config()). + +-spec list_virtual_mfa_devices(string(), string() | aws_config()) -> {ok, [proplist()]} | {ok, [proplist()], string()} | {error, any()}. +list_virtual_mfa_devices(AssignmentStatus, #aws_config{} = Config) -> + list_virtual_mfa_devices(AssignmentStatus, undefined, undefined, Config); +list_virtual_mfa_devices(AssignmentStatus, Marker) -> + list_virtual_mfa_devices(AssignmentStatus, Marker, undefined, default_config()). + +-spec list_virtual_mfa_devices(string(), string(), string()| aws_config()) -> {ok, [proplist()]} | {ok, [proplist()], string()} | {error, any()}. +list_virtual_mfa_devices(AssignmentStatus, Marker, #aws_config{} = Config) -> + list_virtual_mfa_devices(AssignmentStatus, Marker, undefined, Config); +list_virtual_mfa_devices(AssignmentStatus, Marker, MaxItems) -> + list_virtual_mfa_devices(AssignmentStatus, Marker, MaxItems, default_config()). + +-spec list_virtual_mfa_devices(undefined | string(), undefined | string(), undefined | string(), aws_config()) -> {ok, [proplist()]} | {ok, [proplist()], string()} | {error, any()}. +list_virtual_mfa_devices(AssignmentStatus, Marker, MaxItems, #aws_config{} = Config) -> + Params = make_list_virtual_mfa_devices_params(AssignmentStatus, Marker, MaxItems), + ItemPath = "/ListVirtualMFADevicesResponse/ListVirtualMFADevicesResult/VirtualMFADevices/member", + iam_query(Config, "ListVirtualMFADevices", Params, ItemPath, data_type("VirtualMFADeviceMetadata")). + + +-spec list_virtual_mfa_devices_all() -> {ok, [proplist()]} | {error, any()}. +list_virtual_mfa_devices_all() -> + list_virtual_mfa_devices_all(default_config()). + +-spec list_virtual_mfa_devices_all(string() | aws_config()) -> {ok, [proplist()]} | {error, any()}. +list_virtual_mfa_devices_all(#aws_config{} = Config) -> + list_virtual_mfa_devices_all(undefined, Config); +list_virtual_mfa_devices_all(AssignmentStatus) -> + list_virtual_mfa_devices_all(AssignmentStatus, default_config()). + +-spec list_virtual_mfa_devices_all(undefined | string(), aws_config()) -> {ok, [proplist()]} | {error, any()}. +list_virtual_mfa_devices_all(AssignmentStatus, #aws_config{} = Config) -> + Params = make_list_virtual_mfa_devices_params(AssignmentStatus, undefined, undefined), + ItemPath = "/ListVirtualMFADevicesResponse/ListVirtualMFADevicesResult/VirtualMFADevices/member", + iam_query_all(Config, "ListVirtualMFADevices", Params, ItemPath, data_type("VirtualMFADeviceMetadata")). + + % % Utils % + +encode_context_entries(ContextEntries) -> + ParsedContextEntriesValues = [ [{"ContextKeyName", ContextKeyName}, + {"ContextKeyType", ContextKeyType}, + {"ContextKeyValues", erlcloud_util:encode_list("", ContextKeyValues)}] || + [{context_key_name, ContextKeyName}, + {context_key_type, ContextKeyType}, + {context_key_values, ContextKeyValues}] <- + ContextEntries], + EncodedContextEntries = erlcloud_aws:param_list(ParsedContextEntriesValues, "ContextEntries.member"), + lists:flatten([flatten_encoded_context_value(Key, Value) || {Key, Value} <- EncodedContextEntries]). + +flatten_encoded_context_value(Key, Value) -> + flatten_encoded_context_value(Key, Value, []). + +flatten_encoded_context_value(_, [], Acc) -> + Acc; +flatten_encoded_context_value(Key, [{SubKey, Val} | Values], Acc) -> + Acc2 = [{Key++SubKey, Val}] ++ Acc, + flatten_encoded_context_value(Key, Values, Acc2); +flatten_encoded_context_value(Key, Val, _) -> + [{Key, Val}]. + iam_query(Config, Action, Params) -> iam_query(Config, Action, Params, ?API_VERSION). @@ -833,6 +1034,10 @@ extract_account_summary(Item) -> end, [], Entries). +data_type("VirtualMFADeviceMetadata") -> + [{"SerialNumber", serial_number, "String"}, + {"EnableDate", enable_date, "DateTime"}, + {"User", user, data_type("UserDetail")}]; data_type("AccountAuthorizationDetails") -> [{"UserDetailList/member", users, data_type("UserDetail")}, {"GroupDetailList/member", groups, data_type("GroupDetail")}, @@ -847,7 +1052,7 @@ data_type("InstanceProfile") -> {"Arn", arn, "String"}, {"Path", path, "String"}, {"InstanceProfileName", instance_profile_name, "String"}, - {"Roles/member", roles, data_type("Role")}, + {"Roles/member", roles, data_type("RoleList")}, {"InstanceProfileId", instance_profile_id, "String"}]; data_type("Group") -> [{"Path", path, "String"}, @@ -883,13 +1088,21 @@ data_type("PasswordPolicy") -> data_type("PolicyDetail") -> [{"PolicyName", policy_name, "String"}, {"PolicyDocument", policy_document, "Uri"}]; -data_type("Role") -> +data_type("RoleList") -> [{"Arn", arn, "String"}, {"CreateDate", create_date, "DateTime"}, {"AssumeRolePolicyDocument", assume_role_policy_doc, "Uri"}, {"RoleId", role_id, "String"}, {"RoleName", role_name, "String"}, {"Path", path, "String"}]; +data_type("Role") -> + [{"Arn", arn, "String"}, + {"CreateDate", create_date, "DateTime"}, + {"AssumeRolePolicyDocument", assume_role_policy_doc, "Uri"}, + {"RoleId", role_id, "String"}, + {"RoleName", role_name, "String"}, + {"Path", path, "String"}, + {"RoleLastUsed", role_last_used, data_type("RoleLastUsed")}]; data_type("RoleDetail") -> [{"RolePolicyList/member", role_policy_list, data_type("PolicyDetail")}, {"RoleName", role_name, "String"}, @@ -899,6 +1112,9 @@ data_type("RoleDetail") -> {"CreateDate", create_date, "DateTime"}, {"AssumeRolePolicyDocument", assume_role_policy_document, "Uri"}, {"Arn", arn, "String"}]; +data_type("RoleLastUsed") -> + [{"LastUsedDate", last_used_date, "DateTime"}, + {"Region", region, "String"}]; data_type("RolePolicyList") -> [{"PolicyDocument", policy_document, "Uri"}, {"RoleName", role_name, "String"}, @@ -955,7 +1171,17 @@ data_type("GetAccessKeyLastUsedResult") -> [{"UserName", user_name, "String"}, {"AccessKeyLastUsed/Region", access_key_last_used_region, "String"}, {"AccessKeyLastUsed/ServiceName", access_key_last_used_service_name, "String"}, - {"AccessKeyLastUsed/LastUsedDate", access_key_last_used_date, "DateTime"}]. + {"AccessKeyLastUsed/LastUsedDate", access_key_last_used_date, "DateTime"}]; +data_type("ServerCertificateMetadataList") -> + [{"Expiration", expiration, "DateTime"}, + {"UploadDate", upload_date, "DateTime"}, + {"Arn", arn, "String"}, + {"Path", path, "String"}, + {"ServerCertificateId", server_certificate_id, "String"}, + {"ServerCertificateName", server_certificate_name, "String"}]; +data_type("ServerCertificateTags") -> + [{"Value", value, "String"}, + {"Key", key, "String"}]. data_fun("String") -> {erlcloud_xml, get_text}; data_fun("DateTime") -> {erlcloud_xml, get_time}; @@ -964,4 +1190,16 @@ data_fun("Boolean") -> {erlcloud_xml, get_bool}; data_fun("Uri") -> {?MODULE, get_uri}. get_uri(Key, Item) -> - http_uri:decode(erlcloud_xml:get_text(Key, Item)). + erlcloud_util:http_uri_decode(erlcloud_xml:get_text(Key, Item)). + +make_list_virtual_mfa_devices_params(undefined, undefined, undefined) -> + []; +make_list_virtual_mfa_devices_params(AssignmentStatus, Marker, MaxItems) -> + make_list_virtual_mfa_devices_param(AssignmentStatus, "AssignmentStatus") ++ + make_list_virtual_mfa_devices_param(Marker ,"Marker") ++ + make_list_virtual_mfa_devices_param(MaxItems, "MaxItems"). + +make_list_virtual_mfa_devices_param(undefined, _) -> + []; +make_list_virtual_mfa_devices_param(Param, ParamString) -> + [{ParamString, Param}]. diff --git a/src/erlcloud_inspector.erl b/src/erlcloud_inspector.erl index abd47e9b2..5abd82e61 100644 --- a/src/erlcloud_inspector.erl +++ b/src/erlcloud_inspector.erl @@ -134,16 +134,16 @@ inspector_result_fun(#aws_request{response_type = error, error_type = aws} = Req -type attribute() :: proplists:proplist(). -spec add_attributes_to_findings - (Attributes :: [attribute()], - FindingArns :: [string()]) -> + (Attributes :: attribute(), + FindingArns :: [string() | binary()]) -> inspector_return_val(). add_attributes_to_findings(Attributes, FindingArns) -> add_attributes_to_findings(Attributes, FindingArns, default_config()). -spec add_attributes_to_findings - (Attributes :: [attribute()], - FindingArns :: [string()], + (Attributes :: attribute(), + FindingArns :: [string() | binary()], Config :: aws_config()) -> inspector_return_val(). add_attributes_to_findings(Attributes, FindingArns, Config) -> @@ -159,16 +159,16 @@ add_attributes_to_findings(Attributes, FindingArns, Config) -> %%%------------------------------------------------------------------------------ -spec attach_assessment_and_rules_package - (AssessmentArn :: string(), - RulesPackageArn :: string()) -> + (AssessmentArn :: string() | binary(), + RulesPackageArn :: string() | binary()) -> inspector_return_val(). attach_assessment_and_rules_package(AssessmentArn, RulesPackageArn) -> attach_assessment_and_rules_package(AssessmentArn, RulesPackageArn, default_config()). -spec attach_assessment_and_rules_package - (AssessmentArn :: string(), - RulesPackageArn :: string(), + (AssessmentArn :: string() | binary(), + RulesPackageArn :: string() | binary(), Config :: aws_config()) -> inspector_return_val(). attach_assessment_and_rules_package(AssessmentArn, RulesPackageArn, Config) -> @@ -184,16 +184,16 @@ attach_assessment_and_rules_package(AssessmentArn, RulesPackageArn, Config) -> %%%------------------------------------------------------------------------------ -spec create_application - (ApplicationName :: string(), - ResourceGroupArn :: string()) -> + (ApplicationName :: string() | binary(), + ResourceGroupArn :: string() | binary()) -> inspector_return_val(). create_application(ApplicationName, ResourceGroupArn) -> create_application(ApplicationName, ResourceGroupArn, default_config()). -spec create_application - (ApplicationName :: string(), - ResourceGroupArn :: string(), + (ApplicationName :: string() | binary(), + ResourceGroupArn :: string() | binary(), Config :: aws_config()) -> inspector_return_val(). create_application(ApplicationName, ResourceGroupArn, Config) -> @@ -209,8 +209,8 @@ create_application(ApplicationName, ResourceGroupArn, Config) -> %%%------------------------------------------------------------------------------ -spec create_assessment - (ApplicationArn :: string(), - AssessmentName :: string(), + (ApplicationArn :: string() | binary(), + AssessmentName :: string() | binary(), DurationInSeconds :: integer()) -> inspector_return_val(). create_assessment(ApplicationArn, AssessmentName, DurationInSeconds) -> @@ -218,8 +218,8 @@ create_assessment(ApplicationArn, AssessmentName, DurationInSeconds) -> -spec create_assessment - (ApplicationArn :: string(), - AssessmentName :: string(), + (ApplicationArn :: string() | binary(), + AssessmentName :: string() | binary(), DurationInSeconds :: integer(), Options :: inspector_opts()) -> inspector_return_val(). @@ -228,8 +228,8 @@ create_assessment(ApplicationArn, AssessmentName, DurationInSeconds, Options) -> -spec create_assessment - (ApplicationArn :: string(), - AssessmentName :: string(), + (ApplicationArn :: string() | binary(), + AssessmentName :: string() | binary(), DurationInSeconds :: integer(), Options :: inspector_opts(), Config :: aws_config()) -> @@ -271,14 +271,14 @@ create_resource_group(ResourceGroupTags, Config) -> %%%------------------------------------------------------------------------------ -spec delete_application - (ApplicationArn :: string()) -> + (ApplicationArn :: string() | binary()) -> inspector_return_val(). delete_application(ApplicationArn) -> delete_application(ApplicationArn, default_config()). -spec delete_application - (ApplicationArn :: string(), + (ApplicationArn :: string() | binary(), Config :: aws_config()) -> inspector_return_val(). delete_application(ApplicationArn, Config) -> @@ -293,14 +293,14 @@ delete_application(ApplicationArn, Config) -> %%%------------------------------------------------------------------------------ -spec delete_assessment - (AssessmentArn :: string()) -> + (AssessmentArn :: string() | binary()) -> inspector_return_val(). delete_assessment(AssessmentArn) -> delete_assessment(AssessmentArn, default_config()). -spec delete_assessment - (AssessmentArn :: string(), + (AssessmentArn :: string() | binary(), Config :: aws_config()) -> inspector_return_val(). delete_assessment(AssessmentArn, Config) -> @@ -315,14 +315,14 @@ delete_assessment(AssessmentArn, Config) -> %%%------------------------------------------------------------------------------ -spec delete_run - (RunArn :: string()) -> + (RunArn :: string() | binary()) -> inspector_return_val(). delete_run(RunArn) -> delete_run(RunArn, default_config()). -spec delete_run - (RunArn :: string(), + (RunArn :: string() | binary(), Config :: aws_config()) -> inspector_return_val(). delete_run(RunArn, Config) -> @@ -337,14 +337,14 @@ delete_run(RunArn, Config) -> %%%------------------------------------------------------------------------------ -spec describe_application - (ApplicationArn :: string()) -> + (ApplicationArn :: string() | binary()) -> inspector_return_val(). describe_application(ApplicationArn) -> describe_application(ApplicationArn, default_config()). -spec describe_application - (ApplicationArn :: string(), + (ApplicationArn :: string() | binary(), Config :: aws_config()) -> inspector_return_val(). describe_application(ApplicationArn, Config) -> @@ -359,14 +359,14 @@ describe_application(ApplicationArn, Config) -> %%%------------------------------------------------------------------------------ -spec describe_assessment - (AssessmentArn :: string()) -> + (AssessmentArn :: string() | binary()) -> inspector_return_val(). describe_assessment(AssessmentArn) -> describe_assessment(AssessmentArn, default_config()). -spec describe_assessment - (AssessmentArn :: string(), + (AssessmentArn :: string() | binary(), Config :: aws_config()) -> inspector_return_val(). describe_assessment(AssessmentArn, Config) -> @@ -401,14 +401,14 @@ describe_cross_account_access_role(Config) -> %%%------------------------------------------------------------------------------ -spec describe_finding - (FindingArn :: string()) -> + (FindingArn :: string() | binary()) -> inspector_return_val(). describe_finding(FindingArn) -> describe_finding(FindingArn, default_config()). -spec describe_finding - (FindingArn :: string(), + (FindingArn :: string() | binary(), Config :: aws_config()) -> inspector_return_val(). describe_finding(FindingArn, Config) -> @@ -423,14 +423,14 @@ describe_finding(FindingArn, Config) -> %%%------------------------------------------------------------------------------ -spec describe_resource_group - (ResourceGroupArn :: string()) -> + (ResourceGroupArn :: string() | binary()) -> inspector_return_val(). describe_resource_group(ResourceGroupArn) -> describe_resource_group(ResourceGroupArn, default_config()). -spec describe_resource_group - (ResourceGroupArn :: string(), + (ResourceGroupArn :: string() | binary(), Config :: aws_config()) -> inspector_return_val(). describe_resource_group(ResourceGroupArn, Config) -> @@ -445,14 +445,14 @@ describe_resource_group(ResourceGroupArn, Config) -> %%%------------------------------------------------------------------------------ -spec describe_rules_package - (RulesPackageArn :: string()) -> + (RulesPackageArn :: string() | binary()) -> inspector_return_val(). describe_rules_package(RulesPackageArn) -> describe_rules_package(RulesPackageArn, default_config()). -spec describe_rules_package - (RulesPackageArn :: string(), + (RulesPackageArn :: string() | binary(), Config :: aws_config()) -> inspector_return_val(). describe_rules_package(RulesPackageArn, Config) -> @@ -467,14 +467,14 @@ describe_rules_package(RulesPackageArn, Config) -> %%%------------------------------------------------------------------------------ -spec describe_run - (RunArn :: string()) -> + (RunArn :: string() | binary()) -> inspector_return_val(). describe_run(RunArn) -> describe_run(RunArn, default_config()). -spec describe_run - (RunArn :: string(), + (RunArn :: string() | binary(), Config :: aws_config()) -> inspector_return_val(). describe_run(RunArn, Config) -> @@ -489,16 +489,16 @@ describe_run(RunArn, Config) -> %%%------------------------------------------------------------------------------ -spec detach_assessment_and_rules_package - (AssessmentArn :: string(), - RulesPackageArn :: string()) -> + (AssessmentArn :: string() | binary(), + RulesPackageArn :: string() | binary()) -> inspector_return_val(). detach_assessment_and_rules_package(AssessmentArn, RulesPackageArn) -> detach_assessment_and_rules_package(AssessmentArn, RulesPackageArn, default_config()). -spec detach_assessment_and_rules_package - (AssessmentArn :: string(), - RulesPackageArn :: string(), + (AssessmentArn :: string() | binary(), + RulesPackageArn :: string() | binary(), Config :: aws_config()) -> inspector_return_val(). detach_assessment_and_rules_package(AssessmentArn, RulesPackageArn, Config) -> @@ -514,14 +514,14 @@ detach_assessment_and_rules_package(AssessmentArn, RulesPackageArn, Config) -> %%%------------------------------------------------------------------------------ -spec get_assessment_telemetry - (AssessmentArn :: string()) -> + (AssessmentArn :: string() | binary()) -> inspector_return_val(). get_assessment_telemetry(AssessmentArn) -> get_assessment_telemetry(AssessmentArn, default_config()). -spec get_assessment_telemetry - (AssessmentArn :: string(), + (AssessmentArn :: string() | binary(), Config :: aws_config()) -> inspector_return_val(). get_assessment_telemetry(AssessmentArn, Config) -> @@ -565,14 +565,14 @@ list_applications(Options, Config) -> %%%------------------------------------------------------------------------------ -spec list_assessment_agents - (AssessmentArn :: string()) -> + (AssessmentArn :: string() | binary()) -> inspector_return_val(). list_assessment_agents(AssessmentArn) -> list_assessment_agents(AssessmentArn, []). -spec list_assessment_agents - (AssessmentArn :: string(), + (AssessmentArn :: string() | binary(), Options :: inspector_opts()) -> inspector_return_val(). list_assessment_agents(AssessmentArn, Options) -> @@ -580,7 +580,7 @@ list_assessment_agents(AssessmentArn, Options) -> -spec list_assessment_agents - (AssessmentArn :: string(), + (AssessmentArn :: string() | binary(), Options :: inspector_opts(), Config :: aws_config()) -> inspector_return_val(). @@ -626,14 +626,14 @@ list_assessments(Options, Config) -> %%%------------------------------------------------------------------------------ -spec list_attached_assessments - (RulesPackageArn :: string()) -> + (RulesPackageArn :: string() | binary()) -> inspector_return_val(). list_attached_assessments(RulesPackageArn) -> list_attached_assessments(RulesPackageArn, []). -spec list_attached_assessments - (RulesPackageArn :: string(), + (RulesPackageArn :: string() | binary(), Options :: inspector_opts()) -> inspector_return_val(). list_attached_assessments(RulesPackageArn, Options) -> @@ -641,7 +641,7 @@ list_attached_assessments(RulesPackageArn, Options) -> -spec list_attached_assessments - (RulesPackageArn :: string(), + (RulesPackageArn :: string() | binary(), Options :: inspector_opts(), Config :: aws_config()) -> inspector_return_val(). @@ -658,14 +658,14 @@ list_attached_assessments(RulesPackageArn, Options, Config) -> %%%------------------------------------------------------------------------------ -spec list_attached_rules_packages - (AssessmentArn :: string()) -> + (AssessmentArn :: string() | binary()) -> inspector_return_val(). list_attached_rules_packages(AssessmentArn) -> list_attached_rules_packages(AssessmentArn, []). -spec list_attached_rules_packages - (AssessmentArn :: string(), + (AssessmentArn :: string() | binary(), Options :: inspector_opts()) -> inspector_return_val(). list_attached_rules_packages(AssessmentArn, Options) -> @@ -673,7 +673,7 @@ list_attached_rules_packages(AssessmentArn, Options) -> -spec list_attached_rules_packages - (AssessmentArn :: string(), + (AssessmentArn :: string() | binary(), Options :: inspector_opts(), Config :: aws_config()) -> inspector_return_val(). @@ -704,7 +704,7 @@ list_findings(Options) -> -spec list_findings - (Options :: inspector_opts(), + (Options :: inspector_opts(), Config :: aws_config()) -> inspector_return_val(). list_findings(Options, Config) -> @@ -733,7 +733,7 @@ list_rules_packages(Options) -> -spec list_rules_packages - (Options :: inspector_opts(), + (Options :: inspector_opts(), Config :: aws_config()) -> inspector_return_val(). list_rules_packages(Options, Config) -> @@ -762,7 +762,7 @@ list_runs(Options) -> -spec list_runs - (Options :: inspector_opts(), + (Options :: inspector_opts(), Config :: aws_config()) -> inspector_return_val(). list_runs(Options, Config) -> @@ -777,14 +777,14 @@ list_runs(Options, Config) -> %%%------------------------------------------------------------------------------ -spec list_tags_for_resource - (ResourceArn :: string()) -> + (ResourceArn :: string() | binary()) -> inspector_return_val(). list_tags_for_resource(ResourceArn) -> list_tags_for_resource(ResourceArn, []). -spec list_tags_for_resource - (ResourceArn :: string(), + (ResourceArn :: string() | binary(), Options :: inspector_opts()) -> inspector_return_val(). list_tags_for_resource(ResourceArn, Options) -> @@ -792,7 +792,7 @@ list_tags_for_resource(ResourceArn, Options) -> -spec list_tags_for_resource - (ResourceArn :: string(), + (ResourceArn :: string() | binary(), Options :: inspector_opts(), Config :: aws_config()) -> inspector_return_val(). @@ -826,7 +826,7 @@ localize_text(LocalizedTexts) -> -spec localize_text (LocalizedTexts :: [localized_text()], - Locale :: string()) -> + Locale :: string() | binary()) -> inspector_return_val(). localize_text(LocalizedTexts, Locale) -> localize_text(LocalizedTexts, Locale, default_config()). @@ -834,7 +834,7 @@ localize_text(LocalizedTexts, Locale) -> -spec localize_text (LocalizedTexts :: [localized_text()], - Locale :: string(), + Locale :: string() | binary(), Config :: aws_config()) -> inspector_return_val(). localize_text(LocalizedTexts, Locale, Config) -> @@ -850,14 +850,14 @@ localize_text(LocalizedTexts, Locale, Config) -> %%%------------------------------------------------------------------------------ -spec preview_agents_for_resource_group - (ResourceGroupArn :: string()) -> + (ResourceGroupArn :: string() | binary()) -> inspector_return_val(). preview_agents_for_resource_group(ResourceGroupArn) -> preview_agents_for_resource_group(ResourceGroupArn, []). -spec preview_agents_for_resource_group - (ResourceGroupArn :: string(), + (ResourceGroupArn :: string() | binary(), Options :: inspector_opts()) -> inspector_return_val(). preview_agents_for_resource_group(ResourceGroupArn, Options) -> @@ -865,7 +865,7 @@ preview_agents_for_resource_group(ResourceGroupArn, Options) -> -spec preview_agents_for_resource_group - (ResourceGroupArn :: string(), + (ResourceGroupArn :: string() | binary(), Options :: inspector_opts(), Config :: aws_config()) -> inspector_return_val(). @@ -882,14 +882,14 @@ preview_agents_for_resource_group(ResourceGroupArn, Options, Config) -> %%%------------------------------------------------------------------------------ -spec register_cross_account_access_role - (RoleArn :: string()) -> + (RoleArn :: string() | binary()) -> inspector_return_val(). register_cross_account_access_role(RoleArn) -> register_cross_account_access_role(RoleArn, []). -spec register_cross_account_access_role - (RoleArn :: string(), + (RoleArn :: string() | binary(), Options :: inspector_opts()) -> inspector_return_val(). register_cross_account_access_role(RoleArn, Options) -> @@ -897,7 +897,7 @@ register_cross_account_access_role(RoleArn, Options) -> -spec register_cross_account_access_role - (RoleArn :: string(), + (RoleArn :: string() | binary(), Options :: inspector_opts(), Config :: aws_config()) -> inspector_return_val(). @@ -914,16 +914,16 @@ register_cross_account_access_role(RoleArn, Options, Config) -> %%%------------------------------------------------------------------------------ -spec remove_attributes_from_findings - (AttributeKeys :: [string()], - FindingArns :: [string()]) -> + (AttributeKeys :: [string() | binary()], + FindingArns :: [string() | binary()]) -> inspector_return_val(). remove_attributes_from_findings(AttributeKeys, FindingArns) -> remove_attributes_from_findings(AttributeKeys, FindingArns, default_config()). -spec remove_attributes_from_findings - (AttributeKeys :: [string()], - FindingArns :: [string()], + (AttributeKeys :: [string() | binary()], + FindingArns :: [string() | binary()], Config :: aws_config()) -> inspector_return_val(). remove_attributes_from_findings(AttributeKeys, FindingArns, Config) -> @@ -939,16 +939,16 @@ remove_attributes_from_findings(AttributeKeys, FindingArns, Config) -> %%%------------------------------------------------------------------------------ -spec run_assessment - (AssessmentArn :: string(), - RunName :: string()) -> + (AssessmentArn :: string() | binary(), + RunName :: string() | binary()) -> inspector_return_val(). run_assessment(AssessmentArn, RunName) -> run_assessment(AssessmentArn, RunName, default_config()). -spec run_assessment - (AssessmentArn :: string(), - RunName :: string(), + (AssessmentArn :: string() | binary(), + RunName :: string() | binary(), Config :: aws_config()) -> inspector_return_val(). run_assessment(AssessmentArn, RunName, Config) -> @@ -964,7 +964,7 @@ run_assessment(AssessmentArn, RunName, Config) -> %%%------------------------------------------------------------------------------ -spec set_tags_for_resource - (ResourceArn :: string(), + (ResourceArn :: string() | binary(), Tags :: term()) -> inspector_return_val(). set_tags_for_resource(ResourceArn, Tags) -> @@ -972,8 +972,8 @@ set_tags_for_resource(ResourceArn, Tags) -> -spec set_tags_for_resource - (ResourceArn :: string(), - Tags :: string(), + (ResourceArn :: string() | binary(), + Tags :: term(), Config :: aws_config()) -> inspector_return_val(). set_tags_for_resource(ResourceArn, Tags, Config) -> @@ -989,7 +989,7 @@ set_tags_for_resource(ResourceArn, Tags, Config) -> %%%------------------------------------------------------------------------------ -spec start_data_collection - (AssessmentArn :: string()) -> + (AssessmentArn :: string() | binary()) -> inspector_return_val(). start_data_collection(RunArn) -> start_data_collection(RunArn, default_config()). @@ -1011,7 +1011,7 @@ start_data_collection(AssessmentArn, Config) -> %%%------------------------------------------------------------------------------ -spec stop_data_collection - (AssessmentArn :: string()) -> + (AssessmentArn :: string() | binary()) -> inspector_return_val(). stop_data_collection(RunArn) -> stop_data_collection(RunArn, default_config()). @@ -1033,18 +1033,18 @@ stop_data_collection(AssessmentArn, Config) -> %%%------------------------------------------------------------------------------ -spec update_application - (ApplicationArn :: string(), - ApplicationName :: string(), - ResourceGroupArn :: string()) -> + (ApplicationArn :: string() | binary(), + ApplicationName :: string() | binary(), + ResourceGroupArn :: string() | binary()) -> inspector_return_val(). update_application(ApplicationArn, ApplicationName, ResourceGroupArn) -> update_application(ApplicationArn, ApplicationName, ResourceGroupArn, default_config()). -spec update_application - (ApplicationArn :: string(), - ApplicationName :: string(), - ResourceGroupArn :: string(), + (ApplicationArn :: string() | binary(), + ApplicationName :: string() | binary(), + ResourceGroupArn :: string() | binary(), Config :: aws_config()) -> inspector_return_val(). update_application(ApplicationArn, ApplicationName, ResourceGroupArn, Config) -> @@ -1061,8 +1061,8 @@ update_application(ApplicationArn, ApplicationName, ResourceGroupArn, Config) -> %%%------------------------------------------------------------------------------ -spec update_assessment - (AssessmentArn :: string(), - AssessmentName :: string(), + (AssessmentArn :: string() | binary(), + AssessmentName :: string() | binary(), DurationInSeconds :: integer()) -> inspector_return_val(). update_assessment(AssessmentArn, AssessmentName, DurationInSeconds) -> @@ -1070,8 +1070,8 @@ update_assessment(AssessmentArn, AssessmentName, DurationInSeconds) -> -spec update_assessment - (AssessmentArn :: string(), - AssessmentName :: string(), + (AssessmentArn :: string() | binary(), + AssessmentName :: string() | binary(), DurationInSeconds :: integer(), Config :: aws_config()) -> inspector_return_val(). @@ -1109,7 +1109,7 @@ inspector_request_no_update(Config, Operation, Body) -> request_body = Payload}, case erlcloud_aws:request_to_return(erlcloud_retry:request(Config, Request, fun inspector_result_fun/1)) of {ok, {_RespHeaders, <<>>}} -> {ok, []}; - {ok, {_RespHeaders, RespBody}} -> {ok, jsx:decode(RespBody)}; + {ok, {_RespHeaders, RespBody}} -> {ok, jsx:decode(RespBody, [{return_maps, false}])}; {error, _} = Error-> Error end. diff --git a/src/erlcloud_json.erl b/src/erlcloud_json.erl index c6351154a..4825808e0 100644 --- a/src/erlcloud_json.erl +++ b/src/erlcloud_json.erl @@ -4,7 +4,7 @@ -include("erlcloud.hrl"). -type decode_return() :: [{Name :: atom(), Value :: string() | integer()}]. --type decode_value_type() :: optional_string | optional_integer | {optional_map, fun(([{Key :: binary(), Value :: string() | integer()}]) -> decode_return())}. +-type decode_value_type() :: optional_string | optional_integer | optional_boolean | {optional_map, fun(([{Key :: binary(), Value :: string() | integer()}]) -> decode_return())}. -type decode_value() :: {atom(), JsonField :: binary(), Type :: decode_value_type()}. -type decode_value_r() :: {pos_integer(), JsonField :: binary(), Type :: atom()}. @@ -33,6 +33,7 @@ get_value(JsonField, Type, Json) -> case Type of optional_string -> proplists:get_value(JsonField, Json, undefined); optional_integer -> proplists:get_value(JsonField, Json, undefined); + optional_boolean -> proplists:get_value(JsonField, Json, undefined); string -> proplists:get_value(JsonField, Json, ""); integer -> proplists:get_value(JsonField, Json, 0); Fun when is_function(Fun, 1) -> diff --git a/src/erlcloud_kinesis.erl b/src/erlcloud_kinesis.erl index d7e6ebcf3..533b3be27 100644 --- a/src/erlcloud_kinesis.erl +++ b/src/erlcloud_kinesis.erl @@ -7,6 +7,7 @@ -export([create_stream/2, create_stream/3, delete_stream/1, delete_stream/2, + list_shards/1, list_shards/2, list_shards/3, list_streams/0, list_streams/1, list_streams/2, list_streams/3, describe_stream/1, describe_stream/2, describe_stream/3, describe_stream/4, describe_stream_summary/1, describe_stream_summary/2, @@ -15,7 +16,7 @@ get_shard_iterator/3, get_shard_iterator/4, get_shard_iterator/5, get_records/1, get_records/2, get_records/3, get_records/4, put_record/3, put_record/4, put_record/5, put_record/6, put_record/7, - put_records/2, put_records/3, + put_records/2, put_records/3, put_records/4, merge_shards/3, merge_shards/4, split_shards/3, split_shards/4, add_tags_to_stream/2, add_tags_to_stream/3, @@ -30,6 +31,11 @@ -type get_records_limit() :: 1..10000. +-type exclusive_start_shard_id_opt() :: {exclusive_start_shard_id, string()}. +-type max_results_opt() :: {max_results, non_neg_integer()}. +-type next_token_opt() :: {next_token, string()}. +-type stream_creation_timestamp_opt() :: {stream_creation_timestamp, integer()}. + -spec new(string(), string()) -> aws_config(). new(AccessKeyID, SecretAccessKey) -> @@ -72,6 +78,32 @@ configure(AccessKeyID, SecretAccessKey, Host, Port) -> default_config() -> erlcloud_aws:default_config(). +dynamize_option({stream_name, Value}) -> + {<<"StreamName">>, Value}; +dynamize_option({exclusive_start_shard_id, Value}) -> + {<<"ExclusiveStartShardId">>, Value}; +dynamize_option({max_results, Value}) when Value >= 1, Value =< 10000 -> + {<<"MaxResults">>, Value}; +dynamize_option({next_token, Value}) -> + {<<"NextToken">>, Value}; +dynamize_option({stream_creation_timestamp, Value}) -> + {<<"StreamCreationTimestamp">>, Value}; +dynamize_option(Option) -> + {error, {invalid_option, Option}}. + +dynamize_options(Options) -> + lists:foldr(fun + (Option, Acc) when is_list(Acc) -> + case dynamize_option(Option) of + {error, _} = Error -> + Error; + DynamizedOption -> + [DynamizedOption | Acc] + end; + (_Option, Error) -> + Error + end, [], Options). + %%------------------------------------------------------------------------------ %% @doc %% Kinesis API: @@ -130,6 +162,87 @@ delete_stream(StreamName, Config) when is_record(Config, aws_config) -> Json = [{<<"StreamName">>, StreamName}], erlcloud_kinesis_impl:request(Config, "Kinesis_20131202.DeleteStream", Json). + +-type list_shards_opts() :: [ + exclusive_start_shard_id_opt() | + max_results_opt() | + next_token_opt() | + stream_creation_timestamp_opt() +]. + +%%------------------------------------------------------------------------------ +%% @doc +%% Kinesis API: +%% [https://docs.aws.amazon.com/kinesis/latest/APIReference/API_ListShards.html] +%% +%% ===Example=== +%% +%% This operation returns the following information about the stream: an array of shard objects that comprise the stream. +%% +%% ` +%% erlcloud_kinesis:list_shards(<<"staging">>). +%% {ok, [ +%% {<<"NextToken">>, <<"AAAAAAAAAAGK9EEG0sJqVhCUS2JsgigQ5dcpB4q9PYswrH2oK44Skbjtm+WR0xA7/hrAFFsohevH1/OyPnbzKBS1byPyCZuVcokYtQe/b1m4c0SCI7jctPT0oUTLRdwSRirKm9dp9YC/EL+kZHOvYAUnztVGsOAPEFC3ECf/bVC927bDZBbRRzy/44OHfWmrCLcbcWqehRh5D14WnL3yLsumhiHDkyuxSlkBepauvMnNLtTOlRtmQ5Q5reoujfq2gzeCSOtLcfXgBMztJqohPdgMzjTQSbwB9Am8rMpHLsDbSdMNXmITvw==">>}, +%% {<<"Shards">>, [ +%% [ +%% {<<"ShardId">>, <<"shardId-000000000001">>}, +%% {<<"HashKeyRange">>, [ +%% {<<"EndingHashKey">>, <<"68056473384187692692674921486353642280">>}, +%% {<<"StartingHashKey">>, <<"34028236692093846346337460743176821145">>} +%% ]}, +%% {<<"SequenceNumberRange">>, [ +%% {<<"StartingSequenceNumber">>, <<"49579844037727333356165064238440708846556371693205002258">>} +%% ]} +%% ], [ +%% {<<"ShardId">>, <<"shardId-000000000002">>}, +%% {<<"HashKeyRange">>, [ +%% {<<"EndingHashKey">>, <<"102084710076281539039012382229530463436">>}, +%% {<<"StartingHashKey">>, <<"68056473384187692692674921486353642281">>} +%% ]}, +%% {<<"SequenceNumberRange">>, [ +%% {<<"StartingSequenceNumber">>, <<"49579844037749634101363594861582244564829020124710982690">>} +%% ]} +%% ], [ +%% {<<"ShardId">>, <<"shardId-000000000003">>}, +%% {<<"HashKeyRange">>, [ +%% {<<"EndingHashKey">>, <<"136112946768375385385349842972707284581">>}, +%% {<<"StartingHashKey">>, <<"102084710022876281539039012382229530463437">>} +%% ]}, +%% {<<"SequenceNumberRange">>, [ +%% {<<"StartingSequenceNumber">>, <<"49579844037771934846562125484723780283101668556216963122">>} +%% ]} +%% ] +%% ]} +%% ]} +%% ' +%% +%% @end +%%------------------------------------------------------------------------------ + +-spec list_shards(binary() | list_shards_opts()) -> erlcloud_kinesis_impl:json_return() | {error, any()}. +list_shards(StreamNameOrOptions) -> + list_shards(StreamNameOrOptions, default_config()). + +-spec list_shards(binary() | list_shards_opts(), list_shards_opts() | aws_config()) -> erlcloud_kinesis_impl:json_return(). +list_shards(StreamName, Config) when is_record(Config, aws_config), is_binary(StreamName) -> + list_shards(StreamName, [], Config); +list_shards(Options, Config) when is_record(Config, aws_config), is_list(Options) -> + case dynamize_options(Options) of + DynamizedOptions when is_list(DynamizedOptions) -> + erlcloud_kinesis_impl:request(Config, "Kinesis_20131202.ListShards", DynamizedOptions); + Error -> + Error + end; +list_shards(StreamName, Options) -> + list_shards(StreamName, Options, default_config()). + +-spec list_shards(binary(), list_shards_opts(), aws_config()) -> erlcloud_kinesis_impl:json_return(). +list_shards(StreamName, Options, Config) -> + % Syntaxtic sugar as all other erlcloud_kinesis operating on a stream takes + % a stream name as 1st argument. + list_shards([{stream_name, StreamName} | Options], Config). + + %%------------------------------------------------------------------------------ %% @doc %% Kinesis API: @@ -234,15 +347,15 @@ describe_stream(StreamName, Limit, Config) Limit >= 1, Limit =< 10000 -> Json = [{<<"StreamName">>, StreamName}, {<<"Limit">>, Limit}], erlcloud_kinesis_impl:request(Config, "Kinesis_20131202.DescribeStream", Json); -describe_stream(StreamName, Limit, ExcludeShard) -> - describe_stream(StreamName, Limit, ExcludeShard, default_config()). +describe_stream(StreamName, Limit, ExclusiveStartShardId) -> + describe_stream(StreamName, Limit, ExclusiveStartShardId, default_config()). -spec describe_stream(binary(), get_records_limit(), string(), aws_config()) -> erlcloud_kinesis_impl:json_return(). -describe_stream(StreamName, Limit, ExcludeShard, Config) +describe_stream(StreamName, Limit, ExclusiveStartShardId, Config) when is_record(Config, aws_config), is_integer(Limit), Limit >= 1, Limit =< 10000 -> - Json = [{<<"StreamName">>, StreamName}, {<<"Limit">>, Limit}, {<<"ExclusiveStartShardId">>, ExcludeShard}], + Json = [{<<"StreamName">>, StreamName}, {<<"Limit">>, Limit}, {<<"ExclusiveStartShardId">>, ExclusiveStartShardId}], erlcloud_kinesis_impl:request(Config, "Kinesis_20131202.DescribeStream", Json). @@ -442,12 +555,12 @@ get_shard_iterator_request(Config, Json) -> %% @end %%------------------------------------------------------------------------------ --spec get_records(string()) -> erlcloud_kinesis_impl:json_return(). +-spec get_records(binary()) -> erlcloud_kinesis_impl:json_return(). get_records(ShardIterator) -> Json = [{<<"ShardIterator">>, ShardIterator}], get_normalized_records(default_config(), Json). --spec get_records(string(), get_records_limit()| aws_config()) -> erlcloud_kinesis_impl:json_return(). +-spec get_records(binary(), get_records_limit()| aws_config()) -> erlcloud_kinesis_impl:json_return(). get_records(ShardIterator, Config) when is_record(Config, aws_config) -> Json = [{<<"ShardIterator">>, ShardIterator}], get_normalized_records(Config, Json); @@ -599,8 +712,8 @@ put_record(StreamName, PartitionKey, Data, ExplicitHashKey, Ordering, Options, C %% @end %%------------------------------------------------------------------------------ --type put_records_item() :: {Data :: string(), PartitionKey :: string()} - | {Data :: string(), ExplicitHashKey :: string(), PartitionKey :: string()}. +-type put_records_item() :: {Data :: binary(), PartitionKey :: binary()} + | {Data :: binary(), ExplicitHashKey :: binary(), PartitionKey :: binary()}. -type put_records_items() :: [put_records_item()]. -spec put_records(binary(), put_records_items()) -> erlcloud_kinesis_impl:json_return(). @@ -608,28 +721,38 @@ put_record(StreamName, PartitionKey, Data, ExplicitHashKey, Ordering, Options, C put_records(StreamName, Items) -> put_records(StreamName, Items, default_config()). --spec put_records(binary(), put_records_items(), Config) -> erlcloud_kinesis_impl:json_return() when - Config :: aws_config(). +-spec put_records(binary(), put_records_items(), function() | aws_config()) -> erlcloud_kinesis_impl:json_return(). + +put_records(StreamName, Items, EncodingFun) when is_function(EncodingFun, 1) -> + put_records(StreamName, Items, EncodingFun, default_config()); put_records(StreamName, Items, Config) -> + put_records(StreamName, Items, fun default_put_encoding/1, Config). + +-spec put_records(binary(), put_records_items(), + function(), Config :: aws_config()) -> erlcloud_kinesis_impl:json_return(). + +put_records(StreamName, Items, EncodingFun, Config) when is_function(EncodingFun, 1) -> Operation = put_records_operation(), - Json = prepare_put_records_data(StreamName, Items), + Json = prepare_put_records_data(StreamName, Items, EncodingFun), erlcloud_kinesis_impl:request(Config, Operation, Json). +default_put_encoding(D) -> base64:encode(D). + put_records_operation() -> "Kinesis_20131202.PutRecords". -prepare_put_records_data(StreamName, Items) -> - Records = [prepare_put_records_item(X) || X <- Items], +prepare_put_records_data(StreamName, Items, Fun) -> + Records = [prepare_put_records_item(X, Fun) || X <- Items], [{<<"StreamName">>, StreamName}, {<<"Records">>, Records}]. -prepare_put_records_item({Data, PartitionKey}) -> +prepare_put_records_item({Data, PartitionKey}, Fun) -> [{<<"PartitionKey">>, PartitionKey}, - {<<"Data">>, base64:encode(Data)}]; -prepare_put_records_item({Data, ExplicitHashKey, PartitionKey}) -> + {<<"Data">>, Fun(Data)}]; +prepare_put_records_item({Data, ExplicitHashKey, PartitionKey}, Fun) -> [{<<"PartitionKey">>, PartitionKey}, {<<"ExplicitHashKey">>, ExplicitHashKey}, - {<<"Data">>, base64:encode(Data)}]. + {<<"Data">>, Fun(Data)}]. %%------------------------------------------------------------------------------ %% @doc @@ -648,13 +771,13 @@ prepare_put_records_item({Data, ExplicitHashKey, PartitionKey}) -> %% @end %%------------------------------------------------------------------------------ --spec merge_shards(binary(), string(), string()) -> erlcloud_kinesis_impl:json_return(). +-spec merge_shards(binary(), binary(), binary()) -> erlcloud_kinesis_impl:json_return(). merge_shards(StreamName, AdjacentShardToMerge, ShardToMerge) -> Json = [{<<"StreamName">>, StreamName}, {<<"AdjacentShardToMerge">>, AdjacentShardToMerge}, {<<"ShardToMerge">>, ShardToMerge}], erlcloud_kinesis_impl:request(default_config(), "Kinesis_20131202.MergeShards", Json). --spec merge_shards(binary(), string(), string(), aws_config()) -> erlcloud_kinesis_impl:json_return(). +-spec merge_shards(binary(), binary(), binary(), aws_config()) -> erlcloud_kinesis_impl:json_return(). merge_shards(StreamName, AdjacentShardToMerge, ShardToMerge, Config) when is_record(Config, aws_config) -> Json = [{<<"StreamName">>, StreamName}, {<<"AdjacentShardToMerge">>, AdjacentShardToMerge}, {<<"ShardToMerge">>, ShardToMerge}], @@ -677,13 +800,13 @@ merge_shards(StreamName, AdjacentShardToMerge, ShardToMerge, Config) when is_rec %% @end %%------------------------------------------------------------------------------ --spec split_shards(binary(), string(), string()) -> erlcloud_kinesis_impl:json_return(). +-spec split_shards(binary(), binary(), binary()) -> erlcloud_kinesis_impl:json_return(). split_shards(StreamName, ShardToSplit, NewStartingHashKey) -> Json = [{<<"StreamName">>, StreamName}, {<<"ShardToSplit">>, ShardToSplit}, {<<"NewStartingHashKey">>, NewStartingHashKey}], erlcloud_kinesis_impl:request(default_config(), "Kinesis_20131202.SplitShard", Json). --spec split_shards(binary(), string(), string(), aws_config()) -> erlcloud_kinesis_impl:json_return(). +-spec split_shards(binary(), binary(), binary(), aws_config()) -> erlcloud_kinesis_impl:json_return(). split_shards(StreamName, ShardToSplit, NewStartingHashKey, Config) when is_record(Config, aws_config) -> Json = [{<<"StreamName">>, StreamName}, {<<"ShardToSplit">>, ShardToSplit}, {<<"NewStartingHashKey">>, NewStartingHashKey}], @@ -846,7 +969,7 @@ list_all_tags_pagination_test_() -> meck:sequence(EK, request, 3, [{ok, [{<<"HasMoreTags">>, true}, {<<"Tags">>, Tags1}]}, {ok, [{<<"HasMoreTags">>, false}, {<<"Tags">>, Tags2}]}]), - Result = erlcloud_kinesis:list_all_tags_for_stream(<<"stream">>), + Result = list_all_tags_for_stream(<<"stream">>), meck:unload(EK), ?_assertEqual({ok, [{<<"k1">>, <<"v1">>}, {<<"k2">>, <<"v2">>}, diff --git a/src/erlcloud_kinesis_impl.erl b/src/erlcloud_kinesis_impl.erl index 9cdd2fcd8..04490e5cf 100644 --- a/src/erlcloud_kinesis_impl.erl +++ b/src/erlcloud_kinesis_impl.erl @@ -27,7 +27,7 @@ %% @author Ransom Richardson %% @doc %% -%% Implementation of requests to DynamoDB. This code is shared accross +%% Implementation of requests to DynamoDB. This code is shared across %% all API versions. %% %% @end @@ -38,7 +38,7 @@ -include("erlcloud_aws.hrl"). %% Helpers --export([backoff/1, retry/2]). +-export([backoff/1, retry/2, retry/3]). %% Internal impl api -export([request/3, request/4]). @@ -89,11 +89,15 @@ backoff(Attempt) -> timer:sleep(erlcloud_util:rand_uniform((1 bsl (Attempt - 1)) * 100)). -type attempt() :: {attempt, pos_integer()} | {error, term()}. --type retry_fun() :: fun((pos_integer(), term()) -> attempt()). +-type retry_fun() :: fun((pos_integer(), non_neg_integer(), term()) -> attempt()). -spec retry(pos_integer(), term()) -> attempt(). -retry(Attempt, Reason) when Attempt >= ?NUM_ATTEMPTS -> +retry(Attempt, Reason) -> + retry(Attempt, ?NUM_ATTEMPTS, Reason). + +-spec retry(pos_integer(), pos_integer(), term()) -> attempt(). +retry(Attempt, MaxAttempt, Reason) when Attempt > MaxAttempt -> {error, Reason}; -retry(Attempt, _) -> +retry(Attempt, _, _) -> backoff(Attempt), {attempt, Attempt + 1}. @@ -108,6 +112,7 @@ request_and_retry(_, _, _, _, {error, Reason}) -> {error, Reason}; request_and_retry(Config, Headers, Body, ShouldDecode, {attempt, Attempt}) -> RetryFun = Config#aws_config.kinesis_retry, + MaxAttempt = Config#aws_config.retry_num, case erlcloud_httpc:request( url(Config), post, [{<<"content-type">>, <<"application/x-amz-json-1.1">>} | Headers], @@ -123,25 +128,25 @@ request_and_retry(Config, Headers, Body, ShouldDecode, {attempt, Attempt}) -> {ok, {{Status, StatusLine}, _, RespBody}} when Status >= 400 andalso Status < 500 -> case client_error(Status, StatusLine, RespBody) of {retry, Reason} -> - request_and_retry(Config, Headers, Body, ShouldDecode, RetryFun(Attempt, Reason)); + request_and_retry(Config, Headers, Body, ShouldDecode, RetryFun(Attempt, MaxAttempt, Reason)); {error, Reason} -> {error, Reason} end; {ok, {{Status, StatusLine}, _, RespBody}} when Status >= 500 -> - request_and_retry(Config, Headers, Body, ShouldDecode, RetryFun(Attempt, {http_error, Status, StatusLine, RespBody})); + request_and_retry(Config, Headers, Body, ShouldDecode, RetryFun(Attempt, MaxAttempt, {http_error, Status, StatusLine, RespBody})); {ok, {{Status, StatusLine}, _, RespBody}} -> {error, {http_error, Status, StatusLine, RespBody}}; {error, Reason} -> %% TODO there may be some http errors, such as certificate error, that we don't want to retry - request_and_retry(Config, Headers, Body, ShouldDecode, RetryFun(Attempt, Reason)) + request_and_retry(Config, Headers, Body, ShouldDecode, RetryFun(Attempt, MaxAttempt, Reason)) end. -spec client_error(pos_integer(), string(), binary()) -> {retry, term()} | {error, term()}. client_error(Status, StatusLine, Body) -> - try jsx:decode(Body) of + try jsx:decode(Body, [{return_maps, false}]) of Json -> Message = proplists:get_value(<<"message">>, Json, <<>>), case proplists:get_value(<<"__type">>, Json) of @@ -176,4 +181,4 @@ port_spec(#aws_config{kinesis_port=Port}) -> [":", erlang:integer_to_list(Port)]. decode(<<>>) -> []; -decode(JSON) -> jsx:decode(JSON). +decode(JSON) -> jsx:decode(JSON, [{return_maps, false}]). diff --git a/src/erlcloud_kms.erl b/src/erlcloud_kms.erl index a3141b932..6523b45bb 100644 --- a/src/erlcloud_kms.erl +++ b/src/erlcloud_kms.erl @@ -1011,7 +1011,7 @@ kms_request_no_update(Config, Operation, Body) -> request_body = Payload}, case erlcloud_aws:request_to_return(erlcloud_retry:request(Config, Request, fun kms_result_fun/1)) of {ok, {_RespHeaders, <<>>}} -> {ok, []}; - {ok, {_RespHeaders, RespBody}} -> {ok, jsx:decode(RespBody)}; + {ok, {_RespHeaders, RespBody}} -> {ok, jsx:decode(RespBody, [{return_maps, false}])}; {error, _} = Error-> Error end. diff --git a/src/erlcloud_lambda.erl b/src/erlcloud_lambda.erl index da450db7c..0b7b258fb 100644 --- a/src/erlcloud_lambda.erl +++ b/src/erlcloud_lambda.erl @@ -14,6 +14,7 @@ create_event_source_mapping/4, create_event_source_mapping/5, create_function/6, create_function/7, delete_event_source_mapping/1, delete_event_source_mapping/2, + delete_function/1, delete_function/2, delete_function/3, get_alias/2, get_alias/3, get_event_source_mapping/1, get_event_source_mapping/2, get_function/1, get_function/2, get_function/3, @@ -33,7 +34,7 @@ 'python2.7' | 'python3.6' | 'dotnetcore1.0' | 'dotnetcore2.0' | 'dotnetcore2.1' | 'nodejs4.3-edge' | 'go1.x'). -type(return_val() :: any()). - +-import(erlcloud_util, [filter_undef/1]). %%------------------------------------------------------------------------------ %% Library initialization. @@ -107,7 +108,7 @@ create_alias(FunctionName, FunctionVersion, Options :: proplist(), Config :: aws_config()) -> return_val(). create_alias(FunctionName, FunctionVersion, AliasName, Options, Config) -> - Path = base_path() ++ "functions/" ++ binary_to_list(FunctionName) ++ "/aliases", + Path = base_path() ++ "functions/" ++ url_parameter(FunctionName) ++ "/aliases", Json = [{<<"FunctionVersion">>, FunctionVersion}, {<<"Name">>, AliasName} | Options], @@ -164,9 +165,9 @@ create_event_source_mapping(EventSourceArn, FunctionName, %% %%------------------------------------------------------------------------------ -spec create_function(Code :: erlcloud_lambda_code(), - FunctionName :: string(), - Handler :: string(), - Role :: string(), + FunctionName :: binary(), + Handler :: binary(), + Role :: binary(), Runtime :: runtime(), Options :: proplist()) -> return_val(). create_function(#erlcloud_lambda_code{} = Code, @@ -175,9 +176,9 @@ create_function(#erlcloud_lambda_code{} = Code, Runtime, Options, default_config()). -spec create_function(Code :: erlcloud_lambda_code(), - FunctionName :: string(), - Handler :: string(), - Role :: string(), + FunctionName :: binary(), + Handler :: binary(), + Role :: binary(), Runtime :: runtime(), Options :: proplist(), Config :: aws_config()) -> return_val(). @@ -212,9 +213,37 @@ delete_event_source_mapping(Uuid) -> -spec delete_event_source_mapping(Uuid :: binary(), Config :: aws_config()) -> return_val(). delete_event_source_mapping(Uuid, Config) -> - Path = base_path() ++ "event-source-mappings/" ++ binary_to_list(Uuid), + Path = base_path() ++ "event-source-mappings/" ++ url_parameter(Uuid), lambda_request(Config, delete, Path, undefined). +%%------------------------------------------------------------------------------ +%% DeleteFunction +%%------------------------------------------------------------------------------ + +%%------------------------------------------------------------------------------ +%% @doc +%% Lambda API: +%% [https://docs.aws.amazon.com/lambda/latest/dg/API_DeleteFunction.html] +%% +%% ===Example=== +%% +%%------------------------------------------------------------------------------ +-spec delete_function(FunctionName :: binary()) -> return_val(). +delete_function(FunctionName) -> + delete_function(FunctionName, default_config()). + +-spec delete_function(FunctionName :: binary(), + Config :: aws_config()) -> return_val(). +delete_function(FunctionName, Config) -> + delete_function(FunctionName, undefined, Config). + +-spec delete_function(FunctionName :: binary(), + Qualifier :: undefined | binary(), + Config :: aws_config()) -> return_val(). +delete_function(FunctionName, Qualifier, Config) -> + Path = base_path() ++ "functions/" ++ url_parameter(FunctionName), + QParams = filter_undef([{"Qualifier", Qualifier}]), + lambda_request(Config, delete, Path, undefined, QParams). %%------------------------------------------------------------------------------ %% GetAlias @@ -239,7 +268,7 @@ get_alias(FunctionName, Name) -> Config :: aws_config()) -> return_val(). get_alias(FunctionName, Name, Config) -> Path = base_path() ++ "functions/" - ++ binary_to_list(FunctionName) ++ "/aliases/" ++ binary_to_list(Name), + ++ url_parameter(FunctionName) ++ "/aliases/" ++ url_parameter(Name), lambda_request(Config, get, Path, undefined). %%------------------------------------------------------------------------------ @@ -262,7 +291,7 @@ get_event_source_mapping(Uuid) -> -spec get_event_source_mapping(Uuid :: binary(), Config :: aws_config()) -> return_val(). get_event_source_mapping(Uuid, Config) -> - Path = base_path() ++ "event-source-mappings/" ++ binary_to_list(Uuid), + Path = base_path() ++ "event-source-mappings/" ++ url_parameter(Uuid), lambda_request(Config, get, Path, undefined). %%------------------------------------------------------------------------------ @@ -291,7 +320,7 @@ get_function(FunctionName, Config) -> Qualifier :: undefined | binary(), Config :: aws_config()) -> return_val(). get_function(FunctionName, Qualifier, Config) -> - Path = base_path() ++ "functions/" ++ binary_to_list(FunctionName), + Path = base_path() ++ "functions/" ++ url_parameter(FunctionName), QParams = filter_undef([{"Qualifier", Qualifier}]), lambda_request(Config, get, Path, undefined, QParams). @@ -317,7 +346,7 @@ get_function_configuration(FunctionName, Config) -> get_function_configuration(FunctionName, undefined, Config). get_function_configuration(Function, Qualifier, Config) -> - Path = base_path() ++ "functions/" ++ binary_to_list(Function) ++ "/configuration", + Path = base_path() ++ "functions/" ++ url_parameter(Function) ++ "/configuration", QParams = filter_undef([{"Qualifier", Qualifier}]), lambda_request(Config, get, Path, undefined, QParams). @@ -328,7 +357,7 @@ get_function_configuration(Function, Qualifier, Config) -> %% [http://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html] %% %% option {show_headers, true|false} may be used for invoking Lambdas -%% this option changes returned spec of successfull Lambda invocation +%% this option changes returned spec of successful Lambda invocation %% false(default): output spec is {ok, Data} %% true: output spec is {ok, Headers, Data} where additional headers of %% Lambda invocation is returned @@ -357,13 +386,13 @@ invoke(FunctionName, Payload) when is_list(Payload)-> invoke(FunctionName, Payload, default_config()). -spec invoke(FunctionName :: binary(), - Payload :: list(), + Payload :: list() | binary(), Config :: aws_config() | binary()) -> return_val(). invoke(FunctionName, Payload, ConfigOrQualifier) when is_list(Payload)-> invoke(FunctionName, Payload, [], ConfigOrQualifier). -spec invoke(FunctionName :: binary(), - Payload :: list(), + Payload :: list() | binary(), Options :: list(), Config :: aws_config() | binary()) -> return_val(). invoke(FunctionName, Payload, Options, Config = #aws_config{}) -> @@ -372,14 +401,14 @@ invoke(FunctionName, Payload, Options, Qualifier) when is_binary(Qualifier) -> invoke(FunctionName, Payload, Options, Qualifier, default_config()). -spec invoke(FunctionName :: binary(), - Payload :: list(), + Payload :: list() | binary(), Options :: list(), Qualifier :: binary()| undefined, Config :: aws_config()) -> return_val(). invoke(FunctionName, Payload, Options, Qualifier, Config = #aws_config{}) -> - Path = base_path() ++ "functions/" ++ binary_to_list(FunctionName) ++ "/invocations", + Path = base_path() ++ "functions/" ++ url_parameter(FunctionName) ++ "/invocations", QParams = filter_undef([{"Qualifier", Qualifier}]), - lambda_request(Config, post, Path, Options, Payload, QParams). + lambda_request(Config, post, Path, Payload, QParams, Options). %%------------------------------------------------------------------------------ %% ListAliases @@ -418,7 +447,7 @@ list_aliases(FunctionName, FunctionVersion, Marker, MaxItems) -> Config :: aws_config()) -> return_val(). list_aliases(FunctionName, FunctionVersion, Marker, MaxItems, Config) -> Path = base_path() ++ "functions/" - ++ binary_to_list(FunctionName) ++ "/aliases", + ++ url_parameter(FunctionName) ++ "/aliases", QParams = filter_undef([{"Marker", Marker}, {"MaxItems", MaxItems}, {"FunctionVersion", FunctionVersion}]), @@ -520,7 +549,7 @@ list_versions_by_function(Function, Marker, MaxItems) -> MaxItems :: integer() | undefined, Config :: aws_config()) -> return_val(). list_versions_by_function(Function, Marker, MaxItems, Config) -> - Path = base_path() ++ "functions/" ++ binary_to_list(Function) ++ "/versions", + Path = base_path() ++ "functions/" ++ url_parameter(Function) ++ "/versions", QParams = filter_undef([{"Marker", Marker}, {"MaxItems", MaxItems}]), lambda_request(Config, get, Path, undefined, QParams). @@ -553,7 +582,7 @@ publish_version(FunctionName, CodeSha, Description) -> Description :: binary() | undefined, Config :: aws_config()) -> return_val(). publish_version(FunctionName, CodeSha, Description, Config) -> - Path = base_path() ++ "functions/" ++ binary_to_list(FunctionName) ++ "/versions", + Path = base_path() ++ "functions/" ++ url_parameter(FunctionName) ++ "/versions", Json = filter_undef([{<<"CodeSha">>, CodeSha}, {<<"Description">>, Description}]), lambda_request(Config, post, Path, Json). @@ -590,7 +619,7 @@ update_alias(FunctionName, AliasName, Description, FunctionVersion) -> Config :: aws_config()) -> return_val(). update_alias(FunctionName, AliasName, Description, FunctionVersion, Config) -> Path = base_path() ++ "functions/" - ++ binary_to_list(FunctionName) ++ "/aliases/" ++ binary_to_list(AliasName), + ++ url_parameter(FunctionName) ++ "/aliases/" ++ url_parameter(AliasName), Json = filter_undef([{"Description", Description}, {"FunctionVersion", FunctionVersion}]), lambda_request(Config, put, Path, Json). @@ -622,7 +651,7 @@ update_event_source_mapping(Uuid, BatchSize, Enabled, FunctionName) -> FunctionName :: binary() | undefined, Config :: aws_config()) -> return_val(). update_event_source_mapping(Uuid, BatchSize, Enabled, FunctionName, Config) -> - Path = base_path() ++ "event-source-mappings/" ++ binary_to_list(Uuid), + Path = base_path() ++ "event-source-mappings/" ++ url_parameter(Uuid), Json = filter_undef([{<<"BatchSize">>, BatchSize}, {<<"Enabled">>, Enabled}, {<<"FunctionName">>, FunctionName}]), @@ -652,7 +681,7 @@ update_function_code(FunctionName, Publish, Code) -> Code :: erlcloud_lambda_code(), Config :: aws_config()) -> return_val(). update_function_code(FunctionName, Publish, Code, Config) -> - Path = base_path() ++ "functions/" ++ binary_to_list(FunctionName) ++ "/code", + Path = base_path() ++ "functions/" ++ url_parameter(FunctionName) ++ "/code", Json = [{<<"Publish">>, Publish} | from_record(Code)], lambda_request(Config, put, Path, Json). @@ -700,7 +729,7 @@ update_function_configuration(FunctionName, Description, Handler, Configuration :: list(tuple()), % JSX json object Config :: aws_config()) -> return_val(). update_function_configuration(FunctionName, Configuration, Config) when is_list(Configuration) -> - Path = base_path() ++ "functions/" ++ binary_to_list(FunctionName) ++ "/configuration", + Path = base_path() ++ "functions/" ++ url_parameter(FunctionName) ++ "/configuration", Json = filter_undef(Configuration), lambda_request(Config, put, Path, Json). @@ -718,49 +747,54 @@ from_record(#erlcloud_lambda_code{s3Bucket = S3Bucket, {<<"ZipFile">>, ZipFile}], filter_undef(List). -filter_undef(List) -> - lists:filter(fun({_Name, Value}) -> Value =/= undefined end, List). +url_parameter(Param) -> + erlcloud_http:url_encode(binary_to_list(Param)). base_path() -> "/" ++ ?API_VERSION ++ "/". lambda_request(Config, Method, Path, Body) -> - lambda_request(Config, Method, Path, [], Body, []). + lambda_request(Config, Method, Path, Body, []). + lambda_request(Config, Method, Path, Body, QParams) -> - lambda_request(Config, Method, Path, [], Body, QParams). + lambda_request(Config, Method, Path, Body, QParams, []). -lambda_request(Config, Method, Path, Options, Body, QParam) -> +lambda_request(Config, Method, Path, Body, QParams, Options) -> case erlcloud_aws:update_config(Config) of {ok, Config1} -> - lambda_request_no_update(Config1, Method, Path, Options, Body, QParam); + lambda_request_no_update(Config1, Method, Path, Options, Body, QParams); {error, Reason} -> {error, Reason} end. -lambda_request_no_update(Config, Method, Path, Options, Body, QParam) -> - Form = case encode_body(Body) of - <<>> -> erlcloud_http:make_query_string(QParam); - Value -> Value - end, +lambda_request_no_update(Config, Method, Path, Options, Body0, QParams) -> ShowRespHeaders = proplists:get_value(show_headers, Options, false), - Hdrs = proplists:delete(show_headers, Options), - Headers = headers(Method, Path, Hdrs, Config, encode_body(Body), QParam), + RawBody = proplists:get_value(raw_response_body, Options, false), + Hdrs0 = proplists:delete(show_headers, Options), + Hdrs = proplists:delete(raw_response_body, Hdrs0), + Body = encode_body(Body0), + Headers = headers(Method, Path, Hdrs, Config, Body, QParams), + QueryString = erlcloud_http:make_query_string(QParams), case erlcloud_aws:do_aws_request_form_raw( - Method, Config#aws_config.lambda_scheme, Config#aws_config.lambda_host, - Config#aws_config.lambda_port, Path, Form, Headers, Config, ShowRespHeaders) of + Method, Config#aws_config.lambda_scheme, Config#aws_config.lambda_host, + Config#aws_config.lambda_port, Path, Body, Headers, QueryString, Config, ShowRespHeaders) of {ok, RespHeaders, RespBody} -> - {ok, RespHeaders, decode_body(RespBody)}; + {ok, RespHeaders, decode_body(RespBody, RawBody)}; {ok, RespBody} -> - {ok, decode_body(RespBody)}; + {ok, decode_body(RespBody, RawBody)}; E -> E end. -decode_body(<<>>) -> +decode_body(Body, true) -> + Body; +decode_body(<<>>, _RawBody) -> []; -decode_body(BinData) -> - jsx:decode(BinData). +decode_body(BinData, _RawBody) -> + jsx:decode(BinData, [{return_maps, false}]). +encode_body(Bin) when is_binary(Bin) -> + Bin; encode_body(undefined) -> <<>>; encode_body([]) -> @@ -768,9 +802,9 @@ encode_body([]) -> encode_body(Body) -> jsx:encode(Body). -headers(Method, Uri, Hdrs, Config, Body, QParam) -> +headers(Method, Uri, Hdrs, Config, Body, QParams) -> Headers = [{"host", Config#aws_config.lambda_host}, {"content-type", "application/json"} | Hdrs], Region = erlcloud_aws:aws_region_from_host(Config#aws_config.lambda_host), - erlcloud_aws:sign_v4(Method, Uri, Config, - Headers, Body, Region, "lambda", QParam). + erlcloud_aws:sign_v4(Method, erlcloud_http:url_encode_loose(Uri), Config, + Headers, Body, Region, "lambda", QParams). diff --git a/src/erlcloud_mes.erl b/src/erlcloud_mes.erl index f9d031d1e..b14af2e99 100644 --- a/src/erlcloud_mes.erl +++ b/src/erlcloud_mes.erl @@ -169,9 +169,9 @@ mes_request(Config, Operation, Json) -> mes_request_no_update(#aws_config{mes_scheme = Scheme, mes_host = Host, mes_port = Port} = Config, Operation, Json) -> Body = jsx:encode(Json), Headers = headers(Config, Operation, Body), - case erlcloud_aws:aws_request_form_raw(post, Scheme, Host, Port, "/", Body, Headers, Config) of + case erlcloud_aws:aws_request_form_raw(post, Scheme, Host, Port, "/", Body, Headers, [], Config) of {ok, Response} -> - {ok, jsx:decode(Response)}; + {ok, jsx:decode(Response, [{return_maps, false}])}; {error, Reason} -> {error, Reason} end. diff --git a/src/erlcloud_mms.erl b/src/erlcloud_mms.erl index 9b087b29f..b6c46c793 100644 --- a/src/erlcloud_mms.erl +++ b/src/erlcloud_mms.erl @@ -168,9 +168,9 @@ mms_request(Config, Operation, Json) -> mms_request_no_update(#aws_config{mms_scheme = Scheme, mms_host = Host, mms_port = Port} = Config, Operation, Json) -> Body = jsx:encode(Json), Headers = headers(Config, Operation, Body), - case erlcloud_aws:aws_request_form_raw(post, Scheme, Host, Port, "/", Body, Headers, Config) of + case erlcloud_aws:aws_request_form_raw(post, Scheme, Host, Port, "/", Body, Headers, [], Config) of {ok, Response} -> - {ok, jsx:decode(Response)}; + {ok, jsx:decode(Response, [{return_maps, false}])}; {error, Reason} -> {error, Reason} end. diff --git a/src/erlcloud_mon.erl b/src/erlcloud_mon.erl index 2aa8f5b9c..93392d303 100644 --- a/src/erlcloud_mon.erl +++ b/src/erlcloud_mon.erl @@ -16,6 +16,9 @@ -export([ list_metrics/5, list_metrics/4, + describe_alarms_for_metric/2, + describe_alarms_for_metric/7, + describe_alarms_for_metric/8, put_metric_data/3, put_metric_data/2, put_metric_data/6, put_metric_data/5, get_metric_statistics/4, get_metric_statistics/9, get_metric_statistics/8, @@ -30,7 +33,8 @@ -include("erlcloud_mon.hrl"). -include_lib("xmerl/include/xmerl.hrl"). --import(erlcloud_xml, [get_text/2, get_time/2]). +-import(erlcloud_xml, [get_text/2, get_time/2, get_bool/2, get_integer/2, + get_float/2, get_text/1]). -define(XMLNS_MON, "http://monitoring.amazonaws.com/doc/2010-08-01/"). -define(API_VERSION, "2010-08-01"). @@ -55,7 +59,7 @@ MetricName ::string(), DimensionFilter ::[{string(),string()}], NextToken ::string() - ) -> term() | no_return(). + ) -> term(). list_metrics( Namespace, @@ -71,7 +75,7 @@ list_metrics( DimensionFilter ::[{string(),string()}], NextToken ::string(), Config ::aws_config() - ) -> term() | no_return(). + ) -> term(). list_metrics( Namespace, @@ -116,6 +120,183 @@ extract_dimension(Node) -> {value, get_text("Value", Node)} ]. +%%------------------------------------------------------------------------------ +%% @doc CloudWatch API - DescribeAlarmsForMetric +%% [https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_DescribeAlarmsForMetric.html] +%% +%% USAGE: +%% +%% erlcloud_mon:describe_alarms_for_metric("AWS/EC2", +%% "NetworkIn"). +%% [[{metric_name,"NetworkIn"}, +%% {namespace,"AWS/EC2"}, +%% {dimensions,[]}, +%% {actions_enabled,true}, +%% {alarm_actions,[{arn,"arn:aws:sns:us-east-1:123456797777:rgallego_cloudtrail_sns_topic"}]}, +%% {alarm_arn,"arn:aws:cloudwatch:us-east-1:123456797777:alarm:rgallego_unauthorized_alarm"}, +%% {alarm_configuration_updated_timestamp,{{2018,2,7}, +%% {17,38,24}}}, +%% {alarm_description,[]}, +%% {alarm_name,"rgallego_unauthorized_alarm"}, +%% {comparison_operator,"GreaterThanOrEqualToThreshold"}, +%% {evaluate_low_sample_count_percentile,[]}, +%% {evaluation_periods,1}, +%% {extended_statistic,[]}, +%% {insufficient_data_actions,[]}, +%% {ok_actions,[]}, +%% {period,300}, +%% {state_reason,"Threshold Crossed: 1 datapoint [2.0 (07/02/18 17:33:00)] was greater than or equal to the threshold (1.0)."}, +%% {state_reason_data,"{\"version\":\"1.0\",\"queryDate\":\"2018-02-07T17:38:24.953+0000\",\"startDate\":\"2018-02-07T17:33:00.000+0000\",\"statistic\":\"Sum\",\"period\":300,\"recentDatapoints\":[2.0],\"threshold\":1.0}"}, +%% {state_updated_timestamp,{{2018,2,7},{17,38,24}}}, +%% {state_value,"ALARM"}, +%% {statistic,"Sum"}, +%% {threshold,1.0}, +%% {treat_missing_data,[]}, +%% {unit,[]}]] +%% +%% @end +%%------------------------------------------------------------------------------ +-spec describe_alarms_for_metric( + Namespace ::string(), + MetricName ::string() + ) -> term(). + +describe_alarms_for_metric( + Namespace, + MetricName + ) -> + describe_alarms_for_metric(Namespace, MetricName, [], + "", undefined, "", + "", default_config()). + +%%------------------------------------------------------------------------------ +%% @doc CloudWatch API - DescribeAlarmsForMetric +%% [https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_DescribeAlarmsForMetric.html] +%% +%% USAGE: +%% +%% erlcloud_mon:describe_alarms_for_metric("AWS/EC2", +%% "NetworkIn", +%% [{"InstanceType","m1.large"}], +%% "p95", +%% 17, +%% "", +%% "Seconds"). +%% See describe_alarms_for_metric/2 +%% +%% @end +%%------------------------------------------------------------------------------ +-spec describe_alarms_for_metric( + Namespace ::string(), + MetricName ::string(), + DimensionFilter ::[{string(),string()}], + ExtendedStatistic ::string(), + Period ::pos_integer(), + Statistic ::statistic(), + Unit ::unit() + ) -> term(). + +describe_alarms_for_metric( + Namespace, + MetricName, + DimensionFilter, + ExtendedStatistic, + Period, + Statistic, + Unit + ) -> + describe_alarms_for_metric(Namespace, MetricName, DimensionFilter, + ExtendedStatistic, Period, Statistic, + Unit, default_config()). + +-spec describe_alarms_for_metric( + Namespace ::string(), + MetricName ::string(), + DimensionFilter ::[{string(),string()}], + ExtendedStatistic ::string(), + Period ::pos_integer() | undefined, + Statistic ::statistic(), + Unit ::unit(), + Config ::aws_config() + ) -> term(). + +describe_alarms_for_metric( + Namespace, + MetricName, + DimensionFilter, + ExtendedStatistic, + Period, + Statistic, + Unit, + #aws_config{} = Config + ) -> + + Params = + [{"Namespace", Namespace}, + {"MetricName", MetricName}] + ++ + [{"ExtendedStatistic", ExtendedStatistic} || ExtendedStatistic/=""] + ++ + [{"Period", Period} || Period/=undefined] + ++ + [{"Statistic", Statistic} || Statistic/=""] + ++ + [{"Unit", Unit} || Unit/=""] + ++ + lists:flatten( + [begin + {Name, Value} = lists:nth(N, DimensionFilter), + [{?FMT("Dimensions.member.~b.Name", [N]), Name}, + {?FMT("Dimensions.member.~b.Value", [N]), Value}] + end + || N<-lists:seq(1, length(DimensionFilter))] + ), + Doc = mon_query(Config, "DescribeAlarmsForMetric", Params), + Members = xmerl_xpath:string("/DescribeAlarmsForMetricResponse/DescribeAlarmsForMetricResult/MetricAlarms/member", Doc), + [extract_member_dafm(Member) || Member <- Members]. + +extract_member_dafm(Node) -> + [ + {metric_name, get_text("MetricName", Node)}, + {namespace, get_text("Namespace", Node)}, + {dimensions, + [extract_dimension_dafm(Item) || Item <- xmerl_xpath:string("Dimensions/member", Node)] + }, + {actions_enabled, get_bool("ActionsEnabled", Node)}, + {alarm_actions, + [extract_actions_dafm(Item) || Item <- xmerl_xpath:string("AlarmActions/member", Node)]}, + {alarm_arn, get_text("AlarmArn", Node)}, + {alarm_configuration_updated_timestamp, get_time("AlarmConfigurationUpdatedTimestamp", Node)}, + {alarm_description, get_text("AlarmDescription", Node)}, + {alarm_name, get_text("AlarmName", Node)}, + {comparison_operator, get_text("ComparisonOperator", Node)}, + {evaluate_low_sample_count_percentile, get_text("EvaluateLowSampleCountPercentile", Node)}, + {evaluation_periods, get_integer("EvaluationPeriods", Node)}, + {extended_statistic, get_text("ExtendedStatistic", Node)}, + {insufficient_data_actions, + [extract_actions_dafm(Item) || Item <- xmerl_xpath:string("InsufficientDataActions/member", Node)]}, + {ok_actions, + [extract_actions_dafm(Item) || Item <- xmerl_xpath:string("OKActions/member", Node)]}, + {period, get_integer("Period", Node)}, + {state_reason, get_text("StateReason", Node)}, + {state_reason_data, get_text("StateReasonData", Node)}, + {state_updated_timestamp, get_time("StateUpdatedTimestamp", Node)}, + {state_value, get_text("StateValue", Node)}, + {statistic, get_text("Statistic", Node)}, + {threshold, get_float("Threshold", Node)}, + {treat_missing_data, get_text("TreatMissingData", Node)}, + {unit, get_text("Unit", Node)} + ]. + +extract_dimension_dafm(Node) -> + [ + {name, get_text("Name", Node)}, + {value, get_text("Value", Node)} + ]. + +extract_actions_dafm(Node) -> + {arn, get_text(Node)}. + %%------------------------------------------------------------------------------ %% @doc CloudWatch API - PutMetricData %% [http://docs.amazonwebservices.com/AmazonCloudWatch/latest/APIReference/index.html?API_PutMetricData.html] @@ -140,7 +321,7 @@ extract_dimension(Node) -> -spec put_metric_data( Namespace ::string(), MetricData ::[metric_datum()] - ) -> term() | no_return(). + ) -> term(). put_metric_data(Namespace, MetricData) -> put_metric_data(Namespace, MetricData, default_config()). @@ -149,7 +330,7 @@ put_metric_data(Namespace, MetricData) -> Namespace ::string(), MetricData ::[metric_datum()], Config ::aws_config() - ) -> term() | no_return(). + ) -> term(). put_metric_data(Namespace, MetricData, #aws_config{} = Config) -> @@ -226,7 +407,7 @@ params_stat(Prefix, StatisticValues) -> Value ::string(), Unit ::unit(), Timestamp ::datetime()|string() - ) -> term() | no_return(). + ) -> term(). put_metric_data(Namespace, MetricName, Value, Unit, Timestamp) -> put_metric_data(Namespace, MetricName, Value, Unit, Timestamp, default_config()). @@ -238,7 +419,7 @@ put_metric_data(Namespace, MetricName, Value, Unit, Timestamp) -> Unit ::unit(), Timestamp ::datetime()|string(), Config ::aws_config() - ) -> term() | no_return(). + ) -> term(). put_metric_data(Namespace, MetricName, Value, Unit, Timestamp, #aws_config{} = Config) -> Params = @@ -256,7 +437,7 @@ put_metric_data(Namespace, MetricName, Value, Unit, Timestamp, #aws_config{} = C %%------------------------------------------------------------------------------ %% @doc CloudWatch API - GetMetricStatistics - Easy average version -%% Gets average and max stats at 60 second intervals for +%% Gets average and max stats at 60 second intervals for %% the given metric on the given instance for the given interval %% @end %%------------------------------------------------------------------------------ @@ -265,7 +446,7 @@ put_metric_data(Namespace, MetricName, Value, Unit, Timestamp, #aws_config{} = C StartTime ::datetime() | string(), EndTime ::datetime() | string(), InstanceId ::string() - ) -> term() | no_return(). + ) -> term(). get_metric_statistics( MetricName, @@ -297,8 +478,8 @@ get_metric_statistics( %% "Percent", %% ["Average", "Maximum"], %% [{"InstanceType", "t2.micro"}]). -%% -%% @end +%% +%% @end %%------------------------------------------------------------------------------ -spec get_metric_statistics( Namespace ::string(), @@ -309,7 +490,7 @@ get_metric_statistics( Unit ::string(), Statistics ::[string()], Dimensions ::[{string(), string()}] - ) -> term() | no_return(). + ) -> term(). get_metric_statistics( Namespace, @@ -343,7 +524,7 @@ get_metric_statistics( Statistics ::[string()], Dimensions ::[{string(), string()}], Config ::aws_config() - ) -> term() | no_return(). + ) -> term(). get_metric_statistics( Namespace, @@ -403,7 +584,7 @@ extract_metrics(Node, Statistics) -> %%------------------------------------------------------------------------------ -spec get_alarm_state( AlarmName ::string() - ) -> string() | no_return(). + ) -> string(). get_alarm_state(AlarmName) -> get_alarm_state(AlarmName, default_config()). @@ -411,7 +592,7 @@ get_alarm_state(AlarmName) -> -spec get_alarm_state( AlarmName ::string(), Config ::aws_config() - ) -> string() | no_return(). + ) -> string(). get_alarm_state(AlarmName, #aws_config{} = Config) -> Params = [{"AlarmNames.member.1", AlarmName}], diff --git a/src/erlcloud_mturk.erl b/src/erlcloud_mturk.erl index 99bfb64ec..e11653c9c 100644 --- a/src/erlcloud_mturk.erl +++ b/src/erlcloud_mturk.erl @@ -92,22 +92,22 @@ configure(AccessKeyId, SecretAccessKey, Host) -> default_config() -> erlcloud_aws:default_config(). --spec approve_assignment(string(), string() | none) -> ok | no_return(). +-spec approve_assignment(string(), string() | none) -> ok. approve_assignment(AssignmentId, RequesterFeedback) -> approve_assignment(AssignmentId, RequesterFeedback, default_config()). --spec approve_assignment(string(), string() | none, aws_config()) -> ok | no_return(). +-spec approve_assignment(string(), string() | none, aws_config()) -> ok. approve_assignment(AssignmentId, RequesterFeedback, Config) when is_list(AssignmentId), is_list(RequesterFeedback) orelse RequesterFeedback =:= none -> mturk_simple_request(Config, "ApproveAssignment", [{"AssignmentId", AssignmentId}, {"RequesterFeedback", RequesterFeedback}]). --spec assign_qualification(string(), string()) -> ok | no_return(). +-spec assign_qualification(string(), string()) -> ok. assign_qualification(QualificationTypeId, WorkerId) -> assign_qualification(QualificationTypeId, WorkerId, default_config()). --spec assign_qualification(string(), string(), integer() | aws_config()) -> ok | no_return(). +-spec assign_qualification(string(), string(), integer() | aws_config()) -> ok. assign_qualification(QualificationTypeId, WorkerId, Config) when is_record(Config, aws_config) -> assign_qualification(QualificationTypeId, WorkerId, 1, Config); @@ -115,7 +115,7 @@ assign_qualification(QualificationTypeId, WorkerId, IntegerValue) -> assign_qualification(QualificationTypeId, WorkerId, IntegerValue, false). --spec assign_qualification(string(), string(), integer(), boolean() | aws_config()) -> ok | no_return(). +-spec assign_qualification(string(), string(), integer(), boolean() | aws_config()) -> ok. assign_qualification(QualificationTypeId, WorkerId, IntegerValue, Config) when is_record(Config, aws_config) -> assign_qualification(QualificationTypeId, WorkerId, IntegerValue, false, Config); @@ -123,7 +123,7 @@ assign_qualification(QualificationTypeId, WorkerId, IntegerValue, SendNotificati assign_qualification(QualificationTypeId, WorkerId, IntegerValue, SendNotification, default_config()). --spec assign_qualification(string(), string(), integer(), boolean(), aws_config()) -> ok | no_return(). +-spec assign_qualification(string(), string(), integer(), boolean(), aws_config()) -> ok. assign_qualification(QualificationTypeId, WorkerId, IntegerValue, SendNotification, Config) when is_list(QualificationTypeId), is_list(WorkerId), @@ -134,34 +134,34 @@ assign_qualification(QualificationTypeId, WorkerId, IntegerValue, SendNotificati {"IntegerValue", IntegerValue}, {"SendNotification", SendNotification}]). --spec block_worker(string(), string()) -> ok | no_return(). +-spec block_worker(string(), string()) -> ok. block_worker(WorkerId, Reason) -> block_worker(WorkerId, Reason, default_config()). --spec block_worker(string(), string(), aws_config()) -> ok | no_return(). +-spec block_worker(string(), string(), aws_config()) -> ok. block_worker(WorkerId, Reason, Config) when is_list(WorkerId), is_list(Reason) -> mturk_simple_request(Config, "BlockWorker", [{"WorkerId", WorkerId}, {"Reason", Reason}]). --spec change_hit_type_of_hit(string(), string()) -> ok | no_return(). +-spec change_hit_type_of_hit(string(), string()) -> ok. change_hit_type_of_hit(HITId, HITTypeId) -> change_hit_type_of_hit(HITId, HITTypeId, default_config()). --spec change_hit_type_of_hit(string(), string(), aws_config()) -> ok | no_return(). +-spec change_hit_type_of_hit(string(), string(), aws_config()) -> ok. change_hit_type_of_hit(HITId, HITTypeId, Config) when is_list(HITId), is_list(HITTypeId) -> mturk_simple_request(Config, "ChangeHITTypeOfHIT", [{"HITId", HITId}, {"HITTypeId", HITTypeId}]). -spec create_hit(string(), mturk_question(), 30..3153600, - 1..1000000000, string() | none) -> proplist() | no_return(). + 1..1000000000, string() | none) -> proplist(). create_hit(HITTypeId, Question, LifetimeInSeconds, MaxAssignments, RequesterAnnotation) -> create_hit(HITTypeId, Question, LifetimeInSeconds, MaxAssignments, RequesterAnnotation, default_config()). -spec create_hit(string(), mturk_question(), 30..3153600, - 1..1000000000, string() | none, aws_config()) -> proplist() | no_return(). + 1..1000000000, string() | none, aws_config()) -> proplist(). create_hit(HITTypeId, Question, LifetimeInSeconds, MaxAssignments, RequesterAnnotation, Config) when is_list(HITTypeId), @@ -187,11 +187,11 @@ create_hit(HITTypeId, Question, LifetimeInSeconds, MaxAssignments, Doc ). --spec create_hit(#mturk_hit{}) -> proplist() | no_return(). +-spec create_hit(#mturk_hit{}) -> proplist(). create_hit(HIT) -> create_hit(HIT, default_config()). --spec create_hit(#mturk_hit{}, aws_config()) -> proplist() | no_return(). +-spec create_hit(#mturk_hit{}, aws_config()) -> proplist(). create_hit(HIT, Config) -> QuestionXML = xml_to_string(encode_xml(HIT#mturk_hit.question)), Params = [ @@ -245,11 +245,11 @@ encode_locale_value(undefined) -> []; encode_locale_value(#mturk_locale{country_code=Country}) -> [{"Country", Country}]. --spec create_qualification_type(#mturk_qualification_type{}) -> proplist() | no_return(). +-spec create_qualification_type(#mturk_qualification_type{}) -> proplist(). create_qualification_type(QType) -> create_qualification_type(QType, default_config()). --spec create_qualification_type(#mturk_qualification_type{}, aws_config()) -> proplist() | no_return(). +-spec create_qualification_type(#mturk_qualification_type{}, aws_config()) -> proplist(). create_qualification_type(QType, Config) when is_record(QType, mturk_qualification_type) -> Doc = mturk_xml_request(Config, "CreateQualificationType", @@ -274,38 +274,38 @@ qualification_type_params(QType) -> {"AutoGrantedValue", case AutoGranted of true -> AutoGrantedValue; false -> undefined end} ]. --spec disable_hit(string()) -> ok | no_return(). +-spec disable_hit(string()) -> ok. disable_hit(HITId) -> disable_hit(HITId, default_config()). --spec disable_hit(string(), aws_config()) -> ok | no_return(). +-spec disable_hit(string(), aws_config()) -> ok. disable_hit(HITId, Config) when is_list(HITId) -> mturk_simple_request(Config, "DisableHIT", [{"HITId", HITId}]). --spec dispose_hit(string()) -> ok | no_return(). +-spec dispose_hit(string()) -> ok. dispose_hit(HITId) -> dispose_hit(HITId, default_config()). --spec dispose_hit(string(), aws_config()) -> ok | no_return(). +-spec dispose_hit(string(), aws_config()) -> ok. dispose_hit(HITId, Config) when is_list(HITId) -> mturk_simple_request(Config, "DisposeHIT", [{"HITId", HITId}]). --spec dispose_qualification_type(string()) -> ok | no_return(). +-spec dispose_qualification_type(string()) -> ok. dispose_qualification_type(QualificationTypeId) -> dispose_qualification_type(QualificationTypeId, default_config()). --spec dispose_qualification_type(string(), aws_config()) -> ok | no_return(). +-spec dispose_qualification_type(string(), aws_config()) -> ok. dispose_qualification_type(QualificationTypeId, Config) when is_list(QualificationTypeId) -> mturk_simple_request(Config, "DisposeQualificationType", [{"QualificationTypeId", QualificationTypeId}]). --spec extend_hit(string(), 1..1000000000 | none, 3600..31536000 | none) -> ok | no_return(). +-spec extend_hit(string(), 1..1000000000 | none, 3600..31536000 | none) -> ok. extend_hit(HITId, MaxAssignmentsIncrement, ExpirationIncrementInSeconds) -> extend_hit(HITId, MaxAssignmentsIncrement, ExpirationIncrementInSeconds, default_config()). --spec extend_hit(string(), 1..1000000000 | none, 3600..31536000 | none, aws_config()) -> ok | no_return(). +-spec extend_hit(string(), 1..1000000000 | none, 3600..31536000 | none, aws_config()) -> ok. extend_hit(HITId, MaxAssignmentsIncrement, ExpirationIncrementInSeconds, Config) when is_list(HITId), (MaxAssignmentsIncrement >= 1 andalso MaxAssignmentsIncrement =< 1000000000) orelse MaxAssignmentsIncrement =:= none, @@ -316,19 +316,19 @@ extend_hit(HITId, MaxAssignmentsIncrement, ExpirationIncrementInSeconds, Config) {"MaxAssignmentsIncrement", MaxAssignmentsIncrement}, {"ExpirationIncrementInSeconds", ExpirationIncrementInSeconds}]). --spec force_expire_hit(string()) -> ok | no_return(). +-spec force_expire_hit(string()) -> ok. force_expire_hit(HITId) -> force_expire_hit(HITId, default_config()). --spec force_expire_hit(string(), aws_config()) -> ok | no_return(). +-spec force_expire_hit(string(), aws_config()) -> ok. force_expire_hit(HITId, Config) when is_list(HITId) -> mturk_simple_request(Config, "ForceExpireHIT", [{"HITId", HITId}]). --spec get_account_balance() -> proplist() | no_return(). +-spec get_account_balance() -> proplist(). get_account_balance() -> get_account_balance(default_config()). --spec get_account_balance(aws_config()) -> proplist() | no_return(). +-spec get_account_balance(aws_config()) -> proplist(). get_account_balance(Config) -> Doc = mturk_xml_request(Config, "GetAccountBalance", []), erlcloud_xml:decode( @@ -339,18 +339,18 @@ get_account_balance(Config) -> Doc ). --spec get_assignments_for_hit(string()) -> proplist() | no_return(). +-spec get_assignments_for_hit(string()) -> proplist(). get_assignments_for_hit(HITId) -> get_assignments_for_hit(HITId, []). --spec get_assignments_for_hit(string(), proplist() | aws_config()) -> proplist() | no_return(). +-spec get_assignments_for_hit(string(), proplist() | aws_config()) -> proplist(). get_assignments_for_hit(HITId, Config) when is_record(Config, aws_config) -> get_assignments_for_hit(HITId, [], Config); get_assignments_for_hit(HITId, Options) -> get_assignments_for_hit(HITId, Options, default_config()). --spec get_assignments_for_hit(string(), proplist(), aws_config()) -> proplist() | no_return(). +-spec get_assignments_for_hit(string(), proplist(), aws_config()) -> proplist(). get_assignments_for_hit(HITId, Options, Config) when is_list(HITId), is_list(Options) -> Params = [ @@ -411,11 +411,11 @@ extract_assignment(Assignment) -> Assignment ). --spec get_bonus_payments_for_hit(string(), proplist()) -> proplist() | no_return(). +-spec get_bonus_payments_for_hit(string(), proplist()) -> proplist(). get_bonus_payments_for_hit(HITId, Options) -> get_bonus_payments_for_hit(HITId, Options, default_config()). --spec get_bonus_payments_for_hit(string(), proplist(), aws_config()) -> proplist() | no_return(). +-spec get_bonus_payments_for_hit(string(), proplist(), aws_config()) -> proplist(). get_bonus_payments_for_hit(HITId, Options, Config) when is_list(HITId), is_list(Options) -> Params = [ @@ -426,11 +426,11 @@ get_bonus_payments_for_hit(HITId, Options, Config) Doc = mturk_xml_request(Config, "GetBonusPayments", Params), extract_bonus_payments(Doc). --spec get_bonus_payments_for_assignment(string(), proplist()) -> proplist() | no_return(). +-spec get_bonus_payments_for_assignment(string(), proplist()) -> proplist(). get_bonus_payments_for_assignment(AssignmentId, Options) -> get_bonus_payments_for_assignment(AssignmentId, Options, default_config()). --spec get_bonus_payments_for_assignment(string(), proplist(), aws_config()) -> proplist() | no_return(). +-spec get_bonus_payments_for_assignment(string(), proplist(), aws_config()) -> proplist(). get_bonus_payments_for_assignment(AssignmentId, Options, Config) when is_list(AssignmentId), is_list(Options) -> Params = [ @@ -466,11 +466,11 @@ extract_bonus_payment(Payment) -> Payment ). --spec get_file_upload_url(string(), string()) -> string() | no_return(). +-spec get_file_upload_url(string(), string()) -> string(). get_file_upload_url(AssignmentId, QuestionIdentifier) -> get_file_upload_url(AssignmentId, QuestionIdentifier, default_config()). --spec get_file_upload_url(string(), string(), aws_config()) -> string() | no_return(). +-spec get_file_upload_url(string(), string(), aws_config()) -> string(). get_file_upload_url(AssignmentId, QuestionIdentifier, Config) when is_record(Config, aws_config) -> Params = [ @@ -480,27 +480,27 @@ get_file_upload_url(AssignmentId, QuestionIdentifier, Config) Doc = mturk_xml_request(Config, "GetFileUploadURL", Params), erlcloud_xml:get_text("FileUploadURL", Doc). --spec get_hit(string()) -> #mturk_hit{} | no_return(). +-spec get_hit(string()) -> #mturk_hit{}. get_hit(HITId) -> get_hit(HITId, default_config()). --spec get_hit(string(), aws_config()) -> #mturk_hit{} | no_return(). +-spec get_hit(string(), aws_config()) -> #mturk_hit{}. get_hit(HITId, Config) when is_list(HITId) -> Doc = mturk_xml_request(Config, "GetHIT", [{"HITId", HITId}]), hd(extract_hits([Doc])). --spec get_hits_for_qualification_type(string()) -> proplist() | no_return(). +-spec get_hits_for_qualification_type(string()) -> proplist(). get_hits_for_qualification_type(QualificationTypeId) -> get_hits_for_qualification_type(QualificationTypeId, []). --spec get_hits_for_qualification_type(string(), proplist() | aws_config()) -> proplist() | no_return(). +-spec get_hits_for_qualification_type(string(), proplist() | aws_config()) -> proplist(). get_hits_for_qualification_type(QualificationTypeId, Config) when is_record(Config, aws_config) -> get_hits_for_qualification_type(QualificationTypeId, [], Config); get_hits_for_qualification_type(QualificationTypeId, Options) -> get_hits_for_qualification_type(QualificationTypeId, Options, default_config()). --spec get_hits_for_qualification_type(string(), proplist(), aws_config()) -> proplist() | no_return(). +-spec get_hits_for_qualification_type(string(), proplist(), aws_config()) -> proplist(). get_hits_for_qualification_type(QualificationTypeId, Options, Config) when is_list(Options) -> Params = [ @@ -519,18 +519,18 @@ get_hits_for_qualification_type(QualificationTypeId, Options, Config) Doc ). --spec get_reviewable_hits() -> proplist() | no_return(). +-spec get_reviewable_hits() -> proplist(). get_reviewable_hits() -> get_reviewable_hits([]). --spec get_reviewable_hits(proplist() | aws_config()) -> proplist() | no_return(). +-spec get_reviewable_hits(proplist() | aws_config()) -> proplist(). get_reviewable_hits(Config) when is_record(Config, aws_config) -> get_reviewable_hits([], Config); get_reviewable_hits(Options) -> get_reviewable_hits(Options, default_config()). --spec get_reviewable_hits(proplist(), aws_config()) -> proplist() | no_return(). +-spec get_reviewable_hits(proplist(), aws_config()) -> proplist(). get_reviewable_hits(Options, Config) when is_list(Options) -> Params = [ @@ -573,11 +573,11 @@ get_reviewable_hits(Options, Config) Doc ). --spec get_qualification_score(string(), string()) -> proplist() | no_return(). +-spec get_qualification_score(string(), string()) -> proplist(). get_qualification_score(QualificationTypeId, SubjectId) -> get_qualification_score(QualificationTypeId, SubjectId, default_config()). --spec get_qualification_score(string(), string(), aws_config()) -> proplist() | no_return(). +-spec get_qualification_score(string(), string(), aws_config()) -> proplist(). get_qualification_score(QualificationTypeId, SubjectId, Config) when is_list(QualificationTypeId), is_list(SubjectId) -> Doc = mturk_xml_request(Config, "GetQualificationScore", @@ -596,11 +596,11 @@ get_qualification_score(QualificationTypeId, SubjectId, Config) Doc ). --spec get_qualification_type(string()) -> #mturk_qualification_type{} | no_return(). +-spec get_qualification_type(string()) -> #mturk_qualification_type{}. get_qualification_type(QualificationTypeId) -> get_qualification_type(QualificationTypeId, default_config()). --spec get_qualification_type(string(), aws_config()) -> #mturk_qualification_type{} | no_return(). +-spec get_qualification_type(string(), aws_config()) -> #mturk_qualification_type{}. get_qualification_type(QualificationTypeId, Config) when is_record(Config, aws_config) -> Doc = mturk_xml_request(Config, "GetQualificationType", @@ -635,18 +635,18 @@ extract_qualification_type(Node) -> decode_keywords(String) -> [string:strip(Keyword) || Keyword <- string:tokens(String, ",")]. --spec get_qualifications_for_qualification_type(string()) -> [proplist()] | no_return(). +-spec get_qualifications_for_qualification_type(string()) -> [proplist()]. get_qualifications_for_qualification_type(QualificationTypeId) -> get_qualifications_for_qualification_type(QualificationTypeId, default_config()). --spec get_qualifications_for_qualification_type(string(), proplist() | aws_config()) -> [proplist()] | no_return(). +-spec get_qualifications_for_qualification_type(string(), proplist() | aws_config()) -> [proplist()]. get_qualifications_for_qualification_type(QualificationTypeId, Config) when is_record(Config, aws_config) -> get_qualifications_for_qualification_type(QualificationTypeId, [], Config); get_qualifications_for_qualification_type(QualificationTypeId, Options) -> get_qualifications_for_qualification_type(QualificationTypeId, Options, default_config()). --spec get_qualifications_for_qualification_type(string(), proplist(), aws_config()) -> [proplist()] | no_return(). +-spec get_qualifications_for_qualification_type(string(), proplist(), aws_config()) -> [proplist()]. get_qualifications_for_qualification_type(QualificationTypeId, Options, Config) when is_list(QualificationTypeId), is_list(Options) -> Params = [ @@ -672,18 +672,18 @@ get_qualifications_for_qualification_type(QualificationTypeId, Options, Config) Item ) || Item <- xmerl_xpath:string("Qualification", Doc)]. --spec get_qualification_requests() -> proplist() | no_return(). +-spec get_qualification_requests() -> proplist(). get_qualification_requests() -> get_qualification_requests([]). --spec get_qualification_requests(proplist() | aws_config()) -> proplist() | no_return(). +-spec get_qualification_requests(proplist() | aws_config()) -> proplist(). get_qualification_requests(Config) when is_record(Config, aws_config) -> get_qualification_requests([], Config); get_qualification_requests(Options) -> get_qualification_requests(Options, default_config()). --spec get_qualification_requests(proplist(), aws_config()) -> proplist() | no_return(). +-spec get_qualification_requests(proplist(), aws_config()) -> proplist(). get_qualification_requests(Options, Config) when is_list(Options) -> Params = [ @@ -732,18 +732,18 @@ extract_qualification_request(Request) -> Request ). --spec get_requester_statistic(string(), one_day | seven_days | thirty_days | life_to_date) -> [{datetime(), float()}] | no_return(). +-spec get_requester_statistic(string(), one_day | seven_days | thirty_days | life_to_date) -> [{datetime(), float()}]. get_requester_statistic(Statistic, TimePeriod) -> get_requester_statistic(Statistic, TimePeriod, default_config()). --spec get_requester_statistic(string(), one_day | seven_days | thirty_days | life_to_date, pos_integer() | aws_config()) -> [{datetime(), float()}] | no_return(). +-spec get_requester_statistic(string(), one_day | seven_days | thirty_days | life_to_date, pos_integer() | aws_config()) -> [{datetime(), float()}]. get_requester_statistic(Statistic, TimePeriod, Config) when is_record(Config, aws_config) -> get_requester_statistic(Statistic, TimePeriod, 1, Config); get_requester_statistic(Statistic, TimePeriod, Count) -> get_requester_statistic(Statistic, TimePeriod, Count, default_config()). --spec get_requester_statistic(string(), one_day | seven_days | thirty_days | life_to_date, pos_integer(), aws_config()) -> [{datetime(), float()}] | no_return(). +-spec get_requester_statistic(string(), one_day | seven_days | thirty_days | life_to_date, pos_integer(), aws_config()) -> [{datetime(), float()}]. get_requester_statistic(Statistic, TimePeriod, Count, Config) when is_list(Statistic), TimePeriod =:= one_day orelse TimePeriod =:= seven_days orelse @@ -769,11 +769,11 @@ get_requester_statistic(Statistic, TimePeriod, Count, Config) end} || DP <- xmerl_xpath:string("DataPoint", Doc)]. --spec grant_bonus(string(), string(), #mturk_money{}, string()) -> ok | no_return(). +-spec grant_bonus(string(), string(), #mturk_money{}, string()) -> ok. grant_bonus(WorkerId, AssignmentId, BonusAmount, Reason) -> grant_bonus(WorkerId, AssignmentId, BonusAmount, Reason, default_config()). --spec grant_bonus(string(), string(), #mturk_money{}, string(), aws_config()) -> ok | no_return(). +-spec grant_bonus(string(), string(), #mturk_money{}, string(), aws_config()) -> ok. grant_bonus(WorkerId, AssignmentId, BonusAmount, Reason, Config) -> mturk_simple_request(Config, "GrantBonus", [ @@ -784,18 +784,18 @@ grant_bonus(WorkerId, AssignmentId, BonusAmount, Reason, Config) -> ] ). --spec grant_qualification(string()) -> ok | no_return(). +-spec grant_qualification(string()) -> ok. grant_qualification(QualificationRequestId) -> grant_qualification(QualificationRequestId, none). --spec grant_qualification(string(), integer() | none | aws_config()) -> ok | no_return(). +-spec grant_qualification(string(), integer() | none | aws_config()) -> ok. grant_qualification(QualificationRequestId, Config) when is_record(Config, aws_config) -> grant_qualification(QualificationRequestId, none, Config); grant_qualification(QualificationRequestId, Value) -> grant_qualification(QualificationRequestId, Value, default_config()). --spec grant_qualification(string(), integer() | none, aws_config()) -> ok | no_return(). +-spec grant_qualification(string(), integer() | none, aws_config()) -> ok. grant_qualification(QualificationRequestId, Value, Config) when is_list(QualificationRequestId), is_integer(Value) orelse Value =:= none -> @@ -1090,11 +1090,11 @@ extract_money(Money) -> encode_money(#mturk_money{amount=Amount, currency_code=CurrencyCode}) -> [{"Amount", Amount}, {"CurrencyCode", CurrencyCode}]. --spec notify_workers(string(), string(), [string()]) -> ok | no_return(). +-spec notify_workers(string(), string(), [string()]) -> ok. notify_workers(Subject, MessageText, WorkerIds) -> notify_workers(Subject, MessageText, WorkerIds, default_config()). --spec notify_workers(string(), string(), [string()], aws_config()) -> ok | no_return(). +-spec notify_workers(string(), string(), [string()], aws_config()) -> ok. notify_workers(Subject, MessageText, WorkerIds, Config) when is_list(Subject), is_list(MessageText), is_list(WorkerIds), length(WorkerIds) =< 100 -> @@ -1106,11 +1106,11 @@ notify_workers(Subject, MessageText, WorkerIds, Config) ] ). --spec register_hit_type(#mturk_hit{}) -> proplist() | no_return(). +-spec register_hit_type(#mturk_hit{}) -> proplist(). register_hit_type(HIT) -> register_hit_type(HIT, default_config()). --spec register_hit_type(#mturk_hit{}, aws_config()) -> proplist() | no_return(). +-spec register_hit_type(#mturk_hit{}, aws_config()) -> proplist(). register_hit_type(HIT, Config) -> Params = [ {"Title", HIT#mturk_hit.title}, @@ -1130,18 +1130,18 @@ register_hit_type(HIT, Config) -> Doc ). --spec reject_assignment(string()) -> ok | no_return(). +-spec reject_assignment(string()) -> ok. reject_assignment(AssignmentId) -> reject_assignment(AssignmentId, none). --spec reject_assignment(string(), string() | none | aws_config()) -> ok | no_return(). +-spec reject_assignment(string(), string() | none | aws_config()) -> ok. reject_assignment(AssignmentId, Config) when is_record(Config, aws_config) -> reject_assignment(AssignmentId, none, Config); reject_assignment(AssignmentId, Reason) -> reject_assignment(AssignmentId, Reason, default_config()). --spec reject_assignment(string(), string() | none, aws_config()) -> ok | no_return(). +-spec reject_assignment(string(), string() | none, aws_config()) -> ok. reject_assignment(AssignmentId, Reason, Config) when is_list(AssignmentId), is_list(Reason) orelse Reason =:= none -> @@ -1152,18 +1152,18 @@ reject_assignment(AssignmentId, Reason, Config) ] ). --spec reject_qualification_request(string()) -> ok | no_return(). +-spec reject_qualification_request(string()) -> ok. reject_qualification_request(QualificationRequestId) -> reject_qualification_request(QualificationRequestId, none). --spec reject_qualification_request(string(), string() | none | aws_config()) -> ok | no_return(). +-spec reject_qualification_request(string(), string() | none | aws_config()) -> ok. reject_qualification_request(QualificationRequestId, Config) when is_record(Config, aws_config) -> reject_qualification_request(QualificationRequestId, none, Config); reject_qualification_request(QualificationRequestId, Reason) -> reject_qualification_request(QualificationRequestId, Reason, default_config()). --spec reject_qualification_request(string(), string() | none, aws_config()) -> ok | no_return(). +-spec reject_qualification_request(string(), string() | none, aws_config()) -> ok. reject_qualification_request(QualificationRequestId, Reason, Config) when is_list(QualificationRequestId), is_list(Reason) orelse Reason =:= none -> @@ -1174,18 +1174,18 @@ reject_qualification_request(QualificationRequestId, Reason, Config) ] ). --spec revoke_qualification(string(), string()) -> ok | no_return(). +-spec revoke_qualification(string(), string()) -> ok. revoke_qualification(QualificationTypeId, WorkerId) -> revoke_qualification(QualificationTypeId, WorkerId, none). --spec revoke_qualification(string(), string(), string() | none | aws_config()) -> ok | no_return(). +-spec revoke_qualification(string(), string(), string() | none | aws_config()) -> ok. revoke_qualification(QualificationTypeId, WorkerId, Config) when is_record(Config, aws_config) -> revoke_qualification(QualificationTypeId, WorkerId, none, Config); revoke_qualification(QualificationTypeId, WorkerId, Reason) -> revoke_qualification(QualificationTypeId, WorkerId, Reason, default_config()). --spec revoke_qualification(string(), string(), string() | none, aws_config()) -> ok | no_return(). +-spec revoke_qualification(string(), string(), string() | none, aws_config()) -> ok. revoke_qualification(QualificationTypeId, WorkerId, Reason, Config) -> mturk_simple_request(Config, "RevokeQualification", [ @@ -1195,18 +1195,18 @@ revoke_qualification(QualificationTypeId, WorkerId, Reason, Config) -> ] ). --spec search_hits() -> proplist() | no_return(). +-spec search_hits() -> proplist(). search_hits() -> search_hits([]). --spec search_hits(proplist() | aws_config()) -> proplist() | no_return(). +-spec search_hits(proplist() | aws_config()) -> proplist(). search_hits(Config) when is_record(Config, aws_config) -> search_hits([], Config); search_hits(Options) -> search_hits(Options, default_config()). --spec search_hits(proplist(), aws_config()) -> proplist() | no_return(). +-spec search_hits(proplist(), aws_config()) -> proplist(). search_hits(Options, Config) when is_list(Options) -> Params = [ @@ -1241,18 +1241,18 @@ search_hits(Options, Config) Doc ). --spec search_qualification_types() -> proplist() | no_return(). +-spec search_qualification_types() -> proplist(). search_qualification_types() -> search_qualification_types([]). --spec search_qualification_types(proplist() | aws_config()) -> proplist() | no_return(). +-spec search_qualification_types(proplist() | aws_config()) -> proplist(). search_qualification_types(Config) when is_record(Config, aws_config) -> search_qualification_types([], Config); search_qualification_types(Options) -> search_qualification_types(Options, default_config()). --spec search_qualification_types(proplist(), aws_config()) -> proplist() | no_return(). +-spec search_qualification_types(proplist(), aws_config()) -> proplist(). search_qualification_types(Options, Config) -> Params = [ {"Query", proplists:get_value(search_query, Options)}, @@ -1285,11 +1285,11 @@ search_qualification_types(Options, Config) -> Doc ). --spec send_test_event_notification(proplist(), mturk_event_type()) -> ok | no_return(). -send_test_event_notification(Notificaiton, TestEventType) -> - send_test_event_notification(Notificaiton, TestEventType, default_config()). +-spec send_test_event_notification(proplist(), mturk_event_type()) -> ok. +send_test_event_notification(Notification, TestEventType) -> + send_test_event_notification(Notification, TestEventType, default_config()). --spec send_test_event_notification(proplist(), mturk_event_type(), aws_config()) -> ok | no_return(). +-spec send_test_event_notification(proplist(), mturk_event_type(), aws_config()) -> ok. send_test_event_notification(Notification, TestEventType, Config) -> mturk_simple_request(Config, "SendTestEventNotification", [ @@ -1301,18 +1301,18 @@ send_test_event_notification(Notification, TestEventType, Config) -> ] ). --spec set_hit_as_reviewing(string()) -> ok | no_return(). +-spec set_hit_as_reviewing(string()) -> ok. set_hit_as_reviewing(HITId) -> set_hit_as_reviewing(HITId, false). --spec set_hit_as_reviewing(string(), boolean() | aws_config()) -> ok | no_return(). +-spec set_hit_as_reviewing(string(), boolean() | aws_config()) -> ok. set_hit_as_reviewing(HITId, Config) when is_record(Config, aws_config) -> set_hit_as_reviewing(HITId, false, Config); set_hit_as_reviewing(HITId, Revert) -> set_hit_as_reviewing(HITId, Revert, default_config()). --spec set_hit_as_reviewing(string(), boolean(), aws_config()) -> ok | no_return(). +-spec set_hit_as_reviewing(string(), boolean(), aws_config()) -> ok. set_hit_as_reviewing(HITId, Revert, Config) -> mturk_simple_request(Config, "SetHITAsReviewing", [ @@ -1321,18 +1321,18 @@ set_hit_as_reviewing(HITId, Revert, Config) -> ] ). --spec set_hit_type_notification(string(), proplist()) -> ok | no_return(). +-spec set_hit_type_notification(string(), proplist()) -> ok. set_hit_type_notification(HITTypeId, Notification) -> set_hit_type_notification(HITTypeId, Notification, undefined). --spec set_hit_type_notification(string(), proplist(), boolean() | undefined | aws_config()) -> ok | no_return(). +-spec set_hit_type_notification(string(), proplist(), boolean() | undefined | aws_config()) -> ok. set_hit_type_notification(HITTypeId, Notification, Config) when is_record(Config, aws_config) -> set_hit_type_notification(HITTypeId, Notification, undefined, Config); set_hit_type_notification(HITTypeId, Notification, Active) -> set_hit_type_notification(HITTypeId, Notification, Active, default_config()). --spec set_hit_type_notification(string(), proplist(), boolean() | undefined, aws_config()) -> ok | no_return(). +-spec set_hit_type_notification(string(), proplist(), boolean() | undefined, aws_config()) -> ok. set_hit_type_notification(HITTypeId, Notification, Active, Config) when is_list(HITTypeId), is_list(Notification), is_boolean(Active) orelse Active =:= undefined -> @@ -1358,29 +1358,29 @@ encode_transport(email) -> "Email"; encode_transport(soap) -> "SOAP"; encode_transport(rest) -> "REST". --spec unblock_worker(string()) -> ok | no_return(). +-spec unblock_worker(string()) -> ok. unblock_worker(WorkerId) -> unblock_worker(WorkerId, none). --spec unblock_worker(string(), string() | none | aws_config()) -> ok | no_return(). +-spec unblock_worker(string(), string() | none | aws_config()) -> ok. unblock_worker(WorkerId, Config) when is_record(Config, aws_config) -> unblock_worker(WorkerId, none, Config); unblock_worker(WorkerId, Reason) -> unblock_worker(WorkerId, Reason, default_config()). --spec unblock_worker(string(), string() | none, aws_config()) -> ok | no_return(). +-spec unblock_worker(string(), string() | none, aws_config()) -> ok. unblock_worker(WorkerId, Reason, Config) when is_list(WorkerId), is_list(Reason) orelse Reason =:= none -> mturk_simple_request(Config, "UnblockWorker", [{"WorkerId", WorkerId}, {"Reason", Reason}]). --spec update_qualification_score(string(), string(), integer()) -> ok | no_return(). +-spec update_qualification_score(string(), string(), integer()) -> ok. update_qualification_score(QualificationTypeId, SubjectId, IntegerValue) -> update_qualification_score(QualificationTypeId, SubjectId, IntegerValue, default_config()). --spec update_qualification_score(string(), string(), integer(), aws_config()) -> ok | no_return(). +-spec update_qualification_score(string(), string(), integer(), aws_config()) -> ok. update_qualification_score(QualificationTypeId, SubjectId, IntegerValue, Config) when is_list(SubjectId), is_list(QualificationTypeId), is_integer(IntegerValue) -> @@ -1392,11 +1392,11 @@ update_qualification_score(QualificationTypeId, SubjectId, IntegerValue, Config) ] ). --spec update_qualification_type(#mturk_qualification_type{}) -> #mturk_qualification_type{} | no_return(). +-spec update_qualification_type(#mturk_qualification_type{}) -> #mturk_qualification_type{}. update_qualification_type(QType) -> update_qualification_type(QType, default_config()). --spec update_qualification_type(#mturk_qualification_type{}, aws_config()) -> #mturk_qualification_type{} | no_return(). +-spec update_qualification_type(#mturk_qualification_type{}, aws_config()) -> #mturk_qualification_type{}. update_qualification_type(QType, Config) -> Doc = mturk_xml_request(Config, "UpdateQualificationType", [ diff --git a/src/erlcloud_retry.erl b/src/erlcloud_retry.erl index 97511006f..7f0b1066d 100644 --- a/src/erlcloud_retry.erl +++ b/src/erlcloud_retry.erl @@ -5,7 +5,7 @@ %% %% Implementation of retry logic for AWS requests %% -%% Currently only used for S3, but will be extended to other services in the furture. +%% Currently only used for S3, but will be extended to other services in the future. %% %% The pluggable retry function provides a way to customize the retry behavior, as well %% as log and customize errors that are generated by erlcloud. @@ -20,12 +20,16 @@ %% Helpers -export([backoff/1, no_retry/1, - default_retry/1 + default_retry/1, + + only_http_errors/1, + lambda_fun_errors/1 ]). --export_type([should_retry/0, retry_fun/0]). +-export_type([should_retry/0, retry_fun/0, response_type_fun/0]). -type should_retry() :: {retry | error, #aws_request{}}. -type retry_fun() :: fun((#aws_request{}) -> should_retry()). +-type response_type_fun() :: fun((#aws_request{}) -> ok | error). %% Internal impl api -export([request/3]). @@ -52,6 +56,27 @@ request(Config, #aws_request{attempt = 0} = Request, ResultFun) -> MaxAttempts = Config#aws_config.retry_num, request_and_retry(Config, ResultFun, {retry, Request}, MaxAttempts). +-spec only_http_errors(#aws_request{}) -> ok | error. +only_http_errors(#aws_request{response_status=Status}) + when Status >= 200, Status < 300 + -> + ok; +only_http_errors(_) -> + error. + +-spec lambda_fun_errors(#aws_request{}) -> ok | error. +lambda_fun_errors(#aws_request{response_status=Status, response_headers=ResponseHeaders}) + when Status >= 200, Status < 300 + -> + case lists:keymember("x-amz-function-error", 1, ResponseHeaders) of + true -> + error; + false -> + ok + end; +lambda_fun_errors(_) -> + error. + request_and_retry(_, _, {_, Request}, 0) -> Request; request_and_retry(_, _, {error, Request}, _) -> @@ -66,18 +91,19 @@ request_and_retry(Config, ResultFun, {retry, Request}, MaxAttempts) -> } = Request, Request2 = Request#aws_request{attempt = Attempt + 1}, RetryFun = Config#aws_config.retry, + ResponseTypeFun = Config#aws_config.retry_response_type, Rsp = erlcloud_httpc:request(URI, Method, Headers, Body, erlcloud_aws:get_timeout(Config), Config), case Rsp of {ok, {{Status, StatusLine}, ResponseHeaders, ResponseBody}} -> Request3 = Request2#aws_request{ - response_type = if Status >= 200, Status < 300 -> ok; true -> error end, error_type = aws, response_status = Status, response_status_line = StatusLine, response_headers = ResponseHeaders, response_body = ResponseBody}, - Request4 = ResultFun(Request3), + ResponseType = ResponseTypeFun(Request3), + Request4 = ResultFun(Request3#aws_request{response_type=ResponseType}), case Request4#aws_request.response_type of ok -> Request4; diff --git a/src/erlcloud_route53.erl b/src/erlcloud_route53.erl index f540bc884..51cf8ade9 100644 --- a/src/erlcloud_route53.erl +++ b/src/erlcloud_route53.erl @@ -77,7 +77,7 @@ configure(AccessKeyID, SecretAccessKey, Host) -> %% @end %% -------------------------------------------------------------------- -spec describe_zone(ZoneId :: string()) -> - {ok, list(aws_route53_zone())} | + {ok, aws_route53_zone()} | {error, term()}. describe_zone(ZoneId) -> describe_zone(ZoneId, erlcloud_aws:default_config()). @@ -88,7 +88,7 @@ describe_zone(ZoneId) -> %% -------------------------------------------------------------------- -spec describe_zone(ZoneId :: string(), AwsConfig :: aws_config()) -> - {ok, list(aws_route53_zone())} | + {ok, aws_route53_zone()} | {error, term()}. describe_zone(ZoneId, AwsConfig) -> describe_zone(ZoneId, [], AwsConfig). @@ -100,7 +100,7 @@ describe_zone(ZoneId, AwsConfig) -> -spec describe_zone(ZoneId :: string(), Options :: list({string(), string()}), AwsConfig :: aws_config()) -> - {ok, list(aws_route53_zone())} | + {ok, aws_route53_zone()} | {error, term()}. describe_zone(ZoneId, Options, Config) when is_list(Options), is_record(Config, aws_config) -> @@ -132,7 +132,7 @@ describe_zones(AwsConfig) -> %% @doc Describes all zones using provided config + AWS options %% @end %% -------------------------------------------------------------------- --spec describe_zones(Options :: list({string(), string()}), +-spec describe_zones(Options :: list({string(), string() | integer()}), AwsConfig :: aws_config()) -> {ok, list(aws_route53_zone())} | {ok, list(aws_route53_zone()), string()} | @@ -498,7 +498,7 @@ key_replace_or_add(Key, Value, List) -> end. -spec route53_query(get | post, aws_config(), string(), string(), - list({string(), string()}), string()) -> + list({string(), string() | integer()}), string()) -> {ok, term()} | {error, term()}. route53_query(Method, Config, Action, Path, Params, ApiVersion) -> QParams = [{"Action", Action}, {"Version", ApiVersion} | Params], diff --git a/src/erlcloud_s3.erl b/src/erlcloud_s3.erl old mode 100755 new mode 100644 index bad2fa41a..41895e976 --- a/src/erlcloud_s3.erl +++ b/src/erlcloud_s3.erl @@ -23,6 +23,7 @@ delete_object/2, delete_object/3, delete_object_version/3, delete_object_version/4, head_object/2, head_object/3, head_object/4, + head_bucket/1, head_bucket/2, head_bucket/3, get_object/2, get_object/3, get_object/4, get_object_acl/2, get_object_acl/3, get_object_acl/4, get_object_torrent/2, get_object_torrent/3, @@ -35,7 +36,7 @@ upload_part/5, upload_part/7, complete_multipart/4, complete_multipart/6, abort_multipart/3, abort_multipart/6, - list_multipart_uploads/1, list_multipart_uploads/2, + list_multipart_uploads/1, list_multipart_uploads/2, list_multipart_uploads/4, get_object_url/2, get_object_url/3, get_bucket_and_key/1, list_bucket_inventory/1, list_bucket_inventory/2, list_bucket_inventory/3, @@ -44,7 +45,15 @@ delete_bucket_inventory/2, delete_bucket_inventory/3, put_bucket_encryption/2, put_bucket_encryption/3, put_bucket_encryption/4, get_bucket_encryption/1, get_bucket_encryption/2, - delete_bucket_encryption/1, delete_bucket_encryption/2 + delete_bucket_encryption/1, delete_bucket_encryption/2, + put_object_tagging/3, put_object_tagging/4, + delete_object_tagging/2, delete_object_tagging/3, + get_object_tagging/2, get_object_tagging/3, + put_bucket_tagging/2, put_bucket_tagging/3, + delete_bucket_tagging/1, delete_bucket_tagging/2, + get_bucket_tagging/1, get_bucket_tagging/2, + get_public_access_block/1, get_public_access_block/2, + put_public_access_block/2, put_public_access_block/3 ]). -ifdef(TEST). @@ -120,6 +129,7 @@ configure(AccessKeyID, SecretAccessKey, Host, Port, Scheme) -> -type s3_bucket_attribute_name() :: acl | location | logging + | mfa_delete | request_payment | versioning | notification. @@ -137,26 +147,41 @@ configure(AccessKeyID, SecretAccessKey, Host, Port, Scheme) -> | 'us-east-1' | 'us-east-2' | 'us-west-1' + | 'us-west-2' + | 'ca-central-1' | 'eu-west-1' | 'eu-west-2' + | 'eu-west-3' + | 'eu-north-1' | 'eu-central-1' | 'ap-south-1' | 'ap-southeast-1' | 'ap-southeast-2' | 'ap-northeast-1' | 'ap-northeast-2' + | 'ap-northeast-3' + | 'ap-east-1' + | 'me-south-1' | 'sa-east-1'. +-type public_access_block_attrib() :: block_public_policy + | ignore_public_acls + | block_public_policy + | restrict_public_buckets. + +-type public_access_block_param() :: {public_access_block_attrib(), boolean()}. + +-type public_access_block_configuration() :: [public_access_block_param()]. -define(XMLNS_S3, "http://s3.amazonaws.com/doc/2006-03-01/"). -define(XMLNS_SCHEMA_INSTANCE, "http://www.w3.org/2001/XMLSchema-instance"). --spec copy_object(string(), string(), string(), string()) -> proplist() | no_return(). +-spec copy_object(string(), string(), string(), string()) -> proplist(). copy_object(DestBucketName, DestKeyName, SrcBucketName, SrcKeyName) -> copy_object(DestBucketName, DestKeyName, SrcBucketName, SrcKeyName, []). --spec copy_object(string(), string(), string(), string(), proplist() | aws_config()) -> proplist() | no_return(). +-spec copy_object(string(), string(), string(), string(), proplist() | aws_config()) -> proplist(). copy_object(DestBucketName, DestKeyName, SrcBucketName, SrcKeyName, Config) when is_record(Config, aws_config) -> @@ -166,7 +191,7 @@ copy_object(DestBucketName, DestKeyName, SrcBucketName, SrcKeyName, Options) -> copy_object(DestBucketName, DestKeyName, SrcBucketName, SrcKeyName, Options, default_config()). --spec copy_object(string(), string(), string(), string(), proplist(), aws_config()) -> proplist() | no_return(). +-spec copy_object(string(), string(), string(), string(), proplist(), aws_config()) -> proplist(). copy_object(DestBucketName, DestKeyName, SrcBucketName, SrcKeyName, Options, Config) -> SrcVersion = case proplists:get_value(version_id, Options) of undefined -> ""; @@ -185,12 +210,12 @@ copy_object(DestBucketName, DestKeyName, SrcBucketName, SrcKeyName, Options, Con [{copy_source_version_id, proplists:get_value("x-amz-copy-source-version-id", Headers, "false")}, {version_id, proplists:get_value("x-amz-version-id", Headers, "null")}]. --spec create_bucket(string()) -> ok | no_return(). +-spec create_bucket(string()) -> ok. create_bucket(BucketName) -> create_bucket(BucketName, private). --spec create_bucket(string(), s3_bucket_acl() | aws_config()) -> ok | no_return(). +-spec create_bucket(string(), s3_bucket_acl() | aws_config()) -> ok. create_bucket(BucketName, Config) when is_record(Config, aws_config) -> @@ -199,7 +224,7 @@ create_bucket(BucketName, Config) create_bucket(BucketName, ACL) -> create_bucket(BucketName, ACL, none). --spec create_bucket(string(), s3_bucket_acl(), s3_location_constraint() | aws_config()) -> ok | no_return(). +-spec create_bucket(string(), s3_bucket_acl(), s3_location_constraint() | aws_config()) -> ok. create_bucket(BucketName, ACL, Config) when is_record(Config, aws_config) -> @@ -208,7 +233,7 @@ create_bucket(BucketName, ACL, Config) create_bucket(BucketName, ACL, LocationConstraint) -> create_bucket(BucketName, ACL, LocationConstraint, default_config()). --spec create_bucket(string(), s3_bucket_acl(), s3_location_constraint(), aws_config()) -> ok | no_return(). +-spec create_bucket(string(), s3_bucket_acl(), s3_location_constraint(), aws_config()) -> ok. create_bucket(BucketName, ACL, LocationConstraint, Config) when is_list(BucketName), is_atom(ACL), is_atom(LocationConstraint) -> @@ -231,14 +256,20 @@ encode_location_constraint('us-east-1') -> undefined; encode_location_constraint('us-east-2') -> "us-east-2"; encode_location_constraint('us-west-1') -> "us-west-1"; encode_location_constraint('us-west-2') -> "us-west-2"; +encode_location_constraint('ca-central-1') -> "ca-central-1"; encode_location_constraint('eu-west-1') -> "eu-west-1"; encode_location_constraint('eu-west-2') -> "eu-west-2"; +encode_location_constraint('eu-west-3') -> "eu-west-3"; +encode_location_constraint('eu-north-1') -> "eu-north-1"; encode_location_constraint('eu-central-1') -> "eu-central-1"; encode_location_constraint('ap-south-1') -> "ap-south-1"; encode_location_constraint('ap-southeast-1') -> "ap-southeast-1"; encode_location_constraint('ap-southeast-2') -> "ap-southeast-2"; encode_location_constraint('ap-northeast-1') -> "ap-northeast-1"; encode_location_constraint('ap-northeast-2') -> "ap-northeast-2"; +encode_location_constraint('ap-northeast-3') -> "ap-northeast-3"; +encode_location_constraint('ap-east-1') -> "ap-east-1"; +encode_location_constraint('me-south-1') -> "me-south-1"; encode_location_constraint('sa-east-1') -> "sa-east-1"; encode_location_constraint(_) -> undefined. @@ -250,12 +281,12 @@ encode_acl(authenticated_read) -> "authenticated-read"; encode_acl(bucket_owner_read) -> "bucket-owner-read"; encode_acl(bucket_owner_full_control) -> "bucket-owner-full-control". --spec delete_bucket(string()) -> ok | no_return(). +-spec delete_bucket(string()) -> ok. delete_bucket(BucketName) -> delete_bucket(BucketName, default_config()). --spec delete_bucket(string(), aws_config()) -> ok | no_return(). +-spec delete_bucket(string(), aws_config()) -> ok. delete_bucket(BucketName, Config) when is_list(BucketName) -> @@ -279,11 +310,11 @@ check_bucket_access(BucketName, Config) end. --spec delete_objects_batch(string(), list()) -> proplist() | no_return(). +-spec delete_objects_batch(string(), list()) -> proplist(). delete_objects_batch(BucketName, KeyList) -> delete_objects_batch(BucketName, KeyList, default_config()). --spec delete_objects_batch(string(), list(), aws_config()) -> proplist() | no_return(). +-spec delete_objects_batch(string(), list(), aws_config()) -> proplist(). delete_objects_batch(BucketName, KeyList, Config) when is_list(BucketName), is_list(KeyList) -> Data = lists:map(fun(Item) -> @@ -315,47 +346,63 @@ extract_delete_objects_batch_err_contents(Nodes) -> to_flat_format([{key,Key},{code,Code},{message,Message}]) -> {Key,Code,Message}. -% returns paths list from AWS S3 root directory, used as input to delete_objects_batch -% example : -% 25> rp(erlcloud_s3:explore_dirstructure("xmppfiledev", ["sailfish/deleteme"], [])). -% ["sailfish/deleteme/deep/deep1/deep4/ZZZ_1.txt", -% "sailfish/deleteme/deep/deep1/deep4/ZZZ_0.txt", -% "sailfish/deleteme/deep/deep1/ZZZ_0.txt", -% "sailfish/deleteme/deep/ZZZ_0.txt"] -% ok -% --spec explore_dirstructure(string(), list(), list()) -> list() | no_return(). +%% returns paths list from AWS S3 root directory, used as input to +%% delete_objects_batch +%% example : +%% 25> rp(erlcloud_s3:explore_dirstructure("xmppfiledev", +%% ["sailfish/deleteme"], [])). +%% ["sailfish/deleteme/deep/deep1/deep4/ZZZ_1.txt", +%% "sailfish/deleteme/deep/deep1/deep4/ZZZ_0.txt", +%% "sailfish/deleteme/deep/deep1/ZZZ_0.txt", +%% "sailfish/deleteme/deep/ZZZ_0.txt"] +%% ok +%% +-spec explore_dirstructure(string(), list(), list()) -> list(). explore_dirstructure(Bucketname, Branches, Accum) -> explore_dirstructure(Bucketname, Branches, Accum, default_config()). --spec explore_dirstructure(string(), list(), list(), aws_config()) -> list() | no_return(). -explore_dirstructure(_, [], Result, _Config) -> - lists:append(Result); -explore_dirstructure(Bucketname, [Branch|Tail], Accum, Config) +-spec explore_dirstructure(string(), list(), list(), aws_config()) -> + list(). +explore_dirstructure(Bucketname, [Branch|Tail], Accum, Config) -> + explore_dirstructure(Bucketname, [Branch|Tail], Accum, Config, []). + + +explore_dirstructure(_, [], Result, _Config, _Marker) -> + lists:append(Result); +explore_dirstructure(Bucketname, [Branch|Tail], Accum, Config, Marker) when is_record(Config, aws_config) -> ProcessContent = fun(Data)-> Content = proplists:get_value(contents, Data), - lists:foldl(fun(I,Acc)-> R = proplists:get_value(key, I), [R|Acc] end, [], Content) + lists:foldl(fun(I,Acc)-> R = proplists:get_value(key, I), + [R|Acc] end, [], Content) end, - Data = list_objects(Bucketname, [{prefix, Branch}, {delimiter, "/"}], Config), - case proplists:get_value(common_prefixes, Data) of - [] -> % it has reached end of the branch - Files = ProcessContent(Data), - explore_dirstructure(Bucketname, Tail, [Files|Accum], Config); - Sub -> - Files = ProcessContent(Data), - List = lists:foldl(fun(I,Acc)-> R = proplists:get_value(prefix, I), [R|Acc] end, [], Sub), - Result = explore_dirstructure(Bucketname, List, [], Config), - explore_dirstructure(Bucketname, Tail, [Result, Files|Accum], Config) - end. + Data = list_objects(Bucketname, [{prefix, Branch}, + {delimiter, "/"}, + {marker, Marker}], Config), + Files = ProcessContent(Data), + Sub = proplists:get_value(common_prefixes, Data), + SubDirs = lists:foldl(fun(I,Acc)-> R = proplists:get_value(prefix, I), + [R|Acc] end, [], Sub), + SubFiles = explore_dirstructure(Bucketname, SubDirs, [], Config, []), + TruncFiles = + case proplists:get_value(is_truncated, Data) of + false -> + []; + true -> + NextMarker = proplists:get_value(next_marker, Data), + explore_dirstructure(Bucketname, [Branch], [], Config, + NextMarker) + end, + explore_dirstructure(Bucketname, Tail, + [SubFiles, TruncFiles, Files|Accum], Config, []). --spec delete_object(string(), string()) -> proplist() | no_return(). +-spec delete_object(string(), string()) -> proplist(). delete_object(BucketName, Key) -> delete_object(BucketName, Key, default_config()). --spec delete_object(string(), string(), aws_config()) -> proplist() | no_return(). +-spec delete_object(string(), string(), aws_config()) -> proplist(). delete_object(BucketName, Key, Config) when is_list(BucketName), is_list(Key) -> @@ -365,12 +412,12 @@ delete_object(BucketName, Key, Config) [{delete_marker, list_to_existing_atom(Marker)}, {version_id, Id}]. --spec delete_object_version(string(), string(), string()) -> proplist() | no_return(). +-spec delete_object_version(string(), string(), string()) -> proplist(). delete_object_version(BucketName, Key, Version) -> delete_object_version(BucketName, Key, Version, default_config()). --spec delete_object_version(string(), string(), string(), aws_config()) -> proplist() | no_return(). +-spec delete_object_version(string(), string(), string(), aws_config()) -> proplist(). delete_object_version(BucketName, Key, Version, Config) when is_list(BucketName), @@ -383,12 +430,12 @@ delete_object_version(BucketName, Key, Version, Config) [{delete_marker, list_to_existing_atom(Marker)}, {version_id, Id}]. --spec list_buckets() -> proplist() | no_return(). +-spec list_buckets() -> proplist(). list_buckets() -> list_buckets(default_config()). --spec list_buckets(aws_config()) -> proplist() | no_return(). +-spec list_buckets(aws_config()) -> proplist(). list_buckets(Config) -> Doc = s3_xml_request(Config, get, "", "/", "", [], <<>>, []), @@ -406,7 +453,7 @@ get_bucket_policy(BucketName) -> % % Example request: erlcloud_s3:get_bucket_policy("bucket1234", Config). -% Example success repsonse: {ok, "{\"Version\":\"2012-10-17\",\"Statement\": ..........} +% Example success response: {ok, "{\"Version\":\"2012-10-17\",\"Statement\": ..........} % Example error response: {error,{http_error,404,"Not Found", % "\n % @@ -426,20 +473,20 @@ get_bucket_policy(BucketName, Config) Error end. --spec put_bucket_policy(string(), binary()) -> ok | no_return(). +-spec put_bucket_policy(string(), binary()) -> ok. put_bucket_policy(BucketName, Policy) -> put_bucket_policy(BucketName, Policy, default_config()). --spec put_bucket_policy(string(), binary(), aws_config()) -> ok | no_return(). +-spec put_bucket_policy(string(), binary(), aws_config()) -> ok. put_bucket_policy(BucketName, Policy, Config) when is_list(BucketName), is_binary(Policy), is_record(Config, aws_config) -> s3_simple_request(Config, put, BucketName, "/", "policy", [], Policy, []). --spec get_bucket_lifecycle(BucketName::string()) -> ok | {error, Reason::term()}. +-spec get_bucket_lifecycle(BucketName::string()) -> {ok, list(proplist())} | {error, Reason::term()}. get_bucket_lifecycle(BucketName) -> get_bucket_lifecycle(BucketName, default_config()). --spec get_bucket_lifecycle(BucketName::string(), Config::aws_config()) -> {ok, Policy::string()} | {error, Reason::term()}. +-spec get_bucket_lifecycle(BucketName::string(), Config::aws_config()) -> {ok, list(proplist())} | {error, Reason::term()}. get_bucket_lifecycle(BucketName, Config) when is_record(Config, aws_config) -> case s3_request2(Config, get, BucketName, "/", "lifecycle", [], <<>>, []) of @@ -449,11 +496,11 @@ get_bucket_lifecycle(BucketName, Config) Error end. --spec put_bucket_lifecycle(string(), binary()) -> ok | {error, Reason::term()} | no_return(). +-spec put_bucket_lifecycle(string(), list() | binary()) -> ok | {error, Reason::term()}. put_bucket_lifecycle(BucketName, Policy) -> put_bucket_lifecycle(BucketName, Policy, default_config()). --spec put_bucket_lifecycle(string(), list() | binary(), aws_config()) -> ok | {error, Reason::term()} | no_return(). +-spec put_bucket_lifecycle(string(), list() | binary(), aws_config()) -> ok | {error, Reason::term()}. put_bucket_lifecycle(BucketName, Policy, Config) when is_list(BucketName), is_list(Policy), is_record(Config, aws_config) -> XmlPolicy = encode_lifecycle(Policy), @@ -464,21 +511,21 @@ put_bucket_lifecycle(BucketName, XmlPolicy, Config) s3_simple_request(Config, put, BucketName, "/", "lifecycle", [], XmlPolicy, [{"Content-MD5", Md5}]). --spec delete_bucket_lifecycle(string()) -> ok | {error, Reason::term()} | no_return(). +-spec delete_bucket_lifecycle(string()) -> ok | {error, Reason::term()}. delete_bucket_lifecycle(BucketName) -> delete_bucket_lifecycle(BucketName, default_config()). -spec delete_bucket_lifecycle(string(), #aws_config{}) - -> ok | {error, Reason::term()} | no_return(). + -> ok | {error, Reason::term()}. delete_bucket_lifecycle(BucketName, AwsConfig) -> s3_simple_request(AwsConfig, delete, BucketName, "/", "lifecycle", [], <<>>, []). --spec list_objects(string()) -> proplist() | no_return(). +-spec list_objects(string()) -> proplist(). list_objects(BucketName) -> list_objects(BucketName, []). --spec list_objects(string(), proplist() | aws_config()) -> proplist() | no_return(). +-spec list_objects(string(), proplist() | aws_config()) -> proplist(). list_objects(BucketName, Config) when is_record(Config, aws_config) -> @@ -487,7 +534,7 @@ list_objects(BucketName, Config) list_objects(BucketName, Options) -> list_objects(BucketName, Options, default_config()). --spec list_objects(string(), proplist(), aws_config()) -> proplist() | no_return(). +-spec list_objects(string(), proplist(), aws_config()) -> proplist(). list_objects(BucketName, Options, Config) when is_list(BucketName), @@ -495,6 +542,7 @@ list_objects(BucketName, Options, Config) Params = [{"delimiter", proplists:get_value(delimiter, Options)}, {"marker", proplists:get_value(marker, Options)}, {"max-keys", proplists:get_value(max_keys, Options)}, + {"encoding-type", proplists:get_value(encoding_type, Options)}, {"prefix", proplists:get_value(prefix, Options)}], Doc = s3_xml_request(Config, get, BucketName, "/", "", Params, <<>>, []), Attributes = [{name, "Name", text}, @@ -530,12 +578,12 @@ extract_user([Node]) -> ], erlcloud_xml:decode(Attributes, Node). --spec get_bucket_attribute(string(), s3_bucket_attribute_name()) -> term() | no_return(). +-spec get_bucket_attribute(string(), s3_bucket_attribute_name()) -> term(). get_bucket_attribute(BucketName, AttributeName) -> get_bucket_attribute(BucketName, AttributeName, default_config()). --spec get_bucket_attribute(string(), s3_bucket_attribute_name(), aws_config()) -> term() | no_return(). +-spec get_bucket_attribute(string(), s3_bucket_attribute_name(), aws_config()) -> term(). get_bucket_attribute(BucketName, AttributeName, Config) when is_list(BucketName), is_atom(AttributeName) -> @@ -543,6 +591,7 @@ get_bucket_attribute(BucketName, AttributeName, Config) acl -> "acl"; location -> "location"; logging -> "logging"; + mfa_delete -> "versioning"; %% https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketVersioning.html#API_GetBucketVersioning_ResponseElements request_payment -> "requestPayment"; versioning -> "versioning"; notification -> "notification" @@ -572,6 +621,11 @@ get_bucket_attribute(BucketName, AttributeName, Config) {target_trants, "TargetGrants/Grant", fun extract_acl/1}], [{enabled, true}|erlcloud_xml:decode(Attributes, LoggingEnabled)] end; + mfa_delete -> + case erlcloud_xml:get_text("/VersioningConfiguration/MfaDelete", Doc) of + "Enabled" -> enabled; + _ -> disabled + end; request_payment -> case erlcloud_xml:get_text("/RequestPaymentConfiguration/Payer", Doc) of "Requester" -> requester; @@ -744,12 +798,12 @@ decode_permission("WRITE_ACP") -> write_acp; decode_permission("READ") -> read; decode_permission("READ_ACP") -> read_acp. --spec head_object(string(), string()) -> proplist() | no_return(). +-spec head_object(string(), string()) -> proplist(). head_object(BucketName, Key) -> head_object(BucketName, Key, []). --spec head_object(string(), string(), proplist() | aws_config()) -> proplist() | no_return(). +-spec head_object(string(), string(), proplist() | aws_config()) -> proplist(). head_object(BucketName, Key, Config) when is_record(Config, aws_config) -> @@ -757,17 +811,35 @@ head_object(BucketName, Key, Config) head_object(BucketName, Key, Options) -> head_object(BucketName, Key, Options, default_config()). --spec head_object(string(), string(), proplist(), aws_config()) -> proplist() | no_return(). +-spec head_object(string(), string(), proplist(), aws_config()) -> proplist(). head_object(BucketName, Key, Options, Config) -> get_or_head(head, BucketName, Key, Options, Config). --spec get_object(string(), string()) -> proplist() | no_return(). +-spec head_bucket(string()) -> proplist(). + +head_bucket(BucketName) -> + head_bucket(BucketName, []). + +-spec head_bucket(string(), proplist() | aws_config()) -> proplist(). + +head_bucket(BucketName, Config) + when is_record(Config, aws_config) -> + head_bucket(BucketName, [], Config); +head_bucket(BucketName, Options) -> + head_bucket(BucketName, Options, default_config()). + +-spec head_bucket(string(), proplist(), aws_config()) -> proplist(). + +head_bucket(BucketName, Options, Config) -> + get_or_head(head, BucketName, Options, Config). + +-spec get_object(string(), string()) -> proplist(). get_object(BucketName, Key) -> get_object(BucketName, Key, []). --spec get_object(string(), string(), proplist() | aws_config()) -> proplist() | no_return(). +-spec get_object(string(), string(), proplist() | aws_config()) -> proplist(). get_object(BucketName, Key, Config) when is_record(Config, aws_config) -> @@ -776,11 +848,31 @@ get_object(BucketName, Key, Config) get_object(BucketName, Key, Options) -> get_object(BucketName, Key, Options, default_config()). --spec get_object(string(), string(), proplist(), aws_config()) -> proplist() | no_return(). +-spec get_object(string(), string(), proplist(), aws_config()) -> proplist(). get_object(BucketName, Key, Options, Config) -> get_or_head(get, BucketName, Key, Options, Config). +get_or_head(Method, BucketName, Options, Config) -> + RequestHeaders = [{"Range", proplists:get_value(range, Options)}, + {"If-Modified-Since", proplists:get_value(if_modified_since, Options)}, + {"If-Unmodified-Since", proplists:get_value(if_unmodified_since, Options)}, + {"If-Match", proplists:get_value(if_match, Options)}, + {"If-None-Match", proplists:get_value(if_none_match, Options)}, + {"x-amz-server-side-encryption-customer-algorithm", proplists:get_value(server_side_encryption_customer_algorithm, Options)}, + {"x-amz-server-side-encryption-customer-key", proplists:get_value(server_side_encryption_customer_key, Options)}, + {"x-amz-server-side-encryption-customer-key-md5", proplists:get_value(server_side_encryption_customer_key_md5, Options)}], + Subresource = case proplists:get_value(version_id, Options) of + undefined -> ""; + Version -> ["versionId=", Version] + end, + {Headers, _Body} = s3_request(Config, Method, BucketName, [$/], Subresource, [], <<>>, RequestHeaders), + [{content_length, proplists:get_value("content-length", Headers)}, + {content_type, proplists:get_value("content-type", Headers)}, + {access_point_alias, proplists:get_value("x-amz-access-point-alias", Headers)}, + {bucket_region, proplists:get_value("x-amz-bucket-region", Headers)}| + extract_metadata(Headers)]. + get_or_head(Method, BucketName, Key, Options, Config) -> RequestHeaders = [{"Range", proplists:get_value(range, Options)}, {"If-Modified-Since", proplists:get_value(if_modified_since, Options)}, @@ -802,15 +894,16 @@ get_or_head(Method, BucketName, Key, Options, Config) -> {content_encoding, proplists:get_value("content-encoding", Headers)}, {delete_marker, list_to_existing_atom(proplists:get_value("x-amz-delete-marker", Headers, "false"))}, {version_id, proplists:get_value("x-amz-version-id", Headers, "null")}, + {tag_count, proplists:get_value("x-amz-tagging-count", Headers, "null")}, {content, Body}| extract_metadata(Headers)]. --spec get_object_acl(string(), string()) -> proplist() | no_return(). +-spec get_object_acl(string(), string()) -> proplist(). get_object_acl(BucketName, Key) -> get_object_acl(BucketName, Key, default_config()). --spec get_object_acl(string(), string(), proplist() | aws_config()) -> proplist() | no_return(). +-spec get_object_acl(string(), string(), proplist() | aws_config()) -> proplist(). get_object_acl(BucketName, Key, Config) when is_record(Config, aws_config) -> @@ -819,7 +912,7 @@ get_object_acl(BucketName, Key, Config) get_object_acl(BucketName, Key, Options) -> get_object_acl(BucketName, Key, Options, default_config()). --spec get_object_acl(string(), string(), proplist(), aws_config()) -> proplist() | no_return(). +-spec get_object_acl(string(), string(), proplist(), aws_config()) -> proplist(). get_object_acl(BucketName, Key, Options, Config) when is_list(BucketName), is_list(Key), is_list(Options) -> @@ -832,12 +925,12 @@ get_object_acl(BucketName, Key, Options, Config) {access_control_list, "AccessControlList/Grant", fun extract_acl/1}], erlcloud_xml:decode(Attributes, Doc). --spec get_object_metadata(string(), string()) -> proplist() | no_return(). +-spec get_object_metadata(string(), string()) -> proplist(). get_object_metadata(BucketName, Key) -> get_object_metadata(BucketName, Key, []). --spec get_object_metadata(string(), string(), proplist() | aws_config()) -> proplist() | no_return(). +-spec get_object_metadata(string(), string(), proplist() | aws_config()) -> proplist(). get_object_metadata(BucketName, Key, Config) when is_record(Config, aws_config) -> @@ -846,7 +939,7 @@ get_object_metadata(BucketName, Key, Config) get_object_metadata(BucketName, Key, Options) -> get_object_metadata(BucketName, Key, Options, default_config()). --spec get_object_metadata(string(), string(), proplist(), proplist() | aws_config()) -> proplist() | no_return(). +-spec get_object_metadata(string(), string(), proplist(), proplist() | aws_config()) -> proplist(). get_object_metadata(BucketName, Key, Options, Config) -> RequestHeaders = [{"If-Modified-Since", proplists:get_value(if_modified_since, Options)}, @@ -877,12 +970,12 @@ decode_replication_status("COMPLETED") -> completed; decode_replication_status("FAILED") -> failed; decode_replication_status("REPLICA") -> replica. --spec get_object_torrent(string(), string()) -> proplist() | no_return(). +-spec get_object_torrent(string(), string()) -> proplist(). get_object_torrent(BucketName, Key) -> get_object_torrent(BucketName, Key, default_config()). --spec get_object_torrent(string(), string(), aws_config()) -> proplist() | no_return(). +-spec get_object_torrent(string(), string(), aws_config()) -> proplist(). get_object_torrent(BucketName, Key, Config) -> {Headers, Body} = s3_request(Config, get, BucketName, [$/|Key], "torrent", [], <<>>, []), @@ -890,12 +983,12 @@ get_object_torrent(BucketName, Key, Config) -> {version_id, proplists:get_value("x-amz-version-id", Headers, "null")}, {torrent, Body}]. --spec list_object_versions(string()) -> proplist() | no_return(). +-spec list_object_versions(string()) -> proplist(). list_object_versions(BucketName) -> list_object_versions(BucketName, []). --spec list_object_versions(string(), proplist() | aws_config()) -> proplist() | no_return(). +-spec list_object_versions(string(), proplist() | aws_config()) -> proplist(). list_object_versions(BucketName, Config) when is_record(Config, aws_config) -> @@ -904,7 +997,7 @@ list_object_versions(BucketName, Config) list_object_versions(BucketName, Options) -> list_object_versions(BucketName, Options, default_config()). --spec list_object_versions(string(), proplist(), aws_config()) -> proplist() | no_return(). +-spec list_object_versions(string(), proplist(), aws_config()) -> proplist(). list_object_versions(BucketName, Options, Config) when is_list(BucketName), is_list(Options) -> @@ -955,12 +1048,12 @@ extract_bucket(Node) -> {creation_date, "CreationDate", time}], Node). --spec put_object(string(), string(), iodata()) -> proplist() | no_return(). +-spec put_object(string(), string(), iodata()) -> proplist(). put_object(BucketName, Key, Value) -> put_object(BucketName, Key, Value, []). --spec put_object(string(), string(), iodata(), proplist() | aws_config()) -> proplist() | no_return(). +-spec put_object(string(), string(), iodata(), proplist() | aws_config()) -> proplist(). put_object(BucketName, Key, Value, Config) when is_record(Config, aws_config) -> @@ -969,7 +1062,7 @@ put_object(BucketName, Key, Value, Config) put_object(BucketName, Key, Value, Options) -> put_object(BucketName, Key, Value, Options, default_config()). --spec put_object(string(), string(), iodata(), proplist(), [{string(), string()}] | aws_config()) -> proplist() | no_return(). +-spec put_object(string(), string(), iodata(), proplist(), [{string(), string()}] | aws_config()) -> proplist(). put_object(BucketName, Key, Value, Options, Config) when is_record(Config, aws_config) -> @@ -978,25 +1071,27 @@ put_object(BucketName, Key, Value, Options, Config) put_object(BucketName, Key, Value, Options, HTTPHeaders) -> put_object(BucketName, Key, Value, Options, HTTPHeaders, default_config()). --spec put_object(string(), string(), iodata(), proplist(), [{string(), string()}], aws_config()) -> proplist() | no_return(). +-spec put_object(string(), string(), iodata(), proplist(), [{string(), string()}], aws_config()) -> proplist(). put_object(BucketName, Key, Value, Options, HTTPHeaders, Config) when is_list(BucketName), is_list(Key), is_list(Value) orelse is_binary(Value), is_list(Options) -> RequestHeaders = [{"x-amz-acl", encode_acl(proplists:get_value(acl, Options))}|HTTPHeaders] ++ [{"x-amz-meta-" ++ string:to_lower(MKey), MValue} || - {MKey, MValue} <- proplists:get_value(meta, Options, [])], + {MKey, MValue} <- proplists:get_value(meta, Options, [])] + ++ [{"x-amz-tagging-" ++ MKey, MValue} || + {MKey, MValue} <- proplists:get_value(tags, Options, [])], POSTData = iolist_to_binary(Value), {Headers, _Body} = s3_request(Config, put, BucketName, [$/|Key], "", [], POSTData, RequestHeaders), [{version_id, proplists:get_value("x-amz-version-id", Headers, "null")} | Headers]. --spec set_object_acl(string(), string(), proplist()) -> ok | no_return(). +-spec set_object_acl(string(), string(), proplist()) -> ok. set_object_acl(BucketName, Key, ACL) -> set_object_acl(BucketName, Key, ACL, default_config()). --spec set_object_acl(string(), string(), proplist(), aws_config()) -> ok | no_return(). +-spec set_object_acl(string(), string(), proplist(), aws_config()) -> ok. set_object_acl(BucketName, Key, ACL, Config) when is_list(BucketName), is_list(Key), is_list(ACL) -> @@ -1020,7 +1115,7 @@ sign_get(Expire_time, BucketName, Key, Config) SecurityToken -> "x-amz-security-token:" ++ SecurityToken ++ "\n" end, To_sign = lists:flatten(["GET\n\n\n", Expires, "\n", SecurityTokenToSign, "/", BucketName, "/", Key]), - Sig = base64:encode(erlcloud_util:sha_mac(Config#aws_config.secret_access_key, To_sign)), + Sig = base64:encode(erlcloud_util:sha256_mac(Config#aws_config.secret_access_key, To_sign)), {Sig, Expires}. -spec make_link(integer(), string(), string()) -> {integer(), string(), string()}. @@ -1086,7 +1181,9 @@ start_multipart(BucketName, Key, Options, HTTPHeaders, Config) RequestHeaders = [{"x-amz-acl", encode_acl(proplists:get_value(acl, Options))}|HTTPHeaders] ++ [{"x-amz-meta-" ++ string:to_lower(MKey), MValue} || - {MKey, MValue} <- proplists:get_value(meta, Options, [])], + {MKey, MValue} <- proplists:get_value(meta, Options, [])] + ++ [{"x-amz-tagging-" ++ MKey, MValue} || + {MKey, MValue} <- proplists:get_value(tags, Options, [])], POSTData = <<>>, case s3_xml_request2(Config, post, BucketName, [$/|Key], "uploads", [], POSTData, RequestHeaders) of @@ -1196,13 +1293,12 @@ list_multipart_uploads(BucketName, Options, HTTPHeaders, Config) Error end. - --spec set_bucket_attribute(string(), atom(), term()) -> ok | no_return(). +-spec set_bucket_attribute(string(), atom(), term()) -> ok. set_bucket_attribute(BucketName, AttributeName, Value) -> set_bucket_attribute(BucketName, AttributeName, Value, default_config()). --spec set_bucket_attribute(string(), atom(), term(), aws_config()) -> ok | no_return(). +-spec set_bucket_attribute(string(), atom(), term(), aws_config()) -> ok. set_bucket_attribute(BucketName, AttributeName, Value, Config) when is_list(BucketName) -> @@ -1242,7 +1338,8 @@ set_bucket_attribute(BucketName, AttributeName, Value, Config) ] }, {"requestPayment", RPXML}; - versioning -> + Versioning when Versioning =:= versioning; + Versioning =:= mfa_delete -> Status = case proplists:get_value(status, Value) of suspended -> "Suspended"; enabled -> "Enabled" @@ -1262,6 +1359,83 @@ set_bucket_attribute(BucketName, AttributeName, Value, Config) Headers = [{"content-type", "application/xml"}], s3_simple_request(Config, put, BucketName, "/", Subresource, [], POSTData, Headers). +-spec put_object_tagging(string(), string(), list({string(), string()})) -> ok. +put_object_tagging(BucketName, Key, TagList) when is_list(BucketName) -> + put_object_tagging(BucketName, Key, TagList, default_config()). + +-spec put_object_tagging(string(), string(), list({string(), string()}), aws_config()) -> ok. +put_object_tagging(BucketName, Key, TagList, #aws_config{} = Config) when is_list(BucketName) -> + TaggingXML = {'Tagging', + [{'TagSet', encode_tags(TagList)}]}, + POSTData = unicode:characters_to_binary(xmerl:export_simple([TaggingXML], xmerl_xml)), + Md5 = base64:encode(crypto:hash(md5, POSTData)), + Headers = [{"content-md5", Md5}, {"content-type", "application/xml"}], + s3_simple_request(Config, put, BucketName, [$/|Key], "tagging", [], POSTData, Headers). + +-spec delete_object_tagging(string(), string()) -> ok. +delete_object_tagging(BucketName, Key) when is_list(BucketName) -> + delete_object_tagging(BucketName, Key, default_config()). + +-spec delete_object_tagging(string(), string(), aws_config()) -> ok. +delete_object_tagging(BucketName, Key, #aws_config{} = Config) when is_list(BucketName) -> + s3_simple_request(Config, delete, BucketName, [$/|Key], "tagging", [], <<>>, []). + +-spec get_object_tagging(string(), string()) -> {ok, list({string(), string()})}. +get_object_tagging(BucketName, Key) when is_list(BucketName) -> + get_object_tagging(BucketName, Key, default_config()). + +-spec get_object_tagging(string(), string(), aws_config()) -> {ok, list({string(), string()})}. +get_object_tagging(BucketName, Key, #aws_config{} = Config) when is_list(BucketName) -> + Doc = s3_xml_request(Config, get, BucketName, [$/|Key], "tagging", [], <<>>, []), + {ok, + [extract_tag(Node) || Node <- xmerl_xpath:string("/Tagging/TagSet/Tag", Doc)]}. + +-spec put_bucket_tagging(string(), list({string(), string()})) -> ok. +put_bucket_tagging(BucketName, TagList) when is_list(BucketName) -> + put_bucket_tagging(BucketName, TagList, default_config()). + +-spec put_bucket_tagging(string(), list({string(), string()}), aws_config()) -> ok. +put_bucket_tagging(BucketName, TagList, #aws_config{} = Config) when is_list(BucketName) -> + TaggingXML = {'Tagging', + [{'TagSet', encode_tags(TagList)}]}, + POSTData = list_to_binary(xmerl:export_simple([TaggingXML], xmerl_xml)), + Md5 = base64:encode(crypto:hash(md5, POSTData)), + Headers = [{"content-md5", Md5}, {"content-type", "application/xml"}], + s3_simple_request(Config, put, BucketName, "/", "tagging", [], POSTData, Headers). + +-spec delete_bucket_tagging(string()) -> ok. +delete_bucket_tagging(BucketName) when is_list(BucketName) -> + delete_bucket_tagging(BucketName, default_config()). + +-spec delete_bucket_tagging(string(), aws_config()) -> ok. +delete_bucket_tagging(BucketName, #aws_config{} = Config) when is_list(BucketName) -> + s3_simple_request(Config, delete, BucketName, "/", "tagging", [], <<>>, []). + +-spec get_bucket_tagging(string()) -> {ok, list({string(), string()})}. +get_bucket_tagging(BucketName) when is_list(BucketName) -> + get_bucket_tagging(BucketName, default_config()). + +-spec get_bucket_tagging(string(), aws_config()) -> {ok, list({string(), string()})}. +get_bucket_tagging(BucketName, #aws_config{} = Config) when is_list(BucketName) -> + Doc = s3_xml_request(Config, get, BucketName, "/", "tagging", [], <<>>, []), + {ok, + [extract_tag(Node) || Node <- xmerl_xpath:string("/Tagging/TagSet/Tag", Doc)]}. + +extract_tag(Node) -> + List = erlcloud_xml:decode([{key, "Key", text}, {value, "Value", text}], Node), + {proplists:get_value(key, List), proplists:get_value(value, List)}. + +encode_tags(Taglist) -> + lists:map(fun encode_one_tag/1, Taglist). + +encode_one_tag({Key, Value}) -> + {'Tag', + [ + {'Key', [Key]}, + {'Value', [Value]} + ] + }. + -spec list_bucket_inventory(string()) -> {ok, Result:: list(term())} | {error, Reason::term()}. list_bucket_inventory(BucketName) when is_list(BucketName) -> @@ -1395,14 +1569,14 @@ get_bucket_inventory(BucketName, InventoryId, #aws_config{} = Config) Error end. --spec put_bucket_inventory(string(), list()) -> ok | {error, Reason::term()} | no_return(). +-spec put_bucket_inventory(string(), list()) -> ok | {error, Reason::term()}. put_bucket_inventory(BucketName, Inventory) when is_list(BucketName), is_list(Inventory) -> put_bucket_inventory(BucketName, Inventory, default_config()). -spec put_bucket_inventory(string(), list(), aws_config()) - -> ok | {error, Reason::term()} | no_return(). + -> ok | {error, Reason::term()}. put_bucket_inventory(BucketName, Inventory, #aws_config{} = Config) when is_list(BucketName), is_list(Inventory) -> @@ -1411,7 +1585,7 @@ put_bucket_inventory(BucketName, Inventory, #aws_config{} = Config) put_bucket_inventory(BucketName, InventoryId, list_to_binary(XmlInventory), Config). -spec put_bucket_inventory(string(), string(), binary(), aws_config()) - -> ok | {error, Reason::term()} | no_return(). + -> ok | {error, Reason::term()}. put_bucket_inventory(BucketName, InventoryId, XmlInventory, #aws_config{} = Config) when is_list(BucketName), is_list(InventoryId), is_binary(XmlInventory) -> Md5 = base64:encode(crypto:hash(md5, XmlInventory)), @@ -1419,11 +1593,11 @@ put_bucket_inventory(BucketName, InventoryId, XmlInventory, #aws_config{} = Conf Headers = [{"Content-MD5", Md5}, {"content-type", "application/xml"}], s3_simple_request(Config, put, BucketName, "/", "inventory", Params, XmlInventory, Headers). --spec delete_bucket_inventory(string(), string()) -> ok | {error, Reason::term()} | no_return(). +-spec delete_bucket_inventory(string(), string()) -> ok | {error, Reason::term()}. delete_bucket_inventory(BucketName, InventoryId) when is_list(BucketName), is_list(InventoryId) -> delete_bucket_inventory(BucketName, InventoryId, default_config()). --spec delete_bucket_inventory(string(), string(), aws_config()) -> ok | {error, Reason::term()} | no_return(). +-spec delete_bucket_inventory(string(), string(), aws_config()) -> ok | {error, Reason::term()}. delete_bucket_inventory(BucketName, InventoryId, #aws_config{} = Config) when is_list(BucketName), is_list(InventoryId) -> @@ -1564,6 +1738,69 @@ get_bucket_encryption(BucketName, Config) -> Error end. +%% @doc +%% S3 API: +%% https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetPublicAccessBlock.html +%% +%% ` +%% {ok, [{block_public_acls, true}, +%% {ignore_public_acls, true}, +%% {block_public_policy, true}, +%% {restrict_public_buckets, true}]} = erlcloud_s3:get_public_access_block("bucket-name"). +%% ' +%% +-spec get_public_access_block(string()) -> + {ok, public_access_block_configuration()} | {error, any()}. +get_public_access_block(BucketName) -> + get_public_access_block(BucketName, default_config()). + +-spec get_public_access_block(string(), aws_config()) -> + {ok, public_access_block_configuration()} | {error, any()}. +get_public_access_block(BucketName, Config) -> + case s3_xml_request2(Config, get, BucketName, "/", "publicAccessBlock", [], <<>>, []) of + {ok, XML} -> + XPath = "/PublicAccessBlockConfiguration", + BlockPublicAcls = XPath ++ "/BlockPublicAcls", + IgnorePublicAcls = XPath ++ "/IgnorePublicAcls", + BlockPublicPolicy = XPath ++ "/BlockPublicPolicy", + RestrictPublicBuckets = XPath ++ "/RestrictPublicBuckets", + {ok, [ + {block_public_acls, erlcloud_xml:get_bool(BlockPublicAcls, XML)}, + {ignore_public_acls, erlcloud_xml:get_bool(IgnorePublicAcls, XML)}, + {block_public_policy, erlcloud_xml:get_bool(BlockPublicPolicy, XML)}, + {restrict_public_buckets, erlcloud_xml:get_bool(RestrictPublicBuckets, XML)} + ]}; + Error -> + Error + end. + +%% @doc +%% S3 API: +%% https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutPublicAccessBlock.html +%% +%% ` +%% PublicAccessBlock = [{block_public_acls, true}, +%% {ignore_public_acls, true}, +%% {block_public_policy, true}, +%% {restrict_public_buckets, true}], +%% ok = erlcloud_s3:put_public_access_block("bucket-name", PublicAccessBlock). +%% ' +%% +-spec put_public_access_block(string(), public_access_block_configuration()) -> + ok | {error, any()}. +put_public_access_block(BucketName, PublicAccessBlock) -> + put_public_access_block(BucketName, PublicAccessBlock, default_config()). + +-spec put_public_access_block(string(), public_access_block_configuration(), aws_config()) -> + ok | {error, any()}. +put_public_access_block(BucketName, PublicAccessBlock, Config) -> + PublicAccessBlockXML = {'PublicAccessBlockConfiguration', [{xmlns, ?XMLNS_S3}], + encode_public_access_block(PublicAccessBlock)}, + POSTData = list_to_binary(xmerl:export_simple([PublicAccessBlockXML], xmerl_xml)), + Md5 = base64:encode(crypto:hash(md5, POSTData)), + Headers = [{"content-md5", Md5}, {"content-type", "application/xml"}], + s3_simple_request(Config, put, BucketName, "/", "publicAccessBlock", [], POSTData, Headers). + %% @doc %% S3 API: %% https://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketDELETEencryption.html @@ -1583,7 +1820,7 @@ delete_bucket_encryption(BucketName, Config) -> %% takes an S3 bucket notification configuration and creates an xmerl simple %% form out of it. %% for the examples of input / output of this function, see tests. --spec create_notification_xml(proplist()) -> tuple(). +-spec create_notification_xml(list(proplist())) -> tuple(). create_notification_xml(Confs) -> {'NotificationConfiguration', [create_notification_xml(ConfName, Params) || [{ConfName, Params}] <- Confs]}. @@ -1620,7 +1857,7 @@ create_notification_param_xml({cloud_function, CF}, Acc) -> [{'CloudFunction', [ -spec get_bucket_and_key(string()) -> {string(), string()}. get_bucket_and_key(Uri) -> - {ok, Parsed} = http_uri:parse(Uri), + {ok, Parsed} = erlcloud_util:uri_parse(Uri), {Host, Path} = extract_host_and_path(Parsed), extract_location_fields(Host, Path). @@ -1963,6 +2200,26 @@ s3_endpoint_for_region(RegionName) -> case RegionName of "us-east-1" -> "s3-external-1.amazonaws.com"; + "cn-northwest-1" -> + "s3.cn-northwest-1.amazonaws.com.cn"; + "cn-north-1" -> + "s3.cn-north-1.amazonaws.com.cn"; _ -> lists:flatten(["s3-", RegionName, ".amazonaws.com"]) end. + +encode_public_access_block(PublicAccessBlockConfiguration) -> + [encode_public_access_block_param(Param) + || {_Attrib, Value} = Param <- PublicAccessBlockConfiguration, + Value =:= true orelse Value =:= false]. + +encode_public_access_block_param({block_public_acls, Value}) -> + {'BlockPublicAcls', [atom_to_list(Value)]}; +encode_public_access_block_param({ignore_public_acls, Value}) -> + {'IgnorePublicAcls', [atom_to_list(Value)]}; +encode_public_access_block_param({block_public_policy, Value}) -> + {'BlockPublicPolicy', [atom_to_list(Value)]}; +encode_public_access_block_param({restrict_public_buckets, Value}) -> + {'RestrictPublicBuckets', [atom_to_list(Value)]}; +encode_public_access_block_param(_) -> + throw(unknown_public_access_block_param). diff --git a/src/erlcloud_sdb.erl b/src/erlcloud_sdb.erl index 5d88b2e29..d33fec9e1 100644 --- a/src/erlcloud_sdb.erl +++ b/src/erlcloud_sdb.erl @@ -59,29 +59,29 @@ configure(AccessKeyID, SecretAccessKey, Host) -> default_config() -> erlcloud_aws:default_config(). --spec create_domain(string()) -> proplist() | no_return(). +-spec create_domain(string()) -> proplist(). create_domain(Name) -> create_domain(Name, default_config()). --spec create_domain(string(), aws_config()) -> proplist() | no_return(). +-spec create_domain(string(), aws_config()) -> proplist(). create_domain(Name, Config) when is_list(Name) -> sdb_simple_request(Config, "CreateDomain", [{"DomainName", Name}]). --spec delete_domain(string()) -> proplist() | no_return(). +-spec delete_domain(string()) -> proplist(). delete_domain(Name) -> delete_domain(Name, default_config()). --spec delete_domain(string(), aws_config()) -> proplist() | no_return(). +-spec delete_domain(string(), aws_config()) -> proplist(). delete_domain(Name, Config) when is_list(Name) -> sdb_simple_request(Config, "DeleteDomain", [{"DomainName", Name}]). --spec domain_metadata(string()) -> proplist() | no_return(). +-spec domain_metadata(string()) -> proplist(). domain_metadata(Name) -> domain_metadata(Name, default_config()). --spec domain_metadata(string(), aws_config()) -> proplist() | no_return(). +-spec domain_metadata(string(), aws_config()) -> proplist(). domain_metadata(Name, Config) when is_list(Name) -> {Doc, Result} = sdb_request(Config, "DomainMetadata", [{"DomainName", Name}]), @@ -97,36 +97,36 @@ domain_metadata(Name, Config) ], MR), [{domain_metadata, Metadata}|Result]. --spec batch_put_attributes(string(), [{string(), sdb_attributes()}]) -> proplist() | no_return(). +-spec batch_put_attributes(string(), [{string(), sdb_attributes()}]) -> proplist(). batch_put_attributes(DomainName, Items) -> batch_put_attributes(DomainName, Items, default_config()). --spec batch_put_attributes(string(), [{string(), sdb_attributes()}], aws_config()) -> proplist() | no_return(). +-spec batch_put_attributes(string(), [{string(), sdb_attributes()}], aws_config()) -> proplist(). batch_put_attributes(DomainName, Items, Config) when is_list(DomainName), is_list(Items) -> ItemParams = [[{"ItemName", Name}|attributes_list(Attrs)] || {Name, Attrs} <- Items], sdb_simple_request(Config, "BatchPutAttributes", [{"DomainName", DomainName}|erlcloud_aws:param_list(ItemParams, "Item")]). --spec delete_attributes(string(), string()) -> proplist() | no_return(). +-spec delete_attributes(string(), string()) -> proplist(). delete_attributes(DomainName, ItemName) -> delete_attributes(DomainName, ItemName, []). --spec delete_attributes(string(), string(), sdb_delete_attributes() | aws_config()) -> proplist() | no_return(). +-spec delete_attributes(string(), string(), sdb_delete_attributes() | aws_config()) -> proplist(). delete_attributes(DomainName, ItemName, Config) when is_record(Config, aws_config) -> delete_attributes(DomainName, ItemName, [], Config); delete_attributes(DomainName, ItemName, Attributes) -> delete_attributes(DomainName, ItemName, Attributes, []). --spec delete_attributes(string(), string(), sdb_delete_attributes(), sdb_conditionals() | aws_config()) -> proplist() | no_return(). +-spec delete_attributes(string(), string(), sdb_delete_attributes(), sdb_conditionals() | aws_config()) -> proplist(). delete_attributes(DomainName, ItemName, Attributes, Config) when is_record(Config, aws_config) -> delete_attributes(DomainName, ItemName, Attributes, [], Config); delete_attributes(DomainName, ItemName, Attributes, Conditionals) -> delete_attributes(DomainName, ItemName, Attributes, Conditionals, default_config()). --spec delete_attributes(string(), string(), sdb_delete_attributes(), sdb_conditionals(), aws_config()) -> proplist() | no_return(). +-spec delete_attributes(string(), string(), sdb_delete_attributes(), sdb_conditionals(), aws_config()) -> proplist(). delete_attributes(DomainName, ItemName, Attributes, Conditionals, Config) when is_list(DomainName), is_list(ItemName), is_list(Attributes), is_list(Conditionals) -> @@ -134,11 +134,11 @@ delete_attributes(DomainName, ItemName, Attributes, Conditionals, Config) delete_attributes_list(Attributes)] ++ conditionals_list(Conditionals), sdb_simple_request(Config, "DeleteAttributes", Params). --spec get_attributes(string(), string()) -> proplist() | no_return(). +-spec get_attributes(string(), string()) -> proplist(). get_attributes(DomainName, ItemName) -> get_attributes(DomainName, ItemName, []). --spec get_attributes(string(), string(), [string()] | boolean() | aws_config()) -> proplist() | no_return(). +-spec get_attributes(string(), string(), [string()] | boolean() | aws_config()) -> proplist(). get_attributes(DomainName, ItemName, Config) when is_record(Config, aws_config) -> get_attributes(DomainName, ItemName, [], Config); @@ -148,7 +148,7 @@ get_attributes(DomainName, ItemName, ConsistentRead) get_attributes(DomainName, ItemName, AttributeNames) -> get_attributes(DomainName, ItemName, AttributeNames, false). --spec get_attributes(string(), string(), [string()], boolean() | aws_config()) -> proplist() | no_return(). +-spec get_attributes(string(), string(), [string()], boolean() | aws_config()) -> proplist(). get_attributes(DomainName, ItemName, AttributeNames, Config) when is_record(Config, aws_config) -> get_attributes(DomainName, ItemName, AttributeNames, false, Config); @@ -156,7 +156,7 @@ get_attributes(DomainName, ItemName, AttributeNames, ConsistentRead) -> get_attributes(DomainName, ItemName, AttributeNames, ConsistentRead, default_config()). --spec get_attributes(string(), string(), [string()], boolean(), aws_config()) -> proplist() | no_return(). +-spec get_attributes(string(), string(), [string()], boolean(), aws_config()) -> proplist(). get_attributes(DomainName, ItemName, AttributeNames, ConsistentRead, Config) -> {Doc, Result} = sdb_request(Config, "GetAttributes", [{"DomainName", DomainName}, {"ItemName", ItemName}, @@ -172,11 +172,11 @@ extract_attributes(Attributes) -> extract_attribute(Node) -> {erlcloud_xml:get_text("Name", Node), erlcloud_xml:get_text("Value", Node)}. --spec list_domains() -> proplist() | no_return(). +-spec list_domains() -> proplist(). list_domains() -> list_domains(default_config()). --spec list_domains(string() | 1..100 | none | aws_config()) -> proplist() | no_return(). +-spec list_domains(string() | 1..100 | none | aws_config()) -> proplist(). list_domains(Config) when is_record(Config, aws_config) -> list_domains("", Config); list_domains(MaxDomains) when is_integer(MaxDomains); MaxDomains =:= none -> @@ -184,7 +184,7 @@ list_domains(MaxDomains) when is_integer(MaxDomains); MaxDomains =:= none -> list_domains(FirstToken) -> list_domains(FirstToken, none). --spec list_domains(string(), 1..100 | none | aws_config()) -> proplist() | no_return(). +-spec list_domains(string(), 1..100 | none | aws_config()) -> proplist(). list_domains(FirstToken, Config) when is_record(Config, aws_config) -> list_domains(FirstToken, none, Config); list_domains(FirstToken, MaxDomains) -> @@ -200,13 +200,13 @@ maybe_add_nexttoken([], Params) -> maybe_add_nexttoken(Token, Params) -> [{"NextToken", Token} | Params]. --spec list_domains(string(), 1..100 | none, aws_config()) -> proplist() | no_return(). +-spec list_domains(string(), 1..100 | none, aws_config()) -> proplist(). list_domains(FirstToken, MaxDomains, Config) when is_list(FirstToken), is_integer(MaxDomains) orelse MaxDomains =:= none -> - Params = - maybe_add_nexttoken(FirstToken, + Params = + maybe_add_nexttoken(FirstToken, maybe_add_maxdomains(MaxDomains, [])), {Doc, Result} = sdb_request(Config, "ListDomains", Params), @@ -214,18 +214,18 @@ list_domains(FirstToken, MaxDomains, Config) [{domains, erlcloud_xml:get_list("/ListDomainsResponse/ListDomainsResult/DomainName", Doc)}, {next_token, erlcloud_xml:get_text("/ListDomainsResponse/ListDomainsResult/NextToken", Doc)}|Result]. --spec put_attributes(string(), string(), sdb_attributes()) -> proplist() | no_return(). +-spec put_attributes(string(), string(), sdb_attributes()) -> proplist(). put_attributes(DomainName, ItemName, Attributes) -> put_attributes(DomainName, ItemName, Attributes, []). --spec put_attributes(string(), string(), sdb_attributes(), sdb_conditionals() | aws_config()) -> proplist() | no_return(). +-spec put_attributes(string(), string(), sdb_attributes(), sdb_conditionals() | aws_config()) -> proplist(). put_attributes(DomainName, ItemName, Attributes, Config) when is_record(Config, aws_config) -> put_attributes(DomainName, ItemName, Attributes, [], Config); put_attributes(DomainName, ItemName, Attributes, Conditionals) -> put_attributes(DomainName, ItemName, Attributes, Conditionals, default_config()). --spec put_attributes(string(), string(), sdb_attributes(), sdb_conditionals(), aws_config()) -> proplist() | no_return(). +-spec put_attributes(string(), string(), sdb_attributes(), sdb_conditionals(), aws_config()) -> proplist(). put_attributes(DomainName, ItemName, Attributes, Conditionals, Config) when is_list(DomainName), is_list(ItemName), is_list(Attributes), is_list(Conditionals) -> @@ -236,10 +236,10 @@ put_attributes(DomainName, ItemName, Attributes, Conditionals, Config) %% These functions will return the first page of results along with %% a token to retrieve the next page, if any. --spec select(string()) -> proplist() | no_return(). +-spec select(string()) -> proplist(). select(SelectExpression) -> select(SelectExpression, none). --spec select(string(), string() | none | boolean() | aws_config()) -> proplist() | no_return(). +-spec select(string(), string() | none | boolean() | aws_config()) -> proplist(). select(SelectExpression, Config) when is_record(Config, aws_config) -> select(SelectExpression, none, Config); @@ -249,14 +249,14 @@ select(SelectExpression, ConsistentRead) select(SelectExpression, NextToken) -> select(SelectExpression, NextToken, false). --spec select(string(), string() | none, boolean() | aws_config()) -> proplist() | no_return(). +-spec select(string(), string() | none, boolean() | aws_config()) -> proplist(). select(SelectExpression, NextToken, Config) when is_record(Config, aws_config) -> select(SelectExpression, NextToken, false, Config); select(SelectExpression, NextToken, ConsistentRead) -> select(SelectExpression, NextToken, ConsistentRead, default_config()). --spec select(string(), string() | none, boolean(), aws_config()) -> proplist() | no_return(). +-spec select(string(), string() | none, boolean(), aws_config()) -> proplist(). select(SelectExpression, NextToken, ConsistentRead, Config) when is_list(SelectExpression), is_list(NextToken) orelse NextToken =:= none, @@ -274,24 +274,24 @@ select(SelectExpression, NextToken, ConsistentRead, Config) %% These functions will make multiple requests until all %% pages of results have been consumed. --spec select_all(string()) -> proplist() | no_return(). +-spec select_all(string()) -> proplist(). select_all(SelectExpression) -> select_all(SelectExpression, false). --spec select_all(string(), boolean()) -> proplist() | no_return(). +-spec select_all(string(), boolean()) -> proplist(). select_all(SelectExpression, ConsistentRead) when is_boolean(ConsistentRead) -> select_all(SelectExpression, ConsistentRead, default_config()); select_all(SelectExpression, Config) -> select_all(SelectExpression, false, Config). --spec select_all(string(), boolean(), aws_config()) -> proplist() | no_return(). +-spec select_all(string(), boolean(), aws_config()) -> proplist(). select_all(SelectExpression, ConsistentRead, Config) when is_list(SelectExpression), is_boolean(ConsistentRead) -> select_all(SelectExpression, none, ConsistentRead, Config, [], []). --spec select_all(string(), string() | none | done, boolean(), aws_config(), proplist(), proplist()) -> proplist() | no_return(). +-spec select_all(string(), string() | none | done, boolean(), aws_config(), proplist(), proplist()) -> proplist(). select_all(_, done, _, _, Items, Metadata) -> [{items, Items}|Metadata]; select_all(SelectExpression, NextToken, ConsistentRead, Config, Items, Metadata) -> diff --git a/src/erlcloud_securityhub.erl b/src/erlcloud_securityhub.erl new file mode 100644 index 000000000..65484e5c4 --- /dev/null +++ b/src/erlcloud_securityhub.erl @@ -0,0 +1,127 @@ +-module(erlcloud_securityhub). + +-include("erlcloud.hrl"). +-include("erlcloud_aws.hrl"). + +%% API +-export([ + describe_hub/1, + describe_hub/2 +]). + +-type securityhub() :: proplist(). + +-type param_name() :: binary() | string() | atom(). +-type param_value() :: binary() | string() | atom() | integer(). +-type params() :: [param_name() | {param_name(), param_value()}]. + +-spec describe_hub(AwsConfig) -> Result + when AwsConfig :: aws_config(), + Result :: {ok, securityhub()} | {error, not_found} | {error, term()}. +describe_hub(AwsConfig) + when is_record(AwsConfig, aws_config) -> + describe_hub(AwsConfig, _Params = []); +describe_hub(Params) -> + AwsConfig = erlcloud_aws:default_config(), + describe_hub(AwsConfig, Params). + +-spec describe_hub(AwsConfig, Params) -> Result + when AwsConfig :: aws_config(), + Params :: params(), + Result :: {ok, securityhub()}| {error, not_found} | {error, term()}. +describe_hub(AwsConfig, Params) -> + Path = ["accounts"], + case request(AwsConfig, _Method = get, Path, Params) of + {ok, Response} -> + {ok, Response}; + {error, {<<"ResourceNotFoundException">>, _Message}} -> + {error, not_found}; + {error, Reason} -> + {error, Reason} + end. + + +request(AwsConfig, Method, Path, Params) -> + request(AwsConfig, Method, Path, Params, _RequestBody = <<>>). + +request(AwsConfig0, Method, Path, Params, RequestBody) -> + case erlcloud_aws:update_config(AwsConfig0) of + {ok, AwsConfig1} -> + AwsRequest0 = init_request(AwsConfig1, Method, Path, Params, RequestBody), + AwsRequest1 = erlcloud_retry:request(AwsConfig1, AwsRequest0, fun should_retry/1), + case AwsRequest1#aws_request.response_type of + ok -> + decode_response(AwsRequest1); + error -> + decode_error(AwsRequest1) + end; + {error, Reason} -> + {error, Reason} + end. + +init_request(AwsConfig, Method, Path, Params, Payload) -> + Host = AwsConfig#aws_config.securityhub_host, + Service = "securityhub", + NormPath = norm_path(Path), + NormParams = norm_params(Params), + Region = erlcloud_aws:aws_region_from_host(Host), + Headers = [{"host", Host}, {"content-type", "application/json"}], + SignedHeaders = erlcloud_aws:sign_v4(Method, NormPath, AwsConfig, Headers, Payload, Region, Service, Params), + #aws_request{ + service = securityhub, + method = Method, + uri = "https://" ++ Host ++ NormPath ++ NormParams, + request_headers = SignedHeaders, + request_body = Payload + }. + +norm_path(Path) -> + binary_to_list(iolist_to_binary(["/" | lists:join("/", Path)])). + +norm_params([] = _Params) -> + ""; +norm_params(Params) -> + "?" ++ erlcloud_aws:canonical_query_string(Params). + +should_retry(Request) + when Request#aws_request.response_type == ok -> + Request; +should_retry(Request) + when Request#aws_request.response_type == error, + Request#aws_request.response_status == 429 -> + Request#aws_request{should_retry = true}; +should_retry(Request) + when Request#aws_request.response_type == error, + Request#aws_request.response_status >= 500 -> + Request#aws_request{should_retry = true}; +should_retry(Request) -> + Request#aws_request{should_retry = false}. + +decode_response(AwsRequest) -> + case AwsRequest#aws_request.response_body of + <<>> -> + ok; + ResponseBody -> + Json = jsx:decode(ResponseBody, [{return_maps, false}]), + {ok, Json} + end. + +decode_error(AwsRequest) -> + case AwsRequest#aws_request.error_type of + aws -> + Type = extract_error_type(AwsRequest), + Message = extract_error_message(AwsRequest), + {error, {Type, Message}}; + _ -> + erlcloud_aws:request_to_return(AwsRequest) + end. + +extract_error_type(AwsRequest) -> + Headers = AwsRequest#aws_request.response_headers, + Value = proplists:get_value("x-amzn-errortype", Headers), + iolist_to_binary(Value). + +extract_error_message(AwsRequest) -> + ResponseBody = AwsRequest#aws_request.response_body, + Object = jsx:decode(ResponseBody, [{return_maps, false}]), + proplists:get_value(<<"message">>, Object, <<>>). \ No newline at end of file diff --git a/src/erlcloud_ses.erl b/src/erlcloud_ses.erl index 7aa11521e..c52549a57 100644 --- a/src/erlcloud_ses.erl +++ b/src/erlcloud_ses.erl @@ -12,7 +12,6 @@ %% * ListIdentityPolicies %% * ListVerifiedEmailAddresses (deprecated; use ListIdentities) %% * PutIdentityPolicy -%% * SendRawEmail %% * VerifyEmailAddress (deprecated; use VerifyEmailIdentity) %% %% @end @@ -21,9 +20,14 @@ -module(erlcloud_ses). -export([configure/2, configure/3, new/2, new/3]). +-export([create_custom_verification_email_template/6, create_custom_verification_email_template/7]). + +-export([delete_custom_verification_email_template/1, delete_custom_verification_email_template/2]). -export([delete_identity/1, delete_identity/2]). +-export([get_custom_verification_email_template/1, get_custom_verification_email_template/2]). + -export([get_identity_dkim_attributes/1, get_identity_dkim_attributes/2]). -export([get_identity_notification_attributes/1, get_identity_notification_attributes/2]). -export([get_identity_verification_attributes/1, get_identity_verification_attributes/2]). @@ -31,18 +35,28 @@ -export([get_send_quota/0, get_send_quota/1]). -export([get_send_statistics/0, get_send_statistics/1]). +-export([list_custom_verification_email_templates/0, list_custom_verification_email_templates/1]). + -export([list_identities/0, list_identities/1, list_identities/2]). +-export([send_custom_verification_email/2, send_custom_verification_email/3]). + -export([send_email/4, send_email/5, send_email/6]). +-export([send_raw_email/1, send_raw_email/2, send_raw_email/3]). + -export([set_identity_dkim_enabled/2, set_identity_dkim_enabled/3]). -export([set_identity_feedback_forwarding_enabled/2, set_identity_feedback_forwarding_enabled/3]). -export([set_identity_notification_topic/3, set_identity_notification_topic/4]). +-export([set_identity_headers_in_notifications_enabled/3, set_identity_headers_in_notifications_enabled/4]). + +-export([update_custom_verification_email_template/2, update_custom_verification_email_template/3]). -export([verify_domain_dkim/1, verify_domain_dkim/2]). -export([verify_email_identity/1, verify_email_identity/2]). -export([verify_domain_identity/1, verify_domain_identity/2]). + -include("erlcloud.hrl"). -include("erlcloud_aws.hrl"). @@ -52,6 +66,9 @@ %%% Common types %%%------------------------------------------------------------------------------ +-type custom_template_attribute_names() :: template_name | from_email_address | template_subject | template_content | success_redirect_url | failure_redirect_url . +-type custom_template_attributes() :: [{custom_template_attribute_names(), string()}]. + -type identity() :: string() | binary(). %% identities (as input) can be a single identity or a list of them. @@ -66,10 +83,14 @@ -type verification_status() :: pending | success | failed | temporary_failure | not_started. --export_type([identity/0, identities/0, +-type notification_type() :: bounce | complaint | delivery. + +-export_type([custom_template_attribute_names/0, custom_template_attributes/0, + identity/0, identities/0, email/0, emails/0, domain/0, - verification_status/0]). + verification_status/0, + notification_type/0]). %%%------------------------------------------------------------------------------ %%% Library initialization @@ -98,6 +119,80 @@ configure(AccessKeyID, SecretAccessKey, Host) -> default_config() -> erlcloud_aws:default_config(). +%%------------------------------------------------------------------------------ +%% @doc +%% SES API: +%% [https://docs.aws.amazon.com/ses/latest/APIReference/API_CreateCustomVerificationEmailTemplate.html] +%% +%% ===Example=== +%% +%% Creates a new custom verification email template. +%% +%% ` +%% ok = erlcloud_ses:create_custom_verification_email_template_result( +%% "templaneName", +%% "support@example.com", +%% "Welcome to support", +%% "limited html content", +%% "https://www.example.com/success", +%% "https://www.example.com/failure" ). +%% ' +%% +%% Please consult the following for what is and not allowed in the HTML content parameter +%% [https://docs.aws.amazon.com/ses/latest/DeveloperGuide/custom-verification-emails.html#custom-verification-emails-faq] +%% @end +%%------------------------------------------------------------------------------ + +-type create_custom_verification_email_template_result() :: ok | { error, term() }. + +-spec create_custom_verification_email_template( string(), string(), string(), string(), string(), string() ) -> create_custom_verification_email_template_result(). +create_custom_verification_email_template(TemplateName, FromEmailAddress, TemplateSubject, TemplateContent, SuccessRedirectionURL, FailureRedirectionURL) -> + create_custom_verification_email_template(TemplateName, FromEmailAddress, TemplateSubject, TemplateContent, SuccessRedirectionURL, FailureRedirectionURL ,default_config()). + +-spec create_custom_verification_email_template(string(), string(), string(), string(), string(), string(), aws_config()) -> + create_custom_verification_email_template_result(). +create_custom_verification_email_template(TemplateName, FromEmailAddress, TemplateSubject, TemplateContent, SuccessRedirectionURL, FailureRedirectionURL, Config) -> + Params = encode_params([{template_name, TemplateName}, + {from_email_address, FromEmailAddress}, + {template_subject, TemplateSubject}, + {template_content, TemplateContent}, + {success_redirect_url, SuccessRedirectionURL}, + {failure_redirect_url, FailureRedirectionURL}]), + case ses_request(Config, "CreateCustomVerificationEmailTemplate", Params) of + {ok, _Doc} -> ok; + {error, Reason} -> {error, Reason} + end. + +%%------------------------------------------------------------------------------ +%% @doc +%% SES API: +%% [https://docs.aws.amazon.com/ses/latest/APIReference/API_DeleteCustomVerificationEmailTemplate.html] +%% +%% ===Example=== +%% +%% Deletes an existing custom verification email template. +%% +%% ` +%% ok = erlcloud_ses:delete_custom_verification_email_template("templaneName"). +%% ' +%% +%% @end +%%------------------------------------------------------------------------------ + +-type delete_custom_verification_email_template_result() :: ok | {error,term()}. + +-spec delete_custom_verification_email_template(string()) -> delete_custom_verification_email_template_result(). +delete_custom_verification_email_template(TemplateName) -> + delete_custom_verification_email_template(TemplateName, default_config()). + +-spec delete_custom_verification_email_template(string(), aws_config()) -> delete_custom_verification_email_template_result(). +delete_custom_verification_email_template(TemplateName, Config) -> + Params = encode_params([{template_name, TemplateName }]), + case ses_request(Config, "DeleteCustomVerificationEmailTemplate", Params) of + {ok, _Doc} -> ok; + {error, Reason} -> {error, Reason} + end. + %%%------------------------------------------------------------------------------ %%% DeleteIdentity @@ -132,6 +227,52 @@ delete_identity(Identity, Config) -> {error, Reason} -> {error, Reason} end. +%%------------------------------------------------------------------------------ +%% @doc +%% SES API: +%% [https://docs.aws.amazon.com/ses/latest/APIReference/API_GetCustomVerificationEmailTemplate.html] +%% +%% ===Example=== +%% +%% Returns the custom email verification template for the template name you specify. +%% +%% ` +%% {ok,[{template_name,"templateName"}, +%% {from_email_address,"support@example.com"}, +%% {template_subject,"Welcome to Example.com"}, +%% {template_content,"\n\n \n

    Ready to start with Example.com

    \n

    Example.com is very happy to +%% welcome you to the ACME system Please click\non the link below to activate +%% your account.

    \n\n"}, +%% {success_redirect_url,"https://www.example.com/success"}, +%% {failure_redirect_url,"http://example.arilia.com"}]} = +%% erlcloud_ses:delete_custom_verification_email_template("templaneName"). +%% ' +%% +%% @end +%%------------------------------------------------------------------------------ + +-type get_custom_verification_email_template_result() :: {ok, custom_template_attributes()} | {error, term()}. + +-spec get_custom_verification_email_template(string()) -> get_custom_verification_email_template_result(). +get_custom_verification_email_template(TemplateName) -> + get_custom_verification_email_template(TemplateName, default_config()). + +-spec get_custom_verification_email_template( string() , aws_config() ) -> get_custom_verification_email_template_result(). +get_custom_verification_email_template(TemplateName, Config) -> + Params = encode_params([{template_name, TemplateName}]), + case ses_request(Config, "GetCustomVerificationEmailTemplate", Params) of + { ok , Doc } -> + {ok, erlcloud_xml:decode( + [{template_name, "GetCustomVerificationEmailTemplateResult/TemplateName", text}, + {from_email_address, "GetCustomVerificationEmailTemplateResult/FromEmailAddress", text}, + {template_subject, "GetCustomVerificationEmailTemplateResult/TemplateSubject", text}, + {template_content, "GetCustomVerificationEmailTemplateResult/TemplateContent", text}, + {success_redirect_url, "GetCustomVerificationEmailTemplateResult/SuccessRedirectionURL", text}, + {failure_redirect_url, "GetCustomVerificationEmailTemplateResult/FailureRedirectionURL", text}], + Doc)}; + {error, Reason} -> {error, Reason} + end. %%%------------------------------------------------------------------------------ %%% GetIdentityDkimAttributes @@ -368,6 +509,48 @@ get_send_statistics(Config) -> {error, Reason} -> {error, Reason} end. +%%------------------------------------------------------------------------------ +%% @doc +%% SES API: +%% [https://docs.aws.amazon.com/ses/latest/APIReference/API_ListCustomVerificationEmailTemplates.html] +%% +%% ===Example=== +%% +%% Lists the existing custom verification email templates for your account in the current AWS Region. +%% +%% ` +%% {ok,[{custom_templates,[[{template_name,"template1"}, +%% {from_email_address,"support@example.com"}, +%% {template_subject,"Welcome to support"}, +%% {success_redirect_url,"https://www.example.com/success"}, +%% {failure_redirect_url,"https://www.example.com/failure"}], +%% [{template_name,"template2"}, +%% {from_email_address,"applications@example.com"}, +%% {template_subject,"Welcome to Applications"}, +%% {success_redirect_url,"https://www.example.com/success"}, +%% {failure_redirect_url,"https://www.example.com/failure"}]]}]} = +%% erlcloud_ses:list_custom_verification_email_templates(). +%% ' +%% +%% @end +%%------------------------------------------------------------------------------ + +-type list_custom_verification_email_templates_result() :: {ok, [{custom_templates, [custom_template_attributes()]}]} | {error , term()}. + +-spec list_custom_verification_email_templates() -> list_custom_verification_email_templates_result(). +list_custom_verification_email_templates() -> + list_custom_verification_email_templates(default_config()). + +-spec list_custom_verification_email_templates(aws_config()) -> list_custom_verification_email_templates_result(). +list_custom_verification_email_templates(Config) -> + Params = [{"MaxResults",50}], + case ses_request(Config, "ListCustomVerificationEmailTemplates", Params) of + { ok , Doc } -> + {ok, erlcloud_xml:decode([{custom_templates, "ListCustomVerificationEmailTemplatesResult/CustomVerificationEmailTemplates/member", fun decode_custom_template_entry/1}, + {next_token, "ListCustomVerificationEmailTemplatesResult/NextToken", optional_text}], + Doc)}; + {error, Reason } -> {error, Reason} + end. %%%------------------------------------------------------------------------------ %%% ListIdentities @@ -431,6 +614,38 @@ list_identities(Opts, Config) -> {error, Reason} -> {error, Reason} end. +%%------------------------------------------------------------------------------ +%% @doc +%% SES API: +%% [https://docs.aws.amazon.com/ses/latest/APIReference/API_SendCustomVerificationEmail.html] +%% +%% ===Example=== +%% +%% Send a custom verification email. +%% +%% ` +%% {ok, [{message_id, "abcdefghijkllkjdlkj"}]} = +%% erlcloud_ses:send_custom_verification_email_result("newclient@newco.com", "templateName"). +%% ' +%% +%% @end +%%------------------------------------------------------------------------------ + +-type send_custom_verification_email_result() :: {ok, term()} | {error, term()}. + +-spec send_custom_verification_email(string(), string()) -> send_custom_verification_email_result(). +send_custom_verification_email(EmailAddress, TemplateName) -> + send_custom_verification_email(EmailAddress, TemplateName, default_config()). + +-spec send_custom_verification_email(string(), string(), aws_config()) -> send_custom_verification_email_result(). +send_custom_verification_email(EmailAddress, TemplateName, Config) -> + Params = encode_params([{email_address, EmailAddress}, + {template_name, TemplateName}]), + case ses_request(Config, "SendCustomVerificationEmail", Params) of + {ok, Doc} -> + {ok, erlcloud_xml:decode([{message_id, "SendCustomVerificationEmailResult/MessageId", text}], Doc)}; + {error, Reason} -> {error, Reason} + end. %%%------------------------------------------------------------------------------ %%% SendEmail @@ -452,8 +667,10 @@ list_identities(Opts, Config) -> -type send_email_source() :: email(). --type send_email_opt() :: {reply_to_addresses, emails()} | - {return_path, email()}. +-type send_email_opt() :: {configuration_set_name, string() | binary()} | + {reply_to_addresses, emails()} | + {return_path, email()} | + {tags, [{string() | binary(), string() | binary()}]}. -type send_email_opts() :: [send_email_opt()]. @@ -522,6 +739,66 @@ send_email(Destination, Body, Subject, Source, Opts, Config) -> {error, Reason} -> {error, Reason} end. +%%%------------------------------------------------------------------------------ +%%% SendRawEmail +%%%------------------------------------------------------------------------------ + +-type send_raw_email_message() :: binary() | string(). + +-type send_raw_email_opt() :: {source, email()} | + {destinations, emails()}. + +-type send_raw_email_opts() :: [send_raw_email_opt()]. + +-type send_raw_email_result() :: {ok, [{message_id, string()}]} | {error, term()}. + + +send_raw_email(RawMessage) -> + send_raw_email(RawMessage, [], default_config()). + +send_raw_email(RawMessage, #aws_config{} = Config) -> + send_raw_email(RawMessage, [], Config); +send_raw_email(RawMessage, Opts) -> + send_raw_email(RawMessage, Opts, default_config()). + +%%------------------------------------------------------------------------------ +%% @doc +%% SES API: +%% [https://docs.aws.amazon.com/ses/latest/APIReference/API_SendRawEmail.html] +%% +%% ===Example=== +%% +%% Send a raw email. +%% +%% ` +%% {ok, [{message_id, "00000131d51d2292-159ad6eb-077c-46e6-ad09-ae7c05925ed4-000000"}]} = +%% erlcloud_ses:send_raw_email(<<"From: b@from.com\nTo: a@to.com\nSubject: Subject\nMIME-Version: 1.0\nContent-type: Multipart/Mixed; boundary=\"NextPart\"\n\n--NextPart\nContent-Type: text/plain\n\nEmail Body\n\n--NextPart--">>, []). +%% ' +%% +%% All supported inputs. +%% +%% ` +%% {ok, [{message_id, "00000131d51d2292-159ad6eb-077c-46e6-ad09-ae7c05925ed4-000000"}]} = +%% erlcloud_ses:send_email(<<"To: d@to.com\nCC: c@cc.com\nBCC: a@bcc.com, b@bcc.com\nSubject: Subject\nMIME-Version: 1.0\nContent-type: Multipart/Mixed; boundary=\"NextPart\"\n\n--NextPart\nContent-Type: text/plain\n\nEmail Body\n\n--NextPart--">>, +%% [{destinations, [<<"a@bcc.com">>, "b@bcc.com", <<"c@cc.com">>, "d@to.com"]}, +%% {source, "e@from.com"}]. +%% ' +%% @end +%%------------------------------------------------------------------------------ + +-spec send_raw_email(send_raw_email_message(), + send_raw_email_opts(), + aws_config()) -> + send_raw_email_result(). +send_raw_email(RawMessage, Opts, Config) -> + Params = encode_params([{raw_message, RawMessage}, + {send_raw_email_opts, Opts}]), + case ses_request(Config, "SendRawEmail", Params) of + {ok, Doc} -> + {ok, erlcloud_xml:decode([{message_id, "SendRawEmailResult/MessageId", text}], Doc)}; + {error, Reason} -> {error, Reason} + end. + %%%------------------------------------------------------------------------------ %%% SetIdentityDkimEnabled @@ -601,8 +878,6 @@ set_identity_feedback_forwarding_enabled(Identity, ForwardingEnabled, Config) -> %%% SetIdentityNotificationTopic %%%------------------------------------------------------------------------------ --type notification_type() :: bounce | complaint | delivery. - -type sns_topic() :: string() | binary(). -type set_identity_notification_topic_result() :: ok | {error, term()}. @@ -643,6 +918,84 @@ set_identity_notification_topic(Identity, NotificationType, SnsTopic, Config) -> end. +%%%------------------------------------------------------------------------------ +%%% SetIdentityHeadersInNotificationsEnabled +%%%------------------------------------------------------------------------------ + +set_identity_headers_in_notifications_enabled(Identity, NotificationType, Enabled) -> + set_identity_headers_in_notifications_enabled(Identity, NotificationType, Enabled, default_config()). + +%%------------------------------------------------------------------------------ +%% @doc +%% SES API: +%% [https://docs.aws.amazon.com/ses/latest/APIReference/API_SetIdentityHeadersInNotificationsEnabled.html] +%% +%% ===Example=== +%% +%% Enable headers in notifications for an identity. +%% +%% ` +%% ok = erlcloud_ses:set_identity_headers_in_notifications_enabled(<<"user@example.com">>, bounce, true). +%% ' +%% +%% @end +%%------------------------------------------------------------------------------ + +-spec set_identity_headers_in_notifications_enabled(identity(), + notification_type(), + boolean(), + aws_config()) -> + ok | {error, term()}. +set_identity_headers_in_notifications_enabled(Identity, NotificationType, Enabled, Config) -> + Params = encode_params([{identity, Identity}, + {notification_type, NotificationType}, + {enabled, Enabled}]), + case ses_request(Config, "SetIdentityHeadersInNotificationsEnabled", Params) of + {ok, _Doc} -> ok; + {error, Reason} -> {error, Reason} + end. + +%%------------------------------------------------------------------------------ +%% @doc +%% SES API: +%% [https://docs.aws.amazon.com/ses/latest/APIReference/API_UpdateCustomVerificationEmailTemplate.html] +%% +%% Template attributes +%% { template_name , string() } +%% { from_email_address , string() } +%% { template_subject , string() } +%% { template_content , string() } +%% { success_redirect_url , string() } +%% { failure_redirect_url , string() } +%% +%% ===Example=== +%% +%% Updates an existing custom verification email template. +%% +%% ` +%% ok = erlcloud_ses:update_custom_verification_email_template("templateName", +%% [{template_subject, "New subject"}, +%% {from_email_address, "support2@example.com"}]). +%% ' +%% Please consult the following for what is and not allowed in the HTML content parameter +%% [https://docs.aws.amazon.com/ses/latest/DeveloperGuide/custom-verification-emails.html#custom-verification-emails-faq] +%% @end +%%------------------------------------------------------------------------------ + +-type update_custom_verification_email_template_result() :: ok | { error, term() }. + +-spec update_custom_verification_email_template(string(), [{atom(), string()}]) -> update_custom_verification_email_template_result(). +update_custom_verification_email_template(TemplateName, Attributes) -> + update_custom_verification_email_template(TemplateName, Attributes,default_config()). + +-spec update_custom_verification_email_template(string(), [{atom(), string()}], aws_config()) -> update_custom_verification_email_template_result(). +update_custom_verification_email_template(TemplateName, Attributes, Config) -> + Params = encode_params([{template_name, TemplateName} | Attributes]), + case ses_request(Config, "UpdateCustomVerificationEmailTemplate", Params) of + {ok, _Doc} -> ok ; + {error, Reason} -> {error, Reason} + end. + %%%------------------------------------------------------------------------------ %%% VerifyDomainDkim %%%------------------------------------------------------------------------------ @@ -718,7 +1071,6 @@ verify_domain_identity(Domain, Config) -> {error, Reason} -> {error, Reason} end. - %%%------------------------------------------------------------------------------ %%% VerifyEmailIdentity %%%------------------------------------------------------------------------------ @@ -768,6 +1120,8 @@ encode_params([{destination, Destination} | T], Acc) -> encode_params(T, encode_destination(Destination, Acc)); encode_params([{email_address, EmailAddress} | T], Acc) when is_list(EmailAddress); is_binary(EmailAddress) -> encode_params(T, [{"EmailAddress", EmailAddress} | Acc]); +encode_params([{enabled, Enabled} | T], Acc) when is_boolean(Enabled) -> + encode_params(T, [{"Enabled", Enabled} | Acc]); encode_params([{dkim_enabled, DkimEnabled} | T], Acc) when is_boolean(DkimEnabled) -> encode_params(T, [{"DkimEnabled", DkimEnabled} | Acc]); encode_params([{domain, Domain} | T], Acc) when is_list(Domain); is_binary(Domain) -> @@ -786,14 +1140,36 @@ encode_params([{next_token, NextToken} | T], Acc) when is_list(NextToken); is_bi encode_params(T, [{"NextToken", NextToken} | Acc]); encode_params([{notification_type, NotificationType} | T], Acc) -> encode_params(T, encode_notification_type(NotificationType, Acc)); +encode_params([{raw_message, RawMessage} | T], Acc) -> + encode_params(T, [{"RawMessage.Data", base64:encode(RawMessage)} | Acc]); encode_params([{send_email_opts, Opts} | T], Acc) -> encode_params(T, encode_opts(Opts, Acc)); +encode_params([{send_raw_email_opts, Opts} | T], Acc) -> + encode_params(T, encode_raw_opts(Opts, Acc)); encode_params([{sns_topic, SnsTopic} | T], Acc) when is_list(SnsTopic); is_binary(SnsTopic) -> encode_params(T, [{"SnsTopic", SnsTopic} | Acc]); encode_params([{source, Source} | T], Acc) when is_list(Source); is_binary(Source) -> encode_params(T, [{"Source", Source} | Acc]); encode_params([{subject, Subject} | T], Acc) -> encode_params(T, encode_content("Message.Subject", Subject, Acc)); +encode_params([{template_name, TemplateName} | T], Acc) + when is_list(TemplateName); is_binary(TemplateName) -> + encode_params(T, [{"TemplateName", TemplateName} | Acc]); +encode_params([{from_email_address, FromEmailAddress} | T], Acc) + when is_list(FromEmailAddress); is_binary(FromEmailAddress) -> + encode_params(T, [{"FromEmailAddress", FromEmailAddress} | Acc]); +encode_params([{template_subject, TemplateSubject} | T], Acc) + when is_list(TemplateSubject); is_binary(TemplateSubject) -> + encode_params(T, [{"TemplateSubject", TemplateSubject} | Acc]); +encode_params([{template_content, TemplateContent} | T], Acc) + when is_list(TemplateContent); is_binary(TemplateContent) -> + encode_params(T, [{"TemplateContent", TemplateContent} | Acc]); +encode_params([{success_redirect_url, SuccessRedirectionURL} | T], Acc) + when is_list(SuccessRedirectionURL); is_binary(SuccessRedirectionURL) -> + encode_params(T, [{"SuccessRedirectionURL", SuccessRedirectionURL} | Acc]); +encode_params([{failure_redirect_url, FailureRedirectionURL} | T], Acc) + when is_list(FailureRedirectionURL); is_binary(FailureRedirectionURL) -> + encode_params(T, [{"FailureRedirectionURL", FailureRedirectionURL} | Acc]); encode_params([Option | _], _Acc) -> error({erlcloud_ses, {invalid_parameter, Option}}). @@ -802,11 +1178,18 @@ encode_list(Prefix, List, Acc) -> encode_list(_, [], _, Acc) -> Acc; +encode_list(Prefix, [{Name, Value} | T], N, Acc) when is_list(Name) orelse is_binary(Name), + is_list(Value) orelse is_binary(Value) -> + encode_list(Prefix, T, N + 1, + [{encode_param_index(Prefix, N) ++ ".Name", Name}, + {encode_param_index(Prefix, N) ++ ".Value", Value} | Acc]); encode_list(Prefix, [H | T], N, Acc) when is_list(H); is_binary(H) -> - encode_list(Prefix, T, N + 1, [{Prefix ++ ".member." ++ integer_to_list(N), H} | Acc]); + encode_list(Prefix, T, N + 1, [{encode_param_index(Prefix, N), H} | Acc]); encode_list(Prefix, V, N, Acc) when is_list(V); is_binary(V) -> encode_list(Prefix, [V], N, Acc). - + +encode_param_index(Prefix, N) -> Prefix ++ ".member." ++ integer_to_list(N). + encode_destination_pairs([], Acc) -> Acc; encode_destination_pairs([{bcc_addresses, List} | T], Acc) -> @@ -826,6 +1209,12 @@ encode_destination(ToAddress, Acc) when is_list(ToAddress); is_binary(ToAddress) %% Single entry encode_destination_pairs([{to_addresses, [ToAddress]}], Acc). +encode_destinations([Dest | _T] = Destinations, Acc) when is_list(Dest); is_binary(Dest) -> + encode_list("Destinations", Destinations, Acc); +encode_destinations(ToAddress, Acc) when is_list(ToAddress); is_binary(ToAddress) -> + %% Single entry + encode_destinations([ToAddress], Acc). + encode_content_pairs(_, [], Acc) -> Acc; encode_content_pairs(Prefix, [{charset, Charset} | T], Acc) -> @@ -856,11 +1245,21 @@ encode_body(Body, Acc) when is_list(Body); is_binary(Body) -> encode_opts([], Acc) -> Acc; +encode_opts([{configuration_set_name, ConfigurationSet} | T], Acc) -> + encode_opts(T, [{"ConfigurationSetName", ConfigurationSet} | Acc]); encode_opts([{reply_to_addresses, List} | T], Acc) -> encode_opts(T, encode_list("ReplyToAddresses", List, Acc)); encode_opts([{return_path, ReturnPath} | T], Acc) -> - encode_opts(T, [{"ReturnPath", ReturnPath} | Acc]). + encode_opts(T, [{"ReturnPath", ReturnPath} | Acc]); +encode_opts([{tags, Tags} | T], Acc) -> + encode_opts(T, encode_list("Tags", Tags, Acc)). +encode_raw_opts([], Acc) -> + Acc; +encode_raw_opts([{source, Source} | T], Acc) when is_list(Source); is_binary(Source) -> + encode_raw_opts(T, [{"Source", Source} | Acc]); +encode_raw_opts([{destinations, Destination} | T], Acc) -> + encode_raw_opts(T, encode_destinations(Destination, Acc)). encode_identity_type(email_address, Acc) -> [{"IdentityType", "EmailAddress"} | Acc]; @@ -902,6 +1301,15 @@ decode_dkim_attributes(DkimAttributesDoc) -> decode_notification_attributes(NotificationAttributesDoc) -> [{erlcloud_xml:get_text("key", Entry), erlcloud_xml:decode([{forwarding_enabled, "value/ForwardingEnabled", boolean}, + {headers_in_bounce_notifications_enabled, + "value/HeadersInBounceNotificationsEnabled", + boolean}, + {headers_in_complaint_notifications_enabled, + "value/HeadersInComplaintNotificationsEnabled", + boolean}, + {headers_in_delivery_notifications_enabled, + "value/HeadersInDeliveryNotificationsEnabled", + boolean}, {bounce_topic, "value/BounceTopic", optional_text}, {complaint_topic, "value/ComplaintTopic", optional_text}, {delivery_topic, "value/DeliveryTopic", optional_text}], @@ -926,6 +1334,18 @@ decode_send_data_points(SendDataPointsDoc) -> Entry) || Entry <- SendDataPointsDoc]. +%% Added for decoding a custom template entry with the ListCustomVerificationEmailTemplates command +%% Notice that the ListCustomVerificationEmailTemplates API does not return the TemplateContent +%% You must use GetCustomVerificationEmailTemplate to retrieve TemplateContent +decode_custom_template_entry(CustomTemplatesDoc) -> + [ erlcloud_xml:decode([ + { template_name , "TemplateName" , text } , + { from_email_address , "FromEmailAddress" , text } , + { template_subject , "TemplateSubject" , text } , + { success_redirect_url , "SuccessRedirectionURL" , text } , + { failure_redirect_url , "FailureRedirectionURL" , text } ], + Entry) + || Entry <- CustomTemplatesDoc ]. decode_error_code("IncompleteSignature") -> incomplete_signature; decode_error_code("InternalFailure") -> internal_failure; @@ -943,7 +1363,17 @@ decode_error_code("OptInRequired") -> opt_in_required; decode_error_code("RequestExpired") -> request_expired; decode_error_code("ServiceUnavailable") -> service_unavailable; decode_error_code("Throttling") -> throttling; -decode_error_code("ValidationError") -> validation_error. +decode_error_code("ValidationError") -> validation_error; +decode_error_code("LimitExceeded") -> limit_exceeded; +decode_error_code("ConfigurationSetDoesNotExist") -> configuration_set_does_not_exist; +decode_error_code("CustomVerificationEmailTemplateDoesNotExist") -> custom_verification_email_template_does_not_exist; +decode_error_code("ProductionAccessNotGranted") -> production_access_not_granted; +decode_error_code("CustomVerificationEmailInvalidContent") -> custom_verification_email_invalid_content; +decode_error_code("FromEmailAddressNotVerified") -> from_email_address_not_verified; +decode_error_code("CustomVerificationEmailTemplateAlreadyExists") -> custom_verification_email_template_already_exists; +decode_error_code("AccountSendingPaused") -> account_sending_paused; +decode_error_code("ConfigurationSetSendingPaused") -> configuration_set_sending_paused; +decode_error_code("MailFromDomainNotVerified") -> mail_from_domain_not_verified. decode_error(Doc) -> @@ -956,38 +1386,10 @@ decode_error(Doc) -> %%%------------------------------------------------------------------------------ ses_request(Config, Action, Params) -> - case erlcloud_aws:update_config(Config) of - {ok, Config1} -> - ses_request_no_update(Config1, Action, Params); - {error, Reason} -> - {error, Reason} - end. - -ses_request_no_update(Config, Action, Params) -> - Date = httpd_util:rfc1123_date(), - Signature = base64:encode_to_string( - erlcloud_util:sha256_mac(Config#aws_config.secret_access_key, Date)), - Auth = lists:flatten( - ["AWS3-HTTPS AWSAccessKeyId=", - Config#aws_config.access_key_id, - ",Algorithm=HmacSHA256,Signature=", - Signature]), - - Headers = [{"Date", Date}, - {"X-Amzn-Authorization", Auth}], - Headers2 = case Config#aws_config.security_token of - undefined -> - Headers; - Token -> - [{"x-amz-security-token", Token} | Headers] - end, - QParams = [{"Action", Action}, - {"Version", ?API_VERSION} | + QParams = [{"Action", Action}, + {"Version", ?API_VERSION} | Params], - Query = erlcloud_http:make_query_string(QParams), - - case erlcloud_aws:aws_request_form( - post, "https", Config#aws_config.ses_host, 443, "/", Query, Headers2, Config) of + case erlcloud_aws:aws_request4(post, "https", Config#aws_config.ses_host, 443, "/", QParams, "ses", Config) of {ok, Body} -> {ok, element(1, xmerl_scan:string(binary_to_list(Body)))}; {error, {http_error, Code, _, ErrBody}} when Code >= 400; Code =< 599 -> diff --git a/src/erlcloud_sm.erl b/src/erlcloud_sm.erl new file mode 100644 index 000000000..bf0150fb0 --- /dev/null +++ b/src/erlcloud_sm.erl @@ -0,0 +1,1135 @@ +-module(erlcloud_sm). +-author("joshua@halloapp.com"). + +-include("erlcloud.hrl"). +-include("erlcloud_aws.hrl"). + +%%% Library initialization. +-export([new/2, new/3, new/4]). + +%%% API +-export([ + batch_get_secret_value/2, batch_get_secret_value/3, + cancel_rotate_secret/1, cancel_rotate_secret/2, + create_secret/3, create_secret/4, create_secret/5, + create_secret_binary/3, create_secret_binary/4, create_secret_binary/5, + create_secret_string/3, create_secret_string/4, create_secret_string/5, + delete_resource_policy/1, delete_resource_policy/2, + delete_secret/1, delete_secret/2, delete_secret/3, + describe_secret/1, describe_secret/2, + get_random_password/0, get_random_password/1, get_random_password/2, + get_resource_policy/1, get_resource_policy/2, + get_secret_value/2, get_secret_value/3, + list_secrets/0, list_secrets/1, list_secrets/2, + list_secret_version_ids/1, list_secret_version_ids/2, list_secret_version_ids/3, + put_resource_policy/2, put_resource_policy/3, put_resource_policy/4, + put_secret_binary/3, put_secret_binary/4, put_secret_binary/5, + put_secret_string/3, put_secret_string/4, put_secret_string/5, + put_secret_value/3, put_secret_value/4, put_secret_value/5, + remove_regions_from_replication/2, remove_regions_from_replication/3, + replicate_secret_to_regions/2, replicate_secret_to_regions/3, replicate_secret_to_regions/4, + restore_secret/1, restore_secret/2, + rotate_secret/2, rotate_secret/3, rotate_secret/4, + stop_replication_to_replica/1, stop_replication_to_replica/2, + tag_resource/2, tag_resource/3, + untag_resource/2, untag_resource/3, + update_secret/2, update_secret/3, update_secret/4, + update_secret_version_stage/2, update_secret_version_stage/3, update_secret_version_stage/4, + validate_resource_policy/1, validate_resource_policy/2, validate_resource_policy/3 +]). + +%%%------------------------------------------------------------------------------ +%%% Shared types +%%%------------------------------------------------------------------------------ + +-type sm_response() :: {ok, proplists:proplist()} | {error, term()}. + +-type get_secret_value_option() :: {version_id | version_stage, binary()}. +-type get_secret_value_options() :: [get_secret_value_option()]. + +%% replica region is expected to be a proplist of the following tuples: +%% [{<<"KmsKeyId">>, binary()}, {<<"Region">>, binary()}] +-type replica_region() :: [proplist()]. +-type replica_regions() :: [replica_region()]. + +%% batch get secret value types +-type secret_value_filter() :: {key, binary(), values, [binary()]}. +-type secret_value_filters() :: [secret_value_filter()]. + +-type batch_get_secret_value_param() :: {filters, secret_value_filters()} | {secret_id_list, [binary()]}. + +-type batch_get_secret_value_option() :: {max_results, pos_integer()} + | {next_token, binary()}. +-type batch_get_secret_value_options() :: [batch_get_secret_value_option()]. + +-type create_secret_option() :: {add_replica_regions, replica_regions()} + | {client_request_token, binary()} + | {description, binary()} + | {force_overwrite_replica_secret, boolean()} + | {kms_key_id, binary()} + | {secret_binary, binary()} %% Note AWS accepts either SecretBinary or SecretString, + | {secret_string, binary()} %% not both at the same time + | {tags, proplist()}. +-type create_secret_options() :: [create_secret_option()]. + +-type delete_secret_option() :: {force_delete_without_recovery, boolean()} %% Note you can't use both this parameter and RecoveryWindowInDays. + | {recovery_window_in_days, pos_integer()}. %% If none of these two options are specified then SM defaults to 30 day recovery window +-type delete_secret_options() :: [delete_secret_option()]. + +%% get random password options types +-type get_random_password_option() :: {exclude_characters, binary()} + | {exclude_lowercase, boolean()} + | {exclude_numbers, boolean()} + | {exclude_punctuation, boolean()} + | {exclude_uppercase, boolean()} + | {include_space, boolean()} + | {password_length, pos_integer()} + | {require_each_included_type, boolean()}. +-type get_random_password_options() :: [get_random_password_option()]. + +%% list secrets options types +-type list_secrets_option() :: {filters, secret_value_filters()} + | {max_results, pos_integer()} + | {include_planned_deletion, boolean()} + | {next_token, binary()} + | {sort_order, binary()}. +-type list_secrets_options() :: [list_secrets_option()]. + +% list secret version ids options types +-type list_secret_version_ids_option() :: {include_deprecated, boolean()} + | {max_results, pos_integer()} + | {next_token, binary()}. +-type list_secret_version_ids_options() :: [list_secret_version_ids_option()]. + +-type put_resource_policy_option() :: {block_public_policy, boolean()}. +-type put_resource_policy_options() :: [put_resource_policy_option()]. + +-type secret_value() :: {secret_binary, binary()} | {secret_string, binary()}. +-type put_secret_value_option() :: {client_request_token, binary()} + | {secret_binary, binary()} + | {secret_string, binary()} + | {version_stages, [binary()]}. +-type put_secret_value_options() :: [put_secret_value_option()]. + +-type rotation_rules() :: [rotate_rule()]. +-type rotate_rule() :: {automatically_after_days, pos_integer()} + | {duration, binary()} + | {schedule_expression, binary()}. + +-type rotate_secret_options() :: [rotate_secret_option()]. +-type rotate_secret_option() :: {rotate_immediately, boolean()} + | {rotation_lambda_arn, binary()} + | {rotation_rules, proplist()}. + +-type replicate_secret_to_regions_option() :: {force_overwrite_replica_secret, boolean()}. +-type replicate_secret_to_regions_options() :: [replicate_secret_to_regions_option()]. + +-type update_secret_options() :: [update_secret_option()]. +-type update_secret_option() :: {description, binary()} + | {kms_key_id, binary()} + | {secret_binary, binary()} + | {secret_string, binary()}. + +-type update_secret_version_stage_option() :: {move_to_version_id, binary()} + | {remove_from_version_id, binary()}. +-type update_secret_version_stage_options() :: [update_secret_version_stage_option()]. + +-type validate_resource_policy_option() :: {secret_id, binary()}. +-type validate_resource_policy_options() :: [validate_resource_policy_option()]. + + +%%%------------------------------------------------------------------------------ +%%% Library initialization. +%%%------------------------------------------------------------------------------ + +-spec new(string(), string()) -> aws_config(). +new(AccessKeyID, SecretAccessKey) -> + #aws_config{ + access_key_id = AccessKeyID, + secret_access_key = SecretAccessKey + }. + + +-spec new(string(), string(), string()) -> aws_config(). +new(AccessKeyID, SecretAccessKey, Host) -> + #aws_config{ + access_key_id = AccessKeyID, + secret_access_key = SecretAccessKey, + sm_host = Host + }. + + +-spec new(string(), string(), string(), non_neg_integer()) -> aws_config(). +new(AccessKeyID, SecretAccessKey, Host, Port) -> + #aws_config{ + access_key_id = AccessKeyID, + secret_access_key = SecretAccessKey, + sm_host = Host, + sm_port = Port + }. + + +%%------------------------------------------------------------------------------ +%% BatchGetSecretValue +%%------------------------------------------------------------------------------ +%% @doc +%% Retrieves the contents of the encrypted fields SecretString or SecretBinary for up to 20 secrets. +%% To retrieve a single secret, call GetSecretValue. You must include Filters or SecretIdList, but not both. +%% SM API: +%% [https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_BatchGetSecretValue.html] +%% @end +%%------------------------------------------------------------------------------ + +-spec batch_get_secret_value(FiltersOrSecretIdList :: batch_get_secret_value_param(), + Opts :: batch_get_secret_value_options()) -> sm_response(). +batch_get_secret_value(FiltersOrSecretIdList, Opts) -> + batch_get_secret_value(FiltersOrSecretIdList, Opts, erlcloud_aws:default_config()). + +-spec batch_get_secret_value(FiltersOrSecretIdList :: batch_get_secret_value_param(), + Opts :: batch_get_secret_value_options(), + Config :: aws_config()) -> sm_response(). +batch_get_secret_value(FiltersOrSecretIdList, Opts, Config) -> + BatchGetSecretParam = case FiltersOrSecretIdList of + {filters, Filter} -> {<<"Filters">>, Filter}; + {secret_id_list, IdList} -> {<<"SecretIdList">>, IdList} + end, + Json = lists:map( + fun + ({max_results, Val}) -> {<<"MaxResults">>, Val}; + ({next_token, Val}) -> {<<"NextToken">>, Val}; + (Other) -> Other + end, + [BatchGetSecretParam | Opts]), + sm_request(Config, "secretsmanager.BatchGetSecretValue", Json). + +%%------------------------------------------------------------------------------ +%% CancelRotateSecret +%%------------------------------------------------------------------------------ +%% @doc +%% Turns off automatic rotation, and if a rotation is currently in progress, cancels the rotation. +%% +%% SM API: +%% [https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_CancelRotateSecret.html] +%% @end +%%------------------------------------------------------------------------------ + +-spec cancel_rotate_secret(SecretId :: binary()) -> sm_response(). +cancel_rotate_secret(SecretId) -> + cancel_rotate_secret(SecretId, erlcloud_aws:default_config()). + +-spec cancel_rotate_secret(SecretId :: binary(), Config :: aws_config()) -> sm_response(). +cancel_rotate_secret(SecretId, Config) -> + Json = [{<<"SecretId">>, SecretId}], + sm_request(Config, "secretsmanager.CancelRotateSecret", Json). + +%%------------------------------------------------------------------------------ +%% CreateSecret - CreateSecret +%%------------------------------------------------------------------------------ +%% @doc +%% Creates a new secret.A secret can be a password, a set of credentials such as +%% a user name and password, an OAuth token, or other secret information that you +%% store in an encrypted form in Secrets Manager. The secret also includes the connection +%% information to access a database or other service, which Secrets Manager doesn't encrypt. +%% A secret in Secrets Manager consists of both the protected secret data and the important +%% information needed to manage the secret. +%% +%% ClientRequestToken is used by AWS for secret versioning purposes. +%% It is recommended to be a UUID type value, and is required to be between +%% 32 and 64 characters. +%% +%% Note: Use create_secret, as create_secret_binary and create_secret_string functions are kept for backward compatibility. +%% +%% SM API: +%% [https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_CreateSecret.html] +%% +%% @end +%%------------------------------------------------------------------------------ + +-spec create_secret(Name :: binary(), ClientRequestToken :: binary(), + Secret :: secret_value()) -> sm_response(). +create_secret(Name, ClientRequestToken, Secret) -> + create_secret(Name, ClientRequestToken, Secret, []). + +-spec create_secret(Name :: binary(), ClientRequestToken :: binary(), + Secret :: secret_value(), + Opts :: create_secret_options()) -> sm_response(). +create_secret(Name, ClientRequestToken, Secret, Opts) -> + create_secret(Name, ClientRequestToken, Secret, Opts, erlcloud_aws:default_config()). + +-spec create_secret(Name :: binary(), ClientRequestToken :: binary(), + Secret :: secret_value(), Opts :: create_secret_options(), + Config :: aws_config()) -> sm_response(). +create_secret(Name, ClientRequestToken, Secret, Opts, Config) -> + SecretValue = case Secret of + {secret_binary, Val} -> {secret_binary, base64:encode(Val)}; + {secret_string, Val} -> {secret_string, Val} + end, + create_secret_call(Name, ClientRequestToken, [SecretValue | Opts], Config). + +%%------------------------------------------------------------------------------ +%% CreateSecret - SecretBinary +%%------------------------------------------------------------------------------ +%% @doc +%% +%% Note: Use create_secret, as create_secret_binary and create_secret_string functions are kept for backward compatibility. +%% +%% Creates a new secret binary. The function internally base64-encodes the binary +%% as it is expected by the AWS SecretManager API, so raw blob is expected +%% to be passed as an attribute. +%% +%% ClientRequestToken is used by AWS for secret versioning purposes. +%% It is recommended to be a UUID type value, and is required to be between +%% 32 and 64 characters. +%% +%% To store a text secret use CreateSecret - SecretString version of the function +%% instead. +%% +%% SM API: +%% [https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_CreateSecret.html] +%% +%% Example: +%% Name = <<"my-secret-binary">>, +%% ClientRequestToken = <<"7537a353-0de0-4b98-bf55-f8365821ed36">>, +%% %% some binary to store (say, an RSA private key's exponent) +%% {[_E, _Pub], [_E, _N, Priv, _P1, _P2, _E1, _E2, _C]} = crypto:generate_key(rsa, {2048,65537}), +%% erlcloud_sm:create_secret_binary(Name, ClientRequestToken, Priv). +%% @end +%%------------------------------------------------------------------------------ + +-spec create_secret_binary(Name :: binary(), ClientRequestToken :: binary(), + SecretBinary :: binary()) -> sm_response(). +create_secret_binary(Name, ClientRequestToken, SecretBinary) -> + create_secret_binary(Name, ClientRequestToken, SecretBinary, []). + +-spec create_secret_binary(Name :: binary(), ClientRequestToken :: binary(), + SecretBinary :: binary(), + Opts :: create_secret_options()) -> sm_response(). +create_secret_binary(Name, ClientRequestToken, SecretBinary, Opts) -> + create_secret_binary(Name, ClientRequestToken, SecretBinary, Opts, erlcloud_aws:default_config()). + +-spec create_secret_binary(Name :: binary(), ClientRequestToken :: binary(), + SecretBinary :: binary(), Opts :: create_secret_options(), + Config :: aws_config()) -> sm_response(). +create_secret_binary(Name, ClientRequestToken, SecretBinary, Opts, Config) -> + Secret = {secret_binary, base64:encode(SecretBinary)}, + create_secret_call(Name, ClientRequestToken, [Secret | Opts], Config). + +%%------------------------------------------------------------------------------ +%% CreateSecret - SecretString +%%------------------------------------------------------------------------------ +%% @doc +%% +%% Note: Use create_secret, as create_secret_binary and create_secret_string functions are kept for backward compatibility. +%% +%% Creates a new secret string. The API expects SecretString is a text data to +%% encrypt and store in the SecretManager. It is recommended a JSON structure +%% of key/value pairs is used for the secret value. +%% +%% ClientRequestToken is used by AWS for secret versioning purposes. +%% It is recommended to be a UUID type value, and is required to be between +%% 32 and 64 characters. +%% +%% To store a binary (which will be base64 encoded by the library, as it is +%% expected by AWS SecretManager API), use CreateSecret - SecretBinary version +%% of the function. +%% +%% SM API: +%% [https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_CreateSecret.html] +%% +%% Example: +%% Name = <<"my-secret-string">>, +%% ClientRequestToken = <<"7537a353-0de0-4b98-bf55-f8365821ed37">>, +%% %% some user/password json to store +%% Secret = jsx:encode(#{<<"user">> => <<"my-user">>, <<"password">> => <<"superSecretPassword">>}), +%% erlcloud_sm:create_secret_string(Name, ClientRequestToken, Secret). +%% +%% @end +%%------------------------------------------------------------------------------ + +-spec create_secret_string(Name :: binary(), ClientRequestToken :: binary(), + SecretString :: binary()) -> sm_response(). +create_secret_string(Name, ClientRequestToken, SecretString) -> + create_secret_string(Name, ClientRequestToken, SecretString, []). + +-spec create_secret_string(Name :: binary(), ClientRequestToken :: binary(), + SecretString :: binary(), Opts :: create_secret_options()) -> sm_response(). +create_secret_string(Name, ClientRequestToken, SecretString, Opts) -> + create_secret_string(Name, ClientRequestToken, SecretString, Opts, erlcloud_aws:default_config()). + + +-spec create_secret_string(Name :: binary(), ClientRequestToken :: binary(), + SecretString :: binary(), Opts :: create_secret_options(), + Config :: aws_config()) -> sm_response(). +create_secret_string(Name, ClientRequestToken, SecretString, Opts, Config) -> + Secret = {secret_string, SecretString}, + create_secret_call(Name, ClientRequestToken, [Secret | Opts], Config). + +%%------------------------------------------------------------------------------ +%% DeleteResourcePolicy +%%------------------------------------------------------------------------------ +%% @doc +%% SM API: +%% [https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_DeleteResourcePolicy.html] +%% @end +%%------------------------------------------------------------------------------ + +-spec delete_resource_policy(SecretId :: binary()) -> sm_response(). +delete_resource_policy(SecretId) -> + delete_resource_policy(SecretId, erlcloud_aws:default_config()). + +-spec delete_resource_policy(SecretId :: binary(), Config :: aws_config()) -> sm_response(). +delete_resource_policy(SecretId, Config) -> + Json = [{<<"SecretId">>, SecretId}], + sm_request(Config, "secretsmanager.DeleteResourcePolicy", Json). + +%%------------------------------------------------------------------------------ +%% DeleteSecret +%%------------------------------------------------------------------------------ +%% @doc +%% SM API: +%% [https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_DeleteSecret.html] +%% @end +%%------------------------------------------------------------------------------ + +-spec delete_secret(SecretId :: binary()) -> sm_response(). +delete_secret(SecretId) -> + delete_secret(SecretId, []). + +-spec delete_secret(SecretId :: binary(), Opts :: delete_secret_options()) -> sm_response(). +delete_secret(SecretId, Opts) -> + delete_secret(SecretId, Opts, erlcloud_aws:default_config()). + +-spec delete_secret(SecretId :: binary(), Opts :: delete_secret_options(), + Config :: aws_config()) -> sm_response(). +delete_secret(SecretId, Opts, Config) -> + Json = lists:map( + fun + ({force_delete_without_recovery, Val}) -> {<<"ForceDeleteWithoutRecovery">>, Val}; + ({recovery_window_in_days, Val}) -> {<<"RecoveryWindowInDays">>, Val}; + (Other) -> Other + end, + [{<<"SecretId">>, SecretId} | Opts]), + sm_request(Config, "secretsmanager.DeleteSecret", Json). + +%%------------------------------------------------------------------------------ +%% DescribeSecret +%%------------------------------------------------------------------------------ +%% @doc +%% SM API: +%% [https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_DescibeSecret.html] +%% @end +%%------------------------------------------------------------------------------ + +-spec describe_secret(SecretId :: binary()) -> sm_response(). +describe_secret(SecretId) -> + describe_secret(SecretId, erlcloud_aws:default_config()). + +-spec describe_secret(SecretId :: binary(), Config :: aws_config()) -> sm_response(). +describe_secret(SecretId, Config) -> + Json = [{<<"SecretId">>, SecretId}], + sm_request(Config, "secretsmanager.DescribeSecret", Json). + +%%------------------------------------------------------------------------------ +%% GetRandomPassword +%%------------------------------------------------------------------------------ +%% @doc +%% Generates a random password. We recommend that you specify the maximum length and include +%% every character type that the system you are generating a password for can support. +%% By default, Secrets Manager uses uppercase and lowercase letters, numbers, and the +%% following characters in passwords: !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~ +%% +%% SM API: +%% [https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetRandomPassword.html] +%% @end +%%------------------------------------------------------------------------------ + +-spec get_random_password() -> sm_response(). +get_random_password() -> + get_random_password([]). + +-spec get_random_password(Opts :: get_random_password_options()) -> sm_response(). +get_random_password(Opts) -> + get_random_password(Opts, erlcloud_aws:default_config()). + +-spec get_random_password(Opts :: get_random_password_options(), Config :: aws_config()) -> sm_response(). +get_random_password(Opts, Config) -> + Json = lists:map( + fun + ({exclude_characters, Val}) -> {<<"ExcludeCharacters">>, Val}; + ({exclude_lowercase, Val}) -> {<<"ExcludeLowercase">>, Val}; + ({exclude_numbers, Val}) -> {<<"ExcludeNumbers">>, Val}; + ({exclude_punctuation, Val}) -> {<<"ExcludePunctuation">>, Val}; + ({exclude_uppercase, Val}) -> {<<"ExcludeUppercase">>, Val}; + ({include_space, Val}) -> {<<"IncludeSpace">>, Val}; + ({password_length, Val}) -> {<<"PasswordLength">>, Val}; + ({require_each_included_type, Val}) -> {<<"RequireEachIncludedType">>, Val}; + (Other) -> Other + end, + Opts), + sm_request(Config, "secretsmanager.GetRandomPassword", Json). + +%%------------------------------------------------------------------------------ +%% GetResourcePolicy +%%------------------------------------------------------------------------------ +%% @doc +%% SM API: +%% [https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetResourcePolicy.html] +%% @end +%%------------------------------------------------------------------------------ + +-spec get_resource_policy(SecretId :: binary()) -> sm_response(). +get_resource_policy(SecretId) -> + get_resource_policy(SecretId, erlcloud_aws:default_config()). + +-spec get_resource_policy(SecretId :: binary(), Config :: aws_config()) -> sm_response(). +get_resource_policy(SecretId, Config) -> + Json = [{<<"SecretId">>, SecretId}], + sm_request(Config, "secretsmanager.GetResourcePolicy", Json). + +%%------------------------------------------------------------------------------ +%% GetSecretValue +%%------------------------------------------------------------------------------ +%% @doc +%% SM API: +%% [https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html] +%% @end +%%------------------------------------------------------------------------------ + +-spec get_secret_value(SecretId :: binary(), Opts :: get_secret_value_options()) -> sm_response(). +get_secret_value(SecretId, Opts) -> + get_secret_value(SecretId, Opts, erlcloud_aws:default_config()). + +-spec get_secret_value(SecretId :: binary(), Opts :: get_secret_value_options(), + Config :: aws_config()) -> sm_response(). +get_secret_value(SecretId, Opts, Config) -> + Json = lists:map( + fun + ({version_id, Val}) -> {<<"VersionId">>, Val}; + ({version_stage, Val}) -> {<<"VersionStage">>, Val}; + (Other) -> Other + end, + [{<<"SecretId">>, SecretId} | Opts]), + sm_request(Config, "secretsmanager.GetSecretValue", Json). + +%%------------------------------------------------------------------------------ +%% ListSecrets +%%------------------------------------------------------------------------------ +%% @doc +%% Lists the secrets that are stored by Secrets Manager in the AWS account, not including +%% secrets that are marked for deletion. To see secrets marked for deletion, use the Secrets Manager console. +%% +%% All Secrets Manager operations are eventually consistent. ListSecrets might not reflect changes +%% from the last five minutes. You can get more recent information for a specific secret by calling DescribeSecret. +%% +%% SM API: +%% [https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_ListSecrets.html] +%% @end +%%------------------------------------------------------------------------------ + +-spec list_secrets() -> sm_response(). +list_secrets() -> + list_secrets([]). + +-spec list_secrets(Opts :: list_secrets_options()) -> sm_response(). +list_secrets(Opts) -> + list_secrets(Opts, erlcloud_aws:default_config()). + +-spec list_secrets(Opts :: list_secrets_options(), + Config :: aws_config()) -> sm_response(). +list_secrets(Opts, Config) -> + Json = lists:map( + fun + ({filters, Val}) -> {<<"Filters">>, Val}; + ({include_planned_deletion, Val}) -> {<<"IncludePlannedDeletion">>, Val}; + ({max_results, Val}) -> {<<"MaxResults">>, Val}; + ({next_token, Val}) -> {<<"NextToken">>, Val}; + ({sort_order, Val}) -> {<<"SortOrder">>, Val}; + (Other) -> Other + end, + Opts), + sm_request(Config, "secretsmanager.ListSecrets", Json). + +%%------------------------------------------------------------------------------ +%% ListSecretVersionIds +%%------------------------------------------------------------------------------ +%% @doc +%% Lists the versions of a secret. Secrets Manager uses staging labels to indicate the different versions of a secret. +%% +%% SM API: +%% [https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_ListSecretVersionIds.html] +%% @end +%%------------------------------------------------------------------------------ + +-spec list_secret_version_ids(SecretId :: binary()) -> sm_response(). +list_secret_version_ids(SecretId) -> + list_secret_version_ids(SecretId, []). + +-spec list_secret_version_ids(SecretId :: binary(), Opts :: list_secret_version_ids_options()) -> sm_response(). +list_secret_version_ids(SecretId, Opts) -> + list_secret_version_ids(SecretId, Opts, erlcloud_aws:default_config()). + +-spec list_secret_version_ids(SecretId :: binary(), Opts :: list_secret_version_ids_options(), + Config :: aws_config()) -> sm_response(). +list_secret_version_ids(SecretId, Opts, Config) -> + Json = lists:map( + fun + ({include_deprecated, Val}) -> {<<"IncludeDeprecated">>, Val}; + ({max_results, Val}) -> {<<"MaxResults">>, Val}; + ({next_token, Val}) -> {<<"NextToken">>, Val}; + (Other) -> Other + end, + [{<<"SecretId">>, SecretId} | Opts]), + sm_request(Config, "secretsmanager.ListSecretVersionIds", Json). + +%%------------------------------------------------------------------------------ +%% PutResourcePolicy +%%------------------------------------------------------------------------------ +%% @doc +%% SM API: +%% [https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_PutResourcePolicy.html] +%% @end +%%------------------------------------------------------------------------------ + +-spec put_resource_policy(SecretId :: binary(), ResourcePolicy :: binary()) -> sm_response(). +put_resource_policy(SecretId, ResourcePolicy) -> + put_resource_policy(SecretId, ResourcePolicy, []). + +-spec put_resource_policy(SecretId :: binary(), ResourcePolicy :: binary(), + Opts :: put_resource_policy_options()) -> sm_response(). +put_resource_policy(SecretId, ResourcePolicy, Opts) -> + put_resource_policy(SecretId, ResourcePolicy, Opts, erlcloud_aws:default_config()). + +-spec put_resource_policy(SecretId :: binary(), ResourcePolicy :: binary(), + Opts :: put_resource_policy_options(), + Config :: aws_config()) -> sm_response(). +put_resource_policy(SecretId, ResourcePolicy, Opts, Config) -> + Json = lists:map( + fun + ({block_public_policy, Val}) -> {<<"BlockPublicPolicy">>, Val}; + (Other) -> Other + end, + [{<<"SecretId">>, SecretId}, {<<"ResourcePolicy">>, ResourcePolicy} | Opts]), + sm_request(Config, "secretsmanager.PutResourcePolicy", Json). + +%%------------------------------------------------------------------------------ +%% PutSecretValue - put_secret_value +%%------------------------------------------------------------------------------ +%% @doc +%% +%% Note: Use put_secret_value as put_secret_string and put_secret_binary functions are kept for backward compatibility. +%% +%% Creates a new version of your secret by creating a new encrypted value and attaching it to the secret. +%% Version can contain a new SecretString value or a new SecretBinary value. +%% +%% Do not call PutSecretValue at a sustained rate of more than once every 10 minutes. +%% When you update the secret value, Secrets Manager creates a new version of the secret. +%% Secrets Manager keeps 100 of the most recent versions, but it keeps all secret versions created in the last 24 hours. +%% If you call PutSecretValue more than once every 10 minutes, you will create more versions than Secrets Manager removes, +%% and you will reach the quota for secret versions. +%% +%% You can specify the staging labels to attach to the new version in VersionStages. +%% If you don't include VersionStages, then Secrets Manager automatically moves the staging label AWSCURRENT to this version. +%% If this operation creates the first version for the secret, then Secrets Manager automatically attaches the staging label AWSCURRENT to it. +%% If this operation moves the staging label AWSCURRENT from another version to this version, then Secrets Manager also automatically moves +%% the staging label AWSPREVIOUS to the version that AWSCURRENT was removed from. +%% +%% This operation is idempotent. If you call this operation with a ClientRequestToken that matches an existing version's VersionId, +%% and you specify the same secret data, the operation succeeds but does nothing. However, if the secret data is different, then +%% the operation fails because you can't modify an existing version; you can only create new ones. +%% +%% SM API: +%% [https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_PutSecretValue.html] +%% @end +%%------------------------------------------------------------------------------ +-spec put_secret_value(SecretId :: binary(), ClientRequestToken :: binary(), + Secret :: secret_value()) -> sm_response(). +put_secret_value(SecretId, ClientRequestToken, Secret) -> + put_secret_value(SecretId, ClientRequestToken, Secret, []). + +-spec put_secret_value(SecretId :: binary(), ClientRequestToken :: binary(), + Secret :: secret_value(), + Opts :: put_secret_value_options()) -> sm_response(). +put_secret_value(SecretId, ClientRequestToken, Secret, Opts) -> + put_secret_value(SecretId, ClientRequestToken, Secret, Opts, erlcloud_aws:default_config()). + +-spec put_secret_value(SecretId :: binary(), ClientRequestToken :: binary(), + Secret :: secret_value(), + Opts :: put_secret_value_options(), + Config :: aws_config()) -> sm_response(). +put_secret_value(SecretId, ClientRequestToken, Secret, Opts, Config) -> + SecretValue = case Secret of + {secret_binary, Val} -> {secret_binary, base64:encode(Val)}; + {secret_string, Val} -> {secret_string, Val} + end, + put_secret_call(SecretId, ClientRequestToken, [SecretValue | Opts], Config). + + +%%------------------------------------------------------------------------------ +%% PutSecretValue +%%------------------------------------------------------------------------------ +%% @doc +%% Note: Use put_secret_value as put_secret_string and put_secret_binary functions are kept for backward compatibility. +%% SM API: +%% [https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_PutSecretValue.html] +%% @end +%%------------------------------------------------------------------------------ +-spec put_secret_string(SecretId :: binary(), ClientRequestToken :: binary(), + SecretString :: binary()) -> sm_response(). +put_secret_string(SecretId, ClientRequestToken, SecretString) -> + put_secret_string(SecretId, ClientRequestToken, SecretString, []). + +-spec put_secret_string(SecretId :: binary(), ClientRequestToken :: binary(), + SecretString :: binary(), Opts :: put_secret_value_options()) -> sm_response(). +put_secret_string(SecretId, ClientRequestToken, SecretString, Opts) -> + put_secret_string(SecretId, ClientRequestToken, SecretString, Opts, erlcloud_aws:default_config()). + +-spec put_secret_string(SecretId :: binary(), ClientRequestToken :: binary(), + SecretString :: binary(), Opts :: put_secret_value_options(), + Config :: aws_config()) -> sm_response(). +put_secret_string(SecretId, ClientRequestToken, SecretString, Opts, Config) -> + Secret = {secret_string, SecretString}, + put_secret_call(SecretId, ClientRequestToken, [Secret | Opts], Config). + +-spec put_secret_binary(SecretId :: binary(), ClientRequestToken :: binary(), + SecretBinary :: binary()) -> sm_response(). +put_secret_binary(SecretId, ClientRequestToken, SecretBinary) -> + put_secret_binary(SecretId, ClientRequestToken, SecretBinary, []). + +-spec put_secret_binary(SecretId :: binary(), ClientRequestToken :: binary(), + SecretBinary :: binary(), Opts :: put_secret_value_options()) -> sm_response(). +put_secret_binary(SecretId, ClientRequestToken, SecretBinary, Opts) -> + put_secret_binary(SecretId, ClientRequestToken, SecretBinary, Opts, erlcloud_aws:default_config()). + +-spec put_secret_binary(SecretId :: binary(), ClientRequestToken :: binary(), + SecretBinary :: binary(), Opts :: put_secret_value_options(), + Config :: aws_config()) -> sm_response(). +put_secret_binary(SecretId, ClientRequestToken, SecretBinary, Opts, Config) -> + Secret = {secret_binary, base64:encode(SecretBinary)}, + put_secret_call(SecretId, ClientRequestToken, [Secret | Opts], Config). + +%%------------------------------------------------------------------------------ +%% RemoveRegionsFromReplication +%%------------------------------------------------------------------------------ +%% @doc +%% For a secret that is replicated to other Regions, deletes the secret replicas from the Regions you specify. +%% SM API: +%% [https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_RemoveRegionsFromReplication.html] +%% @end +%%------------------------------------------------------------------------------ + +-spec remove_regions_from_replication(SecretId :: binary(), + Regions :: list(binary())) -> sm_response(). +remove_regions_from_replication(SecretId, Regions) -> + remove_regions_from_replication(SecretId, Regions, erlcloud_aws:default_config()). + +-spec remove_regions_from_replication(SecretId :: binary(), + Regions :: list(binary()), + Config :: aws_config()) -> sm_response(). +remove_regions_from_replication(SecretId, Regions, Config) -> + Json = [{<<"SecretId">>, SecretId}, {<<"RemoveReplicaRegions">>, Regions}], + sm_request(Config, "secretsmanager.RemoveRegionsFromReplication", Json). + +%%------------------------------------------------------------------------------ +%% ReplicateSecretToRegions +%%------------------------------------------------------------------------------ +%% @doc +%% Replicates the secret to a new Regions. +%% SM API: +%% [https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_ReplicateSecretToRegions.html] +%% @end +%%------------------------------------------------------------------------------ + +-spec replicate_secret_to_regions(SecretId :: binary(), + Regions :: replica_regions()) -> sm_response(). +replicate_secret_to_regions(SecretId, Regions) -> + replicate_secret_to_regions(SecretId, Regions, []). + +-spec replicate_secret_to_regions(SecretId :: binary(), + Regions :: replica_regions(), + Opts :: replicate_secret_to_regions_options()) -> sm_response(). +replicate_secret_to_regions(SecretId, Regions, Opts) -> + replicate_secret_to_regions(SecretId, Regions, Opts, erlcloud_aws:default_config()). + +-spec replicate_secret_to_regions(SecretId :: binary(), + Regions :: replica_regions(), + Opts :: replicate_secret_to_regions_options(), + Config :: aws_config()) -> sm_response(). +replicate_secret_to_regions(SecretId, Regions, Opts, Config) -> + Json = lists:map( + fun + ({force_overwrite_replica_secret, Val}) -> {<<"ForceOverwriteReplicaSecret">>, Val}; + (Other) -> Other + end, + [{<<"SecretId">>, SecretId}, {<<"ReplicaRegions">>, format_replica_regions(Regions)} | Opts]), + sm_request(Config, "secretsmanager.ReplicateSecretToRegions", Json). + +%%------------------------------------------------------------------------------ +%% RestoreSecret +%%------------------------------------------------------------------------------ +%% @doc +%% Cancels the scheduled deletion of a secret by removing the DeletedDate time stamp. +%% You can access a secret again after it has been restored. +%% SM API: +%% [https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_RestoreSecret.html] +%% @end +%%------------------------------------------------------------------------------ + +-spec restore_secret(SecretId :: binary()) -> sm_response(). +restore_secret(SecretId) -> + restore_secret(SecretId, erlcloud_aws:default_config()). + +-spec restore_secret(SecretId :: binary(), Config :: aws_config()) -> sm_response(). +restore_secret(SecretId, Config) -> + Json = [{<<"SecretId">>, SecretId}], + sm_request(Config, "secretsmanager.RestoreSecret", Json). + +%%------------------------------------------------------------------------------ +%% RotateSecret +%%------------------------------------------------------------------------------ +%% @doc +%% Configures and starts the asynchronous process of rotating the secret. +%% SM API: +%% [https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_RotateSecret.html] +%% @end +%%------------------------------------------------------------------------------ + +-spec rotate_secret(SecretId :: binary(), ClientRequestToken :: binary()) -> sm_response(). +rotate_secret(SecretId, ClientRequestToken) -> + rotate_secret(SecretId, ClientRequestToken, []). +-spec rotate_secret(SecretId :: binary(), ClientRequestToken :: binary(), + Opts :: rotate_secret_options()) -> sm_response(). +rotate_secret(SecretId, ClientRequestToken, Opts) -> + rotate_secret(SecretId, ClientRequestToken, Opts, erlcloud_aws:default_config()). +-spec rotate_secret(SecretId :: binary(), ClientRequestToken :: binary(), + Opts :: rotate_secret_options(), + Config :: aws_config()) -> sm_response(). +rotate_secret(SecretId, ClientRequestToken, Opts, Config) -> + Json = lists:map( + fun + ({rotate_immediately, Val}) -> {<<"RotateImmediately">>, Val}; + ({rotation_lambda_arn, Val}) -> {<<"RotationLambdaARN">>, Val}; + ({rotation_rules, Val}) -> {<<"RotationRules">>, format_rotation_rules(Val)}; + (Other) -> Other + end, + [{<<"SecretId">>, SecretId}, {<<"ClientRequestToken">>, ClientRequestToken} | Opts]), + sm_request(Config, "secretsmanager.RotateSecret", Json). + +%%------------------------------------------------------------------------------ +%% StopReplicationToReplicate +%%------------------------------------------------------------------------------ +%% @doc +%% Removes the link between the replica secret and the primary secret and promotes the replica to a primary secret in the replica Region. +%% +%% You must call this operation from the Region in which you want to promote the replica to a primary secret. +%% SM API: +%% [https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_StopReplicationToReplica.html] +%% @end +%%------------------------------------------------------------------------------ + +-spec stop_replication_to_replica(SecretId :: binary()) -> sm_response(). +stop_replication_to_replica(SecretId) -> + stop_replication_to_replica(SecretId, erlcloud_aws:default_config()). +-spec stop_replication_to_replica(SecretId :: binary(), Config :: aws_config()) -> sm_response(). +stop_replication_to_replica(SecretId, Config) -> + Json = [{<<"SecretId">>, SecretId}], + sm_request(Config, "secretsmanager.StopReplicationToReplica", Json). + +%%------------------------------------------------------------------------------ +%% TagResource +%%------------------------------------------------------------------------------ +%% @doc +%% Attaches tags to a secret. Tags consist of a key name and a value. +%% Tags are part of the secret's metadata. They are not associated with specific versions +%% of the secret. This operation appends tags to the existing list of tags. +%% SM API: +%% [https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_TagResource.html] +%% @end +%%------------------------------------------------------------------------------ + +-spec tag_resource(SecretId :: binary(), Tags :: list()) -> sm_response(). +tag_resource(SecretId, Tags) -> + tag_resource(SecretId, Tags, erlcloud_aws:default_config()). +-spec tag_resource(SecretId :: binary(), Tags :: list(), Config :: aws_config()) -> sm_response(). +tag_resource(SecretId, Tags, Config) -> + Json = [{<<"SecretId">>, SecretId}, {<<"Tags">>, format_tags(Tags)}], + sm_request(Config, "secretsmanager.TagResource", Json). + +%%------------------------------------------------------------------------------ +%% UntagResource +%%------------------------------------------------------------------------------ +%% @doc +%% Removes tags from a secret. +%% This operation is idempotent. If a requested tag is not attached to the secret, no error is returned and the secret metadata is unchanged. +%% %% SM API: +%% [https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_UntagResource.html] +%% @end +%%------------------------------------------------------------------------------ + +-spec untag_resource(SecretId :: binary(), TagKeys :: list(binary())) -> sm_response(). +untag_resource(SecretId, TagKeys) -> + untag_resource(SecretId, TagKeys, erlcloud_aws:default_config()). +-spec untag_resource(SecretId :: binary(), TagKeys :: list(binary()), + Config :: aws_config()) -> sm_response(). +untag_resource(SecretId, TagKeys, Config) -> + Json = [{<<"SecretId">>, SecretId}, {<<"TagKeys">>, TagKeys}], + sm_request(Config, "secretsmanager.UntagResource", Json). + +%%------------------------------------------------------------------------------ +%% UpdateSecret +%%------------------------------------------------------------------------------ +%% @doc +%% Modifies the details of a secret, including metadata and the secret value. To change the secret value, +%% you can also use PutSecretValue. +%% +%% To change the rotation configuration of a secret, use RotateSecret instead. +%% +%% It is recommended to avoid calling UpdateSecret at a sustained rate of more than once every 10 minutes. +%% When you call UpdateSecret to update the secret value, Secrets Manager creates a new version of the secret. +%% Secrets Manager removes outdated versions when there are more than 100, but it does not remove versions +%% created less than 24 hours ago. If you update the secret value more than once every 10 minutes, you create +%% more versions than Secrets Manager removes, and you will reach the quota for secret versions. +%% +%% If you include SecretString or SecretBinary to create a new secret version, Secrets Manager automatically +%% moves the staging label AWSCURRENT to the new version. Then it attaches the label AWSPREVIOUS to the version +%% that AWSCURRENT was removed from. +%% +%% If you call this operation with a ClientRequestToken that matches an existing version's VersionId, +%% the operation results in an error. You can't modify an existing version, you can only create a new version. +%% To remove a version, remove all staging labels from it. See UpdateSecretVersionStage. +%% +%% %% SM API: +%% [https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_UpdateSecret.html] +%% @end +%%------------------------------------------------------------------------------ + +-spec update_secret(SecretId :: binary(), ClientRequestToken :: binary()) -> sm_response(). +update_secret(SecretId, ClientRequestToken) -> + update_secret(SecretId, ClientRequestToken, []). +-spec update_secret(SecretId :: binary(), + ClientRequestToken :: binary(), + Opts :: update_secret_options()) -> sm_response(). +update_secret(SecretId, ClientRequestToken, Opts) -> + update_secret(SecretId, ClientRequestToken, Opts, erlcloud_aws:default_config()). +-spec update_secret(SecretId :: binary(), + ClientRequestToken :: binary(), + Opts :: update_secret_options(), + Config :: aws_config()) -> sm_response(). +update_secret(SecretId, ClientRequestToken, Opts, Config) -> + Json = lists:map( + fun + ({secret_binary, Val}) -> {<<"SecretBinary">>, base64:encode(Val)}; + ({secret_string, Val}) -> {<<"SecretString">>, Val}; + ({description, Val}) -> {<<"Description">>, Val}; + ({kms_key_id, Val}) -> {<<"KmsKeyId">>, Val}; + (Other) -> Other + end, + [{<<"SecretId">>, SecretId}, {<<"ClientRequestToken">>, ClientRequestToken} | Opts]), + sm_request(Config, "secretsmanager.UpdateSecret", Json). + +%%------------------------------------------------------------------------------ +%% UpdateSecretVersionStage +%%------------------------------------------------------------------------------ +%% @doc +%% Modifies the staging labels attached to a version of a secret. Secrets Manager uses +%% staging labels to track a version as it progresses through the secret rotation process. +%% Each staging label can be attached to only one version at a time. To add a staging +%% label to a version when it is already attached to another version, Secrets Manager +%% first removes it from the other version first and then attaches it to this one. +%% +%% The staging labels that you specify in the VersionStage parameter are added to the +%% existing list of staging labels for the version. +%% +%% %% SM API: +%% [https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_UpdateSecretVersionStage.html] +%% @end +%%------------------------------------------------------------------------------ + +-spec update_secret_version_stage(SecretId :: binary(), + VersionStage :: binary()) -> sm_response(). +update_secret_version_stage(SecretId, VersionStage) -> + update_secret_version_stage(SecretId, VersionStage, []). +-spec update_secret_version_stage(SecretId :: binary(), + VersionStage :: binary(), + Opts :: update_secret_version_stage_options()) -> sm_response(). +update_secret_version_stage(SecretId, VersionStage, Opts) -> + update_secret_version_stage(SecretId, VersionStage, Opts, erlcloud_aws:default_config()). +-spec update_secret_version_stage(SecretId :: binary(), + VersionStage :: binary(), + Opts :: update_secret_version_stage_options(), + Config :: aws_config()) -> sm_response(). +update_secret_version_stage(SecretId, VersionStage, Opts, Config) -> + Json = lists:map( + fun + ({remove_from_version_id, Val}) -> {<<"RemoveFromVersionId">>, Val}; + ({move_to_version_id, Val}) -> {<<"MoveToVersionId">>, Val}; + (Other) -> Other + end, + [{<<"SecretId">>, SecretId}, {<<"VersionStage">>, VersionStage} | Opts]), + sm_request(Config, "secretsmanager.UpdateSecretVersionStage", Json). + +%%------------------------------------------------------------------------------ +%% ValidateResourcePolicy +%%------------------------------------------------------------------------------ +%% @doc +%% Modifies the resource policy attached to a secret. Secrets Manager uses resource +%% policies to manage access to secrets. This operation allows you to add or remove +%% permissions for specific principals (IAM users, roles, etc.) on a secret. +%% +%% %% SM API: +%% [https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_ValidateResourcePolicy.html] +%% @end +%%------------------------------------------------------------------------------ + +-spec validate_resource_policy(ResourcePolicy :: binary()) -> sm_response(). +validate_resource_policy(ResourcePolicy) -> + validate_resource_policy(ResourcePolicy, []). +-spec validate_resource_policy(ResourcePolicy :: binary(), + Opts :: validate_resource_policy_options()) -> sm_response(). +validate_resource_policy(ResourcePolicy, Opts) -> + validate_resource_policy(ResourcePolicy, Opts, erlcloud_aws:default_config()). +-spec validate_resource_policy(ResourcePolicy :: binary(), + Opts :: validate_resource_policy_options(), + Config :: aws_config()) -> sm_response(). +validate_resource_policy(ResourcePolicy, Opts, Config) -> + Json = lists:map( + fun + ({secret_id, Val}) -> {<<"SecretId">>, Val}; + (Other) -> Other + end, + [{<<"ResourcePolicy">>, ResourcePolicy} | Opts]), + sm_request(Config, "secretsmanager.ValidateResourcePolicy", Json). + + +%%%------------------------------------------------------------------------------ +%%% Internal Functions +%%%------------------------------------------------------------------------------ + +create_secret_call(SecretName, ClientRequestToken, Opts, Config) -> + Json = lists:map( + fun + ({add_replica_regions, Val}) -> {<<"AddReplicaRegions">>, format_replica_regions(Val)}; + ({description, Val}) -> {<<"Description">>, Val}; + ({force_overwrite_replica_secret, Val}) -> {<<"ForceOverwriteReplicaSecret">>, Val}; + ({kms_key_id, Val}) -> {<<"KmsKeyId">>, Val}; + ({secret_binary, Val}) -> {<<"SecretBinary">>, Val}; + ({secret_string, Val}) -> {<<"SecretString">>, Val}; + ({tags, Val}) -> {<<"Tags">>, format_tags(Val)}; + (Other) -> Other + end, + [{<<"Name">>, SecretName}, {<<"ClientRequestToken">>, ClientRequestToken} | Opts]), + sm_request(Config, "secretsmanager.CreateSecret", Json). + +put_secret_call(SecretId, ClientRequestToken, Opts, Config) -> + Json = lists:map( + fun + ({rotation_token, Val}) -> {<<"RotationToken">>, Val}; + ({secret_binary, Val}) -> {<<"SecretBinary">>, Val}; + ({secret_string, Val}) -> {<<"SecretString">>, Val}; + ({version_stages, Val}) -> {<<"VersionStages">>, Val}; + (Other) -> Other + end, + [{<<"SecretId">>, SecretId}, {<<"ClientRequestToken">>, ClientRequestToken} | Opts]), + sm_request(Config, "secretsmanager.PutSecretValue", Json). + + +-spec format_replica_regions(ReplicaRegions :: replica_regions()) -> list(). +format_replica_regions(ReplicaRegions) -> + lists:map(fun format_replica_region/1, ReplicaRegions). + +-spec format_replica_region(ReplicaRegion :: replica_region()) -> list(). +format_replica_region(ReplicaRegion) -> + format_replica_region(ReplicaRegion, []). + +-spec format_replica_region(ReplicaRegion :: replica_region(), + Acc :: list()) -> list(). +format_replica_region([{region, Region} | Rest], Acc) -> + format_replica_region(Rest, [{<<"Region">>, Region} | Acc]); +format_replica_region([{kms_key_id, KmsKeyId} | Rest], Acc) -> + format_replica_region(Rest, [{<<"KmsKeyId">>, KmsKeyId} | Acc]); +format_replica_region([Val | Rest], Acc) -> + format_replica_region(Rest, [Val | Acc]); +format_replica_region([], Acc) -> + Acc. + + +-spec format_tags(Tags :: list(proplists:proplist())) -> list(proplists:proplist()). +format_tags(Tags) -> + lists:map(fun format_tag/1, Tags). + +-spec format_tag(Tags :: proplists:proplist()) -> list(). +format_tag(Tags) -> + format_tag(Tags, []). + +-spec format_tag(Tags :: proplists:proplist(), + Acc :: list()) -> list(). +format_tag([{key, Key} | Rest], Acc) -> + format_tag(Rest, [{<<"Key">>, Key} | Acc]); +format_tag([{value, Value} | Rest], Acc) -> + format_tag(Rest, [{<<"Value">>, Value} | Acc]); +format_tag([Val | Rest], Acc) -> + format_tag(Rest, [Val | Acc]); +format_tag([], Acc) -> + Acc. + +-spec format_rotation_rules(RotationRules :: rotation_rules()) -> list(). +format_rotation_rules(RotationRules) -> + lists:map( + fun + ({automatically_after_days, Val}) -> {<<"AutomaticallyAfterDays">>, Val}; + ({duration, Val}) -> {<<"Duration">>, Val}; + ({schedule_expression, Val}) -> {<<"ScheduleExpression">>, Val}; + (Other) -> Other + end, + RotationRules). + + +sm_request(Config, Operation, Body) -> + case erlcloud_aws:update_config(Config) of + {ok, Config1} -> + sm_request_no_update(Config1, Operation, Body); + {error, Reason} -> + {error, Reason} + end. + + +sm_request_no_update(Config, Operation, Body) -> + Payload = jsx:encode(Body), + Headers = headers(Config, Operation, Payload), + Request = #aws_request{service = sm, + uri = uri(Config), + method = post, + request_headers = Headers, + request_body = Payload}, + case erlcloud_aws:request_to_return(erlcloud_retry:request(Config, Request, fun sm_result_fun/1)) of + {ok, {_RespHeaders, <<>>}} -> {ok, []}; + {ok, {_RespHeaders, RespBody}} -> {ok, jsx:decode(RespBody, [{return_maps, false}])}; + {error, _} = Error -> Error + end. + + +headers(Config, Operation, Body) -> + Headers = [{"host", Config#aws_config.sm_host}, + {"x-amz-target", Operation}, + {"content-type", "application/x-amz-json-1.1"}], + Region = erlcloud_aws:aws_region_from_host(Config#aws_config.sm_host), + erlcloud_aws:sign_v4_headers(Config, Headers, Body, Region, "secretsmanager"). + + +uri(#aws_config{sm_scheme = Scheme, sm_host = Host} = Config) -> + lists:flatten([Scheme, Host, port_spec(Config)]). + + +port_spec(#aws_config{sm_port = 443}) -> + ""; +port_spec(#aws_config{sm_port = Port}) -> + [":", erlang:integer_to_list(Port)]. + + +-spec sm_result_fun(Request :: aws_request()) -> aws_request(). +sm_result_fun(#aws_request{response_type = ok} = Request) -> + Request; +sm_result_fun(#aws_request{response_type = error, + error_type = aws, response_status = Status} = Request) when Status >= 500 -> + Request#aws_request{should_retry = true}; +sm_result_fun(#aws_request{response_type = error, error_type = aws} = Request) -> + Request#aws_request{should_retry = false}. diff --git a/src/erlcloud_sns.erl b/src/erlcloud_sns.erl index 092cce902..1ba5f1817 100644 --- a/src/erlcloud_sns.erl +++ b/src/erlcloud_sns.erl @@ -5,9 +5,22 @@ -author('elbrujohalcon@inaka.net'). -export([add_permission/3, add_permission/4, + create_platform_application/2, create_platform_application/3, + create_platform_application/4, create_platform_application/5, + + list_platform_applications/0, list_platform_applications/1, + list_platform_applications/2, list_platform_applications/3, + + get_platform_application_attributes/1, get_platform_application_attributes/2, + set_platform_application_attributes/2, set_platform_application_attributes/3, + create_platform_endpoint/2, create_platform_endpoint/3, create_platform_endpoint/4, create_platform_endpoint/5, create_platform_endpoint/6, + + list_endpoints_by_platform_application/1, list_endpoints_by_platform_application/2, + list_endpoints_by_platform_application/3, list_endpoints_by_platform_application/4, + create_topic/1, create_topic/2, delete_endpoint/1, delete_endpoint/2, delete_endpoint/3, delete_topic/1, delete_topic/2, @@ -24,10 +37,6 @@ list_subscriptions_by_topic_all/1, list_subscriptions_by_topic_all/2, - list_endpoints_by_platform_application/1, - list_endpoints_by_platform_application/2, - list_endpoints_by_platform_application/3, - list_endpoints_by_platform_application/4, get_endpoint_attributes/1, get_endpoint_attributes/2, get_endpoint_attributes/3, @@ -37,8 +46,6 @@ publish_to_topic/5, publish_to_target/2, publish_to_target/3, publish_to_target/4, publish_to_target/5, publish_to_phone/2, publish_to_phone/3, publish_to_phone/4, publish/5, publish/6, - list_platform_applications/0, list_platform_applications/1, - list_platform_applications/2, list_platform_applications/3, confirm_subscription/1, confirm_subscription/2, confirm_subscription/3, confirm_subscription2/2, confirm_subscription2/3, confirm_subscription2/4, set_topic_attributes/3, set_topic_attributes/4, @@ -102,7 +109,12 @@ -type sns_application_attribute() :: event_endpoint_created | event_endpoint_deleted | event_endpoint_updated - | event_delivery_failure. + | event_delivery_failure + | platform_credential + | platform_principal + | success_feedback_role_arn + | failure_feedback_role_arn + | success_feedback_sample_rate. -type sns_application() :: [{arn, string()} | {attributes, [{arn|sns_application_attribute(), string()}]}]. -type(sns_topic_attribute_name () :: 'Policy' | 'DisplayName' | 'DeliveryPolicy'). @@ -114,12 +126,12 @@ -export_type([sns_acl/0, sns_endpoint_attribute/0, sns_message_attributes/0, sns_message/0, sns_application/0, sns_endpoint/0]). --spec add_permission(string(), string(), sns_acl()) -> ok | no_return(). +-spec add_permission(string(), string(), sns_acl()) -> ok. add_permission(TopicArn, Label, Permissions) -> add_permission(TopicArn, Label, Permissions, default_config()). --spec add_permission(string(), string(), sns_acl(), aws_config()) -> ok | no_return(). +-spec add_permission(string(), string(), sns_acl(), aws_config()) -> ok. add_permission(TopicArn, Label, Permissions, Config) when is_list(TopicArn), is_list(Label), length(Label) =< 80, @@ -132,19 +144,19 @@ add_permission(TopicArn, Label, Permissions, Config) --spec create_platform_endpoint(string(), string()) -> string() | no_return(). +-spec create_platform_endpoint(string(), string()) -> string(). create_platform_endpoint(PlatformApplicationArn, Token) -> create_platform_endpoint(PlatformApplicationArn, Token, ""). --spec create_platform_endpoint(string(), string(), string()) -> string() | no_return(). +-spec create_platform_endpoint(string(), string(), string()) -> string(). create_platform_endpoint(PlatformApplicationArn, Token, CustomUserData) -> create_platform_endpoint(PlatformApplicationArn, Token, CustomUserData, []). --spec create_platform_endpoint(string(), string(), string(), [{sns_endpoint_attribute(), string()}]) -> string() | no_return(). +-spec create_platform_endpoint(string(), string(), string(), [{sns_endpoint_attribute(), string()}]) -> string(). create_platform_endpoint(PlatformApplicationArn, Token, CustomUserData, Attributes) -> create_platform_endpoint(PlatformApplicationArn, Token, CustomUserData, Attributes, default_config()). --spec create_platform_endpoint(string(), string(), string(), [{sns_endpoint_attribute(), string()}], aws_config()) -> string() | no_return(). +-spec create_platform_endpoint(string(), string(), string(), [{sns_endpoint_attribute(), string()}], aws_config()) -> string(). create_platform_endpoint(PlatformApplicationArn, Token, CustomUserData, Attributes, Config) -> Doc = sns_xml_request( @@ -157,43 +169,95 @@ create_platform_endpoint(PlatformApplicationArn, Token, CustomUserData, Attribut erlcloud_xml:get_text( "CreatePlatformEndpointResult/EndpointArn", Doc). --spec create_platform_endpoint(string(), string(), string(), [{sns_endpoint_attribute(), string()}], string(), string()) -> string() | no_return(). +-spec create_platform_endpoint(string(), string(), string(), [{sns_endpoint_attribute(), string()}], string(), string()) -> string(). create_platform_endpoint(PlatformApplicationArn, Token, CustomUserData, Attributes, AccessKeyID, SecretAccessKey) -> create_platform_endpoint(PlatformApplicationArn, Token, CustomUserData, Attributes, new_config(AccessKeyID, SecretAccessKey)). +-spec create_platform_application(string(), string()) -> string(). +create_platform_application(Name, Platform) -> + create_platform_application(Name, Platform, []). + +-spec create_platform_application(string(), string(), [{sns_endpoint_attribute(), string()}]) -> string(). +create_platform_application(Name, Platform, Attributes) -> + create_platform_application(Name, Platform, Attributes, default_config()). + +-spec create_platform_application(string(), string(), [{sns_endpoint_attribute(), string()}], aws_config()) -> string(). +create_platform_application(Name, Platform, Attributes, Config) -> + Doc = + sns_xml_request( + Config, "CreatePlatformApplication", + [{"Name", Name}, + {"Platform", Platform} + | encode_attributes(Attributes) + ]), + erlcloud_xml:get_text( + "CreatePlatformApplicationResult/PlatformApplicationArn", Doc). + +-spec create_platform_application(string(), string(), [{sns_endpoint_attribute(), string()}], string(), string()) -> string(). +create_platform_application(Name, Platform, Attributes, AccessKeyID, SecretAccessKey) -> + create_platform_application(Name, Platform, Attributes, new_config(AccessKeyID, SecretAccessKey)). + + +-spec get_platform_application_attributes(string()) -> sns_application(). +get_platform_application_attributes(PlatformApplicationArn) -> + get_platform_application_attributes(PlatformApplicationArn, default_config()). + +-spec get_platform_application_attributes(string(), aws_config()) -> sns_application(). +get_platform_application_attributes(PlatformApplicationArn, Config) -> + Params = [{"PlatformApplicationArn", PlatformApplicationArn}], + Doc = sns_xml_request(Config, "GetPlatformApplicationAttributes", Params), + Decoded = + erlcloud_xml:decode( + [{attributes, "GetPlatformApplicationAttributesResult/Attributes/entry", + fun extract_attribute/1 + }], + Doc), + [{arn, PlatformApplicationArn} | Decoded]. + + +-spec set_platform_application_attributes(string(), [{sns_application_attribute(), string()}]) -> string(). +set_platform_application_attributes(PlatformApplicationArn, Attributes) -> + set_platform_application_attributes(PlatformApplicationArn, Attributes, default_config()). + +-spec set_platform_application_attributes(string(), [{sns_application_attribute(), string()}], aws_config()) -> string(). +set_platform_application_attributes(PlatformApplicationArn, Attributes, Config) -> + Params = [{"PlatformApplicationArn", PlatformApplicationArn}] ++ encode_attributes(Attributes), + Doc = sns_xml_request(Config, "SetPlatformApplicationAttributes", Params), + erlcloud_xml:get_text("ResponseMetadata/RequestId", Doc). + --spec create_topic(string()) -> string() | no_return(). +-spec create_topic(string()) -> string(). create_topic(TopicName) -> create_topic(TopicName, default_config()). --spec create_topic(string(), aws_config()) -> string() | no_return(). +-spec create_topic(string(), aws_config()) -> string(). create_topic(TopicName, Config) when is_record(Config, aws_config) -> Doc = sns_xml_request(Config, "CreateTopic", [{"Name", TopicName}]), erlcloud_xml:get_text("/CreateTopicResponse/CreateTopicResult/TopicArn", Doc). --spec confirm_subscription(sns_event()) -> string() | no_return(). +-spec confirm_subscription(sns_event()) -> string(). confirm_subscription(SnsEvent) -> confirm_subscription(SnsEvent, default_config()). --spec confirm_subscription(sns_event(), aws_config()) -> string() | no_return(). +-spec confirm_subscription(sns_event(), aws_config()) -> string(). confirm_subscription(SnsEvent, Config) -> Token = binary_to_list(proplists:get_value(<<"Token">>, SnsEvent, <<>>)), TopicArn = binary_to_list(proplists:get_value(<<"TopicArn">>, SnsEvent, <<>>)), confirm_subscription2(Token, TopicArn, Config). --spec confirm_subscription(sns_event(), string(), string()) -> string() | no_return(). +-spec confirm_subscription(sns_event(), string(), string()) -> string(). confirm_subscription(SnsEvent, AccessKeyID, SecretAccessKey) -> confirm_subscription(SnsEvent, new_config(AccessKeyID, SecretAccessKey)). --spec confirm_subscription2(string(), string()) -> string() | no_return(). +-spec confirm_subscription2(string(), string()) -> string(). confirm_subscription2(Token, TopicArn) -> confirm_subscription2(Token, TopicArn, default_config()). --spec confirm_subscription2(string(), string(), aws_config()) -> string() | no_return(). +-spec confirm_subscription2(string(), string(), aws_config()) -> string(). confirm_subscription2(Token, TopicArn, Config) -> Doc = sns_xml_request( @@ -204,42 +268,42 @@ confirm_subscription2(Token, TopicArn, Config) -> erlcloud_xml:get_text( "ConfirmSubscriptionResult/SubscriptionArn", Doc). --spec confirm_subscription2(string(), string(), string(), string()) -> string() | no_return(). +-spec confirm_subscription2(string(), string(), string(), string()) -> string(). confirm_subscription2(Token, TopicArn, AccessKeyID, SecretAccessKey) -> confirm_subscription2(Token, TopicArn, new_config(AccessKeyID, SecretAccessKey)). --spec delete_endpoint(string()) -> ok | no_return(). +-spec delete_endpoint(string()) -> ok. delete_endpoint(EndpointArn) -> delete_endpoint(EndpointArn, default_config()). --spec delete_endpoint(string(), aws_config()) -> ok | no_return(). +-spec delete_endpoint(string(), aws_config()) -> ok. delete_endpoint(EndpointArn, Config) -> sns_simple_request(Config, "DeleteEndpoint", [{"EndpointArn", EndpointArn}]). --spec delete_endpoint(string(), string(), string()) -> ok | no_return(). +-spec delete_endpoint(string(), string(), string()) -> ok. delete_endpoint(EndpointArn, AccessKeyID, SecretAccessKey) -> delete_endpoint(EndpointArn, new_config(AccessKeyID, SecretAccessKey)). --spec delete_topic(string()) -> ok | no_return(). +-spec delete_topic(string()) -> ok. delete_topic(TopicArn) -> delete_topic(TopicArn, default_config()). --spec delete_topic(string(), aws_config()) -> ok | no_return(). +-spec delete_topic(string(), aws_config()) -> ok. delete_topic(TopicArn, Config) when is_record(Config, aws_config) -> sns_simple_request(Config, "DeleteTopic", [{"TopicArn", TopicArn}]). --spec get_endpoint_attributes(string()) -> sns_endpoint() | no_return(). +-spec get_endpoint_attributes(string()) -> sns_endpoint(). get_endpoint_attributes(EndpointArn) -> get_endpoint_attributes(EndpointArn, default_config()). --spec get_endpoint_attributes(string(), aws_config()) -> sns_endpoint() | no_return(). +-spec get_endpoint_attributes(string(), aws_config()) -> sns_endpoint(). get_endpoint_attributes(EndpointArn, Config) -> Params = [{"EndpointArn", EndpointArn}], Doc = sns_xml_request(Config, "GetEndpointAttributes", Params), @@ -251,16 +315,16 @@ get_endpoint_attributes(EndpointArn, Config) -> Doc), [{arn, EndpointArn} | Decoded]. --spec get_endpoint_attributes(string(), string(), string()) -> sns_endpoint() | no_return(). +-spec get_endpoint_attributes(string(), string(), string()) -> sns_endpoint(). get_endpoint_attributes(EndpointArn, AccessKeyID, SecretAccessKey) -> get_endpoint_attributes(EndpointArn, new_config(AccessKeyID, SecretAccessKey)). --spec set_endpoint_attributes(string(), [{sns_endpoint_attribute(), string()}]) -> string() | no_return(). +-spec set_endpoint_attributes(string(), [{sns_endpoint_attribute(), string()}]) -> string(). set_endpoint_attributes(EndpointArn, Attributes) -> set_endpoint_attributes(EndpointArn, Attributes, default_config()). --spec set_endpoint_attributes(string(), [{sns_endpoint_attribute(), string()}], aws_config()) -> string() | no_return(). +-spec set_endpoint_attributes(string(), [{sns_endpoint_attribute(), string()}], aws_config()) -> string(). set_endpoint_attributes(EndpointArn, Attributes, Config) -> Doc = sns_xml_request(Config, "SetEndpointAttributes", [{"EndpointArn", EndpointArn} | encode_attributes(Attributes)]), @@ -268,13 +332,13 @@ set_endpoint_attributes(EndpointArn, Attributes, Config) -> --spec list_endpoints_by_platform_application(string()) -> [{endpoints, [sns_endpoint()]} | {next_token, string()}] | no_return(). +-spec list_endpoints_by_platform_application(string()) -> [{endpoints, [sns_endpoint()]} | {next_token, string()}]. list_endpoints_by_platform_application(PlatformApplicationArn) -> list_endpoints_by_platform_application(PlatformApplicationArn, undefined). --spec list_endpoints_by_platform_application(string(), undefined|string()) -> [{endpoints, [sns_endpoint()]} | {next_token, string()}] | no_return(). +-spec list_endpoints_by_platform_application(string(), undefined|string()) -> [{endpoints, [sns_endpoint()]} | {next_token, string()}]. list_endpoints_by_platform_application(PlatformApplicationArn, NextToken) -> list_endpoints_by_platform_application(PlatformApplicationArn, NextToken, default_config()). --spec list_endpoints_by_platform_application(string(), undefined|string(), aws_config()) -> [{endpoints, [sns_endpoint()]} | {next_token, string()}] | no_return(). +-spec list_endpoints_by_platform_application(string(), undefined|string(), aws_config()) -> [{endpoints, [sns_endpoint()]} | {next_token, string()}]. list_endpoints_by_platform_application(PlatformApplicationArn, NextToken, Config) -> Params = case NextToken of @@ -290,21 +354,21 @@ list_endpoints_by_platform_application(PlatformApplicationArn, NextToken, Config {next_token, "ListEndpointsByPlatformApplicationResult/NextToken", text}], Doc), Decoded. --spec list_endpoints_by_platform_application(string(), undefined|string(), string(), string()) -> [{endpoints, [sns_endpoint()]} | {next_token, string()}] | no_return(). +-spec list_endpoints_by_platform_application(string(), undefined|string(), string(), string()) -> [{endpoints, [sns_endpoint()]} | {next_token, string()}]. list_endpoints_by_platform_application(PlatformApplicationArn, NextToken, AccessKeyID, SecretAccessKey) -> list_endpoints_by_platform_application(PlatformApplicationArn, NextToken, new_config(AccessKeyID, SecretAccessKey)). --spec list_topics() -> [{topics, [[{arn, string()}]]} | {next_token, string()}] | no_return(). +-spec list_topics() -> [{topics, [[{arn, string()}]]} | {next_token, string()}]. list_topics() -> list_topics(default_config()). --spec list_topics(undefined | string() | aws_config()) -> [{topics, [[{arn, string()}]]} | {next_token, string()}] | no_return(). +-spec list_topics(undefined | string() | aws_config()) -> [{topics, [[{arn, string()}]]} | {next_token, string()}]. list_topics(Config) when is_record(Config, aws_config) -> list_topics(undefined, Config); list_topics(NextToken) -> list_topics(NextToken, default_config()). --spec list_topics(undefined | string(), aws_config()) -> [{topics, [[{arn, string()}]]} | {next_token, string()}] | no_return(). +-spec list_topics(undefined | string(), aws_config()) -> [{topics, [[{arn, string()}]]} | {next_token, string()}]. list_topics(NextToken, Config) -> Params = case NextToken of @@ -321,11 +385,11 @@ list_topics(NextToken, Config) -> Doc), Decoded. --spec list_topics_all() -> [[{arn, string()}]] | no_return(). +-spec list_topics_all() -> [[{arn, string()}]]. list_topics_all() -> list_topics_all(default_config()). --spec list_topics_all(aws_config()) -> [[{arn, string()}]] | no_return(). +-spec list_topics_all(aws_config()) -> [[{arn, string()}]]. list_topics_all(Config) -> list_all(fun list_topics/2, topics, Config, undefined, []). @@ -370,18 +434,18 @@ list_subscriptions_all(Config) -> --spec list_subscriptions_by_topic(string()) -> proplist() | no_return(). +-spec list_subscriptions_by_topic(string()) -> proplist(). list_subscriptions_by_topic(TopicArn) when is_list(TopicArn) -> list_subscriptions_by_topic(TopicArn, default_config()). --spec list_subscriptions_by_topic(string(), string() | aws_config()) -> proplist() | no_return(). +-spec list_subscriptions_by_topic(string(), string() | aws_config()) -> proplist(). list_subscriptions_by_topic(TopicArn, Config) when is_record(Config, aws_config) -> list_subscriptions_by_topic(TopicArn, undefined, Config); list_subscriptions_by_topic(TopicArn, NextToken) when is_list(NextToken) -> list_subscriptions_by_topic(TopicArn, NextToken, default_config()). --spec list_subscriptions_by_topic(string(), undefined | string(), aws_config()) -> proplist() | no_return(). +-spec list_subscriptions_by_topic(string(), undefined | string(), aws_config()) -> proplist(). list_subscriptions_by_topic(TopicArn, NextToken, Config) when is_record(Config, aws_config) -> Params = case NextToken of @@ -398,11 +462,11 @@ list_subscriptions_by_topic(TopicArn, NextToken, Config) when is_record(Config, Doc), Decoded. --spec list_subscriptions_by_topic_all(string()) -> proplist() | no_return(). +-spec list_subscriptions_by_topic_all(string()) -> proplist(). list_subscriptions_by_topic_all(TopicArn) -> list_subscriptions_by_topic_all(TopicArn, default_config()). --spec list_subscriptions_by_topic_all(string(), aws_config()) -> proplist() | no_return(). +-spec list_subscriptions_by_topic_all(string(), aws_config()) -> proplist(). list_subscriptions_by_topic_all(TopicArn, Config) -> list_all(fun (Token, Cfg) -> list_subscriptions_by_topic(TopicArn, Token, Cfg) end, @@ -410,15 +474,15 @@ list_subscriptions_by_topic_all(TopicArn, Config) -> --spec list_platform_applications() -> [sns_application()] | no_return(). +-spec list_platform_applications() -> [sns_application()]. list_platform_applications() -> list_platform_applications(undefined). --spec list_platform_applications(undefined|string()) -> [sns_application()] | no_return(). +-spec list_platform_applications(undefined|string()) -> [sns_application()]. list_platform_applications(NextToken) -> list_platform_applications(NextToken, default_config()). --spec list_platform_applications(undefined|string(), aws_config()) -> [sns_application()] | no_return(). +-spec list_platform_applications(undefined|string(), aws_config()) -> [sns_application()]. list_platform_applications(NextToken, Config) -> Params = case NextToken of @@ -434,54 +498,54 @@ list_platform_applications(NextToken, Config) -> Doc), proplists:get_value(applications, Decoded, []). --spec list_platform_applications(undefined|string(), string(), string()) -> [sns_application()] | no_return(). +-spec list_platform_applications(undefined|string(), string(), string()) -> [sns_application()]. list_platform_applications(NextToken, AccessKeyID, SecretAccessKey) -> list_platform_applications(NextToken, new_config(AccessKeyID, SecretAccessKey)). --spec publish_to_topic(string(), sns_message()) -> string() | no_return(). +-spec publish_to_topic(string(), sns_message()) -> string(). publish_to_topic(TopicArn, Message) -> publish_to_topic(TopicArn, Message, undefined). --spec publish_to_topic(string(), sns_message(), undefined|string()) -> string() | no_return(). +-spec publish_to_topic(string(), sns_message(), undefined|string()) -> string(). publish_to_topic(TopicArn, Message, Subject) -> publish_to_topic(TopicArn, Message, Subject, default_config()). --spec publish_to_topic(string(), sns_message(), undefined|string(), aws_config()) -> string() | no_return(). +-spec publish_to_topic(string(), sns_message(), undefined|string(), aws_config()) -> string(). publish_to_topic(TopicArn, Message, Subject, Config) -> publish(topic, TopicArn, Message, Subject, Config). --spec publish_to_topic(string(), sns_message(), undefined|string(), string(), string()) -> string() | no_return(). +-spec publish_to_topic(string(), sns_message(), undefined|string(), string(), string()) -> string(). publish_to_topic(TopicArn, Message, Subject, AccessKeyID, SecretAccessKey) -> publish_to_topic(TopicArn, Message, Subject, new_config(AccessKeyID, SecretAccessKey)). --spec publish_to_target(string(), sns_message()) -> string() | no_return(). +-spec publish_to_target(string(), sns_message()) -> string(). publish_to_target(TargetArn, Message) -> publish_to_target(TargetArn, Message, undefined). --spec publish_to_target(string(), sns_message(), undefined|string()) -> string() | no_return(). +-spec publish_to_target(string(), sns_message(), undefined|string()) -> string(). publish_to_target(TargetArn, Message, Subject) -> publish_to_target(TargetArn, Message, Subject, default_config()). --spec publish_to_target(string(), sns_message(), undefined|string(), aws_config()) -> string() | no_return(). +-spec publish_to_target(string(), sns_message(), undefined|string(), aws_config()) -> string(). publish_to_target(TargetArn, Message, Subject, Config) -> publish(target, TargetArn, Message, Subject, Config). --spec publish_to_target(string(), sns_message(), undefined|string(), string(), string()) -> string() | no_return(). +-spec publish_to_target(string(), sns_message(), undefined|string(), string(), string()) -> string(). publish_to_target(TargetArn, Message, Subject, AccessKeyID, SecretAccessKey) -> publish_to_target(TargetArn, Message, Subject, new_config(AccessKeyID, SecretAccessKey)). %% TargetArn can be a phone number string, e.g. "+55 (11) 9999-7777" --spec publish_to_phone(string(), sns_message()) -> string() | no_return(). +-spec publish_to_phone(string(), sns_message()) -> string(). publish_to_phone(TargetArn, Message) -> publish_to_phone(TargetArn, Message, default_config()). --spec publish_to_phone(string(), sns_message(), aws_config()) -> string() | no_return(). +-spec publish_to_phone(string(), sns_message(), aws_config()) -> string(). publish_to_phone(TargetArn, Message, Config) -> publish(phone, TargetArn, Message, undefined, Config). --spec publish_to_phone(string(), sns_message(), string(), string()) -> string() | no_return(). +-spec publish_to_phone(string(), sns_message(), string(), string()) -> string(). publish_to_phone(TargetArn, Message, AccessKeyID, SecretAccessKey) -> publish(phone, TargetArn, Message, undefined, new_config(AccessKeyID, SecretAccessKey)). @@ -489,11 +553,11 @@ publish_to_phone(TargetArn, Message, AccessKeyID, SecretAccessKey) -> %% Publish API: %% [http://docs.aws.amazon.com/sns/latest/api/API_Publish.html] --spec publish(topic|target|phone, string(), sns_message(), undefined|string(), aws_config()) -> string() | no_return(). +-spec publish(topic|target|phone, string(), sns_message(), undefined|string(), aws_config()) -> string(). publish(Type, RecipientArn, Message, Subject, Config) -> publish(Type, RecipientArn, Message, Subject, [], Config). --spec publish(topic|target|phone, string(), sns_message(), undefined|string(), sns_message_attributes(), aws_config()) -> string() | no_return(). +-spec publish(topic|target|phone, string(), sns_message(), undefined|string(), sns_message_attributes(), aws_config()) -> string(). publish(Type, RecipientArn, Message, Subject, Attributes, Config) -> RecipientParam = case Type of @@ -525,7 +589,7 @@ publish(Type, RecipientArn, Message, Subject, Attributes, Config) -> -spec parse_event(iodata()) -> sns_event(). parse_event(EventSource) -> - jsx:decode(EventSource). + jsx:decode(EventSource, [{return_maps, false}]). -spec get_event_type(sns_event()) -> sns_event_type(). get_event_type(Event) -> @@ -539,7 +603,7 @@ parse_event_message(Event) -> Message = proplists:get_value(<<"Message">>, Event, <<>>), case get_event_type(Event) of subscription_confirmation -> Message; - notification -> jsx:decode(Message) + notification -> jsx:decode(Message, [{return_maps, false}]) end. -spec get_notification_attribute(binary(), sns_notification()) -> sns_application_attribute() | binary(). @@ -555,11 +619,11 @@ get_notification_attribute(Attribute, Notification) -> --spec set_topic_attributes(sns_topic_attribute_name(), string()|binary(), string()) -> ok | no_return(). +-spec set_topic_attributes(sns_topic_attribute_name(), string()|binary(), string()) -> ok. set_topic_attributes(AttributeName, AttributeValue, TopicArn) -> set_topic_attributes(AttributeName, AttributeValue, TopicArn, default_config()). --spec set_topic_attributes(sns_topic_attribute_name(), string()|binary(), string(), aws_config()) -> ok | no_return(). +-spec set_topic_attributes(sns_topic_attribute_name(), string()|binary(), string(), aws_config()) -> ok. set_topic_attributes(AttributeName, AttributeValue, TopicArn, Config) when is_record(Config, aws_config) -> sns_simple_request(Config, "SetTopicAttributes", [ @@ -568,11 +632,11 @@ set_topic_attributes(AttributeName, AttributeValue, TopicArn, Config) {"TopicArn", TopicArn}]). --spec get_topic_attributes (string()) -> [{attributes, [{atom(), string()}]}] | no_return(). +-spec get_topic_attributes (string()) -> [{attributes, [{atom(), string()}]}]. get_topic_attributes(TopicArn) -> get_topic_attributes(TopicArn, default_config()). --spec get_topic_attributes(string(), aws_config()) -> [{attributes, [{atom(), string()}]}] | no_return(). +-spec get_topic_attributes(string(), aws_config()) -> [{attributes, [{atom(), string()}]}]. get_topic_attributes(TopicArn, Config) when is_record(Config, aws_config) -> Params = [{"TopicArn", TopicArn}], @@ -587,11 +651,11 @@ get_topic_attributes(TopicArn, Config) --spec set_subscription_attributes(sns_subscription_attribute_name(), string()|binary(), string()) -> ok | no_return(). +-spec set_subscription_attributes(sns_subscription_attribute_name(), string()|binary(), string()) -> ok. set_subscription_attributes(AttributeName, AttributeValue, SubscriptionArn) -> set_subscription_attributes(AttributeName, AttributeValue, SubscriptionArn, default_config()). --spec set_subscription_attributes(sns_subscription_attribute_name(), string()|binary(), string(), aws_config()) -> ok | no_return(). +-spec set_subscription_attributes(sns_subscription_attribute_name(), string()|binary(), string(), aws_config()) -> ok. set_subscription_attributes(AttributeName, AttributeValue, SubscriptionArn, Config) when is_record(Config, aws_config) -> sns_simple_request(Config, "SetSubscriptionAttributes", [ @@ -600,11 +664,11 @@ set_subscription_attributes(AttributeName, AttributeValue, SubscriptionArn, Conf {"SubscriptionArn", SubscriptionArn}]). --spec get_subscription_attributes (string()) -> [{attributes, [{sns_subscription_attribute_name() | atom(), string()}]}] | no_return(). +-spec get_subscription_attributes (string()) -> [{attributes, [{sns_subscription_attribute_name() | atom(), string()}]}]. get_subscription_attributes(SubscriptionArn) -> get_subscription_attributes(SubscriptionArn, default_config()). --spec get_subscription_attributes(string(), aws_config()) -> [{attributes, [{sns_subscription_attribute_name() | atom(), string()}]}] | no_return(). +-spec get_subscription_attributes(string(), aws_config()) -> [{attributes, [{sns_subscription_attribute_name() | atom(), string()}]}]. get_subscription_attributes(SubscriptionArn, Config) when is_record(Config, aws_config) -> Params = [{"SubscriptionArn", SubscriptionArn}], @@ -617,11 +681,11 @@ get_subscription_attributes(SubscriptionArn, Config) Doc), Decoded. --spec subscribe(string(), sns_subscribe_protocol_type(), string()) -> Arn::string() | no_return(). +-spec subscribe(string(), sns_subscribe_protocol_type(), string()) -> Arn::string(). subscribe(Endpoint, Protocol, TopicArn) -> subscribe(Endpoint, Protocol, TopicArn, default_config()). --spec subscribe(string(), sns_subscribe_protocol_type(), string(), aws_config()) -> Arn::string() | no_return(). +-spec subscribe(string(), sns_subscribe_protocol_type(), string(), aws_config()) -> Arn::string(). subscribe(Endpoint, Protocol, TopicArn, Config) when is_record(Config, aws_config) -> Doc = sns_xml_request(Config, "Subscribe", [ @@ -630,11 +694,11 @@ subscribe(Endpoint, Protocol, TopicArn, Config) {"TopicArn", TopicArn}]), erlcloud_xml:get_text("/SubscribeResponse/SubscribeResult/SubscriptionArn", Doc). --spec unsubscribe(string()) -> ok | no_return(). +-spec unsubscribe(string()) -> ok. unsubscribe(SubArn) -> unsubscribe(SubArn, default_config()). --spec unsubscribe(string(), aws_config()) -> ok | no_return(). +-spec unsubscribe(string(), aws_config()) -> ok. unsubscribe(SubArn, Config) when is_record(Config, aws_config) -> sns_simple_request(Config, "Unsubscribe", [{"SubscriptionArn", SubArn}]). @@ -682,7 +746,15 @@ encode_attributes(Attributes) -> {Prefix ++ ".value", Value} | Acc] end, [], lists:zip(lists:seq(1, length(Attributes)), Attributes)). -encode_attribute_name(custom_user_data) -> "CustomUserData"; +encode_attribute_name(platform_credential) -> "PlatformCredential"; +encode_attribute_name(platform_principal) -> "PlatformPrincipal"; +encode_attribute_name(event_endpoint_created) -> "EventEndpointCreated"; +encode_attribute_name(event_endpoint_deleted) -> "EventEndpointDeleted"; +encode_attribute_name(event_endpoint_updated) -> "EventEndpointUpdated"; +encode_attribute_name(event_delivery_failure) -> "EventDeliveryFailure"; +encode_attribute_name(success_feedback_role_arn) -> "SuccessFeedbackRoleArn"; +encode_attribute_name(failure_feedback_role_arn) -> "FailureFeedbackRoleArn"; +encode_attribute_name(success_feedback_sample_rate) -> "SuccessFeedbackSampleRate"; encode_attribute_name(enabled) -> "Enabled"; encode_attribute_name(token) -> "Token". @@ -844,7 +916,7 @@ format_attribute_field(Num, {Key, Value}) -> -spec fields_for_attribute(string(), string() | binary() | number()) -> [{string(), string()}]. fields_for_attribute(Name, Value) -> - [{"Key", Name} | fields_for_attribute(Value)]. + [{"Name", Name} | fields_for_attribute(Value)]. -spec fields_for_attribute(string() | binary() | number()) -> [{string(), string()}]. fields_for_attribute(Value) when is_list(Value) -> diff --git a/src/erlcloud_sqs.erl b/src/erlcloud_sqs.erl index f4a0cfc6e..19a906761 100644 --- a/src/erlcloud_sqs.erl +++ b/src/erlcloud_sqs.erl @@ -12,6 +12,7 @@ delete_message/2, delete_message/3, delete_queue/1, delete_queue/2, purge_queue/1, purge_queue/2, + get_queue_url/1, get_queue_url/2, get_queue_attributes/1, get_queue_attributes/2, get_queue_attributes/3, list_queues/0, list_queues/1, list_queues/2, receive_message/1, receive_message/2, receive_message/3, receive_message/4, @@ -41,11 +42,14 @@ approximate_first_receive_timestamp | wait_time_seconds | receive_message_wait_time_seconds). --type(sqs_queue_attribute_name() :: all | approximate_number_of_messages | - kms_master_key_id | kms_data_key_reuse_period_seconds | - approximate_number_of_messages_not_visible | visibility_timeout | - created_timestamp | last_modified_timestamp | policy | - queue_arn). + +-type(sqs_queue_attribute_name() :: all | approximate_number_of_messages | approximate_number_of_messages_not_visible | + approximate_number_of_messages_delayed | created_timestamp | delay_seconds | + last_modified_timestamp | maximum_message_size | message_retention_period | + policy | queue_arn | receive_message_wait_time_seconds | redrive_policy | + visibility_timeout | kms_master_key_id | kms_data_key_reuse_period_seconds | + sqs_managed_sse_enabled | fifo_queue | content_cased_deduplication | + deduplication_scope | fifo_throughput_limit). -type(batch_entry() :: {string(), string()} | {string(), string(), [message_attribute()]} @@ -74,11 +78,11 @@ configure(AccessKeyID, SecretAccessKey, Host) -> ok. --spec add_permission(string(), string(), sqs_acl()) -> ok | no_return(). +-spec add_permission(string(), string(), sqs_acl()) -> ok. add_permission(QueueName, Label, Permissions) -> add_permission(QueueName, Label, Permissions, default_config()). --spec add_permission(string(), string(), sqs_acl(), aws_config()) -> ok | no_return(). +-spec add_permission(string(), string(), sqs_acl(), aws_config()) -> ok. add_permission(QueueName, Label, Permissions, Config) when is_list(QueueName), is_list(Label), length(Label) =< 80, @@ -100,48 +104,48 @@ encode_permission({AccountId, Permission}) -> get_queue_attributes -> "GetQueueAttributes" end}. --spec change_message_visibility(string(), string(), 0..43200) -> ok | no_return(). +-spec change_message_visibility(string(), string(), 0..43200) -> ok. change_message_visibility(QueueName, ReceiptHandle, VisibilityTimeout) -> change_message_visibility(QueueName, ReceiptHandle, VisibilityTimeout, default_config()). --spec change_message_visibility(string(), string(), 0..43200, aws_config()) -> ok | no_return(). +-spec change_message_visibility(string(), string(), 0..43200, aws_config()) -> ok. change_message_visibility(QueueName, ReceiptHandle, VisibilityTimeout, Config) -> sqs_simple_request(Config, QueueName, "ChangeMessageVisibility", [{"ReceiptHandle", ReceiptHandle}, {"VisibilityTimeout", VisibilityTimeout}]). --spec create_queue(string()) -> proplist() | no_return(). +-spec create_queue(string()) -> proplist(). create_queue(QueueName) -> create_queue(QueueName, default_config()). --spec create_queue(string(), 0..43200 | none | aws_config()) -> proplist() | no_return(). +-spec create_queue(string(), 0..43200 | none | aws_config()) -> proplist(). create_queue(QueueName, Config) when is_record(Config, aws_config) -> create_queue(QueueName, none, Config); create_queue(QueueName, DefaultVisibilityTimeout) -> create_queue(QueueName, DefaultVisibilityTimeout, default_config()). --spec create_queue(string(), 0..43200 | none, aws_config()) -> proplist() | no_return(). +-spec create_queue(string(), 0..43200 | none, aws_config()) -> proplist(). create_queue(QueueName, DefaultVisibilityTimeout, Config) -> create_queue_impl(QueueName, DefaultVisibilityTimeout, Config, []). --spec create_fifo_queue(string()) -> proplist() | no_return(). +-spec create_fifo_queue(string()) -> proplist(). create_fifo_queue(QueueName) -> create_fifo_queue(QueueName, default_config()). --spec create_fifo_queue(string(), 0..43200 | none | aws_config()) -> proplist() | no_return(). +-spec create_fifo_queue(string(), 0..43200 | none | aws_config()) -> proplist(). create_fifo_queue(QueueName, Config) when is_record(Config, aws_config) -> create_fifo_queue(QueueName, none, Config); create_fifo_queue(QueueName, DefaultVisibilityTimeout) -> create_fifo_queue(QueueName, DefaultVisibilityTimeout, default_config()). --spec create_fifo_queue(string(), 0..43200 | none, aws_config()) -> proplist() | no_return(). +-spec create_fifo_queue(string(), 0..43200 | none, aws_config()) -> proplist(). create_fifo_queue(QueueName, DefaultVisibilityTimeout, Config) -> Attributes = erlcloud_aws:param_list([[{"Name", "FifoQueue"}, {"Value", true}]], "Attribute"), create_queue_impl(QueueName, DefaultVisibilityTimeout, Config, Attributes). --spec create_queue_impl(string(), 0..43200 | none, aws_config(), proplists:proplist()) -> proplist() | no_return(). +-spec create_queue_impl(string(), 0..43200 | none, aws_config(), proplists:proplist()) -> proplist(). create_queue_impl(QueueName, DefaultVisibilityTimeout, Config, Attributes) when is_list(QueueName), (is_integer(DefaultVisibilityTimeout) andalso @@ -158,46 +162,60 @@ create_queue_impl(QueueName, DefaultVisibilityTimeout, Config, Attributes) Doc ). --spec delete_message(string(), string()) -> ok | no_return(). +-spec delete_message(string(), string()) -> ok. delete_message(QueueName, ReceiptHandle) -> delete_message(QueueName, ReceiptHandle, default_config()). --spec delete_message(string(), string(), aws_config()) -> ok | no_return(). +-spec delete_message(string(), string(), aws_config()) -> ok. delete_message(QueueName, ReceiptHandle, Config) when is_list(QueueName), is_list(ReceiptHandle), is_record(Config, aws_config) -> sqs_simple_request(Config, QueueName, "DeleteMessage", [{"ReceiptHandle", ReceiptHandle}]). --spec delete_queue(string()) -> ok | no_return(). +-spec delete_queue(string()) -> ok. delete_queue(QueueName) -> delete_queue(QueueName, default_config()). --spec delete_queue(string(), aws_config()) -> ok | no_return(). +-spec delete_queue(string(), aws_config()) -> ok. delete_queue(QueueName, Config) when is_list(QueueName), is_record(Config, aws_config) -> sqs_simple_request(Config, QueueName, "DeleteQueue", []). --spec purge_queue(string()) -> ok | no_return(). +-spec purge_queue(string()) -> ok. purge_queue(QueueName) -> purge_queue(QueueName, default_config()). --spec purge_queue(string(), aws_config()) -> ok | no_return(). +-spec purge_queue(string(), aws_config()) -> ok. purge_queue(QueueName, Config) when is_list(QueueName), is_record(Config, aws_config) -> sqs_simple_request(Config, QueueName, "PurgeQueue", []). --spec get_queue_attributes(string()) -> proplist() | no_return(). +-spec get_queue_url(string()) -> proplist(). +get_queue_url(QueueName) -> + get_queue_url(QueueName, default_config()). + +-spec get_queue_url(string(), aws_config()) -> proplist(). +get_queue_url(QueueName, Config) -> + Doc = sqs_xml_request(Config, "/", "GetQueueUrl", [{"QueueName", QueueName}]), + erlcloud_xml:decode( + [ + {queue_url, "GetQueueUrlResult/QueueUrl", text} + ], + Doc + ). + +-spec get_queue_attributes(string()) -> proplist(). get_queue_attributes(QueueName) -> get_queue_attributes(QueueName, all). --spec get_queue_attributes(string(), all | [sqs_queue_attribute_name()] | aws_config()) -> proplist() | no_return(). +-spec get_queue_attributes(string(), all | [sqs_queue_attribute_name()] | aws_config()) -> proplist(). get_queue_attributes(QueueName, Config) when is_record(Config, aws_config) -> get_queue_attributes(QueueName, all, Config); get_queue_attributes(QueueName, AttributeNames) -> get_queue_attributes(QueueName, AttributeNames, default_config()). --spec get_queue_attributes(string(), all | [sqs_queue_attribute_name()], aws_config()) -> proplist() | no_return(). +-spec get_queue_attributes(string(), all | [sqs_queue_attribute_name()], aws_config()) -> proplist(). get_queue_attributes(QueueName, all, Config) when is_record(Config, aws_config) -> get_queue_attributes(QueueName, [all], Config); get_queue_attributes(QueueName, AttributeNames, Config) @@ -208,89 +226,110 @@ get_queue_attributes(QueueName, AttributeNames, Config) [{decode_attribute_name(Name), decode_attribute_value(Name, Value)} || {Name, Value} <- Attrs]. -encode_attribute_name(message_retention_period) -> "MessageRetentionPeriod"; -encode_attribute_name(queue_arn) -> "QueueArn"; -encode_attribute_name(maximum_message_size) -> "MaximumMessageSize"; -encode_attribute_name(visibility_timeout) -> "VisibilityTimeout"; +encode_attribute_name(all) -> "All"; encode_attribute_name(approximate_number_of_messages) -> "ApproximateNumberOfMessages"; encode_attribute_name(approximate_number_of_messages_not_visible) -> "ApproximateNumberOfMessagesNotVisible"; encode_attribute_name(approximate_number_of_messages_delayed) -> "ApproximateNumberOfMessagesDelayed"; -encode_attribute_name(last_modified_timestamp) -> "LastModifiedTimestamp"; encode_attribute_name(created_timestamp) -> "CreatedTimestamp"; encode_attribute_name(delay_seconds) -> "DelaySeconds"; -encode_attribute_name(receive_message_wait_time_seconds) -> "ReceiveMessageWaitTimeSeconds"; +encode_attribute_name(last_modified_timestamp) -> "LastModifiedTimestamp"; +encode_attribute_name(maximum_message_size) -> "MaximumMessageSize"; +encode_attribute_name(message_retention_period) -> "MessageRetentionPeriod"; encode_attribute_name(policy) -> "Policy"; +encode_attribute_name(queue_arn) -> "QueueArn"; +encode_attribute_name(receive_message_wait_time_seconds) -> "ReceiveMessageWaitTimeSeconds"; encode_attribute_name(redrive_policy) -> "RedrivePolicy"; +encode_attribute_name(visibility_timeout) -> "VisibilityTimeout"; +%% The following attributes apply only to server-side-encryption encode_attribute_name(kms_master_key_id) -> "KmsMasterKeyId"; encode_attribute_name(kms_data_key_reuse_period_seconds) -> "KmsDataKeyReusePeriodSeconds"; -encode_attribute_name(all) -> "All". +encode_attribute_name(sqs_managed_sse_enabled) -> "SqsManagedSseEnabled"; +%% The following attributes apply only to FIFO (first-in-first-out) queues +encode_attribute_name(fifo_queue) -> "FifoQueue"; +encode_attribute_name(content_cased_deduplication) -> "ContentBasedDeduplication"; +encode_attribute_name(deduplication_scope) -> "DeduplicationScope"; +encode_attribute_name(fifo_throughput_limit) -> "FifoThroughputLimit". -decode_attribute_name("MessageRetentionPeriod") -> message_retention_period; -decode_attribute_name("QueueArn") -> queue_arn; -decode_attribute_name("MaximumMessageSize") -> maximum_message_size; -decode_attribute_name("VisibilityTimeout") -> visibility_timeout; decode_attribute_name("ApproximateNumberOfMessages") -> approximate_number_of_messages; decode_attribute_name("ApproximateNumberOfMessagesNotVisible") -> approximate_number_of_messages_not_visible; decode_attribute_name("ApproximateNumberOfMessagesDelayed") -> approximate_number_of_messages_delayed; -decode_attribute_name("LastModifiedTimestamp") -> last_modified_timestamp; decode_attribute_name("CreatedTimestamp") -> created_timestamp; decode_attribute_name("DelaySeconds") -> delay_seconds; -decode_attribute_name("ReceiveMessageWaitTimeSeconds") -> receive_message_wait_time_seconds; +decode_attribute_name("LastModifiedTimestamp") -> last_modified_timestamp; +decode_attribute_name("MaximumMessageSize") -> maximum_message_size; +decode_attribute_name("MessageRetentionPeriod") -> message_retention_period; decode_attribute_name("Policy") -> policy; +decode_attribute_name("QueueArn") -> queue_arn; +decode_attribute_name("ReceiveMessageWaitTimeSeconds") -> receive_message_wait_time_seconds; decode_attribute_name("RedrivePolicy") -> redrive_policy; -decode_attribute_name("ContentBasedDeduplication") -> content_based_deduplication; +decode_attribute_name("VisibilityTimeout") -> visibility_timeout; +%% The following attributes apply only to server-side-encryption decode_attribute_name("KmsMasterKeyId") -> kms_master_key_id; decode_attribute_name("KmsDataKeyReusePeriodSeconds") -> kms_data_key_reuse_period_seconds; -decode_attribute_name("FifoQueue") -> fifo_queue. +decode_attribute_name("SqsManagedSseEnabled") -> sqs_managed_sse_enabled; +%% The following attributes apply only to FIFO (first-in-first-out) queues +decode_attribute_name("FifoQueue") -> fifo_queue; +decode_attribute_name("ContentBasedDeduplication") -> content_cased_deduplication; +decode_attribute_name("DeduplicationScope") -> deduplication_scope; +decode_attribute_name("FifoThroughputLimit") -> fifo_throughput_limit; +decode_attribute_name(Name) -> Name. decode_attribute_value("Policy", Value) -> Value; decode_attribute_value("QueueArn", Value) -> Value; decode_attribute_value("RedrivePolicy", Value) -> Value; decode_attribute_value("KmsMasterKeyId", Value) -> Value; +decode_attribute_value("DeduplicationScope", Value) -> Value; +decode_attribute_value("FifoThroughputLimit", Value) -> Value; decode_attribute_value(_, "true") -> true; decode_attribute_value(_, "false") -> false; -decode_attribute_value(_, Value) -> list_to_integer(Value). +decode_attribute_value(_, Value) -> + try + list_to_integer(Value) + catch + error:badarg -> + Value + end. --spec list_queues() -> [string()] | no_return(). +-spec list_queues() -> [string()]. list_queues() -> list_queues(""). --spec list_queues(string() | aws_config()) -> [string()] | no_return(). +-spec list_queues(string() | aws_config()) -> [string()]. list_queues(Config) when is_record(Config, aws_config) -> list_queues("", Config); list_queues(QueueNamePrefix) -> list_queues(QueueNamePrefix, default_config()). --spec list_queues(string(), aws_config()) -> [string()] | no_return(). +-spec list_queues(string(), aws_config()) -> [string()]. list_queues(QueueNamePrefix, Config) when is_list(QueueNamePrefix), is_record(Config, aws_config) -> Doc = sqs_xml_request(Config, "/", "ListQueues", [{"QueueNamePrefix", QueueNamePrefix}]), erlcloud_xml:get_list("ListQueuesResult/QueueUrl", Doc). --spec receive_message(string()) -> proplist() | no_return(). +-spec receive_message(string()) -> proplist(). receive_message(QueueName) -> receive_message(QueueName, default_config()). --spec receive_message(string(), [sqs_msg_attribute_name()] | all | aws_config()) -> proplist() | no_return(). +-spec receive_message(string(), [sqs_msg_attribute_name()] | all | aws_config()) -> proplist(). receive_message(QueueName, Config) when is_record(Config, aws_config) -> receive_message(QueueName, [], Config); receive_message(QueueName, AttributeNames) -> receive_message(QueueName, AttributeNames, default_config()). --spec receive_message(string(), [sqs_msg_attribute_name()] | all, 1..10 | aws_config()) -> proplist() | no_return(). +-spec receive_message(string(), [sqs_msg_attribute_name()] | all, 1..10 | aws_config()) -> proplist(). receive_message(QueueName, AttributeNames, Config) when is_record(Config, aws_config) -> receive_message(QueueName, AttributeNames, 1, Config); receive_message(QueueName, AttributeNames, MaxNumberOfMessages) -> receive_message(QueueName, AttributeNames, MaxNumberOfMessages, default_config()). --spec receive_message(string(), [sqs_msg_attribute_name()] | all, 1..10, 0..43200 | none | aws_config()) -> proplist() | no_return(). +-spec receive_message(string(), [sqs_msg_attribute_name()] | all, 1..10, 0..43200 | none | aws_config()) -> proplist(). receive_message(QueueName, AttributeNames, MaxNumberOfMessages, Config) when is_record(Config, aws_config) -> receive_message(QueueName, AttributeNames, MaxNumberOfMessages, none, Config); @@ -299,7 +338,7 @@ receive_message(QueueName, AttributeNames, MaxNumberOfMessages, VisibilityTimeou VisibilityTimeout, default_config()). -spec receive_message(string(), [sqs_msg_attribute_name()] | all, 1..10, - 0..43200 | none, 0..20 | none | aws_config()) -> proplist() | no_return(). + 0..43200 | none, 0..20 | none | aws_config()) -> proplist(). receive_message(QueueName, AttributeNames, MaxNumberOfMessages, VisibilityTimeout, Config) when is_record(Config, aws_config) -> @@ -311,7 +350,7 @@ receive_message(QueueName, AttributeNames, MaxNumberOfMessages, VisibilityTimeout, WaitTimeSeconds, default_config()). -spec receive_message(string(), [sqs_msg_attribute_name()] | all, 1..10, - 0..43200 | none, 0..20 | none, aws_config()) -> proplist() | no_return(). + 0..43200 | none, 0..20 | none, aws_config()) -> proplist(). receive_message(QueueName, all, MaxNumberOfMessages, VisibilityTimeout, WaitTimeoutSeconds, Config) when is_record(Config, aws_config) -> receive_message(QueueName, [all], MaxNumberOfMessages, @@ -378,7 +417,12 @@ decode_msg_attribute_name(Name) when is_list(Name) -> Name. decode_msg_attribute_value("SenderId", Value) -> Value; decode_msg_attribute_value("MessageGroupId", Value) -> Value; decode_msg_attribute_value("MessageDeduplicationId", Value) -> Value; -decode_msg_attribute_value(_Name, Value) -> list_to_integer(Value). +decode_msg_attribute_value(_Name, Value) -> + try list_to_integer(Value) + catch + _:_ -> + Value + end. decode_messages(Messages) -> [decode_message(Message) || Message <- Messages]. @@ -414,6 +458,8 @@ decode_message_attribute(Attributes) -> end, [F(Attr) || Attr <- Attributes]. +decode_message_attribute_value("Number", Value) -> + list_to_integer(Value); decode_message_attribute_value(["Number", "int"], Value) -> list_to_integer(Value); decode_message_attribute_value(["Number", "int", CustomType], Value) -> @@ -422,16 +468,21 @@ decode_message_attribute_value(["Number", "float"], Value) -> list_to_float(Value); decode_message_attribute_value(["Number", "float", CustomType], Value) -> {CustomType, list_to_float(Value)}; -decode_message_attribute_value(["String"], Value) -> +decode_message_attribute_value("String", Value) -> Value; decode_message_attribute_value(["String", CustomType], Value) -> {CustomType, Value}; -decode_message_attribute_value(["Binary"], Value) -> +decode_message_attribute_value("Binary", Value) -> list_to_binary(Value); decode_message_attribute_value(["Binary", CustomType], Value) -> {CustomType, list_to_binary(Value)}; decode_message_attribute_value(DataType, Value) -> - decode_message_attribute_value(string:tokens(DataType, "."), Value). + case string:tokens(DataType, ".") of + [_Other] -> % check if datatype is something not handled above by Number/String/Binary + erlang:error(decode_message_attribute_value_error, [DataType, Value]); + Parsed -> + decode_message_attribute_value(Parsed, Value) + end. decode_msg_attributes(Attrs) -> [{decode_msg_attribute_name(Name), @@ -441,37 +492,37 @@ decode_attributes(Attrs) -> [{erlcloud_xml:get_text("Name", Attr), erlcloud_xml:get_text("Value", Attr)} || Attr <- Attrs]. --spec remove_permission(string(), string()) -> ok | no_return(). +-spec remove_permission(string(), string()) -> ok. remove_permission(QueueName, Label) -> remove_permission(QueueName, Label, default_config()). --spec remove_permission(string(), string(), aws_config()) -> ok | no_return(). +-spec remove_permission(string(), string(), aws_config()) -> ok. remove_permission(QueueName, Label, Config) when is_list(QueueName), is_list(Label), is_record(Config, aws_config) -> sqs_simple_request(Config, QueueName, "RemovePermission", [{"Label", Label}]). --spec send_message(string(), string()) -> proplist() | no_return(). +-spec send_message(string(), string()) -> proplist(). send_message(QueueName, MessageBody) -> send_message(QueueName, MessageBody, default_config()). --spec send_message(string(), string(), proplists:proplist() | 0..900 | none | aws_config()) -> proplist() | no_return(). +-spec send_message(string(), string(), proplists:proplist() | 0..900 | none | aws_config()) -> proplist(). send_message(QueueName, MessageBody, #aws_config{} = Config) -> send_message(QueueName, MessageBody, none, Config); -send_message(QueueName, MessageBody, DelaySeconds) +send_message(QueueName, MessageBody, DelaySeconds) when ((DelaySeconds >= 0 andalso DelaySeconds =< 900) orelse DelaySeconds =:= none) -> send_message(QueueName, MessageBody, DelaySeconds, default_config()); send_message(QueueName, MessageBody, Opts) when is_list(Opts) -> send_message(QueueName, MessageBody, Opts, default_config()). --spec send_message(string(), string(), proplists:proplist() | 0..900 | none, aws_config()) -> proplist() | no_return(). +-spec send_message(string(), string(), proplists:proplist() | 0..900 | none, aws_config()) -> proplist(). send_message(QueueName, MessageBody, DelaySeconds, Config) when ((DelaySeconds >= 0 andalso DelaySeconds =< 900) orelse DelaySeconds =:= none) -> send_message(QueueName, MessageBody, [{delay_seconds, DelaySeconds}], [], Config); send_message(QueueName, MessageBody, Opts, Config) when is_list(Opts) -> send_message(QueueName, MessageBody, Opts, [], Config). --spec send_message(string(), string(), proplists:proplist() | 0..900 | none, [message_attribute()], aws_config()) -> proplist() | no_return(). +-spec send_message(string(), string(), proplists:proplist() | 0..900 | none, [message_attribute()], aws_config()) -> proplist(). send_message(QueueName, MessageBody, DelaySeconds, MessageAttributes, #aws_config{}=Config) when ((DelaySeconds >= 0 andalso DelaySeconds =< 900) orelse DelaySeconds =:= none) -> send_message(QueueName, MessageBody, [{delay_seconds, DelaySeconds}], MessageAttributes, Config); @@ -489,11 +540,11 @@ send_message(QueueName, MessageBody, Opts, MessageAttributes, #aws_config{}=Conf Doc ). --spec set_queue_attributes(string(), proplists:proplist()) -> ok | no_return(). +-spec set_queue_attributes(string(), proplists:proplist()) -> ok. set_queue_attributes(QueueName, Attributes) -> set_queue_attributes(QueueName, Attributes, default_config()). --spec set_queue_attributes(string(), proplists:proplist(), aws_config()) -> ok | no_return(). +-spec set_queue_attributes(string(), proplists:proplist(), aws_config()) -> ok. set_queue_attributes(QueueName, Attributes, Config) when is_list(QueueName), is_list(Attributes), is_record(Config, aws_config) -> Params = erlcloud_aws:param_list([ @@ -501,18 +552,18 @@ set_queue_attributes(QueueName, Attributes, Config) {"Value", Value}] || {Name, Value} <- Attributes], "Attribute"), sqs_simple_request(Config, QueueName, "SetQueueAttributes", Params). --spec send_message_batch(string(), [batch_entry()]) -> proplist() | no_return(). +-spec send_message_batch(string(), [batch_entry()]) -> proplist(). send_message_batch(QueueName, BatchMessages) -> send_message_batch(QueueName, BatchMessages, default_config()). --spec send_message_batch(string(), [batch_entry()], 0..900 | none | aws_config()) -> proplist() | no_return(). +-spec send_message_batch(string(), [batch_entry()], 0..900 | none | aws_config()) -> proplist(). send_message_batch(QueueName, BatchMessages, Config) when is_record(Config, aws_config) -> send_message_batch(QueueName, BatchMessages, none, Config); send_message_batch(QueueName, BatchMessages, DelaySeconds) -> send_message_batch(QueueName, BatchMessages, DelaySeconds, default_config()). --spec send_message_batch(string(), [batch_entry()], 0..900 | none, aws_config()) -> proplist() | no_return(). +-spec send_message_batch(string(), [batch_entry()], 0..900 | none, aws_config()) -> proplist(). send_message_batch(QueueName, BatchMessages, DelaySeconds, Config) when is_list(QueueName), is_record(Config, aws_config), (DelaySeconds >= 0 andalso DelaySeconds =< 900) orelse @@ -531,11 +582,11 @@ send_message_batch(QueueName, BatchMessages, DelaySeconds, Config) erlcloud_xml:decode(BatchResponse, Doc). --spec delete_message_batch(string(), [batch_entry()]) -> proplists:proplist() | no_return(). +-spec delete_message_batch(string(), [batch_entry()]) -> proplists:proplist(). delete_message_batch(QueueName, BatchReceiptHandles) -> delete_message_batch(QueueName, BatchReceiptHandles, default_config()). --spec delete_message_batch(string(), [batch_entry()], aws_config()) -> proplists:proplist() | no_return(). +-spec delete_message_batch(string(), [batch_entry()], aws_config()) -> proplists:proplist(). delete_message_batch(QueueName, [{Id, Handle}|_]=BatchReceiptHandles, Config) when is_list(QueueName), is_list(Id), is_list(Handle), is_record(Config, aws_config) -> @@ -553,12 +604,12 @@ delete_message_batch(QueueName, [{Id, Handle}|_]=BatchReceiptHandles, Config) {failed, "DeleteMessageBatchResult/BatchResultErrorEntry", fun decode_batch_result_error/1}], erlcloud_xml:decode(BatchResponse, Doc). --spec change_message_visibility_batch(string(), [batch_entry()], 0..43200) -> proplists:proplist() | no_return(). +-spec change_message_visibility_batch(string(), [batch_entry()], 0..43200) -> proplists:proplist(). change_message_visibility_batch(QueueName, BatchReceiptHandles, VisibilityTimeout) -> change_message_visibility_batch(QueueName, BatchReceiptHandles, VisibilityTimeout, default_config()). --spec change_message_visibility_batch(string(), [batch_entry()], 0..43200, aws_config()) -> proplists:proplist() | no_return(). +-spec change_message_visibility_batch(string(), [batch_entry()], 0..43200, aws_config()) -> proplists:proplist(). change_message_visibility_batch(QueueName, [{Id, Handle}|_]=BatchReceiptHandles, VisibilityTimeout, Config) when is_list(QueueName), is_list(Id), is_list(Handle), is_record(Config, aws_config) -> diff --git a/src/erlcloud_ssm.erl b/src/erlcloud_ssm.erl new file mode 100644 index 000000000..ba15b5227 --- /dev/null +++ b/src/erlcloud_ssm.erl @@ -0,0 +1,620 @@ +%% @doc +%% An Erlang interface to AWS Systems Manager (SSM). +%% +%% Output is in the form of `{ok, Value}' or `{error, Reason}'. The +%% format of `Value' is controlled by the `out' option, which defaults +%% to `json'. The possible values are: +%% +%% * `json' - The output from Systems Manager as processed by `jsx:decode' +%% with no further manipulation. +%% +%% * `record' - A record containing all the information from the +%% Systems Manager. +%% +%% * `map' - Same output of `json` opt but in a map formatting. +%% +%% Systems Manager errors are returned in the form `{error, {ErrorCode, Message}}' +%% where `ErrorCode' and 'Message' are both binary +%% strings. + +%% See the unit tests for additional usage examples beyond what are +%% provided for each function. +%% +%% @end + +-module(erlcloud_ssm). + +-include("erlcloud_aws.hrl"). +-include("erlcloud_ssm.hrl"). + +%%% Library initialization. +-export([configure/2, configure/3, configure/4, new/2, new/3, new/4]). + +-define(API_VERSION, "20150408"). +-define(OUTPUT_CHOICES, [json, record, map]). + +-export([ + get_parameter/1, get_parameter/2, + get_parameters/1, get_parameters/2, + get_parameters_by_path/1, get_parameters_by_path/2, + put_parameter/1, put_parameter/2, + delete_parameter/1, delete_parameter/2 +]). + +-export_type([ + get_parameter_opt/0, get_parameter_opts/0, + get_parameters_by_path_opt/0, get_parameters_by_path_opts/0, + get_parameters_opt/0, get_parameters_opts/0, + put_parameter_opt/0, put_parameter_opts/0, + delete_parameter_opt/0, delete_parameter_opts/0 + ]). + +%%%------------------------------------------------------------------------------ +%% Library initialization. +%%%------------------------------------------------------------------------------ + +-spec new(string(), string()) -> aws_config(). + +new(AccessKeyID, SecretAccessKey) -> + #aws_config{ + access_key_id=AccessKeyID, + secret_access_key=SecretAccessKey + }. + +-spec new(string(), string(), string()) -> aws_config(). + +new(AccessKeyID, SecretAccessKey, Host) -> + #aws_config{ + access_key_id=AccessKeyID, + secret_access_key=SecretAccessKey, + ssm_host=Host + }. + +-spec new(string(), string(), string(), non_neg_integer()) -> aws_config(). + +new(AccessKeyID, SecretAccessKey, Host, Port) -> + #aws_config{ + access_key_id=AccessKeyID, + secret_access_key=SecretAccessKey, + ssm_host=Host, + ssm_port=Port + }. + +-spec configure(string(), string()) -> ok. + +configure(AccessKeyID, SecretAccessKey) -> + put(aws_config, new(AccessKeyID, SecretAccessKey)), + ok. + +-spec configure(string(), string(), string()) -> ok. + +configure(AccessKeyID, SecretAccessKey, Host) -> + put(aws_config, new(AccessKeyID, SecretAccessKey, Host)), + ok. + +-spec configure(string(), string(), string(), non_neg_integer()) -> ok. + +configure(AccessKeyID, SecretAccessKey, Host, Port) -> + put(aws_config, new(AccessKeyID, SecretAccessKey, Host, Port)), + ok. + +default_config() -> + erlcloud_aws:default_config(). + + +%%%------------------------------------------------------------------------------ +%% Shared types +%%%------------------------------------------------------------------------------ + +-type string_param() :: binary() | string(). + +-type json_pair() :: {binary() | atom(), jsx:json_term()}. +-type json_return() :: {ok, jsx:json_term()} | {error, term()}. +-type ssm_return(Record) :: {ok, jsx:json_term() | Record } | {error, term()}. +-type decode_fun() :: fun((jsx:json_term(), decode_opts()) -> tuple()). + +%%%------------------------------------------------------------------------------ +%% Shared Options +%%%------------------------------------------------------------------------------ +-type out_type() :: json | record | map. +-type out_opt() :: {out, out_type()}. +-type property() :: proplists:property(). + +-type aws_opts() :: [json_pair()]. +-type ssm_opts() :: [out_opt()]. +-type opts() :: {aws_opts(), ssm_opts()}. + +-spec verify_ssm_opt(atom(), term()) -> ok. +verify_ssm_opt(out, Value) -> + case lists:member(Value, ?OUTPUT_CHOICES) of + true -> + ok; + false -> + error({erlcloud_ssm, {invalid_opt, {out, Value}}}) + end; +verify_ssm_opt(Name, Value) -> + error({erlcloud_ssm, {invalid_opt, {Name, Value}}}). + +-type opt_table_entry() :: {atom(), binary(), fun((_) -> jsx:json_term())}. +-type opt_table() :: [opt_table_entry()]. +-spec opt_folder(opt_table(), property(), opts()) -> opts(). +opt_folder(_, {_, undefined}, Opts) -> + %% ignore options set to undefined + Opts; +opt_folder(Table, {Name, Value}, {AwsOpts, EcsOpts}) -> + case lists:keyfind(Name, 1, Table) of + {Name, Key, ValueFun} -> + {[{Key, ValueFun(Value)} | AwsOpts], EcsOpts}; + false -> + verify_ssm_opt(Name, Value), + {AwsOpts, [{Name, Value} | EcsOpts]} + end. + +-spec opts(opt_table(), proplist()) -> opts(). +opts(Table, Opts) when is_list(Opts) -> + %% remove duplicate options + Opts1 = lists:ukeysort(1, proplists:unfold(Opts)), + lists:foldl(fun(Opt, A) -> opt_folder(Table, Opt, A) end, {[], []}, Opts1); +opts(_, _) -> + error({erlcloud_ssm, opts_not_list}). + +%%%------------------------------------------------------------------------------ +%% Shared Decoders +%%%------------------------------------------------------------------------------ + +-type decode_opt() :: {typed, boolean()}. +-type decode_opts() :: [decode_opt()]. +-type record_desc() :: {tuple(), field_table()}. + +-spec id(X) -> X. +id(X) -> X. + +-spec id(X, decode_opts()) -> X. +id(X, _) -> X. + +-type field_table() :: [{binary(), pos_integer(), + fun((jsx:json_term(), decode_opts()) -> term())}]. + +-spec decode_folder(field_table(), json_pair(), decode_opts(), tuple()) -> tuple(). +decode_folder(Table, {Key, Value}, Opts, A) -> + case lists:keyfind(Key, 1, Table) of + {Key, Index, ValueFun} -> + setelement(Index, A, ValueFun(Value, Opts)); + false -> + A + end. + + +-spec decode_record(record_desc(), jsx:json_term(), decode_opts()) -> tuple(). +decode_record({Record, _}, [{}], _) -> + %% jsx returns [{}] for empty objects + Record; +decode_record({Record, Table}, Json, Opts) -> + lists:foldl(fun(Pair, A) -> decode_folder(Table, Pair, Opts, A) end, Record, Json). + +%%%------------------------------------------------------------------------------ +%% Output +%%%------------------------------------------------------------------------------ +-spec out(json_return(), decode_fun(), ssm_opts()) + -> {ok, jsx:json_term() | tuple()} | + {simple, term()} | + {error, term()}. +out({error, Reason}, _, _) -> + {error, Reason}; +out({ok, Json}, Decode, Opts) -> + case proplists:get_value(out, Opts, json) of + json -> + {ok, Json}; + record -> + {ok, Decode(Json, [])}; + map -> + {ok, erlcloud_util:proplists_to_map(Json)} + end. + +%%%------------------------------------------------------------------------------ +%% Shared Records +%%%------------------------------------------------------------------------------ + +-spec parameter_record() -> record_desc(). +parameter_record() -> + {#ssm_parameter{}, + [ + {<<"ARN">>, #ssm_parameter.arn, fun id/2}, + {<<"DataType">>, #ssm_parameter.data_type, fun id/2}, + {<<"LastModifiedDate">>, #ssm_parameter.last_modified_date, fun id/2}, + {<<"Name">>, #ssm_parameter.name, fun id/2}, + {<<"Selector">>, #ssm_parameter.selector, fun id/2}, + {<<"SourceResult">>, #ssm_parameter.source_result, fun id/2}, + {<<"Type">>, #ssm_parameter.type, fun id/2}, + {<<"Value">>, #ssm_parameter.value, fun id/2}, + {<<"Version">>, #ssm_parameter.version, fun id/2} + ] + }. + +-spec get_parameter_record() -> record_desc(). +get_parameter_record() -> + {#ssm_get_parameter{}, + [ + {<<"Parameter">>, #ssm_get_parameter.parameter, fun decode_parameter/2} + ] + }. + +-spec get_parameters_record() -> record_desc(). +get_parameters_record() -> + {#ssm_get_parameters{}, + [ + {<<"InvalidParameters">>, #ssm_get_parameters.invalid_parameters, fun id/2}, + {<<"Parameters">>, #ssm_get_parameters.parameters, fun decode_parameters/2} + ] + }. + +-spec get_parameters_by_path_record() -> record_desc(). +get_parameters_by_path_record() -> + {#ssm_get_parameters_by_path{}, + [ + {<<"NextToken">>, #ssm_get_parameters_by_path.next_token, fun id/2}, + {<<"Parameters">>, #ssm_get_parameters_by_path.parameters, fun decode_parameters/2} + ] + }. + +-spec put_parameter_record() -> record_desc(). +put_parameter_record() -> + {#ssm_put_parameter{}, + [ + {<<"Tier">>, #ssm_put_parameter.tier, fun id/2}, + {<<"Version">>, #ssm_put_parameter.version, fun id/2} + ] + }. + +decode_parameter(V, Opts) -> + decode_record(parameter_record(), V, Opts). + +decode_parameters(V, Opts) -> + [decode_record(parameter_record(), I, Opts) || I <- V]. + +decode_get_parameter(V, Opts) -> + decode_record(get_parameter_record(), V, Opts). + +decode_get_parameters(V, Opts) -> + decode_record(get_parameters_record(), V, Opts). + +decode_get_parameters_by_path(V, Opts) -> + decode_record(get_parameters_by_path_record(), V, Opts). + +decode_put_parameter(V, Opts) -> + decode_record(put_parameter_record(), V, Opts). + +%%%------------------------------------------------------------------------------ +%% AWS Systems Manager API Functions +%%%------------------------------------------------------------------------------ + +%%%------------------------------------------------------------------------------ +%% GetParameter +%%%------------------------------------------------------------------------------ +-type get_parameter_opt() :: {name, string_param()} | {with_decryption, boolean()} | + out_opt(). +-type get_parameter_opts() :: [get_parameter_opt()]. + +-spec get_parameter_opts() -> opt_table(). +get_parameter_opts() -> + [ + {name, <<"Name">>, fun encode_json_value/1}, + {with_decryption, <<"WithDecryption">>, fun encode_json_value/1} + ]. + +-spec get_parameter(Opts :: get_parameter_opts()) -> ssm_return(#ssm_get_parameter{}). +get_parameter(Opts) -> + get_parameter(Opts, default_config()). + +%%%------------------------------------------------------------------------------ +%% @doc +%% SSM API +%% [https://docs.aws.amazon.com/systems-manager/latest/APIReference/API_GetParameter.html] +%% +%% ===Example=== +%% +%% Get information about a parameter by using the parameter name. +%% +%% ` +%% {ok, Parameter} = erlcloud_ssm:get_parameter([{name, "some_parameter"}, {out, json}]) +%% ' +%% @end +%%%------------------------------------------------------------------------------ +-spec get_parameter(Opts :: get_parameter_opts(), Config :: aws_config()) -> ssm_return(#ssm_get_parameter{}). +get_parameter(Opts, #aws_config{} = Config) -> + {AwsOpts, SSMOpts} = opts(get_parameter_opts(), Opts), + Return = ssm_request( + Config, + "GetParameter", + AwsOpts), + out(Return, fun(Json, UOpts) -> + decode_get_parameter(Json, UOpts) + end, + SSMOpts). + +%%%------------------------------------------------------------------------------ +%% GetParameters +%%%------------------------------------------------------------------------------ +-type get_parameters_opt() :: {names, [string_param()]} | {with_decryption, boolean()} | + out_opt(). +-type get_parameters_opts() :: [get_parameters_opt()]. + +-spec get_parameters_opts() -> opt_table(). +get_parameters_opts() -> + [ + {names, <<"Names">>, fun encode_json_value/1}, + {with_decryption, <<"WithDecryption">>, fun encode_json_value/1} + ]. + +-spec get_parameters(Opts :: get_parameters_opts()) -> ssm_return(#ssm_get_parameters{}). +get_parameters(Opts) -> + get_parameters(Opts, default_config()). + +%%%------------------------------------------------------------------------------ +%% @doc +%% SSM API +%% [https://docs.aws.amazon.com/systems-manager/latest/APIReference/API_GetParameters.html] +%% +%% ===Example=== +%% +%% Get information about parameters by using the parameters' names. +%% +%% ` +%% {ok, Parameter} = erlcloud_ssm:get_parameters([{names, ["some_parameter_1", "some_parameter_2"]}]) +%% ' +%% @end +%%%------------------------------------------------------------------------------ +-spec get_parameters(Opts :: get_parameters_opts(), Config :: aws_config()) -> ssm_return(#ssm_get_parameters{}). +get_parameters(Opts, #aws_config{} = Config) -> + {AwsOpts, SSMOpts} = opts(get_parameters_opts(), Opts), + Return = ssm_request( + Config, + "GetParameters", + AwsOpts), + out(Return, fun(Json, UOpts) -> + decode_get_parameters(Json, UOpts) + end, + SSMOpts). + +%%%------------------------------------------------------------------------------ +%% GetParametersByPath +%%%------------------------------------------------------------------------------ +-type get_parameters_by_path_filter_opt() :: {key, string_param()} | {option, string_param()} | + {values, [string_param()]}. +-type get_parameters_by_path_filter_opts() :: [get_parameters_by_path_filter_opt()]. +-type get_parameters_by_path_opt() :: {max_results, non_neg_integer()} | {next_token, string_param()} | + {parameter_filters, get_parameters_by_path_filter_opts()} | {path, string_param()} | + {recursive, boolean()} | {with_decryption, boolean()} | + out_opt(). +-type get_parameters_by_path_opts() :: [get_parameters_by_path_opt()]. + +-spec get_parameters_by_path_opts() -> opt_table(). +get_parameters_by_path_opts() -> + [ + {max_results, <<"MaxResults">>, fun id/1}, + {next_token, <<"NextToken">>, fun encode_json_value/1}, + {parameter_filters, <<"ParameterFilters">>, fun encode_json_parameter_filters_value/1}, + {path, <<"Path">>, fun encode_json_value/1}, + {recursive, <<"Recursive">>, fun encode_json_value/1}, + {with_decryption, <<"WithDecryption">>, fun encode_json_value/1} + ]. + +-spec get_parameters_by_path(Opts :: get_parameters_by_path_opts()) -> ssm_return(#ssm_get_parameters_by_path{}). +get_parameters_by_path(Opts) -> + get_parameters_by_path(Opts, default_config()). + +%%%------------------------------------------------------------------------------ +%% @doc +%% SSM API +%% [https://docs.aws.amazon.com/systems-manager/latest/APIReference/API_GetParametersByPath.html] +%% +%% ===Example=== +%% +%% Retrieve information about one or more parameters in a specific hierarchy. +%% +%% ` +%% {ok, Parameters} = erlcloud_ssm:get_parameters_by_path([{path, "/desired/path"}]) +%% ' +%% @end +%%%------------------------------------------------------------------------------ +-spec get_parameters_by_path(Opts :: get_parameters_by_path_opts(), Config :: aws_config()) -> ssm_return(#ssm_get_parameters_by_path{}). +get_parameters_by_path(Opts, #aws_config{} = Config) -> + {AwsOpts, SSMOpts} = opts(get_parameters_by_path_opts(), Opts), + Return = ssm_request( + Config, + "GetParametersByPath", + AwsOpts), + out(Return, fun(Json, UOpts) -> + decode_get_parameters_by_path(Json, UOpts) + end, + SSMOpts). + +%%%------------------------------------------------------------------------------ +%% PutParameter +%%%------------------------------------------------------------------------------ +-type tag_parameter_opt() :: {key, string_param()} | {value, string_param()}. +-type tag_parameter_opts() :: [tag_parameter_opt()]. + +-type put_parameter_opt() :: {allowed_pattern, string_param()} | {data_type, string_param()} | + {description, string_param()} | {key_id, string_param()} | + {name, string_param()} | {overwrite, boolean()} | + {policies, string_param()} | {tags, [tag_parameter_opts()]} | + {tier, string_param()} | {type, string_param()} | + {value, string_param()} | out_opt(). +-type put_parameter_opts() :: [put_parameter_opt()]. + +-spec put_parameter_opts() -> opt_table(). +put_parameter_opts() -> + [ + {allowed_pattern, <<"AllowedPattern">>, fun encode_json_value/1}, + {data_type, <<"DataType">>, fun encode_json_value/1}, + {description, <<"Description">>, fun encode_json_value/1}, + {key_id, <<"KeyId">>, fun encode_json_value/1}, + {name, <<"Name">>, fun encode_json_value/1}, + {overwrite, <<"Overwrite">>, fun encode_json_value/1}, + {policies, <<"Policies">>, fun encode_json_value/1}, + {tags, <<"Tags">>, fun encode_json_tags_value/1}, + {tier, <<"Tier">>, fun encode_json_value/1}, + {type, <<"Type">>, fun encode_json_value/1}, + {value, <<"Value">>, fun encode_json_value/1} + ]. + +-spec put_parameter(Opts :: put_parameter_opts()) -> ssm_return(#ssm_put_parameter{}). +put_parameter(Opts) -> + put_parameter(Opts, default_config()). + +%%%------------------------------------------------------------------------------ +%% @doc +%% SSM API +%% [https://docs.aws.amazon.com/systems-manager/latest/APIReference/API_PutParameter.html] +%% +%% ===Example=== +%% +%% Add a parameter to the system. +%% +%% ` +%% {ok, Parameter} = erlcloud_ssm:put_parameter([{name, <<"password">>}, {value, <<"myP@ssw0rd">>}, {type, <<"String">>}]) +%% ' +%% @end +%%%------------------------------------------------------------------------------ +-spec put_parameter(Opts :: put_parameter_opts(), Config :: aws_config()) -> ssm_return(#ssm_put_parameter{}). +put_parameter(Opts, #aws_config{} = Config) -> + {AwsOpts, SSMOpts} = opts(put_parameter_opts(), Opts), + Return = ssm_request( + Config, + "PutParameter", + AwsOpts), + out(Return, fun(Json, UOpts) -> + decode_put_parameter(Json, UOpts) + end, + SSMOpts). + +%%%------------------------------------------------------------------------------ +%% DeleteParameter +%%%------------------------------------------------------------------------------ +-type delete_parameter_opt() :: {name, string_param()} | out_opt(). +-type delete_parameter_opts() :: [delete_parameter_opt()]. + +-spec delete_parameter_opts() -> opt_table(). +delete_parameter_opts() -> + [ + {name, <<"Name">>, fun encode_json_value/1} + ]. + +-spec delete_parameter(Opts :: delete_parameter_opts()) -> ok | {error, term()}. +delete_parameter(Opts) -> + delete_parameter(Opts, default_config()). + +%%%------------------------------------------------------------------------------ +%% @doc +%% SSM API +%% [https://docs.aws.amazon.com/systems-manager/latest/APIReference/API_DeleteParameter.html] +%% +%% ===Example=== +%% +%% Delete a parameter from the system. +%% +%% ` +%% ok = erlcloud_ssm:delete_parameter([{name, <<"password">>}]) +%% ' +%% @end +%%%------------------------------------------------------------------------------ +-spec delete_parameter(Opts :: delete_parameter_opts(), Config :: aws_config()) -> ok | {error, term()}. +delete_parameter(Opts, #aws_config{} = Config) -> + {AwsOpts, _} = opts(delete_parameter_opts(), Opts), + case ssm_request(Config, "DeleteParameter", AwsOpts) of + {ok, _} -> ok; + {error, _} = Error -> Error + end. + +%%%------------------------------------------------------------------------------ +%% Internal Functions +%%%------------------------------------------------------------------------------ +ssm_request(Config, Operation, Body) -> + case erlcloud_aws:update_config(Config) of + {ok, Config1} -> + ssm_request_impl(Config1, Operation, Body); + {error, Reason} -> + {error, Reason} + end. + +ssm_request_impl(Config, Operation, Body) -> + Payload = case Body of + [] -> <<"{}">>; + _ -> jsx:encode(lists:flatten(Body)) + end, + Headers = headers(Config, Operation, Payload), + Request = #aws_request{service = ssm, + uri = uri(Config), + method = post, + request_headers = Headers, + request_body = Payload}, + case erlcloud_aws:request_to_return(erlcloud_retry:request(Config, Request, fun ssm_result_fun/1)) of + {ok, {_RespHeaders, <<>>}} -> {ok, []}; + {ok, {_RespHeaders, RespBody}} -> {ok, jsx:decode(RespBody, [{return_maps, false}])}; + {error, _} = Error -> Error + end. + +-spec ssm_result_fun(Request :: aws_request()) -> aws_request(). +ssm_result_fun(#aws_request{response_type = ok} = Request) -> + Request; +ssm_result_fun(#aws_request{response_type = error, + error_type = aws, + response_status = Status} = Request) when Status >= 500 -> + Request#aws_request{should_retry = true}; +ssm_result_fun(#aws_request{response_type = error, error_type = aws} = Request) -> + Request#aws_request{should_retry = false}. + +headers(Config, Operation, Body) -> + Headers = [{"host", Config#aws_config.ssm_host}, + {"x-amz-target", lists:append(["AmazonSSM.", Operation])}, + {"content-type", "application/x-amz-json-1.1"}], + Region = erlcloud_aws:aws_region_from_host(Config#aws_config.ssm_host), + erlcloud_aws:sign_v4_headers(Config, Headers, Body, Region, "ssm"). + +uri(#aws_config{ssm_scheme = Scheme, ssm_host = Host} = Config) -> + lists:flatten([Scheme, Host, port_spec(Config)]). + +port_spec(#aws_config{ssm_port=80}) -> + ""; +port_spec(#aws_config{ssm_port=Port}) -> + [":", erlang:integer_to_list(Port)]. + +encode_json_value(undefined) -> undefined; +encode_json_value(true) -> true; +encode_json_value(false) -> false; +encode_json_value(L) when is_list(L), is_list(hd(L)) -> [encode_json_value(V) || V <- L]; +encode_json_value(L) when is_list(L), is_binary(hd(L)) -> [encode_json_value(V) || V <- L]; +encode_json_value(L) when is_list(L) -> list_to_binary(L); +encode_json_value(B) when is_binary(B) -> B; +encode_json_value(A) when is_atom(A) -> atom_to_binary(A, latin1). + +encode_json_tags_value(Tags) -> + encode_json_tags_value(Tags, []). + +encode_json_tags_value([], Acc) -> + Acc; +encode_json_tags_value([Tag|Tags], Acc) -> + encode_json_tags_value(Tags, [encode_json_tag_value(Tag)|Acc]). + +encode_json_tag_value(Tag) -> + encode_json_tag_value(Tag, []). + +encode_json_tag_value([], Acc) -> + Acc; +encode_json_tag_value([{key, Key}|Tags], Acc) -> + encode_json_tag_value(Tags, [{<<"Key">>, encode_json_value(Key)}|Acc]); +encode_json_tag_value([{value, Key}|Tags], Acc) -> + encode_json_tag_value(Tags, [{<<"Value">>, encode_json_value(Key)}|Acc]). + +encode_json_parameter_filters_value(ParameterFilters) -> + encode_json_parameter_filters_value(ParameterFilters, []). + +encode_json_parameter_filters_value([], Acc) -> + Acc; +encode_json_parameter_filters_value([{key, Key}|Filters], Acc) -> + encode_json_parameter_filters_value(Filters, [{<<"Key">>, encode_json_value(Key)}|Acc]); +encode_json_parameter_filters_value([{option, Option}|Filters], Acc) -> + encode_json_parameter_filters_value(Filters, [{<<"Option">>, encode_json_value(Option)}|Acc]); +encode_json_parameter_filters_value([{values, Values}|Filters], Acc) -> + encode_json_parameter_filters_value(Filters, [{<<"Values">>, encode_json_value(Values)}|Acc]). diff --git a/src/erlcloud_states.erl b/src/erlcloud_states.erl index 908cbe279..9a85c3a69 100644 --- a/src/erlcloud_states.erl +++ b/src/erlcloud_states.erl @@ -539,7 +539,7 @@ start_execution(StateMachineArn, Options) -> start_execution(StateMachineArn, Options, default_config()). -spec start_execution(StateMachineArn :: binary(), - Options :: list(), + Options :: map(), Config :: aws_config()) -> {ok, map()} | {error, any()}. start_execution(StateMachineArn, Options, Config) @@ -573,7 +573,7 @@ stop_execution(ExecutionArn, Options) -> stop_execution(ExecutionArn, Options, default_config()). -spec stop_execution(ExecutionArn :: binary(), - Options :: list(), + Options :: map(), Config :: aws_config()) -> {ok, map()} | {error, any()}. stop_execution(ExecutionArn, Options, Config) diff --git a/src/erlcloud_sts.erl b/src/erlcloud_sts.erl index 2f5a4dfdd..a115e6a02 100644 --- a/src/erlcloud_sts.erl +++ b/src/erlcloud_sts.erl @@ -15,6 +15,7 @@ -define(API_VERSION, "2011-06-15"). -define(UTC_TO_GREGORIAN, 62167219200). +-define(EXTERNAL_ID_MAX_LEN, 1224). assume_role(AwsConfig, RoleArn, RoleSessionName, DurationSeconds) -> @@ -22,11 +23,11 @@ assume_role(AwsConfig, RoleArn, RoleSessionName, DurationSeconds) -> % See http://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html --spec assume_role(#aws_config{}, string(), string(), 900..3600, undefined | string()) -> {#aws_config{}, proplist()} | no_return(). +-spec assume_role(#aws_config{}, string(), string(), 900..43200, undefined | string()) -> {#aws_config{}, proplist()}. assume_role(AwsConfig, RoleArn, RoleSessionName, DurationSeconds, ExternalId) when length(RoleArn) >= 20, length(RoleSessionName) >= 2, length(RoleSessionName) =< 64, - DurationSeconds >= 900, DurationSeconds =< 3600 -> + DurationSeconds >= 900, DurationSeconds =< 43200 -> Params = [ @@ -37,7 +38,7 @@ assume_role(AwsConfig, RoleArn, RoleSessionName, DurationSeconds, ExternalId) ExternalIdPart = case ExternalId of undefined -> []; - _ when length(ExternalId) >= 2, length(ExternalId) =< 96 -> [{"ExternalId", ExternalId}] + _ when length(ExternalId) >= 2, length(ExternalId) =< ?EXTERNAL_ID_MAX_LEN -> [{"ExternalId", ExternalId}] end, Xml = sts_query(AwsConfig, "AssumeRole", Params ++ ExternalIdPart), @@ -69,7 +70,7 @@ assume_role(AwsConfig, RoleArn, RoleSessionName, DurationSeconds, ExternalId) -type caller_identity_prop() :: {account, string()} | {arn, string()} | {userId, string()}. --spec get_caller_identity(#aws_config{}) -> {ok, [caller_identity_prop()]} | no_return(). +-spec get_caller_identity(#aws_config{}) -> {ok, [caller_identity_prop()]}. get_caller_identity(AwsConfig) -> Xml = sts_query(AwsConfig, "GetCallerIdentity", []), Proplists = erlcloud_xml:decode( @@ -85,7 +86,7 @@ get_federation_token(AwsConfig, DurationSeconds, Name) -> get_federation_token(AwsConfig, DurationSeconds, Name, undefined). % See http://docs.aws.amazon.com/STS/latest/APIReference/API_GetFederationToken.html --spec get_federation_token(#aws_config{}, 900..129600, string(), undefined | string()) -> {#aws_config{}, proplist()} | no_return(). +-spec get_federation_token(#aws_config{}, 900..129600, string(), undefined | string()) -> {#aws_config{}, proplist()}. get_federation_token(AwsConfig, DurationSeconds, Name, Policy) when length(Name) >= 2, length(Name) =< 32, DurationSeconds >= 900, DurationSeconds =< 129600 -> diff --git a/src/erlcloud_util.erl b/src/erlcloud_util.erl index 09626d039..6802d23a4 100644 --- a/src/erlcloud_util.erl +++ b/src/erlcloud_util.erl @@ -1,17 +1,59 @@ -module(erlcloud_util). --export([sha_mac/2, sha256_mac/2, md5/1, sha256/1,rand_uniform/1, - is_dns_compliant_name/1, - query_all/4, query_all/5, query_all_token/4, make_response/2, - get_items/2, to_string/1, encode_list/2, next_token/2]). + +-export([ + sha_mac/2, + sha256_mac/2, + md5/1, + sha256/1, + rand_uniform/1, + is_dns_compliant_name/1, + query_all/4, query_all/5, + query_all_token/4, + make_response/2, + get_items/2, + to_string/1, + encode_list/2, + encode_object/2, + encode_object_list/2, + next_token/2, + filter_undef/1, + filter_empty_map/1, + filter_empty_list/1, + uri_parse/1, + http_uri_decode/1, + http_uri_encode/1, + proplists_to_map/1, + proplists_to_map/2 +]). -define(MAX_ITEMS, 1000). +-ifdef(OTP_RELEASE). +-if(?OTP_RELEASE >= 23). +sha_mac(K, S) -> + crypto:mac(hmac, sha, K, S). +-else. +sha_mac(K, S) -> + crypto:hmac(sha, K, S). +-endif. +-else. sha_mac(K, S) -> crypto:hmac(sha, K, S). +-endif. +-ifdef(OTP_RELEASE). +-if(?OTP_RELEASE >= 23). +sha256_mac(K, S) -> + crypto:mac(hmac, sha256, K, S). +-else. sha256_mac(K, S) -> crypto:hmac(sha256, K, S). +-endif. +-else. +sha256_mac(K, S) -> + crypto:hmac(sha256, K, S). +-endif. sha256(V) -> crypto:hash(sha256, V). @@ -94,11 +136,39 @@ query_all(QueryFun, Config, Action, Params, MaxItems, Marker, Acc) -> Error end. +-spec encode_list(string(), [term()]) -> + proplists:proplist(). encode_list(ElementName, Elements) -> Numbered = lists:zip(lists:seq(1, length(Elements)), Elements), [{ElementName ++ ".member." ++ integer_to_list(N), Element} || {N, Element} <- Numbered]. +-spec encode_object(string(), proplists:proplist()) -> + proplists:proplist(). +encode_object(ElementName, ElementParameters) -> + lists:map( + fun({Key, Value}) -> + {ElementName ++ "." ++ Key, Value} + end, + ElementParameters + ). + +-spec encode_object_list(string(), [proplists:proplist()]) -> + proplists:proplist(). +encode_object_list(Prefix, ElementParameterList) -> + lists:flatten(lists:foldl( + fun(ElementMap, Acc) -> + [lists:map( + fun({Key, Value}) -> + {Prefix ++ ".member." ++ integer_to_list(length(Acc)+1) ++ "." ++ Key, Value} + end, + ElementMap + ) | Acc] + end, + [], + ElementParameterList + )). + make_response(Xml, Result) -> IsTruncated = erlcloud_xml:get_bool("/*/*/IsTruncated", Xml), Marker = erlcloud_xml:get_text("/*/*/Marker", Xml), @@ -128,3 +198,84 @@ next_token(Path, XML) -> ok end. +-spec filter_undef(proplists:proplist() | map()) -> + proplists:proplist() | map(). +filter_undef(List) when is_list(List) -> + lists:filter(fun({_Name, Value}) -> Value =/= undefined end, List); +filter_undef(Map) when is_map(Map) -> + maps:filter(fun(_Key, Value) -> Value =/= undefined end, Map). + +-spec filter_empty_map(map()) -> map(). +filter_empty_map(Map) when is_map(Map) -> + maps:filter(fun(_Key, Value) -> Value =/= #{} end, Map). + +-spec filter_empty_list(map()) -> map(). +filter_empty_list(Map) when is_map(Map) -> + maps:filter(fun(_Key, Value) -> Value =/= [] end, Map). + +-ifdef(OTP_RELEASE). +-if(?OTP_RELEASE >= 23). +uri_parse(Uri) -> + URIMap = uri_string:parse(Uri), + DefaultScheme = "https", + DefaultPort = 443, + Scheme = list_to_atom(maps:get(scheme, URIMap, DefaultScheme)), + UserInfo = maps:get(userinfo, URIMap, ""), + Host = maps:get(host, URIMap, ""), + Port = maps:get(port, URIMap, DefaultPort), + Path = maps:get(path, URIMap, ""), + Query = maps:get(query, URIMap, ""), + {ok, {Scheme, UserInfo, Host, Port, Path, Query}}. +-else. +uri_parse(Uri) -> + http_uri:parse(Uri). +-endif. +-else. +uri_parse(Uri) -> + http_uri:parse(Uri). +-endif. + +-ifdef(OTP_RELEASE). +-if(?OTP_RELEASE >= 23). +http_uri_decode(HexEncodedURI) -> + [{URI, true}] = uri_string:dissect_query(HexEncodedURI), + URI. +-else. +http_uri_decode(HexEncodedURI) -> + http_uri:decode(HexEncodedURI). +-endif. +-else. +http_uri_decode(HexEncodedURI) -> + http_uri:decode(HexEncodedURI). +-endif. + +-ifdef(OTP_RELEASE). +-if(?OTP_RELEASE >= 23). +http_uri_encode(URI) -> + uri_string:compose_query([{URI, true}]). +-else. +http_uri_encode(URI) -> + http_uri:encode(URI). +-endif. +-else. +http_uri_encode(URI) -> + http_uri:encode(URI). +-endif. + +-spec proplists_to_map(proplists:proplist() | any()) -> map() | any(). +proplists_to_map([]) -> []; +proplists_to_map([{}]) -> #{}; +proplists_to_map([{_,_} | _] = Proplist) -> + proplists_to_map(Proplist, #{}); +proplists_to_map([Head | _Tail] = List) when is_list(Head) -> + [proplists_to_map(E) || E <- List]; +proplists_to_map(V) -> V. + +-spec proplists_to_map(proplists:proplist(), map()) -> map(). +proplists_to_map([], Acc) -> + Acc; +proplists_to_map([{Key, Val} | Tail], Acc) when is_list(Val) -> + proplists_to_map(Tail, Acc#{Key => proplists_to_map(Val)}); +proplists_to_map([{Key, Val} | Tail], Acc) -> + proplists_to_map(Tail, Acc#{Key => Val}). + diff --git a/src/erlcloud_waf.erl b/src/erlcloud_waf.erl index a52e1cb07..22875c395 100644 --- a/src/erlcloud_waf.erl +++ b/src/erlcloud_waf.erl @@ -899,7 +899,7 @@ update_ip_set(ChangeToken, IPSetId, Updates, Config) -> %% http://docs.aws.amazon.com/waf/latest/APIReference/API_UpdateRule.html %%%------------------------------------------------------------------------------ -spec update_rule(ChangeToken :: string() | binary(), - RuleId :: string(), + RuleId :: string() | binary(), Updates :: [waf_rule_update()]) -> waf_return_val(). update_rule(ChangeToken, RuleId, Updates) -> @@ -1034,7 +1034,8 @@ update_xss_match_set(ChangeToken, XssMatchSetId, Updates, Config) -> waf_rule_update() | waf_size_constraint_update() | waf_sql_injection_match_set_update() | - waf_web_acl_update()) -> + waf_web_acl_update() | + waf_xss_match_set_update()) -> proplists:proplist(). transform_to_proplist(#waf_byte_match_set_update{action = Action, byte_match_tuple = ByteMatchTuple}) -> [{<<"Action">>, get_update_action(Action)}, @@ -1186,7 +1187,7 @@ waf_request_no_update(Config, Operation, Body) -> request_body = Payload}, case erlcloud_aws:request_to_return(erlcloud_retry:request(Config, Request, fun waf_result_fun/1)) of {ok, {_RespHeaders, <<>>}} -> {ok, []}; - {ok, {_RespHeaders, RespBody}} -> {ok, jsx:decode(RespBody)}; + {ok, {_RespHeaders, RespBody}} -> {ok, jsx:decode(RespBody, [{return_maps, false}])}; {error, _} = Error-> Error end. diff --git a/src/erlcloud_workspaces.erl b/src/erlcloud_workspaces.erl new file mode 100644 index 000000000..d8d3a74b8 --- /dev/null +++ b/src/erlcloud_workspaces.erl @@ -0,0 +1,588 @@ +%% @doc +%% An Erlang interface to AWS Workspaces. +%% +%% Output is in the form of `{ok, Value}' or `{error, Reason}'. The +%% format of `Value' is controlled by the `out' option, which defaults +%% to `json'. The possible values are: +%% +%% * `json' - The output from Workspaces as processed by `jsx:decode' +%% but with no further manipulation. +%% +%% * `record' - A record containing all the information from the +%% Workspaces response except field types. +%% +%% Workspaces errors are returned in the form `{error, {ErrorCode, Message}}' +%% where `ErrorCode' and 'Message' are both binary +%% strings. + +%% See the unit tests for additional usage examples beyond what are +%% provided for each function. +%% +%% @end + +-module(erlcloud_workspaces). + +-include("erlcloud_aws.hrl"). +-include("erlcloud_workspaces.hrl"). + +%%% Library initialization. +-export([configure/2, configure/3, configure/4, new/2, new/3, new/4]). + +-define(API_VERSION, "20150408"). +-define(OUTPUT_CHOICES, [json, record, map]). + +-export([ + describe_tags/1, describe_tags/2, + describe_workspaces/0, describe_workspaces/1, describe_workspaces/2, + describe_workspace_directories/0, describe_workspace_directories/1, describe_workspace_directories/2 +]). + +-export_type([ + describe_workspaces_opt/0, + describe_workspaces_opts/0, + describe_workspace_directories_opt/0, + describe_workspace_directories_opts/0 + ]). + + +%%%------------------------------------------------------------------------------ +%%% Library initialization. +%%%------------------------------------------------------------------------------ + +-spec new(string(), string()) -> aws_config(). + +new(AccessKeyID, SecretAccessKey) -> + #aws_config{ + access_key_id=AccessKeyID, + secret_access_key=SecretAccessKey + }. + +-spec new(string(), string(), string()) -> aws_config(). + +new(AccessKeyID, SecretAccessKey, Host) -> + #aws_config{ + access_key_id=AccessKeyID, + secret_access_key=SecretAccessKey, + workspaces_host=Host + }. + +-spec new(string(), string(), string(), non_neg_integer()) -> aws_config(). + +new(AccessKeyID, SecretAccessKey, Host, Port) -> + #aws_config{ + access_key_id=AccessKeyID, + secret_access_key=SecretAccessKey, + workspaces_host=Host, + workspaces_port=Port + }. + +-spec configure(string(), string()) -> ok. + +configure(AccessKeyID, SecretAccessKey) -> + put(aws_config, new(AccessKeyID, SecretAccessKey)), + ok. + +-spec configure(string(), string(), string()) -> ok. + +configure(AccessKeyID, SecretAccessKey, Host) -> + put(aws_config, new(AccessKeyID, SecretAccessKey, Host)), + ok. + +-spec configure(string(), string(), string(), non_neg_integer()) -> ok. + +configure(AccessKeyID, SecretAccessKey, Host, Port) -> + put(aws_config, new(AccessKeyID, SecretAccessKey, Host, Port)), + ok. + +default_config() -> + erlcloud_aws:default_config(). + + +%%%------------------------------------------------------------------------------ +%%% Shared types +%%%------------------------------------------------------------------------------ + +-type string_param() :: binary() | string(). + +-type json_pair() :: {binary() | atom(), jsx:json_term()}. +-type json_return() :: {ok, jsx:json_term()} | {error, term()}. +-type workspaces_return(Record) :: {ok, jsx:json_term() | Record } | {error, term()}. +-type decode_fun() :: fun((jsx:json_term(), decode_opts()) -> tuple()). + +%%%------------------------------------------------------------------------------ +%%% Shared Options +%%%------------------------------------------------------------------------------ +-type out_type() :: json | record. +-type out_opt() :: {out, out_type()}. +-type property() :: proplists:property(). + +-type aws_opts() :: [json_pair()]. +-type workspaces_opts() :: [out_opt()]. +-type opts() :: {aws_opts(), workspaces_opts()}. + +-spec verify_workspaces_opt(atom(), term()) -> ok. +verify_workspaces_opt(out, Value) -> + case lists:member(Value, ?OUTPUT_CHOICES) of + true -> + ok; + false -> + error({erlcloud_workspaces, {invalid_opt, {out, Value}}}) + end; +verify_workspaces_opt(Name, Value) -> + error({erlcloud_workspaces, {invalid_opt, {Name, Value}}}). + +-type opt_table_entry() :: {atom(), binary(), fun((_) -> jsx:json_term())}. +-type opt_table() :: [opt_table_entry()]. +-spec opt_folder(opt_table(), property(), opts()) -> opts(). +opt_folder(_, {_, undefined}, Opts) -> + %% ignore options set to undefined + Opts; +opt_folder(Table, {Name, Value}, {AwsOpts, EcsOpts}) -> + case lists:keyfind(Name, 1, Table) of + {Name, Key, ValueFun} -> + {[{Key, ValueFun(Value)} | AwsOpts], EcsOpts}; + false -> + verify_workspaces_opt(Name, Value), + {AwsOpts, [{Name, Value} | EcsOpts]} + end. + +-spec opts(opt_table(), proplist()) -> opts(). +opts(Table, Opts) when is_list(Opts) -> + %% remove duplicate options + Opts1 = lists:ukeysort(1, proplists:unfold(Opts)), + lists:foldl(fun(Opt, A) -> opt_folder(Table, Opt, A) end, {[], []}, Opts1); +opts(_, _) -> + error({erlcloud_workspaces, opts_not_list}). + +%%%------------------------------------------------------------------------------ +%%% Shared Decoders +%%%------------------------------------------------------------------------------ + +-type decode_opt() :: {typed, boolean()}. +-type decode_opts() :: [decode_opt()]. +-type record_desc() :: {tuple(), field_table()}. + +-spec id(X) -> X. +id(X) -> X. + +-spec id(X, decode_opts()) -> X. +id(X, _) -> X. + +-type field_table() :: [{binary(), pos_integer(), + fun((jsx:json_term(), decode_opts()) -> term())}]. + +-spec decode_folder(field_table(), json_pair(), decode_opts(), tuple()) -> tuple(). +decode_folder(Table, {Key, Value}, Opts, A) -> + case lists:keyfind(Key, 1, Table) of + {Key, Index, ValueFun} -> + setelement(Index, A, ValueFun(Value, Opts)); + false -> + A + end. + + +-spec decode_record(record_desc(), jsx:json_term(), decode_opts()) -> tuple(). +decode_record({Record, _}, [{}], _) -> + %% jsx returns [{}] for empty objects + Record; +decode_record({Record, Table}, Json, Opts) -> + lists:foldl(fun(Pair, A) -> decode_folder(Table, Pair, Opts, A) end, Record, Json). + +%%%------------------------------------------------------------------------------ +%%% Output +%%%------------------------------------------------------------------------------ +-spec out(json_return(), decode_fun(), workspaces_opts()) + -> {ok, jsx:json_term() | tuple()} | + {simple, term()} | + {error, term()}. +out({error, Reason}, _, _) -> + {error, Reason}; +out({ok, Json}, Decode, Opts) -> + case proplists:get_value(out, Opts, record) of + json -> + {ok, Json}; + record -> + {ok, Decode(Json, [])}; + map -> + {ok, erlcloud_util:proplists_to_map(Json)} + end. + +%%%------------------------------------------------------------------------------ +%%% Shared Records +%%%------------------------------------------------------------------------------ + +-spec workspace_record() -> record_desc(). +workspace_record() -> + {#workspace{}, + [ + {<<"BundleId">>, #workspace.bundle_id, fun id/2}, + {<<"ComputerName">>, #workspace.computer_name, fun id/2}, + {<<"DirectoryId">>, #workspace.directory_id, fun id/2}, + {<<"ErrorCode">>, #workspace.error_code, fun id/2}, + {<<"ErrorMessage">>, #workspace.error_message, fun id/2}, + {<<"IpAddress">>, #workspace.ip_address, fun id/2}, + {<<"ModificationStates">>, #workspace.modification_states, fun decode_modification_state_list/2}, + {<<"RootVolumeEncryptionEnabled">>, #workspace.root_volume_encryption_enabled, fun id/2}, + {<<"State">>, #workspace.state, fun id/2}, + {<<"SubnetId">>, #workspace.subnet_id, fun id/2}, + {<<"UserName">>, #workspace.user_name, fun id/2}, + {<<"UserVolumeEncryptionEnabled">>, #workspace.user_volume_encryption_enabled, fun id/2}, + {<<"VolumeEncryptionKey">>, #workspace.volume_encryption_key, fun id/2}, + {<<"WorkspaceId">>, #workspace.workspace_id, fun id/2}, + {<<"WorkspaceProperties">>, #workspace.workspace_properties, fun decode_workspace_properties/2} + ] + }. + +-spec modification_state_record() -> record_desc(). +modification_state_record() -> + {#workspace_modification_state{}, + [ + {<<"Resource">>, #workspace_modification_state.resource, fun id/2}, + {<<"State">>, #workspace_modification_state.state, fun id/2} + ] + }. + +-spec workspace_properties_record() -> record_desc(). +workspace_properties_record() -> + {#workspace_properties{}, + [ + {<<"ComputeTypeName">>, #workspace_properties.computer_type_name, fun id/2}, + {<<"RootVolumeSizeGib">>, #workspace_properties.root_volume_size_gib, fun id/2}, + {<<"RunningMode">>, #workspace_properties.running_mode, fun id/2}, + {<<"RunningModeAutoStopTimeoutInMinutes">>, #workspace_properties.running_mode_auto_stop_timeout_in_minutes, fun id/2}, + {<<"UserVolumeSizeGib">>, #workspace_properties.user_volume_size_gib, fun id/2} + ] + }. + +-spec tag_record() -> record_desc(). +tag_record() -> + {#workspaces_tag{}, + [ + {<<"Key">>, #workspaces_tag.key, fun id/2}, + {<<"Value">>, #workspaces_tag.value, fun id/2} + ] + }. + +-spec workspace_directory_record() -> record_desc(). +workspace_directory_record() -> + {#workspace_directory{}, + [ + {<<"Alias">>, #workspace_directory.alias, fun id/2}, + {<<"CustomerUserName">>, #workspace_directory.customer_user_name, fun id/2}, + {<<"DirectoryId">>, #workspace_directory.directory_id, fun id/2}, + {<<"DirectoryName">>, #workspace_directory.directory_name, fun id/2}, + {<<"DirectoryType">>, #workspace_directory.directory_type, fun id/2}, + {<<"DnsIpAddresses">>, #workspace_directory.dns_ip_address, fun id/2}, + {<<"IamRoleId">>, #workspace_directory.iam_role_id, fun id/2}, + {<<"ipGroupIds">>, #workspace_directory.ip_group_ids, fun id/2}, + {<<"RegistrationCode">>, #workspace_directory.registration_code, fun id/2}, + {<<"SelfservicePermissions">>, #workspace_directory.selfservice_permissions, fun decode_selfservice_permissions/2}, + {<<"State">>, #workspace_directory.state, fun id/2}, + {<<"SubnetIds">>, #workspace_directory.subnet_ids, fun id/2}, + {<<"Tenancy">>, #workspace_directory.tenancy, fun id/2}, + {<<"WorkspaceAccessProperties">>, #workspace_directory.workspace_access_properties, fun decode_workspace_access_properties/2}, + {<<"WorkspaceCreationProperties">>, #workspace_directory.workspace_creation_properties, fun decode_workspace_creation_properties/2}, + {<<"WorkspaceSecurityGroupId">>, #workspace_directory.workspace_security_group_id, fun id/2} + ] + }. + +-spec workspaces_selfservice_permissions_record() -> record_desc(). +workspaces_selfservice_permissions_record() -> + {#workspaces_selfservice_permissions{}, + [ + {<<"ChangeComputeType">>, #workspaces_selfservice_permissions.change_compute_type, fun id/2}, + {<<"IncreaseVolumeSize">>, #workspaces_selfservice_permissions.increase_volume_size, fun id/2}, + {<<"RebuildWorkspace">>, #workspaces_selfservice_permissions.rebuild_workspace, fun id/2}, + {<<"RestartWorkspace">>, #workspaces_selfservice_permissions.restart_workspace, fun id/2}, + {<<"SwitchRunningMode">>, #workspaces_selfservice_permissions.switch_running_mode, fun id/2} + ] + }. + +-spec workspace_access_properties_record() -> record_desc(). +workspace_access_properties_record() -> + {#workspace_access_properties{}, + [ + {<<"DeviceTypeAndroid">>, #workspace_access_properties.device_type_android, fun id/2}, + {<<"DeviceTypeChromeOs">>, #workspace_access_properties.device_type_chrome_os, fun id/2}, + {<<"DeviceTypeIos">>, #workspace_access_properties.device_type_ios, fun id/2}, + {<<"DeviceTypeOsx">>, #workspace_access_properties.device_type_osx, fun id/2}, + {<<"DeviceTypeWeb">>, #workspace_access_properties.device_type_web, fun id/2}, + {<<"DeviceTypeWindows">>, #workspace_access_properties.device_type_windows, fun id/2}, + {<<"DeviceTypeZeroClient">>, #workspace_access_properties.device_type_zero_client, fun id/2} + ] + }. + +-spec workspace_creation_properties_record() -> record_desc(). +workspace_creation_properties_record() -> + {#workspace_creation_properties{}, + [ + {<<"CustomSecurityGroupId">>, #workspace_creation_properties.custom_security_group_id, fun id/2}, + {<<"DefaultOu">>, #workspace_creation_properties.default_ou, fun id/2}, + {<<"EnableInternetAccess">>, #workspace_creation_properties.enable_internet_access, fun id/2}, + {<<"EnableMaintenanceMode">>, #workspace_creation_properties.enable_maintenance_mode, fun id/2}, + {<<"EnableWorkDocs">>, #workspace_creation_properties.enable_work_docs, fun id/2}, + {<<"UserEnabledAsLocalAdministrator">>, #workspace_creation_properties.user_enabled_as_local_administrator, fun id/2} + ] + }. + +decode_tags_list(V, Opts) -> + [decode_record(tag_record(), I, Opts) || I <- V]. + +decode_workspace_properties(V, Opts) -> + decode_record(workspace_properties_record(), V, Opts). + +decode_modification_state_list(V, Opts) -> + [decode_record(modification_state_record(), I, Opts) || I <- V]. + +decode_workspaces_list(V, Opts) -> + [decode_record(workspace_record(), I, Opts) || I <- V]. + +decode_workspace_directories_list(V, Opts) -> + [decode_record(workspace_directory_record(), I, Opts) || I <- V]. + +decode_selfservice_permissions(V, Opts) -> + decode_record(workspaces_selfservice_permissions_record(), V, Opts). + +decode_workspace_access_properties(V, Opts) -> + decode_record(workspace_access_properties_record(), V, Opts). + +decode_workspace_creation_properties(V, Opts) -> + decode_record(workspace_creation_properties_record(), V, Opts). + +%%%------------------------------------------------------------------------------ +%%% AWS Workspaces API Functions +%%%------------------------------------------------------------------------------ + +%%%------------------------------------------------------------------------------ +%% DescribeTags +%%%------------------------------------------------------------------------------ +-type describe_tags_opt() :: {resource_id, string_param()} | + out_opt(). +-type describe_tags_opts() :: [describe_tags_opt()]. + +-spec describe_tags_opts() -> opt_table(). +describe_tags_opts() -> + [ + {resource_id, <<"ResourceId">>, fun encode_json_value/1} + ]. + +-spec describe_tags(Opts :: describe_tags_opts()) -> workspaces_return([#workspaces_tag{}]). +describe_tags(Opts) -> + describe_tags(Opts, default_config()). + +%%%------------------------------------------------------------------------------ +%% @doc +%% Workspaces API +%% [https://docs.aws.amazon.com/workspaces/latest/api/API_DescribeTags.html] +%% +%% ===Example=== +%% +%% Describe tags for workspace id "ws-c8wvb67py" +%% +%% ` +%% {ok, Tags} = erlcloud_workspaces:describe_tags([{resource_id, "ws-c8wvb67py"}, {out, json}]) +%% ' +%% @end +%%%------------------------------------------------------------------------------ +-spec describe_tags(Opts :: describe_tags_opts(), Config :: aws_config()) -> workspaces_return([#workspaces_tag{}]). +describe_tags(Opts, #aws_config{} = Config) -> + {AwsOpts, WorkspacesOpts} = opts(describe_tags_opts(), Opts), + Return = workspaces_request( + Config, + "DescribeTags", + AwsOpts), + out(Return, fun(Json, UOpts) -> + TagsList = proplists:get_value(<<"TagList">>, Json), + decode_tags_list(TagsList, UOpts) + end, + WorkspacesOpts). + +%%%------------------------------------------------------------------------------ +%% DescribeWorkspaces +%%%------------------------------------------------------------------------------ +-type describe_workspaces_opt() :: {bundle_id, string_param()} | + {directory_id, string_param()} | + {limit, pos_integer()} | + {next_token, string_param()} | + {user_name, string_param()} | + {workspace_ids, [string_param()]} | + out_opt(). +-type describe_workspaces_opts() :: [describe_workspaces_opt()]. + +-spec describe_workspaces_opts() -> opt_table(). +describe_workspaces_opts() -> + [ + {bundle_id, <<"BundleId">>, fun encode_json_value/1}, + {directory_id, <<"DirectoryId">>, fun encode_json_value/1}, + {limit, <<"Limit">>, fun id/1}, + {next_token, <<"NextToken">>, fun encode_json_value/1}, + {user_name, <<"UserName">>, fun encode_json_value/1}, + {workspace_ids, <<"WorkspaceIds">>, fun encode_json_value/1} + ]. + +-spec describe_workspaces_record() -> record_desc(). +describe_workspaces_record() -> + {#describe_workspaces{}, + [{<<"NextToken">>, #describe_workspaces.next_token, fun id/2}, + {<<"Workspaces">>, #describe_workspaces.workspaces, fun decode_workspaces_list/2} + ]}. + +-spec describe_workspaces() -> workspaces_return(#describe_workspaces{}). +describe_workspaces() -> + describe_workspaces([], default_config()). + +-spec describe_workspaces(describe_workspaces_opts() | aws_config()) -> workspaces_return(#describe_workspaces{}). +describe_workspaces(#aws_config{} = Config) -> + describe_workspaces([], Config); +describe_workspaces(Opts) -> + describe_workspaces(Opts, default_config()). + +%%%------------------------------------------------------------------------------ +%% @doc +%% Workspaces API +%% [https://docs.aws.amazon.com/workspaces/latest/api/API_DescribeWorkspaces.html] +%% +%% ===Example=== +%% +%% Describe workspaces in Directory "TestDirectory" +%% +%% ` +%% {ok, Clusters} = erlcloud_workspaces:describe_workspaces([{directory_id, "TestDirectory"}, {out, json}]) +%% ' +%% @end +%%%------------------------------------------------------------------------------ +-spec describe_workspaces(Opts :: describe_workspaces_opts(), Config :: aws_config()) -> workspaces_return(#describe_workspaces{}). +describe_workspaces(Opts, #aws_config{} = Config) -> + {AwsOpts, WorkspacesOpts} = opts(describe_workspaces_opts(), Opts), + Return = workspaces_request( + Config, + "DescribeWorkspaces", + AwsOpts), + out(Return, fun(Json, UOpts) -> decode_record(describe_workspaces_record(), Json, UOpts) end, + WorkspacesOpts). + +%%%------------------------------------------------------------------------------ +%% DescribeWorkspaceDirectories +%%%------------------------------------------------------------------------------ +-type describe_workspace_directories_opt() :: {directory_ids, [string_param()]} | + {limit, pos_integer()} | + {next_token, string_param()} | + out_opt(). +-type describe_workspace_directories_opts() :: [describe_workspace_directories_opt()]. + +-spec describe_workspace_directories_opts() -> opt_table(). +describe_workspace_directories_opts() -> + [ + {directory_ids, <<"DirectoryIds">>, fun encode_json_value/1}, + {limit, <<"Limit">>, fun id/1}, + {next_token, <<"NextToken">>, fun encode_json_value/1} + ]. + +-spec describe_workspace_directories_record() -> record_desc(). +describe_workspace_directories_record() -> + {#describe_workspace_directories{}, + [{<<"NextToken">>, #describe_workspace_directories.next_token, fun id/2}, + {<<"Directories">>, #describe_workspace_directories.workspace_directories, fun decode_workspace_directories_list/2} + ]}. + +-spec describe_workspace_directories() -> workspaces_return(#describe_workspace_directories{}). +describe_workspace_directories() -> + describe_workspace_directories([], default_config()). + +-spec describe_workspace_directories(describe_workspace_directories_opts() | aws_config()) + -> workspaces_return(#describe_workspace_directories{}). +describe_workspace_directories(#aws_config{} = Config) -> + describe_workspace_directories([], Config); +describe_workspace_directories(Opts) -> + describe_workspace_directories(Opts, default_config()). + +%%%------------------------------------------------------------------------------ +%% @doc +%% Workspaces API +%% [https://docs.aws.amazon.com/workspaces/latest/api/API_DescribeWorkspaceDirectories.html] +%% +%% ===Example=== +%% +%% Describe workspaces directory "TestDirectory" +%% +%% ` +%% {ok, Clusters} = erlcloud_workspaces:describe_workspace_directories([{directory_ids, ["TestDirectory"]}, {out, json}]) +%% ' +%% @end +%%%------------------------------------------------------------------------------ +-spec describe_workspace_directories( + Opts :: describe_workspace_directories_opts(), + Config :: aws_config()) + -> workspaces_return(#describe_workspace_directories{}). +describe_workspace_directories(Opts, #aws_config{} = Config) -> + {AwsOpts, WorkspacesOpts} = opts(describe_workspace_directories_opts(), Opts), + Return = workspaces_request( + Config, + "DescribeWorkspaceDirectories", + AwsOpts), + out(Return, fun(Json, UOpts) -> decode_record(describe_workspace_directories_record(), Json, UOpts) end, + WorkspacesOpts). + +%%%------------------------------------------------------------------------------ +%%% Internal Functions +%%%------------------------------------------------------------------------------ +workspaces_request(Config, Operation, Body) -> + case erlcloud_aws:update_config(Config) of + {ok, Config1} -> + workspaces_request_no_update(Config1, Operation, Body); + {error, Reason} -> + {error, Reason} + end. + +workspaces_request_no_update(Config, Operation, Body) -> + Payload = case Body of + [] -> <<"{}">>; + _ -> jsx:encode(lists:flatten(Body)) + end, + Headers = headers(Config, Operation, Payload), + Request = #aws_request{service = workspaces, + uri = uri(Config), + method = post, + request_headers = Headers, + request_body = Payload}, + case erlcloud_aws:request_to_return(erlcloud_retry:request(Config, Request, fun workspaces_result_fun/1)) of + {ok, {_RespHeaders, <<>>}} -> {ok, []}; + {ok, {_RespHeaders, RespBody}} -> {ok, jsx:decode(RespBody, [{return_maps, false}])}; + {error, _} = Error-> Error + end. + +-spec workspaces_result_fun(Request :: aws_request()) -> aws_request(). +workspaces_result_fun(#aws_request{response_type = ok} = Request) -> + Request; +workspaces_result_fun(#aws_request{response_type = error, + error_type = aws, + response_status = Status} = Request) when Status >= 500 -> + Request#aws_request{should_retry = true}; +workspaces_result_fun(#aws_request{response_type = error, error_type = aws} = Request) -> + Request#aws_request{should_retry = false}. + +headers(Config, Operation, Body) -> + Headers = [{"host", Config#aws_config.workspaces_host}, + {"x-amz-target", lists:append(["WorkspacesService.", Operation])}, + {"content-type", "application/x-amz-json-1.1"}], + Region = erlcloud_aws:aws_region_from_host(Config#aws_config.workspaces_host), + erlcloud_aws:sign_v4_headers(Config, Headers, Body, Region, "workspaces"). + +uri(#aws_config{workspaces_scheme = Scheme, workspaces_host = Host} = Config) -> + lists:flatten([Scheme, Host, port_spec(Config)]). + +port_spec(#aws_config{workspaces_port=80}) -> + ""; +port_spec(#aws_config{workspaces_port=Port}) -> + [":", erlang:integer_to_list(Port)]. + + +encode_json_value(undefined) -> undefined; +encode_json_value(true) -> true; +encode_json_value(false) -> false; +encode_json_value(L) when is_list(L), is_list(hd(L)) -> [encode_json_value(V) || V <- L]; +encode_json_value(L) when is_list(L), is_binary(hd(L)) -> [encode_json_value(V) || V <- L]; +encode_json_value(L) when is_list(L) -> list_to_binary(L); +encode_json_value(B) when is_binary(B) -> B; +encode_json_value(A) when is_atom(A) -> atom_to_binary(A, latin1). + diff --git a/src/erlcloud_xml.erl b/src/erlcloud_xml.erl index 49379ad74..631dc4493 100644 --- a/src/erlcloud_xml.erl +++ b/src/erlcloud_xml.erl @@ -79,7 +79,7 @@ get_float(XPath, Node) -> get_text(#xmlText{value=Value}) -> Value; get_text(#xmlElement{content=Content}) -> - lists:flatten([get_text(Node) || Node <- Content]). + lists:flatmap(fun get_text/1, Content). get_text(XPath, Doc) -> get_text(XPath, Doc, ""). get_text({XPath, AttrName}, Doc, Default) -> diff --git a/test/erlcloud_access_analyzer_tests.erl b/test/erlcloud_access_analyzer_tests.erl new file mode 100644 index 000000000..154bcad03 --- /dev/null +++ b/test/erlcloud_access_analyzer_tests.erl @@ -0,0 +1,299 @@ +-module(erlcloud_access_analyzer_tests). + +-include_lib("eunit/include/eunit.hrl"). +-include("erlcloud_aws.hrl"). + +-define(TEST_AWS_CONFIG, + #aws_config{ + access_key_id = "TEST_ACCESS_KEY_ID", + secret_access_key = "TEST_ACCESS_KEY", + security_token = "TEST_SECURITY_TOKEN" + } +). + +api_test_() -> + { + foreach, + fun() -> meck:new(erlcloud_httpc) end, + fun(_) -> meck:unload() end, + [ + fun list_analyzers_tests/1, + fun get_analyzer_tests/1, + fun create_analyzer_tests/1, + fun delete_analyzer_tests/1 + ] + }. + +list_analyzers_tests(_) -> + [ + { + "ListAnalyzers", + fun() -> + AwsConfig = ?TEST_AWS_CONFIG, + meck:expect( + erlcloud_httpc, request, + fun(A1, A2, A3, A4, A5, A6) -> + Url = "https://access-analyzer.us-east-1.amazonaws.com/analyzer?maxResults=10", + Method = get, + RequestContent = <<>>, + case [A1, A2, A3, A4, A5, A6] of + [Url, Method, _Headers, RequestContent, _Timeout, AwsConfig] -> + ResponseContent = << + "{" + "\"analyzers\":[" + "{" + "\"arn\":\"arn:aws:access-analyzer:us-east-1:123456789012:analyzer/test-1\"," + "\"name\":\"test-1\"," + "\"type\":\"ACCOUNT\"," + "\"createdAt\":\"2022-01-01T01:02:03Z\"," + "\"status\":\"ACTIVE\"" + "}," + "{" + "\"arn\":\"arn:aws:access-analyzer:us-east-1:123456789012:analyzer/test-2\"," + "\"name\":\"test-2\"," + "\"type\":\"ACCOUNT\"," + "\"createdAt\":\"2022-02-02T01:02:03Z\"," + "\"status\":\"ACTIVE\"" + "}" + "]" + "}" + >>, + {ok, {{200, "OK"}, [], ResponseContent}} + end + end + ), + Params = [{"maxResults", 10}], + Result = erlcloud_access_analyzer:list_analyzers(AwsConfig, Params), + Analyzers = [ + [ + {<<"arn">>, <<"arn:aws:access-analyzer:us-east-1:123456789012:analyzer/test-1">>}, + {<<"name">>, <<"test-1">>}, + {<<"type">>, <<"ACCOUNT">>}, + {<<"createdAt">>, <<"2022-01-01T01:02:03Z">>}, + {<<"status">>, <<"ACTIVE">>} + ], + [ + {<<"arn">>, <<"arn:aws:access-analyzer:us-east-1:123456789012:analyzer/test-2">>}, + {<<"name">>, <<"test-2">>}, + {<<"type">>, <<"ACCOUNT">>}, + {<<"createdAt">>, <<"2022-02-02T01:02:03Z">>}, + {<<"status">>, <<"ACTIVE">>} + ] + ], + ?assertEqual({ok, Analyzers}, Result) + end + }, + { + "ListAnalyzers [all]", + fun() -> + AwsConfig = ?TEST_AWS_CONFIG, + meck:expect( + erlcloud_httpc, request, + fun(A1, A2, A3, A4, A5, A6) -> + Url1 = "https://access-analyzer.us-east-1.amazonaws.com/analyzer?maxResults=1&type=ACCOUNT", + Url2 = "https://access-analyzer.us-east-1.amazonaws.com/analyzer?maxResults=1&nextToken=TEST_NEXT_TOKEN&type=ACCOUNT", + Method = get, + RequestContent = <<>>, + case [A1, A2, A3, A4, A5, A6] of + [Url1, Method, _Headers, RequestContent, _Timeout, AwsConfig] -> + ResponseContent = << + "{" + "\"analyzers\":[" + "{" + "\"arn\":\"arn:aws:access-analyzer:us-east-1:123456789012:analyzer/test-1\"," + "\"name\":\"test-1\"," + "\"type\":\"ACCOUNT\"," + "\"createdAt\":\"2022-01-01T01:02:03Z\"," + "\"status\":\"ACTIVE\"" + "}" + "]," + "\"nextToken\":\"TEST_NEXT_TOKEN\"" + "}" + >>, + {ok, {{200, "OK"}, [], ResponseContent}}; + [Url2, Method, _Headers, RequestContent, _Timeout, AwsConfig] -> + ResponseContent = << + "{" + "\"analyzers\":[" + "{" + "\"arn\":\"arn:aws:access-analyzer:us-east-1:123456789012:analyzer/test-2\"," + "\"name\":\"test-2\"," + "\"type\":\"ACCOUNT\"," + "\"createdAt\":\"2022-02-02T01:02:03Z\"," + "\"status\":\"ACTIVE\"" + "}" + "]" + "}" + >>, + {ok, {{200, "OK"}, [], ResponseContent}} + end + end + ), + Params = [{"type", "ACCOUNT"}, {"maxResults", 1}], + Result = erlcloud_access_analyzer:list_analyzers_all(AwsConfig, Params), + Analyzers = [ + [ + {<<"arn">>, <<"arn:aws:access-analyzer:us-east-1:123456789012:analyzer/test-1">>}, + {<<"name">>, <<"test-1">>}, + {<<"type">>, <<"ACCOUNT">>}, + {<<"createdAt">>, <<"2022-01-01T01:02:03Z">>}, + {<<"status">>, <<"ACTIVE">>} + ], + [ + {<<"arn">>, <<"arn:aws:access-analyzer:us-east-1:123456789012:analyzer/test-2">>}, + {<<"name">>, <<"test-2">>}, + {<<"type">>, <<"ACCOUNT">>}, + {<<"createdAt">>, <<"2022-02-02T01:02:03Z">>}, + {<<"status">>, <<"ACTIVE">>} + ] + ], + ?assertEqual({ok, Analyzers}, Result) + end + } + ]. + +get_analyzer_tests(_) -> + [ + { + "GetAnalyzer", + fun() -> + AwsConfig = ?TEST_AWS_CONFIG, + meck:expect( + erlcloud_httpc, request, + fun(A1, A2, A3, A4, A5, A6) -> + Url = "https://access-analyzer.us-east-1.amazonaws.com/analyzer/test-1", + Method = get, + RequestContent = <<>>, + case [A1, A2, A3, A4, A5, A6] of + [Url, Method, _Headers, RequestContent, _Timeout, AwsConfig] -> + ResponseContent = << + "{" + "\"analyzer\":{" + "\"arn\":\"arn:aws:access-analyzer:us-east-1:123456789012:analyzer/test-1\"," + "\"name\":\"test-1\"," + "\"type\":\"ACCOUNT\"," + "\"createdAt\":\"2022-01-01T01:02:03Z\"," + "\"status\":\"ACTIVE\"" + "}" + "}" + >>, + {ok, {{200, "OK"}, [], ResponseContent}} + end + end + ), + Result = erlcloud_access_analyzer:get_analyzer(AwsConfig, _AnalyzerName = "test-1"), + Analyzer = [ + {<<"arn">>, <<"arn:aws:access-analyzer:us-east-1:123456789012:analyzer/test-1">>}, + {<<"name">>, <<"test-1">>}, + {<<"type">>, <<"ACCOUNT">>}, + {<<"createdAt">>, <<"2022-01-01T01:02:03Z">>}, + {<<"status">>, <<"ACTIVE">>} + ], + ?assertEqual({ok, Analyzer}, Result) + end + }, + { + "GetAnalyzer -> not_found", + fun() -> + AwsConfig = ?TEST_AWS_CONFIG, + meck:expect( + erlcloud_httpc, request, + fun(A1, A2, A3, A4, A5, A6) -> + Url = "https://access-analyzer.us-east-1.amazonaws.com/analyzer/test-2", + Method = get, + RequestContent = <<>>, + case [A1, A2, A3, A4, A5, A6] of + [Url, Method, _Headers, RequestContent, _Timeout, AwsConfig] -> + ResponseHeaders = [{"x-amzn-errortype", "ResourceNotFoundException"}], + ResponseContent = <<"{\"message\": \"Analyzer not found\"}">>, + {ok, {{404, "NotFound"}, ResponseHeaders, ResponseContent}} + end + end + ), + Result = erlcloud_access_analyzer:get_analyzer(AwsConfig, _AnalyzerName = "test-2"), + ?assertEqual({error, not_found}, Result) + end + } + ]. + +create_analyzer_tests(_) -> + [ + { + "CreateAnalyzer", + fun() -> + AwsConfig = ?TEST_AWS_CONFIG, + meck:expect( + erlcloud_httpc, request, + fun(A1, A2, A3, A4, A5, A6) -> + Url = "https://access-analyzer.us-east-1.amazonaws.com/analyzer", + Method = put, + RequestContent = << + "{" + "\"analyzerName\":\"test-1\"," + "\"type\":\"ACCOUNT\"" + "}" + >>, + case [A1, A2, A3, A4, A5, A6] of + [Url, Method, _Headers, RequestContent, _Timeout, AwsConfig] -> + ResponseContent = <<"{\"arn\":\"arn:aws:access-analyzer:us-east-1:123456789012:analyzer/test-1\"}">>, + {ok, {{200, "OK"}, [], ResponseContent}} + end + end + ), + AnalyzerSpec = [ + {<<"analyzerName">>, <<"test-1">>}, + {<<"type">>, <<"ACCOUNT">>} + ], + Result = erlcloud_access_analyzer:create_analyzer(AwsConfig, AnalyzerSpec), + Arn = <<"arn:aws:access-analyzer:us-east-1:123456789012:analyzer/test-1">>, + ?assertEqual({ok, Arn}, Result) + end + } + ]. + +delete_analyzer_tests(_) -> + [ + { + "DeleteAnalyzer", + fun() -> + AwsConfig = ?TEST_AWS_CONFIG, + meck:expect( + erlcloud_httpc, request, + fun(A1, A2, A3, A4, A5, A6) -> + Url = "https://access-analyzer.us-east-1.amazonaws.com/analyzer/test-1", + Method = delete, + RequestContent = <<>>, + case [A1, A2, A3, A4, A5, A6] of + [Url, Method, _Headers, RequestContent, _Timeout, AwsConfig] -> + ResponseContent = <<>>, + {ok, {{200, "OK"}, [], ResponseContent}} + end + end + ), + Result = erlcloud_access_analyzer:delete_analyzer(AwsConfig, _AnalyzerName = "test-1"), + ?assertEqual(ok, Result) + end + }, + { + "DeleteAnalyzer -> not_found", + fun() -> + AwsConfig = ?TEST_AWS_CONFIG, + meck:expect( + erlcloud_httpc, request, + fun(A1, A2, A3, A4, A5, A6) -> + Url = "https://access-analyzer.us-east-1.amazonaws.com/analyzer/test-1", + Method = delete, + RequestContent = <<>>, + case [A1, A2, A3, A4, A5, A6] of + [Url, Method, _Headers, RequestContent, _Timeout, AwsConfig] -> + ResponseHeaders = [{"x-amzn-errortype", "ResourceNotFoundException"}], + ResponseContent = <<"{\"message\": \"Analyzer not found\"}">>, + {ok, {{404, "NotFound"}, ResponseHeaders, ResponseContent}} + end + end + ), + Result = erlcloud_access_analyzer:delete_analyzer(AwsConfig, _AnalyzerName = "test-1"), + ?assertEqual({error, not_found}, Result) + end + } + ]. diff --git a/test/erlcloud_as_tests.erl b/test/erlcloud_as_tests.erl index f6580d4c1..eeb202f5a 100644 --- a/test/erlcloud_as_tests.erl +++ b/test/erlcloud_as_tests.erl @@ -151,6 +151,37 @@ mocked_groups() -> 10 + + + + my-test-asg-lbs + ELB + 2013-05-06T17:47:15.107Z + + + lt-036fea5ec210c3294 + asg-test-launch-template-spec-1 + 2 + + + 2 + + us-east-1b + us-east-1a + + + my-test-asg-loadbalancer + + 2 + + 120 + 300 + arn:aws:autoscaling:us-east-1:803981987763:autoScalingGroup:ca861182-c8f9-4ca7-b1eb-cd35505f5ebb:autoScalingGroupName/my-test-asg-lbs + + Default + + 10 + @@ -171,7 +202,23 @@ expected_groups() -> vpc_zone_id = [""], instances = [], status = [] - + }, #aws_autoscaling_group{ + group_name = "my-test-asg-lbs", + availability_zones = ["us-east-1b", "us-east-1a"], + load_balancer_names = ["my-test-asg-loadbalancer"], + tags = [], + desired_capacity = 2, + min_size = 2, + max_size = 10, + launch_configuration_name = [], + launch_template = #aws_launch_template_spec{ + id = "lt-036fea5ec210c3294", + name = "asg-test-launch-template-spec-1", + version = "2" + }, + vpc_zone_id = [""], + instances = [], + status = [] }]. mocked_instances() -> diff --git a/test/erlcloud_athena_tests.erl b/test/erlcloud_athena_tests.erl index 36e3462ce..4d4bebe97 100644 --- a/test/erlcloud_athena_tests.erl +++ b/test/erlcloud_athena_tests.erl @@ -21,6 +21,9 @@ -define(LOCATION_1, <<"s3://1/1.csv">>). -define(LOCATION_2, <<"s3://2/2.csv">>). -define(CLIENT_TOKEN, <<"some-token-uuid">>). +-define(STATEMENT_NAME_1, <<"some-statement-name">>). +-define(STATEMENT_NAME_2, <<"some-statement-name-2">>). +-define(WORKGROUP, <<"workgroup-name">>). -define(BATCH_GET_NAMED_QUERY_RESP, #{<<"NamedQueries">> => @@ -37,6 +40,21 @@ <<"UnprocessedNamedQueryIds">> => []} ). +-define(BATCH_GET_PREPARED_STATEMENT_RESP, + #{<<"QueryExecutions">> => + [#{<<"Description">> => ?QUERY_STR_1, + <<"LastModifiedTime">> => 1506093456.234, + <<"QueryStatement">> => ?QUERY_STR_1, + <<"StatementName">> => ?STATEMENT_NAME_1, + <<"WorkGroupName">> => ?WORKGROUP}, + #{<<"Description">> => ?QUERY_STR_2, + <<"LastModifiedTime">> => 1506094499.507, + <<"QueryStatement">> => ?QUERY_STR_2, + <<"StatementName">> => ?STATEMENT_NAME_2, + <<"WorkGroupName">> => ?WORKGROUP}], + <<"UnprocessedQueryExecutionIds">> => []} +). + -define(BATCH_GET_QUERY_EXECUTION_RESP, #{<<"QueryExecutions">> => [#{<<"Query">> => ?QUERY_STR_1, @@ -72,6 +90,15 @@ <<"QueryString">> => ?QUERY_STR_1}} ). +-define(GET_PREPARED_STATEMENT_RESP, + #{<<"PreparedStatement">> => + #{<<"Description">> => ?QUERY_STR_1, + <<"LastModifiedTime">> => 1506093456.234, + <<"QueryStatement">> => ?QUERY_STR_1, + <<"StatementName">> => ?STATEMENT_NAME_1, + <<"WorkGroupName">> => ?WORKGROUP}} +). + -define(GET_QUERY_EXECUTION_RESP, #{<<"QueryExecutions">> => #{<<"Query">> => ?QUERY_STR_1, @@ -121,6 +148,13 @@ #{<<"NamedQueryIds">> => [?QUERY_ID_1, ?QUERY_ID_2], <<"NextToken">> => ?CLIENT_TOKEN}). +-define(LIST_PREPARED_STATEMENTS_RESP, + #{<<"NextToken">> => ?CLIENT_TOKEN, + <<"PreparedStatements">> => + [#{<<"LastModifiedTime">> => 1506094499.507, + <<"StatementName">> => ?STATEMENT_NAME_1}] + }). + -define(LIST_QUERY_EXECUTIONS_RESP, #{<<"QueryExecutionIds">> => [?QUERY_ID_1, ?QUERY_ID_2], <<"NextToken">> => ?CLIENT_TOKEN}). @@ -142,21 +176,28 @@ erlcloud_athena_test_() -> fun meck:unload/1, [ fun test_batch_get_named_query/0, + fun test_batch_get_prepared_statement/0, fun test_batch_get_query_execution/0, fun test_create_named_query/0, + fun test_create_prepared_statement/0, fun test_delete_named_query/0, + fun test_delete_prepared_statements/0, fun test_get_named_query/0, + fun test_get_prepared_statement/0, fun test_get_query_execution/0, fun test_get_query_results/0, fun test_list_named_queries/0, + fun test_list_prepared_statements/0, fun test_list_query_executions/0, fun test_start_query_execution/0, fun test_start_query_execution_with_encryption/0, fun test_start_query_execution_without_kms_key/0, fun test_start_query_execution_without_encrypt_option/0, + fun test_start_query_execution_with_workgroup_option/0, fun test_stop_query_execution/0, fun test_error_no_retry/0, - fun test_error_retry/0 + fun test_error_retry/0, + fun test_update_prepared_statement/0 ] }. @@ -168,6 +209,14 @@ test_batch_get_named_query() -> end, do_test(Request, Expected, TestFun). +test_batch_get_prepared_statement() -> + Request = #{<<"WorkGroup">> => ?WORKGROUP, <<"PreparedStatementNames">> => [?STATEMENT_NAME_1, ?STATEMENT_NAME_2]}, + Expected = {ok, ?BATCH_GET_PREPARED_STATEMENT_RESP}, + TestFun = fun() -> + erlcloud_athena:batch_get_prepared_statement(?WORKGROUP, [?STATEMENT_NAME_1, ?STATEMENT_NAME_2]) + end, + do_test(Request, Expected, TestFun). + test_batch_get_query_execution() -> Request = #{<<"QueryExecutionIds">> => [?QUERY_ID_1, ?QUERY_ID_2]}, Expected = {ok, ?BATCH_GET_QUERY_EXECUTION_RESP}, @@ -181,27 +230,48 @@ test_create_named_query() -> <<"Database">> => ?DB_NAME_1, <<"Name">> => ?QUERY_NAME_1, <<"QueryString">> => ?QUERY_STR_1, - <<"Description">> => ?QUERY_DESC_1}, + <<"Description">> => ?QUERY_DESC_1, + <<"WorkGroup">> => ?WORKGROUP}, Expected = {ok, ?QUERY_ID_1}, TestFun = fun() -> erlcloud_athena:create_named_query(?CLIENT_TOKEN, ?DB_NAME_1, ?QUERY_NAME_1, ?QUERY_STR_1, - ?QUERY_DESC_1) + ?QUERY_DESC_1, [{workgroup, ?WORKGROUP}]) end, do_test(Request, Expected, TestFun). +test_create_prepared_statement() -> + Request = #{<<"WorkGroup">> => ?WORKGROUP, + <<"StatementName">> => ?STATEMENT_NAME_1, + <<"QueryStatement">> => ?QUERY_STR_1}, + Expected = ok, + TestFun = fun() -> erlcloud_athena:create_prepared_statement(?WORKGROUP, ?STATEMENT_NAME_1, ?QUERY_STR_1) end, + do_test(Request, Expected, TestFun). + test_delete_named_query() -> Request = #{<<"NamedQueryId">> => ?QUERY_ID_1}, Expected = ok, TestFun = fun() -> erlcloud_athena:delete_named_query(?QUERY_ID_1) end, do_test(Request, Expected, TestFun). +test_delete_prepared_statements() -> + Request = #{<<"WorkGroup">> => ?WORKGROUP, <<"StatementName">> => ?STATEMENT_NAME_1}, + Expected = ok, + TestFun = fun() -> erlcloud_athena:delete_prepared_statement(?WORKGROUP, ?STATEMENT_NAME_1) end, + do_test(Request, Expected, TestFun). + test_get_named_query() -> Request = #{<<"NamedQueryId">> => ?QUERY_ID_1}, Expected = {ok, ?GET_NAMED_QUERY_RESP}, TestFun = fun() -> erlcloud_athena:get_named_query(?QUERY_ID_1) end, do_test(Request, Expected, TestFun). +test_get_prepared_statement() -> + Request = #{<<"WorkGroup">> => ?WORKGROUP, <<"StatementName">> => ?STATEMENT_NAME_1}, + Expected = {ok, ?GET_PREPARED_STATEMENT_RESP}, + TestFun = fun() -> erlcloud_athena:get_prepared_statement(?WORKGROUP, ?STATEMENT_NAME_1) end, + do_test(Request, Expected, TestFun). + test_get_query_execution() -> Request = #{<<"QueryExecutionId">> => ?QUERY_ID_1}, Expected = {ok, ?GET_QUERY_EXECUTION_RESP}, @@ -220,15 +290,21 @@ test_get_query_results() -> test_list_named_queries() -> Expected = {ok, ?LIST_NAMED_QUERIES_RESP}, - TestFun = fun() -> erlcloud_athena:list_named_queries() end, - do_test(#{}, Expected, TestFun). + TestFun = fun() -> erlcloud_athena:list_named_queries(#{}, [{workgroup, ?WORKGROUP}]) end, + do_test(#{<<"WorkGroup">> => ?WORKGROUP}, Expected, TestFun). + +test_list_prepared_statements() -> + Request = #{<<"WorkGroup">> => ?WORKGROUP}, + Expected = {ok, ?LIST_PREPARED_STATEMENTS_RESP}, + TestFun = fun() -> erlcloud_athena:list_prepared_statements(?WORKGROUP) end, + do_test(Request, Expected, TestFun). test_list_query_executions() -> Request = #{<<"MaxResults">> => 1, <<"NextToken">> => ?CLIENT_TOKEN}, Expected = {ok, ?LIST_QUERY_EXECUTIONS_RESP}, - TestFun = fun() -> erlcloud_athena:list_query_executions(Request) end, - do_test(Request, Expected, TestFun). + TestFun = fun() -> erlcloud_athena:list_query_executions(Request, [{workgroup, ?WORKGROUP}]) end, + do_test(Request#{<<"WorkGroup">> => ?WORKGROUP}, Expected, TestFun). test_start_query_execution() -> Request = get_start_query_execution_req(#{}), @@ -274,6 +350,17 @@ test_start_query_execution_without_encrypt_option() -> end, do_test(Request, Expected, TestFun). +test_start_query_execution_with_workgroup_option() -> + Request = get_start_query_execution_req(#{<<"WorkGroup">> => ?WORKGROUP},#{}), + Expected = {ok, ?QUERY_ID_1}, + TestFun = fun() -> + erlcloud_athena:start_query_execution(?CLIENT_TOKEN, ?DB_NAME_1, + ?QUERY_STR_1, ?LOCATION_1, + undefined, undefined, + [{workgroup, ?WORKGROUP}]) + end, + do_test(Request, Expected, TestFun). + test_stop_query_execution() -> Request = #{<<"QueryExecutionId">> => ?QUERY_ID_1}, Expected = ok, @@ -307,6 +394,14 @@ test_error_retry() -> erlcloud_athena:stop_query_execution(?QUERY_ID_1) ). +test_update_prepared_statement() -> + Request = #{<<"WorkGroup">> => ?WORKGROUP, + <<"StatementName">> => ?STATEMENT_NAME_1, + <<"QueryStatement">> => ?QUERY_STR_2}, + Expected = ok, + TestFun = fun() -> erlcloud_athena:update_prepared_statement(?WORKGROUP, ?STATEMENT_NAME_1, ?QUERY_STR_2) end, + do_test(Request, Expected, TestFun). + do_test(Request, ExpectedResult, TestedFun) -> erlcloud_athena:configure("test-access-key", "test-secret-key"), ?assertEqual(ExpectedResult, TestedFun()), @@ -319,23 +414,31 @@ do_erlcloud_httpc_request(_, post, Headers, _, _, _) -> ["AmazonAthena", Operation] = string:tokens(Target, "."), RespBody = case Operation of - "BatchGetNamedQuery" -> ?BATCH_GET_NAMED_QUERY_RESP; - "BatchGetQueryExecution" -> ?BATCH_GET_QUERY_EXECUTION_RESP; - "CreateNamedQuery" -> ?CREATE_NAMED_QUERY_RESP; - "DeleteNamedQuery" -> #{}; - "GetNamedQuery" -> ?GET_NAMED_QUERY_RESP; - "GetQueryExecution" -> ?GET_QUERY_EXECUTION_RESP; - "GetQueryResults" -> ?GET_QUERY_RESULTS_RESP; - "ListNamedQueries" -> ?LIST_NAMED_QUERIES_RESP; - "ListQueryExecutions" -> ?LIST_QUERY_EXECUTIONS_RESP; - "StartQueryExecution" -> ?START_QUERY_EXECUTION_RESP; - "StopQueryExecution" -> #{} + "BatchGetNamedQuery" -> ?BATCH_GET_NAMED_QUERY_RESP; + "BatchGetPreparedStatement" -> ?BATCH_GET_PREPARED_STATEMENT_RESP; + "BatchGetQueryExecution" -> ?BATCH_GET_QUERY_EXECUTION_RESP; + "CreateNamedQuery" -> ?CREATE_NAMED_QUERY_RESP; + "CreatePreparedStatement" -> #{}; + "DeleteNamedQuery" -> #{}; + "DeletePreparedStatement" -> #{}; + "GetNamedQuery" -> ?GET_NAMED_QUERY_RESP; + "GetPreparedStatement" -> ?GET_PREPARED_STATEMENT_RESP; + "GetQueryExecution" -> ?GET_QUERY_EXECUTION_RESP; + "GetQueryResults" -> ?GET_QUERY_RESULTS_RESP; + "ListNamedQueries" -> ?LIST_NAMED_QUERIES_RESP; + "ListPreparedStatements" -> ?LIST_PREPARED_STATEMENTS_RESP; + "ListQueryExecutions" -> ?LIST_QUERY_EXECUTIONS_RESP; + "StartQueryExecution" -> ?START_QUERY_EXECUTION_RESP; + "StopQueryExecution" -> #{}; + "UpdatePreparedStatement" -> #{} end, {ok, {{200, "OK"}, [], jsx:encode(RespBody)}}. get_start_query_execution_req(EncryptConfig) -> + get_start_query_execution_req(#{}, EncryptConfig). +get_start_query_execution_req(ReqBody, EncryptConfig) -> ResultConfig = EncryptConfig#{<<"OutputLocation">> => ?LOCATION_1}, - #{<<"ClientRequestToken">> => ?CLIENT_TOKEN, - <<"QueryExecutionContext">> => #{<<"Database">> => ?DB_NAME_1}, - <<"QueryString">> => ?QUERY_STR_1, - <<"ResultConfiguration">> => ResultConfig}. + ReqBody#{<<"ClientRequestToken">> => ?CLIENT_TOKEN, + <<"QueryExecutionContext">> => #{<<"Database">> => ?DB_NAME_1}, + <<"QueryString">> => ?QUERY_STR_1, + <<"ResultConfiguration">> => ResultConfig}. diff --git a/test/erlcloud_autoscaling_tests.erl b/test/erlcloud_autoscaling_tests.erl index 833268573..6179e5e0a 100644 --- a/test/erlcloud_autoscaling_tests.erl +++ b/test/erlcloud_autoscaling_tests.erl @@ -140,7 +140,7 @@ output_expect_seq(Responses) -> %% output_test converts an output_test specifier into an eunit test generator --type output_test_spec() :: {pos_integer(), {string(), term()} | {string(), string(), term()}}. +-type output_test_spec() :: {pos_integer(), {string(), term()} | {string(), string() | [string()], term()}}. -spec output_test(fun(), output_test_spec(), fun()) -> tuple(). output_test(Fun, {Line, {Description, Response, Result}}, OutputFun) -> {Description, @@ -306,6 +306,7 @@ describe_autoscaling_groups_all_output_tests(_) -> ", " + blah @@ -339,11 +340,52 @@ describe_autoscaling_groups_all_output_tests(_) -> 0f02a07d-b677-11e2-9eb0-dd50EXAMPLE +", " + + + + + + + my-test-asg-lbs + ELB + 2013-05-06T17:47:15.107Z + + + lt-036fea5ec210c3294 + asg-test-launch-template-spec-1 + 2 + + + 2 + + us-east-1b + us-east-1a + + + my-test-asg-loadbalancer + + 2 + + 120 + 300 + arn:aws:autoscaling:us-east-1:803981987763:autoScalingGroup:ca861182-c8f9-4ca7-b1eb-cd35505f5ebb:autoScalingGroupName/my-test-asg-lbs + + Default + + 10 + + + + + 0f02a07d-b677-11e2-9eb0-dd50EXAMPLE + "], {ok, [[ {autoscaling_group_name, "my-test-asg-lbs"}, {autoscaling_group_arn, "arn:aws:autoscaling:us-east-1:803981987763:autoScalingGroup:ca861182-c8f9-4ca7-b1eb-cd35505f5ebb:autoScalingGroupName/my-test-asg-lbs"}, {launch_configuration_name, "my-test-lc1"}, + {launch_template, []}, {min_size, 2}, {max_size, 10}, {create_time,{{2013,5,6},{17,47,15}}}, @@ -360,6 +402,7 @@ describe_autoscaling_groups_all_output_tests(_) -> {autoscaling_group_name, "my-test-asg-lbs"}, {autoscaling_group_arn, "arn:aws:autoscaling:us-east-1:803981987763:autoScalingGroup:ca861182-c8f9-4ca7-b1eb-cd35505f5ebb:autoScalingGroupName/my-test-asg-lbs"}, {launch_configuration_name, "my-test-lc2"}, + {launch_template, []}, {min_size, 2}, {max_size, 10}, {create_time,{{2013,5,6},{17,47,15}}}, @@ -376,6 +419,7 @@ describe_autoscaling_groups_all_output_tests(_) -> {autoscaling_group_name, "my-test-asg-lbs"}, {autoscaling_group_arn, "arn:aws:autoscaling:us-east-1:803981987763:autoScalingGroup:ca861182-c8f9-4ca7-b1eb-cd35505f5ebb:autoScalingGroupName/my-test-asg-lbs"}, {launch_configuration_name, "my-test-lc3"}, + {launch_template, []}, {min_size, 2}, {max_size, 10}, {create_time,{{2013,5,6},{17,47,15}}}, @@ -392,6 +436,7 @@ describe_autoscaling_groups_all_output_tests(_) -> {autoscaling_group_name, "my-test-asg-lbs"}, {autoscaling_group_arn, "arn:aws:autoscaling:us-east-1:803981987763:autoScalingGroup:ca861182-c8f9-4ca7-b1eb-cd35505f5ebb:autoScalingGroupName/my-test-asg-lbs"}, {launch_configuration_name, "my-test-lc4"}, + {launch_template, []}, {min_size, 2}, {max_size, 10}, {create_time,{{2013,5,6},{17,47,15}}}, @@ -404,7 +449,28 @@ describe_autoscaling_groups_all_output_tests(_) -> {load_balancers,["my-test-asg-loadbalancer"]}, {instances,[]}, {tag_set,[]} - ]]}})], + ], [ + {autoscaling_group_name, "my-test-asg-lbs"}, + {autoscaling_group_arn, "arn:aws:autoscaling:us-east-1:803981987763:autoScalingGroup:ca861182-c8f9-4ca7-b1eb-cd35505f5ebb:autoScalingGroupName/my-test-asg-lbs"}, + {launch_configuration_name, []}, + {launch_template, [ + {launch_template_id, "lt-036fea5ec210c3294"}, + {launch_template_name, "asg-test-launch-template-spec-1"}, + {launch_template_version, "2"} + ]}, + {min_size, 2}, + {max_size, 10}, + {create_time,{{2013,5,6},{17,47,15}}}, + {health_check_type, "ELB"}, + {desired_capacity,2}, + {placement_group,[]}, + {status,[]}, + {subnets, []}, + {availability_zones,["us-east-1b","us-east-1a"]}, + {load_balancers,["my-test-asg-loadbalancer"]}, + {instances,[]}, + {tag_set,[]} + ]]}})], %% Remaining AWS API examples return subsets of the same data output_tests_seq(?_f(erlcloud_autoscaling:describe_autoscaling_groups_all()), Tests). @@ -477,6 +543,7 @@ describe_autoscaling_groups_output_tests(_) -> {autoscaling_group_name, "my-test-asg-lbs"}, {autoscaling_group_arn, "arn:aws:autoscaling:us-east-1:803981987763:autoScalingGroup:ca861182-c8f9-4ca7-b1eb-cd35505f5ebb:autoScalingGroupName/my-test-asg-lbs"}, {launch_configuration_name, "my-test-lc"}, + {launch_template, []}, {min_size, 2}, {max_size, 10}, {create_time,{{2013,5,6},{17,47,15}}}, diff --git a/test/erlcloud_aws_tests.erl b/test/erlcloud_aws_tests.erl index 59668263c..6796b8947 100644 --- a/test/erlcloud_aws_tests.erl +++ b/test/erlcloud_aws_tests.erl @@ -7,16 +7,16 @@ request_test_() -> {foreach, fun start/0, fun stop/1, - [fun request_default_test/1, - fun request_retry_test/1, - fun request_prot_host_port_str_test/1, - fun request_prot_host_port_int_test/1, - fun get_service_status_test/1, - fun auto_config_with_env/1]}. + [fun test_request_default/1, + fun test_request_retry/1, + fun test_request_prot_host_port_str/1, + fun test_request_prot_host_port_int/1, + fun test_get_service_status/1, + fun test_auto_config_with_env/1]}. start() -> meck:new(erlcloud_httpc), - meck:expect(erlcloud_httpc, request, fun(_,_,_,_,_,_) -> {ok, {{200, "OK"}, [], ok}} end), + meck:expect(erlcloud_httpc, request, fun(_,_,_,_,_,_) -> {ok, {{200, "OK"}, [], <<"OkBody">>}} end), ok. stop(_) -> @@ -28,12 +28,12 @@ config() -> retry = fun erlcloud_retry:default_retry/1, retry_num = 3}. -request_default_test(_) -> - ok = erlcloud_aws:aws_request(get, "host", "/", [], "id", "key"), +test_request_default(_) -> + <<"OkBody">> = erlcloud_aws:aws_request(get, "host", "/", [], "id", "key"), Url = get_url_from_history(meck:history(erlcloud_httpc)), test_url(https, "host", 443, "/", Url). -request_retry_test(_) -> +test_request_retry(_) -> Response400 = {ok, {{400, "Bad Request"}, [], <<"\n" " \n" @@ -66,27 +66,27 @@ request_retry_test(_) -> erlcloud_aws:aws_request_xml4(get, "host", "/", [], "any", config()); (ResponseSeq) -> meck:sequence(erlcloud_httpc, request, 6, ResponseSeq), - erlcloud_aws:aws_request(get, "host", "/", [], config()) + <<"OkBody">> = erlcloud_aws:aws_request(get, "host", "/", [], config()) end, - [?_assertNotException(_, _, <<"OkBody">> = MeckAndRequest([Response400, Response200])), - ?_assertNotException(_, _, <<"OkBody">> = MeckAndRequest([Response400, Response500, Response200])), - ?_assertNotException(_, _, <<"OkBody">> = MeckAndRequest([Response429, Response200])), + [?_assertMatch(<<"OkBody">>, MeckAndRequest([Response400, Response200])), + ?_assertMatch(<<"OkBody">>, MeckAndRequest([Response400, Response500, Response200])), + ?_assertMatch(<<"OkBody">>, MeckAndRequest([Response429, Response200])), ?_assertMatch({error, {http_error, 400, "Bad Request", _ErrorMsg}}, MeckAndRequest({[Response400, Response500, Response400, Response200], xml4})) ]. -request_prot_host_port_str_test(_) -> - ok = erlcloud_aws:aws_request(get, "http", "host1", "9999", "/path1", [], "id", "key"), +test_request_prot_host_port_str(_) -> + <<"OkBody">> = erlcloud_aws:aws_request(get, "http", "host1", "9999", "/path1", [], "id", "key"), Url = get_url_from_history(meck:history(erlcloud_httpc)), test_url(http, "host1", 9999, "/path1", Url). -request_prot_host_port_int_test(_) -> - ok = erlcloud_aws:aws_request(get, "http", "host1", 9999, "/path1", [], "id", "key"), +test_request_prot_host_port_int(_) -> + <<"OkBody">> = erlcloud_aws:aws_request(get, "http", "host1", 9999, "/path1", [], "id", "key"), Url = get_url_from_history(meck:history(erlcloud_httpc)), test_url(http, "host1", 9999, "/path1", Url). -get_service_status_test(_) -> +test_get_service_status(_) -> StatusJsonS3 = jsx:encode( [{<<"archive">>, [[{<<"service_name">>, @@ -147,7 +147,7 @@ get_service_status_test(_) -> OKStatusEmpty = erlcloud_aws:get_service_status(["sqs", "sns"]), meck:expect(erlcloud_httpc, request, fun(_,_,_,_,_,_) -> {ok, {{200, "OK"}, [], StatusJsonS3}} end), OKStatus = erlcloud_aws:get_service_status(["cloudformation", "sns", "vpc"]), - + [?_assertEqual(proplists:get_value(<<"status">>, S3Status), 0), ?_assertEqual(proplists:get_value(<<"service">>, S3Status), <<"s3-eu-central-1">>), ?_assertEqual(proplists:get_value(<<"status">>, EC2Status), 2), @@ -157,7 +157,7 @@ get_service_status_test(_) -> ]. -auto_config_with_env(_) -> +test_auto_config_with_env(_) -> % Note: meck do not support os module X_AWS_ACCESS = os:getenv("AWS_ACCESS_KEY_ID"), X_AWS_SECRET = os:getenv("AWS_SECRET_ACCESS_KEY"), @@ -189,7 +189,7 @@ get_url_from_history([{_, {erlcloud_httpc, request, [Url, _, _, _, _, _]}, _}]) Url. test_url(ExpScheme, ExpHost, ExpPort, ExpPath, Url) -> - {ok, {Scheme, _UserInfo, Host, Port, Path, _Query}} = http_uri:parse(Url), + {ok, {Scheme, _UserInfo, Host, Port, Path, _Query}} = erlcloud_util:uri_parse(Url), [?_assertEqual(ExpScheme, Scheme), ?_assertEqual(ExpHost, Host), ?_assertEqual(ExpPort, Port), @@ -237,7 +237,7 @@ profile_indirect_test_() -> erlcloud_aws:profile( blah ) ) ) }. - + profile_indirect_role_test_() -> {setup, fun profiles_assume_setup/0, fun profiles_assume_cleanup/1, ?_test( @@ -309,14 +309,14 @@ profile_undefined_profile_test_() -> ?assertMatch( {error, _}, erlcloud_aws:profile( what ) ) ) }. - + profile_undefined_indirect_profile_test_() -> {setup, fun profiles_test_setup/0, fun profiles_test_cleanup/1, ?_test( ?assertMatch( {error, _}, erlcloud_aws:profile( whoa ) ) ) }. - + profiles_test_setup() -> Profile = <<" @@ -416,7 +416,8 @@ service_config_autoscaling_test() -> <<"eu-west-1">>, <<"eu-central-1">>, <<"ap-northeast-1">>, <<"ap-northeast-2">>, <<"ap-southeast-1">>, <<"ap-southeast-2">>, - <<"sa-east-1">>], + <<"sa-east-1">>, + <<"cn-north-1">>, <<"cn-northwest-1">>], Expected = ["autoscaling.us-east-1.amazonaws.com", "autoscaling.us-west-1.amazonaws.com", "autoscaling.us-west-2.amazonaws.com", @@ -426,7 +427,9 @@ service_config_autoscaling_test() -> "autoscaling.ap-northeast-2.amazonaws.com", "autoscaling.ap-southeast-1.amazonaws.com", "autoscaling.ap-southeast-2.amazonaws.com", - "autoscaling.sa-east-1.amazonaws.com"], + "autoscaling.sa-east-1.amazonaws.com", + "autoscaling.cn-north-1.amazonaws.com.cn", + "autoscaling.cn-northwest-1.amazonaws.com.cn"], ?assertEqual( Expected, [H || #aws_config{ as_host = H } <- [erlcloud_aws:service_config( @@ -440,7 +443,8 @@ service_config_cloudformation_test() -> <<"eu-west-1">>, <<"eu-central-1">>, <<"ap-northeast-1">>, <<"ap-northeast-2">>, <<"ap-southeast-1">>, <<"ap-southeast-2">>, - <<"sa-east-1">>], + <<"sa-east-1">>, + <<"cn-north-1">>, <<"cn-northwest-1">>], Expected = ["cloudformation.us-east-1.amazonaws.com", "cloudformation.us-west-1.amazonaws.com", "cloudformation.us-west-2.amazonaws.com", @@ -450,7 +454,9 @@ service_config_cloudformation_test() -> "cloudformation.ap-northeast-2.amazonaws.com", "cloudformation.ap-southeast-1.amazonaws.com", "cloudformation.ap-southeast-2.amazonaws.com", - "cloudformation.sa-east-1.amazonaws.com"], + "cloudformation.sa-east-1.amazonaws.com", + "cloudformation.cn-north-1.amazonaws.com.cn", + "cloudformation.cn-northwest-1.amazonaws.com.cn"], ?assertEqual( Expected, [H || #aws_config{ cloudformation_host = H } <- [erlcloud_aws:service_config( @@ -468,7 +474,8 @@ service_config_cloudtrail_test() -> <<"eu-west-1">>, <<"eu-central-1">>, <<"ap-northeast-1">>, <<"ap-northeast-2">>, <<"ap-southeast-1">>, <<"ap-southeast-2">>, - <<"sa-east-1">>], + <<"sa-east-1">>, + <<"cn-north-1">>, <<"cn-northwest-1">>], Expected = ["cloudtrail.us-east-1.amazonaws.com", "cloudtrail.us-west-1.amazonaws.com", "cloudtrail.us-west-2.amazonaws.com", @@ -478,7 +485,9 @@ service_config_cloudtrail_test() -> "cloudtrail.ap-northeast-2.amazonaws.com", "cloudtrail.ap-southeast-1.amazonaws.com", "cloudtrail.ap-southeast-2.amazonaws.com", - "cloudtrail.sa-east-1.amazonaws.com"], + "cloudtrail.sa-east-1.amazonaws.com", + "cloudtrail.cn-north-1.amazonaws.com.cn", + "cloudtrail.cn-northwest-1.amazonaws.com.cn"], ?assertEqual( Expected, [H || #aws_config{ cloudtrail_host = H } <- [erlcloud_aws:service_config( @@ -492,7 +501,8 @@ service_config_dynamodb_test() -> <<"eu-west-1">>, <<"eu-central-1">>, <<"ap-northeast-1">>, <<"ap-northeast-2">>, <<"ap-southeast-1">>, <<"ap-southeast-2">>, - <<"sa-east-1">>], + <<"sa-east-1">>, + <<"cn-north-1">>, <<"cn-northwest-1">>], Expected = ["dynamodb.us-east-1.amazonaws.com", "dynamodb.us-west-1.amazonaws.com", "dynamodb.us-west-2.amazonaws.com", @@ -502,7 +512,9 @@ service_config_dynamodb_test() -> "dynamodb.ap-northeast-2.amazonaws.com", "dynamodb.ap-southeast-1.amazonaws.com", "dynamodb.ap-southeast-2.amazonaws.com", - "dynamodb.sa-east-1.amazonaws.com"], + "dynamodb.sa-east-1.amazonaws.com", + "dynamodb.cn-north-1.amazonaws.com.cn", + "dynamodb.cn-northwest-1.amazonaws.com.cn"], ?assertEqual( Expected, [H || #aws_config{ ddb_host = H } <- [erlcloud_aws:service_config( @@ -520,7 +532,8 @@ service_config_dynamodb_streams_test() -> <<"eu-west-1">>, <<"eu-central-1">>, <<"ap-northeast-1">>, <<"ap-northeast-2">>, <<"ap-southeast-1">>, <<"ap-southeast-2">>, - <<"sa-east-1">>], + <<"sa-east-1">>, + <<"cn-north-1">>, <<"cn-northwest-1">>], Expected = ["streams.dynamodb.us-east-1.amazonaws.com", "streams.dynamodb.us-west-1.amazonaws.com", "streams.dynamodb.us-west-2.amazonaws.com", @@ -530,7 +543,9 @@ service_config_dynamodb_streams_test() -> "streams.dynamodb.ap-northeast-2.amazonaws.com", "streams.dynamodb.ap-southeast-1.amazonaws.com", "streams.dynamodb.ap-southeast-2.amazonaws.com", - "streams.dynamodb.sa-east-1.amazonaws.com"], + "streams.dynamodb.sa-east-1.amazonaws.com", + "streams.dynamodb.cn-north-1.amazonaws.com.cn", + "streams.dynamodb.cn-northwest-1.amazonaws.com.cn"], ?assertEqual( Expected, [H || #aws_config{ ddb_streams_host = H } <- [erlcloud_aws:service_config( @@ -543,7 +558,8 @@ service_config_ec2_test() -> <<"eu-west-1">>, <<"eu-central-1">>, <<"ap-northeast-1">>, <<"ap-northeast-2">>, <<"ap-southeast-1">>, <<"ap-southeast-2">>, - <<"sa-east-1">>], + <<"sa-east-1">>, + <<"cn-north-1">>, <<"cn-northwest-1">>], Expected = ["ec2.us-east-1.amazonaws.com", "ec2.us-west-1.amazonaws.com", "ec2.us-west-2.amazonaws.com", @@ -553,7 +569,9 @@ service_config_ec2_test() -> "ec2.ap-northeast-2.amazonaws.com", "ec2.ap-southeast-1.amazonaws.com", "ec2.ap-southeast-2.amazonaws.com", - "ec2.sa-east-1.amazonaws.com"], + "ec2.sa-east-1.amazonaws.com", + "ec2.cn-north-1.amazonaws.com.cn", + "ec2.cn-northwest-1.amazonaws.com.cn"], ?assertEqual( Expected, [H || #aws_config{ ec2_host = H } <- [erlcloud_aws:service_config( @@ -567,7 +585,8 @@ service_config_elasticloadbalancing_test() -> <<"eu-west-1">>, <<"eu-central-1">>, <<"ap-northeast-1">>, <<"ap-northeast-2">>, <<"ap-southeast-1">>, <<"ap-southeast-2">>, - <<"sa-east-1">>], + <<"sa-east-1">>, + <<"cn-north-1">>, <<"cn-northwest-1">>], Expected = ["elasticloadbalancing.us-east-1.amazonaws.com", "elasticloadbalancing.us-west-1.amazonaws.com", "elasticloadbalancing.us-west-2.amazonaws.com", @@ -577,7 +596,9 @@ service_config_elasticloadbalancing_test() -> "elasticloadbalancing.ap-northeast-2.amazonaws.com", "elasticloadbalancing.ap-southeast-1.amazonaws.com", "elasticloadbalancing.ap-southeast-2.amazonaws.com", - "elasticloadbalancing.sa-east-1.amazonaws.com"], + "elasticloadbalancing.sa-east-1.amazonaws.com", + "elasticloadbalancing.cn-north-1.amazonaws.com.cn", + "elasticloadbalancing.cn-northwest-1.amazonaws.com.cn"], ?assertEqual( Expected, [H || #aws_config{ elb_host = H } <- [erlcloud_aws:service_config( @@ -598,7 +619,8 @@ service_config_elasticmapreduce_test() -> <<"ca-central-1">>, <<"eu-west-1">>, <<"eu-central-1">>, <<"ap-northeast-1">>, <<"ap-northeast-2">>, <<"ap-southeast-1">>, <<"ap-southeast-2">>, - <<"sa-east-1">>], + <<"sa-east-1">>, + <<"cn-north-1">>, <<"cn-northwest-1">>], Expected = ["elasticmapreduce.us-east-1.amazonaws.com", "elasticmapreduce.us-east-2.amazonaws.com", "elasticmapreduce.us-west-1.amazonaws.com", @@ -610,7 +632,9 @@ service_config_elasticmapreduce_test() -> "elasticmapreduce.ap-northeast-2.amazonaws.com", "elasticmapreduce.ap-southeast-1.amazonaws.com", "elasticmapreduce.ap-southeast-2.amazonaws.com", - "elasticmapreduce.sa-east-1.amazonaws.com"], + "elasticmapreduce.sa-east-1.amazonaws.com", + "elasticmapreduce.cn-north-1.amazonaws.com.cn", + "elasticmapreduce.cn-northwest-1.amazonaws.com.cn"], ?assertEqual( Expected, [H || #aws_config{ emr_host = H } <- [erlcloud_aws:service_config( @@ -637,13 +661,24 @@ service_config_iam_test() -> Service, Region, #aws_config{} ) || Region <- Regions]] ). +service_config_china_iam_test() -> + Service = <<"iam">>, + Regions = [<<"cn-north-1">>, <<"cn-northwest-1">>], + Expected = lists:duplicate( length(Regions), "iam.amazonaws.com.cn" ), + ?assertEqual( Expected, + [H || #aws_config{ iam_host = H } <- + [erlcloud_aws:service_config( + Service, Region, #aws_config{} ) + || Region <- Regions]] ). + service_config_kinesis_test() -> Service = <<"kinesis">>, Regions = [<<"us-east-1">>, <<"us-west-1">>, <<"us-west-2">>, <<"eu-west-1">>, <<"eu-central-1">>, <<"ap-northeast-1">>, <<"ap-northeast-2">>, <<"ap-southeast-1">>, <<"ap-southeast-2">>, - <<"sa-east-1">>], + <<"sa-east-1">>, + <<"cn-north-1">>, <<"cn-northwest-1">>], Expected = ["kinesis.us-east-1.amazonaws.com", "kinesis.us-west-1.amazonaws.com", "kinesis.us-west-2.amazonaws.com", @@ -653,7 +688,9 @@ service_config_kinesis_test() -> "kinesis.ap-northeast-2.amazonaws.com", "kinesis.ap-southeast-1.amazonaws.com", "kinesis.ap-southeast-2.amazonaws.com", - "kinesis.sa-east-1.amazonaws.com"], + "kinesis.sa-east-1.amazonaws.com", + "kinesis.cn-north-1.amazonaws.com.cn", + "kinesis.cn-northwest-1.amazonaws.com.cn"], ?assertEqual( Expected, [H || #aws_config{ kinesis_host = H } <- [erlcloud_aws:service_config( @@ -681,7 +718,8 @@ service_config_rds_test() -> <<"eu-west-1">>, <<"eu-central-1">>, <<"ap-northeast-1">>, <<"ap-northeast-2">>, <<"ap-southeast-1">>, <<"ap-southeast-2">>, - <<"sa-east-1">>], + <<"sa-east-1">>, + <<"cn-north-1">>, <<"cn-northwest-1">>], Expected = ["rds.us-east-1.amazonaws.com", "rds.us-west-1.amazonaws.com", "rds.us-west-2.amazonaws.com", @@ -691,7 +729,9 @@ service_config_rds_test() -> "rds.ap-northeast-2.amazonaws.com", "rds.ap-southeast-1.amazonaws.com", "rds.ap-southeast-2.amazonaws.com", - "rds.sa-east-1.amazonaws.com"], + "rds.sa-east-1.amazonaws.com", + "rds.cn-north-1.amazonaws.com.cn", + "rds.cn-northwest-1.amazonaws.com.cn"], ?assertEqual( Expected, [H || #aws_config{ rds_host = H } <- [erlcloud_aws:service_config( @@ -704,20 +744,21 @@ service_config_s3_test() -> <<"eu-west-1">>, <<"eu-central-1">>, <<"ap-northeast-1">>, <<"ap-northeast-2">>, <<"ap-southeast-1">>, <<"ap-southeast-2">>, - <<"sa-east-1">>, - <<"us-gov-west-1">>, <<"cn-north-1">>], + <<"sa-east-1">>, <<"us-gov-west-1">>, + <<"cn-north-1">>, <<"cn-northwest-1">>], Expected = ["s3-external-1.amazonaws.com", - "s3-us-west-1.amazonaws.com", - "s3-us-west-2.amazonaws.com", - "s3-eu-west-1.amazonaws.com", - "s3-eu-central-1.amazonaws.com", - "s3-ap-northeast-1.amazonaws.com", - "s3-ap-northeast-2.amazonaws.com", - "s3-ap-southeast-1.amazonaws.com", - "s3-ap-southeast-2.amazonaws.com", - "s3-sa-east-1.amazonaws.com", + "s3.us-west-1.amazonaws.com", + "s3.us-west-2.amazonaws.com", + "s3.eu-west-1.amazonaws.com", + "s3.eu-central-1.amazonaws.com", + "s3.ap-northeast-1.amazonaws.com", + "s3.ap-northeast-2.amazonaws.com", + "s3.ap-southeast-1.amazonaws.com", + "s3.ap-southeast-2.amazonaws.com", + "s3.sa-east-1.amazonaws.com", "s3-fips-us-gov-west-1.amazonaws.com", - "s3.cn-north-1.amazonaws.com.cn"], + "s3.cn-north-1.amazonaws.com.cn", + "s3.cn-northwest-1.amazonaws.com.cn"], ?assertEqual( Expected, [H || #aws_config{ s3_host = H } <- [erlcloud_aws:service_config( @@ -766,7 +807,8 @@ service_config_sns_test() -> <<"ap-northeast-1">>, <<"ap-northeast-2">>, <<"ap-southeast-1">>, <<"ap-southeast-2">>, <<"sa-east-1">>, - <<"us-gov-west-1">>, <<"cn-north-1">>], + <<"us-gov-west-1">>, + <<"cn-north-1">>, <<"cn-northwest-1">>], Expected = ["sns.us-east-1.amazonaws.com", "sns.us-west-1.amazonaws.com", "sns.us-west-2.amazonaws.com", @@ -778,7 +820,8 @@ service_config_sns_test() -> "sns.ap-southeast-2.amazonaws.com", "sns.sa-east-1.amazonaws.com", "sns.us-gov-west-1.amazonaws.com", - "sns.cn-north-1.amazonaws.com"], + "sns.cn-north-1.amazonaws.com.cn", + "sns.cn-northwest-1.amazonaws.com.cn"], ?assertEqual( Expected, [H || #aws_config{ sns_host = H } <- [erlcloud_aws:service_config( @@ -792,8 +835,8 @@ service_config_sqs_test() -> <<"eu-west-1">>, <<"eu-central-1">>, <<"ap-northeast-1">>, <<"ap-northeast-2">>, <<"ap-southeast-1">>, <<"ap-southeast-2">>, - <<"cn-north-1">>, - <<"sa-east-1">>], + <<"sa-east-1">>, + <<"cn-north-1">>, <<"cn-northwest-1">>], Expected = ["sqs.us-east-1.amazonaws.com", "sqs.us-west-1.amazonaws.com", "sqs.us-west-2.amazonaws.com", @@ -804,8 +847,9 @@ service_config_sqs_test() -> "sqs.ap-northeast-2.amazonaws.com", "sqs.ap-southeast-1.amazonaws.com", "sqs.ap-southeast-2.amazonaws.com", - "sqs.cn-north-1.amazonaws.com", - "sqs.sa-east-1.amazonaws.com"], + "sqs.sa-east-1.amazonaws.com", + "sqs.cn-north-1.amazonaws.com.cn", + "sqs.cn-northwest-1.amazonaws.com.cn"], ?assertEqual( Expected, [H || #aws_config{ sqs_host = H } <- [erlcloud_aws:service_config( @@ -821,15 +865,15 @@ service_config_sts_test() -> <<"eu-west-1">>, <<"eu-central-1">>, <<"ap-northeast-1">>, <<"ap-northeast-2">>, <<"ap-southeast-1">>, <<"ap-southeast-2">>, - <<"cn-north-1">>, - <<"sa-east-1">>], + <<"sa-east-1">>, + <<"cn-north-1">>, <<"cn-northwest-1">>], RegionsAlt = ["us-east-1", "us-west-1", "us-west-2", "us-gov-west-1", "eu-west-1", "eu-central-1", "ap-northeast-1", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", - "cn-north-1", - "sa-east-1"], + "sa-east-1", + "cn-north-1", "cn-northwest-1"], Expected = ["sts.us-east-1.amazonaws.com", "sts.us-west-1.amazonaws.com", "sts.us-west-2.amazonaws.com", @@ -840,8 +884,9 @@ service_config_sts_test() -> "sts.ap-northeast-2.amazonaws.com", "sts.ap-southeast-1.amazonaws.com", "sts.ap-southeast-2.amazonaws.com", - "sts.cn-north-1.amazonaws.com", - "sts.sa-east-1.amazonaws.com"], + "sts.sa-east-1.amazonaws.com", + "sts.cn-north-1.amazonaws.com.cn", + "sts.cn-northwest-1.amazonaws.com.cn"], ?assertEqual( Expected, [H || #aws_config{ sts_host = H } <- [erlcloud_aws:service_config( @@ -867,3 +912,83 @@ service_config_waf_test() -> [erlcloud_aws:service_config( Service, Region, #aws_config{} ) || Region <- Regions]] ). + +get_host_vpc_endpoint_setup_fun() -> + meck:new(erlcloud_ec2_meta), + meck:expect(erlcloud_ec2_meta, generate_session_token, + fun(60, #aws_config{}) -> + {ok, <<"60_seconds_imdsv2_token">>} + end), + meck:expect(erlcloud_ec2_meta, get_instance_metadata, + fun("placement/availability-zone", #aws_config{}, <<"60_seconds_imdsv2_token">>) -> + {ok, <<"us-east-1a">>} + end). + +get_host_vpc_endpoint_teardown(_) -> + application:unset_env(erlcloud, availability_zone), + application:unset_env(erlcloud, services_vpc_endpoints), + meck:unload(erlcloud_ec2_meta). + + +get_host_vpc_endpoint_test_() -> + {foreach, + fun get_host_vpc_endpoint_setup_fun/0, + fun get_host_vpc_endpoint_teardown/1, + [fun test_get_host_vpc_endpoint_no_settings/0, + fun test_get_host_vpc_endpoint_no_os_env/0, + fun test_get_host_vpc_endpoint_invalid_os_env/0, + fun test_get_host_vpc_endpoint_valid_os_env/0 + ]}. + +test_get_host_vpc_endpoint_no_settings() -> + Default = <<"kinesis.us-east-1.amazonaws.com">>, + ?assertEqual(Default, + erlcloud_aws:get_host_vpc_endpoint(<<"kinesis">>, Default)), + ?assertEqual(0, meck:num_calls(erlcloud_ec2_meta, get_instance_metadata, '_')). + + +test_get_host_vpc_endpoint_no_os_env() -> + Service = <<"kinesis">>, + EnvVar = "KINESIS_VPC_ENDPOINTS", + Default = <<"kinesis.us-east-1.amazonaws.com">>, + application:set_env(erlcloud, services_vpc_endpoints, [{Service, {env, EnvVar}}]), + ?assertEqual(Default, + erlcloud_aws:get_host_vpc_endpoint(<<"kinesis">>, Default)), + ?assertEqual(0, meck:num_calls(erlcloud_ec2_meta, get_instance_metadata, '_')). + +test_get_host_vpc_endpoint_invalid_os_env() -> + Service = <<"kinesis">>, + EnvVar = "KINESIS_VPC_ENDPOINTS", + EnvSetting = "vpc-xyz123-us-east-foobar.kinesis.us-east-1.vpce.amazonaws.com", + true = os:putenv("KINESIS_VPC_ENDPOINTS", EnvSetting), + Default = <<"kinesis.us-east-1.amazonaws.com">>, + application:set_env(erlcloud, services_vpc_endpoints, [{Service, {env, EnvVar}}]), + try + ?assertEqual(Default, + erlcloud_aws:get_host_vpc_endpoint(<<"kinesis">>, Default)), + ?assertEqual(1, meck:num_calls(erlcloud_ec2_meta, get_instance_metadata, '_')) + after + true = os:unsetenv(EnvVar) + end. + +test_get_host_vpc_endpoint_valid_os_env() -> + Service = <<"kinesis">>, + EnvVar = "KINESIS_VPC_ENDPOINTS", + EnvSetting = "ABC:vpc-xyz123-us-east-1a.kinesis.us-east-1.vpce.amazonaws.com," + "DEF:vpc-xyz123-us-east-1b.kinesis.us-east-1.vpce.amazonaws.com", + true = os:putenv("KINESIS_VPC_ENDPOINTS", EnvSetting), + Default = <<"kinesis.us-east-1.amazonaws.com">>, + application:set_env(erlcloud, services_vpc_endpoints, [{Service, {env, EnvVar}}]), + try + ?assertEqual(undefined, + application:get_env(erlcloud, availability_zone)), + ?assertEqual(<<"vpc-xyz123-us-east-1a.kinesis.us-east-1.vpce.amazonaws.com">>, + erlcloud_aws:get_host_vpc_endpoint(<<"kinesis">>, Default)), + ?assertEqual({ok, <<"us-east-1a">>}, + application:get_env(erlcloud, availability_zone)), + ?assertEqual(<<"vpc-xyz123-us-east-1a.kinesis.us-east-1.vpce.amazonaws.com">>, + erlcloud_aws:get_host_vpc_endpoint(<<"kinesis">>, Default)), + ?assertEqual(1, meck:num_calls(erlcloud_ec2_meta, get_instance_metadata, '_')) + after + true = os:unsetenv(EnvVar) + end. diff --git a/test/erlcloud_cloudformation_tests.erl b/test/erlcloud_cloudformation_tests.erl index 2338387d5..afb005f1f 100644 --- a/test/erlcloud_cloudformation_tests.erl +++ b/test/erlcloud_cloudformation_tests.erl @@ -5,6 +5,7 @@ -include_lib("eunit/include/eunit.hrl"). -include("erlcloud.hrl"). -include("erlcloud_aws.hrl"). +-include("erlcloud_cloudformation.hrl"). -define(_cloudformation_test(T), {?LINE, T}). @@ -16,7 +17,18 @@ describe_cloudformation_test_() -> {foreach, fun start/0, fun stop/1, [ + fun create_stack_input_tests/1, + fun create_stack_input_with_parameters_tests/1, + fun create_stack_input_with_tags_tests/1, + fun create_stack_input_with_rollback_config_tests/1, fun list_stacks_all_output_tests/1, + fun update_stack_input_tests/1, + fun update_stack_input_with_parameters_tests/1, + fun update_stack_input_with_tags_tests/1, + fun update_stack_input_with_rollback_config_tests/1, + fun delete_stack_input_tests/1, + fun delete_stack_input_with_retain_resources_tests/1, + fun delete_stack_input_with_client_request_token_tests/1, fun list_stack_resources_output_tests/1, fun get_template_summary_output_tests/1, fun get_template_output_tests/1, @@ -96,6 +108,113 @@ input_test(Response, {Line, {Description, Fun, Params}}) %% Input Tests %%============================================================================== +create_stack_input_tests(_) -> + Response = {ok, element(1, xmerl_scan:string( + binary_to_list(<<"null">>) + ))}, + + ExpectedParams = [ + {"Action","CreateStack"}, + {"Version","2010-05-15"}, + {"OnFailure","ROLLBACK"}, + {"StackName","TestStack"} + ], + + input_test(Response, ?_cloudformation_test({"Test create stack input", + ?_f(erlcloud_cloudformation:create_stack(#cloudformation_create_stack_input{stack_name="TestStack"}, #aws_config{})), + ExpectedParams})). + +create_stack_input_with_parameters_tests(_) -> + Response = {ok, element(1, xmerl_scan:string( + binary_to_list(<<"null">>) + ))}, + + ExpectedParams = [ + {"Action","CreateStack"}, + {"Version","2010-05-15"}, + {"OnFailure","ROLLBACK"}, + {"StackName","TestStack"}, + {"Parameters.member.1.ParameterKey", "TestParamKey1"}, + {"Parameters.member.1.ParameterValue", "TestParamVal1"}, + {"Parameters.member.2.ParameterKey", "TestParamKey2"}, + {"Parameters.member.2.ParameterValue", "TestParamVal2"}, + {"Parameters.member.3.ParameterKey", "TestParamKey3"}, + {"Parameters.member.3.ParameterValue", "TestParamVal3"} + ], + + input_test(Response, ?_cloudformation_test({"Test create stack input", + ?_f(erlcloud_cloudformation:create_stack( + #cloudformation_create_stack_input{ + stack_name="TestStack", + parameters=[ + #cloudformation_parameter{parameter_key="TestParamKey1", parameter_value="TestParamVal1"}, + #cloudformation_parameter{parameter_key="TestParamKey2", parameter_value="TestParamVal2"}, + #cloudformation_parameter{parameter_key="TestParamKey3", parameter_value="TestParamVal3"} + ] + }, + #aws_config{} + )), + ExpectedParams})). + +create_stack_input_with_tags_tests(_) -> + Response = {ok, element(1, xmerl_scan:string( + binary_to_list(<<"null">>) + ))}, + + ExpectedParams = [ + {"Action","CreateStack"}, + {"Version","2010-05-15"}, + {"OnFailure","ROLLBACK"}, + {"StackName","TestStack"}, + {"Tags.member.1.Key", "TagKey1"}, + {"Tags.member.1.Value", "TagVal1"}, + {"Tags.member.2.Key", "TagKey2"}, + {"Tags.member.2.Value", "TagVal2"} + ], + + input_test(Response, ?_cloudformation_test({"Test create stack input", + ?_f(erlcloud_cloudformation:create_stack( + #cloudformation_create_stack_input{ + stack_name="TestStack", + tags=[ + #cloudformation_tag{key="TagKey0", value="TagVal0"}, + #cloudformation_tag{key="TagKey1", value="TagVal1"} + ] + }, + #aws_config{} + )), + ExpectedParams})). + +create_stack_input_with_rollback_config_tests(_) -> + Response = {ok, element(1, xmerl_scan:string( + binary_to_list(<<"null">>) + ))}, + + ExpectedParams = [ + {"Action","CreateStack"}, + {"Version","2010-05-15"}, + {"OnFailure","ROLLBACK"}, + {"StackName","TestStack"}, + {"RollbackConfiguration.MonitoringTimeInMinutes", 10}, + {"RollbackConfiguration.RollbackTriggers.member.1.Arn", "TestARN"}, + {"RollbackConfiguration.RollbackTriggers.member.1.Type", "TestType"} + ], + + input_test(Response, ?_cloudformation_test({"Test create stack input", + ?_f(erlcloud_cloudformation:create_stack( + #cloudformation_create_stack_input{ + stack_name="TestStack", + rollback_configuration = #cloudformation_rollback_configuration{ + monitoring_time_in_minutes = 10, + rollback_triggers = [ + #cloudformation_rollback_trigger{arn="TestARN", type="TestType"} + ] + } + }, + #aws_config{} + )), + ExpectedParams})). + list_stacks_all_input_tests(_) -> Response = {ok, element(1, xmerl_scan:string( binary_to_list(<<"null">>) @@ -107,6 +226,176 @@ list_stacks_all_input_tests(_) -> ?_f(erlcloud_cloudformation:list_stacks_all([], #aws_config{})), ExpectedParams})). +update_stack_input_tests(_) -> + Response = {ok, element(1, xmerl_scan:string( + binary_to_list(<<"null">>) + ))}, + + ExpectedParams = [ + {"Action","UpdateStack"}, + {"Version","2010-05-15"}, + {"StackName","TestStack"} + ], + + input_test(Response, ?_cloudformation_test({"Test update stack input", + ?_f(erlcloud_cloudformation:update_stack(#cloudformation_update_stack_input{stack_name="TestStack"}, #aws_config{})), + ExpectedParams})). + + +update_stack_input_with_parameters_tests(_) -> + Response = {ok, element(1, xmerl_scan:string( + binary_to_list(<<"null">>) + ))}, + + ExpectedParams = [ + {"Action","UpdateStack"}, + {"Version","2010-05-15"}, + {"StackName","TestStack"}, + {"Parameters.member.1.ParameterKey", "TestParamKey1"}, + {"Parameters.member.1.ParameterValue", "TestParamVal1"}, + {"Parameters.member.2.ParameterKey", "TestParamKey2"}, + {"Parameters.member.2.ParameterValue", "TestParamVal2"}, + {"Parameters.member.3.ParameterKey", "TestParamKey3"}, + {"Parameters.member.3.ParameterValue", "TestParamVal3"} + ], + + input_test(Response, ?_cloudformation_test({"Test update stack input with parameters", + ?_f(erlcloud_cloudformation:update_stack( + #cloudformation_update_stack_input{ + stack_name="TestStack", + parameters=[ + #cloudformation_parameter{parameter_key="TestParamKey1", parameter_value="TestParamVal1"}, + #cloudformation_parameter{parameter_key="TestParamKey2", parameter_value="TestParamVal2"}, + #cloudformation_parameter{parameter_key="TestParamKey3", parameter_value="TestParamVal3"} + ] + }, + #aws_config{} + )), + ExpectedParams})). + +update_stack_input_with_tags_tests(_) -> + Response = {ok, element(1, xmerl_scan:string( + binary_to_list(<<"null">>) + ))}, + + ExpectedParams = [ + {"Action","UpdateStack"}, + {"Version","2010-05-15"}, + {"StackName","TestStack"}, + {"Tags.member.1.Key", "TagKey1"}, + {"Tags.member.1.Value", "TagVal1"}, + {"Tags.member.2.Key", "TagKey2"}, + {"Tags.member.2.Value", "TagVal2"} + ], + + input_test(Response, ?_cloudformation_test({"Test update stack input with tags", + ?_f(erlcloud_cloudformation:update_stack( + #cloudformation_update_stack_input{ + stack_name="TestStack", + tags=[ + #cloudformation_tag{key="TagKey0", value="TagVal0"}, + #cloudformation_tag{key="TagKey1", value="TagVal1"} + ] + }, + #aws_config{} + )), + ExpectedParams})). + +update_stack_input_with_rollback_config_tests(_) -> + Response = {ok, element(1, xmerl_scan:string( + binary_to_list(<<"null">>) + ))}, + + ExpectedParams = [ + {"Action","UpdateStack"}, + {"Version","2010-05-15"}, + {"StackName","TestStack"}, + {"RollbackConfiguration.MonitoringTimeInMinutes", 10}, + {"RollbackConfiguration.RollbackTriggers.member.1.Arn", "TestARN"}, + {"RollbackConfiguration.RollbackTriggers.member.1.Type", "TestType"} + ], + + input_test(Response, ?_cloudformation_test({"Test update stack input with rollback configuration", + ?_f(erlcloud_cloudformation:update_stack( + #cloudformation_update_stack_input{ + stack_name="TestStack", + rollback_configuration = #cloudformation_rollback_configuration{ + monitoring_time_in_minutes = 10, + rollback_triggers = [ + #cloudformation_rollback_trigger{arn="TestARN", type="TestType"} + ] + } + }, + #aws_config{} + )), + ExpectedParams})). + +delete_stack_input_tests(_) -> + Response = {ok, element(1, xmerl_scan:string( + binary_to_list(<<"null">>) + ))}, + + ExpectedParams = [ + {"Action", "DeleteStack"}, + {"Version", "2010-05-15"}, + {"StackName", "TestStack"} + ], + + input_test(Response, ?_cloudformation_test({"Test Delete Stack input", + ?_f(erlcloud_cloudformation:delete_stack( + #cloudformation_delete_stack_input{stack_name="TestStack"}, + #aws_config{} + )), + ExpectedParams})). + +delete_stack_input_with_retain_resources_tests(_) -> + Response = {ok, element(1, xmerl_scan:string( + binary_to_list(<<"null">>) + ))}, + + ExpectedParams = [ + {"Action", "DeleteStack"}, + {"Version", "2010-05-15"}, + {"StackName", "TestStack"}, + {"RetainResources.member.1", "arn::ec2::MyTestInstance1"}, + {"RetainResources.member.2", "arn::ec2::MyTestInstance2"} + ], + + input_test(Response, ?_cloudformation_test({"Test Delete Stack input", + ?_f(erlcloud_cloudformation:delete_stack( + #cloudformation_delete_stack_input{ + stack_name="TestStack", + retain_resources = [ + "arn::ec2::MyTestInstance1", + "arn::ec2::MyTestInstance2" + ] + }, + #aws_config{} + )), + ExpectedParams})). + +delete_stack_input_with_client_request_token_tests(_) -> + Response = {ok, element(1, xmerl_scan:string( + binary_to_list(<<"null">>) + ))}, + + ExpectedParams = [ + {"Action","DeleteStack"}, + {"Version","2010-05-15"}, + {"StackName","TestStack"}, + {"ClientRequestToken","TestClientRequestToken"} + ], + + input_test(Response, ?_cloudformation_test({"Test Delete Stack input", + ?_f(erlcloud_cloudformation:delete_stack( + #cloudformation_delete_stack_input{ + stack_name="TestStack", + client_request_token="TestClientRequestToken" + }, + #aws_config{} + )), + ExpectedParams})). + list_stack_resources_input_tests(_) -> Response = {ok, element(1, xmerl_scan:string( binary_to_list(<<"null" @@ -500,7 +789,7 @@ describe_stack_resource_output_tests(_) -> MyDBInstance Resource ID Resource Type - Tiemstamp + Timestamp Status @@ -525,7 +814,7 @@ describe_stack_resource_output_tests(_) -> describe_stack_events_output_tests(_) -> - Test = ?_cloudformation_test({"Test describe alls tack events", + Test = ?_cloudformation_test({"Test describe all tack events", <<" diff --git a/test/erlcloud_cloudtrail_tests.erl b/test/erlcloud_cloudtrail_tests.erl index 55e27b565..57e2e6acd 100644 --- a/test/erlcloud_cloudtrail_tests.erl +++ b/test/erlcloud_cloudtrail_tests.erl @@ -33,6 +33,8 @@ operation_test_() -> fun delete_trail_output_tests/1, fun describe_trails_input_tests/1, fun describe_trails_output_tests/1, + fun get_event_selectors_input_tests/1, + fun get_event_selectors_output_tests/1, fun get_trail_status_input_tests/1, fun get_trail_status_output_tests/1, fun start_logging_input_tests/1, @@ -69,8 +71,8 @@ sort_json(V) -> %% verifies that the parameters in the body match the expected parameters -spec validate_body(binary(), expected_body()) -> ok. validate_body(Body, Expected) -> - Want = sort_json(jsx:decode(list_to_binary(Expected))), - Actual = sort_json(jsx:decode(Body)), + Want = sort_json(decode(list_to_binary(Expected))), + Actual = sort_json(decode(Body)), case Want =:= Actual of true -> ok; false -> @@ -123,7 +125,7 @@ output_expect(Response) -> end. %% output_test converts an output_test specifier into an eunit test generator --type output_test_spec() :: {pos_integer(), {string(), term()} | {string(), string(), term()}}. +-type output_test_spec() :: {pos_integer(), {string(), term()} | {string(), string() | binary(), term()}}. -spec output_test(fun(), output_test_spec()) -> tuple(). output_test(Fun, {Line, {Description, Response, Result}}) -> {Description, @@ -187,7 +189,7 @@ create_trail_output_tests(_) -> Tests = [?_cloudtrail_test( {"CreateTrail example response", Response, - {ok, jsx:decode(Response)}}) + {ok, decode(Response)}}) ], output_tests(?_f(erlcloud_cloudtrail:create_trail("test", "test_bucket", "test_prefix", "test_topic", true, erlcloud_aws:default_config())), Tests). @@ -214,7 +216,7 @@ delete_trail_output_tests(_) -> Tests = [?_cloudtrail_test( {"DeleteTrail example response", Response, - {ok, jsx:decode(Response)}}) + {ok, decode(Response)}}) ], output_tests(?_f(erlcloud_cloudtrail:delete_trail("test", erlcloud_aws:default_config())), Tests). @@ -240,7 +242,7 @@ start_logging_output_tests(_) -> Tests = [?_cloudtrail_test( {"StartLogging example response", Response, - {ok, jsx:decode(Response)}}) + {ok, decode(Response)}}) ], output_tests(?_f(erlcloud_cloudtrail:start_logging("test", erlcloud_aws:default_config())), Tests). @@ -266,7 +268,7 @@ stop_logging_output_tests(_) -> Tests = [?_cloudtrail_test( {"StopLogging example response", Response, - {ok, jsx:decode(Response)}}) + {ok, decode(Response)}}) ], output_tests(?_f(erlcloud_cloudtrail:stop_logging("test", erlcloud_aws:default_config())), Tests). @@ -301,10 +303,48 @@ describe_trails_output_tests(_) -> Tests = [?_cloudtrail_test( {"DescribeTrails example response", Response, - {ok, jsx:decode(Response)}}) + {ok, decode(Response)}}) ], output_tests(?_f(erlcloud_cloudtrail:describe_trails(["test"], erlcloud_aws:default_config())), Tests). +%% GetEventSelectors test based on the API examples: +%% https://docs.aws.amazon.com/awscloudtrail/latest/APIReference/API_GetEventSelectors.html +get_event_selectors_input_tests(_) -> + Tests = + [?_cloudtrail_test( + {"GetEventSelectors example request", + ?_f(erlcloud_cloudtrail:get_event_selectors("test", erlcloud_aws:default_config())), " +{ + + \"TrailName\": \"test\" +}" + }) + ], + Response = "{}", + + input_tests(Response, Tests). + +get_event_selectors_output_tests(_) -> + Response = <<"{\"EventSelectors\": [ + { + \"DataResources\": [ + { + \"Type\": \"AWS::S3::Object\", + \"Values\": [ \"arn:aws:s3:::test-bucket/\" ] + } ], + \"IncludeManagementEvents\": true, + \"ReadWriteType\": \"All\" + } ], + \"TrailARN\": \"awn:aws:cloudtrail:us-west-1:1234567890:trail/test-trail\" + }">>, + + Tests = + [?_cloudtrail_test( + {"GetEventSelectors example response", Response, + {ok, decode(Response)}}) + ], + output_tests(?_f(erlcloud_cloudtrail:get_event_selectors("test", erlcloud_aws:default_config())), Tests). + %% GetTrailStatus test based on the API examples: %% http://docs.aws.amazon.com/awscloudtrail/latest/APIReference/API_GetTrailStatus.html get_trail_status_input_tests(_) -> @@ -336,7 +376,7 @@ get_trail_status_output_tests(_) -> Tests = [?_cloudtrail_test( {"GetTrailStatus example response", Response, - {ok, jsx:decode(Response)}}) + {ok, decode(Response)}}) ], output_tests(?_f(erlcloud_cloudtrail:get_trail_status("test", erlcloud_aws:default_config())), Tests). @@ -372,8 +412,10 @@ update_trail_output_tests(_) -> Tests = [?_cloudtrail_test( {"UpdateTrail example response", Response, - {ok, jsx:decode(Response)}}) + {ok, decode(Response)}}) ], output_tests(?_f(erlcloud_cloudtrail:update_trail("test", "test_bucket", "test_prefix", "test_topic", true, erlcloud_aws:default_config())), Tests). +decode(S) -> + jsx:decode(S, [{return_maps, false}]). diff --git a/test/erlcloud_cloudwatch_logs_tests.erl b/test/erlcloud_cloudwatch_logs_tests.erl index efb455955..7a81d6910 100644 --- a/test/erlcloud_cloudwatch_logs_tests.erl +++ b/test/erlcloud_cloudwatch_logs_tests.erl @@ -40,7 +40,9 @@ -define(LOG_STREAM_NAME_PREFIX, <<"welcome">>). -define(LOG_STREAM_NAME, <<"welcome">>). -define(PAGING_TOKEN, <<"arn:aws:logs:us-east-1:352773894028:log-group:/aws/apigateway/welcome:*">>). - +-define(FILTER_NAME_PREFIX, <<"aws/apigateway/welcome">>). +-define(METRIC_NAME, <<"ct_test_metric">>). +-define(METRIC_NAMESPACE, <<"CISBenchmark">>). -define(LOG_GROUP, [ {<<"arn">>, <<"arn:aws:logs:us-east-1:352773894028:log-group:/aws/apigateway/welcome:*">>}, @@ -51,6 +53,20 @@ {<<"storedBytes">>, 85} ]). +-define(METRIC_FILTER, [ + {<<"creationTime">>, 1518024063379}, + {<<"filterName">>, <<"ct_test_filter">>}, + {<<"filterPattern">>, <<"{ ($.errorCode = \"*UnauthorizedOperation\") " + "|| ($.errorCode = \"AccessDenied*\") }">>}, + {<<"logGroupName">>, ?LOG_GROUP_NAME}, + {<<"metricTransformations">>, [ + {<<"defaultValue">>, <<"0">>}, + {<<"metricValue">>, <<"1">>}, + {<<"metricNamespace">>, ?METRIC_NAMESPACE}, + {<<"metricName">>, ?METRIC_NAME} + ]} +]). + -define(LOG_STREAM, [ {<<"arn">>, <<"arn:aws:logs:us-east-1:352773894028:log-group:/aws/apigateway/welcome:log-stream:welcome">>}, {<<"creationTime">>, 1476283527335}, @@ -73,13 +89,28 @@ erlcloud_cloudwatch_test_() -> {foreach, fun start/0, fun stop/1, [ + fun create_log_group_input_test/1, + + fun create_log_stream_input_test/1, + + fun delete_log_group_input_test/1, + + fun delete_log_stream_input_test/1, + fun describe_log_groups_input_tests/1, fun describe_log_groups_output_tests/1, + fun describe_metric_filters_input_tests/1, + fun describe_metric_filters_output_tests/1, + fun describe_log_streams_input_tests/1, fun describe_log_streams_output_tests/1, - fun put_logs_events_input_tests/1 + fun put_logs_events_input_tests/1, + + fun start_query_output_tests/1, + fun stop_query_output_tests/1, + fun get_query_results_output_tests/1 ]}. @@ -101,6 +132,133 @@ stop(_) -> %%============================================================================== +create_log_group_input_test(_) -> + input_tests(jsx:encode([]), [ + ?_cloudwatch_test( + {"Tests creating log group", + ?_f(erlcloud_cloudwatch_logs:create_log_group(?LOG_GROUP_NAME)), + [{<<"Action">>, <<"CreateLogGroup">>}, + {<<"Version">>, ?API_VERSION}, + {<<"logGroupName">>, ?LOG_GROUP_NAME}]} + ), + ?_cloudwatch_test( + {"Tests creating log group with custom AWS config provided", + ?_f(erlcloud_cloudwatch_logs:create_log_group( + ?LOG_GROUP_NAME, + erlcloud_aws:default_config() + )), + [{<<"Action">>, <<"CreateLogGroup">>}, + {<<"Version">>, ?API_VERSION}, + {<<"logGroupName">>, ?LOG_GROUP_NAME}]} + ), + ?_cloudwatch_test( + {"Tests creating log group with AWS Tags and KMS key", + ?_f(erlcloud_cloudwatch_logs:create_log_group( + ?LOG_GROUP_NAME, + [{<<"tag_name">>, <<"tag_value">>}], + "alias/example", + erlcloud_aws:default_config() + )), + [{<<"Action">>, <<"CreateLogGroup">>}, + {<<"tags">>, [{<<"tag_name">>, <<"tag_value">>}]}, + {<<"kmsKeyId">>, <<"alias/example">>}, + {<<"Version">>, ?API_VERSION}, + {<<"logGroupName">>, ?LOG_GROUP_NAME}]} + ), + ?_cloudwatch_test( + {"Tests creating log group without AWS Tags and with KMS key", + ?_f(erlcloud_cloudwatch_logs:create_log_group( + ?LOG_GROUP_NAME, + undefined, + "alias/example", + erlcloud_aws:default_config() + )), + [{<<"Action">>, <<"CreateLogGroup">>}, + {<<"kmsKeyId">>, <<"alias/example">>}, + {<<"Version">>, ?API_VERSION}, + {<<"logGroupName">>, ?LOG_GROUP_NAME}]} + ) + ]). + + +delete_log_group_input_test(_) -> + input_tests(jsx:encode([]), [ + ?_cloudwatch_test( + {"Tests creating log group", + ?_f(erlcloud_cloudwatch_logs:delete_log_group(?LOG_GROUP_NAME)), + [{<<"Action">>, <<"DeleteLogGroup">>}, + {<<"Version">>, ?API_VERSION}, + {<<"logGroupName">>, ?LOG_GROUP_NAME}]} + ), + ?_cloudwatch_test( + {"Tests creating log group with custom AWS config provided", + ?_f(erlcloud_cloudwatch_logs:delete_log_group( + ?LOG_GROUP_NAME, + erlcloud_aws:default_config() + )), + [{<<"Action">>, <<"DeleteLogGroup">>}, + {<<"Version">>, ?API_VERSION}, + {<<"logGroupName">>, ?LOG_GROUP_NAME}]} + ) + ]). + + +create_log_stream_input_test(_) -> + input_tests(jsx:encode([]), [ + ?_cloudwatch_test( + {"Tests creating log stream", + ?_f(erlcloud_cloudwatch_logs:create_log_stream( + ?LOG_GROUP_NAME, + ?LOG_STREAM_NAME + )), + [{<<"Action">>, <<"CreateLogStream">>}, + {<<"Version">>, ?API_VERSION}, + {<<"logGroupName">>, ?LOG_GROUP_NAME}, + {<<"logStreamName">>, ?LOG_STREAM_NAME}]} + ), + ?_cloudwatch_test( + {"Tests creating log stream with custom AWS config provided", + ?_f(erlcloud_cloudwatch_logs:create_log_stream( + ?LOG_GROUP_NAME, + ?LOG_STREAM_NAME, + erlcloud_aws:default_config() + )), + [{<<"Action">>, <<"CreateLogStream">>}, + {<<"Version">>, ?API_VERSION}, + {<<"logGroupName">>, ?LOG_GROUP_NAME}, + {<<"logStreamName">>, ?LOG_STREAM_NAME}]} + ) + ]). + + +delete_log_stream_input_test(_) -> + input_tests(jsx:encode([]), [ + ?_cloudwatch_test( + {"Tests creating log stream", + ?_f(erlcloud_cloudwatch_logs:delete_log_stream( + ?LOG_GROUP_NAME, + ?LOG_STREAM_NAME + )), + [{<<"Action">>, <<"DeleteLogStream">>}, + {<<"Version">>, ?API_VERSION}, + {<<"logGroupName">>, ?LOG_GROUP_NAME}, + {<<"logStreamName">>, ?LOG_STREAM_NAME}]} + ), + ?_cloudwatch_test( + {"Tests creating log stream with custom AWS config provided", + ?_f(erlcloud_cloudwatch_logs:delete_log_stream( + ?LOG_GROUP_NAME, + ?LOG_STREAM_NAME, + erlcloud_aws:default_config() + )), + [{<<"Action">>, <<"DeleteLogStream">>}, + {<<"Version">>, ?API_VERSION}, + {<<"logGroupName">>, ?LOG_GROUP_NAME}, + {<<"logStreamName">>, ?LOG_STREAM_NAME}]} + ) + ]). + + describe_log_groups_input_tests(_) -> input_tests(jsx:encode([{<<"logGroups">>, []}]), [ ?_cloudwatch_test( @@ -172,6 +330,118 @@ describe_log_groups_input_tests(_) -> ]). +describe_metric_filters_input_tests(_) -> + input_tests(jsx:encode([{<<"metricFilters">>, []}]), [ + ?_cloudwatch_test( + {"Tests describing metric filters with no parameters", + ?_f(erlcloud_cloudwatch_logs:describe_metric_filters()), + [{<<"Action">>, <<"DescribeMetricFilters">>}, + {<<"Version">>, ?API_VERSION}, + {<<"limit">>, ?DEFAULT_LIMIT}]} + ), + ?_cloudwatch_test( + {"Tests describing metric filters with custom AWS config provided", + ?_f(erlcloud_cloudwatch_logs:describe_metric_filters( + erlcloud_aws:default_config() + )), + [{<<"Action">>, <<"DescribeMetricFilters">>}, + {<<"Version">>, ?API_VERSION}, + {<<"limit">>, ?DEFAULT_LIMIT}]} + ), + ?_cloudwatch_test( + {"Tests describing metric filters with log group name provided", + ?_f(erlcloud_cloudwatch_logs:describe_metric_filters( + ?LOG_GROUP_NAME + )), + [{<<"Action">>, <<"DescribeMetricFilters">>}, + {<<"Version">>, ?API_VERSION}, + {<<"limit">>, ?DEFAULT_LIMIT}, + {<<"logGroupName">>, ?LOG_GROUP_NAME}]} + ), + ?_cloudwatch_test( + {"Tests describing metric filters with custom AWS config and " + "log group name provided", + ?_f(erlcloud_cloudwatch_logs:describe_metric_filters( + ?LOG_GROUP_NAME, + erlcloud_aws:default_config() + )), + [{<<"Action">>, <<"DescribeMetricFilters">>}, + {<<"Version">>, ?API_VERSION}, + {<<"limit">>, ?DEFAULT_LIMIT}, + {<<"logGroupName">>, ?LOG_GROUP_NAME}]} + ), + ?_cloudwatch_test( + {"Tests describing metric filters with custom AWS config, " + "log group name and limit provided", + ?_f(erlcloud_cloudwatch_logs:describe_metric_filters( + ?LOG_GROUP_NAME, + ?NON_DEFAULT_LIMIT, + erlcloud_aws:default_config() + )), + [{<<"Action">>, <<"DescribeMetricFilters">>}, + {<<"Version">>, ?API_VERSION}, + {<<"limit">>, ?NON_DEFAULT_LIMIT}, + {<<"logGroupName">>, ?LOG_GROUP_NAME}]} + ), + ?_cloudwatch_test( + {"Tests describing metric filters with custom AWS config, " + "log group name, limit and filter name prefix provided", + ?_f(erlcloud_cloudwatch_logs:describe_metric_filters( + ?LOG_GROUP_NAME, + ?NON_DEFAULT_LIMIT, + ?FILTER_NAME_PREFIX, + erlcloud_aws:default_config() + )), + [{<<"Action">>, <<"DescribeMetricFilters">>}, + {<<"Version">>, ?API_VERSION}, + {<<"filterNamePrefix">>, ?FILTER_NAME_PREFIX}, + {<<"limit">>, ?NON_DEFAULT_LIMIT}, + {<<"logGroupName">>, ?LOG_GROUP_NAME}]} + ), + ?_cloudwatch_test( + {"Tests describing metric filters with custom AWS config, " + "log group name, limit, filter name prefix, metric name and " + "metric namespace provided", + ?_f(erlcloud_cloudwatch_logs:describe_metric_filters( + ?LOG_GROUP_NAME, + ?NON_DEFAULT_LIMIT, + ?FILTER_NAME_PREFIX, + ?METRIC_NAME, + ?METRIC_NAMESPACE, + erlcloud_aws:default_config() + )), + [{<<"Action">>, <<"DescribeMetricFilters">>}, + {<<"Version">>, ?API_VERSION}, + {<<"limit">>, ?NON_DEFAULT_LIMIT}, + {<<"filterNamePrefix">>, ?FILTER_NAME_PREFIX}, + {<<"metricName">>, ?METRIC_NAME}, + {<<"metricNamespace">>, ?METRIC_NAMESPACE}, + {<<"logGroupName">>, ?LOG_GROUP_NAME}]} + ), + ?_cloudwatch_test( + {"Tests describing metric filters with custom AWS config, " + "log group name, limit, filter name prefix, metric name, " + "metric namespace and pagination token provided", + ?_f(erlcloud_cloudwatch_logs:describe_metric_filters( + ?LOG_GROUP_NAME, + ?NON_DEFAULT_LIMIT, + ?FILTER_NAME_PREFIX, + ?METRIC_NAME, + ?METRIC_NAMESPACE, + ?PAGING_TOKEN, + erlcloud_aws:default_config() + )), + [{<<"Action">>, <<"DescribeMetricFilters">>}, + {<<"Version">>, ?API_VERSION}, + {<<"limit">>, ?NON_DEFAULT_LIMIT}, + {<<"filterNamePrefix">>, ?FILTER_NAME_PREFIX}, + {<<"metricName">>, ?METRIC_NAME}, + {<<"metricNamespace">>, ?METRIC_NAMESPACE}, + {<<"nextToken">>, ?PAGING_TOKEN}, + {<<"logGroupName">>, ?LOG_GROUP_NAME}]} + ) + ]). + describe_log_groups_output_tests(_) -> output_tests(?_f(erlcloud_cloudwatch_logs:describe_log_groups()), [ ?_cloudwatch_test( @@ -182,6 +452,15 @@ describe_log_groups_output_tests(_) -> ]). +describe_metric_filters_output_tests(_) -> + output_tests(?_f(erlcloud_cloudwatch_logs:describe_metric_filters()), [ + ?_cloudwatch_test( + {"Tests describing all metric filters", + jsx:encode([{<<"metricFilters">>, [?METRIC_FILTER]}]), + {ok, [?METRIC_FILTER], undefined}} + ) + ]). + describe_log_streams_input_tests(_) -> input_tests(jsx:encode([{<<"logStreams">>, []}]), [ ?_cloudwatch_test( @@ -197,7 +476,7 @@ describe_log_streams_input_tests(_) -> ?_cloudwatch_test( {"Tests describing log streams with with log group name and stream name prefix", ?_f(erlcloud_cloudwatch_logs:describe_log_streams( - ?LOG_GROUP_NAME, + ?LOG_GROUP_NAME, ?LOG_STREAM_NAME_PREFIX, erlcloud_aws:default_config() )), @@ -213,7 +492,7 @@ describe_log_streams_input_tests(_) -> {"Tests describing log streams with with log group name, stream name prefix" "and stream sorting", ?_f(erlcloud_cloudwatch_logs:describe_log_streams( - ?LOG_GROUP_NAME, + ?LOG_GROUP_NAME, ?LOG_STREAM_NAME_PREFIX, last_event_time, true, @@ -231,7 +510,7 @@ describe_log_streams_input_tests(_) -> {"Tests describing log streams with with log group name, stream name prefix," "stream sorting and limits", ?_f(erlcloud_cloudwatch_logs:describe_log_streams( - ?LOG_GROUP_NAME, + ?LOG_GROUP_NAME, ?LOG_STREAM_NAME_PREFIX, last_event_time, true, @@ -250,7 +529,7 @@ describe_log_streams_input_tests(_) -> {"Tests describing log streams with with log group name, stream name prefix," "stream sorting, limits and page token", ?_f(erlcloud_cloudwatch_logs:describe_log_streams( - ?LOG_GROUP_NAME, + ?LOG_GROUP_NAME, ?LOG_STREAM_NAME_PREFIX, last_event_time, true, @@ -266,7 +545,7 @@ describe_log_streams_input_tests(_) -> {<<"logStreamNamePrefix">>, ?LOG_STREAM_NAME_PREFIX}, {<<"nextToken">>, ?PAGING_TOKEN}, {<<"orderBy">>,<<"LastEventTime">>}]} - ) + ) ]). @@ -301,6 +580,51 @@ put_logs_events_input_tests(_) -> ) ]). +start_query_output_tests(_) -> + output_tests(?_f(erlcloud_cloudwatch_logs:start_query(["LogGroupName1", "LogGroupName2", "LogGroupName3"], + "stats count(*) by eventSource, eventName, awsRegion", + 1546300800, + 1546309800, + 100)), [ + ?_cloudwatch_test( + {"Tests output format for start_query", + jsx:encode([{<<"queryId">>, <<"12ab3456-12ab-123a-789e-1234567890ab">>}]), + {ok, #{ query_id => "12ab3456-12ab-123a-789e-1234567890ab" }}} + ) + ]). + +stop_query_output_tests(_) -> + output_tests(?_f(erlcloud_cloudwatch_logs:stop_query("12ab3456-12ab-123a-789e-1234567890ab")), [ + ?_cloudwatch_test( + {"Tests output format for stop_query", + jsx:encode([{<<"success">>, true}]), + ok} + ) + ]). + +get_query_results_output_tests(_) -> + output_tests(?_f(erlcloud_cloudwatch_logs:get_query_results("12ab3456-12ab-123a-789e-1234567890ab", [{out, map}])), [ + ?_cloudwatch_test( + {"Tests output format for get_query_results", + jsx:encode([{<<"results">>, [[[{<<"field">>, <<"LogEvent1-field1-name">>}, + {<<"value">>, <<"LogEvent1-field1-value">>}], + [{<<"field">>, <<"LogEvent1-field2-name">>}, + {<<"value">>, <<"LogEvent1-field2-value">>}]]]}, + {<<"statistics">>, [{<<"bytesScanned">>, 81349723.0}, + {<<"recordsMatched">>, 360851.0}, + {<<"recordsScanned">>, 610956.0}]}, + {<<"status">>, <<"Complete">>}]), + {ok, #{ results => [[#{ field => <<"LogEvent1-field1-name">>, + value => <<"LogEvent1-field1-value">> }, + #{ field => <<"LogEvent1-field2-name">>, + value => <<"LogEvent1-field2-value">> }]], + statistics => #{ bytes_scanned => 81349723.0, + records_matched => 360851.0, + records_scanned => 610956.0 }, + status => complete }}} + ) + ]). + %%============================================================================== %% Internal functions %%============================================================================== @@ -317,7 +641,7 @@ input_test(ResponseBody, {Line, {Description, Fun, ExpectedParams}}) -> erlcloud_httpc, request, fun(_Url, post, _Headers, RequestBody, _Timeout, _Config) -> - ActualParams = jsx:decode(RequestBody), + ActualParams = jsx:decode(RequestBody, [{return_maps, false}]), ?assertEqual(sort_json(ExpectedParams), sort_json(ActualParams)), {ok, {{200, "OK"}, [], ResponseBody}} end diff --git a/test/erlcloud_cognito_user_pools_tests.erl b/test/erlcloud_cognito_user_pools_tests.erl new file mode 100644 index 000000000..c311a1f8d --- /dev/null +++ b/test/erlcloud_cognito_user_pools_tests.erl @@ -0,0 +1,1059 @@ +-module(erlcloud_cognito_user_pools_tests). + +-include_lib("eunit/include/eunit.hrl"). +-include("erlcloud_aws.hrl"). + +%% API +-export([]). + +%% define response macros +-define(EHTTPC, erlcloud_httpc). + +-define(USER_POOL_ID, <<"testpool">>). +-define(CLIENT_ID, <<"id-1">>). +-define(USERNAME, <<"test.user">>). + +-define(LIST_USERS_RESP, + #{<<"Users">> => + [ + #{<<"Attributes">> => + [ + #{ + <<"Name">> => <<"sub">>, + <<"Value">> => <<"ec64aac9-78da-4c0f-870c-6389a1ef38bf">>}, + #{ + <<"Name">> => <<"email_verified">>, + <<"Value">> => <<"true">>}, + #{ + <<"Name">> => <<"custom:test1">>, + <<"Value">> => <<"test">>}, + #{ + <<"Name">> => <<"email">>, + <<"Value">> => <<"test@fake.email">>}, + #{ + <<"Name">> => <<"custom:test2">>, + <<"Value">> => <<"test">>} + ], + <<"Enabled">> => true, + <<"UserCreateDate">> => 1632222392.623, + <<"UserLastModifiedDate">> => 1633509297.315, + <<"UserStatus">> => <<"CONFIRMED">>, + <<"Username">> => <<"test.user1">>}, + #{<<"Attributes">> => + [ + #{ + <<"Name">> => <<"sub">>, + <<"Value">> => <<"b23652f5-2340-4137-9a5e-4b40641420eb">>}, + #{ + <<"Name">> => <<"email_verified">>, + <<"Value">> => <<"true">>}, + #{ + <<"Name">> => <<"email">>, + <<"Value">> => <<"test2@fake.email">>} + ], + <<"Enabled">> => true, + <<"UserCreateDate">> => 1631791632.572, + <<"UserLastModifiedDate">> => 1631791778.698, + <<"UserStatus">> => <<"CONFIRMED">>, + <<"Username">> => <<"test.user2">>} + ]}). + +-define(LIST_ALL_USERS, + #{<<"Users">> => + [ + #{<<"Attributes">> => + [ + #{ + <<"Name">> => <<"sub">>, + <<"Value">> => <<"ec64aac9-78da-4c0f-870c-6389a1ef38bf">>}, + #{ + <<"Name">> => <<"email_verified">>, + <<"Value">> => <<"true">>}, + #{ + <<"Name">> => <<"custom:test1">>, + <<"Value">> => <<"test">>}, + #{ + <<"Name">> => <<"email">>, + <<"Value">> => <<"test@fake.email">>}, + #{ + <<"Name">> => <<"custom:test2">>, + <<"Value">> => <<"test">>} + ], + <<"Enabled">> => true, + <<"UserCreateDate">> => 1632222392.623, + <<"UserLastModifiedDate">> => 1633509297.315, + <<"UserStatus">> => <<"CONFIRMED">>, + <<"Username">> => <<"test.user1">>}, + #{<<"Attributes">> => + [ + #{ + <<"Name">> => <<"sub">>, + <<"Value">> => <<"b23652f5-2340-4137-9a5e-4b40641420eb">>}, + #{ + <<"Name">> => <<"email_verified">>, + <<"Value">> => <<"true">>}, + #{ + <<"Name">> => <<"email">>, + <<"Value">> => <<"test2@fake.email">>} + ], + <<"Enabled">> => true, + <<"UserCreateDate">> => 1631791632.572, + <<"UserLastModifiedDate">> => 1631791778.698, + <<"UserStatus">> => <<"CONFIRMED">>, + <<"Username">> => <<"test.user2">>}, + #{<<"Attributes">> => + [ + #{ + <<"Name">> => <<"sub">>, + <<"Value">> => <<"b23652f5-2340-4137-9a5e-4b40641420eb">>}, + #{ + <<"Name">> => <<"email_verified">>, + <<"Value">> => <<"true">>}, + #{ + <<"Name">> => <<"email">>, + <<"Value">> => <<"test3@fake.email">>} + ], + <<"Enabled">> => true, + <<"UserCreateDate">> => 1631791632.572, + <<"UserLastModifiedDate">> => 1631791778.698, + <<"UserStatus">> => <<"CONFIRMED">>, + <<"Username">> => <<"test.user3">>} + ]}). + +-define(ADMIN_LIST_GROUPS_FOR_USERS, + #{<<"Groups">> => + [ + #{ + <<"CreationDate">> => 1632236861.937, + <<"GroupName">> => <<"test">>, + <<"LastModifiedDate">> => 1632236861.937, + <<"UserPoolId">> => ?USER_POOL_ID}, + #{ + <<"CreationDate">> => 1632231111.404, + <<"Description">> => <<"test desc">>, + <<"GroupName">> => <<"test2">>, + <<"LastModifiedDate">> => 1632231111.404, + <<"UserPoolId">> => ?USER_POOL_ID} + ]}). + +-define(ADMIN_GET_USER, + #{<<"Enabled">> => true, + <<"UserAttributes">> => + [ + #{ + <<"Name">> => <<"sub">>, + <<"Value">> => <<"ec64aac9-78da-4c0f-870c-6389a1ef38bf">>}, + #{ + <<"Name">> => <<"email_verified">>, + <<"Value">> => <<"true">>}, + #{ + <<"Name">> => <<"custom:test1">>, + <<"Value">> => <<"test">>}, + #{ + <<"Name">> => <<"email">>, + <<"Value">> => <<"test@fake.email">>}, + #{ + <<"Name">> => <<"custom:test2">>, + <<"Value">> => <<"test">>} + ], + <<"UserCreateDate">> => 1632222392.623, + <<"UserLastModifiedDate">> => 1633509297.315, + <<"UserStatus">> => <<"CONFIRMED">>, + <<"Username">> => ?USERNAME} +). + +-define(ADMIN_CREATE_USER, + #{<<"User">> => + #{<<"Attributes">> => + [ + #{ + <<"Name">> => <<"sub">>, + <<"Value">> => <<"1e172333-dd47-45ee-9da2-7b53d7ff3932">>} + ], + <<"Enabled">> => true, + <<"UserCreateDate">> => 1633946606.409, + <<"UserLastModifiedDate">> => 1633946606.409, + <<"UserStatus">> => <<"FORCE_CHANGE_PASSWORD">>, + <<"Username">> => ?USERNAME}} +). + +-define(CREATE_GROUP, + #{ + <<"Group">> => + #{ + <<"CreationDate">> => 1633949776.261, + <<"GroupName">> => <<"test">>, + <<"LastModifiedDate">> => 1633949776.261, + <<"UserPoolId">> => ?USER_POOL_ID} + }). + +-define(LIST_USER_POOLS, + #{ + <<"NextToken">> => <<"nextpage">>, + <<"UserPools">> => + [ + #{ + <<"CreationDate">> => 1634210679.535, + <<"Id">> => <<"eu-west-testpool1">>, + <<"LambdaConfig">> => #{}, + <<"LastModifiedDate">> => 1634210679.535, + <<"Name">> => ?USER_POOL_ID + }, + #{ + <<"CreationDate">> => 1632131813.194, + <<"Id">> => <<"eu-west-testpool2">>, + <<"LambdaConfig">> => #{}, + <<"LastModifiedDate">> => 1634209858.344, + <<"Name">> => <<"TestPool2">> + } + ] + }). + +-define(LIST_ALL_USER_POOLS, + #{ + <<"UserPools">> => + [ + #{ + <<"CreationDate">> => 1634210679.535, + <<"Id">> => <<"eu-west-testpool1">>, + <<"LambdaConfig">> => #{}, + <<"LastModifiedDate">> => 1634210679.535, + <<"Name">> => ?USER_POOL_ID + }, + #{ + <<"CreationDate">> => 1632131813.194, + <<"Id">> => <<"eu-west-testpool2">>, + <<"LambdaConfig">> => #{}, + <<"LastModifiedDate">> => 1634209858.344, + <<"Name">> => <<"TestPool2">> + }, + #{ + <<"CreationDate">> => 1634210679.535, + <<"Id">> => <<"eu-west-testpool3">>, + <<"LambdaConfig">> => #{}, + <<"LastModifiedDate">> => 1634210679.535, + <<"Name">> => <<"TestPool3">> + } + ] + }). + +-define(DESCRIBE_POOL, #{ + <<"UserPool">> => + #{<<"AccountRecoverySetting">> => + #{<<"RecoveryMechanisms">> => + [#{<<"Name">> => <<"verified_email">>,<<"Priority">> => 1}, + #{<<"Name">> => <<"verified_phone_number">>, + <<"Priority">> => 2}]}, + <<"AdminCreateUserConfig">> => + #{<<"AllowAdminCreateUserOnly">> => true, + <<"InviteMessageTemplate">> => + #{<<"EmailMessage">> => + <<"Your username is {username} and temporary password is {####}. ">>, + <<"EmailSubject">> => <<"Your temporary password">>, + <<"SMSMessage">> => + <<"Your username is {username} and temporary password is {####}. ">>}, + <<"UnusedAccountValidityDays">> => 7}, + <<"Arn">> => + <<"arn:aws:cognito-idp:eu-west-1:87806250403242:userpool/testpool">>, + <<"AutoVerifiedAttributes">> => [<<"email">>], + <<"CreationDate">> => 1636620065.355, + <<"Domain">> => <<"nxdomain3323">>, + <<"EmailConfiguration">> => + #{<<"EmailSendingAccount">> => <<"COGNITO_DEFAULT">>}, + <<"EmailVerificationMessage">> => + <<"Your verification code is {####}. ">>, + <<"EmailVerificationSubject">> => + <<"Your verification code">>, + <<"EstimatedNumberOfUsers">> => 3, + <<"Id">> => <<"eu-west-1_XEBAHPImu">>, + <<"LambdaConfig">> => #{}, + <<"LastModifiedDate">> => 1636620065.355, + <<"MfaConfiguration">> => <<"OFF">>, + <<"Name">> => <<"test-pool">>, + <<"Policies">> => + #{<<"PasswordPolicy">> => + #{<<"MinimumLength">> => 8,<<"RequireLowercase">> => true, + <<"RequireNumbers">> => true,<<"RequireSymbols">> => false, + <<"RequireUppercase">> => false, + <<"TemporaryPasswordValidityDays">> => 7}}, + <<"SchemaAttributes">> => [ + #{<<"AttributeDataType">> => <<"String">>, + <<"DeveloperOnlyAttribute">> => false,<<"Mutable">> => true, + <<"Name">> => <<"custom:partner">>,<<"Required">> => false, + <<"StringAttributeConstraints">> => + #{<<"MaxLength">> => <<"256">>,<<"MinLength">> => <<"1">>}}, + #{<<"AttributeDataType">> => <<"String">>, + <<"DeveloperOnlyAttribute">> => false,<<"Mutable">> => true, + <<"Name">> => <<"custom:organisation">>, + <<"Required">> => false, + <<"StringAttributeConstraints">> => + #{<<"MaxLength">> => <<"256">>,<<"MinLength">> => <<"1">>}}, + #{<<"AttributeDataType">> => <<"String">>, + <<"DeveloperOnlyAttribute">> => false,<<"Mutable">> => true, + <<"Name">> => <<"custom:role">>,<<"Required">> => false, + <<"StringAttributeConstraints">> => + #{<<"MaxLength">> => <<"256">>,<<"MinLength">> => <<"1">>}}, + #{<<"AttributeDataType">> => <<"String">>, + <<"DeveloperOnlyAttribute">> => false,<<"Mutable">> => true, + <<"Name">> => <<"custom:viewMode">>,<<"Required">> => false, + <<"StringAttributeConstraints">> => + #{<<"MaxLength">> => <<"256">>,<<"MinLength">> => <<"1">>}} + ], + <<"SmsAuthenticationMessage">> => <<"Your authentication code is {####}. ">>, + <<"SmsVerificationMessage">> => <<"Your verification code is {####}. ">>, + <<"UserPoolTags">> => #{}, + <<"UsernameAttributes">> => [<<"email">>], + <<"UsernameConfiguration">> => #{<<"CaseSensitive">> => false}, + <<"VerificationMessageTemplate">> => #{ + <<"DefaultEmailOption">> => <<"CONFIRM_WITH_CODE">>, + <<"EmailMessage">> => + <<"Your verification code is {####}. ">>, + <<"EmailSubject">> => <<"Your verification code">>, + <<"SmsMessage">> => + <<"Your verification code is {####}. ">> + }} +}). + +-define(MFA_CONFIG, + #{ + <<"MfaConfiguration">> => <<"ON">>, + <<"SoftwareTokenMfaConfiguration">> => #{<<"Enabled">> => true} + }). + +-define(LIST_IDENTITY_PROVIDERS, + #{<<"Providers">> => [#{<<"CreationDate">> => 1636620575.033, + <<"LastModifiedDate">> => 1636620730.0, + <<"ProviderName">> => <<"test">>, + <<"ProviderType">> => <<"SAML">>}], + <<"NextToken">> => <<"nextpage">>}). + +-define(LIST_ALL_IDENTITY_PROVIDERS, + #{<<"Providers">> => + [ + #{ + <<"CreationDate">> => 1636620575.033, + <<"LastModifiedDate">> => 1636620730.0, + <<"ProviderName">> => <<"test">>, + <<"ProviderType">> => <<"SAML">> + }, + #{ + <<"CreationDate">> => 1636620577.033, + <<"LastModifiedDate">> => 1636620730.0, + <<"ProviderName">> => <<"NewOauth">>, + <<"ProviderType">> => <<"SAML">> + } + ]} + ). + +-define(DESCRIBE_PROVIDER, + #{ + <<"IdentityProvider">> => + #{ + <<"AttributeMapping">> => + #{ + <<"email">> => + <<"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress">>}, + <<"CreationDate">> => 1636620575.033, + <<"IdpIdentifiers">> => [], + <<"LastModifiedDate">> => 1636620730.0, + <<"ProviderDetails">> => + #{<<"IDPSignout">> => <<"false">>, + <<"MetadataFile">> => <<"meta xml">>, + <<"SLORedirectBindingURI">> => + <<"https://login.microsoftonline.com/test/saml2">>, + <<"SSORedirectBindingURI">> => + <<"https://login.microsoftonline.com/test/saml2">>}, + <<"ProviderName">> => <<"test">>, + <<"ProviderType">> => <<"SAML">>, + <<"UserPoolId">> => <<"us-east-2_test">>}}). + +-define(DESCRIBE_USER_POOL_CLIENT, + #{<<"UserPoolClient">> => + #{<<"AccessTokenValidity">> => 60, + <<"AllowedOAuthFlows">> => [<<"code">>,<<"implicit">>], + <<"AllowedOAuthFlowsUserPoolClient">> => true, + <<"AllowedOAuthScopes">> => + [<<"aws.cognito.signin.user.admin">>,<<"email">>, + <<"openid">>,<<"phone">>], + <<"CallbackURLs">> => [<<"https://test.dev:30102/login">>], + <<"ClientId">> => ?CLIENT_ID, + <<"ClientName">> => <<"test">>, + <<"CreationDate">> => 1635782978.411, + <<"EnableTokenRevocation">> => true, + <<"ExplicitAuthFlows">> => + [<<"ALLOW_ADMIN_USER_PASSWORD_AUTH">>, + <<"ALLOW_CUSTOM_AUTH">>,<<"ALLOW_REFRESH_TOKEN_AUTH">>, + <<"ALLOW_USER_SRP_AUTH">>], + <<"IdTokenValidity">> => 60, + <<"LastModifiedDate">> => 1636620585.842, + <<"LogoutURLs">> => [<<"https://test.dev:30102/logout">>], + <<"PreventUserExistenceErrors">> => <<"ENABLED">>, + <<"ReadAttributes">> => + [<<"address">>,<<"birthdate">>,<<"email">>, + <<"email_verified">>,<<"family_name">>,<<"gender">>, + <<"given_name">>,<<"locale">>,<<"middle_name">>,<<"name">>, + <<"nickname">>,<<"phone_number">>, + <<"phone_number_verified">>,<<"picture">>, + <<"preferred_username">>,<<"profile">>,<<"updated_at">>, + <<"website">>,<<"zoneinfo">>], + <<"RefreshTokenValidity">> => 30, + <<"SupportedIdentityProviders">> => + [<<"ActualExperience">>,<<"COGNITO">>], + <<"TokenValidityUnits">> => + #{<<"AccessToken">> => <<"minutes">>, + <<"IdToken">> => <<"minutes">>, + <<"RefreshToken">> => <<"days">>}, + <<"UserPoolId">> => <<"us-east-2_test">>, + <<"WriteAttributes">> => + [<<"address">>,<<"birthdate">>,<<"email">>, + <<"family_name">>,<<"gender">>,<<"given_name">>, + <<"locale">>,<<"middle_name">>,<<"name">>,<<"nickname">>, + <<"phone_number">>,<<"picture">>,<<"preferred_username">>, + <<"profile">>,<<"updated_at">>,<<"website">>, + <<"zoneinfo">>]}} +). + +-define(LIST_USER_POOL_CLIENTS, #{ + <<"UserPoolClients">> => [ + #{<<"ClientId">> => ?CLIENT_ID, + <<"ClientName">> => <<"name-1">>, + <<"UserPoolId">> => ?USER_POOL_ID} + ] +}). + +-define(LIST_ALL_USER_POOL_CLIENTS, #{ + <<"UserPoolClients">> => [ + #{<<"ClientId">> => ?CLIENT_ID, + <<"ClientName">> => <<"name-1">>, + <<"UserPoolId">> => ?USER_POOL_ID}, + #{<<"ClientId">> => ?CLIENT_ID, + <<"ClientName">> => <<"name-1">>, + <<"UserPoolId">> => ?USER_POOL_ID} + ] +}). + +-define(ADMIN_LIST_DEVICE, + #{ + <<"Devices">> => [ + #{ + <<"DeviceAttributes">> => [ + #{ + <<"Name">> => <<"test">>, + <<"Value">> => <<"testvalue">> + } + ], + <<"DeviceCreateDate">> => 1635782978.411, + <<"DeviceKey">> => <<"testKey">>, + <<"DeviceLastAuthenticatedDate">> => 1635782978.411, + <<"DeviceLastModifiedDate">> => 1635782978.411 + } + ] + } + ). + +-define(LIST_ALL_DEVICES, + #{ + <<"Devices">> => [ + #{ + <<"DeviceAttributes">> => [ + #{ + <<"Name">> => <<"test">>, + <<"Value">> => <<"testvalue">> + } + ], + <<"DeviceCreateDate">> => 1635782978.411, + <<"DeviceKey">> => <<"testKey">>, + <<"DeviceLastAuthenticatedDate">> => 1635782978.411, + <<"DeviceLastModifiedDate">> => 1635782978.411 + }, + #{ + <<"DeviceAttributes">> => [ + #{ + <<"Name">> => <<"test">>, + <<"Value">> => <<"testvalue">> + } + ], + <<"DeviceCreateDate">> => 1635782978.411, + <<"DeviceKey">> => <<"testKey2">>, + <<"DeviceLastAuthenticatedDate">> => 1635782978.411, + <<"DeviceLastModifiedDate">> => 1635782978.411 + } + ] + } +). + +-define(ADMIN_INITIATE_AUTH, #{ + <<"ChallengeName">> => <<"SOFTWARE_TOKEN_MFA">>, + <<"ChallengeParameters">> => #{<<"USER_ID_FOR_SRP">> => <<"id">>}, + <<"Session">> => <<"session-token">> +}). + +-define(RESPOND_TO_AUTH_CHALLENGE, #{ + <<"AuthenticationResult">> => #{ + <<"AccessToken">> => <<"access-token">>, + <<"ExpiresIn">> => 1800, + <<"IdToken">> => <<"id-token">>, + <<"RefreshToken">> => <<"refresh-token">>, + <<"TokenType">> => <<"Bearer">>, + <<"NewDeviceMetadata">> => #{ + <<"DeviceGroupKey">> => <<"-rowRNkJw">>, + <<"DeviceKey">> => <<"eu-west-1_b6657eba-2850-4a47-9357-fae69fd86b94">> + } + }, + <<"ChallengeParameters">> => #{} +}). + +-define(CREATE_UPDATE_IDP, #{ + <<"IdentityProvider">> => #{ + <<"AttributeMapping">> => #{ + <<"testAttr">> => <<"SAMLAttr">> + }, + <<"CreationDate">> => 1635782978.411, + <<"IdpIdentifiers">> => [ "Test IDP" ], + <<"LastModifiedDate">> => 1635782978.411, + <<"ProviderDetails">> => #{ + <<"MetadataURL">> => <<"http://test-idp.com/1234">> + }, + <<"ProviderName">> => <<"TestIdp">>, + <<"ProviderType">> => <<"SAML">>, + <<"UserPoolId">> => <<"test">> + } + }). + +config() -> + #aws_config{access_key_id = "id", + secret_access_key = "key", + retry = fun erlcloud_retry:default_retry/1, + retry_num = 3}. + +setup() -> + erlcloud_cognito_user_pools:configure("id", "key"), + MockedModules = [?EHTTPC, erlcloud_aws], + meck:new(MockedModules, [passthrough]), + meck:expect(erlcloud_aws, update_config, 1, {ok, config()}), + meck:expect(?EHTTPC, request, 6, fun do_erlcloud_httpc_request/6), + MockedModules. + +erlcloud_cognito_user_pools_test_() -> + { + foreach, + fun setup/0, + fun meck:unload/1, + [ + fun test_list_users/0, + fun test_list_all_users/0, + fun test_admin_list_groups_for_user/0, + fun test_admin_get_user/0, + fun test_admin_create_user/0, + fun test_admin_delete_user/0, + fun test_admin_add_user_to_group/0, + fun test_admin_remove_user_from_group/0, + fun test_create_group/0, + fun test_delete_group/0, + fun test_admin_reset_user_password/0, + fun test_admin_update_user_attributes/0, + fun test_change_password/0, + fun test_list_user_pools/0, + fun test_list_all_user_pools/0, + fun test_admin_set_user_password/0, + fun test_describe_user_pool/0, + fun test_get_user_pool_mfa_config/0, + fun test_list_identity_providers/0, + fun test_list_all_identity_provider/0, + fun test_describe_identity_provider/0, + fun test_describe_user_pool_client/0, + fun test_list_user_pool_clients/0, + fun test_list_all_user_pool_clients/0, + fun test_admin_list_devices/0, + fun test_list_all_devices/0, + fun test_admin_forget_device/0, + fun test_admin_confirm_signup/0, + fun test_admin_initiate_auth/0, + fun test_respond_to_auth_challenge/0, + fun test_create_identity_provider/0, + fun test_delete_identity_provider/0, + fun test_update_identity_provider/0, + fun test_error_no_retry/0, + fun test_error_retry/0 + ] + }. + +test_list_users() -> + Request = #{<<"UserPoolId">> => ?USER_POOL_ID}, + Expected = {ok, ?LIST_USERS_RESP}, + TestFun = fun() -> erlcloud_cognito_user_pools:list_users(?USER_POOL_ID) end, + do_test(Request, Expected, TestFun). + +test_list_all_users() -> + Mocked1 = maps:put(<<"PaginationToken">>, "1", ?LIST_USERS_RESP), + Mocked2 = #{<<"Users">> => [ + #{ + <<"Attributes">> => + [ + #{<<"Name">> => <<"sub">>, + <<"Value">> => <<"b23652f5-2340-4137-9a5e-4b40641420eb">>}, + #{<<"Name">> => <<"email_verified">>, + <<"Value">> => <<"true">>}, + #{<<"Name">> => <<"email">>, + <<"Value">> => <<"test3@fake.email">>} + ], + <<"Enabled">> => true, + <<"UserCreateDate">> => 1631791632.572, + <<"UserLastModifiedDate">> => 1631791778.698, + <<"UserStatus">> => <<"CONFIRMED">>, + <<"Username">> => <<"test.user3">>} + ]}, + meck:sequence(?EHTTPC, request, 6, [{ok, {{200, "OK"}, [], jsx:encode(Mocked1)}}, + {ok, {{200, "OK"}, [], jsx:encode(Mocked2)}}]), + Expected = {ok, ?LIST_ALL_USERS}, + Response = erlcloud_cognito_user_pools:list_all_users(?USER_POOL_ID), + ?assertEqual(Expected, Response). + +test_admin_list_groups_for_user() -> + Request = #{ + <<"Username">> => ?USERNAME, + <<"UserPoolId">> => ?USER_POOL_ID + }, + Expected = {ok, ?ADMIN_LIST_GROUPS_FOR_USERS}, + TestFun = fun() -> + erlcloud_cognito_user_pools:admin_list_groups_for_user(?USERNAME, ?USER_POOL_ID, config()) + end, + do_test(Request, Expected, TestFun). + +test_admin_get_user() -> + Request = #{ + <<"Username">> => ?USERNAME, + <<"UserPoolId">> => ?USER_POOL_ID + }, + Expected = {ok, ?ADMIN_GET_USER}, + TestFun = fun() -> + erlcloud_cognito_user_pools:admin_get_user(?USERNAME, ?USER_POOL_ID, config()) + end, + do_test(Request, Expected, TestFun). + +test_admin_create_user() -> + UserAttributes = #{ + <<"UserAttributes">> => [ + #{<<"Name">> => <<"custom:test">>, + <<"Value">> => <<"test">>}, + #{<<"Name">> => <<"custom:test2">>, + <<"Value">> => <<"test2">>} + ] + }, + Request = UserAttributes#{<<"Username">> => ?USERNAME, + <<"UserPoolId">> => ?USER_POOL_ID}, + Expected = {ok, ?ADMIN_CREATE_USER}, + TestFun = fun() -> + erlcloud_cognito_user_pools:admin_create_user(?USERNAME, ?USER_POOL_ID, UserAttributes) + end, + do_test(Request, Expected, TestFun). + +test_admin_delete_user() -> + Request = #{ + <<"Username">> => ?USERNAME, + <<"UserPoolId">> => ?USER_POOL_ID + }, + TestFun = fun() -> + erlcloud_cognito_user_pools:admin_delete_user(?USERNAME, ?USER_POOL_ID, config()) + end, + do_test(Request, ok, TestFun). + +test_admin_add_user_to_group() -> + GroupName = <<"test">>, + Request = #{ + <<"Username">> => ?USERNAME, + <<"UserPoolId">> => ?USER_POOL_ID, + <<"GroupName">> => GroupName}, + TestFun = fun() -> + erlcloud_cognito_user_pools:admin_add_user_to_group(GroupName, ?USERNAME, ?USER_POOL_ID, config()) + end, + do_test(Request, ok, TestFun). + +test_admin_remove_user_from_group() -> + GroupName = <<"test">>, + Request = #{ + <<"Username">> => ?USERNAME, + <<"UserPoolId">> => ?USER_POOL_ID, + <<"GroupName">> => GroupName}, + TestFun = fun() -> + erlcloud_cognito_user_pools:admin_remove_user_from_group(GroupName, ?USERNAME, ?USER_POOL_ID, config()) + end, + do_test(Request, ok, TestFun). + +test_create_group() -> + GroupName = <<"test">>, + Request = #{ + <<"UserPoolId">> => ?USER_POOL_ID, + <<"GroupName">> => GroupName}, + Expected = {ok, ?CREATE_GROUP}, + TestFun = fun() -> + erlcloud_cognito_user_pools:create_group(GroupName, ?USER_POOL_ID, config()) + end, + do_test(Request, Expected, TestFun). + +test_delete_group() -> + GroupName = <<"test">>, + Request = #{ + <<"UserPoolId">> => ?USER_POOL_ID, + <<"GroupName">> => GroupName}, + TestFun = fun() -> + erlcloud_cognito_user_pools:delete_group(GroupName, ?USER_POOL_ID, config()) + end, + do_test(Request, ok, TestFun). + +test_admin_reset_user_password() -> + Request = #{ + <<"Username">> => ?USERNAME, + <<"UserPoolId">> => ?USER_POOL_ID}, + TestFun = fun() -> + erlcloud_cognito_user_pools:admin_reset_user_password(?USERNAME, ?USER_POOL_ID) + end, + do_test(Request, ok, TestFun). + +test_admin_update_user_attributes() -> + Attributes = [ + #{ + <<"Name">> => <<"custom:partner">>, + <<"Value">> => <<"new value">> + } + ], + Request = #{ + <<"UserAttributes">> => Attributes, + <<"UserPoolId">> => ?USER_POOL_ID, + <<"Username">> => ?USERNAME}, + TestFun = fun() -> + erlcloud_cognito_user_pools:admin_update_user_attributes(?USERNAME, ?USER_POOL_ID, Attributes) + end, + do_test(Request, ok, TestFun). + +test_change_password() -> + AccessToken = <<"test token">>, + OldPass = <<"old p4ss!">>, + NewPass = <<"new p4ss!">>, + Request = #{ + <<"AccessToken">> => AccessToken, + <<"PreviousPassword">> => OldPass, + <<"ProposedPassword">> => NewPass + }, + TestFun = fun() -> + erlcloud_cognito_user_pools:change_password(OldPass, NewPass, AccessToken) + end, + do_test(Request, ok, TestFun). + +test_list_user_pools() -> + Expected = {ok, ?LIST_USER_POOLS}, + MaxResults = 60, + Request = #{<<"MaxResults">> => MaxResults}, + TestFun = fun() -> erlcloud_cognito_user_pools:list_user_pools(MaxResults) end, + do_test(Request, Expected, TestFun). + +test_error_no_retry() -> + erlcloud_cognito_user_pools:configure("test-access-key", "test-secret-key"), + ErrCode = 400, + Status = "Bad Request", + ErrMsg = <<"Message">>, + Operation = "ListUsers", + Config = config(), + Request = #{<<"UserPoolId">> => ?USER_POOL_ID}, + meck:expect(?EHTTPC, request, 6, {ok, {{ErrCode, Status}, [], ErrMsg}}), + ?assertEqual( + {error, {http_error, ErrCode, Status, ErrMsg, []}}, + erlcloud_cognito_user_pools:request(Config, Operation, Request) + ). + +test_list_all_user_pools() -> + Mocked1 = ?LIST_USER_POOLS, + Mocked2 = #{ + <<"UserPools">> => + [ + #{ + <<"CreationDate">> => 1634210679.535, + <<"Id">> => <<"eu-west-testpool3">>, + <<"LambdaConfig">> => #{}, + <<"LastModifiedDate">> => 1634210679.535, + <<"Name">> => <<"TestPool3">> + } + ]}, + meck:sequence(?EHTTPC, request, 6, [{ok, {{200, "OK"}, [], jsx:encode(Mocked1)}}, + {ok, {{200, "OK"}, [], jsx:encode(Mocked2)}}]), + Expected = {ok, ?LIST_ALL_USER_POOLS}, + Response = erlcloud_cognito_user_pools:list_all_user_pools(), + ?assertEqual(Expected, Response). + +test_admin_set_user_password() -> + Pass = <<"test pass">>, + Request = #{ + <<"Username">> => ?USERNAME, + <<"UserPoolId">> => ?USER_POOL_ID, + <<"Password">> => Pass, + <<"Permanent">> => false}, + TestFun = fun() -> + erlcloud_cognito_user_pools:admin_set_user_password(?USERNAME, ?USER_POOL_ID, Pass) + end, + do_test(Request, ok, TestFun). + +test_describe_user_pool() -> + Request = #{<<"UserPoolId">> => ?USER_POOL_ID}, + Expected = {ok, ?DESCRIBE_POOL}, + TestFun = fun() -> erlcloud_cognito_user_pools:describe_user_pool(?USER_POOL_ID) end, + do_test(Request, Expected, TestFun). + +test_get_user_pool_mfa_config() -> + Request = #{<<"UserPoolId">> => ?USER_POOL_ID}, + Expected = {ok, ?MFA_CONFIG}, + TestFun = fun() -> erlcloud_cognito_user_pools:get_user_pool_mfa_config(?USER_POOL_ID) end, + do_test(Request, Expected, TestFun). + +test_list_identity_providers() -> + Request = #{ + <<"UserPoolId">> => ?USER_POOL_ID, + <<"MaxResults">> => 60 + }, + Expected = {ok, ?LIST_IDENTITY_PROVIDERS}, + TestFun = fun() -> erlcloud_cognito_user_pools:list_identity_providers(?USER_POOL_ID) end, + do_test(Request, Expected, TestFun). + +test_list_all_identity_provider() -> + Mocked1 = ?LIST_IDENTITY_PROVIDERS, + Mocked2 = #{ + <<"Providers">> => [ + #{<<"CreationDate">> => 1636620577.033, + <<"LastModifiedDate">> => 1636620730.0, + <<"ProviderName">> => <<"NewOauth">>, + <<"ProviderType">> => <<"SAML">>} + ] + }, + meck:sequence(?EHTTPC, request, 6, [{ok, {{200, "OK"}, [], jsx:encode(Mocked1)}}, + {ok, {{200, "OK"}, [], jsx:encode(Mocked2)}}]), + Expected = {ok, ?LIST_ALL_IDENTITY_PROVIDERS}, + Response = erlcloud_cognito_user_pools:list_all_identity_providers(?USER_POOL_ID), + ?assertEqual(Expected, Response). + +test_describe_identity_provider() -> + ProviderName = <<"test">>, + Request = #{ + <<"UserPoolId">> => ?USER_POOL_ID, + <<"ProviderName">> => ProviderName + }, + Expected = {ok, ?DESCRIBE_PROVIDER}, + TestFun = fun() -> + erlcloud_cognito_user_pools:describe_identity_provider(?USER_POOL_ID, ProviderName) + end, + do_test(Request, Expected, TestFun). + +test_describe_user_pool_client() -> + Request = #{ + <<"UserPoolId">> => ?USER_POOL_ID, + <<"ClientId">> => ?CLIENT_ID + }, + Expected = {ok, ?DESCRIBE_USER_POOL_CLIENT}, + TestFun = fun() -> erlcloud_cognito_user_pools:describe_user_pool_client(?USER_POOL_ID, ?CLIENT_ID) end, + do_test(Request, Expected, TestFun). + +test_list_user_pool_clients() -> + Request = #{<<"UserPoolId">> => ?USER_POOL_ID, + <<"MaxResults">> => 60}, + TestFun = fun() -> erlcloud_cognito_user_pools:list_user_pool_clients(?USER_POOL_ID) end, + Expected = {ok, ?LIST_USER_POOL_CLIENTS}, + do_test(Request, Expected, TestFun). + +test_list_all_user_pool_clients() -> + Mocked1 = maps:put(<<"NextToken">>, <<"next">>, ?LIST_USER_POOL_CLIENTS), + Mocked2 = ?LIST_USER_POOL_CLIENTS, + meck:sequence(?EHTTPC, request, 6, [{ok, {{200, "OK"}, [], jsx:encode(Mocked1)}}, + {ok, {{200, "OK"}, [], jsx:encode(Mocked2)}}]), + Expected = {ok, ?LIST_ALL_USER_POOL_CLIENTS}, + Response = erlcloud_cognito_user_pools:list_all_user_pool_clients(?USER_POOL_ID), + ?assertEqual(Expected, Response). + +test_admin_list_devices() -> + Request = #{ + <<"UserPoolId">> => ?USER_POOL_ID, + <<"Username">> => ?USERNAME, + <<"Limit">> => 60 + }, + Expected = {ok, ?ADMIN_LIST_DEVICE}, + TestFun = fun() -> erlcloud_cognito_user_pools:admin_list_devices(?USER_POOL_ID, ?USERNAME) end, + do_test(Request, Expected, TestFun). + +test_list_all_devices() -> + Mocked1 = maps:put(<<"PaginationToken">>, <<"next page">>, ?ADMIN_LIST_DEVICE), + Devices = hd(maps:get(<<"Devices">>, ?ADMIN_LIST_DEVICE)), + Mocked2 = maps:put(<<"Devices">>, [Devices#{<<"DeviceKey">> => <<"testKey2">>}], ?ADMIN_LIST_DEVICE), + meck:sequence(?EHTTPC, request, 6, [{ok, {{200, "OK"}, [], jsx:encode(Mocked1)}}, + {ok, {{200, "OK"}, [], jsx:encode(Mocked2)}}]), + Expected = {ok, ?LIST_ALL_DEVICES}, + Response = erlcloud_cognito_user_pools:admin_list_all_devices(?USER_POOL_ID, ?USERNAME), + ?assertEqual(Expected, Response). + +test_admin_forget_device() -> + DeviceKey = <<"test key">>, + Request = #{ + <<"UserPoolId">> => ?USER_POOL_ID, + <<"Username">> => ?USERNAME, + <<"DeviceKey">> => DeviceKey + }, + TestFun = fun() -> erlcloud_cognito_user_pools:admin_forget_device(?USER_POOL_ID, ?USERNAME, DeviceKey) end, + do_test(Request, ok, TestFun). + +test_admin_confirm_signup() -> + Request = #{ + <<"UserPoolId">> => ?USER_POOL_ID, + <<"Username">> => ?USERNAME, + <<"ClientMetadata">> => #{} + }, + TestFun = fun() -> erlcloud_cognito_user_pools:admin_confirm_signup(?USER_POOL_ID, ?USERNAME) end, + do_test(Request, {ok, #{}}, TestFun). + +test_admin_initiate_auth() -> + AuthFlow = <<"ADMIN_USER_PASSWORD_AUTH">>, + AuthParams = #{<<"PASSWORD">> => <<"pass">>, + <<"USERNAME">> => ?USERNAME}, + Request = #{ + <<"AuthFlow">> => AuthFlow, + <<"AuthParameters">> => AuthParams, + <<"ClientId">> => ?CLIENT_ID, + <<"UserPoolId">> => ?USER_POOL_ID + }, + TestFun = fun() -> erlcloud_cognito_user_pools:admin_initiate_auth(?USER_POOL_ID, ?CLIENT_ID, + AuthFlow, AuthParams) end, + do_test(Request, {ok, ?ADMIN_INITIATE_AUTH}, TestFun). + +test_respond_to_auth_challenge() -> + ChallengeName = <<"SOFTWARE_TOKEN_MFA">>, + ChallengeResp = #{<<"USERNAME">> => ?USERNAME, + <<"SOFTWARE_TOKEN_MFA_CODE">> => <<"123456">>}, + Session = <<"session-token">>, + Request = #{ + <<"ChallengeName">> => ChallengeName, + <<"ChallengeResponses">> => ChallengeResp, + <<"ClientId">> => ?CLIENT_ID, + <<"Session">> => Session + }, + TestFun = fun() -> erlcloud_cognito_user_pools:respond_to_auth_challenge(?CLIENT_ID, ChallengeName, + ChallengeResp, Session) end, + do_test(Request, {ok, ?RESPOND_TO_AUTH_CHALLENGE}, TestFun). + +test_create_identity_provider() -> + UserPoolId = <<"test">>, + ProviderName = <<"testIdp">>, + ProviderType = <<"SAML">>, + ProviderDetails = #{ + <<"MetadataURL">> => <<"http://test-idp.com/1234">> + }, + AttributeMapping = #{ + <<"testAttr">> => <<"SAMLAttr">> + }, + IdpIdentifiers = [ "Test IDP" ], + + Request = #{ + <<"UserPoolId">> => UserPoolId, + <<"ProviderName">> => ProviderName, + <<"ProviderType">> => ProviderType, + <<"ProviderDetails">> => ProviderDetails, + <<"AttributeMapping">> => AttributeMapping, + <<"IdpIdentifiers">> => IdpIdentifiers + }, + + TestFun = fun() -> erlcloud_cognito_user_pools:create_identity_provider(UserPoolId, ProviderName, + ProviderType, ProviderDetails, + AttributeMapping, IdpIdentifiers) end, + do_test(Request, {ok, ?CREATE_UPDATE_IDP}, TestFun). + +test_delete_identity_provider() -> + UserPoolId = <<"test">>, + ProviderName = <<"testIdp">>, + + Request = #{ + <<"UserPoolId">> => UserPoolId, + <<"ProviderName">> => ProviderName + }, + + TestFun = fun() -> erlcloud_cognito_user_pools:delete_identity_provider(UserPoolId, ProviderName) end, + do_test(Request, ok, TestFun). + +test_update_identity_provider() -> + UserPoolId = <<"test">>, + ProviderName = <<"testIdp">>, + ProviderDetails = #{ + <<"MetadataURL">> => <<"http://test-idp.com/1234">> + }, + AttributeMapping = #{ + <<"testAttr">> => <<"SAMLAttr">> + }, + IdpIdentifiers = [ "Test IDP" ], + + Request = #{ + <<"UserPoolId">> => UserPoolId, + <<"ProviderName">> => ProviderName, + <<"ProviderDetails">> => ProviderDetails, + <<"AttributeMapping">> => AttributeMapping, + <<"IdpIdentifiers">> => IdpIdentifiers + }, + + TestFun = fun() -> erlcloud_cognito_user_pools:update_identity_provider(UserPoolId, ProviderName, + ProviderDetails, AttributeMapping, + IdpIdentifiers) end, + do_test(Request, {ok, ?CREATE_UPDATE_IDP}, TestFun). + +test_error_retry() -> + erlcloud_cognito_user_pools:configure("test-access-key", "test-secret-key"), + ErrCode1 = 500, + ErrCode2 = 400, + Status1 = "Internal Server Error", + Status2 = "Bad Request", + ErrMsg1 = <<"Message-1">>, + ErrMsg2 = <<"Message-2">>, + Operation = "ListUsers", + Config = config(), + Request = #{<<"UserPoolId">> => ?USER_POOL_ID}, + meck:sequence(?EHTTPC, request, 6, + [{ok, {{ErrCode1, Status1}, [], ErrMsg1}}, + {ok, {{ErrCode2, Status2}, [], ErrMsg2}}]), + ?assertEqual( + {error, {http_error, ErrCode2, Status2, ErrMsg2, []}}, + erlcloud_cognito_user_pools:request(Config, Operation, Request) + ). + +do_test(Request, ExpectedResult, TestedFun) -> + erlcloud_cognito_user_pools:configure("test-access-key", "test-secret-key"), + ?assertEqual(ExpectedResult, TestedFun()), + Encoded = jsx:encode(Request), + ?assertMatch([{_, {?EHTTPC, request, [_, post, _, Encoded, _, _]}, _}], + meck:history(?EHTTPC)). + +do_erlcloud_httpc_request(_, post, Headers, _, _, _) -> + Target = proplists:get_value("x-amz-target", Headers), + ["AWSCognitoIdentityProviderService", Operation] = string:tokens(Target, "."), + RespBody = + case Operation of + "ListUsers" -> ?LIST_USERS_RESP; + "AdminListGroupsForUser" -> ?ADMIN_LIST_GROUPS_FOR_USERS; + "AdminGetUser" -> ?ADMIN_GET_USER; + "AdminCreateUser" -> ?ADMIN_CREATE_USER; + "AdminDeleteUser" -> #{}; + "AdminAddUserToGroup" -> #{}; + "AdminRemoveUserFromGroup" -> #{}; + "CreateGroup" -> ?CREATE_GROUP; + "DeleteGroup" -> #{}; + "AdminResetUserPassword" -> #{}; + "AdminUpdateUserAttributes" -> #{}; + "ChangePassword" -> #{}; + "ListUserPools" -> ?LIST_USER_POOLS; + "AdminSetUserPassword" -> #{}; + "DescribeUserPool" -> ?DESCRIBE_POOL; + "ListUserPoolClients" -> ?LIST_USER_POOL_CLIENTS; + "GetUserPoolMfaConfig" -> ?MFA_CONFIG; + "ListIdentityProviders" -> ?LIST_IDENTITY_PROVIDERS; + "DescribeIdentityProvider" -> ?DESCRIBE_PROVIDER; + "DescribeUserPoolClient" -> ?DESCRIBE_USER_POOL_CLIENT; + "AdminListDevices" -> ?ADMIN_LIST_DEVICE; + "AdminForgetDevice" -> #{}; + "AdminConfirmSignUp" -> #{}; + "AdminInitiateAuth" -> ?ADMIN_INITIATE_AUTH; + "RespondToAuthChallenge" -> ?RESPOND_TO_AUTH_CHALLENGE; + "CreateIdentityProvider" -> ?CREATE_UPDATE_IDP; + "DeleteIdentityProvider" -> #{}; + "UpdateIdentityProvider" -> ?CREATE_UPDATE_IDP + end, + {ok, {{200, "OK"}, [], jsx:encode(RespBody)}}. diff --git a/test/erlcloud_ddb2_tests.erl b/test/erlcloud_ddb2_tests.erl index 9195d3e3c..cb72fdc3b 100644 --- a/test/erlcloud_ddb2_tests.erl +++ b/test/erlcloud_ddb2_tests.erl @@ -19,7 +19,7 @@ -define(_f(F), fun() -> F end). -export([validate_body/2]). - + %%%=================================================================== %%% Test entry points %%%=================================================================== @@ -53,8 +53,12 @@ operation_test_() -> fun describe_limits_output_tests/1, fun describe_global_table_input_tests/1, fun describe_global_table_output_tests/1, + fun describe_global_table_settings_input_tests/1, + fun describe_global_table_settings_output_tests/1, fun describe_table_input_tests/1, fun describe_table_output_tests/1, + fun describe_table_replica_auto_scaling_input_tests/1, + fun describe_table_replica_auto_scaling_output_tests/1, fun describe_time_to_live_input_tests/1, fun describe_time_to_live_output_tests/1, fun get_item_input_tests/1, @@ -67,7 +71,7 @@ operation_test_() -> fun list_tables_input_tests/1, fun list_tables_output_tests/1, fun list_tags_of_resource_input_tests/1, - fun list_tags_of_resource_output_tests/1, + fun list_tags_of_resource_output_tests/1, fun put_item_input_tests/1, fun put_item_output_tests/1, fun q_input_tests/1, @@ -80,6 +84,10 @@ operation_test_() -> fun scan_output_tests/1, fun tag_resource_input_tests/1, fun tag_resource_output_tests/1, + fun transact_get_items_input_tests/1, + fun transact_get_items_output_tests/1, + fun transact_write_items_input_tests/1, + fun transact_write_items_output_tests/1, fun untag_resource_input_tests/1, fun untag_resource_output_tests/1, fun update_continuous_backups_input_tests/1, @@ -88,8 +96,12 @@ operation_test_() -> fun update_item_output_tests/1, fun update_global_table_input_tests/1, fun update_global_table_output_tests/1, + fun update_global_table_settings_input_tests/1, + fun update_global_table_settings_output_tests/1, fun update_table_input_tests/1, fun update_table_output_tests/1, + fun update_table_replica_auto_scaling_input_tests/1, + fun update_table_replica_auto_scaling_output_tests/1, fun update_time_to_live_input_tests/1, fun update_time_to_live_output_tests/1 ]}. @@ -124,8 +136,8 @@ validate_body(<<>> = Actual, Want) -> ?debugFmt("~nEXPECTED~n~p~nACTUAL~n~p~n", [Want, Actual]), ?assertEqual(Want, Actual); validate_body(Body, Expected) -> - Want = sort_json(jsx:decode(list_to_binary(Expected))), - Actual = sort_json(jsx:decode(Body)), + Want = sort_json(jsx:decode(list_to_binary(Expected), [{return_maps, false}])), + Actual = sort_json(jsx:decode(Body, [{return_maps, false}])), case Want =:= Actual of true -> ok; false -> @@ -137,7 +149,7 @@ validate_body(Body, Expected) -> %% Validates the request body and responds with the provided response. -spec input_expect(string(), expected_body()) -> fun(). input_expect(Response, Expected) -> - fun(_Url, post, _Headers, Body, _Timeout, _Config) -> + fun(_Url, post, _Headers, Body, _Timeout, _Config) -> validate_body(Body, Expected), {ok, {{200, "OK"}, [], list_to_binary(Response)}} end. @@ -147,7 +159,7 @@ input_expect(Response, Expected) -> -spec input_test(string(), input_test_spec()) -> tuple(). input_test(Response, {Line, {Description, Fun, Expected}}) when is_list(Description) -> - {Description, + {Description, {Line, fun() -> meck:expect(erlcloud_httpc, request, input_expect(Response, Expected)), @@ -169,8 +181,8 @@ input_tests(Response, Tests) -> %% returns the mock of the erlcloud_httpc function output tests expect to be called. -spec output_expect(string()) -> fun(). output_expect(Response) -> - fun(_Url, post, _Headers, _Body, _Timeout, _Config) -> - {ok, {{200, "OK"}, [], list_to_binary(Response)}} + fun(_Url, post, _Headers, _Body, _Timeout, _Config) -> + {ok, {{200, "OK"}, [], list_to_binary(Response)}} end. %% output_test converts an output_test specifier into an eunit test generator @@ -192,9 +204,9 @@ output_test(Fun, {Line, {Description, Response, Result}}) -> end}}. %% output_test(Fun, {Line, {Response, Result}}) -> %% output_test(Fun, {Line, {"", Response, Result}}). - + %% output_tests converts a list of output_test specifiers into an eunit test generator --spec output_tests(fun(), [output_test_spec()]) -> [term()]. +-spec output_tests(fun(), [output_test_spec()]) -> [term()]. output_tests(Fun, Tests) -> [output_test(Fun, Test) || Test <- Tests]. @@ -206,7 +218,7 @@ output_tests(Fun, Tests) -> -spec httpc_response(pos_integer(), string()) -> tuple(). httpc_response(Code, Body) -> {ok, {{Code, ""}, [], list_to_binary(Body)}}. - + -type error_test_spec() :: {pos_integer(), {string(), list(), term()}}. -spec error_test(fun(), error_test_spec()) -> tuple(). error_test(Fun, {Line, {Description, Responses, Result}}) -> @@ -220,7 +232,7 @@ error_test(Fun, {Line, {Description, Responses, Result}}) -> Actual = Fun(), ?assertEqual(Result, Actual) end}}. - + -spec error_tests(fun(), [error_test_spec()]) -> [term()]. error_tests(Fun, Tests) -> [error_test(Fun, Test) || Test <- Tests]. @@ -231,13 +243,16 @@ error_tests(Fun, Tests) -> input_exception_test_() -> [?_assertError({erlcloud_ddb, {invalid_attr_value, {n, "string"}}}, - erlcloud_ddb2:get_item(<<"Table">>, {<<"K">>, {n, "string"}})), + erlcloud_ddb2:get_item(<<"Table">>, {<<"K">>, {n, "string"}}))] + ++ input_exception_failures_test_(). + +-dialyzer({nowarn_function, input_exception_failures_test_/0}). +input_exception_failures_test_() -> %% This test causes an expected dialyzer error - ?_assertError({erlcloud_ddb, {invalid_item, <<"Attr">>}}, + [?_assertError({erlcloud_ddb, {invalid_item, <<"Attr">>}}, erlcloud_ddb2:put_item(<<"Table">>, <<"Attr">>)), ?_assertError({erlcloud_ddb, {invalid_opt, {myopt, myval}}}, - erlcloud_ddb2:list_tables([{myopt, myval}])) - ]. + erlcloud_ddb2:list_tables([{myopt, myval}]))]. %% Error handling tests based on: %% http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ErrorHandling.html @@ -248,12 +263,12 @@ error_handling_tests(_) -> \"status\":{\"S\":\"online\"} }, \"ConsumedCapacityUnits\": 1 -}" +}" ), OkResult = {ok, [{<<"friends">>, [<<"Lynda">>, <<"Aaron">>]}, {<<"status">>, <<"online">>}]}, - Tests = + Tests = [?_ddb_test( {"Test retry after ProvisionedThroughputExceededException", [httpc_response(400, " @@ -275,9 +290,62 @@ error_handling_tests(_) -> OkResponse], OkResult}) ], - - error_tests(?_f(erlcloud_ddb2:get_item(<<"table">>, {<<"k">>, <<"v">>})), Tests). + error_tests(?_f(erlcloud_ddb2:get_item(<<"table">>, {<<"k">>, <<"v">>})), Tests), + + TransactionErrorResult = {error, {<<"TransactionCanceledException">>, + {<<"Transaction cancelled, please refer cancellation reasons for specific reasons [None, ConditionalCheckFailed]">>, + [{<<"None">>, null}, {<<"ConditionalCheckFailed">>, <<"The conditional request failed">>}]}}}, + + TransactOkResponse = httpc_response(200, "{}"), + TransactOkResult = {ok, []}, + + TransactTests = + [?_ddb_test( + {"Test return output for TransactionCanceledException error", + [httpc_response(400, " +{ + \"__type\":\"com.amazonaws.dynamodb.v20120810#TransactionCanceledException\", + \"CancellationReasons\": [ + { + \"Code\":\"None\", + }, + { + \"Code\":\"ConditionalCheckFailed\", + \"Message\":\"The conditional request failed\", + } + ], + \"message\":\"Transaction cancelled, please refer cancellation reasons for specific reasons [None, ConditionalCheckFailed]\" +}" + )], + TransactionErrorResult}), + ?_ddb_test( + {"Test retry after ProvisionedThroughputExceeded cancellation reason", + [httpc_response(400, " +{ + \"__type\":\"com.amazonaws.dynamodb.v20120810#TransactionCanceledException\", + \"CancellationReasons\": [ + { + \"Code\":\"None\", + }, + { + \"Code\":\"ProvisionedThroughputExceeded\", + \"Message\":\"The level of configured provisioned throughput for the table was exceeded. Consider increasing your provisioning level with the UpdateTable API\", + } + ], + \"message\":\"Transaction cancelled, please refer cancellation reasons for specific reasons [None, ProvisionedThroughputExceeded]\" +}" + ), + TransactOkResponse], + TransactOkResult}) + ], + + Transact = [{put, {<<"table">>, [{<<"k">>, {s, <<"v">>}}], []}}, + {condition_check, {<<"table">>, [{<<"k">>, {s, <<"v2">>}}], + [{condition_expression, <<"approved = :a">>}, + {expression_attribute_values, [{<<":a">>, <<"yes">>}]}]}}], + + error_tests(?_f(erlcloud_ddb2:transact_write_items(Transact)), TransactTests). %% BatchGetItem test based on the API examples: %% http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchGetItem.html @@ -286,13 +354,13 @@ batch_get_item_input_tests(_) -> [?_ddb_test( {"BatchGetItem example request", ?_f(erlcloud_ddb2:batch_get_item( - [{<<"Forum">>, + [{<<"Forum">>, [{<<"Name">>, {s, <<"Amazon DynamoDB">>}}, - {<<"Name">>, {s, <<"Amazon RDS">>}}, + {<<"Name">>, {s, <<"Amazon RDS">>}}, {<<"Name">>, {s, <<"Amazon Redshift">>}}], [{attributes_to_get, [<<"Name">>, <<"Threads">>, <<"Messages">>, <<"Views">>]}]}, - {<<"Thread">>, - [[{<<"ForumName">>, {s, <<"Amazon DynamoDB">>}}, + {<<"Thread">>, + [[{<<"ForumName">>, {s, <<"Amazon DynamoDB">>}}, {<<"Subject">>, {s, <<"Concurrent reads">>}}]], [{attributes_to_get, [<<"Tags">>, <<"Message">>]}]}], [{return_consumed_capacity, total}])), " @@ -447,7 +515,7 @@ batch_get_item_input_tests(_) -> input_tests(Response, Tests). batch_get_item_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"BatchGetItem example response", " { @@ -521,10 +589,10 @@ batch_get_item_output_tests(_) -> ] }", {ok, #ddb2_batch_get_item - {consumed_capacity = + {consumed_capacity = [#ddb2_consumed_capacity{table_name = <<"Forum">>, capacity_units = 3}, #ddb2_consumed_capacity{table_name = <<"Thread">>, capacity_units = 1}], - responses = + responses = [#ddb2_batch_get_item_response {table = <<"Forum">>, items = [[{<<"Name">>, <<"Amazon DynamoDB">>}, @@ -579,19 +647,19 @@ batch_get_item_output_tests(_) -> } }", {ok, #ddb2_batch_get_item - {responses = [], - unprocessed_keys = - [{<<"Forum">>, + {responses = [], + unprocessed_keys = + [{<<"Forum">>, [[{<<"Name">>, {s, <<"Amazon DynamoDB">>}}], - [{<<"Name">>, {s, <<"Amazon RDS">>}}], + [{<<"Name">>, {s, <<"Amazon RDS">>}}], [{<<"Name">>, {s, <<"Amazon Redshift">>}}]], [{attributes_to_get, [<<"Name">>, <<"Threads">>, <<"Messages">>, <<"Views">>]}]}, - {<<"Thread">>, - [[{<<"ForumName">>, {s, <<"Amazon DynamoDB">>}}, + {<<"Thread">>, + [[{<<"ForumName">>, {s, <<"Amazon DynamoDB">>}}, {<<"Subject">>, {s, <<"Concurrent reads">>}}]], [{attributes_to_get, [<<"Tags">>, <<"Message">>]}]}]}}}) ], - + output_tests(?_f(erlcloud_ddb2:batch_get_item([{<<"table">>, [{<<"k">>, <<"v">>}]}], [{out, record}])), Tests). %% BatchWriteItem test based on the API examples: @@ -734,7 +802,7 @@ batch_write_item_input_tests(_) -> input_tests(Response, Tests). batch_write_item_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"BatchWriteItem example response", " { @@ -764,7 +832,7 @@ batch_write_item_output_tests(_) -> {ok, #ddb2_batch_write_item {consumed_capacity = [#ddb2_consumed_capacity{table_name = <<"Forum">>, capacity_units = 3}], item_collection_metrics = undefined, - unprocessed_items = [{<<"Forum">>, + unprocessed_items = [{<<"Forum">>, [{put, [{<<"Name">>, {s, <<"Amazon ElastiCache">>}}, {<<"Category">>, {s, <<"Amazon Web Services">>}}]}]}]}}}), ?_ddb_test( @@ -797,7 +865,7 @@ batch_write_item_output_tests(_) -> } }", {ok, #ddb2_batch_write_item - {consumed_capacity = undefined, + {consumed_capacity = undefined, item_collection_metrics = undefined, unprocessed_items = [{<<"Forum">>, [{put, [{<<"Name">>, {s, <<"Amazon DynamoDB">>}}, @@ -848,8 +916,8 @@ batch_write_item_output_tests(_) -> } }", {ok, #ddb2_batch_write_item - {consumed_capacity = undefined, - item_collection_metrics = + {consumed_capacity = undefined, + item_collection_metrics = [{<<"Table1">>, [#ddb2_item_collection_metrics {item_collection_key = <<"value1">>, @@ -865,7 +933,7 @@ batch_write_item_output_tests(_) -> ]}], unprocessed_items = []}}}) ], - + output_tests(?_f(erlcloud_ddb2:batch_write_item([], [{out, record}])), Tests). %% CreateBackup test based on the API examples: @@ -926,10 +994,10 @@ create_global_table_input_tests(_) -> {region_name, <<"us-west-2">>}])), " { \"GlobalTableName\": \"Thread\", - \"ReplicationGroup\": [ - { + \"ReplicationGroup\": [ + { \"RegionName\": \"us-east-1\" - },{ + },{ \"RegionName\": \"us-west-2\" } ] @@ -937,11 +1005,11 @@ create_global_table_input_tests(_) -> }), ?_ddb_test( {"CreateGlobalTable example request (1 region)", - ?_f(erlcloud_ddb2:create_global_table(<<"Thread">>, #ddb2_replica{region_name = <<"us-west-2">>})), " + ?_f(erlcloud_ddb2:create_global_table(<<"Thread">>, [#ddb2_replica{region_name = <<"us-west-2">>}])), " { \"GlobalTableName\": \"Thread\", - \"ReplicationGroup\": [ - { + \"ReplicationGroup\": [ + { \"RegionName\": \"us-west-2\" } ] @@ -949,15 +1017,15 @@ create_global_table_input_tests(_) -> })], Response = " { - \"GlobalTableDescription\": { + \"GlobalTableDescription\": { \"CreationDateTime\": 1519161181.107, \"GlobalTableArn\": \"arn:aws:dynamodb::111122223333:global-table/Thread\", \"GlobalTableName\": \"Thread\", \"GlobalTableStatus\": \"CREATING\", - \"ReplicationGroup\": [ - { + \"ReplicationGroup\": [ + { \"RegionName\": \"us-east-1\" - },{ + },{ \"RegionName\": \"us-west-2\" } ] @@ -967,17 +1035,17 @@ create_global_table_input_tests(_) -> %% CreateGlobalTable output test: create_global_table_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"CreateGlobalTable example response with CREATING status ", " { - \"GlobalTableDescription\": { + \"GlobalTableDescription\": { \"CreationDateTime\": 1519161181.107, \"GlobalTableArn\": \"arn:aws:dynamodb::111122223333:global-table/Thread\", \"GlobalTableName\": \"Thread\", \"GlobalTableStatus\": \"CREATING\", - \"ReplicationGroup\": [ - { + \"ReplicationGroup\": [ + { \"RegionName\": \"us-east-1\" } ] @@ -993,15 +1061,15 @@ create_global_table_output_tests(_) -> ?_ddb_test( {"CreateGlobalTable example response with ACTIVE status ", " { - \"GlobalTableDescription\": { + \"GlobalTableDescription\": { \"CreationDateTime\": 1519161181.107, \"GlobalTableArn\": \"arn:aws:dynamodb::111122223333:global-table/Thread\", \"GlobalTableName\": \"Thread\", \"GlobalTableStatus\": \"ACTIVE\", - \"ReplicationGroup\": [ - { + \"ReplicationGroup\": [ + { \"RegionName\": \"us-east-1\" - },{ + },{ \"RegionName\": \"eu-west-1\" } ] @@ -1014,7 +1082,7 @@ create_global_table_output_tests(_) -> global_table_status = active, replication_group = [#ddb2_replica_description{region_name = <<"us-east-1">>}, #ddb2_replica_description{region_name = <<"eu-west-1">>}]}}})], - output_tests(?_f(erlcloud_ddb2:create_global_table(<<"Thread">>, {region_name, <<"us-east-1">>})), Tests). + output_tests(?_f(erlcloud_ddb2:create_global_table(<<"Thread">>, [{region_name, <<"us-east-1">>}])), Tests). %% CreateTable test based on the API examples: %% http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_CreateTable.html @@ -1028,7 +1096,7 @@ create_table_input_tests(_) -> {<<"Subject">>, s}, {<<"LastPostDateTime">>, s}], {<<"ForumName">>, <<"Subject">>}, - 5, + 5, 5, [{local_secondary_indexes, [{<<"LastPostIndex">>, <<"LastPostDateTime">>, keys_only}]}, @@ -1071,7 +1139,7 @@ create_table_input_tests(_) -> \"WriteCapacityUnits\": 5 } } - ], + ], \"TableName\": \"Thread\", \"KeySchema\": [ { @@ -1115,12 +1183,12 @@ create_table_input_tests(_) -> {<<"Subject">>, s}, {<<"LastPostDateTime">>, s}], {<<"ForumName">>, <<"Subject">>}, - 5, + 5, 5, [{local_secondary_indexes, [{<<"LastPostIndex">>, <<"LastPostDateTime">>, {include, [<<"Author">>, <<"Body">>]}}]}, {global_secondary_indexes, - [{<<"SubjectIndex">>, {<<"Subject">>, <<"LastPostDateTime">>}, {include, [<<"Author">>]}, 10, 5}]}] + [{<<"SubjectIndex">>, {<<"Subject">>, <<"LastPostDateTime">>}, {include, [<<"Author">>]}, 10, 5}]}] )), " { \"AttributeDefinitions\": [ @@ -1161,7 +1229,7 @@ create_table_input_tests(_) -> \"WriteCapacityUnits\": 5 } } - ], + ], \"TableName\": \"Thread\", \"KeySchema\": [ { @@ -1204,7 +1272,7 @@ create_table_input_tests(_) -> <<"Thread">>, {<<"ForumName">>, s}, <<"ForumName">>, - 1, + 1, 1 )), " { @@ -1225,6 +1293,173 @@ create_table_input_tests(_) -> \"ReadCapacityUnits\": 1, \"WriteCapacityUnits\": 1 } +}" + }), + ?_ddb_test( + {"CreateTable with billing_mode = provisioned", + ?_f(erlcloud_ddb2:create_table( + <<"Thread">>, + {<<"ForumName">>, s}, + <<"ForumName">>, + [{billing_mode, provisioned}, + {provisioned_throughput, {1,1}}] + )), " +{ + \"AttributeDefinitions\": [ + { + \"AttributeName\": \"ForumName\", + \"AttributeType\": \"S\" + } + ], + \"BillingMode\": \"PROVISIONED\", + \"TableName\": \"Thread\", + \"KeySchema\": [ + { + \"AttributeName\": \"ForumName\", + \"KeyType\": \"HASH\" + } + ], + \"ProvisionedThroughput\": { + \"ReadCapacityUnits\": 1, + \"WriteCapacityUnits\": 1 + } +}" + }), + ?_ddb_test( + {"CreateTable with billing_mode = pay_per_request", + ?_f(erlcloud_ddb2:create_table( + <<"Thread">>, + {<<"ForumName">>, s}, + <<"ForumName">>, + [{billing_mode, pay_per_request}, + {global_secondary_indexes, + [{<<"SubjectIndex">>, {<<"Subject">>, <<"LastPostDateTime">>}, {include, [<<"Author">>]}}]}] + )), " +{ + \"AttributeDefinitions\": [ + { + \"AttributeName\": \"ForumName\", + \"AttributeType\": \"S\" + } + ], + \"BillingMode\": \"PAY_PER_REQUEST\", + \"GlobalSecondaryIndexes\": [ + { + \"IndexName\": \"SubjectIndex\", + \"KeySchema\": [ + { + \"AttributeName\": \"Subject\", + \"KeyType\": \"HASH\" + }, + { + \"AttributeName\": \"LastPostDateTime\", + \"KeyType\": \"RANGE\" + } + ], + \"Projection\": { + \"NonKeyAttributes\": [ + \"Author\" + ], + \"ProjectionType\": \"INCLUDE\" + } + } + ], + \"TableName\": \"Thread\", + \"KeySchema\": [ + { + \"AttributeName\": \"ForumName\", + \"KeyType\": \"HASH\" + } + ] +}" + }), + ?_ddb_test( + {"CreateTable with deletion_protection_enabled = true", + ?_f(erlcloud_ddb2:create_table( + <<"Thread">>, + [{<<"ForumName">>, s}, + {<<"Subject">>, s}, + {<<"LastPostDateTime">>, s}], + {<<"ForumName">>, <<"Subject">>}, + 5, + 5, + [{local_secondary_indexes, + [{<<"LastPostIndex">>, <<"LastPostDateTime">>, keys_only}]}, + {global_secondary_indexes, + [{<<"SubjectIndex">>, {<<"Subject">>, <<"LastPostDateTime">>}, keys_only, 10, 5}]}, + {deletion_protection_enabled, true}] + )), " +{ + \"AttributeDefinitions\": [ + { + \"AttributeName\": \"ForumName\", + \"AttributeType\": \"S\" + }, + { + \"AttributeName\": \"Subject\", + \"AttributeType\": \"S\" + }, + { + \"AttributeName\": \"LastPostDateTime\", + \"AttributeType\": \"S\" + } + ], + \"GlobalSecondaryIndexes\": [ + { + \"IndexName\": \"SubjectIndex\", + \"KeySchema\": [ + { + \"AttributeName\": \"Subject\", + \"KeyType\": \"HASH\" + }, + { + \"AttributeName\": \"LastPostDateTime\", + \"KeyType\": \"RANGE\" + } + ], + \"Projection\": { + \"ProjectionType\": \"KEYS_ONLY\" + }, + \"ProvisionedThroughput\": { + \"ReadCapacityUnits\": 10, + \"WriteCapacityUnits\": 5 + } + } + ], + \"DeletionProtectionEnabled\": true, + \"TableName\": \"Thread\", + \"KeySchema\": [ + { + \"AttributeName\": \"ForumName\", + \"KeyType\": \"HASH\" + }, + { + \"AttributeName\": \"Subject\", + \"KeyType\": \"RANGE\" + } + ], + \"LocalSecondaryIndexes\": [ + { + \"IndexName\": \"LastPostIndex\", + \"KeySchema\": [ + { + \"AttributeName\": \"ForumName\", + \"KeyType\": \"HASH\" + }, + { + \"AttributeName\": \"LastPostDateTime\", + \"KeyType\": \"RANGE\" + } + ], + \"Projection\": { + \"ProjectionType\": \"KEYS_ONLY\" + } + } + ], + \"ProvisionedThroughput\": { + \"ReadCapacityUnits\": 5, + \"WriteCapacityUnits\": 5 + } }" }) ], @@ -1513,7 +1748,7 @@ delete_backup_output_tests(_) -> output_tests(?_f(erlcloud_ddb2:delete_backup(<<"arn:aws:dynamodb:">>)), Tests). create_table_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"CreateTable example response", " { @@ -1532,6 +1767,10 @@ create_table_output_tests(_) -> \"AttributeType\": \"S\" } ], + \"BillingModeSummary\": { + \"BillingMode\": \"PROVISIONED\", + \"LastUpdateToPayPerRequestDateTime\": 1.36372808007E9 + }, \"CreationDateTime\": 1.36372808007E9, \"GlobalSecondaryIndexes\": [ { @@ -1606,6 +1845,10 @@ create_table_output_tests(_) -> {attribute_definitions = [{<<"ForumName">>, s}, {<<"LastPostDateTime">>, s}, {<<"Subject">>, s}], + billing_mode_summary = + #ddb2_billing_mode_summary{ + billing_mode = provisioned, + last_update_to_pay_per_request_date_time = 1363728080.07}, creation_date_time = 1363728080.07, item_count = 0, key_schema = {<<"ForumName">>, <<"Subject">>}, @@ -1616,7 +1859,7 @@ create_table_output_tests(_) -> item_count = 0, key_schema = {<<"ForumName">>, <<"LastPostDateTime">>}, projection = keys_only}], - global_secondary_indexes = + global_secondary_indexes = [#ddb2_global_secondary_index_description{ index_name = <<"SubjectIndex">>, index_size_bytes = 2048, @@ -1631,7 +1874,7 @@ create_table_output_tests(_) -> read_capacity_units = 3, write_capacity_units = 4} }], - provisioned_throughput = + provisioned_throughput = #ddb2_provisioned_throughput_description{ last_decrease_date_time = undefined, last_increase_date_time = undefined, @@ -1659,6 +1902,10 @@ create_table_output_tests(_) -> \"AttributeType\": \"S\" } ], + \"BillingModeSummary\": { + \"BillingMode\": \"PROVISIONED\", + \"LastUpdateToPayPerRequestDateTime\": 1.36372808007E9 + }, \"GlobalSecondaryIndexes\": [ { \"IndexName\": \"SubjectIndex\", @@ -1689,7 +1936,7 @@ create_table_output_tests(_) -> \"WriteCapacityUnits\": 4 } } - ], + ], \"CreationDateTime\": 1.36372808007E9, \"ItemCount\": 0, \"KeySchema\": [ @@ -1737,6 +1984,10 @@ create_table_output_tests(_) -> {attribute_definitions = [{<<"ForumName">>, s}, {<<"LastPostDateTime">>, s}, {<<"Subject">>, s}], + billing_mode_summary = + #ddb2_billing_mode_summary{ + billing_mode = provisioned, + last_update_to_pay_per_request_date_time = 1363728080.07}, creation_date_time = 1363728080.07, item_count = 0, key_schema = {<<"ForumName">>, <<"Subject">>}, @@ -1747,7 +1998,7 @@ create_table_output_tests(_) -> item_count = 0, key_schema = {<<"ForumName">>, <<"LastPostDateTime">>}, projection = {include, [<<"Author">>, <<"Body">>]}}], - global_secondary_indexes = + global_secondary_indexes = [#ddb2_global_secondary_index_description{ index_name = <<"SubjectIndex">>, index_size_bytes = 2048, @@ -1762,7 +2013,7 @@ create_table_output_tests(_) -> read_capacity_units = 3, write_capacity_units = 4} }], - provisioned_throughput = + provisioned_throughput = #ddb2_provisioned_throughput_description{ last_decrease_date_time = undefined, last_increase_date_time = undefined, @@ -1782,6 +2033,62 @@ create_table_output_tests(_) -> \"AttributeType\": \"S\" } ], + \"BillingModeSummary\": { + \"BillingMode\": \"PROVISIONED\", + \"LastUpdateToPayPerRequestDateTime\": 1.36372808007E9 + }, + \"CreationDateTime\": 1.36372808007E9, + \"ItemCount\": 0, + \"KeySchema\": [ + { + \"AttributeName\": \"ForumName\", + \"KeyType\": \"HASH\" + } + ], + \"ProvisionedThroughput\": { + \"NumberOfDecreasesToday\": 0, + \"ReadCapacityUnits\": 1, + \"WriteCapacityUnits\": 1 + }, + \"TableName\": \"Thread\", + \"TableSizeBytes\": 0, + \"TableStatus\": \"CREATING\" + } +}", + {ok, #ddb2_table_description + {attribute_definitions = [{<<"ForumName">>, s}], + billing_mode_summary = + #ddb2_billing_mode_summary{ + billing_mode = provisioned, + last_update_to_pay_per_request_date_time = 1363728080.07}, + creation_date_time = 1363728080.07, + item_count = 0, + key_schema = <<"ForumName">>, + local_secondary_indexes = undefined, + provisioned_throughput = + #ddb2_provisioned_throughput_description{ + last_decrease_date_time = undefined, + last_increase_date_time = undefined, + number_of_decreases_today = 0, + read_capacity_units = 1, + write_capacity_units = 1}, + table_name = <<"Thread">>, + table_size_bytes = 0, + table_status = creating}}}), + ?_ddb_test( + {"CreateTable response with billing_mode = pay_per_request", " +{ + \"TableDescription\": { + \"AttributeDefinitions\": [ + { + \"AttributeName\": \"ForumName\", + \"AttributeType\": \"S\" + } + ], + \"BillingModeSummary\": { + \"BillingMode\": \"PAY_PER_REQUEST\", + \"LastUpdateToPayPerRequestDateTime\": 1.36372808007E9 + }, \"CreationDateTime\": 1.36372808007E9, \"ItemCount\": 0, \"KeySchema\": [ @@ -1802,11 +2109,15 @@ create_table_output_tests(_) -> }", {ok, #ddb2_table_description {attribute_definitions = [{<<"ForumName">>, s}], + billing_mode_summary = + #ddb2_billing_mode_summary{ + billing_mode = pay_per_request, + last_update_to_pay_per_request_date_time = 1363728080.07}, creation_date_time = 1363728080.07, item_count = 0, key_schema = <<"ForumName">>, local_secondary_indexes = undefined, - provisioned_throughput = + provisioned_throughput = #ddb2_provisioned_throughput_description{ last_decrease_date_time = undefined, last_increase_date_time = undefined, @@ -1817,7 +2128,7 @@ create_table_output_tests(_) -> table_size_bytes = 0, table_status = creating}}}) ], - + output_tests(?_f(erlcloud_ddb2:create_table(<<"name">>, [{<<"key">>, s}], <<"key">>, 5, 10)), Tests). %% DeleteItem test based on the API examples: @@ -1826,7 +2137,7 @@ delete_item_input_tests(_) -> Tests = [?_ddb_test( {"DeleteItem example request", - ?_f(erlcloud_ddb2:delete_item(<<"Thread">>, + ?_f(erlcloud_ddb2:delete_item(<<"Thread">>, [{<<"ForumName">>, {s, <<"Amazon DynamoDB">>}}, {<<"Subject">>, {s, <<"How do I update multiple items?">>}}], [{return_values, all_old}, @@ -1872,7 +2183,7 @@ delete_item_input_tests(_) -> }), ?_ddb_test( {"DeleteItem return metrics", - ?_f(erlcloud_ddb2:delete_item(<<"Thread">>, + ?_f(erlcloud_ddb2:delete_item(<<"Thread">>, {<<"ForumName">>, {s, <<"Amazon DynamoDB">>}}, [{return_consumed_capacity, total}, {return_item_collection_metrics, size}])), " @@ -1915,7 +2226,7 @@ delete_item_input_tests(_) -> input_tests(Response, Tests). delete_item_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"DeleteItem example response", " { @@ -1975,7 +2286,7 @@ delete_item_output_tests(_) -> size_estimate_range_gb = {1,2}} }}}) ], - + output_tests(?_f(erlcloud_ddb2:delete_item(<<"table">>, {<<"k">>, <<"v">>}, [{out, record}])), Tests). %% DeleteTable test based on the API examples: @@ -2008,7 +2319,7 @@ delete_table_input_tests(_) -> input_tests(Response, Tests). delete_table_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"DeleteTable example response", " { @@ -2034,7 +2345,7 @@ delete_table_output_tests(_) -> table_size_bytes = 0, table_status = deleting}}}) ], - + output_tests(?_f(erlcloud_ddb2:delete_table(<<"name">>)), Tests). %% DescribeBackup test based on the API examples: @@ -2363,15 +2674,15 @@ describe_global_table_input_tests(_) -> })], Response = " { - \"GlobalTableDescription\": { + \"GlobalTableDescription\": { \"CreationDateTime\": 1519161181.107, \"GlobalTableArn\": \"arn:aws:dynamodb::111122223333:global-table/Thread\", \"GlobalTableName\": \"Thread\", \"GlobalTableStatus\": \"ACTIVE\", - \"ReplicationGroup\": [ - { + \"ReplicationGroup\": [ + { \"RegionName\": \"us-east-1\" - },{ + },{ \"RegionName\": \"us-west-2\" } ] @@ -2381,17 +2692,17 @@ describe_global_table_input_tests(_) -> %% DescribeGlobalTable output test: describe_global_table_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"DescribeGlobalTable example response with CREATING status ", " { - \"GlobalTableDescription\": { + \"GlobalTableDescription\": { \"CreationDateTime\": 1519161181.107, \"GlobalTableArn\": \"arn:aws:dynamodb::111122223333:global-table/Thread\", \"GlobalTableName\": \"Thread\", \"GlobalTableStatus\": \"CREATING\", - \"ReplicationGroup\": [ - { + \"ReplicationGroup\": [ + { \"RegionName\": \"us-east-1\" } ] @@ -2407,15 +2718,15 @@ describe_global_table_output_tests(_) -> ?_ddb_test( {"DescribeGlobalTable example response with ACTIVE status ", " { - \"GlobalTableDescription\": { + \"GlobalTableDescription\": { \"CreationDateTime\": 1519161181.107, \"GlobalTableArn\": \"arn:aws:dynamodb::111122223333:global-table/Thread\", \"GlobalTableName\": \"Thread\", \"GlobalTableStatus\": \"ACTIVE\", - \"ReplicationGroup\": [ - { + \"ReplicationGroup\": [ + { \"RegionName\": \"us-east-1\" - },{ + },{ \"RegionName\": \"eu-west-1\" } ] @@ -2430,6 +2741,254 @@ describe_global_table_output_tests(_) -> #ddb2_replica_description{region_name = <<"eu-west-1">>}]}}})], output_tests(?_f(erlcloud_ddb2:describe_global_table(<<"Thread">>)), Tests). +%% DescribeGlobalTableSettings tests based on the API request/response syntax: +%% http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DescribeGlobalTableSettings.html +describe_global_table_settings_input_tests(_) -> + Tests = + [?_ddb_test( + {"DescribeGlobalTableSettings example request", + ?_f(erlcloud_ddb2:describe_global_table_settings(<<"Thread">>)), " +{ + \"GlobalTableName\":\"Thread\" +}" + }) + ], + Response = " +{ + \"GlobalTableName\": \"Thread\", + \"ReplicaSettings\": [ + { + \"RegionName\": \"us-west-2\", + \"ReplicaBillingModeSummary\": { + \"BillingMode\": \"PROVISIONED\", + \"LastUpdateToPayPerRequestDateTime\": 1578092745.455 + }, + \"ReplicaGlobalSecondaryIndexSettings\": [ + { + \"IndexName\": \"id-index\", + \"IndexStatus\": \"ACTIVE\", + \"ProvisionedReadCapacityAutoScalingSettings\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"arn:test\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicies\": [ + { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + ] + }, + \"ProvisionedReadCapacityUnits\": 10, + \"ProvisionedWriteCapacityAutoScalingSettings\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"arn:test\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicies\": [ + { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": true, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + ] + }, + \"ProvisionedWriteCapacityUnits\": 10 + } + ], + \"ReplicaProvisionedReadCapacityAutoScalingSettings\": { + \"AutoScalingDisabled\": true, + \"AutoScalingRoleArn\": \"arn:test\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicies\": [ + { + \"PolicyName\": \"policy\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + ] + }, + \"ReplicaProvisionedReadCapacityUnits\": 10, + \"ReplicaProvisionedWriteCapacityAutoScalingSettings\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"arn:test\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicies\": [ + { + \"PolicyName\": \"policy\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + ] + }, + \"ReplicaProvisionedWriteCapacityUnits\": 10, + \"ReplicaStatus\": \"ACTIVE\" + } + ] +}", + input_tests(Response, Tests). + +describe_global_table_settings_output_tests(_) -> + Tests = + [?_ddb_test( + {"DescribeGlobalTableSettings example response", " +{ + \"GlobalTableName\": \"Thread\", + \"ReplicaSettings\": [ + { + \"RegionName\": \"us-west-2\", + \"ReplicaBillingModeSummary\": { + \"BillingMode\": \"PROVISIONED\", + \"LastUpdateToPayPerRequestDateTime\": 1578092745.455 + }, + \"ReplicaGlobalSecondaryIndexSettings\": [ + { + \"IndexName\": \"id-index\", + \"IndexStatus\": \"ACTIVE\", + \"ProvisionedReadCapacityAutoScalingSettings\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"arn:test\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicies\": [ + { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + ] + }, + \"ProvisionedReadCapacityUnits\": 10, + \"ProvisionedWriteCapacityAutoScalingSettings\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"arn:test\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicies\": [ + { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + ] + }, + \"ProvisionedWriteCapacityUnits\": 10 + } + ], + \"ReplicaProvisionedReadCapacityAutoScalingSettings\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"arn:test\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicies\": [ + { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + ] + }, + \"ReplicaProvisionedReadCapacityUnits\": 10, + \"ReplicaProvisionedWriteCapacityAutoScalingSettings\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"arn:test\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicies\": [ + { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + ] + }, + \"ReplicaProvisionedWriteCapacityUnits\": 10, + \"ReplicaStatus\": \"ACTIVE\" + } + ] +}", {ok, [#ddb2_replica_settings_description{region_name = <<"us-west-2">>, + replica_billing_mode_summary = #ddb2_billing_mode_summary{billing_mode = provisioned, + last_update_to_pay_per_request_date_time = 1578092745.455}, + replica_global_secondary_index_settings = [#ddb2_replica_global_secondary_index_settings_description{index_name = <<"id-index">>, + index_status = active, + provisioned_read_capacity_auto_scaling_settings = #ddb2_auto_scaling_settings_description{auto_scaling_disabled = false, + auto_scaling_role_arn = <<"arn:test">>, + maximum_units = 20, + minimum_units = 10, + scaling_policies = [#ddb2_auto_scaling_policy_description{policy_name = <<"PolicyName">>, + target_tracking_scaling_policy_configuration = #ddb2_auto_scaling_target_tracking_scaling_policy_configuration_description{disable_scale_in = false, + scale_in_cooldown = 600, + scale_out_cooldown = 600, + target_value = 70.0}}]}, + provisioned_read_capacity_units = 10, + provisioned_write_capacity_auto_scaling_settings = #ddb2_auto_scaling_settings_description{auto_scaling_disabled = false, + auto_scaling_role_arn = <<"arn:test">>, + maximum_units = 20, + minimum_units = 10, + scaling_policies = [#ddb2_auto_scaling_policy_description{policy_name = <<"PolicyName">>, + target_tracking_scaling_policy_configuration = #ddb2_auto_scaling_target_tracking_scaling_policy_configuration_description{disable_scale_in = false, + scale_in_cooldown = 600, + scale_out_cooldown = 600, + target_value = 70.0}}]}, + provisioned_write_capacity_units = 10}], + replica_provisioned_read_capacity_auto_scaling_settings = #ddb2_auto_scaling_settings_description{auto_scaling_disabled = false, + auto_scaling_role_arn = <<"arn:test">>, + maximum_units = 20, + minimum_units = 10, + scaling_policies = [#ddb2_auto_scaling_policy_description{policy_name = <<"PolicyName">>, + target_tracking_scaling_policy_configuration = #ddb2_auto_scaling_target_tracking_scaling_policy_configuration_description{disable_scale_in = false, + scale_in_cooldown = 600, + scale_out_cooldown = 600, + target_value = 70.0}}]}, + replica_provisioned_read_capacity_units = 10, + replica_provisioned_write_capacity_auto_scaling_settings = #ddb2_auto_scaling_settings_description{auto_scaling_disabled = false, + auto_scaling_role_arn = <<"arn:test">>, + maximum_units = 20, + minimum_units = 10, + scaling_policies = [#ddb2_auto_scaling_policy_description{policy_name = <<"PolicyName">>, + target_tracking_scaling_policy_configuration = #ddb2_auto_scaling_target_tracking_scaling_policy_configuration_description{disable_scale_in = false, + scale_in_cooldown = 600, + scale_out_cooldown = 600, + target_value = 70.0}}]}, + replica_provisioned_write_capacity_units = 10, + replica_status = active}]}})], + output_tests(?_f(erlcloud_ddb2:describe_global_table_settings(<<"Thread">>)), Tests). + %% DescribeTable test based on the API examples: %% http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DescribeTable.html describe_table_input_tests(_) -> @@ -2461,6 +3020,7 @@ describe_table_input_tests(_) -> } ], \"CreationDateTime\": 1.363729002358E9, + \"GlobalTableVersion\": \"2019.11.21\", \"ItemCount\": 0, \"KeySchema\": [ { @@ -2492,6 +3052,16 @@ describe_table_input_tests(_) -> } } ], + \"Replicas\": [ + { + \"RegionName\": \"us-west-2\", + \"ReplicaStatus\": \"ACTIVE\", + }, + { + \"RegionName\": \"eu-west-2\", + \"ReplicaStatus\": \"ACTIVE\" + } + ], \"ProvisionedThroughput\": { \"NumberOfDecreasesToday\": 0, \"ReadCapacityUnits\": 5, @@ -2505,7 +3075,7 @@ describe_table_input_tests(_) -> input_tests(Response, Tests). describe_table_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"DescribeTable example response", " { @@ -2554,8 +3124,9 @@ describe_table_output_tests(_) -> \"WriteCapacityUnits\": 4 } } - ], + ], \"CreationDateTime\": 1.363729002358E9, + \"GlobalTableVersion\": \"2019.11.21\", \"ItemCount\": 0, \"KeySchema\": [ { @@ -2587,6 +3158,16 @@ describe_table_output_tests(_) -> } } ], + \"Replicas\": [ + { + \"RegionName\": \"us-west-2\", + \"ReplicaStatus\": \"ACTIVE\", + }, + { + \"RegionName\": \"eu-west-2\", + \"ReplicaStatus\": \"ACTIVE\" + } + ], \"ProvisionedThroughput\": { \"NumberOfDecreasesToday\": 0, \"ReadCapacityUnits\": 5, @@ -2594,7 +3175,8 @@ describe_table_output_tests(_) -> }, \"TableName\": \"Thread\", \"TableSizeBytes\": 0, - \"TableStatus\": \"ACTIVE\" + \"TableStatus\": \"ACTIVE\", + \"DeletionProtectionEnabled\": true } }", {ok, #ddb2_table_description @@ -2602,6 +3184,7 @@ describe_table_output_tests(_) -> {<<"LastPostDateTime">>, s}, {<<"Subject">>, s}], creation_date_time = 1363729002.358, + global_table_version = <<"2019.11.21">>, item_count = 0, key_schema = {<<"ForumName">>, <<"Subject">>}, local_secondary_indexes = @@ -2611,7 +3194,7 @@ describe_table_output_tests(_) -> item_count = 0, key_schema = {<<"ForumName">>, <<"LastPostDateTime">>}, projection = keys_only}], - global_secondary_indexes = + global_secondary_indexes = [#ddb2_global_secondary_index_description{ index_name = <<"SubjectIndex">>, index_size_bytes = 2048, @@ -2625,22 +3208,237 @@ describe_table_output_tests(_) -> number_of_decreases_today = 2, read_capacity_units = 3, write_capacity_units = 4} - }], - provisioned_throughput = + }], + provisioned_throughput = #ddb2_provisioned_throughput_description{ last_decrease_date_time = undefined, last_increase_date_time = undefined, number_of_decreases_today = 0, read_capacity_units = 5, write_capacity_units = 5}, + replicas = [#ddb2_replica_description{region_name = <<"us-west-2">>, + replica_status = active}, + #ddb2_replica_description{region_name = <<"eu-west-2">>, + replica_status = active}], table_name = <<"Thread">>, table_size_bytes = 0, - table_status = active}}}) + table_status = active, + deletion_protection_enabled = true}}}) ], - + output_tests(?_f(erlcloud_ddb2:describe_table(<<"name">>)), Tests). +%% DescribeTableReplicaAutoScaling test based on API request/response syntax +describe_table_replica_auto_scaling_input_tests(_) -> + Tests = + [?_ddb_test( + {"DescribeTableReplicaAutoScaling example request", + ?_f(erlcloud_ddb2:describe_table_replica_auto_scaling(<<"Thread">>)), " +{ + \"TableName\":\"Thread\" +}" + })], + Response = " +{ + \"TableAutoScalingDescription\": { + \"Replicas\": [ + { + \"GlobalSecondaryIndexes\": [ + { + \"IndexName\": \"id-index\", + \"IndexStatus\": \"ACTIVE\", + \"ProvisionedReadCapacityAutoScalingSettings\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"arn:test\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicies\": [ + { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + ] + }, + \"ProvisionedWriteCapacityAutoScalingSettings\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"arn:test\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicies\": [ + { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + ] + } + } + ], + \"RegionName\": \"us-west-2\", + \"ReplicaProvisionedReadCapacityAutoScalingSettings\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"arn:test\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicies\": [ + { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + ] + }, + \"ReplicaProvisionedWriteCapacityAutoScalingSettings\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"arn:test\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicies\": [ + { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + ] + }, + \"ReplicaStatus\": \"ACTIVE\" + } + ], + \"TableName\": \"Thread\", + \"TableStatus\": \"ACTIVE\" + } +}", + input_tests(Response, Tests). + +describe_table_replica_auto_scaling_output_tests(_) -> + AutoScalingSettings = #ddb2_auto_scaling_settings_description{auto_scaling_disabled = false, + auto_scaling_role_arn = <<"arn:test">>, + maximum_units = 20, + minimum_units = 10, + scaling_policies = [#ddb2_auto_scaling_policy_description{policy_name = <<"PolicyName">>, + target_tracking_scaling_policy_configuration = #ddb2_auto_scaling_target_tracking_scaling_policy_configuration_description{disable_scale_in = false, + scale_in_cooldown = 600, + scale_out_cooldown = 600, + target_value = 70.0}}]}, + Tests = + [?_ddb_test( + {"DescribeGlobalTableSettings example response", " +{ + \"TableAutoScalingDescription\": { + \"Replicas\": [ + { + \"GlobalSecondaryIndexes\": [ + { + \"IndexName\": \"id-index\", + \"IndexStatus\": \"ACTIVE\", + \"ProvisionedReadCapacityAutoScalingSettings\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"arn:test\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicies\": [ + { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + ] + }, + \"ProvisionedWriteCapacityAutoScalingSettings\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"arn:test\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicies\": [ + { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + ] + } + } + ], + \"RegionName\": \"us-west-2\", + \"ReplicaProvisionedReadCapacityAutoScalingSettings\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"arn:test\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicies\": [ + { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + ] + }, + \"ReplicaProvisionedWriteCapacityAutoScalingSettings\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"arn:test\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicies\": [ + { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + ] + }, + \"ReplicaStatus\": \"ACTIVE\" + } + ], + \"TableName\": \"Thread\", + \"TableStatus\": \"ACTIVE\" + } +}", + {ok, #ddb2_table_auto_scaling_description{replicas = [#ddb2_replica_auto_scaling_description{global_secondary_indexes = [#ddb2_replica_global_secondary_index_auto_scaling_description{index_name = <<"id-index">>, + index_status = active, + provisioned_read_capacity_auto_scaling_settings = AutoScalingSettings, + provisioned_write_capacity_auto_scaling_settings = AutoScalingSettings}], + region_name = <<"us-west-2">>, + replica_provisioned_read_capacity_auto_scaling_settings = AutoScalingSettings, + replica_provisioned_write_capacity_auto_scaling_settings = AutoScalingSettings, + replica_status = active}], + table_name = <<"Thread">>, + table_status = active}}})], + output_tests(?_f(erlcloud_ddb2:describe_table_replica_auto_scaling(<<"Thread">>)), Tests). + %% DescribeTimeToLive test describe_time_to_live_input_tests(_) -> Tests = @@ -2652,7 +3450,7 @@ describe_time_to_live_input_tests(_) -> }" })], Response = " -{ +{ \"TimeToLiveDescription\": { \"AttributeName\": \"ExpirationTime\", \"TimeToLiveStatus\": \"ENABLED\" @@ -2662,7 +3460,7 @@ describe_time_to_live_input_tests(_) -> %% DescribeTimeToLive test: describe_time_to_live_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"DescribeTimeToLive example response with enabled TTL", " { @@ -2737,7 +3535,7 @@ get_item_input_tests(_) -> [?_ddb_test( {"GetItem example request, with fully specified keys", ?_f(erlcloud_ddb2:get_item(<<"Thread">>, - [{<<"ForumName">>, {s, <<"Amazon DynamoDB">>}}, + [{<<"ForumName">>, {s, <<"Amazon DynamoDB">>}}, {<<"Subject">>, {s, <<"How do I update multiple items?">>}}], [{attributes_to_get, [<<"LastPostDateTime">>, <<"Message">>, <<"Tags">>]}, consistent_read, @@ -2748,7 +3546,7 @@ get_item_input_tests(_) -> Example1Response}), ?_ddb_test( {"GetItem example request, with inferred key types", - ?_f(erlcloud_ddb2:get_item(<<"Thread">>, + ?_f(erlcloud_ddb2:get_item(<<"Thread">>, [{<<"ForumName">>, "Amazon DynamoDB"}, {<<"Subject">>, <<"How do I update multiple items?">>}], [{attributes_to_get, [<<"LastPostDateTime">>, <<"Message">>, <<"Tags">>]}, @@ -2810,7 +3608,7 @@ get_item_input_tests(_) -> input_tests(Response, Tests). get_item_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"GetItem example response", " { @@ -2869,19 +3667,19 @@ get_item_output_tests(_) -> {<<"empty_map">>, []}], consumed_capacity = undefined}}}), ?_ddb_test( - {"GetItem item not found", + {"GetItem item not found", "{}", {ok, #ddb2_get_item{item = undefined}}}), ?_ddb_test( - {"GetItem no attributes returned", + {"GetItem no attributes returned", "{\"Item\":{}}", {ok, #ddb2_get_item{item = []}}}) ], - + output_tests(?_f(erlcloud_ddb2:get_item(<<"table">>, {<<"k">>, <<"v">>}, [{out, record}])), Tests). get_item_output_typed_tests(_) -> - Tests = + Tests = [?_ddb_test( {"GetItem typed test all attribute types", " {\"Item\": @@ -2914,7 +3712,7 @@ get_item_output_typed_tests(_) -> {<<"empty_map">>, {m, []}}], consumed_capacity = undefined}}}) ], - + output_tests(?_f(erlcloud_ddb2:get_item( <<"table">>, {<<"k">>, <<"v">>}, [{out, typed_record}])), Tests). @@ -3001,7 +3799,7 @@ list_global_tables_input_tests(_) -> }), ?_ddb_test( {"ListGlobalTables empty request", - ?_f(erlcloud_ddb2:list_global_tables()), + ?_f(erlcloud_ddb2:list_global_tables()), "{}" }) @@ -3009,22 +3807,22 @@ list_global_tables_input_tests(_) -> Response = " { - \"GlobalTables\": [ - { + \"GlobalTables\": [ + { \"GlobalTableName\": \"Forum\", - \"ReplicationGroup\": [ - { + \"ReplicationGroup\": [ + { \"RegionName\": \"us-west-2\" - },{ + },{ \"RegionName\": \"us-east-1\" } ] - },{ + },{ \"GlobalTableName\": \"Thread\", - \"ReplicationGroup\": [ - { + \"ReplicationGroup\": [ + { \"RegionName\": \"us-east-1\" - },{ + },{ \"RegionName\": \"eu-west-1\" } ] @@ -3036,26 +3834,26 @@ list_global_tables_input_tests(_) -> %% ListGlobalTables output test: list_global_tables_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"ListGlobalTables example response", " { - \"GlobalTables\": [ - { + \"GlobalTables\": [ + { \"GlobalTableName\": \"Forum\", - \"ReplicationGroup\": [ - { + \"ReplicationGroup\": [ + { \"RegionName\": \"us-west-2\" - },{ + },{ \"RegionName\": \"us-east-1\" } ] - },{ + },{ \"GlobalTableName\": \"Thread\", - \"ReplicationGroup\": [ - { + \"ReplicationGroup\": [ + { \"RegionName\": \"us-east-1\" - },{ + },{ \"RegionName\": \"eu-west-1\" } ] @@ -3074,7 +3872,7 @@ list_global_tables_output_tests(_) -> replication_group = [#ddb2_replica{region_name = <<"us-east-1">>}, #ddb2_replica{region_name = <<"eu-west-1">>}]}]}}}) ], - + output_tests(?_f(erlcloud_ddb2:list_global_tables([{out, record}])), Tests). %% ListTables test based on the API examples: @@ -3091,7 +3889,7 @@ list_tables_input_tests(_) -> }), ?_ddb_test( {"ListTables empty request", - ?_f(erlcloud_ddb2:list_tables()), + ?_f(erlcloud_ddb2:list_tables()), "{}" }) @@ -3105,7 +3903,7 @@ list_tables_input_tests(_) -> input_tests(Response, Tests). list_tables_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"ListTables example response", " { @@ -3116,7 +3914,7 @@ list_tables_output_tests(_) -> {last_evaluated_table_name = <<"Thread">>, table_names = [<<"Forum">>, <<"Reply">>, <<"Thread">>]}}}) ], - + output_tests(?_f(erlcloud_ddb2:list_tables([{out, record}])), Tests). %% ListTagsOfResource test based on the API: @@ -3149,7 +3947,7 @@ list_tags_of_resource_input_tests(_) -> input_tests(Response, Tests). list_tags_of_resource_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"ListTagsOfResource example response", " { @@ -3170,7 +3968,7 @@ list_tags_of_resource_output_tests(_) -> tags = [{<<"example_key1">>, <<"example_value1">>}, {<<"example_key2">>, <<"example_value2">>}]}}}) ], - + output_tests(?_f(erlcloud_ddb2:list_tags_of_resource(<<"arn:aws:dynamodb:us-east-1:111122223333:table/Forum">>, [{out, record}])), Tests). @@ -3181,7 +3979,7 @@ put_item_input_tests(_) -> Tests = [?_ddb_test( {"PutItem example request", - ?_f(erlcloud_ddb2:put_item(<<"Thread">>, + ?_f(erlcloud_ddb2:put_item(<<"Thread">>, [{<<"LastPostedBy">>, <<"fred@example.com">>}, {<<"ForumName">>, <<"Amazon DynamoDB">>}, {<<"LastPostDateTime">>, <<"201303190422">>}, @@ -3223,7 +4021,7 @@ put_item_input_tests(_) -> }), ?_ddb_test( {"PutItem float inputs", - ?_f(erlcloud_ddb2:put_item(<<"Thread">>, + ?_f(erlcloud_ddb2:put_item(<<"Thread">>, [{<<"typed float">>, {n, 1.2}}, {<<"untyped float">>, 3.456}, {<<"mixed set">>, {ns, [7.8, 9.0, 10]}}], @@ -3334,7 +4132,7 @@ put_item_input_tests(_) -> input_tests(Response, Tests). put_item_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"PutItem example response", " { @@ -3431,7 +4229,7 @@ put_item_output_tests(_) -> ] }}}) ], - + output_tests(?_f(erlcloud_ddb2:put_item(<<"table">>, [], [{out, record}])), Tests). %% Query test based on the API examples: @@ -3571,7 +4369,7 @@ q_input_tests(_) -> input_tests(Response, Tests). q_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"Query example 1 response", " { @@ -3667,12 +4465,12 @@ q_output_tests(_) -> } }", {ok, #ddb2_q{count = 17, - last_evaluated_key = + last_evaluated_key = [{<<"ForumName">>, {s, <<"Amazon DynamoDB">>}}, {<<"Subject">>, {s, <<"Exclusive key can have 3 parts">>}}, {<<"LastPostDateTime">>, {s, <<"20130102054211">>}}] }}}) ], - + output_tests(?_f(erlcloud_ddb2:q(<<"table">>, [{<<"k">>, <<"v">>, eq}], [{out, record}])), Tests). %% RestoreTableFromBackup test based on the API examples: @@ -4196,7 +4994,7 @@ scan_input_tests(_) -> }), ?_ddb_test( {"Scan example 2 request", - ?_f(erlcloud_ddb2:scan(<<"Reply">>, + ?_f(erlcloud_ddb2:scan(<<"Reply">>, [{scan_filter, [{<<"PostedBy">>, <<"joe@example.com">>, eq}]}, {return_consumed_capacity, total}])), " { @@ -4225,7 +5023,7 @@ scan_input_tests(_) -> }), ?_ddb_test( {"Scan exclusive start key", - ?_f(erlcloud_ddb2:scan(<<"Reply">>, + ?_f(erlcloud_ddb2:scan(<<"Reply">>, [{exclusive_start_key, [{<<"ForumName">>, {s, <<"Amazon DynamoDB">>}}, {<<"LastPostDateTime">>, {n, 20130102054211}}]}])), " { @@ -4369,7 +5167,7 @@ scan_input_tests(_) -> input_tests(Response, Tests). scan_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"Scan example 1 response", " { @@ -4573,7 +5371,7 @@ scan_output_tests(_) -> {<<"LastPostDateTime">>, {n, 20130102054211}}], scanned_count = 4}}}) ], - + output_tests(?_f(erlcloud_ddb2:scan(<<"name">>, [{out, record}])), Tests). %% TagResource test based on the API: @@ -4604,7 +5402,7 @@ tag_resource_input_tests(_) -> input_tests(Response, Tests). tag_resource_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"ListTagsOfResource example response", "", ok}) @@ -4613,95 +5411,506 @@ tag_resource_output_tests(_) -> [{<<"example_key1">>, <<"example_value1">>}, {<<"example_key2">>, <<"example_value2">>}])), Tests). -%% UntagResource test based on the API: -%% https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UntagResource.html -untag_resource_input_tests(_) -> + +%% TransactGetItem test using synthetic data (there are no AWS provided examples): +%% https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactGetItems.html +transact_get_items_input_tests(_) -> Tests = [?_ddb_test( - {"ListTagsOfResource example request", - ?_f(erlcloud_ddb2:untag_resource(<<"arn:aws:dynamodb:us-east-1:111122223333:table/Forum">>, - [<<"example_key1">>, <<"example_key2">>])), " + {"TransactGetItems request", + ?_f(erlcloud_ddb2:transact_get_items( + [{get, {<<"PersonalInfo">>, [{<<"Name">>, {s, <<"John Smith">>}}, + {<<"DOB">>, {s, <<"11/11/2011">>}}]}}, + {get, {<<"EmployeeRecord">>, [{<<"Name">>, {s, <<"John Smith">>}}, + {<<"DOH">>, {s, <<"11/11/2018">>}}]}}], + [{return_consumed_capacity, total}])), " { - \"ResourceArn\": \"arn:aws:dynamodb:us-east-1:111122223333:table/Forum\", - \"TagKeys\": [ - \"example_key1\", - \"example_key2\" - ] + \"TransactItems\": [ + {\"Get\": { + \"Key\": { + \"Name\": { + \"S\": \"John Smith\" + }, + \"DOB\": { + \"S\": \"11/11/2011\" + } + }, + \"TableName\": \"PersonalInfo\" + }}, + {\"Get\": { + \"Key\": { + \"Name\": { + \"S\": \"John Smith\" + }, + \"DOH\": { + \"S\": \"11/11/2018\" + } + }, + \"TableName\": \"EmployeeRecord\" + }} + ], + \"ReturnConsumedCapacity\": \"TOTAL\" }" }) ], - Response = "", - input_tests(Response, Tests). - -untag_resource_output_tests(_) -> - Tests = - [?_ddb_test( - {"ListTagsOfResource example response", "", - ok}) - ], - output_tests(?_f(erlcloud_ddb2:untag_resource(<<"arn:aws:dynamodb:us-east-1:111122223333:table/Forum">>, - [<<"example_key1">>, <<"example_key2">>])), - Tests). - -%% UpdateContinuousBackups test based on the API examples: -%% https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateContinuousBackups.html -update_continuous_backups_input_tests(_) -> - Tests = - [?_ddb_test( - {"UpdateContinuousBackups example request", - ?_f(erlcloud_ddb2:update_continuous_backups(<<"Thread">>,true)), " - { - \"PointInTimeRecoverySpecification\": { - \"PointInTimeRecoveryEnabled\": true - }, - \"TableName\": \"Thread\" - }" - }) - ], - Response = "{ - \"ContinuousBackupsDescription\": { - \"ContinuousBackupsStatus\": \"ENABLED\", - \"PointInTimeRecoveryDescription\": { - \"EarliestRestorableDateTime\": 1, - \"LatestRestorableDateTime\": 1, - \"PointInTimeRecoveryStatus\": \"DISABLED\" - } - } -}", - input_tests(Response, Tests). -update_continuous_backups_output_tests(_) -> - Tests = - [?_ddb_test( - {"UpdateContinuousBackups example response", "{ - \"ContinuousBackupsDescription\": { - \"ContinuousBackupsStatus\": \"ENABLED\", - \"PointInTimeRecoveryDescription\": { - \"EarliestRestorableDateTime\": 1, - \"LatestRestorableDateTime\": 1, - \"PointInTimeRecoveryStatus\": \"DISABLED\" - } - } + Response = " +{ + \"ConsumedCapacity\": [ + { + \"TableName\": \"PersonalInfo\", + \"CapacityUnits\": 2 + }, + { + \"TableName\": \"EmployeeRecord\", + \"CapacityUnits\": 3 + } + ], + \"Responses\": [ + {\"Item\": { + \"Name\":{ + \"S\":\"John Smith\" + }, + \"DOB\":{ + \"S\":\"11/11/2011\" + }, + \"Creation_ts\":{ + \"N\":\"1500000000\" + } + }}, + {\"Item\": { + \"Name\":{ + \"S\":\"John Smith\" + }, + \"DOH\":{ + \"S\":\"11/11/2018\" + }, + \"Id\":{ + \"N\":\"19\" + } + }} + ] }", - {ok,#ddb2_continuous_backups_description{ - continuous_backups_status = enabled, - point_in_time_recovery_description = - #ddb2_point_in_time_recovery_description{ - earliest_restorable_date_time = 1, - latest_restorable_date_time = 1, - point_in_time_recovery_status = disabled}}} - }) - ], - - output_tests(?_f(erlcloud_ddb2:update_continuous_backups(<<"Thread">>,true)), Tests). + input_tests(Response, Tests). -%% UpdateItem test based on the API examples: -%% http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html -update_item_input_tests(_) -> +transact_get_items_output_tests(_) -> Tests = [?_ddb_test( - {"UpdateItem example request", - ?_f(erlcloud_ddb2:update_item(<<"Thread">>, + {"TransactGetItems response only", " +{ + \"Responses\": [ + {\"Item\": { + \"Name\":{ + \"S\":\"John Smith\" + }, + \"DOB\":{ + \"S\":\"11/11/2011\" + }, + \"Creation_ts\":{ + \"N\":\"1500000000\" + } + }}, + {\"Item\": { + \"Name\":{ + \"S\":\"John Smith\" + }, + \"DOH\":{ + \"S\":\"11/11/2018\" + }, + \"Id\":{ + \"N\":\"19\" + } + }} + ] +}", + {ok, #ddb2_transact_get_items{ + responses = [ + #ddb2_item_response{ + item = [ + {<<"Name">>, <<"John Smith">>}, + {<<"DOB">>, <<"11/11/2011">>}, + {<<"Creation_ts">>, 1500000000} + ] + }, + #ddb2_item_response{ + item = [ + {<<"Name">>, <<"John Smith">>}, + {<<"DOH">>, <<"11/11/2018">>}, + {<<"Id">>, 19} + ] + } + ] + }}}), + ?_ddb_test( + {"TransactGetItems consumed capacity", " +{ + \"ConsumedCapacity\": [ + { + \"TableName\": \"PersonalInfo\", + \"CapacityUnits\": 2 + }, + { + \"TableName\": \"EmployeeRecord\", + \"CapacityUnits\": 3 + } + ], + \"Responses\": [ + {\"Item\": { + \"Name\":{ + \"S\":\"John Smith\" + }, + \"DOB\":{ + \"S\":\"11/11/2011\" + }, + \"Creation_ts\":{ + \"N\":\"1500000000\" + } + }}, + {\"Item\": { + \"Name\":{ + \"S\":\"John Smith\" + }, + \"DOH\":{ + \"S\":\"11/11/2018\" + }, + \"Id\":{ + \"N\":\"19\" + } + }} + ] +}", + {ok, #ddb2_transact_get_items{ + responses = [ + #ddb2_item_response{ + item = [ + {<<"Name">>, <<"John Smith">>}, + {<<"DOB">>, <<"11/11/2011">>}, + {<<"Creation_ts">>, 1500000000} + ] + }, + #ddb2_item_response{ + item = [ + {<<"Name">>, <<"John Smith">>}, + {<<"DOH">>, <<"11/11/2018">>}, + {<<"Id">>, 19} + ] + } + ], + consumed_capacity = [ + #ddb2_consumed_capacity{ + capacity_units = 2, table_name = <<"PersonalInfo">> + }, + #ddb2_consumed_capacity{ + capacity_units = 3, table_name = <<"EmployeeRecord">> + } + ] + }}}) + ], + + output_tests(?_f(erlcloud_ddb2:transact_get_items([], [{out, record}])), + Tests). + +%% TransactWriteItems test using synthetic data (there are no AWS provided examples): +%% https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactWriteItems.html +transact_write_items_input_tests(_) -> + Tests = + [?_ddb_test( + {"TransactWriteItems request", + ?_f(erlcloud_ddb2:transact_write_items( + [{update, { + <<"PersonalInfo">>, + [{<<"Name">>, {s, <<"John Smith">>}}, {<<"DOB">>, {s, <<"11/11/2011">>}}], + <<"SET eye_color = :c">>, + [{expression_attribute_values, [{<<":c">>, <<"brown">>}]}]}}, + {condition_check, { + <<"Scratchpad">>, + [{<<"Name">>, {s, <<"John Smith">>}}, {<<"DOB">>, {s, <<"11/11/2011">>}}], + [{condition_expression, <<"approved = :a">>}, + {expression_attribute_values, [{<<":a">>, <<"yes">>}]}]}}, + {delete, {<<"Scratchpad">>, [{<<"Name">>, {s, <<"John Smith">>}}, + {<<"DOB">>, {s, <<"11/11/2011">>}}]}}, + {put, {<<"EmployeeRecord">>, [{<<"Name">>, {s, <<"John Smith">>}}, + {<<"DOH">>, {s, <<"11/11/2018">>}}]}}], + [{return_consumed_capacity, total}, + {return_item_collection_metrics, size}])), " +{ + \"TransactItems\": [ + {\"Update\": { + \"Key\": { + \"Name\": { + \"S\": \"John Smith\" + }, + \"DOB\": { + \"S\": \"11/11/2011\" + } + }, + \"ExpressionAttributeValues\": { + \":c\": {\"S\": \"brown\"} + }, + \"UpdateExpression\": \"SET eye_color = :c\", + \"TableName\": \"PersonalInfo\" + }}, + {\"ConditionCheck\": { + \"Key\": { + \"Name\": { + \"S\": \"John Smith\" + }, + \"DOB\": { + \"S\": \"11/11/2011\" + } + }, + \"ExpressionAttributeValues\": { + \":a\": {\"S\": \"yes\"} + }, + \"ConditionExpression\": \"approved = :a\", + \"TableName\": \"Scratchpad\" + }}, + {\"Delete\": { + \"Key\": { + \"Name\": { + \"S\": \"John Smith\" + }, + \"DOB\": { + \"S\": \"11/11/2011\" + } + }, + \"TableName\": \"Scratchpad\" + }}, + {\"Put\": { + \"Item\": { + \"Name\": { + \"S\": \"John Smith\" + }, + \"DOH\": { + \"S\": \"11/11/2018\" + } + }, + \"TableName\": \"EmployeeRecord\" + }} + ], + \"ReturnConsumedCapacity\": \"TOTAL\", + \"ReturnItemCollectionMetrics\": \"SIZE\" +}" + }) + ], + + Response = " +{ + \"ItemCollectionMetrics\": { + \"PersonalInfo\": [{ + \"ItemCollectionKey\": { + \"Name\": { + \"S\": \"John Smith\" + } + }, + \"SizeEstimateRangeGB\": [1, 2] + }], + \"Scratchpad\": [{ + \"ItemCollectionKey\": { + \"Name\": { + \"S\": \"John Smith\" + } + }, + \"SizeEstimateRangeGB\": [0, 1] + }], + \"EmployeeRecord\": [{ + \"ItemCollectionKey\": { + \"Name\": { + \"S\": \"John Smith\" + } + }, + \"SizeEstimateRangeGB\": [2, 3] + }] + }, + \"ConsumedCapacity\": [ + { + \"TableName\": \"PersonalInfo\", + \"CapacityUnits\": 3 + }, + { + \"TableName\": \"Scratchpad\", + \"CapacityUnits\": 4 + }, + { + \"TableName\": \"EmployeeRecord\", + \"CapacityUnits\": 3 + } + ] +}", + input_tests(Response, Tests). + +transact_write_items_output_tests(_) -> + Tests = + [?_ddb_test( + {"TransactWriteItems consumed capacity", " +{ + \"ConsumedCapacity\": [ + { + \"TableName\": \"PersonalInfo\", + \"CapacityUnits\": 3 + }, + { + \"TableName\": \"Scratchpad\", + \"CapacityUnits\": 4 + }, + { + \"TableName\": \"EmployeeRecord\", + \"CapacityUnits\": 3 + } + ] +}", + {ok, #ddb2_transact_write_items{ + consumed_capacity = [ + #ddb2_consumed_capacity{ + capacity_units = 3, table_name = <<"PersonalInfo">> + }, + #ddb2_consumed_capacity{ + capacity_units = 4, table_name = <<"Scratchpad">> + }, + #ddb2_consumed_capacity{ + capacity_units = 3, table_name = <<"EmployeeRecord">> + } + ] + }}}), + ?_ddb_test( + {"TransactWriteItems item collection metrics", " +{ + \"ItemCollectionMetrics\": { + \"PersonalInfo\": [{ + \"ItemCollectionKey\": { + \"Name\": { + \"S\": \"John Smith\" + } + }, + \"SizeEstimateRangeGB\": [1, 2] + }], + \"Scratchpad\": [{ + \"ItemCollectionKey\": { + \"Name\": { + \"S\": \"John Smith\" + } + }, + \"SizeEstimateRangeGB\": [0, 1] + }], + \"EmployeeRecord\": [{ + \"ItemCollectionKey\": { + \"Name\": { + \"S\": \"John Smith\" + } + }, + \"SizeEstimateRangeGB\": [2, 3] + }] + } +}", + {ok, #ddb2_transact_write_items{ + item_collection_metrics = [ + {<<"PersonalInfo">>, + [#ddb2_item_collection_metrics + {item_collection_key = <<"John Smith">>, + size_estimate_range_gb = {1, 2}}]}, + {<<"Scratchpad">>, + [#ddb2_item_collection_metrics + {item_collection_key = <<"John Smith">>, + size_estimate_range_gb = {0, 1}}]}, + {<<"EmployeeRecord">>, + [#ddb2_item_collection_metrics + {item_collection_key = <<"John Smith">>, + size_estimate_range_gb = {2, 3}} + ]}]}}}) + ], + + output_tests(?_f(erlcloud_ddb2:transact_write_items([], [{out, record}])), + Tests). + +%% UntagResource test based on the API: +%% https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UntagResource.html +untag_resource_input_tests(_) -> + Tests = + [?_ddb_test( + {"ListTagsOfResource example request", + ?_f(erlcloud_ddb2:untag_resource(<<"arn:aws:dynamodb:us-east-1:111122223333:table/Forum">>, + [<<"example_key1">>, <<"example_key2">>])), " +{ + \"ResourceArn\": \"arn:aws:dynamodb:us-east-1:111122223333:table/Forum\", + \"TagKeys\": [ + \"example_key1\", + \"example_key2\" + ] +}" + }) + ], + Response = "", + input_tests(Response, Tests). + +untag_resource_output_tests(_) -> + Tests = + [?_ddb_test( + {"ListTagsOfResource example response", "", + ok}) + ], + output_tests(?_f(erlcloud_ddb2:untag_resource(<<"arn:aws:dynamodb:us-east-1:111122223333:table/Forum">>, + [<<"example_key1">>, <<"example_key2">>])), + Tests). + +%% UpdateContinuousBackups test based on the API examples: +%% https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateContinuousBackups.html +update_continuous_backups_input_tests(_) -> + Tests = + [?_ddb_test( + {"UpdateContinuousBackups example request", + ?_f(erlcloud_ddb2:update_continuous_backups(<<"Thread">>,true)), " + { + \"PointInTimeRecoverySpecification\": { + \"PointInTimeRecoveryEnabled\": true + }, + \"TableName\": \"Thread\" + }" + }) + ], + Response = "{ + \"ContinuousBackupsDescription\": { + \"ContinuousBackupsStatus\": \"ENABLED\", + \"PointInTimeRecoveryDescription\": { + \"EarliestRestorableDateTime\": 1, + \"LatestRestorableDateTime\": 1, + \"PointInTimeRecoveryStatus\": \"DISABLED\" + } + } +}", + input_tests(Response, Tests). + +update_continuous_backups_output_tests(_) -> + Tests = + [?_ddb_test( + {"UpdateContinuousBackups example response", "{ + \"ContinuousBackupsDescription\": { + \"ContinuousBackupsStatus\": \"ENABLED\", + \"PointInTimeRecoveryDescription\": { + \"EarliestRestorableDateTime\": 1, + \"LatestRestorableDateTime\": 1, + \"PointInTimeRecoveryStatus\": \"DISABLED\" + } + } +}", + {ok,#ddb2_continuous_backups_description{ + continuous_backups_status = enabled, + point_in_time_recovery_description = + #ddb2_point_in_time_recovery_description{ + earliest_restorable_date_time = 1, + latest_restorable_date_time = 1, + point_in_time_recovery_status = disabled}}} + }) + ], + + output_tests(?_f(erlcloud_ddb2:update_continuous_backups(<<"Thread">>,true)), Tests). + +%% UpdateItem test based on the API examples: +%% http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html +update_item_input_tests(_) -> + Tests = + [?_ddb_test( + {"UpdateItem example request", + ?_f(erlcloud_ddb2:update_item(<<"Thread">>, [{<<"ForumName">>, {s, <<"Amazon DynamoDB">>}}, {<<"Subject">>, {s, <<"How do I update multiple items?">>}}], [{<<"LastPostedBy">>, {s, <<"alice@example.com">>}, put}], @@ -4818,7 +6027,7 @@ update_item_input_tests(_) -> input_tests(Response, Tests). update_item_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"UpdateItem example response", " { @@ -4859,7 +6068,7 @@ update_item_output_tests(_) -> }", {ok, []}}) ], - + output_tests(?_f(erlcloud_ddb2:update_item(<<"table">>, {<<"k">>, <<"v">>}, [])), Tests). %% UpdateGlobalTable input test: @@ -4870,9 +6079,9 @@ update_global_table_input_tests(_) -> ?_f(erlcloud_ddb2:update_global_table(<<"Thread">>, [{create, {region_name, <<"us-east-1">>}}])), " { \"GlobalTableName\": \"Thread\", - \"ReplicaUpdates\": [ - { - \"Create\": { + \"ReplicaUpdates\": [ + { + \"Create\": { \"RegionName\": \"us-east-1\" } } @@ -4881,12 +6090,12 @@ update_global_table_input_tests(_) -> }), ?_ddb_test( {"UpdateGlobalTable example request (delete)", - ?_f(erlcloud_ddb2:update_global_table(<<"Thread">>, {delete, #ddb2_replica{region_name = <<"us-west-2">>}})), " + ?_f(erlcloud_ddb2:update_global_table(<<"Thread">>, [{delete, #ddb2_replica{region_name = <<"us-west-2">>}}])), " { \"GlobalTableName\": \"Thread\", - \"ReplicaUpdates\": [ - { - \"Delete\": { + \"ReplicaUpdates\": [ + { + \"Delete\": { \"RegionName\": \"us-west-2\" } } @@ -4899,13 +6108,13 @@ update_global_table_input_tests(_) -> {delete, {region_name, <<"eu-west-1">>}}])), " { \"GlobalTableName\": \"Thread\", - \"ReplicaUpdates\": [ - { - \"Create\": { + \"ReplicaUpdates\": [ + { + \"Create\": { \"RegionName\": \"us-east-1\" } - },{ - \"Delete\": { + },{ + \"Delete\": { \"RegionName\": \"eu-west-1\" } } @@ -4914,13 +6123,13 @@ update_global_table_input_tests(_) -> })], Response = " { - \"GlobalTableDescription\": { + \"GlobalTableDescription\": { \"CreationDateTime\": 1519161181.107, \"GlobalTableArn\": \"arn:aws:dynamodb::111122223333:global-table/Thread\", \"GlobalTableName\": \"Thread\", \"GlobalTableStatus\": \"UPDATING\", - \"ReplicationGroup\": [ - { + \"ReplicationGroup\": [ + { \"RegionName\": \"us-east-1\" } ] @@ -4930,17 +6139,17 @@ update_global_table_input_tests(_) -> %% UpdateGlobalTable output test: update_global_table_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"UpdateGlobalTable example response with UPDATING status", " { - \"GlobalTableDescription\": { + \"GlobalTableDescription\": { \"CreationDateTime\": 1519161181.107, \"GlobalTableArn\": \"arn:aws:dynamodb::111122223333:global-table/Thread\", \"GlobalTableName\": \"Thread\", \"GlobalTableStatus\": \"UPDATING\", - \"ReplicationGroup\": [ - { + \"ReplicationGroup\": [ + { \"RegionName\": \"us-east-1\" } ] @@ -4956,13 +6165,13 @@ update_global_table_output_tests(_) -> ?_ddb_test( {"UpdateGlobalTable example response with DELETING status", " { - \"GlobalTableDescription\": { + \"GlobalTableDescription\": { \"CreationDateTime\": 1519161181.107, \"GlobalTableArn\": \"arn:aws:dynamodb::111122223333:global-table/Thread\", \"GlobalTableName\": \"Thread\", \"GlobalTableStatus\": \"DELETING\", - \"ReplicationGroup\": [ - { + \"ReplicationGroup\": [ + { \"RegionName\": \"eu-west-1\" } ] @@ -4974,7 +6183,376 @@ update_global_table_output_tests(_) -> global_table_name = <<"Thread">>, global_table_status = deleting, replication_group = [#ddb2_replica_description{region_name = <<"eu-west-1">>}]}}})], - output_tests(?_f(erlcloud_ddb2:update_global_table(<<"Thread">>, {create, {region_name, <<"us-east-1">>}})), Tests). + output_tests(?_f(erlcloud_ddb2:update_global_table(<<"Thread">>, [{create, {region_name, <<"us-east-1">>}}])), Tests). + +update_global_table_settings_input_tests(_) -> + ReadUnits = 10, + WriteUnits = 10, + StaticProvisionOpts = [{global_table_billing_mode, provisioned}, + {global_table_global_secondary_index_settings_update, [[{index_name, <<"id-index">>}, + {provisioned_write_capacity_units, WriteUnits}]]}, + {global_table_provisioned_write_capacity_units, WriteUnits}, + {replica_settings_update, [[{region_name, <<"us-west-2">>}, + {replica_global_secondary_index_settings_update, [[{index_name, <<"id-index">>}, + {provisioned_read_capacity_units, ReadUnits}]]}, + {replica_provisioned_read_capacity_units, ReadUnits}]]}], + AutoScalingSettingsUpdate = [{maximum_units, 20}, + {minimum_units, 10}, + {scaling_policy_update, [{target_tracking_scaling_policy_configuration, [{disable_scale_in, false}, + {scale_in_cooldown, 600}, + {scale_out_cooldown, 600}, + {target_value, 70.0}]}]}], + AutoScalingProvisionOpts = [{global_table_billing_mode, provisioned}, + {global_table_global_secondary_index_settings_update, [[{index_name, <<"id-index">>}, + {provisioned_write_capacity_auto_scaling_settings_update, AutoScalingSettingsUpdate}]]}, + {global_table_provisioned_write_capacity_auto_scaling_settings_update, AutoScalingSettingsUpdate}, + {replica_settings_update, [[{region_name, <<"us-west-2">>}, + {replica_global_secondary_index_settings_update, [[{index_name, <<"id-index">>}, + {provisioned_read_capacity_auto_scaling_settings_update, AutoScalingSettingsUpdate}]]}, + {replica_provisioned_read_capacity_auto_scaling_settings_update, AutoScalingSettingsUpdate}]]}], + Tests = + [?_ddb_test( + {"UpdateGlobalTableSettings example request: update all read/write capacity to static values", + ?_f(erlcloud_ddb2:update_global_table_settings(<<"Thread">>, StaticProvisionOpts)), " +{ + \"GlobalTableBillingMode\": \"PROVISIONED\", + \"GlobalTableGlobalSecondaryIndexSettingsUpdate\": [ + { + \"IndexName\": \"id-index\", + \"ProvisionedWriteCapacityUnits\": 10 + } + ], + \"GlobalTableName\": \"Thread\", + \"GlobalTableProvisionedWriteCapacityUnits\": 10, + \"ReplicaSettingsUpdate\": [ + { + \"RegionName\": \"us-west-2\", + \"ReplicaGlobalSecondaryIndexSettingsUpdate\": [ + { + \"IndexName\": \"id-index\", + \"ProvisionedReadCapacityUnits\": 10 + } + ], + \"ReplicaProvisionedReadCapacityUnits\": 10 + } + ] +}" + }), + ?_ddb_test( + {"UpdateGlobalTableSettings example request: update all read/write capacity to autoscale", + ?_f(erlcloud_ddb2:update_global_table_settings(<<"Thread">>, AutoScalingProvisionOpts)), " +{ + \"GlobalTableBillingMode\": \"PROVISIONED\", + \"GlobalTableGlobalSecondaryIndexSettingsUpdate\": [ + { + \"IndexName\": \"id-index\", + \"ProvisionedWriteCapacityAutoScalingSettingsUpdate\": { + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicyUpdate\": { + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + } + } + ], + \"GlobalTableName\": \"Thread\", + \"GlobalTableProvisionedWriteCapacityAutoScalingSettingsUpdate\": { + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicyUpdate\": { + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + }, + \"ReplicaSettingsUpdate\": [ + { + \"RegionName\": \"us-west-2\", + \"ReplicaGlobalSecondaryIndexSettingsUpdate\": [ + { + \"IndexName\": \"id-index\", + \"ProvisionedReadCapacityAutoScalingSettingsUpdate\": { + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicyUpdate\": { + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + } + } + ], + \"ReplicaProvisionedReadCapacityAutoScalingSettingsUpdate\": { + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicyUpdate\": { + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + } + } + ] +}" + })], + Response = " +{ + \"GlobalTableName\": \"Thread\", + \"ReplicaSettings\": [ + { + \"RegionName\": \"us-west-2\", + \"ReplicaBillingModeSummary\": { + \"BillingMode\": \"PROVISIONED\", + \"LastUpdateToPayPerRequestDateTime\": 1578092745.455 + }, + \"ReplicaGlobalSecondaryIndexSettings\": [ + { + \"IndexName\": \"id-index\", + \"IndexStatus\": \"ACTIVE\", + \"ProvisionedReadCapacityAutoScalingSettings\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"arn:test\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicies\": [ + { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + ] + }, + \"ProvisionedReadCapacityUnits\": 10, + \"ProvisionedWriteCapacityAutoScalingSettings\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"string\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicies\": [ + { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + ] + }, + \"ProvisionedWriteCapacityUnits\": 10 + } + ], + \"ReplicaProvisionedReadCapacityAutoScalingSettings\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"string\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicies\": [ + { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + ] + }, + \"ReplicaProvisionedReadCapacityUnits\": 10, + \"ReplicaProvisionedWriteCapacityAutoScalingSettings\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"string\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicies\": [ + { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + ] + }, + \"ReplicaProvisionedWriteCapacityUnits\": 10, + \"ReplicaStatus\": \"ACTIVE\" + } + ] +}", + input_tests(Response, Tests). + +update_global_table_settings_output_tests(_) -> + AutoScalingSettings = #ddb2_auto_scaling_settings_description{auto_scaling_disabled = false, + auto_scaling_role_arn = <<"arn:test">>, + maximum_units = 20, + minimum_units = 10, + scaling_policies = [#ddb2_auto_scaling_policy_description{policy_name = <<"PolicyName">>, + target_tracking_scaling_policy_configuration = #ddb2_auto_scaling_target_tracking_scaling_policy_configuration_description{disable_scale_in = false, + scale_in_cooldown = 600, + scale_out_cooldown = 600, + target_value = 70.0}}]}, + Tests = + [?_ddb_test( + {"UpdateGlobalTableSettings example response: update all read/write capacity to static values", " +{ + \"GlobalTableName\": \"Thread\", + \"ReplicaSettings\": [ + { + \"RegionName\": \"us-west-2\", + \"ReplicaBillingModeSummary\": { + \"BillingMode\": \"PROVISIONED\", + \"LastUpdateToPayPerRequestDateTime\": 1578092745.455 + }, + \"ReplicaGlobalSecondaryIndexSettings\": [ + { + \"IndexName\": \"id-index\", + \"IndexStatus\": \"ACTIVE\", + \"ProvisionedReadCapacityUnits\": 10, + \"ProvisionedWriteCapacityUnits\": 10 + } + ], + \"ReplicaProvisionedReadCapacityUnits\": 10, + \"ReplicaProvisionedWriteCapacityUnits\": 10, + \"ReplicaStatus\": \"ACTIVE\" + } + ] +}", + {ok, [#ddb2_replica_settings_description{region_name = <<"us-west-2">>, + replica_billing_mode_summary = #ddb2_billing_mode_summary{billing_mode = provisioned, + last_update_to_pay_per_request_date_time = 1578092745.455}, + replica_global_secondary_index_settings = [#ddb2_replica_global_secondary_index_settings_description{index_name = <<"id-index">>, + index_status = active, + provisioned_read_capacity_units = 10, + provisioned_write_capacity_units = 10}], + replica_provisioned_read_capacity_units = 10, + replica_provisioned_write_capacity_units = 10, + replica_status = active}]}} + ), + ?_ddb_test( + {"UpdateGlobalTableSettings example response: update all read/write capacity to autoscale", " +{ + \"GlobalTableName\": \"Thread\", + \"ReplicaSettings\": [ + { + \"RegionName\": \"us-west-2\", + \"ReplicaBillingModeSummary\": { + \"BillingMode\": \"PROVISIONED\", + \"LastUpdateToPayPerRequestDateTime\": 1578092745.455 + }, + \"ReplicaGlobalSecondaryIndexSettings\": [ + { + \"IndexName\": \"id-index\", + \"IndexStatus\": \"ACTIVE\", + \"ProvisionedReadCapacityAutoScalingSettings\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"arn:test\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicies\": [ + { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + ] + }, + \"ProvisionedWriteCapacityAutoScalingSettings\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"arn:test\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicies\": [ + { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + ] + }, + } + ], + \"ReplicaProvisionedReadCapacityAutoScalingSettings\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"arn:test\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicies\": [ + { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + ] + }, + \"ReplicaProvisionedWriteCapacityAutoScalingSettings\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"arn:test\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicies\": [ + { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + ] + }, + \"ReplicaStatus\": \"ACTIVE\" + } + ] +} + +", + {ok, [#ddb2_replica_settings_description{region_name = <<"us-west-2">>, + replica_billing_mode_summary = #ddb2_billing_mode_summary{billing_mode = provisioned, + last_update_to_pay_per_request_date_time = 1578092745.455}, + replica_global_secondary_index_settings = [#ddb2_replica_global_secondary_index_settings_description{index_name = <<"id-index">>, + index_status = active, + provisioned_read_capacity_auto_scaling_settings = AutoScalingSettings, + provisioned_write_capacity_auto_scaling_settings = AutoScalingSettings}], + replica_provisioned_read_capacity_auto_scaling_settings = AutoScalingSettings, + replica_provisioned_write_capacity_auto_scaling_settings = AutoScalingSettings, + replica_status = active}]}} + )], + output_tests(?_f(erlcloud_ddb2:update_global_table_settings(<<"Thread">>, [])), Tests). %% UpdateTable test based on the API examples: %% http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateTable.html @@ -5160,12 +6738,62 @@ update_table_input_tests(_) -> } } ] +}" + }), + ?_ddb_test( + {"UpdateTable example request with Create GSI (pay per request)", + ?_f(erlcloud_ddb2:update_table(<<"Thread">>, + [{attribute_definitions, [{<<"HashKey1">>, s}]}, + {global_secondary_index_updates, [ + {<<"Index1">>, <<"HashKey1">>, all}]}])), " +{ + \"TableName\": \"Thread\", + \"AttributeDefinitions\": [ + { + \"AttributeName\": \"HashKey1\", + \"AttributeType\": \"S\" + } + ], + \"GlobalSecondaryIndexUpdates\": [ + { + \"Create\": { + \"IndexName\": \"Index1\", + \"KeySchema\": [ + { + \"AttributeName\": \"HashKey1\", + \"KeyType\": \"HASH\" + } + ], + \"Projection\": { + \"ProjectionType\": \"ALL\" + } + } + } + ] +}" + }), + ?_ddb_test( + {"UpdateTable example request billing_mode = pay_per_request", + ?_f(erlcloud_ddb2:update_table(<<"Thread">>, + [{billing_mode, pay_per_request}])), " +{ + \"TableName\": \"Thread\", + \"BillingMode\": \"PAY_PER_REQUEST\" +}" + }), + ?_ddb_test( + {"UpdateTable example request deletion_protection_enabled = false", + ?_f(erlcloud_ddb2:update_table(<<"Thread">>, + [{deletion_protection_enabled, false}])), " +{ + \"TableName\": \"Thread\", + \"DeletionProtectionEnabled\": false }" }) ], Response = " -{ +{ \"TableDescription\": { \"AttributeDefinitions\": [ { @@ -5227,10 +6855,10 @@ update_table_input_tests(_) -> input_tests(Response, Tests). update_table_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"UpdateTable example response", " -{ +{ \"TableDescription\": { \"AttributeDefinitions\": [ { @@ -5276,7 +6904,7 @@ update_table_output_tests(_) -> \"WriteCapacityUnits\": 4 } } - ], + ], \"CreationDateTime\": 1.363801528686E9, \"ItemCount\": 0, \"KeySchema\": [ @@ -5334,7 +6962,7 @@ update_table_output_tests(_) -> item_count = 0, key_schema = {<<"ForumName">>, <<"LastPostDateTime">>}, projection = keys_only}], - global_secondary_indexes = + global_secondary_indexes = [#ddb2_global_secondary_index_description{ index_name = <<"SubjectIndex">>, index_size_bytes = 2048, @@ -5348,8 +6976,8 @@ update_table_output_tests(_) -> number_of_decreases_today = 2, read_capacity_units = 3, write_capacity_units = 4} - }], - provisioned_throughput = + }], + provisioned_throughput = #ddb2_provisioned_throughput_description{ last_decrease_date_time = undefined, last_increase_date_time = 1363801701.282, @@ -5360,9 +6988,312 @@ update_table_output_tests(_) -> table_size_bytes = 0, table_status = updating}}}) ], - + output_tests(?_f(erlcloud_ddb2:update_table(<<"name">>, 5, 15)), Tests). +update_table_replica_auto_scaling_input_tests(_) -> + AutoScalingSettingsUpdate = [{auto_scaling_disabled, false}, + {auto_scaling_role_arn, <<"arn:test">>}, + {maximum_units, 20}, + {minimum_units, 10}, + {scaling_policy_update, [{policy_name, <<"PolicyName">>}, + {target_tracking_scaling_policy_configuration, [{disable_scale_in, false}, + {scale_in_cooldown, 600}, + {scale_out_cooldown, 600}, + {target_value, 70.0}]}]}], + Opts = [{global_secondary_index_updates, [[{index_name, <<"id-index">>}, + {provisioned_write_capacity_auto_scaling_update, AutoScalingSettingsUpdate}]]}, + {provisioned_write_capacity_auto_scaling_update, AutoScalingSettingsUpdate}, + {replica_updates, [[{region_name, <<"us-west-2">>}, + {replica_global_secondary_index_updates, [[{index_name, <<"id-index">>}, + {provisioned_read_capacity_auto_scaling_update, AutoScalingSettingsUpdate}]]}, + {replica_provisioned_read_capacity_auto_scaling_update, AutoScalingSettingsUpdate}]]}], + Tests = + [?_ddb_test({"UpdateTableReplicaAutoScaling example request", + ?_f(erlcloud_ddb2:update_table_replica_auto_scaling(<<"Thread">>, Opts)), + " +{ + \"GlobalSecondaryIndexUpdates\": [ + { + \"IndexName\": \"id-index\", + \"ProvisionedWriteCapacityAutoScalingUpdate\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"arn:test\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicyUpdate\": { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + } + } + ], + \"ProvisionedWriteCapacityAutoScalingUpdate\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"arn:test\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicyUpdate\": { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + }, + \"ReplicaUpdates\": [ + { + \"RegionName\": \"us-west-2\", + \"ReplicaGlobalSecondaryIndexUpdates\": [ + { + \"IndexName\": \"id-index\", + \"ProvisionedReadCapacityAutoScalingUpdate\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"arn:test\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicyUpdate\": { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + } + } + ], + \"ReplicaProvisionedReadCapacityAutoScalingUpdate\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"arn:test\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicyUpdate\": { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + } + } + ], + \"TableName\": \"Thread\" +} + +"})], + + Response = " +{ + \"TableAutoScalingDescription\": { + \"Replicas\": [ + { + \"GlobalSecondaryIndexes\": [ + { + \"IndexName\": \"id-index\", + \"IndexStatus\": \"ACTIVE\", + \"ProvisionedReadCapacityAutoScalingSettings\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"arn:test\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicies\": [ + { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + ] + }, + \"ProvisionedWriteCapacityAutoScalingSettings\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"arn:test\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicies\": [ + { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + ] + } + } + ], + \"RegionName\": \"us-west-2\", + \"ReplicaProvisionedReadCapacityAutoScalingSettings\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"arn:test\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicies\": [ + { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + ] + }, + \"ReplicaProvisionedWriteCapacityAutoScalingSettings\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"arn:test\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicies\": [ + { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + ] + }, + \"ReplicaStatus\": \"ACTIVE\" + } + ], + \"TableName\": \"Thread\", + \"TableStatus\": \"ACTIVE\" + } +}", + input_tests(Response, Tests). + +update_table_replica_auto_scaling_output_tests(_) -> + AutoScalingSettings = #ddb2_auto_scaling_settings_description{auto_scaling_disabled = false, + auto_scaling_role_arn = <<"arn:test">>, + maximum_units = 20, + minimum_units = 10, + scaling_policies = [#ddb2_auto_scaling_policy_description{policy_name = <<"PolicyName">>, + target_tracking_scaling_policy_configuration = #ddb2_auto_scaling_target_tracking_scaling_policy_configuration_description{disable_scale_in = false, + scale_in_cooldown = 600, + scale_out_cooldown = 600, + target_value = 70.0}}]}, + Tests = + [?_ddb_test( + {"UpdateTableReplicaAutoScaling example", " +{ + \"TableAutoScalingDescription\": { + \"Replicas\": [ + { + \"GlobalSecondaryIndexes\": [ + { + \"IndexName\": \"id-index\", + \"IndexStatus\": \"ACTIVE\", + \"ProvisionedReadCapacityAutoScalingSettings\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"arn:test\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicies\": [ + { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + ] + }, + \"ProvisionedWriteCapacityAutoScalingSettings\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"arn:test\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicies\": [ + { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + ] + } + } + ], + \"RegionName\": \"us-west-2\", + \"ReplicaProvisionedReadCapacityAutoScalingSettings\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"arn:test\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicies\": [ + { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + ] + }, + \"ReplicaProvisionedWriteCapacityAutoScalingSettings\": { + \"AutoScalingDisabled\": false, + \"AutoScalingRoleArn\": \"arn:test\", + \"MaximumUnits\": 20, + \"MinimumUnits\": 10, + \"ScalingPolicies\": [ + { + \"PolicyName\": \"PolicyName\", + \"TargetTrackingScalingPolicyConfiguration\": { + \"DisableScaleIn\": false, + \"ScaleInCooldown\": 600, + \"ScaleOutCooldown\": 600, + \"TargetValue\": 70.0 + } + } + ] + }, + \"ReplicaStatus\": \"ACTIVE\" + } + ], + \"TableName\": \"Thread\", + \"TableStatus\": \"ACTIVE\" + } +}", + {ok, #ddb2_table_auto_scaling_description{replicas = [#ddb2_replica_auto_scaling_description{global_secondary_indexes = [#ddb2_replica_global_secondary_index_auto_scaling_description{index_name = <<"id-index">>, + index_status = active, + provisioned_read_capacity_auto_scaling_settings = AutoScalingSettings, + provisioned_write_capacity_auto_scaling_settings = AutoScalingSettings}], + region_name = <<"us-west-2">>, + replica_provisioned_read_capacity_auto_scaling_settings = AutoScalingSettings, + replica_provisioned_write_capacity_auto_scaling_settings = AutoScalingSettings, + replica_status = active}], + table_name = <<"Thread">>, + table_status = active}} +})], + output_tests(?_f(erlcloud_ddb2:update_table_replica_auto_scaling(<<"Thread">>, [])), Tests). + %% UpdateTimeToLive test: update_time_to_live_input_tests(_) -> Tests = @@ -5386,10 +7317,10 @@ update_time_to_live_input_tests(_) -> \"AttributeName\": \"ExpirationTime\", \"Enabled\": false } -}" +}" })], Response = " -{ +{ \"TimeToLiveSpecification\": { \"AttributeName\": \"ExpirationTime\", \"Enabled\": true @@ -5399,7 +7330,7 @@ update_time_to_live_input_tests(_) -> %% UpdateTimeToLive test: update_time_to_live_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"UpdateTimeToLive example response", " { @@ -5411,5 +7342,5 @@ update_time_to_live_output_tests(_) -> {ok, #ddb2_time_to_live_specification{ attribute_name = <<"ExpirationTime">>, enabled = true}}})], - output_tests(?_f(erlcloud_ddb2:update_time_to_live(<<"SessionData">>, + output_tests(?_f(erlcloud_ddb2:update_time_to_live(<<"SessionData">>, [{attribute_name, <<"ExpirationTime">>}, {enabled, true}])), Tests). diff --git a/test/erlcloud_ddb_streams_tests.erl b/test/erlcloud_ddb_streams_tests.erl index e94a78789..735eade87 100644 --- a/test/erlcloud_ddb_streams_tests.erl +++ b/test/erlcloud_ddb_streams_tests.erl @@ -27,7 +27,8 @@ operation_test_() -> fun get_shard_iterator_input_tests/1, fun get_shard_iterator_output_tests/1, fun list_streams_input_tests/1, - fun list_streams_output_tests/1 + fun list_streams_output_tests/1, + fun undynamize_ddb_streams_record_output_tests/1 ]}. start() -> @@ -56,8 +57,8 @@ sort_json(V) -> %% verifies that the parameters in the body match the expected parameters -spec validate_body(binary(), expected_body()) -> ok. validate_body(Body, Expected) -> - Want = sort_json(jsx:decode(list_to_binary(Expected))), - Actual = sort_json(jsx:decode(Body)), + Want = sort_json(jsx:decode(list_to_binary(Expected), [{return_maps, false}])), + Actual = sort_json(jsx:decode(Body, [{return_maps, false}])), case Want =:= Actual of true -> ok; false -> @@ -369,6 +370,7 @@ get_records_output_tests(_) -> { \"awsRegion\": \"us-west-2\", \"dynamodb\": { + \"ApproximateCreationDateTime\": 1551727994, \"Keys\": { \"ForumName\": {\"S\": \"DynamoDB\"}, \"Subject\": {\"S\": \"DynamoDB Thread 3\"} @@ -385,6 +387,7 @@ get_records_output_tests(_) -> { \"awsRegion\": \"us-west-2\", \"dynamodb\": { + \"ApproximateCreationDateTime\": 1551727994, \"Keys\": { \"ForumName\": {\"S\": \"DynamoDB\"}, \"Subject\": {\"S\": \"DynamoDB Thread 1\"} @@ -401,6 +404,7 @@ get_records_output_tests(_) -> { \"awsRegion\": \"us-west-2\", \"dynamodb\": { + \"ApproximateCreationDateTime\": 1551727994, \"Keys\": { \"ForumName\": {\"S\": \"DynamoDB\"}, \"Subject\": {\"S\": \"DynamoDB Thread 2\"} @@ -419,6 +423,7 @@ get_records_output_tests(_) -> {ok, [#ddb_streams_record{ aws_region = <<"us-west-2">>, dynamodb = #ddb_streams_stream_record{ + approximate_creation_date_time = 1551727994, keys = [{<<"ForumName">>, <<"DynamoDB">>}, {<<"Subject">>, <<"DynamoDB Thread 3">>}], new_image = undefined, @@ -433,6 +438,7 @@ get_records_output_tests(_) -> #ddb_streams_record{ aws_region = <<"us-west-2">>, dynamodb = #ddb_streams_stream_record{ + approximate_creation_date_time = 1551727994, keys = [{<<"ForumName">>, <<"DynamoDB">>}, {<<"Subject">>, <<"DynamoDB Thread 1">>}], new_image = undefined, @@ -447,6 +453,7 @@ get_records_output_tests(_) -> #ddb_streams_record{ aws_region = <<"us-west-2">>, dynamodb = #ddb_streams_stream_record{ + approximate_creation_date_time = 1551727994, keys = [{<<"ForumName">>, <<"DynamoDB">>}, {<<"Subject">>, <<"DynamoDB Thread 2">>}], new_image = undefined, @@ -581,3 +588,48 @@ list_streams_output_tests(_) -> table_name = <<"Forum">>}]}}) ], output_tests(?_f(erlcloud_ddb_streams:list_streams()), Tests). + + +undynamize_ddb_streams_record_output_tests(_) -> + DecodedResponse = jsx:decode( + <<" + { + \"awsRegion\": \"us-west-2\", + \"dynamodb\": { + \"ApproximateCreationDateTime\": 1551727994, + \"Keys\": { + \"ForumName\": {\"S\": \"DynamoDB\"}, + \"Subject\": {\"S\": \"DynamoDB Thread 3\"} + }, + \"SequenceNumber\": \"300000000000000499659\", + \"SizeBytes\": 41, + \"StreamViewType\": \"KEYS_ONLY\" + }, + \"eventID\": \"e2fd9c34eff2d779b297b26f5fef4206\", + \"eventName\": \"INSERT\", + \"eventSource\": \"aws:dynamodb\", + \"eventVersion\": \"1.0\" + }">>, [{return_maps, false}]), + DDBStreamsStreamRecord = #ddb_streams_stream_record{approximate_creation_date_time = 1551727994, + keys = [{<<"ForumName">>, <<"DynamoDB">>}, + {<<"Subject">>, <<"DynamoDB Thread 3">>}], + new_image = undefined, + old_image = undefined, + sequence_number = <<"300000000000000499659">>, + size_bytes = 41, + stream_view_type = keys_only}, + DDBStreamsStreamTypedRecord = DDBStreamsStreamRecord#ddb_streams_stream_record{keys = [{<<"ForumName">>, {s, <<"DynamoDB">>}}, + {<<"Subject">>, {s, <<"DynamoDB Thread 3">>}}]}, + + DDBStreamsRecord = #ddb_streams_record{aws_region = <<"us-west-2">>, + dynamodb = DDBStreamsStreamRecord, + event_id = <<"e2fd9c34eff2d779b297b26f5fef4206">>, + event_name = insert, + event_source = <<"aws:dynamodb">>, + event_version = <<"1.0">>}, + DDBStreamsTypedRecord = DDBStreamsRecord#ddb_streams_record{dynamodb = DDBStreamsStreamTypedRecord}, + + [?_assertEqual({ok, DDBStreamsStreamRecord}, erlcloud_ddb_streams:undynamize_ddb_streams_record(DecodedResponse)), + ?_assertEqual({ok, DDBStreamsStreamRecord}, erlcloud_ddb_streams:undynamize_ddb_streams_record(DecodedResponse, [{out, simple}])), + ?_assertEqual({ok, DDBStreamsRecord}, erlcloud_ddb_streams:undynamize_ddb_streams_record(DecodedResponse, [{out, record}])), + ?_assertEqual({ok, DDBStreamsTypedRecord}, erlcloud_ddb_streams:undynamize_ddb_streams_record(DecodedResponse, [{out, typed_record}]))]. diff --git a/test/erlcloud_ddb_tests.erl b/test/erlcloud_ddb_tests.erl index 15fd70a5a..b607c0fcc 100644 --- a/test/erlcloud_ddb_tests.erl +++ b/test/erlcloud_ddb_tests.erl @@ -19,7 +19,7 @@ -define(_f(F), fun() -> F end). -export([validate_body/2]). - + %%%=================================================================== %%% Test entry points %%%=================================================================== @@ -73,8 +73,8 @@ stop(_) -> %% verifies that the parameters in the body match the expected parameters -spec validate_body(binary(), expected_body()) -> ok. validate_body(Body, Expected) -> - Want = jsx:decode(list_to_binary(Expected)), - Actual = jsx:decode(Body), + Want = jsx:decode(list_to_binary(Expected), [{return_maps, false}]), + Actual = jsx:decode(Body, [{return_maps, false}]), case Want =:= Actual of true -> ok; false -> @@ -86,9 +86,9 @@ validate_body(Body, Expected) -> %% Validates the request body and responds with the provided response. -spec input_expect(string(), expected_body()) -> fun(). input_expect(Response, Expected) -> - fun(_Url, post, _Headers, Body, _Timeout, _Config) -> + fun(_Url, post, _Headers, Body, _Timeout, _Config) -> validate_body(Body, Expected), - {ok, {{200, "OK"}, [], list_to_binary(Response)}} + {ok, {{200, "OK"}, [], list_to_binary(Response)}} end. %% input_test converts an input_test specifier into an eunit test generator @@ -96,7 +96,7 @@ input_expect(Response, Expected) -> -spec input_test(string(), input_test_spec()) -> tuple(). input_test(Response, {Line, {Description, Fun, Expected}}) when is_list(Description) -> - {Description, + {Description, {Line, fun() -> meck:expect(erlcloud_httpc, request, input_expect(Response, Expected)), @@ -118,8 +118,8 @@ input_tests(Response, Tests) -> %% returns the mock of the erlcloud_httpc function output tests expect to be called. -spec output_expect(string()) -> fun(). output_expect(Response) -> - fun(_Url, post, _Headers, _Body, _Timeout, _Config) -> - {ok, {{200, "OK"}, [], list_to_binary(Response)}} + fun(_Url, post, _Headers, _Body, _Timeout, _Config) -> + {ok, {{200, "OK"}, [], list_to_binary(Response)}} end. %% output_test converts an output_test specifier into an eunit test generator @@ -141,9 +141,9 @@ output_test(Fun, {Line, {Description, Response, Result}}) -> end}}. %% output_test(Fun, {Line, {Response, Result}}) -> %% output_test(Fun, {Line, {"", Response, Result}}). - + %% output_tests converts a list of output_test specifiers into an eunit test generator --spec output_tests(fun(), [output_test_spec()]) -> [term()]. +-spec output_tests(fun(), [output_test_spec()]) -> [term()]. output_tests(Fun, Tests) -> [output_test(Fun, Test) || Test <- Tests]. @@ -155,7 +155,7 @@ output_tests(Fun, Tests) -> -spec httpc_response(pos_integer(), string()) -> tuple(). httpc_response(Code, Body) -> {ok, {{Code, ""}, [], list_to_binary(Body)}}. - + -type error_test_spec() :: {pos_integer(), {string(), list(), term()}}. -spec error_test(fun(), error_test_spec()) -> tuple(). error_test(Fun, {Line, {Description, Responses, Result}}) -> @@ -169,7 +169,7 @@ error_test(Fun, {Line, {Description, Responses, Result}}) -> Actual = Fun(), ?assertEqual(Result, Actual) end}}. - + -spec error_tests(fun(), [error_test_spec()]) -> [term()]. error_tests(Fun, Tests) -> [error_test(Fun, Test) || Test <- Tests]. @@ -180,9 +180,13 @@ error_tests(Fun, Tests) -> input_exception_test_() -> [?_assertError({erlcloud_ddb, {invalid_attr_value, {n, "string"}}}, - erlcloud_ddb:get_item(<<"Table">>, {n, "string"})), - %% This test causes an expected dialyzer error - ?_assertError({erlcloud_ddb, {invalid_item, <<"Attr">>}}, + erlcloud_ddb:get_item(<<"Table">>, {n, "string"}))] + ++ input_exception_failures_test_(). + +-dialyzer({nowarn_function, input_exception_failures_test_/0}). +input_exception_failures_test_() -> + %% This test causes an expected dialyzer error + [?_assertError({erlcloud_ddb, {invalid_item, <<"Attr">>}}, erlcloud_ddb:put_item(<<"Table">>, <<"Attr">>)), ?_assertError({erlcloud_ddb, {invalid_opt, {myopt, myval}}}, erlcloud_ddb:list_tables([{myopt, myval}])) @@ -197,12 +201,12 @@ error_handling_tests(_) -> \"status\":{\"S\":\"online\"} }, \"ConsumedCapacityUnits\": 1 -}" +}" ), OkResult = {ok, [{<<"friends">>, [<<"Lynda">>, <<"Aaron">>]}, {<<"status">>, <<"online">>}]}, - Tests = + Tests = [?_ddb_test( {"Test retry after ProvisionedThroughputExceededException", [httpc_response(400, " @@ -224,7 +228,7 @@ error_handling_tests(_) -> OkResponse], OkResult}) ], - + error_tests(?_f(erlcloud_ddb:get_item(<<"table">>, <<"key">>)), Tests). @@ -235,8 +239,8 @@ batch_get_item_input_tests(_) -> [?_ddb_test( {"BatchGetItem example request", ?_f(erlcloud_ddb:batch_get_item( - [{<<"comp2">>, [<<"Julie">>, - <<"Mingus">>], + [{<<"comp2">>, [<<"Julie">>, + <<"Mingus">>], [{attributes_to_get, [<<"user">>, <<"friends">>]}]}, {<<"comp1">>, [{<<"Casey">>, 1319509152}, {<<"Dave">>, 1319509155}, @@ -277,7 +281,7 @@ batch_get_item_input_tests(_) -> input_tests(Response, Tests). batch_get_item_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"BatchGetItem example response", " {\"Responses\": @@ -296,7 +300,7 @@ batch_get_item_output_tests(_) -> \"UnprocessedKeys\":{} }", {ok, #ddb_batch_get_item - {responses = + {responses = [#ddb_batch_get_item_response {table = <<"comp1">>, items = [[{<<"status">>, <<"online">>}, @@ -331,17 +335,17 @@ batch_get_item_output_tests(_) -> } }", {ok, #ddb_batch_get_item - {responses = [], - unprocessed_keys = - [{<<"comp2">>, [{s, <<"Julie">>}, - {s, <<"Mingus">>}], + {responses = [], + unprocessed_keys = + [{<<"comp2">>, [{s, <<"Julie">>}, + {s, <<"Mingus">>}], [{attributes_to_get, [<<"user">>, <<"friends">>]}]}, {<<"comp1">>, [{{s, <<"Casey">>}, {n, 1319509152}}, {{s, <<"Dave">>}, {n, 1319509155}}, {{s, <<"Riley">>}, {n, 1319509158}}], [{attributes_to_get, [<<"user">>, <<"status">>]}]}]}}}) ], - + output_tests(?_f(erlcloud_ddb:batch_get_item([{<<"table">>, [<<"key">>]}], [{out, record}])), Tests). %% BatchWriteItem test based on the API examples: @@ -434,7 +438,7 @@ batch_write_item_input_tests(_) -> input_tests(Response, Tests). batch_write_item_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"BatchWriteItem example response", " { @@ -464,7 +468,7 @@ batch_write_item_output_tests(_) -> } }", {ok, #ddb_batch_write_item - {responses = + {responses = [#ddb_batch_write_item_response {table = <<"Thread">>, consumed_capacity_units = 1.0}, @@ -530,7 +534,7 @@ batch_write_item_output_tests(_) -> {<<"Thread">>, [{put, [{<<"ForumName">>, {s, <<"Amazon DynamoDB">>}}, {<<"Subject">>, {s, <<"DynamoDB Thread 5">>}}]}]}]}}}) ], - + output_tests(?_f(erlcloud_ddb:batch_write_item([], [{out, record}])), Tests). %% CreateTable test based on the API examples: @@ -563,7 +567,7 @@ create_table_input_tests(_) -> input_tests(Response, Tests). create_table_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"CreateTable example response", " {\"TableDescription\": @@ -587,7 +591,7 @@ create_table_output_tests(_) -> table_name = <<"comp-table">>, table_status = <<"CREATING">>}}}) ], - + output_tests(?_f(erlcloud_ddb:create_table(<<"name">>, {<<"key">>, s}, 5, 10)), Tests). %% DeleteItem test based on the API examples: @@ -621,7 +625,7 @@ delete_item_input_tests(_) -> input_tests(Response, Tests). delete_item_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"DeleteItem example response", " {\"Attributes\": @@ -637,7 +641,7 @@ delete_item_output_tests(_) -> {<<"time">>, 200}, {<<"user">>, <<"Mingus">>}]}}) ], - + output_tests(?_f(erlcloud_ddb:delete_item(<<"table">>, <<"key">>)), Tests). %% DeleteTable test based on the API examples: @@ -646,7 +650,7 @@ delete_table_input_tests(_) -> Tests = [?_ddb_test( {"DeleteTable example request", - ?_f(erlcloud_ddb:delete_table(<<"Table1">>)), + ?_f(erlcloud_ddb:delete_table(<<"Table1">>)), "{\"TableName\":\"Table1\"}" }) ], @@ -665,7 +669,7 @@ delete_table_input_tests(_) -> input_tests(Response, Tests). delete_table_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"DeleteTable example response", " {\"TableDescription\": @@ -689,7 +693,7 @@ delete_table_output_tests(_) -> table_name = <<"Table1">>, table_status = <<"DELETING">>}}}) ], - + output_tests(?_f(erlcloud_ddb:delete_table(<<"name">>)), Tests). %% DescribeTable test based on the API examples: @@ -698,7 +702,7 @@ describe_table_input_tests(_) -> Tests = [?_ddb_test( {"DescribeTable example request", - ?_f(erlcloud_ddb:describe_table(<<"Table1">>)), + ?_f(erlcloud_ddb:describe_table(<<"Table1">>)), "{\"TableName\":\"Table1\"}" }) ], @@ -719,7 +723,7 @@ describe_table_input_tests(_) -> input_tests(Response, Tests). describe_table_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"DescribeTable example response", " {\"Table\": @@ -747,7 +751,7 @@ describe_table_output_tests(_) -> table_size_bytes = 949, table_status = <<"ACTIVE">>}}}) ], - + output_tests(?_f(erlcloud_ddb:describe_table(<<"name">>)), Tests). %% GetItem test based on the API examples: @@ -765,14 +769,14 @@ get_item_input_tests(_) -> Tests = [?_ddb_test( {"GetItem example request, with fully specified keys", - ?_f(erlcloud_ddb:get_item(<<"comptable">>, {{s, <<"Julie">>}, {n, 1307654345}}, + ?_f(erlcloud_ddb:get_item(<<"comptable">>, {{s, <<"Julie">>}, {n, 1307654345}}, [consistent_read, {attributes_to_get, [<<"status">>, <<"friends">>]}])), Example1Response}), ?_ddb_test( {"GetItem example request, with inferred key types", - ?_f(erlcloud_ddb:get_item(<<"comptable">>, {"Julie", 1307654345}, - [consistent_read, + ?_f(erlcloud_ddb:get_item(<<"comptable">>, {"Julie", 1307654345}, + [consistent_read, {attributes_to_get, [<<"status">>, <<"friends">>]}])), Example1Response}), ?_ddb_test( @@ -795,7 +799,7 @@ get_item_input_tests(_) -> input_tests(Response, Tests). get_item_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"GetItem example response", " {\"Item\": @@ -831,15 +835,15 @@ get_item_output_tests(_) -> {<<"b">>, <<5,182>>}, {<<"empty">>, <<>>}]}}), ?_ddb_test( - {"GetItem item not found", + {"GetItem item not found", "{\"ConsumedCapacityUnits\": 0.5}", {ok, []}}), ?_ddb_test( - {"GetItem no attributes returned", + {"GetItem no attributes returned", "{\"ConsumedCapacityUnits\":0.5,\"Item\":{}}", {ok, []}}) ], - + output_tests(?_f(erlcloud_ddb:get_item(<<"table">>, <<"key">>)), Tests). %% ListTables test based on the API examples: @@ -848,12 +852,12 @@ list_tables_input_tests(_) -> Tests = [?_ddb_test( {"ListTables example request", - ?_f(erlcloud_ddb:list_tables([{limit, 3}, {exclusive_start_table_name, <<"comp2">>}])), + ?_f(erlcloud_ddb:list_tables([{limit, 3}, {exclusive_start_table_name, <<"comp2">>}])), "{\"ExclusiveStartTableName\":\"comp2\",\"Limit\":3}" }), ?_ddb_test( {"ListTables empty request", - ?_f(erlcloud_ddb:list_tables()), + ?_f(erlcloud_ddb:list_tables()), "{}" }) @@ -863,7 +867,7 @@ list_tables_input_tests(_) -> input_tests(Response, Tests). list_tables_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"ListTables example response", "{\"LastEvaluatedTableName\":\"comp5\",\"TableNames\":[\"comp3\",\"comp4\",\"comp5\"]}", @@ -871,7 +875,7 @@ list_tables_output_tests(_) -> {last_evaluated_table_name = <<"comp5">>, table_names = [<<"comp3">>, <<"comp4">>, <<"comp5">>]}}}) ], - + output_tests(?_f(erlcloud_ddb:list_tables([{out, record}])), Tests). %% PutItem test based on the API examples: @@ -880,8 +884,8 @@ put_item_input_tests(_) -> Tests = [?_ddb_test( {"PutItem example request", - ?_f(erlcloud_ddb:put_item(<<"comp5">>, - [{<<"time">>, 300}, + ?_f(erlcloud_ddb:put_item(<<"comp5">>, + [{<<"time">>, 300}, {<<"feeling">>, <<"not surprised">>}, {<<"user">>, <<"Riley">>}], [{return_values, all_old}, @@ -899,8 +903,8 @@ put_item_input_tests(_) -> }), ?_ddb_test( {"PutItem float inputs", - ?_f(erlcloud_ddb:put_item(<<"comp5">>, - [{<<"time">>, 300}, + ?_f(erlcloud_ddb:put_item(<<"comp5">>, + [{<<"time">>, 300}, {<<"typed float">>, {n, 1.2}}, {<<"untyped float">>, 3.456}, {<<"mixed set">>, {ns, [7.8, 9.0, 10]}}], @@ -926,7 +930,7 @@ put_item_input_tests(_) -> input_tests(Response, Tests). put_item_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"PutItem example response", " {\"Attributes\": @@ -939,7 +943,7 @@ put_item_output_tests(_) -> {<<"time">>, 300}, {<<"user">>, <<"Riley">>}]}}) ], - + output_tests(?_f(erlcloud_ddb:put_item(<<"table">>, [])), Tests). %% Query test based on the API examples: @@ -998,7 +1002,7 @@ q_input_tests(_) -> input_tests(Response, Tests). q_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"Query example 1 response", " {\"Count\":2,\"Items\":[{ @@ -1044,7 +1048,7 @@ q_output_tests(_) -> last_evaluated_key = undefined, consumed_capacity_units = 1}}}) ], - + output_tests(?_f(erlcloud_ddb:q(<<"table">>, <<"key">>, [{out, record}])), Tests). %% Scan test based on the API examples: @@ -1053,7 +1057,7 @@ scan_input_tests(_) -> Tests = [?_ddb_test( {"Scan example 1 request", - ?_f(erlcloud_ddb:scan(<<"1-hash-rangetable">>)), + ?_f(erlcloud_ddb:scan(<<"1-hash-rangetable">>)), "{\"TableName\":\"1-hash-rangetable\"}" }), ?_ddb_test( @@ -1100,7 +1104,7 @@ scan_input_tests(_) -> input_tests(Response, Tests). scan_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"Scan example 1 response", " {\"Count\":4,\"Items\":[{ @@ -1198,7 +1202,7 @@ scan_output_tests(_) -> scanned_count = 2, consumed_capacity_units = 0.5}}}) ], - + output_tests(?_f(erlcloud_ddb:scan(<<"name">>, [{out, record}])), Tests). %% UpdateItem test based on the API examples: @@ -1254,7 +1258,7 @@ update_item_input_tests(_) -> input_tests(Response, Tests). update_item_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"UpdateItem example response", " {\"Attributes\": @@ -1269,7 +1273,7 @@ update_item_output_tests(_) -> {<<"time">>, 1307654350}, {<<"user">>, <<"Julie">>}]}}) ], - + output_tests(?_f(erlcloud_ddb:update_item(<<"table">>, <<"key">>, [])), Tests). %% UpdateTable test based on the API examples: @@ -1302,7 +1306,7 @@ update_table_input_tests(_) -> input_tests(Response, Tests). update_table_output_tests(_) -> - Tests = + Tests = [?_ddb_test( {"UpdateTable example response", " {\"TableDescription\": @@ -1329,6 +1333,5 @@ update_table_output_tests(_) -> table_name = <<"comp1">>, table_status = <<"UPDATING">>}}}) ], - - output_tests(?_f(erlcloud_ddb:update_table(<<"name">>, 5, 15)), Tests). + output_tests(?_f(erlcloud_ddb:update_table(<<"name">>, 5, 15)), Tests). diff --git a/test/erlcloud_ddb_util_tests.erl b/test/erlcloud_ddb_util_tests.erl index 250737843..7fb7dc875 100644 --- a/test/erlcloud_ddb_util_tests.erl +++ b/test/erlcloud_ddb_util_tests.erl @@ -30,6 +30,7 @@ operation_test_() -> [fun delete_all_tests/1, fun delete_hash_key_tests/1, fun get_all_tests/1, + fun list_tables_all_tests/1, fun put_all_tests/1, fun q_all_tests/1, fun q_all_attributes/0, @@ -37,6 +38,7 @@ operation_test_() -> fun scan_all_tests/1, fun scan_all_attributes/0, fun scan_all_count/0, + fun wait_for_table_active_tests/1, fun write_all_tests/1 ]}. @@ -442,6 +444,26 @@ get_all_tests(_) -> ], multi_call_tests(Tests). +list_tables_all_tests(_) -> + Tests = + [?_ddb_test( + {"list_tables_all return 3 tablenames", + ?_f(erlcloud_ddb_util:list_tables_all()), + [{"{}", + "{\"TableNames\":[\"tab1\",\"tab2\",\"tab3\"]}"}], + {ok, [<<"tab1">>,<<"tab2">>,<<"tab3">>]}}), + + ?_ddb_test( + {"list_tables_all tow batches, keep order", + ?_f(erlcloud_ddb_util:list_tables_all()), + [{"{}", + "{\"LastEvaluatedTableName\": \"tab3\", \"TableNames\":[\"tab1\",\"tab2\",\"tab3\"]}"}, + {"{\"ExclusiveStartTableName\":\"tab3\"}", + "{\"TableNames\":[\"tab5\",\"tab4\"]}"}], + {ok, [<<"tab1">>,<<"tab2">>,<<"tab3">>,<<"tab5">>,<<"tab4">>]}}) + ], + multi_call_tests(Tests). + put_all_tests(_) -> Tests = [?_ddb_test( @@ -630,11 +652,11 @@ q_all_tests(_) -> multi_call_tests(Tests). q_all_attributes() -> - Item1 = <<"item_1">>, - Item2 = <<"item_2">>, + Item1 = [{<<"key">>, <<"item_1">>}], + Item2 = [{<<"key">>, <<"item_2">>}], meck:new(EDDB = erlcloud_ddb2), meck:sequence(EDDB, q, 4, [ - {ok, #ddb2_q{last_evaluated_key = <<"key">>, + {ok, #ddb2_q{last_evaluated_key = {<<"key">>, <<"last1">>}, items = [Item1]}}, {ok, #ddb2_q{last_evaluated_key = undefined, items = [Item2]}} @@ -646,7 +668,7 @@ q_all_attributes() -> q_all_count() -> meck:new(EDDB = erlcloud_ddb2), meck:sequence(EDDB, q, 4, [ - {ok, #ddb2_q{last_evaluated_key = <<"key">>, + {ok, #ddb2_q{last_evaluated_key = {<<"key">>, <<"last1">>}, items = undefined, count = 2}}, {ok, #ddb2_q{last_evaluated_key = undefined, @@ -777,11 +799,11 @@ scan_all_tests(_) -> multi_call_tests(Tests). scan_all_attributes() -> - Item1 = <<"item_1">>, - Item2 = <<"item_2">>, + Item1 = [{<<"key">>, <<"item_1">>}], + Item2 = [{<<"key">>, <<"item_2">>}], meck:new(EDDB = erlcloud_ddb2), meck:sequence(EDDB, scan, 3, [ - {ok, #ddb2_scan{last_evaluated_key = <<"key">>, + {ok, #ddb2_scan{last_evaluated_key = {<<"key">>, <<"last1">>}, items = [Item1]}}, {ok, #ddb2_scan{last_evaluated_key = undefined, items = [Item2]}} @@ -793,7 +815,7 @@ scan_all_attributes() -> scan_all_count() -> meck:new(EDDB = erlcloud_ddb2), meck:sequence(EDDB, scan, 3, [ - {ok, #ddb2_scan{last_evaluated_key = <<"key">>, + {ok, #ddb2_scan{last_evaluated_key = {<<"key">>, <<"last1">>}, items = undefined, count = 2}}, {ok, #ddb2_scan{last_evaluated_key = undefined, @@ -804,6 +826,582 @@ scan_all_count() -> erlcloud_ddb_util:scan_all(<<"tbl">>, [])), meck:unload(EDDB). +wait_for_table_active_tests(_) -> + Tests = + [?_ddb_test( + {"wait_for_table_active table is active", + ?_f(erlcloud_ddb_util:wait_for_table_active(<<"Thread">>)), + [{"{\"TableName\":\"Thread\"}", + " +{ + \"Table\": { + \"AttributeDefinitions\": [ + { + \"AttributeName\": \"ForumName\", + \"AttributeType\": \"S\" + }, + { + \"AttributeName\": \"LastPostDateTime\", + \"AttributeType\": \"S\" + }, + { + \"AttributeName\": \"Subject\", + \"AttributeType\": \"S\" + } + ], + \"GlobalSecondaryIndexes\": [ + { + \"IndexName\": \"SubjectIndex\", + \"IndexSizeBytes\": 2048, + \"IndexStatus\": \"CREATING\", + \"ItemCount\": 47, + \"KeySchema\": [ + { + \"AttributeName\": \"Subject\", + \"KeyType\": \"HASH\" + }, + { + \"AttributeName\": \"LastPostDateTime\", + \"KeyType\": \"RANGE\" + } + ], + \"Projection\": { + \"NonKeyAttributes\" : [ + \"Author\" + ], + \"ProjectionType\": \"INCLUDE\" + }, + \"ProvisionedThroughput\": { + \"LastDecreaseDateTime\": 0, + \"LastIncreaseDateTime\": 1, + \"NumberOfDecreasesToday\": 2, + \"ReadCapacityUnits\": 3, + \"WriteCapacityUnits\": 4 + } + } + ], + \"CreationDateTime\": 1.363729002358E9, + \"ItemCount\": 0, + \"KeySchema\": [ + { + \"AttributeName\": \"ForumName\", + \"KeyType\": \"HASH\" + }, + { + \"AttributeName\": \"Subject\", + \"KeyType\": \"RANGE\" + } + ], + \"LocalSecondaryIndexes\": [ + { + \"IndexName\": \"LastPostIndex\", + \"IndexSizeBytes\": 0, + \"ItemCount\": 0, + \"KeySchema\": [ + { + \"AttributeName\": \"ForumName\", + \"KeyType\": \"HASH\" + }, + { + \"AttributeName\": \"LastPostDateTime\", + \"KeyType\": \"RANGE\" + } + ], + \"Projection\": { + \"ProjectionType\": \"KEYS_ONLY\" + } + } + ], + \"ProvisionedThroughput\": { + \"NumberOfDecreasesToday\": 0, + \"ReadCapacityUnits\": 5, + \"WriteCapacityUnits\": 5 + }, + \"TableName\": \"Thread\", + \"TableSizeBytes\": 0, + \"TableStatus\": \"ACTIVE\" + } +} + "}], + ok}), + + ?_ddb_test( + {"wait_for_table_active updating to active, RetryTimes = infinity", + ?_f(erlcloud_ddb_util:wait_for_table_active(<<"Thread">>)), + [{"{\"TableName\":\"Thread\"}", + " +{ + \"Table\": { + \"AttributeDefinitions\": [ + { + \"AttributeName\": \"ForumName\", + \"AttributeType\": \"S\" + }, + { + \"AttributeName\": \"LastPostDateTime\", + \"AttributeType\": \"S\" + }, + { + \"AttributeName\": \"Subject\", + \"AttributeType\": \"S\" + } + ], + \"GlobalSecondaryIndexes\": [ + { + \"IndexName\": \"SubjectIndex\", + \"IndexSizeBytes\": 2048, + \"IndexStatus\": \"CREATING\", + \"ItemCount\": 47, + \"KeySchema\": [ + { + \"AttributeName\": \"Subject\", + \"KeyType\": \"HASH\" + }, + { + \"AttributeName\": \"LastPostDateTime\", + \"KeyType\": \"RANGE\" + } + ], + \"Projection\": { + \"NonKeyAttributes\" : [ + \"Author\" + ], + \"ProjectionType\": \"INCLUDE\" + }, + \"ProvisionedThroughput\": { + \"LastDecreaseDateTime\": 0, + \"LastIncreaseDateTime\": 1, + \"NumberOfDecreasesToday\": 2, + \"ReadCapacityUnits\": 3, + \"WriteCapacityUnits\": 4 + } + } + ], + \"CreationDateTime\": 1.363729002358E9, + \"ItemCount\": 0, + \"KeySchema\": [ + { + \"AttributeName\": \"ForumName\", + \"KeyType\": \"HASH\" + }, + { + \"AttributeName\": \"Subject\", + \"KeyType\": \"RANGE\" + } + ], + \"LocalSecondaryIndexes\": [ + { + \"IndexName\": \"LastPostIndex\", + \"IndexSizeBytes\": 0, + \"ItemCount\": 0, + \"KeySchema\": [ + { + \"AttributeName\": \"ForumName\", + \"KeyType\": \"HASH\" + }, + { + \"AttributeName\": \"LastPostDateTime\", + \"KeyType\": \"RANGE\" + } + ], + \"Projection\": { + \"ProjectionType\": \"KEYS_ONLY\" + } + } + ], + \"ProvisionedThroughput\": { + \"NumberOfDecreasesToday\": 0, + \"ReadCapacityUnits\": 5, + \"WriteCapacityUnits\": 5 + }, + \"TableName\": \"Thread\", + \"TableSizeBytes\": 0, + \"TableStatus\": \"UPDATING\" + } +} + "}, + {"{\"TableName\":\"Thread\"}", + " +{ + \"Table\": { + \"AttributeDefinitions\": [ + { + \"AttributeName\": \"ForumName\", + \"AttributeType\": \"S\" + }, + { + \"AttributeName\": \"LastPostDateTime\", + \"AttributeType\": \"S\" + }, + { + \"AttributeName\": \"Subject\", + \"AttributeType\": \"S\" + } + ], + \"GlobalSecondaryIndexes\": [ + { + \"IndexName\": \"SubjectIndex\", + \"IndexSizeBytes\": 2048, + \"IndexStatus\": \"CREATING\", + \"ItemCount\": 47, + \"KeySchema\": [ + { + \"AttributeName\": \"Subject\", + \"KeyType\": \"HASH\" + }, + { + \"AttributeName\": \"LastPostDateTime\", + \"KeyType\": \"RANGE\" + } + ], + \"Projection\": { + \"NonKeyAttributes\" : [ + \"Author\" + ], + \"ProjectionType\": \"INCLUDE\" + }, + \"ProvisionedThroughput\": { + \"LastDecreaseDateTime\": 0, + \"LastIncreaseDateTime\": 1, + \"NumberOfDecreasesToday\": 2, + \"ReadCapacityUnits\": 3, + \"WriteCapacityUnits\": 4 + } + } + ], + \"CreationDateTime\": 1.363729002358E9, + \"ItemCount\": 0, + \"KeySchema\": [ + { + \"AttributeName\": \"ForumName\", + \"KeyType\": \"HASH\" + }, + { + \"AttributeName\": \"Subject\", + \"KeyType\": \"RANGE\" + } + ], + \"LocalSecondaryIndexes\": [ + { + \"IndexName\": \"LastPostIndex\", + \"IndexSizeBytes\": 0, + \"ItemCount\": 0, + \"KeySchema\": [ + { + \"AttributeName\": \"ForumName\", + \"KeyType\": \"HASH\" + }, + { + \"AttributeName\": \"LastPostDateTime\", + \"KeyType\": \"RANGE\" + } + ], + \"Projection\": { + \"ProjectionType\": \"KEYS_ONLY\" + } + } + ], + \"ProvisionedThroughput\": { + \"NumberOfDecreasesToday\": 0, + \"ReadCapacityUnits\": 5, + \"WriteCapacityUnits\": 5 + }, + \"TableName\": \"Thread\", + \"TableSizeBytes\": 0, + \"TableStatus\": \"ACTIVE\" + } +} + "}], + ok}), + + ?_ddb_test( + {"wait_for_table_active table is deleting", + ?_f(erlcloud_ddb_util:wait_for_table_active(<<"Thread">>)), + [{"{\"TableName\":\"Thread\"}", + " +{ + \"Table\": { + \"AttributeDefinitions\": [ + { + \"AttributeName\": \"ForumName\", + \"AttributeType\": \"S\" + }, + { + \"AttributeName\": \"LastPostDateTime\", + \"AttributeType\": \"S\" + }, + { + \"AttributeName\": \"Subject\", + \"AttributeType\": \"S\" + } + ], + \"GlobalSecondaryIndexes\": [ + { + \"IndexName\": \"SubjectIndex\", + \"IndexSizeBytes\": 2048, + \"IndexStatus\": \"CREATING\", + \"ItemCount\": 47, + \"KeySchema\": [ + { + \"AttributeName\": \"Subject\", + \"KeyType\": \"HASH\" + }, + { + \"AttributeName\": \"LastPostDateTime\", + \"KeyType\": \"RANGE\" + } + ], + \"Projection\": { + \"NonKeyAttributes\" : [ + \"Author\" + ], + \"ProjectionType\": \"INCLUDE\" + }, + \"ProvisionedThroughput\": { + \"LastDecreaseDateTime\": 0, + \"LastIncreaseDateTime\": 1, + \"NumberOfDecreasesToday\": 2, + \"ReadCapacityUnits\": 3, + \"WriteCapacityUnits\": 4 + } + } + ], + \"CreationDateTime\": 1.363729002358E9, + \"ItemCount\": 0, + \"KeySchema\": [ + { + \"AttributeName\": \"ForumName\", + \"KeyType\": \"HASH\" + }, + { + \"AttributeName\": \"Subject\", + \"KeyType\": \"RANGE\" + } + ], + \"LocalSecondaryIndexes\": [ + { + \"IndexName\": \"LastPostIndex\", + \"IndexSizeBytes\": 0, + \"ItemCount\": 0, + \"KeySchema\": [ + { + \"AttributeName\": \"ForumName\", + \"KeyType\": \"HASH\" + }, + { + \"AttributeName\": \"LastPostDateTime\", + \"KeyType\": \"RANGE\" + } + ], + \"Projection\": { + \"ProjectionType\": \"KEYS_ONLY\" + } + } + ], + \"ProvisionedThroughput\": { + \"NumberOfDecreasesToday\": 0, + \"ReadCapacityUnits\": 5, + \"WriteCapacityUnits\": 5 + }, + \"TableName\": \"Thread\", + \"TableSizeBytes\": 0, + \"TableStatus\": \"DELETING\" + } +} + "}], + {error, deleting}}), + + ?_ddb_test( + {"wait_for_table_active retry_threshold_exceeded", + ?_f(erlcloud_ddb_util:wait_for_table_active(<<"Thread">>, 10, 2)), + [{"{\"TableName\":\"Thread\"}", + " +{ + \"Table\": { + \"AttributeDefinitions\": [ + { + \"AttributeName\": \"ForumName\", + \"AttributeType\": \"S\" + }, + { + \"AttributeName\": \"LastPostDateTime\", + \"AttributeType\": \"S\" + }, + { + \"AttributeName\": \"Subject\", + \"AttributeType\": \"S\" + } + ], + \"GlobalSecondaryIndexes\": [ + { + \"IndexName\": \"SubjectIndex\", + \"IndexSizeBytes\": 2048, + \"IndexStatus\": \"CREATING\", + \"ItemCount\": 47, + \"KeySchema\": [ + { + \"AttributeName\": \"Subject\", + \"KeyType\": \"HASH\" + }, + { + \"AttributeName\": \"LastPostDateTime\", + \"KeyType\": \"RANGE\" + } + ], + \"Projection\": { + \"NonKeyAttributes\" : [ + \"Author\" + ], + \"ProjectionType\": \"INCLUDE\" + }, + \"ProvisionedThroughput\": { + \"LastDecreaseDateTime\": 0, + \"LastIncreaseDateTime\": 1, + \"NumberOfDecreasesToday\": 2, + \"ReadCapacityUnits\": 3, + \"WriteCapacityUnits\": 4 + } + } + ], + \"CreationDateTime\": 1.363729002358E9, + \"ItemCount\": 0, + \"KeySchema\": [ + { + \"AttributeName\": \"ForumName\", + \"KeyType\": \"HASH\" + }, + { + \"AttributeName\": \"Subject\", + \"KeyType\": \"RANGE\" + } + ], + \"LocalSecondaryIndexes\": [ + { + \"IndexName\": \"LastPostIndex\", + \"IndexSizeBytes\": 0, + \"ItemCount\": 0, + \"KeySchema\": [ + { + \"AttributeName\": \"ForumName\", + \"KeyType\": \"HASH\" + }, + { + \"AttributeName\": \"LastPostDateTime\", + \"KeyType\": \"RANGE\" + } + ], + \"Projection\": { + \"ProjectionType\": \"KEYS_ONLY\" + } + } + ], + \"ProvisionedThroughput\": { + \"NumberOfDecreasesToday\": 0, + \"ReadCapacityUnits\": 5, + \"WriteCapacityUnits\": 5 + }, + \"TableName\": \"Thread\", + \"TableSizeBytes\": 0, + \"TableStatus\": \"UPDATING\" + } +} + "}, + {"{\"TableName\":\"Thread\"}", + " +{ + \"Table\": { + \"AttributeDefinitions\": [ + { + \"AttributeName\": \"ForumName\", + \"AttributeType\": \"S\" + }, + { + \"AttributeName\": \"LastPostDateTime\", + \"AttributeType\": \"S\" + }, + { + \"AttributeName\": \"Subject\", + \"AttributeType\": \"S\" + } + ], + \"GlobalSecondaryIndexes\": [ + { + \"IndexName\": \"SubjectIndex\", + \"IndexSizeBytes\": 2048, + \"IndexStatus\": \"CREATING\", + \"ItemCount\": 47, + \"KeySchema\": [ + { + \"AttributeName\": \"Subject\", + \"KeyType\": \"HASH\" + }, + { + \"AttributeName\": \"LastPostDateTime\", + \"KeyType\": \"RANGE\" + } + ], + \"Projection\": { + \"NonKeyAttributes\" : [ + \"Author\" + ], + \"ProjectionType\": \"INCLUDE\" + }, + \"ProvisionedThroughput\": { + \"LastDecreaseDateTime\": 0, + \"LastIncreaseDateTime\": 1, + \"NumberOfDecreasesToday\": 2, + \"ReadCapacityUnits\": 3, + \"WriteCapacityUnits\": 4 + } + } + ], + \"CreationDateTime\": 1.363729002358E9, + \"ItemCount\": 0, + \"KeySchema\": [ + { + \"AttributeName\": \"ForumName\", + \"KeyType\": \"HASH\" + }, + { + \"AttributeName\": \"Subject\", + \"KeyType\": \"RANGE\" + } + ], + \"LocalSecondaryIndexes\": [ + { + \"IndexName\": \"LastPostIndex\", + \"IndexSizeBytes\": 0, + \"ItemCount\": 0, + \"KeySchema\": [ + { + \"AttributeName\": \"ForumName\", + \"KeyType\": \"HASH\" + }, + { + \"AttributeName\": \"LastPostDateTime\", + \"KeyType\": \"RANGE\" + } + ], + \"Projection\": { + \"ProjectionType\": \"KEYS_ONLY\" + } + } + ], + \"ProvisionedThroughput\": { + \"NumberOfDecreasesToday\": 0, + \"ReadCapacityUnits\": 5, + \"WriteCapacityUnits\": 5 + }, + \"TableName\": \"Thread\", + \"TableSizeBytes\": 0, + \"TableStatus\": \"UPDATING\" + } +} + "}], + {error, retry_threshold_exceeded}}) + ], + multi_call_tests(Tests). + %% Currently don't have tests for the parallel write (more than 25 items). write_all_tests(_) -> Tests = @@ -918,8 +1516,13 @@ set_out_opt_test_() -> erlcloud_ddb_util:set_out_opt([{typed_out, true}]))}, {"set_out_opt typed_out=false sets out=record", ?_assertEqual([{out, record}], - erlcloud_ddb_util:set_out_opt([{typed_out, false}]))}, - {"set_out_opt preserves location of out opt", + erlcloud_ddb_util:set_out_opt([{typed_out, false}]))}] + ++ set_out_opt_failures_test_(). + +% these will generate dialyzer warnings, so we isolate them +-dialyzer({nowarn_function, set_out_opt_failures_test_/0}). +set_out_opt_failures_test_() -> + [{"set_out_opt preserves location of out opt", ?_assertEqual([{foo, bar}, {out, record}], erlcloud_ddb_util:set_out_opt([{typed_out, false}, {foo, bar}, {out, record}]))}, {"set_out_opt overrides out opt with valid value", diff --git a/test/erlcloud_directconnect_tests.erl b/test/erlcloud_directconnect_tests.erl index 1ee4fcb8d..a57c4d9a3 100644 --- a/test/erlcloud_directconnect_tests.erl +++ b/test/erlcloud_directconnect_tests.erl @@ -59,8 +59,8 @@ sort_json(V) -> %% verifies that the parameters in the body match the expected parameters -spec validate_body(binary(), expected_body()) -> ok. validate_body(Body, Expected) -> - Want = sort_json(jsx:decode(list_to_binary(Expected))), - Actual = sort_json(jsx:decode(Body)), + Want = sort_json(jsx:decode(list_to_binary(Expected), [{return_maps, false}])), + Actual = sort_json(jsx:decode(Body, [{return_maps, false}])), case Want =:= Actual of true -> ok; false -> diff --git a/test/erlcloud_ec2_tests.erl b/test/erlcloud_ec2_tests.erl index 48de885cb..feaa0945d 100644 --- a/test/erlcloud_ec2_tests.erl +++ b/test/erlcloud_ec2_tests.erl @@ -38,6 +38,10 @@ describe_test_() -> fun start/0, fun stop/1, [ + fun describe_route_tables_tests/0, + fun describe_vpcs_tests/1, + fun describe_regions_input_tests/1, + fun describe_regions_output_tests/1, fun describe_tags_input_tests/1, fun describe_tags_output_tests/1, fun request_spot_fleet_input_tests/1, @@ -57,7 +61,12 @@ describe_test_() -> fun delete_flow_logs_input_tests/1, fun delete_flow_logs_output_tests/1, fun describe_flow_logs_input_tests/1, - fun describe_flow_logs_output_tests/1 + fun describe_flow_logs_output_tests/1, + fun describe_launch_template_versions_input_tests/1, + fun describe_launch_template_versions_output_tests/1, + fun describe_security_groups_input_tests/1, + fun describe_security_groups_output_tests/1, + fun describe_instances_output_tests/1 ]}. start() -> @@ -181,6 +190,131 @@ output_tests(Fun, Tests) -> %%% Actual test specifiers %%%=================================================================== +describe_vpcs_tests(_) -> + Tests = [ + ?_ec2_test({ + "Describe all the vpcs", + " + 9a0571b9-6e91-47e7-b75f-785125322853 + + + vpc-00000000000000001 + 000000000001 + available + 10.0.0.0/16 + dopt-00000001 + + + 10.0.0.0/16 + vpc-cidr-assoc-00000000000000001 + + associated + + + + 10.1.0.0/16 + vpc-cidr-assoc-00000000000000002 + + test state + test status message + + + + + + Key + Value + + + default + false + + + ", + {ok, [ + [ + {vpc_id, "vpc-00000000000000001"}, + {state, "available"}, + {cidr_block, "10.0.0.0/16"}, + {dhcp_options_id, "dopt-00000001"}, + {instance_tenancy, "default"}, + {is_default, false}, + {cidr_block_association_set, [ + [ + {cidr_block, "10.0.0.0/16"}, + {association_id, "vpc-cidr-assoc-00000000000000001"}, + {cidr_block_state, [{state, "associated"}]} + ], + [ + {cidr_block, "10.1.0.0/16"}, + {association_id, "vpc-cidr-assoc-00000000000000002"}, + {cidr_block_state, [{state, "test state"}, {status_message, "test status message"}]} + ] + ]}, + {tag_set, [ + [{key, "Key"}, {value, "Value"}] + ]} + ] + ]} + }) + ], + output_tests(?_f(erlcloud_ec2:describe_vpcs()), Tests). + +describe_regions_input_tests(_) -> + Tests = + [?_ec2_test( + {"This example describes all regions.", + ?_f(erlcloud_ec2:describe_regions([], [{"opt-in-status", "opted-in"}], erlcloud_aws:default_config())), + [{"Action", "DescribeRegions"}, + {"Filter.1.Name","opt-in-status"}, + {"Filter.1.Value.1","opted-in"}, + {"AllRegions","false"}]}), + ?_ec2_test( + {"This example describes all regions.", + ?_f(erlcloud_ec2:describe_regions([], [], true, erlcloud_aws:default_config())), + [{"Action", "DescribeRegions"}, + {"AllRegions","true"}]})], + Response = " + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + +", + input_tests(Response, Tests). + +describe_regions_output_tests(_) -> + Tests = [ + ?_ec2_test({ + "Describe all the regions", + " + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + + + af-south-1 + ec2.af-south-1.amazonaws.com + opted-in + + + ap-northeast-3 + ec2.ap-northeast-3.amazonaws.com + opted-in + + + ", + {ok, [ + [ + {region_name,"af-south-1"}, + {region_endpoint,"ec2.af-south-1.amazonaws.com"}, + {opt_in_status,"opted-in"} + ], + [ + {region_name,"ap-northeast-3"}, + {region_endpoint,"ec2.ap-northeast-3.amazonaws.com"}, + {opt_in_status,"opted-in"} + ] + ]} + }) + ], + output_tests(?_f(erlcloud_ec2:describe_regions([], [{"opt-in-status", "opted-in"}], erlcloud_aws:default_config())), Tests). %% DescribeTags test based on the API examples: %% http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-DescribeTags.html @@ -713,7 +847,12 @@ describe_images_tests(_) -> 2016-03-26T12:00:13Z hvm - + + + Key + Value + + xen @@ -735,7 +874,8 @@ describe_images_tests(_) -> {creation_date, {{2016,3,26},{12,0,13}}}, {platform, "windows"}, {block_device_mapping, []}, - {product_codes, []} + {product_codes, []}, + {tag_set, [[{key,"Key"},{value, "Value"}]]} ]]}})], %% Remaining AWS API examples return subsets of the same data @@ -783,12 +923,135 @@ describe_account_attributes_test() -> ]}, meck:new(erlcloud_aws, [passthrough]), meck:expect(erlcloud_aws, aws_request_xml4, - fun(_,_,_,_,_,_) -> + fun(_,_,_,_,_,_,_,_) -> XMERL end), Result = erlcloud_ec2:describe_account_attributes(), meck:unload(erlcloud_aws), ?assertEqual(ExpectedResult, Result). + +describe_route_tables_tests() -> + XML = " + 03c84dd4-bc36-48aa-8ac3-1d1fbaec96b5 + + + rtb-0b70ded185be2a2e4 + vpc-012ff464df6bbf762 + 352283894008 + + + 10.0.0.0/26 + local + active + CreateRouteTable + + + 0.0.0.0/8 + nat-06e45944bb42d3ba2 + active + CreateRoute + + + 0.0.0.0/0 + vgw-09fea3ed190b9ea1e + active + CreateRoute + + + ::/64 + igw-1456db71 + active + CreateRoute + + + + + rtbassoc-123 + rtb-567 + subnet-0bdf348a667f86262 +
    false
    + + associated + +
    + + rtbassoc-789 + rtb-345 +
    true
    + + associated + +
    +
    + + + + Name + PublicRT + + +
    +
    +
    ", + XMERL = {ok, element(1, xmerl_scan:string(XML))}, + ExpectedResult = + {ok,[[{route_table_id,"rtb-0b70ded185be2a2e4"}, + {vpc_id,"vpc-012ff464df6bbf762"}, + {route_set, + [[{destination_cidr_block,"10.0.0.0/26"}, + {destination_ipv6_cidr_block,[]}, + {gateway_id,"local"}, + {nat_gateway_id,[]}, + {instance_id,[]}, + {vpc_peering_conn_id,[]}, + {network_interface_id,[]}, + {state,"active"}, + {origin,"CreateRouteTable"}], + [{destination_cidr_block,"0.0.0.0/8"}, + {destination_ipv6_cidr_block,[]}, + {gateway_id,[]}, + {nat_gateway_id,"nat-06e45944bb42d3ba2"}, + {instance_id,[]}, + {vpc_peering_conn_id,[]}, + {network_interface_id,[]}, + {state,"active"}, + {origin,"CreateRoute"}], + [{destination_cidr_block,"0.0.0.0/0"}, + {destination_ipv6_cidr_block,[]}, + {gateway_id,"vgw-09fea3ed190b9ea1e"}, + {nat_gateway_id,[]}, + {instance_id,[]}, + {vpc_peering_conn_id,[]}, + {network_interface_id,[]}, + {state,"active"}, + {origin,"CreateRoute"}], + [{destination_cidr_block,[]}, + {destination_ipv6_cidr_block,"::/64"}, + {gateway_id,"igw-1456db71"}, + {nat_gateway_id,[]}, + {instance_id,[]}, + {vpc_peering_conn_id,[]}, + {network_interface_id,[]}, + {state,"active"}, + {origin,"CreateRoute"}]]}, + {association_set, + [[{route_table_association_id,"rtbassoc-123"}, + {route_table_id,"rtb-567"}, + {main,"false"}, + {subnet_id,"subnet-0bdf348a667f86262"}], + [{route_table_association_id,"rtbassoc-789"}, + {route_table_id,"rtb-345"}, + {main,"true"}, + {subnet_id,[]}]]}, + {tag_set,[[{key,"Name"},{value,"PublicRT"}]]}]]}, + meck:new(erlcloud_aws, [passthrough]), + meck:expect(erlcloud_aws, aws_request_xml4, + fun(_,_,_,_,_,_,_,_) -> + XMERL + end), + Result = erlcloud_ec2:describe_route_tables(), + meck:unload(erlcloud_aws), + ?assertEqual(ExpectedResult, Result). describe_nat_gateways_test() -> XML = " @@ -842,7 +1105,7 @@ describe_nat_gateways_test() -> }, meck:new(erlcloud_aws, [passthrough]), meck:expect(erlcloud_aws, aws_request_xml4, - fun(_,_,_,_,_,_) -> + fun(_,_,_,_,_,_,_,_) -> XMERL end), Result = erlcloud_ec2:describe_nat_gateways(), @@ -915,7 +1178,7 @@ describe_vpc_peering_connections_test() -> }, meck:new(erlcloud_aws, [passthrough]), meck:expect(erlcloud_aws, aws_request_xml4, - fun(_,_,_,_,_,_) -> + fun(_,_,_,_,_,_,_,_) -> XMERL end), Result = erlcloud_ec2:describe_vpc_peering_connections(), @@ -1121,6 +1384,262 @@ describe_instances_test_() -> {timeout, 60, fun () -> test_pagination(Tests, generate_instances_response, describe_instances, [], [[]]) end} . +describe_instances_output_tests(_) -> + Tests = + [?_ec2_test( + {"This example describes all instances", " + + 95bd274e-560d-46c8-82da-bf41da8f2caf + + + r-000001 + 123456789012 + + + + i-000001 + ami-000001 + + 16 + running + + ip-10-0-1-1.ec2.internal + + + key + 0 + + + false + + m3.medium + 2025-01-31T09:01:15.000Z + + us-east-1a + + default + + + disabled + + subnet-00001 + vpc-00001 + 10.0.1.1 + 12.1.2.3 + true + + + sg-00001 + security-group-01 + + + x86_64 + ebs + /dev/sda1 + + + /dev/sda1 + + vol-00001 + attached + 2025-01-31T09:00:34.000Z + true + + + + hvm + + + + Name + test-instance + + + xen + + + eni-00001 + subnet-00001 + vpc-00001 + Primary network interface + 123456789012 + in-use + 02:bb:10:1a:1a:1a + 10.0.1.1 + true + + + sg-00001 + security-group-01 + + + + eni-attach-00001 + 0 + attached + 2025-01-31T09:00:34.000Z + true + 0 + + + 123.12.123.12 + + amazon + + + + 10.0.1.1 + true + + 123.12.123.12 + + amazon + + + + + interface + + false + + + + false + true + + 1 + 1 + + + open + + + false + + + false + + + applied + optional + 1 + enabled + disabled + disabled + + + default + default + + + default + + Linux/UNIX + RunInstances + 2025-01-31T09:00:34.000Z + + + + + + + ", + {ok, [[ + {reservation_id, "r-000001"}, + {owner_id, "123456789012"}, + {instances_set, [[ + {instance_id, "i-000001"}, + {group_set, [[ + {group_id, "sg-00001"}, + {group_name, "security-group-01"} + ]]}, + {image_id, "ami-000001"}, + {instance_state, [{code, 16}, {name, "running"}]}, + {private_dns_name, "ip-10-0-1-1.ec2.internal"}, + {dns_name, []}, + {reason, none}, + {key_name, "key"}, + {metadata_options, [[ + {http_endpoint, "enabled"}, + {http_protocol_ipv6, "disabled"}, + {http_put_response_hop_limit, 1}, + {http_tokens, "optional"}, + {instance_metadata_tags, "disabled"}, + {state, "applied"} + ]]}, + {ami_launch_index, 0}, + {product_codes, []}, + {instance_type, "m3.medium"}, + {launch_time, {{2025, 1, 31}, {9, 1, 15}}}, + {platform, []}, + {placement, [{availability_zone, "us-east-1a"}]}, + {kernel_id, []}, + {ramdisk_id, []}, + {monitoring, [{enabled, false}, {state, "disabled"}]}, + {subnet_id, "subnet-00001"}, + {vpc_id, "vpc-00001"}, + {private_ip_address, "10.0.1.1"}, + {ip_address, "12.1.2.3"}, + {state_reason, [{code, []}, {message, []}]}, + {architecture, "x86_64"}, + {root_device_type, "ebs"}, + {root_device_name, "/dev/sda1"}, + {block_device_mapping, [[ + {device_name, "/dev/sda1"}, + {volume_id, "vol-00001"}, + {status, "attached"}, + {attach_time, {{2025, 1, 31}, {9, 0, 34}}}, + {delete_on_termination, true} + ]]}, + {instance_lifecycle, none}, + {spot_instance_request_id, none}, + {iam_instance_profile, [{arn, []}, {id, []}]}, + {tag_set, [[{key, "Name"}, {value, "test-instance"}]]}, + {network_interface_set, [[ + {network_interface_id, "eni-00001"}, + {subnet_id, "subnet-00001"}, + {vpc_id, "vpc-00001"}, + {availability_zone, []}, + {description, "Primary network interface"}, + {owner_id, "123456789012"}, + {requester_managed, false}, + {status, "in-use"}, + {mac_address, "02:bb:10:1a:1a:1a"}, + {private_ip_address, "10.0.1.1"}, + {source_dest_check, true}, + {groups_set, [[ + {group_id, "sg-00001"}, + {group_name, "security-group-01"} + ]]}, + {attachment, [ + {attachment_id, "eni-attach-00001"}, + {instance_id, []}, + {instance_owner_id, []}, + {device_index, "0"}, + {status, "attached"}, + {attach_time, {{2025, 1, 31}, {9, 0, 34}}}, + {delete_on_termination, true} + ]}, + {association, [ + {public_ip, "123.12.123.12"}, + {public_dns_name, []}, + {ip_owner_id, "amazon"}, + {allocation_id, []}, + {association_id, []} + ]}, + {tag_set, []}, + {private_ip_addresses_set, [[ + {private_ip_address, "10.0.1.1"}, + {primary, true} + ]]} + ]]} + ]]} + ]]}} + ) + ], + output_tests(?_f(erlcloud_ec2:describe_instances()), Tests) +. + describe_instances_boundaries_test_() -> [ ?_assertException(error, function_clause, erlcloud_ec2:describe_instances([], 4, undefined)), @@ -1153,6 +1672,7 @@ describe_spot_price_history_test_() -> {timeout, 60, fun () -> test_pagination(Tests, generate_spot_price_history_response, describe_spot_price_history, [], ["", "", [], ""]) end} . +-dialyzer({nowarn_function, describe_spot_price_history_boundaries_test_/0}). describe_spot_price_history_boundaries_test_() -> [ ?_assertException(error, function_clause, erlcloud_ec2:describe_spot_price_history(["", "", [], ""], 4, undefined)), @@ -1181,6 +1701,431 @@ describe_reserved_instances_offerings_boundaries_test_() -> ?_assertException(error, function_clause, erlcloud_ec2:describe_reserved_instances_offerings([], 1001, undefined)) ]. +describe_launch_templates_test() -> + XML = " + 1afa6e44-eb38-4229-8db6-d5eaexample + + + 2017-10-31T11:38:52.000Z + arn:aws:iam::123456789012:root + 1 + 1 + lt-0a20c965061f64abc + MyLaunchTemplate + + + ", + XMERL = {ok, element(1, xmerl_scan:string(XML))}, + ExpectedResult = + {ok, + [[ + {created_by,"arn:aws:iam::123456789012:root"}, + {create_time,{{2017,10,31},{11,38,52}}}, + {default_version_number,1}, + {launch_template_id,"lt-0a20c965061f64abc"}, + {launch_template_name,"MyLaunchTemplate"}, + {tag_set,[]} + ]] + }, + meck:new(erlcloud_aws, [passthrough]), + meck:expect(erlcloud_aws, aws_request_xml4, + fun(_,_,_,_,_,_,_,_) -> + XMERL + end), + Result = erlcloud_ec2:describe_launch_templates(), + meck:unload(erlcloud_aws), + ?assertEqual(ExpectedResult, Result). + +describe_launch_template_versions_input_tests(_) -> + Tests = + [?_ec2_test({ + "This example describes a set of launch template versions for template with matching ID", + ?_f(erlcloud_ec2:describe_launch_template_versions("lt-0a20c965061f64abc")), + [{"Action", "DescribeLaunchTemplateVersions"}, + {"LaunchTemplateId", "lt-0a20c965061f64abc"}]}), + ?_ec2_test({ + "This example describes a set of launch template versions for template with matching name", + ?_f(erlcloud_ec2:describe_launch_template_versions(none, "Test-LT-foo", [])), + [{"Action", "DescribeLaunchTemplateVersions"}, + {"LaunchTemplateName", "Test-LT-foo"}]}), + ?_ec2_test({ + "This example describes launch template versions with matching version(s)", + ?_f(erlcloud_ec2:describe_launch_template_versions("lt-0f1b5e0ff43b7f2ad", none, [{launch_template_version, ["1"]}])), + [{"Action", "DescribeLaunchTemplateVersions"}, + {"LaunchTemplateId", "lt-0f1b5e0ff43b7f2ad"}, + {"LaunchTemplateVersion.1", "1"}]}), + ?_ec2_test({ + "This example describes launch template versions with minimum version number", + ?_f(erlcloud_ec2:describe_launch_template_versions("lt-0a20c965061f64abc", none, [{min_version, 1}])), + [{"Action", "DescribeLaunchTemplateVersions"}, + {"LaunchTemplateId", "lt-0a20c965061f64abc"}, + {"MinVersion", "1"}]}), + ?_ec2_test({ + "This example describes launch template versions with maximum version number", + ?_f(erlcloud_ec2:describe_launch_template_versions("lt-0a20c965061f64abc", none, [{max_version, 10}])), + [{"Action", "DescribeLaunchTemplateVersions"}, + {"LaunchTemplateId", "lt-0a20c965061f64abc"}, + {"MaxVersion", "10"}]}), + ?_ec2_test({ + "This example describes launch template versions with max results count configured", + ?_f(erlcloud_ec2:describe_launch_template_versions("lt-0a20c965061f64abc", none, [], none, 10, undefined)), + [{"Action", "DescribeLaunchTemplateVersions"}, + {"LaunchTemplateId", "lt-0a20c965061f64abc"}, + {"MaxResults", "10"}]}), + ?_ec2_test({ + "This example describes retrieval of subsequent launch template version result page", + ?_f(erlcloud_ec2:describe_launch_template_versions("lt-0a20c965061f64abc", none, [], none, 10, "next-token")), + [{"Action", "DescribeLaunchTemplateVersions"}, + {"LaunchTemplateId", "lt-0a20c965061f64abc"}, + {"NextToken", "next-token"}, + {"MaxResults", "10"}]}), + ?_ec2_test({ + "This example describes launch template versions with max results count configured selected by template name", + ?_f(erlcloud_ec2:describe_launch_template_versions(none, "SomeNameHere", [], none, 10, undefined)), + [{"Action", "DescribeLaunchTemplateVersions"}, + {"LaunchTemplateName", "SomeNameHere"}, + {"MaxResults", "10"}]}), + ?_ec2_test({ + "This example describes retrieval of subsequent launch template version result page selected by template name", + ?_f(erlcloud_ec2:describe_launch_template_versions(none, "SomeNameHere", [], none, 10, "next-token")), + [{"Action", "DescribeLaunchTemplateVersions"}, + {"LaunchTemplateName", "SomeNameHere"}, + {"NextToken", "next-token"}, + {"MaxResults", "10"}]}) + ], + Response = " + + 65cadec1-b364-4354-8ca8-4176dexample + + + 2017-10-31T11:38:52.000Z + arn:aws:iam::123456789012:root + true + + ami-8c1be5f6 + t2.micro + + lt-0a20c965061f64abc + MyLaunchTemplate + FirstVersion + 1 + + + 2017-10-31T11:52:03.000Z + arn:aws:iam::123456789012:root + false + + ami-12345678 + + lt-0a20c965061f64abc + MyLaunchTemplate + AMIOnlyv1 + 2 + + + 2017-10-31T11:55:15.000Z + arn:aws:iam::123456789012:root + false + + ami-aabbccdd + + lt-0a20c965061f64abc + MyLaunchTemplate + AMIOnlyv2 + 3 + + + + ", + input_tests(Response, Tests). + +describe_launch_template_versions_output_tests(_) -> + Tests = + [?_ec2_test( + {"This example describes all launch template versions for given template", + " + + c68f69e1-48fd-42c9-83cf-6b36cb07475b + + + 2022-10-21T08:51:54.000Z + arn:aws:sts::123483894321:assumed-role/centralized-users/john.doe + true + + + standard + + + 1 + 2 + + false + false + true + + true + + + arn:aws:iam::123483894321:instance-profile/ecsInstanceRole + + ami-5934df24 + stop + c5.large + + default + + + enabled + optional + + + true + + + + 0 + eni-060ceeb735813d39d + + + + default + + + true + ip-name + + + + instance + + + tag1 + 134274125 + + + + + volume + + + tag1 + 134274125 + + + + + + lt-08ccaba0746110123 + nb-ec-test-1 + Test template foo + 1 + + + ", + {ok, [[{created_by, "arn:aws:sts::123483894321:assumed-role/centralized-users/john.doe"}, + {create_time,{{2022,10,21},{8,51,54}}}, + {default_version,true}, + {launch_template_data, + [[{block_device_mapping_set,[]}, + {capacity_reservation_specification,[]}, + {cpu_options, + [[{core_count,1},{threads_per_core,2}]]}, + {credit_specification,[ + [{cpu_credits, "standard"}]]}, + {disable_api_stop,false}, + {disable_api_termination,false}, + {ebs_optimized,true}, + {elastic_gpu_specification_set,[]}, + {elastic_inference_accelerator_set,[]}, + {enclave_options,[{enabled,false}]}, + {hibernation_options,[{configured,true}]}, + {iam_instance_profile, + [[{arn, + "arn:aws:iam::123483894321:instance-profile/ecsInstanceRole"}, + {id,[]}]]}, + {image_id,"ami-5934df24"}, + {instance_initiated_shutdown_behavior,"stop"}, + {instance_market_options,[]}, + {instance_requirements,[]}, + {instance_type,"c5.large"}, + {kernel_id,[]}, + {key_name,[]}, + {license_set,[]}, + {maintenance_options, + [[{auto_recovery,"default"}]]}, + {metadata_options, + [[{http_endpoint,"enabled"}, + {http_protocol_ipv6,[]}, + {http_put_response_hop_limit,0}, + {http_tokens,"optional"}, + {instance_metadata_tags,[]}, + {state,[]}]]}, + {monitoring,[{enabled,true}]}, + {network_interface_set, + [[{associate_carrier_ip_address,false}, + {associate_public_ip_address,false}, + {delete_on_termination,false}, + {description,[]}, + {device_index,0}, + {group_set,[]}, + {interface_type,[]}, + {ipv4_prefix_count,0}, + {ipv4_prefix_set,[]}, + {ipv6_address_count,0}, + {ipv6_addresses_set,[]}, + {ipv6_prefix_count,0}, + {ipv6_prefix_set,[]}, + {network_card_index,0}, + {network_interface_id, "eni-060ceeb735813d39d"}, + {private_ip_address,[]}, + {private_ip_addresses_set,[]}, + {secondary_private_ip_address_count,0}, + {subnet_id,[]}]]}, + {placement, + [[{affinity,[]}, + {availability_zone,[]}, + {group_name,[]}, + {host_id,[]}, + {host_resource_group_arn,[]}, + {partition_number,0}, + {spread_domain,[]}, + {tenancy,"default"}]]}, + {private_dns_name_options, + [[{enable_resource_name_dns_aaaa_record, false}, + {enable_resource_name_dns_a_record,true}, + {hostname_type,"ip-name"}]]}, + {ram_disk_id,[]}, + {security_group_id_set,[]}, + {security_group_set,[]}, + {tag_specification_set, + [[{resource_type,"instance"}, + {tag_set, + [[{key,"tag1"},{value,"134274125"}]]}], + [{resource_type,"volume"}, + {tag_set, + [[{key,"tag1"}, + {value,"134274125"}]]}]]}, + {user_data,[]}]]}, + {launch_template_id,"lt-08ccaba0746110123"}, + {launch_template_name,"nb-ec-test-1"}, + {version_description, + "Test template foo"}, + {version_number,1}]]}} + ) + ], + output_tests(?_f(erlcloud_ec2:describe_launch_template_versions("lt-08ccaba0746110123")), Tests). + +generate_security_group_response() -> + " + 1d62eae0-acdd-481d-88c9-example + + + 123456789012 + sg-9bf6ceff + SSHAccess + Security group for SSH access + vpc-31896b55 + + + tcp + 22 + 22 + + + + 0.0.0.0/0 + + + + + ::/0 + + + + + + + + -1 + + + + 0.0.0.0/0 + + + + + ::/0 + + + + + + + + ". + +describe_security_groups_input_tests(_) -> + Tests = + [?_ec2_test({ + "Describe security group(s), default call with no additional parameters", + ?_f(erlcloud_ec2:describe_security_groups()), + [{"Action", "DescribeSecurityGroups"}]}), + ?_ec2_test({ + "Describe security group(s) matching the given group name", + ?_f(erlcloud_ec2:describe_security_groups(["SSHAccess"])), + [{"Action", "DescribeSecurityGroups"}, + {"GroupName.1", "SSHAccess"}]}), + ?_ec2_test({ + "Describe security group(s) matching the given group ID(s)", + ?_f(erlcloud_ec2:describe_security_groups(["sg-9bf6ceff"], [], none, erlcloud_aws:default_config())), + [{"Action", "DescribeSecurityGroups"}, + {"GroupId.1", "sg-9bf6ceff"}]}), + ?_ec2_test({ + "Describe security group(s) matching the given set of filters", + ?_f(erlcloud_ec2:describe_security_groups([], [], [{ + "ip-permission-ipv6-cidr", ["::/0"]}], erlcloud_aws:default_config())), + [{"Action", "DescribeSecurityGroups"}, + {"Filter.1.Name" ,"ip-permission-ipv6-cidr"}, + {"Filter.1.Value.1", "%3A%3A%2F0"}]}) + ], + Response = generate_security_group_response(), + input_tests(Response, Tests). + +describe_security_groups_output_tests(_) -> + Tests = [ + ?_ec2_test({ + "Coverage for DescribeSecurityGroup API Request", + generate_security_group_response(), + {ok, [[ + {owner_id, "123456789012"}, + {group_id, "sg-9bf6ceff"}, + {group_name, "SSHAccess"}, + {group_description, "Security group for SSH access"}, + {vpc_id, "vpc-31896b55"}, + {ip_permissions, [[ + {ip_protocol, tcp}, + {from_port, 22}, + {to_port, 22}, + {users, []}, + {groups, []}, + {ip_ranges, ["0.0.0.0/0"]}, + {ipv6_ranges, ["::/0"]}] + ]}, + {ip_permissions_egress, [[ + {ip_protocol, '-1'}, + {from_port, 0}, + {to_port, 0}, + {users, []}, + {groups, []}, + {ip_ranges, ["0.0.0.0/0"]}, + {ipv6_ranges, ["::/0"]}] + ]}, + {tag_set, []}]]} + }) + ], + output_tests(?_f(erlcloud_ec2:describe_security_groups()), Tests). + generate_one_instance(N) -> " r-69 @@ -1202,6 +2147,7 @@ generate_one_instance(N) -> t2.small 2016-09-26T10:35:00.000Z + us-east-1a @@ -1225,6 +2171,14 @@ generate_one_instance(N) -> xen false + + applied + optional + 1 + enabled + disabled + disabled + ". @@ -1378,7 +2332,7 @@ test_pagination([], _, _, _, _) -> ok; test_pagination([{TotalResults, ResultsPerPage} | Rest], ResponseGenerator, OriginalFunction, NormalParams, PagedParams) -> meck:new(erlcloud_aws, [passthrough]), meck:expect(erlcloud_aws, aws_request_xml4, - fun(_,_,_,Params,_,_) -> + fun(_,_,_,_,_,Params,_,_) -> NextTokenString = proplists:get_value("NextToken", Params), MaxResults = proplists:get_value("MaxResults", Params), {Start, End, NT} = diff --git a/test/erlcloud_ecs_tests.erl b/test/erlcloud_ecs_tests.erl index 93a06538e..51adf679c 100644 --- a/test/erlcloud_ecs_tests.erl +++ b/test/erlcloud_ecs_tests.erl @@ -64,6 +64,8 @@ operation_test_() -> fun list_container_instances_output_tests/1, fun list_services_input_tests/1, fun list_services_output_tests/1, + fun list_tags_for_resource_input_tests/1, + fun list_tags_for_resource_output_tests/1, fun list_task_definition_families_input_tests/1, fun list_task_definition_families_output_tests/1, fun list_task_definitions_input_tests/1, @@ -1319,7 +1321,7 @@ describe_container_instances_output_tests(_) -> failures = [] }}}) ], - output_tests(?_f(erlcloud_ecs:describe_container_instances("f9cc75bb-0c94-46b9-bf6d-49d320bc1551", [{out, record}])), Tests). + output_tests(?_f(erlcloud_ecs:describe_container_instances(["f9cc75bb-0c94-46b9-bf6d-49d320bc1551"], [{out, record}])), Tests). %% DescribeServices test based on the API examples: %% http://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_DescribeServices.html @@ -1613,10 +1615,26 @@ describe_tasks_input_tests(_) -> Tests = [?_ecs_test( {"DescribeTasks example request", - ?_f(erlcloud_ecs:describe_tasks(["c09f0188-7f87-4b0f-bfc3-16296622b6fe"])), " + ?_f(erlcloud_ecs:describe_tasks(["c09f0188-7f87-4b0f-bfc3-16296622b6fe"], [{include, ["TAGS"]}])), " { \"tasks\": [ \"c09f0188-7f87-4b0f-bfc3-16296622b6fe\" + ], + \"include\": [ + \"TAGS\" + ] +} +" + }), + ?_ecs_test( + {"DescribeTasks binary input example request", + ?_f(erlcloud_ecs:describe_tasks([<<"c09f0188-7f87-4b0f-bfc3-16296622b6fe">>], [{include, [<<"TAGS">>]}])), " +{ + \"tasks\": [ + \"c09f0188-7f87-4b0f-bfc3-16296622b6fe\" + ], + \"include\": [ + \"TAGS\" ] } " @@ -1680,14 +1698,54 @@ describe_tasks_output_tests(_) -> \"failures\": [], \"tasks\": [ { + \"attachments\": [ + { + \"id\": \"a1fda7d8-9592-4644-81ad-14578bc379ed\", + \"type\": \"ElasticNetworkInterface\", + \"status\": \"ATTACHED\", + \"details\": [ + { + \"name\": \"subnetId\", + \"value\": \"subnet-0c28a8c6b37e69447\" + }, + { + \"name\": \"networkInterfaceId\", + \"value\": \"eni-07f4ac7c00742fbfe\" + }, + { + \"name\": \"macAddress\", + \"value\": \"0e:2b:61:08:f9:21\" + }, + { + \"name\": \"privateIPv4Address\", + \"value\": \"10.0.0.92\" + } + ] + } + ], + \"availabilityZone\": \"us-east-1a\", \"clusterArn\": \"arn:aws:ecs:us-east-1:012345678910:cluster/default\", + \"connectivity\": \"CONNECTED\", + \"connectivityAt\": 1605621285.465, \"containerInstanceArn\": \"arn:aws:ecs:us-east-1:012345678910:container-instance/84818520-995f-4d94-9d70-7714bacc2953\", \"containers\": [ { \"containerArn\": \"arn:aws:ecs:us-east-1:012345678910:container/76c980a8-2454-4a9c-acc4-9eb103117273\", + \"cpu\": \"256\", + \"exitCode\": 0, + \"healthStatus\": \"UNKNOWN\", + \"image\": \"nginx:latest\", \"lastStatus\": \"RUNNING\", + \"memoryReservation\": \"512\", \"name\": \"mysql\", \"networkBindings\": [], + \"networkInterfaces\": [ + { + \"attachmentId\": \"a1fda7d8-9592-4644-81ad-14578bc379ed\", + \"privateIpv4Address\": \"10.0.0.92\" + } + ], + \"runtimeId\": \"19c89c93db7d248eb14044ba24925d9e50ef581e5030b23490e71d1beff3fc4e\", \"taskArn\": \"arn:aws:ecs:us-east-1:012345678910:task/c09f0188-7f87-4b0f-bfc3-16296622b6fe\" }, { @@ -1704,8 +1762,14 @@ describe_tasks_output_tests(_) -> \"taskArn\": \"arn:aws:ecs:us-east-1:012345678910:task/c09f0188-7f87-4b0f-bfc3-16296622b6fe\" } ], + \"cpu\": \"256\", + \"createdAt\": 1605621273.275, \"desiredStatus\": \"RUNNING\", + \"group\": \"service:kktest-fargate-service\", + \"healthStatus\": \"UNKNOWN\", \"lastStatus\": \"RUNNING\", + \"launchType\": \"FARGATE\", + \"memory\": \"512\", \"overrides\": { \"containerOverrides\": [ { @@ -1716,9 +1780,19 @@ describe_tasks_output_tests(_) -> } ] }, + \"platformVersion\": \"1.3.0\", + \"pullStartedAt\": 1605621330.792, + \"pullStoppedAt\": 1605621335.792, + \"startedAt\": 1605621337.792, \"startedBy\": \"ecs-svc/9223370606521064774\", + \"stoppedAt\": 1606906997.776, + \"stoppedReason\": \"Task stopped by user\", + \"tags\": [ + {\"key\": \"test-key\", \"value\": \"test-value\"} + ], \"taskArn\": \"arn:aws:ecs:us-east-1:012345678910:task/c09f0188-7f87-4b0f-bfc3-16296622b6fe\", - \"taskDefinitionArn\": \"arn:aws:ecs:us-east-1:012345678910:task-definition/hello_world:10\" + \"taskDefinitionArn\": \"arn:aws:ecs:us-east-1:012345678910:task-definition/hello_world:10\", + \"version\": 3 } ] } @@ -1727,14 +1801,42 @@ describe_tasks_output_tests(_) -> failures = [], tasks = [ #ecs_task{ + attachments = [ + #ecs_attachment{ + id = <<"a1fda7d8-9592-4644-81ad-14578bc379ed">>, + type = <<"ElasticNetworkInterface">>, + status = <<"ATTACHED">>, + details = [ + #ecs_attachment_detail{name = <<"subnetId">>, value = <<"subnet-0c28a8c6b37e69447">>}, + #ecs_attachment_detail{name = <<"networkInterfaceId">>, value = <<"eni-07f4ac7c00742fbfe">>}, + #ecs_attachment_detail{name = <<"macAddress">>, value = <<"0e:2b:61:08:f9:21">>}, + #ecs_attachment_detail{name = <<"privateIPv4Address">>, value = <<"10.0.0.92">>} + ] + } + ], + availability_zone = <<"us-east-1a">>, cluster_arn = <<"arn:aws:ecs:us-east-1:012345678910:cluster/default">>, + connectivity = <<"CONNECTED">>, + connectivity_at = 1605621285.465, container_instance_arn = <<"arn:aws:ecs:us-east-1:012345678910:container-instance/84818520-995f-4d94-9d70-7714bacc2953">>, containers = [ #ecs_container{ container_arn = <<"arn:aws:ecs:us-east-1:012345678910:container/76c980a8-2454-4a9c-acc4-9eb103117273">>, + cpu = <<"256">>, + exit_code = 0, + health_status = <<"UNKNOWN">>, + image = <<"nginx:latest">>, last_status = <<"RUNNING">>, + memory_reservation = <<"512">>, name = <<"mysql">>, network_bindings = [], + network_interfaces = [ + #ecs_network_interface{ + attachment_id = <<"a1fda7d8-9592-4644-81ad-14578bc379ed">>, + private_ipv4_address = <<"10.0.0.92">> + } + ], + runtime_id = <<"19c89c93db7d248eb14044ba24925d9e50ef581e5030b23490e71d1beff3fc4e">>, task_arn = <<"arn:aws:ecs:us-east-1:012345678910:task/c09f0188-7f87-4b0f-bfc3-16296622b6fe">> }, #ecs_container{ @@ -1751,8 +1853,14 @@ describe_tasks_output_tests(_) -> task_arn = <<"arn:aws:ecs:us-east-1:012345678910:task/c09f0188-7f87-4b0f-bfc3-16296622b6fe">> } ], + cpu = <<"256">>, + created_at = 1605621273.275, + group = <<"service:kktest-fargate-service">>, desired_status = <<"RUNNING">>, last_status = <<"RUNNING">>, + launch_type = <<"FARGATE">>, + health_status = <<"UNKNOWN">>, + memory = <<"512">>, overrides = #ecs_task_override{ container_overrides = [ #ecs_container_override{ @@ -1763,9 +1871,19 @@ describe_tasks_output_tests(_) -> } ] }, + platform_version = <<"1.3.0">>, + pull_started_at = 1605621330.792, + pull_stopped_at = 1605621335.792, + started_at = 1605621337.792, started_by = <<"ecs-svc/9223370606521064774">>, + stopped_at = 1606906997.776, + stopped_reason = <<"Task stopped by user">>, + tags = [ + #ecs_tag{key = <<"test-key">>, value = <<"test-value">>} + ], task_arn = <<"arn:aws:ecs:us-east-1:012345678910:task/c09f0188-7f87-4b0f-bfc3-16296622b6fe">>, - task_definition_arn = <<"arn:aws:ecs:us-east-1:012345678910:task-definition/hello_world:10">> + task_definition_arn = <<"arn:aws:ecs:us-east-1:012345678910:task-definition/hello_world:10">>, + version = 3 } ] }}}) @@ -1895,6 +2013,64 @@ list_services_output_tests(_) -> ], output_tests(?_f(erlcloud_ecs:list_services([{out, record}])), Tests). +%% ListTagsForResource test based on the API examples: +%% https://docs.aws.amazon.com/systems-manager/latest/APIReference/API_ListTagsForResource.html +list_tags_for_resource_input_tests(_) -> + Tests = + [?_ecs_test( + {"ListTagsForResource example request", + ?_f(erlcloud_ecs:list_tags_for_resource("testArn")), " +{ + \"resourceArn\": \"testArn\" +} +" + }) + ], + Response = " +{ + \"tags\": [ + { + \"key\": \"test-key1\", + \"value\": \"test-value1\" + }, + { + \"key\": \"test-key2\", + \"value\": \"test-value2\" + } + ] +} +", + input_tests(Response, Tests). + +list_tags_for_resource_output_tests(_) -> + Tests = + [?_ecs_test( + {"ListTagsForResources example response", " +{ + \"tags\": [ + { + \"key\": \"test-key1\", + \"value\": \"test-value1\" + }, + { + \"key\": \"test-key2\", + \"value\": \"test-value2\" + } + ] +} +", + {ok, [#ecs_tag{ + key = <<"test-key1">>, + value = <<"test-value1">> + }, + #ecs_tag{ + key = <<"test-key2">>, + value = <<"test-value2">> + } + ]}}) + ], + output_tests(?_f(erlcloud_ecs:list_tags_for_resource("testArn")), Tests). + %% ListTaskDefinitionFamilies test based on the API examples: %% http://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_ListTaskDefinitionFamilies.html list_task_definition_families_input_tests(_) -> @@ -2416,6 +2592,7 @@ run_task_output_tests(_) -> ], \"desiredStatus\": \"RUNNING\", \"lastStatus\": \"PENDING\", + \"launchType\": \"EC2\", \"overrides\": { \"containerOverrides\": [ { @@ -2454,6 +2631,7 @@ run_task_output_tests(_) -> ], desired_status = <<"RUNNING">>, last_status = <<"PENDING">>, + launch_type = <<"EC2">>, overrides = #ecs_task_override{ container_overrides = [ #ecs_container_override{ @@ -2556,6 +2734,7 @@ start_task_output_tests(_) -> ], \"desiredStatus\": \"RUNNING\", \"lastStatus\": \"PENDING\", + \"launchType\": \"FARGATE\", \"overrides\": { \"containerOverrides\": [ { @@ -2594,6 +2773,7 @@ start_task_output_tests(_) -> ], desired_status = <<"RUNNING">>, last_status = <<"PENDING">>, + launch_type = <<"FARGATE">>, overrides = #ecs_task_override{ container_overrides = [ #ecs_container_override{ @@ -3240,8 +3420,8 @@ sort_json(V) -> %% verifies that the parameters in the body match the expected parameters -spec validate_body(binary(), expected_body()) -> ok. validate_body(Body, Expected) -> - Want = sort_json(jsx:decode(list_to_binary(Expected))), - Actual = sort_json(jsx:decode(Body)), + Want = sort_json(jsx:decode(list_to_binary(Expected), [{return_maps, false}])), + Actual = sort_json(jsx:decode(Body, [{return_maps, false}])), case Want =:= Actual of true -> ok; false -> @@ -3284,7 +3464,7 @@ input_tests(Response, Tests) -> %%%=================================================================== %% returns the mock of the erlcloud_httpc function output tests expect to be called. --spec output_expect(string()) -> fun(). +-spec output_expect(binary()) -> fun(). output_expect(Response) -> fun(_Url, post, _Headers, _Body, _Timeout, _Config) -> {ok, {{200, "OK"}, [], Response}} diff --git a/test/erlcloud_efs_tests.erl b/test/erlcloud_efs_tests.erl new file mode 100644 index 000000000..aae035898 --- /dev/null +++ b/test/erlcloud_efs_tests.erl @@ -0,0 +1,379 @@ +-module(erlcloud_efs_tests). + +-include_lib("eunit/include/eunit.hrl"). +-include("erlcloud_aws.hrl"). + +-define(TEST_AWS_CONFIG, #aws_config{ + access_key_id = "TEST_ACCESS_KEY_ID", + secret_access_key = "TEST_ACCESS_KEY", + security_token = "TEST_SECURITY_TOKEN" +}). + +api_test_() -> + { + foreach, + fun() -> meck:new(erlcloud_httpc) end, + fun(_) -> meck:unload() end, + [ + fun describe_file_systems_tests/1 + ] + }. + +describe_file_systems_tests(_) -> + [ + { + "DescribeFileSystems", + fun() -> + EfsUrl = "https://elasticfilesystem.us-east-1.amazonaws.com:443/2015-02-01/file-systems", + AwsConfig = ?TEST_AWS_CONFIG, + meck:expect( + erlcloud_httpc, + request, + fun(Url, Method, _Hdrs, Body, _Timeout, AwsCfg) when + Url =:= EfsUrl, Method =:= get, Body =:= <<>>, AwsCfg =:= AwsConfig + -> + ResponseContent = << + "{" + "\"FileSystems\":[" + "{" + "\"AvailabilityZoneId\":null," + "\"AvailabilityZoneName\":null," + "\"CreationTime\":1.7343072E9," + "\"CreationToken\":\"11111111-1111-1111-1111-111111111111\"," + "\"Encrypted\":true," + "\"FileSystemArn\":\"arn:aws:elasticfilesystem:us-east-1:111111111111:file-system/fs-00000000000000001\"," + "\"FileSystemId\":\"fs-00000000000000001\"," + "\"FileSystemProtection\":{" + "\"ReplicationOverwriteProtection\":\"ENABLED\"" + "}," + "\"KmsKeyId\":\"arn:aws:kms:us-east-1:111111111111:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa\"," + "\"LifeCycleState\":\"available\"," + "\"Name\":\"efs-001\"," + "\"NumberOfMountTargets\":0," + "\"OwnerId\":\"111111111111\"," + "\"PerformanceMode\":\"generalPurpose\"," + "\"ProvisionedThroughputInMibps\":null," + "\"SizeInBytes\":{" + "\"Timestamp\":null," + "\"Value\":6144," + "\"ValueInArchive\":0," + "\"ValueInIA\":0," + "\"ValueInStandard\":6144" + "}," + "\"Tags\":[" + "{" + "\"Key\":\"Name\"," + "\"Value\":\"efs-001\"" + "}" + "]," + "\"ThroughputMode\":\"elastic\"" + "}," + "{" + "\"AvailabilityZoneId\":null," + "\"AvailabilityZoneName\":null," + "\"CreationTime\":1.7343072E9," + "\"CreationToken\":\"22222222-2222-2222-2222-222222222222\"," + "\"Encrypted\":true," + "\"FileSystemArn\":\"arn:aws:elasticfilesystem:us-east-1:111111111111:file-system/fs-00000000000000002\"," + "\"FileSystemId\":\"fs-00000000000000002\"," + "\"FileSystemProtection\":{" + "\"ReplicationOverwriteProtection\":\"ENABLED\"" + "}," + "\"KmsKeyId\":\"arn:aws:kms:us-east-1:111111111111:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa\"," + "\"LifeCycleState\":\"available\"," + "\"Name\":\"efs-002\"," + "\"NumberOfMountTargets\":0," + "\"OwnerId\":\"111111111111\"," + "\"PerformanceMode\":\"generalPurpose\"," + "\"ProvisionedThroughputInMibps\":null," + "\"SizeInBytes\":{" + "\"Timestamp\":null," + "\"Value\":6144," + "\"ValueInArchive\":0," + "\"ValueInIA\":0," + "\"ValueInStandard\":6144" + "}," + "\"Tags\":[" + "{" + "\"Key\":\"Name\"," + "\"Value\":\"efs-002\"" + "}" + "]," + "\"ThroughputMode\":\"elastic\"" + "}" + "]," + "\"Marker\":null," + "\"NextMarker\":null" + "}" + >>, + {ok, {{200, "OK"}, [], ResponseContent}} + end + ), + Params = [], + Result = erlcloud_efs:describe_file_systems(AwsConfig, Params), + FileSystems = [ + [ + {<<"AvailabilityZoneId">>, null}, + {<<"AvailabilityZoneName">>, null}, + {<<"CreationTime">>, 1.7343072e9}, + {<<"CreationToken">>, <<"11111111-1111-1111-1111-111111111111">>}, + {<<"Encrypted">>, true}, + {<<"FileSystemArn">>, + <<"arn:aws:elasticfilesystem:us-east-1:111111111111:file-system/fs-00000000000000001">>}, + {<<"FileSystemId">>, <<"fs-00000000000000001">>}, + {<<"FileSystemProtection">>, [ + {<<"ReplicationOverwriteProtection">>, <<"ENABLED">>} + ]}, + {<<"KmsKeyId">>, + <<"arn:aws:kms:us-east-1:111111111111:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa">>}, + {<<"LifeCycleState">>, <<"available">>}, + {<<"Name">>, <<"efs-001">>}, + {<<"NumberOfMountTargets">>, 0}, + {<<"OwnerId">>, <<"111111111111">>}, + {<<"PerformanceMode">>, <<"generalPurpose">>}, + {<<"ProvisionedThroughputInMibps">>, null}, + {<<"SizeInBytes">>, [ + {<<"Timestamp">>, null}, + {<<"Value">>, 6144}, + {<<"ValueInArchive">>, 0}, + {<<"ValueInIA">>, 0}, + {<<"ValueInStandard">>, 6144} + ]}, + {<<"Tags">>, [[{<<"Key">>, <<"Name">>}, {<<"Value">>, <<"efs-001">>}]]}, + {<<"ThroughputMode">>, <<"elastic">>} + ], + [ + {<<"AvailabilityZoneId">>, null}, + {<<"AvailabilityZoneName">>, null}, + {<<"CreationTime">>, 1.7343072e9}, + {<<"CreationToken">>, <<"22222222-2222-2222-2222-222222222222">>}, + {<<"Encrypted">>, true}, + {<<"FileSystemArn">>, + <<"arn:aws:elasticfilesystem:us-east-1:111111111111:file-system/fs-00000000000000002">>}, + {<<"FileSystemId">>, <<"fs-00000000000000002">>}, + {<<"FileSystemProtection">>, [ + {<<"ReplicationOverwriteProtection">>, <<"ENABLED">>} + ]}, + {<<"KmsKeyId">>, + <<"arn:aws:kms:us-east-1:111111111111:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa">>}, + {<<"LifeCycleState">>, <<"available">>}, + {<<"Name">>, <<"efs-002">>}, + {<<"NumberOfMountTargets">>, 0}, + {<<"OwnerId">>, <<"111111111111">>}, + {<<"PerformanceMode">>, <<"generalPurpose">>}, + {<<"ProvisionedThroughputInMibps">>, null}, + {<<"SizeInBytes">>, [ + {<<"Timestamp">>, null}, + {<<"Value">>, 6144}, + {<<"ValueInArchive">>, 0}, + {<<"ValueInIA">>, 0}, + {<<"ValueInStandard">>, 6144} + ]}, + {<<"Tags">>, [[{<<"Key">>, <<"Name">>}, {<<"Value">>, <<"efs-002">>}]]}, + {<<"ThroughputMode">>, <<"elastic">>} + ] + ], + ?assertEqual({ok, FileSystems, undefined}, Result) + end + }, + { + "DescribeFileSystems [all]", + fun() -> + EfsUrl1 = "https://elasticfilesystem.us-east-1.amazonaws.com:443/2015-02-01/file-systems?MaxItems=1", + EfsUrl2 = "https://elasticfilesystem.us-east-1.amazonaws.com:443/2015-02-01/file-systems?Marker=TEST_NEXT_MARKER&MaxItems=1", + AwsConfig = ?TEST_AWS_CONFIG, + meck:expect( + erlcloud_httpc, + request, + fun + (Url, Method, _Hdrs, Body, _Timeout, AwsCfg) when + Url =:= EfsUrl1, Method =:= get, Body =:= <<>>, AwsCfg =:= AwsConfig + -> + ResponseContent = << + "{" + "\"FileSystems\":[" + "{" + "\"AvailabilityZoneId\":null," + "\"AvailabilityZoneName\":null," + "\"CreationTime\":1.7343072E9," + "\"CreationToken\":\"11111111-1111-1111-1111-111111111111\"," + "\"Encrypted\":true," + "\"FileSystemArn\":\"arn:aws:elasticfilesystem:us-east-1:111111111111:file-system/fs-00000000000000001\"," + "\"FileSystemId\":\"fs-00000000000000001\"," + "\"FileSystemProtection\":{" + "\"ReplicationOverwriteProtection\":\"ENABLED\"" + "}," + "\"KmsKeyId\":\"arn:aws:kms:us-east-1:111111111111:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa\"," + "\"LifeCycleState\":\"available\"," + "\"Name\":\"efs-001\"," + "\"NumberOfMountTargets\":0," + "\"OwnerId\":\"111111111111\"," + "\"PerformanceMode\":\"generalPurpose\"," + "\"ProvisionedThroughputInMibps\":null," + "\"SizeInBytes\":{" + "\"Timestamp\":null," + "\"Value\":6144," + "\"ValueInArchive\":0," + "\"ValueInIA\":0," + "\"ValueInStandard\":6144" + "}," + "\"Tags\":[" + "{" + "\"Key\":\"Name\"," + "\"Value\":\"efs-001\"" + "}" + "]," + "\"ThroughputMode\":\"elastic\"" + "}" + "]," + "\"Marker\":null," + "\"NextMarker\":\"TEST_NEXT_MARKER\"" + "}" + >>, + {ok, {{200, "OK"}, [], ResponseContent}}; + (Url, Method, _Hdrs, Body, _Timeout, AwsCfg) when + Url =:= EfsUrl2, Method =:= get, Body =:= <<>>, AwsCfg =:= AwsConfig + -> + ResponseContent = << + "{" + "\"FileSystems\":[" + "{" + "\"AvailabilityZoneId\":null," + "\"AvailabilityZoneName\":null," + "\"CreationTime\":1.7343072E9," + "\"CreationToken\":\"22222222-2222-2222-2222-222222222222\"," + "\"Encrypted\":true," + "\"FileSystemArn\":\"arn:aws:elasticfilesystem:us-east-1:111111111111:file-system/fs-00000000000000002\"," + "\"FileSystemId\":\"fs-00000000000000002\"," + "\"FileSystemProtection\":{" + "\"ReplicationOverwriteProtection\":\"ENABLED\"" + "}," + "\"KmsKeyId\":\"arn:aws:kms:us-east-1:111111111111:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa\"," + "\"LifeCycleState\":\"available\"," + "\"Name\":\"efs-002\"," + "\"NumberOfMountTargets\":0," + "\"OwnerId\":\"111111111111\"," + "\"PerformanceMode\":\"generalPurpose\"," + "\"ProvisionedThroughputInMibps\":null," + "\"SizeInBytes\":{" + "\"Timestamp\":null," + "\"Value\":6144," + "\"ValueInArchive\":0," + "\"ValueInIA\":0," + "\"ValueInStandard\":6144" + "}," + "\"Tags\":[" + "{" + "\"Key\":\"Name\"," + "\"Value\":\"efs-002\"" + "}" + "]," + "\"ThroughputMode\":\"elastic\"" + "}" + "]," + "\"Marker\":\"TEST_NEXT_MARKER\"," + "\"NextMarker\":null" + "}" + >>, + {ok, {{200, "OK"}, [], ResponseContent}} + end + ), + Params = [{<<"MaxItems">>, 1}], + Result = erlcloud_efs:describe_file_systems_all(AwsConfig, Params), + FileSystems = [ + [ + {<<"AvailabilityZoneId">>, null}, + {<<"AvailabilityZoneName">>, null}, + {<<"CreationTime">>, 1.7343072e9}, + {<<"CreationToken">>, <<"11111111-1111-1111-1111-111111111111">>}, + {<<"Encrypted">>, true}, + {<<"FileSystemArn">>, + <<"arn:aws:elasticfilesystem:us-east-1:111111111111:file-system/fs-00000000000000001">>}, + {<<"FileSystemId">>, <<"fs-00000000000000001">>}, + {<<"FileSystemProtection">>, [ + {<<"ReplicationOverwriteProtection">>, <<"ENABLED">>} + ]}, + {<<"KmsKeyId">>, + <<"arn:aws:kms:us-east-1:111111111111:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa">>}, + {<<"LifeCycleState">>, <<"available">>}, + {<<"Name">>, <<"efs-001">>}, + {<<"NumberOfMountTargets">>, 0}, + {<<"OwnerId">>, <<"111111111111">>}, + {<<"PerformanceMode">>, <<"generalPurpose">>}, + {<<"ProvisionedThroughputInMibps">>, null}, + {<<"SizeInBytes">>, [ + {<<"Timestamp">>, null}, + {<<"Value">>, 6144}, + {<<"ValueInArchive">>, 0}, + {<<"ValueInIA">>, 0}, + {<<"ValueInStandard">>, 6144} + ]}, + {<<"Tags">>, [[{<<"Key">>, <<"Name">>}, {<<"Value">>, <<"efs-001">>}]]}, + {<<"ThroughputMode">>, <<"elastic">>} + ], + [ + {<<"AvailabilityZoneId">>, null}, + {<<"AvailabilityZoneName">>, null}, + {<<"CreationTime">>, 1.7343072e9}, + {<<"CreationToken">>, <<"22222222-2222-2222-2222-222222222222">>}, + {<<"Encrypted">>, true}, + {<<"FileSystemArn">>, + <<"arn:aws:elasticfilesystem:us-east-1:111111111111:file-system/fs-00000000000000002">>}, + {<<"FileSystemId">>, <<"fs-00000000000000002">>}, + {<<"FileSystemProtection">>, [ + {<<"ReplicationOverwriteProtection">>, <<"ENABLED">>} + ]}, + {<<"KmsKeyId">>, + <<"arn:aws:kms:us-east-1:111111111111:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa">>}, + {<<"LifeCycleState">>, <<"available">>}, + {<<"Name">>, <<"efs-002">>}, + {<<"NumberOfMountTargets">>, 0}, + {<<"OwnerId">>, <<"111111111111">>}, + {<<"PerformanceMode">>, <<"generalPurpose">>}, + {<<"ProvisionedThroughputInMibps">>, null}, + {<<"SizeInBytes">>, [ + {<<"Timestamp">>, null}, + {<<"Value">>, 6144}, + {<<"ValueInArchive">>, 0}, + {<<"ValueInIA">>, 0}, + {<<"ValueInStandard">>, 6144} + ]}, + {<<"Tags">>, [[{<<"Key">>, <<"Name">>}, {<<"Value">>, <<"efs-002">>}]]}, + {<<"ThroughputMode">>, <<"elastic">>} + ] + ], + ?assertEqual({ok, FileSystems}, Result) + end + }, + { + "DescribeFileSystems [filesystem not found]", + fun() -> + AwsConfig = ?TEST_AWS_CONFIG, + EfsUrl = "https://elasticfilesystem.us-east-1.amazonaws.com:443/2015-02-01/file-systems?FileSystemId=fs-1234bad", + meck:expect( + erlcloud_httpc, + request, + fun(Url, Method, _Hdrs, Body, _Timeout, AwsCfg) when + Url =:= EfsUrl, Method =:= get, Body =:= <<>>, AwsCfg =:= AwsConfig + -> + ResponseContent = << + "{" + "\"ErrorCode\":\"FileSystemNotFound\"," + "\"Message\":\"File system 'fs-1234bad' does not exist.\"" + "}" + >>, + ResponseHeaders = [ + {"x-amzn-errortype", "FileSystemNotFound:"}, + {"content-type", "application/json"} + ], + {ok, {{404, "Not Found"}, ResponseHeaders, ResponseContent}} + end + ), + Params = [{<<"FileSystemId">>, <<"fs-1234bad">>}], + Result = erlcloud_efs:describe_file_systems(AwsConfig, Params), + ErrorType = <<"FileSystemNotFound">>, + ErrorMessage = <<"File system 'fs-1234bad' does not exist.">>, + ?assertEqual({error, {ErrorType, ErrorMessage}}, Result) + end + } + ]. diff --git a/test/erlcloud_elb_tests.erl b/test/erlcloud_elb_tests.erl new file mode 100644 index 000000000..335dfbb9b --- /dev/null +++ b/test/erlcloud_elb_tests.erl @@ -0,0 +1,73 @@ +%% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- +-module(erlcloud_elb_tests). +-include_lib("eunit/include/eunit.hrl"). + + +%%%=================================================================== +%%% Test entry points +%%%=================================================================== + +start() -> + meck:new(erlcloud_aws, [passthrough]), + ok. + + +stop(_) -> + meck:unload(erlcloud_aws). + + +elb_tags_test_() -> + {foreach, + fun start/0, + fun stop/1, + [ + {"Request describe_tags.", + fun() -> + meck:expect(erlcloud_aws, aws_request_xml4, fun(_, _, _, _, _, _) -> + {ok, describe_tags_response_xmerl()} end), + LoadBalancerName = "vvorobyov-classic", + Resp = erlcloud_elb:describe_tags( + [LoadBalancerName], + erlcloud_aws:default_config()), + ?assertMatch( + {ok, [ + [ + {load_balancer_name, LoadBalancerName}, + {tags, [[{value, _}, {key, _}], [{value, _}, {key, _}]]} + ] + ]}, + Resp + ) + end} + ] + }. + + +%%%=================================================================== +%%% Helpers +%%%=================================================================== + +describe_tags_response_xmerl() -> + XML = " + + + + vvorobyov-classic + + + tag-value-2 + tag-key-2 + + + tag-value-2 + tag-key-1 + + + + + + + 75bbeca1-e357-11e8-b2f4-735be4940a86 + +", + element(1, xmerl_scan:string(XML)). diff --git a/test/erlcloud_emr_tests.erl b/test/erlcloud_emr_tests.erl index 30f76d0d7..3354158e3 100644 --- a/test/erlcloud_emr_tests.erl +++ b/test/erlcloud_emr_tests.erl @@ -189,7 +189,7 @@ input_test(ResponseBody, {Line, {Description, Fun, ExpectedParams}}) -> erlcloud_httpc, request, fun(_Url, post, _Headers, RequestBody, _Timeout, _Config) -> - ActualParams = jsx:decode(RequestBody), + ActualParams = jsx:decode(RequestBody, [{return_maps, false}]), ?assertEqual(sort_json(ExpectedParams), sort_json(ActualParams)), {ok, {{200, "OK"}, [], ResponseBody}} end diff --git a/test/erlcloud_iam_tests.erl b/test/erlcloud_iam_tests.erl index 066645539..fa948c893 100644 --- a/test/erlcloud_iam_tests.erl +++ b/test/erlcloud_iam_tests.erl @@ -38,6 +38,8 @@ iam_api_test_() -> fun get_group_policy_output_tests/1, fun get_login_profile_input_tests/1, fun get_login_profile_output_tests/1, + fun get_role_input_tests/1, + fun get_role_output_tests/1, fun get_role_policy_input_tests/1, fun get_role_policy_output_tests/1, fun get_user_policy_input_tests/1, @@ -70,6 +72,14 @@ iam_api_test_() -> fun list_role_policies_input_tests/1, fun list_role_policies_output_tests/1, fun list_role_policies_all_output_tests/1, + fun get_server_certificate_input_tests/1, + fun get_server_certificate_output_tests/1, + fun list_server_certificates_input_tests/1, + fun list_server_certificates_output_tests/1, + fun list_server_certificates_all_output_tests/1, + fun list_server_certificate_tags_input_tests/1, + fun list_server_certificate_tags_output_tests/1, + fun list_server_certificate_tags_all_output_tests/1, fun list_instance_profiles_input_tests/1, fun list_instance_profiles_output_tests/1, fun list_instance_profiles_all_output_tests/1, @@ -99,7 +109,9 @@ iam_api_test_() -> fun simulate_custom_policy_input_test/1, fun simulate_custom_policy_output_test/1, fun simulate_principal_policy_input_test/1, - fun simulate_principal_policy_output_test/1 + fun simulate_principal_policy_output_test/1, + fun list_virtual_mfa_devices_input_test/1, + fun list_virtual_mfa_devices_output_test/1 ]}. start() -> @@ -196,7 +208,7 @@ input_tests(Response, Tests) -> %%%=================================================================== %% returns the mock of the erlcloud_httpc function output tests expect to be called. --spec output_expect(string()) -> fun(). +-spec output_expect(string()) -> meck:ret_spec(). output_expect(Response) -> meck:val({ok, {{200, "OK"}, [], list_to_binary(Response)}}). @@ -205,7 +217,7 @@ output_expect_seq(Responses) -> meck:seq([{ok, {{200, "OK"}, [], list_to_binary(Response)}} || Response <- Responses]). %% output_test converts an output_test specifier into an eunit test generator --type output_test_spec() :: {pos_integer(), {string(), term()} | {string(), string(), term()}}. +-type output_test_spec() :: {pos_integer(), {string(), term()} | {string(), string() | [string()], term()}}. -spec output_test(fun(), output_test_spec(), fun()) -> tuple(). output_test(Fun, {Line, {Description, Response, Result}}, OutputFun) -> {Description, @@ -415,6 +427,102 @@ get_account_summary_output_tests(_) -> ], output_tests(?_f(erlcloud_iam:get_account_summary()), Tests). +-define(LIST_VIRTUAL_MFA_DEVICES_RESP, + " + + false + + + arn:aws:iam::123456789012:mfa/MFAdeviceName + + + arn:aws:iam::123456789012:mfa/RootMFAdeviceName + 2011-10-20T20:49:03Z + + 123456789012 + arn:aws:iam::123456789012:root + 2009-10-13T22:00:36Z + + + + arn:aws:iam:::mfa/ExampleUserMFAdeviceName + 2011-10-31T20:45:02Z + + AIDEXAMPLE4EXAMPLEXYZ + / + ExampleUser + arn:aws:iam::111122223333:user/ExampleUser + 2011-07-01T17:23:07Z + + + + + + b61ce1b1-0401-11e1-b2f8-2dEXAMPLEbfc + + "). + +list_virtual_mfa_devices_input_test(_) -> + Tests = + [?_iam_test( + {"Test returning account registered MFA devices.", + ?_f(erlcloud_iam:list_virtual_mfa_devices()), + [ + {"Action", "ListVirtualMFADevices"} + ]}) + ], + input_tests(?LIST_VIRTUAL_MFA_DEVICES_RESP, Tests). + +list_virtual_mfa_devices_output_test(_) -> + Tests = [?_iam_test( + {"This returns the registered MFA devices", + ?LIST_VIRTUAL_MFA_DEVICES_RESP, + {ok,[ + [ + {user,[]}, + {enable_date,undefined}, + {serial_number,"arn:aws:iam::123456789012:mfa/MFAdeviceName"} + ], + [ + {user, + [ + [ + {arn,"arn:aws:iam::123456789012:root"}, + {create_date,{{2009,10,13},{22,0,36}}}, + {group_list,[]}, + {path,[]}, + {user_id,"123456789012"}, + {user_name,[]}, + {user_policy_list,[]} + ] + ] + }, + {enable_date,{{2011,10,20},{20,49,3}}}, + {serial_number,"arn:aws:iam::123456789012:mfa/RootMFAdeviceName"} + ], + [ + {user, + [ + [ + {arn,"arn:aws:iam::111122223333:user/ExampleUser"}, + {create_date,{{2011,7,1},{17,23,7}}}, + {group_list,[]}, + {path,"/"}, + {user_id,"AIDEXAMPLE4EXAMPLEXYZ"}, + {user_name,"ExampleUser"}, + {user_policy_list,[]} + ] + ] + }, + {enable_date,{{2011,10,31},{20,45,2}}}, + {serial_number,"arn:aws:iam:::mfa/ExampleUserMFAdeviceName"} + ] + ]} + }) + ], + output_tests(?_f(erlcloud_iam:list_virtual_mfa_devices()), Tests). + + -define(GET_ACCOUNT_PASSWORD_POLICY_RESP, " @@ -584,6 +692,55 @@ get_login_profile_output_tests(_) -> ], output_tests(?_f(erlcloud_iam:get_login_profile("Bob")), Tests). +-define(GET_ROLE_RESP, + " + + + /application_abc/component_xyz/ + arn:aws:iam::123456789012:role/application_abc/component_xyz/S3Access + S3Access + {\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":[\"ec2.amazonaws.com\"]},\"Action\":[\"sts:AssumeRole\"]}]} + 2012-05-08T23:34:01Z + AROADBQP57FF2AEXAMPLE + + 2013-05-08T23:34:01Z + us-east-1 + + + + "). + +get_role_input_tests(_) -> + Tests = + [?_iam_test( + {"Test returning role.", + ?_f(erlcloud_iam:get_role("test")), + [ + {"Action", "GetRole"}, + {"RoleName", "test"} + ]}) + ], + + input_tests(?GET_ROLE_RESP, Tests). + +get_role_output_tests(_) -> + Tests = [?_iam_test( + {"This returns the role", + ?GET_ROLE_RESP, + {ok, [{role_last_used, + [[{region,"us-east-1"}, + {last_used_date,{{2013,5,8},{23,34,1}}}]]}, + {path,"/application_abc/component_xyz/"}, + {role_name,"S3Access"}, + {role_id,"AROADBQP57FF2AEXAMPLE"}, + {assume_role_policy_doc, + "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":[\"ec2.amazonaws.com\"]},\"Action\":[\"sts:AssumeRole\"]}]}"}, + {create_date,{{2012,5,8},{23,34,1}}}, + {arn, "arn:aws:iam::123456789012:role/application_abc/component_xyz/S3Access"}]} + }) + ], + output_tests(?_f(erlcloud_iam:get_role("test")), Tests). + -define(GET_ROLE_POLICY_RESP, " @@ -825,7 +982,7 @@ get_access_key_last_used_output_tests() -> {access_key_last_used_service_name, "s3"}] }}) ], - output_tests(?_f(erlcloud_iam:get_access_key_last_used_output("KEYID")), Tests). + output_tests(?_f(erlcloud_iam:get_access_key_last_used("KEYID")), Tests). %% ListUsers test based on the API examples: %% http://docs.aws.amazon.com/IAM/latest/APIReference/API_ListUsers.html @@ -1569,6 +1726,295 @@ list_role_policies_all_output_tests(_) -> ], output_tests_seq(?_f(erlcloud_iam:list_role_policies_all("S3Access")), Tests). +-define(SERVER_CERTIFICATE_BODY, +"-----BEGIN CERTIFICATE----- +MIICdzCCAeCgAwIBAgIGANc+Ha2wMA0GCSqGSIb3DQEBBQUAMFMxCzAJBgNVBAYT +AlVTMRMwEQYDVQQKEwpBbWF6b24uY29tMQwwCgYDVQQLEwNBV1MxITAfBgNVBAMT +GEFXUyBMaW1pdGVkLUFzc3VyYW5jZSBDQTAeFw0wOTAyMDQxNzE5MjdaFw0xMDAy +MDQxNzE5MjdaMFIxCzAJBgNVBAYTAlVTMRMwEQYDVQQKEwpBbWF6b24uY29tMRcw +FQYDVQQLEw5BV1MtRGV2ZWxvcGVyczEVMBMGA1UEAxMMNTdxNDl0c3ZwYjRtMIGf +MA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCpB/vsOwmT/O0td1RqzKjttSBaPjbr +dqwNe9BrOyB08fw2+Ch5oonZYXfGUrT6mkYXH5fQot9HvASrzAKHO596FdJA6DmL +ywdWe1Oggk7zFSXO1Xv+3vPrJtaYxYo3eRIp7w80PMkiOv6M0XK8ubcTouODeJbf +suDqcLnLDxwsvwIDAQABo1cwVTAOBgNVHQ8BAf8EBAMCBaAwFgYDVR0lAQH/BAww +CgYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQULGNaBphBumaKbDRK +CAi0mH8B3mowDQYJKoZIhvcNAQEFBQADgYEAuKxhkXaCLGcqDuweKtO/AEw9ZePH +wr0XqsaIK2HZboqruebXEGsojK4Ks0WzwgrEynuHJwTn760xe39rSqXWIOGrOBaX +wFpWHVjTFMKk+tSDG1lssLHyYWWdFFU4AnejRGORJYNaRHgVTKjHphc5jEhHm0BX +AEaHzTpmEXAMPLE= +-----END CERTIFICATE-----"). + +-define(GET_SERVER_CERTIFICATE_RESP, +" + + + + ProdServerCert + /company/servercerts/ + arn:aws:iam::123456789012:server-certificate/company/servercerts/ProdServerCert + 2010-05-08T01:02:03.004Z + ASCACKCEVSQ6C2EXAMPLE + 2012-05-08T01:02:03.004Z + + " + ++ ?SERVER_CERTIFICATE_BODY ++ + " + + + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + +"). + +get_server_certificate_input_tests(_) -> + Tests = [ + ?_iam_test({ + "Test returning a server certificate.", + ?_f(erlcloud_iam:get_server_certificate("test")), + [{"Action", "GetServerCertificate"}, {"ServerCertificateName", "test"}] + }) + ], + input_tests(?GET_SERVER_CERTIFICATE_RESP, Tests). + +get_server_certificate_output_tests(_) -> + Tests = [ + ?_iam_test({ + "This returns the server certificate", + ?GET_SERVER_CERTIFICATE_RESP, + {ok, [ + {server_certificate_metadata, [ + {server_certificate_name, "ProdServerCert"}, + {server_certificate_id, "ASCACKCEVSQ6C2EXAMPLE"}, + {path, "/company/servercerts/"}, + {arn, "arn:aws:iam::123456789012:server-certificate/company/servercerts/ProdServerCert"}, + {upload_date, {{2010,5,8}, {1,2,3}}}, + {expiration, {{2012,5,8}, {1,2,3}}} + ]}, + {certificate_body, ?SERVER_CERTIFICATE_BODY} + ]} + }) + ], + output_tests(?_f(erlcloud_iam:get_server_certificate("test")), Tests). + +-define(LIST_SERVER_CERTIFICATES_RESP, +" + + false + + + ProdServerCert + /company/servercerts/ + arn:aws:iam::123456789012:server-certificate/company/servercerts/ProdServerCert + 2010-05-08T01:02:03.004Z + ASCACKCEVSQ6C2EXAMPLE + 2012-05-08T01:02:03.004Z + + + + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + +"). + +list_server_certificates_input_tests(_) -> + Tests = [ + ?_iam_test({ + "Test returning a list of server certificates.", + ?_f(erlcloud_iam:list_server_certificates("test")), + [{"Action", "ListServerCertificates"}, {"PathPrefix", "test"}] + }) + ], + input_tests(?LIST_SERVER_CERTIFICATES_RESP, Tests). + +list_server_certificates_output_tests(_) -> + Tests = [ + ?_iam_test({ + "Test returning a list of server certificates.", + ?LIST_SERVER_CERTIFICATES_RESP, + {ok, [ + [ + {server_certificate_name, "ProdServerCert"}, + {server_certificate_id, "ASCACKCEVSQ6C2EXAMPLE"}, + {path, "/company/servercerts/"}, + {arn, "arn:aws:iam::123456789012:server-certificate/company/servercerts/ProdServerCert"}, + {upload_date, {{2010,5,8}, {1,2,3}}}, + {expiration, {{2012,5,8}, {1,2,3}}} + ] + ]} + }) + ], + output_tests(?_f(erlcloud_iam:list_server_certificates("test")), Tests). + +list_server_certificates_all_output_tests(_) -> + Tests = [ + ?_iam_test({ + "Test returning all server certificates", + [ + " + + true + marker + + + ProdServerCert + /company/servercerts/ + arn:aws:iam::123456789012:server-certificate/company/servercerts/ProdServerCert + 2010-05-08T01:02:03.004Z + ASCACKCEVSQ6CEXAMPLE1 + 2012-05-08T01:02:03.004Z + + + + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + + ", + " + + false + + + TestServerCert + /company/servercerts/ + arn:aws:iam::123456789012:server-certificate/company/servercerts/TestServerCert + 2010-05-08T03:01:02.004Z + ASCACKCEVSQ6CEXAMPLE2 + 2012-05-08T03:01:02.004Z + + + + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + + " + ], + {ok, [ + [ + {server_certificate_name, "ProdServerCert"}, + {server_certificate_id, "ASCACKCEVSQ6CEXAMPLE1"}, + {path, "/company/servercerts/"}, + {arn, "arn:aws:iam::123456789012:server-certificate/company/servercerts/ProdServerCert"}, + {upload_date, {{2010,5,8}, {1,2,3}}}, + {expiration, {{2012,5,8}, {1,2,3}}} + ], + [ + {server_certificate_name, "TestServerCert"}, + {server_certificate_id, "ASCACKCEVSQ6CEXAMPLE2"}, + {path, "/company/servercerts/"}, + {arn, "arn:aws:iam::123456789012:server-certificate/company/servercerts/TestServerCert"}, + {upload_date, {{2010,5,8}, {3,1,2}}}, + {expiration, {{2012,5,8}, {3,1,2}}} + ] + ]} + }) + ], + output_tests_seq(?_f(erlcloud_iam:list_server_certificates_all("test")), Tests). + +-define(SERVER_CERTIFICATE_TAGS_RESP, + " + + false + + + Dept + 12345 + + + Team + Accounting + + + + + EXAMPLE8-90ab-cdef-fedc-ba987EXAMPLE + + " +). + +list_server_certificate_tags_input_tests(_) -> + Tests = [ + ?_iam_test({ + "Test input for returning server certificate tags", + ?_f(erlcloud_iam:list_server_certificate_tags("test")), + [{"Action", "ListServerCertificateTags"}, {"ServerCertificateName", "test"}] + }) + ], + input_tests(?SERVER_CERTIFICATE_TAGS_RESP, Tests). + +list_server_certificate_tags_output_tests(_) -> + Tests = [ + ?_iam_test({ + "Test returning server certificate tags", + ?SERVER_CERTIFICATE_TAGS_RESP, + {ok, [ + [{key, "Dept"}, + {value, "12345"}], + [{key, "Team"}, + {value, "Accounting"}] + ]} + }) + ], + output_tests(?_f(erlcloud_iam:list_server_certificate_tags("test")), Tests). + +-define(SERVER_CERTIFICATE_TAGS_ALL_RESP, [ + " + + true + marker + + + Dept + 12345 + + + Team + Accounting + + + + + EXAMPLE8-90ab-cdef-fedc-ba987EXAMPLE + + ", + " + + false + + + Dept + 00001 + + + Team + Engineering + + + + + EXAMPLE8-90ab-cdef-fedc-ba987EXAMPLE + + " +]). + +list_server_certificate_tags_all_output_tests(_) -> + Tests = [ + ?_iam_test({ + "Test returning all pages of server certificate tags", + ?SERVER_CERTIFICATE_TAGS_ALL_RESP, + {ok, [ + [{key, "Dept"}, + {value, "12345"}], + [{key, "Team"}, + {value, "Accounting"}], + [{key, "Dept"}, + {value, "00001"}], + [{key, "Team"}, + {value, "Engineering"}] + ]} + }) + ], + output_tests_seq(?_f(erlcloud_iam:list_server_certificate_tags_all("test")), Tests). + -define(LIST_INSTANCE_PROFILES_RESP, " @@ -1894,6 +2340,247 @@ get_instance_profile_output_tests(_) ->
    "). +-define(GET_ACCOUNT_AUTHORIZATION_DETAILS_RESP_TRUNC, + " + + true + + + + Admins + + + AIDACKCEVSQ6C2EXAMPLE + / + Alice + arn:aws:iam::123456789012:user/Alice + 2013-10-14T18:32:24Z + + + + Admins + + + + + DenyBillingAndIAMPolicy + {\"Version\":\"2012-10-17\",\"Statement\":{\"Effect\":\"Deny\",\"Action\":[\"aws-portal:*\",\"iam:*\"],\"Resource\":\"*\"}} + + + AIDACKCEVSQ6C3EXAMPLE + / + Bob + arn:aws:iam::123456789012:user/Bob + 2013-10-14T18:32:25Z + + + + Dev + + + AIDACKCEVSQ6C4EXAMPLE + / + Charlie + arn:aws:iam::123456789012:user/Charlie + 2013-10-14T18:33:56Z + + + + Dev + + + AIDACKCEVSQ6C5EXAMPLE + / + Danielle + arn:aws:iam::123456789012:user/Danielle + 2013-10-14T18:33:56Z + + + + Finance + + + AIDACKCEVSQ6C6EXAMPLE + / + Elaine + arn:aws:iam::123456789012:user/Elaine + 2013-10-14T18:57:48Z + + + EXAMPLEkakv9BCuUNFDtxWSyfzetYwEx2ADc8dnzfvERF5S6YMvXKx41t6gCl/eeaCX3Jo94/bKqezEAg8TEVS99EKFLxm3jtbpl25FDWEXAMPLE + + + AIDACKCEVSQ6C7EXAMPLE + + + AdministratorAccess + arn:aws:iam::aws:policy/AdministratorAccess + + + Admins + / + arn:aws:iam::123456789012:group/Admins + 2013-10-14T18:32:24Z + + + + AIDACKCEVSQ6C8EXAMPLE + + + PowerUserAccess + arn:aws:iam::aws:policy/PowerUserAccess + + + Dev + / + arn:aws:iam::123456789012:group/Dev + 2013-10-14T18:33:55Z + + + + AIDACKCEVSQ6C9EXAMPLE + + Finance + / + arn:aws:iam::123456789012:group/Finance + 2013-10-14T18:57:48Z + + + policygen-201310141157 + {\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"aws-portal:*\"],\"Sid\":\"Stmt1381777017000\",\"Resource\":[\"*\"],\"Effect\":\"Allow\"}]} + + + + + + + + + + AmazonS3FullAccess + arn:aws:iam::aws:policy/AmazonS3FullAccess + + + AmazonDynamoDBFullAccess + arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess + + + + + EC2role + + + / + arn:aws:iam::123456789012:role/EC2role + EC2role + {\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"ec2.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]} + 2014-07-30T17:09:20Z + AROAFP4BKI7Y7TEXAMPLE + + 2019-11-20T17:09:20Z + us-east-1 + + + + / + arn:aws:iam::123456789012:instance-profile/EC2role + AIPAFFYRBHWXW2EXAMPLE + 2014-07-30T17:09:20Z + + + / + arn:aws:iam::123456789012:role/EC2role + EC2role + {\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"ec2.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]} + 2014-07-30T17:09:20Z + AROAFP4BKI7Y7TEXAMPLE + + + + create-update-delete-set-managed-policies + v1 + ANPAJ2UCCR6DPCEXAMPLE + / + + + {\"Version\":\"2012-10-17\",\"Statement\":{\"Effect\":\"Allow\",\"Action\":[\"iam:CreatePolicy\",\"iam:CreatePolicyVersion\",\"iam:DeletePolicy\",\"iam:DeletePolicyVersion\",\"iam:GetPolicy\",\"iam:GetPolicyVersion\",\"iam:ListPolicies\",\"iam:ListPolicyVersions\",\"iam:SetDefaultPolicyVersion\"],\"Resource\":\"*\"}} + true + v1 + 2015-02-06T19:58:34Z + + + + arn:aws:iam::123456789012:policy/create-update-delete-set-managed-policies + + 1 + 2015-02-06T19:58:34Z + true + 2015-02-06T19:58:34Z + + + S3-read-only-specific-bucket + v1 + ANPAJ4AE5446DAEXAMPLE + / + + + {\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":[\"s3:Get*\",\"s3:List*\"],\"Resource\":[\"arn:aws:s3:::example-bucket\",\"arn:aws:s3:::example-bucket/*\"]}]} + true + v1 + 2015-01-21T21:39:41Z + + + arn:aws:iam::123456789012:policy/S3-read-only-specific-bucket + 1 + 2015-01-21T21:39:41Z + true + 2015-01-21T23:39:41Z + + + AWSOpsWorksRole + v1 + ANPAE376NQ77WV6KGJEBE + /service-role/ + + + {\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":[\"cloudwatch:GetMetricStatistics\",\"ec2:DescribeAccountAttributes\",\"ec2:DescribeAvailabilityZones\",\"ec2:DescribeInstances\",\"ec2:DescribeKeyPairs\",\"ec2:DescribeSecurityGroups\",\"ec2:DescribeSubnets\",\"ec2:DescribeVpcs\",\"elasticloadbalancing:DescribeInstanceHealth\",\"elasticloadbalancing:DescribeLoadBalancers\",\"iam:GetRolePolicy\",\"iam:ListInstanceProfiles\",\"iam:ListRoles\",\"iam:ListUsers\",\"iam:PassRole\",\"opsworks:*\",\"rds:*\"],\"Resource\":[\"*\"]}]} + true + v1 + 2014-12-10T22:57:47Z + + + arn:aws:iam::aws:policy/service-role/AWSOpsWorksRole + 1 + 2015-02-06T18:41:27Z + true + 2015-02-06T18:41:27Z + + + AmazonEC2FullAccess + v1 + ANPAE3QWE5YT46TQ34WLG + / + + + {\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":\"ec2:*\",\"Effect\":\"Allow\",\"Resource\":\"*\"},{\"Effect\":\"Allow\",\"Action\":\"elasticloadbalancing:*\",\"Resource\":\"*\"},{\"Effect\":\"Allow\",\"Action\":\"cloudwatch:*\",\"Resource\":\"*\"},{\"Effect\":\"Allow\",\"Action\":\"autoscaling:*\",\"Resource\":\"*\"}]} + true + v1 + 2014-10-30T20:59:46Z + + + arn:aws:iam::aws:policy/AmazonEC2FullAccess + 1 + 2015-02-06T18:40:15Z + true + 2015-02-06T18:40:15Z + + + + + 92e79ae7-7399-11e4-8c85-4b53eEXAMPLE + + "). + get_account_authorization_details_input_tests(_) -> Tests = [?_iam_test( @@ -1901,6 +2588,13 @@ get_account_authorization_details_input_tests(_) -> ?_f(erlcloud_iam:get_account_authorization_details()), [ {"Action", "GetAccountAuthorizationDetails"} + ]}), + ?_iam_test( + {"Test returning the authorization details.", + ?_f(erlcloud_iam:get_account_authorization_details([{"Marker", "XXX"}])), + [ + {"Action", "GetAccountAuthorizationDetails"}, + {"Marker", "XXX"} ]}) ], @@ -2003,6 +2697,95 @@ get_account_authorization_details_output_tests(_) -> {user_id,"AIDACKCEVSQ6C6EXAMPLE"}, {user_name,"Elaine"}, {user_policy_list,[]}]]}]} + }), + ?_iam_test( + {"This returns the authorization details (truncated)", + ?GET_ACCOUNT_AUTHORIZATION_DETAILS_RESP_TRUNC, + {ok,[{roles, + [[{arn,"arn:aws:iam::123456789012:role/EC2role"}, + {assume_role_policy_document, + "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"ec2.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}"}, + {create_date,{{2014,7,30},{17,9,20}}}, + {instance_profiles, + [[{instance_profile_id,"AIPAFFYRBHWXW2EXAMPLE"}, + {roles, + [[{path,"/"}, + {role_name,"EC2role"}, + {role_id,"AROAFP4BKI7Y7TEXAMPLE"}, + {assume_role_policy_doc, + "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"ec2.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}"}, + {create_date,{{2014,7,30},{17,9,20}}}, + {arn, "arn:aws:iam::123456789012:role/EC2role"}]]}, + {instance_profile_name,"EC2role"}, + {path,"/"}, + {arn,"arn:aws:iam::123456789012:instance-profile/EC2role"}, + {create_date,{{2014,7,30},{17,9,20}}}]]}, + {path,"/"}, + {role_id,"AROAFP4BKI7Y7TEXAMPLE"}, + {role_name,"EC2role"}, + {role_policy_list,[]}]]}, + {groups, + [[{arn,"arn:aws:iam::123456789012:group/Admins"}, + {create_date,{{2013,10,14},{18,32,24}}}, + {group_id,"AIDACKCEVSQ6C7EXAMPLE"}, + {group_name,"Admins"}, + {group_policy_list,[]}, + {path,"/"}], + [{arn,"arn:aws:iam::123456789012:group/Dev"}, + {create_date,{{2013,10,14},{18,33,55}}}, + {group_id,"AIDACKCEVSQ6C8EXAMPLE"}, + {group_name,"Dev"}, + {group_policy_list,[]}, + {path,"/"}], + [{arn,"arn:aws:iam::123456789012:group/Finance"}, + {create_date,{{2013,10,14},{18,57,48}}}, + {group_id,"AIDACKCEVSQ6C9EXAMPLE"}, + {group_name,"Finance"}, + {group_policy_list, + [[{policy_document, + "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"aws-portal:*\"],\"Sid\":\"Stmt1381777017000\",\"Resource\":[\"*\"],\"Effect\":\"Allow\"}]}"}, + {policy_name,"policygen-201310141157"}]]}, + {path,"/"}]]}, + {users, + [[{arn,"arn:aws:iam::123456789012:user/Alice"}, + {create_date,{{2013,10,14},{18,32,24}}}, + {group_list,[[{group_name,"Admins"}]]}, + {path,"/"}, + {user_id,"AIDACKCEVSQ6C2EXAMPLE"}, + {user_name,"Alice"}, + {user_policy_list,[]}], + [{arn,"arn:aws:iam::123456789012:user/Bob"}, + {create_date,{{2013,10,14},{18,32,25}}}, + {group_list,[[{group_name,"Admins"}]]}, + {path,"/"}, + {user_id,"AIDACKCEVSQ6C3EXAMPLE"}, + {user_name,"Bob"}, + {user_policy_list, + [[{policy_document, + "{\"Version\":\"2012-10-17\",\"Statement\":{\"Effect\":\"Deny\",\"Action\":[\"aws-portal:*\",\"iam:*\"],\"Resource\":\"*\"}}"}, + {policy_name,"DenyBillingAndIAMPolicy"}]]}], + [{arn,"arn:aws:iam::123456789012:user/Charlie"}, + {create_date,{{2013,10,14},{18,33,56}}}, + {group_list,[[{group_name,"Dev"}]]}, + {path,"/"}, + {user_id,"AIDACKCEVSQ6C4EXAMPLE"}, + {user_name,"Charlie"}, + {user_policy_list,[]}], + [{arn,"arn:aws:iam::123456789012:user/Danielle"}, + {create_date,{{2013,10,14},{18,33,56}}}, + {group_list,[[{group_name,"Dev"}]]}, + {path,"/"}, + {user_id,"AIDACKCEVSQ6C5EXAMPLE"}, + {user_name,"Danielle"}, + {user_policy_list,[]}], + [{arn,"arn:aws:iam::123456789012:user/Elaine"}, + {create_date,{{2013,10,14},{18,57,48}}}, + {group_list,[[{group_name,"Finance"}]]}, + {path,"/"}, + {user_id,"AIDACKCEVSQ6C6EXAMPLE"}, + {user_name,"Elaine"}, + {user_policy_list,[]}]]}], + "EXAMPLEkakv9BCuUNFDtxWSyfzetYwEx2ADc8dnzfvERF5S6YMvXKx41t6gCl/eeaCX3Jo94/bKqezEAg8TEVS99EKFLxm3jtbpl25FDWEXAMPLE"} }) ], output_tests(?_f(erlcloud_iam:get_account_authorization_details()), Tests). @@ -2099,7 +2882,7 @@ list_attached_user_policies_input_tests(_) -> [ {"Action", "ListAttachedUserPolicies"}, {"UserName", "Alice"}, - {"PathPrefix", http_uri:encode("/")} + {"PathPrefix", erlcloud_util:http_uri_encode("/")} ]}) ], @@ -2168,7 +2951,7 @@ list_attached_group_policies_input_tests(_) -> [ {"Action", "ListAttachedGroupPolicies"}, {"GroupName", "ReadOnlyUsers"}, - {"PathPrefix", http_uri:encode("/")} + {"PathPrefix", erlcloud_util:http_uri_encode("/")} ]}) ], @@ -2237,7 +3020,7 @@ list_attached_role_policies_input_tests(_) -> [ {"Action", "ListAttachedRolePolicies"}, {"RoleName", "ReadOnlyRole"}, - {"PathPrefix", http_uri:encode("/")} + {"PathPrefix", erlcloud_util:http_uri_encode("/")} ]}) ], @@ -2436,7 +3219,7 @@ list_entities_for_policy_input_tests(_) -> ?_f(erlcloud_iam:list_entities_for_policy("test")), [ {"Action", "ListEntitiesForPolicy"}, - {"PathPrefix", http_uri:encode("/")}, + {"PathPrefix", erlcloud_util:http_uri_encode("/")}, {"PolicyArn", "test"} ]}) ], @@ -2560,7 +3343,7 @@ get_policy_input_tests(_) -> ?_f(erlcloud_iam:get_policy("arn:aws:iam::123456789012:policy/S3-read-only-example-bucket")), [ {"Action", "GetPolicy"}, - {"PolicyArn", http_uri:encode("arn:aws:iam::123456789012:policy/S3-read-only-example-bucket")} + {"PolicyArn", erlcloud_util:http_uri_encode("arn:aws:iam::123456789012:policy/S3-read-only-example-bucket")} ]}) ], @@ -2608,7 +3391,7 @@ get_policy_version_input_tests(_) -> ?_f(erlcloud_iam:get_policy_version("arn:aws:iam::123456789012:policy/S3-read-only-example-bucket", "v1")), [ {"Action", "GetPolicyVersion"}, - {"PolicyArn", http_uri:encode("arn:aws:iam::123456789012:policy/S3-read-only-example-bucket")}, + {"PolicyArn", erlcloud_util:http_uri_encode("arn:aws:iam::123456789012:policy/S3-read-only-example-bucket")}, {"VersionId", "v1"} ]}) ], @@ -2696,6 +3479,9 @@ simulate_custom_policy_input_test(_) -> PolicyDoc1 = "policy_doc1", PolicyDoc2 = "policy_doc2", Action = "s3:ListBucket", + ContextEntries = [[{context_key_name,"aws:MultiFactorAuthPresent"}, + {context_key_type,"boolean"}, + {context_key_values,[true]}]], Tests = [?_iam_test( {"SimulateCustomPolicy input", @@ -2704,19 +3490,42 @@ simulate_custom_policy_input_test(_) -> PolicyDoc2])), [ {"Action", "SimulateCustomPolicy"}, - {"ActionNames.member.1", http_uri:encode(Action)}, + {"ActionNames.member.1", erlcloud_util:http_uri_encode(Action)}, {"PolicyInputList.member.1", PolicyDoc1}, {"PolicyInputList.member.2", PolicyDoc2}, {"MaxItems", "1000"} - ]}) + ]}), + ?_iam_test( + {"SimulateCustomPolicy2 input", + ?_f(erlcloud_iam:simulate_custom_policy([Action], + [PolicyDoc1], + ContextEntries)), + [{"Action","SimulateCustomPolicy"}, + {"ActionNames.member.1", erlcloud_util:http_uri_encode(Action)}, + {"PolicyInputList.member.1","policy_doc1"}, + {"ContextEntries.member.1.ContextKeyName",erlcloud_util:http_uri_encode("aws:MultiFactorAuthPresent")}, + {"ContextEntries.member.1.ContextKeyType","boolean"}, + {"ContextEntries.member.1.ContextKeyValues.member.1","true"}, + {"MaxItems","1000"}]}) ], input_tests(?SIMULATE_CUSTOM_POLICY_RESP, Tests). simulate_custom_policy_output_test(_) -> + ContextEntries = [[{context_key_name,"aws:MultiFactorAuthPresent"}, + {context_key_type,"boolean"}, + {context_key_values,[true]}]], Tests = [?_iam_test( {"SimulateCustomPolicy output", ?SIMULATE_CUSTOM_POLICY_RESP, + {ok, [[{eval_action_name, "s3:ListBucket"}, + {eval_decision, "allowed"}, + {eval_resource_name, "arn:aws:s3:::teambucket"}, + {matched_statements_list, + [[{source_policy_id, "PolicyInputList.1"}]]}]]}}), + ?_iam_test( + {"SimulateCustomPolicy2 output", + ?SIMULATE_CUSTOM_POLICY_RESP, {ok, [[{eval_action_name, "s3:ListBucket"}, {eval_decision, "allowed"}, {eval_resource_name, "arn:aws:s3:::teambucket"}, @@ -2725,7 +3534,8 @@ simulate_custom_policy_output_test(_) -> ], output_tests(?_f(erlcloud_iam:simulate_custom_policy(["s3:ListBucket"], ["policy_doc1", - "policy_doc2"])), + "policy_doc2"], + ContextEntries)), Tests). simulate_principal_policy_input_test(_) -> @@ -2738,8 +3548,8 @@ simulate_principal_policy_input_test(_) -> [Action])), [ {"Action", "SimulatePrincipalPolicy"}, - {"ActionNames.member.1", http_uri:encode(Action)}, - {"PolicySourceArn", http_uri:encode(Principal)}, + {"ActionNames.member.1", erlcloud_util:http_uri_encode(Action)}, + {"PolicySourceArn", erlcloud_util:http_uri_encode(Principal)}, {"MaxItems", "1000"} ]}) ], diff --git a/test/erlcloud_inspector_tests.erl b/test/erlcloud_inspector_tests.erl index 653ded4b8..9fd311665 100644 --- a/test/erlcloud_inspector_tests.erl +++ b/test/erlcloud_inspector_tests.erl @@ -499,7 +499,7 @@ update_assessment_tests(_) -> %%% Input test helpers %%%=================================================================== --type expected_body() :: string(). +-type expected_body() :: binary(). sort_json([{_, _} | _] = Json) -> %% Value is an object @@ -514,8 +514,8 @@ sort_json(V) -> %% verifies that the parameters in the body match the expected parameters -spec validate_body(binary(), expected_body()) -> ok. validate_body(Body, Expected) -> - Want = sort_json(jsx:decode(Expected)), - Actual = sort_json(jsx:decode(Body)), + Want = sort_json(jsx:decode(Expected, [{return_maps, false}])), + Actual = sort_json(jsx:decode(Body, [{return_maps, false}])), case Want =:= Actual of true -> ok; false -> @@ -526,7 +526,7 @@ validate_body(Body, Expected) -> %% returns the mock of the erlcloud_httpc function input tests expect to be called. %% Validates the request body and responds with the provided response. --spec input_expect(string(), expected_body()) -> fun(). +-spec input_expect(binary(), expected_body()) -> fun(). input_expect(Response, Expected) -> fun(_Url, post, _Headers, Body, _Timeout, _Config) -> validate_body(Body, Expected), @@ -536,7 +536,7 @@ input_expect(Response, Expected) -> %% input_test converts an input_test specifier into an eunit test generator -type input_test_spec() :: {pos_integer(), {fun(), expected_body()} | {string(), fun(), expected_body()}}. --spec input_test(string(), input_test_spec()) -> tuple(). +-spec input_test(binary(), input_test_spec()) -> tuple(). input_test(Response, {Line, {Description, Fun, Expected}}) when is_list(Description) -> {Description, @@ -549,7 +549,7 @@ input_test(Response, {Line, {Description, Fun, Expected}}) %% input_tests converts a list of input_test specifiers into an eunit test generator --spec input_tests(string(), [input_test_spec()]) -> [tuple()]. +-spec input_tests(binary(), [input_test_spec()]) -> [tuple()]. input_tests(Response, Tests) -> [input_test(Response, Test) || Test <- Tests]. @@ -565,7 +565,7 @@ output_expect(Response) -> end. %% output_test converts an output_test specifier into an eunit test generator --type output_test_spec() :: {pos_integer(), {string(), term()} | {string(), string(), term()}}. +-type output_test_spec() :: {pos_integer(), {string(), term()} | {string(), binary(), term()}}. -spec output_test(fun(), output_test_spec()) -> tuple(). output_test(Fun, {Line, {Description, Response, Result}}) -> {Description, @@ -602,4 +602,4 @@ all_tests(Action, Function, PostData, Response) -> )], input_tests(<<>>, InputTests) ++ - output_tests(Function, OutputTests). \ No newline at end of file + output_tests(Function, OutputTests). diff --git a/test/erlcloud_kinesis_tests.erl b/test/erlcloud_kinesis_tests.erl index 3e45dd14e..c317dfb9d 100644 --- a/test/erlcloud_kinesis_tests.erl +++ b/test/erlcloud_kinesis_tests.erl @@ -33,6 +33,8 @@ operation_test_() -> fun create_stream_output_tests/1, fun delete_stream_input_tests/1, fun delete_stream_output_tests/1, + fun list_shards_input_tests/1, + fun list_shards_output_tests/1, fun list_streams_input_tests/1, fun list_streams_output_tests/1, fun describe_stream_input_tests/1, @@ -97,8 +99,8 @@ sort_json(V) -> %% verifies that the parameters in the body match the expected parameters -spec validate_body(binary(), expected_body()) -> ok. validate_body(Body, Expected) -> - Want = sort_json(jsx:decode(list_to_binary(Expected))), - Actual = sort_json(jsx:decode(Body)), + Want = sort_json(decode(list_to_binary(Expected))), + Actual = sort_json(decode(Body)), case Want =:= Actual of true -> ok; false -> @@ -193,7 +195,7 @@ create_stream_output_tests(_) -> Tests = [?_kinesis_test( {"CreateStream example response", "{}", - {ok, jsx:decode(<<"{}">>)}}) + {ok, decode(<<"{}">>)}}) ], output_tests(?_f(erlcloud_kinesis:create_stream(<<"streamName">>, 2)), Tests). @@ -218,11 +220,137 @@ delete_stream_output_tests(_) -> Tests = [?_kinesis_test( {"DeleteStream example response", "{}", - {ok, jsx:decode(<<"{}">>)}}) + {ok, decode(<<"{}">>)}}) ], output_tests(?_f(erlcloud_kinesis:delete_stream(<<"streamName">>)), Tests). +%% ListStreams test based on the API examples: +%% http://docs.aws.amazon.com/kinesis/latest/APIReference/API_ListShards.html +list_shards_input_tests(_) -> + Tests = + [?_kinesis_test( + {"ListShards example request", + ?_f(erlcloud_kinesis:list_shards(<<"exampleStreamName">>, [{max_results, 3}])), "{ + \"StreamName\" : \"exampleStreamName\", + \"MaxResults\" : 3 + }" + }) + ], + + Response = "{ + \"NextToken\": \"AAAAAAAAAAGK9EEG0sJqVhCUS2JsgigQ5dcpB4q9PYswrH2oK44Skbjtm+WR0xA7/hrAFFsohevH1/OyPnbzKBS1byPyCZuVcokYtQe/b1m4c0SCI7jctPT0oUTLRdwSRirKm9dp9YC/EL+kZHOvYAUnztVGsOAPEFC3ECf/bVC927bDZBbRRzy/44OHfWmrCLcbcWqehRh5D14WnL3yLsumhiHDkyuxSlkBepauvMnNLtTOlRtmQ5Q5reoujfq2gzeCSOtLcfXgBMztJqohPdgMzjTQSbwB9Am8rMpHLsDbSdMNXmITvw==\", + \"Shards\": [ + { + \"ShardId\": \"shardId-000000000001\", + \"HashKeyRange\": { + \"EndingHashKey\": \"68056473384187692692674921486353642280\", + \"StartingHashKey\": \"34028236692093846346337460743176821145\" + }, + \"SequenceNumberRange\": { + \"StartingSequenceNumber\": \"49579844037727333356165064238440708846556371693205002258\" + } + }, + { + \"ShardId\": \"shardId-000000000002\", + \"HashKeyRange\": { + \"EndingHashKey\": \"102084710076281539039012382229530463436\", + \"StartingHashKey\": \"68056473384187692692674921486353642281\" + }, + \"SequenceNumberRange\": { + \"StartingSequenceNumber\": \"49579844037749634101363594861582244564829020124710982690\" + } + }, + { + \"ShardId\": \"shardId-000000000003\", + \"HashKeyRange\": { + \"EndingHashKey\": \"136112946768375385385349842972707284581\", + \"StartingHashKey\": \"102084710076281539039012382229530463437\" + }, + \"SequenceNumberRange\": { + \"StartingSequenceNumber\": \"49579844037771934846562125484723780283101668556216963122\" + } + } + ] + }", + input_tests(Response, Tests). + +list_shards_output_tests(_) -> + Tests = + [?_kinesis_test( + {"ListShards example response", "{ + \"NextToken\": \"AAAAAAAAAAGK9EEG0sJqVhCUS2JsgigQ5dcpB4q9PYswrH2oK44Skbjtm+WR0xA7/hrAFFsohevH1/OyPnbzKBS1byPyCZuVcokYtQe/b1m4c0SCI7jctPT0oUTLRdwSRirKm9dp9YC/EL+kZHOvYAUnztVGsOAPEFC3ECf/bVC927bDZBbRRzy/44OHfWmrCLcbcWqehRh5D14WnL3yLsumhiHDkyuxSlkBepauvMnNLtTOlRtmQ5Q5reoujfq2gzeCSOtLcfXgBMztJqohPdgMzjTQSbwB9Am8rMpHLsDbSdMNXmITvw==\", + \"Shards\": [ + { + \"ShardId\": \"shardId-000000000001\", + \"HashKeyRange\": { + \"EndingHashKey\": \"68056473384187692692674921486353642280\", + \"StartingHashKey\": \"34028236692093846346337460743176821145\" + }, + \"SequenceNumberRange\": { + \"StartingSequenceNumber\": \"49579844037727333356165064238440708846556371693205002258\" + } + }, + { + \"ShardId\": \"shardId-000000000002\", + \"HashKeyRange\": { + \"EndingHashKey\": \"102084710076281539039012382229530463436\", + \"StartingHashKey\": \"68056473384187692692674921486353642281\" + }, + \"SequenceNumberRange\": { + \"StartingSequenceNumber\": \"49579844037749634101363594861582244564829020124710982690\" + } + }, + { + \"ShardId\": \"shardId-000000000003\", + \"HashKeyRange\": { + \"EndingHashKey\": \"136112946768375385385349842972707284581\", + \"StartingHashKey\": \"102084710076281539039012382229530463437\" + }, + \"SequenceNumberRange\": { + \"StartingSequenceNumber\": \"49579844037771934846562125484723780283101668556216963122\" + } + } + ] + }", + {ok, [ + {<<"NextToken">>, <<"AAAAAAAAAAGK9EEG0sJqVhCUS2JsgigQ5dcpB4q9PYswrH2oK44Skbjtm+WR0xA7/hrAFFsohevH1/OyPnbzKBS1byPyCZuVcokYtQe/b1m4c0SCI7jctPT0oUTLRdwSRirKm9dp9YC/EL+kZHOvYAUnztVGsOAPEFC3ECf/bVC927bDZBbRRzy/44OHfWmrCLcbcWqehRh5D14WnL3yLsumhiHDkyuxSlkBepauvMnNLtTOlRtmQ5Q5reoujfq2gzeCSOtLcfXgBMztJqohPdgMzjTQSbwB9Am8rMpHLsDbSdMNXmITvw==">>}, + {<<"Shards">>, [ + [ + {<<"ShardId">>, <<"shardId-000000000001">>}, + {<<"HashKeyRange">>, [ + {<<"EndingHashKey">>, <<"68056473384187692692674921486353642280">>}, + {<<"StartingHashKey">>, <<"34028236692093846346337460743176821145">>} + ]}, + {<<"SequenceNumberRange">>, [ + {<<"StartingSequenceNumber">>, <<"49579844037727333356165064238440708846556371693205002258">>} + ]} + ], [ + {<<"ShardId">>, <<"shardId-000000000002">>}, + {<<"HashKeyRange">>, [ + {<<"EndingHashKey">>, <<"102084710076281539039012382229530463436">>}, + {<<"StartingHashKey">>, <<"68056473384187692692674921486353642281">>} + ]}, + {<<"SequenceNumberRange">>, [ + {<<"StartingSequenceNumber">>, <<"49579844037749634101363594861582244564829020124710982690">>} + ]} + ], [ + {<<"ShardId">>, <<"shardId-000000000003">>}, + {<<"HashKeyRange">>, [ + {<<"EndingHashKey">>, <<"136112946768375385385349842972707284581">>}, + {<<"StartingHashKey">>, <<"102084710076281539039012382229530463437">>} + ]}, + {<<"SequenceNumberRange">>, [ + {<<"StartingSequenceNumber">>, <<"49579844037771934846562125484723780283101668556216963122">>} + ]} + ] + ]} + ]} + } + )], + + output_tests(?_f(erlcloud_kinesis:list_shards(<<"staging">>)), Tests). + %% ListStreams test based on the API examples: %% http://docs.aws.amazon.com/kinesis/latest/APIReference/API_ListStreams.html list_streams_input_tests(_) -> @@ -826,7 +954,7 @@ merge_shards_output_tests(_) -> Tests = [?_kinesis_test( {"MergeShards example response", "{}", - {ok, jsx:decode(<<"{}">>)}}) + {ok, decode(<<"{}">>)}}) ], output_tests(?_f(erlcloud_kinesis:merge_shards(<<"test">>, <<"shardId-000000000001">>, <<"shardId-000000000003">>)), Tests). @@ -853,7 +981,7 @@ split_shards_output_tests(_) -> Tests = [?_kinesis_test( {"SplitShard example response", "{}", - {ok, jsx:decode(<<"{}">>)}}) + {ok, decode(<<"{}">>)}}) ], output_tests(?_f(erlcloud_kinesis:split_shards(<<"test">>, <<"shardId-000000000000">>, <<"10">>)), Tests). @@ -900,7 +1028,7 @@ list_tags_for_stream_output_tests(_) -> \"Tags\": [{\"Key\":\"key1\",\"Value\":\"val1\"}]}", Tests = [?_kinesis_test({"List tags response test", Response, - {ok, jsx:decode(list_to_binary(Response))}})], + {ok, decode(list_to_binary(Response))}})], output_tests( ?_f(erlcloud_kinesis:list_tags_for_stream(<<"stream">>, <<"key1">>, 1)), Tests @@ -948,3 +1076,6 @@ remove_tags_from_stream_output_tests(_) -> [<<"key">>])), Tests ). + +decode(S) -> + jsx:decode(S, [{return_maps, false}]). diff --git a/test/erlcloud_kms_tests.erl b/test/erlcloud_kms_tests.erl index abbe41ee4..f085f5413 100644 --- a/test/erlcloud_kms_tests.erl +++ b/test/erlcloud_kms_tests.erl @@ -101,7 +101,7 @@ stop(_) -> %%% Input test helpers %%%=================================================================== --type expected_body() :: string(). +-type expected_body() :: binary(). sort_json([{_, _} | _] = Json) -> %% Value is an object @@ -116,8 +116,8 @@ sort_json(V) -> %% verifies that the parameters in the body match the expected parameters -spec validate_body(binary(), expected_body()) -> ok. validate_body(Body, Expected) -> - Want = sort_json(jsx:decode(Expected)), - Actual = sort_json(jsx:decode(Body)), + Want = sort_json(jsx:decode(Expected, [{return_maps, false}])), + Actual = sort_json(jsx:decode(Body, [{return_maps, false}])), case Want =:= Actual of true -> ok; false -> @@ -128,7 +128,7 @@ validate_body(Body, Expected) -> %% returns the mock of the erlcloud_httpc function input tests expect to be called. %% Validates the request body and responds with the provided response. --spec input_expect(string(), expected_body()) -> fun(). +-spec input_expect(binary(), expected_body()) -> fun(). input_expect(Response, Expected) -> fun(_Url, post, _Headers, Body, _Timeout, _Config) -> validate_body(Body, Expected), @@ -138,7 +138,7 @@ input_expect(Response, Expected) -> %% input_test converts an input_test specifier into an eunit test generator -type input_test_spec() :: {pos_integer(), {fun(), expected_body()} | {string(), fun(), expected_body()}}. --spec input_test(string(), input_test_spec()) -> tuple(). +-spec input_test(binary(), input_test_spec()) -> tuple(). input_test(Response, {Line, {Description, Fun, Expected}}) when is_list(Description) -> {Description, @@ -151,7 +151,7 @@ input_test(Response, {Line, {Description, Fun, Expected}}) %% input_tests converts a list of input_test specifiers into an eunit test generator --spec input_tests(string(), [input_test_spec()]) -> [tuple()]. +-spec input_tests(binary(), [input_test_spec()]) -> [tuple()]. input_tests(Response, Tests) -> [input_test(Response, Test) || Test <- Tests]. @@ -167,7 +167,7 @@ output_expect(Response) -> end. %% output_test converts an output_test specifier into an eunit test generator --type output_test_spec() :: {pos_integer(), {string(), term()} | {string(), string(), term()}}. +-type output_test_spec() :: {pos_integer(), {string(), term()} | {string(), binary(), term()}}. -spec output_test(fun(), output_test_spec()) -> tuple(). output_test(Fun, {Line, {Description, Response, Result}}) -> {Description, diff --git a/test/erlcloud_lambda_tests.erl b/test/erlcloud_lambda_tests.erl index 7e8a78835..542814d35 100644 --- a/test/erlcloud_lambda_tests.erl +++ b/test/erlcloud_lambda_tests.erl @@ -7,39 +7,42 @@ -define(BASE_URL, "https://lambda.us-east-1.amazonaws.com:443/2015-03-31/"). route53_test_() -> - {foreach, - fun setup/0, - fun meck:unload/1, - [ - fun api_tests/1 - ]}. + {foreach, fun setup/0, fun meck:unload/1, [ + fun api_tests/1 + ]}. mocks() -> [ - mocked_create_alias(), - mocked_create_event_source_mapping(), - mocked_create_function(), - mocked_delete_event_source_mapping(), - mocked_get_alias(), - mocked_get_event_source_mapping(), - mocked_get_function(), - mocked_get_function_configuration(), - mocked_invoke(), - mocked_list_aliases(), - mocked_list_event_source_mappings(), - mocked_list_function(), - mocked_list_versions_by_function(), - mocked_publish_version(), - mocked_update_alias(), - mocked_update_event_source_mapping(), - mocked_update_function_code(), - mocked_update_function_configuration() + mocked_create_alias(), + mocked_create_event_source_mapping(), + mocked_create_function(), + mocked_delete_event_source_mapping(), + mocked_delete_function(), + mocked_delete_function_qualifier(), + mocked_get_alias(), + mocked_get_event_source_mapping(), + mocked_get_function(), + mocked_get_function_configuration(), + mocked_invoke(), + mocked_invoke_alias(), + mocked_invoke_qualifier(), + mocked_list_aliases(), + mocked_list_event_source_mappings(), + mocked_list_function(), + mocked_list_versions_by_function(), + mocked_publish_version(), + mocked_update_alias(), + mocked_update_event_source_mapping(), + mocked_update_function_code(), + mocked_update_function_configuration() ]. setup() -> - Config = #aws_config{access_key_id="AccessId", - secret_access_key="secret", - security_token="token"}, + Config = #aws_config{ + access_key_id = "AccessId", + secret_access_key = "secret", + security_token = "token" + }, meck:new(ECA = erlcloud_aws, [non_strict, passthrough]), meck:expect(ECA, default_config, 0, Config), meck:expect(ECA, update_config, 1, {ok, Config}), @@ -49,750 +52,1298 @@ setup() -> mocked_create_alias() -> { - [?BASE_URL ++ "functions/name/aliases", post, '_', - <<"{\"FunctionVersion\":\"$LATEST\",\"Name\":\"aliasName1\"}">>, '_', '_'], - make_response(<<"{\"AliasArn\":\"arn:aws:lambda:us-east-1:352283894008:" -"function:name:aliasName1\",\"Description\":\"\",\"FunctionVersion\":\"$LATEST" -"\",\"Name\":\"aliasName1\"}">>) + [ + ?BASE_URL ++ "functions/name/aliases", + post, + '_', + <<"{\"FunctionVersion\":\"$LATEST\",\"Name\":\"aliasName1\"}">>, + '_', + '_' + ], + make_response([ + {<<"AliasArn">>, <<"arn:aws:lambda:us-east-1:352283894008:function:name:aliasName1">>}, + {<<"Description">>, <<>>}, + {<<"FunctionVersion">>, <<"$LATEST">>}, + {<<"Name">>, <<"aliasName1">>} + ]) }. mocked_create_event_source_mapping() -> { - [?BASE_URL ++ "event-source-mappings", post, '_', - <<"{\"EventSourceArn\":\"arn:aws:kinesis:us-east-1:352283894008:stream" + [ + ?BASE_URL ++ "event-source-mappings", + post, + '_', + <<"{\"EventSourceArn\":\"arn:aws:kinesis:us-east-1:352283894008:stream" "/eholland-test\",\"FunctionName\":\"name\",\"StartingPosition\":\"TRI" - "M_HORIZON\"}">>, '_', '_'], - make_response(<<"{\"BatchSize\":100,\"EventSourceArn\":\"arn:aws:kinesi" -"s:us-east-1:352283894008:stream/eholland-test\",\"FunctionArn\":\"arn:aws:lam" -"bda:us-east-1:352283894008:function:name\",\"LastModified\":1449845416.123,\"" -"LastProcessingResult\":\"No records processed\",\"State\":\"Creating\",\"Stat" -"eTransitionReason\":\"User action\",\"UUID\":\"3f303f86-7395-43f3-9902-f5c80f" -"0a5382\"}">>) + "M_HORIZON\"}">>, + '_', + '_' + ], + make_response( + [ + {<<"BatchSize">>, 100}, + {<<"EventSourceArn">>, + <<"arn:aws:kinesis:us-east-1:352283894008:stream/eholland-test">>}, + {<<"FunctionArn">>, <<"arn:aws:lambda:us-east-1:352283894008:function:name">>}, + {<<"LastModified">>, 1449845416.123}, + {<<"LastProcessingResult">>, <<"No records processed">>}, + {<<"State">>, <<"Creating">>}, + {<<"StateTransitionReason">>, <<"User action">>}, + {<<"UUID">>, <<"3f303f86-7395-43f3-9902-f5c80f0a5382">>} + ] + ) }. mocked_create_function() -> { - [?BASE_URL ++ "functions", post, '_', - <<"{\"Code\":{\"S3Bucket\":\"bi-lambda\",\"S3Key\":\"local_transform/bi-a" -"ssets-environment-create-environment_1-0-0_latest.zip\"},\"FunctionName\":\"name" -"\",\"Handler\":\"index.process\",\"Role\":\"arn:aws:iam::352283894008:role/lambd" -"a_basic_execution\",\"Runtime\":\"nodejs\"}">> , '_', '_'], - make_response(<<"{\"CodeSha256\":\"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4Ic" -"piPf8QM=\",\"CodeSize\":848,\"Description\":\"\",\"FunctionArn\":\"arn:aws:la" -"mbda:us-east-1:352283894008:function:name3\",\"FunctionName\":\"name3\",\"Han" -"dler\":\"index.process\",\"LastModified\":\"2015-12-11T13:45:31.924+0000\",\"" -"MemorySize\":128,\"Role\":\"arn:aws:iam::352283894008:role/lambda_basic_execu" -"tion\",\"Runtime\":\"nodejs\",\"Timeout\":3,\"Version\":\"$LATEST\",\"VpcConf" -"ig\":null}">>) + [ + ?BASE_URL ++ "functions", + post, + '_', + jsx:encode([ + {<<"Code">>, [ + {<<"S3Bucket">>, <<"bi-lambda">>}, + {<<"S3Key">>, + <<"local_transform/bi-assets-environment-create-environment_1-0-0_latest.zip">>} + ]}, + {<<"FunctionName">>, <<"name">>}, + {<<"Handler">>, <<"index.process">>}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_basic_execution">>}, + {<<"Runtime">>, <<"nodejs">>} + ]), + '_', + '_' + ], + make_response([ + {<<"CodeSha256">>, <<"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=">>}, + {<<"CodeSize">>, 848}, + {<<"Description">>, <<>>}, + {<<"FunctionArn">>, <<"arn:aws:lambda:us-east-1:352283894008:function:name3">>}, + {<<"FunctionName">>, <<"name3">>}, + {<<"Handler">>, <<"index.process">>}, + {<<"LastModified">>, <<"2015-12-11T13:45:31.924+0000">>}, + {<<"MemorySize">>, 128}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_basic_execution">>}, + {<<"Runtime">>, <<"nodejs">>}, + {<<"Timeout">>, 3}, + {<<"Version">>, <<"$LATEST">>}, + {<<"VpcConfig">>, null} + ] + ) }. + mocked_delete_event_source_mapping() -> { - [?BASE_URL ++ "event-source-mappings/6554f300-551b-46a6-829c-41b6af6022c6?", - delete, '_', <<>>, '_', '_'], - make_response(<<"{\"BatchSize\":100,\"EventSourceArn\":\"arn:aws:kinesi" -"s:us-east-1:352283894008:stream/eholland-test\",\"FunctionArn\":\"arn:aws:lam" -"bda:us-east-1:352283894008:function:name\",\"LastModified\":1449843960.0,\"La" -"stProcessingResult\":\"No records processed\",\"State\":\"Deleting\",\"StateT" -"ransitionReason\":\"User action\",\"UUID\":\"a45b58ec-a539-4c47-929e-174b4dd2" -"d963\"}">>) + [ + ?BASE_URL ++ "event-source-mappings/6554f300-551b-46a6-829c-41b6af6022c6", + delete, + '_', + <<>>, + '_', + '_' + ], + make_response( + [ + {<<"BatchSize">>, 100}, + {<<"EventSourceArn">>, + <<"arn:aws:kinesis:us-east-1:352283894008:stream/eholland-test">>}, + {<<"FunctionArn">>, <<"arn:aws:lambda:us-east-1:352283894008:function:name">>}, + {<<"LastModified">>, 1449843960.0}, + {<<"LastProcessingResult">>, <<"No records processed">>}, + {<<"State">>, <<"Deleting">>}, + {<<"StateTransitionReason">>, <<"User action">>}, + {<<"UUID">>, <<"a45b58ec-a539-4c47-929e-174b4dd2d963">>} + ] + ) + }. +mocked_delete_function() -> + { + [ + ?BASE_URL ++ "functions/name", + delete, + '_', + <<>>, + '_', + '_' + ], + make_response({204, "No Content"}, <<"">>) + }. +mocked_delete_function_qualifier() -> + { + [ + ?BASE_URL ++ "functions/name_qualifier?Qualifier=123", + delete, + '_', + <<>>, + '_', + '_' + ], + make_response({204, "No Content"}, <<"">>) }. mocked_get_alias() -> - { - [?BASE_URL ++ "functions/name/aliases/aliasName?", - get, '_', <<>>, '_', '_'], - make_response(<<"{\"AliasArn\":\"arn:aws:lambda:us-east-1:352283894008:" -"function:name:aliasName\",\"Description\":\"\",\"FunctionVersion\":\"$LATEST\" -"",\"Name\":\"aliasName\"}">>) + { + [ + ?BASE_URL ++ "functions/name/aliases/aliasName", + get, + '_', + <<>>, + '_', + '_' + ], + make_response([ + {<<"AliasArn">>, <<"arn:aws:lambda:us-east-1:352283894008:function:name:aliasName">>}, + {<<"Description">>, <<>>}, + {<<"FunctionVersion">>, <<"$LATEST">>}, + {<<"Name">>, <<"aliasName">>} + ]) }. mocked_get_event_source_mapping() -> - { - [?BASE_URL ++ "event-source-mappings/a45b58ec-a539-4c47-929e-174b4dd2d963?", - get, '_', <<>>, '_', '_'], - make_response(<<"{\"BatchSize\":100,\"EventSourceArn\":\"arn:aws:kinesi" -"s:us-east-1:352283894008:stream/eholland-test\",\"FunctionArn\":\"arn:aws:lam" -"bda:us-east-1:352283894008:function:name\",\"LastModified\":1449841860.0,\"La" -"stProcessingResult\":\"No records processed\",\"State\":\"Enabled\",\"StateTr" -"ansitionReason\":\"User action\",\"UUID\":\"a45b58ec-a539-4c47-929e-174b4dd2d" -"963\"}">>) + { + [ + ?BASE_URL ++ "event-source-mappings/a45b58ec-a539-4c47-929e-174b4dd2d963", + get, + '_', + <<>>, + '_', + '_' + ], + make_response( + [ + {<<"BatchSize">>, 100}, + {<<"EventSourceArn">>, + <<"arn:aws:kinesis:us-east-1:352283894008:stream/eholland-test">>}, + {<<"FunctionArn">>, <<"arn:aws:lambda:us-east-1:352283894008:function:name">>}, + {<<"LastModified">>, 1449841860.0}, + {<<"LastProcessingResult">>, <<"No records processed">>}, + {<<"State">>, <<"Enabled">>}, + {<<"StateTransitionReason">>, <<"User action">>}, + {<<"UUID">>, <<"a45b58ec-a539-4c47-929e-174b4dd2d963">>} + ] + ) }. mocked_get_function() -> { - [?BASE_URL ++ "functions/name?", - get, '_', <<>>, '_', '_'], - make_response(<<"{\"Code\":{\"Location\":\"https://awslambda-us-east-1-" -"tasks.s3-us-east-1.amazonaws.com/snapshots/352283894008/name-69237aec-bae9-40" -"86-af73-a6610c2f8eb8?x-amz-security-token=AQoDYXdzENb%2F%2F%2F%2F%2F%2F%2F%2F" -"%2F%2FwEa4APJSJNHsYWnObKlElri5ytVzNg0t%2F0zwADHn40jy%2F1ZDW9%2FYWGi8dTp1l6LWB" -"I9TwJi0LLcgp%2FlCxJIh7hsAPftYX62J9r9lRcmgd9RnYssg1%2Fkpfyjya90epxKg2zdHm%2BuZ" -"GukHYDcmAE1IQcHwsaQbvGAjXPCpFyxClbV6gMcFIsaBtfMxoMcbTCXG9m8l56nKgcX6Mi60vRNaB" -"83AeNVrKMhB8EBUUbYbaB%2BG0iJg32i2HBF6VJMxamOLIEf1GJp1tWt%2FSAHfEkdTwcwtGINH3T" -"NRv%2BY3ddsXs8pJ49eY49NCHANPC%2Bq0JzNQydbIK1shz8w1nozXYQo6%2BNh9tqOlaJNFgfFbt" -"JkUDXv4rFqgVsfgJKJSQBeYUKmlNvIPQIoHWhRjjRzQUGmYDc3eEug7vELsNcHZixI4nNVycH%2BJ" -"ZwaBvswy4eE7gBv3HwHi3SVlg9iXFTrfWTK%2FlCybC7mZIjAmPGiLCG5Pu8SoCgaGdHp8HmSeXXu" -"s4VUFcVTUw1qn7E%2BSaRFg3MTpCdu1f1Nqh4pNu7GpZacyLbH%2BSocuPyTjyYGL8sk0C3rjIWZi" -"pJcfUZsleS1cXLEGw%2FPAK5eg0RNWlVsaF1WgVimqQh3LtoRZ%2BwYCHRXsHjhCyQ8VhoguJCrsw" -"U%3D&AWSAccessKeyId=ASIAIAUY4IMA6JRJ3FSA&Expires=1449843004&Signature=5KUmber" -"1cOBSChlKtnLaI%2FUemU4%3D\",\"RepositoryType\":\"S3\"},\"Configuration\":{\"C" -"odeSha256\":\"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=\",\"CodeSize\":848" -",\"Description\":\"\",\"FunctionArn\":\"arn:aws:lambda:us-east-1:352283894008" -":function:name\",\"FunctionName\":\"name\",\"Handler\":\"index.process\",\"La" -"stModified\":\"2015-12-10T13:57:48.214+0000\",\"MemorySize\":512,\"Role\":\"a" -"rn:aws:iam::352283894008:role/lambda_kinesis_role\",\"Runtime\":\"nodejs\",\"" -"Timeout\":30,\"Version\":\"$LATEST\",\"VpcConfig\":null}}">>) + [ + ?BASE_URL ++ "functions/name", + get, + '_', + <<>>, + '_', + '_' + ], + make_response([ + {<<"Code">>, [ + {<<"Location">>, + <<"https://awslambda-us-east-1-tasks.s3-us-east-1.amazonaws.com/snapshots/352283894008/name-69237aec-bae9-4086-af73-a6610c2f8eb8?x-amz-security-token=AQoDYXdzENb%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEa4APJSJNHsYWnObKlElri5ytVzNg0t%2F0zwADHn40jy%2F1ZDW9%2FYWGi8dTp1l6LWBI9TwJi0LLcgp%2FlCxJIh7hsAPftYX62J9r9lRcmgd9RnYssg1%2Fkpfyjya90epxKg2zdHm%2BuZGukHYDcmAE1IQcHwsaQbvGAjXPCpFyxClbV6gMcFIsaBtfMxoMcbTCXG9m8l56nKgcX6Mi60vRNaB83AeNVrKMhB8EBUUbYbaB%2BG0iJg32i2HBF6VJMxamOLIEf1GJp1tWt%2FSAHfEkdTwcwtGINH3TNRv%2BY3ddsXs8pJ49eY49NCHANPC%2Bq0JzNQydbIK1shz8w1nozXYQo6%2BNh9tqOlaJNFgfFbtJkUDXv4rFqgVsfgJKJSQBeYUKmlNvIPQIoHWhRjjRzQUGmYDc3eEug7vELsNcHZixI4nNVycH%2BJZwaBvswy4eE7gBv3HwHi3SVlg9iXFTrfWTK%2FlCybC7mZIjAmPGiLCG5Pu8SoCgaGdHp8HmSeXXus4VUFcVTUw1qn7E%2BSaRFg3MTpCdu1f1Nqh4pNu7GpZacyLbH%2BSocuPyTjyYGL8sk0C3rjIWZipJcfUZsleS1cXLEGw%2FPAK5eg0RNWlVsaF1WgVimqQh3LtoRZ%2BwYCHRXsHjhCyQ8VhoguJCrswU%3D&AWSAccessKeyId=ASIAIAUY4IMA6JRJ3FSA&Expires=1449843004&Signature=5KUmber1cOBSChlKtnLaI%2FUemU4%3D">>}, + {<<"RepositoryType">>, <<"S3">>} + ]}, + {<<"Configuration">>, [ + {<<"CodeSha256">>, <<"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=">>}, + {<<"CodeSize">>, 848}, + {<<"Description">>, <<>>}, + {<<"FunctionArn">>, <<"arn:aws:lambda:us-east-1:352283894008:function:name">>}, + {<<"FunctionName">>, <<"name">>}, + {<<"Handler">>, <<"index.process">>}, + {<<"LastModified">>, <<"2015-12-10T13:57:48.214+0000">>}, + {<<"MemorySize">>, 512}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, + {<<"Runtime">>, <<"nodejs">>}, + {<<"Timeout">>, 30}, + {<<"Version">>, <<"$LATEST">>}, + {<<"VpcConfig">>, null} + ]} + ]) }. mocked_get_function_configuration() -> { - [?BASE_URL ++ "functions/name/configuration?", get, '_', <<>>, '_', '_'], - make_response(<<"{\"CodeSha256\":\"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4Ic" -"piPf8QM=\",\"CodeSize\":848,\"Description\":\"\",\"FunctionArn\":\"arn:aws:la" -"mbda:us-east-1:352283894008:function:name\",\"FunctionName\":\"name\",\"Handl" -"er\":\"index.process\",\"LastModified\":\"2015-12-10T13:57:48.214+0000\",\"Me" -"morySize\":512,\"Role\":\"arn:aws:iam::352283894008:role/lambda_kinesis_role\" -"",\"Runtime\":\"nodejs\",\"Timeout\":30,\"Version\":\"$LATEST\",\"VpcConfig\"" -":null}">>) + [?BASE_URL ++ "functions/name/configuration", get, '_', <<>>, '_', '_'], + make_response( + [ + {<<"CodeSha256">>, <<"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=">>}, + {<<"CodeSize">>, 848}, + {<<"Description">>, <<>>}, + {<<"FunctionArn">>, <<"arn:aws:lambda:us-east-1:352283894008:function:name">>}, + {<<"FunctionName">>, <<"name">>}, + {<<"Handler">>, <<"index.process">>}, + {<<"LastModified">>, <<"2015-12-10T13:57:48.214+0000">>}, + {<<"MemorySize">>, 512}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, + {<<"Runtime">>, <<"nodejs">>}, + {<<"Timeout">>, 30}, + {<<"Version">>, <<"$LATEST">>}, + {<<"VpcConfig">>, null} + ] + ) }. mocked_invoke() -> - { - [?BASE_URL ++ "functions/name/invocations", post, '_', <<"{}">>, '_', '_'], - make_response(<<"{\"message\":\"Hello World!\"}">>) - }. + { + [?BASE_URL ++ "functions/name/invocations", post, '_', <<"{}">>, '_', '_'], + make_response(<<"{\"message\":\"Hello World!\"}">>) + }. +mocked_invoke_alias() -> + { + [?BASE_URL ++ "functions/name%3Aalias/invocations", post, '_', <<"{}">>, '_', '_'], + make_response(<<"{\"message\":\"Hello World!\"}">>) + }. + +mocked_invoke_qualifier() -> + { + [ + ?BASE_URL ++ "functions/name_qualifier/invocations?Qualifier=123", + post, + '_', + <<"{}">>, + '_', + '_' + ], + make_response(<<"{\"message\":\"Hello World!\"}">>) + }. mocked_list_aliases() -> { - [?BASE_URL ++ "functions/name/aliases?", get, '_', <<>>, '_', '_'], - make_response(<<"{\"Aliases\":[{\"AliasArn\":\"arn:aws:lambda:us-east-1" -":352283894008:function:name:aliasName\",\"Description\":\"\",\"FunctionVersio" -"n\":\"$LATEST\",\"Name\":\"aliasName\"},{\"AliasArn\":\"arn:aws:lambda:us-eas" -"t-1:352283894008:function:name:aliasName1\",\"Description\":\"\",\"FunctionVe" -"rsion\":\"$LATEST\",\"Name\":\"aliasName1\"}],\"NextMarker\":null}">>) + [?BASE_URL ++ "functions/name/aliases", get, '_', <<>>, '_', '_'], + make_response([ + {<<"Aliases">>, [ + [ + {<<"AliasArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:name:aliasName">>}, + {<<"Description">>, <<>>}, + {<<"FunctionVersion">>, <<"$LATEST">>}, + {<<"Name">>, <<"aliasName">>} + ], + [ + {<<"AliasArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:name:aliasName1">>}, + {<<"Description">>, <<>>}, + {<<"FunctionVersion">>, <<"$LATEST">>}, + {<<"Name">>, <<"aliasName1">>} + ] + ]}, + {<<"NextMarker">>, null} + ]) }. mocked_list_event_source_mappings() -> { - [?BASE_URL ++ "event-source-mappings/?EventSourceArn=arn%3Aaws%3Akinesis%" - "3Aus-east-1%3A352283894008%3Astream%2Feholland-test&FunctionName=name", - get, '_', <<>>, '_', '_'], - make_response(<<"{\"EventSourceMappings\":[{\"BatchSize\":100,\"EventSo" -"urceArn\":\"arn:aws:kinesis:us-east-1:352283894008:stream/eholland-test\",\"F" -"unctionArn\":\"arn:aws:lambda:us-east-1:352283894008:function:name\",\"LastMo" -"dified\":1449841860.0,\"LastProcessingResult\":\"No records processed\",\"Sta" -"te\":\"Enabled\",\"StateTransitionReason\":\"User action\",\"UUID\":\"a45b58e" -"c-a539-4c47-929e-174b4dd2d963\"}],\"NextMarker\":null}">>) + [ + ?BASE_URL ++ + "event-source-mappings/?EventSourceArn=arn%3Aaws%3Akinesis%" + "3Aus-east-1%3A352283894008%3Astream%2Feholland-test&FunctionName=name", + get, + '_', + <<>>, + '_', + '_' + ], + make_response([ + {<<"EventSourceMappings">>, [ + [ + {<<"BatchSize">>, 100}, + {<<"EventSourceArn">>, + <<"arn:aws:kinesis:us-east-1:352283894008:stream/eholland-test">>}, + {<<"FunctionArn">>, <<"arn:aws:lambda:us-east-1:352283894008:function:name">>}, + {<<"LastModified">>, 1449841860.0}, + {<<"LastProcessingResult">>, <<"No records processed">>}, + {<<"State">>, <<"Enabled">>}, + {<<"StateTransitionReason">>, <<"User action">>}, + {<<"UUID">>, <<"a45b58ec-a539-4c47-929e-174b4dd2d963">>} + ] + ]}, + {<<"NextMarker">>, null} + ]) }. mocked_list_function() -> { - [?BASE_URL ++ "functions/?", - get, '_', <<>>, '_', '_'], - make_response(<<"{\"Functions\":[{\"CodeSha256\":\"XmLDAZXEkl5KbA8ezZpw" -"FU+bjgTXBehUmWGOScl4F2A=\",\"CodeSize\":5561,\"Description\":\"\",\"FunctionA" -"rn\":\"arn:aws:lambda:us-east-1:352283894008:function:bi-assets-asset-create-" -"host\",\"FunctionName\":\"bi-assets-asset-create-host\",\"Handler\":\"index.h" -"andler\",\"LastModified\":\"2015-11-27T09:55:12.973+0000\",\"MemorySize\":128" -",\"Role\":\"arn:aws:iam::352283894008:role/lambda_basic_execution\",\"Runtime" -"\":\"nodejs\",\"Timeout\":3,\"Version\":\"$LATEST\",\"VpcConfig\":null},{\"Co" -"deSha256\":\"XmLDAZXEkl5KbA8ezZpwFU+bjgTXBehUmWGOScl4F2A=\",\"CodeSize\":5561" -",\"Description\":\"\",\"FunctionArn\":\"arn:aws:lambda:us-east-1:352283894008" -":function:bi-assets-asset-create-host1\",\"FunctionName\":\"bi-assets-asset-c" -"reate-host1\",\"Handler\":\"index.handler\",\"LastModified\":\"2015-12-01T11:" -"20:44.464+0000\",\"MemorySize\":128,\"Role\":\"arn:aws:iam::352283894008:role" -"/lambda_kinesis_role\",\"Runtime\":\"nodejs\",\"Timeout\":10,\"Version\":\"$L" -"ATEST\",\"VpcConfig\":null},{\"CodeSha256\":\"tZJ+kUZVD1vGYwMIUvAoaZmvS4I9NHV" -"c7a/267eChYY=\",\"CodeSize\":132628,\"Description\":\"\",\"FunctionArn\":\"ar" -"n:aws:lambda:us-east-1:352283894008:function:bi-driver\",\"FunctionName\":\"b" -"i-driver\",\"Handler\":\"index.handler\",\"LastModified\":\"2015-12-03T13:59:" -"05.219+0000\",\"MemorySize\":1024,\"Role\":\"arn:aws:iam::352283894008:role/l" -"ambda_kinesis_role\",\"Runtime\":\"nodejs\",\"Timeout\":59,\"Version\":\"$LAT" -"EST\",\"VpcConfig\":null},{\"CodeSha256\":\"QS10seyYGXrrhAnGMbJcTi+JOa4HWLaD+" -"9YCLYG3+VE=\",\"CodeSize\":121486,\"Description\":\"An Amazon Kinesis stream " -"processor that logs the data being published.\",\"FunctionArn\":\"arn:aws:lam" -"bda:us-east-1:352283894008:function:eholland-js-router\",\"FunctionName\":\"e" -"holland-js-router\",\"Handler\":\"index.handler\",\"LastModified\":\"2015-12-" -"02T14:50:41.923+0000\",\"MemorySize\":1024,\"Role\":\"arn:aws:iam::3522838940" -"08:role/lambda_kinesis_role\",\"Runtime\":\"nodejs\",\"Timeout\":60,\"Version" -"\":\"$LATEST\",\"VpcConfig\":null},{\"CodeSha256\":\"ey+6CSWe750XoPSdVSQditxx" -"oHNWmPFwve/MLPNs/Do=\",\"CodeSize\":253,\"Description\":\"\",\"FunctionArn\":" -"\"arn:aws:lambda:us-east-1:352283894008:function:eholland-test-aims-user\",\"" -"FunctionName\":\"eholland-test-aims-user\",\"Handler\":\"lambda_function.lamb" -"da_handler\",\"LastModified\":\"2015-11-09T17:32:46.030+0000\",\"MemorySize\"" -":128,\"Role\":\"arn:aws:iam::352283894008:role/lambda_basic_execution\",\"Run" -"time\":\"python2.7\",\"Timeout\":3,\"Version\":\"$LATEST\",\"VpcConfig\":null" -"},{\"CodeSha256\":\"R9HaKKCPPAem0ikKrkMGpDtX95M8egDCDd+4Ws+Kk5c=\",\"CodeSize" -"\":253,\"Description\":\"aims users transform function\",\"FunctionArn\":\"ar" -"n:aws:lambda:us-east-1:352283894008:function:eholland-test-aims-users\",\"Fun" -"ctionName\":\"eholland-test-aims-users\",\"Handler\":\"lambda_function.lambda" -"_handler\",\"LastModified\":\"2015-11-02T13:59:32.509+0000\",\"MemorySize\":1" -"28,\"Role\":\"arn:aws:iam::352283894008:role/lambda_basic_execution\",\"Runti" -"me\":\"python2.7\",\"Timeout\":3,\"Version\":\"$LATEST\",\"VpcConfig\":null}," -"{\"CodeSha256\":\"PGG1vCAQpc8J7e4HL1z1Pv4DGSZEmhnJaNPTGDS29kk=\",\"CodeSize\"" -":32372,\"Description\":\"An Amazon Kinesis stream processor that logs the dat" -"a being published.\",\"FunctionArn\":\"arn:aws:lambda:us-east-1:352283894008:" -"function:eholland-test-router\",\"FunctionName\":\"eholland-test-router\",\"H" -"andler\":\"ProcessKinesisRecords.lambda_handler\",\"LastModified\":\"2015-11-" -"09T10:58:50.458+0000\",\"MemorySize\":256,\"Role\":\"arn:aws:iam::35228389400" -"8:role/lambda_kinesis_role\",\"Runtime\":\"python2.7\",\"Timeout\":59,\"Versi" -"on\":\"$LATEST\",\"VpcConfig\":null},{\"CodeSha256\":\"8twXwGaAXyr8Li81rGhM6v" -"lMh+dvXglwqgIshfaio+U=\",\"CodeSize\":708740,\"Description\":\"Lambda Router " -"Function for ETL Service\",\"FunctionArn\":\"arn:aws:lambda:us-east-1:3522838" -"94008:function:us-east-1_base_eholland_master_lambda_route\",\"FunctionName\"" -":\"us-east-1_base_eholland_master_lambda_route\",\"Handler\":\"ProcessKinesis" -"Records.handler\",\"LastModified\":\"2015-11-11T10:13:19.043+0000\",\"MemoryS" -"ize\":512,\"Role\":\"arn:aws:iam::352283894008:role/lambda_kinesis_role\",\"R" -"untime\":\"nodejs\",\"Timeout\":59,\"Version\":\"$LATEST\",\"VpcConfig\":null" -"},{\"CodeSha256\":\"aDwLJhljsMVHsaN+LM4jRmsSLKKBdS+eFrAhKmJ/zbQ=\",\"CodeSize" -"\":253,\"Description\":\"\",\"FunctionArn\":\"arn:aws:lambda:us-east-1:352283" -"894008:function:us-east-1_base_eholland_master_lambda_route-aims-user-create\"" -",\"FunctionName\":\"us-east-1_base_eholland_master_lambda_route-aims-user-cr" -"eate\",\"Handler\":\"lambda_function.lambda_handler\",\"LastModified\":\"2015" -"-11-11T09:42:10.303+0000\",\"MemorySize\":128,\"Role\":\"arn:aws:iam::3522838" -"94008:role/lambda_basic_execution\",\"Runtime\":\"python2.7\",\"Timeout\":3," -"\"Version\":\"$LATEST\",\"VpcConfig\":null},{\"CodeSha256\":\"zeoBX1hIWJBHk1mu" -"Je1iFyS1CcAmsT0Ct4IcpiPf8QM=\",\"CodeSize\":848,\"Description\":\"\",\"Functi" -"onArn\":\"arn:aws:lambda:us-east-1:352283894008:function:name\",\"FunctionNam" -"e\":\"name\",\"Handler\":\"index.process\",\"LastModified\":\"2015-12-10T13:5" -"7:48.214+0000\",\"MemorySize\":512,\"Role\":\"arn:aws:iam::352283894008:role/" -"lambda_kinesis_role\",\"Runtime\":\"nodejs\",\"Timeout\":30,\"Version\":\"$LA" -"TEST\",\"VpcConfig\":null},{\"CodeSha256\":\"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0C" -"t4IcpiPf8QM=\",\"CodeSize\":848,\"Description\":\"\",\"FunctionArn\":\"arn:aw" -"s:lambda:us-east-1:352283894008:function:name2\",\"FunctionName\":\"name2\"," -"\"Handler\":\"index.process\",\"LastModified\":\"2015-12-10T11:31:21.106+0000" -"\",\"MemorySize\":128,\"Role\":\"arn:aws:iam::352283894008:role/lambda_basic_e" -"xecution\",\"Runtime\":\"nodejs\",\"Timeout\":3,\"Version\":\"$LATEST\",\"Vpc" -"Config\":null}],\"NextMarker\":null}">>) + [ + ?BASE_URL ++ "functions/", + get, + '_', + <<>>, + '_', + '_' + ], + make_response([ + {<<"Functions">>, [ + [ + {<<"CodeSha256">>, <<"XmLDAZXEkl5KbA8ezZpwFU+bjgTXBehUmWGOScl4F2A=">>}, + {<<"CodeSize">>, 5561}, + {<<"Description">>, <<>>}, + {<<"FunctionArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:bi-assets-asset-create-host">>}, + {<<"FunctionName">>, <<"bi-assets-asset-create-host">>}, + {<<"Handler">>, <<"index.handler">>}, + {<<"LastModified">>, <<"2015-11-27T09:55:12.973+0000">>}, + {<<"MemorySize">>, 128}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_basic_execution">>}, + {<<"Runtime">>, <<"nodejs">>}, + {<<"Timeout">>, 3}, + {<<"Version">>, <<"$LATEST">>}, + {<<"VpcConfig">>, null} + ], + [ + {<<"CodeSha256">>, <<"XmLDAZXEkl5KbA8ezZpwFU+bjgTXBehUmWGOScl4F2A=">>}, + {<<"CodeSize">>, 5561}, + {<<"Description">>, <<>>}, + {<<"FunctionArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:bi-assets-asset-create-host1">>}, + {<<"FunctionName">>, <<"bi-assets-asset-create-host1">>}, + {<<"Handler">>, <<"index.handler">>}, + {<<"LastModified">>, <<"2015-12-01T11:20:44.464+0000">>}, + {<<"MemorySize">>, 128}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, + {<<"Runtime">>, <<"nodejs">>}, + {<<"Timeout">>, 10}, + {<<"Version">>, <<"$LATEST">>}, + {<<"VpcConfig">>, null} + ], + [ + {<<"CodeSha256">>, <<"tZJ+kUZVD1vGYwMIUvAoaZmvS4I9NHVc7a/267eChYY=">>}, + {<<"CodeSize">>, 132628}, + {<<"Description">>, <<>>}, + {<<"FunctionArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:bi-driver">>}, + {<<"FunctionName">>, <<"bi-driver">>}, + {<<"Handler">>, <<"index.handler">>}, + {<<"LastModified">>, <<"2015-12-03T13:59:05.219+0000">>}, + {<<"MemorySize">>, 1024}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, + {<<"Runtime">>, <<"nodejs">>}, + {<<"Timeout">>, 59}, + {<<"Version">>, <<"$LATEST">>}, + {<<"VpcConfig">>, null} + ], + [ + {<<"CodeSha256">>, <<"QS10seyYGXrrhAnGMbJcTi+JOa4HWLaD+9YCLYG3+VE=">>}, + {<<"CodeSize">>, 121486}, + {<<"Description">>, + <<"An Amazon Kinesis stream processor that logs the data being published.">>}, + {<<"FunctionArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:eholland-js-router">>}, + {<<"FunctionName">>, <<"eholland-js-router">>}, + {<<"Handler">>, <<"index.handler">>}, + {<<"LastModified">>, <<"2015-12-02T14:50:41.923+0000">>}, + {<<"MemorySize">>, 1024}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, + {<<"Runtime">>, <<"nodejs">>}, + {<<"Timeout">>, 60}, + {<<"Version">>, <<"$LATEST">>}, + {<<"VpcConfig">>, null} + ], + [ + {<<"CodeSha256">>, <<"ey+6CSWe750XoPSdVSQditxxoHNWmPFwve/MLPNs/Do=">>}, + {<<"CodeSize">>, 253}, + {<<"Description">>, <<>>}, + {<<"FunctionArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:eholland-test-aims-user">>}, + {<<"FunctionName">>, <<"eholland-test-aims-user">>}, + {<<"Handler">>, <<"lambda_function.lambda_handler">>}, + {<<"LastModified">>, <<"2015-11-09T17:32:46.030+0000">>}, + {<<"MemorySize">>, 128}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_basic_execution">>}, + {<<"Runtime">>, <<"python2.7">>}, + {<<"Timeout">>, 3}, + {<<"Version">>, <<"$LATEST">>}, + {<<"VpcConfig">>, null} + ], + [ + {<<"CodeSha256">>, <<"R9HaKKCPPAem0ikKrkMGpDtX95M8egDCDd+4Ws+Kk5c=">>}, + {<<"CodeSize">>, 253}, + {<<"Description">>, <<"aims users transform function">>}, + {<<"FunctionArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:eholland-test-aims-users">>}, + {<<"FunctionName">>, <<"eholland-test-aims-users">>}, + {<<"Handler">>, <<"lambda_function.lambda_handler">>}, + {<<"LastModified">>, <<"2015-11-02T13:59:32.509+0000">>}, + {<<"MemorySize">>, 128}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_basic_execution">>}, + {<<"Runtime">>, <<"python2.7">>}, + {<<"Timeout">>, 3}, + {<<"Version">>, <<"$LATEST">>}, + {<<"VpcConfig">>, null} + ], + [ + {<<"CodeSha256">>, <<"PGG1vCAQpc8J7e4HL1z1Pv4DGSZEmhnJaNPTGDS29kk=">>}, + {<<"CodeSize">>, 32372}, + {<<"Description">>, + <<"An Amazon Kinesis stream processor that logs the data being published.">>}, + {<<"FunctionArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:eholland-test-router">>}, + {<<"FunctionName">>, <<"eholland-test-router">>}, + {<<"Handler">>, <<"ProcessKinesisRecords.lambda_handler">>}, + {<<"LastModified">>, <<"2015-11-09T10:58:50.458+0000">>}, + {<<"MemorySize">>, 256}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, + {<<"Runtime">>, <<"python2.7">>}, + {<<"Timeout">>, 59}, + {<<"Version">>, <<"$LATEST">>}, + {<<"VpcConfig">>, null} + ], + [ + {<<"CodeSha256">>, <<"8twXwGaAXyr8Li81rGhM6vlMh+dvXglwqgIshfaio+U=">>}, + {<<"CodeSize">>, 708740}, + {<<"Description">>, <<"Lambda Router Function for ETL Service">>}, + {<<"FunctionArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:us-east-1_base_eholland_master_lambda_route">>}, + {<<"FunctionName">>, <<"us-east-1_base_eholland_master_lambda_route">>}, + {<<"Handler">>, <<"ProcessKinesisRecords.handler">>}, + {<<"LastModified">>, <<"2015-11-11T10:13:19.043+0000">>}, + {<<"MemorySize">>, 512}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, + {<<"Runtime">>, <<"nodejs">>}, + {<<"Timeout">>, 59}, + {<<"Version">>, <<"$LATEST">>}, + {<<"VpcConfig">>, null} + ], + [ + {<<"CodeSha256">>, <<"aDwLJhljsMVHsaN+LM4jRmsSLKKBdS+eFrAhKmJ/zbQ=">>}, + {<<"CodeSize">>, 253}, + {<<"Description">>, <<>>}, + {<<"FunctionArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:us-east-1_base_eholland_master_lambda_route-aims-user-create">>}, + {<<"FunctionName">>, + <<"us-east-1_base_eholland_master_lambda_route-aims-user-create">>}, + {<<"Handler">>, <<"lambda_function.lambda_handler">>}, + {<<"LastModified">>, <<"2015-11-11T09:42:10.303+0000">>}, + {<<"MemorySize">>, 128}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_basic_execution">>}, + {<<"Runtime">>, <<"python2.7">>}, + {<<"Timeout">>, 3}, + {<<"Version">>, <<"$LATEST">>}, + {<<"VpcConfig">>, null} + ], + [ + {<<"CodeSha256">>, <<"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=">>}, + {<<"CodeSize">>, 848}, + {<<"Description">>, <<>>}, + {<<"FunctionArn">>, <<"arn:aws:lambda:us-east-1:352283894008:function:name">>}, + {<<"FunctionName">>, <<"name">>}, + {<<"Handler">>, <<"index.process">>}, + {<<"LastModified">>, <<"2015-12-10T13:57:48.214+0000">>}, + {<<"MemorySize">>, 512}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, + {<<"Runtime">>, <<"nodejs">>}, + {<<"Timeout">>, 30}, + {<<"Version">>, <<"$LATEST">>}, + {<<"VpcConfig">>, null} + ], + [ + {<<"CodeSha256">>, <<"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=">>}, + {<<"CodeSize">>, 848}, + {<<"Description">>, <<>>}, + {<<"FunctionArn">>, <<"arn:aws:lambda:us-east-1:352283894008:function:name2">>}, + {<<"FunctionName">>, <<"name2">>}, + {<<"Handler">>, <<"index.process">>}, + {<<"LastModified">>, <<"2015-12-10T11:31:21.106+0000">>}, + {<<"MemorySize">>, 128}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_basic_execution">>}, + {<<"Runtime">>, <<"nodejs">>}, + {<<"Timeout">>, 3}, + {<<"Version">>, <<"$LATEST">>}, + {<<"VpcConfig">>, null} + ] + ]}, + {<<"NextMarker">>, null} + ]) }. mocked_list_versions_by_function() -> { - [?BASE_URL ++ "functions/name/versions?", get, '_', <<>>, '_', '_'], - make_response(<<"{\"NextMarker\":null,\"Versions\":[{\"CodeSha256\":\"z" -"eoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=\",\"CodeSize\":848,\"Description" -"\":\"\",\"FunctionArn\":\"arn:aws:lambda:us-east-1:352283894008:function:name:" -"$LATEST\",\"FunctionName\":\"name\",\"Handler\":\"index.process\",\"LastModif" -"ied\":\"2015-12-10T13:57:48.214+0000\",\"MemorySize\":512,\"Role\":\"arn:aws:" -"iam::352283894008:role/lambda_kinesis_role\",\"Runtime\":\"nodejs\",\"Timeout" -"\":30,\"Version\":\"$LATEST\",\"VpcConfig\":null},{\"CodeSha256\":\"zeoBX1hIW" -"JBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=\",\"CodeSize\":848,\"Description\":\"\"," -"\"FunctionArn\":\"arn:aws:lambda:us-east-1:352283894008:function:name:1\",\"Fu" -"nctionName\":\"name\",\"Handler\":\"index.process\",\"LastModified\":\"2015-1" -"2-10T11:36:12.776+0000\",\"MemorySize\":128,\"Role\":\"arn:aws:iam::352283894" -"008:role/lambda_kinesis_role\",\"Runtime\":\"nodejs\",\"Timeout\":3,\"Version" -"\":\"1\",\"VpcConfig\":null},{\"CodeSha256\":\"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT" -"0Ct4IcpiPf8QM=\",\"CodeSize\":848,\"Description\":\"\",\"FunctionArn\":\"arn:" -"aws:lambda:us-east-1:352283894008:function:name:2\",\"FunctionName\":\"name\"" -",\"Handler\":\"index.process\",\"LastModified\":\"2015-12-10T13:56:43.171+000" -"0\",\"MemorySize\":128,\"Role\":\"arn:aws:iam::352283894008:role/lambda_kines" -"is_role\",\"Runtime\":\"nodejs\",\"Timeout\":3,\"Version\":\"2\",\"VpcConfig" -"\":null}]}">>) + [?BASE_URL ++ "functions/name/versions", get, '_', <<>>, '_', '_'], + make_response([ + {<<"NextMarker">>, null}, + {<<"Versions">>, [ + [ + {<<"CodeSha256">>, <<"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=">>}, + {<<"CodeSize">>, 848}, + {<<"Description">>, <<>>}, + {<<"FunctionArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:name:$LATEST">>}, + {<<"FunctionName">>, <<"name">>}, + {<<"Handler">>, <<"index.process">>}, + {<<"LastModified">>, <<"2015-12-10T13:57:48.214+0000">>}, + {<<"MemorySize">>, 512}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, + {<<"Runtime">>, <<"nodejs">>}, + {<<"Timeout">>, 30}, + {<<"Version">>, <<"$LATEST">>}, + {<<"VpcConfig">>, null} + ], + [ + {<<"CodeSha256">>, <<"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=">>}, + {<<"CodeSize">>, 848}, + {<<"Description">>, <<>>}, + {<<"FunctionArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:name:1">>}, + {<<"FunctionName">>, <<"name">>}, + {<<"Handler">>, <<"index.process">>}, + {<<"LastModified">>, <<"2015-12-10T11:36:12.776+0000">>}, + {<<"MemorySize">>, 128}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, + {<<"Runtime">>, <<"nodejs">>}, + {<<"Timeout">>, 3}, + {<<"Version">>, <<"1">>}, + {<<"VpcConfig">>, null} + ], + [ + {<<"CodeSha256">>, <<"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=">>}, + {<<"CodeSize">>, 848}, + {<<"Description">>, <<>>}, + {<<"FunctionArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:name:2">>}, + {<<"FunctionName">>, <<"name">>}, + {<<"Handler">>, <<"index.process">>}, + {<<"LastModified">>, <<"2015-12-10T13:56:43.171+0000">>}, + {<<"MemorySize">>, 128}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, + {<<"Runtime">>, <<"nodejs">>}, + {<<"Timeout">>, 3}, + {<<"Version">>, <<"2">>}, + {<<"VpcConfig">>, null} + ] + ]} + ]) }. mocked_publish_version() -> { - [?BASE_URL ++ "functions/name/versions", post, '_', <<"{}">>, '_', '_'], - make_response(<<"{\"CodeSha256\":\"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4Ic" -"piPf8QM=\",\"CodeSize\":848,\"Description\":\"\",\"FunctionArn\":\"arn:aws:la" -"mbda:us-east-1:352283894008:function:name:3\",\"FunctionName\":\"name\",\"Han" -"dler\":\"index.process\",\"LastModified\":\"2015-12-10T13:57:48.214+0000\",\"" -"MemorySize\":512,\"Role\":\"arn:aws:iam::352283894008:role/lambda_kinesis_rol" -"e\",\"Runtime\":\"nodejs\",\"Timeout\":30,\"Version\":\"3\",\"VpcConfig\":nul" -"l}">>) + [?BASE_URL ++ "functions/name/versions", post, '_', <<"{}">>, '_', '_'], + make_response([ + {<<"CodeSha256">>, <<"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=">>}, + {<<"CodeSize">>, 848}, + {<<"Description">>, <<>>}, + {<<"FunctionArn">>, <<"arn:aws:lambda:us-east-1:352283894008:function:name:3">>}, + {<<"FunctionName">>, <<"name">>}, + {<<"Handler">>, <<"index.process">>}, + {<<"LastModified">>, <<"2015-12-10T13:57:48.214+0000">>}, + {<<"MemorySize">>, 512}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, + {<<"Runtime">>, <<"nodejs">>}, + {<<"Timeout">>, 30}, + {<<"Version">>, <<"3">>}, + {<<"VpcConfig">>, null} + ]) }. mocked_update_alias() -> { - [?BASE_URL ++ "functions/name/aliases/aliasName", put, - '_', <<"{}">>, '_', '_'], - make_response(<<"{\"AliasArn\":\"arn:aws:lambda:us-east-1:352283894008:" -"function:name:aliasName\",\"Description\":\"\",\"FunctionVersion\":\"$LATEST" -"\",\"Name\":\"aliasName\"}">>) + [ + ?BASE_URL ++ "functions/name/aliases/aliasName", + put, + '_', + <<"{}">>, + '_', + '_' + ], + make_response([ + {<<"AliasArn">>, <<"arn:aws:lambda:us-east-1:352283894008:function:name:aliasName">>}, + {<<"Description">>, <<>>}, + {<<"FunctionVersion">>, <<"$LATEST">>}, + {<<"Name">>, <<"aliasName">>} + ]) }. mocked_update_event_source_mapping() -> { - [?BASE_URL ++ "event-source-mappings/a45b58ec-a539-4c47-929e-174b4dd2d963", - put, '_', <<"{\"BatchSize\":100}">>, '_', '_'], - make_response(<<"{\"BatchSize\":100,\"EventSourceArn\":\"arn:aws:kinesi" -"s:us-east-1:352283894008:stream/eholland-test\",\"FunctionArn\":\"arn:aws:lam" -"bda:us-east-1:352283894008:function:name\",\"LastModified\":1449844011.991,\"" -"LastProcessingResult\":\"No records processed\",\"State\":\"Updating\",\"Stat" -"eTransitionReason\":\"User action\",\"UUID\":\"a45b58ec-a539-4c47-929e-174b4d" -"d2d963\"}">>) + [ + ?BASE_URL ++ "event-source-mappings/a45b58ec-a539-4c47-929e-174b4dd2d963", + put, + '_', + <<"{\"BatchSize\":100}">>, + '_', + '_' + ], + make_response([ + {<<"BatchSize">>, 100}, + {<<"EventSourceArn">>, + <<"arn:aws:kinesis:us-east-1:352283894008:stream/eholland-test">>}, + {<<"FunctionArn">>, <<"arn:aws:lambda:us-east-1:352283894008:function:name">>}, + {<<"LastModified">>, 1449844011.991}, + {<<"LastProcessingResult">>, <<"No records processed">>}, + {<<"State">>, <<"Updating">>}, + {<<"StateTransitionReason">>, <<"User action">>}, + {<<"UUID">>, <<"a45b58ec-a539-4c47-929e-174b4dd2d963">>} + ]) }. mocked_update_function_code() -> { - [?BASE_URL ++ "functions/name/code", put, '_', - <<"{\"Publish\":true,\"S3Bucket\":\"bi-lambda\",\"S3Key\":\"local_transf" + [ + ?BASE_URL ++ "functions/name/code", + put, + '_', + <<"{\"Publish\":true,\"S3Bucket\":\"bi-lambda\",\"S3Key\":\"local_transf" "orm/bi-assets-environment-create-environment_1-0-0_latest.zip\"}">>, - '_', '_'], - make_response(<<"{\"CodeSha256\":\"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4Ic" -"piPf8QM=\",\"CodeSize\":848,\"Description\":\"\",\"FunctionArn\":\"arn:aws:la" -"mbda:us-east-1:352283894008:function:name:4\",\"FunctionName\":\"name\",\"Han" -"dler\":\"index.process\",\"LastModified\":\"2015-12-11T14:29:17.023+0000\",\"" -"MemorySize\":512,\"Role\":\"arn:aws:iam::352283894008:role/lambda_kinesis_rol" -"e\",\"Runtime\":\"nodejs\",\"Timeout\":30,\"Version\":\"4\",\"VpcConfig\":nul" -"l}">>) + '_', + '_' + ], + make_response([ + {<<"CodeSha256">>, <<"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=">>}, + {<<"CodeSize">>, 848}, + {<<"Description">>, <<>>}, + {<<"FunctionArn">>, <<"arn:aws:lambda:us-east-1:352283894008:function:name:4">>}, + {<<"FunctionName">>, <<"name">>}, + {<<"Handler">>, <<"index.process">>}, + {<<"LastModified">>, <<"2015-12-11T14:29:17.023+0000">>}, + {<<"MemorySize">>, 512}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, + {<<"Runtime">>, <<"nodejs">>}, + {<<"Timeout">>, 30}, + {<<"Version">>, <<"4">>}, + {<<"VpcConfig">>, null} + ]) }. mocked_update_function_configuration() -> { - [?BASE_URL ++ "functions/name/configuration", put, '_', - <<"{\"MemorySize\":512,\"Timeout\":30}">>, '_', '_'], - make_response(<<"{\"CodeSha256\":\"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4Ic" -"piPf8QM=\",\"CodeSize\":848,\"Description\":\"\",\"FunctionArn\":\"arn:aws:la" -"mbda:us-east-1:352283894008:function:name\",\"FunctionName\":\"name\",\"Handl" -"er\":\"index.process\",\"LastModified\":\"2015-12-11T14:31:52.034+0000\",\"Me" -"morySize\":512,\"Role\":\"arn:aws:iam::352283894008:role/lambda_kinesis_role\" -"",\"Runtime\":\"nodejs\",\"Timeout\":30,\"Version\":\"$LATEST\",\"VpcConfig\"" -":null}">>) + [ + ?BASE_URL ++ "functions/name/configuration", + put, + '_', + <<"{\"MemorySize\":512,\"Timeout\":30}">>, + '_', + '_' + ], + make_response([ + {<<"CodeSha256">>, <<"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=">>}, + {<<"CodeSize">>, 848}, + {<<"Description">>, <<>>}, + {<<"FunctionArn">>, <<"arn:aws:lambda:us-east-1:352283894008:function:name">>}, + {<<"FunctionName">>, <<"name">>}, + {<<"Handler">>, <<"index.process">>}, + {<<"LastModified">>, <<"2015-12-11T14:31:52.034+0000">>}, + {<<"MemorySize">>, 512}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, + {<<"Runtime">>, <<"nodejs">>}, + {<<"Timeout">>, 30}, + {<<"Version">>, <<"$LATEST">>}, + {<<"VpcConfig">>, null} + ]) }. api_tests(_) -> [ - fun() -> - Result = erlcloud_lambda:list_functions(), - Expected = - {ok, - [{<<"Functions">>, - [[{<<"CodeSha256">>,<<"XmLDAZXEkl5KbA8ezZpwFU+bjgTXBehUmWGOScl4F2A=">>}, - {<<"CodeSize">>,5561}, - {<<"Description">>,<<>>}, - {<<"FunctionArn">>,<<"arn:aws:lambda:us-east-1:352283894008:function:bi-assets-asset-create-host">>}, - {<<"FunctionName">>,<<"bi-assets-asset-create-host">>}, - {<<"Handler">>,<<"index.handler">>}, - {<<"LastModified">>,<<"2015-11-27T09:55:12.973+0000">>}, - {<<"MemorySize">>,128}, - {<<"Role">>,<<"arn:aws:iam::352283894008:role/lambda_basic_execution">>}, - {<<"Runtime">>,<<"nodejs">>}, - {<<"Timeout">>,3},{<<"Version">>,<<"$LATEST">>}, - {<<"VpcConfig">>,null}], - [{<<"CodeSha256">>,<<"XmLDAZXEkl5KbA8ezZpwFU+bjgTXBehUmWGOScl4F2A=">>}, - {<<"CodeSize">>,5561}, - {<<"Description">>,<<>>}, - {<<"FunctionArn">>,<<"arn:aws:lambda:us-east-1:352283894008:function:bi-assets-asset-create-host1">>}, - {<<"FunctionName">>,<<"bi-assets-asset-create-host1">>}, - {<<"Handler">>,<<"index.handler">>}, - {<<"LastModified">>,<<"2015-12-01T11:20:44.464+0000">>}, - {<<"MemorySize">>,128}, - {<<"Role">>,<<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, - {<<"Runtime">>,<<"nodejs">>}, - {<<"Timeout">>,10}, - {<<"Version">>,<<"$LATEST">>}, - {<<"VpcConfig">>,null}], - [{<<"CodeSha256">>,<<"tZJ+kUZVD1vGYwMIUvAoaZmvS4I9NHVc7a/267eChYY=">>}, - {<<"CodeSize">>,132628}, - {<<"Description">>,<<>>}, - {<<"FunctionArn">>,<<"arn:aws:lambda:us-east-1:352283894008:function:bi-driver">>}, - {<<"FunctionName">>,<<"bi-driver">>}, - {<<"Handler">>,<<"index.handler">>}, - {<<"LastModified">>,<<"2015-12-03T13:59:05.219+0000">>}, - {<<"MemorySize">>,1024}, - {<<"Role">>,<<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, - {<<"Runtime">>,<<"nodejs">>}, - {<<"Timeout">>,59},{<<"Version">>,<<"$LATEST">>}, - {<<"VpcConfig">>,null}], - [{<<"CodeSha256">>,<<"QS10seyYGXrrhAnGMbJcTi+JOa4HWLaD+9YCLYG3+VE=">>}, - {<<"CodeSize">>,121486}, - {<<"Description">>,<<"An Amazon Kinesis stream processor that logs the data being published.">>}, - {<<"FunctionArn">>,<<"arn:aws:lambda:us-east-1:352283894008:function:eholland-js-router">>}, - {<<"FunctionName">>,<<"eholland-js-router">>}, - {<<"Handler">>,<<"index.handler">>}, - {<<"LastModified">>,<<"2015-12-02T14:50:41.923+0000">>}, - {<<"MemorySize">>,1024}, - {<<"Role">>,<<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, - {<<"Runtime">>,<<"nodejs">>}, - {<<"Timeout">>,60}, - {<<"Version">>,<<"$LATEST">>}, - {<<"VpcConfig">>,null}], - [{<<"CodeSha256">>,<<"ey+6CSWe750XoPSdVSQditxxoHNWmPFwve/MLPNs/Do=">>}, - {<<"CodeSize">>,253}, - {<<"Description">>,<<>>}, - {<<"FunctionArn">>,<<"arn:aws:lambda:us-east-1:352283894008:function:eholland-test-aims-user">>}, - {<<"FunctionName">>,<<"eholland-test-aims-user">>}, - {<<"Handler">>,<<"lambda_function.lambda_handler">>}, - {<<"LastModified">>,<<"2015-11-09T17:32:46.030+0000">>}, - {<<"MemorySize">>,128}, - {<<"Role">>,<<"arn:aws:iam::352283894008:role/lambda_basic_execution">>}, - {<<"Runtime">>,<<"python2.7">>}, - {<<"Timeout">>,3}, - {<<"Version">>,<<"$LATEST">>}, - {<<"VpcConfig">>,null}], - [{<<"CodeSha256">>,<<"R9HaKKCPPAem0ikKrkMGpDtX95M8egDCDd+4Ws+Kk5c=">>}, - {<<"CodeSize">>,253}, - {<<"Description">>,<<"aims users transform function">>}, - {<<"FunctionArn">>,<<"arn:aws:lambda:us-east-1:352283894008:function:eholland-test-aims-users">>}, - {<<"FunctionName">>,<<"eholland-test-aims-users">>}, - {<<"Handler">>,<<"lambda_function.lambda_handler">>}, - {<<"LastModified">>,<<"2015-11-02T13:59:32.509+0000">>}, - {<<"MemorySize">>,128}, - {<<"Role">>,<<"arn:aws:iam::352283894008:role/lambda_basic_execution">>}, - {<<"Runtime">>,<<"python2.7">>}, - {<<"Timeout">>,3}, - {<<"Version">>,<<"$LATEST">>}, - {<<"VpcConfig">>,null}], - [{<<"CodeSha256">>,<<"PGG1vCAQpc8J7e4HL1z1Pv4DGSZEmhnJaNPTGDS29kk=">>}, - {<<"CodeSize">>,32372}, - {<<"Description">>,<<"An Amazon Kinesis stream processor that logs the data being published.">>}, - {<<"FunctionArn">>,<<"arn:aws:lambda:us-east-1:352283894008:function:eholland-test-router">>}, - {<<"FunctionName">>,<<"eholland-test-router">>}, - {<<"Handler">>,<<"ProcessKinesisRecords.lambda_handler">>}, - {<<"LastModified">>,<<"2015-11-09T10:58:50.458+0000">>}, - {<<"MemorySize">>,256}, - {<<"Role">>,<<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, - {<<"Runtime">>,<<"python2.7">>}, - {<<"Timeout">>,59}, - {<<"Version">>, <<"$LATEST">>}, - {<<"VpcConfig">>,null}], - [{<<"CodeSha256">>,<<"8twXwGaAXyr8Li81rGhM6vlMh+dvXglwqgIshfaio+U=">>}, - {<<"CodeSize">>,708740}, - {<<"Description">>,<<"Lambda Router Function for ETL Service">>}, - {<<"FunctionArn">>,<<"arn:aws:lambda:us-east-1:352283894008:function:us-east-1_base_eholland_master_lambda_route">>}, - {<<"FunctionName">>,<<"us-east-1_base_eholland_master_lambda_route">>}, - {<<"Handler">>,<<"ProcessKinesisRecords.handler">>}, - {<<"LastModified">>,<<"2015-11-11T10:13:19.043+0000">>}, - {<<"MemorySize">>,512}, - {<<"Role">>,<<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, - {<<"Runtime">>,<<"nodejs">>}, - {<<"Timeout">>,59}, - {<<"Version">>,<<"$LATEST">>}, - {<<"VpcConfig">>,null}], - [{<<"CodeSha256">>,<<"aDwLJhljsMVHsaN+LM4jRmsSLKKBdS+eFrAhKmJ/zbQ=">>}, - {<<"CodeSize">>,253}, - {<<"Description">>,<<>>}, - {<<"FunctionArn">>,<<"arn:aws:lambda:us-east-1:352283894008:function:us-east-1_base_eholland_master_lambda_route-aims-user-create">>}, - {<<"FunctionName">>,<<"us-east-1_base_eholland_master_lambda_route-aims-user-create">>}, - {<<"Handler">>,<<"lambda_function.lambda_handler">>}, - {<<"LastModified">>,<<"2015-11-11T09:42:10.303+0000">>}, - {<<"MemorySize">>,128}, - {<<"Role">>,<<"arn:aws:iam::352283894008:role/lambda_basic_execution">>}, - {<<"Runtime">>,<<"python2.7">>}, - {<<"Timeout">>,3}, - {<<"Version">>,<<"$LATEST">>}, - {<<"VpcConfig">>,null}], - [{<<"CodeSha256">>,<<"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=">>}, - {<<"CodeSize">>,848}, - {<<"Description">>,<<>>}, - {<<"FunctionArn">>,<<"arn:aws:lambda:us-east-1:352283894008:function:name">>}, - {<<"FunctionName">>,<<"name">>}, - {<<"Handler">>,<<"index.process">>}, - {<<"LastModified">>,<<"2015-12-10T13:57:48.214+0000">>}, - {<<"MemorySize">>,512}, - {<<"Role">>,<<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, - {<<"Runtime">>,<<"nodejs">>}, - {<<"Timeout">>,30}, - {<<"Version">>,<<"$LATEST">>}, - {<<"VpcConfig">>,null}], - [{<<"CodeSha256">>,<<"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=">>}, - {<<"CodeSize">>,848}, - {<<"Description">>,<<>>}, - {<<"FunctionArn">>,<<"arn:aws:lambda:us-east-1:352283894008:function:name2">>}, - {<<"FunctionName">>,<<"name2">>}, - {<<"Handler">>,<<"index.process">>}, - {<<"LastModified">>,<<"2015-12-10T11:31:21.106+0000">>}, - {<<"MemorySize">>,128}, - {<<"Role">>,<<"arn:aws:iam::352283894008:role/lambda_basic_execution">>}, - {<<"Runtime">>,<<"nodejs">>}, - {<<"Timeout">>,3}, - {<<"Version">>,<<"$LATEST">>}, - {<<"VpcConfig">>,null}]]}, - {<<"NextMarker">>,null}]}, - ?assertEqual(Expected, Result) - end, - fun() -> - Result = erlcloud_lambda:create_alias(<<"name">>, <<"$LATEST">>, - <<"aliasName1">>, []), - Expected = {ok,[{<<"AliasArn">>, - <<"arn:aws:lambda:us-east-1:352283894008:function:name:aliasName1">>}, - {<<"Description">>,<<>>}, - {<<"FunctionVersion">>,<<"$LATEST">>}, - {<<"Name">>,<<"aliasName1">>}]}, - ?assertEqual(Expected, Result) - end, - fun() -> - Result = erlcloud_lambda:create_event_source_mapping( - <<"arn:aws:kinesis:us-east-1:352283894008:stream/eholland-test">>, - <<"name">>, <<"TRIM_HORIZON">>, []), - Expected = {ok, - [{<<"BatchSize">>,100}, - {<<"EventSourceArn">>,<<"arn:aws:kinesis:us-east-1:352283894008:stream/eholland-test">>}, - {<<"FunctionArn">>,<<"arn:aws:lambda:us-east-1:352283894008:function:name">>}, - {<<"LastModified">>,1449845416.123}, - {<<"LastProcessingResult">>,<<"No records processed">>}, - {<<"State">>,<<"Creating">>}, - {<<"StateTransitionReason">>,<<"User action">>}, - {<<"UUID">>,<<"3f303f86-7395-43f3-9902-f5c80f0a5382">>}]}, - ?assertEqual(Expected, Result) - end, - fun() -> - Result = erlcloud_lambda:create_function( - #erlcloud_lambda_code{s3Bucket = <<"bi-lambda">>, - s3Key = <<"local_transform/bi-ass" - "ets-environment-create" - "-environment_1-0-0_lat" - "est.zip">> - }, - <<"name">>, <<"index.process">>, - <<"arn:aws:iam::352283894008:role/lambda_basic_execution">>, - nodejs, []), - Expected = {ok,[{<<"CodeSha256">>, - <<"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=">>}, - {<<"CodeSize">>,848}, - {<<"Description">>,<<>>}, - {<<"FunctionArn">>, - <<"arn:aws:lambda:us-east-1:352283894008:function:name3">>}, - {<<"FunctionName">>,<<"name3">>}, - {<<"Handler">>,<<"index.process">>}, - {<<"LastModified">>,<<"2015-12-11T13:45:31.924+0000">>}, - {<<"MemorySize">>,128}, - {<<"Role">>, - <<"arn:aws:iam::352283894008:role/lambda_basic_execution">>}, - {<<"Runtime">>,<<"nodejs">>}, - {<<"Timeout">>,3}, - {<<"Version">>,<<"$LATEST">>}, - {<<"VpcConfig">>,null}]}, - ?assertEqual(Expected, Result) - end, - fun() -> - Result = erlcloud_lambda:delete_event_source_mapping( - <<"6554f300-551b-46a6-829c-41b6af6022c6">>), - Expected = {ok, - [{<<"BatchSize">>,100}, - {<<"EventSourceArn">>,<<"arn:aws:kinesis:us-east-1:352283894008:stream/eholland-test">>}, - {<<"FunctionArn">>,<<"arn:aws:lambda:us-east-1:352283894008:function:name">>}, - {<<"LastModified">>,1449843960.0}, - {<<"LastProcessingResult">>,<<"No records processed">>}, - {<<"State">>,<<"Deleting">>}, - {<<"StateTransitionReason">>,<<"User action">>}, - {<<"UUID">>,<<"a45b58ec-a539-4c47-929e-174b4dd2d963">>}]}, - ?assertEqual(Expected, Result) - end, - fun() -> - Result = erlcloud_lambda:get_alias(<<"name">>, <<"aliasName">>), - Expected = {ok, [{<<"AliasArn">>, - <<"arn:aws:lambda:us-east-1:352283894008:function:name:aliasName">>}, - {<<"Description">>,<<>>}, - {<<"FunctionVersion">>,<<"$LATEST">>}, - {<<"Name">>,<<"aliasName">>}]}, - ?assertEqual(Expected, Result) - end, - fun() -> - Result = erlcloud_lambda:get_event_source_mapping( - <<"a45b58ec-a539-4c47-929e-174b4dd2d963">>), - Expected = {ok, [{<<"BatchSize">>,100}, - {<<"EventSourceArn">>, - <<"arn:aws:kinesis:us-east-1:352283894008:stream/eholland-test">>}, - {<<"FunctionArn">>, - <<"arn:aws:lambda:us-east-1:352283894008:function:name">>}, - {<<"LastModified">>,1449841860.0}, - {<<"LastProcessingResult">>,<<"No records processed">>}, - {<<"State">>,<<"Enabled">>}, - {<<"StateTransitionReason">>,<<"User action">>}, - {<<"UUID">>,<<"a45b58ec-a539-4c47-929e-174b4dd2d963">>}]}, - ?assertEqual(Expected, Result) - end, - fun() -> - Result = erlcloud_lambda:get_function(<<"name">>), - Expected = {ok, [{<<"Code">>, - [{<<"Location">>, <<"https://awslambda-us-east-1-tasks.s3-us-east-1.amazonaws.com/snapshots/352283894008/name-69237aec-bae9-4086-af73-a6610c2f8eb8?x-amz-security-token=AQoDYXdzENb%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEa4APJSJNHsYWnObKlElri5ytVzNg0t%2F0zwADHn40jy%2F1ZDW9%2FYWGi8dTp1l6LWBI9TwJi0LLcgp%2FlCxJIh7hsAPftYX62J9r9lRcmgd9RnYssg1%2Fkpfyjya90epxKg2zdHm%2BuZGukHYDcmAE1IQcHwsaQbvGAjXPCpFyxClbV6gMcFIsaBtfMxoMcbTCXG9m8l56nKgcX6Mi60vRNaB83AeNVrKMhB8EBUUbYbaB%2BG0iJg32i2HBF6VJMxamOLIEf1GJp1tWt%2FSAHfEkdTwcwtGINH3TNRv%2BY3ddsXs8pJ49eY49NCHANPC%2Bq0JzNQydbIK1shz8w1nozXYQo6%2BNh9tqOlaJNFgfFbtJkUDXv4rFqgVsfgJKJSQBeYUKmlNvIPQIoHWhRjjRzQUGmYDc3eEug7vELsNcHZixI4nNVycH%2BJZwaBvswy4eE7gBv3HwHi3SVlg9iXFTrfWTK%2FlCybC7mZIjAmPGiLCG5Pu8SoCgaGdHp8HmSeXXus4VUFcVTUw1qn7E%2BSaRFg3MTpCdu1f1Nqh4pNu7GpZacyLbH%2BSocuPyTjyYGL8sk0C3rjIWZipJcfUZsleS1cXLEGw%2FPAK5eg0RNWlVsaF1WgVimqQh3LtoRZ%2BwYCHRXsHjhCyQ8VhoguJCrswU%3D&AWSAccessKeyId=ASIAIAUY4IMA6JRJ3FSA&Expires=1449843004&Signature=5KUmber1cOBSChlKtnLaI%2FUemU4%3D">>}, - {<<"RepositoryType">>,<<"S3">>}]}, - {<<"Configuration">>, - [{<<"CodeSha256">>,<<"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=">>}, - {<<"CodeSize">>,848}, - {<<"Description">>,<<>>}, - {<<"FunctionArn">>,<<"arn:aws:lambda:us-east-1:352283894008:function:name">>}, - {<<"FunctionName">>,<<"name">>}, - {<<"Handler">>,<<"index.process">>}, - {<<"LastModified">>,<<"2015-12-10T13:57:48.214+0000">>}, - {<<"MemorySize">>,512}, - {<<"Role">>,<<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, - {<<"Runtime">>,<<"nodejs">>}, - {<<"Timeout">>,30}, - {<<"Version">>,<<"$LATEST">>}, - {<<"VpcConfig">>,null}]}]}, - ?assertEqual(Expected, Result) - end, - fun() -> - Result = erlcloud_lambda:get_function_configuration(<<"name">>), - Expected = {ok, [{<<"CodeSha256">>,<<"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=">>}, - {<<"CodeSize">>,848}, - {<<"Description">>,<<>>}, - {<<"FunctionArn">>,<<"arn:aws:lambda:us-east-1:352283894008:function:name">>}, - {<<"FunctionName">>,<<"name">>}, - {<<"Handler">>,<<"index.process">>}, - {<<"LastModified">>,<<"2015-12-10T13:57:48.214+0000">>}, - {<<"MemorySize">>,512}, - {<<"Role">>,<<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, - {<<"Runtime">>,<<"nodejs">>}, - {<<"Timeout">>,30}, - {<<"Version">>,<<"$LATEST">>}, - {<<"VpcConfig">>,null}]}, - ?assertEqual(Expected, Result) - end, - fun() -> - Result = erlcloud_lambda:invoke(<<"name">>, []), - Expected = {ok, [{<<"message">>, <<"Hello World!">>}]}, - ?assertEqual(Expected, Result) - end, - fun() -> - Result = erlcloud_lambda:invoke(<<"name">>, [], [{show_headers, true}], #aws_config{}), - Expected = {ok, [], [{<<"message">>, <<"Hello World!">>}]}, - ?assertEqual(Expected, Result) - end, - fun() -> - Result = erlcloud_lambda:list_aliases(<<"name">>), - Expected = {ok, [{<<"Aliases">>, - [[{<<"AliasArn">>, <<"arn:aws:lambda:us-east-1:352283894008:function:name:aliasName">>}, - {<<"Description">>,<<>>}, - {<<"FunctionVersion">>,<<"$LATEST">>}, - {<<"Name">>,<<"aliasName">>}], - [{<<"AliasArn">>,<<"arn:aws:lambda:us-east-1:352283894008:function:name:aliasName1">>}, - {<<"Description">>,<<>>}, - {<<"FunctionVersion">>,<<"$LATEST">>}, - {<<"Name">>,<<"aliasName1">>}]]}, - {<<"NextMarker">>,null}]}, - ?assertEqual(Expected, Result) - end, - fun() -> - Result = erlcloud_lambda:list_event_source_mappings( - <<"arn:aws:kinesis:us-east-1:352283894008:stream/eholland-test">>, - <<"name">>), - Expected = {ok, [{<<"EventSourceMappings">>, - [[{<<"BatchSize">>,100}, - {<<"EventSourceArn">>,<<"arn:aws:kinesis:us-east-1:352283894008:stream/eholland-test">>}, - {<<"FunctionArn">>,<<"arn:aws:lambda:us-east-1:352283894008:function:name">>}, - {<<"LastModified">>,1449841860.0}, - {<<"LastProcessingResult">>,<<"No records processed">>}, - {<<"State">>,<<"Enabled">>}, - {<<"StateTransitionReason">>,<<"User action">>}, - {<<"UUID">>,<<"a45b58ec-a539-4c47-929e-174b4dd2d963">>}]]}, - {<<"NextMarker">>,null}]}, - ?assertEqual(Expected, Result) - end, - fun() -> - Result = erlcloud_lambda:list_versions_by_function(<<"name">>), - Expected = {ok, [{<<"NextMarker">>,null}, - {<<"Versions">>, - [[{<<"CodeSha256">>, <<"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=">>}, - {<<"CodeSize">>, 848}, - {<<"Description">>, <<>>}, - {<<"FunctionArn">>,<<"arn:aws:lambda:us-east-1:352283894008:function:name:$LATEST">>}, - {<<"FunctionName">>,<<"name">>}, - {<<"Handler">>,<<"index.process">>}, - {<<"LastModified">>,<<"2015-12-10T13:57:48.214+0000">>}, - {<<"MemorySize">>,512}, - {<<"Role">>,<<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, - {<<"Runtime">>,<<"nodejs">>}, - {<<"Timeout">>,30}, - {<<"Version">>,<<"$LATEST">>}, - {<<"VpcConfig">>,null}], - [{<<"CodeSha256">>,<<"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=">>}, - {<<"CodeSize">>,848}, - {<<"Description">>,<<>>}, - {<<"FunctionArn">>,<<"arn:aws:lambda:us-east-1:352283894008:function:name:1">>}, - {<<"FunctionName">>,<<"name">>}, - {<<"Handler">>,<<"index.process">>}, - {<<"LastModified">>,<<"2015-12-10T11:36:12.776+0000">>}, - {<<"MemorySize">>,128}, - {<<"Role">>,<<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, - {<<"Runtime">>,<<"nodejs">>}, - {<<"Timeout">>,3}, - {<<"Version">>,<<"1">>}, - {<<"VpcConfig">>,null}], - [{<<"CodeSha256">>,<<"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=">>}, - {<<"CodeSize">>,848}, - {<<"Description">>,<<>>}, - {<<"FunctionArn">>,<<"arn:aws:lambda:us-east-1:352283894008:function:name:2">>}, - {<<"FunctionName">>,<<"name">>}, - {<<"Handler">>,<<"index.process">>}, - {<<"LastModified">>,<<"2015-12-10T13:56:43.171+0000">>}, - {<<"MemorySize">>,128}, - {<<"Role">>,<<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, - {<<"Runtime">>,<<"nodejs">>}, - {<<"Timeout">>,3}, - {<<"Version">>,<<"2">>}, - {<<"VpcConfig">>,null}]]}]}, - ?assertEqual(Expected, Result) - end, - fun() -> - Result = erlcloud_lambda:publish_version(<<"name">>), - Expected = {ok, [{<<"CodeSha256">>,<<"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=">>}, - {<<"CodeSize">>,848}, - {<<"Description">>,<<>>}, - {<<"FunctionArn">>,<<"arn:aws:lambda:us-east-1:352283894008:function:name:3">>}, - {<<"FunctionName">>,<<"name">>}, - {<<"Handler">>,<<"index.process">>}, - {<<"LastModified">>,<<"2015-12-10T13:57:48.214+0000">>}, - {<<"MemorySize">>,512}, - {<<"Role">>,<<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, - {<<"Runtime">>,<<"nodejs">>}, - {<<"Timeout">>,30}, - {<<"Version">>,<<"3">>}, - {<<"VpcConfig">>,null}]}, - ?assertEqual(Expected, Result) - end, - fun() -> - Result = erlcloud_lambda:update_alias(<<"name">>, <<"aliasName">>), - Expected = {ok, [{<<"AliasArn">>,<<"arn:aws:lambda:us-east-1:352283894008:function:name:aliasName">>}, - {<<"Description">>,<<>>}, - {<<"FunctionVersion">>,<<"$LATEST">>}, - {<<"Name">>,<<"aliasName">>}]}, - ?assertEqual(Expected, Result) - end, - fun() -> - Result = erlcloud_lambda:update_event_source_mapping( - <<"a45b58ec-a539-4c47-929e-174b4dd2d963">>, - 100, undefined, undefined), - Expected = {ok, - [{<<"BatchSize">>,100}, - {<<"EventSourceArn">>,<<"arn:aws:kinesis:us-east-1:352283894008:stream/eholland-test">>}, - {<<"FunctionArn">>,<<"arn:aws:lambda:us-east-1:352283894008:function:name">>}, - {<<"LastModified">>,1449844011.991}, - {<<"LastProcessingResult">>,<<"No records processed">>}, - {<<"State">>,<<"Updating">>}, - {<<"StateTransitionReason">>,<<"User action">>}, - {<<"UUID">>,<<"a45b58ec-a539-4c47-929e-174b4dd2d963">>}]}, - ?assertEqual(Expected, Result) - end, - fun() -> - Result = erlcloud_lambda:update_function_code( - <<"name">>, true, #erlcloud_lambda_code{ - s3Bucket = <<"bi-lambda">>, - s3Key = <<"local_transform/bi-assets-environment-create-environment_1-0-0_latest.zip">> - }), - Expected = {ok, - [{<<"CodeSha256">>,<<"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=">>}, - {<<"CodeSize">>,848}, - {<<"Description">>,<<>>}, - {<<"FunctionArn">>,<<"arn:aws:lambda:us-east-1:352283894008:function:name:4">>}, - {<<"FunctionName">>,<<"name">>}, - {<<"Handler">>,<<"index.process">>}, - {<<"LastModified">>,<<"2015-12-11T14:29:17.023+0000">>}, - {<<"MemorySize">>,512}, - {<<"Role">>,<<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, - {<<"Runtime">>,<<"nodejs">>}, - {<<"Timeout">>,30}, - {<<"Version">>,<<"4">>}, - {<<"VpcConfig">>,null}]}, - ?assertEqual(Expected, Result) - end, - fun() -> - Result = erlcloud_lambda:update_function_configuration( - <<"name">>, undefined, undefined, 512, undefined, 30), - Expected = {ok, [{<<"CodeSha256">>,<<"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=">>}, - {<<"CodeSize">>,848}, - {<<"Description">>,<<>>}, - {<<"FunctionArn">>,<<"arn:aws:lambda:us-east-1:352283894008:function:name">>}, - {<<"FunctionName">>,<<"name">>}, - {<<"Handler">>,<<"index.process">>}, - {<<"LastModified">>,<<"2015-12-11T14:31:52.034+0000">>}, - {<<"MemorySize">>,512}, - {<<"Role">>,<<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, - {<<"Runtime">>,<<"nodejs">>}, - {<<"Timeout">>,30}, - {<<"Version">>,<<"$LATEST">>}, - {<<"VpcConfig">>,null}]}, - ?assertEqual(Expected, Result) - end + fun() -> + Result = erlcloud_lambda:list_functions(), + Expected = + {ok, [ + {<<"Functions">>, [ + [ + {<<"CodeSha256">>, <<"XmLDAZXEkl5KbA8ezZpwFU+bjgTXBehUmWGOScl4F2A=">>}, + {<<"CodeSize">>, 5561}, + {<<"Description">>, <<>>}, + {<<"FunctionArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:bi-assets-asset-create-host">>}, + {<<"FunctionName">>, <<"bi-assets-asset-create-host">>}, + {<<"Handler">>, <<"index.handler">>}, + {<<"LastModified">>, <<"2015-11-27T09:55:12.973+0000">>}, + {<<"MemorySize">>, 128}, + {<<"Role">>, + <<"arn:aws:iam::352283894008:role/lambda_basic_execution">>}, + {<<"Runtime">>, <<"nodejs">>}, + {<<"Timeout">>, 3}, + {<<"Version">>, <<"$LATEST">>}, + {<<"VpcConfig">>, null} + ], + [ + {<<"CodeSha256">>, <<"XmLDAZXEkl5KbA8ezZpwFU+bjgTXBehUmWGOScl4F2A=">>}, + {<<"CodeSize">>, 5561}, + {<<"Description">>, <<>>}, + {<<"FunctionArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:bi-assets-asset-create-host1">>}, + {<<"FunctionName">>, <<"bi-assets-asset-create-host1">>}, + {<<"Handler">>, <<"index.handler">>}, + {<<"LastModified">>, <<"2015-12-01T11:20:44.464+0000">>}, + {<<"MemorySize">>, 128}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, + {<<"Runtime">>, <<"nodejs">>}, + {<<"Timeout">>, 10}, + {<<"Version">>, <<"$LATEST">>}, + {<<"VpcConfig">>, null} + ], + [ + {<<"CodeSha256">>, <<"tZJ+kUZVD1vGYwMIUvAoaZmvS4I9NHVc7a/267eChYY=">>}, + {<<"CodeSize">>, 132628}, + {<<"Description">>, <<>>}, + {<<"FunctionArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:bi-driver">>}, + {<<"FunctionName">>, <<"bi-driver">>}, + {<<"Handler">>, <<"index.handler">>}, + {<<"LastModified">>, <<"2015-12-03T13:59:05.219+0000">>}, + {<<"MemorySize">>, 1024}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, + {<<"Runtime">>, <<"nodejs">>}, + {<<"Timeout">>, 59}, + {<<"Version">>, <<"$LATEST">>}, + {<<"VpcConfig">>, null} + ], + [ + {<<"CodeSha256">>, <<"QS10seyYGXrrhAnGMbJcTi+JOa4HWLaD+9YCLYG3+VE=">>}, + {<<"CodeSize">>, 121486}, + {<<"Description">>, + <<"An Amazon Kinesis stream processor that logs the data being published.">>}, + {<<"FunctionArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:eholland-js-router">>}, + {<<"FunctionName">>, <<"eholland-js-router">>}, + {<<"Handler">>, <<"index.handler">>}, + {<<"LastModified">>, <<"2015-12-02T14:50:41.923+0000">>}, + {<<"MemorySize">>, 1024}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, + {<<"Runtime">>, <<"nodejs">>}, + {<<"Timeout">>, 60}, + {<<"Version">>, <<"$LATEST">>}, + {<<"VpcConfig">>, null} + ], + [ + {<<"CodeSha256">>, <<"ey+6CSWe750XoPSdVSQditxxoHNWmPFwve/MLPNs/Do=">>}, + {<<"CodeSize">>, 253}, + {<<"Description">>, <<>>}, + {<<"FunctionArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:eholland-test-aims-user">>}, + {<<"FunctionName">>, <<"eholland-test-aims-user">>}, + {<<"Handler">>, <<"lambda_function.lambda_handler">>}, + {<<"LastModified">>, <<"2015-11-09T17:32:46.030+0000">>}, + {<<"MemorySize">>, 128}, + {<<"Role">>, + <<"arn:aws:iam::352283894008:role/lambda_basic_execution">>}, + {<<"Runtime">>, <<"python2.7">>}, + {<<"Timeout">>, 3}, + {<<"Version">>, <<"$LATEST">>}, + {<<"VpcConfig">>, null} + ], + [ + {<<"CodeSha256">>, <<"R9HaKKCPPAem0ikKrkMGpDtX95M8egDCDd+4Ws+Kk5c=">>}, + {<<"CodeSize">>, 253}, + {<<"Description">>, <<"aims users transform function">>}, + {<<"FunctionArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:eholland-test-aims-users">>}, + {<<"FunctionName">>, <<"eholland-test-aims-users">>}, + {<<"Handler">>, <<"lambda_function.lambda_handler">>}, + {<<"LastModified">>, <<"2015-11-02T13:59:32.509+0000">>}, + {<<"MemorySize">>, 128}, + {<<"Role">>, + <<"arn:aws:iam::352283894008:role/lambda_basic_execution">>}, + {<<"Runtime">>, <<"python2.7">>}, + {<<"Timeout">>, 3}, + {<<"Version">>, <<"$LATEST">>}, + {<<"VpcConfig">>, null} + ], + [ + {<<"CodeSha256">>, <<"PGG1vCAQpc8J7e4HL1z1Pv4DGSZEmhnJaNPTGDS29kk=">>}, + {<<"CodeSize">>, 32372}, + {<<"Description">>, + <<"An Amazon Kinesis stream processor that logs the data being published.">>}, + {<<"FunctionArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:eholland-test-router">>}, + {<<"FunctionName">>, <<"eholland-test-router">>}, + {<<"Handler">>, <<"ProcessKinesisRecords.lambda_handler">>}, + {<<"LastModified">>, <<"2015-11-09T10:58:50.458+0000">>}, + {<<"MemorySize">>, 256}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, + {<<"Runtime">>, <<"python2.7">>}, + {<<"Timeout">>, 59}, + {<<"Version">>, <<"$LATEST">>}, + {<<"VpcConfig">>, null} + ], + [ + {<<"CodeSha256">>, <<"8twXwGaAXyr8Li81rGhM6vlMh+dvXglwqgIshfaio+U=">>}, + {<<"CodeSize">>, 708740}, + {<<"Description">>, <<"Lambda Router Function for ETL Service">>}, + {<<"FunctionArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:us-east-1_base_eholland_master_lambda_route">>}, + {<<"FunctionName">>, <<"us-east-1_base_eholland_master_lambda_route">>}, + {<<"Handler">>, <<"ProcessKinesisRecords.handler">>}, + {<<"LastModified">>, <<"2015-11-11T10:13:19.043+0000">>}, + {<<"MemorySize">>, 512}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, + {<<"Runtime">>, <<"nodejs">>}, + {<<"Timeout">>, 59}, + {<<"Version">>, <<"$LATEST">>}, + {<<"VpcConfig">>, null} + ], + [ + {<<"CodeSha256">>, <<"aDwLJhljsMVHsaN+LM4jRmsSLKKBdS+eFrAhKmJ/zbQ=">>}, + {<<"CodeSize">>, 253}, + {<<"Description">>, <<>>}, + {<<"FunctionArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:us-east-1_base_eholland_master_lambda_route-aims-user-create">>}, + {<<"FunctionName">>, + <<"us-east-1_base_eholland_master_lambda_route-aims-user-create">>}, + {<<"Handler">>, <<"lambda_function.lambda_handler">>}, + {<<"LastModified">>, <<"2015-11-11T09:42:10.303+0000">>}, + {<<"MemorySize">>, 128}, + {<<"Role">>, + <<"arn:aws:iam::352283894008:role/lambda_basic_execution">>}, + {<<"Runtime">>, <<"python2.7">>}, + {<<"Timeout">>, 3}, + {<<"Version">>, <<"$LATEST">>}, + {<<"VpcConfig">>, null} + ], + [ + {<<"CodeSha256">>, <<"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=">>}, + {<<"CodeSize">>, 848}, + {<<"Description">>, <<>>}, + {<<"FunctionArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:name">>}, + {<<"FunctionName">>, <<"name">>}, + {<<"Handler">>, <<"index.process">>}, + {<<"LastModified">>, <<"2015-12-10T13:57:48.214+0000">>}, + {<<"MemorySize">>, 512}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, + {<<"Runtime">>, <<"nodejs">>}, + {<<"Timeout">>, 30}, + {<<"Version">>, <<"$LATEST">>}, + {<<"VpcConfig">>, null} + ], + [ + {<<"CodeSha256">>, <<"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=">>}, + {<<"CodeSize">>, 848}, + {<<"Description">>, <<>>}, + {<<"FunctionArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:name2">>}, + {<<"FunctionName">>, <<"name2">>}, + {<<"Handler">>, <<"index.process">>}, + {<<"LastModified">>, <<"2015-12-10T11:31:21.106+0000">>}, + {<<"MemorySize">>, 128}, + {<<"Role">>, + <<"arn:aws:iam::352283894008:role/lambda_basic_execution">>}, + {<<"Runtime">>, <<"nodejs">>}, + {<<"Timeout">>, 3}, + {<<"Version">>, <<"$LATEST">>}, + {<<"VpcConfig">>, null} + ] + ]}, + {<<"NextMarker">>, null} + ]}, + ?assertEqual(Expected, Result) + end, + fun() -> + Result = erlcloud_lambda:create_alias( + <<"name">>, + <<"$LATEST">>, + <<"aliasName1">>, + [] + ), + Expected = + {ok, [ + {<<"AliasArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:name:aliasName1">>}, + {<<"Description">>, <<>>}, + {<<"FunctionVersion">>, <<"$LATEST">>}, + {<<"Name">>, <<"aliasName1">>} + ]}, + ?assertEqual(Expected, Result) + end, + fun() -> + Result = erlcloud_lambda:create_event_source_mapping( + <<"arn:aws:kinesis:us-east-1:352283894008:stream/eholland-test">>, + <<"name">>, + <<"TRIM_HORIZON">>, + [] + ), + Expected = + {ok, [ + {<<"BatchSize">>, 100}, + {<<"EventSourceArn">>, + <<"arn:aws:kinesis:us-east-1:352283894008:stream/eholland-test">>}, + {<<"FunctionArn">>, <<"arn:aws:lambda:us-east-1:352283894008:function:name">>}, + {<<"LastModified">>, 1449845416.123}, + {<<"LastProcessingResult">>, <<"No records processed">>}, + {<<"State">>, <<"Creating">>}, + {<<"StateTransitionReason">>, <<"User action">>}, + {<<"UUID">>, <<"3f303f86-7395-43f3-9902-f5c80f0a5382">>} + ]}, + ?assertEqual(Expected, Result) + end, + fun() -> + Result = erlcloud_lambda:create_function( + #erlcloud_lambda_code{ + s3Bucket = <<"bi-lambda">>, + s3Key = << + "local_transform/bi-ass" + "ets-environment-create" + "-environment_1-0-0_lat" + "est.zip" + >> + }, + <<"name">>, + <<"index.process">>, + <<"arn:aws:iam::352283894008:role/lambda_basic_execution">>, + nodejs, + [] + ), + Expected = + {ok, [ + {<<"CodeSha256">>, <<"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=">>}, + {<<"CodeSize">>, 848}, + {<<"Description">>, <<>>}, + {<<"FunctionArn">>, <<"arn:aws:lambda:us-east-1:352283894008:function:name3">>}, + {<<"FunctionName">>, <<"name3">>}, + {<<"Handler">>, <<"index.process">>}, + {<<"LastModified">>, <<"2015-12-11T13:45:31.924+0000">>}, + {<<"MemorySize">>, 128}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_basic_execution">>}, + {<<"Runtime">>, <<"nodejs">>}, + {<<"Timeout">>, 3}, + {<<"Version">>, <<"$LATEST">>}, + {<<"VpcConfig">>, null} + ]}, + ?assertEqual(Expected, Result) + end, + fun() -> + Result = erlcloud_lambda:delete_event_source_mapping( + <<"6554f300-551b-46a6-829c-41b6af6022c6">> + ), + Expected = + {ok, [ + {<<"BatchSize">>, 100}, + {<<"EventSourceArn">>, + <<"arn:aws:kinesis:us-east-1:352283894008:stream/eholland-test">>}, + {<<"FunctionArn">>, <<"arn:aws:lambda:us-east-1:352283894008:function:name">>}, + {<<"LastModified">>, 1449843960.0}, + {<<"LastProcessingResult">>, <<"No records processed">>}, + {<<"State">>, <<"Deleting">>}, + {<<"StateTransitionReason">>, <<"User action">>}, + {<<"UUID">>, <<"a45b58ec-a539-4c47-929e-174b4dd2d963">>} + ]}, + ?assertEqual(Expected, Result) + end, + fun() -> + Result = erlcloud_lambda:delete_function( + <<"name">> + ), + Expected = {ok, []}, + ?assertEqual(Expected, Result) + end, + fun() -> + Result = erlcloud_lambda:delete_function( + <<"name_qualifier">>, <<"123">>, #aws_config{} + ), + Expected = {ok, []}, + ?assertEqual(Expected, Result) + end, + fun() -> + Result = erlcloud_lambda:get_alias(<<"name">>, <<"aliasName">>), + Expected = + {ok, [ + {<<"AliasArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:name:aliasName">>}, + {<<"Description">>, <<>>}, + {<<"FunctionVersion">>, <<"$LATEST">>}, + {<<"Name">>, <<"aliasName">>} + ]}, + ?assertEqual(Expected, Result) + end, + fun() -> + Result = erlcloud_lambda:get_event_source_mapping( + <<"a45b58ec-a539-4c47-929e-174b4dd2d963">> + ), + Expected = + {ok, [ + {<<"BatchSize">>, 100}, + {<<"EventSourceArn">>, + <<"arn:aws:kinesis:us-east-1:352283894008:stream/eholland-test">>}, + {<<"FunctionArn">>, <<"arn:aws:lambda:us-east-1:352283894008:function:name">>}, + {<<"LastModified">>, 1449841860.0}, + {<<"LastProcessingResult">>, <<"No records processed">>}, + {<<"State">>, <<"Enabled">>}, + {<<"StateTransitionReason">>, <<"User action">>}, + {<<"UUID">>, <<"a45b58ec-a539-4c47-929e-174b4dd2d963">>} + ]}, + ?assertEqual(Expected, Result) + end, + fun() -> + Result = erlcloud_lambda:get_function(<<"name">>), + Expected = + {ok, [ + {<<"Code">>, [ + {<<"Location">>, + <<"https://awslambda-us-east-1-tasks.s3-us-east-1.amazonaws.com/snapshots/352283894008/name-69237aec-bae9-4086-af73-a6610c2f8eb8?x-amz-security-token=AQoDYXdzENb%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEa4APJSJNHsYWnObKlElri5ytVzNg0t%2F0zwADHn40jy%2F1ZDW9%2FYWGi8dTp1l6LWBI9TwJi0LLcgp%2FlCxJIh7hsAPftYX62J9r9lRcmgd9RnYssg1%2Fkpfyjya90epxKg2zdHm%2BuZGukHYDcmAE1IQcHwsaQbvGAjXPCpFyxClbV6gMcFIsaBtfMxoMcbTCXG9m8l56nKgcX6Mi60vRNaB83AeNVrKMhB8EBUUbYbaB%2BG0iJg32i2HBF6VJMxamOLIEf1GJp1tWt%2FSAHfEkdTwcwtGINH3TNRv%2BY3ddsXs8pJ49eY49NCHANPC%2Bq0JzNQydbIK1shz8w1nozXYQo6%2BNh9tqOlaJNFgfFbtJkUDXv4rFqgVsfgJKJSQBeYUKmlNvIPQIoHWhRjjRzQUGmYDc3eEug7vELsNcHZixI4nNVycH%2BJZwaBvswy4eE7gBv3HwHi3SVlg9iXFTrfWTK%2FlCybC7mZIjAmPGiLCG5Pu8SoCgaGdHp8HmSeXXus4VUFcVTUw1qn7E%2BSaRFg3MTpCdu1f1Nqh4pNu7GpZacyLbH%2BSocuPyTjyYGL8sk0C3rjIWZipJcfUZsleS1cXLEGw%2FPAK5eg0RNWlVsaF1WgVimqQh3LtoRZ%2BwYCHRXsHjhCyQ8VhoguJCrswU%3D&AWSAccessKeyId=ASIAIAUY4IMA6JRJ3FSA&Expires=1449843004&Signature=5KUmber1cOBSChlKtnLaI%2FUemU4%3D">>}, + {<<"RepositoryType">>, <<"S3">>} + ]}, + {<<"Configuration">>, [ + {<<"CodeSha256">>, <<"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=">>}, + {<<"CodeSize">>, 848}, + {<<"Description">>, <<>>}, + {<<"FunctionArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:name">>}, + {<<"FunctionName">>, <<"name">>}, + {<<"Handler">>, <<"index.process">>}, + {<<"LastModified">>, <<"2015-12-10T13:57:48.214+0000">>}, + {<<"MemorySize">>, 512}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, + {<<"Runtime">>, <<"nodejs">>}, + {<<"Timeout">>, 30}, + {<<"Version">>, <<"$LATEST">>}, + {<<"VpcConfig">>, null} + ]} + ]}, + ?assertEqual(Expected, Result) + end, + fun() -> + Result = erlcloud_lambda:get_function_configuration(<<"name">>), + Expected = + {ok, [ + {<<"CodeSha256">>, <<"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=">>}, + {<<"CodeSize">>, 848}, + {<<"Description">>, <<>>}, + {<<"FunctionArn">>, <<"arn:aws:lambda:us-east-1:352283894008:function:name">>}, + {<<"FunctionName">>, <<"name">>}, + {<<"Handler">>, <<"index.process">>}, + {<<"LastModified">>, <<"2015-12-10T13:57:48.214+0000">>}, + {<<"MemorySize">>, 512}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, + {<<"Runtime">>, <<"nodejs">>}, + {<<"Timeout">>, 30}, + {<<"Version">>, <<"$LATEST">>}, + {<<"VpcConfig">>, null} + ]}, + ?assertEqual(Expected, Result) + end, + fun() -> + Result = erlcloud_lambda:invoke(<<"name">>, []), + Expected = {ok, [{<<"message">>, <<"Hello World!">>}]}, + ?assertEqual(Expected, Result) + end, + fun() -> + Result = erlcloud_lambda:invoke(<<"name">>, [], [{show_headers, true}], #aws_config{}), + Expected = {ok, [], [{<<"message">>, <<"Hello World!">>}]}, + ?assertEqual(Expected, Result) + end, + fun() -> + Result = erlcloud_lambda:invoke(<<"name">>, [], [raw_response_body], #aws_config{}), + Expected = {ok, <<"{\"message\":\"Hello World!\"}">>}, + ?assertEqual(Expected, Result) + end, + fun() -> + Result = erlcloud_lambda:invoke( + <<"name:alias">>, [], [raw_response_body], #aws_config{} + ), + Expected = {ok, <<"{\"message\":\"Hello World!\"}">>}, + ?assertEqual(Expected, Result) + end, + fun() -> + Result = erlcloud_lambda:invoke(<<"name_qualifier">>, [], [], <<"123">>), + Expected = {ok, [{<<"message">>, <<"Hello World!">>}]}, + ?assertEqual(Expected, Result) + end, + fun() -> + Result = erlcloud_lambda:list_aliases(<<"name">>), + Expected = + {ok, [ + {<<"Aliases">>, [ + [ + {<<"AliasArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:name:aliasName">>}, + {<<"Description">>, <<>>}, + {<<"FunctionVersion">>, <<"$LATEST">>}, + {<<"Name">>, <<"aliasName">>} + ], + [ + {<<"AliasArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:name:aliasName1">>}, + {<<"Description">>, <<>>}, + {<<"FunctionVersion">>, <<"$LATEST">>}, + {<<"Name">>, <<"aliasName1">>} + ] + ]}, + {<<"NextMarker">>, null} + ]}, + ?assertEqual(Expected, Result) + end, + fun() -> + Result = erlcloud_lambda:list_event_source_mappings( + <<"arn:aws:kinesis:us-east-1:352283894008:stream/eholland-test">>, + <<"name">> + ), + Expected = + {ok, [ + {<<"EventSourceMappings">>, [ + [ + {<<"BatchSize">>, 100}, + {<<"EventSourceArn">>, + <<"arn:aws:kinesis:us-east-1:352283894008:stream/eholland-test">>}, + {<<"FunctionArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:name">>}, + {<<"LastModified">>, 1449841860.0}, + {<<"LastProcessingResult">>, <<"No records processed">>}, + {<<"State">>, <<"Enabled">>}, + {<<"StateTransitionReason">>, <<"User action">>}, + {<<"UUID">>, <<"a45b58ec-a539-4c47-929e-174b4dd2d963">>} + ] + ]}, + {<<"NextMarker">>, null} + ]}, + ?assertEqual(Expected, Result) + end, + fun() -> + Result = erlcloud_lambda:list_versions_by_function(<<"name">>), + Expected = + {ok, [ + {<<"NextMarker">>, null}, + {<<"Versions">>, [ + [ + {<<"CodeSha256">>, <<"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=">>}, + {<<"CodeSize">>, 848}, + {<<"Description">>, <<>>}, + {<<"FunctionArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:name:$LATEST">>}, + {<<"FunctionName">>, <<"name">>}, + {<<"Handler">>, <<"index.process">>}, + {<<"LastModified">>, <<"2015-12-10T13:57:48.214+0000">>}, + {<<"MemorySize">>, 512}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, + {<<"Runtime">>, <<"nodejs">>}, + {<<"Timeout">>, 30}, + {<<"Version">>, <<"$LATEST">>}, + {<<"VpcConfig">>, null} + ], + [ + {<<"CodeSha256">>, <<"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=">>}, + {<<"CodeSize">>, 848}, + {<<"Description">>, <<>>}, + {<<"FunctionArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:name:1">>}, + {<<"FunctionName">>, <<"name">>}, + {<<"Handler">>, <<"index.process">>}, + {<<"LastModified">>, <<"2015-12-10T11:36:12.776+0000">>}, + {<<"MemorySize">>, 128}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, + {<<"Runtime">>, <<"nodejs">>}, + {<<"Timeout">>, 3}, + {<<"Version">>, <<"1">>}, + {<<"VpcConfig">>, null} + ], + [ + {<<"CodeSha256">>, <<"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=">>}, + {<<"CodeSize">>, 848}, + {<<"Description">>, <<>>}, + {<<"FunctionArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:name:2">>}, + {<<"FunctionName">>, <<"name">>}, + {<<"Handler">>, <<"index.process">>}, + {<<"LastModified">>, <<"2015-12-10T13:56:43.171+0000">>}, + {<<"MemorySize">>, 128}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, + {<<"Runtime">>, <<"nodejs">>}, + {<<"Timeout">>, 3}, + {<<"Version">>, <<"2">>}, + {<<"VpcConfig">>, null} + ] + ]} + ]}, + ?assertEqual(Expected, Result) + end, + fun() -> + Result = erlcloud_lambda:publish_version(<<"name">>), + Expected = + {ok, [ + {<<"CodeSha256">>, <<"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=">>}, + {<<"CodeSize">>, 848}, + {<<"Description">>, <<>>}, + {<<"FunctionArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:name:3">>}, + {<<"FunctionName">>, <<"name">>}, + {<<"Handler">>, <<"index.process">>}, + {<<"LastModified">>, <<"2015-12-10T13:57:48.214+0000">>}, + {<<"MemorySize">>, 512}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, + {<<"Runtime">>, <<"nodejs">>}, + {<<"Timeout">>, 30}, + {<<"Version">>, <<"3">>}, + {<<"VpcConfig">>, null} + ]}, + ?assertEqual(Expected, Result) + end, + fun() -> + Result = erlcloud_lambda:update_alias(<<"name">>, <<"aliasName">>), + Expected = + {ok, [ + {<<"AliasArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:name:aliasName">>}, + {<<"Description">>, <<>>}, + {<<"FunctionVersion">>, <<"$LATEST">>}, + {<<"Name">>, <<"aliasName">>} + ]}, + ?assertEqual(Expected, Result) + end, + fun() -> + Result = erlcloud_lambda:update_event_source_mapping( + <<"a45b58ec-a539-4c47-929e-174b4dd2d963">>, + 100, + undefined, + undefined + ), + Expected = + {ok, [ + {<<"BatchSize">>, 100}, + {<<"EventSourceArn">>, + <<"arn:aws:kinesis:us-east-1:352283894008:stream/eholland-test">>}, + {<<"FunctionArn">>, <<"arn:aws:lambda:us-east-1:352283894008:function:name">>}, + {<<"LastModified">>, 1449844011.991}, + {<<"LastProcessingResult">>, <<"No records processed">>}, + {<<"State">>, <<"Updating">>}, + {<<"StateTransitionReason">>, <<"User action">>}, + {<<"UUID">>, <<"a45b58ec-a539-4c47-929e-174b4dd2d963">>} + ]}, + ?assertEqual(Expected, Result) + end, + fun() -> + Result = erlcloud_lambda:update_function_code( + <<"name">>, true, #erlcloud_lambda_code{ + s3Bucket = <<"bi-lambda">>, + s3Key = + <<"local_transform/bi-assets-environment-create-environment_1-0-0_latest.zip">> + } + ), + Expected = + {ok, [ + {<<"CodeSha256">>, <<"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=">>}, + {<<"CodeSize">>, 848}, + {<<"Description">>, <<>>}, + {<<"FunctionArn">>, + <<"arn:aws:lambda:us-east-1:352283894008:function:name:4">>}, + {<<"FunctionName">>, <<"name">>}, + {<<"Handler">>, <<"index.process">>}, + {<<"LastModified">>, <<"2015-12-11T14:29:17.023+0000">>}, + {<<"MemorySize">>, 512}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, + {<<"Runtime">>, <<"nodejs">>}, + {<<"Timeout">>, 30}, + {<<"Version">>, <<"4">>}, + {<<"VpcConfig">>, null} + ]}, + ?assertEqual(Expected, Result) + end, + fun() -> + Result = erlcloud_lambda:update_function_configuration( + <<"name">>, undefined, undefined, 512, undefined, 30 + ), + Expected = + {ok, [ + {<<"CodeSha256">>, <<"zeoBX1hIWJBHk1muJe1iFyS1CcAmsT0Ct4IcpiPf8QM=">>}, + {<<"CodeSize">>, 848}, + {<<"Description">>, <<>>}, + {<<"FunctionArn">>, <<"arn:aws:lambda:us-east-1:352283894008:function:name">>}, + {<<"FunctionName">>, <<"name">>}, + {<<"Handler">>, <<"index.process">>}, + {<<"LastModified">>, <<"2015-12-11T14:31:52.034+0000">>}, + {<<"MemorySize">>, 512}, + {<<"Role">>, <<"arn:aws:iam::352283894008:role/lambda_kinesis_role">>}, + {<<"Runtime">>, <<"nodejs">>}, + {<<"Timeout">>, 30}, + {<<"Version">>, <<"$LATEST">>}, + {<<"VpcConfig">>, null} + ]}, + ?assertEqual(Expected, Result) + end ]. +make_response(Value) when is_binary(Value) -> + make_response({200, <<"OK">>}, Value); make_response(Value) -> - {ok, {{200, <<"OK">>}, [], Value}}. + make_response({200, <<"OK">>}, jsx:encode(Value)). + +make_response(Status, Value) -> + {ok, {Status, [], Value}}. diff --git a/test/erlcloud_mes_tests.erl b/test/erlcloud_mes_tests.erl index 73863ecaa..d8203cfd2 100644 --- a/test/erlcloud_mes_tests.erl +++ b/test/erlcloud_mes_tests.erl @@ -57,8 +57,8 @@ sort_json(V) -> %% verifies that the parameters in the body match the expected parameters -spec validate_body(binary(), expected_body()) -> ok. validate_body(Body, Expected) -> - Want = sort_json(jsx:decode(list_to_binary(Expected))), - Actual = sort_json(jsx:decode(Body)), + Want = sort_json(decode(list_to_binary(Expected))), + Actual = sort_json(decode(Body)), case Want =:= Actual of true -> ok; false -> @@ -250,7 +250,7 @@ get_entitlement_output_tests(_) -> } ] }", - {ok, jsx:decode(<<" + {ok, decode(<<" { \"Entitlements\": [ { @@ -283,7 +283,7 @@ get_entitlement_output_tests(_) -> } ] }", - {ok, jsx:decode(<<" + {ok, decode(<<" { \"Entitlements\": [ { @@ -308,3 +308,6 @@ get_entitlement_output_tests(_) -> <<"string">>, [{customer_identifier, [<<"string">>]}, {dimension, [<<"string">>]}], [{next_token, <<"token">>}])), Tests). + +decode(S) -> + jsx:decode(S, [{return_maps, false}]). diff --git a/test/erlcloud_mms_tests.erl b/test/erlcloud_mms_tests.erl index e1ead2941..e0eb8d1c1 100644 --- a/test/erlcloud_mms_tests.erl +++ b/test/erlcloud_mms_tests.erl @@ -61,8 +61,8 @@ sort_json(V) -> %% verifies that the parameters in the body match the expected parameters -spec validate_body(binary(), expected_body()) -> ok. validate_body(Body, Expected) -> - Want = sort_json(jsx:decode(list_to_binary(Expected))), - Actual = sort_json(jsx:decode(Body)), + Want = sort_json(decode(list_to_binary(Expected))), + Actual = sort_json(decode(Body)), case Want =:= Actual of true -> ok; false -> @@ -213,7 +213,7 @@ batch_meter_usage_output_tests(_) -> \"Timestamp\": 1471959107 } ] }", - {ok, jsx:decode(<<" + {ok, decode(<<" { \"Results\": [ { @@ -281,7 +281,7 @@ meter_usage_output_tests(_) -> Tests = [?_mms_test( {"MeterUsage example response", "{\"MeteringRecordId\": \"string\"}", - {ok, jsx:decode(<<"{\"MeteringRecordId\": \"string\"}">>)}}) + {ok, decode(<<"{\"MeteringRecordId\": \"string\"}">>)}}) ], output_tests(?_f(erlcloud_mms:meter_usage( @@ -319,7 +319,10 @@ resolve_customer_output_tests(_) -> \"CustomerIdentifier\": \"string\", \"ProductCode\": \"string\" }", - {ok,jsx:decode(<<"{\"CustomerIdentifier\": \"string\",\"ProductCode\": \"string\"}">>)}}) + {ok, decode(<<"{\"CustomerIdentifier\": \"string\",\"ProductCode\": \"string\"}">>)}}) ], output_tests(?_f(erlcloud_mms:resolve_customer(<<"string">>)), Tests). + +decode(S) -> + jsx:decode(S, [{return_maps, false}]). diff --git a/test/erlcloud_mon_tests.erl b/test/erlcloud_mon_tests.erl new file mode 100644 index 000000000..424791157 --- /dev/null +++ b/test/erlcloud_mon_tests.erl @@ -0,0 +1,176 @@ +%% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- +-module(erlcloud_mon_tests). + + +-include_lib("eunit/include/eunit.hrl"). +-include("erlcloud.hrl"). +-include("erlcloud_aws.hrl"). + +-define(_mon_test(T), {?LINE, T}). + +-define(_f(F), fun() -> F end). + +%%============================================================================== +%% Test entry points +%%============================================================================== + +describe_mon_test_() -> + {foreach, fun start/0, fun stop/1, [ + fun describe_alarms_for_metric_input_tests/1, + fun describe_alarms_for_metric_output_tests/1 + ]}. + +start() -> + meck:new(erlcloud_aws), + ok. + +stop(_) -> + meck:unload(erlcloud_aws). + +%%============================================================================== +%% Output Test helpers +%%============================================================================== + +output_test(Fun, {Line, {Description, Response, Result}}) -> + {Description, {Line, fun() -> + meck:expect(erlcloud_aws, aws_request_xml4, 8, + {ok, element(1, xmerl_scan:string( + binary_to_list(Response)))} + ), + Actual = Fun(), + ?assertEqual(Result, Actual) + end}}. + + +%%============================================================================== +%% Input Test helpers +%%============================================================================== + +validate_param(_Param = {Key, _Value}, Params) -> + ?assertEqual(true, proplists:is_defined(Key, Params)). + +validate_params(Params, Expected) -> + [validate_param(X, Params) + || X <- [{"Action", ""}, {"Version", ""} | Expected] + + ]. +input_expect(Response, Params) -> + + fun(get, undefined, "monitoring.amazonaws.com", undefined, "/", QParams, + "monitoring", _) -> + validate_params(QParams, Params), + Response + end. + +input_test(Response, {Line, {Description, Fun, Params}}) + when is_list(Description) -> + + InputFunction = input_expect(Response, Params), + + meck:expect(erlcloud_aws, aws_request_xml4, InputFunction), + + {Description, {Line, fun() -> + Fun() + end}}. + + +%%============================================================================== +%% Input Tests +%%============================================================================== + +describe_alarms_for_metric_input_tests(_) -> + Response = {ok, element(1, xmerl_scan:string( + binary_to_list(<<"" + "">>) + ))}, + + ExpectedParams = [ + {"Namespace", ""}, + {"MetricName", ""}, + {"Dimensions.member.1.Name", ""}, + {"Dimensions.member.1.Value", ""}, + {"ExtendedStatistic", ""}, + {"Period", ""}, + {"Statistic", ""}, + {"Unit", ""} + ], + + input_test(Response, ?_mon_test( + {"Test describe alarms for metric", + ?_f(erlcloud_mon:describe_alarms_for_metric( + "AWS/EC2", "NetworkIn", [{"InstanceType","m1.large"}], "p95", + 17, "Average", "Seconds", + #aws_config{})), ExpectedParams})). + + +%%============================================================================== +%% Output Tests +%%============================================================================== + +describe_alarms_for_metric_output_tests(_) -> + + Test = ?_mon_test({"Test describe alarms for metric", + <<" + + + + rgallego_unauthorized_metric + 2018-02-07T17:38:24.224Z + ALARM + 1.0 + Threshold Crossed: 1 datapoint [2.0 (07/02/18 17:33:00)] was greater than or equal to the threshold (1.0). + + + arn:aws:sns:us-east-1:123456794008:rgallego_cloudtrail_sns_topic + + 2018-02-07T17:38:24.952Z + 300 + Sum + GreaterThanOrEqualToThreshold + rgallego_unauthorized_alarm + 1 + {\"version\":\"1.0\",\"queryDate\":\"2018-02-07T17:38:24.953+0000\",\"startDate\":\"2018-02-07T17:33:00.000+0000\",\"statistic\":\"Sum\",\"period\":300,\"recentDatapoints\":[2.0],\"threshold\":1.0} + true + CISBenchmark + + arn:aws:cloudwatch:us-east-1:123456794008:alarm:rgallego_unauthorized_alarm + + + + + + 0e8470e6-1032-11e8-b27b-7db55194b86f + + ">>, + [[{metric_name,"rgallego_unauthorized_metric"}, + {namespace,"CISBenchmark"}, + {dimensions,[]}, + {actions_enabled,true}, + {alarm_actions,[{arn,"arn:aws:sns:us-east-1:123456794008:rgallego_cloudtrail_sns_topic"}]}, + {alarm_arn,"arn:aws:cloudwatch:us-east-1:123456794008:alarm:rgallego_unauthorized_alarm"}, + {alarm_configuration_updated_timestamp,{{2018,2,7}, + {17,38,24}}}, + {alarm_description,[]}, + {alarm_name,"rgallego_unauthorized_alarm"}, + {comparison_operator,"GreaterThanOrEqualToThreshold"}, + {evaluate_low_sample_count_percentile,[]}, + {evaluation_periods,1}, + {extended_statistic,[]}, + {insufficient_data_actions,[]}, + {ok_actions,[]}, + {period,300}, + {state_reason,"Threshold Crossed: 1 datapoint [2.0 (07/02/18 17:33:00)] was greater than or equal to the threshold (1.0)."}, + {state_reason_data,"{\"version\":\"1.0\",\"queryDate\":\"2018-02-07T17:38:24.953+0000\",\"startDate\":\"2018-02-07T17:33:00.000+0000\",\"statistic\":\"Sum\",\"period\":300,\"recentDatapoints\":[2.0],\"threshold\":1.0}"}, + {state_updated_timestamp,{{2018,2,7},{17,38,24}}}, + {state_value,"ALARM"}, + {statistic,"Sum"}, + {threshold,1.0}, + {treat_missing_data,[]}, + {unit,[]}]] + }), + + output_test(?_f(erlcloud_mon:describe_alarms_for_metric( + "AWS/EC2", "NetworkIn", [{"InstanceType","m1.large"}], "p95", + 17, "Average", "Seconds", #aws_config{} + )), Test). diff --git a/test/erlcloud_s3_tests.erl b/test/erlcloud_s3_tests.erl index df865666e..c2380ca69 100755 --- a/test/erlcloud_s3_tests.erl +++ b/test/erlcloud_s3_tests.erl @@ -40,7 +40,14 @@ operation_test_() -> fun put_bucket_encryption_test/1, fun get_bucket_encryption_test/1, fun get_bucket_encryption_not_found_test/1, - fun delete_bucket_encryption_test/1 + fun delete_bucket_encryption_test/1, + fun hackney_proxy_put_validation_test/1, + fun get_bucket_and_key/1, + fun head_bucket_ok/1, + fun head_bucket_redirect/1, + fun head_bucket_bad_request/1, + fun head_bucket_forbidden/1, + fun head_bucket_not_found/1 ]}. start() -> @@ -62,9 +69,26 @@ httpc_expect(Response) -> httpc_expect(get, Response). httpc_expect(Method, Response) -> - fun(_Url, Method2, _Headers, _Body, _Timeout, _Config) -> - Method = Method2, - Response + fun(_Url, Method2, _Headers, _Body, _Timeout, _Config = #aws_config{hackney_client_options = #hackney_client_options{insecure = Insecure, + proxy = Proxy, + proxy_auth = Proxy_auth}, + http_client = Http_client}) -> + + case Http_client of + hackney -> + Method = Method2, + Insecure = false, + Proxy = <<"10.10.10.10">>, + Proxy_auth = {<<"AAAA">>, <<"BBBB">>}; + + _else -> + Method = Method2, + Insecure = true, + Proxy = undefined, + Proxy_auth = undefined + end, + + Response end. get_bucket_lifecycle_tests(_) -> @@ -792,3 +816,146 @@ delete_bucket_encryption_test(_) -> meck:expect(erlcloud_httpc, request, httpc_expect(delete, Response)), Result = erlcloud_s3:delete_bucket_encryption("bucket", config()), ?_assertEqual(ok, Result). + +hackney_proxy_put_validation_test(_) -> + Response = {ok, {{200, "OK"}, [{"x-amz-version-id", "version_id"}], <<>>}}, + Config2 = #aws_config{hackney_client_options = #hackney_client_options{insecure = false, + proxy = <<"10.10.10.10">>, + proxy_auth = {<<"AAAA">>, <<"BBBB">>}}, + http_client = hackney}, + meck:expect(erlcloud_httpc, request, httpc_expect(put, Response)), + Result = erlcloud_s3:put_object("BucketName", "Key", "Data", config(Config2)), + ?_assertEqual([{version_id, "version_id"} + ,{"x-amz-version-id", "version_id"} + ], Result). + +get_bucket_and_key(_) -> + ErlcloudS3ExportExample = "https://s3.amazonaws.com/some_bucket/path_to_file", + Result = erlcloud_s3:get_bucket_and_key(ErlcloudS3ExportExample), + ?_assertEqual({"some_bucket","path_to_file"}, Result). + +head_bucket_ok(_) -> + Response = {ok, {{200, "OK"}, + [ + {"server","AmazonS3"}, + {"transfer-encoding","chunked"}, + {"content-type","application/xml"}, + {"x-amz-access-point-alias","false"}, + {"x-amz-bucket-region","us-west-2"}, + {"date","Mon, 21 Apr 2025 19:45:15 GMT"}, + {"x-amz-id-2", + "YIgyI9Lb9I/dMpDrRASSD8w5YsNAyhRlF+PDF0jlf9Hq6eVLvSkuj+ftZI2RmU5eXnOKW1Wqh20="}, + {"x-amz-request-id","FAECC30C2CD53BCA"} + ], + <<>>} + }, + meck:expect(erlcloud_httpc, request, httpc_expect(head, Response)), + Result = erlcloud_s3:head_bucket( + "bucket.name", + config()), + ?_assertEqual( + [ + {content_length,undefined}, + {content_type,"application/xml"}, + {access_point_alias,"false"}, + {bucket_region,"us-west-2"} + ], Result + ). + +head_bucket_redirect(_) -> + Response1 = {ok, {{307, "Temporary Redirect"}, + [ + {"server","AmazonS3"}, + {"transfer-encoding","chunked"}, + {"content-type","application/xml"}, + {"location", "https://bucket.name.s3-us-west-2.amazonaws.com/"}, + {"x-amz-bucket-region","us-west-2"}, + {"date","Mon, 21 Apr 2025 19:45:15 GMT"}, + {"x-amz-id-2", + "YIgyI9Lb9I/dMpDrRASSD8w5YsNAyhRlF+PDF0jlf9Hq6eVLvSkuj+ftZI2RmU5eXnOKW1Wqh20="}, + {"x-amz-request-id","FAECC30C2CD53BCA"} + ], + <<>>} + }, + Response2 = {ok, {{200, "OK"}, + [ + {"server","AmazonS3"}, + {"transfer-encoding","chunked"}, + {"content-type","application/xml"}, + {"x-amz-access-point-alias","false"}, + {"x-amz-bucket-region","us-west-2"}, + {"date","Mon, 21 Apr 2025 19:45:15 GMT"}, + {"x-amz-id-2", + "YIgyI9Lb9I/dMpDrRASSD8w5YsNAyhRlF+PDF0jlf9Hq6eVLvSkuj+ftZI2RmU5eXnOKW1Wqh20="}, + {"x-amz-request-id","FAECC30C2CD53BCA"} + ], + <<>>} + }, + meck:sequence(erlcloud_httpc, request, 6, [Response1, Response2]), + Result = erlcloud_s3:head_bucket( + "bucket.name", + config()), + ?_assertEqual( + [ + {content_length,undefined}, + {content_type,"application/xml"}, + {access_point_alias,"false"}, + {bucket_region,"us-west-2"} + ], Result + ). + +head_bucket_bad_request(_) -> + Response = {ok, {{400, "Bad Request"}, + [ + {"server","AmazonS3"}, + {"transfer-encoding","chunked"}, + {"content-type","application/xml"}, + {"date","Mon, 21 Apr 2025 19:45:15 GMT"}, + {"connection", "close"}, + {"x-amz-id-2", + "YIgyI9Lb9I/dMpDrRASSD8w5YsNAyhRlF+PDF0jlf9Hq6eVLvSkuj+ftZI2RmU5eXnOKW1Wqh20="}, + {"x-amz-request-id","FAECC30C2CD53BCA"} + ], <<>>}}, + meck:expect(erlcloud_httpc, request, httpc_expect(head, Response)), + ?_assertException( + error, + {aws_error, {http_error, 400, "Bad Request", <<>>}}, + erlcloud_s3:head_bucket("bucket.name", config()) + ). + +head_bucket_forbidden(_) -> + Response = {ok, {{403, "Forbidden"}, + [ + {"server","AmazonS3"}, + {"transfer-encoding","chunked"}, + {"content-type","application/xml"}, + {"date","Mon, 21 Apr 2025 19:45:15 GMT"}, + {"x-amz-bucket-region","us-west-2"}, + {"x-amz-id-2", + "YIgyI9Lb9I/dMpDrRASSD8w5YsNAyhRlF+PDF0jlf9Hq6eVLvSkuj+ftZI2RmU5eXnOKW1Wqh20="}, + {"x-amz-request-id","FAECC30C2CD54BCA"} + ], <<>>}}, + meck:expect(erlcloud_httpc, request, httpc_expect(head, Response)), + ?_assertException( + error, + {aws_error, {http_error, 403, "Forbidden", <<>>}}, + erlcloud_s3:head_bucket("bucket.name", config()) + ). + +head_bucket_not_found(_) -> + Response = {ok, {{404, "Not Found"}, + [ + {"server","AmazonS3"}, + {"transfer-encoding","chunked"}, + {"content-type","application/xml"}, + {"date","Mon, 21 Apr 2025 19:45:15 GMT"}, + {"x-amz-id-2", + "YIgyI9Lb9I/dMpDrRASSD8w5YsNAyhRlF+PDF0jlf9Hq6eVLvSkuj+ftZI2RmU5eXnOKW1Wqh20="}, + {"x-amz-request-id","FAECC30C2CD54CCA"} + ], <<>>}}, + meck:expect(erlcloud_httpc, request, httpc_expect(head, Response)), + ?_assertException( + error, + {aws_error, {http_error, 404, "Not Found", <<>>}}, + erlcloud_s3:head_bucket("bucket.name", config()) + ). diff --git a/test/erlcloud_sdb_tests.erl b/test/erlcloud_sdb_tests.erl index feb24d162..c8382a67c 100644 --- a/test/erlcloud_sdb_tests.erl +++ b/test/erlcloud_sdb_tests.erl @@ -1,10 +1,18 @@ -module(erlcloud_sdb_tests). --ifdef(TEST). --compile(export_all). - -include_lib("eunit/include/eunit.hrl"). +-export([setup/0]). +-export([cleanup/1]). +-export([select_single_response/0]). +-export([select_next_token/0]). +-export([select_all_single_response/0]). +-export([select_all_failure/0]). +-export([select_all_503/0]). +-export([select_all_next_token/0]). +-export([select_all_next_and_failure/0]). +-export([select_all_two_results/0]). + setup() -> erlcloud_sdb:configure("fake", "fake-secret"), meck:new(erlcloud_httpc). @@ -153,5 +161,3 @@ extract_token_test() -> ?assertEqual(next_token(), erlcloud_sdb:extract_token(parse_document(only_token_response_body()))), ?assertEqual(next_token(), erlcloud_sdb:extract_token(parse_document(single_result_and_token_response_body()))), ?assertEqual(done, erlcloud_sdb:extract_token(parse_document(single_result_response_body("item0")))). - --endif. diff --git a/test/erlcloud_securityhub_test.erl b/test/erlcloud_securityhub_test.erl new file mode 100644 index 000000000..356522496 --- /dev/null +++ b/test/erlcloud_securityhub_test.erl @@ -0,0 +1,82 @@ +-module(erlcloud_securityhub_test). + +-include_lib("eunit/include/eunit.hrl"). +-include("erlcloud_aws.hrl"). + +-define(TEST_AWS_CONFIG, + #aws_config{ + access_key_id = "TEST_ACCESS_KEY_ID", + secret_access_key = "TEST_ACCESS_KEY", + security_token = "TEST_SECURITY_TOKEN" + } +). + +api_test_() -> + { + foreach, + fun() -> meck:new(erlcloud_httpc) end, + fun(_) -> meck:unload() end, + [fun describe_hub_tests/1] + }. + +describe_hub_tests(_) -> + [ + { + "SecurityHub", + fun() -> + AwsConfig = ?TEST_AWS_CONFIG, + meck:expect( + erlcloud_httpc, request, + fun(A1, A2, A3, A4, A5, A6) -> + Url = "https://securityhub.us-east-1.amazonaws.com/accounts", + Method = get, + RequestContent = <<>>, + case [A1, A2, A3, A4, A5, A6] of + [Url, Method, _Headers, RequestContent, _Timeout, AwsConfig] -> + ResponseContent = << + "{" + "\"HubArn\":\"arn:aws:securityhub:us-east-1:123456789012:hub/default\"," + "\"AutoEnableControls\":\"true\"," + "\"SubscribedAt\":\"2023-02-15T15:46:42.158Z\"" + "}" + >>, + {ok, {{200, "OK"}, [], ResponseContent}} + end + end + ), + + Result = erlcloud_securityhub:describe_hub(AwsConfig, []), + DescribeHub = [ + {<<"HubArn">>, <<"arn:aws:securityhub:us-east-1:123456789012:hub/default">>}, + {<<"AutoEnableControls">>, <<"true">>}, + {<<"SubscribedAt">>, <<"2023-02-15T15:46:42.158Z">>} + ], + + ?assertEqual({ok, DescribeHub}, Result) + end + }, + { + "SecurityHub -> not_found", + fun() -> + AwsConfig = ?TEST_AWS_CONFIG, + meck:expect( + erlcloud_httpc, request, + fun(A1, A2, A3, A4, A5, A6) -> + Url = "https://securityhub.us-east-1.amazonaws.com/accounts?HubArn=test", + Method = get, + RequestContent = <<>>, + case [A1, A2, A3, A4, A5, A6] of + [Url, Method, _Headers, RequestContent, _Timeout, AwsConfig] -> + ResponseHeaders = [{"x-amzn-errortype", "ResourceNotFoundException"}], + ResponseContent = <<"{\"message\": \"not found\"}">>, + {ok, {{404, "NotFound"}, ResponseHeaders, ResponseContent}} + end + end + ), + Params = [{<<"HubArn">>, <<"test">>}], + Result = erlcloud_securityhub:describe_hub(AwsConfig, Params), + ?assertEqual({error, not_found}, Result) + end + } + ]. + diff --git a/test/erlcloud_ses_tests.erl b/test/erlcloud_ses_tests.erl index b46516a7d..d483eafed 100644 --- a/test/erlcloud_ses_tests.erl +++ b/test/erlcloud_ses_tests.erl @@ -22,8 +22,10 @@ operation_test_() -> fun get_send_statistics_tests/1, fun list_identities_tests/1, fun send_email_tests/1, + fun send_raw_email_tests/1, fun set_identity_dkim_enabled_tests/1, fun set_identity_feedback_forwarding_enabled_tests/1, + fun set_identity_headers_in_notifications_enabled_tests/1, fun set_identity_notification_topic_tests/1, fun verify_domain_dkim_tests/1, fun verify_domain_identity_tests/1, @@ -134,6 +136,9 @@ get_identity_notification_attributes_tests(_) -> user@example.com true + true + true + true arn:aws:sns:us-east-1:123456789012:example arn:aws:sns:us-east-1:123456789012:example arn:aws:sns:us-east-1:123456789012:example @@ -148,6 +153,9 @@ get_identity_notification_attributes_tests(_) -> meck:expect(erlcloud_httpc, request, input_expect(Response, Expected)), ?assertEqual({ok, [{notification_attributes, [{"user@example.com", [{forwarding_enabled, true}, + {headers_in_bounce_notifications_enabled, true}, + {headers_in_complaint_notifications_enabled, true}, + {headers_in_delivery_notifications_enabled, true}, {bounce_topic, "arn:aws:sns:us-east-1:123456789012:example"}, {complaint_topic, "arn:aws:sns:us-east-1:123456789012:example"}, {delivery_topic, "arn:aws:sns:us-east-1:123456789012:example"}]}]}]}, @@ -322,7 +330,7 @@ send_email_tests(_) -> end, fun() -> configure(), - Expected = "Action=SendEmail&Version=2010-12-01&Destination.BccAddresses.member.1=a%40bcc.com&Destination.BccAddresses.member.2=b%40bcc.com&Destination.CcAddresses.member.1=c%40cc.com&Destination.ToAddresses.member.1=d%40to.com&Message.Body.Html.Charset=html%20charset&Message.Body.Html.Data=html%20data&Message.Body.Text.Charset=text%20charset&Message.Body.Text.Data=text%20data&Message.Subject.Charset=subject%20charset&Message.Subject.Data=subject%20data&Source=e%40from.com&ReplyToAddresses.member.1=f%40reply.com&ReplyToAddresses.member.2=g%40reply.com&ReturnPath=return%20path", + Expected = "Action=SendEmail&Version=2010-12-01&Destination.BccAddresses.member.1=a%40bcc.com&Destination.BccAddresses.member.2=b%40bcc.com&Destination.CcAddresses.member.1=c%40cc.com&Destination.ToAddresses.member.1=d%40to.com&Message.Body.Html.Charset=html%20charset&Message.Body.Html.Data=html%20data&Message.Body.Text.Charset=text%20charset&Message.Body.Text.Data=text%20data&Message.Subject.Charset=subject%20charset&Message.Subject.Data=subject%20data&Source=e%40from.com&ConfigurationSetName=configuration%20set&ReplyToAddresses.member.1=f%40reply.com&ReplyToAddresses.member.2=g%40reply.com&ReturnPath=return%20path&Tags.member.1.Value=value-1&Tags.member.1.Name=tag-1&Tags.member.2.Value=value-2&Tags.member.2.Name=tag-2", Response = " @@ -344,8 +352,48 @@ send_email_tests(_) -> [{charset, "subject charset"}, {data, "subject data"}], "e@from.com", - [{reply_to_addresses, [<<"f@reply.com">>, "g@reply.com"]}, - {return_path, "return path"}])) + [{configuration_set_name, "configuration set"}, + {reply_to_addresses, [<<"f@reply.com">>, "g@reply.com"]}, + {return_path, "return path"}, + {tags, [{"tag-1", "value-1"}, {"tag-2", "value-2"}]}])) + end + ]. + +send_raw_email_tests(_) -> + [ + fun() -> + configure(), + Expected = "Action=SendRawEmail&Version=2010-12-01&RawMessage.Data=RnJvbTogYkBmcm9tLmNvbQpUbzogYUB0by5jb20KU3ViamVjdDogU3ViamVjdApNSU1FLVZlcnNpb246IDEuMApDb250ZW50LXR5cGU6IE11bHRpcGFydC9NaXhlZDsgYm91bmRhcnk9Ik5leHRQYXJ0IgoKLS1OZXh0UGFydApDb250ZW50LVR5cGU6IHRleHQvcGxhaW4KCkVtYWlsIEJvZHkKCi0tTmV4dFBhcnQtLQ%3D%3D", + Response = +" + + 00000131d51d2292-159ad6eb-077c-46e6-ad09-ae7c05925ed4-000000 + + + d5964849-c866-11e0-9beb-01a62d68c57f + +", + meck:expect(erlcloud_httpc, request, input_expect(Response, Expected)), + ?assertEqual({ok, [{message_id, "00000131d51d2292-159ad6eb-077c-46e6-ad09-ae7c05925ed4-000000"}]}, + erlcloud_ses:send_raw_email("From: b@from.com\nTo: a@to.com\nSubject: Subject\nMIME-Version: 1.0\nContent-type: Multipart/Mixed; boundary=\"NextPart\"\n\n--NextPart\nContent-Type: text/plain\n\nEmail Body\n\n--NextPart--", [])) + end, + fun() -> + configure(), + Expected = "Action=SendRawEmail&Version=2010-12-01&RawMessage.Data=VG86IGRAdG8uY29tCkNDOiBjQGNjLmNvbQpCQ0M6IGFAYmNjLmNvbSwgYkBiY2MuY29tClN1YmplY3Q6IFN1YmplY3QKTUlNRS1WZXJzaW9uOiAxLjAKQ29udGVudC10eXBlOiBNdWx0aXBhcnQvTWl4ZWQ7IGJvdW5kYXJ5PSJOZXh0UGFydCIKCi0tTmV4dFBhcnQKQ29udGVudC1UeXBlOiB0ZXh0L3BsYWluCgpFbWFpbCBCb2R5CgotLU5leHRQYXJ0LS0%3D&Source=e%40from.com&Destinations.member.1=d%40to.com&Destinations.member.2=c%40cc.com&Destinations.member.3=a%40bcc.com&Destinations.member.4=b%40bcc.com", + Response = +" + + 00000131d51d2292-159ad6eb-077c-46e6-ad09-ae7c05925ed4-000000 + + + d5964849-c866-11e0-9beb-01a62d68c57f + +", + meck:expect(erlcloud_httpc, request, input_expect(Response, Expected)), + ?assertEqual({ok, [{message_id, "00000131d51d2292-159ad6eb-077c-46e6-ad09-ae7c05925ed4-000000"}]}, + erlcloud_ses:send_raw_email(<<"To: d@to.com\nCC: c@cc.com\nBCC: a@bcc.com, b@bcc.com\nSubject: Subject\nMIME-Version: 1.0\nContent-type: Multipart/Mixed; boundary=\"NextPart\"\n\n--NextPart\nContent-Type: text/plain\n\nEmail Body\n\n--NextPart--">>, + [{source, "e@from.com"}, + {destinations, ["d@to.com", <<"c@cc.com">>, <<"a@bcc.com">>, "b@bcc.com"]}])) end ]. @@ -381,6 +429,22 @@ set_identity_feedback_forwarding_enabled_tests(_) -> end ]. +set_identity_headers_in_notifications_enabled_tests(_) -> + [fun() -> + configure(), + Expected = "Action=SetIdentityHeadersInNotificationsEnabled&Version=2010-12-01&Identity=user%40example.com&NotificationType=Bounce&Enabled=true", + Response = + " + + + 299f4af4-b72a-11e1-901f-1fbd90e8104f + + ", + meck:expect(erlcloud_httpc, request, input_expect(Response, Expected)), + ?assertEqual(ok, erlcloud_ses:set_identity_headers_in_notifications_enabled("user@example.com", bounce, true)) + end + ]. + set_identity_notification_topic_tests(_) -> [fun() -> configure(), diff --git a/test/erlcloud_sm_tests.erl b/test/erlcloud_sm_tests.erl new file mode 100644 index 000000000..83c1b3664 --- /dev/null +++ b/test/erlcloud_sm_tests.erl @@ -0,0 +1,1208 @@ +-module(erlcloud_sm_tests). +-include_lib("eunit/include/eunit.hrl"). +-include("erlcloud.hrl"). + +%% Unit tests for sm. +%% These tests work by using meck to mock erlcloud_httpc. There are two classes of test: input and output. +%% +%% Input tests verify that different function args produce the desired JSON request. +%% An input test list provides a list of funs and the JSON that is expected to result. +%% +%% Output tests verify that the http response produces the correct return from the fun. +%% An output test lists provides a list of response bodies and the expected return. + +%% The _sm_test macro provides line number annotation to a test, similar to _test, but doesn't wrap in a fun +-define(_sm_test(T), {?LINE, T}). +%% The _f macro is a terse way to wrap code in a fun. Similar to _test but doesn't annotate with a line number +-define(_f(F), fun() -> F end). + +-export([validate_body/2]). + +%%%=================================================================== +%%% Common Test Values +%%%=================================================================== + +-define(SECRET_ID, <<"MyTestDatabaseSecret">>). +-define(SECRET_ID2, <<"MyTestDatabaseSecret2">>). +-define(SECRET_STRING, <<"{\"username\":\"david\",\"password\":\"SECRET-PASSWORD\"}">>). +-define(SECRET_BINARY, base64:encode(?SECRET_STRING)). +-define(CLIENT_REQUEST_TOKEN, <<"EXAMPLE2-90ab-cdef-fedc-ba987EXAMPLE">>). +-define(MAX_RESULTS, 10). +-define(NEXT_TOKEN, <<"TOKEN">>). +-define(VERSION_ID, <<"EXAMPLE1-90ab-cdef-fedc-ba987SECRET1">>). +-define(VERSION_ID2, <<"EXAMPLE1-90ab-cdef-fedc-ba987SECRET2">>). +-define(VERSION_STAGE, <<"AWSPREVIOUS">>). + +%%%=================================================================== +%%% Test entry points +%%%=================================================================== + +operation_test_() -> + {foreach, + fun start/0, + fun stop/1, + [ + fun batch_get_secret_value_input_tests/1, + fun batch_get_secret_value_output_tests/1, + fun cancel_rotate_secret_value_input_tests/1, + fun cancel_rotate_secret_value_output_tests/1, + fun create_secret_value_input_tests/1, + fun create_secret_value_output_tests/1, + fun get_random_password_value_input_tests/1, + fun get_random_password_value_output_tests/1, + fun get_secret_value_input_tests/1, + fun get_secret_value_output_tests/1, + fun list_secrets_value_input_tests/1, + fun list_secrets_value_output_tests/1, + fun list_secret_version_ids_value_input_tests/1, + fun list_secret_version_ids_value_output_tests/1, + fun put_secret_value_input_tests/1, + fun put_secret_value_output_tests/1, + fun remove_regions_from_replication_value_input_tests/1, + fun remove_regions_from_replication_value_output_tests/1, + fun replicate_secret_to_regions_value_input_tests/1, + fun replicate_secret_to_regions_value_output_tests/1, + fun restore_secret_value_input_tests/1, + fun restore_secret_value_output_tests/1, + fun rotate_secret_value_input_tests/1, + fun rotate_secret_value_output_tests/1, + fun stop_replication_to_replica_value_input_tests/1, + fun stop_replication_to_replica_value_output_tests/1, + fun tag_resource_value_input_tests/1, + fun tag_resource_value_output_tests/1, + fun untag_resource_value_input_tests/1, + fun untag_resource_value_output_tests/1, + fun update_secret_value_input_tests/1, + fun update_secret_value_output_tests/1, + fun update_secret_version_stage_value_input_tests/1, + fun update_secret_version_stage_value_output_tests/1, + fun validate_resource_policy_value_input_tests/1, + fun validate_resource_policy_value_output_tests/1 + ]}. + +start() -> + meck:new(erlcloud_httpc), + ok. + +stop(_) -> + meck:unload(erlcloud_httpc). + +%%%=================================================================== +%%% Input test helpers +%%%=================================================================== + +sort_json([{_, _} | _] = Json) -> + %% Value is an object + SortedChildren = [{K, sort_json(V)} || {K, V} <- Json], + lists:keysort(1, SortedChildren); +sort_json([_ | _] = Json) -> + %% Value is an array + [sort_json(I) || I <- Json]; +sort_json(V) -> + V. + +%% verifies that the parameters in the body match the expected parameters +-spec validate_body(binary(), binary()) -> ok. +validate_body(Body, Expected) -> + Want = sort_json(jsx:decode(Expected)), + Actual = sort_json(jsx:decode(Body)), + case Want =:= Actual of + true -> + ok; + false -> + ?debugFmt("~nEXPECTED~n~p~nACTUAL~n~p~n", [Want, Actual]) + end, + ?assertEqual(Want, Actual). + +%% returns the mock of the erlcloud_httpc function input tests expect to be called. +%% Validates the request body and responds with the provided response. +-spec input_expect(binary(), binary()) -> fun(). +input_expect(Response, Expected) -> + fun(_Url, post, _Headers, Body, _Timeout, _Config) -> + validate_body(Body, Expected), + {ok, {{200, "OK"}, [], Response}} + end. + +%% input_test converts an input_test specifier into an eunit test generator +-type input_test_spec() :: {pos_integer(), {fun(), binary()} | {string(), fun(), binary()}}. +-spec input_test(binary(), input_test_spec()) -> tuple(). +input_test(Response, {Line, {Description, Fun, Expected}}) when + is_list(Description) -> + {Description, + {Line, + fun() -> + meck:expect(erlcloud_httpc, request, input_expect(Response, Expected)), + erlcloud_config:configure(string:copies("A", 20), + string:copies("a", 40), fun erlcloud_sm:new/2), + Fun() + end}}. + +%% input_tests converts a list of input_test specifiers into an eunit test generator +-spec input_tests(binary(), [input_test_spec()]) -> [tuple()]. +input_tests(Response, Tests) -> + [input_test(Response, Test) || Test <- Tests]. + +%%%=================================================================== +%%% Output test helpers +%%%=================================================================== + +%% returns the mock of the erlcloud_httpc function output tests expect to be called. +-spec output_expect(string()) -> fun(). +output_expect(Response) -> + fun(_Url, post, _Headers, _Body, _Timeout, _Config) -> + {ok, {{200, "OK"}, [], Response}} + end. + +%% output_test converts an output_test specifier into an eunit test generator +-type output_test_spec() :: {pos_integer(), {binary(), term()} | {string(), binary(), term()}}. +-spec output_test(fun(), output_test_spec()) -> tuple(). +output_test(Fun, {Line, {Description, Response, Result}}) -> + {Description, + {Line, + fun() -> + meck:expect(erlcloud_httpc, request, output_expect(Response)), + erlcloud_config:configure(string:copies("A", 20), + string:copies("a", 40), fun erlcloud_sm:new/2), + Actual = Fun(), + case Result =:= Actual of + true -> ok; + false -> + ?debugFmt("~nEXPECTED~n~p~nACTUAL~n~p~n", [Result, Actual]) + end, + ?assertEqual(Result, Actual) + end}}. + +%% output_tests converts a list of output_test specifiers into an eunit test generator +-spec output_tests(fun(), [output_test_spec()]) -> [term()]. +output_tests(Fun, Tests) -> + [output_test(Fun, Test) || Test <- Tests]. + +%%%=================================================================== +%%% Tests +%%%=================================================================== +batch_get_secret_value_input_tests(_) -> + Tests = [ + ?_sm_test( + {"batch_get_secret_value2id input test", + ?_f( + erlcloud_sm:batch_get_secret_value( + {secret_id_list, [?SECRET_ID, ?SECRET_ID2]}, + [{max_results, ?MAX_RESULTS}, {next_token, ?NEXT_TOKEN}] + ) + ), + jsx:encode([ + {<<"SecretIdList">>, [?SECRET_ID, ?SECRET_ID2]}, + {<<"MaxResults">>, ?MAX_RESULTS}, + {<<"NextToken">>, ?NEXT_TOKEN} + ]) + }), + ?_sm_test( + {"batch_get_secret_value2Filter input test", + ?_f( + erlcloud_sm:batch_get_secret_value( + {filters, [[{<<"Key">>,<<"name">>}, {<<"Values">>, [<<"value1">>, <<"value2">>]}]]}, + [{max_results, ?MAX_RESULTS}, {next_token, ?NEXT_TOKEN}] + ) + ), + jsx:encode([ + {<<"Filters">>, [[{<<"Key">>,<<"name">>}, {<<"Values">>, [<<"value1">>, <<"value2">>]}]]}, + {<<"MaxResults">>, ?MAX_RESULTS}, + {<<"NextToken">>, ?NEXT_TOKEN} + ]) + }) + ], + Response = <<>>, + input_tests(Response, Tests). + +-define(BATCH_GET_SECRET_VALUE_RESP,[ + {<<"Errors">>, [ + {<<"ErrorCode">>, <<"ResourceNotFoundException">>}, + {<<"ErrorMessage">>, <<"Secret with id 'NonExistentSecret' not found">>}, + {<<"SecretId">>, <<"NonExistentSecret">>}]}, + {<<"NextToken">>, ?NEXT_TOKEN}, + {<<"SecretValues">>, [ + [ + {<<"ARN">>, + <<"arn:aws:secretsmanager:us-west-2:123456789012:secret:MyTestDatabaseSecret-a1b2c3">>}, + {<<"CreatedDate">>, 1.523477145713E9}, + {<<"Name">>, ?SECRET_ID}, + {<<"SecretString">>, + <<"{\n \"username\":\"david\",\n \"password\":\"BnQw&XDWgaEeT9XGTT29\"\n}\n">>}, + {<<"VersionId">>, ?VERSION_ID}, + {<<"VersionStages">>, [?VERSION_STAGE]} + ], + [ + {<<"ARN">>, + <<"arn:aws:secretsmanager:us-west-2:123456789012:secret:MyTestDatabaseSecret2-b2c3d4">>}, + {<<"CreatedDate">>, 1.523477145713E9}, + {<<"Name">>, ?SECRET_ID2}, + {<<"SecretString">>, + <<"{\n \"username\":\"alice\",\n \"password\":\"XyZ1234567890\"\n}\n">>}, + {<<"VersionId">>, ?VERSION_ID2}, + {<<"VersionStages">>, [?VERSION_STAGE]} + ] + ]} +]). + + +batch_get_secret_value_output_tests(_) -> + Tests = [?_sm_test( + {"batch_get_secret_value output test", + jsx:encode(?BATCH_GET_SECRET_VALUE_RESP), + {ok, ?BATCH_GET_SECRET_VALUE_RESP}} + )], + + output_tests(?_f( + erlcloud_sm:batch_get_secret_value( + {secret_id_list, [?SECRET_ID, ?SECRET_ID2]}, + [{max_results, ?MAX_RESULTS}, {next_token, ?NEXT_TOKEN}]) + ), + Tests), + output_tests(?_f( + erlcloud_sm:batch_get_secret_value( + {filters, [[{<<"Key">>,<<"name">>}, {<<"Values">>, [?SECRET_ID, ?SECRET_ID2]}]]}, + [{max_results, ?MAX_RESULTS}, {next_token, ?NEXT_TOKEN}]) + ), + Tests). + +cancel_rotate_secret_value_input_tests(_) -> + Tests = [ + ?_sm_test( + {"cancel_rotate_secret input test", + ?_f(erlcloud_sm:cancel_rotate_secret(?SECRET_ID)), + jsx:encode([ + {<<"SecretId">>, ?SECRET_ID} + ]) + }) + ], + Response = <<>>, + input_tests(Response, Tests). + +-define(CANCEL_ROTATE_SECRET_RESP,[ + {<<"ARN">>, + <<"arn:aws:secretsmanager:us-west-2:123456789012:secret:MyTestDatabaseSecret-a1b2c3">>}, + {<<"Name">>, ?SECRET_ID}, + {<<"VersionId">>, ?VERSION_ID} +]). + +cancel_rotate_secret_value_output_tests(_) -> + Tests = [?_sm_test( + {"cancel_rotate_secret output test", + jsx:encode(?CANCEL_ROTATE_SECRET_RESP), + {ok, ?CANCEL_ROTATE_SECRET_RESP}} + )], + + output_tests(?_f(erlcloud_sm:cancel_rotate_secret(?SECRET_ID)), Tests). + + +create_secret_value_input_tests(_) -> + Tests = [ + ?_sm_test( + {"create_secret_string input test", + ?_f(erlcloud_sm:create_secret_string(?SECRET_ID, ?CLIENT_REQUEST_TOKEN, ?SECRET_STRING)), + jsx:encode([ + {<<"ClientRequestToken">>,?CLIENT_REQUEST_TOKEN}, + {<<"Name">>,?SECRET_ID}, + {<<"SecretString">>,?SECRET_STRING}]) + }), + ?_sm_test( + {"create_secret_binary input test", + ?_f(erlcloud_sm:create_secret_binary(?SECRET_ID, ?CLIENT_REQUEST_TOKEN, ?SECRET_STRING)), + jsx:encode([ + {<<"ClientRequestToken">>,?CLIENT_REQUEST_TOKEN}, + {<<"Name">>,?SECRET_ID}, + {<<"SecretBinary">>,?SECRET_BINARY}]) + }), + ?_sm_test( + {"create_secret string input test", + ?_f(erlcloud_sm:create_secret(?SECRET_ID, ?CLIENT_REQUEST_TOKEN, {secret_string, ?SECRET_STRING})), + jsx:encode([ + {<<"ClientRequestToken">>,?CLIENT_REQUEST_TOKEN}, + {<<"Name">>,?SECRET_ID}, + {<<"SecretString">>,?SECRET_STRING}]) + }), + ?_sm_test( + {"create_secret binary input test", + ?_f(erlcloud_sm:create_secret(?SECRET_ID, ?CLIENT_REQUEST_TOKEN, {secret_binary, ?SECRET_STRING})), + jsx:encode([ + {<<"ClientRequestToken">>,?CLIENT_REQUEST_TOKEN}, + {<<"Name">>,?SECRET_ID}, + {<<"SecretBinary">>,?SECRET_BINARY}]) + }), + ?_sm_test( + {"create_secret binary with options input test", + ?_f(erlcloud_sm:create_secret( + ?SECRET_ID, + ?CLIENT_REQUEST_TOKEN, + {secret_binary, ?SECRET_STRING}, + [ + {add_replica_regions, [ + [ + {<<"Region">>, <<"us-east-1">>}, + {<<"KmsKeyId">>, <<"alias/aws/abcd1234-a123-456a-a12b-a123b4cd56ef">>} + ] + ]}, + {description, <<"My test database secret">>}, + {force_overwrite_replica_secret, true}, + {kms_key_id, <<"alias/aws/abcd1234-a123-456a-a12b-a123b4cd56ef">>}, + {tags, [ + [ + {<<"Key">>, <<"Environment">>}, + {<<"Value">>, <<"Production">>} + ] + ]} + ] + )), + jsx:encode([ + {<<"ClientRequestToken">>,?CLIENT_REQUEST_TOKEN}, + {<<"Name">>,?SECRET_ID}, + {<<"SecretBinary">>,?SECRET_BINARY}, + {<<"AddReplicaRegions">>, [ + [ + {<<"Region">>, <<"us-east-1">>}, + {<<"KmsKeyId">>, <<"alias/aws/abcd1234-a123-456a-a12b-a123b4cd56ef">>} + ] + ]}, + {<<"Description">>, <<"My test database secret">>}, + {<<"ForceOverwriteReplicaSecret">>, true}, + {<<"KmsKeyId">>, <<"alias/aws/abcd1234-a123-456a-a12b-a123b4cd56ef">>}, + {<<"Tags">>, [ + [ + {<<"Key">>, <<"Environment">>}, + {<<"Value">>, <<"Production">>} + ] + ]} + ]) + }), + ?_sm_test( + {"create_secret binary with options input test", + ?_f(erlcloud_sm:create_secret( + ?SECRET_ID, + ?CLIENT_REQUEST_TOKEN, + {secret_binary, ?SECRET_STRING}, + [ + {add_replica_regions, [ + [ + {region, <<"us-east-1">>}, + {kms_key_id, <<"alias/aws/abcd1234-a123-456a-a12b-a123b4cd56ef">>} + ], + [ + {region, <<"us-west-2">>}, + {kms_key_id, <<"alias/aws/abcd1234-a123-456a-a12b-a123b4cd56ef">>} + ] + ]}, + {description, <<"My test database secret">>}, + {force_overwrite_replica_secret, true}, + {kms_key_id, <<"alias/aws/abcd1234-a123-456a-a12b-a123b4cd56ef">>}, + {tags, [ + [ + {key, <<"Environment">>}, + {value, <<"Production">>} + ] + ]} + ] + )), + jsx:encode([ + {<<"ClientRequestToken">>,?CLIENT_REQUEST_TOKEN}, + {<<"Name">>,?SECRET_ID}, + {<<"SecretBinary">>,?SECRET_BINARY}, + {<<"AddReplicaRegions">>, [ + [ + {<<"Region">>, <<"us-east-1">>}, + {<<"KmsKeyId">>, <<"alias/aws/abcd1234-a123-456a-a12b-a123b4cd56ef">>} + ], + [ + {<<"Region">>, <<"us-west-2">>}, + {<<"KmsKeyId">>, <<"alias/aws/abcd1234-a123-456a-a12b-a123b4cd56ef">>} + ] + ]}, + {<<"Description">>, <<"My test database secret">>}, + {<<"ForceOverwriteReplicaSecret">>, true}, + {<<"KmsKeyId">>, <<"alias/aws/abcd1234-a123-456a-a12b-a123b4cd56ef">>}, + {<<"Tags">>, [ + [ + {<<"Key">>, <<"Environment">>}, + {<<"Value">>, <<"Production">>} + ] + ]} + ]) + }) + ], + Response = <<>>, + input_tests(Response, Tests). + + +-define(CREATE_SECRET_RESP,[ + {<<"ARN">>, + <<"arn:aws:secretsmanager:us-west-2:123456789012:secret:MyTestDatabaseSecret-a1b2c3">>}, + {<<"Name">>, ?SECRET_ID}, + {<<"ReplicationStatus">>, [ + [ + {<<"KmsKeyId">>, + <<"alias/aws/abcd1234-a123-456a-a12b-a123b4cd56ef">>}, + {<<"LastAccessedDate">>, 1.523477145813E9}, + {<<"Region">>, <<"us-east-1">>}, + {<<"Status">>, <<"InSync">>}, + {<<"StatusMessage">>, <<"Replication succeeded">>} + ] + ]}, + {<<"VersionId">>, ?VERSION_ID} +]). + + +create_secret_value_output_tests(_) -> + Tests = [?_sm_test( + {"create_secret output test", + jsx:encode(?CREATE_SECRET_RESP), + {ok, ?CREATE_SECRET_RESP}} + )], + + output_tests(?_f(erlcloud_sm:create_secret_string(?SECRET_ID, ?CLIENT_REQUEST_TOKEN, ?SECRET_STRING)), Tests), + output_tests(?_f(erlcloud_sm:create_secret_binary(?SECRET_ID, ?CLIENT_REQUEST_TOKEN, ?SECRET_STRING)), Tests), + output_tests(?_f(erlcloud_sm:create_secret(?SECRET_ID, ?CLIENT_REQUEST_TOKEN, {secret_string, ?SECRET_STRING})), Tests), + output_tests(?_f(erlcloud_sm:create_secret(?SECRET_ID, ?CLIENT_REQUEST_TOKEN, {secret_binary, ?SECRET_BINARY})), Tests). + +get_random_password_value_input_tests(_) -> + Tests = [ + ?_sm_test( + {"get_random_password value input test", + ?_f(erlcloud_sm:get_random_password()), + jsx:encode([]) + }), + ?_sm_test( + {"get_random_password input test", + ?_f(erlcloud_sm:get_random_password( + [ + {<<"ExcludeCharacters">>, <<"!@#$%^&*()_+">>}, + {<<"ExcludeLowercase">>, true}, + {<<"ExcludeNumbers">>, true}, + {<<"ExcludePunctuation">>, true}, + {<<"ExcludeUppercase">>, true}, + {<<"IncludeSpace">>, true}, + {<<"PasswordLength">>, 16}, + {<<"RequireEachIncludedType">>, true} + ])), + jsx:encode( + [ + {<<"ExcludeCharacters">>, <<"!@#$%^&*()_+">>}, + {<<"ExcludeLowercase">>, true}, + {<<"ExcludeNumbers">>, true}, + {<<"ExcludePunctuation">>, true}, + {<<"ExcludeUppercase">>, true}, + {<<"IncludeSpace">>, true}, + {<<"PasswordLength">>, 16}, + {<<"RequireEachIncludedType">>, true} + ]) + }) + ], + Response = <<>>, + input_tests(Response, Tests). + +-define(GET_RANDOM_PASSWORD_RESP,[ + {<<"RandomPassword">>, <<"BnQw&XDWgaEeT9XGTT29">>} +]). + +get_random_password_value_output_tests(_) -> + Tests = [?_sm_test( + {"get_random_password value output test", + jsx:encode(?GET_RANDOM_PASSWORD_RESP), + {ok, ?GET_RANDOM_PASSWORD_RESP}} + )], + + output_tests(?_f(erlcloud_sm:get_random_password()), Tests). + +get_secret_value_input_tests(_) -> + Tests = [ + ?_sm_test( + {"get_secret_value2id input test", + ?_f(erlcloud_sm:get_secret_value(?SECRET_ID, [{version_id, ?VERSION_ID}])), + jsx:encode([ + {<<"SecretId">>, ?SECRET_ID}, + {<<"VersionId">>, ?VERSION_ID} + ]) + }), + ?_sm_test( + {"get_secret_value2stage input test", + ?_f(erlcloud_sm:get_secret_value(?SECRET_ID, [{version_stage, ?VERSION_STAGE}])), + jsx:encode([ + {<<"SecretId">>, ?SECRET_ID}, + {<<"VersionStage">>, ?VERSION_STAGE} + ]) + }) + ], + Response = <<>>, + input_tests(Response, Tests). + + +-define(GET_SECRET_VALUE_RESP,[ + {<<"ARN">>, + <<"arn:aws:secretsmanager:us-west-2:123456789012:secret:MyTestDatabaseSecret-a1b2c3">>}, + {<<"CreatedDate">>, 1.523477145713E9}, + {<<"Name">>, ?SECRET_ID}, + {<<"SecretString">>, + <<"{\n \"username\":\"david\",\n \"password\":\"BnQw&XDWgaEeT9XGTT29\"\n}\n">>}, + {<<"VersionId">>, ?VERSION_ID}, + {<<"VersionStages">>, [?VERSION_STAGE]} +]). + + +get_secret_value_output_tests(_) -> + Tests = [?_sm_test( + {"get_secret_value output test", + jsx:encode(?GET_SECRET_VALUE_RESP), + {ok, ?GET_SECRET_VALUE_RESP}} + )], + + output_tests(?_f(erlcloud_sm:get_secret_value(?SECRET_ID, [{version_id, ?VERSION_ID}])), Tests), + output_tests(?_f(erlcloud_sm:get_secret_value(?SECRET_ID, [{version_stage, ?VERSION_STAGE}])), Tests). + +list_secrets_value_input_tests(_) -> + Tests = [ + ?_sm_test( + {"list_secrets value input test", + ?_f(erlcloud_sm:list_secrets()), + jsx:encode([]) + }), + ?_sm_test( + {"list_secrets input test", + ?_f(erlcloud_sm:list_secrets( + [ + {filters, [[{<<"Key">>,<<"name">>}, {<<"Values">>, [<<"value1">>, <<"value2">>]}]]}, + {max_results, ?MAX_RESULTS}, + {include_planned_deletion, true}, + {next_token, ?NEXT_TOKEN}, + {sort_order, <<"asc">>} + ])), + jsx:encode( + [ + {<<"Filters">>, [[{<<"Key">>,<<"name">>}, {<<"Values">>, [<<"value1">>, <<"value2">>]}]]}, + {<<"MaxResults">>, ?MAX_RESULTS}, + {<<"IncludePlannedDeletion">>, true}, + {<<"NextToken">>, ?NEXT_TOKEN}, + {<<"SortOrder">>, <<"asc">>} + ]) + }) + ], + Response = <<>>, + input_tests(Response, Tests). + +-define(LIST_SECRETS_VALUE_RESP,[ + {<<"NextToken">>, ?NEXT_TOKEN}, + {<<"SecretList">>, [ + [ + {<<"ARN">>, + <<"arn:aws:secretsmanager:us-west-2:123456789012:secret:MyTestDatabaseSecret-a1b2c3">>}, + {<<"CreatedDate">>, 1.523477145713E9}, + {<<"DeletedDate">>, 1.523477145813E9}, + {<<"Description">>, <<"My test database secret">>}, + {<<"KmsKeyId">>, + <<"alias/aws/abcd1234-a123-456a-a12b-a123b4cd56ef">>}, + {<<"LastAccessedDate">>, 1.523477145913E9}, + {<<"LastChangedDate">>, 1.523477146013E9}, + {<<"LastRotatedDate">>, 1.523477146113E9}, + {<<"Name">>, ?SECRET_ID}, + {<<"NextRotationDate">>, 1.523477146213E9}, + {<<"OwningService">>, <<"secretsmanager">>}, + {<<"PrimaryRegion">>, <<"us-west-2">>}, + {<<"RotationEnabled">>, true}, + {<<"RotationLambdaARN">>, + <<"arn:aws:lambda:us-west-2:123456789012:function:MySecretRotationFunction">>}, + {<<"RotationRules">>, [ + {<<"AutomaticallyAfterDays">>,90}, + {<<"Duration">>,<<"3h">>}, + {<<"ScheduleExpression">>,<<"rate(30 days)">>} + ]}, + {<<"SecretVersionsToStages">>, [ + { ?VERSION_ID, [?VERSION_STAGE]}, + { ?VERSION_ID2, [<<"AWSCURRENT">>]} + ]}, + {<<"Tags">>, [ + [ + {<<"Key">>, <<"Environment">>}, + {<<"Value">>, <<"Production">>} + ] + ]} + ] + ]} +]). + +list_secrets_value_output_tests(_) -> + Tests = [?_sm_test( + {"list_secrets value output test", + jsx:encode(?LIST_SECRETS_VALUE_RESP), + {ok, ?LIST_SECRETS_VALUE_RESP}} + )], + + output_tests(?_f(erlcloud_sm:list_secrets()), Tests). + +list_secret_version_ids_value_input_tests(_) -> + Tests = [ + ?_sm_test( + {"list_secret_version_ids value input test", + ?_f(erlcloud_sm:list_secret_version_ids(?SECRET_ID)), + jsx:encode([{<<"SecretId">>, ?SECRET_ID}]) + }), + ?_sm_test( + {"list_secret_version_ids input test", + ?_f(erlcloud_sm:list_secret_version_ids( + ?SECRET_ID, + [ + {include_deprecated, true}, + {max_results, ?MAX_RESULTS}, + {next_token, ?NEXT_TOKEN} + ])), + jsx:encode( + [ + {<<"SecretId">>, ?SECRET_ID}, + {<<"IncludeDeprecated">>, true}, + {<<"MaxResults">>, ?MAX_RESULTS}, + {<<"NextToken">>, ?NEXT_TOKEN} + ]) + }) + ], + Response = <<>>, + input_tests(Response, Tests). + +-define(LIST_SECRET_VERSION_IDS_VALUE_RESP,[ + {<<"ARN">>, + <<"arn:aws:secretsmanager:us-west-2:123456789012:secret:MyTestDatabaseSecret-a1b2c3">>}, + {<<"Name">>, ?SECRET_ID}, + {<<"NextToken">>, ?NEXT_TOKEN}, + {<<"Versions">>, [ + [ + {<<"CreatedDate">>, 1.523477145713E9}, + {<<"KmsKeyId">>, + <<"alias/aws/abcd1234-a123-456a-a12b-a123b4cd56ef">>}, + {<<"LastAccessedDate">>, 1.523477145813E9}, + {<<"VersionId">>, ?VERSION_ID}, + {<<"VersionStages">>, [?VERSION_STAGE]} + ] + ]} +]). + +list_secret_version_ids_value_output_tests(_) -> + Tests = [?_sm_test( + {"list_secret_version_ids value output test", + jsx:encode(?LIST_SECRET_VERSION_IDS_VALUE_RESP), + {ok, ?LIST_SECRET_VERSION_IDS_VALUE_RESP}} + )], + + output_tests(?_f(erlcloud_sm:list_secret_version_ids(?SECRET_ID)), Tests). + + +put_secret_value_input_tests(_) -> + Tests = [ + ?_sm_test( + {"put_secret_string input test", + ?_f(erlcloud_sm:put_secret_string(?SECRET_ID, ?CLIENT_REQUEST_TOKEN, ?SECRET_STRING)), + jsx:encode([ + {<<"ClientRequestToken">>,?CLIENT_REQUEST_TOKEN}, + {<<"SecretId">>,?SECRET_ID}, + {<<"SecretString">>,?SECRET_STRING}]) + }), + ?_sm_test( + {"put_secret_binary input test", + ?_f(erlcloud_sm:put_secret_binary(?SECRET_ID, ?CLIENT_REQUEST_TOKEN, ?SECRET_STRING)), + jsx:encode([ + {<<"ClientRequestToken">>,?CLIENT_REQUEST_TOKEN}, + {<<"SecretId">>,?SECRET_ID}, + {<<"SecretBinary">>,?SECRET_BINARY}]) + }), + ?_sm_test( + {"put_secret_value string input test", + ?_f(erlcloud_sm:put_secret_value(?SECRET_ID, ?CLIENT_REQUEST_TOKEN, {secret_string, ?SECRET_STRING})), + jsx:encode([ + {<<"ClientRequestToken">>,?CLIENT_REQUEST_TOKEN}, + {<<"SecretId">>,?SECRET_ID}, + {<<"SecretString">>,?SECRET_STRING}]) + }), + ?_sm_test( + {"put_secret_value binary input test", + ?_f(erlcloud_sm:put_secret_value(?SECRET_ID, ?CLIENT_REQUEST_TOKEN, {secret_binary, ?SECRET_STRING})), + jsx:encode([ + {<<"ClientRequestToken">>,?CLIENT_REQUEST_TOKEN}, + {<<"SecretId">>,?SECRET_ID}, + {<<"SecretBinary">>,?SECRET_BINARY}]) + }), + ?_sm_test( + {"put_secret_value binary input test", + ?_f(erlcloud_sm:put_secret_value( + ?SECRET_ID, + ?CLIENT_REQUEST_TOKEN, + {secret_binary, ?SECRET_STRING}, + [ + {version_stages, [?VERSION_STAGE]}, + {rotation_token, <<"ROTATION-TOKEN-EXAMPLE-1234">>} + ])), + jsx:encode([ + {<<"ClientRequestToken">>,?CLIENT_REQUEST_TOKEN}, + {<<"SecretId">>,?SECRET_ID}, + {<<"SecretBinary">>,?SECRET_BINARY}, + {<<"VersionStages">>, [?VERSION_STAGE]}, + {<<"RotationToken">>, <<"ROTATION-TOKEN-EXAMPLE-1234">>} + ]) + }) + ], + Response = <<>>, + input_tests(Response, Tests). + + +-define(PUT_SECRET_VALUE_RESP,[ + {<<"ARN">>, + <<"arn:aws:secretsmanager:us-west-2:123456789012:secret:MyTestDatabaseSecret-a1b2c3">>}, + {<<"Name">>, ?SECRET_ID}, + {<<"VersionId">>, ?VERSION_ID}, + {<<"VersionStages">>, [?VERSION_STAGE]} +]). + + +put_secret_value_output_tests(_) -> + Tests = [?_sm_test( + {"put_secret_string output test", + jsx:encode(?PUT_SECRET_VALUE_RESP), + {ok, ?PUT_SECRET_VALUE_RESP}} + )], + + output_tests(?_f(erlcloud_sm:put_secret_string(?SECRET_ID, ?CLIENT_REQUEST_TOKEN, ?SECRET_STRING)), Tests), + output_tests(?_f(erlcloud_sm:put_secret_binary(?SECRET_ID, ?CLIENT_REQUEST_TOKEN, ?SECRET_STRING)), Tests), + output_tests(?_f(erlcloud_sm:put_secret_value(?SECRET_ID, ?CLIENT_REQUEST_TOKEN, {secret_string, ?SECRET_STRING})), Tests), + output_tests(?_f(erlcloud_sm:put_secret_value(?SECRET_ID, ?CLIENT_REQUEST_TOKEN, {secret_binary, ?SECRET_BINARY})), Tests). + +remove_regions_from_replication_value_input_tests(_) -> + Tests = [ + ?_sm_test( + {"remove_regions_from_replication value input test", + ?_f(erlcloud_sm:remove_regions_from_replication(?SECRET_ID, [<<"us-east-1">>])), + jsx:encode([{<<"SecretId">>, ?SECRET_ID}, {<<"RemoveReplicaRegions">>, [<<"us-east-1">>]}]) + }) + ], + Response = <<>>, + input_tests(Response, Tests). + +-define(REMOVE_REGIONS_FROM_REPLICATION_VALUE_RESP,[ + {<<"ARN">>, + <<"arn:aws:secretsmanager:us-west-2:123456789012:secret:MyTestDatabaseSecret-a1b2c3">>}, + {<<"ReplicationStatus">>, [ + [ + {<<"KmsKeyId">>, + <<"alias/aws/abcd1234-a123-456a-a12b-a123b4cd56ef">>}, + {<<"LastAccessedDate">>, 1.523477145813E9}, + {<<"Region">>, <<"us-west-2">>}, + {<<"Status">>, <<"InSync">>}, + {<<"StatusMessage">>, <<"Replication succeeded">>} + ] + ]} +]). + +remove_regions_from_replication_value_output_tests(_) -> + Tests = [?_sm_test( + {"remove_regions_from_replication value output test", + jsx:encode(?REMOVE_REGIONS_FROM_REPLICATION_VALUE_RESP), + {ok, ?REMOVE_REGIONS_FROM_REPLICATION_VALUE_RESP}} + )], + + output_tests(?_f(erlcloud_sm:remove_regions_from_replication(?SECRET_ID, [<<"us-east-1">>])), Tests). + +replicate_secret_to_regions_value_input_tests(_) -> + Tests = [ + ?_sm_test( + {"replicate_secret_to_regions value input test", + ?_f(erlcloud_sm:replicate_secret_to_regions( + ?SECRET_ID, + [[{<<"Region">>, <<"us-east-1">>}, {<<"KmsKeyId">>, <<"alias/aws/abcd1234-a123-456a-a12b-a123b4cd56ef">>}]] + )), + jsx:encode([ + {<<"SecretId">>, ?SECRET_ID}, + {<<"ReplicaRegions">>, [[{<<"Region">>, <<"us-east-1">>}, {<<"KmsKeyId">>, <<"alias/aws/abcd1234-a123-456a-a12b-a123b4cd56ef">>}]]} + ]) + }), + ?_sm_test( + {"replicate_secret_to_regions value input test", + ?_f(erlcloud_sm:replicate_secret_to_regions( + ?SECRET_ID, + [[{region, <<"us-east-1">>}, {kms_key_id, <<"alias/aws/abcd1234-a123-456a-a12b-a123b4cd56ef">>}]] + )), + jsx:encode([ + {<<"SecretId">>, ?SECRET_ID}, + {<<"ReplicaRegions">>, [[{<<"Region">>, <<"us-east-1">>}, {<<"KmsKeyId">>, <<"alias/aws/abcd1234-a123-456a-a12b-a123b4cd56ef">>}]]} + ]) + }), + ?_sm_test( + {"replicate_secret_to_regions value input test", + ?_f(erlcloud_sm:replicate_secret_to_regions( + ?SECRET_ID, + [[{<<"Region">>, <<"us-east-1">>}, {<<"KmsKeyId">>, <<"alias/aws/abcd1234-a123-456a-a12b-a123b4cd56ef">>}]], + [{force_overwrite_replica_secret, true}] + )), + jsx:encode([ + {<<"SecretId">>, ?SECRET_ID}, + {<<"ReplicaRegions">>, [[{<<"Region">>, <<"us-east-1">>}, {<<"KmsKeyId">>, <<"alias/aws/abcd1234-a123-456a-a12b-a123b4cd56ef">>}]]}, + {<<"ForceOverwriteReplicaSecret">>, true} + ]) + }) + ], + Response = <<>>, + input_tests(Response, Tests). + +-define(REPLICATE_SECRET_TO_REGIONS_VALUE_RESP,[ + {<<"ARN">>, + <<"arn:aws:secretsmanager:us-west-2:123456789012:secret:MyTestDatabaseSecret-a1b2c3">>}, + {<<"ReplicationStatus">>, [ + [ + {<<"KmsKeyId">>, + <<"alias/aws/abcd1234-a123-456a-a12b-a123b4cd56ef">>}, + {<<"LastAccessedDate">>, 1.523477145813E9}, + {<<"Region">>, <<"us-west-2">>}, + {<<"Status">>, <<"InSync">>}, + {<<"StatusMessage">>, <<"Replication succeeded">>} + ], + [ + {<<"KmsKeyId">>, + <<"alias/aws/abcd1234-a123-456a-a12b-a123b4cd56ef">>}, + {<<"LastAccessedDate">>, 1.523477145813E9}, + {<<"Region">>, <<"us-east-1">>}, + {<<"Status">>, <<"InSync">>}, + {<<"StatusMessage">>, <<"Replication succeeded">>} + ] + ]} +]). + +replicate_secret_to_regions_value_output_tests(_) -> + Tests = [?_sm_test( + {"replicate_secret_to_regions value output test", + jsx:encode(?REPLICATE_SECRET_TO_REGIONS_VALUE_RESP), + {ok, ?REPLICATE_SECRET_TO_REGIONS_VALUE_RESP}} + )], + + output_tests(?_f(erlcloud_sm:replicate_secret_to_regions(?SECRET_ID, [[{region, <<"us-east-1">>}, {kms_key_id, <<"alias/aws/abcd1234-a123-456a-a12b-a123b4cd56ef">>}]])), Tests). + +restore_secret_value_input_tests(_) -> + Tests = [ + ?_sm_test( + {"restore_secret input test", + ?_f(erlcloud_sm:restore_secret(?SECRET_ID)), + jsx:encode([{<<"SecretId">>, ?SECRET_ID}]) + }) + ], + Response = <<>>, + input_tests(Response, Tests). + +-define(RESTORE_SECRET_VALUE_RESP,[ + {<<"ARN">>, + <<"arn:aws:secretsmanager:us-west-2:123456789012:secret:MyTestDatabaseSecret-a1b2c3">>}, + {<<"Name">>, ?SECRET_ID}]). + +restore_secret_value_output_tests(_) -> + Tests = [?_sm_test( + {"restore_secret value output test", + jsx:encode(?RESTORE_SECRET_VALUE_RESP), + {ok, ?RESTORE_SECRET_VALUE_RESP}} + )], + + output_tests(?_f(erlcloud_sm:restore_secret(?SECRET_ID)), Tests). + +rotate_secret_value_input_tests(_) -> + Tests = [ + ?_sm_test( + {"rotate_secret input test", + ?_f(erlcloud_sm:rotate_secret(?SECRET_ID, ?CLIENT_REQUEST_TOKEN)), + jsx:encode([ + {<<"ClientRequestToken">>,?CLIENT_REQUEST_TOKEN}, + {<<"SecretId">>,?SECRET_ID}]) + }), + ?_sm_test( + {"rotate_secret with options input test", + ?_f(erlcloud_sm:rotate_secret( + ?SECRET_ID, + ?CLIENT_REQUEST_TOKEN, + [ + {rotation_lambda_arn, <<"arn:aws:lambda:us-west-2:123456789012:function:MySecretRotationFunction">>}, + {rotate_immediately, false}, + {rotation_rules, [ + {automatically_after_days, 30}, + {duration, <<"3h">>}, + {schedule_expression, <<"rate(30 days)">>} + ]} + ] + )), + jsx:encode([ + {<<"ClientRequestToken">>,?CLIENT_REQUEST_TOKEN}, + {<<"SecretId">>,?SECRET_ID}, + {<<"RotationLambdaARN">>, <<"arn:aws:lambda:us-west-2:123456789012:function:MySecretRotationFunction">>}, + {<<"RotateImmediately">>, false}, + {<<"RotationRules">>, [ + {<<"AutomaticallyAfterDays">>,30}, + {<<"Duration">>,<<"3h">>}, + {<<"ScheduleExpression">>,<<"rate(30 days)">>} + ]} + ]) + }) + ], + Response = <<>>, + input_tests(Response, Tests). + + -define(ROTATE_SECRET_VALUE_RESP,[ + {<<"ARN">>, + <<"arn:aws:secretsmanager:us-west-2:123456789012:secret:MyTestDatabaseSecret-a1b2c3">>}, + {<<"Name">>, ?SECRET_ID}, + {<<"VersionId">>, ?VERSION_ID} + ]). + + rotate_secret_value_output_tests(_) -> + Tests = [?_sm_test( + {"rotate_secret value output test", + jsx:encode(?ROTATE_SECRET_VALUE_RESP), + {ok, ?ROTATE_SECRET_VALUE_RESP}} + )], + + output_tests(?_f(erlcloud_sm:rotate_secret(?SECRET_ID, ?CLIENT_REQUEST_TOKEN)), Tests), + output_tests(?_f(erlcloud_sm:rotate_secret( + ?SECRET_ID, + ?CLIENT_REQUEST_TOKEN, + [ + {rotation_lambda_arn, <<"arn:aws:lambda:us-west-2:123456789012:function:MySecretRotationFunction">>}, + {rotate_immediately, false}, + {rotation_rules, [ + {automatically_after_days, 30}, + {duration, <<"3h">>}, + {schedule_expression, <<"rate(30 days)">>} + ]} + ] + )), Tests). + + stop_replication_to_replica_value_input_tests(_) -> + Tests = [ + ?_sm_test( + {"stop_replication_to_replica input test", + ?_f(erlcloud_sm:stop_replication_to_replica(?SECRET_ID)), + jsx:encode([{<<"SecretId">>, ?SECRET_ID}]) + }) + ], + Response = <<>>, + input_tests(Response, Tests). + + -define(STOP_REPLICATION_TO_REPLICA_VALUE_RESP,[ + [{<<"ARN">>, <<"arn:aws:secretsmanager:us-west-2:123456789012:secret:MyTestDatabaseSecret-a1b2c3">>}] + ]). + + stop_replication_to_replica_value_output_tests(_) -> + Tests = [?_sm_test( + {"stop_replication_to_replica value output test", + jsx:encode(?STOP_REPLICATION_TO_REPLICA_VALUE_RESP), + {ok, ?STOP_REPLICATION_TO_REPLICA_VALUE_RESP}} + )], + output_tests(?_f(erlcloud_sm:stop_replication_to_replica(?SECRET_ID)), Tests). + + tag_resource_value_input_tests(_) -> + Tests = [ + ?_sm_test( + {"tag_resource input test", + ?_f(erlcloud_sm:tag_resource(?SECRET_ID, [ + [ {<<"Key">>, <<"Environment">>}, {<<"Value">>, <<"Production">>} ] + ])), + jsx:encode([ + {<<"SecretId">>, ?SECRET_ID}, + {<<"Tags">>, [ + [ {<<"Key">>, <<"Environment">>}, {<<"Value">>, <<"Production">>} ] + ]} + ]) + }), + ?_sm_test( + {"tag_resource input test", + ?_f(erlcloud_sm:tag_resource(?SECRET_ID, [ + [ {key, <<"Environment">>}, {value, <<"Production">>} ] + ])), + jsx:encode([ + {<<"SecretId">>, ?SECRET_ID}, + {<<"Tags">>, [ + [ {<<"Key">>, <<"Environment">>}, {<<"Value">>, <<"Production">>} ] + ]} + ]) + }) + ], + Response = <<>>, + input_tests(Response, Tests). + + tag_resource_value_output_tests(_) -> + Tests = [?_sm_test( + {"tag_resource value output test", + <<>>, + {ok, []}} + )], + output_tests(?_f(erlcloud_sm:tag_resource(?SECRET_ID, [ + [ {<<"Key">>, <<"Environment">>}, {<<"Value">>, <<"Production">>} ] + ])), Tests), + output_tests(?_f(erlcloud_sm:tag_resource(?SECRET_ID, [ + [ {key, <<"Environment">>}, {value, <<"Production">>} ] + ])), Tests). + + untag_resource_value_input_tests(_) -> + Tests = [ + ?_sm_test( + {"untag_resource input test", + ?_f(erlcloud_sm:untag_resource(?SECRET_ID, ["Environment"])), + jsx:encode([{<<"SecretId">>, ?SECRET_ID}, {<<"TagKeys">>, ["Environment"]}]) + }) + ], + Response = <<>>, + input_tests(Response, Tests). + + untag_resource_value_output_tests(_) -> + Tests = [?_sm_test( + {"untag_resource value output test", + <<>>, + {ok, []}} + )], + output_tests(?_f(erlcloud_sm:untag_resource(?SECRET_ID, ["Environment"])), Tests). + + update_secret_value_input_tests(_) -> + Tests = [ + ?_sm_test( + {"update_secret input test", + ?_f(erlcloud_sm:update_secret(?SECRET_ID, ?CLIENT_REQUEST_TOKEN, + [ + {secret_string, ?SECRET_STRING}, + {description, <<"Updated secret description">>}, + {kms_key_id, <<"alias/aws/abcd1234-a123-456a-a12b-a123b4cd56ef">>} + ])), + jsx:encode([ + {<<"ClientRequestToken">>,?CLIENT_REQUEST_TOKEN}, + {<<"SecretId">>,?SECRET_ID}, + {<<"SecretString">>,?SECRET_STRING}, + {<<"Description">>, <<"Updated secret description">>}, + {<<"KmsKeyId">>, <<"alias/aws/abcd1234-a123-456a-a12b-a123b4cd56ef">>} + ]) + }), + ?_sm_test( + {"update_secret input test", + ?_f(erlcloud_sm:update_secret(?SECRET_ID, ?CLIENT_REQUEST_TOKEN, + [ + {secret_binary, ?SECRET_STRING}, + {description, <<"Updated secret description">>}, + {kms_key_id, <<"alias/aws/abcd1234-a123-456a-a12b-a123b4cd56ef">>} + ])), + jsx:encode([ + {<<"ClientRequestToken">>,?CLIENT_REQUEST_TOKEN}, + {<<"SecretId">>,?SECRET_ID}, + {<<"SecretBinary">>,?SECRET_BINARY}, + {<<"Description">>, <<"Updated secret description">>}, + {<<"KmsKeyId">>, <<"alias/aws/abcd1234-a123-456a-a12b-a123b4cd56ef">>} + ]) + }) + ], + Response = <<>>, + input_tests(Response, Tests). + + -define(UPDATE_SECRET_VALUE_RESP,[ + {<<"ARN">>, + <<"arn:aws:secretsmanager:us-west-2:123456789012:secret:MyTestDatabaseSecret-a1b2c3">>}, + {<<"Name">>, ?SECRET_ID}, + {<<"VersionId">>, ?VERSION_ID} + ]). + + update_secret_value_output_tests(_) -> + Tests = [?_sm_test( + {"update_secret value output test", + jsx:encode(?UPDATE_SECRET_VALUE_RESP), + {ok, ?UPDATE_SECRET_VALUE_RESP}} + )], + output_tests(?_f(erlcloud_sm:update_secret(?SECRET_ID, ?CLIENT_REQUEST_TOKEN, + [ + {secret_string, ?SECRET_STRING}, + {description, <<"Updated secret description">>}, + {kms_key_id, <<"alias/aws/abcd1234-a123-456a-a12b-a123b4cd56ef">>} + ])), Tests), + output_tests(?_f(erlcloud_sm:update_secret(?SECRET_ID, ?CLIENT_REQUEST_TOKEN, + [ + {secret_binary, ?SECRET_STRING}, + {description, <<"Updated secret description">>}, + {kms_key_id, <<"alias/aws/abcd1234-a123-456a-a12b-a123b4cd56ef">>} + ])), Tests). + + update_secret_version_stage_value_input_tests(_) -> + Tests = [ + ?_sm_test( + {"update_secret_version_stage input test", + ?_f(erlcloud_sm:update_secret_version_stage( + ?SECRET_ID, + ?VERSION_STAGE + )), + jsx:encode([ + {<<"SecretId">>, ?SECRET_ID}, + {<<"VersionStage">>, ?VERSION_STAGE} + ]) + }), + ?_sm_test( + {"update_secret_version_stage input test", + ?_f(erlcloud_sm:update_secret_version_stage( + ?SECRET_ID, + ?VERSION_STAGE, + [{remove_from_version_id, ?VERSION_ID2}, {move_to_version_id, ?VERSION_ID}] + )), + jsx:encode([ + {<<"SecretId">>, ?SECRET_ID}, + {<<"VersionStage">>, ?VERSION_STAGE}, + {<<"MoveToVersionId">>, ?VERSION_ID}, + {<<"RemoveFromVersionId">>, ?VERSION_ID2} + ]) + }) + ], + Response = <<>>, + input_tests(Response, Tests). + + -define(UPDATE_SECRET_VERSION_STAGE_VALUE_RESP,[ + {<<"ARN">>, + <<"arn:aws:secretsmanager:us-west-2:123456789012:secret:MyTestDatabaseSecret-a1b2c3">>}, + {<<"Name">>, ?SECRET_ID} + ]). + + update_secret_version_stage_value_output_tests(_) -> + Tests = [?_sm_test( + {"update_secret_version_stage value output test", + jsx:encode(?UPDATE_SECRET_VERSION_STAGE_VALUE_RESP), + {ok, ?UPDATE_SECRET_VERSION_STAGE_VALUE_RESP}} + )], + output_tests(?_f(erlcloud_sm:update_secret_version_stage( + ?SECRET_ID, + ?VERSION_STAGE + )), Tests), + output_tests(?_f(erlcloud_sm:update_secret_version_stage( + ?SECRET_ID, + ?VERSION_STAGE, + [{remove_from_version_id, ?VERSION_ID2}, {move_to_version_id, ?VERSION_ID}] + )), Tests). + + validate_resource_policy_value_input_tests(_) -> + Tests = [ + ?_sm_test( + {"validate_resource_policy input test", + ?_f(erlcloud_sm:validate_resource_policy(<<"policy-document">>)), + jsx:encode([ + {<<"ResourcePolicy">>, <<"policy-document">>} + ]) + }), + ?_sm_test( + {"validate_resource_policy input test", + ?_f(erlcloud_sm:validate_resource_policy(<<"policy-document">>, [{secret_id, ?SECRET_ID}])), + jsx:encode([ + {<<"ResourcePolicy">>, <<"policy-document">>}, + {<<"SecretId">>, ?SECRET_ID} + ]) + }) + ], + Response = <<>>, + input_tests(Response, Tests). + + -define(VALIDATE_RESOURCE_POLICY_VALUE_RESP,[ + {<<"PolicyValidationPassed">>, true}, + {<<"ValidationErrors">>, []} + ]). + + validate_resource_policy_value_output_tests(_) -> + Tests = [?_sm_test( + {"validate_resource_policy value output test", + jsx:encode(?VALIDATE_RESOURCE_POLICY_VALUE_RESP), + {ok, ?VALIDATE_RESOURCE_POLICY_VALUE_RESP}} + )], + output_tests(?_f(erlcloud_sm:validate_resource_policy(<<"policy-document">>)), Tests), + output_tests(?_f(erlcloud_sm:validate_resource_policy(<<"policy-document">>, [{secret_id, ?SECRET_ID}])), Tests). \ No newline at end of file diff --git a/test/erlcloud_sns_tests.erl b/test/erlcloud_sns_tests.erl index 3fffbd1f0..60cd9e9f9 100644 --- a/test/erlcloud_sns_tests.erl +++ b/test/erlcloud_sns_tests.erl @@ -37,6 +37,12 @@ sns_api_test_() -> fun delete_topic_input_tests/1, fun subscribe_input_tests/1, fun subscribe_output_tests/1, + fun create_platform_application_input_tests/1, + fun create_platform_application_output_tests/1, + fun get_platform_application_attributes_input_tests/1, + fun get_platform_application_attributes_output_tests/1, + fun set_platform_application_attributes_input_tests/1, + fun set_platform_application_attributes_output_tests/1, fun set_topic_attributes_input_tests/1, fun set_topic_attributes_output_tests/1, fun set_subscription_attributes_input_tests/1, @@ -46,7 +52,8 @@ sns_api_test_() -> fun list_subscriptions_input_tests/1, fun list_subscriptions_output_tests/1, fun list_subscriptions_by_topic_input_tests/1, - fun list_subscriptions_by_topic_output_tests/1 + fun list_subscriptions_by_topic_output_tests/1, + fun publish_invalid_xml_response_output_tests/1 ]}. start() -> @@ -162,7 +169,12 @@ output_test(Fun, {Line, {Description, Response, Result}}) -> fun() -> meck:expect(erlcloud_httpc, request, output_expect(Response)), erlcloud_ec2:configure(string:copies("A", 20), string:copies("a", 40)), - Actual = Fun(), + Actual = try + Fun() + catch + _Class:Error -> + Error + end, ?assertEqual(Result, Actual) end}}. @@ -279,13 +291,142 @@ subscribe_output_tests(_) -> "arn:aws:sns:us-west-2:123456789012:MyTopic")), Tests). +create_platform_application_input_tests(_) -> + Tests = + [?_sns_test( + {"Test to create platform application.", + ?_f(erlcloud_sns:create_platform_application("TestApp", "ADM")), + [ + {"Action", "CreatePlatformApplication"}, + {"Name", "TestApp"}, + {"Platform", "ADM"} + ]}) + ], + + Response = " + + + arn:aws:sns:us-west-2:123456789012:app/ADM/TestApp + + + b6f0e78b-e9d4-5a0e-b973-adc04e8a4ff9 + + ", + + input_tests(Response, Tests). + +create_platform_application_output_tests(_) -> + Tests = [?_sns_test( + {"This is a create platform application test.", + " + + arn:aws:sns:us-west-2:123456789012:app/ADM/TestApp + + + b6f0e78b-e9d4-5a0e-b973-adc04e8a4ff9 + + ", + "arn:aws:sns:us-west-2:123456789012:app/ADM/TestApp"}) + ], + output_tests(?_f(erlcloud_sns:create_platform_application("ADM", "TestApp")), Tests). + + +get_platform_application_attributes_input_tests(_) -> + Tests = + [?_sns_test( + {"Test to get platform application attributes.", + ?_f(erlcloud_sns:get_platform_application_attributes("TestAppArn")), + [ + {"Action", "GetPlatformApplicationAttributes"}, + {"PlatformApplicationArn", "TestAppArn"} + ]}) + ], + + Response = " + + + + + EventDeliveryFailure + arn:aws:sns:us-west-2:123456789012:topicarn + + + + + b6f0e78b-e9d4-5a0e-b973-adc04e8a4ff9 + + ", + + input_tests(Response, Tests). + +get_platform_application_attributes_output_tests(_) -> + Tests = [?_sns_test( + {"This is a get platform application attributes test.", + " + + + + EventDeliveryFailure + arn:aws:sns:us-west-2:123456789012:topicarn + + + + + b6f0e78b-e9d4-5a0e-b973-adc04e8a4ff9 + + ", + [{arn, "TestAppArn"}, + {attributes, [{event_delivery_failure, "arn:aws:sns:us-west-2:123456789012:topicarn"}]}] + }) + ], + output_tests(?_f(erlcloud_sns:get_platform_application_attributes("TestAppArn")), Tests). + + +set_platform_application_attributes_input_tests(_) -> + Tests = + [?_sns_test( + {"Test to set platform application attributes.", + ?_f(erlcloud_sns:set_platform_application_attributes( + "TestAppArn", + [{platform_principal, "some-api-key"}])), + [ + {"Action", "SetPlatformApplicationAttributes"}, + {"PlatformApplicationArn", "TestAppArn"}, + {"Attributes.entry.1.key", "PlatformPrincipal"}, + {"Attributes.entry.1.value", "some-api-key"} + ]}) + ], + + Response = " + + + cf577bcc-b3dc-5463-88f1-3180b9412395 + + ", + input_tests(Response, Tests). + +set_platform_application_attributes_output_tests(_) -> + Tests = [?_sns_test( + {"This is a set platform application attributes test.", + " + + cf577bcc-b3dc-5463-88f1-3180b9412395 + + ", + "cf577bcc-b3dc-5463-88f1-3180b9412395" + }) + ], + output_tests(?_f( + erlcloud_sns:set_platform_application_attributes("TestAppArn", [{platform_principal, "some-api_key"}])), Tests). + + %% Set topic attributes test based on the API examples: %% http://docs.aws.amazon.com/sns/latest/APIReference/API_SetTopicAttributes.html set_topic_attributes_input_tests(_) -> Tests = [?_sns_test( {"Test sets topic's attribute.", - ?_f(erlcloud_sns:set_topic_attributes("DisplayName", "MyTopicName", "arn:aws:sns:us-west-2:123456789012:MyTopic")), + ?_f(erlcloud_sns:set_topic_attributes('DisplayName', "MyTopicName", "arn:aws:sns:us-west-2:123456789012:MyTopic")), [ {"Action", "SetTopicAttributes"}, {"AttributeName", "DisplayName"}, @@ -313,7 +454,7 @@ set_topic_attributes_output_tests(_) -> ", ok}) ], - output_tests(?_f(erlcloud_sns:set_topic_attributes("DisplayName", "MyTopicName", "arn:aws:sns:us-west-2:123456789012:MyTopic")), Tests). + output_tests(?_f(erlcloud_sns:set_topic_attributes('DisplayName', "MyTopicName", "arn:aws:sns:us-west-2:123456789012:MyTopic")), Tests). %% Set subscription attributes test based on the API examples: @@ -322,7 +463,7 @@ set_subscription_attributes_input_tests(_) -> Tests = [?_sns_test( {"Test sets subscriptions's attribute.", - ?_f(erlcloud_sns:set_subscription_attributes("FilterPolicy", "{\"a\": [\"b\"]}", "arn:aws:sns:us-east-1:123456789012:My-Topic:80289ba6-0fd4-4079-afb4-ce8c8260f0ca")), + ?_f(erlcloud_sns:set_subscription_attributes('FilterPolicy', "{\"a\": [\"b\"]}", "arn:aws:sns:us-east-1:123456789012:My-Topic:80289ba6-0fd4-4079-afb4-ce8c8260f0ca")), [ {"Action", "SetSubscriptionAttributes"}, {"AttributeName", "FilterPolicy"}, @@ -350,7 +491,7 @@ set_subscription_attributes_output_tests(_) -> ", ok}) ], - output_tests(?_f(erlcloud_sns:set_subscription_attributes("FilterPolicy", "{\"a\": [\"b\"]}", "arn:aws:sns:us-east-1:123456789012:My-Topic:80289ba6-0fd4-4079-afb4-ce8c8260f0ca")), Tests). + output_tests(?_f(erlcloud_sns:set_subscription_attributes('FilterPolicy', "{\"a\": [\"b\"]}", "arn:aws:sns:us-east-1:123456789012:My-Topic:80289ba6-0fd4-4079-afb4-ce8c8260f0ca")), Tests). %% List topics test based on the API example: @@ -613,7 +754,7 @@ list_subscriptions_by_topic_input_tests(_) -> {"TopicArn", "Arn"} ]}), ?_sns_test( - {"Test lists Subscriptions toke.", + {"Test lists Subscriptions token.", ?_f(erlcloud_sns:list_subscriptions_by_topic("Arn", "Token")), [ {"Action","ListSubscriptionsByTopic"}, @@ -735,6 +876,15 @@ list_subscriptions_by_topic_output_tests(_) -> ]}) ]). +publish_invalid_xml_response_output_tests(_) -> + Config = erlcloud_aws:default_config(), + output_tests(?_f(erlcloud_sns:publish(topic, "arn:aws:sns:us-east-1:123456789012:My-Topic", "test message", undefined, Config)), + [?_sns_test( + {"Test PublishTopic invalid XML return", + "", + {sns_error, {aws_error, {invalid_xml_response_document, <<>>}}}}) + ]). + defaults_to_https(_) -> @@ -762,6 +912,7 @@ doesnt_support_gopher(_) -> ?_assertError({sns_error, {unsupported_scheme,"gopher://"}}, erlcloud_sns:publish_to_topic("topicarn", "message", "subject", Config)). +-dialyzer({nowarn_function, doesnt_accept_non_strings/1}). doesnt_accept_non_strings(_) -> Config = (erlcloud_aws:default_config())#aws_config{sns_scheme=https}, ?_assertError({sns_error, badarg}, diff --git a/test/erlcloud_sqs_tests.erl b/test/erlcloud_sqs_tests.erl index edfa5f8ac..d9fcf3a7e 100644 --- a/test/erlcloud_sqs_tests.erl +++ b/test/erlcloud_sqs_tests.erl @@ -18,11 +18,17 @@ erlcloud_api_test_() -> fun stop/1, [ fun set_queue_attributes/1, + fun get_queue_attributes_all_output/1, + fun get_queue_attributes_all_unknown_output/1, + fun get_queue_attributes_all_input/1, + fun get_queue_attributes_specific_input/1, + fun get_queue_url/1, fun send_message_with_message_opts/1, fun send_message_with_message_attributes/1, fun receive_messages_with_message_attributes/1, fun send_message_batch/1, - fun send_message_batch_with_message_attributes/1 + fun send_message_batch_with_message_attributes/1, + fun receive_messages_with_unknown_attributes/1 ]}. start() -> @@ -161,6 +167,215 @@ set_queue_attributes(_) -> ", input_tests(Response, Tests). +get_queue_attributes_all_output(_) -> + GetQueueAttributesResponse = " + + + + ReceiveMessageWaitTimeSeconds + 2 + + + VisibilityTimeout + 30 + + + ApproximateNumberOfMessages + 0 + + + ApproximateNumberOfMessagesNotVisible + 0 + + + CreatedTimestamp + 1286771522 + + + LastModifiedTimestamp + 1286771522 + + + QueueArn + arn:aws:sqs:us-east-2:123456789012:MyQueue + + + MaximumMessageSize + 8192 + + + MessageRetentionPeriod + 345600 + + + + 1ea71be5-b5a2-4f9d-b85a-945d8d08cd0b + +", + + Expected = [{receive_message_wait_time_seconds, 2}, + {visibility_timeout, 30}, + {approximate_number_of_messages, 0}, + {approximate_number_of_messages_not_visible, 0}, + {created_timestamp, 1286771522}, + {last_modified_timestamp, 1286771522}, + {queue_arn, "arn:aws:sqs:us-east-2:123456789012:MyQueue"}, + {maximum_message_size, 8192}, + {message_retention_period, 345600}], + Tests = + [?_sqs_test( + {"Test receives a get queue attributes result with all attributes (default).", + GetQueueAttributesResponse, Expected})], + + output_tests(?_f(erlcloud_sqs:get_queue_attributes("MyQueue")), Tests). + +get_queue_attributes_all_unknown_output(_) -> + GetQueueAttributesResponse = " + + + + ReceiveMessageWaitTimeSeconds + 2 + + + VisibilityTimeout + 30 + + + ApproximateNumberOfMessages + 0 + + + ApproximateNumberOfMessagesNotVisible + 0 + + + CreatedTimestamp + 1286771522 + + + LastModifiedTimestamp + 1286771522 + + + QueueArn + arn:aws:sqs:us-east-2:123456789012:MyQueue + + + MaximumMessageSize + 8192 + + + MessageRetentionPeriod + 345600 + + + UnrecognizedAttribute + UnrecognizedValue + + + + 1ea71be5-b5a2-4f9d-b85a-945d8d08cd0b + +", + Expected = [{receive_message_wait_time_seconds,2}, + {visibility_timeout, 30}, + {approximate_number_of_messages, 0}, + {approximate_number_of_messages_not_visible, 0}, + {created_timestamp, 1286771522}, + {last_modified_timestamp, 1286771522}, + {queue_arn, "arn:aws:sqs:us-east-2:123456789012:MyQueue"}, + {maximum_message_size, 8192}, + {message_retention_period, 345600}, + {"UnrecognizedAttribute", "UnrecognizedValue"}], + Tests = + [?_sqs_test( + {"Test receives a get queue attributes result with all attributes (default).", + GetQueueAttributesResponse, Expected})], + + output_tests(?_f(erlcloud_sqs:get_queue_attributes("MyQueue")), Tests). + +get_queue_attributes_all_input(_) -> + Expected = [ + {"Action", "GetQueueAttributes"}, + {"AttributeName.1", "All"} + ], + Tests = + [?_sqs_test( + {"Test getting queue attributes (specific).", + ?_f(erlcloud_sqs:get_queue_attributes("MyQueue")), + Expected})], + Response = " + + + + QueueArn + arn:aws:sqs:us-east-2:123456789012:MyQueue + + + + 1ea71be5-b5a2-4f9d-b85a-945d8d08cd0b + +", + input_tests(Response, Tests). + +get_queue_attributes_specific_input(_) -> + Expected = [ + {"Action", "GetQueueAttributes"}, + {"AttributeName.1", "VisibilityTimeout"}, + {"AttributeName.2", "DelaySeconds"}, + {"AttributeName.3", "ReceiveMessageWaitTimeSeconds"} + ], + Tests = + [?_sqs_test( + {"Test getting queue attributes (specific).", + ?_f(erlcloud_sqs:get_queue_attributes("MyQueue", [visibility_timeout, + delay_seconds, + receive_message_wait_time_seconds])), + Expected})], + Response = " + + + + VisibilityTimeout + 30 + + + DelaySeconds + 0 + + + ReceiveMessageWaitTimeSeconds + 2 + + + + 1ea71be5-b5a2-4f9d-b85a-945d8d08cd0b + +", + input_tests(Response, Tests). + +get_queue_url(_) -> + Expected = [ + {"Action", "GetQueueUrl"}, + {"QueueName", "Queue"} + ], + Tests = + [?_sqs_test( + {"Test queue URL getting.", + ?_f(erlcloud_sqs:get_queue_url("Queue")), + Expected})], + Response = " + + + https://sqs.us-east-2.amazonaws.com/123456789012/Queue + + + 470a6f13-2ed9-4181-ad8a-2fdea142988e + +", + input_tests(Response, Tests). + send_message_with_message_opts(_) -> MessageBody = "Hello", MessageOpts = [ @@ -305,6 +520,13 @@ receive_messages_with_message_attributes(_) -> 42 + + number + + Number + 56 + + binary @@ -342,6 +564,7 @@ receive_messages_with_message_attributes(_) -> {"content-type", "application/json"}, {"float", 3.1415926}, {"integer", 42}, + {"number", 56}, {"binary", <<"Binary string">>}, {"uuid", {"uuid", <<"db3bf1fc-0cac-4cf8-8d2c-5c307ad4ac3a">>}} ]} @@ -352,6 +575,44 @@ receive_messages_with_message_attributes(_) -> MessageResponse, Expected})], output_tests(?_f(erlcloud_sqs:receive_message("Queue", all, 1, 30, none, all, erlcloud_aws:default_config())), Tests). + receive_messages_with_unknown_attributes(_) -> + MessageResponse = " + + + + 5fea7756-0ea4-451a-a703-a558b933e274 + MbZj6wDWli+JvwwJaBV+3dcjk2YW2vA3+STFFljTM8tJJg6HRG6PYSasuWXPJB+CwLj1FjgXUv1uSj1gUPAWV66FU/WeR4mq2OKpEGYWbnLmpRCJVAyeMjeU5ZBdtcQ+QEauMZc8ZRv37sIW2iJKq3M9MFx1YvV11A2x/KSbkJ0= + fafb00f5732ab283681e124bf8747ed1 + This is a test message + + unknown + + Unknown + invalid + + + + + + + b6633655-283d-45b4-aee4-4e84e0ae6afa + + + ", + + F = fun() -> + ?assertException(error,decode_message_attribute_value_error, + erlcloud_sqs:receive_message("Queue", all, 1, 30, none, all, erlcloud_aws:default_config())), + decode_message_attribute_value_error + end, + + Tests = + [?_sqs_test( + {"Test receives a message with message unknown attribute.", + MessageResponse, decode_message_attribute_value_error})], + + output_tests(F, Tests). + send_message_batch(_) -> Batch = [{"batch-message-01", "Hello", [], [{message_group_id, "GroupId"}]}, {"batch-message-02", "World", [], [{message_deduplication_id, "DedupId"}]}], diff --git a/test/erlcloud_ssm_tests.erl b/test/erlcloud_ssm_tests.erl new file mode 100644 index 000000000..967ace427 --- /dev/null +++ b/test/erlcloud_ssm_tests.erl @@ -0,0 +1,443 @@ +%% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- +-module(erlcloud_ssm_tests). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("include/erlcloud_ssm.hrl"). + +%% Unit tests for erlcloud_ssm. +%% These tests work by using meck to mock erlcloud_httpc. There are two classes of test: input and output. +%% +%% Input tests verify that different function args produce the desired JSON request. +%% An input test list provides a list of funs and the JSON that is expected to result. +%% +%% Output tests verify that the http response produces the correct return from the fun. +%% An output test lists provides a list of response bodies and the expected return. + +%% The _ssm_test macro provides line number annotation to a test, similar to _test, but doesn't wrap in a fun +-define(_ssm_test(T), {?LINE, T}). +%% The _f macro is a terse way to wrap code in a fun. Similar to _test but doesn't annotate with a line number +-define(_f(F), fun() -> F end). + +-export([validate_body/2]). + +%%%=================================================================== +%%% Test entry points +%%%=================================================================== +operation_test_() -> + {foreach, + fun start/0, + fun stop/1, + [ + fun get_parameter_input_tests/1, + fun get_parameter_output_tests/1, + fun get_parameters_input_tests/1, + fun get_parameters_output_tests/1, + fun get_parameters_by_path_input_tests/1, + fun get_parameters_by_path_output_tests/1, + fun put_parameter_input_tests/1, + fun put_parameter_output_tests/1, + fun delete_parameter_input_tests/1, + fun delete_parameter_output_tests/1 + ] + }. + +start() -> + meck:new(erlcloud_httpc), + ok. + + +stop(_) -> + meck:unload(erlcloud_httpc). + +%%%=================================================================== +%% Actual test specifiers +%%%=================================================================== + +%% GetParameter test based on the API examples: +%% https://docs.aws.amazon.com/systems-manager/latest/APIReference/API_GetParameter.html +get_parameter_input_tests(_) -> + Tests = + [?_ssm_test( + {"GetParameter example request", + ?_f(erlcloud_ssm:get_parameter([{name, "/root/parameter_1"}])), " +{ + \"Name\": \"/root/parameter_1\" +}" + }) + ], + + Response = " +{ + \"Parameter\": [ + { + \"ARN\": \"arn:aws:ssm:us-west-2:000000000000:parameter/root/parameter_1\", + \"DataType\": \"text\", + \"LastModifiedDate\": 1612970194.057, + \"Name\": \"/root/parameter_1\", + \"Type\": \"String\", + \"Value\": \"testvalue\", + \"Version\": 1 + } + ] +}", + input_tests(Response, Tests). + + +get_parameter_output_tests(_) -> + Tests = + [?_ssm_test( + {"GetParameter example response", " +{ + \"Parameter\": + { + \"ARN\": \"arn:aws:ssm:us-west-2:000000000000:parameter/root/parameter_1\", + \"DataType\": \"text\", + \"LastModifiedDate\": 1612970194.057, + \"Name\": \"/root/parameter_1\", + \"Type\": \"String\", + \"Value\": \"testvalue\", + \"Version\": 1 + } +}", + {ok, #ssm_get_parameter{parameter = #ssm_parameter{arn = <<"arn:aws:ssm:us-west-2:000000000000:parameter/root/parameter_1">>, + data_type = <<"text">>, last_modified_date = 1612970194.057, + name = <<"/root/parameter_1">>, selector = undefined, + source_result = undefined, type = <<"String">>, + value = <<"testvalue">>, version = 1}}} + })], + output_tests(?_f(erlcloud_ssm:get_parameter([{name, "/root/parameter_1"}, {out, record}])), Tests). + + +%% GetParameters test based on the API examples: +%% https://docs.aws.amazon.com/systems-manager/latest/APIReference/API_GetParameters.html +get_parameters_input_tests(_) -> + Tests = + [?_ssm_test( + {"GetParameters example request", + ?_f(erlcloud_ssm:get_parameters([{names, ["/root/parameter_1", "/root/secured_parameter", "some_invalid_param"]}, + {with_decryption, true}])), " +{ + \"Names\": [\"/root/parameter_1\", \"/root/secured_parameter\", \"some_invalid_param\"], + \"WithDecryption\": true +}" + }) + ], + + Response = " +{ + \"InvalidParameters\": [\"some_invalid_param\"], + \"Parameters\": [ + { + \"ARN\": \"arn:aws:ssm:us-west-2:000000000000:parameter/root/parameter_1\", + \"DataType\": \"text\", + \"LastModifiedDate\": 1612970194.057, + \"Name\": \"/root/parameter_1\", + \"Type\": \"String\", + \"Value\": \"testvalue\", + \"Version\": 1 + }, + { + \"ARN\": \"arn:aws:ssm:us-west-2:000000000000:parameter/root/secured_parameter\", + \"DataType\": \"text\", + \"LastModifiedDate\": 1612970194.31, + \"Name\": \"/root/secured_parameter\", + \"Type\": \"SecureString\", + \"Value\": \"MyP@ssw0rd\", + \"Version\": 1 + } + ] +}", + input_tests(Response, Tests). + + +get_parameters_output_tests(_) -> + Tests = + [?_ssm_test( + {"GetParameters example response", " +{ + \"InvalidParameters\": [\"some_invalid_param\"], + \"Parameters\": [ + { + \"ARN\": \"arn:aws:ssm:us-west-2:000000000000:parameter/root/parameter_1\", + \"DataType\": \"text\", + \"LastModifiedDate\": 1612970194.057, + \"Name\": \"/root/parameter_1\", + \"Type\": \"String\", + \"Value\": \"testvalue\", + \"Version\": 1 + }, + { + \"ARN\": \"arn:aws:ssm:us-west-2:000000000000:parameter/root/secured_parameter\", + \"DataType\": \"text\", + \"LastModifiedDate\": 1612970194.31, + \"Name\": \"/root/secured_parameter\", + \"Type\": \"SecureString\", + \"Value\": \"MyP@ssw0rd\", + \"Version\": 1 + } + ] +}", + {ok, #ssm_get_parameters{invalid_parameters = [<<"some_invalid_param">>], + parameters = [#ssm_parameter{arn = <<"arn:aws:ssm:us-west-2:000000000000:parameter/root/parameter_1">>, + data_type = <<"text">>, last_modified_date = 1612970194.057, + name = <<"/root/parameter_1">>, selector = undefined, + source_result = undefined, type = <<"String">>, + value = <<"testvalue">>, version = 1}, + #ssm_parameter{arn = <<"arn:aws:ssm:us-west-2:000000000000:parameter/root/secured_parameter">>, + data_type = <<"text">>, last_modified_date = 1612970194.31, + name = <<"/root/secured_parameter">>, selector = undefined, + source_result = undefined, type = <<"SecureString">>, + value = <<"MyP@ssw0rd">>, + version = 1}]}} + })], + output_tests(?_f(erlcloud_ssm:get_parameters([{names, ["/root/parameter_1", "/root/secured_parameter", "some_invalid_param"]}, + {with_decryption, true}, + {out, record}])), Tests). + + +%% GetParametersByPath test based on the API examples: +%% https://docs.aws.amazon.com/systems-manager/latest/APIReference/API_GetParametersByPath.html +get_parameters_by_path_input_tests(_) -> + Tests = + [?_ssm_test( + {"GetParametersByPath example request", + ?_f(erlcloud_ssm:get_parameters_by_path([{max_results, 2}, + {path, <<"/root">>}, + {recursive, true}, + {with_decryption, false}, + {out, record}])), " +{ + \"MaxResults\": 2, + \"Path\": \"/root\", + \"Recursive\": true, + \"WithDecryption\": false, +}" + }) + ], + + Response = " +{ + \"NextToken\": \"AAEAAe2HRCL1+nBkC3kuOVp0r3fpOpPOH+/c+hH5VIdx7vntLivo/iGhhR8yllmtqsCdoNiwS4EQP+QrRDX+T1NT9vN7X2Fj+/Gb/++F089cGZc6/+/OAaqe/==\", + \"Parameters\": [ + { + \"ARN\": \"arn:aws:ssm:us-west-2:000000000000:parameter/root/parameter_1\", + \"DataType\": \"text\", + \"LastModifiedDate\": 1612970194.057, + \"Name\": \"/root/parameter_1\", + \"Type\": \"String\", + \"Value\": \"testvalue\", + \"Version\": 1 + }, + { + \"ARN\": \"arn:aws:ssm:us-west-2:000000000000:parameter/root/secured_parameter\", + \"DataType\": \"text\", + \"LastModifiedDate\": 1612970194.31, + \"Name\": \"/root/secured_parameter\", + \"Type\": \"SecureString\", + \"Value\": \"6EzK+k13aK/VKMYk77pyiErjGpaoEDA==\", + \"Version\": 1 + } + ] +}", + input_tests(Response, Tests). + + +get_parameters_by_path_output_tests(_) -> + Tests = + [?_ssm_test( + {"GetParametersByPath example response", " +{ + \"NextToken\": \"AAEAAe2HRCL1+nBkC3kuOVp0r3fpOpPOH+/c+hH5VIdx7vntLivo/iGhhR8yllmtqsCdoNiwS4EQP+QrRDX+T1NT9vN7X2Fj+/Gb/++F089cGZc6/+/OAaqe/==\", + \"Parameters\": [ + { + \"ARN\": \"arn:aws:ssm:us-west-2:000000000000:parameter/root/parameter_1\", + \"DataType\": \"text\", + \"LastModifiedDate\": 1612970194.057, + \"Name\": \"/root/parameter_1\", + \"Type\": \"String\", + \"Value\": \"testvalue\", + \"Version\": 1 + }, + { + \"ARN\": \"arn:aws:ssm:us-west-2:000000000000:parameter/root/secured_parameter\", + \"DataType\": \"text\", + \"LastModifiedDate\": 1612970194.31, + \"Name\": \"/root/secured_parameter\", + \"Type\": \"SecureString\", + \"Value\": \"6EzK+k13aK/VKMYk77pyiErjGpaoEDA==\", + \"Version\": 1 + } + ] +}", + {ok, #ssm_get_parameters_by_path{next_token = <<"AAEAAe2HRCL1+nBkC3kuOVp0r3fpOpPOH+/c+hH5VIdx7vntLivo/iGhhR8yllmtqsCdoNiwS4EQP+QrRDX+T1NT9vN7X2Fj+/Gb/++F089cGZc6/+/OAaqe/==">>, + parameters = [#ssm_parameter{arn = <<"arn:aws:ssm:us-west-2:000000000000:parameter/root/parameter_1">>, + data_type = <<"text">>, last_modified_date = 1612970194.057, + name = <<"/root/parameter_1">>, selector = undefined, + source_result = undefined, type = <<"String">>, + value = <<"testvalue">>, version = 1}, + #ssm_parameter{arn = <<"arn:aws:ssm:us-west-2:000000000000:parameter/root/secured_parameter">>, + data_type = <<"text">>, last_modified_date = 1612970194.31, + name = <<"/root/secured_parameter">>, selector = undefined, + source_result = undefined, type = <<"SecureString">>, + value = <<"6EzK+k13aK/VKMYk77pyiErjGpaoEDA==">>, + version = 1}]}} + })], + output_tests(?_f(erlcloud_ssm:get_parameters_by_path([{max_results, 2}, + {path, <<"/root">>}, + {recursive, true}, + {with_decryption, false}, + {out, record}])), Tests). + + +%% PutParameter test based on the API examples: +%% https://docs.aws.amazon.com/systems-manager/latest/APIReference/API_PutParameter.html +put_parameter_input_tests(_) -> + Tests = + [?_ssm_test( + {"PutParameter example request", + ?_f(erlcloud_ssm:put_parameter([{name, <<"/root/parameter_1">>}, {value, <<"testvalue">>}, {type, <<"String">>}])), " +{ + \"Name\": \"/root/parameter_1\", + \"Type\": \"String\", + \"Value\": \"testvalue\" +}" + }) + ], + + Response = " +{ + \"Tier\": \"Standard\", + \"Version\": 1 +}", + input_tests(Response, Tests). + + +put_parameter_output_tests(_) -> + Tests = + [?_ssm_test( + {"PutParameter example response", " +{ + \"Tier\": \"Standard\", + \"Version\": 1 +}", + {ok, #ssm_put_parameter{tier = <<"Standard">>, + version = 1}} + })], + output_tests(?_f(erlcloud_ssm:put_parameter([{name, <<"/root/parameter_1">>}, {value, <<"testvalue">>}, {type, <<"String">>}, {out, record}])), Tests). + + +%% DeleteParameter test based on the API examples: +%% https://docs.aws.amazon.com/systems-manager/latest/APIReference/API_DeleteParameter.html +delete_parameter_input_tests(_) -> + Tests = + [?_ssm_test( + {"DeleteParameter example request", + ?_f(erlcloud_ssm:delete_parameter([{name, <<"/root/parameter_1">>}])), " +{ + \"Name\": \"/root/parameter_1\" +}" + }) + ], + Response = "{}", + input_tests(Response, Tests). + + +delete_parameter_output_tests(_) -> + Tests = + [?_ssm_test( + {"DeleteParameter example response", "{}", + ok + })], + output_tests(?_f(erlcloud_ssm:delete_parameter([{name, <<"/root/parameter_1">>}])), Tests). + +%%%=================================================================== +%%% Input test helpers +%%%=================================================================== + +-type expected_body() :: string(). + +sort_json([{_, _} | _] = Json) -> + %% Value is an object + SortedChildren = [{K, sort_json(V)} || {K,V} <- Json], + lists:keysort(1, SortedChildren); +sort_json([_|_] = Json) -> + %% Value is an array + [sort_json(I) || I <- Json]; +sort_json(V) -> + V. + +%% verifies that the parameters in the body match the expected parameters +-spec validate_body(binary(), expected_body()) -> ok. +validate_body(Body, Expected) -> + Want = sort_json(jsx:decode(list_to_binary(Expected), [{return_maps, false}])), + Actual = sort_json(jsx:decode(Body, [{return_maps, false}])), + case Want =:= Actual of + true -> ok; + false -> + ?debugFmt("~nEXPECTED~n~p~nACTUAL~n~p~n", [Want, Actual]) + end, + ?assertEqual(Want, Actual). + + +%% returns the mock of the erlcloud_httpc function input tests expect to be called. +%% Validates the request body and responds with the provided response. +-spec input_expect(string(), expected_body()) -> fun(). +input_expect(Response, Expected) -> + fun(_Url, post, _Headers, Body, _Timeout, _Config) -> + validate_body(Body, Expected), + {ok, {{200, "OK"}, [], list_to_binary(Response)}} + end. + + +%% input_test converts an input_test specifier into an eunit test generator +-type input_test_spec() :: {pos_integer(), {fun(), expected_body()} | {string(), fun(), expected_body()}}. +-spec input_test(string(), input_test_spec()) -> tuple(). +input_test(Response, {Line, {Description, Fun, Expected}}) + when is_list(Description) -> + {Description, + {Line, + fun() -> + meck:expect(erlcloud_httpc, request, input_expect(Response, Expected)), + erlcloud_ssm:configure(string:copies("A", 20), string:copies("a", 40)), + Fun() + end}}. + + +%% input_tests converts a list of input_test specifiers into an eunit test generator +-spec input_tests(string(), [input_test_spec()]) -> [tuple()]. +input_tests(Response, Tests) -> + [input_test(Response, Test) || Test <- Tests]. + +%%%=================================================================== +%%% Output test helpers +%%%=================================================================== + +%% returns the mock of the erlcloud_httpc function output tests expect to be called. +-spec output_expect(binary()) -> fun(). +output_expect(Response) -> + fun(_Url, post, _Headers, _Body, _Timeout, _Config) -> + {ok, {{200, "OK"}, [], Response}} + end. + +%% output_test converts an output_test specifier into an eunit test generator +-type output_test_spec() :: {pos_integer(), {string(), term()} | {string(), string(), term()}}. +-spec output_test(fun(), output_test_spec()) -> tuple(). +output_test(Fun, {Line, {Description, Response, Result}}) -> + {Description, + {Line, + fun() -> + meck:expect(erlcloud_httpc, request, output_expect(list_to_binary(Response))), + erlcloud_ssm:configure(string:copies("A", 20), string:copies("a", 40)), + Actual = Fun(), + case Result =:= Actual of + true -> ok; + false -> + ?debugFmt("~nEXPECTED~n~p~nACTUAL~n~p~n", [Result, Actual]) + end, + ?assertEqual(Result, Actual) + end}}. + + +%% output_tests converts a list of output_test specifiers into an eunit test generator +-spec output_tests(fun(), [output_test_spec()]) -> [term()]. +output_tests(Fun, Tests) -> + [output_test(Fun, Test) || Test <- Tests]. diff --git a/test/erlcloud_util_tests.erl b/test/erlcloud_util_tests.erl new file mode 100644 index 000000000..f7d4fdcac --- /dev/null +++ b/test/erlcloud_util_tests.erl @@ -0,0 +1,61 @@ +-module(erlcloud_util_tests). +-include_lib("eunit/include/eunit.hrl"). +-include("erlcloud.hrl"). + +request_test_() -> + {foreach, + fun start/0, + fun stop/1, + [fun test_proplists_to_map/1]}. + +start() -> + ok. + +stop(_) -> + ok. + +test_proplists_to_map(_) -> + Proplist = [{<<"Directories">>, + [[{<<"Alias">>,<<"d-93671bd1a1">>}, + {<<"DnsIpAddresses">>,[<<"172.16.1.31">>,<<"172.16.0.42">>]}, + {<<"SelfservicePermissions">>,[ + {<<"SwitchRunningMode">>,<<"ENABLED">>}]}, + {<<"State">>,<<"REGISTERED">>}, + {<<"SubnetIds">>, + [<<"subnet-02d62b601c121d0ca">>, + <<"subnet-08b608fe3a2ecc501">>]}, + {<<"Tenancy">>,<<"SHARED">>}, + {<<"WorkspaceAccessProperties">>, + [{<<"DeviceTypeAndroid">>,<<"ALLOW">>}, + {<<"DeviceTypeZeroClient">>,<<"ALLOW">>}]}, + {<<"WorkspaceCreationProperties">>, + [{<<"EnableInternetAccess">>,true}, + {<<"UserEnabledAsLocalAdministrator">>,true}]}, + {<<"WorkspaceSecurityGroupId">>, + <<"sg-064d3dbf40db978bd">>}]]}], + Expected = #{<<"Directories">> => + [#{<<"Alias">> => <<"d-93671bd1a1">>, + <<"DnsIpAddresses">> => + [<<"172.16.1.31">>,<<"172.16.0.42">>], + <<"SelfservicePermissions">> => + #{<<"SwitchRunningMode">> => <<"ENABLED">>}, + <<"State">> => <<"REGISTERED">>, + <<"SubnetIds">> => + [<<"subnet-02d62b601c121d0ca">>, + <<"subnet-08b608fe3a2ecc501">>], + <<"Tenancy">> => <<"SHARED">>, + <<"WorkspaceAccessProperties">> => + #{<<"DeviceTypeAndroid">> => <<"ALLOW">>, + <<"DeviceTypeZeroClient">> => <<"ALLOW">>}, + <<"WorkspaceCreationProperties">> => + #{<<"EnableInternetAccess">> => true, + <<"UserEnabledAsLocalAdministrator">> => true}, + <<"WorkspaceSecurityGroupId">> => + <<"sg-064d3dbf40db978bd">>}]}, + [?_assertEqual(Expected, erlcloud_util:proplists_to_map(Proplist)), + ?_assertEqual(#{}, erlcloud_util:proplists_to_map([{}])), + ?_assertEqual([], erlcloud_util:proplists_to_map([])), + ?_assertEqual([<<"a">>, <<"b">>], erlcloud_util:proplists_to_map([<<"a">>, <<"b">>])) + ]. + + diff --git a/test/erlcloud_waf_tests.erl b/test/erlcloud_waf_tests.erl index 533096566..daf6455fb 100644 --- a/test/erlcloud_waf_tests.erl +++ b/test/erlcloud_waf_tests.erl @@ -41,7 +41,7 @@ byte_match_tuple = #waf_byte_match_tuple{ field_to_match = #waf_field_to_match{type = query_string}, positional_constraint = contains, - target_string = "foobar", + target_string = <<"foobar">>, text_transformation = none}}). -define(UPDATE_IP_SET, @@ -593,7 +593,7 @@ update_xss_match_set_tests(_) -> %%% Input test helpers %%%=================================================================== --type expected_body() :: string(). +-type expected_body() :: binary(). sort_json([{_, _} | _] = Json) -> %% Value is an object @@ -608,8 +608,8 @@ sort_json(V) -> %% verifies that the parameters in the body match the expected parameters -spec validate_body(binary(), expected_body()) -> ok. validate_body(Body, Expected) -> - Want = sort_json(jsx:decode(Expected)), - Actual = sort_json(jsx:decode(Body)), + Want = sort_json(jsx:decode(Expected, [{return_maps, false}])), + Actual = sort_json(jsx:decode(Body, [{return_maps, false}])), case Want =:= Actual of true -> ok; false -> @@ -620,7 +620,7 @@ validate_body(Body, Expected) -> %% returns the mock of the erlcloud_httpc function input tests expect to be called. %% Validates the request body and responds with the provided response. --spec input_expect(string(), expected_body()) -> fun(). +-spec input_expect(binary(), expected_body()) -> fun(). input_expect(Response, Expected) -> fun(_Url, post, _Headers, Body, _Timeout, _Config) -> validate_body(Body, Expected), @@ -630,7 +630,7 @@ input_expect(Response, Expected) -> %% input_test converts an input_test specifier into an eunit test generator -type input_test_spec() :: {pos_integer(), {fun(), expected_body()} | {string(), fun(), expected_body()}}. --spec input_test(string(), input_test_spec()) -> tuple(). +-spec input_test(binary(), input_test_spec()) -> tuple(). input_test(Response, {Line, {Description, Fun, Expected}}) when is_list(Description) -> {Description, @@ -643,7 +643,7 @@ input_test(Response, {Line, {Description, Fun, Expected}}) %% input_tests converts a list of input_test specifiers into an eunit test generator --spec input_tests(string(), [input_test_spec()]) -> [tuple()]. +-spec input_tests(binary(), [input_test_spec()]) -> [tuple()]. input_tests(Response, Tests) -> [input_test(Response, Test) || Test <- Tests]. @@ -659,7 +659,7 @@ output_expect(Response) -> end. %% output_test converts an output_test specifier into an eunit test generator --type output_test_spec() :: {pos_integer(), {string(), term()} | {string(), string(), term()}}. +-type output_test_spec() :: {pos_integer(), {string(), term()} | {string(), binary(), term()}}. -spec output_test(fun(), output_test_spec()) -> tuple(). output_test(Fun, {Line, {Description, Response, Result}}) -> {Description, diff --git a/test/erlcloud_workspaces_tests.erl b/test/erlcloud_workspaces_tests.erl new file mode 100644 index 000000000..bc850069c --- /dev/null +++ b/test/erlcloud_workspaces_tests.erl @@ -0,0 +1,449 @@ +%% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- +-module(erlcloud_workspaces_tests). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("include/erlcloud_workspaces.hrl"). + +%% Unit tests for erlcloud_workspaces. +%% These tests work by using meck to mock erlcloud_httpc. There are two classes of test: input and output. +%% +%% Input tests verify that different function args produce the desired JSON request. +%% An input test list provides a list of funs and the JSON that is expected to result. +%% +%% Output tests verify that the http response produces the correct return from the fun. +%% An output test lists provides a list of response bodies and the expected return. + +%% The _workspaces_test macro provides line number annotation to a test, similar to _test, but doesn't wrap in a fun +-define(_workspaces_test(T), {?LINE, T}). +%% The _f macro is a terse way to wrap code in a fun. Similar to _test but doesn't annotate with a line number +-define(_f(F), fun() -> F end). + +-export([validate_body/2]). + +%%%=================================================================== +%%% Test entry points +%%%=================================================================== +operation_test_() -> + {foreach, + fun start/0, + fun stop/1, + [ + fun describe_tags_input_tests/1, + fun describe_tags_output_tests/1, + fun describe_workspaces_input_tests/1, + fun describe_workspaces_output_tests/1, + fun describe_workspace_directories_input_tests/1, + fun describe_workspace_directories_output_tests/1 + ] + }. + +start() -> + meck:new(erlcloud_httpc), + ok. + + +stop(_) -> + meck:unload(erlcloud_httpc). + +%%%=================================================================== +%%% Actual test specifiers +%%%=================================================================== + +%% DescribeTags test based on the API examples: +%% https://docs.aws.amazon.com/workspaces/latest/api/API_DescribeTags.html +describe_tags_input_tests(_) -> + Tests = + [?_workspaces_test( + {"DescribeTags example request", + ?_f(erlcloud_workspaces:describe_tags([{resource_id, "ws-c8wvb67p1"}])), " +{ + \"ResourceId\": \"ws-c8wvb67p1\" +}" + }) + ], + + Response = " +{ + \"TagList\": [ + { + \"Key\": \"testkey\", + \"Value\": \"testvalue\" + } + ] +}", + input_tests(Response, Tests). + + +describe_tags_output_tests(_) -> + Tests = + [?_workspaces_test( + {"DescribeTags example response", " +{ + \"TagList\": [ + { + \"Key\": \"testkey\", + \"Value\": \"testvalue\" + } + ] +}", + {ok,[#workspaces_tag{ + key = <<"testkey">>, + value = <<"testvalue">> + }]} + })], + output_tests(?_f(erlcloud_workspaces:describe_tags([{out, record}])), Tests). + +%% DescribeWorkspaces test based on the API examples: +%% https://docs.aws.amazon.com/workspaces/latest/api/API_DescribeWorkspaces.html +describe_workspaces_input_tests(_) -> + Tests = + [?_workspaces_test( + {"DescribeWorkspaces example request", + ?_f(erlcloud_workspaces:describe_workspaces([{bundle_id, "wsb-clj85qzj2"}, + {directory_id, "d-93671bd1a2"}, + {limit, 5}, + {next_token, "next-page"}, + {user_name, <<"root">>}, + {workspace_ids, [<<"ws-c8wvb67p1">>, <<"ws-c8wvb67p2">>]}])), " +{ + \"BundleId\": \"wsb-clj85qzj2\", + \"DirectoryId\": \"d-93671bd1a2\", + \"Limit\": 5, + \"NextToken\": \"next-page\", + \"UserName\": \"root\", + \"WorkspaceIds\": [\"ws-c8wvb67p1\", \"ws-c8wvb67p2\"] + +}" + }) + ], + + Response = " +{ + \"Workspaces\": [{ + \"BundleId\": \"wsb-clj85qzj2\", + \"ComputerName\": \"A-3AK2EEJBJC1MA\", + \"DirectoryId\": \"d-93671bd1a2\", + \"IpAddress\": \"172.16.0.121\", + \"ModificationStates\": [], + \"State\": \"STOPPED\", + \"SubnetId\": \"subnet-08b608fe3a2ecc504\", + \"UserName\": \"root1\", + \"UserRealm\": \"corp.amazonworkspaces.com\", + \"WorkspaceId\": \"ws-c8wvb67pa\", + \"WorkspaceProperties\": { + \"ComputeTypeName\": \"STANDARD\", + \"RecycleMode\": \"DISABLED\", + \"RootVolumeSizeGib\": 80, + \"RunningMode\": \"AUTO_STOP\", + \"RunningModeAutoStopTimeoutInMinutes\": 60, + \"UserVolumeSizeGib\":50 + } + }] +}", + input_tests(Response, Tests). + + +describe_workspaces_output_tests(_) -> + Tests = + [?_workspaces_test( + {"DescribeWorkspaces example response", " +{ + \"Workspaces\": [{ + \"BundleId\": \"wsb-clj85qzj2\", + \"ComputerName\": \"A-3AK2EEJBJC1MA\", + \"DirectoryId\": \"d-93671bd1a2\", + \"IpAddress\": \"172.16.0.121\", + \"ModificationStates\": [], + \"State\": \"STOPPED\", + \"SubnetId\": \"subnet-08b608fe3a2ecc504\", + \"UserName\": \"root\", + \"UserRealm\": \"corp.amazonworkspaces.com\", + \"WorkspaceId\": \"ws-c8wvb67pa\", + \"WorkspaceProperties\": { + \"ComputeTypeName\": \"STANDARD\", + \"RecycleMode\": \"DISABLED\", + \"RootVolumeSizeGib\": 80, + \"RunningMode\": \"AUTO_STOP\", + \"RunningModeAutoStopTimeoutInMinutes\": 60, + \"UserVolumeSizeGib\":50 + } + }] +}", + {ok,#describe_workspaces{next_token = undefined, + workspaces = [ + #workspace{ + bundle_id = <<"wsb-clj85qzj2">>, + computer_name = <<"A-3AK2EEJBJC1MA">>, + directory_id = <<"d-93671bd1a2">>,error_code = undefined, + error_message = undefined,ip_address = <<"172.16.0.121">>, + modification_states = [], + root_volume_encryption_enabled = undefined, + state = <<"STOPPED">>, + subnet_id = <<"subnet-08b608fe3a2ecc504">>, + user_name = <<"root">>, + user_volume_encryption_enabled = undefined, + volume_encryption_key = undefined, + workspace_id = <<"ws-c8wvb67pa">>, + workspace_properties = #workspace_properties{ + computer_type_name = <<"STANDARD">>, + root_volume_size_gib = 80,running_mode = <<"AUTO_STOP">>, + running_mode_auto_stop_timeout_in_minutes = 60, + user_volume_size_gib = 50}}]}} + })], + output_tests(?_f(erlcloud_workspaces:describe_workspaces([{out, record}])), Tests). + +%% DescribeWorkspaceDirectories test based on the API examples: +%% https://docs.aws.amazon.com/workspaces/latest/api/API_DescribeWorkspaceDirectories.html +describe_workspace_directories_input_tests(_) -> + Tests = + [?_workspaces_test( + {"DescribeWorkspaceDirectories example request", + ?_f(erlcloud_workspaces:describe_workspace_directories([ + {directory_ids, ["d-93671bd1a1", "d-93671bd1a2"]}, + {limit, 5}, + {next_token, "next-page"}])), " +{ + \"DirectoryIds\": [\"d-93671bd1a1\", \"d-93671bd1a2\"], + \"Limit\": 5, + \"NextToken\": \"next-page\" + +}" + }) + ], + + Response = " +{ + \"Directories\": [ + { + \"DirectoryId\": \"d-93671bd1a1\", + \"Alias\": \"d-93671bd1a1\", + \"DirectoryName\": \"corp.amazonworkspaces.com\", + \"RegistrationCode\": \"wsdub+SATMNS\", + \"SubnetIds\": [ + \"subnet-02d62b601c121d0cd\", + \"subnet-08b608fe3a2ecc505\" + ], + \"DnsIpAddresses\": [ + \"172.16.1.36\", + \"172.16.0.49\" + ], + \"CustomerUserName\": \"Administrator\", + \"IamRoleId\": \"arn:aws:iam::352283894008:role/workspaces_DefaultRole\", + \"DirectoryType\": \"SIMPLE_AD\", + \"WorkspaceSecurityGroupId\": \"sg-064d3dbf40db978bd\", + \"State\": \"REGISTERED\", + \"WorkspaceCreationProperties\": { + \"EnableWorkDocs\": true, + \"EnableInternetAccess\": true, + \"UserEnabledAsLocalAdministrator\": true, + \"EnableMaintenanceMode\": true + }, + \"WorkspaceAccessProperties\": { + \"DeviceTypeWindows\": \"ALLOW\", + \"DeviceTypeOsx\": \"ALLOW\", + \"DeviceTypeWeb\": \"DENY\", + \"DeviceTypeIos\": \"ALLOW\", + \"DeviceTypeAndroid\": \"ALLOW\", + \"DeviceTypeChromeOs\": \"ALLOW\", + \"DeviceTypeZeroClient\": \"ALLOW\" + }, + \"Tenancy\": \"SHARED\", + \"SelfservicePermissions\": { + \"RestartWorkspace\": \"ENABLED\", + \"IncreaseVolumeSize\": \"ENABLED\", + \"ChangeComputeType\": \"ENABLED\", + \"SwitchRunningMode\": \"ENABLED\", + \"RebuildWorkspace\": \"ENABLED\" + } + } + ] +}", + input_tests(Response, Tests). + + +describe_workspace_directories_output_tests(_) -> + Tests = + [?_workspaces_test( + {"DescribeWorkspaceDirectories example response", " +{ + \"Directories\": [ + { + \"DirectoryId\": \"d-93671bd1a1\", + \"Alias\": \"d-93671bd1a1\", + \"DirectoryName\": \"corp.amazonworkspaces.com\", + \"RegistrationCode\": \"wsdub+SATMNS\", + \"SubnetIds\": [ + \"subnet-02d62b601c121d0cd\", + \"subnet-08b608fe3a2ecc505\" + ], + \"DnsIpAddresses\": [ + \"172.16.1.36\", + \"172.16.0.49\" + ], + \"CustomerUserName\": \"Administrator\", + \"IamRoleId\": \"arn:aws:iam::352283111111:role/workspaces_DefaultRole\", + \"DirectoryType\": \"SIMPLE_AD\", + \"WorkspaceSecurityGroupId\": \"sg-064d3dbf40db978bd\", + \"State\": \"REGISTERED\", + \"WorkspaceCreationProperties\": { + \"EnableWorkDocs\": true, + \"EnableInternetAccess\": true, + \"UserEnabledAsLocalAdministrator\": true, + \"EnableMaintenanceMode\": true + }, + \"WorkspaceAccessProperties\": { + \"DeviceTypeWindows\": \"ALLOW\", + \"DeviceTypeOsx\": \"ALLOW\", + \"DeviceTypeWeb\": \"DENY\", + \"DeviceTypeIos\": \"ALLOW\", + \"DeviceTypeAndroid\": \"ALLOW\", + \"DeviceTypeChromeOs\": \"ALLOW\", + \"DeviceTypeZeroClient\": \"ALLOW\" + }, + \"Tenancy\": \"SHARED\", + \"SelfservicePermissions\": { + \"RestartWorkspace\": \"ENABLED\", + \"IncreaseVolumeSize\": \"ENABLED\", + \"ChangeComputeType\": \"ENABLED\", + \"SwitchRunningMode\": \"ENABLED\", + \"RebuildWorkspace\": \"ENABLED\" + } + } + ] +}", + {ok,#describe_workspace_directories{ + next_token = undefined, + workspace_directories = + [#workspace_directory{ + alias = <<"d-93671bd1a1">>, + customer_user_name = <<"Administrator">>, + directory_id = <<"d-93671bd1a1">>, + directory_name = <<"corp.amazonworkspaces.com">>, + directory_type = <<"SIMPLE_AD">>, + dns_ip_address = [<<"172.16.1.36">>,<<"172.16.0.49">>], + iam_role_id = + <<"arn:aws:iam::352283111111:role/workspaces_DefaultRole">>, + ip_group_ids = undefined, + registration_code = <<"wsdub+SATMNS">>, + selfservice_permissions = + #workspaces_selfservice_permissions{ + change_compute_type = <<"ENABLED">>, + increase_volume_size = <<"ENABLED">>, + rebuild_workspace = <<"ENABLED">>, + restart_workspace = <<"ENABLED">>, + switch_running_mode = <<"ENABLED">>}, + state = <<"REGISTERED">>, + subnet_ids = + [<<"subnet-02d62b601c121d0cd">>, + <<"subnet-08b608fe3a2ecc505">>], + tenancy = <<"SHARED">>, + workspace_access_properties = + #workspace_access_properties{ + device_type_android = <<"ALLOW">>, + device_type_chrome_os = <<"ALLOW">>, + device_type_ios = <<"ALLOW">>,device_type_osx = <<"ALLOW">>, + device_type_web = <<"DENY">>, + device_type_windows = <<"ALLOW">>, + device_type_zero_client = <<"ALLOW">>}, + workspace_creation_properties = + #workspace_creation_properties{ + custom_security_group_id = undefined,default_ou = undefined, + enable_internet_access = true, + enable_maintenance_mode = true,enable_work_docs = true, + user_enabled_as_local_administrator = true}, + workspace_security_group_id = <<"sg-064d3dbf40db978bd">>}]}} + })], + output_tests(?_f(erlcloud_workspaces:describe_workspace_directories([{out, record}])), Tests). + + +%%%=================================================================== +%%% Input test helpers +%%%=================================================================== + +-type expected_body() :: string(). + +sort_json([{_, _} | _] = Json) -> + %% Value is an object + SortedChildren = [{K, sort_json(V)} || {K,V} <- Json], + lists:keysort(1, SortedChildren); +sort_json([_|_] = Json) -> + %% Value is an array + [sort_json(I) || I <- Json]; +sort_json(V) -> + V. + +%% verifies that the parameters in the body match the expected parameters +-spec validate_body(binary(), expected_body()) -> ok. +validate_body(Body, Expected) -> + Want = sort_json(jsx:decode(list_to_binary(Expected), [{return_maps, false}])), + Actual = sort_json(jsx:decode(Body, [{return_maps, false}])), + case Want =:= Actual of + true -> ok; + false -> + ?debugFmt("~nEXPECTED~n~p~nACTUAL~n~p~n", [Want, Actual]) + end, + ?assertEqual(Want, Actual). + + +%% returns the mock of the erlcloud_httpc function input tests expect to be called. +%% Validates the request body and responds with the provided response. +-spec input_expect(string(), expected_body()) -> fun(). +input_expect(Response, Expected) -> + fun(_Url, post, _Headers, Body, _Timeout, _Config) -> + validate_body(Body, Expected), + {ok, {{200, "OK"}, [], list_to_binary(Response)}} + end. + + +%% input_test converts an input_test specifier into an eunit test generator +-type input_test_spec() :: {pos_integer(), {fun(), expected_body()} | {string(), fun(), expected_body()}}. +-spec input_test(string(), input_test_spec()) -> tuple(). +input_test(Response, {Line, {Description, Fun, Expected}}) + when is_list(Description) -> + {Description, + {Line, + fun() -> + meck:expect(erlcloud_httpc, request, input_expect(Response, Expected)), + erlcloud_workspaces:configure(string:copies("A", 20), string:copies("a", 40)), + Fun() + end}}. + + +%% input_tests converts a list of input_test specifiers into an eunit test generator +-spec input_tests(string(), [input_test_spec()]) -> [tuple()]. +input_tests(Response, Tests) -> + [input_test(Response, Test) || Test <- Tests]. + +%%%=================================================================== +%%% Output test helpers +%%%=================================================================== + +%% returns the mock of the erlcloud_httpc function output tests expect to be called. +-spec output_expect(binary()) -> fun(). +output_expect(Response) -> + fun(_Url, post, _Headers, _Body, _Timeout, _Config) -> + {ok, {{200, "OK"}, [], Response}} + end. + +%% output_test converts an output_test specifier into an eunit test generator +-type output_test_spec() :: {pos_integer(), {string(), term()} | {string(), string(), term()}}. +-spec output_test(fun(), output_test_spec()) -> tuple(). +output_test(Fun, {Line, {Description, Response, Result}}) -> + {Description, + {Line, + fun() -> + meck:expect(erlcloud_httpc, request, output_expect(list_to_binary(Response))), + erlcloud_workspaces:configure(string:copies("A", 20), string:copies("a", 40)), + Actual = Fun(), + case Result =:= Actual of + true -> ok; + false -> + ?debugFmt("~nEXPECTED~n~p~nACTUAL~n~p~n", [Result, Actual]) + end, + ?assertEqual(Result, Actual) + end}}. + + +%% output_tests converts a list of output_test specifiers into an eunit test generator +-spec output_tests(fun(), [output_test_spec()]) -> [term()]. +output_tests(Fun, Tests) -> + [output_test(Fun, Test) || Test <- Tests].