diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c86a24b --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ +vendor +test/examples/hello-world-docker/pkg +*.iml +.DS_Store \ No newline at end of file diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..edd42a9 --- /dev/null +++ b/Gemfile @@ -0,0 +1,10 @@ +# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +# frozen_string_literal: true + +source 'https://rubygems.org' + +git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } + +# Specify your gem's dependencies in aws_lambda_ric.gemspec +gemspec diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..a0fa45c --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,22 @@ +PATH + remote: . + specs: + aws_lambda_ric (1.0.0) + +GEM + remote: https://rubygems.org/ + specs: + minitest (5.14.2) + rake (10.5.0) + +PLATFORMS + ruby + +DEPENDENCIES + aws_lambda_ric! + bundler (>= 1.17) + minitest (~> 5.0) + rake (~> 10.0) + +BUNDLED WITH + 2.2.0.rc.2 diff --git a/LICENSE b/LICENSE index 67db858..e11442c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,175 +1,229 @@ +Apache License - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ +Version 2.0, January 2004 - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +http://www.apache.org/licenses/ TERMS +AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. + - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. + "License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + + + + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + + + + "Legal Entity" shall mean the +union of the acting entity and all other entities that control, are controlled +by, or are under common control with that entity. For the purposes of this +definition, "control" means (i) the power, direct or indirect, to cause the +direction or management of such entity, whether by contract or otherwise, or (ii) +ownership of fifty percent (50%) or more of the outstanding shares, or (iii) +beneficial ownership of such entity. + + + + "You" (or "Your") shall mean +an individual or Legal Entity exercising permissions granted by this License. + + - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: +including but not limited to software source code, documentation source, and +configuration files. + + + + "Object" form shall mean any form resulting +from mechanical transformation or translation of a Source form, including but not +limited to compiled object code, generated documentation, and conversions to +other media types. + + + + "Work" shall mean the work of authorship, +whether in Source or Object form, made available under the License, as indicated +by a copyright notice that is included in or attached to the work (an example is +provided in the Appendix below). + + + + "Derivative Works" shall mean any +work, whether in Source or Object form, that is based on (or derived from) the +Work and for which the editorial revisions, annotations, elaborations, or other +modifications represent, as a whole, an original work of authorship. For the +purposes of this License, Derivative Works shall not include works that remain +separable from, or merely link (or bind by name) to the interfaces of, the Work +and Derivative Works thereof. + + + + "Contribution" shall mean any work +of authorship, including the original version of the Work and any modifications +or additions to that Work or Derivative Works thereof, that is intentionally +submitted to Licensor for inclusion in the Work by the copyright owner or by an +individual or Legal Entity authorized to submit on behalf of the copyright owner. +For the purposes of this definition, "submitted" means any form of electronic, +verbal, or written communication sent to the Licensor or its representatives, +including but not limited to communication on electronic mailing lists, source +code control systems, and issue tracking systems that are managed by, or on +behalf of, the Licensor for the purpose of discussing and improving the Work, but +excluding communication that is conspicuously marked or otherwise designated in +writing by the copyright owner as "Not a Contribution." + + + + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of +whom a Contribution has been received by Licensor and subsequently incorporated +within the Work. + + 2. Grant of Copyright License. Subject to the terms and +conditions of this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license +to reproduce, prepare Derivative Works of, publicly display, publicly perform, +sublicense, and distribute the Work and such Derivative Works in Source or Object +form. + + 3. Grant of Patent License. Subject to the terms and conditions of this +License, each Contributor hereby grants to You a perpetual, worldwide, +non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this +section) patent license to make, have made, use, offer to sell, sell, import, and +otherwise transfer the Work, where such license applies only to those patent +claims licensable by such Contributor that are necessarily infringed by their +Contribution(s) alone or by combination of their Contribution(s) with the Work to +which such Contribution(s) was submitted. If You institute patent litigation +against any entity (including a cross-claim or counterclaim in a lawsuit) +alleging that the Work or a Contribution incorporated within the Work constitutes +direct or contributory patent infringement, then any patent licenses granted to +You under this License for that Work shall terminate as of the date such +litigation is filed. + + 4. Redistribution. You may reproduce and distribute +copies of the Work or Derivative Works thereof in any medium, with or without +modifications, and in Source or Object form, provided that You meet the following +conditions: (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. +Derivative Works a copy of this License; and + + (b) You must cause any +modified files to carry prominent notices stating that You changed the files; +and + + (c) You must retain, in the Source form of any Derivative Works that +You distribute, all copyright, patent, trademark, and attribution notices from +the Source form of the Work, excluding those notices that do not pertain to any +part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text +file as part of its distribution, then any Derivative Works that You distribute +must include a readable copy of the attribution notices contained within such +NOTICE file, excluding those notices that do not pertain to any part of the +Derivative Works, in at least one of the following places: within a NOTICE text +file distributed as part of the Derivative Works; within the Source form or +documentation, if provided along with the Derivative Works; or, within a display +generated by the Derivative Works, if and wherever such third-party notices +normally appear. The contents of the NOTICE file are for informational purposes +only and do not modify the License. You may add Your own attribution notices +within Derivative Works that You distribute, alongside or as an addendum to the +NOTICE text from the Work, provided that such additional attribution notices +cannot be construed as modifying the License. + + You may add Your own +copyright statement to Your modifications and may provide additional or different +license terms and conditions for use, reproduction, or distribution of Your +modifications, or for any such Derivative Works as a whole, provided Your use, +reproduction, and distribution of the Work otherwise complies with the conditions +stated in this License. + + 5. Submission of Contributions. Unless You explicitly +state otherwise, any Contribution intentionally submitted for inclusion in the +Work by You to the Licensor shall be under the terms and conditions of this +License, without any additional terms or conditions. Notwithstanding the above, +nothing herein shall supersede or modify the terms of any separate license +agreement you may have executed with Licensor regarding such Contributions. + + +6. Trademarks. This License does not grant permission to use the trade names, +trademarks, service marks, or product names of the Licensor, except as required +for reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless +required by applicable law or agreed to in writing, Licensor provides the Work +(and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, +without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, +MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible +for determining the appropriateness of using or redistributing the Work and +assume any risks associated with Your exercise of permissions under this +License. 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. +whether in tort (including negligence), contract, or otherwise, unless required +by applicable law (such as deliberate and grossly negligent acts) or agreed to in +writing, shall any Contributor be liable to You for damages, including any +direct, indirect, special, incidental, or consequential damages of any character +arising as a result of this License or out of the use or inability to use the +Work (including but not limited to damages for loss of goodwill, work stoppage, +computer failure or malfunction, or any and all other commercial damages or +losses), even if such Contributor has been advised of the possibility of such +damages. 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. +the Work or Derivative Works thereof, You may choose to offer, and charge a fee +for, acceptance of support, warranty, indemnity, or other liability obligations +and/or rights consistent with this License. However, in accepting such +obligations, You may act only on Your own behalf and on Your sole responsibility, +not on behalf of any other Contributor, and only if You agree to indemnify, +defend, and hold each Contributor harmless for any liability incurred by, or +claims asserted against, such Contributor by reason of your accepting any such +warranty or additional liability. END OF TERMS AND CONDITIONS + +APPENDIX: How to +apply the Apache License to your work. + +To apply the Apache License to your work, +attach the following boilerplate notice, with the fields enclosed by brackets +"[]" replaced with your own identifying information. (Don't include the +brackets!) The text should be enclosed in the appropriate comment syntax for the +file format. We also recommend that a file or class name and description of +purpose be included on the same "printed page" as the copyright notice for easier +identification within third-party archives. + +Copyright [yyyy] Edgar +Hernandez + +Licensed under the Apache License, Version 2.0 (the "License"); + +you +may not use this file except in compliance with the License. + +You may obtain a +copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless +required by applicable law or agreed to in writing, software + +distributed under +the License is distributed on an "AS IS" BASIS, + +WITHOUT WARRANTIES OR CONDITIONS +OF ANY KIND, either express or implied. + +See the License for the specific +language governing permissions and + +limitations under the License. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a317ad3 --- /dev/null +++ b/Makefile @@ -0,0 +1,43 @@ +.PHONY: target +target: + $(info ${HELP_MESSAGE}) + @exit 0 + +.PHONY: init +init: + bundle install + +.PHONY: setup-codebuild-agent +setup-codebuild-agent: + docker build -t codebuild-agent - < test/integration/codebuild-local/Dockerfile.agent + +.PHONY: test-smoke +test-smoke: setup-codebuild-agent + CODEBUILD_IMAGE_TAG=codebuild-agent test/integration/codebuild-local/test_one.sh test/integration/codebuild/buildspec.os.alpine.yml alpine 3.12 2.7 + +.PHONY: test-unit +test-unit: + ruby test/run_tests.rb unit + +.PHONY: test-integ +test-integ: setup-codebuild-agent + CODEBUILD_IMAGE_TAG=codebuild-agent test/integration/codebuild-local/test_all.sh test/integration/codebuild + +.PHONY: build +build: + rake build + +define HELP_MESSAGE + +Usage: $ make [TARGETS] + +TARGETS + + build Builds the package. + clean Cleans the working directory by removing built artifacts. + init Initialize and install the dependencies and dev-dependencies for this project. + test-integ Run Integration tests. + test-unit Run Unit Tests. + test-smoke Run Sanity/Smoke tests. + +endef diff --git a/README.md b/README.md index 847260c..0bf7a61 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,147 @@ -## My Project +## AWS Lambda Ruby Runtime Interface Client -TODO: Fill this README out! +We have open-sourced a set of software packages, Runtime Interface Clients (RIC), that implement the Lambda + [Runtime API](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html), allowing you to seamlessly extend your preferred + base images to be Lambda compatible. +The Lambda Runtime Interface Client is a lightweight interface that allows your runtime to receive requests from and send requests to the Lambda service. -Be sure to: +The Lambda Ruby Runtime Interface Client is vended through [rubygems](https://rubygems.org/gems/aws_lambda_ric). +You can include this package in your preferred base image to make that base image Lambda compatible. -* Change the title in this README -* Edit your repository description on GitHub +## Requirements +The Ruby Runtime Interface Client package currently supports Ruby versions: + - 2.5.x up to and including 2.7.x + +## Usage + +### Creating a Docker Image for Lambda with the Runtime Interface Client +First step is to choose the base image to be used. The supported Linux OS distributions are: + + - Amazon Linux 2 + - Alpine + - CentOS + - Debian + - Ubuntu + +In order to install the Runtime Interface Client, either add this line to your application's Gemfile: + +```ruby +gem 'aws_lambda_ric' +``` + +And then execute: + + $ bundle + +Or install it manually as: + + $ gem install aws_lambda_ric + + +Next step would be to copy your Lambda function code into the image's working directory. +```dockerfile +# Copy function code +RUN mkdir -p ${FUNCTION_DIR} +COPY app.rb ${FUNCTION_DIR} + +WORKDIR ${FUNCTION_DIR} +``` + +The next step would be to set the `ENTRYPOINT` property of the Docker image to invoke the Runtime Interface Client and then set the `CMD` argument to specify the desired handler. + +Example Dockerfile: +```dockerfile +FROM amazonlinux:latest + +# Define custom function directory +ARG FUNCTION_DIR="/function" + +# Install ruby +RUN amazon-linux-extras install -y ruby2.6 + +# Install the Runtime Interface Client +RUN gem install aws_lambda_ric + +# Copy function code +RUN mkdir -p ${FUNCTION_DIR} +COPY app.rb ${FUNCTION_DIR} + +WORKDIR ${FUNCTION_DIR} + +ENTRYPOINT ["aws_lambda_ric"] +CMD ["app.App::Handler.process"] +``` + +Example Ruby handler `app.rb`: +```ruby +module App + class Handler + def self.process(event:, context:) + "Hello World!" + end + end +end +``` + +### Local Testing + +To make it easy to locally test Lambda functions packaged as container images we open-sourced a lightweight web-server, Lambda Runtime Interface Emulator (RIE), which allows your function packaged as a container image to accept HTTP requests. You can install the [AWS Lambda Runtime Interface Emulator](https://github.com/aws/aws-lambda-runtime-interface-emulator) on your local machine to test your function. Then when you run the image function, you set the entrypoint to be the emulator. + +*To install the emulator and test your Lambda function* + +1) From your project directory, run the following command to download the RIE from GitHub and install it on your local machine. + +```shell script +mkdir -p ~/.aws-lambda-rie && \ + curl -Lo ~/.aws-lambda-rie/aws-lambda-rie https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie && \ + chmod +x ~/.aws-lambda-rie/aws-lambda-rie +``` +2) Run your Lambda image function using the docker run command. + +```shell script +docker run -d -v ~/.aws-lambda-rie:/aws-lambda -p 9000:8080 \ + --entrypoint /aws-lambda/aws-lambda-rie \ + myfunction:latest \ + aws_lambda_ric app.App::Handler.process +``` + +This runs the image as a container and starts up an endpoint locally at `http://localhost:9000/2015-03-31/functions/function/invocations`. + +3) Post an event to the following endpoint using a curl command: + +```shell script +curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}'. +``` + +This command invokes the function running in the container image and returns a response. + +*Alternately, you can also include RIE as a part of your base image. See the AWS documentation on how to [Build RIE into your base image](https://docs.aws.amazon.com/lambda/latest/dg/images-test.html#images-test-alternative).* + +## Development + +### Building the package +Clone this repository and run: + +```shell script +make init +make build +``` + +### Running tests + +Make sure the project is built: +```shell script +make init build +``` +Then, +* to run unit tests: `make test` +* to run integration tests: `make test-integ` +* to run smoke tests: `make test-smoke` ## Security -See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. +If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. ## License -This project is licensed under the Apache-2.0 License. - +This project is licensed under the Apache-2.0 License. \ No newline at end of file diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..4caa98e --- /dev/null +++ b/Rakefile @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require 'bundler/gem_tasks' +require 'rake/testtask' + +Rake::TestTask.new(:test) do |t| + t.libs << 'test' + t.libs << 'lib' + t.test_files = FileList['test/**/*_test.rb'] +end + +task default: :test diff --git a/aws_lambda_ric.gemspec b/aws_lambda_ric.gemspec new file mode 100644 index 0000000..4b43dd3 --- /dev/null +++ b/aws_lambda_ric.gemspec @@ -0,0 +1,37 @@ +# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +# frozen_string_literal: true + +lib = File.expand_path('lib', __dir__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require './lib/aws_lambda_ric/version' + +Gem::Specification.new do |spec| + spec.name = 'aws_lambda_ric' + spec.version = AwsLambdaRuntimeInterfaceClient::VERSION + spec.authors = ['AWS Lambda'] + + spec.summary = 'AWS Lambda Runtime Interface Client for Ruby' + spec.description = 'The AWS Lambda Ruby Runtime Interface Client implements the Lambda programming model for Ruby.' + spec.homepage = 'https://github.com/aws/aws-lambda-ruby-runtime-interface-client' + + # Specify which files should be added to the gem when it is released. + spec.files = %w[ + LICENSE + README.md + Gemfile + NOTICE + Gemfile.lock + aws_lambda_ric.gemspec + bin/aws_lambda_ric + ] + Dir['lib/**/*'] + + spec.bindir = 'bin' + # all application-style files are expected to be found in bindir + spec.executables = 'aws_lambda_ric' + spec.require_paths = ['lib'] + + spec.add_development_dependency 'bundler', '>= 2.0' + spec.add_development_dependency 'minitest', '~> 5.0' + spec.add_development_dependency 'rake', '~> 10.0' +end diff --git a/bin/aws_lambda_ric b/bin/aws_lambda_ric new file mode 100755 index 0000000..e2e5e65 --- /dev/null +++ b/bin/aws_lambda_ric @@ -0,0 +1,10 @@ +#! /usr/bin/env ruby + +# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +# frozen_string_literal: true + +require 'bundler/setup' +require_relative '../lib/aws_lambda_ric/bootstrap' + +Bootstrap.start diff --git a/lib/aws_lambda_ric.rb b/lib/aws_lambda_ric.rb new file mode 100755 index 0000000..0ec0978 --- /dev/null +++ b/lib/aws_lambda_ric.rb @@ -0,0 +1,194 @@ +# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +# frozen_string_literal: true + +require_relative 'aws_lambda_ric/lambda_errors' +require_relative 'aws_lambda_ric/lambda_server' +require_relative 'aws_lambda_ric/lambda_handler' +require_relative 'aws_lambda_ric/lambda_context' +require_relative 'aws_lambda_ric/lambda_logger' +require_relative 'aws_lambda_ric/lambda_log_formatter' +require_relative 'aws_lambda_ric/telemetry_log_sink' +require_relative 'aws_lambda_ric/aws_lambda_marshaller' +require_relative 'aws_lambda_ric/xray_cause' +require_relative 'aws_lambda_ric/version' +require 'logger' + +$stdout.sync = true # Ensures that logs are flushed promptly. + +module AwsLambdaRuntimeInterfaceClient + + class Error < StandardError; end + + # Loads the user code and runs it upon invocation + class LambdaRunner + + ENV_VAR_RUNTIME_API = 'AWS_LAMBDA_RUNTIME_API' + + def initialize(runtime_server_addr) + @lambda_server = LambdaServer.new(runtime_server_addr) + @runtime_loop_active = true # if false, we will exit the program + @exit_code = 0 + end + + def run(app_root, handler) + + $LOAD_PATH.unshift(app_root) unless $LOAD_PATH.include?(app_root) + + begin + @lambda_handler = LambdaHandler.new(env_handler: handler) + require @lambda_handler.handler_file_name + start_runtime_loop + rescue Exception => e # which includes LoadError or any exception within static user code + @runtime_loop_active = false + @exit_code = -4 + send_init_error_to_server(e) + ensure + TelemetryLoggingHelper.close + end + + exit(@exit_code) + end + + private + + def start_runtime_loop + while @runtime_loop_active + lambda_invocation_request = wait_for_invocation + run_user_code(lambda_invocation_request) + end + end + + def wait_for_invocation + request_id, raw_request = @lambda_server.next_invocation + $_global_aws_request_id = request_id + if (trace_id = raw_request['Lambda-Runtime-Trace-Id']) + ENV['_X_AMZN_TRACE_ID'] = trace_id + end + request = AwsLambda::Marshaller.marshall_request(raw_request) + + LambdaInvocationRequest.new(request_id, raw_request, request, trace_id) + rescue LambdaErrors::InvocationError => e + @runtime_loop_active = false # ends the loop + raise e # ends the process + end + + def run_user_code(lambda_invocation_request) + context = LambdaContext.new(lambda_invocation_request.raw_request) # pass in opts + + # start of user code + handler_response, content_type = @lambda_handler.call_handler( + request: lambda_invocation_request.request, + context: context + ) + # end of user code + + @lambda_server.send_response( + request_id: lambda_invocation_request.request_id, + response_object: handler_response, + content_type: content_type + ) + + rescue LambdaErrors::LambdaHandlerError => e + LambdaLogger.log_error(exception: e, message: 'Error raised from handler method') + send_error_response(lambda_invocation_request, e) + rescue LambdaErrors::LambdaHandlerCriticalException => e + LambdaLogger.log_error(exception: e, message: 'Critical exception from handler') + send_error_response(lambda_invocation_request, e, -1, false) + rescue LambdaErrors::LambdaRuntimeError => e + send_error_response(lambda_invocation_request, e, -2, false) + end + + def send_init_error_to_server(err) + ex = LambdaErrors::LambdaRuntimeInitError.new(err) + LambdaLogger.log_error(exception: ex, message: "Init error when loading handler #{@env_handler}") + @lambda_server.send_init_error(error_object: ex.to_lambda_response, error: ex) + end + + def send_error_response(lambda_invocation, err, exit_code = nil, runtime_loop_active: true) + error_object = err.to_lambda_response + @lambda_server.send_error_response( + request_id: lambda_invocation.request_id, + error_object: error_object, + error: err, + xray_cause: XRayCause.new(error_object).as_json + ) + + @exit_code = exit_code unless exit_code.nil? + @runtime_loop_active = runtime_loop_active + end + end + + # Helper class to for mutating std logger with TelemetryLog + class TelemetryLoggingHelper + + ENV_VAR_TELEMETRY_LOG_FD = '_LAMBDA_TELEMETRY_LOG_FD' + + class << self + attr_accessor :telemetry_log_fd_file, :telemetry_log_sink + + def close + telemetry_log_fd_file&.close + end + end + + def initialize(telemetry_log_fd, path_to_fd='/proc/self/fd/') + fd = "#{path_to_fd}#{telemetry_log_fd}" + AwsLambdaRuntimeInterfaceClient::TelemetryLoggingHelper.telemetry_log_fd_file = File.open(fd, 'wb') + AwsLambdaRuntimeInterfaceClient::TelemetryLoggingHelper.telemetry_log_fd_file.sync = true + + AwsLambdaRuntimeInterfaceClient::TelemetryLoggingHelper.telemetry_log_sink = TelemetryLogSink.new(file: AwsLambdaRuntimeInterfaceClient::TelemetryLoggingHelper.telemetry_log_fd_file) + + mutate_std_logger + mutate_kernel_puts + rescue Errno::ENOENT + # If File.open() fails, then the mutation won't happen and the default behaviour (print to stdout) will prevail + end + + private + + def mutate_std_logger + Logger.class_eval do + alias_method :orig_initialize, :initialize + def initialize(logdev, shift_age = 0, shift_size = 1048576, level: 'debug', + progname: nil, formatter: nil, datetime_format: nil, + binmode: false, shift_period_suffix: '%Y%m%d') + # use unpatched constructor if logdev is a filename or an IO Object other than $stdout or $stderr + if logdev && logdev != $stdout && logdev != $stderr + orig_initialize(logdev, shift_age, shift_size, level, progname, + formatter, datetime_format, binmode, shift_period_suffix) + else + self.level = level + self.progname = progname + @default_formatter = LambdaLogFormatter.new + self.datetime_format = datetime_format + self.formatter = formatter + @logdev = AwsLambdaRuntimeInterfaceClient::TelemetryLoggingHelper.telemetry_log_sink + end + end + end + end + + def mutate_kernel_puts + Kernel.module_eval do + def puts(*arg) + msg = arg.flatten.collect { |a| a.to_s.encode('UTF-8') }.join("\n") + AwsLambdaRuntimeInterfaceClient::TelemetryLoggingHelper.telemetry_log_sink.write(msg) + end + end + end + end + + # Represents a single Lambda Invocation Request + class LambdaInvocationRequest + + attr_accessor :request_id, :raw_request, :request, :trace_id + + def initialize(request_id, raw_request, request, trace_id) + @request_id = request_id + @raw_request = raw_request + @request = request + @trace_id = trace_id + end + end +end diff --git a/lib/aws_lambda_ric/aws_lambda_marshaller.rb b/lib/aws_lambda_ric/aws_lambda_marshaller.rb new file mode 100644 index 0000000..7d712b0 --- /dev/null +++ b/lib/aws_lambda_ric/aws_lambda_marshaller.rb @@ -0,0 +1,35 @@ +# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +# frozen_string_literal: true + +module AwsLambda + class Marshaller + class << self + # By default, JSON-parses the raw request body. This can be overwritten + # by users who know what they are doing. + def marshall_request(raw_request) + content_type = raw_request['Content-Type'] + if content_type == 'application/json' + JSON.parse(raw_request.body) + else + raw_request.body # return it unaltered + end + end + + # By default, just runs #to_json on the method's response value. + # This can be overwritten by users who know what they are doing. + # The response is an array of response, content-type. + # If returned without a content-type, it is assumed to be application/json + # Finally, StringIO/IO is used to signal a response that shouldn't be + # formatted as JSON, and should get a different content-type header. + def marshall_response(method_response) + case method_response + when StringIO, IO + [method_response, 'application/unknown'] + else + method_response.to_json # application/json is assumed + end + end + end + end +end diff --git a/lib/aws_lambda_ric/bootstrap.rb b/lib/aws_lambda_ric/bootstrap.rb new file mode 100644 index 0000000..ffca6e3 --- /dev/null +++ b/lib/aws_lambda_ric/bootstrap.rb @@ -0,0 +1,38 @@ +require_relative '../aws_lambda_ric' + +# Bootstrap runtime +module Bootstrap + + def self.start + bootstrap_telemetry_log_sink + bootstrap_handler + end + + def self.fetch_runtime_server + ENV.fetch(AwsLambdaRuntimeInterfaceClient::LambdaRunner::ENV_VAR_RUNTIME_API) + rescue KeyError + puts 'Failed to get runtime server address from AWS_LAMBDA_RUNTIME_API env variable' + exit(-2) + end + + def self.bootstrap_telemetry_log_sink(path_to_fd='/proc/self/fd/') + fd = ENV.fetch(AwsLambdaRuntimeInterfaceClient::TelemetryLoggingHelper::ENV_VAR_TELEMETRY_LOG_FD) + ENV.delete(AwsLambdaRuntimeInterfaceClient::TelemetryLoggingHelper::ENV_VAR_TELEMETRY_LOG_FD) + AwsLambdaRuntimeInterfaceClient::TelemetryLoggingHelper.new(fd, path_to_fd) + rescue KeyError + puts 'Skipped bootstraping TelemetryLog' + end + + def self.bootstrap_handler + if ARGV.empty? + puts 'No handler specified, exiting Runtime Interface Client.' + exit + end + app_root = Dir.pwd + handler = ARGV[0] + lambda_runner = AwsLambdaRuntimeInterfaceClient::LambdaRunner.new(fetch_runtime_server) + puts "Executing '#{handler}' in function directory '#{app_root}'" + lambda_runner.run(app_root, handler) + end + +end \ No newline at end of file diff --git a/lib/aws_lambda_ric/lambda_context.rb b/lib/aws_lambda_ric/lambda_context.rb new file mode 100644 index 0000000..7ed2da9 --- /dev/null +++ b/lib/aws_lambda_ric/lambda_context.rb @@ -0,0 +1,29 @@ +# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +# frozen_string_literal: true + +class LambdaContext + attr_reader :aws_request_id, :invoked_function_arn, :log_group_name, + :log_stream_name, :function_name, :memory_limit_in_mb, :function_version, + :identity, :client_context, :deadline_ms + + def initialize(request) + @clock_diff = Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond) - Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) + @deadline_ms = request['Lambda-Runtime-Deadline-Ms'].to_i + @aws_request_id = request['Lambda-Runtime-Aws-Request-Id'] + @invoked_function_arn = request['Lambda-Runtime-Invoked-Function-Arn'] + @log_group_name = ENV['AWS_LAMBDA_LOG_GROUP_NAME'] + @log_stream_name = ENV['AWS_LAMBDA_LOG_STREAM_NAME'] + @function_name = ENV['AWS_LAMBDA_FUNCTION_NAME'] + @memory_limit_in_mb = ENV['AWS_LAMBDA_FUNCTION_MEMORY_SIZE'] + @function_version = ENV['AWS_LAMBDA_FUNCTION_VERSION'] + @identity = JSON.parse(request['Lambda-Runtime-Cognito-Identity']) if request['Lambda-Runtime-Cognito-Identity'] + @client_context = JSON.parse(request['Lambda-Runtime-Client-Context']) if request['Lambda-Runtime-Client-Context'] + end + + def get_remaining_time_in_millis + now = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) + @clock_diff + remaining = @deadline_ms - now + remaining.positive? ? remaining : 0 + end +end diff --git a/lib/aws_lambda_ric/lambda_errors.rb b/lib/aws_lambda_ric/lambda_errors.rb new file mode 100644 index 0000000..b0b40d4 --- /dev/null +++ b/lib/aws_lambda_ric/lambda_errors.rb @@ -0,0 +1,85 @@ +# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +# frozen_string_literal: true +module LambdaErrors + + class LambdaErrors::InvocationError < StandardError; + end + + class LambdaError < StandardError + def initialize(original_error, classification = 'Function') + @error_class = original_error.class.to_s + @error_type = "#{classification}<#{original_error.class}>" + @error_message = original_error.message + @stack_trace = _sanitize_stacktrace(original_error.backtrace_locations) + super(original_error) + end + + def runtime_error_type + if _allowed_error? + @error_type + else + 'Function' + end + end + + def to_lambda_response + { + :errorMessage => @error_message, + :errorType => @error_type, + :stackTrace => @stack_trace + } + end + + private + + def _sanitize_stacktrace(stacktrace) + ret = [] + safe_trace = true + if stacktrace + stacktrace.first(100).each do |line| + if safe_trace + if line.to_s.match(%r{^lib}) + safe_trace = false + else + ret << line + end + end # else skip + end + end + ret + end + + def _allowed_error? + # _aws_sdk_pattern? || _standard_error? + _standard_error? + end + + # Currently unused, may be activated later. + def _aws_sdk_pattern? + @error_class.match(/Aws(::\w+)*::Errors/) + end + + def _standard_error? + %w[ArgumentError NoMethodError Exception StandardError NameError LoadError SystemExit SystemStackError].include?(@error_class) + end + end + + class LambdaHandlerError < LambdaError + end + + class LambdaHandlerCriticalException < LambdaError + end + + class LambdaRuntimeError < LambdaError + def initialize(original_error) + super(original_error, 'Runtime') + end + end + + class LambdaRuntimeInitError < LambdaError + def initialize(original_error) + super(original_error, 'Init') + end + end +end diff --git a/lib/aws_lambda_ric/lambda_handler.rb b/lib/aws_lambda_ric/lambda_handler.rb new file mode 100644 index 0000000..c834f08 --- /dev/null +++ b/lib/aws_lambda_ric/lambda_handler.rb @@ -0,0 +1,43 @@ +# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +require_relative 'aws_lambda_marshaller' + +# frozen_string_literal: true +class LambdaHandler + attr_reader :handler_file_name, :handler_method_name + + def initialize(env_handler:) + handler_split = env_handler.split('.') + if handler_split.size == 2 + @handler_file_name, @handler_method_name = handler_split + elsif handler_split.size == 3 + @handler_file_name, @handler_class, @handler_method_name = handler_split + else + raise ArgumentError.new("Invalid handler #{handler_split}, must be of form FILENAME.METHOD or FILENAME.CLASS.METHOD where FILENAME corresponds with an existing Ruby source file FILENAME.rb, CLASS is an optional module/class namespace and METHOD is a callable method. If using CLASS, METHOD must be a class-level method.") + end + end + + def call_handler(request:, context:) + opts = { + event: request, + context: context + } + if @handler_class + response = Kernel.const_get(@handler_class).send(@handler_method_name, **opts) + else + response = __send__(@handler_method_name, **opts) + end + # serialization can be a part of user code + AwsLambda::Marshaller.marshall_response(response) + rescue NoMethodError => e + # This is a special case of standard error that we want to hard-fail for + raise LambdaErrors::LambdaHandlerCriticalException.new(e) + rescue NameError => e + # This is a special case error that we want to wrap + raise LambdaErrors::LambdaHandlerCriticalException.new(e) + rescue StandardError => e + raise LambdaErrors::LambdaHandlerError.new(e) + rescue Exception => e + raise LambdaErrors::LambdaHandlerCriticalException.new(e) + end +end diff --git a/lib/aws_lambda_ric/lambda_log_formatter.rb b/lib/aws_lambda_ric/lambda_log_formatter.rb new file mode 100644 index 0000000..dfedc4e --- /dev/null +++ b/lib/aws_lambda_ric/lambda_log_formatter.rb @@ -0,0 +1,14 @@ +# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +# frozen_string_literal: true + +require 'logger' + +class LambdaLogFormatter < Logger::Formatter + FORMAT = '%s, [%s#%d] %5s %s -- %s: %s' + + def call(severity, time, progname, msg) + (FORMAT % {sev: severity[0..0], datetime: format_datetime(time), process: $$, severity: severity, + request_id: $_global_aws_request_id, progname: progname, msg: msg2str(msg)}).encode!('UTF-8') + end +end diff --git a/lib/aws_lambda_ric/lambda_logger.rb b/lib/aws_lambda_ric/lambda_logger.rb new file mode 100644 index 0000000..9afaea2 --- /dev/null +++ b/lib/aws_lambda_ric/lambda_logger.rb @@ -0,0 +1,10 @@ +# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +class LambdaLogger + class << self + def log_error(exception:, message: nil) + puts message if message + puts JSON.pretty_unparse(exception.to_lambda_response) + end + end +end diff --git a/lib/aws_lambda_ric/lambda_server.rb b/lib/aws_lambda_ric/lambda_server.rb new file mode 100644 index 0000000..7c36c64 --- /dev/null +++ b/lib/aws_lambda_ric/lambda_server.rb @@ -0,0 +1,88 @@ +# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +# frozen_string_literal: true + +require 'net/http' +require 'json' +require_relative 'lambda_errors' + +class LambdaServer + LAMBDA_DEFAULT_SERVER_ADDRESS = '127.0.0.1:9001' + LAMBDA_RUNTIME_API_VERSION = '2018-06-01' + + MAX_HEADER_SIZE = 1024 * 1024 + LONG_TIMEOUT = 1_000_000 + + def initialize(server_address) + server_address ||= LAMBDA_DEFAULT_SERVER_ADDRESS + @server_address = 'http://' + server_address + '/' + LAMBDA_RUNTIME_API_VERSION + end + + def next_invocation + next_invocation_uri = URI(@server_address + '/runtime/invocation/next') + begin + http = Net::HTTP.new(next_invocation_uri.host, next_invocation_uri.port) + http.read_timeout = LONG_TIMEOUT + resp = http.start do |connection| + connection.get(next_invocation_uri.path) + end + if resp.is_a?(Net::HTTPSuccess) + request_id = resp['Lambda-Runtime-Aws-Request-Id'] + [request_id, resp] + else + raise LambdaErrors::InvocationError.new( + "Received #{resp.code} when waiting for next invocation." + ) + end + rescue LambdaErrors::InvocationError => e + raise e + rescue StandardError => e + raise LambdaErrors::InvocationError.new(e) + end + end + + def send_response(request_id:, response_object:, content_type: 'application/json') + response_uri = URI(@server_address + "/runtime/invocation/#{request_id}/response") + begin + # unpack IO at this point + if content_type == 'application/unknown' + response_object = response_object.read + end + Net::HTTP.post( + response_uri, + response_object, + { 'Content-Type' => content_type } + ) + rescue StandardError => e + raise LambdaErrors::LambdaRuntimeError.new(e) + end + end + + def send_error_response(request_id:, error_object:, error:, xray_cause:) + response_uri = URI(@server_address + "/runtime/invocation/#{request_id}/error") + begin + headers = { 'Lambda-Runtime-Function-Error-Type' => error.runtime_error_type } + headers['Lambda-Runtime-Function-XRay-Error-Cause'] = xray_cause if xray_cause.bytesize < MAX_HEADER_SIZE + Net::HTTP.post( + response_uri, + error_object.to_json, + headers + ) + rescue StandardError => e + raise LambdaErrors::LambdaRuntimeError.new(e) + end + end + + def send_init_error(error_object:, error:) + uri = URI("#{@server_address}/runtime/init/error") + begin + Net::HTTP.post( + uri, + error_object.to_json, + { 'Lambda-Runtime-Function-Error-Type' => error.runtime_error_type } + ) + rescue StandardError => e + raise LambdaErrors::LambdaRuntimeInitError.new(e) + end + end +end diff --git a/lib/aws_lambda_ric/telemetry_log_sink.rb b/lib/aws_lambda_ric/telemetry_log_sink.rb new file mode 100644 index 0000000..cbd20b2 --- /dev/null +++ b/lib/aws_lambda_ric/telemetry_log_sink.rb @@ -0,0 +1,43 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +# frozen_string_literal: true + +require 'logger' + +class TelemetryLogSink < Logger::LogDevice + + # TelemetryLogSink implements the logging contract between runtimes and the platform. It implements a simple + # framing protocol so message boundaries can be determined. Each frame can be visualized as follows: + # + # +----------------------+------------------------+-----------------------+ + # | Frame Type - 4 bytes | Length (len) - 4 bytes | Message - \'len\' bytes | + # +----------------------+------------------------+-----------------------+ + # + # The first 4 bytes indicate the type of the frame - log frames have a type defined as the hex value 0xa55a0001. The + # second 4 bytes should indicate the message\'s length. The next \'len\' bytes contain the message. The byte order is + # big-endian. + + def initialize(file:) + @file = file + end + + FRAME_BYTES = [0xa55a0001].pack('L>') + + def write(msg) + if @file.nil? || @file.closed? + $stdout.write(msg) + else + @file.write(FRAME_BYTES) + @file.write([msg.bytesize].pack('L>')) + @file.write(msg) + end + end + + def reopen(log = nil) + # do nothing + end + + def close + # do nothing + end +end diff --git a/lib/aws_lambda_ric/version.rb b/lib/aws_lambda_ric/version.rb new file mode 100644 index 0000000..3b33e10 --- /dev/null +++ b/lib/aws_lambda_ric/version.rb @@ -0,0 +1,7 @@ +# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +# frozen_string_literal: true + +module AwsLambdaRuntimeInterfaceClient + VERSION = '1.0.0' +end diff --git a/lib/aws_lambda_ric/xray_cause.rb b/lib/aws_lambda_ric/xray_cause.rb new file mode 100644 index 0000000..8d14e95 --- /dev/null +++ b/lib/aws_lambda_ric/xray_cause.rb @@ -0,0 +1,42 @@ +# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +require_relative 'lambda_errors' + +class XRayCause + MAX_DEPTH = 15 + + def initialize(lambda_error) + @cause = { + working_directory: Dir.pwd, + paths: Gem.paths.path, + exceptions: lambda_error ? normalize(err: lambda_error) : lambda_error + } + end + + def as_json + @as_json ||= begin + JSON.dump(@cause) + end + end + + private + + def normalize(err:) + exception = { + message: err[:errorMessage], + type: err[:errorType] + } + + backtrace = err[:stackTrace] + if backtrace + exception[:stack] = backtrace.first(MAX_DEPTH).collect do |t| + { + path: t.path, + line: t.lineno, + label: t.label + } + end + end + [exception] + end +end diff --git a/test/integration/codebuild-local/Dockerfile.agent b/test/integration/codebuild-local/Dockerfile.agent new file mode 100644 index 0000000..e9a9ffe --- /dev/null +++ b/test/integration/codebuild-local/Dockerfile.agent @@ -0,0 +1,5 @@ +FROM amazonlinux:2 + +RUN amazon-linux-extras enable docker && \ + yum clean metadata && \ + yum install -y docker tar diff --git a/test/integration/codebuild-local/codebuild_build.sh b/test/integration/codebuild-local/codebuild_build.sh new file mode 100755 index 0000000..64af522 --- /dev/null +++ b/test/integration/codebuild-local/codebuild_build.sh @@ -0,0 +1,194 @@ +#!/bin/bash +# This file is copied from https://github.com/aws/aws-codebuild-docker-images/blob/f0912e4b16e427da35351fc102f0f56f4ceb938a/local_builds/codebuild_build.sh + +function allOSRealPath() { + if isOSWindows + then + path="" + case $1 in + .* ) path="$PWD/${1#./}" ;; + /* ) path="$1" ;; + * ) path="/$1" ;; + esac + + echo "/$path" | sed -e 's/\\/\//g' -e 's/://' -e 's/./\U&/3' + else + case $1 in + /* ) echo "$1"; exit;; + * ) echo "$PWD/${1#./}"; exit;; + esac + fi +} + +function isOSWindows() { + if [ $OSTYPE == "msys" ] + then + return 0 + else + return 1 + fi +} + +function usage { + echo "usage: codebuild_build.sh [-i image_name] [-a artifact_output_directory] [options]" + echo "Required:" + echo " -i Used to specify the customer build container image." + echo " -a Used to specify an artifact output directory." + echo "Options:" + echo " -l IMAGE Used to override the default local agent image." + echo " -s Used to specify source information. Defaults to the current working directory for primary source." + echo " * First (-s) is for primary source" + echo " * Use additional (-s) in : format for secondary source" + echo " * For sourceIdentifier, use a value that is fewer than 128 characters and contains only alphanumeric characters and underscores" + echo " -c Use the AWS configuration and credentials from your local host. This includes ~/.aws and any AWS_* environment variables." + echo " -p Used to specify the AWS CLI Profile." + echo " -b FILE Used to specify a buildspec override file. Defaults to buildspec.yml in the source directory." + echo " -m Used to mount the source directory to the customer build container directly." + echo " -d Used to run the build container in docker privileged mode." + echo " -e FILE Used to specify a file containing environment variables." + echo " (-e) File format expectations:" + echo " * Each line is in VAR=VAL format" + echo " * Lines beginning with # are processed as comments and ignored" + echo " * Blank lines are ignored" + echo " * File can be of type .env or .txt" + echo " * There is no special handling of quotation marks, meaning they will be part of the VAL" + exit 1 +} + +image_flag=false +artifact_flag=false +awsconfig_flag=false +mount_src_dir_flag=false +docker_privileged_mode_flag=false + +while getopts "cmdi:a:s:b:e:l:p:h" opt; do + case $opt in + i ) image_flag=true; image_name=$OPTARG;; + a ) artifact_flag=true; artifact_dir=$OPTARG;; + b ) buildspec=$OPTARG;; + c ) awsconfig_flag=true;; + m ) mount_src_dir_flag=true;; + d ) docker_privileged_mode_flag=true;; + s ) source_dirs+=("$OPTARG");; + e ) environment_variable_file=$OPTARG;; + l ) local_agent_image=$OPTARG;; + p ) aws_profile=$OPTARG;; + h ) usage; exit;; + \? ) echo "Unknown option: -$OPTARG" >&2; exit 1;; + : ) echo "Missing option argument for -$OPTARG" >&2; exit 1;; + * ) echo "Invalid option: -$OPTARG" >&2; exit 1;; + esac +done + +if ! $image_flag +then + echo "The image name flag (-i) must be included for a build to run" >&2 +fi + +if ! $artifact_flag +then + echo "The artifact directory (-a) must be included for a build to run" >&2 +fi + +if ! $image_flag || ! $artifact_flag +then + exit 1 +fi + +docker_command="docker run -it " +if isOSWindows +then + docker_command+="-v //var/run/docker.sock:/var/run/docker.sock -e " +else + docker_command+="-v /var/run/docker.sock:/var/run/docker.sock -e " +fi + +docker_command+="\"IMAGE_NAME=$image_name\" -e \ + \"ARTIFACTS=$(allOSRealPath "$artifact_dir")\"" + +if [ -z "$source_dirs" ] +then + docker_command+=" -e \"SOURCE=$(allOSRealPath "$PWD")\"" +else + for index in "${!source_dirs[@]}"; do + if [ $index -eq 0 ] + then + docker_command+=" -e \"SOURCE=$(allOSRealPath "${source_dirs[$index]}")\"" + else + identifier=${source_dirs[$index]%%:*} + src_dir=$(allOSRealPath "${source_dirs[$index]#*:}") + + docker_command+=" -e \"SECONDARY_SOURCE_$index=$identifier:$src_dir\"" + fi + done +fi + +if [ -n "$buildspec" ] +then + docker_command+=" -e \"BUILDSPEC=$(allOSRealPath "$buildspec")\"" +fi + +if [ -n "$environment_variable_file" ] +then + environment_variable_file_path=$(allOSRealPath "$environment_variable_file") + environment_variable_file_dir=$(dirname "$environment_variable_file_path") + environment_variable_file_basename=$(basename "$environment_variable_file") + docker_command+=" -v \"$environment_variable_file_dir:/LocalBuild/envFile/\" -e \"ENV_VAR_FILE=$environment_variable_file_basename\"" +fi + +if [ -n "$local_agent_image" ] +then + docker_command+=" -e \"LOCAL_AGENT_IMAGE_NAME=$local_agent_image\"" +fi + +if $awsconfig_flag +then + if [ -d "$HOME/.aws" ] + then + configuration_file_path=$(allOSRealPath "$HOME/.aws") + docker_command+=" -e \"AWS_CONFIGURATION=$configuration_file_path\"" + else + docker_command+=" -e \"AWS_CONFIGURATION=NONE\"" + fi + + if [ -n "$aws_profile" ] + then + docker_command+=" -e \"AWS_PROFILE=$aws_profile\"" + fi + + docker_command+="$(env | grep ^AWS_ | while read -r line; do echo " -e \"$line\""; done )" +fi + +if $mount_src_dir_flag +then + docker_command+=" -e \"MOUNT_SOURCE_DIRECTORY=TRUE\"" +fi + +if $docker_privileged_mode_flag +then + docker_command+=" -e \"DOCKER_PRIVILEGED_MODE=TRUE\"" +fi + +if isOSWindows +then + docker_command+=" -e \"INITIATOR=$USERNAME\"" +else + docker_command+=" -e \"INITIATOR=$USER\"" +fi + +docker_command+=" amazon/aws-codebuild-local:latest" + +# Note we do not expose the AWS_SECRET_ACCESS_KEY or the AWS_SESSION_TOKEN +exposed_command=$docker_command +secure_variables=( "AWS_SECRET_ACCESS_KEY=" "AWS_SESSION_TOKEN=") +for variable in "${secure_variables[@]}" +do + exposed_command="$(echo $exposed_command | sed "s/\($variable\)[^ ]*/\1********\"/")" +done + +echo "Build Command:" +echo "" +echo $exposed_command +echo "" + +eval $docker_command diff --git a/test/integration/codebuild-local/test_all.sh b/test/integration/codebuild-local/test_all.sh new file mode 100755 index 0000000..0c5168c --- /dev/null +++ b/test/integration/codebuild-local/test_all.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +set -euo pipefail + +CODEBUILD_IMAGE_TAG="${CODEBUILD_IMAGE_TAG:-al2/x86_64/standard/3.0}" +DRYRUN="${DRYRUN-0}" + +function usage { + echo "usage: test_all.sh buildspec_yml_dir" + echo "Runs all buildspec build-matrix combinations via test_one.sh." + echo "Required:" + echo " buildspec_yml_dir Used to specify the CodeBuild buildspec template file." +} + +do_one_yaml() { + local -r YML="$1" + + OS_DISTRIBUTION=$(grep -oE 'OS_DISTRIBUTION:\s*(\S+)' "$YML" | cut -d' ' -f2) + DISTRO_VERSIONS=$(sed '1,/DISTRO_VERSION/d;/RUNTIME_VERSION/,$d' "$YML" | tr -d '\-" ') + RUNTIME_VERSIONS=$(sed '1,/RUNTIME_VERSION/d;/phases/,$d' "$YML" | sed '/#.*$/d' | tr -d '\-" ') + + for DISTRO_VERSION in $DISTRO_VERSIONS ; do + for RUNTIME_VERSION in $RUNTIME_VERSIONS ; do + if (( DRYRUN == 1 )) ; then + echo DRYRUN test_one_combination "$YML" "$OS_DISTRIBUTION" "$DISTRO_VERSION" "$RUNTIME_VERSION" + else + test_one_combination "$YML" "$OS_DISTRIBUTION" "$DISTRO_VERSION" "$RUNTIME_VERSION" + fi + done + done +} + +test_one_combination() { + local -r YML="$1" + local -r OS_DISTRIBUTION="$2" + local -r DISTRO_VERSION="$3" + local -r RUNTIME_VERSION="$4" + + echo Testing: + echo " BUILDSPEC" "$YML" + echo " with" "$OS_DISTRIBUTION"-"$DISTRO_VERSION" "$RUNTIME_VERSION" + + "$(dirname "$0")"/test_one.sh "$YML" "$OS_DISTRIBUTION" "$DISTRO_VERSION" "$RUNTIME_VERSION" \ + > >(sed "s/^/$OS_DISTRIBUTION$DISTRO_VERSION-$RUNTIME_VERSION: /") 2> >(sed "s/^/$OS_DISTRIBUTION-$DISTRO_VERSION:$RUNTIME_VERSION: /" >&2) +} + +main() { + if (( $# != 1 && $# != 2)); then + >&2 echo "Invalid number of parameters." + usage + exit 1 + fi + + BUILDSPEC_YML_DIR="$1" + HAS_YML=0 + for f in "$BUILDSPEC_YML_DIR"/*.yml ; do + [ -f "$f" ] || continue; + do_one_yaml "$f" + HAS_YML=1 + done + + if (( HAS_YML == 0 )); then + >&2 echo At least one buildspec is required. + exit 2 + fi +} + +main "$@" diff --git a/test/integration/codebuild-local/test_one.sh b/test/integration/codebuild-local/test_one.sh new file mode 100755 index 0000000..8a5029d --- /dev/null +++ b/test/integration/codebuild-local/test_one.sh @@ -0,0 +1,59 @@ +#!/bin/bash +# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +set -euo pipefail + +CODEBUILD_IMAGE_TAG="${CODEBUILD_IMAGE_TAG:-al2/x86_64/standard/3.0}" + +function usage { + >&2 echo "usage: test_one.sh buildspec_yml os_distribution distro_version runtime_version [env]" + >&2 echo "Runs one buildspec version combination from a build-matrix buildspec." + >&2 echo "Required:" + >&2 echo " buildspec_yml Used to specify the CodeBuild buildspec template file." + >&2 echo " os_distribution Used to specify the OS distribution to build." + >&2 echo " distro_version Used to specify the distro version of ." + >&2 echo " runtime_version Used to specify the runtime version to test on the selected ." + >&2 echo "Optional:" + >&2 echo " env Additional environment variables file." +} + +main() { + if (( $# != 3 && $# != 4)); then + >&2 echo "Invalid number of parameters." + usage + exit 1 + fi + + BUILDSPEC_YML="$1" + OS_DISTRIBUTION="$2" + DISTRO_VERSION="$3" + RUNTIME_VERSION="$4" + EXTRA_ENV="${5-}" + + CODEBUILD_TEMP_DIR=$(mktemp -d codebuild."$OS_DISTRIBUTION"-"$DISTRO_VERSION"-"$RUNTIME_VERSION".XXXXXXXXXX ) + trap 'rm -rf $CODEBUILD_TEMP_DIR' EXIT + + # Create an env file for codebuild_build. + ENVFILE="$CODEBUILD_TEMP_DIR/.env" + if [ -f "$EXTRA_ENV" ]; then + cat "$EXTRA_ENV" > "$ENVFILE" + fi + { + echo "" + echo "OS_DISTRIBUTION=$OS_DISTRIBUTION" + echo "DISTRO_VERSION=$DISTRO_VERSION" + echo "RUNTIME_VERSION=$RUNTIME_VERSION" + } >> "$ENVFILE" + + ARTIFACTS_DIR="$CODEBUILD_TEMP_DIR/artifacts" + mkdir -p "$ARTIFACTS_DIR" + + # Run CodeBuild local agent. + "$(dirname "$0")"/codebuild_build.sh \ + -i "$CODEBUILD_IMAGE_TAG" \ + -a "$ARTIFACTS_DIR" \ + -e "$ENVFILE" \ + -b "$BUILDSPEC_YML" +} + +main "$@" diff --git a/test/integration/codebuild/buildspec.os.alpine.yml b/test/integration/codebuild/buildspec.os.alpine.yml new file mode 100644 index 0000000..af1a09e --- /dev/null +++ b/test/integration/codebuild/buildspec.os.alpine.yml @@ -0,0 +1,96 @@ +version: 0.2 + +env: + variables: + OS_DISTRIBUTION: alpine + EXECUTABLE: "/usr/local/bundle/bin/aws_lambda_ric" + TEST_NAME: "aws-lambda-ruby-ric-alpine-test" +batch: + build-matrix: + static: + ignore-failure: false + env: + type: LINUX_CONTAINER + privileged-mode: true + dynamic: + env: + variables: + DISTRO_VERSION: + - "3.11" + - "3.12" + RUNTIME_VERSION: + - "2.5" + - "2.6" + - "2.7" +phases: + pre_build: + commands: + - export IMAGE_TAG="ruby-${OS_DISTRIBUTION}-${DISTRO_VERSION}:${RUNTIME_VERSION}" + - echo "Extracting and including the Runtime Interface Emulator" + - SCRATCH_DIR=".scratch" + - mkdir "${SCRATCH_DIR}" + - tar -xvf test/integration/resources/aws-lambda-rie.tar.gz --directory "${SCRATCH_DIR}" + - > + cp "test/integration/docker/Dockerfile.echo.${OS_DISTRIBUTION}" \ + "${SCRATCH_DIR}/Dockerfile.echo.${OS_DISTRIBUTION}.tmp" + - > + echo "RUN apk add curl" >> \ + "${SCRATCH_DIR}/Dockerfile.echo.${OS_DISTRIBUTION}.tmp" + - > + echo "COPY ${SCRATCH_DIR}/aws-lambda-rie /usr/bin/aws-lambda-rie" >> \ + "${SCRATCH_DIR}/Dockerfile.echo.${OS_DISTRIBUTION}.tmp" + - echo "Building image ${IMAGE_TAG}" + - > + docker build . \ + -f "${SCRATCH_DIR}/Dockerfile.echo.${OS_DISTRIBUTION}.tmp" \ + -t "${IMAGE_TAG}" \ + --build-arg RUNTIME_VERSION="${RUNTIME_VERSION}" \ + --build-arg DISTRO_VERSION="${DISTRO_VERSION}" + build: + commands: + - set -x + - echo "Running Image ${IMAGE_TAG}" + - docker network create "${TEST_NAME}-network" + - > + docker run \ + --detach \ + --env "EXECUTABLE=${EXECUTABLE}" \ + --name "${TEST_NAME}-app" \ + --network "${TEST_NAME}-network" \ + --entrypoint="" \ + "${IMAGE_TAG}" \ + sh -c '/usr/bin/aws-lambda-rie "${EXECUTABLE}" "app.App::Handler.process"' + - sleep 2 + - > + docker run \ + --name "${TEST_NAME}-tester" \ + --env "TARGET=${TEST_NAME}-app" \ + --network "${TEST_NAME}-network" \ + --entrypoint="" \ + "${IMAGE_TAG}" \ + sh -c 'curl -X POST "http://${TARGET}:8080/2015-03-31/functions/function/invocations" -d "{}" --max-time 10' + - actual="$(docker logs --tail 1 "${TEST_NAME}-tester" | xargs)" + - expected='success' + - | + echo "Response: ${actual}" + if [[ "$actual" != "$expected" ]]; then + echo "fail! runtime: $RUNTIME - expected output $expected - got $actual" + echo "---------Container Logs: ${TEST_NAME}-app----------" + echo + docker logs "${TEST_NAME}-app" + echo + echo "---------------------------------------------------" + echo "--------Container Logs: ${TEST_NAME}-tester--------" + echo + docker logs "${TEST_NAME}-tester" + echo + echo "---------------------------------------------------" + exit -1 + fi + finally: + - echo "Cleaning up..." + - docker stop "${TEST_NAME}-app" || true + - docker rm --force "${TEST_NAME}-app" || true + - docker stop "${TEST_NAME}-tester" || true + - docker rm --force "${TEST_NAME}-tester" || true + - docker network rm "${TEST_NAME}-network" || true diff --git a/test/integration/codebuild/buildspec.os.amazonlinux.yml b/test/integration/codebuild/buildspec.os.amazonlinux.yml new file mode 100644 index 0000000..9e89200 --- /dev/null +++ b/test/integration/codebuild/buildspec.os.amazonlinux.yml @@ -0,0 +1,92 @@ +version: 0.2 + +env: + variables: + OS_DISTRIBUTION: amazonlinux + EXECUTABLE: "/usr/local/bin/aws_lambda_ric" + TEST_NAME: "aws-lambda-ruby-ric-amazonlinux-test" +batch: + build-matrix: + static: + ignore-failure: false + env: + type: LINUX_CONTAINER + privileged-mode: true + dynamic: + env: + variables: + DISTRO_VERSION: + - "2" + RUNTIME_VERSION: + - "2.5" + - "2.6" + - "2.7" +phases: + pre_build: + commands: + - export IMAGE_TAG="ruby-${OS_DISTRIBUTION}-${DISTRO_VERSION}:${RUNTIME_VERSION}" + - echo "Extracting and including the Runtime Interface Emulator" + - SCRATCH_DIR=".scratch" + - mkdir "${SCRATCH_DIR}" + - tar -xvf test/integration/resources/aws-lambda-rie.tar.gz --directory "${SCRATCH_DIR}" + - > + cp "test/integration/docker/Dockerfile.echo.${OS_DISTRIBUTION}" \ + "${SCRATCH_DIR}/Dockerfile.echo.${OS_DISTRIBUTION}.tmp" + - > + echo "COPY ${SCRATCH_DIR}/aws-lambda-rie /usr/bin/aws-lambda-rie" >> \ + "${SCRATCH_DIR}/Dockerfile.echo.${OS_DISTRIBUTION}.tmp" + - echo "Building image ${IMAGE_TAG}" + - > + docker build . \ + -f "${SCRATCH_DIR}/Dockerfile.echo.${OS_DISTRIBUTION}.tmp" \ + -t "${IMAGE_TAG}" \ + --build-arg RUNTIME_VERSION="${RUNTIME_VERSION}" \ + --build-arg DISTRO_VERSION="${DISTRO_VERSION}" + build: + commands: + - set -x + - echo "Running Image ${IMAGE_TAG}" + - docker network create "${TEST_NAME}-network" + - > + docker run \ + --detach \ + --env "EXECUTABLE=${EXECUTABLE}" \ + --name "${TEST_NAME}-app" \ + --network "${TEST_NAME}-network" \ + --entrypoint="" \ + "${IMAGE_TAG}" \ + sh -c '/usr/bin/aws-lambda-rie "${EXECUTABLE}" "app.App::Handler.process"' + - sleep 2 + - > + docker run \ + --name "${TEST_NAME}-tester" \ + --env "TARGET=${TEST_NAME}-app" \ + --network "${TEST_NAME}-network" \ + --entrypoint="" \ + "${IMAGE_TAG}" \ + sh -c 'curl -X POST "http://${TARGET}:8080/2015-03-31/functions/function/invocations" -d "{}" --max-time 10' + - actual="$(docker logs --tail 1 "${TEST_NAME}-tester" | xargs)" + - expected='success' + - | + echo "Response: ${actual}" + if [[ "$actual" != "$expected" ]]; then + echo "fail! runtime: $RUNTIME - expected output $expected - got $actual" + echo "---------Container Logs: ${TEST_NAME}-app----------" + echo + docker logs "${TEST_NAME}-app" + echo + echo "---------------------------------------------------" + echo "--------Container Logs: ${TEST_NAME}-tester--------" + echo + docker logs "${TEST_NAME}-tester" + echo + echo "---------------------------------------------------" + exit -1 + fi + finally: + - echo "Cleaning up..." + - docker stop "${TEST_NAME}-app" || true + - docker rm --force "${TEST_NAME}-app" || true + - docker stop "${TEST_NAME}-tester" || true + - docker rm --force "${TEST_NAME}-tester" || true + - docker network rm "${TEST_NAME}-network" || true diff --git a/test/integration/codebuild/buildspec.os.centos.yml b/test/integration/codebuild/buildspec.os.centos.yml new file mode 100644 index 0000000..c277ca2 --- /dev/null +++ b/test/integration/codebuild/buildspec.os.centos.yml @@ -0,0 +1,93 @@ +version: 0.2 + +env: + variables: + OS_DISTRIBUTION: centos + EXECUTABLE: "/usr/local/bin/aws_lambda_ric" + TEST_NAME: "aws-lambda-ruby-ric-centos-test" +batch: + build-matrix: + static: + ignore-failure: false + env: + type: LINUX_CONTAINER + privileged-mode: true + dynamic: + env: + variables: + DISTRO_VERSION: + - "7" + - "8" + RUNTIME_VERSION: + - "2.7" + - "2.6" + - "2.5" +phases: + pre_build: + commands: + - export IMAGE_TAG="ruby-${OS_DISTRIBUTION}-${DISTRO_VERSION}:${RUNTIME_VERSION}" + - echo "Extracting and including the Runtime Interface Emulator" + - SCRATCH_DIR=".scratch" + - mkdir "${SCRATCH_DIR}" + - tar -xvf test/integration/resources/aws-lambda-rie.tar.gz --directory "${SCRATCH_DIR}" + - > + cp "test/integration/docker/Dockerfile.echo.${OS_DISTRIBUTION}" \ + "${SCRATCH_DIR}/Dockerfile.echo.${OS_DISTRIBUTION}.tmp" + - > + echo "COPY ${SCRATCH_DIR}/aws-lambda-rie /usr/bin/aws-lambda-rie" >> \ + "${SCRATCH_DIR}/Dockerfile.echo.${OS_DISTRIBUTION}.tmp" + - echo "Building image ${IMAGE_TAG}" + - > + docker build . \ + -f "${SCRATCH_DIR}/Dockerfile.echo.${OS_DISTRIBUTION}.tmp" \ + -t "${IMAGE_TAG}" \ + --build-arg RUNTIME_VERSION="${RUNTIME_VERSION}" \ + --build-arg DISTRO_VERSION="${DISTRO_VERSION}" + build: + commands: + - set -x + - echo "Running Image ${IMAGE_TAG}" + - docker network create "${TEST_NAME}-network" + - > + docker run \ + --detach \ + --env "EXECUTABLE=${EXECUTABLE}" \ + --name "${TEST_NAME}-app" \ + --network "${TEST_NAME}-network" \ + --entrypoint="" \ + "${IMAGE_TAG}" \ + sh -c '/usr/bin/aws-lambda-rie "${EXECUTABLE}" "app.App::Handler.process"' + - sleep 2 + - > + docker run \ + --name "${TEST_NAME}-tester" \ + --env "TARGET=${TEST_NAME}-app" \ + --network "${TEST_NAME}-network" \ + --entrypoint="" \ + "${IMAGE_TAG}" \ + sh -c 'curl -X POST "http://${TARGET}:8080/2015-03-31/functions/function/invocations" -d "{}" --max-time 10' + - actual="$(docker logs --tail 1 "${TEST_NAME}-tester" | xargs)" + - expected='success' + - | + echo "Response: ${actual}" + if [[ "$actual" != "$expected" ]]; then + echo "fail! runtime: $RUNTIME - expected output $expected - got $actual" + echo "---------Container Logs: ${TEST_NAME}-app----------" + echo + docker logs "${TEST_NAME}-app" + echo + echo "---------------------------------------------------" + echo "--------Container Logs: ${TEST_NAME}-tester--------" + echo + docker logs "${TEST_NAME}-tester" + echo + echo "---------------------------------------------------" + exit -1 + fi + finally: + - echo "Cleaning up..." + - docker stop "${TEST_NAME}-app" || true + - docker rm --force "${TEST_NAME}-app" || true + - docker stop "${TEST_NAME}-tester" || true + - docker rm --force "${TEST_NAME}-tester" || true + - docker network rm "${TEST_NAME}-network" || true diff --git a/test/integration/codebuild/buildspec.os.debian.yml b/test/integration/codebuild/buildspec.os.debian.yml new file mode 100644 index 0000000..d99c8c7 --- /dev/null +++ b/test/integration/codebuild/buildspec.os.debian.yml @@ -0,0 +1,92 @@ +version: 0.2 + +env: + variables: + OS_DISTRIBUTION: debian + EXECUTABLE: "/usr/local/bundle/bin/aws_lambda_ric" + TEST_NAME: "aws-lambda-ruby-ric-debian-test" +batch: + build-matrix: + static: + ignore-failure: false + env: + type: LINUX_CONTAINER + privileged-mode: true + dynamic: + env: + variables: + DISTRO_VERSION: + - "buster" + RUNTIME_VERSION: + - "2.5" + - "2.6" + - "2.7" +phases: + pre_build: + commands: + - export IMAGE_TAG="ruby-${OS_DISTRIBUTION}-${DISTRO_VERSION}:${RUNTIME_VERSION}" + - echo "Extracting and including the Runtime Interface Emulator" + - SCRATCH_DIR=".scratch" + - mkdir "${SCRATCH_DIR}" + - tar -xvf test/integration/resources/aws-lambda-rie.tar.gz --directory "${SCRATCH_DIR}" + - > + cp "test/integration/docker/Dockerfile.echo.${OS_DISTRIBUTION}" \ + "${SCRATCH_DIR}/Dockerfile.echo.${OS_DISTRIBUTION}.tmp" + - > + echo "COPY ${SCRATCH_DIR}/aws-lambda-rie /usr/bin/aws-lambda-rie" >> \ + "${SCRATCH_DIR}/Dockerfile.echo.${OS_DISTRIBUTION}.tmp" + - echo "Building image ${IMAGE_TAG}" + - > + docker build . \ + -f "${SCRATCH_DIR}/Dockerfile.echo.${OS_DISTRIBUTION}.tmp" \ + -t "${IMAGE_TAG}" \ + --build-arg RUNTIME_VERSION="${RUNTIME_VERSION}" \ + --build-arg DISTRO_VERSION="${DISTRO_VERSION}" + build: + commands: + - set -x + - echo "Running Image ${IMAGE_TAG}" + - docker network create "${TEST_NAME}-network" + - > + docker run \ + --detach \ + --env "EXECUTABLE=${EXECUTABLE}" \ + --name "${TEST_NAME}-app" \ + --network "${TEST_NAME}-network" \ + --entrypoint="" \ + "${IMAGE_TAG}" \ + sh -c '/usr/bin/aws-lambda-rie "${EXECUTABLE}" "app.App::Handler.process"' + - sleep 2 + - > + docker run \ + --name "${TEST_NAME}-tester" \ + --env "TARGET=${TEST_NAME}-app" \ + --network "${TEST_NAME}-network" \ + --entrypoint="" \ + "${IMAGE_TAG}" \ + sh -c 'curl -X POST "http://${TARGET}:8080/2015-03-31/functions/function/invocations" -d "{}" --max-time 10' + - actual="$(docker logs --tail 1 "${TEST_NAME}-tester" | xargs)" + - expected='success' + - | + echo "Response: ${actual}" + if [[ "$actual" != "$expected" ]]; then + echo "fail! runtime: $RUNTIME - expected output $expected - got $actual" + echo "---------Container Logs: ${TEST_NAME}-app----------" + echo + docker logs "${TEST_NAME}-app" + echo + echo "---------------------------------------------------" + echo "--------Container Logs: ${TEST_NAME}-tester--------" + echo + docker logs "${TEST_NAME}-tester" + echo + echo "---------------------------------------------------" + exit -1 + fi + finally: + - echo "Cleaning up..." + - docker stop "${TEST_NAME}-app" || true + - docker rm --force "${TEST_NAME}-app" || true + - docker stop "${TEST_NAME}-tester" || true + - docker rm --force "${TEST_NAME}-tester" || true + - docker network rm "${TEST_NAME}-network" || true diff --git a/test/integration/codebuild/buildspec.os.ubuntu.yml b/test/integration/codebuild/buildspec.os.ubuntu.yml new file mode 100644 index 0000000..684b444 --- /dev/null +++ b/test/integration/codebuild/buildspec.os.ubuntu.yml @@ -0,0 +1,96 @@ +version: 0.2 + +env: + variables: + OS_DISTRIBUTION: ubuntu + EXECUTABLE: "/usr/local/bin/aws_lambda_ric" + TEST_NAME: "aws-lambda-ruby-ric-ubuntu-test" +batch: + build-matrix: + static: + ignore-failure: false + env: + type: LINUX_CONTAINER + privileged-mode: true + dynamic: + env: + variables: + DISTRO_VERSION: + - "20.04" + - "18.04" + RUNTIME_VERSION: + - "2.7" + - "2.6" + - "2.5" +phases: + pre_build: + commands: + - export IMAGE_TAG="ruby-${OS_DISTRIBUTION}-${DISTRO_VERSION}:${RUNTIME_VERSION}" + - echo "Extracting and including the Runtime Interface Emulator" + - SCRATCH_DIR=".scratch" + - mkdir "${SCRATCH_DIR}" + - tar -xvf test/integration/resources/aws-lambda-rie.tar.gz --directory "${SCRATCH_DIR}" + - > + cp "test/integration/docker/Dockerfile.echo.${OS_DISTRIBUTION}" \ + "${SCRATCH_DIR}/Dockerfile.echo.${OS_DISTRIBUTION}.tmp" + - > + echo "RUN apt-get install -y curl" >> \ + "${SCRATCH_DIR}/Dockerfile.echo.${OS_DISTRIBUTION}.tmp" + - > + echo "COPY ${SCRATCH_DIR}/aws-lambda-rie /usr/bin/aws-lambda-rie" >> \ + "${SCRATCH_DIR}/Dockerfile.echo.${OS_DISTRIBUTION}.tmp" + - echo "Building image ${IMAGE_TAG}" + - > + docker build . \ + -f "${SCRATCH_DIR}/Dockerfile.echo.${OS_DISTRIBUTION}.tmp" \ + -t "${IMAGE_TAG}" \ + --build-arg RUNTIME_VERSION="${RUNTIME_VERSION}" \ + --build-arg DISTRO_VERSION="${DISTRO_VERSION}" + build: + commands: + - set -x + - echo "Running Image ${IMAGE_TAG}" + - docker network create "${TEST_NAME}-network" + - > + docker run \ + --detach \ + --env "EXECUTABLE=${EXECUTABLE}" \ + --name "${TEST_NAME}-app" \ + --network "${TEST_NAME}-network" \ + --entrypoint="" \ + "${IMAGE_TAG}" \ + sh -c '/usr/bin/aws-lambda-rie "${EXECUTABLE}" "app.App::Handler.process"' + - sleep 2 + - > + docker run \ + --name "${TEST_NAME}-tester" \ + --env "TARGET=${TEST_NAME}-app" \ + --network "${TEST_NAME}-network" \ + --entrypoint="" \ + "${IMAGE_TAG}" \ + sh -c 'curl -X POST "http://${TARGET}:8080/2015-03-31/functions/function/invocations" -d "{}" --max-time 10' + - actual="$(docker logs --tail 1 "${TEST_NAME}-tester" | xargs)" + - expected='success' + - | + echo "Response: ${actual}" + if [[ "$actual" != "$expected" ]]; then + echo "fail! runtime: $RUNTIME - expected output $expected - got $actual" + echo "---------Container Logs: ${TEST_NAME}-app----------" + echo + docker logs "${TEST_NAME}-app" + echo + echo "---------------------------------------------------" + echo "--------Container Logs: ${TEST_NAME}-tester--------" + echo + docker logs "${TEST_NAME}-tester" + echo + echo "---------------------------------------------------" + exit -1 + fi + finally: + - echo "Cleaning up..." + - docker stop "${TEST_NAME}-app" || true + - docker rm --force "${TEST_NAME}-app" || true + - docker stop "${TEST_NAME}-tester" || true + - docker rm --force "${TEST_NAME}-tester" || true + - docker network rm "${TEST_NAME}-network" || true diff --git a/test/integration/docker-compose.template.yml b/test/integration/docker-compose.template.yml new file mode 100644 index 0000000..38db376 --- /dev/null +++ b/test/integration/docker-compose.template.yml @@ -0,0 +1,30 @@ +version: '3.3' +services: + function: + build: + context: . + args: + RUNTIME_VERSION: "${runtime_version}" + DISTRO_VERSION: "${distro_version}" + dockerfile: ./docker/Dockerfile.echo.${DISTRO} + environment: + - AWS_LAMBDA_RUNTIME_API=runtime:9001 + + runtime: + build: + context: . + dockerfile: ../docker-helpers/Dockerfile.runtime + + invoker: + build: + context: . + dockerfile: ../docker-helpers/Dockerfile.aws-cli + entrypoint: [ + aws, lambda, invoke, + --endpoint, http://runtime:9001, + --no-sign-request, + --region, us-west-2, + --function-name, ignored, + --payload, '{ "name": "Lambda" }', + /dev/stdout + ] diff --git a/test/integration/docker/Dockerfile.echo.alpine b/test/integration/docker/Dockerfile.echo.alpine new file mode 100644 index 0000000..2ec437f --- /dev/null +++ b/test/integration/docker/Dockerfile.echo.alpine @@ -0,0 +1,39 @@ +# Define global args +ARG RUNTIME_VERSION +ARG DISTRO_VERSION + +# Grab a fresh copy of the image and install ruby and build the runtime interface client gem +FROM ruby:${RUNTIME_VERSION}-alpine${DISTRO_VERSION} AS build-image + +RUN gem install bundler +RUN apk add --no-cache git + +ARG RIC_BUILD_DIR="/build" +# Create directory to build the Runtime Interface Client gem +RUN mkdir -p ${RIC_BUILD_DIR} + +WORKDIR ${RIC_BUILD_DIR} + +COPY . . +RUN rake build + + +# Grab a fresh copy of the Ruby image +FROM ruby:${RUNTIME_VERSION}-alpine${DISTRO_VERSION} + +# Copy the Runtime Interface Client gem and install it +ARG RIC_BUILD_DIR="/build" +COPY --from=build-image ${RIC_BUILD_DIR}/pkg/aws_lambda_ric*.gem aws_lambda_ric.gem +RUN gem install aws_lambda_ric.gem + +ARG FUNCTION_DIR="/function" + +RUN mkdir -p ${FUNCTION_DIR} +# Copy function code +COPY test/integration/test-handlers/echo/* ${FUNCTION_DIR} +# Set working directory to function root directory +WORKDIR ${FUNCTION_DIR} + +ENTRYPOINT ["aws_lambda_ric"] +CMD ["app.App::Handler.process"] + diff --git a/test/integration/docker/Dockerfile.echo.amazonlinux b/test/integration/docker/Dockerfile.echo.amazonlinux new file mode 100644 index 0000000..3232867 --- /dev/null +++ b/test/integration/docker/Dockerfile.echo.amazonlinux @@ -0,0 +1,62 @@ +# Define global args +ARG DISTRO_VERSION + +# Grab a fresh copy of the image and install ruby and build the runtime interface client gem +FROM amazonlinux:${DISTRO_VERSION} AS build-image + +ARG RUNTIME_VERSION + +RUN yum update -y && \ + yum install -y git-core zlib zlib-devel gcc-c++ patch readline readline-devel libffi-devel openssl-devel make bzip2 autoconf automake libtool bison curl sqlite-devel + +RUN rm -rf /root/.rbenv/ +RUN git clone git://github.com/sstephenson/rbenv.git /root/.rbenv +ENV PATH="/root/.rbenv/bin:$PATH" +RUN echo 'eval "$(rbenv init -)"' >> /root/.bashrc +RUN git clone git://github.com/sstephenson/ruby-build.git /root/.rbenv/plugins/ruby-build +RUN source /root/.bashrc +RUN RUNTIME_LATEST_VERSION=$(rbenv install -l | grep -oE $(echo "^${RUNTIME_VERSION}\.[0-9]+" | sed "s/\\./\\\./1")) && \ + rbenv install -v ${RUNTIME_LATEST_VERSION} && \ + rbenv global ${RUNTIME_LATEST_VERSION} && \ + cp /root/.rbenv/versions/${RUNTIME_LATEST_VERSION}/bin/gem /usr/local/bin/gem && \ + cp /root/.rbenv/versions/${RUNTIME_LATEST_VERSION}/bin/rake /usr/local/bin/rake && \ + gem install bundler + +ARG RIC_BUILD_DIR="/build" +# Create directory to build the Runtime Interface Client gem +RUN mkdir -p ${RIC_BUILD_DIR} + +WORKDIR ${RIC_BUILD_DIR} +COPY . . +RUN rake build + + +# Grab a fresh copy of the Ruby image +FROM amazonlinux:${DISTRO_VERSION} + +ARG RUNTIME_VERSION + +# Copy ruby from the build-image +COPY --from=build-image /root/.rbenv /root/.rbenv + +ENV PATH="/root/.rbenv/bin:$PATH" + +# Copy the Runtime Interface Client gem and install it +ARG RIC_BUILD_DIR="/build" +COPY --from=build-image ${RIC_BUILD_DIR}/pkg/aws_lambda_ric*.gem aws_lambda_ric.gem +RUN RUNTIME_LATEST_VERSION=$(rbenv install -l | grep -oE $(echo "^${RUNTIME_VERSION}\.[0-9]+$" | sed "s/\\./\\\./1")) && \ + cp /root/.rbenv/versions/${RUNTIME_LATEST_VERSION}/bin/gem /usr/local/bin/gem && \ + gem install aws_lambda_ric.gem && \ + cp /root/.rbenv/versions/${RUNTIME_LATEST_VERSION}/bin/aws_lambda_ric /usr/local/bin/aws_lambda_ric + +ARG FUNCTION_DIR="/function" + +RUN mkdir -p ${FUNCTION_DIR} +# Copy function code +COPY test/integration/test-handlers/echo/* ${FUNCTION_DIR} +# Set working directory to function root directory +WORKDIR ${FUNCTION_DIR} + +ENTRYPOINT ["aws_lambda_ric"] +CMD ["app.App::Handler.process"] + diff --git a/test/integration/docker/Dockerfile.echo.centos b/test/integration/docker/Dockerfile.echo.centos new file mode 100644 index 0000000..597ad07 --- /dev/null +++ b/test/integration/docker/Dockerfile.echo.centos @@ -0,0 +1,62 @@ +# Define global args +ARG DISTRO_VERSION + +# Grab a fresh copy of the image and install ruby and build the runtime interface client gem +FROM centos:${DISTRO_VERSION} AS build-image + +ARG RUNTIME_VERSION + +RUN yum update -y && \ + yum install -y git-core zlib zlib-devel gcc-c++ patch readline readline-devel libffi-devel openssl-devel make bzip2 autoconf automake libtool bison curl sqlite-devel + +RUN rm -rf /root/.rbenv/ +RUN git clone git://github.com/sstephenson/rbenv.git /root/.rbenv +ENV PATH="/root/.rbenv/bin:$PATH" +RUN echo 'eval "$(rbenv init -)"' >> /root/.bashrc +RUN git clone git://github.com/sstephenson/ruby-build.git /root/.rbenv/plugins/ruby-build +RUN source /root/.bashrc +RUN RUNTIME_LATEST_VERSION=$(rbenv install -l | grep -oE $(echo "^${RUNTIME_VERSION}\.[0-9]+$" | sed "s/\\./\\\./1")) && \ + rbenv install -v ${RUNTIME_LATEST_VERSION} && \ + rbenv global ${RUNTIME_LATEST_VERSION} && \ + cp /root/.rbenv/versions/${RUNTIME_LATEST_VERSION}/bin/gem /usr/local/bin/gem && \ + cp /root/.rbenv/versions/${RUNTIME_LATEST_VERSION}/bin/rake /usr/local/bin/rake && \ + gem install bundler + +ARG RIC_BUILD_DIR="/build" +# Create directory to build the Runtime Interface Client gem +RUN mkdir -p ${RIC_BUILD_DIR} + +WORKDIR ${RIC_BUILD_DIR} +COPY . . +RUN rake build + + +# Grab a fresh copy of the Ruby image +FROM centos:${DISTRO_VERSION} + +ARG RUNTIME_VERSION + +# Copy ruby from the build-image +COPY --from=build-image /root/.rbenv /root/.rbenv + +ENV PATH="/root/.rbenv/bin:$PATH" + +# Copy the Runtime Interface Client gem and install it +ARG RIC_BUILD_DIR="/build" +COPY --from=build-image ${RIC_BUILD_DIR}/pkg/aws_lambda_ric*.gem aws_lambda_ric.gem +RUN RUNTIME_LATEST_VERSION=$(rbenv install -l | grep -oE $(echo "^${RUNTIME_VERSION}\.[0-9]+" | sed "s/\\./\\\./1")) && \ + cp /root/.rbenv/versions/${RUNTIME_LATEST_VERSION}/bin/gem /usr/local/bin/gem && \ + gem install aws_lambda_ric.gem && \ + cp /root/.rbenv/versions/${RUNTIME_LATEST_VERSION}/bin/aws_lambda_ric /usr/local/bin/aws_lambda_ric + +ARG FUNCTION_DIR="/function" + +RUN mkdir -p ${FUNCTION_DIR} +# Copy function code +COPY test/integration/test-handlers/echo/* ${FUNCTION_DIR} +# Set working directory to function root directory +WORKDIR ${FUNCTION_DIR} + +ENTRYPOINT ["aws_lambda_ric"] +CMD ["app.App::Handler.process"] + diff --git a/test/integration/docker/Dockerfile.echo.debian b/test/integration/docker/Dockerfile.echo.debian new file mode 100644 index 0000000..40bfed4 --- /dev/null +++ b/test/integration/docker/Dockerfile.echo.debian @@ -0,0 +1,40 @@ +# Define global args +ARG RUNTIME_VERSION +ARG DISTRO_VERSION + +# Grab a fresh copy of the image and install ruby and build the runtime interface client gem +FROM ruby:${RUNTIME_VERSION}-${DISTRO_VERSION} AS build-image + +RUN gem install bundler +RUN apt -y update && \ + apt -y install git + +ARG RIC_BUILD_DIR="/build" +# Create directory to build the Runtime Interface Client gem +RUN mkdir -p ${RIC_BUILD_DIR} + +WORKDIR ${RIC_BUILD_DIR} + +COPY . . +RUN rake build + + +# Grab a fresh copy of the Ruby image +FROM ruby:${RUNTIME_VERSION}-${DISTRO_VERSION} + +# Copy the Runtime Interface Client gem and install it +ARG RIC_BUILD_DIR="/build" +COPY --from=build-image ${RIC_BUILD_DIR}/pkg/aws_lambda_ric*.gem aws_lambda_ric.gem +RUN gem install aws_lambda_ric.gem + +ARG FUNCTION_DIR="/function" + +RUN mkdir -p ${FUNCTION_DIR} +# Copy function code +COPY test/integration/test-handlers/echo/* ${FUNCTION_DIR} +# Set working directory to function root directory +WORKDIR ${FUNCTION_DIR} + +ENTRYPOINT ["aws_lambda_ric"] +CMD ["app.App::Handler.process"] + diff --git a/test/integration/docker/Dockerfile.echo.ubuntu b/test/integration/docker/Dockerfile.echo.ubuntu new file mode 100644 index 0000000..483d754 --- /dev/null +++ b/test/integration/docker/Dockerfile.echo.ubuntu @@ -0,0 +1,70 @@ +# Define global args +ARG DISTRO_VERSION + +# Grab a fresh copy of the image and install ruby and build the runtime interface client gem +FROM ubuntu:${DISTRO_VERSION} AS build-image + +ENV DEBIAN_FRONTEND=noninteractive + +ARG RUNTIME_VERSION + +RUN apt-get update -y && \ + apt-get install -y git-core curl zlib1g-dev build-essential libssl-dev libreadline-dev libyaml-dev libsqlite3-dev sqlite3 libxml2-dev libxslt1-dev libcurl4-openssl-dev libffi-dev libtool + + +RUN rm -rf /root/.rbenv/ +RUN git clone git://github.com/sstephenson/rbenv.git /root/.rbenv +ENV PATH="/root/.rbenv/bin:$PATH" +RUN echo 'eval "$(rbenv init -)"' >> /root/.bashrc +RUN git clone git://github.com/sstephenson/ruby-build.git /root/.rbenv/plugins/ruby-build +RUN bash /root/.bashrc + +RUN RUNTIME_LATEST_VERSION=$(rbenv install -l | grep -oE $(echo "^${RUNTIME_VERSION}\.[0-9]+" | sed "s/\\./\\\./1")) && \ + rbenv install -v ${RUNTIME_LATEST_VERSION} && \ + rbenv global ${RUNTIME_LATEST_VERSION} && \ + cp /root/.rbenv/versions/${RUNTIME_LATEST_VERSION}/bin/gem /usr/local/bin/gem && \ + cp /root/.rbenv/versions/${RUNTIME_LATEST_VERSION}/bin/rake /usr/local/bin/rake && \ + gem install bundler + +ARG RIC_BUILD_DIR="/build" +# Create directory to build the Runtime Interface Client gem +RUN mkdir -p ${RIC_BUILD_DIR} + +WORKDIR ${RIC_BUILD_DIR} + +COPY . . +RUN rake build + + +# Grab a fresh copy of the Ruby image +FROM ubuntu:${DISTRO_VERSION} + +# Get dependencies for the Runtime Interface Client +RUN apt-get update -y && \ + apt-get install -y libyaml-dev libssl-dev + +ARG RUNTIME_VERSION + +# Copy ruby from the build-image +COPY --from=build-image /root/.rbenv /root/.rbenv + +ENV PATH="/root/.rbenv/bin:$PATH" + +# Copy the Runtime Interface Client gem and install it +ARG RIC_BUILD_DIR="/build" +COPY --from=build-image ${RIC_BUILD_DIR}/pkg/aws_lambda_ric*.gem aws_lambda_ric.gem +RUN RUNTIME_LATEST_VERSION=$(rbenv install -l | grep -oE $(echo "^${RUNTIME_VERSION}\.[0-9]+$" | sed "s/\\./\\\./1")) && \ + cp /root/.rbenv/versions/${RUNTIME_LATEST_VERSION}/bin/gem /usr/local/bin/gem && \ + gem install aws_lambda_ric.gem && \ + cp /root/.rbenv/versions/${RUNTIME_LATEST_VERSION}/bin/aws_lambda_ric /usr/local/bin/aws_lambda_ric +ARG FUNCTION_DIR="/function" + +RUN mkdir -p ${FUNCTION_DIR} +# Copy function code +COPY test/integration/test-handlers/echo/* ${FUNCTION_DIR} +# Set working directory to function root directory +WORKDIR ${FUNCTION_DIR} + +ENTRYPOINT ["aws_lambda_ric"] +CMD ["app.App::Handler.process"] + diff --git a/test/integration/resources/aws-lambda-rie.tar.gz b/test/integration/resources/aws-lambda-rie.tar.gz new file mode 100644 index 0000000..feda16d Binary files /dev/null and b/test/integration/resources/aws-lambda-rie.tar.gz differ diff --git a/test/integration/test-handlers/echo/app.rb b/test/integration/test-handlers/echo/app.rb new file mode 100644 index 0000000..f11c4d3 --- /dev/null +++ b/test/integration/test-handlers/echo/app.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module App + class Handler + def self.process(event:, context:) + 'success' + end + end +end diff --git a/test/run_tests.rb b/test/run_tests.rb new file mode 100644 index 0000000..0a1fb3d --- /dev/null +++ b/test/run_tests.rb @@ -0,0 +1,10 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +# run tests in specified directory and its sub-directories +if ARGV.length != 1 + puts 'We need exactly one argument, the directory name containing tests to run.' + exit +end + +test_directory = File.join(File.dirname(File.absolute_path(__FILE__)), ARGV[0]) +Dir["#{test_directory}/**/*_test.rb"].each { |file| require_relative file } diff --git a/test/unit/harness-suite.json b/test/unit/harness-suite.json new file mode 100644 index 0000000..bd004c2 --- /dev/null +++ b/test/unit/harness-suite.json @@ -0,0 +1,417 @@ +{ + "tests": [ + { + "name": "test_echo", + "handler": "core.ping", + "request": { + "msg": "message" + }, + "assertion": + { + "response": { "msg": "pong[message]" } + } + }, + { + "name": "test_string_payload", + "handler": "core.str_ping", + "request": "message", + "assertion": + { + "response": { "msg": "pong[message]" } + } + }, + { + "name": "test_module_echo", + "handler": "core.HandlerClass.ping", + "request": { + "msg": "MyMessage" + }, + "assertion": + { + "response": "Module Message: 'MyMessage'" + } + }, + { + "name": "test_deep_module_echo", + "handler": "core.DeepModule::Handler.ping", + "request": { + "msg": "MyMessage" + }, + "assertion": + { + "response": "Deep Module Message: 'MyMessage'" + } + }, + { + "name": "test_error", + "handler": "core.broken", + "request": { + "msg": "message" + }, + "assertion": + { + "errorType": "Function" + } + }, + { + "name": "test_string", + "handler": "core.string", + "request": { + "msg": "MyMessage" + }, + "assertion": + { + "response": "Message: 'MyMessage'" + } + }, + { + "name": "test_unicode", + "handler": "unicode.hello", + "request": "", + "assertion": + { + "response": { "hello": "今日はラムダ!" } + } + }, + { + "name": "test_underscore_handler", + "handler": "underscore_file.underscore_method", + "request": "", + "assertion": + { + "response": "Success!" + } + }, + { + "name": "test_dependencies", + "handler": "dependencies.get_tomorrow", + "request": { + "input_time": "2018-10-08 11:34:20 +0100" + }, + "assertion": + { + "response": {"tomorrow": "2018-10-09T11:34:20.000+01:00" } + } + }, + { + "name": "test_dynamic_libs_are_available", + "handler": "dependencies.find_dynamic_libs", + "request": "", + "assertion": + { + "response": "ok" + } + }, + { + "name": "test_echo_env_variable", + "handler": "environment.echo", + "environmentVariables": { + "TEST_ENV_VARIABLE": "Golden Tate made that catch." + }, + "request": { + "varname": "TEST_ENV_VARIABLE" + }, + "assertion": + { + "response": "Golden Tate made that catch." + } + }, + { + "name": "test_env_runtime_api_is_not_deleted", + "handler": "environment.env_var_is_present", + "environmentVariables": { + "AWS_LAMBDA_RUNTIME_API": "127.0.0.1:9001" + }, + "request": { + "varname": "AWS_LAMBDA_RUNTIME_API" + }, + "assertion": + { + "response": "true" + } + }, + { + "name": "test_env_telmetry_log_fd_deleted", + "handler": "environment.env_var_is_present", + "environmentVariables": { + "_LAMBDA_TELEMETRY_LOG_FD": "test/unit/resources/fd/test_fd" + }, + "request": { + "varname": "_LAMBDA_TELEMETRY_LOG_FD" + }, + "assertion": + { + "response": "false" + } + }, + { + "name": "test_put_uses_telemetry_log_fd", + "handler": "log.put_messages_and_read_fd", + "environmentVariables": { + "_LAMBDA_TELEMETRY_LOG_FD": "test/unit/resources/fd/test_fd" + }, + "request": { + "messages": ["Single frame\n even if there are multiple lines\nthird line"], + "fd_path": "test/unit/resources/fd/test_fd" + }, + "assertion": + { + "response": "277413888157Single frame\n even if there are multiple lines\nthird line" + } + }, + { + "name": "test_put_frozen_string", + "handler": "log.put_frozen_string", + "environmentVariables": { + "_LAMBDA_TELEMETRY_LOG_FD": "test/unit/resources/fd/test_fd" + }, + "request": { + "messages": ["ELSA"], + "fd_path": "test/unit/resources/fd/test_fd" + }, + "assertion": + { + "response": "ok" + } + }, + { + "name": "test_put_uses_telemetry_log_fd_multiple_frames", + "handler": "log.put_messages_and_read_fd", + "environmentVariables": { + "_LAMBDA_TELEMETRY_LOG_FD": "test/unit/resources/fd/test_fd" + }, + "request": { + "messages": ["First message first line\n second line", "Second message first line\n second line"], + "fd_path": "test/unit/resources/fd/test_fd" + }, + "assertion": + { + "response": "277413888137First message first line\n second line277413888138Second message first line\n second line" + } + }, + { + "name": "test_logger_uses_telemetry_log_fd_when_logdev_is_stdout", + "handler": "log.log_to_stdout_and_read_fd", + "environmentVariables": { + "_LAMBDA_TELEMETRY_LOG_FD": "test/unit/resources/fd/test_fd" + }, + "request": { + "messages": ["Single frame\n even if there are multiple lines\nthird line"], + "fd_path": "test/unit/resources/fd/test_fd" + }, + "assertion": + { + "transform": "\"277413888114[0-9]I, \\[[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T(2[0-3]|[01][0-9]):[0-5][0-9]:[0-9]{2}.[0-9]{6} #[0-9]*\\] INFO ({){0,1}[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}(}){0,1} -- : Single frame\\\\n even if there are multiple lines\\\\nthird line\"", + "response": true + } + }, + { + "name": "test_logger_uses_telemetry_log_fd_when_logdev_is_stderr", + "handler": "log.log_to_stderr_and_read_fd", + "environmentVariables": { + "_LAMBDA_TELEMETRY_LOG_FD": "test/unit/resources/fd/test_fd" + }, + "request": { + "messages": ["Single frame\n even if there are multiple lines\nthird line"], + "fd_path": "test/unit/resources/fd/test_fd" + }, + "assertion": + { + "transform": "\"277413888114[0-9]E, \\[[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T(2[0-3]|[01][0-9]):[0-5][0-9]:[0-9]{2}.[0-9]{6} #[0-9]*\\] ERROR ({){0,1}[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}(}){0,1} -- : Single frame\\\\n even if there are multiple lines\\\\nthird line\"", + "response": true + } + }, + { + "name": "test_arguments_are_flattened_and_separated_by_newline_when_logging", + "handler": "log.put_messages_and_read_fd", + "environmentVariables": { + "_LAMBDA_TELEMETRY_LOG_FD": "test/unit/resources/fd/test_fd" + }, + "request": { + "messages": [["first_level","an_other_first_level", ["second_level"]]], + "fd_path": "test/unit/resources/fd/test_fd" + }, + "assertion": + { + "response": "277413888145first_level\nan_other_first_level\nsecond_level" + } + }, + { + "name": "test_fallback_to_stdout_silently_when_can_not_open_telemetry_log_fd", + "handler": "log.put_message", + "environmentVariables": { + "_LAMBDA_TELEMETRY_LOG_FD": "/unreachable/test_fd" + }, + "request": { + "messages": ["It should fall back silently"] + }, + "assertion": + { + "response": "ok" + } + }, + { + "name": "test_invalid_handler_definition_short", + "handler": "invalid", + "request": {}, + "assertion": + { + "errorType": "Init" + } + }, + { + "name": "test_invalid_handler_definition_long", + "handler": "invalid.because.too.long", + "request": {}, + "assertion": + { + "errorType": "Init" + } + }, + { + "name": "test_invalid_handler_file", + "handler": "doesnt_exist.doesnt_matter", + "request": {}, + "assertion": + { + "errorType": "Init" + } + }, + { + "name": "test_invalid_handler_method", + "handler": "pandoras_box.sir_not_appearing_in_this_file", + "request": {}, + "assertion": + { + "errorType": "Function" + } + }, + { + "name": "test_invalid_handler_class", + "handler": "pandoras_box.Does::Not::Exist.handler", + "request": {}, + "assertion": + { + "errorType": "Function" + } + }, + { + "name": "test_invalid_handler_method_in_class", + "handler": "pandoras_box.PandoraTest.handler", + "request": {}, + "assertion": + { + "errorType": "Function" + } + }, + { + "name": "test_invalid_dependency", + "handler": "bad_dependency.handler", + "request": {}, + "assertion": + { + "errorType": "Init" + } + }, + { + "name": "test_stack_overflow", + "handler": "pandoras_box.stack_overflow", + "request": {}, + "assertion": + { + "errorType": "Function" + } + }, + { + "name": "test_process_exit", + "handler": "pandoras_box.hard_exit", + "request": {}, + "assertion": + { + "errorType": "Function" + } + }, + { + "name": "test_curl", + "handler": "core.curl", + "request": + { + "url": "http://example.org" + }, + "assertion": + { + "response": { "success": true } + } + }, + { + "name": "test_ctx_cognito_pool_id", + "handler": "ctx.get_cognito_pool_id", + "cognitoIdentity": { + "cognitoIdentityId": "4ab95ea510c14353a7f6da04489c43b8", + "cognitoIdentityPoolId": "35ab4794a79a4f23947d3e851d3d6578" + }, + "request": {}, + "assertion": + { + "response": { "cognito_pool_id": "35ab4794a79a4f23947d3e851d3d6578"} + } + }, + { + "name": "test_ctx_cognito_identity_id", + "handler": "ctx.get_cognito_identity_id", + "cognitoIdentity": { + "cognitoIdentityId": "4ab95ea510c14353a7f6da04489c43b8", + "cognitoIdentityPoolId": "35ab4794a79a4f23947d3e851d3d6578" + }, + "request": {}, + "assertion": + { + "response": { "cognito_identity_id": "4ab95ea510c14353a7f6da04489c43b8"} + } + }, + { + "name": "echo_client_context", + "handler": "ctx.echo_context", + "clientContext": + { + "custom": { + "a": "1", + "b": "1" + }, + "env": { + "make": "samsung", + "platform": "android" + }, + "client": { + "installation_id": "id1234", + "app_title": "title", + "app_version_name": "alpha", + "app_version_code": "9.9", + "app_package_name": "pname" + } + }, + "request": "yo", + "assertion": + { + "response": + { + "custom": { + "a": "1", + "b": "1" + }, + "env": { + "make": "samsung", + "platform": "android" + }, + "client": { + "installation_id": "id1234", + "app_title": "title", + "app_version_name": "alpha", + "app_version_code": "9.9", + "app_package_name": "pname" + } + } + } + } + ] +} diff --git a/test/unit/harness_test.rb b/test/unit/harness_test.rb new file mode 100644 index 0000000..1e61a68 --- /dev/null +++ b/test/unit/harness_test.rb @@ -0,0 +1,115 @@ +require './lib/aws_lambda_ric/lambda_handler' +require './lib/aws_lambda_ric/lambda_context' +require './lib/aws_lambda_ric/aws_lambda_marshaller' +require './lib/aws_lambda_ric/telemetry_log_sink' +require './lib/aws_lambda_ric/bootstrap' +require 'securerandom' +require 'minitest/autorun' +require 'logger' + + +class HarnessTests < Minitest::Test + test_suite = open('test/unit/harness-suite.json') + json = test_suite.read + + parsed_json = JSON.parse(json) + parsed_json['tests'].each do |test| + test_name = test["name"] + if !test_name.start_with?('test') + test_name = "test_#{test_name}" + end + + handler = test['handler'] + + event = test['request'] + + assertion = test['assertion'] + + env_vars = test['environmentVariables'] + + context = {} + if test.key?('cognitoIdentity') + context['Lambda-Runtime-Cognito-Identity'] = test['cognitoIdentity'].to_json + end + if test.key?('clientContext') + context['Lambda-Runtime-Client-Context'] = test['clientContext'].to_json + end + if test.key?('xray') + context['Lambda-Runtime-Trace-Id'] = test['xray']['traceId'] + end + + + + define_method(test_name) do + # Logger uses request id + $_global_aws_request_id = SecureRandom.uuid + + # Set up env variables for the test + if env_vars + env_vars.each do |env_var, value| + ENV[env_var] = value + end + end + + # Set up Telemetry Log fd + Bootstrap::bootstrap_telemetry_log_sink('') + + context = LambdaContext.new(context) + + # If the test is expecting an error + if assertion.key?('errorType') + begin + lambda_handler = LambdaHandler.new(env_handler: "resources/runtime_handlers/#{handler}") + require_relative lambda_handler.handler_file_name + handler_response, content_type = lambda_handler.call_handler( + request: event, + context: context + ) + rescue LambdaErrors::LambdaError => e + assert_equal(assertion['errorType'], e.runtime_error_type) + rescue StandardError => e + assert_equal e.class.to_s, assertion['errorType'][/<(.*?)>/, 1] + rescue ScriptError => e + assert_equal e.class.to_s, assertion['errorType'][/<(.*?)>/, 1] + end + # If the test is expecting an assertion + else + lambda_handler = LambdaHandler.new(env_handler: "resources/runtime_handlers/#{handler}") + require_relative lambda_handler.handler_file_name + handler_response, content_type = lambda_handler.call_handler( + request: event, + context: context + ) + + if assertion.key?('transform') + transform = assertion['transform'] + assert_equal assertion['response'], handler_response.to_s.match?(/#{transform}/) + else + assert_equal assertion['response'].to_json, handler_response + end + end + + # Revert changes made for tests that use Telemetry Log fd + if ENV.key?('_LAMBDA_TELEMETRY_LOG_FD') + Logger.class_eval do + alias_method :initialize, :orig_initialize + end + + Kernel.module_eval do + def puts(*args) + $stdout.puts(*args) + end + end + + File.open('test/unit/resources/fd/test_fd', 'w') {|file| file.truncate(0) } + end + + # Delete env vars set for the test + if env_vars + env_vars.each do |env_var, value| + ENV.delete(env_var) + end + end + end + end +end \ No newline at end of file diff --git a/test/unit/lambda_log_formatter_test.rb b/test/unit/lambda_log_formatter_test.rb new file mode 100644 index 0000000..0af92d2 --- /dev/null +++ b/test/unit/lambda_log_formatter_test.rb @@ -0,0 +1,21 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +require_relative '../../lib/aws_lambda_ric/lambda_log_formatter' +require 'logger' +require 'securerandom' +require 'minitest/autorun' + +class LambdaLogFormatterTest < Minitest::Test + + def test_formatter + $_global_aws_request_id = SecureRandom.uuid + time = Time.now + progname = 'test_progname' + msg = 'log_message' + under_test = LambdaLogFormatter.new + + actual = under_test.call('INFO', time, progname, msg) + + assert_equal "I, [#{time.strftime("%Y-%m-%dT%H:%M:%S.%6N")} ##{$$}] INFO #{$_global_aws_request_id} -- #{progname}: #{msg}", actual + end +end diff --git a/test/unit/lambda_server_test.rb b/test/unit/lambda_server_test.rb new file mode 100644 index 0000000..1ba3926 --- /dev/null +++ b/test/unit/lambda_server_test.rb @@ -0,0 +1,53 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +require_relative '../../lib/aws_lambda_ric/lambda_errors' +require_relative '../../lib/aws_lambda_ric/lambda_server' +require 'net/http' +require 'minitest/autorun' + +class LambdaServerTest < Minitest::Test + def setup + @server_address = '127.0.0.1:9001' + @request_id = 'test_id' + @error = LambdaErrors::LambdaRuntimeError.new(StandardError.new('User error, replace user')) + @error_uri = URI("http://#{@server_address}/2018-06-01/runtime/invocation/#{@request_id}/error") + @under_test = LambdaServer.new(@server_address) + end + + def test_post_invocation_error_with_large_xray_cause + large_xray_cause = ('a' * 1024 * 1024)[0..-2] + headers = {'Lambda-Runtime-Function-Error-Type' => @error.runtime_error_type, + 'Lambda-Runtime-Function-XRay-Error-Cause' => large_xray_cause} + post_mock = Minitest::Mock.new + post_mock.expect :call, nil, [@error_uri, @error.to_lambda_response.to_json, headers] + + Net::HTTP.stub(:post, post_mock) do + @under_test.send_error_response( + request_id: @request_id, + error_object: @error.to_lambda_response, + error: @error, + xray_cause: large_xray_cause + ) + end + + assert_mock post_mock + end + + def test_post_invocation_error_with_too_large_xray_cause + too_large_xray_cause = 'a' * 1024 * 1024 + headers = {'Lambda-Runtime-Function-Error-Type' => @error.runtime_error_type} + post_mock = Minitest::Mock.new + post_mock.expect :call, nil, [@error_uri, @error.to_lambda_response.to_json, headers] + + Net::HTTP.stub(:post, post_mock) do + @under_test.send_error_response( + request_id: @request_id, + error_object: @error.to_lambda_response, + error: @error, + xray_cause: too_large_xray_cause + ) + end + + assert_mock post_mock + end +end diff --git a/test/unit/resources/fd/test_fd b/test/unit/resources/fd/test_fd new file mode 100644 index 0000000..cc8c739 Binary files /dev/null and b/test/unit/resources/fd/test_fd differ diff --git a/test/unit/resources/runtime_handlers/Gemfile b/test/unit/resources/runtime_handlers/Gemfile new file mode 100644 index 0000000..cf4a19c --- /dev/null +++ b/test/unit/resources/runtime_handlers/Gemfile @@ -0,0 +1,3 @@ +source 'https://rubygems.org' + +gem 'activesupport', '~> 6.0.1' diff --git a/test/unit/resources/runtime_handlers/bad_dependency.rb b/test/unit/resources/runtime_handlers/bad_dependency.rb new file mode 100644 index 0000000..6e19a0d --- /dev/null +++ b/test/unit/resources/runtime_handlers/bad_dependency.rb @@ -0,0 +1,7 @@ +# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +require 'there_is_no_cow_file' + +def handler(_) + "This should never be reached." +end diff --git a/test/unit/resources/runtime_handlers/core.rb b/test/unit/resources/runtime_handlers/core.rb new file mode 100644 index 0000000..b21268b --- /dev/null +++ b/test/unit/resources/runtime_handlers/core.rb @@ -0,0 +1,55 @@ +# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +def ping(event:, context:) + resp = {} + if event.nil? + resp[:event_nil] = true + else + resp[:msg] = "pong[#{event["msg"]}]" + end + puts "Hello, loggers!" + resp +end + +def str_ping(event:, context:) + { msg: "pong[#{event}]" } +end + +def broken(_) + raise ArgumentError.new("My error message.") +end + +def string(event:, context:) + "Message: '#{event["msg"]}'" +end + +def curl(event:,context:) + resp = Net::HTTP.get(URI(event["url"])) + if resp.size > 0 + { success: true } + else + raise "Empty response!" + end +end + +def io(_) + StringIO.new("This is IO!") +end + +def execution_env(_) + { "AWS_EXECUTION_ENV" => ENV["AWS_EXECUTION_ENV"] } +end + +class HandlerClass + def self.ping(event:,context:) + "Module Message: '#{event["msg"]}'" + end +end + +module DeepModule + class Handler + def self.ping(event:,context:) + "Deep Module Message: '#{event["msg"]}'" + end + end +end diff --git a/test/unit/resources/runtime_handlers/ctx.rb b/test/unit/resources/runtime_handlers/ctx.rb new file mode 100644 index 0000000..d577572 --- /dev/null +++ b/test/unit/resources/runtime_handlers/ctx.rb @@ -0,0 +1,32 @@ +# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +def get_context(event:,context:) + { + function_name: context.function_name, + deadline_ns: context.deadline_ns, + aws_request_id: context.aws_request_id, + invoked_function_arn: context.invoked_function_arn, + log_group_name: context.log_group_name, + log_stream_name: context.log_stream_name, + memory_limit_in_mb: context.memory_limit_in_mb, + function_version: context.function_version + } +end + +def get_cognito_pool_id(event:,context:) + { cognito_pool_id: context.identity["cognitoIdentityPoolId"]} +end + +def get_cognito_identity_id(event:,context:) + { cognito_identity_id: context.identity["cognitoIdentityId"] } +end + +def echo_context(event:,context:) + context.client_context +end + +def get_remaining_time_from_context(event:, context:) + before = context.get_remaining_time_in_millis() + sleep(event['sleepTimeSeconds']) + {elapsedTime: before - context.get_remaining_time_in_millis() } +end diff --git a/test/unit/resources/runtime_handlers/dependencies.rb b/test/unit/resources/runtime_handlers/dependencies.rb new file mode 100644 index 0000000..5953af1 --- /dev/null +++ b/test/unit/resources/runtime_handlers/dependencies.rb @@ -0,0 +1,16 @@ +# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +def get_tomorrow(event:, context:) + require 'active_support' + ActiveSupport.eager_load! + t = Time.parse(event['input_time']) + {tomorrow: t.next_day} +end + +def find_dynamic_libs(event:, context:) + shared_lib_dep = `ldd /var/lang/lib/ruby/2.7.0/x86_64-linux/*.so` + if shared_lib_dep.include? "not found" + return "Missing dependency" + end + 'ok' +end diff --git a/test/unit/resources/runtime_handlers/environment.rb b/test/unit/resources/runtime_handlers/environment.rb new file mode 100644 index 0000000..05e5a00 --- /dev/null +++ b/test/unit/resources/runtime_handlers/environment.rb @@ -0,0 +1,20 @@ +# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +def echo(event:, context:) + varname = event['varname'] + resp = ENV[varname] + if event['match'] + pattern = event['pattern'] + if resp.match(pattern) + {match: true} + else + {match: false} + end + else + resp + end +end + +def env_var_is_present(event:, context:) + ENV.key?(event['varname']).to_s +end diff --git a/test/unit/resources/runtime_handlers/extensions.rb b/test/unit/resources/runtime_handlers/extensions.rb new file mode 100644 index 0000000..7b7e040 --- /dev/null +++ b/test/unit/resources/runtime_handlers/extensions.rb @@ -0,0 +1,7 @@ +# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +def test_wrapper_executed(event:, context:) + File.open(File.expand_path('../', __FILE__) + '/hi_there') do |file| + return file.read + end +end diff --git a/test/unit/resources/runtime_handlers/index.rb b/test/unit/resources/runtime_handlers/index.rb new file mode 100644 index 0000000..8d6e4e8 --- /dev/null +++ b/test/unit/resources/runtime_handlers/index.rb @@ -0,0 +1,9 @@ +# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +require 'json' + +def get_suite(event:, context:) + file_name = event + '.json' + file = File.read(file_name) + return JSON.parse(file) +end diff --git a/test/unit/resources/runtime_handlers/log.rb b/test/unit/resources/runtime_handlers/log.rb new file mode 100644 index 0000000..5b7d255 --- /dev/null +++ b/test/unit/resources/runtime_handlers/log.rb @@ -0,0 +1,45 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +def put_message(event:, context:) + puts(event['messages']) + 'ok' +end + +def put_frozen_string(event:, context:) + puts(event['messages'].freeze) + 'ok' +end + +def put_messages_and_read_fd(event:, context:) + event['messages'].each { |m| puts(m) } + content = '' + read_fd(content, event) +end + +require 'logger' + +def log_to_stdout_and_read_fd(event:, context:) + logger = Logger.new($stdout) + event['messages'].each { |m| logger.info(m) } + read_fd('', event) +end + +def log_to_stderr_and_read_fd(event:, context:) + logger = Logger.new($stderr) + event['messages'].each { |m| logger.error(m) } + read_fd('', event) +end + +def read_fd(content, event) + File.open(event['fd_path'], 'rb') do |file| + until file.eof? + frame_type = file.read(4).unpack('L>')[0] + content << frame_type.to_s + length = file.read(4).unpack('L>')[0] + content << length.to_s + log = file.read(length) + content << log + end + end + content +end diff --git a/test/unit/resources/runtime_handlers/opensslcheck.rb b/test/unit/resources/runtime_handlers/opensslcheck.rb new file mode 100644 index 0000000..1c10fd5 --- /dev/null +++ b/test/unit/resources/runtime_handlers/opensslcheck.rb @@ -0,0 +1,7 @@ +# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +require 'openssl' + +def version(_) + OpenSSL::OPENSSL_VERSION +end diff --git a/test/unit/resources/runtime_handlers/pandoras_box.rb b/test/unit/resources/runtime_handlers/pandoras_box.rb new file mode 100644 index 0000000..33e9630 --- /dev/null +++ b/test/unit/resources/runtime_handlers/pandoras_box.rb @@ -0,0 +1,20 @@ +# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +def loop_a + loop_b +end + +def loop_b + loop_a +end + +def stack_overflow(_) + loop_a +end + +def hard_exit(_) + exit(42) +end + +class PandoraTest +end diff --git a/test/unit/resources/runtime_handlers/underscore_file.rb b/test/unit/resources/runtime_handlers/underscore_file.rb new file mode 100644 index 0000000..818f475 --- /dev/null +++ b/test/unit/resources/runtime_handlers/underscore_file.rb @@ -0,0 +1,5 @@ +# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +def underscore_method(_) + "Success!" +end diff --git a/test/unit/resources/runtime_handlers/unicode.rb b/test/unit/resources/runtime_handlers/unicode.rb new file mode 100644 index 0000000..7201ca7 --- /dev/null +++ b/test/unit/resources/runtime_handlers/unicode.rb @@ -0,0 +1,6 @@ +# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +# -*- coding: utf-8 -*- +def hello(_) + { hello: "今日はラムダ!" } +end diff --git a/test/unit/resources/runtime_handlers/user_input_marshaller.rb b/test/unit/resources/runtime_handlers/user_input_marshaller.rb new file mode 100644 index 0000000..c2ebd7a --- /dev/null +++ b/test/unit/resources/runtime_handlers/user_input_marshaller.rb @@ -0,0 +1,17 @@ +# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +module AwsLambda + class Marshaller + def self.marshall_request(raw) + event = JSON.parse(raw.body) + event['squared'] = event['numbers'].map do |n| + n * n + end + event + end + end +end + +def squared_input(event:,context:) + event['squared'] +end diff --git a/test/unit/resources/runtime_handlers/user_marshaller.rb b/test/unit/resources/runtime_handlers/user_marshaller.rb new file mode 100644 index 0000000..735ec67 --- /dev/null +++ b/test/unit/resources/runtime_handlers/user_marshaller.rb @@ -0,0 +1,16 @@ +# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +module AwsLambda + class Marshaller + def self.marshall_response(resp) + { + body: resp, + metadata: "Marshaller brought to you by Test Harness Industries." + }.to_json + end + end +end + +def add_metadata(_) + "Simple response." +end diff --git a/test/unit/telemetry_log_sink_test.rb b/test/unit/telemetry_log_sink_test.rb new file mode 100644 index 0000000..7e48328 --- /dev/null +++ b/test/unit/telemetry_log_sink_test.rb @@ -0,0 +1,56 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +# frozen_string_literal: true + +require_relative '../../lib/aws_lambda_ric/telemetry_log_sink' +require 'tempfile' +require 'minitest/autorun' +require 'test/unit/assertions' + +include Test::Unit::Assertions + +class TelemetryLogSinkTest < Minitest::Test + + def test_single_frame + log = "Single frame\n even if there are multiple lines\nthird line" + Tempfile.create do |file| + under_test = TelemetryLogSink.new(file: file) + + under_test.write(log) + file.rewind + + until file.eof? + frame = file.read(4).unpack1('L>') + assert_equal 0xa55a0001, frame + + length = file.read(4).unpack1('L>') + assert_equal log.bytesize, length + + content = file.read(length) + assert_equal log, content + end + end + end + + def test_multiple_frames + log_messages = ["First message first line\n second line", "Second message first line\n second line"] + Tempfile.create do |file| + under_test = TelemetryLogSink.new(file: file) + + log_messages.each { |log| under_test.write(log) } + file.rewind + + log_messages.each do |log| + frame = file.read(4).unpack1('L>') + assert_equal 0xa55a0001, frame + + length = file.read(4).unpack1('L>') + assert_equal log.bytesize, length + + content = file.read(length) + assert_equal log, content + end + assert_true file.eof? + end + end +end diff --git a/test/unit/xray_cause_test.rb b/test/unit/xray_cause_test.rb new file mode 100644 index 0000000..d0d7082 --- /dev/null +++ b/test/unit/xray_cause_test.rb @@ -0,0 +1,54 @@ +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +# frozen_string_literal: true + +require_relative '../../lib/aws_lambda_ric/xray_cause' +require_relative '../../lib/aws_lambda_ric/lambda_errors' +require 'minitest/autorun' +require 'test/unit' + +include Test::Unit::Assertions + +class XRayCauseTest < Minitest::Test + def test_xray_cause + msg = "Unit testing is what I am doing when I don't know what I am doing" + + begin + raise StandardError.new(msg) + rescue StandardError => e + under_test = XRayCause.new(LambdaErrors::LambdaError.new(e).to_lambda_response) + end + + assert_equal Dir.pwd, under_test.instance_variable_get(:@cause)[:working_directory] + assert_equal Gem.paths.path, under_test.instance_variable_get(:@cause)[:paths] + assert_equal 1, under_test.instance_variable_get(:@cause)[:exceptions].count + assert_equal msg, under_test.instance_variable_get(:@cause)[:exceptions][0][:message] + assert_equal 'Function', under_test.instance_variable_get(:@cause)[:exceptions][0][:type] + assert_true under_test.instance_variable_get(:@cause)[:exceptions][0][:stack].count.positive? + assert_equal __FILE__, under_test.instance_variable_get(:@cause)[:exceptions][0][:stack][0][:path] + assert_kind_of Integer, under_test.instance_variable_get(:@cause)[:exceptions][0][:stack][0][:line] + assert_equal __method__.to_s, under_test.instance_variable_get(:@cause)[:exceptions][0][:stack][0][:label] + end + + def test_stack_depth_maximum + msg = 'All code is guilty, until proven innocent' + begin + raise_lambda_err_with_deep_trace(XRayCause::MAX_DEPTH + 1, StandardError.new(msg)) + rescue LambdaErrors::LambdaError => e + under_test = XRayCause.new(e.to_lambda_response) + end + + assert_equal 1, under_test.instance_variable_get(:@cause)[:exceptions].count + assert_equal msg, under_test.instance_variable_get(:@cause)[:exceptions][0][:message] + assert_equal XRayCause::MAX_DEPTH, under_test.instance_variable_get(:@cause)[:exceptions][0][:stack].count + end + + def raise_lambda_err_with_deep_trace(depth, exc) + unless depth.positive? + raise LambdaErrors::LambdaError.new(exc) + end + raise LambdaErrors::LambdaRuntimeError.new(exc) + rescue LambdaErrors::LambdaRuntimeError => e + raise_lambda_err_with_deep_trace(depth - 1, e) + end +end